Pokemon Emerald: Implement New Game (#1813)

This commit is contained in:
Bryce Wilson 2023-11-12 13:39:34 -08:00 committed by GitHub
parent e670ca513b
commit 43041f7292
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 16338 additions and 0 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
*.apmc
*.apz5
*.aptloz
*.apemerald
*.pyc
*.pyd
*.sfc

View File

@ -52,6 +52,7 @@ Currently, the following games are supported:
* DOOM 1993
* Terraria
* Lingo
* Pokémon Emerald
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -95,6 +95,9 @@
# Overcooked! 2
/worlds/overcooked2/ @toasterparty
# Pokemon Emerald
/worlds/pokemon_emerald/ @Zunawe
# Pokemon Red and Blue
/worlds/pokemon_rb/ @Alchav

View File

@ -153,6 +153,11 @@ Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Arc
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoMMBN3Client.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch\shell\open\command"; ValueData: """{app}\ArchipelagoMMBN3Client.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apemerald"; ValueData: "{#MyAppName}pkmnepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/bizhawk
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Archipelago Pokemon Emerald Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/bizhawk
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; Components: client/bizhawk
Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/bizhawk
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";

View File

@ -0,0 +1,19 @@
Copyright (c) 2023 Zunawe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,58 @@
# Pokemon Emerald
Version 1.2.0
This README contains general info useful for understanding the world. Pretty much all the long lists of locations,
regions, and items are stored in `data/` and (mostly) loaded in by `data.py`. Access rules are in `rules.py`. Check
[data/README.md](data/README.md) for more detailed information on the JSON files holding most of the data.
## Warps
Quick note to start, you should not be defining or modifying encoded warps from this repository. They're encoded in the
source code repository for the mod, and then assigned to regions in `data/regions/`. All warps in the game already exist
within `extracted_data.json`, and all relevant warps are already placed in `data/regions/` (unless they were deleted
accidentally).
Many warps are actually two or three events acting as one logical warp. Doorways, for example, are often 2 tiles wide
indoors but only 1 tile wide outdoors. Both indoor warps point to the outdoor warp, and the outdoor warp points to only
one of the indoor warps. We want to describe warps logically in a way that retains information about individual warp
events. That way a 2-tile-wide doorway doesnt look like a one-way warp next to an unrelated two-way warp, but if we want
to randomize the destinations of those warps, we can still get back each individual id of the multi-tile warp.
This is how warps are encoded:
`{source_map}:{source_warp_ids}/{dest_map}:{dest_warp_ids}[!]`
- `source_map`: The map the warp events are located in
- `source_warp_ids`: The ids of all adjacent warp events in source_map which lead to the same destination (these must be
in ascending order)
- `dest_map`: The map of the warp event to which this one is connected
- `dest_warp_ids`: The ids of the warp events in dest_map
- `[!]`: If the warp expects to lead to a destination which doesnot lead back to it, add a ! to the end
Example: `MAP_LAVARIDGE_TOWN_HOUSE:0,1/MAP_LAVARIDGE_TOWN:4`
Example 2: `MAP_AQUA_HIDEOUT_B1F:14/MAP_AQUA_HIDEOUT_B1F:12!`
Note: A warp must have its destination set to another warp event. However, that does not guarantee that the destination
warp event will warp back to the source.
Note 2: Some warps _only_ act as destinations and cannot actually be interacted with by the player as sources. These are
usually places you fall from a hole above. At the time of writing, these are actually not accounted for, but there are
no instances where it changes logical access.
Note 3: Some warp destinations go to the map `MAP_DYNAMIC` and have a special warp id. These edge cases are:
- The Moving Truck
- Terra Cave
- Marine Cave
- The Department Store Elevator
- Secret Bases
- The Trade Center
- The Union Room
- The Record Corner
- 2P/4P Battle Colosseum
Note 4: The trick house on Route 110 changes the warp destinations of its entrance and ending room as you progress
through the puzzles, but the source code only sets the trick house up for the first puzzle, and I assume the destination
gets overwritten at run time when certain flags are set.

View File

@ -0,0 +1,882 @@
"""
Archipelago World definition for Pokemon Emerald Version
"""
from collections import Counter
import copy
import logging
import os
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar
from BaseClasses import ItemClassification, MultiWorld, Tutorial
from Fill import FillError, fill_restrictive
from Options import Toggle
import settings
from worlds.AutoWorld import WebWorld, World
from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient
from .data import (SpeciesData, MapData, EncounterTableData, LearnsetMove, TrainerPokemonData, StaticEncounterData,
TrainerData, data as emerald_data)
from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification,
offset_item_value)
from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map,
create_locations_with_tags)
from .options import (ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms,
RandomizeStarters, LevelUpMoves, RandomizeAbilities, RandomizeTypes, TmCompatibility,
HmCompatibility, RandomizeStaticEncounters, NormanRequirement, PokemonEmeraldOptions)
from .pokemon import get_random_species, get_random_move, get_random_damaging_move, get_random_type
from .regions import create_regions
from .rom import PokemonEmeraldDeltaPatch, generate_output, location_visited_event_to_id_map
from .rules import set_rules
from .sanity_check import validate_regions
from .util import int_to_bool_array, bool_array_to_int
class PokemonEmeraldWebWorld(WebWorld):
"""
Webhost info for Pokemon Emerald
"""
theme = "ocean"
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to playing Pokémon Emerald with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["Zunawe"]
)
tutorials = [setup_en]
class PokemonEmeraldSettings(settings.Group):
class PokemonEmeraldRomFile(settings.UserFilePath):
"""File name of your English Pokemon Emerald ROM"""
description = "Pokemon Emerald ROM File"
copy_to = "Pokemon - Emerald Version (USA, Europe).gba"
md5s = [PokemonEmeraldDeltaPatch.hash]
rom_file: PokemonEmeraldRomFile = PokemonEmeraldRomFile(PokemonEmeraldRomFile.copy_to)
class PokemonEmeraldWorld(World):
"""
Pokémon Emerald is the definitive Gen III Pokémon game and one of the most beloved in the franchise.
Catch, train, and battle Pokémon, explore the Hoenn region, thwart the plots
of Team Magma and Team Aqua, challenge gyms, and become the Pokémon champion!
"""
game = "Pokemon Emerald"
web = PokemonEmeraldWebWorld()
topology_present = True
settings_key = "pokemon_emerald_settings"
settings: ClassVar[PokemonEmeraldSettings]
options_dataclass = PokemonEmeraldOptions
options: PokemonEmeraldOptions
item_name_to_id = create_item_label_to_code_map()
location_name_to_id = create_location_label_to_id_map()
item_name_groups = ITEM_GROUPS
location_name_groups = LOCATION_GROUPS
data_version = 1
required_client_version = (0, 4, 3)
badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] = None
hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] = None
free_fly_location_id: int = 0
modified_species: List[Optional[SpeciesData]]
modified_maps: List[MapData]
modified_tmhm_moves: List[int]
modified_static_encounters: List[int]
modified_starters: Tuple[int, int, int]
modified_trainers: List[TrainerData]
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
if not os.path.exists(cls.settings.rom_file):
raise FileNotFoundError(cls.settings.rom_file)
assert validate_regions()
def get_filler_item_name(self) -> str:
return "Great Ball"
def generate_early(self) -> None:
# If badges or HMs are vanilla, Norman locks you from using Surf, which means you're not guaranteed to be
# able to reach Fortree Gym, Mossdeep Gym, or Sootopolis Gym. So we can't require reaching those gyms to
# challenge Norman or it creates a circular dependency.
# This is never a problem for completely random badges/hms because the algo will not place Surf/Balance Badge
# on Norman on its own. It's never a problem for shuffled badges/hms because there is no scenario where Cut or
# the Stone Badge can be a lynchpin for access to any gyms, so they can always be put on Norman in a worst case
# scenario.
# This will also be a problem in warp rando if direct access to Norman's room requires Surf or if access
# any gym leader in general requires Surf. We will probably have to force this to 0 in that case.
max_norman_count = 7
if self.options.badges == RandomizeBadges.option_vanilla:
max_norman_count = 4
if self.options.hms == RandomizeHms.option_vanilla:
if self.options.norman_requirement == NormanRequirement.option_badges:
if self.options.badges != RandomizeBadges.option_completely_random:
max_norman_count = 4
if self.options.norman_requirement == NormanRequirement.option_gyms:
max_norman_count = 4
if self.options.norman_count.value > max_norman_count:
logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with "
"other settings. Reducing to 4.", self.player, self.multiworld.get_player_name(self.player))
self.options.norman_count.value = max_norman_count
def create_regions(self) -> None:
regions = create_regions(self)
tags = {"Badge", "HM", "KeyItem", "Rod", "Bike"}
if self.options.overworld_items:
tags.add("OverworldItem")
if self.options.hidden_items:
tags.add("HiddenItem")
if self.options.npc_gifts:
tags.add("NpcGift")
if self.options.enable_ferry:
tags.add("Ferry")
create_locations_with_tags(self, regions, tags)
self.multiworld.regions.extend(regions.values())
def create_items(self) -> None:
item_locations: List[PokemonEmeraldLocation] = [
location
for location in self.multiworld.get_locations(self.player)
if location.address is not None
]
# Filter progression items which shouldn't be shuffled into the itempool. Their locations
# still exist, but event items will be placed and locked at their vanilla locations instead.
filter_tags = set()
if not self.options.key_items:
filter_tags.add("KeyItem")
if not self.options.rods:
filter_tags.add("Rod")
if not self.options.bikes:
filter_tags.add("Bike")
if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}:
filter_tags.add("Badge")
if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}:
filter_tags.add("HM")
if self.options.badges == RandomizeBadges.option_shuffle:
self.badge_shuffle_info = [
(location, self.create_item_by_code(location.default_item_code))
for location in [l for l in item_locations if "Badge" in l.tags]
]
if self.options.hms == RandomizeHms.option_shuffle:
self.hm_shuffle_info = [
(location, self.create_item_by_code(location.default_item_code))
for location in [l for l in item_locations if "HM" in l.tags]
]
item_locations = [location for location in item_locations if len(filter_tags & location.tags) == 0]
default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations]
if self.options.item_pool_type == ItemPoolType.option_shuffled:
self.multiworld.itempool += default_itempool
elif self.options.item_pool_type in {ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced}:
item_categories = ["Ball", "Heal", "Vitamin", "EvoStone", "Money", "TM", "Held", "Misc"]
# Count occurrences of types of vanilla items in pool
item_category_counter = Counter()
for item in default_itempool:
if not item.advancement:
item_category_counter.update([tag for tag in item.tags if tag in item_categories])
item_category_weights = [item_category_counter.get(category) for category in item_categories]
item_category_weights = [weight if weight is not None else 0 for weight in item_category_weights]
# Create lists of item codes that can be used to fill
fill_item_candidates = emerald_data.items.values()
fill_item_candidates = [item for item in fill_item_candidates if "Unique" not in item.tags]
fill_item_candidates_by_category = {category: [] for category in item_categories}
for item_data in fill_item_candidates:
for category in item_categories:
if category in item_data.tags:
fill_item_candidates_by_category[category].append(offset_item_value(item_data.item_id))
for category in fill_item_candidates_by_category:
fill_item_candidates_by_category[category].sort()
# Ignore vanilla occurrences and pick completely randomly
if self.options.item_pool_type == ItemPoolType.option_diverse:
item_category_weights = [
len(category_list)
for category_list in fill_item_candidates_by_category.values()
]
# TMs should not have duplicates until every TM has been used already
all_tm_choices = fill_item_candidates_by_category["TM"].copy()
def refresh_tm_choices() -> None:
fill_item_candidates_by_category["TM"] = all_tm_choices.copy()
self.random.shuffle(fill_item_candidates_by_category["TM"])
# Create items
for item in default_itempool:
if not item.advancement and "Unique" not in item.tags:
category = self.random.choices(item_categories, item_category_weights)[0]
if category == "TM":
if len(fill_item_candidates_by_category["TM"]) == 0:
refresh_tm_choices()
item_code = fill_item_candidates_by_category["TM"].pop()
else:
item_code = self.random.choice(fill_item_candidates_by_category[category])
item = self.create_item_by_code(item_code)
self.multiworld.itempool.append(item)
def set_rules(self) -> None:
set_rules(self)
def generate_basic(self) -> None:
locations: List[PokemonEmeraldLocation] = self.multiworld.get_locations(self.player)
# Set our free fly location
# If not enabled, set it to Littleroot Town by default
fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN"
if self.options.free_fly_location:
fly_location_name = self.random.choice([
"EVENT_VISITED_SLATEPORT_CITY",
"EVENT_VISITED_MAUVILLE_CITY",
"EVENT_VISITED_VERDANTURF_TOWN",
"EVENT_VISITED_FALLARBOR_TOWN",
"EVENT_VISITED_LAVARIDGE_TOWN",
"EVENT_VISITED_FORTREE_CITY",
"EVENT_VISITED_LILYCOVE_CITY",
"EVENT_VISITED_MOSSDEEP_CITY",
"EVENT_VISITED_SOOTOPOLIS_CITY",
"EVENT_VISITED_EVER_GRANDE_CITY"
])
self.free_fly_location_id = location_visited_event_to_id_map[fly_location_name]
free_fly_location_location = self.multiworld.get_location("FREE_FLY_LOCATION", self.player)
free_fly_location_location.item = None
free_fly_location_location.place_locked_item(self.create_event(fly_location_name))
# Key items which are considered in access rules but not randomized are converted to events and placed
# in their vanilla locations so that the player can have them in their inventory for logic.
def convert_unrandomized_items_to_events(tag: str) -> None:
for location in locations:
if location.tags is not None and tag in location.tags:
location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code]))
location.address = None
if self.options.badges == RandomizeBadges.option_vanilla:
convert_unrandomized_items_to_events("Badge")
if self.options.hms == RandomizeHms.option_vanilla:
convert_unrandomized_items_to_events("HM")
if not self.options.rods:
convert_unrandomized_items_to_events("Rod")
if not self.options.bikes:
convert_unrandomized_items_to_events("Bike")
if not self.options.key_items:
convert_unrandomized_items_to_events("KeyItem")
def pre_fill(self) -> None:
# Items which are shuffled between their own locations
if self.options.badges == RandomizeBadges.option_shuffle:
badge_locations: List[PokemonEmeraldLocation]
badge_items: List[PokemonEmeraldItem]
# Sort order makes `fill_restrictive` try to place important badges later, which
# makes it less likely to have to swap at all, and more likely for swaps to work.
# In the case of vanilla HMs, navigating Granite Cave is required to access more than 2 gyms,
# so Knuckle Badge deserves highest priority if Flash is logically required.
badge_locations, badge_items = [list(l) for l in zip(*self.badge_shuffle_info)]
badge_priority = {
"Knuckle Badge": 0 if (self.options.hms == RandomizeHms.option_vanilla and self.options.require_flash) else 3,
"Balance Badge": 1,
"Dynamo Badge": 1,
"Mind Badge": 2,
"Heat Badge": 2,
"Rain Badge": 3,
"Stone Badge": 4,
"Feather Badge": 5
}
badge_items.sort(key=lambda item: badge_priority.get(item.name, 0))
collection_state = self.multiworld.get_all_state(False)
if self.hm_shuffle_info is not None:
for _, item in self.hm_shuffle_info:
collection_state.collect(item)
# In specific very constrained conditions, fill_restrictive may run
# out of swaps before it finds a valid solution if it gets unlucky.
# This is a band-aid until fill/swap can reliably find those solutions.
attempts_remaining = 2
while attempts_remaining > 0:
attempts_remaining -= 1
self.random.shuffle(badge_locations)
try:
fill_restrictive(self.multiworld, collection_state, badge_locations, badge_items,
single_player_placement=True, lock=True, allow_excluded=True)
break
except FillError as exc:
if attempts_remaining == 0:
raise exc
logging.debug(f"Failed to shuffle badges for player {self.player}. Retrying.")
continue
if self.options.hms == RandomizeHms.option_shuffle:
hm_locations: List[PokemonEmeraldLocation]
hm_items: List[PokemonEmeraldItem]
# Sort order makes `fill_restrictive` try to place important HMs later, which
# makes it less likely to have to swap at all, and more likely for swaps to work.
# In the case of vanilla badges, navigating Granite Cave is required to access more than 2 gyms,
# so Flash deserves highest priority if it's logically required.
hm_locations, hm_items = [list(l) for l in zip(*self.hm_shuffle_info)]
hm_priority = {
"HM05 Flash": 0 if (self.options.badges == RandomizeBadges.option_vanilla and self.options.require_flash) else 3,
"HM03 Surf": 1,
"HM06 Rock Smash": 1,
"HM08 Dive": 2,
"HM04 Strength": 2,
"HM07 Waterfall": 3,
"HM01 Cut": 4,
"HM02 Fly": 5
}
hm_items.sort(key=lambda item: hm_priority.get(item.name, 0))
collection_state = self.multiworld.get_all_state(False)
# In specific very constrained conditions, fill_restrictive may run
# out of swaps before it finds a valid solution if it gets unlucky.
# This is a band-aid until fill/swap can reliably find those solutions.
attempts_remaining = 2
while attempts_remaining > 0:
attempts_remaining -= 1
self.random.shuffle(hm_locations)
try:
fill_restrictive(self.multiworld, collection_state, hm_locations, hm_items,
single_player_placement=True, lock=True, allow_excluded=True)
break
except FillError as exc:
if attempts_remaining == 0:
raise exc
logging.debug(f"Failed to shuffle HMs for player {self.player}. Retrying.")
continue
def generate_output(self, output_directory: str) -> None:
def randomize_abilities() -> None:
# Creating list of potential abilities
ability_label_to_value = {ability.label.lower(): ability.ability_id for ability in emerald_data.abilities}
ability_blacklist_labels = {"cacophony"}
option_ability_blacklist = self.options.ability_blacklist.value
if option_ability_blacklist is not None:
ability_blacklist_labels |= {ability_label.lower() for ability_label in option_ability_blacklist}
ability_blacklist = {ability_label_to_value[label] for label in ability_blacklist_labels}
ability_whitelist = [a.ability_id for a in emerald_data.abilities if a.ability_id not in ability_blacklist]
if self.options.abilities == RandomizeAbilities.option_follow_evolutions:
already_modified: Set[int] = set()
# Loops through species and only tries to modify abilities if the pokemon has no pre-evolution
# or if the pre-evolution has already been modified. Then tries to modify all species that evolve
# from this one which have the same abilities.
# The outer while loop only runs three times for vanilla ordering: Once for a first pass, once for
# Hitmonlee/Hitmonchan, and once to verify that there's nothing left to do.
while True:
had_clean_pass = True
for species in self.modified_species:
if species is None:
continue
if species.species_id in already_modified:
continue
if species.pre_evolution is not None and species.pre_evolution not in already_modified:
continue
had_clean_pass = False
old_abilities = species.abilities
new_abilities = (
0 if old_abilities[0] == 0 else self.random.choice(ability_whitelist),
0 if old_abilities[1] == 0 else self.random.choice(ability_whitelist)
)
evolutions = [species]
while len(evolutions) > 0:
evolution = evolutions.pop()
if evolution.abilities == old_abilities:
evolution.abilities = new_abilities
already_modified.add(evolution.species_id)
evolutions += [
self.modified_species[evolution.species_id]
for evolution in evolution.evolutions
if evolution.species_id not in already_modified
]
if had_clean_pass:
break
else: # Not following evolutions
for species in self.modified_species:
if species is None:
continue
old_abilities = species.abilities
new_abilities = (
0 if old_abilities[0] == 0 else self.random.choice(ability_whitelist),
0 if old_abilities[1] == 0 else self.random.choice(ability_whitelist)
)
species.abilities = new_abilities
def randomize_types() -> None:
if self.options.types == RandomizeTypes.option_shuffle:
type_map = list(range(18))
self.random.shuffle(type_map)
# We never want to map to the ??? type, so swap whatever index maps to ??? with ???
# So ??? will always map to itself, and there are no pokemon which have the ??? type
mystery_type_index = type_map.index(9)
type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index]
for species in self.modified_species:
if species is not None:
species.types = (type_map[species.types[0]], type_map[species.types[1]])
elif self.options.types == RandomizeTypes.option_completely_random:
for species in self.modified_species:
if species is not None:
new_type_1 = get_random_type(self.random)
new_type_2 = new_type_1
if species.types[0] != species.types[1]:
while new_type_1 == new_type_2:
new_type_2 = get_random_type(self.random)
species.types = (new_type_1, new_type_2)
elif self.options.types == RandomizeTypes.option_follow_evolutions:
already_modified: Set[int] = set()
# Similar to follow evolutions for abilities, but only needs to loop through once.
# For every pokemon without a pre-evolution, generates a random mapping from old types to new types
# and then walks through the evolution tree applying that map. This means that evolutions that share
# types will have those types mapped to the same new types, and evolutions with new or diverging types
# will still have new or diverging types.
# Consider:
# - Charmeleon (Fire/Fire) -> Charizard (Fire/Flying)
# - Onyx (Rock/Ground) -> Steelix (Steel/Ground)
# - Nincada (Bug/Ground) -> Ninjask (Bug/Flying) && Shedinja (Bug/Ghost)
# - Azurill (Normal/Normal) -> Marill (Water/Water)
for species in self.modified_species:
if species is None:
continue
if species.species_id in already_modified:
continue
if species.pre_evolution is not None and species.pre_evolution not in already_modified:
continue
type_map = list(range(18))
self.random.shuffle(type_map)
# We never want to map to the ??? type, so swap whatever index maps to ??? with ???
# So ??? will always map to itself, and there are no pokemon which have the ??? type
mystery_type_index = type_map.index(9)
type_map[mystery_type_index], type_map[9] = type_map[9], type_map[mystery_type_index]
evolutions = [species]
while len(evolutions) > 0:
evolution = evolutions.pop()
evolution.types = (type_map[evolution.types[0]], type_map[evolution.types[1]])
already_modified.add(evolution.species_id)
evolutions += [self.modified_species[evo.species_id] for evo in evolution.evolutions]
def randomize_learnsets() -> None:
type_bias = self.options.move_match_type_bias.value
normal_bias = self.options.move_normal_type_bias.value
for species in self.modified_species:
if species is None:
continue
old_learnset = species.learnset
new_learnset: List[LearnsetMove] = []
i = 0
# Replace filler MOVE_NONEs at start of list
while old_learnset[i].move_id == 0:
if self.options.level_up_moves == LevelUpMoves.option_start_with_four_moves:
new_move = get_random_move(self.random, {move.move_id for move in new_learnset}, type_bias,
normal_bias, species.types)
else:
new_move = 0
new_learnset.append(LearnsetMove(old_learnset[i].level, new_move))
i += 1
while i < len(old_learnset):
# Guarantees the starter has a good damaging move
if i == 3:
new_move = get_random_damaging_move(self.random, {move.move_id for move in new_learnset})
else:
new_move = get_random_move(self.random, {move.move_id for move in new_learnset}, type_bias,
normal_bias, species.types)
new_learnset.append(LearnsetMove(old_learnset[i].level, new_move))
i += 1
species.learnset = new_learnset
def randomize_tm_hm_compatibility() -> None:
for species in self.modified_species:
if species is None:
continue
combatibility_array = int_to_bool_array(species.tm_hm_compatibility)
# TMs
for i in range(0, 50):
if self.options.tm_compatibility == TmCompatibility.option_fully_compatible:
combatibility_array[i] = True
elif self.options.tm_compatibility == TmCompatibility.option_completely_random:
combatibility_array[i] = self.random.choice([True, False])
# HMs
for i in range(50, 58):
if self.options.hm_compatibility == HmCompatibility.option_fully_compatible:
combatibility_array[i] = True
elif self.options.hm_compatibility == HmCompatibility.option_completely_random:
combatibility_array[i] = self.random.choice([True, False])
species.tm_hm_compatibility = bool_array_to_int(combatibility_array)
def randomize_tm_moves() -> None:
new_moves: Set[int] = set()
for i in range(50):
new_move = get_random_move(self.random, new_moves)
new_moves.add(new_move)
self.modified_tmhm_moves[i] = new_move
def randomize_wild_encounters() -> None:
should_match_bst = self.options.wild_pokemon in {
RandomizeWildPokemon.option_match_base_stats,
RandomizeWildPokemon.option_match_base_stats_and_type
}
should_match_type = self.options.wild_pokemon in {
RandomizeWildPokemon.option_match_type,
RandomizeWildPokemon.option_match_base_stats_and_type
}
should_allow_legendaries = self.options.allow_wild_legendaries == Toggle.option_true
for map_data in self.modified_maps:
new_encounters: List[Optional[EncounterTableData]] = [None, None, None]
old_encounters = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
for i, table in enumerate(old_encounters):
if table is not None:
species_old_to_new_map: Dict[int, int] = {}
for species_id in table.slots:
if species_id not in species_old_to_new_map:
original_species = emerald_data.species[species_id]
target_bst = sum(original_species.base_stats) if should_match_bst else None
target_type = self.random.choice(original_species.types) if should_match_type else None
species_old_to_new_map[species_id] = get_random_species(
self.random,
self.modified_species,
target_bst,
target_type,
should_allow_legendaries
).species_id
new_slots: List[int] = []
for species_id in table.slots:
new_slots.append(species_old_to_new_map[species_id])
new_encounters[i] = EncounterTableData(new_slots, table.rom_address)
map_data.land_encounters = new_encounters[0]
map_data.water_encounters = new_encounters[1]
map_data.fishing_encounters = new_encounters[2]
def randomize_static_encounters() -> None:
if self.options.static_encounters == RandomizeStaticEncounters.option_shuffle:
shuffled_species = [encounter.species_id for encounter in emerald_data.static_encounters]
self.random.shuffle(shuffled_species)
self.modified_static_encounters = []
for i, encounter in enumerate(emerald_data.static_encounters):
self.modified_static_encounters.append(StaticEncounterData(
shuffled_species[i],
encounter.rom_address
))
else:
should_match_bst = self.options.static_encounters in {
RandomizeStaticEncounters.option_match_base_stats,
RandomizeStaticEncounters.option_match_base_stats_and_type
}
should_match_type = self.options.static_encounters in {
RandomizeStaticEncounters.option_match_type,
RandomizeStaticEncounters.option_match_base_stats_and_type
}
for encounter in emerald_data.static_encounters:
original_species = self.modified_species[encounter.species_id]
target_bst = sum(original_species.base_stats) if should_match_bst else None
target_type = self.random.choice(original_species.types) if should_match_type else None
self.modified_static_encounters.append(StaticEncounterData(
get_random_species(self.random, self.modified_species, target_bst, target_type).species_id,
encounter.rom_address
))
def randomize_opponent_parties() -> None:
should_match_bst = self.options.trainer_parties in {
RandomizeTrainerParties.option_match_base_stats,
RandomizeTrainerParties.option_match_base_stats_and_type
}
should_match_type = self.options.trainer_parties in {
RandomizeTrainerParties.option_match_type,
RandomizeTrainerParties.option_match_base_stats_and_type
}
allow_legendaries = self.options.allow_trainer_legendaries == Toggle.option_true
per_species_tmhm_moves: Dict[int, List[int]] = {}
for trainer in self.modified_trainers:
new_party = []
for pokemon in trainer.party.pokemon:
original_species = emerald_data.species[pokemon.species_id]
target_bst = sum(original_species.base_stats) if should_match_bst else None
target_type = self.random.choice(original_species.types) if should_match_type else None
new_species = get_random_species(
self.random,
self.modified_species,
target_bst,
target_type,
allow_legendaries
)
if new_species.species_id not in per_species_tmhm_moves:
per_species_tmhm_moves[new_species.species_id] = list({
self.modified_tmhm_moves[i]
for i, is_compatible in enumerate(int_to_bool_array(new_species.tm_hm_compatibility))
if is_compatible
})
tm_hm_movepool = per_species_tmhm_moves[new_species.species_id]
level_up_movepool = list({
move.move_id
for move in new_species.learnset
if move.level <= pokemon.level
})
new_moves = (
self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool),
self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool),
self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool),
self.random.choice(tm_hm_movepool if self.random.random() < 0.25 and len(tm_hm_movepool) > 0 else level_up_movepool)
)
new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves))
trainer.party.pokemon = new_party
def randomize_starters() -> None:
match_bst = self.options.starters in {
RandomizeStarters.option_match_base_stats,
RandomizeStarters.option_match_base_stats_and_type
}
match_type = self.options.starters in {
RandomizeStarters.option_match_type,
RandomizeStarters.option_match_base_stats_and_type
}
allow_legendaries = self.options.allow_starter_legendaries == Toggle.option_true
starter_bsts = (
sum(emerald_data.species[emerald_data.starters[0]].base_stats) if match_bst else None,
sum(emerald_data.species[emerald_data.starters[1]].base_stats) if match_bst else None,
sum(emerald_data.species[emerald_data.starters[2]].base_stats) if match_bst else None
)
starter_types = (
self.random.choice(emerald_data.species[emerald_data.starters[0]].types) if match_type else None,
self.random.choice(emerald_data.species[emerald_data.starters[1]].types) if match_type else None,
self.random.choice(emerald_data.species[emerald_data.starters[2]].types) if match_type else None
)
new_starters = (
get_random_species(self.random, self.modified_species,
starter_bsts[0], starter_types[0], allow_legendaries),
get_random_species(self.random, self.modified_species,
starter_bsts[1], starter_types[1], allow_legendaries),
get_random_species(self.random, self.modified_species,
starter_bsts[2], starter_types[2], allow_legendaries)
)
egg_code = self.options.easter_egg.value
egg_check_1 = 0
egg_check_2 = 0
for i in egg_code:
egg_check_1 += ord(i)
egg_check_2 += egg_check_1 * egg_check_1
egg = 96 + egg_check_2 - (egg_check_1 * 0x077C)
if egg_check_2 == 0x14E03A and egg < 411 and egg > 0 and egg not in range(252, 277):
self.modified_starters = (egg, egg, egg)
else:
self.modified_starters = (
new_starters[0].species_id,
new_starters[1].species_id,
new_starters[2].species_id
)
# Putting the unchosen starter onto the rival's team
rival_teams: List[List[Tuple[str, int, bool]]] = [
[
("TRAINER_BRENDAN_ROUTE_103_TREECKO", 0, False),
("TRAINER_BRENDAN_RUSTBORO_TREECKO", 1, False),
("TRAINER_BRENDAN_ROUTE_110_TREECKO", 2, True ),
("TRAINER_BRENDAN_ROUTE_119_TREECKO", 2, True ),
("TRAINER_BRENDAN_LILYCOVE_TREECKO", 3, True ),
("TRAINER_MAY_ROUTE_103_TREECKO", 0, False),
("TRAINER_MAY_RUSTBORO_TREECKO", 1, False),
("TRAINER_MAY_ROUTE_110_TREECKO", 2, True ),
("TRAINER_MAY_ROUTE_119_TREECKO", 2, True ),
("TRAINER_MAY_LILYCOVE_TREECKO", 3, True )
],
[
("TRAINER_BRENDAN_ROUTE_103_TORCHIC", 0, False),
("TRAINER_BRENDAN_RUSTBORO_TORCHIC", 1, False),
("TRAINER_BRENDAN_ROUTE_110_TORCHIC", 2, True ),
("TRAINER_BRENDAN_ROUTE_119_TORCHIC", 2, True ),
("TRAINER_BRENDAN_LILYCOVE_TORCHIC", 3, True ),
("TRAINER_MAY_ROUTE_103_TORCHIC", 0, False),
("TRAINER_MAY_RUSTBORO_TORCHIC", 1, False),
("TRAINER_MAY_ROUTE_110_TORCHIC", 2, True ),
("TRAINER_MAY_ROUTE_119_TORCHIC", 2, True ),
("TRAINER_MAY_LILYCOVE_TORCHIC", 3, True )
],
[
("TRAINER_BRENDAN_ROUTE_103_MUDKIP", 0, False),
("TRAINER_BRENDAN_RUSTBORO_MUDKIP", 1, False),
("TRAINER_BRENDAN_ROUTE_110_MUDKIP", 2, True ),
("TRAINER_BRENDAN_ROUTE_119_MUDKIP", 2, True ),
("TRAINER_BRENDAN_LILYCOVE_MUDKIP", 3, True ),
("TRAINER_MAY_ROUTE_103_MUDKIP", 0, False),
("TRAINER_MAY_RUSTBORO_MUDKIP", 1, False),
("TRAINER_MAY_ROUTE_110_MUDKIP", 2, True ),
("TRAINER_MAY_ROUTE_119_MUDKIP", 2, True ),
("TRAINER_MAY_LILYCOVE_MUDKIP", 3, True )
]
]
for i, starter in enumerate([new_starters[1], new_starters[2], new_starters[0]]):
potential_evolutions = [evolution.species_id for evolution in starter.evolutions]
picked_evolution = starter.species_id
if len(potential_evolutions) > 0:
picked_evolution = self.random.choice(potential_evolutions)
for trainer_name, starter_position, is_evolved in rival_teams[i]:
trainer_data = self.modified_trainers[emerald_data.constants[trainer_name]]
trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id
self.modified_species = copy.deepcopy(emerald_data.species)
self.modified_trainers = copy.deepcopy(emerald_data.trainers)
self.modified_maps = copy.deepcopy(emerald_data.maps)
self.modified_tmhm_moves = copy.deepcopy(emerald_data.tmhm_moves)
self.modified_static_encounters = copy.deepcopy(emerald_data.static_encounters)
self.modified_starters = copy.deepcopy(emerald_data.starters)
# Randomize species data
if self.options.abilities != RandomizeAbilities.option_vanilla:
randomize_abilities()
if self.options.types != RandomizeTypes.option_vanilla:
randomize_types()
if self.options.level_up_moves != LevelUpMoves.option_vanilla:
randomize_learnsets()
randomize_tm_hm_compatibility() # Options are checked within this function
min_catch_rate = min(self.options.min_catch_rate.value, 255)
for species in self.modified_species:
if species is not None:
species.catch_rate = max(species.catch_rate, min_catch_rate)
if self.options.tm_moves:
randomize_tm_moves()
# Randomize wild encounters
if self.options.wild_pokemon != RandomizeWildPokemon.option_vanilla:
randomize_wild_encounters()
# Randomize static encounters
if self.options.static_encounters != RandomizeStaticEncounters.option_vanilla:
randomize_static_encounters()
# Randomize opponents
if self.options.trainer_parties != RandomizeTrainerParties.option_vanilla:
randomize_opponent_parties()
# Randomize starters
if self.options.starters != RandomizeStarters.option_vanilla:
randomize_starters()
generate_output(self, output_directory)
def fill_slot_data(self) -> Dict[str, Any]:
slot_data = self.options.as_dict(
"goal",
"badges",
"hms",
"key_items",
"bikes",
"rods",
"overworld_items",
"hidden_items",
"npc_gifts",
"require_itemfinder",
"require_flash",
"enable_ferry",
"elite_four_requirement",
"elite_four_count",
"norman_requirement",
"norman_count",
"extra_boulders",
"remove_roadblocks",
"free_fly_location",
"fly_without_badge",
)
slot_data["free_fly_location_id"] = self.free_fly_location_id
return slot_data
def create_item(self, name: str) -> PokemonEmeraldItem:
return self.create_item_by_code(self.item_name_to_id[name])
def create_item_by_code(self, item_code: int) -> PokemonEmeraldItem:
return PokemonEmeraldItem(
self.item_id_to_name[item_code],
get_item_classification(item_code),
item_code,
self.player
)
def create_event(self, name: str) -> PokemonEmeraldItem:
return PokemonEmeraldItem(
name,
ItemClassification.progression,
None,
self.player
)

View File

@ -0,0 +1,277 @@
from typing import TYPE_CHECKING, Dict, Set
from NetUtils import ClientStatus
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
from .data import BASE_OFFSET, data
from .options import Goal
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
EXPECTED_ROM_NAME = "pokemon emerald version / AP 2"
IS_CHAMPION_FLAG = data.constants["FLAG_IS_CHAMPION"]
DEFEATED_STEVEN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_STEVEN"]
DEFEATED_NORMAN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_NORMAN_1"]
# These flags are communicated to the tracker as a bitfield using this order.
# Modifying the order will cause undetectable autotracking issues.
TRACKER_EVENT_FLAGS = [
"FLAG_DEFEATED_RUSTBORO_GYM",
"FLAG_DEFEATED_DEWFORD_GYM",
"FLAG_DEFEATED_MAUVILLE_GYM",
"FLAG_DEFEATED_LAVARIDGE_GYM",
"FLAG_DEFEATED_PETALBURG_GYM",
"FLAG_DEFEATED_FORTREE_GYM",
"FLAG_DEFEATED_MOSSDEEP_GYM",
"FLAG_DEFEATED_SOOTOPOLIS_GYM",
"FLAG_RECEIVED_POKENAV", # Talk to Mr. Stone
"FLAG_DELIVERED_STEVEN_LETTER",
"FLAG_DELIVERED_DEVON_GOODS",
"FLAG_HIDE_ROUTE_119_TEAM_AQUA", # Clear Weather Institute
"FLAG_MET_ARCHIE_METEOR_FALLS", # Magma steals meteorite
"FLAG_GROUDON_AWAKENED_MAGMA_HIDEOUT", # Clear Magma Hideout
"FLAG_MET_TEAM_AQUA_HARBOR", # Aqua steals submarine
"FLAG_TEAM_AQUA_ESCAPED_IN_SUBMARINE", # Clear Aqua Hideout
"FLAG_HIDE_MOSSDEEP_CITY_SPACE_CENTER_MAGMA_NOTE", # Clear Space Center
"FLAG_KYOGRE_ESCAPED_SEAFLOOR_CAVERN",
"FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA", # Rayquaza departs for Sootopolis
"FLAG_OMIT_DIVE_FROM_STEVEN_LETTER", # Steven gives Dive HM (clears seafloor cavern grunt)
"FLAG_IS_CHAMPION",
"FLAG_PURCHASED_HARBOR_MAIL"
]
EVENT_FLAG_MAP = {data.constants[flag_name]: flag_name for flag_name in TRACKER_EVENT_FLAGS}
KEY_LOCATION_FLAGS = [
"NPC_GIFT_RECEIVED_HM01",
"NPC_GIFT_RECEIVED_HM02",
"NPC_GIFT_RECEIVED_HM03",
"NPC_GIFT_RECEIVED_HM04",
"NPC_GIFT_RECEIVED_HM05",
"NPC_GIFT_RECEIVED_HM06",
"NPC_GIFT_RECEIVED_HM07",
"NPC_GIFT_RECEIVED_HM08",
"NPC_GIFT_RECEIVED_ACRO_BIKE",
"NPC_GIFT_RECEIVED_WAILMER_PAIL",
"NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL",
"NPC_GIFT_RECEIVED_LETTER",
"NPC_GIFT_RECEIVED_METEORITE",
"NPC_GIFT_RECEIVED_GO_GOGGLES",
"NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON",
"NPC_GIFT_RECEIVED_ITEMFINDER",
"NPC_GIFT_RECEIVED_DEVON_SCOPE",
"NPC_GIFT_RECEIVED_MAGMA_EMBLEM",
"NPC_GIFT_RECEIVED_POKEBLOCK_CASE",
"NPC_GIFT_RECEIVED_SS_TICKET",
"HIDDEN_ITEM_ABANDONED_SHIP_RM_2_KEY",
"HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY",
"HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY",
"HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY",
"ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_4_SCANNER",
"ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY",
"NPC_GIFT_RECEIVED_OLD_ROD",
"NPC_GIFT_RECEIVED_GOOD_ROD",
"NPC_GIFT_RECEIVED_SUPER_ROD",
]
KEY_LOCATION_FLAG_MAP = {data.locations[location_name].flag: location_name for location_name in KEY_LOCATION_FLAGS}
class PokemonEmeraldClient(BizHawkClient):
game = "Pokemon Emerald"
system = "GBA"
patch_suffix = ".apemerald"
local_checked_locations: Set[int]
local_set_events: Dict[str, bool]
local_found_key_items: Dict[str, bool]
goal_flag: int
def __init__(self) -> None:
super().__init__()
self.local_checked_locations = set()
self.local_set_events = {}
self.local_found_key_items = {}
self.goal_flag = IS_CHAMPION_FLAG
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
try:
# Check ROM name/patch version
rom_name_bytes = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x108, 32, "ROM")]))[0])
rom_name = bytes([byte for byte in rom_name_bytes if byte != 0]).decode("ascii")
if not rom_name.startswith("pokemon emerald version"):
return False
if rom_name == "pokemon emerald version":
logger.info("ERROR: You appear to be running an unpatched version of Pokemon Emerald. "
"You need to generate a patch file and use it to create a patched ROM.")
return False
if rom_name != EXPECTED_ROM_NAME:
logger.info("ERROR: The patch file used to create this ROM is not compatible with "
"this client. Double check your client version against the version being "
"used by the generator.")
return False
except UnicodeDecodeError:
return False
except bizhawk.RequestFailedError:
return False # Should verify on the next pass
ctx.game = self.game
ctx.items_handling = 0b001
ctx.want_slot_data = True
ctx.watcher_timeout = 0.125
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
slot_name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(data.rom_addresses["gArchipelagoInfo"], 64, "ROM")]))[0]
ctx.auth = bytes([byte for byte in slot_name_bytes if byte != 0]).decode("utf-8")
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
if ctx.slot_data is not None:
if ctx.slot_data["goal"] == Goal.option_champion:
self.goal_flag = IS_CHAMPION_FLAG
elif ctx.slot_data["goal"] == Goal.option_steven:
self.goal_flag = DEFEATED_STEVEN_FLAG
elif ctx.slot_data["goal"] == Goal.option_norman:
self.goal_flag = DEFEATED_NORMAN_FLAG
try:
# Checks that the player is in the overworld
overworld_guard = (data.ram_addresses["gMain"] + 4, (data.ram_addresses["CB2_Overworld"] + 1).to_bytes(4, "little"), "System Bus")
# Read save block address
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx,
[(data.ram_addresses["gSaveBlock1Ptr"], 4, "System Bus")],
[overworld_guard]
)
if read_result is None: # Not in overworld
return
# Checks that the save block hasn't moved
save_block_address_guard = (data.ram_addresses["gSaveBlock1Ptr"], read_result[0], "System Bus")
save_block_address = int.from_bytes(read_result[0], "little")
# Handle giving the player items
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx,
[
(save_block_address + 0x3778, 2, "System Bus"), # Number of received items
(data.ram_addresses["gArchipelagoReceivedItem"] + 4, 1, "System Bus") # Received item struct full?
],
[overworld_guard, save_block_address_guard]
)
if read_result is None: # Not in overworld, or save block moved
return
num_received_items = int.from_bytes(read_result[0], "little")
received_item_is_empty = read_result[1][0] == 0
# If the game hasn't received all items yet and the received item struct doesn't contain an item, then
# fill it with the next item
if num_received_items < len(ctx.items_received) and received_item_is_empty:
next_item = ctx.items_received[num_received_items]
await bizhawk.write(ctx.bizhawk_ctx, [
(data.ram_addresses["gArchipelagoReceivedItem"] + 0, (next_item.item - BASE_OFFSET).to_bytes(2, "little"), "System Bus"),
(data.ram_addresses["gArchipelagoReceivedItem"] + 2, (num_received_items + 1).to_bytes(2, "little"), "System Bus"),
(data.ram_addresses["gArchipelagoReceivedItem"] + 4, [1], "System Bus"), # Mark struct full
(data.ram_addresses["gArchipelagoReceivedItem"] + 5, [next_item.flags & 1], "System Bus"),
])
# Read flags in 2 chunks
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx,
[(save_block_address + 0x1450, 0x96, "System Bus")], # Flags
[overworld_guard, save_block_address_guard]
)
if read_result is None: # Not in overworld, or save block moved
return
flag_bytes = read_result[0]
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx,
[(save_block_address + 0x14E6, 0x96, "System Bus")], # Flags
[overworld_guard, save_block_address_guard]
)
if read_result is not None:
flag_bytes += read_result[0]
game_clear = False
local_checked_locations = set()
local_set_events = {flag_name: False for flag_name in TRACKER_EVENT_FLAGS}
local_found_key_items = {location_name: False for location_name in KEY_LOCATION_FLAGS}
# Check set flags
for byte_i, byte in enumerate(flag_bytes):
for i in range(8):
if byte & (1 << i) != 0:
flag_id = byte_i * 8 + i
location_id = flag_id + BASE_OFFSET
if location_id in ctx.server_locations:
local_checked_locations.add(location_id)
if flag_id == self.goal_flag:
game_clear = True
if flag_id in EVENT_FLAG_MAP:
local_set_events[EVENT_FLAG_MAP[flag_id]] = True
if flag_id in KEY_LOCATION_FLAG_MAP:
local_found_key_items[KEY_LOCATION_FLAG_MAP[flag_id]] = True
# Send locations
if local_checked_locations != self.local_checked_locations:
self.local_checked_locations = local_checked_locations
if local_checked_locations is not None:
await ctx.send_msgs([{
"cmd": "LocationChecks",
"locations": list(local_checked_locations)
}])
# Send game clear
if not ctx.finished_game and game_clear:
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL
}])
# Send tracker event flags
if local_set_events != self.local_set_events and ctx.slot is not None:
event_bitfield = 0
for i, flag_name in enumerate(TRACKER_EVENT_FLAGS):
if local_set_events[flag_name]:
event_bitfield |= 1 << i
await ctx.send_msgs([{
"cmd": "Set",
"key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [{"operation": "replace", "value": event_bitfield}]
}])
self.local_set_events = local_set_events
if local_found_key_items != self.local_found_key_items:
key_bitfield = 0
for i, location_name in enumerate(KEY_LOCATION_FLAGS):
if local_found_key_items[location_name]:
key_bitfield |= 1 << i
await ctx.send_msgs([{
"cmd": "Set",
"key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [{"operation": "replace", "value": key_bitfield}]
}])
self.local_found_key_items = local_found_key_items
except bizhawk.RequestFailedError:
# Exit handler and return to main loop to reconnect
pass

View File

@ -0,0 +1,995 @@
"""
Pulls data from JSON files in worlds/pokemon_emerald/data/ into classes.
This also includes marrying automatically extracted data with manually
defined data (like location labels or usable pokemon species), some cleanup
and sorting, and Warp methods.
"""
from dataclasses import dataclass
import copy
from enum import IntEnum
import orjson
from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union
import pkgutil
import pkg_resources
from BaseClasses import ItemClassification
BASE_OFFSET = 3860000
class Warp:
"""
Represents warp events in the game like doorways or warp pads
"""
is_one_way: bool
source_map: str
source_ids: List[int]
dest_map: str
dest_ids: List[int]
parent_region: Optional[str]
def __init__(self, encoded_string: Optional[str] = None, parent_region: Optional[str] = None) -> None:
if encoded_string is not None:
decoded_warp = Warp.decode(encoded_string)
self.is_one_way = decoded_warp.is_one_way
self.source_map = decoded_warp.source_map
self.source_ids = decoded_warp.source_ids
self.dest_map = decoded_warp.dest_map
self.dest_ids = decoded_warp.dest_ids
self.parent_region = parent_region
def encode(self) -> str:
"""
Returns a string encoding of this warp
"""
source_ids_string = ""
for source_id in self.source_ids:
source_ids_string += str(source_id) + ","
source_ids_string = source_ids_string[:-1] # Remove last ","
dest_ids_string = ""
for dest_id in self.dest_ids:
dest_ids_string += str(dest_id) + ","
dest_ids_string = dest_ids_string[:-1] # Remove last ","
return f"{self.source_map}:{source_ids_string}/{self.dest_map}:{dest_ids_string}{'!' if self.is_one_way else ''}"
def connects_to(self, other: 'Warp') -> bool:
"""
Returns true if this warp sends the player to `other`
"""
return self.dest_map == other.source_map and set(self.dest_ids) <= set(other.source_ids)
@staticmethod
def decode(encoded_string: str) -> 'Warp':
"""
Create a Warp object from an encoded string
"""
warp = Warp()
warp.is_one_way = encoded_string.endswith("!")
if warp.is_one_way:
encoded_string = encoded_string[:-1]
warp_source, warp_dest = encoded_string.split("/")
warp_source_map, warp_source_indices = warp_source.split(":")
warp_dest_map, warp_dest_indices = warp_dest.split(":")
warp.source_map = warp_source_map
warp.dest_map = warp_dest_map
warp.source_ids = [int(index) for index in warp_source_indices.split(",")]
warp.dest_ids = [int(index) for index in warp_dest_indices.split(",")]
return warp
class ItemData(NamedTuple):
label: str
item_id: int
classification: ItemClassification
tags: FrozenSet[str]
class LocationData(NamedTuple):
name: str
label: str
parent_region: str
default_item: int
rom_address: int
flag: int
tags: FrozenSet[str]
class EventData(NamedTuple):
name: str
parent_region: str
class RegionData:
name: str
exits: List[str]
warps: List[str]
locations: List[str]
events: List[EventData]
def __init__(self, name: str):
self.name = name
self.exits = []
self.warps = []
self.locations = []
self.events = []
class BaseStats(NamedTuple):
hp: int
attack: int
defense: int
speed: int
special_attack: int
special_defense: int
class LearnsetMove(NamedTuple):
level: int
move_id: int
class EvolutionMethodEnum(IntEnum):
LEVEL = 0
LEVEL_ATK_LT_DEF = 1
LEVEL_ATK_EQ_DEF = 2
LEVEL_ATK_GT_DEF = 3
LEVEL_SILCOON = 4
LEVEL_CASCOON = 5
LEVEL_NINJASK = 6
LEVEL_SHEDINJA = 7
ITEM = 8
FRIENDSHIP = 9
FRIENDSHIP_DAY = 10
FRIENDSHIP_NIGHT = 11
def _str_to_evolution_method(string: str) -> EvolutionMethodEnum:
if string == "LEVEL":
return EvolutionMethodEnum.LEVEL
if string == "LEVEL_ATK_LT_DEF":
return EvolutionMethodEnum.LEVEL_ATK_LT_DEF
if string == "LEVEL_ATK_EQ_DEF":
return EvolutionMethodEnum.LEVEL_ATK_EQ_DEF
if string == "LEVEL_ATK_GT_DEF":
return EvolutionMethodEnum.LEVEL_ATK_GT_DEF
if string == "LEVEL_SILCOON":
return EvolutionMethodEnum.LEVEL_SILCOON
if string == "LEVEL_CASCOON":
return EvolutionMethodEnum.LEVEL_CASCOON
if string == "LEVEL_NINJASK":
return EvolutionMethodEnum.LEVEL_NINJASK
if string == "LEVEL_SHEDINJA":
return EvolutionMethodEnum.LEVEL_SHEDINJA
if string == "FRIENDSHIP":
return EvolutionMethodEnum.FRIENDSHIP
if string == "FRIENDSHIP_DAY":
return EvolutionMethodEnum.FRIENDSHIP_DAY
if string == "FRIENDSHIP_NIGHT":
return EvolutionMethodEnum.FRIENDSHIP_NIGHT
class EvolutionData(NamedTuple):
method: EvolutionMethodEnum
param: int
species_id: int
class StaticEncounterData(NamedTuple):
species_id: int
rom_address: int
@dataclass
class SpeciesData:
name: str
label: str
species_id: int
base_stats: BaseStats
types: Tuple[int, int]
abilities: Tuple[int, int]
evolutions: List[EvolutionData]
pre_evolution: Optional[int]
catch_rate: int
learnset: List[LearnsetMove]
tm_hm_compatibility: int
learnset_rom_address: int
rom_address: int
class AbilityData(NamedTuple):
ability_id: int
label: str
class EncounterTableData(NamedTuple):
slots: List[int]
rom_address: int
@dataclass
class MapData:
name: str
land_encounters: Optional[EncounterTableData]
water_encounters: Optional[EncounterTableData]
fishing_encounters: Optional[EncounterTableData]
class TrainerPokemonDataTypeEnum(IntEnum):
NO_ITEM_DEFAULT_MOVES = 0
ITEM_DEFAULT_MOVES = 1
NO_ITEM_CUSTOM_MOVES = 2
ITEM_CUSTOM_MOVES = 3
def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum:
if string == "NO_ITEM_DEFAULT_MOVES":
return TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES
if string == "ITEM_DEFAULT_MOVES":
return TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES
if string == "NO_ITEM_CUSTOM_MOVES":
return TrainerPokemonDataTypeEnum.NO_ITEM_CUSTOM_MOVES
if string == "ITEM_CUSTOM_MOVES":
return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES
@dataclass
class TrainerPokemonData:
species_id: int
level: int
moves: Optional[Tuple[int, int, int, int]]
@dataclass
class TrainerPartyData:
pokemon: List[TrainerPokemonData]
pokemon_data_type: TrainerPokemonDataTypeEnum
rom_address: int
@dataclass
class TrainerData:
trainer_id: int
party: TrainerPartyData
rom_address: int
battle_script_rom_address: int
class PokemonEmeraldData:
starters: Tuple[int, int, int]
constants: Dict[str, int]
ram_addresses: Dict[str, int]
rom_addresses: Dict[str, int]
regions: Dict[str, RegionData]
locations: Dict[str, LocationData]
items: Dict[int, ItemData]
species: List[Optional[SpeciesData]]
static_encounters: List[StaticEncounterData]
tmhm_moves: List[int]
abilities: List[AbilityData]
maps: List[MapData]
warps: Dict[str, Warp]
warp_map: Dict[str, Optional[str]]
trainers: List[TrainerData]
def __init__(self) -> None:
self.starters = (277, 280, 283)
self.constants = {}
self.ram_addresses = {}
self.rom_addresses = {}
self.regions = {}
self.locations = {}
self.items = {}
self.species = []
self.static_encounters = []
self.tmhm_moves = []
self.abilities = []
self.maps = []
self.warps = {}
self.warp_map = {}
self.trainers = []
def load_json_data(data_name: str) -> Union[List[Any], Dict[str, Any]]:
return orjson.loads(pkgutil.get_data(__name__, "data/" + data_name).decode('utf-8-sig'))
data = PokemonEmeraldData()
def create_data_copy() -> PokemonEmeraldData:
new_copy = PokemonEmeraldData()
new_copy.species = copy.deepcopy(data.species)
new_copy.tmhm_moves = copy.deepcopy(data.tmhm_moves)
new_copy.maps = copy.deepcopy(data.maps)
new_copy.static_encounters = copy.deepcopy(data.static_encounters)
new_copy.trainers = copy.deepcopy(data.trainers)
def _init() -> None:
extracted_data: Dict[str, Any] = load_json_data("extracted_data.json")
data.constants = extracted_data["constants"]
data.ram_addresses = extracted_data["misc_ram_addresses"]
data.rom_addresses = extracted_data["misc_rom_addresses"]
location_attributes_json = load_json_data("locations.json")
# Load/merge region json files
region_json_list = []
for file in pkg_resources.resource_listdir(__name__, "data/regions"):
if not pkg_resources.resource_isdir(__name__, "data/regions/" + file):
region_json_list.append(load_json_data("regions/" + file))
regions_json = {}
for region_subset in region_json_list:
for region_name, region_json in region_subset.items():
if region_name in regions_json:
raise AssertionError("Region [{region_name}] was defined multiple times")
regions_json[region_name] = region_json
# Create region data
claimed_locations: Set[str] = set()
claimed_warps: Set[str] = set()
data.regions = {}
for region_name, region_json in regions_json.items():
new_region = RegionData(region_name)
# Locations
for location_name in region_json["locations"]:
if location_name in claimed_locations:
raise AssertionError(f"Location [{location_name}] was claimed by multiple regions")
location_json = extracted_data["locations"][location_name]
new_location = LocationData(
location_name,
location_attributes_json[location_name]["label"],
region_name,
location_json["default_item"],
location_json["rom_address"],
location_json["flag"],
frozenset(location_attributes_json[location_name]["tags"])
)
new_region.locations.append(location_name)
data.locations[location_name] = new_location
claimed_locations.add(location_name)
new_region.locations.sort()
# Events
for event in region_json["events"]:
new_region.events.append(EventData(event, region_name))
# Exits
for region_exit in region_json["exits"]:
new_region.exits.append(region_exit)
# Warps
for encoded_warp in region_json["warps"]:
if encoded_warp in claimed_warps:
raise AssertionError(f"Warp [{encoded_warp}] was claimed by multiple regions")
new_region.warps.append(encoded_warp)
data.warps[encoded_warp] = Warp(encoded_warp, region_name)
claimed_warps.add(encoded_warp)
new_region.warps.sort()
data.regions[region_name] = new_region
# Create item data
items_json = load_json_data("items.json")
data.items = {}
for item_constant_name, attributes in items_json.items():
item_classification = None
if attributes["classification"] == "PROGRESSION":
item_classification = ItemClassification.progression
elif attributes["classification"] == "USEFUL":
item_classification = ItemClassification.useful
elif attributes["classification"] == "FILLER":
item_classification = ItemClassification.filler
elif attributes["classification"] == "TRAP":
item_classification = ItemClassification.trap
else:
raise ValueError(f"Unknown classification {attributes['classification']} for item {item_constant_name}")
data.items[data.constants[item_constant_name]] = ItemData(
attributes["label"],
data.constants[item_constant_name],
item_classification,
frozenset(attributes["tags"])
)
# Create species data
# Excludes extras like copies of Unown and special species values like SPECIES_EGG.
all_species: List[Tuple[str, str]] = [
("SPECIES_BULBASAUR", "Bulbasaur"),
("SPECIES_IVYSAUR", "Ivysaur"),
("SPECIES_VENUSAUR", "Venusaur"),
("SPECIES_CHARMANDER", "Charmander"),
("SPECIES_CHARMELEON", "Charmeleon"),
("SPECIES_CHARIZARD", "Charizard"),
("SPECIES_SQUIRTLE", "Squirtle"),
("SPECIES_WARTORTLE", "Wartortle"),
("SPECIES_BLASTOISE", "Blastoise"),
("SPECIES_CATERPIE", "Caterpie"),
("SPECIES_METAPOD", "Metapod"),
("SPECIES_BUTTERFREE", "Butterfree"),
("SPECIES_WEEDLE", "Weedle"),
("SPECIES_KAKUNA", "Kakuna"),
("SPECIES_BEEDRILL", "Beedrill"),
("SPECIES_PIDGEY", "Pidgey"),
("SPECIES_PIDGEOTTO", "Pidgeotto"),
("SPECIES_PIDGEOT", "Pidgeot"),
("SPECIES_RATTATA", "Rattata"),
("SPECIES_RATICATE", "Raticate"),
("SPECIES_SPEAROW", "Spearow"),
("SPECIES_FEAROW", "Fearow"),
("SPECIES_EKANS", "Ekans"),
("SPECIES_ARBOK", "Arbok"),
("SPECIES_PIKACHU", "Pikachu"),
("SPECIES_RAICHU", "Raichu"),
("SPECIES_SANDSHREW", "Sandshrew"),
("SPECIES_SANDSLASH", "Sandslash"),
("SPECIES_NIDORAN_F", "Nidoran Female"),
("SPECIES_NIDORINA", "Nidorina"),
("SPECIES_NIDOQUEEN", "Nidoqueen"),
("SPECIES_NIDORAN_M", "Nidoran Male"),
("SPECIES_NIDORINO", "Nidorino"),
("SPECIES_NIDOKING", "Nidoking"),
("SPECIES_CLEFAIRY", "Clefairy"),
("SPECIES_CLEFABLE", "Clefable"),
("SPECIES_VULPIX", "Vulpix"),
("SPECIES_NINETALES", "Ninetales"),
("SPECIES_JIGGLYPUFF", "Jigglypuff"),
("SPECIES_WIGGLYTUFF", "Wigglytuff"),
("SPECIES_ZUBAT", "Zubat"),
("SPECIES_GOLBAT", "Golbat"),
("SPECIES_ODDISH", "Oddish"),
("SPECIES_GLOOM", "Gloom"),
("SPECIES_VILEPLUME", "Vileplume"),
("SPECIES_PARAS", "Paras"),
("SPECIES_PARASECT", "Parasect"),
("SPECIES_VENONAT", "Venonat"),
("SPECIES_VENOMOTH", "Venomoth"),
("SPECIES_DIGLETT", "Diglett"),
("SPECIES_DUGTRIO", "Dugtrio"),
("SPECIES_MEOWTH", "Meowth"),
("SPECIES_PERSIAN", "Persian"),
("SPECIES_PSYDUCK", "Psyduck"),
("SPECIES_GOLDUCK", "Golduck"),
("SPECIES_MANKEY", "Mankey"),
("SPECIES_PRIMEAPE", "Primeape"),
("SPECIES_GROWLITHE", "Growlithe"),
("SPECIES_ARCANINE", "Arcanine"),
("SPECIES_POLIWAG", "Poliwag"),
("SPECIES_POLIWHIRL", "Poliwhirl"),
("SPECIES_POLIWRATH", "Poliwrath"),
("SPECIES_ABRA", "Abra"),
("SPECIES_KADABRA", "Kadabra"),
("SPECIES_ALAKAZAM", "Alakazam"),
("SPECIES_MACHOP", "Machop"),
("SPECIES_MACHOKE", "Machoke"),
("SPECIES_MACHAMP", "Machamp"),
("SPECIES_BELLSPROUT", "Bellsprout"),
("SPECIES_WEEPINBELL", "Weepinbell"),
("SPECIES_VICTREEBEL", "Victreebel"),
("SPECIES_TENTACOOL", "Tentacool"),
("SPECIES_TENTACRUEL", "Tentacruel"),
("SPECIES_GEODUDE", "Geodude"),
("SPECIES_GRAVELER", "Graveler"),
("SPECIES_GOLEM", "Golem"),
("SPECIES_PONYTA", "Ponyta"),
("SPECIES_RAPIDASH", "Rapidash"),
("SPECIES_SLOWPOKE", "Slowpoke"),
("SPECIES_SLOWBRO", "Slowbro"),
("SPECIES_MAGNEMITE", "Magnemite"),
("SPECIES_MAGNETON", "Magneton"),
("SPECIES_FARFETCHD", "Farfetch'd"),
("SPECIES_DODUO", "Doduo"),
("SPECIES_DODRIO", "Dodrio"),
("SPECIES_SEEL", "Seel"),
("SPECIES_DEWGONG", "Dewgong"),
("SPECIES_GRIMER", "Grimer"),
("SPECIES_MUK", "Muk"),
("SPECIES_SHELLDER", "Shellder"),
("SPECIES_CLOYSTER", "Cloyster"),
("SPECIES_GASTLY", "Gastly"),
("SPECIES_HAUNTER", "Haunter"),
("SPECIES_GENGAR", "Gengar"),
("SPECIES_ONIX", "Onix"),
("SPECIES_DROWZEE", "Drowzee"),
("SPECIES_HYPNO", "Hypno"),
("SPECIES_KRABBY", "Krabby"),
("SPECIES_KINGLER", "Kingler"),
("SPECIES_VOLTORB", "Voltorb"),
("SPECIES_ELECTRODE", "Electrode"),
("SPECIES_EXEGGCUTE", "Exeggcute"),
("SPECIES_EXEGGUTOR", "Exeggutor"),
("SPECIES_CUBONE", "Cubone"),
("SPECIES_MAROWAK", "Marowak"),
("SPECIES_HITMONLEE", "Hitmonlee"),
("SPECIES_HITMONCHAN", "Hitmonchan"),
("SPECIES_LICKITUNG", "Lickitung"),
("SPECIES_KOFFING", "Koffing"),
("SPECIES_WEEZING", "Weezing"),
("SPECIES_RHYHORN", "Rhyhorn"),
("SPECIES_RHYDON", "Rhydon"),
("SPECIES_CHANSEY", "Chansey"),
("SPECIES_TANGELA", "Tangela"),
("SPECIES_KANGASKHAN", "Kangaskhan"),
("SPECIES_HORSEA", "Horsea"),
("SPECIES_SEADRA", "Seadra"),
("SPECIES_GOLDEEN", "Goldeen"),
("SPECIES_SEAKING", "Seaking"),
("SPECIES_STARYU", "Staryu"),
("SPECIES_STARMIE", "Starmie"),
("SPECIES_MR_MIME", "Mr. Mime"),
("SPECIES_SCYTHER", "Scyther"),
("SPECIES_JYNX", "Jynx"),
("SPECIES_ELECTABUZZ", "Electabuzz"),
("SPECIES_MAGMAR", "Magmar"),
("SPECIES_PINSIR", "Pinsir"),
("SPECIES_TAUROS", "Tauros"),
("SPECIES_MAGIKARP", "Magikarp"),
("SPECIES_GYARADOS", "Gyarados"),
("SPECIES_LAPRAS", "Lapras"),
("SPECIES_DITTO", "Ditto"),
("SPECIES_EEVEE", "Eevee"),
("SPECIES_VAPOREON", "Vaporeon"),
("SPECIES_JOLTEON", "Jolteon"),
("SPECIES_FLAREON", "Flareon"),
("SPECIES_PORYGON", "Porygon"),
("SPECIES_OMANYTE", "Omanyte"),
("SPECIES_OMASTAR", "Omastar"),
("SPECIES_KABUTO", "Kabuto"),
("SPECIES_KABUTOPS", "Kabutops"),
("SPECIES_AERODACTYL", "Aerodactyl"),
("SPECIES_SNORLAX", "Snorlax"),
("SPECIES_ARTICUNO", "Articuno"),
("SPECIES_ZAPDOS", "Zapdos"),
("SPECIES_MOLTRES", "Moltres"),
("SPECIES_DRATINI", "Dratini"),
("SPECIES_DRAGONAIR", "Dragonair"),
("SPECIES_DRAGONITE", "Dragonite"),
("SPECIES_MEWTWO", "Mewtwo"),
("SPECIES_MEW", "Mew"),
("SPECIES_CHIKORITA", "Chikorita"),
("SPECIES_BAYLEEF", "Bayleaf"),
("SPECIES_MEGANIUM", "Meganium"),
("SPECIES_CYNDAQUIL", "Cindaquil"),
("SPECIES_QUILAVA", "Quilava"),
("SPECIES_TYPHLOSION", "Typhlosion"),
("SPECIES_TOTODILE", "Totodile"),
("SPECIES_CROCONAW", "Croconaw"),
("SPECIES_FERALIGATR", "Feraligatr"),
("SPECIES_SENTRET", "Sentret"),
("SPECIES_FURRET", "Furret"),
("SPECIES_HOOTHOOT", "Hoothoot"),
("SPECIES_NOCTOWL", "Noctowl"),
("SPECIES_LEDYBA", "Ledyba"),
("SPECIES_LEDIAN", "Ledian"),
("SPECIES_SPINARAK", "Spinarak"),
("SPECIES_ARIADOS", "Ariados"),
("SPECIES_CROBAT", "Crobat"),
("SPECIES_CHINCHOU", "Chinchou"),
("SPECIES_LANTURN", "Lanturn"),
("SPECIES_PICHU", "Pichu"),
("SPECIES_CLEFFA", "Cleffa"),
("SPECIES_IGGLYBUFF", "Igglybuff"),
("SPECIES_TOGEPI", "Togepi"),
("SPECIES_TOGETIC", "Togetic"),
("SPECIES_NATU", "Natu"),
("SPECIES_XATU", "Xatu"),
("SPECIES_MAREEP", "Mareep"),
("SPECIES_FLAAFFY", "Flaafy"),
("SPECIES_AMPHAROS", "Ampharos"),
("SPECIES_BELLOSSOM", "Bellossom"),
("SPECIES_MARILL", "Marill"),
("SPECIES_AZUMARILL", "Azumarill"),
("SPECIES_SUDOWOODO", "Sudowoodo"),
("SPECIES_POLITOED", "Politoed"),
("SPECIES_HOPPIP", "Hoppip"),
("SPECIES_SKIPLOOM", "Skiploom"),
("SPECIES_JUMPLUFF", "Jumpluff"),
("SPECIES_AIPOM", "Aipom"),
("SPECIES_SUNKERN", "Sunkern"),
("SPECIES_SUNFLORA", "Sunflora"),
("SPECIES_YANMA", "Yanma"),
("SPECIES_WOOPER", "Wooper"),
("SPECIES_QUAGSIRE", "Quagsire"),
("SPECIES_ESPEON", "Espeon"),
("SPECIES_UMBREON", "Umbreon"),
("SPECIES_MURKROW", "Murkrow"),
("SPECIES_SLOWKING", "Slowking"),
("SPECIES_MISDREAVUS", "Misdreavus"),
("SPECIES_UNOWN", "Unown"),
("SPECIES_WOBBUFFET", "Wobbuffet"),
("SPECIES_GIRAFARIG", "Girafarig"),
("SPECIES_PINECO", "Pineco"),
("SPECIES_FORRETRESS", "Forretress"),
("SPECIES_DUNSPARCE", "Dunsparce"),
("SPECIES_GLIGAR", "Gligar"),
("SPECIES_STEELIX", "Steelix"),
("SPECIES_SNUBBULL", "Snubbull"),
("SPECIES_GRANBULL", "Granbull"),
("SPECIES_QWILFISH", "Qwilfish"),
("SPECIES_SCIZOR", "Scizor"),
("SPECIES_SHUCKLE", "Shuckle"),
("SPECIES_HERACROSS", "Heracross"),
("SPECIES_SNEASEL", "Sneasel"),
("SPECIES_TEDDIURSA", "Teddiursa"),
("SPECIES_URSARING", "Ursaring"),
("SPECIES_SLUGMA", "Slugma"),
("SPECIES_MAGCARGO", "Magcargo"),
("SPECIES_SWINUB", "Swinub"),
("SPECIES_PILOSWINE", "Piloswine"),
("SPECIES_CORSOLA", "Corsola"),
("SPECIES_REMORAID", "Remoraid"),
("SPECIES_OCTILLERY", "Octillery"),
("SPECIES_DELIBIRD", "Delibird"),
("SPECIES_MANTINE", "Mantine"),
("SPECIES_SKARMORY", "Skarmory"),
("SPECIES_HOUNDOUR", "Houndour"),
("SPECIES_HOUNDOOM", "Houndoom"),
("SPECIES_KINGDRA", "Kingdra"),
("SPECIES_PHANPY", "Phanpy"),
("SPECIES_DONPHAN", "Donphan"),
("SPECIES_PORYGON2", "Porygon2"),
("SPECIES_STANTLER", "Stantler"),
("SPECIES_SMEARGLE", "Smeargle"),
("SPECIES_TYROGUE", "Tyrogue"),
("SPECIES_HITMONTOP", "Hitmontop"),
("SPECIES_SMOOCHUM", "Smoochum"),
("SPECIES_ELEKID", "Elekid"),
("SPECIES_MAGBY", "Magby"),
("SPECIES_MILTANK", "Miltank"),
("SPECIES_BLISSEY", "Blissey"),
("SPECIES_RAIKOU", "Raikou"),
("SPECIES_ENTEI", "Entei"),
("SPECIES_SUICUNE", "Suicune"),
("SPECIES_LARVITAR", "Larvitar"),
("SPECIES_PUPITAR", "Pupitar"),
("SPECIES_TYRANITAR", "Tyranitar"),
("SPECIES_LUGIA", "Lugia"),
("SPECIES_HO_OH", "Ho-oh"),
("SPECIES_CELEBI", "Celebi"),
("SPECIES_TREECKO", "Treecko"),
("SPECIES_GROVYLE", "Grovyle"),
("SPECIES_SCEPTILE", "Sceptile"),
("SPECIES_TORCHIC", "Torchic"),
("SPECIES_COMBUSKEN", "Combusken"),
("SPECIES_BLAZIKEN", "Blaziken"),
("SPECIES_MUDKIP", "Mudkip"),
("SPECIES_MARSHTOMP", "Marshtomp"),
("SPECIES_SWAMPERT", "Swampert"),
("SPECIES_POOCHYENA", "Poochyena"),
("SPECIES_MIGHTYENA", "Mightyena"),
("SPECIES_ZIGZAGOON", "Zigzagoon"),
("SPECIES_LINOONE", "Linoon"),
("SPECIES_WURMPLE", "Wurmple"),
("SPECIES_SILCOON", "Silcoon"),
("SPECIES_BEAUTIFLY", "Beautifly"),
("SPECIES_CASCOON", "Cascoon"),
("SPECIES_DUSTOX", "Dustox"),
("SPECIES_LOTAD", "Lotad"),
("SPECIES_LOMBRE", "Lombre"),
("SPECIES_LUDICOLO", "Ludicolo"),
("SPECIES_SEEDOT", "Seedot"),
("SPECIES_NUZLEAF", "Nuzleaf"),
("SPECIES_SHIFTRY", "Shiftry"),
("SPECIES_NINCADA", "Nincada"),
("SPECIES_NINJASK", "Ninjask"),
("SPECIES_SHEDINJA", "Shedinja"),
("SPECIES_TAILLOW", "Taillow"),
("SPECIES_SWELLOW", "Swellow"),
("SPECIES_SHROOMISH", "Shroomish"),
("SPECIES_BRELOOM", "Breloom"),
("SPECIES_SPINDA", "Spinda"),
("SPECIES_WINGULL", "Wingull"),
("SPECIES_PELIPPER", "Pelipper"),
("SPECIES_SURSKIT", "Surskit"),
("SPECIES_MASQUERAIN", "Masquerain"),
("SPECIES_WAILMER", "Wailmer"),
("SPECIES_WAILORD", "Wailord"),
("SPECIES_SKITTY", "Skitty"),
("SPECIES_DELCATTY", "Delcatty"),
("SPECIES_KECLEON", "Kecleon"),
("SPECIES_BALTOY", "Baltoy"),
("SPECIES_CLAYDOL", "Claydol"),
("SPECIES_NOSEPASS", "Nosepass"),
("SPECIES_TORKOAL", "Torkoal"),
("SPECIES_SABLEYE", "Sableye"),
("SPECIES_BARBOACH", "Barboach"),
("SPECIES_WHISCASH", "Whiscash"),
("SPECIES_LUVDISC", "Luvdisc"),
("SPECIES_CORPHISH", "Corphish"),
("SPECIES_CRAWDAUNT", "Crawdaunt"),
("SPECIES_FEEBAS", "Feebas"),
("SPECIES_MILOTIC", "Milotic"),
("SPECIES_CARVANHA", "Carvanha"),
("SPECIES_SHARPEDO", "Sharpedo"),
("SPECIES_TRAPINCH", "Trapinch"),
("SPECIES_VIBRAVA", "Vibrava"),
("SPECIES_FLYGON", "Flygon"),
("SPECIES_MAKUHITA", "Makuhita"),
("SPECIES_HARIYAMA", "Hariyama"),
("SPECIES_ELECTRIKE", "Electrike"),
("SPECIES_MANECTRIC", "Manectric"),
("SPECIES_NUMEL", "Numel"),
("SPECIES_CAMERUPT", "Camerupt"),
("SPECIES_SPHEAL", "Spheal"),
("SPECIES_SEALEO", "Sealeo"),
("SPECIES_WALREIN", "Walrein"),
("SPECIES_CACNEA", "Cacnea"),
("SPECIES_CACTURNE", "Cacturne"),
("SPECIES_SNORUNT", "Snorunt"),
("SPECIES_GLALIE", "Glalie"),
("SPECIES_LUNATONE", "Lunatone"),
("SPECIES_SOLROCK", "Solrock"),
("SPECIES_AZURILL", "Azurill"),
("SPECIES_SPOINK", "Spoink"),
("SPECIES_GRUMPIG", "Grumpig"),
("SPECIES_PLUSLE", "Plusle"),
("SPECIES_MINUN", "Minun"),
("SPECIES_MAWILE", "Mawile"),
("SPECIES_MEDITITE", "Meditite"),
("SPECIES_MEDICHAM", "Medicham"),
("SPECIES_SWABLU", "Swablu"),
("SPECIES_ALTARIA", "Altaria"),
("SPECIES_WYNAUT", "Wynaut"),
("SPECIES_DUSKULL", "Duskull"),
("SPECIES_DUSCLOPS", "Dusclops"),
("SPECIES_ROSELIA", "Roselia"),
("SPECIES_SLAKOTH", "Slakoth"),
("SPECIES_VIGOROTH", "Vigoroth"),
("SPECIES_SLAKING", "Slaking"),
("SPECIES_GULPIN", "Gulpin"),
("SPECIES_SWALOT", "Swalot"),
("SPECIES_TROPIUS", "Tropius"),
("SPECIES_WHISMUR", "Whismur"),
("SPECIES_LOUDRED", "Loudred"),
("SPECIES_EXPLOUD", "Exploud"),
("SPECIES_CLAMPERL", "Clamperl"),
("SPECIES_HUNTAIL", "Huntail"),
("SPECIES_GOREBYSS", "Gorebyss"),
("SPECIES_ABSOL", "Absol"),
("SPECIES_SHUPPET", "Shuppet"),
("SPECIES_BANETTE", "Banette"),
("SPECIES_SEVIPER", "Seviper"),
("SPECIES_ZANGOOSE", "Zangoose"),
("SPECIES_RELICANTH", "Relicanth"),
("SPECIES_ARON", "Aron"),
("SPECIES_LAIRON", "Lairon"),
("SPECIES_AGGRON", "Aggron"),
("SPECIES_CASTFORM", "Castform"),
("SPECIES_VOLBEAT", "Volbeat"),
("SPECIES_ILLUMISE", "Illumise"),
("SPECIES_LILEEP", "Lileep"),
("SPECIES_CRADILY", "Cradily"),
("SPECIES_ANORITH", "Anorith"),
("SPECIES_ARMALDO", "Armaldo"),
("SPECIES_RALTS", "Ralts"),
("SPECIES_KIRLIA", "Kirlia"),
("SPECIES_GARDEVOIR", "Gardevoir"),
("SPECIES_BAGON", "Bagon"),
("SPECIES_SHELGON", "Shelgon"),
("SPECIES_SALAMENCE", "Salamence"),
("SPECIES_BELDUM", "Beldum"),
("SPECIES_METANG", "Metang"),
("SPECIES_METAGROSS", "Metagross"),
("SPECIES_REGIROCK", "Regirock"),
("SPECIES_REGICE", "Regice"),
("SPECIES_REGISTEEL", "Registeel"),
("SPECIES_KYOGRE", "Kyogre"),
("SPECIES_GROUDON", "Groudon"),
("SPECIES_RAYQUAZA", "Rayquaza"),
("SPECIES_LATIAS", "Latias"),
("SPECIES_LATIOS", "Latios"),
("SPECIES_JIRACHI", "Jirachi"),
("SPECIES_DEOXYS", "Deoxys"),
("SPECIES_CHIMECHO", "Chimecho")
]
species_list: List[SpeciesData] = []
max_species_id = 0
for species_name, species_label in all_species:
species_id = data.constants[species_name]
max_species_id = max(species_id, max_species_id)
species_data = extracted_data["species"][species_id]
learnset = [LearnsetMove(item["level"], item["move_id"]) for item in species_data["learnset"]["moves"]]
species_list.append(SpeciesData(
species_name,
species_label,
species_id,
BaseStats(
species_data["base_stats"][0],
species_data["base_stats"][1],
species_data["base_stats"][2],
species_data["base_stats"][3],
species_data["base_stats"][4],
species_data["base_stats"][5]
),
(species_data["types"][0], species_data["types"][1]),
(species_data["abilities"][0], species_data["abilities"][1]),
[EvolutionData(
_str_to_evolution_method(evolution_json["method"]),
evolution_json["param"],
evolution_json["species"],
) for evolution_json in species_data["evolutions"]],
None,
species_data["catch_rate"],
learnset,
int(species_data["tmhm_learnset"], 16),
species_data["learnset"]["rom_address"],
species_data["rom_address"]
))
data.species = [None for i in range(max_species_id + 1)]
for species_data in species_list:
data.species[species_data.species_id] = species_data
for species in data.species:
if species is not None:
for evolution in species.evolutions:
data.species[evolution.species_id].pre_evolution = species.species_id
# Create static encounter data
for static_encounter_json in extracted_data["static_encounters"]:
data.static_encounters.append(StaticEncounterData(
static_encounter_json["species"],
static_encounter_json["rom_address"]
))
# TM moves
data.tmhm_moves = extracted_data["tmhm_moves"]
# Create ability data
data.abilities = [AbilityData(data.constants[ability_data[0]], ability_data[1]) for ability_data in [
("ABILITY_STENCH", "Stench"),
("ABILITY_DRIZZLE", "Drizzle"),
("ABILITY_SPEED_BOOST", "Speed Boost"),
("ABILITY_BATTLE_ARMOR", "Battle Armor"),
("ABILITY_STURDY", "Sturdy"),
("ABILITY_DAMP", "Damp"),
("ABILITY_LIMBER", "Limber"),
("ABILITY_SAND_VEIL", "Sand Veil"),
("ABILITY_STATIC", "Static"),
("ABILITY_VOLT_ABSORB", "Volt Absorb"),
("ABILITY_WATER_ABSORB", "Water Absorb"),
("ABILITY_OBLIVIOUS", "Oblivious"),
("ABILITY_CLOUD_NINE", "Cloud Nine"),
("ABILITY_COMPOUND_EYES", "Compound Eyes"),
("ABILITY_INSOMNIA", "Insomnia"),
("ABILITY_COLOR_CHANGE", "Color Change"),
("ABILITY_IMMUNITY", "Immunity"),
("ABILITY_FLASH_FIRE", "Flash Fire"),
("ABILITY_SHIELD_DUST", "Shield Dust"),
("ABILITY_OWN_TEMPO", "Own Tempo"),
("ABILITY_SUCTION_CUPS", "Suction Cups"),
("ABILITY_INTIMIDATE", "Intimidate"),
("ABILITY_SHADOW_TAG", "Shadow Tag"),
("ABILITY_ROUGH_SKIN", "Rough Skin"),
("ABILITY_WONDER_GUARD", "Wonder Guard"),
("ABILITY_LEVITATE", "Levitate"),
("ABILITY_EFFECT_SPORE", "Effect Spore"),
("ABILITY_SYNCHRONIZE", "Synchronize"),
("ABILITY_CLEAR_BODY", "Clear Body"),
("ABILITY_NATURAL_CURE", "Natural Cure"),
("ABILITY_LIGHTNING_ROD", "Lightning Rod"),
("ABILITY_SERENE_GRACE", "Serene Grace"),
("ABILITY_SWIFT_SWIM", "Swift Swim"),
("ABILITY_CHLOROPHYLL", "Chlorophyll"),
("ABILITY_ILLUMINATE", "Illuminate"),
("ABILITY_TRACE", "Trace"),
("ABILITY_HUGE_POWER", "Huge Power"),
("ABILITY_POISON_POINT", "Poison Point"),
("ABILITY_INNER_FOCUS", "Inner Focus"),
("ABILITY_MAGMA_ARMOR", "Magma Armor"),
("ABILITY_WATER_VEIL", "Water Veil"),
("ABILITY_MAGNET_PULL", "Magnet Pull"),
("ABILITY_SOUNDPROOF", "Soundproof"),
("ABILITY_RAIN_DISH", "Rain Dish"),
("ABILITY_SAND_STREAM", "Sand Stream"),
("ABILITY_PRESSURE", "Pressure"),
("ABILITY_THICK_FAT", "Thick Fat"),
("ABILITY_EARLY_BIRD", "Early Bird"),
("ABILITY_FLAME_BODY", "Flame Body"),
("ABILITY_RUN_AWAY", "Run Away"),
("ABILITY_KEEN_EYE", "Keen Eye"),
("ABILITY_HYPER_CUTTER", "Hyper Cutter"),
("ABILITY_PICKUP", "Pickup"),
("ABILITY_TRUANT", "Truant"),
("ABILITY_HUSTLE", "Hustle"),
("ABILITY_CUTE_CHARM", "Cute Charm"),
("ABILITY_PLUS", "Plus"),
("ABILITY_MINUS", "Minus"),
("ABILITY_FORECAST", "Forecast"),
("ABILITY_STICKY_HOLD", "Sticky Hold"),
("ABILITY_SHED_SKIN", "Shed Skin"),
("ABILITY_GUTS", "Guts"),
("ABILITY_MARVEL_SCALE", "Marvel Scale"),
("ABILITY_LIQUID_OOZE", "Liquid Ooze"),
("ABILITY_OVERGROW", "Overgrow"),
("ABILITY_BLAZE", "Blaze"),
("ABILITY_TORRENT", "Torrent"),
("ABILITY_SWARM", "Swarm"),
("ABILITY_ROCK_HEAD", "Rock Head"),
("ABILITY_DROUGHT", "Drought"),
("ABILITY_ARENA_TRAP", "Arena Trap"),
("ABILITY_VITAL_SPIRIT", "Vital Spirit"),
("ABILITY_WHITE_SMOKE", "White Smoke"),
("ABILITY_PURE_POWER", "Pure Power"),
("ABILITY_SHELL_ARMOR", "Shell Armor"),
("ABILITY_CACOPHONY", "Cacophony"),
("ABILITY_AIR_LOCK", "Air Lock")
]]
# Create map data
for map_name, map_json in extracted_data["maps"].items():
land_encounters = None
water_encounters = None
fishing_encounters = None
if map_json["land_encounters"] is not None:
land_encounters = EncounterTableData(
map_json["land_encounters"]["encounter_slots"],
map_json["land_encounters"]["rom_address"]
)
if map_json["water_encounters"] is not None:
water_encounters = EncounterTableData(
map_json["water_encounters"]["encounter_slots"],
map_json["water_encounters"]["rom_address"]
)
if map_json["fishing_encounters"] is not None:
fishing_encounters = EncounterTableData(
map_json["fishing_encounters"]["encounter_slots"],
map_json["fishing_encounters"]["rom_address"]
)
data.maps.append(MapData(
map_name,
land_encounters,
water_encounters,
fishing_encounters
))
data.maps.sort(key=lambda map: map.name)
# Create warp map
for warp, destination in extracted_data["warps"].items():
data.warp_map[warp] = None if destination == "" else destination
if encoded_warp not in data.warp_map:
data.warp_map[encoded_warp] = None
# Create trainer data
for i, trainer_json in enumerate(extracted_data["trainers"]):
party_json = trainer_json["party"]
pokemon_data_type = _str_to_pokemon_data_type(trainer_json["pokemon_data_type"])
data.trainers.append(TrainerData(
i,
TrainerPartyData(
[TrainerPokemonData(
p["species"],
p["level"],
(p["moves"][0], p["moves"][1], p["moves"][2], p["moves"][3])
) for p in party_json],
pokemon_data_type,
trainer_json["party_rom_address"]
),
trainer_json["rom_address"],
trainer_json["battle_script_rom_address"]
))
_init()

View File

@ -0,0 +1,99 @@
## `regions/`
These define regions, connections, and where locations are. If you know what you're doing, it should be pretty clear how
this works by taking a quick look through the files. The rest of this section is pretty verbose to cover everything. Not
to say you shouldn't read it, but the tl;dr is:
- Every map, even trivial ones, gets a region definition, and they cannot be coalesced (because of warp rando)
- Stick to the naming convention for regions and events (look at Route 103 and Petalburg City for guidance)
- Locations and warps can only be claimed by one region
- Events are declared here
A `Map`, which you will see referenced in `parent_map` attribute in the region JSON, is an id from the source code.
`Map`s are sets of tiles, encounters, warps, events, and so on. Route 103, Littleroot Town, the Oldale Town Mart, the
second floor of Devon Corp, and each level of Victory Road are all examples of `Map`s. You transition between `Map`s by
stepping on a warp (warp pads, doorways, etc...) or walking over a border between `Map`s in the overworld. Some warps
don't go to a different `Map`.
Regions usually describe physical areas which are subsets of a `Map`. Every `Map` must have one or more defined regions.
A region should not contain area from more than one `Map`. We'll need to draw those lines now even when there is no
logical boundary (like between two the first and second floors of your rival's house), for warp rando.
Most `Map`s have been split into multiple regions. In the example below, `MAP_ROUTE103` was split into
`REGION_ROUTE_103/WEST`, `REGION_ROUTE_103/WATER`, and `REGION_ROUTE_103/EAST` (this document may be out of date; the
example is demonstrative). Keeping the name consistent with the `Map` name and adding a label suffix for the subarea
makes it clearer where we are in the world and where within a `Map` we're describing.
Every region (except `Menu`) is configured here. All files in this directory are combined with each other at runtime,
and are only split and ordered for organization. Regions defined in `data/regions/unused` are entirely unused because
they're not yet reachable in the randomizer. They're there for future reference in case we want to pull those maps in
later. Any locations or warps in here should be ignored. Data for a single region looks like this:
```json
"REGION_ROUTE103/EAST": {
"parent_map": "MAP_ROUTE103",
"locations": [
"ITEM_ROUTE_103_GUARD_SPEC",
"ITEM_ROUTE_103_PP_UP"
],
"events": [],
"exits": [
"REGION_ROUTE103/WATER",
"REGION_ROUTE110/MAIN"
],
"warps": [
"MAP_ROUTE103:0/MAP_ALTERING_CAVE:0"
]
}
```
- `[key]`: The name of the object, in this case `REGION_ROUTE103/EAST`, should be the value of `parent_map` where the
`MAP` prefix is replaced with `REGION`. Then there should be a following `/` and a label describing this specific region
within the `Map`. This is not enforced or required by the code, but it makes things much more clear.
- `parent_map`: The name of the `Map` this region exists under. It can relate this region to information like encounter
tables.
- `locations`: Locations contained within this region. This can be anything from an item on the ground to a badge to a
gift from an NPC. Locations themselves are defined in `data/extracted_data.json`, and the names used here should come
directly from it.
- `events`: Events that can be completed in this region. Defeating a gym leader or Aqua/Magma team leader, for example,
can trigger story progression and unblock roads and buildings. Events are defined here and nowhere else, and access
rules are set in `rules.py`.
- `exits`: Names of regions that can be directly accessed from this one. Most often regions within the same `Map`,
neighboring maps in the overworld, or transitions from using HM08 Dive. Most connections between maps/regions come from
warps. Any region in this list should be defined somewhere in `data/regions`.
- `warps`: Warp events contained within this region. Warps are defined in `data/extracted_data.json`, and must exist
there to be referenced here. More on warps in [../README.md](../README.md).
Think of this data as defining which regions are "claiming" a given location, event, or warp. No more than one region
may claim ownership of a location. Even if some "thing" may happen in two different regions and set the same flag, they
should be defined as two different events and anything conditional on said "thing" happening can check whether either of
the two events is accessible. (e.g. Interacting with the Poke Ball in your rival's room and going back downstairs will
both trigger a conversation with them which enables you to rescue Professor Birch. It's the same "thing" on two
different `Map`s.)
Conceptually, you shouldn't have to "add" any new regions. You should only have to "split" existing regions. When you
split a region, make sure to correctly reassign `locations`, `events`, `exits`, and `warps` according to which new
region they now exist in. Make sure to define new `exits` to link the new regions to each other if applicable. And
especially remember to rename incoming `exits` defined in other regions which are still pointing to the pre-split
region. `sanity_check.py` should catch you if there are other regions that point to a region that no longer exists, but
if one of your newly-split regions still has the same name as the original, it won't be detected and you may find that
things aren't connected correctly.
## `extracted_data.json`
DO NOT TOUCH
Contains data automatically pulled from the base rom and its source code when it is built. There should be no reason to
manually modify it. Data from this file is piped through `data.py` to create a data object that's more useful and
complete.
## `items.json`
A map from items as defined in the `constants` in `extracted_data.json` to useful info like a human-friendly label, the
type of progression it enables, and tags to associate. There are many unused items and extra helper constants in
`extracted_data.json`, so this file contains an exhaustive list of items which can actually be found in the modded game.
## `locations.json`
Similar to `items.json`, this associates locations with human-friendly labels and tags that are used for filtering. Any
locations claimed by any region need an entry here.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,396 @@
{
"REGION_BATTLE_FRONTIER_RECEPTION_GATE/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_RECEPTION_GATE",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_RECEPTION_GATE:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8",
"MAP_BATTLE_FRONTIER_RECEPTION_GATE:1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9"
]
},
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/DOCK": {
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
"locations": [],
"events": [],
"exits": [
"REGION_SLATEPORT_CITY_HARBOR/MAIN",
"REGION_LILYCOVE_CITY_HARBOR/MAIN"
],
"warps": [
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8/MAP_BATTLE_FRONTIER_RECEPTION_GATE:0"
]
},
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
"locations": [],
"events": [],
"exits": [
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"
],
"warps": [
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7/MAP_BATTLE_FRONTIER_LOUNGE7:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2/MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9/MAP_BATTLE_FRONTIER_RECEPTION_GATE:1",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0/MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5/MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3/MAP_BATTLE_FRONTIER_LOUNGE2:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4/MAP_BATTLE_FRONTIER_MART:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6/MAP_BATTLE_FRONTIER_LOUNGE4:0"
]
},
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER": {
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
"locations": [],
"events": [],
"exits": [
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER",
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE"
],
"warps": []
},
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/CAVE_ENTRANCE": {
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
"locations": [],
"events": [],
"exits": [
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"
],
"warps": [
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10/MAP_ARTISAN_CAVE_B1F:0"
]
},
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
"locations": [],
"events": [],
"exits": [
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/MAIN",
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL"
],
"warps": [
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5/MAP_BATTLE_FRONTIER_LOUNGE1:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8/MAP_BATTLE_FRONTIER_LOUNGE6:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6/MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10/MAP_BATTLE_FRONTIER_LOUNGE8:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11/MAP_BATTLE_FRONTIER_LOUNGE9:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7/MAP_BATTLE_FRONTIER_LOUNGE5:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4/MAP_BATTLE_FRONTIER_RANKING_HALL:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1/MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3/MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9/MAP_BATTLE_FRONTIER_LOUNGE3:0"
]
},
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/CAVE_ENTRANCE": {
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
"locations": [],
"events": [],
"exits": [
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN"
],
"warps": [
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13/MAP_ARTISAN_CAVE_1F:0"
]
},
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL": {
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
"locations": [],
"events": [],
"exits": [
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/MAIN",
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER"
],
"warps": []
},
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/WATER": {
"parent_map": "MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
"locations": [],
"events": [],
"exits": [
"REGION_BATTLE_FRONTIER_OUTSIDE_EAST/ABOVE_WATERFALL",
"REGION_BATTLE_FRONTIER_OUTSIDE_WEST/WATER"
],
"warps": []
},
"REGION_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_DOME_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:3/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0!"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0",
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2"
]
},
"REGION_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6"
]
},
"REGION_BATTLE_FRONTIER_RANKING_HALL/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_RANKING_HALL",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_RANKING_HALL:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4"
]
},
"REGION_BATTLE_FRONTIER_POKEMON_CENTER_1F/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2/MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0",
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12"
]
},
"REGION_BATTLE_FRONTIER_POKEMON_CENTER_2F/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2"
]
},
"REGION_BATTLE_FRONTIER_MART/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_MART",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_MART:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4"
]
},
"REGION_BATTLE_FRONTIER_SCOTTS_HOUSE/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_SCOTTS_HOUSE",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE1/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE1",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE1:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE2/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE2",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE2:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE3/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE3",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE3:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE4/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE4",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE4:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE5/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE5",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE5:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE6/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE6",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE6:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE7/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE7",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE7:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE8/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE8",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE8:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10"
]
},
"REGION_BATTLE_FRONTIER_LOUNGE9/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_LOUNGE9",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_LOUNGE9:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11"
]
},
"REGION_ARTISAN_CAVE_1F/MAIN": {
"parent_map": "MAP_ARTISAN_CAVE_1F",
"locations": [
"ITEM_ARTISAN_CAVE_1F_CARBOS"
],
"events": [],
"exits": [],
"warps": [
"MAP_ARTISAN_CAVE_1F:1/MAP_ARTISAN_CAVE_B1F:1",
"MAP_ARTISAN_CAVE_1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13"
]
},
"REGION_ARTISAN_CAVE_B1F/MAIN": {
"parent_map": "MAP_ARTISAN_CAVE_B1F",
"locations": [
"ITEM_ARTISAN_CAVE_B1F_HP_UP",
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC",
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM",
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN",
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON"
],
"events": [],
"exits": [],
"warps": [
"MAP_ARTISAN_CAVE_B1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10",
"MAP_ARTISAN_CAVE_B1F:1/MAP_ARTISAN_CAVE_1F:1"
]
}
}

View File

@ -0,0 +1,52 @@
{
"REGION_TERRA_CAVE_ENTRANCE/MAIN": {
"parent_map": "MAP_TERRA_CAVE_ENTRANCE",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!",
"MAP_TERRA_CAVE_ENTRANCE:1/MAP_TERRA_CAVE_END:0"
]
},
"REGION_TERRA_CAVE_END/MAIN": {
"parent_map": "MAP_TERRA_CAVE_END",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_TERRA_CAVE_END:0/MAP_TERRA_CAVE_ENTRANCE:1"
]
},
"REGION_UNDERWATER_MARINE_CAVE/MAIN": {
"parent_map": "MAP_UNDERWATER_MARINE_CAVE",
"locations": [],
"events": [],
"exits": [
"REGION_MARINE_CAVE_ENTRANCE/MAIN"
],
"warps": [
"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!"
]
},
"REGION_MARINE_CAVE_ENTRANCE/MAIN": {
"parent_map": "MAP_MARINE_CAVE_ENTRANCE",
"locations": [],
"events": [],
"exits": [
"REGION_UNDERWATER_MARINE_CAVE/MAIN"
],
"warps": [
"MAP_MARINE_CAVE_ENTRANCE:0/MAP_MARINE_CAVE_END:0"
]
},
"REGION_MARINE_CAVE_END/MAIN": {
"parent_map": "MAP_MARINE_CAVE_END",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_MARINE_CAVE_END:0/MAP_MARINE_CAVE_ENTRANCE:0"
]
}
}

View File

@ -0,0 +1,276 @@
{
"REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN": {
"parent_map": "MAP_SOUTHERN_ISLAND_EXTERIOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1"
]
},
"REGION_SOUTHERN_ISLAND_INTERIOR/MAIN": {
"parent_map": "MAP_SOUTHERN_ISLAND_INTERIOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1"
]
},
"REGION_FARAWAY_ISLAND_ENTRANCE/MAIN": {
"parent_map": "MAP_FARAWAY_ISLAND_ENTRANCE",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1"
]
},
"REGION_FARAWAY_ISLAND_INTERIOR/MAIN": {
"parent_map": "MAP_FARAWAY_ISLAND_INTERIOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1"
]
},
"REGION_BIRTH_ISLAND_HARBOR/MAIN": {
"parent_map": "MAP_BIRTH_ISLAND_HARBOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0"
]
},
"REGION_BIRTH_ISLAND_EXTERIOR/MAIN": {
"parent_map": "MAP_BIRTH_ISLAND_EXTERIOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0"
]
},
"REGION_NAVEL_ROCK_HARBOR/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_HARBOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0"
]
},
"REGION_NAVEL_ROCK_EXTERIOR/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_EXTERIOR",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_EXTERIOR:0/MAP_NAVEL_ROCK_HARBOR:0",
"MAP_NAVEL_ROCK_EXTERIOR:1/MAP_NAVEL_ROCK_ENTRANCE:1"
]
},
"REGION_NAVEL_ROCK_ENTRANCE/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_ENTRANCE",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_ENTRANCE:0/MAP_NAVEL_ROCK_B1F:0",
"MAP_NAVEL_ROCK_ENTRANCE:1/MAP_NAVEL_ROCK_EXTERIOR:1"
]
},
"REGION_NAVEL_ROCK_B1F/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_B1F",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_B1F:0/MAP_NAVEL_ROCK_ENTRANCE:0",
"MAP_NAVEL_ROCK_B1F:1/MAP_NAVEL_ROCK_FORK:1"
]
},
"REGION_NAVEL_ROCK_FORK/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_FORK",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_FORK:0/MAP_NAVEL_ROCK_UP1:0",
"MAP_NAVEL_ROCK_FORK:1/MAP_NAVEL_ROCK_B1F:1",
"MAP_NAVEL_ROCK_FORK:2/MAP_NAVEL_ROCK_DOWN01:0"
]
},
"REGION_NAVEL_ROCK_DOWN01/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN01",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN01:0/MAP_NAVEL_ROCK_FORK:2",
"MAP_NAVEL_ROCK_DOWN01:1/MAP_NAVEL_ROCK_DOWN02:0"
]
},
"REGION_NAVEL_ROCK_DOWN02/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN02",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN02:1/MAP_NAVEL_ROCK_DOWN03:0",
"MAP_NAVEL_ROCK_DOWN02:0/MAP_NAVEL_ROCK_DOWN01:1"
]
},
"REGION_NAVEL_ROCK_DOWN03/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN03",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN03:0/MAP_NAVEL_ROCK_DOWN02:1",
"MAP_NAVEL_ROCK_DOWN03:1/MAP_NAVEL_ROCK_DOWN04:0"
]
},
"REGION_NAVEL_ROCK_DOWN04/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN04",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN04:1/MAP_NAVEL_ROCK_DOWN05:0",
"MAP_NAVEL_ROCK_DOWN04:0/MAP_NAVEL_ROCK_DOWN03:1"
]
},
"REGION_NAVEL_ROCK_DOWN05/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN05",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN05:0/MAP_NAVEL_ROCK_DOWN04:1",
"MAP_NAVEL_ROCK_DOWN05:1/MAP_NAVEL_ROCK_DOWN06:0"
]
},
"REGION_NAVEL_ROCK_DOWN06/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN06",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN06:1/MAP_NAVEL_ROCK_DOWN07:0",
"MAP_NAVEL_ROCK_DOWN06:0/MAP_NAVEL_ROCK_DOWN05:1"
]
},
"REGION_NAVEL_ROCK_DOWN07/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN07",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN07:0/MAP_NAVEL_ROCK_DOWN06:1",
"MAP_NAVEL_ROCK_DOWN07:1/MAP_NAVEL_ROCK_DOWN08:0"
]
},
"REGION_NAVEL_ROCK_DOWN08/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN08",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN08:1/MAP_NAVEL_ROCK_DOWN09:0",
"MAP_NAVEL_ROCK_DOWN08:0/MAP_NAVEL_ROCK_DOWN07:1"
]
},
"REGION_NAVEL_ROCK_DOWN09/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN09",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN09:0/MAP_NAVEL_ROCK_DOWN08:1",
"MAP_NAVEL_ROCK_DOWN09:1/MAP_NAVEL_ROCK_DOWN10:0"
]
},
"REGION_NAVEL_ROCK_DOWN10/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN10",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN10:1/MAP_NAVEL_ROCK_DOWN11:1",
"MAP_NAVEL_ROCK_DOWN10:0/MAP_NAVEL_ROCK_DOWN09:1"
]
},
"REGION_NAVEL_ROCK_DOWN11/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN11",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_DOWN11:1/MAP_NAVEL_ROCK_DOWN10:1",
"MAP_NAVEL_ROCK_DOWN11:0/MAP_NAVEL_ROCK_BOTTOM:0"
]
},
"REGION_NAVEL_ROCK_BOTTOM/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_BOTTOM",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0"
]
},
"REGION_NAVEL_ROCK_UP1/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_UP1",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_UP1:1/MAP_NAVEL_ROCK_UP2:0",
"MAP_NAVEL_ROCK_UP1:0/MAP_NAVEL_ROCK_FORK:0"
]
},
"REGION_NAVEL_ROCK_UP2/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_UP2",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_UP2:0/MAP_NAVEL_ROCK_UP1:1",
"MAP_NAVEL_ROCK_UP2:1/MAP_NAVEL_ROCK_UP3:0"
]
},
"REGION_NAVEL_ROCK_UP3/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_UP3",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_UP3:1/MAP_NAVEL_ROCK_UP4:0",
"MAP_NAVEL_ROCK_UP3:0/MAP_NAVEL_ROCK_UP2:1"
]
},
"REGION_NAVEL_ROCK_UP4/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_UP4",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_UP4:0/MAP_NAVEL_ROCK_UP3:1",
"MAP_NAVEL_ROCK_UP4:1/MAP_NAVEL_ROCK_TOP:0"
]
},
"REGION_NAVEL_ROCK_TOP/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_TOP",
"locations": [
"HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH"
],
"events": [],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1"
]
}
}

View File

@ -0,0 +1,82 @@
{
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE2/MAIN": {
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE2",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
]
},
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE3/MAIN": {
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE3",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
]
},
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE4/MAIN": {
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE4",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
]
},
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE5/MAIN": {
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE5",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
]
},
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE6/MAIN": {
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE6",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
]
},
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE7/MAIN": {
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE7",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6"
]
},
"REGION_ROUTE110_TRICK_HOUSE_PUZZLE8/MAIN": {
"parent_map": "MAP_ROUTE110_TRICK_HOUSE_PUZZLE8",
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:2/MAP_ROUTE110_TRICK_HOUSE_END:0!"
]
}
}

View File

@ -0,0 +1,78 @@
# Pokémon Emerald
## Where is the settings page?
You can read through all the settings and generate a YAML [here](../player-settings).
## What does randomization do to this game?
This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on
the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon,
abilities, types, etc… You can even change a percentage of single battles into double battles. Check the
[settings page](../player-settings) for a more comprehensive list of what can be changed.
## What items and locations get randomized?
The most interesting items that can be added to the item pool are badges and HMs, which most affect what locations you
can access. Key items like the Devon Scope or Mach Bike can also be randomized, as well as the many Potions, Revives,
TMs, and other items that you can find on the ground or receive as gifts.
## What other changes are made to the game?
There are many quality of life improvements meant to speed up the game a little and improve the experience of playing a
randomizer. Here are some of the more important ones:
- Shoal Cave switches between high tide and low tide every time you re-enter
- Bag space is greatly expanded (you're all but guaranteed to never need to store items in the PC)
- Trade evolutions have been changed to level or item evolutions
- You can have both bikes simultaneously
- You can run or bike (almost) anywhere
- The Wally catching tutorial is skipped
- All text is instant, and with a setting it can be automatically progressed by holding A
- When a Repel runs out, you will be prompted to use another
- Many more minor improvements…
## Where is my starting inventory?
Except for badges, your starting inventory will be in the PC.
## What does another world's item look like in Pokémon Emerald?
When you find an item that is not your own, you will instead receive an "ARCHIPELAGO ITEM" which will *not* be added to
your inventory.
## When the player receives an item, what happens?
You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages`
setting, the received item will either be silently added to your bag or you will be shown a text box with the item's
name and the item will be added to your bag while a fanfare plays.
## Can I play offline?
Yes, the client and connector are only necessary for sending and receiving items. If you're playing a solo game, you
don't need to play online unless you want the rest of Archipelago's functionality (like hints and auto-tracking). If
you're playing a multiworld game, the client will sync your game with the server the next time you connect.
## Will battle mechanics be updated?
This is something we'd love to see, but it's unlikely. We don't want to force new mechanics on players who would prefer
to play with the classic mechanics, but trying to switch between old and new mechanics based on an option would be a
monumental task, and is probably best solved some other way.
## Is this randomizer compatible with other mods?
No, other mods cannot be applied. It would be impossible to generalize this implementation's changes in a way that is
compatible with any other mod or romhack. Romhacks could be added as their own games, but they would have to be
implemented separately. Check out [Archipelago's Discord server](https://discord.gg/8Z65BR2) if you want to make a
suggestion or contribute.
## Can I use tools like the Universal Pokémon Randomizer?
No, those tools expect data to be in certain locations and in a certain format, but this randomizer has to shift it
around. Using tools to try to modify the game would only corrupt the ROM.
We realize this means breaking from established habits when it comes to randomizing Pokémon games, but this randomizer
would be many times more complex to develop if it were constrained by something like UPR.
The one exception might be PKHeX. You may be able to extract pokémon from your save using PKHeX, but this isn't a
guarantee, and we make no effort to keep our saves compatible with PKHeX.

View File

@ -0,0 +1,72 @@
# Pokémon Emerald Setup Guide
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- An English Pokémon Emerald ROM. The Archipelago community cannot provide this.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
### Configuring BizHawk
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
tabbed out of EmuHawk.
- Open a `.gba` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
`Controllers…`, load any `.gba` ROM first.
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
clear it.
## Optional Software
- [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest), for use with
[PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Generating and Patching a Game
1. Create your settings file (YAML). You can make one on the
[Pokémon Emerald settings page](../../../games/Pokemon%20Emerald/player-settings).
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
This will generate an output file for you. Your patch file will have the `.apemerald` file extension.
3. Open `ArchipelagoLauncher.exe`
4. Select "Open Patch" on the left side and select your patch file.
5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
6. A patched `.gba` file will be created in the same place as the patch file.
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
BizHawk install.
If you're playing a single-player seed and you don't care about autotracking or hints, you can stop here, close the
client, and load the patched ROM in any emulator. However, for multiworlds and other Archipelago features, continue
below using BizHawk as your emulator.
## Connecting to a Server
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
in case you have to close and reopen a window mid-game for some reason.
1. Pokemon Emerald uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
you can re-open it from the launcher.
2. Ensure EmuHawk is running the patched ROM.
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
4. In the Lua Console window, go to `Script > Open Script…`.
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk
Client window should indicate that it connected and recognized Pokemon Emerald.
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
top text field of the client and click Connect.
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
perfectly safe to make progress offline; everything will re-sync when you reconnect.
## Auto-Tracking
Pokémon Emerald has a fully functional map tracker that supports auto-tracking.
1. Download [Pokémon Emerald AP Tracker](https://github.com/AliceMousie/emerald-ap-tracker/releases/latest) and
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Put the tracker pack into packs/ in your PopTracker install.
3. Open PopTracker, and load the Pokémon Emerald pack.
4. For autotracking, click on the "AP" symbol at the top.
5. Enter the Archipelago server address (the one you connected your client to), slot name, and password.

View File

@ -0,0 +1,77 @@
"""
Classes and functions related to AP items for Pokemon Emerald
"""
from typing import Dict, FrozenSet, Optional
from BaseClasses import Item, ItemClassification
from .data import BASE_OFFSET, data
class PokemonEmeraldItem(Item):
game: str = "Pokemon Emerald"
tags: FrozenSet[str]
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int) -> None:
super().__init__(name, classification, code, player)
if code is None:
self.tags = frozenset(["Event"])
else:
self.tags = data.items[reverse_offset_item_value(code)].tags
def offset_item_value(item_value: int) -> int:
"""
Returns the AP item id (code) for a given item value
"""
return item_value + BASE_OFFSET
def reverse_offset_item_value(item_id: int) -> int:
"""
Returns the item value for a given AP item id (code)
"""
return item_id - BASE_OFFSET
def create_item_label_to_code_map() -> Dict[str, int]:
"""
Creates a map from item labels to their AP item id (code)
"""
label_to_code_map: Dict[str, int] = {}
for item_value, attributes in data.items.items():
label_to_code_map[attributes.label] = offset_item_value(item_value)
return label_to_code_map
ITEM_GROUPS = {
"Badges": {
"Stone Badge", "Knuckle Badge",
"Dynamo Badge", "Heat Badge",
"Balance Badge", "Feather Badge",
"Mind Badge", "Rain Badge"
},
"HMs": {
"HM01 Cut", "HM02 Fly",
"HM03 Surf", "HM04 Strength",
"HM05 Flash", "HM06 Rock Smash",
"HM07 Waterfall", "HM08 Dive"
},
"HM01": {"HM01 Cut"},
"HM02": {"HM02 Fly"},
"HM03": {"HM03 Surf"},
"HM04": {"HM04 Strength"},
"HM05": {"HM05 Flash"},
"HM06": {"HM06 Rock Smash"},
"HM07": {"HM07 Waterfall"},
"HM08": {"HM08 Dive"}
}
def get_item_classification(item_code: int) -> ItemClassification:
"""
Returns the item classification for a given AP item id (code)
"""
return data.items[reverse_offset_item_value(item_code)].classification

View File

@ -0,0 +1,122 @@
"""
Classes and functions related to AP locations for Pokemon Emerald
"""
from typing import TYPE_CHECKING, Dict, List, Optional, FrozenSet, Iterable
from BaseClasses import Location, Region
from .data import BASE_OFFSET, data
from .items import offset_item_value
if TYPE_CHECKING:
from . import PokemonEmeraldWorld
class PokemonEmeraldLocation(Location):
game: str = "Pokemon Emerald"
rom_address: Optional[int]
default_item_code: Optional[int]
tags: FrozenSet[str]
def __init__(
self,
player: int,
name: str,
flag: Optional[int],
parent: Optional[Region] = None,
rom_address: Optional[int] = None,
default_item_value: Optional[int] = None,
tags: FrozenSet[str] = frozenset()) -> None:
super().__init__(player, name, None if flag is None else offset_flag(flag), parent)
self.default_item_code = None if default_item_value is None else offset_item_value(default_item_value)
self.rom_address = rom_address
self.tags = tags
def offset_flag(flag: int) -> int:
"""
Returns the AP location id (address) for a given flag
"""
if flag is None:
return None
return flag + BASE_OFFSET
def reverse_offset_flag(location_id: int) -> int:
"""
Returns the flag id for a given AP location id (address)
"""
if location_id is None:
return None
return location_id - BASE_OFFSET
def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str, Region], tags: Iterable[str]) -> None:
"""
Iterates through region data and adds locations to the multiworld if
those locations include any of the provided tags.
"""
tags = set(tags)
for region_name, region_data in data.regions.items():
region = regions[region_name]
filtered_locations = [loc for loc in region_data.locations if len(tags & data.locations[loc].tags) > 0]
for location_name in filtered_locations:
location_data = data.locations[location_name]
location = PokemonEmeraldLocation(
world.player,
location_data.label,
location_data.flag,
region,
location_data.rom_address,
location_data.default_item,
location_data.tags
)
region.locations.append(location)
def create_location_label_to_id_map() -> Dict[str, int]:
"""
Creates a map from location labels to their AP location id (address)
"""
label_to_id_map: Dict[str, int] = {}
for region_data in data.regions.values():
for location_name in region_data.locations:
location_data = data.locations[location_name]
label_to_id_map[location_data.label] = offset_flag(location_data.flag)
return label_to_id_map
LOCATION_GROUPS = {
"Badges": {
"Rustboro Gym - Stone Badge",
"Dewford Gym - Knuckle Badge",
"Mauville Gym - Dynamo Badge",
"Lavaridge Gym - Heat Badge",
"Petalburg Gym - Balance Badge",
"Fortree Gym - Feather Badge",
"Mossdeep Gym - Mind Badge",
"Sootopolis Gym - Rain Badge",
},
"Gym TMs": {
"Rustboro Gym - TM39 from Roxanne",
"Dewford Gym - TM08 from Brawly",
"Mauville Gym - TM34 from Wattson",
"Lavaridge Gym - TM50 from Flannery",
"Petalburg Gym - TM42 from Norman",
"Fortree Gym - TM40 from Winona",
"Mossdeep Gym - TM04 from Tate and Liza",
"Sootopolis Gym - TM03 from Juan",
},
"Postgame Locations": {
"Littleroot Town - S.S. Ticket from Norman",
"SS Tidal - Hidden Item in Lower Deck Trash Can",
"SS Tidal - TM49 from Thief",
"Safari Zone NE - Hidden Item North",
"Safari Zone NE - Hidden Item East",
"Safari Zone SE - Hidden Item in South Grass 1",
"Safari Zone SE - Hidden Item in South Grass 2",
}
}

View File

@ -0,0 +1,606 @@
"""
Option definitions for Pokemon Emerald
"""
from dataclasses import dataclass
from typing import Dict, Type
from Options import Choice, DefaultOnToggle, Option, OptionSet, Range, Toggle, FreeText, PerGameCommonOptions
from .data import data
class Goal(Choice):
"""
Determines what your goal is to consider the game beaten
Champion: Become the champion and enter the hall of fame
Steven: Defeat Steven in Meteor Falls
Norman: Defeat Norman in Petalburg Gym
"""
display_name = "Goal"
default = 0
option_champion = 0
option_steven = 1
option_norman = 2
class RandomizeBadges(Choice):
"""
Adds Badges to the pool
Vanilla: Gym leaders give their own badge
Shuffle: Gym leaders give a random badge
Completely Random: Badges can be found anywhere
"""
display_name = "Randomize Badges"
default = 2
option_vanilla = 0
option_shuffle = 1
option_completely_random = 2
class RandomizeHms(Choice):
"""
Adds HMs to the pool
Vanilla: HMs are at their vanilla locations
Shuffle: HMs are shuffled among vanilla HM locations
Completely Random: HMs can be found anywhere
"""
display_name = "Randomize HMs"
default = 2
option_vanilla = 0
option_shuffle = 1
option_completely_random = 2
class RandomizeKeyItems(DefaultOnToggle):
"""
Adds most key items to the pool. These are usually required to unlock
a location or region (e.g. Devon Scope, Letter, Basement Key)
"""
display_name = "Randomize Key Items"
class RandomizeBikes(Toggle):
"""
Adds the mach bike and acro bike to the pool
"""
display_name = "Randomize Bikes"
class RandomizeRods(Toggle):
"""
Adds fishing rods to the pool
"""
display_name = "Randomize Fishing Rods"
class RandomizeOverworldItems(DefaultOnToggle):
"""
Adds items on the ground with a Pokeball sprite to the pool
"""
display_name = "Randomize Overworld Items"
class RandomizeHiddenItems(Toggle):
"""
Adds hidden items to the pool
"""
display_name = "Randomize Hidden Items"
class RandomizeNpcGifts(Toggle):
"""
Adds most gifts received from NPCs to the pool (not including key items or HMs)
"""
display_name = "Randomize NPC Gifts"
class ItemPoolType(Choice):
"""
Determines which non-progression items get put into the item pool
Shuffled: Item pool consists of shuffled vanilla items
Diverse Balanced: Item pool consists of random items approximately proportioned
according to what they're replacing (i.e. more pokeballs, fewer X items, etc...)
Diverse: Item pool consists of uniformly random (non-unique) items
"""
display_name = "Item Pool Type"
default = 0
option_shuffled = 0
option_diverse_balanced = 1
option_diverse = 2
class HiddenItemsRequireItemfinder(DefaultOnToggle):
"""
The Itemfinder is logically required to pick up hidden items
"""
display_name = "Require Itemfinder"
class DarkCavesRequireFlash(DefaultOnToggle):
"""
The lower floors of Granite Cave and Victory Road logically require use of HM05 Flash
"""
display_name = "Require Flash"
class EnableFerry(Toggle):
"""
The ferry between Slateport, Lilycove, and the Battle Frontier can be used if you have the S.S. Ticket
"""
display_name = "Enable Ferry"
class EliteFourRequirement(Choice):
"""
Sets the requirements to challenge the elite four
Badges: Obtain some number of badges
Gyms: Defeat some number of gyms
"""
display_name = "Elite Four Requirement"
default = 0
option_badges = 0
option_gyms = 1
class EliteFourCount(Range):
"""
Sets the number of badges/gyms required to challenge the elite four
"""
display_name = "Elite Four Count"
range_start = 0
range_end = 8
default = 8
class NormanRequirement(Choice):
"""
Sets the requirements to challenge the Petalburg Gym
Badges: Obtain some number of badges
Gyms: Defeat some number of gyms
"""
display_name = "Norman Requirement"
default = 0
option_badges = 0
option_gyms = 1
class NormanCount(Range):
"""
Sets the number of badges/gyms required to challenge the Petalburg Gym
"""
display_name = "Norman Count"
range_start = 0
range_end = 7
default = 4
class RandomizeWildPokemon(Choice):
"""
Randomizes wild pokemon encounters (grass, caves, water, fishing)
Vanilla: Wild encounters are unchanged
Match Base Stats: Wild pokemon are replaced with species with approximately the same bst
Match Type: Wild pokemon are replaced with species that share a type with the original
Match Base Stats and Type: Apply both Match Base Stats and Match Type
Completely Random: There are no restrictions
"""
display_name = "Randomize Wild Pokemon"
default = 0
option_vanilla = 0
option_match_base_stats = 1
option_match_type = 2
option_match_base_stats_and_type = 3
option_completely_random = 4
class AllowWildLegendaries(DefaultOnToggle):
"""
Wild encounters can be replaced by legendaries. Only applied if Randomize Wild Pokemon is not Vanilla.
"""
display_name = "Allow Wild Legendaries"
class RandomizeStarters(Choice):
"""
Randomizes the starter pokemon in Professor Birch's bag
Vanilla: Starters are unchanged
Match Base Stats: Starters are replaced with species with approximately the same bst
Match Type: Starters are replaced with species that share a type with the original
Match Base Stats and Type: Apply both Match Base Stats and Match Type
Completely Random: There are no restrictions
"""
display_name = "Randomize Starters"
default = 0
option_vanilla = 0
option_match_base_stats = 1
option_match_type = 2
option_match_base_stats_and_type = 3
option_completely_random = 4
class AllowStarterLegendaries(DefaultOnToggle):
"""
Starters can be replaced by legendaries. Only applied if Randomize Starters is not Vanilla.
"""
display_name = "Allow Starter Legendaries"
class RandomizeTrainerParties(Choice):
"""
Randomizes the parties of all trainers.
Vanilla: Parties are unchanged
Match Base Stats: Trainer pokemon are replaced with species with approximately the same bst
Match Type: Trainer pokemon are replaced with species that share a type with the original
Match Base Stats and Type: Apply both Match Base Stats and Match Type
Completely Random: There are no restrictions
"""
display_name = "Randomize Trainer Parties"
default = 0
option_vanilla = 0
option_match_base_stats = 1
option_match_type = 2
option_match_base_stats_and_type = 3
option_completely_random = 4
class AllowTrainerLegendaries(DefaultOnToggle):
"""
Enemy trainer pokemon can be replaced by legendaries. Only applied if Randomize Trainer Parties is not Vanilla.
"""
display_name = "Allow Trainer Legendaries"
class RandomizeStaticEncounters(Choice):
"""
Randomizes static encounters (Rayquaza, hidden Kekleons, fake Voltorb pokeballs, etc...)
Vanilla: Static encounters are unchanged
Shuffle: Static encounters are shuffled between each other
Match Base Stats: Static encounters are replaced with species with approximately the same bst
Match Type: Static encounters are replaced with species that share a type with the original
Match Base Stats and Type: Apply both Match Base Stats and Match Type
Completely Random: There are no restrictions
"""
display_name = "Randomize Static Encounters"
default = 0
option_vanilla = 0
option_shuffle = 1
option_match_base_stats = 2
option_match_type = 3
option_match_base_stats_and_type = 4
option_completely_random = 5
class RandomizeTypes(Choice):
"""
Randomizes the type(s) of every pokemon. Each species will have the same number of types.
Vanilla: Types are unchanged
Shuffle: Types are shuffled globally for all species (e.g. every Water-type pokemon becomes Fire-type)
Completely Random: Each species has its type(s) randomized
Follow Evolutions: Types are randomized per evolution line instead of per species
"""
display_name = "Randomize Types"
default = 0
option_vanilla = 0
option_shuffle = 1
option_completely_random = 2
option_follow_evolutions = 3
class RandomizeAbilities(Choice):
"""
Randomizes abilities of every species. Each species will have the same number of abilities.
Vanilla: Abilities are unchanged
Completely Random: Each species has its abilities randomized
Follow Evolutions: Abilities are randomized, but if a pokemon would normally retain its ability
when evolving, the random ability will also be retained
"""
display_name = "Randomize Abilities"
default = 0
option_vanilla = 0
option_completely_random = 1
option_follow_evolutions = 2
class AbilityBlacklist(OptionSet):
"""
A list of abilities which no pokemon should have if abilities are randomized.
For example, you could exclude Wonder Guard and Arena Trap like this:
["Wonder Guard", "Arena Trap"]
"""
display_name = "Ability Blacklist"
valid_keys = frozenset([ability.label for ability in data.abilities])
class LevelUpMoves(Choice):
"""
Randomizes the moves a pokemon learns when they reach a level where they would learn a move.
Your starter is guaranteed to have a usable damaging move.
Vanilla: Learnset is unchanged
Randomized: Moves are randomized
Start with Four Moves: Moves are randomized and all Pokemon know 4 moves at level 1
"""
display_name = "Level Up Moves"
default = 0
option_vanilla = 0
option_randomized = 1
option_start_with_four_moves = 2
class MoveMatchTypeBias(Range):
"""
Sets the probability that a learned move will be forced match one of the types of a pokemon.
If a move is not forced to match type, it will roll for Normal type bias.
"""
display_name = "Move Match Type Bias"
range_start = 0
range_end = 100
default = 0
class MoveNormalTypeBias(Range):
"""
After it has been decided that a move will not be forced to match types, sets the probability that a learned move
will be forced to be the Normal type.
If a move is not forced to be Normal, it will be completely random.
"""
display_name = "Move Normal Type Bias"
range_start = 0
range_end = 100
default = 0
class HmCompatibility(Choice):
"""
Modifies the compatibility of HMs
Vanilla: Compatibility is unchanged
Fully Compatible: Every species can learn any HM
Completely Random: Compatibility is 50/50 for every HM (does not remain consistent across evolution)
"""
display_name = "HM Compatibility"
default = 1
option_vanilla = 0
option_fully_compatible = 1
option_completely_random = 2
class TmCompatibility(Choice):
"""
Modifies the compatibility of TMs
Vanilla: Compatibility is unchanged
Fully Compatible: Every species can learn any TM
Completely Random: Compatibility is 50/50 for every TM (does not remain consistent across evolution)
"""
display_name = "TM Compatibility"
default = 0
option_vanilla = 0
option_fully_compatible = 1
option_completely_random = 2
class TmMoves(Toggle):
"""
Randomizes the moves taught by TMs
"""
display_name = "TM Moves"
class ReusableTms(Toggle):
"""
Sets TMs to not break after use (they remain sellable)
"""
display_name = "Reusable TMs"
class MinCatchRate(Range):
"""
Sets the minimum catch rate a pokemon can have. Any pokemon with a catch rate below this floor will have it raised to this value.
Legendaries are often in the single digits
Fully evolved pokemon are often double digits
Pidgey is 255
"""
display_name = "Minimum Catch Rate"
range_start = 3
range_end = 255
default = 3
class GuaranteedCatch(Toggle):
"""
Every throw is guaranteed to catch a wild pokemon
"""
display_name = "Guaranteed Catch"
class ExpModifier(Range):
"""
Multiplies gained experience by a percentage
100 is default
50 is half
200 is double
etc...
"""
display_name = "Exp Modifier"
range_start = 0
range_end = 1000
default = 100
class BlindTrainers(Toggle):
"""
Causes trainers to not start a battle with you unless you talk to them
"""
display_name = "Blind Trainers"
class DoubleBattleChance(Range):
"""
The percent chance that a trainer with more than 1 pokemon will be converted into a double battle.
If these trainers would normally approach you, they will only do so if you have 2 unfainted pokemon.
They can be battled by talking to them no matter what.
"""
display_name = "Double Battle Chance"
range_start = 0
range_end = 100
default = 0
class BetterShops(Toggle):
"""
Pokemarts sell every item that can be obtained in a pokemart (except mail, which is still unique to the relevant city)
"""
display_name = "Better Shops"
class RemoveRoadblocks(OptionSet):
"""
Removes specific NPCs that normally stand in your way until certain events are completed.
This can open up the world a bit and make your playthrough less linear, but careful how many you remove; it may make too much of your world accessible upon receiving Surf.
Possible values are:
"Route 110 Aqua Grunts"
"Route 112 Magma Grunts"
"Route 119 Aqua Grunts"
"Safari Zone Construction Workers"
"Lilycove City Wailmer"
"Aqua Hideout Grunts"
"Seafloor Cavern Aqua Grunt"
"""
display_name = "Remove Roadblocks"
valid_keys = frozenset([
"Route 110 Aqua Grunts",
"Route 112 Magma Grunts",
"Route 119 Aqua Grunts",
"Safari Zone Construction Workers",
"Lilycove City Wailmer",
"Aqua Hideout Grunts",
"Seafloor Cavern Aqua Grunt"
])
class ExtraBoulders(Toggle):
"""
Places strength boulders on Route 115 which block access to Meteor Falls from the beach.
This aims to take some power away from Surf as a tool for access.
"""
display_name = "Extra Boulders"
class FreeFlyLocation(Toggle):
"""
Enables flying to one random location when Mom gives you the running shoes (excluding cities reachable with no items)
"""
display_name = "Free Fly Location"
class FlyWithoutBadge(DefaultOnToggle):
"""
Fly does not require the Feather Badge to use in the field
"""
display_name = "Fly Without Badge"
class TurboA(Toggle):
"""
Holding A will advance most text automatically
"""
display_name = "Turbo A"
class ReceiveItemMessages(Choice):
"""
Determines whether you receive an in-game notification when receiving an item. Items can still only be received in the overworld.
All: Every item shows a message
Progression: Only progression items show a message
None: All items are added to your bag silently (badges will still show)
"""
display_name = "Receive Item Messages"
default = 0
option_all = 0
option_progression = 1
option_none = 2
class EasterEgg(FreeText):
"""
???
"""
default = "Example Passphrase"
@dataclass
class PokemonEmeraldOptions(PerGameCommonOptions):
goal: Goal
badges: RandomizeBadges
hms: RandomizeHms
key_items: RandomizeKeyItems
bikes: RandomizeBikes
rods: RandomizeRods
overworld_items: RandomizeOverworldItems
hidden_items: RandomizeHiddenItems
npc_gifts: RandomizeNpcGifts
item_pool_type: ItemPoolType
require_itemfinder: HiddenItemsRequireItemfinder
require_flash: DarkCavesRequireFlash
elite_four_requirement: EliteFourRequirement
elite_four_count: EliteFourCount
norman_requirement: NormanRequirement
norman_count: NormanCount
wild_pokemon: RandomizeWildPokemon
allow_wild_legendaries: AllowWildLegendaries
starters: RandomizeStarters
allow_starter_legendaries: AllowStarterLegendaries
trainer_parties: RandomizeTrainerParties
allow_trainer_legendaries: AllowTrainerLegendaries
static_encounters: RandomizeStaticEncounters
types: RandomizeTypes
abilities: RandomizeAbilities
ability_blacklist: AbilityBlacklist
level_up_moves: LevelUpMoves
move_match_type_bias: MoveMatchTypeBias
move_normal_type_bias: MoveNormalTypeBias
tm_compatibility: TmCompatibility
hm_compatibility: HmCompatibility
tm_moves: TmMoves
reusable_tms: ReusableTms
min_catch_rate: MinCatchRate
guaranteed_catch: GuaranteedCatch
exp_modifier: ExpModifier
blind_trainers: BlindTrainers
double_battle_chance: DoubleBattleChance
better_shops: BetterShops
enable_ferry: EnableFerry
remove_roadblocks: RemoveRoadblocks
extra_boulders: ExtraBoulders
free_fly_location: FreeFlyLocation
fly_without_badge: FlyWithoutBadge
turbo_a: TurboA
receive_item_messages: ReceiveItemMessages
easter_egg: EasterEgg

View File

@ -0,0 +1,196 @@
"""
Functions related to pokemon species and moves
"""
import time
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
from .data import SpeciesData, data
if TYPE_CHECKING:
from random import Random
_damaging_moves = frozenset({
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13,
16, 17, 20, 21, 22, 23, 24, 25, 26, 27, 29, 30,
31, 33, 34, 35, 36, 37, 38, 40, 41, 42, 44, 51,
52, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 69, 71, 72, 75, 76, 80, 82, 83, 84, 85,
87, 88, 89, 91, 93, 94, 98, 99, 101, 121, 122, 123,
124, 125, 126, 128, 129, 130, 131, 132, 136, 140, 141, 143,
145, 146, 149, 152, 154, 155, 157, 158, 161, 162, 163, 167,
168, 172, 175, 177, 179, 181, 183, 185, 188, 189, 190, 192,
196, 198, 200, 202, 205, 209, 210, 211, 216, 217, 218, 221,
222, 223, 224, 225, 228, 229, 231, 232, 233, 237, 238, 239,
242, 245, 246, 247, 248, 250, 251, 253, 257, 263, 265, 267,
276, 279, 280, 282, 284, 290, 292, 295, 296, 299, 301, 302,
304, 305, 306, 307, 308, 309, 310, 311, 314, 315, 317, 318,
323, 324, 325, 326, 327, 328, 330, 331, 332, 333, 337, 338,
340, 341, 342, 343, 344, 345, 348, 350, 351, 352, 353, 354
})
_move_types = [
0, 0, 1, 0, 0, 0, 0, 10, 15, 13, 0, 0, 0, 0, 0,
0, 2, 2, 0, 2, 0, 0, 12, 0, 1, 0, 1, 1, 4, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 6, 0, 17,
0, 0, 0, 0, 0, 0, 3, 10, 10, 15, 11, 11, 11, 15, 15,
14, 11, 15, 0, 2, 2, 1, 1, 1, 1, 0, 12, 12, 12, 0,
12, 12, 3, 12, 12, 12, 6, 16, 10, 13, 13, 13, 13, 5, 4,
4, 4, 3, 14, 14, 14, 14, 14, 0, 0, 14, 7, 0, 0, 0,
0, 0, 0, 0, 7, 11, 0, 14, 14, 15, 14, 0, 0, 0, 2,
0, 0, 7, 3, 3, 4, 10, 11, 11, 0, 0, 0, 0, 14, 14,
0, 1, 0, 14, 3, 0, 6, 0, 2, 0, 11, 0, 12, 0, 14,
0, 3, 11, 0, 0, 4, 14, 5, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 17, 6, 0, 7, 10, 0, 9, 0, 0, 2, 12, 1,
7, 15, 0, 1, 0, 17, 0, 0, 3, 4, 11, 4, 13, 0, 7,
0, 15, 1, 4, 0, 16, 5, 12, 0, 0, 5, 0, 0, 0, 13,
6, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 4, 1, 6,
16, 0, 0, 17, 0, 0, 8, 8, 1, 0, 12, 0, 0, 1, 16,
11, 10, 17, 14, 0, 0, 5, 7, 14, 1, 11, 17, 0, 0, 0,
0, 0, 10, 15, 17, 17, 10, 17, 0, 1, 0, 0, 0, 13, 17,
0, 14, 14, 0, 0, 12, 1, 14, 0, 1, 1, 0, 17, 0, 10,
14, 14, 0, 7, 17, 0, 11, 1, 0, 6, 14, 14, 2, 0, 10,
4, 15, 12, 0, 0, 3, 0, 10, 11, 8, 7, 0, 12, 17, 2,
10, 0, 5, 6, 8, 12, 0, 14, 11, 6, 7, 14, 1, 4, 15,
11, 12, 2, 15, 8, 0, 0, 16, 12, 1, 2, 4, 3, 0, 13,
12, 11, 14, 12, 16, 5, 13, 11, 8, 14
]
_moves_by_type: Dict[int, List[int]] = {}
for move, type in enumerate(_move_types):
_moves_by_type.setdefault(type, []).append(move)
_move_blacklist = frozenset({
0, # MOVE_NONE
165, # Struggle
15, # Cut
148, # Flash
249, # Rock Smash
70, # Strength
57, # Surf
19, # Fly
291, # Dive
127 # Waterfall
})
_legendary_pokemon = frozenset({
'Mew',
'Mewtwo',
'Articuno',
'Zapdos',
'Moltres',
'Lugia',
'Ho-oh',
'Raikou',
'Suicune',
'Entei',
'Celebi',
'Groudon',
'Kyogre',
'Rayquaza',
'Latios',
'Latias',
'Registeel',
'Regirock',
'Regice',
'Jirachi',
'Deoxys'
})
def get_random_species(
random: "Random",
candidates: List[Optional[SpeciesData]],
nearby_bst: Optional[int] = None,
species_type: Optional[int] = None,
allow_legendaries: bool = True) -> SpeciesData:
candidates: List[SpeciesData] = [species for species in candidates if species is not None]
if species_type is not None:
candidates = [species for species in candidates if species_type in species.types]
if not allow_legendaries:
candidates = [species for species in candidates if species.label not in _legendary_pokemon]
if nearby_bst is not None:
def has_nearby_bst(species: SpeciesData, max_percent_different: int) -> bool:
return abs(sum(species.base_stats) - nearby_bst) < nearby_bst * (max_percent_different / 100)
max_percent_different = 10
bst_filtered_candidates = [species for species in candidates if has_nearby_bst(species, max_percent_different)]
while len(bst_filtered_candidates) == 0:
max_percent_different += 10
bst_filtered_candidates = [
species
for species in candidates
if has_nearby_bst(species, max_percent_different)
]
candidates = bst_filtered_candidates
return random.choice(candidates)
def get_random_type(random: "Random") -> int:
picked_type = random.randrange(0, 18)
while picked_type == 9: # Don't pick the ??? type
picked_type = random.randrange(0, 18)
return picked_type
def get_random_move(
random: "Random",
blacklist: Optional[Set[int]] = None,
type_bias: int = 0,
normal_bias: int = 0,
type_target: Optional[Tuple[int, int]] = None) -> int:
expanded_blacklist = _move_blacklist | (blacklist if blacklist is not None else set())
bias = random.random() * 100
if bias < type_bias:
pass # Keep type_target unchanged
elif bias < type_bias + ((100 - type_bias) * (normal_bias / 100)):
type_target = (0, 0)
else:
type_target = None
chosen_move = None
# The blacklist is relatively small, so if we don't need to restrict
# ourselves to any particular types, it's usually much faster to pick
# a random number and hope it works. Limit this to 5 tries in case the
# blacklist is actually significant enough to make this unlikely to work.
if type_target is None:
remaining_attempts = 5
while remaining_attempts > 0:
remaining_attempts -= 1
chosen_move = random.randrange(0, data.constants["MOVES_COUNT"])
if chosen_move not in expanded_blacklist:
return chosen_move
else:
chosen_move = None
# We're either matching types or failed to pick a move above
if type_target is None:
possible_moves = [i for i in range(data.constants["MOVE_COUNT"]) if i not in expanded_blacklist]
else:
possible_moves = [move for move in _moves_by_type[type_target[0]] if move not in expanded_blacklist] + \
[move for move in _moves_by_type[type_target[1]] if move not in expanded_blacklist]
if len(possible_moves) == 0:
return get_random_move(random, None, type_bias, normal_bias, type_target)
return random.choice(possible_moves)
def get_random_damaging_move(random: "Random", blacklist: Optional[Set[int]] = None) -> int:
expanded_blacklist = _move_blacklist | (blacklist if blacklist is not None else set())
move_options = list(_damaging_moves)
move = random.choice(move_options)
while move in expanded_blacklist:
move = random.choice(move_options)
return move

View File

@ -0,0 +1,49 @@
"""
Functions related to AP regions for Pokemon Emerald (see ./data/regions for region definitions)
"""
from typing import TYPE_CHECKING, Dict, List, Tuple
from BaseClasses import ItemClassification, Region
from .data import data
from .items import PokemonEmeraldItem
from .locations import PokemonEmeraldLocation
if TYPE_CHECKING:
from . import PokemonEmeraldWorld
def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
"""
Iterates through regions created from JSON to create regions and adds them to the multiworld.
Also creates and places events and connects regions via warps and the exits defined in the JSON.
"""
regions: Dict[str, Region] = {}
connections: List[Tuple[str, str, str]] = []
for region_name, region_data in data.regions.items():
new_region = Region(region_name, world.player, world.multiworld)
for event_data in region_data.events:
event = PokemonEmeraldLocation(world.player, event_data.name, None, new_region)
event.place_locked_item(PokemonEmeraldItem(event_data.name, ItemClassification.progression, None, world.player))
new_region.locations.append(event)
for region_exit in region_data.exits:
connections.append((f"{region_name} -> {region_exit}", region_name, region_exit))
for warp in region_data.warps:
dest_warp = data.warps[data.warp_map[warp]]
if dest_warp.parent_region is None:
continue
connections.append((warp, region_name, dest_warp.parent_region))
regions[region_name] = new_region
for name, source, dest in connections:
regions[source].connect(regions[dest], name)
regions["Menu"] = Region("Menu", world.player, world.multiworld)
regions["Menu"].connect(regions["REGION_LITTLEROOT_TOWN/MAIN"], "Start Game")
return regions

View File

@ -0,0 +1,420 @@
"""
Classes and functions related to creating a ROM patch
"""
import os
import pkgutil
from typing import TYPE_CHECKING, List, Tuple
import bsdiff4
from worlds.Files import APDeltaPatch
from settings import get_settings
from .data import PokemonEmeraldData, TrainerPokemonDataTypeEnum, data
from .items import reverse_offset_item_value
from .options import RandomizeWildPokemon, RandomizeTrainerParties, EliteFourRequirement, NormanRequirement
from .pokemon import get_random_species
if TYPE_CHECKING:
from . import PokemonEmeraldWorld
class PokemonEmeraldDeltaPatch(APDeltaPatch):
game = "Pokemon Emerald"
hash = "605b89b67018abcea91e693a4dd25be3"
patch_file_ending = ".apemerald"
result_file_ending = ".gba"
@classmethod
def get_source_data(cls) -> bytes:
return get_base_rom_as_bytes()
location_visited_event_to_id_map = {
"EVENT_VISITED_LITTLEROOT_TOWN": 0,
"EVENT_VISITED_OLDALE_TOWN": 1,
"EVENT_VISITED_PETALBURG_CITY": 2,
"EVENT_VISITED_RUSTBORO_CITY": 3,
"EVENT_VISITED_DEWFORD_TOWN": 4,
"EVENT_VISITED_SLATEPORT_CITY": 5,
"EVENT_VISITED_MAUVILLE_CITY": 6,
"EVENT_VISITED_VERDANTURF_TOWN": 7,
"EVENT_VISITED_FALLARBOR_TOWN": 8,
"EVENT_VISITED_LAVARIDGE_TOWN": 9,
"EVENT_VISITED_FORTREE_CITY": 10,
"EVENT_VISITED_LILYCOVE_CITY": 11,
"EVENT_VISITED_MOSSDEEP_CITY": 12,
"EVENT_VISITED_SOOTOPOLIS_CITY": 13,
"EVENT_VISITED_PACIFIDLOG_TOWN": 14,
"EVENT_VISITED_EVER_GRANDE_CITY": 15,
"EVENT_VISITED_BATTLE_FRONTIER": 16,
"EVENT_VISITED_SOUTHERN_ISLAND": 17
}
def generate_output(world: "PokemonEmeraldWorld", output_directory: str) -> None:
base_rom = get_base_rom_as_bytes()
base_patch = pkgutil.get_data(__name__, "data/base_patch.bsdiff4")
patched_rom = bytearray(bsdiff4.patch(base_rom, base_patch))
# Set item values
for location in world.multiworld.get_locations(world.player):
# Set free fly location
if location.address is None:
if world.options.free_fly_location and location.name == "EVENT_VISITED_LITTLEROOT_TOWN":
_set_bytes_little_endian(
patched_rom,
data.rom_addresses["gArchipelagoOptions"] + 0x16,
1,
world.free_fly_location_id
)
continue
if location.item and location.item.player == world.player:
_set_bytes_little_endian(
patched_rom,
location.rom_address,
2,
reverse_offset_item_value(location.item.code)
)
else:
_set_bytes_little_endian(
patched_rom,
location.rom_address,
2,
data.constants["ITEM_ARCHIPELAGO_PROGRESSION"]
)
# Set start inventory
start_inventory = world.options.start_inventory.value.copy()
starting_badges = 0
if start_inventory.pop("Stone Badge", 0) > 0:
starting_badges |= (1 << 0)
if start_inventory.pop("Knuckle Badge", 0) > 0:
starting_badges |= (1 << 1)
if start_inventory.pop("Dynamo Badge", 0) > 0:
starting_badges |= (1 << 2)
if start_inventory.pop("Heat Badge", 0) > 0:
starting_badges |= (1 << 3)
if start_inventory.pop("Balance Badge", 0) > 0:
starting_badges |= (1 << 4)
if start_inventory.pop("Feather Badge", 0) > 0:
starting_badges |= (1 << 5)
if start_inventory.pop("Mind Badge", 0) > 0:
starting_badges |= (1 << 6)
if start_inventory.pop("Rain Badge", 0) > 0:
starting_badges |= (1 << 7)
pc_slots: List[Tuple[str, int]] = []
while any(qty > 0 for qty in start_inventory.values()):
if len(pc_slots) >= 19:
break
for i, item_name in enumerate(start_inventory.keys()):
if len(pc_slots) >= 19:
break
quantity = min(start_inventory[item_name], 999)
if quantity == 0:
continue
start_inventory[item_name] -= quantity
pc_slots.append((item_name, quantity))
pc_slots.sort(reverse=True)
for i, slot in enumerate(pc_slots):
address = data.rom_addresses["sNewGamePCItems"] + (i * 4)
item = reverse_offset_item_value(world.item_name_to_id[slot[0]])
_set_bytes_little_endian(patched_rom, address + 0, 2, item)
_set_bytes_little_endian(patched_rom, address + 2, 2, slot[1])
# Set species data
_set_species_info(world, patched_rom)
# Set encounter tables
if world.options.wild_pokemon != RandomizeWildPokemon.option_vanilla:
_set_encounter_tables(world, patched_rom)
# Set opponent data
if world.options.trainer_parties != RandomizeTrainerParties.option_vanilla:
_set_opponents(world, patched_rom)
# Set static pokemon
_set_static_encounters(world, patched_rom)
# Set starters
_set_starters(world, patched_rom)
# Set TM moves
_set_tm_moves(world, patched_rom)
# Set TM/HM compatibility
_set_tmhm_compatibility(world, patched_rom)
# Randomize opponent double or single
_randomize_opponent_battle_type(world, patched_rom)
# Options
# struct ArchipelagoOptions
# {
# /* 0x00 */ bool8 advanceTextWithHoldA;
# /* 0x01 */ bool8 isFerryEnabled;
# /* 0x02 */ bool8 areTrainersBlind;
# /* 0x03 */ bool8 canFlyWithoutBadge;
# /* 0x04 */ u16 expMultiplierNumerator;
# /* 0x06 */ u16 expMultiplierDenominator;
# /* 0x08 */ u16 birchPokemon;
# /* 0x0A */ bool8 guaranteedCatch;
# /* 0x0B */ bool8 betterShopsEnabled;
# /* 0x0C */ bool8 eliteFourRequiresGyms;
# /* 0x0D */ u8 eliteFourRequiredCount;
# /* 0x0E */ bool8 normanRequiresGyms;
# /* 0x0F */ u8 normanRequiredCount;
# /* 0x10 */ u8 startingBadges;
# /* 0x11 */ u8 receivedItemMessageFilter; // 0 = Show All; 1 = Show Progression Only; 2 = Show None
# /* 0x12 */ bool8 reusableTms;
# /* 0x14 */ u16 removedBlockers;
# /* 0x13 */ bool8 addRoute115Boulders;
# /* 0x14 */ u16 removedBlockers;
# /* 0x14 */ u16 removedBlockers;
# /* 0x16 */ u8 freeFlyLocation;
# };
options_address = data.rom_addresses["gArchipelagoOptions"]
# Set hold A to advance text
turbo_a = 1 if world.options.turbo_a else 0
_set_bytes_little_endian(patched_rom, options_address + 0x00, 1, turbo_a)
# Set ferry enabled
enable_ferry = 1 if world.options.enable_ferry else 0
_set_bytes_little_endian(patched_rom, options_address + 0x01, 1, enable_ferry)
# Set blind trainers
blind_trainers = 1 if world.options.blind_trainers else 0
_set_bytes_little_endian(patched_rom, options_address + 0x02, 1, blind_trainers)
# Set fly without badge
fly_without_badge = 1 if world.options.fly_without_badge else 0
_set_bytes_little_endian(patched_rom, options_address + 0x03, 1, fly_without_badge)
# Set exp modifier
numerator = min(max(world.options.exp_modifier.value, 0), 2**16 - 1)
_set_bytes_little_endian(patched_rom, options_address + 0x04, 2, numerator)
_set_bytes_little_endian(patched_rom, options_address + 0x06, 2, 100)
# Set Birch pokemon
_set_bytes_little_endian(
patched_rom,
options_address + 0x08,
2,
get_random_species(world.random, data.species).species_id
)
# Set guaranteed catch
guaranteed_catch = 1 if world.options.guaranteed_catch else 0
_set_bytes_little_endian(patched_rom, options_address + 0x0A, 1, guaranteed_catch)
# Set better shops
better_shops = 1 if world.options.better_shops else 0
_set_bytes_little_endian(patched_rom, options_address + 0x0B, 1, better_shops)
# Set elite four requirement
elite_four_requires_gyms = 1 if world.options.elite_four_requirement == EliteFourRequirement.option_gyms else 0
_set_bytes_little_endian(patched_rom, options_address + 0x0C, 1, elite_four_requires_gyms)
# Set elite four count
elite_four_count = min(max(world.options.elite_four_count.value, 0), 8)
_set_bytes_little_endian(patched_rom, options_address + 0x0D, 1, elite_four_count)
# Set norman requirement
norman_requires_gyms = 1 if world.options.norman_requirement == NormanRequirement.option_gyms else 0
_set_bytes_little_endian(patched_rom, options_address + 0x0E, 1, norman_requires_gyms)
# Set norman count
norman_count = min(max(world.options.norman_count.value, 0), 8)
_set_bytes_little_endian(patched_rom, options_address + 0x0F, 1, norman_count)
# Set starting badges
_set_bytes_little_endian(patched_rom, options_address + 0x10, 1, starting_badges)
# Set receive item messages type
receive_item_messages_type = world.options.receive_item_messages.value
_set_bytes_little_endian(patched_rom, options_address + 0x11, 1, receive_item_messages_type)
# Set reusable TMs
reusable_tms = 1 if world.options.reusable_tms else 0
_set_bytes_little_endian(patched_rom, options_address + 0x12, 1, reusable_tms)
# Set route 115 boulders
route_115_boulders = 1 if world.options.extra_boulders else 0
_set_bytes_little_endian(patched_rom, options_address + 0x13, 1, route_115_boulders)
# Set removed blockers
removed_roadblocks = world.options.remove_roadblocks.value
removed_roadblocks_bitfield = 0
removed_roadblocks_bitfield |= (1 << 0) if "Safari Zone Construction Workers" in removed_roadblocks else 0
removed_roadblocks_bitfield |= (1 << 1) if "Lilycove City Wailmer" in removed_roadblocks else 0
removed_roadblocks_bitfield |= (1 << 2) if "Route 110 Aqua Grunts" in removed_roadblocks else 0
removed_roadblocks_bitfield |= (1 << 3) if "Aqua Hideout Grunts" in removed_roadblocks else 0
removed_roadblocks_bitfield |= (1 << 4) if "Route 119 Aqua Grunts" in removed_roadblocks else 0
removed_roadblocks_bitfield |= (1 << 5) if "Route 112 Magma Grunts" in removed_roadblocks else 0
removed_roadblocks_bitfield |= (1 << 6) if "Seafloor Cavern Aqua Grunt" in removed_roadblocks else 0
_set_bytes_little_endian(patched_rom, options_address + 0x14, 2, removed_roadblocks_bitfield)
# Set slot name
player_name = world.multiworld.get_player_name(world.player)
for i, byte in enumerate(player_name.encode("utf-8")):
_set_bytes_little_endian(patched_rom, data.rom_addresses["gArchipelagoInfo"] + i, 1, byte)
# Write Output
out_file_name = world.multiworld.get_out_file_name_base(world.player)
output_path = os.path.join(output_directory, f"{out_file_name}.gba")
with open(output_path, "wb") as out_file:
out_file.write(patched_rom)
patch = PokemonEmeraldDeltaPatch(os.path.splitext(output_path)[0] + ".apemerald", player=world.player,
player_name=player_name, patched_path=output_path)
patch.write()
os.unlink(output_path)
def get_base_rom_as_bytes() -> bytes:
with open(get_settings().pokemon_emerald_settings.rom_file, "rb") as infile:
base_rom_bytes = bytes(infile.read())
return base_rom_bytes
def _set_bytes_little_endian(byte_array: bytearray, address: int, size: int, value: int) -> None:
offset = 0
while size > 0:
byte_array[address + offset] = value & 0xFF
value = value >> 8
offset += 1
size -= 1
def _set_encounter_tables(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
"""
Encounter tables are lists of
struct {
min_level: 0x01 bytes,
max_level: 0x01 bytes,
species_id: 0x02 bytes
}
"""
for map_data in world.modified_maps:
tables = [map_data.land_encounters, map_data.water_encounters, map_data.fishing_encounters]
for table in tables:
if table is not None:
for i, species_id in enumerate(table.slots):
address = table.rom_address + 2 + (4 * i)
_set_bytes_little_endian(rom, address, 2, species_id)
def _set_species_info(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
for species in world.modified_species:
if species is not None:
_set_bytes_little_endian(rom, species.rom_address + 6, 1, species.types[0])
_set_bytes_little_endian(rom, species.rom_address + 7, 1, species.types[1])
_set_bytes_little_endian(rom, species.rom_address + 8, 1, species.catch_rate)
_set_bytes_little_endian(rom, species.rom_address + 22, 1, species.abilities[0])
_set_bytes_little_endian(rom, species.rom_address + 23, 1, species.abilities[1])
for i, learnset_move in enumerate(species.learnset):
level_move = learnset_move.level << 9 | learnset_move.move_id
_set_bytes_little_endian(rom, species.learnset_rom_address + (i * 2), 2, level_move)
def _set_opponents(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
for trainer in world.modified_trainers:
party_address = trainer.party.rom_address
pokemon_data_size: int
if trainer.party.pokemon_data_type in {TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES, TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES}:
pokemon_data_size = 8
else: # Custom Moves
pokemon_data_size = 16
for i, pokemon in enumerate(trainer.party.pokemon):
pokemon_address = party_address + (i * pokemon_data_size)
# Replace species
_set_bytes_little_endian(rom, pokemon_address + 0x04, 2, pokemon.species_id)
# Replace custom moves if applicable
if trainer.party.pokemon_data_type == TrainerPokemonDataTypeEnum.NO_ITEM_CUSTOM_MOVES:
_set_bytes_little_endian(rom, pokemon_address + 0x06, 2, pokemon.moves[0])
_set_bytes_little_endian(rom, pokemon_address + 0x08, 2, pokemon.moves[1])
_set_bytes_little_endian(rom, pokemon_address + 0x0A, 2, pokemon.moves[2])
_set_bytes_little_endian(rom, pokemon_address + 0x0C, 2, pokemon.moves[3])
elif trainer.party.pokemon_data_type == TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES:
_set_bytes_little_endian(rom, pokemon_address + 0x08, 2, pokemon.moves[0])
_set_bytes_little_endian(rom, pokemon_address + 0x0A, 2, pokemon.moves[1])
_set_bytes_little_endian(rom, pokemon_address + 0x0C, 2, pokemon.moves[2])
_set_bytes_little_endian(rom, pokemon_address + 0x0E, 2, pokemon.moves[3])
def _set_static_encounters(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
for encounter in world.modified_static_encounters:
_set_bytes_little_endian(rom, encounter.rom_address, 2, encounter.species_id)
def _set_starters(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
address = data.rom_addresses["sStarterMon"]
(starter_1, starter_2, starter_3) = world.modified_starters
_set_bytes_little_endian(rom, address + 0, 2, starter_1)
_set_bytes_little_endian(rom, address + 2, 2, starter_2)
_set_bytes_little_endian(rom, address + 4, 2, starter_3)
def _set_tm_moves(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
tmhm_list_address = data.rom_addresses["sTMHMMoves"]
for i, move in enumerate(world.modified_tmhm_moves):
# Don't modify HMs
if i >= 50:
break
_set_bytes_little_endian(rom, tmhm_list_address + (i * 2), 2, move)
def _set_tmhm_compatibility(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
learnsets_address = data.rom_addresses["gTMHMLearnsets"]
for species in world.modified_species:
if species is not None:
_set_bytes_little_endian(rom, learnsets_address + (species.species_id * 8), 8, species.tm_hm_compatibility)
def _randomize_opponent_battle_type(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
probability = world.options.double_battle_chance.value / 100
battle_type_map = {
0: 4,
1: 8,
2: 6,
3: 13,
}
for trainer_data in data.trainers:
if trainer_data.battle_script_rom_address != 0 and len(trainer_data.party.pokemon) > 1:
if world.random.random() < probability:
# Set the trainer to be a double battle
_set_bytes_little_endian(rom, trainer_data.rom_address + 0x18, 1, 1)
# Swap the battle type in the script for the purpose of loading the right text
# and setting data to the right places
original_battle_type = rom[trainer_data.battle_script_rom_address + 1]
if original_battle_type in battle_type_map:
_set_bytes_little_endian(
rom,
trainer_data.battle_script_rom_address + 1,
1,
battle_type_map[original_battle_type]
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,352 @@
"""
Looks through data object to double-check it makes sense. Will fail for missing or duplicate definitions or
duplicate claims and give warnings for unused and unignored locations or warps.
"""
import logging
from typing import List
from .data import data
_ignorable_locations = {
# Trick House
"HIDDEN_ITEM_TRICK_HOUSE_NUGGET",
"ITEM_TRICK_HOUSE_PUZZLE_1_ORANGE_MAIL",
"ITEM_TRICK_HOUSE_PUZZLE_2_HARBOR_MAIL",
"ITEM_TRICK_HOUSE_PUZZLE_2_WAVE_MAIL",
"ITEM_TRICK_HOUSE_PUZZLE_3_SHADOW_MAIL",
"ITEM_TRICK_HOUSE_PUZZLE_3_WOOD_MAIL",
"ITEM_TRICK_HOUSE_PUZZLE_4_MECH_MAIL",
"ITEM_TRICK_HOUSE_PUZZLE_6_GLITTER_MAIL",
"ITEM_TRICK_HOUSE_PUZZLE_7_TROPIC_MAIL",
"ITEM_TRICK_HOUSE_PUZZLE_8_BEAD_MAIL",
# Battle Frontier
"ITEM_ARTISAN_CAVE_1F_CARBOS",
"ITEM_ARTISAN_CAVE_B1F_HP_UP",
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_CALCIUM",
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_IRON",
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_PROTEIN",
"HIDDEN_ITEM_ARTISAN_CAVE_B1F_ZINC",
# Event islands
"HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH"
}
_ignorable_warps = {
# Trick House
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:12/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:11",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:4/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:3",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:6/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:5",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:8/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:7",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:9/MAP_ROUTE110_TRICK_HOUSE_PUZZLE7:10",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:0,1/MAP_ROUTE110_TRICK_HOUSE_ENTRANCE:2!",
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8:2/MAP_ROUTE110_TRICK_HOUSE_END:0!",
# Department store elevator
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0,1/MAP_DYNAMIC:-1!",
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F:3/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F:2/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F:1/MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR:0!",
# Intro truck
"MAP_INSIDE_OF_TRUCK:0,1,2/MAP_DYNAMIC:-1!",
# Battle Frontier
"MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1",
"MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!",
"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1",
"MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1!",
"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0,1/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:3/MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM:0!",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2",
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR:0",
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0",
"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3",
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0,1/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2",
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0",
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:2/MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM:0",
"MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0,1,2/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6",
"MAP_BATTLE_FRONTIER_LOUNGE1:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5",
"MAP_BATTLE_FRONTIER_LOUNGE2:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3",
"MAP_BATTLE_FRONTIER_LOUNGE3:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9",
"MAP_BATTLE_FRONTIER_LOUNGE4:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6",
"MAP_BATTLE_FRONTIER_LOUNGE5:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7",
"MAP_BATTLE_FRONTIER_LOUNGE6:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8",
"MAP_BATTLE_FRONTIER_LOUNGE7:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7",
"MAP_BATTLE_FRONTIER_LOUNGE8:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10",
"MAP_BATTLE_FRONTIER_LOUNGE9:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11",
"MAP_BATTLE_FRONTIER_MART:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:0/MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:1/MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:10/MAP_BATTLE_FRONTIER_LOUNGE8:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:11/MAP_BATTLE_FRONTIER_LOUNGE9:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13/MAP_ARTISAN_CAVE_1F:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2/MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:3/MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4/MAP_BATTLE_FRONTIER_RANKING_HALL:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:5/MAP_BATTLE_FRONTIER_LOUNGE1:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:6/MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:7/MAP_BATTLE_FRONTIER_LOUNGE5:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:8/MAP_BATTLE_FRONTIER_LOUNGE6:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST:9/MAP_BATTLE_FRONTIER_LOUNGE3:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:0/MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1/MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10/MAP_ARTISAN_CAVE_B1F:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:2/MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:3/MAP_BATTLE_FRONTIER_LOUNGE2:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:4/MAP_BATTLE_FRONTIER_MART:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5/MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:6/MAP_BATTLE_FRONTIER_LOUNGE4:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:7/MAP_BATTLE_FRONTIER_LOUNGE7:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8/MAP_BATTLE_FRONTIER_RECEPTION_GATE:0",
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9/MAP_BATTLE_FRONTIER_RECEPTION_GATE:1",
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:12",
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2/MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0",
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:0/MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F:2",
"MAP_BATTLE_FRONTIER_RANKING_HALL:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:4",
"MAP_BATTLE_FRONTIER_RECEPTION_GATE:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:8",
"MAP_BATTLE_FRONTIER_RECEPTION_GATE:1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:9",
"MAP_BATTLE_FRONTIER_SCOTTS_HOUSE:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:5",
"MAP_ARTISAN_CAVE_1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:13",
"MAP_ARTISAN_CAVE_1F:1/MAP_ARTISAN_CAVE_B1F:1",
"MAP_ARTISAN_CAVE_B1F:0/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:10",
"MAP_ARTISAN_CAVE_B1F:1/MAP_ARTISAN_CAVE_1F:1",
# Terra Cave and Marine Cave
"MAP_TERRA_CAVE_ENTRANCE:0/MAP_DYNAMIC:-1!",
"MAP_TERRA_CAVE_END:0/MAP_TERRA_CAVE_ENTRANCE:1",
"MAP_TERRA_CAVE_ENTRANCE:1/MAP_TERRA_CAVE_END:0",
"MAP_ROUTE113:1/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE113:2/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE114:3/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE114:4/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE115:1/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE115:2/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE116:3/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE116:4/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE118:0/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_ROUTE118:1/MAP_TERRA_CAVE_ENTRANCE:0!",
"MAP_UNDERWATER_MARINE_CAVE:0/MAP_DYNAMIC:-1!",
"MAP_MARINE_CAVE_END:0/MAP_MARINE_CAVE_ENTRANCE:0",
"MAP_MARINE_CAVE_ENTRANCE:0/MAP_MARINE_CAVE_END:0",
"MAP_UNDERWATER_ROUTE105:0/MAP_UNDERWATER_MARINE_CAVE:0!",
"MAP_UNDERWATER_ROUTE105:1/MAP_UNDERWATER_MARINE_CAVE:0!",
"MAP_UNDERWATER_ROUTE125:0/MAP_UNDERWATER_MARINE_CAVE:0!",
"MAP_UNDERWATER_ROUTE125:1/MAP_UNDERWATER_MARINE_CAVE:0!",
"MAP_UNDERWATER_ROUTE127:0/MAP_UNDERWATER_MARINE_CAVE:0!",
"MAP_UNDERWATER_ROUTE127:1/MAP_UNDERWATER_MARINE_CAVE:0!",
"MAP_UNDERWATER_ROUTE129:0/MAP_UNDERWATER_MARINE_CAVE:0!",
"MAP_UNDERWATER_ROUTE129:1/MAP_UNDERWATER_MARINE_CAVE:0!",
# Event islands
"MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0",
"MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0",
"MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1",
"MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1",
"MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1",
"MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1",
"MAP_NAVEL_ROCK_B1F:0/MAP_NAVEL_ROCK_ENTRANCE:0",
"MAP_NAVEL_ROCK_B1F:1/MAP_NAVEL_ROCK_FORK:1",
"MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0",
"MAP_NAVEL_ROCK_DOWN01:0/MAP_NAVEL_ROCK_FORK:2",
"MAP_NAVEL_ROCK_DOWN01:1/MAP_NAVEL_ROCK_DOWN02:0",
"MAP_NAVEL_ROCK_DOWN02:0/MAP_NAVEL_ROCK_DOWN01:1",
"MAP_NAVEL_ROCK_DOWN02:1/MAP_NAVEL_ROCK_DOWN03:0",
"MAP_NAVEL_ROCK_DOWN03:0/MAP_NAVEL_ROCK_DOWN02:1",
"MAP_NAVEL_ROCK_DOWN03:1/MAP_NAVEL_ROCK_DOWN04:0",
"MAP_NAVEL_ROCK_DOWN04:0/MAP_NAVEL_ROCK_DOWN03:1",
"MAP_NAVEL_ROCK_DOWN04:1/MAP_NAVEL_ROCK_DOWN05:0",
"MAP_NAVEL_ROCK_DOWN05:0/MAP_NAVEL_ROCK_DOWN04:1",
"MAP_NAVEL_ROCK_DOWN05:1/MAP_NAVEL_ROCK_DOWN06:0",
"MAP_NAVEL_ROCK_DOWN06:0/MAP_NAVEL_ROCK_DOWN05:1",
"MAP_NAVEL_ROCK_DOWN06:1/MAP_NAVEL_ROCK_DOWN07:0",
"MAP_NAVEL_ROCK_DOWN07:0/MAP_NAVEL_ROCK_DOWN06:1",
"MAP_NAVEL_ROCK_DOWN07:1/MAP_NAVEL_ROCK_DOWN08:0",
"MAP_NAVEL_ROCK_DOWN08:0/MAP_NAVEL_ROCK_DOWN07:1",
"MAP_NAVEL_ROCK_DOWN08:1/MAP_NAVEL_ROCK_DOWN09:0",
"MAP_NAVEL_ROCK_DOWN09:0/MAP_NAVEL_ROCK_DOWN08:1",
"MAP_NAVEL_ROCK_DOWN09:1/MAP_NAVEL_ROCK_DOWN10:0",
"MAP_NAVEL_ROCK_DOWN10:0/MAP_NAVEL_ROCK_DOWN09:1",
"MAP_NAVEL_ROCK_DOWN10:1/MAP_NAVEL_ROCK_DOWN11:1",
"MAP_NAVEL_ROCK_DOWN11:0/MAP_NAVEL_ROCK_BOTTOM:0",
"MAP_NAVEL_ROCK_DOWN11:1/MAP_NAVEL_ROCK_DOWN10:1",
"MAP_NAVEL_ROCK_ENTRANCE:0/MAP_NAVEL_ROCK_B1F:0",
"MAP_NAVEL_ROCK_ENTRANCE:1/MAP_NAVEL_ROCK_EXTERIOR:1",
"MAP_NAVEL_ROCK_EXTERIOR:0/MAP_NAVEL_ROCK_HARBOR:0",
"MAP_NAVEL_ROCK_EXTERIOR:1/MAP_NAVEL_ROCK_ENTRANCE:1",
"MAP_NAVEL_ROCK_FORK:0/MAP_NAVEL_ROCK_UP1:0",
"MAP_NAVEL_ROCK_FORK:1/MAP_NAVEL_ROCK_B1F:1",
"MAP_NAVEL_ROCK_FORK:2/MAP_NAVEL_ROCK_DOWN01:0",
"MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0",
"MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1",
"MAP_NAVEL_ROCK_UP1:0/MAP_NAVEL_ROCK_FORK:0",
"MAP_NAVEL_ROCK_UP1:1/MAP_NAVEL_ROCK_UP2:0",
"MAP_NAVEL_ROCK_UP2:0/MAP_NAVEL_ROCK_UP1:1",
"MAP_NAVEL_ROCK_UP2:1/MAP_NAVEL_ROCK_UP3:0",
"MAP_NAVEL_ROCK_UP3:0/MAP_NAVEL_ROCK_UP2:1",
"MAP_NAVEL_ROCK_UP3:1/MAP_NAVEL_ROCK_UP4:0",
"MAP_NAVEL_ROCK_UP4:0/MAP_NAVEL_ROCK_UP3:1",
"MAP_NAVEL_ROCK_UP4:1/MAP_NAVEL_ROCK_TOP:0",
# Secret bases
"MAP_SECRET_BASE_BROWN_CAVE1:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_BROWN_CAVE2:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_BROWN_CAVE3:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_BROWN_CAVE4:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_BLUE_CAVE1:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_BLUE_CAVE2:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_BLUE_CAVE3:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_BLUE_CAVE4:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_YELLOW_CAVE1:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_YELLOW_CAVE2:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_YELLOW_CAVE3:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_YELLOW_CAVE4:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_RED_CAVE1:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_RED_CAVE2:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_RED_CAVE3:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_RED_CAVE4:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_SHRUB1:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_SHRUB2:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_SHRUB3:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_SHRUB4:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_TREE1:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_TREE2:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_TREE3:0/MAP_DYNAMIC:-2!",
"MAP_SECRET_BASE_TREE4:0/MAP_DYNAMIC:-2!",
# Multiplayer rooms
"MAP_RECORD_CORNER:0,1,2,3/MAP_DYNAMIC:-1!",
"MAP_UNION_ROOM:0,1/MAP_DYNAMIC:-1!",
"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_PETALBURG_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:1/MAP_UNION_ROOM:0!",
"MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_OLDALE_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_FORTREE_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:1/MAP_UNION_ROOM:0!",
"MAP_TRADE_CENTER:0,1/MAP_DYNAMIC:-1!",
"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_MAUVILLE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_PETALBURG_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F:2/MAP_TRADE_CENTER:0!",
"MAP_DEWFORD_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_OLDALE_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_SLATEPORT_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_RUSTBORO_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_LILYCOVE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_FORTREE_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F:2/MAP_TRADE_CENTER:0!",
"MAP_BATTLE_COLOSSEUM_2P:0,1/MAP_DYNAMIC:-1!",
"MAP_BATTLE_COLOSSEUM_4P:0,1,2,3/MAP_DYNAMIC:-1!",
# Unused content
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:0/MAP_CAVE_OF_ORIGIN_1F:1!",
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0",
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP1:1",
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0",
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:0/MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP2:1",
"MAP_CAVE_OF_ORIGIN_UNUSED_RUBY_SAPPHIRE_MAP3:1/MAP_CAVE_OF_ORIGIN_B1F:0!",
"MAP_LILYCOVE_CITY_UNUSED_MART:0,1/MAP_LILYCOVE_CITY:0!"
}
def validate_regions() -> bool:
error_messages: List[str] = []
warn_messages: List[str] = []
failed = False
def error(message: str) -> None:
nonlocal failed
failed = True
error_messages.append(message)
def warn(message: str) -> None:
warn_messages.append(message)
# Check regions
for name, region in data.regions.items():
for region_exit in region.exits:
if region_exit not in data.regions:
error(f"Pokemon Emerald: Region [{region_exit}] referenced by [{name}] was not defined")
# Check warps
for warp_source, warp_dest in data.warp_map.items():
if warp_source in _ignorable_warps:
continue
if warp_dest is None:
error(f"Pokemon Emerald: Warp [{warp_source}] has no destination")
elif not data.warps[warp_dest].connects_to(data.warps[warp_source]) and not data.warps[warp_source].is_one_way:
error(f"Pokemon Emerald: Warp [{warp_source}] appears to be a one-way warp but was not marked as one")
# Check locations
claimed_locations = [location for region in data.regions.values() for location in region.locations]
claimed_locations_set = set()
for location_name in claimed_locations:
if location_name in claimed_locations_set:
error(f"Pokemon Emerald: Location [{location_name}] was claimed by multiple regions")
claimed_locations_set.add(location_name)
for location_name in data.locations:
if location_name not in claimed_locations and location_name not in _ignorable_locations:
warn(f"Pokemon Emerald: Location [{location_name}] was not claimed by any region")
warn_messages.sort()
error_messages.sort()
for message in warn_messages:
logging.warning(message)
for message in error_messages:
logging.error(message)
logging.debug("Pokemon Emerald sanity check done. Found %s errors and %s warnings.", len(error_messages), len(warn_messages))
return not failed

View File

@ -0,0 +1,5 @@
from test.TestBase import WorldTestBase
class PokemonEmeraldTestBase(WorldTestBase):
game = "Pokemon Emerald"

View File

@ -0,0 +1,178 @@
from Options import Toggle
from . import PokemonEmeraldTestBase
from ..util import location_name_to_label
from ..options import NormanRequirement
class TestBasic(PokemonEmeraldTestBase):
def test_always_accessible(self) -> None:
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_102_POTION")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_SUPER_POTION")))
class TestSurf(PokemonEmeraldTestBase):
options = {
"npc_gifts": Toggle.option_true
}
def test_inaccessible_with_no_surf(self) -> None:
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
self.assertFalse(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
self.assertFalse(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
def test_accessible_with_surf_only(self) -> None:
self.collect_by_name(["HM03 Surf", "Balance Badge"])
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
self.assertTrue(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
self.assertTrue(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_4")))
class TestFreeFly(PokemonEmeraldTestBase):
options = {
"npc_gifts": Toggle.option_true,
"free_fly_location": Toggle.option_true
}
def setUp(self) -> None:
super(PokemonEmeraldTestBase, self).setUp()
# Swap free fly to Sootopolis
free_fly_location = self.multiworld.get_location("FREE_FLY_LOCATION", 1)
free_fly_location.item = None
free_fly_location.place_locked_item(self.multiworld.worlds[1].create_event("EVENT_VISITED_SOOTOPOLIS_CITY"))
def test_sootopolis_gift_inaccessible_with_no_surf(self) -> None:
self.collect_by_name(["HM02 Fly", "Feather Badge"])
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_TM31")))
def test_sootopolis_gift_accessible_with_surf(self) -> None:
self.collect_by_name(["HM03 Surf", "Balance Badge", "HM02 Fly", "Feather Badge"])
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_TM31")))
class TestFerry(PokemonEmeraldTestBase):
options = {
"npc_gifts": Toggle.option_true,
"enable_ferry": Toggle.option_true
}
def test_inaccessible_with_no_items(self) -> None:
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
def test_inaccessible_with_only_slateport_access(self) -> None:
self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge", "Acro Bike"])
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
def test_accessible_with_slateport_access_and_ticket(self) -> None:
self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge", "Acro Bike", "S.S. Ticket"])
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
class TestExtraBouldersOn(PokemonEmeraldTestBase):
options = {
"extra_boulders": Toggle.option_true
}
def test_inaccessible_with_no_items(self) -> None:
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
def test_inaccessible_with_surf_only(self) -> None:
self.collect_by_name(["HM03 Surf", "Balance Badge"])
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
def test_accessible_with_surf_and_strength(self) -> None:
self.collect_by_name(["HM03 Surf", "Balance Badge", "HM04 Strength", "Heat Badge"])
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
class TestExtraBouldersOff(PokemonEmeraldTestBase):
options = {
"extra_boulders": Toggle.option_false
}
def test_inaccessible_with_no_items(self) -> None:
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
def test_accessible_with_surf_only(self) -> None:
self.collect_by_name(["HM03 Surf", "Balance Badge"])
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_115_PP_UP")))
class TestNormanRequirement1(PokemonEmeraldTestBase):
options = {
"norman_requirement": NormanRequirement.option_badges,
"norman_count": 0
}
def test_accessible_with_no_items(self) -> None:
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
class TestNormanRequirement2(PokemonEmeraldTestBase):
options = {
"norman_requirement": NormanRequirement.option_badges,
"norman_count": 4
}
def test_inaccessible_with_no_items(self) -> None:
self.assertFalse(self.can_reach_location(location_name_to_label("BADGE_5")))
def test_accessible_with_enough_badges(self) -> None:
self.collect_by_name(["Stone Badge", "Knuckle Badge", "Feather Badge", "Balance Badge"])
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
class TestNormanRequirement3(PokemonEmeraldTestBase):
options = {
"norman_requirement": NormanRequirement.option_gyms,
"norman_count": 0
}
def test_accessible_with_no_items(self) -> None:
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
class TestNormanRequirement4(PokemonEmeraldTestBase):
options = {
"norman_requirement": NormanRequirement.option_gyms,
"norman_count": 4
}
def test_inaccessible_with_no_items(self) -> None:
self.assertFalse(self.can_reach_location(location_name_to_label("BADGE_5")))
def test_accessible_with_reachable_gyms(self) -> None:
self.collect_by_name(["HM03 Surf", "Balance Badge"]) # Reaches Roxanne, Brawley, Wattson, and Flannery
self.assertTrue(self.can_reach_location(location_name_to_label("BADGE_5")))
class TestVictoryRoad(PokemonEmeraldTestBase):
options = {
"elite_four_requirement": NormanRequirement.option_badges,
"elite_four_count": 0,
"remove_roadblocks": {"Lilycove City Wailmer"}
}
def test_accessible_with_specific_hms(self) -> None:
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
self.collect_by_name(["HM03 Surf", "Balance Badge"])
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
self.collect_by_name(["HM07 Waterfall", "Rain Badge"])
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
self.collect_by_name(["HM04 Strength", "Heat Badge"])
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
self.collect_by_name(["HM06 Rock Smash", "Dynamo Badge"])
self.assertFalse(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))
self.collect_by_name(["HM05 Flash", "Knuckle Badge"])
self.assertTrue(self.can_reach_location("EVENT_DEFEAT_CHAMPION"))

View File

@ -0,0 +1,21 @@
from test.TestBase import TestBase
from ..data import Warp
class TestWarps(TestBase):
def test_warps_connect_ltr(self) -> None:
# 2-way
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:0").connects_to(Warp("FAKE_MAP_B:0/FAKE_MAP_A:0")))
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:0")))
self.assertTrue(Warp("FAKE_MAP_A:0,1/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:0")))
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:2,3/FAKE_MAP_A:0")))
# 1-way
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:3")))
self.assertTrue(Warp("FAKE_MAP_A:0,1/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2/FAKE_MAP_A:3")))
self.assertTrue(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2!").connects_to(Warp("FAKE_MAP_B:2,3/FAKE_MAP_A:3")))
# Invalid
self.assertFalse(Warp("FAKE_MAP_A:0/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:4/FAKE_MAP_A:0")))
self.assertFalse(Warp("FAKE_MAP_A:0,4/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_B:4/FAKE_MAP_A:0")))
self.assertFalse(Warp("FAKE_MAP_A:0,4/FAKE_MAP_B:2").connects_to(Warp("FAKE_MAP_C:2/FAKE_MAP_A:0")))

View File

@ -0,0 +1,19 @@
from typing import List
from .data import data
def location_name_to_label(name: str) -> str:
return data.locations[name].label
def int_to_bool_array(num: int) -> List[bool]:
binary_string = format(num, '064b')
bool_array = [bit == '1' for bit in reversed(binary_string)]
return bool_array
def bool_array_to_int(bool_array: List[bool]) -> int:
binary_string = ''.join(['1' if bit else '0' for bit in reversed(bool_array)])
num = int(binary_string, 2)
return num