mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-18 21:38:13 -07:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
092e8d14ad | ||
|
|
4cfc73b582 | ||
|
|
ff9c11d772 | ||
|
|
b83aec5c12 | ||
|
|
caf63dd737 | ||
|
|
395d35571c | ||
|
|
e0be79639c | ||
|
|
37b7f0d32d | ||
|
|
50677ee6a2 | ||
|
|
f8bc3359c7 | ||
|
|
6e537e17e6 | ||
|
|
e853fc208b | ||
|
|
1a36da33b4 | ||
|
|
56fc614588 | ||
|
|
47f1fcf382 | ||
|
|
51c6be047f | ||
|
|
2c46c48ba9 | ||
|
|
32820ba653 | ||
|
|
6173bc6e03 | ||
|
|
e71ea94fe5 | ||
|
|
e3f169b4c3 | ||
|
|
e4e74074f0 | ||
|
|
149630d532 | ||
|
|
2dcfbff751 | ||
|
|
ec45479c52 | ||
|
|
aee0df5359 | ||
|
|
2cdd03f786 | ||
|
|
ce42fda85f | ||
|
|
78a18dee4e | ||
|
|
b7d46004e2 | ||
|
|
c3fe341736 | ||
|
|
79bb43b77c | ||
|
|
bedc78d335 | ||
|
|
1b582e5b09 | ||
|
|
f278dd95c5 | ||
|
|
92f75f3e03 | ||
|
|
f5adc7bdc5 | ||
|
|
78d4da53a7 | ||
|
|
e206c065bf | ||
|
|
5273812039 | ||
|
|
7c3af68e59 | ||
|
|
449973687b | ||
|
|
f5638552cc | ||
|
|
78ee19de51 | ||
|
|
82444229be | ||
|
|
2cc03d003a | ||
|
|
0e4fa378dd | ||
|
|
ffc000ec91 | ||
|
|
32b8f9f9f3 | ||
|
|
4412434976 | ||
|
|
9bdbced51f | ||
|
|
bd574ef261 | ||
|
|
45719eb7e0 | ||
|
|
d81fd280fa | ||
|
|
6b57275859 | ||
|
|
63f012cce7 | ||
|
|
679cb3e197 | ||
|
|
38b5a90c07 | ||
|
|
203f17f0f6 | ||
|
|
65995cd586 | ||
|
|
64e2d55e92 | ||
|
|
ef66f64030 | ||
|
|
e641c3ca1b | ||
|
|
111c3186bd | ||
|
|
f0e9080108 | ||
|
|
fd8867c782 | ||
|
|
f81d2653e0 | ||
|
|
1288f15e45 | ||
|
|
cde2a6e754 | ||
|
|
81dd1e359b | ||
|
|
8dffd87bee | ||
|
|
67be80e59d | ||
|
|
ff1f5569e7 | ||
|
|
8b9b482972 | ||
|
|
d0ce44cd38 | ||
|
|
aae78a8a12 | ||
|
|
7a5e11e8d4 | ||
|
|
a9ab53cb8b | ||
|
|
5ed8c2e1c0 | ||
|
|
67128ece38 | ||
|
|
8aed24151f |
2
.github/workflows/unittests.yml
vendored
2
.github/workflows/unittests.yml
vendored
@@ -37,4 +37,4 @@ jobs:
|
|||||||
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
|
||||||
- name: Unittests
|
- name: Unittests
|
||||||
run: |
|
run: |
|
||||||
pytest test
|
pytest
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,6 +4,7 @@
|
|||||||
*_Spoiler.txt
|
*_Spoiler.txt
|
||||||
*.bmbp
|
*.bmbp
|
||||||
*.apbp
|
*.apbp
|
||||||
|
*.apl2ac
|
||||||
*.apm3
|
*.apm3
|
||||||
*.apmc
|
*.apmc
|
||||||
*.apz5
|
*.apz5
|
||||||
@@ -48,7 +49,7 @@ Output Logs/
|
|||||||
/freeze_requirements.txt
|
/freeze_requirements.txt
|
||||||
/Archipelago.zip
|
/Archipelago.zip
|
||||||
/setup.ini
|
/setup.ini
|
||||||
|
/installdelete.iss
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@@ -135,6 +136,7 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
.code-workspace
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
|
|||||||
223
BaseClasses.py
223
BaseClasses.py
@@ -26,6 +26,7 @@ class Group(TypedDict, total=False):
|
|||||||
replacement_items: Dict[int, Optional[str]]
|
replacement_items: Dict[int, Optional[str]]
|
||||||
local_items: Set[str]
|
local_items: Set[str]
|
||||||
non_local_items: Set[str]
|
non_local_items: Set[str]
|
||||||
|
link_replacement: bool
|
||||||
|
|
||||||
|
|
||||||
class MultiWorld():
|
class MultiWorld():
|
||||||
@@ -160,7 +161,7 @@ class MultiWorld():
|
|||||||
self.worlds = {}
|
self.worlds = {}
|
||||||
self.slot_seeds = {}
|
self.slot_seeds = {}
|
||||||
|
|
||||||
def get_all_ids(self):
|
def get_all_ids(self) -> Tuple[int, ...]:
|
||||||
return self.player_ids + tuple(self.groups)
|
return self.player_ids + tuple(self.groups)
|
||||||
|
|
||||||
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
||||||
@@ -222,27 +223,32 @@ class MultiWorld():
|
|||||||
|
|
||||||
def set_item_links(self):
|
def set_item_links(self):
|
||||||
item_links = {}
|
item_links = {}
|
||||||
|
replacement_prio = [False, True, None]
|
||||||
for player in self.player_ids:
|
for player in self.player_ids:
|
||||||
for item_link in self.item_links[player].value:
|
for item_link in self.item_links[player].value:
|
||||||
if item_link["name"] in item_links:
|
if item_link["name"] in item_links:
|
||||||
if item_links[item_link["name"]]["game"] != self.game[player]:
|
if item_links[item_link["name"]]["game"] != self.game[player]:
|
||||||
raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}")
|
raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}")
|
||||||
item_links[item_link["name"]]["players"][player] = item_link["replacement_item"]
|
current_link = item_links[item_link["name"]]
|
||||||
item_links[item_link["name"]]["item_pool"] &= set(item_link["item_pool"])
|
current_link["players"][player] = item_link["replacement_item"]
|
||||||
item_links[item_link["name"]]["exclude"] |= set(item_link.get("exclude", []))
|
current_link["item_pool"] &= set(item_link["item_pool"])
|
||||||
item_links[item_link["name"]]["local_items"] &= set(item_link.get("local_items", []))
|
current_link["exclude"] |= set(item_link.get("exclude", []))
|
||||||
item_links[item_link["name"]]["non_local_items"] &= set(item_link.get("non_local_items", []))
|
current_link["local_items"] &= set(item_link.get("local_items", []))
|
||||||
|
current_link["non_local_items"] &= set(item_link.get("non_local_items", []))
|
||||||
|
current_link["link_replacement"] = min(current_link["link_replacement"],
|
||||||
|
replacement_prio.index(item_link["link_replacement"]))
|
||||||
else:
|
else:
|
||||||
if item_link["name"] in self.player_name.values():
|
if item_link["name"] in self.player_name.values():
|
||||||
raise Exception(f"Cannot name a ItemLink group the same as a player ({item_link['name']}) ({self.get_player_name(player)}).")
|
raise Exception(f"Cannot name a ItemLink group the same as a player ({item_link['name']}) "
|
||||||
|
f"({self.get_player_name(player)}).")
|
||||||
item_links[item_link["name"]] = {
|
item_links[item_link["name"]] = {
|
||||||
"players": {player: item_link["replacement_item"]},
|
"players": {player: item_link["replacement_item"]},
|
||||||
"item_pool": set(item_link["item_pool"]),
|
"item_pool": set(item_link["item_pool"]),
|
||||||
"exclude": set(item_link.get("exclude", [])),
|
"exclude": set(item_link.get("exclude", [])),
|
||||||
"game": self.game[player],
|
"game": self.game[player],
|
||||||
"local_items": set(item_link.get("local_items", [])),
|
"local_items": set(item_link.get("local_items", [])),
|
||||||
"non_local_items": set(item_link.get("non_local_items", []))
|
"non_local_items": set(item_link.get("non_local_items", [])),
|
||||||
|
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, item_link in item_links.items():
|
for name, item_link in item_links.items():
|
||||||
@@ -267,10 +273,12 @@ class MultiWorld():
|
|||||||
for group_name, item_link in item_links.items():
|
for group_name, item_link in item_links.items():
|
||||||
game = item_link["game"]
|
game = item_link["game"]
|
||||||
group_id, group = self.add_group(group_name, game, set(item_link["players"]))
|
group_id, group = self.add_group(group_name, game, set(item_link["players"]))
|
||||||
|
|
||||||
group["item_pool"] = item_link["item_pool"]
|
group["item_pool"] = item_link["item_pool"]
|
||||||
group["replacement_items"] = item_link["players"]
|
group["replacement_items"] = item_link["players"]
|
||||||
group["local_items"] = item_link["local_items"]
|
group["local_items"] = item_link["local_items"]
|
||||||
group["non_local_items"] = item_link["non_local_items"]
|
group["non_local_items"] = item_link["non_local_items"]
|
||||||
|
group["link_replacement"] = replacement_prio[item_link["link_replacement"]]
|
||||||
|
|
||||||
# intended for unittests
|
# intended for unittests
|
||||||
def set_default_common_options(self):
|
def set_default_common_options(self):
|
||||||
@@ -285,11 +293,11 @@ class MultiWorld():
|
|||||||
self.is_race = True
|
self.is_race = True
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def player_ids(self):
|
def player_ids(self) -> Tuple[int, ...]:
|
||||||
return tuple(range(1, self.players + 1))
|
return tuple(range(1, self.players + 1))
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache()
|
||||||
def get_game_players(self, game_name: str):
|
def get_game_players(self, game_name: str) -> Tuple[int, ...]:
|
||||||
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
return tuple(player for player in self.player_ids if self.game[player] == game_name)
|
||||||
|
|
||||||
@functools.lru_cache()
|
@functools.lru_cache()
|
||||||
@@ -308,10 +316,7 @@ class MultiWorld():
|
|||||||
|
|
||||||
def get_out_file_name_base(self, player: int) -> str:
|
def get_out_file_name_base(self, player: int) -> str:
|
||||||
""" the base name (without file extension) for each player's output file for a seed """
|
""" the base name (without file extension) for each player's output file for a seed """
|
||||||
return f"AP_{self.seed_name}_P{player}" \
|
return f"AP_{self.seed_name}_P{player}_{self.get_file_safe_player_name(player).replace(' ', '_')}"
|
||||||
+ (f"_{self.get_file_safe_player_name(player).replace(' ', '_')}"
|
|
||||||
if (self.player_name[player] != f"Player{player}")
|
|
||||||
else '')
|
|
||||||
|
|
||||||
def initialize_regions(self, regions=None):
|
def initialize_regions(self, regions=None):
|
||||||
for region in regions if regions else self.regions:
|
for region in regions if regions else self.regions:
|
||||||
@@ -427,46 +432,35 @@ class MultiWorld():
|
|||||||
state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
|
state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic."""
|
||||||
self.indirect_connections.setdefault(region, set()).add(entrance)
|
self.indirect_connections.setdefault(region, set()).add(entrance)
|
||||||
|
|
||||||
def get_locations(self) -> List[Location]:
|
def get_locations(self, player: Optional[int] = None) -> List[Location]:
|
||||||
if self._cached_locations is None:
|
if self._cached_locations is None:
|
||||||
self._cached_locations = [location for region in self.regions for location in region.locations]
|
self._cached_locations = [location for region in self.regions for location in region.locations]
|
||||||
|
if player is not None:
|
||||||
|
return [location for location in self._cached_locations if location.player == player]
|
||||||
return self._cached_locations
|
return self._cached_locations
|
||||||
|
|
||||||
def clear_location_cache(self):
|
def clear_location_cache(self):
|
||||||
self._cached_locations = None
|
self._cached_locations = None
|
||||||
|
|
||||||
def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]:
|
def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]:
|
||||||
if player is not None:
|
return [location for location in self.get_locations(player) if location.item is None]
|
||||||
return [location for location in self.get_locations() if
|
|
||||||
location.player == player and not location.item]
|
|
||||||
return [location for location in self.get_locations() if not location.item]
|
|
||||||
|
|
||||||
def get_unfilled_dungeon_locations(self):
|
|
||||||
return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon]
|
|
||||||
|
|
||||||
def get_filled_locations(self, player: Optional[int] = None) -> List[Location]:
|
def get_filled_locations(self, player: Optional[int] = None) -> List[Location]:
|
||||||
if player is not None:
|
return [location for location in self.get_locations(player) if location.item is not None]
|
||||||
return [location for location in self.get_locations() if
|
|
||||||
location.player == player and location.item is not None]
|
|
||||||
return [location for location in self.get_locations() if location.item is not None]
|
|
||||||
|
|
||||||
def get_reachable_locations(self, state: Optional[CollectionState] = None, player: Optional[int] = None) -> List[Location]:
|
def get_reachable_locations(self, state: Optional[CollectionState] = None, player: Optional[int] = None) -> List[Location]:
|
||||||
if state is None:
|
state: CollectionState = state if state else self.state
|
||||||
state = self.state
|
return [location for location in self.get_locations(player) if location.can_reach(state)]
|
||||||
return [location for location in self.get_locations() if
|
|
||||||
(player is None or location.player == player) and location.can_reach(state)]
|
|
||||||
|
|
||||||
def get_placeable_locations(self, state=None, player=None) -> List[Location]:
|
def get_placeable_locations(self, state=None, player=None) -> List[Location]:
|
||||||
if state is None:
|
state: CollectionState = state if state else self.state
|
||||||
state = self.state
|
return [location for location in self.get_locations(player) if location.item is None and location.can_reach(state)]
|
||||||
return [location for location in self.get_locations() if
|
|
||||||
(player is None or location.player == player) and location.item is None and location.can_reach(state)]
|
|
||||||
|
|
||||||
def get_unfilled_locations_for_players(self, locations: List[str], players: Iterable[int]):
|
def get_unfilled_locations_for_players(self, location_names: List[str], players: Iterable[int]):
|
||||||
for player in players:
|
for player in players:
|
||||||
if len(locations) == 0:
|
if not location_names:
|
||||||
locations = [location.name for location in self.get_unfilled_locations(player)]
|
location_names = [location.name for location in self.get_unfilled_locations(player)]
|
||||||
for location_name in locations:
|
for location_name in location_names:
|
||||||
location = self._location_cache.get((location_name, player), None)
|
location = self._location_cache.get((location_name, player), None)
|
||||||
if location is not None and location.item is None:
|
if location is not None and location.item is None:
|
||||||
yield location
|
yield location
|
||||||
@@ -1375,6 +1369,157 @@ class Spoiler():
|
|||||||
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
||||||
self.bosses[str(player)]["Ganon"] = "Ganon"
|
self.bosses[str(player)]["Ganon"] = "Ganon"
|
||||||
|
|
||||||
|
def create_playthrough(self, create_paths: bool = True):
|
||||||
|
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
||||||
|
from itertools import chain
|
||||||
|
# get locations containing progress items
|
||||||
|
multiworld = self.multiworld
|
||||||
|
prog_locations = {location for location in multiworld.get_filled_locations() if location.item.advancement}
|
||||||
|
state_cache = [None]
|
||||||
|
collection_spheres: List[Set[Location]] = []
|
||||||
|
state = CollectionState(multiworld)
|
||||||
|
sphere_candidates = set(prog_locations)
|
||||||
|
logging.debug('Building up collection spheres.')
|
||||||
|
while sphere_candidates:
|
||||||
|
|
||||||
|
# build up spheres of collection radius.
|
||||||
|
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||||
|
|
||||||
|
sphere = {location for location in sphere_candidates if state.can_reach(location)}
|
||||||
|
|
||||||
|
for location in sphere:
|
||||||
|
state.collect(location.item, True, location)
|
||||||
|
|
||||||
|
sphere_candidates -= sphere
|
||||||
|
collection_spheres.append(sphere)
|
||||||
|
state_cache.append(state.copy())
|
||||||
|
|
||||||
|
logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres),
|
||||||
|
len(sphere),
|
||||||
|
len(prog_locations))
|
||||||
|
if not sphere:
|
||||||
|
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
|
||||||
|
location.item.name, location.item.player, location.name, location.player) for location in
|
||||||
|
sphere_candidates])
|
||||||
|
if any([multiworld.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
|
||||||
|
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
||||||
|
f'Something went terribly wrong here.')
|
||||||
|
else:
|
||||||
|
self.unreachables = sphere_candidates
|
||||||
|
break
|
||||||
|
|
||||||
|
# in the second phase, we cull each sphere such that the game is still beatable,
|
||||||
|
# reducing each range of influence to the bare minimum required inside it
|
||||||
|
restore_later = {}
|
||||||
|
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
||||||
|
to_delete = set()
|
||||||
|
for location in sphere:
|
||||||
|
# we remove the item at location and check if game is still beatable
|
||||||
|
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
||||||
|
location.item.player)
|
||||||
|
old_item = location.item
|
||||||
|
location.item = None
|
||||||
|
if multiworld.can_beat_game(state_cache[num]):
|
||||||
|
to_delete.add(location)
|
||||||
|
restore_later[location] = old_item
|
||||||
|
else:
|
||||||
|
# still required, got to keep it around
|
||||||
|
location.item = old_item
|
||||||
|
|
||||||
|
# cull entries in spheres for spoiler walkthrough at end
|
||||||
|
sphere -= to_delete
|
||||||
|
|
||||||
|
# second phase, sphere 0
|
||||||
|
removed_precollected = []
|
||||||
|
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
|
||||||
|
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||||
|
multiworld.precollected_items[item.player].remove(item)
|
||||||
|
multiworld.state.remove(item)
|
||||||
|
if not multiworld.can_beat_game():
|
||||||
|
multiworld.push_precollected(item)
|
||||||
|
else:
|
||||||
|
removed_precollected.append(item)
|
||||||
|
|
||||||
|
# we are now down to just the required progress items in collection_spheres. Unfortunately
|
||||||
|
# the previous pruning stage could potentially have made certain items dependant on others
|
||||||
|
# in the same or later sphere (because the location had 2 ways to access but the item originally
|
||||||
|
# used to access it was deemed not required.) So we need to do one final sphere collection pass
|
||||||
|
# to build up the correct spheres
|
||||||
|
|
||||||
|
required_locations = {item for sphere in collection_spheres for item in sphere}
|
||||||
|
state = CollectionState(multiworld)
|
||||||
|
collection_spheres = []
|
||||||
|
while required_locations:
|
||||||
|
state.sweep_for_events(key_only=True)
|
||||||
|
|
||||||
|
sphere = set(filter(state.can_reach, required_locations))
|
||||||
|
|
||||||
|
for location in sphere:
|
||||||
|
state.collect(location.item, True, location)
|
||||||
|
|
||||||
|
required_locations -= sphere
|
||||||
|
|
||||||
|
collection_spheres.append(sphere)
|
||||||
|
|
||||||
|
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
|
||||||
|
len(sphere), len(required_locations))
|
||||||
|
if not sphere:
|
||||||
|
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
||||||
|
|
||||||
|
# we can finally output our playthrough
|
||||||
|
self.playthrough = {"0": sorted([str(item) for item in
|
||||||
|
chain.from_iterable(multiworld.precollected_items.values())
|
||||||
|
if item.advancement])}
|
||||||
|
|
||||||
|
for i, sphere in enumerate(collection_spheres):
|
||||||
|
self.playthrough[str(i + 1)] = {
|
||||||
|
str(location): str(location.item) for location in sorted(sphere)}
|
||||||
|
if create_paths:
|
||||||
|
self.create_paths(state, collection_spheres)
|
||||||
|
|
||||||
|
# repair the multiworld again
|
||||||
|
for location, item in restore_later.items():
|
||||||
|
location.item = item
|
||||||
|
|
||||||
|
for item in removed_precollected:
|
||||||
|
multiworld.push_precollected(item)
|
||||||
|
|
||||||
|
def create_paths(self, state: CollectionState, collection_spheres: List[Set[Location]]):
|
||||||
|
from itertools import zip_longest
|
||||||
|
multiworld = self.multiworld
|
||||||
|
|
||||||
|
def flist_to_iter(node):
|
||||||
|
while node:
|
||||||
|
value, node = node
|
||||||
|
yield value
|
||||||
|
|
||||||
|
def get_path(state, region):
|
||||||
|
reversed_path_as_flist = state.path.get(region, (region, None))
|
||||||
|
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
||||||
|
# Now we combine the flat string list into (region, exit) pairs
|
||||||
|
pathsiter = iter(string_path_flat)
|
||||||
|
pathpairs = zip_longest(pathsiter, pathsiter)
|
||||||
|
return list(pathpairs)
|
||||||
|
|
||||||
|
self.paths = {}
|
||||||
|
topology_worlds = (player for player in multiworld.player_ids if multiworld.worlds[player].topology_present)
|
||||||
|
for player in topology_worlds:
|
||||||
|
self.paths.update(
|
||||||
|
{str(location): get_path(state, location.parent_region)
|
||||||
|
for sphere in collection_spheres for location in sphere
|
||||||
|
if location.player == player})
|
||||||
|
if player in multiworld.get_game_players("A Link to the Past"):
|
||||||
|
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
|
||||||
|
# Maybe move the big bomb over to the Event system instead?
|
||||||
|
if any(exit_path == 'Pyramid Fairy' for path in self.paths.values()
|
||||||
|
for (_, exit_path) in path):
|
||||||
|
if multiworld.mode[player] != 'inverted':
|
||||||
|
self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \
|
||||||
|
get_path(state, multiworld.get_region('Big Bomb Shop', player))
|
||||||
|
else:
|
||||||
|
self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \
|
||||||
|
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
self.parse_data()
|
self.parse_data()
|
||||||
out = OrderedDict()
|
out = OrderedDict()
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ class CommonContext:
|
|||||||
tags: typing.Set[str] = {"AP"}
|
tags: typing.Set[str] = {"AP"}
|
||||||
game: typing.Optional[str] = None
|
game: typing.Optional[str] = None
|
||||||
items_handling: typing.Optional[int] = None
|
items_handling: typing.Optional[int] = None
|
||||||
|
want_slot_data: bool = True # should slot_data be retrieved via Connect
|
||||||
|
|
||||||
# datapackage
|
# datapackage
|
||||||
# Contents in flux until connection to server is made, to download correct data for this multiworld.
|
# Contents in flux until connection to server is made, to download correct data for this multiworld.
|
||||||
@@ -309,7 +310,7 @@ class CommonContext:
|
|||||||
'cmd': 'Connect',
|
'cmd': 'Connect',
|
||||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||||
'tags': self.tags, 'items_handling': self.items_handling,
|
'tags': self.tags, 'items_handling': self.items_handling,
|
||||||
'uuid': Utils.get_unique_identifier(), 'game': self.game
|
'uuid': Utils.get_unique_identifier(), 'game': self.game, "slot_data": self.want_slot_data,
|
||||||
}
|
}
|
||||||
if kwargs:
|
if kwargs:
|
||||||
payload.update(kwargs)
|
payload.update(kwargs)
|
||||||
@@ -798,9 +799,10 @@ if __name__ == '__main__':
|
|||||||
# Text Mode to use !hint and such with games that have no text entry
|
# Text Mode to use !hint and such with games that have no text entry
|
||||||
|
|
||||||
class TextContext(CommonContext):
|
class TextContext(CommonContext):
|
||||||
tags = {"AP", "IgnoreGame", "TextOnly"}
|
tags = {"AP", "TextOnly"}
|
||||||
game = "" # empty matches any game since 0.3.2
|
game = "" # empty matches any game since 0.3.2
|
||||||
items_handling = 0b111 # receive all items for /received
|
items_handling = 0b111 # receive all items for /received
|
||||||
|
want_slot_data = False # Can't use game specific slot_data
|
||||||
|
|
||||||
async def server_auth(self, password_requested: bool = False):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
|
|||||||
48
Fill.py
48
Fill.py
@@ -24,7 +24,8 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
|||||||
|
|
||||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||||
itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
itempool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||||
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None) -> None:
|
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
||||||
|
allow_partial: bool = False) -> None:
|
||||||
unplaced_items: typing.List[Item] = []
|
unplaced_items: typing.List[Item] = []
|
||||||
placements: typing.List[Location] = []
|
placements: typing.List[Location] = []
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
|||||||
if on_place:
|
if on_place:
|
||||||
on_place(spot_to_fill)
|
on_place(spot_to_fill)
|
||||||
|
|
||||||
if len(unplaced_items) > 0 and len(locations) > 0:
|
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
|
||||||
# There are leftover unplaceable items and locations that won't accept them
|
# There are leftover unplaceable items and locations that won't accept them
|
||||||
if world.can_beat_game():
|
if world.can_beat_game():
|
||||||
logging.warning(
|
logging.warning(
|
||||||
@@ -252,11 +253,12 @@ def distribute_early_items(world: MultiWorld,
|
|||||||
fill_locations: typing.List[Location],
|
fill_locations: typing.List[Location],
|
||||||
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
|
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
|
||||||
""" returns new fill_locations and itempool """
|
""" returns new fill_locations and itempool """
|
||||||
early_items_count: typing.Dict[typing.Tuple[str, int], int] = {}
|
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
items = itertools.chain(world.early_items[player], world.local_early_items[player])
|
items = itertools.chain(world.early_items[player], world.local_early_items[player])
|
||||||
for item in items:
|
for item in items:
|
||||||
early_items_count[(item, player)] = [world.early_items[player].get(item, 0), world.local_early_items[player].get(item, 0)]
|
early_items_count[item, player] = [world.early_items[player].get(item, 0),
|
||||||
|
world.local_early_items[player].get(item, 0)]
|
||||||
if early_items_count:
|
if early_items_count:
|
||||||
early_locations: typing.List[Location] = []
|
early_locations: typing.List[Location] = []
|
||||||
early_priority_locations: typing.List[Location] = []
|
early_priority_locations: typing.List[Location] = []
|
||||||
@@ -280,42 +282,50 @@ def distribute_early_items(world: MultiWorld,
|
|||||||
for i, item in enumerate(itempool):
|
for i, item in enumerate(itempool):
|
||||||
if (item.name, item.player) in early_items_count:
|
if (item.name, item.player) in early_items_count:
|
||||||
if item.advancement:
|
if item.advancement:
|
||||||
if early_items_count[(item.name, item.player)][1]:
|
if early_items_count[item.name, item.player][1]:
|
||||||
early_local_prog_items[item.player].append(item)
|
early_local_prog_items[item.player].append(item)
|
||||||
early_items_count[(item.name, item.player)][1] -= 1
|
early_items_count[item.name, item.player][1] -= 1
|
||||||
else:
|
else:
|
||||||
early_prog_items.append(item)
|
early_prog_items.append(item)
|
||||||
early_items_count[(item.name, item.player)][0] -= 1
|
early_items_count[item.name, item.player][0] -= 1
|
||||||
else:
|
else:
|
||||||
if early_items_count[(item.name, item.player)][1]:
|
if early_items_count[item.name, item.player][1]:
|
||||||
early_local_rest_items[item.player].append(item)
|
early_local_rest_items[item.player].append(item)
|
||||||
early_items_count[(item.name, item.player)][1] -= 1
|
early_items_count[item.name, item.player][1] -= 1
|
||||||
else:
|
else:
|
||||||
early_rest_items.append(item)
|
early_rest_items.append(item)
|
||||||
early_items_count[(item.name, item.player)][0] -= 1
|
early_items_count[item.name, item.player][0] -= 1
|
||||||
item_indexes_to_remove.add(i)
|
item_indexes_to_remove.add(i)
|
||||||
if early_items_count[(item.name, item.player)] == [0, 0]:
|
if early_items_count[item.name, item.player] == [0, 0]:
|
||||||
del early_items_count[(item.name, item.player)]
|
del early_items_count[item.name, item.player]
|
||||||
if len(early_items_count) == 0:
|
if len(early_items_count) == 0:
|
||||||
break
|
break
|
||||||
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
|
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
|
player_local = early_local_rest_items[player]
|
||||||
fill_restrictive(world, base_state,
|
fill_restrictive(world, base_state,
|
||||||
[loc for loc in early_locations if loc.player == player],
|
[loc for loc in early_locations if loc.player == player],
|
||||||
early_local_rest_items[player], lock=True)
|
player_local, lock=True, allow_partial=True)
|
||||||
|
if player_local:
|
||||||
|
logging.warning(f"Could not fulfill rules of early items: {player_local}")
|
||||||
|
early_rest_items.extend(early_local_rest_items[player])
|
||||||
early_locations = [loc for loc in early_locations if not loc.item]
|
early_locations = [loc for loc in early_locations if not loc.item]
|
||||||
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True)
|
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True)
|
||||||
early_locations += early_priority_locations
|
early_locations += early_priority_locations
|
||||||
for player in world.player_ids:
|
for player in world.player_ids:
|
||||||
|
player_local = early_local_prog_items[player]
|
||||||
fill_restrictive(world, base_state,
|
fill_restrictive(world, base_state,
|
||||||
[loc for loc in early_locations if loc.player == player],
|
[loc for loc in early_locations if loc.player == player],
|
||||||
early_local_prog_items[player], lock=True)
|
player_local, lock=True, allow_partial=True)
|
||||||
|
if player_local:
|
||||||
|
logging.warning(f"Could not fulfill rules of early items: {player_local}")
|
||||||
|
early_prog_items.extend(player_local)
|
||||||
early_locations = [loc for loc in early_locations if not loc.item]
|
early_locations = [loc for loc in early_locations if not loc.item]
|
||||||
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True)
|
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True)
|
||||||
unplaced_early_items = early_rest_items + early_prog_items
|
unplaced_early_items = early_rest_items + early_prog_items
|
||||||
if unplaced_early_items:
|
if unplaced_early_items:
|
||||||
logging.warning("Ran out of early locations for early items. Failed to place "
|
logging.warning("Ran out of early locations for early items. Failed to place "
|
||||||
f"{len(unplaced_early_items)} items early.")
|
f"{unplaced_early_items} early.")
|
||||||
itempool += unplaced_early_items
|
itempool += unplaced_early_items
|
||||||
|
|
||||||
fill_locations.extend(early_locations)
|
fill_locations.extend(early_locations)
|
||||||
|
|||||||
@@ -503,7 +503,8 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
|
|||||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||||
for option_key, option in Options.per_game_common_options.items():
|
for option_key, option in Options.per_game_common_options.items():
|
||||||
# skip setting this option if already set from common_options, defaulting to root option
|
# skip setting this option if already set from common_options, defaulting to root option
|
||||||
if not (option_key in Options.common_options and option_key not in game_weights):
|
if option_key not in world_type.option_definitions and \
|
||||||
|
(option_key not in Options.common_options or option_key in game_weights):
|
||||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||||
if PlandoSettings.items in plando_options:
|
if PlandoSettings.items in plando_options:
|
||||||
ret.plando_items = game_weights.get("plando_items", [])
|
ret.plando_items = game_weights.get("plando_items", [])
|
||||||
|
|||||||
186
Main.py
186
Main.py
@@ -1,5 +1,4 @@
|
|||||||
import collections
|
import collections
|
||||||
from itertools import zip_longest, chain
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
@@ -11,7 +10,7 @@ import zipfile
|
|||||||
from typing import Dict, List, Tuple, Optional, Set
|
from typing import Dict, List, Tuple, Optional, Set
|
||||||
|
|
||||||
from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
|
from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
|
||||||
from worlds.alttp.Items import item_name_groups
|
import worlds
|
||||||
from worlds.alttp.Regions import is_main_entrance
|
from worlds.alttp.Regions import is_main_entrance
|
||||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||||
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||||
@@ -116,19 +115,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
world.push_precollected(world.create_item(item_name, player))
|
world.push_precollected(world.create_item(item_name, player))
|
||||||
|
|
||||||
for player in world.player_ids:
|
|
||||||
if player in world.get_game_players("A Link to the Past"):
|
|
||||||
# enforce pre-defined local items.
|
|
||||||
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
|
||||||
world.local_items[player].value.add('Triforce Piece')
|
|
||||||
|
|
||||||
# Not possible to place pendants/crystals outside boss prizes yet.
|
|
||||||
world.non_local_items[player].value -= item_name_groups['Pendants']
|
|
||||||
world.non_local_items[player].value -= item_name_groups['Crystals']
|
|
||||||
|
|
||||||
# items can't be both local and non-local, prefer local
|
|
||||||
world.non_local_items[player].value -= world.local_items[player].value
|
|
||||||
|
|
||||||
logger.info('Creating World.')
|
logger.info('Creating World.')
|
||||||
AutoWorld.call_all(world, "create_regions")
|
AutoWorld.call_all(world, "create_regions")
|
||||||
|
|
||||||
@@ -136,6 +122,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
AutoWorld.call_all(world, "create_items")
|
AutoWorld.call_all(world, "create_items")
|
||||||
|
|
||||||
logger.info('Calculating Access Rules.')
|
logger.info('Calculating Access Rules.')
|
||||||
|
|
||||||
|
for player in world.player_ids:
|
||||||
|
# items can't be both local and non-local, prefer local
|
||||||
|
world.non_local_items[player].value -= world.local_items[player].value
|
||||||
|
world.non_local_items[player].value -= set(world.local_early_items[player])
|
||||||
|
|
||||||
if world.players > 1:
|
if world.players > 1:
|
||||||
locality_rules(world)
|
locality_rules(world)
|
||||||
else:
|
else:
|
||||||
@@ -217,11 +209,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
while itemcount > len(world.itempool):
|
while itemcount > len(world.itempool):
|
||||||
items_to_add = []
|
items_to_add = []
|
||||||
for player in group["players"]:
|
for player in group["players"]:
|
||||||
|
if group["link_replacement"]:
|
||||||
|
item_player = group_id
|
||||||
|
else:
|
||||||
|
item_player = player
|
||||||
if group["replacement_items"][player]:
|
if group["replacement_items"][player]:
|
||||||
items_to_add.append(AutoWorld.call_single(world, "create_item", player,
|
items_to_add.append(AutoWorld.call_single(world, "create_item", item_player,
|
||||||
group["replacement_items"][player]))
|
group["replacement_items"][player]))
|
||||||
else:
|
else:
|
||||||
items_to_add.append(AutoWorld.call_single(world, "create_filler", player))
|
items_to_add.append(AutoWorld.call_single(world, "create_filler", item_player))
|
||||||
world.random.shuffle(items_to_add)
|
world.random.shuffle(items_to_add)
|
||||||
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
|
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
|
||||||
|
|
||||||
@@ -371,16 +367,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
||||||
precollect_hint(location)
|
precollect_hint(location)
|
||||||
|
|
||||||
|
# custom datapackage
|
||||||
|
datapackage = {}
|
||||||
|
for game_world in world.worlds.values():
|
||||||
|
if game_world.data_version == 0 and game_world.game not in datapackage:
|
||||||
|
datapackage[game_world.game] = worlds.network_data_package["games"][game_world.game]
|
||||||
|
datapackage[game_world.game]["item_name_groups"] = game_world.item_name_groups
|
||||||
|
|
||||||
multidata = {
|
multidata = {
|
||||||
"slot_data": slot_data,
|
"slot_data": slot_data,
|
||||||
"slot_info": slot_info,
|
"slot_info": slot_info,
|
||||||
"names": names, # TODO: remove around 0.2.5 in favor of slot_info
|
"names": names, # TODO: remove around 0.2.5 in favor of slot_info
|
||||||
"games": games, # TODO: remove around 0.2.5 in favor of slot_info
|
"games": games, # TODO: remove around 0.2.5 in favor of slot_info
|
||||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||||
"remote_items": {player for player in world.player_ids if
|
|
||||||
world.worlds[player].remote_items},
|
|
||||||
"remote_start_inventory": {player for player in world.player_ids if
|
|
||||||
world.worlds[player].remote_start_inventory},
|
|
||||||
"locations": locations_data,
|
"locations": locations_data,
|
||||||
"checks_in_area": checks_in_area,
|
"checks_in_area": checks_in_area,
|
||||||
"server_options": baked_server_options,
|
"server_options": baked_server_options,
|
||||||
@@ -390,7 +389,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
"version": tuple(version_tuple),
|
"version": tuple(version_tuple),
|
||||||
"tags": ["AP"],
|
"tags": ["AP"],
|
||||||
"minimum_versions": minimum_versions,
|
"minimum_versions": minimum_versions,
|
||||||
"seed_name": world.seed_name
|
"seed_name": world.seed_name,
|
||||||
|
"datapackage": datapackage,
|
||||||
}
|
}
|
||||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
|
|
||||||
if args.spoiler > 1:
|
if args.spoiler > 1:
|
||||||
logger.info('Calculating playthrough.')
|
logger.info('Calculating playthrough.')
|
||||||
create_playthrough(world)
|
world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||||
|
|
||||||
if args.spoiler:
|
if args.spoiler:
|
||||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||||
@@ -430,143 +430,3 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
|
|
||||||
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
|
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
|
||||||
return world
|
return world
|
||||||
|
|
||||||
|
|
||||||
def create_playthrough(world):
|
|
||||||
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
|
||||||
# get locations containing progress items
|
|
||||||
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
|
|
||||||
state_cache = [None]
|
|
||||||
collection_spheres = []
|
|
||||||
state = CollectionState(world)
|
|
||||||
sphere_candidates = set(prog_locations)
|
|
||||||
logging.debug('Building up collection spheres.')
|
|
||||||
while sphere_candidates:
|
|
||||||
|
|
||||||
# build up spheres of collection radius.
|
|
||||||
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
|
||||||
|
|
||||||
sphere = {location for location in sphere_candidates if state.can_reach(location)}
|
|
||||||
|
|
||||||
for location in sphere:
|
|
||||||
state.collect(location.item, True, location)
|
|
||||||
|
|
||||||
sphere_candidates -= sphere
|
|
||||||
collection_spheres.append(sphere)
|
|
||||||
state_cache.append(state.copy())
|
|
||||||
|
|
||||||
logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere),
|
|
||||||
len(prog_locations))
|
|
||||||
if not sphere:
|
|
||||||
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
|
|
||||||
location.item.name, location.item.player, location.name, location.player) for location in
|
|
||||||
sphere_candidates])
|
|
||||||
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
|
|
||||||
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
|
|
||||||
f'Something went terribly wrong here.')
|
|
||||||
else:
|
|
||||||
world.spoiler.unreachables = sphere_candidates
|
|
||||||
break
|
|
||||||
|
|
||||||
# in the second phase, we cull each sphere such that the game is still beatable,
|
|
||||||
# reducing each range of influence to the bare minimum required inside it
|
|
||||||
restore_later = {}
|
|
||||||
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
|
||||||
to_delete = set()
|
|
||||||
for location in sphere:
|
|
||||||
# we remove the item at location and check if game is still beatable
|
|
||||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
|
||||||
location.item.player)
|
|
||||||
old_item = location.item
|
|
||||||
location.item = None
|
|
||||||
if world.can_beat_game(state_cache[num]):
|
|
||||||
to_delete.add(location)
|
|
||||||
restore_later[location] = old_item
|
|
||||||
else:
|
|
||||||
# still required, got to keep it around
|
|
||||||
location.item = old_item
|
|
||||||
|
|
||||||
# cull entries in spheres for spoiler walkthrough at end
|
|
||||||
sphere -= to_delete
|
|
||||||
|
|
||||||
# second phase, sphere 0
|
|
||||||
removed_precollected = []
|
|
||||||
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
|
|
||||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
|
||||||
world.precollected_items[item.player].remove(item)
|
|
||||||
world.state.remove(item)
|
|
||||||
if not world.can_beat_game():
|
|
||||||
world.push_precollected(item)
|
|
||||||
else:
|
|
||||||
removed_precollected.append(item)
|
|
||||||
|
|
||||||
# we are now down to just the required progress items in collection_spheres. Unfortunately
|
|
||||||
# the previous pruning stage could potentially have made certain items dependant on others
|
|
||||||
# in the same or later sphere (because the location had 2 ways to access but the item originally
|
|
||||||
# used to access it was deemed not required.) So we need to do one final sphere collection pass
|
|
||||||
# to build up the correct spheres
|
|
||||||
|
|
||||||
required_locations = {item for sphere in collection_spheres for item in sphere}
|
|
||||||
state = CollectionState(world)
|
|
||||||
collection_spheres = []
|
|
||||||
while required_locations:
|
|
||||||
state.sweep_for_events(key_only=True)
|
|
||||||
|
|
||||||
sphere = set(filter(state.can_reach, required_locations))
|
|
||||||
|
|
||||||
for location in sphere:
|
|
||||||
state.collect(location.item, True, location)
|
|
||||||
|
|
||||||
required_locations -= sphere
|
|
||||||
|
|
||||||
collection_spheres.append(sphere)
|
|
||||||
|
|
||||||
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
|
|
||||||
len(sphere), len(required_locations))
|
|
||||||
if not sphere:
|
|
||||||
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
|
|
||||||
|
|
||||||
def flist_to_iter(node):
|
|
||||||
while node:
|
|
||||||
value, node = node
|
|
||||||
yield value
|
|
||||||
|
|
||||||
def get_path(state, region):
|
|
||||||
reversed_path_as_flist = state.path.get(region, (region, None))
|
|
||||||
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
|
|
||||||
# Now we combine the flat string list into (region, exit) pairs
|
|
||||||
pathsiter = iter(string_path_flat)
|
|
||||||
pathpairs = zip_longest(pathsiter, pathsiter)
|
|
||||||
return list(pathpairs)
|
|
||||||
|
|
||||||
world.spoiler.paths = {}
|
|
||||||
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
|
|
||||||
for player in topology_worlds:
|
|
||||||
world.spoiler.paths.update(
|
|
||||||
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
|
|
||||||
sphere if location.player == player})
|
|
||||||
if player in world.get_game_players("A Link to the Past"):
|
|
||||||
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
|
|
||||||
# Maybe move the big bomb over to the Event system instead?
|
|
||||||
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
|
|
||||||
if world.mode[player] != 'inverted':
|
|
||||||
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
|
|
||||||
get_path(state, world.get_region('Big Bomb Shop', player))
|
|
||||||
else:
|
|
||||||
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
|
|
||||||
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
|
||||||
|
|
||||||
# we can finally output our playthrough
|
|
||||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in
|
|
||||||
chain.from_iterable(world.precollected_items.values())
|
|
||||||
if item.advancement])}
|
|
||||||
|
|
||||||
for i, sphere in enumerate(collection_spheres):
|
|
||||||
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
|
|
||||||
|
|
||||||
# repair the world again
|
|
||||||
for location, item in restore_later.items():
|
|
||||||
location.item = item
|
|
||||||
|
|
||||||
for item in removed_precollected:
|
|
||||||
world.push_precollected(item)
|
|
||||||
|
|||||||
242
MultiServer.py
242
MultiServer.py
@@ -16,6 +16,7 @@ import pickle
|
|||||||
import itertools
|
import itertools
|
||||||
import time
|
import time
|
||||||
import operator
|
import operator
|
||||||
|
import hashlib
|
||||||
|
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
@@ -58,6 +59,12 @@ modify_functions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_saving_second(seed_name: str, interval: int = 60) -> int:
|
||||||
|
# save at expected times so other systems using savegame can expect it
|
||||||
|
# represents the target second of the auto_save_interval at which to save
|
||||||
|
return int(hashlib.sha256(seed_name.encode()).hexdigest(), 16) % interval
|
||||||
|
|
||||||
|
|
||||||
class Client(Endpoint):
|
class Client(Endpoint):
|
||||||
version = Version(0, 0, 0)
|
version = Version(0, 0, 0)
|
||||||
tags: typing.List[str] = []
|
tags: typing.List[str] = []
|
||||||
@@ -120,9 +127,11 @@ class Context:
|
|||||||
groups: typing.Dict[int, typing.Set[int]]
|
groups: typing.Dict[int, typing.Set[int]]
|
||||||
save_version = 2
|
save_version = 2
|
||||||
stored_data: typing.Dict[str, object]
|
stored_data: typing.Dict[str, object]
|
||||||
|
read_data: typing.Dict[str, object]
|
||||||
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
|
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
|
||||||
|
|
||||||
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
|
||||||
|
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
|
||||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||||
forced_auto_forfeits: typing.Dict[str, bool]
|
forced_auto_forfeits: typing.Dict[str, bool]
|
||||||
@@ -146,8 +155,6 @@ class Context:
|
|||||||
self.player_name_lookup: typing.Dict[str, team_slot] = {}
|
self.player_name_lookup: typing.Dict[str, team_slot] = {}
|
||||||
self.connect_names = {} # names of slots clients can connect to
|
self.connect_names = {} # names of slots clients can connect to
|
||||||
self.allow_forfeits = {}
|
self.allow_forfeits = {}
|
||||||
self.remote_items = set()
|
|
||||||
self.remote_start_inventory = set()
|
|
||||||
# player location_id item_id target_player_id
|
# player location_id item_id target_player_id
|
||||||
self.locations = {}
|
self.locations = {}
|
||||||
self.host = host
|
self.host = host
|
||||||
@@ -191,6 +198,7 @@ class Context:
|
|||||||
self.random = random.Random()
|
self.random = random.Random()
|
||||||
self.stored_data = {}
|
self.stored_data = {}
|
||||||
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
||||||
|
self.read_data = {}
|
||||||
|
|
||||||
# init empty to satisfy linter, I suppose
|
# init empty to satisfy linter, I suppose
|
||||||
self.gamespackage = {}
|
self.gamespackage = {}
|
||||||
@@ -200,7 +208,6 @@ class Context:
|
|||||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||||
|
|
||||||
self._load_game_data()
|
self._load_game_data()
|
||||||
self._init_game_data()
|
|
||||||
|
|
||||||
# Datapackage retrieval
|
# Datapackage retrieval
|
||||||
def _load_game_data(self):
|
def _load_game_data(self):
|
||||||
@@ -342,7 +349,7 @@ class Context:
|
|||||||
return restricted_loads(zlib.decompress(data[1:]))
|
return restricted_loads(zlib.decompress(data[1:]))
|
||||||
|
|
||||||
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
|
||||||
|
self.read_data = {}
|
||||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||||
if mdata_ver > Utils.version_tuple:
|
if mdata_ver > Utils.version_tuple:
|
||||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
||||||
@@ -359,13 +366,15 @@ class Context:
|
|||||||
self.clients[team][player] = []
|
self.clients[team][player] = []
|
||||||
self.player_names[team, player] = name
|
self.player_names[team, player] = name
|
||||||
self.player_name_lookup[name] = team, player
|
self.player_name_lookup[name] = team, player
|
||||||
|
self.read_data[f"hints_{team}_{player}"] = lambda local_team=team, local_player=player: \
|
||||||
|
list(self.get_rechecked_hints(local_team, local_player))
|
||||||
self.seed_name = decoded_obj["seed_name"]
|
self.seed_name = decoded_obj["seed_name"]
|
||||||
self.random.seed(self.seed_name)
|
self.random.seed(self.seed_name)
|
||||||
self.connect_names = decoded_obj['connect_names']
|
self.connect_names = decoded_obj['connect_names']
|
||||||
self.remote_items = decoded_obj['remote_items']
|
|
||||||
self.remote_start_inventory = decoded_obj.get('remote_start_inventory', decoded_obj['remote_items'])
|
|
||||||
self.locations = decoded_obj['locations']
|
self.locations = decoded_obj['locations']
|
||||||
self.slot_data = decoded_obj['slot_data']
|
self.slot_data = decoded_obj['slot_data']
|
||||||
|
for slot, data in self.slot_data.items():
|
||||||
|
self.read_data[f"slot_data_{slot}"] = lambda data=data: data
|
||||||
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
|
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
|
||||||
for player, loc_data in decoded_obj["er_hint_data"].items()}
|
for player, loc_data in decoded_obj["er_hint_data"].items()}
|
||||||
|
|
||||||
@@ -406,6 +415,16 @@ class Context:
|
|||||||
server_options = decoded_obj.get("server_options", {})
|
server_options = decoded_obj.get("server_options", {})
|
||||||
self._set_options(server_options)
|
self._set_options(server_options)
|
||||||
|
|
||||||
|
# custom datapackage
|
||||||
|
for game_name, data in decoded_obj.get("datapackage", {}).items():
|
||||||
|
logging.info(f"Loading custom datapackage for game {game_name}")
|
||||||
|
self.gamespackage[game_name] = data
|
||||||
|
self.item_name_groups[game_name] = data["item_name_groups"]
|
||||||
|
del data["item_name_groups"] # remove from datapackage, but keep in self.item_name_groups
|
||||||
|
self._init_game_data()
|
||||||
|
for game_name, data in self.item_name_groups.items():
|
||||||
|
self.read_data[f"item_name_groups_{game_name}"] = lambda lgame=game_name: self.item_name_groups[lgame]
|
||||||
|
|
||||||
# saving
|
# saving
|
||||||
|
|
||||||
def save(self, now=False) -> bool:
|
def save(self, now=False) -> bool:
|
||||||
@@ -451,10 +470,16 @@ class Context:
|
|||||||
def _start_async_saving(self):
|
def _start_async_saving(self):
|
||||||
if not self.auto_saver_thread:
|
if not self.auto_saver_thread:
|
||||||
def save_regularly():
|
def save_regularly():
|
||||||
import time
|
# time.time() is platform dependent, so using the expensive datetime method instead
|
||||||
|
def get_datetime_second():
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
return now.second + now.microsecond * 0.000001
|
||||||
|
|
||||||
|
second = get_saving_second(self.seed_name, self.auto_save_interval)
|
||||||
while not self.exit_event.is_set():
|
while not self.exit_event.is_set():
|
||||||
try:
|
try:
|
||||||
time.sleep(self.auto_save_interval)
|
next_wakeup = (second - get_datetime_second()) % self.auto_save_interval
|
||||||
|
time.sleep(max(1.0, next_wakeup))
|
||||||
if self.save_dirty:
|
if self.save_dirty:
|
||||||
logging.debug("Saving via thread.")
|
logging.debug("Saving via thread.")
|
||||||
self._save()
|
self._save()
|
||||||
@@ -532,7 +557,7 @@ class Context:
|
|||||||
|
|
||||||
if "stored_data" in savedata:
|
if "stored_data" in savedata:
|
||||||
self.stored_data = savedata["stored_data"]
|
self.stored_data = savedata["stored_data"]
|
||||||
# count items and slots from lists for item_handling = remote
|
# count items and slots from lists for items_handling = remote
|
||||||
logging.info(
|
logging.info(
|
||||||
f'Loaded save file with {sum([len(v) for k, v in self.received_items.items() if k[2]])} received items '
|
f'Loaded save file with {sum([len(v) for k, v in self.received_items.items() if k[2]])} received items '
|
||||||
f'for {sum(k[2] for k in self.received_items)} players')
|
f'for {sum(k[2] for k in self.received_items)} players')
|
||||||
@@ -544,12 +569,17 @@ class Context:
|
|||||||
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def recheck_hints(self):
|
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None):
|
||||||
for team, slot in self.hints:
|
for hint_team, hint_slot in self.hints:
|
||||||
self.hints[team, slot] = {
|
if (team is None or team == hint_team) and (slot is None or slot == hint_slot):
|
||||||
hint.re_check(self, team) for hint in
|
self.hints[hint_team, hint_slot] = {
|
||||||
self.hints[team, slot]
|
hint.re_check(self, hint_team) for hint in
|
||||||
}
|
self.hints[hint_team, hint_slot]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_rechecked_hints(self, team: int, slot: int):
|
||||||
|
self.recheck_hints(team, slot)
|
||||||
|
return self.hints[team, slot]
|
||||||
|
|
||||||
def get_players_package(self):
|
def get_players_package(self):
|
||||||
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
|
||||||
@@ -584,6 +614,44 @@ class Context:
|
|||||||
else:
|
else:
|
||||||
return self.player_names[team, slot]
|
return self.player_names[team, slot]
|
||||||
|
|
||||||
|
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
||||||
|
"""Send and remember hints."""
|
||||||
|
if only_new:
|
||||||
|
hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]]
|
||||||
|
if not hints:
|
||||||
|
return
|
||||||
|
new_hint_events: typing.Set[int] = set()
|
||||||
|
concerns = collections.defaultdict(list)
|
||||||
|
for hint in sorted(hints, key=operator.attrgetter('found'), reverse=True):
|
||||||
|
data = (hint, hint.as_network_message())
|
||||||
|
for player in self.slot_set(hint.receiving_player):
|
||||||
|
concerns[player].append(data)
|
||||||
|
if not hint.local and data not in concerns[hint.finding_player]:
|
||||||
|
concerns[hint.finding_player].append(data)
|
||||||
|
# remember hints in all cases
|
||||||
|
if not hint.found:
|
||||||
|
# since hints are bidirectional, finding player and receiving player,
|
||||||
|
# we can check once if hint already exists
|
||||||
|
if hint not in self.hints[team, hint.finding_player]:
|
||||||
|
self.hints[team, hint.finding_player].add(hint)
|
||||||
|
new_hint_events.add(hint.finding_player)
|
||||||
|
for player in self.slot_set(hint.receiving_player):
|
||||||
|
self.hints[team, player].add(hint)
|
||||||
|
new_hint_events.add(player)
|
||||||
|
|
||||||
|
logging.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
||||||
|
for slot in new_hint_events:
|
||||||
|
self.on_new_hint(team, slot)
|
||||||
|
for slot, hint_data in concerns.items():
|
||||||
|
clients = self.clients[team].get(slot)
|
||||||
|
if not clients:
|
||||||
|
continue
|
||||||
|
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
|
||||||
|
for client in clients:
|
||||||
|
async_start(self.send_msgs(client, client_hints))
|
||||||
|
|
||||||
|
# "events"
|
||||||
|
|
||||||
def on_goal_achieved(self, client: Client):
|
def on_goal_achieved(self, client: Client):
|
||||||
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
||||||
f' has completed their goal.'
|
f' has completed their goal.'
|
||||||
@@ -596,38 +664,11 @@ class Context:
|
|||||||
forfeit_player(self, client.team, client.slot)
|
forfeit_player(self, client.team, client.slot)
|
||||||
self.save() # save goal completion flag
|
self.save() # save goal completion flag
|
||||||
|
|
||||||
|
def on_new_hint(self, team: int, slot: int):
|
||||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
key: str = f"_read_hints_{team}_{slot}"
|
||||||
"""Send and remember hints."""
|
targets: typing.Set[Client] = set(self.stored_data_notification_clients[key])
|
||||||
if only_new:
|
if targets:
|
||||||
hints = [hint for hint in hints if hint not in ctx.hints[team, hint.finding_player]]
|
self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}])
|
||||||
if not hints:
|
|
||||||
return
|
|
||||||
concerns = collections.defaultdict(list)
|
|
||||||
for hint in sorted(hints, key=operator.attrgetter('found'), reverse=True):
|
|
||||||
data = (hint, hint.as_network_message())
|
|
||||||
for player in ctx.slot_set(hint.receiving_player):
|
|
||||||
concerns[player].append(data)
|
|
||||||
if not hint.local and data not in concerns[hint.finding_player]:
|
|
||||||
concerns[hint.finding_player].append(data)
|
|
||||||
# remember hints in all cases
|
|
||||||
if not hint.found:
|
|
||||||
# since hints are bidirectional, finding player and receiving player,
|
|
||||||
# we can check once if hint already exists
|
|
||||||
if hint not in ctx.hints[team, hint.finding_player]:
|
|
||||||
ctx.hints[team, hint.finding_player].add(hint)
|
|
||||||
for player in ctx.slot_set(hint.receiving_player):
|
|
||||||
ctx.hints[team, player].add(hint)
|
|
||||||
|
|
||||||
logging.info("Notice (Team #%d): %s" % (team + 1, format_hint(ctx, team, hint)))
|
|
||||||
|
|
||||||
for slot, hint_data in concerns.items():
|
|
||||||
clients = ctx.clients[team].get(slot)
|
|
||||||
if not clients:
|
|
||||||
continue
|
|
||||||
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
|
|
||||||
for client in clients:
|
|
||||||
async_start(ctx.send_msgs(client, client_hints))
|
|
||||||
|
|
||||||
|
|
||||||
def update_aliases(ctx: Context, team: int):
|
def update_aliases(ctx: Context, team: int):
|
||||||
@@ -676,10 +717,7 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
await ctx.send_msgs(client, [{
|
await ctx.send_msgs(client, [{
|
||||||
'cmd': 'RoomInfo',
|
'cmd': 'RoomInfo',
|
||||||
'password': bool(ctx.password),
|
'password': bool(ctx.password),
|
||||||
# TODO remove around 0.4
|
'games': {ctx.games[x] for x in range(1, len(ctx.games) + 1)},
|
||||||
'players': players,
|
|
||||||
# TODO convert to list of games present in 0.4
|
|
||||||
'games': [ctx.games[x] for x in range(1, len(ctx.games) + 1)],
|
|
||||||
# tags are for additional features in the communication.
|
# tags are for additional features in the communication.
|
||||||
# Name them by feature or fork, as you feel is appropriate.
|
# Name them by feature or fork, as you feel is appropriate.
|
||||||
'tags': ctx.tags,
|
'tags': ctx.tags,
|
||||||
@@ -687,8 +725,6 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||||||
'permissions': get_permissions(ctx),
|
'permissions': get_permissions(ctx),
|
||||||
'hint_cost': ctx.hint_cost,
|
'hint_cost': ctx.hint_cost,
|
||||||
'location_check_points': ctx.location_check_points,
|
'location_check_points': ctx.location_check_points,
|
||||||
'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values())
|
|
||||||
if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0,
|
|
||||||
'datapackage_versions': {game: game_data["version"] for game, game_data
|
'datapackage_versions': {game: game_data["version"] for game, game_data
|
||||||
in ctx.gamespackage.items()},
|
in ctx.gamespackage.items()},
|
||||||
'seed_name': ctx.seed_name,
|
'seed_name': ctx.seed_name,
|
||||||
@@ -1133,13 +1169,15 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
output = f"!admin {command}"
|
output = f"!admin {command}"
|
||||||
if output.lower().startswith(
|
if output.lower().startswith(
|
||||||
"!admin login"): # disallow others from seeing the supplied password, whether or not it is correct.
|
"!admin login"): # disallow others from seeing the supplied password, whether it is correct.
|
||||||
output = f"!admin login {('*' * random.randint(4, 16))}"
|
output = f"!admin login {('*' * random.randint(4, 16))}"
|
||||||
elif output.lower().startswith(
|
elif output.lower().startswith(
|
||||||
"!admin /option server_password"): # disallow others from knowing what the new remote administration password is.
|
# disallow others from knowing what the new remote administration password is.
|
||||||
|
"!admin /option server_password"):
|
||||||
output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
|
output = f"!admin /option server_password {('*' * random.randint(4, 16))}"
|
||||||
|
# Otherwise notify the others what is happening.
|
||||||
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team,
|
self.ctx.notify_all(self.ctx.get_aliased_name(self.client.team,
|
||||||
self.client.slot) + ': ' + output) # Otherwise notify the others what is happening.
|
self.client.slot) + ': ' + output)
|
||||||
|
|
||||||
if not self.ctx.server_password:
|
if not self.ctx.server_password:
|
||||||
self.output("Sorry, Remote administration is disabled")
|
self.output("Sorry, Remote administration is disabled")
|
||||||
@@ -1147,8 +1185,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
|
|
||||||
if not command:
|
if not command:
|
||||||
if self.is_authenticated():
|
if self.is_authenticated():
|
||||||
self.output(
|
self.output("Usage: !admin [Server command].\nUse !admin /help for help.\n"
|
||||||
"Usage: !admin [Server command].\nUse !admin /help for help.\nUse !admin logout to log out of the current session.")
|
"Use !admin logout to log out of the current session.")
|
||||||
else:
|
else:
|
||||||
self.output("Usage: !admin login [password]")
|
self.output("Usage: !admin login [password]")
|
||||||
return True
|
return True
|
||||||
@@ -1338,7 +1376,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||||
self.ctx.hints[self.client.team, self.client.slot]}
|
self.ctx.hints[self.client.team, self.client.slot]}
|
||||||
self.ctx.hints[self.client.team, self.client.slot] = hints
|
self.ctx.hints[self.client.team, self.client.slot] = hints
|
||||||
notify_hints(self.ctx, self.client.team, list(hints))
|
self.ctx.notify_hints(self.client.team, list(hints))
|
||||||
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
|
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
|
||||||
f"You have {points_available} points.")
|
f"You have {points_available} points.")
|
||||||
return True
|
return True
|
||||||
@@ -1391,7 +1429,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||||
old_hints = set(hints) - new_hints
|
old_hints = set(hints) - new_hints
|
||||||
if old_hints:
|
if old_hints:
|
||||||
notify_hints(self.ctx, self.client.team, list(old_hints))
|
self.ctx.notify_hints(self.client.team, list(old_hints))
|
||||||
if not new_hints:
|
if not new_hints:
|
||||||
self.output("Hint was previously used, no points deducted.")
|
self.output("Hint was previously used, no points deducted.")
|
||||||
if new_hints:
|
if new_hints:
|
||||||
@@ -1432,13 +1470,18 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
self.output(f"You can't afford the hint. "
|
self.output(f"You can't afford the hint. "
|
||||||
f"You have {points_available} points and need at least "
|
f"You have {points_available} points and need at least "
|
||||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||||
notify_hints(self.ctx, self.client.team, hints)
|
self.ctx.notify_hints(self.client.team, hints)
|
||||||
self.ctx.save()
|
self.ctx.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if points_available >= cost:
|
if points_available >= cost:
|
||||||
self.output("Nothing found. Item/Location may not exist.")
|
if for_location:
|
||||||
|
self.output(f"Nothing found for recognized location name \"{hint_name}\". "
|
||||||
|
f"Location appears to not exist in this multiworld.")
|
||||||
|
else:
|
||||||
|
self.output(f"Nothing found for recognized item name \"{hint_name}\". "
|
||||||
|
f"Item appears to not exist in this multiworld.")
|
||||||
else:
|
else:
|
||||||
self.output(f"You can't afford the hint. "
|
self.output(f"You can't afford the hint. "
|
||||||
f"You have {points_available} points and need at least "
|
f"You have {points_available} points and need at least "
|
||||||
@@ -1512,27 +1555,16 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
else:
|
else:
|
||||||
team, slot = ctx.connect_names[args['name']]
|
team, slot = ctx.connect_names[args['name']]
|
||||||
game = ctx.games[slot]
|
game = ctx.games[slot]
|
||||||
ignore_game = "IgnoreGame" in args["tags"] or ( # IgnoreGame is deprecated. TODO: remove after 0.3.3?
|
ignore_game = ("TextOnly" in args["tags"] or "Tracker" in args["tags"]) and not args.get("game")
|
||||||
("TextOnly" in args["tags"] or "Tracker" in args["tags"]) and not args.get("game"))
|
|
||||||
if not ignore_game and args['game'] != game:
|
if not ignore_game and args['game'] != game:
|
||||||
errors.add('InvalidGame')
|
errors.add('InvalidGame')
|
||||||
minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot]
|
minver = min_client_version if ignore_game else ctx.minimum_client_versions[slot]
|
||||||
if minver > args['version']:
|
if minver > args['version']:
|
||||||
errors.add('IncompatibleVersion')
|
errors.add('IncompatibleVersion')
|
||||||
if args.get('items_handling', None) is None:
|
try:
|
||||||
# fall back to load from multidata
|
client.items_handling = args['items_handling']
|
||||||
client.no_items = False
|
except (ValueError, TypeError):
|
||||||
client.remote_items = slot in ctx.remote_items
|
errors.add('InvalidItemsHandling')
|
||||||
client.remote_start_inventory = slot in ctx.remote_start_inventory
|
|
||||||
await ctx.send_msgs(client, [{
|
|
||||||
"cmd": "Print", "text":
|
|
||||||
"Warning: Client is not sending items_handling flags, "
|
|
||||||
"which will not be supported in the future."}])
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
client.items_handling = args['items_handling']
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
errors.add('InvalidItemsHandling')
|
|
||||||
|
|
||||||
# only exact version match allowed
|
# only exact version match allowed
|
||||||
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
if ctx.compatibility == 0 and args['version'] != version_tuple:
|
||||||
@@ -1554,15 +1586,15 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
client.version = args['version']
|
client.version = args['version']
|
||||||
client.tags = args['tags']
|
client.tags = args['tags']
|
||||||
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
|
||||||
reply = [{
|
connected_packet = {
|
||||||
"cmd": "Connected",
|
"cmd": "Connected",
|
||||||
"team": client.team, "slot": client.slot,
|
"team": client.team, "slot": client.slot,
|
||||||
"players": ctx.get_players_package(),
|
"players": ctx.get_players_package(),
|
||||||
"missing_locations": get_missing_checks(ctx, team, slot),
|
"missing_locations": get_missing_checks(ctx, team, slot),
|
||||||
"checked_locations": get_checked_checks(ctx, team, slot),
|
"checked_locations": get_checked_checks(ctx, team, slot),
|
||||||
"slot_data": ctx.slot_data[client.slot],
|
|
||||||
"slot_info": ctx.slot_info
|
"slot_info": ctx.slot_info
|
||||||
}]
|
}
|
||||||
|
reply = [connected_packet]
|
||||||
start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory)
|
start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory)
|
||||||
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
|
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
|
||||||
if (start_inventory or items) and not client.no_items:
|
if (start_inventory or items) and not client.no_items:
|
||||||
@@ -1571,7 +1603,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
if not client.auth: # if this was a Re-Connect, don't print to console
|
if not client.auth: # if this was a Re-Connect, don't print to console
|
||||||
client.auth = True
|
client.auth = True
|
||||||
await on_client_joined(ctx, client)
|
await on_client_joined(ctx, client)
|
||||||
|
if args.get("slot_data", True):
|
||||||
|
connected_packet["slot_data"] = ctx.slot_data[client.slot]
|
||||||
await ctx.send_msgs(client, reply)
|
await ctx.send_msgs(client, reply)
|
||||||
|
|
||||||
elif cmd == "GetDataPackage":
|
elif cmd == "GetDataPackage":
|
||||||
@@ -1659,7 +1692,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
if create_as_hint:
|
if create_as_hint:
|
||||||
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
|
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
|
||||||
locs.append(NetworkItem(target_item, location, target_player, flags))
|
locs.append(NetworkItem(target_item, location, target_player, flags))
|
||||||
notify_hints(ctx, client.team, hints, only_new=create_as_hint == 2)
|
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
|
||||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||||
|
|
||||||
elif cmd == 'StatusUpdate':
|
elif cmd == 'StatusUpdate':
|
||||||
@@ -1693,11 +1726,15 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
return
|
return
|
||||||
args["cmd"] = "Retrieved"
|
args["cmd"] = "Retrieved"
|
||||||
keys = args["keys"]
|
keys = args["keys"]
|
||||||
args["keys"] = {key: ctx.stored_data.get(key, None) for key in keys}
|
args["keys"] = {
|
||||||
|
key: ctx.read_data.get(key[6:], lambda: None)() if key.startswith("_read_") else
|
||||||
|
ctx.stored_data.get(key, None)
|
||||||
|
for key in keys
|
||||||
|
}
|
||||||
await ctx.send_msgs(client, [args])
|
await ctx.send_msgs(client, [args])
|
||||||
|
|
||||||
elif cmd == "Set":
|
elif cmd == "Set":
|
||||||
if "key" not in args or \
|
if "key" not in args or args["key"].startswith("_read_") or \
|
||||||
"operations" not in args or not type(args["operations"]) == list:
|
"operations" not in args or not type(args["operations"]) == list:
|
||||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments",
|
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments",
|
||||||
"text": 'Set', "original_cmd": cmd}])
|
"text": 'Set', "original_cmd": cmd}])
|
||||||
@@ -1905,6 +1942,37 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
"""Sends an item to the specified player"""
|
"""Sends an item to the specified player"""
|
||||||
return self._cmd_send_multiple(1, player_name, *item_name)
|
return self._cmd_send_multiple(1, player_name, *item_name)
|
||||||
|
|
||||||
|
def _cmd_send_location(self, player_name: str, *location_name: str) -> bool:
|
||||||
|
"""Send out item from a player's location as though they checked it"""
|
||||||
|
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||||
|
if usable:
|
||||||
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
|
game = self.ctx.games[slot]
|
||||||
|
full_name = " ".join(location_name)
|
||||||
|
|
||||||
|
if full_name.isnumeric():
|
||||||
|
location, usable, response = int(full_name), True, None
|
||||||
|
elif self.ctx.location_names_for_game(game) is not None:
|
||||||
|
location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game))
|
||||||
|
else:
|
||||||
|
self.output("Can't look up location for unknown game. Send by ID instead.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if usable:
|
||||||
|
if isinstance(location, int):
|
||||||
|
register_location_checks(self.ctx, team, slot, [location])
|
||||||
|
else:
|
||||||
|
seeked_location: int = self.ctx.location_names_for_game(self.ctx.games[slot])[location]
|
||||||
|
register_location_checks(self.ctx, team, slot, [seeked_location])
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.output(response)
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.output(response)
|
||||||
|
return False
|
||||||
|
|
||||||
def _cmd_hint(self, player_name: str, *item_name: str) -> bool:
|
def _cmd_hint(self, player_name: str, *item_name: str) -> bool:
|
||||||
"""Send out a hint for a player's item to their team"""
|
"""Send out a hint for a player's item to their team"""
|
||||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||||
@@ -1931,7 +1999,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
hints = collect_hints(self.ctx, team, slot, item)
|
hints = collect_hints(self.ctx, team, slot, item)
|
||||||
|
|
||||||
if hints:
|
if hints:
|
||||||
notify_hints(self.ctx, team, hints)
|
self.ctx.notify_hints(team, hints)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.output("No hints found.")
|
self.output("No hints found.")
|
||||||
@@ -1966,7 +2034,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
else:
|
else:
|
||||||
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
||||||
if hints:
|
if hints:
|
||||||
notify_hints(self.ctx, team, hints)
|
self.ctx.notify_hints(team, hints)
|
||||||
else:
|
else:
|
||||||
self.output("No hints found.")
|
self.output("No hints found.")
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import argparse
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import os
|
import os
|
||||||
|
import zipfile
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
@@ -217,13 +218,18 @@ def adjust(args):
|
|||||||
# Load up the ROM
|
# Load up the ROM
|
||||||
rom = Rom(file=args.rom, force_use=True)
|
rom = Rom(file=args.rom, force_use=True)
|
||||||
delete_zootdec = True
|
delete_zootdec = True
|
||||||
elif os.path.splitext(args.rom)[-1] == '.apz5':
|
elif os.path.splitext(args.rom)[-1] in ['.apz5', '.zpf']:
|
||||||
# Load vanilla ROM
|
# Load vanilla ROM
|
||||||
rom = Rom(file=args.vanilla_rom, force_use=True)
|
rom = Rom(file=args.vanilla_rom, force_use=True)
|
||||||
|
apz5_file = args.rom
|
||||||
|
base_name = os.path.splitext(apz5_file)[0]
|
||||||
# Patch file
|
# Patch file
|
||||||
apply_patch_file(rom, args.rom)
|
apply_patch_file(rom, apz5_file,
|
||||||
|
sub_file=(os.path.basename(base_name) + '.zpf'
|
||||||
|
if zipfile.is_zipfile(apz5_file)
|
||||||
|
else None))
|
||||||
else:
|
else:
|
||||||
raise Exception("Invalid file extension; requires .n64, .z64, .apz5")
|
raise Exception("Invalid file extension; requires .n64, .z64, .apz5, .zpf")
|
||||||
# Call patch_cosmetics
|
# Call patch_cosmetics
|
||||||
try:
|
try:
|
||||||
patch_cosmetics(ootworld, rom)
|
patch_cosmetics(ootworld, rom)
|
||||||
|
|||||||
46
OoTClient.py
46
OoTClient.py
@@ -3,6 +3,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import zipfile
|
||||||
from asyncio import StreamReader, StreamWriter
|
from asyncio import StreamReader, StreamWriter
|
||||||
|
|
||||||
# CommonClient import first to trigger ModuleUpdater
|
# CommonClient import first to trigger ModuleUpdater
|
||||||
@@ -50,7 +51,7 @@ deathlink_sent_this_death: we interacted with the multiworld on this death, wait
|
|||||||
|
|
||||||
oot_loc_name_to_id = network_data_package["games"]["Ocarina of Time"]["location_name_to_id"]
|
oot_loc_name_to_id = network_data_package["games"]["Ocarina of Time"]["location_name_to_id"]
|
||||||
|
|
||||||
script_version: int = 2
|
script_version: int = 3
|
||||||
|
|
||||||
def get_item_value(ap_id):
|
def get_item_value(ap_id):
|
||||||
return ap_id - 66000
|
return ap_id - 66000
|
||||||
@@ -85,6 +86,9 @@ class OoTContext(CommonContext):
|
|||||||
self.n64_status = CONNECTION_INITIAL_STATUS
|
self.n64_status = CONNECTION_INITIAL_STATUS
|
||||||
self.awaiting_rom = False
|
self.awaiting_rom = False
|
||||||
self.location_table = {}
|
self.location_table = {}
|
||||||
|
self.collectible_table = {}
|
||||||
|
self.collectible_override_flags_address = 0
|
||||||
|
self.collectible_offsets = {}
|
||||||
self.deathlink_enabled = False
|
self.deathlink_enabled = False
|
||||||
self.deathlink_pending = False
|
self.deathlink_pending = False
|
||||||
self.deathlink_sent_this_death = False
|
self.deathlink_sent_this_death = False
|
||||||
@@ -117,6 +121,13 @@ class OoTContext(CommonContext):
|
|||||||
self.ui = OoTManager(self)
|
self.ui = OoTManager(self)
|
||||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||||
|
|
||||||
|
def on_package(self, cmd, args):
|
||||||
|
if cmd == 'Connected':
|
||||||
|
slot_data = args.get('slot_data', None)
|
||||||
|
if slot_data:
|
||||||
|
self.collectible_override_flags_address = slot_data.get('collectible_override_flags', 0)
|
||||||
|
self.collectible_offsets = slot_data.get('collectible_flag_offsets', {})
|
||||||
|
|
||||||
|
|
||||||
def get_payload(ctx: OoTContext):
|
def get_payload(ctx: OoTContext):
|
||||||
if ctx.deathlink_enabled and ctx.deathlink_pending:
|
if ctx.deathlink_enabled and ctx.deathlink_pending:
|
||||||
@@ -125,11 +136,14 @@ def get_payload(ctx: OoTContext):
|
|||||||
else:
|
else:
|
||||||
trigger_death = False
|
trigger_death = False
|
||||||
|
|
||||||
return json.dumps({
|
payload = json.dumps({
|
||||||
"items": [get_item_value(item.item) for item in ctx.items_received],
|
"items": [get_item_value(item.item) for item in ctx.items_received],
|
||||||
"playerNames": [name for (i, name) in ctx.player_names.items() if i != 0],
|
"playerNames": [name for (i, name) in ctx.player_names.items() if i != 0],
|
||||||
"triggerDeath": trigger_death
|
"triggerDeath": trigger_death,
|
||||||
|
"collectibleOverrides": ctx.collectible_override_flags_address,
|
||||||
|
"collectibleOffsets": ctx.collectible_offsets
|
||||||
})
|
})
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
|
async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
|
||||||
@@ -141,6 +155,7 @@ async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
|
|||||||
ctx.deathlink_client_override = False
|
ctx.deathlink_client_override = False
|
||||||
ctx.finished_game = False
|
ctx.finished_game = False
|
||||||
ctx.location_table = {}
|
ctx.location_table = {}
|
||||||
|
ctx.collectible_table = {}
|
||||||
ctx.deathlink_pending = False
|
ctx.deathlink_pending = False
|
||||||
ctx.deathlink_sent_this_death = False
|
ctx.deathlink_sent_this_death = False
|
||||||
ctx.auth = payload['playerName']
|
ctx.auth = payload['playerName']
|
||||||
@@ -161,11 +176,17 @@ async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
|
|||||||
ctx.finished_game = True
|
ctx.finished_game = True
|
||||||
|
|
||||||
# Locations handling
|
# Locations handling
|
||||||
if ctx.location_table != payload['locations']:
|
locations = payload['locations']
|
||||||
ctx.location_table = payload['locations']
|
collectibles = payload['collectibles']
|
||||||
|
|
||||||
|
if ctx.location_table != locations or ctx.collectible_table != collectibles:
|
||||||
|
ctx.location_table = locations
|
||||||
|
ctx.collectible_table = collectibles
|
||||||
|
locs1 = [oot_loc_name_to_id[loc] for loc, b in ctx.location_table.items() if b]
|
||||||
|
locs2 = [int(loc) for loc, b in ctx.collectible_table.items() if b]
|
||||||
await ctx.send_msgs([{
|
await ctx.send_msgs([{
|
||||||
"cmd": "LocationChecks",
|
"cmd": "LocationChecks",
|
||||||
"locations": [oot_loc_name_to_id[loc] for loc in ctx.location_table if ctx.location_table[loc]]
|
"locations": locs1 + locs2
|
||||||
}])
|
}])
|
||||||
|
|
||||||
# Deathlink handling
|
# Deathlink handling
|
||||||
@@ -191,13 +212,6 @@ async def n64_sync_task(ctx: OoTContext):
|
|||||||
try:
|
try:
|
||||||
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
||||||
try:
|
try:
|
||||||
# Data will return a dict with up to six fields:
|
|
||||||
# 1. str: player name (always)
|
|
||||||
# 2. int: script version (always)
|
|
||||||
# 3. bool: deathlink active (always)
|
|
||||||
# 4. dict[str, bool]: checked locations
|
|
||||||
# 5. bool: whether Link is currently at 0 HP
|
|
||||||
# 6. bool: whether the game currently registers as complete
|
|
||||||
data = await asyncio.wait_for(reader.readline(), timeout=10)
|
data = await asyncio.wait_for(reader.readline(), timeout=10)
|
||||||
data_decoded = json.loads(data.decode())
|
data_decoded = json.loads(data.decode())
|
||||||
reported_version = data_decoded.get('scriptVersion', 0)
|
reported_version = data_decoded.get('scriptVersion', 0)
|
||||||
@@ -270,12 +284,16 @@ async def run_game(romfile):
|
|||||||
|
|
||||||
|
|
||||||
async def patch_and_run_game(apz5_file):
|
async def patch_and_run_game(apz5_file):
|
||||||
|
apz5_file = os.path.abspath(apz5_file)
|
||||||
base_name = os.path.splitext(apz5_file)[0]
|
base_name = os.path.splitext(apz5_file)[0]
|
||||||
decomp_path = base_name + '-decomp.z64'
|
decomp_path = base_name + '-decomp.z64'
|
||||||
comp_path = base_name + '.z64'
|
comp_path = base_name + '.z64'
|
||||||
# Load vanilla ROM, patch file, compress ROM
|
# Load vanilla ROM, patch file, compress ROM
|
||||||
rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"]))
|
rom = Rom(Utils.local_path(Utils.get_options()["oot_options"]["rom_file"]))
|
||||||
apply_patch_file(rom, apz5_file)
|
apply_patch_file(rom, apz5_file,
|
||||||
|
sub_file=(os.path.basename(base_name) + '.zpf'
|
||||||
|
if zipfile.is_zipfile(apz5_file)
|
||||||
|
else None))
|
||||||
rom.write_to_file(decomp_path)
|
rom.write_to_file(decomp_path)
|
||||||
os.chdir(data_path("Compress"))
|
os.chdir(data_path("Compress"))
|
||||||
compress_rom_file(decomp_path, comp_path)
|
compress_rom_file(decomp_path, comp_path)
|
||||||
|
|||||||
@@ -927,7 +927,8 @@ class ItemLinks(OptionList):
|
|||||||
Optional("exclude"): [And(str, len)],
|
Optional("exclude"): [And(str, len)],
|
||||||
"replacement_item": Or(And(str, len), None),
|
"replacement_item": Or(And(str, len), None),
|
||||||
Optional("local_items"): [And(str, len)],
|
Optional("local_items"): [And(str, len)],
|
||||||
Optional("non_local_items"): [And(str, len)]
|
Optional("non_local_items"): [And(str, len)],
|
||||||
|
Optional("link_replacement"): Or(None, bool),
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -950,6 +951,7 @@ class ItemLinks(OptionList):
|
|||||||
return pool
|
return pool
|
||||||
|
|
||||||
def verify(self, world, player_name: str, plando_options) -> None:
|
def verify(self, world, player_name: str, plando_options) -> None:
|
||||||
|
link: dict
|
||||||
super(ItemLinks, self).verify(world, player_name, plando_options)
|
super(ItemLinks, self).verify(world, player_name, plando_options)
|
||||||
existing_links = set()
|
existing_links = set()
|
||||||
for link in self.value:
|
for link in self.value:
|
||||||
@@ -974,7 +976,9 @@ class ItemLinks(OptionList):
|
|||||||
|
|
||||||
intersection = local_items.intersection(non_local_items)
|
intersection = local_items.intersection(non_local_items)
|
||||||
if intersection:
|
if intersection:
|
||||||
raise Exception(f"item_link {link['name']} has {intersection} items in both its local_items and non_local_items pool.")
|
raise Exception(f"item_link {link['name']} has {intersection} "
|
||||||
|
f"items in both its local_items and non_local_items pool.")
|
||||||
|
link.setdefault("link_replacement", None)
|
||||||
|
|
||||||
|
|
||||||
per_game_common_options = {
|
per_game_common_options = {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP
|
|||||||
get_base_parser
|
get_base_parser
|
||||||
|
|
||||||
from worlds.pokemon_rb.locations import location_data
|
from worlds.pokemon_rb.locations import location_data
|
||||||
|
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
|
||||||
|
|
||||||
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}}
|
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}}
|
||||||
location_bytes_bits = {}
|
location_bytes_bits = {}
|
||||||
@@ -39,6 +40,8 @@ CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
|||||||
|
|
||||||
DISPLAY_MSGS = True
|
DISPLAY_MSGS = True
|
||||||
|
|
||||||
|
SCRIPT_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
class GBCommandProcessor(ClientCommandProcessor):
|
class GBCommandProcessor(ClientCommandProcessor):
|
||||||
def __init__(self, ctx: CommonContext):
|
def __init__(self, ctx: CommonContext):
|
||||||
@@ -53,7 +56,6 @@ class GBCommandProcessor(ClientCommandProcessor):
|
|||||||
class GBContext(CommonContext):
|
class GBContext(CommonContext):
|
||||||
command_processor = GBCommandProcessor
|
command_processor = GBCommandProcessor
|
||||||
game = 'Pokemon Red and Blue'
|
game = 'Pokemon Red and Blue'
|
||||||
items_handling = 0b101
|
|
||||||
|
|
||||||
def __init__(self, server_address, password):
|
def __init__(self, server_address, password):
|
||||||
super().__init__(server_address, password)
|
super().__init__(server_address, password)
|
||||||
@@ -64,6 +66,10 @@ class GBContext(CommonContext):
|
|||||||
self.gb_status = CONNECTION_INITIAL_STATUS
|
self.gb_status = CONNECTION_INITIAL_STATUS
|
||||||
self.awaiting_rom = False
|
self.awaiting_rom = False
|
||||||
self.display_msgs = True
|
self.display_msgs = True
|
||||||
|
self.deathlink_pending = False
|
||||||
|
self.set_deathlink = False
|
||||||
|
self.client_compatibility_mode = 0
|
||||||
|
self.items_handling = 0b001
|
||||||
|
|
||||||
async def server_auth(self, password_requested: bool = False):
|
async def server_auth(self, password_requested: bool = False):
|
||||||
if password_requested and not self.password:
|
if password_requested and not self.password:
|
||||||
@@ -82,6 +88,8 @@ class GBContext(CommonContext):
|
|||||||
def on_package(self, cmd: str, args: dict):
|
def on_package(self, cmd: str, args: dict):
|
||||||
if cmd == 'Connected':
|
if cmd == 'Connected':
|
||||||
self.locations_array = None
|
self.locations_array = None
|
||||||
|
if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
|
||||||
|
self.set_deathlink = True
|
||||||
elif cmd == "RoomInfo":
|
elif cmd == "RoomInfo":
|
||||||
self.seed_name = args['seed_name']
|
self.seed_name = args['seed_name']
|
||||||
elif cmd == 'Print':
|
elif cmd == 'Print':
|
||||||
@@ -92,6 +100,10 @@ class GBContext(CommonContext):
|
|||||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||||
|
|
||||||
|
def on_deathlink(self, data: dict):
|
||||||
|
self.deathlink_pending = True
|
||||||
|
super().on_deathlink(data)
|
||||||
|
|
||||||
def run_gui(self):
|
def run_gui(self):
|
||||||
from kvui import GameManager
|
from kvui import GameManager
|
||||||
|
|
||||||
@@ -107,13 +119,16 @@ class GBContext(CommonContext):
|
|||||||
|
|
||||||
def get_payload(ctx: GBContext):
|
def get_payload(ctx: GBContext):
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
return json.dumps(
|
ret = json.dumps(
|
||||||
{
|
{
|
||||||
"items": [item.item for item in ctx.items_received],
|
"items": [item.item for item in ctx.items_received],
|
||||||
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||||
if key[0] > current_time - 10}
|
if key[0] > current_time - 10},
|
||||||
|
"deathlink": ctx.deathlink_pending
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
ctx.deathlink_pending = False
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
async def parse_locations(data: List, ctx: GBContext):
|
async def parse_locations(data: List, ctx: GBContext):
|
||||||
@@ -121,14 +136,8 @@ async def parse_locations(data: List, ctx: GBContext):
|
|||||||
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
|
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
|
||||||
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]}
|
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E], "Rod": data[0x140 + 0x20 + 0x0E:]}
|
||||||
|
|
||||||
# Check for clear problems
|
|
||||||
if len(flags['Rod']) > 1:
|
if len(flags['Rod']) > 1:
|
||||||
return
|
return
|
||||||
if flags["EventFlag"][1] + flags["EventFlag"][8] + flags["EventFlag"][9] + flags["EventFlag"][12] \
|
|
||||||
+ flags["EventFlag"][61] + flags["EventFlag"][62] + flags["EventFlag"][63] + flags["EventFlag"][64] \
|
|
||||||
+ flags["EventFlag"][65] + flags["EventFlag"][66] + flags["EventFlag"][67] + flags["EventFlag"][68] \
|
|
||||||
+ flags["EventFlag"][69] + flags["EventFlag"][70] != 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
for flag_type, loc_map in location_map.items():
|
for flag_type, loc_map in location_map.items():
|
||||||
for flag, loc_id in loc_map.items():
|
for flag, loc_id in loc_map.items():
|
||||||
@@ -168,8 +177,15 @@ async def gb_sync_task(ctx: GBContext):
|
|||||||
# 2. An array representing the memory values of the locations area (if in game)
|
# 2. An array representing the memory values of the locations area (if in game)
|
||||||
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||||
data_decoded = json.loads(data.decode())
|
data_decoded = json.loads(data.decode())
|
||||||
#print(data_decoded)
|
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
|
||||||
|
msg = "You are connecting with an incompatible Lua script version. Ensure your connector Lua " \
|
||||||
|
"and PokemonClient are from the same Archipelago installation."
|
||||||
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
|
ctx.gui_error('Error', msg)
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
|
ctx.client_compatibility_mode = data_decoded['clientCompatibilityVersion']
|
||||||
|
if ctx.client_compatibility_mode == 0:
|
||||||
|
ctx.items_handling = 0b101 # old patches will not have local start inventory, must be requested
|
||||||
if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]):
|
if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]):
|
||||||
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
|
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
|
||||||
logger.info(msg, extra={'compact_gui': True})
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
@@ -179,13 +195,20 @@ async def gb_sync_task(ctx: GBContext):
|
|||||||
if not ctx.auth:
|
if not ctx.auth:
|
||||||
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
||||||
if ctx.auth == '':
|
if ctx.auth == '':
|
||||||
logger.info("Invalid ROM detected. No player name built into the ROM.")
|
msg = "Invalid ROM detected. No player name built into the ROM."
|
||||||
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
|
ctx.gui_error('Error', msg)
|
||||||
|
error_status = CONNECTION_RESET_STATUS
|
||||||
if ctx.awaiting_rom:
|
if ctx.awaiting_rom:
|
||||||
await ctx.server_auth(False)
|
await ctx.server_auth(False)
|
||||||
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
|
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
|
||||||
and not error_status and ctx.auth:
|
and not error_status and ctx.auth:
|
||||||
# Not just a keep alive ping, parse
|
# Not just a keep alive ping, parse
|
||||||
async_start(parse_locations(data_decoded['locations'], ctx))
|
async_start(parse_locations(data_decoded['locations'], ctx))
|
||||||
|
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
|
||||||
|
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
|
||||||
|
if ctx.set_deathlink:
|
||||||
|
await ctx.update_death_link(True)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.debug("Read Timed Out, Reconnecting")
|
logger.debug("Read Timed Out, Reconnecting")
|
||||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||||
@@ -243,8 +266,16 @@ async def run_game(romfile):
|
|||||||
async def patch_and_run_game(game_version, patch_file, ctx):
|
async def patch_and_run_game(game_version, patch_file, ctx):
|
||||||
base_name = os.path.splitext(patch_file)[0]
|
base_name = os.path.splitext(patch_file)[0]
|
||||||
comp_path = base_name + '.gb'
|
comp_path = base_name + '.gb'
|
||||||
with open(Utils.local_path(Utils.get_options()["pokemon_rb_options"][f"{game_version}_rom_file"]), "rb") as stream:
|
if game_version == "blue":
|
||||||
base_rom = bytes(stream.read())
|
delta_patch = BlueDeltaPatch
|
||||||
|
else:
|
||||||
|
delta_patch = RedDeltaPatch
|
||||||
|
|
||||||
|
try:
|
||||||
|
base_rom = delta_patch.get_source_data()
|
||||||
|
except Exception as msg:
|
||||||
|
logger.info(msg, extra={'compact_gui': True})
|
||||||
|
ctx.gui_error('Error', msg)
|
||||||
|
|
||||||
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
|
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
|
||||||
with patch_archive.open('delta.bsdiff4', 'r') as stream:
|
with patch_archive.open('delta.bsdiff4', 'r') as stream:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Currently, the following games are supported:
|
|||||||
* Hylics 2
|
* Hylics 2
|
||||||
* Overcooked! 2
|
* Overcooked! 2
|
||||||
* Zillion
|
* Zillion
|
||||||
|
* Lufia II Ancient Cave
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
|
|||||||
@@ -639,6 +639,13 @@ def request_unfinished_missions(ctx: SC2Context):
|
|||||||
|
|
||||||
_, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
|
_, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks)
|
||||||
|
|
||||||
|
# Removing All-In from location pool
|
||||||
|
final_mission = lookup_id_to_mission[ctx.final_mission]
|
||||||
|
if final_mission in unfinished_missions.keys():
|
||||||
|
message = f"Final Mission Available: {final_mission}[{ctx.final_mission}]\n" + message
|
||||||
|
if unfinished_missions[final_mission] == -1:
|
||||||
|
unfinished_missions.pop(final_mission)
|
||||||
|
|
||||||
message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
|
message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " +
|
||||||
mark_up_objectives(
|
mark_up_objectives(
|
||||||
f"[{len(unfinished_missions[mission])}/"
|
f"[{len(unfinished_missions[mission])}/"
|
||||||
|
|||||||
44
Utils.py
44
Utils.py
@@ -38,7 +38,7 @@ class Version(typing.NamedTuple):
|
|||||||
build: int
|
build: int
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.3.6"
|
__version__ = "0.3.7"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
@@ -99,7 +99,7 @@ def local_path(*path: str) -> str:
|
|||||||
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||||
else:
|
else:
|
||||||
import __main__
|
import __main__
|
||||||
if hasattr(__main__, "__file__"):
|
if hasattr(__main__, "__file__") and os.path.isfile(__main__.__file__):
|
||||||
# we are running in a normal Python environment
|
# we are running in a normal Python environment
|
||||||
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
|
||||||
else:
|
else:
|
||||||
@@ -236,7 +236,7 @@ def get_default_options() -> OptionsType:
|
|||||||
"bridge_chat_out": True,
|
"bridge_chat_out": True,
|
||||||
},
|
},
|
||||||
"sni_options": {
|
"sni_options": {
|
||||||
"sni": "SNI",
|
"sni_path": "SNI",
|
||||||
"snes_rom_start": True,
|
"snes_rom_start": True,
|
||||||
},
|
},
|
||||||
"sm_options": {
|
"sm_options": {
|
||||||
@@ -268,13 +268,12 @@ def get_default_options() -> OptionsType:
|
|||||||
"log_network": 0
|
"log_network": 0
|
||||||
},
|
},
|
||||||
"generator": {
|
"generator": {
|
||||||
"teams": 1,
|
|
||||||
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core"),
|
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core"),
|
||||||
"player_files_path": "Players",
|
"player_files_path": "Players",
|
||||||
"players": 0,
|
"players": 0,
|
||||||
"weights_file_path": "weights.yaml",
|
"weights_file_path": "weights.yaml",
|
||||||
"meta_file_path": "meta.yaml",
|
"meta_file_path": "meta.yaml",
|
||||||
"spoiler": 2,
|
"spoiler": 3,
|
||||||
"glitch_triforce_room": 1,
|
"glitch_triforce_room": 1,
|
||||||
"race": 0,
|
"race": 0,
|
||||||
"plando_options": "bosses",
|
"plando_options": "bosses",
|
||||||
@@ -286,6 +285,7 @@ def get_default_options() -> OptionsType:
|
|||||||
},
|
},
|
||||||
"oot_options": {
|
"oot_options": {
|
||||||
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
|
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
|
||||||
|
"rom_start": True
|
||||||
},
|
},
|
||||||
"dkc3_options": {
|
"dkc3_options": {
|
||||||
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
|
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
|
||||||
@@ -303,9 +303,14 @@ def get_default_options() -> OptionsType:
|
|||||||
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
|
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
|
||||||
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
|
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
|
||||||
"rom_start": True
|
"rom_start": True
|
||||||
}
|
},
|
||||||
|
"ffr_options": {
|
||||||
|
"display_msgs": True,
|
||||||
|
},
|
||||||
|
"lufia2ac_options": {
|
||||||
|
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
@@ -452,6 +457,7 @@ loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': log
|
|||||||
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
|
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
|
||||||
log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
|
log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
|
||||||
exception_logger: typing.Optional[str] = None):
|
exception_logger: typing.Optional[str] = None):
|
||||||
|
import datetime
|
||||||
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
||||||
log_folder = user_path("logs")
|
log_folder = user_path("logs")
|
||||||
os.makedirs(log_folder, exist_ok=True)
|
os.makedirs(log_folder, exist_ok=True)
|
||||||
@@ -460,6 +466,8 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
|||||||
root_logger.removeHandler(handler)
|
root_logger.removeHandler(handler)
|
||||||
handler.close()
|
handler.close()
|
||||||
root_logger.setLevel(loglevel)
|
root_logger.setLevel(loglevel)
|
||||||
|
if "a" not in write_mode:
|
||||||
|
name += f"_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
|
||||||
file_handler = logging.FileHandler(
|
file_handler = logging.FileHandler(
|
||||||
os.path.join(log_folder, f"{name}.txt"),
|
os.path.join(log_folder, f"{name}.txt"),
|
||||||
write_mode,
|
write_mode,
|
||||||
@@ -487,7 +495,25 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
|||||||
|
|
||||||
sys.excepthook = handle_exception
|
sys.excepthook = handle_exception
|
||||||
|
|
||||||
logging.info(f"Archipelago ({__version__}) logging initialized.")
|
def _cleanup():
|
||||||
|
for file in os.scandir(log_folder):
|
||||||
|
if file.name.endswith(".txt"):
|
||||||
|
last_change = datetime.datetime.fromtimestamp(file.stat().st_mtime)
|
||||||
|
if datetime.datetime.now() - last_change > datetime.timedelta(days=7):
|
||||||
|
try:
|
||||||
|
os.unlink(file.path)
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
else:
|
||||||
|
logging.info(f"Deleted old logfile {file.path}")
|
||||||
|
import threading
|
||||||
|
threading.Thread(target=_cleanup, name="LogCleaner").start()
|
||||||
|
import platform
|
||||||
|
logging.info(
|
||||||
|
f"Archipelago ({__version__}) logging initialized"
|
||||||
|
f" on {platform.platform()}"
|
||||||
|
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def stream_input(stream, queue):
|
def stream_input(stream, queue):
|
||||||
@@ -656,7 +682,7 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
|
|||||||
_faf_tasks: "Set[asyncio.Task[None]]" = set()
|
_faf_tasks: "Set[asyncio.Task[None]]" = set()
|
||||||
|
|
||||||
|
|
||||||
def async_start(co: Coroutine[None, None, None], name: Optional[str] = None) -> None:
|
def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Use this to start a task when you don't keep a reference to it or immediately await it,
|
Use this to start a task when you don't keep a reference to it or immediately await it,
|
||||||
to prevent early garbage collection. "fire-and-forget"
|
to prevent early garbage collection. "fire-and-forget"
|
||||||
|
|||||||
@@ -39,10 +39,11 @@ def get_datapackage():
|
|||||||
|
|
||||||
@api_endpoints.route('/datapackage_version')
|
@api_endpoints.route('/datapackage_version')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
|
|
||||||
def get_datapackage_versions():
|
def get_datapackage_versions():
|
||||||
from worlds import network_data_package, AutoWorldRegister
|
from worlds import network_data_package, AutoWorldRegister
|
||||||
|
|
||||||
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
|
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
|
||||||
version_package["version"] = network_data_package["version"]
|
|
||||||
return version_package
|
return version_package
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import json
|
|||||||
import pickle
|
import pickle
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from flask import request, session, url_for
|
from flask import request, session, url_for, Markup
|
||||||
from pony.orm import commit
|
from pony.orm import commit
|
||||||
|
|
||||||
from WebHostLib import app
|
from WebHostLib import app
|
||||||
@@ -21,13 +21,18 @@ def generate_api():
|
|||||||
if 'file' in request.files:
|
if 'file' in request.files:
|
||||||
file = request.files['file']
|
file = request.files['file']
|
||||||
options = get_yaml_data(file)
|
options = get_yaml_data(file)
|
||||||
if type(options) == str:
|
if isinstance(options, Markup):
|
||||||
|
return {"text": options.striptags()}, 400
|
||||||
|
if isinstance(options, str):
|
||||||
return {"text": options}, 400
|
return {"text": options}, 400
|
||||||
if "race" in request.form:
|
if "race" in request.form:
|
||||||
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
|
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
|
||||||
meta_options_source = request.form
|
meta_options_source = request.form
|
||||||
|
|
||||||
json_data = request.get_json()
|
# json_data is optional, we can have it silently fall to None as it used to do.
|
||||||
|
# See https://flask.palletsprojects.com/en/2.2.x/api/#flask.Request.get_json -> Changelog -> 2.1
|
||||||
|
json_data = request.get_json(silent=True)
|
||||||
|
|
||||||
if json_data:
|
if json_data:
|
||||||
meta_options_source = json_data
|
meta_options_source = json_data
|
||||||
if 'weights' in json_data:
|
if 'weights' in json_data:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import zipfile
|
import zipfile
|
||||||
from typing import *
|
from typing import *
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, render_template
|
from flask import request, flash, redirect, url_for, render_template, Markup
|
||||||
|
|
||||||
from WebHostLib import app
|
from WebHostLib import app
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ def check():
|
|||||||
else:
|
else:
|
||||||
file = request.files['file']
|
file = request.files['file']
|
||||||
options = get_yaml_data(file)
|
options = get_yaml_data(file)
|
||||||
if type(options) == str:
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
results, _ = roll_options(options)
|
results, _ = roll_options(options)
|
||||||
@@ -38,7 +38,7 @@ def mysterycheck():
|
|||||||
return redirect(url_for("check"), 301)
|
return redirect(url_for("check"), 301)
|
||||||
|
|
||||||
|
|
||||||
def get_yaml_data(file) -> Union[Dict[str, str], str]:
|
def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
|
||||||
options = {}
|
options = {}
|
||||||
# if user does not select file, browser also
|
# if user does not select file, browser also
|
||||||
# submit an empty part without filename
|
# submit an empty part without filename
|
||||||
@@ -50,6 +50,10 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
|
|||||||
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):
|
||||||
|
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||||
|
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||||
|
|
||||||
for file in infolist:
|
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. Your file was deleted."
|
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
||||||
|
|||||||
@@ -72,7 +72,14 @@ def download_slot_file(room_id, player_id: int):
|
|||||||
if name.endswith("info.json"):
|
if name.endswith("info.json"):
|
||||||
fname = name.rsplit("/", 1)[0] + ".zip"
|
fname = name.rsplit("/", 1)[0] + ".zip"
|
||||||
elif slot_data.game == "Ocarina of Time":
|
elif slot_data.game == "Ocarina of Time":
|
||||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
|
stream = io.BytesIO(slot_data.data)
|
||||||
|
if zipfile.is_zipfile(stream):
|
||||||
|
with zipfile.ZipFile(stream) as zf:
|
||||||
|
for name in zf.namelist():
|
||||||
|
if name.endswith(".zpf"):
|
||||||
|
fname = name.rsplit(".", 1)[0] + ".apz5"
|
||||||
|
else: # pre-ootr-7.0 support
|
||||||
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
|
||||||
elif slot_data.game == "VVVVVV":
|
elif slot_data.game == "VVVVVV":
|
||||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6"
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6"
|
||||||
elif slot_data.game == "Zillion":
|
elif slot_data.game == "Zillion":
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ def generate(race=False):
|
|||||||
else:
|
else:
|
||||||
file = request.files['file']
|
file = request.files['file']
|
||||||
options = get_yaml_data(file)
|
options = get_yaml_data(file)
|
||||||
if type(options) == str:
|
if isinstance(options, str):
|
||||||
flash(options)
|
flash(options)
|
||||||
else:
|
else:
|
||||||
meta = get_meta(request.form)
|
meta = get_meta(request.form)
|
||||||
@@ -92,7 +92,7 @@ def generate(race=False):
|
|||||||
return render_template("generate.html", race=race, version=__version__)
|
return render_template("generate.html", race=race, version=__version__)
|
||||||
|
|
||||||
|
|
||||||
def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
|
||||||
if not meta:
|
if not meta:
|
||||||
meta: Dict[str, Any] = {}
|
meta: Dict[str, Any] = {}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ def gen_game(gen_options, meta: Optional[Dict[str, Any]] = None, owner=None, sid
|
|||||||
erargs = parse_arguments(['--multi', str(playercount)])
|
erargs = parse_arguments(['--multi', str(playercount)])
|
||||||
erargs.seed = seed
|
erargs.seed = seed
|
||||||
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
|
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
|
||||||
erargs.spoiler = 0 if race else 2
|
erargs.spoiler = 0 if race else 3
|
||||||
erargs.race = race
|
erargs.race = race
|
||||||
erargs.outputname = seedname
|
erargs.outputname = seedname
|
||||||
erargs.outputpath = target.name
|
erargs.outputpath = target.name
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def create():
|
|||||||
|
|
||||||
del file_data
|
del file_data
|
||||||
|
|
||||||
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
|
with open(os.path.join(target_folder, "configs", game_name + ".yaml"), "w", encoding="utf-8") as f:
|
||||||
f.write(res)
|
f.write(res)
|
||||||
|
|
||||||
# Generate JSON files for player-settings pages
|
# Generate JSON files for player-settings pages
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ pony>=0.7.16
|
|||||||
waitress>=2.1.2
|
waitress>=2.1.2
|
||||||
Flask-Caching>=2.0.1
|
Flask-Caching>=2.0.1
|
||||||
Flask-Compress>=1.13
|
Flask-Compress>=1.13
|
||||||
Flask-Limiter>=2.7.0
|
Flask-Limiter>=2.8.1
|
||||||
bokeh>=3.0.0
|
bokeh>=3.0.2
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ window.addEventListener('load', () => {
|
|||||||
"ordering": true,
|
"ordering": true,
|
||||||
"info": false,
|
"info": false,
|
||||||
"dom": "t",
|
"dom": "t",
|
||||||
|
"stateSave": true,
|
||||||
});
|
});
|
||||||
console.log(tables);
|
console.log(tables);
|
||||||
});
|
});
|
||||||
|
|||||||
49
WebHostLib/static/assets/sc2wolTracker.js
Normal file
49
WebHostLib/static/assets/sc2wolTracker.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
window.addEventListener('load', () => {
|
||||||
|
// Reload tracker every 15 seconds
|
||||||
|
const url = window.location;
|
||||||
|
setInterval(() => {
|
||||||
|
const ajax = new XMLHttpRequest();
|
||||||
|
ajax.onreadystatechange = () => {
|
||||||
|
if (ajax.readyState !== 4) { return; }
|
||||||
|
|
||||||
|
// Create a fake DOM using the returned HTML
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||||
|
|
||||||
|
// Update item tracker
|
||||||
|
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||||
|
// Update only counters in the location-table
|
||||||
|
let counters = document.getElementsByClassName('counter');
|
||||||
|
const fakeCounters = fakeDOM.getElementsByClassName('counter');
|
||||||
|
for (let i = 0; i < counters.length; i++) {
|
||||||
|
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ajax.open('GET', url);
|
||||||
|
ajax.send();
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
// Collapsible advancement sections
|
||||||
|
const categories = document.getElementsByClassName("location-category");
|
||||||
|
for (let i = 0; i < categories.length; i++) {
|
||||||
|
let hide_id = categories[i].id.split('-')[0];
|
||||||
|
if (hide_id == 'Total') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
categories[i].addEventListener('click', function() {
|
||||||
|
// Toggle the advancement list
|
||||||
|
document.getElementById(hide_id).classList.toggle("hide");
|
||||||
|
// Change text of the header
|
||||||
|
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||||
|
const orig_text = tab_header.innerHTML;
|
||||||
|
let new_text;
|
||||||
|
if (orig_text.includes("▼")) {
|
||||||
|
new_text = orig_text.replace("▼", "▲");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
new_text = orig_text.replace("▲", "▼");
|
||||||
|
}
|
||||||
|
tab_header.innerHTML = new_text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -17,6 +17,13 @@ window.addEventListener('load', () => {
|
|||||||
paging: false,
|
paging: false,
|
||||||
info: false,
|
info: false,
|
||||||
dom: "t",
|
dom: "t",
|
||||||
|
stateSave: true,
|
||||||
|
stateSaveCallback: function(settings,data) {
|
||||||
|
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
|
||||||
|
},
|
||||||
|
stateLoadCallback: function(settings) {
|
||||||
|
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
|
||||||
|
},
|
||||||
columnDefs: [
|
columnDefs: [
|
||||||
{
|
{
|
||||||
targets: 'hours',
|
targets: 'hours',
|
||||||
@@ -68,10 +75,18 @@ window.addEventListener('load', () => {
|
|||||||
console.info(tables.search());
|
console.info(tables.search());
|
||||||
tables.draw();
|
tables.draw();
|
||||||
});
|
});
|
||||||
|
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
|
||||||
|
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
|
||||||
|
|
||||||
|
function getSleepTimeSeconds(){
|
||||||
|
// -40 % 60 is -40, which is absolutely wrong and should burn
|
||||||
|
var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60;
|
||||||
|
return sleepSeconds || 60;
|
||||||
|
}
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const target = $("<div></div>");
|
const target = $("<div></div>");
|
||||||
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
|
console.log("Updating Tracker...");
|
||||||
target.load("/tracker/" + tracker, function (response, status) {
|
target.load("/tracker/" + tracker, function (response, status) {
|
||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
target.find(".table").each(function (i, new_table) {
|
target.find(".table").each(function (i, new_table) {
|
||||||
@@ -90,9 +105,9 @@ window.addEventListener('load', () => {
|
|||||||
console.log(response);
|
console.log(response);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
setTimeout(update, getSleepTimeSeconds()*1000);
|
||||||
}
|
}
|
||||||
|
setTimeout(update, getSleepTimeSeconds()*1000);
|
||||||
setInterval(update, 30000);
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
adjustTableHeight();
|
adjustTableHeight();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ window.addEventListener('load', () => {
|
|||||||
"order": [[ 3, "desc" ]],
|
"order": [[ 3, "desc" ]],
|
||||||
"info": false,
|
"info": false,
|
||||||
"dom": "t",
|
"dom": "t",
|
||||||
|
"stateSave": true,
|
||||||
});
|
});
|
||||||
$("#seeds-table").DataTable({
|
$("#seeds-table").DataTable({
|
||||||
"paging": false,
|
"paging": false,
|
||||||
@@ -13,5 +14,6 @@ window.addEventListener('load', () => {
|
|||||||
"order": [[ 2, "desc" ]],
|
"order": [[ 2, "desc" ]],
|
||||||
"info": false,
|
"info": false,
|
||||||
"dom": "t",
|
"dom": "t",
|
||||||
|
"stateSave": true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ h5, h6{
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
background-color: #ffff00;
|
background-color: #ffff00;
|
||||||
}
|
}
|
||||||
|
.user-message a{
|
||||||
|
color: #ff7700;
|
||||||
|
}
|
||||||
|
|
||||||
.interactive{
|
.interactive{
|
||||||
color: #ffef00;
|
color: #ffef00;
|
||||||
|
|||||||
@@ -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: 448px;
|
width: 480px;
|
||||||
background-color: rgb(60, 114, 157);
|
background-color: rgb(60, 114, 157);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#location-table{
|
#location-table{
|
||||||
width: 448px;
|
width: 480px;
|
||||||
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;
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#location-table td:first-child {
|
#location-table td:first-child {
|
||||||
width: 272px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-category td:first-child {
|
.location-category td:first-child {
|
||||||
|
|||||||
110
WebHostLib/static/styles/sc2wolTracker.css
Normal file
110
WebHostLib/static/styles/sc2wolTracker.css
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#player-tracker-wrapper{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table{
|
||||||
|
border-top: 2px solid #000000;
|
||||||
|
border-left: 2px solid #000000;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
padding: 3px 3px 10px;
|
||||||
|
width: 500px;
|
||||||
|
background-color: #525494;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table td{
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table td.title{
|
||||||
|
padding-top: 10px;
|
||||||
|
height: 20px;
|
||||||
|
font-family: "JuraBook", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table img{
|
||||||
|
height: 100%;
|
||||||
|
max-width: 40px;
|
||||||
|
max-height: 40px;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
filter: grayscale(100%) contrast(75%) brightness(20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table img.acquired{
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.counted-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inventory-table div.item-count {
|
||||||
|
text-align: left;
|
||||||
|
color: black;
|
||||||
|
font-family: "JuraBook", monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table{
|
||||||
|
width: 500px;
|
||||||
|
border-left: 2px solid #000000;
|
||||||
|
border-right: 2px solid #000000;
|
||||||
|
border-bottom: 2px solid #000000;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
background-color: #525494;
|
||||||
|
padding: 10px 3px 3px;
|
||||||
|
font-family: "JuraBook", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table th{
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table td{
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table td.counter {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table td.toggle-arrow {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table tr#Total-header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table img{
|
||||||
|
height: 100%;
|
||||||
|
max-width: 30px;
|
||||||
|
max-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table tbody.locations {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-table td.location-name {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ def get_db_data(known_games: typing.Set[str]) -> typing.Tuple[typing.Counter[str
|
|||||||
typing.DefaultDict[datetime.date, typing.Dict[str, int]]]:
|
typing.DefaultDict[datetime.date, typing.Dict[str, int]]]:
|
||||||
games_played = defaultdict(Counter)
|
games_played = defaultdict(Counter)
|
||||||
total_games = Counter()
|
total_games = Counter()
|
||||||
cutoff = date.today()-timedelta(days=30)
|
cutoff = date.today() - timedelta(days=30)
|
||||||
room: Room
|
room: Room
|
||||||
for room in select(room for room in Room if room.creation_time >= cutoff):
|
for room in select(room for room in Room if room.creation_time >= cutoff):
|
||||||
for slot in room.seed.slots:
|
for slot in room.seed.slots:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div id="check-result" class="grass-island">
|
<div id="check-result" class="grass-island">
|
||||||
<h1>Verification Results</h1>
|
<h1>Verification Results</h1>
|
||||||
<p>The results of your requested file check are below.</p>
|
<p>The results of your requested file check are below.</p>
|
||||||
<table class="table autodatatable">
|
<table id="results-table" class="table autodatatable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>File</th>
|
<th>File</th>
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% include 'header/dirtHeader.html' %}
|
{% include 'header/dirtHeader.html' %}
|
||||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
|
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}" data-second="{{ saving_second }}">
|
||||||
<div id="tracker-header-bar">
|
<div id="tracker-header-bar">
|
||||||
<input placeholder="Search" id="search"/>
|
<input placeholder="Search" id="search"/>
|
||||||
<span class="info">This tracker will automatically update itself periodically.</span>
|
<span class="info">This tracker will automatically update itself periodically.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table non-unique-item-table">
|
<table id="received-table" class="table non-unique-item-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Item</th>
|
<th>Item</th>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table non-unique-item-table">
|
<table id="locations-table" class="table non-unique-item-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
No file to download for this game.
|
No file to download for this game.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
|
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
233
WebHostLib/templates/sc2wolTracker.html
Normal file
233
WebHostLib/templates/sc2wolTracker.html
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ player_name }}'s Tracker</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/sc2wolTracker.css') }}"/>
|
||||||
|
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/sc2wolTracker.js') }}"></script>
|
||||||
|
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/jura" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||||
|
<table id="inventory-table">
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Starting Resources
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Starting Minerals'] }}" class="{{ 'acquired' if '+15 Starting Minerals' in acquired_items }}" title="Starting Minerals" /></td>
|
||||||
|
<td colspan="2"><div class="item-count">+{{ minerals_count }}</div></td>
|
||||||
|
<td><img src="{{ icons['Starting Vespene'] }}" class="{{ 'acquired' if '+15 Starting Vespene' in acquired_items }}" title="Starting Vespene" /></td>
|
||||||
|
<td colspan="2"><div class="item-count">+{{ vespene_count }}</div></td>
|
||||||
|
<!--
|
||||||
|
<td><img src="{{ icons['Starting Supply'] }}" class="{{ 'acquired' if '+2 Starting Supply' in acquired_items }}" title="Starting Supply" /></td>
|
||||||
|
<td colspan="2"><div class="item-count">+{{ supply_count }}</div></td>
|
||||||
|
-->
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Weapon & Armor Upgrades
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ infantry_weapon_url }}" class="{{ 'acquired' if 'Progressive Infantry Weapon' in acquired_items }}" title="Progressive Infantry Weapons{% if infantry_weapon_level > 0 %} (Level {{ infantry_weapon_level }}){% endif %}" /></td>
|
||||||
|
<td><img src="{{ infantry_armor_url }}" class="{{ 'acquired' if 'Progressive Infantry Armor' in acquired_items }}" title="Progressive Infantry Armor{% if infantry_armor_level > 0 %} (Level {{ infantry_armor_level }}){% endif %}" /></td>
|
||||||
|
<td><img src="{{ vehicle_weapon_url }}" class="{{ 'acquired' if 'Progressive Vehicle Weapon' in acquired_items }}" title="Progressive Vehicle Weapons{% if vehicle_weapon_level > 0 %} (Level {{ vehicle_weapon_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_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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Base
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><img src="{{ icons['Bunker'] }}" class="{{ 'acquired' if 'Bunker' in acquired_items }}" title="Bunker" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Missile Turret'] }}" class="{{ 'acquired' if 'Missile Turret' in acquired_items }}" title="Missile Turret" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Sensor Tower'] }}" class="{{ 'acquired' if 'Sensor Tower' in acquired_items }}" title="Sensor Tower" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['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['Titanium Housing (Missile Turret)'] }}" class="{{ 'acquired' if 'Titanium Housing (Missile Turret)' in acquired_items }}" title="Titanium Housing (Missile Turret)" /></td>
|
||||||
|
<td><img src="{{ icons['Hellstorm Batteries (Missile Turret)'] }}" class="{{ 'acquired' if 'Hellstorm Batteries (Missile Turret)' in acquired_items }}" title="Hellstorm Batteries (Missile Turret)" /></td>
|
||||||
|
<td colspan="2"> </td>
|
||||||
|
<td><img src="{{ icons['Advanced Construction (SCV)'] }}" class="{{ 'acquired' if 'Advanced Construction (SCV)' in acquired_items }}" title="Advanced Construction (SCV)" /></td>
|
||||||
|
<td><img src="{{ icons['Dual-Fusion Welders (SCV)'] }}" class="{{ 'acquired' if 'Dual-Fusion Welders (SCV)' in acquired_items }}" title="Dual-Fusion Welders (SCV)" /></td>
|
||||||
|
<td><img src="{{ icons['Fire-Suppression System (Building)'] }}" class="{{ 'acquired' if 'Fire-Suppression System (Building)' in acquired_items }}" title="Fire-Suppression System (Building)" /></td>
|
||||||
|
<td><img src="{{ icons['Orbital Command (Building)'] }}" class="{{ 'acquired' if 'Orbital Command (Building)' in acquired_items }}" title="Orbital Command (Building)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Infantry
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><img src="{{ icons['Marine'] }}" class="{{ 'acquired' if 'Marine' in acquired_items }}" title="Marine" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Medic'] }}" class="{{ 'acquired' if 'Medic' in acquired_items }}" title="Medic" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Firebat'] }}" class="{{ 'acquired' if 'Firebat' in acquired_items }}" title="Firebat" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Marauder'] }}" class="{{ 'acquired' if 'Marauder' in acquired_items }}" title="Marauder" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Reaper'] }}" class="{{ 'acquired' if 'Reaper' in acquired_items }}" title="Reaper" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Stimpack (Marine)'] }}" class="{{ 'acquired' if 'Stimpack (Marine)' in acquired_items }}" title="Stimpack (Marine)" /></td>
|
||||||
|
<td><img src="{{ icons['Combat Shield (Marine)'] }}" class="{{ 'acquired' if 'Combat Shield (Marine)' in acquired_items }}" title="Combat Shield (Marine)" /></td>
|
||||||
|
<td><img src="{{ icons['Advanced Medic Facilities (Medic)'] }}" class="{{ 'acquired' if 'Advanced Medic Facilities (Medic)' in acquired_items }}" title="Advanced Medic Facilities (Medic)" /></td>
|
||||||
|
<td><img src="{{ icons['Stabilizer Medpacks (Medic)'] }}" class="{{ 'acquired' if 'Stabilizer Medpacks (Medic)' in acquired_items }}" title="Stabilizer Medpacks (Medic)" /></td>
|
||||||
|
<td><img src="{{ icons['Incinerator Gauntlets (Firebat)'] }}" class="{{ 'acquired' if 'Incinerator Gauntlets (Firebat)' in acquired_items }}" title="Incinerator Gauntlets (Firebat)" /></td>
|
||||||
|
<td><img src="{{ icons['Juggernaut Plating (Firebat)'] }}" class="{{ 'acquired' if 'Juggernaut Plating (Firebat)' in acquired_items }}" title="Juggernaut Plating (Firebat)" /></td>
|
||||||
|
<td><img src="{{ icons['Concussive Shells (Marauder)'] }}" class="{{ 'acquired' if 'Concussive Shells (Marauder)' in acquired_items }}" title="Concussive Shells (Marauder)" /></td>
|
||||||
|
<td><img src="{{ icons['Kinetic Foam (Marauder)'] }}" class="{{ 'acquired' if 'Kinetic Foam (Marauder)' in acquired_items }}" title="Kinetic Foam (Marauder)" /></td>
|
||||||
|
<td><img src="{{ icons['U-238 Rounds (Reaper)'] }}" class="{{ 'acquired' if 'U-238 Rounds (Reaper)' in acquired_items }}" title="U-238 Rounds (Reaper)" /></td>
|
||||||
|
<td><img src="{{ icons['G-4 Clusterbomb (Reaper)'] }}" class="{{ 'acquired' if 'G-4 Clusterbomb (Reaper)' in acquired_items }}" title="G-4 Clusterbomb (Reaper)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Vehicles
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><img src="{{ icons['Hellion'] }}" class="{{ 'acquired' if 'Hellion' in acquired_items }}" title="Hellion" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Vulture'] }}" class="{{ 'acquired' if 'Vulture' in acquired_items }}" title="Vulture" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Goliath'] }}" class="{{ 'acquired' if 'Goliath' in acquired_items }}" title="Goliath" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Diamondback'] }}" class="{{ 'acquired' if 'Diamondback' in acquired_items }}" title="Diamondback" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Siege Tank'] }}" class="{{ 'acquired' if 'Siege Tank' in acquired_items }}" title="Siege Tank" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Twin-Linked Flamethrower (Hellion)'] }}" class="{{ 'acquired' if 'Twin-Linked Flamethrower (Hellion)' in acquired_items }}" title="Twin-Linked Flamethrower (Hellion)" /></td>
|
||||||
|
<td><img src="{{ icons['Thermite Filaments (Hellion)'] }}" class="{{ 'acquired' if 'Thermite Filaments (Hellion)' in acquired_items }}" title="Thermite Filaments (Hellion)" /></td>
|
||||||
|
<td><img src="{{ icons['Cerberus Mine (Vulture)'] }}" class="{{ 'acquired' if 'Cerberus Mine (Vulture)' in acquired_items }}" title="Cerberus Mine (Vulture)" /></td>
|
||||||
|
<td><img src="{{ icons['Replenishable Magazine (Vulture)'] }}" class="{{ 'acquired' if 'Replenishable Magazine (Vulture)' in acquired_items }}" title="Replenishable Magazine (Vulture)" /></td>
|
||||||
|
<td><img src="{{ icons['Multi-Lock Weapons System (Goliath)'] }}" class="{{ 'acquired' if 'Multi-Lock Weapons System (Goliath)' in acquired_items }}" title="Multi-Lock Weapons System (Goliath)" /></td>
|
||||||
|
<td><img src="{{ icons['Ares-Class Targeting System (Goliath)'] }}" class="{{ 'acquired' if 'Ares-Class Targeting System (Goliath)' in acquired_items }}" title="Ares-Class Targeting System (Goliath)" /></td>
|
||||||
|
<td><img src="{{ icons['Tri-Lithium Power Cell (Diamondback)'] }}" class="{{ 'acquired' if 'Tri-Lithium Power Cell (Diamondback)' in acquired_items }}" title="Tri-Lithium Power Cell (Diamondback)" /></td>
|
||||||
|
<td><img src="{{ icons['Shaped Hull (Diamondback)'] }}" class="{{ 'acquired' if 'Shaped Hull (Diamondback)' in acquired_items }}" title="Shaped Hull (Diamondback)" /></td>
|
||||||
|
<td><img src="{{ icons['Maelstrom Rounds (Siege Tank)'] }}" class="{{ 'acquired' if 'Maelstrom Rounds (Siege Tank)' in acquired_items }}" title="Maelstrom Rounds (Siege Tank)" /></td>
|
||||||
|
<td><img src="{{ icons['Shaped Blast (Siege Tank)'] }}" class="{{ 'acquired' if 'Shaped Blast (Siege Tank)' in acquired_items }}" title="Shaped Blast (Siege Tank)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Starships
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><img src="{{ icons['Medivac'] }}" class="{{ 'acquired' if 'Medivac' in acquired_items }}" title="Medivac" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Wraith'] }}" class="{{ 'acquired' if 'Wraith' in acquired_items }}" title="Wraith" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Viking'] }}" class="{{ 'acquired' if 'Viking' in acquired_items }}" title="Viking" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Banshee'] }}" class="{{ 'acquired' if 'Banshee' in acquired_items }}" title="Banshee" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Battlecruiser'] }}" class="{{ 'acquired' if 'Battlecruiser' in acquired_items }}" title="Battlecruiser" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['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['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['Ripwave Missiles (Viking)'] }}" class="{{ 'acquired' if 'Ripwave Missiles (Viking)' in acquired_items }}" title="Ripwave Missiles (Viking)" /></td>
|
||||||
|
<td><img src="{{ icons['Phobos-Class Weapons System (Viking)'] }}" class="{{ 'acquired' if 'Phobos-Class Weapons System (Viking)' in acquired_items }}" title="Phobos-Class Weapons System (Viking)" /></td>
|
||||||
|
<td><img src="{{ icons['Cross-Spectrum Dampeners (Banshee)'] }}" class="{{ 'acquired' if 'Cross-Spectrum Dampeners (Banshee)' in acquired_items }}" title="Cross-Spectrum Dampeners (Banshee)" /></td>
|
||||||
|
<td><img src="{{ icons['Shockwave Missile Battery (Banshee)'] }}" class="{{ 'acquired' if 'Shockwave Missile Battery (Banshee)' in acquired_items }}" title="Shockwave Missile Battery (Banshee)" /></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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Dominion
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><img src="{{ icons['Ghost'] }}" class="{{ 'acquired' if 'Ghost' in acquired_items }}" title="Ghost" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Spectre'] }}" class="{{ 'acquired' if 'Spectre' in acquired_items }}" title="Spectre" /></td>
|
||||||
|
<td colspan="2"><img src="{{ icons['Thor'] }}" class="{{ 'acquired' if 'Thor' in acquired_items }}" title="Thor" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Ocular Implants (Ghost)'] }}" class="{{ 'acquired' if 'Ocular Implants (Ghost)' in acquired_items }}" title="Ocular Implants (Ghost)" /></td>
|
||||||
|
<td><img src="{{ icons['Crius Suit (Ghost)'] }}" class="{{ 'acquired' if 'Crius Suit (Ghost)' in acquired_items }}" title="Crius Suit (Ghost)" /></td>
|
||||||
|
<td><img src="{{ icons['Psionic Lash (Spectre)'] }}" class="{{ 'acquired' if 'Psionic Lash (Spectre)' in acquired_items }}" title="Psionic Lash (Spectre)" /></td>
|
||||||
|
<td><img src="{{ icons['Nyx-Class Cloaking Module (Spectre)'] }}" class="{{ 'acquired' if 'Nyx-Class Cloaking Module (Spectre)' in acquired_items }}" title="Nyx-Class Cloaking Module (Spectre)" /></td>
|
||||||
|
<td><img src="{{ icons['330mm Barrage Cannon (Thor)'] }}" class="{{ 'acquired' if '330mm Barrage Cannon (Thor)' in acquired_items }}" title="330mm Barrage Cannon (Thor)" /></td>
|
||||||
|
<td><img src="{{ icons['Immortality Protocol (Thor)'] }}" class="{{ 'acquired' if 'Immortality Protocol (Thor)' in acquired_items }}" title="Immortality Protocol (Thor)" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Mercenaries
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['War Pigs'] }}" class="{{ 'acquired' if 'War Pigs' in acquired_items }}" title="War Pigs" /></td>
|
||||||
|
<td><img src="{{ icons['Devil Dogs'] }}" class="{{ 'acquired' if 'Devil Dogs' in acquired_items }}" title="Devil Dogs" /></td>
|
||||||
|
<td><img src="{{ icons['Hammer Securities'] }}" class="{{ 'acquired' if 'Hammer Securities' in acquired_items }}" title="Hammer Securities" /></td>
|
||||||
|
<td><img src="{{ icons['Spartan Company'] }}" class="{{ 'acquired' if 'Spartan Company' in acquired_items }}" title="Spartan Company" /></td>
|
||||||
|
<td><img src="{{ icons['Siege Breakers'] }}" class="{{ 'acquired' if 'Siege Breakers' in acquired_items }}" title="Siege Breakers" /></td>
|
||||||
|
<td><img src="{{ icons['Hel\'s Angel'] }}" class="{{ 'acquired' if 'Hel\'s Angel' in acquired_items }}" title="Hel's Angel" /></td>
|
||||||
|
<td><img src="{{ icons['Dusk Wings'] }}" class="{{ 'acquired' if 'Dusk Wings' in acquired_items }}" title="Dusk Wings" /></td>
|
||||||
|
<td><img src="{{ icons['Jackson\'s Revenge'] }}" class="{{ 'acquired' if 'Jackson\'s Revenge' in acquired_items }}" title="Jackson's Revenge" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Lab Upgrades
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Ultra-Capacitors'] }}" class="{{ 'acquired' if 'Ultra-Capacitors' in acquired_items }}" title="Ultra-Capacitors" /></td>
|
||||||
|
<td><img src="{{ icons['Vanadium Plating'] }}" class="{{ 'acquired' if 'Vanadium Plating' in acquired_items }}" title="Vanadium Plating" /></td>
|
||||||
|
<td><img src="{{ icons['Orbital Depots'] }}" class="{{ 'acquired' if 'Orbital Depots' in acquired_items }}" title="Orbital Depots" /></td>
|
||||||
|
<td><img src="{{ icons['Micro-Filtering'] }}" class="{{ 'acquired' if 'Micro-Filtering' in acquired_items }}" title="Micro-Filtering" /></td>
|
||||||
|
<td><img src="{{ icons['Automated Refinery'] }}" class="{{ 'acquired' if 'Automated Refinery' in acquired_items }}" title="Automated Refinery" /></td>
|
||||||
|
<td><img src="{{ icons['Command Center Reactor'] }}" class="{{ 'acquired' if 'Command Center Reactor' in acquired_items }}" title="Command Center Reactor" /></td>
|
||||||
|
<td><img src="{{ icons['Raven'] }}" class="{{ 'acquired' if 'Raven' in acquired_items }}" title="Raven" /></td>
|
||||||
|
<td><img src="{{ icons['Science Vessel'] }}" class="{{ 'acquired' if 'Science Vessel' in acquired_items }}" title="Science Vessel" /></td>
|
||||||
|
<td><img src="{{ icons['Tech Reactor'] }}" class="{{ 'acquired' if 'Tech Reactor' in acquired_items }}" title="Tech Reactor" /></td>
|
||||||
|
<td><img src="{{ icons['Orbital Strike'] }}" class="{{ 'acquired' if 'Orbital Strike' in acquired_items }}" title="Orbital Strike" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Shrike Turret'] }}" class="{{ 'acquired' if 'Shrike Turret' in acquired_items }}" title="Shrike Turret" /></td>
|
||||||
|
<td><img src="{{ icons['Fortified Bunker'] }}" class="{{ 'acquired' if 'Fortified Bunker' in acquired_items }}" title="Fortified Bunker" /></td>
|
||||||
|
<td><img src="{{ icons['Planetary Fortress'] }}" class="{{ 'acquired' if 'Planetary Fortress' in acquired_items }}" title="Planetary Fortress" /></td>
|
||||||
|
<td><img src="{{ icons['Perdition Turret'] }}" class="{{ 'acquired' if 'Perdition Turret' in acquired_items }}" title="Perdition Turret" /></td>
|
||||||
|
<td><img src="{{ icons['Predator'] }}" class="{{ 'acquired' if 'Predator' in acquired_items }}" title="Predator" /></td>
|
||||||
|
<td><img src="{{ icons['Hercules'] }}" class="{{ 'acquired' if 'Hercules' in acquired_items }}" title="Hercules" /></td>
|
||||||
|
<td><img src="{{ icons['Cellular Reactor'] }}" class="{{ 'acquired' if 'Cellular Reactor' in acquired_items }}" title="Cellular Reactor" /></td>
|
||||||
|
<td><img src="{{ icons['Regenerative Bio-Steel'] }}" class="{{ 'acquired' if 'Regenerative Bio-Steel' in acquired_items }}" title="Regenerative Bio-Steel" /></td>
|
||||||
|
<td><img src="{{ icons['Hive Mind Emulator'] }}" class="{{ 'acquired' if 'Hive Mind Emulator' in acquired_items }}" title="Hive Mind Emulator" /></td>
|
||||||
|
<td><img src="{{ icons['Psi Disrupter'] }}" class="{{ 'acquired' if 'Psi Disrupter' in acquired_items }}" title="Psi Disrupter" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="title">
|
||||||
|
Protoss Units
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ icons['Zealot'] }}" class="{{ 'acquired' if 'Zealot' in acquired_items }}" title="Zealot" /></td>
|
||||||
|
<td><img src="{{ icons['Stalker'] }}" class="{{ 'acquired' if 'Stalker' in acquired_items }}" title="Stalker" /></td>
|
||||||
|
<td><img src="{{ icons['High Templar'] }}" class="{{ 'acquired' if 'High Templar' in acquired_items }}" title="High Templar" /></td>
|
||||||
|
<td><img src="{{ icons['Dark Templar'] }}" class="{{ 'acquired' if 'Dark Templar' in acquired_items }}" title="Dark Templar" /></td>
|
||||||
|
<td><img src="{{ icons['Immortal'] }}" class="{{ 'acquired' if 'Immortal' in acquired_items }}" title="Immortal" /></td>
|
||||||
|
<td><img src="{{ icons['Colossus'] }}" class="{{ 'acquired' if 'Colossus' in acquired_items }}" title="Colossus" /></td>
|
||||||
|
<td><img src="{{ icons['Phoenix'] }}" class="{{ 'acquired' if 'Phoenix' in acquired_items }}" title="Phoenix" /></td>
|
||||||
|
<td><img src="{{ icons['Void Ray'] }}" class="{{ 'acquired' if 'Void Ray' in acquired_items }}" title="Void Ray" /></td>
|
||||||
|
<td><img src="{{ icons['Carrier'] }}" class="{{ 'acquired' if 'Carrier' in acquired_items }}" title="Carrier" /></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table id="location-table">
|
||||||
|
{% for area in checks_in_area %}
|
||||||
|
{% if checks_in_area[area] > 0 %}
|
||||||
|
<tr class="location-category" id="{{area}}-header">
|
||||||
|
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||||
|
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||||
|
</tr>
|
||||||
|
<tbody class="locations hide" id="{{area}}">
|
||||||
|
{% for location in location_info[area] %}
|
||||||
|
<tr>
|
||||||
|
<td class="location-name">{{ location }}</td>
|
||||||
|
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<div id="tables-container">
|
<div id="tables-container">
|
||||||
{% for team, players in inventory.items() %}
|
{% for team, players in inventory.items() %}
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table unique-item-table">
|
<table id="inventory-table" class="table unique-item-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{%- for player, items in players.items() -%}
|
{%- for player, items in players.items() -%}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
|
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||||
{%- if (team, loop.index) in video -%}
|
{%- if (team, loop.index) in video -%}
|
||||||
{%- if video[(team, loop.index)][0] == "Twitch" -%}
|
{%- if video[(team, loop.index)][0] == "Twitch" -%}
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
{% for team, players in checks_done.items() %}
|
{% for team, players in checks_done.items() %}
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table non-unique-item-table">
|
<table id="checks-table" class="table non-unique-item-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th rowspan="2">#</th>
|
<th rowspan="2">#</th>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{%- for player, checks in players.items() -%}
|
{%- for player, checks in players.items() -%}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
|
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||||
{%- for area in ordered_areas -%}
|
{%- for area in ordered_areas -%}
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for team, hints in hints.items() %}
|
{% for team, hints in hints.items() %}
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Finder</th>
|
<th>Finder</th>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from uuid import UUID
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
from MultiServer import Context
|
from MultiServer import Context, get_saving_second
|
||||||
from NetUtils import SlotType
|
from NetUtils import SlotType
|
||||||
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
|
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||||
@@ -280,16 +280,25 @@ def get_static_room_data(room: Room):
|
|||||||
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
|
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
|
||||||
for playernumber in range(1, len(names[0]) + 1)
|
for playernumber in range(1, len(names[0]) + 1)
|
||||||
if playernumber not in groups}
|
if playernumber not in groups}
|
||||||
|
saving_second = get_saving_second(multidata["seed_name"])
|
||||||
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
|
||||||
multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups
|
multidata["precollected_items"], multidata["games"], multidata["slot_data"], groups, saving_second
|
||||||
_multidata_cache[room.seed.id] = result
|
_multidata_cache[room.seed.id] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
||||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
|
||||||
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
|
key = f"{tracker}_{tracked_team}_{tracked_player}_{want_generic}"
|
||||||
|
tracker_page = cache.get(key)
|
||||||
|
if tracker_page:
|
||||||
|
return tracker_page
|
||||||
|
timeout, tracker_page = _get_player_tracker(tracker, tracked_team, tracked_player, want_generic)
|
||||||
|
cache.set(key, tracker_page, timeout)
|
||||||
|
return tracker_page
|
||||||
|
|
||||||
|
|
||||||
|
def _get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool):
|
||||||
# Team and player must be positive and greater than zero
|
# Team and player must be positive and greater than zero
|
||||||
if tracked_team < 0 or tracked_player < 1:
|
if tracked_team < 0 or tracked_player < 1:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -300,7 +309,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
|
|||||||
|
|
||||||
# Collect seed information and pare it down to a single player
|
# Collect seed information and pare it down to a single player
|
||||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||||
precollected_items, games, slot_data, groups = get_static_room_data(room)
|
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
|
||||||
player_name = names[tracked_team][tracked_player - 1]
|
player_name = names[tracked_team][tracked_player - 1]
|
||||||
location_to_area = player_location_to_area[tracked_player]
|
location_to_area = player_location_to_area[tracked_player]
|
||||||
inventory = collections.Counter()
|
inventory = collections.Counter()
|
||||||
@@ -338,21 +347,24 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
|
|||||||
checks_done["Total"] += 1
|
checks_done["Total"] += 1
|
||||||
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
|
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
|
||||||
if specific_tracker and not want_generic:
|
if specific_tracker and not want_generic:
|
||||||
return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
tracker = specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
||||||
seed_checks_in_area, checks_done, slot_data[tracked_player])
|
seed_checks_in_area, checks_done, slot_data[tracked_player], saving_second)
|
||||||
else:
|
else:
|
||||||
return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
tracker = __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
||||||
seed_checks_in_area, checks_done)
|
seed_checks_in_area, checks_done, saving_second)
|
||||||
|
|
||||||
|
return (saving_second - datetime.datetime.now().second) % 60 or 60, tracker
|
||||||
|
|
||||||
|
|
||||||
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
||||||
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||||
return getPlayerTracker(tracker, tracked_team, tracked_player, True)
|
return get_player_tracker(tracker, tracked_team, tracked_player, True)
|
||||||
|
|
||||||
|
|
||||||
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||||
inventory: Counter, team: int, player: int, player_name: str,
|
inventory: Counter, team: int, player: int, player_name: str,
|
||||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
|
||||||
|
saving_second: int) -> str:
|
||||||
|
|
||||||
# Note the presence of the triforce item
|
# Note the presence of the triforce item
|
||||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||||
@@ -414,7 +426,8 @@ def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[
|
|||||||
|
|
||||||
def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
def __renderMinecraftTracker(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,
|
||||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
|
||||||
|
saving_second: int) -> str:
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||||
@@ -516,14 +529,15 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
|
|||||||
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, saving_second = saving_second,
|
||||||
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 __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
def __renderOoTTracker(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,
|
||||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
|
||||||
|
saving_second: int) -> str:
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
|
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
|
||||||
@@ -636,43 +650,47 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
|||||||
|
|
||||||
# Gather dungeon locations
|
# Gather dungeon locations
|
||||||
area_id_ranges = {
|
area_id_ranges = {
|
||||||
"Overworld": (67000, 67280),
|
"Overworld": ((67000, 67258), (67264, 67280), (67747, 68024), (68054, 68062)),
|
||||||
"Deku Tree": (67281, 67303),
|
"Deku Tree": ((67281, 67303), (68063, 68077)),
|
||||||
"Dodongo's Cavern": (67304, 67334),
|
"Dodongo's Cavern": ((67304, 67334), (68078, 68160)),
|
||||||
"Jabu Jabu's Belly": (67335, 67359),
|
"Jabu Jabu's Belly": ((67335, 67359), (68161, 68188)),
|
||||||
"Bottom of the Well": (67360, 67384),
|
"Bottom of the Well": ((67360, 67384), (68189, 68230)),
|
||||||
"Forest Temple": (67385, 67420),
|
"Forest Temple": ((67385, 67420), (68231, 68281)),
|
||||||
"Fire Temple": (67421, 67457),
|
"Fire Temple": ((67421, 67457), (68282, 68350)),
|
||||||
"Water Temple": (67458, 67484),
|
"Water Temple": ((67458, 67484), (68351, 68483)),
|
||||||
"Shadow Temple": (67485, 67532),
|
"Shadow Temple": ((67485, 67532), (68484, 68565)),
|
||||||
"Spirit Temple": (67533, 67582),
|
"Spirit Temple": ((67533, 67582), (68566, 68625)),
|
||||||
"Ice Cavern": (67583, 67596),
|
"Ice Cavern": ((67583, 67596), (68626, 68649)),
|
||||||
"Gerudo Training Ground": (67597, 67635),
|
"Gerudo Training Ground": ((67597, 67635), (68650, 68656)),
|
||||||
"Thieves' Hideout": (67259, 67263),
|
"Thieves' Hideout": ((67259, 67263), (68025, 68053)),
|
||||||
"Ganon's Castle": (67636, 67673),
|
"Ganon's Castle": ((67636, 67673), (68657, 68705)),
|
||||||
}
|
}
|
||||||
|
|
||||||
def lookup_and_trim(id, area):
|
def lookup_and_trim(id, area):
|
||||||
full_name = lookup_any_location_id_to_name[id]
|
full_name = lookup_any_location_id_to_name[id]
|
||||||
if id == 67673:
|
if 'Ganons Tower' in full_name:
|
||||||
return full_name[13:] # Ganons Tower Boss Key Chest
|
return full_name
|
||||||
if area not in ["Overworld", "Thieves' Hideout"]:
|
if area not in ["Overworld", "Thieves' Hideout"]:
|
||||||
# trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
|
# trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
|
||||||
return full_name[len(area):]
|
return full_name[len(area):]
|
||||||
return full_name
|
return full_name
|
||||||
|
|
||||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player]))
|
checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player]))
|
||||||
location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[player]}
|
location_info = {}
|
||||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
checks_done = {}
|
||||||
checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges}
|
checks_in_area = {}
|
||||||
checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[player]])
|
for area, ranges in area_id_ranges.items():
|
||||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
location_info[area] = {}
|
||||||
|
checks_done[area] = 0
|
||||||
# Remove Thieves' Hideout checks from Overworld, since it's in the middle of the range
|
checks_in_area[area] = 0
|
||||||
checks_in_area["Overworld"] -= checks_in_area["Thieves' Hideout"]
|
for r in ranges:
|
||||||
checks_done["Overworld"] -= checks_done["Thieves' Hideout"]
|
min_id, max_id = r
|
||||||
for loc in location_info["Thieves' Hideout"]:
|
for id in range(min_id, max_id+1):
|
||||||
del location_info["Overworld"][loc]
|
if id in locations[player]:
|
||||||
|
checked = id in checked_locations
|
||||||
|
location_info[area][lookup_and_trim(id, area)] = checked
|
||||||
|
checks_in_area[area] += 1
|
||||||
|
checks_done[area] += checked
|
||||||
|
|
||||||
checks_done['Total'] = sum(checks_done.values())
|
checks_done['Total'] = sum(checks_done.values())
|
||||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||||
@@ -683,25 +701,28 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
|||||||
if "GS" in lookup_and_trim(id, ''):
|
if "GS" in lookup_and_trim(id, ''):
|
||||||
display_data["token_count"] += 1
|
display_data["token_count"] += 1
|
||||||
|
|
||||||
|
oot_y = '✔'
|
||||||
|
oot_x = '✕'
|
||||||
|
|
||||||
# Gather small and boss key info
|
# Gather small and boss key info
|
||||||
small_key_counts = {
|
small_key_counts = {
|
||||||
"Forest Temple": inventory[66175],
|
"Forest Temple": oot_y if inventory[66203] else inventory[66175],
|
||||||
"Fire Temple": inventory[66176],
|
"Fire Temple": oot_y if inventory[66204] else inventory[66176],
|
||||||
"Water Temple": inventory[66177],
|
"Water Temple": oot_y if inventory[66205] else inventory[66177],
|
||||||
"Spirit Temple": inventory[66178],
|
"Spirit Temple": oot_y if inventory[66206] else inventory[66178],
|
||||||
"Shadow Temple": inventory[66179],
|
"Shadow Temple": oot_y if inventory[66207] else inventory[66179],
|
||||||
"Bottom of the Well": inventory[66180],
|
"Bottom of the Well": oot_y if inventory[66208] else inventory[66180],
|
||||||
"Gerudo Training Ground": inventory[66181],
|
"Gerudo Training Ground": oot_y if inventory[66209] else inventory[66181],
|
||||||
"Thieves' Hideout": inventory[66182],
|
"Thieves' Hideout": oot_y if inventory[66210] else inventory[66182],
|
||||||
"Ganon's Castle": inventory[66183],
|
"Ganon's Castle": oot_y if inventory[66211] else inventory[66183],
|
||||||
}
|
}
|
||||||
boss_key_counts = {
|
boss_key_counts = {
|
||||||
"Forest Temple": '✔' if inventory[66149] else '✕',
|
"Forest Temple": oot_y if inventory[66149] else oot_x,
|
||||||
"Fire Temple": '✔' if inventory[66150] else '✕',
|
"Fire Temple": oot_y if inventory[66150] else oot_x,
|
||||||
"Water Temple": '✔' if inventory[66151] else '✕',
|
"Water Temple": oot_y if inventory[66151] else oot_x,
|
||||||
"Spirit Temple": '✔' if inventory[66152] else '✕',
|
"Spirit Temple": oot_y if inventory[66152] else oot_x,
|
||||||
"Shadow Temple": '✔' if inventory[66153] else '✕',
|
"Shadow Temple": oot_y if inventory[66153] else oot_x,
|
||||||
"Ganon's Castle": '✔' if inventory[66154] else '✕',
|
"Ganon's Castle": oot_y if inventory[66154] else oot_x,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Victory condition
|
# Victory condition
|
||||||
@@ -718,7 +739,8 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
|
|||||||
|
|
||||||
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
def __renderTimespinnerTracker(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,
|
||||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict[str, Any]) -> str:
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
|
||||||
|
slot_data: Dict[str, Any], saving_second: int) -> str:
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png",
|
"Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png",
|
||||||
@@ -824,7 +846,8 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
|||||||
|
|
||||||
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
def __renderSuperMetroidTracker(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,
|
||||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict,
|
||||||
|
saving_second: int) -> str:
|
||||||
|
|
||||||
icons = {
|
icons = {
|
||||||
"Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png",
|
"Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/tracker/inventory/ETank.png",
|
||||||
@@ -922,37 +945,285 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
|
|||||||
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 __renderSC2WoLTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||||
|
inventory: Counter, team: int, player: int, playerName: str,
|
||||||
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
|
||||||
|
slot_data: Dict, saving_second: int) -> str:
|
||||||
|
|
||||||
|
SC2WOL_LOC_ID_OFFSET = 1000
|
||||||
|
SC2WOL_ITEM_ID_OFFSET = 1000
|
||||||
|
|
||||||
|
icons = {
|
||||||
|
"Starting Minerals": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-mineral-protoss.png",
|
||||||
|
"Starting Vespene": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/icons/icon-gas-terran.png",
|
||||||
|
"Starting Supply": "https://static.wikia.nocookie.net/starcraft/images/d/d3/TerranSupply_SC2_Icon1.gif",
|
||||||
|
|
||||||
|
"Infantry Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel1.png",
|
||||||
|
"Infantry Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel2.png",
|
||||||
|
"Infantry Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryweaponslevel3.png",
|
||||||
|
"Infantry Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel1.png",
|
||||||
|
"Infantry Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel2.png",
|
||||||
|
"Infantry Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-infantryarmorlevel3.png",
|
||||||
|
"Vehicle Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel1.png",
|
||||||
|
"Vehicle Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel2.png",
|
||||||
|
"Vehicle Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleweaponslevel3.png",
|
||||||
|
"Vehicle Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel1.png",
|
||||||
|
"Vehicle Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel2.png",
|
||||||
|
"Vehicle Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-vehicleplatinglevel3.png",
|
||||||
|
"Ship Weapons Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel1.png",
|
||||||
|
"Ship Weapons Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel2.png",
|
||||||
|
"Ship Weapons Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipweaponslevel3.png",
|
||||||
|
"Ship Armor Level 1": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel1.png",
|
||||||
|
"Ship Armor Level 2": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel2.png",
|
||||||
|
"Ship Armor Level 3": "https://sclegacy.com/images/uploaded/starcraftii_beta/gamefiles/upgrades/btn-upgrade-terran-shipplatinglevel3.png",
|
||||||
|
|
||||||
|
"Bunker": "https://static.wikia.nocookie.net/starcraft/images/c/c5/Bunker_SC2_Icon1.jpg",
|
||||||
|
"Missile Turret": "https://static.wikia.nocookie.net/starcraft/images/5/5f/MissileTurret_SC2_Icon1.jpg",
|
||||||
|
"Sensor Tower": "https://static.wikia.nocookie.net/starcraft/images/d/d2/SensorTower_SC2_Icon1.jpg",
|
||||||
|
|
||||||
|
"Projectile Accelerator (Bunker)": "https://0rganics.org/archipelago/sc2wol/ProjectileAccelerator.png",
|
||||||
|
"Neosteel Bunker (Bunker)": "https://0rganics.org/archipelago/sc2wol/NeosteelBunker.png",
|
||||||
|
"Titanium Housing (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/TitaniumHousing.png",
|
||||||
|
"Hellstorm Batteries (Missile Turret)": "https://0rganics.org/archipelago/sc2wol/HellstormBatteries.png",
|
||||||
|
"Advanced Construction (SCV)": "https://0rganics.org/archipelago/sc2wol/AdvancedConstruction.png",
|
||||||
|
"Dual-Fusion Welders (SCV)": "https://0rganics.org/archipelago/sc2wol/Dual-FusionWelders.png",
|
||||||
|
"Fire-Suppression System (Building)": "https://0rganics.org/archipelago/sc2wol/Fire-SuppressionSystem.png",
|
||||||
|
"Orbital Command (Building)": "https://0rganics.org/archipelago/sc2wol/OrbitalCommandCampaign.png",
|
||||||
|
|
||||||
|
"Marine": "https://static.wikia.nocookie.net/starcraft/images/4/47/Marine_SC2_Icon1.jpg",
|
||||||
|
"Medic": "https://static.wikia.nocookie.net/starcraft/images/7/74/Medic_SC2_Rend1.jpg",
|
||||||
|
"Firebat": "https://static.wikia.nocookie.net/starcraft/images/3/3c/Firebat_SC2_Rend1.jpg",
|
||||||
|
"Marauder": "https://static.wikia.nocookie.net/starcraft/images/b/ba/Marauder_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",
|
||||||
|
"Combat Shield (Marine)": "https://0rganics.org/archipelago/sc2wol/CombatShieldCampaign.png",
|
||||||
|
"Advanced Medic Facilities (Medic)": "https://0rganics.org/archipelago/sc2wol/AdvancedMedicFacilities.png",
|
||||||
|
"Stabilizer Medpacks (Medic)": "https://0rganics.org/archipelago/sc2wol/StabilizerMedpacks.png",
|
||||||
|
"Incinerator Gauntlets (Firebat)": "https://0rganics.org/archipelago/sc2wol/IncineratorGauntlets.png",
|
||||||
|
"Juggernaut Plating (Firebat)": "https://0rganics.org/archipelago/sc2wol/JuggernautPlating.png",
|
||||||
|
"Concussive Shells (Marauder)": "https://0rganics.org/archipelago/sc2wol/ConcussiveShellsCampaign.png",
|
||||||
|
"Kinetic Foam (Marauder)": "https://0rganics.org/archipelago/sc2wol/KineticFoam.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",
|
||||||
|
|
||||||
|
"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",
|
||||||
|
"Goliath": "https://static.wikia.nocookie.net/starcraft/images/e/eb/Goliath_WoL.jpg",
|
||||||
|
"Diamondback": "https://static.wikia.nocookie.net/starcraft/images/a/a6/Diamondback_WoL.jpg",
|
||||||
|
"Siege Tank": "https://static.wikia.nocookie.net/starcraft/images/5/57/SiegeTank_SC2_Icon1.jpg",
|
||||||
|
|
||||||
|
"Twin-Linked Flamethrower (Hellion)": "https://0rganics.org/archipelago/sc2wol/Twin-LinkedFlamethrower.png",
|
||||||
|
"Thermite Filaments (Hellion)": "https://0rganics.org/archipelago/sc2wol/ThermiteFilaments.png",
|
||||||
|
"Cerberus Mine (Vulture)": "https://0rganics.org/archipelago/sc2wol/CerberusMine.png",
|
||||||
|
"Replenishable Magazine (Vulture)": "https://0rganics.org/archipelago/sc2wol/ReplenishableMagazine.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",
|
||||||
|
"Tri-Lithium Power Cell (Diamondback)": "https://0rganics.org/archipelago/sc2wol/Tri-LithiumPowerCell.png",
|
||||||
|
"Shaped Hull (Diamondback)": "https://0rganics.org/archipelago/sc2wol/ShapedHull.png",
|
||||||
|
"Maelstrom Rounds (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/MaelstromRounds.png",
|
||||||
|
"Shaped Blast (Siege Tank)": "https://0rganics.org/archipelago/sc2wol/ShapedBlast.png",
|
||||||
|
|
||||||
|
"Medivac": "https://static.wikia.nocookie.net/starcraft/images/d/db/Medivac_SC2_Icon1.jpg",
|
||||||
|
"Wraith": "https://static.wikia.nocookie.net/starcraft/images/7/75/Wraith_WoL.jpg",
|
||||||
|
"Viking": "https://static.wikia.nocookie.net/starcraft/images/2/2a/Viking_SC2_Icon1.jpg",
|
||||||
|
"Banshee": "https://static.wikia.nocookie.net/starcraft/images/3/32/Banshee_SC2_Icon1.jpg",
|
||||||
|
"Battlecruiser": "https://static.wikia.nocookie.net/starcraft/images/f/f5/Battlecruiser_SC2_Icon1.jpg",
|
||||||
|
|
||||||
|
"Rapid Deployment Tube (Medivac)": "https://0rganics.org/archipelago/sc2wol/RapidDeploymentTube.png",
|
||||||
|
"Advanced Healing AI (Medivac)": "https://0rganics.org/archipelago/sc2wol/AdvancedHealingAI.png",
|
||||||
|
"Tomahawk Power Cells (Wraith)": "https://0rganics.org/archipelago/sc2wol/TomahawkPowerCells.png",
|
||||||
|
"Displacement Field (Wraith)": "https://0rganics.org/archipelago/sc2wol/DisplacementField.png",
|
||||||
|
"Ripwave Missiles (Viking)": "https://0rganics.org/archipelago/sc2wol/RipwaveMissiles.png",
|
||||||
|
"Phobos-Class Weapons System (Viking)": "https://0rganics.org/archipelago/sc2wol/Phobos-ClassWeaponsSystem.png",
|
||||||
|
"Cross-Spectrum Dampeners (Banshee)": "https://0rganics.org/archipelago/sc2wol/Cross-SpectrumDampeners.png",
|
||||||
|
"Shockwave Missile Battery (Banshee)": "https://0rganics.org/archipelago/sc2wol/ShockwaveMissileBattery.png",
|
||||||
|
"Missile Pods (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/MissilePods.png",
|
||||||
|
"Defensive Matrix (Battlecruiser)": "https://0rganics.org/archipelago/sc2wol/DefensiveMatrix.png",
|
||||||
|
|
||||||
|
"Ghost": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Ghost_SC2_Icon1.jpg",
|
||||||
|
"Spectre": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Spectre_WoL.jpg",
|
||||||
|
"Thor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/Thor_SC2_Icon1.jpg",
|
||||||
|
|
||||||
|
"Ocular Implants (Ghost)": "https://0rganics.org/archipelago/sc2wol/OcularImplants.png",
|
||||||
|
"Crius Suit (Ghost)": "https://0rganics.org/archipelago/sc2wol/CriusSuit.png",
|
||||||
|
"Psionic Lash (Spectre)": "https://0rganics.org/archipelago/sc2wol/PsionicLash.png",
|
||||||
|
"Nyx-Class Cloaking Module (Spectre)": "https://0rganics.org/archipelago/sc2wol/Nyx-ClassCloakingModule.png",
|
||||||
|
"330mm Barrage Cannon (Thor)": "https://0rganics.org/archipelago/sc2wol/330mmBarrageCannon.png",
|
||||||
|
"Immortality Protocol (Thor)": "https://0rganics.org/archipelago/sc2wol/ImmortalityProtocol.png",
|
||||||
|
|
||||||
|
"War Pigs": "https://static.wikia.nocookie.net/starcraft/images/e/ed/WarPigs_SC2_Icon1.jpg",
|
||||||
|
"Devil Dogs": "https://static.wikia.nocookie.net/starcraft/images/3/33/DevilDogs_SC2_Icon1.jpg",
|
||||||
|
"Hammer Securities": "https://static.wikia.nocookie.net/starcraft/images/3/3b/HammerSecurity_SC2_Icon1.jpg",
|
||||||
|
"Spartan Company": "https://static.wikia.nocookie.net/starcraft/images/b/be/SpartanCompany_SC2_Icon1.jpg",
|
||||||
|
"Siege Breakers": "https://static.wikia.nocookie.net/starcraft/images/3/31/SiegeBreakers_SC2_Icon1.jpg",
|
||||||
|
"Hel's Angel": "https://static.wikia.nocookie.net/starcraft/images/6/63/HelsAngels_SC2_Icon1.jpg",
|
||||||
|
"Dusk Wings": "https://static.wikia.nocookie.net/starcraft/images/5/52/DuskWings_SC2_Icon1.jpg",
|
||||||
|
"Jackson's Revenge": "https://static.wikia.nocookie.net/starcraft/images/9/95/JacksonsRevenge_SC2_Icon1.jpg",
|
||||||
|
|
||||||
|
"Ultra-Capacitors": "https://static.wikia.nocookie.net/starcraft/images/2/23/SC2_Lab_Ultra_Capacitors_Icon.png",
|
||||||
|
"Vanadium Plating": "https://static.wikia.nocookie.net/starcraft/images/6/67/SC2_Lab_VanPlating_Icon.png",
|
||||||
|
"Orbital Depots": "https://static.wikia.nocookie.net/starcraft/images/0/01/SC2_Lab_Orbital_Depot_Icon.png",
|
||||||
|
"Micro-Filtering": "https://static.wikia.nocookie.net/starcraft/images/2/20/SC2_Lab_MicroFilter_Icon.png",
|
||||||
|
"Automated Refinery": "https://static.wikia.nocookie.net/starcraft/images/7/71/SC2_Lab_Auto_Refinery_Icon.png",
|
||||||
|
"Command Center Reactor": "https://static.wikia.nocookie.net/starcraft/images/e/ef/SC2_Lab_CC_Reactor_Icon.png",
|
||||||
|
"Raven": "https://static.wikia.nocookie.net/starcraft/images/1/19/SC2_Lab_Raven_Icon.png",
|
||||||
|
"Science Vessel": "https://static.wikia.nocookie.net/starcraft/images/c/c3/SC2_Lab_SciVes_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",
|
||||||
|
|
||||||
|
"Shrike Turret": "https://static.wikia.nocookie.net/starcraft/images/4/44/SC2_Lab_Shrike_Turret_Icon.png",
|
||||||
|
"Fortified Bunker": "https://static.wikia.nocookie.net/starcraft/images/4/4f/SC2_Lab_FortBunker_Icon.png",
|
||||||
|
"Planetary Fortress": "https://static.wikia.nocookie.net/starcraft/images/0/0b/SC2_Lab_PlanetFortress_Icon.png",
|
||||||
|
"Perdition Turret": "https://static.wikia.nocookie.net/starcraft/images/a/af/SC2_Lab_PerdTurret_Icon.png",
|
||||||
|
"Predator": "https://static.wikia.nocookie.net/starcraft/images/8/83/SC2_Lab_Predator_Icon.png",
|
||||||
|
"Hercules": "https://static.wikia.nocookie.net/starcraft/images/4/40/SC2_Lab_Hercules_Icon.png",
|
||||||
|
"Cellular Reactor": "https://static.wikia.nocookie.net/starcraft/images/d/d8/SC2_Lab_CellReactor_Icon.png",
|
||||||
|
"Regenerative Bio-Steel": "https://static.wikia.nocookie.net/starcraft/images/d/d3/SC2_Lab_BioSteel_Icon.png",
|
||||||
|
"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",
|
||||||
|
|
||||||
|
"Zealot": "https://static.wikia.nocookie.net/starcraft/images/6/6e/Icon_Protoss_Zealot.jpg",
|
||||||
|
"Stalker": "https://static.wikia.nocookie.net/starcraft/images/0/0d/Icon_Protoss_Stalker.jpg",
|
||||||
|
"High Templar": "https://static.wikia.nocookie.net/starcraft/images/a/a0/Icon_Protoss_High_Templar.jpg",
|
||||||
|
"Dark Templar": "https://static.wikia.nocookie.net/starcraft/images/9/90/Icon_Protoss_Dark_Templar.jpg",
|
||||||
|
"Immortal": "https://static.wikia.nocookie.net/starcraft/images/c/c1/Icon_Protoss_Immortal.jpg",
|
||||||
|
"Colossus": "https://static.wikia.nocookie.net/starcraft/images/4/40/Icon_Protoss_Colossus.jpg",
|
||||||
|
"Phoenix": "https://static.wikia.nocookie.net/starcraft/images/b/b1/Icon_Protoss_Phoenix.jpg",
|
||||||
|
"Void Ray": "https://static.wikia.nocookie.net/starcraft/images/1/1d/VoidRay_SC2_Rend1.jpg",
|
||||||
|
"Carrier": "https://static.wikia.nocookie.net/starcraft/images/2/2c/Icon_Protoss_Carrier.jpg",
|
||||||
|
|
||||||
|
"Nothing": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
sc2wol_location_ids = {
|
||||||
|
"Liberation Day": [SC2WOL_LOC_ID_OFFSET + 100, SC2WOL_LOC_ID_OFFSET + 101, SC2WOL_LOC_ID_OFFSET + 102, SC2WOL_LOC_ID_OFFSET + 103, SC2WOL_LOC_ID_OFFSET + 104, SC2WOL_LOC_ID_OFFSET + 105, SC2WOL_LOC_ID_OFFSET + 106],
|
||||||
|
"The Outlaws": [SC2WOL_LOC_ID_OFFSET + 200, SC2WOL_LOC_ID_OFFSET + 201],
|
||||||
|
"Zero Hour": [SC2WOL_LOC_ID_OFFSET + 300, SC2WOL_LOC_ID_OFFSET + 301, SC2WOL_LOC_ID_OFFSET + 302, SC2WOL_LOC_ID_OFFSET + 303],
|
||||||
|
"Evacuation": [SC2WOL_LOC_ID_OFFSET + 400, SC2WOL_LOC_ID_OFFSET + 401, SC2WOL_LOC_ID_OFFSET + 402, SC2WOL_LOC_ID_OFFSET + 403],
|
||||||
|
"Outbreak": [SC2WOL_LOC_ID_OFFSET + 500, SC2WOL_LOC_ID_OFFSET + 501, SC2WOL_LOC_ID_OFFSET + 502],
|
||||||
|
"Safe Haven": [SC2WOL_LOC_ID_OFFSET + 600, SC2WOL_LOC_ID_OFFSET + 601, SC2WOL_LOC_ID_OFFSET + 602, SC2WOL_LOC_ID_OFFSET + 603],
|
||||||
|
"Haven's Fall": [SC2WOL_LOC_ID_OFFSET + 700, SC2WOL_LOC_ID_OFFSET + 701, SC2WOL_LOC_ID_OFFSET + 702, SC2WOL_LOC_ID_OFFSET + 703],
|
||||||
|
"Smash and Grab": [SC2WOL_LOC_ID_OFFSET + 800, SC2WOL_LOC_ID_OFFSET + 801, SC2WOL_LOC_ID_OFFSET + 802, SC2WOL_LOC_ID_OFFSET + 803, SC2WOL_LOC_ID_OFFSET + 804],
|
||||||
|
"The Dig": [SC2WOL_LOC_ID_OFFSET + 900, SC2WOL_LOC_ID_OFFSET + 901, SC2WOL_LOC_ID_OFFSET + 902, SC2WOL_LOC_ID_OFFSET + 903],
|
||||||
|
"The Moebius Factor": [SC2WOL_LOC_ID_OFFSET + 1000, SC2WOL_LOC_ID_OFFSET + 1003, SC2WOL_LOC_ID_OFFSET + 1004, SC2WOL_LOC_ID_OFFSET + 1005, SC2WOL_LOC_ID_OFFSET + 1006, SC2WOL_LOC_ID_OFFSET + 1007, SC2WOL_LOC_ID_OFFSET + 1008],
|
||||||
|
"Supernova": [SC2WOL_LOC_ID_OFFSET + 1100, SC2WOL_LOC_ID_OFFSET + 1101, SC2WOL_LOC_ID_OFFSET + 1102, SC2WOL_LOC_ID_OFFSET + 1103, SC2WOL_LOC_ID_OFFSET + 1104],
|
||||||
|
"Maw of the Void": [SC2WOL_LOC_ID_OFFSET + 1200, SC2WOL_LOC_ID_OFFSET + 1201, SC2WOL_LOC_ID_OFFSET + 1202, SC2WOL_LOC_ID_OFFSET + 1203, SC2WOL_LOC_ID_OFFSET + 1204, SC2WOL_LOC_ID_OFFSET + 1205],
|
||||||
|
"Devil's Playground": [SC2WOL_LOC_ID_OFFSET + 1300, SC2WOL_LOC_ID_OFFSET + 1301, SC2WOL_LOC_ID_OFFSET + 1302],
|
||||||
|
"Welcome to the Jungle": [SC2WOL_LOC_ID_OFFSET + 1400, SC2WOL_LOC_ID_OFFSET + 1401, SC2WOL_LOC_ID_OFFSET + 1402, SC2WOL_LOC_ID_OFFSET + 1403],
|
||||||
|
"Breakout": [SC2WOL_LOC_ID_OFFSET + 1500, SC2WOL_LOC_ID_OFFSET + 1501, SC2WOL_LOC_ID_OFFSET + 1502],
|
||||||
|
"Ghost of a Chance": [SC2WOL_LOC_ID_OFFSET + 1600, SC2WOL_LOC_ID_OFFSET + 1601, SC2WOL_LOC_ID_OFFSET + 1602, SC2WOL_LOC_ID_OFFSET + 1603, SC2WOL_LOC_ID_OFFSET + 1604, SC2WOL_LOC_ID_OFFSET + 1605],
|
||||||
|
"The Great Train Robbery": [SC2WOL_LOC_ID_OFFSET + 1700, SC2WOL_LOC_ID_OFFSET + 1701, SC2WOL_LOC_ID_OFFSET + 1702, SC2WOL_LOC_ID_OFFSET + 1703],
|
||||||
|
"Cutthroat": [SC2WOL_LOC_ID_OFFSET + 1800, SC2WOL_LOC_ID_OFFSET + 1801, SC2WOL_LOC_ID_OFFSET + 1802, SC2WOL_LOC_ID_OFFSET + 1803, SC2WOL_LOC_ID_OFFSET + 1804],
|
||||||
|
"Engine of Destruction": [SC2WOL_LOC_ID_OFFSET + 1900, SC2WOL_LOC_ID_OFFSET + 1901, SC2WOL_LOC_ID_OFFSET + 1902, SC2WOL_LOC_ID_OFFSET + 1903, SC2WOL_LOC_ID_OFFSET + 1904, SC2WOL_LOC_ID_OFFSET + 1905],
|
||||||
|
"Media Blitz": [SC2WOL_LOC_ID_OFFSET + 2000, SC2WOL_LOC_ID_OFFSET + 2001, SC2WOL_LOC_ID_OFFSET + 2002, SC2WOL_LOC_ID_OFFSET + 2003, SC2WOL_LOC_ID_OFFSET + 2004],
|
||||||
|
"Piercing the Shroud": [SC2WOL_LOC_ID_OFFSET + 2100, SC2WOL_LOC_ID_OFFSET + 2101, SC2WOL_LOC_ID_OFFSET + 2102, SC2WOL_LOC_ID_OFFSET + 2103, SC2WOL_LOC_ID_OFFSET + 2104, SC2WOL_LOC_ID_OFFSET + 2105],
|
||||||
|
"Whispers of Doom": [SC2WOL_LOC_ID_OFFSET + 2200, SC2WOL_LOC_ID_OFFSET + 2201, SC2WOL_LOC_ID_OFFSET + 2202, SC2WOL_LOC_ID_OFFSET + 2203],
|
||||||
|
"A Sinister Turn": [SC2WOL_LOC_ID_OFFSET + 2300, SC2WOL_LOC_ID_OFFSET + 2301, SC2WOL_LOC_ID_OFFSET + 2302, SC2WOL_LOC_ID_OFFSET + 2303],
|
||||||
|
"Echoes of the Future": [SC2WOL_LOC_ID_OFFSET + 2400, SC2WOL_LOC_ID_OFFSET + 2401, SC2WOL_LOC_ID_OFFSET + 2402],
|
||||||
|
"In Utter Darkness": [SC2WOL_LOC_ID_OFFSET + 2500, SC2WOL_LOC_ID_OFFSET + 2501, SC2WOL_LOC_ID_OFFSET + 2502],
|
||||||
|
"Gates of Hell": [SC2WOL_LOC_ID_OFFSET + 2600, SC2WOL_LOC_ID_OFFSET + 2601],
|
||||||
|
"Belly of the Beast": [SC2WOL_LOC_ID_OFFSET + 2700, SC2WOL_LOC_ID_OFFSET + 2701, SC2WOL_LOC_ID_OFFSET + 2702, SC2WOL_LOC_ID_OFFSET + 2703],
|
||||||
|
"Shatter the Sky": [SC2WOL_LOC_ID_OFFSET + 2800, SC2WOL_LOC_ID_OFFSET + 2801, SC2WOL_LOC_ID_OFFSET + 2802, SC2WOL_LOC_ID_OFFSET + 2803, SC2WOL_LOC_ID_OFFSET + 2804, SC2WOL_LOC_ID_OFFSET + 2805],
|
||||||
|
}
|
||||||
|
|
||||||
|
display_data = {}
|
||||||
|
|
||||||
|
# Determine display for progressive items
|
||||||
|
progressive_items = {
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
progressive_names = {
|
||||||
|
"Progressive Infantry Weapon": ["Infantry Weapons Level 1", "Infantry Weapons Level 1", "Infantry Weapons Level 2", "Infantry Weapons Level 3"],
|
||||||
|
"Progressive Infantry Armor": ["Infantry Armor Level 1", "Infantry Armor Level 1", "Infantry Armor Level 2", "Infantry Armor 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 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"]
|
||||||
|
}
|
||||||
|
for item_name, item_id in progressive_items.items():
|
||||||
|
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||||
|
display_name = progressive_names[item_name][level]
|
||||||
|
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||||
|
display_data[base_name + "_level"] = level
|
||||||
|
display_data[base_name + "_url"] = icons[display_name]
|
||||||
|
|
||||||
|
# Multi-items
|
||||||
|
multi_items = {
|
||||||
|
"+15 Starting Minerals": 800 + SC2WOL_ITEM_ID_OFFSET,
|
||||||
|
"+15 Starting Vespene": 801 + SC2WOL_ITEM_ID_OFFSET,
|
||||||
|
"+2 Starting Supply": 802 + SC2WOL_ITEM_ID_OFFSET
|
||||||
|
}
|
||||||
|
for item_name, item_id in multi_items.items():
|
||||||
|
base_name = item_name.split()[-1].lower()
|
||||||
|
count = inventory[item_id]
|
||||||
|
if base_name == "supply":
|
||||||
|
count = count * 2
|
||||||
|
display_data[base_name + "_count"] = count
|
||||||
|
else:
|
||||||
|
count = count * 15
|
||||||
|
display_data[base_name + "_count"] = count
|
||||||
|
|
||||||
|
# Victory condition
|
||||||
|
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||||
|
display_data['game_finished'] = game_state == 30
|
||||||
|
|
||||||
|
# Turn location IDs into mission objective counts
|
||||||
|
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||||
|
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||||
|
location_info = {mission_name: {lookup_name(id): (id in checked_locations) for id in mission_locations if id in set(locations[player])} for mission_name, mission_locations in sc2wol_location_ids.items()}
|
||||||
|
checks_done = {mission_name: len([id for id in mission_locations if id in checked_locations and id in set(locations[player])]) for mission_name, mission_locations in sc2wol_location_ids.items()}
|
||||||
|
checks_done['Total'] = len(checked_locations)
|
||||||
|
checks_in_area = {mission_name: len([id for id in mission_locations if id in set(locations[player])]) for mission_name, mission_locations in sc2wol_location_ids.items()}
|
||||||
|
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||||
|
|
||||||
|
return render_template("sc2wolTracker.html",
|
||||||
|
inventory=inventory, icons=icons,
|
||||||
|
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||||
|
id in lookup_any_item_id_to_name},
|
||||||
|
player=player, team=team, room=room, player_name=playerName,
|
||||||
|
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||||
|
**display_data)
|
||||||
|
|
||||||
|
|
||||||
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
def __renderGenericTracker(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,
|
||||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
|
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int],
|
||||||
|
saving_second: int) -> str:
|
||||||
|
|
||||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||||
player_received_items = {}
|
player_received_items = {}
|
||||||
if multisave.get('version', 0) > 0:
|
if multisave.get('version', 0) > 0:
|
||||||
# add numbering to all items but starter_inventory
|
|
||||||
ordered_items = multisave.get('received_items', {}).get((team, player, True), [])
|
ordered_items = multisave.get('received_items', {}).get((team, player, True), [])
|
||||||
else:
|
else:
|
||||||
ordered_items = multisave.get('received_items', {}).get((team, player), [])
|
ordered_items = multisave.get('received_items', {}).get((team, player), [])
|
||||||
|
|
||||||
|
# add numbering to all items but starter_inventory
|
||||||
for order_index, networkItem in enumerate(ordered_items, start=1):
|
for order_index, networkItem in enumerate(ordered_items, start=1):
|
||||||
player_received_items[networkItem.item] = order_index
|
player_received_items[networkItem.item] = order_index
|
||||||
|
|
||||||
return render_template("genericTracker.html",
|
return render_template("genericTracker.html",
|
||||||
inventory=inventory,
|
inventory=inventory,
|
||||||
player=player, team=team, room=room, player_name=playerName,
|
player=player, team=team, room=room, player_name=playerName,
|
||||||
checked_locations=checked_locations,
|
checked_locations=checked_locations,
|
||||||
not_checked_locations=set(locations[player]) - checked_locations,
|
not_checked_locations=set(locations[player]) - checked_locations,
|
||||||
received_items=player_received_items)
|
received_items=player_received_items,
|
||||||
|
saving_second=saving_second)
|
||||||
|
|
||||||
|
|
||||||
@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=1) # multisave is currently created at most every minute
|
||||||
def getTracker(tracker: UUID):
|
def getTracker(tracker: UUID):
|
||||||
room: Room = Room.get(tracker=tracker)
|
room: Room = Room.get(tracker=tracker)
|
||||||
if not room:
|
if not room:
|
||||||
abort(404)
|
abort(404)
|
||||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||||
precollected_items, games, slot_data, groups = get_static_room_data(room)
|
precollected_items, games, slot_data, groups, saving_second = get_static_room_data(room)
|
||||||
|
|
||||||
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1) if playernumber not in groups}
|
inventory = {teamnumber: {playernumber: collections.Counter() 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)}
|
||||||
@@ -1044,5 +1315,6 @@ game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
|||||||
"Ocarina of Time": __renderOoTTracker,
|
"Ocarina of Time": __renderOoTTracker,
|
||||||
"Timespinner": __renderTimespinnerTracker,
|
"Timespinner": __renderTimespinnerTracker,
|
||||||
"A Link to the Past": __renderAlttpTracker,
|
"A Link to the Past": __renderAlttpTracker,
|
||||||
"Super Metroid": __renderSuperMetroidTracker
|
"Super Metroid": __renderSuperMetroidTracker,
|
||||||
|
"Starcraft 2 Wings of Liberty": __renderSC2WoLTracker
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import uuid
|
|||||||
import zipfile
|
import zipfile
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from flask import request, flash, redirect, url_for, session, render_template
|
from flask import request, flash, redirect, url_for, session, render_template, Markup
|
||||||
from pony.orm import flush, select
|
from pony.orm import flush, select
|
||||||
|
|
||||||
import MultiServer
|
import MultiServer
|
||||||
@@ -15,68 +15,41 @@ from worlds.Files import AutoPatchRegister
|
|||||||
from . import app
|
from . import app
|
||||||
from .models import Seed, Room, Slot
|
from .models import Seed, Room, Slot
|
||||||
|
|
||||||
banned_zip_contents = (".sfc",)
|
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
||||||
|
|
||||||
|
|
||||||
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
||||||
if not owner:
|
if not owner:
|
||||||
owner = session["_id"]
|
owner = session["_id"]
|
||||||
infolist = zfile.infolist()
|
infolist = zfile.infolist()
|
||||||
|
if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist):
|
||||||
|
flash(Markup("Error: Your .zip file only contains .yaml files. "
|
||||||
|
'Did you mean to <a href="/generate">generate a game</a>?'))
|
||||||
|
return
|
||||||
slots: typing.Set[Slot] = set()
|
slots: typing.Set[Slot] = set()
|
||||||
spoiler = ""
|
spoiler = ""
|
||||||
|
files = {}
|
||||||
multidata = None
|
multidata = None
|
||||||
|
|
||||||
|
# Load files.
|
||||||
for file in infolist:
|
for file in infolist:
|
||||||
handler = AutoPatchRegister.get_handler(file.filename)
|
handler = AutoPatchRegister.get_handler(file.filename)
|
||||||
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."
|
||||||
|
|
||||||
|
# AP Container
|
||||||
elif handler:
|
elif handler:
|
||||||
raw = zfile.open(file, "r").read()
|
|
||||||
patch = handler(BytesIO(raw))
|
|
||||||
patch.read()
|
|
||||||
slots.add(Slot(data=raw,
|
|
||||||
player_name=patch.player_name,
|
|
||||||
player_id=patch.player,
|
|
||||||
game=patch.game))
|
|
||||||
|
|
||||||
elif file.filename.endswith(".apmc"):
|
|
||||||
data = zfile.open(file, "r").read()
|
data = zfile.open(file, "r").read()
|
||||||
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
|
patch = handler(BytesIO(data))
|
||||||
slots.add(Slot(data=data,
|
patch.read()
|
||||||
player_name=metadata["player_name"],
|
files[patch.player] = data
|
||||||
player_id=metadata["player_id"],
|
|
||||||
game="Minecraft"))
|
|
||||||
|
|
||||||
elif file.filename.endswith(".apv6"):
|
|
||||||
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
|
|
||||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
|
||||||
player_id=int(slot_id[1:]), game="VVVVVV"))
|
|
||||||
|
|
||||||
elif file.filename.endswith(".apsm64ex"):
|
|
||||||
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
|
|
||||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
|
||||||
player_id=int(slot_id[1:]), game="Super Mario 64"))
|
|
||||||
|
|
||||||
elif file.filename.endswith(".zip"):
|
|
||||||
# Factorio mods need a specific name or they do not function
|
|
||||||
_, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-", 3)
|
|
||||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
|
||||||
player_id=int(slot_id[1:]), game="Factorio"))
|
|
||||||
|
|
||||||
elif file.filename.endswith(".apz5"):
|
|
||||||
# .apz5 must be named specifically since they don't contain any metadata
|
|
||||||
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
|
|
||||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
|
||||||
player_id=int(slot_id[1:]), game="Ocarina of Time"))
|
|
||||||
|
|
||||||
elif file.filename.endswith(".json"):
|
|
||||||
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('-', 3)
|
|
||||||
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
|
|
||||||
player_id=int(slot_id[1:]), game="Dark Souls III"))
|
|
||||||
|
|
||||||
|
# Spoiler
|
||||||
elif file.filename.endswith(".txt"):
|
elif file.filename.endswith(".txt"):
|
||||||
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
|
||||||
|
|
||||||
|
# Multi-data
|
||||||
elif file.filename.endswith(".archipelago"):
|
elif file.filename.endswith(".archipelago"):
|
||||||
try:
|
try:
|
||||||
multidata = zfile.open(file).read()
|
multidata = zfile.open(file).read()
|
||||||
@@ -84,17 +57,36 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
|||||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||||
multidata = None
|
multidata = None
|
||||||
|
|
||||||
|
# Minecraft
|
||||||
|
elif file.filename.endswith(".apmc"):
|
||||||
|
data = zfile.open(file, "r").read()
|
||||||
|
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
|
||||||
|
files[metadata["player_id"]] = data
|
||||||
|
|
||||||
|
# Factorio
|
||||||
|
elif file.filename.endswith(".zip"):
|
||||||
|
_, _, slot_id, *_ = file.filename.split('_')[0].split('-', 3)
|
||||||
|
data = zfile.open(file, "r").read()
|
||||||
|
files[int(slot_id[1:])] = data
|
||||||
|
|
||||||
|
# All other files using the standard MultiWorld.get_out_file_name_base method
|
||||||
|
else:
|
||||||
|
_, _, slot_id, *_ = file.filename.split('.')[0].split('_', 3)
|
||||||
|
data = zfile.open(file, "r").read()
|
||||||
|
files[int(slot_id[1:])] = data
|
||||||
|
|
||||||
|
# Load multi data.
|
||||||
if multidata:
|
if multidata:
|
||||||
decompressed_multidata = MultiServer.Context.decompress(multidata)
|
decompressed_multidata = MultiServer.Context.decompress(multidata)
|
||||||
if "slot_info" in decompressed_multidata:
|
if "slot_info" in decompressed_multidata:
|
||||||
player_names = {slot.player_name for slot in slots}
|
for slot, slot_info in decompressed_multidata["slot_info"].items():
|
||||||
leftover_names: typing.Dict[int, NetworkSlot] = {
|
# Ignore Player Groups (e.g. item links)
|
||||||
slot_id: slot_info for slot_id, slot_info in decompressed_multidata["slot_info"].items()
|
if slot_info.type == SlotType.group:
|
||||||
if slot_info.name not in player_names and slot_info.type != SlotType.group}
|
continue
|
||||||
newslots = [(Slot(data=None, player_name=slot_info.name, player_id=slot, game=slot_info.game))
|
slots.add(Slot(data=files.get(slot, None),
|
||||||
for slot, slot_info in leftover_names.items()]
|
player_name=slot_info.name,
|
||||||
for slot in newslots:
|
player_id=slot,
|
||||||
slots.add(slot)
|
game=slot_info.game))
|
||||||
|
|
||||||
flush() # commit slots
|
flush() # commit slots
|
||||||
|
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ class ZillionContext(CommonContext):
|
|||||||
assert id_ in id_to_loc
|
assert id_ in id_to_loc
|
||||||
self.loc_mem_to_id[mem] = id_
|
self.loc_mem_to_id[mem] = id_
|
||||||
|
|
||||||
|
if len(self.loc_mem_to_id) != 394:
|
||||||
|
logger.warn("invalid Zillion `Connected` packet, "
|
||||||
|
f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}")
|
||||||
|
|
||||||
self.got_slot_data.set()
|
self.got_slot_data.set()
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ local socket = require("socket")
|
|||||||
local json = require('json')
|
local json = require('json')
|
||||||
local math = require('math')
|
local math = require('math')
|
||||||
|
|
||||||
local last_modified_date = '2022-07-24' -- Should be the last modified date
|
local last_modified_date = '2022-11-27' -- Should be the last modified date
|
||||||
local script_version = 2
|
local script_version = 3
|
||||||
|
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
-- Heavily modified form of RiptideSage's tracker
|
-- Heavily modified form of RiptideSage's tracker
|
||||||
@@ -25,6 +25,9 @@ local inf_table_offset = save_context_offset + 0xEF8 -- 0x11B4C8
|
|||||||
|
|
||||||
local temp_context = nil
|
local temp_context = nil
|
||||||
|
|
||||||
|
local collectibles_overrides = nil
|
||||||
|
local collectible_offsets = nil
|
||||||
|
|
||||||
-- Offsets for scenes can be found here
|
-- Offsets for scenes can be found here
|
||||||
-- https://wiki.cloudmodding.com/oot/Scene_Table/NTSC_1.0
|
-- https://wiki.cloudmodding.com/oot/Scene_Table/NTSC_1.0
|
||||||
-- Each scene is 0x1c bits long, chests at 0x0, switches at 0x4, collectibles at 0xc
|
-- Each scene is 0x1c bits long, chests at 0x0, switches at 0x4, collectibles at 0xc
|
||||||
@@ -40,12 +43,16 @@ end
|
|||||||
-- [1] is the scene id
|
-- [1] is the scene id
|
||||||
-- [2] is the location type, which varies as input to the function
|
-- [2] is the location type, which varies as input to the function
|
||||||
-- [3] is the location id within the scene, and represents the bit which was checked
|
-- [3] is the location id within the scene, and represents the bit which was checked
|
||||||
|
-- REORDERED IN 7.0 TO scene id - location type - 0x00 - location id
|
||||||
-- Note that temp_context is 0-indexed and expected_values is 1-indexed, because consistency.
|
-- Note that temp_context is 0-indexed and expected_values is 1-indexed, because consistency.
|
||||||
local check_temp_context = function(expected_values)
|
local check_temp_context = function(expected_values)
|
||||||
if temp_context[0] ~= 0x00 then return false end
|
-- if temp_context[0] ~= 0x00 then return false end
|
||||||
for i=1,3 do
|
-- for i=1,3 do
|
||||||
if temp_context[i] ~= expected_values[i] then return false end
|
-- if temp_context[i] ~= expected_values[i] then return false end
|
||||||
end
|
-- end
|
||||||
|
if temp_context[0] ~= expected_values[1] then return false end
|
||||||
|
if temp_context[1] ~= expected_values[2] then return false end
|
||||||
|
if temp_context[3] ~= expected_values[3] then return false end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -67,7 +74,7 @@ local on_the_ground_check = function(scene_offset, bit_to_check)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local boss_item_check = function(scene_offset)
|
local boss_item_check = function(scene_offset)
|
||||||
return chest_check(scene_offset, 0x1F)
|
return on_the_ground_check(scene_offset, 0x1F)
|
||||||
or check_temp_context({scene_offset, 0x00, 0x4F})
|
or check_temp_context({scene_offset, 0x00, 0x4F})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -226,6 +233,8 @@ local read_kokiri_forest_checks = function()
|
|||||||
checks["KF Shop Item 6"] = shop_check(0x6, 0x1)
|
checks["KF Shop Item 6"] = shop_check(0x6, 0x1)
|
||||||
checks["KF Shop Item 7"] = shop_check(0x6, 0x2)
|
checks["KF Shop Item 7"] = shop_check(0x6, 0x2)
|
||||||
checks["KF Shop Item 8"] = shop_check(0x6, 0x3)
|
checks["KF Shop Item 8"] = shop_check(0x6, 0x3)
|
||||||
|
|
||||||
|
checks["KF Shop Blue Rupee"] = on_the_ground_check(0x2D, 0x1)
|
||||||
return checks
|
return checks
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -454,7 +463,7 @@ local read_kakariko_village_checks = function()
|
|||||||
checks["Kak Impas House Cow"] = cow_check(0x37, 0x18)
|
checks["Kak Impas House Cow"] = cow_check(0x37, 0x18)
|
||||||
|
|
||||||
checks["Kak GS Tree"] = skulltula_check(0x10, 0x5)
|
checks["Kak GS Tree"] = skulltula_check(0x10, 0x5)
|
||||||
checks["Kak GS Guards House"] = skulltula_check(0x10, 0x1)
|
checks["Kak GS Near Gate Guard"] = skulltula_check(0x10, 0x1)
|
||||||
checks["Kak GS Watchtower"] = skulltula_check(0x10, 0x2)
|
checks["Kak GS Watchtower"] = skulltula_check(0x10, 0x2)
|
||||||
checks["Kak GS Skulltula House"] = skulltula_check(0x10, 0x4)
|
checks["Kak GS Skulltula House"] = skulltula_check(0x10, 0x4)
|
||||||
checks["Kak GS House Under Construction"] = skulltula_check(0x10, 0x3)
|
checks["Kak GS House Under Construction"] = skulltula_check(0x10, 0x3)
|
||||||
@@ -480,7 +489,7 @@ local read_graveyard_checks = function()
|
|||||||
checks["Graveyard Royal Familys Tomb Chest"] = chest_check(0x41, 0x00)
|
checks["Graveyard Royal Familys Tomb Chest"] = chest_check(0x41, 0x00)
|
||||||
checks["Graveyard Freestanding PoH"] = on_the_ground_check(0x53, 0x4)
|
checks["Graveyard Freestanding PoH"] = on_the_ground_check(0x53, 0x4)
|
||||||
checks["Graveyard Dampe Gravedigging Tour"] = on_the_ground_check(0x53, 0x8)
|
checks["Graveyard Dampe Gravedigging Tour"] = on_the_ground_check(0x53, 0x8)
|
||||||
checks["Graveyard Hookshot Chest"] = chest_check(0x48, 0x00)
|
checks["Graveyard Dampe Race Hookshot Chest"] = chest_check(0x48, 0x00)
|
||||||
checks["Graveyard Dampe Race Freestanding PoH"] = on_the_ground_check(0x48, 0x7)
|
checks["Graveyard Dampe Race Freestanding PoH"] = on_the_ground_check(0x48, 0x7)
|
||||||
|
|
||||||
checks["Graveyard GS Bean Patch"] = skulltula_check(0x10, 0x0)
|
checks["Graveyard GS Bean Patch"] = skulltula_check(0x10, 0x0)
|
||||||
@@ -545,7 +554,7 @@ local read_shadow_temple_checks = function(mq_table_address)
|
|||||||
checks["Shadow Temple Boss Key Chest"] = chest_check(0x07, 0x0B)
|
checks["Shadow Temple Boss Key Chest"] = chest_check(0x07, 0x0B)
|
||||||
checks["Shadow Temple Invisible Floormaster Chest"] = chest_check(0x07, 0x0D)
|
checks["Shadow Temple Invisible Floormaster Chest"] = chest_check(0x07, 0x0D)
|
||||||
|
|
||||||
checks["Shadow Temple GS Like Like Room"] = skulltula_check(0x07, 0x3)
|
checks["Shadow Temple GS Invisible Blades Room"] = skulltula_check(0x07, 0x3)
|
||||||
checks["Shadow Temple GS Falling Spikes Room"] = skulltula_check(0x07, 0x1)
|
checks["Shadow Temple GS Falling Spikes Room"] = skulltula_check(0x07, 0x1)
|
||||||
checks["Shadow Temple GS Single Giant Pot"] = skulltula_check(0x07, 0x0)
|
checks["Shadow Temple GS Single Giant Pot"] = skulltula_check(0x07, 0x0)
|
||||||
checks["Shadow Temple GS Near Ship"] = skulltula_check(0x07, 0x4)
|
checks["Shadow Temple GS Near Ship"] = skulltula_check(0x07, 0x4)
|
||||||
@@ -723,9 +732,9 @@ local read_fire_temple_checks = function(mq_table_address)
|
|||||||
|
|
||||||
checks["Fire Temple MQ GS Big Lava Room Open Door"] = skulltula_check(0x4, 0x0)
|
checks["Fire Temple MQ GS Big Lava Room Open Door"] = skulltula_check(0x4, 0x0)
|
||||||
checks["Fire Temple MQ GS Skull On Fire"] = skulltula_check(0x4, 0x2)
|
checks["Fire Temple MQ GS Skull On Fire"] = skulltula_check(0x4, 0x2)
|
||||||
checks["Fire Temple MQ GS Fire Wall Maze Center"] = skulltula_check(0x4, 0x3)
|
checks["Fire Temple MQ GS Flame Maze Center"] = skulltula_check(0x4, 0x3)
|
||||||
checks["Fire Temple MQ GS Fire Wall Maze Side Room"] = skulltula_check(0x4, 0x4)
|
checks["Fire Temple MQ GS Flame Maze Side Room"] = skulltula_check(0x4, 0x4)
|
||||||
checks["Fire Temple MQ GS Above Fire Wall Maze"] = skulltula_check(0x4, 0x1)
|
checks["Fire Temple MQ GS Above Flame Maze"] = skulltula_check(0x4, 0x1)
|
||||||
end
|
end
|
||||||
|
|
||||||
checks["Fire Temple Volvagia Heart"] = boss_item_check(0x15)
|
checks["Fire Temple Volvagia Heart"] = boss_item_check(0x15)
|
||||||
@@ -743,6 +752,12 @@ local read_zoras_river_checks = function()
|
|||||||
checks["ZR Deku Scrub Grotto Front"] = scrub_sanity_check(0x15, 0x9)
|
checks["ZR Deku Scrub Grotto Front"] = scrub_sanity_check(0x15, 0x9)
|
||||||
checks["ZR Deku Scrub Grotto Rear"] = scrub_sanity_check(0x15, 0x8)
|
checks["ZR Deku Scrub Grotto Rear"] = scrub_sanity_check(0x15, 0x8)
|
||||||
|
|
||||||
|
checks["ZR Frogs Zeldas Lullaby"] = event_check(0xD, 0x1)
|
||||||
|
checks["ZR Frogs Eponas Song"] = event_check(0xD, 0x2)
|
||||||
|
checks["ZR Frogs Suns Song"] = event_check(0xD, 0x3)
|
||||||
|
checks["ZR Frogs Sarias Song"] = event_check(0xD, 0x4)
|
||||||
|
checks["ZR Frogs Song of Time"] = event_check(0xD, 0x5)
|
||||||
|
|
||||||
checks["ZR GS Tree"] = skulltula_check(0x11, 0x1)
|
checks["ZR GS Tree"] = skulltula_check(0x11, 0x1)
|
||||||
--NOTE: There is no GS in the soft soil. It's the only one that doesn't have one.
|
--NOTE: There is no GS in the soft soil. It's the only one that doesn't have one.
|
||||||
checks["ZR GS Ladder"] = skulltula_check(0x11, 0x0)
|
checks["ZR GS Ladder"] = skulltula_check(0x11, 0x0)
|
||||||
@@ -912,10 +927,10 @@ end
|
|||||||
|
|
||||||
local read_gerudo_fortress_checks = function()
|
local read_gerudo_fortress_checks = function()
|
||||||
local checks = {}
|
local checks = {}
|
||||||
checks["Hideout Jail Guard (1 Torch)"] = on_the_ground_check(0xC, 0xC)
|
checks["Hideout 1 Torch Jail Gerudo Key"] = on_the_ground_check(0xC, 0xC)
|
||||||
checks["Hideout Jail Guard (2 Torches)"] = on_the_ground_check(0xC, 0xF)
|
checks["Hideout 2 Torches Jail Gerudo Key"] = on_the_ground_check(0xC, 0xF)
|
||||||
checks["Hideout Jail Guard (3 Torches)"] = on_the_ground_check(0xC, 0xA)
|
checks["Hideout 3 Torches Jail Gerudo Key"] = on_the_ground_check(0xC, 0xA)
|
||||||
checks["Hideout Jail Guard (4 Torches)"] = on_the_ground_check(0xC, 0xE)
|
checks["Hideout 4 Torches Jail Gerudo Key"] = on_the_ground_check(0xC, 0xE)
|
||||||
checks["Hideout Gerudo Membership Card"] = membership_card_check(0xC, 0x2)
|
checks["Hideout Gerudo Membership Card"] = membership_card_check(0xC, 0x2)
|
||||||
checks["GF Chest"] = chest_check(0x5D, 0x0)
|
checks["GF Chest"] = chest_check(0x5D, 0x0)
|
||||||
checks["GF HBA 1000 Points"] = info_table_check(0x33, 0x0)
|
checks["GF HBA 1000 Points"] = info_table_check(0x33, 0x0)
|
||||||
@@ -1170,9 +1185,22 @@ local check_all_locations = function(mq_table_address)
|
|||||||
for k,v in pairs(read_ganons_castle_checks(mq_table_address)) do location_checks[k] = v end
|
for k,v in pairs(read_ganons_castle_checks(mq_table_address)) do location_checks[k] = v end
|
||||||
for k,v in pairs(read_outside_ganons_castle_checks()) do location_checks[k] = v end
|
for k,v in pairs(read_outside_ganons_castle_checks()) do location_checks[k] = v end
|
||||||
for k,v in pairs(read_song_checks()) do location_checks[k] = v end
|
for k,v in pairs(read_song_checks()) do location_checks[k] = v end
|
||||||
|
-- write 0 to temp context values
|
||||||
|
mainmemory.write_u32_be(0x40002C, 0)
|
||||||
|
mainmemory.write_u32_be(0x400030, 0)
|
||||||
return location_checks
|
return location_checks
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local check_collectibles = function()
|
||||||
|
local retval = {}
|
||||||
|
if collectible_offsets ~= nil then
|
||||||
|
for id, data in pairs(collectible_offsets) do
|
||||||
|
local mem = mainmemory.readbyte(collectible_overrides + data[1] + bit.rshift(data[2], 3))
|
||||||
|
retval[id] = bit.check(mem, data[2] % 8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return retval
|
||||||
|
end
|
||||||
|
|
||||||
-- convenience functions
|
-- convenience functions
|
||||||
|
|
||||||
@@ -1557,9 +1585,10 @@ local outgoing_player_addr = coop_context + 18
|
|||||||
|
|
||||||
local player_names_address = coop_context + 20
|
local player_names_address = coop_context + 20
|
||||||
local player_name_length = 8 -- 8 bytes
|
local player_name_length = 8 -- 8 bytes
|
||||||
local rom_name_location = player_names_address + 0x800
|
local rom_name_location = player_names_address + 0x800 + 0x5 -- 0x800 player names, 0x5 CFG_FILE_SELECT_HASH
|
||||||
|
|
||||||
local master_quest_table_address = rando_context + (mainmemory.read_u32_be(rando_context + 0x0CE0) - 0x03480000)
|
-- TODO: load dynamically from slot data
|
||||||
|
local master_quest_table_address = rando_context + (mainmemory.read_u32_be(rando_context + 0x0E9F) - 0x03480000)
|
||||||
|
|
||||||
local save_context_addr = 0x11A5D0
|
local save_context_addr = 0x11A5D0
|
||||||
local internal_count_addr = save_context_addr + 0x90
|
local internal_count_addr = save_context_addr + 0x90
|
||||||
@@ -1568,7 +1597,7 @@ local item_queue = {}
|
|||||||
local first_connect = true
|
local first_connect = true
|
||||||
local game_complete = false
|
local game_complete = false
|
||||||
|
|
||||||
NUM_BIG_POES_REQUIRED = mainmemory.read_u8(rando_context + 0x0CEE)
|
NUM_BIG_POES_REQUIRED = mainmemory.read_u8(rando_context + 0x0EAD)
|
||||||
|
|
||||||
local bytes_to_string = function(bytes)
|
local bytes_to_string = function(bytes)
|
||||||
local string = ''
|
local string = ''
|
||||||
@@ -1718,7 +1747,7 @@ function is_game_complete()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function deathlink_enabled()
|
function deathlink_enabled()
|
||||||
local death_link_flag = mainmemory.read_u16_be(coop_context + 0xA)
|
local death_link_flag = mainmemory.readbyte(coop_context + 0xB)
|
||||||
return death_link_flag > 0
|
return death_link_flag > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1774,6 +1803,13 @@ function process_block(block)
|
|||||||
mainmemory.write_u16_be(incoming_item_addr, item_queue[received_items_count+1])
|
mainmemory.write_u16_be(incoming_item_addr, item_queue[received_items_count+1])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
-- Record collectible data if necessary
|
||||||
|
if collectible_overrides == nil and block['collectibleOverrides'] ~= 0 then
|
||||||
|
collectible_overrides = mainmemory.read_u32_be(rando_context + block['collectibleOverrides']) - 0x80000000
|
||||||
|
end
|
||||||
|
if collectible_offsets ~= block['collectibleOffsets'] then
|
||||||
|
collectible_offsets = block['collectibleOffsets']
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1805,6 +1841,7 @@ function receive()
|
|||||||
retTable["deathlinkActive"] = deathlink_enabled()
|
retTable["deathlinkActive"] = deathlink_enabled()
|
||||||
if InSafeState() then
|
if InSafeState() then
|
||||||
retTable["locations"] = check_all_locations(master_quest_table_address)
|
retTable["locations"] = check_all_locations(master_quest_table_address)
|
||||||
|
retTable["collectibles"] = check_collectibles()
|
||||||
retTable["isDead"] = get_death_state()
|
retTable["isDead"] = get_death_state()
|
||||||
retTable["gameComplete"] = is_game_complete()
|
retTable["gameComplete"] = is_game_complete()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,18 +7,25 @@ local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
|||||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||||
local STATE_UNINITIALIZED = "Uninitialized"
|
local STATE_UNINITIALIZED = "Uninitialized"
|
||||||
|
|
||||||
|
local SCRIPT_VERSION = 1
|
||||||
|
|
||||||
local APIndex = 0x1A6E
|
local APIndex = 0x1A6E
|
||||||
|
local APDeathLinkAddress = 0x00FD
|
||||||
local APItemAddress = 0x00FF
|
local APItemAddress = 0x00FF
|
||||||
local EventFlagAddress = 0x1735
|
local EventFlagAddress = 0x1735
|
||||||
local MissableAddress = 0x161A
|
local MissableAddress = 0x161A
|
||||||
local HiddenItemsAddress = 0x16DE
|
local HiddenItemsAddress = 0x16DE
|
||||||
local RodAddress = 0x1716
|
local RodAddress = 0x1716
|
||||||
local InGame = 0x1A71
|
local InGame = 0x1A71
|
||||||
|
local ClientCompatibilityAddress = 0xFF00
|
||||||
|
|
||||||
local ItemsReceived = nil
|
local ItemsReceived = nil
|
||||||
local playerName = nil
|
local playerName = nil
|
||||||
local seedName = nil
|
local seedName = nil
|
||||||
|
|
||||||
|
local deathlink_rec = nil
|
||||||
|
local deathlink_send = false
|
||||||
|
|
||||||
local prevstate = ""
|
local prevstate = ""
|
||||||
local curstate = STATE_UNINITIALIZED
|
local curstate = STATE_UNINITIALIZED
|
||||||
local gbSocket = nil
|
local gbSocket = nil
|
||||||
@@ -69,11 +76,10 @@ function processBlock(block)
|
|||||||
end
|
end
|
||||||
local itemsBlock = block["items"]
|
local itemsBlock = block["items"]
|
||||||
memDomain.wram()
|
memDomain.wram()
|
||||||
if itemsBlock ~= nil then-- and u8(0x116B) ~= 0x00 then
|
if itemsBlock ~= nil then
|
||||||
-- print(itemsBlock)
|
ItemsReceived = itemsBlock
|
||||||
ItemsReceived = itemsBlock
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
deathlink_rec = block["deathlink"]
|
||||||
end
|
end
|
||||||
|
|
||||||
function difference(a, b)
|
function difference(a, b)
|
||||||
@@ -104,14 +110,7 @@ function generateLocationsChecked()
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
end
|
end
|
||||||
function generateSerialData()
|
|
||||||
memDomain.wram()
|
|
||||||
status = u8(0x1A73)
|
|
||||||
if status == 0 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return uRange(0x1A76, u8(0x1A74))
|
|
||||||
end
|
|
||||||
local function arrayEqual(a1, a2)
|
local function arrayEqual(a1, a2)
|
||||||
if #a1 ~= #a2 then
|
if #a1 ~= #a2 then
|
||||||
return false
|
return false
|
||||||
@@ -135,7 +134,6 @@ function receive()
|
|||||||
curstate = STATE_UNINITIALIZED
|
curstate = STATE_UNINITIALIZED
|
||||||
return
|
return
|
||||||
elseif e == 'timeout' then
|
elseif e == 'timeout' then
|
||||||
print("timeout")
|
|
||||||
return
|
return
|
||||||
elseif e ~= nil then
|
elseif e ~= nil then
|
||||||
print(e)
|
print(e)
|
||||||
@@ -157,16 +155,16 @@ function receive()
|
|||||||
playerName = newPlayerName
|
playerName = newPlayerName
|
||||||
seedName = newSeedName
|
seedName = newSeedName
|
||||||
local retTable = {}
|
local retTable = {}
|
||||||
|
retTable["scriptVersion"] = SCRIPT_VERSION
|
||||||
|
retTable["clientCompatibilityVersion"] = u8(ClientCompatibilityAddress)
|
||||||
retTable["playerName"] = playerName
|
retTable["playerName"] = playerName
|
||||||
retTable["seedName"] = seedName
|
retTable["seedName"] = seedName
|
||||||
memDomain.wram()
|
memDomain.wram()
|
||||||
if u8(InGame) == 0xAC then
|
if u8(InGame) == 0xAC then
|
||||||
retTable["locations"] = generateLocationsChecked()
|
retTable["locations"] = generateLocationsChecked()
|
||||||
serialData = generateSerialData()
|
|
||||||
if serialData ~= nil then
|
|
||||||
retTable["serial"] = serialData
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
retTable["deathLink"] = deathlink_send
|
||||||
|
deathlink_send = false
|
||||||
msg = json.encode(retTable).."\n"
|
msg = json.encode(retTable).."\n"
|
||||||
local ret, error = gbSocket:send(msg)
|
local ret, error = gbSocket:send(msg)
|
||||||
if ret == nil then
|
if ret == nil then
|
||||||
@@ -197,6 +195,12 @@ function main()
|
|||||||
receive()
|
receive()
|
||||||
if u8(InGame) == 0xAC and u8(APItemAddress) == 0x00 then
|
if u8(InGame) == 0xAC and u8(APItemAddress) == 0x00 then
|
||||||
ItemIndex = u16(APIndex)
|
ItemIndex = u16(APIndex)
|
||||||
|
if deathlink_rec == true then
|
||||||
|
wU8(APDeathLinkAddress, 1)
|
||||||
|
elseif u8(APDeathLinkAddress) == 3 then
|
||||||
|
wU8(APDeathLinkAddress, 0)
|
||||||
|
deathlink_send = true
|
||||||
|
end
|
||||||
if ItemsReceived[ItemIndex + 1] ~= nil then
|
if ItemsReceived[ItemIndex + 1] ~= nil then
|
||||||
wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000)
|
wU8(APItemAddress, ItemsReceived[ItemIndex + 1] - 172000000)
|
||||||
end
|
end
|
||||||
@@ -212,7 +216,6 @@ function main()
|
|||||||
print("Attempting to connect")
|
print("Attempting to connect")
|
||||||
local client, timeout = server:accept()
|
local client, timeout = server:accept()
|
||||||
if timeout == nil then
|
if timeout == nil then
|
||||||
-- print('Initial Connection Made')
|
|
||||||
curstate = STATE_INITIAL_CONNECTION_MADE
|
curstate = STATE_INITIAL_CONNECTION_MADE
|
||||||
gbSocket = client
|
gbSocket = client
|
||||||
gbSocket:settimeout(0)
|
gbSocket:settimeout(0)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
|
Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
|
||||||
Those are located in the `worlds/` folder (source) or `<insall dir>/lib/worlds/` (when installed).
|
Those are located in the `worlds/` folder (source) or `<insall dir>/lib/worlds/` (when installed).
|
||||||
See [world api.md](world api.md) for details.
|
See [world api.md](world%20api.md) for details.
|
||||||
|
|
||||||
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
|
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
|
||||||
file into the worlds folder.
|
file into the worlds folder.
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ Sent to clients when they connect to an Archipelago server.
|
|||||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
||||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
||||||
| games | list\[str\] | List of games present in this multiworld. |
|
| games | list\[str\] | List of games present in this multiworld. |
|
||||||
| datapackage_version | int | Sum of individual games' datapackage version. Deprecated. Use `datapackage_versions` instead. |
|
|
||||||
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). |
|
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). |
|
||||||
| seed_name | str | uniquely identifying name of this generation |
|
| seed_name | str | uniquely identifying name of this generation |
|
||||||
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
|
||||||
@@ -121,15 +120,15 @@ InvalidItemsHandling indicates a wrong value type or flag combination was sent.
|
|||||||
### Connected
|
### Connected
|
||||||
Sent to clients when the connection handshake is successfully completed.
|
Sent to clients when the connection handshake is successfully completed.
|
||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ---- | ---- | ----- |
|
|-------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| team | int | Your team number. See [NetworkPlayer](#NetworkPlayer) for more info on team number. |
|
| team | int | Your team number. See [NetworkPlayer](#NetworkPlayer) for more info on team number. |
|
||||||
| slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. |
|
| slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. |
|
||||||
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. |
|
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. |
|
||||||
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
|
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
|
||||||
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 2<sup>53</sup>-1. |
|
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 2<sup>53</sup>-1. |
|
||||||
| slot_data | dict | Contains a json object for slot related data, differs per game. Empty if not required. |
|
| slot_data | dict\[str, any\] | Contains a json object for slot related data, differs per game. Empty if not required. Not present if slot_data in [Connect](#Connect) is false. |
|
||||||
| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information |
|
| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information |
|
||||||
|
|
||||||
### ReceivedItems
|
### ReceivedItems
|
||||||
Sent to clients when they receive an item.
|
Sent to clients when they receive an item.
|
||||||
@@ -242,11 +241,11 @@ Additional arguments added to the [Get](#Get) package that triggered this [Retri
|
|||||||
### SetReply
|
### SetReply
|
||||||
Sent to clients in response to a [Set](#Set) package if want_reply was set to true, or if the client has registered to receive updates for a certain key using the [SetNotify](#SetNotify) package. SetReply packages are sent even if a [Set](#Set) package did not alter the value for the key.
|
Sent to clients in response to a [Set](#Set) package if want_reply was set to true, or if the client has registered to receive updates for a certain key using the [SetNotify](#SetNotify) package. SetReply packages are sent even if a [Set](#Set) package did not alter the value for the key.
|
||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ---- | ---- | ----- |
|
|----------------|------|--------------------------------------------------------------------------------------------|
|
||||||
| key | str | The key that was updated. |
|
| key | str | The key that was updated. |
|
||||||
| value | any | The new value for the key. |
|
| value | any | The new value for the key. |
|
||||||
| original_value | any | The value the key had before it was updated. |
|
| original_value | any | The value the key had before it was updated. Not present on "_read" prefixed special keys. |
|
||||||
|
|
||||||
Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along.
|
Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along.
|
||||||
|
|
||||||
@@ -269,15 +268,16 @@ These packets are sent purely from client to server. They are not accepted by cl
|
|||||||
Sent by the client to initiate a connection to an Archipelago game session.
|
Sent by the client to initiate a connection to an Archipelago game session.
|
||||||
|
|
||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ---- | ---- | ----- |
|
|----------------|-----------------------------------|----------------------------------------------------------------------------------------------|
|
||||||
| password | str | If the game session requires a password, it should be passed here. |
|
| password | str | If the game session requires a password, it should be passed here. |
|
||||||
| game | str | The name of the game the client is playing. Example: `A Link to the Past` |
|
| game | str | The name of the game the client is playing. Example: `A Link to the Past` |
|
||||||
| name | str | The player name for this client. |
|
| name | str | The player name for this client. |
|
||||||
| uuid | str | Unique identifier for player client. |
|
| uuid | str | Unique identifier for player client. |
|
||||||
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
|
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
|
||||||
| items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. |
|
| items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. |
|
||||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
|
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
|
||||||
|
| slot_data | bool | If true, the Connect answer will contain slot_data |
|
||||||
|
|
||||||
#### items_handling flags
|
#### items_handling flags
|
||||||
| Value | Meaning |
|
| Value | Meaning |
|
||||||
@@ -367,14 +367,23 @@ Used to request a single or multiple values from the server's data storage, see
|
|||||||
|
|
||||||
Additional arguments sent in this package will also be added to the [Retrieved](#Retrieved) package it triggers.
|
Additional arguments sent in this package will also be added to the [Retrieved](#Retrieved) package it triggers.
|
||||||
|
|
||||||
|
Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`.
|
||||||
|
|
||||||
|
| Name | Type | Notes |
|
||||||
|
|-------------------------------|--------------------------|---------------------------------------------------|
|
||||||
|
| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. |
|
||||||
|
| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. |
|
||||||
|
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
|
||||||
|
|
||||||
### Set
|
### Set
|
||||||
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
|
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
|
||||||
|
Keys that start with `_read_` cannot be set.
|
||||||
#### Arguments
|
#### Arguments
|
||||||
| Name | Type | Notes |
|
| Name | Type | Notes |
|
||||||
| ------ | ----- | ------ |
|
|------------|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
|
||||||
| key | str | The key to manipulate. |
|
| key | str | The key to manipulate. Can never start with "_read". |
|
||||||
| default | any | The default value to use in case the key has no value on the server. |
|
| default | any | The default value to use in case the key has no value on the server. |
|
||||||
| want_reply | bool | If true, the server will send a [SetReply](#SetReply) response back to the client. |
|
| want_reply | bool | If true, the server will send a [SetReply](#SetReply) response back to the client. |
|
||||||
| operations | list\[[DataStorageOperation](#DataStorageOperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. |
|
| operations | list\[[DataStorageOperation](#DataStorageOperation)\] | Operations to apply to the value, multiple operations can be present and they will be executed in order of appearance. |
|
||||||
|
|
||||||
Additional arguments sent in this package will also be added to the [SetReply](#SetReply) package it triggers.
|
Additional arguments sent in this package will also be added to the [SetReply](#SetReply) package it triggers.
|
||||||
@@ -591,6 +600,20 @@ class Permission(enum.IntEnum):
|
|||||||
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Hint
|
||||||
|
An object representing a Hint.
|
||||||
|
```python
|
||||||
|
import typing
|
||||||
|
class Hint(typing.NamedTuple):
|
||||||
|
receiving_player: int
|
||||||
|
finding_player: int
|
||||||
|
location: int
|
||||||
|
item: int
|
||||||
|
found: bool
|
||||||
|
entrance: str = ""
|
||||||
|
item_flags: int = 0
|
||||||
|
```
|
||||||
|
|
||||||
### Data Package Contents
|
### Data Package Contents
|
||||||
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
|
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
|
||||||
|
|
||||||
@@ -622,7 +645,6 @@ Tags are represented as a list of strings, the common Client tags follow:
|
|||||||
| Name | Notes |
|
| Name | Notes |
|
||||||
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
|
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
|
||||||
| IgnoreGame | Deprecated. See Tracker and TextOnly. Tells the server to ignore the "game" attribute in the [Connect](#Connect) packet. |
|
|
||||||
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
|
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
|
||||||
| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
||||||
| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. |
|
||||||
|
|||||||
@@ -343,18 +343,6 @@ class MyGameWorld(World):
|
|||||||
option_definitions = mygame_options # assign the options dict to the world
|
option_definitions = mygame_options # assign the options dict to the world
|
||||||
#...
|
#...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local or Remote
|
|
||||||
|
|
||||||
A world with `remote_items` set to `True` gets all items items from the server
|
|
||||||
and no item from the local game. So for an RPG opening a chest would not add
|
|
||||||
any item to your inventory, instead the server will send you what was in that
|
|
||||||
chest. The advantage is that a generic mod can be used that does not need to
|
|
||||||
know anything about the seed.
|
|
||||||
|
|
||||||
A world with `remote_items` set to `False` will locally reward its local items.
|
|
||||||
For console games this can remove delay and make script/animation/dialog flow
|
|
||||||
more natural. These games typically have been edited to 'bake in' the items.
|
|
||||||
|
|
||||||
### A World Class Skeleton
|
### A World Class Skeleton
|
||||||
|
|
||||||
@@ -379,8 +367,6 @@ class MyGameWorld(World):
|
|||||||
game: str = "My Game" # name of the game/world
|
game: str = "My Game" # name of the game/world
|
||||||
option_definitions = mygame_options # options the player can set
|
option_definitions = mygame_options # options the player can set
|
||||||
topology_present: bool = True # show path to required location checks in spoiler
|
topology_present: bool = True # show path to required location checks in spoiler
|
||||||
remote_items: bool = False # True if all items come from the server
|
|
||||||
remote_start_inventory: bool = False # True if start inventory comes from the server
|
|
||||||
|
|
||||||
# data_version is used to signal that items, locations or their names
|
# data_version is used to signal that items, locations or their names
|
||||||
# changed. Set this to 0 during development so other games' clients do not
|
# changed. Set this to 0 during development so other games' clients do not
|
||||||
@@ -415,17 +401,13 @@ The world has to provide the following things for generation
|
|||||||
* additions to the item pool
|
* additions to the item pool
|
||||||
* additions to the regions list: at least one called "Menu"
|
* additions to the regions list: at least one called "Menu"
|
||||||
* locations placed inside those regions
|
* locations placed inside those regions
|
||||||
* a `def create_item(self, item: str) -> MyGameItem` for plando/manual placing
|
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
||||||
* applying `self.world.precollected_items` for plando/start inventory
|
* applying `self.world.push_precollected` for start inventory
|
||||||
if not using a `remote_start_inventory`
|
|
||||||
* a `def generate_output(self, output_directory: str)` that creates the output
|
* a `def generate_output(self, output_directory: str)` that creates the output
|
||||||
if there is output to be generated. If only items are randomized and
|
files if there is output to be generated. When this is
|
||||||
`remote_items = True` it is possible to have a generic mod and output
|
called, `self.world.get_locations(self.player)` has all locations for the player, with
|
||||||
generation can be skipped. In all other cases this is required. When this is
|
attribute `item` pointing to the item.
|
||||||
called, `self.world.get_locations()` has all locations for all players, with
|
`location.item.player` can be used to see if it's a local item.
|
||||||
properties `item` pointing to the item and `player` identifying the player.
|
|
||||||
`self.world.get_filled_locations(self.player)` will filter for this world.
|
|
||||||
`item.player` can be used to see if it's a local item.
|
|
||||||
|
|
||||||
In addition, the following methods can be implemented and attributes can be set
|
In addition, the following methods can be implemented and attributes can be set
|
||||||
|
|
||||||
@@ -433,12 +415,13 @@ In addition, the following methods can be implemented and attributes can be set
|
|||||||
called per player before any items or locations are created. You can set
|
called per player before any items or locations are created. You can set
|
||||||
properties on your world here. Already has access to player options and RNG.
|
properties on your world here. Already has access to player options and RNG.
|
||||||
* `def create_regions(self)`
|
* `def create_regions(self)`
|
||||||
called to place player's regions into the MultiWorld's regions list. If it's
|
called to place player's regions and their locations into the MultiWorld's regions list. If it's
|
||||||
hard to separate, this can be done during `generate_early` or `basic` as well.
|
hard to separate, this can be done during `generate_early` or `basic` as well.
|
||||||
* `def create_items(self)`
|
* `def create_items(self)`
|
||||||
called to place player's items into the MultiWorld's itempool.
|
called to place player's items into the MultiWorld's itempool.
|
||||||
* `def set_rules(self)`
|
* `def set_rules(self)`
|
||||||
called to set access and item rules on locations and entrances.
|
called to set access and item rules on locations and entrances.
|
||||||
|
Locations have to be defined before this, or rule application can miss them.
|
||||||
* `def generate_basic(self)`
|
* `def generate_basic(self)`
|
||||||
called after the previous steps. Some placement and player specific
|
called after the previous steps. Some placement and player specific
|
||||||
randomizations can be done here. After this step all regions and items have
|
randomizations can be done here. After this step all regions and items have
|
||||||
@@ -677,6 +660,7 @@ def generate_output(self, output_directory: str):
|
|||||||
if location.item.player == self.player else "Remote"
|
if location.item.player == self.player else "Remote"
|
||||||
for location in self.multiworld.get_filled_locations(self.player)},
|
for location in self.multiworld.get_filled_locations(self.player)},
|
||||||
# store start_inventory from player's .yaml
|
# store start_inventory from player's .yaml
|
||||||
|
# make sure to mark as not remote_start_inventory when connecting if stored in rom/mod
|
||||||
"starter_items": [item.name for item
|
"starter_items": [item.name for item
|
||||||
in self.multiworld.precollected_items[self.player]],
|
in self.multiworld.precollected_items[self.player]],
|
||||||
"final_boss_hp": self.final_boss_hp,
|
"final_boss_hp": self.final_boss_hp,
|
||||||
|
|||||||
10
host.yaml
10
host.yaml
@@ -68,9 +68,10 @@ generator:
|
|||||||
meta_file_path: "meta.yaml"
|
meta_file_path: "meta.yaml"
|
||||||
# Create a spoiler file
|
# Create a spoiler file
|
||||||
# 0 -> None
|
# 0 -> None
|
||||||
# 1 -> Spoiler without playthrough
|
# 1 -> Spoiler without playthrough or paths to playthrough required items
|
||||||
# 2 -> Full spoiler
|
# 2 -> Spoiler with playthrough (viable solution to goals)
|
||||||
spoiler: 2
|
# 3 -> Spoiler with playthrough and traversal paths towards items
|
||||||
|
spoiler: 3
|
||||||
# Glitch to Triforce room from Ganon
|
# Glitch to Triforce room from Ganon
|
||||||
# When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality + hammer)
|
# When disabled, you have to have a weapon that can hurt ganon (master sword or swordless/easy item functionality + hammer)
|
||||||
# and have completed the goal required for killing ganon to be able to access the triforce room.
|
# and have completed the goal required for killing ganon to be able to access the triforce room.
|
||||||
@@ -92,6 +93,9 @@ sni_options:
|
|||||||
lttp_options:
|
lttp_options:
|
||||||
# File name of the v1.0 J rom
|
# File name of the v1.0 J rom
|
||||||
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
rom_file: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
|
||||||
|
lufia2ac_options:
|
||||||
|
# File name of the US rom
|
||||||
|
rom_file: "Lufia II - Rise of the Sinistrals (USA).sfc"
|
||||||
sm_options:
|
sm_options:
|
||||||
# File name of the v1.0 J rom
|
# File name of the v1.0 J rom
|
||||||
rom_file: "Super Metroid (JU).sfc"
|
rom_file: "Super Metroid (JU).sfc"
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full ho
|
|||||||
Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
Name: "generator/dkc3"; Description: "Donkey Kong Country 3 ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||||
Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
Name: "generator/smw"; Description: "Super Mario World ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||||
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
|
||||||
|
Name: "generator/l2ac"; Description: "Lufia II Ancient Cave ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 2621440; Flags: disablenouninstallwarning
|
||||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
||||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
|
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
|
||||||
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
|
Name: "generator/zl"; Description: "Zillion ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 150000; Flags: disablenouninstallwarning
|
||||||
@@ -69,6 +70,7 @@ Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Se
|
|||||||
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
|
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid 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/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/factorio"; Description: "Factorio"; Types: full playing
|
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||||
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
|
Name: "client/oot"; Description: "Ocarina of Time"; Types: full playing
|
||||||
@@ -90,6 +92,7 @@ Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).s
|
|||||||
Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3
|
Source: "{code:GetDKC3ROMPath}"; DestDir: "{app}"; DestName: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"; Flags: external; Components: client/sni/dkc3 or generator/dkc3
|
||||||
Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
|
Source: "{code:GetSMWROMPath}"; DestDir: "{app}"; DestName: "Super Mario World (USA).sfc"; Flags: external; Components: client/sni/smw or generator/smw
|
||||||
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
|
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
|
||||||
|
Source: "{code:GetL2ACROMPath}"; DestDir: "{app}"; DestName: "Lufia II - Rise of the Sinistrals (USA).sfc"; Flags: external; Components: generator/l2ac
|
||||||
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
|
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: client/oot or generator/oot
|
||||||
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
|
Source: "{code:GetZlROMPath}"; DestDir: "{app}"; DestName: "Zillion (UE) [!].sms"; Flags: external; Components: client/zl or generator/zl
|
||||||
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
|
Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S][!].gb"; Flags: external; Components: client/pkmn/red or generator/pkmn_r
|
||||||
@@ -152,6 +155,7 @@ Type: dirifempty; Name: "{app}"
|
|||||||
[InstallDelete]
|
[InstallDelete]
|
||||||
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
|
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
|
||||||
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
|
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy*"
|
||||||
|
#include "installdelete.iss"
|
||||||
|
|
||||||
[Registry]
|
[Registry]
|
||||||
|
|
||||||
@@ -190,6 +194,11 @@ Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Arch
|
|||||||
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||||
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||||
|
|
||||||
|
Root: HKCR; Subkey: ".apl2ac"; ValueData: "{#MyAppName}l2acpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Archipelago Lufia II Ancient Cave Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
|
||||||
|
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
|
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||||
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
|
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||||
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
|
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
|
||||||
@@ -200,15 +209,15 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf"; ValueData: "Archip
|
|||||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot
|
Root: HKCR; Subkey: "{#MyAppName}n64zpf\DefaultIcon"; ValueData: "{app}\ArchipelagoOoTClient.exe,0"; ValueType: string; ValueName: ""; Components: client/oot
|
||||||
Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot
|
Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{app}\ArchipelagoOoTClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/oot
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/red
|
Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/red
|
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/red
|
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/red
|
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn/blue
|
Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn/blue
|
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn/blue
|
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn/blue
|
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/pkmn
|
||||||
|
|
||||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
||||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
||||||
@@ -262,6 +271,9 @@ var SMWRomFilePage: TInputFileWizardPage;
|
|||||||
var soerom: string;
|
var soerom: string;
|
||||||
var SoERomFilePage: TInputFileWizardPage;
|
var SoERomFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
|
var l2acrom: string;
|
||||||
|
var L2ACROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
var ootrom: string;
|
var ootrom: string;
|
||||||
var OoTROMFilePage: TInputFileWizardPage;
|
var OoTROMFilePage: TInputFileWizardPage;
|
||||||
|
|
||||||
@@ -422,6 +434,8 @@ begin
|
|||||||
Result := not (SMWROMFilePage.Values[0] = '')
|
Result := not (SMWROMFilePage.Values[0] = '')
|
||||||
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
|
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
|
||||||
Result := not (SoEROMFilePage.Values[0] = '')
|
Result := not (SoEROMFilePage.Values[0] = '')
|
||||||
|
else if (assigned(L2ACROMFilePage)) and (CurPageID = L2ACROMFilePage.ID) then
|
||||||
|
Result := not (L2ACROMFilePage.Values[0] = '')
|
||||||
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
|
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
|
||||||
Result := not (OoTROMFilePage.Values[0] = '')
|
Result := not (OoTROMFilePage.Values[0] = '')
|
||||||
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
|
else if (assigned(ZlROMFilePage)) and (CurPageID = ZlROMFilePage.ID) then
|
||||||
@@ -526,6 +540,22 @@ begin
|
|||||||
Result := '';
|
Result := '';
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function GetL2ACROMPath(Param: string): string;
|
||||||
|
begin
|
||||||
|
if Length(l2acrom) > 0 then
|
||||||
|
Result := l2acrom
|
||||||
|
else if Assigned(L2ACROMFilePage) then
|
||||||
|
begin
|
||||||
|
R := CompareStr(GetSNESMD5OfFile(L2ACROMFilePage.Values[0]), '6efc477d6203ed2b3b9133c1cd9e9c5d')
|
||||||
|
if R <> 0 then
|
||||||
|
MsgBox('Lufia II ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||||
|
|
||||||
|
Result := L2ACROMFilePage.Values[0]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Result := '';
|
||||||
|
end;
|
||||||
|
|
||||||
function GetZlROMPath(Param: string): string;
|
function GetZlROMPath(Param: string): string;
|
||||||
begin
|
begin
|
||||||
if Length(zlrom) > 0 then
|
if Length(zlrom) > 0 then
|
||||||
@@ -609,6 +639,10 @@ begin
|
|||||||
bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b');
|
bluerom := CheckRom('Pokemon Blue (UE) [S][!].gb','50927e843568814f7ed45ec4f944bd8b');
|
||||||
if Length(bluerom) = 0 then
|
if Length(bluerom) = 0 then
|
||||||
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
|
BlueROMFilePage:= AddGBRomPage('Pokemon Blue (UE) [S][!].gb');
|
||||||
|
|
||||||
|
l2acrom := CheckRom('Lufia II - Rise of the Sinistrals (USA).sfc', '6efc477d6203ed2b3b9133c1cd9e9c5d');
|
||||||
|
if Length(l2acrom) = 0 then
|
||||||
|
L2ACROMFilePage:= AddRomPage('Lufia II - Rise of the Sinistrals (USA).sfc');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
@@ -623,6 +657,8 @@ begin
|
|||||||
Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3'));
|
Result := not (WizardIsComponentSelected('client/sni/dkc3') or WizardIsComponentSelected('generator/dkc3'));
|
||||||
if (assigned(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then
|
if (assigned(SMWROMFilePage)) and (PageID = SMWROMFilePage.ID) then
|
||||||
Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw'));
|
Result := not (WizardIsComponentSelected('client/sni/smw') or WizardIsComponentSelected('generator/smw'));
|
||||||
|
if (assigned(L2ACROMFilePage)) and (PageID = L2ACROMFilePage.ID) then
|
||||||
|
Result := not (WizardIsComponentSelected('client/sni/l2ac') or WizardIsComponentSelected('generator/l2ac'));
|
||||||
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
|
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
|
||||||
Result := not (WizardIsComponentSelected('generator/soe'));
|
Result := not (WizardIsComponentSelected('generator/soe'));
|
||||||
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
|
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
|
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
|
||||||
null:
|
null:
|
||||||
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
|
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
|
||||||
on: 0 # Force every player into progression balancing
|
normal: 0 # Force every player into default progression balancing
|
||||||
off: 0 # Force every player out of progression balancing, then prepare for a lot of logical BK
|
disabled: 0 # Force every player out of progression balancing, then prepare for a lot of logical BK
|
||||||
null: 1 # Let players decide via their own progression_balancing flag in their yaml, defaulting to on
|
null: 1 # Let players decide via their own progression_balancing setting in their yaml, defaulting to 50
|
||||||
A Link to the Past:
|
A Link to the Past:
|
||||||
goals:
|
goals:
|
||||||
ganon: 100 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
ganon: 100 # Climb GT, defeat Agahnim 2, and then kill Ganon
|
||||||
@@ -36,4 +36,4 @@ A Link to the Past:
|
|||||||
30: 50
|
30: 50
|
||||||
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
|
||||||
# Format "pieces: chance"
|
# Format "pieces: chance"
|
||||||
25: 50
|
25: 50
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ A Link to the Past:
|
|||||||
open_pyramid:
|
open_pyramid:
|
||||||
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
|
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
|
||||||
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
|
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
|
||||||
yes: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
|
open: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
|
||||||
no: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
|
closed: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
|
||||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||||
|
|||||||
46
setup.py
46
setup.py
@@ -1,18 +1,27 @@
|
|||||||
|
import base64
|
||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
import platform
|
|
||||||
from pathlib import Path
|
|
||||||
from hashlib import sha3_512
|
|
||||||
import base64
|
|
||||||
import datetime
|
|
||||||
from Utils import version_tuple, is_windows, is_linux
|
|
||||||
from collections.abc import Iterable
|
|
||||||
import typing
|
import typing
|
||||||
import setuptools
|
import zipfile
|
||||||
from Launcher import components, icon_paths
|
from collections.abc import Iterable
|
||||||
|
from hashlib import sha3_512
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
from Launcher import components, icon_paths
|
||||||
|
from Utils import version_tuple, is_windows, is_linux
|
||||||
|
|
||||||
|
# On Python < 3.10 LogicMixin is not currently supported.
|
||||||
|
apworlds: set = {
|
||||||
|
"Subnautica",
|
||||||
|
"Factorio",
|
||||||
|
"Rogue Legacy",
|
||||||
|
}
|
||||||
|
|
||||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -185,11 +194,26 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
|||||||
from WebHostLib.options import create
|
from WebHostLib.options import create
|
||||||
create()
|
create()
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
assert not apworlds - set(AutoWorldRegister.world_types), "Unknown world designated for .apworld"
|
||||||
|
folders_to_remove: typing.List[str] = []
|
||||||
for worldname, worldtype in AutoWorldRegister.world_types.items():
|
for worldname, worldtype in AutoWorldRegister.world_types.items():
|
||||||
if not worldtype.hidden:
|
if not worldtype.hidden:
|
||||||
file_name = worldname+".yaml"
|
file_name = worldname+".yaml"
|
||||||
shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name),
|
shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name),
|
||||||
self.buildfolder / "Players" / "Templates" / file_name)
|
self.buildfolder / "Players" / "Templates" / file_name)
|
||||||
|
if worldname in apworlds:
|
||||||
|
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
|
||||||
|
world_directory = self.libfolder / "worlds" / file_name
|
||||||
|
# this method creates an apworld that cannot be moved to a different OS or minor python version,
|
||||||
|
# which should be ok
|
||||||
|
with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED,
|
||||||
|
compresslevel=9) as zf:
|
||||||
|
entry: os.DirEntry
|
||||||
|
for path in world_directory.rglob("*.*"):
|
||||||
|
relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:])
|
||||||
|
zf.write(path, relative_path)
|
||||||
|
folders_to_remove.append(file_name)
|
||||||
|
shutil.rmtree(world_directory)
|
||||||
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
|
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
|
||||||
# TODO: fix LttP options one day
|
# TODO: fix LttP options one day
|
||||||
shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml")
|
shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml")
|
||||||
@@ -218,9 +242,13 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
|||||||
self.create_manifest()
|
self.create_manifest()
|
||||||
|
|
||||||
if is_windows:
|
if is_windows:
|
||||||
|
# Inno setup stuff
|
||||||
with open("setup.ini", "w") as f:
|
with open("setup.ini", "w") as f:
|
||||||
min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000"
|
min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000"
|
||||||
f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n")
|
f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n")
|
||||||
|
with open("installdelete.iss", "w") as f:
|
||||||
|
f.writelines("Type: filesandordirs; Name: \"{app}\\lib\\worlds\\"+world_directory+"\"\n"
|
||||||
|
for world_directory in folders_to_remove)
|
||||||
else:
|
else:
|
||||||
# make sure extra programs are executable
|
# make sure extra programs are executable
|
||||||
enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
|
enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core'
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
import typing
|
||||||
import unittest
|
import unittest
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
|
from test.general import gen_steps
|
||||||
|
from worlds import AutoWorld
|
||||||
|
from worlds.AutoWorld import call_all
|
||||||
|
|
||||||
file_path = pathlib.Path(__file__).parent.parent
|
file_path = pathlib.Path(__file__).parent.parent
|
||||||
Utils.local_path.cached_path = file_path
|
Utils.local_path.cached_path = file_path
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, CollectionState, ItemClassification
|
from BaseClasses import MultiWorld, CollectionState, ItemClassification, Item
|
||||||
from worlds.alttp.Items import ItemFactory
|
from worlds.alttp.Items import ItemFactory
|
||||||
|
|
||||||
|
|
||||||
@@ -92,3 +97,94 @@ class TestBase(unittest.TestCase):
|
|||||||
new_items.remove(missing_item)
|
new_items.remove(missing_item)
|
||||||
items = ItemFactory(new_items, 1)
|
items = ItemFactory(new_items, 1)
|
||||||
return self.get_state(items)
|
return self.get_state(items)
|
||||||
|
|
||||||
|
|
||||||
|
class WorldTestBase(unittest.TestCase):
|
||||||
|
options: typing.Dict[str, typing.Any] = {}
|
||||||
|
multiworld: MultiWorld
|
||||||
|
|
||||||
|
game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
|
||||||
|
auto_construct: typing.ClassVar[bool] = True
|
||||||
|
""" automatically set up a world for each test in this class """
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
if self.auto_construct:
|
||||||
|
self.world_setup()
|
||||||
|
|
||||||
|
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
||||||
|
if not hasattr(self, "game"):
|
||||||
|
raise NotImplementedError("didn't define game name")
|
||||||
|
self.multiworld = MultiWorld(1)
|
||||||
|
self.multiworld.game[1] = self.game
|
||||||
|
self.multiworld.player_name = {1: "Tester"}
|
||||||
|
self.multiworld.set_seed(seed)
|
||||||
|
args = Namespace()
|
||||||
|
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items():
|
||||||
|
setattr(args, name, {
|
||||||
|
1: option.from_any(self.options.get(name, getattr(option, "default")))
|
||||||
|
})
|
||||||
|
self.multiworld.set_options(args)
|
||||||
|
self.multiworld.set_default_common_options()
|
||||||
|
for step in gen_steps:
|
||||||
|
call_all(self.multiworld, step)
|
||||||
|
|
||||||
|
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None:
|
||||||
|
if isinstance(item_names, str):
|
||||||
|
item_names = (item_names,)
|
||||||
|
for item in self.multiworld.get_items():
|
||||||
|
if item.name not in item_names:
|
||||||
|
self.multiworld.state.collect(item)
|
||||||
|
|
||||||
|
def get_item_by_name(self, item_name: str) -> Item:
|
||||||
|
for item in self.multiworld.get_items():
|
||||||
|
if item.name == item_name:
|
||||||
|
return item
|
||||||
|
raise ValueError("No such item")
|
||||||
|
|
||||||
|
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||||
|
if isinstance(item_names, str):
|
||||||
|
item_names = (item_names,)
|
||||||
|
return [item for item in self.multiworld.itempool if item.name in item_names]
|
||||||
|
|
||||||
|
def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
||||||
|
""" collect all of the items in the item pool that have the given names """
|
||||||
|
items = self.get_items_by_name(item_names)
|
||||||
|
self.collect(items)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||||
|
if isinstance(items, Item):
|
||||||
|
items = (items,)
|
||||||
|
for item in items:
|
||||||
|
self.multiworld.state.collect(item)
|
||||||
|
|
||||||
|
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
||||||
|
if isinstance(items, Item):
|
||||||
|
items = (items,)
|
||||||
|
for item in items:
|
||||||
|
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
||||||
|
self.multiworld.state.events.remove(item.location)
|
||||||
|
self.multiworld.state.remove(item)
|
||||||
|
|
||||||
|
def can_reach_location(self, location: str) -> bool:
|
||||||
|
return self.multiworld.state.can_reach(location, "Location", 1)
|
||||||
|
|
||||||
|
def count(self, item_name: str) -> int:
|
||||||
|
return self.multiworld.state.count(item_name, 1)
|
||||||
|
|
||||||
|
def assertAccessDependency(self,
|
||||||
|
locations: typing.List[str],
|
||||||
|
possible_items: typing.Iterable[typing.Iterable[str]]) -> None:
|
||||||
|
all_items = [item_name for item_names in possible_items for item_name in item_names]
|
||||||
|
|
||||||
|
self.collect_all_but(all_items)
|
||||||
|
for location in self.multiworld.get_locations():
|
||||||
|
self.assertEqual(self.multiworld.state.can_reach(location), location.name not in locations)
|
||||||
|
for item_names in possible_items:
|
||||||
|
items = self.collect_by_name(item_names)
|
||||||
|
for location in locations:
|
||||||
|
self.assertTrue(self.can_reach_location(location))
|
||||||
|
self.remove(items)
|
||||||
|
|
||||||
|
def assertBeatable(self, beatable: bool):
|
||||||
|
self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
import warnings
|
import warnings
|
||||||
warnings.simplefilter("always")
|
warnings.simplefilter("always")
|
||||||
|
|
||||||
|
|||||||
25
test/general/TestHostYAML.py
Normal file
25
test/general/TestHostYAML.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import Utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestIDs(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
with open(Utils.local_path("host.yaml")) as f:
|
||||||
|
cls.yaml_options = Utils.parse_yaml(f.read())
|
||||||
|
|
||||||
|
def testUtilsHasHost(self):
|
||||||
|
for option_key, option_set in Utils.get_default_options().items():
|
||||||
|
with self.subTest(option_key):
|
||||||
|
self.assertIn(option_key, self.yaml_options)
|
||||||
|
for sub_option_key in option_set:
|
||||||
|
self.assertIn(sub_option_key, self.yaml_options[option_key])
|
||||||
|
|
||||||
|
def testHostHasUtils(self):
|
||||||
|
utils_options = Utils.get_default_options()
|
||||||
|
for option_key, option_set in self.yaml_options.items():
|
||||||
|
with self.subTest(option_key):
|
||||||
|
self.assertIn(option_key, utils_options)
|
||||||
|
for sub_option_key in option_set:
|
||||||
|
self.assertIn(sub_option_key, utils_options[option_key])
|
||||||
@@ -9,14 +9,13 @@ import os
|
|||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
ModuleUpdate.update_ran = True # don't upgrade
|
ModuleUpdate.update_ran = True # don't upgrade
|
||||||
import Generate
|
import Generate
|
||||||
import Utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestGenerateMain(unittest.TestCase):
|
class TestGenerateMain(unittest.TestCase):
|
||||||
"""This tests Generate.py (ArchipelagoGenerate.exe) main"""
|
"""This tests Generate.py (ArchipelagoGenerate.exe) main"""
|
||||||
|
|
||||||
generate_dir = Path(Generate.__file__).parent
|
generate_dir = Path(Generate.__file__).parent
|
||||||
run_dir = generate_dir / 'test' # reproducible cwd that's neither __file__ nor Generate.__file__
|
run_dir = generate_dir / "test" # reproducible cwd that's neither __file__ nor Generate.__file__
|
||||||
abs_input_dir = Path(__file__).parent / 'data' / 'OnePlayer'
|
abs_input_dir = Path(__file__).parent / 'data' / 'OnePlayer'
|
||||||
rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
|
rel_input_dir = abs_input_dir.relative_to(run_dir) # directly supplied relative paths are relative to cwd
|
||||||
yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
|
yaml_input_dir = abs_input_dir.relative_to(generate_dir) # yaml paths are relative to user_path
|
||||||
@@ -30,12 +29,29 @@ class TestGenerateMain(unittest.TestCase):
|
|||||||
f"{list(output_path.glob('*'))}")
|
f"{list(output_path.glob('*'))}")
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
Utils.local_path.cached_path = str(self.generate_dir)
|
self.original_argv = sys.argv.copy()
|
||||||
|
self.original_cwd = os.getcwd()
|
||||||
|
self.original_local_path = Generate.Utils.local_path.cached_path
|
||||||
|
self.original_user_path = Generate.Utils.user_path.cached_path
|
||||||
|
|
||||||
|
# Force both user_path and local_path to a specific path. They have independent caches.
|
||||||
|
Generate.Utils.user_path.cached_path = Generate.Utils.local_path.cached_path = str(self.generate_dir)
|
||||||
os.chdir(self.run_dir)
|
os.chdir(self.run_dir)
|
||||||
self.output_tempdir = TemporaryDirectory(prefix='AP_out_')
|
self.output_tempdir = TemporaryDirectory(prefix='AP_out_')
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.output_tempdir.cleanup()
|
self.output_tempdir.cleanup()
|
||||||
|
os.chdir(self.original_cwd)
|
||||||
|
sys.argv = self.original_argv
|
||||||
|
Generate.Utils.local_path.cached_path = self.original_local_path
|
||||||
|
Generate.Utils.user_path.cached_path = self.original_user_path
|
||||||
|
|
||||||
|
def test_paths(self):
|
||||||
|
self.assertTrue(os.path.exists(self.generate_dir))
|
||||||
|
self.assertTrue(os.path.exists(self.run_dir))
|
||||||
|
self.assertTrue(os.path.exists(self.abs_input_dir))
|
||||||
|
self.assertTrue(os.path.exists(self.rel_input_dir))
|
||||||
|
self.assertFalse(os.path.exists(self.yaml_input_dir)) # relative to user_path, not cwd
|
||||||
|
|
||||||
def test_generate_absolute(self):
|
def test_generate_absolute(self):
|
||||||
sys.argv = [sys.argv[0], '--seed', '0',
|
sys.argv = [sys.argv[0], '--seed', '0',
|
||||||
@@ -57,7 +73,7 @@ class TestGenerateMain(unittest.TestCase):
|
|||||||
|
|
||||||
def test_generate_yaml(self):
|
def test_generate_yaml(self):
|
||||||
# override host.yaml
|
# override host.yaml
|
||||||
defaults = Utils.get_options()["generator"]
|
defaults = Generate.Utils.get_options()["generator"]
|
||||||
defaults["player_files_path"] = str(self.yaml_input_dir)
|
defaults["player_files_path"] = str(self.yaml_input_dir)
|
||||||
defaults["players"] = 0
|
defaults["players"] = 0
|
||||||
|
|
||||||
|
|||||||
40
test/webhost/TestAPIGenerate.py
Normal file
40
test/webhost/TestAPIGenerate.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class TestDocs(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
from WebHost import get_app, raw_app
|
||||||
|
raw_app.config["PONY"] = {
|
||||||
|
"provider": "sqlite",
|
||||||
|
"filename": ":memory:",
|
||||||
|
"create_db": True,
|
||||||
|
}
|
||||||
|
app = get_app()
|
||||||
|
app.config.update({
|
||||||
|
"TESTING": True,
|
||||||
|
})
|
||||||
|
cls.client = app.test_client()
|
||||||
|
|
||||||
|
def testCorrectErrorEmptyRequest(self):
|
||||||
|
response = self.client.post("/api/generate")
|
||||||
|
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
|
||||||
|
|
||||||
|
def testGenerationQueued(self):
|
||||||
|
options = {
|
||||||
|
"Tester1":
|
||||||
|
{
|
||||||
|
"game": "Archipelago",
|
||||||
|
"name": "Tester",
|
||||||
|
"Archipelago": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/generate",
|
||||||
|
data=json.dumps({"weights": options}),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
json_data = response.get_json()
|
||||||
|
self.assertTrue(json_data["text"].startswith("Generation of seed "))
|
||||||
|
self.assertTrue(json_data["text"].endswith(" started successfully."))
|
||||||
@@ -7,8 +7,9 @@ from worlds.AutoWorld import AutoWorldRegister
|
|||||||
|
|
||||||
|
|
||||||
class TestDocs(unittest.TestCase):
|
class TestDocs(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
@classmethod
|
||||||
self.tutorials_data = WebHost.create_ordered_tutorials_file()
|
def setUpClass(cls) -> None:
|
||||||
|
cls.tutorials_data = WebHost.create_ordered_tutorials_file()
|
||||||
|
|
||||||
def testHasTutorial(self):
|
def testHasTutorial(self):
|
||||||
games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
|
games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import WebHost
|
|||||||
|
|
||||||
|
|
||||||
class TestFileGeneration(unittest.TestCase):
|
class TestFileGeneration(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
@classmethod
|
||||||
self.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib")
|
def setUpClass(cls) -> None:
|
||||||
|
cls.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib")
|
||||||
# should not create the folder *here*
|
# should not create the folder *here*
|
||||||
self.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
cls.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib")
|
||||||
|
|
||||||
def testOptions(self):
|
def testOptions(self):
|
||||||
WebHost.create_options_files()
|
WebHost.create_options_files()
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
def load_tests(loader, standard_tests, pattern):
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from ..TestBase import file_path
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
|
suite = unittest.TestSuite()
|
||||||
|
suite.addTests(standard_tests)
|
||||||
|
folders = [os.path.join(os.path.split(world.__file__)[0], "test")
|
||||||
|
for world in AutoWorldRegister.world_types.values()]
|
||||||
|
for folder in folders:
|
||||||
|
if os.path.exists(folder):
|
||||||
|
suite.addTests(loader.discover(folder, top_level_dir=file_path))
|
||||||
|
return suite
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import typing
|
|
||||||
import unittest
|
|
||||||
from argparse import Namespace
|
|
||||||
from test.general import gen_steps
|
|
||||||
from BaseClasses import MultiWorld, Item
|
|
||||||
from worlds import AutoWorld
|
|
||||||
from worlds.AutoWorld import call_all
|
|
||||||
|
|
||||||
|
|
||||||
class WorldTestBase(unittest.TestCase):
|
|
||||||
options: typing.Dict[str, typing.Any] = {}
|
|
||||||
multiworld: MultiWorld
|
|
||||||
|
|
||||||
game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore"
|
|
||||||
auto_construct: typing.ClassVar[bool] = True
|
|
||||||
""" automatically set up a world for each test in this class """
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
if self.auto_construct:
|
|
||||||
self.world_setup()
|
|
||||||
|
|
||||||
def world_setup(self, seed: typing.Optional[int] = None) -> None:
|
|
||||||
if not hasattr(self, "game"):
|
|
||||||
raise NotImplementedError("didn't define game name")
|
|
||||||
self.multiworld = MultiWorld(1)
|
|
||||||
self.multiworld.game[1] = self.game
|
|
||||||
self.multiworld.player_name = {1: "Tester"}
|
|
||||||
self.multiworld.set_seed(seed)
|
|
||||||
args = Namespace()
|
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].option_definitions.items():
|
|
||||||
setattr(args, name, {
|
|
||||||
1: option.from_any(self.options.get(name, getattr(option, "default")))
|
|
||||||
})
|
|
||||||
self.multiworld.set_options(args)
|
|
||||||
self.multiworld.set_default_common_options()
|
|
||||||
for step in gen_steps:
|
|
||||||
call_all(self.multiworld, step)
|
|
||||||
|
|
||||||
def collect_all_but(self, item_names: typing.Union[str, typing.Iterable[str]]) -> None:
|
|
||||||
if isinstance(item_names, str):
|
|
||||||
item_names = (item_names,)
|
|
||||||
for item in self.multiworld.get_items():
|
|
||||||
if item.name not in item_names:
|
|
||||||
self.multiworld.state.collect(item)
|
|
||||||
|
|
||||||
def get_item_by_name(self, item_name: str) -> Item:
|
|
||||||
for item in self.multiworld.get_items():
|
|
||||||
if item.name == item_name:
|
|
||||||
return item
|
|
||||||
raise ValueError("No such item")
|
|
||||||
|
|
||||||
def get_items_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
|
||||||
if isinstance(item_names, str):
|
|
||||||
item_names = (item_names,)
|
|
||||||
return [item for item in self.multiworld.itempool if item.name in item_names]
|
|
||||||
|
|
||||||
def collect_by_name(self, item_names: typing.Union[str, typing.Iterable[str]]) -> typing.List[Item]:
|
|
||||||
""" collect all of the items in the item pool that have the given names """
|
|
||||||
items = self.get_items_by_name(item_names)
|
|
||||||
self.collect(items)
|
|
||||||
return items
|
|
||||||
|
|
||||||
def collect(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
|
||||||
if isinstance(items, Item):
|
|
||||||
items = (items,)
|
|
||||||
for item in items:
|
|
||||||
self.multiworld.state.collect(item)
|
|
||||||
|
|
||||||
def remove(self, items: typing.Union[Item, typing.Iterable[Item]]) -> None:
|
|
||||||
if isinstance(items, Item):
|
|
||||||
items = (items,)
|
|
||||||
for item in items:
|
|
||||||
if item.location and item.location.event and item.location in self.multiworld.state.events:
|
|
||||||
self.multiworld.state.events.remove(item.location)
|
|
||||||
self.multiworld.state.remove(item)
|
|
||||||
|
|
||||||
def can_reach_location(self, location: str) -> bool:
|
|
||||||
return self.multiworld.state.can_reach(location, "Location", 1)
|
|
||||||
|
|
||||||
def count(self, item_name: str) -> int:
|
|
||||||
return self.multiworld.state.count(item_name, 1)
|
|
||||||
|
|
||||||
def assertAccessDependency(self,
|
|
||||||
locations: typing.List[str],
|
|
||||||
possible_items: typing.Iterable[typing.Iterable[str]]) -> None:
|
|
||||||
all_items = [item_name for item_names in possible_items for item_name in item_names]
|
|
||||||
|
|
||||||
self.collect_all_but(all_items)
|
|
||||||
for location in self.multiworld.get_locations():
|
|
||||||
self.assertEqual(self.multiworld.state.can_reach(location), location.name not in locations)
|
|
||||||
for item_names in possible_items:
|
|
||||||
items = self.collect_by_name(item_names)
|
|
||||||
for location in locations:
|
|
||||||
self.assertTrue(self.can_reach_location(location))
|
|
||||||
self.remove(items)
|
|
||||||
|
|
||||||
def assertBeatable(self, beatable: bool):
|
|
||||||
self.assertEqual(self.multiworld.can_beat_game(self.multiworld.state), beatable)
|
|
||||||
@@ -3,7 +3,8 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import pathlib
|
import pathlib
|
||||||
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING
|
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING, \
|
||||||
|
ClassVar
|
||||||
|
|
||||||
from Options import AssembleOptions
|
from Options import AssembleOptions
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
@@ -71,39 +72,39 @@ class AutoLogicRegister(type):
|
|||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
||||||
method = getattr(world.worlds[player], method_name)
|
method = getattr(multiworld.worlds[player], method_name)
|
||||||
return method(*args)
|
return method(*args)
|
||||||
|
|
||||||
|
|
||||||
def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None:
|
def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||||
world_types: Set[AutoWorldRegister] = set()
|
world_types: Set[AutoWorldRegister] = set()
|
||||||
for player in world.player_ids:
|
for player in multiworld.player_ids:
|
||||||
prev_item_count = len(world.itempool)
|
prev_item_count = len(multiworld.itempool)
|
||||||
world_types.add(world.worlds[player].__class__)
|
world_types.add(multiworld.worlds[player].__class__)
|
||||||
call_single(world, method_name, player, *args)
|
call_single(multiworld, method_name, player, *args)
|
||||||
if __debug__:
|
if __debug__:
|
||||||
new_items = world.itempool[prev_item_count:]
|
new_items = multiworld.itempool[prev_item_count:]
|
||||||
for i, item in enumerate(new_items):
|
for i, item in enumerate(new_items):
|
||||||
for other in new_items[i+1:]:
|
for other in new_items[i+1:]:
|
||||||
assert item is not other, (
|
assert item is not other, (
|
||||||
f"Duplicate item reference of \"{item.name}\" in \"{world.worlds[player].game}\" "
|
f"Duplicate item reference of \"{item.name}\" in \"{multiworld.worlds[player].game}\" "
|
||||||
f"of player \"{world.player_name[player]}\". Please make a copy instead.")
|
f"of player \"{multiworld.player_name[player]}\". Please make a copy instead.")
|
||||||
|
|
||||||
# TODO: investigate: Iterating through a set is not a deterministic order.
|
# TODO: investigate: Iterating through a set is not a deterministic order.
|
||||||
# If any random is used, this could make unreproducible seed.
|
# If any random is used, this could make unreproducible seed.
|
||||||
for world_type in world_types:
|
for world_type in world_types:
|
||||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||||
if stage_callable:
|
if stage_callable:
|
||||||
stage_callable(world, *args)
|
stage_callable(multiworld, *args)
|
||||||
|
|
||||||
|
|
||||||
def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None:
|
def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||||
world_types = {world.worlds[player].__class__ for player in world.player_ids}
|
world_types = {multiworld.worlds[player].__class__ for player in multiworld.player_ids}
|
||||||
for world_type in world_types:
|
for world_type in world_types:
|
||||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||||
if stage_callable:
|
if stage_callable:
|
||||||
stage_callable(world, *args)
|
stage_callable(multiworld, *args)
|
||||||
|
|
||||||
|
|
||||||
class WebWorld:
|
class WebWorld:
|
||||||
@@ -130,24 +131,24 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||||
A Game should have its own subclass of World in which it defines the required data structures."""
|
A Game should have its own subclass of World in which it defines the required data structures."""
|
||||||
|
|
||||||
option_definitions: Dict[str, AssembleOptions] = {} # link your Options mapping
|
option_definitions: ClassVar[Dict[str, AssembleOptions]] = {} # link your Options mapping
|
||||||
game: str # name the game
|
game: ClassVar[str] # name the game
|
||||||
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
topology_present: ClassVar[bool] = False # indicate if world type has any meaningful layout/pathing
|
||||||
|
|
||||||
# gets automatically populated with all item and item group names
|
# gets automatically populated with all item and item group names
|
||||||
all_item_and_group_names: FrozenSet[str] = frozenset()
|
all_item_and_group_names: ClassVar[FrozenSet[str]] = frozenset()
|
||||||
|
|
||||||
# map names to their IDs
|
# map names to their IDs
|
||||||
item_name_to_id: Dict[str, int] = {}
|
item_name_to_id: ClassVar[Dict[str, int]] = {}
|
||||||
location_name_to_id: Dict[str, int] = {}
|
location_name_to_id: ClassVar[Dict[str, int]] = {}
|
||||||
|
|
||||||
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
|
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
|
||||||
item_name_groups: Dict[str, Set[str]] = {}
|
item_name_groups: ClassVar[Dict[str, Set[str]]] = {}
|
||||||
|
|
||||||
# increment this every time something in your world's names/id mappings changes.
|
# increment this every time something in your world's names/id mappings changes.
|
||||||
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||||
# retrieved by clients on every connection.
|
# retrieved by clients on every connection.
|
||||||
data_version: int = 1
|
data_version: ClassVar[int] = 1
|
||||||
|
|
||||||
# override this if changes to a world break forward-compatibility of the client
|
# override this if changes to a world break forward-compatibility of the client
|
||||||
# The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
|
# The base version of (0, 1, 6) is provided for backwards compatibility and does *not* need to be updated in the
|
||||||
@@ -157,46 +158,34 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
# update this if the resulting multidata breaks forward-compatibility of the server
|
# update this if the resulting multidata breaks forward-compatibility of the server
|
||||||
required_server_version: Tuple[int, int, int] = (0, 2, 4)
|
required_server_version: Tuple[int, int, int] = (0, 2, 4)
|
||||||
|
|
||||||
hint_blacklist: FrozenSet[str] = frozenset() # any names that should not be hintable
|
hint_blacklist: ClassVar[FrozenSet[str]] = frozenset() # any names that should not be hintable
|
||||||
|
|
||||||
# NOTE: remote_items and remote_start_inventory are now available in the network protocol for the client to set.
|
|
||||||
# These values will be removed.
|
|
||||||
# if a world is set to remote_items, then it just needs to send location checks to the server and the server
|
|
||||||
# sends back the items
|
|
||||||
# if a world is set to remote_items = False, then the server never sends an item where receiver == finder,
|
|
||||||
# the client finds its own items in its own world.
|
|
||||||
remote_items: bool = True
|
|
||||||
|
|
||||||
# If remote_start_inventory is true, the start_inventory/world.precollected_items is sent on connection,
|
|
||||||
# otherwise the world implementation is in charge of writing the items to their output data.
|
|
||||||
remote_start_inventory: bool = True
|
|
||||||
|
|
||||||
# For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
|
# For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
|
||||||
# this forces forfeit: auto for those games.
|
# this forces forfeit: auto for those games.
|
||||||
forced_auto_forfeit: bool = False
|
forced_auto_forfeit: bool = False
|
||||||
|
|
||||||
# Hide World Type from various views. Does not remove functionality.
|
# Hide World Type from various views. Does not remove functionality.
|
||||||
hidden: bool = False
|
hidden: ClassVar[bool] = False
|
||||||
|
|
||||||
# see WebWorld for options
|
# see WebWorld for options
|
||||||
web: WebWorld = WebWorld()
|
web: ClassVar[WebWorld] = WebWorld()
|
||||||
|
|
||||||
# autoset on creation:
|
# autoset on creation:
|
||||||
multiworld: "MultiWorld"
|
multiworld: "MultiWorld"
|
||||||
player: int
|
player: int
|
||||||
|
|
||||||
# automatically generated
|
# automatically generated
|
||||||
item_id_to_name: Dict[int, str]
|
item_id_to_name: ClassVar[Dict[int, str]]
|
||||||
location_id_to_name: Dict[int, str]
|
location_id_to_name: ClassVar[Dict[int, str]]
|
||||||
|
|
||||||
item_names: Set[str] # set of all potential item names
|
item_names: ClassVar[Set[str]] # set of all potential item names
|
||||||
location_names: Set[str] # set of all potential location names
|
location_names: ClassVar[Set[str]] # set of all potential location names
|
||||||
|
|
||||||
zip_path: Optional[pathlib.Path] = None # If loaded from a .apworld, this is the Path to it.
|
zip_path: ClassVar[Optional[pathlib.Path]] = None # If loaded from a .apworld, this is the Path to it.
|
||||||
__file__: str # path it was loaded from
|
__file__: ClassVar[str] # path it was loaded from
|
||||||
|
|
||||||
def __init__(self, world: "MultiWorld", player: int):
|
def __init__(self, multiworld: "MultiWorld", player: int):
|
||||||
self.multiworld = world
|
self.multiworld = multiworld
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
# overridable methods that get called by Main.py, sorted by execution order
|
# overridable methods that get called by Main.py, sorted by execution order
|
||||||
@@ -250,7 +239,10 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
|
def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
|
||||||
"""Fill in the `slot_data` field in the `Connected` network package.
|
"""Fill in the `slot_data` field in the `Connected` network package.
|
||||||
This is a way the generator can give custom data to the client.
|
This is a way the generator can give custom data to the client.
|
||||||
The client will receive this as JSON in the `Connected` response."""
|
The client will receive this as JSON in the `Connected` response.
|
||||||
|
|
||||||
|
The generation does not wait for `generate_output` to complete before calling this.
|
||||||
|
`threading.Event` can be used if you need to wait for something from `generate_output`."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ if typing.TYPE_CHECKING:
|
|||||||
from .AutoWorld import World
|
from .AutoWorld import World
|
||||||
|
|
||||||
|
|
||||||
|
class GamesPackage(typing.TypedDict):
|
||||||
|
item_name_to_id: typing.Dict[str, int]
|
||||||
|
location_name_to_id: typing.Dict[str, int]
|
||||||
|
version: int
|
||||||
|
|
||||||
|
|
||||||
|
class DataPackage(typing.TypedDict):
|
||||||
|
version: int
|
||||||
|
games: typing.Dict[str, GamesPackage]
|
||||||
|
|
||||||
|
|
||||||
class WorldSource(typing.NamedTuple):
|
class WorldSource(typing.NamedTuple):
|
||||||
path: str # typically relative path from this module
|
path: str # typically relative path from this module
|
||||||
is_zip: bool = False
|
is_zip: bool = False
|
||||||
@@ -41,20 +52,26 @@ world_sources.sort()
|
|||||||
for world_source in world_sources:
|
for world_source in world_sources:
|
||||||
if world_source.is_zip:
|
if world_source.is_zip:
|
||||||
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
||||||
spec = importer.find_spec(world_source.path.split(".", 1)[0])
|
if hasattr(importer, "find_spec"): # new in Python 3.10
|
||||||
mod = importlib.util.module_from_spec(spec)
|
spec = importer.find_spec(world_source.path.split(".", 1)[0])
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
else: # TODO: remove with 3.8 support
|
||||||
|
mod = importer.load_module(world_source.path.split(".", 1)[0])
|
||||||
|
|
||||||
mod.__package__ = f"worlds.{mod.__package__}"
|
mod.__package__ = f"worlds.{mod.__package__}"
|
||||||
mod.__name__ = f"worlds.{mod.__name__}"
|
mod.__name__ = f"worlds.{mod.__name__}"
|
||||||
sys.modules[mod.__name__] = mod
|
sys.modules[mod.__name__] = mod
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
||||||
importer.exec_module(mod)
|
# Found no equivalent for < 3.10
|
||||||
|
if hasattr(importer, "exec_module"):
|
||||||
|
importer.exec_module(mod)
|
||||||
else:
|
else:
|
||||||
importlib.import_module(f".{world_source.path}", "worlds")
|
importlib.import_module(f".{world_source.path}", "worlds")
|
||||||
|
|
||||||
lookup_any_item_id_to_name = {}
|
lookup_any_item_id_to_name = {}
|
||||||
lookup_any_location_id_to_name = {}
|
lookup_any_location_id_to_name = {}
|
||||||
games = {}
|
games: typing.Dict[str, GamesPackage] = {}
|
||||||
|
|
||||||
from .AutoWorld import AutoWorldRegister
|
from .AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
@@ -69,14 +86,12 @@ for world_name, world in AutoWorldRegister.world_types.items():
|
|||||||
lookup_any_item_id_to_name.update(world.item_id_to_name)
|
lookup_any_item_id_to_name.update(world.item_id_to_name)
|
||||||
lookup_any_location_id_to_name.update(world.location_id_to_name)
|
lookup_any_location_id_to_name.update(world.location_id_to_name)
|
||||||
|
|
||||||
network_data_package = {
|
network_data_package: DataPackage = {
|
||||||
"version": sum(world.data_version for world in AutoWorldRegister.world_types.values()),
|
|
||||||
"games": games,
|
"games": games,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set entire datapackage to version 0 if any of them are set to 0
|
# Set entire datapackage to version 0 if any of them are set to 0
|
||||||
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
|
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
|
||||||
network_data_package["version"] = 0
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.warning(f"Datapackage is in custom mode. Custom Worlds: "
|
logging.warning(f"Datapackage is in custom mode. Custom Worlds: "
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ def get_dungeon_item_pool_player(world, player) -> typing.List:
|
|||||||
return [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
|
return [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
|
||||||
|
|
||||||
|
|
||||||
|
def get_unfilled_dungeon_locations(multiworld) -> typing.List:
|
||||||
|
return [location for location in multiworld.get_locations() if not location.item and location.parent_region.dungeon]
|
||||||
|
|
||||||
|
|
||||||
def fill_dungeons_restrictive(world):
|
def fill_dungeons_restrictive(world):
|
||||||
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
|
||||||
localized: set = set()
|
localized: set = set()
|
||||||
@@ -134,7 +138,7 @@ def fill_dungeons_restrictive(world):
|
|||||||
if in_dungeon_items:
|
if in_dungeon_items:
|
||||||
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if
|
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if
|
||||||
restricted}
|
restricted}
|
||||||
locations = [location for location in world.get_unfilled_dungeon_locations()
|
locations = [location for location in get_unfilled_dungeon_locations(world)
|
||||||
# filter boss
|
# filter boss
|
||||||
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
|
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
|
||||||
if dungeon_specific:
|
if dungeon_specific:
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ class OpenPyramid(Choice):
|
|||||||
option_auto = 3
|
option_auto = 3
|
||||||
default = option_goal
|
default = option_goal
|
||||||
|
|
||||||
alias_yes = option_open
|
alias_true = option_open
|
||||||
alias_no = option_closed
|
alias_false = option_closed
|
||||||
|
|
||||||
def to_bool(self, world: MultiWorld, player: int) -> bool:
|
def to_bool(self, world: MultiWorld, player: int) -> bool:
|
||||||
if self.value == self.option_goal:
|
if self.value == self.option_goal:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
import worlds.AutoWorld
|
|
||||||
import worlds.Files
|
import worlds.Files
|
||||||
|
|
||||||
LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
|
LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
|
||||||
@@ -17,7 +16,6 @@ import random
|
|||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import xxtea
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import bsdiff4
|
import bsdiff4
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
@@ -39,7 +37,6 @@ from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to
|
|||||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
|
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||||
from worlds.alttp.EntranceShuffle import door_addresses
|
from worlds.alttp.EntranceShuffle import door_addresses
|
||||||
from worlds.alttp.Options import smallkey_shuffle
|
from worlds.alttp.Options import smallkey_shuffle
|
||||||
import Patch
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from maseya import z3pr
|
from maseya import z3pr
|
||||||
@@ -47,6 +44,11 @@ try:
|
|||||||
except:
|
except:
|
||||||
z3pr = None
|
z3pr = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import xxtea
|
||||||
|
except:
|
||||||
|
xxtea = None
|
||||||
|
|
||||||
enemizer_logger = logging.getLogger("Enemizer")
|
enemizer_logger = logging.getLogger("Enemizer")
|
||||||
|
|
||||||
|
|
||||||
@@ -85,6 +87,11 @@ class LocalRom(object):
|
|||||||
self.write_bytes(startaddress + i, bytearray(data))
|
self.write_bytes(startaddress + i, bytearray(data))
|
||||||
|
|
||||||
def encrypt(self, world, player):
|
def encrypt(self, world, player):
|
||||||
|
global xxtea
|
||||||
|
if xxtea is None:
|
||||||
|
# cause crash to provide traceback
|
||||||
|
import xxtea
|
||||||
|
|
||||||
local_random = world.slot_seeds[player]
|
local_random = world.slot_seeds[player]
|
||||||
key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big'))
|
key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big'))
|
||||||
self.write_bytes(0x1800B0, bytearray(key))
|
self.write_bytes(0x1800B0, bytearray(key))
|
||||||
@@ -788,11 +795,11 @@ def patch_rom(world, rom, player, enemized):
|
|||||||
itemid = 0x33
|
itemid = 0x33
|
||||||
elif location.item.compass:
|
elif location.item.compass:
|
||||||
itemid = 0x25
|
itemid = 0x25
|
||||||
if world.worlds[player].remote_items: # remote items does not currently work
|
# if world.worlds[player].remote_items: # remote items does not currently work
|
||||||
itemid = list(location_table.keys()).index(location.name) + 1
|
# itemid = list(location_table.keys()).index(location.name) + 1
|
||||||
assert itemid < 0x100
|
# assert itemid < 0x100
|
||||||
rom.write_byte(location.player_address, 0xFF)
|
# rom.write_byte(location.player_address, 0xFF)
|
||||||
elif location.item.player != player:
|
if location.item.player != player:
|
||||||
if location.player_address is not None:
|
if location.player_address is not None:
|
||||||
rom.write_byte(location.player_address, min(location.item.player, ROM_PLAYER_LIMIT))
|
rom.write_byte(location.player_address, min(location.item.player, ROM_PLAYER_LIMIT))
|
||||||
else:
|
else:
|
||||||
@@ -1647,7 +1654,7 @@ def patch_rom(world, rom, player, enemized):
|
|||||||
write_strings(rom, world, player)
|
write_strings(rom, world, player)
|
||||||
|
|
||||||
# remote items flag, does not currently work
|
# remote items flag, does not currently work
|
||||||
rom.write_byte(0x18637C, int(world.worlds[player].remote_items))
|
rom.write_byte(0x18637C, 0)
|
||||||
|
|
||||||
# set rom name
|
# set rom name
|
||||||
# 21 bytes
|
# 21 bytes
|
||||||
|
|||||||
@@ -121,8 +121,6 @@ class ALTTPWorld(World):
|
|||||||
location_name_to_id = lookup_name_to_id
|
location_name_to_id = lookup_name_to_id
|
||||||
|
|
||||||
data_version = 8
|
data_version = 8
|
||||||
remote_items: bool = False
|
|
||||||
remote_start_inventory: bool = False
|
|
||||||
required_client_version = (0, 3, 2)
|
required_client_version = (0, 3, 2)
|
||||||
web = ALTTPWeb()
|
web = ALTTPWeb()
|
||||||
|
|
||||||
@@ -157,6 +155,8 @@ class ALTTPWorld(World):
|
|||||||
rom_file = get_base_rom_path()
|
rom_file = get_base_rom_path()
|
||||||
if not os.path.exists(rom_file):
|
if not os.path.exists(rom_file):
|
||||||
raise FileNotFoundError(rom_file)
|
raise FileNotFoundError(rom_file)
|
||||||
|
if world.is_race:
|
||||||
|
import xxtea
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
if self.use_enemizer():
|
if self.use_enemizer():
|
||||||
@@ -193,6 +193,14 @@ class ALTTPWorld(World):
|
|||||||
|
|
||||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||||
|
|
||||||
|
# enforce pre-defined local items.
|
||||||
|
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
|
||||||
|
world.local_items[player].value.add('Triforce Piece')
|
||||||
|
|
||||||
|
# Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too).
|
||||||
|
world.non_local_items[player].value -= item_name_groups['Pendants']
|
||||||
|
world.non_local_items[player].value -= item_name_groups['Crystals']
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
player = self.player
|
player = self.player
|
||||||
world = self.multiworld
|
world = self.multiworld
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestAgahnimsTower(TestDungeon):
|
class TestAgahnimsTower(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestDarkPalace(TestDungeon):
|
class TestDarkPalace(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestDesertPalace(TestDungeon):
|
class TestDesertPalace(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestEasternPalace(TestDungeon):
|
class TestEasternPalace(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestGanonsTower(TestDungeon):
|
class TestGanonsTower(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestIcePalace(TestDungeon):
|
class TestIcePalace(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestMiseryMire(TestDungeon):
|
class TestMiseryMire(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestSkullWoods(TestDungeon):
|
class TestSkullWoods(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestSwampPalace(TestDungeon):
|
class TestSwampPalace(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestThievesTown(TestDungeon):
|
class TestThievesTown(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.dungeons.TestDungeon import TestDungeon
|
from .TestDungeon import TestDungeon
|
||||||
|
|
||||||
|
|
||||||
class TestTowerOfHera(TestDungeon):
|
class TestTowerOfHera(TestDungeon):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted.TestInverted import TestInverted
|
from .TestInverted import TestInverted
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedDarkWorld(TestInverted):
|
class TestInvertedDarkWorld(TestInverted):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted.TestInverted import TestInverted
|
from .TestInverted import TestInverted
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedDeathMountain(TestInverted):
|
class TestInvertedDeathMountain(TestInverted):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted.TestInverted import TestInverted
|
from .TestInverted import TestInverted
|
||||||
|
|
||||||
|
|
||||||
class TestEntrances(TestInverted):
|
class TestEntrances(TestInverted):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted.TestInverted import TestInverted
|
from .TestInverted import TestInverted
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedLightWorld(TestInverted):
|
class TestInvertedLightWorld(TestInverted):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted.TestInverted import TestInverted
|
from .TestInverted import TestInverted
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedTurtleRock(TestInverted):
|
class TestInvertedTurtleRock(TestInverted):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted_minor_glitches.TestInvertedMinor import TestInvertedMinor
|
from .TestInvertedMinor import TestInvertedMinor
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedDarkWorld(TestInvertedMinor):
|
class TestInvertedDarkWorld(TestInvertedMinor):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted_minor_glitches.TestInvertedMinor import TestInvertedMinor
|
from .TestInvertedMinor import TestInvertedMinor
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedDeathMountain(TestInvertedMinor):
|
class TestInvertedDeathMountain(TestInvertedMinor):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted_minor_glitches.TestInvertedMinor import TestInvertedMinor
|
from .TestInvertedMinor import TestInvertedMinor
|
||||||
|
|
||||||
|
|
||||||
class TestEntrances(TestInvertedMinor):
|
class TestEntrances(TestInvertedMinor):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted_minor_glitches.TestInvertedMinor import TestInvertedMinor
|
from .TestInvertedMinor import TestInvertedMinor
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedLightWorld(TestInvertedMinor):
|
class TestInvertedLightWorld(TestInvertedMinor):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted_minor_glitches.TestInvertedMinor import TestInvertedMinor
|
from .TestInvertedMinor import TestInvertedMinor
|
||||||
|
|
||||||
|
|
||||||
class TestInvertedTurtleRock(TestInvertedMinor):
|
class TestInvertedTurtleRock(TestInvertedMinor):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted_owg.TestInvertedOWG import TestInvertedOWG
|
from .TestInvertedOWG import TestInvertedOWG
|
||||||
|
|
||||||
|
|
||||||
class TestDarkWorld(TestInvertedOWG):
|
class TestDarkWorld(TestInvertedOWG):
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted_owg.TestInvertedOWG import TestInvertedOWG
|
from .TestInvertedOWG import TestInvertedOWG
|
||||||
|
|
||||||
|
|
||||||
class TestDeathMountain(TestInvertedOWG):
|
class TestDeathMountain(TestInvertedOWG):
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from test.inverted_owg.TestInvertedOWG import TestInvertedOWG
|
from .TestInvertedOWG import TestInvertedOWG
|
||||||
|
|
||||||
|
|
||||||
class TestDungeons(TestInvertedOWG):
|
class TestDungeons(TestInvertedOWG):
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from test.inverted_owg.TestInvertedOWG import TestInvertedOWG
|
from .TestInvertedOWG import TestInvertedOWG
|
||||||
|
|
||||||
|
|
||||||
class TestLightWorld(TestInvertedOWG):
|
class TestLightWorld(TestInvertedOWG):
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user