Compare commits
5 Commits
0.4.3
...
custom_web
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62b3fd4d37 | ||
|
|
e2f7153312 | ||
|
|
96d4143030 | ||
|
|
a1dcaf52e3 | ||
|
|
aab8f31345 |
@@ -853,6 +853,14 @@ class Region:
|
|||||||
state.update_reachable_regions(self.player)
|
state.update_reachable_regions(self.player)
|
||||||
return self in state.reachable_regions[self.player]
|
return self in state.reachable_regions[self.player]
|
||||||
|
|
||||||
|
def can_reach_private(self, state: CollectionState) -> bool:
|
||||||
|
for entrance in self.entrances:
|
||||||
|
if entrance.can_reach(state):
|
||||||
|
if not self in state.path:
|
||||||
|
state.path[self] = (self.name, state.path.get(entrance, None))
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hint_text(self) -> str:
|
def hint_text(self) -> str:
|
||||||
return self._hint_text if self._hint_text else self.name
|
return self._hint_text if self._hint_text else self.name
|
||||||
|
|||||||
8
Fill.py
@@ -840,12 +840,12 @@ def distribute_planned(world: MultiWorld) -> None:
|
|||||||
|
|
||||||
if "early_locations" in locations:
|
if "early_locations" in locations:
|
||||||
locations.remove("early_locations")
|
locations.remove("early_locations")
|
||||||
for target_player in worlds:
|
for player in worlds:
|
||||||
locations += early_locations[target_player]
|
locations += early_locations[player]
|
||||||
if "non_early_locations" in locations:
|
if "non_early_locations" in locations:
|
||||||
locations.remove("non_early_locations")
|
locations.remove("non_early_locations")
|
||||||
for target_player in worlds:
|
for player in worlds:
|
||||||
locations += non_early_locations[target_player]
|
locations += non_early_locations[player]
|
||||||
|
|
||||||
block['locations'] = locations
|
block['locations'] = locations
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ from urllib.request import urlopen
|
|||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
|
from worlds.alttp.Rom import LocalRom, apply_rom_settings, get_base_rom_bytes
|
||||||
|
from worlds.alttp.Sprites import Sprite
|
||||||
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
|
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
|
||||||
get_adjuster_settings, get_adjuster_settings_no_defaults, tkinter_center_window, init_logging
|
get_adjuster_settings, get_adjuster_settings_no_defaults, tkinter_center_window, init_logging
|
||||||
|
|
||||||
|
|||||||
15
Main.py
@@ -139,13 +139,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
exclusion_rules(world, player, world.exclude_locations[player].value)
|
exclusion_rules(world, player, world.exclude_locations[player].value)
|
||||||
world.priority_locations[player].value -= world.exclude_locations[player].value
|
world.priority_locations[player].value -= world.exclude_locations[player].value
|
||||||
for location_name in world.priority_locations[player].value:
|
for location_name in world.priority_locations[player].value:
|
||||||
try:
|
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
|
||||||
location = world.get_location(location_name, player)
|
|
||||||
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
|
||||||
if location_name not in world.worlds[player].location_name_to_id:
|
|
||||||
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
|
|
||||||
else:
|
|
||||||
location.progress_type = LocationProgressType.PRIORITY
|
|
||||||
|
|
||||||
# Set local and non-local item rules.
|
# Set local and non-local item rules.
|
||||||
if world.players > 1:
|
if world.players > 1:
|
||||||
@@ -165,8 +159,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for player, items in depletion_pool.items():
|
for player, items in depletion_pool.items():
|
||||||
player_world: AutoWorld.World = world.worlds[player]
|
player_world: AutoWorld.World = world.worlds[player]
|
||||||
for count in items.values():
|
for count in items.values():
|
||||||
for _ in range(count):
|
new_items.append(player_world.create_filler())
|
||||||
new_items.append(player_world.create_filler())
|
|
||||||
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
||||||
for i, item in enumerate(world.itempool):
|
for i, item in enumerate(world.itempool):
|
||||||
if depletion_pool[item.player].get(item.name, 0):
|
if depletion_pool[item.player].get(item.name, 0):
|
||||||
@@ -186,7 +179,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
if remaining_items:
|
if remaining_items:
|
||||||
raise Exception(f"{world.get_player_name(player)}"
|
raise Exception(f"{world.get_player_name(player)}"
|
||||||
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
||||||
assert len(world.itempool) == len(new_items), "Item Pool amounts should not change."
|
|
||||||
world.itempool[:] = new_items
|
world.itempool[:] = new_items
|
||||||
|
|
||||||
# temporary home for item links, should be moved out of Main
|
# temporary home for item links, should be moved out of Main
|
||||||
@@ -400,7 +392,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
f.write(bytes([3])) # version of format
|
f.write(bytes([3])) # version of format
|
||||||
f.write(multidata)
|
f.write(multidata)
|
||||||
|
|
||||||
output_file_futures.append(pool.submit(write_multidata))
|
multidata_task = pool.submit(write_multidata)
|
||||||
if not check_accessibility_task.result():
|
if not check_accessibility_task.result():
|
||||||
if not world.can_beat_game():
|
if not world.can_beat_game():
|
||||||
raise Exception("Game appears as unbeatable. Aborting.")
|
raise Exception("Game appears as unbeatable. Aborting.")
|
||||||
@@ -408,6 +400,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||||
|
|
||||||
# retrieve exceptions via .result() if they occurred.
|
# retrieve exceptions via .result() if they occurred.
|
||||||
|
multidata_task.result()
|
||||||
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
|
||||||
if i % 10 == 0 or i == len(output_file_futures):
|
if i % 10 == 0 or i == len(output_file_futures):
|
||||||
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
|
||||||
|
|||||||
26
NetUtils.py
@@ -408,21 +408,13 @@ if typing.TYPE_CHECKING: # type-check with pure python implementation until we
|
|||||||
LocationStore = _LocationStore
|
LocationStore = _LocationStore
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from _speedups import LocationStore
|
import pyximport
|
||||||
import _speedups
|
pyximport.install()
|
||||||
import os.path
|
|
||||||
if os.path.isfile("_speedups.pyx") and os.path.getctime(_speedups.__file__) < os.path.getctime("_speedups.pyx"):
|
|
||||||
warnings.warn(f"{_speedups.__file__} outdated! "
|
|
||||||
f"Please rebuild with `cythonize -b -i _speedups.pyx` or delete it!")
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
pyximport = None
|
||||||
import pyximport
|
try:
|
||||||
pyximport.install()
|
from _speedups import LocationStore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pyximport = None
|
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
|
||||||
try:
|
"Install a matching C++ compiler for your platform to compile _speedups.")
|
||||||
from _speedups import LocationStore
|
LocationStore = _LocationStore
|
||||||
except ImportError:
|
|
||||||
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
|
|
||||||
"Install a matching C++ compiler for your platform to compile _speedups.")
|
|
||||||
LocationStore = _LocationStore
|
|
||||||
|
|||||||
23
SNIClient.py
@@ -68,11 +68,12 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
|||||||
options = snes_options.split()
|
options = snes_options.split()
|
||||||
num_options = len(options)
|
num_options = len(options)
|
||||||
|
|
||||||
|
if num_options > 0:
|
||||||
|
snes_device_number = int(options[0])
|
||||||
|
|
||||||
if num_options > 1:
|
if num_options > 1:
|
||||||
snes_address = options[0]
|
snes_address = options[0]
|
||||||
snes_device_number = int(options[1])
|
snes_device_number = int(options[1])
|
||||||
elif num_options > 0:
|
|
||||||
snes_device_number = int(options[0])
|
|
||||||
|
|
||||||
self.ctx.snes_reconnect_address = None
|
self.ctx.snes_reconnect_address = None
|
||||||
if self.ctx.snes_connect_task:
|
if self.ctx.snes_connect_task:
|
||||||
@@ -564,16 +565,14 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
|
|||||||
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||||
try:
|
try:
|
||||||
for address, data in write_list:
|
for address, data in write_list:
|
||||||
while data:
|
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
||||||
# Divide the write into packets of 256 bytes.
|
# REVIEW: above: `if snes_socket is None: return False`
|
||||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
# Does it need to be checked again?
|
||||||
if ctx.snes_socket is not None:
|
if ctx.snes_socket is not None:
|
||||||
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
await ctx.snes_socket.send(dumps(PutAddress_Request))
|
||||||
await ctx.snes_socket.send(data[:256])
|
await ctx.snes_socket.send(data)
|
||||||
address += 256
|
else:
|
||||||
data = data[256:]
|
snes_logger.warning(f"Could not send data to SNES: {data}")
|
||||||
else:
|
|
||||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
|
||||||
except ConnectionClosed:
|
except ConnectionClosed:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
1050
Starcraft2Client.py
@@ -29,31 +29,31 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
def _cmd_patch(self):
|
def _cmd_patch(self):
|
||||||
"""Patch the game."""
|
"""Patch the game."""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
||||||
self.ctx.patch_game()
|
self.ctx.patch_game()
|
||||||
self.output("Patched.")
|
self.output("Patched.")
|
||||||
|
|
||||||
def _cmd_savepath(self, directory: str):
|
def _cmd_savepath(self, directory: str):
|
||||||
"""Redirect to proper save data folder. (Use before connecting!)"""
|
"""Redirect to proper save data folder. (Use before connecting!)"""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
self.ctx.save_game_folder = directory
|
UndertaleContext.save_game_folder = directory
|
||||||
self.output("Changed to the following directory: " + self.ctx.save_game_folder)
|
self.output("Changed to the following directory: " + directory)
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None):
|
||||||
"""Patch the game automatically."""
|
"""Patch the game automatically."""
|
||||||
if isinstance(self.ctx, UndertaleContext):
|
if isinstance(self.ctx, UndertaleContext):
|
||||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True)
|
os.makedirs(name=os.getcwd() + "\\Undertale", exist_ok=True)
|
||||||
tempInstall = steaminstall
|
tempInstall = steaminstall
|
||||||
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
if not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
tempInstall = None
|
tempInstall = None
|
||||||
if tempInstall is None:
|
if tempInstall is None:
|
||||||
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||||
if not os.path.exists(tempInstall):
|
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
||||||
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||||
elif not os.path.exists(tempInstall):
|
elif not os.path.exists(tempInstall):
|
||||||
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
tempInstall = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"
|
||||||
if not os.path.exists(tempInstall):
|
if not os.path.exists("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
||||||
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
tempInstall = "C:\\Program Files\\Steam\\steamapps\\common\\Undertale"
|
||||||
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
if not os.path.exists(tempInstall) or not os.path.exists(tempInstall) or not os.path.isfile(os.path.join(tempInstall, "data.win")):
|
||||||
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
|
self.output("ERROR: Cannot find Undertale. Please rerun the command with the correct folder."
|
||||||
@@ -61,8 +61,8 @@ class UndertaleCommandProcessor(ClientCommandProcessor):
|
|||||||
else:
|
else:
|
||||||
for file_name in os.listdir(tempInstall):
|
for file_name in os.listdir(tempInstall):
|
||||||
if file_name != "steam_api.dll":
|
if file_name != "steam_api.dll":
|
||||||
shutil.copy(os.path.join(tempInstall, file_name),
|
shutil.copy(tempInstall+"\\"+file_name,
|
||||||
os.path.join(os.getcwd(), "Undertale", file_name))
|
os.getcwd() + "\\Undertale\\" + file_name)
|
||||||
self.ctx.patch_game()
|
self.ctx.patch_game()
|
||||||
self.output("Patching successful!")
|
self.output("Patching successful!")
|
||||||
|
|
||||||
@@ -111,13 +111,13 @@ class UndertaleContext(CommonContext):
|
|||||||
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE")
|
||||||
|
|
||||||
def patch_game(self):
|
def patch_game(self):
|
||||||
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f:
|
with open(os.getcwd() + "/Undertale/data.win", "rb") as f:
|
||||||
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
||||||
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f:
|
with open(os.getcwd() + "/Undertale/data.win", "wb") as f:
|
||||||
f.write(patchedFile)
|
f.write(patchedFile)
|
||||||
os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True)
|
os.makedirs(name=os.getcwd() + "\\Undertale\\" + "Custom Sprites", exist_ok=True)
|
||||||
with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites",
|
with open(os.path.expandvars(os.getcwd() + "\\Undertale\\" + "Custom Sprites\\" +
|
||||||
"Which Character.txt")), "w") as f:
|
"Which Character.txt"), "w") as f:
|
||||||
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only "
|
||||||
"line other than this one.\n", "frisk"])
|
"line other than this one.\n", "frisk"])
|
||||||
f.close()
|
f.close()
|
||||||
@@ -385,7 +385,7 @@ async def multi_watcher(ctx: UndertaleContext):
|
|||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if "spots.mine" in file and "Online" in ctx.tags:
|
if "spots.mine" in file and "Online" in ctx.tags:
|
||||||
with open(os.path.join(root, file), "r") as mine:
|
with open(root + "/" + file, "r") as mine:
|
||||||
this_x = mine.readline()
|
this_x = mine.readline()
|
||||||
this_y = mine.readline()
|
this_y = mine.readline()
|
||||||
this_room = mine.readline()
|
this_room = mine.readline()
|
||||||
@@ -408,7 +408,7 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if ".item" in file:
|
if ".item" in file:
|
||||||
os.remove(os.path.join(root, file))
|
os.remove(root+"/"+file)
|
||||||
sync_msg = [{"cmd": "Sync"}]
|
sync_msg = [{"cmd": "Sync"}]
|
||||||
if ctx.locations_checked:
|
if ctx.locations_checked:
|
||||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||||
@@ -424,13 +424,13 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
for file in files:
|
for file in files:
|
||||||
if "DontBeMad.mad" in file:
|
if "DontBeMad.mad" in file:
|
||||||
os.remove(os.path.join(root, file))
|
os.remove(root+"/"+file)
|
||||||
if "DeathLink" in ctx.tags:
|
if "DeathLink" in ctx.tags:
|
||||||
await ctx.send_death()
|
await ctx.send_death()
|
||||||
if "scout" == file:
|
if "scout" == file:
|
||||||
sending = []
|
sending = []
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(root, file), "r") as f:
|
with open(root+"/"+file, "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
for l in lines:
|
for l in lines:
|
||||||
if ctx.server_locations.__contains__(int(l)+12000):
|
if ctx.server_locations.__contains__(int(l)+12000):
|
||||||
@@ -438,11 +438,11 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
finally:
|
finally:
|
||||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": sending,
|
||||||
"create_as_hint": int(2)}])
|
"create_as_hint": int(2)}])
|
||||||
os.remove(os.path.join(root, file))
|
os.remove(root+"/"+file)
|
||||||
if "check.spot" in file:
|
if "check.spot" in file:
|
||||||
sending = []
|
sending = []
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(root, file), "r") as f:
|
with open(root+"/"+file, "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
for l in lines:
|
for l in lines:
|
||||||
sending = sending+[(int(l.rstrip('\n')))+12000]
|
sending = sending+[(int(l.rstrip('\n')))+12000]
|
||||||
@@ -451,7 +451,7 @@ async def game_watcher(ctx: UndertaleContext):
|
|||||||
if "victory" in file and str(ctx.route) in file:
|
if "victory" in file and str(ctx.route) in file:
|
||||||
victory = True
|
victory = True
|
||||||
if ".playerspot" in file and "Online" not in ctx.tags:
|
if ".playerspot" in file and "Online" not in ctx.tags:
|
||||||
os.remove(os.path.join(root, file))
|
os.remove(root+"/"+file)
|
||||||
if "victory" in file:
|
if "victory" in file:
|
||||||
if str(ctx.route) == "all_routes":
|
if str(ctx.route) == "all_routes":
|
||||||
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
||||||
|
|||||||
29
Utils.py
@@ -44,7 +44,7 @@ class Version(typing.NamedTuple):
|
|||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.4.3"
|
__version__ = "0.4.2"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
@@ -359,13 +359,11 @@ safe_builtins = frozenset((
|
|||||||
|
|
||||||
|
|
||||||
class RestrictedUnpickler(pickle.Unpickler):
|
class RestrictedUnpickler(pickle.Unpickler):
|
||||||
generic_properties_module: Optional[object]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
|
||||||
self.options_module = importlib.import_module("Options")
|
self.options_module = importlib.import_module("Options")
|
||||||
self.net_utils_module = importlib.import_module("NetUtils")
|
self.net_utils_module = importlib.import_module("NetUtils")
|
||||||
self.generic_properties_module = None
|
self.generic_properties_module = importlib.import_module("worlds.generic")
|
||||||
|
|
||||||
def find_class(self, module, name):
|
def find_class(self, module, name):
|
||||||
if module == "builtins" and name in safe_builtins:
|
if module == "builtins" and name in safe_builtins:
|
||||||
@@ -375,8 +373,6 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
return getattr(self.net_utils_module, name)
|
return getattr(self.net_utils_module, name)
|
||||||
# Options and Plando are unpickled by WebHost -> Generate
|
# Options and Plando are unpickled by WebHost -> Generate
|
||||||
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
|
||||||
if not self.generic_properties_module:
|
|
||||||
self.generic_properties_module = importlib.import_module("worlds.generic")
|
|
||||||
return getattr(self.generic_properties_module, name)
|
return getattr(self.generic_properties_module, name)
|
||||||
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
|
||||||
if module.lower().endswith("options"):
|
if module.lower().endswith("options"):
|
||||||
@@ -576,7 +572,7 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
|||||||
zenity = which("zenity")
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||||
selection = (f"--filename={suggest}",) if suggest else ()
|
selection = (f'--filename="{suggest}',) if suggest else ()
|
||||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
@@ -588,10 +584,7 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
|||||||
f'This attempt was made because open_filename was used for "{title}".')
|
f'This attempt was made because open_filename was used for "{title}".')
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
try:
|
root = tkinter.Tk()
|
||||||
root = tkinter.Tk()
|
|
||||||
except tkinter.TclError:
|
|
||||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||||
initialfile=suggest or None)
|
initialfile=suggest or None)
|
||||||
@@ -604,14 +597,13 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
|||||||
if is_linux:
|
if is_linux:
|
||||||
# prefer native dialog
|
# prefer native dialog
|
||||||
from shutil import which
|
from shutil import which
|
||||||
kdialog = which("kdialog")
|
kdialog = None#which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
return run(kdialog, f"--title={title}", "--getexistingdirectory",
|
return run(kdialog, f"--title={title}", "--getexistingdirectory", suggest or ".")
|
||||||
os.path.abspath(suggest) if suggest else ".")
|
zenity = None#which("zenity")
|
||||||
zenity = which("zenity")
|
|
||||||
if zenity:
|
if zenity:
|
||||||
z_filters = ("--directory",)
|
z_filters = ("--directory",)
|
||||||
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
|
selection = (f'--filename="{suggest}',) if suggest else ()
|
||||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
@@ -623,10 +615,7 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
|||||||
f'This attempt was made because open_filename was used for "{title}".')
|
f'This attempt was made because open_filename was used for "{title}".')
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
try:
|
root = tkinter.Tk()
|
||||||
root = tkinter.Tk()
|
|
||||||
except tkinter.TclError:
|
|
||||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None)
|
return tkinter.filedialog.askdirectory(title=title, mustexist=True, initialdir=suggest or None)
|
||||||
|
|
||||||
|
|||||||
37
WebHost.py
@@ -13,6 +13,15 @@ import Utils
|
|||||||
import settings
|
import settings
|
||||||
|
|
||||||
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
||||||
|
|
||||||
|
from WebHostLib import register, app as raw_app
|
||||||
|
from waitress import serve
|
||||||
|
|
||||||
|
from WebHostLib.models import db
|
||||||
|
from WebHostLib.autolauncher import autohost, autogen
|
||||||
|
from WebHostLib.options import create as create_options_files
|
||||||
|
import worlds
|
||||||
|
|
||||||
settings.no_gui = True
|
settings.no_gui = True
|
||||||
configpath = os.path.abspath("config.yaml")
|
configpath = os.path.abspath("config.yaml")
|
||||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||||
@@ -20,9 +29,6 @@ if not os.path.exists(configpath): # fall back to config.yaml in home
|
|||||||
|
|
||||||
|
|
||||||
def get_app():
|
def get_app():
|
||||||
from WebHostLib import register, cache, app as raw_app
|
|
||||||
from WebHostLib.models import db
|
|
||||||
|
|
||||||
register()
|
register()
|
||||||
app = raw_app
|
app = raw_app
|
||||||
if os.path.exists(configpath) and not app.config["TESTING"]:
|
if os.path.exists(configpath) and not app.config["TESTING"]:
|
||||||
@@ -34,9 +40,15 @@ def get_app():
|
|||||||
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
|
||||||
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
|
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
|
||||||
|
|
||||||
cache.init_app(app)
|
|
||||||
db.bind(**app.config["PONY"])
|
db.bind(**app.config["PONY"])
|
||||||
db.generate_mapping(create_tables=True)
|
db.generate_mapping(create_tables=True)
|
||||||
|
|
||||||
|
for world in worlds.AutoWorldRegister.world_types.values():
|
||||||
|
try:
|
||||||
|
world.web.run_webhost_app_setup(app)
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
@@ -116,16 +128,16 @@ if __name__ == "__main__":
|
|||||||
multiprocessing.set_start_method('spawn')
|
multiprocessing.set_start_method('spawn')
|
||||||
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
|
||||||
|
|
||||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
for world in worlds.AutoWorldRegister.world_types.values():
|
||||||
from WebHostLib.autolauncher import autohost, autogen
|
try:
|
||||||
from WebHostLib.options import create as create_options_files
|
world.web.run_webhost_setup()
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
|
||||||
try:
|
|
||||||
update_sprites_lttp()
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception(e)
|
|
||||||
logging.warning("Could not update LttP sprites.")
|
|
||||||
app = get_app()
|
app = get_app()
|
||||||
|
|
||||||
|
del world, worlds
|
||||||
|
|
||||||
create_options_files()
|
create_options_files()
|
||||||
create_ordered_tutorials_file()
|
create_ordered_tutorials_file()
|
||||||
if app.config["SELFLAUNCH"]:
|
if app.config["SELFLAUNCH"]:
|
||||||
@@ -136,5 +148,4 @@ if __name__ == "__main__":
|
|||||||
if app.config["DEBUG"]:
|
if app.config["DEBUG"]:
|
||||||
app.run(debug=True, port=app.config["PORT"])
|
app.run(debug=True, port=app.config["PORT"])
|
||||||
else:
|
else:
|
||||||
from waitress import serve
|
|
||||||
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ app.config["PONY"] = {
|
|||||||
'create_db': True
|
'create_db': True
|
||||||
}
|
}
|
||||||
app.config["MAX_ROLL"] = 20
|
app.config["MAX_ROLL"] = 20
|
||||||
app.config["CACHE_TYPE"] = "SimpleCache"
|
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
|
||||||
app.config["JSON_AS_ASCII"] = False
|
app.config["JSON_AS_ASCII"] = False
|
||||||
app.config["HOST_ADDRESS"] = ""
|
app.config["HOST_ADDRESS"] = ""
|
||||||
|
|
||||||
cache = Cache()
|
cache = Cache(app)
|
||||||
Compress(app)
|
Compress(app)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
@@ -11,7 +13,55 @@ from datetime import timedelta, datetime
|
|||||||
from pony.orm import db_session, select, commit
|
from pony.orm import db_session, select, commit
|
||||||
|
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads
|
||||||
from .locker import Locker, AlreadyRunningException
|
|
||||||
|
|
||||||
|
class CommonLocker():
|
||||||
|
"""Uses a file lock to signal that something is already running"""
|
||||||
|
lock_folder = "file_locks"
|
||||||
|
|
||||||
|
def __init__(self, lockname: str, folder=None):
|
||||||
|
if folder:
|
||||||
|
self.lock_folder = folder
|
||||||
|
os.makedirs(self.lock_folder, exist_ok=True)
|
||||||
|
self.lockname = lockname
|
||||||
|
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
||||||
|
|
||||||
|
|
||||||
|
class AlreadyRunningException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
class Locker(CommonLocker):
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.lockfile):
|
||||||
|
os.unlink(self.lockfile)
|
||||||
|
self.fp = os.open(
|
||||||
|
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
||||||
|
except OSError as e:
|
||||||
|
raise AlreadyRunningException() from e
|
||||||
|
|
||||||
|
def __exit__(self, _type, value, tb):
|
||||||
|
fp = getattr(self, "fp", None)
|
||||||
|
if fp:
|
||||||
|
os.close(self.fp)
|
||||||
|
os.unlink(self.lockfile)
|
||||||
|
else: # unix
|
||||||
|
import fcntl
|
||||||
|
|
||||||
|
|
||||||
|
class Locker(CommonLocker):
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
self.fp = open(self.lockfile, "wb")
|
||||||
|
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except OSError as e:
|
||||||
|
raise AlreadyRunningException() from e
|
||||||
|
|
||||||
|
def __exit__(self, _type, value, tb):
|
||||||
|
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
|
||||||
|
self.fp.close()
|
||||||
|
|
||||||
|
|
||||||
def launch_room(room: Room, config: dict):
|
def launch_room(room: Room, config: dict):
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ def check():
|
|||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
flash('No file part')
|
flash('No file part')
|
||||||
else:
|
else:
|
||||||
files = request.files.getlist('file')
|
file = request.files['file']
|
||||||
options = get_yaml_data(files)
|
options = get_yaml_data(file)
|
||||||
if isinstance(options, str):
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
@@ -39,33 +39,30 @@ def mysterycheck():
|
|||||||
return redirect(url_for("check"), 301)
|
return redirect(url_for("check"), 301)
|
||||||
|
|
||||||
|
|
||||||
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
|
def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
|
||||||
options = {}
|
options = {}
|
||||||
for file in files:
|
# if user does not select file, browser also
|
||||||
# if user does not select file, browser also
|
# submit an empty part without filename
|
||||||
# submit an empty part without filename
|
if file.filename == '':
|
||||||
if file.filename == '':
|
return 'No selected file'
|
||||||
return 'No selected file'
|
elif file and allowed_file(file.filename):
|
||||||
elif file.filename in options:
|
if file.filename.endswith(".zip"):
|
||||||
return f'Conflicting files named {file.filename} submitted'
|
|
||||||
elif file and allowed_file(file.filename):
|
|
||||||
if file.filename.endswith(".zip"):
|
|
||||||
|
|
||||||
with zipfile.ZipFile(file, 'r') as zfile:
|
with zipfile.ZipFile(file, 'r') as zfile:
|
||||||
infolist = zfile.infolist()
|
infolist = zfile.infolist()
|
||||||
|
|
||||||
if any(file.filename.endswith(".archipelago") for file in infolist):
|
if any(file.filename.endswith(".archipelago") for file in infolist):
|
||||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||||
|
|
||||||
for file in infolist:
|
for file in infolist:
|
||||||
if file.filename.endswith(banned_zip_contents):
|
if file.filename.endswith(banned_zip_contents):
|
||||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||||
"Your file was deleted."
|
"Your file was deleted."
|
||||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||||
options[file.filename] = zfile.open(file, "r").read()
|
options[file.filename] = zfile.open(file, "r").read()
|
||||||
else:
|
else:
|
||||||
options[file.filename] = file.read()
|
options = {file.filename: file.read()}
|
||||||
if not options:
|
if not options:
|
||||||
return "Did not find a .yaml file to process."
|
return "Did not find a .yaml file to process."
|
||||||
return options
|
return options
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import Utils
|
|||||||
|
|
||||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
|
||||||
from Utils import restricted_loads, cache_argsless
|
from Utils import restricted_loads, cache_argsless
|
||||||
from .locker import Locker
|
|
||||||
from .models import Command, GameDataPackage, Room, db
|
from .models import Command, GameDataPackage, Room, db
|
||||||
|
|
||||||
|
|
||||||
@@ -164,19 +163,16 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
|||||||
db.generate_mapping(check_tables=False)
|
db.generate_mapping(check_tables=False)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
import gc
|
|
||||||
|
|
||||||
Utils.init_logging(str(room_id), write_mode="a")
|
Utils.init_logging(str(room_id), write_mode="a")
|
||||||
ctx = WebHostContext(static_server_data)
|
ctx = WebHostContext(static_server_data)
|
||||||
ctx.load(room_id)
|
ctx.load(room_id)
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||||
gc.collect() # free intermediate objects used during setup
|
|
||||||
try:
|
try:
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
except OSError: # likely port in use
|
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
@@ -202,15 +198,16 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
|||||||
await ctx.shutdown_task
|
await ctx.shutdown_task
|
||||||
logging.info("Shutting down")
|
logging.info("Shutting down")
|
||||||
|
|
||||||
|
from .autolauncher import Locker
|
||||||
with Locker(room_id):
|
with Locker(room_id):
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except KeyboardInterrupt:
|
||||||
with db_session:
|
with db_session:
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||||
except Exception:
|
except:
|
||||||
with db_session:
|
with db_session:
|
||||||
room = Room.get(id=room_id)
|
room = Room.get(id=room_id)
|
||||||
room.last_port = -1
|
room.last_port = -1
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ def generate(race=False):
|
|||||||
if 'file' not in request.files:
|
if 'file' not in request.files:
|
||||||
flash('No file part')
|
flash('No file part')
|
||||||
else:
|
else:
|
||||||
files = request.files.getlist('file')
|
file = request.files['file']
|
||||||
options = get_yaml_data(files)
|
options = get_yaml_data(file)
|
||||||
if isinstance(options, str):
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class CommonLocker:
|
|
||||||
"""Uses a file lock to signal that something is already running"""
|
|
||||||
lock_folder = "file_locks"
|
|
||||||
|
|
||||||
def __init__(self, lockname: str, folder=None):
|
|
||||||
if folder:
|
|
||||||
self.lock_folder = folder
|
|
||||||
os.makedirs(self.lock_folder, exist_ok=True)
|
|
||||||
self.lockname = lockname
|
|
||||||
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
|
||||||
|
|
||||||
|
|
||||||
class AlreadyRunningException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
class Locker(CommonLocker):
|
|
||||||
def __enter__(self):
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.lockfile):
|
|
||||||
os.unlink(self.lockfile)
|
|
||||||
self.fp = os.open(
|
|
||||||
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
|
||||||
except OSError as e:
|
|
||||||
raise AlreadyRunningException() from e
|
|
||||||
|
|
||||||
def __exit__(self, _type, value, tb):
|
|
||||||
fp = getattr(self, "fp", None)
|
|
||||||
if fp:
|
|
||||||
os.close(self.fp)
|
|
||||||
os.unlink(self.lockfile)
|
|
||||||
else: # unix
|
|
||||||
import fcntl
|
|
||||||
|
|
||||||
|
|
||||||
class Locker(CommonLocker):
|
|
||||||
def __enter__(self):
|
|
||||||
try:
|
|
||||||
self.fp = open(self.lockfile, "wb")
|
|
||||||
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
||||||
except OSError as e:
|
|
||||||
raise AlreadyRunningException() from e
|
|
||||||
|
|
||||||
def __exit__(self, _type, value, tb):
|
|
||||||
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
|
|
||||||
self.fp.close()
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import os
|
|
||||||
import threading
|
|
||||||
import json
|
|
||||||
|
|
||||||
from Utils import local_path, user_path
|
|
||||||
from worlds.alttp.Rom import Sprite
|
|
||||||
|
|
||||||
|
|
||||||
def update_sprites_lttp():
|
|
||||||
from tkinter import Tk
|
|
||||||
from LttPAdjuster import get_image_for_sprite
|
|
||||||
from LttPAdjuster import BackgroundTaskProgress
|
|
||||||
from LttPAdjuster import BackgroundTaskProgressNullWindow
|
|
||||||
from LttPAdjuster import update_sprites
|
|
||||||
|
|
||||||
# Target directories
|
|
||||||
input_dir = user_path("data", "sprites", "alttpr")
|
|
||||||
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
|
|
||||||
|
|
||||||
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
|
|
||||||
# update sprites through gui.py's functions
|
|
||||||
done = threading.Event()
|
|
||||||
try:
|
|
||||||
top = Tk()
|
|
||||||
except:
|
|
||||||
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
|
|
||||||
else:
|
|
||||||
top.withdraw()
|
|
||||||
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
|
||||||
while not done.isSet():
|
|
||||||
task.do_events()
|
|
||||||
|
|
||||||
spriteData = []
|
|
||||||
|
|
||||||
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
|
|
||||||
sprite = Sprite(os.path.join(input_dir, file))
|
|
||||||
|
|
||||||
if not sprite.name:
|
|
||||||
print("Warning:", file, "has no name.")
|
|
||||||
sprite.name = file.split(".", 1)[0]
|
|
||||||
if sprite.valid:
|
|
||||||
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
|
|
||||||
image.write(get_image_for_sprite(sprite, True))
|
|
||||||
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
|
|
||||||
else:
|
|
||||||
print(file, "dropped, as it has no valid sprite data.")
|
|
||||||
spriteData.sort(key=lambda entry: entry["name"])
|
|
||||||
with open(f'{output_dir}/spriteData.json', 'w') as file:
|
|
||||||
json.dump({"sprites": spriteData}, file, indent=1)
|
|
||||||
return spriteData
|
|
||||||
@@ -3,8 +3,7 @@ pony>=0.7.16; python_version <= '3.10'
|
|||||||
pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11'
|
pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11'
|
||||||
waitress>=2.1.2
|
waitress>=2.1.2
|
||||||
Flask-Caching>=2.0.2
|
Flask-Caching>=2.0.2
|
||||||
Flask-Compress>=1.14
|
Flask-Compress>=1.13
|
||||||
Flask-Limiter>=3.5.0
|
Flask-Limiter>=3.3.0
|
||||||
bokeh>=3.1.1; python_version <= '3.8'
|
bokeh>=3.1.1
|
||||||
bokeh>=3.2.2; python_version >= '3.9'
|
|
||||||
markupsafe>=2.1.3
|
markupsafe>=2.1.3
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
window.addEventListener('load', () => {
|
|
||||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
|
||||||
Array.from(gameHeaders).forEach((header) => {
|
|
||||||
const gameName = header.getAttribute('data-game');
|
|
||||||
header.addEventListener('click', () => {
|
|
||||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
|
||||||
const gameInfo = document.getElementById(gameName);
|
|
||||||
if (gameInfo.classList.contains('collapsed')) {
|
|
||||||
gameArrow.innerText = '▼';
|
|
||||||
gameInfo.classList.remove('collapsed');
|
|
||||||
} else {
|
|
||||||
gameArrow.innerText = '▶';
|
|
||||||
gameInfo.classList.add('collapsed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle game filter input
|
|
||||||
const gameSearch = document.getElementById('game-search');
|
|
||||||
gameSearch.value = '';
|
|
||||||
|
|
||||||
gameSearch.addEventListener('input', (evt) => {
|
|
||||||
if (!evt.target.value.trim()) {
|
|
||||||
// If input is empty, display all collapsed games
|
|
||||||
return Array.from(gameHeaders).forEach((header) => {
|
|
||||||
header.style.display = null;
|
|
||||||
const gameName = header.getAttribute('data-game');
|
|
||||||
document.getElementById(`${gameName}-arrow`).innerText = '▶';
|
|
||||||
document.getElementById(gameName).classList.add('collapsed');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop over all the games
|
|
||||||
Array.from(gameHeaders).forEach((header) => {
|
|
||||||
const gameName = header.getAttribute('data-game');
|
|
||||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
|
||||||
const gameInfo = document.getElementById(gameName);
|
|
||||||
|
|
||||||
// If the game name includes the search string, display the game. If not, hide it
|
|
||||||
if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) {
|
|
||||||
header.style.display = null;
|
|
||||||
gameArrow.innerText = '▼';
|
|
||||||
gameInfo.classList.remove('collapsed');
|
|
||||||
} else {
|
|
||||||
console.log(header);
|
|
||||||
header.style.display = 'none';
|
|
||||||
gameArrow.innerText = '▶';
|
|
||||||
gameInfo.classList.add('collapsed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('expand-all').addEventListener('click', expandAll);
|
|
||||||
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
|
||||||
});
|
|
||||||
|
|
||||||
const expandAll = () => {
|
|
||||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
|
||||||
// Loop over all the games
|
|
||||||
Array.from(gameHeaders).forEach((header) => {
|
|
||||||
const gameName = header.getAttribute('data-game');
|
|
||||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
|
||||||
const gameInfo = document.getElementById(gameName);
|
|
||||||
|
|
||||||
if (header.style.display === 'none') { return; }
|
|
||||||
gameArrow.innerText = '▼';
|
|
||||||
gameInfo.classList.remove('collapsed');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const collapseAll = () => {
|
|
||||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
|
||||||
// Loop over all the games
|
|
||||||
Array.from(gameHeaders).forEach((header) => {
|
|
||||||
const gameName = header.getAttribute('data-game');
|
|
||||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
|
||||||
const gameInfo = document.getElementById(gameName);
|
|
||||||
|
|
||||||
if (header.style.display === 'none') { return; }
|
|
||||||
gameArrow.innerText = '▶';
|
|
||||||
gameInfo.classList.add('collapsed');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -14,17 +14,6 @@ const adjustTableHeight = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert an integer number of seconds into a human readable HH:MM format
|
|
||||||
* @param {Number} seconds
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
const secondsToHours = (seconds) => {
|
|
||||||
let hours = Math.floor(seconds / 3600);
|
|
||||||
let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0');
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const tables = $(".table").DataTable({
|
const tables = $(".table").DataTable({
|
||||||
paging: false,
|
paging: false,
|
||||||
@@ -38,18 +27,7 @@ window.addEventListener('load', () => {
|
|||||||
stateLoadCallback: function(settings) {
|
stateLoadCallback: function(settings) {
|
||||||
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
|
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
|
||||||
},
|
},
|
||||||
footerCallback: function(tfoot, data, start, end, display) {
|
|
||||||
if (tfoot) {
|
|
||||||
const activityData = this.api().column('lastActivity:name').data().toArray().filter(x => !isNaN(x));
|
|
||||||
Array.from(tfoot?.children).find(td => td.classList.contains('last-activity')).innerText =
|
|
||||||
(activityData.length) ? secondsToHours(Math.min(...activityData)) : 'None';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
columnDefs: [
|
columnDefs: [
|
||||||
{
|
|
||||||
targets: 'last-activity',
|
|
||||||
name: 'lastActivity'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
targets: 'hours',
|
targets: 'hours',
|
||||||
render: function (data, type, row) {
|
render: function (data, type, row) {
|
||||||
@@ -62,7 +40,11 @@ window.addEventListener('load', () => {
|
|||||||
if (data === "None")
|
if (data === "None")
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
return secondsToHours(data);
|
let hours = Math.floor(data / 3600);
|
||||||
|
let minutes = Math.floor((data - (hours * 3600)) / 60);
|
||||||
|
|
||||||
|
if (minutes < 10) {minutes = "0"+minutes;}
|
||||||
|
return hours+':'+minutes;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -132,16 +114,11 @@ window.addEventListener('load', () => {
|
|||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
target.find(".table").each(function (i, new_table) {
|
target.find(".table").each(function (i, new_table) {
|
||||||
const new_trs = $(new_table).find("tbody>tr");
|
const new_trs = $(new_table).find("tbody>tr");
|
||||||
const footer_tr = $(new_table).find("tfoot>tr");
|
|
||||||
const old_table = tables.eq(i);
|
const old_table = tables.eq(i);
|
||||||
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
|
const topscroll = $(old_table.settings()[0].nScrollBody).scrollTop();
|
||||||
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
|
const leftscroll = $(old_table.settings()[0].nScrollBody).scrollLeft();
|
||||||
old_table.clear();
|
old_table.clear();
|
||||||
if (footer_tr.length) {
|
old_table.rows.add(new_trs).draw();
|
||||||
$(old_table.table).find("tfoot").html(footer_tr);
|
|
||||||
}
|
|
||||||
old_table.rows.add(new_trs);
|
|
||||||
old_table.draw();
|
|
||||||
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
||||||
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ const buildUI = (settingData) => {
|
|||||||
weightedSettingsDiv.classList.add('invisible');
|
weightedSettingsDiv.classList.add('invisible');
|
||||||
itemPoolDiv.classList.add('invisible');
|
itemPoolDiv.classList.add('invisible');
|
||||||
hintsDiv.classList.add('invisible');
|
hintsDiv.classList.add('invisible');
|
||||||
locationsDiv.classList.add('invisible');
|
|
||||||
expandButton.classList.remove('invisible');
|
expandButton.classList.remove('invisible');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,7 +168,6 @@ const buildUI = (settingData) => {
|
|||||||
weightedSettingsDiv.classList.remove('invisible');
|
weightedSettingsDiv.classList.remove('invisible');
|
||||||
itemPoolDiv.classList.remove('invisible');
|
itemPoolDiv.classList.remove('invisible');
|
||||||
hintsDiv.classList.remove('invisible');
|
hintsDiv.classList.remove('invisible');
|
||||||
locationsDiv.classList.remove('invisible');
|
|
||||||
expandButton.classList.add('invisible');
|
expandButton.classList.add('invisible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1136,8 +1134,8 @@ const validateSettings = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove any disabled options
|
||||||
Object.keys(settings[game]).forEach((setting) => {
|
Object.keys(settings[game]).forEach((setting) => {
|
||||||
// Remove any disabled options
|
|
||||||
Object.keys(settings[game][setting]).forEach((option) => {
|
Object.keys(settings[game][setting]).forEach((option) => {
|
||||||
if (settings[game][setting][option] === 0) {
|
if (settings[game][setting][option] === 0) {
|
||||||
delete settings[game][setting][option];
|
delete settings[game][setting][option];
|
||||||
@@ -1151,32 +1149,6 @@ const validateSettings = () => {
|
|||||||
) {
|
) {
|
||||||
errorMessage = `${game} // ${setting} has no values above zero!`;
|
errorMessage = `${game} // ${setting} has no values above zero!`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove weights from options with only one possibility
|
|
||||||
if (
|
|
||||||
Object.keys(settings[game][setting]).length === 1 &&
|
|
||||||
!Array.isArray(settings[game][setting]) &&
|
|
||||||
setting !== 'start_inventory'
|
|
||||||
) {
|
|
||||||
settings[game][setting] = Object.keys(settings[game][setting])[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove empty arrays
|
|
||||||
else if (
|
|
||||||
['exclude_locations', 'priority_locations', 'local_items',
|
|
||||||
'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) &&
|
|
||||||
settings[game][setting].length === 0
|
|
||||||
) {
|
|
||||||
delete settings[game][setting];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove empty start inventory
|
|
||||||
else if (
|
|
||||||
setting === 'start_inventory' &&
|
|
||||||
Object.keys(settings[game]['start_inventory']).length === 0
|
|
||||||
) {
|
|
||||||
delete settings[game]['start_inventory'];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1184,11 +1156,6 @@ const validateSettings = () => {
|
|||||||
errorMessage = 'You have not chosen a game to play!';
|
errorMessage = 'You have not chosen a game to play!';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove weights if there is only one game
|
|
||||||
else if (Object.keys(settings.game).length === 1) {
|
|
||||||
settings.game = Object.keys(settings.game)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If an error occurred, alert the user and do not export the file
|
// If an error occurred, alert the user and do not export the file
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
userMessage.innerText = errorMessage;
|
userMessage.innerText = errorMessage;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -9,7 +9,7 @@
|
|||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
padding: 3px 3px 10px;
|
padding: 3px 3px 10px;
|
||||||
width: 710px;
|
width: 500px;
|
||||||
background-color: #525494;
|
background-color: #525494;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,12 +34,10 @@
|
|||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
border: 1px solid #000000;
|
border: 1px solid #000000;
|
||||||
filter: grayscale(100%) contrast(75%) brightness(20%);
|
filter: grayscale(100%) contrast(75%) brightness(20%);
|
||||||
background-color: black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table img.acquired{
|
#inventory-table img.acquired{
|
||||||
filter: none;
|
filter: none;
|
||||||
background-color: black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#inventory-table div.counted-item {
|
#inventory-table div.counted-item {
|
||||||
@@ -54,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#location-table{
|
#location-table{
|
||||||
width: 710px;
|
width: 500px;
|
||||||
border-left: 2px solid #000000;
|
border-left: 2px solid #000000;
|
||||||
border-right: 2px solid #000000;
|
border-right: 2px solid #000000;
|
||||||
border-bottom: 2px solid #000000;
|
border-bottom: 2px solid #000000;
|
||||||
|
|||||||
@@ -18,16 +18,6 @@
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#games h2 .collapse-arrow{
|
|
||||||
font-size: 20px;
|
|
||||||
vertical-align: middle;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#games p.collapsed{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#games a{
|
#games a{
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -41,13 +31,3 @@
|
|||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#games #page-controls{
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#games #page-controls button{
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,16 +55,16 @@ table.dataTable thead{
|
|||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody, table.dataTable tfoot{
|
table.dataTable tbody{
|
||||||
background-color: #dce2bd;
|
background-color: #dce2bd;
|
||||||
font-family: LexendDeca-Light, sans-serif;
|
font-family: LexendDeca-Light, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{
|
table.dataTable tbody tr:hover{
|
||||||
background-color: #e2eabb;
|
background-color: #e2eabb;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody td, table.dataTable tfoot td{
|
table.dataTable tbody td{
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,14 +97,10 @@ table.dataTable thead th.lower-row{
|
|||||||
top: 46px;
|
top: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody td, table.dataTable tfoot td{
|
table.dataTable tbody td{
|
||||||
border: 1px solid #bba967;
|
border: 1px solid #bba967;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tfoot td{
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.dataTables_scrollBody{
|
div.dataTables_scrollBody{
|
||||||
background-color: inherit !important;
|
background-color: inherit !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<div id="check-form-wrapper">
|
<div id="check-form-wrapper">
|
||||||
<form id="check-form" method="post" enctype="multipart/form-data">
|
<form id="check-form" method="post" enctype="multipart/form-data">
|
||||||
<input id="file-input" type="file" name="file" multiple>
|
<input id="file-input" type="file" name="file">
|
||||||
</form>
|
</form>
|
||||||
<button id="check-button">Upload File(s)</button>
|
<button id="check-button">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -203,10 +203,10 @@ Warning: playthrough can take a significant amount of time for larger multiworld
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="generate-form-button-row">
|
<div id="generate-form-button-row">
|
||||||
<input id="file-input" type="file" name="file" multiple>
|
<input id="file-input" type="file" name="file">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<button id="generate-game-button">Upload File(s)</button>
|
<button id="generate-game-button">Upload File</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
<th class="center-column">Checks</th>
|
<th class="center-column">Checks</th>
|
||||||
<th class="center-column">%</th>
|
<th class="center-column">%</th>
|
||||||
<th class="center-column hours last-activity">Last<br>Activity</th>
|
<th class="center-column hours">Last<br>Activity</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -64,19 +64,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</tbody>
|
</tbody>
|
||||||
{% if not self.custom_table_headers() | trim %}
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>Total</td>
|
|
||||||
<td>All Games</td>
|
|
||||||
<td>{{ completed_worlds }}/{{ players|length }} Complete</td>
|
|
||||||
<td class="center-column">{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}</td>
|
|
||||||
<td class="center-column">{{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }}</td>
|
|
||||||
<td class="center-column last-activity"></td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
{% endif %}
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{%- if enabled_multiworld_trackers|length > 1 -%}
|
{%- if enabled_multiworld_trackers|length > 1 -%}
|
||||||
<div id="tracker-navigation">
|
<div id="tracker-navigation">
|
||||||
{% for enabled_tracker in enabled_multiworld_trackers %}
|
{% for enabled_tracker in enabled_multiworld_trackers %}
|
||||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker, game=enabled_tracker.name) %}
|
||||||
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
|
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
|
||||||
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||||
<table id="inventory-table">
|
<table id="inventory-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="15" class="title">
|
<td colspan="10" class="title">
|
||||||
Starting Resources
|
Starting Resources
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
-->
|
-->
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="15" class="title">
|
<td colspan="10" class="title">
|
||||||
Weapon & Armor Upgrades
|
Weapon & Armor Upgrades
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -37,266 +37,120 @@
|
|||||||
<td><img src="{{ vehicle_armor_url }}" class="{{ 'acquired' if 'Progressive Vehicle Armor' in acquired_items }}" title="Progressive Vehicle Armor{% if vehicle_armor_level > 0 %} (Level {{ vehicle_armor_level }}){% endif %}" /></td>
|
<td><img src="{{ vehicle_armor_url }}" class="{{ 'acquired' if 'Progressive Vehicle Armor' in acquired_items }}" title="Progressive Vehicle Armor{% if vehicle_armor_level > 0 %} (Level {{ vehicle_armor_level }}){% endif %}" /></td>
|
||||||
<td><img src="{{ ship_weapon_url }}" class="{{ 'acquired' if 'Progressive Ship Weapon' in acquired_items }}" title="Progressive Ship Weapons{% if ship_weapon_level > 0 %} (Level {{ ship_weapon_level }}){% endif %}" /></td>
|
<td><img src="{{ ship_weapon_url }}" class="{{ 'acquired' if 'Progressive Ship Weapon' in acquired_items }}" title="Progressive Ship Weapons{% if ship_weapon_level > 0 %} (Level {{ ship_weapon_level }}){% endif %}" /></td>
|
||||||
<td><img src="{{ ship_armor_url }}" class="{{ 'acquired' if 'Progressive Ship Armor' in acquired_items }}" title="Progressive Ship Armor{% if ship_armor_level > 0 %} (Level {{ ship_armor_level }}){% endif %}" /></td>
|
<td><img src="{{ ship_armor_url }}" class="{{ 'acquired' if 'Progressive Ship Armor' in acquired_items }}" title="Progressive Ship Armor{% if ship_armor_level > 0 %} (Level {{ ship_armor_level }}){% endif %}" /></td>
|
||||||
<td colspan="2"></td>
|
|
||||||
<td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></td>
|
|
||||||
<td><img src="{{ icons['Vanadium Plating'] }}" class="{{ 'acquired' if 'Vanadium Plating' in acquired_items }}" title="Vanadium Plating" /></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="15" class="title">
|
<td colspan="10" class="title">
|
||||||
Base
|
Base
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td>
|
<td colspan="2"><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Missile Turret'] }}" class="{{ 'acquired' if 'Missile Turret' in acquired_items }}" title="Missile Turret" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td><img src="{{ icons['Projectile Accelerator (Bunker)'] }}" class="{{ 'acquired' if 'Projectile Accelerator (Bunker)' in acquired_items }}" title="Projectile Accelerator (Bunker)" /></td>
|
<td><img src="{{ icons['Projectile Accelerator (Bunker)'] }}" class="{{ 'acquired' if 'Projectile Accelerator (Bunker)' in acquired_items }}" title="Projectile Accelerator (Bunker)" /></td>
|
||||||
<td><img src="{{ icons['Neosteel Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Neosteel Bunker (Bunker)' in acquired_items }}" title="Neosteel Bunker (Bunker)" /></td>
|
<td><img src="{{ icons['Neosteel Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Neosteel Bunker (Bunker)' in acquired_items }}" title="Neosteel Bunker (Bunker)" /></td>
|
||||||
<td><img src="{{ icons['Shrike Turret (Bunker)'] }}" class="{{ 'acquired' if 'Shrike Turret (Bunker)' in acquired_items }}" title="Shrike Turret (Bunker)" /></td>
|
|
||||||
<td><img src="{{ icons['Fortified Bunker (Bunker)'] }}" class="{{ 'acquired' if 'Fortified Bunker (Bunker)' in acquired_items }}" title="Fortified Bunker (Bunker)" /></td>
|
|
||||||
<td colspan="3"></td>
|
|
||||||
<td><img src="{{ icons['Missile Turret'] }}" class="{{ 'acquired' if 'Missile Turret' in acquired_items }}" title="Missile Turret" /></td>
|
|
||||||
<td><img src="{{ icons['Titanium Housing (Missile Turret)'] }}" class="{{ 'acquired' if 'Titanium Housing (Missile Turret)' in acquired_items }}" title="Titanium Housing (Missile Turret)" /></td>
|
<td><img src="{{ icons['Titanium Housing (Missile Turret)'] }}" class="{{ 'acquired' if 'Titanium Housing (Missile Turret)' in acquired_items }}" title="Titanium Housing (Missile Turret)" /></td>
|
||||||
<td><img src="{{ icons['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td>
|
<td><img src="{{ icons['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td>
|
||||||
</tr>
|
<td colspan="2"> </td>
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
|
|
||||||
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
|
|
||||||
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td>
|
<td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td>
|
||||||
<td><img src="{{ icons['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></td>
|
<td><img src="{{ icons['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></td>
|
||||||
<td></td>
|
<td><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
|
||||||
<td><img src="{{ icons['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td>
|
<td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
|
||||||
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
|
<td colspan="10" class="title">
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="7" class="title">
|
|
||||||
Infantry
|
Infantry
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
</tr>
|
||||||
<td colspan="7" class="title">
|
<tr>
|
||||||
|
<td colspan="2"><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Stimpack (Marine)'] }}" class="{{ 'acquired' if 'Stimpack (Marine)' in acquired_items }}" title="Stimpack (Marine)" /></td>
|
||||||
|
<td><img src="{{ icons['Combat Shield (Marine)'] }}" class="{{ 'acquired' if 'Combat Shield (Marine)' in acquired_items }}" title="Combat Shield (Marine)" /></td>
|
||||||
|
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
|
||||||
|
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
|
||||||
|
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
|
||||||
|
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
|
||||||
|
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
|
||||||
|
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
|
||||||
|
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
|
||||||
|
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
Vehicles
|
Vehicles
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
|
<td colspan="2"><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
|
||||||
<td><img src="{{ stimpack_marine_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marine)' in acquired_items }}" title="{{ stimpack_marine_name }}" /></td>
|
<td colspan="2"><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
|
||||||
<td><img src="{{ icons['Combat Shield (Marine)'] }}" class="{{ 'acquired' if 'Combat Shield (Marine)' in acquired_items }}" title="Combat Shield (Marine)" /></td>
|
<td colspan="2"><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
|
||||||
<td><img src="{{ icons['Laser Targeting System (Marine)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marine)' in acquired_items }}" title="Laser Targeting System (Marine)" /></td>
|
<td colspan="2"><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
|
||||||
<td><img src="{{ icons['Magrail Munitions (Marine)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marine)' in acquired_items }}" title="Magrail Munitions (Marine)" /></td>
|
<td colspan="2"><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
|
||||||
<td><img src="{{ icons['Optimized Logistics (Marine)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Marine)' in acquired_items }}" title="Optimized Logistics (Marine)" /></td>
|
</tr>
|
||||||
<td colspan="2"></td>
|
<tr>
|
||||||
<td><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
|
|
||||||
<td><img src="{{ icons['Twin-Linked Flamethrower (Hellion)'] }}" class="{{ 'acquired' if 'Twin-Linked Flamethrower (Hellion)' in acquired_items }}" title="Twin-Linked Flamethrower (Hellion)" /></td>
|
<td><img src="{{ icons['Twin-Linked Flamethrower (Hellion)'] }}" class="{{ 'acquired' if 'Twin-Linked Flamethrower (Hellion)' in acquired_items }}" title="Twin-Linked Flamethrower (Hellion)" /></td>
|
||||||
<td><img src="{{ icons['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (Hellion)" /></td>
|
<td><img src="{{ icons['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (Hellion)" /></td>
|
||||||
<td><img src="{{ icons['Hellbat Aspect (Hellion)'] }}" class="{{ 'acquired' if 'Hellbat Aspect (Hellion)' in acquired_items }}" title="Hellbat Aspect (Hellion)" /></td>
|
<td><img src="{{ icons['Cerberus Mine (Vulture)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Vulture)' in acquired_items }}" title="Cerberus Mine (Vulture)" /></td>
|
||||||
<td><img src="{{ icons['Smart Servos (Hellion)'] }}" class="{{ 'acquired' if 'Smart Servos (Hellion)' in acquired_items }}" title="Smart Servos (Hellion)" /></td>
|
|
||||||
<td><img src="{{ icons['Optimized Logistics (Hellion)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Hellion)' in acquired_items }}" title="Optimized Logistics (Hellion)" /></td>
|
|
||||||
<td><img src="{{ icons['Jump Jets (Hellion)'] }}" class="{{ 'acquired' if 'Jump Jets (Hellion)' in acquired_items }}" title="Jump Jets (Hellion)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
|
|
||||||
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
|
|
||||||
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
|
|
||||||
<td><img src="{{ icons['Restoration (Medic)'] }}" class="{{ 'acquired' if 'Restoration (Medic)' in acquired_items }}" title="Restoration (Medic)" /></td>
|
|
||||||
<td><img src="{{ icons['Optical Flare (Medic)'] }}" class="{{ 'acquired' if 'Optical Flare (Medic)' in acquired_items }}" title="Optical Flare (Medic)" /></td>
|
|
||||||
<td><img src="{{ icons['Optimized Logistics (Medic)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Medic)' in acquired_items }}" title="Optimized Logistics (Medic)" /></td>
|
|
||||||
<td colspan="2"></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ stimpack_hellion_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Hellion)' in acquired_items }}" title="{{ stimpack_hellion_name }}" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
|
|
||||||
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
|
|
||||||
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
|
|
||||||
<td><img src="{{ stimpack_firebat_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Firebat)' in acquired_items }}" title="{{ stimpack_firebat_name }}" /></td>
|
|
||||||
<td><img src="{{ icons['Optimized Logistics (Firebat)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Firebat)' in acquired_items }}" title="Optimized Logistics (Firebat)" /></td>
|
|
||||||
<td colspan="3"></td>
|
|
||||||
<td><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
|
|
||||||
<td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td>
|
<td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td>
|
||||||
<td><img src="{{ icons['Ion Thrusters (Vulture)'] }}" class="{{ 'acquired' if 'Ion Thrusters (Vulture)' in acquired_items }}" title="Ion Thrusters (Vulture)" /></td>
|
|
||||||
<td><img src="{{ icons['Auto Launchers (Vulture)'] }}" class="{{ 'acquired' if 'Auto Launchers (Vulture)' in acquired_items }}" title="Auto Launchers (Vulture)" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Cerberus Mine (Spider Mine)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Spider Mine)' in acquired_items }}" title="Cerberus Mine (Spider Mine)" /></td>
|
|
||||||
<td><img src="{{ icons['High Explosive Munition (Spider Mine)'] }}" class="{{ 'acquired' if 'High Explosive Munition (Spider Mine)' in acquired_items }}" title="High Explosive Munition (Spider Mine)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
|
|
||||||
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
|
|
||||||
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
|
|
||||||
<td><img src="{{ stimpack_marauder_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marauder)' in acquired_items }}" title="{{ stimpack_marauder_name }}" /></td>
|
|
||||||
<td><img src="{{ icons['Laser Targeting System (Marauder)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marauder)' in acquired_items }}" title="Laser Targeting System (Marauder)" /></td>
|
|
||||||
<td><img src="{{ icons['Magrail Munitions (Marauder)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marauder)' in acquired_items }}" title="Magrail Munitions (Marauder)" /></td>
|
|
||||||
<td><img src="{{ icons['Internal Tech Module (Marauder)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Marauder)' in acquired_items }}" title="Internal Tech Module (Marauder)" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
|
|
||||||
<td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td>
|
<td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td>
|
||||||
<td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td>
|
<td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td>
|
||||||
<td><img src="{{ icons['Jump Jets (Goliath)'] }}" class="{{ 'acquired' if 'Jump Jets (Goliath)' in acquired_items }}" title="Jump Jets (Goliath)" /></td>
|
|
||||||
<td><img src="{{ icons['Optimized Logistics (Goliath)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Goliath)' in acquired_items }}" title="Optimized Logistics (Goliath)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
|
|
||||||
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
|
|
||||||
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
|
|
||||||
<td><img src="{{ stimpack_reaper_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Reaper)' in acquired_items }}" title="{{ stimpack_reaper_name }}" /></td>
|
|
||||||
<td><img src="{{ icons['Laser Targeting System (Reaper)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Reaper)' in acquired_items }}" title="Laser Targeting System (Reaper)" /></td>
|
|
||||||
<td><img src="{{ icons['Advanced Cloaking Field (Reaper)'] }}" class="{{ 'acquired' if 'Advanced Cloaking Field (Reaper)' in acquired_items }}" title="Advanced Cloaking Field (Reaper)" /></td>
|
|
||||||
<td><img src="{{ icons['Spider Mines (Reaper)'] }}" class="{{ 'acquired' if 'Spider Mines (Reaper)' in acquired_items }}" title="Spider Mines (Reaper)" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
|
|
||||||
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
|
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
|
||||||
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
|
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
|
||||||
<td><img src="{{ icons['Hyperfluxor (Diamondback)'] }}" class="{{ 'acquired' if 'Hyperfluxor (Diamondback)' in acquired_items }}" title="Hyperfluxor (Diamondback)" /></td>
|
|
||||||
<td><img src="{{ icons['Burst Capacitors (Diamondback)'] }}" class="{{ 'acquired' if 'Burst Capacitors (Diamondback)' in acquired_items }}" title="Burst Capacitors (Diamondback)" /></td>
|
|
||||||
<td><img src="{{ icons['Optimized Logistics (Diamondback)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Diamondback)' in acquired_items }}" title="Optimized Logistics (Diamondback)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Combat Drugs (Reaper)'] }}" class="{{ 'acquired' if 'Combat Drugs (Reaper)' in acquired_items }}" title="Combat Drugs (Reaper)" /></td>
|
|
||||||
<td colspan="6"></td>
|
|
||||||
<td><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
|
|
||||||
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
|
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
|
||||||
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
|
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
|
||||||
<td><img src="{{ icons['Jump Jets (Siege Tank)'] }}" class="{{ 'acquired' if 'Jump Jets (Siege Tank)' in acquired_items }}" title="Jump Jets (Siege Tank)" /></td>
|
|
||||||
<td><img src="{{ icons['Spider Mines (Siege Tank)'] }}" class="{{ 'acquired' if 'Spider Mines (Siege Tank)' in acquired_items }}" title="Spider Mines (Siege Tank)" /></td>
|
|
||||||
<td><img src="{{ icons['Smart Servos (Siege Tank)'] }}" class="{{ 'acquired' if 'Smart Servos (Siege Tank)' in acquired_items }}" title="Smart Servos (Siege Tank)" /></td>
|
|
||||||
<td><img src="{{ icons['Graduating Range (Siege Tank)'] }}" class="{{ 'acquired' if 'Graduating Range (Siege Tank)' in acquired_items }}" title="Graduating Range (Siege Tank)" /></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
|
<td colspan="10" class="title">
|
||||||
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
|
|
||||||
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
|
|
||||||
<td><img src="{{ icons['EMP Rounds (Ghost)'] }}" class="{{ 'acquired' if 'EMP Rounds (Ghost)' in acquired_items }}" title="EMP Rounds (Ghost)" /></td>
|
|
||||||
<td><img src="{{ icons['Lockdown (Ghost)'] }}" class="{{ 'acquired' if 'Lockdown (Ghost)' in acquired_items }}" title="Lockdown (Ghost)" /></td>
|
|
||||||
<td colspan="3"></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Laser Targeting System (Siege Tank)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Siege Tank)' in acquired_items }}" title="Laser Targeting System (Siege Tank)" /></td>
|
|
||||||
<td><img src="{{ icons['Advanced Siege Tech (Siege Tank)'] }}" class="{{ 'acquired' if 'Advanced Siege Tech (Siege Tank)' in acquired_items }}" title="Advanced Siege Tech (Siege Tank)" /></td>
|
|
||||||
<td><img src="{{ icons['Internal Tech Module (Siege Tank)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Siege Tank)' in acquired_items }}" title="Internal Tech Module (Siege Tank)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
|
|
||||||
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
|
|
||||||
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
|
|
||||||
<td><img src="{{ icons['Impaler Rounds (Spectre)'] }}" class="{{ 'acquired' if 'Impaler Rounds (Spectre)' in acquired_items }}" title="Impaler Rounds (Spectre)" /></td>
|
|
||||||
<td colspan="4"></td>
|
|
||||||
<td><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
|
|
||||||
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
|
|
||||||
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
|
|
||||||
<td><img src="{{ high_impact_payload_thor_url }}" class="{{ 'acquired' if 'Progressive High Impact Payload (Thor)' in acquired_items }}" title="{{ high_impact_payload_thor_name }}" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="8"></td>
|
|
||||||
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
|
|
||||||
<td><img src="{{ icons['Optimized Logistics (Predator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Predator)' in acquired_items }}" title="Optimized Logistics (Predator)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="8"></td>
|
|
||||||
<td><img src="{{ icons['Widow Mine'] }}" class="{{ 'acquired' if 'Widow Mine' in acquired_items }}" title="Widow Mine" /></td>
|
|
||||||
<td><img src="{{ icons['Drilling Claws (Widow Mine)'] }}" class="{{ 'acquired' if 'Drilling Claws (Widow Mine)' in acquired_items }}" title="Drilling Claws (Widow Mine)" /></td>
|
|
||||||
<td><img src="{{ icons['Concealment (Widow Mine)'] }}" class="{{ 'acquired' if 'Concealment (Widow Mine)' in acquired_items }}" title="Concealment (Widow Mine)" /></td>
|
|
||||||
<td><img src="{{ icons['Black Market Launchers (Widow Mine)'] }}" class="{{ 'acquired' if 'Black Market Launchers (Widow Mine)' in acquired_items }}" title="Black Market Launchers (Widow Mine)" /></td>
|
|
||||||
<td><img src="{{ icons['Executioner Missiles (Widow Mine)'] }}" class="{{ 'acquired' if 'Executioner Missiles (Widow Mine)' in acquired_items }}" title="Executioner Missiles (Widow Mine)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="8"></td>
|
|
||||||
<td><img src="{{ icons['Cyclone'] }}" class="{{ 'acquired' if 'Cyclone' in acquired_items }}" title="Cyclone" /></td>
|
|
||||||
<td><img src="{{ icons['Mag-Field Accelerators (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Accelerators (Cyclone)' in acquired_items }}" title="Mag-Field Accelerators (Cyclone)" /></td>
|
|
||||||
<td><img src="{{ icons['Mag-Field Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Launchers (Cyclone)' in acquired_items }}" title="Mag-Field Launchers (Cyclone)" /></td>
|
|
||||||
<td><img src="{{ icons['Targeting Optics (Cyclone)'] }}" class="{{ 'acquired' if 'Targeting Optics (Cyclone)' in acquired_items }}" title="Targeting Optics (Cyclone)" /></td>
|
|
||||||
<td><img src="{{ icons['Rapid Fire Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Rapid Fire Launchers (Cyclone)' in acquired_items }}" title="Rapid Fire Launchers (Cyclone)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="15" class="title">
|
|
||||||
Starships
|
Starships
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td>
|
<td colspan="2"><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Wraith'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Wraith" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Viking'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Viking" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Battlecruiser'] }}" class="{{ 'acquired' if 'Battlecruiser' in acquired_items }}" title="Battlecruiser" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
<td><img src="{{ icons['Rapid Deployment Tube (Medivac)'] }}" class="{{ 'acquired' if 'Rapid Deployment Tube (Medivac)' in acquired_items }}" title="Rapid Deployment Tube (Medivac)" /></td>
|
<td><img src="{{ icons['Rapid Deployment Tube (Medivac)'] }}" class="{{ 'acquired' if 'Rapid Deployment Tube (Medivac)' in acquired_items }}" title="Rapid Deployment Tube (Medivac)" /></td>
|
||||||
<td><img src="{{ icons['Advanced Healing AI (Medivac)'] }}" class="{{ 'acquired' if 'Advanced Healing AI (Medivac)' in acquired_items }}" title="Advanced Healing AI (Medivac)" /></td>
|
<td><img src="{{ icons['Advanced Healing AI (Medivac)'] }}" class="{{ 'acquired' if 'Advanced Healing AI (Medivac)' in acquired_items }}" title="Advanced Healing AI (Medivac)" /></td>
|
||||||
<td><img src="{{ icons['Expanded Hull (Medivac)'] }}" class="{{ 'acquired' if 'Expanded Hull (Medivac)' in acquired_items }}" title="Expanded Hull (Medivac)" /></td>
|
|
||||||
<td><img src="{{ icons['Afterburners (Medivac)'] }}" class="{{ 'acquired' if 'Afterburners (Medivac)' in acquired_items }}" title="Afterburners (Medivac)" /></td>
|
|
||||||
<td colspan="3"></td>
|
|
||||||
<td><img src="{{ icons['Raven'] }}" class="{{ 'acquired' if 'Raven' in acquired_items }}" title="Raven" /></td>
|
|
||||||
<td><img src="{{ icons['Bio Mechanical Repair Drone (Raven)'] }}" class="{{ 'acquired' if 'Bio Mechanical Repair Drone (Raven)' in acquired_items }}" title="Bio Mechanical Repair Drone (Raven)" /></td>
|
|
||||||
<td><img src="{{ icons['Spider Mines (Raven)'] }}" class="{{ 'acquired' if 'Spider Mines (Raven)' in acquired_items }}" title="Spider Mines (Raven)" /></td>
|
|
||||||
<td><img src="{{ icons['Railgun Turret (Raven)'] }}" class="{{ 'acquired' if 'Railgun Turret (Raven)' in acquired_items }}" title="Railgun Turret (Raven)" /></td>
|
|
||||||
<td><img src="{{ icons['Hunter-Seeker Weapon (Raven)'] }}" class="{{ 'acquired' if 'Hunter-Seeker Weapon (Raven)' in acquired_items }}" title="Hunter-Seeker Weapon (Raven)" /></td>
|
|
||||||
<td><img src="{{ icons['Interference Matrix (Raven)'] }}" class="{{ 'acquired' if 'Interference Matrix (Raven)' in acquired_items }}" title="Interference Matrix (Raven)" /></td>
|
|
||||||
<td><img src="{{ icons['Anti-Armor Missile (Raven)'] }}" class="{{ 'acquired' if 'Anti-Armor Missile (Raven)' in acquired_items }}" title="Anti-Armor Missile (Raven)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Wraith'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Wraith" /></td>
|
|
||||||
<td><img src="{{ icons['Tomahawk Power Cells (Wraith)'] }}" class="{{ 'acquired' if 'Tomahawk Power Cells (Wraith)' in acquired_items }}" title="Tomahawk Power Cells (Wraith)" /></td>
|
<td><img src="{{ icons['Tomahawk Power Cells (Wraith)'] }}" class="{{ 'acquired' if 'Tomahawk Power Cells (Wraith)' in acquired_items }}" title="Tomahawk Power Cells (Wraith)" /></td>
|
||||||
<td><img src="{{ icons['Displacement Field (Wraith)'] }}" class="{{ 'acquired' if 'Displacement Field (Wraith)' in acquired_items }}" title="Displacement Field (Wraith)" /></td>
|
<td><img src="{{ icons['Displacement Field (Wraith)'] }}" class="{{ 'acquired' if 'Displacement Field (Wraith)' in acquired_items }}" title="Displacement Field (Wraith)" /></td>
|
||||||
<td><img src="{{ icons['Advanced Laser Technology (Wraith)'] }}" class="{{ 'acquired' if 'Advanced Laser Technology (Wraith)' in acquired_items }}" title="Advanced Laser Technology (Wraith)" /></td>
|
|
||||||
<td colspan="4"></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Internal Tech Module (Raven)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Raven)' in acquired_items }}" title="Internal Tech Module (Raven)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Viking'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Viking" /></td>
|
|
||||||
<td><img src="{{ icons['Ripwave Missiles (Viking)'] }}" class="{{ 'acquired' if 'Ripwave Missiles (Viking)' in acquired_items }}" title="Ripwave Missiles (Viking)" /></td>
|
<td><img src="{{ icons['Ripwave Missiles (Viking)'] }}" class="{{ 'acquired' if 'Ripwave Missiles (Viking)' in acquired_items }}" title="Ripwave Missiles (Viking)" /></td>
|
||||||
<td><img src="{{ icons['Phobos-Class Weapons System (Viking)'] }}" class="{{ 'acquired' if 'Phobos-Class Weapons System (Viking)' in acquired_items }}" title="Phobos-Class Weapons System (Viking)" /></td>
|
<td><img src="{{ icons['Phobos-Class Weapons System (Viking)'] }}" class="{{ 'acquired' if 'Phobos-Class Weapons System (Viking)' in acquired_items }}" title="Phobos-Class Weapons System (Viking)" /></td>
|
||||||
<td><img src="{{ icons['Smart Servos (Viking)'] }}" class="{{ 'acquired' if 'Smart Servos (Viking)' in acquired_items }}" title="Smart Servos (Viking)" /></td>
|
<td><img src="{{ icons['Cross-Spectrum Dampeners (Banshee)'] }}" class="{{ 'acquired' if 'Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="Cross-Spectrum Dampeners (Banshee)" /></td>
|
||||||
<td><img src="{{ icons['Magrail Munitions (Viking)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Viking)' in acquired_items }}" title="Magrail Munitions (Viking)" /></td>
|
|
||||||
<td colspan="3"></td>
|
|
||||||
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
|
|
||||||
<td><img src="{{ icons['EMP Shockwave (Science Vessel)'] }}" class="{{ 'acquired' if 'EMP Shockwave (Science Vessel)' in acquired_items }}" title="EMP Shockwave (Science Vessel)" /></td>
|
|
||||||
<td><img src="{{ icons['Defensive Matrix (Science Vessel)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Science Vessel)' in acquired_items }}" title="Defensive Matrix (Science Vessel)" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
|
|
||||||
<td><img src="{{ crossspectrum_dampeners_banshee_url }}" class="{{ 'acquired' if 'Progressive Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="{{ crossspectrum_dampeners_banshee_name }}" /></td>
|
|
||||||
<td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></td>
|
<td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></td>
|
||||||
<td><img src="{{ icons['Hyperflight Rotors (Banshee)'] }}" class="{{ 'acquired' if 'Hyperflight Rotors (Banshee)' in acquired_items }}" title="Hyperflight Rotors (Banshee)" /></td>
|
|
||||||
<td><img src="{{ icons['Laser Targeting System (Banshee)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Banshee)' in acquired_items }}" title="Laser Targeting System (Banshee)" /></td>
|
|
||||||
<td><img src="{{ icons['Internal Tech Module (Banshee)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Banshee)' in acquired_items }}" title="Internal Tech Module (Banshee)" /></td>
|
|
||||||
<td colspan="2"></td>
|
|
||||||
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Battlecruiser'] }}" class="{{ 'acquired' if 'Battlecruiser' in acquired_items }}" title="Battlecruiser" /></td>
|
|
||||||
<td><img src="{{ icons['Missile Pods (Battlecruiser)'] }}" class="{{ 'acquired' if 'Missile Pods (Battlecruiser)' in acquired_items }}" title="Missile Pods (Battlecruiser)" /></td>
|
<td><img src="{{ icons['Missile Pods (Battlecruiser)'] }}" class="{{ 'acquired' if 'Missile Pods (Battlecruiser)' in acquired_items }}" title="Missile Pods (Battlecruiser)" /></td>
|
||||||
<td><img src="{{ icons['Defensive Matrix (Battlecruiser)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Battlecruiser)' in acquired_items }}" title="Defensive Matrix (Battlecruiser)" /></td>
|
<td><img src="{{ icons['Defensive Matrix (Battlecruiser)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Battlecruiser)' in acquired_items }}" title="Defensive Matrix (Battlecruiser)" /></td>
|
||||||
<td><img src="{{ icons['Tactical Jump (Battlecruiser)'] }}" class="{{ 'acquired' if 'Tactical Jump (Battlecruiser)' in acquired_items }}" title="Tactical Jump (Battlecruiser)" /></td>
|
|
||||||
<td><img src="{{ icons['Cloak (Battlecruiser)'] }}" class="{{ 'acquired' if 'Cloak (Battlecruiser)' in acquired_items }}" title="Cloak (Battlecruiser)" /></td>
|
|
||||||
<td><img src="{{ icons['ATX Laser Battery (Battlecruiser)'] }}" class="{{ 'acquired' if 'ATX Laser Battery (Battlecruiser)' in acquired_items }}" title="ATX Laser Battery (Battlecruiser)" /></td>
|
|
||||||
<td><img src="{{ icons['Optimized Logistics (Battlecruiser)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Battlecruiser)' in acquired_items }}" title="Optimized Logistics (Battlecruiser)" /></td>
|
|
||||||
<td></td>
|
|
||||||
<td><img src="{{ icons['Liberator'] }}" class="{{ 'acquired' if 'Liberator' in acquired_items }}" title="Liberator" /></td>
|
|
||||||
<td><img src="{{ icons['Advanced Ballistics (Liberator)'] }}" class="{{ 'acquired' if 'Advanced Ballistics (Liberator)' in acquired_items }}" title="Advanced Ballistics (Liberator)" /></td>
|
|
||||||
<td><img src="{{ icons['Raid Artillery (Liberator)'] }}" class="{{ 'acquired' if 'Raid Artillery (Liberator)' in acquired_items }}" title="Raid Artillery (Liberator)" /></td>
|
|
||||||
<td><img src="{{ icons['Cloak (Liberator)'] }}" class="{{ 'acquired' if 'Cloak (Liberator)' in acquired_items }}" title="Cloak (Liberator)" /></td>
|
|
||||||
<td><img src="{{ icons['Laser Targeting System (Liberator)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Liberator)' in acquired_items }}" title="Laser Targeting System (Liberator)" /></td>
|
|
||||||
<td><img src="{{ icons['Optimized Logistics (Liberator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Liberator)' in acquired_items }}" title="Optimized Logistics (Liberator)" /></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td colspan="10" class="title">
|
||||||
<td><img src="{{ icons['Internal Tech Module (Battlecruiser)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Battlecruiser)' in acquired_items }}" title="Internal Tech Module (Battlecruiser)" /></td>
|
Dominion
|
||||||
<td colspan="6"></td>
|
</td>
|
||||||
<td><img src="{{ icons['Valkyrie'] }}" class="{{ 'acquired' if 'Valkyrie' in acquired_items }}" title="Valkyrie" /></td>
|
|
||||||
<td><img src="{{ icons['Enhanced Cluster Launchers (Valkyrie)'] }}" class="{{ 'acquired' if 'Enhanced Cluster Launchers (Valkyrie)' in acquired_items }}" title="Enhanced Cluster Launchers (Valkyrie)" /></td>
|
|
||||||
<td><img src="{{ icons['Shaped Hull (Valkyrie)'] }}" class="{{ 'acquired' if 'Shaped Hull (Valkyrie)' in acquired_items }}" title="Shaped Hull (Valkyrie)" /></td>
|
|
||||||
<td><img src="{{ icons['Burst Lasers (Valkyrie)'] }}" class="{{ 'acquired' if 'Burst Lasers (Valkyrie)' in acquired_items }}" title="Burst Lasers (Valkyrie)" /></td>
|
|
||||||
<td><img src="{{ icons['Afterburners (Valkyrie)'] }}" class="{{ 'acquired' if 'Afterburners (Valkyrie)' in acquired_items }}" title="Afterburners (Valkyrie)" /></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="15" class="title">
|
<td colspan="2"><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
|
||||||
|
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
|
||||||
|
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
|
||||||
|
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
|
||||||
|
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
|
||||||
|
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
Mercenaries
|
Mercenaries
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -311,18 +165,36 @@
|
|||||||
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
|
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="15" class="title">
|
<td colspan="10" class="title">
|
||||||
General Upgrades
|
Lab Upgrades
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
|
<td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></td>
|
||||||
|
<td><img src="{{ icons['Vanadium Plating'] }}" class="{{ 'acquired' if 'Vanadium Plating' in acquired_items }}" title="Vanadium Plating" /></td>
|
||||||
|
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
|
||||||
|
<td><img src="{{ icons['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td>
|
||||||
|
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
|
||||||
|
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
|
||||||
|
<td><img src="{{ icons['Raven'] }}" class="{{ 'acquired' if 'Raven' in acquired_items }}" title="Raven" /></td>
|
||||||
|
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
|
||||||
|
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
|
||||||
<td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td>
|
<td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td>
|
||||||
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
|
|
||||||
<td><img src="{{ regenerative_biosteel_url }}" class="{{ 'acquired' if 'Progressive Regenerative Bio-Steel' in acquired_items }}" title="Progressive Regenerative Bio-Steel{% if regenerative_biosteel_level > 0 %} (Level {{ regenerative_biosteel_level }}){% endif %}" /></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="15" class="title">
|
<td><img src="{{ icons['Shrike Turret'] }}" class="{{ 'acquired' if 'Shrike Turret' in acquired_items }}" title="Shrike Turret" /></td>
|
||||||
|
<td><img src="{{ icons['Fortified Bunker'] }}" class="{{ 'acquired' if 'Fortified Bunker' in acquired_items }}" title="Fortified Bunker" /></td>
|
||||||
|
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
|
||||||
|
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
|
||||||
|
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
|
||||||
|
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
|
||||||
|
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
|
||||||
|
<td><img src="{{ icons['Regenerative Bio-Steel'] }}" class="{{ 'acquired' if 'Regenerative Bio-Steel' in acquired_items }}" title="Regenerative Bio-Steel" /></td>
|
||||||
|
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
|
||||||
|
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
Protoss Units
|
Protoss Units
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -4,27 +4,16 @@
|
|||||||
<title>Supported Games</title>
|
<title>Supported Games</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" />
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/supportedGames.js") }}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/oceanHeader.html' %}
|
{% include 'header/oceanHeader.html' %}
|
||||||
<div id="games" class="markdown">
|
<div id="games" class="markdown">
|
||||||
<h1>Currently Supported Games</h1>
|
<h1>Currently Supported Games</h1>
|
||||||
<div>
|
|
||||||
<label for="game-search">Search for your game below!</label><br />
|
|
||||||
<div id="page-controls">
|
|
||||||
<input id="game-search" placeholder="Search by title..." autofocus />
|
|
||||||
<button id="expand-all">Expand All</button>
|
|
||||||
<button id="collapse-all">Collapse All</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% for game_name in worlds | title_sorted %}
|
{% for game_name in worlds | title_sorted %}
|
||||||
{% set world = worlds[game_name] %}
|
{% set world = worlds[game_name] %}
|
||||||
<h2 class="collapse-toggle" data-game="{{ game_name }}">
|
<h2>{{ game_name }}</h2>
|
||||||
<span id="{{ game_name }}-arrow" class="collapse-arrow">▶</span> {{ game_name }}
|
<p>
|
||||||
</h2>
|
|
||||||
<p id="{{ game_name }}" class="collapsed">
|
|
||||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||||
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
||||||
{% if world.web.tutorials %}
|
{% if world.web.tutorials %}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import typing
|
import typing
|
||||||
|
import pkgutil
|
||||||
from typing import Counter, Optional, Dict, Any, Tuple, List
|
from typing import Counter, Optional, Dict, Any, Tuple, List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from jinja2 import pass_context, runtime
|
from jinja2 import pass_context, runtime, Template
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
from MultiServer import Context, get_saving_second
|
from MultiServer import Context, get_saving_second
|
||||||
from NetUtils import ClientStatus, SlotType, NetworkSlot
|
from NetUtils import SlotType, NetworkSlot
|
||||||
from Utils import restricted_loads
|
from Utils import restricted_loads
|
||||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games
|
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, AutoWorldRegister
|
||||||
from worlds.alttp import Items
|
from worlds.alttp import Items
|
||||||
from . import app, cache
|
from . import app, cache
|
||||||
from .models import GameDataPackage, Room
|
from .models import GameDataPackage, Room
|
||||||
@@ -990,7 +991,6 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
SC2WOL_LOC_ID_OFFSET = 1000
|
SC2WOL_LOC_ID_OFFSET = 1000
|
||||||
SC2WOL_ITEM_ID_OFFSET = 1000
|
SC2WOL_ITEM_ID_OFFSET = 1000
|
||||||
|
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
|
"Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
|
||||||
"Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
|
"Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
|
||||||
@@ -1035,36 +1035,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
"Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
|
"Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
|
||||||
|
|
||||||
"Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
|
"Stimpack (Marine)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
|
||||||
"Super Stimpack (Marine)": "/static/static/icons/sc2/superstimpack.png",
|
|
||||||
"Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png",
|
"Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png",
|
||||||
"Laser Targeting System (Marine)": "/static/static/icons/sc2/lasertargetingsystem.png",
|
|
||||||
"Magrail Munitions (Marine)": "/static/static/icons/sc2/magrailmunitions.png",
|
|
||||||
"Optimized Logistics (Marine)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png",
|
"Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png",
|
||||||
"Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png",
|
"Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png",
|
||||||
"Restoration (Medic)": "/static/static/icons/sc2/restoration.png",
|
|
||||||
"Optical Flare (Medic)": "/static/static/icons/sc2/opticalflare.png",
|
|
||||||
"Optimized Logistics (Medic)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png",
|
"Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png",
|
||||||
"Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png",
|
"Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png",
|
||||||
"Stimpack (Firebat)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
|
|
||||||
"Super Stimpack (Firebat)": "/static/static/icons/sc2/superstimpack.png",
|
|
||||||
"Optimized Logistics (Firebat)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png",
|
"Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png",
|
||||||
"Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png",
|
"Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.png",
|
||||||
"Stimpack (Marauder)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
|
|
||||||
"Super Stimpack (Marauder)": "/static/static/icons/sc2/superstimpack.png",
|
|
||||||
"Laser Targeting System (Marauder)": "/static/static/icons/sc2/lasertargetingsystem.png",
|
|
||||||
"Magrail Munitions (Marauder)": "/static/static/icons/sc2/magrailmunitions.png",
|
|
||||||
"Internal Tech Module (Marauder)": "/static/static/icons/sc2/internalizedtechmodule.png",
|
|
||||||
"U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png",
|
"U-238 Rounds (Reaper)": "https://0rganics.org/archipelago/sc2wol/U-238Rounds.png",
|
||||||
"G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png",
|
"G-4 Clusterbomb (Reaper)": "https://0rganics.org/archipelago/sc2wol/G-4Clusterbomb.png",
|
||||||
"Stimpack (Reaper)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
|
|
||||||
"Super Stimpack (Reaper)": "/static/static/icons/sc2/superstimpack.png",
|
|
||||||
"Laser Targeting System (Reaper)": "/static/static/icons/sc2/lasertargetingsystem.png",
|
|
||||||
"Advanced Cloaking Field (Reaper)": "/static/static/icons/sc2/terran-cloak-color.png",
|
|
||||||
"Spider Mines (Reaper)": "/static/static/icons/sc2/spidermine.png",
|
|
||||||
"Combat Drugs (Reaper)": "/static/static/icons/sc2/reapercombatdrugs.png",
|
|
||||||
|
|
||||||
"Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
|
"Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
|
||||||
"Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg",
|
"Vulture": "https://static.wikia.nocookie.net/starcraft/images/d/da/Vulture_WoL.jpg",
|
||||||
@@ -1074,35 +1053,14 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
|
|
||||||
"Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
|
"Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
|
||||||
"Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png",
|
"Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png",
|
||||||
"Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png",
|
"Cerberus Mine (Vulture)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
|
||||||
"Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png",
|
|
||||||
"Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png",
|
|
||||||
"Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
|
|
||||||
"Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png",
|
|
||||||
"Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
|
|
||||||
"High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png",
|
|
||||||
"Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png",
|
"Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png",
|
||||||
"Ion Thrusters (Vulture)": "/static/static/icons/sc2/emergencythrusters.png",
|
|
||||||
"Auto Launchers (Vulture)": "/static/static/icons/sc2/jotunboosters.png",
|
|
||||||
"Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png",
|
"Multi-Lock Weapons System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Multi-LockWeaponsSystem.png",
|
||||||
"Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png",
|
"Ares-Class Targeting System (Goliath)": "https://0rganics.org/archipelago/sc2wol/Ares-ClassTargetingSystem.png",
|
||||||
"Jump Jets (Goliath)": "/static/static/icons/sc2/jumpjets.png",
|
|
||||||
"Optimized Logistics (Goliath)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png",
|
"Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png",
|
||||||
"Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
|
"Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
|
||||||
"Hyperfluxor (Diamondback)": "/static/static/icons/sc2/hyperfluxor.png",
|
|
||||||
"Burst Capacitors (Diamondback)": "/static/static/icons/sc2/burstcapacitors.png",
|
|
||||||
"Optimized Logistics (Diamondback)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png",
|
"Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png",
|
||||||
"Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png",
|
"Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png",
|
||||||
"Jump Jets (Siege Tank)": "/static/static/icons/sc2/jumpjets.png",
|
|
||||||
"Spider Mines (Siege Tank)": "/static/static/icons/sc2/siegetank-spidermines.png",
|
|
||||||
"Smart Servos (Siege Tank)": "/static/static/icons/sc2/transformationservos.png",
|
|
||||||
"Graduating Range (Siege Tank)": "/static/static/icons/sc2/siegetankrange.png",
|
|
||||||
"Laser Targeting System (Siege Tank)": "/static/static/icons/sc2/lasertargetingsystem.png",
|
|
||||||
"Advanced Siege Tech (Siege Tank)": "/static/static/icons/sc2/improvedsiegemode.png",
|
|
||||||
"Internal Tech Module (Siege Tank)": "/static/static/icons/sc2/internalizedtechmodule.png",
|
|
||||||
|
|
||||||
"Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg",
|
"Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg",
|
||||||
"Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg",
|
"Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg",
|
||||||
@@ -1112,77 +1070,25 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
|
|
||||||
"Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png",
|
"Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png",
|
||||||
"Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png",
|
"Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png",
|
||||||
"Expanded Hull (Medivac)": "/static/static/icons/sc2/neosteelfortifiedarmor.png",
|
|
||||||
"Afterburners (Medivac)": "/static/static/icons/sc2/medivacemergencythrusters.png",
|
|
||||||
"Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png",
|
"Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png",
|
||||||
"Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png",
|
"Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png",
|
||||||
"Advanced Laser Technology (Wraith)": "/static/static/icons/sc2/improvedburstlaser.png",
|
|
||||||
"Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png",
|
"Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png",
|
||||||
"Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png",
|
"Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png",
|
||||||
"Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png",
|
"Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
|
||||||
"Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png",
|
|
||||||
"Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png",
|
|
||||||
"Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
|
|
||||||
"Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png",
|
"Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png",
|
||||||
"Hyperflight Rotors (Banshee)": "/static/static/icons/sc2/hyperflightrotors.png",
|
|
||||||
"Laser Targeting System (Banshee)": "/static/static/icons/sc2/lasertargetingsystem.png",
|
|
||||||
"Internal Tech Module (Banshee)": "/static/static/icons/sc2/internalizedtechmodule.png",
|
|
||||||
"Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png",
|
"Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png",
|
||||||
"Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
|
"Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
|
||||||
"Tactical Jump (Battlecruiser)": "/static/static/icons/sc2/warpjump.png",
|
|
||||||
"Cloak (Battlecruiser)": "/static/static/icons/sc2/terran-cloak-color.png",
|
|
||||||
"ATX Laser Battery (Battlecruiser)": "/static/static/icons/sc2/specialordance.png",
|
|
||||||
"Optimized Logistics (Battlecruiser)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Internal Tech Module (Battlecruiser)": "/static/static/icons/sc2/internalizedtechmodule.png",
|
|
||||||
|
|
||||||
"Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg",
|
"Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg",
|
||||||
"Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg",
|
"Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg",
|
||||||
"Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg",
|
"Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg",
|
||||||
|
|
||||||
"Widow Mine": "/static/static/icons/sc2/widowmine.png",
|
|
||||||
"Cyclone": "/static/static/icons/sc2/cyclone.png",
|
|
||||||
"Liberator": "/static/static/icons/sc2/liberator.png",
|
|
||||||
"Valkyrie": "/static/static/icons/sc2/valkyrie.png",
|
|
||||||
|
|
||||||
"Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png",
|
"Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png",
|
||||||
"Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png",
|
"Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png",
|
||||||
"EMP Rounds (Ghost)": "/static/static/icons/sc2/terran-emp-color.png",
|
|
||||||
"Lockdown (Ghost)": "/static/static/icons/sc2/lockdown.png",
|
|
||||||
"Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png",
|
"Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png",
|
||||||
"Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png",
|
"Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png",
|
||||||
"Impaler Rounds (Spectre)": "/static/static/icons/sc2/impalerrounds.png",
|
|
||||||
"330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png",
|
"330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png",
|
||||||
"Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png",
|
"Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png",
|
||||||
"High Impact Payload (Thor)": "/static/static/icons/sc2/thorsiegemode.png",
|
|
||||||
"Smart Servos (Thor)": "/static/static/icons/sc2/transformationservos.png",
|
|
||||||
|
|
||||||
"Optimized Logistics (Predator)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Drilling Claws (Widow Mine)": "/static/static/icons/sc2/drillingclaws.png",
|
|
||||||
"Concealment (Widow Mine)": "/static/static/icons/sc2/widowminehidden.png",
|
|
||||||
"Black Market Launchers (Widow Mine)": "/static/static/icons/sc2/widowmine-attackrange.png",
|
|
||||||
"Executioner Missiles (Widow Mine)": "/static/static/icons/sc2/widowmine-deathblossom.png",
|
|
||||||
"Mag-Field Accelerators (Cyclone)": "/static/static/icons/sc2/magfieldaccelerator.png",
|
|
||||||
"Mag-Field Launchers (Cyclone)": "/static/static/icons/sc2/cyclonerangeupgrade.png",
|
|
||||||
"Targeting Optics (Cyclone)": "/static/static/icons/sc2/targetingoptics.png",
|
|
||||||
"Rapid Fire Launchers (Cyclone)": "/static/static/icons/sc2/ripwavemissiles.png",
|
|
||||||
"Bio Mechanical Repair Drone (Raven)": "/static/static/icons/sc2/biomechanicaldrone.png",
|
|
||||||
"Spider Mines (Raven)": "/static/static/icons/sc2/siegetank-spidermines.png",
|
|
||||||
"Railgun Turret (Raven)": "/static/static/icons/sc2/autoturretblackops.png",
|
|
||||||
"Hunter-Seeker Weapon (Raven)": "/static/static/icons/sc2/specialordance.png",
|
|
||||||
"Interference Matrix (Raven)": "/static/static/icons/sc2/interferencematrix.png",
|
|
||||||
"Anti-Armor Missile (Raven)": "/static/static/icons/sc2/shreddermissile.png",
|
|
||||||
"Internal Tech Module (Raven)": "/static/static/icons/sc2/internalizedtechmodule.png",
|
|
||||||
"EMP Shockwave (Science Vessel)": "/static/static/icons/sc2/staticempblast.png",
|
|
||||||
"Defensive Matrix (Science Vessel)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
|
|
||||||
"Advanced Ballistics (Liberator)": "/static/static/icons/sc2/advanceballistics.png",
|
|
||||||
"Raid Artillery (Liberator)": "/static/static/icons/sc2/terrandefendermodestructureattack.png",
|
|
||||||
"Cloak (Liberator)": "/static/static/icons/sc2/terran-cloak-color.png",
|
|
||||||
"Laser Targeting System (Liberator)": "/static/static/icons/sc2/lasertargetingsystem.png",
|
|
||||||
"Optimized Logistics (Liberator)": "/static/static/icons/sc2/optimizedlogistics.png",
|
|
||||||
"Enhanced Cluster Launchers (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png",
|
|
||||||
"Shaped Hull (Valkyrie)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
|
|
||||||
"Burst Lasers (Valkyrie)": "/static/static/icons/sc2/improvedburstlaser.png",
|
|
||||||
"Afterburners (Valkyrie)": "/static/static/icons/sc2/medivacemergencythrusters.png",
|
|
||||||
|
|
||||||
"War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg",
|
"War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg",
|
||||||
"Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg",
|
"Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg",
|
||||||
@@ -1204,15 +1110,14 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
"Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png",
|
"Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png",
|
||||||
"Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png",
|
"Orbital Strike": "https://static.wikia.nocookie.net/starcraft/images/d/df/SC2_Lab_Orb_Strike_Icon.png",
|
||||||
|
|
||||||
"Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
|
"Shrike Turret": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
|
||||||
"Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
|
"Fortified Bunker": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
|
||||||
"Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png",
|
"Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png",
|
||||||
"Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png",
|
"Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png",
|
||||||
"Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png",
|
"Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png",
|
||||||
"Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png",
|
"Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png",
|
||||||
"Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
|
"Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
|
||||||
"Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png",
|
"Regenerative Bio-Steel": "https://static.wikia.nocookie.net/starcraft/images/d/d3/SC2_Lab_BioSteel_Icon.png",
|
||||||
"Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png",
|
|
||||||
"Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png",
|
"Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png",
|
||||||
"Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png",
|
"Psi Disrupter": "https://static.wikia.nocookie.net/starcraft/images/c/cf/SC2_Lab_Psi_Disruptor_Icon.png",
|
||||||
|
|
||||||
@@ -1228,71 +1133,40 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
|
|
||||||
"Nothing": "",
|
"Nothing": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
sc2wol_location_ids = {
|
sc2wol_location_ids = {
|
||||||
"Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200),
|
"Liberation Day": [SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 101, SC2WOL_LOC_ID_OFFSET + 102, SC2WOL_LOC_ID_OFFSET + 103, SC2WOL_LOC_ID_OFFSET + 104, SC2WOL_LOC_ID_OFFSET + 105, SC2WOL_LOC_ID_OFFSET + 106],
|
||||||
"The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300),
|
"The Outlaws": [SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 201],
|
||||||
"Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400),
|
"Zero Hour": [SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 301, SC2WOL_LOC_ID_OFFSET + 302, SC2WOL_LOC_ID_OFFSET + 303],
|
||||||
"Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500),
|
"Evacuation": [SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 401, SC2WOL_LOC_ID_OFFSET + 402, SC2WOL_LOC_ID_OFFSET + 403],
|
||||||
"Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600),
|
"Outbreak": [SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 501, SC2WOL_LOC_ID_OFFSET + 502],
|
||||||
"Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700),
|
"Safe Haven": [SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 601, SC2WOL_LOC_ID_OFFSET + 602, SC2WOL_LOC_ID_OFFSET + 603],
|
||||||
"Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800),
|
"Haven's Fall": [SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 701, SC2WOL_LOC_ID_OFFSET + 702, SC2WOL_LOC_ID_OFFSET + 703],
|
||||||
"Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900),
|
"Smash and Grab": [SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 801, SC2WOL_LOC_ID_OFFSET + 802, SC2WOL_LOC_ID_OFFSET + 803, SC2WOL_LOC_ID_OFFSET + 804],
|
||||||
"The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000),
|
"The Dig": [SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 901, SC2WOL_LOC_ID_OFFSET + 902, SC2WOL_LOC_ID_OFFSET + 903],
|
||||||
"The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100),
|
"The Moebius Factor": [SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1003, SC2WOL_LOC_ID_OFFSET + 1004, SC2WOL_LOC_ID_OFFSET + 1005, SC2WOL_LOC_ID_OFFSET + 1006, SC2WOL_LOC_ID_OFFSET + 1007, SC2WOL_LOC_ID_OFFSET + 1008],
|
||||||
"Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200),
|
"Supernova": [SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1101, SC2WOL_LOC_ID_OFFSET + 1102, SC2WOL_LOC_ID_OFFSET + 1103, SC2WOL_LOC_ID_OFFSET + 1104],
|
||||||
"Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300),
|
"Maw of the Void": [SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1201, SC2WOL_LOC_ID_OFFSET + 1202, SC2WOL_LOC_ID_OFFSET + 1203, SC2WOL_LOC_ID_OFFSET + 1204, SC2WOL_LOC_ID_OFFSET + 1205],
|
||||||
"Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400),
|
"Devil's Playground": [SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1301, SC2WOL_LOC_ID_OFFSET + 1302],
|
||||||
"Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500),
|
"Welcome to the Jungle": [SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1401, SC2WOL_LOC_ID_OFFSET + 1402, SC2WOL_LOC_ID_OFFSET + 1403],
|
||||||
"Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600),
|
"Breakout": [SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1501, SC2WOL_LOC_ID_OFFSET + 1502],
|
||||||
"Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700),
|
"Ghost of a Chance": [SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1601, SC2WOL_LOC_ID_OFFSET + 1602, SC2WOL_LOC_ID_OFFSET + 1603, SC2WOL_LOC_ID_OFFSET + 1604, SC2WOL_LOC_ID_OFFSET + 1605],
|
||||||
"The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800),
|
"The Great Train Robbery": [SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1701, SC2WOL_LOC_ID_OFFSET + 1702, SC2WOL_LOC_ID_OFFSET + 1703],
|
||||||
"Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900),
|
"Cutthroat": [SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1801, SC2WOL_LOC_ID_OFFSET + 1802, SC2WOL_LOC_ID_OFFSET + 1803, SC2WOL_LOC_ID_OFFSET + 1804],
|
||||||
"Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000),
|
"Engine of Destruction": [SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 1901, SC2WOL_LOC_ID_OFFSET + 1902, SC2WOL_LOC_ID_OFFSET + 1903, SC2WOL_LOC_ID_OFFSET + 1904, SC2WOL_LOC_ID_OFFSET + 1905],
|
||||||
"Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100),
|
"Media Blitz": [SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2001, SC2WOL_LOC_ID_OFFSET + 2002, SC2WOL_LOC_ID_OFFSET + 2003, SC2WOL_LOC_ID_OFFSET + 2004],
|
||||||
"Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200),
|
"Piercing the Shroud": [SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2101, SC2WOL_LOC_ID_OFFSET + 2102, SC2WOL_LOC_ID_OFFSET + 2103, SC2WOL_LOC_ID_OFFSET + 2104, SC2WOL_LOC_ID_OFFSET + 2105],
|
||||||
"Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300),
|
"Whispers of Doom": [SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2201, SC2WOL_LOC_ID_OFFSET + 2202, SC2WOL_LOC_ID_OFFSET + 2203],
|
||||||
"A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400),
|
"A Sinister Turn": [SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2301, SC2WOL_LOC_ID_OFFSET + 2302, SC2WOL_LOC_ID_OFFSET + 2303],
|
||||||
"Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500),
|
"Echoes of the Future": [SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2401, SC2WOL_LOC_ID_OFFSET + 2402],
|
||||||
"In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600),
|
"In Utter Darkness": [SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2501, SC2WOL_LOC_ID_OFFSET + 2502],
|
||||||
"Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700),
|
"Gates of Hell": [SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2601],
|
||||||
"Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800),
|
"Belly of the Beast": [SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2701, SC2WOL_LOC_ID_OFFSET + 2702, SC2WOL_LOC_ID_OFFSET + 2703],
|
||||||
"Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900),
|
"Shatter the Sky": [SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2801, SC2WOL_LOC_ID_OFFSET + 2802, SC2WOL_LOC_ID_OFFSET + 2803, SC2WOL_LOC_ID_OFFSET + 2804, SC2WOL_LOC_ID_OFFSET + 2805],
|
||||||
}
|
}
|
||||||
|
|
||||||
display_data = {}
|
display_data = {}
|
||||||
|
|
||||||
# Grouped Items
|
|
||||||
grouped_item_ids = {
|
|
||||||
"Progressive Weapon Upgrade": 107 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Armor Upgrade": 108 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Infantry Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Vehicle Upgrade": 110 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Ship Upgrade": 111 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Weapon/Armor Upgrade": 112 + SC2WOL_ITEM_ID_OFFSET
|
|
||||||
}
|
|
||||||
grouped_item_replacements = {
|
|
||||||
"Progressive Weapon Upgrade": ["Progressive Infantry Weapon", "Progressive Vehicle Weapon", "Progressive Ship Weapon"],
|
|
||||||
"Progressive Armor Upgrade": ["Progressive Infantry Armor", "Progressive Vehicle Armor", "Progressive Ship Armor"],
|
|
||||||
"Progressive Infantry Upgrade": ["Progressive Infantry Weapon", "Progressive Infantry Armor"],
|
|
||||||
"Progressive Vehicle Upgrade": ["Progressive Vehicle Weapon", "Progressive Vehicle Armor"],
|
|
||||||
"Progressive Ship Upgrade": ["Progressive Ship Weapon", "Progressive Ship Armor"]
|
|
||||||
}
|
|
||||||
grouped_item_replacements["Progressive Weapon/Armor Upgrade"] = grouped_item_replacements["Progressive Weapon Upgrade"] + grouped_item_replacements["Progressive Armor Upgrade"]
|
|
||||||
replacement_item_ids = {
|
|
||||||
"Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Infantry Armor": 102 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
}
|
|
||||||
for grouped_item_name, grouped_item_id in grouped_item_ids.items():
|
|
||||||
count: int = inventory[grouped_item_id]
|
|
||||||
if count > 0:
|
|
||||||
for replacement_item in grouped_item_replacements[grouped_item_name]:
|
|
||||||
replacement_id: int = replacement_item_ids[replacement_item]
|
|
||||||
inventory[replacement_id] = count
|
|
||||||
|
|
||||||
# Determine display for progressive items
|
# Determine display for progressive items
|
||||||
progressive_items = {
|
progressive_items = {
|
||||||
"Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
|
"Progressive Infantry Weapon": 100 + SC2WOL_ITEM_ID_OFFSET,
|
||||||
@@ -1300,15 +1174,7 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
"Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
|
"Progressive Vehicle Weapon": 103 + SC2WOL_ITEM_ID_OFFSET,
|
||||||
"Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
|
"Progressive Vehicle Armor": 104 + SC2WOL_ITEM_ID_OFFSET,
|
||||||
"Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
|
"Progressive Ship Weapon": 105 + SC2WOL_ITEM_ID_OFFSET,
|
||||||
"Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET,
|
"Progressive Ship Armor": 106 + SC2WOL_ITEM_ID_OFFSET
|
||||||
"Progressive Stimpack (Marine)": 208 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Stimpack (Firebat)": 226 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Stimpack (Marauder)": 228 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Stimpack (Reaper)": 250 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Stimpack (Hellion)": 259 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive High Impact Payload (Thor)": 361 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Cross-Spectrum Dampeners (Banshee)": 316 + SC2WOL_ITEM_ID_OFFSET,
|
|
||||||
"Progressive Regenerative Bio-Steel": 617 + SC2WOL_ITEM_ID_OFFSET
|
|
||||||
}
|
}
|
||||||
progressive_names = {
|
progressive_names = {
|
||||||
"Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"],
|
"Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"],
|
||||||
@@ -1316,27 +1182,14 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
"Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"],
|
"Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"],
|
||||||
"Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"],
|
"Progressive Vehicle Armor": ["Vehicle Armor Level 1", "Vehicle Armor Level 1", "Vehicle Armor Level 2", "Vehicle Armor Level 3"],
|
||||||
"Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"],
|
"Progressive Ship Weapon": ["Ship Weapons Level 1", "Ship Weapons Level 1", "Ship Weapons Level 2", "Ship Weapons Level 3"],
|
||||||
"Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"],
|
"Progressive Ship Armor": ["Ship Armor Level 1", "Ship Armor Level 1", "Ship Armor Level 2", "Ship Armor Level 3"]
|
||||||
"Progressive Stimpack (Marine)": ["Stimpack (Marine)", "Stimpack (Marine)", "Super Stimpack (Marine)"],
|
|
||||||
"Progressive Stimpack (Firebat)": ["Stimpack (Firebat)", "Stimpack (Firebat)", "Super Stimpack (Firebat)"],
|
|
||||||
"Progressive Stimpack (Marauder)": ["Stimpack (Marauder)", "Stimpack (Marauder)", "Super Stimpack (Marauder)"],
|
|
||||||
"Progressive Stimpack (Reaper)": ["Stimpack (Reaper)", "Stimpack (Reaper)", "Super Stimpack (Reaper)"],
|
|
||||||
"Progressive Stimpack (Hellion)": ["Stimpack (Hellion)", "Stimpack (Hellion)", "Super Stimpack (Hellion)"],
|
|
||||||
"Progressive High Impact Payload (Thor)": ["High Impact Payload (Thor)", "High Impact Payload (Thor)", "Smart Servos (Thor)"],
|
|
||||||
"Progressive Cross-Spectrum Dampeners (Banshee)": ["Cross-Spectrum Dampeners (Banshee)", "Cross-Spectrum Dampeners (Banshee)", "Advanced Cross-Spectrum Dampeners (Banshee)"],
|
|
||||||
"Progressive Regenerative Bio-Steel": ["Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 1", "Regenerative Bio-Steel Level 2"]
|
|
||||||
}
|
}
|
||||||
for item_name, item_id in progressive_items.items():
|
for item_name, item_id in progressive_items.items():
|
||||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||||
display_name = progressive_names[item_name][level]
|
display_name = progressive_names[item_name][level]
|
||||||
base_name = (item_name.split(maxsplit=1)[1].lower()
|
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||||
.replace(' ', '_')
|
|
||||||
.replace("-", "")
|
|
||||||
.replace("(", "")
|
|
||||||
.replace(")", ""))
|
|
||||||
display_data[base_name + "_level"] = level
|
display_data[base_name + "_level"] = level
|
||||||
display_data[base_name + "_url"] = icons[display_name]
|
display_data[base_name + "_url"] = icons[display_name]
|
||||||
display_data[base_name + "_name"] = display_name
|
|
||||||
|
|
||||||
# Multi-items
|
# Multi-items
|
||||||
multi_items = {
|
multi_items = {
|
||||||
@@ -1368,12 +1221,12 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||||
|
|
||||||
return render_template("sc2wolTracker.html",
|
return render_template("sc2wolTracker.html",
|
||||||
inventory=inventory, icons=icons,
|
inventory=inventory, icons=icons,
|
||||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||||
id in lookup_any_item_id_to_name},
|
id in lookup_any_item_id_to_name},
|
||||||
player=player, team=team, room=room, player_name=playerName,
|
player=player, team=team, room=room, player_name=playerName,
|
||||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||||
**display_data)
|
**display_data)
|
||||||
|
|
||||||
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
def __renderChecksfinder(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||||
inventory: Counter, team: int, player: int, playerName: str,
|
inventory: Counter, team: int, player: int, playerName: str,
|
||||||
@@ -1479,104 +1332,9 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
|
|||||||
custom_items=custom_items, custom_locations=custom_locations)
|
custom_items=custom_items, custom_locations=custom_locations)
|
||||||
|
|
||||||
|
|
||||||
def get_enabled_multiworld_trackers(room: Room, current: str):
|
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
|
||||||
enabled = [
|
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
|
||||||
{
|
for teamnumber, team_data in data["checks_done"].items()}
|
||||||
"name": "Generic",
|
|
||||||
"endpoint": "get_multiworld_tracker",
|
|
||||||
"current": current == "Generic"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
for game_name, endpoint in multi_trackers.items():
|
|
||||||
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
|
|
||||||
enabled.append({
|
|
||||||
"name": game_name,
|
|
||||||
"endpoint": endpoint.__name__,
|
|
||||||
"current": current == game_name}
|
|
||||||
)
|
|
||||||
return enabled
|
|
||||||
|
|
||||||
|
|
||||||
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
|
||||||
room: Room = Room.get(tracker=tracker)
|
|
||||||
if not room:
|
|
||||||
return None
|
|
||||||
|
|
||||||
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
|
|
||||||
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
|
||||||
get_static_room_data(room)
|
|
||||||
|
|
||||||
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
|
||||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
|
||||||
for teamnumber, team in enumerate(names)}
|
|
||||||
|
|
||||||
percent_total_checks_done = {teamnumber: {playernumber: 0
|
|
||||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
|
||||||
for teamnumber, team in enumerate(names)}
|
|
||||||
|
|
||||||
total_locations = {teamnumber: sum(len(locations[playernumber])
|
|
||||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups)
|
|
||||||
for teamnumber, team in enumerate(names)}
|
|
||||||
|
|
||||||
hints = {team: set() for team in range(len(names))}
|
|
||||||
if room.multisave:
|
|
||||||
multisave = restricted_loads(room.multisave)
|
|
||||||
else:
|
|
||||||
multisave = {}
|
|
||||||
if "hints" in multisave:
|
|
||||||
for (team, slot), slot_hints in multisave["hints"].items():
|
|
||||||
hints[team] |= set(slot_hints)
|
|
||||||
|
|
||||||
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
|
||||||
if player in groups:
|
|
||||||
continue
|
|
||||||
player_locations = locations[player]
|
|
||||||
checks_done[team][player]["Total"] = len(locations_checked)
|
|
||||||
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
|
|
||||||
len(player_locations) * 100) \
|
|
||||||
if player_locations else 100
|
|
||||||
|
|
||||||
activity_timers = {}
|
|
||||||
now = datetime.datetime.utcnow()
|
|
||||||
for (team, player), timestamp in multisave.get("client_activity_timers", []):
|
|
||||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
|
||||||
|
|
||||||
player_names = {}
|
|
||||||
completed_worlds = 0
|
|
||||||
states: typing.Dict[typing.Tuple[int, int], int] = {}
|
|
||||||
for team, names in enumerate(names):
|
|
||||||
for player, name in enumerate(names, 1):
|
|
||||||
player_names[team, player] = name
|
|
||||||
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
|
|
||||||
if states[team, player] == ClientStatus.CLIENT_GOAL and player not in groups:
|
|
||||||
completed_worlds += 1
|
|
||||||
long_player_names = player_names.copy()
|
|
||||||
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
|
||||||
player_names[team, player] = alias
|
|
||||||
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
|
|
||||||
|
|
||||||
video = {}
|
|
||||||
for (team, player), data in multisave.get("video", []):
|
|
||||||
video[team, player] = data
|
|
||||||
|
|
||||||
return dict(
|
|
||||||
player_names=player_names, room=room, checks_done=checks_done,
|
|
||||||
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
|
|
||||||
activity_timers=activity_timers, video=video, hints=hints,
|
|
||||||
long_player_names=long_player_names,
|
|
||||||
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
|
||||||
locations=locations, total_locations=total_locations, games=games, states=states,
|
|
||||||
completed_worlds=completed_worlds,
|
|
||||||
custom_locations=custom_locations, custom_items=custom_items,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_inventory_data(data: typing.Dict[str, typing.Any]) \
|
|
||||||
-> typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]]:
|
|
||||||
inventory: typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]] = {
|
|
||||||
teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
|
|
||||||
for teamnumber, team_data in data["checks_done"].items()
|
|
||||||
}
|
|
||||||
|
|
||||||
groups = data["groups"]
|
groups = data["groups"]
|
||||||
|
|
||||||
@@ -1594,47 +1352,6 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) \
|
|||||||
inventory[team][recipient][item_id] += 1
|
inventory[team][recipient][item_id] += 1
|
||||||
return inventory
|
return inventory
|
||||||
|
|
||||||
|
|
||||||
def _get_named_inventory(inventory: typing.Dict[int, int], custom_items: typing.Dict[int, str] = None) \
|
|
||||||
-> typing.Dict[str, int]:
|
|
||||||
"""slow"""
|
|
||||||
if custom_items:
|
|
||||||
mapping = collections.ChainMap(custom_items, lookup_any_item_id_to_name)
|
|
||||||
else:
|
|
||||||
mapping = lookup_any_item_id_to_name
|
|
||||||
|
|
||||||
return collections.Counter({mapping.get(item_id, None): count for item_id, count in inventory.items()})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tracker/<suuid:tracker>')
|
|
||||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
|
||||||
def get_multiworld_tracker(tracker: UUID):
|
|
||||||
data = _get_multiworld_tracker_data(tracker)
|
|
||||||
if not data:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
|
|
||||||
|
|
||||||
return render_template("multiTracker.html", **data)
|
|
||||||
|
|
||||||
if "Factorio" in games:
|
|
||||||
@app.route('/tracker/<suuid:tracker>/Factorio')
|
|
||||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
|
||||||
def get_Factorio_multiworld_tracker(tracker: UUID):
|
|
||||||
data = _get_multiworld_tracker_data(tracker)
|
|
||||||
if not data:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
data["inventory"] = _get_inventory_data(data)
|
|
||||||
data["named_inventory"] = {team_id : {
|
|
||||||
player_id: _get_named_inventory(inventory, data["custom_items"])
|
|
||||||
for player_id, inventory in team_inventory.items()
|
|
||||||
} for team_id, team_inventory in data["inventory"].items()}
|
|
||||||
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
|
|
||||||
|
|
||||||
return render_template("multiFactorioTracker.html", **data)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tracker/<suuid:tracker>/A Link to the Past')
|
@app.route('/tracker/<suuid:tracker>/A Link to the Past')
|
||||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||||
def get_LttP_multiworld_tracker(tracker: UUID):
|
def get_LttP_multiworld_tracker(tracker: UUID):
|
||||||
@@ -1683,7 +1400,7 @@ def get_LttP_multiworld_tracker(tracker: UUID):
|
|||||||
for item_id in precollected:
|
for item_id in precollected:
|
||||||
attribute_item(team, player, item_id)
|
attribute_item(team, player, item_id)
|
||||||
for location in locations_checked:
|
for location in locations_checked:
|
||||||
if location not in player_locations or location not in player_location_to_area.get(player, {}):
|
if location not in player_locations or location not in player_location_to_area[player]:
|
||||||
continue
|
continue
|
||||||
item, recipient, flags = player_locations[location]
|
item, recipient, flags = player_locations[location]
|
||||||
recipients = groups.get(recipient, [recipient])
|
recipients = groups.get(recipient, [recipient])
|
||||||
@@ -1760,9 +1477,142 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
|||||||
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
|
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MultiTrackers
|
||||||
|
|
||||||
|
@app.route('/tracker/<suuid:tracker>')
|
||||||
|
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||||
|
def get_multiworld_tracker(tracker: UUID) -> str:
|
||||||
|
data = _get_multiworld_tracker_data(tracker)
|
||||||
|
if not data:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Generic")
|
||||||
|
|
||||||
|
return render_template("multiTracker.html", **data)
|
||||||
|
|
||||||
|
def get_enabled_multiworld_trackers(room: Room, current: str) -> typing.List[typing.Dict[str, typing.Any]]:
|
||||||
|
enabled = [
|
||||||
|
{
|
||||||
|
"name": "Generic",
|
||||||
|
"endpoint": "get_multiworld_tracker",
|
||||||
|
"current": current == "Generic"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for game_name, endpoint in multi_trackers.items():
|
||||||
|
if any(slot.game == game_name for slot in room.seed.slots) or current == game_name:
|
||||||
|
enabled.append({
|
||||||
|
"name": game_name,
|
||||||
|
"endpoint": endpoint.__name__,
|
||||||
|
"current": current == game_name}
|
||||||
|
)
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
|
||||||
|
def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[str, typing.Any]]:
|
||||||
|
room: Room = Room.get(tracker=tracker)
|
||||||
|
if not room:
|
||||||
|
return None
|
||||||
|
|
||||||
|
locations, names, use_door_tracker, checks_in_area, player_location_to_area, \
|
||||||
|
precollected_items, games, slot_data, groups, saving_second, custom_locations, custom_items = \
|
||||||
|
get_static_room_data(room)
|
||||||
|
|
||||||
|
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
|
||||||
|
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||||
|
for teamnumber, team in enumerate(names)}
|
||||||
|
|
||||||
|
percent_total_checks_done = {teamnumber: {playernumber: 0
|
||||||
|
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||||
|
for teamnumber, team in enumerate(names)}
|
||||||
|
|
||||||
|
hints = {team: set() for team in range(len(names))}
|
||||||
|
if room.multisave:
|
||||||
|
multisave = restricted_loads(room.multisave)
|
||||||
|
else:
|
||||||
|
multisave = {}
|
||||||
|
if "hints" in multisave:
|
||||||
|
for (team, slot), slot_hints in multisave["hints"].items():
|
||||||
|
hints[team] |= set(slot_hints)
|
||||||
|
|
||||||
|
for (team, player), locations_checked in multisave.get("location_checks", {}).items():
|
||||||
|
if player in groups:
|
||||||
|
continue
|
||||||
|
player_locations = locations[player]
|
||||||
|
checks_done[team][player]["Total"] = len(locations_checked)
|
||||||
|
percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] /
|
||||||
|
len(player_locations) * 100) \
|
||||||
|
if player_locations else 100
|
||||||
|
|
||||||
|
activity_timers = {}
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
for (team, player), timestamp in multisave.get("client_activity_timers", []):
|
||||||
|
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||||
|
|
||||||
|
player_names = {}
|
||||||
|
states: typing.Dict[typing.Tuple[int, int], int] = {}
|
||||||
|
for team, names in enumerate(names):
|
||||||
|
for player, name in enumerate(names, 1):
|
||||||
|
player_names[team, player] = name
|
||||||
|
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||||
|
long_player_names = player_names.copy()
|
||||||
|
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||||
|
player_names[team, player] = alias
|
||||||
|
long_player_names[(team, player)] = f"{alias} ({long_player_names[team, player]})"
|
||||||
|
|
||||||
|
video = {}
|
||||||
|
for (team, player), data in multisave.get("video", []):
|
||||||
|
video[team, player] = data
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
player_names=player_names, room=room, checks_done=checks_done,
|
||||||
|
percent_total_checks_done=percent_total_checks_done, checks_in_area=checks_in_area,
|
||||||
|
activity_timers=activity_timers, video=video, hints=hints,
|
||||||
|
long_player_names=long_player_names,
|
||||||
|
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
||||||
|
locations=locations, games=games, states=states,
|
||||||
|
custom_locations=custom_locations, custom_items=custom_items,
|
||||||
|
)
|
||||||
|
|
||||||
multi_trackers: typing.Dict[str, typing.Callable] = {
|
multi_trackers: typing.Dict[str, typing.Callable] = {
|
||||||
"A Link to the Past": get_LttP_multiworld_tracker,
|
"A Link to the Past": get_LttP_multiworld_tracker,
|
||||||
}
|
}
|
||||||
|
|
||||||
if "Factorio" in games:
|
class MultiTrackerData(typing.NamedTuple):
|
||||||
multi_trackers["Factorio"] = get_Factorio_multiworld_tracker
|
template: Template
|
||||||
|
item_name_to_id: typing.Dict[str, int]
|
||||||
|
location_name_to_id: typing.Dict[str, int]
|
||||||
|
|
||||||
|
multi_tracker_data: typing.Dict[str, MultiTrackerData] = {}
|
||||||
|
|
||||||
|
@app.route("/tracker/<suuid:tracker>/<game>")
|
||||||
|
@cache.memoize(timeout=60) # multisave is currently created up to every minute
|
||||||
|
def get_game_multiworld_tracker(tracker: UUID, game: str) -> str:
|
||||||
|
current_multi_tracker_data = multi_tracker_data.get(game, None)
|
||||||
|
if not current_multi_tracker_data:
|
||||||
|
abort(404)
|
||||||
|
data = _get_multiworld_tracker_data(tracker)
|
||||||
|
if not data:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
data["inventory"] = _get_inventory_data(data)
|
||||||
|
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], game)
|
||||||
|
data["item_name_to_id"] = current_multi_tracker_data.item_name_to_id
|
||||||
|
data["location_name_to_id"] = current_multi_tracker_data.location_name_to_id
|
||||||
|
|
||||||
|
return render_template(current_multi_tracker_data.template, **data)
|
||||||
|
|
||||||
|
def register_multitrackers() -> None:
|
||||||
|
for world in AutoWorldRegister.world_types.values():
|
||||||
|
multitracker = world.web.multitracker_template
|
||||||
|
if multitracker:
|
||||||
|
multitracker_template = pkgutil.get_data(world.__module__, multitracker).decode()
|
||||||
|
multitracker_template = app.jinja_env.from_string(multitracker_template)
|
||||||
|
|
||||||
|
multi_trackers[world.game] = get_game_multiworld_tracker
|
||||||
|
multi_tracker_data[world.game] = MultiTrackerData(
|
||||||
|
multitracker_template,
|
||||||
|
world.item_name_to_id,
|
||||||
|
world.location_name_to_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
register_multitrackers()
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ local itemsObtained = 0x0677
|
|||||||
local takeAnyCavesChecked = 0x0678
|
local takeAnyCavesChecked = 0x0678
|
||||||
local localTriforce = 0x0679
|
local localTriforce = 0x0679
|
||||||
local bonusItemsObtained = 0x067A
|
local bonusItemsObtained = 0x067A
|
||||||
local itemsObtainedHigh = 0x067B
|
|
||||||
|
|
||||||
itemAPids = {
|
itemAPids = {
|
||||||
["Boomerang"] = 7100,
|
["Boomerang"] = 7100,
|
||||||
@@ -174,18 +173,11 @@ for key, value in pairs(itemAPids) do
|
|||||||
itemIDNames[value] = key
|
itemIDNames[value] = key
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getItemsObtained()
|
|
||||||
return bit.bor(bit.lshift(u8(itemsObtainedHigh), 8), u8(itemsObtained))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function setItemsObtained(value)
|
|
||||||
wU8(itemsObtainedHigh, bit.rshift(value, 8))
|
|
||||||
wU8(itemsObtained, bit.band(value, 0xFF))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function determineItem(array)
|
local function determineItem(array)
|
||||||
memdomain.ram()
|
memdomain.ram()
|
||||||
currentItemsObtained = getItemsObtained()
|
currentItemsObtained = u8(itemsObtained)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -372,8 +364,8 @@ local function gotItem(item)
|
|||||||
wU8(0x505, itemCode)
|
wU8(0x505, itemCode)
|
||||||
wU8(0x506, 128)
|
wU8(0x506, 128)
|
||||||
wU8(0x602, 4)
|
wU8(0x602, 4)
|
||||||
numberObtained = getItemsObtained() + 1
|
numberObtained = u8(itemsObtained) + 1
|
||||||
setItemsObtained(numberObtained)
|
wU8(itemsObtained, numberObtained)
|
||||||
if itemName == "Boomerang" then gotBoomerang() end
|
if itemName == "Boomerang" then gotBoomerang() end
|
||||||
if itemName == "Bow" then gotBow() end
|
if itemName == "Bow" then gotBow() end
|
||||||
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
|
if itemName == "Magical Boomerang" then gotMagicalBoomerang() end
|
||||||
@@ -484,7 +476,7 @@ function processBlock(block)
|
|||||||
if i > u8(bonusItemsObtained) then
|
if i > u8(bonusItemsObtained) then
|
||||||
if u8(0x505) == 0 then
|
if u8(0x505) == 0 then
|
||||||
gotItem(item)
|
gotItem(item)
|
||||||
setItemsObtained(getItemsObtained() - 1)
|
wU8(itemsObtained, u8(itemsObtained) - 1)
|
||||||
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
|
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -502,7 +494,7 @@ function processBlock(block)
|
|||||||
for i, item in ipairs(itemsBlock) do
|
for i, item in ipairs(itemsBlock) do
|
||||||
memDomain.ram()
|
memDomain.ram()
|
||||||
if u8(0x505) == 0 then
|
if u8(0x505) == 0 then
|
||||||
if i > getItemsObtained() then
|
if i > u8(itemsObtained) then
|
||||||
gotItem(item)
|
gotItem(item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -554,7 +546,7 @@ function receive()
|
|||||||
retTable["gameMode"] = gameMode
|
retTable["gameMode"] = gameMode
|
||||||
retTable["overworldHC"] = getHCLocation()
|
retTable["overworldHC"] = getHCLocation()
|
||||||
retTable["overworldPB"] = getPBLocation()
|
retTable["overworldPB"] = getPBLocation()
|
||||||
retTable["itemsObtained"] = getItemsObtained()
|
retTable["itemsObtained"] = u8(itemsObtained)
|
||||||
msg = json.encode(retTable).."\n"
|
msg = json.encode(retTable).."\n"
|
||||||
local ret, error = zeldaSocket:send(msg)
|
local ret, error = zeldaSocket:send(msg)
|
||||||
if ret == nil then
|
if ret == nil then
|
||||||
@@ -614,4 +606,4 @@ function main()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
main()
|
main()
|
||||||
@@ -116,7 +116,6 @@ Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI";
|
|||||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||||
|
|
||||||
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
|
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
|
||||||
Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion;
|
|
||||||
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||||
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||||
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||||
|
|||||||
7
kvui.py
@@ -1,14 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
if sys.platform == "win32":
|
|
||||||
import ctypes
|
|
||||||
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
|
|
||||||
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
|
|
||||||
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
|
||||||
|
|
||||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||||
os.environ["KIVY_NO_ARGS"] = "1"
|
os.environ["KIVY_NO_ARGS"] = "1"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
colorama>=0.4.5
|
colorama>=0.4.5
|
||||||
websockets>=11.0.3
|
websockets>=11.0.3
|
||||||
PyYAML>=6.0.1
|
PyYAML>=6.0.1
|
||||||
jellyfish>=1.0.1
|
jellyfish>=1.0.0
|
||||||
jinja2>=3.1.2
|
jinja2>=3.1.2
|
||||||
schema>=0.7.5
|
schema>=0.7.5
|
||||||
kivy>=2.2.0
|
kivy>=2.2.0
|
||||||
@@ -9,4 +9,4 @@ bsdiff4>=1.2.3
|
|||||||
platformdirs>=3.9.1
|
platformdirs>=3.9.1
|
||||||
certifi>=2023.7.22
|
certifi>=2023.7.22
|
||||||
cython>=0.29.35
|
cython>=0.29.35
|
||||||
cymem>=2.0.8
|
cymem>=2.0.7
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class Group:
|
|||||||
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
|
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
|
||||||
return cls._type_cache
|
return cls._type_cache
|
||||||
|
|
||||||
def get(self, key: str, default: Any = None) -> Any:
|
def get(self, key: str, default: Any) -> Any:
|
||||||
if key in self:
|
if key in self:
|
||||||
return self[key]
|
return self[key]
|
||||||
return default
|
return default
|
||||||
|
|||||||
13
setup.py
@@ -80,6 +80,7 @@ non_apworlds: set = {
|
|||||||
"Raft",
|
"Raft",
|
||||||
"Secret of Evermore",
|
"Secret of Evermore",
|
||||||
"Slay the Spire",
|
"Slay the Spire",
|
||||||
|
"Starcraft 2 Wings of Liberty",
|
||||||
"Sudoku",
|
"Sudoku",
|
||||||
"Super Mario 64",
|
"Super Mario 64",
|
||||||
"VVVVVV",
|
"VVVVVV",
|
||||||
@@ -90,7 +91,6 @@ non_apworlds: set = {
|
|||||||
# LogicMixin is broken before 3.10 import revamp
|
# LogicMixin is broken before 3.10 import revamp
|
||||||
if sys.version_info < (3,10):
|
if sys.version_info < (3,10):
|
||||||
non_apworlds.add("Hollow Knight")
|
non_apworlds.add("Hollow Knight")
|
||||||
non_apworlds.add("Starcraft 2 Wings of Liberty")
|
|
||||||
|
|
||||||
def download_SNI():
|
def download_SNI():
|
||||||
print("Updating SNI")
|
print("Updating SNI")
|
||||||
@@ -185,22 +185,13 @@ def resolve_icon(icon_name: str):
|
|||||||
|
|
||||||
exes = [
|
exes = [
|
||||||
cx_Freeze.Executable(
|
cx_Freeze.Executable(
|
||||||
script=f"{c.script_name}.py",
|
script=f'{c.script_name}.py',
|
||||||
target_name=c.frozen_name + (".exe" if is_windows else ""),
|
target_name=c.frozen_name + (".exe" if is_windows else ""),
|
||||||
icon=resolve_icon(c.icon),
|
icon=resolve_icon(c.icon),
|
||||||
base="Win32GUI" if is_windows and not c.cli else None
|
base="Win32GUI" if is_windows and not c.cli else None
|
||||||
) for c in components if c.script_name and c.frozen_name
|
) for c in components if c.script_name and c.frozen_name
|
||||||
]
|
]
|
||||||
|
|
||||||
if is_windows:
|
|
||||||
# create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help
|
|
||||||
c = next(component for component in components if component.script_name == "Launcher")
|
|
||||||
exes.append(cx_Freeze.Executable(
|
|
||||||
script=f"{c.script_name}.py",
|
|
||||||
target_name=f"{c.frozen_name}(DEBUG).exe",
|
|
||||||
icon=resolve_icon(c.icon),
|
|
||||||
))
|
|
||||||
|
|
||||||
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
|
extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
|
||||||
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import json
|
|||||||
class TestDocs(unittest.TestCase):
|
class TestDocs(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls) -> None:
|
def setUpClass(cls) -> None:
|
||||||
from WebHostLib import app as raw_app
|
from WebHost import get_app, raw_app
|
||||||
from WebHost import get_app
|
|
||||||
raw_app.config["PONY"] = {
|
raw_app.config["PONY"] = {
|
||||||
"provider": "sqlite",
|
"provider": "sqlite",
|
||||||
"filename": ":memory:",
|
"filename": ":memory:",
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ class TestFileGeneration(unittest.TestCase):
|
|||||||
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
||||||
|
|
||||||
def testOptions(self):
|
def testOptions(self):
|
||||||
from WebHostLib.options import create as create_options_files
|
WebHost.create_options_files()
|
||||||
create_options_files()
|
|
||||||
target = os.path.join(self.correct_path, "static", "generated", "configs")
|
target = os.path.join(self.correct_path, "static", "generated", "configs")
|
||||||
self.assertTrue(os.path.exists(target))
|
self.assertTrue(os.path.exists(target))
|
||||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
|
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
|||||||
from BaseClasses import MultiWorld, Item, Location, Tutorial
|
from BaseClasses import MultiWorld, Item, Location, Tutorial
|
||||||
from . import GamesPackage
|
from . import GamesPackage
|
||||||
from settings import Group
|
from settings import Group
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
|
||||||
class AutoWorldRegister(type):
|
class AutoWorldRegister(type):
|
||||||
@@ -155,9 +156,22 @@ class WebWorld:
|
|||||||
"""Choose a theme for you /game/* pages.
|
"""Choose a theme for you /game/* pages.
|
||||||
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
|
Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone"""
|
||||||
|
|
||||||
bug_report_page: Optional[str]
|
bug_report_page: Optional[str] = None
|
||||||
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
|
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
|
||||||
|
|
||||||
|
multitracker_template: Optional[str] = None
|
||||||
|
"""relative path with /-seperator to a MultiTracker Template file."""
|
||||||
|
|
||||||
|
# allows modification of webhost during startup, this is run once
|
||||||
|
@classmethod
|
||||||
|
def run_webhost_setup(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# allows modification of webhost during startup,
|
||||||
|
# this is run whenever a Flask app is created (per-thread/per-process)
|
||||||
|
@classmethod
|
||||||
|
def run_webhost_app_setup(cls, app: "Flask"):
|
||||||
|
pass
|
||||||
|
|
||||||
class World(metaclass=AutoWorldRegister):
|
class World(metaclass=AutoWorldRegister):
|
||||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||||
@@ -412,7 +426,6 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
res["checksum"] = data_package_checksum(res)
|
res["checksum"] = data_package_checksum(res)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
# any methods attached to this can be used as part of CollectionState,
|
# any methods attached to this can be used as part of CollectionState,
|
||||||
# please use a prefix as all of them get clobbered together
|
# please use a prefix as all of them get clobbered together
|
||||||
class LogicMixin(metaclass=AutoLogicRegister):
|
class LogicMixin(metaclass=AutoLogicRegister):
|
||||||
|
|||||||
@@ -8,31 +8,18 @@ from .paths import Paths
|
|||||||
|
|
||||||
|
|
||||||
def get(name: str) -> Map:
|
def get(name: str) -> Map:
|
||||||
|
# Iterate through 2 folder depths
|
||||||
for map_dir in (p for p in Paths.MAPS.iterdir()):
|
for map_dir in (p for p in Paths.MAPS.iterdir()):
|
||||||
map = find_map_in_dir(name, map_dir)
|
if map_dir.is_dir():
|
||||||
if map is not None:
|
for map_file in (p for p in map_dir.iterdir()):
|
||||||
return map
|
if Map.matches_target_map_name(map_file, name):
|
||||||
|
return Map(map_file)
|
||||||
|
elif Map.matches_target_map_name(map_dir, name):
|
||||||
|
return Map(map_dir)
|
||||||
|
|
||||||
raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".")
|
raise KeyError(f"Map '{name}' was not found. Please put the map file in \"/StarCraft II/Maps/\".")
|
||||||
|
|
||||||
|
|
||||||
# Go deeper
|
|
||||||
def find_map_in_dir(name, path):
|
|
||||||
if Map.matches_target_map_name(path, name):
|
|
||||||
return Map(path)
|
|
||||||
|
|
||||||
if path.name.endswith("SC2Map"):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if path.is_dir():
|
|
||||||
for childPath in (p for p in path.iterdir()):
|
|
||||||
map = find_map_in_dir(name, childPath)
|
|
||||||
if map is not None:
|
|
||||||
return map
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class Map:
|
class Map:
|
||||||
|
|
||||||
def __init__(self, path: Path):
|
def __init__(self, path: Path):
|
||||||
|
|||||||
@@ -7,21 +7,19 @@ LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
|
|||||||
RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f"
|
RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f"
|
||||||
ROM_PLAYER_LIMIT: int = 255
|
ROM_PLAYER_LIMIT: int = 255
|
||||||
|
|
||||||
import io
|
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import struct
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import bsdiff4
|
import bsdiff4
|
||||||
from typing import Optional, List
|
from typing import List
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
from BaseClasses import CollectionState, Region, Location, MultiWorld
|
||||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, read_snes_rom
|
||||||
|
|
||||||
from .Shops import ShopType, ShopPriceType
|
from .Shops import ShopType, ShopPriceType
|
||||||
from .Dungeons import dungeon_music_addresses
|
from .Dungeons import dungeon_music_addresses
|
||||||
@@ -37,6 +35,7 @@ from .Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmith
|
|||||||
from .Items import ItemFactory, item_table, item_name_groups, progression_items
|
from .Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||||
from .EntranceShuffle import door_addresses
|
from .EntranceShuffle import door_addresses
|
||||||
from .Options import smallkey_shuffle
|
from .Options import smallkey_shuffle
|
||||||
|
from .Sprites import apply_random_sprite_on_event
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from maseya import z3pr
|
from maseya import z3pr
|
||||||
@@ -212,73 +211,6 @@ def check_enemizer(enemizercli):
|
|||||||
check_enemizer.done = True
|
check_enemizer.done = True
|
||||||
|
|
||||||
|
|
||||||
def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_random_on_event, sprite_pool):
|
|
||||||
userandomsprites = False
|
|
||||||
if sprite and not isinstance(sprite, Sprite):
|
|
||||||
sprite = sprite.lower()
|
|
||||||
userandomsprites = sprite.startswith('randomon')
|
|
||||||
|
|
||||||
racerom = rom.read_byte(0x180213)
|
|
||||||
if allow_random_on_event or not racerom:
|
|
||||||
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
|
|
||||||
# However, if the seed is not a racerom seed, then it is always allowed.
|
|
||||||
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
|
|
||||||
|
|
||||||
onevent = 0
|
|
||||||
if sprite == 'randomonall':
|
|
||||||
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
|
|
||||||
elif sprite == 'randomonnone':
|
|
||||||
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
|
|
||||||
onevent = 0x0000
|
|
||||||
elif sprite == 'randomonrandom':
|
|
||||||
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
|
|
||||||
onevent = local_random.randint(0x0001, 0x003F)
|
|
||||||
elif userandomsprites:
|
|
||||||
onevent = 0x01 if 'hit' in sprite else 0x00
|
|
||||||
onevent += 0x02 if 'enter' in sprite else 0x00
|
|
||||||
onevent += 0x04 if 'exit' in sprite else 0x00
|
|
||||||
onevent += 0x08 if 'slash' in sprite else 0x00
|
|
||||||
onevent += 0x10 if 'item' in sprite else 0x00
|
|
||||||
onevent += 0x20 if 'bonk' in sprite else 0x00
|
|
||||||
|
|
||||||
rom.write_int16(0x18637F, onevent)
|
|
||||||
|
|
||||||
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
|
|
||||||
|
|
||||||
# write link sprite if required
|
|
||||||
if sprite:
|
|
||||||
sprites = list()
|
|
||||||
sprite.write_to_rom(rom)
|
|
||||||
|
|
||||||
_populate_sprite_table()
|
|
||||||
if userandomsprites:
|
|
||||||
if sprite_pool:
|
|
||||||
if isinstance(sprite_pool, str):
|
|
||||||
sprite_pool = sprite_pool.split(':')
|
|
||||||
for spritename in sprite_pool:
|
|
||||||
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
|
|
||||||
spritename, local_random)
|
|
||||||
if sprite:
|
|
||||||
sprites.append(sprite)
|
|
||||||
else:
|
|
||||||
logging.info(f"Sprite {spritename} was not found.")
|
|
||||||
else:
|
|
||||||
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
|
|
||||||
else:
|
|
||||||
sprites.append(sprite)
|
|
||||||
if sprites:
|
|
||||||
while len(sprites) < 32:
|
|
||||||
sprites.extend(sprites)
|
|
||||||
local_random.shuffle(sprites)
|
|
||||||
|
|
||||||
for i, sprite in enumerate(sprites[:32]):
|
|
||||||
if not i and not userandomsprites:
|
|
||||||
continue
|
|
||||||
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
|
|
||||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
|
||||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
|
||||||
|
|
||||||
|
|
||||||
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
||||||
player = world.player
|
player = world.player
|
||||||
multiworld = world.multiworld
|
multiworld = world.multiworld
|
||||||
@@ -487,271 +419,6 @@ class TileSet:
|
|||||||
return localrandom.choice(tile_sets)
|
return localrandom.choice(tile_sets)
|
||||||
|
|
||||||
|
|
||||||
sprite_list_lock = threading.Lock()
|
|
||||||
_sprite_table = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _populate_sprite_table():
|
|
||||||
with sprite_list_lock:
|
|
||||||
if not _sprite_table:
|
|
||||||
def load_sprite_from_file(file):
|
|
||||||
sprite = Sprite(file)
|
|
||||||
if sprite.valid:
|
|
||||||
_sprite_table[sprite.name.lower()] = sprite
|
|
||||||
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
|
|
||||||
else:
|
|
||||||
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
||||||
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
|
|
||||||
for file in os.listdir(dir):
|
|
||||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
|
||||||
|
|
||||||
|
|
||||||
class Sprite():
|
|
||||||
sprite_size = 28672
|
|
||||||
palette_size = 120
|
|
||||||
glove_size = 4
|
|
||||||
author_name: Optional[str] = None
|
|
||||||
base_data: bytes
|
|
||||||
|
|
||||||
def __init__(self, filename):
|
|
||||||
if not hasattr(Sprite, "base_data"):
|
|
||||||
self.get_vanilla_sprite_data()
|
|
||||||
with open(filename, 'rb') as file:
|
|
||||||
filedata = file.read()
|
|
||||||
self.name = os.path.basename(filename)
|
|
||||||
self.valid = True
|
|
||||||
if filename.endswith(".apsprite"):
|
|
||||||
self.from_ap_sprite(filedata)
|
|
||||||
elif len(filedata) == 0x7000:
|
|
||||||
# sprite file with graphics and without palette data
|
|
||||||
self.sprite = filedata[:0x7000]
|
|
||||||
elif len(filedata) == 0x7078:
|
|
||||||
# sprite file with graphics and palette data
|
|
||||||
self.sprite = filedata[:0x7000]
|
|
||||||
self.palette = filedata[0x7000:]
|
|
||||||
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
|
|
||||||
elif len(filedata) == 0x707C:
|
|
||||||
# sprite file with graphics and palette data including gloves
|
|
||||||
self.sprite = filedata[:0x7000]
|
|
||||||
self.palette = filedata[0x7000:0x7078]
|
|
||||||
self.glove_palette = filedata[0x7078:]
|
|
||||||
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
|
|
||||||
# full rom with patched sprite, extract it
|
|
||||||
self.sprite = filedata[0x80000:0x87000]
|
|
||||||
self.palette = filedata[0xDD308:0xDD380]
|
|
||||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
|
||||||
elif filedata.startswith(b'ZSPR'):
|
|
||||||
self.from_zspr(filedata, filename)
|
|
||||||
else:
|
|
||||||
self.valid = False
|
|
||||||
|
|
||||||
def get_vanilla_sprite_data(self):
|
|
||||||
file_name = get_base_rom_path()
|
|
||||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
|
||||||
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
|
||||||
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
|
|
||||||
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
|
|
||||||
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
|
||||||
|
|
||||||
def from_ap_sprite(self, filedata):
|
|
||||||
# noinspection PyBroadException
|
|
||||||
try:
|
|
||||||
obj = parse_yaml(filedata.decode("utf-8-sig"))
|
|
||||||
if obj["min_format_version"] > 1:
|
|
||||||
raise Exception("Sprite file requires an updated reader.")
|
|
||||||
self.author_name = obj["author"]
|
|
||||||
self.name = obj["name"]
|
|
||||||
if obj["data"]: # skip patching for vanilla content
|
|
||||||
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
|
||||||
self.sprite = data[:self.sprite_size]
|
|
||||||
self.palette = data[self.sprite_size:self.palette_size]
|
|
||||||
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
|
||||||
except Exception:
|
|
||||||
logger = logging.getLogger("apsprite")
|
|
||||||
logger.exception("Error parsing apsprite file")
|
|
||||||
self.valid = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def author_game_display(self) -> str:
|
|
||||||
name = getattr(self, "_author_game_display", "")
|
|
||||||
if not name:
|
|
||||||
name = self.author_name
|
|
||||||
|
|
||||||
# At this point, may need some filtering to displayable characters
|
|
||||||
return name
|
|
||||||
|
|
||||||
def to_ap_sprite(self, path):
|
|
||||||
import yaml
|
|
||||||
payload = {"format_version": 1,
|
|
||||||
"min_format_version": 1,
|
|
||||||
"sprite_version": 1,
|
|
||||||
"name": self.name,
|
|
||||||
"author": self.author_name,
|
|
||||||
"game": "A Link to the Past",
|
|
||||||
"data": self.get_delta()}
|
|
||||||
with open(path, "w") as f:
|
|
||||||
f.write(yaml.safe_dump(payload))
|
|
||||||
|
|
||||||
def get_delta(self):
|
|
||||||
modified_data = self.sprite + self.palette + self.glove_palette
|
|
||||||
return bsdiff4.diff(Sprite.base_data, modified_data)
|
|
||||||
|
|
||||||
def from_zspr(self, filedata, filename):
|
|
||||||
result = self.parse_zspr(filedata, 1)
|
|
||||||
if result is None:
|
|
||||||
self.valid = False
|
|
||||||
return
|
|
||||||
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
|
|
||||||
if self.name == "":
|
|
||||||
self.name = os.path.split(filename)[1].split(".")[0]
|
|
||||||
|
|
||||||
if len(sprite) != 0x7000:
|
|
||||||
self.valid = False
|
|
||||||
return
|
|
||||||
self.sprite = sprite
|
|
||||||
if len(palette) == 0:
|
|
||||||
pass
|
|
||||||
elif len(palette) == 0x78:
|
|
||||||
self.palette = palette
|
|
||||||
elif len(palette) == 0x7C:
|
|
||||||
self.palette = palette[:0x78]
|
|
||||||
self.glove_palette = palette[0x78:]
|
|
||||||
else:
|
|
||||||
self.valid = False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
|
|
||||||
_populate_sprite_table()
|
|
||||||
name = name.lower()
|
|
||||||
if name.startswith('random'):
|
|
||||||
sprites = list(set(_sprite_table.values()))
|
|
||||||
sprites.sort(key=lambda x: x.name)
|
|
||||||
return local_random.choice(sprites)
|
|
||||||
return _sprite_table.get(name, None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def default_link_sprite():
|
|
||||||
return Sprite(local_path('data', 'default.apsprite'))
|
|
||||||
|
|
||||||
def decode8(self, pos):
|
|
||||||
arr = [[0 for _ in range(8)] for _ in range(8)]
|
|
||||||
for y in range(8):
|
|
||||||
for x in range(8):
|
|
||||||
position = 1 << (7 - x)
|
|
||||||
val = 0
|
|
||||||
if self.sprite[pos + 2 * y] & position:
|
|
||||||
val += 1
|
|
||||||
if self.sprite[pos + 2 * y + 1] & position:
|
|
||||||
val += 2
|
|
||||||
if self.sprite[pos + 2 * y + 16] & position:
|
|
||||||
val += 4
|
|
||||||
if self.sprite[pos + 2 * y + 17] & position:
|
|
||||||
val += 8
|
|
||||||
arr[y][x] = val
|
|
||||||
return arr
|
|
||||||
|
|
||||||
def decode16(self, pos):
|
|
||||||
arr = [[0 for _ in range(16)] for _ in range(16)]
|
|
||||||
top_left = self.decode8(pos)
|
|
||||||
top_right = self.decode8(pos + 0x20)
|
|
||||||
bottom_left = self.decode8(pos + 0x200)
|
|
||||||
bottom_right = self.decode8(pos + 0x220)
|
|
||||||
for x in range(8):
|
|
||||||
for y in range(8):
|
|
||||||
arr[y][x] = top_left[y][x]
|
|
||||||
arr[y][x + 8] = top_right[y][x]
|
|
||||||
arr[y + 8][x] = bottom_left[y][x]
|
|
||||||
arr[y + 8][x + 8] = bottom_right[y][x]
|
|
||||||
return arr
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_zspr(filedata, expected_kind):
|
|
||||||
logger = logging.getLogger("ZSPR")
|
|
||||||
headerstr = "<4xBHHIHIHH6x"
|
|
||||||
headersize = struct.calcsize(headerstr)
|
|
||||||
if len(filedata) < headersize:
|
|
||||||
return None
|
|
||||||
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
|
||||||
headerstr, filedata)
|
|
||||||
if version not in [1]:
|
|
||||||
logger.error("Error parsing ZSPR file: Version %g not supported", version)
|
|
||||||
return None
|
|
||||||
if kind != expected_kind:
|
|
||||||
return None
|
|
||||||
|
|
||||||
stream = io.BytesIO(filedata)
|
|
||||||
stream.seek(headersize)
|
|
||||||
|
|
||||||
def read_utf16le(stream):
|
|
||||||
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
|
|
||||||
raw = bytearray()
|
|
||||||
while True:
|
|
||||||
char = stream.read(2)
|
|
||||||
if char in [b"", b"\x00\x00"]:
|
|
||||||
break
|
|
||||||
raw += char
|
|
||||||
return raw.decode("utf-16_le")
|
|
||||||
|
|
||||||
# noinspection PyBroadException
|
|
||||||
try:
|
|
||||||
sprite_name = read_utf16le(stream)
|
|
||||||
author_name = read_utf16le(stream)
|
|
||||||
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
|
||||||
|
|
||||||
# Ignoring the Author Rom name for the time being.
|
|
||||||
|
|
||||||
real_csum = sum(filedata) % 0x10000
|
|
||||||
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
|
||||||
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
|
|
||||||
|
|
||||||
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
|
||||||
palette = filedata[palette_offset:palette_offset + palette_size]
|
|
||||||
|
|
||||||
if len(sprite) != sprite_size or len(palette) != palette_size:
|
|
||||||
logger.error("Error parsing ZSPR file: Unexpected end of file")
|
|
||||||
return None
|
|
||||||
|
|
||||||
return sprite, palette, sprite_name, author_name, author_credits_name
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Error parsing ZSPR file")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def decode_palette(self):
|
|
||||||
"""Returns the palettes as an array of arrays of 15 colors"""
|
|
||||||
|
|
||||||
def array_chunk(arr, size):
|
|
||||||
return list(zip(*[iter(arr)] * size))
|
|
||||||
|
|
||||||
def make_int16(pair):
|
|
||||||
return pair[1] << 8 | pair[0]
|
|
||||||
|
|
||||||
def expand_color(i):
|
|
||||||
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
|
|
||||||
|
|
||||||
# turn palette data into a list of RGB tuples with 8 bit values
|
|
||||||
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
|
|
||||||
|
|
||||||
# split into palettes of 15 colors
|
|
||||||
return array_chunk(palette_as_colors, 15)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.name)
|
|
||||||
|
|
||||||
def write_to_rom(self, rom: LocalRom):
|
|
||||||
if not self.valid:
|
|
||||||
logging.warning("Tried writing invalid sprite to rom, skipping.")
|
|
||||||
return
|
|
||||||
rom.write_bytes(0x80000, self.sprite)
|
|
||||||
rom.write_bytes(0xDD308, self.palette)
|
|
||||||
rom.write_bytes(0xDEDF5, self.glove_palette)
|
|
||||||
rom.write_bytes(0x300000, self.sprite)
|
|
||||||
rom.write_bytes(0x307000, self.palette)
|
|
||||||
rom.write_bytes(0x307078, self.glove_palette)
|
|
||||||
|
|
||||||
|
|
||||||
bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A,
|
bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A,
|
||||||
0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD,
|
0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD,
|
||||||
0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D,
|
0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ def set_rules(world):
|
|||||||
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
||||||
|
|
||||||
if world.players == 1:
|
if world.players == 1:
|
||||||
|
world.get_region('Menu', player).can_reach_private = lambda state: True
|
||||||
no_logic_rules(world, player)
|
no_logic_rules(world, player)
|
||||||
for exit in world.get_region('Menu', player).exits:
|
for exit in world.get_region('Menu', player).exits:
|
||||||
exit.hide_path = True
|
exit.hide_path = True
|
||||||
@@ -195,6 +196,7 @@ def global_rules(world, player):
|
|||||||
add_item_rule(world.get_location(prize_location, player),
|
add_item_rule(world.get_location(prize_location, player),
|
||||||
lambda item: item.name in crystals_and_pendants and item.player == player)
|
lambda item: item.name in crystals_and_pendants and item.player == player)
|
||||||
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
|
# determines which S&Q locations are available - hide from paths since it isn't an in-game location
|
||||||
|
world.get_region('Menu', player).can_reach_private = lambda state: True
|
||||||
for exit in world.get_region('Menu', player).exits:
|
for exit in world.get_region('Menu', player).exits:
|
||||||
exit.hide_path = True
|
exit.hide_path = True
|
||||||
|
|
||||||
|
|||||||
393
worlds/alttp/Sprites.py
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import concurrent.futures
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
import bsdiff4
|
||||||
|
|
||||||
|
from Utils import user_path, read_snes_rom, parse_yaml, local_path
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .Rom import LocalRom
|
||||||
|
|
||||||
|
sprite_list_lock = threading.Lock()
|
||||||
|
_sprite_table = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _populate_sprite_table():
|
||||||
|
with sprite_list_lock:
|
||||||
|
if not _sprite_table:
|
||||||
|
def load_sprite_from_file(file):
|
||||||
|
sprite = Sprite(file)
|
||||||
|
if sprite.valid:
|
||||||
|
_sprite_table[sprite.name.lower()] = sprite
|
||||||
|
_sprite_table[os.path.basename(file).split(".")[0].lower()] = sprite # alias for filename base
|
||||||
|
else:
|
||||||
|
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||||
|
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
|
||||||
|
for file in os.listdir(dir):
|
||||||
|
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
||||||
|
|
||||||
|
|
||||||
|
class Sprite():
|
||||||
|
sprite_size = 28672
|
||||||
|
palette_size = 120
|
||||||
|
glove_size = 4
|
||||||
|
author_name: Optional[str] = None
|
||||||
|
base_data: bytes
|
||||||
|
|
||||||
|
def __init__(self, filename):
|
||||||
|
if not hasattr(Sprite, "base_data"):
|
||||||
|
self.get_vanilla_sprite_data()
|
||||||
|
with open(filename, 'rb') as file:
|
||||||
|
filedata = file.read()
|
||||||
|
self.name = os.path.basename(filename)
|
||||||
|
self.valid = True
|
||||||
|
if filename.endswith(".apsprite"):
|
||||||
|
self.from_ap_sprite(filedata)
|
||||||
|
elif len(filedata) == 0x7000:
|
||||||
|
# sprite file with graphics and without palette data
|
||||||
|
self.sprite = filedata[:0x7000]
|
||||||
|
elif len(filedata) == 0x7078:
|
||||||
|
# sprite file with graphics and palette data
|
||||||
|
self.sprite = filedata[:0x7000]
|
||||||
|
self.palette = filedata[0x7000:]
|
||||||
|
self.glove_palette = filedata[0x7036:0x7038] + filedata[0x7054:0x7056]
|
||||||
|
elif len(filedata) == 0x707C:
|
||||||
|
# sprite file with graphics and palette data including gloves
|
||||||
|
self.sprite = filedata[:0x7000]
|
||||||
|
self.palette = filedata[0x7000:0x7078]
|
||||||
|
self.glove_palette = filedata[0x7078:]
|
||||||
|
elif len(filedata) in [0x100000, 0x200000, 0x400000]:
|
||||||
|
# full rom with patched sprite, extract it
|
||||||
|
self.sprite = filedata[0x80000:0x87000]
|
||||||
|
self.palette = filedata[0xDD308:0xDD380]
|
||||||
|
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
||||||
|
elif filedata.startswith(b'ZSPR'):
|
||||||
|
self.from_zspr(filedata, filename)
|
||||||
|
else:
|
||||||
|
self.valid = False
|
||||||
|
|
||||||
|
def get_vanilla_sprite_data(self):
|
||||||
|
from .Rom import get_base_rom_path
|
||||||
|
file_name = get_base_rom_path()
|
||||||
|
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||||
|
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
||||||
|
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
|
||||||
|
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
|
||||||
|
Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette
|
||||||
|
|
||||||
|
def from_ap_sprite(self, filedata):
|
||||||
|
# noinspection PyBroadException
|
||||||
|
try:
|
||||||
|
obj = parse_yaml(filedata.decode("utf-8-sig"))
|
||||||
|
if obj["min_format_version"] > 1:
|
||||||
|
raise Exception("Sprite file requires an updated reader.")
|
||||||
|
self.author_name = obj["author"]
|
||||||
|
self.name = obj["name"]
|
||||||
|
if obj["data"]: # skip patching for vanilla content
|
||||||
|
data = bsdiff4.patch(Sprite.base_data, obj["data"])
|
||||||
|
self.sprite = data[:self.sprite_size]
|
||||||
|
self.palette = data[self.sprite_size:self.palette_size]
|
||||||
|
self.glove_palette = data[self.sprite_size + self.palette_size:]
|
||||||
|
except Exception:
|
||||||
|
logger = logging.getLogger("apsprite")
|
||||||
|
logger.exception("Error parsing apsprite file")
|
||||||
|
self.valid = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def author_game_display(self) -> str:
|
||||||
|
name = getattr(self, "_author_game_display", "")
|
||||||
|
if not name:
|
||||||
|
name = self.author_name
|
||||||
|
|
||||||
|
# At this point, may need some filtering to displayable characters
|
||||||
|
return name
|
||||||
|
|
||||||
|
def to_ap_sprite(self, path):
|
||||||
|
import yaml
|
||||||
|
payload = {"format_version": 1,
|
||||||
|
"min_format_version": 1,
|
||||||
|
"sprite_version": 1,
|
||||||
|
"name": self.name,
|
||||||
|
"author": self.author_name,
|
||||||
|
"game": "A Link to the Past",
|
||||||
|
"data": self.get_delta()}
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(yaml.safe_dump(payload))
|
||||||
|
|
||||||
|
def get_delta(self):
|
||||||
|
modified_data = self.sprite + self.palette + self.glove_palette
|
||||||
|
return bsdiff4.diff(Sprite.base_data, modified_data)
|
||||||
|
|
||||||
|
def from_zspr(self, filedata, filename):
|
||||||
|
result = self.parse_zspr(filedata, 1)
|
||||||
|
if result is None:
|
||||||
|
self.valid = False
|
||||||
|
return
|
||||||
|
(sprite, palette, self.name, self.author_name, self._author_game_display) = result
|
||||||
|
if self.name == "":
|
||||||
|
self.name = os.path.split(filename)[1].split(".")[0]
|
||||||
|
|
||||||
|
if len(sprite) != 0x7000:
|
||||||
|
self.valid = False
|
||||||
|
return
|
||||||
|
self.sprite = sprite
|
||||||
|
if len(palette) == 0:
|
||||||
|
pass
|
||||||
|
elif len(palette) == 0x78:
|
||||||
|
self.palette = palette
|
||||||
|
elif len(palette) == 0x7C:
|
||||||
|
self.palette = palette[:0x78]
|
||||||
|
self.glove_palette = palette[0x78:]
|
||||||
|
else:
|
||||||
|
self.valid = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_sprite_from_name(name: str, local_random=random) -> Optional[Sprite]:
|
||||||
|
_populate_sprite_table()
|
||||||
|
name = name.lower()
|
||||||
|
if name.startswith('random'):
|
||||||
|
sprites = list(set(_sprite_table.values()))
|
||||||
|
sprites.sort(key=lambda x: x.name)
|
||||||
|
return local_random.choice(sprites)
|
||||||
|
return _sprite_table.get(name, None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def default_link_sprite():
|
||||||
|
return Sprite(local_path('data', 'default.apsprite'))
|
||||||
|
|
||||||
|
def decode8(self, pos):
|
||||||
|
arr = [[0 for _ in range(8)] for _ in range(8)]
|
||||||
|
for y in range(8):
|
||||||
|
for x in range(8):
|
||||||
|
position = 1 << (7 - x)
|
||||||
|
val = 0
|
||||||
|
if self.sprite[pos + 2 * y] & position:
|
||||||
|
val += 1
|
||||||
|
if self.sprite[pos + 2 * y + 1] & position:
|
||||||
|
val += 2
|
||||||
|
if self.sprite[pos + 2 * y + 16] & position:
|
||||||
|
val += 4
|
||||||
|
if self.sprite[pos + 2 * y + 17] & position:
|
||||||
|
val += 8
|
||||||
|
arr[y][x] = val
|
||||||
|
return arr
|
||||||
|
|
||||||
|
def decode16(self, pos):
|
||||||
|
arr = [[0 for _ in range(16)] for _ in range(16)]
|
||||||
|
top_left = self.decode8(pos)
|
||||||
|
top_right = self.decode8(pos + 0x20)
|
||||||
|
bottom_left = self.decode8(pos + 0x200)
|
||||||
|
bottom_right = self.decode8(pos + 0x220)
|
||||||
|
for x in range(8):
|
||||||
|
for y in range(8):
|
||||||
|
arr[y][x] = top_left[y][x]
|
||||||
|
arr[y][x + 8] = top_right[y][x]
|
||||||
|
arr[y + 8][x] = bottom_left[y][x]
|
||||||
|
arr[y + 8][x + 8] = bottom_right[y][x]
|
||||||
|
return arr
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_zspr(filedata, expected_kind):
|
||||||
|
logger = logging.getLogger("ZSPR")
|
||||||
|
headerstr = "<4xBHHIHIHH6x"
|
||||||
|
headersize = struct.calcsize(headerstr)
|
||||||
|
if len(filedata) < headersize:
|
||||||
|
return None
|
||||||
|
version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from(
|
||||||
|
headerstr, filedata)
|
||||||
|
if version not in [1]:
|
||||||
|
logger.error("Error parsing ZSPR file: Version %g not supported", version)
|
||||||
|
return None
|
||||||
|
if kind != expected_kind:
|
||||||
|
return None
|
||||||
|
|
||||||
|
stream = io.BytesIO(filedata)
|
||||||
|
stream.seek(headersize)
|
||||||
|
|
||||||
|
def read_utf16le(stream):
|
||||||
|
"""Decodes a null-terminated UTF-16_LE string of unknown size from a stream"""
|
||||||
|
raw = bytearray()
|
||||||
|
while True:
|
||||||
|
char = stream.read(2)
|
||||||
|
if char in [b"", b"\x00\x00"]:
|
||||||
|
break
|
||||||
|
raw += char
|
||||||
|
return raw.decode("utf-16_le")
|
||||||
|
|
||||||
|
# noinspection PyBroadException
|
||||||
|
try:
|
||||||
|
sprite_name = read_utf16le(stream)
|
||||||
|
author_name = read_utf16le(stream)
|
||||||
|
author_credits_name = stream.read().split(b"\x00", 1)[0].decode()
|
||||||
|
|
||||||
|
# Ignoring the Author Rom name for the time being.
|
||||||
|
|
||||||
|
real_csum = sum(filedata) % 0x10000
|
||||||
|
if real_csum != csum or real_csum ^ 0xFFFF != icsum:
|
||||||
|
logger.warning("ZSPR file has incorrect checksum. It may be corrupted.")
|
||||||
|
|
||||||
|
sprite = filedata[sprite_offset:sprite_offset + sprite_size]
|
||||||
|
palette = filedata[palette_offset:palette_offset + palette_size]
|
||||||
|
|
||||||
|
if len(sprite) != sprite_size or len(palette) != palette_size:
|
||||||
|
logger.error("Error parsing ZSPR file: Unexpected end of file")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return sprite, palette, sprite_name, author_name, author_credits_name
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error parsing ZSPR file")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def decode_palette(self):
|
||||||
|
"""Returns the palettes as an array of arrays of 15 colors"""
|
||||||
|
|
||||||
|
def array_chunk(arr, size):
|
||||||
|
return list(zip(*[iter(arr)] * size))
|
||||||
|
|
||||||
|
def make_int16(pair):
|
||||||
|
return pair[1] << 8 | pair[0]
|
||||||
|
|
||||||
|
def expand_color(i):
|
||||||
|
return (i & 0x1F) * 8, (i >> 5 & 0x1F) * 8, (i >> 10 & 0x1F) * 8
|
||||||
|
|
||||||
|
# turn palette data into a list of RGB tuples with 8 bit values
|
||||||
|
palette_as_colors = [expand_color(make_int16(chnk)) for chnk in array_chunk(self.palette, 2)]
|
||||||
|
|
||||||
|
# split into palettes of 15 colors
|
||||||
|
return array_chunk(palette_as_colors, 15)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.name)
|
||||||
|
|
||||||
|
def write_to_rom(self, rom: "LocalRom"):
|
||||||
|
if not self.valid:
|
||||||
|
logging.warning("Tried writing invalid sprite to rom, skipping.")
|
||||||
|
return
|
||||||
|
rom.write_bytes(0x80000, self.sprite)
|
||||||
|
rom.write_bytes(0xDD308, self.palette)
|
||||||
|
rom.write_bytes(0xDEDF5, self.glove_palette)
|
||||||
|
rom.write_bytes(0x300000, self.sprite)
|
||||||
|
rom.write_bytes(0x307000, self.palette)
|
||||||
|
rom.write_bytes(0x307078, self.glove_palette)
|
||||||
|
|
||||||
|
|
||||||
|
def update_sprites():
|
||||||
|
from tkinter import Tk
|
||||||
|
from LttPAdjuster import get_image_for_sprite
|
||||||
|
from LttPAdjuster import BackgroundTaskProgress
|
||||||
|
from LttPAdjuster import BackgroundTaskProgressNullWindow
|
||||||
|
from LttPAdjuster import update_sprites
|
||||||
|
|
||||||
|
# Target directories
|
||||||
|
input_dir = user_path("data", "sprites", "alttpr")
|
||||||
|
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
|
||||||
|
|
||||||
|
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
|
||||||
|
# update sprites through gui.py's functions
|
||||||
|
done = threading.Event()
|
||||||
|
try:
|
||||||
|
top = Tk()
|
||||||
|
except:
|
||||||
|
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
|
||||||
|
else:
|
||||||
|
top.withdraw()
|
||||||
|
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
|
||||||
|
while not done.isSet():
|
||||||
|
task.do_events()
|
||||||
|
|
||||||
|
spriteData = []
|
||||||
|
|
||||||
|
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
|
||||||
|
sprite = Sprite(os.path.join(input_dir, file))
|
||||||
|
|
||||||
|
if not sprite.name:
|
||||||
|
print("Warning:", file, "has no name.")
|
||||||
|
sprite.name = file.split(".", 1)[0]
|
||||||
|
if sprite.valid:
|
||||||
|
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
|
||||||
|
image.write(get_image_for_sprite(sprite, True))
|
||||||
|
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
|
||||||
|
else:
|
||||||
|
print(file, "dropped, as it has no valid sprite data.")
|
||||||
|
spriteData.sort(key=lambda entry: entry["name"])
|
||||||
|
with open(f'{output_dir}/spriteData.json', 'w') as file:
|
||||||
|
json.dump({"sprites": spriteData}, file, indent=1)
|
||||||
|
return spriteData
|
||||||
|
|
||||||
|
|
||||||
|
def apply_random_sprite_on_event(rom: "LocalRom", sprite, local_random, allow_random_on_event, sprite_pool):
|
||||||
|
userandomsprites = False
|
||||||
|
if sprite and not isinstance(sprite, Sprite):
|
||||||
|
sprite = sprite.lower()
|
||||||
|
userandomsprites = sprite.startswith('randomon')
|
||||||
|
|
||||||
|
racerom = rom.read_byte(0x180213)
|
||||||
|
if allow_random_on_event or not racerom:
|
||||||
|
# Changes to this byte for race rom seeds are only permitted on initial rolling of the seed.
|
||||||
|
# However, if the seed is not a racerom seed, then it is always allowed.
|
||||||
|
rom.write_byte(0x186381, 0x00 if userandomsprites else 0x01)
|
||||||
|
|
||||||
|
onevent = 0
|
||||||
|
if sprite == 'randomonall':
|
||||||
|
onevent = 0xFFFF # Support all current and future events that can cause random sprite changes.
|
||||||
|
elif sprite == 'randomonnone':
|
||||||
|
# Allows for opting into random on events on race rom seeds, without actually enabling any of the events initially.
|
||||||
|
onevent = 0x0000
|
||||||
|
elif sprite == 'randomonrandom':
|
||||||
|
# Allows random to take the wheel on which events apply. (at least one event will be applied.)
|
||||||
|
onevent = local_random.randint(0x0001, 0x003F)
|
||||||
|
elif userandomsprites:
|
||||||
|
onevent = 0x01 if 'hit' in sprite else 0x00
|
||||||
|
onevent += 0x02 if 'enter' in sprite else 0x00
|
||||||
|
onevent += 0x04 if 'exit' in sprite else 0x00
|
||||||
|
onevent += 0x08 if 'slash' in sprite else 0x00
|
||||||
|
onevent += 0x10 if 'item' in sprite else 0x00
|
||||||
|
onevent += 0x20 if 'bonk' in sprite else 0x00
|
||||||
|
|
||||||
|
rom.write_int16(0x18637F, onevent)
|
||||||
|
|
||||||
|
sprite = Sprite(sprite) if os.path.isfile(sprite) else Sprite.get_sprite_from_name(sprite, local_random)
|
||||||
|
|
||||||
|
# write link sprite if required
|
||||||
|
if sprite:
|
||||||
|
sprites = list()
|
||||||
|
sprite.write_to_rom(rom)
|
||||||
|
|
||||||
|
_populate_sprite_table()
|
||||||
|
if userandomsprites:
|
||||||
|
if sprite_pool:
|
||||||
|
if isinstance(sprite_pool, str):
|
||||||
|
sprite_pool = sprite_pool.split(':')
|
||||||
|
for spritename in sprite_pool:
|
||||||
|
sprite = Sprite(spritename) if os.path.isfile(spritename) else Sprite.get_sprite_from_name(
|
||||||
|
spritename, local_random)
|
||||||
|
if sprite:
|
||||||
|
sprites.append(sprite)
|
||||||
|
else:
|
||||||
|
logging.info(f"Sprite {spritename} was not found.")
|
||||||
|
else:
|
||||||
|
sprites = list(set(_sprite_table.values())) # convert to list and remove dupes
|
||||||
|
else:
|
||||||
|
sprites.append(sprite)
|
||||||
|
if sprites:
|
||||||
|
while len(sprites) < 32:
|
||||||
|
sprites.extend(sprites)
|
||||||
|
local_random.shuffle(sprites)
|
||||||
|
|
||||||
|
for i, sprite in enumerate(sprites[:32]):
|
||||||
|
if not i and not userandomsprites:
|
||||||
|
continue
|
||||||
|
rom.write_bytes(0x300000 + (i * 0x8000), sprite.sprite)
|
||||||
|
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||||
|
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||||
@@ -124,6 +124,14 @@ class ALTTPWeb(WebWorld):
|
|||||||
|
|
||||||
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
|
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run_webhost_setup(cls):
|
||||||
|
rom_file = get_base_rom_path()
|
||||||
|
if os.path.exists(rom_file):
|
||||||
|
from .Sprites import update_sprites
|
||||||
|
update_sprites()
|
||||||
|
else:
|
||||||
|
logging.warning("Could not update LttP sprites.")
|
||||||
|
|
||||||
class ALTTPWorld(World):
|
class ALTTPWorld(World):
|
||||||
"""
|
"""
|
||||||
@@ -807,7 +815,6 @@ class ALTTPWorld(World):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return slot_data
|
return slot_data
|
||||||
|
|
||||||
|
|
||||||
def get_same_seed(world, seed_def: tuple) -> str:
|
def get_same_seed(world, seed_def: tuple) -> str:
|
||||||
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
|
||||||
|
|||||||
@@ -367,25 +367,25 @@ def can_beat_boss(state: CollectionState, boss: str, logic: int, player: int) ->
|
|||||||
elif boss == "Graveyard":
|
elif boss == "Graveyard":
|
||||||
return (
|
return (
|
||||||
has_boss_strength("amanecida")
|
has_boss_strength("amanecida")
|
||||||
and state.has_all({"D01Z06S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
|
and state.has_all({"D01BZ07S01[Santos]", "D02Z03S23[E]", "D02Z02S14[W]", "Wall Climb Ability"}, player)
|
||||||
)
|
)
|
||||||
elif boss == "Jondo":
|
elif boss == "Jondo":
|
||||||
return (
|
return (
|
||||||
has_boss_strength("amanecida")
|
has_boss_strength("amanecida")
|
||||||
and state.has("D01Z06S01[Santos]", player)
|
and state.has("D01BZ07S01[Santos]", player)
|
||||||
and state.has_any({"D20Z01S05[W]", "D20Z01S05[E]"}, player)
|
and state.has_any({"D20Z01S05[W]", "D20Z01S05[E]"}, player)
|
||||||
and state.has_any({"D03Z01S03[W]", "D03Z01S03[SW]"}, player)
|
and state.has_any({"D03Z01S03[W]", "D03Z01S03[SW]"}, player)
|
||||||
)
|
)
|
||||||
elif boss == "Patio":
|
elif boss == "Patio":
|
||||||
return (
|
return (
|
||||||
has_boss_strength("amanecida")
|
has_boss_strength("amanecida")
|
||||||
and state.has_all({"D01Z06S01[Santos]", "D06Z01S18[E]"}, player)
|
and state.has_all({"D01BZ07S01[Santos]", "D06Z01S18[E]"}, player)
|
||||||
and state.has_any({"D04Z01S04[W]", "D04Z01S04[E]", "D04Z01S04[Cherubs]"}, player)
|
and state.has_any({"D04Z01S04[W]", "D04Z01S04[E]", "D04Z01S04[Cherubs]"}, player)
|
||||||
)
|
)
|
||||||
elif boss == "Wall":
|
elif boss == "Wall":
|
||||||
return (
|
return (
|
||||||
has_boss_strength("amanecida")
|
has_boss_strength("amanecida")
|
||||||
and state.has_all({"D01Z06S01[Santos]", "D09BZ01S01[Cell24]"}, player)
|
and state.has_all({"D01BZ07S01[Santos]", "D09BZ01S01[Cell24]"}, player)
|
||||||
and state.has_any({"D09Z01S01[W]", "D09Z01S01[E]"}, player)
|
and state.has_any({"D09Z01S01[W]", "D09Z01S01[E]"}, player)
|
||||||
)
|
)
|
||||||
elif boss == "Hall":
|
elif boss == "Hall":
|
||||||
@@ -2451,8 +2451,6 @@ def rules(blasphemousworld):
|
|||||||
# Items
|
# Items
|
||||||
set_rule(world.get_location("PotSS: 4th meeting with Redento", player),
|
set_rule(world.get_location("PotSS: 4th meeting with Redento", player),
|
||||||
lambda state: redento(state, blasphemousworld, player, 4))
|
lambda state: redento(state, blasphemousworld, player, 4))
|
||||||
set_rule(world.get_location("PotSS: Amanecida of the Chiselled Steel", player),
|
|
||||||
lambda state: can_beat_boss(state, "Patio", logic, player))
|
|
||||||
# No doors
|
# No doors
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
## Required Software
|
## Required Software
|
||||||
|
|
||||||
Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases), or from the [Bumper Stickers AP Itch page](https://kewliomzx.itch.io/bumpstik-ap), where you can also play it in your browser.
|
Download the game from the [Bumper Stickers GitHub releases page](https://github.com/FelicitusNeko/FlixelBumpStik/releases).
|
||||||
|
|
||||||
|
*A web version will be made available on itch.io at a later time.*
|
||||||
|
|
||||||
## Installation Procedures
|
## Installation Procedures
|
||||||
|
|
||||||
Simply download the latest version of Bumper Stickers from the link above, and extract it wherever you like.
|
Simply download the latest version of Bumper Stickers from the link above, and extract it wherever you like.
|
||||||
|
|
||||||
- ⚠️ It is not recommended to copy this game, or any files, directly into your Program Files folder under Windows.
|
- ⚠️ Do not extract Bumper Stickers to Program Files, as this will cause file access issues.
|
||||||
|
|
||||||
## Joining a Multiworld Game
|
## Joining a Multiworld Game
|
||||||
|
|
||||||
1. Run `BumpStikAP.exe`.
|
1. Run `BumpStik-AP.exe`.
|
||||||
2. Select "Archipelago Mode".
|
2. Select "Archipelago Mode".
|
||||||
3. Enter your server details in the fields provided, and click "Start".
|
3. Enter your server details in the fields provided, and click "Start".
|
||||||
- The game will attempt to automatically detect whether to connect via normal (WS) or secure (WSS) server, but you can specify `ws://` or `wss://` to prioritise one or the other.
|
- ※ If you are connecting to a WSS server (such as archipelago.gg), specify `wss://` in the host name. Otherwise, the game will assume `ws://`.
|
||||||
|
|
||||||
## How to play Bumper Stickers (Classic)
|
## How to play Bumper Stickers (Classic)
|
||||||
|
|
||||||
|
|||||||