Compare commits

..

5 Commits

Author SHA1 Message Date
Fabian Dill
62b3fd4d37 WebHost: invert multitracker control back to webhost 2023-08-28 17:18:13 +02:00
Fabian Dill
e2f7153312 Factorio: convert multitracker to Template once, instead of per render 2023-08-28 13:52:56 +02:00
Fabian Dill
96d4143030 WebHost: move new API hooks to WebWorld 2023-08-28 13:49:14 +02:00
Fabian Dill
a1dcaf52e3 WebHost: offer API to modify WebHost 2023-08-28 01:37:50 +02:00
Fabian Dill
aab8f31345 Factorio: fix website multitracker 2023-08-28 01:08:19 +02:00
187 changed files with 2884 additions and 4817 deletions

View File

@@ -853,6 +853,14 @@ class Region:
state.update_reachable_regions(self.player) state.update_reachable_regions(self.player)
return self in state.reachable_regions[self.player] return self in state.reachable_regions[self.player]
def can_reach_private(self, state: CollectionState) -> bool:
for entrance in self.entrances:
if entrance.can_reach(state):
if not self in state.path:
state.path[self] = (self.name, state.path.get(entrance, None))
return True
return False
@property @property
def hint_text(self) -> str: def hint_text(self) -> str:
return self._hint_text if self._hint_text else self.name return self._hint_text if self._hint_text else self.name

View File

@@ -840,12 +840,12 @@ def distribute_planned(world: MultiWorld) -> None:
if "early_locations" in locations: if "early_locations" in locations:
locations.remove("early_locations") locations.remove("early_locations")
for target_player in worlds: for player in worlds:
locations += early_locations[target_player] locations += early_locations[player]
if "non_early_locations" in locations: if "non_early_locations" in locations:
locations.remove("non_early_locations") locations.remove("non_early_locations")
for target_player in worlds: for player in worlds:
locations += non_early_locations[target_player] locations += non_early_locations[player]
block['locations'] = locations block['locations'] = locations

View File

@@ -23,7 +23,8 @@ from urllib.request import urlopen
import ModuleUpdate import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes from worlds.alttp.Rom import LocalRom, apply_rom_settings, get_base_rom_bytes
from worlds.alttp.Sprites import Sprite
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \ from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
get_adjuster_settings, get_adjuster_settings_no_defaults, tkinter_center_window, init_logging get_adjuster_settings, get_adjuster_settings_no_defaults, tkinter_center_window, init_logging

15
Main.py
View File

@@ -139,13 +139,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
exclusion_rules(world, player, world.exclude_locations[player].value) exclusion_rules(world, player, world.exclude_locations[player].value)
world.priority_locations[player].value -= world.exclude_locations[player].value world.priority_locations[player].value -= world.exclude_locations[player].value
for location_name in world.priority_locations[player].value: for location_name in world.priority_locations[player].value:
try: world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
location = world.get_location(location_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
if location_name not in world.worlds[player].location_name_to_id:
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
else:
location.progress_type = LocationProgressType.PRIORITY
# Set local and non-local item rules. # Set local and non-local item rules.
if world.players > 1: if world.players > 1:
@@ -165,8 +159,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for player, items in depletion_pool.items(): for player, items in depletion_pool.items():
player_world: AutoWorld.World = world.worlds[player] player_world: AutoWorld.World = world.worlds[player]
for count in items.values(): for count in items.values():
for _ in range(count): new_items.append(player_world.create_filler())
new_items.append(player_world.create_filler())
target: int = sum(sum(items.values()) for items in depletion_pool.values()) target: int = sum(sum(items.values()) for items in depletion_pool.values())
for i, item in enumerate(world.itempool): for i, item in enumerate(world.itempool):
if depletion_pool[item.player].get(item.name, 0): if depletion_pool[item.player].get(item.name, 0):
@@ -186,7 +179,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if remaining_items: if remaining_items:
raise Exception(f"{world.get_player_name(player)}" raise Exception(f"{world.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}") f" is trying to remove items from their pool that don't exist: {remaining_items}")
assert len(world.itempool) == len(new_items), "Item Pool amounts should not change."
world.itempool[:] = new_items world.itempool[:] = new_items
# temporary home for item links, should be moved out of Main # temporary home for item links, should be moved out of Main
@@ -400,7 +392,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
f.write(bytes([3])) # version of format f.write(bytes([3])) # version of format
f.write(multidata) f.write(multidata)
output_file_futures.append(pool.submit(write_multidata)) multidata_task = pool.submit(write_multidata)
if not check_accessibility_task.result(): if not check_accessibility_task.result():
if not world.can_beat_game(): if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.") raise Exception("Game appears as unbeatable. Aborting.")
@@ -408,6 +400,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.warning("Location Accessibility requirements not fulfilled.") logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occurred. # retrieve exceptions via .result() if they occurred.
multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1): for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
if i % 10 == 0 or i == len(output_file_futures): if i % 10 == 0 or i == len(output_file_futures):
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).') logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')

View File

@@ -408,21 +408,13 @@ if typing.TYPE_CHECKING: # type-check with pure python implementation until we
LocationStore = _LocationStore LocationStore = _LocationStore
else: else:
try: try:
from _speedups import LocationStore import pyximport
import _speedups pyximport.install()
import os.path
if os.path.isfile("_speedups.pyx") and os.path.getctime(_speedups.__file__) < os.path.getctime("_speedups.pyx"):
warnings.warn(f"{_speedups.__file__} outdated! "
f"Please rebuild with `cythonize -b -i _speedups.pyx` or delete it!")
except ImportError: except ImportError:
try: pyximport = None
import pyximport try:
pyximport.install() from _speedups import LocationStore
except ImportError: except ImportError:
pyximport = None warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
try: "Install a matching C++ compiler for your platform to compile _speedups.")
from _speedups import LocationStore LocationStore = _LocationStore
except ImportError:
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
"Install a matching C++ compiler for your platform to compile _speedups.")
LocationStore = _LocationStore

View File

@@ -68,11 +68,12 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
options = snes_options.split() options = snes_options.split()
num_options = len(options) num_options = len(options)
if num_options > 0:
snes_device_number = int(options[0])
if num_options > 1: if num_options > 1:
snes_address = options[0] snes_address = options[0]
snes_device_number = int(options[1]) snes_device_number = int(options[1])
elif num_options > 0:
snes_device_number = int(options[0])
self.ctx.snes_reconnect_address = None self.ctx.snes_reconnect_address = None
if self.ctx.snes_connect_task: if self.ctx.snes_connect_task:
@@ -564,16 +565,14 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'} PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
try: try:
for address, data in write_list: for address, data in write_list:
while data: PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
# Divide the write into packets of 256 bytes. # REVIEW: above: `if snes_socket is None: return False`
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]] # Does it need to be checked again?
if ctx.snes_socket is not None: if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request)) await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data[:256]) await ctx.snes_socket.send(data)
address += 256 else:
data = data[256:] snes_logger.warning(f"Could not send data to SNES: {data}")
else:
snes_logger.warning(f"Could not send data to SNES: {data}")
except ConnectionClosed: except ConnectionClosed:
return False return False

File diff suppressed because it is too large Load Diff

View File

@@ -29,31 +29,31 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
def _cmd_patch(self): def _cmd_patch(self):
"""Patch the game.""" """Patch the game."""
if isinstance(self.ctx, UndertaleContext): if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
self.ctx.patch_game() self.ctx.patch_game()
self.output("Patched.") self.output("Patched.")
def _cmd_savepath(self, directory: str): def _cmd_savepath(self, directory: str):
"""Redirect to proper save data folder. (Use before connecting!)""" """Redirect to proper save data folder. (Use before connecting!)"""
if isinstance(self.ctx, UndertaleContext): if isinstance(self.ctx, UndertaleContext):
self.ctx.save_game_folder = directory UndertaleContext.save_game_folder = directory
self.output("Changed to the following directory: " + self.ctx.save_game_folder) self.output("Changed to the following directory: " + directory)
@mark_raw @mark_raw
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
"""Patch the game automatically.""" """Patch the game automatically."""
if isinstance(self.ctx, UndertaleContext): if isinstance(self.ctx, UndertaleContext):
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
tempInstall = steaminstall tempInstall = steaminstall
if not os.path.isfile(os.path.join(tempInstall, "data.win")): if not os.path.isfile(os.path.join(tempInstall, "data.win")):
tempInstall = None tempInstall = None
if tempInstall is None: if tempInstall is None:
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale" tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists(tempInstall): if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale" tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
elif not os.path.exists(tempInstall): elif not os.path.exists(tempInstall):
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale" tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists(tempInstall): if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale" tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")): if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder." self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
@@ -61,8 +61,8 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
else: else:
for file_name in os.listdir(tempInstall): for file_name in os.listdir(tempInstall):
if file_name != "steam_api.dll": if file_name != "steam_api.dll":
shutil.copy(os.path.join(tempInstall, file_name), shutil.copy(tempInstall+"\\"+file_name,
os.path.join(os.getcwd(), "Undertale", file_name)) os.getcwd() + "\\Undertale\\" + file_name)
self.ctx.patch_game() self.ctx.patch_game()
self.output("Patching successful!") self.output("Patching successful!")
@@ -111,13 +111,13 @@ class UndertaleContext(CommonContext):
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE") self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
def patch_game(self): def patch_game(self):
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f: with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff")) patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f: with open(os.getcwd() + "/Undertale/data.win", "wb") as f:
f.write(patchedFile) f.write(patchedFile)
os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True) os.makedirs(name=os.getcwd() + "\\Undertale\\" + "Custom Sprites", exist_ok=True)
with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites", with open(os.path.expandvars(os.getcwd() + "\\Undertale\\" + "Custom Sprites\\" +
"Which Character.txt")), "w") as f: "Which Character.txt"), "w") as f:
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only " f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
"line other than this one.\n", "frisk"]) "line other than this one.\n", "frisk"])
f.close() f.close()
@@ -385,7 +385,7 @@ async def multi_watcher(ctx: UndertaleContext):
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
for file in files: for file in files:
if "spots.mine" in file and "Online" in ctx.tags: if "spots.mine" in file and "Online" in ctx.tags:
with open(os.path.join(root, file), "r") as mine: with open(root + "/" + file, "r") as mine:
this_x = mine.readline() this_x = mine.readline()
this_y = mine.readline() this_y = mine.readline()
this_room = mine.readline() this_room = mine.readline()
@@ -408,7 +408,7 @@ async def game_watcher(ctx: UndertaleContext):
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
for file in files: for file in files:
if ".item" in file: if ".item" in file:
os.remove(os.path.join(root, file)) os.remove(root+"/"+file)
sync_msg = [{"cmd": "Sync"}] sync_msg = [{"cmd": "Sync"}]
if ctx.locations_checked: if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)}) sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
@@ -424,13 +424,13 @@ async def game_watcher(ctx: UndertaleContext):
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
for file in files: for file in files:
if "DontBeMad.mad" in file: if "DontBeMad.mad" in file:
os.remove(os.path.join(root, file)) os.remove(root+"/"+file)
if "DeathLink" in ctx.tags: if "DeathLink" in ctx.tags:
await ctx.send_death() await ctx.send_death()
if "scout" == file: if "scout" == file:
sending = [] sending = []
try: try:
with open(os.path.join(root, file), "r") as f: with open(root+"/"+file, "r") as f:
lines = f.readlines() lines = f.readlines()
for l in lines: for l in lines:
if ctx.server_locations.__contains__(int(l)+12000): if ctx.server_locations.__contains__(int(l)+12000):
@@ -438,11 +438,11 @@ async def game_watcher(ctx: UndertaleContext):
finally: finally:
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending, await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
"create_as_hint": int(2)}]) "create_as_hint": int(2)}])
os.remove(os.path.join(root, file)) os.remove(root+"/"+file)
if "check.spot" in file: if "check.spot" in file:
sending = [] sending = []
try: try:
with open(os.path.join(root, file), "r") as f: with open(root+"/"+file, "r") as f:
lines = f.readlines() lines = f.readlines()
for l in lines: for l in lines:
sending = sending+[(int(l.rstrip('\n')))+12000] sending = sending+[(int(l.rstrip('\n')))+12000]
@@ -451,7 +451,7 @@ async def game_watcher(ctx: UndertaleContext):
if "victory" in file and str(ctx.route) in file: if "victory" in file and str(ctx.route) in file:
victory = True victory = True
if ".playerspot" in file and "Online" not in ctx.tags: if ".playerspot" in file and "Online" not in ctx.tags:
os.remove(os.path.join(root, file)) os.remove(root+"/"+file)
if "victory" in file: if "victory" in file:
if str(ctx.route) == "all_routes": if str(ctx.route) == "all_routes":
if "neutral" in file and ctx.completed_routes["neutral"] != 1: if "neutral" in file and ctx.completed_routes["neutral"] != 1:

View File

@@ -44,7 +44,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.4.3" __version__ = "0.4.2"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")
@@ -359,13 +359,11 @@ safe_builtins = frozenset((
class RestrictedUnpickler(pickle.Unpickler): class RestrictedUnpickler(pickle.Unpickler):
generic_properties_module: Optional[object]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(RestrictedUnpickler, self).__init__(*args, **kwargs) super(RestrictedUnpickler, self).__init__(*args, **kwargs)
self.options_module = importlib.import_module("Options") self.options_module = importlib.import_module("Options")
self.net_utils_module = importlib.import_module("NetUtils") self.net_utils_module = importlib.import_module("NetUtils")
self.generic_properties_module = None self.generic_properties_module = importlib.import_module("worlds.generic")
def find_class(self, module, name): def find_class(self, module, name):
if module == "builtins" and name in safe_builtins: if module == "builtins" and name in safe_builtins:
@@ -375,8 +373,6 @@ class RestrictedUnpickler(pickle.Unpickler):
return getattr(self.net_utils_module, name) return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate # Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
if not self.generic_properties_module:
self.generic_properties_module = importlib.import_module("worlds.generic")
return getattr(self.generic_properties_module, name) return getattr(self.generic_properties_module, name)
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options) # pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
if module.lower().endswith("options"): if module.lower().endswith("options"):
@@ -576,7 +572,7 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
zenity = which("zenity") zenity = which("zenity")
if zenity: if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes) z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
selection = (f"--filename={suggest}",) if suggest else () selection = (f'--filename="{suggest}',) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection) return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
# fall back to tk # fall back to tk
@@ -588,10 +584,7 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
f'This attempt was made because open_filename was used for "{title}".') f'This attempt was made because open_filename was used for "{title}".')
raise e raise e
else: else:
try: root = tkinter.Tk()
root = tkinter.Tk()
except tkinter.TclError:
return None # GUI not available. None is the same as a user clicking "cancel"
root.withdraw() root.withdraw()
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes), return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
initialfile=suggest or None) initialfile=suggest or None)
@@ -604,14 +597,13 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
if is_linux: if is_linux:
# prefer native dialog # prefer native dialog
from shutil import which from shutil import which
kdialog = which("kdialog") kdialog = None#which("kdialog")
if kdialog: if kdialog:
return run(kdialog, f"--title={title}", "--getexistingdirectory", return run(kdialog, f"--title={title}", "--getexistingdirectory", suggest or ".")
os.path.abspath(suggest) if suggest else ".") zenity = None#which("zenity")
zenity = which("zenity")
if zenity: if zenity:
z_filters = ("--directory",) z_filters = ("--directory",)
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else () selection = (f'--filename="{suggest}',) if suggest else ()
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection) return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
# fall back to tk # fall back to tk
@@ -623,10 +615,7 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
f'This attempt was made because open_filename was used for "{title}".') f'This attempt was made because open_filename was used for "{title}".')
raise e raise e
else: else:
try: root = tkinter.Tk()
root = tkinter.Tk()
except tkinter.TclError:
return None # GUI not available. None is the same as a user clicking "cancel"
root.withdraw() root.withdraw()
return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None) return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None)

View File

@@ -13,6 +13,15 @@ import Utils
import settings import settings
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
from WebHostLib import register, app as raw_app
from waitress import serve
from WebHostLib.models import db
from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.options import create as create_options_files
import worlds
settings.no_gui = True settings.no_gui = True
configpath = os.path.abspath("config.yaml") configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home if not os.path.exists(configpath): # fall back to config.yaml in home
@@ -20,9 +29,6 @@ if not os.path.exists(configpath): # fall back to config.yaml in home
def get_app(): def get_app():
from WebHostLib import register, cache, app as raw_app
from WebHostLib.models import db
register() register()
app = raw_app app = raw_app
if os.path.exists(configpath) and not app.config["TESTING"]: if os.path.exists(configpath) and not app.config["TESTING"]:
@@ -34,9 +40,15 @@ def get_app():
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4() app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}") logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
cache.init_app(app)
db.bind(**app.config["PONY"]) db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True) db.generate_mapping(create_tables=True)
for world in worlds.AutoWorldRegister.world_types.values():
try:
world.web.run_webhost_app_setup(app)
except Exception as e:
logging.exception(e)
return app return app
@@ -116,16 +128,16 @@ if __name__ == "__main__":
multiprocessing.set_start_method('spawn') multiprocessing.set_start_method('spawn')
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO) logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
from WebHostLib.lttpsprites import update_sprites_lttp for world in worlds.AutoWorldRegister.world_types.values():
from WebHostLib.autolauncher import autohost, autogen try:
from WebHostLib.options import create as create_options_files world.web.run_webhost_setup()
except Exception as e:
logging.exception(e)
try:
update_sprites_lttp()
except Exception as e:
logging.exception(e)
logging.warning("Could not update LttP sprites.")
app = get_app() app = get_app()
del world, worlds
create_options_files() create_options_files()
create_ordered_tutorials_file() create_ordered_tutorials_file()
if app.config["SELFLAUNCH"]: if app.config["SELFLAUNCH"]:
@@ -136,5 +148,4 @@ if __name__ == "__main__":
if app.config["DEBUG"]: if app.config["DEBUG"]:
app.run(debug=True, port=app.config["PORT"]) app.run(debug=True, port=app.config["PORT"])
else: else:
from waitress import serve
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"]) serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])

View File

@@ -49,11 +49,11 @@ app.config["PONY"] = {
'create_db': True 'create_db': True
} }
app.config["MAX_ROLL"] = 20 app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "SimpleCache" app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
app.config["JSON_AS_ASCII"] = False app.config["JSON_AS_ASCII"] = False
app.config["HOST_ADDRESS"] = "" app.config["HOST_ADDRESS"] = ""
cache = Cache() cache = Cache(app)
Compress(app) Compress(app)

View File

@@ -3,6 +3,8 @@ from __future__ import annotations
import json import json
import logging import logging
import multiprocessing import multiprocessing
import os
import sys
import threading import threading
import time import time
import typing import typing
@@ -11,7 +13,55 @@ from datetime import timedelta, datetime
from pony.orm import db_session, select, commit from pony.orm import db_session, select, commit
from Utils import restricted_loads from Utils import restricted_loads
from .locker import Locker, AlreadyRunningException
class CommonLocker():
"""Uses a file lock to signal that something is already running"""
lock_folder = "file_locks"
def __init__(self, lockname: str, folder=None):
if folder:
self.lock_folder = folder
os.makedirs(self.lock_folder, exist_ok=True)
self.lockname = lockname
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
class AlreadyRunningException(Exception):
pass
if sys.platform == 'win32':
class Locker(CommonLocker):
def __enter__(self):
try:
if os.path.exists(self.lockfile):
os.unlink(self.lockfile)
self.fp = os.open(
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
except OSError as e:
raise AlreadyRunningException() from e
def __exit__(self, _type, value, tb):
fp = getattr(self, "fp", None)
if fp:
os.close(self.fp)
os.unlink(self.lockfile)
else: # unix
import fcntl
class Locker(CommonLocker):
def __enter__(self):
try:
self.fp = open(self.lockfile, "wb")
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as e:
raise AlreadyRunningException() from e
def __exit__(self, _type, value, tb):
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
self.fp.close()
def launch_room(room: Room, config: dict): def launch_room(room: Room, config: dict):

View File

@@ -24,8 +24,8 @@ def check():
if 'file' not in request.files: if 'file' not in request.files:
flash('No file part') flash('No file part')
else: else:
files = request.files.getlist('file') file = request.files['file']
options = get_yaml_data(files) options = get_yaml_data(file)
if isinstance(options, str): if isinstance(options, str):
flash(options) flash(options)
else: else:
@@ -39,33 +39,30 @@ def mysterycheck():
return redirect(url_for("check"), 301) return redirect(url_for("check"), 301)
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]: def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
options = {} options = {}
for file in files: # if user does not select file, browser also
# if user does not select file, browser also # submit an empty part without filename
# submit an empty part without filename if file.filename == '':
if file.filename == '': return 'No selected file'
return 'No selected file' elif file and allowed_file(file.filename):
elif file.filename in options: if file.filename.endswith(".zip"):
return f'Conflicting files named {file.filename} submitted'
elif file and allowed_file(file.filename):
if file.filename.endswith(".zip"):
with zipfile.ZipFile(file, 'r') as zfile: with zipfile.ZipFile(file, 'r') as zfile:
infolist = zfile.infolist() infolist = zfile.infolist()
if any(file.filename.endswith(".archipelago") for file in infolist): if any(file.filename.endswith(".archipelago") for file in infolist):
return Markup("Error: Your .zip file contains an .archipelago file. " return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?') 'Did you mean to <a href="/uploads">host a game</a>?')
for file in infolist: for file in infolist:
if file.filename.endswith(banned_zip_contents): if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \ return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
"Your file was deleted." "Your file was deleted."
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")): elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
options[file.filename] = zfile.open(file, "r").read() options[file.filename] = zfile.open(file, "r").read()
else: else:
options[file.filename] = file.read() options = {file.filename: file.read()}
if not options: if not options:
return "Did not find a .yaml file to process." return "Did not find a .yaml file to process."
return options return options

View File

@@ -19,7 +19,6 @@ import Utils
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
from Utils import restricted_loads, cache_argsless from Utils import restricted_loads, cache_argsless
from .locker import Locker
from .models import Command, GameDataPackage, Room, db from .models import Command, GameDataPackage, Room, db
@@ -164,19 +163,16 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
db.generate_mapping(check_tables=False) db.generate_mapping(check_tables=False)
async def main(): async def main():
import gc
Utils.init_logging(str(room_id), write_mode="a") Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext(static_server_data) ctx = WebHostContext(static_server_data)
ctx.load(room_id) ctx.load(room_id)
ctx.init_save() ctx.init_save()
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
gc.collect() # free intermediate objects used during setup
try: try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
await ctx.server await ctx.server
except OSError: # likely port in use except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context) ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
await ctx.server await ctx.server
@@ -202,15 +198,16 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
await ctx.shutdown_task await ctx.shutdown_task
logging.info("Shutting down") logging.info("Shutting down")
from .autolauncher import Locker
with Locker(room_id): with Locker(room_id):
try: try:
asyncio.run(main()) asyncio.run(main())
except (KeyboardInterrupt, SystemExit): except KeyboardInterrupt:
with db_session: with db_session:
room = Room.get(id=room_id) room = Room.get(id=room_id)
# ensure the Room does not spin up again on its own, minute of safety buffer # ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout) room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
except Exception: except:
with db_session: with db_session:
room = Room.get(id=room_id) room = Room.get(id=room_id)
room.last_port = -1 room.last_port = -1

View File

@@ -64,8 +64,8 @@ def generate(race=False):
if 'file' not in request.files: if 'file' not in request.files:
flash('No file part') flash('No file part')
else: else:
files = request.files.getlist('file') file = request.files['file']
options = get_yaml_data(files) options = get_yaml_data(file)
if isinstance(options, str): if isinstance(options, str):
flash(options) flash(options)
else: else:

View File

@@ -1,51 +0,0 @@
import os
import sys
class CommonLocker:
"""Uses a file lock to signal that something is already running"""
lock_folder = "file_locks"
def __init__(self, lockname: str, folder=None):
if folder:
self.lock_folder = folder
os.makedirs(self.lock_folder, exist_ok=True)
self.lockname = lockname
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
class AlreadyRunningException(Exception):
pass
if sys.platform == 'win32':
class Locker(CommonLocker):
def __enter__(self):
try:
if os.path.exists(self.lockfile):
os.unlink(self.lockfile)
self.fp = os.open(
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
except OSError as e:
raise AlreadyRunningException() from e
def __exit__(self, _type, value, tb):
fp = getattr(self, "fp", None)
if fp:
os.close(self.fp)
os.unlink(self.lockfile)
else: # unix
import fcntl
class Locker(CommonLocker):
def __enter__(self):
try:
self.fp = open(self.lockfile, "wb")
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as e:
raise AlreadyRunningException() from e
def __exit__(self, _type, value, tb):
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
self.fp.close()

View File

@@ -1,50 +0,0 @@
import os
import threading
import json
from Utils import local_path, user_path
from worlds.alttp.Rom import Sprite
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
input_dir = user_path("data", "sprites", "alttpr")
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
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()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
task.do_events()
spriteData = []
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
sprite = Sprite(os.path.join(input_dir, file))
if not sprite.name:
print("Warning:", file, "has no name.")
sprite.name = file.split(".", 1)[0]
if sprite.valid:
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
image.write(get_image_for_sprite(sprite, True))
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
else:
print(file, "dropped, as it has no valid sprite data.")
spriteData.sort(key=lambda entry: entry["name"])
with open(f'{output_dir}/spriteData.json', 'w') as file:
json.dump({"sprites": spriteData}, file, indent=1)
return spriteData

View File

@@ -3,8 +3,7 @@ pony>=0.7.16; python_version <= '3.10'
pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11' pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11'
waitress>=2.1.2 waitress>=2.1.2
Flask-Caching>=2.0.2 Flask-Caching>=2.0.2
Flask-Compress>=1.14 Flask-Compress>=1.13
Flask-Limiter>=3.5.0 Flask-Limiter>=3.3.0
bokeh>=3.1.1; python_version <= '3.8' bokeh>=3.1.1
bokeh>=3.2.2; python_version >= '3.9'
markupsafe>=2.1.3 markupsafe>=2.1.3

View File

@@ -1,83 +0,0 @@
window.addEventListener('load', () => {
const gameHeaders = document.getElementsByClassName('collapse-toggle');
Array.from(gameHeaders).forEach((header) => {
const gameName = header.getAttribute('data-game');
header.addEventListener('click', () => {
const gameArrow = document.getElementById(`${gameName}-arrow`);
const gameInfo = document.getElementById(gameName);
if (gameInfo.classList.contains('collapsed')) {
gameArrow.innerText = '▼';
gameInfo.classList.remove('collapsed');
} else {
gameArrow.innerText = '▶';
gameInfo.classList.add('collapsed');
}
});
});
// Handle game filter input
const gameSearch = document.getElementById('game-search');
gameSearch.value = '';
gameSearch.addEventListener('input', (evt) => {
if (!evt.target.value.trim()) {
// If input is empty, display all collapsed games
return Array.from(gameHeaders).forEach((header) => {
header.style.display = null;
const gameName = header.getAttribute('data-game');
document.getElementById(`${gameName}-arrow`).innerText = '▶';
document.getElementById(gameName).classList.add('collapsed');
});
}
// Loop over all the games
Array.from(gameHeaders).forEach((header) => {
const gameName = header.getAttribute('data-game');
const gameArrow = document.getElementById(`${gameName}-arrow`);
const gameInfo = document.getElementById(gameName);
// If the game name includes the search string, display the game. If not, hide it
if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) {
header.style.display = null;
gameArrow.innerText = '▼';
gameInfo.classList.remove('collapsed');
} else {
console.log(header);
header.style.display = 'none';
gameArrow.innerText = '▶';
gameInfo.classList.add('collapsed');
}
});
});
document.getElementById('expand-all').addEventListener('click', expandAll);
document.getElementById('collapse-all').addEventListener('click', collapseAll);
});
const expandAll = () => {
const gameHeaders = document.getElementsByClassName('collapse-toggle');
// Loop over all the games
Array.from(gameHeaders).forEach((header) => {
const gameName = header.getAttribute('data-game');
const gameArrow = document.getElementById(`${gameName}-arrow`);
const gameInfo = document.getElementById(gameName);
if (header.style.display === 'none') { return; }
gameArrow.innerText = '▼';
gameInfo.classList.remove('collapsed');
});
};
const collapseAll = () => {
const gameHeaders = document.getElementsByClassName('collapse-toggle');
// Loop over all the games
Array.from(gameHeaders).forEach((header) => {
const gameName = header.getAttribute('data-game');
const gameArrow = document.getElementById(`${gameName}-arrow`);
const gameInfo = document.getElementById(gameName);
if (header.style.display === 'none') { return; }
gameArrow.innerText = '▶';
gameInfo.classList.add('collapsed');
});
};

View File

@@ -14,17 +14,6 @@ const adjustTableHeight = () => {
} }
}; };
/**
* Convert an integer number of seconds into a human readable HH:MM format
* @param {Number} seconds
* @returns {string}
*/
const secondsToHours = (seconds) => {
let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
window.addEventListener('load', () => { window.addEventListener('load', () => {
const tables = $(".table").DataTable({ const tables = $(".table").DataTable({
paging: false, paging: false,
@@ -38,18 +27,7 @@ window.addEventListener('load', () => {
stateLoadCallback: function(settings) { stateLoadCallback: function(settings) {
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`)); return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
}, },
footerCallback: function(tfoot, data, start, end, display) {
if (tfoot) {
const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x));
Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText =
(activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None';
}
},
columnDefs: [ columnDefs: [
{
targets: 'last-activity',
name: 'lastActivity'
},
{ {
targets: 'hours', targets: 'hours',
render: function (data, type, row) { render: function (data, type, row) {
@@ -62,7 +40,11 @@ window.addEventListener('load', () => {
if (data === "None") if (data === "None")
return data; return data;
return secondsToHours(data); let hours = Math.floor(data / 3600);
let minutes = Math.floor((data - (hours * 3600)) / 60);
if (minutes < 10) {minutes = "0"+minutes;}
return hours+':'+minutes;
} }
}, },
{ {
@@ -132,16 +114,11 @@ window.addEventListener('load', () => {
if (status === "success") { if (status === "success") {
target.find(".table").each(function (i, new_table) { target.find(".table").each(function (i, new_table) {
const new_trs = $(new_table).find("tbody>tr"); const new_trs = $(new_table).find("tbody>tr");
const footer_tr = $(new_table).find("tfoot>tr");
const old_table = tables.eq(i); const old_table = tables.eq(i);
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop(); const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft(); const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
old_table.clear(); old_table.clear();
if (footer_tr.length) { old_table.rows.add(new_trs).draw();
$(old_table.table).find("tfoot").html(footer_tr);
}
old_table.rows.add(new_trs);
old_table.draw();
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll); $(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll); $(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
}); });

View File

@@ -160,7 +160,6 @@ const buildUI = (settingData) => {
weightedSettingsDiv.classList.add('invisible'); weightedSettingsDiv.classList.add('invisible');
itemPoolDiv.classList.add('invisible'); itemPoolDiv.classList.add('invisible');
hintsDiv.classList.add('invisible'); hintsDiv.classList.add('invisible');
locationsDiv.classList.add('invisible');
expandButton.classList.remove('invisible'); expandButton.classList.remove('invisible');
}); });
@@ -169,7 +168,6 @@ const buildUI = (settingData) => {
weightedSettingsDiv.classList.remove('invisible'); weightedSettingsDiv.classList.remove('invisible');
itemPoolDiv.classList.remove('invisible'); itemPoolDiv.classList.remove('invisible');
hintsDiv.classList.remove('invisible'); hintsDiv.classList.remove('invisible');
locationsDiv.classList.remove('invisible');
expandButton.classList.add('invisible'); expandButton.classList.add('invisible');
}); });
}); });
@@ -1136,8 +1134,8 @@ const validateSettings = () => {
return; return;
} }
// Remove any disabled options
Object.keys(settings[game]).forEach((setting) => { Object.keys(settings[game]).forEach((setting) => {
// Remove any disabled options
Object.keys(settings[game][setting]).forEach((option) => { Object.keys(settings[game][setting]).forEach((option) => {
if (settings[game][setting][option] === 0) { if (settings[game][setting][option] === 0) {
delete settings[game][setting][option]; delete settings[game][setting][option];
@@ -1151,32 +1149,6 @@ const validateSettings = () => {
) { ) {
errorMessage = `${game} // ${setting} has no values above zero!`; errorMessage = `${game} // ${setting} has no values above zero!`;
} }
// Remove weights from options with only one possibility
if (
Object.keys(settings[game][setting]).length === 1 &&
!Array.isArray(settings[game][setting]) &&
setting !== 'start_inventory'
) {
settings[game][setting] = Object.keys(settings[game][setting])[0];
}
// Remove empty arrays
else if (
['exclude_locations', 'priority_locations', 'local_items',
'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) &&
settings[game][setting].length === 0
) {
delete settings[game][setting];
}
// Remove empty start inventory
else if (
setting === 'start_inventory' &&
Object.keys(settings[game]['start_inventory']).length === 0
) {
delete settings[game]['start_inventory'];
}
}); });
}); });
@@ -1184,11 +1156,6 @@ const validateSettings = () => {
errorMessage = 'You have not chosen a game to play!'; errorMessage = 'You have not chosen a game to play!';
} }
// Remove weights if there is only one game
else if (Object.keys(settings.game).length === 1) {
settings.game = Object.keys(settings.game)[0];
}
// If an error occurred, alert the user and do not export the file // If an error occurred, alert the user and do not export the file
if (errorMessage) { if (errorMessage) {
userMessage.innerText = errorMessage; userMessage.innerText = errorMessage;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -9,7 +9,7 @@
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
padding: 3px 3px 10px; padding: 3px 3px 10px;
width: 710px; width: 500px;
background-color: #525494; background-color: #525494;
} }
@@ -34,12 +34,10 @@
max-height: 40px; max-height: 40px;
border: 1px solid #000000; border: 1px solid #000000;
filter: grayscale(100%) contrast(75%) brightness(20%); filter: grayscale(100%) contrast(75%) brightness(20%);
background-color: black;
} }
#inventory-table img.acquired{ #inventory-table img.acquired{
filter: none; filter: none;
background-color: black;
} }
#inventory-table div.counted-item { #inventory-table div.counted-item {
@@ -54,7 +52,7 @@
} }
#location-table{ #location-table{
width: 710px; width: 500px;
border-left: 2px solid #000000; border-left: 2px solid #000000;
border-right: 2px solid #000000; border-right: 2px solid #000000;
border-bottom: 2px solid #000000; border-bottom: 2px solid #000000;

View File

@@ -18,16 +18,6 @@
margin-bottom: 2px; margin-bottom: 2px;
} }
#games h2 .collapse-arrow{
font-size: 20px;
vertical-align: middle;
cursor: pointer;
}
#games p.collapsed{
display: none;
}
#games a{ #games a{
font-size: 16px; font-size: 16px;
} }
@@ -41,13 +31,3 @@
line-height: 25px; line-height: 25px;
margin-bottom: 7px; margin-bottom: 7px;
} }
#games #page-controls{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
#games #page-controls button{
margin-left: 0.5rem;
}

View File

@@ -55,16 +55,16 @@ table.dataTable thead{
font-family: LexendDeca-Regular, sans-serif; font-family: LexendDeca-Regular, sans-serif;
} }
table.dataTable tbody, table.dataTable tfoot{ table.dataTable tbody{
background-color: #dce2bd; background-color: #dce2bd;
font-family: LexendDeca-Light, sans-serif; font-family: LexendDeca-Light, sans-serif;
} }
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{ table.dataTable tbody tr:hover{
background-color: #e2eabb; background-color: #e2eabb;
} }
table.dataTable tbody td, table.dataTable tfoot td{ table.dataTable tbody td{
padding: 4px 6px; padding: 4px 6px;
} }
@@ -97,14 +97,10 @@ table.dataTable thead th.lower-row{
top: 46px; top: 46px;
} }
table.dataTable tbody td, table.dataTable tfoot td{ table.dataTable tbody td{
border: 1px solid #bba967; border: 1px solid #bba967;
} }
table.dataTable tfoot td{
font-weight: bold;
}
div.dataTables_scrollBody{ div.dataTables_scrollBody{
background-color: inherit !important; background-color: inherit !important;
} }

View File

@@ -17,9 +17,9 @@
</p> </p>
<div id="check-form-wrapper"> <div id="check-form-wrapper">
<form id="check-form" method="post" enctype="multipart/form-data"> <form id="check-form" method="post" enctype="multipart/form-data">
<input id="file-input" type="file" name="file" multiple> <input id="file-input" type="file" name="file">
</form> </form>
<button id="check-button">Upload File(s)</button> <button id="check-button">Upload</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -203,10 +203,10 @@ Warning: playthrough can take a significant amount of time for larger multiworld
</div> </div>
</div> </div>
<div id="generate-form-button-row"> <div id="generate-form-button-row">
<input id="file-input" type="file" name="file" multiple> <input id="file-input" type="file" name="file">
</div> </div>
</form> </form>
<button id="generate-game-button">Upload File(s)</button> <button id="generate-game-button">Upload File</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -37,7 +37,7 @@
{% endblock %} {% endblock %}
<th class="center-column">Checks</th> <th class="center-column">Checks</th>
<th class="center-column">&percnt;</th> <th class="center-column">&percnt;</th>
<th class="center-column hours last-activity">Last<br>Activity</th> <th class="center-column hours">Last<br>Activity</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -64,19 +64,6 @@
</tr> </tr>
{%- endfor -%} {%- endfor -%}
</tbody> </tbody>
{% if not self.custom_table_headers() | trim %}
<tfoot>
<tr>
<td></td>
<td>Total</td>
<td>All Games</td>
<td>{{ completed_worlds }}/{{ players|length }} Complete</td>
<td class="center-column">{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}</td>
<td class="center-column">{{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }}</td>
<td class="center-column last-activity"></td>
</tr>
</tfoot>
{% endif %}
</table> </table>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -1,7 +1,7 @@
{%- if enabled_multiworld_trackers|length > 1 -%} {%- if enabled_multiworld_trackers|length > 1 -%}
<div id="tracker-navigation"> <div id="tracker-navigation">
{% for enabled_tracker in enabled_multiworld_trackers %} {% for enabled_tracker in enabled_multiworld_trackers %}
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %} {% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker, game=enabled_tracker.name) %}
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}" <a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a> href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
{% endfor %} {% endfor %}

View File

@@ -11,7 +11,7 @@
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}"> <div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table"> <table id="inventory-table">
<tr> <tr>
<td colspan="15" class="title"> <td colspan="10" class="title">
Starting Resources Starting Resources
</td> </td>
</tr> </tr>
@@ -26,7 +26,7 @@
--> -->
</tr> </tr>
<tr> <tr>
<td colspan="15" class="title"> <td colspan="10" class="title">
Weapon & Armor Upgrades Weapon & Armor Upgrades
</td> </td>
</tr> </tr>
@@ -37,266 +37,120 @@
<td><img src="{{ vehicle_armor_url }}" class="{{ 'acquired' if 'Progressive Vehicle Armor' in acquired_items }}" title="Progressive Vehicle Armor{% if vehicle_armor_level > 0 %} (Level {{ vehicle_armor_level }}){% endif %}" /></td> <td><img src="{{ vehicle_armor_url }}" class="{{ 'acquired' if 'Progressive Vehicle Armor' in acquired_items }}" title="Progressive Vehicle Armor{% if vehicle_armor_level > 0 %} (Level {{ vehicle_armor_level }}){% endif %}" /></td>
<td><img src="{{ ship_weapon_url }}" class="{{ 'acquired' if 'Progressive Ship Weapon' in acquired_items }}" title="Progressive Ship Weapons{% if ship_weapon_level > 0 %} (Level {{ ship_weapon_level }}){% endif %}" /></td> <td><img src="{{ ship_weapon_url }}" class="{{ 'acquired' if 'Progressive Ship Weapon' in acquired_items }}" title="Progressive Ship Weapons{% if ship_weapon_level > 0 %} (Level {{ ship_weapon_level }}){% endif %}" /></td>
<td><img src="{{ ship_armor_url }}" class="{{ 'acquired' if 'Progressive Ship Armor' in acquired_items }}" title="Progressive Ship Armor{% if ship_armor_level > 0 %} (Level {{ ship_armor_level }}){% endif %}" /></td> <td><img src="{{ ship_armor_url }}" class="{{ 'acquired' if 'Progressive Ship Armor' in acquired_items }}" title="Progressive Ship Armor{% if ship_armor_level > 0 %} (Level {{ ship_armor_level }}){% endif %}" /></td>
<td colspan="2"></td>
<td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></td>
<td><img src="{{ icons['Vanadium Plating'] }}" class="{{ 'acquired' if 'Vanadium Plating' in acquired_items }}" title="Vanadium Plating" /></td>
</tr> </tr>
<tr> <tr>
<td colspan="15" class="title"> <td colspan="10" class="title">
Base Base
</td> </td>
</tr> </tr>
<tr> <tr>
<td><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td> <td colspan="2"><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td>
<td colspan="2"><img src="{{ icons['Missile Turret'] }}" class="{{ 'acquired' if 'Missile Turret' in acquired_items }}" title="Missile Turret" /></td>
<td colspan="2"><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
</tr>
<tr>
<td><img src="{{ icons['Projectile Accelerator (Bunker)'] }}" class="{{ 'acquired' if 'Projectile Accelerator (Bunker)' in acquired_items }}" title="Projectile Accelerator (Bunker)" /></td> <td><img src="{{ icons['Projectile Accelerator (Bunker)'] }}" class="{{ 'acquired' if 'Projectile Accelerator (Bunker)' in acquired_items }}" title="Projectile Accelerator (Bunker)" /></td>
<td><img src="{{ icons['Neosteel Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Neosteel Bunker (Bunker)' in acquired_items }}" title="Neosteel Bunker (Bunker)" /></td> <td><img src="{{ icons['Neosteel Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Neosteel Bunker (Bunker)' in acquired_items }}" title="Neosteel Bunker (Bunker)" /></td>
<td><img src="{{ icons['Shrike Turret (Bunker)'] }}" class="{{ 'acquired' if 'Shrike Turret (Bunker)' in acquired_items }}" title="Shrike Turret (Bunker)" /></td>
<td><img src="{{ icons['Fortified Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Fortified Bunker (Bunker)' in acquired_items }}" title="Fortified Bunker (Bunker)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Missile Turret'] }}" class="{{ 'acquired' if 'Missile Turret' in acquired_items }}" title="Missile Turret" /></td>
<td><img src="{{ icons['Titanium Housing (Missile Turret)'] }}" class="{{ 'acquired' if 'Titanium Housing (Missile Turret)' in acquired_items }}" title="Titanium Housing (Missile Turret)" /></td> <td><img src="{{ icons['Titanium Housing (Missile Turret)'] }}" class="{{ 'acquired' if 'Titanium Housing (Missile Turret)' in acquired_items }}" title="Titanium Housing (Missile Turret)" /></td>
<td><img src="{{ icons['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td> <td><img src="{{ icons['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td>
</tr> <td colspan="2">&nbsp;</td>
<tr>
<td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
<td></td>
<td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td> <td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td>
<td><img src="{{ icons['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></td> <td><img src="{{ icons['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></td>
<td></td> <td><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
<td><img src="{{ icons['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td> <td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
<td></td>
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
<td></td>
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
</tr> </tr>
<tr> <tr>
<td><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td> <td colspan="10" class="title">
<td></td>
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
<td></td>
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
<td></td>
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
</tr>
<tr>
<td colspan="7" class="title">
Infantry Infantry
</td> </td>
<td></td> </tr>
<td colspan="7" class="title"> <tr>
<td colspan="2"><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
<td colspan="2"><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
<td colspan="2"><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
<td colspan="2"><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
<td colspan="2"><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
</tr>
<tr>
<td><img src="{{ icons['Stimpack (Marine)'] }}" class="{{ 'acquired' if 'Stimpack (Marine)' in acquired_items }}" title="Stimpack (Marine)" /></td>
<td><img src="{{ icons['Combat Shield (Marine)'] }}" class="{{ 'acquired' if 'Combat Shield (Marine)' in acquired_items }}" title="Combat Shield (Marine)" /></td>
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
</tr>
<tr>
<td colspan="10" class="title">
Vehicles Vehicles
</td> </td>
</tr> </tr>
<tr> <tr>
<td><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td> <td colspan="2"><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
<td><img src="{{ stimpack_marine_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marine)' in acquired_items }}" title="{{ stimpack_marine_name }}" /></td> <td colspan="2"><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
<td><img src="{{ icons['Combat Shield (Marine)'] }}" class="{{ 'acquired' if 'Combat Shield (Marine)' in acquired_items }}" title="Combat Shield (Marine)" /></td> <td colspan="2"><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
<td><img src="{{ icons['Laser Targeting System (Marine)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marine)' in acquired_items }}" title="Laser Targeting System (Marine)" /></td> <td colspan="2"><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
<td><img src="{{ icons['Magrail Munitions (Marine)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marine)' in acquired_items }}" title="Magrail Munitions (Marine)" /></td> <td colspan="2"><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
<td><img src="{{ icons['Optimized Logistics (Marine)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Marine)' in acquired_items }}" title="Optimized Logistics (Marine)" /></td> </tr>
<td colspan="2"></td> <tr>
<td><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
<td><img src="{{ icons['Twin-Linked Flamethrower (Hellion)'] }}" class="{{ 'acquired' if 'Twin-Linked Flamethrower (Hellion)' in acquired_items }}" title="Twin-Linked Flamethrower (Hellion)" /></td> <td><img src="{{ icons['Twin-Linked Flamethrower (Hellion)'] }}" class="{{ 'acquired' if 'Twin-Linked Flamethrower (Hellion)' in acquired_items }}" title="Twin-Linked Flamethrower (Hellion)" /></td>
<td><img src="{{ icons['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (Hellion)" /></td> <td><img src="{{ icons['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (Hellion)" /></td>
<td><img src="{{ icons['Hellbat Aspect (Hellion)'] }}" class="{{ 'acquired' if 'Hellbat Aspect (Hellion)' in acquired_items }}" title="Hellbat Aspect (Hellion)" /></td> <td><img src="{{ icons['Cerberus Mine (Vulture)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Vulture)' in acquired_items }}" title="Cerberus Mine (Vulture)" /></td>
<td><img src="{{ icons['Smart Servos (Hellion)'] }}" class="{{ 'acquired' if 'Smart Servos (Hellion)' in acquired_items }}" title="Smart Servos (Hellion)" /></td>
<td><img src="{{ icons['Optimized Logistics (Hellion)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Hellion)' in acquired_items }}" title="Optimized Logistics (Hellion)" /></td>
<td><img src="{{ icons['Jump Jets (Hellion)'] }}" class="{{ 'acquired' if 'Jump Jets (Hellion)' in acquired_items }}" title="Jump Jets (Hellion)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
<td><img src="{{ icons['Restoration (Medic)'] }}" class="{{ 'acquired' if 'Restoration (Medic)' in acquired_items }}" title="Restoration (Medic)" /></td>
<td><img src="{{ icons['Optical Flare (Medic)'] }}" class="{{ 'acquired' if 'Optical Flare (Medic)' in acquired_items }}" title="Optical Flare (Medic)" /></td>
<td><img src="{{ icons['Optimized Logistics (Medic)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Medic)' in acquired_items }}" title="Optimized Logistics (Medic)" /></td>
<td colspan="2"></td>
<td></td>
<td><img src="{{ stimpack_hellion_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Hellion)' in acquired_items }}" title="{{ stimpack_hellion_name }}" /></td>
</tr>
<tr>
<td><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
<td><img src="{{ stimpack_firebat_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Firebat)' in acquired_items }}" title="{{ stimpack_firebat_name }}" /></td>
<td><img src="{{ icons['Optimized Logistics (Firebat)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Firebat)' in acquired_items }}" title="Optimized Logistics (Firebat)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
<td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td> <td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td>
<td><img src="{{ icons['Ion Thrusters (Vulture)'] }}" class="{{ 'acquired' if 'Ion Thrusters (Vulture)' in acquired_items }}" title="Ion Thrusters (Vulture)" /></td>
<td><img src="{{ icons['Auto Launchers (Vulture)'] }}" class="{{ 'acquired' if 'Auto Launchers (Vulture)' in acquired_items }}" title="Auto Launchers (Vulture)" /></td>
<td></td>
<td><img src="{{ icons['Cerberus Mine (Spider Mine)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Spider Mine)' in acquired_items }}" title="Cerberus Mine (Spider Mine)" /></td>
<td><img src="{{ icons['High Explosive Munition (Spider Mine)'] }}" class="{{ 'acquired' if 'High Explosive Munition (Spider Mine)' in acquired_items }}" title="High Explosive Munition (Spider Mine)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
<td><img src="{{ stimpack_marauder_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marauder)' in acquired_items }}" title="{{ stimpack_marauder_name }}" /></td>
<td><img src="{{ icons['Laser Targeting System (Marauder)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marauder)' in acquired_items }}" title="Laser Targeting System (Marauder)" /></td>
<td><img src="{{ icons['Magrail Munitions (Marauder)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marauder)' in acquired_items }}" title="Magrail Munitions (Marauder)" /></td>
<td><img src="{{ icons['Internal Tech Module (Marauder)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Marauder)' in acquired_items }}" title="Internal Tech Module (Marauder)" /></td>
<td></td>
<td><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
<td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td> <td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td>
<td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td> <td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td>
<td><img src="{{ icons['Jump Jets (Goliath)'] }}" class="{{ 'acquired' if 'Jump Jets (Goliath)' in acquired_items }}" title="Jump Jets (Goliath)" /></td>
<td><img src="{{ icons['Optimized Logistics (Goliath)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Goliath)' in acquired_items }}" title="Optimized Logistics (Goliath)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
<td><img src="{{ stimpack_reaper_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Reaper)' in acquired_items }}" title="{{ stimpack_reaper_name }}" /></td>
<td><img src="{{ icons['Laser Targeting System (Reaper)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Reaper)' in acquired_items }}" title="Laser Targeting System (Reaper)" /></td>
<td><img src="{{ icons['Advanced Cloaking Field (Reaper)'] }}" class="{{ 'acquired' if 'Advanced Cloaking Field (Reaper)' in acquired_items }}" title="Advanced Cloaking Field (Reaper)" /></td>
<td><img src="{{ icons['Spider Mines (Reaper)'] }}" class="{{ 'acquired' if 'Spider Mines (Reaper)' in acquired_items }}" title="Spider Mines (Reaper)" /></td>
<td></td>
<td><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td> <td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td> <td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
<td><img src="{{ icons['Hyperfluxor (Diamondback)'] }}" class="{{ 'acquired' if 'Hyperfluxor (Diamondback)' in acquired_items }}" title="Hyperfluxor (Diamondback)" /></td>
<td><img src="{{ icons['Burst Capacitors (Diamondback)'] }}" class="{{ 'acquired' if 'Burst Capacitors (Diamondback)' in acquired_items }}" title="Burst Capacitors (Diamondback)" /></td>
<td><img src="{{ icons['Optimized Logistics (Diamondback)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Diamondback)' in acquired_items }}" title="Optimized Logistics (Diamondback)" /></td>
</tr>
<tr>
<td></td>
<td><img src="{{ icons['Combat Drugs (Reaper)'] }}" class="{{ 'acquired' if 'Combat Drugs (Reaper)' in acquired_items }}" title="Combat Drugs (Reaper)" /></td>
<td colspan="6"></td>
<td><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td> <td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td> <td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
<td><img src="{{ icons['Jump Jets (Siege Tank)'] }}" class="{{ 'acquired' if 'Jump Jets (Siege Tank)' in acquired_items }}" title="Jump Jets (Siege Tank)" /></td>
<td><img src="{{ icons['Spider Mines (Siege Tank)'] }}" class="{{ 'acquired' if 'Spider Mines (Siege Tank)' in acquired_items }}" title="Spider Mines (Siege Tank)" /></td>
<td><img src="{{ icons['Smart Servos (Siege Tank)'] }}" class="{{ 'acquired' if 'Smart Servos (Siege Tank)' in acquired_items }}" title="Smart Servos (Siege Tank)" /></td>
<td><img src="{{ icons['Graduating Range (Siege Tank)'] }}" class="{{ 'acquired' if 'Graduating Range (Siege Tank)' in acquired_items }}" title="Graduating Range (Siege Tank)" /></td>
</tr> </tr>
<tr> <tr>
<td><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td> <td colspan="10" class="title">
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
<td><img src="{{ icons['EMP Rounds (Ghost)'] }}" class="{{ 'acquired' if 'EMP Rounds (Ghost)' in acquired_items }}" title="EMP Rounds (Ghost)" /></td>
<td><img src="{{ icons['Lockdown (Ghost)'] }}" class="{{ 'acquired' if 'Lockdown (Ghost)' in acquired_items }}" title="Lockdown (Ghost)" /></td>
<td colspan="3"></td>
<td></td>
<td><img src="{{ icons['Laser Targeting System (Siege Tank)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Siege Tank)' in acquired_items }}" title="Laser Targeting System (Siege Tank)" /></td>
<td><img src="{{ icons['Advanced Siege Tech (Siege Tank)'] }}" class="{{ 'acquired' if 'Advanced Siege Tech (Siege Tank)' in acquired_items }}" title="Advanced Siege Tech (Siege Tank)" /></td>
<td><img src="{{ icons['Internal Tech Module (Siege Tank)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Siege Tank)' in acquired_items }}" title="Internal Tech Module (Siege Tank)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
<td><img src="{{ icons['Impaler Rounds (Spectre)'] }}" class="{{ 'acquired' if 'Impaler Rounds (Spectre)' in acquired_items }}" title="Impaler Rounds (Spectre)" /></td>
<td colspan="4"></td>
<td><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
<td><img src="{{ high_impact_payload_thor_url }}" class="{{ 'acquired' if 'Progressive High Impact Payload (Thor)' in acquired_items }}" title="{{ high_impact_payload_thor_name }}" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
<td><img src="{{ icons['Optimized Logistics (Predator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Predator)' in acquired_items }}" title="Optimized Logistics (Predator)" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Widow Mine'] }}" class="{{ 'acquired' if 'Widow Mine' in acquired_items }}" title="Widow Mine" /></td>
<td><img src="{{ icons['Drilling Claws (Widow Mine)'] }}" class="{{ 'acquired' if 'Drilling Claws (Widow Mine)' in acquired_items }}" title="Drilling Claws (Widow Mine)" /></td>
<td><img src="{{ icons['Concealment (Widow Mine)'] }}" class="{{ 'acquired' if 'Concealment (Widow Mine)' in acquired_items }}" title="Concealment (Widow Mine)" /></td>
<td><img src="{{ icons['Black Market Launchers (Widow Mine)'] }}" class="{{ 'acquired' if 'Black Market Launchers (Widow Mine)' in acquired_items }}" title="Black Market Launchers (Widow Mine)" /></td>
<td><img src="{{ icons['Executioner Missiles (Widow Mine)'] }}" class="{{ 'acquired' if 'Executioner Missiles (Widow Mine)' in acquired_items }}" title="Executioner Missiles (Widow Mine)" /></td>
</tr>
<tr>
<td colspan="8"></td>
<td><img src="{{ icons['Cyclone'] }}" class="{{ 'acquired' if 'Cyclone' in acquired_items }}" title="Cyclone" /></td>
<td><img src="{{ icons['Mag-Field Accelerators (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Accelerators (Cyclone)' in acquired_items }}" title="Mag-Field Accelerators (Cyclone)" /></td>
<td><img src="{{ icons['Mag-Field Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Launchers (Cyclone)' in acquired_items }}" title="Mag-Field Launchers (Cyclone)" /></td>
<td><img src="{{ icons['Targeting Optics (Cyclone)'] }}" class="{{ 'acquired' if 'Targeting Optics (Cyclone)' in acquired_items }}" title="Targeting Optics (Cyclone)" /></td>
<td><img src="{{ icons['Rapid Fire Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Rapid Fire Launchers (Cyclone)' in acquired_items }}" title="Rapid Fire Launchers (Cyclone)" /></td>
</tr>
<tr>
<td colspan="15" class="title">
Starships Starships
</td> </td>
</tr> </tr>
<tr> <tr>
<td><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td> <td colspan="2"><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td>
<td colspan="2"><img src="{{ icons['Wraith'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Wraith" /></td>
<td colspan="2"><img src="{{ icons['Viking'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Viking" /></td>
<td colspan="2"><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
<td colspan="2"><img src="{{ icons['Battlecruiser'] }}" class="{{ 'acquired' if 'Battlecruiser' in acquired_items }}" title="Battlecruiser" /></td>
</tr>
<tr>
<td><img src="{{ icons['Rapid Deployment Tube (Medivac)'] }}" class="{{ 'acquired' if 'Rapid Deployment Tube (Medivac)' in acquired_items }}" title="Rapid Deployment Tube (Medivac)" /></td> <td><img src="{{ icons['Rapid Deployment Tube (Medivac)'] }}" class="{{ 'acquired' if 'Rapid Deployment Tube (Medivac)' in acquired_items }}" title="Rapid Deployment Tube (Medivac)" /></td>
<td><img src="{{ icons['Advanced Healing AI (Medivac)'] }}" class="{{ 'acquired' if 'Advanced Healing AI (Medivac)' in acquired_items }}" title="Advanced Healing AI (Medivac)" /></td> <td><img src="{{ icons['Advanced Healing AI (Medivac)'] }}" class="{{ 'acquired' if 'Advanced Healing AI (Medivac)' in acquired_items }}" title="Advanced Healing AI (Medivac)" /></td>
<td><img src="{{ icons['Expanded Hull (Medivac)'] }}" class="{{ 'acquired' if 'Expanded Hull (Medivac)' in acquired_items }}" title="Expanded Hull (Medivac)" /></td>
<td><img src="{{ icons['Afterburners (Medivac)'] }}" class="{{ 'acquired' if 'Afterburners (Medivac)' in acquired_items }}" title="Afterburners (Medivac)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Raven'] }}" class="{{ 'acquired' if 'Raven' in acquired_items }}" title="Raven" /></td>
<td><img src="{{ icons['Bio Mechanical Repair Drone (Raven)'] }}" class="{{ 'acquired' if 'Bio Mechanical Repair Drone (Raven)' in acquired_items }}" title="Bio Mechanical Repair Drone (Raven)" /></td>
<td><img src="{{ icons['Spider Mines (Raven)'] }}" class="{{ 'acquired' if 'Spider Mines (Raven)' in acquired_items }}" title="Spider Mines (Raven)" /></td>
<td><img src="{{ icons['Railgun Turret (Raven)'] }}" class="{{ 'acquired' if 'Railgun Turret (Raven)' in acquired_items }}" title="Railgun Turret (Raven)" /></td>
<td><img src="{{ icons['Hunter-Seeker Weapon (Raven)'] }}" class="{{ 'acquired' if 'Hunter-Seeker Weapon (Raven)' in acquired_items }}" title="Hunter-Seeker Weapon (Raven)" /></td>
<td><img src="{{ icons['Interference Matrix (Raven)'] }}" class="{{ 'acquired' if 'Interference Matrix (Raven)' in acquired_items }}" title="Interference Matrix (Raven)" /></td>
<td><img src="{{ icons['Anti-Armor Missile (Raven)'] }}" class="{{ 'acquired' if 'Anti-Armor Missile (Raven)' in acquired_items }}" title="Anti-Armor Missile (Raven)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Wraith'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Wraith" /></td>
<td><img src="{{ icons['Tomahawk Power Cells (Wraith)'] }}" class="{{ 'acquired' if 'Tomahawk Power Cells (Wraith)' in acquired_items }}" title="Tomahawk Power Cells (Wraith)" /></td> <td><img src="{{ icons['Tomahawk Power Cells (Wraith)'] }}" class="{{ 'acquired' if 'Tomahawk Power Cells (Wraith)' in acquired_items }}" title="Tomahawk Power Cells (Wraith)" /></td>
<td><img src="{{ icons['Displacement Field (Wraith)'] }}" class="{{ 'acquired' if 'Displacement Field (Wraith)' in acquired_items }}" title="Displacement Field (Wraith)" /></td> <td><img src="{{ icons['Displacement Field (Wraith)'] }}" class="{{ 'acquired' if 'Displacement Field (Wraith)' in acquired_items }}" title="Displacement Field (Wraith)" /></td>
<td><img src="{{ icons['Advanced Laser Technology (Wraith)'] }}" class="{{ 'acquired' if 'Advanced Laser Technology (Wraith)' in acquired_items }}" title="Advanced Laser Technology (Wraith)" /></td>
<td colspan="4"></td>
<td></td>
<td><img src="{{ icons['Internal Tech Module (Raven)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Raven)' in acquired_items }}" title="Internal Tech Module (Raven)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Viking'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Viking" /></td>
<td><img src="{{ icons['Ripwave Missiles (Viking)'] }}" class="{{ 'acquired' if 'Ripwave Missiles (Viking)' in acquired_items }}" title="Ripwave Missiles (Viking)" /></td> <td><img src="{{ icons['Ripwave Missiles (Viking)'] }}" class="{{ 'acquired' if 'Ripwave Missiles (Viking)' in acquired_items }}" title="Ripwave Missiles (Viking)" /></td>
<td><img src="{{ icons['Phobos-Class Weapons System (Viking)'] }}" class="{{ 'acquired' if 'Phobos-Class Weapons System (Viking)' in acquired_items }}" title="Phobos-Class Weapons System (Viking)" /></td> <td><img src="{{ icons['Phobos-Class Weapons System (Viking)'] }}" class="{{ 'acquired' if 'Phobos-Class Weapons System (Viking)' in acquired_items }}" title="Phobos-Class Weapons System (Viking)" /></td>
<td><img src="{{ icons['Smart Servos (Viking)'] }}" class="{{ 'acquired' if 'Smart Servos (Viking)' in acquired_items }}" title="Smart Servos (Viking)" /></td> <td><img src="{{ icons['Cross-Spectrum Dampeners (Banshee)'] }}" class="{{ 'acquired' if 'Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="Cross-Spectrum Dampeners (Banshee)" /></td>
<td><img src="{{ icons['Magrail Munitions (Viking)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Viking)' in acquired_items }}" title="Magrail Munitions (Viking)" /></td>
<td colspan="3"></td>
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
<td><img src="{{ icons['EMP Shockwave (Science Vessel)'] }}" class="{{ 'acquired' if 'EMP Shockwave (Science Vessel)' in acquired_items }}" title="EMP Shockwave (Science Vessel)" /></td>
<td><img src="{{ icons['Defensive Matrix (Science Vessel)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Science Vessel)' in acquired_items }}" title="Defensive Matrix (Science Vessel)" /></td>
</tr>
<tr>
<td><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
<td><img src="{{ crossspectrum_dampeners_banshee_url }}" class="{{ 'acquired' if 'Progressive Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="{{ crossspectrum_dampeners_banshee_name }}" /></td>
<td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></td> <td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></td>
<td><img src="{{ icons['Hyperflight Rotors (Banshee)'] }}" class="{{ 'acquired' if 'Hyperflight Rotors (Banshee)' in acquired_items }}" title="Hyperflight Rotors (Banshee)" /></td>
<td><img src="{{ icons['Laser Targeting System (Banshee)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Banshee)' in acquired_items }}" title="Laser Targeting System (Banshee)" /></td>
<td><img src="{{ icons['Internal Tech Module (Banshee)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Banshee)' in acquired_items }}" title="Internal Tech Module (Banshee)" /></td>
<td colspan="2"></td>
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
</tr>
<tr>
<td><img src="{{ icons['Battlecruiser'] }}" class="{{ 'acquired' if 'Battlecruiser' in acquired_items }}" title="Battlecruiser" /></td>
<td><img src="{{ icons['Missile Pods (Battlecruiser)'] }}" class="{{ 'acquired' if 'Missile Pods (Battlecruiser)' in acquired_items }}" title="Missile Pods (Battlecruiser)" /></td> <td><img src="{{ icons['Missile Pods (Battlecruiser)'] }}" class="{{ 'acquired' if 'Missile Pods (Battlecruiser)' in acquired_items }}" title="Missile Pods (Battlecruiser)" /></td>
<td><img src="{{ icons['Defensive Matrix (Battlecruiser)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Battlecruiser)' in acquired_items }}" title="Defensive Matrix (Battlecruiser)" /></td> <td><img src="{{ icons['Defensive Matrix (Battlecruiser)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Battlecruiser)' in acquired_items }}" title="Defensive Matrix (Battlecruiser)" /></td>
<td><img src="{{ icons['Tactical Jump (Battlecruiser)'] }}" class="{{ 'acquired' if 'Tactical Jump (Battlecruiser)' in acquired_items }}" title="Tactical Jump (Battlecruiser)" /></td>
<td><img src="{{ icons['Cloak (Battlecruiser)'] }}" class="{{ 'acquired' if 'Cloak (Battlecruiser)' in acquired_items }}" title="Cloak (Battlecruiser)" /></td>
<td><img src="{{ icons['ATX Laser Battery (Battlecruiser)'] }}" class="{{ 'acquired' if 'ATX Laser Battery (Battlecruiser)' in acquired_items }}" title="ATX Laser Battery (Battlecruiser)" /></td>
<td><img src="{{ icons['Optimized Logistics (Battlecruiser)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Battlecruiser)' in acquired_items }}" title="Optimized Logistics (Battlecruiser)" /></td>
<td></td>
<td><img src="{{ icons['Liberator'] }}" class="{{ 'acquired' if 'Liberator' in acquired_items }}" title="Liberator" /></td>
<td><img src="{{ icons['Advanced Ballistics (Liberator)'] }}" class="{{ 'acquired' if 'Advanced Ballistics (Liberator)' in acquired_items }}" title="Advanced Ballistics (Liberator)" /></td>
<td><img src="{{ icons['Raid Artillery (Liberator)'] }}" class="{{ 'acquired' if 'Raid Artillery (Liberator)' in acquired_items }}" title="Raid Artillery (Liberator)" /></td>
<td><img src="{{ icons['Cloak (Liberator)'] }}" class="{{ 'acquired' if 'Cloak (Liberator)' in acquired_items }}" title="Cloak (Liberator)" /></td>
<td><img src="{{ icons['Laser Targeting System (Liberator)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Liberator)' in acquired_items }}" title="Laser Targeting System (Liberator)" /></td>
<td><img src="{{ icons['Optimized Logistics (Liberator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Liberator)' in acquired_items }}" title="Optimized Logistics (Liberator)" /></td>
</tr> </tr>
<tr> <tr>
<td></td> <td colspan="10" class="title">
<td><img src="{{ icons['Internal Tech Module (Battlecruiser)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Battlecruiser)' in acquired_items }}" title="Internal Tech Module (Battlecruiser)" /></td> Dominion
<td colspan="6"></td> </td>
<td><img src="{{ icons['Valkyrie'] }}" class="{{ 'acquired' if 'Valkyrie' in acquired_items }}" title="Valkyrie" /></td>
<td><img src="{{ icons['Enhanced Cluster Launchers (Valkyrie)'] }}" class="{{ 'acquired' if 'Enhanced Cluster Launchers (Valkyrie)' in acquired_items }}" title="Enhanced Cluster Launchers (Valkyrie)" /></td>
<td><img src="{{ icons['Shaped Hull (Valkyrie)'] }}" class="{{ 'acquired' if 'Shaped Hull (Valkyrie)' in acquired_items }}" title="Shaped Hull (Valkyrie)" /></td>
<td><img src="{{ icons['Burst Lasers (Valkyrie)'] }}" class="{{ 'acquired' if 'Burst Lasers (Valkyrie)' in acquired_items }}" title="Burst Lasers (Valkyrie)" /></td>
<td><img src="{{ icons['Afterburners (Valkyrie)'] }}" class="{{ 'acquired' if 'Afterburners (Valkyrie)' in acquired_items }}" title="Afterburners (Valkyrie)" /></td>
</tr> </tr>
<tr> <tr>
<td colspan="15" class="title"> <td colspan="2"><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
<td colspan="2"><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
<td colspan="2"><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
</tr>
<tr>
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
</tr>
<tr>
<td colspan="10" class="title">
Mercenaries Mercenaries
</td> </td>
</tr> </tr>
@@ -311,18 +165,36 @@
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td> <td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
</tr> </tr>
<tr> <tr>
<td colspan="15" class="title"> <td colspan="10" class="title">
General Upgrades Lab Upgrades
</td> </td>
</tr> </tr>
<tr> <tr>
<td><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td> <td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></td>
<td><img src="{{ icons['Vanadium Plating'] }}" class="{{ 'acquired' if 'Vanadium Plating' in acquired_items }}" title="Vanadium Plating" /></td>
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
<td><img src="{{ icons['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td>
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
<td><img src="{{ icons['Raven'] }}" class="{{ 'acquired' if 'Raven' in acquired_items }}" title="Raven" /></td>
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
<td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td> <td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td>
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
<td><img src="{{ regenerative_biosteel_url }}" class="{{ 'acquired' if 'Progressive Regenerative Bio-Steel' in acquired_items }}" title="Progressive Regenerative Bio-Steel{% if regenerative_biosteel_level > 0 %} (Level {{ regenerative_biosteel_level }}){% endif %}" /></td>
</tr> </tr>
<tr> <tr>
<td colspan="15" class="title"> <td><img src="{{ icons['Shrike Turret'] }}" class="{{ 'acquired' if 'Shrike Turret' in acquired_items }}" title="Shrike Turret" /></td>
<td><img src="{{ icons['Fortified Bunker'] }}" class="{{ 'acquired' if 'Fortified Bunker' in acquired_items }}" title="Fortified Bunker" /></td>
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
<td><img src="{{ icons['Regenerative Bio-Steel'] }}" class="{{ 'acquired' if 'Regenerative Bio-Steel' in acquired_items }}" title="Regenerative Bio-Steel" /></td>
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
</tr>
<tr>
<td colspan="10" class="title">
Protoss Units Protoss Units
</td> </td>
</tr> </tr>

View File

@@ -4,27 +4,16 @@
<title>Supported Games</title> <title>Supported Games</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" />
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/supportedGames.js") }}"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% include 'header/oceanHeader.html' %} {% include 'header/oceanHeader.html' %}
<div id="games" class="markdown"> <div id="games" class="markdown">
<h1>Currently Supported Games</h1> <h1>Currently Supported Games</h1>
<div>
<label for="game-search">Search for your game below!</label><br />
<div id="page-controls">
<input id="game-search" placeholder="Search by title..." autofocus />
<button id="expand-all">Expand All</button>
<button id="collapse-all">Collapse All</button>
</div>
</div>
{% for game_name in worlds | title_sorted %} {% for game_name in worlds | title_sorted %}
{% set world = worlds[game_name] %} {% set world = worlds[game_name] %}
<h2 class="collapse-toggle" data-game="{{ game_name }}"> <h2>{{ game_name }}</h2>
<span id="{{ game_name }}-arrow" class="collapse-arrow"></span>&nbsp;{{ game_name }} <p>
</h2>
<p id="{{ game_name }}" class="collapsed">
{{ world.__doc__ | default("No description provided.", true) }}<br /> {{ world.__doc__ | default("No description provided.", true) }}<br />
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a> <a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %} {% if world.web.tutorials %}

View File

@@ -1,17 +1,18 @@
import collections import collections
import datetime import datetime
import typing import typing
import pkgutil
from typing import Counter, Optional, Dict, Any, Tuple, List from typing import Counter, Optional, Dict, Any, Tuple, List
from uuid import UUID from uuid import UUID
from flask import render_template from flask import render_template
from jinja2 import pass_context, runtime from jinja2 import pass_context, runtime, Template
from werkzeug.exceptions import abort from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second from MultiServer import Context, get_saving_second
from NetUtils import ClientStatus, SlotType, NetworkSlot from NetUtils import SlotType, NetworkSlot
from Utils import restricted_loads from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, AutoWorldRegister
from worlds.alttp import Items from worlds.alttp import Items
from . import app, cache from . import app, cache
from .models import GameDataPackage, Room from .models import GameDataPackage, Room
@@ -990,7 +991,6 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
SC2WOL_LOC_ID_OFFSET = 1000 SC2WOL_LOC_ID_OFFSET = 1000
SC2WOL_ITEM_ID_OFFSET = 1000 SC2WOL_ITEM_ID_OFFSET = 1000
icons = { icons = {
"Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png", "Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
"Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png", "Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
@@ -1035,36 +1035,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg", "Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
"Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png", "Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
"Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png",
"Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png", "Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png",
"Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png",
"Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png",
"Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png",
"Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png", "Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png",
"Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png", "Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png",
"Restoration (Medic)": "/static/static/icons/sc2/restoration.png",
"Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png",
"Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png",
"Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png", "Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png",
"Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png", "Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png",
"Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
"Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png",
"Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png",
"Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png", "Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png",
"Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png", "Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png",
"Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
"Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png",
"Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png",
"Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png",
"Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png",
"U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png", "U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png",
"G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png", "G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png",
"Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
"Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png",
"Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png",
"Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png",
"Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png",
"Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png",
"Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg", "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
"Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg", "Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg",
@@ -1074,35 +1053,14 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png", "Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
"Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png", "Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png",
"Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png", "Cerberus Mine (Vulture)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
"Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png",
"Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png",
"Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png",
"Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
"Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png",
"Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
"High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png",
"Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png", "Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png",
"Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png",
"Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png",
"Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png", "Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png",
"Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png", "Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png",
"Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png",
"Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png",
"Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png", "Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png",
"Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png", "Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
"Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png",
"Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png",
"Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png",
"Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png", "Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png",
"Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png", "Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png",
"Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png",
"Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png",
"Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png",
"Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png",
"Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png",
"Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png",
"Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg", "Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg",
"Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg", "Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg",
@@ -1112,77 +1070,25 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png", "Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png",
"Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png", "Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png",
"Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png",
"Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png",
"Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png", "Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png",
"Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png", "Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png",
"Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png",
"Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png", "Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png",
"Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png", "Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png",
"Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png", "Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
"Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png",
"Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png",
"Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
"Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png", "Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png",
"Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png",
"Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png",
"Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png", "Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png",
"Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png", "Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
"Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png",
"Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png",
"ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png",
"Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png",
"Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png",
"Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg", "Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg",
"Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg", "Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg",
"Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg", "Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg",
"Widow Mine": "/static/static/icons/sc2/widowmine.png",
"Cyclone": "/static/static/icons/sc2/cyclone.png",
"Liberator": "/static/static/icons/sc2/liberator.png",
"Valkyrie": "/static/static/icons/sc2/valkyrie.png",
"Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png", "Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png",
"Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png", "Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png",
"EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png",
"Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png",
"Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png", "Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png",
"Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png", "Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png",
"Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png",
"330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png", "330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png",
"Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png", "Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png",
"High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png",
"Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png",
"Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png",
"Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png",
"Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png",
"Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png",
"Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png",
"Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png",
"Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png",
"Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png",
"Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png",
"Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png",
"Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png",
"Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png",
"Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png",
"Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png",
"Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png",
"Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png",
"EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png",
"Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
"Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png",
"Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png",
"Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png",
"Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png",
"Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png",
"Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png",
"Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
"Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png",
"Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.png",
"War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg", "War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg",
"Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg", "Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg",
@@ -1204,15 +1110,14 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png", "Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png",
"Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png", "Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png",
"Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png", "Shrike Turret": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
"Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png", "Fortified Bunker": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
"Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png", "Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png",
"Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png", "Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png",
"Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png", "Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png",
"Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png", "Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png",
"Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png", "Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
"Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png", "Regenerative Bio-Steel": "https://static.wikia.nocookie.net/starcraft/images/d/d3/SC2_Lab_BioSteel_Icon.png",
"Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png",
"Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png", "Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png",
"Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png", "Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png",
@@ -1228,71 +1133,40 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Nothing": "", "Nothing": "",
} }
sc2wol_location_ids = { sc2wol_location_ids = {
"Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200), "Liberation Day": [SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 101, SC2WOL_LOC_ID_OFFSET + 102, SC2WOL_LOC_ID_OFFSET + 103, SC2WOL_LOC_ID_OFFSET + 104, SC2WOL_LOC_ID_OFFSET + 105, SC2WOL_LOC_ID_OFFSET + 106],
"The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300), "The Outlaws": [SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 201],
"Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400), "Zero Hour": [SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 301, SC2WOL_LOC_ID_OFFSET + 302, SC2WOL_LOC_ID_OFFSET + 303],
"Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500), "Evacuation": [SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 401, SC2WOL_LOC_ID_OFFSET + 402, SC2WOL_LOC_ID_OFFSET + 403],
"Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600), "Outbreak": [SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 501, SC2WOL_LOC_ID_OFFSET + 502],
"Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700), "Safe Haven": [SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 601, SC2WOL_LOC_ID_OFFSET + 602, SC2WOL_LOC_ID_OFFSET + 603],
"Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800), "Haven's Fall": [SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 701, SC2WOL_LOC_ID_OFFSET + 702, SC2WOL_LOC_ID_OFFSET + 703],
"Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900), "Smash and Grab": [SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 801, SC2WOL_LOC_ID_OFFSET + 802, SC2WOL_LOC_ID_OFFSET + 803, SC2WOL_LOC_ID_OFFSET + 804],
"The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000), "The Dig": [SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 901, SC2WOL_LOC_ID_OFFSET + 902, SC2WOL_LOC_ID_OFFSET + 903],
"The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100), "The Moebius Factor": [SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1003, SC2WOL_LOC_ID_OFFSET + 1004, SC2WOL_LOC_ID_OFFSET + 1005, SC2WOL_LOC_ID_OFFSET + 1006, SC2WOL_LOC_ID_OFFSET + 1007, SC2WOL_LOC_ID_OFFSET + 1008],
"Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200), "Supernova": [SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1101, SC2WOL_LOC_ID_OFFSET + 1102, SC2WOL_LOC_ID_OFFSET + 1103, SC2WOL_LOC_ID_OFFSET + 1104],
"Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300), "Maw of the Void": [SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1201, SC2WOL_LOC_ID_OFFSET + 1202, SC2WOL_LOC_ID_OFFSET + 1203, SC2WOL_LOC_ID_OFFSET + 1204, SC2WOL_LOC_ID_OFFSET + 1205],
"Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400), "Devil's Playground": [SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1301, SC2WOL_LOC_ID_OFFSET + 1302],
"Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500), "Welcome to the Jungle": [SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1401, SC2WOL_LOC_ID_OFFSET + 1402, SC2WOL_LOC_ID_OFFSET + 1403],
"Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600), "Breakout": [SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1501, SC2WOL_LOC_ID_OFFSET + 1502],
"Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700), "Ghost of a Chance": [SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1601, SC2WOL_LOC_ID_OFFSET + 1602, SC2WOL_LOC_ID_OFFSET + 1603, SC2WOL_LOC_ID_OFFSET + 1604, SC2WOL_LOC_ID_OFFSET + 1605],
"The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800), "The Great Train Robbery": [SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1701, SC2WOL_LOC_ID_OFFSET + 1702, SC2WOL_LOC_ID_OFFSET + 1703],
"Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900), "Cutthroat": [SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1801, SC2WOL_LOC_ID_OFFSET + 1802, SC2WOL_LOC_ID_OFFSET + 1803, SC2WOL_LOC_ID_OFFSET + 1804],
"Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000), "Engine of Destruction": [SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 1901, SC2WOL_LOC_ID_OFFSET + 1902, SC2WOL_LOC_ID_OFFSET + 1903, SC2WOL_LOC_ID_OFFSET + 1904, SC2WOL_LOC_ID_OFFSET + 1905],
"Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100), "Media Blitz": [SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2001, SC2WOL_LOC_ID_OFFSET + 2002, SC2WOL_LOC_ID_OFFSET + 2003, SC2WOL_LOC_ID_OFFSET + 2004],
"Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200), "Piercing the Shroud": [SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2101, SC2WOL_LOC_ID_OFFSET + 2102, SC2WOL_LOC_ID_OFFSET + 2103, SC2WOL_LOC_ID_OFFSET + 2104, SC2WOL_LOC_ID_OFFSET + 2105],
"Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300), "Whispers of Doom": [SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2201, SC2WOL_LOC_ID_OFFSET + 2202, SC2WOL_LOC_ID_OFFSET + 2203],
"A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400), "A Sinister Turn": [SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2301, SC2WOL_LOC_ID_OFFSET + 2302, SC2WOL_LOC_ID_OFFSET + 2303],
"Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500), "Echoes of the Future": [SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2401, SC2WOL_LOC_ID_OFFSET + 2402],
"In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600), "In Utter Darkness": [SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2501, SC2WOL_LOC_ID_OFFSET + 2502],
"Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700), "Gates of Hell": [SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2601],
"Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800), "Belly of the Beast": [SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2701, SC2WOL_LOC_ID_OFFSET + 2702, SC2WOL_LOC_ID_OFFSET + 2703],
"Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900), "Shatter the Sky": [SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2801, SC2WOL_LOC_ID_OFFSET + 2802, SC2WOL_LOC_ID_OFFSET + 2803, SC2WOL_LOC_ID_OFFSET + 2804, SC2WOL_LOC_ID_OFFSET + 2805],
} }
display_data = {} display_data = {}
# Grouped Items
grouped_item_ids = {
"Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET
}
grouped_item_replacements = {
"Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", "Progressive Ship Weapon"],
"Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", "Progressive Ship Armor"],
"Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"],
"Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"],
"Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"]
}
grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements["Progressive Weapon Upgrade"] + grouped_item_replacements["Progressive Armor Upgrade"]
replacement_item_ids = {
"Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
}
for grouped_item_name, grouped_item_id in grouped_item_ids.items():
count: int = inventory[grouped_item_id]
if count > 0:
for replacement_item in grouped_item_replacements[grouped_item_name]:
replacement_id: int = replacement_item_ids[replacement_item]
inventory[replacement_id] = count
# Determine display for progressive items # Determine display for progressive items
progressive_items = { progressive_items = {
"Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET, "Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
@@ -1300,15 +1174,7 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET, "Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET, "Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET, "Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET, "Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET
"Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET,
"Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET,
"Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET
} }
progressive_names = { progressive_names = {
"Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"], "Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"],
@@ -1316,27 +1182,14 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
"Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"], "Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"],
"Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"], "Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"],
"Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"], "Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"],
"Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"], "Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"]
"Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", "Super Stimpack (Marine)"],
"Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", "Super Stimpack (Firebat)"],
"Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", "Super Stimpack (Marauder)"],
"Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", "Super Stimpack (Reaper)"],
"Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", "Super Stimpack (Hellion)"],
"Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", "High Impact Payload (Thor)", "Smart Servos (Thor)"],
"Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", "Cross-Spectrum Dampeners (Banshee)", "Advanced Cross-Spectrum Dampeners (Banshee)"],
"Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 2"]
} }
for item_name, item_id in progressive_items.items(): for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name]) - 1) level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
display_name = progressive_names[item_name][level] display_name = progressive_names[item_name][level]
base_name = (item_name.split(maxsplit=1)[1].lower() base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
.replace(' ', '_')
.replace("-", "")
.replace("(", "")
.replace(")", ""))
display_data[base_name + "_level"] = level display_data[base_name + "_level"] = level
display_data[base_name + "_url"] = icons[display_name] display_data[base_name + "_url"] = icons[display_name]
display_data[base_name + "_name"] = display_name
# Multi-items # Multi-items
multi_items = { multi_items = {
@@ -1368,12 +1221,12 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
checks_in_area['Total'] = sum(checks_in_area.values()) checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("sc2wolTracker.html", return render_template("sc2wolTracker.html",
inventory=inventory, icons=icons, inventory=inventory, icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name}, id in lookup_any_item_id_to_name},
player=player, team=team, room=room, player_name=playerName, player=player, team=team, room=room, player_name=playerName,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info, checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data) **display_data)
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]], def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str, inventory: Counter, team: int, player: int, playerName: str,
@@ -1479,104 +1332,9 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
custom_items=custom_items, custom_locations=custom_locations) custom_items=custom_items, custom_locations=custom_locations)
def get_enabled_multiworld_trackers(room: Room, current: str): def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
enabled = [ inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
{ for teamnumber, team_data in data["checks_done"].items()}
"name": "Generic",
"endpoint": "get_multiworld_tracker",
"current": current == "Generic"
}
]
for game_name, endpoint in multi_trackers.items():
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
enabled.append({
"name": game_name,
"endpoint": endpoint.__name__,
"current": current == game_name}
)
return enabled
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
room: Room = Room.get(tracker=tracker)
if not room:
return None
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
get_static_room_data(room)
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
percent_total_checks_done = {teamnumber: {playernumber: 0
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
total_locations = {teamnumber: sum(len(locations[playernumber])
for playernumber in range(1, len(team) + 1) if playernumber not in groups)
for teamnumber, team in enumerate(names)}
hints = {team: set() for team in range(len(names))}
if room.multisave:
multisave = restricted_loads(room.multisave)
else:
multisave = {}
if "hints" in multisave:
for (team, slot), slot_hints in multisave["hints"].items():
hints[team] |= set(slot_hints)
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
if player in groups:
continue
player_locations = locations[player]
checks_done[team][player]["Total"] = len(locations_checked)
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
len(player_locations) * 100) \
if player_locations else 100
activity_timers = {}
now = datetime.datetime.utcnow()
for (team, player), timestamp in multisave.get("client_activity_timers", []):
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {}
completed_worlds = 0
states: typing.Dict[typing.Tuple[int, int], int] = {}
for team, names in enumerate(names):
for player, name in enumerate(names, 1):
player_names[team, player] = name
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
if states[team, player] == ClientStatus.CLIENT_GOAL and player not in groups:
completed_worlds += 1
long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[team, player] = alias
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
video = {}
for (team, player), data in multisave.get("video", []):
video[team, player] = data
return dict(
player_names=player_names, room=room, checks_done=checks_done,
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
activity_timers=activity_timers, video=video, hints=hints,
long_player_names=long_player_names,
multisave=multisave, precollected_items=precollected_items, groups=groups,
locations=locations, total_locations=total_locations, games=games, states=states,
completed_worlds=completed_worlds,
custom_locations=custom_locations, custom_items=custom_items,
)
def _get_inventory_data(data: typing.Dict[str, typing.Any]) \
-> typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]]:
inventory: typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]] = {
teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
for teamnumber, team_data in data["checks_done"].items()
}
groups = data["groups"] groups = data["groups"]
@@ -1594,47 +1352,6 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) \
inventory[team][recipient][item_id] += 1 inventory[team][recipient][item_id] += 1
return inventory return inventory
def _get_named_inventory(inventory: typing.Dict[int, int], custom_items: typing.Dict[int, str] = None) \
-> typing.Dict[str, int]:
"""slow"""
if custom_items:
mapping = collections.ChainMap(custom_items, lookup_any_item_id_to_name)
else:
mapping = lookup_any_item_id_to_name
return collections.Counter({mapping.get(item_id, None): count for item_id, count in inventory.items()})
@app.route('/tracker/<suuid:tracker>')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_multiworld_tracker(tracker: UUID):
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
return render_template("multiTracker.html", **data)
if "Factorio" in games:
@app.route('/tracker/<suuid:tracker>/Factorio')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_Factorio_multiworld_tracker(tracker: UUID):
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["inventory"] = _get_inventory_data(data)
data["named_inventory"] = {team_id : {
player_id: _get_named_inventory(inventory, data["custom_items"])
for player_id, inventory in team_inventory.items()
} for team_id, team_inventory in data["inventory"].items()}
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
return render_template("multiFactorioTracker.html", **data)
@app.route('/tracker/<suuid:tracker>/A Link to the Past') @app.route('/tracker/<suuid:tracker>/A Link to the Past')
@cache.memoize(timeout=60) # multisave is currently created at most every minute @cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_LttP_multiworld_tracker(tracker: UUID): def get_LttP_multiworld_tracker(tracker: UUID):
@@ -1683,7 +1400,7 @@ def get_LttP_multiworld_tracker(tracker: UUID):
for item_id in precollected: for item_id in precollected:
attribute_item(team, player, item_id) attribute_item(team, player, item_id)
for location in locations_checked: for location in locations_checked:
if location not in player_locations or location not in player_location_to_area.get(player, {}): if location not in player_locations or location not in player_location_to_area[player]:
continue continue
item, recipient, flags = player_locations[location] item, recipient, flags = player_locations[location]
recipients = groups.get(recipient, [recipient]) recipients = groups.get(recipient, [recipient])
@@ -1760,9 +1477,142 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker "Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
} }
# MultiTrackers
@app.route('/tracker/<suuid:tracker>')
@cache.memoize(timeout=60) # multisave is currently created at most every minute
def get_multiworld_tracker(tracker: UUID) -> str:
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
return render_template("multiTracker.html", **data)
def get_enabled_multiworld_trackers(room: Room, current: str) -> typing.List[typing.Dict[str, typing.Any]]:
enabled = [
{
"name": "Generic",
"endpoint": "get_multiworld_tracker",
"current": current == "Generic"
}
]
for game_name, endpoint in multi_trackers.items():
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
enabled.append({
"name": game_name,
"endpoint": endpoint.__name__,
"current": current == game_name}
)
return enabled
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
room: Room = Room.get(tracker=tracker)
if not room:
return None
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
get_static_room_data(room)
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
percent_total_checks_done = {teamnumber: {playernumber: 0
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
for teamnumber, team in enumerate(names)}
hints = {team: set() for team in range(len(names))}
if room.multisave:
multisave = restricted_loads(room.multisave)
else:
multisave = {}
if "hints" in multisave:
for (team, slot), slot_hints in multisave["hints"].items():
hints[team] |= set(slot_hints)
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
if player in groups:
continue
player_locations = locations[player]
checks_done[team][player]["Total"] = len(locations_checked)
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
len(player_locations) * 100) \
if player_locations else 100
activity_timers = {}
now = datetime.datetime.utcnow()
for (team, player), timestamp in multisave.get("client_activity_timers", []):
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {}
states: typing.Dict[typing.Tuple[int, int], int] = {}
for team, names in enumerate(names):
for player, name in enumerate(names, 1):
player_names[team, player] = name
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
long_player_names = player_names.copy()
for (team, player), alias in multisave.get("name_aliases", {}).items():
player_names[team, player] = alias
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
video = {}
for (team, player), data in multisave.get("video", []):
video[team, player] = data
return dict(
player_names=player_names, room=room, checks_done=checks_done,
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
activity_timers=activity_timers, video=video, hints=hints,
long_player_names=long_player_names,
multisave=multisave, precollected_items=precollected_items, groups=groups,
locations=locations, games=games, states=states,
custom_locations=custom_locations, custom_items=custom_items,
)
multi_trackers: typing.Dict[str, typing.Callable] = { multi_trackers: typing.Dict[str, typing.Callable] = {
"A Link to the Past": get_LttP_multiworld_tracker, "A Link to the Past": get_LttP_multiworld_tracker,
} }
if "Factorio" in games: class MultiTrackerData(typing.NamedTuple):
multi_trackers["Factorio"] = get_Factorio_multiworld_tracker template: Template
item_name_to_id: typing.Dict[str, int]
location_name_to_id: typing.Dict[str, int]
multi_tracker_data: typing.Dict[str, MultiTrackerData] = {}
@app.route("/tracker/<suuid:tracker>/<game>")
@cache.memoize(timeout=60) # multisave is currently created up to every minute
def get_game_multiworld_tracker(tracker: UUID, game: str) -> str:
current_multi_tracker_data = multi_tracker_data.get(game, None)
if not current_multi_tracker_data:
abort(404)
data = _get_multiworld_tracker_data(tracker)
if not data:
abort(404)
data["inventory"] = _get_inventory_data(data)
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], game)
data["item_name_to_id"] = current_multi_tracker_data.item_name_to_id
data["location_name_to_id"] = current_multi_tracker_data.location_name_to_id
return render_template(current_multi_tracker_data.template, **data)
def register_multitrackers() -> None:
for world in AutoWorldRegister.world_types.values():
multitracker = world.web.multitracker_template
if multitracker:
multitracker_template = pkgutil.get_data(world.__module__, multitracker).decode()
multitracker_template = app.jinja_env.from_string(multitracker_template)
multi_trackers[world.game] = get_game_multiworld_tracker
multi_tracker_data[world.game] = MultiTrackerData(
multitracker_template,
world.item_name_to_id,
world.location_name_to_id,
)
register_multitrackers()

View File

@@ -67,7 +67,6 @@ local itemsObtained = 0x0677
local takeAnyCavesChecked = 0x0678 local takeAnyCavesChecked = 0x0678
local localTriforce = 0x0679 local localTriforce = 0x0679
local bonusItemsObtained = 0x067A local bonusItemsObtained = 0x067A
local itemsObtainedHigh = 0x067B
itemAPids = { itemAPids = {
["Boomerang"] = 7100, ["Boomerang"] = 7100,
@@ -174,18 +173,11 @@ for key, value in pairs(itemAPids) do
itemIDNames[value] = key itemIDNames[value] = key
end end
local function getItemsObtained()
return bit.bor(bit.lshift(u8(itemsObtainedHigh), 8), u8(itemsObtained))
end
local function setItemsObtained(value)
wU8(itemsObtainedHigh, bit.rshift(value, 8))
wU8(itemsObtained, bit.band(value, 0xFF))
end
local function determineItem(array) local function determineItem(array)
memdomain.ram() memdomain.ram()
currentItemsObtained = getItemsObtained() currentItemsObtained = u8(itemsObtained)
end end
@@ -372,8 +364,8 @@ local function gotItem(item)
wU8(0x505, itemCode) wU8(0x505, itemCode)
wU8(0x506, 128) wU8(0x506, 128)
wU8(0x602, 4) wU8(0x602, 4)
numberObtained = getItemsObtained() + 1 numberObtained = u8(itemsObtained) + 1
setItemsObtained(numberObtained) wU8(itemsObtained, numberObtained)
if itemName == "Boomerang" then gotBoomerang() end if itemName == "Boomerang" then gotBoomerang() end
if itemName == "Bow" then gotBow() end if itemName == "Bow" then gotBow() end
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
@@ -484,7 +476,7 @@ function processBlock(block)
if i > u8(bonusItemsObtained) then if i > u8(bonusItemsObtained) then
if u8(0x505) == 0 then if u8(0x505) == 0 then
gotItem(item) gotItem(item)
setItemsObtained(getItemsObtained() - 1) wU8(itemsObtained, u8(itemsObtained) - 1)
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1) wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
end end
end end
@@ -502,7 +494,7 @@ function processBlock(block)
for i, item in ipairs(itemsBlock) do for i, item in ipairs(itemsBlock) do
memDomain.ram() memDomain.ram()
if u8(0x505) == 0 then if u8(0x505) == 0 then
if i > getItemsObtained() then if i > u8(itemsObtained) then
gotItem(item) gotItem(item)
end end
end end
@@ -554,7 +546,7 @@ function receive()
retTable["gameMode"] = gameMode retTable["gameMode"] = gameMode
retTable["overworldHC"] = getHCLocation() retTable["overworldHC"] = getHCLocation()
retTable["overworldPB"] = getPBLocation() retTable["overworldPB"] = getPBLocation()
retTable["itemsObtained"] = getItemsObtained() retTable["itemsObtained"] = u8(itemsObtained)
msg = json.encode(retTable).."\n" msg = json.encode(retTable).."\n"
local ret, error = zeldaSocket:send(msg) local ret, error = zeldaSocket:send(msg)
if ret == nil then if ret == nil then
@@ -614,4 +606,4 @@ function main()
end end
end end
main() main()

View File

@@ -116,7 +116,6 @@ Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI";
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion; Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion;
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio

View File

@@ -1,14 +1,7 @@
import os import os
import logging import logging
import sys
import typing import typing
if sys.platform == "win32":
import ctypes
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
ctypes.windll.shcore.SetProcessDpiAwareness(0)
os.environ["KIVY_NO_CONSOLELOG"] = "1" os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1" os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1" os.environ["KIVY_NO_ARGS"] = "1"

View File

@@ -1,7 +1,7 @@
colorama>=0.4.5 colorama>=0.4.5
websockets>=11.0.3 websockets>=11.0.3
PyYAML>=6.0.1 PyYAML>=6.0.1
jellyfish>=1.0.1 jellyfish>=1.0.0
jinja2>=3.1.2 jinja2>=3.1.2
schema>=0.7.5 schema>=0.7.5
kivy>=2.2.0 kivy>=2.2.0
@@ -9,4 +9,4 @@ bsdiff4>=1.2.3
platformdirs>=3.9.1 platformdirs>=3.9.1
certifi>=2023.7.22 certifi>=2023.7.22
cython>=0.29.35 cython>=0.29.35
cymem>=2.0.8 cymem>=2.0.7

View File

@@ -118,7 +118,7 @@ class Group:
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__) cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
return cls._type_cache return cls._type_cache
def get(self, key: str, default: Any = None) -> Any: def get(self, key: str, default: Any) -> Any:
if key in self: if key in self:
return self[key] return self[key]
return default return default

View File

@@ -80,6 +80,7 @@ non_apworlds: set = {
"Raft", "Raft",
"Secret of Evermore", "Secret of Evermore",
"Slay the Spire", "Slay the Spire",
"Starcraft 2 Wings of Liberty",
"Sudoku", "Sudoku",
"Super Mario 64", "Super Mario 64",
"VVVVVV", "VVVVVV",
@@ -90,7 +91,6 @@ non_apworlds: set = {
# LogicMixin is broken before 3.10 import revamp # LogicMixin is broken before 3.10 import revamp
if sys.version_info < (3,10): if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight") non_apworlds.add("Hollow Knight")
non_apworlds.add("Starcraft 2 Wings of Liberty")
def download_SNI(): def download_SNI():
print("Updating SNI") print("Updating SNI")
@@ -185,22 +185,13 @@ def resolve_icon(icon_name: str):
exes = [ exes = [
cx_Freeze.Executable( cx_Freeze.Executable(
script=f"{c.script_name}.py", script=f'{c.script_name}.py',
target_name=c.frozen_name + (".exe" if is_windows else ""), target_name=c.frozen_name + (".exe" if is_windows else ""),
icon=resolve_icon(c.icon), icon=resolve_icon(c.icon),
base="Win32GUI" if is_windows and not c.cli else None base="Win32GUI" if is_windows and not c.cli else None
) for c in components if c.script_name and c.frozen_name ) for c in components if c.script_name and c.frozen_name
] ]
if is_windows:
# create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help
c = next(component for component in components if component.script_name == "Launcher")
exes.append(cx_Freeze.Executable(
script=f"{c.script_name}.py",
target_name=f"{c.frozen_name}(DEBUG).exe",
icon=resolve_icon(c.icon),
))
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []

View File

@@ -5,8 +5,7 @@ import json
class TestDocs(unittest.TestCase): class TestDocs(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls) -> None: def setUpClass(cls) -> None:
from WebHostLib import app as raw_app from WebHost import get_app, raw_app
from WebHost import get_app
raw_app.config["PONY"] = { raw_app.config["PONY"] = {
"provider": "sqlite", "provider": "sqlite",
"filename": ":memory:", "filename": ":memory:",

View File

@@ -14,8 +14,7 @@ class TestFileGeneration(unittest.TestCase):
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib") cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
def testOptions(self): def testOptions(self):
from WebHostLib.options import create as create_options_files WebHost.create_options_files()
create_options_files()
target = os.path.join(self.correct_path, "static", "generated", "configs") target = os.path.join(self.correct_path, "static", "generated", "configs")
self.assertTrue(os.path.exists(target)) self.assertTrue(os.path.exists(target))
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs"))) self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))

View File

@@ -15,6 +15,7 @@ if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial from BaseClasses import MultiWorld, Item, Location, Tutorial
from . import GamesPackage from . import GamesPackage
from settings import Group from settings import Group
from flask import Flask
class AutoWorldRegister(type): class AutoWorldRegister(type):
@@ -155,9 +156,22 @@ class WebWorld:
"""Choose a theme for you /game/* pages. """Choose a theme for you /game/* pages.
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone""" Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
bug_report_page: Optional[str] bug_report_page: Optional[str] = None
"""display a link to a bug report page, most likely a link to a GitHub issue page.""" """display a link to a bug report page, most likely a link to a GitHub issue page."""
multitracker_template: Optional[str] = None
"""relative path with /-seperator to a MultiTracker Template file."""
# allows modification of webhost during startup, this is run once
@classmethod
def run_webhost_setup(cls):
pass
# allows modification of webhost during startup,
# this is run whenever a Flask app is created (per-thread/per-process)
@classmethod
def run_webhost_app_setup(cls, app: "Flask"):
pass
class World(metaclass=AutoWorldRegister): class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
@@ -412,7 +426,6 @@ class World(metaclass=AutoWorldRegister):
res["checksum"] = data_package_checksum(res) res["checksum"] = data_package_checksum(res)
return res return res
# any methods attached to this can be used as part of CollectionState, # any methods attached to this can be used as part of CollectionState,
# please use a prefix as all of them get clobbered together # please use a prefix as all of them get clobbered together
class LogicMixin(metaclass=AutoLogicRegister): class LogicMixin(metaclass=AutoLogicRegister):

View File

@@ -8,31 +8,18 @@ from .paths import Paths
def get(name: str) -> Map: def get(name: str) -> Map:
# Iterate through 2 folder depths
for map_dir in (p for p in Paths.MAPS.iterdir()): for map_dir in (p for p in Paths.MAPS.iterdir()):
map = find_map_in_dir(name, map_dir) if map_dir.is_dir():
if map is not None: for map_file in (p for p in map_dir.iterdir()):
return map if Map.matches_target_map_name(map_file, name):
return Map(map_file)
elif Map.matches_target_map_name(map_dir, name):
return Map(map_dir)
raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".") raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".")
# Go deeper
def find_map_in_dir(name, path):
if Map.matches_target_map_name(path, name):
return Map(path)
if path.name.endswith("SC2Map"):
return None
if path.is_dir():
for childPath in (p for p in path.iterdir()):
map = find_map_in_dir(name, childPath)
if map is not None:
return map
return None
class Map: class Map:
def __init__(self, path: Path): def __init__(self, path: Path):

View File

@@ -7,21 +7,19 @@ LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f" RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f"
ROM_PLAYER_LIMIT: int = 255 ROM_PLAYER_LIMIT: int = 255
import io
import json import json
import hashlib import hashlib
import logging import logging
import os import os
import random import random
import struct
import subprocess import subprocess
import threading import threading
import concurrent.futures import concurrent.futures
import bsdiff4 import bsdiff4
from typing import Optional, List from typing import List
from BaseClasses import CollectionState, Region, Location, MultiWorld from BaseClasses import CollectionState, Region, Location, MultiWorld
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, read_snes_rom
from .Shops import ShopType, ShopPriceType from .Shops import ShopType, ShopPriceType
from .Dungeons import dungeon_music_addresses from .Dungeons import dungeon_music_addresses
@@ -37,6 +35,7 @@ from .Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmith
from .Items import ItemFactory, item_table, item_name_groups, progression_items from .Items import ItemFactory, item_table, item_name_groups, progression_items
from .EntranceShuffle import door_addresses from .EntranceShuffle import door_addresses
from .Options import smallkey_shuffle from .Options import smallkey_shuffle
from .Sprites import apply_random_sprite_on_event
try: try:
from maseya import z3pr from maseya import z3pr
@@ -212,73 +211,6 @@ def check_enemizer(enemizercli):
check_enemizer.done = True check_enemizer.done = True
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
userandomsprites = False
if sprite and not isinstance(sprite, Sprite):
sprite = sprite.lower()
userandomsprites = sprite.startswith('randomon')
racerom = rom.read_byte(0x180213)
if allow_random_on_event or not racerom:
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
# However, if the seed is not a racerom seed, then it is always allowed.
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
onevent = 0
if sprite == 'randomonall':
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
elif sprite == 'randomonnone':
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
onevent = 0x0000
elif sprite == 'randomonrandom':
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
onevent = local_random.randint(0x0001, 0x003F)
elif userandomsprites:
onevent = 0x01 if 'hit' in sprite else 0x00
onevent += 0x02 if 'enter' in sprite else 0x00
onevent += 0x04 if 'exit' in sprite else 0x00
onevent += 0x08 if 'slash' in sprite else 0x00
onevent += 0x10 if 'item' in sprite else 0x00
onevent += 0x20 if 'bonk' in sprite else 0x00
rom.write_int16(0x18637F, onevent)
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
# write link sprite if required
if sprite:
sprites = list()
sprite.write_to_rom(rom)
_populate_sprite_table()
if userandomsprites:
if sprite_pool:
if isinstance(sprite_pool, str):
sprite_pool = sprite_pool.split(':')
for spritename in sprite_pool:
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
spritename, local_random)
if sprite:
sprites.append(sprite)
else:
logging.info(f"Sprite {spritename} was not found.")
else:
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
else:
sprites.append(sprite)
if sprites:
while len(sprites) < 32:
sprites.extend(sprites)
local_random.shuffle(sprites)
for i, sprite in enumerate(sprites[:32]):
if not i and not userandomsprites:
continue
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory): def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
player = world.player player = world.player
multiworld = world.multiworld multiworld = world.multiworld
@@ -487,271 +419,6 @@ class TileSet:
return localrandom.choice(tile_sets) return localrandom.choice(tile_sets)
sprite_list_lock = threading.Lock()
_sprite_table = {}
def _populate_sprite_table():
with sprite_list_lock:
if not _sprite_table:
def load_sprite_from_file(file):
sprite = Sprite(file)
if sprite.valid:
_sprite_table[sprite.name.lower()] = sprite
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
else:
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
with concurrent.futures.ThreadPoolExecutor() as pool:
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file))
class Sprite():
sprite_size = 28672
palette_size = 120
glove_size = 4
author_name: Optional[str] = None
base_data: bytes
def __init__(self, filename):
if not hasattr(Sprite, "base_data"):
self.get_vanilla_sprite_data()
with open(filename, 'rb') as file:
filedata = file.read()
self.name = os.path.basename(filename)
self.valid = True
if filename.endswith(".apsprite"):
self.from_ap_sprite(filedata)
elif len(filedata) == 0x7000:
# sprite file with graphics and without palette data
self.sprite = filedata[:0x7000]
elif len(filedata) == 0x7078:
# sprite file with graphics and palette data
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:]
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
elif len(filedata) == 0x707C:
# sprite file with graphics and palette data including gloves
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:0x7078]
self.glove_palette = filedata[0x7078:]
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
# full rom with patched sprite, extract it
self.sprite = filedata[0x80000:0x87000]
self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
elif filedata.startswith(b'ZSPR'):
self.from_zspr(filedata, filename)
else:
self.valid = False
def get_vanilla_sprite_data(self):
file_name = get_base_rom_path()
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
def from_ap_sprite(self, filedata):
# noinspection PyBroadException
try:
obj = parse_yaml(filedata.decode("utf-8-sig"))
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
except Exception:
logger = logging.getLogger("apsprite")
logger.exception("Error parsing apsprite file")
self.valid = False
@property
def author_game_display(self) -> str:
name = getattr(self, "_author_game_display", "")
if not name:
name = self.author_name
# At this point, may need some filtering to displayable characters
return name
def to_ap_sprite(self, path):
import yaml
payload = {"format_version": 1,
"min_format_version": 1,
"sprite_version": 1,
"name": self.name,
"author": self.author_name,
"game": "A Link to the Past",
"data": self.get_delta()}
with open(path, "w") as f:
f.write(yaml.safe_dump(payload))
def get_delta(self):
modified_data = self.sprite + self.palette + self.glove_palette
return bsdiff4.diff(Sprite.base_data, modified_data)
def from_zspr(self, filedata, filename):
result = self.parse_zspr(filedata, 1)
if result is None:
self.valid = False
return
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
if self.name == "":
self.name = os.path.split(filename)[1].split(".")[0]
if len(sprite) != 0x7000:
self.valid = False
return
self.sprite = sprite
if len(palette) == 0:
pass
elif len(palette) == 0x78:
self.palette = palette
elif len(palette) == 0x7C:
self.palette = palette[:0x78]
self.glove_palette = palette[0x78:]
else:
self.valid = False
@staticmethod
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
_populate_sprite_table()
name = name.lower()
if name.startswith('random'):
sprites = list(set(_sprite_table.values()))
sprites.sort(key=lambda x: x.name)
return local_random.choice(sprites)
return _sprite_table.get(name, None)
@staticmethod
def default_link_sprite():
return Sprite(local_path('data', 'default.apsprite'))
def decode8(self, pos):
arr = [[0 for _ in range(8)] for _ in range(8)]
for y in range(8):
for x in range(8):
position = 1 << (7 - x)
val = 0
if self.sprite[pos + 2 * y] & position:
val += 1
if self.sprite[pos + 2 * y + 1] & position:
val += 2
if self.sprite[pos + 2 * y + 16] & position:
val += 4
if self.sprite[pos + 2 * y + 17] & position:
val += 8
arr[y][x] = val
return arr
def decode16(self, pos):
arr = [[0 for _ in range(16)] for _ in range(16)]
top_left = self.decode8(pos)
top_right = self.decode8(pos + 0x20)
bottom_left = self.decode8(pos + 0x200)
bottom_right = self.decode8(pos + 0x220)
for x in range(8):
for y in range(8):
arr[y][x] = top_left[y][x]
arr[y][x + 8] = top_right[y][x]
arr[y + 8][x] = bottom_left[y][x]
arr[y + 8][x + 8] = bottom_right[y][x]
return arr
@staticmethod
def parse_zspr(filedata, expected_kind):
logger = logging.getLogger("ZSPR")
headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr)
if len(filedata) < headersize:
return None
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
headerstr, filedata)
if version not in [1]:
logger.error("Error parsing ZSPR file: Version %g not supported", version)
return None
if kind != expected_kind:
return None
stream = io.BytesIO(filedata)
stream.seek(headersize)
def read_utf16le(stream):
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
raw = bytearray()
while True:
char = stream.read(2)
if char in [b"", b"\x00\x00"]:
break
raw += char
return raw.decode("utf-16_le")
# noinspection PyBroadException
try:
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# Ignoring the Author Rom name for the time being.
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error("Error parsing ZSPR file: Unexpected end of file")
return None
return sprite, palette, sprite_name, author_name, author_credits_name
except Exception:
logger.exception("Error parsing ZSPR file")
return None
def decode_palette(self):
"""Returns the palettes as an array of arrays of 15 colors"""
def array_chunk(arr, size):
return list(zip(*[iter(arr)] * size))
def make_int16(pair):
return pair[1] << 8 | pair[0]
def expand_color(i):
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
# turn palette data into a list of RGB tuples with 8 bit values
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
# split into palettes of 15 colors
return array_chunk(palette_as_colors, 15)
def __hash__(self):
return hash(self.name)
def write_to_rom(self, rom: LocalRom):
if not self.valid:
logging.warning("Tried writing invalid sprite to rom, skipping.")
return
rom.write_bytes(0x80000, self.sprite)
rom.write_bytes(0xDD308, self.palette)
rom.write_bytes(0xDEDF5, self.glove_palette)
rom.write_bytes(0x300000, self.sprite)
rom.write_bytes(0x307000, self.palette)
rom.write_bytes(0x307078, self.glove_palette)
bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A,
0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD,
0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D,

View File

@@ -32,6 +32,7 @@ def set_rules(world):
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!') 'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
if world.players == 1: if world.players == 1:
world.get_region('Menu', player).can_reach_private = lambda state: True
no_logic_rules(world, player) no_logic_rules(world, player)
for exit in world.get_region('Menu', player).exits: for exit in world.get_region('Menu', player).exits:
exit.hide_path = True exit.hide_path = True
@@ -195,6 +196,7 @@ def global_rules(world, player):
add_item_rule(world.get_location(prize_location, player), add_item_rule(world.get_location(prize_location, player),
lambda item: item.name in crystals_and_pendants and item.player == player) lambda item: item.name in crystals_and_pendants and item.player == player)
# determines which S&Q locations are available - hide from paths since it isn't an in-game location # determines which S&Q locations are available - hide from paths since it isn't an in-game location
world.get_region('Menu', player).can_reach_private = lambda state: True
for exit in world.get_region('Menu', player).exits: for exit in world.get_region('Menu', player).exits:
exit.hide_path = True exit.hide_path = True

393
worlds/alttp/Sprites.py Normal file
View File

@@ -0,0 +1,393 @@
from __future__ import annotations
import concurrent.futures
import io
import json
import logging
import os
import random
import struct
import threading
from typing import Optional, TYPE_CHECKING
import bsdiff4
from Utils import user_path, read_snes_rom, parse_yaml, local_path
if TYPE_CHECKING:
from .Rom import LocalRom
sprite_list_lock = threading.Lock()
_sprite_table = {}
def _populate_sprite_table():
with sprite_list_lock:
if not _sprite_table:
def load_sprite_from_file(file):
sprite = Sprite(file)
if sprite.valid:
_sprite_table[sprite.name.lower()] = sprite
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
else:
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
with concurrent.futures.ThreadPoolExecutor() as pool:
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file))
class Sprite():
sprite_size = 28672
palette_size = 120
glove_size = 4
author_name: Optional[str] = None
base_data: bytes
def __init__(self, filename):
if not hasattr(Sprite, "base_data"):
self.get_vanilla_sprite_data()
with open(filename, 'rb') as file:
filedata = file.read()
self.name = os.path.basename(filename)
self.valid = True
if filename.endswith(".apsprite"):
self.from_ap_sprite(filedata)
elif len(filedata) == 0x7000:
# sprite file with graphics and without palette data
self.sprite = filedata[:0x7000]
elif len(filedata) == 0x7078:
# sprite file with graphics and palette data
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:]
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
elif len(filedata) == 0x707C:
# sprite file with graphics and palette data including gloves
self.sprite = filedata[:0x7000]
self.palette = filedata[0x7000:0x7078]
self.glove_palette = filedata[0x7078:]
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
# full rom with patched sprite, extract it
self.sprite = filedata[0x80000:0x87000]
self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
elif filedata.startswith(b'ZSPR'):
self.from_zspr(filedata, filename)
else:
self.valid = False
def get_vanilla_sprite_data(self):
from .Rom import get_base_rom_path
file_name = get_base_rom_path()
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
def from_ap_sprite(self, filedata):
# noinspection PyBroadException
try:
obj = parse_yaml(filedata.decode("utf-8-sig"))
if obj["min_format_version"] > 1:
raise Exception("Sprite file requires an updated reader.")
self.author_name = obj["author"]
self.name = obj["name"]
if obj["data"]: # skip patching for vanilla content
data = bsdiff4.patch(Sprite.base_data, obj["data"])
self.sprite = data[:self.sprite_size]
self.palette = data[self.sprite_size:self.palette_size]
self.glove_palette = data[self.sprite_size + self.palette_size:]
except Exception:
logger = logging.getLogger("apsprite")
logger.exception("Error parsing apsprite file")
self.valid = False
@property
def author_game_display(self) -> str:
name = getattr(self, "_author_game_display", "")
if not name:
name = self.author_name
# At this point, may need some filtering to displayable characters
return name
def to_ap_sprite(self, path):
import yaml
payload = {"format_version": 1,
"min_format_version": 1,
"sprite_version": 1,
"name": self.name,
"author": self.author_name,
"game": "A Link to the Past",
"data": self.get_delta()}
with open(path, "w") as f:
f.write(yaml.safe_dump(payload))
def get_delta(self):
modified_data = self.sprite + self.palette + self.glove_palette
return bsdiff4.diff(Sprite.base_data, modified_data)
def from_zspr(self, filedata, filename):
result = self.parse_zspr(filedata, 1)
if result is None:
self.valid = False
return
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
if self.name == "":
self.name = os.path.split(filename)[1].split(".")[0]
if len(sprite) != 0x7000:
self.valid = False
return
self.sprite = sprite
if len(palette) == 0:
pass
elif len(palette) == 0x78:
self.palette = palette
elif len(palette) == 0x7C:
self.palette = palette[:0x78]
self.glove_palette = palette[0x78:]
else:
self.valid = False
@staticmethod
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
_populate_sprite_table()
name = name.lower()
if name.startswith('random'):
sprites = list(set(_sprite_table.values()))
sprites.sort(key=lambda x: x.name)
return local_random.choice(sprites)
return _sprite_table.get(name, None)
@staticmethod
def default_link_sprite():
return Sprite(local_path('data', 'default.apsprite'))
def decode8(self, pos):
arr = [[0 for _ in range(8)] for _ in range(8)]
for y in range(8):
for x in range(8):
position = 1 << (7 - x)
val = 0
if self.sprite[pos + 2 * y] & position:
val += 1
if self.sprite[pos + 2 * y + 1] & position:
val += 2
if self.sprite[pos + 2 * y + 16] & position:
val += 4
if self.sprite[pos + 2 * y + 17] & position:
val += 8
arr[y][x] = val
return arr
def decode16(self, pos):
arr = [[0 for _ in range(16)] for _ in range(16)]
top_left = self.decode8(pos)
top_right = self.decode8(pos + 0x20)
bottom_left = self.decode8(pos + 0x200)
bottom_right = self.decode8(pos + 0x220)
for x in range(8):
for y in range(8):
arr[y][x] = top_left[y][x]
arr[y][x + 8] = top_right[y][x]
arr[y + 8][x] = bottom_left[y][x]
arr[y + 8][x + 8] = bottom_right[y][x]
return arr
@staticmethod
def parse_zspr(filedata, expected_kind):
logger = logging.getLogger("ZSPR")
headerstr = "<4xBHHIHIHH6x"
headersize = struct.calcsize(headerstr)
if len(filedata) < headersize:
return None
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
headerstr, filedata)
if version not in [1]:
logger.error("Error parsing ZSPR file: Version %g not supported", version)
return None
if kind != expected_kind:
return None
stream = io.BytesIO(filedata)
stream.seek(headersize)
def read_utf16le(stream):
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
raw = bytearray()
while True:
char = stream.read(2)
if char in [b"", b"\x00\x00"]:
break
raw += char
return raw.decode("utf-16_le")
# noinspection PyBroadException
try:
sprite_name = read_utf16le(stream)
author_name = read_utf16le(stream)
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
# Ignoring the Author Rom name for the time being.
real_csum = sum(filedata) % 0x10000
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
palette = filedata[palette_offset:palette_offset + palette_size]
if len(sprite) != sprite_size or len(palette) != palette_size:
logger.error("Error parsing ZSPR file: Unexpected end of file")
return None
return sprite, palette, sprite_name, author_name, author_credits_name
except Exception:
logger.exception("Error parsing ZSPR file")
return None
def decode_palette(self):
"""Returns the palettes as an array of arrays of 15 colors"""
def array_chunk(arr, size):
return list(zip(*[iter(arr)] * size))
def make_int16(pair):
return pair[1] << 8 | pair[0]
def expand_color(i):
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
# turn palette data into a list of RGB tuples with 8 bit values
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
# split into palettes of 15 colors
return array_chunk(palette_as_colors, 15)
def __hash__(self):
return hash(self.name)
def write_to_rom(self, rom: "LocalRom"):
if not self.valid:
logging.warning("Tried writing invalid sprite to rom, skipping.")
return
rom.write_bytes(0x80000, self.sprite)
rom.write_bytes(0xDD308, self.palette)
rom.write_bytes(0xDEDF5, self.glove_palette)
rom.write_bytes(0x300000, self.sprite)
rom.write_bytes(0x307000, self.palette)
rom.write_bytes(0x307078, self.glove_palette)
def update_sprites():
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
input_dir = user_path("data", "sprites", "alttpr")
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
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()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
task.do_events()
spriteData = []
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
sprite = Sprite(os.path.join(input_dir, file))
if not sprite.name:
print("Warning:", file, "has no name.")
sprite.name = file.split(".", 1)[0]
if sprite.valid:
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
image.write(get_image_for_sprite(sprite, True))
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
else:
print(file, "dropped, as it has no valid sprite data.")
spriteData.sort(key=lambda entry: entry["name"])
with open(f'{output_dir}/spriteData.json', 'w') as file:
json.dump({"sprites": spriteData}, file, indent=1)
return spriteData
def apply_random_sprite_on_event(rom: "LocalRom", sprite, local_random, allow_random_on_event, sprite_pool):
userandomsprites = False
if sprite and not isinstance(sprite, Sprite):
sprite = sprite.lower()
userandomsprites = sprite.startswith('randomon')
racerom = rom.read_byte(0x180213)
if allow_random_on_event or not racerom:
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
# However, if the seed is not a racerom seed, then it is always allowed.
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
onevent = 0
if sprite == 'randomonall':
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
elif sprite == 'randomonnone':
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
onevent = 0x0000
elif sprite == 'randomonrandom':
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
onevent = local_random.randint(0x0001, 0x003F)
elif userandomsprites:
onevent = 0x01 if 'hit' in sprite else 0x00
onevent += 0x02 if 'enter' in sprite else 0x00
onevent += 0x04 if 'exit' in sprite else 0x00
onevent += 0x08 if 'slash' in sprite else 0x00
onevent += 0x10 if 'item' in sprite else 0x00
onevent += 0x20 if 'bonk' in sprite else 0x00
rom.write_int16(0x18637F, onevent)
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
# write link sprite if required
if sprite:
sprites = list()
sprite.write_to_rom(rom)
_populate_sprite_table()
if userandomsprites:
if sprite_pool:
if isinstance(sprite_pool, str):
sprite_pool = sprite_pool.split(':')
for spritename in sprite_pool:
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
spritename, local_random)
if sprite:
sprites.append(sprite)
else:
logging.info(f"Sprite {spritename} was not found.")
else:
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
else:
sprites.append(sprite)
if sprites:
while len(sprites) < 32:
sprites.extend(sprites)
local_random.shuffle(sprites)
for i, sprite in enumerate(sprites[:32]):
if not i and not userandomsprites:
continue
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)

View File

@@ -124,6 +124,14 @@ class ALTTPWeb(WebWorld):
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound] tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
@classmethod
def run_webhost_setup(cls):
rom_file = get_base_rom_path()
if os.path.exists(rom_file):
from .Sprites import update_sprites
update_sprites()
else:
logging.warning("Could not update LttP sprites.")
class ALTTPWorld(World): class ALTTPWorld(World):
""" """
@@ -807,7 +815,6 @@ class ALTTPWorld(World):
} }
) )
return slot_data return slot_data
def get_same_seed(world, seed_def: tuple) -> str: def get_same_seed(world, seed_def: tuple) -> str:
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {}) seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})

View File

@@ -367,25 +367,25 @@ def can_beat_boss(state: CollectionState, boss: str, logic: int, player: int) ->
elif boss == "Graveyard": elif boss == "Graveyard":
return ( return (
has_boss_strength("amanecida") has_boss_strength("amanecida")
and state.has_all({"D01Z06S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player) and state.has_all({"D01BZ07S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
) )
elif boss == "Jondo": elif boss == "Jondo":
return ( return (
has_boss_strength("amanecida") has_boss_strength("amanecida")
and state.has("D01Z06S01[Santos]", player) and state.has("D01BZ07S01[Santos]", player)
and state.has_any({"D20Z01S05[W]", "D20Z01S05[E]"}, player) and state.has_any({"D20Z01S05[W]", "D20Z01S05[E]"}, player)
and state.has_any({"D03Z01S03[W]", "D03Z01S03[SW]"}, player) and state.has_any({"D03Z01S03[W]", "D03Z01S03[SW]"}, player)
) )
elif boss == "Patio": elif boss == "Patio":
return ( return (
has_boss_strength("amanecida") has_boss_strength("amanecida")
and state.has_all({"D01Z06S01[Santos]", "D06Z01S18[E]"}, player) and state.has_all({"D01BZ07S01[Santos]", "D06Z01S18[E]"}, player)
and state.has_any({"D04Z01S04[W]", "D04Z01S04[E]", "D04Z01S04[Cherubs]"}, player) and state.has_any({"D04Z01S04[W]", "D04Z01S04[E]", "D04Z01S04[Cherubs]"}, player)
) )
elif boss == "Wall": elif boss == "Wall":
return ( return (
has_boss_strength("amanecida") has_boss_strength("amanecida")
and state.has_all({"D01Z06S01[Santos]", "D09BZ01S01[Cell24]"}, player) and state.has_all({"D01BZ07S01[Santos]", "D09BZ01S01[Cell24]"}, player)
and state.has_any({"D09Z01S01[W]", "D09Z01S01[E]"}, player) and state.has_any({"D09Z01S01[W]", "D09Z01S01[E]"}, player)
) )
elif boss == "Hall": elif boss == "Hall":
@@ -2451,8 +2451,6 @@ def rules(blasphemousworld):
# Items # Items
set_rule(world.get_location("PotSS: 4th meeting with Redento", player), set_rule(world.get_location("PotSS: 4th meeting with Redento", player),
lambda state: redento(state, blasphemousworld, player, 4)) lambda state: redento(state, blasphemousworld, player, 4))
set_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player),
lambda state: can_beat_boss(state, "Patio", logic, player))
# No doors # No doors

View File

@@ -1,19 +1,21 @@
## Required Software ## Required Software
Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases), or from the [Bumper Stickers AP Itch page](https://kewliomzx.itch.io/bumpstik-ap), where you can also play it in your browser. Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases).
*A web version will be made available on itch.io at a later time.*
## Installation Procedures ## Installation Procedures
Simply download the latest version of Bumper Stickers from the link above, and extract it wherever you like. Simply download the latest version of Bumper Stickers from the link above, and extract it wherever you like.
- ⚠️ It is not recommended to copy this game, or any files, directly into your Program Files folder under Windows. - ⚠️ Do not extract Bumper Stickers to Program Files, as this will cause file access issues.
## Joining a Multiworld Game ## Joining a Multiworld Game
1. Run `BumpStikAP.exe`. 1. Run `BumpStik-AP.exe`.
2. Select "Archipelago Mode". 2. Select "Archipelago Mode".
3. Enter your server details in the fields provided, and click "Start". 3. Enter your server details in the fields provided, and click "Start".
- The game will attempt to automatically detect whether to connect via normal (WS) or secure (WSS) server, but you can specify `ws://` or `wss://` to prioritise one or the other. - ※ If you are connecting to a WSS server (such as archipelago.gg), specify `wss://` in the host name. Otherwise, the game will assume `ws://`.
## How to play Bumper Stickers (Classic) ## How to play Bumper Stickers (Classic)

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