Pokémon R/B: Migrate support into Bizhawk Client (#2466)

- Removes the Pokémon Client, adding support for Red and Blue to the Bizhawk Client.
- Adds `/bank` commands that mirror SDV's, allowing transferring money into and out of the EnergyLink storage.
- Adds a fix to the base patch so that the progressive card key counter will not increment beyond 10, which would lead to receiving glitch items. This value is checked against and verified that it is not > 10 as part of crash detection by the client, to prevent erroneous location checks when the game crashes, so this is relevant to the new client (although shouldn't happen unless you're using !getitem, or putting progressive card keys as item link replacement items)
This commit is contained in:
Alchav 2023-11-25 05:57:02 -05:00 committed by GitHub
parent edb62004ef
commit 8a852abdc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 439 additions and 751 deletions

View File

@ -1,382 +0,0 @@
import asyncio
import json
import time
import os
import bsdiff4
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
from worlds.pokemon_rb.locations import location_data
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
location_bytes_bits = {}
for location in location_data:
if location.ram_address is not None:
if type(location.ram_address) == list:
location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
{'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
else:
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
and location.address is not None}
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
SCRIPT_VERSION = 3
class GBCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_gb(self):
"""Check Gameboy Connection State"""
if isinstance(self.ctx, GBContext):
logger.info(f"Gameboy Status: {self.ctx.gb_status}")
class GBContext(CommonContext):
command_processor = GBCommandProcessor
game = 'Pokemon Red and Blue'
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.gb_streams: (StreamReader, StreamWriter) = None
self.gb_sync_task = None
self.messages = {}
self.locations_array = None
self.gb_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
self.deathlink_pending = False
self.set_deathlink = False
self.client_compatibility_mode = 0
self.items_handling = 0b001
self.sent_release = False
self.sent_collect = False
self.auto_hints = set()
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(GBContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to EmuHawk to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
self.set_deathlink = True
elif cmd == "RoomInfo":
self.seed_name = args['seed_name']
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
def on_deathlink(self, data: dict):
self.deathlink_pending = True
super().on_deathlink(data)
def run_gui(self):
from kvui import GameManager
class GBManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Pokémon Client"
self.ui = GBManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: GBContext):
current_time = time.time()
ret = json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10},
"deathlink": ctx.deathlink_pending,
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
}
)
ctx.deathlink_pending = False
return ret
async def parse_locations(data: List, ctx: GBContext):
locations = []
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
else:
flags["DexSanityFlag"] = [0] * 19
for flag_type, loc_map in location_map.items():
for flag, loc_id in loc_map.items():
if flag_type == "list":
if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit']
and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']):
locations.append(loc_id)
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
locations.append(loc_id)
hints = []
if flags["EventFlag"][280] & 16:
hints.append("Cerulean Bicycle Shop")
if flags["EventFlag"][280] & 32:
hints.append("Route 2 Gate - Oak's Aide")
if flags["EventFlag"][280] & 64:
hints.append("Route 11 Gate 2F - Oak's Aide")
if flags["EventFlag"][280] & 128:
hints.append("Route 15 Gate 2F - Oak's Aide")
if flags["EventFlag"][281] & 1:
hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2",
"Celadon Prize Corner - Item Prize 3"]
if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id["Fossil - Choice B"]
not in ctx.checked_locations):
hints.append("Fossil - Choice B")
elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id["Fossil - Choice A"]
not in ctx.checked_locations):
hints.append("Fossil - Choice A")
hints = [
location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in ctx.auto_hints and
location_name_to_id[loc] in ctx.missing_locations and location_name_to_id[loc] not in ctx.locations_checked
]
if hints:
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}])
ctx.auto_hints.update(hints)
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
if locations == ctx.locations_array:
return
ctx.locations_array = locations
if locations is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
async def gb_sync_task(ctx: GBContext):
logger.info("Starting GB connector. Use /gb for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.gb_streams:
(reader, writer) = ctx.gb_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
msg = "You are connecting with an incompatible Lua script version. Ensure your connector Lua " \
"and PokemonClient are from the same Archipelago installation."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.client_compatibility_mode = data_decoded['clientCompatibilityVersion']
if ctx.client_compatibility_mode == 0:
ctx.items_handling = 0b101 # old patches will not have local start inventory, must be requested
if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]):
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.seed_name = ''.join([chr(i) for i in data_decoded['seedName'] if i != 0])
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
msg = "Invalid ROM detected. No player name built into the ROM."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if ctx.awaiting_rom:
await ctx.server_auth(False)
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
and not error_status and ctx.auth:
# Not just a keep alive ping, parse
async_start(parse_locations(data_decoded['locations'], ctx))
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
if 'options' in data_decoded:
msgs = []
if data_decoded['options'] & 4 and not ctx.sent_release:
ctx.sent_release = True
msgs.append({"cmd": "Say", "text": "!release"})
if data_decoded['options'] & 8 and not ctx.sent_collect:
ctx.sent_collect = True
msgs.append({"cmd": "Say", "text": "!collect"})
if msgs:
await ctx.send_msgs(msgs)
if ctx.set_deathlink:
await ctx.update_death_link(True)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
if ctx.gb_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to Gameboy")
ctx.gb_status = CONNECTION_CONNECTED_STATUS
else:
ctx.gb_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.gb_status = error_status
logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates")
else:
try:
logger.debug("Attempting to connect to Gameboy")
ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10)
ctx.gb_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.gb_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.gb_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True)
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(game_version, patch_file, ctx):
base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.gb'
if game_version == "blue":
delta_patch = BlueDeltaPatch
else:
delta_patch = RedDeltaPatch
try:
base_rom = delta_patch.get_source_data()
except Exception as msg:
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
with patch_archive.open('delta.bsdiff4', 'r') as stream:
patch = stream.read()
patched_rom_data = bsdiff4.patch(base_rom, patch)
with open(comp_path, "wb") as patched_rom_file:
patched_rom_file.write(patched_rom_data)
async_start(run_game(comp_path))
if __name__ == '__main__':
Utils.init_logging("PokemonClient")
options = Utils.get_options()
async def main():
parser = get_base_parser()
parser.add_argument('patch_file', default="", type=str, nargs="?",
help='Path to an APRED or APBLUE patch file')
args = parser.parse_args()
ctx = GBContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync")
if args.patch_file:
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
if ext == "apred":
logger.info("APRED file supplied, beginning patching process...")
async_start(patch_and_run_game("red", args.patch_file, ctx))
elif ext == "apblue":
logger.info("APBLUE file supplied, beginning patching process...")
async_start(patch_and_run_game("blue", args.patch_file, ctx))
else:
logger.warning(f"Unknown patch file extension {ext}")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.gb_sync_task:
await ctx.gb_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@ -1,224 +0,0 @@
local socket = require("socket")
local json = require('json')
local math = require('math')
require("common")
local STATE_OK = "Ok"
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
local STATE_UNINITIALIZED = "Uninitialized"
local SCRIPT_VERSION = 3
local APIndex = 0x1A6E
local APDeathLinkAddress = 0x00FD
local APItemAddress = 0x00FF
local EventFlagAddress = 0x1735
local MissableAddress = 0x161A
local HiddenItemsAddress = 0x16DE
local RodAddress = 0x1716
local DexSanityAddress = 0x1A71
local InGameAddress = 0x1A84
local ClientCompatibilityAddress = 0xFF00
local ItemsReceived = nil
local playerName = nil
local seedName = nil
local deathlink_rec = nil
local deathlink_send = false
local prevstate = ""
local curstate = STATE_UNINITIALIZED
local gbSocket = nil
local frame = 0
local compat = nil
local function defineMemoryFunctions()
local memDomain = {}
local domains = memory.getmemorydomainlist()
memDomain["rom"] = function() memory.usememorydomain("ROM") end
memDomain["wram"] = function() memory.usememorydomain("WRAM") end
return memDomain
end
local memDomain = defineMemoryFunctions()
u8 = memory.read_u8
wU8 = memory.write_u8
u16 = memory.read_u16_le
function uRange(address, bytes)
data = memory.readbyterange(address - 1, bytes + 1)
data[0] = nil
return data
end
function generateLocationsChecked()
memDomain.wram()
events = uRange(EventFlagAddress, 0x140)
missables = uRange(MissableAddress, 0x20)
hiddenitems = uRange(HiddenItemsAddress, 0x0E)
rod = {u8(RodAddress)}
dexsanity = uRange(DexSanityAddress, 19)
data = {}
categories = {events, missables, hiddenitems, rod}
if compat > 1 then
table.insert(categories, dexsanity)
end
for _, category in ipairs(categories) do
for _, v in ipairs(category) do
table.insert(data, v)
end
end
return data
end
local function arrayEqual(a1, a2)
if #a1 ~= #a2 then
return false
end
for i, v in ipairs(a1) do
if v ~= a2[i] then
return false
end
end
return true
end
function receive()
l, e = gbSocket:receive()
if e == 'closed' then
if curstate == STATE_OK then
print("Connection closed")
end
curstate = STATE_UNINITIALIZED
return
elseif e == 'timeout' then
return
elseif e ~= nil then
print(e)
curstate = STATE_UNINITIALIZED
return
end
if l ~= nil then
block = json.decode(l)
if block ~= nil then
local itemsBlock = block["items"]
if itemsBlock ~= nil then
ItemsReceived = itemsBlock
end
deathlink_rec = block["deathlink"]
end
end
-- Determine Message to send back
memDomain.rom()
newPlayerName = uRange(0xFFF0, 0x10)
newSeedName = uRange(0xFFDB, 21)
if (playerName ~= nil and not arrayEqual(playerName, newPlayerName)) or (seedName ~= nil and not arrayEqual(seedName, newSeedName)) then
print("ROM changed, quitting")
curstate = STATE_UNINITIALIZED
return
end
playerName = newPlayerName
seedName = newSeedName
local retTable = {}
retTable["scriptVersion"] = SCRIPT_VERSION
if compat == nil then
compat = u8(ClientCompatibilityAddress)
if compat < 2 then
InGameAddress = 0x1A71
end
end
retTable["clientCompatibilityVersion"] = compat
retTable["playerName"] = playerName
retTable["seedName"] = seedName
memDomain.wram()
in_game = u8(InGameAddress)
if in_game == 0x2A or in_game == 0xAC then
retTable["locations"] = generateLocationsChecked()
elseif in_game ~= 0 then
print("Game may have crashed")
curstate = STATE_UNINITIALIZED
return
end
retTable["deathLink"] = deathlink_send
deathlink_send = false
msg = json.encode(retTable).."\n"
local ret, error = gbSocket:send(msg)
if ret == nil then
print(error)
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
curstate = STATE_TENTATIVELY_CONNECTED
elseif curstate == STATE_TENTATIVELY_CONNECTED then
print("Connected!")
curstate = STATE_OK
end
end
function main()
if not checkBizHawkVersion() then
return
end
server, error = socket.bind('localhost', 17242)
while true do
frame = frame + 1
if not (curstate == prevstate) then
print("Current state: "..curstate)
prevstate = curstate
end
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 5 == 0) then
receive()
in_game = u8(InGameAddress)
if in_game == 0x2A or in_game == 0xAC then
if u8(APItemAddress) == 0x00 then
ItemIndex = u16(APIndex)
if deathlink_rec == true then
wU8(APDeathLinkAddress, 1)
elseif u8(APDeathLinkAddress) == 3 then
wU8(APDeathLinkAddress, 0)
deathlink_send = true
end
if ItemsReceived[ItemIndex + 1] ~= nil then
item_id = ItemsReceived[ItemIndex + 1] - 172000000
if item_id > 255 then
item_id = item_id - 256
end
wU8(APItemAddress, item_id)
end
end
end
end
elseif (curstate == STATE_UNINITIALIZED) then
if (frame % 60 == 0) then
print("Waiting for client.")
emu.frameadvance()
server:settimeout(2)
print("Attempting to connect")
local client, timeout = server:accept()
if timeout == nil then
curstate = STATE_INITIAL_CONNECTION_MADE
gbSocket = client
gbSocket:settimeout(0)
end
end
end
emu.frameadvance()
end
end
main()

View File

@ -140,13 +140,13 @@ Root: HKCR; Subkey: "{#MyAppName}n64zpf\shell\open\command"; ValueData: """{
Root: HKCR; Subkey: ".apred"; ValueData: "{#MyAppName}pkmnrpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch"; ValueData: "Archipelago Pokemon Red Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnrpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apblue"; ValueData: "{#MyAppName}pkmnbpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch"; ValueData: "Archipelago Pokemon Blue Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoPokemonClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoPokemonClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}pkmnbpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apbn3"; ValueData: "{#MyAppName}bn3bpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}bn3bpatch"; ValueData: "Archipelago MegaMan Battle Network 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";

View File

@ -101,8 +101,6 @@ components: List[Component] = [
Component('OoT Adjuster', 'OoTAdjuster'),
# FF1
Component('FF1 Client', 'FF1Client'),
# Pokémon
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
# TLoZ
Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')),
# ChecksFinder

View File

@ -2,9 +2,11 @@ import os
import settings
import typing
import threading
import base64
from copy import deepcopy
from typing import TextIO
from Utils import __version__
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification, LocationProgressType
from Fill import fill_restrictive, FillError, sweep_from_pool
from worlds.AutoWorld import World, WebWorld
@ -22,6 +24,7 @@ from .rules import set_rules
from .level_scaling import level_scaling
from . import logic
from . import poke_data
from . import client
class PokemonSettings(settings.Group):
@ -36,16 +39,8 @@ class PokemonSettings(settings.Group):
copy_to = "Pokemon Blue (UE) [S][!].gb"
md5s = [BlueDeltaPatch.hash]
class RomStart(str):
"""
Set this to false to never autostart a rom (such as after patching)
True for operating system default program
Alternatively, a path to a program to open the .gb file with
"""
red_rom_file: RedRomFile = RedRomFile(RedRomFile.copy_to)
blue_rom_file: BlueRomFile = BlueRomFile(BlueRomFile.copy_to)
rom_start: typing.Union[RomStart, bool] = True
class PokemonWebWorld(WebWorld):
@ -141,9 +136,6 @@ class PokemonRedBlueWorld(World):
else:
self.rival_name = encode_name(self.multiworld.rival_name[self.player].value, "Rival")
if len(self.multiworld.player_name[self.player].encode()) > 16:
raise Exception(f"Player name too long for {self.multiworld.get_player_name(self.player)}. Player name cannot exceed 16 bytes for Pokémon Red and Blue.")
if not self.multiworld.badgesanity[self.player]:
self.multiworld.non_local_items[self.player].value -= self.item_name_groups["Badges"]
@ -621,6 +613,13 @@ class PokemonRedBlueWorld(World):
def generate_output(self, output_directory: str):
generate_output(self, output_directory)
def modify_multidata(self, multidata: dict):
rom_name = bytearray(f'AP{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
'utf8')[:21]
rom_name.extend([0] * (21 - len(rom_name)))
new_name = base64.b64encode(bytes(rom_name)).decode()
multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]]
def write_spoiler_header(self, spoiler_handle: TextIO):
spoiler_handle.write(f"Cerulean Cave Total Key Items: {self.multiworld.cerulean_cave_key_items_condition[self.player].total}\n")
spoiler_handle.write(f"Elite Four Total Key Items: {self.multiworld.elite_four_key_items_condition[self.player].total}\n")

277
worlds/pokemon_rb/client.py Normal file
View File

@ -0,0 +1,277 @@
import base64
import logging
import time
from NetUtils import ClientStatus
from worlds._bizhawk.client import BizHawkClient
from worlds._bizhawk import read, write, guarded_write
from worlds.pokemon_rb.locations import location_data
logger = logging.getLogger("Client")
BANK_EXCHANGE_RATE = 100000000
DATA_LOCATIONS = {
"ItemIndex": (0x1A6E, 0x02),
"Deathlink": (0x00FD, 0x01),
"APItem": (0x00FF, 0x01),
"EventFlag": (0x1735, 0x140),
"Missable": (0x161A, 0x20),
"Hidden": (0x16DE, 0x0E),
"Rod": (0x1716, 0x01),
"DexSanityFlag": (0x1A71, 19),
"GameStatus": (0x1A84, 0x01),
"Money": (0x141F, 3),
"ResetCheck": (0x0100, 4),
# First and second Vermilion Gym trash can selection. Second is not used, so should always be 0.
# First should never be above 0x0F. This is just before Event Flags.
"CrashCheck1": (0x1731, 2),
# Unused, should always be 0. This is just before Missables flags.
"CrashCheck2": (0x1617, 1),
# Progressive keys, should never be above 10. Just before Dexsanity flags.
"CrashCheck3": (0x1A70, 1),
# Route 18 script value. Should never be above 2. Just before Hidden items flags.
"CrashCheck4": (0x16DD, 1),
}
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
location_bytes_bits = {}
for location in location_data:
if location.ram_address is not None:
if type(location.ram_address) == list:
location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
{'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
else:
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
and location.address is not None}
class PokemonRBClient(BizHawkClient):
system = ("GB", "SGB")
patch_suffix = (".apred", ".apblue")
game = "Pokemon Red and Blue"
def __init__(self):
super().__init__()
self.auto_hints = set()
self.locations_array = None
self.disconnect_pending = False
self.set_deathlink = False
self.banking_command = None
self.game_state = False
self.last_death_link = 0
async def validate_rom(self, ctx):
game_name = await read(ctx.bizhawk_ctx, [(0x134, 12, "ROM")])
game_name = game_name[0].decode("ascii")
if game_name in ("POKEMON RED\00", "POKEMON BLUE"):
ctx.game = self.game
ctx.items_handling = 0b001
ctx.command_processor.commands["bank"] = cmd_bank
seed_name = await read(ctx.bizhawk_ctx, [(0xFFDB, 21, "ROM")])
ctx.seed_name = seed_name[0].split(b"\0")[0].decode("ascii")
self.set_deathlink = False
self.banking_command = None
self.locations_array = None
self.disconnect_pending = False
return True
return False
async def set_auth(self, ctx):
auth_name = await read(ctx.bizhawk_ctx, [(0xFFC6, 21, "ROM")])
if auth_name[0] == bytes([0] * 21):
# rom was patched before rom names implemented, use player name
auth_name = await read(ctx.bizhawk_ctx, [(0xFFF0, 16, "ROM")])
auth_name = auth_name[0].decode("ascii").split("\x00")[0]
else:
auth_name = base64.b64encode(auth_name[0]).decode()
ctx.auth = auth_name
async def game_watcher(self, ctx):
if not ctx.server or not ctx.server.socket.open or ctx.server.socket.closed:
return
data = await read(ctx.bizhawk_ctx, [(loc_data[0], loc_data[1], "WRAM")
for loc_data in DATA_LOCATIONS.values()])
data = {data_set_name: data_name for data_set_name, data_name in zip(DATA_LOCATIONS.keys(), data)}
if self.set_deathlink:
self.set_deathlink = False
await ctx.update_death_link(True)
if self.disconnect_pending:
self.disconnect_pending = False
await ctx.disconnect()
if data["GameStatus"][0] == 0 or data["ResetCheck"] == b'\xff\xff\xff\x7f':
# Do not handle anything before game save is loaded
self.game_state = False
return
elif (data["GameStatus"][0] not in (0x2A, 0xAC)
or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF
or data["CrashCheck2"][0]
or data["CrashCheck3"][0] > 10
or data["CrashCheck4"][0] > 2):
# Should mean game crashed
logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.")
self.game_state = False
await ctx.disconnect()
return
self.game_state = True
# SEND ITEMS TO CLIENT
if data["APItem"][0] == 0:
item_index = int.from_bytes(data["ItemIndex"], "little")
if len(ctx.items_received) > item_index:
item_code = ctx.items_received[item_index].item - 172000000
if item_code > 255:
item_code -= 256
await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["APItem"][0],
[item_code], "WRAM")])
# LOCATION CHECKS
locations = set()
for flag_type, loc_map in location_map.items():
for flag, loc_id in loc_map.items():
if flag_type == "list":
if (data["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 <<
location_bytes_bits[loc_id][0]['bit']
and data["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 <<
location_bytes_bits[loc_id][1]['bit']):
locations.add(loc_id)
elif data[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
locations.add(loc_id)
if locations != self.locations_array:
if locations:
self.locations_array = locations
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locations)}])
# AUTO HINTS
hints = []
if data["EventFlag"][280] & 16:
hints.append("Cerulean Bicycle Shop")
if data["EventFlag"][280] & 32:
hints.append("Route 2 Gate - Oak's Aide")
if data["EventFlag"][280] & 64:
hints.append("Route 11 Gate 2F - Oak's Aide")
if data["EventFlag"][280] & 128:
hints.append("Route 15 Gate 2F - Oak's Aide")
if data["EventFlag"][281] & 1:
hints += ["Celadon Prize Corner - Item Prize 1", "Celadon Prize Corner - Item Prize 2",
"Celadon Prize Corner - Item Prize 3"]
if (location_name_to_id["Fossil - Choice A"] in ctx.checked_locations and location_name_to_id[
"Fossil - Choice B"]
not in ctx.checked_locations):
hints.append("Fossil - Choice B")
elif (location_name_to_id["Fossil - Choice B"] in ctx.checked_locations and location_name_to_id[
"Fossil - Choice A"]
not in ctx.checked_locations):
hints.append("Fossil - Choice A")
hints = [
location_name_to_id[loc] for loc in hints if location_name_to_id[loc] not in self.auto_hints and
location_name_to_id[loc] in ctx.missing_locations and
location_name_to_id[loc] not in ctx.locations_checked
]
if hints:
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}])
self.auto_hints.update(hints)
# DEATHLINK
if "DeathLink" in ctx.tags:
if data["Deathlink"][0] == 3:
await ctx.send_death(ctx.player_names[ctx.slot] + " is out of usable Pokémon! "
+ ctx.player_names[ctx.slot] + " blacked out!")
await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["Deathlink"][0], [0], "WRAM")])
self.last_death_link = ctx.last_death_link
elif ctx.last_death_link > self.last_death_link:
self.last_death_link = ctx.last_death_link
await write(ctx.bizhawk_ctx, [(DATA_LOCATIONS["Deathlink"][0], [1], "WRAM")])
# BANK
if self.banking_command:
original_money = data["Money"]
# Money is stored as binary-coded decimal.
money = int(original_money.hex())
if self.banking_command > money:
logger.warning(f"You do not have ${self.banking_command} to deposit!")
elif (-self.banking_command * BANK_EXCHANGE_RATE) > ctx.stored_data[f"EnergyLink{ctx.team}"]:
logger.warning("Not enough money in the EnergyLink storage!")
else:
if self.banking_command + money > 999999:
self.banking_command = 999999 - money
money = str(money - self.banking_command).zfill(6)
money = [int(money[:2], 16), int(money[2:4], 16), int(money[4:], 16)]
money_written = await guarded_write(ctx.bizhawk_ctx, [(0x141F, money, "WRAM")],
[(0x141F, original_money, "WRAM")])
if money_written:
if self.banking_command >= 0:
deposit = self.banking_command - int(self.banking_command / 4)
tax = self.banking_command - deposit
logger.info(f"Deposited ${deposit}, and charged a tax of ${tax}.")
self.banking_command = deposit
else:
logger.info(f"Withdrew ${-self.banking_command}.")
await ctx.send_msgs([{
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "operations":
[{"operation": "add", "value": self.banking_command * BANK_EXCHANGE_RATE},
{"operation": "max", "value": 0}],
}])
self.banking_command = None
# VICTORY
if data["EventFlag"][280] & 1 and not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
def on_package(self, ctx, cmd, args):
if cmd == 'Connected':
if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
self.set_deathlink = True
self.last_death_link = time.time()
ctx.set_notify(f"EnergyLink{ctx.team}")
elif cmd == 'RoomInfo':
if ctx.seed_name and ctx.seed_name != args["seed_name"]:
# CommonClient's on_package displays an error to the user in this case, but connection is not cancelled.
self.game_state = False
self.disconnect_pending = True
super().on_package(ctx, cmd, args)
def cmd_bank(self, cmd: str = "", amount: str = ""):
"""Deposit or withdraw money with the server's EnergyLink storage.
/bank - check server balance.
/bank deposit # - deposit money. One quarter of the amount will be lost to taxation.
/bank withdraw # - withdraw money."""
if self.ctx.game != "Pokemon Red and Blue":
logger.warning("This command can only be used while playing Pokémon Red and Blue")
return
if not cmd:
logger.info(f"Money available: {int(self.ctx.stored_data[f'EnergyLink{self.ctx.team}'] / BANK_EXCHANGE_RATE)}")
return
elif (not self.ctx.server) or self.ctx.server.socket.closed or not self.ctx.client_handler.game_state:
logger.info(f"Must be connected to server and in game.")
elif not amount:
logger.warning("You must specify an amount.")
elif cmd == "withdraw":
self.ctx.client_handler.banking_command = -int(amount)
elif cmd == "deposit":
if int(amount) < 4:
logger.warning("You must deposit at least $4, for tax purposes.")
return
self.ctx.client_handler.banking_command = int(amount)
else:
logger.warning(f"Invalid bank command {cmd}")
return

View File

@ -83,6 +83,9 @@ you until these have ended.
## Unique Local Commands
The following command is only available when using the PokemonClient to play with Archipelago.
You can use `/bank` commands to deposit and withdraw money from the server's EnergyLink storage. This can be accessed by
any players playing games that use the EnergyLink feature.
- `/gb` Check Gameboy Connection State
- `/bank` - check the amount of money available on the server.
- `/bank withdraw #` - withdraw money from the server.
- `/bank deposit #` - deposit money into the server. 25% of the amount will be lost to taxation.

View File

@ -11,7 +11,6 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `Pokemon Client` during installation).
- Pokémon Red and/or Blue ROM files. The Archipelago community cannot provide these.
## Optional Software
@ -71,28 +70,41 @@ And the following special characters (these each count as one character):
## Joining a MultiWorld Game
### Obtain your Pokémon patch file
### Generating and Patching a Game
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
files. Your data file should have a `.apred` or `.apblue` extension.
1. Create your settings file (YAML).
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
This will generate an output file for you. Your patch file will have a `.apred` or `.apblue` file extension.
3. Open `ArchipelagoLauncher.exe`
4. Select "Open Patch" on the left side and select your patch file.
5. If this is your first time patching, you will be prompted to locate your vanilla ROM.
6. A patched `.gb` file will be created in the same place as the patch file.
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
BizHawk install.
Double-click on your patch file to start your client and start the ROM patch process. Once the process is finished
(this can take a while), the client and the emulator will be started automatically (if you associated the extension
to the emulator as recommended).
If you're playing a single-player seed and you don't care about autotracking or hints, you can stop here, close the
client, and load the patched ROM in any emulator. However, for multiworlds and other Archipelago features, continue
below using BizHawk as your emulator.
### Connect to the Multiserver
Once both the client and the emulator are started, you must connect them. Navigate to your Archipelago install folder,
then to `data/lua`, and drag+drop the `connector_pkmn_rb.lua` script onto the main EmuHawk window. (You could instead
open the Lua Console manually, click `Script``Open Script`, and navigate to `connector_pkmn_rb.lua` with the file
picker.)
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
in case you have to close and reopen a window mid-game for some reason.
1. Pokémon Red and Blue use Archipelago's BizHawk Client. If the client isn't still open from when you patched your
game, you can re-open it from the launcher.
2. Ensure EmuHawk is running the patched ROM.
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
4. In the Lua Console window, go to `Script > Open Script…`.
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk
Client window should indicate that it connected and recognized Pokémon Red/Blue.
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
top text field of the client and click Connect.
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
Now you are ready to start your adventure in Kanto.
## Auto-Tracking
Pokémon Red and Blue has a fully functional map tracker that supports auto-tracking.
@ -102,4 +114,5 @@ Pokémon Red and Blue has a fully functional map tracker that supports auto-trac
3. Click on the "AP" symbol at the top.
4. Enter the AP address, slot name and password.
The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It will hide checks & adjust logic accordingly.
The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It
will hide checks & adjust logic accordingly.

View File

@ -539,6 +539,10 @@ def generate_output(self, output_directory: str):
write_bytes(data, self.rival_name, rom_addresses['Rival_Name'])
data[0xFF00] = 2 # client compatibility version
rom_name = bytearray(f'AP{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0',
'utf8')[:21]
rom_name.extend([0] * (21 - len(rom_name)))
write_bytes(data, rom_name, 0xFFC6)
write_bytes(data, self.multiworld.seed_name.encode(), 0xFFDB)
write_bytes(data, self.multiworld.player_name[self.player].encode(), 0xFFF0)

View File

@ -12,101 +12,101 @@ rom_addresses = {
"Player_Name": 0x4568,
"Rival_Name": 0x4570,
"Price_Master_Ball": 0x45c8,
"Title_Seed": 0x5f1b,
"Title_Slot_Name": 0x5f3b,
"PC_Item": 0x6309,
"PC_Item_Quantity": 0x630e,
"Fly_Location": 0x631c,
"Skip_Player_Name": 0x6335,
"Skip_Rival_Name": 0x6343,
"Pallet_Fly_Coords": 0x666e,
"Option_Old_Man": 0xcb0e,
"Option_Old_Man_Lying": 0xcb11,
"Option_Route3_Guard_A": 0xcb17,
"Option_Trashed_House_Guard_A": 0xcb20,
"Option_Trashed_House_Guard_B": 0xcb26,
"Option_Boulders": 0xcdb7,
"Option_Rock_Tunnel_Extra_Items": 0xcdc0,
"Wild_Route1": 0xd13b,
"Wild_Route2": 0xd151,
"Wild_Route22": 0xd167,
"Wild_ViridianForest": 0xd17d,
"Wild_Route3": 0xd193,
"Wild_MtMoon1F": 0xd1a9,
"Wild_MtMoonB1F": 0xd1bf,
"Wild_MtMoonB2F": 0xd1d5,
"Wild_Route4": 0xd1eb,
"Wild_Route24": 0xd201,
"Wild_Route25": 0xd217,
"Wild_Route9": 0xd22d,
"Wild_Route5": 0xd243,
"Wild_Route6": 0xd259,
"Wild_Route11": 0xd26f,
"Wild_RockTunnel1F": 0xd285,
"Wild_RockTunnelB1F": 0xd29b,
"Wild_Route10": 0xd2b1,
"Wild_Route12": 0xd2c7,
"Wild_Route8": 0xd2dd,
"Wild_Route7": 0xd2f3,
"Wild_PokemonTower3F": 0xd30d,
"Wild_PokemonTower4F": 0xd323,
"Wild_PokemonTower5F": 0xd339,
"Wild_PokemonTower6F": 0xd34f,
"Wild_PokemonTower7F": 0xd365,
"Wild_Route13": 0xd37b,
"Wild_Route14": 0xd391,
"Wild_Route15": 0xd3a7,
"Wild_Route16": 0xd3bd,
"Wild_Route17": 0xd3d3,
"Wild_Route18": 0xd3e9,
"Wild_SafariZoneCenter": 0xd3ff,
"Wild_SafariZoneEast": 0xd415,
"Wild_SafariZoneNorth": 0xd42b,
"Wild_SafariZoneWest": 0xd441,
"Wild_SeaRoutes": 0xd458,
"Wild_SeafoamIslands1F": 0xd46d,
"Wild_SeafoamIslandsB1F": 0xd483,
"Wild_SeafoamIslandsB2F": 0xd499,
"Wild_SeafoamIslandsB3F": 0xd4af,
"Wild_SeafoamIslandsB4F": 0xd4c5,
"Wild_PokemonMansion1F": 0xd4db,
"Wild_PokemonMansion2F": 0xd4f1,
"Wild_PokemonMansion3F": 0xd507,
"Wild_PokemonMansionB1F": 0xd51d,
"Wild_Route21": 0xd533,
"Wild_Surf_Route21": 0xd548,
"Wild_CeruleanCave1F": 0xd55d,
"Wild_CeruleanCave2F": 0xd573,
"Wild_CeruleanCaveB1F": 0xd589,
"Wild_PowerPlant": 0xd59f,
"Wild_Route23": 0xd5b5,
"Wild_VictoryRoad2F": 0xd5cb,
"Wild_VictoryRoad3F": 0xd5e1,
"Wild_VictoryRoad1F": 0xd5f7,
"Wild_DiglettsCave": 0xd60d,
"Ghost_Battle5": 0xd781,
"HM_Surf_Badge_a": 0xda73,
"HM_Surf_Badge_b": 0xda78,
"Option_Fix_Combat_Bugs_Heal_Stat_Modifiers": 0xdcc2,
"Option_Silph_Scope_Skip": 0xe207,
"Wild_Old_Rod": 0xe382,
"Wild_Good_Rod": 0xe3af,
"Option_Fix_Combat_Bugs_PP_Restore": 0xe541,
"Option_Reusable_TMs": 0xe675,
"Wild_Super_Rod_A": 0xeaa9,
"Wild_Super_Rod_B": 0xeaae,
"Wild_Super_Rod_C": 0xeab3,
"Wild_Super_Rod_D": 0xeaba,
"Wild_Super_Rod_E": 0xeabf,
"Wild_Super_Rod_F": 0xeac4,
"Wild_Super_Rod_G": 0xeacd,
"Wild_Super_Rod_H": 0xead6,
"Wild_Super_Rod_I": 0xeadf,
"Wild_Super_Rod_J": 0xeae8,
"Starting_Money_High": 0xf9aa,
"Starting_Money_Middle": 0xf9ad,
"Starting_Money_Low": 0xf9b0,
"Option_Pokedex_Seen": 0xf9cb,
"Title_Seed": 0x5f22,
"Title_Slot_Name": 0x5f42,
"PC_Item": 0x6310,
"PC_Item_Quantity": 0x6315,
"Fly_Location": 0x6323,
"Skip_Player_Name": 0x633c,
"Skip_Rival_Name": 0x634a,
"Pallet_Fly_Coords": 0x6675,
"Option_Old_Man": 0xcb0b,
"Option_Old_Man_Lying": 0xcb0e,
"Option_Route3_Guard_A": 0xcb14,
"Option_Trashed_House_Guard_A": 0xcb1d,
"Option_Trashed_House_Guard_B": 0xcb23,
"Option_Boulders": 0xcdb4,
"Option_Rock_Tunnel_Extra_Items": 0xcdbd,
"Wild_Route1": 0xd138,
"Wild_Route2": 0xd14e,
"Wild_Route22": 0xd164,
"Wild_ViridianForest": 0xd17a,
"Wild_Route3": 0xd190,
"Wild_MtMoon1F": 0xd1a6,
"Wild_MtMoonB1F": 0xd1bc,
"Wild_MtMoonB2F": 0xd1d2,
"Wild_Route4": 0xd1e8,
"Wild_Route24": 0xd1fe,
"Wild_Route25": 0xd214,
"Wild_Route9": 0xd22a,
"Wild_Route5": 0xd240,
"Wild_Route6": 0xd256,
"Wild_Route11": 0xd26c,
"Wild_RockTunnel1F": 0xd282,
"Wild_RockTunnelB1F": 0xd298,
"Wild_Route10": 0xd2ae,
"Wild_Route12": 0xd2c4,
"Wild_Route8": 0xd2da,
"Wild_Route7": 0xd2f0,
"Wild_PokemonTower3F": 0xd30a,
"Wild_PokemonTower4F": 0xd320,
"Wild_PokemonTower5F": 0xd336,
"Wild_PokemonTower6F": 0xd34c,
"Wild_PokemonTower7F": 0xd362,
"Wild_Route13": 0xd378,
"Wild_Route14": 0xd38e,
"Wild_Route15": 0xd3a4,
"Wild_Route16": 0xd3ba,
"Wild_Route17": 0xd3d0,
"Wild_Route18": 0xd3e6,
"Wild_SafariZoneCenter": 0xd3fc,
"Wild_SafariZoneEast": 0xd412,
"Wild_SafariZoneNorth": 0xd428,
"Wild_SafariZoneWest": 0xd43e,
"Wild_SeaRoutes": 0xd455,
"Wild_SeafoamIslands1F": 0xd46a,
"Wild_SeafoamIslandsB1F": 0xd480,
"Wild_SeafoamIslandsB2F": 0xd496,
"Wild_SeafoamIslandsB3F": 0xd4ac,
"Wild_SeafoamIslandsB4F": 0xd4c2,
"Wild_PokemonMansion1F": 0xd4d8,
"Wild_PokemonMansion2F": 0xd4ee,
"Wild_PokemonMansion3F": 0xd504,
"Wild_PokemonMansionB1F": 0xd51a,
"Wild_Route21": 0xd530,
"Wild_Surf_Route21": 0xd545,
"Wild_CeruleanCave1F": 0xd55a,
"Wild_CeruleanCave2F": 0xd570,
"Wild_CeruleanCaveB1F": 0xd586,
"Wild_PowerPlant": 0xd59c,
"Wild_Route23": 0xd5b2,
"Wild_VictoryRoad2F": 0xd5c8,
"Wild_VictoryRoad3F": 0xd5de,
"Wild_VictoryRoad1F": 0xd5f4,
"Wild_DiglettsCave": 0xd60a,
"Ghost_Battle5": 0xd77e,
"HM_Surf_Badge_a": 0xda70,
"HM_Surf_Badge_b": 0xda75,
"Option_Fix_Combat_Bugs_Heal_Stat_Modifiers": 0xdcbf,
"Option_Silph_Scope_Skip": 0xe204,
"Wild_Old_Rod": 0xe37f,
"Wild_Good_Rod": 0xe3ac,
"Option_Fix_Combat_Bugs_PP_Restore": 0xe53e,
"Option_Reusable_TMs": 0xe672,
"Wild_Super_Rod_A": 0xeaa6,
"Wild_Super_Rod_B": 0xeaab,
"Wild_Super_Rod_C": 0xeab0,
"Wild_Super_Rod_D": 0xeab7,
"Wild_Super_Rod_E": 0xeabc,
"Wild_Super_Rod_F": 0xeac1,
"Wild_Super_Rod_G": 0xeaca,
"Wild_Super_Rod_H": 0xead3,
"Wild_Super_Rod_I": 0xeadc,
"Wild_Super_Rod_J": 0xeae5,
"Starting_Money_High": 0xf9a7,
"Starting_Money_Middle": 0xf9aa,
"Starting_Money_Low": 0xf9ad,
"Option_Pokedex_Seen": 0xf9c8,
"HM_Fly_Badge_a": 0x13182,
"HM_Fly_Badge_b": 0x13187,
"HM_Cut_Badge_a": 0x131b8,
@ -1164,22 +1164,22 @@ rom_addresses = {
"Prize_Mon_E": 0x52944,
"Prize_Mon_F": 0x52946,
"Start_Inventory": 0x52a7b,
"Map_Fly_Location": 0x52c6f,
"Reset_A": 0x52d1b,
"Reset_B": 0x52d47,
"Reset_C": 0x52d73,
"Reset_D": 0x52d9f,
"Reset_E": 0x52dcb,
"Reset_F": 0x52df7,
"Reset_G": 0x52e23,
"Reset_H": 0x52e4f,
"Reset_I": 0x52e7b,
"Reset_J": 0x52ea7,
"Reset_K": 0x52ed3,
"Reset_L": 0x52eff,
"Reset_M": 0x52f2b,
"Reset_N": 0x52f57,
"Reset_O": 0x52f83,
"Map_Fly_Location": 0x52c75,
"Reset_A": 0x52d21,
"Reset_B": 0x52d4d,
"Reset_C": 0x52d79,
"Reset_D": 0x52da5,
"Reset_E": 0x52dd1,
"Reset_F": 0x52dfd,
"Reset_G": 0x52e29,
"Reset_H": 0x52e55,
"Reset_I": 0x52e81,
"Reset_J": 0x52ead,
"Reset_K": 0x52ed9,
"Reset_L": 0x52f05,
"Reset_M": 0x52f31,
"Reset_N": 0x52f5d,
"Reset_O": 0x52f89,
"Warps_Route2": 0x54026,
"Missable_Route_2_Item_1": 0x5404a,
"Missable_Route_2_Item_2": 0x54051,