Compare commits

..

56 Commits

Author SHA1 Message Date
Chris Wilson
f9dc1d5142 Try to improve the explanation of the intention behind the style restrictions. 2022-08-22 18:48:55 -04:00
Chris Wilson
614c50e495 Attempt to make the WebHost change guide describe the intent of the style restrictions more accurately. 2022-08-19 18:39:32 -04:00
Chris Wilson
60e2b818b1 First pass at a contribution guide for the website. Suggestions are welcome. 2022-08-17 23:47:27 -04:00
Fabian Dill
0d61192c67 Factorio: make apworld compatible(#935) 2022-08-18 01:33:40 +02:00
Fabian Dill
a1aa9c17ff Core: convert is_zip to zip_path 2022-08-18 01:20:30 +02:00
Henrique Gemignani Passos Lima
d0faa36eef Fix CommonClient.server_loop with nogui
When running client without a gui, ctx.ui is None
2022-08-18 01:18:01 +02:00
Fabian Dill
22c8153ba8 WebHost: fix indentation in tracker.py 2022-08-17 22:15:56 +02:00
CaitSith2
6602c580f4 Fix another item.type crash bug. (#927)
* Fix another item.type crash bug.

* Another location that can crash, in the instance of plando fixed.
2022-08-17 07:16:14 -07:00
Joethepic
431a9b7023 Docs: Mc: fix version in setup guide (#873)
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2022-08-17 09:59:22 +02:00
Fabian Dill
d426226bce LttP: run optimize imports on __init__ 2022-08-16 23:57:59 +02:00
Fabian Dill
09afdc2553 Webhost: prevent tracker crashes with LttP key itemlinks (#922) 2022-08-16 23:57:26 +02:00
Fabian Dill
ca83905d9f Core: allow loading worlds from zip modules (#747)
* Core: allow loading worlds from zip modules
RoR2: make it zipimport compatible (remove relative imports beyond local top-level)

* WebHost: add support for .apworld
2022-08-15 23:52:03 +02:00
black-sliver
086295adbb AutoWorld: add preliminary .apworld specification (#903)
* AutoWorld: add preliminary .apworld specification

* Doc: apworld specification: fix typo
2022-08-15 23:47:32 +02:00
alwaysintreble
81cf1508e0 Core: Refactor Autoworld.options to Autoworld.option_definitions (#906)
* refactor `world.options` -> `world.option_definitions`

* rename world api reference

* missed some self.options
2022-08-15 23:46:59 +02:00
Fabian Dill
8484193151 Core: crash if non_local pool is too big 2022-08-15 23:36:07 +02:00
Fabian Dill
d10fbf8263 Minecraft: update requests 2022-08-15 23:35:51 +02:00
Fabian Dill
f73b3d71bf Factorio: fix typo 2022-08-15 23:03:03 +02:00
Fabian Dill
d48d775a59 Subnautica: fix 2 logic/locations bugs and add a bit of docs (#917) 2022-08-15 22:53:59 +02:00
Yussur Mustafa Oraji
f716bfc58f sm64ex: Fix Second Floor Door Cost (#909) 2022-08-15 17:29:35 +02:00
Jarno Westhof
97b388747a Docs: Added DS3 & DK3 to network graph 2022-08-15 16:56:55 +02:00
lordlou
898fa203ad Smz3 updated to version 11.3 (#886) 2022-08-15 16:48:13 +02:00
CaitSith2
c02c6ee58c Fix generation failure for Final Fantasy 1 and Dark Souls 3. (#907)
* Fix generation failure for Final Fantasy 1.

* Fix spoiler log giving "Location (Player x): Item (Player y)" for FF1.

* Dark Soul 3 Items/Locations now get player names in spoiler log.
2022-08-14 12:34:46 -07:00
black-sliver
23b04b5069 SM: correctly check if items are SM items 2022-08-14 13:38:52 +02:00
Ludovic Marechal
0ed0d17f38 DS3: Update the setup guide (#878)
* Merge pull request #1 from eudaimonistic/patch-2

Update setup_en.md

(cherry picked from commit 41567697fb89e74301afe651fbde0bafca5946e0)

* DS3: Update english documentation

* DS3: Add French setup guide

* DS3: Fix space formatting in doc

* DS3: Resolve comment
2022-08-14 00:07:36 +02:00
espeon65536
645ede869f OoT: Fix blind item.type reference (#905)
* oot: remove blind reference to item.type

* oot: logical reasoning is hard

* oot: fix blind item.type reference
2022-08-13 04:36:06 +02:00
black-sliver
f5e48c850d Utils: lazy decimal import
decimal is kinda big, there is no noticable difference in performance and the import is unused by webhost's customserver
2022-08-13 00:20:08 +02:00
Fabian Dill
9bd035a19d WebHost: make a fresh Room reload page once if port is not assigned yet 2022-08-12 16:01:02 +02:00
Fabian Dill
2e428f906c Core: document KeyedDefaultDict 2022-08-12 08:34:33 +02:00
black-sliver
b702ae482b Core: clean up Utils.py
* fix import order
* lazy import shutil
* lazy import jellyfish (also speed-up by 0.8%, probably because of inlining)
* yaml:
  * explicitely call Loader UnsafeLoader
  * use CDumper, twice as fast
  * stop leaking leak imported names load and load_all
* open_file: use absolute path
* replace quotes in touched code
* add some typing in touched code
* stringify type hinting for non-imports
* %s/.format -> f
* freeze safe_builtins
* remove double-caching in get_options()
* get rid of some warnings
2022-08-12 08:07:45 +02:00
black-sliver
b8ca41b45f Utils: SI: fix rounding problems (#895)
* Utils: SI: fix rounding problems

999.999 would give 1000.00 instead of 1.00k

* Tests: add Utils: SI tests
2022-08-12 00:46:11 +02:00
CaitSith2
adc16fdd3d Factorio: Don't send researches completed by editor extensions testing forces. (#894) 2022-08-11 18:11:34 +02:00
NewSoupVi
b32d0efe6d Witness: Logic fix for Treehouse in Doors (#892) 2022-08-11 15:57:33 +02:00
black-sliver
c96acbfa23 TextClient: receive all items
By popular demand, this makes /received work again.

Closes #887
2022-08-11 01:06:58 +02:00
black-sliver
ffe528467e Generate: remove period for easy copy&paste
Double-clicking in terminal may select the period, resulting in a bad filename in clipboard.
Also fixing quotes.
2022-08-11 01:06:43 +02:00
Fabian Dill
b989698740 WebHost: fix datapackage typo 2022-08-11 01:04:53 +02:00
Fabian Dill
29e0975832 Clients: prepare for removal of players key in RoomInfo 2022-08-11 00:48:38 +02:00
CaitSith2
e1e2526322 LttP: Do a check for enemizer much earlier in generation. (#875) 2022-08-10 22:21:52 +02:00
Fabian Dill
f2e83c37e9 WebHost: use title-typical sorting for game titles (#883) 2022-08-09 22:21:45 +02:00
Fabian Dill
debda5d111 MultiServer: swap auto-forfeit with auto-collect order
That way the forfeit for items for players that are still playing appear last in the log, which is the visible text in at least the py clients
2022-08-09 16:58:02 +02:00
alwaysintreble
2c4e819010 docs: plando update (#861)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-09 10:47:01 +02:00
alwaysintreble
b3700dabf2 Core: Fix meta.yaml and allow the None game category for common options (#845) 2022-08-09 02:29:00 +02:00
TheCondor07
fb2979d9ef SC2: Added Difficulty Override to Client (#863) 2022-08-09 00:20:51 +02:00
Fabian Dill
a378d62dfd SC2: fix Moebius Factor rescue condition (#882) 2022-08-08 23:20:18 +02:00
lordlou
eb5ba72cfc Smz3 min accessibility fix (#880) 2022-08-08 22:23:22 +02:00
Fabian Dill
c1e9d0ab4f WebHost: allow customserver to skip importing worlds subsystem for hosting a Room (#877)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-07 18:28:50 +02:00
black-sliver
181cc47079 Core: cleanup BaseClasses.Location
This is just cleanup and has virtually no performance impact.
2022-08-07 13:11:12 +02:00
Zach Parks
04eef669f9 StS: Add a description for the game. (#876) 2022-08-06 21:36:32 -05:00
PoryGone
9167e5363d DKC3: Correct File Extension in Setup Guide (#872) 2022-08-06 13:26:02 +02:00
Zach Parks
f1c5c9a148 WebHost: Fix OptionDicts that define valid_keys from outputting as [] on Weighted Settings export. (#874)
* WebHost: Fix OptionDicts that define valid_keys from outputting as [] on Weighted Settings export
2022-08-06 13:25:37 +02:00
Joethepic
69e5627cd7 HK: fix indentation on mimic grubs (#868) 2022-08-06 02:11:10 +02:00
PoryGone
ae3e6c29e3 DKC3: Add Link to Tracker from Setup Guide (#871) 2022-08-06 00:53:48 +02:00
black-sliver
f6da81ac70 Core: cleanup Item classes (#849) 2022-08-06 00:49:54 +02:00
Jarno Westhof
dd6e212519 [Core] Colorama fix 2022-08-05 17:17:40 +02:00
Fabian Dill
95bba50223 WebHost: fix filename rename in flask update 2022-08-05 17:16:26 +02:00
Fabian Dill
21f7c6c0ad Core: optimize away Item.world (#840)
* Core: optimize away Item.world

* Update test/general/TestFill.py

* Test: undo unnecessary changes

* lttp: remove two more Item.world writes

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-05 17:09:21 +02:00
Fabian Dill
d15c30f63b Stats: limit to recognized games 2022-08-05 17:01:02 +02:00
135 changed files with 2023 additions and 1156 deletions

View File

@@ -166,7 +166,7 @@ class MultiWorld():
self.player_types[new_id] = NetUtils.SlotType.group self.player_types[new_id] = NetUtils.SlotType.group
self._region_cache[new_id] = {} self._region_cache[new_id] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[game] world_type = AutoWorld.AutoWorldRegister.world_types[game]
for option_key, option in world_type.options.items(): for option_key, option in world_type.option_definitions.items():
getattr(self, option_key)[new_id] = option(option.default) getattr(self, option_key)[new_id] = option(option.default)
for option_key, option in Options.common_options.items(): for option_key, option in Options.common_options.items():
getattr(self, option_key)[new_id] = option(option.default) getattr(self, option_key)[new_id] = option(option.default)
@@ -204,7 +204,7 @@ class MultiWorld():
for player in self.player_ids: for player in self.player_ids:
self.custom_data[player] = {} self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
for option_key in world_type.options: for option_key in world_type.option_definitions:
setattr(self, option_key, getattr(args, option_key, {})) setattr(self, option_key, getattr(args, option_key, {}))
self.worlds[player] = world_type(self, player) self.worlds[player] = world_type(self, player)
@@ -384,7 +384,6 @@ class MultiWorld():
return self.worlds[player].create_item(item_name) return self.worlds[player].create_item(item_name)
def push_precollected(self, item: Item): def push_precollected(self, item: Item):
item.world = self
self.precollected_items[item.player].append(item) self.precollected_items[item.player].append(item)
self.state.collect(item, True) self.state.collect(item, True)
@@ -392,7 +391,6 @@ class MultiWorld():
assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}." assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}."
location.item = item location.item = item
item.location = location item.location = location
item.world = self # try to not have this here anymore and create it with item?
if collect: if collect:
self.state.collect(item, location.event, location) self.state.collect(item, location.event, location)
@@ -1066,26 +1064,25 @@ class LocationProgressType(IntEnum):
class Location: class Location:
# If given as integer, then this is the shop's inventory index game: str = "Generic"
shop_slot: Optional[int] = None player: int
shop_slot_disabled: bool = False name: str
address: Optional[int]
parent_region: Optional[Region]
event: bool = False event: bool = False
locked: bool = False locked: bool = False
game: str = "Generic"
show_in_spoiler: bool = True show_in_spoiler: bool = True
crystal: bool = False
progress_type: LocationProgressType = LocationProgressType.DEFAULT progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda item, state: False) always_allow = staticmethod(lambda item, state: False)
access_rule = staticmethod(lambda state: True) access_rule = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True) item_rule = staticmethod(lambda item: True)
item: Optional[Item] = None item: Optional[Item] = None
parent_region: Optional[Region]
def __init__(self, player: int, name: str = '', address: int = None, parent=None): def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
self.name: str = name self.player = player
self.address: Optional[int] = address self.name = name
self.address = address
self.parent_region = parent self.parent_region = parent
self.player: int = player
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state))) return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
@@ -1102,7 +1099,6 @@ class Location:
self.item = item self.item = item
item.location = self item.location = self
self.event = item.advancement self.event = item.advancement
self.item.world = self.parent_region.world
self.locked = True self.locked = True
def __repr__(self): def __repr__(self):
@@ -1147,39 +1143,28 @@ class ItemClassification(IntFlag):
class Item: class Item:
location: Optional[Location] = None
world: Optional[MultiWorld] = None
code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata
name: str
game: str = "Generic" game: str = "Generic"
type: str = None __slots__ = ("name", "classification", "code", "player", "location")
name: str
classification: ItemClassification classification: ItemClassification
code: Optional[int]
# need to find a decent place for these to live and to allow other games to register texts if they want. """an item with code None is called an Event, and does not get written to multidata"""
pedestal_credit_text: str = "and the Unknown Item" player: int
sickkid_credit_text: Optional[str] = None location: Optional[Location]
magicshop_credit_text: Optional[str] = None
zora_credit_text: Optional[str] = None
fluteboy_credit_text: Optional[str] = None
# hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem
smallkey: bool = False
bigkey: bool = False
map: bool = False
compass: bool = False
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int): def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
self.name = name self.name = name
self.classification = classification self.classification = classification
self.player = player self.player = player
self.code = code self.code = code
self.location = None
@property @property
def hint_text(self): def hint_text(self) -> str:
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " ")) return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
@property @property
def pedestal_hint_text(self): def pedestal_hint_text(self) -> str:
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " ")) return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
@property @property
@@ -1205,7 +1190,7 @@ class Item:
def __eq__(self, other): def __eq__(self, other):
return self.name == other.name and self.player == other.player return self.name == other.name and self.player == other.player
def __lt__(self, other: Item): def __lt__(self, other: Item) -> bool:
if other.player != self.player: if other.player != self.player:
return other.player < self.player return other.player < self.player
return self.name < other.name return self.name < other.name
@@ -1213,11 +1198,13 @@ class Item:
def __hash__(self): def __hash__(self):
return hash((self.name, self.player)) return hash((self.name, self.player))
def __repr__(self): def __repr__(self) -> str:
return self.__str__() return self.__str__()
def __str__(self): def __str__(self) -> str:
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' if self.location and self.location.parent_region and self.location.parent_region.world:
return self.location.parent_region.world.get_name_string_for_object(self)
return f"{self.name} (Player {self.player})"
class Spoiler(): class Spoiler():
@@ -1401,7 +1388,7 @@ class Spoiler():
outfile.write('Game: %s\n' % self.world.game[player]) outfile.write('Game: %s\n' % self.world.game[player])
for f_option, option in Options.per_game_common_options.items(): for f_option, option in Options.per_game_common_options.items():
write_option(f_option, option) write_option(f_option, option)
options = self.world.worlds[player].options options = self.world.worlds[player].option_definitions
if options: if options:
for f_option, option in options.items(): for f_option, option in options.items():
write_option(f_option, option) write_option(f_option, option)

View File

@@ -493,7 +493,8 @@ async def server_loop(ctx: CommonContext, address=None):
logger.info(f'Connecting to Archipelago server at {address}') logger.info(f'Connecting to Archipelago server at {address}')
try: try:
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None) socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
ctx.ui.update_address_bar(server_url.netloc) if ctx.ui is not None:
ctx.ui.update_address_bar(server_url.netloc)
ctx.server = Endpoint(socket) ctx.server = Endpoint(socket)
logger.info('Connected') logger.info('Connected')
ctx.server_address = address ctx.server_address = address
@@ -562,18 +563,21 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
f" for each location checked. Use !hint for more information.") f" for each location checked. Use !hint for more information.")
ctx.hint_cost = int(args['hint_cost']) ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points']) ctx.check_points = int(args['location_check_points'])
players = args.get("players", [])
if len(players) < 1: if "players" in args: # TODO remove when servers sending this are outdated
logger.info('No player connected') players = args.get("players", [])
else: if len(players) < 1:
players.sort() logger.info('No player connected')
current_team = -1 else:
logger.info('Connected Players:') players.sort()
for network_player in players: current_team = -1
if network_player.team != current_team: logger.info('Connected Players:')
logger.info(f' Team #{network_player.team + 1}') for network_player in players:
current_team = network_player.team if network_player.team != current_team:
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) logger.info(f' Team #{network_player.team + 1}')
current_team = network_player.team
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
# update datapackage # update datapackage
await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"]) await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"])
@@ -723,7 +727,7 @@ if __name__ == '__main__':
class TextContext(CommonContext): class TextContext(CommonContext):
tags = {"AP", "IgnoreGame", "TextOnly"} tags = {"AP", "IgnoreGame", "TextOnly"}
game = "" # empty matches any game since 0.3.2 game = "" # empty matches any game since 0.3.2
items_handling = 0 # don't receive any NetworkItems items_handling = 0b111 # receive all items for /received
async def server_auth(self, password_requested: bool = False): async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password: if password_requested and not self.password:

View File

@@ -20,8 +20,7 @@ import Utils
if __name__ == "__main__": if __name__ == "__main__":
Utils.init_logging("FactorioClient", exception_logger="Client") Utils.init_logging("FactorioClient", exception_logger="Client")
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \ from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser
get_base_parser
from MultiServer import mark_raw from MultiServer import mark_raw
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart

View File

@@ -220,8 +220,8 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
world.push_item(defaultlocations.pop(i), item_to_place, False) world.push_item(defaultlocations.pop(i), item_to_place, False)
break break
else: else:
logging.warning( raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. "
f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.") f"Too many non-local items for too few remaining locations.")
world.random.shuffle(defaultlocations) world.random.shuffle(defaultlocations)

View File

@@ -7,7 +7,7 @@ import urllib.request
import urllib.parse import urllib.parse
from typing import Set, Dict, Tuple, Callable, Any, Union from typing import Set, Dict, Tuple, Callable, Any, Union
import os import os
from collections import Counter from collections import Counter, ChainMap
import string import string
import enum import enum
@@ -133,12 +133,14 @@ def main(args=None, callback=ERmain):
if args.meta_file_path and os.path.exists(args.meta_file_path): if args.meta_file_path and os.path.exists(args.meta_file_path):
try: try:
weights_cache[args.meta_file_path] = read_weights_yamls(args.meta_file_path) meta_weights = read_weights_yamls(args.meta_file_path)[-1]
except Exception as e: except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
meta_weights = weights_cache[args.meta_file_path][-1]
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}") print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
del(meta_weights["meta_description"]) try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
del(meta_weights["meta_description"])
except Exception as e:
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
if args.samesettings: if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta") raise Exception("Cannot mix --samesettings with --meta")
else: else:
@@ -164,7 +166,7 @@ def main(args=None, callback=ERmain):
player_files[player_id] = filename player_files[player_id] = filename
player_id += 1 player_id += 1
args.multi = max(player_id-1, args.multi) args.multi = max(player_id - 1, args.multi)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: " print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
f"{args.plando}") f"{args.plando}")
@@ -186,26 +188,28 @@ def main(args=None, callback=ERmain):
erargs.enemizercli = args.enemizercli erargs.enemizercli = args.enemizercli
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
for fname, yamls in weights_cache.items()} for fname, yamls in weights_cache.items()}
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
if meta_weights: if meta_weights:
for category_name, category_dict in meta_weights.items(): for category_name, category_dict in meta_weights.items():
for key in category_dict: for key in category_dict:
option = get_choice(key, category_dict) option = roll_meta_option(key, category_name, category_dict)
if option is not None: if option is not None:
for player, path in player_path_cache.items(): for path in weights_cache:
for yaml in weights_cache[path]: for yaml in weights_cache[path]:
if category_name is None: if category_name is None:
yaml[key] = option for category in yaml:
if category in AutoWorldRegister.world_types and key in Options.common_options:
yaml[category][key] = option
elif category_name not in yaml: elif category_name not in yaml:
logging.warning(f"Meta: Category {category_name} is not present in {path}.") logging.warning(f"Meta: Category {category_name} is not present in {path}.")
else: else:
yaml[category_name][key] = option yaml[category_name][key] = option
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
name_counter = Counter() name_counter = Counter()
erargs.player_settings = {} erargs.player_settings = {}
@@ -387,6 +391,28 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
return weights return weights
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if not game:
return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types:
game_world = AutoWorldRegister.world_types[game]
options = ChainMap(game_world.option_definitions, Options.per_game_common_options)
if option_key in options:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return options[option_key]
if game == "A Link to the Past": # TODO wow i hate this
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
"random_sprite_on_event"}:
return get_choice(option_key, category_dict)
raise Exception(f"Error generating meta option {option_key} for {game}.")
def roll_linked_options(weights: dict) -> dict: def roll_linked_options(weights: dict) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
for option_set in weights["linked_options"]: for option_set in weights["linked_options"]:
@@ -531,7 +557,7 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default))) setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
if ret.game in AutoWorldRegister.world_types: if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.options.items(): for option_key, option in world_type.option_definitions.items():
handle_option(ret, game_weights, option_key, option) handle_option(ret, game_weights, option_key, option)
for option_key, option in Options.per_game_common_options.items(): for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option # skip setting this option if already set from common_options, defaulting to root option

View File

@@ -217,9 +217,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info("Running Item Plando") logger.info("Running Item Plando")
for item in world.itempool:
item.world = world
distribute_planned(world) distribute_planned(world)
logger.info('Running Pre Main Fill.') logger.info('Running Pre Main Fill.')
@@ -426,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
zipfilename = output_path(f"AP_{world.seed_name}.zip") zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f'Creating final archive at {zipfilename}.') logger.info(f"Creating final archive at {zipfilename}")
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf: compresslevel=9) as zf:
for file in os.scandir(temp_dir): for file in os.scandir(temp_dir):

View File

@@ -30,13 +30,8 @@ except ImportError:
OperationalError = ConnectionError OperationalError = ConnectionError
import NetUtils import NetUtils
from worlds.AutoWorld import AutoWorldRegister
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
import Utils import Utils
from Utils import get_item_name_from_id, get_location_name_from_id, \ from Utils import version_tuple, restricted_loads, Version
version_tuple, restricted_loads, Version
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
SlotType SlotType
@@ -126,6 +121,11 @@ class Context:
stored_data: typing.Dict[str, object] stored_data: typing.Dict[str, object]
stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] stored_data_notification_clients: typing.Dict[str, typing.Set[Client]]
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
forced_auto_forfeits: typing.Dict[str, bool]
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled", hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
@@ -190,8 +190,43 @@ class Context:
self.stored_data = {} self.stored_data = {}
self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet)
# General networking # init empty to satisfy linter, I suppose
self.gamespackage = {}
self.item_name_groups = {}
self.all_item_and_group_names = {}
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
self.non_hintable_names = {}
self._load_game_data()
self._init_game_data()
# Datapackage retrieval
def _load_game_data(self):
import worlds
self.gamespackage = worlds.network_data_package["games"]
self.item_name_groups = {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit
self.non_hintable_names[world_name] = world.hint_blacklist
def _init_game_data(self):
for game_name, game_package in self.gamespackage.items():
for item_name, item_id in game_package["item_name_to_id"].items():
self.item_names[item_id] = item_name
for location_name, location_id in game_package["location_name_to_id"].items():
self.location_names[location_id] = location_name
self.all_item_and_group_names[game_name] = \
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["item_name_to_id"]
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
return self.gamespackage[game]["location_name_to_id"]
# General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
if not endpoint.socket or not endpoint.socket.open: if not endpoint.socket or not endpoint.socket.open:
return False return False
@@ -544,12 +579,12 @@ class Context:
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \ finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
f' has completed their goal.' f' has completed their goal.'
self.notify_all(finished_msg) self.notify_all(finished_msg)
if "auto" in self.forfeit_mode:
forfeit_player(self, client.team, client.slot)
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
forfeit_player(self, client.team, client.slot)
if "auto" in self.collect_mode: if "auto" in self.collect_mode:
collect_player(self, client.team, client.slot) collect_player(self, client.team, client.slot)
if "auto" in self.forfeit_mode:
forfeit_player(self, client.team, client.slot)
elif self.forced_auto_forfeits[self.games[client.slot]]:
forfeit_player(self, client.team, client.slot)
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False): def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
@@ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client):
'permissions': get_permissions(ctx), 'permissions': get_permissions(ctx),
'hint_cost': ctx.hint_cost, 'hint_cost': ctx.hint_cost,
'location_check_points': ctx.location_check_points, 'location_check_points': ctx.location_check_points,
'datapackage_version': network_data_package["version"], 'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values())
if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0,
'datapackage_versions': {game: game_data["version"] for game, game_data 'datapackage_versions': {game: game_data["version"] for game, game_data
in network_data_package["games"].items()}, in ctx.gamespackage.items()},
'seed_name': ctx.seed_name, 'seed_name': ctx.seed_name,
'time': time.time(), 'time': time.time(),
}]) }])
@@ -822,8 +858,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
send_items_to(ctx, team, target_player, new_item) send_items_to(ctx, team, target_player, new_item)
logging.info('(Team #%d) %s sent %s to %s (%s)' % ( logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id), team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id],
ctx.player_names[(team, target_player)], get_location_name_from_id(location))) ctx.player_names[(team, target_player)], ctx.location_names[location]))
info_text = json_format_send_event(new_item, target_player) info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text]) ctx.broadcast_team(team, [info_text])
@@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
ctx.save() ctx.save()
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
hints = [] hints = []
slots: typing.Set[int] = {slot} slots: typing.Set[int] = {slot}
for group_id, group in ctx.groups.items(): for group_id, group in ctx.groups.items():
if slot in group: if slot in group:
slots.add(group_id) slots.add(group_id)
seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item]
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
for finding_player, check_data in ctx.locations.items(): for finding_player, check_data in ctx.locations.items():
for location_id, (item_id, receiving_player, item_flags) in check_data.items(): for location_id, (item_id, receiving_player, item_flags) in check_data.items():
if receiving_player in slots and item_id == seeked_item_id: if receiving_player in slots and item_id == seeked_item_id:
@@ -857,7 +894,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location] seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
return collect_hint_location_id(ctx, team, slot, seeked_location) return collect_hint_location_id(ctx, team, slot, seeked_location)
@@ -874,8 +911,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
f"{lookup_any_item_id_to_name[hint.item]} is " \ f"{ctx.item_names[hint.item]} is " \
f"at {get_location_name_from_id(hint.location)} " \ f"at {ctx.location_names[hint.location]} " \
f"in {ctx.player_names[team, hint.finding_player]}'s World" f"in {ctx.player_names[team, hint.finding_player]}'s World"
if hint.entrance: if hint.entrance:
@@ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
forfeit_player(self.ctx, self.client.team, self.client.slot) forfeit_player(self.ctx, self.client.team, self.client.slot)
return True return True
elif "disabled" in self.ctx.forfeit_mode: elif "disabled" in self.ctx.forfeit_mode:
self.output( self.output("Sorry, client item releasing has been disabled on this server. "
"Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release") "You can ask the server admin for a /release")
return False return False
else: # is auto or goal else: # is auto or goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
@@ -1170,7 +1207,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if self.ctx.remaining_mode == "enabled": if self.ctx.remaining_mode == "enabled":
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids: if remaining_item_ids:
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
for item_id in remaining_item_ids)) for item_id in remaining_item_ids))
else: else:
self.output("No remaining items found.") self.output("No remaining items found.")
@@ -1183,7 +1220,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids: if remaining_item_ids:
self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id]
for item_id in remaining_item_ids)) for item_id in remaining_item_ids))
else: else:
self.output("No remaining items found.") self.output("No remaining items found.")
@@ -1199,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
if locations: if locations:
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations] texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations]
texts.append(f"Found {len(locations)} missing location checks") texts.append(f"Found {len(locations)} missing location checks")
self.ctx.notify_client_multiple(self.client, texts) self.ctx.notify_client_multiple(self.client, texts)
else: else:
@@ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
if locations: if locations:
texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations] texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations]
texts.append(f"Found {len(locations)} done location checks") texts.append(f"Found {len(locations)} done location checks")
self.ctx.notify_client_multiple(self.client, texts) self.ctx.notify_client_multiple(self.client, texts)
else: else:
@@ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_getitem(self, item_name: str) -> bool: def _cmd_getitem(self, item_name: str) -> bool:
"""Cheat in an item, if it is enabled on this server""" """Cheat in an item, if it is enabled on this server"""
if self.ctx.item_cheat: if self.ctx.item_cheat:
world = proxy_worlds[self.ctx.games[self.client.slot]] names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot])
item_name, usable, response = get_intended_text(item_name, item_name, usable, response = get_intended_text(
world.item_names) item_name,
names
)
if usable: if usable:
new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot) new_item = NetworkItem(names[item_name], -1, self.client.slot)
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item) get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item) get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
self.ctx.notify_all( self.ctx.notify_all(
@@ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
f"You have {points_available} points.") f"You have {points_available} points.")
return True return True
else: else:
world = proxy_worlds[self.ctx.games[self.client.slot]] game = self.ctx.games[self.client.slot]
names = world.location_names if for_location else world.all_item_and_group_names names = self.ctx.location_names_for_game(game) \
if for_location else \
self.ctx.all_item_and_group_names[game]
hint_name, usable, response = get_intended_text(input_text, hint_name, usable, response = get_intended_text(input_text,
names) names)
if usable: if usable:
if hint_name in world.hint_blacklist: if hint_name in self.ctx.non_hintable_names[game]:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
hints = [] hints = []
elif not for_location and hint_name in world.item_name_groups: # item group name elif not for_location and hint_name in self.ctx.item_name_groups[game]: # item group name
hints = [] hints = []
for item in world.item_name_groups[hint_name]: for item_name in self.ctx.item_name_groups[game][hint_name]:
if item in world.item_name_to_id: # ensure item has an ID if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item)) hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
elif not for_location and hint_name in world.item_names: # item name elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
else: # location name else: # location name
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
@@ -1346,12 +1387,12 @@ class ClientMessageProcessor(CommonCommandProcessor):
return False return False
@mark_raw @mark_raw
def _cmd_hint(self, item: str = "") -> bool: def _cmd_hint(self, item_name: str = "") -> bool:
"""Use !hint {item_name}, """Use !hint {item_name},
for example !hint Lamp to get a spoiler peek for that item. for example !hint Lamp to get a spoiler peek for that item.
If hint costs are on, this will only give you one new result, If hint costs are on, this will only give you one new result,
you can rerun the command to get more in that case.""" you can rerun the command to get more in that case."""
return self.get_hints(item) return self.get_hints(item_name)
@mark_raw @mark_raw
def _cmd_hint_location(self, location: str = "") -> bool: def _cmd_hint_location(self, location: str = "") -> bool:
@@ -1477,23 +1518,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
elif cmd == "GetDataPackage": elif cmd == "GetDataPackage":
exclusions = args.get("exclusions", []) exclusions = args.get("exclusions", [])
if "games" in args: if "games" in args:
games = {name: game_data for name, game_data in network_data_package["games"].items() games = {name: game_data for name, game_data in ctx.gamespackage.items()
if name in set(args.get("games", []))} if name in set(args.get("games", []))}
await ctx.send_msgs(client, [{"cmd": "DataPackage", await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": {"games": games}}]) "data": {"games": games}}])
# TODO: remove exclusions behaviour around 0.5.0 # TODO: remove exclusions behaviour around 0.5.0
elif exclusions: elif exclusions:
exclusions = set(exclusions) exclusions = set(exclusions)
games = {name: game_data for name, game_data in network_data_package["games"].items() games = {name: game_data for name, game_data in ctx.gamespackage.items()
if name not in exclusions} if name not in exclusions}
package = network_data_package.copy()
package["games"] = games package = {"games": games}
await ctx.send_msgs(client, [{"cmd": "DataPackage", await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": package}]) "data": package}])
else: else:
await ctx.send_msgs(client, [{"cmd": "DataPackage", await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": network_data_package}]) "data": {"games": ctx.gamespackage}}])
elif client.auth: elif client.auth:
if cmd == "ConnectUpdate": if cmd == "ConnectUpdate":
@@ -1549,7 +1590,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
create_as_hint: int = int(args.get("create_as_hint", 0)) create_as_hint: int = int(args.get("create_as_hint", 0))
hints = [] hints = []
for location in args["locations"]: for location in args["locations"]:
if type(location) is not int or location not in lookup_any_location_id_to_name: if type(location) is not int:
await ctx.send_msgs(client, await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
"original_cmd": cmd}]) "original_cmd": cmd}])
@@ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor):
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable: if usable:
team, slot = self.ctx.player_name_lookup[seeked_player] team, slot = self.ctx.player_name_lookup[seeked_player]
item = " ".join(item_name) item_name = " ".join(item_name)
world = proxy_worlds[self.ctx.games[slot]] names = self.ctx.item_names_for_game(self.ctx.games[slot])
item, usable, response = get_intended_text(item, world.item_names) item_name, usable, response = get_intended_text(item_name, names)
if usable: if usable:
amount: int = int(amount) amount: int = int(amount)
new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))] new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))]
send_items_to(self.ctx, team, slot, *new_items) send_items_to(self.ctx, team, slot, *new_items)
send_new_items(self.ctx) send_new_items(self.ctx)
self.ctx.notify_all( self.ctx.notify_all(
'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') + 'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') +
f'"{item}" to {self.ctx.get_aliased_name(team, slot)}') f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}')
return True return True
else: else:
self.output(response) self.output(response)
@@ -1787,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
"""Sends an item to the specified player""" """Sends an item to the specified player"""
return self._cmd_send_multiple(1, player_name, *item_name) return self._cmd_send_multiple(1, player_name, *item_name)
def _cmd_hint(self, player_name: str, *item: str) -> bool: def _cmd_hint(self, player_name: str, *item_name: str) -> bool:
"""Send out a hint for a player's item to their team""" """Send out a hint for a player's item to their team"""
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable: if usable:
team, slot = self.ctx.player_name_lookup[seeked_player] team, slot = self.ctx.player_name_lookup[seeked_player]
item = " ".join(item) item_name = " ".join(item_name)
world = proxy_worlds[self.ctx.games[slot]] game = self.ctx.games[slot]
item, usable, response = get_intended_text(item, world.all_item_and_group_names) item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
if usable: if usable:
if item in world.item_name_groups: if item_name in self.ctx.item_name_groups[game]:
hints = [] hints = []
for item in world.item_name_groups[item]: for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
if item in world.item_name_to_id: # ensure item has an ID if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
hints.extend(collect_hints(self.ctx, team, slot, item)) hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
else: # item name else: # item name
hints = collect_hints(self.ctx, team, slot, item) hints = collect_hints(self.ctx, team, slot, item_name)
if hints: if hints:
notify_hints(self.ctx, team, hints) notify_hints(self.ctx, team, hints)
@@ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(response) self.output(response)
return False return False
def _cmd_hint_location(self, player_name: str, *location: str) -> bool: def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool:
"""Send out a hint for a player's location to their team""" """Send out a hint for a player's location to their team"""
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable: if usable:
team, slot = self.ctx.player_name_lookup[seeked_player] team, slot = self.ctx.player_name_lookup[seeked_player]
item = " ".join(location) location_name = " ".join(location_name)
world = proxy_worlds[self.ctx.games[slot]] location_name, usable, response = get_intended_text(location_name,
item, usable, response = get_intended_text(item, world.location_names) self.ctx.location_names_for_game(self.ctx.games[slot]))
if usable: if usable:
hints = collect_hint_location_name(self.ctx, team, slot, item) hints = collect_hint_location_name(self.ctx, team, slot, location_name)
if hints: if hints:
notify_hints(self.ctx, team, hints) notify_hints(self.ctx, team, hints)
else: else:

View File

@@ -270,7 +270,7 @@ class RawJSONtoTextParser(JSONtoTextParser):
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47} 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
def color_code(*args): def color_code(*args):

View File

@@ -44,11 +44,38 @@ nest_asyncio.apply()
class StarcraftClientProcessor(ClientCommandProcessor): class StarcraftClientProcessor(ClientCommandProcessor):
ctx: SC2Context ctx: SC2Context
def _cmd_difficulty(self, difficulty: str = "") -> bool:
"""Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal"""
options = difficulty.split()
num_options = len(options)
difficulty_choice = options[0].lower()
if num_options > 0:
if difficulty_choice == "casual":
self.ctx.difficulty_override = 0
elif difficulty_choice == "normal":
self.ctx.difficulty_override = 1
elif difficulty_choice == "hard":
self.ctx.difficulty_override = 2
elif difficulty_choice == "brutal":
self.ctx.difficulty_override = 3
else:
self.output("Unable to parse difficulty '" + options[0] + "'")
return False
self.output("Difficulty set to " + options[0])
return True
else:
self.output("Difficulty needs to be specified in the command.")
return False
def _cmd_disable_mission_check(self) -> bool: def _cmd_disable_mission_check(self) -> bool:
"""Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play
the next mission in a chain the other player is doing.""" the next mission in a chain the other player is doing."""
self.ctx.missions_unlocked = True self.ctx.missions_unlocked = True
sc2_logger.info("Mission check has been disabled") sc2_logger.info("Mission check has been disabled")
return True
def _cmd_play(self, mission_id: str = "") -> bool: def _cmd_play(self, mission_id: str = "") -> bool:
"""Start a Starcraft 2 mission""" """Start a Starcraft 2 mission"""
@@ -64,6 +91,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
else: else:
sc2_logger.info( sc2_logger.info(
"Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.") "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.")
return False
return True return True
@@ -108,6 +136,7 @@ class SC2Context(CommonContext):
missions_unlocked = False missions_unlocked = False
current_tooltip = None current_tooltip = None
last_loc_list = None last_loc_list = None
difficulty_override = -1
async def server_auth(self, password_requested: bool = False): async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password: if password_requested and not self.password:
@@ -470,7 +499,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
game_state = 0 game_state = 0
if iteration == 0: if iteration == 0:
start_items = calculate_items(self.ctx.items_received) start_items = calculate_items(self.ctx.items_received)
difficulty = calc_difficulty(self.ctx.difficulty) if self.ctx.difficulty_override >= 0:
difficulty = calc_difficulty(self.ctx.difficulty_override)
else:
difficulty = calc_difficulty(self.ctx.difficulty)
await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format( await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format(
difficulty, difficulty,
start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], start_items[0], start_items[1], start_items[2], start_items[3], start_items[4],

176
Utils.py
View File

@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import shutil
import typing import typing
import builtins import builtins
import os import os
@@ -12,12 +11,18 @@ import io
import collections import collections
import importlib import importlib
import logging import logging
import decimal from yaml import load, load_all, dump, SafeLoader
try:
from yaml import CLoader as UnsafeLoader
from yaml import CDumper as Dumper
except ImportError:
from yaml import Loader as UnsafeLoader
from yaml import Dumper
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from tkinter import Tk import tkinter
else: import pathlib
Tk = typing.Any
def tuplize_version(version: str) -> Version: def tuplize_version(version: str) -> Version:
@@ -33,18 +38,10 @@ class Version(typing.NamedTuple):
__version__ = "0.3.4" __version__ = "0.3.4"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith('linux') is_linux = sys.platform.startswith("linux")
is_macos = sys.platform == 'darwin' is_macos = sys.platform == "darwin"
is_windows = sys.platform in ("win32", "cygwin", "msys") is_windows = sys.platform in ("win32", "cygwin", "msys")
import jellyfish
from yaml import load, load_all, dump, SafeLoader
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
def int16_as_bytes(value: int) -> typing.List[int]: def int16_as_bytes(value: int) -> typing.List[int]:
value = value & 0xFFFF value = value & 0xFFFF
@@ -125,17 +122,18 @@ def home_path(*path: str) -> str:
def user_path(*path: str) -> str: def user_path(*path: str) -> str:
"""Returns either local_path or home_path based on write permissions.""" """Returns either local_path or home_path based on write permissions."""
if hasattr(user_path, 'cached_path'): if hasattr(user_path, "cached_path"):
pass pass
elif os.access(local_path(), os.W_OK): elif os.access(local_path(), os.W_OK):
user_path.cached_path = local_path() user_path.cached_path = local_path()
else: else:
user_path.cached_path = home_path() user_path.cached_path = home_path()
# populate home from local - TODO: upgrade feature # populate home from local - TODO: upgrade feature
if user_path.cached_path != local_path() and not os.path.exists(user_path('host.yaml')): if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
for dn in ('Players', 'data/sprites'): import shutil
for dn in ("Players", "data/sprites"):
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True) shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
for fn in ('manifest.json', 'host.yaml'): for fn in ("manifest.json", "host.yaml"):
shutil.copy2(local_path(fn), user_path(fn)) shutil.copy2(local_path(fn), user_path(fn))
return os.path.join(user_path.cached_path, *path) return os.path.join(user_path.cached_path, *path)
@@ -150,11 +148,12 @@ def output_path(*path: str):
return path return path
def open_file(filename): def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
if sys.platform == 'win32': if is_windows:
os.startfile(filename) os.startfile(filename)
else: else:
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open' from shutil import which
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
subprocess.call([open_command, filename]) subprocess.call([open_command, filename])
@@ -173,7 +172,9 @@ class UniqueKeyLoader(SafeLoader):
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader) parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader) parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader)
unsafe_parse_yaml = functools.partial(load, Loader=Loader) unsafe_parse_yaml = functools.partial(load, Loader=UnsafeLoader)
del load, load_all # should not be used. don't leak their names
def get_cert_none_ssl_context(): def get_cert_none_ssl_context():
@@ -191,11 +192,12 @@ def get_public_ipv4() -> str:
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context() ctx = get_cert_none_ssl_context()
try: try:
ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip() ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip()
except Exception as e: except Exception as e:
# noinspection PyBroadException
try: try:
ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip() ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip()
except: except Exception:
logging.exception(e) logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out pass # we could be offline, in a local game, so no point in erroring out
return ip return ip
@@ -208,7 +210,7 @@ def get_public_ipv6() -> str:
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context() ctx = get_cert_none_ssl_context()
try: try:
ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip() ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip()
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
pass # we could be offline, in a local game, or ipv6 may not be available pass # we could be offline, in a local game, or ipv6 may not be available
@@ -309,33 +311,19 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
@cache_argsless @cache_argsless
def get_options() -> dict: def get_options() -> dict:
if not hasattr(get_options, "options"): filenames = ("options.yaml", "host.yaml")
filenames = ("options.yaml", "host.yaml") locations = []
locations = [] if os.path.join(os.getcwd()) != local_path():
if os.path.join(os.getcwd()) != local_path(): locations += filenames # use files from cwd only if it's not the local_path
locations += filenames # use files from cwd only if it's not the local_path locations += [user_path(filename) for filename in filenames]
locations += [user_path(filename) for filename in filenames]
for location in locations: for location in locations:
if os.path.exists(location): if os.path.exists(location):
with open(location) as f: with open(location) as f:
options = parse_yaml(f.read()) options = parse_yaml(f.read())
return update_options(get_default_options(), options, location, list())
get_options.options = update_options(get_default_options(), options, location, list()) raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
break
else:
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
return get_options.options
def get_item_name_from_id(code: int) -> str:
from worlds import lookup_any_item_id_to_name
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
def get_location_name_from_id(code: int) -> str:
from worlds import lookup_any_location_id_to_name
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
def persistent_store(category: str, key: typing.Any, value: typing.Any): def persistent_store(category: str, key: typing.Any, value: typing.Any):
@@ -344,10 +332,10 @@ def persistent_store(category: str, key: typing.Any, value: typing.Any):
category = storage.setdefault(category, {}) category = storage.setdefault(category, {})
category[key] = value category[key] = value
with open(path, "wt") as f: with open(path, "wt") as f:
f.write(dump(storage)) f.write(dump(storage, Dumper=Dumper))
def persistent_load() -> typing.Dict[dict]: def persistent_load() -> typing.Dict[str, dict]:
storage = getattr(persistent_load, "storage", None) storage = getattr(persistent_load, "storage", None)
if storage: if storage:
return storage return storage
@@ -365,8 +353,8 @@ def persistent_load() -> typing.Dict[dict]:
return storage return storage
def get_adjuster_settings(gameName: str): def get_adjuster_settings(game_name: str):
adjuster_settings = persistent_load().get("adjuster", {}).get(gameName, {}) adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
return adjuster_settings return adjuster_settings
@@ -382,10 +370,10 @@ def get_unique_identifier():
return uuid return uuid
safe_builtins = { safe_builtins = frozenset((
'set', 'set',
'frozenset', 'frozenset',
} ))
class RestrictedUnpickler(pickle.Unpickler): class RestrictedUnpickler(pickle.Unpickler):
@@ -413,8 +401,7 @@ class RestrictedUnpickler(pickle.Unpickler):
if issubclass(obj, self.options_module.Option): if issubclass(obj, self.options_module.Option):
return obj return obj
# Forbid everything else. # Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
(module, name))
def restricted_loads(s): def restricted_loads(s):
@@ -423,6 +410,9 @@ def restricted_loads(s):
class KeyedDefaultDict(collections.defaultdict): class KeyedDefaultDict(collections.defaultdict):
"""defaultdict variant that uses the missing key as argument to default_factory"""
default_factory: typing.Callable[[typing.Any], typing.Any]
def __missing__(self, key): def __missing__(self, key):
self[key] = value = self.default_factory(key) self[key] = value = self.default_factory(key)
return value return value
@@ -432,6 +422,10 @@ def get_text_between(text: str, start: str, end: str) -> str:
return text[text.index(start) + len(start): text.rindex(end)] return text[text.index(start) + len(start): text.rindex(end)]
def get_text_after(text: str, start: str) -> str:
return text[text.index(start) + len(start):]
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG} loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
@@ -493,11 +487,11 @@ def stream_input(stream, queue):
return thread return thread
def tkinter_center_window(window: Tk): def tkinter_center_window(window: "tkinter.Tk") -> None:
window.update() window.update()
xPos = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2) x = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
yPos = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2) y = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
window.geometry("+{}+{}".format(xPos, yPos)) window.geometry(f"+{x}+{y}")
class VersionException(Exception): class VersionException(Exception):
@@ -514,24 +508,27 @@ def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
# noinspection PyPep8Naming # noinspection PyPep8Naming
def format_SI_prefix(value, power=1000, power_labels=('', 'k', 'M', 'G', 'T', "P", "E", "Z", "Y")) -> str: def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P", "E", "Z", "Y")) -> str:
"""Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix""" """Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix"""
import decimal
n = 0 n = 0
value = decimal.Decimal(value) value = decimal.Decimal(value)
while value >= power: limit = power - decimal.Decimal("0.005")
while value >= limit:
value /= power value /= power
n += 1 n += 1
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}" return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
def get_fuzzy_ratio(word1: str, word2: str) -> float:
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
/ max(len(word1), len(word2)))
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
-> typing.List[typing.Tuple[str, int]]: -> typing.List[typing.Tuple[str, int]]:
import jellyfish
def get_fuzzy_ratio(word1: str, word2: str) -> float:
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
/ max(len(word1), len(word2)))
limit: int = limit if limit else len(wordlist) limit: int = limit if limit else len(wordlist)
return list( return list(
map( map(
@@ -549,18 +546,19 @@ def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: ty
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
-> typing.Optional[str]: -> typing.Optional[str]:
def run(*args: str): def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_linux: if is_linux:
# prefer native dialog # prefer native dialog
kdialog = shutil.which('kdialog') from shutil import which
kdialog = which("kdialog")
if kdialog: if kdialog:
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes)) k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
return run(kdialog, f'--title={title}', '--getopenfilename', '.', k_filters) return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
zenity = shutil.which('zenity') zenity = which("zenity")
if zenity: if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes) z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
return run(zenity, f'--title={title}', '--file-selection', *z_filters) return run(zenity, f"--title={title}", "--file-selection", *z_filters)
# fall back to tk # fall back to tk
try: try:
@@ -578,10 +576,10 @@ def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typin
def messagebox(title: str, text: str, error: bool = False) -> None: def messagebox(title: str, text: str, error: bool = False) -> None:
def run(*args: str): def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split('\n', 1)[0] or None return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
def is_kivy_running(): def is_kivy_running():
if 'kivy' in sys.modules: if "kivy" in sys.modules:
from kivy.app import App from kivy.app import App
return App.get_running_app() is not None return App.get_running_app() is not None
return False return False
@@ -591,14 +589,15 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
MessageBox(title, text, error).open() MessageBox(title, text, error).open()
return return
if is_linux and not 'tkinter' in sys.modules: if is_linux and "tkinter" not in sys.modules:
# prefer native dialog # prefer native dialog
kdialog = shutil.which('kdialog') from shutil import which
kdialog = which("kdialog")
if kdialog: if kdialog:
return run(kdialog, f'--title={title}', '--error' if error else '--msgbox', text) return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
zenity = shutil.which('zenity') zenity = which("zenity")
if zenity: if zenity:
return run(zenity, f'--title={title}', f'--text={text}', '--error' if error else '--info') return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
# fall back to tk # fall back to tk
try: try:
@@ -613,3 +612,14 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
root.withdraw() root.withdraw()
showerror(title, text) if error else showinfo(title, text) showerror(title, text) if error else showinfo(title, text)
root.update() root.update()
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
def sorter(element: str) -> str:
parts = element.split(maxsplit=1)
if parts[0].lower() in ignore:
return parts[1]
else:
return element
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))

View File

@@ -14,7 +14,7 @@ import Utils
Utils.local_path.cached_path = os.path.dirname(__file__) Utils.local_path.cached_path = os.path.dirname(__file__)
from WebHostLib import app as raw_app from WebHostLib import register, app as raw_app
from waitress import serve from waitress import serve
from WebHostLib.models import db from WebHostLib.models import db
@@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files from WebHostLib.options import create as create_options_files
from worlds.AutoWorld import AutoWorldRegister
configpath = os.path.abspath("config.yaml") configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home if not os.path.exists(configpath): # fall back to config.yaml in home
configpath = os.path.abspath(Utils.user_path('config.yaml')) configpath = os.path.abspath(Utils.user_path('config.yaml'))
def get_app(): def get_app():
register()
app = raw_app app = raw_app
if os.path.exists(configpath): if os.path.exists(configpath):
import yaml import yaml
@@ -43,19 +42,39 @@ def get_app():
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]: def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
import json import json
import shutil import shutil
import zipfile
zfile: zipfile.ZipInfo
from worlds.AutoWorld import AutoWorldRegister
worlds = {} worlds = {}
data = [] data = []
for game, world in AutoWorldRegister.world_types.items(): for game, world in AutoWorldRegister.world_types.items():
if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'): if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'):
worlds[game] = world worlds[game] = world
base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs")
for game, world in worlds.items(): for game, world in worlds.items():
# copy files from world's docs folder to the generated folder # copy files from world's docs folder to the generated folder
source_path = Utils.local_path(os.path.dirname(sys.modules[world.__module__].__file__), 'docs') target_path = os.path.join(base_target_path, game)
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", game) os.makedirs(target_path, exist_ok=True)
files = os.listdir(source_path)
for file in files: if world.zip_path:
os.makedirs(os.path.dirname(Utils.local_path(target_path, file)), exist_ok=True) zipfile_path = world.zip_path
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
assert os.path.isfile(zipfile_path), f"{zipfile_path} is not a valid file(path)."
assert zipfile.is_zipfile(zipfile_path), f"{zipfile_path} is not a valid zipfile."
with zipfile.ZipFile(zipfile_path) as zf:
for zfile in zf.infolist():
if not zfile.is_dir() and "/docs/" in zfile.filename:
zf.extract(zfile, target_path)
else:
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
files = os.listdir(source_path)
for file in files:
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
# build a json tutorial dict per game # build a json tutorial dict per game
game_data = {'gameTitle': game, 'tutorials': []} game_data = {'gameTitle': game, 'tutorials': []}
for tutorial in world.web.tutorials: for tutorial in world.web.tutorials:
@@ -85,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
for games in data: for games in data:
if 'Archipelago' in games['gameTitle']: if 'Archipelago' in games['gameTitle']:
generic_data = data.pop(data.index(games)) generic_data = data.pop(data.index(games))
sorted_data = [generic_data] + sorted(data, key=lambda entry: entry["gameTitle"].lower()) sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower())
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False) json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
return sorted_data return sorted_data

46
WebHostLib/README.md Normal file
View 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.

View File

@@ -3,13 +3,13 @@ import uuid
import base64 import base64
import socket import socket
import jinja2.exceptions
from pony.flask import Pony from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory from flask import Flask
from flask_caching import Cache from flask_caching import Cache
from flask_compress import Compress from flask_compress import Compress
from worlds.AutoWorld import AutoWorldRegister from werkzeug.routing import BaseConverter
from Utils import title_sorted
from .models import * from .models import *
UPLOAD_FOLDER = os.path.relpath('uploads') UPLOAD_FOLDER = os.path.relpath('uploads')
@@ -53,8 +53,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg"
cache = Cache(app) cache = Cache(app)
Compress(app) Compress(app)
from werkzeug.routing import BaseConverter
class B64UUIDConverter(BaseConverter): class B64UUIDConverter(BaseConverter):
@@ -68,174 +66,18 @@ class B64UUIDConverter(BaseConverter):
# short UUID # short UUID
app.url_map.converters["suuid"] = B64UUIDConverter app.url_map.converters["suuid"] = B64UUIDConverter
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
app.jinja_env.filters["title_sorted"] = title_sorted
# has automatic patch integration
import Patch
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
def get_world_theme(game_name: str): def register():
if game_name in AutoWorldRegister.world_types: """Import submodules, triggering their registering on flask routing.
return AutoWorldRegister.world_types[game_name].web.theme Note: initializes worlds subsystem."""
return 'grass' # has automatic patch integration
import Patch
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types
from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
@app.before_request app.register_blueprint(api.api_endpoints)
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
# Start Playing Page
@app.route('/start-playing')
def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
# List of supported games
@app.route('/games')
def games():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("supportedGames.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
@app.route('/tutorial/')
def tutorial_landing():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("tutorialLanding.html")
@app.route('/faq/<string:lang>/')
def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/')
def terms(lang):
return render_template("glossary.html", lang=lang)
@app.route('/seed/<suuid:seed>')
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
@app.route('/new_room/<suuid:seed>')
def new_room(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("host_room", room=room.id))
def _read_log(path: str):
if os.path.exists(path):
with open(path, encoding="utf-8-sig") as log:
yield from log
else:
yield f"Logfile {path} does not exist. " \
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if room.owner == session["_id"]:
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def host_room(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
with db_session:
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
return render_template("hostRoom.html", room=room)
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/discord')
def discord():
return redirect("https://discord.gg/archipelago")
@app.route('/datapackage')
@cache.cached()
def get_datapackge():
"""A pretty print version of /api/datapackage"""
from worlds import network_data_package
import json
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
@app.route('/index')
@app.route('/sitemap')
def get_sitemap():
available_games = []
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
available_games.append(game)
return render_template("siteMap.html", games=available_games)
from WebHostLib.customserver import run_server_process
from . import tracker, upload, landing, check, generate, downloads, api, stats # to trigger app routing picking up on it
app.register_blueprint(api.api_endpoints)

View File

@@ -32,14 +32,14 @@ def room_info(room: UUID):
@api_endpoints.route('/datapackage') @api_endpoints.route('/datapackage')
@cache.cached() @cache.cached()
def get_datapackge(): def get_datapackage():
from worlds import network_data_package from worlds import network_data_package
return network_data_package return network_data_package
@api_endpoints.route('/datapackage_version') @api_endpoints.route('/datapackage_version')
@cache.cached() @cache.cached()
def get_datapackge_versions(): def get_datapackage_versions():
from worlds import network_data_package, AutoWorldRegister from worlds import network_data_package, AutoWorldRegister
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
version_package["version"] = network_data_package["version"] version_package["version"] = network_data_package["version"]

View File

@@ -184,7 +184,7 @@ class MultiworldInstance():
logging.info(f"Spinning up {self.room_id}") logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process, process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig), args=(self.room_id, self.ponyconfig, get_static_server_data()),
name="MultiHost") name="MultiHost")
process.start() process.start()
# bind after start to prevent thread sync issues with guardian. # bind after start to prevent thread sync issues with guardian.
@@ -238,5 +238,5 @@ def run_guardian():
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
from .customserver import run_server_process from .customserver import run_server_process, get_static_server_data
from .generate import gen_game from .generate import gen_game

View File

@@ -9,12 +9,13 @@ import time
import random import random
import pickle import pickle
import logging import logging
import datetime
import Utils import Utils
from .models import * from .models import db_session, Room, select, commit, Command, db
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
class CustomClientMessageProcessor(ClientMessageProcessor): class CustomClientMessageProcessor(ClientMessageProcessor):
@@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
import MultiServer import MultiServer
MultiServer.client_message_processor = CustomClientMessageProcessor MultiServer.client_message_processor = CustomClientMessageProcessor
del (MultiServer) del MultiServer
class DBCommandProcessor(ServerCommandProcessor): class DBCommandProcessor(ServerCommandProcessor):
@@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context): class WebHostContext(Context):
def __init__(self): def __init__(self, static_server_data: dict):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
self.static_server_data = static_server_data
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2) super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
del self.static_server_data
self.main_loop = asyncio.get_running_loop() self.main_loop = asyncio.get_running_loop()
self.video = {} self.video = {}
self.tags = ["AP", "WebHost"] self.tags = ["AP", "WebHost"]
def _load_game_data(self):
for key, value in self.static_server_data.items():
setattr(self, key, value)
def listen_to_db_commands(self): def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self) cmdprocessor = DBCommandProcessor(self)
@@ -107,14 +116,32 @@ def get_random_port():
return random.randint(49152, 65535) return random.randint(49152, 65535)
def run_server_process(room_id, ponyconfig: dict): @cache_argsless
def get_static_server_data() -> dict:
import worlds
data = {
"forced_auto_forfeits": {},
"non_hintable_names": {},
"gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit
data["non_hintable_names"][world_name] = world.hint_blacklist
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict):
# establish DB connection for multidata and multisave # establish DB connection for multidata and multisave
db.bind(**ponyconfig) db.bind(**ponyconfig)
db.generate_mapping(check_tables=False) db.generate_mapping(check_tables=False)
async def main(): async def main():
Utils.init_logging(str(room_id), write_mode="a") Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext() ctx = WebHostContext(static_server_data)
ctx.load(room_id) ctx.load(room_id)
ctx.init_save() ctx.init_save()

View File

@@ -36,14 +36,14 @@ def download_patch(room_id, patch_id):
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \ fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \
f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}" f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}"
new_file.seek(0) new_file.seek(0)
return send_file(new_file, as_attachment=True, attachment_filename=fname) return send_file(new_file, as_attachment=True, download_name=fname)
else: else:
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}") patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
patch_data = BytesIO(patch_data) patch_data = BytesIO(patch_data)
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \ fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
f"{preferred_endings[patch.game]}" f"{preferred_endings[patch.game]}"
return send_file(patch_data, as_attachment=True, attachment_filename=fname) return send_file(patch_data, as_attachment=True, download_name=fname)
@app.route("/dl_spoiler/<suuid:seed_id>") @app.route("/dl_spoiler/<suuid:seed_id>")
@@ -66,7 +66,7 @@ def download_slot_file(room_id, player_id: int):
from worlds.minecraft import mc_update_output from worlds.minecraft import mc_update_output
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc" fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port) data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
return send_file(io.BytesIO(data), as_attachment=True, attachment_filename=fname) return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
elif slot_data.game == "Factorio": elif slot_data.game == "Factorio":
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf: with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
for name in zf.namelist(): for name in zf.namelist():
@@ -82,7 +82,7 @@ def download_slot_file(room_id, player_id: int):
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json" fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
else: else:
return "Game download not supported." return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname) return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
@app.route("/templates") @app.route("/templates")

173
WebHostLib/misc.py Normal file
View 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)

View File

@@ -60,7 +60,7 @@ def create():
for game_name, world in AutoWorldRegister.world_types.items(): for game_name, world in AutoWorldRegister.world_types.items():
all_options = {**Options.per_game_common_options, **world.options} all_options = {**Options.per_game_common_options, **world.option_definitions}
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=all_options, options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump, __version__=__version__, game=game_name, yaml_dump=yaml.dump,
@@ -110,7 +110,7 @@ def create():
if option.default == "random": if option.default == "random":
this_option["defaultValue"] = "random" this_option["defaultValue"] = "random"
elif hasattr(option, "range_start") and hasattr(option, "range_end"): elif issubclass(option, Options.Range):
game_options[option_name] = { game_options[option_name] = {
"type": "range", "type": "range",
"displayName": option.display_name if hasattr(option, "display_name") else option_name, "displayName": option.display_name if hasattr(option, "display_name") else option_name,
@@ -121,7 +121,7 @@ def create():
"max": option.range_end, "max": option.range_end,
} }
if hasattr(option, "special_range_names"): if issubclass(option, Options.SpecialRange):
game_options[option_name]["type"] = 'special_range' game_options[option_name]["type"] = 'special_range'
game_options[option_name]["value_names"] = {} game_options[option_name]["value_names"] = {}
for key, val in option.special_range_names.items(): for key, val in option.special_range_names.items():
@@ -141,7 +141,7 @@ def create():
"description": option.__doc__ if option.__doc__ else "Please document me!", "description": option.__doc__ if option.__doc__ else "Please document me!",
} }
elif hasattr(option, "valid_keys"): elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet):
if option.valid_keys: if option.valid_keys:
game_options[option_name] = { game_options[option_name] = {
"type": "custom-list", "type": "custom-list",

View File

@@ -18,15 +18,16 @@ from .models import Room
PLOT_WIDTH = 600 PLOT_WIDTH = 600
def get_db_data() -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]: def get_db_data(known_games: str) -> typing.Tuple[typing.Dict[str, int], typing.Dict[datetime.date, typing.Dict[str, int]]]:
games_played = defaultdict(Counter) games_played = defaultdict(Counter)
total_games = Counter() total_games = Counter()
cutoff = date.today()-timedelta(days=30) cutoff = date.today()-timedelta(days=30)
room: Room room: Room
for room in select(room for room in Room if room.creation_time >= cutoff): for room in select(room for room in Room if room.creation_time >= cutoff):
for slot in room.seed.slots: for slot in room.seed.slots:
total_games[slot.game] += 1 if slot.game in known_games:
games_played[room.creation_time.date()][slot.game] += 1 total_games[slot.game] += 1
games_played[room.creation_time.date()][slot.game] += 1
return total_games, games_played return total_games, games_played
@@ -73,10 +74,12 @@ def create_game_played_figure(all_games_data: typing.Dict[datetime.date, typing.
@app.route('/stats') @app.route('/stats')
@cache.memoize(timeout=60 * 60) # regen once per hour should be plenty @cache.memoize(timeout=60 * 60) # regen once per hour should be plenty
def stats(): def stats():
from worlds import network_data_package
known_games = set(network_data_package["games"])
plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date", plot = figure(title="Games Played Per Day", x_axis_type='datetime', x_axis_label="Date",
y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500) y_axis_label="Games Played", sizing_mode="scale_both", width=PLOT_WIDTH, height=500)
total_games, games_played = get_db_data() total_games, games_played = get_db_data(known_games)
days = sorted(games_played) days = sorted(games_played)
color_palette = get_color_palette(len(total_games)) color_palette = get_color_palette(len(total_games))

View File

@@ -2,6 +2,7 @@
{% import "macros.html" as macros %} {% import "macros.html" as macros %}
{% block head %} {% block head %}
<title>Multiworld {{ room.id|suuid }}</title> <title>Multiworld {{ room.id|suuid }}</title>
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
{% endblock %} {% endblock %}

View File

@@ -10,7 +10,8 @@
{% include 'header/oceanHeader.html' %} {% include 'header/oceanHeader.html' %}
<div id="games" class="markdown"> <div id="games" class="markdown">
<h1>Currently Supported Games</h1> <h1>Currently Supported Games</h1>
{% for game_name, world in worlds.items() | sort(attribute=0) %} {% for game_name in worlds | title_sorted %}
{% set world = worlds[game_name] %}
<h2>{{ game_name }}</h2> <h2>{{ game_name }}</h2>
<p> <p>
{{ world.__doc__ | default("No description provided.", true) }}<br /> {{ world.__doc__ | default("No description provided.", true) }}<br />

View File

@@ -11,7 +11,7 @@ from worlds.alttp import Items
from WebHostLib import app, cache, Room from WebHostLib import app, cache, Room
from Utils import restricted_loads from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
from MultiServer import get_item_name_from_id, Context from MultiServer import Context
from NetUtils import SlotType from NetUtils import SlotType
alttp_icons = { alttp_icons = {
@@ -987,10 +987,10 @@ def getTracker(tracker: UUID):
if game_state == 30: if game_state == 30:
inventory[team][player][106] = 1 # Triforce inventory[team][player][106] = 1 # Triforce
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups} player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1) if playernumber not in groups} player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
for loc_data in locations.values(): for loc_data in locations.values():
for values in loc_data.values(): for values in loc_data.values():
item_id, item_player, flags = values item_id, item_player, flags = values
if item_id in ids_big_key: if item_id in ids_big_key:
@@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID):
for (team, player), data in multisave.get("video", []): for (team, player), data in multisave.get("video", []):
video[(team, player)] = data video[(team, player)] = data
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

View File

@@ -69,6 +69,12 @@ flowchart LR
end end
SNI <-- Various, depending on SNES device --> SMZ SNI <-- Various, depending on SNES device --> SMZ
%% Donkey Kong Country 3
subgraph Donkey Kong Country 3
DK3[SNES]
end
SNI <-- Various, depending on SNES device --> DK3
%% Native Clients or Games %% Native Clients or Games
%% Games or clients which compile to native or which the client is integrated in the game. %% Games or clients which compile to native or which the client is integrated in the game.
subgraph "Native" subgraph "Native"
@@ -82,10 +88,12 @@ flowchart LR
MT[Meritous] MT[Meritous]
TW[The Witness] TW[The Witness]
SA2B[Sonic Adventure 2: Battle] SA2B[Sonic Adventure 2: Battle]
DS3[Dark Souls 3]
APCLIENTPP <--> SOE APCLIENTPP <--> SOE
APCLIENTPP <--> MT APCLIENTPP <--> MT
APCLIENTPP <-- The Witness Randomizer --> TW APCLIENTPP <-- The Witness Randomizer --> TW
APCLIENTPP <--> DS3
APCPP <--> SM64 APCPP <--> SM64
APCPP <--> V6 APCPP <--> V6
APCPP <--> SA2B APCPP <--> SA2B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -501,7 +501,7 @@ Color options:
* green_bg * green_bg
* yellow_bg * yellow_bg
* blue_bg * blue_bg
* purple_bg * magenta_bg
* cyan_bg * cyan_bg
* white_bg * white_bg

View File

@@ -86,7 +86,7 @@ inside a World object.
Players provide customized settings for their World in the form of yamls. Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.world.<option_name>[self.player]`. A dict Those are accessible through `self.world.<option_name>[self.player]`. A dict
of valid options has to be provided in `self.options`. Options are automatically of valid options has to be provided in `self.option_definitions`. Options are automatically
added to the `World` object for easy access. added to the `World` object for easy access.
### World Options ### World Options
@@ -252,7 +252,7 @@ to describe it and a `display_name` property for display on the website and in
spoiler logs. spoiler logs.
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
assigned to the world under `self.options`. assigned to the world under `self.option_definitions`.
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`. Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
For more see `Options.py` in AP's base directory. For more see `Options.py` in AP's base directory.
@@ -328,7 +328,7 @@ from .Options import mygame_options # import the options dict
class MyGameWorld(World): class MyGameWorld(World):
#... #...
options = mygame_options # assign the options dict to the world option_definitions = mygame_options # assign the options dict to the world
#... #...
``` ```
@@ -365,7 +365,7 @@ class MyGameLocation(Location): # or from Locations import MyGameLocation
class MyGameWorld(World): class MyGameWorld(World):
"""Insert description of the world/game here.""" """Insert description of the world/game here."""
game: str = "My Game" # name of the game/world game: str = "My Game" # name of the game/world
options = mygame_options # options the player can set option_definitions = mygame_options # options the player can set
topology_present: bool = True # show path to required location checks in spoiler topology_present: bool = True # show path to required location checks in spoiler
remote_items: bool = False # True if all items come from the server remote_items: bool = False # True if all items come from the server
remote_start_inventory: bool = False # True if start inventory comes from the server remote_start_inventory: bool = False # True if start inventory comes from the server

View File

@@ -17,7 +17,7 @@ from Launcher import components, icon_paths
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
import subprocess import subprocess
import pkg_resources import pkg_resources
requirement = 'cx-Freeze==6.10' requirement = 'cx-Freeze>=6.11'
try: try:
pkg_resources.require(requirement) pkg_resources.require(requirement)
import cx_Freeze import cx_Freeze
@@ -70,7 +70,7 @@ def _threaded_hash(filepath):
# cx_Freeze's build command runs other commands. Override to accept --yes and store that. # cx_Freeze's build command runs other commands. Override to accept --yes and store that.
class BuildCommand(cx_Freeze.dist.build): class BuildCommand(cx_Freeze.command.build.Build):
user_options = [ user_options = [
('yes', 'y', 'Answer "yes" to all questions.'), ('yes', 'y', 'Answer "yes" to all questions.'),
] ]
@@ -87,8 +87,8 @@ class BuildCommand(cx_Freeze.dist.build):
# Override cx_Freeze's build_exe command for pre and post build steps # Override cx_Freeze's build_exe command for pre and post build steps
class BuildExeCommand(cx_Freeze.dist.build_exe): class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
user_options = cx_Freeze.dist.build_exe.user_options + [ user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
('yes', 'y', 'Answer "yes" to all questions.'), ('yes', 'y', 'Answer "yes" to all questions.'),
('extra-data=', None, 'Additional files to add.'), ('extra-data=', None, 'Additional files to add.'),
] ]

View File

@@ -16,7 +16,7 @@ class TestDungeon(unittest.TestCase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
args = Namespace() args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options() self.world.set_default_common_options()

View File

@@ -49,8 +49,7 @@ class PlayerDefinition(object):
region_name = "player" + str(self.id) + region_tag region_name = "player" + str(self.id) + region_tag
region = Region("player" + str(self.id) + region_tag, RegionType.Generic, region = Region("player" + str(self.id) + region_tag, RegionType.Generic,
"Region Hint", self.id, self.world) "Region Hint", self.id, self.world)
self.locations += generate_locations(size, self.locations += generate_locations(size, self.id, None, region, region_tag)
self.id, None, region, region_tag)
entrance = Entrance(self.id, region_name + "_entrance", parent) entrance = Entrance(self.id, region_name + "_entrance", parent)
parent.exits.append(entrance) parent.exits.append(entrance)

View File

@@ -12,7 +12,7 @@ def setup_default_world(world_type) -> MultiWorld:
world.player_name = {1: "Tester"} world.player_name = {1: "Tester"}
world.set_seed() world.set_seed()
args = Namespace() args = Namespace()
for name, option in world_type.options.items(): for name, option in world_type.option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
world.set_options(args) world.set_options(args)
world.set_default_common_options() world.set_default_common_options()

View File

@@ -16,7 +16,7 @@ class TestInverted(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
args = Namespace() args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options() self.world.set_default_common_options()

View File

@@ -17,7 +17,7 @@ class TestInvertedBombRules(unittest.TestCase):
self.world = MultiWorld(1) self.world = MultiWorld(1)
self.world.mode[1] = "inverted" self.world.mode[1] = "inverted"
args = Namespace args = Namespace
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options() self.world.set_default_common_options()

View File

@@ -17,7 +17,7 @@ class TestInvertedMinor(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
args = Namespace() args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options() self.world.set_default_common_options()

View File

@@ -18,7 +18,7 @@ class TestInvertedOWG(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
args = Namespace() args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options() self.world.set_default_common_options()

View File

@@ -17,7 +17,7 @@ class TestMinor(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
args = Namespace() args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options() self.world.set_default_common_options()

View File

@@ -18,7 +18,7 @@ class TestVanillaOWG(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
args = Namespace() args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options() self.world.set_default_common_options()

View 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
View File

View File

@@ -16,7 +16,7 @@ class TestVanilla(TestBase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
args = Namespace() args = Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items(): for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].option_definitions.items():
setattr(args, name, {1: option.from_any(option.default)}) setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args) self.world.set_options(args)
self.world.set_default_common_options() self.world.set_default_common_options()

View File

@@ -2,10 +2,14 @@ from __future__ import annotations
import logging import logging
import sys import sys
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, NamedTuple import pathlib
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING
from BaseClasses import MultiWorld, Item, CollectionState, Location, Tutorial
from Options import Option from Options import Option
from BaseClasses import CollectionState
if TYPE_CHECKING:
from BaseClasses import MultiWorld, Item, Location, Tutorial
class AutoWorldRegister(type): class AutoWorldRegister(type):
@@ -41,14 +45,18 @@ class AutoWorldRegister(type):
# construct class # construct class
new_class = super().__new__(mcs, name, bases, dct) new_class = super().__new__(mcs, name, bases, dct)
if "game" in dct: if "game" in dct:
if dct["game"] in AutoWorldRegister.world_types:
raise RuntimeError(f"""Game {dct["game"]} already registered.""")
AutoWorldRegister.world_types[dct["game"]] = new_class AutoWorldRegister.world_types[dct["game"]] = new_class
new_class.__file__ = sys.modules[new_class.__module__].__file__ new_class.__file__ = sys.modules[new_class.__module__].__file__
if ".apworld" in new_class.__file__:
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
return new_class return new_class
class AutoLogicRegister(type): class AutoLogicRegister(type):
def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister: def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoLogicRegister:
new_class = super().__new__(cls, name, bases, dct) new_class = super().__new__(mcs, name, bases, dct)
function: Callable[..., Any] function: Callable[..., Any]
for item_name, function in dct.items(): for item_name, function in dct.items():
if item_name == "copy_mixin": if item_name == "copy_mixin":
@@ -62,12 +70,12 @@ class AutoLogicRegister(type):
return new_class return new_class
def call_single(world: MultiWorld, method_name: str, player: int, *args: Any) -> Any: def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
method = getattr(world.worlds[player], method_name) method = getattr(world.worlds[player], method_name)
return method(*args) return method(*args)
def call_all(world: MultiWorld, method_name: str, *args: Any) -> None: def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None:
world_types: Set[AutoWorldRegister] = set() world_types: Set[AutoWorldRegister] = set()
for player in world.player_ids: for player in world.player_ids:
world_types.add(world.worlds[player].__class__) world_types.add(world.worlds[player].__class__)
@@ -79,7 +87,7 @@ def call_all(world: MultiWorld, method_name: str, *args: Any) -> None:
stage_callable(world, *args) stage_callable(world, *args)
def call_stage(world: MultiWorld, method_name: str, *args: Any) -> None: def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None:
world_types = {world.worlds[player].__class__ for player in world.player_ids} world_types = {world.worlds[player].__class__ for player in world.player_ids}
for world_type in world_types: for world_type in world_types:
stage_callable = getattr(world_type, f"stage_{method_name}", None) stage_callable = getattr(world_type, f"stage_{method_name}", None)
@@ -97,7 +105,7 @@ class WebWorld:
# docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial # docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial
# class is to be used for one guide. # class is to be used for one guide.
tutorials: List[Tutorial] tutorials: List["Tutorial"]
# Choose a theme for your /game/* pages # Choose a theme for your /game/* pages
# Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone # Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone
@@ -111,7 +119,7 @@ class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required. """A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
A Game should have its own subclass of World in which it defines the required data structures.""" A Game should have its own subclass of World in which it defines the required data structures."""
options: Dict[str, Option[Any]] = {} # link your Options mapping option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping
game: str # name the game game: str # name the game
topology_present: bool = False # indicate if world type has any meaningful layout/pathing topology_present: bool = False # indicate if world type has any meaningful layout/pathing
@@ -159,8 +167,11 @@ class World(metaclass=AutoWorldRegister):
# Hide World Type from various views. Does not remove functionality. # Hide World Type from various views. Does not remove functionality.
hidden: bool = False hidden: bool = False
# see WebWorld for options
web: WebWorld = WebWorld()
# autoset on creation: # autoset on creation:
world: MultiWorld world: "MultiWorld"
player: int player: int
# automatically generated # automatically generated
@@ -170,9 +181,10 @@ class World(metaclass=AutoWorldRegister):
item_names: Set[str] # set of all potential item names item_names: Set[str] # set of all potential item names
location_names: Set[str] # set of all potential location names location_names: Set[str] # set of all potential location names
web: WebWorld = WebWorld() zip_path: Optional[pathlib.Path] = None # If loaded from a .apworld, this is the Path to it.
__file__: str # path it was loaded from
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: "MultiWorld", player: int):
self.world = world self.world = world
self.player = player self.player = player
@@ -207,12 +219,12 @@ class World(metaclass=AutoWorldRegister):
@classmethod @classmethod
def fill_hook(cls, def fill_hook(cls,
progitempool: List[Item], progitempool: List["Item"],
nonexcludeditempool: List[Item], nonexcludeditempool: List["Item"],
localrestitempool: Dict[int, List[Item]], localrestitempool: Dict[int, List["Item"]],
nonlocalrestitempool: Dict[int, List[Item]], nonlocalrestitempool: Dict[int, List["Item"]],
restitempool: List[Item], restitempool: List["Item"],
fill_locations: List[Location]) -> None: fill_locations: List["Location"]) -> None:
"""Special method that gets called as part of distribute_items_restrictive (main fill). """Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type.""" This gets called once per present world type."""
pass pass
@@ -250,7 +262,7 @@ class World(metaclass=AutoWorldRegister):
# end of ordered Main.py calls # end of ordered Main.py calls
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> "Item":
"""Create an item for this world type and player. """Create an item for this world type and player.
Warning: this may be called with self.world = None, for example by MultiServer""" Warning: this may be called with self.world = None, for example by MultiServer"""
raise NotImplementedError raise NotImplementedError
@@ -261,7 +273,7 @@ class World(metaclass=AutoWorldRegister):
return self.world.random.choice(tuple(self.item_name_to_id.keys())) return self.world.random.choice(tuple(self.item_name_to_id.keys()))
# decent place to implement progressive items, in most cases can stay as-is # decent place to implement progressive items, in most cases can stay as-is
def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]: def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped. """Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
Collect None to skip item. Collect None to skip item.
:param state: CollectionState to collect into :param state: CollectionState to collect into
@@ -272,18 +284,18 @@ class World(metaclass=AutoWorldRegister):
return None return None
# called to create all_state, return Items that are created during pre_fill # called to create all_state, return Items that are created during pre_fill
def get_pre_fill_items(self) -> List[Item]: def get_pre_fill_items(self) -> List["Item"]:
return [] return []
# following methods should not need to be overridden. # following methods should not need to be overridden.
def collect(self, state: CollectionState, item: Item) -> bool: def collect(self, state: "CollectionState", item: "Item") -> bool:
name = self.collect_item(state, item) name = self.collect_item(state, item)
if name: if name:
state.prog_items[name, self.player] += 1 state.prog_items[name, self.player] += 1
return True return True
return False return False
def remove(self, state: CollectionState, item: Item) -> bool: def remove(self, state: "CollectionState", item: "Item") -> bool:
name = self.collect_item(state, item, True) name = self.collect_item(state, item, True)
if name: if name:
state.prog_items[name, self.player] -= 1 state.prog_items[name, self.player] -= 1
@@ -292,7 +304,7 @@ class World(metaclass=AutoWorldRegister):
return True return True
return False return False
def create_filler(self) -> Item: def create_filler(self) -> "Item":
return self.create_item(self.get_filler_item_name()) return self.create_item(self.get_filler_item_name())

View File

@@ -1,29 +1,56 @@
import importlib import importlib
import zipimport
import os import os
import typing
__all__ = {"lookup_any_item_id_to_name", folder = os.path.dirname(__file__)
"lookup_any_location_id_to_name",
"network_data_package", __all__ = {
"AutoWorldRegister"} "lookup_any_item_id_to_name",
"lookup_any_location_id_to_name",
"network_data_package",
"AutoWorldRegister",
"world_sources",
"folder",
}
if typing.TYPE_CHECKING:
from .AutoWorld import World
class WorldSource(typing.NamedTuple):
path: str # typically relative path from this module
is_zip: bool = False
# find potential world containers, currently folders and zip-importable .apworld's
world_sources: typing.List[WorldSource] = []
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
for file in os.scandir(folder):
if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
if file.is_dir():
world_sources.append(WorldSource(file.name))
elif file.is_file() and file.name.endswith(".apworld"):
world_sources.append(WorldSource(file.name, is_zip=True))
# import all submodules to trigger AutoWorldRegister # import all submodules to trigger AutoWorldRegister
world_folders = [] world_sources.sort()
for file in os.scandir(os.path.dirname(__file__)): for world_source in world_sources:
if file.is_dir(): if world_source.is_zip:
world_folders.append(file.name) importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
world_folders.sort() importer.load_module(world_source.path.split(".", 1)[0])
for world in world_folders: else:
if not world.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders importlib.import_module(f".{world_source.path}", "worlds")
importlib.import_module(f".{world}", "worlds")
from .AutoWorld import AutoWorldRegister
lookup_any_item_id_to_name = {} lookup_any_item_id_to_name = {}
lookup_any_location_id_to_name = {} lookup_any_location_id_to_name = {}
games = {} games = {}
from .AutoWorld import AutoWorldRegister
for world_name, world in AutoWorldRegister.world_types.items(): for world_name, world in AutoWorldRegister.world_types.items():
games[world_name] = { games[world_name] = {
"item_name_to_id" : world.item_name_to_id, "item_name_to_id": world.item_name_to_id,
"location_name_to_id": world.location_name_to_id, "location_name_to_id": world.location_name_to_id,
"version": world.data_version, "version": world.data_version,
# seems clients don't actually want this. Keeping it here in case someone changes their mind. # seems clients don't actually want this. Keeping it here in case someone changes their mind.
@@ -41,5 +68,6 @@ network_data_package = {
if any(not world.data_version for world in AutoWorldRegister.world_types.values()): if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
network_data_package["version"] = 0 network_data_package["version"] = 0
import logging import logging
logging.warning(f"Datapackage is in custom mode. Custom Worlds: " logging.warning(f"Datapackage is in custom mode. Custom Worlds: "
f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}") f"{[world for world in AutoWorldRegister.world_types.values() if not world.data_version]}")

View File

@@ -15,7 +15,6 @@ def create_dungeons(world, player):
dungeon_items, player) dungeon_items, player)
for item in dungeon.all_items: for item in dungeon.all_items:
item.dungeon = dungeon item.dungeon = dungeon
item.world = world
dungeon.boss = BossFactory(default_boss, player) if default_boss else None dungeon.boss = BossFactory(default_boss, player) if default_boss else None
for region in dungeon.regions: for region in dungeon.regions:
world.get_region(region, player).dungeon = dungeon world.get_region(region, player).dungeon = dungeon

View File

@@ -51,6 +51,11 @@ class ItemData(typing.NamedTuple):
flute_boy_credit: typing.Optional[str] flute_boy_credit: typing.Optional[str]
hint_text: typing.Optional[str] hint_text: typing.Optional[str]
def as_init_dict(self) -> typing.Dict[str, typing.Any]:
return {key: getattr(self, key) for key in
('classification', 'type', 'item_code', 'pedestal_hint', 'hint_text')}
# Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text) # Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'), item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
'Progressive Bow': ItemData(IC.progression, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'), 'Progressive Bow': ItemData(IC.progression, None, 0x64, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'a Bow'),
@@ -218,7 +223,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None), 'Open Floodgate': ItemData(IC.progression, 'Event', None, None, None, None, None, None, None, None),
} }
as_dict_item_table = {name: data._asdict() for name, data in item_table.items()} item_init_table = {name: data.as_init_dict() for name, data in item_table.items()}
progression_mapping = { progression_mapping = {
"Golden Sword": ("Progressive Sword", 4), "Golden Sword": ("Progressive Sword", 4),

View File

@@ -2091,7 +2091,9 @@ def write_string_to_rom(rom, target, string):
def write_strings(rom, world, player): def write_strings(rom, world, player):
from . import ALTTPWorld
local_random = world.slot_seeds[player] local_random = world.slot_seeds[player]
w: ALTTPWorld = world.worlds[player]
tt = TextTable() tt = TextTable()
tt.removeUnwantedText() tt.removeUnwantedText()
@@ -2420,7 +2422,8 @@ def write_strings(rom, world, player):
pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem, pedestal_text = 'Some Hot Air' if pedestalitem is None else hint_text(pedestalitem,
True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item' True) if pedestalitem.pedestal_hint_text is not None else 'Unknown Item'
tt['mastersword_pedestal_translated'] = pedestal_text tt['mastersword_pedestal_translated'] = pedestal_text
pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else pedestalitem.pedestal_credit_text if pedestalitem.pedestal_credit_text is not None else 'and the Unknown Item' pedestal_credit_text = 'and the Hot Air' if pedestalitem is None else \
w.pedestal_credit_texts.get(pedestalitem.code, 'and the Unknown Item')
etheritem = world.get_location('Ether Tablet', player).item etheritem = world.get_location('Ether Tablet', player).item
ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem, ether_text = 'Some Hot Air' if etheritem is None else hint_text(etheritem,
@@ -2448,20 +2451,24 @@ def write_strings(rom, world, player):
credits = Credits() credits = Credits()
sickkiditem = world.get_location('Sick Kid', player).item sickkiditem = world.get_location('Sick Kid', player).item
sickkiditem_text = local_random.choice( sickkiditem_text = local_random.choice(SickKid_texts) \
SickKid_texts) if sickkiditem is None or sickkiditem.sickkid_credit_text is None else sickkiditem.sickkid_credit_text if sickkiditem is None or sickkiditem.code not in w.sickkid_credit_texts \
else w.sickkid_credit_texts[sickkiditem.code]
zoraitem = world.get_location('King Zora', player).item zoraitem = world.get_location('King Zora', player).item
zoraitem_text = local_random.choice( zoraitem_text = local_random.choice(Zora_texts) \
Zora_texts) if zoraitem is None or zoraitem.zora_credit_text is None else zoraitem.zora_credit_text if zoraitem is None or zoraitem.code not in w.zora_credit_texts \
else w.zora_credit_texts[zoraitem.code]
magicshopitem = world.get_location('Potion Shop', player).item magicshopitem = world.get_location('Potion Shop', player).item
magicshopitem_text = local_random.choice( magicshopitem_text = local_random.choice(MagicShop_texts) \
MagicShop_texts) if magicshopitem is None or magicshopitem.magicshop_credit_text is None else magicshopitem.magicshop_credit_text if magicshopitem is None or magicshopitem.code not in w.magicshop_credit_texts \
else w.magicshop_credit_texts[magicshopitem.code]
fluteboyitem = world.get_location('Flute Spot', player).item fluteboyitem = world.get_location('Flute Spot', player).item
fluteboyitem_text = local_random.choice( fluteboyitem_text = local_random.choice(FluteBoy_texts) \
FluteBoy_texts) if fluteboyitem is None or fluteboyitem.fluteboy_credit_text is None else fluteboyitem.fluteboy_credit_text if fluteboyitem is None or fluteboyitem.code not in w.fluteboy_credit_texts \
else w.fluteboy_credit_texts[fluteboyitem.code]
credits.update_credits_line('castle', 0, local_random.choice(KingsReturn_texts)) credits.update_credits_line('castle', 0, local_random.choice(KingsReturn_texts))
credits.update_credits_line('sanctuary', 0, local_random.choice(Sanctuary_texts)) credits.update_credits_line('sanctuary', 0, local_random.choice(Sanctuary_texts))

View File

@@ -935,7 +935,6 @@ def set_trock_key_rules(world, player):
else: else:
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works # A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
item = ItemFactory('Small Key (Turtle Rock)', player) item = ItemFactory('Small Key (Turtle Rock)', player)
item.world = world
location = world.get_location('Turtle Rock - Big Key Chest', player) location = world.get_location('Turtle Rock - Big Key Chest', player)
location.place_locked_item(item) location.place_locked_item(item)
location.event = True location.event = True

View File

@@ -207,10 +207,10 @@ def ShopSlotFill(world):
shops_per_sphere.append(current_shops_slots) shops_per_sphere.append(current_shops_slots)
candidates_per_sphere.append(current_candidates) candidates_per_sphere.append(current_candidates)
for location in sphere: for location in sphere:
if location.shop_slot is not None: if isinstance(location, ALttPLocation) and location.shop_slot is not None:
if not location.shop_slot_disabled: if not location.shop_slot_disabled:
current_shops_slots.append(location) current_shops_slots.append(location)
elif not location.locked and not location.item.name in blacklist_words: elif not location.locked and location.item.name not in blacklist_words:
current_candidates.append(location) current_candidates.append(location)
if cumu_weights: if cumu_weights:
x = cumu_weights[-1] x = cumu_weights[-1]
@@ -335,7 +335,6 @@ def create_shops(world, player: int):
else: else:
loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player) loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)
loc.shop_slot_disabled = True loc.shop_slot_disabled = True
loc.item.world = world
shop.region.locations.append(loc) shop.region.locations.append(loc)
world.clear_location_cache() world.clear_location_cache()

View File

@@ -6,31 +6,33 @@ from BaseClasses import Location, Item, ItemClassification
class ALttPLocation(Location): class ALttPLocation(Location):
game: str = "A Link to the Past" game: str = "A Link to the Past"
crystal: bool
player_address: Optional[int]
_hint_text: Optional[str]
shop_slot: Optional[int] = None
"""If given as integer, shop_slot is the shop's inventory index."""
shop_slot_disabled: bool = False
def __init__(self, player: int, name: str = '', address=None, crystal: bool = False, def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False,
hint_text: Optional[str] = None, parent=None, hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None):
player_address=None):
super(ALttPLocation, self).__init__(player, name, address, parent) super(ALttPLocation, self).__init__(player, name, address, parent)
self.crystal = crystal self.crystal = crystal
self.player_address = player_address self.player_address = player_address
self._hint_text: str = hint_text self._hint_text = hint_text
class ALttPItem(Item): class ALttPItem(Item):
game: str = "A Link to the Past" game: str = "A Link to the Past"
type: Optional[str]
_pedestal_hint_text: Optional[str]
_hint_text: Optional[str]
dungeon = None dungeon = None
def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None, pedestal_hint=None, def __init__(self, name, player, classification=ItemClassification.filler, type=None, item_code=None,
pedestal_credit=None, sick_kid_credit=None, zora_credit=None, witch_credit=None, pedestal_hint=None, hint_text=None):
flute_boy_credit=None, hint_text=None):
super(ALttPItem, self).__init__(name, classification, item_code, player) super(ALttPItem, self).__init__(name, classification, item_code, player)
self.type = type self.type = type
self._pedestal_hint_text = pedestal_hint self._pedestal_hint_text = pedestal_hint
self.pedestal_credit_text = pedestal_credit
self.sickkid_credit_text = sick_kid_credit
self.zora_credit_text = zora_credit
self.magicshop_credit_text = witch_credit
self.fluteboy_credit_text = flute_boy_credit
self._hint_text = hint_text self._hint_text = hint_text
@property @property

View File

@@ -1,26 +1,23 @@
import random
import logging import logging
import os import os
import random
import threading import threading
import typing import typing
from BaseClasses import Item, CollectionState, Tutorial from BaseClasses import Item, CollectionState, Tutorial
from .Dungeons import create_dungeons
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .ItemPool import generate_itempool, difficulties
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
from .Options import alttp_options, smallkey_shuffle
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
get_hash_string, get_base_rom_path, LttPDeltaPatch
from .Rules import set_rules
from .Shops import create_shops, ShopSlotFill
from .SubClasses import ALttPItem from .SubClasses import ALttPItem
from ..AutoWorld import World, WebWorld, LogicMixin from ..AutoWorld import World, WebWorld, LogicMixin
from .Options import alttp_options, smallkey_shuffle
from .Items import as_dict_item_table, item_name_groups, item_table, GetBeemizerItem
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
from .Rules import set_rules
from .ItemPool import generate_itempool, difficulties
from .Shops import create_shops, ShopSlotFill
from .Dungeons import create_dungeons
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \
get_base_rom_path, LttPDeltaPatch
import Patch
from itertools import chain
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
lttp_logger = logging.getLogger("A Link to the Past") lttp_logger = logging.getLogger("A Link to the Past")
@@ -110,7 +107,7 @@ class ALTTPWorld(World):
Ganon! Ganon!
""" """
game: str = "A Link to the Past" game: str = "A Link to the Past"
options = alttp_options option_definitions = alttp_options
topology_present = True topology_present = True
item_name_groups = item_name_groups item_name_groups = item_name_groups
hint_blacklist = {"Triforce"} hint_blacklist = {"Triforce"}
@@ -124,6 +121,17 @@ class ALTTPWorld(World):
required_client_version = (0, 3, 2) required_client_version = (0, 3, 2)
web = ALTTPWeb() web = ALTTPWeb()
pedestal_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.pedestal_credit for data in item_table.values() if data.pedestal_credit}
sickkid_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.sick_kid_credit for data in item_table.values() if data.sick_kid_credit}
zora_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.zora_credit for data in item_table.values() if data.zora_credit}
magicshop_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.witch_credit for data in item_table.values() if data.witch_credit}
fluteboy_credit_texts: typing.Dict[int, str] = \
{data.item_code: data.flute_boy_credit for data in item_table.values() if data.flute_boy_credit}
set_rules = set_rules set_rules = set_rules
create_items = generate_itempool create_items = generate_itempool
@@ -145,6 +153,9 @@ class ALTTPWorld(World):
player = self.player player = self.player
world = self.world world = self.world
if self.use_enemizer():
check_enemizer(world.enemizer)
# system for sharing ER layouts # system for sharing ER layouts
self.er_seed = str(world.random.randint(0, 2 ** 64)) self.er_seed = str(world.random.randint(0, 2 ** 64))
@@ -330,14 +341,19 @@ class ALTTPWorld(World):
def stage_post_fill(cls, world): def stage_post_fill(cls, world):
ShopSlotFill(world) ShopSlotFill(world)
def use_enemizer(self):
world = self.world
player = self.player
return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
world = self.world world = self.world
player = self.player player = self.player
try: try:
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] use_enemizer = self.use_enemizer()
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.pot_shuffle[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
rom = LocalRom(get_base_rom_path()) rom = LocalRom(get_base_rom_path())
@@ -400,7 +416,7 @@ class ALTTPWorld(World):
multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]] multidata["connect_names"][new_name] = multidata["connect_names"][self.world.player_name[self.player]]
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **as_dict_item_table[name]) return ALttPItem(name, self.player, **item_init_table[name])
@classmethod @classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,

View File

@@ -47,13 +47,12 @@ class ArchipIDLEWorld(World):
item_pool = [] item_pool = []
for i in range(100): for i in range(100):
item = Item( item = ArchipIDLEItem(
item_table_copy[i], item_table_copy[i],
ItemClassification.progression if i < 20 else ItemClassification.filler, ItemClassification.progression if i < 20 else ItemClassification.filler,
self.item_name_to_id[item_table_copy[i]], self.item_name_to_id[item_table_copy[i]],
self.player self.player
) )
item.game = 'ArchipIDLE'
item_pool.append(item) item_pool.append(item)
self.world.itempool += item_pool self.world.itempool += item_pool
@@ -93,6 +92,10 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi
return region return region
class ArchipIDLEItem(Item):
game = "ArchipIDLE"
class ArchipIDLELocation(Location): class ArchipIDLELocation(Location):
game: str = "ArchipIDLE" game: str = "ArchipIDLE"

View File

@@ -27,7 +27,7 @@ class ChecksFinderWorld(World):
with the mines! You win when you get all your items and beat the board! with the mines! You win when you get all your items and beat the board!
""" """
game: str = "ChecksFinder" game: str = "ChecksFinder"
options = checksfinder_options option_definitions = checksfinder_options
topology_present = True topology_present = True
web = ChecksFinderWeb() web = ChecksFinderWeb()

View File

@@ -16,14 +16,26 @@ from ..generic.Rules import set_rule
class DarkSouls3Web(WebWorld): class DarkSouls3Web(WebWorld):
tutorials = [Tutorial( bug_report_page = "https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/issues"
setup_en = Tutorial(
"Multiworld Setup Tutorial", "Multiworld Setup Tutorial",
"A guide to setting up the Archipelago Dark Souls III randomizer on your computer.", "A guide to setting up the Archipelago Dark Souls III randomizer on your computer.",
"English", "English",
"setup_en.md", "setup_en.md",
"setup/en", "setup/en",
["Marech"] ["Marech"]
)] )
setup_fr = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Français",
"setup_fr.md",
"setup/fr",
["Marech"]
)
tutorials = [setup_en, setup_fr]
class DarkSouls3World(World): class DarkSouls3World(World):
@@ -34,7 +46,7 @@ class DarkSouls3World(World):
""" """
game: str = "Dark Souls III" game: str = "Dark Souls III"
options = dark_souls_options option_definitions = dark_souls_options
topology_present: bool = True topology_present: bool = True
remote_items: bool = False remote_items: bool = False
remote_start_inventory: bool = False remote_start_inventory: bool = False
@@ -146,7 +158,7 @@ class DarkSouls3World(World):
# For each region, add the associated locations retrieved from the corresponding location_table # For each region, add the associated locations retrieved from the corresponding location_table
def create_region(self, region_name, location_table) -> Region: def create_region(self, region_name, location_table) -> Region:
new_region = Region(region_name, RegionType.Generic, region_name, self.player) new_region = Region(region_name, RegionType.Generic, region_name, self.player, self.world)
if location_table: if location_table:
for name, address in location_table.items(): for name, address in location_table.items():
location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region) location = DarkSouls3Location(self.player, name, self.location_name_to_id[name], new_region)

View File

@@ -10,7 +10,10 @@ config file.
In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized. In Dark Souls III, all unique items you can earn from a static corpse, a chest or the death of a Boss/NPC are randomized.
This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at This exclude the upgrade materials such as the titanite shards, the estus shards and the consumables which remain at
the same location. I also added an option available from the settings page to randomize the level of the generated the same location. I also added an option available from the settings page to randomize the level of the generated
weapons( from +0 to +10/+5 ) weapons(from +0 to +10/+5)
To beat the game you need to collect the 4 "Cinders of a Lord" randomized in the multiworld
and kill the final boss "Soul of Cinder"
## What Dark Souls III items can appear in other players' worlds? ## What Dark Souls III items can appear in other players' worlds?

View File

@@ -3,7 +3,7 @@
## Required Software ## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client) - [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
## General Concept ## General Concept
@@ -14,22 +14,24 @@ The randomization is performed by the AP.json file, an output file generated by
## Installation Procedures ## Installation Procedures
**This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed** <span style="color:tomato">
**This mod can ban you permanently from the FromSoftware servers if used online.**
</span>
This client has only been tested with the Official Steam version of the game (v1.15/1.35) not matter which DLCs are installed.
Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client). Get the dinput8.dll from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases).
Then you need to add the two following files at the root folder of your game Then you need to add the two following files at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game"):
( e.g. "SteamLibrary\steamapps\common\DARK SOULS III\Game" ):
- **dinput8.dll** - **dinput8.dll**
- **AP.json** (renamed from the generated file AP-{ROOM_ID}.json) - **AP.json** : The .json file downloaded from the multiworld room or provided by the host, named AP-{ROOM_ID}.json, has to be renamed to AP.json.
## Joining a MultiWorld Game ## Joining a MultiWorld Game
1. Run DarkSoulsIII.exe or run the game through Steam 1. Run DarkSoulsIII.exe or run the game through Steam
2. Type in /connect {SERVER_IP}:{SERVER_PORT} in the "Windows Command Prompt" that opened 2. Type in "/connect {SERVER_IP}:{SERVER_PORT}" in the "Windows Command Prompt" that opened
3. Once connected, create a new game, choose a class and wait for the others before starting 3. Once connected, create a new game, choose a class and wait for the others before starting
4. You can quit and launch at anytime during a game 4. You can quit and launch at anytime during a game
## Where do I get a config file? ## Where do I get a config file?
The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to
configure your personal settings and export them into a config file configure your personal settings and export them into a config file

View 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.

View File

@@ -38,7 +38,7 @@ class DKC3World(World):
mystery of why Donkey Kong and Diddy disappeared while on vacation. mystery of why Donkey Kong and Diddy disappeared while on vacation.
""" """
game: str = "Donkey Kong Country 3" game: str = "Donkey Kong Country 3"
options = dkc3_options option_definitions = dkc3_options
topology_present = False topology_present = False
data_version = 1 data_version = 1
#hint_blacklist = {LocationName.rocket_rush_flag} #hint_blacklist = {LocationName.rocket_rush_flag}

View File

@@ -15,6 +15,11 @@
compatible hardware compatible hardware
- Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc` - Your legally obtained Donkey Kong Country 3 ROM file, probably named `Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc`
## Optional Software
- Donkey Kong Country 3 Tracker
- PopTracker from: [PopTracker Releases Page](https://github.com/black-sliver/PopTracker/releases/)
- Donkey Kong Country 3 Archipelago PopTracker pack from: [DKC3 AP Tracker Releases Page](https://github.com/PoryGone/DKC3_AP_Tracker/releases/)
## Installation Procedures ## Installation Procedures
### Windows Setup ### Windows Setup
@@ -57,7 +62,6 @@ validator page: [YAML Validation page](/mysterycheck)
4. You will be presented with a server page, from which you can download your patch file. 4. You will be presented with a server page, from which you can download your patch file.
5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the 5. Double-click on your patch file, and the Donkey Kong Country 3 Client will launch automatically, create your ROM from the
patch file, and open your emulator for you. patch file, and open your emulator for you.
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
## Joining a MultiWorld Game ## Joining a MultiWorld Game
@@ -65,7 +69,7 @@ validator page: [YAML Validation page](/mysterycheck)
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
files. Your patch file should have a `.apsm` extension. files. Your patch file should have a `.apdkc3` extension.
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
client, and will also create your ROM in the same place as your patch file. client, and will also create your ROM in the same place as your patch file.

View File

@@ -78,9 +78,14 @@ def generate_mod(world, output_directory: str):
global data_final_template, locale_template, control_template, data_template, settings_template global data_final_template, locale_template, control_template, data_template, settings_template
with template_load_lock: with template_load_lock:
if not data_final_template: if not data_final_template:
mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template") def load_template(name: str):
import pkgutil
data = pkgutil.get_data(__name__, "data/mod_template/" + name).decode()
return data, name, lambda: False
template_env: Optional[jinja2.Environment] = \ template_env: Optional[jinja2.Environment] = \
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder])) jinja2.Environment(loader=jinja2.FunctionLoader(load_template))
data_template = template_env.get_template("data.lua") data_template = template_env.get_template("data.lua")
data_final_template = template_env.get_template("data-final-fixes.lua") data_final_template = template_env.get_template("data-final-fixes.lua")
locale_template = template_env.get_template(r"locale/en/locale.cfg") locale_template = template_env.get_template(r"locale/en/locale.cfg")
@@ -158,7 +163,21 @@ def generate_mod(world, output_directory: str):
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
en_locale_dir = os.path.join(mod_dir, "locale", "en") en_locale_dir = os.path.join(mod_dir, "locale", "en")
os.makedirs(en_locale_dir, exist_ok=True) os.makedirs(en_locale_dir, exist_ok=True)
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
if world.zip_path:
# Maybe investigate read from zip, write to zip, without temp file?
with zipfile.ZipFile(world.zip_path) as zf:
for file in zf.infolist():
if not file.is_dir() and "/data/mod/" in file.filename:
path_part = Utils.get_text_after(file.filename, "/data/mod/")
target = os.path.join(mod_dir, path_part)
os.makedirs(os.path.split(target)[0], exist_ok=True)
with open(target, "wb") as f:
f.write(zf.read(file))
else:
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
with open(os.path.join(mod_dir, "data.lua"), "wt") as f: with open(os.path.join(mod_dir, "data.lua"), "wt") as f:
f.write(data_template_code) f.write(data_template_code)
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f: with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:

View File

@@ -1,7 +1,7 @@
from typing import Dict, List, Set from typing import Dict, List, Set
from collections import deque from collections import deque
from worlds.factorio.Options import TechTreeLayout from .Options import TechTreeLayout
funnel_layers = {TechTreeLayout.option_small_funnels: 3, funnel_layers = {TechTreeLayout.option_small_funnels: 3,
TechTreeLayout.option_medium_funnels: 4, TechTreeLayout.option_medium_funnels: 4,

View File

@@ -19,8 +19,8 @@ pool = ThreadPoolExecutor(1)
def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
with open(os.path.join(source_folder, f"{data_name}.json")) as f: import pkgutil
return json.load(f) return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())
techs_future = pool.submit(load_json_data, "techs") techs_future = pool.submit(load_json_data, "techs")

View File

@@ -1,7 +1,7 @@
import collections import collections
import typing import typing
from ..AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification from BaseClasses import Region, Entrance, Location, Item, RegionType, Tutorial, ItemClassification
from .Technologies import base_tech_table, recipe_sources, base_technology_table, \ from .Technologies import base_tech_table, recipe_sources, base_technology_table, \
@@ -193,7 +193,7 @@ class Factorio(World):
return super(Factorio, self).collect_item(state, item, remove) return super(Factorio, self).collect_item(state, item, remove)
options = factorio_options option_definitions = factorio_options
@classmethod @classmethod
def stage_write_spoiler(cls, world, spoiler_handle): def stage_write_spoiler(cls, world, spoiler_handle):

View File

@@ -286,6 +286,12 @@ end)
-- hook into researches done -- hook into researches done
script.on_event(defines.events.on_research_finished, function(event) script.on_event(defines.events.on_research_finished, function(event)
local technology = event.research local technology = event.research
if string.find(technology.force.name, "EE_TESTFORCE") == 1 then
--Don't acknowledge AP research as an Editor Extensions test force
--Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces
--are worked on exclusively in editor mode.
return
end
if technology.researched and string.find(technology.name, "ap%-") == 1 then if technology.researched and string.find(technology.name, "ap%-") == 1 then
-- check if it came from the server anyway, then we don't need to double send. -- check if it came from the server anyway, then we don't need to double send.
dumpInfo(technology.force) --is sendable dumpInfo(technology.force) --is sendable

View File

@@ -211,8 +211,8 @@ copy_factorio_icon(new_tree_copy, "{{ progressive_technology_table[item_name][0]
{%- endif -%} {%- endif -%}
{#- connect Technology #} {#- connect Technology #}
{%- if original_tech_name in tech_tree_layout_prerequisites %} {%- if original_tech_name in tech_tree_layout_prerequisites %}
{%- for prerequesite in tech_tree_layout_prerequisites[original_tech_name] %} {%- for prerequisite in tech_tree_layout_prerequisites[original_tech_name] %}
table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequesite] }}-") table.insert(new_tree_copy.prerequisites, "ap-{{ tech_table[prerequisite] }}-")
{% endfor %} {% endfor %}
{% endif -%} {% endif -%}
{#- add new Technology to game #} {#- add new Technology to game #}

View File

@@ -27,7 +27,7 @@ class FF1World(World):
Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made. Part puzzle and part speed-run, it breathes new life into one of the most influential games ever made.
""" """
options = ff1_options option_definitions = ff1_options
game = "Final Fantasy" game = "Final Fantasy"
topology_present = False topology_present = False
remote_items = True remote_items = True
@@ -54,6 +54,7 @@ class FF1World(World):
locations = get_options(self.world, 'locations', self.player) locations = get_options(self.world, 'locations', self.player)
rules = get_options(self.world, 'rules', self.player) rules = get_options(self.world, 'rules', self.player)
menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules) menu_region = self.ff1_locations.create_menu_region(self.player, locations, rules)
menu_region.world = self.world
terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region) terminated_event = Location(self.player, CHAOS_TERMINATED_EVENT, EventId, menu_region)
terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player) terminated_item = Item(CHAOS_TERMINATED_EVENT, ItemClassification.progression, EventId, self.player)
terminated_event.place_locked_item(terminated_item) terminated_event.place_locked_item(terminated_item)

View File

@@ -7,8 +7,7 @@ changes it up by allowing you to plan out certain aspects of the game by placing
certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of
these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`, these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary.
plando.
### Enabling Plando ### Enabling Plando

View File

@@ -50,7 +50,7 @@ option_docstrings = {
"pool and open their locations for randomization.", "pool and open their locations for randomization.",
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.", "RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization." "RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
"Mimic Grubs are always placed in your own game.", "Mimic Grubs are always placed in your own game.",
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see" "RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
" and buy an item that is randomized into that location as well.", " and buy an item that is randomized into that location as well.",
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items " "RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "

View File

@@ -142,7 +142,7 @@ class HKWorld(World):
As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils. As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils.
""" # from https://www.hollowknight.com """ # from https://www.hollowknight.com
game: str = "Hollow Knight" game: str = "Hollow Knight"
options = hollow_knight_options option_definitions = hollow_knight_options
web = HKWeb() web = HKWeb()
@@ -435,7 +435,7 @@ class HKWorld(World):
slot_data = {} slot_data = {}
options = slot_data["options"] = {} options = slot_data["options"] = {}
for option_name in self.options: for option_name in self.option_definitions:
option = getattr(self.world, option_name)[self.player] option = getattr(self.world, option_name)[self.player]
try: try:
optionvalue = int(option.value) optionvalue = int(option.value)
@@ -632,8 +632,9 @@ class HKLocation(Location):
class HKItem(Item): class HKItem(Item):
game = "Hollow Knight" game = "Hollow Knight"
type: str
def __init__(self, name, advancement, code, type, player: int = None): def __init__(self, name, advancement, code, type: str, player: int = None):
if name == "Mimic_Grub": if name == "Mimic_Grub":
classification = ItemClassification.trap classification = ItemClassification.trap
elif type in ("Grub", "DreamWarrior", "Root", "Egg"): elif type in ("Grub", "DreamWarrior", "Root", "Egg"):

View File

@@ -6,14 +6,9 @@
import typing import typing
from BaseClasses import Item, ItemClassification from BaseClasses import Item, ItemClassification
from worlds.alttp import ALTTPWorld
# pedestal_credit_text: str = "and the Unknown Item"
# sickkid_credit_text: Optional[str] = None
# magicshop_credit_text: Optional[str] = None
# zora_credit_text: Optional[str] = None
# fluteboy_credit_text: Optional[str] = None
class MeritousLttPText(typing.NamedTuple): class MeritousLttPText(typing.NamedTuple):
pedestal: typing.Optional[str] pedestal: typing.Optional[str]
sickkid: typing.Optional[str] sickkid: typing.Optional[str]
@@ -143,6 +138,7 @@ LttPCreditsText = {
class MeritousItem(Item): class MeritousItem(Item):
game: str = "Meritous" game: str = "Meritous"
type: str
def __init__(self, name, advancement, code, player): def __init__(self, name, advancement, code, player):
super(MeritousItem, self).__init__(name, super(MeritousItem, self).__init__(name,
@@ -171,14 +167,6 @@ class MeritousItem(Item):
self.type = "Artifact" self.type = "Artifact"
self.classification = ItemClassification.useful self.classification = ItemClassification.useful
if name in LttPCreditsText:
lttp = LttPCreditsText[name]
self.pedestal_credit_text = f"and the {lttp.pedestal}"
self.sickkid_credit_text = lttp.sickkid
self.magicshop_credit_text = lttp.magicshop
self.zora_credit_text = lttp.zora
self.fluteboy_credit_text = lttp.fluteboy
offset = 593_000 offset = 593_000
@@ -217,3 +205,10 @@ item_groups = {
"Important Artifacts": ["Shield Boost", "Circuit Booster", "Metabolism", "Dodge Enhancer"], "Important Artifacts": ["Shield Boost", "Circuit Booster", "Metabolism", "Dodge Enhancer"],
"Crystals": ["Crystals x500", "Crystals x1000", "Crystals x2000"] "Crystals": ["Crystals x500", "Crystals x1000", "Crystals x2000"]
} }
ALTTPWorld.pedestal_credit_texts.update({item_table[name]: f"and the {texts.pedestal}"
for name, texts in LttPCreditsText.items()})
ALTTPWorld.sickkid_credit_texts.update({item_table[name]: texts.sickkid for name, texts in LttPCreditsText.items()})
ALTTPWorld.magicshop_credit_texts.update({item_table[name]: texts.magicshop for name, texts in LttPCreditsText.items()})
ALTTPWorld.zora_credit_texts.update({item_table[name]: texts.zora for name, texts in LttPCreditsText.items()})
ALTTPWorld.fluteboy_credit_texts.update({item_table[name]: texts.fluteboy for name, texts in LttPCreditsText.items()})

View File

@@ -45,12 +45,11 @@ class MeritousWorld(World):
item_name_groups = item_groups item_name_groups = item_groups
data_version = 2 data_version = 2
forced_auto_forfeit = False
# NOTE: Remember to change this before this game goes live # NOTE: Remember to change this before this game goes live
required_client_version = (0, 2, 4) required_client_version = (0, 2, 4)
options = meritous_options option_definitions = meritous_options
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: MultiWorld, player: int):
super(MeritousWorld, self).__init__(world, player) super(MeritousWorld, self).__init__(world, player)

View File

@@ -58,7 +58,7 @@ class MinecraftWorld(World):
victory! victory!
""" """
game: str = "Minecraft" game: str = "Minecraft"
options = minecraft_options option_definitions = minecraft_options
topology_present = True topology_present = True
web = MinecraftWebWorld() web = MinecraftWebWorld()

View File

@@ -33,12 +33,12 @@ leave this window open as this is your server console.
### Connect to the MultiServer ### Connect to the MultiServer
Using Minecraft 1.18.2 connect to the server `localhost`. Open Minecraft, go to `Multiplayer > Direct Connection`, and join the `localhost` server address.
If you are using the website to host the game then it should auto-connect to the AP server without the need to `/connect` If you are using the website to host the game then it should auto-connect to the AP server without the need to `/connect`
otherwise once you are in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the otherwise once you are in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. Note that there is no colon between `<AP-Address>` and `(Port)`.
`(Password)` is only required if the Archipelago server you are using has a password set. `(Password)` is only required if the Archipelago server you are using has a password set.
### Play the game ### Play the game

View File

@@ -1 +1 @@
requests >= 2.27.1 # used by client requests >= 2.28.1 # used by client

View File

@@ -1,6 +1,7 @@
import random import random
from BaseClasses import LocationProgressType from BaseClasses import LocationProgressType
from .Items import OOTItem
# Abbreviations # Abbreviations
# DMC Death Mountain Crater # DMC Death Mountain Crater
@@ -1260,7 +1261,7 @@ def hintExclusions(world, clear_cache=False):
world.hint_exclusions = [] world.hint_exclusions = []
for location in world.get_locations(): for location in world.get_locations():
if (location.locked and (location.item.type != 'Song' or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED: if (location.locked and ((isinstance(location.item, OOTItem) and location.item.type != 'Song') or world.shuffle_song_items != 'song')) or location.progress_type == LocationProgressType.EXCLUDED:
world.hint_exclusions.append(location.name) world.hint_exclusions.append(location.name)
world_location_names = [ world_location_names = [

View File

@@ -10,6 +10,7 @@ from urllib.error import URLError, HTTPError
import json import json
from enum import Enum from enum import Enum
from .Items import OOTItem
from .HintList import getHint, getHintGroup, Hint, hintExclusions from .HintList import getHint, getHintGroup, Hint, hintExclusions
from .Messages import COLOR_MAP, update_message_by_id from .Messages import COLOR_MAP, update_message_by_id
from .TextBox import line_wrap from .TextBox import line_wrap
@@ -480,7 +481,7 @@ def get_specific_item_hint(world, checked):
def get_random_location_hint(world, checked): def get_random_location_hint(world, checked):
locations = list(filter(lambda location: locations = list(filter(lambda location:
is_not_checked(location, checked) is_not_checked(location, checked)
and location.item.type not in ('Drop', 'Event', 'Shop', 'DungeonReward') and not (isinstance(location.item, OOTItem) and location.item.type in ('Drop', 'Event', 'Shop', 'DungeonReward'))
# and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items # and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items
and not location.locked and not location.locked
and location.name not in world.hint_exclusions and location.name not in world.hint_exclusions

View File

@@ -24,6 +24,7 @@ def ap_id_to_oot_data(ap_id):
class OOTItem(Item): class OOTItem(Item):
game: str = "Ocarina of Time" game: str = "Ocarina of Time"
type: str
def __init__(self, name, player, data, event, force_not_advancement): def __init__(self, name, player, data, event, force_not_advancement):
(type, advancement, index, special) = data (type, advancement, index, special) = data
@@ -38,7 +39,6 @@ class OOTItem(Item):
classification = ItemClassification.progression classification = ItemClassification.progression
else: else:
classification = ItemClassification.filler classification = ItemClassification.filler
adv = bool(advancement) and not force_not_advancement
super(OOTItem, self).__init__(name, classification, oot_data_to_ap_id(data, event), player) super(OOTItem, self).__init__(name, classification, oot_data_to_ap_id(data, event), player)
self.type = type self.type = type
self.index = index self.index = index
@@ -46,25 +46,12 @@ class OOTItem(Item):
self.looks_like_item = None self.looks_like_item = None
self.price = special.get('price', None) if special else None self.price = special.get('price', None) if special else None
self.internal = False self.internal = False
# The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)"
# This checks if the item it's looking for is a small key, using the small key property.
# Because of overlapping item fields, this means that OoT small keys are technically counted, unless we do this.
# This causes them to be double-collected during playthrough and generation.
@property
def smallkey(self) -> bool:
return False
@property
def bigkey(self) -> bool:
return False
@property @property
def dungeonitem(self) -> bool: def dungeonitem(self) -> bool:
return self.type in ['SmallKey', 'HideoutSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass'] return self.type in ['SmallKey', 'HideoutSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass']
# Progressive: True -> Advancement # Progressive: True -> Advancement
# False -> Priority # False -> Priority
# None -> Normal # None -> Normal

View File

@@ -5,6 +5,7 @@ import zlib
from collections import defaultdict from collections import defaultdict
from functools import partial from functools import partial
from .Items import OOTItem
from .LocationList import business_scrubs from .LocationList import business_scrubs
from .Hints import writeGossipStoneHints, buildAltarHints, \ from .Hints import writeGossipStoneHints, buildAltarHints, \
buildGanonText, getSimpleHintNoPrefix buildGanonText, getSimpleHintNoPrefix
@@ -1881,9 +1882,9 @@ def get_override_entry(player_id, location):
type = 2 type = 2
elif location.type == 'GS Token': elif location.type == 'GS Token':
type = 3 type = 3
elif location.type == 'Shop' and location.item.type != 'Shop': elif location.type == 'Shop' and not (isinstance(location.item, OOTItem) and location.item.type == 'Shop'):
type = 0 type = 0
elif location.type == 'GrottoNPC' and location.item.type != 'Shop': elif location.type == 'GrottoNPC' and not (isinstance(location.item, OOTItem) and location.item.type == 'Shop'):
type = 4 type = 4
elif location.type in ['Song', 'Cutscene']: elif location.type in ['Song', 'Cutscene']:
type = 5 type = 5
@@ -2103,7 +2104,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F
shop_objs = { 0x0148 } # "Sold Out" object shop_objs = { 0x0148 } # "Sold Out" object
for location in locations: for location in locations:
if location.item.type == 'Shop': if isinstance(location.item, OOTItem) and location.item.type == 'Shop':
shop_objs.add(location.item.special['object']) shop_objs.add(location.item.special['object'])
rom.write_int16(location.address1, location.item.index) rom.write_int16(location.address1, location.item.index)
else: else:

View File

@@ -95,7 +95,7 @@ class OOTWorld(World):
to rescue the Seven Sages, and then confront Ganondorf to save Hyrule! to rescue the Seven Sages, and then confront Ganondorf to save Hyrule!
""" """
game: str = "Ocarina of Time" game: str = "Ocarina of Time"
options: dict = oot_options option_definitions: dict = oot_options
topology_present: bool = True topology_present: bool = True
item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if
data[2] is not None} data[2] is not None}

View File

@@ -17,7 +17,7 @@ class OriBlindForest(World):
item_name_to_id = item_table item_name_to_id = item_table
location_name_to_id = lookup_name_to_id location_name_to_id = lookup_name_to_id
options = options option_definitions = options
hidden = True hidden = True

View File

@@ -37,7 +37,7 @@ class RaftWorld(World):
lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values())) lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values()))
location_name_to_id = locations_lookup_name_to_id location_name_to_id = locations_lookup_name_to_id
options = raft_options option_definitions = raft_options
data_version = 2 data_version = 2
required_client_version = (0, 3, 4) required_client_version = (0, 3, 4)

View File

@@ -30,7 +30,7 @@ class LegacyWorld(World):
But that's OK, because no one is perfect, and you don't have to be to succeed. But that's OK, because no one is perfect, and you don't have to be to succeed.
""" """
game: str = "Rogue Legacy" game: str = "Rogue Legacy"
options = legacy_options option_definitions = legacy_options
topology_present = False topology_present = False
data_version = 3 data_version = 3
required_client_version = (0, 2, 3) required_client_version = (0, 2, 3)

View File

@@ -1,5 +1,5 @@
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from ..generic.Rules import set_rule, add_rule from worlds.generic.Rules import set_rule, add_rule
def set_rules(world: MultiWorld, player: int): def set_rules(world: MultiWorld, player: int):

View File

@@ -5,7 +5,7 @@ from .Rules import set_rules
from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial
from .Options import ror2_options from .Options import ror2_options
from ..AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
client_version = 1 client_version = 1
@@ -28,7 +28,7 @@ class RiskOfRainWorld(World):
first crash landing. first crash landing.
""" """
game: str = "Risk of Rain 2" game: str = "Risk of Rain 2"
options = ror2_options option_definitions = ror2_options
topology_present = False topology_present = False
item_name_to_id = item_table item_name_to_id = item_table

View File

@@ -2,6 +2,7 @@ import typing
from BaseClasses import Item, ItemClassification from BaseClasses import Item, ItemClassification
from .Names import ItemName from .Names import ItemName
from worlds.alttp import ALTTPWorld
class ItemData(typing.NamedTuple): class ItemData(typing.NamedTuple):
@@ -18,9 +19,6 @@ class SA2BItem(Item):
def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None): def __init__(self, name, classification: ItemClassification, code: int = None, player: int = None):
super(SA2BItem, self).__init__(name, classification, code, player) super(SA2BItem, self).__init__(name, classification, code, player)
if self.name == ItemName.sonic_light_shoes or self.name == ItemName.shadow_air_shoes:
self.pedestal_credit_text = "and the Soap Shoes"
# Separate tables for each type of item. # Separate tables for each type of item.
emblems_table = { emblems_table = {
@@ -94,3 +92,6 @@ item_table = {
} }
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.sonic_light_shoes].code] = "and the Soap Shoes"
ALTTPWorld.pedestal_credit_texts[item_table[ItemName.shadow_air_shoes].code] = "and the Soap Shoes"

View File

@@ -49,7 +49,7 @@ class SA2BWorld(World):
Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth. Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth.
""" """
game: str = "Sonic Adventure 2 Battle" game: str = "Sonic Adventure 2 Battle"
options = sa2b_options option_definitions = sa2b_options
topology_present = False topology_present = False
data_version = 2 data_version = 2

View File

@@ -103,19 +103,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
lambda state: state._sc2wol_has_air(world, player)), lambda state: state._sc2wol_has_air(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001, LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001,
lambda state: state._sc2wol_has_air(world, player) or True), lambda state: True),
LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002,
lambda state: state._sc2wol_has_air(world, player)), lambda state: state._sc2wol_has_air(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
lambda state: state._sc2wol_able_to_rescue(world, player) or True), lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
lambda state: state._sc2wol_able_to_rescue(world, player) or True), lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005,
lambda state: state._sc2wol_able_to_rescue(world, player) or True), lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006,
lambda state: state._sc2wol_able_to_rescue(world, player) or True), lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
lambda state: state._sc2wol_able_to_rescue(world, player) or True), lambda state: state._sc2wol_able_to_rescue(world, player)),
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
lambda state: state._sc2wol_has_air(world, player)), lambda state: state._sc2wol_has_air(world, player)),
LocationData("The Moebius Factor", "Beat The Moebius Factor", None, LocationData("The Moebius Factor", "Beat The Moebius Factor", None,

View File

@@ -37,7 +37,7 @@ class SC2WoLLogic(LogicMixin):
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Orbital Strike'}, player) return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player)
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool: def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)

View File

@@ -37,7 +37,7 @@ class SC2WoLWorld(World):
item_name_to_id = {name: data.code for name, data in item_table.items()} item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None)} location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
options = sc2wol_options option_definitions = sc2wol_options
item_name_groups = item_name_groups item_name_groups = item_name_groups
locked_locations: typing.List[str] locked_locations: typing.List[str]

View File

@@ -5,7 +5,7 @@ import copy
import os import os
import threading import threading
import base64 import base64
from typing import Set, List, TextIO from typing import Set, TextIO
from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils
@@ -79,7 +79,7 @@ class SMWorld(World):
game: str = "Super Metroid" game: str = "Super Metroid"
topology_present = True topology_present = True
data_version = 1 data_version = 1
options = sm_options option_definitions = sm_options
item_names: Set[str] = frozenset(items_lookup_name_to_id) item_names: Set[str] = frozenset(items_lookup_name_to_id)
location_names: Set[str] = frozenset(locations_lookup_name_to_id) location_names: Set[str] = frozenset(locations_lookup_name_to_id)
item_name_to_id = items_lookup_name_to_id item_name_to_id = items_lookup_name_to_id
@@ -293,15 +293,15 @@ class SMWorld(World):
if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None: if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None:
# this SM world can find this item: write full item data to tables and assign player data for writing # this SM world can find this item: write full item data to tables and assign player data for writing
romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0 romPlayerID = itemLoc.item.player if itemLoc.item.player <= ROM_PLAYER_LIMIT else 0
if itemLoc.item.type in ItemManager.Items: if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items:
itemId = ItemManager.Items[itemLoc.item.type].Id itemId = ItemManager.Items[itemLoc.item.type].Id
else: else:
itemId = ItemManager.Items['ArchipelagoItem'].Id + idx itemId = ItemManager.Items['ArchipelagoItem'].Id + idx
multiWorldItems.append({"sym": symbols["message_item_names"], multiWorldItems.append({"sym": symbols["message_item_names"],
"offset": (vanillaItemTypesCount + idx)*64, "offset": (vanillaItemTypesCount + idx)*64,
"values": self.convertToROMItemName(itemLoc.item.name)}) "values": self.convertToROMItemName(itemLoc.item.name)})
idx += 1 idx += 1
if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()): if (romPlayerID > 0 and romPlayerID not in self.playerIDMap.keys()):
playerIDCount += 1 playerIDCount += 1
self.playerIDMap[romPlayerID] = playerIDCount self.playerIDMap[romPlayerID] = playerIDCount
@@ -488,7 +488,13 @@ class SMWorld(World):
# commit all the changes we've made here to the ROM # commit all the changes we've made here to the ROM
romPatcher.commitIPS() romPatcher.commitIPS()
itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.world.get_locations() if itemLoc.player == self.player] itemLocs = [
ItemLocation(ItemManager.Items[itemLoc.item.type
if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else
'ArchipelagoItem'],
locationsDict[itemLoc.name], True)
for itemLoc in self.world.get_locations() if itemLoc.player == self.player
]
romPatcher.writeItemsLocs(itemLocs) romPatcher.writeItemsLocs(itemLocs)
itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player] itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.world.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.world.get_locations() if itemLoc.item.player == self.player]
@@ -561,7 +567,7 @@ class SMWorld(World):
def fill_slot_data(self): def fill_slot_data(self):
slot_data = {} slot_data = {}
if not self.world.is_race: if not self.world.is_race:
for option_name in self.options: for option_name in self.option_definitions:
option = getattr(self.world, option_name)[self.player] option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = option.value slot_data[option_name] = option.value
@@ -735,7 +741,8 @@ class SMLocation(Location):
class SMItem(Item): class SMItem(Item):
game = "Super Metroid" game = "Super Metroid"
type: str
def __init__(self, name, classification, type, code, player: int = None): def __init__(self, name, classification, type: str, code, player: int):
super(SMItem, self).__init__(name, classification, code, player) super(SMItem, self).__init__(name, classification, code, player)
self.type = type self.type = type

View File

@@ -35,13 +35,11 @@ class SM64World(World):
location_name_to_id = location_table location_name_to_id = location_table
data_version = 6 data_version = 6
required_client_version = (0,3,0) required_client_version = (0, 3, 0)
forced_auto_forfeit = False
area_connections: typing.Dict[int, int] area_connections: typing.Dict[int, int]
options = sm64_options option_definitions = sm64_options
def generate_early(self): def generate_early(self):
self.topology_present = self.world.AreaRandomizer[self.player].value self.topology_present = self.world.AreaRandomizer[self.player].value
@@ -120,7 +118,7 @@ class SM64World(World):
"AreaRando": self.area_connections, "AreaRando": self.area_connections,
"FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value, "FirstBowserDoorCost": self.world.FirstBowserStarDoorCost[self.player].value,
"BasementDoorCost": self.world.BasementStarDoorCost[self.player].value, "BasementDoorCost": self.world.BasementStarDoorCost[self.player].value,
"SecondFloorCost": self.world.SecondFloorStarDoorCost[self.player].value, "SecondFloorDoorCost": self.world.SecondFloorStarDoorCost[self.player].value,
"MIPS1Cost": self.world.MIPS1Cost[self.player].value, "MIPS1Cost": self.world.MIPS1Cost[self.player].value,
"MIPS2Cost": self.world.MIPS2Cost[self.player].value, "MIPS2Cost": self.world.MIPS2Cost[self.player].value,
"StarsToFinish": self.world.StarsToFinish[self.player].value, "StarsToFinish": self.world.StarsToFinish[self.player].value,

View File

@@ -1,5 +1,5 @@
import typing import typing
from Options import Choice, Option from Options import Choice, Option, Toggle, DefaultOnToggle, Range
class SMLogic(Choice): class SMLogic(Choice):
"""This option selects what kind of logic to use for item placement inside """This option selects what kind of logic to use for item placement inside
@@ -45,6 +45,22 @@ class MorphLocation(Choice):
option_Original = 2 option_Original = 2
default = 0 default = 0
class Goal(Choice):
"""This option decides what goal is required to finish the randomizer.
Defeat Ganon and Mother Brain - Find the required crystals and boss tokens kill both bosses.
Fast Ganon and Defeat Mother Brain - The hole to ganon is open without having to defeat Agahnim in
Ganon's Tower and Ganon can be defeat as soon you have the required
crystals to make Ganon vulnerable. For keysanity, this mode also removes
the Crateria Boss Key requirement from Tourian to allow faster access.
All Dungeons and Defeat Mother Brain - Similar to "Defeat Ganon and Mother Brain", but also requires all dungeons
to be beaten including Castle Tower and Agahnim."""
display_name = "Goal"
option_DefeatBoth = 0
option_FastGanonDefeatMotherBrain = 1
option_AllDungeonsDefeatMotherBrain = 2
default = 0
class KeyShuffle(Choice): class KeyShuffle(Choice):
"""This option decides how dungeon items such as keys are shuffled. """This option decides how dungeon items such as keys are shuffled.
None - A Link to the Past dungeon items can only be placed inside the None - A Link to the Past dungeon items can only be placed inside the
@@ -55,9 +71,75 @@ class KeyShuffle(Choice):
option_Keysanity = 1 option_Keysanity = 1
default = 0 default = 0
class OpenTower(Range):
"""The amount of crystals required to be able to enter Ganon's Tower.
If this is set to Random, the amount can be found in-game on a sign next to Ganon's Tower."""
display_name = "Open Tower"
range_start = 0
range_end = 7
default = 7
class GanonVulnerable(Range):
"""The amount of crystals required to be able to harm Ganon. The amount can be found
in-game on a sign near the top of the Pyramid."""
display_name = "Ganon Vulnerable"
range_start = 0
range_end = 7
default = 7
class OpenTourian(Range):
"""The amount of boss tokens required to enter Tourian. The amount can be found in-game
on a sign above the door leading to the Tourian entrance."""
display_name = "Open Tourian"
range_start = 0
range_end = 4
default = 4
class SpinJumpsAnimation(Toggle):
"""Enable separate space/screw jump animations"""
display_name = "Spin Jumps Animation"
class HeartBeepSpeed(Choice):
"""Sets the speed of the heart beep sound in A Link to the Past."""
display_name = "Heart Beep Speed"
option_Off = 0
option_Quarter = 1
option_Half = 2
option_Normal = 3
option_Double = 4
alias_false = 0
default = 3
class HeartColor(Choice):
"""Changes the color of the hearts in the HUD for A Link to the Past."""
display_name = "Heart Color"
option_Red = 0
option_Green = 1
option_Blue = 2
option_Yellow = 3
default = 0
class QuickSwap(Toggle):
"""When enabled, lets you switch items in ALTTP with L/R"""
display_name = "Quick Swap"
class EnergyBeep(DefaultOnToggle):
"""Toggles the low health energy beep in Super Metroid."""
display_name = "Energy Beep"
smz3_options: typing.Dict[str, type(Option)] = { smz3_options: typing.Dict[str, type(Option)] = {
"sm_logic": SMLogic, "sm_logic": SMLogic,
"sword_location": SwordLocation, "sword_location": SwordLocation,
"morph_location": MorphLocation, "morph_location": MorphLocation,
"key_shuffle": KeyShuffle "goal": Goal,
"key_shuffle": KeyShuffle,
"open_tower": OpenTower,
"ganon_vulnerable": GanonVulnerable,
"open_tourian": OpenTourian,
"spin_jumps_animation": SpinJumpsAnimation,
"heart_beep_speed": HeartBeepSpeed,
"heart_color": HeartColor,
"quick_swap": QuickSwap,
"energy_beep": EnergyBeep
} }

View File

@@ -26,16 +26,42 @@ class MorphLocation(Enum):
class Goal(Enum): class Goal(Enum):
DefeatBoth = 0 DefeatBoth = 0
FastGanonDefeatMotherBrain = 1
AllDungeonsDefeatMotherBrain = 2
class KeyShuffle(Enum): class KeyShuffle(Enum):
Null = 0 Null = 0
Keysanity = 1 Keysanity = 1
class GanonInvincible(Enum): class OpenTower(Enum):
Never = 0 Random = -1
BeforeCrystals = 1 NoCrystals = 0
BeforeAllDungeons = 2 OneCrystal = 1
Always = 3 TwoCrystals = 2
ThreeCrystals = 3
FourCrystals = 4
FiveCrystals = 5
SixCrystals = 6
SevenCrystals = 7
class GanonVulnerable(Enum):
Random = -1
NoCrystals = 0
OneCrystal = 1
TwoCrystals = 2
ThreeCrystals = 3
FourCrystals = 4
FiveCrystals = 5
SixCrystals = 6
SevenCrystals = 7
class OpenTourian(Enum):
Random = -1
NoBosses = 0
OneBoss = 1
TwoBosses = 2
ThreeBosses = 3
FourBosses = 4
class Config: class Config:
GameMode: GameMode = GameMode.Multiworld GameMode: GameMode = GameMode.Multiworld
@@ -45,64 +71,20 @@ class Config:
MorphLocation: MorphLocation = MorphLocation.Randomized MorphLocation: MorphLocation = MorphLocation.Randomized
Goal: Goal = Goal.DefeatBoth Goal: Goal = Goal.DefeatBoth
KeyShuffle: KeyShuffle = KeyShuffle.Null KeyShuffle: KeyShuffle = KeyShuffle.Null
Keysanity: bool = KeyShuffle != KeyShuffle.Null
Race: bool = False Race: bool = False
GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals
MinimalAccessibility: bool = False # AP specific accessibility: minimal
def __init__(self, options: Dict[str, str]): OpenTower: OpenTower = OpenTower.SevenCrystals
self.GameMode = self.ParseOption(options, GameMode.Multiworld) GanonVulnerable: GanonVulnerable = GanonVulnerable.SevenCrystals
self.Z3Logic = self.ParseOption(options, Z3Logic.Normal) OpenTourian: OpenTourian = OpenTourian.FourBosses
self.SMLogic = self.ParseOption(options, SMLogic.Normal)
self.SwordLocation = self.ParseOption(options, SwordLocation.Randomized)
self.MorphLocation = self.ParseOption(options, MorphLocation.Randomized)
self.Goal = self.ParseOption(options, Goal.DefeatBoth)
self.GanonInvincible = self.ParseOption(options, GanonInvincible.BeforeCrystals)
self.KeyShuffle = self.ParseOption(options, KeyShuffle.Null)
self.Keysanity = self.KeyShuffle != KeyShuffle.Null
self.Race = self.ParseOptionWith(options, "Race", False)
def ParseOption(self, options:Dict[str, str], defaultValue:Enum): @property
enumKey = defaultValue.__class__.__name__.lower() def SingleWorld(self) -> bool:
if (enumKey in options): return self.GameMode == GameMode.Normal
return defaultValue.__class__[options[enumKey]]
return defaultValue @property
def Multiworld(self) -> bool:
return self.GameMode == GameMode.Multiworld
def ParseOptionWith(self, options:Dict[str, str], option:str, defaultValue:bool): @property
if (option.lower() in options): def Keysanity(self) -> bool:
return options[option.lower()] return self.KeyShuffle != KeyShuffle.Null
return defaultValue
""" public static RandomizerOption GetRandomizerOption<T>(string description, string defaultOption = "") where T : Enum {
var enumType = typeof(T);
var values = Enum.GetValues(enumType).Cast<Enum>();
return new RandomizerOption {
Key = enumType.Name.ToLower(),
Description = description,
Type = RandomizerOptionType.Dropdown,
Default = string.IsNullOrEmpty(defaultOption) ? GetDefaultValue<T>().ToLString() : defaultOption,
Values = values.ToDictionary(k => k.ToLString(), v => v.GetDescription())
};
}
public static RandomizerOption GetRandomizerOption(string name, string description, bool defaultOption = false) {
return new RandomizerOption {
Key = name.ToLower(),
Description = description,
Type = RandomizerOptionType.Checkbox,
Default = defaultOption.ToString().ToLower(),
Values = new Dictionary<string, string>()
};
}
public static TEnum GetDefaultValue<TEnum>() where TEnum : Enum {
Type t = typeof(TEnum);
var attributes = (DefaultValueAttribute[])t.GetCustomAttributes(typeof(DefaultValueAttribute), false);
if ((attributes?.Length ?? 0) > 0) {
return (TEnum)attributes.First().Value;
}
else {
return default;
}
} """

View File

@@ -130,6 +130,11 @@ class ItemType(Enum):
CardLowerNorfairL1 = 0xDE CardLowerNorfairL1 = 0xDE
CardLowerNorfairBoss = 0xDF CardLowerNorfairBoss = 0xDF
SmMapBrinstar = 0xCA
SmMapWreckedShip = 0xCB
SmMapMaridia = 0xCC
SmMapLowerNorfair = 0xCD
Missile = 0xC2 Missile = 0xC2
Super = 0xC3 Super = 0xC3
PowerBomb = 0xC4 PowerBomb = 0xC4
@@ -174,6 +179,7 @@ class Item:
map = re.compile("^Map") map = re.compile("^Map")
compass = re.compile("^Compass") compass = re.compile("^Compass")
keycard = re.compile("^Card") keycard = re.compile("^Card")
smMap = re.compile("^SmMap")
def IsDungeonItem(self): return self.dungeon.match(self.Type.name) def IsDungeonItem(self): return self.dungeon.match(self.Type.name)
def IsBigKey(self): return self.bigKey.match(self.Type.name) def IsBigKey(self): return self.bigKey.match(self.Type.name)
@@ -181,6 +187,7 @@ class Item:
def IsMap(self): return self.map.match(self.Type.name) def IsMap(self): return self.map.match(self.Type.name)
def IsCompass(self): return self.compass.match(self.Type.name) def IsCompass(self): return self.compass.match(self.Type.name)
def IsKeycard(self): return self.keycard.match(self.Type.name) def IsKeycard(self): return self.keycard.match(self.Type.name)
def IsSmMap(self): return self.smMap.match(self.Type.name)
def Is(self, type: ItemType, world): def Is(self, type: ItemType, world):
return self.Type == type and self.World == world return self.Type == type and self.World == world
@@ -313,7 +320,7 @@ class Item:
Item.AddRange(itemPool, 4, Item(ItemType.BombUpgrade5)) Item.AddRange(itemPool, 4, Item(ItemType.BombUpgrade5))
Item.AddRange(itemPool, 2, Item(ItemType.OneRupee)) Item.AddRange(itemPool, 2, Item(ItemType.OneRupee))
Item.AddRange(itemPool, 4, Item(ItemType.FiveRupees)) Item.AddRange(itemPool, 4, Item(ItemType.FiveRupees))
Item.AddRange(itemPool, 25 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees)) Item.AddRange(itemPool, 21 if world.Config.Keysanity else 28, Item(ItemType.TwentyRupees))
Item.AddRange(itemPool, 7, Item(ItemType.FiftyRupees)) Item.AddRange(itemPool, 7, Item(ItemType.FiftyRupees))
Item.AddRange(itemPool, 5, Item(ItemType.ThreeHundredRupees)) Item.AddRange(itemPool, 5, Item(ItemType.ThreeHundredRupees))
@@ -421,6 +428,21 @@ class Item:
return itemPool return itemPool
@staticmethod
def CreateSmMaps(world):
itemPool = [
Item(ItemType.SmMapBrinstar, world),
Item(ItemType.SmMapWreckedShip, world),
Item(ItemType.SmMapMaridia, world),
Item(ItemType.SmMapLowerNorfair, world)
]
for item in itemPool:
item.Progression = True
item.World = world
return itemPool
@staticmethod @staticmethod
def Get(items, itemType:ItemType): def Get(items, itemType:ItemType):
item = next((i for i in items if i.Type == itemType), None) item = next((i for i in items if i.Type == itemType), None)
@@ -725,7 +747,7 @@ class Progression:
def CanAccessMiseryMirePortal(self, config: Config): def CanAccessMiseryMirePortal(self, config: Config):
if (config.SMLogic == SMLogic.Normal): if (config.SMLogic == SMLogic.Normal):
return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and (self.Gravity and self.SpaceJump) and self.CanUsePowerBombs() return (self.CardNorfairL2 or (self.SpeedBooster and self.Wave)) and self.Varia and self.Super and self.Gravity and self.SpaceJump and self.CanUsePowerBombs()
else: else:
return (self.CardNorfairL2 or self.SpeedBooster) and self.Varia and self.Super and \ return (self.CardNorfairL2 or self.SpeedBooster) and self.Varia and self.Super and \
(self.CanFly() or self.HiJump or self.SpeedBooster or self.CanSpringBallJump() or self.Ice) \ (self.CanFly() or self.HiJump or self.SpeedBooster or self.CanSpringBallJump() or self.Ice) \
@@ -769,11 +791,11 @@ class Progression:
if (world.Config.SMLogic == SMLogic.Normal): if (world.Config.SMLogic == SMLogic.Normal):
return self.MoonPearl and self.Flippers and \ return self.MoonPearl and self.Flippers and \
self.Gravity and self.Morph and \ self.Gravity and self.Morph and \
(world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy())
else: else:
return self.MoonPearl and self.Flippers and \ return self.MoonPearl and self.Flippers and \
(self.CanSpringBallJump() or self.HiJump or self.Gravity) and self.Morph and \ (self.CanSpringBallJump() or self.HiJump or self.Gravity) and self.Morph and \
(world.CanAquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy())
# Start of AP integration # Start of AP integration
items_start_id = 84000 items_start_id = 84000

View File

@@ -6,7 +6,7 @@ import typing
from BaseClasses import Location from BaseClasses import Location
from worlds.smz3.TotalSMZ3.Item import Item, ItemType from worlds.smz3.TotalSMZ3.Item import Item, ItemType
from worlds.smz3.TotalSMZ3.Location import LocationType from worlds.smz3.TotalSMZ3.Location import LocationType
from worlds.smz3.TotalSMZ3.Region import IMedallionAccess, IReward, RewardType, SMRegion, Z3Region from worlds.smz3.TotalSMZ3.Region import IReward, RewardType, SMRegion, Z3Region
from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace
from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace
from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera
@@ -18,10 +18,14 @@ from worlds.smz3.TotalSMZ3.Regions.Zelda.IcePalace import IcePalace
from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire
from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock
from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower
from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Kraid import Kraid
from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.WreckedShip import WreckedShip
from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Maridia.Inner import Inner
from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairLower.East import East
from worlds.smz3.TotalSMZ3.Text.StringTable import StringTable from worlds.smz3.TotalSMZ3.Text.StringTable import StringTable
from worlds.smz3.TotalSMZ3.World import World from worlds.smz3.TotalSMZ3.World import World
from worlds.smz3.TotalSMZ3.Config import Config, GameMode, GanonInvincible from worlds.smz3.TotalSMZ3.Config import Config, OpenTourian, Goal
from worlds.smz3.TotalSMZ3.Text.Texts import Texts from worlds.smz3.TotalSMZ3.Text.Texts import Texts
from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog
@@ -30,6 +34,11 @@ class KeycardPlaque:
Level2 = 0xe1 Level2 = 0xe1
Boss = 0xe2 Boss = 0xe2
Null = 0x00 Null = 0x00
Zero = 0xe3
One = 0xe4
Two = 0xe5
Three = 0xe6
Four = 0xe7
class KeycardDoors: class KeycardDoors:
Left = 0xd414 Left = 0xd414
@@ -73,8 +82,8 @@ class DropPrize(Enum):
Fairy = 0xE3 Fairy = 0xE3
class Patch: class Patch:
Major = 0 Major = 11
Minor = 1 Minor = 3
allWorlds: List[World] allWorlds: List[World]
myWorld: World myWorld: World
seedGuid: str seedGuid: str
@@ -105,13 +114,16 @@ class Patch:
self.WriteDiggingGameRng() self.WriteDiggingGameRng()
self.WritePrizeShuffle() self.WritePrizeShuffle(self.myWorld.WorldState.DropPrizes)
self.WriteRemoveEquipmentFromUncle( self.myWorld.GetLocation("Link's Uncle").APLocation.item.item if self.WriteRemoveEquipmentFromUncle( self.myWorld.GetLocation("Link's Uncle").APLocation.item.item if
self.myWorld.GetLocation("Link's Uncle").APLocation.item.game == "SMZ3" else self.myWorld.GetLocation("Link's Uncle").APLocation.item.game == "SMZ3" else
Item(ItemType.Something)) Item(ItemType.Something))
self.WriteGanonInvicible(config.GanonInvincible) self.WriteGanonInvicible(config.Goal)
self.WritePreOpenPyramid(config.Goal)
self.WriteCrystalsNeeded(self.myWorld.TowerCrystals, self.myWorld.GanonCrystals)
self.WriteBossesNeeded(self.myWorld.TourianBossTokens)
self.WriteRngBlock() self.WriteRngBlock()
self.WriteSaveAndQuitFromBossRoom() self.WriteSaveAndQuitFromBossRoom()
@@ -135,26 +147,27 @@ class Patch:
return {patch[0]:patch[1] for patch in self.patches} return {patch[0]:patch[1] for patch in self.patches}
def WriteMedallions(self): def WriteMedallions(self):
from worlds.smz3.TotalSMZ3.WorldState import Medallion
turtleRock = next(region for region in self.myWorld.Regions if isinstance(region, TurtleRock)) turtleRock = next(region for region in self.myWorld.Regions if isinstance(region, TurtleRock))
miseryMire = next(region for region in self.myWorld.Regions if isinstance(region, MiseryMire)) miseryMire = next(region for region in self.myWorld.Regions if isinstance(region, MiseryMire))
turtleRockAddresses = [0x308023, 0xD020, 0xD0FF, 0xD1DE ] turtleRockAddresses = [0x308023, 0xD020, 0xD0FF, 0xD1DE ]
miseryMireAddresses = [ 0x308022, 0xCFF2, 0xD0D1, 0xD1B0 ] miseryMireAddresses = [ 0x308022, 0xCFF2, 0xD0D1, 0xD1B0 ]
if turtleRock.Medallion == ItemType.Bombos: if turtleRock.Medallion == Medallion.Bombos:
turtleRockValues = [0x00, 0x51, 0x10, 0x00] turtleRockValues = [0x00, 0x51, 0x10, 0x00]
elif turtleRock.Medallion == ItemType.Ether: elif turtleRock.Medallion == Medallion.Ether:
turtleRockValues = [0x01, 0x51, 0x18, 0x00] turtleRockValues = [0x01, 0x51, 0x18, 0x00]
elif turtleRock.Medallion == ItemType.Quake: elif turtleRock.Medallion == Medallion.Quake:
turtleRockValues = [0x02, 0x14, 0xEF, 0xC4] turtleRockValues = [0x02, 0x14, 0xEF, 0xC4]
else: else:
raise exception(f"Tried using {turtleRock.Medallion} in place of Turtle Rock medallion") raise exception(f"Tried using {turtleRock.Medallion} in place of Turtle Rock medallion")
if miseryMire.Medallion == ItemType.Bombos: if miseryMire.Medallion == Medallion.Bombos:
miseryMireValues = [0x00, 0x51, 0x00, 0x00] miseryMireValues = [0x00, 0x51, 0x00, 0x00]
elif miseryMire.Medallion == ItemType.Ether: elif miseryMire.Medallion == Medallion.Ether:
miseryMireValues = [0x01, 0x13, 0x9F, 0xF1] miseryMireValues = [0x01, 0x13, 0x9F, 0xF1]
elif miseryMire.Medallion == ItemType.Quake: elif miseryMire.Medallion == Medallion.Quake:
miseryMireValues = [0x02, 0x51, 0x08, 0x00] miseryMireValues = [0x02, 0x51, 0x08, 0x00]
else: else:
raise exception(f"Tried using {miseryMire.Medallion} in place of Misery Mire medallion") raise exception(f"Tried using {miseryMire.Medallion} in place of Misery Mire medallion")
@@ -174,12 +187,19 @@ class Patch:
self.rnd.shuffle(pendantsBlueRed) self.rnd.shuffle(pendantsBlueRed)
pendantRewards = pendantsGreen + pendantsBlueRed pendantRewards = pendantsGreen + pendantsBlueRed
bossTokens = [ 1, 2, 3, 4 ]
regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)] regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)]
crystalRegions = [region for region in regions if region.Reward == RewardType.CrystalBlue] + [region for region in regions if region.Reward == RewardType.CrystalRed] crystalRegions = [region for region in regions if region.Reward == RewardType.CrystalBlue] + [region for region in regions if region.Reward == RewardType.CrystalRed]
pendantRegions = [region for region in regions if region.Reward == RewardType.PendantGreen] + [region for region in regions if region.Reward == RewardType.PendantNonGreen] pendantRegions = [region for region in regions if region.Reward == RewardType.PendantGreen] + [region for region in regions if region.Reward == RewardType.PendantNonGreen]
bossRegions = [region for region in regions if region.Reward == RewardType.BossTokenKraid] + \
[region for region in regions if region.Reward == RewardType.BossTokenPhantoon] + \
[region for region in regions if region.Reward == RewardType.BossTokenDraygon] + \
[region for region in regions if region.Reward == RewardType.BossTokenRidley]
self.patches += self.RewardPatches(crystalRegions, crystalRewards, self.CrystalValues) self.patches += self.RewardPatches(crystalRegions, crystalRewards, self.CrystalValues)
self.patches += self.RewardPatches(pendantRegions, pendantRewards, self.PendantValues) self.patches += self.RewardPatches(pendantRegions, pendantRewards, self.PendantValues)
self.patches += self.RewardPatches(bossRegions, bossTokens, self.BossTokenValues)
def RewardPatches(self, regions: List[IReward], rewards: List[int], rewardValues: Callable): def RewardPatches(self, regions: List[IReward], rewards: List[int], rewardValues: Callable):
addresses = [self.RewardAddresses(region) for region in regions] addresses = [self.RewardAddresses(region) for region in regions]
@@ -189,17 +209,22 @@ class Patch:
def RewardAddresses(self, region: IReward): def RewardAddresses(self, region: IReward):
regionType = { regionType = {
EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE ], EasternPalace : [ 0x2A09D, 0xABEF8, 0xABEF9, 0x308052, 0x30807C, 0x1C6FE, 0x30D100],
DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF ], DesertPalace : [ 0x2A09E, 0xABF1C, 0xABF1D, 0x308053, 0x308078, 0x1C6FF, 0x30D101 ],
TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706 ], TowerOfHera : [ 0x2A0A5, 0xABF0A, 0xABF0B, 0x30805A, 0x30807A, 0x1C706, 0x30D102 ],
PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702 ], PalaceOfDarkness : [ 0x2A0A1, 0xABF00, 0xABF01, 0x308056, 0x30807D, 0x1C702, 0x30D103 ],
SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701 ], SwampPalace : [ 0x2A0A0, 0xABF6C, 0xABF6D, 0x308055, 0x308071, 0x1C701, 0x30D104 ],
SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704 ], SkullWoods : [ 0x2A0A3, 0xABF12, 0xABF13, 0x308058, 0x30807B, 0x1C704, 0x30D105 ],
ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707 ], ThievesTown : [ 0x2A0A6, 0xABF36, 0xABF37, 0x30805B, 0x308077, 0x1C707, 0x30D106 ],
IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705 ], IcePalace : [ 0x2A0A4, 0xABF5A, 0xABF5B, 0x308059, 0x308073, 0x1C705, 0x30D107 ],
MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703 ], MiseryMire : [ 0x2A0A2, 0xABF48, 0xABF49, 0x308057, 0x308075, 0x1C703, 0x30D108 ],
TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708 ] TurtleRock : [ 0x2A0A7, 0xABF24, 0xABF25, 0x30805C, 0x308079, 0x1C708, 0x30D109 ],
Kraid : [ 0xF26002, 0xF26004, 0xF26005, 0xF26000, 0xF26006, 0xF26007, 0x82FD36 ],
WreckedShip : [ 0xF2600A, 0xF2600C, 0xF2600D, 0xF26008, 0xF2600E, 0xF2600F, 0x82FE26 ],
Inner : [ 0xF26012, 0xF26014, 0xF26015, 0xF26010, 0xF26016, 0xF26017, 0x82FE76 ],
East : [ 0xF2601A, 0xF2601C, 0xF2601D, 0xF26018, 0xF2601E, 0xF2601F, 0x82FDD6 ]
} }
result = regionType.get(type(region), None) result = regionType.get(type(region), None)
if result is None: if result is None:
raise exception(f"Region {region} should not be a dungeon reward region") raise exception(f"Region {region} should not be a dungeon reward region")
@@ -208,13 +233,13 @@ class Patch:
def CrystalValues(self, crystal: int): def CrystalValues(self, crystal: int):
crystalMap = { crystalMap = {
1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06 ], 1 : [ 0x02, 0x34, 0x64, 0x40, 0x7F, 0x06, 0x10 ],
2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06 ], 2 : [ 0x10, 0x34, 0x64, 0x40, 0x79, 0x06, 0x10 ],
3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06 ], 3 : [ 0x40, 0x34, 0x64, 0x40, 0x6C, 0x06, 0x10 ],
4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06 ], 4 : [ 0x20, 0x34, 0x64, 0x40, 0x6D, 0x06, 0x10 ],
5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06 ], 5 : [ 0x04, 0x32, 0x64, 0x40, 0x6E, 0x06, 0x11 ],
6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06 ], 6 : [ 0x01, 0x32, 0x64, 0x40, 0x6F, 0x06, 0x11 ],
7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06 ], 7 : [ 0x08, 0x34, 0x64, 0x40, 0x7C, 0x06, 0x10 ],
} }
result = crystalMap.get(crystal, None) result = crystalMap.get(crystal, None)
if result is None: if result is None:
@@ -224,15 +249,28 @@ class Patch:
def PendantValues(self, pendant: int): def PendantValues(self, pendant: int):
pendantMap = { pendantMap = {
1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01 ], 1 : [ 0x04, 0x38, 0x62, 0x00, 0x69, 0x01, 0x12 ],
2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03 ], 2 : [ 0x01, 0x32, 0x60, 0x00, 0x69, 0x03, 0x14 ],
3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02 ], 3 : [ 0x02, 0x34, 0x60, 0x00, 0x69, 0x02, 0x13 ]
} }
result = pendantMap.get(pendant, None) result = pendantMap.get(pendant, None)
if result is None: if result is None:
raise exception(f"Tried using {pendant} as a pendant number") raise exception(f"Tried using {pendant} as a pendant number")
else: else:
return result return result
def BossTokenValues(self, token: int):
tokenMap = {
1 : [ 0x01, 0x38, 0x40, 0x80, 0x69, 0x80, 0x15 ],
2 : [ 0x02, 0x34, 0x42, 0x80, 0x69, 0x81, 0x16 ],
3 : [ 0x04, 0x34, 0x44, 0x80, 0x69, 0x82, 0x17 ],
4 : [ 0x08, 0x32, 0x46, 0x80, 0x69, 0x83, 0x18 ]
}
result = tokenMap.get(token, None)
if result is None:
raise exception(f"Tried using {token} as a boss token number")
else:
return result
def WriteSMLocations(self, locations: List[Location]): def WriteSMLocations(self, locations: List[Location]):
def GetSMItemPLM(location:Location): def GetSMItemPLM(location:Location):
@@ -259,7 +297,7 @@ class Patch:
ItemType.SpaceJump : 0xEF1B, ItemType.SpaceJump : 0xEF1B,
ItemType.ScrewAttack : 0xEF1F ItemType.ScrewAttack : 0xEF1F
} }
plmId = 0xEFE0 if self.myWorld.Config.GameMode == GameMode.Multiworld else \ plmId = 0xEFE0 if self.myWorld.Config.Multiworld else \
itemMap.get(location.APLocation.item.item.Type, 0xEFE0) itemMap.get(location.APLocation.item.item.Type, 0xEFE0)
if (plmId == 0xEFE0): if (plmId == 0xEFE0):
plmId += 4 if location.Type == LocationType.Chozo else 8 if location.Type == LocationType.Hidden else 0 plmId += 4 if location.Type == LocationType.Chozo else 8 if location.Type == LocationType.Hidden else 0
@@ -268,7 +306,7 @@ class Patch:
return plmId return plmId
for location in locations: for location in locations:
if (self.myWorld.Config.GameMode == GameMode.Multiworld): if (self.myWorld.Config.Multiworld):
self.patches.append((Snes(location.Address), getWordArray(GetSMItemPLM(location)))) self.patches.append((Snes(location.Address), getWordArray(GetSMItemPLM(location))))
self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location))) self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location)))
else: else:
@@ -283,18 +321,14 @@ class Patch:
self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB])) self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB]))
elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]): elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]):
text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something)) text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something))
dialog = Dialog.Simple(text)
if (location.Type == LocationType.Pedestal): if (location.Type == LocationType.Pedestal):
self.stringTable.SetPedestalText(text) self.stringTable.SetPedestalText(text)
self.patches.append((Snes(0x308300), dialog))
elif (location.Type == LocationType.Ether): elif (location.Type == LocationType.Ether):
self.stringTable.SetEtherText(text) self.stringTable.SetEtherText(text)
self.patches.append((Snes(0x308F00), dialog))
elif (location.Type == LocationType.Bombos): elif (location.Type == LocationType.Bombos):
self.stringTable.SetBombosText(text) self.stringTable.SetBombosText(text)
self.patches.append((Snes(0x309000), dialog))
if (self.myWorld.Config.GameMode == GameMode.Multiworld): if (self.myWorld.Config.Multiworld):
self.patches.append((Snes(location.Address), [(location.Id - 256)])) self.patches.append((Snes(location.Address), [(location.Id - 256)]))
self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location))) self.patches.append(self.ItemTablePatch(location, self.GetZ3ItemId(location)))
else: else:
@@ -305,11 +339,11 @@ class Patch:
item = location.APLocation.item.item item = location.APLocation.item.item
itemDungeon = None itemDungeon = None
if item.IsKey(): if item.IsKey():
itemDungeon = ItemType.Key if (not item.World.Config.Keysanity or item.Type != ItemType.KeyHC) else ItemType.KeyHC itemDungeon = ItemType.Key
elif item.IsBigKey(): elif item.IsBigKey():
itemDungeon = ItemType.BigKey itemDungeon = ItemType.BigKey
elif item.IsMap(): elif item.IsMap():
itemDungeon = ItemType.Map if (not item.World.Config.Keysanity or item.Type != ItemType.MapHC) else ItemType.MapHC itemDungeon = ItemType.Map
elif item.IsCompass(): elif item.IsCompass():
itemDungeon = ItemType.Compass itemDungeon = ItemType.Compass
@@ -327,15 +361,11 @@ class Patch:
def WriteDungeonMusic(self, keysanity: bool): def WriteDungeonMusic(self, keysanity: bool):
if (not keysanity): if (not keysanity):
regions = [region for region in self.myWorld.Regions if isinstance(region, IReward)] regions = [region for region in self.myWorld.Regions if isinstance(region, Z3Region) and isinstance(region, IReward) and
music = [] region.Reward != None and region.Reward != RewardType.Agahnim]
pendantRegions = [region for region in regions if region.Reward in [RewardType.PendantGreen, RewardType.PendantNonGreen]] pendantRegions = [region for region in regions if region.Reward in [RewardType.PendantGreen, RewardType.PendantNonGreen]]
crystalRegions = [region for region in regions if region.Reward in [RewardType.CrystalBlue, RewardType.CrystalRed]] crystalRegions = [region for region in regions if region.Reward in [RewardType.CrystalBlue, RewardType.CrystalRed]]
regions = pendantRegions + crystalRegions music = [0x11 if (region.Reward == RewardType.PendantGreen or region.Reward == RewardType.PendantNonGreen) else 0x16 for region in regions]
music = [
0x11, 0x11, 0x11, 0x16, 0x16,
0x16, 0x16, 0x16, 0x16, 0x16,
]
self.patches += self.MusicPatches(regions, music) self.patches += self.MusicPatches(regions, music)
#IEnumerable<byte> RandomDungeonMusic() { #IEnumerable<byte> RandomDungeonMusic() {
@@ -366,51 +396,13 @@ class Patch:
else: else:
return result return result
def WritePrizeShuffle(self): def WritePrizeShuffle(self, dropPrizes):
prizePackItems = 56 self.patches.append((Snes(0x6FA78), [e.value for e in dropPrizes.Packs]))
treePullItems = 3 self.patches.append((Snes(0x1DFBD4), [e.value for e in dropPrizes.TreePulls]))
self.patches.append((Snes(0x6A9C8), [dropPrizes.CrabContinous.value]))
bytes = [] self.patches.append((Snes(0x6A9C4), [dropPrizes.CrabFinal.value]))
drop = 0 self.patches.append((Snes(0x6F993), [dropPrizes.Stun.value]))
final = 0 self.patches.append((Snes(0x1D82CC), [dropPrizes.Fish.value]))
pool = [
DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, DropPrize.Heart, DropPrize.Heart, DropPrize.Green, #// pack 1
DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Red, DropPrize.Blue, DropPrize.Green, DropPrize.Blue, DropPrize.Blue, #// pack 2
DropPrize.FullMagic, DropPrize.Magic, DropPrize.Magic, DropPrize.Blue, DropPrize.FullMagic, DropPrize.Magic, DropPrize.Heart, DropPrize.Magic, #// pack 3
DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb4, DropPrize.Bomb1, DropPrize.Bomb1, DropPrize.Bomb8, DropPrize.Bomb1, #// pack 4
DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10, DropPrize.Arrow5, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Arrow10,#// pack 5
DropPrize.Magic, DropPrize.Green, DropPrize.Heart, DropPrize.Arrow5, DropPrize.Magic, DropPrize.Bomb1, DropPrize.Green, DropPrize.Heart, #// pack 6
DropPrize.Heart, DropPrize.Fairy, DropPrize.FullMagic, DropPrize.Red, DropPrize.Bomb8, DropPrize.Heart, DropPrize.Red, DropPrize.Arrow10, #// pack 7
DropPrize.Green, DropPrize.Blue, DropPrize.Red,#// from pull trees
DropPrize.Green, DropPrize.Red,#// from prize crab
DropPrize.Green, #// stunned prize
DropPrize.Red,#// saved fish prize
]
prizes = pool
self.rnd.shuffle(prizes)
#/* prize pack drop order */
(bytes, prizes) = SplitOff(prizes, prizePackItems)
self.patches.append((Snes(0x6FA78), [byte.value for byte in bytes]))
#/* tree pull prizes */
(bytes, prizes) = SplitOff(prizes, treePullItems)
self.patches.append((Snes(0x1DFBD4), [byte.value for byte in bytes]))
#/* crab prizes */
(drop, final, prizes) = (prizes[0], prizes[1], prizes[2:])
self.patches.append((Snes(0x6A9C8), [ drop.value ]))
self.patches.append((Snes(0x6A9C4), [ final.value ]))
#/* stun prize */
(drop, prizes) = (prizes[0], prizes[1:])
self.patches.append((Snes(0x6F993), [ drop.value ]))
#/* fish prize */
drop = prizes[0]
self.patches.append((Snes(0x1D82CC), [ drop.value ]))
self.patches += self.EnemyPrizePackDistribution() self.patches += self.EnemyPrizePackDistribution()
@@ -524,46 +516,29 @@ class Patch:
redCrystalDungeons = [region for region in regions if region.Reward == RewardType.CrystalRed] redCrystalDungeons = [region for region in regions if region.Reward == RewardType.CrystalRed]
sahasrahla = Texts.SahasrahlaReveal(greenPendantDungeon) sahasrahla = Texts.SahasrahlaReveal(greenPendantDungeon)
self.patches.append((Snes(0x308A00), Dialog.Simple(sahasrahla)))
self.stringTable.SetSahasrahlaRevealText(sahasrahla) self.stringTable.SetSahasrahlaRevealText(sahasrahla)
bombShop = Texts.BombShopReveal(redCrystalDungeons) bombShop = Texts.BombShopReveal(redCrystalDungeons)
self.patches.append((Snes(0x308E00), Dialog.Simple(bombShop)))
self.stringTable.SetBombShopRevealText(bombShop) self.stringTable.SetBombShopRevealText(bombShop)
blind = Texts.Blind(self.rnd) blind = Texts.Blind(self.rnd)
self.patches.append((Snes(0x308800), Dialog.Simple(blind)))
self.stringTable.SetBlindText(blind) self.stringTable.SetBlindText(blind)
tavernMan = Texts.TavernMan(self.rnd) tavernMan = Texts.TavernMan(self.rnd)
self.patches.append((Snes(0x308C00), Dialog.Simple(tavernMan)))
self.stringTable.SetTavernManText(tavernMan) self.stringTable.SetTavernManText(tavernMan)
ganon = Texts.GanonFirstPhase(self.rnd) ganon = Texts.GanonFirstPhase(self.rnd)
self.patches.append((Snes(0x308600), Dialog.Simple(ganon)))
self.stringTable.SetGanonFirstPhaseText(ganon) self.stringTable.SetGanonFirstPhaseText(ganon)
#// Todo: Verify these two are correct if ganon invincible patch is ever added
#// ganon_fall_in_alt in v30
ganonFirstPhaseInvincible = "You think you\nare ready to\nface me?\n\nI will not die\n\nunless you\ncomplete your\ngoals. Dingus!"
self.patches.append((Snes(0x309100), Dialog.Simple(ganonFirstPhaseInvincible)))
#// ganon_phase_3_alt in v30
ganonThirdPhaseInvincible = "Got wax in\nyour ears?\nI cannot die!"
self.patches.append((Snes(0x309200), Dialog.Simple(ganonThirdPhaseInvincible)))
#// ---
silversLocation = [loc for world in self.allWorlds for loc in world.Locations if loc.ItemIs(ItemType.SilverArrows, self.myWorld)] silversLocation = [loc for world in self.allWorlds for loc in world.Locations if loc.ItemIs(ItemType.SilverArrows, self.myWorld)]
if len(silversLocation) == 0: if len(silversLocation) == 0:
silvers = Texts.GanonThirdPhaseMulti(None, self.myWorld, self.silversWorldID, self.playerIDToNames[self.silversWorldID]) silvers = Texts.GanonThirdPhaseMulti(None, self.myWorld, self.silversWorldID, self.playerIDToNames[self.silversWorldID])
else: else:
silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.GameMode == GameMode.Multiworld else \ silvers = Texts.GanonThirdPhaseMulti(silversLocation[0].Region, self.myWorld) if config.Multiworld else \
Texts.GanonThirdPhaseSingle(silversLocation[0].Region) Texts.GanonThirdPhaseSingle(silversLocation[0].Region)
self.patches.append((Snes(0x308700), Dialog.Simple(silvers)))
self.stringTable.SetGanonThirdPhaseText(silvers) self.stringTable.SetGanonThirdPhaseText(silvers)
triforceRoom = Texts.TriforceRoom(self.rnd) triforceRoom = Texts.TriforceRoom(self.rnd)
self.patches.append((Snes(0x308400), Dialog.Simple(triforceRoom)))
self.stringTable.SetTriforceRoomText(triforceRoom) self.stringTable.SetTriforceRoomText(triforceRoom)
def WriteStringTable(self): def WriteStringTable(self):
@@ -579,26 +554,32 @@ class Patch:
return bytearray(name, 'utf8') return bytearray(name, 'utf8')
def WriteSeedData(self): def WriteSeedData(self):
configField = \ configField1 = \
((1 if self.myWorld.Config.Race else 0) << 15) | \ ((1 if self.myWorld.Config.Race else 0) << 15) | \
((1 if self.myWorld.Config.Keysanity else 0) << 13) | \ ((1 if self.myWorld.Config.Keysanity else 0) << 13) | \
((1 if self.myWorld.Config.GameMode == Config.GameMode.Multiworld else 0) << 12) | \ ((1 if self.myWorld.Config.Multiworld else 0) << 12) | \
(self.myWorld.Config.Z3Logic.value << 10) | \ (self.myWorld.Config.Z3Logic.value << 10) | \
(self.myWorld.Config.SMLogic.value << 8) | \ (self.myWorld.Config.SMLogic.value << 8) | \
(Patch.Major << 4) | \ (Patch.Major << 4) | \
(Patch.Minor << 0) (Patch.Minor << 0)
configField2 = \
((1 if self.myWorld.Config.SwordLocation else 0) << 14) | \
((1 if self.myWorld.Config.MorphLocation else 0) << 12) | \
((1 if self.myWorld.Config.Goal else 0) << 8)
self.patches.append((Snes(0x80FF50), getWordArray(self.myWorld.Id))) self.patches.append((Snes(0x80FF50), getWordArray(self.myWorld.Id)))
self.patches.append((Snes(0x80FF52), getWordArray(configField))) self.patches.append((Snes(0x80FF52), getWordArray(configField1)))
self.patches.append((Snes(0x80FF54), getDoubleWordArray(self.seed))) self.patches.append((Snes(0x80FF54), getDoubleWordArray(self.seed)))
self.patches.append((Snes(0x80FF58), getWordArray(configField2)))
#/* Reserve the rest of the space for future use */ #/* Reserve the rest of the space for future use */
self.patches.append((Snes(0x80FF58), [0x00] * 8)) self.patches.append((Snes(0x80FF5A), [0x00] * 6))
self.patches.append((Snes(0x80FF60), bytearray(self.seedGuid, 'utf8'))) self.patches.append((Snes(0x80FF60), bytearray(self.seedGuid, 'utf8')))
self.patches.append((Snes(0x80FF80), bytearray(self.myWorld.Guid, 'utf8'))) self.patches.append((Snes(0x80FF80), bytearray(self.myWorld.Guid, 'utf8')))
def WriteCommonFlags(self): def WriteCommonFlags(self):
#/* Common Combo Configuration flags at [asm]/config.asm */ #/* Common Combo Configuration flags at [asm]/config.asm */
if (self.myWorld.Config.GameMode == GameMode.Multiworld): if (self.myWorld.Config.Multiworld):
self.patches.append((Snes(0xF47000), getWordArray(0x0001))) self.patches.append((Snes(0xF47000), getWordArray(0x0001)))
if (self.myWorld.Config.Keysanity): if (self.myWorld.Config.Keysanity):
self.patches.append((Snes(0xF47006), getWordArray(0x0001))) self.patches.append((Snes(0xF47006), getWordArray(0x0001)))
@@ -619,97 +600,104 @@ class Patch:
if (self.myWorld.Config.Keysanity): if (self.myWorld.Config.Keysanity):
self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item self.patches.append((Snes(0x40003B), [ 1 ])) #// MapMode #$00 = Always On (default) - #$01 = Require Map Item
self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass self.patches.append((Snes(0x400045), [ 0x0f ])) #// display ----dcba a: Small Keys, b: Big Key, c: Map, d: Compass
self.patches.append((Snes(0x40016A), [ 0x01 ])) #// enable local item dialog boxes for dungeon and keycard items self.patches.append((Snes(0x40016A), [ 0x01 ])) #// FreeItemText: db #$01 ; #00 = Off (default) - #$01 = On
def WriteSMKeyCardDoors(self): def WriteSMKeyCardDoors(self):
if (not self.myWorld.Config.Keysanity): plaquePlm = 0xd410
return
plaquePLm = 0xd410
doorList = [
#// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created)
#// Crateria
[ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet
[ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB
[ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door)
[ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4
[ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo
#// Brinstar
[ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room
[ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area
[ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn
[ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank
[ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super
[ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid
#// Upper Norfair
[ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice
[ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice
[ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain
[ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain
[ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain
[ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain
[ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire
#// Lower Norfair
[ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre
[ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry"
[ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley
#// Maridia
[ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia
[ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach
[ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon
[ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon
[ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon
[ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking)
#// Wrecked Ship
[ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check
[ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley
[ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley
[ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon
]
doorId = 0x0000
plmTablePos = 0xf800 plmTablePos = 0xf800
for door in doorList:
doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3]
if (door[6] == 0):
#// Write dynamic door
doorData = []
for x in door[0:3]:
doorData += getWordArray(x)
doorData += getWordArray(doorArgs)
self.patches.append((Snes(0x8f0000 + plmTablePos), doorData))
plmTablePos += 0x08
else:
#// Overwrite existing door
doorData = []
for x in door[1:3]:
doorData += getWordArray(x)
doorData += getWordArray(doorArgs)
self.patches.append((Snes(0x8f0000 + door[6]), doorData))
if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss):
#// Overwrite the extra parts of the Gadora with a PLM that just deletes itself
self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ]))
#// Plaque data if ( self.myWorld.Config.Keysanity):
if (door[4] != KeycardPlaque.Null): doorList = [
plaqueData = getWordArray(door[0]) + getWordArray(plaquePLm) + getWordArray(door[5]) + getWordArray(door[4]) #// RoomId Door Facing yyxx Keycard Event Type Plaque type yyxx, Address (if 0 a dynamic PLM is created)
self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData)) #// Crateria
plmTablePos += 0x08 [ 0x91F8, KeycardDoors.Right, 0x2601, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x2400, 0x0000 ], #// Crateria - Landing Site - Door to gauntlet
doorId += 1 [ 0x91F8, KeycardDoors.Left, 0x168E, KeycardEvents.CrateriaLevel1, KeycardPlaque.Level1, 0x148F, 0x801E ], #// Crateria - Landing Site - Door to landing site PB
[ 0x948C, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaLevel2, KeycardPlaque.Level2, 0x042F, 0x8222 ], #// Crateria - Before Moat - Door to moat (overwrite PB door)
[ 0x99BD, KeycardDoors.Left, 0x660E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x640F, 0x8470 ], #// Crateria - Before G4 - Door to G4
[ 0x9879, KeycardDoors.Left, 0x062E, KeycardEvents.CrateriaBoss, KeycardPlaque.Boss, 0x042F, 0x8420 ], #// Crateria - Before BT - Door to Bomb Torizo
#// Brinstar
[ 0x9F11, KeycardDoors.Left, 0x060E, KeycardEvents.BrinstarLevel1, KeycardPlaque.Level1, 0x040F, 0x8784 ], #// Brinstar - Blue Brinstar - Door to ceiling e-tank room
[ 0x9AD9, KeycardDoors.Right, 0xA601, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0xA400, 0x0000 ], #// Brinstar - Green Brinstar - Door to etecoon area
[ 0x9D9C, KeycardDoors.Down, 0x0336, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x0234, 0x863A ], #// Brinstar - Pink Brinstar - Door to spore spawn
[ 0xA130, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x141F, 0x881C ], #// Brinstar - Pink Brinstar - Door to wave gate e-tank
[ 0xA0A4, KeycardDoors.Left, 0x062E, KeycardEvents.BrinstarLevel2, KeycardPlaque.Level2, 0x042F, 0x0000 ], #// Brinstar - Pink Brinstar - Door to spore spawn super
[ 0xA56B, KeycardDoors.Left, 0x161E, KeycardEvents.BrinstarBoss, KeycardPlaque.Boss, 0x141F, 0x8A1A ], #// Brinstar - Before Kraid - Door to Kraid
#// Upper Norfair
[ 0xA7DE, KeycardDoors.Right, 0x3601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x3400, 0x8B00 ], #// Norfair - Business Centre - Door towards Ice
[ 0xA923, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel1, KeycardPlaque.Level1, 0x0400, 0x0000 ], #// Norfair - Pre-Crocomire - Door towards Ice
[ 0xA788, KeycardDoors.Left, 0x162E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x142F, 0x8AEA ], #// Norfair - Lava Missile Room - Door towards Bubble Mountain
[ 0xAF72, KeycardDoors.Left, 0x061E, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x041F, 0x0000 ], #// Norfair - After frog speedway - Door to Bubble Mountain
[ 0xAEDF, KeycardDoors.Down, 0x0206, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0204, 0x0000 ], #// Norfair - Below bubble mountain - Door to Bubble Mountain
[ 0xAD5E, KeycardDoors.Right, 0x0601, KeycardEvents.NorfairLevel2, KeycardPlaque.Level2, 0x0400, 0x0000 ], #// Norfair - LN Escape - Door to Bubble Mountain
[ 0xA923, KeycardDoors.Up, 0x2DC6, KeycardEvents.NorfairBoss, KeycardPlaque.Boss, 0x2EC4, 0x8B96 ], #// Norfair - Pre-Crocomire - Door to Crocomire
#// Lower Norfair
[ 0xB4AD, KeycardDoors.Left, 0x160E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x140F, 0x0000 ], #// Lower Norfair - WRITG - Door to Amphitheatre
[ 0xAD5E, KeycardDoors.Left, 0x065E, KeycardEvents.LowerNorfairLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Lower Norfair - Exit - Door to "Reverse LN Entry"
[ 0xB37A, KeycardDoors.Right, 0x0601, KeycardEvents.LowerNorfairBoss, KeycardPlaque.Boss, 0x0400, 0x8EA6 ], #// Lower Norfair - Pre-Ridley - Door to Ridley
#// Maridia
[ 0xD0B9, KeycardDoors.Left, 0x065E, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x045F, 0x0000 ], #// Maridia - Mt. Everest - Door to Pink Maridia
[ 0xD5A7, KeycardDoors.Right, 0x1601, KeycardEvents.MaridiaLevel1, KeycardPlaque.Level1, 0x1400, 0x0000 ], #// Maridia - Aqueduct - Door towards Beach
[ 0xD617, KeycardDoors.Left, 0x063E, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x043F, 0x0000 ], #// Maridia - Pre-Botwoon - Door to Botwoon
[ 0xD913, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaLevel2, KeycardPlaque.Level2, 0x2400, 0x0000 ], #// Maridia - Pre-Colloseum - Door to post-botwoon
[ 0xD78F, KeycardDoors.Right, 0x2601, KeycardEvents.MaridiaBoss, KeycardPlaque.Boss, 0x2400, 0xC73B ], #// Maridia - Precious Room - Door to Draygon
[ 0xDA2B, KeycardDoors.BossLeft, 0x164E, 0x00f0, KeycardPlaque.Null, 0x144F, 0x0000 ], #// Maridia - Change Cac Alley Door to Boss Door (prevents key breaking)
#// Wrecked Ship
[ 0x93FE, KeycardDoors.Left, 0x167E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x147F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Reserve Tank Check
[ 0x968F, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Outside Wrecked Ship West - Door to Bowling Alley
[ 0xCE40, KeycardDoors.Left, 0x060E, KeycardEvents.WreckedShipLevel1, KeycardPlaque.Level1, 0x040F, 0x0000 ], #// Wrecked Ship - Gravity Suit - Door to Bowling Alley
[ 0xCC6F, KeycardDoors.Left, 0x064E, KeycardEvents.WreckedShipBoss, KeycardPlaque.Boss, 0x044F, 0xC29D ], #// Wrecked Ship - Pre-Phantoon - Door to Phantoon
]
doorId = 0x0000
for door in doorList:
#/* When "Fast Ganon" is set, don't place the G4 Boss key door to enable faster games */
if (door[0] == 0x99BD and self.myWorld.Config.Goal == Goal.FastGanonDefeatMotherBrain):
continue
doorArgs = doorId | door[3] if door[4] != KeycardPlaque.Null else door[3]
if (door[6] == 0):
#// Write dynamic door
doorData = []
for x in door[0:3]:
doorData += getWordArray(x)
doorData += getWordArray(doorArgs)
self.patches.append((Snes(0x8f0000 + plmTablePos), doorData))
plmTablePos += 0x08
else:
#// Overwrite existing door
doorData = []
for x in door[1:3]:
doorData += getWordArray(x)
doorData += getWordArray(doorArgs)
self.patches.append((Snes(0x8f0000 + door[6]), doorData))
if((door[3] == KeycardEvents.BrinstarBoss and door[0] != 0x9D9C) or door[3] == KeycardEvents.LowerNorfairBoss or door[3] == KeycardEvents.MaridiaBoss or door[3] == KeycardEvents.WreckedShipBoss):
#// Overwrite the extra parts of the Gadora with a PLM that just deletes itself
self.patches.append((Snes(0x8f0000 + door[6] + 0x06), [ 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 ]))
#// Plaque data
if (door[4] != KeycardPlaque.Null):
plaqueData = getWordArray(door[0]) + getWordArray(plaquePlm) + getWordArray(door[5]) + getWordArray(door[4])
self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData))
plmTablePos += 0x08
doorId += 1
#/* Write plaque showing SM bosses that needs to be killed */
if (self.myWorld.Config.OpenTourian != OpenTourian.FourBosses):
plaqueData = getWordArray(0xA5ED) + getWordArray(plaquePlm) + getWordArray(0x044F) + getWordArray(KeycardPlaque.Zero + self.myWorld.TourianBossTokens)
self.patches.append((Snes(0x8f0000 + plmTablePos), plaqueData))
plmTablePos += 0x08
self.patches.append((Snes(0x8f0000 + plmTablePos), [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ])) self.patches.append((Snes(0x8f0000 + plmTablePos), [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]))
@@ -745,20 +733,32 @@ class Patch:
(Snes(0xDD313), [ 0x00, 0x00, 0xE4, 0xFF, 0x08, 0x0E ]), (Snes(0xDD313), [ 0x00, 0x00, 0xE4, 0xFF, 0x08, 0x0E ]),
] ]
def WriteGanonInvicible(self, invincible: GanonInvincible): def WritePreOpenPyramid(self, goal: Goal):
if (goal == Goal.FastGanonDefeatMotherBrain):
self.patches.append((Snes(0x30808B), [0x01]))
def WriteGanonInvicible(self, goal: Goal):
#/* Defaults to $00 (never) at [asm]/z3/randomizer/tables.asm */ #/* Defaults to $00 (never) at [asm]/z3/randomizer/tables.asm */
invincibleMap = { valueMap = {
GanonInvincible.Never : 0x00, Goal.DefeatBoth : 0x03,
GanonInvincible.Always : 0x01, Goal.FastGanonDefeatMotherBrain : 0x04,
GanonInvincible.BeforeAllDungeons : 0x02, Goal.AllDungeonsDefeatMotherBrain : 0x02
GanonInvincible.BeforeCrystals : 0x03, }
} value = valueMap.get(goal, None)
value = invincibleMap.get(invincible, None)
if (value is None): if (value is None):
raise exception(f"Unknown Ganon invincible value {invincible}") raise exception(f"Unknown Ganon invincible value {goal}")
else: else:
self.patches.append((Snes(0x30803E), [value])) self.patches.append((Snes(0x30803E), [value]))
def WriteBossesNeeded(self, tourianBossTokens):
self.patches.append((Snes(0xF47200), getWordArray(tourianBossTokens)))
def WriteCrystalsNeeded(self, towerCrystals, ganonCrystals):
self.patches.append((Snes(0x30805E), [towerCrystals]))
self.patches.append((Snes(0x30805F), [ganonCrystals]))
self.stringTable.SetTowerRequirementText(f"You need {towerCrystals} crystals to enter Ganon's Tower.")
self.stringTable.SetGanonRequirementText(f"You need {ganonCrystals} crystals to defeat Ganon.")
def WriteRngBlock(self): def WriteRngBlock(self):
#/* Repoint RNG Block */ #/* Repoint RNG Block */

View File

@@ -5,12 +5,19 @@ from worlds.smz3.TotalSMZ3.Item import Item, ItemType
class RewardType(Enum): class RewardType(Enum):
Null = 0 Null = 0
Agahnim = 1 Agahnim = 1 << 0
PendantGreen = 2 PendantGreen = 1 << 1
PendantNonGreen = 3 PendantNonGreen = 1 << 2
CrystalBlue = 4 CrystalBlue = 1 << 3
CrystalRed = 5 CrystalRed = 1 << 4
GoldenFourBoss = 6 BossTokenKraid = 1 << 5
BossTokenPhantoon = 1 << 6
BossTokenDraygon = 1 << 7
BossTokenRidley = 1 << 8
AnyPendant = PendantGreen | PendantNonGreen
AnyCrystal = CrystalBlue | CrystalRed
AnyBossToken = BossTokenKraid | BossTokenPhantoon | BossTokenDraygon | BossTokenRidley
class IReward: class IReward:
Reward: RewardType Reward: RewardType
@@ -18,7 +25,7 @@ class IReward:
pass pass
class IMedallionAccess: class IMedallionAccess:
Medallion: object Medallion = None
class Region: class Region:
import worlds.smz3.TotalSMZ3.Location as Location import worlds.smz3.TotalSMZ3.Location as Location

View File

@@ -7,7 +7,7 @@ class Kraid(SMRegion, IReward):
Name = "Brinstar Kraid" Name = "Brinstar Kraid"
Area = "Brinstar" Area = "Brinstar"
Reward = RewardType.GoldenFourBoss Reward = RewardType.Null
def __init__(self, world, config: Config): def __init__(self, world, config: Config):
super().__init__(world, config) super().__init__(world, config)

View File

@@ -40,5 +40,5 @@ class Pink(SMRegion):
else: else:
return items.CanOpenRedDoors() and (items.CanDestroyBombWalls() or items.SpeedBooster) or \ return items.CanOpenRedDoors() and (items.CanDestroyBombWalls() or items.SpeedBooster) or \
items.CanUsePowerBombs() or \ items.CanUsePowerBombs() or \
items.CanAccessNorfairUpperPortal() and items.Morph and (items.CanOpenRedDoors() or items.Wave) and \ items.CanAccessNorfairUpperPortal() and items.Morph and (items.Missile or items.Super or items.Wave ) and \
(items.Ice or items.HiJump or items.CanSpringBallJump() or items.CanFly()) (items.Ice or items.HiJump or items.CanSpringBallJump() or items.CanFly())

Some files were not shown because too many files have changed in this diff Show More