From ff9b24e88e83955f9693ae2eaf41afa880fc97cc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Feb 2021 20:17:24 +0100 Subject: [PATCH] Hollow Knight integration (prototype status) --- BaseClasses.py | 215 +++--- Gui.py | 2 +- worlds/alttp/Main.py => Main.py | 88 ++- MultiClient.py | 66 +- MultiServer.py | 63 +- Mystery.py | 6 +- NetUtils.py | 39 +- Utils.py | 19 +- WebHostLib/generate.py | 4 +- WebHostLib/requirements.txt | 2 +- worlds/__init__.py | 11 + worlds/alttp/EntranceRandomizer.py | 10 +- worlds/alttp/ItemPool.py | 9 +- worlds/alttp/Items.py | 6 +- worlds/alttp/Regions.py | 6 +- worlds/alttp/Rom.py | 21 +- worlds/alttp/Shops.py | 14 +- worlds/alttp/__init__.py | 233 ++++--- worlds/hk/Items.py | 325 +++++++++ worlds/hk/Locations.py | 1018 ++++++++++++++++++++++++++++ worlds/hk/__init__.py | 63 ++ 21 files changed, 1869 insertions(+), 351 deletions(-) rename worlds/alttp/Main.py => Main.py (93%) create mode 100644 worlds/hk/Items.py create mode 100644 worlds/hk/Locations.py create mode 100644 worlds/hk/__init__.py diff --git a/BaseClasses.py b/BaseClasses.py index 4f8caf31..8fd3c4ee 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -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"])}: ' diff --git a/Gui.py b/Gui.py index e2494d58..3177b57a 100755 --- a/Gui.py +++ b/Gui.py @@ -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 diff --git a/worlds/alttp/Main.py b/Main.py similarity index 93% rename from worlds/alttp/Main.py rename to Main.py index 3586364a..351d6de9 100644 --- a/worlds/alttp/Main.py +++ b/Main.py @@ -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])} diff --git a/MultiClient.py b/MultiClient.py index b88e23fd..98f5dec5 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -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) diff --git a/MultiServer.py b/MultiServer.py index b2b92abd..703d4e2d 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -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) diff --git a/Mystery.py b/Mystery.py index 98bc6935..373f58f6 100644 --- a/Mystery.py +++ b/Mystery.py @@ -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") diff --git a/NetUtils.py b/NetUtils.py index aa69b166..843f2cfa 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -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 \ No newline at end of file diff --git a/Utils.py b/Utils.py index 82c75136..b7587b5d 100644 --- a/Utils.py +++ b/Utils.py @@ -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)) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index ae9b7dec..f0a5f208 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -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 * diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index f7a97f65..22dda00c 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -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 diff --git a/worlds/__init__.py b/worlds/__init__.py index e69de29b..c3fca7d5 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -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} diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index b1eb7ba4..4699668a 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -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: diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index bf1443e6..ba58c754 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -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), diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index 21c8b4df..36a1373b 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -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"} diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 980926f9..e703eba9 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -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 diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 17d87751..f59e42f2 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -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]}" diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 231d2860..4be0cb0a 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -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(): diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index f3823e97..19376059 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -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'} \ No newline at end of file + 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 \ No newline at end of file diff --git a/worlds/hk/Items.py b/worlds/hk/Items.py new file mode 100644 index 00000000..e75883c6 --- /dev/null +++ b/worlds/hk/Items.py @@ -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()} \ No newline at end of file diff --git a/worlds/hk/Locations.py b/worlds/hk/Locations.py new file mode 100644 index 00000000..1f687bd2 --- /dev/null +++ b/worlds/hk/Locations.py @@ -0,0 +1,1018 @@ +locations = \ +{ 17825793: { 'name': 'Lurien', + 'scene': 'Ruins2_Watcher_Room', + 'x': 58.0, + 'y': 136.0}, + 17825794: { 'name': 'Monomon', + 'scene': 'Fungus3_archive_02', + 'x': 54.0, + 'y': 89.0}, + 17825795: { 'name': 'Herrah', + 'scene': 'Deepnest_Spider_Town', + 'x': 68.0, + 'y': 154.0}, + 17825796: { 'name': 'World_Sense', + 'scene': 'Room_Final_Boss_Atrium', + 'x': 202.0, + 'y': 7.0}, + 17825797: {'name': 'Dreamer', 'scene': None, 'x': 0, 'y': 0}, + 17825798: {'name': 'Mothwing_Cloak', 'scene': 'Fungus1_04', 'x': 0, 'y': 0}, + 17825799: {'name': 'Mantis_Claw', 'scene': 'Fungus2_14', 'x': 0, 'y': 0}, + 17825800: {'name': 'Crystal_Heart', 'scene': 'Mines_31', 'x': 0, 'y': 0}, + 17825801: {'name': 'Monarch_Wings', 'scene': 'Abyss_21', 'x': 0, 'y': 0}, + 17825802: {'name': 'Shade_Cloak', 'scene': 'Abyss_10', 'x': 0, 'y': 0}, + 17825803: {'name': "Isma's_Tear", 'scene': 'Waterways_13', 'x': 0, 'y': 0}, + 17825804: { 'name': 'Dream_Nail', + 'scene': 'Dream_Nailcollection', + 'x': 0, + 'y': 0}, + 17825805: { 'name': 'Dream_Gate', + 'scene': 'RestingGrounds_07', + 'x': 30.0, + 'y': 10.0}, + 17825806: { 'name': 'Awoken_Dream_Nail', + 'scene': 'RestingGrounds_07', + 'x': 21.0, + 'y': 10.0}, + 17825807: { 'name': 'Vengeful_Spirit', + 'scene': 'Crossroads_ShamanTemple', + 'x': 0, + 'y': 0}, + 17825808: {'name': 'Shade_Soul', 'scene': 'Ruins1_31b', 'x': 0, 'y': 0}, + 17825809: {'name': 'Desolate_Dive', 'scene': 'Ruins1_24', 'x': 0, 'y': 0}, + 17825810: {'name': 'Descending_Dark', 'scene': 'Mines_35', 'x': 0, 'y': 0}, + 17825811: { 'name': 'Howling_Wraiths', + 'scene': 'Room_Fungus_Shaman', + 'x': 0, + 'y': 0}, + 17825812: {'name': 'Abyss_Shriek', 'scene': 'Abyss_12', 'x': 0, 'y': 0}, + 17825813: { 'name': 'Cyclone_Slash', + 'scene': 'Room_nailmaster', + 'x': 0, + 'y': 0}, + 17825814: { 'name': 'Dash_Slash', + 'scene': 'Room_nailmaster_03', + 'x': 0, + 'y': 0}, + 17825815: { 'name': 'Great_Slash', + 'scene': 'Room_nailmaster_02', + 'x': 0, + 'y': 0}, + 17825816: {'name': 'Focus', 'scene': 'Tutorial_01', 'x': 111.5, 'y': 31.4}, + 17825817: {'name': 'Gathering_Swarm', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825818: { 'name': 'Wayward_Compass', + 'scene': 'Room_mapper', + 'x': 0, + 'y': 0}, + 17825819: { 'name': 'Grubsong', + 'scene': 'Crossroads_38', + 'x': 51.0, + 'y': 4.0}, + 17825820: {'name': 'Stalwart_Shell', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825821: {'name': 'Baldur_Shell', 'scene': 'Fungus1_28', 'x': 0, 'y': 0}, + 17825822: { 'name': 'Fury_of_the_Fallen', + 'scene': 'Tutorial_01', + 'x': 0, + 'y': 0}, + 17825823: { 'name': 'Quick_Focus', + 'scene': 'Room_Charm_Shop', + 'x': 0, + 'y': 0}, + 17825824: { 'name': 'Lifeblood_Heart', + 'scene': 'Room_Charm_Shop', + 'x': 0, + 'y': 0}, + 17825825: {'name': 'Lifeblood_Core', 'scene': 'Abyss_08', 'x': 0, 'y': 0}, + 17825826: { 'name': "Defender's_Crest", + 'scene': 'Waterways_05', + 'x': 0, + 'y': 0}, + 17825827: {'name': 'Flukenest', 'scene': 'Waterways_12', 'x': 0, 'y': 0}, + 17825828: { 'name': 'Thorns_of_Agony', + 'scene': 'Fungus1_14', + 'x': 0, + 'y': 0}, + 17825829: {'name': 'Mark_of_Pride', 'scene': 'Fungus2_31', 'x': 0, 'y': 0}, + 17825830: { 'name': 'Steady_Body', + 'scene': 'Room_Charm_Shop', + 'x': 0, + 'y': 0}, + 17825831: {'name': 'Heavy_Blow', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825832: {'name': 'Sharp_Shadow', 'scene': 'Deepnest_44', 'x': 0, 'y': 0}, + 17825833: {'name': 'Spore_Shroom', 'scene': 'Fungus2_20', 'x': 0, 'y': 0}, + 17825834: {'name': 'Longnail', 'scene': 'Room_Charm_Shop', 'x': 0, 'y': 0}, + 17825835: { 'name': 'Shaman_Stone', + 'scene': 'Room_Charm_Shop', + 'x': 0, + 'y': 0}, + 17825836: { 'name': 'Soul_Catcher', + 'scene': 'Crossroads_ShamanTemple', + 'x': 0, + 'y': 0}, + 17825837: { 'name': 'Soul_Eater', + 'scene': 'RestingGrounds_10', + 'x': 0, + 'y': 0}, + 17825838: { 'name': 'Glowing_Womb', + 'scene': 'Crossroads_22', + 'x': 0, + 'y': 0}, + 17825839: {'name': 'Fragile_Heart', 'scene': 'Fungus2_26', 'x': 0, 'y': 0}, + 17825840: {'name': 'Fragile_Greed', 'scene': 'Fungus2_26', 'x': 0, 'y': 0}, + 17825841: { 'name': 'Fragile_Strength', + 'scene': 'Fungus2_26', + 'x': 0, + 'y': 0}, + 17825842: { 'name': "Nailmaster's_Glory", + 'scene': 'Room_Sly_Storeroom', + 'x': 0, + 'y': 0}, + 17825843: {'name': "Joni's_Blessing", 'scene': 'Cliffs_05', 'x': 0, 'y': 0}, + 17825844: {'name': 'Shape_of_Unn', 'scene': 'Fungus1_Slug', 'x': 0, 'y': 0}, + 17825845: {'name': 'Hiveblood', 'scene': 'Hive_05', 'x': 0, 'y': 0}, + 17825846: { 'name': 'Dream_Wielder', + 'scene': 'RestingGrounds_07', + 'x': 36.0, + 'y': 10.0}, + 17825847: {'name': 'Dashmaster', 'scene': 'Fungus2_23', 'x': 0, 'y': 0}, + 17825848: { 'name': 'Quick_Slash', + 'scene': 'Deepnest_East_14b', + 'x': 0, + 'y': 0}, + 17825849: {'name': 'Spell_Twister', 'scene': 'Ruins1_30', 'x': 0, 'y': 0}, + 17825850: {'name': 'Deep_Focus', 'scene': 'Mines_36', 'x': 0, 'y': 0}, + 17825851: { 'name': "Grubberfly's_Elegy", + 'scene': 'Crossroads_38', + 'x': 36.0, + 'y': 4.0}, + 17825852: {'name': 'Queen_Fragment', 'scene': 'Room_Queen', 'x': 0, 'y': 0}, + 17825853: { 'name': 'King_Fragment', + 'scene': 'White_Palace_09', + 'x': 0, + 'y': 0}, + 17825854: { 'name': 'Void_Heart', + 'scene': 'Abyss_15', + 'x': 141.0, + 'y': 11.0}, + 17825855: {'name': 'Sprintmaster', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825856: { 'name': 'Dreamshield', + 'scene': 'RestingGrounds_17', + 'x': 0, + 'y': 0}, + 17825857: { 'name': 'Weaversong', + 'scene': 'Deepnest_45_v02', + 'x': 0, + 'y': 0}, + 17825858: { 'name': 'Grimmchild', + 'scene': 'Grimm_Main_Tent', + 'x': 75.0, + 'y': 7.0}, + 17825859: {'name': 'City_Crest', 'scene': 'Crossroads_10', 'x': 0, 'y': 0}, + 17825860: {'name': 'Lumafly_Lantern', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825861: {'name': 'Tram_Pass', 'scene': 'Deepnest_26b', 'x': 0, 'y': 0}, + 17825862: {'name': 'Simple_Key-Sly', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825863: {'name': 'Simple_Key-Basin', 'scene': 'Abyss_20', 'x': 0, 'y': 0}, + 17825864: {'name': 'Simple_Key-City', 'scene': 'Ruins1_17', 'x': 0, 'y': 0}, + 17825865: { 'name': 'Simple_Key-Lurker', + 'scene': 'GG_Lurker', + 'x': 169.5, + 'y': 52.4}, + 17825866: {'name': "Shopkeeper's_Key", 'scene': 'Mines_11', 'x': 0, 'y': 0}, + 17825867: {'name': 'Elegant_Key', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825868: {'name': 'Love_Key', 'scene': 'Fungus3_39', 'x': 0, 'y': 0}, + 17825869: {'name': "King's_Brand", 'scene': 'Room_Wyrm', 'x': 0, 'y': 0}, + 17825870: {'name': 'Godtuner', 'scene': 'GG_Waterways', 'x': 0, 'y': 0}, + 17825871: {'name': "Collector's_Map", 'scene': 'Ruins2_11', 'x': 0, 'y': 0}, + 17825872: {'name': 'Mask_Shard-Sly1', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825873: {'name': 'Mask_Shard-Sly2', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825874: {'name': 'Mask_Shard-Sly3', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825875: {'name': 'Mask_Shard-Sly4', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825876: { 'name': 'Mask_Shard-Seer', + 'scene': 'RestingGrounds_07', + 'x': 24.0, + 'y': 10.0}, + 17825877: { 'name': 'Mask_Shard-5_Grubs', + 'scene': 'Crossroads_38', + 'x': 54.0, + 'y': 4.0}, + 17825878: { 'name': 'Mask_Shard-Brooding_Mawlek', + 'scene': 'Crossroads_09', + 'x': 0, + 'y': 0}, + 17825879: { 'name': 'Mask_Shard-Crossroads_Goam', + 'scene': 'Crossroads_13', + 'x': 0, + 'y': 0}, + 17825880: { 'name': 'Mask_Shard-Stone_Sanctuary', + 'scene': 'Fungus1_36', + 'x': 0, + 'y': 0}, + 17825881: { 'name': "Mask_Shard-Queen's_Station", + 'scene': 'Fungus2_01', + 'x': 0, + 'y': 0}, + 17825882: { 'name': 'Mask_Shard-Deepnest', + 'scene': 'Fungus2_25', + 'x': 0, + 'y': 0}, + 17825883: { 'name': 'Mask_Shard-Waterways', + 'scene': 'Waterways_04b', + 'x': 0, + 'y': 0}, + 17825884: { 'name': 'Mask_Shard-Enraged_Guardian', + 'scene': 'Mines_32', + 'x': 0, + 'y': 0}, + 17825885: {'name': 'Mask_Shard-Hive', 'scene': 'Hive_04', 'x': 0, 'y': 0}, + 17825886: { 'name': 'Mask_Shard-Grey_Mourner', + 'scene': 'Room_Mansion', + 'x': 0, + 'y': 0}, + 17825887: { 'name': 'Mask_Shard-Bretta', + 'scene': 'Room_Bretta', + 'x': 0, + 'y': 0}, + 17825888: { 'name': 'Vessel_Fragment-Sly1', + 'scene': 'Room_shop', + 'x': 0, + 'y': 0}, + 17825889: { 'name': 'Vessel_Fragment-Sly2', + 'scene': 'Room_shop', + 'x': 0, + 'y': 0}, + 17825890: { 'name': 'Vessel_Fragment-Seer', + 'scene': 'RestingGrounds_07', + 'x': 33.0, + 'y': 10.0}, + 17825891: { 'name': 'Vessel_Fragment-Greenpath', + 'scene': 'Fungus1_13', + 'x': 0, + 'y': 0}, + 17825892: { 'name': 'Vessel_Fragment-City', + 'scene': 'Ruins2_09', + 'x': 0, + 'y': 0}, + 17825893: { 'name': 'Vessel_Fragment-Crossroads', + 'scene': 'Crossroads_37', + 'x': 0, + 'y': 0}, + 17825894: { 'name': 'Vessel_Fragment-Basin', + 'scene': 'Abyss_04', + 'x': 65.0, + 'y': 58.0}, + 17825895: { 'name': 'Vessel_Fragment-Deepnest', + 'scene': 'Deepnest_38', + 'x': 0, + 'y': 0}, + 17825896: { 'name': 'Vessel_Fragment-Stag_Nest', + 'scene': 'Cliffs_03', + 'x': 0, + 'y': 0}, + 17825897: { 'name': 'Charm_Notch-Shrumal_Ogres', + 'scene': 'Fungus2_05', + 'x': 0, + 'y': 0}, + 17825898: { 'name': 'Charm_Notch-Fog_Canyon', + 'scene': 'Fungus3_28', + 'x': 0, + 'y': 0}, + 17825899: { 'name': 'Charm_Notch-Colosseum', + 'scene': 'Room_Colosseum_Bronze', + 'x': 0, + 'y': 0}, + 17825900: { 'name': 'Charm_Notch-Grimm', + 'scene': 'Grimm_Main_Tent', + 'x': 0, + 'y': 0}, + 17825901: {'name': 'Pale_Ore-Basin', 'scene': 'Abyss_17', 'x': 0, 'y': 0}, + 17825902: { 'name': 'Pale_Ore-Crystal_Peak', + 'scene': 'Mines_34', + 'x': 0, + 'y': 0}, + 17825903: {'name': 'Pale_Ore-Nosk', 'scene': 'Deepnest_32', 'x': 0, 'y': 0}, + 17825904: { 'name': 'Pale_Ore-Seer', + 'scene': 'RestingGrounds_07', + 'x': 39.0, + 'y': 10.0}, + 17825905: { 'name': 'Pale_Ore-Grubs', + 'scene': 'Crossroads_38', + 'x': 42.0, + 'y': 4.0}, + 17825906: { 'name': 'Pale_Ore-Colosseum', + 'scene': 'Room_Colosseum_Silver', + 'x': 0, + 'y': 0}, + 17825907: { 'name': '200_Geo-False_Knight_Chest', + 'scene': 'Crossroads_10', + 'x': 0, + 'y': 0}, + 17825908: { 'name': '380_Geo-Soul_Master_Chest', + 'scene': 'Ruins1_32', + 'x': 0, + 'y': 0}, + 17825909: { 'name': '655_Geo-Watcher_Knights_Chest', + 'scene': 'Ruins2_03', + 'x': 0, + 'y': 0}, + 17825910: { 'name': '85_Geo-Greenpath_Chest', + 'scene': 'Fungus1_13', + 'x': 0, + 'y': 0}, + 17825911: { 'name': '620_Geo-Mantis_Lords_Chest', + 'scene': 'Fungus2_31', + 'x': 0, + 'y': 0}, + 17825912: { 'name': '150_Geo-Resting_Grounds_Chest', + 'scene': 'RestingGrounds_10', + 'x': 0, + 'y': 0}, + 17825913: { 'name': '80_Geo-Crystal_Peak_Chest', + 'scene': 'Mines_37', + 'x': 0, + 'y': 0}, + 17825914: { 'name': '160_Geo-Weavers_Den_Chest', + 'scene': 'Deepnest_45_v02', + 'x': 0, + 'y': 0}, + 17825915: {'name': '1_Geo', 'scene': None, 'x': 0, 'y': 0}, + 17825916: {'name': 'Rancid_Egg-Sly', 'scene': 'Room_shop', 'x': 0, 'y': 0}, + 17825917: { 'name': 'Rancid_Egg-Grubs', + 'scene': 'Crossroads_38', + 'x': 48.0, + 'y': 4.0}, + 17825918: { 'name': 'Rancid_Egg-Sheo', + 'scene': 'Fungus1_15', + 'x': 0, + 'y': 0}, + 17825919: { 'name': 'Rancid_Egg-Fungal_Core', + 'scene': 'Fungus2_29', + 'x': 0, + 'y': 0}, + 17825920: { 'name': "Rancid_Egg-Queen's_Gardens", + 'scene': 'Fungus3_34', + 'x': 0, + 'y': 0}, + 17825921: { 'name': 'Rancid_Egg-Blue_Lake', + 'scene': 'Crossroads_50', + 'x': 0, + 'y': 0}, + 17825922: { 'name': 'Rancid_Egg-Crystal_Peak_Dive_Entrance', + 'scene': 'Mines_01', + 'x': 0, + 'y': 0}, + 17825923: { 'name': 'Rancid_Egg-Crystal_Peak_Dark_Room', + 'scene': 'Mines_29', + 'x': 0, + 'y': 0}, + 17825924: { 'name': 'Rancid_Egg-Crystal_Peak_Tall_Room', + 'scene': 'Mines_20', + 'x': 0, + 'y': 0}, + 17825925: { 'name': 'Rancid_Egg-City_of_Tears_Left', + 'scene': 'Ruins1_05c', + 'x': 0, + 'y': 0}, + 17825926: { 'name': 'Rancid_Egg-City_of_Tears_Pleasure_House', + 'scene': 'Ruins_Elevator', + 'x': 0, + 'y': 0}, + 17825927: { 'name': "Rancid_Egg-Beast's_Den", + 'scene': 'Deepnest_Spider_Town', + 'x': 0, + 'y': 0}, + 17825928: { 'name': 'Rancid_Egg-Dark_Deepnest', + 'scene': 'Deepnest_39', + 'x': 0, + 'y': 0}, + 17825929: { 'name': "Rancid_Egg-Weaver's_Den", + 'scene': 'Deepnest_45_v02', + 'x': 0, + 'y': 0}, + 17825930: { 'name': 'Rancid_Egg-Near_Quick_Slash', + 'scene': 'Deepnest_East_14', + 'x': 0, + 'y': 0}, + 17825931: { 'name': "Rancid_Egg-Upper_Kingdom's_Edge", + 'scene': 'Deepnest_East_07', + 'x': 0, + 'y': 0}, + 17825932: { 'name': 'Rancid_Egg-Waterways_East', + 'scene': 'Waterways_07', + 'x': 0, + 'y': 0}, + 17825933: { 'name': 'Rancid_Egg-Waterways_Main', + 'scene': 'Waterways_02', + 'x': 0, + 'y': 0}, + 17825934: { 'name': 'Rancid_Egg-Waterways_West_Bluggsac', + 'scene': 'Waterways_04', + 'x': 0, + 'y': 0}, + 17825935: { 'name': 'Rancid_Egg-Waterways_West_Pickup', + 'scene': 'Waterways_04b', + 'x': 0, + 'y': 0}, + 17825936: { 'name': "Wanderer's_Journal-Cliffs", + 'scene': 'Cliffs_01', + 'x': 0, + 'y': 0}, + 17825937: { 'name': "Wanderer's_Journal-Greenpath_Stag", + 'scene': 'Fungus1_22', + 'x': 0, + 'y': 0}, + 17825938: { 'name': "Wanderer's_Journal-Greenpath_Lower", + 'scene': 'Fungus1_11', + 'x': 0, + 'y': 0}, + 17825939: { 'name': "Wanderer's_Journal-Fungal_Wastes_Thorns_Gauntlet", + 'scene': 'Fungus2_04', + 'x': 0, + 'y': 0}, + 17825940: { 'name': "Wanderer's_Journal-Above_Mantis_Village", + 'scene': 'Fungus2_17', + 'x': 0, + 'y': 0}, + 17825941: { 'name': "Wanderer's_Journal-Crystal_Peak_Crawlers", + 'scene': 'Mines_20', + 'x': 0, + 'y': 0}, + 17825942: { 'name': "Wanderer's_Journal-Resting_Grounds_Catacombs", + 'scene': 'RestingGrounds_10', + 'x': 0, + 'y': 0}, + 17825943: { 'name': "Wanderer's_Journal-King's_Station", + 'scene': 'Ruins2_05', + 'x': 0, + 'y': 0}, + 17825944: { 'name': "Wanderer's_Journal-Pleasure_House", + 'scene': 'Ruins_Elevator', + 'x': 0, + 'y': 0}, + 17825945: { 'name': "Wanderer's_Journal-City_Storerooms", + 'scene': 'Ruins1_28', + 'x': 0, + 'y': 0}, + 17825946: { 'name': "Wanderer's_Journal-Ancient_Basin", + 'scene': 'Abyss_02', + 'x': 0, + 'y': 0}, + 17825947: { 'name': "Wanderer's_Journal-Kingdom's_Edge_Entrance", + 'scene': 'Deepnest_East_07', + 'x': 0, + 'y': 0}, + 17825948: { 'name': "Wanderer's_Journal-Kingdom's_Edge_Camp", + 'scene': 'Deepnest_East_13', + 'x': 0, + 'y': 0}, + 17825949: { 'name': "Wanderer's_Journal-Kingdom's_Edge_Requires_Dive", + 'scene': 'Deepnest_East_18', + 'x': 0, + 'y': 0}, + 17825950: { 'name': 'Hallownest_Seal-Crossroads_Well', + 'scene': 'Crossroads_01', + 'x': 0, + 'y': 0}, + 17825951: { 'name': 'Hallownest_Seal-Grubs', + 'scene': 'Crossroads_38', + 'x': 45.0, + 'y': 4.0}, + 17825952: { 'name': 'Hallownest_Seal-Greenpath', + 'scene': 'Fungus1_10', + 'x': 0, + 'y': 0}, + 17825953: { 'name': 'Hallownest_Seal-Fog_Canyon_West', + 'scene': 'Fungus3_30', + 'x': 0, + 'y': 0}, + 17825954: { 'name': 'Hallownest_Seal-Fog_Canyon_East', + 'scene': 'Fungus3_26', + 'x': 0, + 'y': 0}, + 17825955: { 'name': "Hallownest_Seal-Queen's_Station", + 'scene': 'Fungus2_34', + 'x': 0, + 'y': 0}, + 17825956: { 'name': 'Hallownest_Seal-Fungal_Wastes_Sporgs', + 'scene': 'Fungus2_03', + 'x': 0, + 'y': 0}, + 17825957: { 'name': 'Hallownest_Seal-Mantis_Lords', + 'scene': 'Fungus2_31', + 'x': 0, + 'y': 0}, + 17825958: { 'name': 'Hallownest_Seal-Seer', + 'scene': 'RestingGrounds_07', + 'x': 42.0, + 'y': 10.0}, + 17825959: { 'name': 'Hallownest_Seal-Resting_Grounds_Catacombs', + 'scene': 'RestingGrounds_10', + 'x': 0, + 'y': 0}, + 17825960: { 'name': "Hallownest_Seal-King's_Station", + 'scene': 'Ruins2_08', + 'x': 0, + 'y': 0}, + 17825961: { 'name': 'Hallownest_Seal-City_Rafters', + 'scene': 'Ruins1_03', + 'x': 0, + 'y': 0}, + 17825962: { 'name': 'Hallownest_Seal-Soul_Sanctum', + 'scene': 'Ruins1_32', + 'x': 0, + 'y': 0}, + 17825963: { 'name': 'Hallownest_Seal-Watcher_Knight', + 'scene': 'Ruins2_03', + 'x': 0, + 'y': 0}, + 17825964: { 'name': 'Hallownest_Seal-Deepnest_By_Mantis_Lords', + 'scene': 'Deepnest_16', + 'x': 0, + 'y': 0}, + 17825965: { 'name': "Hallownest_Seal-Beast's_Den", + 'scene': 'Deepnest_Spider_Town', + 'x': 0, + 'y': 0}, + 17825966: { 'name': "Hallownest_Seal-Queen's_Gardens", + 'scene': 'Fungus3_48', + 'x': 0, + 'y': 0}, + 17825967: { 'name': "King's_Idol-Grubs", + 'scene': 'Crossroads_38', + 'x': 39.0, + 'y': 4.0}, + 17825968: { 'name': "King's_Idol-Cliffs", + 'scene': 'Cliffs_01', + 'x': 0, + 'y': 0}, + 17825969: { 'name': "King's_Idol-Crystal_Peak", + 'scene': 'Mines_30', + 'x': 0, + 'y': 0}, + 17825970: { 'name': "King's_Idol-Glade_of_Hope", + 'scene': 'RestingGrounds_08', + 'x': 0, + 'y': 0}, + 17825971: { 'name': "King's_Idol-Dung_Defender", + 'scene': 'Waterways_15', + 'x': 0, + 'y': 0}, + 17825972: { 'name': "King's_Idol-Great_Hopper", + 'scene': 'Deepnest_East_08', + 'x': 0, + 'y': 0}, + 17825973: { 'name': "King's_Idol-Pale_Lurker", + 'scene': 'GG_Lurker', + 'x': 0, + 'y': 0}, + 17825974: { 'name': "King's_Idol-Deepnest", + 'scene': 'Deepnest_33', + 'x': 0, + 'y': 0}, + 17825975: { 'name': 'Arcane_Egg-Seer', + 'scene': 'RestingGrounds_07', + 'x': 27.0, + 'y': 10.0}, + 17825976: { 'name': 'Arcane_Egg-Lifeblood_Core', + 'scene': 'Abyss_08', + 'x': 0, + 'y': 0}, + 17825977: { 'name': 'Arcane_Egg-Shade_Cloak', + 'scene': 'Abyss_10', + 'x': 0, + 'y': 0}, + 17825978: { 'name': 'Arcane_Egg-Birthplace', + 'scene': 'Abyss_15', + 'x': 0, + 'y': 0}, + 17825979: { 'name': 'Whispering_Root-Crossroads', + 'scene': 'Crossroads_07', + 'x': 6.8, + 'y': 103.2}, + 17825980: { 'name': 'Whispering_Root-Greenpath', + 'scene': 'Fungus1_13', + 'x': 28.7, + 'y': 23.3}, + 17825981: { 'name': 'Whispering_Root-Leg_Eater', + 'scene': 'Fungus2_33', + 'x': 34.7, + 'y': 7.4}, + 17825982: { 'name': 'Whispering_Root-Mantis_Village', + 'scene': 'Fungus2_17', + 'x': 27.7, + 'y': 3.4}, + 17825983: { 'name': 'Whispering_Root-Deepnest', + 'scene': 'Deepnest_39', + 'x': 13.4, + 'y': 36.3}, + 17825984: { 'name': 'Whispering_Root-Queens_Gardens', + 'scene': 'Fungus3_11', + 'x': 34.0, + 'y': 8.5}, + 17825985: { 'name': 'Whispering_Root-Kingdoms_Edge', + 'scene': 'Deepnest_East_07', + 'x': 59.6, + 'y': 10.2}, + 17825986: { 'name': 'Whispering_Root-Waterways', + 'scene': 'Abyss_01', + 'x': 37.9, + 'y': 10.4}, + 17825987: { 'name': 'Whispering_Root-City', + 'scene': 'Ruins1_17', + 'x': 78.0, + 'y': 22.0}, + 17825988: { 'name': 'Whispering_Root-Resting_Grounds', + 'scene': 'RestingGrounds_05', + 'x': 15.7, + 'y': 69.4}, + 17825989: { 'name': 'Whispering_Root-Spirits_Glade', + 'scene': 'RestingGrounds_08', + 'x': 153.4, + 'y': 56.5}, + 17825990: { 'name': 'Whispering_Root-Crystal_Peak', + 'scene': 'Mines_23', + 'x': 91.6, + 'y': 8.0}, + 17825991: { 'name': 'Whispering_Root-Howling_Cliffs', + 'scene': 'Cliffs_01', + 'x': 83.7, + 'y': 15.3}, + 17825992: { 'name': 'Whispering_Root-Ancestral_Mound', + 'scene': 'Crossroads_ShamanTemple', + 'x': 20.4, + 'y': 60.4}, + 17825993: { 'name': 'Whispering_Root-Hive', + 'scene': 'Hive_02', + 'x': 168.7, + 'y': 24.1}, + 17825994: { 'name': 'Boss_Essence-Elder_Hu', + 'scene': 'Fungus2_32', + 'x': 0, + 'y': 0}, + 17825995: { 'name': 'Boss_Essence-Xero', + 'scene': 'RestingGrounds_02_boss', + 'x': 0, + 'y': 0}, + 17825996: { 'name': 'Boss_Essence-Gorb', + 'scene': 'Cliffs_02_boss', + 'x': 0, + 'y': 0}, + 17825997: { 'name': 'Boss_Essence-Marmu', + 'scene': 'Fungus3_40_boss', + 'x': 0, + 'y': 0}, + 17825998: { 'name': 'Boss_Essence-No_Eyes', + 'scene': 'Fungus1_35', + 'x': 0, + 'y': 0}, + 17825999: { 'name': 'Boss_Essence-Galien', + 'scene': 'Deepnest_40', + 'x': 0, + 'y': 0}, + 17826000: { 'name': 'Boss_Essence-Markoth', + 'scene': 'Deepnest_East_10', + 'x': 0, + 'y': 0}, + 17826001: { 'name': 'Boss_Essence-Failed_Champion', + 'scene': 'Crossroads_10', + 'x': 0, + 'y': 0}, + 17826002: { 'name': 'Boss_Essence-Soul_Tyrant', + 'scene': 'Ruins1_24_boss_defeated', + 'x': 0, + 'y': 0}, + 17826003: { 'name': 'Boss_Essence-Lost_Kin', + 'scene': 'Abyss_19', + 'x': 0, + 'y': 0}, + 17826004: { 'name': 'Boss_Essence-White_Defender', + 'scene': 'Waterways_15', + 'x': 0, + 'y': 0}, + 17826005: { 'name': 'Boss_Essence-Grey_Prince_Zote', + 'scene': 'Room_Bretta_Basement', + 'x': 0, + 'y': 0}, + 17826006: { 'name': 'Grub-Crossroads_Acid', + 'scene': 'Crossroads_35', + 'x': 0, + 'y': 0}, + 17826007: { 'name': 'Grub-Crossroads_Center', + 'scene': 'Crossroads_05', + 'x': 0, + 'y': 0}, + 17826008: { 'name': 'Grub-Crossroads_Stag', + 'scene': 'Crossroads_03', + 'x': 0, + 'y': 0}, + 17826009: { 'name': 'Grub-Crossroads_Spike', + 'scene': 'Crossroads_31', + 'x': 0, + 'y': 0}, + 17826010: { 'name': 'Grub-Crossroads_Guarded', + 'scene': 'Crossroads_48', + 'x': 0, + 'y': 0}, + 17826011: { 'name': 'Grub-Greenpath_Cornifer', + 'scene': 'Fungus1_06', + 'x': 0, + 'y': 0}, + 17826012: { 'name': 'Grub-Greenpath_Journal', + 'scene': 'Fungus1_07', + 'x': 0, + 'y': 0}, + 17826013: { 'name': 'Grub-Greenpath_MMC', + 'scene': 'Fungus1_13', + 'x': 0, + 'y': 0}, + 17826014: { 'name': 'Grub-Greenpath_Stag', + 'scene': 'Fungus1_21', + 'x': 0, + 'y': 0}, + 17826015: { 'name': 'Grub-Fog_Canyon', + 'scene': 'Fungus3_47', + 'x': 0, + 'y': 0}, + 17826016: { 'name': 'Grub-Fungal_Bouncy', + 'scene': 'Fungus2_18', + 'x': 0, + 'y': 0}, + 17826017: { 'name': 'Grub-Fungal_Spore_Shroom', + 'scene': 'Fungus2_20', + 'x': 0, + 'y': 0}, + 17826018: { 'name': 'Grub-Deepnest_Mimic', + 'scene': 'Deepnest_36', + 'x': 0, + 'y': 0}, + 17826019: { 'name': 'Grub-Deepnest_Nosk', + 'scene': 'Deepnest_31', + 'x': 0, + 'y': 0}, + 17826020: { 'name': 'Grub-Deepnest_Spike', + 'scene': 'Deepnest_03', + 'x': 0, + 'y': 0}, + 17826021: { 'name': 'Grub-Dark_Deepnest', + 'scene': 'Deepnest_39', + 'x': 0, + 'y': 0}, + 17826022: { 'name': "Grub-Beast's_Den", + 'scene': 'Deepnest_Spider_Town', + 'x': 0, + 'y': 0}, + 17826023: { 'name': "Grub-Kingdom's_Edge_Oro", + 'scene': 'Deepnest_East_14', + 'x': 0, + 'y': 0}, + 17826024: { 'name': "Grub-Kingdom's_Edge_Camp", + 'scene': 'Deepnest_East_11', + 'x': 0, + 'y': 0}, + 17826025: { 'name': 'Grub-Hive_External', + 'scene': 'Hive_03', + 'x': 0, + 'y': 0}, + 17826026: { 'name': 'Grub-Hive_Internal', + 'scene': 'Hive_04', + 'x': 0, + 'y': 0}, + 17826027: { 'name': 'Grub-Basin_Requires_Wings', + 'scene': 'Abyss_19', + 'x': 0, + 'y': 0}, + 17826028: { 'name': 'Grub-Basin_Requires_Dive', + 'scene': 'Abyss_17', + 'x': 0, + 'y': 0}, + 17826029: { 'name': 'Grub-Waterways_Main', + 'scene': 'Waterways_04', + 'x': 0, + 'y': 0}, + 17826030: { 'name': 'Grub-Waterways_East', + 'scene': 'Waterways_13', + 'x': 0, + 'y': 0}, + 17826031: { 'name': 'Grub-Waterways_Requires_Tram', + 'scene': 'Waterways_14', + 'x': 0, + 'y': 0}, + 17826032: { 'name': 'Grub-City_of_Tears_Left', + 'scene': 'Ruins1_05', + 'x': 0, + 'y': 0}, + 17826033: { 'name': 'Grub-Soul_Sanctum', + 'scene': 'Ruins1_32', + 'x': 0, + 'y': 0}, + 17826034: { 'name': "Grub-Watcher's_Spire", + 'scene': 'Ruins2_03', + 'x': 0, + 'y': 0}, + 17826035: { 'name': 'Grub-City_of_Tears_Guarded', + 'scene': 'Ruins_House_01', + 'x': 0, + 'y': 0}, + 17826036: { 'name': "Grub-King's_Station", + 'scene': 'Ruins2_07', + 'x': 0, + 'y': 0}, + 17826037: { 'name': 'Grub-Resting_Grounds', + 'scene': 'RestingGrounds_10', + 'x': 0, + 'y': 0}, + 17826038: { 'name': 'Grub-Crystal_Peak_Below_Chest', + 'scene': 'Mines_04', + 'x': 0, + 'y': 0}, + 17826039: { 'name': 'Grub-Crystallized_Mound', + 'scene': 'Mines_35', + 'x': 0, + 'y': 0}, + 17826040: { 'name': 'Grub-Crystal_Peak_Spike', + 'scene': 'Mines_03', + 'x': 0, + 'y': 0}, + 17826041: { 'name': 'Grub-Crystal_Peak_Mimic', + 'scene': 'Mines_16', + 'x': 0, + 'y': 0}, + 17826042: { 'name': 'Grub-Crystal_Peak_Crushers', + 'scene': 'Mines_19', + 'x': 0, + 'y': 0}, + 17826043: { 'name': 'Grub-Crystal_Heart', + 'scene': 'Mines_31', + 'x': 0, + 'y': 0}, + 17826044: { 'name': 'Grub-Hallownest_Crown', + 'scene': 'Mines_24', + 'x': 0, + 'y': 0}, + 17826045: { 'name': 'Grub-Howling_Cliffs', + 'scene': 'Fungus1_28', + 'x': 0, + 'y': 0}, + 17826046: { 'name': "Grub-Queen's_Gardens_Stag", + 'scene': 'Fungus3_10', + 'x': 0, + 'y': 0}, + 17826047: { 'name': "Grub-Queen's_Gardens_Marmu", + 'scene': 'Fungus3_48', + 'x': 0, + 'y': 0}, + 17826048: { 'name': "Grub-Queen's_Gardens_Top", + 'scene': 'Fungus3_22', + 'x': 0, + 'y': 0}, + 17826049: { 'name': 'Grub-Collector_1', + 'scene': 'Ruins2_11', + 'x': 64.2, + 'y': 115.4}, + 17826050: { 'name': 'Grub-Collector_2', + 'scene': 'Ruins2_11', + 'x': 55.3, + 'y': 115.4}, + 17826051: { 'name': 'Grub-Collector_3', + 'scene': 'Ruins2_11', + 'x': 38.5, + 'y': 115.4}, + 17826052: { 'name': 'Crossroads_Map', + 'scene': 'Crossroads_33', + 'x': 36.5, + 'y': 34.0}, + 17826053: { 'name': 'Greenpath_Map', + 'scene': 'Fungus1_06', + 'x': 160.0, + 'y': 3.3}, + 17826054: { 'name': 'Fog_Canyon_Map', + 'scene': 'Fungus3_25', + 'x': 36.4, + 'y': 32.0}, + 17826055: { 'name': 'Fungal_Wastes_Map', + 'scene': 'Fungus2_18', + 'x': 11.0, + 'y': 35.0}, + 17826056: { 'name': 'Deepnest_Map-Upper', + 'scene': 'Deepnest_01b', + 'x': 7.8, + 'y': 4.5}, + 17826057: { 'name': 'Deepnest_Map-Right_[Gives_Quill]', + 'scene': 'Fungus2_25', + 'x': 49.5, + 'y': 23.2}, + 17826058: { 'name': 'Ancient_Basin_Map', + 'scene': 'Abyss_04', + 'x': 68.3, + 'y': 41.4}, + 17826059: { 'name': "Kingdom's_Edge_Map", + 'scene': 'Deepnest_East_03', + 'x': 7.5, + 'y': 60.1}, + 17826060: { 'name': 'City_of_Tears_Map', + 'scene': 'Ruins1_31', + 'x': 35.1, + 'y': 15.3}, + 17826061: { 'name': 'Royal_Waterways_Map', + 'scene': 'Waterways_09', + 'x': 25.4, + 'y': 30.5}, + 17826062: { 'name': 'Howling_Cliffs_Map', + 'scene': 'Cliffs_01', + 'x': 125.8, + 'y': 91.6}, + 17826063: { 'name': 'Crystal_Peak_Map', + 'scene': 'Mines_30', + 'x': 157.0, + 'y': 8.4}, + 17826064: { 'name': "Queen's_Gardens_Map", + 'scene': 'Fungus1_24', + 'x': 53.7, + 'y': 22.3}, + 17826065: { 'name': 'Resting_Grounds_Map', + 'scene': 'RestingGrounds_09', + 'x': 8.2, + 'y': 6.1}, + 17826066: { 'name': 'Dirtmouth_Stag', + 'scene': 'Room_Town_Stag_Station', + 'x': 39.4, + 'y': 20.4}, + 17826067: { 'name': 'Crossroads_Stag', + 'scene': 'Crossroads_47', + 'x': 19.7, + 'y': 6.4}, + 17826068: { 'name': 'Greenpath_Stag', + 'scene': 'Fungus1_16_alt', + 'x': 19.7, + 'y': 6.4}, + 17826069: { 'name': "Queen's_Station_Stag", + 'scene': 'Fungus2_02', + 'x': 20.3, + 'y': 6.4}, + 17826070: { 'name': "Queen's_Gardens_Stag", + 'scene': 'Fungus3_40', + 'x': 159.8, + 'y': 13.4}, + 17826071: { 'name': 'City_Storerooms_Stag', + 'scene': 'Ruins1_29', + 'x': 28.1, + 'y': 6.4}, + 17826072: { 'name': "King's_Station_Stag", + 'scene': 'Ruins2_08', + 'x': 28.1, + 'y': 6.4}, + 17826073: { 'name': 'Resting_Grounds_Stag', + 'scene': 'RestingGrounds_09', + 'x': 27.6, + 'y': 6.4}, + 17826074: { 'name': 'Distant_Village_Stag', + 'scene': 'Deepnest_09', + 'x': 27.6, + 'y': 6.4}, + 17826075: { 'name': 'Hidden_Station_Stag', + 'scene': 'Abyss_22', + 'x': 28.0, + 'y': 6.4}, + 17826076: { 'name': 'Stag_Nest_Stag', + 'scene': 'Cliffs_03', + 'x': 19.3, + 'y': 6.3}, + 17826077: { 'name': "Lifeblood_Cocoon-King's_Pass", + 'scene': 'Tutorial_01', + 'x': 58.3, + 'y': 58.4}, + 17826078: { 'name': 'Lifeblood_Cocoon-Ancestral_Mound', + 'scene': 'Crossroads_ShamanTemple', + 'x': 32.0, + 'y': 46.4}, + 17826079: { 'name': 'Lifeblood_Cocoon-Greenpath', + 'scene': 'Fungus1_32', + 'x': 33.3, + 'y': 23.4}, + 17826080: { 'name': 'Lifeblood_Cocoon-Fog_Canyon_West', + 'scene': 'Fungus3_30', + 'x': 5.2, + 'y': 7.4}, + 17826081: { 'name': 'Lifeblood_Cocoon-Mantis_Village', + 'scene': 'Fungus2_15', + 'x': 5.5, + 'y': 23.4}, + 17826082: { 'name': 'Lifeblood_Cocoon-Failed_Tramway', + 'scene': 'Deepnest_26', + 'x': 137.8, + 'y': 36.4}, + 17826083: { 'name': 'Lifeblood_Cocoon-Galien', + 'scene': 'Deepnest_40', + 'x': 72.8, + 'y': 48.4}, + 17826084: { 'name': "Lifeblood_Cocoon-Kingdom's_Edge", + 'scene': 'Deepnest_East_15', + 'x': 30.8, + 'y': 4.4}, + 17826085: {'name': 'Grubfather', 'scene': None, 'x': 0, 'y': 0}, + 17826086: {'name': 'Seer', 'scene': None, 'x': 0, 'y': 0}, + 17826087: {'name': 'Equipped', 'scene': None, 'x': 0, 'y': 0}, + 17826088: {'name': 'Placeholder', 'scene': None, 'x': 0, 'y': 0}} + + +lookup_id_to_name = {location_id: data["name"] for location_id, data in locations.items()} +lookup_name_to_id = {data["name"]: location_id for location_id, data in locations.items()} \ No newline at end of file diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py new file mode 100644 index 00000000..8859247d --- /dev/null +++ b/worlds/hk/__init__.py @@ -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