From 6318a1aa89b4f55756e6916c0d3f98178807040c Mon Sep 17 00:00:00 2001 From: CookieCat Date: Wed, 8 May 2024 16:55:02 -0400 Subject: [PATCH] 1.5 Update --- worlds/ahit/Options.py | 14 +- worlds/ahit/Regions.py | 499 ++++++++++++++++++----------------- worlds/ahit/Rules.py | 2 +- worlds/ahit/__init__.py | 2 + worlds/ahit/docs/setup_en.md | 69 ++++- 5 files changed, 331 insertions(+), 255 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index eb4f833748..40b737468c 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -125,13 +125,24 @@ class ActRandomizer(Choice): class ActPlando(OptionDict): - """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" + """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\" will place Alpine Free Roam + at Train Rush.""" display_name = "Act Plando" schema = Schema({ Optional(str): str }) +class ActBlacklist(OptionDict): + """Blacklist acts from being shuffled onto other acts. Multiple can be listed per act. + For example, \"Barrel Battle\": [\"The Big Parade\", \"Dead Bird Studio\"] + will prevent The Big Parade and Dead Bird Studio from being shuffled onto Barrel Battle.""" + display_name = "Act Blacklist" + schema = Schema({ + Optional(str): list + }) + + class FinaleShuffle(Toggle): """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" display_name = "Finale Shuffle" @@ -619,6 +630,7 @@ class AHITOptions(PerGameCommonOptions): EndGoal: EndGoal ActRandomizer: ActRandomizer ActPlando: ActPlando + ActBlacklist: ActBlacklist ShuffleAlpineZiplines: ShuffleAlpineZiplines FinaleShuffle: FinaleShuffle LogicDifficulty: LogicDifficulty diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index aa26db17d2..9aa93ca65c 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -257,6 +257,9 @@ blacklisted_acts = { blacklisted_combos = { "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + + # Bon Voyage is here to prevent the cycle: Owl Express -> Bon Voyage -> Deep Sea -> MOTOE -> Owl Express + # which would make them all inaccessible since those rifts have no other entrances "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!", "Contractual Obligations"], @@ -266,7 +269,10 @@ blacklisted_combos = { "Time Rift - The Twilight Bell": ["Nyakuza Free Roam", "Contractual Obligations"], "Time Rift - Alpine Skyline": ["Nyakuza Free Roam", "Contractual Obligations"], "Time Rift - Rumbi Factory": ["Alpine Free Roam", "Contractual Obligations"], - "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + + # See above comment + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations", + "Murder on the Owl Express"], } @@ -425,17 +431,10 @@ def create_regions(world: "HatInTimeWorld"): def create_rift_connections(world: "HatInTimeWorld", region: Region): - i = 1 - for name in rift_access_regions[region.name]: + for i, name in enumerate(rift_access_regions[region.name]): act_region = world.multiworld.get_region(name, world.player) - entrance_name = f"{region.name} Portal - Entrance {i}" + entrance_name = f"{region.name} Portal - Entrance {i+1}" connect_regions(act_region, region, entrance_name, world.player) - i += 1 - - # fix for some weird keyerror from tests - if region.name == "Time Rift - Rumbi Factory": - for entrance in region.entrances: - world.multiworld.get_entrance(entrance.name, world.player) def create_tasksanity_locations(world: "HatInTimeWorld"): @@ -446,260 +445,87 @@ def create_tasksanity_locations(world: "HatInTimeWorld"): ship_shape.locations.append(location) -def is_valid_plando(world: "HatInTimeWorld", region: str, is_candidate: bool = False) -> bool: - # Duplicated keys will throw an exception for us, but we still need to check for duplicated values - if is_candidate: - found_list: List = [] - old_region = region - for name in world.options.ActPlando.keys(): - act = world.options.ActPlando.get(name) - if act == old_region: - region = name - found_list.append(name) - - if len(found_list) == 0: - return False - - if len(found_list) > 1: - raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - " - f"Duplicated act plando mapping found for act: \"{old_region}\"") - elif region not in world.options.ActPlando.keys(): - return False - - if region in blacklisted_acts.values() or (region not in act_entrances.keys() and "Time Rift" not in region): - return False - - act = world.options.ActPlando.get(region) - try: - world.multiworld.get_region(region, world.player) - world.multiworld.get_region(act, world.player) - except KeyError: - return False - - if act in blacklisted_acts.values() or (act not in act_entrances.keys() and "Time Rift" not in act): - return False - - # Don't allow plando-ing things onto the first act that aren't completable with nothing - is_first_act: bool = act_chapters[region] == get_first_chapter_region(world).name \ - and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region]) - - if is_first_act: - if act_chapters[act] == "Subcon Forest" and world.options.ShuffleSubconPaintings.value > 0: - return False - - if world.options.UmbrellaLogic.value > 0 \ - and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"): - return False - - if act not in guaranteed_first_acts: - return False - - # Don't allow straight up impossible mappings - if (region == "Time Rift - Curly Tail Trail" - or region == "Time Rift - The Twilight Bell" - or region == "The Illness has Spread") \ - and act == "Alpine Free Roam": - return False - - if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") \ - and act == "Nyakuza Free Roam": - return False - - if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": - return False - - if region == "Time Rift - Deep Sea" and act == "Bon Voyage!": - return False - - return any(a.name == world.options.ActPlando.get(region) for a in world.multiworld.get_regions(world.player)) - - def randomize_act_entrances(world: "HatInTimeWorld"): region_list: List[Region] = get_act_regions(world) world.random.shuffle(region_list) + region_list.sort(key=sort_acts) + candidate_list: List[Region] = region_list.copy() + rift_dict: Dict[str, Region] = {} - separate_rifts: bool = bool(world.options.ActRandomizer.value == 1) - - for region in region_list.copy(): - if (act_chapters[region.name] == "Alpine Skyline" or act_chapters[region.name] == "Nyakuza Metro") \ - and "Time Rift" not in region.name: - region_list.remove(region) - region_list.append(region) - - for region in region_list.copy(): - if region.name in chapter_finales: - region_list.remove(region) - region_list.append(region) - - for region in region_list.copy(): - if "Time Rift" in region.name: - region_list.remove(region) - region_list.append(region) - - for name in world.options.ActPlando.keys(): - try: - world.multiworld.get_region(name, world.player) - except KeyError: - print(f"[WARNING] ActPlando ({world.multiworld.get_player_name(world.player)}) - " - f"Act \"{name}\" does not exist in the multiworld." - f"Possible reasons are typos, case-sensitivity, or DLC options.") - - for region in region_list.copy(): - if region.name in world.options.ActPlando.keys(): + # Check if Plando's are valid, if so, map them + if len(world.options.ActPlando) > 0: + player_name = world.multiworld.get_player_name(world.player) + for (name1, name2) in world.options.ActPlando.items(): + region: Region + act: Region try: - act = world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player) + region = world.multiworld.get_region(name1, world.player) except KeyError: - print(f"[WARNING] ActPlando ({world.multiworld.get_player_name(world.player)}) - " - f"Act \"{world.options.ActPlando.get(region.name)}\" does not exist in the multiworld." + print(f"ActPlando ({player_name}) - " + f"Act \"{name1}\" does not exist in the multiworld. " + f"Possible reasons are typos, case-sensitivity, or DLC options.") + continue + + try: + act = world.multiworld.get_region(name2, world.player) + except KeyError: + print(f"ActPlando ({player_name}) - " + f"Act \"{name2}\" does not exist in the multiworld. " f"Possible reasons are typos, case-sensitivity, or DLC options.") continue if is_valid_plando(world, region.name) and is_valid_plando(world, act.name, True): region_list.remove(region) - region_list.append(region) - region_list.remove(act) - region_list.append(act) + candidate_list.remove(act) + connect_acts(world, region, act, rift_dict) else: - print(f"[WARNING] ActPlando " - f"({world.multiworld.get_player_name(world.player)}) - " - f"\"{region.name}: {world.options.ActPlando.get(region.name)}\" " + print(f"ActPlando " + f"({player_name}) - " + f"\"{name1}: {name2}\" " f"is an invalid or disallowed act plando combination!") - # Reverse the list, so we can do what we want to do first - region_list.reverse() - - shuffled_list: List[Region] = [] - mapped_list: List[Region] = [] - rift_dict: Dict[str, Region] = {} - first_chapter: Region = get_first_chapter_region(world) - has_guaranteed: bool = False - - i = 0 - while i < len(region_list): - region = region_list[i] - i += 1 - - # Get the first accessible act, so we can map that to something first - if not has_guaranteed: - if act_chapters[region.name] != first_chapter.name: - continue - - if region.name not in act_entrances.keys() or "Act 1" not in act_entrances[region.name] \ - and "Free Roam" not in act_entrances[region.name]: - continue - - if is_valid_plando(world, region.name): - has_guaranteed = True - - i = 0 - - # Already mapped to something else - if region in mapped_list: - continue - - mapped_list.append(region) - - # Look for candidates to map this act to - candidate_list: List[Region] = [] - for candidate in region_list: - # We're mapping something to the first act, make sure it is valid - if not has_guaranteed: - if candidate.name not in guaranteed_first_acts: - continue - - if is_valid_plando(world, candidate.name, True): - continue - - # Not completable without Umbrella - if world.options.UmbrellaLogic.value > 0 \ - and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): - continue - - # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either - if world.options.ShuffleSubconPaintings.value > 0 \ - and "Subcon Forest" in act_entrances[candidate.name]: - continue - - candidate_list.append(candidate) - has_guaranteed = True - break - - if is_valid_plando(world, region.name): - candidate_list.clear() - candidate_list.append( - world.multiworld.get_region(world.options.ActPlando.get(region.name), world.player)) - break - - # Already mapped onto something else - if candidate in shuffled_list: - continue - - if separate_rifts: - # Don't map Time Rifts to normal acts - if "Time Rift" in region.name and "Time Rift" not in candidate.name: - continue - - # Don't map normal acts to Time Rifts - if "Time Rift" not in region.name and "Time Rift" in candidate.name: - continue - - # Separate purple rifts - if region.name in purple_time_rifts and candidate.name not in purple_time_rifts \ - or region.name not in purple_time_rifts and candidate.name in purple_time_rifts: - continue - - if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]: - continue - - # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled - if world.options.ShuffleActContracts.value == 0: - if (region.name == "Your Contract has Expired" or region.name == "The Subcon Well") \ - and candidate.name == "Contractual Obligations": - continue - - if world.options.FinaleShuffle.value > 0 and region.name in chapter_finales: - if candidate.name not in chapter_finales: - continue - - if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: - continue - - candidate_list.append(candidate) + first_act_mapped: bool = False + ignore_certain_rules: bool = False + while len(region_list) > 0: + region: Region + if not first_act_mapped: + region = get_first_act(world) + else: + region = region_list[0] candidate: Region - if len(candidate_list) > 0: - candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)] + valid_candidates: List[Region] = [] + + # Look for candidates to map this act to + for c in candidate_list: + # Map the first act before anything + if not first_act_mapped: + if not is_valid_first_act(world, c): + continue + + valid_candidates.append(c) + first_act_mapped = True + break # we can stop here, as we only need one + + if is_valid_act_combo(world, region, c, bool(world.options.ActRandomizer.value == 1), ignore_certain_rules): + valid_candidates.append(c) + + if len(valid_candidates) > 0: + candidate = valid_candidates[world.random.randint(0, len(valid_candidates)-1)] else: - # plando can still break certain rules, so acts may not always end up shuffled. - for c in region_list: - if c not in shuffled_list: - candidate = c - break + # If we fail here, try again with less shuffle rules. If we still somehow fail, there's an issue for sure + if ignore_certain_rules: + raise Exception(f"Failed to find act shuffle candidate for {region}" + f"\nRemaining acts to map to: {region_list}" + f"\nRemaining candidates: {candidate_list}") - # noinspection PyUnboundLocalVariable - shuffled_list.append(candidate) - - # Vanilla - if candidate.name == region.name: - if region.name in rift_access_regions.keys(): - rift_dict.setdefault(region.name, candidate) - - update_chapter_act_info(world, region, candidate) + ignore_certain_rules = True continue - if region.name in rift_access_regions.keys(): - connect_time_rift(world, region, candidate) - rift_dict.setdefault(region.name, candidate) - else: - if candidate.name in rift_access_regions.keys(): - for e in candidate.entrances.copy(): - e.parent_region.exits.remove(e) - e.connected_region.entrances.remove(e) - - entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) - reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) - - update_chapter_act_info(world, region, candidate) + ignore_certain_rules = False + region_list.remove(region) + candidate_list.remove(candidate) + connect_acts(world, region, candidate, rift_dict) for name in blacklisted_acts.values(): if not is_act_blacklisted(world, name): @@ -711,6 +537,130 @@ def randomize_act_entrances(world: "HatInTimeWorld"): set_rift_rules(world, rift_dict) +# Try to do levels that may have specific mapping rules first +def sort_acts(act: Region) -> int: + if "Time Rift" in act.name: + return -5 + + if act.name in chapter_finales: + return -4 + + # Free Roam + if (act_chapters[act.name] == "Alpine Skyline" or act_chapters[act.name] == "Nyakuza Metro") \ + and "Time Rift" not in act.name: + return -3 + + if act.name == "Contractual Obligations": + return -2 + + world = act.multiworld.worlds[act.player] + blacklist = world.options.ActBlacklist + if len(blacklist) > 0: + for name, act_list in blacklist.items(): + if act.name == name or act.name in act_list: + return -1 + + return 0 + + +def get_first_act(world: "HatInTimeWorld") -> Region: + first_chapter = get_first_chapter_region(world) + act: Region + for e in first_chapter.exits: + if "Act 1" in e.name or "Free Roam" in e.name: + act = e.connected_region + break + + # noinspection PyUnboundLocalVariable + return act + + +def connect_acts(world: "HatInTimeWorld", entrance_act: Region, exit_act: Region, rift_dict: Dict[str, Region]): + # Vanilla + if exit_act.name == entrance_act.name: + if entrance_act.name in rift_access_regions.keys(): + rift_dict.setdefault(entrance_act.name, exit_act) + + update_chapter_act_info(world, entrance_act, exit_act) + return + + if entrance_act.name in rift_access_regions.keys(): + connect_time_rift(world, entrance_act, exit_act) + rift_dict.setdefault(entrance_act.name, exit_act) + else: + if exit_act.name in rift_access_regions.keys(): + for e in exit_act.entrances.copy(): + e.parent_region.exits.remove(e) + e.connected_region.entrances.remove(e) + + entrance = world.multiworld.get_entrance(act_entrances[entrance_act.name], world.player) + chapter = world.multiworld.get_region(act_chapters[entrance_act.name], world.player) + reconnect_regions(entrance, chapter, exit_act) + + update_chapter_act_info(world, entrance_act, exit_act) + + +def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region, + exit_act: Region, separate_rifts: bool, ignore_certain_rules=False) -> bool: + + # Ignore certain rules that aren't to prevent impossible combos. This is needed for ActPlando. + if not ignore_certain_rules: + if separate_rifts and not ignore_certain_rules: + # Don't map Time Rifts to normal acts + if "Time Rift" in entrance_act.name and "Time Rift" not in exit_act.name: + return False + + # Don't map normal acts to Time Rifts + if "Time Rift" not in entrance_act.name and "Time Rift" in exit_act.name: + return False + + # Separate purple rifts + if entrance_act.name in purple_time_rifts and exit_act.name not in purple_time_rifts \ + or entrance_act.name not in purple_time_rifts and exit_act.name in purple_time_rifts: + return False + + if world.options.FinaleShuffle.value > 0 and entrance_act.name in chapter_finales: + if exit_act.name not in chapter_finales: + return False + + if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]: + return False + + # Blacklisted? + if entrance_act.name in blacklisted_combos.keys() and exit_act.name in blacklisted_combos[entrance_act.name]: + return False + + if len(world.options.ActBlacklist) > 0: + act_blacklist = world.options.ActBlacklist.get(entrance_act.name) + if act_blacklist is not None and exit_act.name in act_blacklist: + return False + + # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled + if world.options.ShuffleActContracts.value == 0: + if (entrance_act.name == "Your Contract has Expired" or entrance_act.name == "The Subcon Well") \ + and exit_act.name == "Contractual Obligations": + return False + + return True + + +def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: + if act.name not in guaranteed_first_acts: + return False + + # Not completable without Umbrella + if world.options.UmbrellaLogic.value > 0 \ + and (act.name == "Heating Up Mafia Town" or act.name == "Queen Vanessa's Manor"): + return False + + # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either + if world.options.ShuffleSubconPaintings.value > 0 \ + and "Subcon Forest" in act_entrances[act.name]: + return False + + return True + + def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): count: int = len(rift_access_regions[time_rift.name]) i: int = 1 @@ -720,7 +670,10 @@ def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: R try: entrance = world.multiworld.get_entrance(name, world.player) except KeyError: - entrance = time_rift.entrances[0] + if len(time_rift.entrances) > 0: + entrance = time_rift.entrances[i-1] + else: + entrance = connect_regions(time_rift, exit_region, name, world.player) # noinspection PyUnboundLocalVariable reconnect_regions(entrance, entrance.parent_region, exit_region) @@ -753,6 +706,62 @@ def is_act_blacklisted(world: "HatInTimeWorld", name: str) -> bool: return name in blacklisted_acts.values() +def is_valid_plando(world: "HatInTimeWorld", region: str, is_candidate: bool = False) -> bool: + # Duplicated keys will throw an exception for us, but we still need to check for duplicated values + if is_candidate: + found_list: List = [] + old_region = region + for name, act in world.options.ActPlando.items(): + if act == old_region: + region = name + found_list.append(name) + + if len(found_list) == 0: + return False + + if len(found_list) > 1: + raise Exception(f"ActPlando ({world.multiworld.get_player_name(world.player)}) - " + f"Duplicated act plando mapping found for act: \"{old_region}\"") + elif region not in world.options.ActPlando.keys(): + return False + + if region in blacklisted_acts.values() or (region not in act_entrances.keys() and "Time Rift" not in region): + return False + + act = world.options.ActPlando.get(region) + try: + world.multiworld.get_region(region, world.player) + world.multiworld.get_region(act, world.player) + except KeyError: + return False + + if act in blacklisted_acts.values() or (act not in act_entrances.keys() and "Time Rift" not in act): + return False + + # Don't allow plando-ing things onto the first act that aren't completable with nothing + if act == get_first_act(world).name and not is_valid_first_act(world, act): + return False + + # Don't allow straight up impossible mappings + if (region == "Time Rift - Curly Tail Trail" + or region == "Time Rift - The Twilight Bell" + or region == "The Illness has Spread") \ + and act == "Alpine Free Roam": + return False + + if (region == "Rush Hour" or region == "Time Rift - Rumbi Factory") \ + and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": + return False + + if region == "Time Rift - Deep Sea" and act == "Bon Voyage!": + return False + + return any(a.name == world.options.ActPlando.get(region) for a in world.multiworld.get_regions(world.player)) + + def create_region(world: "HatInTimeWorld", name: str) -> Region: reg = Region(name, world.player, world.multiworld) diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 319c277dae..14263c5d4e 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -499,7 +499,7 @@ def set_hard_rules(world: "HatInTimeWorld"): # Hard: Goat Refinery from TIHS with nothing add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player), - lambda state: state.has("TIHS Access", world.player, "or")) + lambda state: state.has("TIHS Access", world.player), "or") if world.is_dlc1(): # Hard: clear Deep Sea without Dweller Mask diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 676b16b70c..3c050730b2 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -95,6 +95,8 @@ class HatInTimeWorld(World): if self.options.ActRandomizer.value == 0: if start_chapter == 4: self.multiworld.push_precollected(self.create_item("Hookshot Badge")) + if self.options.UmbrellaLogic.value > 0: + self.multiworld.push_precollected(self.create_item("Umbrella")) if start_chapter == 3 and self.options.ShuffleSubconPaintings.value > 0: self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 65ca612eb6..c07857ee37 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -10,34 +10,87 @@ 1. Have Steam running. Open the Steam console with [this link.](steam://open/console) + 2. In the Steam console, enter the following command: `download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** -This can take a while to finish (30+ minutes) so please be patient. +This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally, +**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,** +or else the download may potentially become corrupted (see first FAQ question below). + 3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. + 4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. -5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number **253230** on the first line. -6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. +5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. +In this new text file, input the number **253230** on the first line. -7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. + +6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. +You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. + + +7. Start up the game using your new shortcut. To confirm if you are on the correct version, +go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running +the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. ## Connecting to the Archipelago server -To connect to the multiworld server, simply run the **ArchipelagoAHITClient** and connect it to the Archipelago server. The game will connect to the client automatically when you create a new save file. +To connect to the multiworld server, simply run the **ArchipelagoAHITClient** +(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server. +The game will connect to the client automatically when you create a new save file. ## Console Commands -Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. +Commands will not work on the title screen, you must be in-game to use them. To use console commands, +make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. `ap_say ` - Send a chat message to the server. Supports commands, such as `!hint` or `!release`. `ap_deathlink` - Toggle Death Link. -`ap_set_connection_info ` - Usually not necessary. Set the connection info for the save file. **The IP address MUST be in double quotes!** -`ap_show_connection_info` - Show the connection info for the save file. \ No newline at end of file +## FAQ/Common Issues +### I followed the setup, but I receive an odd error message upon starting the game or creating a save file! +If you receive an error message such as +**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or +**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot +download was likely corrupted. The only way to fix this is to start the entire download all over again. +Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this +from happening is to ensure that your connection is not interrupted or slowed while downloading. + +### The game keeps crashing on startup after the splash screen! +This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however, +try the following: + +- Close Steam **entirely**. +- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen. +- Close the game, and then open Steam again. +- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does. + +### I followed the setup, but "Live Game Events" still shows up in the options menu! +The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by +default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file +extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect. +To show file extensions in Windows 10, open any folder, click the View tab at the top, +and make sure "File name extensions" is checked, and correct the name of the file. If the name of the file is correct, +and you're still running into the issue, re-read the setup guide again in case you missed a step. +If you still can't get it to work, ask for help in the Discord thread. + +### The game is running on the older version, but it's not connecting when starting a new save! +For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu +(rocket icon) in-game, and re-enable the mod. + +### Why do relics disappear from the stands in the Spaceship after they're completed? +This is intentional behaviour. Because of how randomizer logic works, there is no way to predict the order that +a player will place their relics. Since there are a limited amount of relic stands in the Spaceship, relics are removed +after being completed to allow for the placement of more relics without being potentially locked out. +The level that the relic set unlocked will stay unlocked. + +### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work! +There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly +if you have too many save files. Delete them and it should fix the problem. \ No newline at end of file