Compare commits
74 Commits
custom_web
...
0.4.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aed2be93b | ||
|
|
974bab2b24 | ||
|
|
98d61b32af | ||
|
|
4141a50d8c | ||
|
|
93c18cd9a7 | ||
|
|
5af47425b0 | ||
|
|
b41a1e69b4 | ||
|
|
124113f3d3 | ||
|
|
2b69820619 | ||
|
|
f147f9e5a0 | ||
|
|
db7c0c9db9 | ||
|
|
b40fba0840 | ||
|
|
ea799c494e | ||
|
|
b4b8426def | ||
|
|
39a50da55c | ||
|
|
9931605f94 | ||
|
|
8834ba88aa | ||
|
|
5e46967b7d | ||
|
|
638d6807db | ||
|
|
d471dcc067 | ||
|
|
4a27fae1ab | ||
|
|
794959e182 | ||
|
|
aff852fb45 | ||
|
|
a0eea3a650 | ||
|
|
0012584e51 | ||
|
|
6e02a4ca3c | ||
|
|
2ef05a1799 | ||
|
|
fa2891f785 | ||
|
|
d5d13a6d4d | ||
|
|
b24037e9d9 | ||
|
|
6d6de4a98e | ||
|
|
0e7c7bd1bf | ||
|
|
9312f14ffb | ||
|
|
ce8f07b347 | ||
|
|
cff6c7c4da | ||
|
|
f9120c620f | ||
|
|
44f1a93d31 | ||
|
|
6d61eae522 | ||
|
|
f05a9ecd2f | ||
|
|
648d682add | ||
|
|
47cf3e06c0 | ||
|
|
fdac50523b | ||
|
|
7522a32ad6 | ||
|
|
8ee743ac8a | ||
|
|
c3cfbf8e1c | ||
|
|
1756a30acc | ||
|
|
57c13ff273 | ||
|
|
3d9837678c | ||
|
|
3e95ccd06c | ||
|
|
0e21a3e121 | ||
|
|
5eef7a34d3 | ||
|
|
6c844750ae | ||
|
|
8649b15787 | ||
|
|
fbd64651e4 | ||
|
|
e01eb4e00c | ||
|
|
72b44be41c | ||
|
|
2bdb1b2029 | ||
|
|
bf685dc850 | ||
|
|
faf4887616 | ||
|
|
a1418ccb66 | ||
|
|
29f8053d6e | ||
|
|
f6dafa2b56 | ||
|
|
2b9e8fa273 | ||
|
|
5368451867 | ||
|
|
77a349c1c6 | ||
|
|
c4a3204af7 | ||
|
|
9323f7d892 | ||
|
|
30e747bb4c | ||
|
|
9d29c6d301 | ||
|
|
aa19a79d26 | ||
|
|
5a34471266 | ||
|
|
ae96010ff1 | ||
|
|
944fe6cb8c | ||
|
|
21baa302d4 |
@@ -853,14 +853,6 @@ class Region:
|
||||
state.update_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
|
||||
def hint_text(self) -> str:
|
||||
return self._hint_text if self._hint_text else self.name
|
||||
|
||||
8
Fill.py
@@ -840,12 +840,12 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
|
||||
if "early_locations" in locations:
|
||||
locations.remove("early_locations")
|
||||
for player in worlds:
|
||||
locations += early_locations[player]
|
||||
for target_player in worlds:
|
||||
locations += early_locations[target_player]
|
||||
if "non_early_locations" in locations:
|
||||
locations.remove("non_early_locations")
|
||||
for player in worlds:
|
||||
locations += non_early_locations[player]
|
||||
for target_player in worlds:
|
||||
locations += non_early_locations[target_player]
|
||||
|
||||
block['locations'] = locations
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@ from urllib.request import urlopen
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from worlds.alttp.Rom import LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||
from worlds.alttp.Sprites import Sprite
|
||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||
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
|
||||
|
||||
|
||||
15
Main.py
@@ -139,7 +139,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
exclusion_rules(world, player, world.exclude_locations[player].value)
|
||||
world.priority_locations[player].value -= world.exclude_locations[player].value
|
||||
for location_name in world.priority_locations[player].value:
|
||||
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
|
||||
try:
|
||||
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.
|
||||
if world.players > 1:
|
||||
@@ -159,7 +165,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for player, items in depletion_pool.items():
|
||||
player_world: AutoWorld.World = world.worlds[player]
|
||||
for count in items.values():
|
||||
new_items.append(player_world.create_filler())
|
||||
for _ in range(count):
|
||||
new_items.append(player_world.create_filler())
|
||||
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
||||
for i, item in enumerate(world.itempool):
|
||||
if depletion_pool[item.player].get(item.name, 0):
|
||||
@@ -179,6 +186,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
if remaining_items:
|
||||
raise Exception(f"{world.get_player_name(player)}"
|
||||
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
|
||||
|
||||
# temporary home for item links, should be moved out of Main
|
||||
@@ -392,7 +400,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
f.write(bytes([3])) # version of format
|
||||
f.write(multidata)
|
||||
|
||||
multidata_task = pool.submit(write_multidata)
|
||||
output_file_futures.append(pool.submit(write_multidata))
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
@@ -400,7 +408,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
|
||||
# retrieve exceptions via .result() if they occurred.
|
||||
multidata_task.result()
|
||||
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
||||
if i % 10 == 0 or i == len(output_file_futures):
|
||||
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
||||
|
||||
24
NetUtils.py
@@ -407,14 +407,22 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
|
||||
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
|
||||
LocationStore = _LocationStore
|
||||
else:
|
||||
try:
|
||||
import pyximport
|
||||
pyximport.install()
|
||||
except ImportError:
|
||||
pyximport = None
|
||||
try:
|
||||
from _speedups import LocationStore
|
||||
import _speedups
|
||||
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:
|
||||
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
|
||||
"Install a matching C++ compiler for your platform to compile _speedups.")
|
||||
LocationStore = _LocationStore
|
||||
try:
|
||||
import pyximport
|
||||
pyximport.install()
|
||||
except ImportError:
|
||||
pyximport = None
|
||||
try:
|
||||
from _speedups import 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
|
||||
|
||||
23
SNIClient.py
@@ -68,12 +68,11 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
||||
options = snes_options.split()
|
||||
num_options = len(options)
|
||||
|
||||
if num_options > 0:
|
||||
snes_device_number = int(options[0])
|
||||
|
||||
if num_options > 1:
|
||||
snes_address = options[0]
|
||||
snes_device_number = int(options[1])
|
||||
elif num_options > 0:
|
||||
snes_device_number = int(options[0])
|
||||
|
||||
self.ctx.snes_reconnect_address = None
|
||||
if self.ctx.snes_connect_task:
|
||||
@@ -565,14 +564,16 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
|
||||
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||
try:
|
||||
for address, data in write_list:
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||
# REVIEW: above: `if snes_socket is None: return False`
|
||||
# Does it need to be checked again?
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
await ctx.snes_socket.send(data)
|
||||
else:
|
||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||
while data:
|
||||
# Divide the write into packets of 256 bytes.
|
||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
||||
if ctx.snes_socket is not None:
|
||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||
await ctx.snes_socket.send(data[:256])
|
||||
address += 256
|
||||
data = data[256:]
|
||||
else:
|
||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||
except ConnectionClosed:
|
||||
return False
|
||||
|
||||
|
||||
1050
Starcraft2Client.py
@@ -29,31 +29,31 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_patch(self):
|
||||
"""Patch the game."""
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
||||
self.ctx.patch_game()
|
||||
self.output("Patched.")
|
||||
|
||||
def _cmd_savepath(self, directory: str):
|
||||
"""Redirect to proper save data folder. (Use before connecting!)"""
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
UndertaleContext.save_game_folder = directory
|
||||
self.output("Changed to the following directory: " + directory)
|
||||
self.ctx.save_game_folder = directory
|
||||
self.output("Changed to the following directory: " + self.ctx.save_game_folder)
|
||||
|
||||
@mark_raw
|
||||
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
||||
"""Patch the game automatically."""
|
||||
if isinstance(self.ctx, UndertaleContext):
|
||||
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
||||
tempInstall = steaminstall
|
||||
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||
tempInstall = None
|
||||
if tempInstall is None:
|
||||
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
||||
if not os.path.exists(tempInstall):
|
||||
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||
elif not os.path.exists(tempInstall):
|
||||
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
||||
if not os.path.exists(tempInstall):
|
||||
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")):
|
||||
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
|
||||
@@ -61,8 +61,8 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
||||
else:
|
||||
for file_name in os.listdir(tempInstall):
|
||||
if file_name != "steam_api.dll":
|
||||
shutil.copy(tempInstall+"\\"+file_name,
|
||||
os.getcwd() + "\\Undertale\\" + file_name)
|
||||
shutil.copy(os.path.join(tempInstall, file_name),
|
||||
os.path.join(os.getcwd(), "Undertale", file_name))
|
||||
self.ctx.patch_game()
|
||||
self.output("Patching successful!")
|
||||
|
||||
@@ -111,13 +111,13 @@ class UndertaleContext(CommonContext):
|
||||
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||
|
||||
def patch_game(self):
|
||||
with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
|
||||
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f:
|
||||
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
||||
with open(os.getcwd() + "/Undertale/data.win", "wb") as f:
|
||||
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f:
|
||||
f.write(patchedFile)
|
||||
os.makedirs(name=os.getcwd() + "\\Undertale\\" + "Custom Sprites", exist_ok=True)
|
||||
with open(os.path.expandvars(os.getcwd() + "\\Undertale\\" + "Custom Sprites\\" +
|
||||
"Which Character.txt"), "w") as f:
|
||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True)
|
||||
with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites",
|
||||
"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 "
|
||||
"line other than this one.\n", "frisk"])
|
||||
f.close()
|
||||
@@ -385,7 +385,7 @@ async def multi_watcher(ctx: UndertaleContext):
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
if "spots.mine" in file and "Online" in ctx.tags:
|
||||
with open(root + "/" + file, "r") as mine:
|
||||
with open(os.path.join(root, file), "r") as mine:
|
||||
this_x = mine.readline()
|
||||
this_y = 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 file in files:
|
||||
if ".item" in file:
|
||||
os.remove(root+"/"+file)
|
||||
os.remove(os.path.join(root, file))
|
||||
sync_msg = [{"cmd": "Sync"}]
|
||||
if 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 file in files:
|
||||
if "DontBeMad.mad" in file:
|
||||
os.remove(root+"/"+file)
|
||||
os.remove(os.path.join(root, file))
|
||||
if "DeathLink" in ctx.tags:
|
||||
await ctx.send_death()
|
||||
if "scout" == file:
|
||||
sending = []
|
||||
try:
|
||||
with open(root+"/"+file, "r") as f:
|
||||
with open(os.path.join(root, file), "r") as f:
|
||||
lines = f.readlines()
|
||||
for l in lines:
|
||||
if ctx.server_locations.__contains__(int(l)+12000):
|
||||
@@ -438,11 +438,11 @@ async def game_watcher(ctx: UndertaleContext):
|
||||
finally:
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
||||
"create_as_hint": int(2)}])
|
||||
os.remove(root+"/"+file)
|
||||
os.remove(os.path.join(root, file))
|
||||
if "check.spot" in file:
|
||||
sending = []
|
||||
try:
|
||||
with open(root+"/"+file, "r") as f:
|
||||
with open(os.path.join(root, file), "r") as f:
|
||||
lines = f.readlines()
|
||||
for l in lines:
|
||||
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:
|
||||
victory = True
|
||||
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||
os.remove(root+"/"+file)
|
||||
os.remove(os.path.join(root, file))
|
||||
if "victory" in file:
|
||||
if str(ctx.route) == "all_routes":
|
||||
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
||||
|
||||
29
Utils.py
@@ -44,7 +44,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.4.2"
|
||||
__version__ = "0.4.3"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -359,11 +359,13 @@ safe_builtins = frozenset((
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
generic_properties_module: Optional[object]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||
self.options_module = importlib.import_module("Options")
|
||||
self.net_utils_module = importlib.import_module("NetUtils")
|
||||
self.generic_properties_module = importlib.import_module("worlds.generic")
|
||||
self.generic_properties_module = None
|
||||
|
||||
def find_class(self, module, name):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
@@ -373,6 +375,8 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
return getattr(self.net_utils_module, name)
|
||||
# Options and Plando are unpickled by WebHost -> Generate
|
||||
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)
|
||||
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
||||
if module.lower().endswith("options"):
|
||||
@@ -572,7 +576,7 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
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)
|
||||
|
||||
# fall back to tk
|
||||
@@ -584,7 +588,10 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
||||
f'This attempt was made because open_filename was used for "{title}".')
|
||||
raise e
|
||||
else:
|
||||
root = tkinter.Tk()
|
||||
try:
|
||||
root = tkinter.Tk()
|
||||
except tkinter.TclError:
|
||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||
root.withdraw()
|
||||
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None)
|
||||
@@ -597,13 +604,14 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||
if is_linux:
|
||||
# prefer native dialog
|
||||
from shutil import which
|
||||
kdialog = None#which("kdialog")
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
return run(kdialog, f"--title={title}", "--getexistingdirectory", suggest or ".")
|
||||
zenity = None#which("zenity")
|
||||
return run(kdialog, f"--title={title}", "--getexistingdirectory",
|
||||
os.path.abspath(suggest) if suggest else ".")
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
z_filters = ("--directory",)
|
||||
selection = (f'--filename="{suggest}',) if suggest else ()
|
||||
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
|
||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||
|
||||
# fall back to tk
|
||||
@@ -615,7 +623,10 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||
f'This attempt was made because open_filename was used for "{title}".')
|
||||
raise e
|
||||
else:
|
||||
root = tkinter.Tk()
|
||||
try:
|
||||
root = tkinter.Tk()
|
||||
except tkinter.TclError:
|
||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||
root.withdraw()
|
||||
return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None)
|
||||
|
||||
|
||||
37
WebHost.py
@@ -13,15 +13,6 @@ import Utils
|
||||
import settings
|
||||
|
||||
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
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
@@ -29,6 +20,9 @@ if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
|
||||
|
||||
def get_app():
|
||||
from WebHostLib import register, cache, app as raw_app
|
||||
from WebHostLib.models import db
|
||||
|
||||
register()
|
||||
app = raw_app
|
||||
if os.path.exists(configpath) and not app.config["TESTING"]:
|
||||
@@ -40,15 +34,9 @@ def get_app():
|
||||
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
|
||||
|
||||
cache.init_app(app)
|
||||
db.bind(**app.config["PONY"])
|
||||
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
|
||||
|
||||
|
||||
@@ -128,16 +116,16 @@ if __name__ == "__main__":
|
||||
multiprocessing.set_start_method('spawn')
|
||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||
|
||||
for world in worlds.AutoWorldRegister.world_types.values():
|
||||
try:
|
||||
world.web.run_webhost_setup()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||
from WebHostLib.autolauncher import autohost, autogen
|
||||
from WebHostLib.options import create as create_options_files
|
||||
|
||||
try:
|
||||
update_sprites_lttp()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
logging.warning("Could not update LttP sprites.")
|
||||
app = get_app()
|
||||
|
||||
del world, worlds
|
||||
|
||||
create_options_files()
|
||||
create_ordered_tutorials_file()
|
||||
if app.config["SELFLAUNCH"]:
|
||||
@@ -148,4 +136,5 @@ if __name__ == "__main__":
|
||||
if app.config["DEBUG"]:
|
||||
app.run(debug=True, port=app.config["PORT"])
|
||||
else:
|
||||
from waitress import serve
|
||||
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
||||
|
||||
@@ -49,11 +49,11 @@ app.config["PONY"] = {
|
||||
'create_db': True
|
||||
}
|
||||
app.config["MAX_ROLL"] = 20
|
||||
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
|
||||
app.config["CACHE_TYPE"] = "SimpleCache"
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
app.config["HOST_ADDRESS"] = ""
|
||||
|
||||
cache = Cache(app)
|
||||
cache = Cache()
|
||||
Compress(app)
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
@@ -13,55 +11,7 @@ from datetime import timedelta, datetime
|
||||
from pony.orm import db_session, select, commit
|
||||
|
||||
from Utils import restricted_loads
|
||||
|
||||
|
||||
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()
|
||||
from .locker import Locker, AlreadyRunningException
|
||||
|
||||
|
||||
def launch_room(room: Room, config: dict):
|
||||
|
||||
@@ -24,8 +24,8 @@ def check():
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
else:
|
||||
file = request.files['file']
|
||||
options = get_yaml_data(file)
|
||||
files = request.files.getlist('file')
|
||||
options = get_yaml_data(files)
|
||||
if isinstance(options, str):
|
||||
flash(options)
|
||||
else:
|
||||
@@ -39,30 +39,33 @@ def mysterycheck():
|
||||
return redirect(url_for("check"), 301)
|
||||
|
||||
|
||||
def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
|
||||
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
|
||||
options = {}
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if file.filename == '':
|
||||
return 'No selected file'
|
||||
elif file and allowed_file(file.filename):
|
||||
if file.filename.endswith(".zip"):
|
||||
for file in files:
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if file.filename == '':
|
||||
return 'No selected file'
|
||||
elif file.filename in options:
|
||||
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:
|
||||
infolist = zfile.infolist()
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
infolist = zfile.infolist()
|
||||
|
||||
if any(file.filename.endswith(".archipelago") for file in infolist):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
if any(file.filename.endswith(".archipelago") for file in infolist):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
|
||||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||
"Your file was deleted."
|
||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
else:
|
||||
options = {file.filename: file.read()}
|
||||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||
"Your file was deleted."
|
||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
else:
|
||||
options[file.filename] = file.read()
|
||||
if not options:
|
||||
return "Did not find a .yaml file to process."
|
||||
return options
|
||||
|
||||
@@ -19,6 +19,7 @@ import Utils
|
||||
|
||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
||||
from Utils import restricted_loads, cache_argsless
|
||||
from .locker import Locker
|
||||
from .models import Command, GameDataPackage, Room, db
|
||||
|
||||
|
||||
@@ -163,16 +164,19 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
db.generate_mapping(check_tables=False)
|
||||
|
||||
async def main():
|
||||
import gc
|
||||
|
||||
Utils.init_logging(str(room_id), write_mode="a")
|
||||
ctx = WebHostContext(static_server_data)
|
||||
ctx.load(room_id)
|
||||
ctx.init_save()
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||
gc.collect() # free intermediate objects used during setup
|
||||
try:
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
|
||||
await ctx.server
|
||||
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
||||
except OSError: # likely port in use
|
||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||
|
||||
await ctx.server
|
||||
@@ -198,16 +202,15 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
await ctx.shutdown_task
|
||||
logging.info("Shutting down")
|
||||
|
||||
from .autolauncher import Locker
|
||||
with Locker(room_id):
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
# 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)
|
||||
except:
|
||||
except Exception:
|
||||
with db_session:
|
||||
room = Room.get(id=room_id)
|
||||
room.last_port = -1
|
||||
|
||||
@@ -64,8 +64,8 @@ def generate(race=False):
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
else:
|
||||
file = request.files['file']
|
||||
options = get_yaml_data(file)
|
||||
files = request.files.getlist('file')
|
||||
options = get_yaml_data(files)
|
||||
if isinstance(options, str):
|
||||
flash(options)
|
||||
else:
|
||||
|
||||
51
WebHostLib/locker.py
Normal file
@@ -0,0 +1,51 @@
|
||||
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()
|
||||
50
WebHostLib/lttpsprites.py
Normal file
@@ -0,0 +1,50 @@
|
||||
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
|
||||
@@ -3,7 +3,8 @@ 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
|
||||
Flask-Caching>=2.0.2
|
||||
Flask-Compress>=1.13
|
||||
Flask-Limiter>=3.3.0
|
||||
bokeh>=3.1.1
|
||||
Flask-Compress>=1.14
|
||||
Flask-Limiter>=3.5.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.2.2; python_version >= '3.9'
|
||||
markupsafe>=2.1.3
|
||||
|
||||
83
WebHostLib/static/assets/supportedGames.js
Normal file
@@ -0,0 +1,83 @@
|
||||
window.addEventListener('load', () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
header.addEventListener('click', () => {
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
if (gameInfo.classList.contains('collapsed')) {
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
} else {
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle game filter input
|
||||
const gameSearch = document.getElementById('game-search');
|
||||
gameSearch.value = '';
|
||||
|
||||
gameSearch.addEventListener('input', (evt) => {
|
||||
if (!evt.target.value.trim()) {
|
||||
// If input is empty, display all collapsed games
|
||||
return Array.from(gameHeaders).forEach((header) => {
|
||||
header.style.display = null;
|
||||
const gameName = header.getAttribute('data-game');
|
||||
document.getElementById(`${gameName}-arrow`).innerText = '▶';
|
||||
document.getElementById(gameName).classList.add('collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
|
||||
// If the game name includes the search string, display the game. If not, hide it
|
||||
if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||
header.style.display = null;
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
} else {
|
||||
console.log(header);
|
||||
header.style.display = 'none';
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('expand-all').addEventListener('click', expandAll);
|
||||
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
||||
});
|
||||
|
||||
const expandAll = () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
|
||||
if (header.style.display === 'none') { return; }
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
});
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
|
||||
if (header.style.display === 'none') { return; }
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
});
|
||||
};
|
||||
@@ -14,6 +14,17 @@ 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', () => {
|
||||
const tables = $(".table").DataTable({
|
||||
paging: false,
|
||||
@@ -27,7 +38,18 @@ window.addEventListener('load', () => {
|
||||
stateLoadCallback: function(settings) {
|
||||
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: [
|
||||
{
|
||||
targets: 'last-activity',
|
||||
name: 'lastActivity'
|
||||
},
|
||||
{
|
||||
targets: 'hours',
|
||||
render: function (data, type, row) {
|
||||
@@ -40,11 +62,7 @@ window.addEventListener('load', () => {
|
||||
if (data === "None")
|
||||
return data;
|
||||
|
||||
let hours = Math.floor(data / 3600);
|
||||
let minutes = Math.floor((data - (hours * 3600)) / 60);
|
||||
|
||||
if (minutes < 10) {minutes = "0"+minutes;}
|
||||
return hours+':'+minutes;
|
||||
return secondsToHours(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -114,11 +132,16 @@ window.addEventListener('load', () => {
|
||||
if (status === "success") {
|
||||
target.find(".table").each(function (i, new_table) {
|
||||
const new_trs = $(new_table).find("tbody>tr");
|
||||
const footer_tr = $(new_table).find("tfoot>tr");
|
||||
const old_table = tables.eq(i);
|
||||
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
|
||||
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
|
||||
old_table.clear();
|
||||
old_table.rows.add(new_trs).draw();
|
||||
if (footer_tr.length) {
|
||||
$(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).scrollLeft(leftscroll);
|
||||
});
|
||||
|
||||
@@ -160,6 +160,7 @@ const buildUI = (settingData) => {
|
||||
weightedSettingsDiv.classList.add('invisible');
|
||||
itemPoolDiv.classList.add('invisible');
|
||||
hintsDiv.classList.add('invisible');
|
||||
locationsDiv.classList.add('invisible');
|
||||
expandButton.classList.remove('invisible');
|
||||
});
|
||||
|
||||
@@ -168,6 +169,7 @@ const buildUI = (settingData) => {
|
||||
weightedSettingsDiv.classList.remove('invisible');
|
||||
itemPoolDiv.classList.remove('invisible');
|
||||
hintsDiv.classList.remove('invisible');
|
||||
locationsDiv.classList.remove('invisible');
|
||||
expandButton.classList.add('invisible');
|
||||
});
|
||||
});
|
||||
@@ -1134,8 +1136,8 @@ const validateSettings = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any disabled options
|
||||
Object.keys(settings[game]).forEach((setting) => {
|
||||
// Remove any disabled options
|
||||
Object.keys(settings[game][setting]).forEach((option) => {
|
||||
if (settings[game][setting][option] === 0) {
|
||||
delete settings[game][setting][option];
|
||||
@@ -1149,6 +1151,32 @@ const validateSettings = () => {
|
||||
) {
|
||||
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'];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1156,6 +1184,11 @@ const validateSettings = () => {
|
||||
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 (errorMessage) {
|
||||
userMessage.innerText = errorMessage;
|
||||
|
||||
BIN
WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/advanceballistics.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/autoturretblackops.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/biomechanicaldrone.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/burstcapacitors.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/cyclone.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/drillingclaws.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/emergencythrusters.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/hellionbattlemode.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/hyperflightrotors.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/hyperfluxor.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/impalerrounds.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/improvedburstlaser.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/improvedsiegemode.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/interferencematrix.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
WebHostLib/static/static/icons/sc2/internalizedtechmodule.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
WebHostLib/static/static/icons/sc2/jotunboosters.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/jumpjets.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
WebHostLib/static/static/icons/sc2/lasertargetingsystem.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/liberator.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/lockdown.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
WebHostLib/static/static/icons/sc2/magfieldaccelerator.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/magrailmunitions.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/opticalflare.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/optimizedlogistics.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/reapercombatdrugs.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
WebHostLib/static/static/icons/sc2/restoration.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/ripwavemissiles.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/shreddermissile.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/siegetank-spidermines.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/siegetankrange.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/specialordance.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/spidermine.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/staticempblast.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/superstimpack.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
WebHostLib/static/static/icons/sc2/targetingoptics.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/terran-cloak-color.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
WebHostLib/static/static/icons/sc2/terran-emp-color.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/thorsiegemode.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/transformationservos.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/valkyrie.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/warpjump.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine-attackrange.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowminehidden.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -9,7 +9,7 @@
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 500px;
|
||||
width: 710px;
|
||||
background-color: #525494;
|
||||
}
|
||||
|
||||
@@ -34,10 +34,12 @@
|
||||
max-height: 40px;
|
||||
border: 1px solid #000000;
|
||||
filter: grayscale(100%) contrast(75%) brightness(20%);
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
@@ -52,7 +54,7 @@
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 500px;
|
||||
width: 710px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#games h2 .collapse-arrow{
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#games p.collapsed{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#games a{
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -31,3 +41,13 @@
|
||||
line-height: 25px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
#games #page-controls{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#games #page-controls button{
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -55,16 +55,16 @@ table.dataTable thead{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody{
|
||||
table.dataTable tbody, table.dataTable tfoot{
|
||||
background-color: #dce2bd;
|
||||
font-family: LexendDeca-Light, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody tr:hover{
|
||||
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{
|
||||
background-color: #e2eabb;
|
||||
}
|
||||
|
||||
table.dataTable tbody td{
|
||||
table.dataTable tbody td, table.dataTable tfoot td{
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
@@ -97,10 +97,14 @@ table.dataTable thead th.lower-row{
|
||||
top: 46px;
|
||||
}
|
||||
|
||||
table.dataTable tbody td{
|
||||
table.dataTable tbody td, table.dataTable tfoot td{
|
||||
border: 1px solid #bba967;
|
||||
}
|
||||
|
||||
table.dataTable tfoot td{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody{
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
</p>
|
||||
<div id="check-form-wrapper">
|
||||
<form id="check-form" method="post" enctype="multipart/form-data">
|
||||
<input id="file-input" type="file" name="file">
|
||||
<input id="file-input" type="file" name="file" multiple>
|
||||
</form>
|
||||
<button id="check-button">Upload</button>
|
||||
<button id="check-button">Upload File(s)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,10 +203,10 @@ Warning: playthrough can take a significant amount of time for larger multiworld
|
||||
</div>
|
||||
</div>
|
||||
<div id="generate-form-button-row">
|
||||
<input id="file-input" type="file" name="file">
|
||||
<input id="file-input" type="file" name="file" multiple>
|
||||
</div>
|
||||
</form>
|
||||
<button id="generate-game-button">Upload File</button>
|
||||
<button id="generate-game-button">Upload File(s)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
{% endblock %}
|
||||
{% block custom_table_row scoped %}
|
||||
{% if games[player] == "Factorio" %}
|
||||
{% set player_inventory = inventory[team][player] %}
|
||||
{% set prog_science = player_inventory[item_name_to_id["progressive-science-pack"]] %}
|
||||
<td class="center-column">{% if player_inventory[item_name_to_id["logistic-science-pack"]] or prog_science %}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory[item_name_to_id["military-science-pack"]] or prog_science > 1%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory[item_name_to_id["chemical-science-pack"]] or prog_science > 2%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory[item_name_to_id["production-science-pack"]] or prog_science > 3%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory[item_name_to_id["utility-science-pack"]] or prog_science > 4%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory[item_name_to_id["space-science-pack"]] or prog_science > 5%}✔{% endif %}</td>
|
||||
{% set player_inventory = named_inventory[team][player] %}
|
||||
{% set prog_science = player_inventory["progressive-science-pack"] %}
|
||||
<td class="center-column">{% if player_inventory["logistic-science-pack"] or prog_science %}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["military-science-pack"] or prog_science > 1%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["chemical-science-pack"] or prog_science > 2%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["production-science-pack"] or prog_science > 3%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["utility-science-pack"] or prog_science > 4%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["space-science-pack"] or prog_science > 5%}✔{% endif %}</td>
|
||||
{% else %}
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
@@ -37,7 +37,7 @@
|
||||
{% endblock %}
|
||||
<th class="center-column">Checks</th>
|
||||
<th class="center-column">%</th>
|
||||
<th class="center-column hours">Last<br>Activity</th>
|
||||
<th class="center-column hours last-activity">Last<br>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -64,6 +64,19 @@
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</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>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{%- if enabled_multiworld_trackers|length > 1 -%}
|
||||
<div id="tracker-navigation">
|
||||
{% for enabled_tracker in enabled_multiworld_trackers %}
|
||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker, game=enabled_tracker.name) %}
|
||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
||||
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
|
||||
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
||||
{% endfor %}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td colspan="10" class="title">
|
||||
<td colspan="15" class="title">
|
||||
Starting Resources
|
||||
</td>
|
||||
</tr>
|
||||
@@ -26,7 +26,7 @@
|
||||
-->
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="10" class="title">
|
||||
<td colspan="15" class="title">
|
||||
Weapon & Armor Upgrades
|
||||
</td>
|
||||
</tr>
|
||||
@@ -37,120 +37,266 @@
|
||||
<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_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>
|
||||
<td colspan="10" class="title">
|
||||
<td colspan="15" class="title">
|
||||
Base
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="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['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['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td>
|
||||
<td colspan="2"> </td>
|
||||
</tr>
|
||||
<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['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></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['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
|
||||
<td></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></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>
|
||||
<td colspan="10" class="title">
|
||||
<td><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
|
||||
<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
|
||||
</td>
|
||||
</tr>
|
||||
<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">
|
||||
<td></td>
|
||||
<td colspan="7" class="title">
|
||||
Vehicles
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
|
||||
<td colspan="2"><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
|
||||
<td colspan="2"><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
|
||||
<td colspan="2"><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
|
||||
<td colspan="2"><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
|
||||
<td><img src="{{ stimpack_marine_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marine)' in acquired_items }}" title="{{ stimpack_marine_name }}" /></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['Laser Targeting System (Marine)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marine)' in acquired_items }}" title="Laser Targeting System (Marine)" /></td>
|
||||
<td><img src="{{ icons['Magrail Munitions (Marine)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marine)' in acquired_items }}" title="Magrail Munitions (Marine)" /></td>
|
||||
<td><img src="{{ icons['Optimized Logistics (Marine)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Marine)' in acquired_items }}" title="Optimized Logistics (Marine)" /></td>
|
||||
<td colspan="2"></td>
|
||||
<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['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (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['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></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['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['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['Hellbat Aspect (Hellion)'] }}" class="{{ 'acquired' if 'Hellbat Aspect (Hellion)' in acquired_items }}" title="Hellbat Aspect (Hellion)" /></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 colspan="10" class="title">
|
||||
<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['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['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['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['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>
|
||||
<td><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
|
||||
<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
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="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['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['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['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['Cross-Spectrum Dampeners (Banshee)'] }}" class="{{ 'acquired' if 'Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="Cross-Spectrum Dampeners (Banshee)" /></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['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['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['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>
|
||||
<td colspan="10" class="title">
|
||||
Dominion
|
||||
</td>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Internal Tech Module (Battlecruiser)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Battlecruiser)' in acquired_items }}" title="Internal Tech Module (Battlecruiser)" /></td>
|
||||
<td colspan="6"></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>
|
||||
<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">
|
||||
<td colspan="15" class="title">
|
||||
Mercenaries
|
||||
</td>
|
||||
</tr>
|
||||
@@ -165,36 +311,18 @@
|
||||
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="10" class="title">
|
||||
Lab Upgrades
|
||||
<td colspan="15" class="title">
|
||||
General Upgrades
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
|
||||
<td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
<td colspan="10" class="title">
|
||||
<td colspan="15" class="title">
|
||||
Protoss Units
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -4,16 +4,27 @@
|
||||
<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/supportedGames.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/supportedGames.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="games" class="markdown">
|
||||
<h1>Currently Supported Games</h1>
|
||||
<div>
|
||||
<label for="game-search">Search for your game below!</label><br />
|
||||
<div id="page-controls">
|
||||
<input id="game-search" placeholder="Search by title..." autofocus />
|
||||
<button id="expand-all">Expand All</button>
|
||||
<button id="collapse-all">Collapse All</button>
|
||||
</div>
|
||||
</div>
|
||||
{% for game_name in worlds | title_sorted %}
|
||||
{% set world = worlds[game_name] %}
|
||||
<h2>{{ game_name }}</h2>
|
||||
<p>
|
||||
<h2 class="collapse-toggle" data-game="{{ game_name }}">
|
||||
<span id="{{ game_name }}-arrow" class="collapse-arrow">▶</span> {{ game_name }}
|
||||
</h2>
|
||||
<p id="{{ game_name }}" class="collapsed">
|
||||
{{ 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 %}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import collections
|
||||
import datetime
|
||||
import typing
|
||||
import pkgutil
|
||||
from typing import Counter, Optional, Dict, Any, Tuple, List
|
||||
from uuid import UUID
|
||||
|
||||
from flask import render_template
|
||||
from jinja2 import pass_context, runtime, Template
|
||||
from jinja2 import pass_context, runtime
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from MultiServer import Context, get_saving_second
|
||||
from NetUtils import SlotType, NetworkSlot
|
||||
from NetUtils import ClientStatus, SlotType, NetworkSlot
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, AutoWorldRegister
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games
|
||||
from worlds.alttp import Items
|
||||
from . import app, cache
|
||||
from .models import GameDataPackage, Room
|
||||
@@ -991,6 +990,7 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
||||
SC2WOL_LOC_ID_OFFSET = 1000
|
||||
SC2WOL_ITEM_ID_OFFSET = 1000
|
||||
|
||||
|
||||
icons = {
|
||||
"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",
|
||||
@@ -1035,15 +1035,36 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
||||
"Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
|
||||
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg",
|
||||
@@ -1053,14 +1074,35 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
||||
|
||||
"Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
|
||||
"Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png",
|
||||
"Cerberus Mine (Vulture)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
|
||||
"Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg",
|
||||
@@ -1070,25 +1112,77 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
||||
|
||||
"Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.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",
|
||||
"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",
|
||||
"Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png",
|
||||
"Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
|
||||
"Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg",
|
||||
@@ -1110,14 +1204,15 @@ 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",
|
||||
"Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png",
|
||||
|
||||
"Shrike Turret": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
|
||||
"Fortified Bunker": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
|
||||
"Shrike Turret (Bunker)": "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",
|
||||
"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",
|
||||
"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",
|
||||
"Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
|
||||
"Regenerative Bio-Steel": "https://static.wikia.nocookie.net/starcraft/images/d/d3/SC2_Lab_BioSteel_Icon.png",
|
||||
"Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.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",
|
||||
"Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png",
|
||||
|
||||
@@ -1133,40 +1228,71 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
||||
|
||||
"Nothing": "",
|
||||
}
|
||||
|
||||
sc2wol_location_ids = {
|
||||
"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": [SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 201],
|
||||
"Zero Hour": [SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 301, SC2WOL_LOC_ID_OFFSET + 302, SC2WOL_LOC_ID_OFFSET + 303],
|
||||
"Evacuation": [SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 401, SC2WOL_LOC_ID_OFFSET + 402, SC2WOL_LOC_ID_OFFSET + 403],
|
||||
"Outbreak": [SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 501, SC2WOL_LOC_ID_OFFSET + 502],
|
||||
"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": [SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 701, SC2WOL_LOC_ID_OFFSET + 702, SC2WOL_LOC_ID_OFFSET + 703],
|
||||
"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": [SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 901, SC2WOL_LOC_ID_OFFSET + 902, SC2WOL_LOC_ID_OFFSET + 903],
|
||||
"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": [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": [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": [SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1301, SC2WOL_LOC_ID_OFFSET + 1302],
|
||||
"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": [SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1501, SC2WOL_LOC_ID_OFFSET + 1502],
|
||||
"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": [SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1701, SC2WOL_LOC_ID_OFFSET + 1702, SC2WOL_LOC_ID_OFFSET + 1703],
|
||||
"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": [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": [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": [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": [SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2201, SC2WOL_LOC_ID_OFFSET + 2202, SC2WOL_LOC_ID_OFFSET + 2203],
|
||||
"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": [SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2401, SC2WOL_LOC_ID_OFFSET + 2402],
|
||||
"In Utter Darkness": [SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2501, SC2WOL_LOC_ID_OFFSET + 2502],
|
||||
"Gates of Hell": [SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2601],
|
||||
"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": [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],
|
||||
"Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200),
|
||||
"The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300),
|
||||
"Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400),
|
||||
"Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500),
|
||||
"Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600),
|
||||
"Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700),
|
||||
"Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800),
|
||||
"Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900),
|
||||
"The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000),
|
||||
"The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100),
|
||||
"Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200),
|
||||
"Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300),
|
||||
"Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400),
|
||||
"Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500),
|
||||
"Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600),
|
||||
"Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700),
|
||||
"The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800),
|
||||
"Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900),
|
||||
"Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000),
|
||||
"Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100),
|
||||
"Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200),
|
||||
"Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300),
|
||||
"A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400),
|
||||
"Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500),
|
||||
"In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600),
|
||||
"Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700),
|
||||
"Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800),
|
||||
"Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900),
|
||||
}
|
||||
|
||||
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
|
||||
progressive_items = {
|
||||
"Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
|
||||
@@ -1174,7 +1300,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
||||
"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
|
||||
"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 Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"],
|
||||
@@ -1182,14 +1316,27 @@ 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 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 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():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
base_name = (item_name.split(maxsplit=1)[1].lower()
|
||||
.replace(' ', '_')
|
||||
.replace("-", "")
|
||||
.replace("(", "")
|
||||
.replace(")", ""))
|
||||
display_data[base_name + "_level"] = level
|
||||
display_data[base_name + "_url"] = icons[display_name]
|
||||
display_data[base_name + "_name"] = display_name
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
@@ -1221,12 +1368,12 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
return render_template("sc2wolTracker.html",
|
||||
inventory=inventory, icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||
id in lookup_any_item_id_to_name},
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
inventory=inventory, icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||
id in lookup_any_item_id_to_name},
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
@@ -1332,9 +1479,104 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
|
||||
custom_items=custom_items, custom_locations=custom_locations)
|
||||
|
||||
|
||||
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
|
||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
|
||||
for teamnumber, team_data in data["checks_done"].items()}
|
||||
def get_enabled_multiworld_trackers(room: Room, current: str):
|
||||
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)}
|
||||
|
||||
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"]
|
||||
|
||||
@@ -1352,6 +1594,47 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int,
|
||||
inventory[team][recipient][item_id] += 1
|
||||
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')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def get_LttP_multiworld_tracker(tracker: UUID):
|
||||
@@ -1400,7 +1683,7 @@ def get_LttP_multiworld_tracker(tracker: UUID):
|
||||
for item_id in precollected:
|
||||
attribute_item(team, player, item_id)
|
||||
for location in locations_checked:
|
||||
if location not in player_locations or location not in player_location_to_area[player]:
|
||||
if location not in player_locations or location not in player_location_to_area.get(player, {}):
|
||||
continue
|
||||
item, recipient, flags = player_locations[location]
|
||||
recipients = groups.get(recipient, [recipient])
|
||||
@@ -1477,142 +1760,9 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
||||
"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] = {
|
||||
"A Link to the Past": get_LttP_multiworld_tracker,
|
||||
}
|
||||
|
||||
class MultiTrackerData(typing.NamedTuple):
|
||||
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()
|
||||
if "Factorio" in games:
|
||||
multi_trackers["Factorio"] = get_Factorio_multiworld_tracker
|
||||
|
||||
@@ -67,6 +67,7 @@ local itemsObtained = 0x0677
|
||||
local takeAnyCavesChecked = 0x0678
|
||||
local localTriforce = 0x0679
|
||||
local bonusItemsObtained = 0x067A
|
||||
local itemsObtainedHigh = 0x067B
|
||||
|
||||
itemAPids = {
|
||||
["Boomerang"] = 7100,
|
||||
@@ -173,11 +174,18 @@ for key, value in pairs(itemAPids) do
|
||||
itemIDNames[value] = key
|
||||
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)
|
||||
memdomain.ram()
|
||||
currentItemsObtained = u8(itemsObtained)
|
||||
currentItemsObtained = getItemsObtained()
|
||||
|
||||
end
|
||||
|
||||
@@ -364,8 +372,8 @@ local function gotItem(item)
|
||||
wU8(0x505, itemCode)
|
||||
wU8(0x506, 128)
|
||||
wU8(0x602, 4)
|
||||
numberObtained = u8(itemsObtained) + 1
|
||||
wU8(itemsObtained, numberObtained)
|
||||
numberObtained = getItemsObtained() + 1
|
||||
setItemsObtained(numberObtained)
|
||||
if itemName == "Boomerang" then gotBoomerang() end
|
||||
if itemName == "Bow" then gotBow() end
|
||||
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
|
||||
@@ -476,7 +484,7 @@ function processBlock(block)
|
||||
if i > u8(bonusItemsObtained) then
|
||||
if u8(0x505) == 0 then
|
||||
gotItem(item)
|
||||
wU8(itemsObtained, u8(itemsObtained) - 1)
|
||||
setItemsObtained(getItemsObtained() - 1)
|
||||
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
|
||||
end
|
||||
end
|
||||
@@ -494,7 +502,7 @@ function processBlock(block)
|
||||
for i, item in ipairs(itemsBlock) do
|
||||
memDomain.ram()
|
||||
if u8(0x505) == 0 then
|
||||
if i > u8(itemsObtained) then
|
||||
if i > getItemsObtained() then
|
||||
gotItem(item)
|
||||
end
|
||||
end
|
||||
@@ -546,7 +554,7 @@ function receive()
|
||||
retTable["gameMode"] = gameMode
|
||||
retTable["overworldHC"] = getHCLocation()
|
||||
retTable["overworldPB"] = getPBLocation()
|
||||
retTable["itemsObtained"] = u8(itemsObtained)
|
||||
retTable["itemsObtained"] = getItemsObtained()
|
||||
msg = json.encode(retTable).."\n"
|
||||
local ret, error = zeldaSocket:send(msg)
|
||||
if ret == nil then
|
||||
@@ -606,4 +614,4 @@ function main()
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -116,6 +116,7 @@ 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}\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}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||
|
||||
7
kvui.py
@@ -1,7 +1,14 @@
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
|
||||
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
||||
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
os.environ["KIVY_NO_ARGS"] = "1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
colorama>=0.4.5
|
||||
websockets>=11.0.3
|
||||
PyYAML>=6.0.1
|
||||
jellyfish>=1.0.0
|
||||
jellyfish>=1.0.1
|
||||
jinja2>=3.1.2
|
||||
schema>=0.7.5
|
||||
kivy>=2.2.0
|
||||
@@ -9,4 +9,4 @@ bsdiff4>=1.2.3
|
||||
platformdirs>=3.9.1
|
||||
certifi>=2023.7.22
|
||||
cython>=0.29.35
|
||||
cymem>=2.0.7
|
||||
cymem>=2.0.8
|
||||
|
||||
@@ -118,7 +118,7 @@ class Group:
|
||||
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
|
||||
return cls._type_cache
|
||||
|
||||
def get(self, key: str, default: Any) -> Any:
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
if key in self:
|
||||
return self[key]
|
||||
return default
|
||||
|
||||
13
setup.py
@@ -80,7 +80,6 @@ non_apworlds: set = {
|
||||
"Raft",
|
||||
"Secret of Evermore",
|
||||
"Slay the Spire",
|
||||
"Starcraft 2 Wings of Liberty",
|
||||
"Sudoku",
|
||||
"Super Mario 64",
|
||||
"VVVVVV",
|
||||
@@ -91,6 +90,7 @@ non_apworlds: set = {
|
||||
# LogicMixin is broken before 3.10 import revamp
|
||||
if sys.version_info < (3,10):
|
||||
non_apworlds.add("Hollow Knight")
|
||||
non_apworlds.add("Starcraft 2 Wings of Liberty")
|
||||
|
||||
def download_SNI():
|
||||
print("Updating SNI")
|
||||
@@ -185,13 +185,22 @@ def resolve_icon(icon_name: str):
|
||||
|
||||
exes = [
|
||||
cx_Freeze.Executable(
|
||||
script=f'{c.script_name}.py',
|
||||
script=f"{c.script_name}.py",
|
||||
target_name=c.frozen_name + (".exe" if is_windows else ""),
|
||||
icon=resolve_icon(c.icon),
|
||||
base="Win32GUI" if is_windows and not c.cli else None
|
||||
) for c in components if c.script_name and c.frozen_name
|
||||
]
|
||||
|
||||
if is_windows:
|
||||
# create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help
|
||||
c = next(component for component in components if component.script_name == "Launcher")
|
||||
exes.append(cx_Freeze.Executable(
|
||||
script=f"{c.script_name}.py",
|
||||
target_name=f"{c.frozen_name}(DEBUG).exe",
|
||||
icon=resolve_icon(c.icon),
|
||||
))
|
||||
|
||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
|
||||
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import json
|
||||
class TestDocs(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
from WebHost import get_app, raw_app
|
||||
from WebHostLib import app as raw_app
|
||||
from WebHost import get_app
|
||||
raw_app.config["PONY"] = {
|
||||
"provider": "sqlite",
|
||||
"filename": ":memory:",
|
||||
|
||||
@@ -14,7 +14,8 @@ class TestFileGeneration(unittest.TestCase):
|
||||
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
||||
|
||||
def testOptions(self):
|
||||
WebHost.create_options_files()
|
||||
from WebHostLib.options import create as create_options_files
|
||||
create_options_files()
|
||||
target = os.path.join(self.correct_path, "static", "generated", "configs")
|
||||
self.assertTrue(os.path.exists(target))
|
||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
|
||||
|
||||
@@ -15,7 +15,6 @@ if TYPE_CHECKING:
|
||||
from BaseClasses import MultiWorld, Item, Location, Tutorial
|
||||
from . import GamesPackage
|
||||
from settings import Group
|
||||
from flask import Flask
|
||||
|
||||
|
||||
class AutoWorldRegister(type):
|
||||
@@ -156,22 +155,9 @@ class WebWorld:
|
||||
"""Choose a theme for you /game/* pages.
|
||||
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
|
||||
|
||||
bug_report_page: Optional[str] = None
|
||||
bug_report_page: Optional[str]
|
||||
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
|
||||
|
||||
multitracker_template: Optional[str] = None
|
||||
"""relative path with /-seperator to a MultiTracker Template file."""
|
||||
|
||||
# allows modification of webhost during startup, this is run once
|
||||
@classmethod
|
||||
def run_webhost_setup(cls):
|
||||
pass
|
||||
|
||||
# allows modification of webhost during startup,
|
||||
# this is run whenever a Flask app is created (per-thread/per-process)
|
||||
@classmethod
|
||||
def run_webhost_app_setup(cls, app: "Flask"):
|
||||
pass
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
@@ -426,6 +412,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
res["checksum"] = data_package_checksum(res)
|
||||
return res
|
||||
|
||||
|
||||
# any methods attached to this can be used as part of CollectionState,
|
||||
# please use a prefix as all of them get clobbered together
|
||||
class LogicMixin(metaclass=AutoLogicRegister):
|
||||
|
||||
@@ -8,18 +8,31 @@ from .paths import Paths
|
||||
|
||||
|
||||
def get(name: str) -> Map:
|
||||
# Iterate through 2 folder depths
|
||||
for map_dir in (p for p in Paths.MAPS.iterdir()):
|
||||
if map_dir.is_dir():
|
||||
for map_file in (p for p in map_dir.iterdir()):
|
||||
if Map.matches_target_map_name(map_file, name):
|
||||
return Map(map_file)
|
||||
elif Map.matches_target_map_name(map_dir, name):
|
||||
return Map(map_dir)
|
||||
map = find_map_in_dir(name, map_dir)
|
||||
if map is not None:
|
||||
return map
|
||||
|
||||
raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".")
|
||||
|
||||
|
||||
# Go deeper
|
||||
def find_map_in_dir(name, path):
|
||||
if Map.matches_target_map_name(path, name):
|
||||
return Map(path)
|
||||
|
||||
if path.name.endswith("SC2Map"):
|
||||
return None
|
||||
|
||||
if path.is_dir():
|
||||
for childPath in (p for p in path.iterdir()):
|
||||
map = find_map_in_dir(name, childPath)
|
||||
if map is not None:
|
||||
return map
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class Map:
|
||||
|
||||
def __init__(self, path: Path):
|
||||
|
||||
@@ -7,19 +7,21 @@ LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
|
||||
RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f"
|
||||
ROM_PLAYER_LIMIT: int = 255
|
||||
|
||||
import io
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import bsdiff4
|
||||
from typing import List
|
||||
from typing import Optional, List
|
||||
|
||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, read_snes_rom
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||
|
||||
from .Shops import ShopType, ShopPriceType
|
||||
from .Dungeons import dungeon_music_addresses
|
||||
@@ -35,7 +37,6 @@ from .Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmith
|
||||
from .Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||
from .EntranceShuffle import door_addresses
|
||||
from .Options import smallkey_shuffle
|
||||
from .Sprites import apply_random_sprite_on_event
|
||||
|
||||
try:
|
||||
from maseya import z3pr
|
||||
@@ -211,6 +212,73 @@ def check_enemizer(enemizercli):
|
||||
check_enemizer.done = True
|
||||
|
||||
|
||||
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
|
||||
userandomsprites = False
|
||||
if sprite and not isinstance(sprite, Sprite):
|
||||
sprite = sprite.lower()
|
||||
userandomsprites = sprite.startswith('randomon')
|
||||
|
||||
racerom = rom.read_byte(0x180213)
|
||||
if allow_random_on_event or not racerom:
|
||||
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
|
||||
# However, if the seed is not a racerom seed, then it is always allowed.
|
||||
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
|
||||
|
||||
onevent = 0
|
||||
if sprite == 'randomonall':
|
||||
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
|
||||
elif sprite == 'randomonnone':
|
||||
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
|
||||
onevent = 0x0000
|
||||
elif sprite == 'randomonrandom':
|
||||
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
|
||||
onevent = local_random.randint(0x0001, 0x003F)
|
||||
elif userandomsprites:
|
||||
onevent = 0x01 if 'hit' in sprite else 0x00
|
||||
onevent += 0x02 if 'enter' in sprite else 0x00
|
||||
onevent += 0x04 if 'exit' in sprite else 0x00
|
||||
onevent += 0x08 if 'slash' in sprite else 0x00
|
||||
onevent += 0x10 if 'item' in sprite else 0x00
|
||||
onevent += 0x20 if 'bonk' in sprite else 0x00
|
||||
|
||||
rom.write_int16(0x18637F, onevent)
|
||||
|
||||
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
|
||||
|
||||
# write link sprite if required
|
||||
if sprite:
|
||||
sprites = list()
|
||||
sprite.write_to_rom(rom)
|
||||
|
||||
_populate_sprite_table()
|
||||
if userandomsprites:
|
||||
if sprite_pool:
|
||||
if isinstance(sprite_pool, str):
|
||||
sprite_pool = sprite_pool.split(':')
|
||||
for spritename in sprite_pool:
|
||||
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
|
||||
spritename, local_random)
|
||||
if sprite:
|
||||
sprites.append(sprite)
|
||||
else:
|
||||
logging.info(f"Sprite {spritename} was not found.")
|
||||
else:
|
||||
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
|
||||
else:
|
||||
sprites.append(sprite)
|
||||
if sprites:
|
||||
while len(sprites) < 32:
|
||||
sprites.extend(sprites)
|
||||
local_random.shuffle(sprites)
|
||||
|
||||
for i, sprite in enumerate(sprites[:32]):
|
||||
if not i and not userandomsprites:
|
||||
continue
|
||||
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
|
||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
|
||||
|
||||
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
||||
player = world.player
|
||||
multiworld = world.multiworld
|
||||
@@ -419,6 +487,271 @@ class TileSet:
|
||||
return localrandom.choice(tile_sets)
|
||||
|
||||
|
||||
sprite_list_lock = threading.Lock()
|
||||
_sprite_table = {}
|
||||
|
||||
|
||||
def _populate_sprite_table():
|
||||
with sprite_list_lock:
|
||||
if not _sprite_table:
|
||||
def load_sprite_from_file(file):
|
||||
sprite = Sprite(file)
|
||||
if sprite.valid:
|
||||
_sprite_table[sprite.name.lower()] = sprite
|
||||
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
|
||||
else:
|
||||
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
|
||||
for file in os.listdir(dir):
|
||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
||||
|
||||
|
||||
class Sprite():
|
||||
sprite_size = 28672
|
||||
palette_size = 120
|
||||
glove_size = 4
|
||||
author_name: Optional[str] = None
|
||||
base_data: bytes
|
||||
|
||||
def __init__(self, filename):
|
||||
if not hasattr(Sprite, "base_data"):
|
||||
self.get_vanilla_sprite_data()
|
||||
with open(filename, 'rb') as file:
|
||||
filedata = file.read()
|
||||
self.name = os.path.basename(filename)
|
||||
self.valid = True
|
||||
if filename.endswith(".apsprite"):
|
||||
self.from_ap_sprite(filedata)
|
||||
elif len(filedata) == 0x7000:
|
||||
# sprite file with graphics and without palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
elif len(filedata) == 0x7078:
|
||||
# sprite file with graphics and palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
self.palette = filedata[0x7000:]
|
||||
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
|
||||
elif len(filedata) == 0x707C:
|
||||
# sprite file with graphics and palette data including gloves
|
||||
self.sprite = filedata[:0x7000]
|
||||
self.palette = filedata[0x7000:0x7078]
|
||||
self.glove_palette = filedata[0x7078:]
|
||||
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
|
||||
# full rom with patched sprite, extract it
|
||||
self.sprite = filedata[0x80000:0x87000]
|
||||
self.palette = filedata[0xDD308:0xDD380]
|
||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
||||
elif filedata.startswith(b'ZSPR'):
|
||||
self.from_zspr(filedata, filename)
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def get_vanilla_sprite_data(self):
|
||||
file_name = get_base_rom_path()
|
||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
||||
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
|
||||
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
|
||||
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
||||
|
||||
def from_ap_sprite(self, filedata):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
obj = parse_yaml(filedata.decode("utf-8-sig"))
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
except Exception:
|
||||
logger = logging.getLogger("apsprite")
|
||||
logger.exception("Error parsing apsprite file")
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def author_game_display(self) -> str:
|
||||
name = getattr(self, "_author_game_display", "")
|
||||
if not name:
|
||||
name = self.author_name
|
||||
|
||||
# At this point, may need some filtering to displayable characters
|
||||
return name
|
||||
|
||||
def to_ap_sprite(self, path):
|
||||
import yaml
|
||||
payload = {"format_version": 1,
|
||||
"min_format_version": 1,
|
||||
"sprite_version": 1,
|
||||
"name": self.name,
|
||||
"author": self.author_name,
|
||||
"game": "A Link to the Past",
|
||||
"data": self.get_delta()}
|
||||
with open(path, "w") as f:
|
||||
f.write(yaml.safe_dump(payload))
|
||||
|
||||
def get_delta(self):
|
||||
modified_data = self.sprite + self.palette + self.glove_palette
|
||||
return bsdiff4.diff(Sprite.base_data, modified_data)
|
||||
|
||||
def from_zspr(self, filedata, filename):
|
||||
result = self.parse_zspr(filedata, 1)
|
||||
if result is None:
|
||||
self.valid = False
|
||||
return
|
||||
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
|
||||
if self.name == "":
|
||||
self.name = os.path.split(filename)[1].split(".")[0]
|
||||
|
||||
if len(sprite) != 0x7000:
|
||||
self.valid = False
|
||||
return
|
||||
self.sprite = sprite
|
||||
if len(palette) == 0:
|
||||
pass
|
||||
elif len(palette) == 0x78:
|
||||
self.palette = palette
|
||||
elif len(palette) == 0x7C:
|
||||
self.palette = palette[:0x78]
|
||||
self.glove_palette = palette[0x78:]
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@staticmethod
|
||||
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
|
||||
_populate_sprite_table()
|
||||
name = name.lower()
|
||||
if name.startswith('random'):
|
||||
sprites = list(set(_sprite_table.values()))
|
||||
sprites.sort(key=lambda x: x.name)
|
||||
return local_random.choice(sprites)
|
||||
return _sprite_table.get(name, None)
|
||||
|
||||
@staticmethod
|
||||
def default_link_sprite():
|
||||
return Sprite(local_path('data', 'default.apsprite'))
|
||||
|
||||
def decode8(self, pos):
|
||||
arr = [[0 for _ in range(8)] for _ in range(8)]
|
||||
for y in range(8):
|
||||
for x in range(8):
|
||||
position = 1 << (7 - x)
|
||||
val = 0
|
||||
if self.sprite[pos + 2 * y] & position:
|
||||
val += 1
|
||||
if self.sprite[pos + 2 * y + 1] & position:
|
||||
val += 2
|
||||
if self.sprite[pos + 2 * y + 16] & position:
|
||||
val += 4
|
||||
if self.sprite[pos + 2 * y + 17] & position:
|
||||
val += 8
|
||||
arr[y][x] = val
|
||||
return arr
|
||||
|
||||
def decode16(self, pos):
|
||||
arr = [[0 for _ in range(16)] for _ in range(16)]
|
||||
top_left = self.decode8(pos)
|
||||
top_right = self.decode8(pos + 0x20)
|
||||
bottom_left = self.decode8(pos + 0x200)
|
||||
bottom_right = self.decode8(pos + 0x220)
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
arr[y][x] = top_left[y][x]
|
||||
arr[y][x + 8] = top_right[y][x]
|
||||
arr[y + 8][x] = bottom_left[y][x]
|
||||
arr[y + 8][x + 8] = bottom_right[y][x]
|
||||
return arr
|
||||
|
||||
@staticmethod
|
||||
def parse_zspr(filedata, expected_kind):
|
||||
logger = logging.getLogger("ZSPR")
|
||||
headerstr = "<4xBHHIHIHH6x"
|
||||
headersize = struct.calcsize(headerstr)
|
||||
if len(filedata) < headersize:
|
||||
return None
|
||||
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
||||
headerstr, filedata)
|
||||
if version not in [1]:
|
||||
logger.error("Error parsing ZSPR file: Version %g not supported", version)
|
||||
return None
|
||||
if kind != expected_kind:
|
||||
return None
|
||||
|
||||
stream = io.BytesIO(filedata)
|
||||
stream.seek(headersize)
|
||||
|
||||
def read_utf16le(stream):
|
||||
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
|
||||
raw = bytearray()
|
||||
while True:
|
||||
char = stream.read(2)
|
||||
if char in [b"", b"\x00\x00"]:
|
||||
break
|
||||
raw += char
|
||||
return raw.decode("utf-16_le")
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
|
||||
real_csum = sum(filedata) % 0x10000
|
||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
|
||||
|
||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||
|
||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||
logger.error("Error parsing ZSPR file: Unexpected end of file")
|
||||
return None
|
||||
|
||||
return sprite, palette, sprite_name, author_name, author_credits_name
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error parsing ZSPR file")
|
||||
return None
|
||||
|
||||
def decode_palette(self):
|
||||
"""Returns the palettes as an array of arrays of 15 colors"""
|
||||
|
||||
def array_chunk(arr, size):
|
||||
return list(zip(*[iter(arr)] * size))
|
||||
|
||||
def make_int16(pair):
|
||||
return pair[1] << 8 | pair[0]
|
||||
|
||||
def expand_color(i):
|
||||
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
|
||||
|
||||
# turn palette data into a list of RGB tuples with 8 bit values
|
||||
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
|
||||
|
||||
# split into palettes of 15 colors
|
||||
return array_chunk(palette_as_colors, 15)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def write_to_rom(self, rom: LocalRom):
|
||||
if not self.valid:
|
||||
logging.warning("Tried writing invalid sprite to rom, skipping.")
|
||||
return
|
||||
rom.write_bytes(0x80000, self.sprite)
|
||||
rom.write_bytes(0xDD308, self.palette)
|
||||
rom.write_bytes(0xDEDF5, self.glove_palette)
|
||||
rom.write_bytes(0x300000, self.sprite)
|
||||
rom.write_bytes(0x307000, self.palette)
|
||||
rom.write_bytes(0x307078, self.glove_palette)
|
||||
|
||||
|
||||
bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A,
|
||||
0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD,
|
||||
0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D,
|
||||
|
||||
@@ -32,7 +32,6 @@ def set_rules(world):
|
||||
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
||||
|
||||
if world.players == 1:
|
||||
world.get_region('Menu', player).can_reach_private = lambda state: True
|
||||
no_logic_rules(world, player)
|
||||
for exit in world.get_region('Menu', player).exits:
|
||||
exit.hide_path = True
|
||||
@@ -196,7 +195,6 @@ def global_rules(world, player):
|
||||
add_item_rule(world.get_location(prize_location, player),
|
||||
lambda item: item.name in crystals_and_pendants and item.player == player)
|
||||
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
|
||||
world.get_region('Menu', player).can_reach_private = lambda state: True
|
||||
for exit in world.get_region('Menu', player).exits:
|
||||
exit.hide_path = True
|
||||
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
import threading
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
import bsdiff4
|
||||
|
||||
from Utils import user_path, read_snes_rom, parse_yaml, local_path
|
||||
if TYPE_CHECKING:
|
||||
from .Rom import LocalRom
|
||||
|
||||
sprite_list_lock = threading.Lock()
|
||||
_sprite_table = {}
|
||||
|
||||
|
||||
def _populate_sprite_table():
|
||||
with sprite_list_lock:
|
||||
if not _sprite_table:
|
||||
def load_sprite_from_file(file):
|
||||
sprite = Sprite(file)
|
||||
if sprite.valid:
|
||||
_sprite_table[sprite.name.lower()] = sprite
|
||||
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
|
||||
else:
|
||||
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
|
||||
for file in os.listdir(dir):
|
||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
||||
|
||||
|
||||
class Sprite():
|
||||
sprite_size = 28672
|
||||
palette_size = 120
|
||||
glove_size = 4
|
||||
author_name: Optional[str] = None
|
||||
base_data: bytes
|
||||
|
||||
def __init__(self, filename):
|
||||
if not hasattr(Sprite, "base_data"):
|
||||
self.get_vanilla_sprite_data()
|
||||
with open(filename, 'rb') as file:
|
||||
filedata = file.read()
|
||||
self.name = os.path.basename(filename)
|
||||
self.valid = True
|
||||
if filename.endswith(".apsprite"):
|
||||
self.from_ap_sprite(filedata)
|
||||
elif len(filedata) == 0x7000:
|
||||
# sprite file with graphics and without palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
elif len(filedata) == 0x7078:
|
||||
# sprite file with graphics and palette data
|
||||
self.sprite = filedata[:0x7000]
|
||||
self.palette = filedata[0x7000:]
|
||||
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
|
||||
elif len(filedata) == 0x707C:
|
||||
# sprite file with graphics and palette data including gloves
|
||||
self.sprite = filedata[:0x7000]
|
||||
self.palette = filedata[0x7000:0x7078]
|
||||
self.glove_palette = filedata[0x7078:]
|
||||
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
|
||||
# full rom with patched sprite, extract it
|
||||
self.sprite = filedata[0x80000:0x87000]
|
||||
self.palette = filedata[0xDD308:0xDD380]
|
||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
||||
elif filedata.startswith(b'ZSPR'):
|
||||
self.from_zspr(filedata, filename)
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
def get_vanilla_sprite_data(self):
|
||||
from .Rom import get_base_rom_path
|
||||
file_name = get_base_rom_path()
|
||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
||||
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
|
||||
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
|
||||
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
||||
|
||||
def from_ap_sprite(self, filedata):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
obj = parse_yaml(filedata.decode("utf-8-sig"))
|
||||
if obj["min_format_version"] > 1:
|
||||
raise Exception("Sprite file requires an updated reader.")
|
||||
self.author_name = obj["author"]
|
||||
self.name = obj["name"]
|
||||
if obj["data"]: # skip patching for vanilla content
|
||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||
self.sprite = data[:self.sprite_size]
|
||||
self.palette = data[self.sprite_size:self.palette_size]
|
||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||
except Exception:
|
||||
logger = logging.getLogger("apsprite")
|
||||
logger.exception("Error parsing apsprite file")
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def author_game_display(self) -> str:
|
||||
name = getattr(self, "_author_game_display", "")
|
||||
if not name:
|
||||
name = self.author_name
|
||||
|
||||
# At this point, may need some filtering to displayable characters
|
||||
return name
|
||||
|
||||
def to_ap_sprite(self, path):
|
||||
import yaml
|
||||
payload = {"format_version": 1,
|
||||
"min_format_version": 1,
|
||||
"sprite_version": 1,
|
||||
"name": self.name,
|
||||
"author": self.author_name,
|
||||
"game": "A Link to the Past",
|
||||
"data": self.get_delta()}
|
||||
with open(path, "w") as f:
|
||||
f.write(yaml.safe_dump(payload))
|
||||
|
||||
def get_delta(self):
|
||||
modified_data = self.sprite + self.palette + self.glove_palette
|
||||
return bsdiff4.diff(Sprite.base_data, modified_data)
|
||||
|
||||
def from_zspr(self, filedata, filename):
|
||||
result = self.parse_zspr(filedata, 1)
|
||||
if result is None:
|
||||
self.valid = False
|
||||
return
|
||||
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
|
||||
if self.name == "":
|
||||
self.name = os.path.split(filename)[1].split(".")[0]
|
||||
|
||||
if len(sprite) != 0x7000:
|
||||
self.valid = False
|
||||
return
|
||||
self.sprite = sprite
|
||||
if len(palette) == 0:
|
||||
pass
|
||||
elif len(palette) == 0x78:
|
||||
self.palette = palette
|
||||
elif len(palette) == 0x7C:
|
||||
self.palette = palette[:0x78]
|
||||
self.glove_palette = palette[0x78:]
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@staticmethod
|
||||
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
|
||||
_populate_sprite_table()
|
||||
name = name.lower()
|
||||
if name.startswith('random'):
|
||||
sprites = list(set(_sprite_table.values()))
|
||||
sprites.sort(key=lambda x: x.name)
|
||||
return local_random.choice(sprites)
|
||||
return _sprite_table.get(name, None)
|
||||
|
||||
@staticmethod
|
||||
def default_link_sprite():
|
||||
return Sprite(local_path('data', 'default.apsprite'))
|
||||
|
||||
def decode8(self, pos):
|
||||
arr = [[0 for _ in range(8)] for _ in range(8)]
|
||||
for y in range(8):
|
||||
for x in range(8):
|
||||
position = 1 << (7 - x)
|
||||
val = 0
|
||||
if self.sprite[pos + 2 * y] & position:
|
||||
val += 1
|
||||
if self.sprite[pos + 2 * y + 1] & position:
|
||||
val += 2
|
||||
if self.sprite[pos + 2 * y + 16] & position:
|
||||
val += 4
|
||||
if self.sprite[pos + 2 * y + 17] & position:
|
||||
val += 8
|
||||
arr[y][x] = val
|
||||
return arr
|
||||
|
||||
def decode16(self, pos):
|
||||
arr = [[0 for _ in range(16)] for _ in range(16)]
|
||||
top_left = self.decode8(pos)
|
||||
top_right = self.decode8(pos + 0x20)
|
||||
bottom_left = self.decode8(pos + 0x200)
|
||||
bottom_right = self.decode8(pos + 0x220)
|
||||
for x in range(8):
|
||||
for y in range(8):
|
||||
arr[y][x] = top_left[y][x]
|
||||
arr[y][x + 8] = top_right[y][x]
|
||||
arr[y + 8][x] = bottom_left[y][x]
|
||||
arr[y + 8][x + 8] = bottom_right[y][x]
|
||||
return arr
|
||||
|
||||
@staticmethod
|
||||
def parse_zspr(filedata, expected_kind):
|
||||
logger = logging.getLogger("ZSPR")
|
||||
headerstr = "<4xBHHIHIHH6x"
|
||||
headersize = struct.calcsize(headerstr)
|
||||
if len(filedata) < headersize:
|
||||
return None
|
||||
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
||||
headerstr, filedata)
|
||||
if version not in [1]:
|
||||
logger.error("Error parsing ZSPR file: Version %g not supported", version)
|
||||
return None
|
||||
if kind != expected_kind:
|
||||
return None
|
||||
|
||||
stream = io.BytesIO(filedata)
|
||||
stream.seek(headersize)
|
||||
|
||||
def read_utf16le(stream):
|
||||
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
|
||||
raw = bytearray()
|
||||
while True:
|
||||
char = stream.read(2)
|
||||
if char in [b"", b"\x00\x00"]:
|
||||
break
|
||||
raw += char
|
||||
return raw.decode("utf-16_le")
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
sprite_name = read_utf16le(stream)
|
||||
author_name = read_utf16le(stream)
|
||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||
|
||||
# Ignoring the Author Rom name for the time being.
|
||||
|
||||
real_csum = sum(filedata) % 0x10000
|
||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
|
||||
|
||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||
|
||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||
logger.error("Error parsing ZSPR file: Unexpected end of file")
|
||||
return None
|
||||
|
||||
return sprite, palette, sprite_name, author_name, author_credits_name
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error parsing ZSPR file")
|
||||
return None
|
||||
|
||||
def decode_palette(self):
|
||||
"""Returns the palettes as an array of arrays of 15 colors"""
|
||||
|
||||
def array_chunk(arr, size):
|
||||
return list(zip(*[iter(arr)] * size))
|
||||
|
||||
def make_int16(pair):
|
||||
return pair[1] << 8 | pair[0]
|
||||
|
||||
def expand_color(i):
|
||||
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
|
||||
|
||||
# turn palette data into a list of RGB tuples with 8 bit values
|
||||
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
|
||||
|
||||
# split into palettes of 15 colors
|
||||
return array_chunk(palette_as_colors, 15)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def write_to_rom(self, rom: "LocalRom"):
|
||||
if not self.valid:
|
||||
logging.warning("Tried writing invalid sprite to rom, skipping.")
|
||||
return
|
||||
rom.write_bytes(0x80000, self.sprite)
|
||||
rom.write_bytes(0xDD308, self.palette)
|
||||
rom.write_bytes(0xDEDF5, self.glove_palette)
|
||||
rom.write_bytes(0x300000, self.sprite)
|
||||
rom.write_bytes(0x307000, self.palette)
|
||||
rom.write_bytes(0x307078, self.glove_palette)
|
||||
|
||||
|
||||
def update_sprites():
|
||||
from tkinter import Tk
|
||||
from LttPAdjuster import get_image_for_sprite
|
||||
from LttPAdjuster import BackgroundTaskProgress
|
||||
from LttPAdjuster import BackgroundTaskProgressNullWindow
|
||||
from LttPAdjuster import update_sprites
|
||||
|
||||
# Target directories
|
||||
input_dir = user_path("data", "sprites", "alttpr")
|
||||
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
|
||||
|
||||
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
|
||||
# update sprites through gui.py's functions
|
||||
done = threading.Event()
|
||||
try:
|
||||
top = Tk()
|
||||
except:
|
||||
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
|
||||
else:
|
||||
top.withdraw()
|
||||
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||
while not done.isSet():
|
||||
task.do_events()
|
||||
|
||||
spriteData = []
|
||||
|
||||
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
|
||||
sprite = Sprite(os.path.join(input_dir, file))
|
||||
|
||||
if not sprite.name:
|
||||
print("Warning:", file, "has no name.")
|
||||
sprite.name = file.split(".", 1)[0]
|
||||
if sprite.valid:
|
||||
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
|
||||
image.write(get_image_for_sprite(sprite, True))
|
||||
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
|
||||
else:
|
||||
print(file, "dropped, as it has no valid sprite data.")
|
||||
spriteData.sort(key=lambda entry: entry["name"])
|
||||
with open(f'{output_dir}/spriteData.json', 'w') as file:
|
||||
json.dump({"sprites": spriteData}, file, indent=1)
|
||||
return spriteData
|
||||
|
||||
|
||||
def apply_random_sprite_on_event(rom: "LocalRom", sprite, local_random, allow_random_on_event, sprite_pool):
|
||||
userandomsprites = False
|
||||
if sprite and not isinstance(sprite, Sprite):
|
||||
sprite = sprite.lower()
|
||||
userandomsprites = sprite.startswith('randomon')
|
||||
|
||||
racerom = rom.read_byte(0x180213)
|
||||
if allow_random_on_event or not racerom:
|
||||
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
|
||||
# However, if the seed is not a racerom seed, then it is always allowed.
|
||||
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
|
||||
|
||||
onevent = 0
|
||||
if sprite == 'randomonall':
|
||||
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
|
||||
elif sprite == 'randomonnone':
|
||||
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
|
||||
onevent = 0x0000
|
||||
elif sprite == 'randomonrandom':
|
||||
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
|
||||
onevent = local_random.randint(0x0001, 0x003F)
|
||||
elif userandomsprites:
|
||||
onevent = 0x01 if 'hit' in sprite else 0x00
|
||||
onevent += 0x02 if 'enter' in sprite else 0x00
|
||||
onevent += 0x04 if 'exit' in sprite else 0x00
|
||||
onevent += 0x08 if 'slash' in sprite else 0x00
|
||||
onevent += 0x10 if 'item' in sprite else 0x00
|
||||
onevent += 0x20 if 'bonk' in sprite else 0x00
|
||||
|
||||
rom.write_int16(0x18637F, onevent)
|
||||
|
||||
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
|
||||
|
||||
# write link sprite if required
|
||||
if sprite:
|
||||
sprites = list()
|
||||
sprite.write_to_rom(rom)
|
||||
|
||||
_populate_sprite_table()
|
||||
if userandomsprites:
|
||||
if sprite_pool:
|
||||
if isinstance(sprite_pool, str):
|
||||
sprite_pool = sprite_pool.split(':')
|
||||
for spritename in sprite_pool:
|
||||
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
|
||||
spritename, local_random)
|
||||
if sprite:
|
||||
sprites.append(sprite)
|
||||
else:
|
||||
logging.info(f"Sprite {spritename} was not found.")
|
||||
else:
|
||||
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
|
||||
else:
|
||||
sprites.append(sprite)
|
||||
if sprites:
|
||||
while len(sprites) < 32:
|
||||
sprites.extend(sprites)
|
||||
local_random.shuffle(sprites)
|
||||
|
||||
for i, sprite in enumerate(sprites[:32]):
|
||||
if not i and not userandomsprites:
|
||||
continue
|
||||
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
|
||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||
@@ -124,14 +124,6 @@ class ALTTPWeb(WebWorld):
|
||||
|
||||
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
|
||||
|
||||
@classmethod
|
||||
def run_webhost_setup(cls):
|
||||
rom_file = get_base_rom_path()
|
||||
if os.path.exists(rom_file):
|
||||
from .Sprites import update_sprites
|
||||
update_sprites()
|
||||
else:
|
||||
logging.warning("Could not update LttP sprites.")
|
||||
|
||||
class ALTTPWorld(World):
|
||||
"""
|
||||
@@ -815,6 +807,7 @@ class ALTTPWorld(World):
|
||||
}
|
||||
)
|
||||
return slot_data
|
||||
|
||||
|
||||
def get_same_seed(world, seed_def: tuple) -> str:
|
||||
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||
|
||||
@@ -367,25 +367,25 @@ def can_beat_boss(state: CollectionState, boss: str, logic: int, player: int) ->
|
||||
elif boss == "Graveyard":
|
||||
return (
|
||||
has_boss_strength("amanecida")
|
||||
and state.has_all({"D01BZ07S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
|
||||
and state.has_all({"D01Z06S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
|
||||
)
|
||||
elif boss == "Jondo":
|
||||
return (
|
||||
has_boss_strength("amanecida")
|
||||
and state.has("D01BZ07S01[Santos]", player)
|
||||
and state.has("D01Z06S01[Santos]", player)
|
||||
and state.has_any({"D20Z01S05[W]", "D20Z01S05[E]"}, player)
|
||||
and state.has_any({"D03Z01S03[W]", "D03Z01S03[SW]"}, player)
|
||||
)
|
||||
elif boss == "Patio":
|
||||
return (
|
||||
has_boss_strength("amanecida")
|
||||
and state.has_all({"D01BZ07S01[Santos]", "D06Z01S18[E]"}, player)
|
||||
and state.has_all({"D01Z06S01[Santos]", "D06Z01S18[E]"}, player)
|
||||
and state.has_any({"D04Z01S04[W]", "D04Z01S04[E]", "D04Z01S04[Cherubs]"}, player)
|
||||
)
|
||||
elif boss == "Wall":
|
||||
return (
|
||||
has_boss_strength("amanecida")
|
||||
and state.has_all({"D01BZ07S01[Santos]", "D09BZ01S01[Cell24]"}, player)
|
||||
and state.has_all({"D01Z06S01[Santos]", "D09BZ01S01[Cell24]"}, player)
|
||||
and state.has_any({"D09Z01S01[W]", "D09Z01S01[E]"}, player)
|
||||
)
|
||||
elif boss == "Hall":
|
||||
@@ -2451,6 +2451,8 @@ def rules(blasphemousworld):
|
||||
# Items
|
||||
set_rule(world.get_location("PotSS: 4th meeting with Redento", player),
|
||||
lambda state: redento(state, blasphemousworld, player, 4))
|
||||
set_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player),
|
||||
lambda state: can_beat_boss(state, "Patio", logic, player))
|
||||
# No doors
|
||||
|
||||
|
||||
|
||||