Merge branch 'main' into docs_consolidation

# Conflicts:
#	WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
#	WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md
#	WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md
This commit is contained in:
Hussein Farran 2021-12-19 22:53:06 -05:00
commit 75d94b04aa
69 changed files with 1457 additions and 3297 deletions

4
.gitignore vendored
View File

@ -152,3 +152,7 @@ dmypy.json
cython_debug/
Archipelago.zip
#minecraft server stuff
jdk*/
minecraft*/

View File

@ -586,7 +586,7 @@ class CollectionState(object):
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for
shop in self.world.shops)
def item_count(self, item, player: int) -> int:
def item_count(self, item: str, player: int) -> int:
return self.prog_items[item, player]
def has_triforce_pieces(self, count: int, player: int) -> bool:
@ -713,23 +713,23 @@ class CollectionState(object):
def has_turtle_rock_medallion(self, player: int) -> bool:
return self.has(self.world.required_medallions[player][1], player)
def can_boots_clip_lw(self, player):
def can_boots_clip_lw(self, player: int):
if self.world.mode[player] == 'inverted':
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
return self.has('Pegasus Boots', player)
def can_boots_clip_dw(self, player):
def can_boots_clip_dw(self, player: int):
if self.world.mode[player] != 'inverted':
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
return self.has('Pegasus Boots', player)
def can_get_glitched_speed_lw(self, player):
def can_get_glitched_speed_lw(self, player: int):
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
if self.world.mode[player] == 'inverted':
rules.append(self.has('Moon Pearl', player))
return all(rules)
def can_superbunny_mirror_with_sword(self, player):
def can_superbunny_mirror_with_sword(self, player: int):
return self.has('Magic Mirror', player) and self.has_sword(player)
def can_get_glitched_speed_dw(self, player: int):
@ -758,7 +758,7 @@ class CollectionState(object):
return changed
def remove(self, item):
def remove(self, item: Item):
changed = self.world.worlds[item.player].remove(self, item)
if changed:
# invalidate caches, nothing can be trusted anymore now
@ -776,14 +776,14 @@ class RegionType(int, Enum):
Dungeon = 4
@property
def is_indoors(self):
def is_indoors(self) -> bool:
"""Shorthand for checking if Cave or Dungeon"""
return self in (RegionType.Cave, RegionType.Dungeon)
class Region(object):
def __init__(self, name: str, type, hint, player: int, world: Optional[MultiWorld] = None):
def __init__(self, name: str, type: str, hint, player: int, world: Optional[MultiWorld] = None):
self.name = name
self.type = type
self.entrances = []
@ -798,12 +798,12 @@ class Region(object):
self.hint_text = hint
self.player = player
def can_reach(self, state: CollectionState):
def can_reach(self, state: CollectionState) -> bool:
if state.stale[self.player]:
state.update_reachable_regions(self.player)
return self in state.reachable_regions[self.player]
def can_reach_private(self, state: CollectionState):
def can_reach_private(self, state: CollectionState) -> bool:
for entrance in self.entrances:
if entrance.can_reach(state):
if not self in state.path:
@ -831,7 +831,7 @@ class Entrance(object):
self.player = player
self.hide_path = False
def can_reach(self, state):
def can_reach(self, state: CollectionState) -> bool:
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
@ -839,7 +839,7 @@ class Entrance(object):
return False
def connect(self, region, addresses=None, target=None):
def connect(self, region: Region, addresses=None, target = None):
self.connected_region = region
self.target = target
self.addresses = addresses
@ -865,11 +865,11 @@ class Dungeon(object):
self.world = None
@property
def boss(self):
def boss(self) -> Optional[Boss]:
return self.bosses.get(None, None)
@boss.setter
def boss(self, value):
def boss(self, value: Optional[Boss]):
self.bosses[None] = value
@property
@ -896,7 +896,7 @@ class Dungeon(object):
class Boss():
def __init__(self, name, enemizer_name, defeat_rule, player: int):
def __init__(self, name: str, enemizer_name: str, defeat_rule, player: int):
self.name = name
self.enemizer_name = enemizer_name
self.defeat_rule = defeat_rule

View File

@ -82,16 +82,19 @@ class ClientCommandProcessor(CommandProcessor):
return True
def _cmd_items(self):
"""List all item names for the currently running game."""
self.output(f"Item Names for {self.ctx.game}")
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name)
def _cmd_locations(self):
"""List all location names for the currently running game."""
self.output(f"Location Names for {self.ctx.game}")
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name)
def _cmd_ready(self):
"""Send ready status to server."""
self.ctx.ready = not self.ctx.ready
if self.ctx.ready:
state = ClientStatus.CLIENT_READY

View File

@ -11,9 +11,9 @@ from CommonClient import CommonContext, server_loop, gui_enabled, console_loop,
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator then restart ff1_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator make sure ff1_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator then restart ff1_connector.lua"
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure ff1_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
@ -219,8 +219,8 @@ if __name__ == '__main__':
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
input_task = None
from kvui import TextManager
ctx.ui = TextManager(ctx)
from kvui import FF1Manager
ctx.ui = FF1Manager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
input_task = asyncio.create_task(console_loop(ctx), name="Input")

View File

@ -20,7 +20,7 @@ from urllib.parse import urlparse
from urllib.request import urlopen
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from Utils import output_path, local_path, open_file
from Utils import output_path, local_path, open_file, get_cert_none_ssl_context, persistent_store
class AdjusterWorld(object):
@ -102,14 +102,15 @@ def main():
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args()
args.music = not args.disablemusic
if args.update_sprites:
run_sprite_update()
sys.exit()
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
args.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel)
if args.update_sprites:
run_sprite_update()
sys.exit()
if not os.path.isfile(args.rom):
adjustGUI()
else:
@ -118,7 +119,6 @@ def main():
sys.exit(1)
args, path = adjust(args=args)
from Utils import persistent_store
if isinstance(args.sprite, Sprite):
args.sprite = args.sprite.name
persistent_store("adjuster", "last_settings_3", args)
@ -224,7 +224,6 @@ def adjustGUI():
messagebox.showerror(title="Error while adjusting Rom", message=str(e))
else:
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
from Utils import persistent_store
if isinstance(guiargs.sprite, Sprite):
guiargs.sprite = guiargs.sprite.name
persistent_store("adjuster", "last_settings_3", guiargs)
@ -241,12 +240,16 @@ def adjustGUI():
def run_sprite_update():
import threading
done = threading.Event()
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
top.update()
print("Done updating sprites")
task.do_events()
logging.info("Done updating sprites")
def update_sprites(task, on_finish=None):
@ -254,7 +257,7 @@ def update_sprites(task, on_finish=None):
successful = True
sprite_dir = local_path("data", "sprites", "alttpr")
os.makedirs(sprite_dir, exist_ok=True)
ctx = get_cert_none_ssl_context()
def finished():
task.close_window()
if on_finish:
@ -262,7 +265,7 @@ def update_sprites(task, on_finish=None):
try:
task.update_status("Downloading alttpr sprites list")
with urlopen('https://alttpr.com/sprites') as response:
with urlopen('https://alttpr.com/sprites', context=ctx) as response:
sprites_arr = json.loads(response.read().decode("utf-8"))
except Exception as e:
resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
@ -289,7 +292,7 @@ def update_sprites(task, on_finish=None):
def dl(sprite_url, filename):
target = os.path.join(sprite_dir, filename)
with urlopen(sprite_url) as response, open(target, 'wb') as out:
with urlopen(sprite_url, context=ctx) as response, open(target, 'wb') as out:
shutil.copyfileobj(response, out)
def rem(sprite):
@ -400,12 +403,39 @@ class BackgroundTaskProgress(BackgroundTask):
def update_status(self, text):
self.queue_event(lambda: self.label_var.set(text))
def do_events(self):
self.parent.update()
# only call this in an event callback
def close_window(self):
self.stop()
self.window.destroy()
class BackgroundTaskProgressNullWindow(BackgroundTask):
def __init__(self, code_to_run, *args):
super().__init__(None, code_to_run, *args)
def process_queue(self):
try:
while True:
if not self.running:
return
event = self.queue.get_nowait()
event()
except queue.Empty:
pass
def do_events(self):
self.process_queue()
def update_status(self, text):
self.queue_event(lambda: logging.info(text))
def close_window(self):
self.stop()
def get_rom_frame(parent=None):
romFrame = Frame(parent)
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')

View File

@ -321,7 +321,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occured.
if multidata_task:
multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
if i % 10 == 0 or i == len(output_file_futures):

View File

@ -15,6 +15,7 @@ atexit.register(input, "Press enter to exit.")
# 1 or more digits followed by m or g, then optional b
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
forge_version = "1.17.1-37.1.1"
def prompt_yes_no(prompt):
@ -30,25 +31,14 @@ def prompt_yes_no(prompt):
print('Please respond with "y" or "n".')
# Find Forge jar file; raise error if not found
def find_forge_jar(forge_dir):
for entry in os.scandir(forge_dir):
if ".jar" in entry.name and "forge" in entry.name:
logging.info(f"Found forge .jar: {entry.name}")
return entry.name
raise FileNotFoundError(f"Could not find forge .jar in {forge_dir}.")
# Create mods folder if needed; find AP randomizer jar; return None if not found.
def find_ap_randomizer_jar(forge_dir):
mods_dir = os.path.join(forge_dir, 'mods')
if os.path.isdir(mods_dir):
ap_mod_re = re.compile(r"^aprandomizer-[\d\.]+\.jar$")
for entry in os.scandir(mods_dir):
match = ap_mod_re.match(entry.name)
if match:
logging.info(f"Found AP randomizer mod: {match.group()}")
return match.group()
if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"):
logging.info(f"Found AP randomizer mod: {entry.name}")
return entry.name
return None
else:
os.mkdir(mods_dir)
@ -77,14 +67,30 @@ def replace_apmc_files(forge_dir, apmc_file):
logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
def read_apmc_file(apmc_file):
from base64 import b64decode
import json
with open(apmc_file, 'r') as f:
data = json.loads(b64decode(f.read()))
return data
# Check mod version, download new mod from GitHub releases page if needed.
def update_mod(forge_dir):
def update_mod(forge_dir, apmc_file, get_prereleases=False):
ap_randomizer = find_ap_randomizer_jar(forge_dir)
if apmc_file is not None:
data = read_apmc_file(apmc_file)
minecraft_version = data.get('minecraft_version', '')
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
resp = requests.get(client_releases_endpoint)
if resp.status_code == 200: # OK
latest_release = resp.json()[0]
try:
latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and
(apmc_file is None or minecraft_version in release['assets'][0]['name']),
resp.json()))
if ap_randomizer != 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']}")
@ -108,6 +114,10 @@ def update_mod(forge_dir):
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)
except StopIteration:
logging.warning(f"No compatible mod version found for {minecraft_version}.")
if not prompt_yes_no("Run server anyway?"):
sys.exit(0)
else:
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.")
@ -139,11 +149,69 @@ def check_eula(forge_dir):
sys.exit(0)
# Run the Forge server. Return process object
def run_forge_server(forge_dir, heap_arg):
forge_server = find_forge_jar(forge_dir)
# get the current JDK16
def find_jdk_dir() -> str:
for entry in os.listdir():
if os.path.isdir(entry) and entry.startswith("jdk16"):
return os.path.abspath(entry)
java_exe = os.path.abspath(os.path.join('jre8', 'bin', 'java.exe'))
# get the java exe location
def find_jdk() -> str:
jdk = find_jdk_dir()
jdk_exe = os.path.join(jdk, "bin", "java.exe")
if os.path.isfile(jdk_exe):
return jdk_exe
# Download Corretto 16 (Amazon JDK)
def download_java():
jdk = find_jdk_dir()
if jdk is not None:
print(f"Removing old JDK...")
from shutil import rmtree
rmtree(jdk)
print(f"Downloading Java...")
jdk_url = "https://corretto.aws/downloads/latest/amazon-corretto-16-x64-windows-jdk.zip"
resp = requests.get(jdk_url)
if resp.status_code == 200: # OK
print(f"Extracting...")
import zipfile
from io import BytesIO
with zipfile.ZipFile(BytesIO(resp.content)) as zf:
zf.extractall()
else:
print(f"Error downloading Java (status code {resp.status_code}).")
print(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)
# download and install forge
def install_forge(directory: str):
jdk = find_jdk()
if jdk is not None:
print(f"Downloading Forge {forge_version}...")
forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar"
resp = requests.get(forge_url)
if resp.status_code == 200: # OK
forge_install_jar = os.path.join(directory, "forge_install.jar")
if not os.path.exists(directory):
os.mkdir(directory)
with open(forge_install_jar, 'wb') as f:
f.write(resp.content)
print(f"Installing Forge...")
argstring = ' '.join([jdk, "-jar", "\"" + forge_install_jar+ "\"", "--installServer", "\"" + directory + "\""])
install_process = Popen(argstring)
install_process.wait()
os.remove(forge_install_jar)
# Run the Forge server. Return process object
def run_forge_server(forge_dir: str, heap_arg):
java_exe = find_jdk()
if not os.path.isfile(java_exe):
java_exe = "java" # try to fall back on java in the PATH
@ -152,7 +220,13 @@ def run_forge_server(forge_dir, heap_arg):
heap_arg = heap_arg[:-1]
heap_arg = "-Xmx" + heap_arg
argstring = ' '.join([java_exe, heap_arg, "-jar", forge_server, "-nogui"])
args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, "win_args.txt")
win_args = []
with open(args_file) as argfile:
for line in argfile:
win_args.append(line.strip())
argstring = ' '.join([java_exe, heap_arg] + win_args + ["-nogui"])
logging.info(f"Running Forge server: {argstring}")
os.chdir(forge_dir)
return Popen(argstring)
@ -162,6 +236,10 @@ 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)")
parser.add_argument('--install', '-i', dest='install', default=False, action='store_true',
help="Download and install Java and the Forge server. Does not launch the client afterwards.")
parser.add_argument('--prerelease', default=False, action='store_true',
help="Auto-update prerelease versions.")
args = parser.parse_args()
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
@ -173,6 +251,12 @@ if __name__ == '__main__':
forge_dir = options["minecraft_options"]["forge_directory"]
max_heap = options["minecraft_options"]["max_heap_size"]
if args.install:
print("Installing Java and Minecraft Forge")
download_java()
install_forge(forge_dir)
sys.exit(0)
if apmc_file is not None and not os.path.isfile(apmc_file):
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
if not os.path.isdir(forge_dir):
@ -180,7 +264,7 @@ if __name__ == '__main__':
if not max_heap_re.match(max_heap):
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
update_mod(forge_dir)
update_mod(forge_dir, apmc_file, args.prerelease)
replace_apmc_files(forge_dir, apmc_file)
check_eula(forge_dir)
server_process = run_forge_server(forge_dir, max_heap)

View File

@ -22,8 +22,7 @@ ModuleUpdate.update()
import websockets
import colorama
import prompt_toolkit
from prompt_toolkit.patch_stdout import patch_stdout
from fuzzywuzzy import process as fuzzy_process
import NetUtils

View File

@ -1,4 +1,6 @@
from __future__ import annotations
import logging
import typing
import enum
from json import JSONEncoder, JSONDecoder
@ -138,11 +140,11 @@ class HandlerMeta(type):
break
def __init__(self, *args, **kwargs):
if orig_init:
orig_init(self, *args, **kwargs)
# turn functions into bound methods
self.handlers = {name: method.__get__(self, type(self)) for name, method in
handlers.items()}
if orig_init:
orig_init(self, *args, **kwargs)
attrs['__init__'] = __init__
return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs)
@ -192,11 +194,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_color(node)
def _handle_item_name(self, node: JSONMessagePart):
# todo: use a better info source
from worlds.alttp.Items import progression_items
node["color"] = 'green' if node.get("found", False) else 'cyan'
if node["text"] in progression_items:
node["color"] += ";white_bg"
node["color"] = 'cyan'
return self._handle_color(node)
def _handle_item_id(self, node: JSONMessagePart):
@ -205,13 +203,13 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_item_name(node)
def _handle_location_name(self, node: JSONMessagePart):
node["color"] = 'blue_bg;white'
node["color"] = 'green'
return self._handle_color(node)
def _handle_location_id(self, node: JSONMessagePart):
item_id = int(node["text"])
node["text"] = self.ctx.location_name_getter(item_id)
return self._handle_item_name(node)
return self._handle_location_name(node)
def _handle_entrance_name(self, node: JSONMessagePart):
node["color"] = 'blue'
@ -282,10 +280,11 @@ class Hint(typing.NamedTuple):
add_json_text(parts, self.entrance, type="entrance_name")
else:
add_json_text(parts, "'s World")
if self.found:
add_json_text(parts, ". (found)")
else:
add_json_text(parts, ". ")
if self.found:
add_json_text(parts, "(found)", type="color", color="green")
else:
add_json_text(parts, "(not found)", type="color", color="red")
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,

View File

@ -277,8 +277,8 @@ class OptionList(Option):
supports_weighting = False
value: list
def __init__(self, value: typing.List[str, typing.Any]):
self.value = value
def __init__(self, value: typing.List[typing.Any]):
self.value = value or []
super(OptionList, self).__init__()
@classmethod
@ -292,7 +292,7 @@ class OptionList(Option):
return cls.from_text(str(data))
def get_option_name(self, value):
return ", ".join(value)
return ", ".join(map(str, value))
def __contains__(self, item):
return item in self.value

View File

@ -87,7 +87,7 @@ def get_base_rom_data(game: str):
elif game == GAME_SM:
from worlds.sm.Rom import get_base_rom_bytes
elif game == GAME_SOE:
file_name = Utils.get_options()["soe_options"]["rom"]
file_name = Utils.get_options()["soe_options"]["rom_file"]
get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb")))
else:
raise RuntimeError("Selected game for base rom not found.")

View File

@ -13,6 +13,7 @@ Currently, the following games are supported:
* Timespinner
* Super Metroid
* Secret of Evermore
* Final Fantasy
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -175,14 +175,21 @@ async def deathlink_kill_player(ctx: Context):
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
if not ctx.death_link_allow_survive:
snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve 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)
if not gamemode or gamemode[0] in DEATH_MODES:
ctx.death_state = DeathState.dead
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):
health = await snes_read(ctx, WRAM_START + 0x09C2, 2)
if health is not None:
health = health[0] | (health[1] << 8)
if not gamemode or gamemode[0] in SM_DEATH_MODES or (ctx.death_link_allow_survive and health is not None and health > 0):
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
@ -884,6 +891,7 @@ async def game_watcher(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
ctx.death_link_allow_survive = False
game_name = await snes_read(ctx, SM_ROMNAME_START, 2)
if game_name is None:
continue
@ -900,6 +908,7 @@ async def game_watcher(ctx: Context):
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:
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set()
@ -1039,6 +1048,7 @@ async def game_watcher(ctx: Context):
ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)
async def run_game(romfile):
auto_start = Utils.get_options()["lttp_options"].get("rom_start", True)
if auto_start is True:
@ -1062,11 +1072,13 @@ async def main():
import Patch
logging.info("Patch file was supplied. Creating sfc rom..")
meta, romfile = Patch.create_rom_file(args.diff_file)
if "server" in meta:
args.connect = meta["server"]
logging.info(f"Wrote rom file to {romfile}")
if args.diff_file.endswith(".apsoe"):
import webbrowser
webbrowser.open("http://www.evermizer.com/apclient/")
webbrowser.open("http://www.evermizer.com/apclient/" +
(f"#server={meta['server']}" if "server" in meta else ""))
logging.info("Starting Evermizer Client in your Browser...")
import time
time.sleep(3)

View File

@ -23,7 +23,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.2.0"
__version__ = "0.2.1"
version_tuple = tuplize_version(__version__)
from yaml import load, dump, safe_load

View File

@ -14,7 +14,7 @@ from WebHostLib import app as raw_app
from waitress import serve
from WebHostLib.models import db
from WebHostLib.autolauncher import autohost
from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
@ -45,6 +45,8 @@ if __name__ == "__main__":
create_options_files()
if app.config["SELFLAUNCH"]:
autohost(app.config)
if app.config["SELFGEN"]:
autogen(app.config)
if app.config["SELFHOST"]: # using WSGI, you just want to run get_app()
if app.config["DEBUG"]:
autohost(app.config)

View File

@ -22,9 +22,10 @@ Pony(app)
app.jinja_env.filters['any'] = any
app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["SELFLAUNCH"] = True
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
@ -166,6 +167,7 @@ def host_room(room: UUID):
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()

View File

@ -110,6 +110,26 @@ def autohost(config: dict):
def keep_running():
try:
with Locker("autohost"):
while 1:
time.sleep(0.1)
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running, name="AP_Autohost").start()
def autogen(config: dict):
def keep_running():
try:
with Locker("autogen"):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
initargs=(config["PONY"],)) as generator_pool:
@ -129,22 +149,17 @@ def autohost(config: dict):
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
while 1:
time.sleep(0.50)
time.sleep(0.1)
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
to_start = select(
generation for generation in Generation if generation.state == STATE_QUEUED)
for generation in to_start:
launch_generator(generator_pool, generation)
except AlreadyRunningException:
pass
logging.info("Autogen reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running).start()
threading.Thread(target=keep_running, name="AP_Autogen").start()
multiworlds = {}

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import functools
import logging
import os
import websockets
import asyncio
import socket
@ -20,6 +19,7 @@ from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
class CustomClientMessageProcessor(ClientMessageProcessor):
ctx: WebHostContext
def _cmd_video(self, platform, user):
"""Set a link for your name in the WebHostLib tracker pointing to a video stream"""
if platform.lower().startswith("t"): # twitch
@ -37,6 +37,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
# inject
import MultiServer
MultiServer.client_message_processor = CustomClientMessageProcessor
del (MultiServer)
@ -101,6 +102,7 @@ class WebHostContext(Context):
d["video"] = [(tuple(playerslot), videodata) for playerslot, videodata in self.video.items()]
return d
def get_random_port():
return random.randint(49152, 65535)

View File

@ -10,6 +10,7 @@ def update_sprites_lttp():
from tkinter import Tk
from LttPAdjuster import get_image_for_sprite
from LttPAdjuster import BackgroundTaskProgress
from LttPAdjuster import BackgroundTaskProgressNullWindow
from LttPAdjuster import update_sprites
# Target directories
@ -19,11 +20,15 @@ def update_sprites_lttp():
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
# update sprites through gui.py's functions
done = threading.Event()
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
top.update()
task.do_events()
spriteData = []

View File

@ -6,12 +6,9 @@ Unlike most games on Archipelago.gg, Final Fantasy 1's settings are controlled e
## What does randomization do to this game?
A better questions is what isn't randomized at this point. Enemies stats and spell, character spells, shop inventory and boss stats and spells are all commonly randomized. Unlike most other randomizers it is also most standard to shuffle progression items and non-progression items into separate pools and then redistribute them to their respective locations. So ,for example, Princess Sarah may have the CANOE instead of the LUTE; however, she will never have a Heal Pot or some armor. There are plenty of other things that can be randomized on the main randomizer site: [FF1R Website](https://finalfantasyrandomizer.com/)
Some features are not currently supported by AP. A non-exhaustive list includes:
- Shard Hunt
- Deep Dungeon
## What Final Fantasy items can appear in other players' worlds?
Currently, only progression items can appear in other players' worlds. Armor, Weapons and Consumable Items can not.
All items can appear in other players worlds. This includes consumables, shards, weapons, armor and, of course,
key items.
## What does another world's item look like in Final Fantasy
All local and remote items appear the same. It will say that you received an item and then BOTH the client log and the emulator will display what was found external to the in-game text box.

View File

@ -6,7 +6,7 @@ The player settings page for this game contains all the options you need to conf
## What does randomization do to this game?
Items which would normally be acquired throughout the game have been moved around! Progression logic remains, so the game is always able to be completed. However, because of the item shuffle, the player may need to access certain areas before they would in the vanilla game. For example, the Windwalker (flying machine) is accessible as soon as any weapon is obtained.
Additional help can be found in the [Evermizer guide](https://github.com/black-sliver/evermizer/blob/feat-mw/guide.md).
Additional help can be found in the [Evermizer guide](https://github.com/black-sliver/evermizer/blob/master/guide.md).
## What items and locations get shuffled?
All gourds/chests/pots, boss drops and alchemists are shuffled. Alchemy ingredients, sniff spot items, call bead spells and the dog can be randomized using yaml options.

View File

@ -0,0 +1,49 @@
window.addEventListener('load', () => {
// Reload tracker every 15 seconds
const url = window.location;
setInterval(() => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
// Create a fake DOM using the returned HTML
const domParser = new DOMParser();
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
// Update item tracker
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
// Update only counters in the location-table
let counters = document.getElementsByClassName('counter');
const fakeCounters = fakeDOM.getElementsByClassName('counter');
for (let i = 0; i < counters.length; i++) {
counters[i].innerHTML = fakeCounters[i].innerHTML;
}
};
ajax.open('GET', url);
ajax.send();
}, 15000)
// Collapsible advancement sections
const categories = document.getElementsByClassName("location-category");
for (let i = 0; i < categories.length; i++) {
let hide_id = categories[i].id.split('-')[0];
if (hide_id == 'Total') {
continue;
}
categories[i].addEventListener('click', function() {
// Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header
const tab_header = document.getElementById(hide_id+'-header').children[0];
const orig_text = tab_header.innerHTML;
let new_text;
if (orig_text.includes("▼")) {
new_text = orig_text.replace("▼", "▲");
}
else {
new_text = orig_text.replace("▲", "▼");
}
tab_header.innerHTML = new_text;
});
}
});

View File

@ -1,23 +1,23 @@
# Advanced YAML Guide
This guide covers more the more advanced options available in YAML files. This guide is intended for the user who is intent on editing their YAML file manually. This guide should take about 10 minutes to read.
# Advanced Game Options Guide
If you would like to generate a basic, fully playable, YAML without editing a file then visit the settings page for the game you intend to play.
The settings page can be found on the supported games page, just click the "Settings Page" link under the name of the game you would like. Supported games page: [Archipelago Games List](https://archipelago.gg/games)
Clicking on the "Export Settings" button at the bottom-left will provide you with a pre-filled YAML with your options. The player settings page also has an option to download a fully filled out yaml containing every option with every available setting for the available options.
## YAML Overview
The Archipelago system generates games using player configuration files as input. These are going to be YAML files and each world will have one of these containing their custom settings for the game that world will play.
The Archipelago system generates games using player configuration files as input. Generally these are going to be
YAML files and each player will have one of these containing their custom settings for the randomized game they want to play.
On the website when you customize your settings from one of the game player settings pages which you can reach from the
[supported games page](/games). Clicking on the export settings button at the bottom will provide you with a pre-filled out
YAML with your options. The player settings page also has an option to download a fully filled out yaml containing every
option with every available setting for the available options.
## YAML Formatting
YAML files are a format of human-readable config files. The basic syntax of a yaml file will have a `root` node and then different levels of `nested` nodes that the generator reads in order to determine your settings.
YAML files are a format of <span data-tooltip="Allegedly.">human-readable</span> markup config files. The basic syntax
of a yaml file will have `root` and then different levels of `nested` text that the generator "reads" in order to determine
your settings. To nest text, the correct syntax is **two spaces over** from its root option. A YAML file can be edited
with whatever text editor you choose to use though I personally recommend that you use [Sublime Text](https://www.sublimetext.com/).
This program out of the box supports the correct formatting for the YAML file, so you will be able to tab and get proper
highlighting for any potential errors made while editing the file. If using any other text editor such as Notepad or
Notepad++ whenever you move to nest an option that it is done with two spaces and not tabs.
To nest text, the correct syntax is to indent **two spaces over** from its root option. A YAML file can be edited with whatever text editor you choose to use though I personally recommend that you use Sublime Text. Sublime text website: [SublimeText Website](https://www.sublimetext.com)
This program out of the box supports the correct formatting for the YAML file, so you will be able to use the tab key and get proper highlighting for any potential errors made while editing the file. If using any other text editor you should ensure your indentation is done correctly with two spaces.
A typical YAML file will look like:
Typical YAML format will look as follows:
```yaml
root_option:
nested_option_one:
@ -28,38 +28,49 @@ root_option:
option_two_setting_two: 43
```
In Archipelago, YAML options are always written out in full lowercase with underscores separating any words. The numbers following the colons here are weights. The generator will read the weight of every option the roll that option that many times, the next option as many times as its numbered and so forth.
For the above example `nested_option_one` will have `option_one_setting_one` 1 time and `option_one_setting_two` 0 times so `option_one_setting_one` is guaranteed to occur.
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43 times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed adding more randomness and "mystery" to your settings.
Every configurable setting supports weights.
In Archipelago YAML options are always written out in full lowercase with underscores separating any words. The numbers
following the colons here are weights. The generator will read the weight of every option the roll that option that many
times, the next option as many times as its numbered and so forth. For the above example `nested_option_one` will have
`option_one_setting_one` 1 time and `option_one_setting_two` 0 times so `option_one_setting_one` is guaranteed to occur.
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43
times against each other. This means `option_two_setting_two` will be more likely to occur but it isn't guaranteed adding
more randomness and "mystery" to your settings. Every configurable setting supports weights.
### Root Options
Currently, there are only a few options that are root options. Everything else should be nested within one of these root options or in some cases nested within other nested options. The only options that should exist in root are `description`, `name`, `game`, `requires`, `accessibility`, `progression_balancing`, `triggers`, and the name of the games you want settings for.
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files using this to detail the intention of the file.
* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can also be filled with multiple names each having a weight to it.
* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with different weights.
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this is good for detailing the version of Archipelago this YAML was prepared for as if it is rolled on an older version may be missing settings and as such will not work as expected. If any plando is used in the file then requiring it here to ensure it will be used is good practice.
* `accessibility` determines the level of access to the game the generation will expect you to have in order to reach your completion goal. This supports `items`, `locations`, and `none` and is set to `locations` by default.
* `locations` will guarantee all locations are accessible in your world.
* `items` will guarantee you can acquire all items in your world but may not be able to access all locations. This mostly comes into play if there is any entrance shuffle in the seed as locations without items in them can be placed in areas that make them unreachable.
* `none` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest
Currently there are only a few options that are root options. Everything else should be nested within one of these root
options or in some cases nested within other nested options. The only options that should exist in root are `description`,
`name`, `game`, `requires`, `accessibility`, `progression_balancing`, `triggers`, and the name of the games you want
settings for.
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files using
this to detail the intention of the file.
* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can also
be filled with multiple names each having a weight to it.
* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with different
weights.
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
is good for detailing the version of Archipelago this YAML was prepared for as if it is rolled on an older version may be
missing settings and as such will not work as expected. If any plando is used in the file then requiring it here to ensure
it will be used is good practice.
* `accessibility` determines the level of access to the game the generation will expect you to have in order to reach your
completion goal. This supports `items`, `locations`, and `none` and is set to `locations` by default.
* `items` will guarantee you can acquire all items in your world but may not be able to access all locations. This mostly
comes into play if there is any entrance shuffle in the seed as locations without items in them can be placed in areas
that make them unreachable.
* `none` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but
may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest
in a dungeon in ALTTP making it impossible to get and finish the dungeon.
* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible. This primarily involves moving necessary progression items into earlier logic spheres to make the games more accessible so that players almost always have something to do. This can be turned `on` or `off` and is `on` by default.
* `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read more triggers in the triggers guide. Triggers guide: [Archipelago Triggers Guide](https://archipelago.gg/tutorial/archipelago/triggers/en)
* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible. This
primarily involves moving necessary progression items into earlier logic spheres to make the games more accessible so that
players almost always have something to do. This can be turned `on` or `off` and is `on` by default.
* `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read more
about this [here](/tutorial/archipelago/triggers/en).
### Game Options
One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to give a weight to any option it is possible to have one file that can generate a seed for you where you don't know which game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these settings. If a game can be rolled it **must** have a settings section even if it is empty.
One of your root settings will be the name of the game you would like to populate with settings in the format
`GameName`. since it is possible to give a weight to any option it is possible to have one file that can generate a seed
for you where you don't know which game you'll play. For these cases you'll want to fill the game options for every game
that can be rolled by these settings. If a game can be rolled it **must** have a settings section even if it is empty.
#### Universal Game Options
@ -67,21 +78,21 @@ Some options in Archipelago can be used by every game but must still be placed w
Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`, `exclude_locations`, and various plando options.
See the plando guide for more info on plando options. Plando guide: [Archipelago Plando Guide](https://archipelago.gg/tutorial/archipelago/plando/en)
See the plando guide for more info on plando options. Plando guide: [Archipelago Plando Guide](/tutorial/archipelago/plando/en)
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which will give you the item `Rupees(5)` six times, totalling 30 rupees.
* `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for the location without using any hint points.
* `local_items` will force any items you want to be in your world instead of being in another world.
* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be located
in your own.
* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in
it to see how important the location is.
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
item which isn't necessary for progression to go in these locations.
* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be located in your own.
* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in it to see how important the location is.
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk" item which isn't necessary for progression to go in these locations.
### Example YAML
### Example
```yaml
@ -124,33 +135,28 @@ triggers:
#### This is a fully functional yaml file that will do all the following things:
* `description` gives us a general overview so if we pull up this file later we can understand the intent.
* `name` is `Example Player` and this will be used in the server console when sending and receiving items.
* `game` is set to `A Link to the Past` meaning that is what game we will play with this file.
* `requires` is set to require release version 0.2.0 or higher.
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be completely inaccessible but the seed will still be completable.
* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of having things to do.
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
completely inaccessible but the seed will still be completable.
* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of having
things to do.
* `A Link to the Past` defines a location for us to nest all the game options we would like to use for our game `A Link to the Past`.
* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this example we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be placed anywhere amongst the multiworld.
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this example we have:
* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this example
we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be placed anywhere
amongst the multiworld.
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this example
we have:
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
* `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs
* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use with no cost.
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we have to find it ourselves.
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
have to find it ourselves.
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld that can be used for no cost.
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the multiworld
that can be used for no cost.
* `exclude_locations` forces a not important item to be placed on the `Cave 45` location.
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world` result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the `any_world` result.
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world`
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to the `any_world`
result.

View File

@ -1,86 +1,18 @@
# Minecraft Randomizer Setup Guide
#Automatic Hosting Install
- Download and install Archipelago at: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- Choose the `Minecraft Client` module during the installation.
## Required Software
- Minecraft Java Edition from: [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition)
- Minecraft Java Edition from the [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition) (update 1.17.1)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- (select `Minecraft Client` during installation.)
## Configuring your YAML file
### What is a YAML file and why do I need one?
See the guide on setting up a basic YAML at the Archipelago setup guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en)
### What Does a YAML Look Like for Minecraft?
A basic minecraft yaml will look like this.
```yaml
description: Basic Minecraft Yaml
# Your name in-game. Spaces will be replaced with underscores and
# there is a 16 character limit
name: YourName
game: Minecraft
# Shared Options supported by all games:
accessibility: locations
progression_balancing: on
# Minecraft Specific Options
Minecraft:
# Number of advancements required (87 max) to spawn the Ender Dragon and complete the game.
advancement_goal: 50
# Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn.
egg_shards_required: 10
# Number of egg shards available in the pool (30 max).
egg_shards_available: 15
# Modifies the level of items logically required for
# exploring dangerous areas and fighting bosses.
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Junk-fills certain RNG-reliant or tedious advancements.
include_hard_advancements:
on: 0
off: 1
# Junk-fills extremely difficult advancements;
# this is only How Did We Get Here? and Adventuring Time.
include_insane_advancements:
on: 0
off: 1
# Some advancements require defeating the Ender Dragon;
# this will junk-fill them, so you won't have to finish them to send some items.
include_postgame_advancements:
on: 0
off: 1
# Enables shuffling of villages, outposts, fortresses, bastions, and end cities.
shuffle_structures:
on: 0
off: 1
# Adds structure compasses to the item pool,
# which point to the nearest indicated structure.
structure_compasses:
on: 0
off: 1
# Replaces a percentage of junk items with bee traps
# which spawn multiple angered bees around every player when received.
bee_traps:
0: 1
25: 0
50: 0
75: 0
100: 0
```
### Where do I get a YAML file?
You can customize your settings by visiting the [Minecraft Player Settings Page](/games/Minecraft/player-settings)
## Joining a MultiWorld Game
@ -89,35 +21,23 @@ Minecraft:
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, the host will provide you with either a link to download your data file, or with a zip file containing everyone's data files. Your data file should have a `.apmc` extension.
Double-click on your `.apmc` file to have the minecraft client auto-launch the installed forge server.
Double-click on your `.apmc` file to have the Minecraft client auto-launch the installed forge server. Make sure to leave this window open as this is your server console.
### Connect to the MultiServer
If you are running Forge manually, you must place the `.apmc` file in your `APData` folder in the Forge installation directory. If the `APData` folder does not exist then you may create it. After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client.
Using minecraft 1.17.1 connect to the server `localhost`.
In all cases, no matter how the Forge server is hosted: once you are in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. `(Password)` is only required if the Archipelago server you are using has a password set.
Once you are in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. `(Password)` is only required if the Archipelago server you are using has a password set.
### Play the game
When the console tells you that you have joined the room, you're ready to begin playing. Congratulations on successfully joining a multiworld game! At this point any additional minecraft players may connect to your forge server. When you are ready to start the game use the `/start` command within the Minecraft game.
When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a multiworld game! At this point any additional minecraft players may connect to your forge server. To start the game once everyone is ready use the command `/start`.
## Manual Installation Procedures
This is only required if you wish to set up a forge install yourself, it's recommended to just use the Archipelago Installer.
## Manual Installation
It is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you. support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so, the following links are the versions of the software we use.
###Required Software
- Minecraft Forge from: [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
- Minecraft Archipelago Randomizer Mod from: [AP Randomizer Forge Server Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
- **DO NOT INSTALL THIS ON YOUR CLIENT**
### Manual install Software links
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.17.1.html)
- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
**DO NOT INSTALL THIS ON YOUR CLIENT**
- [Java 16 Download Page](https://docs.aws.amazon.com/corretto/latest/corretto-16-ug/downloads-list.html)
### Dedicated Server Setup
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
- This will create the appropriate directories for you to place the files in the following step.
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!

View File

@ -74,3 +74,4 @@ Below is a list of MSU packs which, so far as we know, are safe to stream. More
we learn of them. If you know of any we missed, please let us know!
- Vanilla Game Music
- [Smooth McGroove](https://drive.google.com/open?id=1JDa1jCKg5hG0Km6xNpmIgf4kDMOxVp3n)
- Zelda community member Amarith assembled the following list for the purpose of competitive restreams. While we have not ourselves verified this list, all submissions required VoD proof they were not muted. Generally speaking, MSU-1 packs are less safe if they contain lyrics at any point. This list was only tested on Twitch and results for other platforms may vary. [Restream-Safe List](https://tinyurl.com/MSUsApprovedForLeagueChannels)

View File

@ -67,7 +67,7 @@ When the client launched automatically, SNI should have also automatically launc
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- Z3Client users should download `sniConnector.lua` from the client download page
- SNIClient users should look in their Archipelago folder for `/sni/Connector.lua`
- SNIClient users should look in their Archipelago folder for `/sni/lua`
##### BizHawk
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
@ -76,10 +76,11 @@ When the client launched automatically, SNI should have also automatically launc
Once you have changed the loaded core, you must restart BizHawk.
2. Load your ROM file if it hasn't already been loaded.
3. Click on the Tools menu and click on **Lua Console**
4. Click the button to open a new Lua script.
5. Select the `sniConnector.lua` file you downloaded above
4. Click Script -> Open Script...
5. Select the `Connector.lua` file you downloaded above
- Z3Client users should download `sniConnector.lua` from the client download page
- SNIClient users should look in their Archipelago folder for `/sni/Connector.lua`
- SNIClient users should look in their Archipelago folder for `/sni/lua`
6. Run the script by double-clicking it in the listing
#### With hardware
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do this now. SD2SNES and FXPak Pro users may download the appropriate firmware [from the sd2snes releases page](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information [on the usb2snes supported platforms page](http://usb2snes.com/#supported-platforms).

File diff suppressed because it is too large Load Diff

View File

@ -1,449 +0,0 @@
# What is this file?
# This file contains options which allow you to configure your multiworld experience while allowing others
# to play how they want as well.
# How do I use it?
# The options in this file are weighted. This means the higher number you assign to a value, the more
# chances you have for that option to be chosen. For example, an option like this:
#
# map_shuffle:
# on: 5
# off: 15
#
# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off
# I've never seen a file like this before. What characters am I allowed to use?
# This is a .yaml file. You are allowed to use most characters.
# To test if your yaml is valid or not, you can use this website:
# http://www.yamllint.com/
# For use with the weighted-settings page on the website. Changing this value will cause all users to be prompted
# to update their settings. The version number should match the current released version number, and the revision
# should be updated manually by whoever edits this file.
ws_version: 4.1.1 rev0
description: Template Name # Used to describe your yaml. Useful if you have multiple files
name: YourName # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
### Logic Section ###
glitches_required: # Determine the logic required to complete the seed
none: 50 # No glitches required
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches (fake flipper, super bunny shenanigans, water walk and etc.)
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
# Other players items are placed into your world under OWG logic
dark_room_logic: # Logic for unlit dark rooms
lamp: 50 # require the Lamp for these rooms to be considered accessible.
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
restrict_dungeon_item_on_boss: # aka ambrosia boss items
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
off: 50
### End of Logic Section ###
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
mode:
- inverted # Never play inverted seeds
retro:
- on # Never play retro seeds
weapons:
- swordless # Never play a swordless seed
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
on: 0
off: 50
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
on: 0
off: 50
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
on: 0
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
off: 50
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
on: 0
off: 50
local_keys: # Keep small keys and big keys local to your world
on: 0
off: 50
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
mc: 0 # Shuffle maps and compasses
none: 50 # Shuffle none of the 4
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
ub: 0 # universal small keys and shuffled big keys
# you can add more combos of these letters here
dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon
pickup: 50 # Show when compass is picked up
default: 0 # Show when compass is picked up if the compass itself is shuffled
off: 0 # Never show item count in dungeons
accessibility:
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
locations: 50 # Guarantees you will be able to access all locations, and therefore all items
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
progressive: # Enable or disable progressive items (swords, shields, bow)
on: 50 # All items are progressive
off: 0 # No items are progressive
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
entrance_shuffle: # Documentation: https://alttpr.com/en/options#entrance_shuffle
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
restricted: 0 # Less strict than simple
full: 0 # Less strict than restricted
crossed: 0 # Less strict than full
insanity: 0 # Very few grouping rules. Good luck
goals:
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
fast_ganon: 0 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT
dungeons: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
pyramid_open:
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
auto: 0 # Same as Goal, but also opens when any non-dungeon entrance shuffle is used
yes: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
no: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
available: 50 # available = triforce_pieces_available
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
# Format "pieces: chance"
0: 0
5: 50
10: 50
15: 0
20: 0
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
# Format "pieces: chance"
100: 0 #No extra
150: 50 #Half the required will be added as extra
200: 0 #There are the double of the required ones available.
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
# Format "pieces: chance"
25: 0
30: 50
40: 0
50: 0
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
# Format "pieces: chance"
15: 0
20: 50
30: 0
40: 0
50: 0
tower_open: # Crystals required to open GT
'0': 80
'1': 70
'2': 60
'3': 50
'4': 40
'5': 30
'6': 20
'7': 10
random: 0
ganon_open: # Crystals required to hurt Ganon
'0': 80
'1': 70
'2': 60
'3': 50
'4': 40
'5': 30
'6': 20
'7': 10
random: 0
mode:
standard: 50 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
retro:
on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
off: 50
hints:
'on': 50 # Hint tiles sometimes give item location hints
'off': 0 # Hint tiles provide gameplay tips
weapons: # Specifically, swords
randomized: 0 # Swords are placed randomly throughout the world
assured: 50 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
item_pool:
easy: 0 # Doubled upgrades, progressives, and etc
normal: 50 # Item availability remains unchanged from vanilla game
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
item_functionality:
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
normal: 50 # Vanilla item functionality
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
progression_balancing:
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch around missing items.
tile_shuffle: # Randomize the tile layouts in flying tile rooms
on: 0
off: 50
misery_mire_medallion: # required medallion to open Misery Mire front entrance
random: 50
ether: 0
bombos: 0
quake: 0
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
random: 50
ether: 0
bombos: 0
quake: 0
### Enemizer Section ###
boss_shuffle:
none: 50 # Vanilla bosses
simple: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
full: 0 # 3 bosses can occur twice
random: 0 # Any boss can appear any amount of times
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
enemy_shuffle: # Randomize enemy placement
on: 0
off: 50
killable_thieves: # Make thieves killable
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
off: 50
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
on: 0
off: 50
enemy_damage:
default: 50 # Vanilla enemy damage
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
enemy_health:
default: 50 # Vanilla enemy HP
easy: 0 # Enemies have reduced health
hard: 0 # Enemies have increased health
expert: 0 # Enemies have greatly increased health
pot_shuffle:
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
'off': 50 # Default pot item locations
### End of Enemizer Section ###
# can add weights for any whole number between 0 and 100
beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
0: 50 # No junk fill items are replaced (Beemizer is off)
25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees
beemizer_trap_chance:
60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee
90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee
100: 0 # All beemizer replacements are traps
### Shop Settings ###
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
0: 50
10: 0
20: 0
30: 0
shop_shuffle:
none: 50
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
f: 0 # Generate new default inventories for every shop independently
p: 0 # Randomize the prices of the items in shop inventories
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
gp: 0 # Shuffle inventories and randomize prices
fp: 0 # Randomize items in every shop and their prices
ufp: 0 # Randomize items and prices in every shop, and include capacity upgrades in item pool
wfp: 0 # Randomize items and prices in every shop, and include potion shop inventory in shuffle
ufpw: 0 # Randomize items and prices in every shop, shuffle potion shop inventory, and include capacity upgrades
# You can add more combos
### End of Shop Section ###
shuffle_prizes: # aka drops
none: 0 # do not shuffle prize packs
g: 50 # shuffle "general" price packs, as in enemy, tree pull, dig etc.
b: 0 # shuffle "bonk" price packs
bg: 0 # shuffle both
timer:
none: 50 # No timer will be displayed.
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
ohko: 0 # Timer always at zero. Permanent OHKO.
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
10: 50
20: 0
30: 0
60: 0
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
-2: 50
1: 0
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
1: 0
2: 50
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
4: 50
10: 0
15: 0
# Can be uncommented to use it
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl"
# - "Small Keys"
# - "Big Keys"
# Can be uncommented to use it
# startinventory: # Begin the file with the listed items/upgrades
# Pegasus Boots: on
# Bomb Upgrade (+10): 4
# Arrow Upgrade (+10): 4
glitch_boots:
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
off: 0
linked_options:
- name: crosskeys
options: # These overwrite earlier options if the percentage chance triggers
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
percentage: 0 # Set this to the percentage chance you want crosskeys
- name: localcrosskeys
options: # These overwrite earlier options if the percentage chance triggers
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
local_items: # Forces keys to be local to your own world
- "Small Keys"
- "Big Keys"
percentage: 0 # Set this to the percentage chance you want local crosskeys
- name: enemizer
options:
boss_shuffle: # Subchances can be injected too, which then get rolled
simple: 1
full: 1
random: 1
singularity: 1
enemy_damage:
shuffled: 1
random: 1
enemy_health:
easy: 1
hard: 1
expert: 1
percentage: 0 # Set this to the percentage chance you want enemizer
### door rando only options ###
door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
vanilla: 50 # Everything should be like in vanilla
basic: 0 # Dungeons are shuffled within themselves
crossed: 0 # Dungeons are shuffled across each other
intensity: # Only available if the host uses the doors branch, it is ignored otherwise
1: 50 # Shuffles normal doors and spiral staircases
2: 0 # And shuffles open edges and straight staircases
3: 0 # And shuffles dungeon lobbies
random: 0 # Picks one of those at random
key_drop_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables the small keys dropped by enemies or under pots, and the big key dropped by the Ball & Chain guard to be shuffled into the pool. This extends the number of checks to 249.
off: 50
experimental: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables experimental features.
off: 50
debug: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables debugging features. Currently, these are the Item collection counter. (overwrites total triforce pieces) and Castle Gate closed indicator.
off: 50
### end of door rando only options ###
rom:
#sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this.
# - link
# - pride link
# - penguin link
# - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool.
sprite: # Enter the name of your preferred sprite and weight it appropriately
random: 0
link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
disablemusic: # If "on", all in-game music will be disabled
on: 0
off: 50
quickswap: # Enable switching items by pressing the L+R shoulder buttons
on: 50
off: 0
triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala
normal: 50 # original behavior (always visible)
hide_goal: 0 # hide counter until a piece is collected or speaking to Murahadala
hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala
hide_both: 0 # Hide both under above circumstances
reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more.
on: 50
off: 0
menuspeed: # Controls how fast the item menu opens and closes
normal: 50
instant: 0
double: 0
triple: 0
quadruple: 0
half: 0
heartcolor: # Controls the color of your health hearts
red: 50
blue: 0
green: 0
yellow: 0
random: 0
heartbeep: # Controls the frequency of the low-health beeping
double: 0
normal: 50
half: 0
quarter: 0
off: 0
ow_palettes: # Change the colors of the overworld
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
uw_palettes: # Change the colors of caves and dungeons
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
hud_palettes: # Change the colors of the hud
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
sword_palettes: # Change the colors of swords
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
shield_palettes: # Change the colors of shields
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0

View File

@ -0,0 +1,104 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 384px;
background-color: #546E7A;
}
#inventory-table td{
width: 45px;
height: 45px;
text-align: center;
vertical-align: middle;
}
#inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
min-width: 40px;
min-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%);
}
#inventory-table img.acquired{
filter: none;
}
#inventory-table div.counted-item {
position: relative;
}
#inventory-table div.item-count {
position: absolute;
color: white;
font-family: "Minecraftia", monospace;
font-weight: bold;
bottom: 0px;
right: 0px;
}
#location-table{
width: 384px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #546E7A;
color: #000000;
padding: 0 3px 3px;
font-size: 14px;
cursor: default;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 12px;
}
#location-table td.location-name {
padding-left: 16px;
}
.hide {
display: none;
}

View File

@ -42,6 +42,7 @@
<td><img src="{{ icons['Enchanting Table'] }}" class="{{ 'acquired' if 'Enchanting' in acquired_items }}" title="Enchanting" /></td>
<td><img src="{{ icons['Fishing Rod'] }}" class="{{ 'acquired' if 'Fishing Rod' in acquired_items }}" title="Fishing Rod" /></td>
<td><img src="{{ icons['Campfire'] }}" class="{{ 'acquired' if 'Campfire' in acquired_items }}" title="Campfire" /></td>
<td><img src="{{ icons['Spyglass'] }}" class="{{ 'acquired' if 'Spyglass' in acquired_items }}" title="Spyglass" /></td>
<td><img src="{{ icons['Dragon Head'] }}" class="{{ 'acquired' if game_finished }}" title="Ender Dragon" /></td>
</tr>
</table>

View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/supermetroidTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/supermetroidTracker.js') }}"></script>
</head>
<body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td><img src="{{ icons['Charge Beam'] }}" class="{{ 'acquired' if 'Charge Beam' in acquired_items }}" title="Charge Beam" /></td>
<td><img src="{{ icons['Ice Beam'] }}" class="{{ 'acquired' if 'Ice Beam' in acquired_items }}" title="Ice Beam" /></td>
<td><img src="{{ icons['Wave Beam'] }}" class="{{ 'acquired' if 'Wave Beam' in acquired_items }}" title="Wave Beam" /></td>
<td><img src="{{ icons['Spazer'] }}" class="{{ 'acquired' if 'Spazer' in acquired_items }}" title="S p a z e r" /></td>
<td><img src="{{ icons['Plasma Beam'] }}" class="{{ 'acquired' if 'Plasma Beam' in acquired_items }}" title="Plasma Beam" /></td>
<td><img src="{{ icons['Varia Suit'] }}" class="{{ 'acquired' if 'Varia Suit' in acquired_items }}" title="Varia Suit" /></td>
<td><img src="{{ icons['Gravity Suit'] }}" class="{{ 'acquired' if 'Gravity Suit' in acquired_items }}" title="Gravity Suit" /></td>
</tr>
<tr>
<td><img src="{{ icons['Morph Ball'] }}" class="{{ 'acquired' if 'Morph Ball' in acquired_items }}" title="Morph Ball" /></td>
<td><img src="{{ icons['Bomb'] }}" class="{{ 'acquired' if 'Bomb' in acquired_items }}" title="Bomb" /></td>
<td><img src="{{ icons['Spring Ball'] }}" class="{{ 'acquired' if 'Spring Ball' in acquired_items }}" title="Spring Ball" /></td>
<td><img src="{{ icons['Screw Attack'] }}" class="{{ 'acquired' if 'Screw Attack' in acquired_items }}" title="Screw Attack" /></td>
<td><img src="{{ icons['Hi-Jump Boots'] }}" class="{{ 'acquired' if 'Hi-Jump Boots' in acquired_items }}" title="Hi-Jump Boots" /></td>
<td><img src="{{ icons['Space Jump'] }}" class="{{ 'acquired' if 'Space Jump' in acquired_items }}" title="Space Jump" /></td>
<td><img src="{{ icons['Speed Booster'] }}" class="{{ 'acquired' if 'Speed Booster' in acquired_items }}" title="Speed Booster" /></td>
</tr>
<tr>
<td>
<div class="counted-item">
<img src="{{ icons['Energy Tank'] }}" class="{{ 'acquired' if energy_count > 0 }}" title="Energy Tank" />
<div class="item-count">{{ energy_count }}</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Reserve Tank'] }}" class="{{ 'acquired' if reserve_count > 0 }}" title="Reserve Tank" />
<div class="item-count">{{ reserve_count }}</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Missile'] }}" class="{{ 'acquired' if missile_count > 0 }}" title="Missile ({{missile_count * 5}})" />
<div class="item-count">{{ missile_count }}</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Super Missile'] }}" class="{{ 'acquired' if super_count > 0 }}" title="Super Missile ({{super_count * 5}})" />
<div class="item-count">{{ super_count }}</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Power Bomb'] }}" class="{{ 'acquired' if power_count > 0 }}" title="Power Bomb ({{power_count * 5}})" />
<div class="item-count">{{ power_count }}</div>
</div>
</td>
<td><img src="{{ icons['Grappling Beam'] }}" class="{{ 'acquired' if 'Grappling Beam' in acquired_items }}" title="Grappling Beam" /></td>
<td><img src="{{ icons['X-Ray Scope'] }}" class="{{ 'acquired' if 'X-Ray Scope' in acquired_items }}" title="X-Ray Scope" /></td>
</tr>
</table>
<table id="location-table">
{% for area in checks_done %}
<tr class="location-category" id="{{area}}-header">
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
</tr>
<tbody class="locations hide" id="{{area}}">
{% for location in location_info[area] %}
<tr>
<td class="location-name">{{ location }}</td>
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>
</body>
</html>

View File

@ -20,7 +20,7 @@
Multistream
</a>
</span>
<span class="info">This tracker will automatically update itself periodically.</span>
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
</div>
<div id="tables-container">
{% for team, players in inventory.items() %}

View File

@ -281,7 +281,7 @@ def get_static_room_data(room: Room):
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
# Team and player must be positive and greater than zero
if tracked_team < 0 or tracked_player < 1:
abort(404)
@ -324,7 +324,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
checks_done[location_to_area[location]] += 1
checks_done["Total"] += 1
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
if specific_tracker:
if specific_tracker and not want_generic:
return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
seed_checks_in_area, checks_done)
else:
@ -332,6 +332,11 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
seed_checks_in_area, checks_done)
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
return getPlayerTracker(tracker, tracked_team, tracked_player, True)
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
inventory: Counter, team: int, player: int, player_name: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
@ -423,19 +428,21 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
"Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png",
}
minecraft_location_ids = {
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
42070, 42041, 42049, 42090, 42004, 42031, 42025, 42029, 42051, 42077, 42089],
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
"Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42088],
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
42036, 42057, 42063, 42053, 42083, 42084, 42091]
"Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42099, 42100],
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028, 42036,
42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095],
"Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091],
}
display_data = {}
@ -767,6 +774,106 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
icons = {
"Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/ETank.png",
"Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Missile.png",
"Super Missile": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Super.png",
"Power Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/PowerBomb.png",
"Bomb": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Bomb.png",
"Charge Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Charge.png",
"Ice Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Ice.png",
"Hi-Jump Boots": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/HiJump.png",
"Speed Booster": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpeedBooster.png",
"Wave Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Wave.png",
"Spazer": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Spazer.png",
"Spring Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpringBall.png",
"Varia Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Varia.png",
"Plasma Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Plasma.png",
"Grappling Beam": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Grapple.png",
"Morph Ball": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Morph.png",
"Reserve Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Reserve.png",
"Gravity Suit": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/Gravity.png",
"X-Ray Scope": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/XRayScope.png",
"Space Jump": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/SpaceJump.png",
"Screw Attack": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/ScrewAttack.png",
"Nothing": "",
"No Energy": "",
"Kraid": "",
"Phantoon": "",
"Draygon": "",
"Ridley": "",
"Mother Brain": "",
}
multi_items = {
"Energy Tank": 83000,
"Missile": 83001,
"Super Missile": 83002,
"Power Bomb": 83003,
"Reserve Tank": 83020,
}
supermetroid_location_ids = {
'Crateria/Blue Brinstar': [82005, 82007, 82008, 82026, 82029,
82000, 82004, 82006, 82009, 82010,
82011, 82012, 82027, 82028, 82034,
82036, 82037],
'Green/Pink Brinstar': [82017, 82023, 82030, 82033, 82035,
82013, 82014, 82015, 82016, 82018,
82019, 82021, 82022, 82024, 82025,
82031],
'Red Brinstar': [82038, 82042, 82039, 82040, 82041],
'Kraid': [82043, 82048, 82044],
'Norfair': [82050, 82053, 82061, 82066, 82068,
82049, 82051, 82054, 82055, 82056,
82062, 82063, 82064, 82065, 82067],
'Lower Norfair': [82078, 82079, 82080, 82070, 82071,
82073, 82074, 82075, 82076, 82077],
'Crocomire': [82052, 82060, 82057, 82058, 82059],
'Wrecked Ship': [82129, 82132, 82134, 82135, 82001,
82002, 82003, 82128, 82130, 82131,
82133],
'West Maridia': [82138, 82136, 82137, 82139, 82140,
82141, 82142],
'East Maridia': [82143, 82145, 82150, 82152, 82154,
82144, 82146, 82147, 82148, 82149,
82151],
}
display_data = {}
for item_name, item_id in multi_items.items():
base_name = item_name.split()[0].lower()
count = inventory[item_id]
display_data[base_name+"_count"] = inventory[item_id]
# Victory condition
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
display_data['game_finished'] = game_state == 30
# Turn location IDs into advancement tab counts
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
lookup_name = lambda id: lookup_any_location_id_to_name[id]
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
for tab_name, tab_locations in supermetroid_location_ids.items()}
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
for tab_name, tab_locations in supermetroid_location_ids.items()}
checks_done['Total'] = len(checked_locations)
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()}
checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("supermetroidTracker.html",
inventory=inventory, icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name},
player=player, team=team, room=room, player_name=playerName,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
@ -880,5 +987,6 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
"Minecraft": __renderMinecraftTracker,
"Ocarina of Time": __renderOoTTracker,
"Timespinner": __renderTimespinnerTracker,
"A Link to the Past": __renderAlttpTracker
"A Link to the Past": __renderAlttpTracker,
"Super Metroid": __renderSuperMetroidTracker
}

View File

@ -32,6 +32,8 @@
pos: (0, 0)
<ServerToolTip>:
size: self.texture_size
size_hint: None, None
font_size: dp(18)
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
halign: "left"
canvas.before:
@ -40,3 +42,13 @@
Rectangle:
size: self.size
pos: self.pos
Color:
rgba: 0.098, 0.337, 0.431, 1
Line:
width: 3
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
Color:
rgba: 0.235, 0.678, 0.843, 1
Line:
width: 1
rectangle: self.x-2, self.y-2, self.width+4, self.height+4

View File

@ -349,12 +349,21 @@ class JSONMessagePart(TypedDict):
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
Possible values for `type` include:
* player_id
* item_id
* location_id
* entrance_name
`color` is used to denote a console color to display the message part with. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time.
| Name | Notes |
| ---- | ----- |
| text | Regular text content. Is the default type and as such may be omitted. |
| player_id | player ID of someone on your team, should be resolved to Player Name |
| player_name | Player Name, could be a player within a multiplayer game or from another team, not ID resolvable |
| item_id | Item ID, should be resolved to Item Name |
| item_name | Item Name, not currently used over network, but supported by reference Clients. |
| location_id | Location ID, should be resolved to Location Name |
| location_name |Location Name, not currently used over network, but supported by reference Clients. |
| entrance_name | Entrance Name. No ID mapping exists. |
| color | Regular text that should be colored. Only `type` that will contain `color` data. |
`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time.
Color options:
* bold

View File

@ -62,6 +62,7 @@ Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup";
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
[Dirs]
@ -84,11 +85,9 @@ Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignor
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: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#sourcepath}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
;minecraft temp files
Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesntexist external deleteafterinstall; Components: client/minecraft
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
@ -96,16 +95,19 @@ Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient
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: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
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
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.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/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
Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft
[UninstallDelete]
Type: dirifempty; Name: "{app}"
@ -125,6 +127,11 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archi
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: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1""";
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
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
@ -140,7 +147,6 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{
const
SHCONTCH_NOPROGRESSBOX = 4;
SHCONTCH_RESPONDYESTOALL = 16;
FORGE_VERSION = '1.16.5-36.2.0';
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
@ -162,48 +168,6 @@ begin
end;
end;
function IsForgeNeeded(): boolean;
begin
Result := True;
if (FileExists(ExpandConstant('{app}')+'\Minecraft Forge Server\forge-'+FORGE_VERSION+'.jar')) then
Result := False;
end;
function IsJavaNeeded(): boolean;
begin
Result := True;
if (FileExists(ExpandConstant('{app}')+'\jre8\bin\java.exe')) then
Result := False;
end;
function OnDownloadMinecraftProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean;
begin
if Progress = ProgressMax then
Log(Format('Successfully downloaded Minecraft additional files to {tmp}: %s', [FileName]));
Result := True;
end;
procedure UnZip(ZipPath, TargetPath: string);
var
Shell: Variant;
ZipFile: Variant;
TargetFolder: Variant;
begin
Shell := CreateOleObject('Shell.Application');
ZipFile := Shell.NameSpace(ZipPath);
if VarIsClear(ZipFile) then
RaiseException(
Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath]));
TargetFolder := Shell.NameSpace(TargetPath);
if VarIsClear(TargetFolder) then
RaiseException(Format('Target path "%s" does not exist', [TargetPath]));
TargetFolder.CopyHere(
ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;
var R : longint;
var lttprom: string;
@ -218,8 +182,6 @@ var SoERomFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
var MinecraftDownloadPage: TDownloadWizardPage;
function GetSNESMD5OfFile(const rom: string): string;
var data: AnsiString;
begin
@ -267,11 +229,6 @@ begin
'.sfc');
end;
procedure AddMinecraftDownloads();
begin
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
end;
procedure AddOoTRomPage();
begin
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
@ -304,33 +261,7 @@ end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
if (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin
MinecraftDownloadPage.Clear;
if(IsForgeNeeded()) then
MinecraftDownloadPage.Add('https://maven.minecraftforge.net/net/minecraftforge/forge/'+FORGE_VERSION+'/forge-'+FORGE_VERSION+'-installer.jar','forge-installer.jar','');
if(IsJavaNeedeD()) then
MinecraftDownloadPage.Add('https://corretto.aws/downloads/latest/amazon-corretto-8-x64-windows-jre.zip','java.zip','');
MinecraftDownloadPage.Show;
try
try
MinecraftDownloadPage.Download;
Result := True;
except
if MinecraftDownloadPage.AbortedByUser then
Log('Aborted by user.')
else
SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
Result := False;
end;
finally
if( isJavaNeeded() ) then
if(ForceDirectories(ExpandConstant('{app}'))) then
UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}'));
MinecraftDownloadPage.Hide;
end;
Result := True;
end
else if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
Result := not (LttPROMFilePage.Values[0] = '')
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
Result := not (SMROMFilePage.Values[0] = '')
@ -394,7 +325,7 @@ function GetOoTROMPath(Param: string): string;
begin
if Length(ootrom) > 0 then
Result := ootrom
else if (Assigned(OoTROMFilePage)) then
else if Assigned(OoTROMFilePage) then
begin
R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
if R <> 0 then
@ -421,8 +352,6 @@ begin
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
if Length(soerom) = 0 then
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
AddMinecraftDownloads();
end;

View File

@ -62,6 +62,7 @@ Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup";
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
[Dirs]
@ -84,11 +85,9 @@ Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignor
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: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#sourcepath}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
;minecraft temp files
Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesntexist external deleteafterinstall; Components: client/minecraft
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
@ -96,16 +95,19 @@ Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient
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: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
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
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.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/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
Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft
[UninstallDelete]
Type: dirifempty; Name: "{app}"
@ -125,6 +127,11 @@ Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archi
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: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1""";
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
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
@ -140,7 +147,6 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{
const
SHCONTCH_NOPROGRESSBOX = 4;
SHCONTCH_RESPONDYESTOALL = 16;
FORGE_VERSION = '1.16.5-36.2.0';
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
@ -162,48 +168,6 @@ begin
end;
end;
function IsForgeNeeded(): boolean;
begin
Result := True;
if (FileExists(ExpandConstant('{app}')+'\Minecraft Forge Server\forge-'+FORGE_VERSION+'.jar')) then
Result := False;
end;
function IsJavaNeeded(): boolean;
begin
Result := True;
if (FileExists(ExpandConstant('{app}')+'\jre8\bin\java.exe')) then
Result := False;
end;
function OnDownloadMinecraftProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean;
begin
if Progress = ProgressMax then
Log(Format('Successfully downloaded Minecraft additional files to {tmp}: %s', [FileName]));
Result := True;
end;
procedure UnZip(ZipPath, TargetPath: string);
var
Shell: Variant;
ZipFile: Variant;
TargetFolder: Variant;
begin
Shell := CreateOleObject('Shell.Application');
ZipFile := Shell.NameSpace(ZipPath);
if VarIsClear(ZipFile) then
RaiseException(
Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath]));
TargetFolder := Shell.NameSpace(TargetPath);
if VarIsClear(TargetFolder) then
RaiseException(Format('Target path "%s" does not exist', [TargetPath]));
TargetFolder.CopyHere(
ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;
var R : longint;
var lttprom: string;
@ -218,8 +182,6 @@ var SoERomFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
var MinecraftDownloadPage: TDownloadWizardPage;
function GetSNESMD5OfFile(const rom: string): string;
var data: AnsiString;
begin
@ -267,11 +229,6 @@ begin
'.sfc');
end;
procedure AddMinecraftDownloads();
begin
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
end;
procedure AddOoTRomPage();
begin
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
@ -304,33 +261,7 @@ end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
if (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin
MinecraftDownloadPage.Clear;
if(IsForgeNeeded()) then
MinecraftDownloadPage.Add('https://maven.minecraftforge.net/net/minecraftforge/forge/'+FORGE_VERSION+'/forge-'+FORGE_VERSION+'-installer.jar','forge-installer.jar','');
if(IsJavaNeedeD()) then
MinecraftDownloadPage.Add('https://corretto.aws/downloads/latest/amazon-corretto-8-x64-windows-jre.zip','java.zip','');
MinecraftDownloadPage.Show;
try
try
MinecraftDownloadPage.Download;
Result := True;
except
if MinecraftDownloadPage.AbortedByUser then
Log('Aborted by user.')
else
SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
Result := False;
end;
finally
if( isJavaNeeded() ) then
if(ForceDirectories(ExpandConstant('{app}'))) then
UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}'));
MinecraftDownloadPage.Hide;
end;
Result := True;
end
else if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
Result := not (LttPROMFilePage.Values[0] = '')
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
Result := not (SMROMFilePage.Values[0] = '')
@ -421,8 +352,6 @@ begin
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
if Length(soerom) = 0 then
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
AddMinecraftDownloads();
end;

18
kvui.py
View File

@ -8,11 +8,16 @@ os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
os.environ["KIVY_LOG_ENABLE"] = "0"
from kivy.base import Config
Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set('kivy', 'exit_on_escape', '0')
Config.set('graphics', 'multisamples', '0') # multisamples crash old intel drivers
from kivy.app import App
from kivy.core.window import Window
from kivy.core.clipboard import Clipboard
from kivy.core.text.markup import MarkupLabel
from kivy.base import ExceptionHandler, ExceptionManager, Config, Clock
from kivy.base import ExceptionHandler, ExceptionManager, Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.uix.button import Button
@ -207,7 +212,7 @@ class GameManager(App):
# keep track of last used command to autofill on click
self.last_autofillable_command = "hint"
autofillable_commands = ("hint", "getitem")
autofillable_commands = ("hint_location", "hint", "getitem")
original_say = ctx.on_user_say
def intercept_say(text):
@ -365,6 +370,13 @@ class TextManager(GameManager):
base_title = "Archipelago Text Client"
class FF1Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Final Fantasy 1 Client"
class LogtoUI(logging.Handler):
def __init__(self, on_log):
super(LogtoUI, self).__init__(logging.INFO)
@ -424,6 +436,4 @@ class KivyJSONtoTextParser(JSONtoTextParser):
ExceptionManager.add_handler(E())
Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set('kivy', 'exit_on_escape', '0')
Builder.load_file(Utils.local_path("data", "client.kv"))

View File

@ -141,12 +141,12 @@ for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, libfolder, dirs_exist_ok=True)
print('copying', folder, '->', libfolder)
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"]
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
for data in extra_data:
installfile(Path(data))
os.makedirs(buildfolder / "Players", exist_ok=True)
os.makedirs(buildfolder / "Players" / "Templates", exist_ok=True)
from WebHostLib.options import create
create()
from worlds.AutoWorld import AutoWorldRegister
@ -154,7 +154,8 @@ for worldname, worldtype in AutoWorldRegister.world_types.items():
if not worldtype.hidden:
file_name = worldname+".yaml"
shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name),
buildfolder / "Players" / file_name)
buildfolder / "Players" / "Templates" / file_name)
shutil.copyfile("meta.yaml", buildfolder / "Players" / "Templates" / "meta.yaml")
try:
from maseya import z3pr

View File

@ -613,19 +613,24 @@ class TestAdvancements(TestMinecraft):
["You Need a Mint", False, [], ['Progressive Resource Crafting']],
["You Need a Mint", False, [], ['Flint and Steel']],
["You Need a Mint", False, [], ['Progressive Tools']],
["You Need a Mint", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']],
["You Need a Mint", False, [], ['Progressive Armor']],
["You Need a Mint", False, [], ['Progressive Weapons']],
["You Need a Mint", False, [], ['Progressive Armor', 'Shield']],
["You Need a Mint", False, [], ['Brewing']],
["You Need a Mint", False, [], ['Bottles']],
["You Need a Mint", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
["You Need a Mint", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
["You Need a Mint", False, [], ['Archery']],
["You Need a Mint", False, [], ['Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
'Progressive Weapons', 'Progressive Armor', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
'Progressive Weapons', 'Progressive Armor', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Shield', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Shield', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
])
def test_42047(self):
@ -954,7 +959,11 @@ class TestAdvancements(TestMinecraft):
def test_42072(self):
self.run_location_tests([
["A Throwaway Joke", True, []],
["A Throwaway Joke", False, []],
["A Throwaway Joke", False, [], ['Progressive Weapons']],
["A Throwaway Joke", False, [], ['Campfire', 'Progressive Resource Crafting']],
["A Throwaway Joke", True, ['Progressive Weapons', 'Campfire']],
["A Throwaway Joke", True, ['Progressive Weapons', 'Progressive Resource Crafting']],
])
def test_42073(self):
@ -1143,3 +1152,127 @@ class TestAdvancements(TestMinecraft):
["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield']],
])
def test_42092(self):
self.run_location_tests([
["Wax On", False, []],
["Wax On", False, [], ["Progressive Tools"]],
["Wax On", False, [], ["Campfire"]],
["Wax On", False, ["Progressive Resource Crafting"], ["Progressive Resource Crafting"]],
["Wax On", True, ["Progressive Tools", "Progressive Resource Crafting", "Progressive Resource Crafting", "Campfire"]],
])
def test_42093(self):
self.run_location_tests([
["Wax Off", False, []],
["Wax Off", False, [], ["Progressive Tools"]],
["Wax Off", False, [], ["Campfire"]],
["Wax Off", False, ["Progressive Resource Crafting"], ["Progressive Resource Crafting"]],
["Wax Off", True, ["Progressive Tools", "Progressive Resource Crafting", "Progressive Resource Crafting", "Campfire"]],
])
def test_42094(self):
self.run_location_tests([
["The Cutest Predator", False, []],
["The Cutest Predator", False, [], ["Progressive Tools"]],
["The Cutest Predator", False, [], ["Progressive Resource Crafting"]],
["The Cutest Predator", False, [], ["Bucket"]],
["The Cutest Predator", True, ["Progressive Tools", "Progressive Resource Crafting", "Bucket"]],
])
def test_42095(self):
self.run_location_tests([
["The Healing Power of Friendship", False, []],
["The Healing Power of Friendship", False, [], ["Progressive Tools"]],
["The Healing Power of Friendship", False, [], ["Progressive Resource Crafting"]],
["The Healing Power of Friendship", False, [], ["Bucket"]],
["The Healing Power of Friendship", True, ["Progressive Tools", "Progressive Resource Crafting", "Bucket"]],
])
def test_42096(self):
self.run_location_tests([
["Is It a Bird?", False, []],
["Is It a Bird?", False, [], ["Progressive Weapons"]],
["Is It a Bird?", False, [], ["Progressive Tools"]],
["Is It a Bird?", False, [], ["Progressive Resource Crafting"]],
["Is It a Bird?", False, [], ["Spyglass"]],
["Is It a Bird?", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Spyglass"]],
])
def test_42097(self):
self.run_location_tests([
["Is It a Balloon?", False, []],
["Is It a Balloon?", False, [], ['Progressive Resource Crafting']],
["Is It a Balloon?", False, [], ['Flint and Steel']],
["Is It a Balloon?", False, [], ['Progressive Tools']],
["Is It a Balloon?", False, [], ['Progressive Weapons']],
["Is It a Balloon?", False, [], ['Spyglass']],
["Is It a Balloon?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
["Is It a Balloon?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Spyglass']],
["Is It a Balloon?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Spyglass']],
])
def test_42098(self):
self.run_location_tests([
["Is It a Plane?", False, []],
["Is It a Plane?", False, [], ['Progressive Resource Crafting']],
["Is It a Plane?", False, [], ['Flint and Steel']],
["Is It a Plane?", False, [], ['Progressive Tools']],
["Is It a Plane?", False, [], ['Progressive Weapons']],
["Is It a Plane?", False, [], ['Progressive Armor', 'Shield']],
["Is It a Plane?", False, [], ['Brewing']],
["Is It a Plane?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
["Is It a Plane?", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
["Is It a Plane?", False, [], ['Spyglass']],
["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Progressive Armor', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']],
["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Progressive Armor', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']],
["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Shield', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']],
["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Shield', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']],
])
def test_42099(self):
self.run_location_tests([
["Surge Protector", False, []],
["Surge Protector", False, [], ['Channeling Book']],
["Surge Protector", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']],
["Surge Protector", False, [], ['Enchanting']],
["Surge Protector", False, [], ['Progressive Tools']],
["Surge Protector", False, [], ['Progressive Weapons']],
["Surge Protector", True, ['Progressive Weapons', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Enchanting', 'Progressive Resource Crafting', 'Progressive Resource Crafting', 'Channeling Book']],
])
def test_42100(self):
self.run_location_tests([
["Light as a Rabbit", False, []],
["Light as a Rabbit", False, [], ["Progressive Weapons"]],
["Light as a Rabbit", False, [], ["Progressive Tools"]],
["Light as a Rabbit", False, [], ["Progressive Resource Crafting"]],
["Light as a Rabbit", False, [], ["Bucket"]],
["Light as a Rabbit", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Bucket"]],
])
def test_42101(self):
self.run_location_tests([
["Glow and Behold!", False, []],
["Glow and Behold!", False, [], ["Progressive Weapons"]],
["Glow and Behold!", False, [], ["Progressive Resource Crafting", "Campfire"]],
["Glow and Behold!", True, ["Progressive Weapons", "Progressive Resource Crafting"]],
["Glow and Behold!", True, ["Progressive Weapons", "Campfire"]],
])
def test_42102(self):
self.run_location_tests([
["Whatever Floats Your Goat!", False, []],
["Whatever Floats Your Goat!", False, [], ["Progressive Weapons"]],
["Whatever Floats Your Goat!", False, [], ["Progressive Resource Crafting", "Campfire"]],
["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Progressive Resource Crafting"]],
["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Campfire"]],
])

View File

@ -4,7 +4,7 @@ from BaseClasses import MultiWorld
from worlds import AutoWorld
from worlds.minecraft import MinecraftWorld
from worlds.minecraft.Items import MinecraftItem, item_table
from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty, BeeTraps
from worlds.minecraft.Options import *
from Options import Toggle, Range
# Converts the name of an item into an item object
@ -30,16 +30,17 @@ class TestMinecraft(TestBase):
self.world = MultiWorld(1)
self.world.game[1] = "Minecraft"
self.world.worlds[1] = MinecraftWorld(self.world, 1)
exclusion_pools = ['hard', 'insane', 'postgame']
exclusion_pools = ['hard', 'unreasonable', 'postgame']
for pool in exclusion_pools:
setattr(self.world, f"include_{pool}_advancements", [False, False])
setattr(self.world, f"include_{pool}_advancements", {1: False})
setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)})
setattr(self.world, "shuffle_structures", {1: Toggle(False)})
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
setattr(self.world, "egg_shards_required", {1: EggShardsRequired(0)})
setattr(self.world, "egg_shards_available", {1: EggShardsAvailable(0)})
setattr(self.world, "required_bosses", {1: BossGoal(1)}) # ender dragon
setattr(self.world, "shuffle_structures", {1: ShuffleStructures(False)})
setattr(self.world, "bee_traps", {1: BeeTraps(0)})
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
setattr(self.world, "structure_compasses", {1: Toggle(False)})
setattr(self.world, "egg_shards_required", {1: Range(0)})
setattr(self.world, "egg_shards_available", {1: Range(0)})
AutoWorld.call_single(self.world, "create_regions", 1)
AutoWorld.call_single(self.world, "generate_basic", 1)
AutoWorld.call_single(self.world, "set_rules", 1)

View File

@ -1796,6 +1796,7 @@ def link_inverted_entrances(world, player):
if world.get_entrance('Inverted Ganons Tower', player).connected_region.name != 'Ganons Tower (Entrance)':
world.ganonstower_vanilla[player] = False
def connect_simple(world, exitname, regionname, player):
world.get_entrance(exitname, player).connect(world.get_region(regionname, player))
@ -1820,6 +1821,7 @@ def connect_entrance(world, entrancename: str, exitname: str, player: int):
entrance.connect(region, addresses, target)
world.spoiler.set_entrance(entrance.name, exit.name if exit is not None else region.name, 'entrance', player)
def connect_exit(world, exitname, entrancename, player):
entrance = world.get_entrance(entrancename, player)
exit = world.get_entrance(exitname, player)

View File

@ -2287,7 +2287,7 @@ def write_strings(rom, world, player):
if hint_count:
locations = world.find_items_in_locations(set(items_to_hint), player)
local_random.shuffle(locations)
for x in range(hint_count):
for x in range(min(hint_count, len(locations))):
this_location = locations.pop()
this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.'
tt[hint_locations.pop(0)] = this_hint

View File

@ -109,9 +109,12 @@ def generate_mod(world, output_directory: str):
progressive_technology_table.values()},
"custom_recipes": world.custom_recipes,
"max_science_pack": multiworld.max_science_pack[player].value,
"liquids": liquids}
"liquids": liquids,
"goal": multiworld.goal[player].value}
for factorio_option in Options.factorio_options:
if factorio_option in ["free_sample_blacklist", "free_sample_whitelist"]:
continue
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
@ -120,6 +123,9 @@ def generate_mod(world, output_directory: str):
if getattr(multiworld, "satellite")[player].value == Options.Satellite.option_randomize_recipe:
template_data["free_sample_blacklist"]["satellite"] = 1
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
control_code = control_template.render(**template_data)
data_template_code = data_template.render(**template_data)
data_final_fixes_code = data_final_template.render(**template_data)

View File

@ -1,7 +1,7 @@
from __future__ import annotations
import typing
from Options import Choice, OptionDict, ItemDict, Option, DefaultOnToggle, Range, DeathLink
from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink
from schema import Schema, Optional, And, Or
# schema helpers
@ -33,6 +33,14 @@ class MaxSciencePack(Choice):
return self.get_ordered_science_packs()[self.value].replace("_", "-")
class Goal(Choice):
"""Goal required to complete the game."""
displayname = "Goal"
option_rocket = 0
option_satellite = 1
default = 0
class TechCost(Choice):
"""How expensive are the technologies."""
displayname = "Technology Cost Scale"
@ -86,6 +94,8 @@ class TechTreeLayout(Choice):
option_small_funnels = 7
option_medium_funnels = 8
option_large_funnels = 9
option_trees = 10
option_choices = 11
default = 0
@ -146,6 +156,15 @@ class FactorioStartItems(ItemDict):
default = {"burner-mining-drill": 19, "stone-furnace": 19}
class FactorioFreeSampleBlacklist(OptionSet):
displayname = "Free Sample Blacklist"
class FactorioFreeSampleWhitelist(OptionSet):
"""overrides any free sample blacklist present. This may ruin the balance of the mod, be forewarned."""
displayname = "Free Sample Whitelist"
class TrapCount(Range):
range_end = 4
@ -306,6 +325,7 @@ class ImportedBlueprint(DefaultOnToggle):
factorio_options: typing.Dict[str, type(Option)] = {
"max_science_pack": MaxSciencePack,
"goal": Goal,
"tech_tree_layout": TechTreeLayout,
"tech_cost": TechCost,
"silo": Silo,
@ -313,6 +333,8 @@ factorio_options: typing.Dict[str, type(Option)] = {
"free_samples": FreeSamples,
"tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems,
"free_sample_blacklist": FactorioFreeSampleBlacklist,
"free_sample_whitelist": FactorioFreeSampleWhitelist,
"recipe_time": RecipeTime,
"recipe_ingredients": RecipeIngredients,
"imported_blueprints": ImportedBlueprint,

View File

@ -1,4 +1,5 @@
from typing import Dict, List, Set
from collections import deque
from worlds.factorio.Options import TechTreeLayout
@ -21,7 +22,9 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
tech_names.sort()
world.random.shuffle(tech_names)
if layout == TechTreeLayout.option_small_diamonds:
if layout == TechTreeLayout.option_single:
pass
elif layout == TechTreeLayout.option_small_diamonds:
slice_size = 4
while len(tech_names) > slice_size:
slice = tech_names[:slice_size]
@ -189,6 +192,55 @@ def get_shapes(factorio_world) -> Dict[str, List[str]]:
prerequisites.setdefault(tech_name, set()).update(previous_slice[i:i+2])
previous_slice = slice
layer_size -= 1
elif layout == TechTreeLayout.option_trees:
# 0 |
# 1 2 |
# 3 |
# 4 5 6 7 |
# 8 |
# 9 10 11 12 13 14 |
# 15 |
# 16 |
slice_size = 17
while len(tech_names) > slice_size:
slice = tech_names[:slice_size]
tech_names = tech_names[slice_size:]
slice.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
prerequisites[slice[1]] = {slice[0]}
prerequisites[slice[2]] = {slice[0]}
prerequisites[slice[3]] = {slice[1], slice[2]}
prerequisites[slice[4]] = {slice[3]}
prerequisites[slice[5]] = {slice[3]}
prerequisites[slice[6]] = {slice[3]}
prerequisites[slice[7]] = {slice[3]}
prerequisites[slice[8]] = {slice[4], slice[5], slice[6], slice[7]}
prerequisites[slice[9]] = {slice[8]}
prerequisites[slice[10]] = {slice[8]}
prerequisites[slice[11]] = {slice[8]}
prerequisites[slice[12]] = {slice[8]}
prerequisites[slice[13]] = {slice[8]}
prerequisites[slice[14]] = {slice[8]}
prerequisites[slice[15]] = {slice[9], slice[10], slice[11], slice[12], slice[13], slice[14]}
prerequisites[slice[16]] = {slice[15]}
elif layout == TechTreeLayout.option_choices:
tech_names.sort(key=lambda tech_name: len(custom_technologies[tech_name].get_prior_technologies()))
current_choices = deque([tech_names[0]])
tech_names = tech_names[1:]
while len(tech_names) > 1:
source = current_choices.pop()
choices = tech_names[:2]
tech_names = tech_names[2:]
for choice in choices:
prerequisites[choice] = {source}
current_choices.extendleft(choices)
else:
raise NotImplementedError(f"Layout {layout} is not implemented.")
world.tech_tree_layout_prerequisites[player] = prerequisites
return prerequisites

View File

@ -11,7 +11,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
liquids
from .Shapes import get_shapes
from .Mod import generate_mod
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
import logging
@ -142,12 +142,14 @@ class Factorio(World):
Rules.add_rule(location, lambda state,
locations=locations: all(state.can_reach(loc) for loc in locations))
silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \
else self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
silo_recipe = None
if self.world.silo[self.player] == Silo.option_spawn:
silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \
else next(iter(all_product_sources.get("rocket-silo")))
part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None if self.world.max_science_pack[self.player].value != MaxSciencePack.option_space_science_pack \
else self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
satellite_recipe = None
if self.world.goal[self.player] == Goal.option_satellite:
satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \
else next(iter(all_product_sources.get("satellite")))
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe)
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
@ -171,7 +173,7 @@ class Factorio(World):
return super(Factorio, self).collect_item(state, item, remove)
def get_required_client_version(self) -> tuple:
return max((0, 1, 6), super(Factorio, self).get_required_client_version())
return max((0, 2, 1), super(Factorio, self).get_required_client_version())
options = factorio_options
@ -339,7 +341,7 @@ class Factorio(World):
needed_recipes = self.world.max_science_pack[self.player].get_allowed_packs() | {"rocket-part"}
if self.world.silo[self.player] != Silo.option_spawn:
needed_recipes |= {"rocket-silo"}
if self.world.max_science_pack[self.player].value == MaxSciencePack.option_space_science_pack:
if self.world.goal[self.player].value == Goal.option_satellite:
needed_recipes |= {"satellite"}
for recipe in needed_recipes:

View File

@ -9,6 +9,7 @@ SEED_NAME = "{{ seed_name }}"
FREE_SAMPLE_BLACKLIST = {{ dict_to_lua(free_sample_blacklist) }}
TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100
MAX_SCIENCE_PACK = {{ max_science_pack }}
GOAL = {{ goal }}
ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}"
if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
@ -136,7 +137,7 @@ script.on_event(defines.events.on_player_removed, on_player_removed)
function on_rocket_launched(event)
if event.rocket and event.rocket.valid and global.forcedata[event.rocket.force.name]['victory'] == 0 then
if event.rocket.get_item_count("satellite") > 0 or MAX_SCIENCE_PACK < 6 then
if event.rocket.get_item_count("satellite") > 0 or GOAL == 0 then
global.forcedata[event.rocket.force.name]['victory'] = 1
dumpInfo(event.rocket.force)
game.set_game_state

View File

@ -25,6 +25,26 @@ template_tech.upgrade = false
template_tech.effects = {}
template_tech.prerequisites = {}
{%- if max_science_pack < 6 %}
technologies["space-science-pack"].effects = {}
{%- if max_science_pack == 0 %}
table.insert (technologies["automation"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 1 %}
table.insert (technologies["logistic-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 2 %}
table.insert (technologies["military-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 3 %}
table.insert (technologies["chemical-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 4 %}
table.insert (technologies["production-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 5 %}
table.insert (technologies["utility-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{% endif %}
{% endif %}
{%- if silo == 2 %}
data.raw["recipe"]["rocket-silo"].enabled = true
{% endif %}
function prep_copy(new_copy, old_tech)
old_tech.hidden = true
local ingredient_filter = allowed_ingredients[old_tech.name]

View File

@ -56,10 +56,12 @@ item_table = {
"Structure Compass (End City)": ItemData(45041, True),
"Shulker Box": ItemData(45042, False),
"Dragon Egg Shard": ItemData(45043, True),
"Spyglass": ItemData(45044, True),
"Bee Trap (Minecraft)": ItemData(45100, False),
"Blaze Rods": ItemData(None, True),
"Victory": ItemData(None, True)
"Defeat Ender Dragon": ItemData(None, True),
"Defeat Wither": ItemData(None, True),
}
# 33 required items
@ -87,6 +89,7 @@ required_items = {
"Infinity Book": 1,
"3 Ender Pearls": 4,
"Saddle": 1,
"Spyglass": 1,
}
junk_weights = {

View File

@ -108,9 +108,21 @@ advancement_table = {
"Overkill": AdvData(42089, 'Nether Fortress'),
"Librarian": AdvData(42090, 'Overworld'),
"Overpowered": AdvData(42091, 'Bastion Remnant'),
"Wax On": AdvData(42092, 'Overworld'),
"Wax Off": AdvData(42093, 'Overworld'),
"The Cutest Predator": AdvData(42094, 'Overworld'),
"The Healing Power of Friendship": AdvData(42095, 'Overworld'),
"Is It a Bird?": AdvData(42096, 'Overworld'),
"Is It a Balloon?": AdvData(42097, 'The Nether'),
"Is It a Plane?": AdvData(42098, 'The End'),
"Surge Protector": AdvData(42099, 'Overworld'),
"Light as a Rabbit": AdvData(42100, 'Overworld'),
"Glow and Behold!": AdvData(42101, 'Overworld'),
"Whatever Floats Your Goat!": AdvData(42102, 'Overworld'),
"Blaze Spawner": AdvData(None, 'Nether Fortress'),
"Ender Dragon": AdvData(None, 'The End')
"Ender Dragon": AdvData(None, 'The End'),
"Wither": AdvData(None, 'Nether Fortress'),
}
exclusion_table = {
@ -126,23 +138,39 @@ exclusion_table = {
"Uneasy Alliance",
"Cover Me in Debris",
"A Complete Catalogue",
"Surge Protector",
"Light as a Rabbit", # will be normal in 1.18
},
"insane": {
"unreasonable": {
"How Did We Get Here?",
"Adventuring Time",
},
"postgame": {
}
def get_postgame_advancements(required_bosses):
postgame_advancements = {
"ender_dragon": {
"Free the End",
"The Next Generation",
"The End... Again...",
"You Need a Mint",
"Monsters Hunted",
"Is It a Plane?",
},
"wither": {
"Withering Heights",
"Bring Home the Beacon",
"Beaconator",
"A Furious Cocktail",
"How Did We Get Here?",
"Monsters Hunted",
}
}
events_table = {
"Ender Dragon": "Victory"
}
lookup_id_to_name: typing.Dict[int, str] = {loc_data.id: loc_name for loc_name, loc_data in advancement_table.items() if
loc_data.id}
advancements = set()
if required_bosses in {"ender_dragon", "both"}:
advancements.update(postgame_advancements["ender_dragon"])
if required_bosses in {"wither", "both"}:
advancements.update(postgame_advancements["wither"])
return advancements

View File

@ -1,37 +1,51 @@
import typing
from Options import Choice, Option, Toggle, Range
from Options import Choice, Option, Toggle, Range, OptionList, DeathLink
class AdvancementGoal(Range):
"""Number of advancements required to spawn the Ender Dragon."""
"""Number of advancements required to spawn bosses."""
displayname = "Advancement Goal"
range_start = 0
range_end = 87
default = 50
range_end = 92
default = 40
class EggShardsRequired(Range):
"""Number of dragon egg shards to collect before the Ender Dragon will spawn."""
"""Number of dragon egg shards to collect to spawn bosses."""
displayname = "Egg Shards Required"
range_start = 0
range_end = 30
range_end = 40
default = 0
class EggShardsAvailable(Range):
"""Number of dragon egg shards available to collect."""
displayname = "Egg Shards Available"
range_start = 0
range_end = 30
range_end = 40
default = 0
class BossGoal(Choice):
"""Bosses which must be defeated to finish the game."""
displayname = "Required Bosses"
option_none = 0
option_ender_dragon = 1
option_wither = 2
option_both = 3
default = 1
class ShuffleStructures(Toggle):
"""Enables shuffling of villages, outposts, fortresses, bastions, and end cities."""
displayname = "Shuffle Structures"
default = 1
class StructureCompasses(Toggle):
"""Adds structure compasses to the item pool, which point to the nearest indicated structure."""
displayname = "Structure Compasses"
default = 1
class BeeTraps(Range):
@ -39,6 +53,7 @@ class BeeTraps(Range):
displayname = "Bee Trap Percentage"
range_start = 0
range_end = 100
default = 0
class CombatDifficulty(Choice):
@ -53,33 +68,45 @@ class CombatDifficulty(Choice):
class HardAdvancements(Toggle):
"""Enables certain RNG-reliant or tedious advancements."""
displayname = "Include Hard Advancements"
default = 0
class InsaneAdvancements(Toggle):
class UnreasonableAdvancements(Toggle):
"""Enables the extremely difficult advancements "How Did We Get Here?" and "Adventuring Time.\""""
displayname = "Include Insane Advancements"
displayname = "Include Unreasonable Advancements"
default = 0
class PostgameAdvancements(Toggle):
"""Enables advancements that require spawning and defeating the Ender Dragon."""
"""Enables advancements that require spawning and defeating the required bosses."""
displayname = "Include Postgame Advancements"
default = 0
class SendDefeatedMobs(Toggle):
"""Send killed mobs to other Minecraft worlds which have this option enabled."""
displayname = "Send Defeated Mobs"
default = 0
class StartingItems(OptionList):
"""Start with these items. Each entry should be of this format: {item: "item_name", amount: #, nbt: "nbt_string"}"""
displayname = "Starting Items"
minecraft_options: typing.Dict[str, type(Option)] = {
"advancement_goal": AdvancementGoal,
"egg_shards_required": EggShardsRequired,
"egg_shards_available": EggShardsAvailable,
"required_bosses": BossGoal,
"shuffle_structures": ShuffleStructures,
"structure_compasses": StructureCompasses,
"bee_traps": BeeTraps,
"combat_difficulty": CombatDifficulty,
"include_hard_advancements": HardAdvancements,
"include_insane_advancements": InsaneAdvancements,
"include_unreasonable_advancements": UnreasonableAdvancements,
"include_postgame_advancements": PostgameAdvancements,
"send_defeated_mobs": SendDefeatedMobs,
"starting_items": StartingItems,
"death_link": DeathLink,
}

View File

@ -1,5 +1,5 @@
from ..generic.Rules import set_rule
from .Locations import exclusion_table, events_table
from ..generic.Rules import set_rule, add_rule
from .Locations import exclusion_table, get_postgame_advancements
from BaseClasses import MultiWorld
from ..AutoWorld import LogicMixin
@ -9,6 +9,9 @@ class MinecraftLogic(LogicMixin):
def _mc_has_iron_ingots(self, player: int):
return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player)
def _mc_has_copper_ingots(self, player: int):
return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player)
def _mc_has_gold_ingots(self, player: int):
return self.has('Progressive Resource Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player))
@ -21,6 +24,9 @@ class MinecraftLogic(LogicMixin):
def _mc_has_bottle(self, player: int):
return self.has('Bottles', player) and self.has('Progressive Resource Crafting', player)
def _mc_has_spyglass(self, player: int):
return self._mc_has_copper_ingots(player) and self.has('Spyglass', player) and self._mc_can_adventure(player)
def _mc_can_enchant(self, player: int):
return self.has('Enchanting', player) and self._mc_has_diamond_pickaxe(player) # mine obsidian and lapis
@ -81,48 +87,32 @@ class MinecraftLogic(LogicMixin):
return self._mc_fortress_loot(player) and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player))
return self._mc_fortress_loot(player) and normal_kill
def _mc_can_respawn_ender_dragon(self, player: int):
return self.can_reach('The Nether', 'Region', player) and self.can_reach('The End', 'Region', player) and \
self.has('Progressive Resource Crafting', player) # smelt sand into glass
def _mc_can_kill_ender_dragon(self, player: int):
# Since it is possible to kill the dragon without getting any of the advancements related to it, we need to require that it can be respawned.
respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Progressive Resource Crafting', player)
if self._mc_combat_difficulty(player) == 'easy':
return respawn_dragon and self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \
return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \
self.has('Archery', player) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player)
if self._mc_combat_difficulty(player) == 'hard':
return respawn_dragon and ((self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player)))
return respawn_dragon and self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
def _mc_has_structure_compass(self, entrance_name: str, player: int):
if not self.world.structure_compasses[player]:
return True
return self.has(f"Structure Compass ({self.world.get_entrance(entrance_name, player).connected_region.name})", player)
def set_rules(world: MultiWorld, player: int):
def reachable_locations(state):
postgame_advancements = exclusion_table['postgame'].copy()
for event in events_table.keys():
postgame_advancements.add(event)
return [location for location in world.get_locations() if
location.player == player and
location.name not in postgame_advancements and
location.can_reach(state)]
# Sets rules on entrances and advancements that are always applied
def set_advancement_rules(world: MultiWorld, player: int):
# Retrieves the appropriate structure compass for the given entrance
def get_struct_compass(entrance_name):
struct = world.get_entrance(entrance_name, player).connected_region.name
return f"Structure Compass ({struct})"
# 92 total advancements. Goal is to complete X advancements and then Free the End.
# There are 5 advancements which cannot be included for dragon spawning (4 postgame, Free the End)
# Hence the true maximum is (92 - 5) = 87
goal = world.advancement_goal[player]
egg_shards = min(world.egg_shards_required[player], world.egg_shards_available[player])
can_complete = lambda state: len(reachable_locations(state)) >= goal and state.has("Dragon Egg Shard", player, egg_shards) and state.can_reach('The End', 'Region', player) and state._mc_can_kill_ender_dragon(player)
if world.logic[player] != 'nologic':
world.completion_condition[player] = lambda state: state.has('Victory', player)
set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and
(state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and
state._mc_has_iron_ingots(player))
@ -133,7 +123,8 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_entrance("Nether Structure 2", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Nether Structure 2", player))
set_rule(world.get_entrance("The End Structure", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("The End Structure", player))
set_rule(world.get_location("Ender Dragon", player), lambda state: can_complete(state))
set_rule(world.get_location("Ender Dragon", player), lambda state: state._mc_can_kill_ender_dragon(player))
set_rule(world.get_location("Wither", player), lambda state: state._mc_can_kill_wither(player))
set_rule(world.get_location("Blaze Spawner", player), lambda state: state._mc_fortress_loot(player))
set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state._mc_can_piglin_trade(player))
@ -142,7 +133,7 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and \
((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) # need villager into the overworld for lightning strike
set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player))
set_rule(world.get_location("Free the End", player), lambda state: can_complete(state))
set_rule(world.get_location("Free the End", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player))
set_rule(world.get_location("A Furious Cocktail", player), lambda state: state._mc_can_brew_potions(player) and
state.has("Fishing Rod", player) and # Water Breathing
state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets
@ -154,7 +145,7 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state._mc_has_iron_ingots(player))
set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player))
set_rule(world.get_location("Local Brewery", player), lambda state: state._mc_can_brew_potions(player))
set_rule(world.get_location("The Next Generation", player), lambda state: can_complete(state))
set_rule(world.get_location("The Next Generation", player), lambda state: state._mc_can_kill_ender_dragon(player))
set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player))
set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: True)
set_rule(world.get_location("This Boat Has Legs", player), lambda state: (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and
@ -188,7 +179,7 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player))
set_rule(world.get_location("Arbalistic", player), lambda state: state._mc_craft_crossbow(player) and state.has("Piercing IV Book", player) and
state._mc_can_use_anvil(player) and state._mc_can_enchant(player))
set_rule(world.get_location("The End... Again...", player), lambda state: can_complete(state))
set_rule(world.get_location("The End... Again...", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player))
set_rule(world.get_location("Acquire Hardware", player), lambda state: state._mc_has_iron_ingots(player))
set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state._mc_can_piglin_trade(player) and state.has("Progressive Resource Crafting", player, 2))
set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player))
@ -196,9 +187,10 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Hired Help", player), lambda state: state.has("Progressive Resource Crafting", player, 2) and state._mc_has_iron_ingots(player))
set_rule(world.get_location("Return to Sender", player), lambda state: True)
set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player))
set_rule(world.get_location("You Need a Mint", player), lambda state: can_complete(state) and state._mc_has_bottle(player))
set_rule(world.get_location("You Need a Mint", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_has_bottle(player))
set_rule(world.get_location("Adventure", player), lambda state: True)
set_rule(world.get_location("Monsters Hunted", player), lambda state: can_complete(state) and state._mc_can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing
set_rule(world.get_location("Monsters Hunted", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player) and
state._mc_can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing
set_rule(world.get_location("Enchanter", player), lambda state: state._mc_can_enchant(player))
set_rule(world.get_location("Voluntary Exile", player), lambda state: state._mc_basic_combat(player))
set_rule(world.get_location("Eye Spy", player), lambda state: state._mc_enter_stronghold(player))
@ -224,7 +216,7 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Uneasy Alliance", player), lambda state: state._mc_has_diamond_pickaxe(player) and state.has('Fishing Rod', player))
set_rule(world.get_location("Diamonds!", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player))
set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything
set_rule(world.get_location("A Throwaway Joke", player), lambda state: True) # kill drowned
set_rule(world.get_location("A Throwaway Joke", player), lambda state: state._mc_can_adventure(player)) # kill drowned
set_rule(world.get_location("Minecraft", player), lambda state: True)
set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player))
set_rule(world.get_location("Ol' Betsy", player), lambda state: state._mc_craft_crossbow(player))
@ -249,3 +241,42 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player))
set_rule(world.get_location("Overpowered", player), lambda state: state._mc_has_iron_ingots(player) and
state.has('Progressive Tools', player, 2) and state._mc_basic_combat(player)) # mine gold blocks w/ iron pick
set_rule(world.get_location("Wax On", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and
state.has('Progressive Resource Crafting', player, 2))
set_rule(world.get_location("Wax Off", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and
state.has('Progressive Resource Crafting', player, 2))
set_rule(world.get_location("The Cutest Predator", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player))
set_rule(world.get_location("The Healing Power of Friendship", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player))
set_rule(world.get_location("Is It a Bird?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_adventure(player))
set_rule(world.get_location("Is It a Balloon?", player), lambda state: state._mc_has_spyglass(player))
set_rule(world.get_location("Is It a Plane?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_respawn_ender_dragon(player))
set_rule(world.get_location("Surge Protector", player), lambda state: state.has("Channeling Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and \
((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player)))
set_rule(world.get_location("Light as a Rabbit", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has('Bucket', player))
set_rule(world.get_location("Glow and Behold!", player), lambda state: state._mc_can_adventure(player))
set_rule(world.get_location("Whatever Floats Your Goat!", player), lambda state: state._mc_can_adventure(player))
# Sets rules on completion condition and postgame advancements
def set_completion_rules(world: MultiWorld, player: int):
def reachable_locations(state):
postgame_advancements = get_postgame_advancements(world.required_bosses[player].current_key)
return [location for location in world.get_locations() if
location.player == player and
location.name not in postgame_advancements and
location.address != None and
location.can_reach(state)]
def defeated_required_bosses(state):
return (world.required_bosses[player].current_key not in {"ender_dragon", "both"} or state.has("Defeat Ender Dragon", player)) and \
(world.required_bosses[player].current_key not in {"wither", "both"} or state.has("Defeat Wither", player))
# 103 total advancements. Goal is to complete X advancements and then defeat the dragon.
# There are 11 possible postgame advancements; 5 for dragon, 5 for wither, 1 shared between them
# Hence the max for completion is 92
egg_shards = min(world.egg_shards_required[player], world.egg_shards_available[player])
completion_requirements = lambda state: len(reachable_locations(state)) >= world.advancement_goal[player] and \
state.has("Dragon Egg Shard", player, egg_shards)
world.completion_condition[player] = lambda state: completion_requirements(state) and defeated_required_bosses(state)
# Set rules on postgame advancements
for adv_name in get_postgame_advancements(world.required_bosses[player].current_key):
add_rule(world.get_location(adv_name, player), completion_requirements)

View File

@ -4,16 +4,17 @@ from base64 import b64encode, b64decode
from math import ceil
from .Items import MinecraftItem, item_table, required_items, junk_weights
from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table
from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, get_postgame_advancements
from .Regions import mc_regions, link_minecraft_structures, default_connections
from .Rules import set_rules
from .Rules import set_advancement_rules, set_completion_rules
from worlds.generic.Rules import exclusion_rules
from BaseClasses import Region, Entrance, Item
from .Options import minecraft_options
from ..AutoWorld import World
client_version = 6
client_version = 7
minecraft_version = "1.17.1"
class MinecraftWorld(World):
"""
@ -29,7 +30,7 @@ class MinecraftWorld(World):
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {name: data.id for name, data in advancement_table.items()}
data_version = 3
data_version = 4
def _get_mc_data(self):
exits = [connection[0] for connection in default_connections]
@ -39,12 +40,16 @@ class MinecraftWorld(World):
'player_name': self.world.get_player_name(self.player),
'player_id': self.player,
'client_version': client_version,
'minecraft_version': minecraft_version,
'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits},
'advancement_goal': self.world.advancement_goal[self.player],
'egg_shards_required': min(self.world.egg_shards_required[self.player], self.world.egg_shards_available[self.player]),
'egg_shards_available': self.world.egg_shards_available[self.player],
'required_bosses': self.world.required_bosses[self.player].current_key,
'MC35': bool(self.world.send_defeated_mobs[self.player]),
'race': self.world.is_race
'death_link': bool(self.world.death_link[self.player]),
'starting_items': str(self.world.starting_items[self.player].value),
'race': self.world.is_race,
}
def generate_basic(self):
@ -72,20 +77,24 @@ class MinecraftWorld(World):
# Choose locations to automatically exclude based on settings
exclusion_pool = set()
exclusion_types = ['hard', 'insane', 'postgame']
exclusion_types = ['hard', 'unreasonable']
for key in exclusion_types:
if not getattr(self.world, f"include_{key}_advancements")[self.player]:
exclusion_pool.update(exclusion_table[key])
# For postgame advancements, check with the boss goal
exclusion_pool.update(get_postgame_advancements(self.world.required_bosses[self.player].current_key))
exclusion_rules(self.world, self.player, exclusion_pool)
# Prefill event locations with their events
self.world.get_location("Blaze Spawner", self.player).place_locked_item(self.create_item("Blaze Rods"))
self.world.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Victory"))
self.world.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Defeat Ender Dragon"))
self.world.get_location("Wither", self.player).place_locked_item(self.create_item("Defeat Wither"))
self.world.itempool += itempool
def set_rules(self):
set_rules(self.world, self.player)
set_advancement_rules(self.world, self.player)
set_completion_rules(self.world, self.player)
def create_regions(self):
def MCRegion(region_name: str, exits=[]):
@ -110,6 +119,7 @@ class MinecraftWorld(World):
slot_data = self._get_mc_data()
for option_name in minecraft_options:
option = getattr(self.world, option_name)[self.player]
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
slot_data[option_name] = int(option.value)
return slot_data

View File

@ -282,22 +282,22 @@ class Rom(BigStream):
def compress_rom_file(input_file, output_file):
subcall = []
compressor_path = data_path("Compress")
if platform.system() == 'Windows':
compressor_path += "\\Compress.exe"
executable_path = "Compress.exe"
elif platform.system() == 'Linux':
if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64':
compressor_path += "/Compress_ARM64"
executable_path = "Compress_ARM64"
else:
compressor_path += "/Compress"
executable_path = "Compress"
elif platform.system() == 'Darwin':
compressor_path += "/Compress.out"
executable_path = "Compress.out"
else:
raise RuntimeError('Unsupported operating system for compression.')
compressor_path = os.path.join(compressor_path, executable_path)
if not os.path.exists(compressor_path):
raise RuntimeError(f'Compressor does not exist! Please place it at {compressor_path}.')
process = subprocess.call([compressor_path, input_file, output_file], **subprocess_args(include_stdout=False))
import logging
logging.info(subprocess.check_output([compressor_path, input_file, output_file],
**subprocess_args(include_stdout=False)))

View File

@ -1,5 +1,5 @@
import typing
from Options import Choice, Range, OptionDict, OptionList, Option, Toggle, DefaultOnToggle, DeathLink
from Options import Choice, Range, OptionDict, OptionList, Option, Toggle, DefaultOnToggle
class StartItemsRemovesFromPool(Toggle):
displayname = "StartItems Removes From Item Pool"
@ -40,6 +40,16 @@ class StartLocation(Choice):
option_Golden_Four = 14
default = 1
class DeathLink(Choice):
"""When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you."""
displayname = "Death Link"
option_disable = 0
option_enable = 1
option_enable_survive = 3
alias_false = 0
alias_true = 1
default = 0
class MaxDifficulty(Choice):
displayname = "Maximum Difficulty"
option_easy = 0
@ -57,9 +67,6 @@ class MorphPlacement(Choice):
option_normal = 1
default = 0
class SuitsRestriction(DefaultOnToggle):
displayname = "Suits Restriction"
class StrictMinors(Toggle):
displayname = "Strict Minors"
@ -117,12 +124,15 @@ class BossRandomization(Toggle):
displayname = "Boss Randomization"
class FunCombat(Toggle):
"""if used, might force 'items' accessibility"""
displayname = "Fun Combat"
class FunMovement(Toggle):
"""if used, might force 'items' accessibility"""
displayname = "Fun Movement"
class FunSuits(Toggle):
"""if used, might force 'items' accessibility"""
displayname = "Fun Suits"
class LayoutPatches(DefaultOnToggle):
@ -197,7 +207,7 @@ sm_options: typing.Dict[str, type(Option)] = {
#"progression_speed": "medium",
#"progression_difficulty": "normal",
"morph_placement": MorphPlacement,
"suits_restriction": SuitsRestriction,
#"suits_restriction": SuitsRestriction,
#"hide_items": "off",
"strict_minors": StrictMinors,
"missile_qty": MissileQty,

View File

@ -2,7 +2,7 @@ import logging
import copy
import os
import threading
from typing import Set
from typing import Set, List
logger = logging.getLogger("Super Metroid")
@ -59,7 +59,7 @@ class SMWorld(World):
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")}
self.smbm = {player: SMBoolManager(player, parent.state.smbm[player].maxDiff, parent.state.smbm[player].onlyBossLeft) for player in parent.get_game_players("Super Metroid")}
orig_init(self, parent)
@ -89,6 +89,10 @@ class SMWorld(World):
if (self.variaRando.args.morphPlacement == "early"):
self.world.local_items[self.player].value.add('Morph')
if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0):
self.world.accessibility[self.player] = self.world.accessibility[self.player].from_text("items")
logger.warning(f"accessibility forced to 'items' for player {self.world.get_player_name(self.player)} because of 'fun' settings")
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]
@ -157,6 +161,10 @@ class SMWorld(World):
create_locations(self, self.player)
create_regions(self, self.world, self.player)
def get_required_client_version(self):
# changes to client DeathLink handling for 0.2.1
return max(super(SMWorld, self).get_required_client_version(), (0, 2, 1))
def getWord(self, w):
return (w & 0x00FF, (w & 0xFF00) >> 8)
@ -274,7 +282,7 @@ class SMWorld(World):
openTourianGreyDoors = {0x07C823 + 5: [0x0C], 0x07C831 + 5: [0x0C]}
deathLink = {0x277f04: [int(self.world.death_link[self.player])]}
deathLink = {0x277f04: [self.world.death_link[self.player].value]}
playerNames = {}
playerNameIDMap = {}
@ -476,17 +484,6 @@ class SMWorld(World):
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):
@ -494,6 +491,22 @@ class SMWorld(World):
progitempool.sort(
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)
def post_fill(self):
new_state = CollectionState(self.world)
progitempool = []
for item in self.world.itempool:
if item.player == self.player and item.advancement:
progitempool.append(item)
for item in progitempool:
new_state.collect(item, True)
bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain']
for bossLoc in bossesLoc:
if (not self.world.get_location(bossLoc, self.player).can_reach(new_state)):
self.world.state.smbm[self.player].onlyBossLeft = True
break
def create_locations(self, player: int):
for name, id in locations_lookup_name_to_id.items():
self.locations[name] = SMLocation(player, name, id)

View File

@ -134,9 +134,9 @@ class AccessGraph(object):
def printGraph(self):
if self.log.getEffectiveLevel() == logging.DEBUG:
self.log("Area graph:")
self.log.debug("Area graph:")
for s, d in self.InterAreaTransitions:
self.log("{} -> {}".format(s.Name, d.Name))
self.log.debug("{} -> {}".format(s.Name, d.Name))
def addAccessPoint(self, ap):
ap.distance = 0

View File

@ -566,6 +566,8 @@ class Helpers(object):
# print('RIDLEY', ammoMargin, secs)
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
Settings.bossesDifficulty['Ridley'])
if (sm.onlyBossLeft):
diff = 1
if diff < 0:
return smboolFalse
else:
@ -580,6 +582,8 @@ class Helpers(object):
#print('KRAID True ', ammoMargin, secs)
(diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
Settings.bossesDifficulty['Kraid'])
if (sm.onlyBossLeft):
diff = 1
if diff < 0:
return smboolFalse
@ -621,6 +625,8 @@ class Helpers(object):
if sm.haveItem('Gravity') and sm.haveItem('ScrewAttack'):
fight.difficulty /= Settings.algoSettings['draygonScrewBonus']
fight.difficulty = self.adjustHealthDropDiff(fight.difficulty)
if (sm.onlyBossLeft):
fight.difficulty = 1
else:
fight = smboolFalse
# for grapple kill considers energy drained by wall socket + 2 spankings by Dray
@ -661,6 +667,8 @@ class Helpers(object):
elif not hasCharge and sm.itemCount('Missile') <= 2: # few missiles is harder
difficulty *= Settings.algoSettings['phantoonLowMissileMalus']
difficulty = self.adjustHealthDropDiff(difficulty)
if (sm.onlyBossLeft):
difficulty = 1
fight = SMBool(True, difficulty, items=ammoItems+defenseItems)
return sm.wor(fight,
@ -707,6 +715,8 @@ class Helpers(object):
# 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 (sm.onlyBossLeft):
diff = 1
if diff < 0:
return smboolFalse
return SMBool(True, diff, items=ammoItems+defenseItems)

View File

@ -13,12 +13,13 @@ 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):
def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False):
self._items = { }
self._counts = { }
self.player = player
self.maxDiff = maxDiff
self.onlyBossLeft = onlyBossLeft
# cache related
self.cacheKey = 0

View File

@ -76,7 +76,7 @@ class ItemLocContainer(object):
locs = copy.copy(self.unusedLocations)
# we don't copy restriction state on purpose: it depends on
# outside context we don't want to bring to the copy
ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff),
ret = ItemLocContainer(SMBoolManager(self.sm.player, self.sm.maxDiff, self.sm.onlyBossLeft),
self.itemPoolBackup[:] if self.itemPoolBackup != None else self.itemPool[:],
locs)
ret.currentItems = self.currentItems[:]
@ -103,7 +103,7 @@ class ItemLocContainer(object):
# transfer collected items/locations to another container
def transferCollected(self, dest):
dest.currentItems = self.currentItems[:]
dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff)
dest.sm = SMBoolManager(self.sm.player, self.sm.maxDiff, self.sm.onlyBossLeft)
dest.sm.addItems([item.Type for item in dest.currentItems])
dest.itemLocations = copy.copy(self.itemLocations)
dest.unrestrictedItems = copy.copy(self.unrestrictedItems)

View File

@ -348,8 +348,7 @@ class VariaRandomizer:
if response.ok:
PresetLoader.factory(json.loads(response.text)).load(self.player)
else:
print("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name))
sys.exit(-1)
raise Exception("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name))
else:
preset = 'default'
PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player)
@ -365,13 +364,11 @@ class VariaRandomizer:
self.seed = args.seed
logger.debug("seed: {}".format(self.seed))
seed4rand = self.seed
if args.raceMagic is not None:
if args.raceMagic <= 0 or args.raceMagic >= 0x10000:
print("Invalid magic")
sys.exit(-1)
seed4rand = self.seed ^ args.raceMagic
# random.seed(seed4rand)
# if no max diff, set it very high
if args.maxDifficulty:
if args.maxDifficulty == 'random':

View File

@ -311,7 +311,7 @@ def loadRandoPreset(world, player, args):
args.animals = world.animals[player].value
args.noVariaTweaks = not world.varia_tweaks[player].value
args.maxDifficulty = diffs[world.max_difficulty[player].value]
args.suitsRestriction = world.suits_restriction[player].value
#args.suitsRestriction = world.suits_restriction[player].value
#args.hideItems = world.hide_items[player].value
args.strictMinors = world.strict_minors[player].value
args.noLayout = not world.layout_patches[player].value

View File

@ -16,10 +16,11 @@ class EvermizerFlag:
return self.flag if self.value != self.default else ''
class OffOnChaosChoice(Choice):
class OffOnFullChoice(Choice):
option_off = 0
option_on = 1
option_chaos = 2
option_full = 2
alias_chaos = 2
alias_false = 0
alias_true = 1
@ -30,7 +31,8 @@ class Difficulty(EvermizerFlags, Choice):
option_easy = 0
option_normal = 1
option_hard = 2
option_chaos = 3 # random is reserved pre 0.2
option_mystery = 3 # 'random' is reserved
alias_chaos = 3
default = 1
flags = ['e', 'n', 'h', 'x']
@ -88,27 +90,27 @@ class ShorterDialogs(EvermizerFlag, Toggle):
class ShortBossRush(EvermizerFlag, Toggle):
"""Start boss rush at Magmar, cut HP in half"""
"""Start boss rush at Metal Magmar, cut enemy HP in half"""
displayname = "Short Boss Rush"
flag = 'f'
class Ingredienizer(EvermizerFlags, OffOnChaosChoice):
"""Shuffles or randomizes spell ingredients"""
class Ingredienizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles, Full randomizes spell ingredients"""
displayname = "Ingredienizer"
default = 1
flags = ['i', '', 'I']
class Sniffamizer(EvermizerFlags, OffOnChaosChoice):
"""Shuffles or randomizes drops in sniff locations"""
class Sniffamizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles, Full randomizes drops in sniff locations"""
displayname = "Sniffamizer"
default = 1
flags = ['s', '', 'S']
class Callbeadamizer(EvermizerFlags, OffOnChaosChoice):
"""Shuffles call bead characters or spells"""
class Callbeadamizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles call bead characters, Full shuffles individual spells"""
displayname = "Callbeadamizer"
default = 1
flags = ['c', '', 'C']
@ -120,8 +122,8 @@ class Musicmizer(EvermizerFlag, Toggle):
flag = 'm'
class Doggomizer(EvermizerFlags, OffOnChaosChoice):
"""On shuffles dog per act, Chaos randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
class Doggomizer(EvermizerFlags, OffOnFullChoice):
"""On shuffles dog per act, Full randomizes dog per screen, Pupdunk gives you Everpupper everywhere"""
displayname = "Doggomizer"
option_pupdunk = 3
default = 0

View File

@ -1,14 +1,14 @@
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp38-cp38-macosx_10_9_amd64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-macosx_10_9_amd64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.39.2/pyevermizer-0.39.2-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8'
#https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9'
https://github.com/black-sliver/pyevermizer/releases/download/v0.40.0/pyevermizer-0.40.0-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10'
bsdiff4>=1.2.1

View File

@ -213,6 +213,7 @@ starter_melee_weapons: Tuple[str, ...] = (
)
starter_spells: Tuple[str, ...] = (
'Aura Blast',
'Colossal Blade',
'Infernal Flames',
'Plasma Geyser',

View File

@ -27,4 +27,7 @@ def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
else:
gates = (*past_teleportation_gates, *present_teleportation_gates)
if not world:
return gates[0]
return world.random.choice(gates)

View File

@ -1,4 +1,4 @@
from typing import List, Dict, Tuple, Optional, Callable
from typing import List, Set, Dict, Tuple, Optional, Callable
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
from .Options import is_option_enabled
from .Locations import LocationData
@ -6,7 +6,7 @@ from .Locations import LocationData
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], pyramid_keys_unlock: str):
locations_per_region = get_locations_per_region(locations)
world.regions += [
regions = [
create_region(world, player, locations_per_region, location_cache, 'Menu'),
create_region(world, player, locations_per_region, location_cache, 'Tutorial'),
create_region(world, player, locations_per_region, location_cache, 'Lake desolation'),
@ -45,6 +45,11 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
create_region(world, player, locations_per_region, location_cache, 'Space time continuum')
]
if __debug__:
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
world.regions += regions
connectStartingRegion(world, player)
names: Dict[str, int] = {}
@ -94,8 +99,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', lambda state: state._timespinner_has_keycard_A(world, player))
connect(world, player, names, 'Skeleton Shaft', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
connect(world, player, names, 'Sealed Caves (upper)', 'Skeleton Shaft')
connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_forwarddash_doublejump(world, player))
connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
connect(world, player, names, 'Sealed Caves (upper)', 'Sealed Caves (Xarion)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_doublejump(world, player))
connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_doublejump(world, player))
connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
connect(world, player, names, 'Refugee Camp', 'Forest')
connect(world, player, names, 'Refugee Camp', 'Library', lambda state: not is_option_enabled(world, player, "Inverted"))
@ -114,9 +119,9 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
connect(world, player, names, 'Lower Lake Serene', 'Left Side forest Caves')
connect(world, player, names, 'Lower Lake Serene', 'Caves of Banishment (upper)')
connect(world, player, names, 'Caves of Banishment (upper)', 'Upper Lake Serene', lambda state: state.has('Water Mask', player))
connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_forwarddash_doublejump(world, player))
connect(world, player, names, 'Caves of Banishment (upper)', 'Caves of Banishment (Maw)', lambda state: state.has('Twin Pyramid Key', player) or state._timespinner_has_doublejump(world, player))
connect(world, player, names, 'Caves of Banishment (upper)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (upper)', lambda state: state._timespinner_has_doublejump(world, player))
connect(world, player, names, 'Caves of Banishment (Maw)', 'Caves of Banishment (Sirens)', lambda state: state.has('Gas Mask', player))
connect(world, player, names, 'Caves of Banishment (Maw)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
connect(world, player, names, 'Caves of Banishment (Sirens)', 'Forest')
@ -150,6 +155,16 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment")
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):
existingRegions = set()
for region in regions:
existingRegions.add(region.name)
if (regionNames - existingRegions):
raise Exception("Tiemspinner: the following regions are used in locations: {}, but no such region exists".format(regionNames - existingRegions))
def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location:
location = Location(player, location_data.name, location_data.code, region)
location.access_rule = location_data.rule

View File

@ -1,4 +1,4 @@
from typing import Dict, List, Set, TextIO
from typing import Dict, List, Set, Tuple, TextIO
from BaseClasses import Item, MultiWorld, Location
from ..AutoWorld import World
from .LogicMixin import TimespinnerLogic
@ -24,19 +24,31 @@ class TimespinnerWorld(World):
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
item_name_groups = get_item_names_per_category()
locked_locations: Dict[int, List[str]] = {}
pyramid_keys_unlock: Dict[int, str] = {}
location_cache: Dict[int, List[Location]] = {}
locked_locations: List[str]
pyramid_keys_unlock: str
location_cache: List[Location]
def __init__(self, world: MultiWorld, player: int):
super().__init__(world, player)
self.locked_locations = []
self.location_cache = []
self.pyramid_keys_unlock = get_pyramid_keys_unlock(world, player)
def generate_early(self):
self.locked_locations[self.player] = []
self.location_cache[self.player] = []
self.pyramid_keys_unlock[self.player] = get_pyramid_keys_unlock(self.world, self.player)
# in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly
if self.world.start_inventory[self.player].value.pop('Meyef', 0) > 0:
self.world.StartWithMeyef[self.player].value = self.world.StartWithMeyef[self.player].option_true
if self.world.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0:
self.world.QuickSeed[self.player].value = self.world.QuickSeed[self.player].option_true
if self.world.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0:
self.world.StartWithJewelryBox[self.player].value = self.world.StartWithJewelryBox[self.player].option_true
def create_regions(self):
create_regions(self.world, self.player, get_locations(self.world, self.player),
self.location_cache[self.player], self.pyramid_keys_unlock[self.player])
self.location_cache, self.pyramid_keys_unlock)
def create_item(self, name: str) -> Item:
@ -44,22 +56,22 @@ class TimespinnerWorld(World):
def set_rules(self):
setup_events(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player])
setup_events(self.world, self.player, self.locked_locations, self.location_cache)
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
def generate_basic(self):
excluded_items = get_excluded_items_based_on_options(self.world, self.player)
excluded_items = get_excluded_items(self, self.world, self.player)
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations[self.player])
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations)
if not is_option_enabled(self.world, self.player, "QuickSeed") and not is_option_enabled(self.world, self.player, "Inverted"):
place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations[self.player])
place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations)
pool = get_item_pool(self.world, self.player, excluded_items)
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player], pool)
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations, self.location_cache, pool)
self.world.itempool += pool
@ -73,17 +85,17 @@ class TimespinnerWorld(World):
slot_data["StinkyMaw"] = True
slot_data["ProgressiveVerticalMovement"] = False
slot_data["ProgressiveKeycards"] = False
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player]
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache[self.player])
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache)
return slot_data
def write_spoiler_header(self, spoiler_handle: TextIO):
spoiler_handle.write('Twin Pyramid Keys unlock: %s\n' % (self.pyramid_keys_unlock[self.player]))
spoiler_handle.write('Twin Pyramid Keys unlock: %s\n' % (self.pyramid_keys_unlock))
def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]:
def get_excluded_items(self: TimespinnerWorld, world: MultiWorld, player: int) -> Set[str]:
excluded_items: Set[str] = set()
if is_option_enabled(world, player, "StartWithJewelryBox"):
@ -93,24 +105,46 @@ def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[s
if is_option_enabled(world, player, "QuickSeed"):
excluded_items.add('Talaria Attachment')
for item in world.precollected_items[player]:
if item.name not in self.item_name_groups['UseItem']:
excluded_items.add(item.name)
return excluded_items
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
melee_weapon = world.random.choice(starter_melee_weapons)
spell = world.random.choice(starter_spells)
non_local_items = world.non_local_items[player].value
excluded_items.add(melee_weapon)
excluded_items.add(spell)
local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item not in non_local_items)
if not local_starter_melee_weapons:
if 'Plasma Orb' in non_local_items:
raise Exception("Atleast one melee orb must be local")
else:
local_starter_melee_weapons = ('Plasma Orb',)
melee_weapon_item = create_item_with_correct_settings(world, player, melee_weapon)
spell_item = create_item_with_correct_settings(world, player, spell)
local_starter_spells = tuple(item for item in starter_spells if item not in non_local_items)
if not local_starter_spells:
if 'Lightwall' in non_local_items:
raise Exception("Atleast one spell must be local")
else:
local_starter_spells = ('Lightwall',)
world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item)
world.get_location('Yo Momma 2', player).place_locked_item(spell_item)
assign_starter_item(world, player, excluded_items, locked_locations, 'Yo Momma 1', local_starter_melee_weapons)
assign_starter_item(world, player, excluded_items, locked_locations, 'Yo Momma 2', local_starter_spells)
locked_locations.append('Yo Momma 1')
locked_locations.append('Yo Momma 2')
def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
location: str, item_list: Tuple[str, ...]):
item_name = world.random.choice(item_list)
excluded_items.add(item_name)
item = create_item_with_correct_settings(world, player, item_name)
world.get_location(location, player).place_locked_item(item)
locked_locations.append(location)
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
@ -133,8 +167,20 @@ def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locat
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
progression_item = world.random.choice(starter_progression_items)
location = world.random.choice(starter_progression_locations)
for item in world.precollected_items[player]:
if item.name in starter_progression_items:
return
local_starter_progression_items = tuple(
item for item in starter_progression_items if item not in world.non_local_items[player].value)
non_excluded_starter_progression_locations = tuple(
location for location in starter_progression_locations if location not in world.exclude_locations[player].value)
if not local_starter_progression_items or not non_excluded_starter_progression_locations:
return
progression_item = world.random.choice(local_starter_progression_items)
location = world.random.choice(non_excluded_starter_progression_locations)
excluded_items.add(progression_item)
locked_locations.append(location)