Hollow Knight integration

(prototype status)
This commit is contained in:
Fabian Dill 2021-02-21 20:17:24 +01:00
parent dcce53f8c8
commit ff9b24e88e
21 changed files with 1869 additions and 351 deletions

View File

@ -136,6 +136,7 @@ class MultiWorld():
set_player_attr('plando_items', [])
set_player_attr('plando_texts', {})
set_player_attr('plando_connections', [])
set_player_attr('game', "A Link to the Past")
self.worlds = []
#for i in range(players):
@ -148,6 +149,14 @@ class MultiWorld():
def player_ids(self):
yield from range(1, self.players + 1)
@property
def alttp_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "A Link to the Past")
@property
def hk_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Hollow Knight")
def get_name_string_for_object(self, obj) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
@ -1012,7 +1021,9 @@ class Dungeon(object):
def is_dungeon_item(self, item: Item) -> bool:
return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items]
def __eq__(self, other: Item) -> bool:
def __eq__(self, other: Dungeon) -> bool:
if not other:
return False
return self.name == other.name and self.player == other.player
def __repr__(self):
@ -1031,29 +1042,25 @@ class Boss():
def can_defeat(self, state) -> bool:
return self.defeat_rule(state, self.player)
class Location():
shop_slot: bool = False
shop_slot_disabled: bool = False
event: bool = False
locked: bool = False
spot_type = 'Location'
game: str = "Generic"
crystal: bool = False
def __init__(self, player: int, name: str = '', address=None, crystal: bool = False,
hint_text: Optional[str] = None, parent=None,
player_address=None):
def __init__(self, player: int, name: str = '', address:int = None, parent=None):
self.name = name
self.parent_region = parent
self.item = None
self.crystal = crystal
self.address = address
self.player_address = player_address
self.hint_text: str = hint_text if hint_text else name
self.parent_region = parent
self.recursion_count = 0
self.player = player
self.item = None
self.always_allow = lambda item, state: False
self.access_rule = lambda state: True
self.item_rule = lambda item: True
self.player = player
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)))
@ -1077,24 +1084,42 @@ class Location():
def __lt__(self, other):
return (self.player, self.name) < (other.player, other.name)
@property
def hint_text(self):
hint_text = getattr(self, "_hint_text", None)
if not hint_text:
return self.name
class Item(object):
class Item():
location: Optional[Location] = None
world: Optional[World] = None
game: str = "Generic"
type: str = None
pedestal_credit_text = "and the Unknown Item"
sickkid_credit_text = None
magicshop_credit_text = None
zora_credit_text = None
fluteboy_credit_text = None
def __init__(self, name='', advancement=False, type=None, code=None, pedestal_hint=None, pedestal_credit=None, sickkid_credit=None, zora_credit=None, witch_credit=None, fluteboy_credit=None, hint_text=None, player=None):
def __init__(self, name: str, advancement:bool, code:int, player:int):
self.name = name
self.advancement = advancement
self.type = type
self.pedestal_hint_text = pedestal_hint
self.pedestal_credit_text = pedestal_credit
self.sickkid_credit_text = sickkid_credit
self.zora_credit_text = zora_credit
self.magicshop_credit_text = witch_credit
self.fluteboy_credit_text = fluteboy_credit
self.hint_text = hint_text
self.code = code
self.player = player
self.code = code
@property
def hint_text(self):
hint_text = getattr(self, "_hint_text", None)
if not hint_text:
return self.name
return hint_text
@property
def pedestal_hint_text(self):
pedestal_hint_text = getattr(self, "_pedestal_hint_text", None)
if not pedestal_hint_text:
return self.name
return pedestal_hint_text
def __eq__(self, other):
return self.name == other.name and self.player == other.player
@ -1134,11 +1159,6 @@ class Item(object):
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
# have 6 address that need to be filled
class Crystal(Item):
pass
class Spoiler(object):
world: MultiWorld
@ -1224,7 +1244,7 @@ class Spoiler(object):
shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement']
self.shops.append(shopdata)
for player in range(1, self.world.players + 1):
for player in self.world.alttp_player_ids:
self.bosses[str(player)] = OrderedDict()
self.bosses[str(player)]["Eastern Palace"] = self.world.get_dungeon("Eastern Palace", player).boss.name
self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name
@ -1252,8 +1272,8 @@ class Spoiler(object):
if self.world.players == 1:
self.bosses = self.bosses["1"]
from Utils import __version__ as ERVersion
self.metadata = {'version': ERVersion,
from Utils import __version__ as APVersion
self.metadata = {'version': APVersion,
'logic': self.world.logic,
'dark_room_logic': self.world.dark_room_logic,
'mode': self.world.mode,
@ -1292,6 +1312,7 @@ class Spoiler(object):
'shuffle_prizes': self.world.shuffle_prizes,
'sprite_pool': self.world.sprite_pool,
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
'game': self.world.game,
'er_seeds': self.world.er_seeds
}
@ -1331,74 +1352,80 @@ class Spoiler(object):
for player in range(1, self.world.players + 1):
if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
for team in range(self.world.teams):
outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if self.world.teams > 1 else 'Hash: ',
self.hashes[player, team]))
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
outfile.write('Restricted Boss Drops: %s\n' %
bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][player]))
outfile.write('Game: %s\n' % self.metadata['game'][player])
if self.world.players > 1:
outfile.write('Progression Balanced: %s\n' % (
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Retro: %s\n' %
('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if "triforce" in self.metadata["goal"][player]: # triforce hunt
outfile.write("Pieces available for Triforce: %s\n" %
self.metadata['triforce_pieces_available'][player])
outfile.write("Pieces required for Triforce: %s\n" %
self.metadata["triforce_pieces_required"][player])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Item Progression: %s\n' % self.metadata['progressive'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
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' %
bool_to_text("p" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop upgrade shuffle: %s\n' %
bool_to_text("u" in self.metadata["shop_shuffle"][player]))
outfile.write('New Shop inventory: %s\n' %
bool_to_text("g" in self.metadata["shop_shuffle"][player] or
"f" in self.metadata["shop_shuffle"][player]))
outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop Slots: %s\n' %
self.metadata["shop_shuffle_slots"][player])
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
outfile.write(
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
outfile.write(f'Killable thieves: {bool_to_text(self.metadata["killable_thieves"][player])}\n')
outfile.write(f'Shuffled tiles: {bool_to_text(self.metadata["tile_shuffle"][player])}\n')
outfile.write(f'Shuffled bushes: {bool_to_text(self.metadata["bush_shuffle"][player])}\n')
outfile.write(
'Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
outfile.write('Beemizer: %s\n' % self.metadata['beemizer'][player])
outfile.write('Pot shuffle %s\n'
% ('Yes' if self.metadata['shufflepots'][player] else 'No'))
outfile.write('Prize shuffle %s\n' %
self.metadata['shuffle_prizes'][player])
if player in self.world.alttp_player_ids:
for team in range(self.world.teams):
outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
(player in self.world.alttp_player_ids and self.world.teams > 1) else 'Hash: ',
self.hashes[player, team]))
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
outfile.write('Restricted Boss Drops: %s\n' %
bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][player]))
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Retro: %s\n' %
('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if "triforce" in self.metadata["goal"][player]: # triforce hunt
outfile.write("Pieces available for Triforce: %s\n" %
self.metadata['triforce_pieces_available'][player])
outfile.write("Pieces required for Triforce: %s\n" %
self.metadata["triforce_pieces_required"][player])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Item Progression: %s\n' % self.metadata['progressive'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
outfile.write('Crystals required for GT: %s\n' % self.metadata['gt_crystals'][player])
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][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' %
bool_to_text("p" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop upgrade shuffle: %s\n' %
bool_to_text("u" in self.metadata["shop_shuffle"][player]))
outfile.write('New Shop inventory: %s\n' %
bool_to_text("g" in self.metadata["shop_shuffle"][player] or
"f" in self.metadata["shop_shuffle"][player]))
outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop Slots: %s\n' %
self.metadata["shop_shuffle_slots"][player])
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
outfile.write(
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
outfile.write(f'Killable thieves: {bool_to_text(self.metadata["killable_thieves"][player])}\n')
outfile.write(f'Shuffled tiles: {bool_to_text(self.metadata["tile_shuffle"][player])}\n')
outfile.write(f'Shuffled bushes: {bool_to_text(self.metadata["bush_shuffle"][player])}\n')
outfile.write(
'Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
outfile.write('Beemizer: %s\n' % self.metadata['beemizer'][player])
outfile.write('Pot shuffle %s\n'
% ('Yes' if self.metadata['shufflepots'][player] else 'No'))
outfile.write('Prize shuffle %s\n' %
self.metadata['shuffle_prizes'][player])
if self.entrances:
outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: '

2
Gui.py
View File

@ -17,7 +17,7 @@ ModuleUpdate.update()
from worlds.alttp.AdjusterMain import adjust
from worlds.alttp.EntranceRandomizer import parse_arguments
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
from worlds.alttp.Main import main, get_seed, __version__ as MWVersion
from Main import main, get_seed, __version__ as MWVersion
from worlds.alttp.Rom import Sprite
from Utils import local_path, output_path, open_file

View File

@ -9,7 +9,8 @@ import zlib
import concurrent.futures
import pickle
from BaseClasses import MultiWorld, CollectionState, Item, Region, Location
from BaseClasses import MultiWorld, CollectionState, Region
from worlds.alttp import ALttPLocation, ALttPItem
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
lookup_vanilla_location_to_entrance
@ -22,6 +23,7 @@ from Fill import distribute_items_restrictive, flood_items, balance_multiworld_p
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
from worlds.hk import *
import Patch
seeddigits = 20
@ -96,6 +98,7 @@ def main(args, seed=None):
world.er_seeds = args.er_seeds.copy()
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy()
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
@ -119,17 +122,7 @@ def main(args, seed=None):
logger.info('')
for player in range(1, world.players + 1):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull'} or not world.shuffle_ganon)
else:
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
for player in world.player_ids:
for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player)
if item:
@ -164,6 +157,19 @@ def main(args, seed=None):
world.non_local_items[player] -= item_name_groups['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals']
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull'} or not world.shuffle_ganon)
else:
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
if world.mode[player] != 'inverted':
@ -175,7 +181,7 @@ def main(args, seed=None):
logger.info('Shuffling the World about.')
for player in range(1, world.players + 1):
for player in world.alttp_player_ids:
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
@ -196,14 +202,19 @@ def main(args, seed=None):
logger.info('Generating Item Pool.')
for player in range(1, world.players + 1):
for player in world.alttp_player_ids:
generate_itempool(world, player)
logger.info('Calculating Access Rules.')
for player in range(1, world.players + 1):
for player in world.alttp_player_ids:
set_rules(world, player)
logger.info("Doing Hollow Knight things")
for player in world.hk_player_ids:
gen_hollow(world, player)
logger.info("Running Item Plando")
distribute_planned(world)
@ -239,9 +250,7 @@ def main(args, seed=None):
if world.players > 1:
balance_multiworld_progression(world)
logger.info('Patching ROM.')
logger.info('Generating output files.')
outfilebase = 'AP_%s' % (args.outputname if args.outputname else world.seed)
@ -349,7 +358,7 @@ def main(args, seed=None):
rom_futures = []
for team in range(world.teams):
for player in range(1, world.players + 1):
for player in world.alttp_player_ids:
rom_futures.append(pool.submit(_gen_rom, team, player))
def get_entrance_to_region(region: Region):
@ -382,7 +391,9 @@ def main(args, seed=None):
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.parent_region.dungeon:
if location.game == "Hollow Knight":
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)
@ -423,9 +434,15 @@ def main(args, seed=None):
rom_name = future.result()
rom_names.append(rom_name)
minimum_versions = {"server": (0, 0, 1)}
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names}
for i, team in enumerate(parsed_names):
for player, name in enumerate(team, 1):
if player in world.hk_player_ids:
connect_names[name] = (i, player)
multidata = zlib.compress(pickle.dumps({"names": parsed_names,
"roms": {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names},
"connect_names": connect_names,
"remote_items": {player for player in range(1, world.players + 1) if
world.remote_items[player]},
"locations": {
@ -509,8 +526,9 @@ def copy_world(world):
ret.shop_shuffle_slots = world.shop_shuffle_slots.copy()
ret.dark_room_logic = world.dark_room_logic.copy()
ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.copy()
ret.game = world.game.copy()
for player in range(1, world.players + 1):
for player in world.alttp_player_ids:
if world.mode[player] != 'inverted':
create_regions(ret, player)
else:
@ -518,6 +536,9 @@ def copy_world(world):
create_shops(ret, player)
create_dungeons(ret, player)
for player in world.hk_player_ids:
gen_regions(ret, player)
copy_dynamic_regions_and_locations(world, ret)
# copy bosses
@ -541,7 +562,7 @@ def copy_world(world):
# fill locations
for location in world.get_locations():
if location.item is not None:
item = Item(location.item.name, location.item.advancement, location.item.type, player = location.item.player)
item = ALttPItem(location.item.name, location.item.advancement, location.item.type, player = location.item.player)
ret.get_location(location.name, location.player).item = item
item.location = ret.get_location(location.name, location.player)
item.world = ret
@ -552,7 +573,7 @@ def copy_world(world):
# copy remaining itempool. No item in itempool should have an assigned location
for item in world.itempool:
ret.itempool.append(Item(item.name, item.advancement, item.type, player = item.player))
ret.itempool.append(ALttPItem(item.name, item.advancement, item.type, player = item.player))
for item in world.precollected_items:
ret.push_precollected(ItemFactory(item.name, item.player))
@ -561,7 +582,7 @@ def copy_world(world):
ret.state.prog_items = world.state.prog_items.copy()
ret.state.stale = {player: True for player in range(1, world.players + 1)}
for player in range(1, world.players + 1):
for player in world.alttp_player_ids:
set_rules(ret, player)
@ -584,7 +605,7 @@ def copy_dynamic_regions_and_locations(world, ret):
for location in world.dynamic_locations:
new_reg = ret.get_region(location.parent_region.name, location.parent_region.player)
new_loc = Location(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg)
new_loc = ALttPLocation(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg)
# todo: this is potentially dangerous. later refactor so we
# can apply dynamic region rules on top of copied world like other rules
new_loc.access_rule = location.access_rule
@ -702,12 +723,13 @@ def create_playthrough(world):
old_world.spoiler.paths = dict()
for player in range(1, world.players + 1):
old_world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
for path in dict(old_world.spoiler.paths).values():
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode[player] != 'inverted':
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
else:
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
if player in world.alttp_player_ids:
for path in dict(old_world.spoiler.paths).values():
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode[player] != 'inverted':
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
else:
old_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
old_world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}

View File

@ -14,7 +14,7 @@ import shutil
from random import randrange
from Utils import get_item_name_from_id, get_location_name_from_address, ReceivedItem
from Utils import get_item_name_from_id, get_location_name_from_address
exit_func = atexit.register(input, "Press enter to close.")
@ -30,8 +30,8 @@ from NetUtils import *
import WebUI
from worlds.alttp import Regions, Shops
from worlds.alttp import Items
import Utils
import Items
# logging note:
# logging.* gets send to only the text console, logger.* gets send to the WebUI as well, if it's initialized.
@ -44,8 +44,6 @@ def create_named_task(coro, *args, name=None):
return asyncio.create_task(coro, *args, name=name)
class Context():
def __init__(self, snes_address, server_address, password, found_items, port: int):
self.snes_address = snes_address
@ -122,6 +120,9 @@ class Context():
return
await self.server.socket.send(dumps(msgs))
def consume_players_package(self, package:typing.List[tuple]):
self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team}
def color_item(item_id: int, green: bool = False) -> str:
item_name = get_item_name_from_id(item_id)
@ -819,26 +820,24 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
version = ".".join(str(item) for item in version)
logger.info(f'Server protocol version: {version}')
if "tags" in args:
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if args['password']:
logger.info('Password required')
if "forfeit_mode" in args: # could also be version > 2.2.1, but going with implicit content here
logging.info(f"Forfeit setting: {args['forfeit_mode']}")
logging.info(f"Remaining setting: {args['remaining_mode']}")
logging.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}"
f" for each location checked.")
ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points'])
ctx.forfeit_mode = args['forfeit_mode']
ctx.remaining_mode = args['remaining_mode']
ctx.ui_node.send_game_info(ctx)
logging.info(f"Forfeit setting: {args['forfeit_mode']}")
logging.info(f"Remaining setting: {args['remaining_mode']}")
logging.info(f"A !hint costs {args['hint_cost']} points and you get {args['location_check_points']}"
f" for each location checked.")
ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points'])
ctx.forfeit_mode = args['forfeit_mode']
ctx.remaining_mode = args['remaining_mode']
ctx.ui_node.send_game_info(ctx)
if len(args['players']) < 1:
logger.info('No player connected')
else:
args['players'].sort()
current_team = -1
logger.info('Connected players:')
logger.info('Players:')
for team, slot, name in args['players']:
if team != current_team:
logger.info(f' Team #{team + 1}')
@ -848,7 +847,7 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
elif cmd == 'ConnectionRefused':
errors = args["errors"]
if 'InvalidRom' in errors:
if 'InvalidSlot' in errors:
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
asyncio.create_task(ctx.snes_socket.close())
raise Exception('Invalid ROM detected, '
@ -871,7 +870,7 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
Utils.persistent_store("servers", ctx.rom, ctx.server_address)
ctx.team = args["team"]
ctx.slot = args["slot"]
ctx.player_names = {p: n for p, n in args["playernames"]}
ctx.consume_players_package(args["players"])
msgs = []
if ctx.locations_checked:
msgs.append(['LocationChecks',
@ -904,7 +903,7 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
if start_index == len(ctx.items_received):
for item in args['items']:
ctx.items_received.append(ReceivedItem(*item))
ctx.items_received.append(NetworkItem(*item))
ctx.watcher_event.set()
elif cmd == 'LocationInfo':
@ -917,13 +916,13 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
ctx.locations_info[location] = (item, player)
ctx.watcher_event.set()
elif cmd == 'ItemSent':
found = ReceivedItem(*args["item"])
elif cmd == 'ItemSent': # going away
found = NetworkItem(*args["item"])
receiving_player = args["receiver"]
ctx.ui_node.notify_item_sent(ctx.player_names[found.player], ctx.player_names[receiving_player],
get_item_name_from_id(found.item), get_location_name_from_address(found.location),
found.player == ctx.slot, receiving_player == ctx.slot,
get_item_name_from_id(item) in Items.progression_items)
get_item_name_from_id(found.item) in Items.progression_items)
item = color(get_item_name_from_id(found.item), 'cyan' if found.player != ctx.slot else 'green')
found_player = color(ctx.player_names[found.player], 'yellow' if found.player != ctx.slot else 'magenta')
receiving_player = color(ctx.player_names[receiving_player], 'yellow' if receiving_player != ctx.slot else 'magenta')
@ -931,8 +930,8 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
'%s sent %s to %s (%s)' % (found_player, item, receiving_player,
color(get_location_name_from_address(found.location), 'blue_bg', 'white')))
elif cmd == 'ItemFound':
found = ReceivedItem(*args["item"])
elif cmd == 'ItemFound': # going away
found = NetworkItem(*args["item"])
ctx.ui_node.notify_item_found(ctx.player_names[found.player], get_item_name_from_id(found.item),
get_location_name_from_address(found.location), found.player == ctx.slot,
get_item_name_from_id(found.item) in Items.progression_items)
@ -941,7 +940,7 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
logging.info('%s found %s (%s)' % (player_sent, item, color(get_location_name_from_address(found.location),
'blue_bg', 'white')))
elif cmd == 'Hint':
elif cmd == 'Hint': # going away
hints = [Utils.Hint(*hint) for hint in args["hints"]]
for hint in hints:
ctx.ui_node.send_hint(ctx.player_names[hint.finding_player], ctx.player_names[hint.receiving_player],
@ -962,8 +961,10 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
logging.info(text + (f". {color('(found)', 'green_bg', 'black')} " if hint.found else "."))
elif cmd == "RoomUpdate":
if "playernames" in args:
ctx.player_names = {p: n for p, n in args["playernames"]}
if "players" in args:
ctx.consume_players_package(args["players"])
if "hint_points" in args:
ctx.hint_points = args['hint_points']
elif cmd == 'Print':
logger.info(args["text"])
@ -971,9 +972,6 @@ async def process_server_cmd(ctx: Context, cmd: str, args: typing.Optional[dict]
elif cmd == 'PrintJSON':
logger.info(ctx.jsontotextparser(args["data"]))
elif cmd == 'HintPointUpdate':
ctx.hint_points = args['points']
elif cmd == 'InvalidArguments':
logger.warning(f"Invalid Arguments: {args['text']}")
@ -1001,8 +999,8 @@ async def server_auth(ctx: Context, password_requested):
ctx.auth = ctx.rom
auth = base64.b64encode(ctx.rom).decode()
await ctx.send_msgs([['Connect', {
'password': ctx.password, 'rom': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx),
'uuid': Utils.get_unique_identifier(), 'game': "ALTTP"
'password': ctx.password, 'name': auth, 'version': Utils._version_tuple, 'tags': get_tags(ctx),
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
}]])
@ -1247,7 +1245,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
async def send_finished_game(ctx: Context):
try:
await ctx.send_msgs([['StatusUpdate', {"status": CLIENT_GOAL}]])
await ctx.send_msgs([['StatusUpdate', {"status": CLientStatus.CLIENT_GOAL}]])
ctx.finished_game = True
except Exception as ex:
logger.exception(ex)

View File

@ -28,8 +28,8 @@ from fuzzywuzzy import process as fuzzy_process
from worlds.alttp import Items, Regions
import Utils
from Utils import get_item_name_from_id, get_location_name_from_address, \
ReceivedItem, _version_tuple, restricted_loads
from NetUtils import Node, Endpoint, CLIENT_GOAL
_version_tuple, restricted_loads
from NetUtils import Node, Endpoint, CLientStatus, NetworkItem
colorama.init()
console_names = frozenset(set(Items.item_table) | set(Items.item_name_groups) | set(Regions.lookup_name_to_id))
@ -76,7 +76,7 @@ class Context(Node):
self.save_filename = None
self.saving = False
self.player_names = {}
self.rom_names = {}
self.connect_names = {} # names of slots clients can connect to
self.allow_forfeits = {}
self.remote_items = set()
self.locations = {}
@ -140,7 +140,7 @@ class Context(Node):
for player, name in enumerate(names, 1):
self.player_names[(team, player)] = name
self.rom_names = decoded_obj['roms']
self.connect_names = decoded_obj['connect_names']
self.remote_items = decoded_obj['remote_items']
self.locations = decoded_obj['locations']
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
@ -149,6 +149,9 @@ class Context(Node):
server_options = decoded_obj.get("server_options", {})
self._set_options(server_options)
def get_players_package(self):
return [(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)
@ -225,7 +228,7 @@ class Context(Node):
def get_save(self) -> dict:
d = {
"rom_names": list(self.rom_names.items()),
"rom_names": list(self.connect_names.items()),
"received_items": tuple((k, v) for k, v in self.received_items.items()),
"hints_used": tuple((key, value) for key, value in self.hints_used.items()),
"hints": tuple(
@ -246,15 +249,15 @@ class Context(Node):
adjusted = {rom: (team, slot) for rom, (team, slot) in rom_names}
except TypeError:
adjusted = {tuple(rom): (team, slot) for (rom, (team, slot)) in rom_names} # old format, ponyorm friendly
if self.rom_names != adjusted:
if self.connect_names != adjusted:
logging.warning('Save file mismatch, will start a new game')
return
else:
if adjusted != self.rom_names:
if adjusted != self.connect_names:
logging.warning('Save file mismatch, will start a new game')
return
received_items = {tuple(k): [ReceivedItem(*i) for i in v] for k, v in savedata["received_items"]}
received_items = {tuple(k): [NetworkItem(*i) for i in v] for k, v in savedata["received_items"]}
self.received_items = received_items
self.hints_used.update({tuple(key): value for key, value in savedata["hints_used"]})
@ -330,7 +333,7 @@ class Context(Node):
# separated out, due to compatibilty between clients
def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
cmd = dumps([["Hint", {"hints", hints}]])
cmd = dumps([["Hint", {"hints" : hints}]])
texts = [['PrintHTML', format_hint(ctx, team, hint)] for hint in hints]
for _, text in texts:
logging.info("Notice (Team #%d): %s" % (team + 1, text))
@ -341,8 +344,7 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[Utils.Hint]):
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
cmd = dumps([["RoomUpdate",
{"playernames": [(key[1], ctx.get_aliased_name(*key)) for key, value in ctx.player_names.items() if
key[0] == team]}]])
{"players": ctx.get_players_package()}]])
if client is None:
for client in ctx.endpoints:
if client.team == team and client.auth:
@ -367,6 +369,8 @@ async def server(websocket, path, ctx: Context):
await ctx.disconnect(client)
async def on_client_connected(ctx: Context, client: Client):
await ctx.send_msgs(client, [['RoomInfo', {
'password': ctx.password is not None,
@ -419,7 +423,7 @@ async def countdown(ctx: Context, timer):
async def missing(ctx: Context, client: Client, locations: list, checked_locations: list):
await ctx.send_msgs(client, [['Missing', {
'locations': dumps(locations),
'checked_locations': json.dumps(checked_locations)
'checked_locations': dumps(checked_locations)
}]])
@ -441,7 +445,7 @@ def get_players_string(ctx: Context):
return f'{len(auth_clients)} players of {len(ctx.player_names)} connected ' + text[:-1]
def get_received_items(ctx: Context, team: int, player: int) -> typing.List[ReceivedItem]:
def get_received_items(ctx: Context, team: int, player: int) -> typing.List[NetworkItem]:
return ctx.received_items.setdefault((team, player), [])
@ -495,7 +499,7 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
break
if not found:
new_item = ReceivedItem(target_item, location, slot)
new_item = NetworkItem(target_item, location, slot)
recvd_items.append(new_item)
if slot != target_player:
ctx.broadcast_team(team,
@ -511,14 +515,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
if client.team == team and client.wants_item_notification:
asyncio.create_task(
ctx.send_msgs(client, [['ItemFound',
{"item": ReceivedItem(target_item, location, slot)}]]))
{"item": NetworkItem(target_item, location, slot)}]]))
ctx.location_checks[team, slot] |= known_locations
send_new_items(ctx)
if found_items:
for client in ctx.endpoints:
if client.team == team and client.slot == slot:
asyncio.create_task(ctx.send_msgs(client, [["HintPointUpdate", {"points": get_client_points(ctx, client)}]]))
asyncio.create_task(ctx.send_msgs(client, [["RoomUpdate", {"hint_points": get_client_points(ctx, client)}]]))
ctx.save()
@ -672,7 +676,8 @@ class CommandProcessor(metaclass=CommandMeta):
self.output(f"Could not find command {raw}. Known commands: {', '.join(self.commands)}")
def _error_parsing_command(self, exception: Exception):
self.output(str(exception))
import traceback
self.output(traceback.format_exc())
class CommonCommandProcessor(CommandProcessor):
@ -780,7 +785,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
"Sorry, client forfeiting has been disabled on this server. You can ask the server admin for a /forfeit")
return False
else: # is auto or goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == CLIENT_GOAL:
if self.ctx.client_game_state[self.client.team, self.client.slot] == CLientStatus.CLIENT_GOAL:
forfeit_player(self.ctx, self.client.team, self.client.slot)
return True
else:
@ -807,7 +812,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
"Sorry, !remaining has been disabled on this server.")
return False
else: # is goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == CLIENT_GOAL:
if self.ctx.client_game_state[self.client.team, self.client.slot] == CLientStatus.CLIENT_GOAL:
remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot)
if remaining_item_ids:
self.output("Remaining items: " + ", ".join(Items.lookup_id_to_name.get(item_id, "unknown item")
@ -862,7 +867,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if self.ctx.item_cheat:
item_name, usable, response = get_intended_text(item_name, Items.item_table.keys())
if usable:
new_item = ReceivedItem(Items.item_table[item_name][2], -1, self.client.slot)
new_item = NetworkItem(Items.item_table[item_name][2], -1, self.client.slot)
get_received_items(self.ctx, self.client.team, self.client.slot).append(new_item)
self.ctx.notify_all('Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team, self.client.slot))
send_new_items(self.ctx)
@ -993,11 +998,11 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin
if ctx.password and args['password'] != ctx.password:
errors.add('InvalidPassword')
if args['rom'] not in ctx.rom_names:
logging.info((args["rom"], ctx.rom_names))
errors.add('InvalidRom')
if args['name'] not in ctx.connect_names:
logging.info((args["name"], ctx.connect_names))
errors.add('InvalidSlot')
else:
team, slot = ctx.rom_names[args['rom']]
team, slot = ctx.connect_names[args['name']]
# this can only ever be 0 or 1 elements
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
if clients:
@ -1031,14 +1036,14 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin
client.version = args['version']
client.tags = args['tags']
reply = [['Connected', {"team": client.team, "slot": client.slot,
"playernames": [(p, ctx.get_aliased_name(t, p)) for (t, p), n in
ctx.player_names.items() if t == client.team],
"players": ctx.get_players_package(),
"missing_checks": get_missing_checks(ctx, client),
"items_checked": get_checked_checks(ctx, client)}]]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append(['ReceivedItems', {"index": 0, "items": tuplize_received_items(items)}])
client.send_index = len(items)
await ctx.send_msgs(client, reply)
await on_client_joined(ctx, client)
@ -1079,8 +1084,8 @@ async def process_client_cmd(ctx: Context, client: Client, cmd: str, args: typin
elif cmd == 'StatusUpdate':
current = ctx.client_game_state[client.team, client.slot]
if current != CLIENT_GOAL: # can't undo goal completion
if args["status"] == CLIENT_GOAL:
if current != CLientStatus.CLIENT_GOAL: # can't undo goal completion
if args["status"] == CLientStatus.CLIENT_GOAL:
finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.'
ctx.notify_all(finished_msg)
if "auto" in ctx.forfeit_mode:
@ -1218,7 +1223,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
if usable:
for client in self.ctx.endpoints:
if client.name == seeked_player:
new_item = ReceivedItem(Items.item_table[item][2], -1, client.slot)
new_item = NetworkItem(Items.item_table[item][2], -1, client.slot)
get_received_items(self.ctx, client.team, client.slot).append(new_item)
self.ctx.notify_all('Cheat console: sending "' + item + '" to ' + self.ctx.get_aliased_name(client.team, client.slot))
send_new_items(self.ctx)

View File

@ -14,8 +14,8 @@ ModuleUpdate.update()
from Utils import parse_yaml
from worlds.alttp.Rom import Sprite
from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.alttp.Main import main as ERmain
from worlds.alttp.Main import get_seed, seeddigits
from Main import main as ERmain
from Main import get_seed, seeddigits
from worlds.alttp.Items import item_name_groups, item_table
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable
@ -360,6 +360,8 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
if ret.name:
ret.name = handle_name(ret.name)
ret.game = get_choice("game", weights, "A Link to the Past")
glitches_required = get_choice('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG and No Logic supported")

View File

@ -2,10 +2,15 @@ from __future__ import annotations
import asyncio
import logging
import typing
import enum
from json import loads, dumps
import websockets
class JSONMessagePart(typing.TypedDict):
type: typing.Optional[str]
color: typing.Optional[str]
text: typing.Optional[str]
class Node:
endpoints: typing.List
@ -80,32 +85,32 @@ class JSONtoTextParser(metaclass=HandlerMeta):
def __init__(self, ctx: "MultiClient.Context"):
self.ctx = ctx
def __call__(self, input_object: typing.List[dict]) -> str:
def __call__(self, input_object: typing.List[JSONMessagePart]) -> str:
return "".join(self.handle_node(section) for section in input_object)
def handle_node(self, node: dict):
def handle_node(self, node: JSONMessagePart):
type = node.get("type", None)
handler = self.handlers.get(type, self.handlers["text"])
return handler(node)
def _handle_color(self, node: dict):
def _handle_color(self, node: JSONMessagePart):
if node["color"] in color_codes:
return color_code(node["color"]) + self._handle_text(node) + color_code("reset")
else:
logging.warning(f"Unknown color in node {node}")
return self._handle_text(node)
def _handle_text(self, node: dict):
def _handle_text(self, node: JSONMessagePart):
return node.get("text", "")
def _handle_player_id(self, node: dict):
def _handle_player_id(self, node: JSONMessagePart):
player = node["player"]
node["color"] = 'yellow' if player != self.ctx.slot else 'magenta'
node["text"] = self.ctx.player_names[player]
return self._handle_color(node)
# for other teams, spectators etc.? Only useful if player isn't in the clientside mapping
def _handle_player_name(self, node: dict):
def _handle_player_name(self, node: JSONMessagePart):
node["color"] = 'yellow'
return self._handle_color(node)
@ -124,7 +129,21 @@ def color(text, *args):
return color_code(*args) + text + color_code('reset')
CLIENT_UNKNOWN = 0
CLIENT_READY = 10
CLIENT_PLAYING = 20
CLIENT_GOAL = 30
class CLientStatus(enum.IntEnum):
CLIENT_UNKNOWN = 0
# CLIENT_CONNECTED = 5 maybe?
CLIENT_READY = 10
CLIENT_PLAYING = 20
CLIENT_GOAL = 30
class NetworkPlayer(typing.NamedTuple):
team: int
slot: int
alias: str
name: str
class NetworkItem(typing.NamedTuple):
item: int
location: int
player: int

View File

@ -279,13 +279,13 @@ def get_options() -> dict:
def get_item_name_from_id(code):
from worlds.alttp import Items
return Items.lookup_id_to_name.get(code, f'Unknown item (ID:{code})')
from worlds import lookup_any_item_id_to_name
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
def get_location_name_from_address(address):
from worlds.alttp import Regions
return Regions.lookup_id_to_name.get(address, f'Unknown location (ID:{address})')
from worlds import lookup_any_location_id_to_name
return lookup_any_location_id_to_name.get(address, f'Unknown location (ID:{address})')
def persistent_store(category, key, value):
@ -357,12 +357,6 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
return romfile, False
class ReceivedItem(typing.NamedTuple):
item: int
location: int
player: int
def get_unique_identifier():
uuid = persistent_load().get("client", {}).get("uuid", None)
if uuid:
@ -384,8 +378,9 @@ class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
if module == "Utils" and name in {"ReceivedItem"}:
return globals()[name]
if module == "NetUtils" and name in {"NetworkItem"}:
import NetUtils
return getattr(NetUtils, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))

View File

@ -5,8 +5,8 @@ import random
from flask import request, flash, redirect, url_for, session, render_template
from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.alttp.Main import main as ERmain
from worlds.alttp.Main import get_seed, seeddigits
from Main import main as ERmain
from Main import get_seed, seeddigits
import pickle
from .models import *

View File

@ -3,5 +3,5 @@ pony>=0.7.14
waitress>=1.4.4
flask-caching>=1.9.0
Flask-Autoversion>=0.2.0
Flask-Compress>=1.8.0
Flask-Compress>=1.9.0
Flask-Limiter>=1.4

View File

@ -0,0 +1,11 @@
__all__ = {"lookup_any_item_id_to_name",
"lookup_any_location_id_to_name"}
from .alttp.Items import lookup_id_to_name as alttp
from .hk.Items import lookup_id_to_name as hk
lookup_any_item_id_to_name = {**alttp, **hk}
from .alttp import Regions
from .hk import Locations
lookup_any_location_id_to_name = {**Regions.lookup_id_to_name, **Locations.lookup_id_to_name}

View File

@ -1,15 +1,8 @@
#!/usr/bin/env python3
import argparse
import copy
import os
import logging
import textwrap
import shlex
import sys
from worlds.alttp.Main import main, get_seed
from worlds.alttp.Rom import Sprite
from Utils import is_bundled, close_console
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@ -359,6 +352,7 @@ def parse_arguments(argv, no_defaults=False):
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('--create_diff', default=defval(False), action='store_true', help='''\
@ -412,7 +406,7 @@ def parse_arguments(argv, no_defaults=False):
"plando_items", "plando_texts", "plando_connections", "er_seeds",
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
'restrict_dungeon_item_on_boss', 'reduceflashing',
'restrict_dungeon_item_on_boss', 'reduceflashing', 'game',
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:

View File

@ -1,7 +1,8 @@
from collections import namedtuple
import logging
from BaseClasses import Region, RegionType, Location
from BaseClasses import Region, RegionType
from worlds.alttp 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
@ -243,7 +244,7 @@ def generate_itempool(world, player: int):
if world.goal[player] in ['triforcehunt', 'localtriforcehunt']:
region = world.get_region('Light World', player)
loc = Location(player, "Murahdahla", parent=region)
loc = ALttPLocation(player, "Murahdahla", parent=region)
loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player)
region.locations.append(loc)
@ -501,7 +502,7 @@ def create_dynamic_shop_locations(world, player):
if item is None:
continue
if item['create_location']:
loc = Location(player, "{} Slot {}".format(shop.region.name, i + 1), parent=shop.region)
loc = ALttPLocation(player, "{} Slot {}".format(shop.region.name, i + 1), parent=shop.region)
shop.region.locations.append(loc)
world.dynamic_locations.append(loc)
@ -515,7 +516,7 @@ def create_dynamic_shop_locations(world, player):
def fill_prizes(world, attempts=15):
all_state = world.get_all_state(keys=True)
for player in range(1, world.players + 1):
for player in world.alttp_player_ids:
crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player)
crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player),
world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player),

View File

@ -16,7 +16,7 @@ def GetBeemizerItem(world, player, item):
def ItemFactory(items, player):
from BaseClasses import Item
from worlds.alttp import ALttPItem
ret = []
singleton = False
if isinstance(items, str):
@ -24,7 +24,7 @@ def ItemFactory(items, player):
singleton = True
for item in items:
if item in item_table:
ret.append(Item(item, *item_table[item], player))
ret.append(ALttPItem(item, *item_table[item], player))
else:
raise Exception(f"Unknown item {item}")
@ -200,7 +200,7 @@ item_table = {'Bow': (True, None, 0x0B, 'You have\nchosen the\narcher class.', '
'Open Floodgate': (True, 'Event', None, None, None, None, None, None, None, None),
}
lookup_id_to_name = {data[2]: name for name, data in item_table.items()}
lookup_id_to_name = {data[2]: name for name, data in item_table.items() if data[2]}
hint_blacklist = {"Triforce"}

View File

@ -1,8 +1,8 @@
import collections
import typing
from BaseClasses import Region, Location, Entrance, RegionType
from BaseClasses import Region, Entrance, RegionType
from worlds.alttp import ALttPLocation
def create_regions(world, player):
@ -333,7 +333,7 @@ def _create_region(player: int, name: str, type: RegionType, hint: str, location
ret.exits.append(Entrance(player, exit, ret))
for location in locations:
address, player_address, crystal, hint_text = location_table[location]
ret.locations.append(Location(player, location, address, crystal, hint_text, ret, player_address))
ret.locations.append(ALttPLocation(player, location, address, crystal, hint_text, ret, player_address))
return ret

View File

@ -16,7 +16,8 @@ import xxtea
import concurrent.futures
from typing import Optional
from BaseClasses import CollectionState, Region, Location
from BaseClasses import CollectionState, Region
from worlds.alttp import ALttPLocation
from worlds.alttp.Shops import ShopType
from worlds.alttp.Dungeons import dungeon_music_addresses
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
@ -700,18 +701,24 @@ def patch_rom(world, rom, player, team, enemized):
itemid = location.item.code if location.item is not None else 0x5A
if location.item.game != "A Link to the Past":
itemid = itemid
if not location.crystal:
if location.item is not None:
if location.item.game != "A Link to the Past":
itemid = 0x21
# Keys in their native dungeon should use the orignal item code for keys
if location.parent_region.dungeon:
elif location.parent_region.dungeon:
if location.parent_region.dungeon.is_dungeon_item(location.item):
if location.item.bigkey:
itemid = 0x32
if location.item.smallkey:
elif location.item.smallkey:
itemid = 0x24
if location.item.map:
elif location.item.map:
itemid = 0x33
if location.item.compass:
elif location.item.compass:
itemid = 0x25
if world.remote_items[player]:
itemid = list(location_table.keys()).index(location.name) + 1
@ -1572,7 +1579,7 @@ def patch_rom(world, rom, player, team, enemized):
# set rom name
# 21 bytes
from worlds.alttp.Main import __version__
from Main import __version__
# TODO: Adjust Enemizer to accept AP and AD
rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{team + 1}_{player}_{world.seed:09}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name)))
@ -2007,7 +2014,7 @@ def write_strings(rom, world, player, team):
if dest.player != player:
if ped_hint:
hint += f" for {world.player_names[dest.player][team]}!"
elif type(dest) in [Region, Location]:
elif type(dest) in [Region, ALttPLocation]:
hint += f" in {world.player_names[dest.player][team]}'s world"
else:
hint += f" for {world.player_names[dest.player][team]}"

View File

@ -3,7 +3,7 @@ from enum import unique, Enum
from typing import List, Union, Optional, Set, NamedTuple, Dict
import logging
from BaseClasses import Location
from worlds.alttp import ALttPLocation
from worlds.alttp.EntranceShuffle import door_addresses
from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem
from Utils import int16_as_bytes
@ -130,8 +130,8 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
def FillDisabledShopSlots(world):
shop_slots: Set[Location] = {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}
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_slots:
location.shop_slot_disabled = True
slot_num = int(location.name[-1]) - 1
@ -141,8 +141,8 @@ def FillDisabledShopSlots(world):
def ShopSlotFill(world):
shop_slots: Set[Location] = {location for shop_locations in (shop.region.locations for shop in world.shops)
for location in shop_locations if location.shop_slot}
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}
removed = set()
for location in shop_slots:
slot_num = int(location.name[-1]) - 1
@ -282,8 +282,8 @@ def create_shops(world, player: int):
shop.add_inventory(index, *item)
if not locked and num_slots:
slot_name = "{} Slot {}".format(region.name, index + 1)
loc = Location(player, slot_name, address=shop_table_by_location[slot_name],
parent=region, hint_text="for sale")
loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name],
parent=region, hint_text="for sale")
loc.shop_slot = True
loc.locked = True
if single_purchase_slots.pop():

View File

@ -1,110 +1,141 @@
from typing import Optional
from BaseClasses import Location, Item
from worlds.generic import World
class ALTTPWorld(World):
"""WIP"""
def __init__(self, options, slot: int):
self._region_cache = {}
self.slot = slot
self.shuffle = shuffle
self.logic = logic
self.mode = mode
self.swords = swords
self.difficulty = difficulty
self.difficulty_adjustments = difficulty_adjustments
self.timer = timer
self.progressive = progressive
self.goal = goal
self.dungeons = []
self.regions = []
self.shops = []
self.itempool = []
self.seed = None
self.precollected_items = []
self.state = CollectionState(self)
self._cached_entrances = None
self._cached_locations = None
self._entrance_cache = {}
self._location_cache = {}
self.required_locations = []
self.light_world_light_cone = False
self.dark_world_light_cone = False
self.rupoor_cost = 10
self.aga_randomness = True
self.lock_aga_door_in_escape = False
self.save_and_quit_from_boss = True
self.accessibility = accessibility
self.shuffle_ganon = shuffle_ganon
self.fix_gtower_exit = self.shuffle_ganon
self.retro = retro
self.custom = custom
self.customitemarray: List[int] = customitemarray
self.hints = hints
self.dynamic_regions = []
self.dynamic_locations = []
#class ALTTPWorld(World):
# """WIP"""
# def __init__(self, options, slot: int):
# self._region_cache = {}
# self.slot = slot
# self.shuffle = shuffle
# self.logic = logic
# self.mode = mode
# self.swords = swords
# self.difficulty = difficulty
# self.difficulty_adjustments = difficulty_adjustments
# self.timer = timer
# self.progressive = progressive
# self.goal = goal
# self.dungeons = []
# self.regions = []
# self.shops = []
# self.itempool = []
# self.seed = None
# self.precollected_items = []
# self.state = CollectionState(self)
# self._cached_entrances = None
# self._cached_locations = None
# self._entrance_cache = {}
# self._location_cache = {}
# self.required_locations = []
# self.light_world_light_cone = False
# self.dark_world_light_cone = False
# self.rupoor_cost = 10
# self.aga_randomness = True
# self.lock_aga_door_in_escape = False
# self.save_and_quit_from_boss = True
# self.accessibility = accessibility
# self.shuffle_ganon = shuffle_ganon
# self.fix_gtower_exit = self.shuffle_ganon
# self.retro = retro
# self.custom = custom
# self.customitemarray: List[int] = customitemarray
# self.hints = hints
# self.dynamic_regions = []
# self.dynamic_locations = []
#
#
# self.remote_items = False
# self.required_medallions = ['Ether', 'Quake']
# self.swamp_patch_required = False
# self.powder_patch_required = False
# self.ganon_at_pyramid = True
# self.ganonstower_vanilla = True
#
#
# self.can_access_trock_eyebridge = None
# self.can_access_trock_front = None
# self.can_access_trock_big_chest = None
# self.can_access_trock_middle = None
# self.fix_fake_world = True
# self.mapshuffle = False
# self.compassshuffle = False
# self.keyshuffle = False
# self.bigkeyshuffle = False
# self.difficulty_requirements = None
# self.boss_shuffle = 'none'
# self.enemy_shuffle = False
# self.enemy_health = 'default'
# self.enemy_damage = 'default'
# self.killable_thieves = False
# self.tile_shuffle = False
# self.bush_shuffle = False
# self.beemizer = 0
# self.escape_assist = []
# self.crystals_needed_for_ganon = 7
# self.crystals_needed_for_gt = 7
# self.open_pyramid = False
# self.treasure_hunt_icon = 'Triforce Piece'
# self.treasure_hunt_count = 0
# self.clock_mode = False
# self.can_take_damage = True
# self.glitch_boots = True
# self.progression_balancing = True
# self.local_items = set()
# self.triforce_pieces_available = 30
# self.triforce_pieces_required = 20
# self.shop_shuffle = 'off'
# self.shuffle_prizes = "g"
# self.sprite_pool = []
# self.dark_room_logic = "lamp"
# self.restrict_dungeon_item_on_boss = False
#
# @property
# def sewer_light_cone(self):
# return self.mode == "standard"
#
# @property
# def fix_trock_doors(self):
# return self.shuffle != 'vanilla' or self.mode == 'inverted'
#
# @property
# def fix_skullwoods_exit(self):
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
#
# @property
# def fix_palaceofdarkness_exit(self):
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
#
# @property
# def fix_trock_exit(self):
# return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
self.remote_items = False
self.required_medallions = ['Ether', 'Quake']
self.swamp_patch_required = False
self.powder_patch_required = False
self.ganon_at_pyramid = True
self.ganonstower_vanilla = True
class ALttPLocation(Location):
game: str = "A Link to the Past"
def __init__(self, player: int, name: str = '', address=None, crystal: bool = False,
hint_text: Optional[str] = None, parent=None,
player_address=None):
super(ALttPLocation, self).__init__(player, name, address, parent)
self.crystal = crystal
self.player_address = player_address
self._hint_text: str = hint_text
self.can_access_trock_eyebridge = None
self.can_access_trock_front = None
self.can_access_trock_big_chest = None
self.can_access_trock_middle = None
self.fix_fake_world = True
self.mapshuffle = False
self.compassshuffle = False
self.keyshuffle = False
self.bigkeyshuffle = False
self.difficulty_requirements = None
self.boss_shuffle = 'none'
self.enemy_shuffle = False
self.enemy_health = 'default'
self.enemy_damage = 'default'
self.killable_thieves = False
self.tile_shuffle = False
self.bush_shuffle = False
self.beemizer = 0
self.escape_assist = []
self.crystals_needed_for_ganon = 7
self.crystals_needed_for_gt = 7
self.open_pyramid = False
self.treasure_hunt_icon = 'Triforce Piece'
self.treasure_hunt_count = 0
self.clock_mode = False
self.can_take_damage = True
self.glitch_boots = True
self.progression_balancing = True
self.local_items = set()
self.triforce_pieces_available = 30
self.triforce_pieces_required = 20
self.shop_shuffle = 'off'
self.shuffle_prizes = "g"
self.sprite_pool = []
self.dark_room_logic = "lamp"
self.restrict_dungeon_item_on_boss = False
class ALttPItem(Item):
@property
def sewer_light_cone(self):
return self.mode == "standard"
game: str = "A Link to the Past"
@property
def fix_trock_doors(self):
return self.shuffle != 'vanilla' or self.mode == 'inverted'
@property
def fix_skullwoods_exit(self):
return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
@property
def fix_palaceofdarkness_exit(self):
return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
@property
def fix_trock_exit(self):
return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
def __init__(self, name='', advancement=False, type=None, code=None, pedestal_hint=None, pedestal_credit=None, sickkid_credit=None, zora_credit=None, witch_credit=None, fluteboy_credit=None, hint_text=None, player=None):
super(ALttPItem, self).__init__(name, advancement, code, player)
self.type = type
self._pedestal_hint_text = pedestal_hint
self.pedestal_credit_text = pedestal_credit
self.sickkid_credit_text = sickkid_credit
self.zora_credit_text = zora_credit
self.magicshop_credit_text = witch_credit
self.fluteboy_credit_text = fluteboy_credit
self._hint_text = hint_text

325
worlds/hk/Items.py Normal file
View File

@ -0,0 +1,325 @@
items = \
{ 16777217: {'advancement': True, 'name': 'Lurien'},
16777218: {'advancement': True, 'name': 'Monomon'},
16777219: {'advancement': True, 'name': 'Herrah'},
16777220: {'advancement': False, 'name': 'World_Sense'},
16777221: {'advancement': True, 'name': 'Dreamer'},
16777222: {'advancement': True, 'name': 'Mothwing_Cloak'},
16777223: {'advancement': True, 'name': 'Mantis_Claw'},
16777224: {'advancement': True, 'name': 'Crystal_Heart'},
16777225: {'advancement': True, 'name': 'Monarch_Wings'},
16777226: {'advancement': True, 'name': 'Shade_Cloak'},
16777227: {'advancement': True, 'name': "Isma's_Tear"},
16777228: {'advancement': True, 'name': 'Dream_Nail'},
16777229: {'advancement': True, 'name': 'Dream_Gate'},
16777230: {'advancement': True, 'name': 'Awoken_Dream_Nail'},
16777231: {'advancement': True, 'name': 'Vengeful_Spirit'},
16777232: {'advancement': True, 'name': 'Shade_Soul'},
16777233: {'advancement': True, 'name': 'Desolate_Dive'},
16777234: {'advancement': True, 'name': 'Descending_Dark'},
16777235: {'advancement': True, 'name': 'Howling_Wraiths'},
16777236: {'advancement': True, 'name': 'Abyss_Shriek'},
16777237: {'advancement': True, 'name': 'Cyclone_Slash'},
16777238: {'advancement': True, 'name': 'Dash_Slash'},
16777239: {'advancement': True, 'name': 'Great_Slash'},
16777240: {'advancement': True, 'name': 'Focus'},
16777241: {'advancement': False, 'name': 'Gathering_Swarm'},
16777242: {'advancement': False, 'name': 'Wayward_Compass'},
16777243: {'advancement': False, 'name': 'Grubsong'},
16777244: {'advancement': False, 'name': 'Stalwart_Shell'},
16777245: {'advancement': False, 'name': 'Baldur_Shell'},
16777246: {'advancement': False, 'name': 'Fury_of_the_Fallen'},
16777247: {'advancement': False, 'name': 'Quick_Focus'},
16777248: {'advancement': True, 'name': 'Lifeblood_Heart'},
16777249: {'advancement': True, 'name': 'Lifeblood_Core'},
16777250: {'advancement': False, 'name': "Defender's_Crest"},
16777251: {'advancement': False, 'name': 'Flukenest'},
16777252: {'advancement': False, 'name': 'Thorns_of_Agony'},
16777253: {'advancement': True, 'name': 'Mark_of_Pride'},
16777254: {'advancement': False, 'name': 'Steady_Body'},
16777255: {'advancement': False, 'name': 'Heavy_Blow'},
16777256: {'advancement': True, 'name': 'Sharp_Shadow'},
16777257: {'advancement': True, 'name': 'Spore_Shroom'},
16777258: {'advancement': False, 'name': 'Longnail'},
16777259: {'advancement': False, 'name': 'Shaman_Stone'},
16777260: {'advancement': False, 'name': 'Soul_Catcher'},
16777261: {'advancement': False, 'name': 'Soul_Eater'},
16777262: {'advancement': True, 'name': 'Glowing_Womb'},
16777263: {'advancement': False, 'name': 'Fragile_Heart'},
16777264: {'advancement': False, 'name': 'Fragile_Greed'},
16777265: {'advancement': False, 'name': 'Fragile_Strength'},
16777266: {'advancement': False, 'name': "Nailmaster's_Glory"},
16777267: {'advancement': True, 'name': "Joni's_Blessing"},
16777268: {'advancement': False, 'name': 'Shape_of_Unn'},
16777269: {'advancement': False, 'name': 'Hiveblood'},
16777270: {'advancement': False, 'name': 'Dream_Wielder'},
16777271: {'advancement': True, 'name': 'Dashmaster'},
16777272: {'advancement': False, 'name': 'Quick_Slash'},
16777273: {'advancement': False, 'name': 'Spell_Twister'},
16777274: {'advancement': False, 'name': 'Deep_Focus'},
16777275: {'advancement': True, 'name': "Grubberfly's_Elegy"},
16777276: {'advancement': True, 'name': 'Queen_Fragment'},
16777277: {'advancement': True, 'name': 'King_Fragment'},
16777278: {'advancement': True, 'name': 'Void_Heart'},
16777279: {'advancement': True, 'name': 'Sprintmaster'},
16777280: {'advancement': False, 'name': 'Dreamshield'},
16777281: {'advancement': True, 'name': 'Weaversong'},
16777282: {'advancement': True, 'name': 'Grimmchild'},
16777283: {'advancement': True, 'name': 'City_Crest'},
16777284: {'advancement': True, 'name': 'Lumafly_Lantern'},
16777285: {'advancement': True, 'name': 'Tram_Pass'},
16777286: {'advancement': True, 'name': 'Simple_Key-Sly'},
16777287: {'advancement': True, 'name': 'Simple_Key-Basin'},
16777288: {'advancement': True, 'name': 'Simple_Key-City'},
16777289: {'advancement': True, 'name': 'Simple_Key-Lurker'},
16777290: {'advancement': True, 'name': "Shopkeeper's_Key"},
16777291: {'advancement': True, 'name': 'Elegant_Key'},
16777292: {'advancement': True, 'name': 'Love_Key'},
16777293: {'advancement': True, 'name': "King's_Brand"},
16777294: {'advancement': False, 'name': 'Godtuner'},
16777295: {'advancement': False, 'name': "Collector's_Map"},
16777296: {'advancement': False, 'name': 'Mask_Shard-Sly1'},
16777297: {'advancement': False, 'name': 'Mask_Shard-Sly2'},
16777298: {'advancement': False, 'name': 'Mask_Shard-Sly3'},
16777299: {'advancement': False, 'name': 'Mask_Shard-Sly4'},
16777300: {'advancement': False, 'name': 'Mask_Shard-Seer'},
16777301: {'advancement': False, 'name': 'Mask_Shard-5_Grubs'},
16777302: {'advancement': False, 'name': 'Mask_Shard-Brooding_Mawlek'},
16777303: {'advancement': False, 'name': 'Mask_Shard-Crossroads_Goam'},
16777304: {'advancement': False, 'name': 'Mask_Shard-Stone_Sanctuary'},
16777305: {'advancement': False, 'name': "Mask_Shard-Queen's_Station"},
16777306: {'advancement': False, 'name': 'Mask_Shard-Deepnest'},
16777307: {'advancement': False, 'name': 'Mask_Shard-Waterways'},
16777308: {'advancement': False, 'name': 'Mask_Shard-Enraged_Guardian'},
16777309: {'advancement': False, 'name': 'Mask_Shard-Hive'},
16777310: {'advancement': False, 'name': 'Mask_Shard-Grey_Mourner'},
16777311: {'advancement': False, 'name': 'Mask_Shard-Bretta'},
16777312: {'advancement': False, 'name': 'Vessel_Fragment-Sly1'},
16777313: {'advancement': False, 'name': 'Vessel_Fragment-Sly2'},
16777314: {'advancement': False, 'name': 'Vessel_Fragment-Seer'},
16777315: {'advancement': False, 'name': 'Vessel_Fragment-Greenpath'},
16777316: {'advancement': False, 'name': 'Vessel_Fragment-City'},
16777317: {'advancement': False, 'name': 'Vessel_Fragment-Crossroads'},
16777318: {'advancement': False, 'name': 'Vessel_Fragment-Basin'},
16777319: {'advancement': False, 'name': 'Vessel_Fragment-Deepnest'},
16777320: {'advancement': False, 'name': 'Vessel_Fragment-Stag_Nest'},
16777321: {'advancement': False, 'name': 'Charm_Notch-Shrumal_Ogres'},
16777322: {'advancement': False, 'name': 'Charm_Notch-Fog_Canyon'},
16777323: {'advancement': False, 'name': 'Charm_Notch-Colosseum'},
16777324: {'advancement': False, 'name': 'Charm_Notch-Grimm'},
16777325: {'advancement': False, 'name': 'Pale_Ore-Basin'},
16777326: {'advancement': False, 'name': 'Pale_Ore-Crystal_Peak'},
16777327: {'advancement': False, 'name': 'Pale_Ore-Nosk'},
16777328: {'advancement': False, 'name': 'Pale_Ore-Seer'},
16777329: {'advancement': False, 'name': 'Pale_Ore-Grubs'},
16777330: {'advancement': False, 'name': 'Pale_Ore-Colosseum'},
16777331: {'advancement': False, 'name': '200_Geo-False_Knight_Chest'},
16777332: {'advancement': False, 'name': '380_Geo-Soul_Master_Chest'},
16777333: {'advancement': False, 'name': '655_Geo-Watcher_Knights_Chest'},
16777334: {'advancement': False, 'name': '85_Geo-Greenpath_Chest'},
16777335: {'advancement': False, 'name': '620_Geo-Mantis_Lords_Chest'},
16777336: {'advancement': False, 'name': '150_Geo-Resting_Grounds_Chest'},
16777337: {'advancement': False, 'name': '80_Geo-Crystal_Peak_Chest'},
16777338: {'advancement': False, 'name': '160_Geo-Weavers_Den_Chest'},
16777339: {'advancement': False, 'name': '1_Geo'},
16777340: {'advancement': False, 'name': 'Rancid_Egg-Sly'},
16777341: {'advancement': False, 'name': 'Rancid_Egg-Grubs'},
16777342: {'advancement': False, 'name': 'Rancid_Egg-Sheo'},
16777343: {'advancement': False, 'name': 'Rancid_Egg-Fungal_Core'},
16777344: {'advancement': False, 'name': "Rancid_Egg-Queen's_Gardens"},
16777345: {'advancement': False, 'name': 'Rancid_Egg-Blue_Lake'},
16777346: { 'advancement': False,
'name': 'Rancid_Egg-Crystal_Peak_Dive_Entrance'},
16777347: { 'advancement': False,
'name': 'Rancid_Egg-Crystal_Peak_Dark_Room'},
16777348: { 'advancement': False,
'name': 'Rancid_Egg-Crystal_Peak_Tall_Room'},
16777349: {'advancement': False, 'name': 'Rancid_Egg-City_of_Tears_Left'},
16777350: { 'advancement': False,
'name': 'Rancid_Egg-City_of_Tears_Pleasure_House'},
16777351: {'advancement': False, 'name': "Rancid_Egg-Beast's_Den"},
16777352: {'advancement': False, 'name': 'Rancid_Egg-Dark_Deepnest'},
16777353: {'advancement': False, 'name': "Rancid_Egg-Weaver's_Den"},
16777354: {'advancement': False, 'name': 'Rancid_Egg-Near_Quick_Slash'},
16777355: {'advancement': False, 'name': "Rancid_Egg-Upper_Kingdom's_Edge"},
16777356: {'advancement': False, 'name': 'Rancid_Egg-Waterways_East'},
16777357: {'advancement': False, 'name': 'Rancid_Egg-Waterways_Main'},
16777358: { 'advancement': False,
'name': 'Rancid_Egg-Waterways_West_Bluggsac'},
16777359: { 'advancement': False,
'name': 'Rancid_Egg-Waterways_West_Pickup'},
16777360: {'advancement': False, 'name': "Wanderer's_Journal-Cliffs"},
16777361: { 'advancement': False,
'name': "Wanderer's_Journal-Greenpath_Stag"},
16777362: { 'advancement': False,
'name': "Wanderer's_Journal-Greenpath_Lower"},
16777363: { 'advancement': False,
'name': "Wanderer's_Journal-Fungal_Wastes_Thorns_Gauntlet"},
16777364: { 'advancement': False,
'name': "Wanderer's_Journal-Above_Mantis_Village"},
16777365: { 'advancement': False,
'name': "Wanderer's_Journal-Crystal_Peak_Crawlers"},
16777366: { 'advancement': False,
'name': "Wanderer's_Journal-Resting_Grounds_Catacombs"},
16777367: { 'advancement': False,
'name': "Wanderer's_Journal-King's_Station"},
16777368: { 'advancement': False,
'name': "Wanderer's_Journal-Pleasure_House"},
16777369: { 'advancement': False,
'name': "Wanderer's_Journal-City_Storerooms"},
16777370: { 'advancement': False,
'name': "Wanderer's_Journal-Ancient_Basin"},
16777371: { 'advancement': False,
'name': "Wanderer's_Journal-Kingdom's_Edge_Entrance"},
16777372: { 'advancement': False,
'name': "Wanderer's_Journal-Kingdom's_Edge_Camp"},
16777373: { 'advancement': False,
'name': "Wanderer's_Journal-Kingdom's_Edge_Requires_Dive"},
16777374: {'advancement': False, 'name': 'Hallownest_Seal-Crossroads_Well'},
16777375: {'advancement': False, 'name': 'Hallownest_Seal-Grubs'},
16777376: {'advancement': False, 'name': 'Hallownest_Seal-Greenpath'},
16777377: {'advancement': False, 'name': 'Hallownest_Seal-Fog_Canyon_West'},
16777378: {'advancement': False, 'name': 'Hallownest_Seal-Fog_Canyon_East'},
16777379: {'advancement': False, 'name': "Hallownest_Seal-Queen's_Station"},
16777380: { 'advancement': False,
'name': 'Hallownest_Seal-Fungal_Wastes_Sporgs'},
16777381: {'advancement': False, 'name': 'Hallownest_Seal-Mantis_Lords'},
16777382: {'advancement': False, 'name': 'Hallownest_Seal-Seer'},
16777383: { 'advancement': False,
'name': 'Hallownest_Seal-Resting_Grounds_Catacombs'},
16777384: {'advancement': False, 'name': "Hallownest_Seal-King's_Station"},
16777385: {'advancement': False, 'name': 'Hallownest_Seal-City_Rafters'},
16777386: {'advancement': False, 'name': 'Hallownest_Seal-Soul_Sanctum'},
16777387: {'advancement': False, 'name': 'Hallownest_Seal-Watcher_Knight'},
16777388: { 'advancement': False,
'name': 'Hallownest_Seal-Deepnest_By_Mantis_Lords'},
16777389: {'advancement': False, 'name': "Hallownest_Seal-Beast's_Den"},
16777390: {'advancement': False, 'name': "Hallownest_Seal-Queen's_Gardens"},
16777391: {'advancement': False, 'name': "King's_Idol-Grubs"},
16777392: {'advancement': False, 'name': "King's_Idol-Cliffs"},
16777393: {'advancement': False, 'name': "King's_Idol-Crystal_Peak"},
16777394: {'advancement': False, 'name': "King's_Idol-Glade_of_Hope"},
16777395: {'advancement': False, 'name': "King's_Idol-Dung_Defender"},
16777396: {'advancement': False, 'name': "King's_Idol-Great_Hopper"},
16777397: {'advancement': False, 'name': "King's_Idol-Pale_Lurker"},
16777398: {'advancement': False, 'name': "King's_Idol-Deepnest"},
16777399: {'advancement': False, 'name': 'Arcane_Egg-Seer'},
16777400: {'advancement': False, 'name': 'Arcane_Egg-Lifeblood_Core'},
16777401: {'advancement': False, 'name': 'Arcane_Egg-Shade_Cloak'},
16777402: {'advancement': False, 'name': 'Arcane_Egg-Birthplace'},
16777403: {'advancement': True, 'name': 'Whispering_Root-Crossroads'},
16777404: {'advancement': True, 'name': 'Whispering_Root-Greenpath'},
16777405: {'advancement': True, 'name': 'Whispering_Root-Leg_Eater'},
16777406: {'advancement': True, 'name': 'Whispering_Root-Mantis_Village'},
16777407: {'advancement': True, 'name': 'Whispering_Root-Deepnest'},
16777408: {'advancement': True, 'name': 'Whispering_Root-Queens_Gardens'},
16777409: {'advancement': True, 'name': 'Whispering_Root-Kingdoms_Edge'},
16777410: {'advancement': True, 'name': 'Whispering_Root-Waterways'},
16777411: {'advancement': True, 'name': 'Whispering_Root-City'},
16777412: {'advancement': True, 'name': 'Whispering_Root-Resting_Grounds'},
16777413: {'advancement': True, 'name': 'Whispering_Root-Spirits_Glade'},
16777414: {'advancement': True, 'name': 'Whispering_Root-Crystal_Peak'},
16777415: {'advancement': True, 'name': 'Whispering_Root-Howling_Cliffs'},
16777416: {'advancement': True, 'name': 'Whispering_Root-Ancestral_Mound'},
16777417: {'advancement': True, 'name': 'Whispering_Root-Hive'},
16777418: {'advancement': True, 'name': 'Boss_Essence-Elder_Hu'},
16777419: {'advancement': True, 'name': 'Boss_Essence-Xero'},
16777420: {'advancement': True, 'name': 'Boss_Essence-Gorb'},
16777421: {'advancement': True, 'name': 'Boss_Essence-Marmu'},
16777422: {'advancement': True, 'name': 'Boss_Essence-No_Eyes'},
16777423: {'advancement': True, 'name': 'Boss_Essence-Galien'},
16777424: {'advancement': True, 'name': 'Boss_Essence-Markoth'},
16777425: {'advancement': True, 'name': 'Boss_Essence-Failed_Champion'},
16777426: {'advancement': True, 'name': 'Boss_Essence-Soul_Tyrant'},
16777427: {'advancement': True, 'name': 'Boss_Essence-Lost_Kin'},
16777428: {'advancement': True, 'name': 'Boss_Essence-White_Defender'},
16777429: {'advancement': True, 'name': 'Boss_Essence-Grey_Prince_Zote'},
16777430: {'advancement': True, 'name': 'Grub-Crossroads_Acid'},
16777431: {'advancement': True, 'name': 'Grub-Crossroads_Center'},
16777432: {'advancement': True, 'name': 'Grub-Crossroads_Stag'},
16777433: {'advancement': True, 'name': 'Grub-Crossroads_Spike'},
16777434: {'advancement': True, 'name': 'Grub-Crossroads_Guarded'},
16777435: {'advancement': True, 'name': 'Grub-Greenpath_Cornifer'},
16777436: {'advancement': True, 'name': 'Grub-Greenpath_Journal'},
16777437: {'advancement': True, 'name': 'Grub-Greenpath_MMC'},
16777438: {'advancement': True, 'name': 'Grub-Greenpath_Stag'},
16777439: {'advancement': True, 'name': 'Grub-Fog_Canyon'},
16777440: {'advancement': True, 'name': 'Grub-Fungal_Bouncy'},
16777441: {'advancement': True, 'name': 'Grub-Fungal_Spore_Shroom'},
16777442: {'advancement': True, 'name': 'Grub-Deepnest_Mimic'},
16777443: {'advancement': True, 'name': 'Grub-Deepnest_Nosk'},
16777444: {'advancement': True, 'name': 'Grub-Deepnest_Spike'},
16777445: {'advancement': True, 'name': 'Grub-Dark_Deepnest'},
16777446: {'advancement': True, 'name': "Grub-Beast's_Den"},
16777447: {'advancement': True, 'name': "Grub-Kingdom's_Edge_Oro"},
16777448: {'advancement': True, 'name': "Grub-Kingdom's_Edge_Camp"},
16777449: {'advancement': True, 'name': 'Grub-Hive_External'},
16777450: {'advancement': True, 'name': 'Grub-Hive_Internal'},
16777451: {'advancement': True, 'name': 'Grub-Basin_Requires_Wings'},
16777452: {'advancement': True, 'name': 'Grub-Basin_Requires_Dive'},
16777453: {'advancement': True, 'name': 'Grub-Waterways_Main'},
16777454: {'advancement': True, 'name': 'Grub-Waterways_East'},
16777455: {'advancement': True, 'name': 'Grub-Waterways_Requires_Tram'},
16777456: {'advancement': True, 'name': 'Grub-City_of_Tears_Left'},
16777457: {'advancement': True, 'name': 'Grub-Soul_Sanctum'},
16777458: {'advancement': True, 'name': "Grub-Watcher's_Spire"},
16777459: {'advancement': True, 'name': 'Grub-City_of_Tears_Guarded'},
16777460: {'advancement': True, 'name': "Grub-King's_Station"},
16777461: {'advancement': True, 'name': 'Grub-Resting_Grounds'},
16777462: {'advancement': True, 'name': 'Grub-Crystal_Peak_Below_Chest'},
16777463: {'advancement': True, 'name': 'Grub-Crystallized_Mound'},
16777464: {'advancement': True, 'name': 'Grub-Crystal_Peak_Spike'},
16777465: {'advancement': True, 'name': 'Grub-Crystal_Peak_Mimic'},
16777466: {'advancement': True, 'name': 'Grub-Crystal_Peak_Crushers'},
16777467: {'advancement': True, 'name': 'Grub-Crystal_Heart'},
16777468: {'advancement': True, 'name': 'Grub-Hallownest_Crown'},
16777469: {'advancement': True, 'name': 'Grub-Howling_Cliffs'},
16777470: {'advancement': True, 'name': "Grub-Queen's_Gardens_Stag"},
16777471: {'advancement': True, 'name': "Grub-Queen's_Gardens_Marmu"},
16777472: {'advancement': True, 'name': "Grub-Queen's_Gardens_Top"},
16777473: {'advancement': True, 'name': 'Grub-Collector_1'},
16777474: {'advancement': True, 'name': 'Grub-Collector_2'},
16777475: {'advancement': True, 'name': 'Grub-Collector_3'},
16777476: {'advancement': False, 'name': 'Crossroads_Map'},
16777477: {'advancement': False, 'name': 'Greenpath_Map'},
16777478: {'advancement': False, 'name': 'Fog_Canyon_Map'},
16777479: {'advancement': False, 'name': 'Fungal_Wastes_Map'},
16777480: {'advancement': False, 'name': 'Deepnest_Map-Upper'},
16777481: { 'advancement': False,
'name': 'Deepnest_Map-Right_[Gives_Quill]'},
16777482: {'advancement': False, 'name': 'Ancient_Basin_Map'},
16777483: {'advancement': False, 'name': "Kingdom's_Edge_Map"},
16777484: {'advancement': False, 'name': 'City_of_Tears_Map'},
16777485: {'advancement': False, 'name': 'Royal_Waterways_Map'},
16777486: {'advancement': False, 'name': 'Howling_Cliffs_Map'},
16777487: {'advancement': False, 'name': 'Crystal_Peak_Map'},
16777488: {'advancement': False, 'name': "Queen's_Gardens_Map"},
16777489: {'advancement': False, 'name': 'Resting_Grounds_Map'},
16777490: {'advancement': True, 'name': 'Dirtmouth_Stag'},
16777491: {'advancement': True, 'name': 'Crossroads_Stag'},
16777492: {'advancement': True, 'name': 'Greenpath_Stag'},
16777493: {'advancement': True, 'name': "Queen's_Station_Stag"},
16777494: {'advancement': True, 'name': "Queen's_Gardens_Stag"},
16777495: {'advancement': True, 'name': 'City_Storerooms_Stag'},
16777496: {'advancement': True, 'name': "King's_Station_Stag"},
16777497: {'advancement': True, 'name': 'Resting_Grounds_Stag'},
16777498: {'advancement': True, 'name': 'Distant_Village_Stag'},
16777499: {'advancement': True, 'name': 'Hidden_Station_Stag'},
16777500: {'advancement': True, 'name': 'Stag_Nest_Stag'},
16777501: {'advancement': False, 'name': "Lifeblood_Cocoon-King's_Pass"},
16777502: { 'advancement': False,
'name': 'Lifeblood_Cocoon-Ancestral_Mound'},
16777503: {'advancement': False, 'name': 'Lifeblood_Cocoon-Greenpath'},
16777504: { 'advancement': False,
'name': 'Lifeblood_Cocoon-Fog_Canyon_West'},
16777505: {'advancement': False, 'name': 'Lifeblood_Cocoon-Mantis_Village'},
16777506: {'advancement': False, 'name': 'Lifeblood_Cocoon-Failed_Tramway'},
16777507: {'advancement': False, 'name': 'Lifeblood_Cocoon-Galien'},
16777508: {'advancement': False, 'name': "Lifeblood_Cocoon-Kingdom's_Edge"},
16777509: {'advancement': False, 'name': 'Grubfather'},
16777510: {'advancement': False, 'name': 'Seer'},
16777511: {'advancement': False, 'name': 'Equipped'},
16777512: {'advancement': False, 'name': 'Placeholder'}}
item_table = {data["name"]: item_id for item_id, data in items.items()}
lookup_id_to_name = {item_id: data["name"] for item_id, data in items.items()}

1018
worlds/hk/Locations.py Normal file

File diff suppressed because it is too large Load Diff

63
worlds/hk/__init__.py Normal file
View File

@ -0,0 +1,63 @@
import logging
logger = logging.getLogger("Hollow Knight")
from .Locations import locations, lookup_name_to_id
from .Items import items
from BaseClasses import Region, Entrance, Location, MultiWorld, Item
class HKLocation(Location):
game: str = "Hollow Knight"
def __init__(self, player: int, name: str, address=None, parent=None):
super(HKLocation, self).__init__(player, name, address, parent)
class HKItem(Item):
def __init__(self, name, advancement, code, player: int = None):
super(HKItem, self).__init__(name, advancement, code, player)
def gen_hollow(world: MultiWorld, player: int):
logger.info("Doing buggy things.")
gen_regions(world, player)
link_regions(world, player)
gen_items(world, player)
world.clear_location_cache()
world.clear_entrance_cache()
def gen_regions(world: MultiWorld, player: int):
world.regions += [
create_region(world, player, 'Menu', None, ['Hollow Nest S&Q']),
create_region(world, player, 'Hollow Nest', [location["name"] for location in locations.values()])
]
def link_regions(world: MultiWorld, player: int):
world.get_entrance('Hollow Nest S&Q', player).connect(world.get_region('Hollow Nest', player))
def gen_items(world: MultiWorld, player: int):
pool = []
for item_id, item_data in items.items():
name = item_data["name"]
item = HKItem(name, item_data["advancement"], item_id, player=player)
item.game = "Hollow Knight"
pool.append(item)
world.itempool += pool
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 = lookup_name_to_id[location]
location = HKLocation(player, location, loc_id, ret)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret