Merge branch 'main' into soe

This commit is contained in:
Fabian Dill 2021-11-12 23:47:34 +00:00 committed by GitHub
commit 2d55cf4bbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
177 changed files with 51848 additions and 6858 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
*_Spoiler.txt
*.bmbp
*.apbp
*.apm3
*.apmc
*.apz5
*.pyc

View File

@ -3,13 +3,16 @@ import logging
import asyncio
import urllib.parse
import sys
import os
import typing
import time
import websockets
import Utils
if __name__ == "__main__":
Utils.init_logging("TextClient")
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
from Utils import Version
@ -17,10 +20,8 @@ from worlds import network_data_package, AutoWorldRegister
logger = logging.getLogger("Client")
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
log_folder = Utils.local_path("logs")
os.makedirs(log_folder, exist_ok=True)
# without terminal we have to use gui mode
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
class ClientCommandProcessor(CommandProcessor):
@ -58,7 +59,7 @@ class ClientCommandProcessor(CommandProcessor):
"""List all missing location checks, from your local game state"""
if not self.ctx.game:
self.output("No game set, cannot determine missing checks.")
return
return False
count = 0
checked_count = 0
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
@ -270,6 +271,7 @@ class CommonContext():
logger.info(f"DeathLink: Received from {data['source']}")
async def send_death(self, death_text: str = ""):
logger.info("Sending death to your friends...")
self.last_death_link = time.time()
await self.send_msgs([{
"cmd": "Bounce", "tags": ["DeathLink"],
@ -517,19 +519,18 @@ async def console_loop(ctx: CommonContext):
logger.exception(e)
def init_logging(name: str):
if gui_enabled:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join(log_folder, f"{name}.txt"), filemode="w", force=True)
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, f"{name}.txt"), "w"))
def get_base_parser(description=None):
import argparse
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
if sys.stdout: # If terminal output exists, offer gui-less mode
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
return parser
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
init_logging("TextClient")
class TextContext(CommonContext):
tags = {"AP", "IgnoreGame"}
@ -581,15 +582,9 @@ if __name__ == '__main__':
if input_task:
input_task.cancel()
import argparse
import colorama
parser = argparse.ArgumentParser(description="Gameless Archipelago Client, for text interfaction.")
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
parser = get_base_parser(description="Gameless Archipelago Client, for text interfaction.")
args, rest = parser.parse_known_args()
colorama.init()

View File

@ -6,24 +6,24 @@ import string
import copy
import subprocess
import time
import random
import factorio_rcon
import colorama
import asyncio
from queue import Queue
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
init_logging
from MultiServer import mark_raw
import Utils
import random
if __name__ == "__main__":
Utils.init_logging("FactorioClient")
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
get_base_parser
from MultiServer import mark_raw
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from worlds.factorio import Factorio
init_logging("FactorioClient")
class FactorioCommandProcessor(ClientCommandProcessor):
ctx: FactorioContext
@ -353,17 +353,11 @@ class FactorioJSONtoTextParser(JSONtoTextParser):
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
args, rest = parser.parse_known_args()
colorama.init()

View File

@ -56,7 +56,6 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
continue
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')

View File

@ -12,6 +12,7 @@ import ModuleUpdate
ModuleUpdate.update()
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoItem, PlandoConnection
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
@ -39,12 +40,12 @@ def mystery_argparse():
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"], help="Path to the 1.0 JP SM Baserom.")
parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
parser.add_argument('--outputpath', default=options["general_options"]["output_path"])
parser.add_argument('--race', action='store_true', default=defaults["race"])
parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
parser.add_argument('--log_output_path', help='Path to store output log')
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
@ -125,20 +126,10 @@ def main(args=None, callback=ERmain):
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
# set up logger
if args.log_level:
erargs.loglevel = args.log_level
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
erargs.loglevel]
Utils.init_logging(f"Generate_{seed}.txt", loglevel=args.log_level)
if args.log_output_path:
os.makedirs(args.log_output_path, exist_ok=True)
logging.basicConfig(format='%(message)s', level=loglevel, force=True,
filename=os.path.join(args.log_output_path, f"{seed}.log"))
else:
logging.basicConfig(format='%(message)s', level=loglevel, force=True)
erargs.rom = args.rom
erargs.lttp_rom = args.lttp_rom
erargs.sm_rom = args.sm_rom
erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)

View File

@ -15,7 +15,7 @@ from argparse import Namespace
from concurrent.futures import as_completed, ThreadPoolExecutor
from glob import glob
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, LEFT, X, TOP, LabelFrame, \
IntVar, Checkbutton, E, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
from urllib.parse import urlparse
from urllib.request import urlopen
@ -51,6 +51,7 @@ def main():
(default: %(default)s)
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--deathlink', help='Enable DeathLink system.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
@ -152,7 +153,8 @@ def adjust(args):
world = getattr(args, "world")
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world)
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
deathlink=args.deathlink)
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
rom.write_to_file(path)
@ -205,6 +207,7 @@ def adjustGUI():
guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
guiargs.music = bool(rom_vars.MusicVar.get())
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
guiargs.deathlink = bool(rom_vars.DeathLinkVar.get())
guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get()
guiargs.sprite = rom_vars.sprite
@ -272,17 +275,18 @@ def update_sprites(task, on_finish=None):
current_sprites = [os.path.basename(file) for file in glob(sprite_dir + '/*')]
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path))
for sprite in sprites_arr if sprite["author"] != "Nintendo"]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if
filename not in current_sprites]
alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in alttpr_filenames]
except Exception as e:
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (
type(e).__name__, e)
successful = False
task.queue_event(finished)
return
def dl(sprite_url, filename):
target = os.path.join(sprite_dir, filename)
with urlopen(sprite_url) as response, open(target, 'wb') as out:
@ -291,7 +295,6 @@ def update_sprites(task, on_finish=None):
def rem(sprite):
os.remove(os.path.join(sprite_dir, sprite))
with ThreadPoolExecutor() as pool:
dl_tasks = []
rem_tasks = []
@ -313,7 +316,7 @@ def update_sprites(task, on_finish=None):
except Exception as e:
logging.exception(e)
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (
type(e).__name__, e)
type(e).__name__, e)
successful = False
for rem_task in as_completed(rem_tasks):
@ -324,7 +327,7 @@ def update_sprites(task, on_finish=None):
except Exception as e:
logging.exception(e)
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (
type(e).__name__, e)
type(e).__name__, e)
successful = False
if successful:
@ -362,7 +365,7 @@ class BackgroundTask(object):
event = self.queue.get_nowait()
event()
if self.running:
#if self is no longer running self.window may no longer be valid
# if self is no longer running self.window may no longer be valid
self.window.update_idletasks()
except queue.Empty:
pass
@ -420,6 +423,7 @@ def get_rom_frame(parent=None):
romVar.set(rom)
romSelectButton['state'] = "disabled"
romSelectButton["text"] = "ROM verified"
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
@ -444,17 +448,21 @@ def get_rom_options_frame(parent=None):
MusicCheckbutton.grid(row=0, column=0, sticky=E)
vars.disableFlashingVar = IntVar(value=1)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar)
disableFlashingCheckbutton.grid(row=6, column=0, sticky=E)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)",
variable=vars.disableFlashingVar)
disableFlashingCheckbutton.grid(row=6, column=0, sticky=W)
vars.DeathLinkVar = IntVar(value=0)
DeathLinkCheckbutton = Checkbutton(romOptionsFrame, text="DeathLink (Team Deaths)", variable=vars.DeathLinkVar)
DeathLinkCheckbutton.grid(row=7, column=0, sticky=W)
spriteDialogFrame = Frame(romOptionsFrame)
spriteDialogFrame.grid(row=0, column=1)
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
vars.spriteNameVar = StringVar()
vars.sprite = None
def set_sprite(sprite_param):
nonlocal vars
if isinstance(sprite_param, str):
@ -491,7 +499,8 @@ def get_rom_options_frame(parent=None):
menuspeedLabel.pack(side=LEFT)
vars.menuspeedVar = StringVar()
vars.menuspeedVar.set('normal')
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple',
'quadruple', 'half')
menuspeedOptionMenu.pack(side=LEFT)
heartcolorFrame = Frame(romOptionsFrame)
@ -518,7 +527,8 @@ def get_rom_options_frame(parent=None):
owPalettesLabel.pack(side=LEFT)
vars.owPalettesVar = StringVar()
vars.owPalettesVar.set('default')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale',
'negative', 'classic', 'dizzy', 'sick', 'puke')
owPalettesOptionMenu.pack(side=LEFT)
uwPalettesFrame = Frame(romOptionsFrame)
@ -527,7 +537,8 @@ def get_rom_options_frame(parent=None):
uwPalettesLabel.pack(side=LEFT)
vars.uwPalettesVar = StringVar()
vars.uwPalettesVar.set('default')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale',
'negative', 'classic', 'dizzy', 'sick', 'puke')
uwPalettesOptionMenu.pack(side=LEFT)
hudPalettesFrame = Frame(romOptionsFrame)
@ -536,7 +547,8 @@ def get_rom_options_frame(parent=None):
hudPalettesLabel.pack(side=LEFT)
vars.hudPalettesVar = StringVar()
vars.hudPalettesVar.set('default')
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
hudPalettesOptionMenu.pack(side=LEFT)
swordPalettesFrame = Frame(romOptionsFrame)
@ -545,7 +557,8 @@ def get_rom_options_frame(parent=None):
swordPalettesLabel.pack(side=LEFT)
vars.swordPalettesVar = StringVar()
vars.swordPalettesVar.set('default')
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
swordPalettesOptionMenu.pack(side=LEFT)
shieldPalettesFrame = Frame(romOptionsFrame)
@ -554,7 +567,8 @@ def get_rom_options_frame(parent=None):
shieldPalettesLabel.pack(side=LEFT)
vars.shieldPalettesVar = StringVar()
vars.shieldPalettesVar.set('default')
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
shieldPalettesOptionMenu.pack(side=LEFT)
spritePoolFrame = Frame(romOptionsFrame)
@ -563,6 +577,7 @@ def get_rom_options_frame(parent=None):
vars.spritePoolCountVar = StringVar()
vars.sprite_pool = []
def set_sprite_pool(sprite_param):
nonlocal vars
operation = "add"
@ -632,8 +647,10 @@ class SpriteSelector():
title_link.pack(side=LEFT)
title_link.bind("<Button-1>", open_custom_sprite_dir)
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir, 'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
self.icon_section(custom_frametitle, self.custom_sprite_dir, 'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir,
'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
self.icon_section(custom_frametitle, self.custom_sprite_dir,
'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
if not randomOnEvent:
self.sprite_pool_section(spritePool)
@ -683,7 +700,8 @@ class SpriteSelector():
button = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
button.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Random", command=self.update_random_button, variable=self.randomOnRandomVar)
button = Checkbutton(frame, text="Random", command=self.update_random_button,
variable=self.randomOnRandomVar)
button.pack(side=LEFT, padx=(0, 5))
if adjuster:
@ -805,7 +823,6 @@ class SpriteSelector():
BackgroundTaskProgress(self.parent, update_sprites, "Updating Sprites", on_finish)
def browse_for_sprite(self):
sprite = filedialog.askopenfilename(
filetypes=[("All Sprite Sources", (".zspr", ".spr", ".sfc", ".smc")),
@ -819,7 +836,6 @@ class SpriteSelector():
self.callback(None)
self.window.destroy()
def use_default_sprite(self):
self.callback(None)
self.window.destroy()
@ -923,7 +939,8 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
gif_lsd = bytearray(7)
gif_lsd[0] = width
gif_lsd[2] = height
gif_lsd[4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[
4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[5] = 0 # background color is zero
gif_lsd[6] = 0 # aspect raio not specified
gif_gct = bytearray(3 * 32)
@ -943,7 +960,8 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
gif_id[7] = height
gif_id[9] = 0 # no local color table
gif_img_minimum_code_size = bytes([7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
gif_img_minimum_code_size = bytes(
[7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
clear = 0x80
stop = 0x81
@ -1100,5 +1118,6 @@ class ToolTips(object):
widget.after_cancel(cls.after_id)
cls.after_id = None
if __name__ == '__main__':
main()
main()

View File

@ -34,7 +34,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_path.cached_path = args.outputpath
start = time.perf_counter()
# initialize the world
world = MultiWorld(args.multi)

View File

@ -4,8 +4,8 @@ import re
import atexit
from subprocess import Popen
from shutil import copyfile
from base64 import b64decode
from time import strftime
import logging
import requests
@ -34,7 +34,7 @@ def prompt_yes_no(prompt):
def find_forge_jar(forge_dir):
for entry in os.scandir(forge_dir):
if ".jar" in entry.name and "forge" in entry.name:
print(f"Found forge .jar: {entry.name}")
logging.info(f"Found forge .jar: {entry.name}")
return entry.name
raise FileNotFoundError(f"Could not find forge .jar in {forge_dir}.")
@ -47,12 +47,12 @@ def find_ap_randomizer_jar(forge_dir):
for entry in os.scandir(mods_dir):
match = ap_mod_re.match(entry.name)
if match:
print(f"Found AP randomizer mod: {match.group()}")
logging.info(f"Found AP randomizer mod: {match.group()}")
return match.group()
return None
else:
os.mkdir(mods_dir)
print(f"Created mods folder in {forge_dir}")
logging.info(f"Created mods folder in {forge_dir}")
return None
@ -64,17 +64,17 @@ def replace_apmc_files(forge_dir, apmc_file):
copy_apmc = True
if not os.path.isdir(apdata_dir):
os.mkdir(apdata_dir)
print(f"Created APData folder in {forge_dir}")
logging.info(f"Created APData folder in {forge_dir}")
for entry in os.scandir(apdata_dir):
if entry.name.endswith(".apmc") and entry.is_file():
if not os.path.samefile(apmc_file, entry.path):
os.remove(entry.path)
print(f"Removed {entry.name} in {apdata_dir}")
logging.info(f"Removed {entry.name} in {apdata_dir}")
else: # apmc already in apdata
copy_apmc = False
if copy_apmc:
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
print(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
# Check mod version, download new mod from GitHub releases page if needed.
@ -86,30 +86,31 @@ def update_mod(forge_dir):
if resp.status_code == 200: # OK
latest_release = resp.json()[0]
if ap_randomizer != latest_release['assets'][0]['name']:
print(f"A new release of the Minecraft AP randomizer mod was found: {latest_release['assets'][0]['name']}")
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
f"{latest_release['assets'][0]['name']}")
if ap_randomizer is not None:
print(f"Your current mod is {ap_randomizer}.")
logging.info(f"Your current mod is {ap_randomizer}.")
else:
print(f"You do not have the AP randomizer mod installed.")
logging.info(f"You do not have the AP randomizer mod installed.")
if prompt_yes_no("Would you like to update?"):
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
print("Downloading AP randomizer mod. This may take a moment...")
logging.info("Downloading AP randomizer mod. This may take a moment...")
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
if apmod_resp.status_code == 200:
with open(new_ap_mod, 'wb') as f:
f.write(apmod_resp.content)
print(f"Wrote new mod file to {new_ap_mod}")
logging.info(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
print(f"Removed old mod file from {old_ap_mod}")
logging.info(f"Removed old mod file from {old_ap_mod}")
else:
print(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
print(f"Please report this issue on the Archipelago Discord server.")
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
logging.error(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
else:
print(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.")
if not prompt_yes_no("Continue anyways?"):
sys.exit(0)
@ -127,13 +128,13 @@ def check_eula(forge_dir):
text = f.read()
if 'false' in text:
# Prompt user to agree to the EULA
print("You need to agree to the Minecraft EULA in order to run the server.")
print("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
logging.info("You need to agree to the Minecraft EULA in order to run the server.")
logging.info("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
if prompt_yes_no("Do you agree to the EULA?"):
f.seek(0)
f.write(text.replace('false', 'true'))
f.truncate()
print(f"Set {eula_path} to true")
logging.info(f"Set {eula_path} to true")
else:
sys.exit(0)
@ -152,12 +153,13 @@ def run_forge_server(forge_dir, heap_arg):
heap_arg = "-Xmx" + heap_arg
argstring = ' '.join([java_exe, heap_arg, "-jar", forge_server, "-nogui"])
print(f"Running Forge server: {argstring}")
logging.info(f"Running Forge server: {argstring}")
os.chdir(forge_dir)
return Popen(argstring)
if __name__ == '__main__':
Utils.init_logging("MinecraftClient")
parser = argparse.ArgumentParser()
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")

View File

@ -431,6 +431,17 @@ class Context:
else:
return self.player_names[team, slot]
def on_goal_achieved(self, client: Client):
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
f' has completed their goal.'
self.notify_all(finished_msg)
if "auto" in self.forfeit_mode:
forfeit_player(self, client.team, client.slot)
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
forfeit_player(self, client.team, client.slot)
if "auto" in self.collect_mode:
collect_player(self, client.team, client.slot)
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
concerns = collections.defaultdict(list)
@ -1203,19 +1214,20 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
cmd: str = args["cmd"]
except:
logging.exception(f"Could not get command from {args}")
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
"text": f"Could not get command from {args} at `cmd`"}])
raise
if type(cmd) is not str:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
"text": f"Command should be str, got {type(cmd)}"}])
return
if cmd == 'Connect':
if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \
'game' not in args:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': 'Connect'}])
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': 'Connect',
"original_cmd": cmd}])
return
errors = set()
@ -1282,14 +1294,21 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
else:
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": network_data_package}])
elif client.auth:
if cmd == "ConnectUpdate":
if not args:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': cmd}])
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': cmd,
"original_cmd": cmd}])
return
if "tags" in args:
old_tags = client.tags
client.tags = args["tags"]
if set(old_tags) != set(client.tags):
ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.")
elif cmd == 'Sync':
items = get_received_items(ctx, client.team, client.slot)
@ -1301,7 +1320,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
elif cmd == 'LocationChecks':
if "Tracker" in client.tags:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
"text": "Trackers can't register new Location Checks"}])
"text": "Trackers can't register new Location Checks",
"original_cmd": cmd}])
else:
register_location_checks(ctx, client.team, client.slot, args["locations"])
@ -1310,7 +1330,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
for location in args["locations"]:
if type(location) is not int or location not in lookup_any_location_id_to_name:
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
"original_cmd": cmd}])
return
target_item, target_player = ctx.locations[client.slot][location]
locs.append(NetworkItem(target_item, location, target_player))
@ -1322,7 +1343,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
elif cmd == 'Say':
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say'}])
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say',
"original_cmd": cmd}])
return
client.messageprocessor(args["text"])
@ -1345,14 +1367,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
current = ctx.client_game_state[client.team, client.slot]
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
if new_status == ClientStatus.CLIENT_GOAL:
finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.'
ctx.notify_all(finished_msg)
if "auto" in ctx.forfeit_mode:
forfeit_player(ctx, client.team, client.slot)
elif proxy_worlds[ctx.games[client.slot]].forced_auto_forfeit:
forfeit_player(ctx, client.team, client.slot)
if "auto" in ctx.collect_mode:
collect_player(ctx, client.team, client.slot)
ctx.on_goal_achieved(client)
ctx.client_game_state[client.team, client.slot] = new_status
@ -1641,8 +1656,7 @@ async def auto_shutdown(ctx, to_cancel=None):
async def main(args: argparse.Namespace):
logging.basicConfig(force=True,
format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
Utils.init_logging("Server", loglevel=args.loglevel.lower())
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.collect_mode,

View File

@ -13,8 +13,6 @@ class JSONMessagePart(typing.TypedDict, total=False):
# optional
type: str
color: str
# mainly for items, optional
found: bool
# owning player for location/item
player: int
@ -274,7 +272,7 @@ class Hint(typing.NamedTuple):
add_json_text(parts, "[Hint]: ")
add_json_text(parts, self.receiving_player, type="player_id")
add_json_text(parts, "'s ")
add_json_item(parts, self.item, self.receiving_player, found=self.found)
add_json_item(parts, self.item, self.receiving_player)
add_json_text(parts, " is at ")
add_json_location(parts, self.location, self.finding_player)
add_json_text(parts, " in ")
@ -291,7 +289,8 @@ class Hint(typing.NamedTuple):
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,
"item": NetworkItem(self.item, self.location, self.finding_player)}
"item": NetworkItem(self.item, self.location, self.finding_player),
"found": self.found}
@property
def local(self):

View File

@ -10,45 +10,74 @@ from typing import Tuple, Optional
import Utils
current_patch_version = 3
current_patch_version = 2
GAME_ALTTP = "A Link to the Past"
GAME_SM = "Super Metroid"
supported_games = {"A Link to the Past", "Super Metroid"}
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import JAP10HASH
def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
if game == GAME_ALTTP:
from worlds.alttp.Rom import JAP10HASH
elif game == GAME_SM:
from worlds.sm.Rom import JAP10HASH
else:
raise RuntimeError("Selected game for base rom not found.")
patch = yaml.dump({"meta": metadata,
"patch": patch,
"game": "A Link to the Past",
"game": game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 1,
"compatible_version": 3,
"version": current_patch_version,
"base_checksum": JAP10HASH})
return patch.encode(encoding="utf-8-sig")
def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import get_base_rom_bytes
def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
if game == GAME_ALTTP:
from worlds.alttp.Rom import get_base_rom_bytes
elif game == GAME_SM:
from worlds.sm.Rom import get_base_rom_bytes
else:
raise RuntimeError("Selected game for base rom not found.")
if metadata is None:
metadata = {}
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
return generate_yaml(patch, metadata)
return generate_yaml(patch, metadata, game)
def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None,
player: int = 0, player_name: str = "") -> str:
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
player: int = 0, player_name: str = "", game: str = GAME_ALTTP) -> str:
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
"player_id": player,
"player_name": player_name}
bytes = generate_patch(load_bytes(rom_file_to_patch),
meta)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp"
meta,
game)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + (
".apbp" if game == GAME_ALTTP else ".apm3")
write_lzma(bytes, target)
return target
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
from worlds.alttp.Rom import get_base_rom_bytes
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
game_name = data["game"]
if game_name in supported_games:
if game_name == GAME_ALTTP:
from worlds.alttp.Rom import get_base_rom_bytes
elif game_name == GAME_SM:
from worlds.sm.Rom import get_base_rom_bytes
else:
raise Exception(f"No Patch handler for game {game_name}")
elif game_name == "alttp": # old version for A Link to the Past
from worlds.alttp.Rom import get_base_rom_bytes
else:
raise Exception(f"Cannot handle game {game_name}")
if not ignore_version and data["compatible_version"] > current_patch_version:
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
@ -68,7 +97,7 @@ def create_rom_file(patch_file: str) -> Tuple[dict, str]:
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:
data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig"))
data["meta"]["server"] = server
bytes = generate_yaml(data["patch"], data["meta"])
bytes = generate_yaml(data["patch"], data["meta"], data["game"])
return lzma.compress(bytes)
@ -82,6 +111,14 @@ def write_lzma(data: bytes, path: str):
f.write(data)
def read_rom(stream, strip_header=True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer
if __name__ == "__main__":
host = Utils.get_public_ipv4()
options = Utils.get_options()['server_options']
@ -113,7 +150,13 @@ if __name__ == "__main__":
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".apm3"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
print(f"Created rom {target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".archipelago"):
import json
import zlib
@ -139,7 +182,7 @@ if __name__ == "__main__":
def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str):
data = zfr.read(zfinfo)
if zfinfo.filename.endswith(".apbp"):
if zfinfo.filename.endswith(".apbp") or zfinfo.filename.endswith(".apm3"):
data = update_patch_data(data, server)
with ziplock:
zfw.writestr(zfinfo, data)
@ -160,12 +203,4 @@ if __name__ == "__main__":
import traceback
traceback.print_exc()
input("Press enter to close.")
def read_rom(stream, strip_header=True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer
input("Press enter to close.")

View File

@ -1,8 +1,5 @@
from __future__ import annotations
import argparse
import atexit
exit_func = atexit.register(input, "Press enter to close.")
import threading
import time
import multiprocessing
@ -14,28 +11,33 @@ import logging
import asyncio
from json import loads, dumps
import ModuleUpdate
from Utils import get_item_name_from_id, init_logging
ModuleUpdate.update()
if __name__ == "__main__":
init_logging("SNIClient")
from Utils import get_item_name_from_id
import colorama
from NetUtils import *
from worlds.alttp import Regions, Shops
from worlds.alttp import Items
from worlds.alttp.Rom import ROM_PLAYER_LIMIT
import Utils
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, init_logging
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, get_base_parser
from Patch import GAME_ALTTP, GAME_SM
init_logging("LttPClient")
snes_logger = logging.getLogger("SNES")
from MultiServer import mark_raw
class DeathState(enum.IntEnum):
killing_player = 1
alive = 2
dead = 3
class LttPCommandProcessor(ClientCommandProcessor):
ctx: Context
@ -92,6 +94,10 @@ class LttPCommandProcessor(ClientCommandProcessor):
self.output("Data Sent")
return True
def _cmd_test_death(self):
self.ctx.on_deathlink({"source": "Console",
"time": time.time()})
class Context(CommonContext):
command_processor = LttPCommandProcessor
@ -110,7 +116,8 @@ class Context(CommonContext):
self.snes_request_lock = asyncio.Lock()
self.snes_write_buffer = []
self.snes_connector_lock = threading.Lock()
self.death_state = False # for death link flop behaviour
self.death_state = DeathState.alive # for death link flop behaviour
self.killing_player_task = None
self.awaiting_rom = False
self.rom = None
@ -140,16 +147,51 @@ class Context(CommonContext):
await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': auth, 'version': Utils.version_tuple,
'tags': self.tags,
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
'uuid': Utils.get_unique_identifier(),
'game': self.game
}])
def on_deathlink(self, data: dict):
snes_buffered_write(self, WRAM_START + 0xF36D, bytes([0]))
snes_buffered_write(self, WRAM_START + 0x0373, bytes([8]))
asyncio.create_task(snes_flush_writes(self))
self.death_state = True
if not self.killing_player_task or self.killing_player_task.done():
self.killing_player_task = asyncio.create_task(deathlink_kill_player(self))
super(Context, self).on_deathlink(data)
async def handle_deathlink_state(self, currently_dead: bool):
# in this state we only care about triggering a death send
if self.death_state == DeathState.alive:
if currently_dead:
self.death_state = DeathState.dead
await self.send_death()
# in this state we care about confirming a kill, to move state to dead
elif self.death_state == DeathState.killing_player:
# this is being handled in deathlink_kill_player(ctx) already
pass
# in this state we wait until the player is alive again
elif self.death_state == DeathState.dead:
if not currently_dead:
self.death_state = DeathState.alive
async def deathlink_kill_player(ctx: Context):
ctx.death_state = DeathState.killing_player
while ctx.death_state == DeathState.killing_player and \
ctx.snes_state == SNESState.SNES_ATTACHED:
if ctx.game == GAME_ALTTP:
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity
elif ctx.game == GAME_SM:
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([0, 0])) # set current health to 0
await snes_flush_writes(ctx)
await asyncio.sleep(1)
gamemode = None
if ctx.game == GAME_ALTTP:
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
elif ctx.game == GAME_SM:
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
if not gamemode or gamemode[0] in (DEATH_MODES if ctx.game == GAME_ALTTP else SM_DEATH_MODES):
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
def color_item(item_id: int, green: bool = False) -> str:
item_name = get_item_name_from_id(item_id)
@ -161,6 +203,7 @@ def color_item(item_id: int, green: bool = False) -> str:
SNES_RECONNECT_DELAY = 5
# LttP
ROM_START = 0x000000
WRAM_START = 0xF50000
WRAM_SIZE = 0x20000
@ -187,7 +230,20 @@ SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x18008D # 1 byte
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
# SM
SM_ROMNAME_START = 0x1C4F00
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
SM_ENDGAME_MODES = {0x26, 0x27}
SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A}
SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes
SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
@ -473,8 +529,8 @@ def launch_sni(ctx: Context):
if os.path.isfile(sni_path):
snes_logger.info(f"Attempting to start {sni_path}")
import subprocess
if Utils.is_frozen(): # if it spawns a visible console, may as well populate it
import sys
if not sys.stdout: # if it spawns a visible console, may as well populate it
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path))
else:
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL,
@ -540,7 +596,7 @@ async def verify_snes_app(socket):
await socket.send(dumps(AppVersion_Request))
app: str = loads(await socket.recv())["Results"][0]
if not "SNI" in app:
if "SNI" not in app:
snes_logger.warning(f"Warning: Did not find SNI as the endpoint, instead {app} was found.")
@ -839,14 +895,23 @@ async def game_watcher(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
gameName = await snes_read(ctx, SM_ROMNAME_START, 2)
if gameName is None:
continue
elif gameName == b"SM":
ctx.game = GAME_SM
else:
ctx.game = GAME_ALTTP
rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
ctx.rom = rom
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
SM_DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
death_link = bool(death_link[0])
death_link = bool(death_link[0] & 0b1)
old_tags = ctx.tags.copy()
if death_link:
ctx.tags.add("DeathLink")
@ -866,77 +931,131 @@ async def game_watcher(ctx: Context):
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
await ctx.disconnect()
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
if gamemode[0] in DEATH_MODES:
if not ctx.death_state: # new death
await ctx.send_death()
ctx.death_state = True
else:
ctx.death_state = False # reset death state, so next death can trigger
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
if gamemode is None or gameend is None or game_timer is None or \
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
continue
if ctx.game == GAME_ALTTP:
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in DEATH_MODES
await ctx.handle_deathlink_state(currently_dead)
delay = 7 if ctx.slow_mode else 2
if gameend[0]:
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if time.perf_counter() - perf_counter < delay:
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
if gamemode is None or gameend is None or game_timer is None or \
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
continue
delay = 7 if ctx.slow_mode else 2
if gameend[0]:
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if time.perf_counter() - perf_counter < delay:
continue
else:
perf_counter = time.perf_counter()
else:
perf_counter = time.perf_counter()
else:
game_timer = game_timer[0] | (game_timer[1] << 8) | (game_timer[2] << 16) | (game_timer[3] << 24)
if abs(game_timer - prev_game_timer) < (delay * 60):
game_timer = game_timer[0] | (game_timer[1] << 8) | (game_timer[2] << 16) | (game_timer[3] << 24)
if abs(game_timer - prev_game_timer) < (delay * 60):
continue
else:
prev_game_timer = game_timer
if gamemode in ENDGAME_MODES: # triforce room and credits
continue
else:
prev_game_timer = game_timer
if gamemode in ENDGAME_MODES: # triforce room and credits
continue
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
if data is None:
continue
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
if data is None:
continue
recv_index = data[0] | (data[1] << 8)
recv_item = data[2]
roomid = data[4] | (data[5] << 8)
roomdata = data[6]
scout_location = data[7]
recv_index = data[0] | (data[1] << 8)
recv_item = data[2]
roomid = data[4] | (data[5] << 8)
roomdata = data[6]
scout_location = data[7]
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, RECV_ITEM_ADDR,
bytes([item.item]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
if scout_location > 0 and scout_location in ctx.locations_info:
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
bytes([scout_location]))
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
bytes([ctx.locations_info[scout_location][0]]))
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location][1])]))
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, RECV_ITEM_ADDR,
bytes([item.item]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
if scout_location > 0 and scout_location in ctx.locations_info:
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
bytes([scout_location]))
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
bytes([ctx.locations_info[scout_location][0]]))
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location][1])]))
await snes_flush_writes(ctx)
await snes_flush_writes(ctx)
if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location)
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata)
elif ctx.game == GAME_SM:
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in SM_DEATH_MODES
await ctx.handle_deathlink_state(currently_dead)
if gamemode is not None and gamemode[0] in SM_ENDGAME_MODES:
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
continue
if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location)
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata)
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4)
if data is None:
continue
recv_index = data[0] | (data[1] << 8)
recv_item = data[2] | (data[3] << 8)
while (recv_index < recv_item):
itemAdress = recv_index * 8
message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8)
# worldId = message[0] | (message[1] << 8) # unused
# itemId = message[2] | (message[3] << 8) # unused
itemIndex = (message[4] | (message[5] << 8)) >> 3
recv_index += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from worlds.sm.Locations import locations_start_id
location_id = locations_start_id + itemIndex
ctx.locations_checked.add(location_id)
location = ctx.location_name_getter(location_id)
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4)
if data is None:
continue
# recv_itemOutPtr = data[0] | (data[1] << 8) # unused
itemOutPtr = data[2] | (data[3] << 8)
from worlds.sm.Items import items_start_id
if itemOutPtr < len(ctx.items_received):
item = ctx.items_received[itemOutPtr]
itemId = item.item - items_start_id
playerID = (item.player-1) if item.player != 0 else (len(ctx.player_names)-1)
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes([playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, (itemId >> 8) & 0xFF]))
itemOutPtr += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602, bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)
async def run_game(romfile):
@ -951,17 +1070,13 @@ async def run_game(romfile):
async def main():
multiprocessing.freeze_support()
parser = argparse.ArgumentParser()
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
args = parser.parse_args()
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
if args.diff_file:
import Patch
logging.info("Patch file was supplied. Creating sfc rom..")
@ -983,8 +1098,8 @@ async def main():
if gui_enabled:
input_task = None
from kvui import LttPManager
ctx.ui = LttPManager(ctx)
from kvui import SNIManager
ctx.ui = SNIManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
@ -1026,4 +1141,3 @@ if __name__ == '__main__':
loop.run_until_complete(main())
loop.close()
colorama.deinit()
atexit.unregister(exit_func)

View File

@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.1.9"
__version__ = "0.2.0"
version_tuple = tuplize_version(__version__)
import builtins
@ -25,6 +25,8 @@ import functools
import io
import collections
import importlib
import logging
from yaml import load, dump, safe_load
try:
@ -124,7 +126,6 @@ unsafe_parse_yaml = functools.partial(load, Loader=Loader)
def get_public_ipv4() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
try:
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
@ -141,7 +142,6 @@ def get_public_ipv4() -> str:
def get_public_ipv6() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
try:
ip = urllib.request.urlopen('https://v6.ident.me').read().decode('utf8').strip()
@ -161,6 +161,11 @@ def get_default_options() -> dict:
"factorio_options": {
"executable": "factorio\\bin\\x64\\factorio",
},
"sm_options": {
"rom_file": "Super Metroid (JU).sfc",
"sni": "SNI",
"rom_start": True,
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
"sni": "SNI",
@ -211,7 +216,6 @@ def get_default_options() -> dict:
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
import logging
for key, value in src.items():
new_keys = keys.copy()
new_keys.append(key)
@ -278,7 +282,6 @@ def persistent_load() -> typing.Dict[dict]:
with open(path, "r") as f:
storage = unsafe_parse_yaml(f.read())
except Exception as e:
import logging
logging.debug(f"Could not read store: {e}")
if storage is None:
storage = {}
@ -337,7 +340,6 @@ def get_adjuster_settings(romfile: str, skip_questions: bool = False) -> typing.
return romfile, False
else:
adjusted = False
import logging
if not hasattr(get_adjuster_settings, "adjust_wanted"):
logging.info(f"Skipping post-patch adjustment")
get_adjuster_settings.adjuster_settings = adjuster_settings
@ -406,3 +408,28 @@ class KeyedDefaultDict(collections.defaultdict):
def get_text_between(text: str, start: str, end: str) -> str:
return text[text.index(start) + len(start): text.rindex(end)]
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
log_format: str = "[%(name)s]: %(message)s"):
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
log_folder = local_path("logs")
os.makedirs(log_folder, exist_ok=True)
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
handler.close()
root_logger.setLevel(loglevel)
file_handler = logging.FileHandler(
os.path.join(log_folder, f"{name}.txt"),
write_mode,
encoding="utf-8-sig")
file_handler.setFormatter(logging.Formatter(log_format))
root_logger.addHandler(file_handler)
if sys.stdout:
root_logger.addHandler(
logging.StreamHandler(sys.stdout)
)

View File

@ -155,7 +155,6 @@ def _read_log(path: str):
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
# noinspection PyTypeChecker
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")

View File

@ -11,7 +11,7 @@ import time
import random
import pickle
import Utils
from .models import *
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
@ -111,11 +111,7 @@ def run_server_process(room_id, ponyconfig: dict):
db.generate_mapping(check_tables=False)
async def main():
logging.basicConfig(format='[%(asctime)s] %(message)s',
level=logging.INFO,
handlers=[
logging.FileHandler(os.path.join(LOGS_FOLDER, f"{room_id}.txt"), 'a', 'utf-8-sig')])
Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext()
ctx.load(room_id)
ctx.init_save()

Binary file not shown.

View File

@ -44,6 +44,7 @@ These packets are are sent from the multiworld server to the client. They are no
* [PrintJSON](#PrintJSON)
* [DataPackage](#DataPackage)
* [Bounced](#Bounced)
* [InvalidPacket](#InvalidPacket)
### RoomInfo
Sent to clients when they connect to an Archipelago server.
@ -157,9 +158,10 @@ Sent to clients purely to display a message to the player. This packet differs f
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | list\[JSONMessagePart\] | See [JSONMessagePart](#JSONMessagePart) for more details on this type. |
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend.
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID.
| item | NetworkItem | Is present if type is Hint or ItemSend and marks the source player id, location id and item id.
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. |
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
| item | NetworkItem | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. |
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
### DataPackage
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.
@ -184,6 +186,7 @@ Sent to clients if the server caught a problem with a packet. This only occurs f
| ---- | ---- | ----- |
| type | string | "cmd" if the Packet isn't available/allowed, "arguments" if the problem is with the package data. |
| text | string | Error text explaining the caught error. |
| original_cmd | string | Echoes the cmd it failed on. May be null if the cmd was not found.
## (Client -> Server)
These packets are sent purely from client to server. They are not accepted by clients.
@ -212,6 +215,14 @@ Sent by the client to initiate a connection to an Archipelago game session.
#### Authentication
Many, if not all, other packets require a successfully authenticated client. This is described in more detail in [Archipelago Connection Handshake](#Archipelago-Connection-Handshake).
### ConnectUpdate
Update arguments from the Connect package, currently only updating tags is supported.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
### Sync
Sent to server to request a [ReceivedItems](#ReceivedItems) packet to synchronize items.
#### Arguments
@ -409,6 +420,8 @@ We encourage clients to cache the data package they receive on disk, or otherwis
Note:
* Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once.
* Any Name is unique to its type across its own Game only: Single Arrow can exist in two games.
* The IDs from the game "Archipelago" may be used in any other game.
Especially Location ID -1: Cheat Console and -2: Server (typically Remote Start Inventory)
#### Contents
| Name | Type | Notes |

View File

@ -91,6 +91,15 @@ lttp_options:
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
sm_options:
# File name of the v1.0 J rom
rom_file: "Super Metroid (JU).sfc"
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
sni: "SNI"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
factorio_options:
executable: "factorio\\bin\\x64\\factorio"
minecraft_options:

View File

@ -34,7 +34,6 @@ SignTool= signtool
LicenseFile= LICENSE
WizardStyle= modern
SetupLogging=yes
MinVersion=6.3.9200
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@ -51,11 +50,14 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
[Components]
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
Name: "generator"; Description: "Generator"; Types: full hosting
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
@ -64,18 +66,19 @@ Name: "client/text"; Description: "Text, to !command and chat"; Types: full play
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator/lttp
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
@ -85,30 +88,38 @@ Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesnt
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/lttp
Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
Name: "{commondesktop}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Tasks: desktopicon; Components: client/lttp
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/lttp or generator/lttp
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp
Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft
[UninstallDelete]
Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
[Registry]
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoLttPClient.exe,0"; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoLttPClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apm3"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
@ -190,11 +201,17 @@ begin
ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;
var ROMFilePage: TInputFileWizardPage;
var R : longint;
var rom: string;
var ROMFilePage: TInputFileWizardPage;
var smrom: string;
var SMRomFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
var MinecraftDownloadPage: TDownloadWizardPage;
procedure AddRomPage();
@ -225,6 +242,34 @@ begin
'.sfc');
end;
procedure AddSMRomPage();
begin
smrom := FileSearch('Super Metroid (JU).sfc', WizardDirValue());
if Length(smrom) > 0 then
begin
log('existing SM ROM found');
log(IntToStr(CompareStr(GetMD5OfFile(smrom), '21f3e98df4780ee1c667b84e57d88675')));
if CompareStr(GetMD5OfFile(smrom), '21f3e98df4780ee1c667b84e57d88675') = 0 then
begin
log('existing SM ROM verified');
exit;
end;
log('existing SM ROM failed verification');
end;
smrom := ''
SMROMFilePage :=
CreateInputFilePage(
wpSelectComponents,
'Select ROM File',
'Where is your Super Metroid located?',
'Select the file, then click Next.');
SMROMFilePage.Add(
'Location of Super Metroid ROM file:',
'SNES ROM files|*.sfc|All files|*.*',
'.sfc');
end;
procedure AddMinecraftDownloads();
begin
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
@ -295,6 +340,7 @@ procedure InitializeWizard();
begin
AddOoTRomPage();
AddRomPage();
AddSMRomPage();
AddMinecraftDownloads();
end;
@ -303,7 +349,9 @@ function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator/lttp'));
Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot'));
end;
@ -324,6 +372,22 @@ begin
Result := '';
end;
function GetSMROMPath(Param: string): string;
begin
if Length(smrom) > 0 then
Result := smrom
else if Assigned(SMRomFilePage) then
begin
R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
if R <> 0 then
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := SMROMFilePage.Values[0]
end
else
Result := '';
end;
function GetOoTROMPath(Param: string): string;
begin
if Length(ootrom) > 0 then

View File

@ -50,11 +50,14 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
[Components]
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
Name: "generator"; Description: "Generator"; Types: full hosting
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
@ -63,18 +66,19 @@ Name: "client/text"; Description: "Text, to !command and chat"; Types: full play
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator/lttp
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
@ -84,30 +88,38 @@ Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesnt
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/lttp
Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
Name: "{commondesktop}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Tasks: desktopicon; Components: client/lttp
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/lttp or generator/lttp
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp
Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft
[UninstallDelete]
Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
[Registry]
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoLttPClient.exe,0"; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoLttPClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apm3"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
@ -189,11 +201,17 @@ begin
ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;
var ROMFilePage: TInputFileWizardPage;
var R : longint;
var rom: string;
var ROMFilePage: TInputFileWizardPage;
var smrom: string;
var SMRomFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
var MinecraftDownloadPage: TDownloadWizardPage;
procedure AddRomPage();
@ -224,6 +242,34 @@ begin
'.sfc');
end;
procedure AddSMRomPage();
begin
smrom := FileSearch('Super Metroid (JU).sfc', WizardDirValue());
if Length(smrom) > 0 then
begin
log('existing SM ROM found');
log(IntToStr(CompareStr(GetMD5OfFile(smrom), '21f3e98df4780ee1c667b84e57d88675')));
if CompareStr(GetMD5OfFile(smrom), '21f3e98df4780ee1c667b84e57d88675') = 0 then
begin
log('existing SM ROM verified');
exit;
end;
log('existing SM ROM failed verification');
end;
smrom := ''
SMROMFilePage :=
CreateInputFilePage(
wpSelectComponents,
'Select ROM File',
'Where is your Super Metroid located?',
'Select the file, then click Next.');
SMROMFilePage.Add(
'Location of Super Metroid ROM file:',
'SNES ROM files|*.sfc|All files|*.*',
'.sfc');
end;
procedure AddMinecraftDownloads();
begin
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
@ -294,6 +340,7 @@ procedure InitializeWizard();
begin
AddOoTRomPage();
AddRomPage();
AddSMRomPage();
AddMinecraftDownloads();
end;
@ -302,7 +349,9 @@ function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator/lttp'));
Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot'));
end;
@ -323,6 +372,22 @@ begin
Result := '';
end;
function GetSMROMPath(Param: string): string;
begin
if Length(smrom) > 0 then
Result := smrom
else if Assigned(SMRomFilePage) then
begin
R := CompareStr(GetMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
if R <> 0 then
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := SMROMFilePage.Values[0]
end
else
Result := '';
end;
function GetOoTROMPath(Param: string): string;
begin
if Length(ootrom) > 0 then

View File

@ -7,6 +7,7 @@ import sys
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
os.environ["KIVY_LOG_ENABLE"] = "0"
from kivy.app import App
from kivy.core.window import Window
@ -286,12 +287,12 @@ class FactorioManager(GameManager):
base_title = "Archipelago Factorio Client"
class LttPManager(GameManager):
class SNIManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("SNES", "SNES"),
]
base_title = "Archipelago LttP Client"
base_title = "Archipelago SNI Client"
class TextManager(GameManager):

View File

@ -30,6 +30,8 @@ game: # Pick a game to play
Subnautica: 0
Slay the Spire: 0
Ocarina of Time: 0
Super Metroid: 0
requires:
version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games:
@ -57,6 +59,270 @@ progression_balancing:
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
# - "Master Sword Pedestal"
Super Metroid: # see https://randommetroidsolver.pythonanywhere.com/randomizer advanced tab for detailed info on each option
# start_inventory: # Begin the file with the listed items/upgrades
# Screw Attack: 1
# Bomb: 1
# Speed Booster: 1
# Grappling Beam: 1
# Space Jump: 1
# Hi-Jump Boots: 1
# Spring Ball: 1
# Charge Beam: 1
# Ice Beam: 1
# Spazer: 1
# Reserve Tank: 4
# Missile: 46
# Super Missile: 20
# Power Bomb: 20
# Energy Tank: 14
# Morph Ball: 1
# X-Ray Scope: 1
# Wave Beam: 1
# Plasma Beam: 1
# Varia Suit: 1
# Gravity Suit: 1
start_inventory_removes_from_pool:
on: 0
off: 1
death_link:
on: 0
off: 1
preset: # choose one of the preset or specify "custom" to use customPreset option
newbie: 0
casual: 0
regular: 1
veteran: 0
expert: 0
master: 0
samus: 0
Season_Races: 0
SMRAT2021: 0
solution: 0
custom: 0 # see https://randommetroidsolver.pythonanywhere.com/presets for detailed info on each preset settings
varia_custom: 0 # use an entry from the preset list on https://randommetroidsolver.pythonanywhere.com/presets
varia_custom_preset: # use an entry from the preset list on https://randommetroidsolver.pythonanywhere.com/presets
regular
start_location:
Ceres: 0
Landing_Site: 1
Gauntlet_Top: 0
Green_Brinstar_Elevator: 0
Big_Pink: 0
Etecoons_Supers: 0
Wrecked_Ship_Main: 0
Firefleas_Top: 0
Business_Center: 0
Bubble_Mountain: 0
Mama_Turtle: 0
Watering_Hole: 0
Aqueduct: 0
Red_Brinstar_Elevator: 0
Golden_Four: 0
max_difficulty:
easy: 0
medium: 0
hard: 0
harder: 0
hardcore: 1
mania: 0
infinity: 0
morph_placement:
early: 1
normal: 0
suits_restriction:
on: 1
off: 0
strict_minors:
on: 0
off: 1
missile_qty: 30 # a range between 10 and 90 that is divided by 10 as a float
super_qty: 20 # a range between 10 and 90 that is divided by 10 as a float
power_bomb_qty: 10 # a range between 10 and 90 that is divided by 10 as a float
minor_qty: 100 # a range between 7 (minimum to beat the game) and 100
energy_qty:
ultra_sparse: 0
sparse: 0
medium: 0
vanilla: 1
area_randomization:
on: 0
light: 0
off: 1
area_layout:
on: 0
off: 1
doors_colors_rando:
on: 0
off: 1
allow_grey_doors:
on: 0
off: 1
boss_randomization:
on: 0
off: 1
fun_combat:
on: 0
off: 1
fun_movement:
on: 0
off: 1
fun_suits:
on: 0
off: 1
layout_patches:
on: 1
off: 0
varia_tweaks:
on: 0
off: 1
nerfed_charge:
on: 0
off: 1
gravity_behaviour:
Vanilla: 0
Balanced: 1
Progressive: 0
elevators_doors_speed:
on: 1
off: 0
spin_jump_restart:
on: 0
off: 1
infinite_space_jump:
on: 0
off: 1
refill_before_save:
on: 0
off: 1
hud:
on: 0
off: 1
animals:
on: 0
off: 1
no_music:
on: 0
off: 1
random_music:
on: 0
off: 1
#item_sounds: always forced on due to a conflict in patching
#majors_split: not supported always "Full"
#scav_num_locs: not supported always off
#scav_randomized: not supported always off
#scav_escape: not supported always off
#progression_speed: not supported always random
#progression_difficulty: not supported always random
#hide_items: not supported always off
#minimizer: not supported always off
#minimizer_qty: not supported always off
#minimizer_tourian: not supported always off
#escape_rando: not supported always off
#remove_escape_enemies: not supported always off
#rando_speed: not supported always off
custom_preset: # see https://randommetroidsolver.pythonanywhere.com/presets for detailed info on each preset settings
Knows: # each skill (know) has a pair [can use, perceived difficulty using one of the following values]
# easy = 1
# medium = 5
# hard = 10
# harder = 25
# hardcore = 50
# mania = 100
Mockball: [True, 1]
SimpleShortCharge: [True, 1]
InfiniteBombJump: [True, 5]
GreenGateGlitch: [True, 5]
ShortCharge: [False, 0]
GravityJump: [True, 10]
SpringBallJump: [True, 10]
SpringBallJumpFromWall: [False, 0]
GetAroundWallJump: [True, 10]
DraygonGrappleKill: [True, 5]
DraygonSparkKill: [False, 0]
MicrowaveDraygon: [True, 1]
MicrowavePhantoon: [True, 5]
IceZebSkip: [False, 0]
SpeedZebSkip: [False, 0]
HiJumpMamaTurtle: [False, 0]
GravLessLevel1: [True, 50]
GravLessLevel2: [False, 0]
GravLessLevel3: [False, 0]
CeilingDBoost: [True, 1]
BillyMays: [True, 1]
AlcatrazEscape: [True, 25]
ReverseGateGlitch: [True, 5]
ReverseGateGlitchHiJumpLess: [False, 0]
EarlyKraid: [True, 1]
XrayDboost: [False, 0]
XrayIce: [True, 10]
RedTowerClimb: [True, 25]
RonPopeilScrew: [False, 0]
OldMBWithSpeed: [False, 0]
Moondance: [False, 0]
HiJumpLessGauntletAccess: [True, 50]
HiJumpGauntletAccess: [True, 25]
LowGauntlet: [False, 0]
IceEscape: [False, 0]
WallJumpCathedralExit: [True, 5]
BubbleMountainWallJump: [True, 5]
NovaBoost: [False, 0]
NorfairReserveDBoost: [False, 0]
CrocPBsDBoost: [False, 0]
CrocPBsIce: [False, 0]
IceMissileFromCroc: [False, 0]
FrogSpeedwayWithoutSpeed: [False, 0]
LavaDive: [True, 50]
LavaDiveNoHiJump: [False, 0]
WorstRoomIceCharge: [False, 0]
ScrewAttackExit: [False, 0]
ScrewAttackExitWithoutScrew: [False, 0]
FirefleasWalljump: [True, 25]
ContinuousWallJump: [False, 0]
DiagonalBombJump: [False, 0]
MockballWs: [False, 0]
SpongeBathBombJump: [False, 0]
SpongeBathHiJump: [True, 1]
SpongeBathSpeed: [True, 5]
TediousMountEverest: [False, 0]
DoubleSpringBallJump: [False, 0]
BotwoonToDraygonWithIce: [False, 0]
DraygonRoomGrappleExit: [False, 0]
DraygonRoomCrystalFlash: [False, 0]
PreciousRoomXRayExit: [False, 0]
MochtroidClip: [True, 5]
PuyoClip: [False, 0]
PuyoClipXRay: [False, 0]
SnailClip: [False, 0]
SuitlessPuyoClip: [False, 0]
KillPlasmaPiratesWithSpark: [False, 0]
KillPlasmaPiratesWithCharge: [True, 5]
AccessSpringBallWithHiJump: [True, 1]
AccessSpringBallWithSpringBallBombJumps: [True, 10]
AccessSpringBallWithBombJumps: [False, 0]
AccessSpringBallWithSpringBallJump: [False, 0]
AccessSpringBallWithXRayClimb: [False, 0]
AccessSpringBallWithGravJump: [False, 0]
Controller:
A: Jump
B: Dash
X: Shoot
Y: Item Cancel
L: Angle Down
R: Angle Up
Select: Item Select
Moonwalk: False
Settings:
Ice: "Gimme energy"
MainUpperNorfair: "Gimme energy"
LowerNorfair: "Default"
Kraid: "Default"
Phantoon: "Default"
Draygon: "Default"
Ridley: "Default"
MotherBrain: "Default"
X-Ray: "I don't like spikes"
Gauntlet: "I don't like acid"
Subnautica: {}
Slay the Spire:
character: # Pick What Character you wish to play with.
@ -1415,4 +1681,4 @@ triggers:
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
options: # then inserts these options
A Link to the Past:
swordless: off
swordless: off

View File

@ -2,7 +2,7 @@ colorama>=0.4.4
websockets>=10.0
PyYAML>=6.0
fuzzywuzzy>=0.18.0
prompt_toolkit>=3.0.20
prompt_toolkit>=3.0.22
appdirs>=1.4.4
jinja2>=3.0.2
jinja2>=3.0.3
schema>=0.7.4

View File

@ -73,8 +73,8 @@ scripts = {
"MultiServer.py": ("ArchipelagoServer", False, icon),
"Generate.py": ("ArchipelagoGenerate", False, icon),
"CommonClient.py": ("ArchipelagoTextClient", True, icon),
# LttP
"LttPClient.py": ("ArchipelagoLttPClient", True, icon),
# SNI
"SNIClient.py": ("ArchipelagoSNIClient", True, icon),
"LttPAdjuster.py": ("ArchipelagoLttPAdjuster", True, icon),
# Factorio
"FactorioClient.py": ("ArchipelagoFactorioClient", True, icon),

12
test/general/TestItems.py Normal file
View File

@ -0,0 +1,12 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
class TestBase(unittest.TestCase):
def testCreateItem(self):
for game_name, world_type in AutoWorldRegister.world_types.items():
proxy_world = world_type(None, 0) # this is identical to MultiServer.py creating worlds
for item_name in world_type.item_name_to_id:
with self.subTest("Create Item", item_name=item_name, game_name=game_name):
item = proxy_world.create_item(item_name)
self.assertEqual(item.name, item_name)

View File

@ -6,7 +6,6 @@ from worlds.AutoWorld import AutoWorldRegister
from . import setup_default_world
class TestBase(unittest.TestCase):
_state_cache = {}
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
def testAllStateCanReachEverything(self):

View File

@ -1,12 +1,8 @@
import unittest
from BaseClasses import MultiWorld
from worlds.AutoWorld import AutoWorldRegister
class TestBase(unittest.TestCase):
world: MultiWorld
_state_cache = {}
def testUniqueItems(self):
known_item_ids = set()
for gamename, world_type in AutoWorldRegister.world_types.items():

View File

@ -4,7 +4,7 @@ import Utils
from Patch import read_rom
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'e397fef0e947d1bd760c68c4fe99a600'
RANDOMIZERBASEHASH = '9952c2a3ec1b421e408df0d20c8f0c7f'
ROM_PLAYER_LIMIT = 255
import io
@ -1645,7 +1645,6 @@ def patch_rom(world, rom, player, enemized):
# remote items flag, does not currently work
rom.write_byte(0x18637C, int(world.worlds[player].remote_items))
rom.write_byte(0x18008D, int(world.death_link[player]))
# set rom name
# 21 bytes
from Main import __version__
@ -1768,7 +1767,7 @@ def hud_format_text(text):
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None):
triforcehud: str = None, deathlink: bool = False):
local_random = random if not world else world.slot_seeds[player]
disable_music: bool = not music
# enable instant item menu
@ -1902,6 +1901,8 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri
elif palettes_options['dungeon'] == 'random':
randomize_uw_palettes(rom, local_random)
rom.write_byte(0x18008D, int(deathlink))
apply_random_sprite_on_event(rom, sprite, local_random, allow_random_on_event,
world.sprite_pool[player] if world else [])
if isinstance(rom, LocalRom):

View File

@ -294,7 +294,8 @@ class ALTTPWorld(World):
world.sprite[player],
palettes_options, world, player, True,
reduceflashing=world.reduceflashing[player] or world.is_race,
triforcehud=world.triforcehud[player].current_key)
triforcehud=world.triforcehud[player].current_key,
deathlink=world.death_link[player])
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
@ -323,7 +324,7 @@ class ALTTPWorld(World):
del (multidata["connect_names"][self.world.player_name[self.player]])
def get_required_client_version(self) -> tuple:
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())
return max((0, 2, 0), super(ALTTPWorld, self).get_required_client_version())
def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **as_dict_item_table[name])

View File

@ -29,7 +29,11 @@ base_info = {
"author": "Berserker",
"homepage": "https://archipelago.gg",
"description": "Integration client for the Archipelago Randomizer",
"factorio_version": "1.1"
"factorio_version": "1.1",
"dependencies": [
"base >= 1.1.0",
"? science-not-invited"
]
}
recipe_time_scales = {
@ -95,7 +99,8 @@ def generate_mod(world, output_directory: str):
"free_sample_blacklist": {item : 1 for item in free_sample_blacklist},
"progressive_technology_table": {tech.name : tech.progressive for tech in
progressive_technology_table.values()},
"custom_recipes": world.custom_recipes}
"custom_recipes": world.custom_recipes,
"max_science_pack": multiworld.max_science_pack[player].value}
for factorio_option in Options.factorio_options:
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value

View File

@ -85,7 +85,8 @@ class CustomTechnology(Technology):
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
ingredients = origin.ingredients & allowed_packs
military_allowed = "military-science-pack" in allowed_packs \
and (ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
or origin.name == "rocket-silo")
self.player = player
if origin.name not in world.worlds[player].static_nodes:
if military_allowed:

View File

@ -5,5 +5,9 @@
"author": "Berserker and Dewiniaid",
"homepage": "https://archipelago.gg",
"description": "Integration client for the Archipelago Randomizer",
"factorio_version": "1.1"
"factorio_version": "1.1",
"dependencies": [
"base >= 1.1.0",
"? science-not-invited"
]
}

View File

@ -26,11 +26,31 @@ template_tech.prerequisites = {}
function prep_copy(new_copy, old_tech)
old_tech.hidden = true
new_copy.unit = table.deepcopy(old_tech.unit)
local ingredient_filter = allowed_ingredients[old_tech.name]
if ingredient_filter ~= nil then
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter)
new_copy.unit.ingredients = add_ingredients(new_copy.unit.ingredients, ingredient_filter)
if mods["science-not-invited"] then
local weights = {
["automation-science-pack"] = 0, -- Red science
["logistic-science-pack"] = 0, -- Green science
["military-science-pack"] = 0, -- Black science
["chemical-science-pack"] = 0, -- Blue science
["production-science-pack"] = 0, -- Purple science
["utility-science-pack"] = 0, -- Yellow science
["space-science-pack"] = 0 -- Space science
}
for key, value in pairs(ingredient_filter) do
weights[key] = value
end
SNI.setWeights(weights)
SNI.sendInvite(old_tech)
-- SCIENCE-not-invited could potentially make tech cost 9.223e+18.
old_tech.unit.count = math.min(10000, old_tech.unit.count)
end
new_copy.unit = table.deepcopy(old_tech.unit)
new_copy.unit.ingredients = filter_ingredients(new_copy.unit.ingredients, ingredient_filter)
new_copy.unit.ingredients = add_ingredients(new_copy.unit.ingredients, ingredient_filter)
else
new_copy.unit = table.deepcopy(old_tech.unit)
end
end

View File

@ -1,2 +1,20 @@
{% from "macros.lua" import dict_to_lua %}
data.raw["map-gen-presets"].default["archipelago"] = {{ dict_to_lua({"default": False, "order": "a", "basic_settings": world_gen["basic"], "advanced_settings": world_gen["advanced"]}) }}
if mods["science-not-invited"] then
local weights = {
["automation-science-pack"] = 0, -- Red science
["logistic-science-pack"] = 0, -- Green science
["military-science-pack"] = 0, -- Black science
["chemical-science-pack"] = 0, -- Blue science
["production-science-pack"] = 0, -- Purple science
["utility-science-pack"] = 0, -- Yellow science
["space-science-pack"] = 0 -- Space science
}
{% if max_science_pack == 6 -%}
weights["space-science-pack"] = 1
{%- endif %}
{% for key in allowed_science_packs -%}
weights["{{key}}"] = 1
{% endfor %}
SNI.setWeights(weights)
end

View File

@ -1,6 +1,8 @@
from typing import NamedTuple, Union
import logging
from BaseClasses import Item
from ..AutoWorld import World
@ -11,11 +13,17 @@ class GenericWorld(World):
"Nothing": -1
}
location_name_to_id = {
"Cheat Console" : -1,
"Cheat Console": -1,
"Server": -2
}
hidden = True
def create_item(self, name: str) -> Item:
if name == "Nothing":
return Item(name, False, -1, self.player)
raise KeyError(name)
class PlandoItem(NamedTuple):
item: str
location: str

View File

@ -5,8 +5,9 @@ from .Regions import TimeOfDay
class OOTEntrance(Entrance):
game: str = 'Ocarina of Time'
def __init__(self, player, name='', parent=None):
def __init__(self, player, world, name='', parent=None):
super(OOTEntrance, self).__init__(player, name, parent)
self.world = world
self.access_rules = []
self.reverse = None
self.replaces = None
@ -17,3 +18,27 @@ class OOTEntrance(Entrance):
self.primary = False
self.always = False
self.never = False
def bind_two_way(self, other_entrance):
self.reverse = other_entrance
other_entrance.reverse = self
def disconnect(self):
self.connected_region.entrances.remove(self)
previously_connected = self.connected_region
self.connected_region = None
return previously_connected
def get_new_target(self):
root = self.world.get_region('Root Exits', self.player)
target_entrance = OOTEntrance(self.player, self.world, 'Root -> ' + self.connected_region.name, root)
target_entrance.connect(self.connected_region)
target_entrance.replaces = self
root.exits.append(target_entrance)
return target_entrance
def assume_reachable(self):
if self.assumed == None:
self.assumed = self.get_new_target()
self.disconnect()
return self.assumed

View File

@ -1,25 +1,771 @@
from itertools import chain
import logging
from worlds.generic.Rules import set_rule
from .Hints import get_hint_area, HintAreaNotFound
from .Regions import TimeOfDay
def set_all_entrances_data(world, player):
for type, forward_entry, *return_entry in entrance_shuffle_table:
forward_entrance = world.get_entrance(forward_entry[0], player)
forward_entrance.data = forward_entry[1]
forward_entrance.type = type
forward_entrance.primary = True
if type == 'Grotto':
forward_entrance.data['index'] = 0x1000 + forward_entrance.data['grotto_id']
if return_entry:
return_entry = return_entry[0]
return_entrance = world.get_entrance(return_entry[0], player)
return_entrance.data = return_entry[1]
return_entrance.type = type
forward_entrance.bind_two_way(return_entrance)
if type == 'Grotto':
return_entrance.data['index'] = 0x7FFF
def assume_entrance_pool(entrance_pool, ootworld):
assumed_pool = []
for entrance in entrance_pool:
assumed_forward = entrance.assume_reachable()
if entrance.reverse != None:
assumed_return = entrance.reverse.assume_reachable()
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
# In most cases, Dungeon, Grotto/Grave and Simple Interior exits shouldn't be assumed able to give access to their parent region
set_rule(assumed_return, lambda state, **kwargs: False)
assumed_forward.bind_two_way(assumed_return)
assumed_pool.append(assumed_forward)
return assumed_pool
def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()):
one_way_entrances = []
for pool_type in types_to_include:
one_way_entrances += world.get_shufflable_entrances(type=pool_type)
valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances))
if target_region_names:
return [entrance.get_new_target() for entrance in valid_one_way_entrances
if entrance.connected_region.name in target_region_names]
return [entrance.get_new_target() for entrance in valid_one_way_entrances]
# Abbreviations
# DMC Death Mountain Crater
# DMT Death Mountain Trail
# GC Goron City
# GF Gerudo Fortress
# GS Gold Skulltula
# GV Gerudo Valley
# HC Hyrule Castle
# HF Hyrule Field
# KF Kokiri Forest
# LH Lake Hylia
# LLR Lon Lon Ranch
# LW Lost Woods
# OGC Outside Ganon's Castle
# SFM Sacred Forest Meadow
# ToT Temple of Time
# ZD Zora's Domain
# ZF Zora's Fountain
# ZR Zora's River
entrance_shuffle_table = [
('Dungeon', ('KF Outside Deku Tree -> Deku Tree Lobby', { 'index': 0x0000 }),
('Deku Tree Lobby -> KF Outside Deku Tree', { 'index': 0x0209, 'blue_warp': 0x0457 })),
('Dungeon', ('Death Mountain -> Dodongos Cavern Beginning', { 'index': 0x0004 }),
('Dodongos Cavern Beginning -> Death Mountain', { 'index': 0x0242, 'blue_warp': 0x047A })),
('Dungeon', ('Zoras Fountain -> Jabu Jabus Belly Beginning', { 'index': 0x0028 }),
('Jabu Jabus Belly Beginning -> Zoras Fountain', { 'index': 0x0221, 'blue_warp': 0x010E })),
('Dungeon', ('SFM Forest Temple Entrance Ledge -> Forest Temple Lobby', { 'index': 0x0169 }),
('Forest Temple Lobby -> SFM Forest Temple Entrance Ledge', { 'index': 0x0215, 'blue_warp': 0x0608 })),
('Dungeon', ('DMC Fire Temple Entrance -> Fire Temple Lower', { 'index': 0x0165 }),
('Fire Temple Lower -> DMC Fire Temple Entrance', { 'index': 0x024A, 'blue_warp': 0x0564 })),
('Dungeon', ('Lake Hylia -> Water Temple Lobby', { 'index': 0x0010 }),
('Water Temple Lobby -> Lake Hylia', { 'index': 0x021D, 'blue_warp': 0x060C })),
('Dungeon', ('Desert Colossus -> Spirit Temple Lobby', { 'index': 0x0082 }),
('Spirit Temple Lobby -> Desert Colossus From Spirit Lobby', { 'index': 0x01E1, 'blue_warp': 0x0610 })),
('Dungeon', ('Graveyard Warp Pad Region -> Shadow Temple Entryway', { 'index': 0x0037 }),
('Shadow Temple Entryway -> Graveyard Warp Pad Region', { 'index': 0x0205, 'blue_warp': 0x0580 })),
('Dungeon', ('Kakariko Village -> Bottom of the Well', { 'index': 0x0098 }),
('Bottom of the Well -> Kakariko Village', { 'index': 0x02A6 })),
('Dungeon', ('ZF Ice Ledge -> Ice Cavern Beginning', { 'index': 0x0088 }),
('Ice Cavern Beginning -> ZF Ice Ledge', { 'index': 0x03D4 })),
('Dungeon', ('Gerudo Fortress -> Gerudo Training Grounds Lobby', { 'index': 0x0008 }),
('Gerudo Training Grounds Lobby -> Gerudo Fortress', { 'index': 0x03A8 })),
('Interior', ('Kokiri Forest -> KF Midos House', { 'index': 0x0433 }),
('KF Midos House -> Kokiri Forest', { 'index': 0x0443 })),
('Interior', ('Kokiri Forest -> KF Sarias House', { 'index': 0x0437 }),
('KF Sarias House -> Kokiri Forest', { 'index': 0x0447 })),
('Interior', ('Kokiri Forest -> KF House of Twins', { 'index': 0x009C }),
('KF House of Twins -> Kokiri Forest', { 'index': 0x033C })),
('Interior', ('Kokiri Forest -> KF Know It All House', { 'index': 0x00C9 }),
('KF Know It All House -> Kokiri Forest', { 'index': 0x026A })),
('Interior', ('Kokiri Forest -> KF Kokiri Shop', { 'index': 0x00C1 }),
('KF Kokiri Shop -> Kokiri Forest', { 'index': 0x0266 })),
('Interior', ('Lake Hylia -> LH Lab', { 'index': 0x0043 }),
('LH Lab -> Lake Hylia', { 'index': 0x03CC })),
('Interior', ('LH Fishing Island -> LH Fishing Hole', { 'index': 0x045F }),
('LH Fishing Hole -> LH Fishing Island', { 'index': 0x0309 })),
('Interior', ('GV Fortress Side -> GV Carpenter Tent', { 'index': 0x03A0 }),
('GV Carpenter Tent -> GV Fortress Side', { 'index': 0x03D0 })),
('Interior', ('Market Entrance -> Market Guard House', { 'index': 0x007E }),
('Market Guard House -> Market Entrance', { 'index': 0x026E })),
('Interior', ('Market -> Market Mask Shop', { 'index': 0x0530 }),
('Market Mask Shop -> Market', { 'index': 0x01D1, 'addresses': [0xC6DA5E] })),
('Interior', ('Market -> Market Bombchu Bowling', { 'index': 0x0507 }),
('Market Bombchu Bowling -> Market', { 'index': 0x03BC })),
('Interior', ('Market -> Market Potion Shop', { 'index': 0x0388 }),
('Market Potion Shop -> Market', { 'index': 0x02A2 })),
('Interior', ('Market -> Market Treasure Chest Game', { 'index': 0x0063 }),
('Market Treasure Chest Game -> Market', { 'index': 0x01D5 })),
('Interior', ('Market Back Alley -> Market Bombchu Shop', { 'index': 0x0528 }),
('Market Bombchu Shop -> Market Back Alley', { 'index': 0x03C0 })),
('Interior', ('Market Back Alley -> Market Man in Green House', { 'index': 0x043B }),
('Market Man in Green House -> Market Back Alley', { 'index': 0x0067 })),
('Interior', ('Kakariko Village -> Kak Carpenter Boss House', { 'index': 0x02FD }),
('Kak Carpenter Boss House -> Kakariko Village', { 'index': 0x0349 })),
('Interior', ('Kakariko Village -> Kak House of Skulltula', { 'index': 0x0550 }),
('Kak House of Skulltula -> Kakariko Village', { 'index': 0x04EE })),
('Interior', ('Kakariko Village -> Kak Impas House', { 'index': 0x039C }),
('Kak Impas House -> Kakariko Village', { 'index': 0x0345 })),
('Interior', ('Kak Impas Ledge -> Kak Impas House Back', { 'index': 0x05C8 }),
('Kak Impas House Back -> Kak Impas Ledge', { 'index': 0x05DC })),
('Interior', ('Kak Backyard -> Kak Odd Medicine Building', { 'index': 0x0072 }),
('Kak Odd Medicine Building -> Kak Backyard', { 'index': 0x034D })),
('Interior', ('Graveyard -> Graveyard Dampes House', { 'index': 0x030D }),
('Graveyard Dampes House -> Graveyard', { 'index': 0x0355 })),
('Interior', ('Goron City -> GC Shop', { 'index': 0x037C }),
('GC Shop -> Goron City', { 'index': 0x03FC })),
('Interior', ('Zoras Domain -> ZD Shop', { 'index': 0x0380 }),
('ZD Shop -> Zoras Domain', { 'index': 0x03C4 })),
('Interior', ('Lon Lon Ranch -> LLR Talons House', { 'index': 0x004F }),
('LLR Talons House -> Lon Lon Ranch', { 'index': 0x0378 })),
('Interior', ('Lon Lon Ranch -> LLR Stables', { 'index': 0x02F9 }),
('LLR Stables -> Lon Lon Ranch', { 'index': 0x042F })),
('Interior', ('Lon Lon Ranch -> LLR Tower', { 'index': 0x05D0 }),
('LLR Tower -> Lon Lon Ranch', { 'index': 0x05D4 })),
('Interior', ('Market -> Market Bazaar', { 'index': 0x052C }),
('Market Bazaar -> Market', { 'index': 0x03B8, 'addresses': [0xBEFD74] })),
('Interior', ('Market -> Market Shooting Gallery', { 'index': 0x016D }),
('Market Shooting Gallery -> Market', { 'index': 0x01CD, 'addresses': [0xBEFD7C] })),
('Interior', ('Kakariko Village -> Kak Bazaar', { 'index': 0x00B7 }),
('Kak Bazaar -> Kakariko Village', { 'index': 0x0201, 'addresses': [0xBEFD72] })),
('Interior', ('Kakariko Village -> Kak Shooting Gallery', { 'index': 0x003B }),
('Kak Shooting Gallery -> Kakariko Village', { 'index': 0x0463, 'addresses': [0xBEFD7A] })),
('Interior', ('Desert Colossus -> Colossus Great Fairy Fountain', { 'index': 0x0588 }),
('Colossus Great Fairy Fountain -> Desert Colossus', { 'index': 0x057C, 'addresses': [0xBEFD82] })),
('Interior', ('Hyrule Castle Grounds -> HC Great Fairy Fountain', { 'index': 0x0578 }),
('HC Great Fairy Fountain -> Castle Grounds', { 'index': 0x0340, 'addresses': [0xBEFD80] })),
('Interior', ('Ganons Castle Grounds -> OGC Great Fairy Fountain', { 'index': 0x04C2 }),
('OGC Great Fairy Fountain -> Castle Grounds', { 'index': 0x0340, 'addresses': [0xBEFD6C] })),
('Interior', ('DMC Lower Nearby -> DMC Great Fairy Fountain', { 'index': 0x04BE }),
('DMC Great Fairy Fountain -> DMC Lower Local', { 'index': 0x0482, 'addresses': [0xBEFD6A] })),
('Interior', ('Death Mountain Summit -> DMT Great Fairy Fountain', { 'index': 0x0315 }),
('DMT Great Fairy Fountain -> Death Mountain Summit', { 'index': 0x045B, 'addresses': [0xBEFD68] })),
('Interior', ('Zoras Fountain -> ZF Great Fairy Fountain', { 'index': 0x0371 }),
('ZF Great Fairy Fountain -> Zoras Fountain', { 'index': 0x0394, 'addresses': [0xBEFD7E] })),
('SpecialInterior', ('Kokiri Forest -> KF Links House', { 'index': 0x0272 }),
('KF Links House -> Kokiri Forest', { 'index': 0x0211 })),
('SpecialInterior', ('ToT Entrance -> Temple of Time', { 'index': 0x0053 }),
('Temple of Time -> ToT Entrance', { 'index': 0x0472 })),
('SpecialInterior', ('Kakariko Village -> Kak Windmill', { 'index': 0x0453 }),
('Kak Windmill -> Kakariko Village', { 'index': 0x0351 })),
('SpecialInterior', ('Kakariko Village -> Kak Potion Shop Front', { 'index': 0x0384 }),
('Kak Potion Shop Front -> Kakariko Village', { 'index': 0x044B })),
('SpecialInterior', ('Kak Backyard -> Kak Potion Shop Back', { 'index': 0x03EC }),
('Kak Potion Shop Back -> Kak Backyard', { 'index': 0x04FF })),
('Grotto', ('Desert Colossus -> Colossus Grotto', { 'grotto_id': 0x00, 'entrance': 0x05BC, 'content': 0xFD, 'scene': 0x5C }),
('Colossus Grotto -> Desert Colossus', { 'grotto_id': 0x00 })),
('Grotto', ('Lake Hylia -> LH Grotto', { 'grotto_id': 0x01, 'entrance': 0x05A4, 'content': 0xEF, 'scene': 0x57 }),
('LH Grotto -> Lake Hylia', { 'grotto_id': 0x01 })),
('Grotto', ('Zora River -> ZR Storms Grotto', { 'grotto_id': 0x02, 'entrance': 0x05BC, 'content': 0xEB, 'scene': 0x54 }),
('ZR Storms Grotto -> Zora River', { 'grotto_id': 0x02 })),
('Grotto', ('Zora River -> ZR Fairy Grotto', { 'grotto_id': 0x03, 'entrance': 0x036D, 'content': 0xE6, 'scene': 0x54 }),
('ZR Fairy Grotto -> Zora River', { 'grotto_id': 0x03 })),
('Grotto', ('Zora River -> ZR Open Grotto', { 'grotto_id': 0x04, 'entrance': 0x003F, 'content': 0x29, 'scene': 0x54 }),
('ZR Open Grotto -> Zora River', { 'grotto_id': 0x04 })),
('Grotto', ('DMC Lower Nearby -> DMC Hammer Grotto', { 'grotto_id': 0x05, 'entrance': 0x05A4, 'content': 0xF9, 'scene': 0x61 }),
('DMC Hammer Grotto -> DMC Lower Local', { 'grotto_id': 0x05 })),
('Grotto', ('DMC Upper Nearby -> DMC Upper Grotto', { 'grotto_id': 0x06, 'entrance': 0x003F, 'content': 0x7A, 'scene': 0x61 }),
('DMC Upper Grotto -> DMC Upper Local', { 'grotto_id': 0x06 })),
('Grotto', ('GC Grotto Platform -> GC Grotto', { 'grotto_id': 0x07, 'entrance': 0x05A4, 'content': 0xFB, 'scene': 0x62 }),
('GC Grotto -> GC Grotto Platform', { 'grotto_id': 0x07 })),
('Grotto', ('Death Mountain -> DMT Storms Grotto', { 'grotto_id': 0x08, 'entrance': 0x003F, 'content': 0x57, 'scene': 0x60 }),
('DMT Storms Grotto -> Death Mountain', { 'grotto_id': 0x08 })),
('Grotto', ('Death Mountain Summit -> DMT Cow Grotto', { 'grotto_id': 0x09, 'entrance': 0x05FC, 'content': 0xF8, 'scene': 0x60 }),
('DMT Cow Grotto -> Death Mountain Summit', { 'grotto_id': 0x09 })),
('Grotto', ('Kak Backyard -> Kak Open Grotto', { 'grotto_id': 0x0A, 'entrance': 0x003F, 'content': 0x28, 'scene': 0x52 }),
('Kak Open Grotto -> Kak Backyard', { 'grotto_id': 0x0A })),
('Grotto', ('Kakariko Village -> Kak Redead Grotto', { 'grotto_id': 0x0B, 'entrance': 0x05A0, 'content': 0xE7, 'scene': 0x52 }),
('Kak Redead Grotto -> Kakariko Village', { 'grotto_id': 0x0B })),
('Grotto', ('Hyrule Castle Grounds -> HC Storms Grotto', { 'grotto_id': 0x0C, 'entrance': 0x05B8, 'content': 0xF6, 'scene': 0x5F }),
('HC Storms Grotto -> Castle Grounds', { 'grotto_id': 0x0C })),
('Grotto', ('Hyrule Field -> HF Tektite Grotto', { 'grotto_id': 0x0D, 'entrance': 0x05C0, 'content': 0xE1, 'scene': 0x51 }),
('HF Tektite Grotto -> Hyrule Field', { 'grotto_id': 0x0D })),
('Grotto', ('Hyrule Field -> HF Near Kak Grotto', { 'grotto_id': 0x0E, 'entrance': 0x0598, 'content': 0xE5, 'scene': 0x51 }),
('HF Near Kak Grotto -> Hyrule Field', { 'grotto_id': 0x0E })),
('Grotto', ('Hyrule Field -> HF Fairy Grotto', { 'grotto_id': 0x0F, 'entrance': 0x036D, 'content': 0xFF, 'scene': 0x51 }),
('HF Fairy Grotto -> Hyrule Field', { 'grotto_id': 0x0F })),
('Grotto', ('Hyrule Field -> HF Near Market Grotto', { 'grotto_id': 0x10, 'entrance': 0x003F, 'content': 0x00, 'scene': 0x51 }),
('HF Near Market Grotto -> Hyrule Field', { 'grotto_id': 0x10 })),
('Grotto', ('Hyrule Field -> HF Cow Grotto', { 'grotto_id': 0x11, 'entrance': 0x05A8, 'content': 0xE4, 'scene': 0x51 }),
('HF Cow Grotto -> Hyrule Field', { 'grotto_id': 0x11 })),
('Grotto', ('Hyrule Field -> HF Inside Fence Grotto', { 'grotto_id': 0x12, 'entrance': 0x059C, 'content': 0xE6, 'scene': 0x51 }),
('HF Inside Fence Grotto -> Hyrule Field', { 'grotto_id': 0x12 })),
('Grotto', ('Hyrule Field -> HF Open Grotto', { 'grotto_id': 0x13, 'entrance': 0x003F, 'content': 0x03, 'scene': 0x51 }),
('HF Open Grotto -> Hyrule Field', { 'grotto_id': 0x13 })),
('Grotto', ('Hyrule Field -> HF Southeast Grotto', { 'grotto_id': 0x14, 'entrance': 0x003F, 'content': 0x22, 'scene': 0x51 }),
('HF Southeast Grotto -> Hyrule Field', { 'grotto_id': 0x14 })),
('Grotto', ('Lon Lon Ranch -> LLR Grotto', { 'grotto_id': 0x15, 'entrance': 0x05A4, 'content': 0xFC, 'scene': 0x63 }),
('LLR Grotto -> Lon Lon Ranch', { 'grotto_id': 0x15 })),
('Grotto', ('SFM Entryway -> SFM Wolfos Grotto', { 'grotto_id': 0x16, 'entrance': 0x05B4, 'content': 0xED, 'scene': 0x56 }),
('SFM Wolfos Grotto -> SFM Entryway', { 'grotto_id': 0x16 })),
('Grotto', ('Sacred Forest Meadow -> SFM Storms Grotto', { 'grotto_id': 0x17, 'entrance': 0x05BC, 'content': 0xEE, 'scene': 0x56 }),
('SFM Storms Grotto -> Sacred Forest Meadow', { 'grotto_id': 0x17 })),
('Grotto', ('Sacred Forest Meadow -> SFM Fairy Grotto', { 'grotto_id': 0x18, 'entrance': 0x036D, 'content': 0xFF, 'scene': 0x56 }),
('SFM Fairy Grotto -> Sacred Forest Meadow', { 'grotto_id': 0x18 })),
('Grotto', ('LW Beyond Mido -> LW Scrubs Grotto', { 'grotto_id': 0x19, 'entrance': 0x05B0, 'content': 0xF5, 'scene': 0x5B }),
('LW Scrubs Grotto -> LW Beyond Mido', { 'grotto_id': 0x19 })),
('Grotto', ('Lost Woods -> LW Near Shortcuts Grotto', { 'grotto_id': 0x1A, 'entrance': 0x003F, 'content': 0x14, 'scene': 0x5B }),
('LW Near Shortcuts Grotto -> Lost Woods', { 'grotto_id': 0x1A })),
('Grotto', ('Kokiri Forest -> KF Storms Grotto', { 'grotto_id': 0x1B, 'entrance': 0x003F, 'content': 0x2C, 'scene': 0x55 }),
('KF Storms Grotto -> Kokiri Forest', { 'grotto_id': 0x1B })),
('Grotto', ('Zoras Domain -> ZD Storms Grotto', { 'grotto_id': 0x1C, 'entrance': 0x036D, 'content': 0xFF, 'scene': 0x58 }),
('ZD Storms Grotto -> Zoras Domain', { 'grotto_id': 0x1C })),
('Grotto', ('Gerudo Fortress -> GF Storms Grotto', { 'grotto_id': 0x1D, 'entrance': 0x036D, 'content': 0xFF, 'scene': 0x5D }),
('GF Storms Grotto -> Gerudo Fortress', { 'grotto_id': 0x1D })),
('Grotto', ('GV Fortress Side -> GV Storms Grotto', { 'grotto_id': 0x1E, 'entrance': 0x05BC, 'content': 0xF0, 'scene': 0x5A }),
('GV Storms Grotto -> GV Fortress Side', { 'grotto_id': 0x1E })),
('Grotto', ('GV Grotto Ledge -> GV Octorok Grotto', { 'grotto_id': 0x1F, 'entrance': 0x05AC, 'content': 0xF2, 'scene': 0x5A }),
('GV Octorok Grotto -> GV Grotto Ledge', { 'grotto_id': 0x1F })),
('Grotto', ('LW Beyond Mido -> Deku Theater', { 'grotto_id': 0x20, 'entrance': 0x05C4, 'content': 0xF3, 'scene': 0x5B }),
('Deku Theater -> LW Beyond Mido', { 'grotto_id': 0x20 })),
('Grave', ('Graveyard -> Graveyard Shield Grave', { 'index': 0x004B }),
('Graveyard Shield Grave -> Graveyard', { 'index': 0x035D })),
('Grave', ('Graveyard -> Graveyard Heart Piece Grave', { 'index': 0x031C }),
('Graveyard Heart Piece Grave -> Graveyard', { 'index': 0x0361 })),
('Grave', ('Graveyard -> Graveyard Composers Grave', { 'index': 0x002D }),
('Graveyard Composers Grave -> Graveyard', { 'index': 0x050B })),
('Grave', ('Graveyard -> Graveyard Dampes Grave', { 'index': 0x044F }),
('Graveyard Dampes Grave -> Graveyard', { 'index': 0x0359 })),
('Overworld', ('Kokiri Forest -> LW Bridge From Forest', { 'index': 0x05E0 }),
('LW Bridge -> Kokiri Forest', { 'index': 0x020D })),
('Overworld', ('Kokiri Forest -> Lost Woods', { 'index': 0x011E }),
('LW Forest Exit -> Kokiri Forest', { 'index': 0x0286 })),
('Overworld', ('Lost Woods -> GC Woods Warp', { 'index': 0x04E2 }),
('GC Woods Warp -> Lost Woods', { 'index': 0x04D6 })),
('Overworld', ('Lost Woods -> Zora River', { 'index': 0x01DD }),
('Zora River -> Lost Woods', { 'index': 0x04DA })),
('Overworld', ('LW Beyond Mido -> SFM Entryway', { 'index': 0x00FC }),
('SFM Entryway -> LW Beyond Mido', { 'index': 0x01A9 })),
('Overworld', ('LW Bridge -> Hyrule Field', { 'index': 0x0185 }),
('Hyrule Field -> LW Bridge', { 'index': 0x04DE })),
('Overworld', ('Hyrule Field -> Lake Hylia', { 'index': 0x0102 }),
('Lake Hylia -> Hyrule Field', { 'index': 0x0189 })),
('Overworld', ('Hyrule Field -> Gerudo Valley', { 'index': 0x0117 }),
('Gerudo Valley -> Hyrule Field', { 'index': 0x018D })),
('Overworld', ('Hyrule Field -> Market Entrance', { 'index': 0x0276 }),
('Market Entrance -> Hyrule Field', { 'index': 0x01FD })),
('Overworld', ('Hyrule Field -> Kakariko Village', { 'index': 0x00DB }),
('Kakariko Village -> Hyrule Field', { 'index': 0x017D })),
('Overworld', ('Hyrule Field -> ZR Front', { 'index': 0x00EA }),
('ZR Front -> Hyrule Field', { 'index': 0x0181 })),
('Overworld', ('Hyrule Field -> Lon Lon Ranch', { 'index': 0x0157 }),
('Lon Lon Ranch -> Hyrule Field', { 'index': 0x01F9 })),
('Overworld', ('Lake Hylia -> Zoras Domain', { 'index': 0x0328 }),
('Zoras Domain -> Lake Hylia', { 'index': 0x0560 })),
('Overworld', ('GV Fortress Side -> Gerudo Fortress', { 'index': 0x0129 }),
('Gerudo Fortress -> GV Fortress Side', { 'index': 0x022D })),
('Overworld', ('GF Outside Gate -> Wasteland Near Fortress', { 'index': 0x0130 }),
('Wasteland Near Fortress -> GF Outside Gate', { 'index': 0x03AC })),
('Overworld', ('Wasteland Near Colossus -> Desert Colossus', { 'index': 0x0123 }),
('Desert Colossus -> Wasteland Near Colossus', { 'index': 0x0365 })),
('Overworld', ('Market Entrance -> Market', { 'index': 0x00B1 }),
('Market -> Market Entrance', { 'index': 0x0033 })),
('Overworld', ('Market -> Castle Grounds', { 'index': 0x0138 }),
('Castle Grounds -> Market', { 'index': 0x025A })),
('Overworld', ('Market -> ToT Entrance', { 'index': 0x0171 }),
('ToT Entrance -> Market', { 'index': 0x025E })),
('Overworld', ('Kakariko Village -> Graveyard', { 'index': 0x00E4 }),
('Graveyard -> Kakariko Village', { 'index': 0x0195 })),
('Overworld', ('Kak Behind Gate -> Death Mountain', { 'index': 0x013D }),
('Death Mountain -> Kak Behind Gate', { 'index': 0x0191 })),
('Overworld', ('Death Mountain -> Goron City', { 'index': 0x014D }),
('Goron City -> Death Mountain', { 'index': 0x01B9 })),
('Overworld', ('GC Darunias Chamber -> DMC Lower Local', { 'index': 0x0246 }),
('DMC Lower Nearby -> GC Darunias Chamber', { 'index': 0x01C1 })),
('Overworld', ('Death Mountain Summit -> DMC Upper Local', { 'index': 0x0147 }),
('DMC Upper Nearby -> Death Mountain Summit', { 'index': 0x01BD })),
('Overworld', ('ZR Behind Waterfall -> Zoras Domain', { 'index': 0x0108 }),
('Zoras Domain -> ZR Behind Waterfall', { 'index': 0x019D })),
('Overworld', ('ZD Behind King Zora -> Zoras Fountain', { 'index': 0x0225 }),
('Zoras Fountain -> ZD Behind King Zora', { 'index': 0x01A1 })),
('OwlDrop', ('LH Owl Flight -> Hyrule Field', { 'index': 0x027E, 'addresses': [0xAC9F26] })),
('OwlDrop', ('DMT Owl Flight -> Kak Impas Rooftop', { 'index': 0x0554, 'addresses': [0xAC9EF2] })),
('Spawn', ('Child Spawn -> KF Links House', { 'index': 0x00BB, 'addresses': [0xB06342] })),
('Spawn', ('Adult Spawn -> Temple of Time', { 'index': 0x05F4, 'addresses': [0xB06332] })),
('WarpSong', ('Minuet of Forest Warp -> Sacred Forest Meadow', { 'index': 0x0600, 'addresses': [0xBF023C] })),
('WarpSong', ('Bolero of Fire Warp -> DMC Central Local', { 'index': 0x04F6, 'addresses': [0xBF023E] })),
('WarpSong', ('Serenade of Water Warp -> Lake Hylia', { 'index': 0x0604, 'addresses': [0xBF0240] })),
('WarpSong', ('Requiem of Spirit Warp -> Desert Colossus', { 'index': 0x01F1, 'addresses': [0xBF0242] })),
('WarpSong', ('Nocturne of Shadow Warp -> Graveyard Warp Pad Region', { 'index': 0x0568, 'addresses': [0xBF0244] })),
('WarpSong', ('Prelude of Light Warp -> Temple of Time', { 'index': 0x05F4, 'addresses': [0xBF0246] })),
('Extra', ('ZD Eyeball Frog Timeout -> Zoras Domain', { 'index': 0x0153 })),
('Extra', ('ZR Top of Waterfall -> Zora River', { 'index': 0x0199 })),
]
# Basically, the entrances in the list above that go to:
# - DMC Central Local (child access for the bean and skull)
# - Desert Colossus (child access to colossus and spirit)
# - Graveyard Warp Pad Region (access to shadow, plus the gossip stone)
# We will always need to pick one from each list to receive a one-way entrance
# if shuffling warp songs (depending on other settings).
# Table maps: short key -> ([target regions], [allowed types])
priority_entrance_table = {
'Bolero': (['DMC Central Local'], ['OwlDrop', 'WarpSong']),
'Nocturne': (['Graveyard Warp Pad Region'], ['OwlDrop', 'Spawn', 'WarpSong']),
'Requiem': (['Desert Colossus', 'Desert Colossus From Spirit Lobby'], ['OwlDrop', 'Spawn', 'WarpSong']),
}
class EntranceShuffleError(Exception):
pass
def shuffle_random_entrances(ootworld):
world = ootworld.world
player = ootworld.player
# Gather locations to keep reachable for validation
all_state = world.get_all_state(use_cache=True)
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
# Set entrance data for all entrances
set_all_entrances_data(world, player)
# Determine entrance pools based on settings
one_way_entrance_pools = {}
entrance_pools = {}
one_way_priorities = {}
if ootworld.owl_drops:
one_way_entrance_pools['OwlDrop'] = ootworld.get_shufflable_entrances(type='OwlDrop')
if ootworld.spawn_positions:
one_way_entrance_pools['Spawn'] = ootworld.get_shufflable_entrances(type='Spawn')
if ootworld.warp_songs:
one_way_entrance_pools['WarpSong'] = ootworld.get_shufflable_entrances(type='WarpSong')
if world.accessibility[player].current_key != 'minimal' and ootworld.logic_rules == 'glitchless':
one_way_priorities['Bolero'] = priority_entrance_table['Bolero']
one_way_priorities['Nocturne'] = priority_entrance_table['Nocturne']
if not ootworld.shuffle_dungeon_entrances and not ootworld.shuffle_overworld_entrances:
one_way_priorities['Requiem'] = priority_entrance_table['Requiem']
if ootworld.shuffle_dungeon_entrances:
entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True)
if ootworld.open_forest == 'closed':
entrance_pools['Dungeon'].remove(world.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby', player))
if ootworld.shuffle_interior_entrances != 'off':
entrance_pools['Interior'] = ootworld.get_shufflable_entrances(type='Interior', only_primary=True)
if ootworld.shuffle_special_interior_entrances:
entrance_pools['Interior'] += ootworld.get_shufflable_entrances(type='SpecialInterior', only_primary=True)
if ootworld.shuffle_grotto_entrances:
entrance_pools['GrottoGrave'] = ootworld.get_shufflable_entrances(type='Grotto', only_primary=True)
entrance_pools['GrottoGrave'] += ootworld.get_shufflable_entrances(type='Grave', only_primary=True)
if ootworld.shuffle_overworld_entrances:
entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld')
# Mark shuffled entrances
for entrance in chain(chain.from_iterable(one_way_entrance_pools.values()), chain.from_iterable(entrance_pools.values())):
entrance.shuffled = True
if entrance.reverse:
entrance.reverse.shuffled = True
# Build target entrance pools
one_way_target_entrance_pools = {}
for pool_type, entrance_pool in one_way_entrance_pools.items():
if pool_type == 'OwlDrop':
valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra')
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time'])
for target in one_way_target_entrance_pools[pool_type]:
set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player))
elif pool_type in {'Spawn', 'WarpSong'}:
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types)
# Ensure that the last entrance doesn't assume the rest of the targets are reachable?
# Disconnect one-way entrances for priority placement
for entrance in chain.from_iterable(one_way_entrance_pools.values()):
entrance.disconnect()
target_entrance_pools = {}
for pool_type, entrance_pool in entrance_pools.items():
target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld)
# Build all_state and none_state
all_state = ootworld.get_state_with_complete_itempool()
none_state = all_state.copy()
for item_tuple in none_state.prog_items:
if item_tuple[1] == player:
none_state.prog_items[item_tuple] = 0
# Plando entrances?
# Place priority entrances
shuffle_one_way_priority_entrances(ootworld, one_way_priorities, one_way_entrance_pools, one_way_target_entrance_pools, locations_to_ensure_reachable, all_state, none_state, retry_count=2)
# Delete priority targets from one-way pools
replaced_entrances = [entrance.replaces for entrance in chain.from_iterable(one_way_entrance_pools.values())]
for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()):
if remaining_target.replaces in replaced_entrances:
delete_target_entrance(remaining_target)
for pool_type, entrance_pool in one_way_entrance_pools.items():
shuffle_entrance_pool(ootworld, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5)
replaced_entrances = [entrance.replaces for entrance in entrance_pool]
for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()):
if remaining_target.replaces in replaced_entrances:
delete_target_entrance(remaining_target)
for unused_target in one_way_target_entrance_pools[pool_type]:
delete_target_entrance(unused_target)
# Shuffle all entrance pools, in order
for pool_type, entrance_pool in entrance_pools.items():
shuffle_entrance_pool(ootworld, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state)
# Verification steps:
# All entrances are properly connected to a region
# Multiple checks after shuffling to ensure everything is OK
# Check that all entrances hook up correctly
for entrance in ootworld.get_shuffled_entrances():
if entrance.connected_region == None:
logging.getLogger('').error(f'{entrance} was shuffled but is not connected to any region')
if entrance.replaces == None:
logging.getLogger('').error(f'{entrance} was shuffled but does not replace any entrance')
if len(ootworld.get_region('Root Exits').exits) > 8:
for exit in ootworld.get_region('Root Exits').exits:
logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}')
logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances')
# Game is beatable
new_all_state = world.get_all_state(use_cache=False)
if not world.has_beaten_game(new_all_state, player):
raise EntranceShuffleError('Cannot beat game')
# Validate world
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
def replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state):
try:
check_entrances_compatibility(entrance, target, rollbacks)
change_connections(entrance, target)
validate_world(ootworld, entrance, locations_to_ensure_reachable, all_state, none_state)
rollbacks.append((entrance, target))
return True
except EntranceShuffleError as e:
logging.getLogger('').debug(f'Failed to connect {entrance} to {target}, reason: {e}')
if entrance.connected_region:
restore_connections(entrance, target)
return False
def shuffle_one_way_priority_entrances(ootworld, one_way_priorities, one_way_entrance_pools, one_way_target_entrance_pools,
locations_to_ensure_reachable, all_state, none_state, retry_count=2):
ootworld.priority_entrances = []
while retry_count:
retry_count -= 1
rollbacks = []
try:
for key, (regions, types) in one_way_priorities.items():
place_one_way_priority_entrance(ootworld, key, regions, types, rollbacks, locations_to_ensure_reachable,
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools)
for entrance, target in rollbacks:
confirm_replacement(entrance, target)
return
except EntranceShuffleError as error:
for entrance, target in rollbacks:
restore_connections(entrance, target)
logging.getLogger('').debug(f'Failed to place all priority one-way entrances, retrying {retry_count} more times')
raise EntranceShuffleError(f'Priority one-way entrance placement attempt count exceeded for world {ootworld.player}')
def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, allowed_types, rollbacks, locations_to_ensure_reachable,
all_state, none_state, one_way_entrance_pools, one_way_target_entrance_pools):
avail_pool = list(chain.from_iterable(one_way_entrance_pools[t] for t in allowed_types if t in one_way_entrance_pools))
ootworld.world.random.shuffle(avail_pool)
for entrance in avail_pool:
if entrance.replaces:
continue
if entrance.parent_region.name == 'Adult Spawn' and (priority_name != 'Nocturne' or ootworld.hints == 'mask'):
continue
if not ootworld.shuffle_dungeon_entrances and priority_name == 'Nocturne':
if entrance.type != 'WarpSong' and entrance.parent_region.name != 'Adult Spawn':
continue
for target in one_way_target_entrance_pools[entrance.type]:
if target.connected_region and target.connected_region.name in allowed_regions:
if replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state):
logging.getLogger('').debug(f'Priority placing {entrance} as {target} for {priority_name}')
ootworld.priority_entrances.append(entrance)
return
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')
def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)
while retry_count:
retry_count -= 1
rollbacks = []
try:
shuffle_entrances(ootworld, restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
if check_all:
shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
else:
shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, set(), all_state, none_state)
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
for entrance, target in rollbacks:
confirm_replacement(entrance, target)
return
except EntranceShuffleError as e:
for entrance, target in rollbacks:
restore_connections(entrance, target)
logging.getLogger('').debug(f'Failed to place all entrances in pool, retrying {retry_count} more times')
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
def shuffle_entrances(ootworld, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
ootworld.world.random.shuffle(entrances)
for entrance in entrances:
if entrance.connected_region != None:
continue
ootworld.world.random.shuffle(target_entrances)
for target in target_entrances:
if target.connected_region == None:
continue
if replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state):
break
if entrance.connected_region == None:
raise EntranceShuffleError('No more valid entrances')
def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances):
world = ootworld.world
player = ootworld.player
# Disconnect all root assumed entrances and save original connections
original_connected_regions = {}
entrances_to_disconnect = set(assumed_entrances).union(entrance.reverse for entrance in assumed_entrances if entrance.reverse)
for entrance in entrances_to_disconnect:
if entrance.connected_region:
original_connected_regions[entrance] = entrance.disconnect()
all_state = world.get_all_state(use_cache=False)
restrictive_entrances = []
soft_entrances = []
for entrance in entrances_to_split:
all_state.age[player] = 'child'
if not all_state.can_reach(entrance, 'Entrance', player):
restrictive_entrances.append(entrance)
continue
all_state.age[player] = 'adult'
if not all_state.can_reach(entrance, 'Entrance', player):
restrictive_entrances.append(entrance)
continue
all_state.age[player] = None
if not all_state._oot_reach_at_time(entrance.parent_region.name, TimeOfDay.ALL, [], player):
restrictive_entrances.append(entrance)
continue
soft_entrances.append(entrance)
# Reconnect assumed entrances
for entrance in entrances_to_disconnect:
if entrance in original_connected_regions:
entrance.connect(original_connected_regions[entrance])
return restrictive_entrances, soft_entrances
# Check to ensure the world is valid.
# TODO: improve this function
def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all_state_orig, none_state_orig):
world = ootworld.world
player = ootworld.player
all_state = all_state_orig.copy()
none_state = none_state_orig.copy()
all_state.sweep_for_events()
none_state.sweep_for_events()
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
time_travel_state = none_state.copy()
time_travel_state.collect(ootworld.create_item('Time Travel'), event=True)
time_travel_state._oot_update_age_reachable_regions(player)
# For various reasons, we don't want the player to end up through certain entrances as the wrong age
# This means we need to hard check that none of the relevant entrances are ever reachable as that age
# This is mostly relevant when shuffling special interiors (such as windmill or kak potion shop)
# Warp Songs and Overworld Spawns can also end up inside certain indoors so those need to be handled as well
CHILD_FORBIDDEN = ['OGC Great Fairy Fountain -> Castle Grounds', 'GV Carpenter Tent -> GV Fortress Side']
ADULT_FORBIDDEN = ['HC Great Fairy Fountain -> Castle Grounds', 'HC Storms Grotto -> Castle Grounds']
for entrance in ootworld.get_shufflable_entrances():
if entrance.shuffled and entrance.replaces:
if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]):
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential child access')
if entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]):
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential adult access')
else:
if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]):
raise EntranceShuffleError(f'{entrance.name} potentially accessible as child')
if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]):
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult')
# Check if all locations are reachable if not beatable-only or game is not yet complete
if locations_to_ensure_reachable:
if world.accessibility[player].current_key != 'minimal' or not world.can_beat_game(all_state):
for loc in locations_to_ensure_reachable:
if not all_state.can_reach(loc, 'Location', player):
raise EntranceShuffleError(f'{loc} is unreachable')
if ootworld.shuffle_interior_entrances and (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
potion_front_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
if potion_front_entrance is not None and potion_back_entrance is not None and not same_hint_area(potion_front_entrance, potion_back_entrance):
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
if ootworld.shuffle_cows:
impas_front_entrance = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
impas_back_entrance = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
if impas_front_entrance is not None and impas_back_entrance is not None and not same_hint_area(impas_front_entrance, impas_back_entrance):
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
# Check basic refills, time passing, return to ToT
if (ootworld.shuffle_special_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions) and \
(entrance_placed == None or entrance_placed.type in ['SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
valid_starting_regions = {'Kokiri Forest', 'Kakariko Village'}
if not any(region for region in valid_starting_regions if none_state.can_reach(region, 'Region', player)):
raise EntranceShuffleError('Invalid starting area')
if not (any(region for region in time_travel_state.child_reachable_regions[player] if region.time_passes) and
any(region for region in time_travel_state.adult_reachable_regions[player] if region.time_passes)):
raise EntranceShuffleError('Time passing is not guaranteed as both ages')
if ootworld.starting_age == 'child' and (world.get_region('Temple of Time', player) not in time_travel_state.adult_reachable_regions[player]):
raise EntranceShuffleError('Path to ToT as adult not guaranteed')
if ootworld.starting_age == 'adult' and (world.get_region('Temple of Time', player) not in time_travel_state.child_reachable_regions[player]):
raise EntranceShuffleError('Path to ToT as child not guaranteed')
if (ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances) and \
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior', 'Overworld', 'Spawn', 'WarpSong', 'OwlDrop']):
# Ensure big poe shop is always reachable as adult
if world.get_region('Market Guard House', player) not in time_travel_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Big Poe Shop access not guaranteed as adult')
if ootworld.shopsanity == 'off':
# Ensure that Goron and Zora shops are accessible as adult
if world.get_region('GC Shop', player) not in all_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Goron City Shop not accessible as adult')
if world.get_region('ZD Shop', player) not in all_state.adult_reachable_regions[player]:
raise EntranceShuffleError('Zora\'s Domain Shop not accessible as adult')
# Recursively check if a given entrance is unreachable as a given age
def entrance_unreachable_as(entrance, age, already_checked=[]):
already_checked.append(entrance)
if entrance.type in {'WarpSong', 'Overworld'}:
return False
elif entrance.type == 'OwlDrop':
return age == 'adult'
elif entrance.name == 'Child Spawn -> KF Links House':
return age == 'adult'
elif entrance.name == 'Adult Spawn -> Temple of Time':
return age == 'child'
for parent_entrance in entrance.parent_region.entrances:
if parent_entrance in already_checked:
continue
unreachable = entrance_unreachable_as(parent_entrance, age, already_checked)
if not unreachable:
return False
return True
def same_hint_area(first, second):
try:
return get_hint_area(first) == get_hint_area(second)
except HintAreaNotFound:
return False
def get_entrance_replacing(region, entrance_name, player):
original_entrance = region.world.get_entrance(entrance_name, player)
if not original_entrance.shuffled:
return original_entrance
try:
return next(filter(lambda entrance: entrance.replaces and entrance.replaces.name == entrance_name and \
entrance.parent_region and entrance.parent_region.name != 'Root Exits' and \
entrance.type not in ('OwlDrop', 'Spawn', 'WarpSong') and entrance.player == player,
region.entrances))
except StopIteration:
return None
def change_connections(entrance, target):
entrance.connect(target.disconnect())
entrance.replaces = target.replaces
if entrance.reverse:
target.replaces.reverse.connect(entrance.reverse.assumed.disconnect())
target.replaces.reverse.replaces = entrance.reverse
def restore_connections(entrance, target):
target.connect(entrance.disconnect())
entrance.replaces = None
if entrance.reverse:
entrance.reverse.assumed.connect(target.replaces.reverse.disconnect())
target.replaces.reverse.replaces = None
def check_entrances_compatibility(entrance, target, rollbacks):
# An entrance shouldn't be connected to its own scene
if entrance.parent_region.get_scene() and entrance.parent_region.get_scene() == target.connected_region.get_scene():
raise EntranceShuffleError('Self-scene connections are forbidden')
# One-way entrances shouldn't lead to the same scene as other one-ways
if entrance.type in {'OwlDrop', 'Spawn', 'WarpSong'} and \
any([rollback[0].connected_region.get_scene() == target.connected_region.get_scene() for rollback in rollbacks]):
raise EntranceShuffleError('Another one-way entrance leads to the same scene')
def confirm_replacement(entrance, target):
delete_target_entrance(target)
logging.getLogger('').debug(f'Connected {entrance} to {entrance.connected_region}')
if entrance.reverse:
replaced_reverse = target.replaces.reverse
delete_target_entrance(entrance.reverse.assumed)
logging.getLogger('').debug(f'Connected {replaced_reverse} to {replaced_reverse.connected_region}')
def delete_target_entrance(target):
if target.connected_region != None:
target.disconnect()
if target.parent_region != None:
target.parent_region.exits.remove(target)
target.parent_region = None

View File

@ -397,6 +397,8 @@ def get_barren_hint(world, checked):
return None
area_weights = [world.empty_areas[area]['weight'] for area in areas]
if not any(area_weights):
return None
area = world.hint_rng.choices(areas, weights=area_weights)[0]
if world.empty_areas[area]['dungeon']:
@ -637,8 +639,6 @@ hint_dist_keys = {
# builds out general hints based on location and whether an item is required or not
def buildWorldGossipHints(world, checkedLocations=None):
# Seed the RNG
world.hint_rng = world.world.slot_seeds[world.player]
# rebuild hint exclusion list
hintExclusions(world, clear_cache=True)

View File

@ -727,6 +727,14 @@ known_logic_tricks = {
To kill it, the logic normally guarantees one of
Hookshot, Bow, or Magic.
'''},
'Skip King Zora as Adult with Nothing': {
'name' : 'logic_king_zora_skip',
'tags' : ("Zora's Domain",),
'tooltip' : '''\
With a precise jump as adult, it is possible to
get on the fence next to King Zora from the front
to access Zora's Fountain.
'''},
'Shadow Temple River Statue with Bombchu': {
'name' : 'logic_shadow_statue',
'tags' : ("Shadow Temple",),

View File

@ -1,5 +1,5 @@
import typing
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionList
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionList, DeathLink
from .ColorSFXOptions import *
@ -94,12 +94,37 @@ class StartingAge(Choice):
option_adult = 1
# TODO: document and name ER options
class InteriorEntrances(Choice):
"""Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, Temple of Time, and Kak potion shop."""
option_off = 0
option_simple = 1
option_all = 2
alias_false = 0
alias_true = 2
class GrottoEntrances(Toggle):
"""Shuffles grotto and grave entrances."""
class DungeonEntrances(Toggle):
"""Shuffles dungeon entrances, excluding Ganon's Castle. Opens Deku, Fire and BotW to both ages."""
class OverworldEntrances(Toggle):
"""Shuffles overworld loading zones."""
class OwlDrops(Toggle):
"""Randomizes owl drops from Lake Hylia or Death Mountain Trail as child."""
class WarpSongs(Toggle):
"""Randomizes warp song destinations."""
class SpawnPositions(Toggle):
"""Randomizes the starting position on loading a save. Consistent between savewarps."""
class TriforceHunt(Toggle):
@ -138,13 +163,13 @@ class MQDungeons(Range):
world_options: typing.Dict[str, type(Option)] = {
"starting_age": StartingAge,
# "shuffle_interior_entrances": InteriorEntrances,
# "shuffle_grotto_entrances": Toggle,
# "shuffle_dungeon_entrances": Toggle,
# "shuffle_overworld_entrances": Toggle,
# "owl_drops": Toggle,
# "warp_songs": Toggle,
# "spawn_positions": Toggle,
"shuffle_interior_entrances": InteriorEntrances,
"shuffle_grotto_entrances": GrottoEntrances,
"shuffle_dungeon_entrances": DungeonEntrances,
"shuffle_overworld_entrances": OverworldEntrances,
"owl_drops": OwlDrops,
"warp_songs": WarpSongs,
"spawn_positions": SpawnPositions,
"triforce_hunt": TriforceHunt,
"triforce_goal": TriforceGoal,
"extra_triforce_percentage": ExtraTriforces,
@ -765,6 +790,13 @@ sfx_options: typing.Dict[str, type(Option)] = {
}
class LogicTricks(OptionList):
"""Set various tricks for logic in Ocarina of Time.
Format as a comma-separated list of "nice" names: ["Fewer Tunic Requirements", "Hidden Grottos without Stone of Agony"].
A full list of supported tricks can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py"""
displayname = "Logic Tricks"
# All options assembled into a single dict
oot_options: typing.Dict[str, type(Option)] = {
"logic_rules": Logic,
@ -780,5 +812,6 @@ oot_options: typing.Dict[str, type(Option)] = {
**itempool_options,
**cosmetic_options,
**sfx_options,
"logic_tricks": OptionList,
"logic_tricks": LogicTricks,
"death_link": DeathLink,
}

View File

@ -3,6 +3,7 @@ import itertools
import re
import zlib
from collections import defaultdict
from functools import partial
from .LocationList import business_scrubs
from .Hints import writeGossipStoneHints, buildAltarHints, \
@ -1321,9 +1322,12 @@ def patch_rom(world, rom):
# Write item overrides
override_table = get_override_table(world)
rom.write_bytes(rom.sym('cfg_item_overrides'), get_override_table_bytes(override_table))
rom.write_byte(rom.sym('PLAYER_ID'), world.player) # Write player ID
rom.write_byte(rom.sym('PLAYER_ID'), min(world.player, 255)) # Write player ID
rom.write_bytes(rom.sym('AP_PLAYER_NAME'), bytearray(world.world.get_player_name(world.player), 'ascii'))
if world.death_link:
rom.write_byte(rom.sym('DEATH_LINK'), 0x01)
# Revert Song Get Override Injection
if not songs_as_items:
# general get song
@ -1804,7 +1808,7 @@ def write_rom_item(rom, item_id, item):
def get_override_table(world):
return list(filter(lambda val: val != None, map(get_override_entry, world.world.get_filled_locations(world.player))))
return list(filter(lambda val: val != None, map(partial(get_override_entry, world.player), world.world.get_filled_locations(world.player))))
override_struct = struct.Struct('>xBBBHBB') # match override_t in get_items.c
@ -1812,10 +1816,10 @@ def get_override_table_bytes(override_table):
return b''.join(sorted(itertools.starmap(override_struct.pack, override_table)))
def get_override_entry(location):
def get_override_entry(player_id, location):
scene = location.scene
default = location.default
player_id = location.item.player
player_id = 0 if player_id == location.item.player else min(location.item.player, 255)
if location.item.game != 'Ocarina of Time':
# This is an AP sendable. It's guaranteed to not be None.
looks_like_item_id = 0

View File

@ -451,14 +451,16 @@ class Rule_AST_Transformer(ast.NodeTransformer):
if self.world.ensure_tod_access:
# tod has DAY or (tod == NONE and (ss or find a path from a provider))
# parsing is better than constructing this expression by hand
return ast.parse("(tod & TimeOfDay.DAY) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAY))", mode='eval').body
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
return ast.parse(f"(state.has('Ocarina', player) and state.has('Suns Song', player)) or state._oot_reach_at_time('{r.name}', TimeOfDay.DAY, [], player)", mode='eval').body
return ast.NameConstant(True)
def at_dampe_time(self, node):
if self.world.ensure_tod_access:
# tod has DAMPE or (tod == NONE and (find a path from a provider))
# parsing is better than constructing this expression by hand
return ast.parse("(tod & TimeOfDay.DAMPE) if tod else state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE)", mode='eval').body
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
return ast.parse(f"state._oot_reach_at_time('{r.name}', TimeOfDay.DAMPE, [], player)", mode='eval').body
return ast.NameConstant(True)
def at_night(self, node):
@ -468,7 +470,8 @@ class Rule_AST_Transformer(ast.NodeTransformer):
if self.world.ensure_tod_access:
# tod has DAMPE or (tod == NONE and (ss or find a path from a provider))
# parsing is better than constructing this expression by hand
return ast.parse("(tod & TimeOfDay.DAMPE) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE))", mode='eval').body
r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
return ast.parse(f"(state.has('Ocarina', player) and state.has('Suns Song', player)) or state._oot_reach_at_time('{r.name}', TimeOfDay.DAMPE, [], player)", mode='eval').body
return ast.NameConstant(True)

View File

@ -2,6 +2,7 @@ from collections import deque
import logging
from .SaveContext import SaveContext
from .Regions import TimeOfDay
from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item
@ -42,6 +43,36 @@ class OOTLogic(LogicMixin):
return can_reach
return self.age[player] == age
def _oot_reach_at_time(self, regionname, tod, already_checked, player):
name_map = {
TimeOfDay.DAY: self.day_reachable_regions[player],
TimeOfDay.DAMPE: self.dampe_reachable_regions[player],
TimeOfDay.ALL: self.day_reachable_regions[player].intersection(self.dampe_reachable_regions[player])
}
if regionname in name_map[tod]:
return True
region = self.world.get_region(regionname, player)
if region.provides_time == TimeOfDay.ALL or regionname == 'Root':
self.day_reachable_regions[player].add(regionname)
self.dampe_reachable_regions[player].add(regionname)
return True
if region.provides_time == TimeOfDay.DAMPE:
self.dampe_reachable_regions[player].add(regionname)
return tod == TimeOfDay.DAMPE
for entrance in region.entrances:
if entrance.parent_region.name in already_checked:
continue
if self._oot_reach_at_time(entrance.parent_region.name, tod, already_checked + [regionname], player):
if tod == TimeOfDay.DAY:
self.day_reachable_regions[player].add(regionname)
elif tod == TimeOfDay.DAMPE:
self.dampe_reachable_regions[player].add(regionname)
elif tod == TimeOfDay.ALL:
self.day_reachable_regions[player].add(regionname)
self.dampe_reachable_regions[player].add(regionname)
return True
return False
# Store the age before calling this!
def _oot_update_age_reachable_regions(self, player):
self.stale[player] = False
@ -62,6 +93,8 @@ class OOTLogic(LogicMixin):
while queue:
connection = queue.popleft()
new_region = connection.connected_region
if new_region is None:
continue
if new_region in rrp:
bc.remove(connection)
elif connection.can_reach(self):

View File

@ -7,7 +7,7 @@ logger = logging.getLogger("Ocarina of Time")
from .Location import OOTLocation, LocationFactory, location_name_to_id
from .Entrance import OOTEntrance
from .EntranceShuffle import shuffle_random_entrances
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
from .Items import OOTItem, item_table, oot_data_to_ap_id
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool
from .Regions import OOTRegion, TimeOfDay
@ -39,6 +39,11 @@ i_o_limiter = threading.Semaphore(2)
class OOTWorld(World):
"""
The Legend of Zelda: Ocarina of Time is a 3D action/adventure game. Travel through Hyrule in two time periods,
learn magical ocarina songs, and explore twelve dungeons on your quest. Use Link's many items and abilities
to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
"""
game: str = "Ocarina of Time"
options: dict = oot_options
topology_present: bool = True
@ -61,6 +66,8 @@ class OOTWorld(World):
self.adult_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.child_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
self.adult_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
self.day_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.dampe_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
self.age = {player: None for player in range(1, parent.players + 1)}
def oot_copy(self):
@ -73,6 +80,10 @@ class OOTWorld(World):
range(1, self.world.players + 1)}
ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
range(1, self.world.players + 1)}
ret.day_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.dampe_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
range(1, self.world.players + 1)}
return ret
CollectionState.__init__ = oot_init
@ -83,6 +94,8 @@ class OOTWorld(World):
world.state.adult_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.child_blocked_connections = {player: set() for player in range(1, world.players + 1)}
world.state.adult_blocked_connections = {player: set() for player in range(1, world.players + 1)}
world.state.day_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.dampe_reachable_regions = {player: set() for player in range(1, world.players + 1)}
world.state.age = {player: None for player in range(1, world.players + 1)}
return super().__new__(cls)
@ -173,14 +186,6 @@ class OOTWorld(World):
self.mq_dungeons_random = False # this will be a deprecated option later
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
self.big_poe_count = 1 # disabled due to client-side issues for now
# ER options
self.shuffle_interior_entrances = 'off'
self.shuffle_grotto_entrances = False
self.shuffle_dungeon_entrances = False
self.shuffle_overworld_entrances = False
self.owl_drops = False
self.warp_songs = False
self.spawn_positions = False
# Set internal names used by the OoT generator
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
@ -313,7 +318,7 @@ class OOTWorld(World):
new_location.show_in_spoiler = False
if 'exits' in region:
for exit, rule in region['exits'].items():
new_exit = OOTEntrance(self.player, '%s => %s' % (new_region.name, exit), new_region)
new_exit = OOTEntrance(self.player, self.world, '%s -> %s' % (new_region.name, exit), new_region)
new_exit.vanilla_connected_region = exit
new_exit.rule_string = rule
if self.world.logic_rules != 'none':
@ -411,7 +416,8 @@ class OOTWorld(World):
def create_item(self, name: str):
if name in item_table:
return OOTItem(name, self.player, item_table[name], False, (name in self.nonadvancement_items))
return OOTItem(name, self.player, item_table[name], False,
(name in self.nonadvancement_items if getattr(self, 'nonadvancement_items', None) else False))
return OOTItem(name, self.player, ('Event', True, None, None), True, False)
def make_event_item(self, name, location, item=None):
@ -431,7 +437,7 @@ class OOTWorld(World):
world_type = 'Glitched World'
overworld_data_path = data_path(world_type, 'Overworld.json')
menu = OOTRegion('Menu', None, None, self.player)
start = OOTEntrance(self.player, 'New Game', menu)
start = OOTEntrance(self.player, self.world, 'New Game', menu)
menu.exits.append(start)
self.world.regions.append(menu)
self.load_regions_from_json(overworld_data_path)
@ -443,14 +449,10 @@ class OOTWorld(World):
self.random_shop_prices()
self.set_scrub_prices()
# logger.info('Setting Entrances.')
# set_entrances(self)
# Enforce vanilla for now
# Bind entrances to vanilla
for region in self.regions:
for exit in region.exits:
exit.connect(self.world.get_region(exit.vanilla_connected_region, self.player))
if self.entrance_shuffle:
shuffle_random_entrances(self)
def create_items(self):
# Generate itempool
@ -481,6 +483,50 @@ class OOTWorld(World):
self.remove_from_start_inventory.extend(removed_items)
def set_rules(self):
# This has to run AFTER creating items but BEFORE set_entrances_based_rules
if self.entrance_shuffle:
# 10 attempts at shuffling entrances
tries = 10
while tries:
try:
shuffle_random_entrances(self)
except EntranceShuffleError as e:
tries -= 1
logging.getLogger('').debug(f"Failed shuffling entrances for world {self.player}, retrying {tries} more times")
if tries == 0:
raise e
# Restore original state and delete assumed entrances
for entrance in self.get_shuffled_entrances():
entrance.connect(self.world.get_region(entrance.vanilla_connected_region, self.player))
if entrance.assumed:
assumed_entrance = entrance.assumed
if assumed_entrance.connected_region is not None:
assumed_entrance.disconnect()
del assumed_entrance
entrance.reverse = None
entrance.replaces = None
entrance.assumed = None
entrance.shuffled = False
# Clean up root entrances
root = self.get_region("Root Exits")
root.exits = root.exits[:8]
else:
break
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(key=lambda x: x.name)
all_entrances.sort(key=lambda x: x.type)
for loadzone in all_entrances:
if loadzone.primary:
entrance = loadzone
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'both', self.player)
else:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
set_rules(self)
set_entrances_based_rules(self)
@ -506,7 +552,7 @@ class OOTWorld(World):
all_locations = self.get_locations()
reachable = self.world.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if
loc.internal and loc.event and loc.locked and loc not in reachable]
(loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable]
for loc in unreachable:
loc.parent_region.locations.remove(loc)
# Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
@ -618,18 +664,35 @@ class OOTWorld(World):
songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.world.itempool))
for song in songs:
self.world.itempool.remove(song)
important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or
self.warp_songs or self.spawn_positions)
song_order = {
'Zeldas Lullaby': 1,
'Eponas Song': 1,
'Sarias Song': 3 if important_warps else 0,
'Suns Song': 0,
'Song of Time': 0,
'Song of Storms': 3,
'Minuet of Forest': 2 if important_warps else 0,
'Bolero of Fire': 2 if important_warps else 0,
'Serenade of Water': 2 if important_warps else 0,
'Requiem of Spirit': 2,
'Nocturne of Shadow': 2,
'Prelude of Light': 2 if important_warps else 0,
}
songs.sort(key=lambda song: song_order.get(song.name, 0))
while tries:
try:
self.world.random.shuffle(songs) # shuffling songs makes it less likely to fail by placing ZL last
self.world.random.shuffle(song_locations)
fill_restrictive(self.world, self.world.get_all_state(False), song_locations[:], songs[:],
True, True)
logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)")
tries = 0
except FillError as e:
tries -= 1
if tries == 0:
raise e
raise Exception(f"Failed placing songs for player {self.player}. Error cause: {e}")
logger.debug(f"Failed placing songs for player {self.player}. Retries left: {tries}")
# undo what was done
for song in songs:
@ -639,6 +702,8 @@ class OOTWorld(World):
location.item = None
location.locked = False
location.event = False
else:
break
# Place shop items
# fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
@ -712,6 +777,9 @@ class OOTWorld(World):
for trap in ice_traps:
trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
# Seed hint RNG, used for ganon text lines also
self.hint_rng = self.world.slot_seeds[self.player]
outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}"
rom = Rom(file=get_options()['oot_options']['rom_file'])
if self.hints != 'none':
@ -787,6 +855,23 @@ class OOTWorld(World):
autoworld.hint_data_available.set()
def modify_multidata(self, multidata: dict):
hint_entrances = set()
for entrance in entrance_shuffle_table:
hint_entrances.add(entrance[1][0])
if len(entrance) > 2:
hint_entrances.add(entrance[2][0])
def get_entrance_to_region(region):
if region.name == 'Root':
return None
for entrance in region.entrances:
if entrance.name in hint_entrances:
return entrance
for entrance in region.entrances:
return get_entrance_to_region(entrance.parent_region)
# Remove undesired items from start_inventory
for item_name in self.remove_from_start_inventory:
item_id = self.item_name_to_id.get(item_name, None)
try:
@ -794,10 +879,26 @@ class OOTWorld(World):
except ValueError as e:
logger.warning(f"Attempted to remove nonexistent item id {item_id} from OoT precollected items ({item_name})")
# Add ER hint data
if self.shuffle_interior_entrances != 'off' or self.shuffle_dungeon_entrances or self.shuffle_grotto_entrances:
er_hint_data = {}
for region in self.regions:
main_entrance = get_entrance_to_region(region)
if main_entrance is not None and main_entrance.shuffled:
for location in region.locations:
if type(location.address) == int:
er_hint_data[location.address] = main_entrance.name
multidata['er_hint_data'][self.player] = er_hint_data
# Helper functions
def get_shuffled_entrances(self):
return [] # later this will return all entrances modified by ER. patching process needs it now though
def get_shufflable_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.world.get_entrances() if (entrance.player == self.player and
(type == None or entrance.type == type) and
(not only_primary or entrance.primary))]
def get_shuffled_entrances(self, type=None, only_primary=False):
return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if entrance.shuffled]
def get_locations(self):
for region in self.regions:
@ -810,6 +911,9 @@ class OOTWorld(World):
def get_region(self, region):
return self.world.get_region(region, self.player)
def get_entrance(self, entrance):
return self.world.get_entrance(entrance, self.player)
def is_major_item(self, item: OOTItem):
if item.type == 'Token':
return self.bridge == 'tokens' or self.lacs_condition == 'tokens'
@ -835,3 +939,29 @@ class OOTWorld(World):
return False
return True
# Specifically ensures that only real items are gotten, not any events.
# In particular, ensures that Time Travel needs to be found.
def get_state_with_complete_itempool(self):
all_state = self.world.get_all_state(use_cache=False)
# Remove event progression items
for item, player in all_state.prog_items:
if (item not in item_table or item_table[item][2] is None) and player == self.player:
all_state.prog_items[(item, player)] = 0
# Remove all events and checked locations
all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player}
all_state.events = {loc for loc in all_state.events if loc.player != self.player}
# If free_scarecrow give Scarecrow Song
if self.free_scarecrow:
all_state.collect(self.create_item("Scarecrow Song"), event=True)
# Invalidate caches
all_state.child_reachable_regions[self.player] = set()
all_state.adult_reachable_regions[self.player] = set()
all_state.child_blocked_connections[self.player] = set()
all_state.adult_blocked_connections[self.player] = set()
all_state.day_reachable_regions[self.player] = set()
all_state.dampe_reachable_regions[self.player] = set()
all_state.stale[self.player] = True
return all_state

View File

@ -1720,7 +1720,8 @@
"Lake Hylia": "is_child and can_dive",
"ZD Behind King Zora": "
Deliver_Letter or zora_fountain == 'open' or
(zora_fountain == 'adult' and is_adult)",
(zora_fountain == 'adult' and is_adult) or
(logic_king_zora_skip and is_adult)",
"ZD Shop": "is_child or Blue_Fire",
"ZD Storms Grotto": "can_open_storm_grotto"
}

File diff suppressed because it is too large Load Diff

View File

@ -38,21 +38,22 @@
"CFG_TEXT_CURSOR_COLOR": "03480866",
"CHAIN_HBA_REWARDS": "03483950",
"CHEST_SIZE_MATCH_CONTENTS": "034826F0",
"COMPLETE_MASK_QUEST": "0348B1C9",
"COMPLETE_MASK_QUEST": "0348B1D1",
"COOP_CONTEXT": "03480020",
"COOP_VERSION": "03480020",
"COSMETIC_CONTEXT": "03480844",
"COSMETIC_FORMAT_VERSION": "03480844",
"CURRENT_GROTTO_ID": "03482E82",
"DEATH_LINK": "0348002A",
"DEBUG_OFFSET": "034828A0",
"DISABLE_TIMERS": "03480CDC",
"DPAD_TEXTURE": "0348D748",
"DPAD_TEXTURE": "0348D750",
"DUNGEONS_SHUFFLED": "03480CDE",
"EXTENDED_OBJECT_TABLE": "03480C9C",
"EXTERN_DAMAGE_MULTIPLYER": "03482CB1",
"FAST_BUNNY_HOOD_ENABLED": "03480CE0",
"FAST_CHESTS": "03480CD6",
"FONT_TEXTURE": "0348C280",
"FONT_TEXTURE": "0348C288",
"FREE_SCARECROW_ENABLED": "03480CCC",
"GET_CHEST_OVERRIDE_COLOR_WRAPPER": "03482720",
"GET_CHEST_OVERRIDE_SIZE_WRAPPER": "034826F4",
@ -68,12 +69,12 @@
"LACS_CONDITION_COUNT": "03480CD2",
"MALON_GAVE_ICETRAP": "0348368C",
"MALON_TEXT_ID": "03480CDB",
"MAX_RUPEES": "0348B1CB",
"MAX_RUPEES": "0348B1D3",
"MOVED_ADULT_KING_ZORA": "03482FFC",
"NO_ESCAPE_SEQUENCE": "0348B194",
"NO_ESCAPE_SEQUENCE": "0348B19C",
"NO_FOG_STATE": "03480CDD",
"OCARINAS_SHUFFLED": "03480CD5",
"OPEN_KAKARIKO": "0348B1CA",
"OPEN_KAKARIKO": "0348B1D2",
"OUTGOING_ITEM": "03480030",
"OUTGOING_KEY": "0348002C",
"OUTGOING_PLAYER": "03480032",
@ -96,88 +97,88 @@
"SPEED_MULTIPLIER": "03482760",
"START_TWINROVA_FIGHT": "0348307C",
"TIME_TRAVEL_SAVED_EQUIPS": "03481A64",
"TRIFORCE_ICON_TEXTURE": "0348DF48",
"TRIFORCE_ICON_TEXTURE": "0348DF50",
"TWINROVA_ACTION_TIMER": "03483080",
"WINDMILL_SONG_ID": "03480CD9",
"WINDMILL_TEXT_ID": "03480CDA",
"a_button": "0348B158",
"a_note_b": "0348B144",
"a_note_font_glow_base": "0348B12C",
"a_note_font_glow_max": "0348B128",
"a_note_g": "0348B148",
"a_note_glow_base": "0348B134",
"a_note_glow_max": "0348B130",
"a_note_r": "0348B14C",
"active_item_action_id": "0348B1AC",
"active_item_fast_chest": "0348B19C",
"active_item_graphic_id": "0348B1A0",
"active_item_object_id": "0348B1A4",
"active_item_row": "0348B1B0",
"active_item_text_id": "0348B1A8",
"active_override": "0348B1B8",
"active_override_is_outgoing": "0348B1B4",
"b_button": "0348B154",
"beating_dd": "0348B160",
"beating_no_dd": "0348B168",
"c_button": "0348B150",
"c_note_b": "0348B138",
"c_note_font_glow_base": "0348B11C",
"c_note_font_glow_max": "0348B118",
"c_note_g": "0348B13C",
"c_note_glow_base": "0348B124",
"c_note_glow_max": "0348B120",
"c_note_r": "0348B140",
"cfg_dungeon_info_enable": "0348B0E4",
"cfg_dungeon_info_mq_enable": "0348B188",
"cfg_dungeon_info_mq_need_map": "0348B184",
"cfg_dungeon_info_reward_enable": "0348B0E0",
"cfg_dungeon_info_reward_need_altar": "0348B17C",
"cfg_dungeon_info_reward_need_compass": "0348B180",
"cfg_dungeon_is_mq": "0348B1E8",
"cfg_dungeon_rewards": "03489EDC",
"cfg_file_select_hash": "0348B190",
"cfg_item_overrides": "0348B23C",
"defaultDDHeart": "0348B16C",
"defaultHeart": "0348B174",
"dpad_sprite": "0348A050",
"dummy_actor": "0348B1C0",
"dungeon_count": "0348B0E8",
"dungeons": "03489F00",
"empty_dlist": "0348B100",
"extern_ctxt": "03489F9C",
"font_sprite": "0348A060",
"freecam_modes": "03489C5C",
"hash_sprites": "0348B0F4",
"hash_symbols": "03489FB0",
"heap_next": "0348B1E4",
"heart_sprite": "03489FF0",
"icon_sprites": "03489E20",
"item_digit_sprite": "0348A010",
"item_overrides_count": "0348B1C4",
"item_table": "0348A0D8",
"items_sprite": "0348A080",
"key_rupee_clock_sprite": "0348A020",
"last_fog_distance": "0348B0EC",
"linkhead_skull_sprite": "0348A000",
"medal_colors": "03489EEC",
"medals_sprite": "0348A090",
"normal_dd": "0348B15C",
"normal_no_dd": "0348B164",
"object_slots": "0348C23C",
"pending_freezes": "0348B1C8",
"pending_item_queue": "0348B224",
"quest_items_sprite": "0348A070",
"rupee_colors": "03489E2C",
"satisified_pending_frames": "0348B198",
"scene_fog_distance": "0348B0F0",
"setup_db": "0348A0B0",
"song_note_sprite": "0348A030",
"stones_sprite": "0348A0A0",
"text_cursor_border_base": "0348B10C",
"text_cursor_border_max": "0348B108",
"text_cursor_inner_base": "0348B114",
"text_cursor_inner_max": "0348B110",
"triforce_hunt_enabled": "0348B1D8",
"triforce_pieces_requied": "0348B17A",
"triforce_sprite": "0348A040"
"a_button": "0348B160",
"a_note_b": "0348B14C",
"a_note_font_glow_base": "0348B134",
"a_note_font_glow_max": "0348B130",
"a_note_g": "0348B150",
"a_note_glow_base": "0348B13C",
"a_note_glow_max": "0348B138",
"a_note_r": "0348B154",
"active_item_action_id": "0348B1B4",
"active_item_fast_chest": "0348B1A4",
"active_item_graphic_id": "0348B1A8",
"active_item_object_id": "0348B1AC",
"active_item_row": "0348B1B8",
"active_item_text_id": "0348B1B0",
"active_override": "0348B1C0",
"active_override_is_outgoing": "0348B1BC",
"b_button": "0348B15C",
"beating_dd": "0348B168",
"beating_no_dd": "0348B170",
"c_button": "0348B158",
"c_note_b": "0348B140",
"c_note_font_glow_base": "0348B124",
"c_note_font_glow_max": "0348B120",
"c_note_g": "0348B144",
"c_note_glow_base": "0348B12C",
"c_note_glow_max": "0348B128",
"c_note_r": "0348B148",
"cfg_dungeon_info_enable": "0348B0EC",
"cfg_dungeon_info_mq_enable": "0348B190",
"cfg_dungeon_info_mq_need_map": "0348B18C",
"cfg_dungeon_info_reward_enable": "0348B0E8",
"cfg_dungeon_info_reward_need_altar": "0348B184",
"cfg_dungeon_info_reward_need_compass": "0348B188",
"cfg_dungeon_is_mq": "0348B1F0",
"cfg_dungeon_rewards": "03489EE4",
"cfg_file_select_hash": "0348B198",
"cfg_item_overrides": "0348B244",
"defaultDDHeart": "0348B174",
"defaultHeart": "0348B17C",
"dpad_sprite": "0348A058",
"dummy_actor": "0348B1C8",
"dungeon_count": "0348B0F0",
"dungeons": "03489F08",
"empty_dlist": "0348B108",
"extern_ctxt": "03489FA4",
"font_sprite": "0348A068",
"freecam_modes": "03489C60",
"hash_sprites": "0348B0FC",
"hash_symbols": "03489FB8",
"heap_next": "0348B1EC",
"heart_sprite": "03489FF8",
"icon_sprites": "03489E24",
"item_digit_sprite": "0348A018",
"item_overrides_count": "0348B1CC",
"item_table": "0348A0E0",
"items_sprite": "0348A088",
"key_rupee_clock_sprite": "0348A028",
"last_fog_distance": "0348B0F4",
"linkhead_skull_sprite": "0348A008",
"medal_colors": "03489EF4",
"medals_sprite": "0348A098",
"normal_dd": "0348B164",
"normal_no_dd": "0348B16C",
"object_slots": "0348C244",
"pending_freezes": "0348B1D0",
"pending_item_queue": "0348B22C",
"quest_items_sprite": "0348A078",
"rupee_colors": "03489E30",
"satisified_pending_frames": "0348B1A0",
"scene_fog_distance": "0348B0F8",
"setup_db": "0348A0B8",
"song_note_sprite": "0348A038",
"stones_sprite": "0348A0A8",
"text_cursor_border_base": "0348B114",
"text_cursor_border_max": "0348B110",
"text_cursor_inner_base": "0348B11C",
"text_cursor_inner_max": "0348B118",
"triforce_hunt_enabled": "0348B1E0",
"triforce_pieces_requied": "0348B182",
"triforce_sprite": "0348A048"
}

View File

@ -42,4 +42,4 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Victory", player),
lambda state: state._ror_has_items(player, 5 * items_per_level) and state.has("Beat Level Five", player))
world.completion_condition[player] = lambda state: state.has("Victory", player)
world.completion_condition[player] = lambda state: state.has("Victory", player)

14
worlds/sm/Items.py Normal file
View File

@ -0,0 +1,14 @@
from worlds.sm.variaRandomizer.rando.Items import ItemManager
items_start_id = 83000
def gen_special_id():
special_id_value_start = 32
while True:
yield special_id_value_start
special_id_value_start += 1
gen_run = gen_special_id()
lookup_id_to_name = dict((items_start_id + (value.Id if value.Id != None else next(gen_run)), value.Name) for key, value in ItemManager.Items.items())
lookup_name_to_id = {item_name: item_id for item_id, item_name in lookup_id_to_name.items()}

14
worlds/sm/Locations.py Normal file
View File

@ -0,0 +1,14 @@
from worlds.sm.variaRandomizer.graph.location import locationsDict
locations_start_id = 82000
def gen_boss_id():
boss_id_value_start = 256
while True:
yield boss_id_value_start
boss_id_value_start += 1
gen_run = gen_boss_id()
lookup_id_to_name = dict((locations_start_id + (value.Id if value.Id != None else next(gen_run)), key) for key, value in locationsDict.items())
lookup_name_to_id = {location_name: location_id for location_id, location_name in lookup_id_to_name.items()}

237
worlds/sm/Options.py Normal file
View File

@ -0,0 +1,237 @@
import typing
from Options import Choice, Range, OptionDict, OptionList, Option, Toggle, DefaultOnToggle, DeathLink
class StartItemsRemovesFromPool(Toggle):
displayname = "StartItems Removes From Item Pool"
class Preset(Choice):
"""choose one of the preset or specify "varia_custom" to use varia_custom_preset option or specify "custom" to use custom_preset option"""
displayname = "Preset"
option_newbie = 0
option_casual = 1
option_regular = 2
option_veteran = 3
option_expert = 4
option_master = 5
option_samus = 6
option_Season_Races = 7
option_SMRAT2021 = 8
option_solution = 9
option_custom = 10
option_varia_custom = 11
default = 2
class StartLocation(Choice):
displayname = "Start Location"
option_Ceres = 0
option_Landing_Site = 1
option_Gauntlet_Top = 2
option_Green_Brinstar_Elevator = 3
option_Big_Pink = 4
option_Etecoons_Supers = 5
option_Wrecked_Ship_Main = 6
option_Firefleas_Top = 7
option_Business_Center = 8
option_Bubble_Mountain = 9
option_Mama_Turtle = 10
option_Watering_Hole = 11
option_Aqueduct = 12
option_Red_Brinstar_Elevator = 13
option_Golden_Four = 14
default = 1
class MaxDifficulty(Choice):
displayname = "Maximum Difficulty"
option_easy = 0
option_medium = 1
option_hard = 2
option_harder = 3
option_hardcore = 4
option_mania = 5
option_infinity = 6
default = 4
class MorphPlacement(Choice):
displayname = "Morph Placement"
option_early = 0
option_normal = 1
default = 0
class SuitsRestriction(DefaultOnToggle):
displayname = "Suits Restriction"
class StrictMinors(Toggle):
displayname = "Strict Minors"
class MissileQty(Range):
displayname = "Missile Quantity"
range_start = 10
range_end = 90
default = 30
class SuperQty(Range):
displayname = "Super Quantity"
range_start = 10
range_end = 90
default = 20
class PowerBombQty(Range):
displayname = "Power Bomb Quantity"
range_start = 10
range_end = 90
default = 10
class MinorQty(Range):
displayname = "Minor Quantity"
range_start = 7
range_end = 100
default = 100
class EnergyQty(Choice):
displayname = "Energy Quantity"
option_ultra_sparse = 0
option_sparse = 1
option_medium = 2
option_vanilla = 3
default = 3
class AreaRandomization(Choice):
displayname = "Area Randomization"
option_off = 0
option_light = 1
option_on = 2
alias_false = 0
alias_true = 2
default = 0
class AreaLayout(Toggle):
displayname = "Area Layout"
class DoorsColorsRando(Toggle):
displayname = "Doors Colors Rando"
class AllowGreyDoors(Toggle):
displayname = "Allow Grey Doors"
class BossRandomization(Toggle):
displayname = "Boss Randomization"
class FunCombat(Toggle):
displayname = "Fun Combat"
class FunMovement(Toggle):
displayname = "Fun Movement"
class FunSuits(Toggle):
displayname = "Fun Suits"
class LayoutPatches(DefaultOnToggle):
displayname = "Layout Patches"
class VariaTweaks(Toggle):
displayname = "Varia Tweaks"
class NerfedCharge(Toggle):
displayname = "Nerfed Charge"
class GravityBehaviour(Choice):
displayname = "Gravity Behaviour"
option_Vanilla = 0
option_Balanced = 1
option_Progressive = 2
default = 1
class ElevatorsDoorsSpeed(DefaultOnToggle):
displayname = "Elevators doors speed"
class SpinJumpRestart(Toggle):
displayname = "Spin Jump Restart"
class InfiniteSpaceJump(Toggle):
displayname = "Infinite Space Jump"
class RefillBeforeSave(Toggle):
displayname = "Refill Before Save"
class Hud(Toggle):
displayname = "Hud"
class Animals(Toggle):
displayname = "Animals"
class NoMusic(Toggle):
displayname = "No Music"
class RandomMusic(Toggle):
displayname = "Random Music"
class CustomPreset(OptionDict):
"""
see https://randommetroidsolver.pythonanywhere.com/presets for detailed info on each preset settings
knows: each skill (know) has a pair [can use, perceived difficulty using one of 1, 5, 10, 25, 50 or 100 each one matching a max_difficulty]
settings: hard rooms, hellruns and bosses settings
controller: predefined controller mapping and moon walk setting
"""
displayname = "Custom Preset"
default = { "knows": {},
"settings": {},
"controller": {}
}
class VariaCustomPreset(OptionList):
"""use an entry from the preset list on https://randommetroidsolver.pythonanywhere.com/presets"""
displayname = "Varia Custom Preset"
default = {}
sm_options: typing.Dict[str, type(Option)] = {
"start_inventory_removes_from_pool": StartItemsRemovesFromPool,
"preset": Preset,
"start_location": StartLocation,
"death_link": DeathLink,
#"majors_split": "Full",
#"scav_num_locs": "10",
#"scav_randomized": "off",
#"scav_escape": "off",
"max_difficulty": MaxDifficulty,
#"progression_speed": "medium",
#"progression_difficulty": "normal",
"morph_placement": MorphPlacement,
"suits_restriction": SuitsRestriction,
#"hide_items": "off",
"strict_minors": StrictMinors,
"missile_qty": MissileQty,
"super_qty": SuperQty,
"power_bomb_qty": PowerBombQty,
"minor_qty": MinorQty,
"energy_qty": EnergyQty,
"area_randomization": AreaRandomization,
"area_layout": AreaLayout,
"doors_colors_rando": DoorsColorsRando,
"allow_grey_doors": AllowGreyDoors,
"boss_randomization": BossRandomization,
#"minimizer": "off",
#"minimizer_qty": "45",
#"minimizer_tourian": "off",
#"escape_rando": "off",
#"remove_escape_enemies": "off",
"fun_combat": FunCombat,
"fun_movement": FunMovement,
"fun_suits": FunSuits,
"layout_patches": LayoutPatches,
"varia_tweaks": VariaTweaks,
"nerfed_charge": NerfedCharge,
"gravity_behaviour": GravityBehaviour,
#"item_sounds": "on",
"elevators_doors_speed": ElevatorsDoorsSpeed,
"spin_jump_restart": SpinJumpRestart,
#"rando_speed": "off",
"infinite_space_jump": InfiniteSpaceJump,
"refill_before_save": RefillBeforeSave,
"hud": Hud,
"animals": Animals,
"no_music": NoMusic,
"random_music": RandomMusic,
"custom_preset": CustomPreset,
"varia_custom_preset": VariaCustomPreset
}

41
worlds/sm/Regions.py Normal file
View File

@ -0,0 +1,41 @@
def create_regions(self, world, player: int):
from . import create_region
from BaseClasses import Entrance
from logic.logic import Logic
from graph.vanilla.graph_locations import locationsDict
regions = []
for accessPoint in Logic.accessPoints:
regions.append(create_region( self,
world,
player,
accessPoint.Name,
None,
[accessPoint.Name + "->" + key for key in accessPoint.intraTransitions.keys()]))
world.regions += regions
# create a region for each location and link each to what the location has access
# we make them one way so that the filler (and spoiler log) doesnt try to use those region as intermediary path
# this is required in AP because a location cant have multiple parent regions
locationRegions = []
for locationName, value in locationsDict.items():
locationRegions.append(create_region( self,
world,
player,
locationName,
[locationName]))
for key in value.AccessFrom.keys():
currentRegion =world.get_region(key, player)
currentRegion.exits.append(Entrance(player, key + "->"+ locationName, currentRegion))
world.regions += locationRegions
#create entrances
regionConcat = regions + locationRegions
for region in regionConcat:
for exit in region.exits:
exit.connect(world.get_region(exit.name[exit.name.find("->") + 2:], player))
world.regions += [
create_region(self, world, player, 'Menu', None, ['StartAP'])
]

30
worlds/sm/Rom.py Normal file
View File

@ -0,0 +1,30 @@
import Utils
from Patch import read_rom
JAP10HASH = '21f3e98df4780ee1c667b84e57d88675'
import hashlib
import os
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:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if JAP10HASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
if not file_name:
file_name = options["sm_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name

40
worlds/sm/Rules.py Normal file
View File

@ -0,0 +1,40 @@
from ..generic.Rules import set_rule, add_rule
from graph.vanilla.graph_locations import locationsDict
from graph.graph_utils import vanillaTransitions, getAccessPoint
from logic.logic import Logic
from rom.rom_patches import RomPatches
from utils.doorsmanager import DoorsManager
def evalSMBool(smbool, maxDiff):
return smbool.bool == True and smbool.difficulty <= maxDiff
def add_accessFrom_rule(location, player, accessFrom):
add_rule(location, lambda state: any((state.can_reach(accessName, player=player) and evalSMBool(rule(state.smbm[player]), state.smbm[player].maxDiff)) for accessName, rule in accessFrom.items()))
def add_postAvailable_rule(location, player, func):
add_rule(location, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff))
def set_available_rule(location, player, func):
set_rule(location, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff))
def set_entrance_rule(entrance, player, func):
set_rule(entrance, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff))
def add_entrance_rule(entrance, player, func):
add_rule(entrance, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff))
def set_rules(world, player):
world.completion_condition[player] = lambda state: state.has('Mother Brain', player)
for key, value in locationsDict.items():
location = world.get_location(key, player)
set_available_rule(location, player, value.Available)
if value.AccessFrom is not None:
add_accessFrom_rule(location, player, value.AccessFrom)
if value.PostAvailable is not None:
add_postAvailable_rule(location, player, value.PostAvailable)
for accessPoint in Logic.accessPoints:
for key, value1 in accessPoint.intraTransitions.items():
set_entrance_rule(world.get_entrance(accessPoint.Name + "->" + key, player), player, value1)

511
worlds/sm/__init__.py Normal file
View File

@ -0,0 +1,511 @@
import logging
import copy
import os
import threading
from typing import Set
logger = logging.getLogger("Super Metroid")
from .Locations import lookup_name_to_id as locations_lookup_name_to_id
from .Items import lookup_name_to_id as items_lookup_name_to_id
from .Regions import create_regions
from .Rules import set_rules, add_entrance_rule
from .Options import sm_options
from .Rom import get_base_rom_path
import Utils
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, CollectionState
from ..AutoWorld import World
import Patch
from logic.smboolmanager import SMBoolManager
from graph.vanilla.graph_locations import locationsDict
from graph.graph_utils import getAccessPoint
from rando.ItemLocContainer import ItemLocation
from rando.Items import ItemManager
from utils.parameters import *
from logic.logic import Logic
from randomizer import VariaRandomizer
class SMWorld(World):
game: str = "Super Metroid"
topology_present = True
data_version = 1
options = sm_options
item_names: Set[str] = frozenset(items_lookup_name_to_id)
location_names: Set[str] = frozenset(locations_lookup_name_to_id)
item_name_to_id = items_lookup_name_to_id
location_name_to_id = locations_lookup_name_to_id
remote_items: bool = False
remote_start_inventory: bool = False
itemManager: ItemManager
locations = {}
Logic.factory('vanilla')
def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
super().__init__(world, player)
def __new__(cls, world, player):
# Add necessary objects to CollectionState on initialization
orig_init = CollectionState.__init__
orig_copy = CollectionState.copy
def sm_init(self, parent: MultiWorld):
if (hasattr(parent, "state")): # for unit tests where MultiWorld is instanciated before worlds
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff) for player in parent.get_game_players("Super Metroid")}
orig_init(self, parent)
def sm_copy(self):
ret = orig_copy(self)
ret.smbm = {player: copy.deepcopy(self.smbm[player]) for player in self.world.get_game_players("Super Metroid")}
return ret
CollectionState.__init__ = sm_init
CollectionState.copy = sm_copy
if world:
world.state.smbm = {}
return super().__new__(cls)
def generate_early(self):
Logic.factory('vanilla')
self.variaRando = VariaRandomizer(self.world, get_base_rom_path(), self.player)
self.world.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty)
# keeps Nothing items local so no player will ever pickup Nothing
# doing so reduces contribution of this world to the Multiworld the more Nothing there is though
self.world.local_items[self.player].value.add('Nothing')
if (self.variaRando.args.morphPlacement == "early"):
self.world.local_items[self.player].value.add('Morph')
def generate_basic(self):
itemPool = self.variaRando.container.itemPool
self.startItems = [variaItem for item in self.world.precollected_items[self.player] for variaItem in ItemManager.Items.values() if variaItem.Name == item.name]
if self.world.start_inventory_removes_from_pool[self.player]:
for item in self.startItems:
if (item in itemPool):
itemPool.remove(item)
missingPool = 105 - len(itemPool) + 1
for i in range(1, missingPool):
itemPool.append(ItemManager.Items['Nothing'])
# Generate item pool
pool = []
self.locked_items = {}
weaponCount = [0, 0, 0]
for item in itemPool:
isAdvancement = True
if item.Type == 'Missile':
if weaponCount[0] < 3:
weaponCount[0] += 1
else:
isAdvancement = False
elif item.Type == 'Super':
if weaponCount[1] < 2:
weaponCount[1] += 1
else:
isAdvancement = False
elif item.Type == 'PowerBomb':
if weaponCount[2] < 3:
weaponCount[2] += 1
else:
isAdvancement = False
elif item.Type == 'Nothing':
isAdvancement = False
itemClass = ItemManager.Items[item.Type].Class
smitem = SMItem(item.Name, isAdvancement, item.Type, None if itemClass == 'Boss' else self.item_name_to_id[item.Name], player = self.player)
if itemClass == 'Boss':
self.locked_items[item.Name] = smitem
else:
pool.append(smitem)
self.world.itempool += pool
for (location, item) in self.locked_items.items():
self.world.get_location(location, self.player).place_locked_item(item)
self.world.get_location(location, self.player).address = None
startAP = self.world.get_entrance('StartAP', self.player)
startAP.connect(self.world.get_region(self.variaRando.args.startLocation, self.player))
for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions:
src_region = self.world.get_region(src.Name, self.player)
dest_region = self.world.get_region(dest.Name, self.player)
src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region))
srcDestEntrance = self.world.get_entrance(src.Name + "->" + dest.Name, self.player)
srcDestEntrance.connect(dest_region)
add_entrance_rule(self.world.get_entrance(src.Name + "->" + dest.Name, self.player), self.player, getAccessPoint(src.Name).traverse)
def set_rules(self):
set_rules(self.world, self.player)
def create_regions(self):
create_locations(self, self.player)
create_regions(self, self.world, self.player)
def getWord(self, w):
return (w & 0x00FF, (w & 0xFF00) >> 8)
# used for remote location Credits Spoiler of local items
class DummyLocation:
def __init__(self, name):
self.Name = name
def isBoss(self):
return False
def convertToROMItemName(self, itemName):
charMap = { "A" : 0x3CE0,
"B" : 0x3CE1,
"C" : 0x3CE2,
"D" : 0x3CE3,
"E" : 0x3CE4,
"F" : 0x3CE5,
"G" : 0x3CE6,
"H" : 0x3CE7,
"I" : 0x3CE8,
"J" : 0x3CE9,
"K" : 0x3CEA,
"L" : 0x3CEB,
"M" : 0x3CEC,
"N" : 0x3CED,
"O" : 0x3CEE,
"P" : 0x3CEF,
"Q" : 0x3CF0,
"R" : 0x3CF1,
"S" : 0x3CF2,
"T" : 0x3CF3,
"U" : 0x3CF4,
"V" : 0x3CF5,
"W" : 0x3CF6,
"X" : 0x3CF7,
"Y" : 0x3CF8,
"Z" : 0x3CF9,
" " : 0x3C4E,
"!" : 0x3CFF,
"?" : 0x3CFE,
"'" : 0x3CFD,
"," : 0x3CFB,
"." : 0x3CFA,
"-" : 0x3CCF,
"_" : 0x000E,
"1" : 0x3C00,
"2" : 0x3C01,
"3" : 0x3C02,
"4" : 0x3C03,
"5" : 0x3C04,
"6" : 0x3C05,
"7" : 0x3C06,
"8" : 0x3C07,
"9" : 0x3C08,
"0" : 0x3C09,
"%" : 0x3C0A}
data = []
itemName = itemName.upper()[:26]
itemName = itemName.strip()
itemName = itemName.center(26, " ")
itemName = "___" + itemName + "___"
for char in itemName:
(w0, w1) = self.getWord(charMap.get(char, 0x3C4E))
data.append(w0)
data.append(w1)
return data
def APPatchRom(self, romPatcher):
multiWorldLocations = {}
multiWorldItems = {}
idx = 0
itemId = 0
for itemLoc in self.world.get_locations():
if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None:
if itemLoc.item.type in ItemManager.Items:
itemId = ItemManager.Items[itemLoc.item.type].Id
else:
itemId = ItemManager.Items['ArchipelagoItem'].Id + idx
multiWorldItems[0x029EA3 + idx*64] = self.convertToROMItemName(itemLoc.item.name)
idx += 1
(w0, w1) = self.getWord(0 if itemLoc.item.player == self.player else 1)
(w2, w3) = self.getWord(itemId)
(w4, w5) = self.getWord(itemLoc.item.player - 1)
(w6, w7) = self.getWord(0 if itemLoc.item.advancement else 1)
multiWorldLocations[0x1C6000 + locationsDict[itemLoc.name].Id*8] = [w0, w1, w2, w3, w4, w5, w6, w7]
itemSprites = ["off_world_prog_item.bin", "off_world_item.bin"]
idx = 0
offworldSprites = {}
for fileName in itemSprites:
with open(Utils.local_path("lib", "worlds", "sm", "data", "custom_sprite", fileName) if Utils.is_frozen() else Utils.local_path("worlds", "sm", "data", "custom_sprite", fileName), 'rb') as stream:
buffer = bytearray(stream.read())
offworldSprites[0x027882 + 10*(21 + idx) + 2] = buffer[0:8]
offworldSprites[0x049100 + idx*256] = buffer[8:264]
idx += 1
openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]}
deathLink = {0x277f04: [int(self.world.death_link[self.player])]}
patchDict = { 'MultiWorldLocations': multiWorldLocations,
'MultiWorldItems': multiWorldItems,
'offworldSprites': offworldSprites,
'openTourianGreyDoors': openTourianGreyDoors,
'deathLink': deathLink}
romPatcher.applyIPSPatchDict(patchDict)
playerNames = {}
for p in range(1, self.world.players + 1):
playerNames[0x1C5000 + (p - 1) * 16] = self.world.player_name[p][:16].upper().center(16).encode()
playerNames[0x1C5000 + (self.world.players) * 16] = "Archipelago".upper().center(16).encode()
romPatcher.applyIPSPatch('PlayerName', { 'PlayerName': playerNames })
# set rom name
# 21 bytes
from Main import __version__
self.romName = bytearray(f'SM{__version__.replace(".", "")[0:3]}_{self.player}_{self.world.seed:11}\0', 'utf8')[:21]
self.romName.extend([0] * (21 - len(self.romName)))
romPatcher.applyIPSPatch('ROMName', { 'ROMName': {0x1C4F00 : self.romName, 0x007FC0 : self.romName} })
startItemROMAddressBase = 0x2FD8B9
# current, base value or bitmask, max, base value or bitmask
startItemROMDict = {'ETank': [0x8, 0x64, 0xA, 0x64],
'Missile': [0xC, 0x5, 0xE, 0x5],
'Super': [0x10, 0x5, 0x12, 0x5],
'PowerBomb': [0x14, 0x5, 0x16, 0x5],
'Reserve': [0x1A, 0x64, 0x18, 0x64],
'Morph': [0x2, 0x4, 0x0, 0x4],
'Bomb': [0x3, 0x10, 0x1, 0x10],
'SpringBall': [0x2, 0x2, 0x0, 0x2],
'HiJump': [0x3, 0x1, 0x1, 0x1],
'Varia': [0x2, 0x1, 0x0, 0x1],
'Gravity': [0x2, 0x20, 0x0, 0x20],
'SpeedBooster': [0x3, 0x20, 0x1, 0x20],
'SpaceJump': [0x3, 0x2, 0x1, 0x2],
'ScrewAttack': [0x2, 0x8, 0x0, 0x8],
'Charge': [0x7, 0x10, 0x5, 0x10],
'Ice': [0x6, 0x2, 0x4, 0x2],
'Wave': [0x6, 0x1, 0x4, 0x1],
'Spazer': [0x6, 0x4, 0x4, 0x4],
'Plasma': [0x6, 0x8, 0x4, 0x8],
'Grapple': [0x3, 0x40, 0x1, 0x40],
'XRayScope': [0x3, 0x80, 0x1, 0x80]
}
mergedData = {}
hasETank = False
hasSpazer = False
hasPlasma = False
for startItem in self.startItems:
item = startItem.Type
if item == 'ETank': hasETank = True
if item == 'Spazer': hasSpazer = True
if item == 'Plasma': hasPlasma = True
if (item in ['ETank', 'Missile', 'Super', 'PowerBomb', 'Reserve']):
(currentValue, currentBase, maxValue, maxBase) = startItemROMDict[item]
if (startItemROMAddressBase + currentValue) in mergedData:
mergedData[startItemROMAddressBase + currentValue] += currentBase
mergedData[startItemROMAddressBase + maxValue] += maxBase
else:
mergedData[startItemROMAddressBase + currentValue] = currentBase
mergedData[startItemROMAddressBase + maxValue] = maxBase
else:
(collected, currentBitmask, equipped, maxBitmask) = startItemROMDict[item]
if (startItemROMAddressBase + collected) in mergedData:
mergedData[startItemROMAddressBase + collected] |= currentBitmask
mergedData[startItemROMAddressBase + equipped] |= maxBitmask
else:
mergedData[startItemROMAddressBase + collected] = currentBitmask
mergedData[startItemROMAddressBase + equipped] = maxBitmask
if hasETank:
mergedData[startItemROMAddressBase + 0x8] += 99
mergedData[startItemROMAddressBase + 0xA] += 99
if hasSpazer and hasPlasma:
mergedData[startItemROMAddressBase + 0x4] &= ~0x4
for key, value in mergedData.items():
if (key - startItemROMAddressBase > 7):
(w0, w1) = self.getWord(value)
mergedData[key] = [w0, w1]
else:
mergedData[key] = [value]
startItemPatch = { 'startItemPatch': mergedData }
romPatcher.applyIPSPatch('startItemPatch', startItemPatch)
romPatcher.commitIPS()
itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.world.get_locations() if itemLoc.player == self.player]
romPatcher.writeItemsLocs(itemLocs)
itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player]
progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player and itemLoc.item.advancement == True]
# progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.world.get_locations() if itemLoc.player == self.player and itemLoc.item.player == self.player and itemLoc.item.advancement == True]
# romPatcher.writeSplitLocs(self.variaRando.args.majorsSplit, itemLocs, progItemLocs)
romPatcher.writeSpoiler(itemLocs, progItemLocs)
romPatcher.writeRandoSettings(self.variaRando.randoExec.randoSettings, itemLocs)
def generate_output(self, output_directory: str):
try:
outfilebase = 'AP_' + self.world.seed_name
outfilepname = f'_P{self.player}'
outfilepname += f"_{self.world.player_name[self.player].replace(' ', '_')}" \
outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
self.variaRando.PatchRom(outputFilename, self.APPatchRom)
self.write_crc(outputFilename)
Patch.create_patch_file(outputFilename, player=self.player, player_name=self.world.player_name[self.player], game=Patch.GAME_SM)
os.unlink(outputFilename)
self.rom_name = self.romName
except:
raise
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def checksum_mirror_sum(self, start, length, mask = 0x800000):
while (not(length & mask) and mask):
mask >>= 1
part1 = sum(start[:mask]) & 0xFFFF
part2 = 0
next_length = length - mask
if next_length:
part2 = self.checksum_mirror_sum(start[mask:], next_length, mask >> 1)
while (next_length < mask):
next_length += next_length
part2 += part2
length = mask + mask
return (part1 + part2) & 0xFFFF
def write_bytes(self, buffer, startaddress: int, values):
buffer[startaddress:startaddress + len(values)] = values
def write_crc(self, romName):
with open(romName, 'rb') as stream:
buffer = bytearray(stream.read())
crc = self.checksum_mirror_sum(buffer, len(buffer))
inv = crc ^ 0xFFFF
self.write_bytes(buffer, 0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF])
with open(romName, 'wb') as outfile:
outfile.write(buffer)
def modify_multidata(self, multidata: dict):
import base64
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
rom_name = getattr(self, "rom_name", None)
# we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name:
new_name = base64.b64encode(bytes(self.rom_name)).decode()
payload = multidata["connect_names"][self.world.player_name[self.player]]
multidata["connect_names"][new_name] = payload
del (multidata["connect_names"][self.world.player_name[self.player]])
def fill_slot_data(self):
slot_data = {}
return slot_data
def collect(self, state: CollectionState, item: Item) -> bool:
state.smbm[item.player].addItem(item.type)
if item.advancement:
state.prog_items[item.name, item.player] += 1
return True # indicate that a logical state change has occured
return False
def create_item(self, name: str) -> Item:
item = next(x for x in ItemManager.Items.values() if x.Name == name)
return SMItem(item.Name, True, item.Type, self.item_name_to_id[item.Name], player = self.player)
def pre_fill(self):
if (self.variaRando.args.morphPlacement == "early") and next((item for item in self.world.itempool if item.player == self.player and item.name == "Morph Ball"), False):
viable = []
for location in self.world.get_locations():
if location.player == self.player \
and location.item is None \
and location.can_reach(self.world.state):
viable.append(location)
self.world.random.shuffle(viable)
key = self.world.create_item("Morph Ball", self.player)
loc = viable.pop()
loc.place_locked_item(key)
self.world.itempool[:] = [item for item in self.world.itempool if
item.player != self.player or
item.name != "Morph Ball"]
def post_fill(self):
# increase maxDifficulty if only bosses is too difficult to beat game
new_state = CollectionState(self.world)
for item in self.world.itempool:
if item.player == self.player:
new_state.collect(item, True)
new_state.sweep_for_events()
if (any(not self.world.get_location(bossLoc, self.player).can_reach(new_state) for bossLoc in self.locked_items)):
if (self.variaRando.randoExec.setup.services.onlyBossesLeft(self.variaRando.randoExec.setup.startAP, self.variaRando.randoExec.setup.container)):
self.world.state.smbm[self.player].maxDiff = infinity
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
restitempool, fill_locations):
if world.get_game_players("Super Metroid"):
progitempool.sort(
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)
def create_locations(self, player: int):
for name, id in locations_lookup_name_to_id.items():
self.locations[name] = SMLocation(player, name, id)
def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, RegionType.LightWorld, name, player)
ret.world = world
if locations:
for loc in locations:
location = self.locations[loc]
location.parent_region = ret
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret
class SMLocation(Location):
game: str = "Super Metroid"
def __init__(self, player: int, name: str, address=None, parent=None):
super(SMLocation, self).__init__(player, name, address, parent)
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
class SMItem(Item):
game = "Super Metroid"
def __init__(self, name, advancement, type, code, player: int = None):
super(SMItem, self).__init__(name, advancement, code, player)
self.type = type

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -0,0 +1,3 @@
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

View File

@ -0,0 +1,413 @@
import copy, logging
from operator import attrgetter
import utils.log
from logic.smbool import SMBool, smboolFalse
from utils.parameters import infinity
from logic.helpers import Bosses
class Path(object):
__slots__ = ( 'path', 'pdiff', 'distance' )
def __init__(self, path, pdiff, distance):
self.path = path
self.pdiff = pdiff
self.distance = distance
class AccessPoint(object):
# name : AccessPoint name
# graphArea : graph area the node is located in
# transitions : intra-area transitions
# traverse: traverse function, will be wand to the added transitions
# exitInfo : dict carrying vanilla door information : 'DoorPtr': door address, 'direction', 'cap', 'screen', 'bitFlag', 'distanceToSpawn', 'doorAsmPtr' : door properties
# entryInfo : dict carrying forced samus X/Y position with keys 'SamusX' and 'SamusY'.
# (to be updated after reading vanillaTransitions and gather entry info from matching exit door)
# roomInfo : dict with 'RoomPtr' : room address, 'area'
# shortName : short name for the credits
# internal : if true, shall not be used for connecting areas
def __init__(self, name, graphArea, transitions,
traverse=lambda sm: SMBool(True),
exitInfo=None, entryInfo=None, roomInfo=None,
internal=False, boss=False, escape=False,
start=None,
dotOrientation='w'):
self.Name = name
self.GraphArea = graphArea
self.ExitInfo = exitInfo
self.EntryInfo = entryInfo
self.RoomInfo = roomInfo
self.Internal = internal
self.Boss = boss
self.Escape = escape
self.Start = start
self.DotOrientation = dotOrientation
self.intraTransitions = self.sortTransitions(transitions)
self.transitions = copy.copy(self.intraTransitions)
self.traverse = traverse
self.distance = 0
# inter-area connection
self.ConnectedTo = None
def __copy__(self):
exitInfo = copy.deepcopy(self.ExitInfo) if self.ExitInfo is not None else None
entryInfo = copy.deepcopy(self.EntryInfo) if self.EntryInfo is not None else None
roomInfo = copy.deepcopy(self.RoomInfo) if self.RoomInfo is not None else None
start = copy.deepcopy(self.Start) if self.Start is not None else None
# in any case, do not copy connections
return AccessPoint(self.Name, self.GraphArea, self.intraTransitions, self.traverse,
exitInfo, entryInfo, roomInfo,
self.Internal, self.Boss, self.Escape,
start, self.DotOrientation)
def __str__(self):
return "[" + self.GraphArea + "] " + self.Name
def __repr__(self):
return self.Name
def sortTransitions(self, transitions=None):
# sort transitions before the loop in getNewAvailNodes.
# as of python3.7 insertion order is guaranteed in dictionaires.
if transitions is None:
transitions = self.transitions
return { key: transitions[key] for key in sorted(transitions.keys()) }
# connect to inter-area access point
def connect(self, destName):
self.disconnect()
if self.Internal is False:
self.transitions[destName] = self.traverse
self.ConnectedTo = destName
else:
raise RuntimeError("Cannot add an internal access point as inter-are transition")
self.transitions = self.sortTransitions()
def disconnect(self):
if self.ConnectedTo is not None:
if self.ConnectedTo not in self.intraTransitions:
del self.transitions[self.ConnectedTo]
else:
self.transitions[self.ConnectedTo] = self.intraTransitions[self.ConnectedTo]
self.ConnectedTo = None
# tells if this node is to connect areas together
def isArea(self):
return not self.Internal and not self.Boss and not self.Escape
# used by the solver to get area and boss APs
def isInternal(self):
return self.Internal or self.Escape
def isLoop(self):
return self.ConnectedTo == self.Name
class AccessGraph(object):
__slots__ = ( 'log', 'accessPoints', 'InterAreaTransitions',
'EscapeAttributes', 'apCache', '_useCache',
'availAccessPoints' )
def __init__(self, accessPointList, transitions, dotFile=None):
self.log = utils.log.get('Graph')
self.accessPoints = {}
self.InterAreaTransitions = []
self.EscapeAttributes = {
'Timer': None,
'Animals': None
}
for ap in accessPointList:
self.addAccessPoint(ap)
for srcName, dstName in transitions:
self.addTransition(srcName, dstName)
if dotFile is not None:
self.toDot(dotFile)
self.apCache = {}
self._useCache = False
# store the avail access points to display in vcr
self.availAccessPoints = {}
def useCache(self, use):
self._useCache = use
if self._useCache:
self.resetCache()
def resetCache(self):
self.apCache = {}
def printGraph(self):
if self.log.getEffectiveLevel() == logging.DEBUG:
self.log("Area graph:")
for s, d in self.InterAreaTransitions:
self.log("{} -> {}".format(s.Name, d.Name))
def addAccessPoint(self, ap):
ap.distance = 0
self.accessPoints[ap.Name] = ap
def toDot(self, dotFile):
colors = ['red', 'blue', 'green', 'yellow', 'skyblue', 'violet', 'orange',
'lawngreen', 'crimson', 'chocolate', 'turquoise', 'tomato',
'navyblue', 'darkturquoise', 'green', 'blue', 'maroon', 'magenta',
'bisque', 'coral', 'chartreuse', 'chocolate', 'cyan']
with open(dotFile, "w") as f:
f.write("digraph {\n")
f.write('size="30,30!";\n')
f.write('rankdir=LR;\n')
f.write('ranksep=2.2;\n')
f.write('overlap=scale;\n')
f.write('edge [dir="both",arrowhead="box",arrowtail="box",arrowsize=0.5,fontsize=7,style=dotted];\n')
f.write('node [shape="box",fontsize=10];\n')
for area in set([ap.GraphArea for ap in self.accessPoints.values()]):
f.write(area + ";\n") # TODO area long name and color
drawn = []
i = 0
for src, dst in self.InterAreaTransitions:
if src.Name in drawn:
continue
f.write('%s:%s -> %s:%s [taillabel="%s",headlabel="%s",color=%s];\n' % (src.GraphArea, src.DotOrientation, dst.GraphArea, dst.DotOrientation, src.Name, dst.Name, colors[i]))
drawn += [src.Name,dst.Name]
i += 1
f.write("}\n")
def addTransition(self, srcName, dstName, both=True):
src = self.accessPoints[srcName]
dst = self.accessPoints[dstName]
src.connect(dstName)
self.InterAreaTransitions.append((src, dst))
if both is True:
self.addTransition(dstName, srcName, False)
# availNodes: all already available nodes
# nodesToCheck: nodes we have to check transitions for
# smbm: smbm to test logic on. if None, discard logic check, assume we can reach everything
# maxDiff: difficulty limit
# return newly opened access points
def getNewAvailNodes(self, availNodes, nodesToCheck, smbm, maxDiff, item=None):
newAvailNodes = {}
# with python >= 3.6 the insertion order in a dict is keeps when looping on the keys,
# so we no longer have to sort them.
for src in nodesToCheck:
for dstName in src.transitions:
dst = self.accessPoints[dstName]
if dst in availNodes or dst in newAvailNodes:
continue
if smbm is not None:
if self._useCache == True and (src, dst, item) in self.apCache:
diff = self.apCache[(src, dst, item)]
else:
tFunc = src.transitions[dstName]
diff = tFunc(smbm)
if self._useCache == True:
self.apCache[(src, dst, item)] = diff
else:
diff = SMBool(True)
if diff.bool and diff.difficulty <= maxDiff:
if src.GraphArea == dst.GraphArea:
dst.distance = src.distance + 0.01
else:
dst.distance = src.distance + 1
newAvailNodes[dst] = { 'difficulty': diff, 'from': src }
#self.log.debug("{} -> {}: {}".format(src.Name, dstName, diff))
return newAvailNodes
# rootNode: starting AccessPoint instance
# smbm: smbm to test logic on. if None, discard logic check, assume we can reach everything
# maxDiff: difficulty limit.
# smbm: if None, discard logic check, assume we can reach everything
# return available AccessPoint list
def getAvailableAccessPoints(self, rootNode, smbm, maxDiff, item=None):
availNodes = { rootNode : { 'difficulty' : SMBool(True, 0), 'from' : None } }
newAvailNodes = availNodes
rootNode.distance = 0
while len(newAvailNodes) > 0:
newAvailNodes = self.getNewAvailNodes(availNodes, newAvailNodes, smbm, maxDiff, item)
availNodes.update(newAvailNodes)
return availNodes
# gets path from the root AP used to compute availAps
def getPath(self, dstAp, availAps):
path = []
root = dstAp
while root != None:
path = [root] + path
root = availAps[root]['from']
return path
def getAvailAPPaths(self, availAccessPoints, locsAPs):
paths = {}
for ap in availAccessPoints:
if ap.Name in locsAPs:
path = self.getPath(ap, availAccessPoints)
pdiff = SMBool.wandmax(*(availAccessPoints[ap]['difficulty'] for ap in path))
paths[ap.Name] = Path(path, pdiff, len(path))
return paths
def getSortedAPs(self, paths, locAccessFrom):
ret = []
for apName in locAccessFrom:
path = paths.get(apName, None)
if path is None:
continue
difficulty = paths[apName].pdiff.difficulty
ret.append((difficulty if difficulty != -1 else infinity, path.distance, apName))
ret.sort()
return [apName for diff, dist, apName in ret]
# locations: locations to check
# items: collected items
# maxDiff: difficulty limit
# rootNode: starting AccessPoint
# return available locations list, also stores difficulty in locations
def getAvailableLocations(self, locations, smbm, maxDiff, rootNode='Landing Site'):
rootAp = self.accessPoints[rootNode]
self.availAccessPoints = self.getAvailableAccessPoints(rootAp, smbm, maxDiff)
availAreas = set([ap.GraphArea for ap in self.availAccessPoints.keys()])
availLocs = []
# get all the current locations APs first to only compute these paths
locsAPs = set()
for loc in locations:
for ap in loc.AccessFrom:
locsAPs.add(ap)
# sort availAccessPoints based on difficulty to take easier paths first
availAPPaths = self.getAvailAPPaths(self.availAccessPoints, locsAPs)
for loc in locations:
if loc.GraphArea not in availAreas:
loc.distance = 30000
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} locDiff is area nok".format(loc.Name))
continue
locAPs = self.getSortedAPs(availAPPaths, loc.AccessFrom)
if len(locAPs) == 0:
loc.distance = 40000
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} no aps".format(loc.Name))
continue
for apName in locAPs:
if apName == None:
loc.distance = 20000
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} ap is none".format(loc.Name))
break
tFunc = loc.AccessFrom[apName]
ap = self.accessPoints[apName]
tdiff = tFunc(smbm)
#if loc.Name == "Kraid":
# print("{} root: {} ap: {}".format(loc.Name, rootNode, apName))
if tdiff.bool == True and tdiff.difficulty <= maxDiff:
diff = loc.Available(smbm)
if diff.bool == True:
path = availAPPaths[apName].path
#if loc.Name == "Kraid":
# print("{} path: {}".format(loc.Name, [a.Name for a in path]))
pdiff = availAPPaths[apName].pdiff
(allDiff, locDiff) = self.computeLocDiff(tdiff, diff, pdiff)
if allDiff.bool == True and allDiff.difficulty <= maxDiff:
loc.distance = ap.distance + 1
loc.accessPoint = apName
loc.difficulty = allDiff
loc.path = path
# used only by solver
loc.pathDifficulty = pdiff
loc.locDifficulty = locDiff
availLocs.append(loc)
#if loc.Name == "Kraid":
# print("{} diff: {} tdiff: {} pdiff: {}".format(loc.Name, diff, tdiff, pdiff))
break
else:
loc.distance = 1000 + tdiff.difficulty
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} allDiff is false".format(loc.Name))
else:
loc.distance = 1000 + tdiff.difficulty
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} allDiff is false".format(loc.Name))
else:
loc.distance = 10000 + tdiff.difficulty
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {} tdiff is false".format(loc.Name))
if loc.difficulty is None:
#if loc.Name == "Kraid":
# print("loc: {} no difficulty in loc".format(loc.Name))
loc.distance = 100000
loc.difficulty = smboolFalse
#if loc.Name == "Kraid":
# print("loc: {}: {}".format(loc.Name, loc))
#print("availableLocs: {}".format([loc.Name for loc in availLocs]))
return availLocs
# test access from an access point to another, given an optional item
def canAccess(self, smbm, srcAccessPointName, destAccessPointName, maxDiff, item=None):
if item is not None:
smbm.addItem(item)
#print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName))
destAccessPoint = self.accessPoints[destAccessPointName]
srcAccessPoint = self.accessPoints[srcAccessPointName]
availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff, item)
can = destAccessPoint in availAccessPoints
# if not can:
# self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()]))
if item is not None:
smbm.removeItem(item)
#print("canAccess: {}".format(can))
return can
# returns a list of AccessPoint instances from srcAccessPointName to destAccessPointName
# (not including source ap)
# or None if no possible path
def accessPath(self, smbm, srcAccessPointName, destAccessPointName, maxDiff):
destAccessPoint = self.accessPoints[destAccessPointName]
srcAccessPoint = self.accessPoints[srcAccessPointName]
availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff)
if destAccessPoint not in availAccessPoints:
return None
return self.getPath(destAccessPoint, availAccessPoints)
# gives theoretically accessible APs in the graph (no logic check)
def getAccessibleAccessPoints(self, rootNode='Landing Site'):
rootAp = self.accessPoints[rootNode]
inBossChk = lambda ap: ap.Boss and ap.Name.endswith("In")
allAreas = {dst.GraphArea for (src, dst) in self.InterAreaTransitions if not inBossChk(dst) and not dst.isLoop()}
self.log.debug("allAreas="+str(allAreas))
nonBossAPs = [ap for ap in self.getAvailableAccessPoints(rootAp, None, 0) if ap.GraphArea in allAreas]
bossesAPs = [self.accessPoints[boss+'RoomIn'] for boss in Bosses.Golden4()] + [self.accessPoints['Draygon Room Bottom']]
return nonBossAPs + bossesAPs
# gives theoretically accessible locations within a base list
# returns locations with accessible GraphArea in this graph (no logic considered)
def getAccessibleLocations(self, locations, rootNode='Landing Site'):
availAccessPoints = self.getAccessibleAccessPoints(rootNode)
self.log.debug("availAccessPoints="+str([ap.Name for ap in availAccessPoints]))
return [loc for loc in locations if any(ap.Name in loc.AccessFrom for ap in availAccessPoints)]
class AccessGraphSolver(AccessGraph):
def computeLocDiff(self, tdiff, diff, pdiff):
# tdiff: difficulty from the location's access point to the location's room
# diff: difficulty to reach the item in the location's room
# pdiff: difficulty of the path from the current access point to the location's access point
# in output we need the global difficulty but we also need to separate pdiff and (tdiff + diff)
locDiff = SMBool.wandmax(tdiff, diff)
allDiff = SMBool.wandmax(locDiff, pdiff)
return (allDiff, locDiff)
class AccessGraphRando(AccessGraph):
def computeLocDiff(self, tdiff, diff, pdiff):
allDiff = SMBool.wandmax(tdiff, diff, pdiff)
return (allDiff, None)

View File

@ -0,0 +1,575 @@
import copy
import random
from logic.logic import Logic
from utils.parameters import Knows
from graph.location import locationsDict
from rom.rom import snes_to_pc
import utils.log
# order expected by ROM patches
graphAreas = [
"Ceres",
"Crateria",
"GreenPinkBrinstar",
"RedBrinstar",
"WreckedShip",
"Kraid",
"Norfair",
"Crocomire",
"LowerNorfair",
"WestMaridia",
"EastMaridia",
"Tourian"
]
vanillaTransitions = [
('Lower Mushrooms Left', 'Green Brinstar Elevator'),
('Morph Ball Room Left', 'Green Hill Zone Top Right'),
('Moat Right', 'West Ocean Left'),
('Keyhunter Room Bottom', 'Red Brinstar Elevator'),
('Noob Bridge Right', 'Red Tower Top Left'),
('Crab Maze Left', 'Le Coude Right'),
('Kronic Boost Room Bottom Left', 'Lava Dive Right'),
('Crocomire Speedway Bottom', 'Crocomire Room Top'),
('Three Muskateers Room Left', 'Single Chamber Top Right'),
('Warehouse Entrance Left', 'East Tunnel Right'),
('East Tunnel Top Right', 'Crab Hole Bottom Left'),
('Caterpillar Room Top Right', 'Red Fish Room Left'),
('Glass Tunnel Top', 'Main Street Bottom'),
('Green Pirates Shaft Bottom Right', 'Golden Four'),
('Warehouse Entrance Right', 'Warehouse Zeela Room Left'),
('Crab Shaft Right', 'Aqueduct Top Left')
]
vanillaBossesTransitions = [
('KraidRoomOut', 'KraidRoomIn'),
('PhantoonRoomOut', 'PhantoonRoomIn'),
('DraygonRoomOut', 'DraygonRoomIn'),
('RidleyRoomOut', 'RidleyRoomIn')
]
# vanilla escape transition in first position
vanillaEscapeTransitions = [
('Tourian Escape Room 4 Top Right', 'Climb Bottom Left'),
('Brinstar Pre-Map Room Right', 'Green Brinstar Main Shaft Top Left'),
('Wrecked Ship Map Room', 'Basement Left'),
('Norfair Map Room', 'Business Center Mid Left'),
('Maridia Map Room', 'Crab Hole Bottom Right')
]
vanillaEscapeAnimalsTransitions = [
('Flyway Right 0', 'Bomb Torizo Room Left'),
('Flyway Right 1', 'Bomb Torizo Room Left'),
('Flyway Right 2', 'Bomb Torizo Room Left'),
('Flyway Right 3', 'Bomb Torizo Room Left'),
('Bomb Torizo Room Left Animals', 'Flyway Right')
]
escapeSource = 'Tourian Escape Room 4 Top Right'
escapeTargets = ['Green Brinstar Main Shaft Top Left', 'Basement Left', 'Business Center Mid Left', 'Crab Hole Bottom Right']
locIdsByAreaAddresses = {
"Ceres": snes_to_pc(0xA1F568),
"Crateria": snes_to_pc(0xA1F569),
"GreenPinkBrinstar": snes_to_pc(0xA1F57B),
"RedBrinstar": snes_to_pc(0xA1F58C),
"WreckedShip": snes_to_pc(0xA1F592),
"Kraid": snes_to_pc(0xA1F59E),
"Norfair": snes_to_pc(0xA1F5A2),
"Crocomire": snes_to_pc(0xA1F5B2),
"LowerNorfair": snes_to_pc(0xA1F5B8),
"WestMaridia": snes_to_pc(0xA1F5C3),
"EastMaridia": snes_to_pc(0xA1F5CB),
"Tourian": snes_to_pc(0xA1F5D7)
}
def getAccessPoint(apName, apList=None):
if apList is None:
apList = Logic.accessPoints
return next(ap for ap in apList if ap.Name == apName)
class GraphUtils:
log = utils.log.get('GraphUtils')
def getStartAccessPointNames():
return [ap.Name for ap in Logic.accessPoints if ap.Start is not None]
def getStartAccessPointNamesCategory():
ret = {'regular': [], 'custom': [], 'area': []}
for ap in Logic.accessPoints:
if ap.Start == None:
continue
elif 'areaMode' in ap.Start and ap.Start['areaMode'] == True:
ret['area'].append(ap.Name)
elif GraphUtils.isStandardStart(ap.Name):
ret['regular'].append(ap.Name)
else:
ret['custom'].append(ap.Name)
return ret
def isStandardStart(startApName):
return startApName == 'Ceres' or startApName == 'Landing Site'
def getPossibleStartAPs(areaMode, maxDiff, morphPlacement, player):
ret = []
refused = {}
allStartAPs = GraphUtils.getStartAccessPointNames()
for apName in allStartAPs:
start = getAccessPoint(apName).Start
ok = True
cause = ""
if 'knows' in start:
for k in start['knows']:
if not Knows.knowsDict[player].knows(k, maxDiff):
ok = False
cause += Knows.desc[k]['display']+" is not known. "
break
if 'areaMode' in start and start['areaMode'] != areaMode:
ok = False
cause += "Start location available only with area randomization enabled. "
if 'forcedEarlyMorph' in start and start['forcedEarlyMorph'] == True and morphPlacement == 'late':
ok = False
cause += "Start location unavailable with late morph placement. "
if ok:
ret.append(apName)
else:
refused[apName] = cause
return ret, refused
def updateLocClassesStart(startGraphArea, split, possibleMajLocs, preserveMajLocs, nLocs):
locs = locationsDict
preserveMajLocs = [locs[locName] for locName in preserveMajLocs if locs[locName].isClass(split)]
possLocs = [locs[locName] for locName in possibleMajLocs][:nLocs]
GraphUtils.log.debug("possLocs="+str([loc.Name for loc in possLocs]))
candidates = [loc for loc in locs.values() if loc.GraphArea == startGraphArea and loc.isClass(split) and loc not in preserveMajLocs]
remLocs = [loc for loc in locs.values() if loc not in possLocs and loc not in candidates and loc.isClass(split)]
newLocs = []
while len(newLocs) < nLocs:
if len(candidates) == 0:
candidates = remLocs
loc = possLocs.pop(random.randint(0,len(possLocs)-1))
newLocs.append(loc)
loc.setClass([split])
if not loc in preserveMajLocs:
GraphUtils.log.debug("newMajor="+loc.Name)
loc = candidates.pop(random.randint(0,len(candidates)-1))
loc.setClass(["Minor"])
GraphUtils.log.debug("replaced="+loc.Name)
def getGraphPatches(startApName):
ap = getAccessPoint(startApName)
return ap.Start['patches'] if 'patches' in ap.Start else []
def createBossesTransitions():
transitions = vanillaBossesTransitions
def isVanilla():
for t in vanillaBossesTransitions:
if t not in transitions:
return False
return True
while isVanilla():
transitions = []
srcs = []
dsts = []
for (src,dst) in vanillaBossesTransitions:
srcs.append(src)
dsts.append(dst)
while len(srcs) > 0:
src = srcs.pop(random.randint(0,len(srcs)-1))
dst = dsts.pop(random.randint(0,len(dsts)-1))
transitions.append((src,dst))
return transitions
def createAreaTransitions(lightAreaRando=False):
if lightAreaRando:
return GraphUtils.createLightAreaTransitions()
else:
return GraphUtils.createRegularAreaTransitions()
def createRegularAreaTransitions(apList=None, apPred=None):
if apList is None:
apList = Logic.accessPoints
if apPred is None:
apPred = lambda ap: ap.isArea()
tFrom = []
tTo = []
apNames = [ap.Name for ap in apList if apPred(ap) == True]
transitions = []
def findTo(trFrom):
ap = getAccessPoint(trFrom, apList)
fromArea = ap.GraphArea
targets = [apName for apName in apNames if apName not in tTo and getAccessPoint(apName, apList).GraphArea != fromArea]
if len(targets) == 0: # fallback if no area transition is found
targets = [apName for apName in apNames if apName != ap.Name]
if len(targets) == 0: # extreme fallback: loop on itself
targets = [ap.Name]
return random.choice(targets)
def addTransition(src, dst):
tFrom.append(src)
tTo.append(dst)
while len(apNames) > 0:
sources = [apName for apName in apNames if apName not in tFrom]
src = random.choice(sources)
dst = findTo(src)
transitions.append((src, dst))
addTransition(src, dst)
addTransition(dst, src)
toRemove = [apName for apName in apNames if apName in tFrom and apName in tTo]
for apName in toRemove:
apNames.remove(apName)
return transitions
def getAPs(apPredicate, apList=None):
if apList is None:
apList = Logic.accessPoints
return [ap for ap in apList if apPredicate(ap) == True]
def loopUnusedTransitions(transitions, apList=None):
if apList is None:
apList = Logic.accessPoints
usedAPs = set()
for (src,dst) in transitions:
usedAPs.add(getAccessPoint(src, apList))
usedAPs.add(getAccessPoint(dst, apList))
unusedAPs = [ap for ap in apList if not ap.isInternal() and ap not in usedAPs]
for ap in unusedAPs:
transitions.append((ap.Name, ap.Name))
def createMinimizerTransitions(startApName, locLimit):
if startApName == 'Ceres':
startApName = 'Landing Site'
startAp = getAccessPoint(startApName)
def getNLocs(locsPredicate, locList=None):
if locList is None:
locList = Logic.locations
# leave out bosses and count post boss locs systematically
return len([loc for loc in locList if locsPredicate(loc) == True and not loc.SolveArea.endswith(" Boss") and not loc.isBoss()])
availAreas = list(sorted({ap.GraphArea for ap in Logic.accessPoints if ap.GraphArea != startAp.GraphArea and getNLocs(lambda loc: loc.GraphArea == ap.GraphArea) > 0}))
areas = [startAp.GraphArea]
GraphUtils.log.debug("availAreas: {}".format(availAreas))
GraphUtils.log.debug("areas: {}".format(areas))
inBossCheck = lambda ap: ap.Boss and ap.Name.endswith("In")
nLocs = 0
transitions = []
usedAPs = []
trLimit = 5
locLimit -= 3 # 3 "post boss" locs will always be available, and are filtered out in getNLocs
def openTransitions():
nonlocal areas, inBossCheck, usedAPs
return GraphUtils.getAPs(lambda ap: ap.GraphArea in areas and not ap.isInternal() and not inBossCheck(ap) and not ap in usedAPs)
while nLocs < locLimit or len(openTransitions()) < trLimit:
GraphUtils.log.debug("openTransitions="+str([ap.Name for ap in openTransitions()]))
fromAreas = availAreas
if nLocs >= locLimit:
GraphUtils.log.debug("not enough open transitions")
# we just need transitions, avoid adding a huge area
fromAreas = []
n = trLimit - len(openTransitions())
while len(fromAreas) == 0:
fromAreas = [area for area in availAreas if len(GraphUtils.getAPs(lambda ap: not ap.isInternal())) > n]
n -= 1
minLocs = min([getNLocs(lambda loc: loc.GraphArea == area) for area in fromAreas])
fromAreas = [area for area in fromAreas if getNLocs(lambda loc: loc.GraphArea == area) == minLocs]
elif len(openTransitions()) <= 1: # dont' get stuck by adding dead ends
fromAreas = [area for area in fromAreas if len(GraphUtils.getAPs(lambda ap: ap.GraphArea == area and not ap.isInternal())) > 1]
nextArea = random.choice(fromAreas)
GraphUtils.log.debug("nextArea="+str(nextArea))
apCheck = lambda ap: not ap.isInternal() and not inBossCheck(ap) and ap not in usedAPs
possibleSources = GraphUtils.getAPs(lambda ap: ap.GraphArea in areas and apCheck(ap))
possibleTargets = GraphUtils.getAPs(lambda ap: ap.GraphArea == nextArea and apCheck(ap))
src = random.choice(possibleSources)
dst = random.choice(possibleTargets)
usedAPs += [src,dst]
GraphUtils.log.debug("add transition: (src: {}, dst: {})".format(src.Name, dst.Name))
transitions.append((src.Name,dst.Name))
availAreas.remove(nextArea)
areas.append(nextArea)
GraphUtils.log.debug("areas: {}".format(areas))
nLocs = getNLocs(lambda loc:loc.GraphArea in areas)
GraphUtils.log.debug("nLocs: {}".format(nLocs))
# we picked the areas, add transitions (bosses and tourian first)
sourceAPs = openTransitions()
random.shuffle(sourceAPs)
targetAPs = GraphUtils.getAPs(lambda ap: (inBossCheck(ap) or ap.Name == "Golden Four") and not ap in usedAPs)
random.shuffle(targetAPs)
assert len(sourceAPs) >= len(targetAPs), "Minimizer: less source than target APs"
while len(targetAPs) > 0:
transitions.append((sourceAPs.pop().Name, targetAPs.pop().Name))
transitions += GraphUtils.createRegularAreaTransitions(sourceAPs, lambda ap: not ap.isInternal())
GraphUtils.log.debug("FINAL MINIMIZER transitions: {}".format(transitions))
GraphUtils.loopUnusedTransitions(transitions)
GraphUtils.log.debug("FINAL MINIMIZER nLocs: "+str(nLocs+3))
GraphUtils.log.debug("FINAL MINIMIZER areas: "+str(areas))
return transitions
def createLightAreaTransitions():
# group APs by area
aps = {}
totalCount = 0
for ap in Logic.accessPoints:
if not ap.isArea():
continue
if not ap.GraphArea in aps:
aps[ap.GraphArea] = {'totalCount': 0, 'transCount': {}, 'apNames': []}
aps[ap.GraphArea]['apNames'].append(ap.Name)
# count number of vanilla transitions between each area
for (srcName, destName) in vanillaTransitions:
srcAP = getAccessPoint(srcName)
destAP = getAccessPoint(destName)
aps[srcAP.GraphArea]['transCount'][destAP.GraphArea] = aps[srcAP.GraphArea]['transCount'].get(destAP.GraphArea, 0) + 1
aps[srcAP.GraphArea]['totalCount'] += 1
aps[destAP.GraphArea]['transCount'][srcAP.GraphArea] = aps[destAP.GraphArea]['transCount'].get(srcAP.GraphArea, 0) + 1
aps[destAP.GraphArea]['totalCount'] += 1
totalCount += 1
transitions = []
while totalCount > 0:
# choose transition
srcArea = random.choice(list(aps.keys()))
srcName = random.choice(aps[srcArea]['apNames'])
src = getAccessPoint(srcName)
destArea = random.choice(list(aps[src.GraphArea]['transCount'].keys()))
destName = random.choice(aps[destArea]['apNames'])
transitions.append((srcName, destName))
# update counts
totalCount -= 1
aps[srcArea]['totalCount'] -= 1
aps[destArea]['totalCount'] -= 1
aps[srcArea]['transCount'][destArea] -= 1
if aps[srcArea]['transCount'][destArea] == 0:
del aps[srcArea]['transCount'][destArea]
aps[destArea]['transCount'][srcArea] -= 1
if aps[destArea]['transCount'][srcArea] == 0:
del aps[destArea]['transCount'][srcArea]
aps[srcArea]['apNames'].remove(srcName)
aps[destArea]['apNames'].remove(destName)
if aps[srcArea]['totalCount'] == 0:
del aps[srcArea]
if aps[destArea]['totalCount'] == 0:
del aps[destArea]
return transitions
def getVanillaExit(apName):
allVanillaTransitions = vanillaTransitions + vanillaBossesTransitions + vanillaEscapeTransitions
for (src,dst) in allVanillaTransitions:
if apName == src:
return dst
if apName == dst:
return src
return None
def isEscapeAnimals(apName):
return 'Flyway Right' in apName or 'Bomb Torizo Room Left' in apName
# gets dict like
# (RoomPtr, (vanilla entry screen X, vanilla entry screen Y)): AP
def getRooms():
rooms = {}
for ap in Logic.accessPoints:
if ap.Internal == True:
continue
# special ap for random escape animals surprise
if GraphUtils.isEscapeAnimals(ap.Name):
continue
roomPtr = ap.RoomInfo['RoomPtr']
vanillaExitName = GraphUtils.getVanillaExit(ap.Name)
# special ap for random escape animals surprise
if GraphUtils.isEscapeAnimals(vanillaExitName):
continue
connAP = getAccessPoint(vanillaExitName)
entryInfo = connAP.ExitInfo
rooms[(roomPtr, entryInfo['screen'], entryInfo['direction'])] = ap
rooms[(roomPtr, entryInfo['screen'], (ap.EntryInfo['SamusX'], ap.EntryInfo['SamusY']))] = ap
# for boss rando with incompatible ridley transition, also register this one
if ap.Name == 'RidleyRoomIn':
rooms[(roomPtr, (0x0, 0x1), 0x5)] = ap
rooms[(roomPtr, (0x0, 0x1), (0xbf, 0x198))] = ap
return rooms
def escapeAnimalsTransitions(graph, possibleTargets, firstEscape):
n = len(possibleTargets)
assert (n < 4 and firstEscape is not None) or (n <= 4 and firstEscape is None), "Invalid possibleTargets list: " + str(possibleTargets)
# first get our list of 4 entries for escape patch
if n >= 1:
# get actual animals: pick one of the remaining targets
animalsAccess = possibleTargets.pop()
graph.EscapeAttributes['Animals'] = animalsAccess
# we now have at most 3 targets left, fill up to fill cycling 4 targets for animals suprise
possibleTargets.append('Climb Bottom Left')
if firstEscape is not None:
possibleTargets.append(firstEscape)
poss = possibleTargets[:]
while len(possibleTargets) < 4:
possibleTargets.append(poss.pop(random.randint(0, len(poss)-1)))
else:
# failsafe: if not enough targets left, abort and do vanilla animals
animalsAccess = 'Flyway Right'
possibleTargets = ['Bomb Torizo Room Left'] * 4
GraphUtils.log.debug("escapeAnimalsTransitions. animalsAccess="+animalsAccess)
assert len(possibleTargets) == 4, "Invalid possibleTargets list: " + str(possibleTargets)
# actually add the 4 connections for successive escapes challenge
basePtr = 0xADAC
btDoor = getAccessPoint('Flyway Right')
for i in range(len(possibleTargets)):
ap = copy.copy(btDoor)
ap.Name += " " + str(i)
ap.ExitInfo['DoorPtr'] = basePtr + i*24
graph.addAccessPoint(ap)
target = possibleTargets[i]
graph.addTransition(ap.Name, target)
# add the connection for animals access
bt = getAccessPoint('Bomb Torizo Room Left')
btCpy = copy.copy(bt)
btCpy.Name += " Animals"
btCpy.ExitInfo['DoorPtr'] = 0xAE00
graph.addAccessPoint(btCpy)
graph.addTransition(animalsAccess, btCpy.Name)
def isHorizontal(dir):
# up: 0x3, 0x7
# down: 0x2, 0x6
# left: 0x1, 0x5
# right: 0x0, 0x4
return dir in [0x1, 0x5, 0x0, 0x4]
def removeCap(dir):
if dir < 4:
return dir
return dir - 4
def getDirection(src, dst):
exitDir = src.ExitInfo['direction']
entryDir = dst.EntryInfo['direction']
# compatible transition
if exitDir == entryDir:
return exitDir
# if incompatible but horizontal we keep entry dir (looks more natural)
if GraphUtils.isHorizontal(exitDir) and GraphUtils.isHorizontal(entryDir):
return entryDir
# otherwise keep exit direction and remove cap
return GraphUtils.removeCap(exitDir)
def getBitFlag(srcArea, dstArea, origFlag):
flags = origFlag
if srcArea == dstArea:
flags &= 0xBF
else:
flags |= 0x40
return flags
def getDoorConnections(graph, areas=True, bosses=False,
escape=True, escapeAnimals=True):
transitions = []
if areas:
transitions += vanillaTransitions
if bosses:
transitions += vanillaBossesTransitions
if escape:
transitions += vanillaEscapeTransitions
if escapeAnimals:
transitions += vanillaEscapeAnimalsTransitions
for srcName, dstName in transitions:
src = graph.accessPoints[srcName]
dst = graph.accessPoints[dstName]
dst.EntryInfo.update(src.ExitInfo)
src.EntryInfo.update(dst.ExitInfo)
connections = []
for src, dst in graph.InterAreaTransitions:
if not (escape and src.Escape and dst.Escape):
# area only
if not bosses and src.Boss:
continue
# boss only
if not areas and not src.Boss:
continue
# no random escape
if not escape and src.Escape:
continue
conn = {}
conn['ID'] = str(src) + ' -> ' + str(dst)
# remove duplicates (loop transitions)
if any(c['ID'] == conn['ID'] for c in connections):
continue
# print(conn['ID'])
# where to write
conn['DoorPtr'] = src.ExitInfo['DoorPtr']
# door properties
conn['RoomPtr'] = dst.RoomInfo['RoomPtr']
conn['doorAsmPtr'] = dst.EntryInfo['doorAsmPtr']
if 'exitAsmPtr' in src.ExitInfo:
conn['exitAsmPtr'] = src.ExitInfo['exitAsmPtr']
conn['direction'] = GraphUtils.getDirection(src, dst)
conn['bitFlag'] = GraphUtils.getBitFlag(src.RoomInfo['area'], dst.RoomInfo['area'],
dst.EntryInfo['bitFlag'])
conn['cap'] = dst.EntryInfo['cap']
conn['screen'] = dst.EntryInfo['screen']
if conn['direction'] != src.ExitInfo['direction']: # incompatible transition
conn['distanceToSpawn'] = 0
conn['SamusX'] = dst.EntryInfo['SamusX']
conn['SamusY'] = dst.EntryInfo['SamusY']
if dst.Name == 'RidleyRoomIn': # special case: spawn samus on ridley platform
conn['screen'] = (0x0, 0x1)
else:
conn['distanceToSpawn'] = dst.EntryInfo['distanceToSpawn']
if 'song' in dst.EntryInfo:
conn['song'] = dst.EntryInfo['song']
conn['songs'] = dst.RoomInfo['songs']
connections.append(conn)
return connections
def getDoorsPtrs2Aps():
ret = {}
for ap in Logic.accessPoints:
if ap.Internal == True:
continue
ret[ap.ExitInfo["DoorPtr"]] = ap.Name
return ret
def getAps2DoorsPtrs():
ret = {}
for ap in Logic.accessPoints:
if ap.Internal == True:
continue
ret[ap.Name] = ap.ExitInfo["DoorPtr"]
return ret
def getTransitions(addresses):
# build address -> name dict
doorsPtrs = GraphUtils.getDoorsPtrs2Aps()
transitions = []
# (src.ExitInfo['DoorPtr'], dst.ExitInfo['DoorPtr'])
for (srcDoorPtr, destDoorPtr) in addresses:
transitions.append((doorsPtrs[srcDoorPtr], doorsPtrs[destDoorPtr]))
return transitions
def hasMixedTransitions(areaTransitions, bossTransitions):
vanillaAPs = []
for (src, dest) in vanillaTransitions:
vanillaAPs += [src, dest]
vanillaBossesAPs = []
for (src, dest) in vanillaBossesTransitions:
vanillaBossesAPs += [src, dest]
for (src, dest) in areaTransitions:
if src in vanillaBossesAPs or dest in vanillaBossesAPs:
return True
for (src, dest) in bossTransitions:
if src in vanillaAPs or dest in vanillaAPs:
return True
return False

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,758 @@
from graph.graph import AccessPoint
from utils.parameters import Settings
from rom.rom_patches import RomPatches
from logic.smbool import SMBool
from logic.helpers import Bosses
from logic.cache import Cache
# all access points and traverse functions
accessPoints = [
### Ceres Station
AccessPoint('Ceres', 'Ceres', {
'Landing Site': lambda sm: SMBool(True)
}, internal=True,
start={'spawn': 0xfffe, 'doors':[0x32], 'patches':[RomPatches.BlueBrinstarBlueDoor], 'solveArea': "Crateria Landing Site"}),
### Crateria and Blue Brinstar
AccessPoint('Landing Site', 'Crateria', {
'Lower Mushrooms Left': Cache.ldeco(lambda sm: sm.wand(sm.canPassTerminatorBombWall(),
sm.canPassCrateriaGreenPirates())),
'Keyhunter Room Bottom': Cache.ldeco(lambda sm: sm.traverse('LandingSiteRight')),
'Blue Brinstar Elevator Bottom': lambda sm: SMBool(True)
}, internal=True,
start={'spawn': 0x0000, 'doors':[0x32], 'patches':[RomPatches.BlueBrinstarBlueDoor], 'solveArea': "Crateria Landing Site"}),
AccessPoint('Blue Brinstar Elevator Bottom', 'Crateria', {
'Morph Ball Room Left': lambda sm: sm.canUsePowerBombs(),
'Landing Site': lambda sm: SMBool(True)
}, internal=True),
AccessPoint('Gauntlet Top', 'Crateria', {
'Green Pirates Shaft Bottom Right': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'), sm.canPassCrateriaGreenPirates()))
}, internal=True,
start={'spawn': 0x0006, 'solveArea': "Crateria Gauntlet", 'save':"Save_Gauntlet", 'forcedEarlyMorph':True}),
AccessPoint('Lower Mushrooms Left', 'Crateria', {
'Landing Site': Cache.ldeco(lambda sm: sm.wand(sm.canPassTerminatorBombWall(False),
sm.canPassCrateriaGreenPirates())),
'Green Pirates Shaft Bottom Right': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0x9969, "area": 0x0, 'songs':[0x997a]},
exitInfo = {'DoorPtr':0x8c22, 'direction': 0x5, "cap": (0xe, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x36, 'SamusY':0x88, 'song': 0x9},
dotOrientation = 'nw'),
AccessPoint('Green Pirates Shaft Bottom Right', 'Crateria', {
'Lower Mushrooms Left': lambda sm: SMBool(True)
}, traverse = Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoMoreBlueDoors),
sm.traverse('GreenPiratesShaftBottomRight'))),
roomInfo = {'RoomPtr':0x99bd, "area": 0x0, 'songs':[0x99ce]},
# the doorAsmPtr 7FE00 is set by the g4_skip.ips patch, we have to call it
exitInfo = {'DoorPtr':0x8c52, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xfe00},
entryInfo = {'SamusX':0xcc, 'SamusY':0x688, 'song': 0x9},
dotOrientation = 'e'),
AccessPoint('Moat Right', 'Crateria', {
'Moat Left': lambda sm: sm.canPassMoatReverse()
}, roomInfo = {'RoomPtr':0x95ff, "area": 0x0, 'songs':[0x9610]},
exitInfo = {'DoorPtr':0x8aea, 'direction': 0x4, "cap": (0x1, 0x46), "bitFlag": 0x0,
"screen": (0x0, 0x4), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x1cf, 'SamusY':0x88, 'song': 0xc},
dotOrientation = 'ne'),
AccessPoint('Moat Left', 'Crateria', {
'Keyhunter Room Bottom': lambda sm: SMBool(True),
'Moat Right': lambda sm: sm.canPassMoatFromMoat()
}, internal=True),
AccessPoint('Keyhunter Room Bottom', 'Crateria', {
'Moat Left': Cache.ldeco(lambda sm: sm.traverse('KihunterRight')),
'Moat Right': Cache.ldeco(lambda sm: sm.wand(sm.traverse('KihunterRight'), sm.canPassMoat())),
'Landing Site': lambda sm: SMBool(True)
}, traverse = Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoMoreBlueDoors),
sm.traverse('KihunterBottom'))),
roomInfo = { 'RoomPtr':0x948c, "area": 0x0, 'songs':[0x949d] },
exitInfo = {'DoorPtr':0x8a42, 'direction': 0x6, "cap": (0x6, 0x2), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x14c, 'SamusY':0x2b8, 'song': 0xc},
dotOrientation = 'se'),
AccessPoint('Morph Ball Room Left', 'Crateria', {
'Blue Brinstar Elevator Bottom': lambda sm: sm.canUsePowerBombs()
}, roomInfo = { 'RoomPtr':0x9e9f, "area": 0x1},
exitInfo = {'DoorPtr':0x8e9e, 'direction': 0x5, "cap": (0x1e, 0x6), "bitFlag": 0x0,
"screen": (0x1, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x34, 'SamusY':0x288},
dotOrientation = 'sw'),
# Escape APs
AccessPoint('Climb Bottom Left', 'Crateria', {
'Landing Site': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0x96ba, "area": 0x0},
exitInfo = {'DoorPtr':0x8b6e, 'direction': 0x5, "cap": (0x2e, 0x16), "bitFlag": 0x0,
"screen": (0x2, 0x1), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x34, 'SamusY':0x888},
escape = True,
dotOrientation = 'ne'),
AccessPoint('Flyway Right', 'Crateria', {},
roomInfo = {'RoomPtr':0x9879, "area": 0x0},
exitInfo = {'DoorPtr':0x8bc2, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000,
"exitAsmPtr": 0xf030}, # setup_next_escape in rando_escape.asm
entryInfo = {'SamusX':0xffff, 'SamusY':0xffff}, # unused
escape = True),
AccessPoint('Bomb Torizo Room Left', 'Crateria', {},
roomInfo = {'RoomPtr':0x9804, "area": 0x0},
exitInfo = {'DoorPtr':0x8baa, 'direction': 0x5, "cap": (0x2e, 0x6), "bitFlag": 0x0,
"screen": (0x2, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x34, 'SamusY':0xb8},
escape = True),
### Green and Pink Brinstar
AccessPoint('Green Brinstar Elevator', 'GreenPinkBrinstar', {
'Big Pink': Cache.ldeco(lambda sm: sm.wand(sm.canPassDachoraRoom(),
sm.traverse('MainShaftBottomRight'))),
'Etecoons Bottom': lambda sm: sm.canAccessEtecoons()
}, roomInfo = {'RoomPtr':0x9938, "area": 0x0},
exitInfo = {'DoorPtr':0x8bfe, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xcc, 'SamusY':0x88},
start = {'spawn': 0x0108, 'doors':[0x1f, 0x21, 0x26], 'patches':[RomPatches.BrinReserveBlueDoors], 'solveArea': "Green Brinstar"}, # XXX test if it would be better in brin reserve room with custom save
dotOrientation = 'ne'),
AccessPoint('Big Pink', 'GreenPinkBrinstar', {
'Green Hill Zone Top Right': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'),
sm.traverse('BigPinkBottomRight'))),
'Green Brinstar Elevator': lambda sm: sm.canPassDachoraRoom()
}, internal=True, start={'spawn': 0x0100, 'solveArea': "Pink Brinstar"}),
AccessPoint('Green Hill Zone Top Right', 'GreenPinkBrinstar', {
'Noob Bridge Right': lambda sm: SMBool(True),
'Big Pink': Cache.ldeco(lambda sm: sm.haveItem('Morph'))
}, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoBlueDoors), sm.traverse('GreenHillZoneTopRight'))),
roomInfo = {'RoomPtr':0x9e52, "area": 0x1 },
exitInfo = {'DoorPtr':0x8e86, 'direction': 0x4, "cap": (0x1, 0x26), "bitFlag": 0x0,
"screen": (0x0, 0x2), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x1c7, 'SamusY':0x88},
dotOrientation = 'e'),
AccessPoint('Noob Bridge Right', 'GreenPinkBrinstar', {
'Green Hill Zone Top Right': Cache.ldeco(lambda sm: sm.wor(sm.haveItem('Wave'),
sm.wor(sm.canBlueGateGlitch(),
RomPatches.has(sm.player, RomPatches.AreaRandoGatesOther))))
}, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoBlueDoors), sm.traverse('NoobBridgeRight'))),
roomInfo = {'RoomPtr':0x9fba, "area": 0x1 },
exitInfo = {'DoorPtr':0x8f0a, 'direction': 0x4, "cap": (0x1, 0x46), "bitFlag": 0x0,
"screen": (0x0, 0x4), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x5ce, 'SamusY':0x88},
dotOrientation = 'se'),
AccessPoint('Green Brinstar Main Shaft Top Left', 'GreenPinkBrinstar', {
'Green Brinstar Elevator': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0x9ad9, "area": 0x1},
exitInfo = {'DoorPtr':0x8cb2, 'direction': 0x5, "cap": (0x2e, 0x6), "bitFlag": 0x0,
"screen": (0x2, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x34, 'SamusY':0x488},
escape = True,
dotOrientation = 'ne'),
AccessPoint('Brinstar Pre-Map Room Right', 'GreenPinkBrinstar', {
}, roomInfo = {'RoomPtr':0x9b9d, "area": 0x1},
exitInfo = {'DoorPtr':0x8d42, 'direction': 0x4, "cap": (0x1, 0x46), "bitFlag": 0x0,
"screen": (0x0, 0x4), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xffff, 'SamusY':0xffff}, # unused
escape = True,
dotOrientation = 'ne'),
AccessPoint('Etecoons Supers', 'GreenPinkBrinstar', {
'Etecoons Bottom': lambda sm: SMBool(True)
}, internal=True,
start={'spawn': 0x0107, 'doors':[0x34], 'patches':[RomPatches.EtecoonSupersBlueDoor],
'save':"Save_Etecoons" ,'solveArea': "Green Brinstar",
'forcedEarlyMorph':True, 'needsPreRando': True}),
AccessPoint('Etecoons Bottom', 'GreenPinkBrinstar', {
'Etecoons Supers': Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.EtecoonSupersBlueDoor),
sm.traverse('EtecoonEnergyTankLeft'))),
'Green Brinstar Elevator': lambda sm: sm.canUsePowerBombs()
}, internal=True),
### Wrecked Ship
AccessPoint('West Ocean Left', 'WreckedShip', {
'Wrecked Ship Main': Cache.ldeco(lambda sm: sm.traverse('WestOceanRight'))
}, roomInfo = {'RoomPtr':0x93fe, "area": 0x0},
exitInfo = {'DoorPtr':0x89ca, 'direction': 0x5, "cap": (0x1e, 0x6), "bitFlag": 0x0,
"screen": (0x1, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x34, 'SamusY':0x488},
dotOrientation = 'w'),
AccessPoint('Wrecked Ship Main', 'WreckedShip', {
'West Ocean Left': lambda sm: SMBool(True),
'Wrecked Ship Back': Cache.ldeco(lambda sm: sm.wor(sm.wand(Bosses.bossDead(sm, 'Phantoon'),
sm.canPassSpongeBath()),
sm.wand(sm.wnot(Bosses.bossDead(sm, 'Phantoon')),
RomPatches.has(sm.player, RomPatches.SpongeBathBlueDoor)))),
'PhantoonRoomOut': Cache.ldeco(lambda sm: sm.wand(sm.traverse('WreckedShipMainShaftBottom'), sm.canPassBombPassages()))
}, internal=True,
start={'spawn':0x0300,
'doors':[0x83,0x8b], 'patches':[RomPatches.SpongeBathBlueDoor, RomPatches.WsEtankBlueDoor],
'solveArea': "WreckedShip Main",
'needsPreRando':True}),
AccessPoint('Wrecked Ship Back', 'WreckedShip', {
'Wrecked Ship Main': lambda sm: SMBool(True),
'Crab Maze Left': Cache.ldeco(lambda sm: sm.canPassForgottenHighway(True))
}, internal=True),
AccessPoint('Crab Maze Left', 'WreckedShip', {
'Wrecked Ship Back': Cache.ldeco(lambda sm: sm.canPassForgottenHighway(False))
}, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoBlueDoors),
sm.traverse('LeCoudeBottom'))), # it is not exactly coude's door
# but it's equivalent in vanilla anyway
roomInfo = {'RoomPtr':0x957d, "area": 0x0, 'songs':[0x958e]},
exitInfo = {'DoorPtr':0x8aae, 'direction': 0x5, "cap": (0xe, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x34, 'SamusY':0x188, 'song': 0xc},
dotOrientation = 'e'),
AccessPoint('PhantoonRoomOut', 'WreckedShip', {
'Wrecked Ship Main': lambda sm: sm.canPassBombPassages()
}, boss = True,
roomInfo = {'RoomPtr':0xcc6f, "area": 0x3},
exitInfo = {'DoorPtr':0xa2ac, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0},
entryInfo = {'SamusX':0x49f, 'SamusY':0xb8},
traverse=lambda sm: sm.canOpenEyeDoors(),
dotOrientation = 's'),
AccessPoint('PhantoonRoomIn', 'WreckedShip', {},
boss = True,
roomInfo = {'RoomPtr':0xcd13, "area": 0x3},
exitInfo = {'DoorPtr':0xa2c4, 'direction': 0x5, "cap": (0x4e, 0x6), "bitFlag": 0x0,
"screen": (0x4, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe1fe,
"exitAsmPtr": 0xf7f0},
entryInfo = {'SamusX':0x2e, 'SamusY':0xb8},
dotOrientation = 's'),
AccessPoint('Basement Left', 'WreckedShip', {
'Wrecked Ship Main': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0xcc6f, "area": 0x3},
exitInfo = {'DoorPtr':0xa2a0, 'direction': 0x5, "cap": (0xe, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x2e, 'SamusY':0x88},
escape = True,
dotOrientation = 'ne'),
AccessPoint('Wrecked Ship Map Room', 'WreckedShip', {
}, roomInfo = {'RoomPtr':0xcccb, "area": 0x3},
exitInfo = {'DoorPtr':0xa2b8, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xffff, 'SamusY':0xffff}, # unused
escape = True,
dotOrientation = 'ne'),
### Lower Norfair
AccessPoint('Lava Dive Right', 'LowerNorfair', {
'LN Entrance': lambda sm: sm.canPassLavaPit()
}, roomInfo = {'RoomPtr':0xaf14, "area": 0x2, 'songs':[0xaf25]},
exitInfo = {'DoorPtr':0x96d2, 'direction': 0x4, "cap": (0x11, 0x26), "bitFlag": 0x0,
"screen": (0x1, 0x2), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x3d0, 'SamusY':0x88, 'song': 0x15},
dotOrientation = 'w'),
AccessPoint('LN Entrance', 'LowerNorfair', {
'Lava Dive Right': lambda sm: sm.canPassLavaPitReverse(),
'LN Above GT': lambda sm: sm.canPassLowerNorfairChozo(),
'Screw Attack Bottom': Cache.ldeco(lambda sm: sm.wand(sm.canUsePowerBombs(),
sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canGreenGateGlitch(),
sm.canDestroyBombWalls())),
'Firefleas': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canPassWorstRoom(),
sm.canUsePowerBombs()))
}, internal=True),
AccessPoint('LN Above GT', 'LowerNorfair', {
'Screw Attack Bottom': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.enoughStuffGT()))
}, internal=True),
AccessPoint('Screw Attack Bottom', 'LowerNorfair', {
'LN Entrance': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canExitScrewAttackArea(),
sm.haveItem('Super'),
sm.canUsePowerBombs()))
}, internal=True),
AccessPoint('Firefleas', 'LowerNorfair', {
'LN Entrance': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canPassAmphitheaterReverse(),
sm.canPassWorstRoomPirates(),
sm.canUsePowerBombs())),
'Three Muskateers Room Left': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.haveItem('Morph'),
# check for only 3 ki hunters this way
sm.canPassRedKiHunters())),
'Ridley Zone': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.traverse('WastelandLeft'),
sm.traverse('RedKihunterShaftBottom'),
sm.canGetBackFromRidleyZone(),
sm.canPassRedKiHunters(),
sm.canPassWastelandDessgeegas(),
sm.canPassNinjaPirates())),
'Screw Attack Bottom': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canPassAmphitheaterReverse(),
sm.canDestroyBombWalls(),
sm.canGreenGateGlitch())),
'Firefleas Top': Cache.ldeco(lambda sm: sm.wand(sm.canPassBombPassages(),
sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])))
}, internal=True),
AccessPoint('Firefleas Top', 'LowerNorfair', {
# this weird condition basically says: "if we start here, give heat protection"
'Firefleas': Cache.ldeco(lambda sm: sm.wor(sm.wnot(RomPatches.has(sm.player, RomPatches.LowerNorfairPBRoomHeatDisable)),
sm.heatProof()))
}, internal=True,
start={'spawn':0x0207,
'rom_patches': ['LN_PB_Heat_Disable', 'LN_Firefleas_Remove_Fune','firefleas_shot_block.ips'],
'patches':[RomPatches.LowerNorfairPBRoomHeatDisable, RomPatches.FirefleasRemoveFune],
'knows': ["FirefleasWalljump"],
'save': "Save_Firefleas", 'needsPreRando': True,
'solveArea': "Lower Norfair After Amphitheater",
'forcedEarlyMorph':True}),
AccessPoint('Ridley Zone', 'LowerNorfair', {
'Firefleas': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canGetBackFromRidleyZone(),
sm.canPassWastelandDessgeegas(),
sm.canPassRedKiHunters())),
'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']))
}, internal=True),
AccessPoint('Three Muskateers Room Left', 'LowerNorfair', {
'Firefleas': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.haveItem('Morph'),
sm.canPassThreeMuskateers()))
}, roomInfo = {'RoomPtr':0xb656, "area": 0x2},
exitInfo = {'DoorPtr':0x9a4a, 'direction': 0x5, "cap": (0x5e, 0x6), "bitFlag": 0x0,
"screen": (0x5, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x134, 'SamusY':0x88},
dotOrientation = 'n'),
AccessPoint('RidleyRoomOut', 'LowerNorfair', {
'Ridley Zone': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']))
}, boss = True,
roomInfo = {'RoomPtr':0xb37a, "area": 0x2},
exitInfo = {'DoorPtr':0x98ca, 'direction': 0x5, "cap": (0xe, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0},
entryInfo = {'SamusX':0x2e, 'SamusY':0x98},
traverse=Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canOpenEyeDoors())),
dotOrientation = 'e'),
AccessPoint('RidleyRoomIn', 'LowerNorfair', {},
boss = True,
roomInfo = {'RoomPtr':0xb32e, "area": 0x2},
exitInfo = {'DoorPtr':0x98be, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0},
entryInfo = {'SamusX':0xbf, 'SamusY':0x198}, # on Ridley's platform. entry screen has to be changed (see getDoorConnections)
dotOrientation = 'e'),
### Kraid
AccessPoint('Warehouse Zeela Room Left', 'Kraid', {
'KraidRoomOut': lambda sm: sm.canPassBombPassages()
}, roomInfo = {'RoomPtr': 0xa471, "area": 0x1, 'songs':[0xa482]},
exitInfo = {'DoorPtr': 0x913e, 'direction': 0x5, "cap": (0x2e, 0x6), "bitFlag": 0x0,
"screen": (0x2, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xbd3f},
entryInfo = {'SamusX':0x34, 'SamusY':0x88, 'song':0x12},
dotOrientation = 'w'),
AccessPoint('KraidRoomOut', 'Kraid', {
'Warehouse Zeela Room Left': lambda sm: sm.canPassBombPassages()
}, boss = True,
roomInfo = {'RoomPtr':0xa56b, "area": 0x1,
# put red brin song in both pre-kraid rooms,
# (vanilla music only makes sense if kraid is
# vanilla)
"songs":[0xa57c,0xa537,0xa551]},
exitInfo = {'DoorPtr':0x91b6, 'direction': 0x4, "cap": (0x1, 0x16), "bitFlag": 0x0,
"screen": (0x0, 0x1), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0},
entryInfo = {'SamusX':0x1cd, 'SamusY':0x188, 'song':0x12},
traverse=lambda sm: sm.canOpenEyeDoors(),
dotOrientation = 'e'),
AccessPoint('KraidRoomIn', 'Kraid', {},
boss = True,
roomInfo = {'RoomPtr':0xa59f, "area": 0x1},
exitInfo = {'DoorPtr':0x91ce, 'direction': 0x5, "cap": (0x1e, 0x16), "bitFlag": 0x0,
"screen": (0x1, 0x1), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0},
entryInfo = {'SamusX':0x34, 'SamusY':0x188},
dotOrientation = 'e'),
### Norfair
AccessPoint('Warehouse Entrance Left', 'Norfair', {
'Warehouse Entrance Right': lambda sm: sm.canAccessKraidsLair(),
'Business Center': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0xa6a1, "area": 0x1},
exitInfo = {'DoorPtr':0x922e, 'direction': 0x5, "cap": (0xe, 0x16), "bitFlag": 0x40,
"screen": (0x0, 0x1), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xbdd1},
entryInfo = {'SamusX':0x34, 'SamusY':0x88},
dotOrientation = 'sw'),
AccessPoint('Warehouse Entrance Right', 'Norfair', {
'Warehouse Entrance Left': Cache.ldeco(lambda sm: sm.haveItem('Super'))
}, roomInfo = {'RoomPtr': 0xa6a1, "area": 0x1},
exitInfo = {'DoorPtr': 0x923a, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX': 0x2c7, 'SamusY': 0x98},
dotOrientation = 'nw'),
AccessPoint('Business Center', 'Norfair', {
'Cathedral': Cache.ldeco(lambda sm: sm.canEnterCathedral(Settings.hellRunsTable['MainUpperNorfair']['Norfair Entrance -> Cathedral Missiles']['mult'])),
'Bubble Mountain': Cache.ldeco(# go through cathedral
lambda sm: sm.wand(sm.traverse('CathedralRight'),
sm.canEnterCathedral(Settings.hellRunsTable['MainUpperNorfair']['Norfair Entrance -> Bubble']['mult']))),
'Bubble Mountain Bottom': Cache.ldeco(lambda sm: sm.haveItem('SpeedBooster')), # frog speedway
'Crocomire Speedway Bottom': Cache.ldeco(lambda sm: sm.wor(sm.wand(sm.haveItem('SpeedBooster'), # frog speedway
sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Norfair Entrance -> Croc via Frog w/Wave' if sm.haveItem('Wave') else 'Norfair Entrance -> Croc via Frog']),
sm.wor(sm.canBlueGateGlitch(),
sm.haveItem('Wave'))),
# below ice
sm.wand(sm.traverse('BusinessCenterTopLeft'),
sm.haveItem('SpeedBooster'),
sm.canUsePowerBombs(),
sm.canHellRun(**Settings.hellRunsTable['Ice']['Norfair Entrance -> Croc via Ice'])))),
'Warehouse Entrance Left': lambda sm: SMBool(True)
}, internal=True,
start={'spawn':0x0208, 'doors':[0x4d], 'patches':[RomPatches.HiJumpAreaBlueDoor], 'solveArea': "Norfair Entrance", 'needsPreRando':True}),
AccessPoint('Single Chamber Top Right', 'Norfair', {
'Bubble Mountain Top': Cache.ldeco(lambda sm: sm.wand(sm.canDestroyBombWalls(),
sm.haveItem('Morph'),
sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Single Chamber <-> Bubble Mountain'])))
}, roomInfo = {'RoomPtr':0xad5e, "area": 0x2},
exitInfo = {'DoorPtr':0x95fa, 'direction': 0x4, "cap": (0x11, 0x6), "bitFlag": 0x0,
"screen": (0x1, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x5cf, 'SamusY':0x88},
dotOrientation = 'ne'),
AccessPoint('Cathedral', 'Norfair', {
'Business Center': Cache.ldeco(lambda sm: sm.canExitCathedral(Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Cathedral Missiles'])),
'Bubble Mountain': Cache.ldeco(lambda sm: sm.wand(sm.traverse('CathedralRight'),
sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Norfair Entrance -> Cathedral Missiles'])))
}, internal=True),
AccessPoint('Kronic Boost Room Bottom Left', 'Norfair', {
'Bubble Mountain Bottom': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Single Chamber <-> Bubble Mountain'])),
'Bubble Mountain Top': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'),
sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Kronic Boost Room -> Bubble Mountain Top']))), # go all the way around
'Crocomire Speedway Bottom': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Kronic Boost Room <-> Croc']),
sm.wor(sm.haveItem('Wave'),
sm.canBlueGateGlitch()))),
}, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoBlueDoors), sm.traverse('KronicBoostBottomLeft'))),
roomInfo = {'RoomPtr':0xae74, "area": 0x2, 'songs':[0xae85]},
exitInfo = {'DoorPtr':0x967e, 'direction': 0x5, "cap": (0x3e, 0x6), "bitFlag": 0x0,
"screen": (0x3, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x134, 'SamusY':0x288, 'song': 0x15},
dotOrientation = 'se'),
AccessPoint('Crocomire Speedway Bottom', 'Norfair', {
'Business Center': Cache.ldeco(lambda sm: sm.wor(sm.wand(sm.canPassFrogSpeedwayRightToLeft(),
sm.canHellRun(**Settings.hellRunsTable['Ice']['Croc -> Norfair Entrance'])),
sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Croc -> Norfair Entrance']),
sm.canGrappleEscape(),
sm.haveItem('Super')))),
'Bubble Mountain Bottom': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['Ice']['Croc -> Bubble Mountain'])),
'Kronic Boost Room Bottom Left': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Kronic Boost Room <-> Croc']),
sm.haveItem('Morph')))
}, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.CrocBlueDoors), sm.traverse('CrocomireSpeedwayBottom'))),
roomInfo = {'RoomPtr':0xa923, "area": 0x2},
exitInfo = {'DoorPtr':0x93d2, 'direction': 0x6, "cap": (0x36, 0x2), "bitFlag": 0x0,
"screen": (0x3, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xc57, 'SamusY':0x2b8},
dotOrientation = 'se'),
AccessPoint('Bubble Mountain', 'Norfair', {
'Business Center': lambda sm: sm.canExitCathedral(Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Norfair Entrance']),
'Bubble Mountain Top': lambda sm: sm.canClimbBubbleMountain(),
'Cathedral': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Cathedral Missiles'])),
'Bubble Mountain Bottom': lambda sm: sm.canPassBombPassages()
}, internal=True,
start={'spawn':0x0201, 'doors':[0x54,0x55], 'patches':[RomPatches.SpeedAreaBlueDoors], 'knows':['BubbleMountainWallJump'], 'solveArea': "Bubble Norfair Bottom"}),
AccessPoint('Bubble Mountain Top', 'Norfair', {
'Kronic Boost Room Bottom Left': Cache.ldeco(# go all the way around
lambda sm: sm.wand(sm.haveItem('Morph'),
sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Kronic Boost Room wo/Bomb']))),
'Single Chamber Top Right': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Single Chamber <-> Bubble Mountain']),
sm.canDestroyBombWalls(),
sm.haveItem('Morph'),
RomPatches.has(sm.player, RomPatches.SingleChamberNoCrumble))),
'Bubble Mountain': lambda sm: SMBool(True),
# all the way around
'Bubble Mountain Bottom': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'),
sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble Top <-> Bubble Bottom'])))
}, internal=True),
AccessPoint('Bubble Mountain Bottom', 'Norfair', {
'Bubble Mountain': lambda sm: sm.canPassBombPassages(),
'Crocomire Speedway Bottom': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Croc']),
sm.wor(sm.canBlueGateGlitch(),
sm.haveItem('Wave')))),
'Kronic Boost Room Bottom Left': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Kronic Boost Room'])),
'Business Center': lambda sm: sm.canPassFrogSpeedwayRightToLeft(),
# all the way around
'Bubble Mountain Top': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'),
sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble Top <-> Bubble Bottom'])))
}, internal=True),
AccessPoint('Business Center Mid Left', 'Norfair', {
'Warehouse Entrance Left': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0xa7de, "area": 0x2},
exitInfo = {'DoorPtr':0x9306, 'direction': 0x5, "cap": (0xe, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x34, 'SamusY':0x488},
escape = True,
dotOrientation = 'ne'),
AccessPoint('Norfair Map Room', 'Norfair', {
}, roomInfo = {'RoomPtr':0xb0b4, "area": 0x2},
exitInfo = {'DoorPtr':0x97c2, 'direction': 0x4, "cap": (0x1, 0x46), "bitFlag": 0x0,
"screen": (0x0, 0x4), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xffff, 'SamusY':0xffff}, # unused
escape = True,
dotOrientation = 'ne'),
### Croc
AccessPoint('Crocomire Room Top', 'Crocomire', {
}, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.CrocBlueDoors), sm.enoughStuffCroc())),
roomInfo = {'RoomPtr':0xa98d, "area": 0x2, 'songs':[0xa9bd]},
exitInfo = {'DoorPtr':0x93ea, 'direction': 0x7, "cap": (0xc6, 0x2d), "bitFlag": 0x0,
"screen": (0xc, 0x2), "distanceToSpawn": 0x1c0, "doorAsmPtr": 0x0000,
"exitAsmPtr": 0xf7f0},
entryInfo = {'SamusX':0x383, 'SamusY':0x98, 'song': 0x15},
dotOrientation = 'se'),
### West Maridia
AccessPoint('Main Street Bottom', 'WestMaridia', {
'Red Fish Room Left': Cache.ldeco(lambda sm: sm.wand(sm.canGoUpMtEverest(),
sm.haveItem('Morph'))),
'Crab Hole Bottom Left': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'),
sm.canTraverseCrabTunnelLeftToRight())),
# this transition leads to EastMaridia directly
'Oasis Bottom': Cache.ldeco(lambda sm: sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.MaridiaSandWarp)),
sm.traverse('MainStreetBottomRight'),
sm.wor(sm.haveItem('Super'),
RomPatches.has(sm.player, RomPatches.AreaRandoGatesOther)),
sm.canTraverseWestSandHallLeftToRight())),
'Crab Shaft Left': lambda sm: sm.canPassMtEverest()
}, roomInfo = {'RoomPtr':0xcfc9, "area": 0x4},
exitInfo = {'DoorPtr':0xa39c, 'direction': 0x6, "cap": (0x6, 0x2), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x170, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x14a, 'SamusY':0x7a8},
dotOrientation = 's'),
AccessPoint('Mama Turtle', 'WestMaridia', {
'Main Street Bottom': lambda sm: sm.canJumpUnderwater()
}, internal=True,
start = {'spawn': 0x0406, 'solveArea': "Maridia Green",
'save':"Save_Mama", 'needsPreRando':True,
'patches':[RomPatches.MamaTurtleBlueDoor],
'rom_patches':['mama_save.ips'], 'doors': [0x8e]}),
AccessPoint('Crab Hole Bottom Left', 'WestMaridia', {
'Main Street Bottom': Cache.ldeco(lambda sm: sm.wand(sm.canExitCrabHole(),
sm.wor(sm.canGreenGateGlitch(),
RomPatches.has(sm.player, RomPatches.AreaRandoGatesOther)))),
# this transition leads to EastMaridia directly
'Oasis Bottom': Cache.ldeco(lambda sm: sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.MaridiaSandWarp)),
sm.canExitCrabHole(),
sm.canTraverseWestSandHallLeftToRight()))
}, roomInfo = {'RoomPtr':0xd21c, "area": 0x4},
exitInfo = {'DoorPtr':0xa510, 'direction': 0x5,
"cap": (0x3e, 0x6), "screen": (0x3, 0x0), "bitFlag": 0x0,
"distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x28, 'SamusY':0x188},
dotOrientation = 'se'),
AccessPoint('Red Fish Room Left', 'WestMaridia', {
'Main Street Bottom': Cache.ldeco(lambda sm: sm.haveItem('Morph')) # just go down
}, roomInfo = {'RoomPtr':0xd104, "area": 0x4},
exitInfo = {'DoorPtr':0xa480, 'direction': 0x5, "cap": (0x2e, 0x36), "bitFlag": 0x40,
"screen": (0x2, 0x3), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe367},
entryInfo = {'SamusX':0x34, 'SamusY':0x88},
dotOrientation = 'w'),
AccessPoint('Crab Shaft Left', 'WestMaridia', {
'Main Street Bottom': lambda sm: SMBool(True), # fall down
'Beach': lambda sm: sm.canDoOuterMaridia(),
'Crab Shaft Right': lambda sm: SMBool(True)
}, internal=True),
AccessPoint('Watering Hole', 'WestMaridia', {
'Beach': lambda sm: sm.haveItem('Morph'),
'Watering Hole Bottom': lambda sm: SMBool(True)
}, internal=True,
start = {'spawn': 0x0407, 'solveArea': "Maridia Pink Bottom", 'save':"Save_Watering_Hole",
'patches':[RomPatches.MaridiaTubeOpened], 'rom_patches':['wh_open_tube.ips'],
'forcedEarlyMorph':True}),
AccessPoint('Watering Hole Bottom', 'WestMaridia', {
'Watering Hole': lambda sm: sm.canJumpUnderwater()
}, internal=True),
AccessPoint('Beach', 'WestMaridia', {
'Crab Shaft Left': lambda sm: SMBool(True), # fall down
'Watering Hole': Cache.ldeco(lambda sm: sm.wand(sm.wor(sm.canPassBombPassages(),
sm.canUseSpringBall()),
sm.canDoOuterMaridia()))
}, internal=True),
AccessPoint('Crab Shaft Right', 'WestMaridia', {
'Crab Shaft Left': lambda sm: sm.canJumpUnderwater()
}, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.CrabShaftBlueDoor),
sm.traverse('CrabShaftRight'))),
roomInfo = {'RoomPtr':0xd1a3, "area": 0x4},
exitInfo = {'DoorPtr':0xa4c8, 'direction': 0x4, "cap": (0x1, 0x16), "bitFlag": 0x0,
"screen": (0x0, 0x1), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0},
entryInfo = {'SamusX':0x1ca, 'SamusY':0x388},
dotOrientation = 'e'),
# escape APs
AccessPoint('Crab Hole Bottom Right', 'WestMaridia', {
'Crab Hole Bottom Left': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0xd21c, "area": 0x4},
exitInfo = {'DoorPtr':0xa51c, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xd7, 'SamusY':0x188},
escape = True,
dotOrientation = 'ne'),
AccessPoint('Maridia Map Room', 'WestMaridia', {
}, roomInfo = {'RoomPtr':0xd3b6, "area": 0x4},
exitInfo = {'DoorPtr':0xa5e8, 'direction': 0x5, "cap": (0xe, 0x16), "bitFlag": 0x0,
"screen": (0x0, 0x1), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe356},
entryInfo = {'SamusX':0xffff, 'SamusY':0xffff}, # unused
escape = True,
dotOrientation = 'ne'),
### East Maridia
AccessPoint('Aqueduct Top Left', 'EastMaridia', {
'Aqueduct Bottom': lambda sm: sm.canUsePowerBombs()
}, roomInfo = {'RoomPtr':0xd5a7, "area": 0x4},
exitInfo = {'DoorPtr':0xa708, 'direction': 0x5, "cap": (0x1e, 0x36), "bitFlag": 0x0,
"screen": (0x1, 0x3), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe398},
entryInfo = {'SamusX':0x34, 'SamusY':0x188},
dotOrientation = 'w'),
AccessPoint('Aqueduct Bottom', 'EastMaridia', {
'Aqueduct Top Left': Cache.ldeco(lambda sm: sm.wand(sm.canDestroyBombWallsUnderwater(), # top left bomb blocks
sm.canJumpUnderwater())),
'Post Botwoon': Cache.ldeco(lambda sm: sm.wand(sm.canJumpUnderwater(),
sm.canDefeatBotwoon())), # includes botwoon hallway conditions
'Left Sandpit': lambda sm: sm.canAccessSandPits(),
'Right Sandpit': lambda sm: sm.canAccessSandPits(),
'Aqueduct': Cache.ldeco(lambda sm: sm.wand(sm.wor(sm.haveItem('SpeedBooster'),
sm.wand(sm.knowsSnailClip(),
sm.haveItem('Morph'))),
sm.haveItem('Gravity')))
}, internal=True),
AccessPoint('Aqueduct', 'EastMaridia', {
'Aqueduct Bottom': lambda sm: SMBool(True) # go down
}, internal=True,
start = {'spawn': 0x0405, 'solveArea': "Maridia Pink Bottom",
'save':"Save_Aqueduct", 'needsPreRando':True,
'doors': [0x96]}),
AccessPoint('Post Botwoon', 'EastMaridia', {
'Aqueduct Bottom': Cache.ldeco(lambda sm: sm.wor(sm.wand(sm.canJumpUnderwater(), # can't access the sand pits from the right side of the room
sm.haveItem('Morph')),
sm.wand(sm.haveItem('Gravity'),
sm.haveItem('SpeedBooster')))),
'Colosseum Top Right': lambda sm: sm.canBotwoonExitToColosseum(),
'Toilet Top': Cache.ldeco(lambda sm: sm.wand(sm.canReachCacatacAlleyFromBotowoon(),
sm.canPassCacatacAlley()))
}, internal=True),
AccessPoint('West Sand Hall Left', 'EastMaridia', {
# XXX there might be some tech to do this suitless, but HJ+ice is not enough
'Oasis Bottom': Cache.ldeco(lambda sm: sm.haveItem('Gravity')),
'Aqueduct Bottom': Cache.ldeco(lambda sm: RomPatches.has(sm.player, RomPatches.MaridiaSandWarp)),
# this goes directly to WestMaridia
'Main Street Bottom': Cache.ldeco(lambda sm: sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.MaridiaSandWarp)),
sm.wor(sm.canGreenGateGlitch(),
RomPatches.has(sm.player, RomPatches.AreaRandoGatesOther)))),
# this goes directly to WestMaridia
'Crab Hole Bottom Left': Cache.ldeco(lambda sm: sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.MaridiaSandWarp)),
sm.haveItem('Morph')))
}, internal=True),
AccessPoint('Left Sandpit', 'EastMaridia', {
'West Sand Hall Left': lambda sm: sm.canAccessSandPits(),
'Oasis Bottom': lambda sm: sm.canAccessSandPits()
}, internal=True),
AccessPoint('Oasis Bottom', 'EastMaridia', {
'Toilet Top': Cache.ldeco(lambda sm: sm.wand(sm.traverse('OasisTop'), sm.canDestroyBombWallsUnderwater())),
'West Sand Hall Left': lambda sm: sm.canAccessSandPits()
}, internal=True),
AccessPoint('Right Sandpit', 'EastMaridia', {
'Oasis Bottom': lambda sm: sm.canAccessSandPits()
}, internal=True),
AccessPoint('Le Coude Right', 'EastMaridia', {
'Toilet Top': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0x95a8, "area": 0x0},
exitInfo = {'DoorPtr':0x8aa2, 'direction': 0x4, "cap": (0x1, 0x16), "bitFlag": 0x0,
"screen": (0x0, 0x1), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xd1, 'SamusY':0x88},
dotOrientation = 'ne'),
AccessPoint('Toilet Top', 'EastMaridia', {
'Oasis Bottom': Cache.ldeco(lambda sm: sm.wand(sm.traverse('PlasmaSparkBottom'), sm.canDestroyBombWallsUnderwater())),
'Le Coude Right': lambda sm: SMBool(True),
'Colosseum Top Right': Cache.ldeco(lambda sm: sm.wand(Bosses.bossDead(sm, 'Draygon'),
# suitless could be possible with this but unreasonable: https://youtu.be/rtLwytH-u8o
sm.haveItem('Gravity'),
sm.haveItem('Morph')))
}, internal=True),
AccessPoint('Colosseum Top Right', 'EastMaridia', {
'Post Botwoon': lambda sm: sm.canColosseumToBotwoonExit(),
'Precious Room Top': Cache.ldeco(lambda sm: sm.traverse('ColosseumBottomRight')), # go down
}, internal = True),
AccessPoint('Precious Room Top', 'EastMaridia', {
'Colosseum Top Right': lambda sm: sm.canClimbColosseum(),
'DraygonRoomOut': lambda sm: SMBool(True) # go down
}, internal = True),
# boss APs
AccessPoint('DraygonRoomOut', 'EastMaridia', {
'Precious Room Top': lambda sm: sm.canExitPreciousRoom()
}, boss = True,
roomInfo = {'RoomPtr':0xd78f, "area": 0x4, "songs":[0xd7a5]},
exitInfo = {'DoorPtr':0xa840, 'direction': 0x5, "cap": (0x1e, 0x6), "bitFlag": 0x0,
"screen": (0x1, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0},
entryInfo = {'SamusX':0x34, 'SamusY':0x288, 'song':0x1b},
traverse=lambda sm: sm.canOpenEyeDoors(),
dotOrientation = 'e'),
AccessPoint('DraygonRoomIn', 'EastMaridia', {
'Draygon Room Bottom': Cache.ldeco(lambda sm: sm.wor(Bosses.bossDead(sm, "Draygon"),
sm.wand(sm.canFightDraygon(),
sm.enoughStuffsDraygon())))
}, boss = True,
roomInfo = {'RoomPtr':0xda60, "area": 0x4},
exitInfo = {'DoorPtr':0xa96c, 'direction': 0x4, "cap": (0x1, 0x26), "bitFlag": 0x0,
"screen": (0x0, 0x2), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe3d9,
"exitAsmPtr": 0xf7f0},
entryInfo = {'SamusX':0x1c8, 'SamusY':0x88},
dotOrientation = 'e'),
AccessPoint('Draygon Room Bottom', 'EastMaridia', {
'DraygonRoomIn': Cache.ldeco(lambda sm: sm.wand(Bosses.bossDead(sm, 'Draygon'), sm.canExitDraygon()))
}, internal = True),
### Red Brinstar. Main nodes: Red Tower Top Left, East Tunnel Right
AccessPoint('Red Tower Top Left', 'RedBrinstar', {
# go up
'Red Brinstar Elevator': lambda sm: sm.canClimbRedTower(),
'Caterpillar Room Top Right': Cache.ldeco(lambda sm: sm.wand(sm.canPassRedTowerToMaridiaNode(),
sm.canClimbRedTower())),
# go down
'East Tunnel Right': lambda sm: SMBool(True)
}, roomInfo = {'RoomPtr':0xa253, "area": 0x1},
exitInfo = {'DoorPtr':0x902a, 'direction': 0x5, "cap": (0x5e, 0x6), "bitFlag": 0x0,
"screen": (0x5, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x2f, 'SamusY':0x488},
dotOrientation = 'w'),
AccessPoint('Caterpillar Room Top Right', 'RedBrinstar', {
'Red Brinstar Elevator': lambda sm: sm.canPassMaridiaToRedTowerNode()
}, roomInfo = {'RoomPtr':0xa322, "area": 0x1},
exitInfo = {'DoorPtr':0x90c6, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x40,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xbdaf},
entryInfo = {'SamusX':0x2cd, 'SamusY':0x388},
dotOrientation = 'ne'),
AccessPoint('Red Brinstar Elevator', 'RedBrinstar', {
'Caterpillar Room Top Right': lambda sm: sm.canPassRedTowerToMaridiaNode(),
'Red Tower Top Left': Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.HellwayBlueDoor), sm.traverse('RedTowerElevatorLeft')))
}, traverse=Cache.ldeco(lambda sm:sm.wor(RomPatches.has(sm.player, RomPatches.RedTowerBlueDoors), sm.traverse('RedBrinstarElevatorTop'))),
roomInfo = {'RoomPtr':0x962a, "area": 0x0},
exitInfo = {'DoorPtr':0x8af6, 'direction': 0x7, "cap": (0x16, 0x2d), "bitFlag": 0x0,
"screen": (0x1, 0x2), "distanceToSpawn": 0x1c0, "doorAsmPtr": 0xb9f1},
entryInfo = {'SamusX':0x80, 'SamusY':0x58},
start={'spawn':0x010a, 'doors':[0x3c], 'patches':[RomPatches.HellwayBlueDoor], 'solveArea': "Red Brinstar Top", 'areaMode':True},
dotOrientation = 'n'),
AccessPoint('East Tunnel Right', 'RedBrinstar', {
'East Tunnel Top Right': lambda sm: SMBool(True), # handled by room traverse function
'Glass Tunnel Top': Cache.ldeco(lambda sm: sm.wand(sm.canUsePowerBombs(),
sm.wor(sm.haveItem('Gravity'),
sm.haveItem('HiJump')))),
'Red Tower Top Left': lambda sm: sm.canClimbBottomRedTower()
}, roomInfo = {'RoomPtr':0xcf80, "area": 0x4},
exitInfo = {'DoorPtr':0xa384, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x40,
"screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0xce, 'SamusY':0x188},
dotOrientation = 'se'),
AccessPoint('East Tunnel Top Right', 'RedBrinstar', {
'East Tunnel Right': Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoGatesBase),
sm.haveItem('Super')))
}, traverse=Cache.ldeco(lambda sm: RomPatches.has(sm.player, RomPatches.AreaRandoGatesBase)),
roomInfo = {'RoomPtr':0xcf80, "area": 0x4},
exitInfo = {'DoorPtr':0xa390, 'direction': 0x4, "cap": (0x1, 0x16), "bitFlag": 0x0,
"screen": (0x0, 0x1), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe356},
entryInfo = {'SamusX':0x3c6, 'SamusY':0x88},
dotOrientation = 'e'),
AccessPoint('Glass Tunnel Top', 'RedBrinstar', {
'East Tunnel Right': Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.MaridiaTubeOpened),
sm.canUsePowerBombs()))
}, traverse=Cache.ldeco(lambda sm: sm.wand(sm.wor(sm.haveItem('Gravity'),
sm.haveItem('HiJump')),
sm.wor(RomPatches.has(sm.player, RomPatches.MaridiaTubeOpened),
sm.canUsePowerBombs()))),
roomInfo = {'RoomPtr':0xcefb, "area": 0x4},
exitInfo = {'DoorPtr':0xa330, 'direction': 0x7, "cap": (0x16, 0x7d), "bitFlag": 0x0,
"screen": (0x1, 0x7), "distanceToSpawn": 0x200, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x81, 'SamusY':0x78},
dotOrientation = 's'),
### Tourian
AccessPoint('Golden Four', 'Tourian', {},
roomInfo = {'RoomPtr':0xa5ed, "area": 0x0},
exitInfo = {'DoorPtr':0x91e6, 'direction': 0x5, "cap": (0xe, 0x66), "bitFlag": 0x0,
"screen": (0x0, 0x6), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000},
entryInfo = {'SamusX':0x34, 'SamusY':0x88},
start={'spawn':0x0007, 'solveArea': "Tourian", "save": "Save_G4", 'areaMode':True},
dotOrientation = 'w'),
AccessPoint('Tourian Escape Room 4 Top Right', 'Tourian', {},
roomInfo = {'RoomPtr':0xdede, "area": 0x5},
exitInfo = {'DoorPtr':0xab34, 'direction': 0x4, "cap": (0x1, 0x86), "bitFlag": 0x40,
"screen": (0x0, 0x8), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe4cf},
entryInfo = {'SamusX':0xffff, 'SamusY':0xffff}, # unused
escape = True,
dotOrientation = 'ne'),
]

View File

@ -0,0 +1,766 @@
from math import ceil
from logic.smbool import SMBool
from logic.helpers import Helpers, Bosses
from logic.cache import Cache
from rom.rom_patches import RomPatches
from graph.graph_utils import getAccessPoint
from utils.parameters import Settings
class HelpersGraph(Helpers):
def __init__(self, smbm):
self.smbm = smbm
def canEnterAndLeaveGauntletQty(self, nPB, nTanksSpark):
sm = self.smbm
# EXPLAINED: to access Gauntlet Entrance from Landing site we can either:
# -fly to it (infinite bomb jumps or space jump)
# -shinespark to it
# -wall jump with high jump boots
# -wall jump without high jump boots
# then inside it to break the bomb wals:
# -use screw attack (easy way)
# -use power bombs
# -use bombs
# -perform a simple short charge on the way in
# and use power bombs on the way out
return sm.wand(sm.wor(sm.canFly(),
sm.haveItem('SpeedBooster'),
sm.wand(sm.knowsHiJumpGauntletAccess(),
sm.haveItem('HiJump')),
sm.knowsHiJumpLessGauntletAccess()),
sm.wor(sm.haveItem('ScrewAttack'),
sm.wor(sm.wand(sm.energyReserveCountOkHardRoom('Gauntlet'),
sm.wand(sm.canUsePowerBombs(),
sm.wor(sm.itemCountOk('PowerBomb', nPB),
sm.wand(sm.haveItem('SpeedBooster'),
sm.energyReserveCountOk(nTanksSpark))))),
sm.wand(sm.energyReserveCountOkHardRoom('Gauntlet', 0.51),
sm.canUseBombs()))))
@Cache.decorator
def canEnterAndLeaveGauntlet(self):
sm = self.smbm
return sm.wor(sm.wand(sm.canShortCharge(),
sm.canEnterAndLeaveGauntletQty(2, 2)),
sm.canEnterAndLeaveGauntletQty(2, 3))
def canPassTerminatorBombWall(self, fromLandingSite=True):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('SpeedBooster'),
sm.wor(SMBool(not fromLandingSite, 0), sm.knowsSimpleShortCharge(), sm.knowsShortCharge())),
sm.canDestroyBombWalls())
@Cache.decorator
def canPassCrateriaGreenPirates(self):
sm = self.smbm
return sm.wor(sm.canPassBombPassages(),
sm.haveMissileOrSuper(),
sm.energyReserveCountOk(1),
sm.wor(sm.haveItem('Charge'),
sm.haveItem('Ice'),
sm.haveItem('Wave'),
sm.wor(sm.haveItem('Spazer'),
sm.haveItem('Plasma'),
sm.haveItem('ScrewAttack'))))
# from blue brin elevator
@Cache.decorator
def canAccessBillyMays(self):
sm = self.smbm
return sm.wand(sm.wor(RomPatches.has(sm.player, RomPatches.BlueBrinstarBlueDoor),
sm.traverse('ConstructionZoneRight')),
sm.canUsePowerBombs(),
sm.wor(sm.knowsBillyMays(),
sm.haveItem('Gravity'),
sm.haveItem('SpaceJump')))
@Cache.decorator
def canAccessKraidsLair(self):
sm = self.smbm
# EXPLAINED: access the upper right platform with either:
# -hijump boots (easy regular way)
# -fly (space jump or infinite bomb jump)
# -know how to wall jump on the platform without the hijump boots
return sm.wand(sm.haveItem('Super'),
sm.wor(sm.haveItem('HiJump'),
sm.canFly(),
sm.knowsEarlyKraid()))
@Cache.decorator
def canPassMoat(self):
sm = self.smbm
# EXPLAINED: In the Moat we can either:
# -use grapple or space jump (easy way)
# -do a continuous wall jump (https://www.youtube.com/watch?v=4HVhTwwax6g)
# -do a diagonal bomb jump from the middle platform (https://www.youtube.com/watch?v=5NRqQ7RbK3A&t=10m58s)
# -do a short charge from the Keyhunter room (https://www.youtube.com/watch?v=kFAYji2gFok)
# -do a gravity jump from below the right platform
# -do a mock ball and a bounce ball (https://www.youtube.com/watch?v=WYxtRF--834)
# -with gravity, either hijump or IBJ
return sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpaceJump'),
sm.knowsContinuousWallJump(),
sm.wand(sm.knowsDiagonalBombJump(), sm.canUseBombs()),
sm.canSimpleShortCharge(),
sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.knowsGravityJump(),
sm.haveItem('HiJump'),
sm.canInfiniteBombJump())),
sm.wand(sm.knowsMockballWs(), sm.canUseSpringBall()))
@Cache.decorator
def canPassMoatFromMoat(self):
sm = self.smbm
return sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpaceJump'),
sm.wand(sm.knowsDiagonalBombJump(), sm.canUseBombs()),
sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.knowsGravityJump(),
sm.haveItem('HiJump'),
sm.canInfiniteBombJump())))
@Cache.decorator
def canPassMoatReverse(self):
sm = self.smbm
return sm.wor(RomPatches.has(sm.player, RomPatches.MoatShotBlock),
sm.haveItem('Grapple'),
sm.haveItem('SpaceJump'),
sm.haveItem('Gravity'),
sm.canPassBombPassages())
@Cache.decorator
def canPassSpongeBath(self):
sm = self.smbm
return sm.wor(sm.wand(sm.canPassBombPassages(),
sm.knowsSpongeBathBombJump()),
sm.wand(sm.haveItem('HiJump'),
sm.knowsSpongeBathHiJump()),
sm.haveItem('Gravity'),
sm.haveItem('SpaceJump'),
sm.wand(sm.haveItem('SpeedBooster'),
sm.knowsSpongeBathSpeed()),
sm.canSpringBallJump())
@Cache.decorator
def canPassBowling(self):
sm = self.smbm
return sm.wand(Bosses.bossDead(sm, 'Phantoon'),
sm.wor(SMBool(sm.getDmgReduction()[0] >= 2),
sm.energyReserveCountOk(1),
sm.haveItem("SpaceJump"),
sm.haveItem("Grapple")))
@Cache.decorator
def canAccessEtecoons(self):
sm = self.smbm
return sm.wor(sm.canUsePowerBombs(),
sm.wand(sm.knowsMoondance(), sm.canUseBombs(), sm.traverse('MainShaftBottomRight')))
@Cache.decorator
def canKillBeetoms(self):
sm = self.smbm
# can technically be killed with bomb, but it's harder
return sm.wor(sm.haveMissileOrSuper(), sm.canUsePowerBombs(), sm.haveItem('ScrewAttack'))
# the water zone east of WS
def canPassForgottenHighway(self, fromWs):
sm = self.smbm
suitless = sm.wand(sm.haveItem('HiJump'), sm.knowsGravLessLevel1())
if fromWs is True and RomPatches.has(sm.player, RomPatches.EastOceanPlatforms).bool is False:
suitless = sm.wand(suitless,
sm.wor(sm.canSpringBallJump(), # two sbj on the far right
# to break water line and go through the door on the right
sm.haveItem('SpaceJump')))
return sm.wand(sm.wor(sm.haveItem('Gravity'),
suitless),
sm.haveItem('Morph')) # for crab maze
@Cache.decorator
def canExitCrabHole(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'), # morph to exit the hole
sm.wor(sm.wand(sm.haveItem('Gravity'), # even with gravity you need some way to climb...
sm.wor(sm.haveItem('Ice'), # ...on crabs...
sm.wand(sm.haveItem('HiJump'), sm.knowsMaridiaWallJumps()), # ...or by jumping
sm.knowsGravityJump(),
sm.canFly())),
sm.wand(sm.haveItem('Ice'), sm.canDoSuitlessOuterMaridia()), # climbing crabs
sm.canDoubleSpringBallJump()))
# bottom sandpits with the evirs except west sand hall left to right
@Cache.decorator
def canTraverseSandPits(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.knowsGravLessLevel3(),
sm.haveItem('HiJump'),
sm.haveItem('Ice')))
@Cache.decorator
def canTraverseWestSandHallLeftToRight(self):
sm = self.smbm
return sm.haveItem('Gravity') # FIXME find suitless condition
@Cache.decorator
def canPassMaridiaToRedTowerNode(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'),
sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoGatesBase),
sm.haveItem('Super')))
@Cache.decorator
def canPassRedTowerToMaridiaNode(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'),
RomPatches.has(sm.player, RomPatches.AreaRandoGatesBase))
def canEnterCathedral(self, mult=1.0):
sm = self.smbm
return sm.wand(sm.traverse('CathedralEntranceRight'),
sm.wor(sm.wand(sm.canHellRun('MainUpperNorfair', mult),
sm.wor(sm.wor(RomPatches.has(sm.player, RomPatches.CathedralEntranceWallJump),
sm.haveItem('HiJump'),
sm.canFly()),
sm.wor(sm.haveItem('SpeedBooster'), # spark
sm.canSpringBallJump()))),
sm.wand(sm.canHellRun('MainUpperNorfair', 0.5*mult),
sm.haveItem('Morph'),
sm.knowsNovaBoost())))
@Cache.decorator
def canClimbBubbleMountain(self):
sm = self.smbm
return sm.wor(sm.haveItem('HiJump'),
sm.canFly(),
sm.haveItem('Ice'),
sm.knowsBubbleMountainWallJump())
@Cache.decorator
def canHellRunToSpeedBooster(self):
sm = self.smbm
return sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Speed Booster w/Speed' if sm.haveItem('SpeedBooster') else 'Bubble -> Speed Booster'])
@Cache.decorator
def canAccessDoubleChamberItems(self):
sm = self.smbm
hellRun = Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Wave']
return sm.wor(sm.wand(sm.traverse('SingleChamberRight'),
sm.canHellRun(**hellRun)),
sm.wand(sm.wor(sm.haveItem('HiJump'),
sm.canSimpleShortCharge(),
sm.canFly(),
sm.knowsDoubleChamberWallJump()),
sm.canHellRun(hellRun['hellRun'], hellRun['mult']*0.8, hellRun['minE'])))
def canExitCathedral(self, hellRun):
# from top: can use bomb/powerbomb jumps
# from bottom: can do a shinespark or use space jump
# can do it with highjump + wall jump
# can do it with only two wall jumps (the first one is delayed like on alcatraz)
# can do it with a spring ball jump from wall
sm = self.smbm
return sm.wand(sm.wor(sm.canHellRun(**hellRun),
sm.heatProof()),
sm.wor(sm.wor(sm.canPassBombPassages(),
sm.haveItem("SpeedBooster")),
sm.wor(sm.haveItem("SpaceJump"),
sm.haveItem("HiJump"),
sm.knowsWallJumpCathedralExit(),
sm.wand(sm.knowsSpringBallJumpFromWall(), sm.canUseSpringBall()))))
@Cache.decorator
def canGrappleEscape(self):
sm = self.smbm
return sm.wor(sm.wor(sm.haveItem('SpaceJump'),
sm.wand(sm.canInfiniteBombJump(), # IBJ from lava...either have grav or freeze the enemy there if hellrunning (otherwise single DBJ at the end)
sm.wor(sm.heatProof(),
sm.haveItem('Gravity'),
sm.haveItem('Ice')))),
sm.haveItem('Grapple'),
sm.wand(sm.haveItem('SpeedBooster'),
sm.wor(sm.haveItem('HiJump'), # jump from the blocks below
sm.knowsShortCharge())), # spark from across the grapple blocks
sm.wand(sm.haveItem('HiJump'), sm.canSpringBallJump())) # jump from the blocks below
@Cache.decorator
def canPassFrogSpeedwayRightToLeft(self):
sm = self.smbm
return sm.wor(sm.haveItem('SpeedBooster'),
sm.wand(sm.knowsFrogSpeedwayWithoutSpeed(),
sm.haveItem('Wave'),
sm.wor(sm.haveItem('Spazer'),
sm.haveItem('Plasma'))))
@Cache.decorator
def canEnterNorfairReserveAreaFromBubbleMoutain(self):
sm = self.smbm
return sm.wand(sm.traverse('BubbleMountainTopLeft'),
sm.wor(sm.canFly(),
sm.haveItem('Ice'),
sm.wand(sm.haveItem('HiJump'),
sm.knowsGetAroundWallJump()),
sm.wand(sm.canUseSpringBall(),
sm.knowsSpringBallJumpFromWall())))
@Cache.decorator
def canEnterNorfairReserveAreaFromBubbleMoutainTop(self):
sm = self.smbm
return sm.wand(sm.traverse('BubbleMountainTopLeft'),
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpaceJump'),
sm.knowsNorfairReserveDBoost()))
@Cache.decorator
def canPassLavaPit(self):
sm = self.smbm
nTanks4Dive = 8 / sm.getDmgReduction()[0]
if sm.haveItem('HiJump').bool == False:
nTanks4Dive = ceil(nTanks4Dive * 1.25)
return sm.wand(sm.wor(sm.wand(sm.haveItem('Gravity'), sm.haveItem('SpaceJump')),
sm.wand(sm.knowsGravityJump(), sm.haveItem('Gravity'), sm.wor(sm.haveItem('HiJump'), sm.knowsLavaDive())),
sm.wand(sm.wor(sm.wand(sm.knowsLavaDive(), sm.haveItem('HiJump')),
sm.knowsLavaDiveNoHiJump()),
sm.energyReserveCountOk(nTanks4Dive))),
sm.canUsePowerBombs()) # power bomb blocks left and right of LN entrance without any items before
@Cache.decorator
def canPassLavaPitReverse(self):
sm = self.smbm
nTanks = 2
if sm.heatProof().bool == False:
nTanks = 6
return sm.energyReserveCountOk(nTanks)
@Cache.decorator
def canPassLowerNorfairChozo(self):
sm = self.smbm
# to require one more CF if no heat protection because of distance to cover, wait times, acid...
return sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Entrance -> GT via Chozo']),
sm.canUsePowerBombs(),
sm.wor(RomPatches.has(sm.player, RomPatches.LNChozoSJCheckDisabled), sm.haveItem('SpaceJump')))
@Cache.decorator
def canExitScrewAttackArea(self):
sm = self.smbm
return sm.wand(sm.canDestroyBombWalls(),
sm.wor(sm.canFly(),
sm.wand(sm.haveItem('HiJump'),
sm.haveItem('SpeedBooster'),
sm.wor(sm.wand(sm.haveItem('ScrewAttack'), sm.knowsScrewAttackExit()),
sm.knowsScrewAttackExitWithoutScrew())),
sm.wand(sm.canUseSpringBall(),
sm.knowsSpringBallJumpFromWall()),
sm.wand(sm.canSimpleShortCharge(), # fight GT and spark out
sm.enoughStuffGT())))
@Cache.decorator
def canPassWorstRoom(self):
sm = self.smbm
return sm.wand(sm.canDestroyBombWalls(),
sm.canPassWorstRoomPirates(),
sm.wor(sm.canFly(),
sm.wand(sm.knowsWorstRoomIceCharge(), sm.haveItem('Ice'), sm.canFireChargedShots()),
sm.wor(sm.wand(sm.knowsGetAroundWallJump(), sm.haveItem('HiJump')),
sm.knowsWorstRoomWallJump()),
sm.wand(sm.knowsSpringBallJumpFromWall(), sm.canUseSpringBall())))
# checks mix of super missiles/health
def canGoThroughLowerNorfairEnemy(self, nmyHealth, nbNmy, nmyHitDmg, supDmg=300.0):
sm = self.smbm
# supers only
if sm.itemCount('Super')*5*supDmg >= nbNmy*nmyHealth:
return SMBool(True, 0, items=['Super'])
# - or with taking damage as well?
(dmgRed, redItems) = sm.getDmgReduction(envDmg=False)
dmg = nmyHitDmg / dmgRed
if sm.heatProof() and (sm.itemCount('Super')*5*supDmg)/nmyHealth + (sm.energyReserveCount()*100 - 2)/dmg >= nbNmy:
# require heat proof as long as taking damage is necessary.
# display all the available energy in the solver.
return sm.wand(sm.heatProof(), SMBool(True, 0, items=redItems+['Super', '{}-ETank - {}-Reserve'.format(self.smbm.itemCount('ETank'), self.smbm.itemCount('Reserve'))]))
return sm.knowsDodgeLowerNorfairEnemies()
def canKillRedKiHunters(self, n):
sm = self.smbm
return sm.wor(sm.haveItem('Plasma'),
sm.haveItem('ScrewAttack'),
sm.wand(sm.heatProof(), # this takes a loooong time ...
sm.wor(sm.haveItem('Spazer'),
sm.haveItem('Ice'),
sm.wand(sm.haveItem('Charge'),
sm.haveItem('Wave')))),
sm.canGoThroughLowerNorfairEnemy(1800.0, float(n), 200.0))
@Cache.decorator
def canPassThreeMuskateers(self):
sm = self.smbm
return sm.canKillRedKiHunters(6)
@Cache.decorator
def canPassRedKiHunters(self):
sm = self.smbm
return sm.canKillRedKiHunters(3)
@Cache.decorator
def canPassWastelandDessgeegas(self):
sm = self.smbm
return sm.wor(sm.haveItem('Plasma'),
sm.haveItem('ScrewAttack'),
sm.wand(sm.heatProof(), # this takes a loooong time ...
sm.wor(sm.haveItem('Spazer'),
sm.wand(sm.haveItem('Charge'),
sm.haveItem('Wave')))),
sm.itemCountOk('PowerBomb', 4),
sm.canGoThroughLowerNorfairEnemy(800.0, 3.0, 160.0))
@Cache.decorator
def canPassNinjaPirates(self):
sm = self.smbm
return sm.wor(sm.itemCountOk('Missile', 10),
sm.itemCountOk('Super', 2),
sm.haveItem('Plasma'),
sm.wor(sm.haveItem('Spazer'),
sm.wand(sm.haveItem('Charge'),
sm.wor(sm.haveItem('Wave'),
sm.haveItem('Ice')))),
sm.canShortCharge()) # echoes kill
@Cache.decorator
def canPassWorstRoomPirates(self):
sm = self.smbm
return sm.wor(sm.haveItem('ScrewAttack'),
sm.itemCountOk('Missile', 6),
sm.itemCountOk('Super', 3),
sm.wand(sm.canFireChargedShots(), sm.haveItem('Plasma')),
sm.wand(sm.haveItem('Charge'),
sm.wor(sm.haveItem('Spazer'),
sm.haveItem('Wave'),
sm.haveItem('Ice'))),
sm.knowsDodgeLowerNorfairEnemies())
# go though the pirates room filled with acid
@Cache.decorator
def canPassAmphitheaterReverse(self):
sm = self.smbm
dmgRed = sm.getDmgReduction()[0]
nTanksGrav = 4 * 4/dmgRed
nTanksNoGrav = 6 * 4/dmgRed
return sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.energyReserveCountOk(nTanksGrav)),
sm.wand(sm.energyReserveCountOk(nTanksNoGrav),
sm.knowsLavaDive())) # should be a good enough skill filter for acid wall jumps with no grav...
@Cache.decorator
def canGetBackFromRidleyZone(self):
sm = self.smbm
return sm.wand(sm.canUsePowerBombs(),
sm.wor(sm.canUseSpringBall(),
sm.canUseBombs(),
sm.itemCountOk('PowerBomb', 2),
sm.haveItem('ScrewAttack'),
sm.canShortCharge()), # speedball
# in escape you don't have PBs and can't shoot bomb blocks in long tunnels
# in wasteland and ki hunter room
sm.wnot(sm.canUseHyperBeam()))
@Cache.decorator
def canClimbRedTower(self):
sm = self.smbm
return sm.wor(sm.knowsRedTowerClimb(),
sm.haveItem('Ice'),
sm.haveItem('SpaceJump'))
@Cache.decorator
def canClimbBottomRedTower(self):
sm = self.smbm
return sm.wor(RomPatches.has(sm.player, RomPatches.RedTowerLeftPassage),
sm.haveItem('HiJump'),
sm.haveItem('Ice'),
sm.canFly(),
sm.canShortCharge())
@Cache.decorator
def canGoUpMtEverest(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpeedBooster'),
sm.canFly(),
sm.wand(sm.knowsGravityJump(),
sm.wor(sm.haveItem('HiJump'),
sm.knowsMtEverestGravJump())))),
sm.wand(sm.canDoSuitlessOuterMaridia(),
sm.haveItem('Grapple')))
@Cache.decorator
def canPassMtEverest(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpeedBooster'),
sm.canFly(),
sm.knowsGravityJump())),
sm.wand(sm.canDoSuitlessOuterMaridia(),
sm.wor(sm.haveItem('Grapple'),
sm.wand(sm.haveItem('Ice'), sm.knowsTediousMountEverest(), sm.haveItem('Super')),
sm.canDoubleSpringBallJump())))
@Cache.decorator
def canJumpUnderwater(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.knowsGravLessLevel1(),
sm.haveItem('HiJump')))
@Cache.decorator
def canDoSuitlessOuterMaridia(self):
sm = self.smbm
return sm.wand(sm.knowsGravLessLevel1(),
sm.haveItem('HiJump'),
sm.wor(sm.haveItem('Ice'),
sm.canSpringBallJump()))
@Cache.decorator
def canDoOuterMaridia(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.canDoSuitlessOuterMaridia())
@Cache.decorator
def canPassBotwoonHallway(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('SpeedBooster'),
sm.haveItem('Gravity')),
sm.wand(sm.knowsMochtroidClip(), sm.haveItem('Ice')),
sm.canCrystalFlashClip())
@Cache.decorator
def canDefeatBotwoon(self):
sm = self.smbm
return sm.wand(sm.enoughStuffBotwoon(),
sm.canPassBotwoonHallway())
# the sandpits from aqueduct
@Cache.decorator
def canAccessSandPits(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.haveItem('HiJump'),
sm.knowsGravLessLevel3()))
@Cache.decorator
def canReachCacatacAlleyFromBotowoon(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.knowsGravLessLevel2(),
sm.haveItem("HiJump"),
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('Ice'),
sm.canDoubleSpringBallJump())))
@Cache.decorator
def canPassCacatacAlley(self):
sm = self.smbm
return sm.wand(Bosses.bossDead(sm, 'Draygon'),
sm.haveItem('Morph'),
sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.knowsGravLessLevel2(),
sm.haveItem('HiJump'),
sm.haveItem('SpaceJump'))))
@Cache.decorator
def canGoThroughColosseumSuitless(self):
sm = self.smbm
return sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpaceJump'),
sm.wand(sm.haveItem('Ice'),
sm.energyReserveCountOk(int(7.0/sm.getDmgReduction(False)[0])), # mochtroid dmg
sm.knowsBotwoonToDraygonWithIce()))
@Cache.decorator
def canBotwoonExitToColosseum(self):
sm = self.smbm
# traverse Botwoon Energy Tank Room
return sm.wand(sm.wor(sm.wand(sm.haveItem('Gravity'), sm.haveItem('SpeedBooster')),
sm.wand(sm.haveItem('Morph'), sm.canJumpUnderwater())),
# after Botwoon Energy Tank Room
sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.knowsGravLessLevel2(),
sm.haveItem("HiJump"),
# get to top right door
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('Ice'), # climb mochtroids
sm.wand(sm.canDoubleSpringBallJump(),
sm.haveItem('SpaceJump'))),
sm.canGoThroughColosseumSuitless())))
@Cache.decorator
def canColosseumToBotwoonExit(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.knowsGravLessLevel2(),
sm.haveItem("HiJump"),
sm.canGoThroughColosseumSuitless()))
@Cache.decorator
def canClimbColosseum(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.knowsGravLessLevel2(),
sm.haveItem("HiJump"),
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('Ice'),
sm.knowsPreciousRoomGravJumpExit())))
@Cache.decorator
def canClimbWestSandHole(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.haveItem('HiJump'),
sm.knowsGravLessLevel3(),
sm.wor(sm.haveItem('SpaceJump'),
sm.canSpringBallJump(),
sm.knowsWestSandHoleSuitlessWallJumps())))
@Cache.decorator
def canAccessItemsInWestSandHole(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('HiJump'), # vanilla strat
sm.canUseSpringBall()),
sm.wand(sm.haveItem('SpaceJump'), # alternate strat with possible double bomb jump but no difficult wj
sm.wor(sm.canUseSpringBall(),
sm.canUseBombs())),
sm.wand(sm.canPassBombPassages(), # wjs and/or 3 tile mid air morph
sm.knowsMaridiaWallJumps()))
@Cache.decorator
def getDraygonConnection(self):
return getAccessPoint('DraygonRoomOut').ConnectedTo
@Cache.decorator
def isVanillaDraygon(self):
return SMBool(self.getDraygonConnection() == 'DraygonRoomIn')
@Cache.decorator
def canUseCrocRoomToChargeSpeed(self):
sm = self.smbm
crocRoom = getAccessPoint('Crocomire Room Top')
speedway = getAccessPoint('Crocomire Speedway Bottom')
return sm.wand(SMBool(crocRoom.ConnectedTo == 'Crocomire Speedway Bottom'),
crocRoom.traverse(sm),
speedway.traverse(sm))
@Cache.decorator
def canFightDraygon(self):
sm = self.smbm
return sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.haveItem('HiJump'),
sm.wor(sm.knowsGravLessLevel2(),
sm.knowsGravLessLevel3())))
@Cache.decorator
def canDraygonCrystalFlashSuit(self):
sm = self.smbm
return sm.wand(sm.canCrystalFlash(),
sm.knowsDraygonRoomCrystalFlash(),
# ask for 4 PB pack as an ugly workaround for
# a rando bug which can place a PB at space
# jump to "get you out" (this check is in
# PostAvailable condition of the Dray/Space
# Jump locs)
sm.itemCountOk('PowerBomb', 4))
@Cache.decorator
def canExitDraygonRoomWithGravity(self):
sm = self.smbm
return sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.canFly(),
sm.knowsGravityJump(),
sm.wand(sm.haveItem('HiJump'),
sm.haveItem('SpeedBooster'))))
@Cache.decorator
def canGrappleExitDraygon(self):
sm = self.smbm
return sm.wand(sm.haveItem('Grapple'),
sm.knowsDraygonRoomGrappleExit())
@Cache.decorator
def canExitDraygonVanilla(self):
sm = self.smbm
# to get out of draygon room:
# with gravity but without highjump/bomb/space jump: gravity jump
# to exit draygon room: grapple or crystal flash (for free shine spark)
# to exit precious room: spring ball jump, xray scope glitch or stored spark
return sm.wor(sm.canExitDraygonRoomWithGravity(),
sm.wand(sm.canDraygonCrystalFlashSuit(),
# use the spark either to exit draygon room or precious room
sm.wor(sm.canGrappleExitDraygon(),
sm.wand(sm.haveItem('XRayScope'),
sm.knowsPreciousRoomXRayExit()),
sm.canSpringBallJump())),
# spark-less exit (no CF)
sm.wand(sm.canGrappleExitDraygon(),
sm.wor(sm.wand(sm.haveItem('XRayScope'),
sm.knowsPreciousRoomXRayExit()),
sm.canSpringBallJump())),
sm.canDoubleSpringBallJump())
@Cache.decorator
def canExitDraygonRandomized(self):
sm = self.smbm
# disregard precious room
return sm.wor(sm.canExitDraygonRoomWithGravity(),
sm.canDraygonCrystalFlashSuit(),
sm.canGrappleExitDraygon(),
sm.canDoubleSpringBallJump())
@Cache.decorator
def canExitDraygon(self):
sm = self.smbm
if self.isVanillaDraygon():
return self.canExitDraygonVanilla()
else:
return self.canExitDraygonRandomized()
@Cache.decorator
def canExitPreciousRoomVanilla(self):
return SMBool(True) # handled by canExitDraygonVanilla
@Cache.decorator
def canExitPreciousRoomRandomized(self):
sm = self.smbm
suitlessRoomExit = sm.canSpringBallJump()
if suitlessRoomExit.bool == False:
if self.getDraygonConnection() == 'KraidRoomIn':
suitlessRoomExit = sm.canShortCharge() # charge spark in kraid's room
elif self.getDraygonConnection() == 'RidleyRoomIn':
suitlessRoomExit = sm.wand(sm.haveItem('XRayScope'), # get doorstuck in compatible transition
sm.knowsPreciousRoomXRayExit())
return sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.canFly(),
sm.knowsGravityJump(),
sm.haveItem('HiJump'))),
suitlessRoomExit)
@Cache.decorator
def canExitPreciousRoom(self):
if self.isVanillaDraygon():
return self.canExitPreciousRoomVanilla()
else:
return self.canExitPreciousRoomRandomized()
@Cache.decorator
def canPassDachoraRoom(self):
sm = self.smbm
return sm.wor(sm.haveItem('SpeedBooster'), sm.canDestroyBombWalls())
@Cache.decorator
def canTraverseCrabTunnelLeftToRight(self):
sm = self.smbm
return sm.wand(sm.traverse('MainStreetBottomRight'),
sm.wor(sm.haveItem('Super'),
RomPatches.has(sm.player, RomPatches.AreaRandoGatesOther)))

View File

@ -0,0 +1,994 @@
from logic.helpers import Bosses
from utils.parameters import Settings
from rom.rom_patches import RomPatches
from logic.smbool import SMBool
from graph.location import locationsDict
locationsDict["Energy Tank, Gauntlet"].AccessFrom = {
'Landing Site': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Gauntlet"].Available = (
lambda sm: sm.wor(sm.canEnterAndLeaveGauntlet(),
sm.wand(sm.canShortCharge(),
sm.canEnterAndLeaveGauntletQty(1, 0)), # thanks ponk! https://youtu.be/jil5zTBCF1s
sm.canDoLowGauntlet())
)
locationsDict["Bomb"].AccessFrom = {
'Landing Site': lambda sm: SMBool(True)
}
locationsDict["Bomb"].Available = (
lambda sm: sm.wand(sm.haveItem('Morph'),
sm.traverse('FlywayRight'))
)
locationsDict["Bomb"].PostAvailable = (
lambda sm: sm.wor(sm.knowsAlcatrazEscape(),
sm.canPassBombPassages())
)
locationsDict["Energy Tank, Terminator"].AccessFrom = {
'Landing Site': lambda sm: sm.canPassTerminatorBombWall(),
'Lower Mushrooms Left': lambda sm: sm.canPassCrateriaGreenPirates(),
'Gauntlet Top': lambda sm: sm.haveItem('Morph')
}
locationsDict["Energy Tank, Terminator"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Reserve Tank, Brinstar"].AccessFrom = {
'Green Brinstar Elevator': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.BrinReserveBlueDoors), sm.traverse('MainShaftRight'))
}
locationsDict["Reserve Tank, Brinstar"].Available = (
lambda sm: sm.wand(sm.wor(sm.canMockball(),
sm.haveItem('SpeedBooster')),
sm.wor(RomPatches.has(sm.player, RomPatches.BrinReserveBlueDoors), sm.traverse('EarlySupersRight')))
)
locationsDict["Charge Beam"].AccessFrom = {
'Big Pink': lambda sm: SMBool(True)
}
locationsDict["Charge Beam"].Available = (
lambda sm: sm.canPassBombPassages()
)
locationsDict["Morphing Ball"].AccessFrom = {
'Blue Brinstar Elevator Bottom': lambda sm: SMBool(True)
}
locationsDict["Morphing Ball"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Energy Tank, Brinstar Ceiling"].AccessFrom = {
'Blue Brinstar Elevator Bottom': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.BlueBrinstarBlueDoor), sm.traverse('ConstructionZoneRight'))
}
locationsDict["Energy Tank, Brinstar Ceiling"].Available = (
lambda sm: sm.wor(sm.knowsCeilingDBoost(),
sm.canFly(),
sm.wor(sm.haveItem('HiJump'),
sm.haveItem('Ice'),
sm.wand(sm.canUsePowerBombs(),
sm.haveItem('SpeedBooster')),
sm.canSimpleShortCharge()))
)
locationsDict["Energy Tank, Etecoons"].AccessFrom = {
'Etecoons Bottom': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Etecoons"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Energy Tank, Waterway"].AccessFrom = {
'Big Pink': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Waterway"].Available = (
lambda sm: sm.wand(sm.canUsePowerBombs(),
sm.traverse('BigPinkBottomLeft'),
sm.haveItem('SpeedBooster'),
sm.wor(sm.haveItem('Gravity'),
sm.canSimpleShortCharge())) # from the blocks above the water
)
locationsDict["Energy Tank, Brinstar Gate"].AccessFrom = {
'Big Pink': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Brinstar Gate"].Available = (
lambda sm: sm.wand(sm.traverse('BigPinkRight'),
sm.wor(sm.haveItem('Wave'),
sm.wand(sm.haveItem('Super'),
sm.haveItem('HiJump'),
sm.knowsReverseGateGlitch()),
sm.wand(sm.haveItem('Super'),
sm.knowsReverseGateGlitchHiJumpLess())))
)
locationsDict["X-Ray Scope"].AccessFrom = {
'Red Tower Top Left': lambda sm: SMBool(True)
}
locationsDict["X-Ray Scope"].Available = (
lambda sm: sm.wand(sm.canUsePowerBombs(),
sm.traverse('RedTowerLeft'),
sm.traverse('RedBrinstarFirefleaLeft'),
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpaceJump'),
sm.wand(sm.energyReserveCountOkHardRoom('X-Ray'),
sm.wor(sm.knowsXrayDboost(),
sm.wand(sm.haveItem('Ice'),
sm.wor(sm.haveItem('HiJump'), sm.knowsXrayIce())),
sm.canInfiniteBombJump(),
sm.wand(sm.haveItem('HiJump'),
sm.wor(sm.haveItem('SpeedBooster'),
sm.canSpringBallJump()))))))
)
locationsDict["Spazer"].AccessFrom = {
'East Tunnel Right': lambda sm: SMBool(True)
}
locationsDict["Spazer"].Available = (
lambda sm: sm.wand(sm.traverse('BelowSpazerTopRight'),
sm.wor(sm.canPassBombPassages(),
sm.wand(sm.haveItem('Morph'),
RomPatches.has(sm.player, RomPatches.SpazerShotBlock))))
)
locationsDict["Energy Tank, Kraid"].AccessFrom = {
'Warehouse Zeela Room Left': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Kraid"].Available = (
lambda sm: sm.wand(Bosses.bossDead(sm, 'Kraid'),
# kill the beetoms to unlock the door to get out
sm.canKillBeetoms())
)
locationsDict["Kraid"].AccessFrom = {
'KraidRoomIn': lambda sm: SMBool(True)
}
locationsDict["Kraid"].Available = (
lambda sm: sm.enoughStuffsKraid()
)
locationsDict["Varia Suit"].AccessFrom = {
'KraidRoomIn': lambda sm: SMBool(True)
}
locationsDict["Varia Suit"].Available = (
lambda sm: Bosses.bossDead(sm, 'Kraid')
)
locationsDict["Ice Beam"].AccessFrom = {
'Business Center': lambda sm: sm.traverse('BusinessCenterTopLeft')
}
locationsDict["Ice Beam"].Available = (
lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['Ice']['Norfair Entrance -> Ice Beam']),
sm.wor(sm.canPassBombPassages(), # to exit, or if you fail entrance
sm.wand(sm.haveItem('Ice'), # harder strat
sm.haveItem('Morph'),
sm.knowsIceEscape())),
sm.wor(sm.wand(sm.haveItem('Morph'),
sm.knowsMockball()),
sm.haveItem('SpeedBooster')))
)
locationsDict["Energy Tank, Crocomire"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Crocomire"].Available = (
lambda sm: sm.wand(sm.enoughStuffCroc(),
sm.wor(sm.haveItem('Grapple'),
sm.haveItem('SpaceJump'),
sm.energyReserveCountOk(3/sm.getDmgReduction()[0])))
)
locationsDict["Hi-Jump Boots"].AccessFrom = {
'Business Center': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.HiJumpAreaBlueDoor), sm.traverse('BusinessCenterBottomLeft'))
}
locationsDict["Hi-Jump Boots"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Hi-Jump Boots"].PostAvailable = (
lambda sm: sm.wor(sm.canPassBombPassages(),
sm.wand(sm.haveItem('Morph'), RomPatches.has(sm.player, RomPatches.HiJumpShotBlock)))
)
locationsDict["Grapple Beam"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Grapple Beam"].Available = (
lambda sm: sm.wand(sm.enoughStuffCroc(),
sm.wor(sm.wand(sm.haveItem('Morph'),
sm.canFly()),
sm.wand(sm.haveItem('SpeedBooster'),
sm.wor(sm.knowsShortCharge(),
sm.canUsePowerBombs())),
sm.wand(sm.haveItem('Morph'),
sm.wor(sm.haveItem('SpeedBooster'),
sm.canSpringBallJump()),
sm.haveItem('HiJump')), # jump from the yellow plateform ennemy
sm.canGreenGateGlitch()))
)
locationsDict["Grapple Beam"].PostAvailable = (
lambda sm: sm.wor(sm.haveItem('Morph'), # regular exit
sm.wand(sm.haveItem('Super'), # grapple escape reverse
sm.wor(sm.canFly(), # Grapple Tutorial Room 2
sm.haveItem('HiJump'),
sm.haveItem('Grapple')),
sm.wor(sm.haveItem('Gravity'), # Grapple Tutorial Room 3
sm.haveItem('SpaceJump'),
sm.haveItem('Grapple'))))
)
locationsDict["Reserve Tank, Norfair"].AccessFrom = {
'Bubble Mountain': lambda sm: sm.canEnterNorfairReserveAreaFromBubbleMoutain(),
'Bubble Mountain Top': lambda sm: sm.canEnterNorfairReserveAreaFromBubbleMoutainTop(),
}
locationsDict["Reserve Tank, Norfair"].Available = (
lambda sm: sm.wand(sm.haveItem('Morph'), sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Norfair Reserve']))
)
locationsDict["Speed Booster"].AccessFrom = {
'Bubble Mountain Top': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.SpeedAreaBlueDoors),
sm.wand(sm.traverse('BubbleMountainTopRight'),
sm.traverse('SpeedBoosterHallRight')))
}
locationsDict["Speed Booster"].Available = (
lambda sm: sm.canHellRunToSpeedBooster()
)
locationsDict["Wave Beam"].AccessFrom = {
'Bubble Mountain Top': lambda sm: sm.canAccessDoubleChamberItems()
}
locationsDict["Wave Beam"].Available = (
lambda sm: sm.traverse('DoubleChamberRight')
)
locationsDict["Wave Beam"].PostAvailable = (
lambda sm: sm.wor(sm.haveItem('Morph'), # exit through lower passage under the spikes
sm.wand(sm.wor(sm.haveItem('SpaceJump'), # exit through blue gate
sm.haveItem('Grapple')),
sm.wor(sm.wand(sm.canBlueGateGlitch(), sm.heatProof()), # hell run + green gate glitch is too much
sm.haveItem('Wave'))))
)
locationsDict["Ridley"].AccessFrom = {
'RidleyRoomIn': lambda sm: SMBool(True)
}
locationsDict["Ridley"].Available = (
lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), sm.enoughStuffsRidley())
)
locationsDict["Energy Tank, Ridley"].AccessFrom = {
'RidleyRoomIn': lambda sm: sm.haveItem('Ridley')
}
locationsDict["Energy Tank, Ridley"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Screw Attack"].AccessFrom = {
'Screw Attack Bottom': lambda sm: SMBool(True)
}
locationsDict["Screw Attack"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Screw Attack"].PostAvailable = (
lambda sm: sm.canExitScrewAttackArea()
)
locationsDict["Energy Tank, Firefleas"].AccessFrom = {
'Firefleas': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Firefleas"].Available = (
lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.FirefleasRemoveFune),
# get past the fune
sm.haveItem('Super'),
sm.canPassBombPassages(),
sm.canUseSpringBall())
)
locationsDict["Energy Tank, Firefleas"].PostAvailable = (
lambda sm: sm.wor(sm.knowsFirefleasWalljump(),
sm.wor(sm.haveItem('Ice'),
sm.haveItem('HiJump'),
sm.canFly(),
sm.canSpringBallJump()))
)
locationsDict["Reserve Tank, Wrecked Ship"].AccessFrom = {
'Wrecked Ship Main': lambda sm: SMBool(True)
}
locationsDict["Reserve Tank, Wrecked Ship"].Available = (
lambda sm: sm.wand(sm.canUsePowerBombs(),
sm.haveItem('SpeedBooster'),
sm.canPassBowling())
)
locationsDict["Energy Tank, Wrecked Ship"].AccessFrom = {
'Wrecked Ship Back': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.WsEtankBlueDoor),
sm.traverse('ElectricDeathRoomTopLeft'))
}
locationsDict["Energy Tank, Wrecked Ship"].Available = (
lambda sm: sm.wor(Bosses.bossDead(sm, 'Phantoon'),
RomPatches.has(sm.player, RomPatches.WsEtankPhantoonAlive))
)
locationsDict["Phantoon"].AccessFrom = {
'PhantoonRoomIn': lambda sm: SMBool(True)
}
locationsDict["Phantoon"].Available = (
lambda sm: sm.enoughStuffsPhantoon()
)
locationsDict["Right Super, Wrecked Ship"].AccessFrom = {
'Wrecked Ship Main': lambda sm: Bosses.bossDead(sm, 'Phantoon')
}
locationsDict["Right Super, Wrecked Ship"].Available = (
lambda sm: sm.canPassBombPassages()
)
locationsDict["Gravity Suit"].AccessFrom = {
'Wrecked Ship Main': lambda sm: SMBool(True)
}
locationsDict["Gravity Suit"].Available = (
lambda sm: sm.wand(sm.canPassBombPassages(),
sm.canPassBowling())
)
locationsDict["Energy Tank, Mama turtle"].AccessFrom = {
'Main Street Bottom': lambda sm: sm.wand(sm.canDoOuterMaridia(),
sm.wor(sm.traverse('FishTankRight'),
RomPatches.has(sm.player, RomPatches.MamaTurtleBlueDoor)),
sm.wor(sm.wor(sm.canFly(),
sm.wand(sm.haveItem('Gravity'),
sm.haveItem('SpeedBooster')),
sm.wand(sm.haveItem('HiJump'),
sm.haveItem('SpeedBooster'),
sm.knowsHiJumpMamaTurtle())),
sm.wor(sm.wand(sm.canUseSpringBall(),
sm.wor(sm.wand(sm.haveItem('HiJump'),
sm.knowsSpringBallJump()),
sm.knowsSpringBallJumpFromWall())),
sm.haveItem('Grapple')))),
'Mama Turtle': lambda sm: SMBool(True)
}
locationsDict["Energy Tank, Mama turtle"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Plasma Beam"].AccessFrom = {
'Toilet Top': lambda sm: SMBool(True)
}
locationsDict["Plasma Beam"].Available = (
lambda sm: Bosses.bossDead(sm, 'Draygon')
)
locationsDict["Plasma Beam"].PostAvailable = (
lambda sm: sm.wand(sm.wor(sm.wand(sm.canShortCharge(),
sm.knowsKillPlasmaPiratesWithSpark()),
sm.wand(sm.canFireChargedShots(),
sm.knowsKillPlasmaPiratesWithCharge(),
# 160/80/40 dmg * 4 ground plasma pirates
# => 640/320/160 damage take required
# check below is 1099/599/299 (give margin for taking dmg a bit)
# (* 4 for nerfed charge, since you need to take hits 4 times instead of one)
sm.energyReserveCountOk(int(10.0 * sm.getPiratesPseudoScrewCoeff()/sm.getDmgReduction(False)[0]))),
sm.haveItem('ScrewAttack'),
sm.haveItem('Plasma')),
sm.wor(sm.canFly(),
sm.wand(sm.haveItem('HiJump'),
sm.knowsGetAroundWallJump()),
sm.canShortCharge(),
sm.wand(sm.canSpringBallJump(),
sm.knowsSpringBallJumpFromWall())))
)
locationsDict["Reserve Tank, Maridia"].AccessFrom = {
'Left Sandpit': lambda sm: sm.canClimbWestSandHole()
}
locationsDict["Reserve Tank, Maridia"].Available = (
lambda sm: sm.canAccessItemsInWestSandHole()
)
locationsDict["Spring Ball"].AccessFrom = {
'Oasis Bottom': lambda sm: sm.canTraverseSandPits()
}
locationsDict["Spring Ball"].Available = (
lambda sm: sm.wand(sm.canUsePowerBombs(), # in Shaktool room to let Shaktool access the sand blocks
sm.wor(sm.wand(sm.haveItem('Ice'), # puyo clip
sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.knowsPuyoClip()),
sm.wand(sm.haveItem('Gravity'),
sm.haveItem('XRayScope'),
sm.knowsPuyoClipXRay()),
sm.knowsSuitlessPuyoClip())),
sm.wand(sm.haveItem('Grapple'), # go through grapple block
sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.wor(sm.wand(sm.haveItem('HiJump'), sm.knowsAccessSpringBallWithHiJump()),
sm.haveItem('SpaceJump')),
sm.knowsAccessSpringBallWithGravJump(),
sm.wand(sm.haveItem('Bomb'),
sm.wor(sm.knowsAccessSpringBallWithBombJumps(),
sm.wand(sm.haveItem('SpringBall'),
sm.knowsAccessSpringBallWithSpringBallBombJumps()))),
sm.wand(sm.haveItem('SpringBall'), sm.knowsAccessSpringBallWithSpringBallJump()))),
sm.wand(sm.haveItem('SpaceJump'), sm.knowsAccessSpringBallWithFlatley()))),
sm.wand(sm.haveItem('XRayScope'), sm.knowsAccessSpringBallWithXRayClimb()), # XRay climb
sm.canCrystalFlashClip()),
sm.wor(sm.haveItem('Gravity'), sm.canUseSpringBall())) # acess the item in spring ball room
)
locationsDict["Spring Ball"].PostAvailable = (
lambda sm: sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.wor(sm.haveItem('HiJump'),
sm.canFly(),
sm.knowsMaridiaWallJumps())),
sm.canSpringBallJump())
)
locationsDict["Energy Tank, Botwoon"].AccessFrom = {
'Post Botwoon': lambda sm: sm.canJumpUnderwater()
}
locationsDict["Energy Tank, Botwoon"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Draygon"].AccessFrom = {
'Draygon Room Bottom': lambda sm: SMBool(True)
}
locationsDict["Draygon"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Space Jump"].AccessFrom = {
'Draygon Room Bottom': lambda sm: SMBool(True)
}
locationsDict["Space Jump"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Space Jump"].PostAvailable = (
lambda sm: Bosses.bossDead(sm, 'Draygon')
)
locationsDict["Mother Brain"].AccessFrom = {
'Golden Four': lambda sm: Bosses.allBossesDead(sm)
}
locationsDict["Mother Brain"].Available = (
lambda sm: sm.enoughStuffTourian()
)
locationsDict["Power Bomb (Crateria surface)"].AccessFrom = {
'Landing Site': lambda sm: SMBool(True)
}
locationsDict["Power Bomb (Crateria surface)"].Available = (
lambda sm: sm.wand(sm.traverse('LandingSiteTopRight'),
sm.wor(sm.haveItem('SpeedBooster'),
sm.canFly()))
)
locationsDict["Missile (outside Wrecked Ship bottom)"].AccessFrom = {
'West Ocean Left': lambda sm: SMBool(True)
}
locationsDict["Missile (outside Wrecked Ship bottom)"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Missile (outside Wrecked Ship bottom)"].PostAvailable = (
lambda sm: sm.canPassBombPassages()
)
locationsDict["Missile (outside Wrecked Ship top)"].AccessFrom = {
'Wrecked Ship Main': lambda sm: SMBool(True)
}
locationsDict["Missile (outside Wrecked Ship top)"].Available = (
lambda sm: Bosses.bossDead(sm, 'Phantoon')
)
locationsDict["Missile (outside Wrecked Ship middle)"].AccessFrom = {
'Wrecked Ship Main': lambda sm: SMBool(True)
}
locationsDict["Missile (outside Wrecked Ship middle)"].Available = (
lambda sm: sm.wand(sm.haveItem('Super'), sm.haveItem('Morph'), Bosses.bossDead(sm, 'Phantoon'))
)
locationsDict["Missile (Crateria moat)"].AccessFrom = {
'Moat Left': lambda sm: SMBool(True)
}
locationsDict["Missile (Crateria moat)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (Crateria bottom)"].AccessFrom = {
'Landing Site': lambda sm: SMBool(True)
}
locationsDict["Missile (Crateria bottom)"].Available = (
lambda sm: sm.wor(sm.canDestroyBombWalls(),
sm.wand(sm.haveItem('SpeedBooster'),
sm.knowsOldMBWithSpeed()))
)
locationsDict["Missile (Crateria gauntlet right)"].AccessFrom = {
'Landing Site': lambda sm: sm.wor(sm.wand(sm.canEnterAndLeaveGauntlet(),
sm.canPassBombPassages()),
sm.canDoLowGauntlet()),
'Gauntlet Top': lambda sm: SMBool(True)
}
locationsDict["Missile (Crateria gauntlet right)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (Crateria gauntlet left)"].AccessFrom = {
'Landing Site': lambda sm: sm.wor(sm.wand(sm.canEnterAndLeaveGauntlet(),
sm.canPassBombPassages()),
sm.canDoLowGauntlet()),
'Gauntlet Top': lambda sm: SMBool(True)
}
locationsDict["Missile (Crateria gauntlet left)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Super Missile (Crateria)"].AccessFrom = {
'Landing Site': lambda sm: SMBool(True)
}
locationsDict["Super Missile (Crateria)"].Available = (
lambda sm: sm.wand(sm.canPassBombPassages(),
sm.traverse("ClimbRight"),
sm.haveItem('SpeedBooster'),
# reserves are hard to trigger midspark when not having ETanks
sm.wor(sm.wand(sm.energyReserveCountOk(2), sm.itemCountOk('ETank', 1)), # need energy to get out
sm.wand(sm.itemCountOk('ETank', 1),
sm.wor(sm.haveItem('Grapple'), # use grapple/space or dmg protection to get out
sm.haveItem('SpaceJump'),
sm.heatProof()))),
sm.wor(sm.haveItem('Ice'),
sm.wand(sm.canSimpleShortCharge(), sm.canUsePowerBombs()))) # there's also a dboost involved in simple short charge or you have to kill the yellow enemies with some power bombs
)
locationsDict["Missile (Crateria middle)"].AccessFrom = {
'Landing Site': lambda sm: SMBool(True)
}
locationsDict["Missile (Crateria middle)"].Available = (
lambda sm: sm.canPassBombPassages()
)
locationsDict["Power Bomb (green Brinstar bottom)"].AccessFrom = {
'Etecoons Bottom': lambda sm: SMBool(True)
}
locationsDict["Power Bomb (green Brinstar bottom)"].Available = (
lambda sm: sm.wand(sm.haveItem('Morph'),
sm.canKillBeetoms())
)
locationsDict["Super Missile (pink Brinstar)"].AccessFrom = {
'Big Pink': lambda sm: SMBool(True)
}
locationsDict["Super Missile (pink Brinstar)"].Available = (
lambda sm: sm.wor(sm.wand(sm.traverse('BigPinkTopRight'),
sm.enoughStuffSporeSpawn()),
# back way into spore spawn
sm.wand(sm.canOpenGreenDoors(),
sm.canPassBombPassages()))
)
locationsDict["Super Missile (pink Brinstar)"].PostAvailable = (
lambda sm: sm.wand(sm.canOpenGreenDoors(),
sm.canPassBombPassages())
)
locationsDict["Missile (green Brinstar below super missile)"].AccessFrom = {
'Green Brinstar Elevator': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.BrinReserveBlueDoors), sm.traverse('MainShaftRight'))
}
locationsDict["Missile (green Brinstar below super missile)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (green Brinstar below super missile)"].PostAvailable = (
lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.EarlySupersShotBlock), sm.canPassBombPassages())
)
locationsDict["Super Missile (green Brinstar top)"].AccessFrom = {
'Green Brinstar Elevator': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.BrinReserveBlueDoors), sm.traverse('MainShaftRight'))
}
locationsDict["Super Missile (green Brinstar top)"].Available = (
lambda sm: sm.wor(sm.canMockball(),
sm.haveItem('SpeedBooster'))
)
locationsDict["Missile (green Brinstar behind missile)"].AccessFrom = {
'Green Brinstar Elevator': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.BrinReserveBlueDoors), sm.traverse('MainShaftRight'))
}
locationsDict["Missile (green Brinstar behind missile)"].Available = (
lambda sm: sm.wand(sm.haveItem('Morph'),
sm.wor(sm.canMockball(),
sm.haveItem('SpeedBooster')),
sm.traverse('EarlySupersRight'),
sm.wor(sm.canPassBombPassages(),
sm.wand(sm.knowsRonPopeilScrew(),
sm.haveItem('ScrewAttack'))))
)
locationsDict["Missile (green Brinstar behind reserve tank)"].AccessFrom = {
'Green Brinstar Elevator': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.BrinReserveBlueDoors), sm.traverse('MainShaftRight'))
}
locationsDict["Missile (green Brinstar behind reserve tank)"].Available = (
lambda sm: sm.wand(sm.traverse('EarlySupersRight'),
sm.haveItem('Morph'),
sm.wor(sm.canMockball(),
sm.haveItem('SpeedBooster')))
)
locationsDict["Missile (pink Brinstar top)"].AccessFrom = {
'Big Pink': lambda sm: SMBool(True)
}
locationsDict["Missile (pink Brinstar top)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (pink Brinstar bottom)"].AccessFrom = {
'Big Pink': lambda sm: SMBool(True)
}
locationsDict["Missile (pink Brinstar bottom)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Power Bomb (pink Brinstar)"].AccessFrom = {
'Big Pink': lambda sm: SMBool(True)
}
locationsDict["Power Bomb (pink Brinstar)"].Available = (
lambda sm: sm.wand(sm.canUsePowerBombs(),
sm.haveItem('Super'))
)
locationsDict["Missile (green Brinstar pipe)"].AccessFrom = {
'Green Hill Zone Top Right': lambda sm: SMBool(True)
}
locationsDict["Missile (green Brinstar pipe)"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Power Bomb (blue Brinstar)"].AccessFrom = {
'Blue Brinstar Elevator Bottom': lambda sm: sm.canUsePowerBombs(),
'Morph Ball Room Left': lambda sm: sm.wor(sm.canPassBombPassages(),
sm.wand(sm.haveItem('Morph'),
sm.canShortCharge())) # speedball
}
locationsDict["Power Bomb (blue Brinstar)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (blue Brinstar middle)"].AccessFrom = {
'Blue Brinstar Elevator Bottom': lambda sm: SMBool(True)
}
locationsDict["Missile (blue Brinstar middle)"].Available = (
lambda sm: sm.wand(sm.wor(RomPatches.has(sm.player, RomPatches.BlueBrinstarMissile), sm.haveItem('Morph')),
sm.wor(RomPatches.has(sm.player, RomPatches.BlueBrinstarBlueDoor), sm.traverse('ConstructionZoneRight')))
)
locationsDict["Super Missile (green Brinstar bottom)"].AccessFrom = {
'Etecoons Supers': lambda sm: SMBool(True)
}
locationsDict["Super Missile (green Brinstar bottom)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (blue Brinstar bottom)"].AccessFrom = {
'Blue Brinstar Elevator Bottom': lambda sm: SMBool(True)
}
locationsDict["Missile (blue Brinstar bottom)"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Missile (blue Brinstar top)"].AccessFrom = {
'Blue Brinstar Elevator Bottom': lambda sm: SMBool(True)
}
locationsDict["Missile (blue Brinstar top)"].Available = (
lambda sm: sm.canAccessBillyMays()
)
locationsDict["Missile (blue Brinstar behind missile)"].AccessFrom = {
'Blue Brinstar Elevator Bottom': lambda sm: SMBool(True)
}
locationsDict["Missile (blue Brinstar behind missile)"].Available = (
lambda sm: sm.canAccessBillyMays()
)
locationsDict["Power Bomb (red Brinstar sidehopper room)"].AccessFrom = {
'Red Brinstar Elevator': lambda sm: SMBool(True)
}
locationsDict["Power Bomb (red Brinstar sidehopper room)"].Available = (
lambda sm: sm.wand(sm.traverse('RedTowerElevatorTopLeft'),
sm.canUsePowerBombs())
)
locationsDict["Power Bomb (red Brinstar spike room)"].AccessFrom = {
'Red Brinstar Elevator': lambda sm: SMBool(True)
}
locationsDict["Power Bomb (red Brinstar spike room)"].Available = (
lambda sm: sm.traverse('RedTowerElevatorBottomLeft')
)
locationsDict["Missile (red Brinstar spike room)"].AccessFrom = {
'Red Brinstar Elevator': lambda sm: SMBool(True)
}
locationsDict["Missile (red Brinstar spike room)"].Available = (
lambda sm: sm.wand(sm.traverse('RedTowerElevatorBottomLeft'),
sm.canUsePowerBombs())
)
locationsDict["Missile (Kraid)"].AccessFrom = {
'Warehouse Zeela Room Left': lambda sm: SMBool(True)
}
locationsDict["Missile (Kraid)"].Available = (
lambda sm: sm.canUsePowerBombs()
)
locationsDict["Missile (lava room)"].AccessFrom = {
'Cathedral': lambda sm: SMBool(True)
}
locationsDict["Missile (lava room)"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Missile (below Ice Beam)"].AccessFrom = {
'Business Center': lambda sm: sm.wand(sm.traverse('BusinessCenterTopLeft'),
sm.canUsePowerBombs(),
sm.canHellRun(**Settings.hellRunsTable['Ice']['Norfair Entrance -> Ice Beam']),
sm.wor(sm.wand(sm.haveItem('Morph'),
sm.knowsMockball()),
sm.haveItem('SpeedBooster'))),
'Crocomire Speedway Bottom': lambda sm: sm.wand(sm.canUseCrocRoomToChargeSpeed(),
sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Croc -> Ice Missiles']),
sm.haveItem('SpeedBooster'),
sm.knowsIceMissileFromCroc())
}
locationsDict["Missile (below Ice Beam)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (above Crocomire)"].AccessFrom = {
'Crocomire Speedway Bottom': lambda sm: sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Croc -> Grapple Escape Missiles'])
}
locationsDict["Missile (above Crocomire)"].Available = (
lambda sm: sm.canGrappleEscape()
)
locationsDict["Missile (Hi-Jump Boots)"].AccessFrom = {
'Business Center': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.HiJumpAreaBlueDoor), sm.traverse('BusinessCenterBottomLeft'))
}
locationsDict["Missile (Hi-Jump Boots)"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Missile (Hi-Jump Boots)"].PostAvailable = (
lambda sm: sm.wor(sm.canPassBombPassages(),
sm.wand(RomPatches.has(sm.player, RomPatches.HiJumpShotBlock), sm.haveItem('Morph')))
)
locationsDict["Energy Tank (Hi-Jump Boots)"].AccessFrom = {
'Business Center': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.HiJumpAreaBlueDoor), sm.traverse('BusinessCenterBottomLeft'))
}
locationsDict["Energy Tank (Hi-Jump Boots)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Power Bomb (Crocomire)"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Power Bomb (Crocomire)"].Available = (
lambda sm: sm.wand(sm.traverse('PostCrocomireUpperLeft'),
sm.enoughStuffCroc(),
sm.wor(sm.wor(sm.canFly(),
sm.haveItem('Grapple'),
sm.wand(sm.haveItem('SpeedBooster'),
sm.wor(sm.heatProof(),
sm.energyReserveCountOk(1)))), # spark from the room before
sm.wor(sm.haveItem('HiJump'), # run and jump from yellow platform
sm.wand(sm.haveItem('Ice'),
sm.knowsCrocPBsIce()),
sm.knowsCrocPBsDBoost())))
)
locationsDict["Missile (below Crocomire)"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Missile (below Crocomire)"].Available = (
lambda sm: sm.wand(sm.traverse('PostCrocomireShaftRight'), sm.enoughStuffCroc(), sm.haveItem('Morph'))
)
locationsDict["Missile (Grapple Beam)"].AccessFrom = {
'Crocomire Room Top': lambda sm: SMBool(True)
}
locationsDict["Missile (Grapple Beam)"].Available = (
lambda sm: sm.wand(sm.enoughStuffCroc(),
sm.wor(sm.wor(sm.wand(sm.haveItem('Morph'), # from below
sm.canFly()),
sm.wand(sm.haveItem('SpeedBooster'),
sm.wor(sm.knowsShortCharge(),
sm.canUsePowerBombs()))),
sm.wand(sm.canGreenGateGlitch(), # from grapple room
sm.canFly()))) # TODO::test if accessible with a spark (short charge), and how many etanks required
)
locationsDict["Missile (Grapple Beam)"].PostAvailable = (
lambda sm: sm.wor(sm.haveItem('Morph'), # normal exit
sm.wand(sm.haveItem('Super'), # go back to grapple room
sm.wor(sm.haveItem('SpaceJump'),
sm.wand(sm.haveItem('SpeedBooster'), sm.haveItem('HiJump'))))) # jump from the yellow plateform ennemy
)
locationsDict["Missile (Norfair Reserve Tank)"].AccessFrom = {
'Bubble Mountain': lambda sm: sm.canEnterNorfairReserveAreaFromBubbleMoutain(),
'Bubble Mountain Top': lambda sm: sm.canEnterNorfairReserveAreaFromBubbleMoutainTop()
}
locationsDict["Missile (Norfair Reserve Tank)"].Available = (
lambda sm: sm.wand(sm.haveItem('Morph'), sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Norfair Reserve']))
)
locationsDict["Missile (bubble Norfair green door)"].AccessFrom = {
'Bubble Mountain': lambda sm: sm.canEnterNorfairReserveAreaFromBubbleMoutain(),
'Bubble Mountain Top': lambda sm: sm.canEnterNorfairReserveAreaFromBubbleMoutainTop()
}
locationsDict["Missile (bubble Norfair green door)"].Available = (
lambda sm: sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Norfair Reserve Missiles'])
)
locationsDict["Missile (bubble Norfair)"].AccessFrom = {
'Bubble Mountain': lambda sm: SMBool(True)
}
locationsDict["Missile (bubble Norfair)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (Speed Booster)"].AccessFrom = {
'Bubble Mountain Top': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.SpeedAreaBlueDoors),
sm.traverse('BubbleMountainTopRight'))
}
locationsDict["Missile (Speed Booster)"].Available = (
lambda sm: sm.canHellRunToSpeedBooster()
)
locationsDict["Missile (Wave Beam)"].AccessFrom = {
'Bubble Mountain Top': lambda sm: sm.canAccessDoubleChamberItems()
}
locationsDict["Missile (Wave Beam)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (Gold Torizo)"].AccessFrom = {
'LN Above GT': lambda sm: SMBool(True)
}
locationsDict["Missile (Gold Torizo)"].Available = (
lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])
)
locationsDict["Missile (Gold Torizo)"].PostAvailable = (
lambda sm: sm.enoughStuffGT()
)
locationsDict["Super Missile (Gold Torizo)"].AccessFrom = {
'Screw Attack Bottom': lambda sm: SMBool(True)
}
locationsDict["Super Missile (Gold Torizo)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Super Missile (Gold Torizo)"].PostAvailable = (
lambda sm: sm.enoughStuffGT()
)
locationsDict["Missile (Mickey Mouse room)"].AccessFrom = {
'LN Entrance': lambda sm: sm.wand(sm.canUsePowerBombs(), sm.canPassWorstRoom()),
}
locationsDict["Missile (Mickey Mouse room)"].Available = (
lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])
)
locationsDict["Missile (lower Norfair above fire flea room)"].AccessFrom = {
'Firefleas': lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])
}
locationsDict["Missile (lower Norfair above fire flea room)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Power Bomb (lower Norfair above fire flea room)"].AccessFrom = {
'Firefleas Top': lambda sm: SMBool(True)
}
locationsDict["Power Bomb (lower Norfair above fire flea room)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Power Bomb (Power Bombs of shame)"].AccessFrom = {
'Ridley Zone': lambda sm: sm.canUsePowerBombs()
}
locationsDict["Power Bomb (Power Bombs of shame)"].Available = (
lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])
)
locationsDict["Missile (lower Norfair near Wave Beam)"].AccessFrom = {
'Firefleas': lambda sm: SMBool(True)
}
locationsDict["Missile (lower Norfair near Wave Beam)"].Available = (
lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']),
sm.canDestroyBombWalls(),
sm.haveItem('Morph'))
)
locationsDict["Missile (Wrecked Ship middle)"].AccessFrom = {
'Wrecked Ship Main': lambda sm: SMBool(True)
}
locationsDict["Missile (Wrecked Ship middle)"].Available = (
lambda sm: sm.canPassBombPassages()
)
locationsDict["Missile (Gravity Suit)"].AccessFrom = {
'Wrecked Ship Main': lambda sm: SMBool(True)
}
locationsDict["Missile (Gravity Suit)"].Available = (
lambda sm: sm.wand(sm.canPassBowling(),
sm.canPassBombPassages())
)
locationsDict["Missile (Wrecked Ship top)"].AccessFrom = {
'Wrecked Ship Main': lambda sm: SMBool(True)
}
locationsDict["Missile (Wrecked Ship top)"].Available = (
lambda sm: Bosses.bossDead(sm, 'Phantoon')
)
locationsDict["Super Missile (Wrecked Ship left)"].AccessFrom = {
'Wrecked Ship Main': lambda sm: SMBool(True)
}
locationsDict["Super Missile (Wrecked Ship left)"].Available = (
lambda sm: Bosses.bossDead(sm, 'Phantoon')
)
locationsDict["Missile (green Maridia shinespark)"].AccessFrom = {
'Main Street Bottom': lambda sm: SMBool(True)
}
locationsDict["Missile (green Maridia shinespark)"].Available = (
lambda sm: sm.wand(sm.haveItem('Gravity'),
sm.haveItem('SpeedBooster'),
sm.wor(sm.wand(sm.traverse('MainStreetBottomRight'), # run from room on the right
sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoGatesOther),
sm.haveItem('Super')),
sm.itemCountOk('ETank', 1)), # etank for the spark since sparking from low ground
sm.canSimpleShortCharge())) # run from above
)
locationsDict["Super Missile (green Maridia)"].AccessFrom = {
'Main Street Bottom': lambda sm: sm.canDoOuterMaridia()
}
locationsDict["Super Missile (green Maridia)"].Available = (
lambda sm: sm.haveItem('Morph')
)
locationsDict["Missile (green Maridia tatori)"].AccessFrom = {
'Main Street Bottom': lambda sm: sm.wand(sm.wor(sm.traverse('FishTankRight'),
RomPatches.has(sm.player, RomPatches.MamaTurtleBlueDoor)),
sm.canDoOuterMaridia()),
'Mama Turtle': lambda sm: SMBool(True)
}
locationsDict["Missile (green Maridia tatori)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Super Missile (yellow Maridia)"].AccessFrom = {
'Watering Hole Bottom': lambda sm: SMBool(True)
}
locationsDict["Super Missile (yellow Maridia)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (yellow Maridia super missile)"].AccessFrom = {
'Watering Hole Bottom': lambda sm: SMBool(True)
}
locationsDict["Missile (yellow Maridia super missile)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (yellow Maridia false wall)"].AccessFrom = {
'Beach': lambda sm: SMBool(True)
}
locationsDict["Missile (yellow Maridia false wall)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (left Maridia sand pit room)"].AccessFrom = {
'Left Sandpit': lambda sm: sm.canClimbWestSandHole()
}
locationsDict["Missile (left Maridia sand pit room)"].Available = (
lambda sm: sm.canAccessItemsInWestSandHole()
)
locationsDict["Missile (right Maridia sand pit room)"].AccessFrom = {
'Right Sandpit': lambda sm: SMBool(True)
}
locationsDict["Missile (right Maridia sand pit room)"].Available = (
lambda sm: sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.haveItem('HiJump'),
sm.knowsGravLessLevel3()))
)
locationsDict["Power Bomb (right Maridia sand pit room)"].AccessFrom = {
'Right Sandpit': lambda sm: sm.haveItem('Morph')
}
locationsDict["Power Bomb (right Maridia sand pit room)"].Available = (
lambda sm: sm.wor(sm.haveItem('Gravity'),
sm.wand(sm.knowsGravLessLevel3(),
sm.haveItem('HiJump'),
sm.canSpringBallJump())) # https://www.youtube.com/watch?v=7LYYxphRRT0
)
locationsDict["Missile (pink Maridia)"].AccessFrom = {
'Aqueduct': lambda sm: SMBool(True)
}
locationsDict["Missile (pink Maridia)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Super Missile (pink Maridia)"].AccessFrom = {
'Aqueduct': lambda sm: SMBool(True)
}
locationsDict["Super Missile (pink Maridia)"].Available = (
lambda sm: SMBool(True)
)
locationsDict["Missile (Draygon)"].AccessFrom = {
'Precious Room Top': lambda sm: SMBool(True)
}
locationsDict["Missile (Draygon)"].Available = (
lambda sm: SMBool(True)
)
# TODO::use the dict in solver/randomizer
# create the list that the solver/randomizer use
locations = [loc for loc in locationsDict.values()]
class LocationsHelper:
# used by FillerRandom to know how many front fill steps it must perform
def getRandomFillHelp(startLocation):
helpByAp = {
"Firefleas Top": 3,
"Aqueduct": 1,
"Mama Turtle": 1,
"Watering Hole": 2,
"Etecoons Supers": 2,
"Gauntlet Top":1,
"Bubble Mountain":1
}
return helpByAp[startLocation] if startLocation in helpByAp else 0
# for a given start AP, gives:
# - locations that can be used as majors/chozo in the start area
# - locations to preserve in the split
# - number of necessary majors locations to add in the start area,
# - number of necessary chozo locations to add in the start area
# locs are taken in the first n in the list
def getStartMajors(startLocation):
majLocsByAp = {
'Gauntlet Top': ([
"Missile (Crateria gauntlet right)",
"Missile (Crateria gauntlet left)"
], ["Energy Tank, Terminator"], 1, 2),
'Green Brinstar Elevator': ([
"Missile (green Brinstar below super missile)"
], ["Reserve Tank, Brinstar"], 1, 1),
'Big Pink': ([
"Missile (pink Brinstar top)",
"Missile (pink Brinstar bottom)"
], ["Charge Beam"], 1, 2),
'Etecoons Supers': ([
"Energy Tank, Etecoons",
"Super Missile (green Brinstar bottom)",
], ["Energy Tank, Etecoons"], 1, 2),
'Firefleas Top': ([
"Power Bomb (lower Norfair above fire flea room)",
"Energy Tank, Firefleas",
"Missile (lower Norfair near Wave Beam)",
"Missile (lower Norfair above fire flea room)"
], ["Energy Tank, Firefleas"], 3, 4),
'Business Center': ([
"Energy Tank (Hi-Jump Boots)",
], ["Hi-Jump Boots"], 1, 1),
'Bubble Mountain': ([
"Missile (bubble Norfair)"
], ["Speed Booster", "Wave Beam"], 1, 1),
'Mama Turtle': ([
"Energy Tank, Mama turtle",
"Missile (green Maridia tatori)",
"Super Missile (green Maridia)"
], ["Energy Tank, Mama turtle"], 2, 3),
'Watering Hole': ([
"Missile (yellow Maridia super missile)",
"Super Missile (yellow Maridia)",
"Missile (yellow Maridia false wall)"
], [], 2, 3),
'Aqueduct': ([
"Missile (pink Maridia)",
"Super Missile (pink Maridia)",
"Missile (right Maridia sand pit room)"
], ["Reserve Tank, Maridia"], 2, 3)
}
return majLocsByAp[startLocation] if startLocation in majLocsByAp else ([],[],0,0)

View File

@ -0,0 +1,63 @@
# the caching decorator for helpers functions
class VersionedCache(object):
__slots__ = ( 'cache', 'masterCache', 'nextSlot', 'size')
def __init__(self):
self.cache = []
self.masterCache = {}
self.nextSlot = 0
self.size = 0
def reset(self):
# reinit the whole cache
self.masterCache = {}
self.update(0)
def update(self, newKey):
cache = self.masterCache.get(newKey, None)
if cache is None:
cache = [ None ] * self.size
self.masterCache[newKey] = cache
self.cache = cache
def decorator(self, func):
return self._decorate(func.__name__, self._new_slot(), func)
# for lambdas
def ldeco(self, func):
return self._decorate(func.__code__, self._new_slot(), func)
def _new_slot(self):
slot = self.nextSlot
self.nextSlot += 1
self.size += 1
return slot
def _decorate(self, name, slot, func):
def _decorator(arg):
#ret = self.cache[slot]
#if ret is not None:
# return ret
#else:
ret = func(arg)
# self.cache[slot] = ret
return ret
return _decorator
Cache = VersionedCache()
class RequestCache(object):
def __init__(self):
self.results = {}
def request(self, request, *args):
return ''.join([request] + [str(arg) for arg in args])
def store(self, request, result):
self.results[request] = result
def get(self, request):
return self.results[request] if request in self.results else None
def reset(self):
self.results.clear()

View File

@ -0,0 +1,831 @@
import math
from logic.cache import Cache
from logic.smbool import SMBool, smboolFalse
from utils.parameters import Settings, easy, medium, diff2text
from rom.rom_patches import RomPatches
from utils.utils import normalizeRounding
class Helpers(object):
def __init__(self, smbm):
self.smbm = smbm
# return bool
def haveItemCount(self, item, count):
return self.smbm.itemCount(item) >= count
# return integer
@Cache.decorator
def energyReserveCount(self):
return self.smbm.itemCount('ETank') + self.smbm.itemCount('Reserve')
def energyReserveCountOkDiff(self, difficulties, mult=1.0):
if difficulties is None or len(difficulties) == 0:
return smboolFalse
def f(difficulty):
return self.smbm.energyReserveCountOk(normalizeRounding(difficulty[0] / mult), difficulty=difficulty[1])
result = f(difficulties[0])
for difficulty in difficulties[1:]:
result = self.smbm.wor(result, f(difficulty))
return result
def energyReserveCountOkHellRun(self, hellRunName, mult=1.0):
difficulties = Settings.hellRuns[hellRunName]
result = self.energyReserveCountOkDiff(difficulties, mult)
if result == True:
result.knows = [hellRunName+'HellRun']
return result
# gives damage reduction factor with the current suits
# envDmg : if true (default) will return environmental damage reduction
def getDmgReduction(self, envDmg=True):
ret = 1.0
sm = self.smbm
hasVaria = sm.haveItem('Varia')
hasGrav = sm.haveItem('Gravity')
items = []
if RomPatches.has(sm.player, RomPatches.NoGravityEnvProtection):
if hasVaria:
items = ['Varia']
if envDmg:
ret = 4.0
else:
ret = 2.0
if hasGrav and not envDmg:
ret = 4.0
items = ['Gravity']
elif RomPatches.has(sm.player, RomPatches.ProgressiveSuits):
if hasVaria:
items.append('Varia')
ret *= 2
if hasGrav:
items.append('Gravity')
ret *= 2
else:
if hasVaria:
ret = 2.0
items = ['Varia']
if hasGrav:
ret = 4.0
items = ['Gravity']
return (ret, items)
# higher values for mult means room is that much "easier" (HP mult)
def energyReserveCountOkHardRoom(self, roomName, mult=1.0):
difficulties = Settings.hardRooms[roomName]
(dmgRed, items) = self.getDmgReduction()
mult *= dmgRed
result = self.energyReserveCountOkDiff(difficulties, mult)
if result == True:
result.knows = ['HardRoom-'+roomName]
if dmgRed != 1.0:
result._items.append(items)
return result
@Cache.decorator
def heatProof(self):
sm = self.smbm
return sm.wor(sm.haveItem('Varia'),
sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.NoGravityEnvProtection)),
sm.wnot(RomPatches.has(sm.player, RomPatches.ProgressiveSuits)),
sm.haveItem('Gravity')))
# helper here because we can't define "sublambdas" in locations
def getPiratesPseudoScrewCoeff(self):
sm = self.smbm
ret = 1.0
if RomPatches.has(sm.player, RomPatches.NerfedCharge).bool == True:
ret = 4.0
return ret
@Cache.decorator
def canFireChargedShots(self):
sm = self.smbm
return sm.wor(sm.haveItem('Charge'), RomPatches.has(sm.player, RomPatches.NerfedCharge))
# higher values for mult means hell run is that much "easier" (HP mult)
def canHellRun(self, hellRun, mult=1.0, minE=2):
sm = self.smbm
items = []
isHeatProof = sm.heatProof()
if isHeatProof == True:
return isHeatProof
if sm.wand(RomPatches.has(sm.player, RomPatches.ProgressiveSuits), sm.haveItem('Gravity')).bool == True:
# half heat protection
mult *= 2.0
minE /= 2.0
items.append('Gravity')
if self.energyReserveCount() >= minE:
if hellRun != 'LowerNorfair':
ret = self.energyReserveCountOkHellRun(hellRun, mult)
if ret.bool == True:
ret._items.append(items)
return ret
else:
tanks = self.energyReserveCount()
multCF = mult
if tanks >= 14:
multCF *= 2.0
nCF = int(math.ceil(2/multCF))
ret = sm.wand(self.energyReserveCountOkHellRun(hellRun, mult),
self.canCrystalFlash(nCF))
if ret.bool == True:
if sm.haveItem('Gravity') == True:
ret.difficulty *= 0.7
ret._items.append('Gravity')
elif sm.haveItem('ScrewAttack') == True:
ret.difficulty *= 0.7
ret._items.append('ScrewAttack')
#nPB = self.smbm.itemCount('PowerBomb')
#print("canHellRun LN. tanks=" + str(tanks) + ", nCF=" + str(nCF) + ", nPB=" + str(nPB) + ", mult=" + str(mult) + ", heatProof=" + str(isHeatProof.bool) + ", ret=" + str(ret))
return ret
else:
return smboolFalse
@Cache.decorator
def canMockball(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'),
sm.knowsMockball())
@Cache.decorator
def canFly(self):
sm = self.smbm
return sm.wor(sm.haveItem('SpaceJump'),
sm.canInfiniteBombJump())
@Cache.decorator
def canSimpleShortCharge(self):
sm = self.smbm
return sm.wand(sm.haveItem('SpeedBooster'),
sm.wor(sm.knowsSimpleShortCharge(),
sm.knowsShortCharge()))
@Cache.decorator
def canShortCharge(self):
sm = self.smbm
return sm.wand(sm.haveItem('SpeedBooster'), sm.knowsShortCharge())
@Cache.decorator
def canUseBombs(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'))
@Cache.decorator
def canInfiniteBombJump(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'), sm.knowsInfiniteBombJump())
@Cache.decorator
def canInfiniteBombJumpSuitless(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'), sm.knowsInfiniteBombJumpSuitless())
@Cache.decorator
def haveMissileOrSuper(self):
sm = self.smbm
return sm.wor(sm.haveItem('Missile'), sm.haveItem('Super'))
@Cache.decorator
def canOpenRedDoors(self):
sm = self.smbm
return sm.wor(sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.RedDoorsMissileOnly)),
sm.haveMissileOrSuper()),
sm.haveItem('Missile'))
@Cache.decorator
def canOpenEyeDoors(self):
sm = self.smbm
return sm.wor(RomPatches.has(sm.player, RomPatches.NoGadoras),
sm.haveMissileOrSuper())
@Cache.decorator
def canOpenGreenDoors(self):
return self.smbm.haveItem('Super')
@Cache.decorator
def canGreenGateGlitch(self):
sm = self.smbm
return sm.wand(sm.haveItem('Super'),
sm.knowsGreenGateGlitch())
@Cache.decorator
def canBlueGateGlitch(self):
sm = self.smbm
return sm.wand(sm.haveMissileOrSuper(),
sm.knowsGreenGateGlitch())
@Cache.decorator
def canUsePowerBombs(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'), sm.haveItem('PowerBomb'))
canOpenYellowDoors = canUsePowerBombs
@Cache.decorator
def canUseSpringBall(self):
sm = self.smbm
return sm.wand(sm.haveItem('Morph'),
sm.haveItem('SpringBall'))
@Cache.decorator
def canSpringBallJump(self):
sm = self.smbm
return sm.wand(sm.canUseSpringBall(),
sm.knowsSpringBallJump())
@Cache.decorator
def canDoubleSpringBallJump(self):
sm = self.smbm
return sm.wand(sm.canUseSpringBall(),
sm.haveItem('HiJump'),
sm.knowsDoubleSpringBallJump())
@Cache.decorator
def canSpringBallJumpFromWall(self):
sm = self.smbm
return sm.wand(sm.canUseSpringBall(),
sm.knowsSpringBallJumpFromWall())
@Cache.decorator
def canDestroyBombWalls(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('Morph'),
sm.wor(sm.haveItem('Bomb'),
sm.haveItem('PowerBomb'))),
sm.haveItem('ScrewAttack'))
@Cache.decorator
def canDestroyBombWallsUnderwater(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.canDestroyBombWalls()),
sm.wand(sm.haveItem('Morph'),
sm.wor(sm.haveItem('Bomb'),
sm.haveItem('PowerBomb'))))
@Cache.decorator
def canPassBombPassages(self):
sm = self.smbm
return sm.wor(sm.canUseBombs(),
sm.canUsePowerBombs())
@Cache.decorator
def canMorphJump(self):
# small hop in morph ball form
sm = self.smbm
return sm.wor(sm.canPassBombPassages(), sm.haveItem('SpringBall'))
def canCrystalFlash(self, n=1):
sm = self.smbm
return sm.wand(sm.canUsePowerBombs(),
sm.itemCountOk('Missile', 2*n),
sm.itemCountOk('Super', 2*n),
sm.itemCountOk('PowerBomb', 2*n+1))
@Cache.decorator
def canCrystalFlashClip(self):
sm = self.smbm
return sm.wand(sm.canCrystalFlash(),
sm.wor(sm.wand(sm.haveItem('Gravity'),
sm.canUseBombs(),
sm.knowsCrystalFlashClip()),
sm.wand(sm.knowsSuitlessCrystalFlashClip(),
sm.itemCountOk('PowerBomb', 4))))
@Cache.decorator
def canDoLowGauntlet(self):
sm = self.smbm
return sm.wand(sm.canShortCharge(),
sm.canUsePowerBombs(),
sm.itemCountOk('ETank', 1),
sm.knowsLowGauntlet())
@Cache.decorator
def canUseHyperBeam(self):
sm = self.smbm
return sm.haveItem('Hyper')
@Cache.decorator
def getBeamDamage(self):
sm = self.smbm
standardDamage = 20
if sm.wand(sm.haveItem('Ice'),
sm.haveItem('Wave'),
sm.haveItem('Plasma')) == True:
standardDamage = 300
elif sm.wand(sm.haveItem('Wave'),
sm.haveItem('Plasma')) == True:
standardDamage = 250
elif sm.wand(sm.haveItem('Ice'),
sm.haveItem('Plasma')) == True:
standardDamage = 200
elif sm.haveItem('Plasma') == True:
standardDamage = 150
elif sm.wand(sm.haveItem('Ice'),
sm.haveItem('Wave'),
sm.haveItem('Spazer')) == True:
standardDamage = 100
elif sm.wand(sm.haveItem('Wave'),
sm.haveItem('Spazer')) == True:
standardDamage = 70
elif sm.wand(sm.haveItem('Ice'),
sm.haveItem('Spazer')) == True:
standardDamage = 60
elif sm.wand(sm.haveItem('Ice'),
sm.haveItem('Wave')) == True:
standardDamage = 60
elif sm.haveItem('Wave') == True:
standardDamage = 50
elif sm.haveItem('Spazer') == True:
standardDamage = 40
elif sm.haveItem('Ice') == True:
standardDamage = 30
return standardDamage
# returns a tuple with :
#
# - a floating point number : 0 if boss is unbeatable with
# current equipment, and an ammo "margin" (ex : 1.5 means we have 50%
# more firepower than absolutely necessary). Useful to compute boss
# difficulty when not having charge. If player has charge, the actual
# value is not useful, and is guaranteed to be > 2.
#
# - estimation of the fight duration in seconds (well not really, it
# is if you fire and land shots perfectly and constantly), giving info
# to compute boss fight difficulty
def canInflictEnoughDamages(self, bossEnergy, doubleSuper=False, charge=True, power=False, givesDrops=True, ignoreMissiles=False, ignoreSupers=False):
# TODO: handle special beam attacks ? (http://deanyd.net/sm/index.php?title=Charge_Beam_Combos)
sm = self.smbm
items = []
# http://deanyd.net/sm/index.php?title=Damage
standardDamage = 0
if sm.canFireChargedShots().bool == True and charge == True:
standardDamage = self.getBeamDamage()
items.append('Charge')
# charge triples the damage
chargeDamage = standardDamage
if sm.haveItem('Charge').bool == True:
chargeDamage *= 3.0
# missile 100 damages, super missile 300 damages, PBs 200 dmg, 5 in each extension
missilesAmount = sm.itemCount('Missile') * 5
if ignoreMissiles == True:
missilesDamage = 0
else:
missilesDamage = missilesAmount * 100
if missilesAmount > 0:
items.append('Missile')
supersAmount = sm.itemCount('Super') * 5
if ignoreSupers == True:
oneSuper = 0
else:
oneSuper = 300.0
if supersAmount > 0:
items.append('Super')
if doubleSuper == True:
oneSuper *= 2
supersDamage = supersAmount * oneSuper
powerDamage = 0
powerAmount = 0
if power == True and sm.haveItem('PowerBomb') == True:
powerAmount = sm.itemCount('PowerBomb') * 5
powerDamage = powerAmount * 200
items.append('PowerBomb')
canBeatBoss = chargeDamage > 0 or givesDrops or (missilesDamage + supersDamage + powerDamage) >= bossEnergy
if not canBeatBoss:
return (0, 0, [])
ammoMargin = (missilesDamage + supersDamage + powerDamage) / bossEnergy
if chargeDamage > 0:
ammoMargin += 2
missilesDPS = Settings.algoSettings['missilesPerSecond'] * 100.0
supersDPS = Settings.algoSettings['supersPerSecond'] * 300.0
if doubleSuper == True:
supersDPS *= 2
if powerDamage > 0:
powerDPS = Settings.algoSettings['powerBombsPerSecond'] * 200.0
else:
powerDPS = 0.0
chargeDPS = chargeDamage * Settings.algoSettings['chargedShotsPerSecond']
# print("chargeDPS=" + str(chargeDPS))
dpsDict = {
missilesDPS: (missilesAmount, 100.0),
supersDPS: (supersAmount, oneSuper),
powerDPS: (powerAmount, 200.0),
# no boss will take more 10000 charged shots
chargeDPS: (10000, chargeDamage)
}
secs = 0
for dps in sorted(dpsDict, reverse=True):
amount = dpsDict[dps][0]
one = dpsDict[dps][1]
if dps == 0 or one == 0 or amount == 0:
continue
fire = min(bossEnergy / one, amount)
secs += fire * (one / dps)
bossEnergy -= fire * one
if bossEnergy <= 0:
break
if bossEnergy > 0:
# print ('!! drops !! ')
secs += bossEnergy * Settings.algoSettings['missileDropsPerMinute'] * 100 / 60
# print('ammoMargin = ' + str(ammoMargin) + ', secs = ' + str(secs))
return (ammoMargin, secs, items)
# return diff score, or -1 if below minimum energy in diffTbl
def computeBossDifficulty(self, ammoMargin, secs, diffTbl, energyDiff=0):
sm = self.smbm
# actual fight duration :
rate = None
if 'Rate' in diffTbl:
rate = float(diffTbl['Rate'])
if rate is None:
duration = 120.0
else:
duration = secs / rate
# print('rate=' + str(rate) + ', duration=' + str(duration))
(suitsCoeff, items) = sm.getDmgReduction(envDmg=False)
suitsCoeff /= 2.0
energyCount = self.energyReserveCount()
energy = suitsCoeff * (1 + energyCount + energyDiff)
# print("energy="+str(energy)+", energyCount="+str(energyCount)+",energyDiff="+str(energyDiff)+",suitsCoeff="+str(suitsCoeff))
# add all energy in used items
items += sm.energyReserveCountOk(energyCount).items
energyDict = None
if 'Energy' in diffTbl:
energyDict = diffTbl['Energy']
difficulty = medium
# get difficulty by energy
if energyDict:
energyDict = {float(k):float(v) for k,v in energyDict.items()}
keyz = sorted(energyDict.keys())
if len(keyz) > 0:
current = keyz[0]
if energy < current:
return (-1, [])
sup = None
difficulty = energyDict[current]
for k in keyz:
if k > energy:
sup=k
break
current = k
difficulty = energyDict[k]
# interpolate if we can
if energy > current and sup is not None:
difficulty += (energyDict[sup] - difficulty)/(sup - current) * (energy - current)
# print("energy=" + str(energy) + ", base diff=" + str(difficulty))
# adjust by fight duration
difficulty *= (duration / 120)
# and by ammo margin
# only augment difficulty in case of no charge, don't lower it.
# if we have charge, ammoMargin will have a huge value (see canInflictEnoughDamages),
# so this does not apply
diffAdjust = (1 - (ammoMargin - Settings.algoSettings['ammoMarginIfNoCharge']))
if diffAdjust > 1:
difficulty *= diffAdjust
# print("final diff: "+str(round(difficulty, 2)))
return (round(difficulty, 2), items)
@Cache.decorator
def enoughStuffSporeSpawn(self):
sm = self.smbm
return sm.wor(sm.haveItem('Missile'), sm.haveItem('Super'), sm.haveItem('Charge'))
@Cache.decorator
def enoughStuffCroc(self):
sm = self.smbm
# say croc has ~5000 energy, and ignore its useless drops
(ammoMargin, secs, items) = self.canInflictEnoughDamages(5000, givesDrops=False)
if ammoMargin == 0:
return sm.wand(sm.knowsLowAmmoCroc(),
sm.wor(sm.itemCountOk("Missile", 2),
sm.wand(sm.haveItem('Missile'),
sm.haveItem('Super'))))
else:
return SMBool(True, easy, items=items)
@Cache.decorator
def enoughStuffBotwoon(self):
sm = self.smbm
(ammoMargin, secs, items) = self.canInflictEnoughDamages(6000, givesDrops=False)
diff = SMBool(True, easy, [], items)
lowStuff = sm.knowsLowStuffBotwoon()
if ammoMargin == 0 and lowStuff.bool:
(ammoMargin, secs, items) = self.canInflictEnoughDamages(3500, givesDrops=False)
diff = SMBool(lowStuff.bool, lowStuff.difficulty, lowStuff.knows, items)
if ammoMargin == 0:
return smboolFalse
fight = sm.wor(sm.energyReserveCountOk(math.ceil(4/sm.getDmgReduction(envDmg=False)[0])),
lowStuff)
return sm.wandmax(fight, diff)
@Cache.decorator
def enoughStuffGT(self):
sm = self.smbm
hasBeams = sm.wand(sm.haveItem('Charge'), sm.haveItem('Plasma')).bool
(ammoMargin, secs, items) = self.canInflictEnoughDamages(9000, ignoreMissiles=True, givesDrops=hasBeams)
diff = SMBool(True, easy, [], items)
lowStuff = sm.knowsLowStuffGT()
if ammoMargin == 0 and lowStuff.bool:
(ammoMargin, secs, items) = self.canInflictEnoughDamages(3000, ignoreMissiles=True)
diff = SMBool(lowStuff.bool, lowStuff.difficulty, lowStuff.knows, items)
if ammoMargin == 0:
return smboolFalse
fight = sm.wor(sm.energyReserveCountOk(math.ceil(8/sm.getDmgReduction(envDmg=False)[0])),
lowStuff)
return sm.wandmax(fight, diff)
@Cache.decorator
def enoughStuffsRidley(self):
sm = self.smbm
if not sm.haveItem('Morph') and not sm.haveItem('ScrewAttack'):
return smboolFalse
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(18000, doubleSuper=True, power=True, givesDrops=False)
if ammoMargin == 0:
return smboolFalse
# print('RIDLEY', ammoMargin, secs)
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
Settings.bossesDifficulty['Ridley'])
if diff < 0:
return smboolFalse
else:
return SMBool(True, diff, items=ammoItems+defenseItems)
@Cache.decorator
def enoughStuffsKraid(self):
sm = self.smbm
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(1000)
if ammoMargin == 0:
return smboolFalse
#print('KRAID True ', ammoMargin, secs)
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
Settings.bossesDifficulty['Kraid'])
if diff < 0:
return smboolFalse
return SMBool(True, diff, items=ammoItems+defenseItems)
def adjustHealthDropDiff(self, difficulty):
(dmgRed, items) = self.getDmgReduction(envDmg=False)
# 2 is Varia suit, considered standard eqt for boss fights
# there's certainly a smarter way to do this but...
if dmgRed < 2:
difficulty *= Settings.algoSettings['dmgReductionDifficultyFactor']
elif dmgRed > 2:
difficulty /= Settings.algoSettings['dmgReductionDifficultyFactor']
return difficulty
@Cache.decorator
def enoughStuffsDraygon(self):
sm = self.smbm
if not sm.haveItem('Morph') and not sm.haveItem('Gravity'):
return smboolFalse
# some ammo to destroy the turrets during the fight
if not sm.haveMissileOrSuper():
return smboolFalse
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(6000)
# print('DRAY', ammoMargin, secs)
if ammoMargin > 0:
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
Settings.bossesDifficulty['Draygon'])
if diff < 0:
fight = smboolFalse
else:
fight = SMBool(True, diff, items=ammoItems+defenseItems)
if sm.haveItem('Gravity') == False:
fight.difficulty *= Settings.algoSettings['draygonNoGravityMalus']
else:
fight._items.append('Gravity')
if not sm.haveItem('Morph'):
fight.difficulty *= Settings.algoSettings['draygonNoMorphMalus']
if sm.haveItem('Gravity') and sm.haveItem('ScrewAttack'):
fight.difficulty /= Settings.algoSettings['draygonScrewBonus']
fight.difficulty = self.adjustHealthDropDiff(fight.difficulty)
else:
fight = smboolFalse
# for grapple kill considers energy drained by wall socket + 2 spankings by Dray
# (original 99 energy used for rounding)
nTanksGrapple = (240/sm.getDmgReduction(envDmg=True)[0] + 2*160/sm.getDmgReduction(envDmg=False)[0])/100
return sm.wor(fight,
sm.wand(sm.knowsDraygonGrappleKill(),
sm.haveItem('Grapple'),
sm.energyReserveCountOk(nTanksGrapple)),
sm.wand(sm.knowsMicrowaveDraygon(),
sm.haveItem('Plasma'),
sm.canFireChargedShots(),
sm.haveItem('XRayScope')),
sm.wand(sm.haveItem('Gravity'),
sm.energyReserveCountOk(3),
sm.knowsDraygonSparkKill(),
sm.haveItem('SpeedBooster')))
@Cache.decorator
def enoughStuffsPhantoon(self):
sm = self.smbm
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(2500, doubleSuper=True)
if ammoMargin == 0:
return smboolFalse
# print('PHANTOON', ammoMargin, secs)
(difficulty, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
Settings.bossesDifficulty['Phantoon'])
if difficulty < 0:
return smboolFalse
hasCharge = sm.canFireChargedShots()
hasScrew = sm.haveItem('ScrewAttack')
if hasScrew:
difficulty /= Settings.algoSettings['phantoonFlamesAvoidBonusScrew']
defenseItems += hasScrew.items
elif hasCharge:
difficulty /= Settings.algoSettings['phantoonFlamesAvoidBonusCharge']
defenseItems += hasCharge.items
elif not hasCharge and sm.itemCount('Missile') <= 2: # few missiles is harder
difficulty *= Settings.algoSettings['phantoonLowMissileMalus']
difficulty = self.adjustHealthDropDiff(difficulty)
fight = SMBool(True, difficulty, items=ammoItems+defenseItems)
return sm.wor(fight,
sm.wand(sm.knowsMicrowavePhantoon(),
sm.haveItem('Plasma'),
sm.canFireChargedShots(),
sm.haveItem('XRayScope')))
def mbEtankCheck(self):
sm = self.smbm
if sm.wor(RomPatches.has(sm.player, RomPatches.NerfedRainbowBeam), RomPatches.has(sm.player, RomPatches.TourianSpeedup)):
# "add" energy for difficulty calculations
energy = 2.8 if sm.haveItem('Varia') else 2.6
return (True, energy)
nTanks = sm.energyReserveCount()
energyDiff = 0
if sm.haveItem('Varia') == False:
# "remove" 3 etanks (accounting for rainbow beam damage without varia)
if nTanks < 6:
return (False, 0)
energyDiff = -3
elif nTanks < 3:
return (False, 0)
return (True, energyDiff)
@Cache.decorator
def enoughStuffsMotherbrain(self):
sm = self.smbm
# MB1 can't be hit by charge beam
(ammoMargin, secs, _) = self.canInflictEnoughDamages(3000, charge=False, givesDrops=False)
if ammoMargin == 0:
return smboolFalse
# requires 10-10 to break the glass
if sm.itemCount('Missile') <= 1 or sm.itemCount('Super') <= 1:
return smboolFalse
# we actually don't give a shit about MB1 difficulty,
# since we embark its health in the following calc
(ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(18000 + 3000, givesDrops=False)
if ammoMargin == 0:
return smboolFalse
(possible, energyDiff) = self.mbEtankCheck()
if possible == False:
return smboolFalse
# print('MB2', ammoMargin, secs)
#print("ammoMargin: {}, secs: {}, settings: {}, energyDiff: {}".format(ammoMargin, secs, Settings.bossesDifficulty['MotherBrain'], energyDiff))
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs, Settings.bossesDifficulty['MotherBrain'], energyDiff)
if diff < 0:
return smboolFalse
return SMBool(True, diff, items=ammoItems+defenseItems)
@Cache.decorator
def canPassMetroids(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('Ice'), sm.haveMissileOrSuper()),
# to avoid leaving tourian to refill power bombs
sm.itemCountOk('PowerBomb', 3))
@Cache.decorator
def canPassZebetites(self):
sm = self.smbm
return sm.wor(sm.wand(sm.haveItem('Ice'), sm.knowsIceZebSkip()),
sm.wand(sm.haveItem('SpeedBooster'), sm.knowsSpeedZebSkip()),
# account for one zebetite, refill may be necessary
SMBool(self.canInflictEnoughDamages(1100, charge=False, givesDrops=False, ignoreSupers=True)[0] >= 1, 0))
@Cache.decorator
def enoughStuffTourian(self):
sm = self.smbm
ret = self.smbm.wand(sm.wor(RomPatches.has(sm.player, RomPatches.TourianSpeedup),
sm.wand(sm.canPassMetroids(), sm.canPassZebetites())),
sm.canOpenRedDoors(),
sm.enoughStuffsMotherbrain(),
sm.wor(RomPatches.has(sm.player, RomPatches.OpenZebetites), sm.haveItem('Morph')))
return ret
class Pickup:
def __init__(self, itemsPickup):
self.itemsPickup = itemsPickup
def enoughMinors(self, smbm, minorLocations):
if self.itemsPickup == 'all':
return len(minorLocations) == 0
else:
return True
def enoughMajors(self, smbm, majorLocations):
if self.itemsPickup == 'all':
return len(majorLocations) == 0
else:
return True
class Bosses:
# bosses helpers to know if they are dead
areaBosses = {
# classic areas
'Brinstar': 'Kraid',
'Norfair': 'Ridley',
'LowerNorfair': 'Ridley',
'WreckedShip': 'Phantoon',
'Maridia': 'Draygon',
# solver areas
'Blue Brinstar': 'Kraid',
'Brinstar Hills': 'Kraid',
'Bubble Norfair': 'Ridley',
'Bubble Norfair Bottom': 'Ridley',
'Bubble Norfair Reserve': 'Ridley',
'Bubble Norfair Speed': 'Ridley',
'Bubble Norfair Wave': 'Ridley',
'Draygon Boss': 'Draygon',
'Green Brinstar': 'Kraid',
'Green Brinstar Reserve': 'Kraid',
'Kraid': 'Kraid',
'Kraid Boss': 'Kraid',
'Left Sandpit': 'Draygon',
'Lower Norfair After Amphitheater': 'Ridley',
'Lower Norfair Before Amphitheater': 'Ridley',
'Lower Norfair Screw Attack': 'Ridley',
'Maridia Forgotten Highway': 'Draygon',
'Maridia Green': 'Draygon',
'Maridia Pink Bottom': 'Draygon',
'Maridia Pink Top': 'Draygon',
'Maridia Sandpits': 'Draygon',
'Norfair Entrance': 'Ridley',
'Norfair Grapple Escape': 'Ridley',
'Norfair Ice': 'Ridley',
'Phantoon Boss': 'Phantoon',
'Pink Brinstar': 'Kraid',
'Red Brinstar': 'Kraid',
'Red Brinstar Top': 'Kraid',
'Ridley Boss': 'Ridley',
'Right Sandpit': 'Draygon',
'Warehouse': 'Kraid',
'WreckedShip': 'Phantoon',
'WreckedShip Back': 'Phantoon',
'WreckedShip Bottom': 'Phantoon',
'WreckedShip Gravity': 'Phantoon',
'WreckedShip Main': 'Phantoon',
'WreckedShip Top': 'Phantoon'
}
@staticmethod
def Golden4():
return ['Draygon', 'Kraid', 'Phantoon', 'Ridley']
@staticmethod
def bossDead(sm, boss):
return sm.haveItem(boss)
@staticmethod
def areaBossDead(sm, area):
if area not in Bosses.areaBosses:
return True
return Bosses.bossDead(sm, Bosses.areaBosses[area])
@staticmethod
def allBossesDead(smbm):
return smbm.wand(Bosses.bossDead(smbm, 'Kraid'),
Bosses.bossDead(smbm, 'Phantoon'),
Bosses.bossDead(smbm, 'Draygon'),
Bosses.bossDead(smbm, 'Ridley'))
def diffValue2txt(diff):
last = 0
for d in sorted(diff2text.keys()):
if diff >= last and diff < d:
return diff2text[last]
last = d
return None

View File

@ -0,0 +1,26 @@
# entry point for the logic implementation
class Logic(object):
@staticmethod
def factory(implementation):
if implementation == 'vanilla':
from graph.vanilla.graph_helpers import HelpersGraph
from graph.vanilla.graph_access import accessPoints
from graph.vanilla.graph_locations import locations
from graph.vanilla.graph_locations import LocationsHelper
Logic.locations = locations
Logic.accessPoints = accessPoints
Logic.HelpersGraph = HelpersGraph
Logic.patches = implementation
Logic.LocationsHelper = LocationsHelper
elif implementation == 'rotation':
from graph.rotation.graph_helpers import HelpersGraph
from graph.rotation.graph_access import accessPoints
from graph.rotation.graph_locations import locations
from graph.rotation.graph_locations import LocationsHelper
Logic.locations = locations
Logic.accessPoints = accessPoints
Logic.HelpersGraph = HelpersGraph
Logic.patches = implementation
Logic.LocationsHelper = LocationsHelper
Logic.implementation = implementation

View File

@ -0,0 +1,122 @@
def flatten(l):
if type(l) is list:
return [ y for x in l for y in flatten(x) ]
else:
return [ l ]
# super metroid boolean
class SMBool:
__slots__ = ('bool', 'difficulty', '_knows', '_items')
def __init__(self, boolean, difficulty=0, knows=[], items=[]):
self.bool = boolean
self.difficulty = difficulty
self._knows = knows
self._items = items
@property
def knows(self):
self._knows = list(set(flatten(self._knows)))
return self._knows
@knows.setter
def knows(self, knows):
self._knows = knows
@property
def items(self):
self._items = list(set(flatten(self._items)))
return self._items
@items.setter
def items(self, items):
self._items = items
def __repr__(self):
# to display the smbool as a string
return 'SMBool({}, {}, {}, {})'.format(self.bool, self.difficulty, sorted(self.knows), sorted(self.items))
def __getitem__(self, index):
# to acces the smbool as [0] for the bool and [1] for the difficulty.
# required when we load a json preset where the smbool is stored as a list,
# and we add missing smbools to it, so we have a mix of lists and smbools.
if index == 0:
return self.bool
elif index == 1:
return self.difficulty
def __bool__(self):
# when used in boolean expressions (with and/or/not) (python3)
return self.bool
def __eq__(self, other):
# for ==
return self.bool == other
def __ne__(self, other):
# for !=
return self.bool != other
def __lt__(self, other):
# for <
if self.bool and other.bool:
return self.difficulty < other.difficulty
else:
return self.bool
def __copy__(self):
return SMBool(self.bool, self.difficulty, self._knows, self._items)
def json(self):
# as we have slots instead of dict
return {'bool': self.bool, 'difficulty': self.difficulty, 'knows': self.knows, 'items': self.items}
def wand(*args):
# looping here is faster than using "if ... in" construct
for smb in args:
if not smb.bool:
return smboolFalse
difficulty = 0
for smb in args:
difficulty += smb.difficulty
return SMBool(True,
difficulty,
[ smb._knows for smb in args ],
[ smb._items for smb in args ])
def wandmax(*args):
# looping here is faster than using "if ... in" construct
for smb in args:
if not smb.bool:
return smboolFalse
difficulty = 0
for smb in args:
if smb.difficulty > difficulty:
difficulty = smb.difficulty
return SMBool(True,
difficulty,
[ smb._knows for smb in args ],
[ smb._items for smb in args ])
def wor(*args):
# looping here is faster than using "if ... in" construct
for smb in args:
if smb.bool:
return min(args)
return smboolFalse
# negates boolean part of the SMBool
def wnot(a):
return smboolFalse if a.bool else SMBool(True, a.difficulty)
__and__ = wand
__or__ = wor
__not__ = wnot
smboolFalse = SMBool(False)

View File

@ -0,0 +1,241 @@
# object to handle the smbools and optimize them
from logic.cache import Cache
from logic.smbool import SMBool, smboolFalse
from logic.helpers import Bosses
from logic.logic import Logic
from utils.doorsmanager import DoorsManager
from utils.parameters import Knows, isKnows
import logging
import sys
class SMBoolManager(object):
items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper'] + Bosses.Golden4()
countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve']
def __init__(self, player=0, maxDiff=sys.maxsize):
self._items = { }
self._counts = { }
self.player = player
self.maxDiff = maxDiff
# cache related
self.cacheKey = 0
self.computeItemsPositions()
Cache.reset()
Logic.factory('vanilla')
self.helpers = Logic.HelpersGraph(self)
self.doorsManager = DoorsManager()
self.createFacadeFunctions()
self.createKnowsFunctions(player)
self.resetItems()
def computeItemsPositions(self):
# compute index in cache key for each items
self.itemsPositions = {}
maxBitsForCountItem = 7 # 128 values with 7 bits
for (i, item) in enumerate(self.countItems):
pos = i*maxBitsForCountItem
bitMask = (2<<(maxBitsForCountItem-1))-1
bitMask = bitMask << pos
self.itemsPositions[item] = (pos, bitMask)
for (i, item) in enumerate(self.items, (i+1)*maxBitsForCountItem+1):
if item in self.countItems:
continue
self.itemsPositions[item] = (i, 1<<i)
def computeNewCacheKey(self, item, value):
# generate an unique integer for each items combinations which is use as key in the cache.
if item in ['Nothing', 'NoEnergy']:
return
(pos, bitMask) = self.itemsPositions[item]
# print("--------------------- {} {} ----------------------------".format(item, value))
# print("old: "+format(self.cacheKey, '#067b'))
self.cacheKey = (self.cacheKey & (~bitMask)) | (value<<pos)
# print("new: "+format(self.cacheKey, '#067b'))
# self.printItemsInKey(self.cacheKey)
def printItemsInKey(self, key):
# for debug purpose
print("key: "+format(key, '#067b'))
msg = ""
for (item, (pos, bitMask)) in self.itemsPositions.items():
value = (key & bitMask) >> pos
if value != 0:
msg += " {}: {}".format(item, value)
print("items:{}".format(msg))
def isEmpty(self):
for item in self.items:
if self.haveItem(item):
return False
for item in self.countItems:
if self.itemCount(item) > 0:
return False
return True
def getItems(self):
# get a dict of collected items and how many (to be displayed on the solver spoiler)
itemsDict = {}
for item in self.items:
itemsDict[item] = 1 if self._items[item] == True else 0
for item in self.countItems:
itemsDict[item] = self._counts[item]
return itemsDict
def withItem(self, item, func):
self.addItem(item)
ret = func(self)
self.removeItem(item)
return ret
def resetItems(self):
self._items = { item : smboolFalse for item in self.items }
self._counts = { item : 0 for item in self.countItems }
self.cacheKey = 0
Cache.update(self.cacheKey)
def addItem(self, item):
# a new item is available
self._items[item] = SMBool(True, items=[item])
if self.isCountItem(item):
count = self._counts[item] + 1
self._counts[item] = count
self.computeNewCacheKey(item, count)
else:
self.computeNewCacheKey(item, 1)
Cache.update(self.cacheKey)
def addItems(self, items):
if len(items) == 0:
return
for item in items:
self._items[item] = SMBool(True, items=[item])
if self.isCountItem(item):
count = self._counts[item] + 1
self._counts[item] = count
self.computeNewCacheKey(item, count)
else:
self.computeNewCacheKey(item, 1)
Cache.update(self.cacheKey)
def removeItem(self, item):
# randomizer removed an item (or the item was added to test a post available)
if self.isCountItem(item):
count = self._counts[item] - 1
self._counts[item] = count
if count == 0:
self._items[item] = smboolFalse
self.computeNewCacheKey(item, count)
else:
self._items[item] = smboolFalse
self.computeNewCacheKey(item, 0)
Cache.update(self.cacheKey)
def createFacadeFunctions(self):
for fun in dir(self.helpers):
if fun != 'smbm' and fun[0:2] != '__':
setattr(self, fun, getattr(self.helpers, fun))
def traverse(self, doorName):
return self.doorsManager.traverse(self, doorName)
def createKnowsFunctions(self, player):
# for each knows we have a function knowsKnows (ex: knowsAlcatrazEscape()) which
# take no parameter
for knows in Knows.__dict__:
if isKnows(knows):
if knows in Knows.knowsDict[player].__dict__:
setattr(self, 'knows'+knows, lambda knows=knows: SMBool(Knows.knowsDict[player].__dict__[knows].bool,
Knows.knowsDict[player].__dict__[knows].difficulty,
knows=[knows]))
else:
# if knows not in preset, use default values
setattr(self, 'knows'+knows, lambda knows=knows: SMBool(Knows.__dict__[knows].bool,
Knows.__dict__[knows].difficulty,
knows=[knows]))
def isCountItem(self, item):
return item in self.countItems
def itemCount(self, item):
# return integer
#self.state.item_count(item, self.player)
return self._counts[item]
def haveItem(self, item):
#return self.state.has(item, self.player)
return self._items[item]
wand = staticmethod(SMBool.wand)
wandmax = staticmethod(SMBool.wandmax)
wor = staticmethod(SMBool.wor)
wnot = staticmethod(SMBool.wnot)
def itemCountOk(self, item, count, difficulty=0):
if self.itemCount(item) >= count:
if item in ['ETank', 'Reserve']:
item = str(count)+'-'+item
return SMBool(True, difficulty, items = [item])
else:
return smboolFalse
def energyReserveCountOk(self, count, difficulty=0):
if self.energyReserveCount() >= count:
nEtank = self.itemCount('ETank')
if nEtank > count:
nEtank = int(count)
items = str(nEtank)+'-ETank'
nReserve = self.itemCount('Reserve')
if nEtank < count:
nReserve = int(count) - nEtank
items += ' - '+str(nReserve)+'-Reserve'
return SMBool(True, difficulty, items = [items])
else:
return smboolFalse
class SMBoolManagerPlando(SMBoolManager):
def __init__(self):
super(SMBoolManagerPlando, self).__init__()
def addItem(self, item):
# a new item is available
already = self.haveItem(item)
isCount = self.isCountItem(item)
if isCount or not already:
self._items[item] = SMBool(True, items=[item])
else:
# handle duplicate major items (plandos)
self._items['dup_'+item] = True
if isCount:
count = self._counts[item] + 1
self._counts[item] = count
self.computeNewCacheKey(item, count)
else:
self.computeNewCacheKey(item, 1)
Cache.update(self.cacheKey)
def removeItem(self, item):
# randomizer removed an item (or the item was added to test a post available)
if self.isCountItem(item):
count = self._counts[item] - 1
self._counts[item] = count
if count == 0:
self._items[item] = smboolFalse
self.computeNewCacheKey(item, count)
else:
dup = 'dup_'+item
if self._items.get(dup, None) is None:
self._items[item] = smboolFalse
self.computeNewCacheKey(item, 0)
else:
del self._items[dup]
self.computeNewCacheKey(item, 1)
Cache.update(self.cacheKey)

Some files were not shown because too many files have changed in this diff Show More