Pokemon Emerald: Fix dexsanity encounters using non-enabled encounter types for logical access (#6161)

This commit is contained in:
Bryce Wilson authored and GitHub committed 2026-09-28 01:29:56 +02:00
1 parent 6f3cd85690
commit c6697337bb
10 files changed
+124 -59

No files matched your search

+13
View File
@@ -1,3 +1,16 @@
# 2.6.0
### Features
- Added `dexsanity_encounter_types` and `world_version` to slot data for trackers.
# 2.5.1
### Fixes
- Fixed dexsanity logic to correctly account only for catching species in their enabled encounter types, even if they
can be found elsewhere.
# 2.5.0
### Features
+47 -3
View File
@@ -8,16 +8,18 @@ import os
import pkgutil
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar, TextIO, Union
from BaseClasses import CollectionState, ItemClassification, MultiWorld, Tutorial, LocationProgressType
from BaseClasses import CollectionState, Item, ItemClassification, MultiWorld, Tutorial, LocationProgressType
from Fill import FillError, fill_restrictive
from Options import OptionError, Toggle
import settings
from worlds.AutoWorld import WebWorld, World
from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient
from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, LocationCategory, data as emerald_data
from .data import (LEGENDARY_POKEMON, PokemonSource, MapData, SpeciesData, TrainerData, LocationCategory,
data as emerald_data)
from .groups import ITEM_GROUPS, LOCATION_GROUPS
from .items import PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, offset_item_value
from .items import (PokemonEmeraldItem, PokemonEmeraldObtainPokemonEventItem, create_item_label_to_code_map,
get_item_classification, offset_item_value)
from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_by_category,
set_free_fly, set_legendary_cave_entrances)
from .opponents import randomize_opponent_parties
@@ -124,6 +126,7 @@ class PokemonEmeraldWorld(World):
blacklisted_starters: Set[int]
blacklisted_opponent_pokemon: Set[int]
allowed_dexsanity_species: set[int]
enabled_dexsanity_encounter_types: set[PokemonSource]
hm_requirements: Dict[str, Union[int, List[str]]]
auth: bytes
@@ -144,6 +147,7 @@ class PokemonEmeraldWorld(World):
self.blacklisted_starters = set()
self.blacklisted_opponent_pokemon = set()
self.allowed_dexsanity_species = set()
self.enabled_dexsanity_encounter_types = set()
self.modified_maps = copy.deepcopy(emerald_data.maps)
self.modified_species = copy.deepcopy(emerald_data.species)
self.modified_tmhm_moves = []
@@ -201,6 +205,16 @@ class PokemonEmeraldWorld(World):
if "_Legendaries" in self.options.trainer_party_blacklist.value:
self.blacklisted_opponent_pokemon |= LEGENDARY_POKEMON
encounter_table = {
"Land": PokemonSource.LAND,
"Water": PokemonSource.WATER,
"Fishing": PokemonSource.FISHING,
}
self.enabled_dexsanity_encounter_types = {
encounter_table[encounter_type]
for encounter_type in self.options.dexsanity_encounter_types.value
}
# In race mode we don't patch any item location information into the ROM
if self.multiworld.is_race and not self.options.remote_items:
logging.warning("Pokemon Emerald: Forcing player %s (%s) to use remote items due to race mode.",
@@ -739,9 +753,11 @@ class PokemonEmeraldWorld(World):
"modify_118",
"death_link",
"normalize_encounter_rates",
"dexsanity_encounter_types",
)
slot_data["free_fly_location_id"] = self.free_fly_location_id
slot_data["hm_requirements"] = self.hm_requirements
slot_data["world_version"] = self.world_version
return slot_data
def create_item(self, name: str) -> PokemonEmeraldItem:
@@ -762,3 +778,31 @@ class PokemonEmeraldWorld(World):
None,
self.player
)
def collect(self, state: CollectionState, item: Item) -> bool:
changed = super().collect(state, item)
if changed:
if isinstance(item, PokemonEmeraldObtainPokemonEventItem):
if item.source in self.enabled_dexsanity_encounter_types:
state.prog_items[self.player].update({
f"DEXSANITY_{emerald_data.species[item.species].name}": 1,
})
if item.species in (emerald_data.constants["SPECIES_WAILORD"], emerald_data.constants["SPECIES_RELICANTH"]):
state.prog_items[self.player].update({
f"REGI_WALL_{emerald_data.species[item.species].name}": 1,
})
return changed
def remove(self, state: CollectionState, item: Item) -> bool:
changed = super().remove(state, item)
if changed:
if isinstance(item, PokemonEmeraldObtainPokemonEventItem):
if item.source in self.enabled_dexsanity_encounter_types:
state.prog_items[self.player].subtract({
f"DEXSANITY_{emerald_data.species[item.species].name}": 1,
})
if item.species in (emerald_data.constants["SPECIES_WAILORD"], emerald_data.constants["SPECIES_RELICANTH"]):
state.prog_items[self.player].subtract({
f"REGI_WALL_{emerald_data.species[item.species].name}": 1,
})
return changed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"game": "Pokemon Emerald",
"world_version": "2.5.0",
"world_version": "2.6.0",
"minimum_ap_version": "0.6.1",
"authors": ["Zunawe"]
}
+8 -9
View File
@@ -5,7 +5,7 @@ defined data (like location labels or usable pokemon species), some cleanup
and sorting, and Warp methods.
"""
from dataclasses import dataclass
from enum import IntEnum, Enum
from enum import IntEnum, StrEnum
import orjson
from typing import Dict, List, NamedTuple, Optional, Set, FrozenSet, Tuple, Any, Union
import pkgutil
@@ -148,8 +148,7 @@ class EncounterTableData(NamedTuple):
address: int
# class EncounterType(StrEnum): # StrEnum introduced in python 3.11
class EncounterType(Enum):
class PokemonSource(StrEnum):
LAND = "LAND"
WATER = "WATER"
FISHING = "FISHING"
@@ -161,7 +160,7 @@ class MapData:
name: str
label: str
header_address: int
encounters: Dict[EncounterType, EncounterTableData]
encounters: Dict[PokemonSource, EncounterTableData]
class EventData(NamedTuple):
@@ -354,24 +353,24 @@ def _init() -> None:
if map_name in IGNORABLE_MAPS:
continue
encounter_tables: Dict[EncounterType, EncounterTableData] = {}
encounter_tables: Dict[PokemonSource, EncounterTableData] = {}
if "land_encounters" in map_json:
encounter_tables[EncounterType.LAND] = EncounterTableData(
encounter_tables[PokemonSource.LAND] = EncounterTableData(
map_json["land_encounters"]["slots"],
map_json["land_encounters"]["address"]
)
if "water_encounters" in map_json:
encounter_tables[EncounterType.WATER] = EncounterTableData(
encounter_tables[PokemonSource.WATER] = EncounterTableData(
map_json["water_encounters"]["slots"],
map_json["water_encounters"]["address"]
)
if "fishing_encounters" in map_json:
encounter_tables[EncounterType.FISHING] = EncounterTableData(
encounter_tables[PokemonSource.FISHING] = EncounterTableData(
map_json["fishing_encounters"]["slots"],
map_json["fishing_encounters"]["address"]
)
if "rock_smash_encounters" in map_json:
encounter_tables[EncounterType.ROCK_SMASH] = EncounterTableData(
encounter_tables[PokemonSource.ROCK_SMASH] = EncounterTableData(
map_json["rock_smash_encounters"]["slots"],
map_json["rock_smash_encounters"]["address"]
)
+16 -2
View File
@@ -1,11 +1,11 @@
"""
Classes and functions related to AP items for Pokemon Emerald
"""
from typing import Dict, FrozenSet, Set, Optional
from typing import Dict, FrozenSet, Optional
from BaseClasses import Item, ItemClassification
from .data import BASE_OFFSET, data
from .data import BASE_OFFSET, PokemonSource, data
class PokemonEmeraldItem(Item):
@@ -21,6 +21,20 @@ class PokemonEmeraldItem(Item):
self.tags = data.items[reverse_offset_item_value(code)].tags
class PokemonEmeraldObtainPokemonEventItem(PokemonEmeraldItem):
source: PokemonSource
species: int
def __init__(self, player: int, source: PokemonSource, species: int) -> None:
super().__init__("", ItemClassification.progression, None, player)
self.replace_species(species)
self.source = source
def replace_species(self, new_species: int):
self.species = new_species
self.name = f"OBTAIN_{data.species[new_species].name}"
def offset_item_value(item_value: int) -> int:
"""
Returns the AP item id (code) for a given item value
+3 -3
View File
@@ -4,7 +4,7 @@ Option definitions for Pokemon Emerald
from dataclasses import dataclass
from Options import (Choice, DeathLink, DefaultOnToggle, OptionSet, NamedRange, Range, Toggle, FreeText,
PerGameCommonOptions, OptionGroup, StartInventory, OptionList)
PerGameCommonOptions, OptionGroup, StartInventory)
from .data import data
@@ -129,14 +129,14 @@ class Dexsanity(Toggle):
display_name = "Dexsanity"
class DexsanityEncounterTypes(OptionList):
class DexsanityEncounterTypes(OptionSet):
"""
Determines which Dexsanity encounter areas are in logic.
Logic will only consider access to Pokemon at these encounter types, but they may still be found elsewhere.
"""
display_name = "Dexsanity Encounter Types"
valid_keys = {"Land", "Water", "Fishing"}
valid_keys = ["Land", "Water", "Fishing"]
default = valid_keys.copy()
+16 -20
View File
@@ -4,8 +4,9 @@ Functions related to pokemon species and moves
import functools
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterType, EncounterTableData, LearnsetMove, SpeciesData,
from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, PokemonSource, EncounterTableData, LearnsetMove, SpeciesData,
MapData, data)
from .items import PokemonEmeraldObtainPokemonEventItem
from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
TmTutorCompatibility)
@@ -227,14 +228,14 @@ def randomize_types(world: "PokemonEmeraldWorld") -> None:
evolutions += [world.modified_species[evo.species_id] for evo in evolution.evolutions]
_encounter_subcategory_ranges: Dict[EncounterType, Dict[range, Optional[str]]] = {
EncounterType.LAND: {range(0, 12): None},
EncounterType.WATER: {range(0, 5): None},
EncounterType.FISHING: {range(0, 2): "OLD_ROD", range(2, 5): "GOOD_ROD", range(5, 10): "SUPER_ROD"},
_encounter_subcategory_ranges: Dict[PokemonSource, Dict[range, Optional[str]]] = {
PokemonSource.LAND: {range(0, 12): None},
PokemonSource.WATER: {range(0, 5): None},
PokemonSource.FISHING: {range(0, 2): "OLD_ROD", range(2, 5): "GOOD_ROD", range(5, 10): "SUPER_ROD"},
}
def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slots: List[int], encounter_type: EncounterType):
def _adjust_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slots: List[int], encounter_type: PokemonSource):
"""
Renames the events that correspond to wild encounters to reflect the new species there after randomization
"""
@@ -254,22 +255,17 @@ def _rename_wild_events(world: "PokemonEmeraldWorld", map_data: MapData, new_slo
# Fishing locations include the rod name
subcategory_str = "" if subcategory_name is None else "_" + subcategory_name
encounter_location_index = subcategory_species.index(new_species_id) + 1
encounter_location_name = f"{map_data.name}_{encounter_type.value}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
encounter_location_name = f"{map_data.name}_{encounter_type}_ENCOUNTERS{subcategory_str}_{encounter_location_index}"
try:
# Get the corresponding location and change the event name to reflect the new species
slot_location = world.multiworld.get_location(encounter_location_name, world.player)
slot_location.item.name = f"CATCH_{data.species[new_species_id].name}"
assert isinstance(slot_location.item, PokemonEmeraldObtainPokemonEventItem)
slot_location.item.replace_species(new_species_id)
except KeyError:
pass # Map probably isn't included; should be careful here about bad encounter location names
def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
encounter_table = {
"Land": EncounterType.LAND,
"Water": EncounterType.WATER,
"Fishing": EncounterType.FISHING,
}
enabled_encounters = {encounter_table[encounter_type] for encounter_type in world.options.dexsanity_encounter_types.value}
if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
return
@@ -296,7 +292,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
placed_priority_species = False
map_data = world.modified_maps[map_name]
new_encounters: Dict[EncounterType, EncounterTableData] = {}
new_encounters: Dict[PokemonSource, EncounterTableData] = {}
for encounter_type, table in map_data.encounters.items():
# Create a map from the original species to new species
@@ -306,7 +302,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
for species_id in table.slots:
if species_id not in species_old_to_new_map:
if not placed_priority_species and len(priority_species) > 0 \
and encounter_type != EncounterType.ROCK_SMASH and map_name not in OUT_OF_LOGIC_MAPS:
and encounter_type != PokemonSource.ROCK_SMASH and map_name not in OUT_OF_LOGIC_MAPS:
new_species_id = priority_species.pop()
placed_priority_species = True
else:
@@ -370,7 +366,7 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
species_old_to_new_map[species_id] = new_species_id
if world.options.dexsanity and encounter_type != EncounterType.ROCK_SMASH \
if world.options.dexsanity and encounter_type != PokemonSource.ROCK_SMASH \
and map_name not in OUT_OF_LOGIC_MAPS and new_species_id not in world.blacklisted_wilds:
already_placed.add(new_species_id)
@@ -378,14 +374,14 @@ def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
new_slots: List[int] = []
for species_id in table.slots:
new_slots.append(species_old_to_new_map[species_id])
if encounter_type in enabled_encounters:
if encounter_type in world.enabled_dexsanity_encounter_types:
world.allowed_dexsanity_species.update(new_slots)
new_encounters[encounter_type] = EncounterTableData(new_slots, table.address)
# Rock smash encounters not used in logic, so they have no events
if encounter_type != EncounterType.ROCK_SMASH:
_rename_wild_events(world, map_data, new_slots, encounter_type)
if encounter_type != PokemonSource.ROCK_SMASH:
_adjust_wild_events(world, map_data, new_slots, encounter_type)
map_data.encounters = new_encounters
+12 -13
View File
@@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
from BaseClasses import CollectionState, ItemClassification, Region
from .data import EncounterType, data
from .items import PokemonEmeraldItem
from .data import PokemonSource, data
from .items import PokemonEmeraldItem, PokemonEmeraldObtainPokemonEventItem
from .locations import PokemonEmeraldLocation
if TYPE_CHECKING:
@@ -20,10 +20,10 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
"""
# Used in connect_to_map_encounters. Splits encounter categories into "subcategories" and gives them names
# and rules so the rods can only access their specific slots. Rock smash encounters are not considered in logic.
encounter_categories: Dict[EncounterType, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
EncounterType.LAND: [(None, range(0, 12), None)],
EncounterType.WATER: [(None, range(0, 5), None)],
EncounterType.FISHING: [
encounter_categories: Dict[PokemonSource, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
PokemonSource.LAND: [(None, range(0, 12), None)],
PokemonSource.WATER: [(None, range(0, 5), None)],
PokemonSource.FISHING: [
("OLD_ROD", range(0, 2), lambda state: state.has("Old Rod", world.player)),
("GOOD_ROD", range(2, 5), lambda state: state.has("Good Rod", world.player)),
("SUPER_ROD", range(5, 10), lambda state: state.has("Super Rod", world.player)),
@@ -43,7 +43,7 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
# For each of land, water, and fishing, connect the region if indicated by include_slots
for i, (encounter_type, subcategories) in enumerate(encounter_categories.items()):
if include_slots[i]:
region_name = f"{map_name}_{encounter_type.value}_ENCOUNTERS"
region_name = f"{map_name}_{encounter_type}_ENCOUNTERS"
# If the region hasn't been created yet, create it now
try:
@@ -56,7 +56,7 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
for subcategory in subcategories:
# Want to create locations per species, not per slot
# encounter_categories includes info on which slots belong to which subcategory
unique_species = []
unique_species: list[int] = []
for j, species_id in enumerate(encounter_slots):
if j in subcategory[1] and not species_id in unique_species:
unique_species.append(species_id)
@@ -76,11 +76,10 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
encounter_location.access_rule = subcategory[2]
# Fill the location with an event for catching that species
encounter_location.place_locked_item(PokemonEmeraldItem(
f"CATCH_{data.species[species_id].name}",
ItemClassification.progression_skip_balancing,
None,
world.player
encounter_location.place_locked_item(PokemonEmeraldObtainPokemonEventItem(
world.player,
encounter_type,
species_id,
))
encounter_region.locations.append(encounter_location)
+2 -2
View File
@@ -586,7 +586,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
entrance_rules["REGION_SKY_PILLAR_4F/MAIN -> REGION_SKY_PILLAR_4F/ABOVE_3F_TOP_CENTER"] = Has("Mach Bike")
# Route 134
location_rules["EVENT_UNDO_REGI_SEAL"] = HasAll("CATCH_SPECIES_WAILORD", "CATCH_SPECIES_RELICANTH")
location_rules["EVENT_UNDO_REGI_SEAL"] = HasAll("REGI_WALL_SPECIES_WAILORD", "REGI_WALL_SPECIES_RELICANTH")
entrance_rules["REGION_ROUTE134/MAIN -> REGION_UNDERWATER_ROUTE134/MAIN"] = hm_rules["HM08 Dive"]
entrance_rules["REGION_SEALED_CHAMBER_OUTER_ROOM/MAIN -> REGION_SEALED_CHAMBER_OUTER_ROOM/CRUMBLED_WALL"] = Has("EVENT_MOVE_TUTOR_DIG")
@@ -637,7 +637,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
if species.species_id in world.blacklisted_wilds or species.species_id not in world.allowed_dexsanity_species:
continue
location_rules[f"Pokedex - {species.label}"] = Has(f"CATCH_{species.name}")
location_rules[f"Pokedex - {species.label}"] = Has(f"DEXSANITY_{species.name}")
# Legendary hunt prevents Latios from being a wild spawn so the roamer
# can be tracked, and also guarantees that the roamer is a Latios.
+6 -6
View File
@@ -1,7 +1,7 @@
import orjson
from typing import Any, Dict, List, Optional, Tuple, Iterable
from .data import NATIONAL_ID_TO_SPECIES_ID, EncounterType, data
from .data import NATIONAL_ID_TO_SPECIES_ID, PokemonSource, data
CHARACTER_DECODING_MAP = {
@@ -86,8 +86,8 @@ def decode_string(string_data: Iterable[int]) -> str:
return string
def get_encounter_type_label(encounter_type: EncounterType, slot: int) -> str:
if encounter_type == EncounterType.FISHING:
def get_encounter_type_label(encounter_type: PokemonSource, slot: int) -> str:
if encounter_type == PokemonSource.FISHING:
return {
0: "Old Rod",
1: "Old Rod",
@@ -102,9 +102,9 @@ def get_encounter_type_label(encounter_type: EncounterType, slot: int) -> str:
}[slot]
return {
EncounterType.LAND: 'Land',
EncounterType.WATER: 'Water',
EncounterType.ROCK_SMASH: 'Rock Smash',
PokemonSource.LAND: 'Land',
PokemonSource.WATER: 'Water',
PokemonSource.ROCK_SMASH: 'Rock Smash',
}[encounter_type]