diff --git a/PokemonClient.py b/PokemonClient.py deleted file mode 100644 index 6b43a53b8ff7..000000000000 --- a/PokemonClient.py +++ /dev/null @@ -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() diff --git a/data/lua/connector_pkmn_rb.lua b/data/lua/connector_pkmn_rb.lua deleted file mode 100644 index 3f56435bdbee..000000000000 --- a/data/lua/connector_pkmn_rb.lua +++ /dev/null @@ -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() diff --git a/inno_setup.iss b/inno_setup.iss index b4779b1067b7..4744fa2b724d 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -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: ""; diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index c3ae2b0495b0..31739bb24606 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -101,8 +101,6 @@ def launch_textclient(): 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 diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index b2ee0702c91e..d9bd6dde76e5 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -2,9 +2,11 @@ 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 .level_scaling import level_scaling from . import logic from . import poke_data +from . import client class PokemonSettings(settings.Group): @@ -36,16 +39,8 @@ class BlueRomFile(settings.UserFilePath): 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 @@ def encode_name(name, t): 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 @@ def stage_generate_output(cls, multiworld, output_directory): 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") diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index eb4d83360cd8..bee5a8d2f499 100644 Binary files a/worlds/pokemon_rb/basepatch_blue.bsdiff4 and b/worlds/pokemon_rb/basepatch_blue.bsdiff4 differ diff --git a/worlds/pokemon_rb/basepatch_red.bsdiff4 b/worlds/pokemon_rb/basepatch_red.bsdiff4 index cffb0b7e0653..f2db54a84fda 100644 Binary files a/worlds/pokemon_rb/basepatch_red.bsdiff4 and b/worlds/pokemon_rb/basepatch_red.bsdiff4 differ diff --git a/worlds/pokemon_rb/client.py b/worlds/pokemon_rb/client.py new file mode 100644 index 000000000000..fb29045cf4e8 --- /dev/null +++ b/worlds/pokemon_rb/client.py @@ -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 diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index 086ec347f34f..b164d4b0fef6 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -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. \ No newline at end of file diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md index 7ba9b3aa09e3..c9344959f6b9 100644 --- a/worlds/pokemon_rb/docs/setup_en.md +++ b/worlds/pokemon_rb/docs/setup_en.md @@ -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 `
:` on the textfield on top and press enter (if the server uses password, type in the bottom textfield `/connect
: [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. diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 096ab8e0a1f6..81ab6648dd19 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -539,6 +539,10 @@ def set_trade_mon(address, loc): 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) diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index 97faf7bff205..cd57e317bdeb 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -12,101 +12,101 @@ "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 @@ "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,