Hollow Knight integration

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

View File

@ -136,6 +136,7 @@ class MultiWorld():
set_player_attr('plando_items', []) set_player_attr('plando_items', [])
set_player_attr('plando_texts', {}) set_player_attr('plando_texts', {})
set_player_attr('plando_connections', []) set_player_attr('plando_connections', [])
set_player_attr('game', "A Link to the Past")
self.worlds = [] self.worlds = []
#for i in range(players): #for i in range(players):
@ -148,6 +149,14 @@ class MultiWorld():
def player_ids(self): def player_ids(self):
yield from range(1, self.players + 1) 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: 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)})' 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: 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] 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 return self.name == other.name and self.player == other.player
def __repr__(self): def __repr__(self):
@ -1031,29 +1042,25 @@ class Boss():
def can_defeat(self, state) -> bool: def can_defeat(self, state) -> bool:
return self.defeat_rule(state, self.player) return self.defeat_rule(state, self.player)
class Location(): class Location():
shop_slot: bool = False shop_slot: bool = False
shop_slot_disabled: bool = False shop_slot_disabled: bool = False
event: bool = False event: bool = False
locked: bool = False locked: bool = False
spot_type = 'Location' spot_type = 'Location'
game: str = "Generic"
crystal: bool = False
def __init__(self, player: int, name: str = '', address=None, crystal: bool = False, def __init__(self, player: int, name: str = '', address:int = None, parent=None):
hint_text: Optional[str] = None, parent=None,
player_address=None):
self.name = name self.name = name
self.parent_region = parent
self.item = None
self.crystal = crystal
self.address = address self.address = address
self.player_address = player_address self.parent_region = parent
self.hint_text: str = hint_text if hint_text else name
self.recursion_count = 0 self.recursion_count = 0
self.player = player
self.item = None
self.always_allow = lambda item, state: False self.always_allow = lambda item, state: False
self.access_rule = lambda state: True self.access_rule = lambda state: True
self.item_rule = lambda item: True self.item_rule = lambda item: True
self.player = player
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state))) return self.always_allow(state, item) or (self.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): def __lt__(self, other):
return (self.player, self.name) < (other.player, other.name) 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 location: Optional[Location] = None
world: Optional[World] = 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.name = name
self.advancement = advancement 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.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): def __eq__(self, other):
return self.name == other.name and self.player == other.player 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})' 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): class Spoiler(object):
world: MultiWorld 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'] shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement']
self.shops.append(shopdata) 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)] = OrderedDict()
self.bosses[str(player)]["Eastern Palace"] = self.world.get_dungeon("Eastern Palace", player).boss.name 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 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: if self.world.players == 1:
self.bosses = self.bosses["1"] self.bosses = self.bosses["1"]
from Utils import __version__ as ERVersion from Utils import __version__ as APVersion
self.metadata = {'version': ERVersion, self.metadata = {'version': APVersion,
'logic': self.world.logic, 'logic': self.world.logic,
'dark_room_logic': self.world.dark_room_logic, 'dark_room_logic': self.world.dark_room_logic,
'mode': self.world.mode, 'mode': self.world.mode,
@ -1292,6 +1312,7 @@ class Spoiler(object):
'shuffle_prizes': self.world.shuffle_prizes, 'shuffle_prizes': self.world.shuffle_prizes,
'sprite_pool': self.world.sprite_pool, 'sprite_pool': self.world.sprite_pool,
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss, 'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
'game': self.world.game,
'er_seeds': self.world.er_seeds 'er_seeds': self.world.er_seeds
} }
@ -1331,17 +1352,23 @@ class Spoiler(object):
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
if self.world.players > 1: if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(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('Accessibility: %s\n' % self.metadata['accessibility'][player])
if player in self.world.alttp_player_ids:
for team in range(self.world.teams): for team in range(self.world.teams):
outfile.write('%s%s\n' % ( outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if self.world.teams > 1 else 'Hash: ', 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])) self.hashes[player, team]))
outfile.write('Logic: %s\n' % self.metadata['logic'][player]) outfile.write('Logic: %s\n' % self.metadata['logic'][player])
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player]) outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
outfile.write('Restricted Boss Drops: %s\n' % outfile.write('Restricted Boss Drops: %s\n' %
bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][player])) bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][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('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Retro: %s\n' % outfile.write('Retro: %s\n' %
('Yes' if self.metadata['retro'][player] else 'No')) ('Yes' if self.metadata['retro'][player] else 'No'))
@ -1362,7 +1389,7 @@ class Spoiler(object):
outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player]) outfile.write('Crystals required for Ganon: %s\n' % self.metadata['ganon_crystals'][player])
outfile.write('Pyramid hole pre-opened: %s\n' % ( outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.metadata['open_pyramid'][player] else 'No')) 'Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
outfile.write('Map shuffle: %s\n' % outfile.write('Map shuffle: %s\n' %
('Yes' if self.metadata['mapshuffle'][player] else 'No')) ('Yes' if self.metadata['mapshuffle'][player] else 'No'))
outfile.write('Compass shuffle: %s\n' % outfile.write('Compass shuffle: %s\n' %

2
Gui.py
View File

@ -17,7 +17,7 @@ ModuleUpdate.update()
from worlds.alttp.AdjusterMain import adjust from worlds.alttp.AdjusterMain import adjust
from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.EntranceRandomizer import parse_arguments
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress 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 worlds.alttp.Rom import Sprite
from Utils import local_path, output_path, open_file from Utils import local_path, output_path, open_file

View File

@ -9,7 +9,8 @@ import zlib
import concurrent.futures import concurrent.futures
import pickle 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.Items import ItemFactory, item_table, item_name_groups
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \ from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
lookup_vanilla_location_to_entrance 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.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
from worlds.hk import *
import Patch import Patch
seeddigits = 20 seeddigits = 20
@ -96,6 +98,7 @@ def main(args, seed=None):
world.er_seeds = args.er_seeds.copy() world.er_seeds = args.er_seeds.copy()
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy() world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
world.required_medallions = args.required_medallions.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)} 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('') logger.info('')
for player in range(1, world.players + 1): for player in world.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])
for tok in filter(None, args.startinventory[player].split(',')): for tok in filter(None, args.startinventory[player].split(',')):
item = ItemFactory(tok.strip(), player) item = ItemFactory(tok.strip(), player)
if item: 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['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals'] 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]) world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
@ -175,7 +181,7 @@ def main(args, seed=None):
logger.info('Shuffling the World about.') 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 \ if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}: {"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
world.fix_fake_world[player] = False world.fix_fake_world[player] = False
@ -196,14 +202,19 @@ def main(args, seed=None):
logger.info('Generating Item Pool.') logger.info('Generating Item Pool.')
for player in range(1, world.players + 1): for player in world.alttp_player_ids:
generate_itempool(world, player) generate_itempool(world, player)
logger.info('Calculating Access Rules.') logger.info('Calculating Access Rules.')
for player in range(1, world.players + 1): for player in world.alttp_player_ids:
set_rules(world, player) 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") logger.info("Running Item Plando")
distribute_planned(world) distribute_planned(world)
@ -239,9 +250,7 @@ def main(args, seed=None):
if world.players > 1: if world.players > 1:
balance_multiworld_progression(world) balance_multiworld_progression(world)
logger.info('Patching ROM.') logger.info('Generating output files.')
outfilebase = 'AP_%s' % (args.outputname if args.outputname else world.seed) outfilebase = 'AP_%s' % (args.outputname if args.outputname else world.seed)
@ -349,7 +358,7 @@ def main(args, seed=None):
rom_futures = [] rom_futures = []
for team in range(world.teams): 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)) rom_futures.append(pool.submit(_gen_rom, team, player))
def get_entrance_to_region(region: Region): 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]: 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) 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', dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'}\ 'Inverted Ganons Tower': 'Ganons Tower'}\
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) .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_name = future.result()
rom_names.append(rom_name) rom_names.append(rom_name)
minimum_versions = {"server": (0, 0, 1)} 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, multidata = zlib.compress(pickle.dumps({"names": parsed_names,
"roms": {base64.b64encode(rom_name).decode(): (team, slot) for "connect_names": connect_names,
slot, team, rom_name in rom_names},
"remote_items": {player for player in range(1, world.players + 1) if "remote_items": {player for player in range(1, world.players + 1) if
world.remote_items[player]}, world.remote_items[player]},
"locations": { "locations": {
@ -509,8 +526,9 @@ def copy_world(world):
ret.shop_shuffle_slots = world.shop_shuffle_slots.copy() ret.shop_shuffle_slots = world.shop_shuffle_slots.copy()
ret.dark_room_logic = world.dark_room_logic.copy() ret.dark_room_logic = world.dark_room_logic.copy()
ret.restrict_dungeon_item_on_boss = world.restrict_dungeon_item_on_boss.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': if world.mode[player] != 'inverted':
create_regions(ret, player) create_regions(ret, player)
else: else:
@ -518,6 +536,9 @@ def copy_world(world):
create_shops(ret, player) create_shops(ret, player)
create_dungeons(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_dynamic_regions_and_locations(world, ret)
# copy bosses # copy bosses
@ -541,7 +562,7 @@ def copy_world(world):
# fill locations # fill locations
for location in world.get_locations(): for location in world.get_locations():
if location.item is not None: 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 ret.get_location(location.name, location.player).item = item
item.location = ret.get_location(location.name, location.player) item.location = ret.get_location(location.name, location.player)
item.world = ret item.world = ret
@ -552,7 +573,7 @@ def copy_world(world):
# copy remaining itempool. No item in itempool should have an assigned location # copy remaining itempool. No item in itempool should have an assigned location
for item in world.itempool: 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: for item in world.precollected_items:
ret.push_precollected(ItemFactory(item.name, item.player)) 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.prog_items = world.state.prog_items.copy()
ret.state.stale = {player: True for player in range(1, world.players + 1)} 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) set_rules(ret, player)
@ -584,7 +605,7 @@ def copy_dynamic_regions_and_locations(world, ret):
for location in world.dynamic_locations: for location in world.dynamic_locations:
new_reg = ret.get_region(location.parent_region.name, location.parent_region.player) 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 # todo: this is potentially dangerous. later refactor so we
# can apply dynamic region rules on top of copied world like other rules # can apply dynamic region rules on top of copied world like other rules
new_loc.access_rule = location.access_rule new_loc.access_rule = location.access_rule
@ -702,6 +723,7 @@ def create_playthrough(world):
old_world.spoiler.paths = dict() old_world.spoiler.paths = dict()
for player in range(1, world.players + 1): 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}) 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})
if player in world.alttp_player_ids:
for path in dict(old_world.spoiler.paths).values(): for path in dict(old_world.spoiler.paths).values():
if any(exit == 'Pyramid Fairy' for (_, exit) in path): if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':

View File

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

View File

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

View File

@ -14,8 +14,8 @@ ModuleUpdate.update()
from Utils import parse_yaml from Utils import parse_yaml
from worlds.alttp.Rom import Sprite from worlds.alttp.Rom import Sprite
from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.alttp.Main import main as ERmain from Main import main as ERmain
from worlds.alttp.Main import get_seed, seeddigits from Main import get_seed, seeddigits
from worlds.alttp.Items import item_name_groups, item_table from worlds.alttp.Items import item_name_groups, item_table
from worlds.alttp import Bosses from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable 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: if ret.name:
ret.name = handle_name(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) glitches_required = get_choice('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']: if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG and No Logic supported") logging.warning("Only NMG, OWG and No Logic supported")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import copy import copy
import os
import logging
import textwrap import textwrap
import shlex 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): class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@ -359,6 +352,7 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--names', default=defval('')) parser.add_argument('--names', default=defval(''))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1)) parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--outputpath') 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('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname') parser.add_argument('--outputname')
parser.add_argument('--create_diff', default=defval(False), action='store_true', help='''\ 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", "plando_items", "plando_texts", "plando_connections", "er_seeds",
'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', 'remote_items', 'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', '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']: 'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:

View File

@ -1,7 +1,8 @@
from collections import namedtuple from collections import namedtuple
import logging 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.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops
from worlds.alttp.Bosses import place_bosses from worlds.alttp.Bosses import place_bosses
from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.Dungeons import get_dungeon_item_pool
@ -243,7 +244,7 @@ def generate_itempool(world, player: int):
if world.goal[player] in ['triforcehunt', 'localtriforcehunt']: if world.goal[player] in ['triforcehunt', 'localtriforcehunt']:
region = world.get_region('Light World', player) 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) loc.access_rule = lambda state: state.has_triforce_pieces(state.world.treasure_hunt_count[player], player)
region.locations.append(loc) region.locations.append(loc)
@ -501,7 +502,7 @@ def create_dynamic_shop_locations(world, player):
if item is None: if item is None:
continue continue
if item['create_location']: 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) shop.region.locations.append(loc)
world.dynamic_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): def fill_prizes(world, attempts=15):
all_state = world.get_all_state(keys=True) 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) 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), 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), world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player),

View File

@ -16,7 +16,7 @@ def GetBeemizerItem(world, player, item):
def ItemFactory(items, player): def ItemFactory(items, player):
from BaseClasses import Item from worlds.alttp import ALttPItem
ret = [] ret = []
singleton = False singleton = False
if isinstance(items, str): if isinstance(items, str):
@ -24,7 +24,7 @@ def ItemFactory(items, player):
singleton = True singleton = True
for item in items: for item in items:
if item in item_table: if item in item_table:
ret.append(Item(item, *item_table[item], player)) ret.append(ALttPItem(item, *item_table[item], player))
else: else:
raise Exception(f"Unknown item {item}") 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), '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"} hint_blacklist = {"Triforce"}

View File

@ -1,8 +1,8 @@
import collections import collections
import typing 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): 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)) ret.exits.append(Entrance(player, exit, ret))
for location in locations: for location in locations:
address, player_address, crystal, hint_text = location_table[location] 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 return ret

View File

@ -16,7 +16,8 @@ import xxtea
import concurrent.futures import concurrent.futures
from typing import Optional 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.Shops import ShopType
from worlds.alttp.Dungeons import dungeon_music_addresses from worlds.alttp.Dungeons import dungeon_music_addresses
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address 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 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 not location.crystal:
if location.item is not None: 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 # 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.parent_region.dungeon.is_dungeon_item(location.item):
if location.item.bigkey: if location.item.bigkey:
itemid = 0x32 itemid = 0x32
if location.item.smallkey: elif location.item.smallkey:
itemid = 0x24 itemid = 0x24
if location.item.map: elif location.item.map:
itemid = 0x33 itemid = 0x33
if location.item.compass: elif location.item.compass:
itemid = 0x25 itemid = 0x25
if world.remote_items[player]: if world.remote_items[player]:
itemid = list(location_table.keys()).index(location.name) + 1 itemid = list(location_table.keys()).index(location.name) + 1
@ -1572,7 +1579,7 @@ def patch_rom(world, rom, player, team, enemized):
# set rom name # set rom name
# 21 bytes # 21 bytes
from worlds.alttp.Main import __version__ from Main import __version__
# TODO: Adjust Enemizer to accept AP and AD # 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 = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{team + 1}_{player}_{world.seed:09}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name))) rom.name.extend([0] * (21 - len(rom.name)))
@ -2007,7 +2014,7 @@ def write_strings(rom, world, player, team):
if dest.player != player: if dest.player != player:
if ped_hint: if ped_hint:
hint += f" for {world.player_names[dest.player][team]}!" 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" hint += f" in {world.player_names[dest.player][team]}'s world"
else: else:
hint += f" for {world.player_names[dest.player][team]}" hint += f" for {world.player_names[dest.player][team]}"

View File

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

View File

@ -1,110 +1,141 @@
from typing import Optional
from BaseClasses import Location, Item
from worlds.generic import World from worlds.generic import World
class ALTTPWorld(World): #class ALTTPWorld(World):
"""WIP""" # """WIP"""
def __init__(self, options, slot: int): # def __init__(self, options, slot: int):
self._region_cache = {} # self._region_cache = {}
self.slot = slot # self.slot = slot
self.shuffle = shuffle # self.shuffle = shuffle
self.logic = logic # self.logic = logic
self.mode = mode # self.mode = mode
self.swords = swords # self.swords = swords
self.difficulty = difficulty # self.difficulty = difficulty
self.difficulty_adjustments = difficulty_adjustments # self.difficulty_adjustments = difficulty_adjustments
self.timer = timer # self.timer = timer
self.progressive = progressive # self.progressive = progressive
self.goal = goal # self.goal = goal
self.dungeons = [] # self.dungeons = []
self.regions = [] # self.regions = []
self.shops = [] # self.shops = []
self.itempool = [] # self.itempool = []
self.seed = None # self.seed = None
self.precollected_items = [] # self.precollected_items = []
self.state = CollectionState(self) # self.state = CollectionState(self)
self._cached_entrances = None # self._cached_entrances = None
self._cached_locations = None # self._cached_locations = None
self._entrance_cache = {} # self._entrance_cache = {}
self._location_cache = {} # self._location_cache = {}
self.required_locations = [] # self.required_locations = []
self.light_world_light_cone = False # self.light_world_light_cone = False
self.dark_world_light_cone = False # self.dark_world_light_cone = False
self.rupoor_cost = 10 # self.rupoor_cost = 10
self.aga_randomness = True # self.aga_randomness = True
self.lock_aga_door_in_escape = False # self.lock_aga_door_in_escape = False
self.save_and_quit_from_boss = True # self.save_and_quit_from_boss = True
self.accessibility = accessibility # self.accessibility = accessibility
self.shuffle_ganon = shuffle_ganon # self.shuffle_ganon = shuffle_ganon
self.fix_gtower_exit = self.shuffle_ganon # self.fix_gtower_exit = self.shuffle_ganon
self.retro = retro # self.retro = retro
self.custom = custom # self.custom = custom
self.customitemarray: List[int] = customitemarray # self.customitemarray: List[int] = customitemarray
self.hints = hints # self.hints = hints
self.dynamic_regions = [] # self.dynamic_regions = []
self.dynamic_locations = [] # 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 class ALttPLocation(Location):
self.required_medallions = ['Ether', 'Quake'] game: str = "A Link to the Past"
self.swamp_patch_required = False
self.powder_patch_required = False def __init__(self, player: int, name: str = '', address=None, crystal: bool = False,
self.ganon_at_pyramid = True hint_text: Optional[str] = None, parent=None,
self.ganonstower_vanilla = True 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 class ALttPItem(Item):
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 game: str = "A Link to the Past"
def sewer_light_cone(self):
return self.mode == "standard"
@property 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 fix_trock_doors(self): super(ALttPItem, self).__init__(name, advancement, code, player)
return self.shuffle != 'vanilla' or self.mode == 'inverted' self.type = type
self._pedestal_hint_text = pedestal_hint
@property self.pedestal_credit_text = pedestal_credit
def fix_skullwoods_exit(self): self.sickkid_credit_text = sickkid_credit
return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'} self.zora_credit_text = zora_credit
self.magicshop_credit_text = witch_credit
@property self.fluteboy_credit_text = fluteboy_credit
def fix_palaceofdarkness_exit(self): self._hint_text = hint_text
return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}
@property
def fix_trock_exit(self):
return self.shuffle not in {'vanilla', 'simple', 'restricted', 'dungeonssimple'}

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

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

1018
worlds/hk/Locations.py Normal file

File diff suppressed because it is too large Load Diff

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

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