Merge branch 'main' of https://github.com/Ijwu/Archipelago into main

This commit is contained in:
Hussein Farran 2021-08-30 21:43:18 -04:00
commit e960d7b58c
42 changed files with 1177 additions and 655 deletions

View File

@ -6,7 +6,7 @@ import logging
import json
import functools
from collections import OrderedDict, Counter, deque
from typing import List, Dict, Optional, Set, Iterable, Union, Any
from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
import secrets
import random
@ -38,7 +38,7 @@ class MultiWorld():
self.players = players
self.glitch_triforce = False
self.algorithm = 'balanced'
self.dungeons = []
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
self.regions = []
self.shops = []
self.itempool = []
@ -95,10 +95,6 @@ class MultiWorld():
set_player_attr('can_access_trock_big_chest', None)
set_player_attr('can_access_trock_middle', None)
set_player_attr('fix_fake_world', True)
set_player_attr('mapshuffle', False)
set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', False)
set_player_attr('bigkeyshuffle', False)
set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_shuffle', False)
@ -118,7 +114,6 @@ class MultiWorld():
set_player_attr('blue_clock_time', 2)
set_player_attr('green_clock_time', 4)
set_player_attr('can_take_damage', True)
set_player_attr('glitch_boots', True)
set_player_attr('progression_balancing', True)
set_player_attr('local_items', set())
set_player_attr('non_local_items', set())
@ -158,6 +153,10 @@ class MultiWorld():
def get_game_players(self, game_name: str):
return tuple(player for player in self.player_ids if self.game[player] == game_name)
@functools.lru_cache()
def get_game_worlds(self, game_name: str):
return tuple(world for player, world in self.worlds.items() if self.game[player] == game_name)
def get_name_string_for_object(self, obj) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
@ -209,10 +208,10 @@ class MultiWorld():
return self._location_cache[location, player]
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
for dungeon in self.dungeons:
if dungeon.name == dungeonname and dungeon.player == player:
return dungeon
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player))
try:
return self.dungeons[dungeonname, player]
except KeyError as e:
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
def get_all_state(self, keys=False) -> CollectionState:
key = f"_all_state_{keys}"
@ -537,9 +536,7 @@ class CollectionState(object):
locations = {location for location in locations if location.event}
while new_locations:
reachable_events = {location for location in locations if
(not key_only or
(not self.world.keyshuffle[location.item.player] and location.item.smallkey)
or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
(not key_only or getattr(location.item, "locked_dungeon_item", False))
and location.can_reach(self)}
new_locations = reachable_events - self.events
for event in new_locations:
@ -569,13 +566,6 @@ class CollectionState(object):
found += self.prog_items[item_name, player]
return found
def has_key(self, item, player, count: int = 1):
if self.world.logic[player] == 'nologic':
return True
if self.world.keyshuffle[player] == "universal":
return self.can_buy_unlimited('Small Key (Universal)', player)
return self.prog_items[item, player] >= count
def can_buy_unlimited(self, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
shop in self.world.shops)
@ -807,13 +797,6 @@ class Region(object):
return True
return False
def can_fill(self, item: Item):
inside_dungeon_item = item.locked_dungeon_item
if inside_dungeon_item:
return self.dungeon.is_dungeon_item(item) and item.player == self.player
return True
def __repr__(self):
return self.__str__()
@ -855,8 +838,8 @@ class Entrance(object):
world = self.parent_region.world if self.parent_region else None
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
class Dungeon(object):
class Dungeon(object):
def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int):
self.name = name
self.regions = regions
@ -911,7 +894,8 @@ class Boss():
return f"Boss({self.name})"
class Location():
shop_slot: bool = False
# If given as integer, then this is the shop's inventory index
shop_slot: Optional[int] = None
shop_slot_disabled: bool = False
event: bool = False
locked: bool = False
@ -930,7 +914,7 @@ class Location():
self.item: Optional[Item] = None
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and 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)))
def can_reach(self, state: CollectionState) -> bool:
# self.access_rule computes faster on average, so placing it first for faster abort
@ -966,21 +950,33 @@ class Location():
@property
def hint_text(self):
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " "))
hint_text = getattr(self, "_hint_text", None)
if hint_text:
return hint_text
return "at " + self.name.replace("_", " ").replace("-", " ")
class Item():
location: Optional[Location] = None
world: Optional[MultiWorld] = None
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
game: str = "Generic"
type: str = None
never_exclude = False # change manually to ensure that a specific nonprogression item never goes on an excluded location
# change manually to ensure that a specific non-progression item never goes on an excluded location
never_exclude = False
# need to find a decent place for these to live and to allow other games to register texts if they want.
pedestal_credit_text: str = "and the Unknown Item"
sickkid_credit_text: Optional[str] = None
magicshop_credit_text: Optional[str] = None
zora_credit_text: Optional[str] = None
fluteboy_credit_text: Optional[str] = None
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
# 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, advancement: bool, code: Optional[int], player: int):
self.name = name
@ -1007,51 +1003,6 @@ class Item():
def __hash__(self):
return hash((self.name, self.player))
@property
def crystal(self) -> bool:
return self.type == 'Crystal'
@property
def smallkey(self) -> bool:
return self.type == 'SmallKey'
@property
def bigkey(self) -> bool:
return self.type == 'BigKey'
@property
def map(self) -> bool:
return self.type == 'Map'
@property
def compass(self) -> bool:
return self.type == 'Compass'
@property
def dungeon_item(self) -> Optional[str]:
if self.game == "A Link to the Past" and self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
return self.type
@property
def shuffled_dungeon_item(self) -> bool:
dungeon_item_type = self.dungeon_item
if dungeon_item_type:
return {"SmallKey" : self.world.keyshuffle,
"BigKey": self.world.bigkeyshuffle,
"Map": self.world.mapshuffle,
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
return False
@property
def locked_dungeon_item(self) -> bool:
dungeon_item_type = self.dungeon_item
if dungeon_item_type:
return not {"SmallKey" : self.world.keyshuffle,
"BigKey": self.world.bigkeyshuffle,
"Map": self.world.mapshuffle,
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
return False
def __repr__(self):
return self.__str__()
@ -1105,7 +1056,7 @@ class Spoiler():
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
listed_locations.update(cave_locations)
for dungeon in self.world.dungeons:
for dungeon in self.world.dungeons.values():
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon]
self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
listed_locations.update(dungeon_locations)
@ -1179,10 +1130,6 @@ class Spoiler():
'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility,
'hints': self.world.hints,
'mapshuffle': self.world.mapshuffle,
'compassshuffle': self.world.compassshuffle,
'keyshuffle': self.world.keyshuffle,
'bigkeyshuffle': self.world.bigkeyshuffle,
'boss_shuffle': self.world.boss_shuffle,
'enemy_shuffle': self.world.enemy_shuffle,
'enemy_health': self.world.enemy_health,
@ -1277,15 +1224,6 @@ class Spoiler():
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Map shuffle: %s\n' %
('Yes' if self.metadata['mapshuffle'][player] else 'No'))
outfile.write('Compass shuffle: %s\n' %
('Yes' if self.metadata['compassshuffle'][player] else 'No'))
outfile.write(
'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player])))
outfile.write('Big Key shuffle: %s\n' % (
'Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
outfile.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop price shuffle: %s\n' %

View File

@ -19,15 +19,17 @@ from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePar
from worlds.factorio import Factorio
os.makedirs("logs", exist_ok=True)
log_folder = Utils.local_path("logs")
os.makedirs(log_folder, exist_ok=True)
if gui_enabled:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w", force=True)
filename=os.path.join(log_folder, "FactorioClient.txt"), filemode="w", force=True)
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "FactorioClient.txt"), "w"))
class FactorioCommandProcessor(ClientCommandProcessor):
@ -110,7 +112,7 @@ class FactorioContext(CommonContext):
if cmd == "Connected":
# catch up sync anything that is already cleared.
if args["checked_locations"]:
self.rcon_client.send_commands({item_name: f'/ap-get-technology {item_name}\t-1' for
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
item_name in args["checked_locations"]})

13
Fill.py
View File

@ -81,6 +81,7 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
progitempool = []
nonexcludeditempool = []
localrestitempool = {player: [] for player in range(1, world.players + 1)}
nonlocalrestitempool = []
restitempool = []
for item in world.itempool:
@ -90,11 +91,13 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
nonexcludeditempool.append(item)
elif item.name in world.local_items[item.player]:
localrestitempool[item.player].append(item)
elif item.name in world.non_local_items[item.player]:
nonlocalrestitempool.append(item)
else:
restitempool.append(item)
world.random.shuffle(fill_locations)
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations)
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
fill_restrictive(world, world.state, fill_locations, progitempool)
@ -120,6 +123,14 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
for item_to_place in nonlocalrestitempool:
for i, location in enumerate(fill_locations):
if location.player != item_to_place.player:
world.push_item(fill_locations.pop(i), item_to_place, False)
break
else:
logging.warning(f"Could not place non_local_item {item_to_place} among {fill_locations}, tossing.")
world.random.shuffle(fill_locations)
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)

View File

@ -120,7 +120,6 @@ def main(args=None, callback=ERmain):
f"A mix is also permitted.")
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
erargs.create_spoiler = args.spoiler > 0
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.race = args.race
@ -189,6 +188,9 @@ def main(args=None, callback=ERmain):
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
if len(set(erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {erargs.name}")
if args.yaml_output:
import yaml
important = {}
@ -237,7 +239,7 @@ def convert_to_on_off(value):
return {True: "on", False: "off"}.get(value, value)
def get_choice(option, root, value=None) -> typing.Any:
def get_choice_legacy(option, root, value=None) -> typing.Any:
if option not in root:
return value
if type(root[option]) is list:
@ -252,6 +254,20 @@ def get_choice(option, root, value=None) -> typing.Any:
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
def get_choice(option, root, value=None) -> typing.Any:
if option not in root:
return value
if type(root[option]) is list:
return random.choices(root[option])[0]
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return value
if any(root[option].values()):
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
@ -465,7 +481,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
inventoryweights = game_weights.get('start_inventory', {})
startitems = []
for item in inventoryweights.keys():
itemvalue = get_choice(item, inventoryweights)
itemvalue = get_choice_legacy(item, inventoryweights)
if isinstance(itemvalue, int):
for i in range(int(itemvalue)):
startitems.append(item)
@ -513,7 +529,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
glitches_required = get_choice('glitches_required', weights)
glitches_required = get_choice_legacy('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none'
@ -521,7 +537,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
glitches_required]
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces":
@ -529,94 +545,78 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False)
ret.restrict_dungeon_item_on_boss = get_choice_legacy('restrict_dungeon_item_on_boss', weights, False)
dungeon_items = get_choice('dungeon_items', weights)
if dungeon_items == 'full' or dungeon_items == True:
dungeon_items = 'mcsb'
elif dungeon_items == 'standard':
dungeon_items = ""
elif not dungeon_items:
dungeon_items = ""
if "u" in dungeon_items:
dungeon_items.replace("s", "")
ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items)
ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items)
ret.keyshuffle = get_choice('smallkey_shuffle', weights,
'universal' if 'u' in dungeon_items else 's' in dungeon_items)
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items)
entrance_shuffle = get_choice('entrance_shuffle', weights, 'vanilla')
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla'
else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice('goals', weights, 'ganon')
goal = get_choice_legacy('goals', weights, 'ganon')
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
# sum a percentage to required
if extra_pieces == 'percentage':
percentage = max(100, float(get_choice('triforce_pieces_percentage', weights, 150))) / 100
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice('triforce_pieces_available', weights, 30))
get_choice_legacy('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
if not ret.shop_shuffle:
ret.shop_shuffle = ''
ret.mode = get_choice("mode", weights)
ret.retro = get_choice("retro", weights)
ret.mode = get_choice_legacy("mode", weights)
ret.retro = get_choice_legacy("retro", weights)
ret.hints = get_choice('hints', weights)
ret.hints = get_choice_legacy('hints', weights)
ret.swordless = get_choice('swordless', weights, False)
ret.swordless = get_choice_legacy('swordless', weights, False)
ret.difficulty = get_choice('item_pool', weights)
ret.difficulty = get_choice_legacy('item_pool', weights)
ret.item_functionality = get_choice('item_functionality', weights)
ret.item_functionality = get_choice_legacy('item_functionality', weights)
boss_shuffle = get_choice('boss_shuffle', weights)
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
ret.enemy_shuffle = bool(get_choice_legacy('enemy_shuffle', weights, False))
ret.killable_thieves = get_choice('killable_thieves', weights, False)
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
ret.killable_thieves = get_choice_legacy('killable_thieves', weights, False)
ret.tile_shuffle = get_choice_legacy('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice_legacy('bush_shuffle', weights, False)
ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos', # to be removed
'chaos': 'chaos',
}[get_choice('enemy_damage', weights)]
}[get_choice_legacy('enemy_damage', weights)]
ret.enemy_health = get_choice('enemy_health', weights)
ret.enemy_health = get_choice_legacy('enemy_health', weights)
ret.shufflepots = get_choice('pot_shuffle', weights)
ret.shufflepots = get_choice_legacy('pot_shuffle', weights)
ret.beemizer = int(get_choice('beemizer', weights, 0))
ret.beemizer = int(get_choice_legacy('beemizer', weights, 0))
ret.timer = {'none': False,
None: False,
@ -625,19 +625,19 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice('timer', weights, False)]
'display': 'display'}[get_choice_legacy('timer', weights, False)]
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4))
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"),
get_choice("turtle_rock_medallion", weights, "random")]
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
get_choice_legacy("turtle_rock_medallion", weights, "random")]
for index, medallion in enumerate(ret.required_medallions):
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
@ -645,13 +645,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.glitch_boots = get_choice('glitch_boots', weights, True)
if get_choice("local_keys", weights, "l" in dungeon_items):
# () important for ordering of commands, without them the Big Keys section is part of the Small Key else
ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set()
ret.local_items |= item_name_groups["Big Keys"] if ret.bigkeyshuffle else set()
ret.plando_items = []
if "items" in plando_options:
@ -665,10 +658,10 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
options = weights.get("plando_items", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
from_pool = get_choice("from_pool", placement, PlandoItem._field_defaults["from_pool"])
location_world = get_choice("world", placement, PlandoItem._field_defaults["world"])
force = str(get_choice("force", placement, PlandoItem._field_defaults["force"])).lower()
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
from_pool = get_choice_legacy("from_pool", placement, PlandoItem._field_defaults["from_pool"])
location_world = get_choice_legacy("world", placement, PlandoItem._field_defaults["world"])
force = str(get_choice_legacy("force", placement, PlandoItem._field_defaults["force"])).lower()
if "items" in placement and "locations" in placement:
items = placement["items"]
locations = placement["locations"]
@ -684,8 +677,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
for item, location in zip(items, locations):
add_plando_item(item, location)
else:
item = get_choice("item", placement, get_choice("items", placement))
location = get_choice("location", placement)
item = get_choice_legacy("item", placement, get_choice_legacy("items", placement))
location = get_choice_legacy("location", placement)
add_plando_item(item, location)
ret.plando_texts = {}
@ -694,39 +687,39 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
at = str(get_choice("at", placement))
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
at = str(get_choice_legacy("at", placement))
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice("text", placement))
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = []
if "connections" in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
get_choice_legacy("entrance", placement),
get_choice_legacy("exit", placement),
get_choice_legacy("direction", placement, "both")
))
ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice('sprite', weights, "Link")
ret.sprite = get_choice_legacy('sprite', weights, "Link")
if 'random_sprite_on_event' in weights:
randomoneventweights = weights['random_sprite_on_event']
if get_choice('enabled', randomoneventweights, False):
if get_choice_legacy('enabled', randomoneventweights, False):
ret.sprite = 'randomon'
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
ret.sprite += '-hit' if get_choice_legacy('on_hit', randomoneventweights, True) else ''
ret.sprite += '-enter' if get_choice_legacy('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice_legacy('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice_legacy('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice_legacy('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice_legacy('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice_legacy('on_everything', randomoneventweights, False) else ret.sprite
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \
if (not ret.sprite_pool or get_choice_legacy('use_weighted_sprite_pool', randomoneventweights, False)) \
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
for key, value in weights['sprite'].items():
if key.startswith('random'):

View File

@ -8,6 +8,8 @@ import os
import subprocess
import base64
import shutil
import logging
import asyncio
from json import loads, dumps
from Utils import get_item_name_from_id
@ -30,15 +32,16 @@ snes_logger = logging.getLogger("SNES")
from MultiServer import mark_raw
os.makedirs("logs", exist_ok=True)
log_folder = Utils.local_path("logs")
os.makedirs(log_folder, exist_ok=True)
# Log to file in gui case
if gui_enabled:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join("logs", "LttPClient.txt"), filemode="w", force=True)
filename=os.path.join(log_folder, "LttPClient.txt"), filemode="w", force=True)
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "LttPClient.txt"), "w"))
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "LttPClient.txt"), "w"))
class LttPCommandProcessor(ClientCommandProcessor):
@ -52,11 +55,26 @@ class LttPCommandProcessor(ClientCommandProcessor):
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
@mark_raw
def _cmd_snes(self, snes_address: str = "") -> bool:
"""Connect to a snes.
Optionally include network address of a snes to connect to, otherwise show available devices"""
def _cmd_snes(self, snes_options: str = "") -> bool:
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices; and a SNES device number if more than one SNES is detected"""
snes_address = self.ctx.snes_address
snes_device_number = -1
options = snes_options.split()
num_options = len(options)
if num_options > 0:
snes_address = options[0]
if num_options > 1:
try:
snes_device_number = int(options[1])
except:
pass
self.ctx.snes_reconnect_address = None
asyncio.create_task(snes_connect(self.ctx, snes_address if snes_address else self.ctx.snes_address))
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number))
return True
def _cmd_snes_close(self) -> bool:
@ -495,7 +513,7 @@ async def get_snes_devices(ctx: Context):
return devices
async def snes_connect(ctx: Context, address):
async def snes_connect(ctx: Context, address, deviceIndex = -1):
global SNES_RECONNECT_DELAY
if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
if ctx.rom:
@ -504,6 +522,7 @@ async def snes_connect(ctx: Context, address):
snes_logger.error('Already connected to SNI, likely awaiting a device.')
return
device = None
recv_task = None
ctx.snes_state = SNESState.SNES_CONNECTING
socket = await _snes_connect(ctx, address)
@ -512,15 +531,29 @@ async def snes_connect(ctx: Context, address):
try:
devices = await get_snes_devices(ctx)
numDevices = len(devices)
if len(devices) == 1:
if numDevices == 1:
device = devices[0]
elif ctx.snes_reconnect_address:
if ctx.snes_attached_device[1] in devices:
device = ctx.snes_attached_device[1]
else:
device = devices[ctx.snes_attached_device[0]]
else:
elif numDevices > 1:
if deviceIndex == -1:
snes_logger.info("Found " + str(numDevices) + " SNES devices; connect to one with /snes <address> <device number>:")
for idx, availableDevice in enumerate(devices):
snes_logger.info(str(idx + 1) + ": " + availableDevice)
elif (deviceIndex < 0) or (deviceIndex - 1) > numDevices:
snes_logger.warning("SNES device number out of range")
else:
device = devices[deviceIndex - 1]
if device is None:
await snes_disconnect(ctx)
return

136
Main.py
View File

@ -14,8 +14,7 @@ from BaseClasses import MultiWorld, CollectionState, Region, RegionType
from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules
from worlds import AutoWorld
@ -30,15 +29,6 @@ def get_seed(seed=None):
return seed
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
if seed_def in seeds:
return seeds[seed_def]
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
world.__named_seeds = seeds
return seeds[seed_def]
def main(args, seed=None):
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
@ -76,11 +66,6 @@ def main(args, seed=None):
world.retro = args.retro.copy()
world.hints = args.hints.copy()
world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_shuffle = args.enemy_shuffle.copy()
@ -97,7 +82,6 @@ def main(args, seed=None):
world.green_clock_time = args.green_clock_time.copy()
world.shufflepots = args.shufflepots.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.glitch_boots = args.glitch_boots.copy()
world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy()
@ -122,40 +106,23 @@ def main(args, seed=None):
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
range(1, world.players + 1)}
AutoWorld.call_all(world, "generate_early")
# system for sharing ER layouts
for player in world.get_game_players("A Link to the Past"):
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
if "-" in world.shuffle[player]:
shuffle, seed = world.shuffle[player].split("-", 1)
world.shuffle[player] = shuffle
if shuffle == "vanilla":
world.er_seeds[player] = "vanilla"
elif seed.startswith("group-") or args.race:
world.er_seeds[player] = get_same_seed(world, (
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
else: # not a race or group seed, use set seed as is.
world.er_seeds[player] = seed
elif world.shuffle[player] == "vanilla":
world.er_seeds[player] = "vanilla"
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
logger.info("Found World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
numlength = 8
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations")
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
f"{max(cls.item_id_to_name):{numlength}} | "
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
f"{max(cls.location_id_to_name):{numlength}}")
if not cls.hidden:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | "
f"{len(cls.location_names):3} Locations")
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
f"{max(cls.item_id_to_name):{numlength}} | "
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
f"{max(cls.location_id_to_name):{numlength}}")
AutoWorld.call_all(world, "generate_early")
logger.info('')
for player in world.get_game_players("A Link to the Past"):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids:
for item_name in args.startinventory[player]:
@ -167,21 +134,6 @@ def main(args, seed=None):
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece')
# dungeon items can't be in non-local if the appropriate dungeon item shuffle setting is not set.
if not world.mapshuffle[player]:
world.non_local_items[player] -= item_name_groups['Maps']
if not world.compassshuffle[player]:
world.non_local_items[player] -= item_name_groups['Compasses']
if not world.keyshuffle[player]:
world.non_local_items[player] -= item_name_groups['Small Keys']
# This could probably use a more elegant solution.
elif world.keyshuffle[player] == True and world.mode[player] == "Standard":
world.local_items[player].add("Small Key (Hyrule Castle)")
if not world.bigkeyshuffle[player]:
world.non_local_items[player] -= item_name_groups['Big Keys']
# Not possible to place pendants/crystals out side of boss prizes yet.
world.non_local_items[player] -= item_name_groups['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals']
@ -225,9 +177,7 @@ def main(args, seed=None):
elif world.algorithm == 'balanced':
distribute_items_restrictive(world)
logger.info("Filling Shop Slots")
ShopSlotFill(world)
AutoWorld.call_all(world, 'post_fill')
if world.players > 1:
balance_multiworld_progression(world)
@ -276,20 +226,21 @@ def main(args, seed=None):
for player in range(1, world.players + 1):
checks_in_area[player]["Total"] = 0
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
main_entrance = get_entrance_to_region(location.parent_region)
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
for location in world.get_filled_locations():
if type(location.address) is int:
main_entrance = get_entrance_to_region(location.parent_region)
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
oldmancaves = []
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
@ -338,6 +289,8 @@ def main(args, seed=None):
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations():
if type(location.address) == int:
# item code None should be event, location.address should then also be None
assert location.item.code is not None
locations_data[location.player][location.address] = location.item.code, location.item.player
if location.player in sending_visible_players and location.item.player != location.player:
hint = NetUtils.Hint(location.item.player, location.player, location.address,
@ -377,29 +330,34 @@ def main(args, seed=None):
f.write(bytes([1])) # version of format
f.write(multidata)
multidata_task = pool.submit(write_multidata)
if not check_accessibility_task.result():
if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
else:
logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occured.
if multidata_task:
multidata_task.result() # retrieve exception if one exists
multidata_task.result()
for future in output_file_futures:
future.result()
pool.shutdown() # wait for all queued tasks to complete
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
create_playthrough(world)
if args.create_spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
for future in output_file_futures:
future.result()
zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f'Creating final archive at {zipfilename}.')
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
for file in os.scandir(temp_dir):
zf.write(os.path.join(temp_dir, file), arcname=file.name)
zf.write(file.path, arcname=file.name)
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return world
@ -415,7 +373,6 @@ def create_playthrough(world):
sphere_candidates = set(prog_locations)
logging.debug('Building up collection spheres.')
while sphere_candidates:
state.sweep_for_events(key_only=True)
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
@ -520,14 +477,15 @@ def create_playthrough(world):
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player})
if player in world.get_game_players("A Link to the Past"):
for path in dict(world.spoiler.paths).values():
if any(exit_path == 'Pyramid Fairy' for (_, exit_path) in path):
if world.mode[player] != 'inverted':
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
get_path(state,world.get_region('Big Bomb Shop', player))
else:
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state,world.get_region('Inverted Big Bomb Shop', player))
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
# Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
if world.mode[player] != 'inverted':
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
get_path(state, world.get_region('Big Bomb Shop', player))
else:
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
# we can finally output our playthrough
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}

View File

@ -31,10 +31,11 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
import Utils
from Utils import get_item_name_from_id, get_location_name_from_id, \
version_tuple, restricted_loads, Version
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer
colorama.init()
class Client(Endpoint):
version = Version(0, 0, 0)
tags: typing.List[str] = []
@ -50,9 +51,14 @@ class Client(Endpoint):
self.messageprocessor = client_message_processor(ctx, self)
self.ctx = weakref.ref(ctx)
team_slot = typing.Tuple[int, int]
class Context(Node):
class Context:
dumper = staticmethod(encode)
loader = staticmethod(decode)
simple_options = {"hint_cost": int,
"location_check_points": int,
"server_password": str,
@ -64,8 +70,10 @@ class Context(Node):
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", remaining_mode: str = "disabled",
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2):
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, log_network: bool = False):
super(Context, self).__init__()
self.log_network = log_network
self.endpoints = []
self.compatibility: int = compatibility
self.shutdown_task = None
self.data_filename = None
@ -113,10 +121,70 @@ class Context(Node):
self.seed_name = ""
self.random = random.Random()
def get_hint_cost(self, slot):
if self.hint_cost:
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
return 0
# General networking
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
msg = self.dumper(msgs)
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception("Exception during send_encoded_msgs")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
for endpoint in self.endpoints:
if endpoint.auth:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
def broadcast_team(self, team, msgs):
msgs = self.dumper(msgs)
for client in self.endpoints:
if client.auth and client.team == team:
asyncio.create_task(self.send_encoded_msgs(client, msgs))
async def disconnect(self, endpoint):
if endpoint in self.endpoints:
self.endpoints.remove(endpoint)
await on_client_disconnected(self, endpoint)
# text
def notify_all(self, text):
logging.info("Notice (all): %s" % text)
self.broadcast_all([{"cmd": "Print", "text": text}])
def notify_client(self, client: Client, text: str):
if not client.auth:
return
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
if not client.auth:
return
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
# loading
def load(self, multidatapath: str, use_embedded_server_options: bool = False):
if multidatapath.lower().endswith(".zip"):
@ -177,27 +245,7 @@ class Context(Node):
server_options = decoded_obj.get("server_options", {})
self._set_options(server_options)
def get_players_package(self):
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
def _set_options(self, server_options: dict):
for key, value in server_options.items():
data_type = self.simple_options.get(key, None)
if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password
try:
value = data_type(value)
except Exception as e:
try:
raise Exception(f"Could not set server option {key}, skipping.") from e
except Exception as e:
logging.exception(e)
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
setattr(self, key, value)
elif key == "disable_item_cheat":
self.item_cheat = not bool(value)
else:
logging.debug(f"Unrecognized server option {key}")
# saving
def save(self, now=False) -> bool:
if self.saving:
@ -228,7 +276,7 @@ class Context(Node):
import os
name, ext = os.path.splitext(self.data_filename)
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
else self.data_filename + '_' + 'apsave'
else self.data_filename + '_' + 'apsave'
try:
with open(self.save_filename, 'rb') as f:
save_data = restricted_loads(zlib.decompress(f.read()))
@ -256,13 +304,6 @@ class Context(Node):
import atexit
atexit.register(self._save, True) # make sure we save on exit too
def recheck_hints(self):
for team, slot in self.hints:
self.hints[team, slot] = {
hint.re_check(self, team) for hint in
self.hints[team, slot]
}
def get_save(self) -> dict:
self.recheck_hints()
d = {
@ -303,43 +344,48 @@ class Context(Node):
logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items '
f'for {len(self.received_items)} players')
# rest
def get_hint_cost(self, slot):
if self.hint_cost:
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
return 0
def recheck_hints(self):
for team, slot in self.hints:
self.hints[team, slot] = {
hint.re_check(self, team) for hint in
self.hints[team, slot]
}
def get_players_package(self):
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
def _set_options(self, server_options: dict):
for key, value in server_options.items():
data_type = self.simple_options.get(key, None)
if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password
try:
value = data_type(value)
except Exception as e:
try:
raise Exception(f"Could not set server option {key}, skipping.") from e
except Exception as e:
logging.exception(e)
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
setattr(self, key, value)
elif key == "disable_item_cheat":
self.item_cheat = not bool(value)
else:
logging.debug(f"Unrecognized server option {key}")
def get_aliased_name(self, team: int, slot: int):
if (team, slot) in self.name_aliases:
return f"{self.name_aliases[team, slot]} ({self.player_names[team, slot]})"
else:
return self.player_names[team, slot]
def notify_all(self, text):
logging.info("Notice (all): %s" % text)
self.broadcast_all([{"cmd": "Print", "text": text}])
def notify_client(self, client: Client, text: str):
if not client.auth:
return
logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str]):
if not client.auth:
return
asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts]))
def broadcast_team(self, team, msgs):
msgs = self.dumper(msgs)
for client in self.endpoints:
if client.auth and client.team == team:
asyncio.create_task(self.send_encoded_msgs(client, msgs))
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
for endpoint in self.endpoints:
if endpoint.auth:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
async def disconnect(self, endpoint):
await super(Context, self).disconnect(endpoint)
await on_client_disconnected(self, endpoint)
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
concerns = collections.defaultdict(list)
@ -1431,8 +1477,7 @@ async def main(args: argparse.Namespace):
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
args.auto_shutdown, args.compatibility)
ctx.log_network = args.log_network
args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata
try:

View File

@ -1,6 +1,4 @@
from __future__ import annotations
import asyncio
import logging
import typing
import enum
from json import JSONEncoder, JSONDecoder
@ -94,52 +92,6 @@ def _object_hook(o: typing.Any) -> typing.Any:
decode = JSONDecoder(object_hook=_object_hook).decode
class Node:
endpoints: typing.List
dumper = staticmethod(encode)
loader = staticmethod(decode)
def __init__(self):
self.endpoints = []
super(Node, self).__init__()
self.log_network = 0
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
for endpoint in self.endpoints:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
msg = self.dumper(msgs)
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception("Exception during send_encoded_msgs")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
async def disconnect(self, endpoint):
if endpoint in self.endpoints:
self.endpoints.remove(endpoint)
class Endpoint:
socket: websockets.WebSocketServerProtocol

View File

@ -9,7 +9,7 @@ class AssembleOptions(type):
name_lookup = attrs["name_lookup"] = {}
# merge parent class options
for base in bases:
if hasattr(base, "options"):
if getattr(base, "options", None):
options.update(base.options)
name_lookup.update(base.name_lookup)
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
@ -147,6 +147,29 @@ class Choice(Option):
return cls(data)
return cls.from_text(str(data))
def __eq__(self, other):
if isinstance(other, str):
assert other in self.options
return other == self.current_key
elif isinstance(other, int):
assert other in self.name_lookup
return other == self.value
elif isinstance(other, bool):
return other == bool(self.value)
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
def __ne__(self, other):
if isinstance(other, str):
assert other in self.options
return other != self.current_key
elif isinstance(other, int):
assert other in self.name_lookup
return other != self.value
elif isinstance(other, bool):
return other != bool(self.value)
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class Range(Option, int):
range_start = 0
@ -233,14 +256,14 @@ if __name__ == "__main__":
from worlds.alttp.Options import Logic
import argparse
mapshuffle = Toggle
compassshuffle = Toggle
map_shuffle = Toggle
compass_shuffle = Toggle
keyshuffle = Toggle
bigkeyshuffle = Toggle
bigkey_shuffle = Toggle
hints = Toggle
test = argparse.Namespace()
test.logic = Logic.from_text("no_logic")
test.mapshuffle = mapshuffle.from_text("ON")
test.map_shuffle = map_shuffle.from_text("ON")
test.hints = hints.from_text('OFF')
try:
test.logic = Logic.from_text("overworld_glitches_typo")
@ -250,7 +273,7 @@ if __name__ == "__main__":
test.logic_owg = Logic.from_text("owg")
except KeyError as e:
print(e)
if test.mapshuffle:
print("Mapshuffle is on")
if test.map_shuffle:
print("map_shuffle is on")
print(f"Hints are {bool(test.hints)}")
print(test)

View File

@ -7,8 +7,9 @@ Currently, the following games are supported:
* Factorio
* Minecraft
* Subnautica
* Slay the Spire
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
windows binaries.

View File

@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.1.6"
__version__ = "0.1.7"
version_tuple = tuplize_version(__version__)
import builtins

View File

@ -93,10 +93,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
"sid": generation.id,
"owner": generation.owner},
handle_generation_success, handle_generation_failure)
except:
except Exception as e:
generation.state = STATE_ERROR
commit()
raise
logging.exception(e)
else:
generation.state = STATE_STARTED

View File

@ -52,8 +52,8 @@ progression_balancing:
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
# - "Master Sword Pedestal"
{{ game }}:
{%- for option_name, option in options.items() %}
{{ option_name }}:{% if option.__doc__ %} # {{ option.__doc__ }}{% endif %}
{%- for option_key, option in options.items() %}
{{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %}
{%- if option.range_start is defined %}
# you can add additional values between minimum and maximum
{{ option.range_start }}: 0 # minimum value
@ -62,7 +62,7 @@ progression_balancing:
random-low: 0
random-high: 0
{%- elif option.options -%}
{%- for sub_option_name, suboption_option_id in option.options.items() %}
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
{%- endfor -%}
{%- else %}

View File

@ -28,6 +28,7 @@ game: # Pick a game to play
Factorio: 0
Minecraft: 0
Subnautica: 0
Slay the Spire: 0
requires:
version: 0.1.6 # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games:
@ -56,7 +57,23 @@ progression_balancing:
# - "Master Sword Pedestal"
Subnautica: {}
Slay the Spire:
character: # Pick What Character you wish to play with.
ironclad: 50
silent: 50
defect: 50
watcher: 50
ascension: # What Ascension do you wish to play with.
# you can add additional values between minimum and maximum
0: 50 # minimum value
20: 0 # maximum value
random: 0
random-low: 0
random-high: 0
heart_run: # Whether or not you will need to collect they 3 keys to unlock the final act
# and beat the heart to finish the game.
false: 50
true: 0
Factorio:
tech_tree_layout:
single: 1
@ -279,29 +296,31 @@ A Link to the Past:
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
off: 50
### End of Logic Section ###
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
on: 0
off: 50
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
on: 0
off: 50
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
on: 0
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
off: 50
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
on: 0
off: 50
local_keys: # Keep small keys and big keys local to your world
on: 0
off: 50
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
mc: 0 # Shuffle maps and compasses
none: 50 # Shuffle none of the 4
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
ub: 0 # universal small keys and shuffled big keys
# you can add more combos of these letters here
bigkey_shuffle: # Big Key Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
smallkey_shuffle: # Small Key Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
universal: 0
compass_shuffle: # Compass Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
map_shuffle: # Map Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon
pickup: 50 # Show when compass is picked up

View File

@ -4,11 +4,10 @@ from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import generate_itempool, difficulties
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from worlds import AutoWorld

View File

@ -1,4 +1,5 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons
@ -7,6 +8,7 @@ from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances
from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Rules import set_inverted_big_bomb_rules
from worlds import AutoWorld
class TestInvertedBombRules(unittest.TestCase):
@ -14,6 +16,10 @@ class TestInvertedBombRules(unittest.TestCase):
def setUp(self):
self.world = MultiWorld(1)
self.world.mode[1] = "inverted"
args = Namespace
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.world, 1)
create_dungeons(self.world, 1)

View File

@ -91,6 +91,9 @@ class World(metaclass=AutoWorldRegister):
# the client finds its own items in its own world.
remote_items: bool = True
# Hide World Type from various views. Does not remove functionality.
hidden = False
# autoset on creation:
world: MultiWorld
player: int
@ -130,11 +133,15 @@ class World(metaclass=AutoWorldRegister):
pass
def fill_hook(cls, progitempool: List[Item], nonexcludeditempool: List[Item],
localrestitempool: Dict[int, List[Item]], restitempool: List[Item], fill_locations: List[Location]):
localrestitempool: Dict[int, List[Item]], nonlocalrestitempool: Dict[int, List[Item]],
restitempool: List[Item], fill_locations: List[Location]):
"""Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type."""
pass
def post_fill(self):
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation."""
def generate_output(self, output_directory: str):
"""This method gets called from a threadpool, do not use world.random here.
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""

View File

@ -3,12 +3,16 @@ from worlds.alttp.Bosses import BossFactory
from Fill import fill_restrictive
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import lookup_boss_drops
from worlds.alttp.Options import smallkey_shuffle
def create_dungeons(world, player):
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == "universal" else small_keys,
dungeon = Dungeon(name, dungeon_regions, big_key,
[] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys,
dungeon_items, player)
for item in dungeon.all_items:
item.dungeon = dungeon
dungeon.boss = BossFactory(default_boss, player) if default_boss else None
for region in dungeon.regions:
world.get_region(region, player).dungeon = dungeon
@ -21,56 +25,139 @@ def create_dungeons(world, player):
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'],
ItemFactory('Big Key (Eastern Palace)', player), [],
ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
DP = make_dungeon('Desert Palace', 'Lanmolas', ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)', 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
ToH = make_dungeon('Tower of Hera', 'Moldorm', ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'], ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King', ['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)', 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)', 'Palace of Darkness (North)', 'Palace of Darkness (Maze)', 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'], ItemFactory('Big Key (Palace of Darkness)', player), ItemFactory(['Small Key (Palace of Darkness)'] * 6, player), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)], ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)', player), ItemFactory(['Small Key (Skull Woods)'] * 3, player), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
SP = make_dungeon('Swamp Palace', 'Arrghus', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player), [ItemFactory('Small Key (Swamp Palace)', player)], ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
IP = make_dungeon('Ice Palace', 'Kholdstare', ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
TR = make_dungeon('Turtle Rock', 'Trinexx', ['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'], ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
DP = make_dungeon('Desert Palace', 'Lanmolas',
['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)',
'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player),
[ItemFactory('Small Key (Desert Palace)', player)],
ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
ToH = make_dungeon('Tower of Hera', 'Moldorm',
['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'],
ItemFactory('Big Key (Tower of Hera)', player),
[ItemFactory('Small Key (Tower of Hera)', player)],
ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King',
['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)',
'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)',
'Palace of Darkness (North)', 'Palace of Darkness (Maze)',
'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'],
ItemFactory('Big Key (Palace of Darkness)', player),
ItemFactory(['Small Key (Palace of Darkness)'] * 6, player),
ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'],
ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)],
ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section',
'Skull Woods Second Section', 'Skull Woods Second Section (Drop)',
'Skull Woods Final Section (Mothula)',
'Skull Woods First Section (Right)',
'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'],
ItemFactory('Big Key (Skull Woods)', player),
ItemFactory(['Small Key (Skull Woods)'] * 3, player),
ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
SP = make_dungeon('Swamp Palace', 'Arrghus',
['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)',
'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player),
[ItemFactory('Small Key (Swamp Palace)', player)],
ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
IP = make_dungeon('Ice Palace', 'Kholdstare',
['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)',
'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
ItemFactory(['Small Key (Ice Palace)'] * 2, player),
ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
MM = make_dungeon('Misery Mire', 'Vitreous',
['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)',
'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player),
ItemFactory(['Small Key (Misery Mire)'] * 3, player),
ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
TR = make_dungeon('Turtle Rock', 'Trinexx',
['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)',
'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)',
'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'],
ItemFactory('Big Key (Turtle Rock)', player),
ItemFactory(['Small Key (Turtle Rock)'] * 4, player),
ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
if world.mode[player] != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2', ['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2',
['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)',
'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)',
'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)',
'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'],
ItemFactory('Big Key (Ganons Tower)', player),
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
else:
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2', ['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None,
ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2',
['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)',
'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)',
'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)',
'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)',
'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player),
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
GT.bosses['bottom'] = BossFactory('Armos Knights', player)
GT.bosses['middle'] = BossFactory('Lanmolas', player)
GT.bosses['top'] = BossFactory('Moldorm', player)
world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]
for dungeon in [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]:
world.dungeons[dungeon.name, dungeon.player] = dungeon
def get_dungeon_item_pool(world):
items = [item for dungeon in world.dungeons for item in dungeon.all_items]
items = [item for dungeon in world.dungeons.values() for item in dungeon.all_items]
for item in items:
item.world = world
return items
def fill_dungeons_restrictive(world):
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
dungeon_items = [item for item in get_dungeon_item_pool(world) if
(((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player])
or (item.map and not world.mapshuffle[item.player])
or (item.compass and not world.compassshuffle[item.player])
) and world.goal[item.player] != 'icerodhunt')]
if dungeon_items:
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted}
locations = [location for location in world.get_unfilled_dungeon_locations()
if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss
def get_dungeon_item_pool_player(world, player):
items = [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
for item in items:
item.world = world
return items
world.random.shuffle(locations)
all_state_base = world.get_all_state()
# sort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2}
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
fill_restrictive(world, all_state_base, locations, dungeon_items, True, True)
def fill_dungeons_restrictive(autoworld, world):
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
localized: set = set()
dungeon_specific: set = set()
for subworld in world.get_game_worlds("A Link to the Past"):
player = subworld.player
localized |= {(player, item_name) for item_name in
subworld.dungeon_local_item_names}
dungeon_specific |= {(player, item_name) for item_name in
subworld.dungeon_specific_item_names}
if localized:
in_dungeon_items = [item for item in get_dungeon_item_pool(world) if (item.player, item.name) in localized]
if in_dungeon_items:
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if
restricted}
locations = [location for location in world.get_unfilled_dungeon_locations()
# filter boss
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
if dungeon_specific:
for location in locations:
dungeon = location.parent_region.dungeon
orig_rule = location.item_rule
location.item_rule = lambda item, dungeon=dungeon, orig_rule=orig_rule: \
(not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item)
world.random.shuffle(locations)
all_state_base = world.get_all_state()
# Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items
# subsort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2}
in_dungeon_items.sort(
key=lambda item: sort_order.get(item.type, 1) +
(5 if (item.player, item.name) in dungeon_specific else 0))
fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True)
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
@ -79,7 +166,8 @@ dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
'Palace of Darkness - Prize': [0x155B8],
'Swamp Palace - Prize': [0x155B7],
'Thieves\' Town - Prize': [0x155C6],
'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A, 0x1560B],
'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A,
0x1560B],
'Ice Palace - Prize': [0x155BF],
'Misery Mire - Prize': [0x155B9],
'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]}

View File

@ -206,26 +206,10 @@ def parse_arguments(argv, no_defaults=False):
time).
''', type=int)
parser.add_argument('--mapshuffle', default=defval(False),
help='Maps are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--compassshuffle', default=defval(False),
help='Compasses are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--keyshuffle', default=defval("off"), help='\
on: Small Keys are no longer restricted to their dungeons, but can be anywhere.\
universal: Makes all Small Keys usable in any dungeon and places shops to buy more keys.',
choices=["on", "universal", "off"])
parser.add_argument('--bigkeyshuffle', default=defval(False),
help='Big Keys are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(False), help='''\
Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1.
''', action='store_true')
parser.add_argument('--startinventory', default=defval(''),
help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--local_items', default=defval(''),
help='Specifies a list of items that will not spread across the multiworld (separated by commas)')
parser.add_argument('--non_local_items', default=defval(''),
@ -291,13 +275,10 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--restrict_dungeon_item_on_boss', default=defval(False), action="store_true")
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval(''))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--outputpath')
parser.add_argument('--game', default="A Link to the Past")
parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname')
parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\
turns off starting with Pegasus Boots in glitched modes.''')
parser.add_argument('--start_hints')
if multiargs.multi:
for player in range(1, multiargs.multi + 1):
@ -314,7 +295,6 @@ def parse_arguments(argv, no_defaults=False):
ret.plando_connections = []
ret.er_seeds = {}
ret.glitch_boots = not ret.disable_glitch_boots
if ret.timer == "none":
ret.timer = False
if ret.dungeon_counters == 'on':
@ -322,12 +302,6 @@ def parse_arguments(argv, no_defaults=False):
elif ret.dungeon_counters == 'off':
ret.dungeon_counters = False
if ret.keysanity:
ret.mapshuffle = ret.compassshuffle = ret.keyshuffle = ret.bigkeyshuffle = True
elif ret.keyshuffle == "on":
ret.keyshuffle = True
elif ret.keyshuffle == "off":
ret.keyshuffle = False
if multiargs.multi:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):
@ -336,7 +310,6 @@ def parse_arguments(argv, no_defaults=False):
for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'open_pyramid', 'timer',
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
'sprite',
@ -344,7 +317,7 @@ def parse_arguments(argv, no_defaults=False):
"triforce_pieces_required", "shop_shuffle",
"required_medallions", "start_hints",
"plando_items", "plando_texts", "plando_connections", "er_seeds",
'dungeon_counters', 'glitch_boots', 'killable_thieves',
'dungeon_counters', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
'restrict_dungeon_item_on_boss', 'game']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)

View File

@ -5,10 +5,11 @@ from BaseClasses import Region, RegionType
from worlds.alttp.SubClasses import ALttPLocation
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops
from worlds.alttp.Bosses import place_bosses
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.Dungeons import get_dungeon_item_pool_player
from worlds.alttp.EntranceShuffle import connect_entrance
from Fill import FillError, fill_restrictive
from Fill import FillError
from worlds.alttp.Items import ItemFactory, GetBeemizerItem
from worlds.alttp.Options import smallkey_shuffle
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@ -274,7 +275,7 @@ def generate_itempool(world):
itempool.extend(['Rupees (300)'] * 34)
itempool.extend(['Bombs (10)'] * 5)
itempool.extend(['Arrows (10)'] * 7)
if world.keyshuffle[player] == 'universal':
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
itempool.extend(itemdiff.universal_keys)
itempool.append('Small Key (Universal)')
@ -362,12 +363,8 @@ def generate_itempool(world):
if treasure_hunt_icon is not None:
world.treasure_hunt_icon[player] = treasure_hunt_icon
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.player == player
and ((item.smallkey and world.keyshuffle[player])
or (item.bigkey and world.bigkeyshuffle[player])
or (item.map and world.mapshuffle[player])
or (item.compass and world.compassshuffle[player])
or world.goal[player] == 'icerodhunt')]
dungeon_items = [item for item in get_dungeon_item_pool_player(world, player)
if item.name not in world.worlds[player].dungeon_local_item_names]
if world.goal[player] == 'icerodhunt':
for item in dungeon_items:
@ -500,14 +497,14 @@ def create_dynamic_shop_locations(world, player):
if item is None:
continue
if item['create_location']:
loc = ALttPLocation(player, "{} Slot {}".format(shop.region.name, i + 1), parent=shop.region)
loc = ALttPLocation(player, f"{shop.region.name} {shop.slot_names[i]}", parent=shop.region)
shop.region.locations.append(loc)
world.dynamic_locations.append(loc)
world.clear_location_cache()
world.push_item(loc, ItemFactory(item['item'], player), False)
loc.shop_slot = True
loc.shop_slot = i
loc.event = True
loc.locked = True
@ -637,7 +634,7 @@ def get_pool_core(world, player: int):
if retro:
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)'}
pool = ['Rupees (5)' if item in replace else item for item in pool]
if world.keyshuffle[player] == "universal":
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
pool.extend(diff.universal_keys)
item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing'
if mode == 'standard':
@ -774,7 +771,7 @@ def make_custom_item_pool(world, player):
itemtotal = itemtotal + 1
if mode == 'standard':
if world.keyshuffle[player] == "universal":
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
key_location = world.random.choice(
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
@ -797,7 +794,7 @@ def make_custom_item_pool(world, player):
pool.extend(['Magic Mirror'] * customitemarray[22])
pool.extend(['Moon Pearl'] * customitemarray[28])
if world.keyshuffle == "universal":
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))

View File

@ -28,6 +28,46 @@ class Goal(Choice):
option_hand_in = 2
class DungeonItem(Choice):
value: int
option_original_dungeon = 0
option_own_dungeons = 1
option_own_world = 2
option_any_world = 3
option_different_world = 4
alias_true = 3
alias_false = 0
@property
def in_dungeon(self):
return self.value in {0, 1}
class bigkey_shuffle(DungeonItem):
"""Big Key Placement"""
item_name_group = "Big Keys"
displayname = "Big Key Shuffle"
class smallkey_shuffle(DungeonItem):
"""Small Key Placement"""
option_universal = 5
item_name_group = "Small Keys"
displayname = "Small Key Shuffle"
class compass_shuffle(DungeonItem):
"""Compass Placement"""
item_name_group = "Compasses"
displayname = "Compass Shuffle"
class map_shuffle(DungeonItem):
"""Map Placement"""
item_name_group = "Maps"
displayname = "Map Shuffle"
class Crystals(Range):
range_start = 0
range_end = 7
@ -85,6 +125,7 @@ class Progressive(Choice):
def want_progressives(self, random):
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
class Palette(Choice):
option_default = 0
option_good = 1
@ -126,9 +167,10 @@ class HeartBeep(Choice):
displayname = "Heart Beep Rate"
option_normal = 0
option_double = 1
option_half = 2,
option_half = 2
option_quarter = 3
option_off = 4
alias_false = 4
class HeartColor(Choice):
@ -179,6 +221,10 @@ class TriforceHud(Choice):
alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,
"bigkey_shuffle": bigkey_shuffle,
"smallkey_shuffle": smallkey_shuffle,
"compass_shuffle": compass_shuffle,
"map_shuffle": map_shuffle,
"progressive": Progressive,
"shop_item_slots": ShopItemSlots,
"ow_palettes": OWPalette,
@ -193,6 +239,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"menuspeed": MenuSpeed,
"music": Music,
"reduceflashing": ReduceFlashing,
"triforcehud": TriforceHud
"triforcehud": TriforceHud,
"glitch_boots": DefaultOnToggle
}

View File

@ -676,12 +676,10 @@ location_table: typing.Dict[str,
from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()},
-1: "Cheat Console", -2: "Server"}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
lookup_id_to_name.update(shop_table_by_location_id)
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()},
"Cheat Console": -1, "Server": -2}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}}
lookup_name_to_id.update(shop_table_by_location)
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',

View File

@ -37,6 +37,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
from worlds.alttp.Items import ItemFactory, item_table
from worlds.alttp.EntranceShuffle import door_addresses
from worlds.alttp.Options import smallkey_shuffle
import Patch
try:
@ -763,7 +764,7 @@ def patch_rom(world, rom, player, enemized):
# patch items
for location in world.get_locations():
if location.player != player or location.address is None or location.shop_slot:
if location.player != player or location.address is None or location.shop_slot is not None:
continue
itemid = location.item.code if location.item is not None else 0x5A
@ -802,14 +803,14 @@ def patch_rom(world, rom, player, enemized):
# patch music
music_addresses = dungeon_music_addresses[location.name]
if world.mapshuffle[player]:
if world.map_shuffle[player]:
music = local_random.choice([0x11, 0x16])
else:
music = 0x11 if 'Pendant' in location.item.name else 0x16
for music_address in music_addresses:
rom.write_byte(music_address, music)
if world.mapshuffle[player]:
if world.map_shuffle[player]:
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
# patch entrance/exits/holes
@ -1491,18 +1492,18 @@ def patch_rom(world, rom, player, enemized):
# block HC upstairs doors in rain state in standard mode
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00)
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] is True else 0x00)
| (0x02 if world.compassshuffle[player] else 0x00)
| (0x04 if world.mapshuffle[player] else 0x00)
| (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.smallkey_shuffle[player] else 0x00)
| (0x02 if world.compass_shuffle[player] else 0x00)
| (0x04 if world.map_shuffle[player] else 0x00)
| (0x08 if world.bigkey_shuffle[player] else 0x00))) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
if world.clock_mode[player] or not world.dungeon_counters[player]:
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player] is True:
rom.write_byte(0x18003C, 0x02) # always on
elif world.compassshuffle[player] or world.dungeon_counters[player] == 'pickup':
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup
else:
rom.write_byte(0x18003C, 0x00)
@ -1515,10 +1516,11 @@ def patch_rom(world, rom, player, enemized):
# b - Big Key
# a - Small Key
#
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] is True else 0x00)
| (0x02 if world.bigkeyshuffle[player] else 0x00)
| (0x04 if world.mapshuffle[player] else 0x00)
| (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu
rom.write_byte(0x180045, ((0x00 if (world.smallkey_shuffle[player] == smallkey_shuffle.option_original_dungeon or
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) else 0x01)
| (0x02 if world.bigkey_shuffle[player] else 0x00)
| (0x04 if world.map_shuffle[player] else 0x00)
| (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu
# Map reveals
reveal_bytes = {
@ -1544,11 +1546,11 @@ def patch_rom(world, rom, player, enemized):
return 0x0000
rom.write_int16(0x18017A,
get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.mapshuffle[
get_reveal_bytes('Green Pendant') if world.map_shuffle[player] else 0x0000) # Sahasrahla reveal
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[
player] else 0x0000) # Bomb Shop Reveal
rom.write_byte(0x180172, 0x01 if world.keyshuffle[player] == "universal" else 0x00) # universal keys
rom.write_byte(0x180172, 0x01 if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else 0x00) # universal keys
rom.write_byte(0x18637E, 0x01 if world.retro[player] else 0x00) # Skip quiver in item shops once bought
rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
@ -2087,9 +2089,9 @@ def write_strings(rom, world, player):
if not dest:
return "nothing"
if ped_hint:
hint = dest.pedestal_hint_text if dest.pedestal_hint_text else "unknown item"
hint = dest.pedestal_hint_text
else:
hint = dest.hint_text if dest.hint_text else "something"
hint = dest.hint_text
if dest.player != player:
if ped_hint:
hint += f" for {world.player_name[dest.player]}!"
@ -2247,9 +2249,9 @@ def write_strings(rom, world, player):
# Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well.
items_to_hint = RelevantItems.copy()
if world.keyshuffle[player]:
if world.smallkey_shuffle[player]:
items_to_hint.extend(SmallKeys)
if world.bigkeyshuffle[player]:
if world.bigkey_shuffle[player]:
items_to_hint.extend(BigKeys)
local_random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 8

View File

@ -8,6 +8,7 @@ from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules
from worlds.alttp.Bosses import GanonDefeatRule
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \
item_name
from worlds.alttp.Options import smallkey_shuffle
def set_rules(world):
@ -99,7 +100,7 @@ def set_rules(world):
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
set_bunny_rules(world, player, world.mode[player] == 'inverted')
def mirrorless_path_to_castle_courtyard(world, player):
# If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch.
@ -211,17 +212,17 @@ def global_rules(world, player):
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Sewers Door', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player) or (
world.keyshuffle[player] == "universal" and world.mode[
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player) or (
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal and world.mode[
player] == 'standard')) # standard universal small keys cannot access the shop
set_rule(world.get_entrance('Sewers Back Door', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
set_rule(world.get_entrance('Agahnim 1', player),
lambda state: state.has_sword(player) and state.has_key('Small Key (Agahnims Tower)', player, 2))
lambda state: state.has_sword(player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8))
set_rule(world.get_location('Castle Tower - Dark Maze', player),
lambda state: state.can_kill_most_things(player, 8) and state.has_key('Small Key (Agahnims Tower)',
lambda state: state.can_kill_most_things(player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
player))
set_rule(world.get_location('Eastern Palace - Big Chest', player),
@ -238,15 +239,15 @@ def global_rules(world, player):
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state.has_key('Small Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state.has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
if not (world.keyshuffle[player] and world.bigkeyshuffle[player]):
if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]):
add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.world.get_region('Desert Palace Main (Outer)', player).can_reach(state))
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state.has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player))
@ -254,36 +255,36 @@ def global_rules(world, player):
set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state.has_key('Small Key (Swamp Palace)', player))
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player))
if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player)
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
if not world.keyshuffle[player] and world.logic[player] != 'nologic':
if not world.smallkey_shuffle[player] and world.logic[player] != 'nologic':
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
set_rule(world.get_entrance('Blind Fight', player), lambda state: state.has_key('Small Key (Thieves Town)', player))
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player))
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state.has_key('Small Key (Thieves Town)', player))
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2))
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2))
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player))
if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player)
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player))
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state.has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state.has_key('Small Key (Ice Palace)', player, 1))))
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state.has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ...
@ -292,13 +293,13 @@ def global_rules(world, player):
set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
# you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
# big key gives backdoor access to that from the teleporter in the north west
set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has_key('Big Key (Misery Mire)', player))
set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player))
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 2) if ((
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if ((
item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
(
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state.has_key('Small Key (Misery Mire)', player, 3))
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player))
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player))
@ -317,27 +318,27 @@ def global_rules(world, player):
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
if not world.enemy_shuffle[player]:
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 4))
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 3)))
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))
if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5))
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or (
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 4)))
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5))
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6))
set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
# these key rules are conservative, you might be able to get away with more lenient rules
randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right']
@ -346,30 +347,30 @@ def global_rules(world, player):
set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state.has_key('Small Key (Ganons Tower)', player, 3)))
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state.has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
# It is possible to need more than 2 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
# However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos.
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 2))
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 2))
# It is possible to need more than 3 keys ....
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3))
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
#The actual requirements for these rooms to avoid key-lock
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) or ((
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state.has_key('Small Key (Ganons Tower)', player, 2)))
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or ((
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2)))
for location in randomizer_room_chests:
set_rule(world.get_location(location, player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or (
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3)))
set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
# Once again it is possible to need more than 3 keys...
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
# Actual requirements
for location in compass_room_chests:
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state.has_key('Small Key (Ganons Tower)', player, 4) or (
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3))))
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))))
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
@ -388,9 +389,9 @@ def global_rules(world, player):
set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
lambda state: state.has_fire_source(player) and state.world.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
lambda state: state.has_key('Small Key (Ganons Tower)', player, 3))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
lambda state: state.has_key('Small Key (Ganons Tower)', player, 4))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4))
set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.world.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
@ -799,14 +800,14 @@ def add_conditional_lamps(world, player):
def open_rules(world, player):
# softlock protection as you can reach the sewers small key door with a guard drop key
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player))
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
def swordless_rules(world, player):
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state.has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
@ -849,7 +850,7 @@ def toss_junk_item(world, player):
def set_trock_key_rules(world, player):
# First set all relevant locked doors to impassible.
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room']:
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(True)
@ -877,28 +878,32 @@ def set_trock_key_rules(world, player):
# The following represent the common key rules.
# Big key door requires the big key, obviously. We removed this rule in the previous section to flag front_locked_locations correctly,
# otherwise crystaroller room might not be properly marked as reachable through the back.
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
# No matter what, the key requirement for going from the middle to the bottom should be three keys.
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3))
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
# might open all the locked doors in any order so we need maximally restrictive rules.
if can_reach_back:
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state.has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 4))
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
# Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3))
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
else:
# Middle to front requires 2 keys if the back is locked, otherwise 4
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2)
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)
if item_in_locations(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
else state.has_key('Small Key (Turtle Rock)', player, 4))
else state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
# Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2))
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 1))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
def tr_big_key_chest_keys_needed(state):
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
@ -911,14 +916,14 @@ def set_trock_key_rules(world, player):
return 4
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
if not can_reach_front and not world.keyshuffle[player]:
if not can_reach_front and not world.smallkey_shuffle[player]:
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
if not can_reach_big_chest:
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt':
if world.bigkeyshuffle[player] and can_reach_big_chest:
if world.bigkey_shuffle[player] and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:

View File

@ -6,6 +6,7 @@ import logging
from worlds.alttp.SubClasses import ALttPLocation
from worlds.alttp.EntranceShuffle import door_addresses
from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem
from worlds.alttp.Options import smallkey_shuffle
from Utils import int16_as_bytes
logger = logging.getLogger("Shops")
@ -22,6 +23,11 @@ class Shop():
slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots
blacklist: Set[str] = set() # items that don't work, todo: actually check against this
type = ShopType.Shop
slot_names: Dict[int, str] = {
0: "Left",
1: "Center",
2: "Right"
}
def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int):
self.region = region
@ -131,23 +137,22 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
def FillDisabledShopSlots(world):
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
for location in shop_locations if location.shop_slot and location.shop_slot_disabled}
for location in shop_locations
if location.shop_slot is not None and location.shop_slot_disabled}
for location in shop_slots:
location.shop_slot_disabled = True
slot_num = int(location.name[-1]) - 1
shop: Shop = location.parent_region.shop
location.item = ItemFactory(shop.inventory[slot_num]['item'], location.player)
location.item = ItemFactory(shop.inventory[location.shop_slot]['item'], location.player)
location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
def ShopSlotFill(world):
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
for location in shop_locations if location.shop_slot}
for location in shop_locations if location.shop_slot is not None}
removed = set()
for location in shop_slots:
slot_num = int(location.name[-1]) - 1
shop: Shop = location.parent_region.shop
if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled:
if not shop.can_push_inventory(location.shop_slot) or location.shop_slot_disabled:
location.shop_slot_disabled = True
removed.add(location)
@ -155,6 +160,7 @@ def ShopSlotFill(world):
shop_slots -= removed
if shop_slots:
logger.info("Filling LttP Shop Slots")
del shop_slots
from Fill import swap_location_item
@ -179,7 +185,7 @@ def ShopSlotFill(world):
shops_per_sphere.append(current_shops_slots)
candidates_per_sphere.append(current_candidates)
for location in sphere:
if location.shop_slot:
if location.shop_slot is not None:
if not location.shop_slot_disabled:
current_shops_slots.append(location)
elif not location.locked and not location.item.name in blacklist_words:
@ -229,7 +235,7 @@ def ShopSlotFill(world):
else:
price = world.random.randrange(8, 56)
shop.push_inventory(int(location.name[-1]) - 1, item_name, price * 5, 1,
shop.push_inventory(location.shop_slot, item_name, price * 5, 1,
location.item.player if location.item.player != location.player else 0)
@ -266,7 +272,7 @@ def create_shops(world, player: int):
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
player_shop_table["Dark Lake Hylia Shop"] = \
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
chance_100 = int(world.retro[player])*0.25+int(world.keyshuffle[player] == "universal") * 0.5
chance_100 = int(world.retro[player])*0.25+int(world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
region = world.get_region(region_name, player)
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
@ -278,10 +284,10 @@ def create_shops(world, player: int):
for index, item in enumerate(inventory):
shop.add_inventory(index, *item)
if not locked and num_slots:
slot_name = "{} Slot {}".format(region.name, index + 1)
slot_name = f"{region.name} {shop.slot_names[index]}"
loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name],
parent=region, hint_text="for sale")
loc.shop_slot = True
loc.shop_slot = index
loc.locked = True
if single_purchase_slots.pop():
if world.goal[player] != 'icerodhunt':
@ -337,9 +343,10 @@ total_shop_slots = len(shop_table) * 3
total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not data[4]) # data[4] -> locked
SHOP_ID_START = 0x400000
shop_table_by_location_id = {cnt: s for cnt, s in enumerate(
(f"{name} Slot {num}" for name in [key for key, value in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)]
for num in range(1, 4)), start=SHOP_ID_START)}
shop_table_by_location_id = dict(enumerate(
(f"{name} {Shop.slot_names[num]}" for name, shop_data in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
for num in range(3)), start=SHOP_ID_START))
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 1)] = "Take-Any #1"
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 2)] = "Take-Any #2"
@ -365,13 +372,13 @@ def set_up_shops(world, player: int):
rss = world.get_region('Red Shield Shop', player).shop
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
['Blue Shield', 50], ['Small Heart', 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
if world.keyshuffle[player] == "universal":
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
replacement_items.append(['Small Key (Universal)', 100])
replacement_item = world.random.choice(replacement_items)
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
rss.locked = True
if world.keyshuffle[player] == "universal" or world.retro[player]:
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal or world.retro[player]:
for shop in world.random.sample([s for s in world.shops if
s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player],
5):
@ -379,7 +386,7 @@ def set_up_shops(world, player: int):
slots = [0, 1, 2]
world.random.shuffle(slots)
slots = iter(slots)
if world.keyshuffle[player] == "universal":
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
if world.retro[player]:
shop.push_inventory(next(slots), 'Single Arrow', 80)

View File

@ -18,6 +18,7 @@ class ALttPLocation(Location):
class ALttPItem(Item):
game: str = "A Link to the Past"
dungeon = None
def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None,
sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None):
@ -29,4 +30,33 @@ class ALttPItem(Item):
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
def crystal(self) -> bool:
return self.type == 'Crystal'
@property
def smallkey(self) -> bool:
return self.type == 'SmallKey'
@property
def bigkey(self) -> bool:
return self.type == 'BigKey'
@property
def map(self) -> bool:
return self.type == 'Map'
@property
def compass(self) -> bool:
return self.type == 'Compass'
@property
def dungeon_item(self) -> Optional[str]:
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
return self.type
@property
def locked_dungeon_item(self):
return self.location.locked and self.dungeon_item

View File

@ -2,16 +2,17 @@ import random
import logging
import os
import threading
import typing
from BaseClasses import Item, CollectionState
from .SubClasses import ALttPItem
from ..AutoWorld import World
from .Options import alttp_options
from ..AutoWorld import World, LogicMixin
from .Options import alttp_options, smallkey_shuffle
from .Items import as_dict_item_table, item_name_groups, item_table
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
from .Rules import set_rules
from .ItemPool import generate_itempool
from .Shops import create_shops
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
import Patch
@ -21,28 +22,63 @@ from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_con
lttp_logger = logging.getLogger("A Link to the Past")
class ALTTPWorld(World):
game: str = "A Link to the Past"
options = alttp_options
topology_present = True
item_name_groups = item_name_groups
item_names = frozenset(item_table)
location_names = frozenset(lookup_name_to_id)
hint_blacklist = {"Triforce"}
item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int}
location_name_to_id = lookup_name_to_id
data_version = 7
data_version = 8
remote_items: bool = False
set_rules = set_rules
create_items = generate_itempool
def create_regions(self):
def __init__(self, *args, **kwargs):
self.dungeon_local_item_names = set()
self.dungeon_specific_item_names = set()
self.rom_name_available_event = threading.Event()
super(ALTTPWorld, self).__init__(*args, **kwargs)
def generate_early(self):
player = self.player
world = self.world
# system for sharing ER layouts
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
if "-" in world.shuffle[player]:
shuffle, seed = world.shuffle[player].split("-", 1)
world.shuffle[player] = shuffle
if shuffle == "vanilla":
world.er_seeds[player] = "vanilla"
elif seed.startswith("group-") or world.is_race:
world.er_seeds[player] = get_same_seed(world, (
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
else: # not a race or group seed, use set seed as is.
world.er_seeds[player] = seed
elif world.shuffle[player] == "vanilla":
world.er_seeds[player] = "vanilla"
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
option = getattr(world, dungeon_item)[player]
if option == "own_world":
world.local_items[player] |= self.item_name_groups[option.item_name_group]
elif option == "different_world":
world.non_local_items[player] |= self.item_name_groups[option.item_name_group]
elif option.in_dungeon:
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon":
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
def create_regions(self):
player = self.player
world = self.world
if world.open_pyramid[player] == 'goal':
@ -67,7 +103,6 @@ class ALTTPWorld(World):
create_shops(world, player)
create_dungeons(world, player)
if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
world.fix_fake_world[player] = False
@ -155,9 +190,11 @@ class ALTTPWorld(World):
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
return 'Blue Shield'
elif 'Bow' in item_name:
if state.has('Silver', item.player):
if state.has('Silver Bow', item.player):
return
elif state.has('Bow', item.player) and self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2:
elif state.has('Bow', item.player) and (self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2
or self.world.logic[item.player] == 'noglitches'
or self.world.swordless[item.player]): # modes where silver bow is always required for ganon
return 'Silver Bow'
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
return 'Bow'
@ -203,7 +240,11 @@ class ALTTPWorld(World):
@classmethod
def stage_pre_fill(cls, world):
from .Dungeons import fill_dungeons_restrictive
fill_dungeons_restrictive(world)
fill_dungeons_restrictive(cls, world)
@classmethod
def stage_post_fill(cls, world):
ShopSlotFill(world)
def generate_output(self, output_directory: str):
world = self.world
@ -279,11 +320,13 @@ class ALTTPWorld(World):
return ALttPItem(name, self.player, **as_dict_item_table[name])
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, restitempool, fill_locations):
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
restitempool, fill_locations):
trash_counts = {}
standard_keyshuffle_players = set()
for player in world.get_game_players("A Link to the Past"):
if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \
and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal:
standard_keyshuffle_players.add(player)
if not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
@ -341,3 +384,21 @@ class ALTTPWorld(World):
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill) # very slow, unfortunately
trash_count -= 1
def get_same_seed(world, seed_def: tuple) -> str:
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
if seed_def in seeds:
return seeds[seed_def]
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
world.__named_seeds = seeds
return seeds[seed_def]
class ALttPLogic(LogicMixin):
def _lttp_has_key(self, item, player, count: int = 1):
if self.world.logic[player] == 'nologic':
return True
if self.world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
return self.can_buy_unlimited('Small Key (Universal)', player)
return self.prog_items[item, player] >= count

View File

@ -184,10 +184,7 @@ class Factorio(World):
max_energy = remaining_energy * 0.75
min_energy = (remaining_energy - max_energy) / remaining_num_ingredients
ingredient = pool.pop()
if ingredient not in recipes:
logging.warning(f"missing recipe for {ingredient}")
continue
ingredient_recipe = recipes[ingredient]
ingredient_recipe = min(all_product_sources[ingredient], key=lambda recipe: recipe.rel_cost)
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
ingredient_energy = ingredient_recipe.total_energy
min_num_raw = min_raw/ingredient_raw

View File

@ -1,6 +1,20 @@
from typing import NamedTuple, Union
import logging
from ..AutoWorld import World
class GenericWorld(World):
game = "Archipelago"
topology_present = False
item_name_to_id = {
"Nothing": -1
}
location_name_to_id = {
"Cheat Console" : -1,
"Server": -2
}
hidden = True
class PlandoItem(NamedTuple):
item: str

View File

@ -19,6 +19,8 @@ class HKWorld(World):
item_name_to_id = {name: data.id for name, data in item_table.items() if data.type != "Event"}
location_name_to_id = lookup_name_to_id
hidden = True
def generate_basic(self):
# Link regions
self.world.get_entrance('Hollow Nest S&Q', self.player).connect(self.world.get_region('Hollow Nest', self.player))

View File

@ -58,6 +58,7 @@ item_table = {
"Dragon Egg Shard": ItemData(45043, True),
"Bee Trap (Minecraft)": ItemData(45100, False),
"Blaze Rods": ItemData(None, True),
"Victory": ItemData(None, True)
}

View File

@ -109,6 +109,7 @@ advancement_table = {
"Librarian": AdvData(42090, 'Overworld'),
"Overpowered": AdvData(42091, 'Overworld'),
"Blaze Spawner": AdvData(None, 'Nether Fortress'),
"Ender Dragon": AdvData(None, 'The End')
}

View File

@ -78,13 +78,13 @@ mandatory_connections = [
('End Portal', 'The End')
]
default_connections = {
default_connections = [
('Overworld Structure 1', 'Village'),
('Overworld Structure 2', 'Pillager Outpost'),
('Nether Structure 1', 'Nether Fortress'),
('Nether Structure 2', 'Bastion Remnant'),
('The End Structure', 'End City')
}
]
# Structure: illegal locations
illegal_connections = {

View File

@ -31,7 +31,7 @@ class MinecraftLogic(LogicMixin):
return self.can_reach('Nether Fortress', 'Region', player) and self._mc_basic_combat(player)
def _mc_can_brew_potions(self, player: int):
return self._mc_fortress_loot(player) and self.has('Brewing', player) and self._mc_has_bottle(player)
return self.has('Blaze Rods', player) and self.has('Brewing', player) and self._mc_has_bottle(player)
def _mc_can_piglin_trade(self, player: int):
return self._mc_has_gold_ingots(player) and (
@ -39,7 +39,7 @@ class MinecraftLogic(LogicMixin):
player))
def _mc_enter_stronghold(self, player: int):
return self._mc_fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
return self.has('Blaze Rods', player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
# Difficulty-dependent functions
def _mc_combat_difficulty(self, player: int):
@ -135,6 +135,7 @@ def set_rules(world: MultiWorld, player: int):
set_rule(world.get_entrance("The End Structure", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("The End Structure", player))
set_rule(world.get_location("Ender Dragon", player), lambda state: can_complete(state))
set_rule(world.get_location("Blaze Spawner", player), lambda state: state._mc_fortress_loot(player))
set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state._mc_can_piglin_trade(player))
set_rule(world.get_location("Oh Shiny", player), lambda state: state._mc_can_piglin_trade(player))

View File

@ -72,9 +72,9 @@ class MinecraftWorld(World):
exclusion_pool.update(exclusion_table[key])
exclusion_rules(self.world, self.player, exclusion_pool)
# Prefill the Ender Dragon with the completion condition
completion = self.create_item("Victory")
self.world.get_location("Ender Dragon", self.player).place_locked_item(completion)
# Prefill event locations with their events
self.world.get_location("Blaze Spawner", self.player).place_locked_item(self.create_item("Blaze Rods"))
self.world.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Victory"))
self.world.itempool += itempool

View File

@ -19,6 +19,8 @@ class OriBlindForest(World):
options = options
hidden = True
def generate_early(self):
logic_sets = {"casual-core"}
for logic_set in location_rules:

39
worlds/spire/Items.py Normal file
View File

@ -0,0 +1,39 @@
import typing
from BaseClasses import Item
from typing import Dict
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
progression: bool
event: bool = False
item_table: Dict[str, ItemData] = {
'Card Draw': ItemData(8000, True),
'Rare Card Draw': ItemData(8001, True),
'Relic': ItemData(8002, True),
'Boss Relic': ItemData(8003, True),
# Event Items
'Victory': ItemData(None, True, True),
'Beat Act 1 Boss': ItemData(None, True, True),
'Beat Act 2 Boss': ItemData(None, True, True),
'Beat Act 3 Boss': ItemData(None, True, True),
}
item_pool: Dict[str, int] = {
'Card Draw': 15,
'Rare Card Draw': 3,
'Relic': 10,
'Boss Relic': 3
}
event_item_pairs: Dict[str, str] = {
"Heart Room": "Victory",
"Act 1 Boss": "Beat Act 1 Boss",
"Act 2 Boss": "Beat Act 2 Boss",
"Act 3 Boss": "Beat Act 3 Boss"
}

37
worlds/spire/Locations.py Normal file
View File

@ -0,0 +1,37 @@
location_table = {
'Card Draw 1': 19001,
'Card Draw 2': 19002,
'Card Draw 3': 19003,
'Card Draw 4': 19004,
'Card Draw 5': 19005,
'Card Draw 6': 19006,
'Card Draw 7': 19007,
'Card Draw 8': 19008,
'Card Draw 9': 19009,
'Card Draw 10': 19010,
'Card Draw 11': 19011,
'Card Draw 12': 19012,
'Card Draw 13': 19013,
'Card Draw 14': 19014,
'Card Draw 15': 19015,
'Rare Card Draw 1': 21001,
'Rare Card Draw 2': 21002,
'Rare Card Draw 3': 21003,
'Relic 1': 20001,
'Relic 2': 20002,
'Relic 3': 20003,
'Relic 4': 20004,
'Relic 5': 20005,
'Relic 6': 20006,
'Relic 7': 20007,
'Relic 8': 20008,
'Relic 9': 20009,
'Relic 10': 20010,
'Boss Relic 1': 22001,
'Boss Relic 2': 22002,
'Boss Relic 3': 22003,
'Heart Room': None,
'Act 1 Boss': None,
'Act 2 Boss': None,
'Act 3 Boss': None
}

36
worlds/spire/Options.py Normal file
View File

@ -0,0 +1,36 @@
import typing
from Options import Choice, Option, Range, Toggle
class Character(Choice):
"""Pick What Character you wish to play with."""
display_name = "Character"
option_ironclad = 0
option_silent = 1
option_defect = 2
option_watcher = 3
default = 0
class Ascension(Range):
"""What Ascension do you wish to play with."""
display_name = "Ascension"
range_start = 0
range_end = 20
default = 0
class HeartRun(Toggle):
"""Whether or not you will need to collect they 3 keys to unlock the final act
and beat the heart to finish the game."""
display_name = "Heart Run"
option_true = 1
option_false = 0
default = 0
spire_options: typing.Dict[str, type(Option)] = {
"character": Character,
"ascension": Ascension,
"heart_run": HeartRun
}

11
worlds/spire/Regions.py Normal file
View File

@ -0,0 +1,11 @@
def create_regions(world, player: int):
from . import create_region
from .Locations import location_table
world.regions += [
create_region(world, player, 'Menu', None, ['Neow\'s Room']),
create_region(world, player, 'The Spire', [location for location in location_table])
]
# link up our region with the entrance we just made
world.get_entrance('Neow\'s Room', player).connect(world.get_region('The Spire', player))

76
worlds/spire/Rules.py Normal file
View File

@ -0,0 +1,76 @@
from BaseClasses import MultiWorld
from ..AutoWorld import LogicMixin
from ..generic.Rules import set_rule
class SpireLogic(LogicMixin):
def _spire_has_relics(self, player: int, amount: int) -> bool:
count: int = self.item_count("Relic", player) + self.item_count("Boss Relic", player)
return count >= amount
def _spire_has_cards(self, player: int, amount: int) -> bool:
count = self.item_count("Card Draw", player) + self.item_count("Rare Card Draw", player)
return count >= amount
def set_rules(world: MultiWorld, player: int):
# Act 1 Card Draws
set_rule(world.get_location("Card Draw 1", player), lambda state: True)
set_rule(world.get_location("Card Draw 2", player), lambda state: True)
set_rule(world.get_location("Card Draw 3", player), lambda state: True)
set_rule(world.get_location("Card Draw 4", player), lambda state: state._spire_has_relics(player, 1))
set_rule(world.get_location("Card Draw 5", player), lambda state: state._spire_has_relics(player, 1))
# Act 1 Relics
set_rule(world.get_location("Relic 1", player), lambda state: state._spire_has_cards(player, 1))
set_rule(world.get_location("Relic 2", player), lambda state: state._spire_has_cards(player, 2))
set_rule(world.get_location("Relic 3", player), lambda state: state._spire_has_cards(player, 2))
# Act 1 Boss Event
set_rule(world.get_location("Act 1 Boss", player), lambda state: state._spire_has_cards(player, 3) and state._spire_has_relics(player, 2))
# Act 1 Boss Rewards
set_rule(world.get_location("Rare Card Draw 1", player), lambda state: state.has("Beat Act 1 Boss", player))
set_rule(world.get_location("Boss Relic 1", player), lambda state: state.has("Beat Act 1 Boss", player))
# Act 2 Card Draws
set_rule(world.get_location("Card Draw 6", player), lambda state: state.has("Beat Act 1 Boss", player))
set_rule(world.get_location("Card Draw 7", player), lambda state: state.has("Beat Act 1 Boss", player))
set_rule(world.get_location("Card Draw 8", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 6) and state._spire_has_relics(player, 3))
set_rule(world.get_location("Card Draw 9", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 6) and state._spire_has_relics(player, 4))
set_rule(world.get_location("Card Draw 10", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 4))
# Act 2 Relics
set_rule(world.get_location("Relic 4", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 2))
set_rule(world.get_location("Relic 5", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 2))
set_rule(world.get_location("Relic 6", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 3))
# Act 2 Boss Event
set_rule(world.get_location("Act 2 Boss", player), lambda state: state.has("Beat Act 1 Boss", player) and state._spire_has_cards(player, 7) and state._spire_has_relics(player, 4) and state.has("Boss Relic", player))
# Act 2 Boss Rewards
set_rule(world.get_location("Rare Card Draw 2", player), lambda state: state.has("Beat Act 2 Boss", player))
set_rule(world.get_location("Boss Relic 2", player), lambda state: state.has("Beat Act 2 Boss", player))
# Act 3 Card Draws
set_rule(world.get_location("Card Draw 11", player), lambda state: state.has("Beat Act 2 Boss", player))
set_rule(world.get_location("Card Draw 12", player), lambda state: state.has("Beat Act 2 Boss", player))
set_rule(world.get_location("Card Draw 13", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4))
set_rule(world.get_location("Card Draw 14", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4))
set_rule(world.get_location("Card Draw 15", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4))
# Act 3 Relics
set_rule(world.get_location("Relic 7", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 4))
set_rule(world.get_location("Relic 8", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5))
set_rule(world.get_location("Relic 9", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5))
set_rule(world.get_location("Relic 10", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 5))
# Act 3 Boss Event
set_rule(world.get_location("Act 3 Boss", player), lambda state: state.has("Beat Act 2 Boss", player) and state._spire_has_relics(player, 7) and state.has("Boss Relic", player, 2))
# Act 3 Boss Rewards
set_rule(world.get_location("Rare Card Draw 3", player), lambda state: state.has("Beat Act 3 Boss", player))
set_rule(world.get_location("Boss Relic 3", player), lambda state: state.has("Beat Act 3 Boss", player))
set_rule(world.get_location("Heart Room", player), lambda state: state.has("Beat Act 3 Boss", player))

110
worlds/spire/__init__.py Normal file
View File

@ -0,0 +1,110 @@
import string
from BaseClasses import Item, MultiWorld, Region, Location, Entrance
from .Items import item_table, item_pool, event_item_pairs
from .Locations import location_table
from .Regions import create_regions
from .Rules import set_rules
from ..AutoWorld import World
from .Options import spire_options
class SpireWorld(World):
options = spire_options
game = "Slay the Spire"
topology_present = False
data_version = 1
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = location_table
def _get_slot_data(self):
return {
'seed': "".join(self.world.slot_seeds[self.player].choice(string.ascii_letters) for i in range(16)),
'character': self.world.character[self.player],
'ascension': self.world.ascension[self.player],
'heart_run': self.world.heart_run[self.player]
}
def generate_basic(self):
# Fill out our pool with our items from item_pool, assuming 1 item if not present in item_pool
pool = []
for name, data in item_table.items():
if not data.event:
if name in item_pool:
card_draw = 0
for amount in range(item_pool[name]):
item = SpireItem(name, self.player)
# This feels wrong but it makes our failure rate drop dramatically
# makes all but 7 basic card draws trash fill
if item.name == "Card Draw":
card_draw += 1
if card_draw > 7:
item.advancement = False
pool.append(item)
else:
item = SpireItem(name, self.player)
pool.append(item)
self.world.itempool += pool
# Pair up our event locations with our event items
for event, item in event_item_pairs.items():
event_item = SpireItem(item, self.player)
self.world.get_location(event, self.player).place_locked_item(event_item)
if self.world.logic[self.player] != 'no logic':
self.world.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
def set_rules(self):
set_rules(self.world, self.player)
def create_item(self, name: str) -> Item:
item_data = item_table[name]
return Item(name, item_data.progression, item_data.code, self.player)
def create_regions(self):
create_regions(self.world, self.player)
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in spire_options:
option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = int(option.value)
return slot_data
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, None, name, player)
ret.world = world
if locations:
for location in locations:
loc_id = location_table.get(location, 0)
location = SpireLocation(player, location, loc_id, ret)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret
class SpireLocation(Location):
game: str = "Slay the Spire"
def __init__(self, player: int, name: str, address=None, parent=None):
super(SpireLocation, self).__init__(player, name, address, parent)
if address is None:
self.event = True
self.locked = True
class SpireItem(Item):
game = "Slay the Spire"
def __init__(self, name, player: int = None):
item_data = item_table[name]
super(SpireItem, self).__init__(name, item_data.progression, item_data.code, player)