forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
430 lines
17 KiB
Python
430 lines
17 KiB
Python
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .Items import pokemon_stadium_items, gym_badge_codes, box_upgrade_items, cup_tier_upgrade_items
|
|
from .Locations import pokemon_stadium_locations, event_locations
|
|
from NetUtils import ClientStatus
|
|
from .Types import LocData
|
|
import Utils
|
|
import worlds._bizhawk as bizhawk
|
|
from worlds._bizhawk.client import BizHawkClient
|
|
|
|
logger = logging.getLogger('Client')
|
|
|
|
if TYPE_CHECKING:
|
|
from worlds._bizhawk.context import BizHawkClientContext
|
|
|
|
class PokemonStadiumClient(BizHawkClient):
|
|
game = 'Pokemon Stadium'
|
|
system = 'N64'
|
|
patch_suffix = '.apstadium'
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.local_checked_locations = set()
|
|
self.glc_loaded = False
|
|
self.cups_loaded = False
|
|
self.minigame_index = None
|
|
self.minigame_done = False
|
|
self.minigame_check_sent = False
|
|
|
|
async def validate_rom(self, ctx: 'BizHawkClientContext') -> bool:
|
|
try:
|
|
# Check ROM name
|
|
rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x20, 15, 'ROM')]))[0]).decode('ascii')
|
|
if rom_name != 'POKEMON STADIUM':
|
|
logger.info('Invalid ROM for Pokemon Stadium AP World')
|
|
return False
|
|
except bizhawk.RequestFailedError:
|
|
return False
|
|
|
|
ctx.game = self.game
|
|
ctx.items_handling = 0b111
|
|
ctx.want_slot_data = True
|
|
|
|
return True
|
|
|
|
async def game_watcher(self, ctx: 'BizHawkClientContext') -> None:
|
|
item_codes = {net_item.item for net_item in ctx.items_received}
|
|
|
|
flags = await bizhawk.read(ctx.bizhawk_ctx, [
|
|
(0x420000, 4, 'RDRAM'), # GLC Flag
|
|
(0x420010, 4, 'RDRAM'), # Entered Battle Flag
|
|
(0x148AC8, 12, 'RDRAM'), # Beat Rival Flag
|
|
(0x12FC1C, 4, 'RDRAM'), # Minigame being played
|
|
(0x124860, 4, 'RDRAM'), # Minigame results
|
|
(0xAE77F, 1, 'RDRAM'), # Enemy team HP slot 1
|
|
(0xAE7D3, 1, 'RDRAM'), # Enemy team HP slot 2
|
|
(0xAE827, 1, 'RDRAM'), # Enemy team HP slot 3
|
|
(0x220C19, 3, 'RDRAM'), # GLC Rentals address
|
|
(0x221D99, 3, 'RDRAM'), # GLC Registration table address
|
|
(0x218CE9, 3, 'RDRAM'), # Poke Cup Rentals address
|
|
(0x219E69, 3, 'RDRAM'), # Poke Cup Registration table address
|
|
(0x218CB9, 3, 'RDRAM'), # Prime Cup Rentals address
|
|
(0x219E39, 3, 'RDRAM'), # Prime Cup Registration table address
|
|
(0x218C99, 3, 'RDRAM'), # Petit Cup Rentals address
|
|
(0x219E19, 3, 'RDRAM'), # Petit Cup Registration table address
|
|
(0x218CA9, 3, 'RDRAM'), # Pika Cup Rentals address
|
|
(0x219E29, 3, 'RDRAM'), # Pika Cup Registration table address
|
|
(0x420020, 4, 'RDRAM'), # Picking a Cup tier
|
|
]
|
|
)
|
|
|
|
player_has_battled = flags[1] != b'\x00\x00\x00\x00'
|
|
battle_info = await bizhawk.read(ctx.bizhawk_ctx, [(0x0AE540, 4, 'RDRAM')])
|
|
mode = int(battle_info[0].hex()[:2])
|
|
gym_info = battle_info[0].hex()[4:]
|
|
gym_number = int(battle_info[0].hex()[4:6])
|
|
trainer_index = int(battle_info[0].hex()[6:])
|
|
|
|
if player_has_battled:
|
|
player_won = all(x == b'\x00' for x in flags[5:8])
|
|
|
|
if player_won:
|
|
ap_code = 20000000 + (mode * 100) + (gym_number * 10) + trainer_index
|
|
|
|
# If a Gym Leader was beaten or the last trainer for a Cup was beaten an additional check must be sent
|
|
if mode == 7 and trainer_index == 4:
|
|
locations_to_check = set([ap_code, ap_code + 1])
|
|
elif trainer_index == 8:
|
|
locations_to_check = set([ap_code, ap_code - trainer_index, ap_code + 1])
|
|
else:
|
|
locations_to_check = set([ap_code])
|
|
|
|
try:
|
|
await ctx.check_locations(locations_to_check)
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x420010, [0x00, 0x00, 0x00, 0x00], 'RDRAM')])
|
|
self.glc_loaded = False
|
|
except:
|
|
pass
|
|
|
|
glc_flag = int.from_bytes(flags[0], byteorder='big')
|
|
if glc_flag == 2 and not self.glc_loaded:
|
|
self.glc_loaded = True
|
|
|
|
self.GLC_UNLOCK_FLAGS = [
|
|
0x147B70, # Pewter
|
|
0x147B98, # Cerulean
|
|
0x147BC0, # Vermilion
|
|
0x147BE8, # Celadon
|
|
0x147C10, # Fuchsia
|
|
0x147C38, # Saffron
|
|
0x147C60, # Cinnabar
|
|
0x147C88, # Viridian
|
|
0x147CB1, # E4 entrance
|
|
0x147CD9, # E4 exit
|
|
0x147D01, # E4
|
|
]
|
|
|
|
# UUDDLLRR
|
|
self.GLC_CURSOR_TARGETS = [
|
|
0x147B84, # Brock, 00000002
|
|
0x147BAC, # Misty, 03000103
|
|
0x147BD4, # Surge, 04020200
|
|
0x147BFC, # Erika, 05030500
|
|
0x147C24, # Koga, 06040604
|
|
0x147C4C, # Sabrina, 07050007
|
|
0x147C74, # Blaine, 00080608
|
|
0x147C9C, # Giovanni, 07000709
|
|
]
|
|
|
|
gym_codes = [
|
|
pokemon_stadium_items['Pewter City Key'].ap_code,
|
|
pokemon_stadium_items['Cerulean City Key'].ap_code,
|
|
pokemon_stadium_items['Vermillion City Key'].ap_code,
|
|
pokemon_stadium_items['Celadon City Key'].ap_code,
|
|
pokemon_stadium_items['Fuchsia City Key'].ap_code,
|
|
pokemon_stadium_items['Saffron City Key'].ap_code,
|
|
pokemon_stadium_items['Cinnabar Island Key'].ap_code,
|
|
pokemon_stadium_items['Viridian City Key'].ap_code,
|
|
]
|
|
|
|
self.unlocked_gyms = [i + 1 for i, code in enumerate(gym_codes) if code in item_codes]
|
|
victory_road_open = set(gym_badge_codes).issubset(item_codes)
|
|
if victory_road_open:
|
|
self.unlocked_gyms.append(9)
|
|
|
|
if gym_codes[0] in item_codes:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[0], [0x00, 0x01], 'RDRAM')])
|
|
await self.update_brock_cursor(ctx)
|
|
|
|
if gym_codes[1] in item_codes:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[1], [0x00, 0x01], 'RDRAM')])
|
|
await self.update_misty_cursor(ctx)
|
|
|
|
if gym_codes[2] in item_codes:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[2], [0x00, 0x01], 'RDRAM')])
|
|
await self.update_surge_cursor(ctx)
|
|
|
|
if gym_codes[3] in item_codes:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[3], [0x00, 0x01], 'RDRAM')])
|
|
await self.update_erika_cursor(ctx)
|
|
|
|
if gym_codes[4] in item_codes:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[4], [0x00, 0x01], 'RDRAM')])
|
|
await self.update_koga_cursor(ctx)
|
|
|
|
if gym_codes[5] in item_codes:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[5], [0x00, 0x01], 'RDRAM')])
|
|
await self.update_sabrina_cursor(ctx)
|
|
|
|
if gym_codes[6] in item_codes:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[6], [0x00, 0x01], 'RDRAM')])
|
|
await self.update_blaine_cursor(ctx)
|
|
|
|
if gym_codes[7] in item_codes:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[7], [0x00, 0x01], 'RDRAM')])
|
|
await self.update_giovanni_cursor(ctx, item_codes)
|
|
|
|
if victory_road_open:
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[8], [0x01], 'RDRAM')])
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[9], [0x01], 'RDRAM')])
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_UNLOCK_FLAGS[10], [0x01], 'RDRAM')])
|
|
|
|
if len(self.unlocked_gyms) > 0 and gym_info != '0804':
|
|
first_gym = self.unlocked_gyms[0] - 1
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x147D50, [0x00, first_gym], 'RDRAM')])
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x146F38, [0x52, 0x61, 0xFF, 0x82], 'RDRAM')])
|
|
elif glc_flag != 2 and self.glc_loaded:
|
|
self.glc_loaded = False
|
|
|
|
text = flags[2].decode("ascii", errors="ignore")
|
|
if text == 'Magnificent!':
|
|
await ctx.check_locations(set([event_locations['Beat Rival'].ap_code]))
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x420010, [0x00, 0x00, 0x00, 0x00], 'RDRAM')])
|
|
|
|
cups_flag = int.from_bytes(flags[18], byteorder='big')
|
|
if cups_flag != 0 and not self.cups_loaded:
|
|
self.cups_loaded = True
|
|
|
|
if mode == 3:
|
|
cup_tier_item = cup_tier_upgrade_items['Poké Cup - Tier Upgrade'].ap_code
|
|
else:
|
|
cup_tier_item = cup_tier_upgrade_items['Prime Cup - Tier Upgrade'].ap_code
|
|
|
|
cup_tier = sum(1 for net_item in ctx.items_received if net_item.item == cup_tier_item)
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(0x147018, [0x00, 0x00, 0x00, cup_tier], 'RDRAM')])
|
|
elif cups_flag == 0:
|
|
self.cups_loaded = False
|
|
|
|
# GLC Boxes
|
|
selecting_team = flags[8] == b'\x22\x0E\x20'
|
|
registering_team = flags[9] == b'\x22\x1F\xA0'
|
|
if selecting_team or registering_team:
|
|
address = 0x220E23 if selecting_team else 0x221FA3
|
|
item = box_upgrade_items['GLC PC Box Upgrade'].ap_code
|
|
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
|
|
table_size = 29 + 20 * box_count
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
|
|
|
|
# Poke Boxes
|
|
selecting_team = flags[10] == b'\x21\x8F\x10'
|
|
registering_team = flags[11] == b'\x21\xA0\x90'
|
|
if selecting_team or registering_team:
|
|
address = 0x218F13 if selecting_team else 0x21A093
|
|
item = box_upgrade_items['Poke Cup PC Box Upgrade'].ap_code
|
|
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
|
|
table_size = 29 + 20 * box_count
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
|
|
|
|
# Prime Boxes
|
|
selecting_team = flags[12] == b'\x21\x8F\x10'
|
|
registering_team = flags[13] == b'\x21\xA0\x90'
|
|
if selecting_team or registering_team:
|
|
address = 0x218F13 if selecting_team else 0x21A093
|
|
item = box_upgrade_items['Prime Cup PC Box Upgrade'].ap_code
|
|
box_count = sum(1 for net_item in ctx.items_received if net_item.item == item)
|
|
table_size = 29 + 20 * box_count
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(address, [table_size], 'RDRAM')])
|
|
|
|
# Minigames
|
|
if flags[3].startswith(b'\x00\x03\x00') and flags[3][3] in range(9):
|
|
self.minigame_index = flags[3][3]
|
|
|
|
if self.minigame_index != None and flags[4] == b'\x00\x00\x00\x00':
|
|
self.minigame_done = False
|
|
|
|
if self.minigame_index != None and not self.minigame_done and flags[4] == b'\x01\x00\x00\x00':
|
|
self.minigame_done = True
|
|
self.minigame_check_sent = False
|
|
|
|
if self.minigame_done and self.minigame_index != None and not self.minigame_check_sent:
|
|
minigame_ap_acode = 20000100 + self.minigame_index
|
|
await ctx.check_locations([minigame_ap_acode])
|
|
|
|
self.minigame_check_sent = True
|
|
|
|
# Send game clear
|
|
if not ctx.finished_game and pokemon_stadium_items['Victory'].ap_code in item_codes:
|
|
ctx.finished_game = True
|
|
await ctx.send_msgs([{
|
|
"cmd": "StatusUpdate",
|
|
"status": ClientStatus.CLIENT_GOAL,
|
|
}])
|
|
|
|
def lowest_unlocked_from(self, lower_bound):
|
|
for i in range(lower_bound, 9):
|
|
if i in self.unlocked_gyms:
|
|
return i
|
|
return 0
|
|
|
|
def highest_unlocked_from(self, upper_bound):
|
|
for i in range(upper_bound, 0, -1):
|
|
if i in self.unlocked_gyms:
|
|
return i
|
|
return 0
|
|
|
|
async def update_brock_cursor(self, ctx):
|
|
# Determine UP: lowest unlocked gym from 4 to 9
|
|
up = self.lowest_unlocked_from(4)
|
|
|
|
# Determine RIGHT: lowest of 2 or 3 or 4 if any are unlocked
|
|
right = 0
|
|
misty_unlocked = 2 in self.unlocked_gyms
|
|
surge_unlocked = 3 in self.unlocked_gyms
|
|
erika_unlocked = 4 in self.unlocked_gyms
|
|
|
|
if misty_unlocked:
|
|
right = 2
|
|
elif surge_unlocked:
|
|
right = 3
|
|
elif erika_unlocked:
|
|
right = 4
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[0], [up, 0x00, 0x00, right], 'RDRAM')])
|
|
|
|
async def update_misty_cursor(self, ctx):
|
|
# Determine UP: lowest unlocked gym from 4 to 9
|
|
up = self.lowest_unlocked_from(4)
|
|
|
|
# Determine LEFT: is Brock unlocked
|
|
left = 1 if 1 in self.unlocked_gyms else 0
|
|
|
|
# Determine RIGHT: is Surge unlocked
|
|
right = 3 if 3 in self.unlocked_gyms else 0
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[1], [up, 0x00, left, right], 'RDRAM')])
|
|
|
|
async def update_surge_cursor(self, ctx):
|
|
# Determine UP: lowest unlocked gym from 4 to 9
|
|
up = self.lowest_unlocked_from(4)
|
|
|
|
# Determine DOWN: is Misty unlocked
|
|
down = 2 if 2 in self.unlocked_gyms else 0
|
|
|
|
# Determine LEFT: is Misty or Brock unlocked
|
|
left = 0
|
|
misty_unlocked = 2 if 2 in self.unlocked_gyms else 0
|
|
brock_unlocked = 1 if 1 in self.unlocked_gyms else 0
|
|
|
|
if misty_unlocked:
|
|
left = 2
|
|
elif brock_unlocked:
|
|
left = 1
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[2], [up, down, left, 0x00], 'RDRAM')])
|
|
|
|
async def update_erika_cursor(self, ctx):
|
|
# Determine UP: lowest unlocked gym from 5 to 9
|
|
up = self.lowest_unlocked_from(5)
|
|
|
|
# Determine DOWN: highest unlocked gym from 3 to 1
|
|
down = self.highest_unlocked_from(3)
|
|
|
|
# Determine LEFT: is Koga or Sabrina unlocked
|
|
left = 0
|
|
koga_unlocked = 5 if 5 in self.unlocked_gyms else 0
|
|
sabrina_unlocked = 6 if 6 in self.unlocked_gyms else 0
|
|
|
|
if koga_unlocked:
|
|
left = 5
|
|
elif sabrina_unlocked:
|
|
left = 6
|
|
|
|
# Determine RIGHT: is Surge unlocked
|
|
right = 3 if 3 in self.unlocked_gyms else 0
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[3], [up, down, left, right], 'RDRAM')])
|
|
|
|
async def update_koga_cursor(self, ctx):
|
|
# Determine UP: lowest unlocked gym from 6 to 9
|
|
up = self.lowest_unlocked_from(6)
|
|
|
|
# Determine DOWN: highest unlocked gym from 2 to 1
|
|
down = self.highest_unlocked_from(2)
|
|
|
|
# Determine LEFT: is Sabrina unlocked
|
|
left = 6 if 6 in self.unlocked_gyms else 0
|
|
|
|
# Determine RIGHT: is Erika or Surge unlocked
|
|
right = 0
|
|
erika_unlocked = 4 if 4 in self.unlocked_gyms else 0
|
|
surge_unlocked = 3 if 3 in self.unlocked_gyms else 0
|
|
|
|
if erika_unlocked:
|
|
right = 4
|
|
elif surge_unlocked:
|
|
right = 3
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[4], [up, down, left, right], 'RDRAM')])
|
|
|
|
async def update_sabrina_cursor(self, ctx):
|
|
# Determine DOWN: highest unlocked gym from 5 to 1
|
|
down = self.highest_unlocked_from(5)
|
|
|
|
# Determine RIGHT: is Blaine or Giovanni unlocked
|
|
right = 0
|
|
blaine_unlocked = 7 if 7 in self.unlocked_gyms else 0
|
|
giovanni_unlocked = 8 if 8 in self.unlocked_gyms else 0
|
|
|
|
if blaine_unlocked:
|
|
right = 7
|
|
elif giovanni_unlocked:
|
|
right = 8
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[5], [0x00, down, 0x00, right], 'RDRAM')])
|
|
|
|
async def update_blaine_cursor(self, ctx):
|
|
# Determine DOWN: highest unlocked gym from 5 to 1
|
|
down = self.highest_unlocked_from(5)
|
|
|
|
# Determine LEFT: is Sabrina unlocked
|
|
left = 6 if 6 in self.unlocked_gyms else 0
|
|
|
|
# Determine RIGHT: is Giovanni unlocked or do you have all badges needed
|
|
if 8 in self.unlocked_gyms:
|
|
right = 8
|
|
elif 9 in self.unlocked_gyms:
|
|
right = 9
|
|
else:
|
|
right = 0
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[6], [0x00, down, left, right], 'RDRAM')])
|
|
|
|
async def update_giovanni_cursor(self, ctx, item_codes):
|
|
# Determine UP: All badges obtained?
|
|
up = 9 if set(gym_badge_codes).issubset(item_codes) else 0
|
|
|
|
# Determine DOWN: highest unlocked gym from 5 to 1
|
|
down = self.highest_unlocked_from(5)
|
|
|
|
# Determine LEFT: is Blaine or Sabrina unlocked
|
|
left = 0
|
|
blaine_unlocked = 7 if 7 in self.unlocked_gyms else 0
|
|
sabrina_unlocked = 6 if 6 in self.unlocked_gyms else 0
|
|
|
|
if blaine_unlocked:
|
|
left = 7
|
|
elif sabrina_unlocked:
|
|
left = 6
|
|
|
|
# Determine RIGHT: All badges obtained?
|
|
right = up
|
|
|
|
await bizhawk.write(ctx.bizhawk_ctx, [(self.GLC_CURSOR_TARGETS[7], [up, down, left, right], 'RDRAM')])
|