Files
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

590 lines
31 KiB
Python

import logging
from typing import TYPE_CHECKING
from Options import Toggle
from .data import data, StartingTown, FlyRegion, RegionData
from .mart_data import CUSTOM_MART_SLOT_NAMES
from .options import FreeFlyLocation, Route32Condition, JohtoOnly, RandomizeBadges, UndergroundsRequirePower, \
Route3Access, EliteFourRequirement, Goal, Route44AccessRequirement, BlackthornDarkCaveAccess, RedRequirement, \
MtSilverRequirement, HMBadgeRequirements, RedGyaradosAccess, EarlyFly, RadioTowerRequirement, \
BreedingMethodsRequired, Shopsanity, KantoTrainersanity, JohtoTrainersanity, RandomizePokemonRequests, \
RandomizeTypes, RandomizeEvolution, RandomizeTrades, TradesRequired, MagnetTrainAccess, \
Dexsanity, EncounterGrouping, SouthKantoAccess, LevelScaling, LockKantoGyms
from ..Files import APTokenTypes
if TYPE_CHECKING:
from .world import PokemonCrystalWorld
def adjust_options(world: "PokemonCrystalWorld"):
__adjust_option_problems(world)
def __adjust_option_problems(world: "PokemonCrystalWorld"):
__adjust_options_radio_tower_and_route_44(world)
__adjust_options_victory_road_badges(world)
__adjust_options_johto_only(world)
__adjust_options_restrictive_region_travel(world)
__adjust_options_gyarados(world)
__adjust_options_early_fly(world)
__adjust_options_encounters_and_breeding(world)
__adjust_options_race_mode(world)
__adjust_options_pokemon_requests(world)
__adjust_options_trades(world)
__adjust_options_dark_areas(world)
__adjust_options_randomize_types(world)
__adjust_options_tm_plando(world)
__adjust_options_traps(world)
__adjust_options_mischief_bounds(world)
__adjust_options_backwards_compat(world)
__adjust_options_level_scaling(world)
def __adjust_options_radio_tower_and_route_44(world: "PokemonCrystalWorld"):
if (world.options.randomize_badges.value != RandomizeBadges.option_completely_random
and world.options.radio_tower_count.value > (7 if world.options.johto_only else 15)):
world.options.radio_tower_count.value = 7 if world.options.johto_only else 15
logging.warning(
"Pokemon Crystal: Radio Tower Count >%d incompatible with vanilla or shuffled badges. "
"Changing Radio Tower Count to %d for player %s.",
world.options.radio_tower_count.value,
world.options.radio_tower_count.value,
world.player_name)
if (world.options.route_44_access_count.value > (7 if world.options.johto_only else 15)
and world.options.randomize_badges.value != RandomizeBadges.option_completely_random):
world.options.route_44_access_count.value = 7 if world.options.johto_only else 15
logging.warning(
"Pokemon Crystal: Route 44 Access Count >%d incompatible with vanilla or shuffled badges. "
"Changing Route 44 Access Count to %d for player %s.",
world.options.route_44_access_count.value,
world.options.route_44_access_count.value,
world.player_name)
if (world.options.route_44_access_requirement.value == Route44AccessRequirement.option_gyms
and world.options.blackthorn_dark_cave_access.value == BlackthornDarkCaveAccess.option_vanilla
and world.options.route_44_access_count.value > (7 if world.options.johto_only else 15)):
world.options.route_44_access_count.value = 7 if world.options.johto_only else 15
logging.warning(
"Pokemon Crystal: Route 44 Access Gyms >%d incompatible with vanilla Dark Cave. "
"Changing Route 44 Access Gyms to %d for player %s.",
world.options.route_44_access_count.value,
world.options.route_44_access_count.value,
world.player_name)
if (world.options.radio_tower_requirement.value == RadioTowerRequirement.option_gyms
and world.options.radio_tower_count.value > (7 if world.options.johto_only else 15)):
world.options.radio_tower_count.value = 7 if world.options.johto_only else 15
logging.warning(
"Pokemon Crystal: Radio Tower Gyms >%d is impossible. "
"Changing Radio Tower Gyms to %d for player %s.",
world.options.radio_tower_count.value,
world.options.radio_tower_count.value,
world.player_name)
def __adjust_options_victory_road_badges(world: "PokemonCrystalWorld"):
if (world.options.elite_four_requirement == EliteFourRequirement.option_johto_badges
and world.options.elite_four_count > 8):
world.options.elite_four_count.value = 8
logging.warning(
"Pokemon Crystal: Elite Four count cannot be greater than 8 if Elite Four requirement is Johto Badges. "
"Changing Elite Four Count to 8 for player %s.",
world.player_name)
def __adjust_options_johto_only(world: "PokemonCrystalWorld"):
if world.options.johto_only:
if world.options.goal == Goal.option_red and world.options.johto_only == JohtoOnly.option_on:
world.options.goal.value = Goal.option_elite_four
logging.warning(
"Pokemon Crystal: Red goal is incompatible with Johto Only "
"without Silver Cave. Changing goal to Elite Four for player %s.",
world.player_name)
if world.options.goal == Goal.option_diploma and world.options.johto_only != JohtoOnly.option_off:
world.options.goal.value = Goal.option_elite_four
logging.warning(
"Pokemon Crystal: Diploma goal is incompatible with Johto Only. "
"Changing goal to Elite Four for player %s.",
world.player_name)
if (world.options.elite_four_requirement.value == EliteFourRequirement.option_gyms
and world.options.elite_four_count.value > 8):
world.options.elite_four_count.value = 8
logging.warning(
"Pokemon Crystal: Elite Four Gyms >8 incompatible with Johto Only. "
"Changing Elite Four Gyms to 8 for player %s.",
world.player_name)
if (world.options.red_requirement.value == RedRequirement.option_gyms
and world.options.red_count.value > 8):
world.options.red_count.value = 8
logging.warning(
"Pokemon Crystal: Red Gyms >8 incompatible with Johto Only. "
"Changing Red Gyms to 8 for player %s.",
world.player_name)
if (world.options.mt_silver_requirement.value == MtSilverRequirement.option_gyms
and world.options.mt_silver_count.value > 8):
world.options.mt_silver_count.value = 8
logging.warning(
"Pokemon Crystal: Mt. Silver Gyms >8 incompatible with Johto Only. "
"Changing Mt. Silver Gyms to 8 for player %s.",
world.player_name)
if (world.options.route_44_access_requirement.value == Route44AccessRequirement.option_gyms
and world.options.route_44_access_count.value > 8):
world.options.route_44_access_count.value = 8
logging.warning(
"Pokemon Crystal: Route 44 Access Gyms >8 incompatible with Johto Only. "
"Changing Route 44 Access Gyms to 8 for player %s.",
world.player_name)
if (world.options.radio_tower_requirement.value == RadioTowerRequirement.option_gyms
and world.options.radio_tower_count.value > 7):
world.options.radio_tower_count.value = 7
logging.warning(
"Pokemon Crystal: Radio Tower Gyms >7 incompatible with Johto Only. "
"Changing Radio Tower Gyms to 7 for player %s.",
world.player_name)
if world.options.evolution_gym_levels.value < 8:
world.options.evolution_gym_levels.value = 8
logging.warning(
"Pokemon Crystal: Evolution Gym Levels <8 incompatible with Johto Only. "
"Changing Evolution Gym Levels to 8 for player %s.",
world.player_name)
if world.options.randomize_badges != RandomizeBadges.option_completely_random:
if world.options.red_count.value > 8 and world.options.red_requirement == RedRequirement.option_badges:
world.options.red_count.value = 8
logging.warning(
"Pokemon Crystal: Red Badges >8 incompatible with Johto Only "
"if badges are not completely random. Changing Red Badges to 8 for player %s.",
world.player_name)
if (world.options.elite_four_count.value > 8 and
world.options.elite_four_requirement.value == EliteFourRequirement.option_badges):
world.options.elite_four_count.value = 8
logging.warning(
"Pokemon Crystal: Elite Four Badges >8 incompatible with Johto Only "
"if badges are not completely random. Changing Elite Four Badges to 8 for player %s.",
world.player_name)
if (world.options.radio_tower_count.value > 8
and world.options.radio_tower_requirement.value == RadioTowerRequirement.option_badges):
world.options.radio_tower_count.value = 8
logging.warning(
"Pokemon Crystal: Radio Tower Badges >8 incompatible with Johto Only "
"if badges are not completely random. Changing Radio Tower Badges to 8 for player %s.",
world.player_name)
if (world.options.mt_silver_count.value > 8 and
world.options.mt_silver_requirement.value == MtSilverRequirement.option_badges):
world.options.mt_silver_count.value = 8
logging.warning(
"Pokemon Crystal: Mt. Silver Badges >8 incompatible with Johto Only "
"if badges are not completely random. Changing Mt. Silver Badges to 8 for player %s.",
world.player_name)
if (world.options.route_44_access_count.value > 8 and
world.options.route_44_access_requirement.value == Route44AccessRequirement.option_badges):
world.options.route_44_access_count.value = 8
logging.warning(
"Pokemon Crystal: Route 44 Access Badges >8 incompatible with Johto Only "
"if badges are not completely random. Changing Route 44 Access Badges to 8 for player %s.",
world.player_name)
def __adjust_options_restrictive_region_travel(world: "PokemonCrystalWorld") -> None:
if world.options.randomize_badges != RandomizeBadges.option_completely_random:
if world.options.magnet_train_access == MagnetTrainAccess.option_pass_and_power:
world.options.magnet_train_access.value = MagnetTrainAccess.option_pass
logging.warning("Pokemon Crystal: Magnet Train requires power not compatible with badges in gyms. "
"Changing Magnet Train Access to Pass for player %s.",
world.player_name)
def __adjust_options_gyarados(world: "PokemonCrystalWorld"):
if (world.options.red_gyarados_access
and world.options.randomize_badges.value == RandomizeBadges.option_vanilla
and "Whirlpool" and not world.options.hm_badge_requirements == HMBadgeRequirements.option_no_badges
and "Whirlpool" not in world.options.remove_badge_requirement):
world.options.red_gyarados_access.value = RedGyaradosAccess.option_vanilla
logging.warning("Pokemon Crystal: Red Gyarados access requires Whirlpool and Vanilla Badges are not "
"compatible, setting Red Gyarados access to vanilla for player %s.",
world.player_name)
def __adjust_options_early_fly(world: "PokemonCrystalWorld"):
if (world.options.early_fly
and world.options.randomize_starting_town
and world.options.randomize_badges.value != RandomizeBadges.option_completely_random
and "Fly" not in world.options.remove_badge_requirement
and world.options.hm_badge_requirements != HMBadgeRequirements.option_no_badges):
world.options.early_fly.value = EarlyFly.option_false
logging.warning("Pokemon Crystal: Early fly is not compatible with Random Starting Town if Badges are "
"not completely random. Disabling Early Fly for player %s",
world.player_name)
def __adjust_options_encounters_and_breeding(world: "PokemonCrystalWorld"):
if (world.options.breeding_methods_required == BreedingMethodsRequired.option_with_ditto
and "Ditto" in world.options.wild_encounter_blocklist):
world.options.breeding_methods_required.value = BreedingMethodsRequired.option_none
logging.warning(
"Pokemon Crystal: Ditto cannot be blocklisted while Ditto only breeding is enabled. "
"Disabling breeding logic for player %s.",
world.player_name)
if "Land" not in world.options.wild_encounter_methods_required and "Fishing" not in world.options.wild_encounter_methods_required:
world.options.wild_encounter_methods_required.value.add(world.random.choice(("Land", "Fishing")))
logging.warning(
"Pokemon Crystal: At least one of Land or Fishing must be enabled in wild encounter methods required. "
"Adding one at random for player %s.",
world.player_name)
if (not world.options.randomize_wilds and
world.options.breeding_methods_required == BreedingMethodsRequired.option_with_ditto
and "Land" not in world.options.wild_encounter_methods_required):
world.options.breeding_methods_required.value = BreedingMethodsRequired.option_none
logging.warning(
"Pokemon Crystal: Ditto only breeding is not available for vanilla wilds with no Land encounters. "
"Disabling breeding logic for player %s.",
world.player_name)
def __adjust_options_race_mode(world: "PokemonCrystalWorld"):
# In race mode we don't patch any item location information into the ROM
if world.multiworld.is_race and not world.options.remote_items:
logging.warning("Pokemon Crystal: Forcing Player %s (%s) to use remote items due to race mode.",
world.player, world.player_name)
world.options.remote_items.value = Toggle.option_true
def __adjust_options_pokemon_requests(world: "PokemonCrystalWorld"):
if world.options.randomize_pokemon_requests == RandomizePokemonRequests.option_items and not world.options.randomize_wilds:
logging.warning("Pokemon Crystal: Randomize Pokemon Requests items only is not compatible with vanilla wilds. "
"Disabling Randomize Pokemon Requests for player %s (%s).", world.player, world.player_name)
world.options.randomize_pokemon_requests.value = RandomizePokemonRequests.option_off
def __adjust_options_trades(world: "PokemonCrystalWorld"):
if (world.options.trades_required and world.options.randomize_trades.value in (RandomizeTrades.option_vanilla,
RandomizeTrades.option_received)
and not world.options.randomize_wilds):
logging.warning("Pokemon Crystal: Requested trade Pokemon must be randomized for vanilla wilds. "
"Disabling Trades Required for player %s (%s).", world.player, world.player_name)
world.options.trades_required.value = TradesRequired.option_false
def __adjust_options_dark_areas(world: "PokemonCrystalWorld"):
if (sorted(world.options.dark_areas.value) != world.options.dark_areas.default
and world.options.randomize_badges != RandomizeBadges.option_completely_random):
logging.warning(
"Pokemon Crystal: Non-vanilla dark areas are not compatible with badges that are not completely random. "
"Resetting dark areas to vanilla for %s (%s).", world.player, world.player_name)
world.options.dark_areas.value = world.options.dark_areas.default
def __adjust_options_randomize_types(world: "PokemonCrystalWorld"):
if (world.options.randomize_types == RandomizeTypes.option_follow_evolutions and
world.options.randomize_evolution == RandomizeEvolution.option_match_a_type):
logging.warning(
"Pokemon Crystal: Types follow evolutions and evolutions follow types are incompatible. "
"Setting Randomize Types to completely random for %s (%s).", world.player, world.player_name)
world.options.randomize_types.value = RandomizeTypes.option_completely_random
def __adjust_options_tm_plando(world: "PokemonCrystalWorld"):
if 12 in world.options.tm_plando.value and "Sweet Scent" not in world.options.tm_plando.value.values() \
and (world.options.dexsanity or world.options.dexcountsanity):
logging.warning(
"Pokemon Crystal: A Sweet Scent TM must exist if Dexsanity or Dexcountsanity are enabled. "
"Resetting TM12 to vanilla for Player %s (%s).", world.player, world.player_name)
world.options.tm_plando.value.pop(12)
def __adjust_options_traps(world: "PokemonCrystalWorld"):
if world.options.filler_trap_percentage > world.settings.maximum_filler_trap_percentage:
maximum_trap_weight = world.settings.maximum_filler_trap_percentage
logging.warning(
"Pokemon Crystal: Trap Weight is greater than allowed maximum. "
f"Reducing filler trap percentage to {maximum_trap_weight} for Player %s (%s).",
world.player,
world.player_name
)
world.options.filler_trap_percentage.value = maximum_trap_weight
def __adjust_options_mischief_bounds(world: "PokemonCrystalWorld"):
if world.options.enable_mischief and \
world.options.mischief_lower_bound.value > world.options.mischief_upper_bound.value:
world.options.mischief_upper_bound.value = world.options.mischief_lower_bound.value
logging.warning("Pokemon Crystal: Adjusted mischief bounds for player %s (%s) :3",
world.player,
world.player_name
)
def __adjust_options_backwards_compat(world: "PokemonCrystalWorld"):
for trap in world.options.trap_weights:
snake_case_trap_name = trap.replace(" ", "_").lower()
option_name = f"{snake_case_trap_name}_weight"
if hasattr(world.options, option_name):
option = getattr(world.options, option_name)
if option: world.options.trap_weights.value[trap] = option.value
if world.options.randomize_move_types:
world.options.randomize_moves.value.add("Type")
def __adjust_options_level_scaling(world: "PokemonCrystalWorld"):
if (world.options.level_scaling != LevelScaling.option_off
and world.options.lock_kanto_gyms != LockKantoGyms.option_off):
world.options.lock_kanto_gyms.value = LockKantoGyms.option_off
logging.warning(
"Pokemon Crystal: Lock Kanto Gyms is incompatible with Level Scaling. "
"Disabling Lock Kanto Gyms for player %s.",
world.player_name)
def should_include_region(region: RegionData, world: "PokemonCrystalWorld"):
# check if region should be included
return (region.johto
or world.options.johto_only.value == JohtoOnly.option_off
or (region.silver_cave and world.options.johto_only == JohtoOnly.option_include_silver_cave)) and (
not world.options.skip_elite_four or not region.elite_4
)
def randomize_starting_town(world: "PokemonCrystalWorld"):
if world.is_universal_tracker or not world.options.randomize_starting_town: return
location_pool = data.starting_towns[:]
location_pool = [loc for loc in location_pool if _starting_town_valid(world, loc)]
blocklist = set(world.options.starting_town_blocklist.value)
if "_Johto" in blocklist:
blocklist.remove("_Johto")
blocklist.update(town.name for town in data.starting_towns if town.johto)
if "_Kanto" in blocklist:
blocklist.remove("_Kanto")
blocklist.update(town.name for town in data.starting_towns if not town.johto)
filtered_pool = [loc for loc in location_pool if loc.name not in blocklist]
if not filtered_pool:
logging.warning("Pokemon Crystal: All valid starting town locations blocked for player %s (%s). "
"Using global list instead.", world.player, world.player_name)
filtered_pool = location_pool
world.random.shuffle(filtered_pool)
world.starting_town = filtered_pool.pop()
logging.debug(f"Starting town({world.player_name}): {world.starting_town.name}")
if world.starting_town.name == "Cianwood City":
world.multiworld.early_items[world.player]["HM03 Surf"] = 1
def _starting_town_valid(world: "PokemonCrystalWorld", starting_town: StartingTown):
if world.options.johto_only and not starting_town.johto: return False
if world.options.randomize_badges != RandomizeBadges.option_completely_random and starting_town.restrictive_start:
return False
immediate_hiddens = world.options.randomize_hidden_items and not world.options.require_itemfinder
full_johto_trainersanity = world.options.johto_trainersanity == JohtoTrainersanity.range_end
full_kanto_trainersanity = world.options.kanto_trainersanity == KantoTrainersanity.range_end
johto_shopsanity = Shopsanity.johto_marts in world.options.shopsanity.value
kanto_shopsanity = Shopsanity.kanto_marts in world.options.shopsanity.value
full_dexsanity = (world.options.dexsanity == Dexsanity.range_end
or (world.options.dexcountsanity >= 10 and world.options.dexcountsanity_step == 1))
immediate_wilds = ("Land" in world.options.wild_encounter_methods_required.value
and world.options.encounter_grouping != EncounterGrouping.option_one_per_method)
immediate_dexsanity = full_dexsanity and immediate_wilds
if starting_town.name == "Cianwood City":
return world.options.static_pokemon_required and (
(full_johto_trainersanity and immediate_hiddens) or johto_shopsanity)
if starting_town.name in ("Lake of Rage", "Mahogany Town"):
return ((not world.options.mount_mortar_access and "Mount Mortar" not in world.options.dark_areas)
or johto_shopsanity or full_johto_trainersanity)
if starting_town.name == "Azalea Town":
return ("Slowpoke Well" not in world.options.dark_areas
or "Union Cave" not in world.options.dark_areas or immediate_dexsanity)
if starting_town.name in ("Pallet Town", "Viridian City", "Pewter City"):
west_kanto_escapable = (world.options.randomize_pokegear or not world.options.lock_kanto_gyms
or world.options.south_kanto_access != SouthKantoAccess.option_route_21)
return (immediate_hiddens or world.options.route_3_access == Route3Access.option_vanilla or kanto_shopsanity
or world.options.randomize_berry_trees or immediate_dexsanity) and west_kanto_escapable
if starting_town.name == "Rock Tunnel":
return full_kanto_trainersanity or immediate_dexsanity or ("Rock Tunnel" not in world.options.dark_areas.value)
if starting_town.name == "Vermilion City":
return ("South" not in world.options.saffron_gatehouse_tea or world.options.undergrounds_require_power not in (
UndergroundsRequirePower.option_both, UndergroundsRequirePower.option_north_south) or kanto_shopsanity
or immediate_dexsanity)
if starting_town.name == "Cerulean City":
return ("North" not in world.options.saffron_gatehouse_tea or immediate_hiddens or kanto_shopsanity
or full_kanto_trainersanity or immediate_dexsanity)
if starting_town.name == "Celadon City":
return ("West" not in world.options.saffron_gatehouse_tea or immediate_hiddens or kanto_shopsanity
or immediate_dexsanity)
if starting_town.name == "Lavender Town":
return ("East" not in world.options.saffron_gatehouse_tea or full_kanto_trainersanity or kanto_shopsanity
or (immediate_dexsanity and "Rock Tunnel" not in world.options.dark_areas.value) or (
not world.options.route_12_access and immediate_hiddens and world.options.randomize_berry_trees))
if starting_town.name == "Fuchsia City":
return ("East" not in world.options.saffron_gatehouse_tea and not world.options.route_12_access) or (
immediate_hiddens and world.options.randomize_berry_trees) or immediate_dexsanity or (
not world.options.route_12_access and kanto_shopsanity) or full_kanto_trainersanity
return True
def get_fly_regions(world: "PokemonCrystalWorld") -> list[FlyRegion]:
fly_regions = list(data.fly_regions)
if world.options.johto_only == JohtoOnly.option_on:
fly_regions = [region for region in fly_regions if region.name != "Silver Cave"]
if world.options.johto_only:
fly_regions = [region for region in fly_regions if region.johto]
return fly_regions
def get_free_fly_locations(world: "PokemonCrystalWorld"):
location_pool = data.fly_regions[:]
if not world.options.randomize_starting_town:
location_pool = \
[region for region in location_pool if not region.exclude_vanilla_start]
if world.options.route_32_condition.value != Route32Condition.option_any_badge:
# Azalea, Goldenrod
location_pool = [region for region in location_pool if region.name not in ("Azalea Town", "Goldenrod City")]
if not world.options.remove_ilex_cut_tree and world.options.route_32_condition.value != Route32Condition.option_any_badge:
# Goldenrod
location_pool = [region for region in location_pool if region.name != "Goldenrod City"]
if world.options.johto_only:
location_pool = [region for region in location_pool if region.johto]
if world.options.johto_only.value == JohtoOnly.option_on:
# Mt. Silver
location_pool = [region for region in location_pool if region.name != "Silver Cave"]
if world.options.randomize_starting_town:
world.options.free_fly_blocklist.value.add(world.starting_town.name)
blocklist = set(world.options.free_fly_blocklist.value)
if "_Johto" in blocklist:
blocklist.remove("_Johto")
blocklist.update(town.name for town in data.fly_regions if town.johto)
if "_Kanto" in blocklist:
blocklist.remove("_Kanto")
blocklist.update(town.name for town in data.fly_regions if not town.johto)
# only do any of this if there even is a fly location blocklist
if blocklist:
# figure out how many fly locations are needed
locations_required = 1
if world.options.free_fly_location.value == FreeFlyLocation.option_free_fly_and_map_card:
locations_required = 2
# calculate what the list of locations would be after the blocklist
location_pool_after_blocklist = [item for item in location_pool if
item.name not in blocklist]
# if the list after the blocked locations are removed is long enough to satisfy all the requested fly locations, set the location pool to it
if len(location_pool_after_blocklist) >= locations_required:
location_pool = location_pool_after_blocklist
else:
logging.warning("Pokemon Crystal: All valid free fly locations blocked for player %s (%s). "
"Using global list instead.", world.player, world.player_name)
world.random.shuffle(location_pool)
if world.options.free_fly_location.value in (FreeFlyLocation.option_free_fly,
FreeFlyLocation.option_free_fly_and_map_card):
world.free_fly_location = location_pool.pop()
if world.options.free_fly_location.value in (FreeFlyLocation.option_free_fly_and_map_card,
FreeFlyLocation.option_map_card):
world.map_card_fly_location = location_pool.pop()
def get_mart_slot_location_name(mart: str, index: int):
if mart in CUSTOM_MART_SLOT_NAMES:
return CUSTOM_MART_SLOT_NAMES[mart][index]
else:
return f"Shop Item {index + 1}"
def convert_to_ingame_text(text: str, string_terminator: bool = False) -> list[int]:
charmap = {
"": 0x75, " ": 0x7f, "A": 0x80, "B": 0x81, "C": 0x82, "D": 0x83, "E": 0x84, "F": 0x85, "G": 0x86, "H": 0x87,
"I": 0x88, "J": 0x89, "K": 0x8a, "L": 0x8b, "M": 0x8c, "N": 0x8d, "O": 0x8e, "P": 0x8f, "Q": 0x90, "R": 0x91,
"S": 0x92, "T": 0x93, "U": 0x94, "V": 0x95, "W": 0x96, "X": 0x97, "Y": 0x98, "Z": 0x99, "(": 0x9a, ")": 0x9b,
":": 0x9c, ";": 0x9d, "[": 0x9e, "]": 0x9f, "a": 0xa0, "b": 0xa1, "c": 0xa2, "d": 0xa3, "e": 0xa4, "f": 0xa5,
"g": 0xa6, "h": 0xa7, "i": 0xa8, "j": 0xa9, "k": 0xaa, "l": 0xab, "m": 0xac, "n": 0xad, "o": 0xae, "p": 0xaf,
"q": 0xb0, "r": 0xb1, "s": 0xb2, "t": 0xb3, "u": 0xb4, "v": 0xb5, "w": 0xb6, "x": 0xb7, "y": 0xb8, "z": 0xb9,
"Ä": 0xc0, "Ö": 0xc1, "Ü": 0xc2, "ä": 0xc3, "ö": 0xc4, "ü": 0xc5, "'": 0xe0, "-": 0xe3, "?": 0xe6, "!": 0xe7,
".": 0xe8, "&": 0xe9, "é": 0xea, "": 0xeb, "": 0xec, "": 0xed, "": 0xee, "": 0xef, "¥": 0xf0, "/": 0xf3,
",": 0xf4, "0": 0xf6, "1": 0xf7, "2": 0xf8, "3": 0xf9, "4": 0xfa, "5": 0xfb, "6": 0xfc, "7": 0xfd, "8": 0xfe,
"9": 0xff, "_": 0xe3, "": 0xf5, "$": 0xf0, "£": 0xf0, "": 0xf0, "É": 0xea,
}
apostrophe_specials = {
"d": 0xd0, "l": 0xd1, "m": 0xd2, "r": 0xd3, "s": 0xd4, "t": 0xd5, "v": 0xd6
}
current_char = 0
ingame_string = []
while current_char < len(text):
if text[current_char] == "'" and current_char < len(text) - 1 and text[current_char + 1] in apostrophe_specials:
current_char += 1
ingame_string.append(apostrophe_specials[text[current_char]])
else:
ingame_string.append(charmap.get(text[current_char], charmap["?"]))
current_char += 1
if string_terminator:
ingame_string.append(0x50)
return ingame_string
def bound(value: int, lower_bound: int, upper_bound: int) -> int:
return max(min(value, upper_bound), lower_bound)
def rom_offset_to_address(offset: int) -> tuple[int, int]:
if offset < 0x4000: return 0, offset
bank = offset // 0x4000
address = offset - (bank - 1) * 0x4000
return bank, address
def replace_map_tiles(patch, map_name: str, x: int, y: int, tiles):
# x and y are 0 indexed
tile_index = (y * data.maps[map_name].width) + x
base_address = data.rom_addresses[f"{map_name}_Blocks"]
logging.debug(f"Writing {len(tiles)} new tile(s) to map {map_name} at {x},{y}")
write_appp_tokens(patch, tiles, base_address + tile_index)
def write_appp_tokens(patch, byte_array, address):
patch.write_token(
APTokenTypes.WRITE,
address,
bytes(byte_array)
)
def write_rom_bytes(rom, byte_array, address):
rom[address:address + len(byte_array)] = bytes(byte_array)