mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 17:43:53 -07:00
Compare commits
56 Commits
0.3.4
...
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._region_cache[new_id] = {}
|
||||
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)
|
||||
for option_key, option in Options.common_options.items():
|
||||
getattr(self, option_key)[new_id] = option(option.default)
|
||||
@@ -204,7 +204,7 @@ class MultiWorld():
|
||||
for player in self.player_ids:
|
||||
self.custom_data[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, {}))
|
||||
|
||||
self.worlds[player] = world_type(self, player)
|
||||
@@ -384,7 +384,6 @@ class MultiWorld():
|
||||
return self.worlds[player].create_item(item_name)
|
||||
|
||||
def push_precollected(self, item: Item):
|
||||
item.world = self
|
||||
self.precollected_items[item.player].append(item)
|
||||
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}."
|
||||
location.item = item
|
||||
item.location = location
|
||||
item.world = self # try to not have this here anymore and create it with item?
|
||||
if collect:
|
||||
self.state.collect(item, location.event, location)
|
||||
|
||||
@@ -1066,26 +1064,25 @@ class LocationProgressType(IntEnum):
|
||||
|
||||
|
||||
class Location:
|
||||
# If given as integer, then this is the shop's inventory index
|
||||
shop_slot: Optional[int] = None
|
||||
shop_slot_disabled: bool = False
|
||||
game: str = "Generic"
|
||||
player: int
|
||||
name: str
|
||||
address: Optional[int]
|
||||
parent_region: Optional[Region]
|
||||
event: bool = False
|
||||
locked: bool = False
|
||||
game: str = "Generic"
|
||||
show_in_spoiler: bool = True
|
||||
crystal: bool = False
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda item, state: False)
|
||||
access_rule = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
parent_region: Optional[Region]
|
||||
|
||||
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
|
||||
self.name: str = name
|
||||
self.address: Optional[int] = address
|
||||
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
|
||||
self.player = player
|
||||
self.name = name
|
||||
self.address = address
|
||||
self.parent_region = parent
|
||||
self.player: int = player
|
||||
|
||||
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)))
|
||||
@@ -1102,7 +1099,6 @@ class Location:
|
||||
self.item = item
|
||||
item.location = self
|
||||
self.event = item.advancement
|
||||
self.item.world = self.parent_region.world
|
||||
self.locked = True
|
||||
|
||||
def __repr__(self):
|
||||
@@ -1147,39 +1143,28 @@ class ItemClassification(IntFlag):
|
||||
|
||||
|
||||
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"
|
||||
type: str = None
|
||||
__slots__ = ("name", "classification", "code", "player", "location")
|
||||
name: str
|
||||
classification: ItemClassification
|
||||
|
||||
# need to find a decent place for these to live and to allow other games to register texts if they want.
|
||||
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
|
||||
|
||||
# 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
|
||||
code: Optional[int]
|
||||
"""an item with code None is called an Event, and does not get written to multidata"""
|
||||
player: int
|
||||
location: Optional[Location]
|
||||
|
||||
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
|
||||
self.name = name
|
||||
self.classification = classification
|
||||
self.player = player
|
||||
self.code = code
|
||||
self.location = None
|
||||
|
||||
@property
|
||||
def hint_text(self):
|
||||
def hint_text(self) -> str:
|
||||
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
|
||||
@property
|
||||
def pedestal_hint_text(self):
|
||||
def pedestal_hint_text(self) -> str:
|
||||
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
|
||||
|
||||
@property
|
||||
@@ -1205,7 +1190,7 @@ class Item:
|
||||
def __eq__(self, other):
|
||||
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:
|
||||
return other.player < self.player
|
||||
return self.name < other.name
|
||||
@@ -1213,11 +1198,13 @@ class Item:
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||
def __str__(self) -> str:
|
||||
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():
|
||||
@@ -1401,7 +1388,7 @@ class Spoiler():
|
||||
outfile.write('Game: %s\n' % self.world.game[player])
|
||||
for f_option, option in Options.per_game_common_options.items():
|
||||
write_option(f_option, option)
|
||||
options = self.world.worlds[player].options
|
||||
options = self.world.worlds[player].option_definitions
|
||||
if options:
|
||||
for f_option, option in options.items():
|
||||
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}')
|
||||
try:
|
||||
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)
|
||||
logger.info('Connected')
|
||||
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.")
|
||||
ctx.hint_cost = int(args['hint_cost'])
|
||||
ctx.check_points = int(args['location_check_points'])
|
||||
players = args.get("players", [])
|
||||
if len(players) < 1:
|
||||
logger.info('No player connected')
|
||||
else:
|
||||
players.sort()
|
||||
current_team = -1
|
||||
logger.info('Connected Players:')
|
||||
for network_player in players:
|
||||
if network_player.team != current_team:
|
||||
logger.info(f' Team #{network_player.team + 1}')
|
||||
current_team = network_player.team
|
||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||
|
||||
if "players" in args: # TODO remove when servers sending this are outdated
|
||||
players = args.get("players", [])
|
||||
if len(players) < 1:
|
||||
logger.info('No player connected')
|
||||
else:
|
||||
players.sort()
|
||||
current_team = -1
|
||||
logger.info('Connected Players:')
|
||||
for network_player in players:
|
||||
if network_player.team != current_team:
|
||||
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
|
||||
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
|
||||
|
||||
@@ -723,7 +727,7 @@ if __name__ == '__main__':
|
||||
class TextContext(CommonContext):
|
||||
tags = {"AP", "IgnoreGame", "TextOnly"}
|
||||
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):
|
||||
if password_requested and not self.password:
|
||||
|
||||
@@ -20,8 +20,7 @@ import Utils
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("FactorioClient", exception_logger="Client")
|
||||
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
|
||||
get_base_parser
|
||||
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser
|
||||
from MultiServer import mark_raw
|
||||
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)
|
||||
break
|
||||
else:
|
||||
logging.warning(
|
||||
f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.")
|
||||
raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. "
|
||||
f"Too many non-local items for too few remaining locations.")
|
||||
|
||||
world.random.shuffle(defaultlocations)
|
||||
|
||||
|
||||
56
Generate.py
56
Generate.py
@@ -7,7 +7,7 @@ import urllib.request
|
||||
import urllib.parse
|
||||
from typing import Set, Dict, Tuple, Callable, Any, Union
|
||||
import os
|
||||
from collections import Counter
|
||||
from collections import Counter, ChainMap
|
||||
import string
|
||||
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):
|
||||
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:
|
||||
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)}")
|
||||
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:
|
||||
raise Exception("Cannot mix --samesettings with --meta")
|
||||
else:
|
||||
@@ -164,7 +166,7 @@ def main(args=None, callback=ERmain):
|
||||
player_files[player_id] = filename
|
||||
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: "
|
||||
f"{args.plando}")
|
||||
|
||||
@@ -186,26 +188,28 @@ def main(args=None, callback=ERmain):
|
||||
erargs.enemizercli = args.enemizercli
|
||||
|
||||
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
|
||||
{fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
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)
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
|
||||
if meta_weights:
|
||||
for category_name, category_dict in meta_weights.items():
|
||||
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:
|
||||
for player, path in player_path_cache.items():
|
||||
for path in weights_cache:
|
||||
for yaml in weights_cache[path]:
|
||||
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:
|
||||
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
|
||||
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()
|
||||
erargs.player_settings = {}
|
||||
|
||||
@@ -387,6 +391,28 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
|
||||
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:
|
||||
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"]:
|
||||
@@ -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)))
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
for item in world.itempool:
|
||||
item.world = world
|
||||
|
||||
distribute_planned(world)
|
||||
|
||||
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))
|
||||
|
||||
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,
|
||||
compresslevel=9) as zf:
|
||||
for file in os.scandir(temp_dir):
|
||||
|
||||
171
MultiServer.py
171
MultiServer.py
@@ -30,13 +30,8 @@ except ImportError:
|
||||
OperationalError = ConnectionError
|
||||
|
||||
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
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
version_tuple, restricted_loads, Version
|
||||
from Utils import version_tuple, restricted_loads, Version
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||
SlotType
|
||||
|
||||
@@ -126,6 +121,11 @@ class Context:
|
||||
stored_data: typing.Dict[str, object]
|
||||
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,
|
||||
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,
|
||||
@@ -190,8 +190,43 @@ class Context:
|
||||
self.stored_data = {}
|
||||
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:
|
||||
if not endpoint.socket or not endpoint.socket.open:
|
||||
return False
|
||||
@@ -544,12 +579,12 @@ class Context:
|
||||
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
|
||||
f' has completed their goal.'
|
||||
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:
|
||||
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):
|
||||
@@ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client):
|
||||
'permissions': get_permissions(ctx),
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'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
|
||||
in network_data_package["games"].items()},
|
||||
in ctx.gamespackage.items()},
|
||||
'seed_name': ctx.seed_name,
|
||||
'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)
|
||||
|
||||
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
|
||||
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
|
||||
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
|
||||
team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
|
||||
ctx.player_names[(team, target_player)], ctx.location_names[location]))
|
||||
info_text = json_format_send_event(new_item, target_player)
|
||||
ctx.broadcast_team(team, [info_text])
|
||||
|
||||
@@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
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 = []
|
||||
slots: typing.Set[int] = {slot}
|
||||
for group_id, group in ctx.groups.items():
|
||||
if slot in group:
|
||||
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 location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
||||
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]:
|
||||
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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||
f"{lookup_any_item_id_to_name[hint.item]} is " \
|
||||
f"at {get_location_name_from_id(hint.location)} " \
|
||||
f"{ctx.item_names[hint.item]} is " \
|
||||
f"at {ctx.location_names[hint.location]} " \
|
||||
f"in {ctx.player_names[team, hint.finding_player]}'s World"
|
||||
|
||||
if hint.entrance:
|
||||
@@ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
forfeit_player(self.ctx, self.client.team, self.client.slot)
|
||||
return True
|
||||
elif "disabled" in self.ctx.forfeit_mode:
|
||||
self.output(
|
||||
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release")
|
||||
self.output("Sorry, client item releasing has been disabled on this server. "
|
||||
"You can ask the server admin for a /release")
|
||||
return False
|
||||
else: # is auto or 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":
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
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))
|
||||
else:
|
||||
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:
|
||||
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
|
||||
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))
|
||||
else:
|
||||
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)
|
||||
|
||||
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")
|
||||
self.ctx.notify_client_multiple(self.client, texts)
|
||||
else:
|
||||
@@ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
|
||||
|
||||
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")
|
||||
self.ctx.notify_client_multiple(self.client, texts)
|
||||
else:
|
||||
@@ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
def _cmd_getitem(self, item_name: str) -> bool:
|
||||
"""Cheat in an item, if it is enabled on this server"""
|
||||
if self.ctx.item_cheat:
|
||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||
item_name, usable, response = get_intended_text(item_name,
|
||||
world.item_names)
|
||||
names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot])
|
||||
item_name, usable, response = get_intended_text(
|
||||
item_name,
|
||||
names
|
||||
)
|
||||
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, True).append(new_item)
|
||||
self.ctx.notify_all(
|
||||
@@ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
f"You have {points_available} points.")
|
||||
return True
|
||||
else:
|
||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||
names = world.location_names if for_location else world.all_item_and_group_names
|
||||
game = self.ctx.games[self.client.slot]
|
||||
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,
|
||||
names)
|
||||
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.")
|
||||
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 = []
|
||||
for item in world.item_name_groups[hint_name]:
|
||||
if item in world.item_name_to_id: # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
||||
elif not for_location and hint_name in world.item_names: # item name
|
||||
for item_name in self.ctx.item_name_groups[game][hint_name]:
|
||||
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_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)
|
||||
else: # location 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
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item: str = "") -> bool:
|
||||
def _cmd_hint(self, item_name: str = "") -> bool:
|
||||
"""Use !hint {item_name},
|
||||
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,
|
||||
you can rerun the command to get more in that case."""
|
||||
return self.get_hints(item)
|
||||
return self.get_hints(item_name)
|
||||
|
||||
@mark_raw
|
||||
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":
|
||||
exclusions = args.get("exclusions", [])
|
||||
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", []))}
|
||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||
"data": {"games": games}}])
|
||||
# TODO: remove exclusions behaviour around 0.5.0
|
||||
elif 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}
|
||||
package = network_data_package.copy()
|
||||
package["games"] = games
|
||||
|
||||
package = {"games": games}
|
||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||
"data": package}])
|
||||
|
||||
else:
|
||||
await ctx.send_msgs(client, [{"cmd": "DataPackage",
|
||||
"data": network_data_package}])
|
||||
"data": {"games": ctx.gamespackage}}])
|
||||
|
||||
elif client.auth:
|
||||
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))
|
||||
hints = []
|
||||
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,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
|
||||
"original_cmd": cmd}])
|
||||
@@ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
item = " ".join(item_name)
|
||||
world = proxy_worlds[self.ctx.games[slot]]
|
||||
item, usable, response = get_intended_text(item, world.item_names)
|
||||
item_name = " ".join(item_name)
|
||||
names = self.ctx.item_names_for_game(self.ctx.games[slot])
|
||||
item_name, usable, response = get_intended_text(item_name, names)
|
||||
if usable:
|
||||
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_new_items(self.ctx)
|
||||
self.ctx.notify_all(
|
||||
'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
|
||||
else:
|
||||
self.output(response)
|
||||
@@ -1787,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
"""Sends an item to the specified player"""
|
||||
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"""
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
item = " ".join(item)
|
||||
world = proxy_worlds[self.ctx.games[slot]]
|
||||
item, usable, response = get_intended_text(item, world.all_item_and_group_names)
|
||||
item_name = " ".join(item_name)
|
||||
game = self.ctx.games[slot]
|
||||
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
|
||||
if usable:
|
||||
if item in world.item_name_groups:
|
||||
if item_name in self.ctx.item_name_groups[game]:
|
||||
hints = []
|
||||
for item in world.item_name_groups[item]:
|
||||
if item in world.item_name_to_id: # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item))
|
||||
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
|
||||
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_name_from_group))
|
||||
else: # item name
|
||||
hints = collect_hints(self.ctx, team, slot, item)
|
||||
hints = collect_hints(self.ctx, team, slot, item_name)
|
||||
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
@@ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
self.output(response)
|
||||
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"""
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
item = " ".join(location)
|
||||
world = proxy_worlds[self.ctx.games[slot]]
|
||||
item, usable, response = get_intended_text(item, world.location_names)
|
||||
location_name = " ".join(location_name)
|
||||
location_name, usable, response = get_intended_text(location_name,
|
||||
self.ctx.location_names_for_game(self.ctx.games[slot]))
|
||||
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:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
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,
|
||||
'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):
|
||||
|
||||
@@ -44,11 +44,38 @@ nest_asyncio.apply()
|
||||
class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
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:
|
||||
"""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."""
|
||||
self.ctx.missions_unlocked = True
|
||||
sc2_logger.info("Mission check has been disabled")
|
||||
return True
|
||||
|
||||
def _cmd_play(self, mission_id: str = "") -> bool:
|
||||
"""Start a Starcraft 2 mission"""
|
||||
@@ -64,6 +91,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
|
||||
else:
|
||||
sc2_logger.info(
|
||||
"Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -108,6 +136,7 @@ class SC2Context(CommonContext):
|
||||
missions_unlocked = False
|
||||
current_tooltip = None
|
||||
last_loc_list = None
|
||||
difficulty_override = -1
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -470,7 +499,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
game_state = 0
|
||||
if iteration == 0:
|
||||
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(
|
||||
difficulty,
|
||||
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
|
||||
|
||||
import shutil
|
||||
import typing
|
||||
import builtins
|
||||
import os
|
||||
@@ -12,12 +11,18 @@ import io
|
||||
import collections
|
||||
import importlib
|
||||
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:
|
||||
from tkinter import Tk
|
||||
else:
|
||||
Tk = typing.Any
|
||||
import tkinter
|
||||
import pathlib
|
||||
|
||||
|
||||
def tuplize_version(version: str) -> Version:
|
||||
@@ -33,18 +38,10 @@ class Version(typing.NamedTuple):
|
||||
__version__ = "0.3.4"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith('linux')
|
||||
is_macos = sys.platform == 'darwin'
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
is_macos = sys.platform == "darwin"
|
||||
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]:
|
||||
value = value & 0xFFFF
|
||||
@@ -125,17 +122,18 @@ def home_path(*path: str) -> str:
|
||||
|
||||
def user_path(*path: str) -> str:
|
||||
"""Returns either local_path or home_path based on write permissions."""
|
||||
if hasattr(user_path, 'cached_path'):
|
||||
if hasattr(user_path, "cached_path"):
|
||||
pass
|
||||
elif os.access(local_path(), os.W_OK):
|
||||
user_path.cached_path = local_path()
|
||||
else:
|
||||
user_path.cached_path = home_path()
|
||||
# populate home from local - TODO: upgrade feature
|
||||
if user_path.cached_path != local_path() and not os.path.exists(user_path('host.yaml')):
|
||||
for dn in ('Players', 'data/sprites'):
|
||||
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
|
||||
import shutil
|
||||
for dn in ("Players", "data/sprites"):
|
||||
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))
|
||||
|
||||
return os.path.join(user_path.cached_path, *path)
|
||||
@@ -150,11 +148,12 @@ def output_path(*path: str):
|
||||
return path
|
||||
|
||||
|
||||
def open_file(filename):
|
||||
if sys.platform == 'win32':
|
||||
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
|
||||
if is_windows:
|
||||
os.startfile(filename)
|
||||
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])
|
||||
|
||||
|
||||
@@ -173,7 +172,9 @@ class UniqueKeyLoader(SafeLoader):
|
||||
|
||||
parse_yaml = functools.partial(load, 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():
|
||||
@@ -191,11 +192,12 @@ def get_public_ipv4() -> str:
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
ctx = get_cert_none_ssl_context()
|
||||
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:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip()
|
||||
except:
|
||||
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip()
|
||||
except Exception:
|
||||
logging.exception(e)
|
||||
pass # we could be offline, in a local game, so no point in erroring out
|
||||
return ip
|
||||
@@ -208,7 +210,7 @@ def get_public_ipv6() -> str:
|
||||
ip = socket.gethostbyname(socket.gethostname())
|
||||
ctx = get_cert_none_ssl_context()
|
||||
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:
|
||||
logging.exception(e)
|
||||
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
|
||||
def get_options() -> dict:
|
||||
if not hasattr(get_options, "options"):
|
||||
filenames = ("options.yaml", "host.yaml")
|
||||
locations = []
|
||||
if os.path.join(os.getcwd()) != local_path():
|
||||
locations += filenames # use files from cwd only if it's not the local_path
|
||||
locations += [user_path(filename) for filename in filenames]
|
||||
filenames = ("options.yaml", "host.yaml")
|
||||
locations = []
|
||||
if os.path.join(os.getcwd()) != local_path():
|
||||
locations += filenames # use files from cwd only if it's not the local_path
|
||||
locations += [user_path(filename) for filename in filenames]
|
||||
|
||||
for location in locations:
|
||||
if os.path.exists(location):
|
||||
with open(location) as f:
|
||||
options = parse_yaml(f.read())
|
||||
for location in locations:
|
||||
if os.path.exists(location):
|
||||
with open(location) as f:
|
||||
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())
|
||||
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})')
|
||||
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
|
||||
|
||||
|
||||
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[key] = value
|
||||
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)
|
||||
if storage:
|
||||
return storage
|
||||
@@ -365,8 +353,8 @@ def persistent_load() -> typing.Dict[dict]:
|
||||
return storage
|
||||
|
||||
|
||||
def get_adjuster_settings(gameName: str):
|
||||
adjuster_settings = persistent_load().get("adjuster", {}).get(gameName, {})
|
||||
def get_adjuster_settings(game_name: str):
|
||||
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
|
||||
return adjuster_settings
|
||||
|
||||
|
||||
@@ -382,10 +370,10 @@ def get_unique_identifier():
|
||||
return uuid
|
||||
|
||||
|
||||
safe_builtins = {
|
||||
safe_builtins = frozenset((
|
||||
'set',
|
||||
'frozenset',
|
||||
}
|
||||
))
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
@@ -413,8 +401,7 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
if issubclass(obj, self.options_module.Option):
|
||||
return obj
|
||||
# Forbid everything else.
|
||||
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
|
||||
(module, name))
|
||||
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
||||
|
||||
|
||||
def restricted_loads(s):
|
||||
@@ -423,6 +410,9 @@ def restricted_loads(s):
|
||||
|
||||
|
||||
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):
|
||||
self[key] = value = self.default_factory(key)
|
||||
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)]
|
||||
|
||||
|
||||
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}
|
||||
|
||||
|
||||
@@ -493,11 +487,11 @@ def stream_input(stream, queue):
|
||||
return thread
|
||||
|
||||
|
||||
def tkinter_center_window(window: Tk):
|
||||
def tkinter_center_window(window: "tkinter.Tk") -> None:
|
||||
window.update()
|
||||
xPos = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
|
||||
yPos = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
|
||||
window.geometry("+{}+{}".format(xPos, yPos))
|
||||
x = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
|
||||
y = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
|
||||
window.geometry(f"+{x}+{y}")
|
||||
|
||||
|
||||
class VersionException(Exception):
|
||||
@@ -514,24 +508,27 @@ def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
|
||||
|
||||
|
||||
# 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"""
|
||||
import decimal
|
||||
n = 0
|
||||
value = decimal.Decimal(value)
|
||||
while value >= power:
|
||||
limit = power - decimal.Decimal("0.005")
|
||||
while value >= limit:
|
||||
value /= power
|
||||
n += 1
|
||||
|
||||
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) \
|
||||
-> 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)
|
||||
return list(
|
||||
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]]]) \
|
||||
-> typing.Optional[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:
|
||||
# prefer native dialog
|
||||
kdialog = shutil.which('kdialog')
|
||||
from shutil import which
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
||||
return run(kdialog, f'--title={title}', '--getopenfilename', '.', k_filters)
|
||||
zenity = shutil.which('zenity')
|
||||
return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
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
|
||||
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 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():
|
||||
if 'kivy' in sys.modules:
|
||||
if "kivy" in sys.modules:
|
||||
from kivy.app import App
|
||||
return App.get_running_app() is not None
|
||||
return False
|
||||
@@ -591,14 +589,15 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
MessageBox(title, text, error).open()
|
||||
return
|
||||
|
||||
if is_linux and not 'tkinter' in sys.modules:
|
||||
if is_linux and "tkinter" not in sys.modules:
|
||||
# prefer native dialog
|
||||
kdialog = shutil.which('kdialog')
|
||||
from shutil import which
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
return run(kdialog, f'--title={title}', '--error' if error else '--msgbox', text)
|
||||
zenity = shutil.which('zenity')
|
||||
return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
|
||||
zenity = which("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
|
||||
try:
|
||||
@@ -613,3 +612,14 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
root.withdraw()
|
||||
showerror(title, text) if error else showinfo(title, text)
|
||||
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__)
|
||||
|
||||
from WebHostLib import app as raw_app
|
||||
from WebHostLib import register, app as raw_app
|
||||
from waitress import serve
|
||||
|
||||
from WebHostLib.models import db
|
||||
@@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen
|
||||
from WebHostLib.lttpsprites import update_sprites_lttp
|
||||
from WebHostLib.options import create as create_options_files
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||
|
||||
|
||||
def get_app():
|
||||
register()
|
||||
app = raw_app
|
||||
if os.path.exists(configpath):
|
||||
import yaml
|
||||
@@ -43,19 +42,39 @@ def get_app():
|
||||
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
zfile: zipfile.ZipInfo
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
worlds = {}
|
||||
data = []
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'):
|
||||
worlds[game] = world
|
||||
|
||||
base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs")
|
||||
for game, world in worlds.items():
|
||||
# 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 = Utils.local_path("WebHostLib", "static", "generated", "docs", game)
|
||||
files = os.listdir(source_path)
|
||||
for file in files:
|
||||
os.makedirs(os.path.dirname(Utils.local_path(target_path, file)), exist_ok=True)
|
||||
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
|
||||
target_path = os.path.join(base_target_path, game)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
|
||||
if world.zip_path:
|
||||
zipfile_path = world.zip_path
|
||||
|
||||
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
|
||||
game_data = {'gameTitle': game, '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:
|
||||
if 'Archipelago' in games['gameTitle']:
|
||||
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)
|
||||
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 socket
|
||||
|
||||
import jinja2.exceptions
|
||||
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_compress import Compress
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
from Utils import title_sorted
|
||||
from .models import *
|
||||
|
||||
UPLOAD_FOLDER = os.path.relpath('uploads')
|
||||
@@ -53,8 +53,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg"
|
||||
cache = Cache(app)
|
||||
Compress(app)
|
||||
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
|
||||
class B64UUIDConverter(BaseConverter):
|
||||
|
||||
@@ -68,174 +66,18 @@ class B64UUIDConverter(BaseConverter):
|
||||
# short UUID
|
||||
app.url_map.converters["suuid"] = B64UUIDConverter
|
||||
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
|
||||
|
||||
# has automatic patch integration
|
||||
import Patch
|
||||
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
|
||||
app.jinja_env.filters["title_sorted"] = title_sorted
|
||||
|
||||
|
||||
def get_world_theme(game_name: str):
|
||||
if game_name in AutoWorldRegister.world_types:
|
||||
return AutoWorldRegister.world_types[game_name].web.theme
|
||||
return 'grass'
|
||||
def register():
|
||||
"""Import submodules, triggering their registering on flask routing.
|
||||
Note: initializes worlds subsystem."""
|
||||
# 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
|
||||
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)
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
|
||||
@@ -32,14 +32,14 @@ def room_info(room: UUID):
|
||||
|
||||
@api_endpoints.route('/datapackage')
|
||||
@cache.cached()
|
||||
def get_datapackge():
|
||||
def get_datapackage():
|
||||
from worlds import network_data_package
|
||||
return network_data_package
|
||||
|
||||
|
||||
@api_endpoints.route('/datapackage_version')
|
||||
@cache.cached()
|
||||
def get_datapackge_versions():
|
||||
def get_datapackage_versions():
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
|
||||
version_package["version"] = network_data_package["version"]
|
||||
|
||||
@@ -184,7 +184,7 @@ class MultiworldInstance():
|
||||
|
||||
logging.info(f"Spinning up {self.room_id}")
|
||||
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")
|
||||
process.start()
|
||||
# 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 .customserver import run_server_process
|
||||
from .customserver import run_server_process, get_static_server_data
|
||||
from .generate import gen_game
|
||||
|
||||
@@ -9,12 +9,13 @@ import time
|
||||
import random
|
||||
import pickle
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
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 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):
|
||||
@@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
import MultiServer
|
||||
|
||||
MultiServer.client_message_processor = CustomClientMessageProcessor
|
||||
del (MultiServer)
|
||||
del MultiServer
|
||||
|
||||
|
||||
class DBCommandProcessor(ServerCommandProcessor):
|
||||
@@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor):
|
||||
|
||||
|
||||
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)
|
||||
del self.static_server_data
|
||||
self.main_loop = asyncio.get_running_loop()
|
||||
self.video = {}
|
||||
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):
|
||||
cmdprocessor = DBCommandProcessor(self)
|
||||
|
||||
@@ -107,14 +116,32 @@ def get_random_port():
|
||||
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
|
||||
db.bind(**ponyconfig)
|
||||
db.generate_mapping(check_tables=False)
|
||||
|
||||
async def main():
|
||||
Utils.init_logging(str(room_id), write_mode="a")
|
||||
ctx = WebHostContext()
|
||||
ctx = WebHostContext(static_server_data)
|
||||
ctx.load(room_id)
|
||||
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)}" \
|
||||
f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}"
|
||||
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:
|
||||
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
|
||||
patch_data = BytesIO(patch_data)
|
||||
|
||||
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
|
||||
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>")
|
||||
@@ -66,7 +66,7 @@ def download_slot_file(room_id, player_id: int):
|
||||
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"
|
||||
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":
|
||||
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
|
||||
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"
|
||||
else:
|
||||
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")
|
||||
|
||||
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():
|
||||
|
||||
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(
|
||||
options=all_options,
|
||||
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
|
||||
@@ -110,7 +110,7 @@ def create():
|
||||
if option.default == "random":
|
||||
this_option["defaultValue"] = "random"
|
||||
|
||||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
||||
elif issubclass(option, Options.Range):
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
|
||||
@@ -121,7 +121,7 @@ def create():
|
||||
"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]["value_names"] = {}
|
||||
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!",
|
||||
}
|
||||
|
||||
elif hasattr(option, "valid_keys"):
|
||||
elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
|
||||
if option.valid_keys:
|
||||
game_options[option_name] = {
|
||||
"type": "custom-list",
|
||||
|
||||
@@ -18,15 +18,16 @@ from .models import Room
|
||||
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)
|
||||
total_games = Counter()
|
||||
cutoff = date.today()-timedelta(days=30)
|
||||
room: Room
|
||||
for room in select(room for room in Room if room.creation_time >= cutoff):
|
||||
for slot in room.seed.slots:
|
||||
total_games[slot.game] += 1
|
||||
games_played[room.creation_time.date()][slot.game] += 1
|
||||
if slot.game in known_games:
|
||||
total_games[slot.game] += 1
|
||||
games_played[room.creation_time.date()][slot.game] += 1
|
||||
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')
|
||||
@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty
|
||||
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",
|
||||
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)
|
||||
|
||||
color_palette = get_color_palette(len(total_games))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% import "macros.html" as macros %}
|
||||
{% block head %}
|
||||
<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") }}"/>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="games" class="markdown">
|
||||
<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>
|
||||
<p>
|
||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||
|
||||
@@ -11,7 +11,7 @@ from worlds.alttp import Items
|
||||
from WebHostLib import app, cache, Room
|
||||
from Utils import restricted_loads
|
||||
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
|
||||
|
||||
alttp_icons = {
|
||||
@@ -987,10 +987,10 @@ def getTracker(tracker: UUID):
|
||||
if game_state == 30:
|
||||
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_small_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)}
|
||||
for loc_data in locations.values():
|
||||
for values in loc_data.values():
|
||||
for values in loc_data.values():
|
||||
item_id, item_player, flags = values
|
||||
|
||||
if item_id in ids_big_key:
|
||||
@@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID):
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
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,
|
||||
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,
|
||||
|
||||
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
|
||||
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
|
||||
%% Games or clients which compile to native or which the client is integrated in the game.
|
||||
subgraph "Native"
|
||||
@@ -82,10 +88,12 @@ flowchart LR
|
||||
MT[Meritous]
|
||||
TW[The Witness]
|
||||
SA2B[Sonic Adventure 2: Battle]
|
||||
DS3[Dark Souls 3]
|
||||
|
||||
APCLIENTPP <--> SOE
|
||||
APCLIENTPP <--> MT
|
||||
APCLIENTPP <-- The Witness Randomizer --> TW
|
||||
APCLIENTPP <--> DS3
|
||||
APCPP <--> SM64
|
||||
APCPP <--> V6
|
||||
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
|
||||
* yellow_bg
|
||||
* blue_bg
|
||||
* purple_bg
|
||||
* magenta_bg
|
||||
* cyan_bg
|
||||
* white_bg
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ inside a World object.
|
||||
|
||||
Players provide customized settings for their World in the form of yamls.
|
||||
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.
|
||||
|
||||
### World Options
|
||||
@@ -252,7 +252,7 @@ to describe it and a `display_name` property for display on the website and in
|
||||
spoiler logs.
|
||||
|
||||
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`.
|
||||
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):
|
||||
#...
|
||||
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):
|
||||
"""Insert description of the world/game here."""
|
||||
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
|
||||
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
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestDungeon(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
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)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
|
||||
@@ -49,8 +49,7 @@ class PlayerDefinition(object):
|
||||
region_name = "player" + str(self.id) + region_tag
|
||||
region = Region("player" + str(self.id) + region_tag, RegionType.Generic,
|
||||
"Region Hint", self.id, self.world)
|
||||
self.locations += generate_locations(size,
|
||||
self.id, None, region, region_tag)
|
||||
self.locations += generate_locations(size, self.id, None, region, region_tag)
|
||||
|
||||
entrance = Entrance(self.id, region_name + "_entrance", parent)
|
||||
parent.exits.append(entrance)
|
||||
|
||||
@@ -12,7 +12,7 @@ def setup_default_world(world_type) -> MultiWorld:
|
||||
world.player_name = {1: "Tester"}
|
||||
world.set_seed()
|
||||
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)})
|
||||
world.set_options(args)
|
||||
world.set_default_common_options()
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestInverted(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
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)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestInvertedBombRules(unittest.TestCase):
|
||||
self.world = MultiWorld(1)
|
||||
self.world.mode[1] = "inverted"
|
||||
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)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestInvertedMinor(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
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)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
|
||||
@@ -18,7 +18,7 @@ class TestInvertedOWG(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
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)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
|
||||
@@ -17,7 +17,7 @@ class TestMinor(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
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)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
|
||||
@@ -18,7 +18,7 @@ class TestVanillaOWG(TestBase):
|
||||
def setUp(self):
|
||||
self.world = MultiWorld(1)
|
||||
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)})
|
||||
self.world.set_options(args)
|
||||
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):
|
||||
self.world = MultiWorld(1)
|
||||
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)})
|
||||
self.world.set_options(args)
|
||||
self.world.set_default_common_options()
|
||||
|
||||
@@ -2,10 +2,14 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
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 BaseClasses import CollectionState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import MultiWorld, Item, Location, Tutorial
|
||||
|
||||
|
||||
class AutoWorldRegister(type):
|
||||
@@ -41,14 +45,18 @@ class AutoWorldRegister(type):
|
||||
# construct class
|
||||
new_class = super().__new__(mcs, name, bases, 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
|
||||
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
|
||||
|
||||
|
||||
class AutoLogicRegister(type):
|
||||
def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister:
|
||||
new_class = super().__new__(cls, name, bases, dct)
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister:
|
||||
new_class = super().__new__(mcs, name, bases, dct)
|
||||
function: Callable[..., Any]
|
||||
for item_name, function in dct.items():
|
||||
if item_name == "copy_mixin":
|
||||
@@ -62,12 +70,12 @@ class AutoLogicRegister(type):
|
||||
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)
|
||||
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()
|
||||
for player in world.player_ids:
|
||||
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)
|
||||
|
||||
|
||||
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}
|
||||
for world_type in world_types:
|
||||
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
|
||||
# class is to be used for one guide.
|
||||
tutorials: List[Tutorial]
|
||||
tutorials: List["Tutorial"]
|
||||
|
||||
# Choose a theme for your /game/* pages
|
||||
# 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 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
|
||||
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.
|
||||
hidden: bool = False
|
||||
|
||||
# see WebWorld for options
|
||||
web: WebWorld = WebWorld()
|
||||
|
||||
# autoset on creation:
|
||||
world: MultiWorld
|
||||
world: "MultiWorld"
|
||||
player: int
|
||||
|
||||
# automatically generated
|
||||
@@ -170,9 +181,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
item_names: Set[str] # set of all potential item 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.player = player
|
||||
|
||||
@@ -207,12 +219,12 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
@classmethod
|
||||
def fill_hook(cls,
|
||||
progitempool: List[Item],
|
||||
nonexcludeditempool: List[Item],
|
||||
localrestitempool: Dict[int, List[Item]],
|
||||
nonlocalrestitempool: Dict[int, List[Item]],
|
||||
restitempool: List[Item],
|
||||
fill_locations: List[Location]) -> None:
|
||||
progitempool: List["Item"],
|
||||
nonexcludeditempool: List["Item"],
|
||||
localrestitempool: Dict[int, List["Item"]],
|
||||
nonlocalrestitempool: Dict[int, List["Item"]],
|
||||
restitempool: List["Item"],
|
||||
fill_locations: List["Location"]) -> None:
|
||||
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
||||
This gets called once per present world type."""
|
||||
pass
|
||||
@@ -250,7 +262,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
|
||||
# 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.
|
||||
Warning: this may be called with self.world = None, for example by MultiServer"""
|
||||
raise NotImplementedError
|
||||
@@ -261,7 +273,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
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
|
||||
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 None to skip item.
|
||||
:param state: CollectionState to collect into
|
||||
@@ -272,18 +284,18 @@ class World(metaclass=AutoWorldRegister):
|
||||
return None
|
||||
|
||||
# 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 []
|
||||
|
||||
# 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)
|
||||
if name:
|
||||
state.prog_items[name, self.player] += 1
|
||||
return True
|
||||
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)
|
||||
if name:
|
||||
state.prog_items[name, self.player] -= 1
|
||||
@@ -292,7 +304,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
return True
|
||||
return False
|
||||
|
||||
def create_filler(self) -> Item:
|
||||
def create_filler(self) -> "Item":
|
||||
return self.create_item(self.get_filler_item_name())
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +1,56 @@
|
||||
import importlib
|
||||
import zipimport
|
||||
import os
|
||||
import typing
|
||||
|
||||
__all__ = {"lookup_any_item_id_to_name",
|
||||
"lookup_any_location_id_to_name",
|
||||
"network_data_package",
|
||||
"AutoWorldRegister"}
|
||||
folder = os.path.dirname(__file__)
|
||||
|
||||
__all__ = {
|
||||
"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
|
||||
world_folders = []
|
||||
for file in os.scandir(os.path.dirname(__file__)):
|
||||
if file.is_dir():
|
||||
world_folders.append(file.name)
|
||||
world_folders.sort()
|
||||
for world in world_folders:
|
||||
if not world.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
|
||||
importlib.import_module(f".{world}", "worlds")
|
||||
world_sources.sort()
|
||||
for world_source in world_sources:
|
||||
if world_source.is_zip:
|
||||
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
||||
importer.load_module(world_source.path.split(".", 1)[0])
|
||||
else:
|
||||
importlib.import_module(f".{world_source.path}", "worlds")
|
||||
|
||||
from .AutoWorld import AutoWorldRegister
|
||||
lookup_any_item_id_to_name = {}
|
||||
lookup_any_location_id_to_name = {}
|
||||
games = {}
|
||||
|
||||
from .AutoWorld import AutoWorldRegister
|
||||
|
||||
for world_name, world in AutoWorldRegister.world_types.items():
|
||||
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,
|
||||
"version": world.data_version,
|
||||
# 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()):
|
||||
network_data_package["version"] = 0
|
||||
import logging
|
||||
|
||||
logging.warning(f"Datapackage is in custom mode. Custom Worlds: "
|
||||
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)
|
||||
for item in dungeon.all_items:
|
||||
item.dungeon = dungeon
|
||||
item.world = world
|
||||
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
|
||||
for region in dungeon.regions:
|
||||
world.get_region(region, player).dungeon = dungeon
|
||||
|
||||
@@ -51,6 +51,11 @@ class ItemData(typing.NamedTuple):
|
||||
flute_boy_credit: 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)
|
||||
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'),
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
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 = {
|
||||
"Golden Sword": ("Progressive Sword", 4),
|
||||
|
||||
@@ -2091,7 +2091,9 @@ def write_string_to_rom(rom, target, string):
|
||||
|
||||
|
||||
def write_strings(rom, world, player):
|
||||
from . import ALTTPWorld
|
||||
local_random = world.slot_seeds[player]
|
||||
w: ALTTPWorld = world.worlds[player]
|
||||
|
||||
tt = TextTable()
|
||||
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,
|
||||
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
|
||||
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
|
||||
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()
|
||||
|
||||
sickkiditem = world.get_location('Sick Kid', player).item
|
||||
sickkiditem_text = local_random.choice(
|
||||
SickKid_texts) if sickkiditem is None or sickkiditem.sickkid_credit_text is None else sickkiditem.sickkid_credit_text
|
||||
sickkiditem_text = local_random.choice(SickKid_texts) \
|
||||
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_text = local_random.choice(
|
||||
Zora_texts) if zoraitem is None or zoraitem.zora_credit_text is None else zoraitem.zora_credit_text
|
||||
zoraitem_text = local_random.choice(Zora_texts) \
|
||||
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_text = local_random.choice(
|
||||
MagicShop_texts) if magicshopitem is None or magicshopitem.magicshop_credit_text is None else magicshopitem.magicshop_credit_text
|
||||
magicshopitem_text = local_random.choice(MagicShop_texts) \
|
||||
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_text = local_random.choice(
|
||||
FluteBoy_texts) if fluteboyitem is None or fluteboyitem.fluteboy_credit_text is None else fluteboyitem.fluteboy_credit_text
|
||||
fluteboyitem_text = local_random.choice(FluteBoy_texts) \
|
||||
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('sanctuary', 0, local_random.choice(Sanctuary_texts))
|
||||
|
||||
@@ -935,7 +935,6 @@ def set_trock_key_rules(world, player):
|
||||
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
|
||||
item = ItemFactory('Small Key (Turtle Rock)', player)
|
||||
item.world = world
|
||||
location = world.get_location('Turtle Rock - Big Key Chest', player)
|
||||
location.place_locked_item(item)
|
||||
location.event = True
|
||||
|
||||
@@ -207,10 +207,10 @@ def ShopSlotFill(world):
|
||||
shops_per_sphere.append(current_shops_slots)
|
||||
candidates_per_sphere.append(current_candidates)
|
||||
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:
|
||||
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)
|
||||
if cumu_weights:
|
||||
x = cumu_weights[-1]
|
||||
@@ -335,7 +335,6 @@ def create_shops(world, player: int):
|
||||
else:
|
||||
loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
|
||||
loc.shop_slot_disabled = True
|
||||
loc.item.world = world
|
||||
shop.region.locations.append(loc)
|
||||
world.clear_location_cache()
|
||||
|
||||
|
||||
@@ -6,31 +6,33 @@ from BaseClasses import Location, Item, ItemClassification
|
||||
|
||||
class ALttPLocation(Location):
|
||||
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,
|
||||
hint_text: Optional[str] = None, parent=None,
|
||||
player_address=None):
|
||||
def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False,
|
||||
hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None):
|
||||
super(ALttPLocation, self).__init__(player, name, address, parent)
|
||||
self.crystal = crystal
|
||||
self.player_address = player_address
|
||||
self._hint_text: str = hint_text
|
||||
self._hint_text = hint_text
|
||||
|
||||
|
||||
class ALttPItem(Item):
|
||||
game: str = "A Link to the Past"
|
||||
type: Optional[str]
|
||||
_pedestal_hint_text: Optional[str]
|
||||
_hint_text: Optional[str]
|
||||
dungeon = None
|
||||
|
||||
def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None, pedestal_hint=None,
|
||||
pedestal_credit=None, sick_kid_credit=None, zora_credit=None, witch_credit=None,
|
||||
flute_boy_credit=None, hint_text=None):
|
||||
def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None,
|
||||
pedestal_hint=None, hint_text=None):
|
||||
super(ALttPItem, self).__init__(name, classification, item_code, player)
|
||||
self.type = type
|
||||
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
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import random
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
import typing
|
||||
|
||||
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 ..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")
|
||||
|
||||
@@ -110,7 +107,7 @@ class ALTTPWorld(World):
|
||||
Ganon!
|
||||
"""
|
||||
game: str = "A Link to the Past"
|
||||
options = alttp_options
|
||||
option_definitions = alttp_options
|
||||
topology_present = True
|
||||
item_name_groups = item_name_groups
|
||||
hint_blacklist = {"Triforce"}
|
||||
@@ -124,6 +121,17 @@ class ALTTPWorld(World):
|
||||
required_client_version = (0, 3, 2)
|
||||
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
|
||||
|
||||
create_items = generate_itempool
|
||||
@@ -145,6 +153,9 @@ class ALTTPWorld(World):
|
||||
player = self.player
|
||||
world = self.world
|
||||
|
||||
if self.use_enemizer():
|
||||
check_enemizer(world.enemizer)
|
||||
|
||||
# system for sharing ER layouts
|
||||
self.er_seed = str(world.random.randint(0, 2 ** 64))
|
||||
|
||||
@@ -330,14 +341,19 @@ class ALTTPWorld(World):
|
||||
def stage_post_fill(cls, 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):
|
||||
world = self.world
|
||||
player = self.player
|
||||
try:
|
||||
use_enemizer = (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])
|
||||
use_enemizer = self.use_enemizer()
|
||||
|
||||
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]]
|
||||
|
||||
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
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
|
||||
@@ -47,13 +47,12 @@ class ArchipIDLEWorld(World):
|
||||
|
||||
item_pool = []
|
||||
for i in range(100):
|
||||
item = Item(
|
||||
item = ArchipIDLEItem(
|
||||
item_table_copy[i],
|
||||
ItemClassification.progression if i < 20 else ItemClassification.filler,
|
||||
self.item_name_to_id[item_table_copy[i]],
|
||||
self.player
|
||||
)
|
||||
item.game = 'ArchipIDLE'
|
||||
item_pool.append(item)
|
||||
|
||||
self.world.itempool += item_pool
|
||||
@@ -93,6 +92,10 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi
|
||||
return region
|
||||
|
||||
|
||||
class ArchipIDLEItem(Item):
|
||||
game = "ArchipIDLE"
|
||||
|
||||
|
||||
class ArchipIDLELocation(Location):
|
||||
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!
|
||||
"""
|
||||
game: str = "ChecksFinder"
|
||||
options = checksfinder_options
|
||||
option_definitions = checksfinder_options
|
||||
topology_present = True
|
||||
web = ChecksFinderWeb()
|
||||
|
||||
|
||||
@@ -16,14 +16,26 @@ from ..generic.Rules import set_rule
|
||||
|
||||
|
||||
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",
|
||||
"A guide to setting up the Archipelago Dark Souls III randomizer on your computer.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["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):
|
||||
@@ -34,7 +46,7 @@ class DarkSouls3World(World):
|
||||
"""
|
||||
|
||||
game: str = "Dark Souls III"
|
||||
options = dark_souls_options
|
||||
option_definitions = dark_souls_options
|
||||
topology_present: bool = True
|
||||
remote_items: 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
|
||||
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:
|
||||
for name, address in location_table.items():
|
||||
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.
|
||||
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
|
||||
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?
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Required Software
|
||||
|
||||
- [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
|
||||
|
||||
@@ -14,22 +14,24 @@ The randomization is performed by the AP.json file, an output file generated by
|
||||
|
||||
## 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).
|
||||
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" ):
|
||||
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 (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game"):
|
||||
- **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
|
||||
|
||||
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
|
||||
4. You can quit and launch at anytime during a game
|
||||
|
||||
## Where do I get a config file?
|
||||
|
||||
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.
|
||||
"""
|
||||
game: str = "Donkey Kong Country 3"
|
||||
options = dkc3_options
|
||||
option_definitions = dkc3_options
|
||||
topology_present = False
|
||||
data_version = 1
|
||||
#hint_blacklist = {LocationName.rocket_rush_flag}
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
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`
|
||||
|
||||
## 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
|
||||
|
||||
### 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.
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
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
|
||||
with template_load_lock:
|
||||
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] = \
|
||||
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
|
||||
jinja2.Environment(loader=jinja2.FunctionLoader(load_template))
|
||||
|
||||
data_template = template_env.get_template("data.lua")
|
||||
data_final_template = template_env.get_template("data-final-fixes.lua")
|
||||
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__)
|
||||
en_locale_dir = os.path.join(mod_dir, "locale", "en")
|
||||
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:
|
||||
f.write(data_template_code)
|
||||
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 collections import deque
|
||||
|
||||
from worlds.factorio.Options import TechTreeLayout
|
||||
from .Options import TechTreeLayout
|
||||
|
||||
funnel_layers = {TechTreeLayout.option_small_funnels: 3,
|
||||
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]]:
|
||||
with open(os.path.join(source_folder, f"{data_name}.json")) as f:
|
||||
return json.load(f)
|
||||
import pkgutil
|
||||
return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())
|
||||
|
||||
|
||||
techs_future = pool.submit(load_json_data, "techs")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import collections
|
||||
import typing
|
||||
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
|
||||
from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification
|
||||
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)
|
||||
|
||||
options = factorio_options
|
||||
option_definitions = factorio_options
|
||||
|
||||
@classmethod
|
||||
def stage_write_spoiler(cls, world, spoiler_handle):
|
||||
|
||||
@@ -286,6 +286,12 @@ end)
|
||||
-- hook into researches done
|
||||
script.on_event(defines.events.on_research_finished, function(event)
|
||||
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
|
||||
-- check if it came from the server anyway, then we don't need to double send.
|
||||
dumpInfo(technology.force) --is sendable
|
||||
|
||||
@@ -211,8 +211,8 @@ copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0]
|
||||
{%- endif -%}
|
||||
{#- connect Technology #}
|
||||
{%- if original_tech_name in tech_tree_layout_prerequisites %}
|
||||
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %}
|
||||
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-")
|
||||
{%- for prerequisite in tech_tree_layout_prerequisites[original_tech_name] %}
|
||||
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequisite] }}-")
|
||||
{% endfor %}
|
||||
{% endif -%}
|
||||
{#- 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.
|
||||
"""
|
||||
|
||||
options = ff1_options
|
||||
option_definitions = ff1_options
|
||||
game = "Final Fantasy"
|
||||
topology_present = False
|
||||
remote_items = True
|
||||
@@ -54,6 +54,7 @@ class FF1World(World):
|
||||
locations = get_options(self.world, 'locations', self.player)
|
||||
rules = get_options(self.world, 'rules', self.player)
|
||||
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_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
|
||||
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
|
||||
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
|
||||
by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss
|
||||
plando.
|
||||
by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary.
|
||||
|
||||
### Enabling Plando
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ option_docstrings = {
|
||||
"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."
|
||||
"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"
|
||||
" 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 "
|
||||
|
||||
@@ -142,7 +142,7 @@ class HKWorld(World):
|
||||
As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils.
|
||||
""" # from https://www.hollowknight.com
|
||||
game: str = "Hollow Knight"
|
||||
options = hollow_knight_options
|
||||
option_definitions = hollow_knight_options
|
||||
|
||||
web = HKWeb()
|
||||
|
||||
@@ -435,7 +435,7 @@ class HKWorld(World):
|
||||
slot_data = {}
|
||||
|
||||
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]
|
||||
try:
|
||||
optionvalue = int(option.value)
|
||||
@@ -632,8 +632,9 @@ class HKLocation(Location):
|
||||
|
||||
class HKItem(Item):
|
||||
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":
|
||||
classification = ItemClassification.trap
|
||||
elif type in ("Grub", "DreamWarrior", "Root", "Egg"):
|
||||
|
||||
@@ -6,14 +6,9 @@
|
||||
import typing
|
||||
|
||||
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):
|
||||
pedestal: typing.Optional[str]
|
||||
sickkid: typing.Optional[str]
|
||||
@@ -143,6 +138,7 @@ LttPCreditsText = {
|
||||
|
||||
class MeritousItem(Item):
|
||||
game: str = "Meritous"
|
||||
type: str
|
||||
|
||||
def __init__(self, name, advancement, code, player):
|
||||
super(MeritousItem, self).__init__(name,
|
||||
@@ -171,14 +167,6 @@ class MeritousItem(Item):
|
||||
self.type = "Artifact"
|
||||
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
|
||||
|
||||
@@ -217,3 +205,10 @@ item_groups = {
|
||||
"Important Artifacts": ["Shield Boost", "Circuit Booster", "Metabolism", "Dodge Enhancer"],
|
||||
"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
|
||||
|
||||
data_version = 2
|
||||
forced_auto_forfeit = False
|
||||
|
||||
# NOTE: Remember to change this before this game goes live
|
||||
required_client_version = (0, 2, 4)
|
||||
|
||||
options = meritous_options
|
||||
option_definitions = meritous_options
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super(MeritousWorld, self).__init__(world, player)
|
||||
|
||||
@@ -58,7 +58,7 @@ class MinecraftWorld(World):
|
||||
victory!
|
||||
"""
|
||||
game: str = "Minecraft"
|
||||
options = minecraft_options
|
||||
option_definitions = minecraft_options
|
||||
topology_present = True
|
||||
web = MinecraftWebWorld()
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ leave this window open as this is your server console.
|
||||
|
||||
### 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`
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
from BaseClasses import LocationProgressType
|
||||
from .Items import OOTItem
|
||||
|
||||
# Abbreviations
|
||||
# DMC Death Mountain Crater
|
||||
@@ -1260,7 +1261,7 @@ def hintExclusions(world, clear_cache=False):
|
||||
world.hint_exclusions = []
|
||||
|
||||
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_location_names = [
|
||||
|
||||
@@ -10,6 +10,7 @@ from urllib.error import URLError, HTTPError
|
||||
import json
|
||||
from enum import Enum
|
||||
|
||||
from .Items import OOTItem
|
||||
from .HintList import getHint, getHintGroup, Hint, hintExclusions
|
||||
from .Messages import COLOR_MAP, update_message_by_id
|
||||
from .TextBox import line_wrap
|
||||
@@ -480,7 +481,7 @@ def get_specific_item_hint(world, checked):
|
||||
def get_random_location_hint(world, checked):
|
||||
locations = list(filter(lambda location:
|
||||
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.locked
|
||||
and location.name not in world.hint_exclusions
|
||||
|
||||
@@ -24,6 +24,7 @@ def ap_id_to_oot_data(ap_id):
|
||||
|
||||
class OOTItem(Item):
|
||||
game: str = "Ocarina of Time"
|
||||
type: str
|
||||
|
||||
def __init__(self, name, player, data, event, force_not_advancement):
|
||||
(type, advancement, index, special) = data
|
||||
@@ -38,7 +39,6 @@ class OOTItem(Item):
|
||||
classification = ItemClassification.progression
|
||||
else:
|
||||
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)
|
||||
self.type = type
|
||||
self.index = index
|
||||
@@ -46,25 +46,12 @@ class OOTItem(Item):
|
||||
self.looks_like_item = None
|
||||
self.price = special.get('price', None) if special else None
|
||||
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
|
||||
def dungeonitem(self) -> bool:
|
||||
return self.type in ['SmallKey', 'HideoutSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass']
|
||||
|
||||
|
||||
|
||||
# Progressive: True -> Advancement
|
||||
# False -> Priority
|
||||
# None -> Normal
|
||||
|
||||
@@ -5,6 +5,7 @@ import zlib
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
|
||||
from .Items import OOTItem
|
||||
from .LocationList import business_scrubs
|
||||
from .Hints import writeGossipStoneHints, buildAltarHints, \
|
||||
buildGanonText, getSimpleHintNoPrefix
|
||||
@@ -1881,9 +1882,9 @@ def get_override_entry(player_id, location):
|
||||
type = 2
|
||||
elif location.type == 'GS Token':
|
||||
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
|
||||
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
|
||||
elif location.type in ['Song', 'Cutscene']:
|
||||
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
|
||||
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'])
|
||||
rom.write_int16(location.address1, location.item.index)
|
||||
else:
|
||||
|
||||
@@ -95,7 +95,7 @@ class OOTWorld(World):
|
||||
to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
|
||||
"""
|
||||
game: str = "Ocarina of Time"
|
||||
options: dict = oot_options
|
||||
option_definitions: dict = oot_options
|
||||
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
|
||||
data[2] is not None}
|
||||
|
||||
@@ -17,7 +17,7 @@ class OriBlindForest(World):
|
||||
item_name_to_id = item_table
|
||||
location_name_to_id = lookup_name_to_id
|
||||
|
||||
options = options
|
||||
option_definitions = options
|
||||
|
||||
hidden = True
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class RaftWorld(World):
|
||||
lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values()))
|
||||
|
||||
location_name_to_id = locations_lookup_name_to_id
|
||||
options = raft_options
|
||||
option_definitions = raft_options
|
||||
|
||||
data_version = 2
|
||||
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.
|
||||
"""
|
||||
game: str = "Rogue Legacy"
|
||||
options = legacy_options
|
||||
option_definitions = legacy_options
|
||||
topology_present = False
|
||||
data_version = 3
|
||||
required_client_version = (0, 2, 3)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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):
|
||||
|
||||
@@ -5,7 +5,7 @@ from .Rules import set_rules
|
||||
|
||||
from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from .Options import ror2_options
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
|
||||
client_version = 1
|
||||
|
||||
@@ -28,7 +28,7 @@ class RiskOfRainWorld(World):
|
||||
first crash landing.
|
||||
"""
|
||||
game: str = "Risk of Rain 2"
|
||||
options = ror2_options
|
||||
option_definitions = ror2_options
|
||||
topology_present = False
|
||||
|
||||
item_name_to_id = item_table
|
||||
|
||||
@@ -2,6 +2,7 @@ import typing
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .Names import ItemName
|
||||
from worlds.alttp import ALTTPWorld
|
||||
|
||||
|
||||
class ItemData(typing.NamedTuple):
|
||||
@@ -18,9 +19,6 @@ class SA2BItem(Item):
|
||||
def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None):
|
||||
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.
|
||||
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}
|
||||
|
||||
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.
|
||||
"""
|
||||
game: str = "Sonic Adventure 2 Battle"
|
||||
options = sa2b_options
|
||||
option_definitions = sa2b_options
|
||||
topology_present = False
|
||||
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,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
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,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
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))
|
||||
|
||||
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:
|
||||
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()}
|
||||
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
|
||||
locked_locations: typing.List[str]
|
||||
|
||||
@@ -5,7 +5,7 @@ import copy
|
||||
import os
|
||||
import threading
|
||||
import base64
|
||||
from typing import Set, List, TextIO
|
||||
from typing import Set, TextIO
|
||||
|
||||
from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils
|
||||
|
||||
@@ -79,7 +79,7 @@ class SMWorld(World):
|
||||
game: str = "Super Metroid"
|
||||
topology_present = True
|
||||
data_version = 1
|
||||
options = sm_options
|
||||
option_definitions = sm_options
|
||||
item_names: Set[str] = frozenset(items_lookup_name_to_id)
|
||||
location_names: Set[str] = frozenset(locations_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:
|
||||
# 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
|
||||
if itemLoc.item.type in ItemManager.Items:
|
||||
itemId = ItemManager.Items[itemLoc.item.type].Id
|
||||
if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items:
|
||||
itemId = ItemManager.Items[itemLoc.item.type].Id
|
||||
else:
|
||||
itemId = ItemManager.Items['ArchipelagoItem'].Id + idx
|
||||
multiWorldItems.append({"sym": symbols["message_item_names"],
|
||||
"offset": (vanillaItemTypesCount + idx)*64,
|
||||
"values": self.convertToROMItemName(itemLoc.item.name)})
|
||||
idx += 1
|
||||
|
||||
|
||||
if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()):
|
||||
playerIDCount += 1
|
||||
self.playerIDMap[romPlayerID] = playerIDCount
|
||||
@@ -488,7 +488,13 @@ class SMWorld(World):
|
||||
# commit all the changes we've made here to the ROM
|
||||
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)
|
||||
|
||||
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):
|
||||
slot_data = {}
|
||||
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]
|
||||
slot_data[option_name] = option.value
|
||||
|
||||
@@ -735,7 +741,8 @@ class SMLocation(Location):
|
||||
|
||||
class SMItem(Item):
|
||||
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)
|
||||
self.type = type
|
||||
|
||||
@@ -35,13 +35,11 @@ class SM64World(World):
|
||||
location_name_to_id = location_table
|
||||
|
||||
data_version = 6
|
||||
required_client_version = (0,3,0)
|
||||
|
||||
forced_auto_forfeit = False
|
||||
required_client_version = (0, 3, 0)
|
||||
|
||||
area_connections: typing.Dict[int, int]
|
||||
|
||||
options = sm64_options
|
||||
option_definitions = sm64_options
|
||||
|
||||
def generate_early(self):
|
||||
self.topology_present = self.world.AreaRandomizer[self.player].value
|
||||
@@ -120,7 +118,7 @@ class SM64World(World):
|
||||
"AreaRando": self.area_connections,
|
||||
"FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[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,
|
||||
"MIPS2Cost": self.world.MIPS2Cost[self.player].value,
|
||||
"StarsToFinish": self.world.StarsToFinish[self.player].value,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import typing
|
||||
from Options import Choice, Option
|
||||
from Options import Choice, Option, Toggle, DefaultOnToggle, Range
|
||||
|
||||
class SMLogic(Choice):
|
||||
"""This option selects what kind of logic to use for item placement inside
|
||||
@@ -45,6 +45,22 @@ class MorphLocation(Choice):
|
||||
option_Original = 2
|
||||
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):
|
||||
"""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
|
||||
@@ -55,9 +71,75 @@ class KeyShuffle(Choice):
|
||||
option_Keysanity = 1
|
||||
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)] = {
|
||||
"sm_logic": SMLogic,
|
||||
"sword_location": SwordLocation,
|
||||
"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):
|
||||
DefeatBoth = 0
|
||||
FastGanonDefeatMotherBrain = 1
|
||||
AllDungeonsDefeatMotherBrain = 2
|
||||
|
||||
class KeyShuffle(Enum):
|
||||
Null = 0
|
||||
Keysanity = 1
|
||||
|
||||
class GanonInvincible(Enum):
|
||||
Never = 0
|
||||
BeforeCrystals = 1
|
||||
BeforeAllDungeons = 2
|
||||
Always = 3
|
||||
class OpenTower(Enum):
|
||||
Random = -1
|
||||
NoCrystals = 0
|
||||
OneCrystal = 1
|
||||
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:
|
||||
GameMode: GameMode = GameMode.Multiworld
|
||||
@@ -45,64 +71,20 @@ class Config:
|
||||
MorphLocation: MorphLocation = MorphLocation.Randomized
|
||||
Goal: Goal = Goal.DefeatBoth
|
||||
KeyShuffle: KeyShuffle = KeyShuffle.Null
|
||||
Keysanity: bool = KeyShuffle != KeyShuffle.Null
|
||||
Race: bool = False
|
||||
GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals
|
||||
MinimalAccessibility: bool = False # AP specific accessibility: minimal
|
||||
|
||||
def __init__(self, options: Dict[str, str]):
|
||||
self.GameMode = self.ParseOption(options, GameMode.Multiworld)
|
||||
self.Z3Logic = self.ParseOption(options, Z3Logic.Normal)
|
||||
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)
|
||||
OpenTower: OpenTower = OpenTower.SevenCrystals
|
||||
GanonVulnerable: GanonVulnerable = GanonVulnerable.SevenCrystals
|
||||
OpenTourian: OpenTourian = OpenTourian.FourBosses
|
||||
|
||||
def ParseOption(self, options:Dict[str, str], defaultValue:Enum):
|
||||
enumKey = defaultValue.__class__.__name__.lower()
|
||||
if (enumKey in options):
|
||||
return defaultValue.__class__[options[enumKey]]
|
||||
return defaultValue
|
||||
@property
|
||||
def SingleWorld(self) -> bool:
|
||||
return self.GameMode == GameMode.Normal
|
||||
|
||||
@property
|
||||
def Multiworld(self) -> bool:
|
||||
return self.GameMode == GameMode.Multiworld
|
||||
|
||||
def ParseOptionWith(self, options:Dict[str, str], option:str, defaultValue:bool):
|
||||
if (option.lower() in options):
|
||||
return options[option.lower()]
|
||||
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;
|
||||
}
|
||||
} """
|
||||
@property
|
||||
def Keysanity(self) -> bool:
|
||||
return self.KeyShuffle != KeyShuffle.Null
|
||||
@@ -130,6 +130,11 @@ class ItemType(Enum):
|
||||
CardLowerNorfairL1 = 0xDE
|
||||
CardLowerNorfairBoss = 0xDF
|
||||
|
||||
SmMapBrinstar = 0xCA
|
||||
SmMapWreckedShip = 0xCB
|
||||
SmMapMaridia = 0xCC
|
||||
SmMapLowerNorfair = 0xCD
|
||||
|
||||
Missile = 0xC2
|
||||
Super = 0xC3
|
||||
PowerBomb = 0xC4
|
||||
@@ -174,6 +179,7 @@ class Item:
|
||||
map = re.compile("^Map")
|
||||
compass = re.compile("^Compass")
|
||||
keycard = re.compile("^Card")
|
||||
smMap = re.compile("^SmMap")
|
||||
|
||||
def IsDungeonItem(self): return self.dungeon.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 IsCompass(self): return self.compass.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):
|
||||
return self.Type == type and self.World == world
|
||||
@@ -313,7 +320,7 @@ class Item:
|
||||
Item.AddRange(itemPool, 4, Item(ItemType.BombUpgrade5))
|
||||
Item.AddRange(itemPool, 2, Item(ItemType.OneRupee))
|
||||
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, 5, Item(ItemType.ThreeHundredRupees))
|
||||
|
||||
@@ -421,6 +428,21 @@ class Item:
|
||||
|
||||
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
|
||||
def Get(items, itemType:ItemType):
|
||||
item = next((i for i in items if i.Type == itemType), None)
|
||||
@@ -725,7 +747,7 @@ class Progression:
|
||||
|
||||
def CanAccessMiseryMirePortal(self, config: Config):
|
||||
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:
|
||||
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) \
|
||||
@@ -769,11 +791,11 @@ class Progression:
|
||||
if (world.Config.SMLogic == SMLogic.Normal):
|
||||
return self.MoonPearl and self.Flippers 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:
|
||||
return self.MoonPearl and self.Flippers 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
|
||||
items_start_id = 84000
|
||||
|
||||
@@ -6,7 +6,7 @@ import typing
|
||||
from BaseClasses import Location
|
||||
from worlds.smz3.TotalSMZ3.Item import Item, ItemType
|
||||
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.DesertPalace import DesertPalace
|
||||
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.TurtleRock import TurtleRock
|
||||
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.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.Dialog import Dialog
|
||||
|
||||
@@ -30,6 +34,11 @@ class KeycardPlaque:
|
||||
Level2 = 0xe1
|
||||
Boss = 0xe2
|
||||
Null = 0x00
|
||||
Zero = 0xe3
|
||||
One = 0xe4
|
||||
Two = 0xe5
|
||||
Three = 0xe6
|
||||
Four = 0xe7
|
||||
|
||||
class KeycardDoors:
|
||||
Left = 0xd414
|
||||
@@ -73,8 +82,8 @@ class DropPrize(Enum):
|
||||
Fairy = 0xE3
|
||||
|
||||
class Patch:
|
||||
Major = 0
|
||||
Minor = 1
|
||||
Major = 11
|
||||
Minor = 3
|
||||
allWorlds: List[World]
|
||||
myWorld: World
|
||||
seedGuid: str
|
||||
@@ -105,13 +114,16 @@ class Patch:
|
||||
|
||||
self.WriteDiggingGameRng()
|
||||
|
||||
self.WritePrizeShuffle()
|
||||
self.WritePrizeShuffle(self.myWorld.WorldState.DropPrizes)
|
||||
|
||||
self.WriteRemoveEquipmentFromUncle( self.myWorld.GetLocation("Link's Uncle").APLocation.item.item if
|
||||
self.myWorld.GetLocation("Link's Uncle").APLocation.item.game == "SMZ3" else
|
||||
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.WriteSaveAndQuitFromBossRoom()
|
||||
@@ -135,26 +147,27 @@ class Patch:
|
||||
return {patch[0]:patch[1] for patch in self.patches}
|
||||
|
||||
def WriteMedallions(self):
|
||||
from worlds.smz3.TotalSMZ3.WorldState import Medallion
|
||||
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))
|
||||
|
||||
turtleRockAddresses = [0x308023, 0xD020, 0xD0FF, 0xD1DE ]
|
||||
miseryMireAddresses = [ 0x308022, 0xCFF2, 0xD0D1, 0xD1B0 ]
|
||||
|
||||
if turtleRock.Medallion == ItemType.Bombos:
|
||||
if turtleRock.Medallion == Medallion.Bombos:
|
||||
turtleRockValues = [0x00, 0x51, 0x10, 0x00]
|
||||
elif turtleRock.Medallion == ItemType.Ether:
|
||||
elif turtleRock.Medallion == Medallion.Ether:
|
||||
turtleRockValues = [0x01, 0x51, 0x18, 0x00]
|
||||
elif turtleRock.Medallion == ItemType.Quake:
|
||||
elif turtleRock.Medallion == Medallion.Quake:
|
||||
turtleRockValues = [0x02, 0x14, 0xEF, 0xC4]
|
||||
else:
|
||||
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]
|
||||
elif miseryMire.Medallion == ItemType.Ether:
|
||||
elif miseryMire.Medallion == Medallion.Ether:
|
||||
miseryMireValues = [0x01, 0x13, 0x9F, 0xF1]
|
||||
elif miseryMire.Medallion == ItemType.Quake:
|
||||
elif miseryMire.Medallion == Medallion.Quake:
|
||||
miseryMireValues = [0x02, 0x51, 0x08, 0x00]
|
||||
else:
|
||||
raise exception(f"Tried using {miseryMire.Medallion} in place of Misery Mire medallion")
|
||||
@@ -174,12 +187,19 @@ class Patch:
|
||||
self.rnd.shuffle(pendantsBlueRed)
|
||||
pendantRewards = pendantsGreen + pendantsBlueRed
|
||||
|
||||
bossTokens = [ 1, 2, 3, 4 ]
|
||||
|
||||
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]
|
||||
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(pendantRegions, pendantRewards, self.PendantValues)
|
||||
self.patches += self.RewardPatches(bossRegions, bossTokens, self.BossTokenValues)
|
||||
|
||||
def RewardPatches(self, regions: List[IReward], rewards: List[int], rewardValues: Callable):
|
||||
addresses = [self.RewardAddresses(region) for region in regions]
|
||||
@@ -189,17 +209,22 @@ class Patch:
|
||||
|
||||
def RewardAddresses(self, region: IReward):
|
||||
regionType = {
|
||||
EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE ],
|
||||
DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF ],
|
||||
TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706 ],
|
||||
PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702 ],
|
||||
SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701 ],
|
||||
SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704 ],
|
||||
ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707 ],
|
||||
IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705 ],
|
||||
MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703 ],
|
||||
TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708 ]
|
||||
EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE, 0x30D100],
|
||||
DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF, 0x30D101 ],
|
||||
TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706, 0x30D102 ],
|
||||
PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702, 0x30D103 ],
|
||||
SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701, 0x30D104 ],
|
||||
SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704, 0x30D105 ],
|
||||
ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707, 0x30D106 ],
|
||||
IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705, 0x30D107 ],
|
||||
MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703, 0x30D108 ],
|
||||
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)
|
||||
if result is None:
|
||||
raise exception(f"Region {region} should not be a dungeon reward region")
|
||||
@@ -208,13 +233,13 @@ class Patch:
|
||||
|
||||
def CrystalValues(self, crystal: int):
|
||||
crystalMap = {
|
||||
1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06 ],
|
||||
2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06 ],
|
||||
3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06 ],
|
||||
4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06 ],
|
||||
5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06 ],
|
||||
6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06 ],
|
||||
7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06 ],
|
||||
1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x10 ],
|
||||
2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x10 ],
|
||||
3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x10 ],
|
||||
4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06, 0x10 ],
|
||||
5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x11 ],
|
||||
6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x11 ],
|
||||
7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x10 ],
|
||||
}
|
||||
result = crystalMap.get(crystal, None)
|
||||
if result is None:
|
||||
@@ -224,15 +249,28 @@ class Patch:
|
||||
|
||||
def PendantValues(self, pendant: int):
|
||||
pendantMap = {
|
||||
1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01 ],
|
||||
2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03 ],
|
||||
3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02 ],
|
||||
1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x12 ],
|
||||
2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x14 ],
|
||||
3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x13 ]
|
||||
}
|
||||
result = pendantMap.get(pendant, None)
|
||||
if result is None:
|
||||
raise exception(f"Tried using {pendant} as a pendant number")
|
||||
else:
|
||||
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 GetSMItemPLM(location:Location):
|
||||
@@ -259,7 +297,7 @@ class Patch:
|
||||
ItemType.SpaceJump : 0xEF1B,
|
||||
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)
|
||||
if (plmId == 0xEFE0):
|
||||
plmId += 4 if location.Type == LocationType.Chozo else 8 if location.Type == LocationType.Hidden else 0
|
||||
@@ -268,7 +306,7 @@ class Patch:
|
||||
return plmId
|
||||
|
||||
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(self.ItemTablePatch(location, self.GetZ3ItemId(location)))
|
||||
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]))
|
||||
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))
|
||||
dialog = Dialog.Simple(text)
|
||||
if (location.Type == LocationType.Pedestal):
|
||||
self.stringTable.SetPedestalText(text)
|
||||
self.patches.append((Snes(0x308300), dialog))
|
||||
elif (location.Type == LocationType.Ether):
|
||||
self.stringTable.SetEtherText(text)
|
||||
self.patches.append((Snes(0x308F00), dialog))
|
||||
elif (location.Type == LocationType.Bombos):
|
||||
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(self.ItemTablePatch(location, self.GetZ3ItemId(location)))
|
||||
else:
|
||||
@@ -305,11 +339,11 @@ class Patch:
|
||||
item = location.APLocation.item.item
|
||||
itemDungeon = None
|
||||
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():
|
||||
itemDungeon = ItemType.BigKey
|
||||
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():
|
||||
itemDungeon = ItemType.Compass
|
||||
|
||||
@@ -327,15 +361,11 @@ class Patch:
|
||||
|
||||
def WriteDungeonMusic(self, keysanity: bool):
|
||||
if (not keysanity):
|
||||
regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)]
|
||||
music = []
|
||||
regions = [region for region in self.myWorld.Regions if isinstance(region, Z3Region) and isinstance(region, IReward) and
|
||||
region.Reward != None and region.Reward != RewardType.Agahnim]
|
||||
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]]
|
||||
regions = pendantRegions + crystalRegions
|
||||
music = [
|
||||
0x11, 0x11, 0x11, 0x16, 0x16,
|
||||
0x16, 0x16, 0x16, 0x16, 0x16,
|
||||
]
|
||||
music = [0x11 if (region.Reward == RewardType.PendantGreen or region.Reward == RewardType.PendantNonGreen) else 0x16 for region in regions]
|
||||
self.patches += self.MusicPatches(regions, music)
|
||||
|
||||
#IEnumerable<byte> RandomDungeonMusic() {
|
||||
@@ -366,51 +396,13 @@ class Patch:
|
||||
else:
|
||||
return result
|
||||
|
||||
def WritePrizeShuffle(self):
|
||||
prizePackItems = 56
|
||||
treePullItems = 3
|
||||
|
||||
bytes = []
|
||||
drop = 0
|
||||
final = 0
|
||||
|
||||
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 ]))
|
||||
def WritePrizeShuffle(self, dropPrizes):
|
||||
self.patches.append((Snes(0x6FA78), [e.value for e in dropPrizes.Packs]))
|
||||
self.patches.append((Snes(0x1DFBD4), [e.value for e in dropPrizes.TreePulls]))
|
||||
self.patches.append((Snes(0x6A9C8), [dropPrizes.CrabContinous.value]))
|
||||
self.patches.append((Snes(0x6A9C4), [dropPrizes.CrabFinal.value]))
|
||||
self.patches.append((Snes(0x6F993), [dropPrizes.Stun.value]))
|
||||
self.patches.append((Snes(0x1D82CC), [dropPrizes.Fish.value]))
|
||||
|
||||
self.patches += self.EnemyPrizePackDistribution()
|
||||
|
||||
@@ -524,46 +516,29 @@ class Patch:
|
||||
redCrystalDungeons = [region for region in regions if region.Reward == RewardType.CrystalRed]
|
||||
|
||||
sahasrahla = Texts.SahasrahlaReveal(greenPendantDungeon)
|
||||
self.patches.append((Snes(0x308A00), Dialog.Simple(sahasrahla)))
|
||||
self.stringTable.SetSahasrahlaRevealText(sahasrahla)
|
||||
|
||||
bombShop = Texts.BombShopReveal(redCrystalDungeons)
|
||||
self.patches.append((Snes(0x308E00), Dialog.Simple(bombShop)))
|
||||
self.stringTable.SetBombShopRevealText(bombShop)
|
||||
|
||||
blind = Texts.Blind(self.rnd)
|
||||
self.patches.append((Snes(0x308800), Dialog.Simple(blind)))
|
||||
self.stringTable.SetBlindText(blind)
|
||||
|
||||
tavernMan = Texts.TavernMan(self.rnd)
|
||||
self.patches.append((Snes(0x308C00), Dialog.Simple(tavernMan)))
|
||||
self.stringTable.SetTavernManText(tavernMan)
|
||||
|
||||
ganon = Texts.GanonFirstPhase(self.rnd)
|
||||
self.patches.append((Snes(0x308600), Dialog.Simple(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)]
|
||||
if len(silversLocation) == 0:
|
||||
silvers = Texts.GanonThirdPhaseMulti(None, self.myWorld, self.silversWorldID, self.playerIDToNames[self.silversWorldID])
|
||||
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)
|
||||
self.patches.append((Snes(0x308700), Dialog.Simple(silvers)))
|
||||
self.stringTable.SetGanonThirdPhaseText(silvers)
|
||||
|
||||
triforceRoom = Texts.TriforceRoom(self.rnd)
|
||||
self.patches.append((Snes(0x308400), Dialog.Simple(triforceRoom)))
|
||||
self.stringTable.SetTriforceRoomText(triforceRoom)
|
||||
|
||||
def WriteStringTable(self):
|
||||
@@ -579,26 +554,32 @@ class Patch:
|
||||
return bytearray(name, 'utf8')
|
||||
|
||||
def WriteSeedData(self):
|
||||
configField = \
|
||||
configField1 = \
|
||||
((1 if self.myWorld.Config.Race else 0) << 15) | \
|
||||
((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.SMLogic.value << 8) | \
|
||||
(Patch.Major << 4) | \
|
||||
(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(0x80FF52), getWordArray(configField)))
|
||||
self.patches.append((Snes(0x80FF52), getWordArray(configField1)))
|
||||
self.patches.append((Snes(0x80FF54), getDoubleWordArray(self.seed)))
|
||||
self.patches.append((Snes(0x80FF58), getWordArray(configField2)))
|
||||
#/* 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(0x80FF80), bytearray(self.myWorld.Guid, 'utf8')))
|
||||
|
||||
def WriteCommonFlags(self):
|
||||
#/* 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)))
|
||||
if (self.myWorld.Config.Keysanity):
|
||||
self.patches.append((Snes(0xF47006), getWordArray(0x0001)))
|
||||
@@ -619,97 +600,104 @@ class Patch:
|
||||
if (self.myWorld.Config.Keysanity):
|
||||
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(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):
|
||||
if (not self.myWorld.Config.Keysanity):
|
||||
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
|
||||
plaquePlm = 0xd410
|
||||
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 (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
|
||||
if ( self.myWorld.Config.Keysanity):
|
||||
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
|
||||
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 ]))
|
||||
|
||||
@@ -745,20 +733,32 @@ class Patch:
|
||||
(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 */
|
||||
invincibleMap = {
|
||||
GanonInvincible.Never : 0x00,
|
||||
GanonInvincible.Always : 0x01,
|
||||
GanonInvincible.BeforeAllDungeons : 0x02,
|
||||
GanonInvincible.BeforeCrystals : 0x03,
|
||||
}
|
||||
value = invincibleMap.get(invincible, None)
|
||||
valueMap = {
|
||||
Goal.DefeatBoth : 0x03,
|
||||
Goal.FastGanonDefeatMotherBrain : 0x04,
|
||||
Goal.AllDungeonsDefeatMotherBrain : 0x02
|
||||
}
|
||||
value = valueMap.get(goal, None)
|
||||
if (value is None):
|
||||
raise exception(f"Unknown Ganon invincible value {invincible}")
|
||||
raise exception(f"Unknown Ganon invincible value {goal}")
|
||||
else:
|
||||
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):
|
||||
#/* Repoint RNG Block */
|
||||
|
||||
@@ -5,12 +5,19 @@ from worlds.smz3.TotalSMZ3.Item import Item, ItemType
|
||||
|
||||
class RewardType(Enum):
|
||||
Null = 0
|
||||
Agahnim = 1
|
||||
PendantGreen = 2
|
||||
PendantNonGreen = 3
|
||||
CrystalBlue = 4
|
||||
CrystalRed = 5
|
||||
GoldenFourBoss = 6
|
||||
Agahnim = 1 << 0
|
||||
PendantGreen = 1 << 1
|
||||
PendantNonGreen = 1 << 2
|
||||
CrystalBlue = 1 << 3
|
||||
CrystalRed = 1 << 4
|
||||
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:
|
||||
Reward: RewardType
|
||||
@@ -18,7 +25,7 @@ class IReward:
|
||||
pass
|
||||
|
||||
class IMedallionAccess:
|
||||
Medallion: object
|
||||
Medallion = None
|
||||
|
||||
class Region:
|
||||
import worlds.smz3.TotalSMZ3.Location as Location
|
||||
|
||||
@@ -7,7 +7,7 @@ class Kraid(SMRegion, IReward):
|
||||
Name = "Brinstar Kraid"
|
||||
Area = "Brinstar"
|
||||
|
||||
Reward = RewardType.GoldenFourBoss
|
||||
Reward = RewardType.Null
|
||||
|
||||
def __init__(self, world, config: Config):
|
||||
super().__init__(world, config)
|
||||
|
||||
@@ -40,5 +40,5 @@ class Pink(SMRegion):
|
||||
else:
|
||||
return items.CanOpenRedDoors() and (items.CanDestroyBombWalls() or items.SpeedBooster) 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())
|
||||
|
||||
@@ -17,9 +17,9 @@ class East(SMRegion):
|
||||
self.world.CanEnter("Wrecked Ship", items)) if self.Logic == SMLogic.Normal else \
|
||||
lambda items: items.Morph),
|
||||
Location(self, 2, 0x8F81EE, LocationType.Hidden, "Missile (outside Wrecked Ship top)",
|
||||
lambda items: self.world.CanEnter("Wrecked Ship", items) and (not self.Config.Keysanity or items.CardWreckedShipBoss) and items.CanPassBombPassages()),
|
||||
lambda items: self.world.CanEnter("Wrecked Ship", items) and items.CardWreckedShipBoss and items.CanPassBombPassages()),
|
||||
Location(self, 3, 0x8F81F4, LocationType.Visible, "Missile (outside Wrecked Ship middle)",
|
||||
lambda items: self.world.CanEnter("Wrecked Ship", items) and (not self.Config.Keysanity or items.CardWreckedShipBoss) and items.CanPassBombPassages()),
|
||||
lambda items: self.world.CanEnter("Wrecked Ship", items) and items.CardWreckedShipBoss and items.CanPassBombPassages()),
|
||||
Location(self, 4, 0x8F8248, LocationType.Visible, "Missile (Crateria moat)",
|
||||
lambda items: True)
|
||||
]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user