Pokemon Emerald: v2 Update (#2918)

This commit is contained in:
Bryce Wilson 2024-03-14 05:37:10 -06:00 committed by GitHub
parent 3e3965272d
commit fa233b2583
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 14212 additions and 3240 deletions

View File

@ -0,0 +1,186 @@
# 2.0.0
### Features
- Picking up items for other players will display the actual item name and receiving player in-game instead of
"ARCHIPELAGO ITEM". (This does have a limit, but you're unlikely to reach it in all but the largest multiworlds.)
- New goal `legendary_hunt`. Your goal is to catch/defeat some number of legendary encounters. That is, the static
encounters themselves, whatever species they may be. Legendary species found in the wild don't count.
- You can force the goal to require captures with `legendary_hunt_catch`. If you accidentally faint a legendary, you
can respawn it by beating the champion.
- The number of legendaries needed is controlled by the `legendary_hunt_count` option.
- The caves containing Kyogre and Groudon are fixed to one location per seed. You need to go to the weather
institute to trigger a permanent weather event at the corresponding locations. Only one weather event can be active
at a time.
- The move tutor for the move Sleep Talk has been changed to Dig and is unlimited use (for Sealed Chamber).
- Relicanth and Wailord are guaranteed to be reachable in the wild (for Sealed Chamber). Interacting with the Sealed
Chamber wall will give you dex info for Wailord and Relicanth.
- Event legendaries are included for this goal (see below for new ferry behavior and event tickets).
- The roamer is included in this count. It will _always_ be Latios no matter what your options are. Otherwise you
might not have any way of knowing which species is roaming to be able to track it. In legendary hunt, Latios will
never appear as a wild pokemon to make tracking it easier. The television broadcast that creates the roamer will
give you dex info for Latios.
- You can set which encounters are considered for this goal with the `allowed_legendary_hunt_encounters` option.
- New option `dexsanity`. Adds pokedex entries as locations.
- Added locations contribute either a Poke Ball, Great Ball, or Ultra Ball to the item pool, based on the evolution
stage.
- Logic uses only wild encounters for now.
- Defeating a gym leader awards "seen" info on 1/8th of the pokedex.
- New option `trainersanity`. Defeating a trainer awards a random item.
- Trainers no longer award money upon defeat. Instead they add a sellable item to the item pool.
- Missable trainers are prevented from disappearing when this is enabled.
- Gym trainers remain active after their leader is defeated.
- Does not include trainers in the Trick House.
- New option `berry_trees`. Adds berry trees as locations.
- All soil patches start with a fully grown berry tree that gives one item.
- There are 88 berry trees.
- Berries cannot be planted in soil with this option enabled.
- Soil that doesn't start with a tree on a fresh save contributes a Sitrus Berry to the item pool.
- New option `death_link`. Forgive me, Figment.
- Added Artisan Cave locations
- Requires Wailmer Pail and the ability to Surf to access.
- Added Trick House locations. The Trick Master is finally here!
- He will make new layouts only if you have the corresponding badge (or beat the game) and have completed the
previous layout (all vanilla behavior).
- If you neglect to pick up an item in a puzzle before completing it, the Trick Master will give the item to you
alongside the prize.
- Locations are enabled or disabled with their broader categories (npc gifts, overworld items, etc...)
- Added daily berry gift locations. There are a half dozen or so NPCs that give you one or two berries per day.
- All these locations are considered NPC gifts.
- The NPCs have been reworked to give this gift once permanently so they can be added as locations.
- New option `remote_items`. All randomized items are sent from the server instead of being patched into your game
(except for start inventory, which remains in the PC)
- As a side effect, when you pick up your own item, there will be a gap between the item disappearing from the
overworld and your game actually receiving it. It also causes gifts from NPCs which contain your own items to not
show up until after their text box closes. It can feel odd, but there should be no danger to it.
- If the seed is in race mode, this is forcibly enabled.
- Benefits include:
- Two players can play the same slot and both receive items that slot picks up for itself (as long as it was
randomized)
- You receive items you picked up for yourself if you lose progress on your save
- Competitive integrity; the patch file no longer has any knowledge of item placement
- New option `match_trainer_levels`. This is a sort of pseudo level cap for a randomizer context.
- When you start a trainer fight, all your pokemon have their levels temporarily set to the highest level in the
opponent's party.
- During the battle, all earned exp is set to 0 (EVs are still gained during battle as normal). When the outcome of
the battle is decided, your pokemon have their levels reset to what they were before the fight and exp is awarded as
it would have been without this option. Think of it as holding earned exp in reserve and awarding it at the end
instead, even giving it to fainted pokemon if they earned any before fainting.
- Exp gain is based on _your_ party's average level to moderate exp over the course of a seed. Wild battles are
entirely unchanged by this option.
- New option `match_trainer_levels_bonus`. A flat bonus to apply to your party's levels when using
`match_trainer_levels`. In case you want to give yourself a nerf or buff while still approximately matching your
opponent.
- New option `force_fully_evolved`. Define a level at which trainers will stop using pokemon that have further evolution
stages.
- New option `move_blacklist`. Define a list of moves that should not be given randomly to learnsets or TMs. Move names
are accurate to Gen 3 except for capitalization.
- New option `extra_bumpy_slope`. Adds a "bumpy slope" to Route 115 that lets you hop up the ledge with the Acro Bike.
- New option `modify_118`. Changes Route 118 so that it must be crossed with the Acro Bike, and cannot be crossed by
surfing.
- Changed `require_flash` option to a choice between none, only granite cave, only victory road, or both caves.
- Removed `static_encounters` option.
- New option `legendary_encounters`. Replaces `static_encounters`, but only concerns legendaries.
- New option `misc_pokemon`. Replaces `static_encounters`, but only concerns non-legendaries.
- Removed `fly_without_badge` option. (Don't worry)
- New option `hm_requirements`. Will eventually be able to give you more control over the badge requirements for all
HMs. For now, only includes the presets `vanilla` and `fly_without_badge`.
- Removed `allow_wild_legendaries`, `allow_starter_legendaries`, and `allow_trainer_legendaries` options.
- New options `wild_encounter_blacklist`, `starter_blacklist`, and `trainer_party_blacklist`.
- These take lists of species and prevent them from randomizing into the corresponding categories
- If adhering to your blacklist would make it impossible to choose a random species, your blacklist is ignored in
that case
- All three include a shorthand for excluding legendaries
- Removed `enable_ferry` option.
- The ferry is now always present.
- The S.S. Ticket item/location is now part of `key_items`.
- Added event tickets and islands.
- All event tickets are given to the player by Norman after defeating the Champion alongside the S.S. Ticket.
- As in vanilla, these tickets are only usable from Lilycove. Not Slateport or the Battle Frontier.
- New option `event_tickets`. Randomizes the above-mentioned tickets into the item pool.
- New option `enable_wonder_trading`. You can participate in Wonder Trading by interacting with the center receptionist
on the second floor of Pokemon Centers.
- Why is this an option instead of just being enabled? You might want to disable wonder trading in a meta yaml to
make sure certain rules can't be broken. Or you may want to turn it off for yourself to definitively prevent being
asked for help if you prefer to keep certain walls up between your game and others. Trades _do_ include items and
known moves, which means there is potential for an extra level of cooperation and even ways to go out of logic. But
that's not a boundary everyone wants broken down all the time. Please be respectful of someone's choice to not
participate if that's their preference.
- A lot of time was spent trying to make this all work without having to touch your client. Hopefully it goes
smoothly, but there's room for jank. Anything you decide to give to her you should consider gone forever, whether
because it was traded away or because something "went wrong in transit" and the pokemon's data got lost after being
removed from the server.
- Wonder Trading is _not_ resistant to save scumming in either direction. You _could_ abuse it to dupe pokemon,
because there's not realistically a way for me to prevent it, but I'd urge you to stick to the spirit of the design
unless everyone involved doesn't mind.
- The wonder trades you receive are stored in your save data even before you pick them up, so if you save after the
client tells you that you received a wonder trade, it's safe. You don't need to retrieve it from a poke center for
it to persist. However, if you reset your game to a point in time before your client popped the "Wonder trade
received" message, that pokemon is lost forever.
- New `easter_egg` passphrase system.
- All valid easter egg passphrases will be a phrase that it's possible to submit as a trendy phrase in Dewford Town.
Changing the trendy phrase does ***not*** trigger easter eggs. Only the phrase you put in your YAML can trigger an
easter egg.
- There may be other ways to learn more information.
- Phrases are case insensitive. Here are a couple examples of possible phrases: `"GET FOE"`,
`"HERE GOES GRANDMOTHER"`, `"late eh?"` (None of those do anything, but I'd love to hear what you think they would.)
- Added three new easter egg effects.
- Changed the original easter egg phrase to use the new system.
- Renamed `tm_moves` to `tm_tutor_moves`. Move tutors are also affected by this option (except the new Dig tutor).
- Renamed `tm_compatibility` to `tm_tutor_compatibility`. Move tutors are also affected by this option.
- Changed `tm_tutor_compatibility` to be a percent chance instead of a choice. Use `-1` for vanilla.
- Changed `hm_compatibility` to be a percent chance instead of a choice. Use `-1` for vanilla.
- New option `music`. Shuffles all looping music. Includes FRLG tracks and possibly some unused stuff.
- New option `fanfares`. Shuffles all fanfares. Includes FRLG tracks. When this is enabled, pressing B will interrupt
most fanfares.
- New option `purge_spinners`. Trainers that change which direction they face will do so predictably, and will no longer
turn to face you when you run.
- New option `normalize_encounter_rates`. Sets every encounter slot to (almost) equal probability. Does NOT make every
species equally likely to appear, but makes rare encounters less rare.
- Added `Trick House` location group.
- Removed `Postgame Locations` location group.
### QoL
- Can teach moves over HM moves.
- Fishing is much less random; pokemon will always bite if there's an encounter there.
- Mirage Island is now always present.
- Waking Rayquaza is no longer required. After releasing Kyogre, going to Sootopolis will immediately trigger the
Rayquaza cutscene.
- Renamed some locations to be more accurate.
- Most trainers will no longer ask to be registered in your Pokegear after battle. Also removed most step-based match
calls.
- Removed a ledge on Route 123. With careful routing, it's now possible to check every location without having to save
scum or go back around.
- Added "GO HOME" button on the start menu where "EXIT" used to be. Will teleport you to Littleroot.
- Some locations which are directly blocked by completing your goal are automatically excluded.
- For example, the S.S. Ticket and a Champion goal, or the Sludge Bomb TM and the Norman goal.
- Your particular options might still result in locations that can't be reached until after your goal. For example,
setting a Norman goal and setting your E4 requirement to 8 gyms means that post-Champion locations will not be
reachable before defeating Norman, but they are NOT excluded by this modification. That's one of the simpler
examples. It is extremely tedious to try to detect these sorts of situations, so I'll instead leave it to you to be
aware of your own options.
- Species in the pokedex are searchable by type even if you haven't caught that species yet
### Fixes
- Mt. Pyre summit state no longer changes when you finish the Sootopolis events, which would lock you out of one or two
locations.
- Whiting out under certain conditions no longer softlocks you by moving Mr. Briney to an inaccessible area.
- It's no longer possible to join a room using the wrong patch file, even if the slot names match.
- NPCs now stop moving while you're receiving an item.
- Creating a secret base no longer triggers sending the Secret Power TM location.
- Hopefully fix bug where receiving an item while walking over a trigger can skip that trigger (the Moving
Truck/Petalburg wrong warp)
## Easter Eggs
There are plenty among you who are capable of ~~cheating~~ finding information about the easter egg phrases by reading
source code, writing brute force scripts, and inspecting memory for clues and answers. By all means, go ahead, that can
be your version of this puzzle and I don't intend to stand in your way. **However**, I would ask that any information
you come up with by doing this, you keep entirely to yourself until the community as a whole has figured out what you
know. There was not previously a way to reasonably learn about or make guesses at the easter egg, but that has changed.
There are mechanisms by which solutions can be found or guessed over the course of multiple games by multiple people,
and I'd rather the fun not get spoiled immediately.
Once a solution has been found I'd _still_ prefer discussion about hints and effects remain behind spoiler tags just in
case there are people who want to do the hunt on their own. Thank you all, and good luck.

View File

@ -1,58 +1,3 @@
# Pokemon Emerald
Version 1.2.1
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.
Version 2.0.0

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,28 @@
from typing import TYPE_CHECKING, Dict, Set
import asyncio
import copy
import orjson
import random
import time
from typing import TYPE_CHECKING, Optional, Dict, Set, Tuple
import uuid
from NetUtils import ClientStatus
from Options import Toggle
import Utils
import worlds._bizhawk as bizhawk
from worlds._bizhawk.client import BizHawkClient
from .data import BASE_OFFSET, data
from .options import Goal
from .data import BASE_OFFSET, POKEDEX_OFFSET, data
from .options import Goal, RemoteItems
from .util import pokemon_data_to_json, json_to_pokemon_data
if TYPE_CHECKING:
from worlds._bizhawk.context import BizHawkClientContext
EXPECTED_ROM_NAME = "pokemon emerald version / AP 2"
EXPECTED_ROM_NAME = "pokemon emerald version / AP 5"
IS_CHAMPION_FLAG = data.constants["FLAG_IS_CHAMPION"]
DEFEATED_WALLACE_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_WALLACE"]
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"]
@ -31,7 +40,7 @@ TRACKER_EVENT_FLAGS = [
"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_HIDE_ROUTE_119_TEAM_AQUA_SHELLY", # 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
@ -41,19 +50,19 @@ TRACKER_EVENT_FLAGS = [
"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"
"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_HM_CUT",
"NPC_GIFT_RECEIVED_HM_FLY",
"NPC_GIFT_RECEIVED_HM_SURF",
"NPC_GIFT_RECEIVED_HM_STRENGTH",
"NPC_GIFT_RECEIVED_HM_FLASH",
"NPC_GIFT_RECEIVED_HM_ROCK_SMASH",
"NPC_GIFT_RECEIVED_HM_WATERFALL",
"NPC_GIFT_RECEIVED_HM_DIVE",
"NPC_GIFT_RECEIVED_ACRO_BIKE",
"NPC_GIFT_RECEIVED_WAILMER_PAIL",
"NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL",
@ -70,7 +79,7 @@ KEY_LOCATION_FLAGS = [
"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_HIDDEN_FLOOR_ROOM_2_SCANNER",
"ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY",
"NPC_GIFT_RECEIVED_OLD_ROD",
"NPC_GIFT_RECEIVED_GOOD_ROD",
@ -78,6 +87,24 @@ KEY_LOCATION_FLAGS = [
]
KEY_LOCATION_FLAG_MAP = {data.locations[location_name].flag: location_name for location_name in KEY_LOCATION_FLAGS}
LEGENDARY_NAMES = {
"Groudon": "GROUDON",
"Kyogre": "KYOGRE",
"Rayquaza": "RAYQUAZA",
"Latias": "LATIAS",
"Latios": "LATIOS",
"Regirock": "REGIROCK",
"Regice": "REGICE",
"Registeel": "REGISTEEL",
"Mew": "MEW",
"Deoxys": "DEOXYS",
"Ho-oh": "HO_OH",
"Lugia": "LUGIA",
}
DEFEATED_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_DEFEATED_{name}"]: name for name in LEGENDARY_NAMES.values()}
CAUGHT_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_CAUGHT_{name}"]: name for name in LEGENDARY_NAMES.values()}
class PokemonEmeraldClient(BizHawkClient):
game = "Pokemon Emerald"
@ -86,14 +113,31 @@ class PokemonEmeraldClient(BizHawkClient):
local_checked_locations: Set[int]
local_set_events: Dict[str, bool]
local_found_key_items: Dict[str, bool]
goal_flag: int
local_defeated_legendaries: Dict[str, bool]
goal_flag: Optional[int]
wonder_trade_update_event: asyncio.Event
latest_wonder_trade_reply: dict
wonder_trade_cooldown: int
wonder_trade_cooldown_timer: int
death_counter: Optional[int]
previous_death_link: float
ignore_next_death_link: bool
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
self.local_defeated_legendaries = {}
self.goal_flag = None
self.wonder_trade_update_event = asyncio.Event()
self.wonder_trade_cooldown = 5000
self.wonder_trade_cooldown_timer = 0
self.death_counter = None
self.previous_death_link = 0
self.ignore_next_death_link = False
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
from CommonClient import logger
@ -123,88 +167,103 @@ class PokemonEmeraldClient(BizHawkClient):
ctx.want_slot_data = True
ctx.watcher_timeout = 0.125
self.death_counter = None
self.previous_death_link = 0
self.ignore_next_death_link = False
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")
import base64
auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(data.rom_addresses["gArchipelagoInfo"], 16, "ROM")]))[0]
ctx.auth = base64.b64encode(auth_raw).decode("utf-8")
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
if ctx.slot_data is not None:
if ctx.server is None or ctx.server.socket.closed or ctx.slot_data is None:
return
if ctx.slot_data["goal"] == Goal.option_champion:
self.goal_flag = IS_CHAMPION_FLAG
self.goal_flag = DEFEATED_WALLACE_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
elif ctx.slot_data["goal"] == Goal.option_legendary_hunt:
self.goal_flag = None
if ctx.slot_data["remote_items"] == RemoteItems.option_true and not ctx.items_handling & 0b010:
ctx.items_handling = 0b011
Utils.async_start(ctx.send_msgs([{
"cmd": "ConnectUpdate",
"items_handling": ctx.items_handling
}]))
try:
guards: Dict[str, Tuple[int, bytes, str]] = {}
# 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]
guards["IN OVERWORLD"] = (
data.ram_addresses["gMain"] + 4,
(data.ram_addresses["CB2_Overworld"] + 1).to_bytes(4, "little"),
"System Bus"
)
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(
# Read save block addresses
read_result = await bizhawk.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]
(data.ram_addresses["gSaveBlock1Ptr"], 4, "System Bus"),
(data.ram_addresses["gSaveBlock2Ptr"], 4, "System Bus"),
]
)
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
# Checks that the save data hasn't moved
guards["SAVE BLOCK 1"] = (data.ram_addresses["gSaveBlock1Ptr"], read_result[0], "System Bus")
guards["SAVE BLOCK 2"] = (data.ram_addresses["gSaveBlock2Ptr"], read_result[1], "System Bus")
# 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"),
])
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
await self.handle_death_link(ctx, guards)
await self.handle_received_items(ctx, guards)
await self.handle_wonder_trade(ctx, guards)
# 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]
[(sb1_address + 0x1450, 0x96, "System Bus")], # Flags
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
)
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]
[(sb1_address + 0x14E6, 0x96, "System Bus")], # Flags continued
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
)
if read_result is not None:
flag_bytes += read_result[0]
# Read pokedex flags
pokedex_caught_bytes = bytes(0)
if ctx.slot_data["dexsanity"] == Toggle.option_true:
# Read pokedex flags
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx,
[(sb2_address + 0x28, 0x34, "System Bus")],
[guards["IN OVERWORLD"], guards["SAVE BLOCK 2"]]
)
if read_result is not None:
pokedex_caught_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}
defeated_legendaries = {legendary_name: False for legendary_name in LEGENDARY_NAMES.values()}
caught_legendaries = {legendary_name: False for legendary_name in LEGENDARY_NAMES.values()}
# Check set flags
for byte_i, byte in enumerate(flag_bytes):
@ -219,12 +278,45 @@ class PokemonEmeraldClient(BizHawkClient):
if flag_id == self.goal_flag:
game_clear = True
if flag_id in DEFEATED_LEGENDARY_FLAG_MAP:
defeated_legendaries[DEFEATED_LEGENDARY_FLAG_MAP[flag_id]] = True
if flag_id in CAUGHT_LEGENDARY_FLAG_MAP:
caught_legendaries[CAUGHT_LEGENDARY_FLAG_MAP[flag_id]] = 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
# Check pokedex
if ctx.slot_data["dexsanity"] == Toggle.option_true:
for byte_i, byte in enumerate(pokedex_caught_bytes):
for i in range(8):
if byte & (1 << i) != 0:
dex_number = (byte_i * 8 + i) + 1
location_id = dex_number + BASE_OFFSET + POKEDEX_OFFSET
if location_id in ctx.server_locations:
local_checked_locations.add(location_id)
# Count legendary hunt flags
if ctx.slot_data["goal"] == Goal.option_legendary_hunt:
# If legendary hunt doesn't require catching, add defeated legendaries to caught_legendaries
if ctx.slot_data["legendary_hunt_catch"] == Toggle.option_false:
for legendary, is_defeated in defeated_legendaries.items():
if is_defeated:
caught_legendaries[legendary] = True
num_caught = 0
for legendary, is_caught in caught_legendaries.items():
if is_caught and legendary in [LEGENDARY_NAMES[name] for name in ctx.slot_data["allowed_legendary_hunt_encounters"]]:
num_caught += 1
if num_caught >= ctx.slot_data["legendary_hunt_count"]:
game_clear = True
# Send locations
if local_checked_locations != self.local_checked_locations:
self.local_checked_locations = local_checked_locations
@ -232,14 +324,14 @@ class PokemonEmeraldClient(BizHawkClient):
if local_checked_locations is not None:
await ctx.send_msgs([{
"cmd": "LocationChecks",
"locations": list(local_checked_locations)
"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
"status": ClientStatus.CLIENT_GOAL,
}])
# Send tracker event flags
@ -254,7 +346,7 @@ class PokemonEmeraldClient(BizHawkClient):
"key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [{"operation": "or", "value": event_bitfield}]
"operations": [{"operation": "or", "value": event_bitfield}],
}])
self.local_set_events = local_set_events
@ -269,9 +361,313 @@ class PokemonEmeraldClient(BizHawkClient):
"key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [{"operation": "or", "value": key_bitfield}]
"operations": [{"operation": "or", "value": key_bitfield}],
}])
self.local_found_key_items = local_found_key_items
if ctx.slot_data["goal"] == Goal.option_legendary_hunt:
if caught_legendaries != self.local_defeated_legendaries and ctx.slot is not None:
legendary_bitfield = 0
for i, legendary_name in enumerate(LEGENDARY_NAMES.values()):
if caught_legendaries[legendary_name]:
legendary_bitfield |= 1 << i
await ctx.send_msgs([{
"cmd": "Set",
"key": f"pokemon_emerald_legendaries_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [{"operation": "or", "value": legendary_bitfield}],
}])
self.local_defeated_legendaries = caught_legendaries
except bizhawk.RequestFailedError:
# Exit handler and return to main loop to reconnect
pass
async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
"""
Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game
if a new one has been received.
"""
if ctx.slot_data.get("death_link", Toggle.option_false) == Toggle.option_true:
if "DeathLink" not in ctx.tags:
await ctx.update_death_link(True)
self.previous_death_link = ctx.last_death_link
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx, [
(sb1_address + 0x177C + (52 * 4), 4, "System Bus"), # White out stat
(sb1_address + 0x177C + (22 * 4), 4, "System Bus"), # Canary stat
(sb2_address + 0xAC, 4, "System Bus"), # Encryption key
],
[guards["SAVE BLOCK 1"], guards["SAVE BLOCK 2"]]
)
if read_result is None: # Save block moved
return
encryption_key = int.from_bytes(read_result[2], "little")
times_whited_out = int.from_bytes(read_result[0], "little") ^ encryption_key
# Canary is an unused stat that will always be 0. There is a low chance that we've done this read on
# a frame where the user has just entered a battle and the encryption key has been changed, but the data
# has not yet been encrypted with the new key. If `canary` is 0, `times_whited_out` is correct.
canary = int.from_bytes(read_result[1], "little") ^ encryption_key
# Skip all deathlink code if save is not yet loaded (encryption key is zero) or white out stat not yet
# initialized (starts at 100 as a safety for subtracting values from an unsigned int).
if canary == 0 and encryption_key != 0 and times_whited_out >= 100:
if self.previous_death_link != ctx.last_death_link:
self.previous_death_link = ctx.last_death_link
if self.ignore_next_death_link:
self.ignore_next_death_link = False
else:
await bizhawk.write(
ctx.bizhawk_ctx,
[(data.ram_addresses["gArchipelagoDeathLinkQueued"], [1], "System Bus")]
)
if self.death_counter is None:
self.death_counter = times_whited_out
elif times_whited_out > self.death_counter:
await ctx.send_death(f"{ctx.player_names[ctx.slot]} is out of usable POKéMON! "
f"{ctx.player_names[ctx.slot]} whited out!")
self.ignore_next_death_link = True
self.death_counter = times_whited_out
async def handle_received_items(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
"""
Checks the index of the most recently received item and whether the item queue is full. Writes the next item
into the game if necessary.
"""
received_item_address = data.ram_addresses["gArchipelagoReceivedItem"]
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx,
[
(sb1_address + 0x3778, 2, "System Bus"), # Number of received items
(received_item_address + 4, 1, "System Bus") # Received item struct full?
],
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
)
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]
should_display = 1 if next_item.flags & 1 or next_item.player == ctx.slot else 0
await bizhawk.write(ctx.bizhawk_ctx, [
(received_item_address + 0, (next_item.item - BASE_OFFSET).to_bytes(2, "little"), "System Bus"),
(received_item_address + 2, (num_received_items + 1).to_bytes(2, "little"), "System Bus"),
(received_item_address + 4, [1], "System Bus"),
(received_item_address + 5, [should_display], "System Bus"),
])
async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
"""
Read wonder trade status from save data and either send a queued pokemon to data storage or attempt to retrieve
one from data storage and write it into the save.
"""
from CommonClient import logger
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
read_result = await bizhawk.guarded_read(
ctx.bizhawk_ctx,
[
(sb1_address + 0x377C, 0x50, "System Bus"), # Wonder trade data
(sb1_address + 0x37CC, 1, "System Bus"), # Is wonder trade sent
],
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
)
if read_result is not None:
wonder_trade_pokemon_data = read_result[0]
trade_is_sent = read_result[1][0]
if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2:
# Game has wonder trade data to send. Send it to data storage, remove it from the game's memory,
# and mark that the game is waiting on receiving a trade
Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
await bizhawk.write(ctx.bizhawk_ctx, [
(sb1_address + 0x377C, bytes(0x50), "System Bus"),
(sb1_address + 0x37CC, [1], "System Bus"),
])
elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2:
# Game is waiting on receiving a trade. See if there are any available trades that were not
# sent by this player, and if so, try to receive one.
if self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data:
if any(item[0] != ctx.slot
for key, item in ctx.stored_data.get(f"pokemon_wonder_trades_{ctx.team}", {}).items()
if key != "_lock" and orjson.loads(item[1])["species"] <= 386):
received_trade = await self.wonder_trade_receive(ctx)
if received_trade is None:
self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown
self.wonder_trade_cooldown *= 2
self.wonder_trade_cooldown += random.randrange(0, 500)
else:
await bizhawk.write(ctx.bizhawk_ctx, [
(sb1_address + 0x377C, json_to_pokemon_data(received_trade), "System Bus"),
])
logger.info("Wonder trade received!")
self.wonder_trade_cooldown = 5000
else:
# Very approximate "time since last loop", but extra delay is fine for this
self.wonder_trade_cooldown_timer -= int(ctx.watcher_timeout * 1000)
async def wonder_trade_acquire(self, ctx: "BizHawkClientContext", keep_trying: bool = False) -> Optional[dict]:
"""
Acquires a lock on the `pokemon_wonder_trades_{ctx.team}` key in
datastorage. Locking the key means you have exclusive access
to modifying the value until you unlock it or the key expires (5
seconds).
If `keep_trying` is `True`, it will keep trying to acquire the lock
until successful. Otherwise it will return `None` if it fails to
acquire the lock.
"""
while not ctx.exit_event.is_set():
lock = int(time.time_ns() / 1000000)
message_uuid = str(uuid.uuid4())
await ctx.send_msgs([{
"cmd": "Set",
"key": f"pokemon_wonder_trades_{ctx.team}",
"default": {"_lock": 0},
"want_reply": True,
"operations": [{"operation": "update", "value": {"_lock": lock}}],
"uuid": message_uuid,
}])
self.wonder_trade_update_event.clear()
try:
await asyncio.wait_for(self.wonder_trade_update_event.wait(), 5)
except asyncio.TimeoutError:
if not keep_trying:
return None
continue
reply = copy.deepcopy(self.latest_wonder_trade_reply)
# Make sure the most recently received update was triggered by our lock attempt
if reply.get("uuid", None) != message_uuid:
if not keep_trying:
return None
await asyncio.sleep(self.wonder_trade_cooldown)
continue
# Make sure the current value of the lock is what we set it to
# (I think this should theoretically never run)
if reply["value"]["_lock"] != lock:
if not keep_trying:
return None
await asyncio.sleep(self.wonder_trade_cooldown)
continue
# Make sure that the lock value we replaced is at least 5 seconds old
# If it was unlocked before our change, its value was 0 and it will look decades old
if lock - reply["original_value"]["_lock"] < 5000:
# Multiple clients trying to lock the key may get stuck in a loop of checking the lock
# by trying to set it, which will extend its expiration. So if we see that the lock was
# too new when we replaced it, we should wait for increasingly longer periods so that
# eventually the lock will expire and a client will acquire it.
self.wonder_trade_cooldown *= 2
self.wonder_trade_cooldown += random.randrange(0, 500)
if not keep_trying:
self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown
return None
await asyncio.sleep(self.wonder_trade_cooldown)
continue
# We have the lock, reset the cooldown and return
self.wonder_trade_cooldown = 5000
return reply
async def wonder_trade_send(self, ctx: "BizHawkClientContext", data: str) -> None:
"""
Sends a wonder trade pokemon to data storage
"""
from CommonClient import logger
reply = await self.wonder_trade_acquire(ctx, True)
wonder_trade_slot = 0
while str(wonder_trade_slot) in reply["value"]:
wonder_trade_slot += 1
await ctx.send_msgs([{
"cmd": "Set",
"key": f"pokemon_wonder_trades_{ctx.team}",
"default": {"_lock": 0},
"operations": [{"operation": "update", "value": {
"_lock": 0,
str(wonder_trade_slot): (ctx.slot, data),
}}],
}])
logger.info("Wonder trade sent! We'll notify you here when a trade has been found.")
async def wonder_trade_receive(self, ctx: "BizHawkClientContext") -> Optional[str]:
"""
Tries to pop a pokemon out of the wonder trades. Returns `None` if
for some reason it can't immediately remove a compatible pokemon.
"""
reply = await self.wonder_trade_acquire(ctx)
if reply is None:
return None
candidate_slots = [
int(slot)
for slot in reply["value"]
if slot != "_lock" \
and reply["value"][slot][0] != ctx.slot \
and orjson.loads(reply["value"][slot][1])["species"] <= 386
]
if len(candidate_slots) == 0:
await ctx.send_msgs([{
"cmd": "Set",
"key": f"pokemon_wonder_trades_{ctx.team}",
"default": {"_lock": 0},
"operations": [{"operation": "update", "value": {"_lock": 0}}],
}])
return None
wonder_trade_slot = max(candidate_slots)
await ctx.send_msgs([{
"cmd": "Set",
"key": f"pokemon_wonder_trades_{ctx.team}",
"default": {"_lock": 0},
"operations": [
{"operation": "update", "value": {"_lock": 0}},
{"operation": "pop", "value": str(wonder_trade_slot)},
]
}])
return reply["value"][str(wonder_trade_slot)][1]
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
if cmd == "Connected":
Utils.async_start(ctx.send_msgs([{
"cmd": "SetNotify",
"keys": [f"pokemon_wonder_trades_{ctx.team}"],
}, {
"cmd": "Get",
"keys": [f"pokemon_wonder_trades_{ctx.team}"],
}]))
elif cmd == "SetReply":
if args.get("key", "") == f"pokemon_wonder_trades_{ctx.team}":
self.latest_wonder_trade_reply = args
self.wonder_trade_update_event.set()

File diff suppressed because it is too large Load Diff

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

View File

@ -0,0 +1,458 @@
{
"REGION_BATTLE_FRONTIER_RECEPTION_GATE/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_RECEPTION_GATE",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [
"REGION_SS_TIDAL_CORRIDOR/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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_WEST:1"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
"warps": [
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY:0,1/MAP_BATTLE_FRONTIER_OUTSIDE_EAST:2"
]
},
"REGION_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": true,
"has_water": false,
"has_fishing": false,
"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",
"has_grass": true,
"has_water": false,
"has_fishing": false,
"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"
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,27 @@
{
"REGION_SOUTHERN_ISLAND_EXTERIOR/MAIN": {
"parent_map": "MAP_SOUTHERN_ISLAND_EXTERIOR",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
"exits": [
"REGION_LILYCOVE_CITY_HARBOR/MAIN"
],
"warps": [
"MAP_SOUTHERN_ISLAND_EXTERIOR:0,1/MAP_SOUTHERN_ISLAND_INTERIOR:0,1"
]
},
"REGION_SOUTHERN_ISLAND_INTERIOR/MAIN": {
"parent_map": "MAP_SOUTHERN_ISLAND_INTERIOR",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"events": [
"EVENT_ENCOUNTER_LATIAS"
],
"exits": [],
"warps": [
"MAP_SOUTHERN_ISLAND_INTERIOR:0,1/MAP_SOUTHERN_ISLAND_EXTERIOR:0,1"
@ -19,17 +29,27 @@
},
"REGION_FARAWAY_ISLAND_ENTRANCE/MAIN": {
"parent_map": "MAP_FARAWAY_ISLAND_ENTRANCE",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
"exits": [
"REGION_LILYCOVE_CITY_HARBOR/MAIN"
],
"warps": [
"MAP_FARAWAY_ISLAND_ENTRANCE:0,1/MAP_FARAWAY_ISLAND_INTERIOR:0,1"
]
},
"REGION_FARAWAY_ISLAND_INTERIOR/MAIN": {
"parent_map": "MAP_FARAWAY_ISLAND_INTERIOR",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"events": [
"EVENT_ENCOUNTER_MEW"
],
"exits": [],
"warps": [
"MAP_FARAWAY_ISLAND_INTERIOR:0,1/MAP_FARAWAY_ISLAND_ENTRANCE:0,1"
@ -37,17 +57,27 @@
},
"REGION_BIRTH_ISLAND_HARBOR/MAIN": {
"parent_map": "MAP_BIRTH_ISLAND_HARBOR",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
"exits": [
"REGION_LILYCOVE_CITY_HARBOR/MAIN"
],
"warps": [
"MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0"
]
},
"REGION_BIRTH_ISLAND_EXTERIOR/MAIN": {
"parent_map": "MAP_BIRTH_ISLAND_EXTERIOR",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"events": [
"EVENT_ENCOUNTER_DEOXYS"
],
"exits": [],
"warps": [
"MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0"
@ -55,15 +85,23 @@
},
"REGION_NAVEL_ROCK_HARBOR/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_HARBOR",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
"exits": [
"REGION_LILYCOVE_CITY_HARBOR/MAIN"
],
"warps": [
"MAP_NAVEL_ROCK_HARBOR:0/MAP_NAVEL_ROCK_EXTERIOR:0"
]
},
"REGION_NAVEL_ROCK_EXTERIOR/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_EXTERIOR",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -74,6 +112,9 @@
},
"REGION_NAVEL_ROCK_ENTRANCE/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_ENTRANCE",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -84,6 +125,9 @@
},
"REGION_NAVEL_ROCK_B1F/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_B1F",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -94,6 +138,9 @@
},
"REGION_NAVEL_ROCK_FORK/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_FORK",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -105,6 +152,9 @@
},
"REGION_NAVEL_ROCK_DOWN01/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN01",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -115,6 +165,9 @@
},
"REGION_NAVEL_ROCK_DOWN02/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN02",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -125,6 +178,9 @@
},
"REGION_NAVEL_ROCK_DOWN03/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN03",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -135,6 +191,9 @@
},
"REGION_NAVEL_ROCK_DOWN04/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN04",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -145,6 +204,9 @@
},
"REGION_NAVEL_ROCK_DOWN05/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN05",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -155,6 +217,9 @@
},
"REGION_NAVEL_ROCK_DOWN06/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN06",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -165,6 +230,9 @@
},
"REGION_NAVEL_ROCK_DOWN07/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN07",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -175,6 +243,9 @@
},
"REGION_NAVEL_ROCK_DOWN08/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN08",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -185,6 +256,9 @@
},
"REGION_NAVEL_ROCK_DOWN09/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN09",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -195,6 +269,9 @@
},
"REGION_NAVEL_ROCK_DOWN10/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN10",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -205,6 +282,9 @@
},
"REGION_NAVEL_ROCK_DOWN11/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_DOWN11",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -215,8 +295,13 @@
},
"REGION_NAVEL_ROCK_BOTTOM/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_BOTTOM",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"events": [
"EVENT_ENCOUNTER_LUGIA"
],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_BOTTOM:0/MAP_NAVEL_ROCK_DOWN11:0"
@ -224,6 +309,9 @@
},
"REGION_NAVEL_ROCK_UP1/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_UP1",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -234,6 +322,9 @@
},
"REGION_NAVEL_ROCK_UP2/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_UP2",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -244,6 +335,9 @@
},
"REGION_NAVEL_ROCK_UP3/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_UP3",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -254,6 +348,9 @@
},
"REGION_NAVEL_ROCK_UP4/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_UP4",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -264,10 +361,15 @@
},
"REGION_NAVEL_ROCK_TOP/MAIN": {
"parent_map": "MAP_NAVEL_ROCK_TOP",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [
"HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH"
],
"events": [],
"events": [
"EVENT_ENCOUNTER_HO_OH"
],
"exits": [],
"warps": [
"MAP_NAVEL_ROCK_TOP:0/MAP_NAVEL_ROCK_UP4:1"

File diff suppressed because it is too large Load Diff

View File

@ -1,168 +1,9 @@
{
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -172,6 +13,9 @@
},
"REGION_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -179,18 +23,11 @@
"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",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"locations": [],
"events": [],
"exits": [],
@ -202,195 +39,14 @@
},
"REGION_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM/MAIN": {
"parent_map": "MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM",
"has_grass": false,
"has_water": false,
"has_fishing": false,
"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

@ -1,52 +1,14 @@
{
"REGION_TERRA_CAVE_ENTRANCE/MAIN": {
"parent_map": "MAP_TERRA_CAVE_ENTRANCE",
"REGION_ALTERING_CAVE/MAIN": {
"parent_map": "MAP_ALTERING_CAVE",
"has_grass": true,
"has_water": false,
"has_fishing": false,
"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"
"MAP_ALTERING_CAVE:0/MAP_ROUTE103:0"
]
}
}

View File

@ -1,82 +0,0 @@
{
"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,162 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Pokemon",
"type": "object",
"required": ["version", "language", "nickname", "personality", "species", "experience", "ivs", "evs", "moves", "trainer"],
"properties": {
"version": {
"description": "The version of this schema that the data is formatted to match",
"type": "string",
"const": "1"
},
"language": {
"description": "The language of origin",
"enum": [
"Japanese",
"English",
"French",
"Italian",
"German",
"Spanish"
]
},
"nickname": {
"description": "The pokemon's nickname",
"type": "string",
"minLength": 1
},
"personality": {
"description": "The pokemon's 32-bit personality value",
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"species": {
"description": "The national dex number of the pokemon species",
"type": "integer",
"minimum": 0
},
"item": {
"description": "The id of the item the pokemon is holding according to modern tables",
"type": "integer"
},
"experience": {
"description": "The current total EXP",
"type": "integer",
"minimum": 0
},
"ability": {
"description": "The value of the ability bit (hidden abilities should be a separate bit)",
"type": "integer",
"minimum": 0,
"maximum": 1
},
"ivs": {
"description": "The 6 IVs of the pokemon",
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 31
},
"minItems": 6,
"maxItems": 6
},
"evs": {
"description": "The 6 EVs of the pokemon",
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 6,
"maxItems": 6
},
"conditions": {
"description": "The 6 condition (contest) stats of the pokemon",
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 6,
"maxItems": 6
},
"pokerus": {
"description": "The value of the pokerus status byte",
"type": "integer",
"minimum": 0
},
"game": {
"description": "The id of the game the pokemon originated in",
"type": "integer",
"minimum": 0
},
"location_met": {
"description": "The location id for where the pokemon was met",
"type": "integer",
"minimum": 0
},
"level_met": {
"description": "The level the pokemon was met at",
"type": "integer",
"minimum": 0,
"maximum": 100
},
"ball": {
"description": "The type of poke ball the pokemon was caught in",
"type": "integer",
"minimum": 1
},
"moves": {
"description": "The move id, PP, and PP Ups used for each move slot",
"type": "array",
"items": {
"type": "array",
"prefixItems": [
{
"description": "The move's id according to modern tables (use 0 for an empty slot)",
"type": "integer"
},
{
"description": "The move's max PP",
"type": "integer",
"minimum": 1
},
{
"description": "The number of times a PP Up has been used on this move",
"type": "integer",
"minimum": 0,
"maximum": 3
}
],
"minLength": 4,
"maxLength": 4
}
},
"trainer": {
"description": "Original trainer info",
"type": "object",
"properties": {
"name": {
"description": "The original trainer's name",
"type": "string",
"minLength": 1
},
"id": {
"description": "The original trainer's 32-bit ID (includes secret id as higher order bytes)",
"type": "integer",
"minimum": 0,
"maximum": 4294967295
},
"female": {
"description": "Whether the original trainer is female",
"type": "boolean"
}
},
"required": ["name", "id"]
}
}
}

View File

@ -38,8 +38,8 @@ 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 you find an item that is not your own, you will see the item's name and its owner while the item received jingle
plays.
## When the player receives an item, what happens?
@ -55,9 +55,9 @@ you're playing a multiworld game, the client will sync your game with the server
## 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.
Unfortunately, no. We don't want to force new mechanics on players who would prefer to play with the classic mechanics,
but updating would require such drastic changes to the underlying code that it would be unreasonable to toggle between
them.
## Is this randomizer compatible with other mods?
@ -68,11 +68,21 @@ 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
No, tools like UPR 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.
### There are two possible exceptions
#### 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. Box and party pokémon are the only aspects of your save file likely to work.
#### PokéFinder/RNG Reporter
In the spirit of randomization, Emerald's broken RNG is fixed in Archipelago. More specifically, it's reverted to work
as it did in Ruby/Sapphire. So while you can't make the assumption that the RNG is seeded at 0, you can set the battery
to dry, which will seed it in the same way that Ruby/Sapphire are seeded when the battery is dry.

View File

@ -1,8 +1,7 @@
## `regions/`
## Region Data
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:
Regions, connections, and associated locations are defined in `data/regions`. If you know what you're doing, it should
be pretty clear how the data works by taking a quick look through the files. But the quick 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)
@ -12,7 +11,7 @@ to say you shouldn't read it, but the tl;dr is:
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
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.
@ -25,9 +24,9 @@ example is demonstrative). Keeping the name consistent with the `Map` name and a
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:
and are only split and ordered for organization. Regions defined in `data/regions/unused` are remnants from
automatically generated regions and represent places that exist but aren't reachable or aren't currently relevant to the
randomizer. Any locations or warps in there should be ignored. Data for a single region looks like this:
```json
"REGION_ROUTE103/EAST": {
@ -60,9 +59,9 @@ can trigger story progression and unblock roads and buildings. Events are define
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. 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).
there to be referenced here. More on warps in [../docs/warps.md](../docs/warps.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
@ -78,22 +77,3 @@ especially remember to rename incoming `exits` defined in other regions which ar
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.

View File

@ -1,7 +1,20 @@
## New Behaviors
- The union room receptionist on the second floor of Pokemon Centers was reworked for wonder trading via Archipelago
- Norman will give you all event ticket items when he gives you the S.S. Ticket
- Use of event tickets is streamlined and the scripts are refactored to skip "first time use" stuff
- The roaming pokemon is forced to Latios
- The pokemon at Southern Island is forced to Latias
- There is new code for changing your party's levels during trainer battles which also modifies exp gain
## QoL
- The menu has a GO HOME option instead of EXIT, which will immediately teleport you to Birch's Lab
- It is possible to teach over HM moves
- The catch tutorial and cutscenes during your first visit to Petalburg are skipped
- The match call tutorial after you leave Devon Corp is skipped
- Random match calls in general are skipped, and trainers no longer ask to register you after a battle
- Searching by type in the pokedex includes species you have seen but not yet caught
- Cycling and running is allowed in every map (some exceptions like Fortree and Pacifidlog)
- When you run out of Repel steps, you'll be prompted to use another one if you have more in your bag
- Text is always rendered in its entirety on the first frame (instant text)
@ -12,6 +25,9 @@ you can still read the species when deciding whether to change pokemon
- When receiving TMs and HMs, the move that it teaches is consistently displayed in the "received item" message (by
default, certain ways of receiving items would only display the TM/HM number)
- The Pokedex starts in national mode
- The fishing minigame is always successful at finding a catch, only requires one round, and will always show four dots
- With an option in Archipelago, spinning trainers become predictable
- Removed a ledge on Route 123 which allows you to collect every item without backtracking
- The Oldale Pokemart sells Poke Balls at the start of the game
- Pauses during battles (e.g. the ~1 second pause at the start of a turn before an opponent uses a potion) are shorter
by 62.5%
@ -29,6 +45,10 @@ predetermined priority
- Shoal cave changes state every time you reload the map and is no longer tied to the RTC
- Increased safari zone steps from 500 to 50000
- Trainers will not approach the player if the blind trainers option is set
- Defeating the elite 4 respawns all legendary encounters where the encounter ended by fainting the pokemon
- The cutscene revealing the existence of Latios also gives you dex info for having seen Latios
- The braille wall hinting at the solution to the Wailord/Relicanth puzzle gives you dex info for having seen Wailord
and Relicanth
- Changed trade evolutions to be possible without trading:
- Politoed: Use King's Rock in bag menu
- Alakazam: Level 37
@ -47,11 +67,23 @@ predetermined priority
## Game State Changes/Softlock Prevention
- Mr. Briney never disappears or stops letting you use his ferry
- Upon releasing Kyogre, Sootopolis and Sky Pillar will be advanced to after Rayquaza has been awakened, skipping the
Wallace and Rayquaza fetch quest
- Prevent the player from flying or surfing until they have received the Pokedex
- The S.S. Tidal will be available at all times if you have the option enabled
- The S.S. Tidal will be available at all times
- All time-based berry gifts are locked to a one-time gift of a specific berry
- Terra and Marine Cave are given fixed locations, and the weather events revealing them are permanent until the
legendary encounter is resolved
- Mirage Island is always present
- During dexsanity, certain trainers don't disappear/deactivate
- During berry randomization, it is impossible to plant berries or for berry trees to change state
- Some NPCs or tiles are removed on the creation of a new save file based on player options
- Ensured that every species has some damaging move by level 5
- Route 115 may have strength boulders between the beach and cave entrance based on player options
- Route 115 has an alternate layout (must be enabled through Archipelago) which includes a bumpy slope that can cross
the ledge normally blocking you from entering Meteor Falls from Rustboro City
- Route 115 may have strength boulders (must be enabled through Archipelago) between the beach and cave entrance
- Route 118 has an alternate layout (must be enabled through Archipelago) that blocks you from surfing between shores
and adds a rail so that it can be crossed using the Acro Bike
- The Petalburg Gym is set up based on your player options rather than after the first 4 gyms
- The E4 guards will actually check all your badges (or gyms beaten based on your options) instead of just the Feather
Badge

View File

@ -0,0 +1,50 @@
## 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 does not 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,103 @@
# Wonder Trades
Pokemon Emerald uses Archipelago's data storage to reproduce what the Pokemon series calls wonder trading. Wonder
trading is meant as a sort of gacha game surprise trade where you give up one of your pokemon and at some point in the
future you'll receive one in return from another player who decided to participate. In practice, small groups will be
able to use it as a means of simple trading as well by coordinating when they participate.
The goal of the implementation used by Pokemon Emerald is to allow players to interact with an NPC in-game to deposit
and withdraw pokemon without having to touch their client. The client will automatically detect their state, look for
available trades, and notify the player when they've received something.
It's also intended to work for Pokemon games other than Emerald, should any other games decide to opt in and implement
the feature into their clients.
## Data Storage Format
There is one wonder trade entry per team at `pokemon_wonder_trades_{team number}`.
It should be a dict that looks something like this:
```json
{
"_lock": 0,
"0": [3, "{some json data}"],
"3": [2, "{some json data}"]
}
```
### Lock
`_lock` tells you whether you're allowed to try to modify the key. Its value should be either `0` to represent an
unlocked state, or a timestamp represented by time since Epoch in ms (`int(time.time_ns() / 1000000)`).
[See below](#preventing-race-conditions) for more info.
### Non-lock Keys
All other keys are just non-negative integers as strings. You can think of them as wonder trade slots. Pidgeon holes
with a label. For consistency and ease of use, keep the keys between 0 and 255, and prefer the lowest number you can
use. They ONLY act as names that can be easily written to and removed from.
- You SHOULD NOT rely on those numbers being contiguous or starting at 0.
- You SHOULD NOT rely on a "trade" residing at a single slot until it is removed.
- You SHOULD NOT assume that the number has any significance to a player's slot, or trade order, or anything really.
### Values
The first entry in the tuple represents which slot put the pokemon up for trade. You could use this to display in your
game or client who the trade came from, but its primary purpose is to discriminate entries you can take from those you
can't. You don't want to send something to the server, see that the server has something to take, and then take your own
pokemon right back.
The JSON data should match the schema currently located at `data/trade_pokemon_schema.json`. It should be universally
understandable by anything trying to interact with wonder trades. Of course, some Pokemon games include more data than
others for a given pokemon, some games don't have species introduced in later generations, and some data is of a
different format, has different values, or is even spelled differently. The hope is that translating to and from JSON is
reasonable for any game (or at least any game likely to be integrated into AP), and you can easily tell from the JSON
whether your game is capable of giving the pokemon to the player in-game.
## Preventing Race Conditions
This caused by far the most headache of implementing wonder trades. You should be very thorough in trying to prevent
issues here.
If you prefer more technical explanations, the Pokemon Emerald client has documented wonder trade functions. The rest of
this section explains what problems are being solved and why the solutions work.
The problem that needs solving is that your client needs to know what the value of the trade data is before it commits
some sort of action. By design, multiple clients are writing to and removing from the same key in data storage, so if
two clients try to interact and there's ambiguity in what the data looks like, it will cause issues of duplication and
loss of data.
For example, client 1 and client 2 both see a pokemon that they can take, so they copy the pokemon to their respective
games, and both send a command to remove that pokemon from the data store. The first command works and removes the
entry, which sends an update to both clients that there no longer exists a pokemon at that slot. And then the second
command, which was already sent, tries to remove the same entry. At best, the data was duplicated, and at worst the
server raises an exception or crashes.
Thankfully, when you receive an update from the server that a storage value changed, it will tell you both the previous
and current value. That's where the lock comes in. At a basic level, your client attempts to claim ownership of the key
temporarily while it makes its modifications, and all other clients respect that claim by not interacting until the lock
is released. You know you locked the key because the `SetReply` you receive for modifying the lock is the one that set
it from an unlocked state to a locked state. When two clients try to lock at the same time, one will see an unlocked
state move to a locked state, and the other will see an already locked state move to a locked state. You can identify
whether a `SetReply` was triggered by your client's `Set` by attaching a uuid to the `Set` command, which will also be
attached to the `SetReply`. See the Emerald client for an example.
Which brings us to problem 2, which is the scenario where a client crashes or closes before unlocking the key. One rogue
client might prevent all other clients from ever interacting with wonder trading again.
So for this reason, the lock is a timestamp, and the key is considered "locked" if that timestamp is less than 5 seconds
in the past. If a client dies after locking, its lock will expire, and other clients will be able to make modifications.
Setting the lock to 0 is the canonical way of marking it as unlocked, but it's not a special case really. It's
equivalent to marking the key as last locked in 1970.
Which brings us to problem 3. Multiple clients which want to obtain the lock can only check whether the lock is
obtainable by refreshing the current lock's timestamp. So two clients trying to secure a lock made by a dead client may
trade back and forth, updating the lock to see if it is expired yet, seeing that it is not, and then waiting 5 seconds
while the other client does the same thing, which causes the lock to again be less than 5 seconds old.
Using a cooldown period longer than the time to expire only increases the minimum number of clients that can trigger
this cycle. Instead, the solution is to double your cooldown every time you bounce off an expired lock (and reset it
once you acquire it). Eventually the amount of time every client is waiting will be enough to create a gap large enough
for one client to consider the lock expired, and it will acquire the lock, make its changes, and set the lock state to
definitively unlocked, which will let the next client claim it, and so on.

View File

@ -51,13 +51,13 @@ ITEM_GROUPS = {
"Stone Badge", "Knuckle Badge",
"Dynamo Badge", "Heat Badge",
"Balance Badge", "Feather Badge",
"Mind Badge", "Rain Badge"
"Mind Badge", "Rain Badge",
},
"HMs": {
"HM01 Cut", "HM02 Fly",
"HM03 Surf", "HM04 Strength",
"HM05 Flash", "HM06 Rock Smash",
"HM07 Waterfall", "HM08 Dive"
"HM07 Waterfall", "HM08 Dive",
},
"HM01": {"HM01 Cut"},
"HM02": {"HM02 Fly"},
@ -66,7 +66,7 @@ ITEM_GROUPS = {
"HM05": {"HM05 Flash"},
"HM06": {"HM06 Rock Smash"},
"HM07": {"HM07 Waterfall"},
"HM08": {"HM08 Dive"}
"HM08": {"HM08 Dive"},
}

View File

@ -1,20 +1,84 @@
"""
Classes and functions related to AP locations for Pokemon Emerald
"""
from typing import TYPE_CHECKING, Dict, List, Optional, FrozenSet, Iterable
from typing import TYPE_CHECKING, Dict, Optional, FrozenSet, Iterable
from BaseClasses import Location, Region
from .data import BASE_OFFSET, data
from .data import BASE_OFFSET, POKEDEX_OFFSET, data
from .items import offset_item_value
if TYPE_CHECKING:
from . import PokemonEmeraldWorld
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",
},
"Trick House": {
"Trick House Puzzle 1 - Item",
"Trick House Puzzle 2 - Item 1",
"Trick House Puzzle 2 - Item 2",
"Trick House Puzzle 3 - Item 1",
"Trick House Puzzle 3 - Item 2",
"Trick House Puzzle 4 - Item",
"Trick House Puzzle 6 - Item",
"Trick House Puzzle 7 - Item",
"Trick House Puzzle 8 - Item",
"Trick House Puzzle 1 - Reward",
"Trick House Puzzle 2 - Reward",
"Trick House Puzzle 3 - Reward",
"Trick House Puzzle 4 - Reward",
"Trick House Puzzle 5 - Reward",
"Trick House Puzzle 6 - Reward",
"Trick House Puzzle 7 - Reward",
}
}
VISITED_EVENT_NAME_TO_ID = {
"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,
}
class PokemonEmeraldLocation(Location):
game: str = "Pokemon Emerald"
rom_address: Optional[int]
item_address: Optional[int]
default_item_code: Optional[int]
tags: FrozenSet[str]
@ -22,14 +86,14 @@ class PokemonEmeraldLocation(Location):
self,
player: int,
name: str,
flag: Optional[int],
address: Optional[int],
parent: Optional[Region] = None,
rom_address: Optional[int] = None,
item_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)
super().__init__(player, name, address, 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.item_address = item_address
self.tags = tags
@ -64,12 +128,17 @@ def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str,
for location_name in filtered_locations:
location_data = data.locations[location_name]
location_id = offset_flag(location_data.flag)
if location_data.flag == 0:
location_id += POKEDEX_OFFSET + int(location_name[15:])
location = PokemonEmeraldLocation(
world.player,
location_data.label,
location_data.flag,
location_id,
region,
location_data.rom_address,
location_data.address,
location_data.default_item,
location_data.tags
)
@ -84,41 +153,68 @@ def create_location_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]
if location_data.flag == 0:
label_to_id_map[location_data.label] = BASE_OFFSET + POKEDEX_OFFSET + int(location_data.name[15:])
else:
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 - Item on Ledge",
"Safari Zone NE - Hidden Item North",
"Safari Zone NE - Hidden Item East",
"Safari Zone SE - Item in Grass",
"Safari Zone SE - Hidden Item in South Grass 1",
"Safari Zone SE - Hidden Item in South Grass 2",
}
}
def set_free_fly(world: "PokemonEmeraldWorld") -> None:
# Set our free fly location
# If not enabled, set it to Littleroot Town by default
fly_location_name = "EVENT_VISITED_LITTLEROOT_TOWN"
if world.options.free_fly_location:
fly_location_name = world.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",
])
world.free_fly_location_id = VISITED_EVENT_NAME_TO_ID[fly_location_name]
free_fly_location_location = world.multiworld.get_location("FREE_FLY_LOCATION", world.player)
free_fly_location_location.item = None
free_fly_location_location.place_locked_item(world.create_event(fly_location_name))
def set_legendary_cave_entrances(world: "PokemonEmeraldWorld") -> None:
# Set Marine Cave and Terra Cave entrances
terra_cave_location_name = world.random.choice([
"TERRA_CAVE_ROUTE_114_1",
"TERRA_CAVE_ROUTE_114_2",
"TERRA_CAVE_ROUTE_115_1",
"TERRA_CAVE_ROUTE_115_2",
"TERRA_CAVE_ROUTE_116_1",
"TERRA_CAVE_ROUTE_116_2",
"TERRA_CAVE_ROUTE_118_1",
"TERRA_CAVE_ROUTE_118_2",
])
terra_cave_location_location = world.multiworld.get_location("TERRA_CAVE_LOCATION", world.player)
terra_cave_location_location.item = None
terra_cave_location_location.place_locked_item(world.create_event(terra_cave_location_name))
marine_cave_location_name = world.random.choice([
"MARINE_CAVE_ROUTE_105_1",
"MARINE_CAVE_ROUTE_105_2",
"MARINE_CAVE_ROUTE_125_1",
"MARINE_CAVE_ROUTE_125_2",
"MARINE_CAVE_ROUTE_127_1",
"MARINE_CAVE_ROUTE_127_2",
"MARINE_CAVE_ROUTE_129_1",
"MARINE_CAVE_ROUTE_129_2",
])
marine_cave_location_location = world.multiworld.get_location("MARINE_CAVE_LOCATION", world.player)
marine_cave_location_location.item = None
marine_cave_location_location.place_locked_item(world.create_event(marine_cave_location_name))

View File

@ -0,0 +1,116 @@
from typing import TYPE_CHECKING, Dict, List, Set
from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, TrainerPokemonData, data
from .options import RandomizeTrainerParties
from .pokemon import filter_species_by_nearby_bst
from .util import int_to_bool_array
if TYPE_CHECKING:
from . import PokemonEmeraldWorld
def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None:
if world.options.trainer_parties == RandomizeTrainerParties.option_vanilla:
return
from collections import defaultdict
should_match_bst = world.options.trainer_parties in {
RandomizeTrainerParties.option_match_base_stats,
RandomizeTrainerParties.option_match_base_stats_and_type,
}
should_match_type = world.options.trainer_parties in {
RandomizeTrainerParties.option_match_type,
RandomizeTrainerParties.option_match_base_stats_and_type,
}
per_species_tmhm_moves: Dict[int, List[int]] = {}
for trainer in world.modified_trainers:
new_party = []
for pokemon in trainer.party.pokemon:
original_species = data.species[pokemon.species_id]
# Construct progressive tiers of blacklists that can be peeled back if they
# collectively cover too much of the pokedex. A lower index in `blacklists`
# indicates a more important set of species to avoid. Entries at `0` will
# always be blacklisted.
blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
# Blacklist unevolved species
if pokemon.level >= world.options.force_fully_evolved:
blacklists[0].append(UNEVOLVED_POKEMON)
# Blacklist from player options
blacklists[2].append(world.blacklisted_opponent_pokemon)
# Type matching blacklist
if should_match_type:
blacklists[3].append({
species.species_id
for species in world.modified_species.values()
if not bool(set(species.types) & set(original_species.types))
})
merged_blacklist: Set[int] = set()
for max_priority in reversed(sorted(blacklists.keys())):
merged_blacklist = set()
for priority in blacklists.keys():
if priority <= max_priority:
for blacklist in blacklists[priority]:
merged_blacklist |= blacklist
if len(merged_blacklist) < NUM_REAL_SPECIES:
break
else:
raise RuntimeError("This should never happen")
candidates = [
species
for species in world.modified_species.values()
if species.species_id not in merged_blacklist
]
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
new_species = world.random.choice(candidates)
if new_species.species_id not in per_species_tmhm_moves:
per_species_tmhm_moves[new_species.species_id] = sorted({
world.modified_tmhm_moves[i]
for i, is_compatible in enumerate(int_to_bool_array(new_species.tm_hm_compatibility))
if is_compatible
})
# TMs and HMs compatible with the species
tm_hm_movepool = per_species_tmhm_moves[new_species.species_id]
# Moves the pokemon could have learned by now
level_up_movepool = sorted({
move.move_id
for move in new_species.learnset
if move.move_id != 0 and move.level <= pokemon.level
})
if len(level_up_movepool) < 4:
level_up_moves = [level_up_movepool[i] if i < len(level_up_movepool) else 0 for i in range(4)]
else:
level_up_moves = world.random.sample(level_up_movepool, 4)
if len(tm_hm_movepool) < 4:
hm_moves = list(reversed(list(tm_hm_movepool[i] if i < len(tm_hm_movepool) else 0 for i in range(4))))
else:
hm_moves = world.random.sample(tm_hm_movepool, 4)
# 25% chance to pick a move from TMs or HMs
new_moves = (
hm_moves[0] if world.random.random() < 0.25 else level_up_moves[0],
hm_moves[1] if world.random.random() < 0.25 else level_up_moves[1],
hm_moves[2] if world.random.random() < 0.25 else level_up_moves[2],
hm_moves[3] if world.random.random() < 0.25 else level_up_moves[3]
)
new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves))
trainer.party.pokemon = new_party

View File

@ -2,9 +2,9 @@
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 Options import (Choice, DeathLink, DefaultOnToggle, TextChoice, OptionSet, NamedRange, Range, Toggle, FreeText,
PerGameCommonOptions)
from .data import data
@ -16,12 +16,14 @@ class Goal(Choice):
Champion: Become the champion and enter the hall of fame
Steven: Defeat Steven in Meteor Falls
Norman: Defeat Norman in Petalburg Gym
Legendary Hunt: Defeat or catch legendary pokemon (or whatever was randomized into their encounters)
"""
display_name = "Goal"
default = 0
option_champion = 0
option_steven = 1
option_norman = 2
option_legendary_hunt = 3
class RandomizeBadges(Choice):
@ -69,6 +71,13 @@ class RandomizeBikes(Toggle):
display_name = "Randomize Bikes"
class RandomizeEventTickets(Toggle):
"""
Adds the event tickets to the pool, which let you access legendaries by sailing from Lilycove
"""
display_name = "Randomize Event Tickets"
class RandomizeRods(Toggle):
"""
Adds fishing rods to the pool
@ -97,13 +106,40 @@ class RandomizeNpcGifts(Toggle):
display_name = "Randomize NPC Gifts"
class RandomizeBerryTrees(Toggle):
"""
Adds berry trees to the pool. Empty soil patches are converted to locations and contribute Sitrus Berries to the pool.
"""
display_name = "Randomize Berry Trees"
class Dexsanity(Toggle):
"""
Adding a "caught" pokedex entry gives you an item (catching, evolving, trading, etc.).
Defeating gym leaders provides dex info, allowing you to see where on the map you can catch species you need.
Each pokedex entry adds a Poke Ball, Great Ball, or Ultra Ball to the pool.
"""
display_name = "Dexsanity"
class Trainersanity(Toggle):
"""
Defeating a trainer for the first time gives you an item. Trainers are no longer missable.
Trainers no longer give you money for winning. Each trainer adds a valuable item (nugget, stardust, etc.) to the pool.
"""
display_name = "Trainersanity"
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...)
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"
@ -120,18 +156,16 @@ class HiddenItemsRequireItemfinder(DefaultOnToggle):
display_name = "Require Itemfinder"
class DarkCavesRequireFlash(DefaultOnToggle):
class DarkCavesRequireFlash(Choice):
"""
The lower floors of Granite Cave and Victory Road logically require use of HM05 Flash
Determines whether HM05 Flash is logically required to navigate a dark cave
"""
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"
default = 3
option_neither = 0
option_only_granite_cave = 1
option_only_victory_road = 2
option_both = 3
class EliteFourRequirement(Choice):
@ -180,6 +214,61 @@ class NormanCount(Range):
default = 4
class LegendaryHuntCatch(Toggle):
"""
Sets whether legendaries need to be caught to satisfy the Legendary Hunt win condition. Defeated legendaries can be respawned by defeating the Elite 4.
"""
display_name = "Legendary Hunt Requires Catching"
class LegendaryHuntCount(Range):
"""
Sets the number of legendaries that must be caught/defeated for the Legendary Hunt goal
"""
display_name = "Legendary Hunt Count"
range_start = 1
range_end = 12
default = 3
class AllowedLegendaryHuntEncounters(OptionSet):
"""
Sets which legendary encounters can contribute to the Legendary Hunt goal.
Latios will always be the roamer. Latias will always be at Southern Island.
Possible values are:
"Groudon"
"Kyogre"
"Rayquaza"
"Latios"
"Latias"
"Regirock"
"Registeel"
"Regice"
"Ho-oh"
"Lugia"
"Deoxys"
"Mew"
"""
display_name = "Allowed Legendary Hunt Encounters"
valid_keys = frozenset([
"Groudon",
"Kyogre",
"Rayquaza",
"Latios",
"Latias",
"Regirock",
"Registeel",
"Regice",
"Ho-oh",
"Lugia",
"Deoxys",
"Mew",
])
default = valid_keys.copy()
class RandomizeWildPokemon(Choice):
"""
Randomizes wild pokemon encounters (grass, caves, water, fishing)
@ -199,11 +288,16 @@ class RandomizeWildPokemon(Choice):
option_completely_random = 4
class AllowWildLegendaries(DefaultOnToggle):
class WildEncounterBlacklist(OptionSet):
"""
Wild encounters can be replaced by legendaries. Only applied if Randomize Wild Pokemon is not Vanilla.
Prevents listed species from appearing in the wild when wild encounters are randomized.
May be overridden if enforcing other restrictions in combination with this blacklist is impossible.
Use "_Legendaries" as a shortcut for legendary pokemon.
"""
display_name = "Allow Wild Legendaries"
display_name = "Wild Encounter Blacklist"
valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"}
class RandomizeStarters(Choice):
@ -225,11 +319,16 @@ class RandomizeStarters(Choice):
option_completely_random = 4
class AllowStarterLegendaries(DefaultOnToggle):
class StarterBlacklist(OptionSet):
"""
Starters can be replaced by legendaries. Only applied if Randomize Starters is not Vanilla.
Prevents listed species from appearing as starters when starters are randomized.
May be overridden if enforcing other restrictions in combination with this blacklist is impossible.
Use "_Legendaries" as a shortcut for legendary pokemon.
"""
display_name = "Allow Starter Legendaries"
display_name = "Starter Blacklist"
valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"}
class RandomizeTrainerParties(Choice):
@ -251,25 +350,61 @@ class RandomizeTrainerParties(Choice):
option_completely_random = 4
class AllowTrainerLegendaries(DefaultOnToggle):
class TrainerPartyBlacklist(OptionSet):
"""
Enemy trainer pokemon can be replaced by legendaries. Only applied if Randomize Trainer Parties is not Vanilla.
Prevents listed species from appearing in opponent trainers' parties if opponent parties are randomized.
May be overridden if enforcing other restrictions in combination with this blacklist is impossible.
Use "_Legendaries" as a shortcut for legendary pokemon.
"""
display_name = "Allow Trainer Legendaries"
display_name = "Trainer Party Blacklist"
valid_keys = frozenset(species.label for species in data.species.values()) | {"_Legendaries"}
class RandomizeStaticEncounters(Choice):
class ForceFullyEvolved(Range):
"""
Randomizes static encounters (Rayquaza, hidden Kekleons, fake Voltorb pokeballs, etc...)
When an opponent uses a pokemon of the specified level or higher, restricts the species to only fully evolved pokemon.
"""
display_name = "Force Fully Evolved"
range_start = 1
range_end = 100
default = 100
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
class RandomizeLegendaryEncounters(Choice):
"""
Randomizes legendary encounters (Rayquaza, Regice, Latias, etc.). The roamer will always be Latios during legendary hunts.
Vanilla: Legendary encounters are unchanged
Shuffle: Legendary encounters are shuffled between each other
Match Base Stats: Legendary encounters are replaced with species with approximately the same bst
Match Type: Legendary 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"
display_name = "Randomize Legendary 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 RandomizeMiscPokemon(Choice):
"""
Randomizes non-legendary static encounters. May grow to include other pokemon like trades or gifts.
Vanilla: Species are unchanged
Shuffle: Species are shuffled between each other
Match Base Stats: Species are replaced with species with approximately the same bst
Match Type: Species 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 Misc Pokemon"
default = 0
option_vanilla = 0
option_shuffle = 1
@ -363,48 +498,52 @@ class MoveNormalTypeBias(Range):
default = 0
class HmCompatibility(Choice):
class MoveBlacklist(OptionSet):
"""
Modifies the compatibility of HMs
A list of moves which should be excluded from learnsets, TMs, and move tutors.
"""
display_name = "Move Blacklist"
valid_keys = frozenset(data.move_labels.keys())
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)
class HmCompatibility(NamedRange):
"""
Sets the percent chance that a given HM is compatible with a species
"""
display_name = "HM Compatibility"
default = 1
option_vanilla = 0
option_fully_compatible = 1
option_completely_random = 2
default = -1
range_start = 50
range_end = 100
special_range_names = {
"vanilla": -1
}
class TmCompatibility(Choice):
class TmTutorCompatibility(NamedRange):
"""
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)
Sets the percent chance that a given TM or move tutor is compatible with a species
"""
display_name = "TM Compatibility"
default = 0
option_vanilla = 0
option_fully_compatible = 1
option_completely_random = 2
display_name = "TM/Tutor Compatibility"
default = -1
range_start = 0
range_end = 100
special_range_names = {
"vanilla": -1
}
class TmMoves(Toggle):
class TmTutorMoves(Toggle):
"""
Randomizes the moves taught by TMs
Randomizes the moves taught by TMs and move tutors
"""
display_name = "TM Moves"
display_name = "TM/Tutor Moves"
class ReusableTms(Toggle):
class ReusableTmsTutors(Toggle):
"""
Sets TMs to not break after use (they remain sellable)
Sets TMs to not break after use (they remain sellable). Sets move tutors to infinite use.
"""
display_name = "Reusable TMs"
display_name = "Reusable TMs and Tutors"
class MinCatchRate(Range):
@ -428,6 +567,15 @@ class GuaranteedCatch(Toggle):
display_name = "Guaranteed Catch"
class NormalizeEncounterRates(Toggle):
"""
Make every slot on an encounter table approximately equally likely.
This does NOT mean every species is equally likely. But it will make rarer encounters less rare overall.
"""
display_name = "Normalize Encounter Rates"
class ExpModifier(Range):
"""
Multiplies gained experience by a percentage
@ -435,7 +583,7 @@ class ExpModifier(Range):
100 is default
50 is half
200 is double
etc...
etc.
"""
display_name = "Exp Modifier"
range_start = 0
@ -450,6 +598,48 @@ class BlindTrainers(Toggle):
display_name = "Blind Trainers"
class PurgeSpinners(Toggle):
"""
Trainers will rotate in predictable patterns on a set interval instead of randomly and don't turn toward you when you run
"""
display_name = "Purge Spinners"
class MatchTrainerLevels(Choice):
"""
When you start a battle with a trainer, your party's levels will be automatically set to match that trainer's highest level pokemon.
The experience you receive will match your party's average actual level, and will only be awarded when you win the battle.
This is a pseudo-replacement for a level cap and makes every trainer battle a fair fight while still allowing you to level up.
Off: The vanilla experience
Additive: The modifier you apply to your team is a flat bonus
Multiplicative: The modifier you apply to your team is a percent bonus
"""
display_name = "Match Trainer Levels"
default = 0
option_off = 0
option_additive = 1
option_multiplicative = 2
class MatchTrainerLevelsBonus(Range):
"""
A level bonus (or penalty) to apply to your team when matching an opponent's levels.
When the match trainer levels option is "additive", this value is added to your team's levels during a battle.
For example, if this value is 5 (+5 levels), you'll have a level 25 team against a level 20 team, and a level 45 team against a level 40 team.
When the match trainer levels option is "multiplicative", this is a percent bonus.
For example, if this value is 5 (+5%), you'll have a level 21 team against a level 20 team, and a level 42 team against a level 40 team.
"""
display_name = "Match Trainer Levels Modifier"
range_start = -100
range_end = 100
default = 0
class DoubleBattleChance(Range):
"""
The percent chance that a trainer with more than 1 pokemon will be converted into a double battle.
@ -492,18 +682,34 @@ class RemoveRoadblocks(OptionSet):
"Safari Zone Construction Workers",
"Lilycove City Wailmer",
"Aqua Hideout Grunts",
"Seafloor Cavern Aqua Grunt"
"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.
This aims to take some power away from Surf by restricting how much it allows you to access.
"""
display_name = "Extra Boulders"
class ExtraBumpySlope(Toggle):
"""
Adds a bumpy slope to Route 115 which allows access to Meteor Falls if you have the Acro Bike.
This aims to take some power away from Surf by adding a new way to exit the Rustboro area.
"""
display_name = "Extra Bumpy Slope"
class ModifyRoute118(Toggle):
"""
Changes the layout of Route 118 so that it must be crossed with the Acro Bike instead of Surf.
This aims to take some power away from Surf by restricting how much it allows you to access.
"""
display_name = "Modify Route 118"
class FreeFlyLocation(Toggle):
"""
Enables flying to one random location when Mom gives you the running shoes (excluding cities reachable with no items)
@ -511,11 +717,14 @@ class FreeFlyLocation(Toggle):
display_name = "Free Fly Location"
class FlyWithoutBadge(DefaultOnToggle):
class HmRequirements(TextChoice):
"""
Fly does not require the Feather Badge to use in the field
Sets the requirements to use HMs outside of battle
"""
display_name = "Fly Without Badge"
display_name = "HM Requirements"
default = 0
option_vanilla = 0
option_fly_without_badge = 1
class TurboA(Toggle):
@ -540,11 +749,53 @@ class ReceiveItemMessages(Choice):
option_none = 2
class RemoteItems(Toggle):
"""
Instead of placing your own items directly into the ROM, all items are received from the server, including items you find for yourself.
This enables co-op of a single slot and recovering more items after a lost save file (if you're so unlucky).
But it changes pickup behavior slightly and requires connection to the server to receive any items.
"""
display_name = "Remote Items"
class RandomizeMusic(Toggle):
"""
Shuffles music played in any situation where it loops. Includes many FRLG tracks.
"""
display_name = "Randomize Music"
class RandomizeFanfares(Toggle):
"""
Shuffles fanfares for item pickups, healing at the pokecenter, etc.
When this option is enabled, pressing B will interrupt most fanfares.
"""
display_name = "Randomize Fanfares"
class WonderTrading(DefaultOnToggle):
"""
Allows participation in wonder trading with other players in your current multiworld. Speak with the center receptionist on the second floor of any pokecenter.
Wonder trading NEVER affects logic.
Certain aspects of a pokemon species are per-game, not per-pokemon.
As a result, some things are not retained during a trade, including type, ability, level up learnset, and so on.
Receiving a pokemon this way does not mark it as found in your pokedex.
Trade evolutions do not evolve this way; they retain their modified methods (level ups and item use).
"""
display_name = "Wonder Trading"
class EasterEgg(FreeText):
"""
???
Enter certain phrases and something special might happen.
All secret phrases are something that could be a trendy phrase in Dewford Town. They are case insensitive.
"""
default = "Example Passphrase"
default = "EMERALD SECRET"
@dataclass
@ -555,10 +806,14 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
hms: RandomizeHms
key_items: RandomizeKeyItems
bikes: RandomizeBikes
event_tickets: RandomizeEventTickets
rods: RandomizeRods
overworld_items: RandomizeOverworldItems
hidden_items: RandomizeHiddenItems
npc_gifts: RandomizeNpcGifts
berry_trees: RandomizeBerryTrees
dexsanity: Dexsanity
trainersanity: Trainersanity
item_pool_type: ItemPoolType
require_itemfinder: HiddenItemsRequireItemfinder
@ -567,14 +822,19 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
elite_four_count: EliteFourCount
norman_requirement: NormanRequirement
norman_count: NormanCount
legendary_hunt_catch: LegendaryHuntCatch
legendary_hunt_count: LegendaryHuntCount
allowed_legendary_hunt_encounters: AllowedLegendaryHuntEncounters
wild_pokemon: RandomizeWildPokemon
allow_wild_legendaries: AllowWildLegendaries
wild_encounter_blacklist: WildEncounterBlacklist
starters: RandomizeStarters
allow_starter_legendaries: AllowStarterLegendaries
starter_blacklist: StarterBlacklist
trainer_parties: RandomizeTrainerParties
allow_trainer_legendaries: AllowTrainerLegendaries
static_encounters: RandomizeStaticEncounters
trainer_party_blacklist: TrainerPartyBlacklist
force_fully_evolved: ForceFullyEvolved
legendary_encounters: RandomizeLegendaryEncounters
misc_pokemon: RandomizeMiscPokemon
types: RandomizeTypes
abilities: RandomizeAbilities
ability_blacklist: AbilityBlacklist
@ -582,25 +842,38 @@ class PokemonEmeraldOptions(PerGameCommonOptions):
level_up_moves: LevelUpMoves
move_match_type_bias: MoveMatchTypeBias
move_normal_type_bias: MoveNormalTypeBias
tm_compatibility: TmCompatibility
tm_tutor_compatibility: TmTutorCompatibility
hm_compatibility: HmCompatibility
tm_moves: TmMoves
reusable_tms: ReusableTms
tm_tutor_moves: TmTutorMoves
reusable_tms_tutors: ReusableTmsTutors
move_blacklist: MoveBlacklist
min_catch_rate: MinCatchRate
guaranteed_catch: GuaranteedCatch
normalize_encounter_rates: NormalizeEncounterRates
exp_modifier: ExpModifier
blind_trainers: BlindTrainers
purge_spinners: PurgeSpinners
match_trainer_levels: MatchTrainerLevels
match_trainer_levels_bonus: MatchTrainerLevelsBonus
double_battle_chance: DoubleBattleChance
better_shops: BetterShops
enable_ferry: EnableFerry
remove_roadblocks: RemoveRoadblocks
extra_boulders: ExtraBoulders
extra_bumpy_slope: ExtraBumpySlope
modify_118: ModifyRoute118
free_fly_location: FreeFlyLocation
fly_without_badge: FlyWithoutBadge
hm_requirements: HmRequirements
turbo_a: TurboA
receive_item_messages: ReceiveItemMessages
remote_items: RemoteItems
music: RandomizeMusic
fanfares: RandomizeFanfares
death_link: DeathLink
enable_wonder_trading: WonderTrading
easter_egg: EasterEgg

View File

@ -1,16 +1,23 @@
"""
Functions related to pokemon species and moves
"""
import time
import functools
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple
from .data import SpeciesData, data
from Options import Toggle
from .data import NUM_REAL_SPECIES, POSTGAME_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData, SpeciesData, data
from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters,
RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon,
TmTutorCompatibility)
from .util import bool_array_to_int, get_easter_egg, int_to_bool_array
if TYPE_CHECKING:
from random import Random
from . import PokemonEmeraldWorld
_damaging_moves = frozenset({
_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,
@ -26,10 +33,13 @@ _damaging_moves = frozenset({
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
340, 341, 342, 343, 344, 345, 348, 350, 351, 352, 353, 354,
})
"""IDs for moves that safely deal direct damage, for avoiding putting the
player in a situation where they can only use status moves, or are forced
to faint themselves, or something of that nature."""
_move_types = [
_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,
@ -53,82 +63,35 @@ _move_types = [
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
12, 11, 14, 12, 16, 5, 13, 11, 8, 14,
]
"""Maps move ids to the type of that move"""
_moves_by_type: Dict[int, List[int]] = {}
for move, type in enumerate(_move_types):
_moves_by_type.setdefault(type, []).append(move)
_MOVES_BY_TYPE: Dict[int, List[int]] = {}
"""Categorizes move ids by their type"""
for move, type in enumerate(_MOVE_TYPES):
_MOVES_BY_TYPE.setdefault(type, []).append(move)
_move_blacklist = frozenset({
HM_MOVES = frozenset({
data.constants["MOVE_CUT"],
data.constants["MOVE_FLY"],
data.constants["MOVE_SURF"],
data.constants["MOVE_STRENGTH"],
data.constants["MOVE_FLASH"],
data.constants["MOVE_ROCK_SMASH"],
data.constants["MOVE_WATERFALL"],
data.constants["MOVE_DIVE"],
})
_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'
})
} | HM_MOVES)
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)
@functools.lru_cache(maxsize=386)
def get_species_id_by_label(label: str) -> int:
return next(species.species_id for species in data.species.values() if species.label == label)
def get_random_type(random: "Random") -> int:
@ -145,7 +108,7 @@ def get_random_move(
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())
expanded_blacklist = _MOVE_BLACKLIST | (blacklist if blacklist is not None else set())
bias = random.random() * 100
if bias < type_bias:
@ -175,8 +138,8 @@ def get_random_move(
if type_target is None:
possible_moves = [i for i in range(data.constants["MOVES_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]
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)
@ -185,12 +148,549 @@ def get_random_move(
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)
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
def filter_species_by_nearby_bst(species: List[SpeciesData], target_bst: int) -> List[SpeciesData]:
# Sort by difference in bst, then chop off the tail of the list that's more than
# 10% different. If that leaves the list empty, increase threshold to 20%, then 30%, etc.
species = sorted(species, key=lambda species: abs(sum(species.base_stats) - target_bst))
cutoff_index = 0
max_percent_different = 10
while cutoff_index == 0 and max_percent_different < 10000:
while cutoff_index < len(species) and abs(sum(species[cutoff_index].base_stats) - target_bst) < target_bst * (max_percent_different / 100):
cutoff_index += 1
max_percent_different += 10
return species[:cutoff_index + 1]
def randomize_types(world: "PokemonEmeraldWorld") -> None:
if world.options.types == RandomizeTypes.option_shuffle:
type_map = list(range(18))
world.random.shuffle(type_map)
# We never want to map to the ??? type, so swap whatever index maps to ??? with ???
# which forces ??? to always map to itself. 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 world.modified_species.values():
species.types = (type_map[species.types[0]], type_map[species.types[1]])
elif world.options.types == RandomizeTypes.option_completely_random:
for species in world.modified_species.values():
new_type_1 = get_random_type(world.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(world.random)
species.types = (new_type_1, new_type_2)
elif world.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 world.modified_species.values():
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))
world.random.shuffle(type_map)
# We never want to map to the ??? type, so swap whatever index maps to ??? with ???
# which forces ??? to always map to itself. 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 += [world.modified_species[evo.species_id] for evo in evolution.evolutions]
def randomize_wild_encounters(world: "PokemonEmeraldWorld") -> None:
if world.options.wild_pokemon == RandomizeWildPokemon.option_vanilla:
return
from collections import defaultdict
should_match_bst = world.options.wild_pokemon in {
RandomizeWildPokemon.option_match_base_stats,
RandomizeWildPokemon.option_match_base_stats_and_type,
}
should_match_type = world.options.wild_pokemon in {
RandomizeWildPokemon.option_match_type,
RandomizeWildPokemon.option_match_base_stats_and_type,
}
catch_em_all = world.options.dexsanity == Toggle.option_true
catch_em_all_placed = set()
priority_species = [data.constants["SPECIES_WAILORD"], data.constants["SPECIES_RELICANTH"]]
# Loop over map data to modify their encounter slots
map_names = list(world.modified_maps.keys())
world.random.shuffle(map_names)
for map_name in map_names:
placed_priority_species = False
map_data = world.modified_maps[map_name]
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:
# Create a map from the original species to new species
# instead of just randomizing every slot.
# Force area 1-to-1 mapping, in other words.
species_old_to_new_map: Dict[int, int] = {}
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:
new_species_id = priority_species.pop()
placed_priority_species = True
else:
original_species = data.species[species_id]
# Construct progressive tiers of blacklists that can be peeled back if they
# collectively cover too much of the pokedex. A lower index in `blacklists`
# indicates a more important set of species to avoid. Entries at `0` will
# always be blacklisted.
blacklists: Dict[int, List[Set[int]]] = defaultdict(list)
# Blacklist pokemon already on this table
blacklists[0].append(set(species_old_to_new_map.values()))
# If doing legendary hunt, blacklist Latios from wild encounters so
# it can be tracked as the roamer. Otherwise it may be impossible
# to tell whether a highlighted route is the roamer or a wild
# encounter.
if world.options.goal == Goal.option_legendary_hunt:
blacklists[0].append({data.constants["SPECIES_LATIOS"]})
# If dexsanity/catch 'em all mode, blacklist already placed species
# until every species has been placed once
if catch_em_all and len(catch_em_all_placed) < NUM_REAL_SPECIES:
blacklists[1].append(catch_em_all_placed)
# Blacklist from player options
blacklists[2].append(world.blacklisted_wilds)
# Type matching blacklist
if should_match_type:
blacklists[3].append({
species.species_id
for species in world.modified_species.values()
if not bool(set(species.types) & set(original_species.types))
})
merged_blacklist: Set[int] = set()
for max_priority in reversed(sorted(blacklists.keys())):
merged_blacklist = set()
for priority in blacklists.keys():
if priority <= max_priority:
for blacklist in blacklists[priority]:
merged_blacklist |= blacklist
if len(merged_blacklist) < NUM_REAL_SPECIES:
break
else:
raise RuntimeError("This should never happen")
candidates = [
species
for species in world.modified_species.values()
if species.species_id not in merged_blacklist
]
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
new_species_id = world.random.choice(candidates).species_id
species_old_to_new_map[species_id] = new_species_id
if catch_em_all and map_data.name not in POSTGAME_MAPS:
catch_em_all_placed.add(new_species_id)
# Actually create the new list of slots and encounter table
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.address)
# Rename event items for the new wild pokemon species
slot_category: Tuple[str, List[Tuple[Optional[str], range]]] = [
("LAND", [(None, range(0, 12))]),
("WATER", [(None, range(0, 5))]),
("FISHING", [("OLD_ROD", range(0, 2)), ("GOOD_ROD", range(2, 5)), ("SUPER_ROD", range(5, 10))]),
][i]
for j, new_species_id in enumerate(new_slots):
# Get the subcategory for rods
subcategory = next(sc for sc in slot_category[1] if j in sc[1])
subcategory_species = []
for k in subcategory[1]:
if new_slots[k] not in subcategory_species:
subcategory_species.append(new_slots[k])
# Create the name of the location that corresponds to this encounter slot
# Fishing locations include the rod name
subcategory_str = "" if subcategory[0] is None else "_" + subcategory[0]
encounter_location_index = subcategory_species.index(new_species_id) + 1
encounter_location_name = f"{map_data.name}_{slot_category[0]}_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}"
except KeyError:
pass # Map probably isn't included; should be careful here about bad encounter location names
map_data.land_encounters = new_encounters[0]
map_data.water_encounters = new_encounters[1]
map_data.fishing_encounters = new_encounters[2]
def randomize_abilities(world: "PokemonEmeraldWorld") -> None:
if world.options.abilities == RandomizeAbilities.option_vanilla:
return
# Creating list of potential abilities
ability_label_to_value = {ability.label.lower(): ability.ability_id for ability in data.abilities}
ability_blacklist_labels = {"cacophony"} # Cacophony is defined and has a description, but no effect
option_ability_blacklist = world.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 data.abilities if a.ability_id not in ability_blacklist]
if world.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 world.modified_species.values():
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
# 0 is the value for "no ability"; species with only 1 ability have the other set to 0
new_abilities = (
0 if old_abilities[0] == 0 else world.random.choice(ability_whitelist),
0 if old_abilities[1] == 0 else world.random.choice(ability_whitelist)
)
# Recursively modify the abilities of anything that evolves from this pokemon
# until the evolution doesn't have a matching set of abilities
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 += [
world.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 world.modified_species.values():
old_abilities = species.abilities
# 0 is the value for "no ability"; species with only 1 ability have the other set to 0
new_abilities = (
0 if old_abilities[0] == 0 else world.random.choice(ability_whitelist),
0 if old_abilities[1] == 0 else world.random.choice(ability_whitelist)
)
species.abilities = new_abilities
def randomize_learnsets(world: "PokemonEmeraldWorld") -> None:
if world.options.level_up_moves == LevelUpMoves.option_vanilla:
return
type_bias = world.options.move_match_type_bias.value
normal_bias = world.options.move_normal_type_bias.value
for species in world.modified_species.values():
old_learnset = species.learnset
new_learnset: List[LearnsetMove] = []
# All species have 4 moves at level 0. Up to 3 of them are blank spaces reserved for the
# start with four moves option. This either replaces those moves or leaves it blank
# and moves the cursor.
cursor = 0
while old_learnset[cursor].move_id == 0:
if world.options.level_up_moves == LevelUpMoves.option_start_with_four_moves:
new_move = get_random_move(world.random,
{move.move_id for move in new_learnset} | world.blacklisted_moves,
type_bias, normal_bias, species.types)
else:
new_move = 0
new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move))
cursor += 1
# All moves from here onward are actual moves.
while cursor < len(old_learnset):
# Guarantees the starter has a good damaging move; i will always be <=3 when entering this loop
if cursor == 3:
new_move = get_random_damaging_move(world.random, {move.move_id for move in new_learnset})
else:
new_move = get_random_move(world.random,
{move.move_id for move in new_learnset} | world.blacklisted_moves,
type_bias, normal_bias, species.types)
new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move))
cursor += 1
species.learnset = new_learnset
def randomize_starters(world: "PokemonEmeraldWorld") -> None:
if world.options.starters == RandomizeStarters.option_vanilla:
return
should_match_bst = world.options.starters in {
RandomizeStarters.option_match_base_stats,
RandomizeStarters.option_match_base_stats_and_type,
}
should_match_type = world.options.starters in {
RandomizeStarters.option_match_type,
RandomizeStarters.option_match_base_stats_and_type,
}
new_starters: List[SpeciesData] = []
easter_egg_type, easter_egg_value = get_easter_egg(world.options.easter_egg.value)
if easter_egg_type == 1:
new_starters = [
world.modified_species[easter_egg_value],
world.modified_species[easter_egg_value],
world.modified_species[easter_egg_value]
]
else:
for i, starter_id in enumerate(data.starters):
original_starter = data.species[starter_id]
type_blacklist = {
species.species_id
for species in world.modified_species.values()
if not bool(set(species.types) & set(original_starter.types))
} if should_match_type else set()
merged_blacklist = set(s.species_id for s in new_starters) | world.blacklisted_starters | type_blacklist
if len(merged_blacklist) == NUM_REAL_SPECIES:
merged_blacklist = set(s.species_id for s in new_starters) | world.blacklisted_starters
if len(merged_blacklist) == NUM_REAL_SPECIES:
merged_blacklist = set(s.species_id for s in new_starters)
candidates = [
species
for species in world.modified_species.values()
if species.species_id not in merged_blacklist
]
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_starter.base_stats))
new_starters.append(world.random.choice(candidates))
world.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
# (trainer name, index of starter in team, whether the starter is evolved)
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 = world.random.choice(potential_evolutions)
for trainer_name, starter_position, is_evolved in rival_teams[i]:
trainer_data = world.modified_trainers[data.constants[trainer_name]]
trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id
def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None:
if world.options.legendary_encounters == RandomizeLegendaryEncounters.option_vanilla:
return
elif world.options.legendary_encounters == RandomizeLegendaryEncounters.option_shuffle:
# Just take the existing species and shuffle them
shuffled_species = [encounter.species_id for encounter in data.legendary_encounters]
world.random.shuffle(shuffled_species)
for i, encounter in enumerate(data.legendary_encounters):
world.modified_legendary_encounters.append(MiscPokemonData(
shuffled_species[i],
encounter.address
))
else:
should_match_bst = world.options.legendary_encounters in {
RandomizeLegendaryEncounters.option_match_base_stats,
RandomizeLegendaryEncounters.option_match_base_stats_and_type
}
should_match_type = world.options.legendary_encounters in {
RandomizeLegendaryEncounters.option_match_type,
RandomizeLegendaryEncounters.option_match_base_stats_and_type
}
for encounter in data.legendary_encounters:
original_species = world.modified_species[encounter.species_id]
candidates = list(world.modified_species.values())
if should_match_type:
candidates = [
species
for species in candidates
if bool(set(species.types) & set(original_species.types))
]
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
world.modified_legendary_encounters.append(MiscPokemonData(
world.random.choice(candidates).species_id,
encounter.address
))
def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None:
if world.options.misc_pokemon == RandomizeMiscPokemon.option_vanilla:
return
elif world.options.misc_pokemon == RandomizeMiscPokemon.option_shuffle:
# Just take the existing species and shuffle them
shuffled_species = [encounter.species_id for encounter in data.misc_pokemon]
world.random.shuffle(shuffled_species)
world.modified_misc_pokemon = []
for i, encounter in enumerate(data.misc_pokemon):
world.modified_misc_pokemon.append(MiscPokemonData(
shuffled_species[i],
encounter.address
))
else:
should_match_bst = world.options.misc_pokemon in {
RandomizeMiscPokemon.option_match_base_stats,
RandomizeMiscPokemon.option_match_base_stats_and_type,
}
should_match_type = world.options.misc_pokemon in {
RandomizeMiscPokemon.option_match_type,
RandomizeMiscPokemon.option_match_base_stats_and_type,
}
for encounter in data.misc_pokemon:
original_species = world.modified_species[encounter.species_id]
candidates = list(world.modified_species.values())
if should_match_type:
candidates = [
species
for species in candidates
if bool(set(species.types) & set(original_species.types))
]
if should_match_bst:
candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats))
player_filtered_candidates = [
species
for species in candidates
if species.species_id not in world.blacklisted_wilds
]
if len(player_filtered_candidates) > 0:
candidates = player_filtered_candidates
world.modified_misc_pokemon.append(MiscPokemonData(
world.random.choice(candidates).species_id,
encounter.address
))
def randomize_tm_hm_compatibility(world: "PokemonEmeraldWorld") -> None:
for species in world.modified_species.values():
# TM and HM compatibility is stored as a 64-bit bitfield
combatibility_array = int_to_bool_array(species.tm_hm_compatibility)
# TMs
if world.options.tm_tutor_compatibility != TmTutorCompatibility.special_range_names["vanilla"]:
for i in range(0, 50):
combatibility_array[i] = world.random.random() < world.options.tm_tutor_compatibility / 100
# HMs
if world.options.hm_compatibility != HmCompatibility.special_range_names["vanilla"]:
for i in range(50, 58):
combatibility_array[i] = world.random.random() < world.options.hm_compatibility / 100
species.tm_hm_compatibility = bool_array_to_int(combatibility_array)

View File

@ -1,9 +1,9 @@
"""
Functions related to AP regions for Pokemon Emerald (see ./data/regions for region definitions)
"""
from typing import TYPE_CHECKING, Dict, List, Tuple
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
from BaseClasses import ItemClassification, Region
from BaseClasses import CollectionState, ItemClassification, Region
from .data import data
from .items import PokemonEmeraldItem
@ -18,9 +18,80 @@ 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.
"""
# 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.
encounter_categories: Dict[str, List[Tuple[Optional[str], range, Optional[Callable[[CollectionState], bool]]]]] = {
"LAND": [(None, range(0, 12), None)],
"WATER": [(None, range(0, 5), None)],
"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)),
],
}
def connect_to_map_encounters(region: Region, map_name: str, include_slots: Tuple[bool, bool, bool]):
"""
Connects the provided region to the corresponding wild encounters for the given parent map.
Each in-game map may have a non-physical Region for encountering wild pokemon in each of the three categories
land, water, and fishing. Region data defines whether a given region includes places where those encounters can
be accessed (i.e. whether the region has tall grass, a river bank, is on water, etc.).
These regions are created lazily and dynamically so as not to bother with unused maps.
"""
# For each of land, water, and fishing, connect the region if indicated by include_slots
for i, encounter_category in enumerate(encounter_categories.items()):
if include_slots[i]:
region_name = f"{map_name}_{encounter_category[0]}_ENCOUNTERS"
# If the region hasn't been created yet, create it now
try:
encounter_region = world.multiworld.get_region(region_name, world.player)
except KeyError:
encounter_region = Region(region_name, world.player, world.multiworld)
encounter_slots = getattr(data.maps[map_name], f"{encounter_category[0].lower()}_encounters").slots
# Subcategory is for splitting fishing rods; land and water only have one subcategory
for subcategory in encounter_category[1]:
# Want to create locations per species, not per slot
# encounter_categories includes info on which slots belong to which subcategory
unique_species = []
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)
# Create a location for the species
for j, species_id in enumerate(unique_species):
encounter_location = PokemonEmeraldLocation(
world.player,
f"{region_name}{'_' + subcategory[0] if subcategory[0] is not None else ''}_{j + 1}",
None,
encounter_region
)
encounter_location.show_in_spoiler = False
# Add access rule
if subcategory[2] is not None:
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_region.locations.append(encounter_location)
# Add the new encounter region to the multiworld
world.multiworld.regions.append(encounter_region)
# Encounter region exists, just connect to it
region.connect(encounter_region, f"{region.name} -> {region_name}")
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)
@ -40,6 +111,9 @@ def create_regions(world: "PokemonEmeraldWorld") -> Dict[str, Region]:
regions[region_name] = new_region
connect_to_map_encounters(new_region, region_data.parent_map.name,
(region_data.has_grass, region_data.has_water, region_data.has_fishing))
for name, source, dest in connections:
regions[source].connect(regions[dest], name)

View File

@ -1,24 +1,110 @@
"""
Classes and functions related to creating a ROM patch
"""
import copy
import os
import pkgutil
from typing import TYPE_CHECKING, List, Tuple
from typing import TYPE_CHECKING, Dict, List, Tuple
import bsdiff4
from worlds.Files import APDeltaPatch
from settings import get_settings
from .data import PokemonEmeraldData, TrainerPokemonDataTypeEnum, data
from .data import TrainerPokemonDataTypeEnum, BASE_OFFSET, data
from .items import reverse_offset_item_value
from .options import RandomizeWildPokemon, RandomizeTrainerParties, EliteFourRequirement, NormanRequirement
from .pokemon import get_random_species
from .options import (RandomizeWildPokemon, RandomizeTrainerParties, EliteFourRequirement, NormanRequirement,
MatchTrainerLevels)
from .pokemon import HM_MOVES, get_random_move
from .util import bool_array_to_int, encode_string, get_easter_egg
if TYPE_CHECKING:
from . import PokemonEmeraldWorld
_LOOPING_MUSIC = [
"MUS_GSC_ROUTE38", "MUS_GSC_PEWTER", "MUS_ROUTE101", "MUS_ROUTE110", "MUS_ROUTE120", "MUS_ROUTE122",
"MUS_PETALBURG", "MUS_OLDALE", "MUS_GYM", "MUS_SURF", "MUS_PETALBURG_WOODS", "MUS_LILYCOVE_MUSEUM",
"MUS_OCEANIC_MUSEUM", "MUS_ENCOUNTER_GIRL", "MUS_ENCOUNTER_MALE", "MUS_ABANDONED_SHIP", "MUS_FORTREE",
"MUS_BIRCH_LAB", "MUS_B_TOWER_RS", "MUS_ENCOUNTER_SWIMMER", "MUS_CAVE_OF_ORIGIN", "MUS_ENCOUNTER_RICH",
"MUS_VERDANTURF", "MUS_RUSTBORO", "MUS_POKE_CENTER", "MUS_CAUGHT", "MUS_VICTORY_GYM_LEADER", "MUS_VICTORY_LEAGUE",
"MUS_VICTORY_WILD", "MUS_C_VS_LEGEND_BEAST", "MUS_ROUTE104", "MUS_ROUTE119", "MUS_CYCLING", "MUS_POKE_MART",
"MUS_LITTLEROOT", "MUS_MT_CHIMNEY", "MUS_ENCOUNTER_FEMALE", "MUS_LILYCOVE", "MUS_DESERT", "MUS_HELP",
"MUS_UNDERWATER", "MUS_VICTORY_TRAINER", "MUS_ENCOUNTER_MAY", "MUS_ENCOUNTER_INTENSE", "MUS_ENCOUNTER_COOL",
"MUS_ROUTE113", "MUS_ENCOUNTER_AQUA", "MUS_FOLLOW_ME", "MUS_ENCOUNTER_BRENDAN", "MUS_EVER_GRANDE",
"MUS_ENCOUNTER_SUSPICIOUS", "MUS_VICTORY_AQUA_MAGMA", "MUS_GAME_CORNER", "MUS_DEWFORD", "MUS_SAFARI_ZONE",
"MUS_VICTORY_ROAD", "MUS_AQUA_MAGMA_HIDEOUT", "MUS_SAILING", "MUS_MT_PYRE", "MUS_SLATEPORT", "MUS_MT_PYRE_EXTERIOR",
"MUS_SCHOOL", "MUS_HALL_OF_FAME", "MUS_FALLARBOR", "MUS_SEALED_CHAMBER", "MUS_CONTEST_WINNER", "MUS_CONTEST",
"MUS_ENCOUNTER_MAGMA", "MUS_ABNORMAL_WEATHER", "MUS_WEATHER_GROUDON", "MUS_SOOTOPOLIS", "MUS_HALL_OF_FAME_ROOM",
"MUS_TRICK_HOUSE", "MUS_ENCOUNTER_TWINS", "MUS_ENCOUNTER_ELITE_FOUR", "MUS_ENCOUNTER_HIKER", "MUS_CONTEST_LOBBY",
"MUS_ENCOUNTER_INTERVIEWER", "MUS_ENCOUNTER_CHAMPION", "MUS_B_FRONTIER", "MUS_B_ARENA", "MUS_B_PYRAMID",
"MUS_B_PYRAMID_TOP", "MUS_B_PALACE", "MUS_B_TOWER", "MUS_B_DOME", "MUS_B_PIKE", "MUS_B_FACTORY", "MUS_VS_RAYQUAZA",
"MUS_VS_FRONTIER_BRAIN", "MUS_VS_MEW", "MUS_B_DOME_LOBBY", "MUS_VS_WILD", "MUS_VS_AQUA_MAGMA", "MUS_VS_TRAINER",
"MUS_VS_GYM_LEADER", "MUS_VS_CHAMPION", "MUS_VS_REGI", "MUS_VS_KYOGRE_GROUDON", "MUS_VS_RIVAL", "MUS_VS_ELITE_FOUR",
"MUS_VS_AQUA_MAGMA_LEADER", "MUS_RG_FOLLOW_ME", "MUS_RG_GAME_CORNER", "MUS_RG_ROCKET_HIDEOUT", "MUS_RG_GYM",
"MUS_RG_CINNABAR", "MUS_RG_LAVENDER", "MUS_RG_CYCLING", "MUS_RG_ENCOUNTER_ROCKET", "MUS_RG_ENCOUNTER_GIRL",
"MUS_RG_ENCOUNTER_BOY", "MUS_RG_HALL_OF_FAME", "MUS_RG_VIRIDIAN_FOREST", "MUS_RG_MT_MOON", "MUS_RG_POKE_MANSION",
"MUS_RG_ROUTE1", "MUS_RG_ROUTE24", "MUS_RG_ROUTE3", "MUS_RG_ROUTE11", "MUS_RG_VICTORY_ROAD", "MUS_RG_VS_GYM_LEADER",
"MUS_RG_VS_TRAINER", "MUS_RG_VS_WILD", "MUS_RG_VS_CHAMPION", "MUS_RG_PALLET", "MUS_RG_OAK_LAB", "MUS_RG_OAK",
"MUS_RG_POKE_CENTER", "MUS_RG_SS_ANNE", "MUS_RG_SURF", "MUS_RG_POKE_TOWER", "MUS_RG_SILPH", "MUS_RG_FUCHSIA",
"MUS_RG_CELADON", "MUS_RG_VICTORY_TRAINER", "MUS_RG_VICTORY_WILD", "MUS_RG_VICTORY_GYM_LEADER", "MUS_RG_VERMILLION",
"MUS_RG_PEWTER", "MUS_RG_ENCOUNTER_RIVAL", "MUS_RG_RIVAL_EXIT", "MUS_RG_CAUGHT", "MUS_RG_POKE_JUMP",
"MUS_RG_UNION_ROOM", "MUS_RG_NET_CENTER", "MUS_RG_MYSTERY_GIFT", "MUS_RG_BERRY_PICK", "MUS_RG_SEVII_CAVE",
"MUS_RG_TEACHY_TV_SHOW", "MUS_RG_SEVII_ROUTE", "MUS_RG_SEVII_DUNGEON", "MUS_RG_SEVII_123", "MUS_RG_SEVII_45",
"MUS_RG_SEVII_67", "MUS_RG_VS_DEOXYS", "MUS_RG_VS_MEWTWO", "MUS_RG_VS_LEGEND", "MUS_RG_ENCOUNTER_GYM_LEADER",
"MUS_RG_ENCOUNTER_DEOXYS", "MUS_RG_TRAINER_TOWER", "MUS_RG_SLOW_PALLET", "MUS_RG_TEACHY_TV_MENU",
]
_FANFARES: Dict[str, int] = {
"MUS_LEVEL_UP": 80,
"MUS_OBTAIN_ITEM": 160,
"MUS_EVOLVED": 220,
"MUS_OBTAIN_TMHM": 220,
"MUS_HEAL": 160,
"MUS_OBTAIN_BADGE": 340,
"MUS_MOVE_DELETED": 180,
"MUS_OBTAIN_BERRY": 120,
"MUS_AWAKEN_LEGEND": 710,
"MUS_SLOTS_JACKPOT": 250,
"MUS_SLOTS_WIN": 150,
"MUS_TOO_BAD": 160,
"MUS_RG_POKE_FLUTE": 450,
"MUS_RG_OBTAIN_KEY_ITEM": 170,
"MUS_RG_DEX_RATING": 196,
"MUS_OBTAIN_B_POINTS": 313,
"MUS_OBTAIN_SYMBOL": 318,
"MUS_REGISTER_MATCH_CALL": 135,
}
CAVE_EVENT_NAME_TO_ID = {
"TERRA_CAVE_ROUTE_114_1": 1,
"TERRA_CAVE_ROUTE_114_2": 2,
"TERRA_CAVE_ROUTE_115_1": 3,
"TERRA_CAVE_ROUTE_115_2": 4,
"TERRA_CAVE_ROUTE_116_1": 5,
"TERRA_CAVE_ROUTE_116_2": 6,
"TERRA_CAVE_ROUTE_118_1": 7,
"TERRA_CAVE_ROUTE_118_2": 8,
"MARINE_CAVE_ROUTE_105_1": 9,
"MARINE_CAVE_ROUTE_105_2": 10,
"MARINE_CAVE_ROUTE_125_1": 11,
"MARINE_CAVE_ROUTE_125_2": 12,
"MARINE_CAVE_ROUTE_127_1": 13,
"MARINE_CAVE_ROUTE_127_2": 14,
"MARINE_CAVE_ROUTE_129_1": 15,
"MARINE_CAVE_ROUTE_129_2": 16,
}
def _set_bytes_le(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
class PokemonEmeraldDeltaPatch(APDeltaPatch):
game = "Pokemon Emerald"
hash = "605b89b67018abcea91e693a4dd25be3"
@ -30,60 +116,128 @@ class PokemonEmeraldDeltaPatch(APDeltaPatch):
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:
def create_patch(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(
if world.options.free_fly_location:
_set_bytes_le(
patched_rom,
data.rom_addresses["gArchipelagoOptions"] + 0x16,
data.rom_addresses["gArchipelagoOptions"] + 0x20,
1,
world.free_fly_location_id
)
location_info: List[Tuple[int, int, str]] = []
for location in world.multiworld.get_locations(world.player):
if location.address is None:
continue
if location.item and location.item.player == world.player:
_set_bytes_little_endian(
if location.item is None:
continue
# Set local item values
if not world.options.remote_items and location.item.player == world.player:
if type(location.item_address) is int:
_set_bytes_le(
patched_rom,
location.rom_address,
location.item_address,
2,
reverse_offset_item_value(location.item.code)
)
elif type(location.item_address) is list:
for address in location.item_address:
_set_bytes_le(patched_rom, address, 2, reverse_offset_item_value(location.item.code))
else:
_set_bytes_little_endian(
if type(location.item_address) is int:
_set_bytes_le(
patched_rom,
location.rom_address,
location.item_address,
2,
data.constants["ITEM_ARCHIPELAGO_PROGRESSION"]
)
elif type(location.item_address) is list:
for address in location.item_address:
_set_bytes_le(patched_rom, address, 2, data.constants["ITEM_ARCHIPELAGO_PROGRESSION"])
# Creates a list of item information to store in tables later. Those tables are used to display the item and
# player name in a text box. In the case of not enough space, the game will default to "found an ARCHIPELAGO
# ITEM"
location_info.append((location.address - BASE_OFFSET, location.item.player, location.item.name))
if world.options.trainersanity:
# Duplicate entries for rival fights
# For each of the 5 fights, there are 6 variations that have to be accounted for (for starters * genders)
# The Brendan Mudkip is used as a proxy in the rest of the AP code
for locale in ["ROUTE_103", "ROUTE_110", "ROUTE_119", "RUSTBORO", "LILYCOVE"]:
location = world.multiworld.get_location(data.locations[f"TRAINER_BRENDAN_{locale}_MUDKIP_REWARD"].label, world.player)
alternates = [
f"TRAINER_BRENDAN_{locale}_TREECKO",
f"TRAINER_BRENDAN_{locale}_TORCHIC",
f"TRAINER_MAY_{locale}_MUDKIP",
f"TRAINER_MAY_{locale}_TREECKO",
f"TRAINER_MAY_{locale}_TORCHIC",
]
location_info.extend((
data.constants["TRAINER_FLAGS_START"] + data.constants[trainer],
location.item.player,
location.item.name
) for trainer in alternates)
player_name_ids: Dict[str, int] = {world.multiworld.player_name[world.player]: 0}
item_name_offsets: Dict[str, int] = {}
next_item_name_offset = 0
for i, (flag, item_player, item_name) in enumerate(sorted(location_info, key=lambda t: t[0])):
# The player's own items are still set in the table with the value 0 to indicate the game should not show any
# message (the message for receiving an item will pop up when the client eventually gives it to them).
# In race mode, no item location data is included, and only recieved (or own) items will show any text box.
if item_player == world.player or world.multiworld.is_race:
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 0, 2, flag)
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 2, 2, 0)
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 4, 1, 0)
else:
player_name = world.multiworld.player_name[item_player]
if player_name not in player_name_ids:
# Only space for 50 player names
if len(player_name_ids) >= 50:
continue
player_name_ids[player_name] = len(player_name_ids)
for j, b in enumerate(encode_string(player_name, 17)):
_set_bytes_le(
patched_rom,
data.rom_addresses["gArchipelagoPlayerNames"] + (player_name_ids[player_name] * 17) + j,
1,
b
)
if item_name not in item_name_offsets:
if len(item_name) > 35:
item_name = item_name[:34] + ""
# Only 36 * 250 bytes for item names
if next_item_name_offset + len(item_name) + 1 > 36 * 250:
continue
item_name_offsets[item_name] = next_item_name_offset
next_item_name_offset += len(item_name) + 1
for j, b in enumerate(encode_string(item_name) + b"\xFF"):
_set_bytes_le(
patched_rom,
data.rom_addresses["gArchipelagoItemNames"] + (item_name_offsets[item_name]) + j,
1,
b
)
# There should always be enough space for one entry per location
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 0, 2, flag)
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 2, 2, item_name_offsets[item_name])
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoNameTable"] + (i * 5) + 4, 1, player_name_ids[player_name])
easter_egg = get_easter_egg(world.options.easter_egg.value)
# Set start inventory
start_inventory = world.options.start_inventory.value.copy()
@ -128,28 +282,34 @@ def generate_output(world: "PokemonEmeraldWorld", output_directory: str) -> None
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_bytes_le(patched_rom, address + 0, 2, item)
_set_bytes_le(patched_rom, address + 2, 2, slot[1])
# Set species data
_set_species_info(world, patched_rom)
_set_species_info(world, patched_rom, easter_egg)
# 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)
if world.options.trainer_parties != RandomizeTrainerParties.option_vanilla or easter_egg[0] == 2:
_set_opponents(world, patched_rom, easter_egg)
# Set static pokemon
_set_static_encounters(world, patched_rom)
# Set legendary pokemon
_set_legendary_encounters(world, patched_rom)
# Set misc pokemon
_set_misc_pokemon(world, patched_rom)
# Set starters
_set_starters(world, patched_rom)
# Set TM moves
_set_tm_moves(world, patched_rom)
_set_tm_moves(world, patched_rom, easter_egg)
# Randomize move tutor moves
_randomize_move_tutor_moves(world, patched_rom, easter_egg)
# Set TM/HM compatibility
_set_tmhm_compatibility(world, patched_rom)
@ -160,97 +320,164 @@ def generate_output(world: "PokemonEmeraldWorld", output_directory: str) -> None
# 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;
# /* 0x00 */ u16 birchPokemon;
# /* 0x02 */ bool8 advanceTextWithHoldA;
# /* 0x03 */ u8 receivedItemMessageFilter; // 0 = Show All; 1 = Show Progression Only; 2 = Show None
# /* 0x04 */ bool8 betterShopsEnabled;
# /* 0x05 */ bool8 reusableTms;
# /* 0x06 */ bool8 guaranteedCatch;
# /* 0x07 */ bool8 purgeSpinners;
# /* 0x08 */ bool8 areTrainersBlind;
# /* 0x09 */ u16 expMultiplierNumerator;
# /* 0x0B */ u16 expMultiplierDenominator;
# /* 0x0D */ bool8 matchTrainerLevels;
# /* 0x0E */ s8 matchTrainerLevelBonus;
# /* 0x0F */ bool8 eliteFourRequiresGyms;
# /* 0x10 */ u8 eliteFourRequiredCount;
# /* 0x11 */ bool8 normanRequiresGyms;
# /* 0x12 */ u8 normanRequiredCount;
# /* 0x13 */ u8 startingBadges;
# /* 0x14 */ u32 hmTotalBadgeRequirements;
# /* 0x18 */ u8 hmSpecificBadgeRequirements[8];
# /* 0x20 */ u8 freeFlyLocation;
# /* 0x21 */ u8 terraCaveLocationId:4;
# u8 marineCaveLocationId:4;
# /* 0x22 */ bool8 addRoute115Boulders;
# /* 0x23 */ bool8 addBumpySlopes;
# /* 0x24 */ bool8 modifyRoute118;
# /* 0x25 */ u16 removedBlockers;
# /* 0x27 */ bool8 berryTreesRandomized;
# /* 0x28 */ bool8 isDexsanity;
# /* 0x29 */ bool8 isTrainersanity;
# /* 0x2A */ bool8 isWarpRando;
# /* 0x2B */ u8 activeEasterEgg;
# /* 0x2C */ bool8 normalizeEncounterRates;
# /* 0x2D */ bool8 allowWonderTrading;
# /* 0x2E */ u16 matchTrainerLevelMultiplierNumerator;
# /* 0x30 */ u16 matchTrainerLevelMultiplierDenominator;
# /* 0x32 */ bool8 allowSkippingFanfares;
# };
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(
_set_bytes_le(
patched_rom,
options_address + 0x08,
options_address + 0x00,
2,
get_random_species(world.random, data.species).species_id
world.random.choice(list(data.species.keys()))
)
# 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 hold A to advance text
_set_bytes_le(patched_rom, options_address + 0x02, 1, 1 if world.options.turbo_a else 0)
# 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_bytes_le(patched_rom, options_address + 0x03, 1, world.options.receive_item_messages.value)
# Set better shops
_set_bytes_le(patched_rom, options_address + 0x04, 1, 1 if world.options.better_shops else 0)
# 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_bytes_le(patched_rom, options_address + 0x05, 1, 1 if world.options.reusable_tms_tutors else 0)
# Set guaranteed catch
_set_bytes_le(patched_rom, options_address + 0x06, 1, 1 if world.options.guaranteed_catch else 0)
# Set purge spinners
_set_bytes_le(patched_rom, options_address + 0x07, 1, 1 if world.options.purge_spinners else 0)
# Set blind trainers
_set_bytes_le(patched_rom, options_address + 0x08, 1, 1 if world.options.blind_trainers else 0)
# Set exp modifier
_set_bytes_le(patched_rom, options_address + 0x09, 2, min(max(world.options.exp_modifier.value, 0), 2**16 - 1))
_set_bytes_le(patched_rom, options_address + 0x0B, 2, 100)
# Set match trainer levels
_set_bytes_le(patched_rom, options_address + 0x0D, 1, 1 if world.options.match_trainer_levels else 0)
# Set match trainer levels bonus
if world.options.match_trainer_levels == MatchTrainerLevels.option_additive:
match_trainer_levels_bonus = max(min(world.options.match_trainer_levels_bonus.value, 100), -100)
_set_bytes_le(patched_rom, options_address + 0x0E, 1, match_trainer_levels_bonus) # Works with negatives
elif world.options.match_trainer_levels == MatchTrainerLevels.option_multiplicative:
_set_bytes_le(patched_rom, options_address + 0x2E, 2, world.options.match_trainer_levels_bonus.value + 100)
_set_bytes_le(patched_rom, options_address + 0x30, 2, 100)
# Set elite four requirement
_set_bytes_le(
patched_rom,
options_address + 0x0F,
1,
1 if world.options.elite_four_requirement == EliteFourRequirement.option_gyms else 0
)
# Set elite four count
_set_bytes_le(patched_rom, options_address + 0x10, 1, min(max(world.options.elite_four_count.value, 0), 8))
# Set norman requirement
_set_bytes_le(
patched_rom,
options_address + 0x11,
1,
1 if world.options.norman_requirement == NormanRequirement.option_gyms else 0
)
# Set norman count
_set_bytes_le(patched_rom, options_address + 0x12, 1, min(max(world.options.norman_count.value, 0), 8))
# Set starting badges
_set_bytes_le(patched_rom, options_address + 0x13, 1, starting_badges)
# Set HM badge requirements
field_move_order = [
"HM01 Cut",
"HM05 Flash",
"HM06 Rock Smash",
"HM04 Strength",
"HM03 Surf",
"HM02 Fly",
"HM08 Dive",
"HM07 Waterfall",
]
badge_to_bit = {
"Stone Badge": 1 << 0,
"Knuckle Badge": 1 << 1,
"Dynamo Badge": 1 << 2,
"Heat Badge": 1 << 3,
"Balance Badge": 1 << 4,
"Feather Badge": 1 << 5,
"Mind Badge": 1 << 6,
"Rain Badge": 1 << 7,
}
# Number of badges
# Uses 4 bits per HM. 0-8 means it's a valid requirement, otherwise use specific badges.
hm_badge_counts = 0
for i, hm in enumerate(field_move_order):
hm_badge_counts |= (world.hm_requirements[hm] if isinstance(world.hm_requirements[hm], int) else 0xF) << (i * 4)
_set_bytes_le(patched_rom, options_address + 0x14, 4, hm_badge_counts)
# Specific badges
for i, hm in enumerate(field_move_order):
if isinstance(world.hm_requirements, list):
bitfield = 0
for badge in world.hm_requirements:
bitfield |= badge_to_bit[badge]
_set_bytes_le(patched_rom, options_address + 0x18 + i, 1, bitfield)
# Set terra/marine cave locations
terra_cave_id = CAVE_EVENT_NAME_TO_ID[world.multiworld.get_location("TERRA_CAVE_LOCATION", world.player).item.name]
marine_cave_id = CAVE_EVENT_NAME_TO_ID[world.multiworld.get_location("MARINE_CAVE_LOCATION", world.player).item.name]
_set_bytes_le(patched_rom, options_address + 0x21, 1, terra_cave_id | (marine_cave_id << 4))
# 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_bytes_le(patched_rom, options_address + 0x22, 1, 1 if world.options.extra_boulders else 0)
# Swap route 115 layout if bumpy slope enabled
_set_bytes_le(patched_rom, options_address + 0x23, 1, 1 if world.options.extra_bumpy_slope else 0)
# Swap route 115 layout if bumpy slope enabled
_set_bytes_le(patched_rom, options_address + 0x24, 1, 1 if world.options.modify_118 else 0)
# Set removed blockers
removed_roadblocks = world.options.remove_roadblocks.value
@ -262,12 +489,76 @@ def generate_output(world: "PokemonEmeraldWorld", output_directory: str) -> None
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_bytes_le(patched_rom, options_address + 0x25, 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)
# Mark berry trees as randomized
_set_bytes_le(patched_rom, options_address + 0x27, 1, 1 if world.options.berry_trees else 0)
# Mark dexsanity as enabled
_set_bytes_le(patched_rom, options_address + 0x28, 1, 1 if world.options.dexsanity else 0)
# Mark trainersanity as enabled
_set_bytes_le(patched_rom, options_address + 0x29, 1, 1 if world.options.trainersanity else 0)
# Set easter egg data
_set_bytes_le(patched_rom, options_address + 0x2B, 1, easter_egg[0])
# Set normalize encounter rates
_set_bytes_le(patched_rom, options_address + 0x2C, 1, 1 if world.options.normalize_encounter_rates else 0)
# Set allow wonder trading
_set_bytes_le(patched_rom, options_address + 0x2D, 1, 1 if world.options.enable_wonder_trading else 0)
# Set allowed to skip fanfares
_set_bytes_le(patched_rom, options_address + 0x32, 1, 1 if world.options.fanfares else 0)
if easter_egg[0] == 2:
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (easter_egg[1] * 12) + 4, 1, 50)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_CUT"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_FLY"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_SURF"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_STRENGTH"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_FLASH"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_ROCK_SMASH"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_WATERFALL"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_DIVE"] * 12) + 4, 1, 1)
_set_bytes_le(patched_rom, data.rom_addresses["gBattleMoves"] + (data.constants["MOVE_DIG"] * 12) + 4, 1, 1)
# Set slot auth
for i, byte in enumerate(world.auth):
_set_bytes_le(patched_rom, data.rom_addresses["gArchipelagoInfo"] + i, 1, byte)
# Randomize music
if world.options.music:
# The "randomized sound table" is a patchboard that redirects sounds just before they get played
randomized_looping_music = copy.copy(_LOOPING_MUSIC)
world.random.shuffle(randomized_looping_music)
for original_music, randomized_music in zip(_LOOPING_MUSIC, randomized_looping_music):
_set_bytes_le(
patched_rom,
data.rom_addresses["gRandomizedSoundTable"] + (data.constants[original_music] * 2),
2,
data.constants[randomized_music]
)
# Randomize fanfares
if world.options.fanfares:
# Shuffle the lists, pair new tracks with original tracks, set the new track ids, and set new fanfare durations
randomized_fanfares = [fanfare_name for fanfare_name in _FANFARES]
world.random.shuffle(randomized_fanfares)
for i, fanfare_pair in enumerate(zip(_FANFARES.keys(), randomized_fanfares)):
_set_bytes_le(
patched_rom,
data.rom_addresses["gRandomizedSoundTable"] + (data.constants[fanfare_pair[0]] * 2),
2,
data.constants[fanfare_pair[1]]
)
_set_bytes_le(
patched_rom,
data.rom_addresses["sFanfares"] + (i * 4) + 2,
2,
_FANFARES[fanfare_pair[1]]
)
# Write Output
out_file_name = world.multiworld.get_out_file_name_base(world.player)
@ -275,7 +566,8 @@ def generate_output(world: "PokemonEmeraldWorld", output_directory: str) -> None
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)
player_name=world.multiworld.get_player_name(world.player),
patched_path=output_path)
patch.write()
os.unlink(output_path)
@ -288,15 +580,6 @@ def get_base_rom_as_bytes() -> bytes:
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
@ -306,33 +589,38 @@ def _set_encounter_tables(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
species_id: 0x02 bytes
}
"""
for map_data in world.modified_maps:
for map_data in world.modified_maps.values():
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)
address = table.address + 2 + (4 * i)
_set_bytes_le(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])
def _set_species_info(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tuple[int, int]) -> None:
for species in world.modified_species.values():
_set_bytes_le(rom, species.address + 6, 1, species.types[0])
_set_bytes_le(rom, species.address + 7, 1, species.types[1])
_set_bytes_le(rom, species.address + 8, 1, species.catch_rate)
_set_bytes_le(rom, species.address + 22, 1, species.abilities[0])
_set_bytes_le(rom, species.address + 23, 1, species.abilities[1])
if easter_egg[0] == 3:
_set_bytes_le(rom, species.address + 22, 1, easter_egg[1])
_set_bytes_le(rom, species.address + 23, 1, easter_egg[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)
if easter_egg[0] == 2:
level_move = learnset_move.level << 9 | easter_egg[1]
_set_bytes_le(rom, species.learnset_address + (i * 2), 2, level_move)
def _set_opponents(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
def _set_opponents(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tuple[int, int]) -> None:
for trainer in world.modified_trainers:
party_address = trainer.party.rom_address
party_address = trainer.party.address
pokemon_data_size: int
if trainer.party.pokemon_data_type in {TrainerPokemonDataTypeEnum.NO_ITEM_DEFAULT_MOVES, TrainerPokemonDataTypeEnum.ITEM_DEFAULT_MOVES}:
@ -344,36 +632,53 @@ def _set_opponents(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
pokemon_address = party_address + (i * pokemon_data_size)
# Replace species
_set_bytes_little_endian(rom, pokemon_address + 0x04, 2, pokemon.species_id)
_set_bytes_le(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])
if easter_egg[0] == 2:
_set_bytes_le(rom, pokemon_address + 0x06, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x08, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0A, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0C, 2, easter_egg[1])
else:
_set_bytes_le(rom, pokemon_address + 0x06, 2, pokemon.moves[0])
_set_bytes_le(rom, pokemon_address + 0x08, 2, pokemon.moves[1])
_set_bytes_le(rom, pokemon_address + 0x0A, 2, pokemon.moves[2])
_set_bytes_le(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])
if easter_egg[0] == 2:
_set_bytes_le(rom, pokemon_address + 0x08, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0A, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0C, 2, easter_egg[1])
_set_bytes_le(rom, pokemon_address + 0x0E, 2, easter_egg[1])
else:
_set_bytes_le(rom, pokemon_address + 0x08, 2, pokemon.moves[0])
_set_bytes_le(rom, pokemon_address + 0x0A, 2, pokemon.moves[1])
_set_bytes_le(rom, pokemon_address + 0x0C, 2, pokemon.moves[2])
_set_bytes_le(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_legendary_encounters(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
for encounter in world.modified_legendary_encounters:
_set_bytes_le(rom, encounter.address, 2, encounter.species_id)
def _set_misc_pokemon(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
for encounter in world.modified_misc_pokemon:
_set_bytes_le(rom, encounter.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)
_set_bytes_le(rom, address + 0, 2, starter_1)
_set_bytes_le(rom, address + 2, 2, starter_2)
_set_bytes_le(rom, address + 4, 2, starter_3)
def _set_tm_moves(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
def _set_tm_moves(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tuple[int, int]) -> None:
tmhm_list_address = data.rom_addresses["sTMHMMoves"]
for i, move in enumerate(world.modified_tmhm_moves):
@ -381,15 +686,16 @@ def _set_tm_moves(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
if i >= 50:
break
_set_bytes_little_endian(rom, tmhm_list_address + (i * 2), 2, move)
_set_bytes_le(rom, tmhm_list_address + (i * 2), 2, move)
if easter_egg[0] == 2:
_set_bytes_le(rom, tmhm_list_address + (i * 2), 2, easter_egg[1])
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)
for species in world.modified_species.values():
_set_bytes_le(rom, learnsets_address + (species.species_id * 8), 8, species.tm_hm_compatibility)
def _randomize_opponent_battle_type(world: "PokemonEmeraldWorld", rom: bytearray) -> None:
@ -403,18 +709,45 @@ def _randomize_opponent_battle_type(world: "PokemonEmeraldWorld", rom: bytearray
}
for trainer_data in data.trainers:
if trainer_data.battle_script_rom_address != 0 and len(trainer_data.party.pokemon) > 1:
if trainer_data.script_address != 0 and len(trainer_data.party.pokemon) > 1:
original_battle_type = rom[trainer_data.script_address + 1]
if original_battle_type in battle_type_map: # Don't touch anything other than regular single battles
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)
_set_bytes_le(rom, trainer_data.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(
_set_bytes_le(
rom,
trainer_data.battle_script_rom_address + 1,
trainer_data.script_address + 1,
1,
battle_type_map[original_battle_type]
)
def _randomize_move_tutor_moves(world: "PokemonEmeraldWorld", rom: bytearray, easter_egg: Tuple[int, int]) -> None:
if easter_egg[0] == 2:
for i in range(30):
_set_bytes_le(rom, data.rom_addresses["gTutorMoves"] + (i * 2), 2, easter_egg[1])
else:
if world.options.tm_tutor_moves:
new_tutor_moves = []
for i in range(30):
new_move = get_random_move(world.random, set(new_tutor_moves) | world.blacklisted_moves | HM_MOVES)
new_tutor_moves.append(new_move)
_set_bytes_le(rom, data.rom_addresses["gTutorMoves"] + (i * 2), 2, new_move)
# Always set Fortree move tutor to Dig
_set_bytes_le(rom, data.rom_addresses["gTutorMoves"] + (24 * 2), 2, data.constants["MOVE_DIG"])
# Modify compatibility
if world.options.tm_tutor_compatibility.value != -1:
for species in data.species.values():
_set_bytes_le(
rom,
data.rom_addresses["sTutorLearnsets"] + (species.species_id * 4),
4,
bool_array_to_int([world.random.randrange(0, 100) < world.options.tm_tutor_compatibility.value for _ in range(32)])
)

File diff suppressed because it is too large Load Diff

View File

@ -5,35 +5,41 @@ duplicate claims and give warnings for unused and unignored locations or warps.
import logging
from typing import List
from .data import data
from .data import load_json_data, 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",
_IGNORABLE_LOCATIONS = frozenset({
"HIDDEN_ITEM_TRICK_HOUSE_NUGGET", # Is permanently mssiable and has special behavior that sets the flag early
# 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",
# Duplicate rival fights. All variations are represented by the Brandon + Mudkip version
"TRAINER_BRENDAN_ROUTE_103_TREECKO_REWARD",
"TRAINER_BRENDAN_ROUTE_103_TORCHIC_REWARD",
"TRAINER_MAY_ROUTE_103_MUDKIP_REWARD",
"TRAINER_MAY_ROUTE_103_TREECKO_REWARD",
"TRAINER_MAY_ROUTE_103_TORCHIC_REWARD",
"TRAINER_BRENDAN_ROUTE_110_TREECKO_REWARD",
"TRAINER_BRENDAN_ROUTE_110_TORCHIC_REWARD",
"TRAINER_MAY_ROUTE_110_MUDKIP_REWARD",
"TRAINER_MAY_ROUTE_110_TREECKO_REWARD",
"TRAINER_MAY_ROUTE_110_TORCHIC_REWARD",
"TRAINER_BRENDAN_ROUTE_119_TREECKO_REWARD",
"TRAINER_BRENDAN_ROUTE_119_TORCHIC_REWARD",
"TRAINER_MAY_ROUTE_119_MUDKIP_REWARD",
"TRAINER_MAY_ROUTE_119_TREECKO_REWARD",
"TRAINER_MAY_ROUTE_119_TORCHIC_REWARD",
"TRAINER_BRENDAN_RUSTBORO_TREECKO_REWARD",
"TRAINER_BRENDAN_RUSTBORO_TORCHIC_REWARD",
"TRAINER_MAY_RUSTBORO_MUDKIP_REWARD",
"TRAINER_MAY_RUSTBORO_TREECKO_REWARD",
"TRAINER_MAY_RUSTBORO_TORCHIC_REWARD",
"TRAINER_BRENDAN_LILYCOVE_TREECKO_REWARD",
"TRAINER_BRENDAN_LILYCOVE_TORCHIC_REWARD",
"TRAINER_MAY_LILYCOVE_MUDKIP_REWARD",
"TRAINER_MAY_LILYCOVE_TREECKO_REWARD",
"TRAINER_MAY_LILYCOVE_TORCHIC_REWARD",
})
# Event islands
"HIDDEN_ITEM_NAVEL_ROCK_TOP_SACRED_ASH"
}
_ignorable_warps = {
_IGNORABLE_WARPS = frozenset({
# 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!",
@ -72,75 +78,16 @@ _ignorable_warps = {
"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!",
@ -153,8 +100,6 @@ _ignorable_warps = {
"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!",
@ -164,6 +109,10 @@ _ignorable_warps = {
"MAP_UNDERWATER_ROUTE129:0/MAP_UNDERWATER_MARINE_CAVE:0!",
"MAP_UNDERWATER_ROUTE129:1/MAP_UNDERWATER_MARINE_CAVE:0!",
# Altering Cave
"MAP_ALTERING_CAVE:0/MAP_ROUTE103:0",
"MAP_ROUTE103:0/MAP_ALTERING_CAVE:0",
# Event islands
"MAP_BIRTH_ISLAND_EXTERIOR:0/MAP_BIRTH_ISLAND_HARBOR:0",
"MAP_BIRTH_ISLAND_HARBOR:0/MAP_BIRTH_ISLAND_EXTERIOR:0",
@ -294,11 +243,17 @@ _ignorable_warps = {
"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!"
}
"MAP_LILYCOVE_CITY_UNUSED_MART:0,1/MAP_LILYCOVE_CITY:0!",
})
def validate_regions() -> bool:
"""
Verifies that Emerald's data doesn't have duplicate or missing
regions/warps/locations. Meant to catch problems during development like
forgetting to add a new location or incorrectly splitting a region.
"""
extracted_data_json = load_json_data("extracted_data.json")
error_messages: List[str] = []
warn_messages: List[str] = []
failed = False
@ -319,7 +274,7 @@ def validate_regions() -> bool:
# Check warps
for warp_source, warp_dest in data.warp_map.items():
if warp_source in _ignorable_warps:
if warp_source in _IGNORABLE_WARPS:
continue
if warp_dest is None:
@ -335,8 +290,8 @@ def validate_regions() -> bool:
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:
for location_name in extracted_data_json["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()

View File

@ -21,25 +21,25 @@ class TestScorchedSlabPond(PokemonEmeraldTestBase):
self.collect_by_name(["S.S. Ticket", "Letter", "Stone Badge", "HM01 Cut"])
self.assertTrue(self.can_reach_region("REGION_ROUTE120/NORTH"))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_NEST_BALL")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM11")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM_SUNNY_DAY")))
def test_with_surf(self) -> None:
self.collect_by_name(["S.S. Ticket", "Letter", "Stone Badge", "HM01 Cut", "HM03 Surf", "Balance Badge"])
self.assertTrue(self.can_reach_region("REGION_ROUTE120/NORTH"))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_NEST_BALL")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM11")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM_SUNNY_DAY")))
def test_with_scope(self) -> None:
self.collect_by_name(["S.S. Ticket", "Letter", "Stone Badge", "HM01 Cut", "Devon Scope"])
self.assertTrue(self.can_reach_region("REGION_ROUTE120/NORTH"))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_NEST_BALL")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM11")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM_SUNNY_DAY")))
def test_with_both(self) -> None:
self.collect_by_name(["S.S. Ticket", "Letter", "Stone Badge", "HM01 Cut", "Devon Scope", "HM03 Surf", "Balance Badge"])
self.assertTrue(self.can_reach_region("REGION_ROUTE120/NORTH"))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_NEST_BALL")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM11")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_SCORCHED_SLAB_TM_SUNNY_DAY")))
class TestSurf(PokemonEmeraldTestBase):
@ -55,7 +55,7 @@ class TestSurf(PokemonEmeraldTestBase):
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
self.assertFalse(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/EAST_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"))
@ -66,7 +66,7 @@ class TestSurf(PokemonEmeraldTestBase):
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
self.assertTrue(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/EAST_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")))
@ -88,17 +88,16 @@ class TestFreeFly(PokemonEmeraldTestBase):
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")))
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_TM_BRICK_BREAK")))
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")))
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_TM_BRICK_BREAK")))
class TestFerry(PokemonEmeraldTestBase):
options = {
"npc_gifts": Toggle.option_true,
"enable_ferry": Toggle.option_true
"npc_gifts": Toggle.option_true
}
def test_inaccessible_with_no_items(self) -> None:

View File

@ -1,6 +1,115 @@
from typing import List
import orjson
from typing import Any, Dict, List, Optional, Tuple, Iterable
from .data import data
from .data import NATIONAL_ID_TO_SPECIES_ID, data
CHARACTER_DECODING_MAP = {
0x00: " ", 0x01: "À", 0x02: "Á", 0x03: "Â", 0x04: "Ç",
0x05: "È", 0x06: "É", 0x07: "Ê", 0x08: "Ë", 0x09: "Ì",
0x0B: "Î", 0x0C: "Ï", 0x0D: "Ò", 0x0E: "Ó", 0x0F: "Ô",
0x10: "Œ", 0x11: "Ù", 0x12: "Ú", 0x13: "Û", 0x14: "Ñ",
0x15: "ß", 0x16: "à", 0x17: "á", 0x19: "ç", 0x1A: "è",
0x1B: "é", 0x1C: "ê", 0x1D: "ë", 0x1E: "ì", 0x20: "î",
0x21: "ï", 0x22: "ò", 0x23: "ó", 0x24: "ô", 0x25: "œ",
0x26: "ù", 0x27: "ú", 0x28: "û", 0x29: "ñ", 0x2A: "°",
0x2B: "ª", 0x2D: "&", 0x2E: "+", 0x35: "=", 0x36: ";",
0x50: "", 0x51: "¿", 0x52: "¡", 0x5A: "Í", 0x5B: "%",
0x5C: "(", 0x5D: ")", 0x68: "â", 0x6F: "í", 0x79: "",
0x7A: "", 0x7B: "", 0x7C: "", 0x7D: "*", 0x84: "",
0x85: "<", 0x86: ">", 0xA1: "0", 0xA2: "1", 0xA3: "2",
0xA4: "3", 0xA5: "4", 0xA6: "5", 0xA7: "6", 0xA8: "7",
0xA9: "8", 0xAA: "9", 0xAB: "!", 0xAC: "?", 0xAD: ".",
0xAE: "-", 0xB0: "", 0xB1: "", 0xB2: "", 0xB3: "",
0xB4: "", 0xB5: "", 0xB6: "", 0xB8: ",", 0xB9: "×",
0xBA: "/", 0xBB: "A", 0xBC: "B", 0xBD: "C", 0xBE: "D",
0xBF: "E", 0xC0: "F", 0xC1: "G", 0xC2: "H", 0xC3: "I",
0xC4: "J", 0xC5: "K", 0xC6: "L", 0xC7: "M", 0xC8: "N",
0xC9: "O", 0xCA: "P", 0xCB: "Q", 0xCC: "R", 0xCD: "S",
0xCE: "T", 0xCF: "U", 0xD0: "V", 0xD1: "W", 0xD2: "X",
0xD3: "Y", 0xD4: "Z", 0xD5: "a", 0xD6: "b", 0xD7: "c",
0xD8: "d", 0xD9: "e", 0xDA: "f", 0xDB: "g", 0xDC: "h",
0xDD: "i", 0xDE: "j", 0xDF: "k", 0xE0: "l", 0xE1: "m",
0xE2: "n", 0xE3: "o", 0xE4: "p", 0xE5: "q", 0xE6: "r",
0xE7: "s", 0xE8: "t", 0xE9: "u", 0xEA: "v", 0xEB: "w",
0xEC: "x", 0xED: "y", 0xEE: "z", 0xEF: "", 0xF0: ":",
}
CHARACTER_ENCODING_MAP = {value: key for key, value in CHARACTER_DECODING_MAP.items()}
CHARACTER_ENCODING_MAP.update({
"'": CHARACTER_ENCODING_MAP[""],
"\"": CHARACTER_ENCODING_MAP[""],
"_": CHARACTER_ENCODING_MAP[" "],
})
ALLOWED_TRAINER_NAME_CHARACTERS = frozenset({
" ", "0", "1", "2", "3", "4", "5", "6", "7", "8",
"9", "!", "?", ".", "-", "", "", "", "", "",
"", "", ",", "/", "A", "B", "C", "D", "E", "F",
"G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
"Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "y", "z",
})
def encode_string(string: str, length: Optional[int] = None) -> bytes:
arr = []
length = len(string) if length is None else length
for i in range(length):
if i >= len(string):
arr.append(0xFF)
continue
char = string[i]
if char in CHARACTER_ENCODING_MAP:
arr.append(CHARACTER_ENCODING_MAP[char])
else:
arr.append(CHARACTER_ENCODING_MAP["?"])
return bytes(arr)
def decode_string(string_data: Iterable[int]) -> str:
string = ""
for code in string_data:
if code == 0xFF:
break
if code in CHARACTER_DECODING_MAP:
string += CHARACTER_DECODING_MAP[code]
else:
raise KeyError(f"The following value does not correspond to a character in Pokemon Emerald: {code}")
return string
def get_easter_egg(easter_egg: str) -> Tuple[int, int]:
easter_egg = easter_egg.upper()
result1 = 0
result2 = 0
for c in easter_egg:
result1 = ((result1 << 5) - result1 + ord(c)) & 0xFFFFFFFF
result2 = ((result2 << 4) - result2 + ord(c)) & 0xFF
if result1 == 0x9137C17B:
value = (result2 + 23) & 0xFF
if value > 0 and (value < 252 or (value > 276 and value < 412)):
return (1, value)
elif result1 == 0x9AECC7C6:
value = (result2 + 64) & 0xFF
if value > 0 and value < 355:
return (2, value)
elif result1 == 0x506D2690:
value = (result2 + 169) & 0xFF
if value > 0 and value < 78:
return (3, value)
elif result1 == 0xA7850E45 and (result1 ^ result2) & 0xFF == 96:
return (4, 0)
return (0, 0)
def location_name_to_label(name: str) -> str:
@ -8,12 +117,233 @@ def location_name_to_label(name: str) -> str:
def int_to_bool_array(num: int) -> List[bool]:
binary_string = format(num, '064b')
bool_array = [bit == '1' for bit in reversed(binary_string)]
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)])
binary_string = "".join(["1" if bit else "0" for bit in reversed(bool_array)])
num = int(binary_string, 2)
return num
_SUBSTRUCT_ORDERS = [
[0, 1, 2, 3], [0, 1, 3, 2], [0, 2, 1, 3], [0, 3, 1, 2],
[0, 2, 3, 1], [0, 3, 2, 1], [1, 0, 2, 3], [1, 0, 3, 2],
[2, 0, 1, 3], [3, 0, 1, 2], [2, 0, 3, 1], [3, 0, 2, 1],
[1, 2, 0, 3], [1, 3, 0, 2], [2, 1, 0, 3], [3, 1, 0, 2],
[2, 3, 0, 1], [3, 2, 0, 1], [1, 2, 3, 0], [1, 3, 2, 0],
[2, 1, 3, 0], [3, 1, 2, 0], [2, 3, 1, 0], [3, 2, 1, 0],
]
_LANGUAGE_IDS = {
"Japanese": 1,
"English": 2,
"French": 3,
"Italian": 4,
"German": 5,
"Spanish": 7,
}
_MODERN_ITEM_TO_EMERALD_ITEM = {
item.modern_id: item.item_id
for item in data.items.values()
if item.modern_id is not None
}
def _encrypt_or_decrypt_substruct(substruct_data: Iterable[int], key: int) -> bytearray:
modified_data = bytearray()
for i in range(int(len(substruct_data) / 4)):
modified_data.extend((int.from_bytes(substruct_data[i * 4 : (i + 1) * 4], "little") ^ key).to_bytes(4, "little"))
return modified_data
def pokemon_data_to_json(pokemon_data: Iterable[int]) -> str:
personality = int.from_bytes(pokemon_data[0:4], "little")
tid = int.from_bytes(pokemon_data[4:8], "little")
substruct_order = _SUBSTRUCT_ORDERS[personality % 24]
substructs = []
for i in substruct_order:
substructs.append(pokemon_data[32 + (i * 12) : 32 + ((i + 1) * 12)])
decrypted_substructs = [_encrypt_or_decrypt_substruct(substruct, personality ^ tid) for substruct in substructs]
iv_ability_info = int.from_bytes(decrypted_substructs[3][4:8], "little")
met_info = int.from_bytes(decrypted_substructs[3][2:4], "little")
held_item = int.from_bytes(decrypted_substructs[0][2:4], "little")
json_object = {
"version": "1",
"personality": personality,
"nickname": decode_string(pokemon_data[8:18]),
"language": {v: k for k, v in _LANGUAGE_IDS.items()}[pokemon_data[18]],
"species": data.species[int.from_bytes(decrypted_substructs[0][0:2], "little")].national_dex_number,
"experience": int.from_bytes(decrypted_substructs[0][4:8], "little"),
"ability": iv_ability_info >> 31,
"ivs": [(iv_ability_info >> (i * 5)) & 0x1F for i in range(6)],
"evs": list(decrypted_substructs[2][0:6]),
"conditions": list(decrypted_substructs[2][6:12]),
"pokerus": decrypted_substructs[3][0],
"location_met": decrypted_substructs[3][1],
"level_met": met_info & 0b0000000001111111,
"game": (met_info & 0b0000011110000000) >> 7,
"ball": (met_info & 0b0111100000000000) >> 11,
"moves": [
[
int.from_bytes(decrypted_substructs[1][i * 2 : (i + 1) * 2], "little"),
decrypted_substructs[1][8 + i],
(decrypted_substructs[0][8] & (0b00000011 << (i * 2))) >> (i * 2)
] for i in range(4)
],
"trainer": {
"name": decode_string(pokemon_data[20:27]),
"id": tid,
"female": (met_info & 0b1000000000000000) != 0,
},
}
if held_item != 0:
json_object["item"] = data.items[held_item].modern_id
return orjson.dumps(json_object).decode("utf-8")
def json_to_pokemon_data(json_str: str) -> bytearray:
pokemon_json: Dict[str, Any] = orjson.loads(json_str)
# Default values to cover for optional or accidentally missed fields
default_pokemon = {
"nickname": "A",
"personality": 0,
"species": 1,
"experience": 0,
"ability": 0,
"ivs": [0, 0, 0, 0, 0, 0],
"evs": [0, 0, 0, 0, 0, 0],
"conditions": [0, 0, 0, 0, 0, 0],
"pokerus": 0,
"game": 3,
"location_met": 0,
"level_met": 1,
"ball": 4,
"moves": [[33, 35, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
}
default_trainer = {
"name": "A",
"id": 0,
"female": False,
}
pokemon_json = {**default_pokemon, **{k: v for k, v in pokemon_json.items()}}
pokemon_json["trainer"] = {**default_trainer, **pokemon_json["trainer"]}
# Cutting string lengths to Emerald sizes
pokemon_json["nickname"] = pokemon_json["nickname"][0:10]
pokemon_json["trainer"]["name"] = pokemon_json["trainer"]["name"][0:7]
# Handle data from incompatible games
if pokemon_json["species"] > 387:
pokemon_json["species"] = 201 # Unown
if pokemon_json["ball"] > 12:
pokemon_json["ball"] = 4 # Pokeball
if "game" not in pokemon_json or (pokemon_json["game"] > 5 and pokemon_json["game"] != 15):
pokemon_json["game"] = 0 # Unknown
pokemon_json["location_met"] = 0 # Littleroot
substructs = [bytearray([0 for _ in range(12)]) for _ in range(4)]
# Substruct type 0
for i, byte in enumerate(NATIONAL_ID_TO_SPECIES_ID[pokemon_json["species"]].to_bytes(2, "little")):
substructs[0][0 + i] = byte
if "item" in pokemon_json:
if pokemon_json["item"] in _MODERN_ITEM_TO_EMERALD_ITEM:
for i, byte in enumerate(_MODERN_ITEM_TO_EMERALD_ITEM[pokemon_json["item"]].to_bytes(2, "little")):
substructs[0][2 + i] = byte
for i, byte in enumerate((pokemon_json["experience"]).to_bytes(4, "little")):
substructs[0][4 + i] = byte
for i, move_info in enumerate(pokemon_json["moves"]):
substructs[0][8] |= ((move_info[2] & 0b11) << (2 * i))
substructs[0][9] = data.species[NATIONAL_ID_TO_SPECIES_ID[pokemon_json["species"]]].friendship
# Substruct type 1
for i, move_info in enumerate(pokemon_json["moves"]):
for j, byte in enumerate(move_info[0].to_bytes(2, "little")):
substructs[1][(i * 2) + j] = byte
substructs[1][8 + i] = move_info[1]
# Substruct type 2
for i, ev in enumerate(pokemon_json["evs"]):
substructs[2][0 + i] = ev
for i, condition in enumerate(pokemon_json["conditions"]):
substructs[2][6 + i] = condition
# Substruct type 3
substructs[3][0] = pokemon_json["pokerus"]
substructs[3][1] = pokemon_json["location_met"]
origin = pokemon_json["level_met"] | (pokemon_json["game"] << 7) | (pokemon_json["ball"] << 11)
origin |= (1 << 15) if pokemon_json["trainer"]["female"] else 0
for i, byte in enumerate(origin.to_bytes(2, "little")):
substructs[3][2 + i] = byte
iv_ability_info = 0
for i, iv in enumerate(pokemon_json["ivs"]):
iv_ability_info |= iv << (i * 5)
iv_ability_info |= 1 << 31 if pokemon_json["ability"] == 1 else 0
for i, byte in enumerate(iv_ability_info.to_bytes(4, "little")):
substructs[3][4 + i] = byte
# Main data
pokemon_data = bytearray([0 for _ in range(80)])
for i, byte in enumerate(pokemon_json["personality"].to_bytes(4, "little")):
pokemon_data[0 + i] = byte
for i, byte in enumerate(pokemon_json["trainer"]["id"].to_bytes(4, "little")):
pokemon_data[4 + i] = byte
for i, byte in enumerate(encode_string(pokemon_json["nickname"], 10)):
pokemon_data[8 + i] = byte
pokemon_data[18] = _LANGUAGE_IDS[pokemon_json["language"]]
pokemon_data[19] = 0b00000010 # Flags for Bad Egg, Has Species, Is Egg, padding bits (low to high)
for i, byte in enumerate(encode_string(pokemon_json["trainer"]["name"], 7)):
pokemon_data[20 + i] = byte
# Markings, 1 byte
checksum = 0
for i in range(4):
for j in range(6):
checksum += int.from_bytes(substructs[i][j * 2 : (j + 1) * 2], "little")
checksum &= 0xFFFF
for i, byte in enumerate(checksum.to_bytes(2, "little")):
pokemon_data[28 + i] = byte
# Separator, 2 bytes
substruct_order = [_SUBSTRUCT_ORDERS[pokemon_json["personality"] % 24].index(n) for n in [0, 1, 2, 3]]
encrypted_substructs = [None for _ in range(4)]
encryption_key = pokemon_json["personality"] ^ pokemon_json["trainer"]["id"]
encrypted_substructs[0] = _encrypt_or_decrypt_substruct(substructs[substruct_order[0]], encryption_key)
encrypted_substructs[1] = _encrypt_or_decrypt_substruct(substructs[substruct_order[1]], encryption_key)
encrypted_substructs[2] = _encrypt_or_decrypt_substruct(substructs[substruct_order[2]], encryption_key)
encrypted_substructs[3] = _encrypt_or_decrypt_substruct(substructs[substruct_order[3]], encryption_key)
for i in range(4):
for j in range(12):
pokemon_data[32 + (i * 12) + j] = encrypted_substructs[i][j]
return pokemon_data