Pokemon Emerald: v2 Update (#2918)
This commit is contained in:
parent
3e3965272d
commit
fa233b2583
|
@ -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.
|
|
@ -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
|
@ -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.slot_data["goal"] == Goal.option_champion:
|
||||
self.goal_flag = IS_CHAMPION_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_steven:
|
||||
self.goal_flag = DEFEATED_STEVEN_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_norman:
|
||||
self.goal_flag = DEFEATED_NORMAN_FLAG
|
||||
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 = 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
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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
|
@ -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
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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"},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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]
|
||||
label_to_id_map[location_data.label] = offset_flag(location_data.flag)
|
||||
|
||||
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))
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
# Set free fly location
|
||||
if world.options.free_fly_location:
|
||||
_set_bytes_le(
|
||||
patched_rom,
|
||||
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):
|
||||
# Set free fly location
|
||||
if location.address is None:
|
||||
if world.options.free_fly_location and location.name == "EVENT_VISITED_LITTLEROOT_TOWN":
|
||||
_set_bytes_little_endian(
|
||||
patched_rom,
|
||||
data.rom_addresses["gArchipelagoOptions"] + 0x16,
|
||||
1,
|
||||
world.free_fly_location_id
|
||||
)
|
||||
continue
|
||||
|
||||
if location.item and location.item.player == world.player:
|
||||
_set_bytes_little_endian(
|
||||
patched_rom,
|
||||
location.rom_address,
|
||||
2,
|
||||
reverse_offset_item_value(location.item.code)
|
||||
)
|
||||
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.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(
|
||||
patched_rom,
|
||||
location.rom_address,
|
||||
2,
|
||||
data.constants["ITEM_ARCHIPELAGO_PROGRESSION"]
|
||||
)
|
||||
if type(location.item_address) is int:
|
||||
_set_bytes_le(
|
||||
patched_rom,
|
||||
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])
|
||||
|
||||
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] == 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
|
||||
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 world.random.random() < probability:
|
||||
# Set the trainer to be a double battle
|
||||
_set_bytes_little_endian(rom, trainer_data.rom_address + 0x18, 1, 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_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(
|
||||
# Swap the battle type in the script for the purpose of loading the right text
|
||||
# and setting data to the right places
|
||||
_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
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue