Files
dockipelago/worlds/glover/JsonReader.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

794 lines
37 KiB
Python

import json
import pkgutil
from BaseClasses import Entrance, Location, MultiWorld, Region
from typing import TYPE_CHECKING, Any, List, NamedTuple
from operator import attrgetter
from .Options import GaribLogic, DifficultyLogic
from .Rules import move_lookup, switches_to_event_items, access_methods_to_rules
from worlds.generic.Rules import add_rule, set_rule
if TYPE_CHECKING:
from . import GloverWorld
else:
GloverWorld = object
#Level name, followed by the region indexes of each checkpoint
levels_in_order = [
["AtlH", 0],
["Atl1", 0, 0],
["Atl2", 0, 4, 8],
["Atl3", 0, 3, 10],
["Atl!", 0],
["Atl?", 0],
["CrnH", 0],
["Crn1", 0, 2, 5, 9],
["Crn2", 0, 3, 6, 13, 15],
["Crn3", 0, 5, 9, 13],
["Crn!", 0],
["Crn?", 0],
["PrtH", 0],
["Prt1", 0, 11, 15],
["Prt2", 0, 3, 5],
["Prt3", 0, 5, 15, 19],
["Prt!", 0],
["Prt?", 0],
["PhtH", 0],
["Pht1", 0, 9, 12],
["Pht2", 0, 13, 18, 19],
["Pht3", 0, 5, 8, 11],
["Pht!", 0],
["Pht?", 0],
["FoFH", 0],
["FoF1", 0, 6, 13],
["FoF2", 0, 2, 7],
["FoF3", 0, 3, 4, 11, 17],
["FoF!", 0],
["FoF?", 0],
["OoWH", 0],
["OoW1", 0, 9],
["OoW2", 0],
["OoW3", 0, 2, 12, 17],
["OoW!", 0],
["OoW?", 0],
["Hubworld", 0],
["Castle Cave", 0],
["Training", 0]
]
file = pkgutil.get_data(__name__, "Logic.json").decode("utf-8")
#List of worlds, dictionary of levels, dictionary of locations and regions in those levels
#If it's a list, it's a location. Contains first entry AP_ID/ID, and all other entries methods
#If it's a dictionary, it's a region. B for Ball, D for No Ball, I for Local ID
#B/D in region has AP_ID/IDs, followed by methods as per Locations
logic_data : list[
dict[str,
dict[str,
dict[str,
list[
dict[str, any]] |
int] |
list[
dict[str, any]
]]]] = json.loads(file)
class AccessMethod(NamedTuple):
region_index : int
ball_in_region : bool
difficulty : int
required_items : list[str]
location_type_lookup = ["SWITCH", "GARIB", "LIFE", "CHECKPOINT", "POTION", "GOAL", "TIP", "LOADING_ZONE", "REGION", "MISC", "ENEMY", "INSECT"]
class LocationData(NamedTuple):
name : str
#The same index as in location type lookup
type : int
default_region : int
default_needs_ball : bool
ap_ids : List[int]
rom_ids : List[int]
methods : List[AccessMethod]
def remove_higher_difficulty_methods(self : GloverWorld, check_methods : list[AccessMethod]) -> list[AccessMethod]:
out_methods : list[AccessMethod] = []
#Find something of the correct difficulty based on logic methods
difficulty_cutoff : int = 3
match self.options.difficulty_logic:
case DifficultyLogic.option_intended:
difficulty_cutoff = 0
case DifficultyLogic.option_easy_tricks:
difficulty_cutoff = 1
case DifficultyLogic.option_hard_tricks:
difficulty_cutoff = 2
for each_method in check_methods:
#Difficulty Logic
if each_method.difficulty <= difficulty_cutoff:
out_methods.append(each_method)
return out_methods
def create_location_data(self : GloverWorld, check_name : str, check_info : list, level_name : str) -> list[LocationData]:
prefix = level_name + ": "
outputs : List[LocationData] = []
methods : List[AccessMethod] = []
for check_index in range(1, len(check_info)):
check_method = check_info[check_index]
new_method = create_access_method(self, check_method, level_name)
if new_method != None:
methods.append(new_method)
#Only create if there's a method for it
methods = remove_higher_difficulty_methods(self, methods)
if len(methods) == 0:
return outputs
ap_ids : list[int] = []
rom_ids : list[int] = []
for each_id in non_blank_ap_ids(check_info[0]["AP_IDS"]):
ap_ids.append(int(each_id, 0))
for each_id in non_blank_ap_ids(check_info[0]["IDS"]):
if each_id == "" or each_id == "N/A" or each_id == "?":
continue
if each_id.isdigit():
rom_ids.append(int(each_id, 0))
else:
rom_ids.append(-1)
#Is it an enemy?
enemy_with_garibs : bool = False
if check_info[0]["TYPE"] == 10:
#If so, are there garibs with this enemy?
enemy_with_garibs = len(ap_ids) > check_info[0]["COUNT"]
#When you have enemies with garibs
if enemy_with_garibs:
#Enemies get the first half of the array, garibs create the second
garib_ap_ids = []
garib_rom_ids = []
for _ in range(check_info[0]["COUNT"]):
garib_ap_ids.append(ap_ids.pop())
garib_rom_ids.append(rom_ids.pop())
garib_ap_ids.reverse()
garib_rom_ids.reverse()
garib_suffix = " Garib"
if check_info[0]["COUNT"] > 1:
garib_suffix += "s"
outputs.append(LocationData(prefix + check_name.removesuffix("s") + garib_suffix, 1, check_info[0]["REGION"], check_info[0]["NEEDS_BALL"], garib_ap_ids, garib_rom_ids, methods))
outputs.append(LocationData(prefix + check_name, 10, check_info[0]["REGION"], check_info[0]["NEEDS_BALL"], ap_ids, rom_ids, methods))
#All other checks
else:
outputs.append(LocationData(prefix + check_name, check_info[0]["TYPE"], check_info[0]["REGION"], check_info[0]["NEEDS_BALL"], ap_ids, rom_ids, methods))
return outputs
class RegionPair(NamedTuple):
name : str
base_id : int
ball_region_methods : list[AccessMethod]
no_ball_region_methods : list[AccessMethod]
ball_region_exists : bool
no_ball_region_exists : bool
def create_region_pair(self : GloverWorld, check_info : dict, check_name : str, level_name : str) -> RegionPair:
prefix = level_name + ": "
player : int = self.player
multiworld : MultiWorld = self.multiworld
region_name = prefix + check_name
ball_region_methods : list[AccessMethod] = []
no_ball_region_methods : list[AccessMethod] = []
base_id : int = check_info["I"]
ball_region = Region(region_name + " W/Ball", player, multiworld, level_name)
for index, check_pairing in enumerate(check_info["B"]):
#Skip the settings entry
if index == 0:
continue
#All other entries are methods
new_method = create_access_method(self, check_pairing, level_name)
if new_method != None:
ball_region_methods.append(new_method)
no_ball_region = Region(region_name, player, multiworld, level_name)
for index, check_pairing in enumerate(check_info["D"]):
#Skip the settings entry
if index == 0:
continue
new_method = create_access_method(self, check_pairing, level_name)
if new_method != None:
no_ball_region_methods.append(new_method)
#Ball regions that work
ball_region_exists : bool = False
ball_region_methods = remove_higher_difficulty_methods(self, ball_region_methods)
if len(ball_region_methods) > 0:
multiworld.regions.append(ball_region)
ball_region_exists = True
ball_region_methods = list(filter(lambda a, b = base_id, c = True: self_ref_region(a, b, c), ball_region_methods))
#No ball regions that exist
no_ball_region_exists : bool = False
no_ball_region_methods = remove_higher_difficulty_methods(self, no_ball_region_methods)
if len(no_ball_region_methods) > 0:
multiworld.regions.append(no_ball_region)
no_ball_region_exists = True
no_ball_region_methods = list(filter(lambda a, b = base_id, c = True: self_ref_region(a, b, c), no_ball_region_methods))
return RegionPair(region_name, base_id, ball_region_methods, no_ball_region_methods, ball_region_exists, no_ball_region_exists)
def self_ref_region(check_method : AccessMethod, base_region : int, base_ball : bool) -> bool:
return len(check_method.required_items) > 0 or check_method.region_index != base_region or check_method.ball_in_region != base_ball
def connect_region_pairs(self : GloverWorld, pairs : List[RegionPair]):
multiworld : MultiWorld = self.multiworld
player : int = self.player
for each_pair in pairs:
#If there's a ball region
if each_pair.ball_region_exists:
#Construct it
ball_region = multiworld.get_region(each_pair.name + " W/Ball", player)
#Gather methods
pair_ball_connections : dict[Region, list[AccessMethod]] = {}
for each_method in each_pair.ball_region_methods:
region_to_connect : Region = get_region_from_method(multiworld, player, pairs, each_method)
if region_to_connect == None:
continue
if not region_to_connect in pair_ball_connections.keys():
pair_ball_connections[region_to_connect] = [each_method]
#Create the rules using methods
for each_connection, methods in pair_ball_connections.items():
entrance : Entrance | Any = each_connection.connect(ball_region)
access_methods_to_rules(self, methods, entrance)
#If there's a no ball region
if each_pair.no_ball_region_exists:
#Construct it
pair_no_ball_connections : dict[Region, list[AccessMethod]] = {}
for each_method in each_pair.no_ball_region_methods:
region_to_connect : Region = get_region_from_method(multiworld, player, pairs, each_method)
if region_to_connect == None:
continue
if not region_to_connect in pair_no_ball_connections.keys():
pair_no_ball_connections[region_to_connect] = [each_method]
no_ball_region = multiworld.get_region(each_pair.name, player)
#Create the rules using methods
for each_connection, methods in pair_no_ball_connections.items():
entrance : Entrance | Any = each_connection.connect(no_ball_region)
access_methods_to_rules(self, methods, entrance)
#Ball region always leads to no ball region
if each_pair.ball_region_exists and each_pair.no_ball_region_exists:
ball_region.connect(no_ball_region)
class RegionLevel(NamedTuple):
name : str
#region : Region | None
starting_checkpoint : int
map_regions : List[RegionPair]
start_without_ball : bool
def create_region_level(self : GloverWorld, level_name : str, checkpoint_for_use : int | None, checkpoint_entry_pairs : list, map_regions : List[RegionPair]):
#By default, the region level leads to the first checkpoint's region
multiworld : MultiWorld = self.multiworld
player : int = self.player
default_checkpoint : int = 1
region : Region = Region(level_name, player, multiworld)
#Get the checkpoint from the core levels
if type(checkpoint_for_use) is int:
default_checkpoint = checkpoint_for_use
#See if you get the ball with the checkpoint
start_without_ball : bool = level_name[3:4] == "1" or level_name[:3] == "OoW"
#If you can get the ball in the level, you can use it to checkpoint warp with it to a later spot.
ball_regions : list[Region] = []
if start_without_ball:
ball_access_location = region.add_event(level_name + ": Ball", level_name + " Ball").location
for region_index, each_region_pair in enumerate(map_regions):
if each_region_pair.ball_region_exists:
ball_region = multiworld.get_region(each_region_pair.name + " W/Ball", player)
ball_regions.append(ball_region)
if region_index == 0:
set_rule(ball_access_location, lambda state, in_player = player, ball_reg = ball_region.name : state.can_reach_region(ball_reg, in_player))
else:
add_rule(ball_access_location, lambda state, in_player = player, ball_reg = ball_region.name : state.can_reach_region(ball_reg, in_player), "or")
#Assign entrances required by checkpoints here
for checkpoint_number in range(1, len(checkpoint_entry_pairs)):
#Create a checkpoint connection
checkpoint_name : str = level_name + " Checkpoint " + str(checkpoint_number)
for each_region_pair in map_regions:
#If the checkpoint leads to that region
if each_region_pair.base_id == checkpoint_entry_pairs[checkpoint_number]:
is_default = default_checkpoint == checkpoint_number
#If you normally spawn without the ball, the logic goes here
if start_without_ball:
if each_region_pair.no_ball_region_exists:
connecting_region : Region = multiworld.get_region(each_region_pair.name, player)
checkpoint_bridge(self, region, connecting_region, checkpoint_name, is_default)
#You can theoretically always spawn with the ball, if you can reach it
if each_region_pair.ball_region_exists:
connecting_region : Region = multiworld.get_region(each_region_pair.name + " W/Ball", player)
checkpoint_bridge(self, region, connecting_region, checkpoint_name, is_default, start_without_ball, ball_regions)
multiworld.regions.append(region)
return RegionLevel(level_name, default_checkpoint, map_regions, start_without_ball)
def checkpoint_bridge(self : GloverWorld, region : Region, connecting_region : Region, checkpoint_name : str, is_default : bool, requires_ball_access = False, ball_regions : list[Region] = []):
name_suffix : str = ""
if requires_ball_access:
name_suffix += " W/Ball"
entrance : Entrance | Any = region.connect(connecting_region, checkpoint_name + name_suffix)
requirements : list[str] = []
#The default checkpoint's always useable
if not is_default:
requirements.append(checkpoint_name)
if requires_ball_access:
ball_access = checkpoint_name.split(" ")[0]+ " Ball"
requirements.append(ball_access)
#Notify that getting the ball can let you use the checkpoint for the region sweeper
for each_ball_region in ball_regions:
self.multiworld.register_indirect_condition(each_ball_region, entrance)
if len(requirements) > 0:
#Other ones need the checkpoint item
set_rule(entrance, lambda state, reqs = requirements, in_player = self.player : state.has_all(reqs, in_player))
def create_access_method(self, info : dict, level_name : str) -> AccessMethod:#
required_moves : list = []
for each_key, each_result in info.items():
if each_key.startswith("mv"):
each_move = move_lookup[each_result]
required_moves.append(each_move)
if each_key.startswith("ck"):
required_moves.append(level_name + " " + each_result)
#Remove Power Ball methods
if "Power Ball" in required_moves and (not self.options.include_power_ball and self.starting_ball != "Power Ball"):
return None
#Combine Not Bowling and Not Crystal into Not Bowling Or Crystal
if "Not Bowling" in required_moves and "Not Crystal" in required_moves:
required_moves.remove("Not Bowling")
required_moves.remove("Not Crystal")
required_moves.append("Not Bowling or Crystal")
#Make sure there's a jump required for double jumps
if not "Jump" in required_moves and ("Double Jump" in required_moves):
required_moves.append("Jump")
#Here's the access method!
return AccessMethod(info["regionIndex"], info["ballRequirement"], info["trickDifficulty"], required_moves)
def assign_locations_to_regions(self : GloverWorld, region_level : RegionLevel, map_regions : List[RegionPair], location_data_list : List[LocationData], target_score : int):
player : int = self.player
multiworld : MultiWorld = self.multiworld
score_locations : list[Location] = []
for each_location_data in location_data_list:
#Should this location be generated?
ap_ids : list[int] = each_location_data.ap_ids
match each_location_data.type:
case 0:
#Switches
if not self.options.switches_checks:
ap_ids.clear()
case 3:
#Checkpoints don't give their starting location
if region_level.starting_checkpoint == int(each_location_data.name[-1]):
continue
#As Events
if not self.options.checkpoint_checks:
ap_ids.clear()
case 6:
#Tip hints
if self.options.mr_hints:
self.tip_locations[each_location_data.name] = ap_ids[0]
#Tips
if not self.options.mr_tip_checks:
continue
case 7:
#Loading Zones
if not self.options.bonus_levels:
#Bonus loading zones
if each_location_data.name.endswith("Entry Bonus"):
continue
#case 9:
#Misc
case 10:
#Enemysanity
if not self.options.enemysanity:
ap_ids.clear()
case 11:
#Insectity
if not self.options.insectity:
ap_ids.clear()
rules_applied : bool = False
#Is this a mono location?
location_regions : dict[str, list[AccessMethod]] = {}
for each_method in each_location_data.methods:
region_index = each_method.region_index
region_exists : bool = False
region_name : str
for each_pair in map_regions:
#If you're in the right region
if region_index == each_pair.base_id:
#That's a valid region name
region_name = each_pair.name
if each_method.ball_in_region:
region_name = region_name + " W/Ball"
region_exists = each_pair.ball_region_exists
else:
region_exists = each_pair.no_ball_region_exists
#If the region exists, assign it to the location regions
if region_exists:
if not region_name in location_regions:
location_regions[region_name] = []
location_regions[region_name].append(each_method)
#Depending on if it is or not
region_for_use : Region
if len(location_regions) > 1:
#Multi location construction creates a shared region to reach this location
region_for_use = Region(each_location_data.name + " Region", player, multiworld, region_level.name)
#Apply the rules here
rules_applied = True
for each_region_name, each_region_methods in location_regions.items():
#Only use the methods relevant to this entrance from the target region
entrance : Entrance | Any = multiworld.get_region(each_region_name, player).connect(region_for_use, each_location_data.name + " from " + each_region_name)
access_methods_to_rules(self, each_region_methods, entrance)
elif len(location_regions) == 1:
#Single location construction assigns to the specific element in the RegionPair
region_for_use = multiworld.get_region(list(location_regions.keys())[0], player)
else:
#If there are no methods, continue
continue
#Construct location data
if each_location_data.type == 1:
#Garibsanity
if self.options.garib_logic == GaribLogic.option_garibsanity:
for each_garib_index in range(len(each_location_data.ap_ids)):
each_garib = each_location_data.ap_ids[each_garib_index]
location : Location = Location(player, self.location_id_to_name[each_garib], each_garib, region_for_use)
region_for_use.locations.append(location)
if not rules_applied:
access_methods_to_rules(self, each_location_data.methods, location)
#Garibs give score
score_locations.append(location)
#Garib Groups
elif self.options.garib_logic == GaribLogic.option_garib_groups:
#Regular Locations
group_offset : int = each_location_data.ap_ids[0]
if len(ap_ids) > 1:
group_offset += 10000
location : Location = Location(player, each_location_data.name, group_offset, region_for_use)
region_for_use.locations.append(location)
if not rules_applied:
access_methods_to_rules(self, each_location_data.methods, location)
#Garibs give score
score_locations.append(location)
#All Garibs in Level
else:
#It's an event location, with an event item
location : Location = Location(player, each_location_data.name, None, region_for_use)
region_for_use.locations.append(location)
if not rules_applied:
access_methods_to_rules(self, each_location_data.methods, location)
#These are used to create star gate unlock logic
location.place_locked_item(self.create_event(each_location_data.name + " Reached"))
score_locations.append(location)
else:
#Regular Locations
address : int | None = None
#Single AP Item
if len(each_location_data.ap_ids) == 1:
address = each_location_data.ap_ids[0]
location : Location = Location(player, each_location_data.name, address, region_for_use)
region_for_use.locations.append(location)
if not rules_applied:
access_methods_to_rules(self, each_location_data.methods, location)
#Lives, Potions and Enemies give Score
if each_location_data.type in [2, 4, 10]:
score_locations.append(location)
score_locations.append(location)
#Multiple AP Items
elif len(each_location_data.ap_ids) > 1:
for each_index in range(len(each_location_data.ap_ids)):
each_ap_id = each_location_data.ap_ids[each_index]
location_name : str = each_location_data.name.removesuffix("s")
location_name += " " + str(each_index + 1)
location : Location = Location(player, location_name, each_ap_id, region_for_use)
region_for_use.locations.append(location)
if not rules_applied:
access_methods_to_rules(self, each_location_data.methods, location)
#Lives, Potions and Enemies give Score
if each_location_data.type in [2, 4, 10]:
score_locations.append(location)
score_locations.append(location)
else:
#Event Item
location : Location = Location(player, each_location_data.name, address, region_for_use)
region_for_use.locations.append(location)
if not rules_applied:
access_methods_to_rules(self, each_location_data.methods, location)
match each_location_data.type:
#Switches with no paired level event store their level event
#items as event items rather than AP items.
case 0:
new_event_item : str = switches_to_event_items[each_location_data.name]
location.place_locked_item(self.create_event(new_event_item))
#Enemies contribute to score even if disabled
case 10:
new_event_item : str = each_location_data.name.replace(":", "")
location.place_locked_item(self.create_event(new_event_item))
score_locations.append(location)
#Checkpoints & Level Warps
case _:
new_event_item : str = each_location_data.name.replace(":", "")
location.place_locked_item(self.create_event(new_event_item))
#Create score addresses anywhere that score is something you can get
#(AKA any non-boss level, excluding Space Boss)
world_index = 1 + self.world_from_string(region_level.name)
level_index = self.level_from_string(region_level.name)
if (world_index == 6 or level_index != 4) and level_index != 0:
#Score location at root
score_address = None
if target_score != 0:
score_address = ((world_index * 10) + level_index) * 100000
level_root = self.get_region(region_level.name)
score_location = Location(player, region_level.name + ": Score", score_address, level_root)
if score_address == None:
score_location.place_locked_item(self.create_event(region_level.name + " Score"))
level_root.locations.append(score_location)
for each_index, each_location in enumerate(score_locations):
if each_index == 0:
set_rule(score_location, lambda state, plr = player, scl = each_location: state.can_reach(scl, plr))
else:
add_rule(score_location, lambda state, plr = player, scl = each_location: state.can_reach(scl, plr), "or")
def get_region_from_method(multiworld : MultiWorld, player : int, region_pairs : List[RegionPair], method : AccessMethod) -> Region:
for each_pair in region_pairs:
lookup_name = each_pair.name
if method.ball_in_region:
lookup_name += " W/Ball"
if each_pair.base_id == method.region_index:
if method.ball_in_region:
if not each_pair.ball_region_exists:
return None
elif not each_pair.no_ball_region_exists:
return None
return multiworld.get_region(lookup_name, player)
raise IndexError(region_pairs[0].name.split(':')[0] + " method calls for region indexed " + str(method.region_index) + " that does not exist!")
def build_data(self : GloverWorld) -> List[RegionLevel]:
all_levels : List[RegionLevel] = []
#Build Logic
loc_con_index = 0
for world_index, each_world in enumerate(logic_data):
world_prefix : str = create_world_prefix(self.world_prefixes, world_index)
#Go over the Glover worlds
for level_index, level_key in enumerate(each_world):
if level_index == 5 and not self.options.bonus_levels:
loc_con_index += 1
continue
each_level = each_world[level_key]
checkpoint_entry_pairs : list = levels_in_order[loc_con_index]
loc_con_index += 1
level_prefix = create_level_prefix(self.level_prefixes, world_index, level_index)
level_name : str = world_prefix + level_prefix
prefix : str = level_name + ": "
map_regions : List[RegionPair] = []
location_data_list : List[LocationData] = []
#Bonus levels
if not (level_index == 5 and not self.options.bonus_levels):
for check_name in each_level:
check_info = each_level[check_name]
#Location
if type(check_info) is list:
location_data_list.extend(create_location_data(self, check_name, check_info, level_name))
#In-Level Region
if type(check_info) is dict:
new_region_pair = create_region_pair(self, check_info, check_name, level_name)
map_regions.append(new_region_pair)
#Sort the in-level regions
map_regions = sorted(map_regions, key=attrgetter('base_id'))
connect_region_pairs(self, map_regions)
#Create the level info attached to it
checkpoint_for_use : int | None = None
if level_index > 0 and level_index < 4 and world_index < 6:
checkpoint_for_use = self.spawn_checkpoint[(world_index * 3) + (level_index - 1)]
region_level : RegionLevel = create_region_level(self, level_name, checkpoint_for_use, checkpoint_entry_pairs, map_regions)
#Target Score
target_score = 0
if level_name in self.options.level_scores.value:
target_score = self.options.level_scores.value[level_name]
#Attach the locations to the regions
assign_locations_to_regions(self, region_level, map_regions, location_data_list, target_score)
#Aside from the wayrooms, the hubworld and the castle cave, levels have star marks
if (world_index < 6 and not level_index == 0) or level_index == 2:
create_star_mark(self, level_index, world_index, prefix, location_data_list)
#Append it to the level list
all_levels.append(region_level)
return all_levels
def create_star_mark(self, level_index : int, world_index : int, prefix : str, location_data_list : List[LocationData]):
player : int = self.player
#Does this location have a star mark item, or a random one?
star_mark_ap_id : int | None = None
if self.options.portalsanity:
#They contain a random item
star_mark_ap_id = 30000 + level_index + (world_index * 10)
secondary_condition : str
#Does it unlock via garibs?
level_has_garibs : bool = level_index != 4 and world_index < 6
if level_has_garibs:
#Yes
secondary_condition = "All Garibs"
else:
#No
secondary_condition = "Completion"
#Which means, don't give them a second item here
star_mark_ap_id = None
#Otherwise, they contain the star marks
menu_region : Region = self.multiworld.get_region("Menu", player)
star_mark_location = Location(player, prefix + secondary_condition, star_mark_ap_id, menu_region)
#Boss levels and the well just give you it if you reach the goal
if not level_has_garibs:
goal_location : Location
#Boss levels look for the location 'Boss'
if level_index == 4:
goal_location = self.multiworld.get_location(prefix + "Boss", player)
else:
#The tutorial well looks for the locaiton 'Goal'
goal_location = self.multiworld.get_location(prefix + "Goal", player)
set_rule(star_mark_location, lambda state, for_completion = goal_location: state.can_reach(for_completion, player))
#Level garibs means you take all garib methods from before
elif self.options.garib_logic == GaribLogic.option_level_garibs:
all_garibs_rule(self, star_mark_location, location_data_list)
menu_region.locations.append(star_mark_location)
def all_garibs_rule(self, star_mark_location : Location, location_data_list : List[LocationData]):
#If it's all garibs in level logic, logic construction looks diffrent
garib_location_names : List[str] = []
for each_garib_location in location_data_list:
#Skip non-garibs obviously
if not each_garib_location.type == 1:
continue
garib_location_names.append(each_garib_location.name + " Reached")
set_rule(star_mark_location, lambda state, required_garibs = garib_location_names: state.has_all(required_garibs, self.player))
def build_location_pairings(base_name : str, check_info : dict, ap_ids : list[str]) -> list[list]:
#Nothing at all
if len(ap_ids) == 0:
return []
#If the location data accounts for 1 location
if len(ap_ids) == 1:
return [[base_name, ap_ids[0]]]
#A single enemy
if len(ap_ids) == 2 and check_info["TYPE"] == 10:
if check_info["COUNT"] == 1:
return [[base_name, ap_ids[0]], [base_name + " Garib", ap_ids[1]]]
output : list[list] = []
#If the location accounts for multiple locations
for each_ap_id_index, each_ap_id in enumerate(ap_ids):
sublocation_name : str = base_name.removesuffix("s")
#Not enemies
if check_info["TYPE"] != 10:
sublocation_name += " " + str(each_ap_id_index + 1)
else:
#Enemies
if each_ap_id_index < check_info["COUNT"]:
sublocation_name += " " + str(each_ap_id_index + 1)
else:
sublocation_name += " Garib " + str(each_ap_id_index + 1 - check_info["COUNT"])
output.append([sublocation_name, each_ap_id])
return output
def non_blank_ap_ids(ap_ids : list[str]) -> list[str]:
return list(filter(lambda a: a != "", ap_ids))
def create_world_prefix(world_prefixes : list[str], index : int) -> str:
if index < 6:
return world_prefixes[index]
else:
return ""
def create_level_prefix(level_prefixes : list[str], world_index : int, level_index : int) -> str:
if world_index == 6:
match level_index:
case 0:
return "Hubworld"
case 1:
return "Castle Cave"
case 2:
return "Training"
else:
return level_prefixes[level_index]
def generate_location_information(world_prefixes : list[str], level_prefixes : list[str]) -> list:
location_name_to_id : dict = {}
#Setup the location types here
location_name_groups : dict = {}
for each_type in location_type_lookup:
if each_type == "LOADING_ZONE" or each_type == "REGION":
continue
group_name = each_type.title()
location_name_groups[group_name] = []
location_name_groups["Score"] = []
location_name_groups["Crystals"] = []
#Each World
for each_world_index, each_world in enumerate(logic_data):
world_prefix : str = create_world_prefix(world_prefixes, each_world_index)
for level_key, level_data in each_world.items():
level_prefix : str = create_level_prefix(level_prefixes, each_world_index, int(level_key[-1]))
level_name = world_prefix + level_prefix
prefix : str = level_name + ": "
if prefix == "Castle Cave: ":
continue
location_name_groups[level_name] = []
for location_name in level_data:
#Not regions
if type(level_data[location_name]) is dict:
continue
#Only locations remain
ap_ids : list[str] = level_data[location_name][0]["AP_IDS"]
ap_ids = non_blank_ap_ids(ap_ids)
for each_pairing in build_location_pairings(prefix + location_name, level_data[location_name][0], ap_ids):
#Name to ID
location_name_to_id[each_pairing[0]] = int(each_pairing[1], 0)
#Name Groups
location_name_groups[level_name].append(each_pairing[0])
location_type : str = location_type_lookup[level_data[location_name][0]["TYPE"]]
location_name_groups[location_type.title()].append(each_pairing[0])
#Garib Groups
if level_data[location_name][0]["TYPE"] == 1 and len(ap_ids) > 1:
group_id : int = int(ap_ids[0], 0) + 10000
location_name_to_id[prefix + location_name] = group_id
#Enemy Garib Groups
if level_data[location_name][0]["TYPE"] == 10:
enemy_count = level_data[location_name][0]["COUNT"]
if enemy_count < len(ap_ids) and enemy_count > 1:
group_id : int = int(ap_ids[enemy_count], 0) + 10000
location_name_to_id[prefix + location_name.removesuffix("s") + " Garibs"] = group_id
#Levels with garibs in them
if each_world_index < 6:
match level_key:
case "l1":
location_name_to_id[prefix + "All Garibs"] = 30000 + (each_world_index * 10) + 1
case "l2":
location_name_to_id[prefix + "All Garibs"] = 30000 + (each_world_index * 10) + 2
case "l3":
location_name_to_id[prefix + "All Garibs"] = 30000 + (each_world_index * 10) + 3
case "l5":
location_name_to_id[prefix + "All Garibs"] = 30000 + (each_world_index * 10) + 5
#Scores
for world_index, world_prefix in enumerate(world_prefixes, 1):
for level_index, level_prefix in enumerate(level_prefixes):
level_score_address = 100000 * ((world_index * 10) + level_index)
if (level_index != 4 or world_index == 6) and level_index != 0:
level_name = world_prefix + level_prefix
prefix = level_name + ": "
location_name_to_id[prefix + "Score"] = level_score_address
location_name_groups["Score"].append(prefix + "Score")
location_name_groups[level_name].append(prefix + "Score")
for each_score in range(10000, 100000000, 10000):
location_name_to_id[str(each_score) + " Score"] = 100000000 + each_score
location_name_groups["Score"].append(str(each_score) + " Score")
for each_crystal in range(1,8):
#for each_loc, each_id in location_name_groups.items():
# if each_id == 1945 + each_crystal:
# each_loc
turn_in_name = "Ball Turn-In " + str(each_crystal)
location_name_to_id[turn_in_name] = 1945 + each_crystal
location_name_groups["Crystals"].append(turn_in_name)
return [location_name_to_id, location_name_groups]