mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-28 03:53:23 -07:00
1.5 Update
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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 <message>` - Send a chat message to the server. Supports commands, such as `!hint` or `!release`.
|
||||
|
||||
`ap_deathlink` - Toggle Death Link.
|
||||
|
||||
`ap_set_connection_info <ip> <port>` - 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.
|
||||
## 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.
|
||||
Reference in New Issue
Block a user