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:
parent
edb62004ef
commit
8a852abdc4
382
PokemonClient.py
382
PokemonClient.py
|
@ -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()
|
|
@ -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()
|
|
@ -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: "";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue