Skip to content
Open

Custom #3074

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 269 additions & 2 deletions __init__.py

Large diffs are not rendered by default.

77 changes: 69 additions & 8 deletions archipelago/DK64Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ class DK64Client:
item_names = None
players = None
_purchase_cache = {}
custom_check_id_to_name = {}
custom_check_id_to_flag = {}
custom_flag_to_check_id = {} # Reverse mapping: flag_id -> location_id
ENABLE_DEATHLINK = False
ENABLE_RINGLINK = False
ENABLE_TAGLINK = False
Expand Down Expand Up @@ -696,14 +699,25 @@ def getCheckStatus(self, check_type, flag_index=None, shop_index=None, level_ind
async def readChecks(self, cb):
"""Run checks in parallel using asyncio."""
new_checks = []
checks_to_read = self.remaining_checks
checks_to_read = self.remaining_checks.copy()

for id in checks_to_read:
name = check_id_to_name.get(id)
# Try to get the check via location_name_to_flag
check = location_name_to_flag.get(name)
# Get location name (prefer custom name if available)
name = self.custom_check_id_to_name.get(id, check_id_to_name.get(id))

# Determine which flag to check for this location
check = None

# For custom locations (crowns, dirt, crates), ONLY use the custom flag
# Do not fall back to location_name_to_flag as that would use vanilla flags
if id in self.custom_check_id_to_flag:
check = self.custom_check_id_to_flag.get(id)
else:
# For non-custom locations, use the vanilla flag mapping
check = location_name_to_flag.get(name)

if check:
# Assuming we did find it in location_name_to_flag
# Check if this flag is set in memory
check_status = self.getCheckStatus("location", check)
if check_status:
self.remaining_checks.remove(id)
Expand Down Expand Up @@ -1076,7 +1090,8 @@ def reset_checks(self):
# Debug logging for shared shops
shared_shop_ids = set()
for location_id in actual_checks:
location_name = check_id_to_name.get(location_id, "")
# Use custom name if available, otherwise fall back to check_id_to_name
location_name = self.custom_check_id_to_name.get(location_id, check_id_to_name.get(location_id, ""))
if "Shared" in location_name:
shared_shop_ids.add(location_id)
else:
Expand All @@ -1086,6 +1101,7 @@ def reset_checks(self):
self.client.pending_checks = []
self.found_checks = []
self.client.flag_lookup = None
self.custom_flag_to_check_id = {}
self.handled_scouts = []
self.create_hints_params = []

Expand All @@ -1094,6 +1110,8 @@ def __init__(self, server_address: typing.Optional[str], password: typing.Option
self.client = DK64Client()
self.client.game = self.game.upper()
self.slot_data = {}
self.custom_check_id_to_name = {}
self.custom_check_id_to_flag = {}
self.reset_checks()

super().__init__(server_address, password)
Expand Down Expand Up @@ -1137,6 +1155,46 @@ async def send_checks(self):

had_invalid_slot_data: typing.Optional[bool] = None

def update_custom_location_names(self):
"""Update the check_id_to_name dictionary with custom location names from slot_data."""
custom_location_data = self.slot_data.get("CustomLocationNames", {})
if custom_location_data:
# Convert string keys back to integers and extract both name and flag
self.custom_check_id_to_name = {}
self.custom_check_id_to_flag = {}
for id_str, data in custom_location_data.items():
loc_id = int(id_str)
if isinstance(data, dict):
self.custom_check_id_to_name[loc_id] = data.get("name")
if data.get("flag") is not None:
flag_id = data.get("flag")
self.custom_check_id_to_flag[loc_id] = flag_id
else:
# Backwards compatibility: if data is just a string (old format)
self.custom_check_id_to_name[loc_id] = data

# Also update the client's dictionaries
self.client.custom_check_id_to_name = self.custom_check_id_to_name
self.client.custom_check_id_to_flag = self.custom_check_id_to_flag

# Update the location_names lookup used by Archipelago's notification system
# We need to inject custom names so they take priority over base names
if hasattr(self, "location_names") and self.location_names:
try:
# Get the game's location store ChainMap
if self.game in self.location_names._game_store:
game_store = self.location_names._game_store[self.game]

# Create our custom names dict
custom_names_dict = {loc_id: name for loc_id, name in self.custom_check_id_to_name.items() if name}

# Use new_child() to prepend our custom names to the existing ChainMap
# This modifies the ChainMap in place and ensures any existing references see the update
updated_chain = game_store.new_child(custom_names_dict)
self.location_names._game_store[self.game] = updated_chain
except Exception as e:
logger.error(f"Failed to update location_names: {e}", exc_info=True)

def event_invalid_slot(self):
"""Handle an invalid slot event."""
# The next time we try to connect, reset the game loop for new auth
Expand Down Expand Up @@ -1187,6 +1245,7 @@ def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
self.slot_data = args.get("slot_data", {})
self.update_custom_location_names()
self.setup_hint_locations()
if self.slot_data.get("Version"):
ap_version = get_ap_version()
Expand Down Expand Up @@ -1248,7 +1307,8 @@ def on_package(self, cmd: str, args: dict):
# If the location is in the list, remove it
player_name = self.player_names.get(location.player)
location_id = location.location
item_name = self.item_names.lookup_in_game(location.item, self.slot_info[location.player].game)
# Always use DK64's game context to get the correct item name for items in DK64 locations
item_name = self.item_names.lookup_in_game(location.item, self.game)
self.client.locations_scouted[location_id] = {"player": player_name, "item_name": item_name}
if isinstance(args, dict) and isinstance(args.get("data", {}), dict):
source_name = args.get("data", {}).get("source", None)
Expand Down Expand Up @@ -1663,7 +1723,8 @@ def on_item_get(dk64_checks):
"""Handle an item get."""
built_checks_list = []
for check in dk64_checks:
check_name = check_id_to_name.get(check)
# Use custom name if available, otherwise fall back to check_id_to_name
check_name = self.custom_check_id_to_name.get(check, check_id_to_name.get(check))
if check_name:
built_checks_list.append(check)
continue
Expand Down
42 changes: 37 additions & 5 deletions archipelago/FillSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,13 @@ def get_default_settings() -> dict:
"boss_location_rando": True,
"cannons_require_blast": True,
"cb_medal_behavior_new": CBRequirement.pre_selected,
"cb_rando_enabled": False,
"chunky_phase_slam_req": SlamRequirement.green,
"coin_door_item": HelmDoorItem.opened,
"coin_door_item_count": 1,
"coin_rando": False,
"crown_door_item": HelmDoorItem.opened,
"crown_door_item_count": 1,
"crown_enemy_difficulty": CrownEnemyDifficulty.easy,
"crown_placement_rando": False,
"damage_amount": DamageAmount.default,
"decouple_item_rando": False,
"dim_solved_hints": False,
Expand Down Expand Up @@ -268,9 +266,7 @@ def get_default_settings() -> dict:
"progressive_hint_item": ProgressiveHintItem.off,
"puzzle_rando_difficulty": PuzzleRando.medium,
"race_coin_rando": False,
"random_crates": False,
"random_fairies": False,
"random_patches": False,
"random_starting_region": False,
"random_starting_region_new": RandomStartingRegion.off,
"randomize_enemy_sizes": False,
Expand Down Expand Up @@ -318,6 +314,8 @@ def get_default_settings() -> dict:
"wrinkly_available": True,
"wrinkly_hints": WrinklyHints.standard,
"wrinkly_location_rando": False,
"cb_rando_enabled": False,
"cb_rando_list_selected": [],
}


Expand Down Expand Up @@ -361,9 +359,15 @@ def apply_archipelago_settings(settings_dict: dict, options, multiworld) -> None
elif options.galleon_water_level == GalleonWaterLevel.option_raised:
settings_dict["galleon_water"] = GalleonWaterSetting.raised
else:
settings_dict["galleon_water"] = GalleonWaterSetting.vanilla
settings_dict["galleon_water"] = GalleonWaterSetting.lowered
settings_dict["no_consumable_upgrades"] = options.remove_bait_potions.value

# Custom location settings
settings_dict["crown_placement_rando"] = options.crown_placement_rando.value
settings_dict["random_crates"] = options.random_crates.value
settings_dict["random_patches"] = options.random_patches.value
# settings_dict["cb_rando_enabled"] = options.cb_rando_enabled.value


def apply_blocker_settings(settings_dict: dict, options) -> None:
"""Apply level blocker settings."""
Expand Down Expand Up @@ -904,6 +908,10 @@ def handle_fake_generation_settings(settings: Settings, multiworld) -> None:
passthrough = multiworld.re_gen_passthrough["Donkey Kong 64"]
settings.level_order = passthrough["LevelOrder"]

# Store custom location names for later restoration (after spoiler is created)
if passthrough.get("CustomLocationNames"):
settings.ut_custom_location_names = passthrough["CustomLocationNames"]

# Switch logic lifted out of level shuffle due to static levels for UT
if settings.alter_switch_allocation:
for x in range(8):
Expand Down Expand Up @@ -963,6 +971,30 @@ def handle_fake_generation_settings(settings: Settings, multiworld) -> None:
settings.shuffled_location_types.append(Types.Candy)
settings.shuffled_location_types.append(Types.Snide)

# Restore starting region
if passthrough.get("StartingRegion"):
from randomizer.Enums.Regions import Regions
from randomizer.Enums.Maps import Maps as DK64Maps

starting_region_data = passthrough["StartingRegion"]
# Ensure all fields exist and are not None
if all(key in starting_region_data and starting_region_data[key] is not None for key in ["region", "map", "exit", "region_name", "exit_name"]):
try:
settings.starting_region = {
"region": Regions[starting_region_data["region"]],
"map": DK64Maps[starting_region_data["map"]],
"exit": starting_region_data["exit"],
"region_name": starting_region_data["region_name"],
"exit_name": starting_region_data["exit_name"],
}
except (KeyError, TypeError) as e:
# If there's an error converting the data, just skip it
pass

# Store DK Portal locations for later restoration (after spoiler is created)
if passthrough.get("DKPortalLocations"):
settings.ut_dk_portal_locations = passthrough["DKPortalLocations"]


def fillsettings(options, multiworld, random_obj):
"""Fill and configure all DK64 settings."""
Expand Down
41 changes: 41 additions & 0 deletions archipelago/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,34 @@ class SwitchSanity(Choice):
default = 0


class CrownPlacementRando(Toggle):
"""Randomizes the locations of Battle Arena crown pads to alternate positions throughout the levels."""

display_name = "Crown Placement Randomization"


class RandomCrates(Toggle):
"""Randomizes the locations of Melon Crates to alternate positions throughout the levels."""

display_name = "Random Melon Crates"


class RandomPatches(Toggle):
"""Randomizes the locations of Dirt Patches (Rainbow Coins) to alternate positions throughout the levels."""

display_name = "Random Dirt Patches"


## TODO: Figure this out
# class CBRando(Toggle):
# """Randomizes the locations of Colored Bananas throughout the levels.

# This does NOT make individual bananas checks - they remain collectibles that count toward medals.
# """

# display_name = "Colored Banana Randomization"


class LogicType(Choice):
"""Determines what type of logic is needed to beat the seed.

Expand Down Expand Up @@ -1468,6 +1496,10 @@ class DK64Options(PerGameCommonOptions):
level_blockers: LevelBlockers
open_lobbies: OpenLobbies
switchsanity: SwitchSanity
crown_placement_rando: CrownPlacementRando
random_crates: RandomCrates
random_patches: RandomPatches
# cb_rando_enabled: CBRando
climbing_shuffle: ClimbingShuffle
starting_kong_count: StartingKongCount
starting_move_count: StartingMoveCount
Expand Down Expand Up @@ -1603,6 +1635,15 @@ class DK64Options(PerGameCommonOptions):
DKPortalLocationRando,
],
),
OptionGroup(
"Custom Locations",
[
CrownPlacementRando,
RandomCrates,
RandomPatches,
# CBRando,
],
),
OptionGroup(
"Logic",
[
Expand Down
Loading