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
233 changed files with 3671 additions and 7477 deletions

View File

@@ -54,9 +54,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install pytest pytest-subtests pytest-xdist pip install pytest pytest-subtests
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
python Launcher.py --update_settings # make sure host.yaml exists for tests python Launcher.py --update_settings # make sure host.yaml exists for tests
- name: Unittests - name: Unittests
run: | run: |
pytest -n auto pytest

5
.gitignore vendored
View File

@@ -27,20 +27,16 @@
*.archipelago *.archipelago
*.apsave *.apsave
*.BIN *.BIN
*.puml
setups setups
build build
bundle/components.wxs bundle/components.wxs
dist dist
/prof/
README.html README.html
.vs/ .vs/
EnemizerCLI/ EnemizerCLI/
/Players/ /Players/
/SNI/ /SNI/
/sni-*/
/appimagetool*
/host.yaml /host.yaml
/options.yaml /options.yaml
/config.yaml /config.yaml
@@ -143,7 +139,6 @@ ipython_config.py
.venv* .venv*
env/ env/
venv/ venv/
/venv*/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/

View File

@@ -8,7 +8,6 @@ import secrets
import typing # this can go away when Python 3.8 support is dropped import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace from argparse import Namespace
from collections import ChainMap, Counter, deque from collections import ChainMap, Counter, deque
from collections.abc import Collection
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \ from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
Type, ClassVar Type, ClassVar
@@ -203,7 +202,14 @@ class MultiWorld():
self.player_types[new_id] = NetUtils.SlotType.group self.player_types[new_id] = NetUtils.SlotType.group
self._region_cache[new_id] = {} self._region_cache[new_id] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[game] world_type = AutoWorld.AutoWorldRegister.world_types[game]
self.worlds[new_id] = world_type.create_group(self, new_id, players) for option_key, option in world_type.option_definitions.items():
getattr(self, option_key)[new_id] = option(option.default)
for option_key, option in Options.common_options.items():
getattr(self, option_key)[new_id] = option(option.default)
for option_key, option in Options.per_game_common_options.items():
getattr(self, option_key)[new_id] = option(option.default)
self.worlds[new_id] = world_type(self, new_id)
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
self.player_name[new_id] = name self.player_name[new_id] = name
@@ -358,7 +364,7 @@ class MultiWorld():
for r_location in region.locations: for r_location in region.locations:
self._location_cache[r_location.name, player] = r_location self._location_cache[r_location.name, player] = r_location
def get_regions(self, player: Optional[int] = None) -> Collection[Region]: def get_regions(self, player=None):
return self.regions if player is None else self._region_cache[player].values() return self.regions if player is None else self._region_cache[player].values()
def get_region(self, regionname: str, player: int) -> Region: def get_region(self, regionname: str, player: int) -> Region:
@@ -847,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

@@ -1,9 +0,0 @@
from __future__ import annotations
import ModuleUpdate
ModuleUpdate.update()
from worlds._bizhawk.context import launch
if __name__ == "__main__":
launch()

14
Fill.py
View File

@@ -753,6 +753,8 @@ def distribute_planned(world: MultiWorld) -> None:
else: # not reachable with swept state else: # not reachable with swept state
non_early_locations[loc.player].append(loc.name) non_early_locations[loc.player].append(loc.name)
# TODO: remove. Preferably by implementing key drop
from worlds.alttp.Regions import key_drop_data
world_name_lookup = world.world_name_lookup world_name_lookup = world.world_name_lookup
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str] block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
@@ -838,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
@@ -895,6 +897,10 @@ def distribute_planned(world: MultiWorld) -> None:
for item_name in items: for item_name in items:
item = world.worlds[player].create_item(item_name) item = world.worlds[player].create_item(item_name)
for location in reversed(candidates): for location in reversed(candidates):
if location in key_drop_data:
warn(
f"Can't place '{item_name}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
continue
if not location.item: if not location.item:
if location.item_rule(item): if location.item_rule(item):
if location.can_fill(world.state, item, False): if location.can_fill(world.state, item, False):

View File

@@ -50,22 +50,17 @@ def open_host_yaml():
def open_patch(): def open_patch():
suffixes = [] suffixes = []
for c in components: for c in components:
if c.type == Type.CLIENT and \ if isfile(get_exe(c)[-1]):
isinstance(c.file_identifier, SuffixIdentifier) and \ suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \
(c.script_name is None or isfile(get_exe(c)[-1])): isinstance(c.file_identifier, SuffixIdentifier) else []
suffixes += c.file_identifier.suffixes
try: try:
filename = open_filename("Select patch", (("Patches", suffixes),)) filename = open_filename('Select patch', (('Patches', suffixes),))
except Exception as e: except Exception as e:
messagebox("Error", str(e), error=True) messagebox('Error', str(e), error=True)
else: else:
file, component = identify(filename) file, component = identify(filename)
if file and component: if file and component:
exe = get_exe(component) launch([*get_exe(component), file], component.cli)
if exe is None or not isfile(exe[-1]):
exe = get_exe("Launcher")
launch([*exe, file], component.cli)
def generate_yamls(): def generate_yamls():
@@ -112,7 +107,7 @@ def identify(path: Union[None, str]):
return None, None return None, None
for component in components: for component in components:
if component.handles_file(path): if component.handles_file(path):
return path, component return path, component
elif path == component.display_name or path == component.script_name: elif path == component.display_name or path == component.script_name:
return None, component return None, component
return None, None return None, None
@@ -122,25 +117,25 @@ def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
if isinstance(component, str): if isinstance(component, str):
name = component name = component
component = None component = None
if name.startswith("Archipelago"): if name.startswith('Archipelago'):
name = name[11:] name = name[11:]
if name.endswith(".exe"): if name.endswith('.exe'):
name = name[:-4] name = name[:-4]
if name.endswith(".py"): if name.endswith('.py'):
name = name[:-3] name = name[:-3]
if not name: if not name:
return None return None
for c in components: for c in components:
if c.script_name == name or c.frozen_name == f"Archipelago{name}": if c.script_name == name or c.frozen_name == f'Archipelago{name}':
component = c component = c
break break
if not component: if not component:
return None return None
if is_frozen(): if is_frozen():
suffix = ".exe" if is_windows else "" suffix = '.exe' if is_windows else ''
return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None return [local_path(f'{component.frozen_name}{suffix}')]
else: else:
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None return [sys.executable, local_path(f'{component.script_name}.py')]
def launch(exe, in_terminal=False): def launch(exe, in_terminal=False):

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:

157
Utils.py
View File

@@ -13,7 +13,6 @@ import io
import collections import collections
import importlib import importlib
import logging import logging
import warnings
from argparse import Namespace from argparse import Namespace
from settings import Settings, get_settings from settings import Settings, get_settings
@@ -30,7 +29,6 @@ except ImportError:
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
import tkinter import tkinter
import pathlib import pathlib
from BaseClasses import Region
def tuplize_version(version: str) -> Version: def tuplize_version(version: str) -> Version:
@@ -46,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")
@@ -217,13 +215,7 @@ def get_cert_none_ssl_context():
def get_public_ipv4() -> str: def get_public_ipv4() -> str:
import socket import socket
import urllib.request import urllib.request
try: ip = socket.gethostbyname(socket.gethostname())
ip = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
# if hostname or resolvconf is not set up properly, this may fail
warnings.warn("Could not resolve own hostname, falling back to 127.0.0.1")
ip = "127.0.0.1"
ctx = get_cert_none_ssl_context() ctx = get_cert_none_ssl_context()
try: try:
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip() ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
@@ -241,13 +233,7 @@ def get_public_ipv4() -> str:
def get_public_ipv6() -> str: def get_public_ipv6() -> str:
import socket import socket
import urllib.request import urllib.request
try: ip = socket.gethostbyname(socket.gethostname())
ip = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
# if hostname or resolvconf is not set up properly, this may fail
warnings.warn("Could not resolve own hostname, falling back to ::1")
ip = "::1"
ctx = get_cert_none_ssl_context() ctx = get_cert_none_ssl_context()
try: try:
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip() ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
@@ -373,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:
@@ -389,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"):
@@ -590,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
@@ -602,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)
@@ -618,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
@@ -637,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)
@@ -780,113 +755,3 @@ def freeze_support() -> None:
import multiprocessing import multiprocessing
_extend_freeze_support() _extend_freeze_support()
multiprocessing.freeze_support() multiprocessing.freeze_support()
def visualize_regions(root_region: Region, file_name: str, *,
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
linetype_ortho: bool = True) -> None:
"""Visualize the layout of a world as a PlantUML diagram.
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
:param file_name: The name of the destination .puml file.
:param show_entrance_names: (default False) If enabled, the name of the entrance will be shown near each connection.
:param show_locations: (default True) If enabled, the locations will be listed inside each region.
Priority locations will be shown in bold.
Excluded locations will be stricken out.
Locations without ID will be shown in italics.
Locked locations will be shown with a padlock icon.
For filled locations, the item name will be shown after the location name.
Progression items will be shown in bold.
Items without ID will be shown in italics.
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
Example usage in World code:
from Utils import visualize_regions
visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
Example usage in Main code:
from Utils import visualize_regions
for player in world.player_ids:
visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
"""
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
from collections import deque
import re
uml: typing.List[str] = list()
seen: typing.Set[Region] = set()
regions: typing.Deque[Region] = deque((root_region,))
multiworld: MultiWorld = root_region.multiworld
def fmt(obj: Union[Entrance, Item, Location, Region]) -> str:
name = obj.name
if isinstance(obj, Item):
name = multiworld.get_name_string_for_object(obj)
if obj.advancement:
name = f"**{name}**"
if obj.code is None:
name = f"//{name}//"
if isinstance(obj, Location):
if obj.progress_type == LocationProgressType.PRIORITY:
name = f"**{name}**"
elif obj.progress_type == LocationProgressType.EXCLUDED:
name = f"--{name}--"
if obj.address is None:
name = f"//{name}//"
return re.sub("[\".:]", "", name)
def visualize_exits(region: Region) -> None:
for exit_ in region.exits:
if exit_.connected_region:
if show_entrance_names:
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"")
else:
try:
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"")
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"")
except ValueError:
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"")
else:
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"")
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"")
def visualize_locations(region: Region) -> None:
any_lock = any(location.locked for location in region.locations)
for location in region.locations:
lock = "<&lock-locked> " if location.locked else "<&lock-unlocked,color=transparent> " if any_lock else ""
if location.item:
uml.append(f"\"{fmt(region)}\" : {{method}} {lock}{fmt(location)}: {fmt(location.item)}")
else:
uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}")
def visualize_region(region: Region) -> None:
uml.append(f"class \"{fmt(region)}\"")
if show_locations:
visualize_locations(region)
visualize_exits(region)
def visualize_other_regions() -> None:
if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]:
uml.append("package \"other regions\" <<Cloud>> {")
for region in other_regions:
uml.append(f"class \"{fmt(region)}\"")
uml.append("}")
uml.append("@startuml")
uml.append("hide circle")
uml.append("hide empty members")
if linetype_ortho:
uml.append("skinparam linetype ortho")
while regions:
if (current_region := regions.popleft()) not in seen:
seen.add(current_region)
visualize_region(current_region)
regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region)
if show_other_regions:
visualize_other_regions()
uml.append("@enduml")
with open(file_name, "wt", encoding="utf-8") as f:
f.write("\n".join(uml))

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

@@ -11,7 +11,6 @@ import socket
import threading import threading
import time import time
import typing import typing
import sys
import websockets import websockets
from pony.orm import commit, db_session, select from pony.orm import commit, db_session, select
@@ -20,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
@@ -165,21 +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():
if "worlds" in sys.modules:
raise Exception("Worlds system should not be loaded in the custom server.")
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
@@ -205,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

@@ -32,34 +32,29 @@ def page_not_found(err):
# Start Playing Page # Start Playing Page
@app.route('/start-playing') @app.route('/start-playing')
@cache.cached()
def start_playing(): def start_playing():
return render_template(f"startPlaying.html") return render_template(f"startPlaying.html")
@app.route('/weighted-settings') @app.route('/weighted-settings')
@cache.cached()
def weighted_settings(): def weighted_settings():
return render_template(f"weighted-settings.html") return render_template(f"weighted-settings.html")
# Player settings pages # Player settings pages
@app.route('/games/<string:game>/player-settings') @app.route('/games/<string:game>/player-settings')
@cache.cached()
def player_settings(game): def player_settings(game):
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
# Game Info Pages # Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>') @app.route('/games/<string:game>/info/<string:lang>')
@cache.cached()
def game_info(game, lang): def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
# List of supported games # List of supported games
@app.route('/games') @app.route('/games')
@cache.cached()
def games(): def games():
worlds = {} worlds = {}
for game, world in AutoWorldRegister.world_types.items(): for game, world in AutoWorldRegister.world_types.items():
@@ -69,25 +64,21 @@ def games():
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>') @app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
@cache.cached()
def tutorial(game, file, lang): def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
@app.route('/tutorial/') @app.route('/tutorial/')
@cache.cached()
def tutorial_landing(): def tutorial_landing():
return render_template("tutorialLanding.html") return render_template("tutorialLanding.html")
@app.route('/faq/<string:lang>/') @app.route('/faq/<string:lang>/')
@cache.cached()
def faq(lang): def faq(lang):
return render_template("faq.html", lang=lang) return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/') @app.route('/glossary/<string:lang>/')
@cache.cached()
def terms(lang): def terms(lang):
return render_template("glossary.html", lang=lang) return render_template("glossary.html", lang=lang)
@@ -156,7 +147,7 @@ def host_room(room: UUID):
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): def favicon():
return send_from_directory(os.path.join(app.root_path, "static", "static"), return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon') 'favicon.ico', mimetype='image/vnd.microsoft.icon')
@@ -176,7 +167,6 @@ def get_datapackage():
@app.route('/index') @app.route('/index')
@app.route('/sitemap') @app.route('/sitemap')
@cache.cached()
def get_sitemap(): def get_sitemap():
available_games: List[Dict[str, Union[str, bool]]] = [] available_games: List[Dict[str, Union[str, bool]]] = []
for game, world in AutoWorldRegister.world_types.items(): for game, world in AutoWorldRegister.world_types.items():

View File

@@ -1,9 +1,9 @@
flask>=2.2.3 flask>=2.2.3
pony>=0.7.17 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'
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,84 +0,0 @@
window.addEventListener('load', () => {
document.getElementById('js-enabled').style.display = 'block';
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

@@ -235,6 +235,9 @@ html{
line-height: 30px; line-height: 30px;
} }
#landing .variable{
color: #ffff00;
}
.landing-deco{ .landing-deco{
position: absolute; position: absolute;

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,20 +18,6 @@
margin-bottom: 2px; margin-bottom: 2px;
} }
#games h2 .collapse-arrow{
font-size: 20px;
vertical-align: middle;
cursor: pointer;
}
#games h2 .game-name{
cursor: pointer;
}
#games p.collapsed{
display: none;
}
#games a{ #games a{
font-size: 16px; font-size: 16px;
} }
@@ -45,17 +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;
}
#games #js-enabled{
display: none;
}

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

@@ -49,9 +49,9 @@
our crazy idea into a reality. our crazy idea into a reality.
</p> </p>
<p> <p>
<a href="{{ url_for("stats") }}">{{ seeds }}</a> <span class="variable">{{ seeds }}</span>
games were generated and games were generated and
<a href="{{ url_for("stats") }}">{{ rooms }}</a> <span class="variable">{{ rooms }}</span>
were hosted in the last 7 days. were hosted in the last 7 days.
</p> </p>
</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,35 +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!&nbsp;
<noscript>(You need to enable Javascript for this to work.)</noscript>
</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>
<!-- This is always written to the page, but is hidden by default. If the user has JS enabled,
it will be un-hidden immediately when the page is ready. -->
<div id="js-enabled">
{% 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; <p>
<span class="game-name">{{ game_name }}</span>
</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 %}
@@ -52,34 +33,5 @@
{% endif %} {% endif %}
</p> </p>
{% endfor %} {% endfor %}
</div>
<!-- This is only printed when the user has JS disabled, allowing them to see all the information for
each game in a noscript-friendly way. -->
<noscript>
{% for game_name in worlds | title_sorted %}
{% set world = worlds[game_name] %}
<h2 class="collapse-toggle">{{ game_name }}</h2>
<p id="{{ game_name }}">
{{ world.__doc__ | default("No description provided.", true) }}<br />
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
{% if world.web.tutorials %}
<span class="link-spacer">|</span>
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
{% endif %}
{% if world.web.settings_page is string %}
<span class="link-spacer">|</span>
<a href="{{ world.web.settings_page }}">Settings Page</a>
{% elif world.web.settings_page %}
<span class="link-spacer">|</span>
<a href="{{ url_for("player_settings", game=game_name) }}">Settings Page</a>
{% endif %}
{% if world.web.bug_report_page %}
<span class="link-spacer">|</span>
<a href="{{ world.web.bug_report_page }}">Report a Bug</a>
{% endif %}
</p>
{% endfor %}
</noscript>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -34,7 +34,7 @@
{% endif %} {% endif %}
<tr> <tr>
<td>Rooms:&nbsp;</td> <td>Rooms:&nbsp;</td>
<td> <td>
{% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %} {% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}
<li> <li>
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a> <a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>

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

@@ -1,119 +0,0 @@
-- This file originates from this repository: https://github.com/iskolbin/lbase64
-- It was modified to translate between base64 strings and lists of bytes instead of base64 strings and strings.
local base64 = {}
local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
if not extract then
if _G._VERSION == "Lua 5.4" then
extract = load[[return function( v, from, width )
return ( v >> from ) & ((1 << width) - 1)
end]]()
elseif _G.bit then -- LuaJIT
local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
extract = function( v, from, width )
return band( shr( v, from ), shl( 1, width ) - 1 )
end
elseif _G._VERSION == "Lua 5.1" then
extract = function( v, from, width )
local w = 0
local flag = 2^from
for i = 0, width-1 do
local flag2 = flag + flag
if v % flag2 >= flag then
w = w + 2^i
end
flag = flag2
end
return w
end
end
end
function base64.makeencoder( s62, s63, spad )
local encoder = {}
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
encoder[b64code] = char:byte()
end
return encoder
end
function base64.makedecoder( s62, s63, spad )
local decoder = {}
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
decoder[charcode] = b64code
end
return decoder
end
local DEFAULT_ENCODER = base64.makeencoder()
local DEFAULT_DECODER = base64.makedecoder()
local char, concat = string.char, table.concat
function base64.encode( arr, encoder )
encoder = encoder or DEFAULT_ENCODER
local t, k, n = {}, 1, #arr
local lastn = n % 3
for i = 1, n-lastn, 3 do
local a, b, c = arr[i], arr[i + 1], arr[i + 2]
local v = a*0x10000 + b*0x100 + c
local s
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
t[k] = s
k = k + 1
end
if lastn == 2 then
local a, b = arr[n-1], arr[n]
local v = a*0x10000 + b*0x100
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
elseif lastn == 1 then
local v = arr[n]*0x10000
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
end
return concat( t )
end
function base64.decode( b64, decoder )
decoder = decoder or DEFAULT_DECODER
local pattern = '[^%w%+%/%=]'
if decoder then
local s62, s63
for charcode, b64code in pairs( decoder ) do
if b64code == 62 then s62 = charcode
elseif b64code == 63 then s63 = charcode
end
end
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
end
b64 = b64:gsub( pattern, '' )
local t, k = {}, 1
local n = #b64
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = b64:byte( i, i+3 )
local s
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
table.insert(t,extract(v,16,8))
table.insert(t,extract(v,8,8))
table.insert(t,extract(v,0,8))
end
if padding == 1 then
local a, b, c = b64:byte( n-3, n-1 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
table.insert(t,extract(v,16,8))
table.insert(t,extract(v,8,8))
elseif padding == 2 then
local a, b = b64:byte( n-3, n-2 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000
table.insert(t,extract(v,16,8))
end
return t
end
return base64

View File

@@ -1,564 +0,0 @@
--[[
Copyright (c) 2023 Zunawe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
local SCRIPT_VERSION = 1
--[[
This script expects to receive JSON and will send JSON back. A message should
be a list of 1 or more requests which will be executed in order. Each request
will have a corresponding response in the same order.
Every individual request and response is a JSON object with at minimum one
field `type`. The value of `type` determines what other fields may exist.
To get the script version, instead of JSON, send "VERSION" to get the script
version directly (e.g. "2").
#### Ex. 1
Request: `[{"type": "PING"}]`
Response: `[{"type": "PONG"}]`
---
#### Ex. 2
Request: `[{"type": "LOCK"}, {"type": "HASH"}]`
Response: `[{"type": "LOCKED"}, {"type": "HASH_RESPONSE", "value": "F7D18982"}]`
---
#### Ex. 3
Request:
```json
[
{"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"},
{"type": "READ", "address": 500, "size": 4, "domain": "ROM"}
]
```
Response:
```json
[
{"type": "GUARD_RESPONSE", "address": 100, "value": true},
{"type": "READ_RESPONSE", "value": "dGVzdA=="}
]
```
---
#### Ex. 4
Request:
```json
[
{"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"},
{"type": "READ", "address": 500, "size": 4, "domain": "ROM"}
]
```
Response:
```json
[
{"type": "GUARD_RESPONSE", "address": 100, "value": false},
{"type": "GUARD_RESPONSE", "address": 100, "value": false}
]
```
---
### Supported Request Types
- `PING`
Does nothing; resets timeout.
Expected Response Type: `PONG`
- `SYSTEM`
Returns the system of the currently loaded ROM (N64, GBA, etc...).
Expected Response Type: `SYSTEM_RESPONSE`
- `PREFERRED_CORES`
Returns the user's default cores for systems with multiple cores. If the
current ROM's system has multiple cores, the one that is currently
running is very probably the preferred core.
Expected Response Type: `PREFERRED_CORES_RESPONSE`
- `HASH`
Returns the hash of the currently loaded ROM calculated by BizHawk.
Expected Response Type: `HASH_RESPONSE`
- `GUARD`
Checks a section of memory against `expected_data`. If the bytes starting
at `address` do not match `expected_data`, the response will have `value`
set to `false`, and all subsequent requests will not be executed and
receive the same `GUARD_RESPONSE`.
Expected Response Type: `GUARD_RESPONSE`
Additional Fields:
- `address` (`int`): The address of the memory to check
- `expected_data` (string): A base64 string of contiguous data
- `domain` (`string`): The name of the memory domain the address
corresponds to
- `LOCK`
Halts emulation and blocks on incoming requests until an `UNLOCK` request
is received or the client times out. All requests processed while locked
will happen on the same frame.
Expected Response Type: `LOCKED`
- `UNLOCK`
Resumes emulation after the current list of requests is done being
executed.
Expected Response Type: `UNLOCKED`
- `READ`
Reads an array of bytes at the provided address.
Expected Response Type: `READ_RESPONSE`
Additional Fields:
- `address` (`int`): The address of the memory to read
- `size` (`int`): The number of bytes to read
- `domain` (`string`): The name of the memory domain the address
corresponds to
- `WRITE`
Writes an array of bytes to the provided address.
Expected Response Type: `WRITE_RESPONSE`
Additional Fields:
- `address` (`int`): The address of the memory to write to
- `value` (`string`): A base64 string representing the data to write
- `domain` (`string`): The name of the memory domain the address
corresponds to
- `DISPLAY_MESSAGE`
Adds a message to the message queue which will be displayed using
`gui.addmessage` according to the message interval.
Expected Response Type: `DISPLAY_MESSAGE_RESPONSE`
Additional Fields:
- `message` (`string`): The string to display
- `SET_MESSAGE_INTERVAL`
Sets the minimum amount of time to wait between displaying messages.
Potentially useful if you add many messages quickly but want players
to be able to read each of them.
Expected Response Type: `SET_MESSAGE_INTERVAL_RESPONSE`
Additional Fields:
- `value` (`number`): The number of seconds to set the interval to
### Response Types
- `PONG`
Acknowledges `PING`.
- `SYSTEM_RESPONSE`
Contains the name of the system for currently running ROM.
Additional Fields:
- `value` (`string`): The returned system name
- `PREFERRED_CORES_RESPONSE`
Contains the user's preferred cores for systems with multiple supported
cores. Currently includes NES, SNES, GB, GBC, DGB, SGB, PCE, PCECD, and
SGX.
Additional Fields:
- `value` (`{[string]: [string]}`): A dictionary map from system name to
core name
- `HASH_RESPONSE`
Contains the hash of the currently loaded ROM calculated by BizHawk.
Additional Fields:
- `value` (`string`): The returned hash
- `GUARD_RESPONSE`
The result of an attempted `GUARD` request.
Additional Fields:
- `value` (`boolean`): true if the memory was validated, false if not
- `address` (`int`): The address of the memory that was invalid (the same
address provided by the `GUARD`, not the address of the individual invalid
byte)
- `LOCKED`
Acknowledges `LOCK`.
- `UNLOCKED`
Acknowledges `UNLOCK`.
- `READ_RESPONSE`
Contains the result of a `READ` request.
Additional Fields:
- `value` (`string`): A base64 string representing the read data
- `WRITE_RESPONSE`
Acknowledges `WRITE`.
- `DISPLAY_MESSAGE_RESPONSE`
Acknowledges `DISPLAY_MESSAGE`.
- `SET_MESSAGE_INTERVAL_RESPONSE`
Acknowledges `SET_MESSAGE_INTERVAL`.
- `ERROR`
Signifies that something has gone wrong while processing a request.
Additional Fields:
- `err` (`string`): A description of the problem
]]
local base64 = require("base64")
local socket = require("socket")
local json = require("json")
-- Set to log incoming requests
-- Will cause lag due to large console output
local DEBUG = false
local SOCKET_PORT = 43055
local STATE_NOT_CONNECTED = 0
local STATE_CONNECTED = 1
local server = nil
local client_socket = nil
local current_state = STATE_NOT_CONNECTED
local timeout_timer = 0
local message_timer = 0
local message_interval = 0
local prev_time = 0
local current_time = 0
local locked = false
local rom_hash = nil
local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)")
lua_major = tonumber(lua_major)
lua_minor = tonumber(lua_minor)
if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then
require("lua_5_3_compat")
end
local bizhawk_version = client.getversion()
local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
bizhawk_major = tonumber(bizhawk_major)
bizhawk_minor = tonumber(bizhawk_minor)
if bizhawk_patch == "" then
bizhawk_patch = 0
else
bizhawk_patch = tonumber(bizhawk_patch)
end
function queue_push (self, value)
self[self.right] = value
self.right = self.right + 1
end
function queue_is_empty (self)
return self.right == self.left
end
function queue_shift (self)
value = self[self.left]
self[self.left] = nil
self.left = self.left + 1
return value
end
function new_queue ()
local queue = {left = 1, right = 1}
return setmetatable(queue, {__index = {is_empty = queue_is_empty, push = queue_push, shift = queue_shift}})
end
local message_queue = new_queue()
function lock ()
locked = true
client_socket:settimeout(2)
end
function unlock ()
locked = false
client_socket:settimeout(0)
end
function process_request (req)
local res = {}
if req["type"] == "PING" then
res["type"] = "PONG"
elseif req["type"] == "SYSTEM" then
res["type"] = "SYSTEM_RESPONSE"
res["value"] = emu.getsystemid()
elseif req["type"] == "PREFERRED_CORES" then
local preferred_cores = client.getconfig().PreferredCores
res["type"] = "PREFERRED_CORES_RESPONSE"
res["value"] = {}
res["value"]["NES"] = preferred_cores.NES
res["value"]["SNES"] = preferred_cores.SNES
res["value"]["GB"] = preferred_cores.GB
res["value"]["GBC"] = preferred_cores.GBC
res["value"]["DGB"] = preferred_cores.DGB
res["value"]["SGB"] = preferred_cores.SGB
res["value"]["PCE"] = preferred_cores.PCE
res["value"]["PCECD"] = preferred_cores.PCECD
res["value"]["SGX"] = preferred_cores.SGX
elseif req["type"] == "HASH" then
res["type"] = "HASH_RESPONSE"
res["value"] = rom_hash
elseif req["type"] == "GUARD" then
res["type"] = "GUARD_RESPONSE"
local expected_data = base64.decode(req["expected_data"])
local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"])
local data_is_validated = true
for i, byte in ipairs(actual_data) do
if byte ~= expected_data[i] then
data_is_validated = false
break
end
end
res["value"] = data_is_validated
res["address"] = req["address"]
elseif req["type"] == "LOCK" then
res["type"] = "LOCKED"
lock()
elseif req["type"] == "UNLOCK" then
res["type"] = "UNLOCKED"
unlock()
elseif req["type"] == "READ" then
res["type"] = "READ_RESPONSE"
res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"]))
elseif req["type"] == "WRITE" then
res["type"] = "WRITE_RESPONSE"
memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"])
elseif req["type"] == "DISPLAY_MESSAGE" then
res["type"] = "DISPLAY_MESSAGE_RESPONSE"
message_queue:push(req["message"])
elseif req["type"] == "SET_MESSAGE_INTERVAL" then
res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE"
message_interval = req["value"]
else
res["type"] = "ERROR"
res["err"] = "Unknown command: "..req["type"]
end
return res
end
-- Receive data from AP client and send message back
function send_receive ()
local message, err = client_socket:receive()
-- Handle errors
if err == "closed" then
if current_state == STATE_CONNECTED then
print("Connection to client closed")
end
current_state = STATE_NOT_CONNECTED
return
elseif err == "timeout" then
unlock()
return
elseif err ~= nil then
print(err)
current_state = STATE_NOT_CONNECTED
unlock()
return
end
-- Reset timeout timer
timeout_timer = 5
-- Process received data
if DEBUG then
print("Received Message ["..emu.framecount().."]: "..'"'..message..'"')
end
if message == "VERSION" then
local result, err client_socket:send(tostring(SCRIPT_VERSION).."\n")
else
local res = {}
local data = json.decode(message)
local failed_guard_response = nil
for i, req in ipairs(data) do
if failed_guard_response ~= nil then
res[i] = failed_guard_response
else
-- An error is more likely to cause an NLua exception than to return an error here
local status, response = pcall(process_request, req)
if status then
res[i] = response
-- If the GUARD validation failed, skip the remaining commands
if response["type"] == "GUARD_RESPONSE" and not response["value"] then
failed_guard_response = response
end
else
res[i] = {type = "ERROR", err = response}
end
end
end
client_socket:send(json.encode(res).."\n")
end
end
function main ()
server, err = socket.bind("localhost", SOCKET_PORT)
if err ~= nil then
print(err)
return
end
while true do
current_time = socket.socket.gettime()
timeout_timer = timeout_timer - (current_time - prev_time)
message_timer = message_timer - (current_time - prev_time)
prev_time = current_time
if message_timer <= 0 and not message_queue:is_empty() then
gui.addmessage(message_queue:shift())
message_timer = message_interval
end
if current_state == STATE_NOT_CONNECTED then
if emu.framecount() % 60 == 0 then
server:settimeout(2)
local client, timeout = server:accept()
if timeout == nil then
print("Client connected")
current_state = STATE_CONNECTED
client_socket = client
client_socket:settimeout(0)
else
print("No client found. Trying again...")
end
end
else
repeat
send_receive()
until not locked
if timeout_timer <= 0 then
print("Client timed out")
current_state = STATE_NOT_CONNECTED
end
end
coroutine.yield()
end
end
event.onexit(function ()
print("\n-- Restarting Script --\n")
if server ~= nil then
server:close()
end
end)
if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then
print("Must use BizHawk 2.7.0 or newer")
elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then
print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.")
else
if emu.getsystemid() == "NULL" then
print("No ROM is loaded. Please load a ROM.")
while emu.getsystemid() == "NULL" do
emu.frameadvance()
end
end
rom_hash = gameinfo.getromhash()
print("Waiting for client to connect. Emulation will freeze intermittently until a client is found.\n")
local co = coroutine.create(main)
function tick ()
local status, err = coroutine.resume(co)
if not status then
print("\nERROR: "..err)
print("Consider reporting this crash.\n")
if server ~= nil then
server:close()
end
co = coroutine.create(main)
end
end
-- Gambatte has a setting which can cause script execution to become
-- misaligned, so for GB and GBC we explicitly set the callback on
-- vblank instead.
-- https://github.com/TASEmulators/BizHawk/issues/3711
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then
event.onmemoryexecute(tick, 0x40, "tick", "System Bus")
else
event.onframeend(tick)
end
while true do
emu.frameadvance()
end
end

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

View File

@@ -559,12 +559,6 @@ def generate_basic(self) -> None:
# in most cases it's better to do this at the same time the itempool is # in most cases it's better to do this at the same time the itempool is
# filled to avoid accidental duplicates: # filled to avoid accidental duplicates:
# manually placed and still in the itempool # manually placed and still in the itempool
# for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to
# write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations
# are connected and placed as desired
# from Utils import visualize_regions
# visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
``` ```
### Setting Rules ### Setting Rules

View File

@@ -74,7 +74,6 @@ Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup";
Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/bizhawk"; Description: "BizHawk Client"; Types: full playing
Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
@@ -117,13 +116,11 @@ 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
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#source_path}\ArchipelagoBizHawkClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/bizhawk
Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
@@ -148,7 +145,6 @@ Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
Name: "{group}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Components: client/bizhawk
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
@@ -169,7 +165,6 @@ Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopic
Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
Name: "{commondesktop}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Tasks: desktopicon; Components: client/bizhawk
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot

10
kvui.py
View File

@@ -1,17 +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
try:
ctypes.windll.shcore.SetProcessDpiAwareness(0)
except FileNotFoundError: # shcore may not be found on <= Windows 7
pass # TODO: remove silent except when Python 3.8 is phased out.
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

@@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
game: # Pick a game to play game: # Pick a game to play
A Link to the Past: 1 A Link to the Past: 1
requires: requires:
version: 0.4.3 # Version of Archipelago required for this yaml to work as expected. version: 0.3.3 # Version of Archipelago required for this yaml to work as expected.
A Link to the Past: A Link to the Past:
progression_balancing: progression_balancing:
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. # A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
@@ -114,9 +114,6 @@ A Link to the Past:
different_world: 0 different_world: 0
universal: 0 universal: 0
start_with: 0 start_with: 0
key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies
off: 50
on: 0
compass_shuffle: # Compass Placement compass_shuffle: # Compass Placement
original_dungeon: 50 original_dungeon: 50
own_dungeons: 0 own_dungeons: 0

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

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