2022-10-13 05:45:52 +00:00
|
|
|
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
|
2022-11-02 14:51:35 +00:00
|
|
|
from Utils import async_start
|
2022-10-13 05:45:52 +00:00
|
|
|
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
|
|
|
get_base_parser
|
|
|
|
|
|
|
|
from worlds.pokemon_rb.locations import location_data
|
2022-12-11 19:51:28 +00:00
|
|
|
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
|
2022-10-13 05:45:52 +00:00
|
|
|
|
2023-03-13 22:40:55 +00:00
|
|
|
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
|
2022-10-13 05:45:52 +00:00
|
|
|
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}
|
|
|
|
|
Pokémon Red and Blue: Version 4 update (#1963)
## What is this fixing or adding?
Adds a large number of new options, including:
- Door Shuffle
- Sphere-based level scaling
- Key Item and Pokedex requirement options to reach the Elite Four
- Split Card Key option
- Dexsanity option can be set to a percentage of Pokémon that will be checks
- Stonesanity: remove the stones from the Celadon Department Store and shuffle them into the item pool, replacing 4 of the 5 Moon Stone items
- Sleep Trap items option
- Randomize Move Types option
- Town Map Fly Location option, to unlock a flight location when finding/receiving the Town Map
Many enhancements have been made, including:
- Game allows you to continue your save file _from Pallet Town_ as a way to save warp back to the beginning of the game. The one-way drop from Diglett's Cave to north Route 2 that had been added to the randomizer has been removed.
- Client auto-hints some locations when you are able to see the item before you can obtain it (but would only show AP Item if it is for another player), including Bike Shop, Oak's Aides, Celadon Prize Corner, and the unchosen Fossil location.
Various bugs have been fixed, including:
- Route 13 wild Pokémon not correctly logically requiring Cut
- Vanilla tm/hm compatibility options giving compatibility for many TMs/HMs erroneously
- If an item that exists in multiple quantities in the item pool is chosen for one of the locations that are pre-filled with local items, it will continue placing that same item in the remaining locations as long as more of that item exist
- `start_with` option for `randomize_pokedex` still shuffling a Pokédex into the item pool
- The obedience threshold levels being incorrect with 0-2 badges, with Pokémon up to level 30 obeying with 0-1 badges and up to 10 with 2 badges
- Receiving a DeathLink trigger in the Safari Zone causing issues. Now, you will have your steps remaining set to 0 instead of blacking out when you're in the Safari Zone.
Many location names have been changed, as location names are automatically prepended using the Region name and a large number of areas have been split into new regions as part of the overhaul to add Door Shuffle.
2023-07-23 22:46:54 +00:00
|
|
|
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"
|
|
|
|
and location.address is not None}
|
|
|
|
|
2022-10-13 05:45:52 +00:00
|
|
|
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
|
|
|
|
|
2023-03-13 22:40:55 +00:00
|
|
|
SCRIPT_VERSION = 3
|
2022-12-07 23:38:34 +00:00
|
|
|
|
2022-10-13 05:45:52 +00:00
|
|
|
|
|
|
|
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
|
2022-12-07 23:38:34 +00:00
|
|
|
self.deathlink_pending = False
|
|
|
|
self.set_deathlink = False
|
|
|
|
self.client_compatibility_mode = 0
|
|
|
|
self.items_handling = 0b001
|
2023-03-13 22:40:55 +00:00
|
|
|
self.sent_release = False
|
|
|
|
self.sent_collect = False
|
Pokémon Red and Blue: Version 4 update (#1963)
## What is this fixing or adding?
Adds a large number of new options, including:
- Door Shuffle
- Sphere-based level scaling
- Key Item and Pokedex requirement options to reach the Elite Four
- Split Card Key option
- Dexsanity option can be set to a percentage of Pokémon that will be checks
- Stonesanity: remove the stones from the Celadon Department Store and shuffle them into the item pool, replacing 4 of the 5 Moon Stone items
- Sleep Trap items option
- Randomize Move Types option
- Town Map Fly Location option, to unlock a flight location when finding/receiving the Town Map
Many enhancements have been made, including:
- Game allows you to continue your save file _from Pallet Town_ as a way to save warp back to the beginning of the game. The one-way drop from Diglett's Cave to north Route 2 that had been added to the randomizer has been removed.
- Client auto-hints some locations when you are able to see the item before you can obtain it (but would only show AP Item if it is for another player), including Bike Shop, Oak's Aides, Celadon Prize Corner, and the unchosen Fossil location.
Various bugs have been fixed, including:
- Route 13 wild Pokémon not correctly logically requiring Cut
- Vanilla tm/hm compatibility options giving compatibility for many TMs/HMs erroneously
- If an item that exists in multiple quantities in the item pool is chosen for one of the locations that are pre-filled with local items, it will continue placing that same item in the remaining locations as long as more of that item exist
- `start_with` option for `randomize_pokedex` still shuffling a Pokédex into the item pool
- The obedience threshold levels being incorrect with 0-2 badges, with Pokémon up to level 30 obeying with 0-1 badges and up to 10 with 2 badges
- Receiving a DeathLink trigger in the Safari Zone causing issues. Now, you will have your steps remaining set to 0 instead of blacking out when you're in the Safari Zone.
Many location names have been changed, as location names are automatically prepended using the Region name and a large number of areas have been split into new regions as part of the overhaul to add Door Shuffle.
2023-07-23 22:46:54 +00:00
|
|
|
self.auto_hints = set()
|
2022-10-13 05:45:52 +00:00
|
|
|
|
|
|
|
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
|
2023-06-26 06:53:44 +00:00
|
|
|
logger.info('Awaiting connection to EmuHawk to get Player information')
|
2022-10-13 05:45:52 +00:00
|
|
|
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
|
2022-12-07 23:38:34 +00:00
|
|
|
if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
|
|
|
|
self.set_deathlink = True
|
2022-10-13 05:45:52 +00:00
|
|
|
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)
|
|
|
|
|
2022-12-07 23:38:34 +00:00
|
|
|
def on_deathlink(self, data: dict):
|
|
|
|
self.deathlink_pending = True
|
|
|
|
super().on_deathlink(data)
|
|
|
|
|
2022-10-13 05:45:52 +00:00
|
|
|
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()
|
2022-12-07 23:38:34 +00:00
|
|
|
ret = json.dumps(
|
2022-10-13 05:45:52 +00:00
|
|
|
{
|
|
|
|
"items": [item.item for item in ctx.items_received],
|
|
|
|
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
2022-12-07 23:38:34 +00:00
|
|
|
if key[0] > current_time - 10},
|
2023-03-13 22:40:55 +00:00
|
|
|
"deathlink": ctx.deathlink_pending,
|
|
|
|
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
|
2022-10-13 05:45:52 +00:00
|
|
|
}
|
|
|
|
)
|
2022-12-07 23:38:34 +00:00
|
|
|
ctx.deathlink_pending = False
|
|
|
|
return ret
|
2022-10-13 05:45:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def parse_locations(data: List, ctx: GBContext):
|
|
|
|
locations = []
|
|
|
|
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
|
2023-03-13 22:40:55 +00:00
|
|
|
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
|
|
|
|
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
|
2022-10-13 05:45:52 +00:00
|
|
|
|
2023-03-13 22:40:55 +00:00
|
|
|
if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
|
|
|
|
flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
|
|
|
|
else:
|
|
|
|
flags["DexSanityFlag"] = [0] * 19
|
2022-10-13 05:45:52 +00:00
|
|
|
|
|
|
|
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)
|
Pokémon Red and Blue: Version 4 update (#1963)
## What is this fixing or adding?
Adds a large number of new options, including:
- Door Shuffle
- Sphere-based level scaling
- Key Item and Pokedex requirement options to reach the Elite Four
- Split Card Key option
- Dexsanity option can be set to a percentage of Pokémon that will be checks
- Stonesanity: remove the stones from the Celadon Department Store and shuffle them into the item pool, replacing 4 of the 5 Moon Stone items
- Sleep Trap items option
- Randomize Move Types option
- Town Map Fly Location option, to unlock a flight location when finding/receiving the Town Map
Many enhancements have been made, including:
- Game allows you to continue your save file _from Pallet Town_ as a way to save warp back to the beginning of the game. The one-way drop from Diglett's Cave to north Route 2 that had been added to the randomizer has been removed.
- Client auto-hints some locations when you are able to see the item before you can obtain it (but would only show AP Item if it is for another player), including Bike Shop, Oak's Aides, Celadon Prize Corner, and the unchosen Fossil location.
Various bugs have been fixed, including:
- Route 13 wild Pokémon not correctly logically requiring Cut
- Vanilla tm/hm compatibility options giving compatibility for many TMs/HMs erroneously
- If an item that exists in multiple quantities in the item pool is chosen for one of the locations that are pre-filled with local items, it will continue placing that same item in the remaining locations as long as more of that item exist
- `start_with` option for `randomize_pokedex` still shuffling a Pokédex into the item pool
- The obedience threshold levels being incorrect with 0-2 badges, with Pokémon up to level 30 obeying with 0-1 badges and up to 10 with 2 badges
- Receiving a DeathLink trigger in the Safari Zone causing issues. Now, you will have your steps remaining set to 0 instead of blacking out when you're in the Safari Zone.
Many location names have been changed, as location names are automatically prepended using the Region name and a large number of areas have been split into new regions as part of the overhaul to add Door Shuffle.
2023-07-23 22:46:54 +00:00
|
|
|
|
|
|
|
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")
|
2023-08-02 17:51:53 +00:00
|
|
|
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
|
|
|
|
]
|
Pokémon Red and Blue: Version 4 update (#1963)
## What is this fixing or adding?
Adds a large number of new options, including:
- Door Shuffle
- Sphere-based level scaling
- Key Item and Pokedex requirement options to reach the Elite Four
- Split Card Key option
- Dexsanity option can be set to a percentage of Pokémon that will be checks
- Stonesanity: remove the stones from the Celadon Department Store and shuffle them into the item pool, replacing 4 of the 5 Moon Stone items
- Sleep Trap items option
- Randomize Move Types option
- Town Map Fly Location option, to unlock a flight location when finding/receiving the Town Map
Many enhancements have been made, including:
- Game allows you to continue your save file _from Pallet Town_ as a way to save warp back to the beginning of the game. The one-way drop from Diglett's Cave to north Route 2 that had been added to the randomizer has been removed.
- Client auto-hints some locations when you are able to see the item before you can obtain it (but would only show AP Item if it is for another player), including Bike Shop, Oak's Aides, Celadon Prize Corner, and the unchosen Fossil location.
Various bugs have been fixed, including:
- Route 13 wild Pokémon not correctly logically requiring Cut
- Vanilla tm/hm compatibility options giving compatibility for many TMs/HMs erroneously
- If an item that exists in multiple quantities in the item pool is chosen for one of the locations that are pre-filled with local items, it will continue placing that same item in the remaining locations as long as more of that item exist
- `start_with` option for `randomize_pokedex` still shuffling a Pokédex into the item pool
- The obedience threshold levels being incorrect with 0-2 badges, with Pokémon up to level 30 obeying with 0-1 badges and up to 10 with 2 badges
- Receiving a DeathLink trigger in the Safari Zone causing issues. Now, you will have your steps remaining set to 0 instead of blacking out when you're in the Safari Zone.
Many location names have been changed, as location names are automatically prepended using the Region name and a large number of areas have been split into new regions as part of the overhaul to add Door Shuffle.
2023-07-23 22:46:54 +00:00
|
|
|
if hints:
|
|
|
|
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": hints, "create_as_hint": 2}])
|
|
|
|
ctx.auto_hints.update(hints)
|
|
|
|
|
2022-10-13 05:45:52 +00:00
|
|
|
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())
|
2022-12-07 23:38:34 +00:00
|
|
|
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
|
2022-11-01 06:02:15 +00:00
|
|
|
if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]):
|
2022-10-13 05:45:52 +00:00
|
|
|
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
|
2022-11-01 06:02:15 +00:00
|
|
|
ctx.seed_name = ''.join([chr(i) for i in data_decoded['seedName'] if i != 0])
|
2022-10-13 05:45:52 +00:00
|
|
|
if not ctx.auth:
|
|
|
|
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
|
|
|
if ctx.auth == '':
|
2022-12-07 23:38:34 +00:00
|
|
|
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
|
2022-10-13 05:45:52 +00:00
|
|
|
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
|
2022-11-02 14:51:35 +00:00
|
|
|
async_start(parse_locations(data_decoded['locations'], ctx))
|
2022-12-07 23:38:34 +00:00
|
|
|
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!")
|
2023-03-13 22:40:55 +00:00
|
|
|
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)
|
2022-12-07 23:38:34 +00:00
|
|
|
if ctx.set_deathlink:
|
|
|
|
await ctx.update_death_link(True)
|
2022-10-13 05:45:52 +00:00
|
|
|
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'
|
2022-12-11 19:51:28 +00:00
|
|
|
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)
|
2022-10-13 05:45:52 +00:00
|
|
|
|
|
|
|
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
|
|
|
|
with patch_archive.open('delta.bsdiff4', 'r') as stream:
|
|
|
|
patch = stream.read()
|
2022-11-06 08:07:41 +00:00
|
|
|
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))
|
2022-10-13 05:45:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
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...")
|
2022-11-02 14:51:35 +00:00
|
|
|
async_start(patch_and_run_game("red", args.patch_file, ctx))
|
2022-10-13 05:45:52 +00:00
|
|
|
elif ext == "apblue":
|
|
|
|
logger.info("APBLUE file supplied, beginning patching process...")
|
2022-11-02 14:51:35 +00:00
|
|
|
async_start(patch_and_run_game("blue", args.patch_file, ctx))
|
2022-10-13 05:45:52 +00:00
|
|
|
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()
|