Compare commits
99 Commits
custom_web
...
misc-webho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c06d1193a | ||
|
|
6b48f9aac5 | ||
|
|
bc11c9dfd4 | ||
|
|
24403eba1b | ||
|
|
e377068d1f | ||
|
|
c7c94eebeb | ||
|
|
9d38725688 | ||
|
|
18bf7425c4 | ||
|
|
17127a4117 | ||
|
|
5d9b47355e | ||
|
|
f9761ad4e5 | ||
|
|
485aa23afd | ||
|
|
e08deff6f9 | ||
|
|
d5d630dcf0 | ||
|
|
58b696e986 | ||
|
|
a3907e800b | ||
|
|
5c640c6c52 | ||
|
|
fe6096464c | ||
|
|
5bf3de45f4 | ||
|
|
f33babc420 | ||
|
|
1c9199761b | ||
|
|
368fa64914 | ||
|
|
e114ed5566 | ||
|
|
5d47c5b316 | ||
|
|
812dc413e5 | ||
|
|
4aed2be93b | ||
|
|
974bab2b24 | ||
|
|
98d61b32af | ||
|
|
4141a50d8c | ||
|
|
93c18cd9a7 | ||
|
|
5af47425b0 | ||
|
|
b41a1e69b4 | ||
|
|
124113f3d3 | ||
|
|
2b69820619 | ||
|
|
f147f9e5a0 | ||
|
|
db7c0c9db9 | ||
|
|
b40fba0840 | ||
|
|
ea799c494e | ||
|
|
b4b8426def | ||
|
|
39a50da55c | ||
|
|
9931605f94 | ||
|
|
8834ba88aa | ||
|
|
5e46967b7d | ||
|
|
638d6807db | ||
|
|
d471dcc067 | ||
|
|
4a27fae1ab | ||
|
|
794959e182 | ||
|
|
aff852fb45 | ||
|
|
a0eea3a650 | ||
|
|
0012584e51 | ||
|
|
6e02a4ca3c | ||
|
|
2ef05a1799 | ||
|
|
fa2891f785 | ||
|
|
d5d13a6d4d | ||
|
|
b24037e9d9 | ||
|
|
6d6de4a98e | ||
|
|
0e7c7bd1bf | ||
|
|
9312f14ffb | ||
|
|
ce8f07b347 | ||
|
|
cff6c7c4da | ||
|
|
f9120c620f | ||
|
|
44f1a93d31 | ||
|
|
6d61eae522 | ||
|
|
f05a9ecd2f | ||
|
|
648d682add | ||
|
|
47cf3e06c0 | ||
|
|
fdac50523b | ||
|
|
7522a32ad6 | ||
|
|
8ee743ac8a | ||
|
|
c3cfbf8e1c | ||
|
|
1756a30acc | ||
|
|
57c13ff273 | ||
|
|
3d9837678c | ||
|
|
3e95ccd06c | ||
|
|
0e21a3e121 | ||
|
|
5eef7a34d3 | ||
|
|
6c844750ae | ||
|
|
8649b15787 | ||
|
|
fbd64651e4 | ||
|
|
e01eb4e00c | ||
|
|
72b44be41c | ||
|
|
2bdb1b2029 | ||
|
|
bf685dc850 | ||
|
|
faf4887616 | ||
|
|
a1418ccb66 | ||
|
|
29f8053d6e | ||
|
|
f6dafa2b56 | ||
|
|
2b9e8fa273 | ||
|
|
5368451867 | ||
|
|
77a349c1c6 | ||
|
|
c4a3204af7 | ||
|
|
9323f7d892 | ||
|
|
30e747bb4c | ||
|
|
9d29c6d301 | ||
|
|
aa19a79d26 | ||
|
|
5a34471266 | ||
|
|
ae96010ff1 | ||
|
|
944fe6cb8c | ||
|
|
21baa302d4 |
4
.github/workflows/unittests.yml
vendored
@@ -54,9 +54,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install pytest pytest-subtests
|
pip install pytest pytest-subtests pytest-xdist
|
||||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||||
python Launcher.py --update_settings # make sure host.yaml exists for tests
|
python Launcher.py --update_settings # make sure host.yaml exists for tests
|
||||||
- name: Unittests
|
- name: Unittests
|
||||||
run: |
|
run: |
|
||||||
pytest
|
pytest -n auto
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -27,16 +27,20 @@
|
|||||||
*.archipelago
|
*.archipelago
|
||||||
*.apsave
|
*.apsave
|
||||||
*.BIN
|
*.BIN
|
||||||
|
*.puml
|
||||||
|
|
||||||
setups
|
setups
|
||||||
build
|
build
|
||||||
bundle/components.wxs
|
bundle/components.wxs
|
||||||
dist
|
dist
|
||||||
|
/prof/
|
||||||
README.html
|
README.html
|
||||||
.vs/
|
.vs/
|
||||||
EnemizerCLI/
|
EnemizerCLI/
|
||||||
/Players/
|
/Players/
|
||||||
/SNI/
|
/SNI/
|
||||||
|
/sni-*/
|
||||||
|
/appimagetool*
|
||||||
/host.yaml
|
/host.yaml
|
||||||
/options.yaml
|
/options.yaml
|
||||||
/config.yaml
|
/config.yaml
|
||||||
@@ -139,6 +143,7 @@ ipython_config.py
|
|||||||
.venv*
|
.venv*
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
|
/venv*/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import secrets
|
|||||||
import typing # this can go away when Python 3.8 support is dropped
|
import typing # this can go away when Python 3.8 support is dropped
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from collections import ChainMap, Counter, deque
|
from collections import ChainMap, Counter, deque
|
||||||
|
from collections.abc import Collection
|
||||||
from enum import IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \
|
||||||
Type, ClassVar
|
Type, ClassVar
|
||||||
@@ -202,14 +203,7 @@ class MultiWorld():
|
|||||||
self.player_types[new_id] = NetUtils.SlotType.group
|
self.player_types[new_id] = NetUtils.SlotType.group
|
||||||
self._region_cache[new_id] = {}
|
self._region_cache[new_id] = {}
|
||||||
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
||||||
for option_key, option in world_type.option_definitions.items():
|
self.worlds[new_id] = world_type.create_group(self, new_id, players)
|
||||||
getattr(self, option_key)[new_id] = option(option.default)
|
|
||||||
for option_key, option in Options.common_options.items():
|
|
||||||
getattr(self, option_key)[new_id] = option(option.default)
|
|
||||||
for option_key, option in Options.per_game_common_options.items():
|
|
||||||
getattr(self, option_key)[new_id] = option(option.default)
|
|
||||||
|
|
||||||
self.worlds[new_id] = world_type(self, new_id)
|
|
||||||
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
|
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id])
|
||||||
self.player_name[new_id] = name
|
self.player_name[new_id] = name
|
||||||
|
|
||||||
@@ -364,7 +358,7 @@ class MultiWorld():
|
|||||||
for r_location in region.locations:
|
for r_location in region.locations:
|
||||||
self._location_cache[r_location.name, player] = r_location
|
self._location_cache[r_location.name, player] = r_location
|
||||||
|
|
||||||
def get_regions(self, player=None):
|
def get_regions(self, player: Optional[int] = None) -> Collection[Region]:
|
||||||
return self.regions if player is None else self._region_cache[player].values()
|
return self.regions if player is None else self._region_cache[player].values()
|
||||||
|
|
||||||
def get_region(self, regionname: str, player: int) -> Region:
|
def get_region(self, regionname: str, player: int) -> Region:
|
||||||
@@ -853,14 +847,6 @@ 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
|
||||||
|
|||||||
9
BizHawkClient.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ModuleUpdate
|
||||||
|
ModuleUpdate.update()
|
||||||
|
|
||||||
|
from worlds._bizhawk.context import launch
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
launch()
|
||||||
14
Fill.py
@@ -753,8 +753,6 @@ def distribute_planned(world: MultiWorld) -> None:
|
|||||||
else: # not reachable with swept state
|
else: # not reachable with swept state
|
||||||
non_early_locations[loc.player].append(loc.name)
|
non_early_locations[loc.player].append(loc.name)
|
||||||
|
|
||||||
# TODO: remove. Preferably by implementing key drop
|
|
||||||
from worlds.alttp.Regions import key_drop_data
|
|
||||||
world_name_lookup = world.world_name_lookup
|
world_name_lookup = world.world_name_lookup
|
||||||
|
|
||||||
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
|
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
|
||||||
@@ -840,12 +838,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 player in worlds:
|
for target_player in worlds:
|
||||||
locations += early_locations[player]
|
locations += early_locations[target_player]
|
||||||
if "non_early_locations" in locations:
|
if "non_early_locations" in locations:
|
||||||
locations.remove("non_early_locations")
|
locations.remove("non_early_locations")
|
||||||
for player in worlds:
|
for target_player in worlds:
|
||||||
locations += non_early_locations[player]
|
locations += non_early_locations[target_player]
|
||||||
|
|
||||||
block['locations'] = locations
|
block['locations'] = locations
|
||||||
|
|
||||||
@@ -897,10 +895,6 @@ def distribute_planned(world: MultiWorld) -> None:
|
|||||||
for item_name in items:
|
for item_name in items:
|
||||||
item = world.worlds[player].create_item(item_name)
|
item = world.worlds[player].create_item(item_name)
|
||||||
for location in reversed(candidates):
|
for location in reversed(candidates):
|
||||||
if location in key_drop_data:
|
|
||||||
warn(
|
|
||||||
f"Can't place '{item_name}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
|
|
||||||
continue
|
|
||||||
if not location.item:
|
if not location.item:
|
||||||
if location.item_rule(item):
|
if location.item_rule(item):
|
||||||
if location.can_fill(world.state, item, False):
|
if location.can_fill(world.state, item, False):
|
||||||
|
|||||||
33
Launcher.py
@@ -50,17 +50,22 @@ def open_host_yaml():
|
|||||||
def open_patch():
|
def open_patch():
|
||||||
suffixes = []
|
suffixes = []
|
||||||
for c in components:
|
for c in components:
|
||||||
if isfile(get_exe(c)[-1]):
|
if c.type == Type.CLIENT and \
|
||||||
suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \
|
isinstance(c.file_identifier, SuffixIdentifier) and \
|
||||||
isinstance(c.file_identifier, SuffixIdentifier) else []
|
(c.script_name is None or isfile(get_exe(c)[-1])):
|
||||||
|
suffixes += c.file_identifier.suffixes
|
||||||
try:
|
try:
|
||||||
filename = open_filename('Select patch', (('Patches', suffixes),))
|
filename = open_filename("Select patch", (("Patches", suffixes),))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox('Error', str(e), error=True)
|
messagebox("Error", str(e), error=True)
|
||||||
else:
|
else:
|
||||||
file, component = identify(filename)
|
file, component = identify(filename)
|
||||||
if file and component:
|
if file and component:
|
||||||
launch([*get_exe(component), file], component.cli)
|
exe = get_exe(component)
|
||||||
|
if exe is None or not isfile(exe[-1]):
|
||||||
|
exe = get_exe("Launcher")
|
||||||
|
|
||||||
|
launch([*exe, file], component.cli)
|
||||||
|
|
||||||
|
|
||||||
def generate_yamls():
|
def generate_yamls():
|
||||||
@@ -107,7 +112,7 @@ def identify(path: Union[None, str]):
|
|||||||
return None, None
|
return None, None
|
||||||
for component in components:
|
for component in components:
|
||||||
if component.handles_file(path):
|
if component.handles_file(path):
|
||||||
return path, component
|
return path, component
|
||||||
elif path == component.display_name or path == component.script_name:
|
elif path == component.display_name or path == component.script_name:
|
||||||
return None, component
|
return None, component
|
||||||
return None, None
|
return None, None
|
||||||
@@ -117,25 +122,25 @@ def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
|
|||||||
if isinstance(component, str):
|
if isinstance(component, str):
|
||||||
name = component
|
name = component
|
||||||
component = None
|
component = None
|
||||||
if name.startswith('Archipelago'):
|
if name.startswith("Archipelago"):
|
||||||
name = name[11:]
|
name = name[11:]
|
||||||
if name.endswith('.exe'):
|
if name.endswith(".exe"):
|
||||||
name = name[:-4]
|
name = name[:-4]
|
||||||
if name.endswith('.py'):
|
if name.endswith(".py"):
|
||||||
name = name[:-3]
|
name = name[:-3]
|
||||||
if not name:
|
if not name:
|
||||||
return None
|
return None
|
||||||
for c in components:
|
for c in components:
|
||||||
if c.script_name == name or c.frozen_name == f'Archipelago{name}':
|
if c.script_name == name or c.frozen_name == f"Archipelago{name}":
|
||||||
component = c
|
component = c
|
||||||
break
|
break
|
||||||
if not component:
|
if not component:
|
||||||
return None
|
return None
|
||||||
if is_frozen():
|
if is_frozen():
|
||||||
suffix = '.exe' if is_windows else ''
|
suffix = ".exe" if is_windows else ""
|
||||||
return [local_path(f'{component.frozen_name}{suffix}')]
|
return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None
|
||||||
else:
|
else:
|
||||||
return [sys.executable, local_path(f'{component.script_name}.py')]
|
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
|
||||||
|
|
||||||
|
|
||||||
def launch(exe, in_terminal=False):
|
def launch(exe, in_terminal=False):
|
||||||
|
|||||||
15
Main.py
@@ -139,7 +139,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
exclusion_rules(world, player, world.exclude_locations[player].value)
|
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:
|
||||||
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
|
try:
|
||||||
|
location = world.get_location(location_name, player)
|
||||||
|
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
||||||
|
if location_name not in world.worlds[player].location_name_to_id:
|
||||||
|
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
|
||||||
|
else:
|
||||||
|
location.progress_type = LocationProgressType.PRIORITY
|
||||||
|
|
||||||
# Set local and non-local item rules.
|
# Set local and non-local item rules.
|
||||||
if world.players > 1:
|
if world.players > 1:
|
||||||
@@ -159,7 +165,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for player, items in depletion_pool.items():
|
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():
|
||||||
new_items.append(player_world.create_filler())
|
for _ in range(count):
|
||||||
|
new_items.append(player_world.create_filler())
|
||||||
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
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):
|
||||||
@@ -179,6 +186,7 @@ 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
|
||||||
@@ -392,7 +400,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
f.write(bytes([3])) # version of format
|
f.write(bytes([3])) # version of format
|
||||||
f.write(multidata)
|
f.write(multidata)
|
||||||
|
|
||||||
multidata_task = pool.submit(write_multidata)
|
output_file_futures.append(pool.submit(write_multidata))
|
||||||
if not check_accessibility_task.result():
|
if not 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.")
|
||||||
@@ -400,7 +408,6 @@ 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)}).')
|
||||||
|
|||||||
24
NetUtils.py
@@ -407,14 +407,22 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
|
|||||||
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
|
if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub
|
||||||
LocationStore = _LocationStore
|
LocationStore = _LocationStore
|
||||||
else:
|
else:
|
||||||
try:
|
|
||||||
import pyximport
|
|
||||||
pyximport.install()
|
|
||||||
except ImportError:
|
|
||||||
pyximport = None
|
|
||||||
try:
|
try:
|
||||||
from _speedups import LocationStore
|
from _speedups import LocationStore
|
||||||
|
import _speedups
|
||||||
|
import os.path
|
||||||
|
if os.path.isfile("_speedups.pyx") and os.path.getctime(_speedups.__file__) < os.path.getctime("_speedups.pyx"):
|
||||||
|
warnings.warn(f"{_speedups.__file__} outdated! "
|
||||||
|
f"Please rebuild with `cythonize -b -i _speedups.pyx` or delete it!")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
|
try:
|
||||||
"Install a matching C++ compiler for your platform to compile _speedups.")
|
import pyximport
|
||||||
LocationStore = _LocationStore
|
pyximport.install()
|
||||||
|
except ImportError:
|
||||||
|
pyximport = None
|
||||||
|
try:
|
||||||
|
from _speedups import LocationStore
|
||||||
|
except ImportError:
|
||||||
|
warnings.warn("_speedups not available. Falling back to pure python LocationStore. "
|
||||||
|
"Install a matching C++ compiler for your platform to compile _speedups.")
|
||||||
|
LocationStore = _LocationStore
|
||||||
|
|||||||
23
SNIClient.py
@@ -68,12 +68,11 @@ class SNIClientCommandProcessor(ClientCommandProcessor):
|
|||||||
options = snes_options.split()
|
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:
|
||||||
@@ -565,14 +564,16 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
|
|||||||
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
|
||||||
try:
|
try:
|
||||||
for address, data in write_list:
|
for address, data in write_list:
|
||||||
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
|
while data:
|
||||||
# REVIEW: above: `if snes_socket is None: return False`
|
# Divide the write into packets of 256 bytes.
|
||||||
# Does it need to be checked again?
|
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
|
||||||
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)
|
await ctx.snes_socket.send(data[:256])
|
||||||
else:
|
address += 256
|
||||||
snes_logger.warning(f"Could not send data to SNES: {data}")
|
data = data[256:]
|
||||||
|
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.getcwd() + "\\Undertale", exist_ok=True)
|
os.makedirs(name=os.path.join(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):
|
||||||
UndertaleContext.save_game_folder = directory
|
self.ctx.save_game_folder = directory
|
||||||
self.output("Changed to the following directory: " + directory)
|
self.output("Changed to the following directory: " + self.ctx.save_game_folder)
|
||||||
|
|
||||||
@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.getcwd() + "\\Undertale", exist_ok=True)
|
os.makedirs(name=os.path.join(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("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
if not os.path.exists(tempInstall):
|
||||||
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("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Undertale"):
|
if not os.path.exists(tempInstall):
|
||||||
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(tempInstall+"\\"+file_name,
|
shutil.copy(os.path.join(tempInstall, file_name),
|
||||||
os.getcwd() + "\\Undertale\\" + file_name)
|
os.path.join(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.getcwd() + "/Undertale/data.win", "rb") as f:
|
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f:
|
||||||
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff"))
|
||||||
with open(os.getcwd() + "/Undertale/data.win", "wb") as f:
|
with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f:
|
||||||
f.write(patchedFile)
|
f.write(patchedFile)
|
||||||
os.makedirs(name=os.getcwd() + "\\Undertale\\" + "Custom Sprites", exist_ok=True)
|
os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True)
|
||||||
with open(os.path.expandvars(os.getcwd() + "\\Undertale\\" + "Custom Sprites\\" +
|
with open(os.path.expandvars(os.path.join(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(root + "/" + file, "r") as mine:
|
with open(os.path.join(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(root+"/"+file)
|
os.remove(os.path.join(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(root+"/"+file)
|
os.remove(os.path.join(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(root+"/"+file, "r") as f:
|
with open(os.path.join(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(root+"/"+file)
|
os.remove(os.path.join(root, file))
|
||||||
if "check.spot" in file:
|
if "check.spot" in file:
|
||||||
sending = []
|
sending = []
|
||||||
try:
|
try:
|
||||||
with open(root+"/"+file, "r") as f:
|
with open(os.path.join(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(root+"/"+file)
|
os.remove(os.path.join(root, file))
|
||||||
if "victory" in file:
|
if "victory" in file:
|
||||||
if str(ctx.route) == "all_routes":
|
if str(ctx.route) == "all_routes":
|
||||||
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
if "neutral" in file and ctx.completed_routes["neutral"] != 1:
|
||||||
|
|||||||
157
Utils.py
@@ -13,6 +13,7 @@ import io
|
|||||||
import collections
|
import collections
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from settings import Settings, get_settings
|
from settings import Settings, get_settings
|
||||||
@@ -29,6 +30,7 @@ except ImportError:
|
|||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
import tkinter
|
import tkinter
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from BaseClasses import Region
|
||||||
|
|
||||||
|
|
||||||
def tuplize_version(version: str) -> Version:
|
def tuplize_version(version: str) -> Version:
|
||||||
@@ -44,7 +46,7 @@ class Version(typing.NamedTuple):
|
|||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.4.2"
|
__version__ = "0.4.3"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
@@ -215,7 +217,13 @@ def get_cert_none_ssl_context():
|
|||||||
def get_public_ipv4() -> str:
|
def get_public_ipv4() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
ip = socket.gethostbyname(socket.gethostname())
|
try:
|
||||||
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
except socket.gaierror:
|
||||||
|
# if hostname or resolvconf is not set up properly, this may fail
|
||||||
|
warnings.warn("Could not resolve own hostname, falling back to 127.0.0.1")
|
||||||
|
ip = "127.0.0.1"
|
||||||
|
|
||||||
ctx = get_cert_none_ssl_context()
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
|
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||||
@@ -233,7 +241,13 @@ def get_public_ipv4() -> str:
|
|||||||
def get_public_ipv6() -> str:
|
def get_public_ipv6() -> str:
|
||||||
import socket
|
import socket
|
||||||
import urllib.request
|
import urllib.request
|
||||||
ip = socket.gethostbyname(socket.gethostname())
|
try:
|
||||||
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
|
except socket.gaierror:
|
||||||
|
# if hostname or resolvconf is not set up properly, this may fail
|
||||||
|
warnings.warn("Could not resolve own hostname, falling back to ::1")
|
||||||
|
ip = "::1"
|
||||||
|
|
||||||
ctx = get_cert_none_ssl_context()
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
|
||||||
@@ -359,11 +373,13 @@ 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 = importlib.import_module("worlds.generic")
|
self.generic_properties_module = None
|
||||||
|
|
||||||
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:
|
||||||
@@ -373,6 +389,8 @@ 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"):
|
||||||
@@ -572,7 +590,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
|
||||||
@@ -584,7 +602,10 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
|||||||
f'This attempt was made because open_filename was used for "{title}".')
|
f'This attempt was made because open_filename was used for "{title}".')
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
root = tkinter.Tk()
|
try:
|
||||||
|
root = tkinter.Tk()
|
||||||
|
except tkinter.TclError:
|
||||||
|
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||||
root.withdraw()
|
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)
|
||||||
@@ -597,13 +618,14 @@ 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 = None#which("kdialog")
|
kdialog = which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
return run(kdialog, f"--title={title}", "--getexistingdirectory", suggest or ".")
|
return run(kdialog, f"--title={title}", "--getexistingdirectory",
|
||||||
zenity = None#which("zenity")
|
os.path.abspath(suggest) if suggest else ".")
|
||||||
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
z_filters = ("--directory",)
|
z_filters = ("--directory",)
|
||||||
selection = (f'--filename="{suggest}',) if suggest else ()
|
selection = (f"--filename={os.path.abspath(suggest)}/",) if suggest else ()
|
||||||
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
return run(zenity, f"--title={title}", "--file-selection", *z_filters, *selection)
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
@@ -615,7 +637,10 @@ 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:
|
||||||
root = tkinter.Tk()
|
try:
|
||||||
|
root = tkinter.Tk()
|
||||||
|
except tkinter.TclError:
|
||||||
|
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||||
root.withdraw()
|
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)
|
||||||
|
|
||||||
@@ -755,3 +780,113 @@ def freeze_support() -> None:
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
_extend_freeze_support()
|
_extend_freeze_support()
|
||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
|
|
||||||
|
|
||||||
|
def visualize_regions(root_region: Region, file_name: str, *,
|
||||||
|
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
|
||||||
|
linetype_ortho: bool = True) -> None:
|
||||||
|
"""Visualize the layout of a world as a PlantUML diagram.
|
||||||
|
|
||||||
|
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
|
||||||
|
:param file_name: The name of the destination .puml file.
|
||||||
|
:param show_entrance_names: (default False) If enabled, the name of the entrance will be shown near each connection.
|
||||||
|
:param show_locations: (default True) If enabled, the locations will be listed inside each region.
|
||||||
|
Priority locations will be shown in bold.
|
||||||
|
Excluded locations will be stricken out.
|
||||||
|
Locations without ID will be shown in italics.
|
||||||
|
Locked locations will be shown with a padlock icon.
|
||||||
|
For filled locations, the item name will be shown after the location name.
|
||||||
|
Progression items will be shown in bold.
|
||||||
|
Items without ID will be shown in italics.
|
||||||
|
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
|
||||||
|
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
|
||||||
|
|
||||||
|
Example usage in World code:
|
||||||
|
from Utils import visualize_regions
|
||||||
|
visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
|
||||||
|
|
||||||
|
Example usage in Main code:
|
||||||
|
from Utils import visualize_regions
|
||||||
|
for player in world.player_ids:
|
||||||
|
visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
|
||||||
|
"""
|
||||||
|
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
|
||||||
|
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
|
||||||
|
from collections import deque
|
||||||
|
import re
|
||||||
|
|
||||||
|
uml: typing.List[str] = list()
|
||||||
|
seen: typing.Set[Region] = set()
|
||||||
|
regions: typing.Deque[Region] = deque((root_region,))
|
||||||
|
multiworld: MultiWorld = root_region.multiworld
|
||||||
|
|
||||||
|
def fmt(obj: Union[Entrance, Item, Location, Region]) -> str:
|
||||||
|
name = obj.name
|
||||||
|
if isinstance(obj, Item):
|
||||||
|
name = multiworld.get_name_string_for_object(obj)
|
||||||
|
if obj.advancement:
|
||||||
|
name = f"**{name}**"
|
||||||
|
if obj.code is None:
|
||||||
|
name = f"//{name}//"
|
||||||
|
if isinstance(obj, Location):
|
||||||
|
if obj.progress_type == LocationProgressType.PRIORITY:
|
||||||
|
name = f"**{name}**"
|
||||||
|
elif obj.progress_type == LocationProgressType.EXCLUDED:
|
||||||
|
name = f"--{name}--"
|
||||||
|
if obj.address is None:
|
||||||
|
name = f"//{name}//"
|
||||||
|
return re.sub("[\".:]", "", name)
|
||||||
|
|
||||||
|
def visualize_exits(region: Region) -> None:
|
||||||
|
for exit_ in region.exits:
|
||||||
|
if exit_.connected_region:
|
||||||
|
if show_entrance_names:
|
||||||
|
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"")
|
||||||
|
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"")
|
||||||
|
except ValueError:
|
||||||
|
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"")
|
||||||
|
else:
|
||||||
|
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"")
|
||||||
|
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"")
|
||||||
|
|
||||||
|
def visualize_locations(region: Region) -> None:
|
||||||
|
any_lock = any(location.locked for location in region.locations)
|
||||||
|
for location in region.locations:
|
||||||
|
lock = "<&lock-locked> " if location.locked else "<&lock-unlocked,color=transparent> " if any_lock else ""
|
||||||
|
if location.item:
|
||||||
|
uml.append(f"\"{fmt(region)}\" : {{method}} {lock}{fmt(location)}: {fmt(location.item)}")
|
||||||
|
else:
|
||||||
|
uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}")
|
||||||
|
|
||||||
|
def visualize_region(region: Region) -> None:
|
||||||
|
uml.append(f"class \"{fmt(region)}\"")
|
||||||
|
if show_locations:
|
||||||
|
visualize_locations(region)
|
||||||
|
visualize_exits(region)
|
||||||
|
|
||||||
|
def visualize_other_regions() -> None:
|
||||||
|
if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]:
|
||||||
|
uml.append("package \"other regions\" <<Cloud>> {")
|
||||||
|
for region in other_regions:
|
||||||
|
uml.append(f"class \"{fmt(region)}\"")
|
||||||
|
uml.append("}")
|
||||||
|
|
||||||
|
uml.append("@startuml")
|
||||||
|
uml.append("hide circle")
|
||||||
|
uml.append("hide empty members")
|
||||||
|
if linetype_ortho:
|
||||||
|
uml.append("skinparam linetype ortho")
|
||||||
|
while regions:
|
||||||
|
if (current_region := regions.popleft()) not in seen:
|
||||||
|
seen.add(current_region)
|
||||||
|
visualize_region(current_region)
|
||||||
|
regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region)
|
||||||
|
if show_other_regions:
|
||||||
|
visualize_other_regions()
|
||||||
|
uml.append("@enduml")
|
||||||
|
|
||||||
|
with open(file_name, "wt", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(uml))
|
||||||
|
|||||||
19
WebHost.py
@@ -13,15 +13,6 @@ 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.lttpsprites import update_sprites_lttp
|
|
||||||
from WebHostLib.options import create as create_options_files
|
|
||||||
|
|
||||||
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
|
||||||
@@ -29,6 +20,9 @@ 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"]:
|
||||||
@@ -40,6 +34,7 @@ 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)
|
||||||
return app
|
return app
|
||||||
@@ -120,6 +115,11 @@ if __name__ == "__main__":
|
|||||||
multiprocessing.freeze_support()
|
multiprocessing.freeze_support()
|
||||||
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
|
||||||
|
from WebHostLib.autolauncher import autohost, autogen
|
||||||
|
from WebHostLib.options import create as create_options_files
|
||||||
|
|
||||||
try:
|
try:
|
||||||
update_sprites_lttp()
|
update_sprites_lttp()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -136,4 +136,5 @@ 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"] = "flask_caching.backends.SimpleCache"
|
app.config["CACHE_TYPE"] = "SimpleCache"
|
||||||
app.config["JSON_AS_ASCII"] = False
|
app.config["JSON_AS_ASCII"] = False
|
||||||
app.config["HOST_ADDRESS"] = ""
|
app.config["HOST_ADDRESS"] = ""
|
||||||
|
|
||||||
cache = Cache(app)
|
cache = Cache()
|
||||||
Compress(app)
|
Compress(app)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ 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
|
||||||
@@ -13,55 +11,7 @@ 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:
|
||||||
file = request.files['file']
|
files = request.files.getlist('file')
|
||||||
options = get_yaml_data(file)
|
options = get_yaml_data(files)
|
||||||
if isinstance(options, str):
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
@@ -39,30 +39,33 @@ def mysterycheck():
|
|||||||
return redirect(url_for("check"), 301)
|
return redirect(url_for("check"), 301)
|
||||||
|
|
||||||
|
|
||||||
def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
|
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
|
||||||
options = {}
|
options = {}
|
||||||
# if user does not select file, browser also
|
for file in files:
|
||||||
# submit an empty part without filename
|
# if user does not select file, browser also
|
||||||
if file.filename == '':
|
# submit an empty part without filename
|
||||||
return 'No selected file'
|
if file.filename == '':
|
||||||
elif file and allowed_file(file.filename):
|
return 'No selected file'
|
||||||
if file.filename.endswith(".zip"):
|
elif file.filename in options:
|
||||||
|
return f'Conflicting files named {file.filename} submitted'
|
||||||
|
elif file and allowed_file(file.filename):
|
||||||
|
if file.filename.endswith(".zip"):
|
||||||
|
|
||||||
with zipfile.ZipFile(file, 'r') as zfile:
|
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
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import socket
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
|
import sys
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
from pony.orm import commit, db_session, select
|
from pony.orm import commit, db_session, select
|
||||||
@@ -19,6 +20,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -163,16 +165,21 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
|||||||
db.generate_mapping(check_tables=False)
|
db.generate_mapping(check_tables=False)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
if "worlds" in sys.modules:
|
||||||
|
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||||
|
|
||||||
|
import gc
|
||||||
Utils.init_logging(str(room_id), write_mode="a")
|
Utils.init_logging(str(room_id), write_mode="a")
|
||||||
ctx = WebHostContext(static_server_data)
|
ctx = WebHostContext(static_server_data)
|
||||||
ctx.load(room_id)
|
ctx.load(room_id)
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||||
|
gc.collect() # free intermediate objects used during setup
|
||||||
try:
|
try:
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
|
except OSError: # likely port in use
|
||||||
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||||
|
|
||||||
await ctx.server
|
await ctx.server
|
||||||
@@ -198,16 +205,15 @@ 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:
|
except (KeyboardInterrupt, SystemExit):
|
||||||
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:
|
except Exception:
|
||||||
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:
|
||||||
file = request.files['file']
|
files = request.files.getlist('file')
|
||||||
options = get_yaml_data(file)
|
options = get_yaml_data(files)
|
||||||
if isinstance(options, str):
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
|
|||||||
51
WebHostLib/locker.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class CommonLocker:
|
||||||
|
"""Uses a file lock to signal that something is already running"""
|
||||||
|
lock_folder = "file_locks"
|
||||||
|
|
||||||
|
def __init__(self, lockname: str, folder=None):
|
||||||
|
if folder:
|
||||||
|
self.lock_folder = folder
|
||||||
|
os.makedirs(self.lock_folder, exist_ok=True)
|
||||||
|
self.lockname = lockname
|
||||||
|
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
|
||||||
|
|
||||||
|
|
||||||
|
class AlreadyRunningException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
class Locker(CommonLocker):
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.lockfile):
|
||||||
|
os.unlink(self.lockfile)
|
||||||
|
self.fp = os.open(
|
||||||
|
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
||||||
|
except OSError as e:
|
||||||
|
raise AlreadyRunningException() from e
|
||||||
|
|
||||||
|
def __exit__(self, _type, value, tb):
|
||||||
|
fp = getattr(self, "fp", None)
|
||||||
|
if fp:
|
||||||
|
os.close(self.fp)
|
||||||
|
os.unlink(self.lockfile)
|
||||||
|
else: # unix
|
||||||
|
import fcntl
|
||||||
|
|
||||||
|
|
||||||
|
class Locker(CommonLocker):
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
self.fp = open(self.lockfile, "wb")
|
||||||
|
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except OSError as e:
|
||||||
|
raise AlreadyRunningException() from e
|
||||||
|
|
||||||
|
def __exit__(self, _type, value, tb):
|
||||||
|
fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
|
||||||
|
self.fp.close()
|
||||||
@@ -32,29 +32,34 @@ def page_not_found(err):
|
|||||||
|
|
||||||
# Start Playing Page
|
# Start Playing Page
|
||||||
@app.route('/start-playing')
|
@app.route('/start-playing')
|
||||||
|
@cache.cached()
|
||||||
def start_playing():
|
def start_playing():
|
||||||
return render_template(f"startPlaying.html")
|
return render_template(f"startPlaying.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/weighted-settings')
|
@app.route('/weighted-settings')
|
||||||
|
@cache.cached()
|
||||||
def weighted_settings():
|
def weighted_settings():
|
||||||
return render_template(f"weighted-settings.html")
|
return render_template(f"weighted-settings.html")
|
||||||
|
|
||||||
|
|
||||||
# Player settings pages
|
# Player settings pages
|
||||||
@app.route('/games/<string:game>/player-settings')
|
@app.route('/games/<string:game>/player-settings')
|
||||||
|
@cache.cached()
|
||||||
def player_settings(game):
|
def player_settings(game):
|
||||||
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
# Game Info Pages
|
# Game Info Pages
|
||||||
@app.route('/games/<string:game>/info/<string:lang>')
|
@app.route('/games/<string:game>/info/<string:lang>')
|
||||||
|
@cache.cached()
|
||||||
def game_info(game, lang):
|
def game_info(game, lang):
|
||||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
# List of supported games
|
# List of supported games
|
||||||
@app.route('/games')
|
@app.route('/games')
|
||||||
|
@cache.cached()
|
||||||
def games():
|
def games():
|
||||||
worlds = {}
|
worlds = {}
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
@@ -64,21 +69,25 @@ def games():
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||||
|
@cache.cached()
|
||||||
def tutorial(game, file, lang):
|
def tutorial(game, file, lang):
|
||||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial/')
|
@app.route('/tutorial/')
|
||||||
|
@cache.cached()
|
||||||
def tutorial_landing():
|
def tutorial_landing():
|
||||||
return render_template("tutorialLanding.html")
|
return render_template("tutorialLanding.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/faq/<string:lang>/')
|
@app.route('/faq/<string:lang>/')
|
||||||
|
@cache.cached()
|
||||||
def faq(lang):
|
def faq(lang):
|
||||||
return render_template("faq.html", lang=lang)
|
return render_template("faq.html", lang=lang)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/glossary/<string:lang>/')
|
@app.route('/glossary/<string:lang>/')
|
||||||
|
@cache.cached()
|
||||||
def terms(lang):
|
def terms(lang):
|
||||||
return render_template("glossary.html", lang=lang)
|
return render_template("glossary.html", lang=lang)
|
||||||
|
|
||||||
@@ -147,7 +156,7 @@ def host_room(room: UUID):
|
|||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
def favicon():
|
def favicon():
|
||||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
return send_from_directory(os.path.join(app.root_path, "static", "static"),
|
||||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
|
|
||||||
@@ -167,6 +176,7 @@ def get_datapackage():
|
|||||||
|
|
||||||
@app.route('/index')
|
@app.route('/index')
|
||||||
@app.route('/sitemap')
|
@app.route('/sitemap')
|
||||||
|
@cache.cached()
|
||||||
def get_sitemap():
|
def get_sitemap():
|
||||||
available_games: List[Dict[str, Union[str, bool]]] = []
|
available_games: List[Dict[str, Union[str, bool]]] = []
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
flask>=2.2.3
|
flask>=2.2.3
|
||||||
pony>=0.7.16; python_version <= '3.10'
|
pony>=0.7.17
|
||||||
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.13
|
Flask-Compress>=1.14
|
||||||
Flask-Limiter>=3.3.0
|
Flask-Limiter>=3.5.0
|
||||||
bokeh>=3.1.1
|
bokeh>=3.1.1; python_version <= '3.8'
|
||||||
|
bokeh>=3.2.2; python_version >= '3.9'
|
||||||
markupsafe>=2.1.3
|
markupsafe>=2.1.3
|
||||||
|
|||||||
84
WebHostLib/static/assets/supportedGames.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
window.addEventListener('load', () => {
|
||||||
|
document.getElementById('js-enabled').style.display = 'block';
|
||||||
|
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||||
|
Array.from(gameHeaders).forEach((header) => {
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||||
|
const gameInfo = document.getElementById(gameName);
|
||||||
|
if (gameInfo.classList.contains('collapsed')) {
|
||||||
|
gameArrow.innerText = '▼';
|
||||||
|
gameInfo.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
gameArrow.innerText = '▶';
|
||||||
|
gameInfo.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle game filter input
|
||||||
|
const gameSearch = document.getElementById('game-search');
|
||||||
|
gameSearch.value = '';
|
||||||
|
|
||||||
|
gameSearch.addEventListener('input', (evt) => {
|
||||||
|
if (!evt.target.value.trim()) {
|
||||||
|
// If input is empty, display all collapsed games
|
||||||
|
return Array.from(gameHeaders).forEach((header) => {
|
||||||
|
header.style.display = null;
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
document.getElementById(`${gameName}-arrow`).innerText = '▶';
|
||||||
|
document.getElementById(gameName).classList.add('collapsed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over all the games
|
||||||
|
Array.from(gameHeaders).forEach((header) => {
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||||
|
const gameInfo = document.getElementById(gameName);
|
||||||
|
|
||||||
|
// If the game name includes the search string, display the game. If not, hide it
|
||||||
|
if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||||
|
header.style.display = null;
|
||||||
|
gameArrow.innerText = '▼';
|
||||||
|
gameInfo.classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
console.log(header);
|
||||||
|
header.style.display = 'none';
|
||||||
|
gameArrow.innerText = '▶';
|
||||||
|
gameInfo.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('expand-all').addEventListener('click', expandAll);
|
||||||
|
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandAll = () => {
|
||||||
|
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||||
|
// Loop over all the games
|
||||||
|
Array.from(gameHeaders).forEach((header) => {
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||||
|
const gameInfo = document.getElementById(gameName);
|
||||||
|
|
||||||
|
if (header.style.display === 'none') { return; }
|
||||||
|
gameArrow.innerText = '▼';
|
||||||
|
gameInfo.classList.remove('collapsed');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapseAll = () => {
|
||||||
|
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||||
|
// Loop over all the games
|
||||||
|
Array.from(gameHeaders).forEach((header) => {
|
||||||
|
const gameName = header.getAttribute('data-game');
|
||||||
|
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||||
|
const gameInfo = document.getElementById(gameName);
|
||||||
|
|
||||||
|
if (header.style.display === 'none') { return; }
|
||||||
|
gameArrow.innerText = '▶';
|
||||||
|
gameInfo.classList.add('collapsed');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -14,6 +14,17 @@ const adjustTableHeight = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an integer number of seconds into a human readable HH:MM format
|
||||||
|
* @param {Number} seconds
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const secondsToHours = (seconds) => {
|
||||||
|
let hours = Math.floor(seconds / 3600);
|
||||||
|
let minutes = Math.floor((seconds - (hours * 3600)) / 60).toString().padStart(2, '0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const tables = $(".table").DataTable({
|
const tables = $(".table").DataTable({
|
||||||
paging: false,
|
paging: false,
|
||||||
@@ -27,7 +38,18 @@ 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) {
|
||||||
@@ -40,11 +62,7 @@ window.addEventListener('load', () => {
|
|||||||
if (data === "None")
|
if (data === "None")
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
let hours = Math.floor(data / 3600);
|
return secondsToHours(data);
|
||||||
let minutes = Math.floor((data - (hours * 3600)) / 60);
|
|
||||||
|
|
||||||
if (minutes < 10) {minutes = "0"+minutes;}
|
|
||||||
return hours+':'+minutes;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -114,11 +132,16 @@ 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();
|
||||||
old_table.rows.add(new_trs).draw();
|
if (footer_tr.length) {
|
||||||
|
$(old_table.table).find("tfoot").html(footer_tr);
|
||||||
|
}
|
||||||
|
old_table.rows.add(new_trs);
|
||||||
|
old_table.draw();
|
||||||
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
$(old_table.settings()[0].nScrollBody).scrollTop(topscroll);
|
||||||
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
$(old_table.settings()[0].nScrollBody).scrollLeft(leftscroll);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ 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');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,6 +169,7 @@ 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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1134,8 +1136,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];
|
||||||
@@ -1149,6 +1151,32 @@ 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'];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1156,6 +1184,11 @@ 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;
|
||||||
|
|||||||
BIN
WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/advanceballistics.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/autoturretblackops.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/biomechanicaldrone.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/burstcapacitors.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/crossspectrumdampeners.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/cyclone.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/cyclonerangeupgrade.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/drillingclaws.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/emergencythrusters.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/hellionbattlemode.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/high-explosive-spidermine.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/hyperflightrotors.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/hyperfluxor.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/impalerrounds.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/improvedburstlaser.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/improvedsiegemode.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/interferencematrix.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
WebHostLib/static/static/icons/sc2/internalizedtechmodule.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
WebHostLib/static/static/icons/sc2/jotunboosters.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/jumpjets.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
WebHostLib/static/static/icons/sc2/lasertargetingsystem.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/liberator.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/lockdown.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
WebHostLib/static/static/icons/sc2/magfieldaccelerator.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/magrailmunitions.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
WebHostLib/static/static/icons/sc2/medivacemergencythrusters.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
WebHostLib/static/static/icons/sc2/neosteelfortifiedarmor.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/opticalflare.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/optimizedlogistics.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/reapercombatdrugs.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
WebHostLib/static/static/icons/sc2/restoration.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/ripwavemissiles.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/shreddermissile.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
WebHostLib/static/static/icons/sc2/siegetank-spidermines.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/siegetankrange.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/specialordance.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/spidermine.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
WebHostLib/static/static/icons/sc2/staticempblast.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/icons/sc2/superstimpack.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
WebHostLib/static/static/icons/sc2/targetingoptics.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
WebHostLib/static/static/icons/sc2/terran-cloak-color.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
WebHostLib/static/static/icons/sc2/terran-emp-color.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/icons/sc2/thorsiegemode.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/icons/sc2/transformationservos.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
WebHostLib/static/static/icons/sc2/valkyrie.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
WebHostLib/static/static/icons/sc2/warpjump.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine-attackrange.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine-deathblossom.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowmine.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
WebHostLib/static/static/icons/sc2/widowminehidden.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -235,9 +235,6 @@ html{
|
|||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#landing .variable{
|
|
||||||
color: #ffff00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.landing-deco{
|
.landing-deco{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -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: 500px;
|
width: 710px;
|
||||||
background-color: #525494;
|
background-color: #525494;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,10 +34,12 @@
|
|||||||
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 {
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#location-table{
|
#location-table{
|
||||||
width: 500px;
|
width: 710px;
|
||||||
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,6 +18,20 @@
|
|||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#games h2 .collapse-arrow{
|
||||||
|
font-size: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games h2 .game-name{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games p.collapsed{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
#games a{
|
#games a{
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -31,3 +45,17 @@
|
|||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#games #page-controls{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games #page-controls button{
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#games #js-enabled{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,16 +55,16 @@ table.dataTable thead{
|
|||||||
font-family: LexendDeca-Regular, sans-serif;
|
font-family: LexendDeca-Regular, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody{
|
table.dataTable tbody, table.dataTable tfoot{
|
||||||
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 tbody tr:hover, table.dataTable tfoot tr:hover{
|
||||||
background-color: #e2eabb;
|
background-color: #e2eabb;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody td{
|
table.dataTable tbody td, table.dataTable tfoot td{
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +97,14 @@ table.dataTable thead th.lower-row{
|
|||||||
top: 46px;
|
top: 46px;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable tbody td{
|
table.dataTable tbody td, table.dataTable tfoot 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">
|
<input id="file-input" type="file" name="file" multiple>
|
||||||
</form>
|
</form>
|
||||||
<button id="check-button">Upload</button>
|
<button id="check-button">Upload File(s)</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">
|
<input id="file-input" type="file" name="file" multiple>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<button id="generate-game-button">Upload File</button>
|
<button id="generate-game-button">Upload File(s)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,9 +49,9 @@
|
|||||||
our crazy idea into a reality.
|
our crazy idea into a reality.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="variable">{{ seeds }}</span>
|
<a href="{{ url_for("stats") }}">{{ seeds }}</a>
|
||||||
games were generated and
|
games were generated and
|
||||||
<span class="variable">{{ rooms }}</span>
|
<a href="{{ url_for("stats") }}">{{ rooms }}</a>
|
||||||
were hosted in the last 7 days.
|
were hosted in the last 7 days.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block custom_table_row scoped %}
|
{% block custom_table_row scoped %}
|
||||||
{% if games[player] == "Factorio" %}
|
{% if games[player] == "Factorio" %}
|
||||||
{% set player_inventory = inventory[team][player] %}
|
{% set player_inventory = named_inventory[team][player] %}
|
||||||
{% set prog_science = player_inventory[custom_items["progressive-science-pack"]] %}
|
{% set prog_science = player_inventory["progressive-science-pack"] %}
|
||||||
<td class="center-column">{% if player_inventory[custom_items["logistic-science-pack"]] or prog_science %}✔{% endif %}</td>
|
<td class="center-column">{% if player_inventory["logistic-science-pack"] or prog_science %}✔{% endif %}</td>
|
||||||
<td class="center-column">{% if player_inventory[custom_items["military-science-pack"]] or prog_science > 1%}✔{% endif %}</td>
|
<td class="center-column">{% if player_inventory["military-science-pack"] or prog_science > 1%}✔{% endif %}</td>
|
||||||
<td class="center-column">{% if player_inventory[custom_items["chemical-science-pack"]] or prog_science > 2%}✔{% endif %}</td>
|
<td class="center-column">{% if player_inventory["chemical-science-pack"] or prog_science > 2%}✔{% endif %}</td>
|
||||||
<td class="center-column">{% if player_inventory[custom_items["production-science-pack"]] or prog_science > 3%}✔{% endif %}</td>
|
<td class="center-column">{% if player_inventory["production-science-pack"] or prog_science > 3%}✔{% endif %}</td>
|
||||||
<td class="center-column">{% if player_inventory[custom_items["utility-science-pack"]] or prog_science > 4%}✔{% endif %}</td>
|
<td class="center-column">{% if player_inventory["utility-science-pack"] or prog_science > 4%}✔{% endif %}</td>
|
||||||
<td class="center-column">{% if player_inventory[custom_items["space-science-pack"]] or prog_science > 5%}✔{% endif %}</td>
|
<td class="center-column">{% if player_inventory["space-science-pack"] or prog_science > 5%}✔{% endif %}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="center-column">❌</td>
|
<td class="center-column">❌</td>
|
||||||
<td class="center-column">❌</td>
|
<td class="center-column">❌</td>
|
||||||
|
|||||||
@@ -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<br>Activity</th>
|
<th class="center-column hours last-activity">Last<br>Activity</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -64,6 +64,19 @@
|
|||||||
</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 %}
|
||||||
|
|||||||
@@ -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="10" class="title">
|
<td colspan="15" class="title">
|
||||||
Starting Resources
|
Starting Resources
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
-->
|
-->
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="title">
|
<td colspan="15" class="title">
|
||||||
Weapon & Armor Upgrades
|
Weapon & Armor Upgrades
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -37,120 +37,266 @@
|
|||||||
<td><img src="{{ vehicle_armor_url }}" class="{{ 'acquired' if 'Progressive Vehicle Armor' in acquired_items }}" title="Progressive Vehicle Armor{% if vehicle_armor_level > 0 %} (Level {{ vehicle_armor_level }}){% endif %}" /></td>
|
<td><img src="{{ 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="10" class="title">
|
<td colspan="15" class="title">
|
||||||
Base
|
Base
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td>
|
<td><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>
|
||||||
<td colspan="2"> </td>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
|
||||||
|
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
|
||||||
|
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
|
||||||
|
<td></td>
|
||||||
<td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td>
|
<td><img src="{{ icons['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><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
|
<td></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['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td>
|
||||||
|
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="title">
|
<td><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="title">
|
||||||
Infantry
|
Infantry
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td></td>
|
||||||
<tr>
|
<td colspan="7" class="title">
|
||||||
<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 colspan="2"><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
|
<td><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
|
||||||
<td colspan="2"><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></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['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></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['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></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['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
|
<td><img src="{{ icons['Magrail Munitions (Marine)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marine)' in acquired_items }}" title="Magrail Munitions (Marine)" /></td>
|
||||||
</tr>
|
<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>
|
||||||
|
<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['Cerberus Mine (Vulture)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Vulture)' in acquired_items }}" title="Cerberus Mine (Vulture)" /></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['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (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['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['Optimized Logistics (Hellion)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Hellion)' in acquired_items }}" title="Optimized Logistics (Hellion)" /></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 (Hellion)'] }}" class="{{ 'acquired' if 'Jump Jets (Hellion)' in acquired_items }}" title="Jump Jets (Hellion)" /></td>
|
||||||
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
|
|
||||||
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
|
|
||||||
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
|
|
||||||
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="title">
|
<td><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
|
||||||
|
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
|
||||||
|
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
|
||||||
|
<td><img src="{{ icons['Restoration (Medic)'] }}" class="{{ 'acquired' if 'Restoration (Medic)' in acquired_items }}" title="Restoration (Medic)" /></td>
|
||||||
|
<td><img src="{{ icons['Optical Flare (Medic)'] }}" class="{{ 'acquired' if 'Optical Flare (Medic)' in acquired_items }}" title="Optical Flare (Medic)" /></td>
|
||||||
|
<td><img src="{{ icons['Optimized Logistics (Medic)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Medic)' in acquired_items }}" title="Optimized Logistics (Medic)" /></td>
|
||||||
|
<td colspan="2"></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ stimpack_hellion_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Hellion)' in acquired_items }}" title="{{ stimpack_hellion_name }}" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
|
||||||
|
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
|
||||||
|
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
|
||||||
|
<td><img src="{{ stimpack_firebat_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Firebat)' in acquired_items }}" title="{{ stimpack_firebat_name }}" /></td>
|
||||||
|
<td><img src="{{ icons['Optimized Logistics (Firebat)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Firebat)' in acquired_items }}" title="Optimized Logistics (Firebat)" /></td>
|
||||||
|
<td colspan="3"></td>
|
||||||
|
<td><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
|
||||||
|
<td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td>
|
||||||
|
<td><img src="{{ icons['Ion Thrusters (Vulture)'] }}" class="{{ 'acquired' if 'Ion Thrusters (Vulture)' in acquired_items }}" title="Ion Thrusters (Vulture)" /></td>
|
||||||
|
<td><img src="{{ icons['Auto Launchers (Vulture)'] }}" class="{{ 'acquired' if 'Auto Launchers (Vulture)' in acquired_items }}" title="Auto Launchers (Vulture)" /></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Cerberus Mine (Spider Mine)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Spider Mine)' in acquired_items }}" title="Cerberus Mine (Spider Mine)" /></td>
|
||||||
|
<td><img src="{{ icons['High Explosive Munition (Spider Mine)'] }}" class="{{ 'acquired' if 'High Explosive Munition (Spider Mine)' in acquired_items }}" title="High Explosive Munition (Spider Mine)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
|
||||||
|
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
|
||||||
|
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
|
||||||
|
<td><img src="{{ stimpack_marauder_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Marauder)' in acquired_items }}" title="{{ stimpack_marauder_name }}" /></td>
|
||||||
|
<td><img src="{{ icons['Laser Targeting System (Marauder)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Marauder)' in acquired_items }}" title="Laser Targeting System (Marauder)" /></td>
|
||||||
|
<td><img src="{{ icons['Magrail Munitions (Marauder)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Marauder)' in acquired_items }}" title="Magrail Munitions (Marauder)" /></td>
|
||||||
|
<td><img src="{{ icons['Internal Tech Module (Marauder)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Marauder)' in acquired_items }}" title="Internal Tech Module (Marauder)" /></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
|
||||||
|
<td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td>
|
||||||
|
<td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td>
|
||||||
|
<td><img src="{{ icons['Jump Jets (Goliath)'] }}" class="{{ 'acquired' if 'Jump Jets (Goliath)' in acquired_items }}" title="Jump Jets (Goliath)" /></td>
|
||||||
|
<td><img src="{{ icons['Optimized Logistics (Goliath)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Goliath)' in acquired_items }}" title="Optimized Logistics (Goliath)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
|
||||||
|
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
|
||||||
|
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
|
||||||
|
<td><img src="{{ stimpack_reaper_url }}" class="{{ 'acquired' if 'Progressive Stimpack (Reaper)' in acquired_items }}" title="{{ stimpack_reaper_name }}" /></td>
|
||||||
|
<td><img src="{{ icons['Laser Targeting System (Reaper)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Reaper)' in acquired_items }}" title="Laser Targeting System (Reaper)" /></td>
|
||||||
|
<td><img src="{{ icons['Advanced Cloaking Field (Reaper)'] }}" class="{{ 'acquired' if 'Advanced Cloaking Field (Reaper)' in acquired_items }}" title="Advanced Cloaking Field (Reaper)" /></td>
|
||||||
|
<td><img src="{{ icons['Spider Mines (Reaper)'] }}" class="{{ 'acquired' if 'Spider Mines (Reaper)' in acquired_items }}" title="Spider Mines (Reaper)" /></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
|
||||||
|
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
|
||||||
|
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
|
||||||
|
<td><img src="{{ icons['Hyperfluxor (Diamondback)'] }}" class="{{ 'acquired' if 'Hyperfluxor (Diamondback)' in acquired_items }}" title="Hyperfluxor (Diamondback)" /></td>
|
||||||
|
<td><img src="{{ icons['Burst Capacitors (Diamondback)'] }}" class="{{ 'acquired' if 'Burst Capacitors (Diamondback)' in acquired_items }}" title="Burst Capacitors (Diamondback)" /></td>
|
||||||
|
<td><img src="{{ icons['Optimized Logistics (Diamondback)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Diamondback)' in acquired_items }}" title="Optimized Logistics (Diamondback)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Combat Drugs (Reaper)'] }}" class="{{ 'acquired' if 'Combat Drugs (Reaper)' in acquired_items }}" title="Combat Drugs (Reaper)" /></td>
|
||||||
|
<td colspan="6"></td>
|
||||||
|
<td><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
|
||||||
|
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
|
||||||
|
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
|
||||||
|
<td><img src="{{ icons['Jump Jets (Siege Tank)'] }}" class="{{ 'acquired' if 'Jump Jets (Siege Tank)' in acquired_items }}" title="Jump Jets (Siege Tank)" /></td>
|
||||||
|
<td><img src="{{ icons['Spider Mines (Siege Tank)'] }}" class="{{ 'acquired' if 'Spider Mines (Siege Tank)' in acquired_items }}" title="Spider Mines (Siege Tank)" /></td>
|
||||||
|
<td><img src="{{ icons['Smart Servos (Siege Tank)'] }}" class="{{ 'acquired' if 'Smart Servos (Siege Tank)' in acquired_items }}" title="Smart Servos (Siege Tank)" /></td>
|
||||||
|
<td><img src="{{ icons['Graduating Range (Siege Tank)'] }}" class="{{ 'acquired' if 'Graduating Range (Siege Tank)' in acquired_items }}" title="Graduating Range (Siege Tank)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
|
||||||
|
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
|
||||||
|
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
|
||||||
|
<td><img src="{{ icons['EMP Rounds (Ghost)'] }}" class="{{ 'acquired' if 'EMP Rounds (Ghost)' in acquired_items }}" title="EMP Rounds (Ghost)" /></td>
|
||||||
|
<td><img src="{{ icons['Lockdown (Ghost)'] }}" class="{{ 'acquired' if 'Lockdown (Ghost)' in acquired_items }}" title="Lockdown (Ghost)" /></td>
|
||||||
|
<td colspan="3"></td>
|
||||||
|
<td></td>
|
||||||
|
<td><img src="{{ icons['Laser Targeting System (Siege Tank)'] }}" class="{{ 'acquired' if 'Laser Targeting System (Siege Tank)' in acquired_items }}" title="Laser Targeting System (Siege Tank)" /></td>
|
||||||
|
<td><img src="{{ icons['Advanced Siege Tech (Siege Tank)'] }}" class="{{ 'acquired' if 'Advanced Siege Tech (Siege Tank)' in acquired_items }}" title="Advanced Siege Tech (Siege Tank)" /></td>
|
||||||
|
<td><img src="{{ icons['Internal Tech Module (Siege Tank)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Siege Tank)' in acquired_items }}" title="Internal Tech Module (Siege Tank)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
|
||||||
|
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
|
||||||
|
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
|
||||||
|
<td><img src="{{ icons['Impaler Rounds (Spectre)'] }}" class="{{ 'acquired' if 'Impaler Rounds (Spectre)' in acquired_items }}" title="Impaler Rounds (Spectre)" /></td>
|
||||||
|
<td colspan="4"></td>
|
||||||
|
<td><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
|
||||||
|
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
|
||||||
|
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
|
||||||
|
<td><img src="{{ high_impact_payload_thor_url }}" class="{{ 'acquired' if 'Progressive High Impact Payload (Thor)' in acquired_items }}" title="{{ high_impact_payload_thor_name }}" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8"></td>
|
||||||
|
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
|
||||||
|
<td><img src="{{ icons['Optimized Logistics (Predator)'] }}" class="{{ 'acquired' if 'Optimized Logistics (Predator)' in acquired_items }}" title="Optimized Logistics (Predator)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8"></td>
|
||||||
|
<td><img src="{{ icons['Widow Mine'] }}" class="{{ 'acquired' if 'Widow Mine' in acquired_items }}" title="Widow Mine" /></td>
|
||||||
|
<td><img src="{{ icons['Drilling Claws (Widow Mine)'] }}" class="{{ 'acquired' if 'Drilling Claws (Widow Mine)' in acquired_items }}" title="Drilling Claws (Widow Mine)" /></td>
|
||||||
|
<td><img src="{{ icons['Concealment (Widow Mine)'] }}" class="{{ 'acquired' if 'Concealment (Widow Mine)' in acquired_items }}" title="Concealment (Widow Mine)" /></td>
|
||||||
|
<td><img src="{{ icons['Black Market Launchers (Widow Mine)'] }}" class="{{ 'acquired' if 'Black Market Launchers (Widow Mine)' in acquired_items }}" title="Black Market Launchers (Widow Mine)" /></td>
|
||||||
|
<td><img src="{{ icons['Executioner Missiles (Widow Mine)'] }}" class="{{ 'acquired' if 'Executioner Missiles (Widow Mine)' in acquired_items }}" title="Executioner Missiles (Widow Mine)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8"></td>
|
||||||
|
<td><img src="{{ icons['Cyclone'] }}" class="{{ 'acquired' if 'Cyclone' in acquired_items }}" title="Cyclone" /></td>
|
||||||
|
<td><img src="{{ icons['Mag-Field Accelerators (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Accelerators (Cyclone)' in acquired_items }}" title="Mag-Field Accelerators (Cyclone)" /></td>
|
||||||
|
<td><img src="{{ icons['Mag-Field Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Mag-Field Launchers (Cyclone)' in acquired_items }}" title="Mag-Field Launchers (Cyclone)" /></td>
|
||||||
|
<td><img src="{{ icons['Targeting Optics (Cyclone)'] }}" class="{{ 'acquired' if 'Targeting Optics (Cyclone)' in acquired_items }}" title="Targeting Optics (Cyclone)" /></td>
|
||||||
|
<td><img src="{{ icons['Rapid Fire Launchers (Cyclone)'] }}" class="{{ 'acquired' if 'Rapid Fire Launchers (Cyclone)' in acquired_items }}" title="Rapid Fire Launchers (Cyclone)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="15" class="title">
|
||||||
Starships
|
Starships
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td>
|
<td><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['Cross-Spectrum Dampeners (Banshee)'] }}" class="{{ 'acquired' if 'Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="Cross-Spectrum Dampeners (Banshee)" /></td>
|
<td><img src="{{ icons['Smart Servos (Viking)'] }}" class="{{ 'acquired' if 'Smart Servos (Viking)' in acquired_items }}" title="Smart Servos (Viking)" /></td>
|
||||||
|
<td><img src="{{ icons['Magrail Munitions (Viking)'] }}" class="{{ 'acquired' if 'Magrail Munitions (Viking)' in acquired_items }}" title="Magrail Munitions (Viking)" /></td>
|
||||||
|
<td colspan="3"></td>
|
||||||
|
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
|
||||||
|
<td><img src="{{ icons['EMP Shockwave (Science Vessel)'] }}" class="{{ 'acquired' if 'EMP Shockwave (Science Vessel)' in acquired_items }}" title="EMP Shockwave (Science Vessel)" /></td>
|
||||||
|
<td><img src="{{ icons['Defensive Matrix (Science Vessel)'] }}" class="{{ 'acquired' if 'Defensive Matrix (Science Vessel)' in acquired_items }}" title="Defensive Matrix (Science Vessel)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
|
||||||
|
<td><img src="{{ crossspectrum_dampeners_banshee_url }}" class="{{ 'acquired' if 'Progressive Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="{{ crossspectrum_dampeners_banshee_name }}" /></td>
|
||||||
<td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></td>
|
<td><img src="{{ icons['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 colspan="10" class="title">
|
<td></td>
|
||||||
Dominion
|
<td><img src="{{ icons['Internal Tech Module (Battlecruiser)'] }}" class="{{ 'acquired' if 'Internal Tech Module (Battlecruiser)' in acquired_items }}" title="Internal Tech Module (Battlecruiser)" /></td>
|
||||||
</td>
|
<td colspan="6"></td>
|
||||||
|
<td><img src="{{ icons['Valkyrie'] }}" class="{{ 'acquired' if 'Valkyrie' in acquired_items }}" title="Valkyrie" /></td>
|
||||||
|
<td><img src="{{ icons['Enhanced Cluster Launchers (Valkyrie)'] }}" class="{{ 'acquired' if 'Enhanced Cluster Launchers (Valkyrie)' in acquired_items }}" title="Enhanced Cluster Launchers (Valkyrie)" /></td>
|
||||||
|
<td><img src="{{ icons['Shaped Hull (Valkyrie)'] }}" class="{{ 'acquired' if 'Shaped Hull (Valkyrie)' in acquired_items }}" title="Shaped Hull (Valkyrie)" /></td>
|
||||||
|
<td><img src="{{ icons['Burst Lasers (Valkyrie)'] }}" class="{{ 'acquired' if 'Burst Lasers (Valkyrie)' in acquired_items }}" title="Burst Lasers (Valkyrie)" /></td>
|
||||||
|
<td><img src="{{ icons['Afterburners (Valkyrie)'] }}" class="{{ 'acquired' if 'Afterburners (Valkyrie)' in acquired_items }}" title="Afterburners (Valkyrie)" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
|
<td colspan="15" class="title">
|
||||||
<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>
|
||||||
@@ -165,36 +311,18 @@
|
|||||||
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
|
<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="10" class="title">
|
<td colspan="15" class="title">
|
||||||
Lab Upgrades
|
General Upgrades
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></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['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>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="{{ icons['Shrike Turret'] }}" class="{{ 'acquired' if 'Shrike Turret' in acquired_items }}" title="Shrike Turret" /></td>
|
|
||||||
<td><img src="{{ icons['Fortified Bunker'] }}" class="{{ 'acquired' if 'Fortified Bunker' in acquired_items }}" title="Fortified Bunker" /></td>
|
|
||||||
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
|
|
||||||
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
|
|
||||||
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
|
|
||||||
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
|
|
||||||
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
|
<td><img src="{{ icons['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="{{ 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>
|
||||||
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="title">
|
<td colspan="15" class="title">
|
||||||
Protoss Units
|
Protoss Units
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -4,16 +4,35 @@
|
|||||||
<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!
|
||||||
|
<noscript>(You need to enable Javascript for this to work.)</noscript>
|
||||||
|
</label><br />
|
||||||
|
<div id="page-controls">
|
||||||
|
<input id="game-search" placeholder="Search by title..." autofocus />
|
||||||
|
<button id="expand-all">Expand All</button>
|
||||||
|
<button id="collapse-all">Collapse All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- This is always written to the page, but is hidden by default. If the user has JS enabled,
|
||||||
|
it will be un-hidden immediately when the page is ready. -->
|
||||||
|
<div id="js-enabled">
|
||||||
{% for game_name in worlds | title_sorted %}
|
{% for game_name in worlds | title_sorted %}
|
||||||
{% set world = worlds[game_name] %}
|
{% set world = worlds[game_name] %}
|
||||||
<h2>{{ game_name }}</h2>
|
<h2 class="collapse-toggle" data-game="{{ game_name }}">
|
||||||
<p>
|
<span id="{{ game_name }}-arrow" class="collapse-arrow">▶</span>
|
||||||
|
<span class="game-name">{{ game_name }}</span>
|
||||||
|
</h2>
|
||||||
|
<p id="{{ game_name }}" class="collapsed">
|
||||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||||
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
||||||
{% if world.web.tutorials %}
|
{% if world.web.tutorials %}
|
||||||
@@ -33,5 +52,34 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- This is only printed when the user has JS disabled, allowing them to see all the information for
|
||||||
|
each game in a noscript-friendly way. -->
|
||||||
|
<noscript>
|
||||||
|
{% for game_name in worlds | title_sorted %}
|
||||||
|
{% set world = worlds[game_name] %}
|
||||||
|
<h2 class="collapse-toggle">{{ game_name }}</h2>
|
||||||
|
<p id="{{ game_name }}">
|
||||||
|
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||||
|
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
||||||
|
{% if world.web.tutorials %}
|
||||||
|
<span class="link-spacer">|</span>
|
||||||
|
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if world.web.settings_page is string %}
|
||||||
|
<span class="link-spacer">|</span>
|
||||||
|
<a href="{{ world.web.settings_page }}">Settings Page</a>
|
||||||
|
{% elif world.web.settings_page %}
|
||||||
|
<span class="link-spacer">|</span>
|
||||||
|
<a href="{{ url_for("player_settings", game=game_name) }}">Settings Page</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if world.web.bug_report_page %}
|
||||||
|
<span class="link-spacer">|</span>
|
||||||
|
<a href="{{ world.web.bug_report_page }}">Report a Bug</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</noscript>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Rooms: </td>
|
<td>Rooms: </td>
|
||||||
<td>
|
<td>
|
||||||
{% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}
|
{% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from jinja2 import pass_context, runtime
|
|||||||
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 SlotType, NetworkSlot
|
from NetUtils import ClientStatus, 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
|
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name, network_data_package, games
|
||||||
from worlds.alttp import Items
|
from worlds.alttp import Items
|
||||||
from . import app, cache
|
from . import app, cache
|
||||||
from .models import GameDataPackage, Room
|
from .models import GameDataPackage, Room
|
||||||
@@ -990,6 +990,7 @@ 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",
|
||||||
@@ -1034,15 +1035,36 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
"Reaper": "https://static.wikia.nocookie.net/starcraft/images/7/7d/Reaper_SC2_Icon1.jpg",
|
"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",
|
||||||
@@ -1052,14 +1074,35 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
|
|
||||||
"Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
|
"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",
|
||||||
"Cerberus Mine (Vulture)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
|
"Hellbat Aspect (Hellion)": "/static/static/icons/sc2/hellionbattlemode.png",
|
||||||
|
"Smart Servos (Hellion)": "/static/static/icons/sc2/transformationservos.png",
|
||||||
|
"Optimized Logistics (Hellion)": "/static/static/icons/sc2/optimizedlogistics.png",
|
||||||
|
"Jump Jets (Hellion)": "/static/static/icons/sc2/jumpjets.png",
|
||||||
|
"Stimpack (Hellion)": "https://0rganics.org/archipelago/sc2wol/StimpacksCampaign.png",
|
||||||
|
"Super Stimpack (Hellion)": "/static/static/icons/sc2/superstimpack.png",
|
||||||
|
"Cerberus Mine (Spider Mine)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
|
||||||
|
"High Explosive Munition (Spider Mine)": "/static/static/icons/sc2/high-explosive-spidermine.png",
|
||||||
"Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.png",
|
"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",
|
||||||
@@ -1069,25 +1112,77 @@ 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",
|
||||||
"Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
|
"Smart Servos (Viking)": "/static/static/icons/sc2/transformationservos.png",
|
||||||
|
"Magrail Munitions (Viking)": "/static/static/icons/sc2/magrailmunitions.png",
|
||||||
|
"Cross-Spectrum Dampeners (Banshee)": "/static/static/icons/sc2/crossspectrumdampeners.png",
|
||||||
|
"Advanced Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
|
||||||
"Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png",
|
"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",
|
||||||
@@ -1109,14 +1204,15 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
"Tech Reactor": "https://static.wikia.nocookie.net/starcraft/images/c/c5/SC2_Lab_Tech_Reactor_Icon.png",
|
"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": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
|
"Shrike Turret (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
|
||||||
"Fortified Bunker": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
|
"Fortified Bunker (Bunker)": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
|
||||||
"Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png",
|
"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": "https://static.wikia.nocookie.net/starcraft/images/d/d3/SC2_Lab_BioSteel_Icon.png",
|
"Regenerative Bio-Steel Level 1": "/static/static/icons/sc2/SC2_Lab_BioSteel_L1.png",
|
||||||
|
"Regenerative Bio-Steel Level 2": "/static/static/icons/sc2/SC2_Lab_BioSteel_L2.png",
|
||||||
"Hive Mind Emulator": "https://static.wikia.nocookie.net/starcraft/images/b/bc/SC2_Lab_Hive_Emulator_Icon.png",
|
"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",
|
||||||
|
|
||||||
@@ -1132,40 +1228,71 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
|
|
||||||
"Nothing": "",
|
"Nothing": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
sc2wol_location_ids = {
|
sc2wol_location_ids = {
|
||||||
"Liberation Day": [SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 101, SC2WOL_LOC_ID_OFFSET + 102, SC2WOL_LOC_ID_OFFSET + 103, SC2WOL_LOC_ID_OFFSET + 104, SC2WOL_LOC_ID_OFFSET + 105, SC2WOL_LOC_ID_OFFSET + 106],
|
"Liberation Day": range(SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 200),
|
||||||
"The Outlaws": [SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 201],
|
"The Outlaws": range(SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 300),
|
||||||
"Zero Hour": [SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 301, SC2WOL_LOC_ID_OFFSET + 302, SC2WOL_LOC_ID_OFFSET + 303],
|
"Zero Hour": range(SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 400),
|
||||||
"Evacuation": [SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 401, SC2WOL_LOC_ID_OFFSET + 402, SC2WOL_LOC_ID_OFFSET + 403],
|
"Evacuation": range(SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 500),
|
||||||
"Outbreak": [SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 501, SC2WOL_LOC_ID_OFFSET + 502],
|
"Outbreak": range(SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 600),
|
||||||
"Safe Haven": [SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 601, SC2WOL_LOC_ID_OFFSET + 602, SC2WOL_LOC_ID_OFFSET + 603],
|
"Safe Haven": range(SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 700),
|
||||||
"Haven's Fall": [SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 701, SC2WOL_LOC_ID_OFFSET + 702, SC2WOL_LOC_ID_OFFSET + 703],
|
"Haven's Fall": range(SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 800),
|
||||||
"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],
|
"Smash and Grab": range(SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 900),
|
||||||
"The Dig": [SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 901, SC2WOL_LOC_ID_OFFSET + 902, SC2WOL_LOC_ID_OFFSET + 903],
|
"The Dig": range(SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 1000),
|
||||||
"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],
|
"The Moebius Factor": range(SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1100),
|
||||||
"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],
|
"Supernova": range(SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1200),
|
||||||
"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],
|
"Maw of the Void": range(SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1300),
|
||||||
"Devil's Playground": [SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1301, SC2WOL_LOC_ID_OFFSET + 1302],
|
"Devil's Playground": range(SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1400),
|
||||||
"Welcome to the Jungle": [SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1401, SC2WOL_LOC_ID_OFFSET + 1402, SC2WOL_LOC_ID_OFFSET + 1403],
|
"Welcome to the Jungle": range(SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1500),
|
||||||
"Breakout": [SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1501, SC2WOL_LOC_ID_OFFSET + 1502],
|
"Breakout": range(SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1600),
|
||||||
"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],
|
"Ghost of a Chance": range(SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1700),
|
||||||
"The Great Train Robbery": [SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1701, SC2WOL_LOC_ID_OFFSET + 1702, SC2WOL_LOC_ID_OFFSET + 1703],
|
"The Great Train Robbery": range(SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1800),
|
||||||
"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],
|
"Cutthroat": range(SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1900),
|
||||||
"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],
|
"Engine of Destruction": range(SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 2000),
|
||||||
"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],
|
"Media Blitz": range(SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2100),
|
||||||
"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],
|
"Piercing the Shroud": range(SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2200),
|
||||||
"Whispers of Doom": [SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2201, SC2WOL_LOC_ID_OFFSET + 2202, SC2WOL_LOC_ID_OFFSET + 2203],
|
"Whispers of Doom": range(SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2300),
|
||||||
"A Sinister Turn": [SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2301, SC2WOL_LOC_ID_OFFSET + 2302, SC2WOL_LOC_ID_OFFSET + 2303],
|
"A Sinister Turn": range(SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2400),
|
||||||
"Echoes of the Future": [SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2401, SC2WOL_LOC_ID_OFFSET + 2402],
|
"Echoes of the Future": range(SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2500),
|
||||||
"In Utter Darkness": [SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2501, SC2WOL_LOC_ID_OFFSET + 2502],
|
"In Utter Darkness": range(SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2600),
|
||||||
"Gates of Hell": [SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2601],
|
"Gates of Hell": range(SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2700),
|
||||||
"Belly of the Beast": [SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2701, SC2WOL_LOC_ID_OFFSET + 2702, SC2WOL_LOC_ID_OFFSET + 2703],
|
"Belly of the Beast": range(SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2800),
|
||||||
"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],
|
"Shatter the Sky": range(SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2900),
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -1173,7 +1300,15 @@ 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"],
|
||||||
@@ -1181,14 +1316,27 @@ def __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict
|
|||||||
"Progressive Vehicle Weapon": ["Vehicle Weapons Level 1", "Vehicle Weapons Level 1", "Vehicle Weapons Level 2", "Vehicle Weapons Level 3"],
|
"Progressive Vehicle 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().replace(' ', '_')
|
base_name = (item_name.split(maxsplit=1)[1].lower()
|
||||||
|
.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 = {
|
||||||
@@ -1220,12 +1368,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,
|
||||||
@@ -1366,6 +1514,10 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
|
|||||||
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
||||||
for teamnumber, team in enumerate(names)}
|
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))}
|
hints = {team: set() for team in range(len(names))}
|
||||||
if room.multisave:
|
if room.multisave:
|
||||||
multisave = restricted_loads(room.multisave)
|
multisave = restricted_loads(room.multisave)
|
||||||
@@ -1390,11 +1542,14 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
|
|||||||
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
|
||||||
|
|
||||||
player_names = {}
|
player_names = {}
|
||||||
|
completed_worlds = 0
|
||||||
states: typing.Dict[typing.Tuple[int, int], int] = {}
|
states: typing.Dict[typing.Tuple[int, int], int] = {}
|
||||||
for team, names in enumerate(names):
|
for team, names in enumerate(names):
|
||||||
for player, name in enumerate(names, 1):
|
for player, name in enumerate(names, 1):
|
||||||
player_names[team, player] = name
|
player_names[team, player] = name
|
||||||
states[team, player] = multisave.get("client_game_state", {}).get((team, player), 0)
|
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()
|
long_player_names = player_names.copy()
|
||||||
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
for (team, player), alias in multisave.get("name_aliases", {}).items():
|
||||||
player_names[team, player] = alias
|
player_names[team, player] = alias
|
||||||
@@ -1410,14 +1565,18 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s
|
|||||||
activity_timers=activity_timers, video=video, hints=hints,
|
activity_timers=activity_timers, video=video, hints=hints,
|
||||||
long_player_names=long_player_names,
|
long_player_names=long_player_names,
|
||||||
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
multisave=multisave, precollected_items=precollected_items, groups=groups,
|
||||||
locations=locations, games=games, states=states,
|
locations=locations, total_locations=total_locations, games=games, states=states,
|
||||||
|
completed_worlds=completed_worlds,
|
||||||
custom_locations=custom_locations, custom_items=custom_items,
|
custom_locations=custom_locations, custom_items=custom_items,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int, typing.Dict[int, int]]:
|
def _get_inventory_data(data: typing.Dict[str, typing.Any]) \
|
||||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in team_data}
|
-> typing.Dict[int, typing.Dict[int, typing.Dict[int, int]]]:
|
||||||
for teamnumber, team_data in data["checks_done"].items()}
|
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"]
|
||||||
|
|
||||||
@@ -1436,6 +1595,17 @@ def _get_inventory_data(data: typing.Dict[str, typing.Any]) -> typing.Dict[int,
|
|||||||
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>')
|
@app.route('/tracker/<suuid:tracker>')
|
||||||
@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_multiworld_tracker(tracker: UUID):
|
def get_multiworld_tracker(tracker: UUID):
|
||||||
@@ -1447,18 +1617,22 @@ def get_multiworld_tracker(tracker: UUID):
|
|||||||
|
|
||||||
return render_template("multiTracker.html", **data)
|
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)
|
||||||
|
|
||||||
@app.route('/tracker/<suuid:tracker>/Factorio')
|
data["inventory"] = _get_inventory_data(data)
|
||||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
data["named_inventory"] = {team_id : {
|
||||||
def get_Factorio_multiworld_tracker(tracker: UUID):
|
player_id: _get_named_inventory(inventory, data["custom_items"])
|
||||||
data = _get_multiworld_tracker_data(tracker)
|
for player_id, inventory in team_inventory.items()
|
||||||
if not data:
|
} for team_id, team_inventory in data["inventory"].items()}
|
||||||
abort(404)
|
data["enabled_multiworld_trackers"] = get_enabled_multiworld_trackers(data["room"], "Factorio")
|
||||||
|
|
||||||
data["inventory"] = _get_inventory_data(data)
|
return render_template("multiFactorioTracker.html", **data)
|
||||||
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')
|
||||||
@@ -1509,7 +1683,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[player]:
|
if location not in player_locations or location not in player_location_to_area.get(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])
|
||||||
@@ -1588,5 +1762,7 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
|||||||
|
|
||||||
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,
|
||||||
"Factorio": get_Factorio_multiworld_tracker,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if "Factorio" in games:
|
||||||
|
multi_trackers["Factorio"] = get_Factorio_multiworld_tracker
|
||||||
|
|||||||
119
data/lua/base64.lua
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
-- This file originates from this repository: https://github.com/iskolbin/lbase64
|
||||||
|
-- It was modified to translate between base64 strings and lists of bytes instead of base64 strings and strings.
|
||||||
|
|
||||||
|
local base64 = {}
|
||||||
|
|
||||||
|
local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
|
||||||
|
if not extract then
|
||||||
|
if _G._VERSION == "Lua 5.4" then
|
||||||
|
extract = load[[return function( v, from, width )
|
||||||
|
return ( v >> from ) & ((1 << width) - 1)
|
||||||
|
end]]()
|
||||||
|
elseif _G.bit then -- LuaJIT
|
||||||
|
local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
|
||||||
|
extract = function( v, from, width )
|
||||||
|
return band( shr( v, from ), shl( 1, width ) - 1 )
|
||||||
|
end
|
||||||
|
elseif _G._VERSION == "Lua 5.1" then
|
||||||
|
extract = function( v, from, width )
|
||||||
|
local w = 0
|
||||||
|
local flag = 2^from
|
||||||
|
for i = 0, width-1 do
|
||||||
|
local flag2 = flag + flag
|
||||||
|
if v % flag2 >= flag then
|
||||||
|
w = w + 2^i
|
||||||
|
end
|
||||||
|
flag = flag2
|
||||||
|
end
|
||||||
|
return w
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function base64.makeencoder( s62, s63, spad )
|
||||||
|
local encoder = {}
|
||||||
|
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
|
||||||
|
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
|
||||||
|
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
|
||||||
|
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
|
||||||
|
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
|
||||||
|
encoder[b64code] = char:byte()
|
||||||
|
end
|
||||||
|
return encoder
|
||||||
|
end
|
||||||
|
|
||||||
|
function base64.makedecoder( s62, s63, spad )
|
||||||
|
local decoder = {}
|
||||||
|
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
|
||||||
|
decoder[charcode] = b64code
|
||||||
|
end
|
||||||
|
return decoder
|
||||||
|
end
|
||||||
|
|
||||||
|
local DEFAULT_ENCODER = base64.makeencoder()
|
||||||
|
local DEFAULT_DECODER = base64.makedecoder()
|
||||||
|
|
||||||
|
local char, concat = string.char, table.concat
|
||||||
|
|
||||||
|
function base64.encode( arr, encoder )
|
||||||
|
encoder = encoder or DEFAULT_ENCODER
|
||||||
|
local t, k, n = {}, 1, #arr
|
||||||
|
local lastn = n % 3
|
||||||
|
for i = 1, n-lastn, 3 do
|
||||||
|
local a, b, c = arr[i], arr[i + 1], arr[i + 2]
|
||||||
|
local v = a*0x10000 + b*0x100 + c
|
||||||
|
local s
|
||||||
|
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
|
||||||
|
t[k] = s
|
||||||
|
k = k + 1
|
||||||
|
end
|
||||||
|
if lastn == 2 then
|
||||||
|
local a, b = arr[n-1], arr[n]
|
||||||
|
local v = a*0x10000 + b*0x100
|
||||||
|
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
|
||||||
|
elseif lastn == 1 then
|
||||||
|
local v = arr[n]*0x10000
|
||||||
|
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
|
||||||
|
end
|
||||||
|
return concat( t )
|
||||||
|
end
|
||||||
|
|
||||||
|
function base64.decode( b64, decoder )
|
||||||
|
decoder = decoder or DEFAULT_DECODER
|
||||||
|
local pattern = '[^%w%+%/%=]'
|
||||||
|
if decoder then
|
||||||
|
local s62, s63
|
||||||
|
for charcode, b64code in pairs( decoder ) do
|
||||||
|
if b64code == 62 then s62 = charcode
|
||||||
|
elseif b64code == 63 then s63 = charcode
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
|
||||||
|
end
|
||||||
|
b64 = b64:gsub( pattern, '' )
|
||||||
|
local t, k = {}, 1
|
||||||
|
local n = #b64
|
||||||
|
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
|
||||||
|
for i = 1, padding > 0 and n-4 or n, 4 do
|
||||||
|
local a, b, c, d = b64:byte( i, i+3 )
|
||||||
|
local s
|
||||||
|
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
|
||||||
|
table.insert(t,extract(v,16,8))
|
||||||
|
table.insert(t,extract(v,8,8))
|
||||||
|
table.insert(t,extract(v,0,8))
|
||||||
|
end
|
||||||
|
if padding == 1 then
|
||||||
|
local a, b, c = b64:byte( n-3, n-1 )
|
||||||
|
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
|
||||||
|
table.insert(t,extract(v,16,8))
|
||||||
|
table.insert(t,extract(v,8,8))
|
||||||
|
elseif padding == 2 then
|
||||||
|
local a, b = b64:byte( n-3, n-2 )
|
||||||
|
local v = decoder[a]*0x40000 + decoder[b]*0x1000
|
||||||
|
table.insert(t,extract(v,16,8))
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
|
||||||
|
return base64
|
||||||
564
data/lua/connector_bizhawk_generic.lua
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
--[[
|
||||||
|
Copyright (c) 2023 Zunawe
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local SCRIPT_VERSION = 1
|
||||||
|
|
||||||
|
--[[
|
||||||
|
This script expects to receive JSON and will send JSON back. A message should
|
||||||
|
be a list of 1 or more requests which will be executed in order. Each request
|
||||||
|
will have a corresponding response in the same order.
|
||||||
|
|
||||||
|
Every individual request and response is a JSON object with at minimum one
|
||||||
|
field `type`. The value of `type` determines what other fields may exist.
|
||||||
|
|
||||||
|
To get the script version, instead of JSON, send "VERSION" to get the script
|
||||||
|
version directly (e.g. "2").
|
||||||
|
|
||||||
|
#### Ex. 1
|
||||||
|
|
||||||
|
Request: `[{"type": "PING"}]`
|
||||||
|
|
||||||
|
Response: `[{"type": "PONG"}]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Ex. 2
|
||||||
|
|
||||||
|
Request: `[{"type": "LOCK"}, {"type": "HASH"}]`
|
||||||
|
|
||||||
|
Response: `[{"type": "LOCKED"}, {"type": "HASH_RESPONSE", "value": "F7D18982"}]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Ex. 3
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"},
|
||||||
|
{"type": "READ", "address": 500, "size": 4, "domain": "ROM"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"type": "GUARD_RESPONSE", "address": 100, "value": true},
|
||||||
|
{"type": "READ_RESPONSE", "value": "dGVzdA=="}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Ex. 4
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"},
|
||||||
|
{"type": "READ", "address": 500, "size": 4, "domain": "ROM"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"type": "GUARD_RESPONSE", "address": 100, "value": false},
|
||||||
|
{"type": "GUARD_RESPONSE", "address": 100, "value": false}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Supported Request Types
|
||||||
|
|
||||||
|
- `PING`
|
||||||
|
Does nothing; resets timeout.
|
||||||
|
|
||||||
|
Expected Response Type: `PONG`
|
||||||
|
|
||||||
|
- `SYSTEM`
|
||||||
|
Returns the system of the currently loaded ROM (N64, GBA, etc...).
|
||||||
|
|
||||||
|
Expected Response Type: `SYSTEM_RESPONSE`
|
||||||
|
|
||||||
|
- `PREFERRED_CORES`
|
||||||
|
Returns the user's default cores for systems with multiple cores. If the
|
||||||
|
current ROM's system has multiple cores, the one that is currently
|
||||||
|
running is very probably the preferred core.
|
||||||
|
|
||||||
|
Expected Response Type: `PREFERRED_CORES_RESPONSE`
|
||||||
|
|
||||||
|
- `HASH`
|
||||||
|
Returns the hash of the currently loaded ROM calculated by BizHawk.
|
||||||
|
|
||||||
|
Expected Response Type: `HASH_RESPONSE`
|
||||||
|
|
||||||
|
- `GUARD`
|
||||||
|
Checks a section of memory against `expected_data`. If the bytes starting
|
||||||
|
at `address` do not match `expected_data`, the response will have `value`
|
||||||
|
set to `false`, and all subsequent requests will not be executed and
|
||||||
|
receive the same `GUARD_RESPONSE`.
|
||||||
|
|
||||||
|
Expected Response Type: `GUARD_RESPONSE`
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `address` (`int`): The address of the memory to check
|
||||||
|
- `expected_data` (string): A base64 string of contiguous data
|
||||||
|
- `domain` (`string`): The name of the memory domain the address
|
||||||
|
corresponds to
|
||||||
|
|
||||||
|
- `LOCK`
|
||||||
|
Halts emulation and blocks on incoming requests until an `UNLOCK` request
|
||||||
|
is received or the client times out. All requests processed while locked
|
||||||
|
will happen on the same frame.
|
||||||
|
|
||||||
|
Expected Response Type: `LOCKED`
|
||||||
|
|
||||||
|
- `UNLOCK`
|
||||||
|
Resumes emulation after the current list of requests is done being
|
||||||
|
executed.
|
||||||
|
|
||||||
|
Expected Response Type: `UNLOCKED`
|
||||||
|
|
||||||
|
- `READ`
|
||||||
|
Reads an array of bytes at the provided address.
|
||||||
|
|
||||||
|
Expected Response Type: `READ_RESPONSE`
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `address` (`int`): The address of the memory to read
|
||||||
|
- `size` (`int`): The number of bytes to read
|
||||||
|
- `domain` (`string`): The name of the memory domain the address
|
||||||
|
corresponds to
|
||||||
|
|
||||||
|
- `WRITE`
|
||||||
|
Writes an array of bytes to the provided address.
|
||||||
|
|
||||||
|
Expected Response Type: `WRITE_RESPONSE`
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `address` (`int`): The address of the memory to write to
|
||||||
|
- `value` (`string`): A base64 string representing the data to write
|
||||||
|
- `domain` (`string`): The name of the memory domain the address
|
||||||
|
corresponds to
|
||||||
|
|
||||||
|
- `DISPLAY_MESSAGE`
|
||||||
|
Adds a message to the message queue which will be displayed using
|
||||||
|
`gui.addmessage` according to the message interval.
|
||||||
|
|
||||||
|
Expected Response Type: `DISPLAY_MESSAGE_RESPONSE`
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `message` (`string`): The string to display
|
||||||
|
|
||||||
|
- `SET_MESSAGE_INTERVAL`
|
||||||
|
Sets the minimum amount of time to wait between displaying messages.
|
||||||
|
Potentially useful if you add many messages quickly but want players
|
||||||
|
to be able to read each of them.
|
||||||
|
|
||||||
|
Expected Response Type: `SET_MESSAGE_INTERVAL_RESPONSE`
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `value` (`number`): The number of seconds to set the interval to
|
||||||
|
|
||||||
|
|
||||||
|
### Response Types
|
||||||
|
|
||||||
|
- `PONG`
|
||||||
|
Acknowledges `PING`.
|
||||||
|
|
||||||
|
- `SYSTEM_RESPONSE`
|
||||||
|
Contains the name of the system for currently running ROM.
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `value` (`string`): The returned system name
|
||||||
|
|
||||||
|
- `PREFERRED_CORES_RESPONSE`
|
||||||
|
Contains the user's preferred cores for systems with multiple supported
|
||||||
|
cores. Currently includes NES, SNES, GB, GBC, DGB, SGB, PCE, PCECD, and
|
||||||
|
SGX.
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `value` (`{[string]: [string]}`): A dictionary map from system name to
|
||||||
|
core name
|
||||||
|
|
||||||
|
- `HASH_RESPONSE`
|
||||||
|
Contains the hash of the currently loaded ROM calculated by BizHawk.
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `value` (`string`): The returned hash
|
||||||
|
|
||||||
|
- `GUARD_RESPONSE`
|
||||||
|
The result of an attempted `GUARD` request.
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `value` (`boolean`): true if the memory was validated, false if not
|
||||||
|
- `address` (`int`): The address of the memory that was invalid (the same
|
||||||
|
address provided by the `GUARD`, not the address of the individual invalid
|
||||||
|
byte)
|
||||||
|
|
||||||
|
- `LOCKED`
|
||||||
|
Acknowledges `LOCK`.
|
||||||
|
|
||||||
|
- `UNLOCKED`
|
||||||
|
Acknowledges `UNLOCK`.
|
||||||
|
|
||||||
|
- `READ_RESPONSE`
|
||||||
|
Contains the result of a `READ` request.
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `value` (`string`): A base64 string representing the read data
|
||||||
|
|
||||||
|
- `WRITE_RESPONSE`
|
||||||
|
Acknowledges `WRITE`.
|
||||||
|
|
||||||
|
- `DISPLAY_MESSAGE_RESPONSE`
|
||||||
|
Acknowledges `DISPLAY_MESSAGE`.
|
||||||
|
|
||||||
|
- `SET_MESSAGE_INTERVAL_RESPONSE`
|
||||||
|
Acknowledges `SET_MESSAGE_INTERVAL`.
|
||||||
|
|
||||||
|
- `ERROR`
|
||||||
|
Signifies that something has gone wrong while processing a request.
|
||||||
|
|
||||||
|
Additional Fields:
|
||||||
|
- `err` (`string`): A description of the problem
|
||||||
|
]]
|
||||||
|
|
||||||
|
local base64 = require("base64")
|
||||||
|
local socket = require("socket")
|
||||||
|
local json = require("json")
|
||||||
|
|
||||||
|
-- Set to log incoming requests
|
||||||
|
-- Will cause lag due to large console output
|
||||||
|
local DEBUG = false
|
||||||
|
|
||||||
|
local SOCKET_PORT = 43055
|
||||||
|
|
||||||
|
local STATE_NOT_CONNECTED = 0
|
||||||
|
local STATE_CONNECTED = 1
|
||||||
|
|
||||||
|
local server = nil
|
||||||
|
local client_socket = nil
|
||||||
|
|
||||||
|
local current_state = STATE_NOT_CONNECTED
|
||||||
|
|
||||||
|
local timeout_timer = 0
|
||||||
|
local message_timer = 0
|
||||||
|
local message_interval = 0
|
||||||
|
local prev_time = 0
|
||||||
|
local current_time = 0
|
||||||
|
|
||||||
|
local locked = false
|
||||||
|
|
||||||
|
local rom_hash = nil
|
||||||
|
|
||||||
|
local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)")
|
||||||
|
lua_major = tonumber(lua_major)
|
||||||
|
lua_minor = tonumber(lua_minor)
|
||||||
|
|
||||||
|
if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then
|
||||||
|
require("lua_5_3_compat")
|
||||||
|
end
|
||||||
|
|
||||||
|
local bizhawk_version = client.getversion()
|
||||||
|
local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)")
|
||||||
|
bizhawk_major = tonumber(bizhawk_major)
|
||||||
|
bizhawk_minor = tonumber(bizhawk_minor)
|
||||||
|
if bizhawk_patch == "" then
|
||||||
|
bizhawk_patch = 0
|
||||||
|
else
|
||||||
|
bizhawk_patch = tonumber(bizhawk_patch)
|
||||||
|
end
|
||||||
|
|
||||||
|
function queue_push (self, value)
|
||||||
|
self[self.right] = value
|
||||||
|
self.right = self.right + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
function queue_is_empty (self)
|
||||||
|
return self.right == self.left
|
||||||
|
end
|
||||||
|
|
||||||
|
function queue_shift (self)
|
||||||
|
value = self[self.left]
|
||||||
|
self[self.left] = nil
|
||||||
|
self.left = self.left + 1
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
function new_queue ()
|
||||||
|
local queue = {left = 1, right = 1}
|
||||||
|
return setmetatable(queue, {__index = {is_empty = queue_is_empty, push = queue_push, shift = queue_shift}})
|
||||||
|
end
|
||||||
|
|
||||||
|
local message_queue = new_queue()
|
||||||
|
|
||||||
|
function lock ()
|
||||||
|
locked = true
|
||||||
|
client_socket:settimeout(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
function unlock ()
|
||||||
|
locked = false
|
||||||
|
client_socket:settimeout(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function process_request (req)
|
||||||
|
local res = {}
|
||||||
|
|
||||||
|
if req["type"] == "PING" then
|
||||||
|
res["type"] = "PONG"
|
||||||
|
|
||||||
|
elseif req["type"] == "SYSTEM" then
|
||||||
|
res["type"] = "SYSTEM_RESPONSE"
|
||||||
|
res["value"] = emu.getsystemid()
|
||||||
|
|
||||||
|
elseif req["type"] == "PREFERRED_CORES" then
|
||||||
|
local preferred_cores = client.getconfig().PreferredCores
|
||||||
|
res["type"] = "PREFERRED_CORES_RESPONSE"
|
||||||
|
res["value"] = {}
|
||||||
|
res["value"]["NES"] = preferred_cores.NES
|
||||||
|
res["value"]["SNES"] = preferred_cores.SNES
|
||||||
|
res["value"]["GB"] = preferred_cores.GB
|
||||||
|
res["value"]["GBC"] = preferred_cores.GBC
|
||||||
|
res["value"]["DGB"] = preferred_cores.DGB
|
||||||
|
res["value"]["SGB"] = preferred_cores.SGB
|
||||||
|
res["value"]["PCE"] = preferred_cores.PCE
|
||||||
|
res["value"]["PCECD"] = preferred_cores.PCECD
|
||||||
|
res["value"]["SGX"] = preferred_cores.SGX
|
||||||
|
|
||||||
|
elseif req["type"] == "HASH" then
|
||||||
|
res["type"] = "HASH_RESPONSE"
|
||||||
|
res["value"] = rom_hash
|
||||||
|
|
||||||
|
elseif req["type"] == "GUARD" then
|
||||||
|
res["type"] = "GUARD_RESPONSE"
|
||||||
|
local expected_data = base64.decode(req["expected_data"])
|
||||||
|
|
||||||
|
local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"])
|
||||||
|
|
||||||
|
local data_is_validated = true
|
||||||
|
for i, byte in ipairs(actual_data) do
|
||||||
|
if byte ~= expected_data[i] then
|
||||||
|
data_is_validated = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
res["value"] = data_is_validated
|
||||||
|
res["address"] = req["address"]
|
||||||
|
|
||||||
|
elseif req["type"] == "LOCK" then
|
||||||
|
res["type"] = "LOCKED"
|
||||||
|
lock()
|
||||||
|
|
||||||
|
elseif req["type"] == "UNLOCK" then
|
||||||
|
res["type"] = "UNLOCKED"
|
||||||
|
unlock()
|
||||||
|
|
||||||
|
elseif req["type"] == "READ" then
|
||||||
|
res["type"] = "READ_RESPONSE"
|
||||||
|
res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"]))
|
||||||
|
|
||||||
|
elseif req["type"] == "WRITE" then
|
||||||
|
res["type"] = "WRITE_RESPONSE"
|
||||||
|
memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"])
|
||||||
|
|
||||||
|
elseif req["type"] == "DISPLAY_MESSAGE" then
|
||||||
|
res["type"] = "DISPLAY_MESSAGE_RESPONSE"
|
||||||
|
message_queue:push(req["message"])
|
||||||
|
|
||||||
|
elseif req["type"] == "SET_MESSAGE_INTERVAL" then
|
||||||
|
res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE"
|
||||||
|
message_interval = req["value"]
|
||||||
|
|
||||||
|
else
|
||||||
|
res["type"] = "ERROR"
|
||||||
|
res["err"] = "Unknown command: "..req["type"]
|
||||||
|
end
|
||||||
|
|
||||||
|
return res
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Receive data from AP client and send message back
|
||||||
|
function send_receive ()
|
||||||
|
local message, err = client_socket:receive()
|
||||||
|
|
||||||
|
-- Handle errors
|
||||||
|
if err == "closed" then
|
||||||
|
if current_state == STATE_CONNECTED then
|
||||||
|
print("Connection to client closed")
|
||||||
|
end
|
||||||
|
current_state = STATE_NOT_CONNECTED
|
||||||
|
return
|
||||||
|
elseif err == "timeout" then
|
||||||
|
unlock()
|
||||||
|
return
|
||||||
|
elseif err ~= nil then
|
||||||
|
print(err)
|
||||||
|
current_state = STATE_NOT_CONNECTED
|
||||||
|
unlock()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reset timeout timer
|
||||||
|
timeout_timer = 5
|
||||||
|
|
||||||
|
-- Process received data
|
||||||
|
if DEBUG then
|
||||||
|
print("Received Message ["..emu.framecount().."]: "..'"'..message..'"')
|
||||||
|
end
|
||||||
|
|
||||||
|
if message == "VERSION" then
|
||||||
|
local result, err client_socket:send(tostring(SCRIPT_VERSION).."\n")
|
||||||
|
else
|
||||||
|
local res = {}
|
||||||
|
local data = json.decode(message)
|
||||||
|
local failed_guard_response = nil
|
||||||
|
for i, req in ipairs(data) do
|
||||||
|
if failed_guard_response ~= nil then
|
||||||
|
res[i] = failed_guard_response
|
||||||
|
else
|
||||||
|
-- An error is more likely to cause an NLua exception than to return an error here
|
||||||
|
local status, response = pcall(process_request, req)
|
||||||
|
if status then
|
||||||
|
res[i] = response
|
||||||
|
|
||||||
|
-- If the GUARD validation failed, skip the remaining commands
|
||||||
|
if response["type"] == "GUARD_RESPONSE" and not response["value"] then
|
||||||
|
failed_guard_response = response
|
||||||
|
end
|
||||||
|
else
|
||||||
|
res[i] = {type = "ERROR", err = response}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
client_socket:send(json.encode(res).."\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function main ()
|
||||||
|
server, err = socket.bind("localhost", SOCKET_PORT)
|
||||||
|
if err ~= nil then
|
||||||
|
print(err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
current_time = socket.socket.gettime()
|
||||||
|
timeout_timer = timeout_timer - (current_time - prev_time)
|
||||||
|
message_timer = message_timer - (current_time - prev_time)
|
||||||
|
prev_time = current_time
|
||||||
|
|
||||||
|
if message_timer <= 0 and not message_queue:is_empty() then
|
||||||
|
gui.addmessage(message_queue:shift())
|
||||||
|
message_timer = message_interval
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_state == STATE_NOT_CONNECTED then
|
||||||
|
if emu.framecount() % 60 == 0 then
|
||||||
|
server:settimeout(2)
|
||||||
|
local client, timeout = server:accept()
|
||||||
|
if timeout == nil then
|
||||||
|
print("Client connected")
|
||||||
|
current_state = STATE_CONNECTED
|
||||||
|
client_socket = client
|
||||||
|
client_socket:settimeout(0)
|
||||||
|
else
|
||||||
|
print("No client found. Trying again...")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
repeat
|
||||||
|
send_receive()
|
||||||
|
until not locked
|
||||||
|
|
||||||
|
if timeout_timer <= 0 then
|
||||||
|
print("Client timed out")
|
||||||
|
current_state = STATE_NOT_CONNECTED
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
event.onexit(function ()
|
||||||
|
print("\n-- Restarting Script --\n")
|
||||||
|
if server ~= nil then
|
||||||
|
server:close()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then
|
||||||
|
print("Must use BizHawk 2.7.0 or newer")
|
||||||
|
elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then
|
||||||
|
print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.")
|
||||||
|
else
|
||||||
|
if emu.getsystemid() == "NULL" then
|
||||||
|
print("No ROM is loaded. Please load a ROM.")
|
||||||
|
while emu.getsystemid() == "NULL" do
|
||||||
|
emu.frameadvance()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
rom_hash = gameinfo.getromhash()
|
||||||
|
|
||||||
|
print("Waiting for client to connect. Emulation will freeze intermittently until a client is found.\n")
|
||||||
|
|
||||||
|
local co = coroutine.create(main)
|
||||||
|
function tick ()
|
||||||
|
local status, err = coroutine.resume(co)
|
||||||
|
|
||||||
|
if not status then
|
||||||
|
print("\nERROR: "..err)
|
||||||
|
print("Consider reporting this crash.\n")
|
||||||
|
|
||||||
|
if server ~= nil then
|
||||||
|
server:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
co = coroutine.create(main)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gambatte has a setting which can cause script execution to become
|
||||||
|
-- misaligned, so for GB and GBC we explicitly set the callback on
|
||||||
|
-- vblank instead.
|
||||||
|
-- https://github.com/TASEmulators/BizHawk/issues/3711
|
||||||
|
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then
|
||||||
|
event.onmemoryexecute(tick, 0x40, "tick", "System Bus")
|
||||||
|
else
|
||||||
|
event.onframeend(tick)
|
||||||
|
end
|
||||||
|
|
||||||
|
while true do
|
||||||
|
emu.frameadvance()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -67,6 +67,7 @@ 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,
|
||||||
@@ -173,11 +174,18 @@ 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 = u8(itemsObtained)
|
currentItemsObtained = getItemsObtained()
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -364,8 +372,8 @@ local function gotItem(item)
|
|||||||
wU8(0x505, itemCode)
|
wU8(0x505, itemCode)
|
||||||
wU8(0x506, 128)
|
wU8(0x506, 128)
|
||||||
wU8(0x602, 4)
|
wU8(0x602, 4)
|
||||||
numberObtained = u8(itemsObtained) + 1
|
numberObtained = getItemsObtained() + 1
|
||||||
wU8(itemsObtained, numberObtained)
|
setItemsObtained(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
|
||||||
@@ -476,7 +484,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)
|
||||||
wU8(itemsObtained, u8(itemsObtained) - 1)
|
setItemsObtained(getItemsObtained() - 1)
|
||||||
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
|
wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -494,7 +502,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 > u8(itemsObtained) then
|
if i > getItemsObtained() then
|
||||||
gotItem(item)
|
gotItem(item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -546,7 +554,7 @@ function receive()
|
|||||||
retTable["gameMode"] = gameMode
|
retTable["gameMode"] = gameMode
|
||||||
retTable["overworldHC"] = getHCLocation()
|
retTable["overworldHC"] = getHCLocation()
|
||||||
retTable["overworldPB"] = getPBLocation()
|
retTable["overworldPB"] = getPBLocation()
|
||||||
retTable["itemsObtained"] = u8(itemsObtained)
|
retTable["itemsObtained"] = getItemsObtained()
|
||||||
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
|
||||||
@@ -606,4 +614,4 @@ function main()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -559,6 +559,12 @@ def generate_basic(self) -> None:
|
|||||||
# in most cases it's better to do this at the same time the itempool is
|
# in most cases it's better to do this at the same time the itempool is
|
||||||
# filled to avoid accidental duplicates:
|
# filled to avoid accidental duplicates:
|
||||||
# manually placed and still in the itempool
|
# manually placed and still in the itempool
|
||||||
|
|
||||||
|
# for debugging purposes, you may want to visualize the layout of your world. Uncomment the following code to
|
||||||
|
# write a PlantUML diagram to the file "my_world.puml" that can help you see whether your regions and locations
|
||||||
|
# are connected and placed as desired
|
||||||
|
# from Utils import visualize_regions
|
||||||
|
# visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Setting Rules
|
### Setting Rules
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup";
|
|||||||
Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
||||||
|
Name: "client/bizhawk"; Description: "BizHawk Client"; Types: full playing
|
||||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||||
Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing
|
Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing
|
||||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||||
@@ -116,11 +117,13 @@ Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI";
|
|||||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||||
|
|
||||||
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
|
Source: "{#source_path}\ArchipelagoLauncher.exe"; DestDir: "{app}"; Flags: ignoreversion;
|
||||||
|
Source: "{#source_path}\ArchipelagoLauncher(DEBUG).exe"; DestDir: "{app}"; Flags: ignoreversion;
|
||||||
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||||
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||||
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||||
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
||||||
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
|
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
|
||||||
|
Source: "{#source_path}\ArchipelagoBizHawkClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/bizhawk
|
||||||
Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx
|
Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx
|
||||||
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
|
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
|
||||||
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||||
@@ -145,6 +148,7 @@ Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"
|
|||||||
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server
|
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server
|
||||||
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
|
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
|
||||||
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
|
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
|
||||||
|
Name: "{group}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Components: client/bizhawk
|
||||||
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
||||||
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
|
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
|
||||||
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
|
Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot
|
||||||
@@ -165,6 +169,7 @@ Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopic
|
|||||||
Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon
|
Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon
|
||||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server
|
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server
|
||||||
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
|
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
|
||||||
|
Name: "{commondesktop}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Tasks: desktopicon; Components: client/bizhawk
|
||||||
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
|
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
|
||||||
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
|
Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft
|
||||||
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
|
Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot
|
||||||
|
|||||||
10
kvui.py
@@ -1,7 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import ctypes
|
||||||
|
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
|
||||||
|
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
|
||||||
|
try:
|
||||||
|
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
||||||
|
except FileNotFoundError: # shcore may not be found on <= Windows 7
|
||||||
|
pass # TODO: remove silent except when Python 3.8 is phased out.
|
||||||
|
|
||||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||||
os.environ["KIVY_NO_ARGS"] = "1"
|
os.environ["KIVY_NO_ARGS"] = "1"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
|
|||||||
game: # Pick a game to play
|
game: # Pick a game to play
|
||||||
A Link to the Past: 1
|
A Link to the Past: 1
|
||||||
requires:
|
requires:
|
||||||
version: 0.3.3 # Version of Archipelago required for this yaml to work as expected.
|
version: 0.4.3 # Version of Archipelago required for this yaml to work as expected.
|
||||||
A Link to the Past:
|
A Link to the Past:
|
||||||
progression_balancing:
|
progression_balancing:
|
||||||
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||||
@@ -114,6 +114,9 @@ A Link to the Past:
|
|||||||
different_world: 0
|
different_world: 0
|
||||||
universal: 0
|
universal: 0
|
||||||
start_with: 0
|
start_with: 0
|
||||||
|
key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies
|
||||||
|
off: 50
|
||||||
|
on: 0
|
||||||
compass_shuffle: # Compass Placement
|
compass_shuffle: # Compass Placement
|
||||||
original_dungeon: 50
|
original_dungeon: 50
|
||||||
own_dungeons: 0
|
own_dungeons: 0
|
||||||
|
|||||||
@@ -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.0
|
jellyfish>=1.0.1
|
||||||
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.7
|
cymem>=2.0.8
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class Group:
|
|||||||
cls._type_cache = typing.get_type_hints(cls, globalns=mod_dict, localns=cls.__dict__)
|
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) -> Any:
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
if key in self:
|
if key in self:
|
||||||
return self[key]
|
return self[key]
|
||||||
return default
|
return default
|
||||||
|
|||||||
13
setup.py
@@ -80,7 +80,6 @@ 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",
|
||||||
@@ -91,6 +90,7 @@ 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,13 +185,22 @@ 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 []
|
||||||
|
|
||||||
|
|||||||