mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-21 06:43:42 -07:00
Compare commits
56 Commits
appimage-0
...
style-lock
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9dc1d5142 | ||
|
|
614c50e495 | ||
|
|
60e2b818b1 | ||
|
|
0d61192c67 | ||
|
|
a1aa9c17ff | ||
|
|
d0faa36eef | ||
|
|
22c8153ba8 | ||
|
|
6602c580f4 | ||
|
|
431a9b7023 | ||
|
|
d426226bce | ||
|
|
09afdc2553 | ||
|
|
ca83905d9f | ||
|
|
086295adbb | ||
|
|
81cf1508e0 | ||
|
|
8484193151 | ||
|
|
d10fbf8263 | ||
|
|
f73b3d71bf | ||
|
|
d48d775a59 | ||
|
|
f716bfc58f | ||
|
|
97b388747a | ||
|
|
898fa203ad | ||
|
|
c02c6ee58c | ||
|
|
23b04b5069 | ||
|
|
0ed0d17f38 | ||
|
|
645ede869f | ||
|
|
f5e48c850d | ||
|
|
9bd035a19d | ||
|
|
2e428f906c | ||
|
|
b702ae482b | ||
|
|
b8ca41b45f | ||
|
|
adc16fdd3d | ||
|
|
b32d0efe6d | ||
|
|
c96acbfa23 | ||
|
|
ffe528467e | ||
|
|
b989698740 | ||
|
|
29e0975832 | ||
|
|
e1e2526322 | ||
|
|
f2e83c37e9 | ||
|
|
debda5d111 | ||
|
|
2c4e819010 | ||
|
|
b3700dabf2 | ||
|
|
fb2979d9ef | ||
|
|
a378d62dfd | ||
|
|
eb5ba72cfc | ||
|
|
c1e9d0ab4f | ||
|
|
181cc47079 | ||
|
|
04eef669f9 | ||
|
|
9167e5363d | ||
|
|
f1c5c9a148 | ||
|
|
69e5627cd7 | ||
|
|
ae3e6c29e3 | ||
|
|
f6da81ac70 | ||
|
|
dd6e212519 | ||
|
|
95bba50223 | ||
|
|
21f7c6c0ad | ||
|
|
d15c30f63b |
@@ -166,7 +166,7 @@ class MultiWorld():
|
|||||||
self.player_types[new_id] = NetUtils.SlotType.group
|
self.player_types[new_id] = NetUtils.SlotType.group
|
||||||
self._region_cache[new_id] = {}
|
self._region_cache[new_id] = {}
|
||||||
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
world_type = AutoWorld.AutoWorldRegister.world_types[game]
|
||||||
for option_key, option in world_type.options.items():
|
for option_key, option in world_type.option_definitions.items():
|
||||||
getattr(self, option_key)[new_id] = option(option.default)
|
getattr(self, option_key)[new_id] = option(option.default)
|
||||||
for option_key, option in Options.common_options.items():
|
for option_key, option in Options.common_options.items():
|
||||||
getattr(self, option_key)[new_id] = option(option.default)
|
getattr(self, option_key)[new_id] = option(option.default)
|
||||||
@@ -204,7 +204,7 @@ class MultiWorld():
|
|||||||
for player in self.player_ids:
|
for player in self.player_ids:
|
||||||
self.custom_data[player] = {}
|
self.custom_data[player] = {}
|
||||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||||
for option_key in world_type.options:
|
for option_key in world_type.option_definitions:
|
||||||
setattr(self, option_key, getattr(args, option_key, {}))
|
setattr(self, option_key, getattr(args, option_key, {}))
|
||||||
|
|
||||||
self.worlds[player] = world_type(self, player)
|
self.worlds[player] = world_type(self, player)
|
||||||
@@ -384,7 +384,6 @@ class MultiWorld():
|
|||||||
return self.worlds[player].create_item(item_name)
|
return self.worlds[player].create_item(item_name)
|
||||||
|
|
||||||
def push_precollected(self, item: Item):
|
def push_precollected(self, item: Item):
|
||||||
item.world = self
|
|
||||||
self.precollected_items[item.player].append(item)
|
self.precollected_items[item.player].append(item)
|
||||||
self.state.collect(item, True)
|
self.state.collect(item, True)
|
||||||
|
|
||||||
@@ -392,7 +391,6 @@ class MultiWorld():
|
|||||||
assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}."
|
assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}."
|
||||||
location.item = item
|
location.item = item
|
||||||
item.location = location
|
item.location = location
|
||||||
item.world = self # try to not have this here anymore and create it with item?
|
|
||||||
if collect:
|
if collect:
|
||||||
self.state.collect(item, location.event, location)
|
self.state.collect(item, location.event, location)
|
||||||
|
|
||||||
@@ -1066,26 +1064,25 @@ class LocationProgressType(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class Location:
|
class Location:
|
||||||
# If given as integer, then this is the shop's inventory index
|
game: str = "Generic"
|
||||||
shop_slot: Optional[int] = None
|
player: int
|
||||||
shop_slot_disabled: bool = False
|
name: str
|
||||||
|
address: Optional[int]
|
||||||
|
parent_region: Optional[Region]
|
||||||
event: bool = False
|
event: bool = False
|
||||||
locked: bool = False
|
locked: bool = False
|
||||||
game: str = "Generic"
|
|
||||||
show_in_spoiler: bool = True
|
show_in_spoiler: bool = True
|
||||||
crystal: bool = False
|
|
||||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||||
always_allow = staticmethod(lambda item, state: False)
|
always_allow = staticmethod(lambda item, state: False)
|
||||||
access_rule = staticmethod(lambda state: True)
|
access_rule = staticmethod(lambda state: True)
|
||||||
item_rule = staticmethod(lambda item: True)
|
item_rule = staticmethod(lambda item: True)
|
||||||
item: Optional[Item] = None
|
item: Optional[Item] = None
|
||||||
parent_region: Optional[Region]
|
|
||||||
|
|
||||||
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
|
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
|
||||||
self.name: str = name
|
self.player = player
|
||||||
self.address: Optional[int] = address
|
self.name = name
|
||||||
|
self.address = address
|
||||||
self.parent_region = parent
|
self.parent_region = parent
|
||||||
self.player: int = player
|
|
||||||
|
|
||||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||||
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||||
@@ -1102,7 +1099,6 @@ class Location:
|
|||||||
self.item = item
|
self.item = item
|
||||||
item.location = self
|
item.location = self
|
||||||
self.event = item.advancement
|
self.event = item.advancement
|
||||||
self.item.world = self.parent_region.world
|
|
||||||
self.locked = True
|
self.locked = True
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -1147,39 +1143,28 @@ class ItemClassification(IntFlag):
|
|||||||
|
|
||||||
|
|
||||||
class Item:
|
class Item:
|
||||||
location: Optional[Location] = None
|
|
||||||
world: Optional[MultiWorld] = None
|
|
||||||
code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata
|
|
||||||
name: str
|
|
||||||
game: str = "Generic"
|
game: str = "Generic"
|
||||||
type: str = None
|
__slots__ = ("name", "classification", "code", "player", "location")
|
||||||
|
name: str
|
||||||
classification: ItemClassification
|
classification: ItemClassification
|
||||||
|
code: Optional[int]
|
||||||
# need to find a decent place for these to live and to allow other games to register texts if they want.
|
"""an item with code None is called an Event, and does not get written to multidata"""
|
||||||
pedestal_credit_text: str = "and the Unknown Item"
|
player: int
|
||||||
sickkid_credit_text: Optional[str] = None
|
location: Optional[Location]
|
||||||
magicshop_credit_text: Optional[str] = None
|
|
||||||
zora_credit_text: Optional[str] = None
|
|
||||||
fluteboy_credit_text: Optional[str] = None
|
|
||||||
|
|
||||||
# hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem
|
|
||||||
smallkey: bool = False
|
|
||||||
bigkey: bool = False
|
|
||||||
map: bool = False
|
|
||||||
compass: bool = False
|
|
||||||
|
|
||||||
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
|
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.classification = classification
|
self.classification = classification
|
||||||
self.player = player
|
self.player = player
|
||||||
self.code = code
|
self.code = code
|
||||||
|
self.location = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hint_text(self):
|
def hint_text(self) -> str:
|
||||||
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pedestal_hint_text(self):
|
def pedestal_hint_text(self) -> str:
|
||||||
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
|
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1205,7 +1190,7 @@ class Item:
|
|||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.name == other.name and self.player == other.player
|
return self.name == other.name and self.player == other.player
|
||||||
|
|
||||||
def __lt__(self, other: Item):
|
def __lt__(self, other: Item) -> bool:
|
||||||
if other.player != self.player:
|
if other.player != self.player:
|
||||||
return other.player < self.player
|
return other.player < self.player
|
||||||
return self.name < other.name
|
return self.name < other.name
|
||||||
@@ -1213,11 +1198,13 @@ class Item:
|
|||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash((self.name, self.player))
|
return hash((self.name, self.player))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
if self.location and self.location.parent_region and self.location.parent_region.world:
|
||||||
|
return self.location.parent_region.world.get_name_string_for_object(self)
|
||||||
|
return f"{self.name} (Player {self.player})"
|
||||||
|
|
||||||
|
|
||||||
class Spoiler():
|
class Spoiler():
|
||||||
@@ -1401,7 +1388,7 @@ class Spoiler():
|
|||||||
outfile.write('Game: %s\n' % self.world.game[player])
|
outfile.write('Game: %s\n' % self.world.game[player])
|
||||||
for f_option, option in Options.per_game_common_options.items():
|
for f_option, option in Options.per_game_common_options.items():
|
||||||
write_option(f_option, option)
|
write_option(f_option, option)
|
||||||
options = self.world.worlds[player].options
|
options = self.world.worlds[player].option_definitions
|
||||||
if options:
|
if options:
|
||||||
for f_option, option in options.items():
|
for f_option, option in options.items():
|
||||||
write_option(f_option, option)
|
write_option(f_option, option)
|
||||||
|
|||||||
@@ -493,7 +493,8 @@ async def server_loop(ctx: CommonContext, address=None):
|
|||||||
logger.info(f'Connecting to Archipelago server at {address}')
|
logger.info(f'Connecting to Archipelago server at {address}')
|
||||||
try:
|
try:
|
||||||
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
|
||||||
ctx.ui.update_address_bar(server_url.netloc)
|
if ctx.ui is not None:
|
||||||
|
ctx.ui.update_address_bar(server_url.netloc)
|
||||||
ctx.server = Endpoint(socket)
|
ctx.server = Endpoint(socket)
|
||||||
logger.info('Connected')
|
logger.info('Connected')
|
||||||
ctx.server_address = address
|
ctx.server_address = address
|
||||||
@@ -562,18 +563,21 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
f" for each location checked. Use !hint for more information.")
|
f" for each location checked. Use !hint for more information.")
|
||||||
ctx.hint_cost = int(args['hint_cost'])
|
ctx.hint_cost = int(args['hint_cost'])
|
||||||
ctx.check_points = int(args['location_check_points'])
|
ctx.check_points = int(args['location_check_points'])
|
||||||
players = args.get("players", [])
|
|
||||||
if len(players) < 1:
|
if "players" in args: # TODO remove when servers sending this are outdated
|
||||||
logger.info('No player connected')
|
players = args.get("players", [])
|
||||||
else:
|
if len(players) < 1:
|
||||||
players.sort()
|
logger.info('No player connected')
|
||||||
current_team = -1
|
else:
|
||||||
logger.info('Connected Players:')
|
players.sort()
|
||||||
for network_player in players:
|
current_team = -1
|
||||||
if network_player.team != current_team:
|
logger.info('Connected Players:')
|
||||||
logger.info(f' Team #{network_player.team + 1}')
|
for network_player in players:
|
||||||
current_team = network_player.team
|
if network_player.team != current_team:
|
||||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
logger.info(f' Team #{network_player.team + 1}')
|
||||||
|
current_team = network_player.team
|
||||||
|
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||||
|
|
||||||
# update datapackage
|
# update datapackage
|
||||||
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
|
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
|
||||||
|
|
||||||
@@ -723,7 +727,7 @@ if __name__ == '__main__':
|
|||||||
class TextContext(CommonContext):
|
class TextContext(CommonContext):
|
||||||
tags = {"AP", "IgnoreGame", "TextOnly"}
|
tags = {"AP", "IgnoreGame", "TextOnly"}
|
||||||
game = "" # empty matches any game since 0.3.2
|
game = "" # empty matches any game since 0.3.2
|
||||||
items_handling = 0 # don't receive any NetworkItems
|
items_handling = 0b111 # receive all items for /received
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ import Utils
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
Utils.init_logging("FactorioClient", exception_logger="Client")
|
Utils.init_logging("FactorioClient", exception_logger="Client")
|
||||||
|
|
||||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
|
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser
|
||||||
get_base_parser
|
|
||||||
from MultiServer import mark_raw
|
from MultiServer import mark_raw
|
||||||
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
|
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
|
||||||
|
|
||||||
|
|||||||
4
Fill.py
4
Fill.py
@@ -220,8 +220,8 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
|||||||
world.push_item(defaultlocations.pop(i), item_to_place, False)
|
world.push_item(defaultlocations.pop(i), item_to_place, False)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. "
|
||||||
f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.")
|
f"Too many non-local items for too few remaining locations.")
|
||||||
|
|
||||||
world.random.shuffle(defaultlocations)
|
world.random.shuffle(defaultlocations)
|
||||||
|
|
||||||
|
|||||||
56
Generate.py
56
Generate.py
@@ -7,7 +7,7 @@ import urllib.request
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
from typing import Set, Dict, Tuple, Callable, Any, Union
|
from typing import Set, Dict, Tuple, Callable, Any, Union
|
||||||
import os
|
import os
|
||||||
from collections import Counter
|
from collections import Counter, ChainMap
|
||||||
import string
|
import string
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
@@ -133,12 +133,14 @@ def main(args=None, callback=ERmain):
|
|||||||
|
|
||||||
if args.meta_file_path and os.path.exists(args.meta_file_path):
|
if args.meta_file_path and os.path.exists(args.meta_file_path):
|
||||||
try:
|
try:
|
||||||
weights_cache[args.meta_file_path] = read_weights_yamls(args.meta_file_path)
|
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
|
||||||
meta_weights = weights_cache[args.meta_file_path][-1]
|
|
||||||
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
|
||||||
del(meta_weights["meta_description"])
|
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
|
||||||
|
del(meta_weights["meta_description"])
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
|
||||||
if args.samesettings:
|
if args.samesettings:
|
||||||
raise Exception("Cannot mix --samesettings with --meta")
|
raise Exception("Cannot mix --samesettings with --meta")
|
||||||
else:
|
else:
|
||||||
@@ -164,7 +166,7 @@ def main(args=None, callback=ERmain):
|
|||||||
player_files[player_id] = filename
|
player_files[player_id] = filename
|
||||||
player_id += 1
|
player_id += 1
|
||||||
|
|
||||||
args.multi = max(player_id-1, args.multi)
|
args.multi = max(player_id - 1, args.multi)
|
||||||
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
|
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
|
||||||
f"{args.plando}")
|
f"{args.plando}")
|
||||||
|
|
||||||
@@ -186,26 +188,28 @@ def main(args=None, callback=ERmain):
|
|||||||
erargs.enemizercli = args.enemizercli
|
erargs.enemizercli = args.enemizercli
|
||||||
|
|
||||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||||
{fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||||
for fname, yamls in weights_cache.items()}
|
for fname, yamls in weights_cache.items()}
|
||||||
player_path_cache = {}
|
|
||||||
for player in range(1, args.multi + 1):
|
|
||||||
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
|
||||||
|
|
||||||
if meta_weights:
|
if meta_weights:
|
||||||
for category_name, category_dict in meta_weights.items():
|
for category_name, category_dict in meta_weights.items():
|
||||||
for key in category_dict:
|
for key in category_dict:
|
||||||
option = get_choice(key, category_dict)
|
option = roll_meta_option(key, category_name, category_dict)
|
||||||
if option is not None:
|
if option is not None:
|
||||||
for player, path in player_path_cache.items():
|
for path in weights_cache:
|
||||||
for yaml in weights_cache[path]:
|
for yaml in weights_cache[path]:
|
||||||
if category_name is None:
|
if category_name is None:
|
||||||
yaml[key] = option
|
for category in yaml:
|
||||||
|
if category in AutoWorldRegister.world_types and key in Options.common_options:
|
||||||
|
yaml[category][key] = option
|
||||||
elif category_name not in yaml:
|
elif category_name not in yaml:
|
||||||
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
|
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
|
||||||
else:
|
else:
|
||||||
yaml[category_name][key] = option
|
yaml[category_name][key] = option
|
||||||
|
|
||||||
|
player_path_cache = {}
|
||||||
|
for player in range(1, args.multi + 1):
|
||||||
|
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
erargs.player_settings = {}
|
erargs.player_settings = {}
|
||||||
|
|
||||||
@@ -387,6 +391,28 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
|
|||||||
return weights
|
return weights
|
||||||
|
|
||||||
|
|
||||||
|
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
|
||||||
|
if not game:
|
||||||
|
return get_choice(option_key, category_dict)
|
||||||
|
if game in AutoWorldRegister.world_types:
|
||||||
|
game_world = AutoWorldRegister.world_types[game]
|
||||||
|
options = ChainMap(game_world.option_definitions, Options.per_game_common_options)
|
||||||
|
if option_key in options:
|
||||||
|
if options[option_key].supports_weighting:
|
||||||
|
return get_choice(option_key, category_dict)
|
||||||
|
return options[option_key]
|
||||||
|
if game == "A Link to the Past": # TODO wow i hate this
|
||||||
|
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
|
||||||
|
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
|
||||||
|
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
|
||||||
|
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
|
||||||
|
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
|
||||||
|
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
|
||||||
|
"random_sprite_on_event"}:
|
||||||
|
return get_choice(option_key, category_dict)
|
||||||
|
raise Exception(f"Error generating meta option {option_key} for {game}.")
|
||||||
|
|
||||||
|
|
||||||
def roll_linked_options(weights: dict) -> dict:
|
def roll_linked_options(weights: dict) -> dict:
|
||||||
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
||||||
for option_set in weights["linked_options"]:
|
for option_set in weights["linked_options"]:
|
||||||
@@ -531,7 +557,7 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
|
|||||||
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
|
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
|
||||||
|
|
||||||
if ret.game in AutoWorldRegister.world_types:
|
if ret.game in AutoWorldRegister.world_types:
|
||||||
for option_key, option in world_type.options.items():
|
for option_key, option in world_type.option_definitions.items():
|
||||||
handle_option(ret, game_weights, option_key, option)
|
handle_option(ret, game_weights, option_key, option)
|
||||||
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
|
||||||
|
|||||||
5
Main.py
5
Main.py
@@ -217,9 +217,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
|
|
||||||
logger.info("Running Item Plando")
|
logger.info("Running Item Plando")
|
||||||
|
|
||||||
for item in world.itempool:
|
|
||||||
item.world = world
|
|
||||||
|
|
||||||
distribute_planned(world)
|
distribute_planned(world)
|
||||||
|
|
||||||
logger.info('Running Pre Main Fill.')
|
logger.info('Running Pre Main Fill.')
|
||||||
@@ -426,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
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))
|
||||||
|
|
||||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||||
logger.info(f'Creating final archive at {zipfilename}.')
|
logger.info(f"Creating final archive at {zipfilename}")
|
||||||
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||||
compresslevel=9) as zf:
|
compresslevel=9) as zf:
|
||||||
for file in os.scandir(temp_dir):
|
for file in os.scandir(temp_dir):
|
||||||
|
|||||||
171
MultiServer.py
171
MultiServer.py
@@ -30,13 +30,8 @@ except ImportError:
|
|||||||
OperationalError = ConnectionError
|
OperationalError = ConnectionError
|
||||||
|
|
||||||
import NetUtils
|
import NetUtils
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
|
||||||
|
|
||||||
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
|
|
||||||
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
|
||||||
import Utils
|
import Utils
|
||||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
from Utils import version_tuple, restricted_loads, Version
|
||||||
version_tuple, restricted_loads, Version
|
|
||||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||||
SlotType
|
SlotType
|
||||||
|
|
||||||
@@ -126,6 +121,11 @@ class Context:
|
|||||||
stored_data: typing.Dict[str, object]
|
stored_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})')
|
||||||
|
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]]
|
||||||
|
forced_auto_forfeits: typing.Dict[str, bool]
|
||||||
|
|
||||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
|
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
|
||||||
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
|
||||||
@@ -190,8 +190,43 @@ class Context:
|
|||||||
self.stored_data = {}
|
self.stored_data = {}
|
||||||
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
|
||||||
|
|
||||||
# General networking
|
# init empty to satisfy linter, I suppose
|
||||||
|
self.gamespackage = {}
|
||||||
|
self.item_name_groups = {}
|
||||||
|
self.all_item_and_group_names = {}
|
||||||
|
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
|
||||||
|
self.non_hintable_names = {}
|
||||||
|
|
||||||
|
self._load_game_data()
|
||||||
|
self._init_game_data()
|
||||||
|
|
||||||
|
# Datapackage retrieval
|
||||||
|
def _load_game_data(self):
|
||||||
|
import worlds
|
||||||
|
self.gamespackage = worlds.network_data_package["games"]
|
||||||
|
|
||||||
|
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
|
||||||
|
worlds.AutoWorldRegister.world_types.items()}
|
||||||
|
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||||
|
self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit
|
||||||
|
self.non_hintable_names[world_name] = world.hint_blacklist
|
||||||
|
|
||||||
|
def _init_game_data(self):
|
||||||
|
for game_name, game_package in self.gamespackage.items():
|
||||||
|
for item_name, item_id in game_package["item_name_to_id"].items():
|
||||||
|
self.item_names[item_id] = item_name
|
||||||
|
for location_name, location_id in game_package["location_name_to_id"].items():
|
||||||
|
self.location_names[location_id] = location_name
|
||||||
|
self.all_item_and_group_names[game_name] = \
|
||||||
|
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
||||||
|
|
||||||
|
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
|
||||||
|
return self.gamespackage[game]["item_name_to_id"]
|
||||||
|
|
||||||
|
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
|
||||||
|
return self.gamespackage[game]["location_name_to_id"]
|
||||||
|
|
||||||
|
# General networking
|
||||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||||
if not endpoint.socket or not endpoint.socket.open:
|
if not endpoint.socket or not endpoint.socket.open:
|
||||||
return False
|
return False
|
||||||
@@ -544,12 +579,12 @@ class Context:
|
|||||||
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.'
|
||||||
self.notify_all(finished_msg)
|
self.notify_all(finished_msg)
|
||||||
if "auto" in self.forfeit_mode:
|
|
||||||
forfeit_player(self, client.team, client.slot)
|
|
||||||
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
|
|
||||||
forfeit_player(self, client.team, client.slot)
|
|
||||||
if "auto" in self.collect_mode:
|
if "auto" in self.collect_mode:
|
||||||
collect_player(self, client.team, client.slot)
|
collect_player(self, client.team, client.slot)
|
||||||
|
if "auto" in self.forfeit_mode:
|
||||||
|
forfeit_player(self, client.team, client.slot)
|
||||||
|
elif self.forced_auto_forfeits[self.games[client.slot]]:
|
||||||
|
forfeit_player(self, client.team, client.slot)
|
||||||
|
|
||||||
|
|
||||||
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
|
||||||
@@ -642,9 +677,10 @@ 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': network_data_package["version"],
|
'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 network_data_package["games"].items()},
|
in ctx.gamespackage.items()},
|
||||||
'seed_name': ctx.seed_name,
|
'seed_name': ctx.seed_name,
|
||||||
'time': time.time(),
|
'time': time.time(),
|
||||||
}])
|
}])
|
||||||
@@ -822,8 +858,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
|||||||
send_items_to(ctx, team, target_player, new_item)
|
send_items_to(ctx, team, target_player, new_item)
|
||||||
|
|
||||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
|
||||||
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
|
ctx.player_names[(team, target_player)], ctx.location_names[location]))
|
||||||
info_text = json_format_send_event(new_item, target_player)
|
info_text = json_format_send_event(new_item, target_player)
|
||||||
ctx.broadcast_team(team, [info_text])
|
ctx.broadcast_team(team, [info_text])
|
||||||
|
|
||||||
@@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
|||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
||||||
|
|
||||||
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
|
def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
|
||||||
hints = []
|
hints = []
|
||||||
slots: typing.Set[int] = {slot}
|
slots: typing.Set[int] = {slot}
|
||||||
for group_id, group in ctx.groups.items():
|
for group_id, group in ctx.groups.items():
|
||||||
if slot in group:
|
if slot in group:
|
||||||
slots.add(group_id)
|
slots.add(group_id)
|
||||||
seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item]
|
|
||||||
|
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
|
||||||
for finding_player, check_data in ctx.locations.items():
|
for finding_player, check_data in ctx.locations.items():
|
||||||
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
||||||
if receiving_player in slots and item_id == seeked_item_id:
|
if receiving_player in slots and item_id == seeked_item_id:
|
||||||
@@ -857,7 +894,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
|
|||||||
|
|
||||||
|
|
||||||
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
||||||
seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location]
|
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
|
||||||
return collect_hint_location_id(ctx, team, slot, seeked_location)
|
return collect_hint_location_id(ctx, team, slot, seeked_location)
|
||||||
|
|
||||||
|
|
||||||
@@ -874,8 +911,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location
|
|||||||
|
|
||||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||||
f"{lookup_any_item_id_to_name[hint.item]} is " \
|
f"{ctx.item_names[hint.item]} is " \
|
||||||
f"at {get_location_name_from_id(hint.location)} " \
|
f"at {ctx.location_names[hint.location]} " \
|
||||||
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
||||||
|
|
||||||
if hint.entrance:
|
if hint.entrance:
|
||||||
@@ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||||
return True
|
return True
|
||||||
elif "disabled" in self.ctx.forfeit_mode:
|
elif "disabled" in self.ctx.forfeit_mode:
|
||||||
self.output(
|
self.output("Sorry, client item releasing has been disabled on this server. "
|
||||||
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release")
|
"You can ask the server admin for a /release")
|
||||||
return False
|
return False
|
||||||
else: # is auto or goal
|
else: # is auto or goal
|
||||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||||
@@ -1170,7 +1207,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if self.ctx.remaining_mode == "enabled":
|
if self.ctx.remaining_mode == "enabled":
|
||||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if remaining_item_ids:
|
if remaining_item_ids:
|
||||||
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
|
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
|
||||||
for item_id in remaining_item_ids))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
@@ -1183,7 +1220,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
|
||||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||||
if remaining_item_ids:
|
if remaining_item_ids:
|
||||||
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item")
|
self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
|
||||||
for item_id in remaining_item_ids))
|
for item_id in remaining_item_ids))
|
||||||
else:
|
else:
|
||||||
self.output("No remaining items found.")
|
self.output("No remaining items found.")
|
||||||
@@ -1199,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
|
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
|
||||||
|
|
||||||
if locations:
|
if locations:
|
||||||
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations]
|
texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
|
||||||
texts.append(f"Found {len(locations)} missing location checks")
|
texts.append(f"Found {len(locations)} missing location checks")
|
||||||
self.ctx.notify_client_multiple(self.client, texts)
|
self.ctx.notify_client_multiple(self.client, texts)
|
||||||
else:
|
else:
|
||||||
@@ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
||||||
|
|
||||||
if locations:
|
if locations:
|
||||||
texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations]
|
texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
|
||||||
texts.append(f"Found {len(locations)} done location checks")
|
texts.append(f"Found {len(locations)} done location checks")
|
||||||
self.ctx.notify_client_multiple(self.client, texts)
|
self.ctx.notify_client_multiple(self.client, texts)
|
||||||
else:
|
else:
|
||||||
@@ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
def _cmd_getitem(self, item_name: str) -> bool:
|
def _cmd_getitem(self, item_name: str) -> bool:
|
||||||
"""Cheat in an item, if it is enabled on this server"""
|
"""Cheat in an item, if it is enabled on this server"""
|
||||||
if self.ctx.item_cheat:
|
if self.ctx.item_cheat:
|
||||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot])
|
||||||
item_name, usable, response = get_intended_text(item_name,
|
item_name, usable, response = get_intended_text(
|
||||||
world.item_names)
|
item_name,
|
||||||
|
names
|
||||||
|
)
|
||||||
if usable:
|
if usable:
|
||||||
new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot)
|
new_item = NetworkItem(names[item_name], -1, self.client.slot)
|
||||||
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
|
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
|
||||||
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
|
||||||
self.ctx.notify_all(
|
self.ctx.notify_all(
|
||||||
@@ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
f"You have {points_available} points.")
|
f"You have {points_available} points.")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
game = self.ctx.games[self.client.slot]
|
||||||
names = world.location_names if for_location else world.all_item_and_group_names
|
names = self.ctx.location_names_for_game(game) \
|
||||||
|
if for_location else \
|
||||||
|
self.ctx.all_item_and_group_names[game]
|
||||||
hint_name, usable, response = get_intended_text(input_text,
|
hint_name, usable, response = get_intended_text(input_text,
|
||||||
names)
|
names)
|
||||||
if usable:
|
if usable:
|
||||||
if hint_name in world.hint_blacklist:
|
if hint_name in self.ctx.non_hintable_names[game]:
|
||||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||||
hints = []
|
hints = []
|
||||||
elif not for_location and hint_name in world.item_name_groups: # item group name
|
elif not for_location and hint_name in self.ctx.item_name_groups[game]: # item group name
|
||||||
hints = []
|
hints = []
|
||||||
for item in world.item_name_groups[hint_name]:
|
for item_name in self.ctx.item_name_groups[game][hint_name]:
|
||||||
if item in world.item_name_to_id: # ensure item has an ID
|
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
||||||
elif not for_location and hint_name in world.item_names: # item name
|
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
||||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||||
else: # location name
|
else: # location name
|
||||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||||
@@ -1346,12 +1387,12 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_hint(self, item: str = "") -> bool:
|
def _cmd_hint(self, item_name: str = "") -> bool:
|
||||||
"""Use !hint {item_name},
|
"""Use !hint {item_name},
|
||||||
for example !hint Lamp to get a spoiler peek for that item.
|
for example !hint Lamp to get a spoiler peek for that item.
|
||||||
If hint costs are on, this will only give you one new result,
|
If hint costs are on, this will only give you one new result,
|
||||||
you can rerun the command to get more in that case."""
|
you can rerun the command to get more in that case."""
|
||||||
return self.get_hints(item)
|
return self.get_hints(item_name)
|
||||||
|
|
||||||
@mark_raw
|
@mark_raw
|
||||||
def _cmd_hint_location(self, location: str = "") -> bool:
|
def _cmd_hint_location(self, location: str = "") -> bool:
|
||||||
@@ -1477,23 +1518,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
elif cmd == "GetDataPackage":
|
elif cmd == "GetDataPackage":
|
||||||
exclusions = args.get("exclusions", [])
|
exclusions = args.get("exclusions", [])
|
||||||
if "games" in args:
|
if "games" in args:
|
||||||
games = {name: game_data for name, game_data in network_data_package["games"].items()
|
games = {name: game_data for name, game_data in ctx.gamespackage.items()
|
||||||
if name in set(args.get("games", []))}
|
if name in set(args.get("games", []))}
|
||||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
"data": {"games": games}}])
|
"data": {"games": games}}])
|
||||||
# TODO: remove exclusions behaviour around 0.5.0
|
# TODO: remove exclusions behaviour around 0.5.0
|
||||||
elif exclusions:
|
elif exclusions:
|
||||||
exclusions = set(exclusions)
|
exclusions = set(exclusions)
|
||||||
games = {name: game_data for name, game_data in network_data_package["games"].items()
|
games = {name: game_data for name, game_data in ctx.gamespackage.items()
|
||||||
if name not in exclusions}
|
if name not in exclusions}
|
||||||
package = network_data_package.copy()
|
|
||||||
package["games"] = games
|
package = {"games": games}
|
||||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
"data": package}])
|
"data": package}])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||||
"data": network_data_package}])
|
"data": {"games": ctx.gamespackage}}])
|
||||||
|
|
||||||
elif client.auth:
|
elif client.auth:
|
||||||
if cmd == "ConnectUpdate":
|
if cmd == "ConnectUpdate":
|
||||||
@@ -1549,7 +1590,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
create_as_hint: int = int(args.get("create_as_hint", 0))
|
create_as_hint: int = int(args.get("create_as_hint", 0))
|
||||||
hints = []
|
hints = []
|
||||||
for location in args["locations"]:
|
for location in args["locations"]:
|
||||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
if type(location) is not int:
|
||||||
await ctx.send_msgs(client,
|
await ctx.send_msgs(client,
|
||||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
||||||
"original_cmd": cmd}])
|
"original_cmd": cmd}])
|
||||||
@@ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
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())
|
||||||
if usable:
|
if usable:
|
||||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
item = " ".join(item_name)
|
item_name = " ".join(item_name)
|
||||||
world = proxy_worlds[self.ctx.games[slot]]
|
names = self.ctx.item_names_for_game(self.ctx.games[slot])
|
||||||
item, usable, response = get_intended_text(item, world.item_names)
|
item_name, usable, response = get_intended_text(item_name, names)
|
||||||
if usable:
|
if usable:
|
||||||
amount: int = int(amount)
|
amount: int = int(amount)
|
||||||
new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))]
|
new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
|
||||||
send_items_to(self.ctx, team, slot, *new_items)
|
send_items_to(self.ctx, team, slot, *new_items)
|
||||||
|
|
||||||
send_new_items(self.ctx)
|
send_new_items(self.ctx)
|
||||||
self.ctx.notify_all(
|
self.ctx.notify_all(
|
||||||
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
|
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
|
||||||
f'"{item}" to {self.ctx.get_aliased_name(team, slot)}')
|
f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}')
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.output(response)
|
self.output(response)
|
||||||
@@ -1787,22 +1828,22 @@ 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_hint(self, player_name: str, *item: 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())
|
||||||
if usable:
|
if usable:
|
||||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
item = " ".join(item)
|
item_name = " ".join(item_name)
|
||||||
world = proxy_worlds[self.ctx.games[slot]]
|
game = self.ctx.games[slot]
|
||||||
item, usable, response = get_intended_text(item, world.all_item_and_group_names)
|
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
|
||||||
if usable:
|
if usable:
|
||||||
if item in world.item_name_groups:
|
if item_name in self.ctx.item_name_groups[game]:
|
||||||
hints = []
|
hints = []
|
||||||
for item in world.item_name_groups[item]:
|
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
|
||||||
if item in world.item_name_to_id: # ensure item has an ID
|
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||||
hints.extend(collect_hints(self.ctx, team, slot, item))
|
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
|
||||||
else: # item name
|
else: # item name
|
||||||
hints = collect_hints(self.ctx, team, slot, item)
|
hints = collect_hints(self.ctx, team, slot, item_name)
|
||||||
|
|
||||||
if hints:
|
if hints:
|
||||||
notify_hints(self.ctx, team, hints)
|
notify_hints(self.ctx, team, hints)
|
||||||
@@ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||||||
self.output(response)
|
self.output(response)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _cmd_hint_location(self, player_name: str, *location: str) -> bool:
|
def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool:
|
||||||
"""Send out a hint for a player's location to their team"""
|
"""Send out a hint for a player's location 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())
|
||||||
if usable:
|
if usable:
|
||||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||||
item = " ".join(location)
|
location_name = " ".join(location_name)
|
||||||
world = proxy_worlds[self.ctx.games[slot]]
|
location_name, usable, response = get_intended_text(location_name,
|
||||||
item, usable, response = get_intended_text(item, world.location_names)
|
self.ctx.location_names_for_game(self.ctx.games[slot]))
|
||||||
if usable:
|
if usable:
|
||||||
hints = collect_hint_location_name(self.ctx, team, slot, item)
|
hints = collect_hint_location_name(self.ctx, team, slot, location_name)
|
||||||
if hints:
|
if hints:
|
||||||
notify_hints(self.ctx, team, hints)
|
notify_hints(self.ctx, team, hints)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ class RawJSONtoTextParser(JSONtoTextParser):
|
|||||||
|
|
||||||
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
|
||||||
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
|
||||||
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
|
||||||
|
|
||||||
|
|
||||||
def color_code(*args):
|
def color_code(*args):
|
||||||
|
|||||||
@@ -44,11 +44,38 @@ nest_asyncio.apply()
|
|||||||
class StarcraftClientProcessor(ClientCommandProcessor):
|
class StarcraftClientProcessor(ClientCommandProcessor):
|
||||||
ctx: SC2Context
|
ctx: SC2Context
|
||||||
|
|
||||||
|
def _cmd_difficulty(self, difficulty: str = "") -> bool:
|
||||||
|
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
|
||||||
|
options = difficulty.split()
|
||||||
|
num_options = len(options)
|
||||||
|
difficulty_choice = options[0].lower()
|
||||||
|
|
||||||
|
if num_options > 0:
|
||||||
|
if difficulty_choice == "casual":
|
||||||
|
self.ctx.difficulty_override = 0
|
||||||
|
elif difficulty_choice == "normal":
|
||||||
|
self.ctx.difficulty_override = 1
|
||||||
|
elif difficulty_choice == "hard":
|
||||||
|
self.ctx.difficulty_override = 2
|
||||||
|
elif difficulty_choice == "brutal":
|
||||||
|
self.ctx.difficulty_override = 3
|
||||||
|
else:
|
||||||
|
self.output("Unable to parse difficulty '" + options[0] + "'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.output("Difficulty set to " + options[0])
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.output("Difficulty needs to be specified in the command.")
|
||||||
|
return False
|
||||||
|
|
||||||
def _cmd_disable_mission_check(self) -> bool:
|
def _cmd_disable_mission_check(self) -> bool:
|
||||||
"""Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
|
"""Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
|
||||||
the next mission in a chain the other player is doing."""
|
the next mission in a chain the other player is doing."""
|
||||||
self.ctx.missions_unlocked = True
|
self.ctx.missions_unlocked = True
|
||||||
sc2_logger.info("Mission check has been disabled")
|
sc2_logger.info("Mission check has been disabled")
|
||||||
|
return True
|
||||||
|
|
||||||
def _cmd_play(self, mission_id: str = "") -> bool:
|
def _cmd_play(self, mission_id: str = "") -> bool:
|
||||||
"""Start a Starcraft 2 mission"""
|
"""Start a Starcraft 2 mission"""
|
||||||
@@ -64,6 +91,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
|||||||
else:
|
else:
|
||||||
sc2_logger.info(
|
sc2_logger.info(
|
||||||
"Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
|
"Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -108,6 +136,7 @@ class SC2Context(CommonContext):
|
|||||||
missions_unlocked = False
|
missions_unlocked = False
|
||||||
current_tooltip = None
|
current_tooltip = None
|
||||||
last_loc_list = None
|
last_loc_list = None
|
||||||
|
difficulty_override = -1
|
||||||
|
|
||||||
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:
|
||||||
@@ -470,7 +499,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
|||||||
game_state = 0
|
game_state = 0
|
||||||
if iteration == 0:
|
if iteration == 0:
|
||||||
start_items = calculate_items(self.ctx.items_received)
|
start_items = calculate_items(self.ctx.items_received)
|
||||||
difficulty = calc_difficulty(self.ctx.difficulty)
|
if self.ctx.difficulty_override >= 0:
|
||||||
|
difficulty = calc_difficulty(self.ctx.difficulty_override)
|
||||||
|
else:
|
||||||
|
difficulty = calc_difficulty(self.ctx.difficulty)
|
||||||
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
|
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
|
||||||
difficulty,
|
difficulty,
|
||||||
start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
|
start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],
|
||||||
|
|||||||
176
Utils.py
176
Utils.py
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import shutil
|
|
||||||
import typing
|
import typing
|
||||||
import builtins
|
import builtins
|
||||||
import os
|
import os
|
||||||
@@ -12,12 +11,18 @@ import io
|
|||||||
import collections
|
import collections
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import decimal
|
from yaml import load, load_all, dump, SafeLoader
|
||||||
|
|
||||||
|
try:
|
||||||
|
from yaml import CLoader as UnsafeLoader
|
||||||
|
from yaml import CDumper as Dumper
|
||||||
|
except ImportError:
|
||||||
|
from yaml import Loader as UnsafeLoader
|
||||||
|
from yaml import Dumper
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from tkinter import Tk
|
import tkinter
|
||||||
else:
|
import pathlib
|
||||||
Tk = typing.Any
|
|
||||||
|
|
||||||
|
|
||||||
def tuplize_version(version: str) -> Version:
|
def tuplize_version(version: str) -> Version:
|
||||||
@@ -33,18 +38,10 @@ class Version(typing.NamedTuple):
|
|||||||
__version__ = "0.3.4"
|
__version__ = "0.3.4"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith('linux')
|
is_linux = sys.platform.startswith("linux")
|
||||||
is_macos = sys.platform == 'darwin'
|
is_macos = sys.platform == "darwin"
|
||||||
is_windows = sys.platform in ("win32", "cygwin", "msys")
|
is_windows = sys.platform in ("win32", "cygwin", "msys")
|
||||||
|
|
||||||
import jellyfish
|
|
||||||
from yaml import load, load_all, dump, SafeLoader
|
|
||||||
|
|
||||||
try:
|
|
||||||
from yaml import CLoader as Loader
|
|
||||||
except ImportError:
|
|
||||||
from yaml import Loader
|
|
||||||
|
|
||||||
|
|
||||||
def int16_as_bytes(value: int) -> typing.List[int]:
|
def int16_as_bytes(value: int) -> typing.List[int]:
|
||||||
value = value & 0xFFFF
|
value = value & 0xFFFF
|
||||||
@@ -125,17 +122,18 @@ def home_path(*path: str) -> str:
|
|||||||
|
|
||||||
def user_path(*path: str) -> str:
|
def user_path(*path: str) -> str:
|
||||||
"""Returns either local_path or home_path based on write permissions."""
|
"""Returns either local_path or home_path based on write permissions."""
|
||||||
if hasattr(user_path, 'cached_path'):
|
if hasattr(user_path, "cached_path"):
|
||||||
pass
|
pass
|
||||||
elif os.access(local_path(), os.W_OK):
|
elif os.access(local_path(), os.W_OK):
|
||||||
user_path.cached_path = local_path()
|
user_path.cached_path = local_path()
|
||||||
else:
|
else:
|
||||||
user_path.cached_path = home_path()
|
user_path.cached_path = home_path()
|
||||||
# populate home from local - TODO: upgrade feature
|
# populate home from local - TODO: upgrade feature
|
||||||
if user_path.cached_path != local_path() and not os.path.exists(user_path('host.yaml')):
|
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
|
||||||
for dn in ('Players', 'data/sprites'):
|
import shutil
|
||||||
|
for dn in ("Players", "data/sprites"):
|
||||||
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
|
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
|
||||||
for fn in ('manifest.json', 'host.yaml'):
|
for fn in ("manifest.json", "host.yaml"):
|
||||||
shutil.copy2(local_path(fn), user_path(fn))
|
shutil.copy2(local_path(fn), user_path(fn))
|
||||||
|
|
||||||
return os.path.join(user_path.cached_path, *path)
|
return os.path.join(user_path.cached_path, *path)
|
||||||
@@ -150,11 +148,12 @@ def output_path(*path: str):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def open_file(filename):
|
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
||||||
if sys.platform == 'win32':
|
if is_windows:
|
||||||
os.startfile(filename)
|
os.startfile(filename)
|
||||||
else:
|
else:
|
||||||
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
|
from shutil import which
|
||||||
|
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
|
||||||
subprocess.call([open_command, filename])
|
subprocess.call([open_command, filename])
|
||||||
|
|
||||||
|
|
||||||
@@ -173,7 +172,9 @@ class UniqueKeyLoader(SafeLoader):
|
|||||||
|
|
||||||
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
|
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
|
||||||
parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader)
|
parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader)
|
||||||
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
|
unsafe_parse_yaml = functools.partial(load, Loader=UnsafeLoader)
|
||||||
|
|
||||||
|
del load, load_all # should not be used. don't leak their names
|
||||||
|
|
||||||
|
|
||||||
def get_cert_none_ssl_context():
|
def get_cert_none_ssl_context():
|
||||||
@@ -191,11 +192,12 @@ def get_public_ipv4() -> str:
|
|||||||
ip = socket.gethostbyname(socket.gethostname())
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
ctx = get_cert_none_ssl_context()
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip()
|
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip()
|
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip()
|
||||||
except:
|
except Exception:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
pass # we could be offline, in a local game, so no point in erroring out
|
pass # we could be offline, in a local game, so no point in erroring out
|
||||||
return ip
|
return ip
|
||||||
@@ -208,7 +210,7 @@ def get_public_ipv6() -> str:
|
|||||||
ip = socket.gethostbyname(socket.gethostname())
|
ip = socket.gethostbyname(socket.gethostname())
|
||||||
ctx = get_cert_none_ssl_context()
|
ctx = get_cert_none_ssl_context()
|
||||||
try:
|
try:
|
||||||
ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip()
|
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
pass # we could be offline, in a local game, or ipv6 may not be available
|
pass # we could be offline, in a local game, or ipv6 may not be available
|
||||||
@@ -309,33 +311,19 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
|
|||||||
|
|
||||||
@cache_argsless
|
@cache_argsless
|
||||||
def get_options() -> dict:
|
def get_options() -> dict:
|
||||||
if not hasattr(get_options, "options"):
|
filenames = ("options.yaml", "host.yaml")
|
||||||
filenames = ("options.yaml", "host.yaml")
|
locations = []
|
||||||
locations = []
|
if os.path.join(os.getcwd()) != local_path():
|
||||||
if os.path.join(os.getcwd()) != local_path():
|
locations += filenames # use files from cwd only if it's not the local_path
|
||||||
locations += filenames # use files from cwd only if it's not the local_path
|
locations += [user_path(filename) for filename in filenames]
|
||||||
locations += [user_path(filename) for filename in filenames]
|
|
||||||
|
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if os.path.exists(location):
|
if os.path.exists(location):
|
||||||
with open(location) as f:
|
with open(location) as f:
|
||||||
options = parse_yaml(f.read())
|
options = parse_yaml(f.read())
|
||||||
|
return update_options(get_default_options(), options, location, list())
|
||||||
|
|
||||||
get_options.options = update_options(get_default_options(), options, location, list())
|
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
|
|
||||||
return get_options.options
|
|
||||||
|
|
||||||
|
|
||||||
def get_item_name_from_id(code: int) -> str:
|
|
||||||
from worlds import lookup_any_item_id_to_name
|
|
||||||
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
|
|
||||||
|
|
||||||
|
|
||||||
def get_location_name_from_id(code: int) -> str:
|
|
||||||
from worlds import lookup_any_location_id_to_name
|
|
||||||
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
|
|
||||||
|
|
||||||
|
|
||||||
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
||||||
@@ -344,10 +332,10 @@ def persistent_store(category: str, key: typing.Any, value: typing.Any):
|
|||||||
category = storage.setdefault(category, {})
|
category = storage.setdefault(category, {})
|
||||||
category[key] = value
|
category[key] = value
|
||||||
with open(path, "wt") as f:
|
with open(path, "wt") as f:
|
||||||
f.write(dump(storage))
|
f.write(dump(storage, Dumper=Dumper))
|
||||||
|
|
||||||
|
|
||||||
def persistent_load() -> typing.Dict[dict]:
|
def persistent_load() -> typing.Dict[str, dict]:
|
||||||
storage = getattr(persistent_load, "storage", None)
|
storage = getattr(persistent_load, "storage", None)
|
||||||
if storage:
|
if storage:
|
||||||
return storage
|
return storage
|
||||||
@@ -365,8 +353,8 @@ def persistent_load() -> typing.Dict[dict]:
|
|||||||
return storage
|
return storage
|
||||||
|
|
||||||
|
|
||||||
def get_adjuster_settings(gameName: str):
|
def get_adjuster_settings(game_name: str):
|
||||||
adjuster_settings = persistent_load().get("adjuster", {}).get(gameName, {})
|
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
|
||||||
return adjuster_settings
|
return adjuster_settings
|
||||||
|
|
||||||
|
|
||||||
@@ -382,10 +370,10 @@ def get_unique_identifier():
|
|||||||
return uuid
|
return uuid
|
||||||
|
|
||||||
|
|
||||||
safe_builtins = {
|
safe_builtins = frozenset((
|
||||||
'set',
|
'set',
|
||||||
'frozenset',
|
'frozenset',
|
||||||
}
|
))
|
||||||
|
|
||||||
|
|
||||||
class RestrictedUnpickler(pickle.Unpickler):
|
class RestrictedUnpickler(pickle.Unpickler):
|
||||||
@@ -413,8 +401,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
if issubclass(obj, self.options_module.Option):
|
if issubclass(obj, self.options_module.Option):
|
||||||
return obj
|
return obj
|
||||||
# Forbid everything else.
|
# Forbid everything else.
|
||||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
||||||
(module, name))
|
|
||||||
|
|
||||||
|
|
||||||
def restricted_loads(s):
|
def restricted_loads(s):
|
||||||
@@ -423,6 +410,9 @@ def restricted_loads(s):
|
|||||||
|
|
||||||
|
|
||||||
class KeyedDefaultDict(collections.defaultdict):
|
class KeyedDefaultDict(collections.defaultdict):
|
||||||
|
"""defaultdict variant that uses the missing key as argument to default_factory"""
|
||||||
|
default_factory: typing.Callable[[typing.Any], typing.Any]
|
||||||
|
|
||||||
def __missing__(self, key):
|
def __missing__(self, key):
|
||||||
self[key] = value = self.default_factory(key)
|
self[key] = value = self.default_factory(key)
|
||||||
return value
|
return value
|
||||||
@@ -432,6 +422,10 @@ def get_text_between(text: str, start: str, end: str) -> str:
|
|||||||
return text[text.index(start) + len(start): text.rindex(end)]
|
return text[text.index(start) + len(start): text.rindex(end)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_text_after(text: str, start: str) -> str:
|
||||||
|
return text[text.index(start) + len(start):]
|
||||||
|
|
||||||
|
|
||||||
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
|
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
|
||||||
|
|
||||||
|
|
||||||
@@ -493,11 +487,11 @@ def stream_input(stream, queue):
|
|||||||
return thread
|
return thread
|
||||||
|
|
||||||
|
|
||||||
def tkinter_center_window(window: Tk):
|
def tkinter_center_window(window: "tkinter.Tk") -> None:
|
||||||
window.update()
|
window.update()
|
||||||
xPos = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
|
x = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
|
||||||
yPos = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
|
y = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
|
||||||
window.geometry("+{}+{}".format(xPos, yPos))
|
window.geometry(f"+{x}+{y}")
|
||||||
|
|
||||||
|
|
||||||
class VersionException(Exception):
|
class VersionException(Exception):
|
||||||
@@ -514,24 +508,27 @@ def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# noinspection PyPep8Naming
|
# noinspection PyPep8Naming
|
||||||
def format_SI_prefix(value, power=1000, power_labels=('', 'k', 'M', 'G', 'T', "P", "E", "Z", "Y")) -> str:
|
def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P", "E", "Z", "Y")) -> str:
|
||||||
"""Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix"""
|
"""Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix"""
|
||||||
|
import decimal
|
||||||
n = 0
|
n = 0
|
||||||
value = decimal.Decimal(value)
|
value = decimal.Decimal(value)
|
||||||
while value >= power:
|
limit = power - decimal.Decimal("0.005")
|
||||||
|
while value >= limit:
|
||||||
value /= power
|
value /= power
|
||||||
n += 1
|
n += 1
|
||||||
|
|
||||||
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
|
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
|
||||||
|
|
||||||
|
|
||||||
def get_fuzzy_ratio(word1: str, word2: str) -> float:
|
|
||||||
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
|
||||||
/ max(len(word1), len(word2)))
|
|
||||||
|
|
||||||
|
|
||||||
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
|
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
|
||||||
-> typing.List[typing.Tuple[str, int]]:
|
-> typing.List[typing.Tuple[str, int]]:
|
||||||
|
import jellyfish
|
||||||
|
|
||||||
|
def get_fuzzy_ratio(word1: str, word2: str) -> float:
|
||||||
|
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
||||||
|
/ max(len(word1), len(word2)))
|
||||||
|
|
||||||
limit: int = limit if limit else len(wordlist)
|
limit: int = limit if limit else len(wordlist)
|
||||||
return list(
|
return list(
|
||||||
map(
|
map(
|
||||||
@@ -549,18 +546,19 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
|
|||||||
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
|
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
|
||||||
-> typing.Optional[str]:
|
-> typing.Optional[str]:
|
||||||
def run(*args: str):
|
def run(*args: str):
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None
|
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||||
|
|
||||||
if is_linux:
|
if is_linux:
|
||||||
# prefer native dialog
|
# prefer native dialog
|
||||||
kdialog = shutil.which('kdialog')
|
from shutil import which
|
||||||
|
kdialog = which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
||||||
return run(kdialog, f'--title={title}', '--getopenfilename', '.', k_filters)
|
return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
|
||||||
zenity = shutil.which('zenity')
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||||
return run(zenity, f'--title={title}', '--file-selection', *z_filters)
|
return run(zenity, f"--title={title}", "--file-selection", *z_filters)
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
try:
|
try:
|
||||||
@@ -578,10 +576,10 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
|
|||||||
|
|
||||||
def messagebox(title: str, text: str, error: bool = False) -> None:
|
def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||||
def run(*args: str):
|
def run(*args: str):
|
||||||
return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None
|
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||||
|
|
||||||
def is_kivy_running():
|
def is_kivy_running():
|
||||||
if 'kivy' in sys.modules:
|
if "kivy" in sys.modules:
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
return App.get_running_app() is not None
|
return App.get_running_app() is not None
|
||||||
return False
|
return False
|
||||||
@@ -591,14 +589,15 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
|||||||
MessageBox(title, text, error).open()
|
MessageBox(title, text, error).open()
|
||||||
return
|
return
|
||||||
|
|
||||||
if is_linux and not 'tkinter' in sys.modules:
|
if is_linux and "tkinter" not in sys.modules:
|
||||||
# prefer native dialog
|
# prefer native dialog
|
||||||
kdialog = shutil.which('kdialog')
|
from shutil import which
|
||||||
|
kdialog = which("kdialog")
|
||||||
if kdialog:
|
if kdialog:
|
||||||
return run(kdialog, f'--title={title}', '--error' if error else '--msgbox', text)
|
return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
|
||||||
zenity = shutil.which('zenity')
|
zenity = which("zenity")
|
||||||
if zenity:
|
if zenity:
|
||||||
return run(zenity, f'--title={title}', f'--text={text}', '--error' if error else '--info')
|
return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
|
||||||
|
|
||||||
# fall back to tk
|
# fall back to tk
|
||||||
try:
|
try:
|
||||||
@@ -613,3 +612,14 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
|||||||
root.withdraw()
|
root.withdraw()
|
||||||
showerror(title, text) if error else showinfo(title, text)
|
showerror(title, text) if error else showinfo(title, text)
|
||||||
root.update()
|
root.update()
|
||||||
|
|
||||||
|
|
||||||
|
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
|
||||||
|
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
||||||
|
def sorter(element: str) -> str:
|
||||||
|
parts = element.split(maxsplit=1)
|
||||||
|
if parts[0].lower() in ignore:
|
||||||
|
return parts[1]
|
||||||
|
else:
|
||||||
|
return element
|
||||||
|
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))
|
||||||
|
|||||||
39
WebHost.py
39
WebHost.py
@@ -14,7 +14,7 @@ import Utils
|
|||||||
|
|
||||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
from WebHostLib import app as raw_app
|
from WebHostLib import register, app as raw_app
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
|
||||||
from WebHostLib.models import db
|
from WebHostLib.models import db
|
||||||
@@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen
|
|||||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||||
from WebHostLib.options import create as create_options_files
|
from WebHostLib.options import create as create_options_files
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
|
||||||
|
|
||||||
configpath = os.path.abspath("config.yaml")
|
configpath = os.path.abspath("config.yaml")
|
||||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||||
|
|
||||||
|
|
||||||
def get_app():
|
def get_app():
|
||||||
|
register()
|
||||||
app = raw_app
|
app = raw_app
|
||||||
if os.path.exists(configpath):
|
if os.path.exists(configpath):
|
||||||
import yaml
|
import yaml
|
||||||
@@ -43,19 +42,39 @@ def get_app():
|
|||||||
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
|
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
zfile: zipfile.ZipInfo
|
||||||
|
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
worlds = {}
|
worlds = {}
|
||||||
data = []
|
data = []
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'):
|
if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'):
|
||||||
worlds[game] = world
|
worlds[game] = world
|
||||||
|
|
||||||
|
base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs")
|
||||||
for game, world in worlds.items():
|
for game, world in worlds.items():
|
||||||
# copy files from world's docs folder to the generated folder
|
# copy files from world's docs folder to the generated folder
|
||||||
source_path = Utils.local_path(os.path.dirname(sys.modules[world.__module__].__file__), 'docs')
|
target_path = os.path.join(base_target_path, game)
|
||||||
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game)
|
os.makedirs(target_path, exist_ok=True)
|
||||||
files = os.listdir(source_path)
|
|
||||||
for file in files:
|
if world.zip_path:
|
||||||
os.makedirs(os.path.dirname(Utils.local_path(target_path, file)), exist_ok=True)
|
zipfile_path = world.zip_path
|
||||||
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
|
|
||||||
|
assert os.path.isfile(zipfile_path), f"{zipfile_path} is not a valid file(path)."
|
||||||
|
assert zipfile.is_zipfile(zipfile_path), f"{zipfile_path} is not a valid zipfile."
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zipfile_path) as zf:
|
||||||
|
for zfile in zf.infolist():
|
||||||
|
if not zfile.is_dir() and "/docs/" in zfile.filename:
|
||||||
|
zf.extract(zfile, target_path)
|
||||||
|
else:
|
||||||
|
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
|
||||||
|
files = os.listdir(source_path)
|
||||||
|
for file in files:
|
||||||
|
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
|
||||||
|
|
||||||
# build a json tutorial dict per game
|
# build a json tutorial dict per game
|
||||||
game_data = {'gameTitle': game, 'tutorials': []}
|
game_data = {'gameTitle': game, 'tutorials': []}
|
||||||
for tutorial in world.web.tutorials:
|
for tutorial in world.web.tutorials:
|
||||||
@@ -85,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
|||||||
for games in data:
|
for games in data:
|
||||||
if 'Archipelago' in games['gameTitle']:
|
if 'Archipelago' in games['gameTitle']:
|
||||||
generic_data = data.pop(data.index(games))
|
generic_data = data.pop(data.index(games))
|
||||||
sorted_data = [generic_data] + sorted(data, key=lambda entry: entry["gameTitle"].lower())
|
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower())
|
||||||
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
|
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
|
||||||
return sorted_data
|
return sorted_data
|
||||||
|
|
||||||
|
|||||||
46
WebHostLib/README.md
Normal file
46
WebHostLib/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# WebHost
|
||||||
|
|
||||||
|
## Contribution Guidelines
|
||||||
|
**Thank you for your interest in contributing to the Archipelago website!**
|
||||||
|
Much of the content on the website is generated automatically, but there are some things
|
||||||
|
that need a personal touch. For those things, we rely on contributions from both the core
|
||||||
|
team and the community. The current primary maintainer of the website is Farrak Kilhn.
|
||||||
|
He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`.
|
||||||
|
|
||||||
|
### Small Changes
|
||||||
|
Little changes like adding a button or a couple new select elements are perfectly fine.
|
||||||
|
Tweaks to style specific to a PR's content are also probably not a problem. For example, if
|
||||||
|
you build a new page which needs two side by side tables, and you need to write a CSS file
|
||||||
|
specific to your page, that is perfectly reasonable.
|
||||||
|
|
||||||
|
### Content Additions
|
||||||
|
Once you develop a new feature or add new content the website, make a pull request. It will
|
||||||
|
be reviewed by the community and there will probably be some discussion around it. Depending
|
||||||
|
on the size of the feature, and if new styles are required, there may be an additional step
|
||||||
|
before the PR is accepted wherein Farrak works with the designer to implement styles.
|
||||||
|
|
||||||
|
### Restrictions on Style Changes
|
||||||
|
A professional designer is paid to develop the styles and assets for the Archipelago website.
|
||||||
|
In an effort to maintain a consistent look and feel, pull requests which *exclusively*
|
||||||
|
change site styles are rejected. Please note this applies to code which changes the overall
|
||||||
|
look and feel of the site, not to small tweaks to CSS for your custom page. The intention
|
||||||
|
behind these restrictions is to maintain a curated feel for the design of the site. If
|
||||||
|
any PR affects the overall feel of the site but includes additive changes, there will
|
||||||
|
likely be a conversation about how to implement those changes without compromising the
|
||||||
|
curated site style. It is therefore worth noting there are a couple files which, if
|
||||||
|
changed in your pull request, will cause it to draw additional scrutiny.
|
||||||
|
|
||||||
|
These closely guarded files are:
|
||||||
|
- `globalStyles.css`
|
||||||
|
- `islandFooter.css`
|
||||||
|
- `landing.css`
|
||||||
|
- `markdown.css`
|
||||||
|
- `tooltip.css`
|
||||||
|
|
||||||
|
### Site Themes
|
||||||
|
There are several themes available for game pages. It is possible to request a new theme in
|
||||||
|
the `#art-and-design` channel on Discord. Because themes are created by the designer, they
|
||||||
|
are not free, and take some time to create. Farrak works closely with the designer to implement
|
||||||
|
these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year
|
||||||
|
are added. If a proposed theme seems like a cool idea and the community likes it, there is a
|
||||||
|
good chance it will become a reality.
|
||||||
@@ -3,13 +3,13 @@ import uuid
|
|||||||
import base64
|
import base64
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import jinja2.exceptions
|
|
||||||
from pony.flask import Pony
|
from pony.flask import Pony
|
||||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
from flask import Flask
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
from flask_compress import Compress
|
from flask_compress import Compress
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from werkzeug.routing import BaseConverter
|
||||||
|
|
||||||
|
from Utils import title_sorted
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
UPLOAD_FOLDER = os.path.relpath('uploads')
|
UPLOAD_FOLDER = os.path.relpath('uploads')
|
||||||
@@ -53,8 +53,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg"
|
|||||||
cache = Cache(app)
|
cache = Cache(app)
|
||||||
Compress(app)
|
Compress(app)
|
||||||
|
|
||||||
from werkzeug.routing import BaseConverter
|
|
||||||
|
|
||||||
|
|
||||||
class B64UUIDConverter(BaseConverter):
|
class B64UUIDConverter(BaseConverter):
|
||||||
|
|
||||||
@@ -68,174 +66,18 @@ class B64UUIDConverter(BaseConverter):
|
|||||||
# short UUID
|
# short UUID
|
||||||
app.url_map.converters["suuid"] = B64UUIDConverter
|
app.url_map.converters["suuid"] = B64UUIDConverter
|
||||||
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
|
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
|
||||||
|
app.jinja_env.filters["title_sorted"] = title_sorted
|
||||||
# has automatic patch integration
|
|
||||||
import Patch
|
|
||||||
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
|
|
||||||
|
|
||||||
|
|
||||||
def get_world_theme(game_name: str):
|
def register():
|
||||||
if game_name in AutoWorldRegister.world_types:
|
"""Import submodules, triggering their registering on flask routing.
|
||||||
return AutoWorldRegister.world_types[game_name].web.theme
|
Note: initializes worlds subsystem."""
|
||||||
return 'grass'
|
# has automatic patch integration
|
||||||
|
import Patch
|
||||||
|
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
|
||||||
|
|
||||||
|
from WebHostLib.customserver import run_server_process
|
||||||
|
# to trigger app routing picking up on it
|
||||||
|
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
|
||||||
|
|
||||||
@app.before_request
|
app.register_blueprint(api.api_endpoints)
|
||||||
def register_session():
|
|
||||||
session.permanent = True # technically 31 days after the last visit
|
|
||||||
if not session.get("_id", None):
|
|
||||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
|
||||||
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
|
||||||
def page_not_found(err):
|
|
||||||
return render_template('404.html'), 404
|
|
||||||
|
|
||||||
|
|
||||||
# Start Playing Page
|
|
||||||
@app.route('/start-playing')
|
|
||||||
def start_playing():
|
|
||||||
return render_template(f"startPlaying.html")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/weighted-settings')
|
|
||||||
def weighted_settings():
|
|
||||||
return render_template(f"weighted-settings.html")
|
|
||||||
|
|
||||||
|
|
||||||
# Player settings pages
|
|
||||||
@app.route('/games/<string:game>/player-settings')
|
|
||||||
def player_settings(game):
|
|
||||||
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
|
||||||
|
|
||||||
|
|
||||||
# Game Info Pages
|
|
||||||
@app.route('/games/<string:game>/info/<string:lang>')
|
|
||||||
def game_info(game, lang):
|
|
||||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
|
||||||
|
|
||||||
|
|
||||||
# List of supported games
|
|
||||||
@app.route('/games')
|
|
||||||
def games():
|
|
||||||
worlds = {}
|
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
|
||||||
if not world.hidden:
|
|
||||||
worlds[game] = world
|
|
||||||
return render_template("supportedGames.html", worlds=worlds)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
|
||||||
def tutorial(game, file, lang):
|
|
||||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial/')
|
|
||||||
def tutorial_landing():
|
|
||||||
worlds = {}
|
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
|
||||||
if not world.hidden:
|
|
||||||
worlds[game] = world
|
|
||||||
return render_template("tutorialLanding.html")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/faq/<string:lang>/')
|
|
||||||
def faq(lang):
|
|
||||||
return render_template("faq.html", lang=lang)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/glossary/<string:lang>/')
|
|
||||||
def terms(lang):
|
|
||||||
return render_template("glossary.html", lang=lang)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/seed/<suuid:seed>')
|
|
||||||
def view_seed(seed: UUID):
|
|
||||||
seed = Seed.get(id=seed)
|
|
||||||
if not seed:
|
|
||||||
abort(404)
|
|
||||||
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/new_room/<suuid:seed>')
|
|
||||||
def new_room(seed: UUID):
|
|
||||||
seed = Seed.get(id=seed)
|
|
||||||
if not seed:
|
|
||||||
abort(404)
|
|
||||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
|
||||||
commit()
|
|
||||||
return redirect(url_for("host_room", room=room.id))
|
|
||||||
|
|
||||||
|
|
||||||
def _read_log(path: str):
|
|
||||||
if os.path.exists(path):
|
|
||||||
with open(path, encoding="utf-8-sig") as log:
|
|
||||||
yield from log
|
|
||||||
else:
|
|
||||||
yield f"Logfile {path} does not exist. " \
|
|
||||||
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/log/<suuid:room>')
|
|
||||||
def display_log(room: UUID):
|
|
||||||
room = Room.get(id=room)
|
|
||||||
if room is None:
|
|
||||||
return abort(404)
|
|
||||||
if room.owner == session["_id"]:
|
|
||||||
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
|
||||||
return "Access Denied", 403
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
|
||||||
def host_room(room: UUID):
|
|
||||||
room = Room.get(id=room)
|
|
||||||
if room is None:
|
|
||||||
return abort(404)
|
|
||||||
if request.method == "POST":
|
|
||||||
if room.owner == session["_id"]:
|
|
||||||
cmd = request.form["cmd"]
|
|
||||||
if cmd:
|
|
||||||
Command(room=room, commandtext=cmd)
|
|
||||||
commit()
|
|
||||||
|
|
||||||
with db_session:
|
|
||||||
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
|
|
||||||
|
|
||||||
return render_template("hostRoom.html", room=room)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
|
||||||
def favicon():
|
|
||||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
|
||||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/discord')
|
|
||||||
def discord():
|
|
||||||
return redirect("https://discord.gg/archipelago")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/datapackage')
|
|
||||||
@cache.cached()
|
|
||||||
def get_datapackge():
|
|
||||||
"""A pretty print version of /api/datapackage"""
|
|
||||||
from worlds import network_data_package
|
|
||||||
import json
|
|
||||||
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/index')
|
|
||||||
@app.route('/sitemap')
|
|
||||||
def get_sitemap():
|
|
||||||
available_games = []
|
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
|
||||||
if not world.hidden:
|
|
||||||
available_games.append(game)
|
|
||||||
return render_template("siteMap.html", games=available_games)
|
|
||||||
|
|
||||||
|
|
||||||
from WebHostLib.customserver import run_server_process
|
|
||||||
from . import tracker, upload, landing, check, generate, downloads, api, stats # to trigger app routing picking up on it
|
|
||||||
|
|
||||||
app.register_blueprint(api.api_endpoints)
|
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ def room_info(room: UUID):
|
|||||||
|
|
||||||
@api_endpoints.route('/datapackage')
|
@api_endpoints.route('/datapackage')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def get_datapackge():
|
def get_datapackage():
|
||||||
from worlds import network_data_package
|
from worlds import network_data_package
|
||||||
return network_data_package
|
return network_data_package
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route('/datapackage_version')
|
@api_endpoints.route('/datapackage_version')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def get_datapackge_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"]
|
version_package["version"] = network_data_package["version"]
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ class MultiworldInstance():
|
|||||||
|
|
||||||
logging.info(f"Spinning up {self.room_id}")
|
logging.info(f"Spinning up {self.room_id}")
|
||||||
process = multiprocessing.Process(group=None, target=run_server_process,
|
process = multiprocessing.Process(group=None, target=run_server_process,
|
||||||
args=(self.room_id, self.ponyconfig),
|
args=(self.room_id, self.ponyconfig, get_static_server_data()),
|
||||||
name="MultiHost")
|
name="MultiHost")
|
||||||
process.start()
|
process.start()
|
||||||
# bind after start to prevent thread sync issues with guardian.
|
# bind after start to prevent thread sync issues with guardian.
|
||||||
@@ -238,5 +238,5 @@ def run_guardian():
|
|||||||
|
|
||||||
|
|
||||||
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
|
||||||
from .customserver import run_server_process
|
from .customserver import run_server_process, get_static_server_data
|
||||||
from .generate import gen_game
|
from .generate import gen_game
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import time
|
|||||||
import random
|
import random
|
||||||
import pickle
|
import pickle
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
from .models import *
|
from .models import db_session, Room, select, commit, Command, db
|
||||||
|
|
||||||
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
|
||||||
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
|
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
|
||||||
|
|
||||||
|
|
||||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||||
@@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
|
|||||||
import MultiServer
|
import MultiServer
|
||||||
|
|
||||||
MultiServer.client_message_processor = CustomClientMessageProcessor
|
MultiServer.client_message_processor = CustomClientMessageProcessor
|
||||||
del (MultiServer)
|
del MultiServer
|
||||||
|
|
||||||
|
|
||||||
class DBCommandProcessor(ServerCommandProcessor):
|
class DBCommandProcessor(ServerCommandProcessor):
|
||||||
@@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor):
|
|||||||
|
|
||||||
|
|
||||||
class WebHostContext(Context):
|
class WebHostContext(Context):
|
||||||
def __init__(self):
|
def __init__(self, static_server_data: dict):
|
||||||
|
# static server data is used during _load_game_data to load required data,
|
||||||
|
# without needing to import worlds system, which takes quite a bit of memory
|
||||||
|
self.static_server_data = static_server_data
|
||||||
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
|
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
|
||||||
|
del self.static_server_data
|
||||||
self.main_loop = asyncio.get_running_loop()
|
self.main_loop = asyncio.get_running_loop()
|
||||||
self.video = {}
|
self.video = {}
|
||||||
self.tags = ["AP", "WebHost"]
|
self.tags = ["AP", "WebHost"]
|
||||||
|
|
||||||
|
def _load_game_data(self):
|
||||||
|
for key, value in self.static_server_data.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
def listen_to_db_commands(self):
|
def listen_to_db_commands(self):
|
||||||
cmdprocessor = DBCommandProcessor(self)
|
cmdprocessor = DBCommandProcessor(self)
|
||||||
|
|
||||||
@@ -107,14 +116,32 @@ def get_random_port():
|
|||||||
return random.randint(49152, 65535)
|
return random.randint(49152, 65535)
|
||||||
|
|
||||||
|
|
||||||
def run_server_process(room_id, ponyconfig: dict):
|
@cache_argsless
|
||||||
|
def get_static_server_data() -> dict:
|
||||||
|
import worlds
|
||||||
|
data = {
|
||||||
|
"forced_auto_forfeits": {},
|
||||||
|
"non_hintable_names": {},
|
||||||
|
"gamespackage": worlds.network_data_package["games"],
|
||||||
|
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
|
||||||
|
worlds.AutoWorldRegister.world_types.items()},
|
||||||
|
}
|
||||||
|
|
||||||
|
for world_name, world in worlds.AutoWorldRegister.world_types.items():
|
||||||
|
data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit
|
||||||
|
data["non_hintable_names"][world_name] = world.hint_blacklist
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
|
||||||
# establish DB connection for multidata and multisave
|
# establish DB connection for multidata and multisave
|
||||||
db.bind(**ponyconfig)
|
db.bind(**ponyconfig)
|
||||||
db.generate_mapping(check_tables=False)
|
db.generate_mapping(check_tables=False)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
Utils.init_logging(str(room_id), write_mode="a")
|
Utils.init_logging(str(room_id), write_mode="a")
|
||||||
ctx = WebHostContext()
|
ctx = WebHostContext(static_server_data)
|
||||||
ctx.load(room_id)
|
ctx.load(room_id)
|
||||||
ctx.init_save()
|
ctx.init_save()
|
||||||
|
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ def download_patch(room_id, patch_id):
|
|||||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \
|
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \
|
||||||
f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}"
|
f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}"
|
||||||
new_file.seek(0)
|
new_file.seek(0)
|
||||||
return send_file(new_file, as_attachment=True, attachment_filename=fname)
|
return send_file(new_file, as_attachment=True, download_name=fname)
|
||||||
else:
|
else:
|
||||||
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
||||||
patch_data = BytesIO(patch_data)
|
patch_data = BytesIO(patch_data)
|
||||||
|
|
||||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
|
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
|
||||||
f"{preferred_endings[patch.game]}"
|
f"{preferred_endings[patch.game]}"
|
||||||
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
|
return send_file(patch_data, as_attachment=True, download_name=fname)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/dl_spoiler/<suuid:seed_id>")
|
@app.route("/dl_spoiler/<suuid:seed_id>")
|
||||||
@@ -66,7 +66,7 @@ def download_slot_file(room_id, player_id: int):
|
|||||||
from worlds.minecraft import mc_update_output
|
from worlds.minecraft import mc_update_output
|
||||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
|
||||||
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
|
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
|
||||||
return send_file(io.BytesIO(data), as_attachment=True, attachment_filename=fname)
|
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
|
||||||
elif slot_data.game == "Factorio":
|
elif slot_data.game == "Factorio":
|
||||||
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||||
for name in zf.namelist():
|
for name in zf.namelist():
|
||||||
@@ -82,7 +82,7 @@ def download_slot_file(room_id, player_id: int):
|
|||||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
|
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
|
||||||
else:
|
else:
|
||||||
return "Game download not supported."
|
return "Game download not supported."
|
||||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
|
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/templates")
|
@app.route("/templates")
|
||||||
|
|||||||
173
WebHostLib/misc.py
Normal file
173
WebHostLib/misc.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
import jinja2.exceptions
|
||||||
|
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||||
|
|
||||||
|
from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
from . import app, cache
|
||||||
|
|
||||||
|
|
||||||
|
def get_world_theme(game_name: str):
|
||||||
|
if game_name in AutoWorldRegister.world_types:
|
||||||
|
return AutoWorldRegister.world_types[game_name].web.theme
|
||||||
|
return 'grass'
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def register_session():
|
||||||
|
session.permanent = True # technically 31 days after the last visit
|
||||||
|
if not session.get("_id", None):
|
||||||
|
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
||||||
|
def page_not_found(err):
|
||||||
|
return render_template('404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
# Start Playing Page
|
||||||
|
@app.route('/start-playing')
|
||||||
|
def start_playing():
|
||||||
|
return render_template(f"startPlaying.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/weighted-settings')
|
||||||
|
def weighted_settings():
|
||||||
|
return render_template(f"weighted-settings.html")
|
||||||
|
|
||||||
|
|
||||||
|
# Player settings pages
|
||||||
|
@app.route('/games/<string:game>/player-settings')
|
||||||
|
def player_settings(game):
|
||||||
|
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
|
# Game Info Pages
|
||||||
|
@app.route('/games/<string:game>/info/<string:lang>')
|
||||||
|
def game_info(game, lang):
|
||||||
|
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
|
# List of supported games
|
||||||
|
@app.route('/games')
|
||||||
|
def games():
|
||||||
|
worlds = {}
|
||||||
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
|
if not world.hidden:
|
||||||
|
worlds[game] = world
|
||||||
|
return render_template("supportedGames.html", worlds=worlds)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||||
|
def tutorial(game, file, lang):
|
||||||
|
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tutorial/')
|
||||||
|
def tutorial_landing():
|
||||||
|
worlds = {}
|
||||||
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
|
if not world.hidden:
|
||||||
|
worlds[game] = world
|
||||||
|
return render_template("tutorialLanding.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/faq/<string:lang>/')
|
||||||
|
def faq(lang):
|
||||||
|
return render_template("faq.html", lang=lang)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/glossary/<string:lang>/')
|
||||||
|
def terms(lang):
|
||||||
|
return render_template("glossary.html", lang=lang)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/seed/<suuid:seed>')
|
||||||
|
def view_seed(seed: UUID):
|
||||||
|
seed = Seed.get(id=seed)
|
||||||
|
if not seed:
|
||||||
|
abort(404)
|
||||||
|
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/new_room/<suuid:seed>')
|
||||||
|
def new_room(seed: UUID):
|
||||||
|
seed = Seed.get(id=seed)
|
||||||
|
if not seed:
|
||||||
|
abort(404)
|
||||||
|
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||||
|
commit()
|
||||||
|
return redirect(url_for("host_room", room=room.id))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_log(path: str):
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, encoding="utf-8-sig") as log:
|
||||||
|
yield from log
|
||||||
|
else:
|
||||||
|
yield f"Logfile {path} does not exist. " \
|
||||||
|
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/log/<suuid:room>')
|
||||||
|
def display_log(room: UUID):
|
||||||
|
room = Room.get(id=room)
|
||||||
|
if room is None:
|
||||||
|
return abort(404)
|
||||||
|
if room.owner == session["_id"]:
|
||||||
|
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
||||||
|
return "Access Denied", 403
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
|
||||||
|
def host_room(room: UUID):
|
||||||
|
room: Room = Room.get(id=room)
|
||||||
|
if room is None:
|
||||||
|
return abort(404)
|
||||||
|
if request.method == "POST":
|
||||||
|
if room.owner == session["_id"]:
|
||||||
|
cmd = request.form["cmd"]
|
||||||
|
if cmd:
|
||||||
|
Command(room=room, commandtext=cmd)
|
||||||
|
commit()
|
||||||
|
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
# indicate that the page should reload to get the assigned port
|
||||||
|
should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3)
|
||||||
|
with db_session:
|
||||||
|
room.last_activity = now # will trigger a spinup, if it's not already running
|
||||||
|
|
||||||
|
return render_template("hostRoom.html", room=room, should_refresh=should_refresh)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/favicon.ico')
|
||||||
|
def favicon():
|
||||||
|
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||||
|
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/discord')
|
||||||
|
def discord():
|
||||||
|
return redirect("https://discord.gg/archipelago")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/datapackage')
|
||||||
|
@cache.cached()
|
||||||
|
def get_datapackage():
|
||||||
|
"""A pretty print version of /api/datapackage"""
|
||||||
|
from worlds import network_data_package
|
||||||
|
import json
|
||||||
|
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/index')
|
||||||
|
@app.route('/sitemap')
|
||||||
|
def get_sitemap():
|
||||||
|
available_games = []
|
||||||
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
|
if not world.hidden:
|
||||||
|
available_games.append(game)
|
||||||
|
return render_template("siteMap.html", games=available_games)
|
||||||
@@ -60,7 +60,7 @@ def create():
|
|||||||
|
|
||||||
for game_name, world in AutoWorldRegister.world_types.items():
|
for game_name, world in AutoWorldRegister.world_types.items():
|
||||||
|
|
||||||
all_options = {**Options.per_game_common_options, **world.options}
|
all_options = {**Options.per_game_common_options, **world.option_definitions}
|
||||||
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
|
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
|
||||||
options=all_options,
|
options=all_options,
|
||||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||||
@@ -110,7 +110,7 @@ def create():
|
|||||||
if option.default == "random":
|
if option.default == "random":
|
||||||
this_option["defaultValue"] = "random"
|
this_option["defaultValue"] = "random"
|
||||||
|
|
||||||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
elif issubclass(option, Options.Range):
|
||||||
game_options[option_name] = {
|
game_options[option_name] = {
|
||||||
"type": "range",
|
"type": "range",
|
||||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||||
@@ -121,7 +121,7 @@ def create():
|
|||||||
"max": option.range_end,
|
"max": option.range_end,
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasattr(option, "special_range_names"):
|
if issubclass(option, Options.SpecialRange):
|
||||||
game_options[option_name]["type"] = 'special_range'
|
game_options[option_name]["type"] = 'special_range'
|
||||||
game_options[option_name]["value_names"] = {}
|
game_options[option_name]["value_names"] = {}
|
||||||
for key, val in option.special_range_names.items():
|
for key, val in option.special_range_names.items():
|
||||||
@@ -141,7 +141,7 @@ def create():
|
|||||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||||
}
|
}
|
||||||
|
|
||||||
elif hasattr(option, "valid_keys"):
|
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
|
||||||
if option.valid_keys:
|
if option.valid_keys:
|
||||||
game_options[option_name] = {
|
game_options[option_name] = {
|
||||||
"type": "custom-list",
|
"type": "custom-list",
|
||||||
|
|||||||
@@ -18,15 +18,16 @@ from .models import Room
|
|||||||
PLOT_WIDTH = 600
|
PLOT_WIDTH = 600
|
||||||
|
|
||||||
|
|
||||||
def get_db_data() -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]:
|
def get_db_data(known_games: str) -> typing.Tuple[typing.Dict[str, int], typing.Dict[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:
|
||||||
total_games[slot.game] += 1
|
if slot.game in known_games:
|
||||||
games_played[room.creation_time.date()][slot.game] += 1
|
total_games[slot.game] += 1
|
||||||
|
games_played[room.creation_time.date()][slot.game] += 1
|
||||||
return total_games, games_played
|
return total_games, games_played
|
||||||
|
|
||||||
|
|
||||||
@@ -73,10 +74,12 @@ def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing.
|
|||||||
@app.route('/stats')
|
@app.route('/stats')
|
||||||
@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty
|
@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty
|
||||||
def stats():
|
def stats():
|
||||||
|
from worlds import network_data_package
|
||||||
|
known_games = set(network_data_package["games"])
|
||||||
plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date",
|
plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date",
|
||||||
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500)
|
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500)
|
||||||
|
|
||||||
total_games, games_played = get_db_data()
|
total_games, games_played = get_db_data(known_games)
|
||||||
days = sorted(games_played)
|
days = sorted(games_played)
|
||||||
|
|
||||||
color_palette = get_color_palette(len(total_games))
|
color_palette = get_color_palette(len(total_games))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Multiworld {{ room.id|suuid }}</title>
|
<title>Multiworld {{ room.id|suuid }}</title>
|
||||||
|
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
{% include 'header/oceanHeader.html' %}
|
{% include 'header/oceanHeader.html' %}
|
||||||
<div id="games" class="markdown">
|
<div id="games" class="markdown">
|
||||||
<h1>Currently Supported Games</h1>
|
<h1>Currently Supported Games</h1>
|
||||||
{% for game_name, world in worlds.items() | sort(attribute=0) %}
|
{% for game_name in worlds | title_sorted %}
|
||||||
|
{% set world = worlds[game_name] %}
|
||||||
<h2>{{ game_name }}</h2>
|
<h2>{{ game_name }}</h2>
|
||||||
<p>
|
<p>
|
||||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from worlds.alttp import Items
|
|||||||
from WebHostLib import app, cache, Room
|
from WebHostLib import app, cache, Room
|
||||||
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
|
||||||
from MultiServer import get_item_name_from_id, Context
|
from MultiServer import Context
|
||||||
from NetUtils import SlotType
|
from NetUtils import SlotType
|
||||||
|
|
||||||
alttp_icons = {
|
alttp_icons = {
|
||||||
@@ -987,10 +987,10 @@ def getTracker(tracker: UUID):
|
|||||||
if game_state == 30:
|
if game_state == 30:
|
||||||
inventory[team][player][106] = 1 # Triforce
|
inventory[team][player][106] = 1 # Triforce
|
||||||
|
|
||||||
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups}
|
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||||
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups}
|
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
|
||||||
for loc_data in locations.values():
|
for loc_data in locations.values():
|
||||||
for values in loc_data.values():
|
for values in loc_data.values():
|
||||||
item_id, item_player, flags = values
|
item_id, item_player, flags = values
|
||||||
|
|
||||||
if item_id in ids_big_key:
|
if item_id in ids_big_key:
|
||||||
@@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID):
|
|||||||
for (team, player), data in multisave.get("video", []):
|
for (team, player), data in multisave.get("video", []):
|
||||||
video[(team, player)] = data
|
video[(team, player)] = data
|
||||||
|
|
||||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
|
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
|
||||||
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
||||||
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
|
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
|
||||||
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
||||||
|
|||||||
25
docs/apworld specification.md
Normal file
25
docs/apworld specification.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# apworld Specification
|
||||||
|
|
||||||
|
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).
|
||||||
|
See [world api.md](world api.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`
|
||||||
|
file into the worlds folder.
|
||||||
|
|
||||||
|
|
||||||
|
## File Format
|
||||||
|
|
||||||
|
apworld files are zip archives with the case-sensitive file ending `.apworld`.
|
||||||
|
The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in
|
||||||
|
the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`.
|
||||||
|
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
No metadata is specified yet.
|
||||||
|
|
||||||
|
|
||||||
|
## Extra Data
|
||||||
|
|
||||||
|
The zip can contain arbitrary files in addition what was specified above.
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 246 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 82 KiB |
BIN
docs/network diagram/network diagram.jpg
Normal file
BIN
docs/network diagram/network diagram.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 526 KiB |
@@ -69,6 +69,12 @@ flowchart LR
|
|||||||
end
|
end
|
||||||
SNI <-- Various, depending on SNES device --> SMZ
|
SNI <-- Various, depending on SNES device --> SMZ
|
||||||
|
|
||||||
|
%% Donkey Kong Country 3
|
||||||
|
subgraph Donkey Kong Country 3
|
||||||
|
DK3[SNES]
|
||||||
|
end
|
||||||
|
SNI <-- Various, depending on SNES device --> DK3
|
||||||
|
|
||||||
%% Native Clients or Games
|
%% Native Clients or Games
|
||||||
%% Games or clients which compile to native or which the client is integrated in the game.
|
%% Games or clients which compile to native or which the client is integrated in the game.
|
||||||
subgraph "Native"
|
subgraph "Native"
|
||||||
@@ -82,10 +88,12 @@ flowchart LR
|
|||||||
MT[Meritous]
|
MT[Meritous]
|
||||||
TW[The Witness]
|
TW[The Witness]
|
||||||
SA2B[Sonic Adventure 2: Battle]
|
SA2B[Sonic Adventure 2: Battle]
|
||||||
|
DS3[Dark Souls 3]
|
||||||
|
|
||||||
APCLIENTPP <--> SOE
|
APCLIENTPP <--> SOE
|
||||||
APCLIENTPP <--> MT
|
APCLIENTPP <--> MT
|
||||||
APCLIENTPP <-- The Witness Randomizer --> TW
|
APCLIENTPP <-- The Witness Randomizer --> TW
|
||||||
|
APCLIENTPP <--> DS3
|
||||||
APCPP <--> SM64
|
APCPP <--> SM64
|
||||||
APCPP <--> V6
|
APCPP <--> V6
|
||||||
APCPP <--> SA2B
|
APCPP <--> SA2B
|
||||||
1
docs/network diagram/network diagram.svg
Normal file
1
docs/network diagram/network diagram.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 92 KiB |
@@ -501,7 +501,7 @@ Color options:
|
|||||||
* green_bg
|
* green_bg
|
||||||
* yellow_bg
|
* yellow_bg
|
||||||
* blue_bg
|
* blue_bg
|
||||||
* purple_bg
|
* magenta_bg
|
||||||
* cyan_bg
|
* cyan_bg
|
||||||
* white_bg
|
* white_bg
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ inside a World object.
|
|||||||
|
|
||||||
Players provide customized settings for their World in the form of yamls.
|
Players provide customized settings for their World in the form of yamls.
|
||||||
Those are accessible through `self.world.<option_name>[self.player]`. A dict
|
Those are accessible through `self.world.<option_name>[self.player]`. A dict
|
||||||
of valid options has to be provided in `self.options`. Options are automatically
|
of valid options has to be provided in `self.option_definitions`. Options are automatically
|
||||||
added to the `World` object for easy access.
|
added to the `World` object for easy access.
|
||||||
|
|
||||||
### World Options
|
### World Options
|
||||||
@@ -252,7 +252,7 @@ to describe it and a `display_name` property for display on the website and in
|
|||||||
spoiler logs.
|
spoiler logs.
|
||||||
|
|
||||||
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
|
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
|
||||||
assigned to the world under `self.options`.
|
assigned to the world under `self.option_definitions`.
|
||||||
|
|
||||||
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
|
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
|
||||||
For more see `Options.py` in AP's base directory.
|
For more see `Options.py` in AP's base directory.
|
||||||
@@ -328,7 +328,7 @@ from .Options import mygame_options # import the options dict
|
|||||||
|
|
||||||
class MyGameWorld(World):
|
class MyGameWorld(World):
|
||||||
#...
|
#...
|
||||||
options = mygame_options # assign the options dict to the world
|
option_definitions = mygame_options # assign the options dict to the world
|
||||||
#...
|
#...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
|
|||||||
class MyGameWorld(World):
|
class MyGameWorld(World):
|
||||||
"""Insert description of the world/game here."""
|
"""Insert description of the world/game here."""
|
||||||
game: str = "My Game" # name of the game/world
|
game: str = "My Game" # name of the game/world
|
||||||
options = 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_items: bool = False # True if all items come from the server
|
||||||
remote_start_inventory: bool = False # True if start inventory comes from the server
|
remote_start_inventory: bool = False # True if start inventory comes from the server
|
||||||
|
|||||||
8
setup.py
8
setup.py
@@ -17,7 +17,7 @@ from Launcher import components, icon_paths
|
|||||||
# 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
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
requirement = 'cx-Freeze==6.10'
|
requirement = 'cx-Freeze>=6.11'
|
||||||
try:
|
try:
|
||||||
pkg_resources.require(requirement)
|
pkg_resources.require(requirement)
|
||||||
import cx_Freeze
|
import cx_Freeze
|
||||||
@@ -70,7 +70,7 @@ def _threaded_hash(filepath):
|
|||||||
|
|
||||||
|
|
||||||
# cx_Freeze's build command runs other commands. Override to accept --yes and store that.
|
# cx_Freeze's build command runs other commands. Override to accept --yes and store that.
|
||||||
class BuildCommand(cx_Freeze.dist.build):
|
class BuildCommand(cx_Freeze.command.build.Build):
|
||||||
user_options = [
|
user_options = [
|
||||||
('yes', 'y', 'Answer "yes" to all questions.'),
|
('yes', 'y', 'Answer "yes" to all questions.'),
|
||||||
]
|
]
|
||||||
@@ -87,8 +87,8 @@ class BuildCommand(cx_Freeze.dist.build):
|
|||||||
|
|
||||||
|
|
||||||
# Override cx_Freeze's build_exe command for pre and post build steps
|
# Override cx_Freeze's build_exe command for pre and post build steps
|
||||||
class BuildExeCommand(cx_Freeze.dist.build_exe):
|
class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||||
user_options = cx_Freeze.dist.build_exe.user_options + [
|
user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
|
||||||
('yes', 'y', 'Answer "yes" to all questions.'),
|
('yes', 'y', 'Answer "yes" to all questions.'),
|
||||||
('extra-data=', None, 'Additional files to add.'),
|
('extra-data=', None, 'Additional files to add.'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class TestDungeon(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
args = Namespace()
|
args = Namespace()
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
self.world.set_options(args)
|
self.world.set_options(args)
|
||||||
self.world.set_default_common_options()
|
self.world.set_default_common_options()
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ class PlayerDefinition(object):
|
|||||||
region_name = "player" + str(self.id) + region_tag
|
region_name = "player" + str(self.id) + region_tag
|
||||||
region = Region("player" + str(self.id) + region_tag, RegionType.Generic,
|
region = Region("player" + str(self.id) + region_tag, RegionType.Generic,
|
||||||
"Region Hint", self.id, self.world)
|
"Region Hint", self.id, self.world)
|
||||||
self.locations += generate_locations(size,
|
self.locations += generate_locations(size, self.id, None, region, region_tag)
|
||||||
self.id, None, region, region_tag)
|
|
||||||
|
|
||||||
entrance = Entrance(self.id, region_name + "_entrance", parent)
|
entrance = Entrance(self.id, region_name + "_entrance", parent)
|
||||||
parent.exits.append(entrance)
|
parent.exits.append(entrance)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def setup_default_world(world_type) -> MultiWorld:
|
|||||||
world.player_name = {1: "Tester"}
|
world.player_name = {1: "Tester"}
|
||||||
world.set_seed()
|
world.set_seed()
|
||||||
args = Namespace()
|
args = Namespace()
|
||||||
for name, option in world_type.options.items():
|
for name, option in world_type.option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
world.set_options(args)
|
world.set_options(args)
|
||||||
world.set_default_common_options()
|
world.set_default_common_options()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class TestInverted(TestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
args = Namespace()
|
args = Namespace()
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
self.world.set_options(args)
|
self.world.set_options(args)
|
||||||
self.world.set_default_common_options()
|
self.world.set_default_common_options()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TestInvertedBombRules(unittest.TestCase):
|
|||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
self.world.mode[1] = "inverted"
|
self.world.mode[1] = "inverted"
|
||||||
args = Namespace
|
args = Namespace
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
self.world.set_options(args)
|
self.world.set_options(args)
|
||||||
self.world.set_default_common_options()
|
self.world.set_default_common_options()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TestInvertedMinor(TestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
args = Namespace()
|
args = Namespace()
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
self.world.set_options(args)
|
self.world.set_options(args)
|
||||||
self.world.set_default_common_options()
|
self.world.set_default_common_options()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class TestInvertedOWG(TestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
args = Namespace()
|
args = Namespace()
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
self.world.set_options(args)
|
self.world.set_options(args)
|
||||||
self.world.set_default_common_options()
|
self.world.set_default_common_options()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TestMinor(TestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
args = Namespace()
|
args = Namespace()
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
self.world.set_options(args)
|
self.world.set_options(args)
|
||||||
self.world.set_default_common_options()
|
self.world.set_default_common_options()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class TestVanillaOWG(TestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
args = Namespace()
|
args = Namespace()
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
self.world.set_options(args)
|
self.world.set_options(args)
|
||||||
self.world.set_default_common_options()
|
self.world.set_default_common_options()
|
||||||
|
|||||||
44
test/utils/TestSIPrefix.py
Normal file
44
test/utils/TestSIPrefix.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Tests for SI prefix in Utils.py
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from decimal import Decimal
|
||||||
|
from Utils import format_SI_prefix
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateMain(unittest.TestCase):
|
||||||
|
"""This tests SI prefix formatting in Utils.py"""
|
||||||
|
def assertEqual(self, first, second, msg=None):
|
||||||
|
# we strip spaces everywhere because that is an undefined implementation detail
|
||||||
|
super().assertEqual(first.replace(" ", ""), second.replace(" ", ""), msg)
|
||||||
|
|
||||||
|
def test_rounding(self):
|
||||||
|
# we don't care if float(999.995) would fail due to error in precision
|
||||||
|
self.assertEqual(format_SI_prefix(999.999), "1.00k")
|
||||||
|
self.assertEqual(format_SI_prefix(1000.001), "1.00k")
|
||||||
|
self.assertEqual(format_SI_prefix(Decimal("999.995")), "1.00k")
|
||||||
|
self.assertEqual(format_SI_prefix(Decimal("1000.004")), "1.00k")
|
||||||
|
|
||||||
|
def test_letters(self):
|
||||||
|
self.assertEqual(format_SI_prefix(0e0), "0.00")
|
||||||
|
self.assertEqual(format_SI_prefix(1e3), "1.00k")
|
||||||
|
self.assertEqual(format_SI_prefix(2e6), "2.00M")
|
||||||
|
self.assertEqual(format_SI_prefix(3e9), "3.00G")
|
||||||
|
self.assertEqual(format_SI_prefix(4e12), "4.00T")
|
||||||
|
self.assertEqual(format_SI_prefix(5e15), "5.00P")
|
||||||
|
self.assertEqual(format_SI_prefix(6e18), "6.00E")
|
||||||
|
self.assertEqual(format_SI_prefix(7e21), "7.00Z")
|
||||||
|
self.assertEqual(format_SI_prefix(8e24), "8.00Y")
|
||||||
|
|
||||||
|
def test_multiple_letters(self):
|
||||||
|
self.assertEqual(format_SI_prefix(9e27), "9.00kY")
|
||||||
|
|
||||||
|
def test_custom_power(self):
|
||||||
|
self.assertEqual(format_SI_prefix(1023.99, 1024), "1023.99")
|
||||||
|
self.assertEqual(format_SI_prefix(1034.24, 1024), "1.01k")
|
||||||
|
|
||||||
|
def test_custom_labels(self):
|
||||||
|
labels = ("E", "da", "h", "k")
|
||||||
|
self.assertEqual(format_SI_prefix(1, 10, labels), "1.00E")
|
||||||
|
self.assertEqual(format_SI_prefix(10, 10, labels), "1.00da")
|
||||||
|
self.assertEqual(format_SI_prefix(100, 10, labels), "1.00h")
|
||||||
|
self.assertEqual(format_SI_prefix(1000, 10, labels), "1.00k")
|
||||||
0
test/utils/__init__.py
Normal file
0
test/utils/__init__.py
Normal file
@@ -16,7 +16,7 @@ class TestVanilla(TestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
args = Namespace()
|
args = Namespace()
|
||||||
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
|
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
|
||||||
setattr(args, name, {1: option.from_any(option.default)})
|
setattr(args, name, {1: option.from_any(option.default)})
|
||||||
self.world.set_options(args)
|
self.world.set_options(args)
|
||||||
self.world.set_default_common_options()
|
self.world.set_default_common_options()
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, NamedTuple
|
import pathlib
|
||||||
|
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING
|
||||||
|
|
||||||
from BaseClasses import MultiWorld, Item, CollectionState, Location, Tutorial
|
|
||||||
from Options import Option
|
from Options import Option
|
||||||
|
from BaseClasses import CollectionState
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from BaseClasses import MultiWorld, Item, Location, Tutorial
|
||||||
|
|
||||||
|
|
||||||
class AutoWorldRegister(type):
|
class AutoWorldRegister(type):
|
||||||
@@ -41,14 +45,18 @@ class AutoWorldRegister(type):
|
|||||||
# construct class
|
# construct class
|
||||||
new_class = super().__new__(mcs, name, bases, dct)
|
new_class = super().__new__(mcs, name, bases, dct)
|
||||||
if "game" in dct:
|
if "game" in dct:
|
||||||
|
if dct["game"] in AutoWorldRegister.world_types:
|
||||||
|
raise RuntimeError(f"""Game {dct["game"]} already registered.""")
|
||||||
AutoWorldRegister.world_types[dct["game"]] = new_class
|
AutoWorldRegister.world_types[dct["game"]] = new_class
|
||||||
new_class.__file__ = sys.modules[new_class.__module__].__file__
|
new_class.__file__ = sys.modules[new_class.__module__].__file__
|
||||||
|
if ".apworld" in new_class.__file__:
|
||||||
|
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
class AutoLogicRegister(type):
|
class AutoLogicRegister(type):
|
||||||
def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister:
|
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister:
|
||||||
new_class = super().__new__(cls, name, bases, dct)
|
new_class = super().__new__(mcs, name, bases, dct)
|
||||||
function: Callable[..., Any]
|
function: Callable[..., Any]
|
||||||
for item_name, function in dct.items():
|
for item_name, function in dct.items():
|
||||||
if item_name == "copy_mixin":
|
if item_name == "copy_mixin":
|
||||||
@@ -62,12 +70,12 @@ 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(world: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
||||||
method = getattr(world.worlds[player], method_name)
|
method = getattr(world.worlds[player], method_name)
|
||||||
return method(*args)
|
return method(*args)
|
||||||
|
|
||||||
|
|
||||||
def call_all(world: MultiWorld, method_name: str, *args: Any) -> None:
|
def call_all(world: "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 world.player_ids:
|
||||||
world_types.add(world.worlds[player].__class__)
|
world_types.add(world.worlds[player].__class__)
|
||||||
@@ -79,7 +87,7 @@ def call_all(world: MultiWorld, method_name: str, *args: Any) -> None:
|
|||||||
stage_callable(world, *args)
|
stage_callable(world, *args)
|
||||||
|
|
||||||
|
|
||||||
def call_stage(world: MultiWorld, method_name: str, *args: Any) -> None:
|
def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||||
world_types = {world.worlds[player].__class__ for player in world.player_ids}
|
world_types = {world.worlds[player].__class__ for player in world.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)
|
||||||
@@ -97,7 +105,7 @@ class WebWorld:
|
|||||||
|
|
||||||
# docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial
|
# docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial
|
||||||
# class is to be used for one guide.
|
# class is to be used for one guide.
|
||||||
tutorials: List[Tutorial]
|
tutorials: List["Tutorial"]
|
||||||
|
|
||||||
# Choose a theme for your /game/* pages
|
# Choose a theme for your /game/* pages
|
||||||
# Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone
|
# Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone
|
||||||
@@ -111,7 +119,7 @@ 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."""
|
||||||
|
|
||||||
options: Dict[str, Option[Any]] = {} # link your Options mapping
|
option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping
|
||||||
game: str # name the game
|
game: str # name the game
|
||||||
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
||||||
|
|
||||||
@@ -159,8 +167,11 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
# Hide World Type from various views. Does not remove functionality.
|
# Hide World Type from various views. Does not remove functionality.
|
||||||
hidden: bool = False
|
hidden: bool = False
|
||||||
|
|
||||||
|
# see WebWorld for options
|
||||||
|
web: WebWorld = WebWorld()
|
||||||
|
|
||||||
# autoset on creation:
|
# autoset on creation:
|
||||||
world: MultiWorld
|
world: "MultiWorld"
|
||||||
player: int
|
player: int
|
||||||
|
|
||||||
# automatically generated
|
# automatically generated
|
||||||
@@ -170,9 +181,10 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
item_names: Set[str] # set of all potential item names
|
item_names: Set[str] # set of all potential item names
|
||||||
location_names: Set[str] # set of all potential location names
|
location_names: Set[str] # set of all potential location names
|
||||||
|
|
||||||
web: WebWorld = WebWorld()
|
zip_path: Optional[pathlib.Path] = None # If loaded from a .apworld, this is the Path to it.
|
||||||
|
__file__: str # path it was loaded from
|
||||||
|
|
||||||
def __init__(self, world: MultiWorld, player: int):
|
def __init__(self, world: "MultiWorld", player: int):
|
||||||
self.world = world
|
self.world = world
|
||||||
self.player = player
|
self.player = player
|
||||||
|
|
||||||
@@ -207,12 +219,12 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fill_hook(cls,
|
def fill_hook(cls,
|
||||||
progitempool: List[Item],
|
progitempool: List["Item"],
|
||||||
nonexcludeditempool: List[Item],
|
nonexcludeditempool: List["Item"],
|
||||||
localrestitempool: Dict[int, List[Item]],
|
localrestitempool: Dict[int, List["Item"]],
|
||||||
nonlocalrestitempool: Dict[int, List[Item]],
|
nonlocalrestitempool: Dict[int, List["Item"]],
|
||||||
restitempool: List[Item],
|
restitempool: List["Item"],
|
||||||
fill_locations: List[Location]) -> None:
|
fill_locations: List["Location"]) -> None:
|
||||||
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
||||||
This gets called once per present world type."""
|
This gets called once per present world type."""
|
||||||
pass
|
pass
|
||||||
@@ -250,7 +262,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
|
|
||||||
# end of ordered Main.py calls
|
# end of ordered Main.py calls
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> "Item":
|
||||||
"""Create an item for this world type and player.
|
"""Create an item for this world type and player.
|
||||||
Warning: this may be called with self.world = None, for example by MultiServer"""
|
Warning: this may be called with self.world = None, for example by MultiServer"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -261,7 +273,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
return self.world.random.choice(tuple(self.item_name_to_id.keys()))
|
return self.world.random.choice(tuple(self.item_name_to_id.keys()))
|
||||||
|
|
||||||
# decent place to implement progressive items, in most cases can stay as-is
|
# decent place to implement progressive items, in most cases can stay as-is
|
||||||
def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]:
|
def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
|
||||||
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
|
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
|
||||||
Collect None to skip item.
|
Collect None to skip item.
|
||||||
:param state: CollectionState to collect into
|
:param state: CollectionState to collect into
|
||||||
@@ -272,18 +284,18 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# called to create all_state, return Items that are created during pre_fill
|
# called to create all_state, return Items that are created during pre_fill
|
||||||
def get_pre_fill_items(self) -> List[Item]:
|
def get_pre_fill_items(self) -> List["Item"]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# following methods should not need to be overridden.
|
# following methods should not need to be overridden.
|
||||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
def collect(self, state: "CollectionState", item: "Item") -> bool:
|
||||||
name = self.collect_item(state, item)
|
name = self.collect_item(state, item)
|
||||||
if name:
|
if name:
|
||||||
state.prog_items[name, self.player] += 1
|
state.prog_items[name, self.player] += 1
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||||
name = self.collect_item(state, item, True)
|
name = self.collect_item(state, item, True)
|
||||||
if name:
|
if name:
|
||||||
state.prog_items[name, self.player] -= 1
|
state.prog_items[name, self.player] -= 1
|
||||||
@@ -292,7 +304,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def create_filler(self) -> Item:
|
def create_filler(self) -> "Item":
|
||||||
return self.create_item(self.get_filler_item_name())
|
return self.create_item(self.get_filler_item_name())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,56 @@
|
|||||||
import importlib
|
import importlib
|
||||||
|
import zipimport
|
||||||
import os
|
import os
|
||||||
|
import typing
|
||||||
|
|
||||||
__all__ = {"lookup_any_item_id_to_name",
|
folder = os.path.dirname(__file__)
|
||||||
"lookup_any_location_id_to_name",
|
|
||||||
"network_data_package",
|
__all__ = {
|
||||||
"AutoWorldRegister"}
|
"lookup_any_item_id_to_name",
|
||||||
|
"lookup_any_location_id_to_name",
|
||||||
|
"network_data_package",
|
||||||
|
"AutoWorldRegister",
|
||||||
|
"world_sources",
|
||||||
|
"folder",
|
||||||
|
}
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from .AutoWorld import World
|
||||||
|
|
||||||
|
|
||||||
|
class WorldSource(typing.NamedTuple):
|
||||||
|
path: str # typically relative path from this module
|
||||||
|
is_zip: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
# find potential world containers, currently folders and zip-importable .apworld's
|
||||||
|
world_sources: typing.List[WorldSource] = []
|
||||||
|
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
|
||||||
|
for file in os.scandir(folder):
|
||||||
|
if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
|
||||||
|
if file.is_dir():
|
||||||
|
world_sources.append(WorldSource(file.name))
|
||||||
|
elif file.is_file() and file.name.endswith(".apworld"):
|
||||||
|
world_sources.append(WorldSource(file.name, is_zip=True))
|
||||||
|
|
||||||
# import all submodules to trigger AutoWorldRegister
|
# import all submodules to trigger AutoWorldRegister
|
||||||
world_folders = []
|
world_sources.sort()
|
||||||
for file in os.scandir(os.path.dirname(__file__)):
|
for world_source in world_sources:
|
||||||
if file.is_dir():
|
if world_source.is_zip:
|
||||||
world_folders.append(file.name)
|
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
||||||
world_folders.sort()
|
importer.load_module(world_source.path.split(".", 1)[0])
|
||||||
for world in world_folders:
|
else:
|
||||||
if not world.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
|
importlib.import_module(f".{world_source.path}", "worlds")
|
||||||
importlib.import_module(f".{world}", "worlds")
|
|
||||||
|
|
||||||
from .AutoWorld import AutoWorldRegister
|
|
||||||
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 = {}
|
||||||
|
|
||||||
|
from .AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
for world_name, world in AutoWorldRegister.world_types.items():
|
for world_name, world in AutoWorldRegister.world_types.items():
|
||||||
games[world_name] = {
|
games[world_name] = {
|
||||||
"item_name_to_id" : world.item_name_to_id,
|
"item_name_to_id": world.item_name_to_id,
|
||||||
"location_name_to_id": world.location_name_to_id,
|
"location_name_to_id": world.location_name_to_id,
|
||||||
"version": world.data_version,
|
"version": world.data_version,
|
||||||
# seems clients don't actually want this. Keeping it here in case someone changes their mind.
|
# seems clients don't actually want this. Keeping it here in case someone changes their mind.
|
||||||
@@ -41,5 +68,6 @@ network_data_package = {
|
|||||||
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
|
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: "
|
||||||
f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}")
|
f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}")
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ def create_dungeons(world, player):
|
|||||||
dungeon_items, player)
|
dungeon_items, player)
|
||||||
for item in dungeon.all_items:
|
for item in dungeon.all_items:
|
||||||
item.dungeon = dungeon
|
item.dungeon = dungeon
|
||||||
item.world = world
|
|
||||||
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
|
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
|
||||||
for region in dungeon.regions:
|
for region in dungeon.regions:
|
||||||
world.get_region(region, player).dungeon = dungeon
|
world.get_region(region, player).dungeon = dungeon
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ class ItemData(typing.NamedTuple):
|
|||||||
flute_boy_credit: typing.Optional[str]
|
flute_boy_credit: typing.Optional[str]
|
||||||
hint_text: typing.Optional[str]
|
hint_text: typing.Optional[str]
|
||||||
|
|
||||||
|
def as_init_dict(self) -> typing.Dict[str, typing.Any]:
|
||||||
|
return {key: getattr(self, key) for key in
|
||||||
|
('classification', 'type', 'item_code', 'pedestal_hint', 'hint_text')}
|
||||||
|
|
||||||
|
|
||||||
# Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
|
# Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
|
||||||
item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
|
item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
|
||||||
'Progressive Bow': ItemData(IC.progression, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
|
'Progressive Bow': ItemData(IC.progression, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
|
||||||
@@ -218,7 +223,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
|
|||||||
'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None),
|
'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None),
|
||||||
}
|
}
|
||||||
|
|
||||||
as_dict_item_table = {name: data._asdict() for name, data in item_table.items()}
|
item_init_table = {name: data.as_init_dict() for name, data in item_table.items()}
|
||||||
|
|
||||||
progression_mapping = {
|
progression_mapping = {
|
||||||
"Golden Sword": ("Progressive Sword", 4),
|
"Golden Sword": ("Progressive Sword", 4),
|
||||||
|
|||||||
@@ -2091,7 +2091,9 @@ def write_string_to_rom(rom, target, string):
|
|||||||
|
|
||||||
|
|
||||||
def write_strings(rom, world, player):
|
def write_strings(rom, world, player):
|
||||||
|
from . import ALTTPWorld
|
||||||
local_random = world.slot_seeds[player]
|
local_random = world.slot_seeds[player]
|
||||||
|
w: ALTTPWorld = world.worlds[player]
|
||||||
|
|
||||||
tt = TextTable()
|
tt = TextTable()
|
||||||
tt.removeUnwantedText()
|
tt.removeUnwantedText()
|
||||||
@@ -2420,7 +2422,8 @@ def write_strings(rom, world, player):
|
|||||||
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem,
|
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem,
|
||||||
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
|
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
|
||||||
tt['mastersword_pedestal_translated'] = pedestal_text
|
tt['mastersword_pedestal_translated'] = pedestal_text
|
||||||
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else pedestalitem.pedestal_credit_text if pedestalitem.pedestal_credit_text is not None else 'and the Unknown Item'
|
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \
|
||||||
|
w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item')
|
||||||
|
|
||||||
etheritem = world.get_location('Ether Tablet', player).item
|
etheritem = world.get_location('Ether Tablet', player).item
|
||||||
ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem,
|
ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem,
|
||||||
@@ -2448,20 +2451,24 @@ def write_strings(rom, world, player):
|
|||||||
credits = Credits()
|
credits = Credits()
|
||||||
|
|
||||||
sickkiditem = world.get_location('Sick Kid', player).item
|
sickkiditem = world.get_location('Sick Kid', player).item
|
||||||
sickkiditem_text = local_random.choice(
|
sickkiditem_text = local_random.choice(SickKid_texts) \
|
||||||
SickKid_texts) if sickkiditem is None or sickkiditem.sickkid_credit_text is None else sickkiditem.sickkid_credit_text
|
if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \
|
||||||
|
else w.sickkid_credit_texts[sickkiditem.code]
|
||||||
|
|
||||||
zoraitem = world.get_location('King Zora', player).item
|
zoraitem = world.get_location('King Zora', player).item
|
||||||
zoraitem_text = local_random.choice(
|
zoraitem_text = local_random.choice(Zora_texts) \
|
||||||
Zora_texts) if zoraitem is None or zoraitem.zora_credit_text is None else zoraitem.zora_credit_text
|
if zoraitem is None or zoraitem.code not in w.zora_credit_texts \
|
||||||
|
else w.zora_credit_texts[zoraitem.code]
|
||||||
|
|
||||||
magicshopitem = world.get_location('Potion Shop', player).item
|
magicshopitem = world.get_location('Potion Shop', player).item
|
||||||
magicshopitem_text = local_random.choice(
|
magicshopitem_text = local_random.choice(MagicShop_texts) \
|
||||||
MagicShop_texts) if magicshopitem is None or magicshopitem.magicshop_credit_text is None else magicshopitem.magicshop_credit_text
|
if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \
|
||||||
|
else w.magicshop_credit_texts[magicshopitem.code]
|
||||||
|
|
||||||
fluteboyitem = world.get_location('Flute Spot', player).item
|
fluteboyitem = world.get_location('Flute Spot', player).item
|
||||||
fluteboyitem_text = local_random.choice(
|
fluteboyitem_text = local_random.choice(FluteBoy_texts) \
|
||||||
FluteBoy_texts) if fluteboyitem is None or fluteboyitem.fluteboy_credit_text is None else fluteboyitem.fluteboy_credit_text
|
if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \
|
||||||
|
else w.fluteboy_credit_texts[fluteboyitem.code]
|
||||||
|
|
||||||
credits.update_credits_line('castle', 0, local_random.choice(KingsReturn_texts))
|
credits.update_credits_line('castle', 0, local_random.choice(KingsReturn_texts))
|
||||||
credits.update_credits_line('sanctuary', 0, local_random.choice(Sanctuary_texts))
|
credits.update_credits_line('sanctuary', 0, local_random.choice(Sanctuary_texts))
|
||||||
|
|||||||
@@ -935,7 +935,6 @@ def set_trock_key_rules(world, player):
|
|||||||
else:
|
else:
|
||||||
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
|
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
|
||||||
item = ItemFactory('Small Key (Turtle Rock)', player)
|
item = ItemFactory('Small Key (Turtle Rock)', player)
|
||||||
item.world = world
|
|
||||||
location = world.get_location('Turtle Rock - Big Key Chest', player)
|
location = world.get_location('Turtle Rock - Big Key Chest', player)
|
||||||
location.place_locked_item(item)
|
location.place_locked_item(item)
|
||||||
location.event = True
|
location.event = True
|
||||||
|
|||||||
@@ -207,10 +207,10 @@ def ShopSlotFill(world):
|
|||||||
shops_per_sphere.append(current_shops_slots)
|
shops_per_sphere.append(current_shops_slots)
|
||||||
candidates_per_sphere.append(current_candidates)
|
candidates_per_sphere.append(current_candidates)
|
||||||
for location in sphere:
|
for location in sphere:
|
||||||
if location.shop_slot is not None:
|
if isinstance(location, ALttPLocation) and location.shop_slot is not None:
|
||||||
if not location.shop_slot_disabled:
|
if not location.shop_slot_disabled:
|
||||||
current_shops_slots.append(location)
|
current_shops_slots.append(location)
|
||||||
elif not location.locked and not location.item.name in blacklist_words:
|
elif not location.locked and location.item.name not in blacklist_words:
|
||||||
current_candidates.append(location)
|
current_candidates.append(location)
|
||||||
if cumu_weights:
|
if cumu_weights:
|
||||||
x = cumu_weights[-1]
|
x = cumu_weights[-1]
|
||||||
@@ -335,7 +335,6 @@ def create_shops(world, player: int):
|
|||||||
else:
|
else:
|
||||||
loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
|
loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
|
||||||
loc.shop_slot_disabled = True
|
loc.shop_slot_disabled = True
|
||||||
loc.item.world = world
|
|
||||||
shop.region.locations.append(loc)
|
shop.region.locations.append(loc)
|
||||||
world.clear_location_cache()
|
world.clear_location_cache()
|
||||||
|
|
||||||
|
|||||||
@@ -6,31 +6,33 @@ from BaseClasses import Location, Item, ItemClassification
|
|||||||
|
|
||||||
class ALttPLocation(Location):
|
class ALttPLocation(Location):
|
||||||
game: str = "A Link to the Past"
|
game: str = "A Link to the Past"
|
||||||
|
crystal: bool
|
||||||
|
player_address: Optional[int]
|
||||||
|
_hint_text: Optional[str]
|
||||||
|
shop_slot: Optional[int] = None
|
||||||
|
"""If given as integer, shop_slot is the shop's inventory index."""
|
||||||
|
shop_slot_disabled: bool = False
|
||||||
|
|
||||||
def __init__(self, player: int, name: str = '', address=None, crystal: bool = False,
|
def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False,
|
||||||
hint_text: Optional[str] = None, parent=None,
|
hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None):
|
||||||
player_address=None):
|
|
||||||
super(ALttPLocation, self).__init__(player, name, address, parent)
|
super(ALttPLocation, self).__init__(player, name, address, parent)
|
||||||
self.crystal = crystal
|
self.crystal = crystal
|
||||||
self.player_address = player_address
|
self.player_address = player_address
|
||||||
self._hint_text: str = hint_text
|
self._hint_text = hint_text
|
||||||
|
|
||||||
|
|
||||||
class ALttPItem(Item):
|
class ALttPItem(Item):
|
||||||
game: str = "A Link to the Past"
|
game: str = "A Link to the Past"
|
||||||
|
type: Optional[str]
|
||||||
|
_pedestal_hint_text: Optional[str]
|
||||||
|
_hint_text: Optional[str]
|
||||||
dungeon = None
|
dungeon = None
|
||||||
|
|
||||||
def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None, pedestal_hint=None,
|
def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None,
|
||||||
pedestal_credit=None, sick_kid_credit=None, zora_credit=None, witch_credit=None,
|
pedestal_hint=None, hint_text=None):
|
||||||
flute_boy_credit=None, hint_text=None):
|
|
||||||
super(ALttPItem, self).__init__(name, classification, item_code, player)
|
super(ALttPItem, self).__init__(name, classification, item_code, player)
|
||||||
self.type = type
|
self.type = type
|
||||||
self._pedestal_hint_text = pedestal_hint
|
self._pedestal_hint_text = pedestal_hint
|
||||||
self.pedestal_credit_text = pedestal_credit
|
|
||||||
self.sickkid_credit_text = sick_kid_credit
|
|
||||||
self.zora_credit_text = zora_credit
|
|
||||||
self.magicshop_credit_text = witch_credit
|
|
||||||
self.fluteboy_credit_text = flute_boy_credit
|
|
||||||
self._hint_text = hint_text
|
self._hint_text = hint_text
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
import random
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import threading
|
import threading
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import Item, CollectionState, Tutorial
|
from BaseClasses import Item, CollectionState, Tutorial
|
||||||
|
from .Dungeons import create_dungeons
|
||||||
|
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
||||||
|
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||||
|
from .ItemPool import generate_itempool, difficulties
|
||||||
|
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
|
||||||
|
from .Options import alttp_options, smallkey_shuffle
|
||||||
|
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
|
||||||
|
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||||
|
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||||
|
from .Rules import set_rules
|
||||||
|
from .Shops import create_shops, ShopSlotFill
|
||||||
from .SubClasses import ALttPItem
|
from .SubClasses import ALttPItem
|
||||||
from ..AutoWorld import World, WebWorld, LogicMixin
|
from ..AutoWorld import World, WebWorld, LogicMixin
|
||||||
from .Options import alttp_options, smallkey_shuffle
|
|
||||||
from .Items import as_dict_item_table, item_name_groups, item_table, GetBeemizerItem
|
|
||||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
|
|
||||||
from .Rules import set_rules
|
|
||||||
from .ItemPool import generate_itempool, difficulties
|
|
||||||
from .Shops import create_shops, ShopSlotFill
|
|
||||||
from .Dungeons import create_dungeons
|
|
||||||
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \
|
|
||||||
get_base_rom_path, LttPDeltaPatch
|
|
||||||
import Patch
|
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
|
||||||
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
|
||||||
|
|
||||||
lttp_logger = logging.getLogger("A Link to the Past")
|
lttp_logger = logging.getLogger("A Link to the Past")
|
||||||
|
|
||||||
@@ -110,7 +107,7 @@ class ALTTPWorld(World):
|
|||||||
Ganon!
|
Ganon!
|
||||||
"""
|
"""
|
||||||
game: str = "A Link to the Past"
|
game: str = "A Link to the Past"
|
||||||
options = alttp_options
|
option_definitions = alttp_options
|
||||||
topology_present = True
|
topology_present = True
|
||||||
item_name_groups = item_name_groups
|
item_name_groups = item_name_groups
|
||||||
hint_blacklist = {"Triforce"}
|
hint_blacklist = {"Triforce"}
|
||||||
@@ -124,6 +121,17 @@ class ALTTPWorld(World):
|
|||||||
required_client_version = (0, 3, 2)
|
required_client_version = (0, 3, 2)
|
||||||
web = ALTTPWeb()
|
web = ALTTPWeb()
|
||||||
|
|
||||||
|
pedestal_credit_texts: typing.Dict[int, str] = \
|
||||||
|
{data.item_code: data.pedestal_credit for data in item_table.values() if data.pedestal_credit}
|
||||||
|
sickkid_credit_texts: typing.Dict[int, str] = \
|
||||||
|
{data.item_code: data.sick_kid_credit for data in item_table.values() if data.sick_kid_credit}
|
||||||
|
zora_credit_texts: typing.Dict[int, str] = \
|
||||||
|
{data.item_code: data.zora_credit for data in item_table.values() if data.zora_credit}
|
||||||
|
magicshop_credit_texts: typing.Dict[int, str] = \
|
||||||
|
{data.item_code: data.witch_credit for data in item_table.values() if data.witch_credit}
|
||||||
|
fluteboy_credit_texts: typing.Dict[int, str] = \
|
||||||
|
{data.item_code: data.flute_boy_credit for data in item_table.values() if data.flute_boy_credit}
|
||||||
|
|
||||||
set_rules = set_rules
|
set_rules = set_rules
|
||||||
|
|
||||||
create_items = generate_itempool
|
create_items = generate_itempool
|
||||||
@@ -145,6 +153,9 @@ class ALTTPWorld(World):
|
|||||||
player = self.player
|
player = self.player
|
||||||
world = self.world
|
world = self.world
|
||||||
|
|
||||||
|
if self.use_enemizer():
|
||||||
|
check_enemizer(world.enemizer)
|
||||||
|
|
||||||
# system for sharing ER layouts
|
# system for sharing ER layouts
|
||||||
self.er_seed = str(world.random.randint(0, 2 ** 64))
|
self.er_seed = str(world.random.randint(0, 2 ** 64))
|
||||||
|
|
||||||
@@ -330,14 +341,19 @@ class ALTTPWorld(World):
|
|||||||
def stage_post_fill(cls, world):
|
def stage_post_fill(cls, world):
|
||||||
ShopSlotFill(world)
|
ShopSlotFill(world)
|
||||||
|
|
||||||
|
def use_enemizer(self):
|
||||||
|
world = self.world
|
||||||
|
player = self.player
|
||||||
|
return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
||||||
|
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||||
|
or world.pot_shuffle[player] or world.bush_shuffle[player]
|
||||||
|
or world.killable_thieves[player])
|
||||||
|
|
||||||
def generate_output(self, output_directory: str):
|
def generate_output(self, output_directory: str):
|
||||||
world = self.world
|
world = self.world
|
||||||
player = self.player
|
player = self.player
|
||||||
try:
|
try:
|
||||||
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
use_enemizer = self.use_enemizer()
|
||||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
|
||||||
or world.pot_shuffle[player] or world.bush_shuffle[player]
|
|
||||||
or world.killable_thieves[player])
|
|
||||||
|
|
||||||
rom = LocalRom(get_base_rom_path())
|
rom = LocalRom(get_base_rom_path())
|
||||||
|
|
||||||
@@ -400,7 +416,7 @@ class ALTTPWorld(World):
|
|||||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
|
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
def create_item(self, name: str) -> Item:
|
||||||
return ALttPItem(name, self.player, **as_dict_item_table[name])
|
return ALttPItem(name, self.player, **item_init_table[name])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||||
|
|||||||
@@ -47,13 +47,12 @@ class ArchipIDLEWorld(World):
|
|||||||
|
|
||||||
item_pool = []
|
item_pool = []
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
item = Item(
|
item = ArchipIDLEItem(
|
||||||
item_table_copy[i],
|
item_table_copy[i],
|
||||||
ItemClassification.progression if i < 20 else ItemClassification.filler,
|
ItemClassification.progression if i < 20 else ItemClassification.filler,
|
||||||
self.item_name_to_id[item_table_copy[i]],
|
self.item_name_to_id[item_table_copy[i]],
|
||||||
self.player
|
self.player
|
||||||
)
|
)
|
||||||
item.game = 'ArchipIDLE'
|
|
||||||
item_pool.append(item)
|
item_pool.append(item)
|
||||||
|
|
||||||
self.world.itempool += item_pool
|
self.world.itempool += item_pool
|
||||||
@@ -93,6 +92,10 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi
|
|||||||
return region
|
return region
|
||||||
|
|
||||||
|
|
||||||
|
class ArchipIDLEItem(Item):
|
||||||
|
game = "ArchipIDLE"
|
||||||
|
|
||||||
|
|
||||||
class ArchipIDLELocation(Location):
|
class ArchipIDLELocation(Location):
|
||||||
game: str = "ArchipIDLE"
|
game: str = "ArchipIDLE"
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class ChecksFinderWorld(World):
|
|||||||
with the mines! You win when you get all your items and beat the board!
|
with the mines! You win when you get all your items and beat the board!
|
||||||
"""
|
"""
|
||||||
game: str = "ChecksFinder"
|
game: str = "ChecksFinder"
|
||||||
options = checksfinder_options
|
option_definitions = checksfinder_options
|
||||||
topology_present = True
|
topology_present = True
|
||||||
web = ChecksFinderWeb()
|
web = ChecksFinderWeb()
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,26 @@ from ..generic.Rules import set_rule
|
|||||||
|
|
||||||
|
|
||||||
class DarkSouls3Web(WebWorld):
|
class DarkSouls3Web(WebWorld):
|
||||||
tutorials = [Tutorial(
|
bug_report_page = "https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/issues"
|
||||||
|
setup_en = Tutorial(
|
||||||
"Multiworld Setup Tutorial",
|
"Multiworld Setup Tutorial",
|
||||||
"A guide to setting up the Archipelago Dark Souls III randomizer on your computer.",
|
"A guide to setting up the Archipelago Dark Souls III randomizer on your computer.",
|
||||||
"English",
|
"English",
|
||||||
"setup_en.md",
|
"setup_en.md",
|
||||||
"setup/en",
|
"setup/en",
|
||||||
["Marech"]
|
["Marech"]
|
||||||
)]
|
)
|
||||||
|
|
||||||
|
setup_fr = Tutorial(
|
||||||
|
setup_en.tutorial_name,
|
||||||
|
setup_en.description,
|
||||||
|
"Français",
|
||||||
|
"setup_fr.md",
|
||||||
|
"setup/fr",
|
||||||
|
["Marech"]
|
||||||
|
)
|
||||||
|
|
||||||
|
tutorials = [setup_en, setup_fr]
|
||||||
|
|
||||||
|
|
||||||
class DarkSouls3World(World):
|
class DarkSouls3World(World):
|
||||||
@@ -34,7 +46,7 @@ class DarkSouls3World(World):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
game: str = "Dark Souls III"
|
game: str = "Dark Souls III"
|
||||||
options = dark_souls_options
|
option_definitions = dark_souls_options
|
||||||
topology_present: bool = True
|
topology_present: bool = True
|
||||||
remote_items: bool = False
|
remote_items: bool = False
|
||||||
remote_start_inventory: bool = False
|
remote_start_inventory: bool = False
|
||||||
@@ -146,7 +158,7 @@ class DarkSouls3World(World):
|
|||||||
|
|
||||||
# For each region, add the associated locations retrieved from the corresponding location_table
|
# For each region, add the associated locations retrieved from the corresponding location_table
|
||||||
def create_region(self, region_name, location_table) -> Region:
|
def create_region(self, region_name, location_table) -> Region:
|
||||||
new_region = Region(region_name, RegionType.Generic, region_name, self.player)
|
new_region = Region(region_name, RegionType.Generic, region_name, self.player, self.world)
|
||||||
if location_table:
|
if location_table:
|
||||||
for name, address in location_table.items():
|
for name, address in location_table.items():
|
||||||
location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region)
|
location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region)
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ config file.
|
|||||||
In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized.
|
In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized.
|
||||||
This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at
|
This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at
|
||||||
the same location. I also added an option available from the settings page to randomize the level of the generated
|
the same location. I also added an option available from the settings page to randomize the level of the generated
|
||||||
weapons( from +0 to +10/+5 )
|
weapons(from +0 to +10/+5)
|
||||||
|
|
||||||
|
To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
|
||||||
|
and kill the final boss "Soul of Cinder"
|
||||||
|
|
||||||
## What Dark Souls III items can appear in other players' worlds?
|
## What Dark Souls III items can appear in other players' worlds?
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Required Software
|
## Required Software
|
||||||
|
|
||||||
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
||||||
- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client)
|
- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
|
||||||
|
|
||||||
## General Concept
|
## General Concept
|
||||||
|
|
||||||
@@ -14,22 +14,24 @@ The randomization is performed by the AP.json file, an output file generated by
|
|||||||
|
|
||||||
## Installation Procedures
|
## Installation Procedures
|
||||||
|
|
||||||
**This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed**
|
<span style="color:tomato">
|
||||||
|
**This mod can ban you permanently from the FromSoftware servers if used online.**
|
||||||
|
</span>
|
||||||
|
This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed.
|
||||||
|
|
||||||
Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client).
|
Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases).
|
||||||
Then you need to add the two following files at the root folder of your game
|
Then you need to add the two following files at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game"):
|
||||||
( e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game" ):
|
|
||||||
- **dinput8.dll**
|
- **dinput8.dll**
|
||||||
- **AP.json** (renamed from the generated file AP-{ROOM_ID}.json)
|
- **AP.json** : The .json file downloaded from the multiworld room or provided by the host, named AP-{ROOM_ID}.json, has to be renamed to AP.json.
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
1. Run DarkSoulsIII.exe or run the game through Steam
|
1. Run DarkSoulsIII.exe or run the game through Steam
|
||||||
2. Type in /connect {SERVER_IP}:{SERVER_PORT} in the "Windows Command Prompt" that opened
|
2. Type in "/connect {SERVER_IP}:{SERVER_PORT}" in the "Windows Command Prompt" that opened
|
||||||
3. Once connected, create a new game, choose a class and wait for the others before starting
|
3. Once connected, create a new game, choose a class and wait for the others before starting
|
||||||
4. You can quit and launch at anytime during a game
|
4. You can quit and launch at anytime during a game
|
||||||
|
|
||||||
## Where do I get a config file?
|
## Where do I get a config file?
|
||||||
|
|
||||||
The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
|
The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
|
||||||
configure your personal settings and export them into a config file
|
configure your personal settings and export them into a config file
|
||||||
|
|||||||
38
worlds/dark_souls_3/docs/setup_fr.md
Normal file
38
worlds/dark_souls_3/docs/setup_fr.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Guide d'installation de Dark Souls III Randomizer
|
||||||
|
|
||||||
|
## Logiciels requis
|
||||||
|
|
||||||
|
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
||||||
|
- [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
|
||||||
|
|
||||||
|
## Concept général
|
||||||
|
|
||||||
|
Le client Archipelago de Dark Souls III est un fichier dinput8.dll. Cette .dll va lancer une invite de commande Windows
|
||||||
|
permettant de lire des informations de la partie et écrire des commandes pour intéragir avec le serveur Archipelago.
|
||||||
|
|
||||||
|
Le mélange des objets est réalisé par le fichier AP.json, un fichier généré par le serveur Archipelago.
|
||||||
|
|
||||||
|
## Procédures d'installation
|
||||||
|
|
||||||
|
<span style="color:tomato">
|
||||||
|
**Il y a des risques de bannissement permanent des serveurs FromSoftware si ce mod est utilisé en ligne.**
|
||||||
|
</span>
|
||||||
|
Ce client a été testé sur la version Steam officielle du jeu (v1.15/1.35), peu importe les DLCs actuellement installés.
|
||||||
|
|
||||||
|
Télécharger le fichier dinput8.dll disponible dans le [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases).
|
||||||
|
Vous devez ensuite ajouter les deux fichiers suivants à la racine du jeu
|
||||||
|
(ex: "SteamLibrary\steamapps\common\DARK SOULS III\Game"):
|
||||||
|
- **dinput8.dll**
|
||||||
|
- **AP.json** : Le fichier .json téléchargé depuis la <em>room</em> ou donné par l'hôte de la partie, nommé AP-{ROOM_ID}.json, doit être renommé en AP.json.
|
||||||
|
|
||||||
|
## Rejoindre une partie Multiworld
|
||||||
|
|
||||||
|
1. Lancer DarkSoulsIII.exe ou lancer le jeu depuis Steam
|
||||||
|
2. Ecrire "/connect {SERVER_IP}:{SERVER_PORT}" dans l'invite de commande Windows ouverte au lancement du jeu
|
||||||
|
3. Une fois connecté, créez une nouvelle partie, choisissez une classe et attendez que les autres soient prêts avant de lancer
|
||||||
|
4. Vous pouvez quitter et lancer le jeu n'importe quand pendant une partie
|
||||||
|
|
||||||
|
## Où trouver le fichier de configuration ?
|
||||||
|
|
||||||
|
La [Page de configuration](/games/Dark%20Souls%20III/player-settings) sur le site vous permez de configurer vos
|
||||||
|
paramètres et de les exporter sous la forme d'un fichier.
|
||||||
@@ -38,7 +38,7 @@ class DKC3World(World):
|
|||||||
mystery of why Donkey Kong and Diddy disappeared while on vacation.
|
mystery of why Donkey Kong and Diddy disappeared while on vacation.
|
||||||
"""
|
"""
|
||||||
game: str = "Donkey Kong Country 3"
|
game: str = "Donkey Kong Country 3"
|
||||||
options = dkc3_options
|
option_definitions = dkc3_options
|
||||||
topology_present = False
|
topology_present = False
|
||||||
data_version = 1
|
data_version = 1
|
||||||
#hint_blacklist = {LocationName.rocket_rush_flag}
|
#hint_blacklist = {LocationName.rocket_rush_flag}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
compatible hardware
|
compatible hardware
|
||||||
- Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc`
|
- Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc`
|
||||||
|
|
||||||
|
## Optional Software
|
||||||
|
- Donkey Kong Country 3 Tracker
|
||||||
|
- PopTracker from: [PopTracker Releases Page](https://github.com/black-sliver/PopTracker/releases/)
|
||||||
|
- Donkey Kong Country 3 Archipelago PopTracker pack from: [DKC3 AP Tracker Releases Page](https://github.com/PoryGone/DKC3_AP_Tracker/releases/)
|
||||||
|
|
||||||
## Installation Procedures
|
## Installation Procedures
|
||||||
|
|
||||||
### Windows Setup
|
### Windows Setup
|
||||||
@@ -57,7 +62,6 @@ validator page: [YAML Validation page](/mysterycheck)
|
|||||||
4. You will be presented with a server page, from which you can download your patch file.
|
4. You will be presented with a server page, from which you can download your patch file.
|
||||||
5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the
|
5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the
|
||||||
patch file, and open your emulator for you.
|
patch file, and open your emulator for you.
|
||||||
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
|
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
@@ -65,7 +69,7 @@ validator page: [YAML Validation page](/mysterycheck)
|
|||||||
|
|
||||||
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
|
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
|
||||||
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
|
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
|
||||||
files. Your patch file should have a `.apsm` extension.
|
files. Your patch file should have a `.apdkc3` extension.
|
||||||
|
|
||||||
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
|
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
|
||||||
client, and will also create your ROM in the same place as your patch file.
|
client, and will also create your ROM in the same place as your patch file.
|
||||||
|
|||||||
@@ -78,9 +78,14 @@ def generate_mod(world, output_directory: str):
|
|||||||
global data_final_template, locale_template, control_template, data_template, settings_template
|
global data_final_template, locale_template, control_template, data_template, settings_template
|
||||||
with template_load_lock:
|
with template_load_lock:
|
||||||
if not data_final_template:
|
if not data_final_template:
|
||||||
mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template")
|
def load_template(name: str):
|
||||||
|
import pkgutil
|
||||||
|
data = pkgutil.get_data(__name__, "data/mod_template/" + name).decode()
|
||||||
|
return data, name, lambda: False
|
||||||
|
|
||||||
template_env: Optional[jinja2.Environment] = \
|
template_env: Optional[jinja2.Environment] = \
|
||||||
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
|
jinja2.Environment(loader=jinja2.FunctionLoader(load_template))
|
||||||
|
|
||||||
data_template = template_env.get_template("data.lua")
|
data_template = template_env.get_template("data.lua")
|
||||||
data_final_template = template_env.get_template("data-final-fixes.lua")
|
data_final_template = template_env.get_template("data-final-fixes.lua")
|
||||||
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
locale_template = template_env.get_template(r"locale/en/locale.cfg")
|
||||||
@@ -158,7 +163,21 @@ def generate_mod(world, output_directory: str):
|
|||||||
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
|
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
|
||||||
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
||||||
os.makedirs(en_locale_dir, exist_ok=True)
|
os.makedirs(en_locale_dir, exist_ok=True)
|
||||||
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
|
|
||||||
|
if world.zip_path:
|
||||||
|
# Maybe investigate read from zip, write to zip, without temp file?
|
||||||
|
with zipfile.ZipFile(world.zip_path) as zf:
|
||||||
|
for file in zf.infolist():
|
||||||
|
if not file.is_dir() and "/data/mod/" in file.filename:
|
||||||
|
path_part = Utils.get_text_after(file.filename, "/data/mod/")
|
||||||
|
target = os.path.join(mod_dir, path_part)
|
||||||
|
os.makedirs(os.path.split(target)[0], exist_ok=True)
|
||||||
|
|
||||||
|
with open(target, "wb") as f:
|
||||||
|
f.write(zf.read(file))
|
||||||
|
else:
|
||||||
|
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
|
||||||
|
|
||||||
with open(os.path.join(mod_dir, "data.lua"), "wt") as f:
|
with open(os.path.join(mod_dir, "data.lua"), "wt") as f:
|
||||||
f.write(data_template_code)
|
f.write(data_template_code)
|
||||||
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
|
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Dict, List, Set
|
from typing import Dict, List, Set
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
from worlds.factorio.Options import TechTreeLayout
|
from .Options import TechTreeLayout
|
||||||
|
|
||||||
funnel_layers = {TechTreeLayout.option_small_funnels: 3,
|
funnel_layers = {TechTreeLayout.option_small_funnels: 3,
|
||||||
TechTreeLayout.option_medium_funnels: 4,
|
TechTreeLayout.option_medium_funnels: 4,
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ pool = ThreadPoolExecutor(1)
|
|||||||
|
|
||||||
|
|
||||||
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
|
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
|
||||||
with open(os.path.join(source_folder, f"{data_name}.json")) as f:
|
import pkgutil
|
||||||
return json.load(f)
|
return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())
|
||||||
|
|
||||||
|
|
||||||
techs_future = pool.submit(load_json_data, "techs")
|
techs_future = pool.submit(load_json_data, "techs")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import collections
|
import collections
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from ..AutoWorld import World, WebWorld
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification
|
from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification
|
||||||
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
|
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
|
||||||
@@ -193,7 +193,7 @@ class Factorio(World):
|
|||||||
|
|
||||||
return super(Factorio, self).collect_item(state, item, remove)
|
return super(Factorio, self).collect_item(state, item, remove)
|
||||||
|
|
||||||
options = factorio_options
|
option_definitions = factorio_options
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stage_write_spoiler(cls, world, spoiler_handle):
|
def stage_write_spoiler(cls, world, spoiler_handle):
|
||||||
|
|||||||
@@ -286,6 +286,12 @@ end)
|
|||||||
-- hook into researches done
|
-- hook into researches done
|
||||||
script.on_event(defines.events.on_research_finished, function(event)
|
script.on_event(defines.events.on_research_finished, function(event)
|
||||||
local technology = event.research
|
local technology = event.research
|
||||||
|
if string.find(technology.force.name, "EE_TESTFORCE") == 1 then
|
||||||
|
--Don't acknowledge AP research as an Editor Extensions test force
|
||||||
|
--Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces
|
||||||
|
--are worked on exclusively in editor mode.
|
||||||
|
return
|
||||||
|
end
|
||||||
if technology.researched and string.find(technology.name, "ap%-") == 1 then
|
if technology.researched and string.find(technology.name, "ap%-") == 1 then
|
||||||
-- check if it came from the server anyway, then we don't need to double send.
|
-- check if it came from the server anyway, then we don't need to double send.
|
||||||
dumpInfo(technology.force) --is sendable
|
dumpInfo(technology.force) --is sendable
|
||||||
|
|||||||
@@ -211,8 +211,8 @@ copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0]
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{#- connect Technology #}
|
{#- connect Technology #}
|
||||||
{%- if original_tech_name in tech_tree_layout_prerequisites %}
|
{%- if original_tech_name in tech_tree_layout_prerequisites %}
|
||||||
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
|
{%- for prerequisite in tech_tree_layout_prerequisites[original_tech_name] %}
|
||||||
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
|
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequisite] }}-")
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{#- add new Technology to game #}
|
{#- add new Technology to game #}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class FF1World(World):
|
|||||||
Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made.
|
Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
options = ff1_options
|
option_definitions = ff1_options
|
||||||
game = "Final Fantasy"
|
game = "Final Fantasy"
|
||||||
topology_present = False
|
topology_present = False
|
||||||
remote_items = True
|
remote_items = True
|
||||||
@@ -54,6 +54,7 @@ class FF1World(World):
|
|||||||
locations = get_options(self.world, 'locations', self.player)
|
locations = get_options(self.world, 'locations', self.player)
|
||||||
rules = get_options(self.world, 'rules', self.player)
|
rules = get_options(self.world, 'rules', self.player)
|
||||||
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules)
|
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules)
|
||||||
|
menu_region.world = self.world
|
||||||
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
|
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
|
||||||
terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
|
terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
|
||||||
terminated_event.place_locked_item(terminated_item)
|
terminated_event.place_locked_item(terminated_item)
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ changes it up by allowing you to plan out certain aspects of the game by placing
|
|||||||
certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of
|
certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of
|
||||||
these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
|
these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
|
||||||
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
|
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
|
||||||
by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss
|
by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary.
|
||||||
plando.
|
|
||||||
|
|
||||||
### Enabling Plando
|
### Enabling Plando
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ option_docstrings = {
|
|||||||
"pool and open their locations for randomization.",
|
"pool and open their locations for randomization.",
|
||||||
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
|
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
|
||||||
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
|
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
|
||||||
"Mimic Grubs are always placed in your own game.",
|
"Mimic Grubs are always placed in your own game.",
|
||||||
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
|
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
|
||||||
" and buy an item that is randomized into that location as well.",
|
" and buy an item that is randomized into that location as well.",
|
||||||
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
|
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class HKWorld(World):
|
|||||||
As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils.
|
As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils.
|
||||||
""" # from https://www.hollowknight.com
|
""" # from https://www.hollowknight.com
|
||||||
game: str = "Hollow Knight"
|
game: str = "Hollow Knight"
|
||||||
options = hollow_knight_options
|
option_definitions = hollow_knight_options
|
||||||
|
|
||||||
web = HKWeb()
|
web = HKWeb()
|
||||||
|
|
||||||
@@ -435,7 +435,7 @@ class HKWorld(World):
|
|||||||
slot_data = {}
|
slot_data = {}
|
||||||
|
|
||||||
options = slot_data["options"] = {}
|
options = slot_data["options"] = {}
|
||||||
for option_name in self.options:
|
for option_name in self.option_definitions:
|
||||||
option = getattr(self.world, option_name)[self.player]
|
option = getattr(self.world, option_name)[self.player]
|
||||||
try:
|
try:
|
||||||
optionvalue = int(option.value)
|
optionvalue = int(option.value)
|
||||||
@@ -632,8 +632,9 @@ class HKLocation(Location):
|
|||||||
|
|
||||||
class HKItem(Item):
|
class HKItem(Item):
|
||||||
game = "Hollow Knight"
|
game = "Hollow Knight"
|
||||||
|
type: str
|
||||||
|
|
||||||
def __init__(self, name, advancement, code, type, player: int = None):
|
def __init__(self, name, advancement, code, type: str, player: int = None):
|
||||||
if name == "Mimic_Grub":
|
if name == "Mimic_Grub":
|
||||||
classification = ItemClassification.trap
|
classification = ItemClassification.trap
|
||||||
elif type in ("Grub", "DreamWarrior", "Root", "Egg"):
|
elif type in ("Grub", "DreamWarrior", "Root", "Egg"):
|
||||||
|
|||||||
@@ -6,14 +6,9 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import Item, ItemClassification
|
from BaseClasses import Item, ItemClassification
|
||||||
|
from worlds.alttp import ALTTPWorld
|
||||||
|
|
||||||
|
|
||||||
# pedestal_credit_text: str = "and the Unknown Item"
|
|
||||||
# sickkid_credit_text: Optional[str] = None
|
|
||||||
# magicshop_credit_text: Optional[str] = None
|
|
||||||
# zora_credit_text: Optional[str] = None
|
|
||||||
# fluteboy_credit_text: Optional[str] = None
|
|
||||||
|
|
||||||
class MeritousLttPText(typing.NamedTuple):
|
class MeritousLttPText(typing.NamedTuple):
|
||||||
pedestal: typing.Optional[str]
|
pedestal: typing.Optional[str]
|
||||||
sickkid: typing.Optional[str]
|
sickkid: typing.Optional[str]
|
||||||
@@ -143,6 +138,7 @@ LttPCreditsText = {
|
|||||||
|
|
||||||
class MeritousItem(Item):
|
class MeritousItem(Item):
|
||||||
game: str = "Meritous"
|
game: str = "Meritous"
|
||||||
|
type: str
|
||||||
|
|
||||||
def __init__(self, name, advancement, code, player):
|
def __init__(self, name, advancement, code, player):
|
||||||
super(MeritousItem, self).__init__(name,
|
super(MeritousItem, self).__init__(name,
|
||||||
@@ -171,14 +167,6 @@ class MeritousItem(Item):
|
|||||||
self.type = "Artifact"
|
self.type = "Artifact"
|
||||||
self.classification = ItemClassification.useful
|
self.classification = ItemClassification.useful
|
||||||
|
|
||||||
if name in LttPCreditsText:
|
|
||||||
lttp = LttPCreditsText[name]
|
|
||||||
self.pedestal_credit_text = f"and the {lttp.pedestal}"
|
|
||||||
self.sickkid_credit_text = lttp.sickkid
|
|
||||||
self.magicshop_credit_text = lttp.magicshop
|
|
||||||
self.zora_credit_text = lttp.zora
|
|
||||||
self.fluteboy_credit_text = lttp.fluteboy
|
|
||||||
|
|
||||||
|
|
||||||
offset = 593_000
|
offset = 593_000
|
||||||
|
|
||||||
@@ -217,3 +205,10 @@ item_groups = {
|
|||||||
"Important Artifacts": ["Shield Boost", "Circuit Booster", "Metabolism", "Dodge Enhancer"],
|
"Important Artifacts": ["Shield Boost", "Circuit Booster", "Metabolism", "Dodge Enhancer"],
|
||||||
"Crystals": ["Crystals x500", "Crystals x1000", "Crystals x2000"]
|
"Crystals": ["Crystals x500", "Crystals x1000", "Crystals x2000"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ALTTPWorld.pedestal_credit_texts.update({item_table[name]: f"and the {texts.pedestal}"
|
||||||
|
for name, texts in LttPCreditsText.items()})
|
||||||
|
ALTTPWorld.sickkid_credit_texts.update({item_table[name]: texts.sickkid for name, texts in LttPCreditsText.items()})
|
||||||
|
ALTTPWorld.magicshop_credit_texts.update({item_table[name]: texts.magicshop for name, texts in LttPCreditsText.items()})
|
||||||
|
ALTTPWorld.zora_credit_texts.update({item_table[name]: texts.zora for name, texts in LttPCreditsText.items()})
|
||||||
|
ALTTPWorld.fluteboy_credit_texts.update({item_table[name]: texts.fluteboy for name, texts in LttPCreditsText.items()})
|
||||||
|
|||||||
@@ -45,12 +45,11 @@ class MeritousWorld(World):
|
|||||||
item_name_groups = item_groups
|
item_name_groups = item_groups
|
||||||
|
|
||||||
data_version = 2
|
data_version = 2
|
||||||
forced_auto_forfeit = False
|
|
||||||
|
|
||||||
# NOTE: Remember to change this before this game goes live
|
# NOTE: Remember to change this before this game goes live
|
||||||
required_client_version = (0, 2, 4)
|
required_client_version = (0, 2, 4)
|
||||||
|
|
||||||
options = meritous_options
|
option_definitions = meritous_options
|
||||||
|
|
||||||
def __init__(self, world: MultiWorld, player: int):
|
def __init__(self, world: MultiWorld, player: int):
|
||||||
super(MeritousWorld, self).__init__(world, player)
|
super(MeritousWorld, self).__init__(world, player)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class MinecraftWorld(World):
|
|||||||
victory!
|
victory!
|
||||||
"""
|
"""
|
||||||
game: str = "Minecraft"
|
game: str = "Minecraft"
|
||||||
options = minecraft_options
|
option_definitions = minecraft_options
|
||||||
topology_present = True
|
topology_present = True
|
||||||
web = MinecraftWebWorld()
|
web = MinecraftWebWorld()
|
||||||
|
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ leave this window open as this is your server console.
|
|||||||
|
|
||||||
### Connect to the MultiServer
|
### Connect to the MultiServer
|
||||||
|
|
||||||
Using Minecraft 1.18.2 connect to the server `localhost`.
|
Open Minecraft, go to `Multiplayer > Direct Connection`, and join the `localhost` server address.
|
||||||
|
|
||||||
If you are using the website to host the game then it should auto-connect to the AP server without the need to `/connect`
|
If you are using the website to host the game then it should auto-connect to the AP server without the need to `/connect`
|
||||||
|
|
||||||
otherwise once you are in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
|
otherwise once you are in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
|
||||||
Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281.
|
Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. Note that there is no colon between `<AP-Address>` and `(Port)`.
|
||||||
`(Password)` is only required if the Archipelago server you are using has a password set.
|
`(Password)` is only required if the Archipelago server you are using has a password set.
|
||||||
|
|
||||||
### Play the game
|
### Play the game
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
requests >= 2.27.1 # used by client
|
requests >= 2.28.1 # used by client
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from BaseClasses import LocationProgressType
|
from BaseClasses import LocationProgressType
|
||||||
|
from .Items import OOTItem
|
||||||
|
|
||||||
# Abbreviations
|
# Abbreviations
|
||||||
# DMC Death Mountain Crater
|
# DMC Death Mountain Crater
|
||||||
@@ -1260,7 +1261,7 @@ def hintExclusions(world, clear_cache=False):
|
|||||||
world.hint_exclusions = []
|
world.hint_exclusions = []
|
||||||
|
|
||||||
for location in world.get_locations():
|
for location in world.get_locations():
|
||||||
if (location.locked and (location.item.type != 'Song' or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED:
|
if (location.locked and ((isinstance(location.item, OOTItem) and location.item.type != 'Song') or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED:
|
||||||
world.hint_exclusions.append(location.name)
|
world.hint_exclusions.append(location.name)
|
||||||
|
|
||||||
world_location_names = [
|
world_location_names = [
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from urllib.error import URLError, HTTPError
|
|||||||
import json
|
import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from .Items import OOTItem
|
||||||
from .HintList import getHint, getHintGroup, Hint, hintExclusions
|
from .HintList import getHint, getHintGroup, Hint, hintExclusions
|
||||||
from .Messages import COLOR_MAP, update_message_by_id
|
from .Messages import COLOR_MAP, update_message_by_id
|
||||||
from .TextBox import line_wrap
|
from .TextBox import line_wrap
|
||||||
@@ -480,7 +481,7 @@ def get_specific_item_hint(world, checked):
|
|||||||
def get_random_location_hint(world, checked):
|
def get_random_location_hint(world, checked):
|
||||||
locations = list(filter(lambda location:
|
locations = list(filter(lambda location:
|
||||||
is_not_checked(location, checked)
|
is_not_checked(location, checked)
|
||||||
and location.item.type not in ('Drop', 'Event', 'Shop', 'DungeonReward')
|
and not (isinstance(location.item, OOTItem) and location.item.type in ('Drop', 'Event', 'Shop', 'DungeonReward'))
|
||||||
# and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items
|
# and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items
|
||||||
and not location.locked
|
and not location.locked
|
||||||
and location.name not in world.hint_exclusions
|
and location.name not in world.hint_exclusions
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ def ap_id_to_oot_data(ap_id):
|
|||||||
|
|
||||||
class OOTItem(Item):
|
class OOTItem(Item):
|
||||||
game: str = "Ocarina of Time"
|
game: str = "Ocarina of Time"
|
||||||
|
type: str
|
||||||
|
|
||||||
def __init__(self, name, player, data, event, force_not_advancement):
|
def __init__(self, name, player, data, event, force_not_advancement):
|
||||||
(type, advancement, index, special) = data
|
(type, advancement, index, special) = data
|
||||||
@@ -38,7 +39,6 @@ class OOTItem(Item):
|
|||||||
classification = ItemClassification.progression
|
classification = ItemClassification.progression
|
||||||
else:
|
else:
|
||||||
classification = ItemClassification.filler
|
classification = ItemClassification.filler
|
||||||
adv = bool(advancement) and not force_not_advancement
|
|
||||||
super(OOTItem, self).__init__(name, classification, oot_data_to_ap_id(data, event), player)
|
super(OOTItem, self).__init__(name, classification, oot_data_to_ap_id(data, event), player)
|
||||||
self.type = type
|
self.type = type
|
||||||
self.index = index
|
self.index = index
|
||||||
@@ -46,25 +46,12 @@ class OOTItem(Item):
|
|||||||
self.looks_like_item = None
|
self.looks_like_item = None
|
||||||
self.price = special.get('price', None) if special else None
|
self.price = special.get('price', None) if special else None
|
||||||
self.internal = False
|
self.internal = False
|
||||||
|
|
||||||
# The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)"
|
|
||||||
# This checks if the item it's looking for is a small key, using the small key property.
|
|
||||||
# Because of overlapping item fields, this means that OoT small keys are technically counted, unless we do this.
|
|
||||||
# This causes them to be double-collected during playthrough and generation.
|
|
||||||
@property
|
|
||||||
def smallkey(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bigkey(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dungeonitem(self) -> bool:
|
def dungeonitem(self) -> bool:
|
||||||
return self.type in ['SmallKey', 'HideoutSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass']
|
return self.type in ['SmallKey', 'HideoutSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Progressive: True -> Advancement
|
# Progressive: True -> Advancement
|
||||||
# False -> Priority
|
# False -> Priority
|
||||||
# None -> Normal
|
# None -> Normal
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import zlib
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
from .Items import OOTItem
|
||||||
from .LocationList import business_scrubs
|
from .LocationList import business_scrubs
|
||||||
from .Hints import writeGossipStoneHints, buildAltarHints, \
|
from .Hints import writeGossipStoneHints, buildAltarHints, \
|
||||||
buildGanonText, getSimpleHintNoPrefix
|
buildGanonText, getSimpleHintNoPrefix
|
||||||
@@ -1881,9 +1882,9 @@ def get_override_entry(player_id, location):
|
|||||||
type = 2
|
type = 2
|
||||||
elif location.type == 'GS Token':
|
elif location.type == 'GS Token':
|
||||||
type = 3
|
type = 3
|
||||||
elif location.type == 'Shop' and location.item.type != 'Shop':
|
elif location.type == 'Shop' and not (isinstance(location.item, OOTItem) and location.item.type == 'Shop'):
|
||||||
type = 0
|
type = 0
|
||||||
elif location.type == 'GrottoNPC' and location.item.type != 'Shop':
|
elif location.type == 'GrottoNPC' and not (isinstance(location.item, OOTItem) and location.item.type == 'Shop'):
|
||||||
type = 4
|
type = 4
|
||||||
elif location.type in ['Song', 'Cutscene']:
|
elif location.type in ['Song', 'Cutscene']:
|
||||||
type = 5
|
type = 5
|
||||||
@@ -2103,7 +2104,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
|
|||||||
|
|
||||||
shop_objs = { 0x0148 } # "Sold Out" object
|
shop_objs = { 0x0148 } # "Sold Out" object
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if location.item.type == 'Shop':
|
if isinstance(location.item, OOTItem) and location.item.type == 'Shop':
|
||||||
shop_objs.add(location.item.special['object'])
|
shop_objs.add(location.item.special['object'])
|
||||||
rom.write_int16(location.address1, location.item.index)
|
rom.write_int16(location.address1, location.item.index)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class OOTWorld(World):
|
|||||||
to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
|
to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
|
||||||
"""
|
"""
|
||||||
game: str = "Ocarina of Time"
|
game: str = "Ocarina of Time"
|
||||||
options: dict = oot_options
|
option_definitions: dict = oot_options
|
||||||
topology_present: bool = True
|
topology_present: bool = True
|
||||||
item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if
|
item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if
|
||||||
data[2] is not None}
|
data[2] is not None}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class OriBlindForest(World):
|
|||||||
item_name_to_id = item_table
|
item_name_to_id = item_table
|
||||||
location_name_to_id = lookup_name_to_id
|
location_name_to_id = lookup_name_to_id
|
||||||
|
|
||||||
options = options
|
option_definitions = options
|
||||||
|
|
||||||
hidden = True
|
hidden = True
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class RaftWorld(World):
|
|||||||
lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values()))
|
lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values()))
|
||||||
|
|
||||||
location_name_to_id = locations_lookup_name_to_id
|
location_name_to_id = locations_lookup_name_to_id
|
||||||
options = raft_options
|
option_definitions = raft_options
|
||||||
|
|
||||||
data_version = 2
|
data_version = 2
|
||||||
required_client_version = (0, 3, 4)
|
required_client_version = (0, 3, 4)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class LegacyWorld(World):
|
|||||||
But that's OK, because no one is perfect, and you don't have to be to succeed.
|
But that's OK, because no one is perfect, and you don't have to be to succeed.
|
||||||
"""
|
"""
|
||||||
game: str = "Rogue Legacy"
|
game: str = "Rogue Legacy"
|
||||||
options = legacy_options
|
option_definitions = legacy_options
|
||||||
topology_present = False
|
topology_present = False
|
||||||
data_version = 3
|
data_version = 3
|
||||||
required_client_version = (0, 2, 3)
|
required_client_version = (0, 2, 3)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from BaseClasses import MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from ..generic.Rules import set_rule, add_rule
|
from worlds.generic.Rules import set_rule, add_rule
|
||||||
|
|
||||||
|
|
||||||
def set_rules(world: MultiWorld, player: int):
|
def set_rules(world: MultiWorld, player: int):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from .Rules import set_rules
|
|||||||
|
|
||||||
from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
||||||
from .Options import ror2_options
|
from .Options import ror2_options
|
||||||
from ..AutoWorld import World, WebWorld
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
|
||||||
client_version = 1
|
client_version = 1
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class RiskOfRainWorld(World):
|
|||||||
first crash landing.
|
first crash landing.
|
||||||
"""
|
"""
|
||||||
game: str = "Risk of Rain 2"
|
game: str = "Risk of Rain 2"
|
||||||
options = ror2_options
|
option_definitions = ror2_options
|
||||||
topology_present = False
|
topology_present = False
|
||||||
|
|
||||||
item_name_to_id = item_table
|
item_name_to_id = item_table
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import typing
|
|||||||
|
|
||||||
from BaseClasses import Item, ItemClassification
|
from BaseClasses import Item, ItemClassification
|
||||||
from .Names import ItemName
|
from .Names import ItemName
|
||||||
|
from worlds.alttp import ALTTPWorld
|
||||||
|
|
||||||
|
|
||||||
class ItemData(typing.NamedTuple):
|
class ItemData(typing.NamedTuple):
|
||||||
@@ -18,9 +19,6 @@ class SA2BItem(Item):
|
|||||||
def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None):
|
def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None):
|
||||||
super(SA2BItem, self).__init__(name, classification, code, player)
|
super(SA2BItem, self).__init__(name, classification, code, player)
|
||||||
|
|
||||||
if self.name == ItemName.sonic_light_shoes or self.name == ItemName.shadow_air_shoes:
|
|
||||||
self.pedestal_credit_text = "and the Soap Shoes"
|
|
||||||
|
|
||||||
|
|
||||||
# Separate tables for each type of item.
|
# Separate tables for each type of item.
|
||||||
emblems_table = {
|
emblems_table = {
|
||||||
@@ -94,3 +92,6 @@ item_table = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
|
||||||
|
|
||||||
|
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes"
|
||||||
|
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class SA2BWorld(World):
|
|||||||
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth.
|
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth.
|
||||||
"""
|
"""
|
||||||
game: str = "Sonic Adventure 2 Battle"
|
game: str = "Sonic Adventure 2 Battle"
|
||||||
options = sa2b_options
|
option_definitions = sa2b_options
|
||||||
topology_present = False
|
topology_present = False
|
||||||
data_version = 2
|
data_version = 2
|
||||||
|
|
||||||
|
|||||||
@@ -103,19 +103,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
|||||||
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
|
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
|
||||||
lambda state: state._sc2wol_has_air(world, player)),
|
lambda state: state._sc2wol_has_air(world, player)),
|
||||||
LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001,
|
LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001,
|
||||||
lambda state: state._sc2wol_has_air(world, player) or True),
|
lambda state: True),
|
||||||
LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002,
|
LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002,
|
||||||
lambda state: state._sc2wol_has_air(world, player)),
|
lambda state: state._sc2wol_has_air(world, player)),
|
||||||
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
|
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
|
||||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||||
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
|
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
|
||||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||||
LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005,
|
LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005,
|
||||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||||
LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006,
|
LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006,
|
||||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||||
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
|
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
|
||||||
lambda state: state._sc2wol_able_to_rescue(world, player) or True),
|
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||||
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
|
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
|
||||||
lambda state: state._sc2wol_has_air(world, player)),
|
lambda state: state._sc2wol_has_air(world, player)),
|
||||||
LocationData("The Moebius Factor", "Beat The Moebius Factor", None,
|
LocationData("The Moebius Factor", "Beat The Moebius Factor", None,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class SC2WoLLogic(LogicMixin):
|
|||||||
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
|
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
|
||||||
|
|
||||||
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
|
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
|
||||||
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Orbital Strike'}, player)
|
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player)
|
||||||
|
|
||||||
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
|
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
|
||||||
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)
|
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class SC2WoLWorld(World):
|
|||||||
|
|
||||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||||
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
||||||
options = sc2wol_options
|
option_definitions = sc2wol_options
|
||||||
|
|
||||||
item_name_groups = item_name_groups
|
item_name_groups = item_name_groups
|
||||||
locked_locations: typing.List[str]
|
locked_locations: typing.List[str]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import copy
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import base64
|
import base64
|
||||||
from typing import Set, List, TextIO
|
from typing import Set, TextIO
|
||||||
|
|
||||||
from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils
|
from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class SMWorld(World):
|
|||||||
game: str = "Super Metroid"
|
game: str = "Super Metroid"
|
||||||
topology_present = True
|
topology_present = True
|
||||||
data_version = 1
|
data_version = 1
|
||||||
options = sm_options
|
option_definitions = sm_options
|
||||||
item_names: Set[str] = frozenset(items_lookup_name_to_id)
|
item_names: Set[str] = frozenset(items_lookup_name_to_id)
|
||||||
location_names: Set[str] = frozenset(locations_lookup_name_to_id)
|
location_names: Set[str] = frozenset(locations_lookup_name_to_id)
|
||||||
item_name_to_id = items_lookup_name_to_id
|
item_name_to_id = items_lookup_name_to_id
|
||||||
@@ -293,15 +293,15 @@ class SMWorld(World):
|
|||||||
if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None:
|
if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None:
|
||||||
# this SM world can find this item: write full item data to tables and assign player data for writing
|
# this SM world can find this item: write full item data to tables and assign player data for writing
|
||||||
romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0
|
romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0
|
||||||
if itemLoc.item.type in ItemManager.Items:
|
if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items:
|
||||||
itemId = ItemManager.Items[itemLoc.item.type].Id
|
itemId = ItemManager.Items[itemLoc.item.type].Id
|
||||||
else:
|
else:
|
||||||
itemId = ItemManager.Items['ArchipelagoItem'].Id + idx
|
itemId = ItemManager.Items['ArchipelagoItem'].Id + idx
|
||||||
multiWorldItems.append({"sym": symbols["message_item_names"],
|
multiWorldItems.append({"sym": symbols["message_item_names"],
|
||||||
"offset": (vanillaItemTypesCount + idx)*64,
|
"offset": (vanillaItemTypesCount + idx)*64,
|
||||||
"values": self.convertToROMItemName(itemLoc.item.name)})
|
"values": self.convertToROMItemName(itemLoc.item.name)})
|
||||||
idx += 1
|
idx += 1
|
||||||
|
|
||||||
if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()):
|
if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()):
|
||||||
playerIDCount += 1
|
playerIDCount += 1
|
||||||
self.playerIDMap[romPlayerID] = playerIDCount
|
self.playerIDMap[romPlayerID] = playerIDCount
|
||||||
@@ -488,7 +488,13 @@ class SMWorld(World):
|
|||||||
# commit all the changes we've made here to the ROM
|
# commit all the changes we've made here to the ROM
|
||||||
romPatcher.commitIPS()
|
romPatcher.commitIPS()
|
||||||
|
|
||||||
itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.world.get_locations() if itemLoc.player == self.player]
|
itemLocs = [
|
||||||
|
ItemLocation(ItemManager.Items[itemLoc.item.type
|
||||||
|
if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else
|
||||||
|
'ArchipelagoItem'],
|
||||||
|
locationsDict[itemLoc.name], True)
|
||||||
|
for itemLoc in self.world.get_locations() if itemLoc.player == self.player
|
||||||
|
]
|
||||||
romPatcher.writeItemsLocs(itemLocs)
|
romPatcher.writeItemsLocs(itemLocs)
|
||||||
|
|
||||||
itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player]
|
itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player]
|
||||||
@@ -561,7 +567,7 @@ class SMWorld(World):
|
|||||||
def fill_slot_data(self):
|
def fill_slot_data(self):
|
||||||
slot_data = {}
|
slot_data = {}
|
||||||
if not self.world.is_race:
|
if not self.world.is_race:
|
||||||
for option_name in self.options:
|
for option_name in self.option_definitions:
|
||||||
option = getattr(self.world, option_name)[self.player]
|
option = getattr(self.world, option_name)[self.player]
|
||||||
slot_data[option_name] = option.value
|
slot_data[option_name] = option.value
|
||||||
|
|
||||||
@@ -735,7 +741,8 @@ class SMLocation(Location):
|
|||||||
|
|
||||||
class SMItem(Item):
|
class SMItem(Item):
|
||||||
game = "Super Metroid"
|
game = "Super Metroid"
|
||||||
|
type: str
|
||||||
|
|
||||||
def __init__(self, name, classification, type, code, player: int = None):
|
def __init__(self, name, classification, type: str, code, player: int):
|
||||||
super(SMItem, self).__init__(name, classification, code, player)
|
super(SMItem, self).__init__(name, classification, code, player)
|
||||||
self.type = type
|
self.type = type
|
||||||
|
|||||||
@@ -35,13 +35,11 @@ class SM64World(World):
|
|||||||
location_name_to_id = location_table
|
location_name_to_id = location_table
|
||||||
|
|
||||||
data_version = 6
|
data_version = 6
|
||||||
required_client_version = (0,3,0)
|
required_client_version = (0, 3, 0)
|
||||||
|
|
||||||
forced_auto_forfeit = False
|
|
||||||
|
|
||||||
area_connections: typing.Dict[int, int]
|
area_connections: typing.Dict[int, int]
|
||||||
|
|
||||||
options = sm64_options
|
option_definitions = sm64_options
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
self.topology_present = self.world.AreaRandomizer[self.player].value
|
self.topology_present = self.world.AreaRandomizer[self.player].value
|
||||||
@@ -120,7 +118,7 @@ class SM64World(World):
|
|||||||
"AreaRando": self.area_connections,
|
"AreaRando": self.area_connections,
|
||||||
"FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value,
|
"FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value,
|
||||||
"BasementDoorCost": self.world.BasementStarDoorCost[self.player].value,
|
"BasementDoorCost": self.world.BasementStarDoorCost[self.player].value,
|
||||||
"SecondFloorCost": self.world.SecondFloorStarDoorCost[self.player].value,
|
"SecondFloorDoorCost": self.world.SecondFloorStarDoorCost[self.player].value,
|
||||||
"MIPS1Cost": self.world.MIPS1Cost[self.player].value,
|
"MIPS1Cost": self.world.MIPS1Cost[self.player].value,
|
||||||
"MIPS2Cost": self.world.MIPS2Cost[self.player].value,
|
"MIPS2Cost": self.world.MIPS2Cost[self.player].value,
|
||||||
"StarsToFinish": self.world.StarsToFinish[self.player].value,
|
"StarsToFinish": self.world.StarsToFinish[self.player].value,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import typing
|
import typing
|
||||||
from Options import Choice, Option
|
from Options import Choice, Option, Toggle, DefaultOnToggle, Range
|
||||||
|
|
||||||
class SMLogic(Choice):
|
class SMLogic(Choice):
|
||||||
"""This option selects what kind of logic to use for item placement inside
|
"""This option selects what kind of logic to use for item placement inside
|
||||||
@@ -45,6 +45,22 @@ class MorphLocation(Choice):
|
|||||||
option_Original = 2
|
option_Original = 2
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Goal(Choice):
|
||||||
|
"""This option decides what goal is required to finish the randomizer.
|
||||||
|
Defeat Ganon and Mother Brain - Find the required crystals and boss tokens kill both bosses.
|
||||||
|
Fast Ganon and Defeat Mother Brain - The hole to ganon is open without having to defeat Agahnim in
|
||||||
|
Ganon's Tower and Ganon can be defeat as soon you have the required
|
||||||
|
crystals to make Ganon vulnerable. For keysanity, this mode also removes
|
||||||
|
the Crateria Boss Key requirement from Tourian to allow faster access.
|
||||||
|
All Dungeons and Defeat Mother Brain - Similar to "Defeat Ganon and Mother Brain", but also requires all dungeons
|
||||||
|
to be beaten including Castle Tower and Agahnim."""
|
||||||
|
display_name = "Goal"
|
||||||
|
option_DefeatBoth = 0
|
||||||
|
option_FastGanonDefeatMotherBrain = 1
|
||||||
|
option_AllDungeonsDefeatMotherBrain = 2
|
||||||
|
default = 0
|
||||||
|
|
||||||
class KeyShuffle(Choice):
|
class KeyShuffle(Choice):
|
||||||
"""This option decides how dungeon items such as keys are shuffled.
|
"""This option decides how dungeon items such as keys are shuffled.
|
||||||
None - A Link to the Past dungeon items can only be placed inside the
|
None - A Link to the Past dungeon items can only be placed inside the
|
||||||
@@ -55,9 +71,75 @@ class KeyShuffle(Choice):
|
|||||||
option_Keysanity = 1
|
option_Keysanity = 1
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
class OpenTower(Range):
|
||||||
|
"""The amount of crystals required to be able to enter Ganon's Tower.
|
||||||
|
If this is set to Random, the amount can be found in-game on a sign next to Ganon's Tower."""
|
||||||
|
display_name = "Open Tower"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 7
|
||||||
|
default = 7
|
||||||
|
|
||||||
|
class GanonVulnerable(Range):
|
||||||
|
"""The amount of crystals required to be able to harm Ganon. The amount can be found
|
||||||
|
in-game on a sign near the top of the Pyramid."""
|
||||||
|
display_name = "Ganon Vulnerable"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 7
|
||||||
|
default = 7
|
||||||
|
|
||||||
|
class OpenTourian(Range):
|
||||||
|
"""The amount of boss tokens required to enter Tourian. The amount can be found in-game
|
||||||
|
on a sign above the door leading to the Tourian entrance."""
|
||||||
|
display_name = "Open Tourian"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 4
|
||||||
|
default = 4
|
||||||
|
|
||||||
|
class SpinJumpsAnimation(Toggle):
|
||||||
|
"""Enable separate space/screw jump animations"""
|
||||||
|
display_name = "Spin Jumps Animation"
|
||||||
|
|
||||||
|
class HeartBeepSpeed(Choice):
|
||||||
|
"""Sets the speed of the heart beep sound in A Link to the Past."""
|
||||||
|
display_name = "Heart Beep Speed"
|
||||||
|
option_Off = 0
|
||||||
|
option_Quarter = 1
|
||||||
|
option_Half = 2
|
||||||
|
option_Normal = 3
|
||||||
|
option_Double = 4
|
||||||
|
alias_false = 0
|
||||||
|
default = 3
|
||||||
|
|
||||||
|
class HeartColor(Choice):
|
||||||
|
"""Changes the color of the hearts in the HUD for A Link to the Past."""
|
||||||
|
display_name = "Heart Color"
|
||||||
|
option_Red = 0
|
||||||
|
option_Green = 1
|
||||||
|
option_Blue = 2
|
||||||
|
option_Yellow = 3
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
class QuickSwap(Toggle):
|
||||||
|
"""When enabled, lets you switch items in ALTTP with L/R"""
|
||||||
|
display_name = "Quick Swap"
|
||||||
|
|
||||||
|
class EnergyBeep(DefaultOnToggle):
|
||||||
|
"""Toggles the low health energy beep in Super Metroid."""
|
||||||
|
display_name = "Energy Beep"
|
||||||
|
|
||||||
|
|
||||||
smz3_options: typing.Dict[str, type(Option)] = {
|
smz3_options: typing.Dict[str, type(Option)] = {
|
||||||
"sm_logic": SMLogic,
|
"sm_logic": SMLogic,
|
||||||
"sword_location": SwordLocation,
|
"sword_location": SwordLocation,
|
||||||
"morph_location": MorphLocation,
|
"morph_location": MorphLocation,
|
||||||
"key_shuffle": KeyShuffle
|
"goal": Goal,
|
||||||
|
"key_shuffle": KeyShuffle,
|
||||||
|
"open_tower": OpenTower,
|
||||||
|
"ganon_vulnerable": GanonVulnerable,
|
||||||
|
"open_tourian": OpenTourian,
|
||||||
|
"spin_jumps_animation": SpinJumpsAnimation,
|
||||||
|
"heart_beep_speed": HeartBeepSpeed,
|
||||||
|
"heart_color": HeartColor,
|
||||||
|
"quick_swap": QuickSwap,
|
||||||
|
"energy_beep": EnergyBeep
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,42 @@ class MorphLocation(Enum):
|
|||||||
|
|
||||||
class Goal(Enum):
|
class Goal(Enum):
|
||||||
DefeatBoth = 0
|
DefeatBoth = 0
|
||||||
|
FastGanonDefeatMotherBrain = 1
|
||||||
|
AllDungeonsDefeatMotherBrain = 2
|
||||||
|
|
||||||
class KeyShuffle(Enum):
|
class KeyShuffle(Enum):
|
||||||
Null = 0
|
Null = 0
|
||||||
Keysanity = 1
|
Keysanity = 1
|
||||||
|
|
||||||
class GanonInvincible(Enum):
|
class OpenTower(Enum):
|
||||||
Never = 0
|
Random = -1
|
||||||
BeforeCrystals = 1
|
NoCrystals = 0
|
||||||
BeforeAllDungeons = 2
|
OneCrystal = 1
|
||||||
Always = 3
|
TwoCrystals = 2
|
||||||
|
ThreeCrystals = 3
|
||||||
|
FourCrystals = 4
|
||||||
|
FiveCrystals = 5
|
||||||
|
SixCrystals = 6
|
||||||
|
SevenCrystals = 7
|
||||||
|
|
||||||
|
class GanonVulnerable(Enum):
|
||||||
|
Random = -1
|
||||||
|
NoCrystals = 0
|
||||||
|
OneCrystal = 1
|
||||||
|
TwoCrystals = 2
|
||||||
|
ThreeCrystals = 3
|
||||||
|
FourCrystals = 4
|
||||||
|
FiveCrystals = 5
|
||||||
|
SixCrystals = 6
|
||||||
|
SevenCrystals = 7
|
||||||
|
|
||||||
|
class OpenTourian(Enum):
|
||||||
|
Random = -1
|
||||||
|
NoBosses = 0
|
||||||
|
OneBoss = 1
|
||||||
|
TwoBosses = 2
|
||||||
|
ThreeBosses = 3
|
||||||
|
FourBosses = 4
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
GameMode: GameMode = GameMode.Multiworld
|
GameMode: GameMode = GameMode.Multiworld
|
||||||
@@ -45,64 +71,20 @@ class Config:
|
|||||||
MorphLocation: MorphLocation = MorphLocation.Randomized
|
MorphLocation: MorphLocation = MorphLocation.Randomized
|
||||||
Goal: Goal = Goal.DefeatBoth
|
Goal: Goal = Goal.DefeatBoth
|
||||||
KeyShuffle: KeyShuffle = KeyShuffle.Null
|
KeyShuffle: KeyShuffle = KeyShuffle.Null
|
||||||
Keysanity: bool = KeyShuffle != KeyShuffle.Null
|
|
||||||
Race: bool = False
|
Race: bool = False
|
||||||
GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals
|
|
||||||
MinimalAccessibility: bool = False # AP specific accessibility: minimal
|
|
||||||
|
|
||||||
def __init__(self, options: Dict[str, str]):
|
OpenTower: OpenTower = OpenTower.SevenCrystals
|
||||||
self.GameMode = self.ParseOption(options, GameMode.Multiworld)
|
GanonVulnerable: GanonVulnerable = GanonVulnerable.SevenCrystals
|
||||||
self.Z3Logic = self.ParseOption(options, Z3Logic.Normal)
|
OpenTourian: OpenTourian = OpenTourian.FourBosses
|
||||||
self.SMLogic = self.ParseOption(options, SMLogic.Normal)
|
|
||||||
self.SwordLocation = self.ParseOption(options, SwordLocation.Randomized)
|
|
||||||
self.MorphLocation = self.ParseOption(options, MorphLocation.Randomized)
|
|
||||||
self.Goal = self.ParseOption(options, Goal.DefeatBoth)
|
|
||||||
self.GanonInvincible = self.ParseOption(options, GanonInvincible.BeforeCrystals)
|
|
||||||
self.KeyShuffle = self.ParseOption(options, KeyShuffle.Null)
|
|
||||||
self.Keysanity = self.KeyShuffle != KeyShuffle.Null
|
|
||||||
self.Race = self.ParseOptionWith(options, "Race", False)
|
|
||||||
|
|
||||||
def ParseOption(self, options:Dict[str, str], defaultValue:Enum):
|
@property
|
||||||
enumKey = defaultValue.__class__.__name__.lower()
|
def SingleWorld(self) -> bool:
|
||||||
if (enumKey in options):
|
return self.GameMode == GameMode.Normal
|
||||||
return defaultValue.__class__[options[enumKey]]
|
|
||||||
return defaultValue
|
@property
|
||||||
|
def Multiworld(self) -> bool:
|
||||||
|
return self.GameMode == GameMode.Multiworld
|
||||||
|
|
||||||
def ParseOptionWith(self, options:Dict[str, str], option:str, defaultValue:bool):
|
@property
|
||||||
if (option.lower() in options):
|
def Keysanity(self) -> bool:
|
||||||
return options[option.lower()]
|
return self.KeyShuffle != KeyShuffle.Null
|
||||||
return defaultValue
|
|
||||||
|
|
||||||
""" public static RandomizerOption GetRandomizerOption<T>(string description, string defaultOption = "") where T : Enum {
|
|
||||||
var enumType = typeof(T);
|
|
||||||
var values = Enum.GetValues(enumType).Cast<Enum>();
|
|
||||||
|
|
||||||
return new RandomizerOption {
|
|
||||||
Key = enumType.Name.ToLower(),
|
|
||||||
Description = description,
|
|
||||||
Type = RandomizerOptionType.Dropdown,
|
|
||||||
Default = string.IsNullOrEmpty(defaultOption) ? GetDefaultValue<T>().ToLString() : defaultOption,
|
|
||||||
Values = values.ToDictionary(k => k.ToLString(), v => v.GetDescription())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RandomizerOption GetRandomizerOption(string name, string description, bool defaultOption = false) {
|
|
||||||
return new RandomizerOption {
|
|
||||||
Key = name.ToLower(),
|
|
||||||
Description = description,
|
|
||||||
Type = RandomizerOptionType.Checkbox,
|
|
||||||
Default = defaultOption.ToString().ToLower(),
|
|
||||||
Values = new Dictionary<string, string>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TEnum GetDefaultValue<TEnum>() where TEnum : Enum {
|
|
||||||
Type t = typeof(TEnum);
|
|
||||||
var attributes = (DefaultValueAttribute[])t.GetCustomAttributes(typeof(DefaultValueAttribute), false);
|
|
||||||
if ((attributes?.Length ?? 0) > 0) {
|
|
||||||
return (TEnum)attributes.First().Value;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
} """
|
|
||||||
@@ -130,6 +130,11 @@ class ItemType(Enum):
|
|||||||
CardLowerNorfairL1 = 0xDE
|
CardLowerNorfairL1 = 0xDE
|
||||||
CardLowerNorfairBoss = 0xDF
|
CardLowerNorfairBoss = 0xDF
|
||||||
|
|
||||||
|
SmMapBrinstar = 0xCA
|
||||||
|
SmMapWreckedShip = 0xCB
|
||||||
|
SmMapMaridia = 0xCC
|
||||||
|
SmMapLowerNorfair = 0xCD
|
||||||
|
|
||||||
Missile = 0xC2
|
Missile = 0xC2
|
||||||
Super = 0xC3
|
Super = 0xC3
|
||||||
PowerBomb = 0xC4
|
PowerBomb = 0xC4
|
||||||
@@ -174,6 +179,7 @@ class Item:
|
|||||||
map = re.compile("^Map")
|
map = re.compile("^Map")
|
||||||
compass = re.compile("^Compass")
|
compass = re.compile("^Compass")
|
||||||
keycard = re.compile("^Card")
|
keycard = re.compile("^Card")
|
||||||
|
smMap = re.compile("^SmMap")
|
||||||
|
|
||||||
def IsDungeonItem(self): return self.dungeon.match(self.Type.name)
|
def IsDungeonItem(self): return self.dungeon.match(self.Type.name)
|
||||||
def IsBigKey(self): return self.bigKey.match(self.Type.name)
|
def IsBigKey(self): return self.bigKey.match(self.Type.name)
|
||||||
@@ -181,6 +187,7 @@ class Item:
|
|||||||
def IsMap(self): return self.map.match(self.Type.name)
|
def IsMap(self): return self.map.match(self.Type.name)
|
||||||
def IsCompass(self): return self.compass.match(self.Type.name)
|
def IsCompass(self): return self.compass.match(self.Type.name)
|
||||||
def IsKeycard(self): return self.keycard.match(self.Type.name)
|
def IsKeycard(self): return self.keycard.match(self.Type.name)
|
||||||
|
def IsSmMap(self): return self.smMap.match(self.Type.name)
|
||||||
|
|
||||||
def Is(self, type: ItemType, world):
|
def Is(self, type: ItemType, world):
|
||||||
return self.Type == type and self.World == world
|
return self.Type == type and self.World == world
|
||||||
@@ -313,7 +320,7 @@ class Item:
|
|||||||
Item.AddRange(itemPool, 4, Item(ItemType.BombUpgrade5))
|
Item.AddRange(itemPool, 4, Item(ItemType.BombUpgrade5))
|
||||||
Item.AddRange(itemPool, 2, Item(ItemType.OneRupee))
|
Item.AddRange(itemPool, 2, Item(ItemType.OneRupee))
|
||||||
Item.AddRange(itemPool, 4, Item(ItemType.FiveRupees))
|
Item.AddRange(itemPool, 4, Item(ItemType.FiveRupees))
|
||||||
Item.AddRange(itemPool, 25 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees))
|
Item.AddRange(itemPool, 21 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees))
|
||||||
Item.AddRange(itemPool, 7, Item(ItemType.FiftyRupees))
|
Item.AddRange(itemPool, 7, Item(ItemType.FiftyRupees))
|
||||||
Item.AddRange(itemPool, 5, Item(ItemType.ThreeHundredRupees))
|
Item.AddRange(itemPool, 5, Item(ItemType.ThreeHundredRupees))
|
||||||
|
|
||||||
@@ -421,6 +428,21 @@ class Item:
|
|||||||
|
|
||||||
return itemPool
|
return itemPool
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def CreateSmMaps(world):
|
||||||
|
itemPool = [
|
||||||
|
Item(ItemType.SmMapBrinstar, world),
|
||||||
|
Item(ItemType.SmMapWreckedShip, world),
|
||||||
|
Item(ItemType.SmMapMaridia, world),
|
||||||
|
Item(ItemType.SmMapLowerNorfair, world)
|
||||||
|
]
|
||||||
|
|
||||||
|
for item in itemPool:
|
||||||
|
item.Progression = True
|
||||||
|
item.World = world
|
||||||
|
|
||||||
|
return itemPool
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def Get(items, itemType:ItemType):
|
def Get(items, itemType:ItemType):
|
||||||
item = next((i for i in items if i.Type == itemType), None)
|
item = next((i for i in items if i.Type == itemType), None)
|
||||||
@@ -725,7 +747,7 @@ class Progression:
|
|||||||
|
|
||||||
def CanAccessMiseryMirePortal(self, config: Config):
|
def CanAccessMiseryMirePortal(self, config: Config):
|
||||||
if (config.SMLogic == SMLogic.Normal):
|
if (config.SMLogic == SMLogic.Normal):
|
||||||
return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and (self.Gravity and self.SpaceJump) and self.CanUsePowerBombs()
|
return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and self.Gravity and self.SpaceJump and self.CanUsePowerBombs()
|
||||||
else:
|
else:
|
||||||
return (self.CardNorfairL2 or self.SpeedBooster) and self.Varia and self.Super and \
|
return (self.CardNorfairL2 or self.SpeedBooster) and self.Varia and self.Super and \
|
||||||
(self.CanFly() or self.HiJump or self.SpeedBooster or self.CanSpringBallJump() or self.Ice) \
|
(self.CanFly() or self.HiJump or self.SpeedBooster or self.CanSpringBallJump() or self.Ice) \
|
||||||
@@ -769,11 +791,11 @@ class Progression:
|
|||||||
if (world.Config.SMLogic == SMLogic.Normal):
|
if (world.Config.SMLogic == SMLogic.Normal):
|
||||||
return self.MoonPearl and self.Flippers and \
|
return self.MoonPearl and self.Flippers and \
|
||||||
self.Gravity and self.Morph and \
|
self.Gravity and self.Morph and \
|
||||||
(world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy())
|
(world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy())
|
||||||
else:
|
else:
|
||||||
return self.MoonPearl and self.Flippers and \
|
return self.MoonPearl and self.Flippers and \
|
||||||
(self.CanSpringBallJump() or self.HiJump or self.Gravity) and self.Morph and \
|
(self.CanSpringBallJump() or self.HiJump or self.Gravity) and self.Morph and \
|
||||||
(world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy())
|
(world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy())
|
||||||
|
|
||||||
# Start of AP integration
|
# Start of AP integration
|
||||||
items_start_id = 84000
|
items_start_id = 84000
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import typing
|
|||||||
from BaseClasses import Location
|
from BaseClasses import Location
|
||||||
from worlds.smz3.TotalSMZ3.Item import Item, ItemType
|
from worlds.smz3.TotalSMZ3.Item import Item, ItemType
|
||||||
from worlds.smz3.TotalSMZ3.Location import LocationType
|
from worlds.smz3.TotalSMZ3.Location import LocationType
|
||||||
from worlds.smz3.TotalSMZ3.Region import IMedallionAccess, IReward, RewardType, SMRegion, Z3Region
|
from worlds.smz3.TotalSMZ3.Region import IReward, RewardType, SMRegion, Z3Region
|
||||||
from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace
|
from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace
|
||||||
from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace
|
from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace
|
||||||
from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera
|
from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera
|
||||||
@@ -18,10 +18,14 @@ from worlds.smz3.TotalSMZ3.Regions.Zelda.IcePalace import IcePalace
|
|||||||
from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire
|
from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire
|
||||||
from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock
|
from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock
|
||||||
from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower
|
from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower
|
||||||
|
from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Kraid import Kraid
|
||||||
|
from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.WreckedShip import WreckedShip
|
||||||
|
from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Maridia.Inner import Inner
|
||||||
|
from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairLower.East import East
|
||||||
from worlds.smz3.TotalSMZ3.Text.StringTable import StringTable
|
from worlds.smz3.TotalSMZ3.Text.StringTable import StringTable
|
||||||
|
|
||||||
from worlds.smz3.TotalSMZ3.World import World
|
from worlds.smz3.TotalSMZ3.World import World
|
||||||
from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible
|
from worlds.smz3.TotalSMZ3.Config import Config, OpenTourian, Goal
|
||||||
from worlds.smz3.TotalSMZ3.Text.Texts import Texts
|
from worlds.smz3.TotalSMZ3.Text.Texts import Texts
|
||||||
from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog
|
from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog
|
||||||
|
|
||||||
@@ -30,6 +34,11 @@ class KeycardPlaque:
|
|||||||
Level2 = 0xe1
|
Level2 = 0xe1
|
||||||
Boss = 0xe2
|
Boss = 0xe2
|
||||||
Null = 0x00
|
Null = 0x00
|
||||||
|
Zero = 0xe3
|
||||||
|
One = 0xe4
|
||||||
|
Two = 0xe5
|
||||||
|
Three = 0xe6
|
||||||
|
Four = 0xe7
|
||||||
|
|
||||||
class KeycardDoors:
|
class KeycardDoors:
|
||||||
Left = 0xd414
|
Left = 0xd414
|
||||||
@@ -73,8 +82,8 @@ class DropPrize(Enum):
|
|||||||
Fairy = 0xE3
|
Fairy = 0xE3
|
||||||
|
|
||||||
class Patch:
|
class Patch:
|
||||||
Major = 0
|
Major = 11
|
||||||
Minor = 1
|
Minor = 3
|
||||||
allWorlds: List[World]
|
allWorlds: List[World]
|
||||||
myWorld: World
|
myWorld: World
|
||||||
seedGuid: str
|
seedGuid: str
|
||||||
@@ -105,13 +114,16 @@ class Patch:
|
|||||||
|
|
||||||
self.WriteDiggingGameRng()
|
self.WriteDiggingGameRng()
|
||||||
|
|
||||||
self.WritePrizeShuffle()
|
self.WritePrizeShuffle(self.myWorld.WorldState.DropPrizes)
|
||||||
|
|
||||||
self.WriteRemoveEquipmentFromUncle( self.myWorld.GetLocation("Link's Uncle").APLocation.item.item if
|
self.WriteRemoveEquipmentFromUncle( self.myWorld.GetLocation("Link's Uncle").APLocation.item.item if
|
||||||
self.myWorld.GetLocation("Link's Uncle").APLocation.item.game == "SMZ3" else
|
self.myWorld.GetLocation("Link's Uncle").APLocation.item.game == "SMZ3" else
|
||||||
Item(ItemType.Something))
|
Item(ItemType.Something))
|
||||||
|
|
||||||
self.WriteGanonInvicible(config.GanonInvincible)
|
self.WriteGanonInvicible(config.Goal)
|
||||||
|
self.WritePreOpenPyramid(config.Goal)
|
||||||
|
self.WriteCrystalsNeeded(self.myWorld.TowerCrystals, self.myWorld.GanonCrystals)
|
||||||
|
self.WriteBossesNeeded(self.myWorld.TourianBossTokens)
|
||||||
self.WriteRngBlock()
|
self.WriteRngBlock()
|
||||||
|
|
||||||
self.WriteSaveAndQuitFromBossRoom()
|
self.WriteSaveAndQuitFromBossRoom()
|
||||||
@@ -135,26 +147,27 @@ class Patch:
|
|||||||
return {patch[0]:patch[1] for patch in self.patches}
|
return {patch[0]:patch[1] for patch in self.patches}
|
||||||
|
|
||||||
def WriteMedallions(self):
|
def WriteMedallions(self):
|
||||||
|
from worlds.smz3.TotalSMZ3.WorldState import Medallion
|
||||||
turtleRock = next(region for region in self.myWorld.Regions if isinstance(region, TurtleRock))
|
turtleRock = next(region for region in self.myWorld.Regions if isinstance(region, TurtleRock))
|
||||||
miseryMire = next(region for region in self.myWorld.Regions if isinstance(region, MiseryMire))
|
miseryMire = next(region for region in self.myWorld.Regions if isinstance(region, MiseryMire))
|
||||||
|
|
||||||
turtleRockAddresses = [0x308023, 0xD020, 0xD0FF, 0xD1DE ]
|
turtleRockAddresses = [0x308023, 0xD020, 0xD0FF, 0xD1DE ]
|
||||||
miseryMireAddresses = [ 0x308022, 0xCFF2, 0xD0D1, 0xD1B0 ]
|
miseryMireAddresses = [ 0x308022, 0xCFF2, 0xD0D1, 0xD1B0 ]
|
||||||
|
|
||||||
if turtleRock.Medallion == ItemType.Bombos:
|
if turtleRock.Medallion == Medallion.Bombos:
|
||||||
turtleRockValues = [0x00, 0x51, 0x10, 0x00]
|
turtleRockValues = [0x00, 0x51, 0x10, 0x00]
|
||||||
elif turtleRock.Medallion == ItemType.Ether:
|
elif turtleRock.Medallion == Medallion.Ether:
|
||||||
turtleRockValues = [0x01, 0x51, 0x18, 0x00]
|
turtleRockValues = [0x01, 0x51, 0x18, 0x00]
|
||||||
elif turtleRock.Medallion == ItemType.Quake:
|
elif turtleRock.Medallion == Medallion.Quake:
|
||||||
turtleRockValues = [0x02, 0x14, 0xEF, 0xC4]
|
turtleRockValues = [0x02, 0x14, 0xEF, 0xC4]
|
||||||
else:
|
else:
|
||||||
raise exception(f"Tried using {turtleRock.Medallion} in place of Turtle Rock medallion")
|
raise exception(f"Tried using {turtleRock.Medallion} in place of Turtle Rock medallion")
|
||||||
|
|
||||||
if miseryMire.Medallion == ItemType.Bombos:
|
if miseryMire.Medallion == Medallion.Bombos:
|
||||||
miseryMireValues = [0x00, 0x51, 0x00, 0x00]
|
miseryMireValues = [0x00, 0x51, 0x00, 0x00]
|
||||||
elif miseryMire.Medallion == ItemType.Ether:
|
elif miseryMire.Medallion == Medallion.Ether:
|
||||||
miseryMireValues = [0x01, 0x13, 0x9F, 0xF1]
|
miseryMireValues = [0x01, 0x13, 0x9F, 0xF1]
|
||||||
elif miseryMire.Medallion == ItemType.Quake:
|
elif miseryMire.Medallion == Medallion.Quake:
|
||||||
miseryMireValues = [0x02, 0x51, 0x08, 0x00]
|
miseryMireValues = [0x02, 0x51, 0x08, 0x00]
|
||||||
else:
|
else:
|
||||||
raise exception(f"Tried using {miseryMire.Medallion} in place of Misery Mire medallion")
|
raise exception(f"Tried using {miseryMire.Medallion} in place of Misery Mire medallion")
|
||||||
@@ -174,12 +187,19 @@ class Patch:
|
|||||||
self.rnd.shuffle(pendantsBlueRed)
|
self.rnd.shuffle(pendantsBlueRed)
|
||||||
pendantRewards = pendantsGreen + pendantsBlueRed
|
pendantRewards = pendantsGreen + pendantsBlueRed
|
||||||
|
|
||||||
|
bossTokens = [ 1, 2, 3, 4 ]
|
||||||
|
|
||||||
regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)]
|
regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)]
|
||||||
crystalRegions = [region for region in regions if region.Reward == RewardType.CrystalBlue] + [region for region in regions if region.Reward == RewardType.CrystalRed]
|
crystalRegions = [region for region in regions if region.Reward == RewardType.CrystalBlue] + [region for region in regions if region.Reward == RewardType.CrystalRed]
|
||||||
pendantRegions = [region for region in regions if region.Reward == RewardType.PendantGreen] + [region for region in regions if region.Reward == RewardType.PendantNonGreen]
|
pendantRegions = [region for region in regions if region.Reward == RewardType.PendantGreen] + [region for region in regions if region.Reward == RewardType.PendantNonGreen]
|
||||||
|
bossRegions = [region for region in regions if region.Reward == RewardType.BossTokenKraid] + \
|
||||||
|
[region for region in regions if region.Reward == RewardType.BossTokenPhantoon] + \
|
||||||
|
[region for region in regions if region.Reward == RewardType.BossTokenDraygon] + \
|
||||||
|
[region for region in regions if region.Reward == RewardType.BossTokenRidley]
|
||||||
|
|
||||||
self.patches += self.RewardPatches(crystalRegions, crystalRewards, self.CrystalValues)
|
self.patches += self.RewardPatches(crystalRegions, crystalRewards, self.CrystalValues)
|
||||||
self.patches += self.RewardPatches(pendantRegions, pendantRewards, self.PendantValues)
|
self.patches += self.RewardPatches(pendantRegions, pendantRewards, self.PendantValues)
|
||||||
|
self.patches += self.RewardPatches(bossRegions, bossTokens, self.BossTokenValues)
|
||||||
|
|
||||||
def RewardPatches(self, regions: List[IReward], rewards: List[int], rewardValues: Callable):
|
def RewardPatches(self, regions: List[IReward], rewards: List[int], rewardValues: Callable):
|
||||||
addresses = [self.RewardAddresses(region) for region in regions]
|
addresses = [self.RewardAddresses(region) for region in regions]
|
||||||
@@ -189,17 +209,22 @@ class Patch:
|
|||||||
|
|
||||||
def RewardAddresses(self, region: IReward):
|
def RewardAddresses(self, region: IReward):
|
||||||
regionType = {
|
regionType = {
|
||||||
EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE ],
|
EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE, 0x30D100],
|
||||||
DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF ],
|
DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF, 0x30D101 ],
|
||||||
TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706 ],
|
TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706, 0x30D102 ],
|
||||||
PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702 ],
|
PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702, 0x30D103 ],
|
||||||
SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701 ],
|
SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701, 0x30D104 ],
|
||||||
SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704 ],
|
SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704, 0x30D105 ],
|
||||||
ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707 ],
|
ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707, 0x30D106 ],
|
||||||
IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705 ],
|
IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705, 0x30D107 ],
|
||||||
MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703 ],
|
MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703, 0x30D108 ],
|
||||||
TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708 ]
|
TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708, 0x30D109 ],
|
||||||
|
Kraid : [ 0xF26002, 0xF26004, 0xF26005, 0xF26000, 0xF26006, 0xF26007, 0x82FD36 ],
|
||||||
|
WreckedShip : [ 0xF2600A, 0xF2600C, 0xF2600D, 0xF26008, 0xF2600E, 0xF2600F, 0x82FE26 ],
|
||||||
|
Inner : [ 0xF26012, 0xF26014, 0xF26015, 0xF26010, 0xF26016, 0xF26017, 0x82FE76 ],
|
||||||
|
East : [ 0xF2601A, 0xF2601C, 0xF2601D, 0xF26018, 0xF2601E, 0xF2601F, 0x82FDD6 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
result = regionType.get(type(region), None)
|
result = regionType.get(type(region), None)
|
||||||
if result is None:
|
if result is None:
|
||||||
raise exception(f"Region {region} should not be a dungeon reward region")
|
raise exception(f"Region {region} should not be a dungeon reward region")
|
||||||
@@ -208,13 +233,13 @@ class Patch:
|
|||||||
|
|
||||||
def CrystalValues(self, crystal: int):
|
def CrystalValues(self, crystal: int):
|
||||||
crystalMap = {
|
crystalMap = {
|
||||||
1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06 ],
|
1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x10 ],
|
||||||
2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06 ],
|
2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x10 ],
|
||||||
3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06 ],
|
3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x10 ],
|
||||||
4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06 ],
|
4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06, 0x10 ],
|
||||||
5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06 ],
|
5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x11 ],
|
||||||
6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06 ],
|
6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x11 ],
|
||||||
7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06 ],
|
7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x10 ],
|
||||||
}
|
}
|
||||||
result = crystalMap.get(crystal, None)
|
result = crystalMap.get(crystal, None)
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -224,15 +249,28 @@ class Patch:
|
|||||||
|
|
||||||
def PendantValues(self, pendant: int):
|
def PendantValues(self, pendant: int):
|
||||||
pendantMap = {
|
pendantMap = {
|
||||||
1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01 ],
|
1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x12 ],
|
||||||
2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03 ],
|
2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x14 ],
|
||||||
3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02 ],
|
3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x13 ]
|
||||||
}
|
}
|
||||||
result = pendantMap.get(pendant, None)
|
result = pendantMap.get(pendant, None)
|
||||||
if result is None:
|
if result is None:
|
||||||
raise exception(f"Tried using {pendant} as a pendant number")
|
raise exception(f"Tried using {pendant} as a pendant number")
|
||||||
else:
|
else:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def BossTokenValues(self, token: int):
|
||||||
|
tokenMap = {
|
||||||
|
1 : [ 0x01, 0x38, 0x40, 0x80, 0x69, 0x80, 0x15 ],
|
||||||
|
2 : [ 0x02, 0x34, 0x42, 0x80, 0x69, 0x81, 0x16 ],
|
||||||
|
3 : [ 0x04, 0x34, 0x44, 0x80, 0x69, 0x82, 0x17 ],
|
||||||
|
4 : [ 0x08, 0x32, 0x46, 0x80, 0x69, 0x83, 0x18 ]
|
||||||
|
}
|
||||||
|
result = tokenMap.get(token, None)
|
||||||
|
if result is None:
|
||||||
|
raise exception(f"Tried using {token} as a boss token number")
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
|
||||||
def WriteSMLocations(self, locations: List[Location]):
|
def WriteSMLocations(self, locations: List[Location]):
|
||||||
def GetSMItemPLM(location:Location):
|
def GetSMItemPLM(location:Location):
|
||||||
@@ -259,7 +297,7 @@ class Patch:
|
|||||||
ItemType.SpaceJump : 0xEF1B,
|
ItemType.SpaceJump : 0xEF1B,
|
||||||
ItemType.ScrewAttack : 0xEF1F
|
ItemType.ScrewAttack : 0xEF1F
|
||||||
}
|
}
|
||||||
plmId = 0xEFE0 if self.myWorld.Config.GameMode == GameMode.Multiworld else \
|
plmId = 0xEFE0 if self.myWorld.Config.Multiworld else \
|
||||||
itemMap.get(location.APLocation.item.item.Type, 0xEFE0)
|
itemMap.get(location.APLocation.item.item.Type, 0xEFE0)
|
||||||
if (plmId == 0xEFE0):
|
if (plmId == 0xEFE0):
|
||||||
plmId += 4 if location.Type == LocationType.Chozo else 8 if location.Type == LocationType.Hidden else 0
|
plmId += 4 if location.Type == LocationType.Chozo else 8 if location.Type == LocationType.Hidden else 0
|
||||||
@@ -268,7 +306,7 @@ class Patch:
|
|||||||
return plmId
|
return plmId
|
||||||
|
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if (self.myWorld.Config.GameMode == GameMode.Multiworld):
|
if (self.myWorld.Config.Multiworld):
|
||||||
self.patches.append((Snes(location.Address), getWordArray(GetSMItemPLM(location))))
|
self.patches.append((Snes(location.Address), getWordArray(GetSMItemPLM(location))))
|
||||||
self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location)))
|
self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location)))
|
||||||
else:
|
else:
|
||||||
@@ -283,18 +321,14 @@ class Patch:
|
|||||||
self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB]))
|
self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB]))
|
||||||
elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]):
|
elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]):
|
||||||
text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something))
|
text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something))
|
||||||
dialog = Dialog.Simple(text)
|
|
||||||
if (location.Type == LocationType.Pedestal):
|
if (location.Type == LocationType.Pedestal):
|
||||||
self.stringTable.SetPedestalText(text)
|
self.stringTable.SetPedestalText(text)
|
||||||
self.patches.append((Snes(0x308300), dialog))
|
|
||||||
elif (location.Type == LocationType.Ether):
|
elif (location.Type == LocationType.Ether):
|
||||||
self.stringTable.SetEtherText(text)
|
self.stringTable.SetEtherText(text)
|
||||||
self.patches.append((Snes(0x308F00), dialog))
|
|
||||||
elif (location.Type == LocationType.Bombos):
|
elif (location.Type == LocationType.Bombos):
|
||||||
self.stringTable.SetBombosText(text)
|
self.stringTable.SetBombosText(text)
|
||||||
self.patches.append((Snes(0x309000), dialog))
|
|
||||||
|
|
||||||
if (self.myWorld.Config.GameMode == GameMode.Multiworld):
|
if (self.myWorld.Config.Multiworld):
|
||||||
self.patches.append((Snes(location.Address), [(location.Id - 256)]))
|
self.patches.append((Snes(location.Address), [(location.Id - 256)]))
|
||||||
self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location)))
|
self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location)))
|
||||||
else:
|
else:
|
||||||
@@ -305,11 +339,11 @@ class Patch:
|
|||||||
item = location.APLocation.item.item
|
item = location.APLocation.item.item
|
||||||
itemDungeon = None
|
itemDungeon = None
|
||||||
if item.IsKey():
|
if item.IsKey():
|
||||||
itemDungeon = ItemType.Key if (not item.World.Config.Keysanity or item.Type != ItemType.KeyHC) else ItemType.KeyHC
|
itemDungeon = ItemType.Key
|
||||||
elif item.IsBigKey():
|
elif item.IsBigKey():
|
||||||
itemDungeon = ItemType.BigKey
|
itemDungeon = ItemType.BigKey
|
||||||
elif item.IsMap():
|
elif item.IsMap():
|
||||||
itemDungeon = ItemType.Map if (not item.World.Config.Keysanity or item.Type != ItemType.MapHC) else ItemType.MapHC
|
itemDungeon = ItemType.Map
|
||||||
elif item.IsCompass():
|
elif item.IsCompass():
|
||||||
itemDungeon = ItemType.Compass
|
itemDungeon = ItemType.Compass
|
||||||
|
|
||||||
@@ -327,15 +361,11 @@ class Patch:
|
|||||||
|
|
||||||
def WriteDungeonMusic(self, keysanity: bool):
|
def WriteDungeonMusic(self, keysanity: bool):
|
||||||
if (not keysanity):
|
if (not keysanity):
|
||||||
regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)]
|
regions = [region for region in self.myWorld.Regions if isinstance(region, Z3Region) and isinstance(region, IReward) and
|
||||||
music = []
|
region.Reward != None and region.Reward != RewardType.Agahnim]
|
||||||
pendantRegions = [region for region in regions if region.Reward in [RewardType.PendantGreen, RewardType.PendantNonGreen]]
|
pendantRegions = [region for region in regions if region.Reward in [RewardType.PendantGreen, RewardType.PendantNonGreen]]
|
||||||
crystalRegions = [region for region in regions if region.Reward in [RewardType.CrystalBlue, RewardType.CrystalRed]]
|
crystalRegions = [region for region in regions if region.Reward in [RewardType.CrystalBlue, RewardType.CrystalRed]]
|
||||||
regions = pendantRegions + crystalRegions
|
music = [0x11 if (region.Reward == RewardType.PendantGreen or region.Reward == RewardType.PendantNonGreen) else 0x16 for region in regions]
|
||||||
music = [
|
|
||||||
0x11, 0x11, 0x11, 0x16, 0x16,
|
|
||||||
0x16, 0x16, 0x16, 0x16, 0x16,
|
|
||||||
]
|
|
||||||
self.patches += self.MusicPatches(regions, music)
|
self.patches += self.MusicPatches(regions, music)
|
||||||
|
|
||||||
#IEnumerable<byte> RandomDungeonMusic() {
|
#IEnumerable<byte> RandomDungeonMusic() {
|
||||||
@@ -366,51 +396,13 @@ class Patch:
|
|||||||
else:
|
else:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def WritePrizeShuffle(self):
|
def WritePrizeShuffle(self, dropPrizes):
|
||||||
prizePackItems = 56
|
self.patches.append((Snes(0x6FA78), [e.value for e in dropPrizes.Packs]))
|
||||||
treePullItems = 3
|
self.patches.append((Snes(0x1DFBD4), [e.value for e in dropPrizes.TreePulls]))
|
||||||
|
self.patches.append((Snes(0x6A9C8), [dropPrizes.CrabContinous.value]))
|
||||||
bytes = []
|
self.patches.append((Snes(0x6A9C4), [dropPrizes.CrabFinal.value]))
|
||||||
drop = 0
|
self.patches.append((Snes(0x6F993), [dropPrizes.Stun.value]))
|
||||||
final = 0
|
self.patches.append((Snes(0x1D82CC), [dropPrizes.Fish.value]))
|
||||||
|
|
||||||
pool = [
|
|
||||||
DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, #// pack 1
|
|
||||||
DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Red, DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Blue, #// pack 2
|
|
||||||
DropPrize.FullMagic, DropPrize.Magic, DropPrize.Magic, DropPrize.Blue, DropPrize.FullMagic, DropPrize.Magic, DropPrize.Heart, DropPrize.Magic, #// pack 3
|
|
||||||
DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb4, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb8, DropPrize.Bomb1, #// pack 4
|
|
||||||
DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10, DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10,#// pack 5
|
|
||||||
DropPrize.Magic, DropPrize.Green, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Magic, DropPrize.Bomb1, DropPrize.Green, DropPrize.Heart, #// pack 6
|
|
||||||
DropPrize.Heart, DropPrize.Fairy, DropPrize.FullMagic, DropPrize.Red, DropPrize.Bomb8, DropPrize.Heart, DropPrize.Red, DropPrize.Arrow10, #// pack 7
|
|
||||||
DropPrize.Green, DropPrize.Blue, DropPrize.Red,#// from pull trees
|
|
||||||
DropPrize.Green, DropPrize.Red,#// from prize crab
|
|
||||||
DropPrize.Green, #// stunned prize
|
|
||||||
DropPrize.Red,#// saved fish prize
|
|
||||||
]
|
|
||||||
|
|
||||||
prizes = pool
|
|
||||||
self.rnd.shuffle(prizes)
|
|
||||||
|
|
||||||
#/* prize pack drop order */
|
|
||||||
(bytes, prizes) = SplitOff(prizes, prizePackItems)
|
|
||||||
self.patches.append((Snes(0x6FA78), [byte.value for byte in bytes]))
|
|
||||||
|
|
||||||
#/* tree pull prizes */
|
|
||||||
(bytes, prizes) = SplitOff(prizes, treePullItems)
|
|
||||||
self.patches.append((Snes(0x1DFBD4), [byte.value for byte in bytes]))
|
|
||||||
|
|
||||||
#/* crab prizes */
|
|
||||||
(drop, final, prizes) = (prizes[0], prizes[1], prizes[2:])
|
|
||||||
self.patches.append((Snes(0x6A9C8), [ drop.value ]))
|
|
||||||
self.patches.append((Snes(0x6A9C4), [ final.value ]))
|
|
||||||
|
|
||||||
#/* stun prize */
|
|
||||||
(drop, prizes) = (prizes[0], prizes[1:])
|
|
||||||
self.patches.append((Snes(0x6F993), [ drop.value ]))
|
|
||||||
|
|
||||||
#/* fish prize */
|
|
||||||
drop = prizes[0]
|
|
||||||
self.patches.append((Snes(0x1D82CC), [ drop.value ]))
|
|
||||||
|
|
||||||
self.patches += self.EnemyPrizePackDistribution()
|
self.patches += self.EnemyPrizePackDistribution()
|
||||||
|
|
||||||
@@ -524,46 +516,29 @@ class Patch:
|
|||||||
redCrystalDungeons = [region for region in regions if region.Reward == RewardType.CrystalRed]
|
redCrystalDungeons = [region for region in regions if region.Reward == RewardType.CrystalRed]
|
||||||
|
|
||||||
sahasrahla = Texts.SahasrahlaReveal(greenPendantDungeon)
|
sahasrahla = Texts.SahasrahlaReveal(greenPendantDungeon)
|
||||||
self.patches.append((Snes(0x308A00), Dialog.Simple(sahasrahla)))
|
|
||||||
self.stringTable.SetSahasrahlaRevealText(sahasrahla)
|
self.stringTable.SetSahasrahlaRevealText(sahasrahla)
|
||||||
|
|
||||||
bombShop = Texts.BombShopReveal(redCrystalDungeons)
|
bombShop = Texts.BombShopReveal(redCrystalDungeons)
|
||||||
self.patches.append((Snes(0x308E00), Dialog.Simple(bombShop)))
|
|
||||||
self.stringTable.SetBombShopRevealText(bombShop)
|
self.stringTable.SetBombShopRevealText(bombShop)
|
||||||
|
|
||||||
blind = Texts.Blind(self.rnd)
|
blind = Texts.Blind(self.rnd)
|
||||||
self.patches.append((Snes(0x308800), Dialog.Simple(blind)))
|
|
||||||
self.stringTable.SetBlindText(blind)
|
self.stringTable.SetBlindText(blind)
|
||||||
|
|
||||||
tavernMan = Texts.TavernMan(self.rnd)
|
tavernMan = Texts.TavernMan(self.rnd)
|
||||||
self.patches.append((Snes(0x308C00), Dialog.Simple(tavernMan)))
|
|
||||||
self.stringTable.SetTavernManText(tavernMan)
|
self.stringTable.SetTavernManText(tavernMan)
|
||||||
|
|
||||||
ganon = Texts.GanonFirstPhase(self.rnd)
|
ganon = Texts.GanonFirstPhase(self.rnd)
|
||||||
self.patches.append((Snes(0x308600), Dialog.Simple(ganon)))
|
|
||||||
self.stringTable.SetGanonFirstPhaseText(ganon)
|
self.stringTable.SetGanonFirstPhaseText(ganon)
|
||||||
|
|
||||||
#// Todo: Verify these two are correct if ganon invincible patch is ever added
|
|
||||||
#// ganon_fall_in_alt in v30
|
|
||||||
ganonFirstPhaseInvincible = "You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!"
|
|
||||||
self.patches.append((Snes(0x309100), Dialog.Simple(ganonFirstPhaseInvincible)))
|
|
||||||
|
|
||||||
#// ganon_phase_3_alt in v30
|
|
||||||
ganonThirdPhaseInvincible = "Got wax in\nyour ears?\nI cannot die!"
|
|
||||||
self.patches.append((Snes(0x309200), Dialog.Simple(ganonThirdPhaseInvincible)))
|
|
||||||
#// ---
|
|
||||||
|
|
||||||
silversLocation = [loc for world in self.allWorlds for loc in world.Locations if loc.ItemIs(ItemType.SilverArrows, self.myWorld)]
|
silversLocation = [loc for world in self.allWorlds for loc in world.Locations if loc.ItemIs(ItemType.SilverArrows, self.myWorld)]
|
||||||
if len(silversLocation) == 0:
|
if len(silversLocation) == 0:
|
||||||
silvers = Texts.GanonThirdPhaseMulti(None, self.myWorld, self.silversWorldID, self.playerIDToNames[self.silversWorldID])
|
silvers = Texts.GanonThirdPhaseMulti(None, self.myWorld, self.silversWorldID, self.playerIDToNames[self.silversWorldID])
|
||||||
else:
|
else:
|
||||||
silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.GameMode == GameMode.Multiworld else \
|
silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.Multiworld else \
|
||||||
Texts.GanonThirdPhaseSingle(silversLocation[0].Region)
|
Texts.GanonThirdPhaseSingle(silversLocation[0].Region)
|
||||||
self.patches.append((Snes(0x308700), Dialog.Simple(silvers)))
|
|
||||||
self.stringTable.SetGanonThirdPhaseText(silvers)
|
self.stringTable.SetGanonThirdPhaseText(silvers)
|
||||||
|
|
||||||
triforceRoom = Texts.TriforceRoom(self.rnd)
|
triforceRoom = Texts.TriforceRoom(self.rnd)
|
||||||
self.patches.append((Snes(0x308400), Dialog.Simple(triforceRoom)))
|
|
||||||
self.stringTable.SetTriforceRoomText(triforceRoom)
|
self.stringTable.SetTriforceRoomText(triforceRoom)
|
||||||
|
|
||||||
def WriteStringTable(self):
|
def WriteStringTable(self):
|
||||||
@@ -579,26 +554,32 @@ class Patch:
|
|||||||
return bytearray(name, 'utf8')
|
return bytearray(name, 'utf8')
|
||||||
|
|
||||||
def WriteSeedData(self):
|
def WriteSeedData(self):
|
||||||
configField = \
|
configField1 = \
|
||||||
((1 if self.myWorld.Config.Race else 0) << 15) | \
|
((1 if self.myWorld.Config.Race else 0) << 15) | \
|
||||||
((1 if self.myWorld.Config.Keysanity else 0) << 13) | \
|
((1 if self.myWorld.Config.Keysanity else 0) << 13) | \
|
||||||
((1 if self.myWorld.Config.GameMode == Config.GameMode.Multiworld else 0) << 12) | \
|
((1 if self.myWorld.Config.Multiworld else 0) << 12) | \
|
||||||
(self.myWorld.Config.Z3Logic.value << 10) | \
|
(self.myWorld.Config.Z3Logic.value << 10) | \
|
||||||
(self.myWorld.Config.SMLogic.value << 8) | \
|
(self.myWorld.Config.SMLogic.value << 8) | \
|
||||||
(Patch.Major << 4) | \
|
(Patch.Major << 4) | \
|
||||||
(Patch.Minor << 0)
|
(Patch.Minor << 0)
|
||||||
|
|
||||||
|
configField2 = \
|
||||||
|
((1 if self.myWorld.Config.SwordLocation else 0) << 14) | \
|
||||||
|
((1 if self.myWorld.Config.MorphLocation else 0) << 12) | \
|
||||||
|
((1 if self.myWorld.Config.Goal else 0) << 8)
|
||||||
|
|
||||||
self.patches.append((Snes(0x80FF50), getWordArray(self.myWorld.Id)))
|
self.patches.append((Snes(0x80FF50), getWordArray(self.myWorld.Id)))
|
||||||
self.patches.append((Snes(0x80FF52), getWordArray(configField)))
|
self.patches.append((Snes(0x80FF52), getWordArray(configField1)))
|
||||||
self.patches.append((Snes(0x80FF54), getDoubleWordArray(self.seed)))
|
self.patches.append((Snes(0x80FF54), getDoubleWordArray(self.seed)))
|
||||||
|
self.patches.append((Snes(0x80FF58), getWordArray(configField2)))
|
||||||
#/* Reserve the rest of the space for future use */
|
#/* Reserve the rest of the space for future use */
|
||||||
self.patches.append((Snes(0x80FF58), [0x00] * 8))
|
self.patches.append((Snes(0x80FF5A), [0x00] * 6))
|
||||||
self.patches.append((Snes(0x80FF60), bytearray(self.seedGuid, 'utf8')))
|
self.patches.append((Snes(0x80FF60), bytearray(self.seedGuid, 'utf8')))
|
||||||
self.patches.append((Snes(0x80FF80), bytearray(self.myWorld.Guid, 'utf8')))
|
self.patches.append((Snes(0x80FF80), bytearray(self.myWorld.Guid, 'utf8')))
|
||||||
|
|
||||||
def WriteCommonFlags(self):
|
def WriteCommonFlags(self):
|
||||||
#/* Common Combo Configuration flags at [asm]/config.asm */
|
#/* Common Combo Configuration flags at [asm]/config.asm */
|
||||||
if (self.myWorld.Config.GameMode == GameMode.Multiworld):
|
if (self.myWorld.Config.Multiworld):
|
||||||
self.patches.append((Snes(0xF47000), getWordArray(0x0001)))
|
self.patches.append((Snes(0xF47000), getWordArray(0x0001)))
|
||||||
if (self.myWorld.Config.Keysanity):
|
if (self.myWorld.Config.Keysanity):
|
||||||
self.patches.append((Snes(0xF47006), getWordArray(0x0001)))
|
self.patches.append((Snes(0xF47006), getWordArray(0x0001)))
|
||||||
@@ -619,97 +600,104 @@ class Patch:
|
|||||||
if (self.myWorld.Config.Keysanity):
|
if (self.myWorld.Config.Keysanity):
|
||||||
self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item
|
self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item
|
||||||
self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass
|
self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass
|
||||||
self.patches.append((Snes(0x40016A), [ 0x01 ])) #// enable local item dialog boxes for dungeon and keycard items
|
self.patches.append((Snes(0x40016A), [ 0x01 ])) #// FreeItemText: db #$01 ; #00 = Off (default) - #$01 = On
|
||||||
|
|
||||||
def WriteSMKeyCardDoors(self):
|
def WriteSMKeyCardDoors(self):
|
||||||
if (not self.myWorld.Config.Keysanity):
|
plaquePlm = 0xd410
|
||||||
return
|
|
||||||
|
|
||||||
plaquePLm = 0xd410
|
|
||||||
|
|
||||||
doorList = [
|
|
||||||
#// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created)
|
|
||||||
#// Crateria
|
|
||||||
[ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet
|
|
||||||
[ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB
|
|
||||||
[ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door)
|
|
||||||
[ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4
|
|
||||||
[ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo
|
|
||||||
|
|
||||||
#// Brinstar
|
|
||||||
[ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room
|
|
||||||
|
|
||||||
[ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area
|
|
||||||
[ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn
|
|
||||||
[ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank
|
|
||||||
[ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super
|
|
||||||
|
|
||||||
[ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid
|
|
||||||
|
|
||||||
#// Upper Norfair
|
|
||||||
[ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice
|
|
||||||
[ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice
|
|
||||||
|
|
||||||
[ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain
|
|
||||||
[ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain
|
|
||||||
[ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain
|
|
||||||
[ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain
|
|
||||||
|
|
||||||
[ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire
|
|
||||||
|
|
||||||
#// Lower Norfair
|
|
||||||
[ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre
|
|
||||||
[ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry"
|
|
||||||
[ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley
|
|
||||||
|
|
||||||
#// Maridia
|
|
||||||
[ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia
|
|
||||||
[ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach
|
|
||||||
|
|
||||||
[ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon
|
|
||||||
[ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon
|
|
||||||
|
|
||||||
[ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon
|
|
||||||
|
|
||||||
[ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking)
|
|
||||||
|
|
||||||
#// Wrecked Ship
|
|
||||||
[ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check
|
|
||||||
[ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley
|
|
||||||
[ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley
|
|
||||||
|
|
||||||
[ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon
|
|
||||||
]
|
|
||||||
|
|
||||||
doorId = 0x0000
|
|
||||||
plmTablePos = 0xf800
|
plmTablePos = 0xf800
|
||||||
for door in doorList:
|
|
||||||
doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3]
|
|
||||||
if (door[6] == 0):
|
|
||||||
#// Write dynamic door
|
|
||||||
doorData = []
|
|
||||||
for x in door[0:3]:
|
|
||||||
doorData += getWordArray(x)
|
|
||||||
doorData += getWordArray(doorArgs)
|
|
||||||
self.patches.append((Snes(0x8f0000 + plmTablePos), doorData))
|
|
||||||
plmTablePos += 0x08
|
|
||||||
else:
|
|
||||||
#// Overwrite existing door
|
|
||||||
doorData = []
|
|
||||||
for x in door[1:3]:
|
|
||||||
doorData += getWordArray(x)
|
|
||||||
doorData += getWordArray(doorArgs)
|
|
||||||
self.patches.append((Snes(0x8f0000 + door[6]), doorData))
|
|
||||||
if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss):
|
|
||||||
#// Overwrite the extra parts of the Gadora with a PLM that just deletes itself
|
|
||||||
self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ]))
|
|
||||||
|
|
||||||
#// Plaque data
|
if ( self.myWorld.Config.Keysanity):
|
||||||
if (door[4] != KeycardPlaque.Null):
|
doorList = [
|
||||||
plaqueData = getWordArray(door[0]) + getWordArray(plaquePLm) + getWordArray(door[5]) + getWordArray(door[4])
|
#// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created)
|
||||||
self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData))
|
#// Crateria
|
||||||
plmTablePos += 0x08
|
[ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet
|
||||||
doorId += 1
|
[ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB
|
||||||
|
[ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door)
|
||||||
|
[ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4
|
||||||
|
[ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo
|
||||||
|
|
||||||
|
#// Brinstar
|
||||||
|
[ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room
|
||||||
|
|
||||||
|
[ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area
|
||||||
|
[ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn
|
||||||
|
[ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank
|
||||||
|
[ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super
|
||||||
|
|
||||||
|
[ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid
|
||||||
|
|
||||||
|
#// Upper Norfair
|
||||||
|
[ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice
|
||||||
|
[ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice
|
||||||
|
|
||||||
|
[ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain
|
||||||
|
[ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain
|
||||||
|
[ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain
|
||||||
|
[ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain
|
||||||
|
|
||||||
|
[ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire
|
||||||
|
|
||||||
|
#// Lower Norfair
|
||||||
|
[ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre
|
||||||
|
[ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry"
|
||||||
|
[ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley
|
||||||
|
|
||||||
|
#// Maridia
|
||||||
|
[ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia
|
||||||
|
[ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach
|
||||||
|
|
||||||
|
[ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon
|
||||||
|
[ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon
|
||||||
|
|
||||||
|
[ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon
|
||||||
|
|
||||||
|
[ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking)
|
||||||
|
|
||||||
|
#// Wrecked Ship
|
||||||
|
[ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check
|
||||||
|
[ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley
|
||||||
|
[ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley
|
||||||
|
|
||||||
|
[ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon
|
||||||
|
]
|
||||||
|
|
||||||
|
doorId = 0x0000
|
||||||
|
for door in doorList:
|
||||||
|
#/* When "Fast Ganon" is set, don't place the G4 Boss key door to enable faster games */
|
||||||
|
if (door[0] == 0x99BD and self.myWorld.Config.Goal == Goal.FastGanonDefeatMotherBrain):
|
||||||
|
continue
|
||||||
|
doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3]
|
||||||
|
if (door[6] == 0):
|
||||||
|
#// Write dynamic door
|
||||||
|
doorData = []
|
||||||
|
for x in door[0:3]:
|
||||||
|
doorData += getWordArray(x)
|
||||||
|
doorData += getWordArray(doorArgs)
|
||||||
|
self.patches.append((Snes(0x8f0000 + plmTablePos), doorData))
|
||||||
|
plmTablePos += 0x08
|
||||||
|
else:
|
||||||
|
#// Overwrite existing door
|
||||||
|
doorData = []
|
||||||
|
for x in door[1:3]:
|
||||||
|
doorData += getWordArray(x)
|
||||||
|
doorData += getWordArray(doorArgs)
|
||||||
|
self.patches.append((Snes(0x8f0000 + door[6]), doorData))
|
||||||
|
if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss):
|
||||||
|
#// Overwrite the extra parts of the Gadora with a PLM that just deletes itself
|
||||||
|
self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ]))
|
||||||
|
|
||||||
|
#// Plaque data
|
||||||
|
if (door[4] != KeycardPlaque.Null):
|
||||||
|
plaqueData = getWordArray(door[0]) + getWordArray(plaquePlm) + getWordArray(door[5]) + getWordArray(door[4])
|
||||||
|
self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData))
|
||||||
|
plmTablePos += 0x08
|
||||||
|
doorId += 1
|
||||||
|
|
||||||
|
#/* Write plaque showing SM bosses that needs to be killed */
|
||||||
|
if (self.myWorld.Config.OpenTourian != OpenTourian.FourBosses):
|
||||||
|
plaqueData = getWordArray(0xA5ED) + getWordArray(plaquePlm) + getWordArray(0x044F) + getWordArray(KeycardPlaque.Zero + self.myWorld.TourianBossTokens)
|
||||||
|
self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData))
|
||||||
|
plmTablePos += 0x08
|
||||||
|
|
||||||
self.patches.append((Snes(0x8f0000 + plmTablePos), [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]))
|
self.patches.append((Snes(0x8f0000 + plmTablePos), [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]))
|
||||||
|
|
||||||
@@ -745,20 +733,32 @@ class Patch:
|
|||||||
(Snes(0xDD313), [ 0x00, 0x00, 0xE4, 0xFF, 0x08, 0x0E ]),
|
(Snes(0xDD313), [ 0x00, 0x00, 0xE4, 0xFF, 0x08, 0x0E ]),
|
||||||
]
|
]
|
||||||
|
|
||||||
def WriteGanonInvicible(self, invincible: GanonInvincible):
|
def WritePreOpenPyramid(self, goal: Goal):
|
||||||
|
if (goal == Goal.FastGanonDefeatMotherBrain):
|
||||||
|
self.patches.append((Snes(0x30808B), [0x01]))
|
||||||
|
|
||||||
|
def WriteGanonInvicible(self, goal: Goal):
|
||||||
#/* Defaults to $00 (never) at [asm]/z3/randomizer/tables.asm */
|
#/* Defaults to $00 (never) at [asm]/z3/randomizer/tables.asm */
|
||||||
invincibleMap = {
|
valueMap = {
|
||||||
GanonInvincible.Never : 0x00,
|
Goal.DefeatBoth : 0x03,
|
||||||
GanonInvincible.Always : 0x01,
|
Goal.FastGanonDefeatMotherBrain : 0x04,
|
||||||
GanonInvincible.BeforeAllDungeons : 0x02,
|
Goal.AllDungeonsDefeatMotherBrain : 0x02
|
||||||
GanonInvincible.BeforeCrystals : 0x03,
|
}
|
||||||
}
|
value = valueMap.get(goal, None)
|
||||||
value = invincibleMap.get(invincible, None)
|
|
||||||
if (value is None):
|
if (value is None):
|
||||||
raise exception(f"Unknown Ganon invincible value {invincible}")
|
raise exception(f"Unknown Ganon invincible value {goal}")
|
||||||
else:
|
else:
|
||||||
self.patches.append((Snes(0x30803E), [value]))
|
self.patches.append((Snes(0x30803E), [value]))
|
||||||
|
|
||||||
|
def WriteBossesNeeded(self, tourianBossTokens):
|
||||||
|
self.patches.append((Snes(0xF47200), getWordArray(tourianBossTokens)))
|
||||||
|
|
||||||
|
def WriteCrystalsNeeded(self, towerCrystals, ganonCrystals):
|
||||||
|
self.patches.append((Snes(0x30805E), [towerCrystals]))
|
||||||
|
self.patches.append((Snes(0x30805F), [ganonCrystals]))
|
||||||
|
|
||||||
|
self.stringTable.SetTowerRequirementText(f"You need {towerCrystals} crystals to enter Ganon's Tower.")
|
||||||
|
self.stringTable.SetGanonRequirementText(f"You need {ganonCrystals} crystals to defeat Ganon.")
|
||||||
|
|
||||||
def WriteRngBlock(self):
|
def WriteRngBlock(self):
|
||||||
#/* Repoint RNG Block */
|
#/* Repoint RNG Block */
|
||||||
|
|||||||
@@ -5,12 +5,19 @@ from worlds.smz3.TotalSMZ3.Item import Item, ItemType
|
|||||||
|
|
||||||
class RewardType(Enum):
|
class RewardType(Enum):
|
||||||
Null = 0
|
Null = 0
|
||||||
Agahnim = 1
|
Agahnim = 1 << 0
|
||||||
PendantGreen = 2
|
PendantGreen = 1 << 1
|
||||||
PendantNonGreen = 3
|
PendantNonGreen = 1 << 2
|
||||||
CrystalBlue = 4
|
CrystalBlue = 1 << 3
|
||||||
CrystalRed = 5
|
CrystalRed = 1 << 4
|
||||||
GoldenFourBoss = 6
|
BossTokenKraid = 1 << 5
|
||||||
|
BossTokenPhantoon = 1 << 6
|
||||||
|
BossTokenDraygon = 1 << 7
|
||||||
|
BossTokenRidley = 1 << 8
|
||||||
|
|
||||||
|
AnyPendant = PendantGreen | PendantNonGreen
|
||||||
|
AnyCrystal = CrystalBlue | CrystalRed
|
||||||
|
AnyBossToken = BossTokenKraid | BossTokenPhantoon | BossTokenDraygon | BossTokenRidley
|
||||||
|
|
||||||
class IReward:
|
class IReward:
|
||||||
Reward: RewardType
|
Reward: RewardType
|
||||||
@@ -18,7 +25,7 @@ class IReward:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class IMedallionAccess:
|
class IMedallionAccess:
|
||||||
Medallion: object
|
Medallion = None
|
||||||
|
|
||||||
class Region:
|
class Region:
|
||||||
import worlds.smz3.TotalSMZ3.Location as Location
|
import worlds.smz3.TotalSMZ3.Location as Location
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class Kraid(SMRegion, IReward):
|
|||||||
Name = "Brinstar Kraid"
|
Name = "Brinstar Kraid"
|
||||||
Area = "Brinstar"
|
Area = "Brinstar"
|
||||||
|
|
||||||
Reward = RewardType.GoldenFourBoss
|
Reward = RewardType.Null
|
||||||
|
|
||||||
def __init__(self, world, config: Config):
|
def __init__(self, world, config: Config):
|
||||||
super().__init__(world, config)
|
super().__init__(world, config)
|
||||||
|
|||||||
@@ -40,5 +40,5 @@ class Pink(SMRegion):
|
|||||||
else:
|
else:
|
||||||
return items.CanOpenRedDoors() and (items.CanDestroyBombWalls() or items.SpeedBooster) or \
|
return items.CanOpenRedDoors() and (items.CanDestroyBombWalls() or items.SpeedBooster) or \
|
||||||
items.CanUsePowerBombs() or \
|
items.CanUsePowerBombs() or \
|
||||||
items.CanAccessNorfairUpperPortal() and items.Morph and (items.CanOpenRedDoors() or items.Wave) and \
|
items.CanAccessNorfairUpperPortal() and items.Morph and (items.Missile or items.Super or items.Wave ) and \
|
||||||
(items.Ice or items.HiJump or items.CanSpringBallJump() or items.CanFly())
|
(items.Ice or items.HiJump or items.CanSpringBallJump() or items.CanFly())
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user