mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
Pokemon Emerald: v2 Update (#2918)
This commit is contained in:
@@ -1,19 +1,28 @@
|
||||
from typing import TYPE_CHECKING, Dict, Set
|
||||
import asyncio
|
||||
import copy
|
||||
import orjson
|
||||
import random
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Set, Tuple
|
||||
import uuid
|
||||
|
||||
from NetUtils import ClientStatus
|
||||
from Options import Toggle
|
||||
import Utils
|
||||
import worlds._bizhawk as bizhawk
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
|
||||
from .data import BASE_OFFSET, data
|
||||
from .options import Goal
|
||||
from .data import BASE_OFFSET, POKEDEX_OFFSET, data
|
||||
from .options import Goal, RemoteItems
|
||||
from .util import pokemon_data_to_json, json_to_pokemon_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
|
||||
|
||||
EXPECTED_ROM_NAME = "pokemon emerald version / AP 2"
|
||||
EXPECTED_ROM_NAME = "pokemon emerald version / AP 5"
|
||||
|
||||
IS_CHAMPION_FLAG = data.constants["FLAG_IS_CHAMPION"]
|
||||
DEFEATED_WALLACE_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_WALLACE"]
|
||||
DEFEATED_STEVEN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_STEVEN"]
|
||||
DEFEATED_NORMAN_FLAG = data.constants["TRAINER_FLAGS_START"] + data.constants["TRAINER_NORMAN_1"]
|
||||
|
||||
@@ -31,7 +40,7 @@ TRACKER_EVENT_FLAGS = [
|
||||
"FLAG_RECEIVED_POKENAV", # Talk to Mr. Stone
|
||||
"FLAG_DELIVERED_STEVEN_LETTER",
|
||||
"FLAG_DELIVERED_DEVON_GOODS",
|
||||
"FLAG_HIDE_ROUTE_119_TEAM_AQUA", # Clear Weather Institute
|
||||
"FLAG_HIDE_ROUTE_119_TEAM_AQUA_SHELLY", # Clear Weather Institute
|
||||
"FLAG_MET_ARCHIE_METEOR_FALLS", # Magma steals meteorite
|
||||
"FLAG_GROUDON_AWAKENED_MAGMA_HIDEOUT", # Clear Magma Hideout
|
||||
"FLAG_MET_TEAM_AQUA_HARBOR", # Aqua steals submarine
|
||||
@@ -41,19 +50,19 @@ TRACKER_EVENT_FLAGS = [
|
||||
"FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA", # Rayquaza departs for Sootopolis
|
||||
"FLAG_OMIT_DIVE_FROM_STEVEN_LETTER", # Steven gives Dive HM (clears seafloor cavern grunt)
|
||||
"FLAG_IS_CHAMPION",
|
||||
"FLAG_PURCHASED_HARBOR_MAIL"
|
||||
"FLAG_PURCHASED_HARBOR_MAIL",
|
||||
]
|
||||
EVENT_FLAG_MAP = {data.constants[flag_name]: flag_name for flag_name in TRACKER_EVENT_FLAGS}
|
||||
|
||||
KEY_LOCATION_FLAGS = [
|
||||
"NPC_GIFT_RECEIVED_HM01",
|
||||
"NPC_GIFT_RECEIVED_HM02",
|
||||
"NPC_GIFT_RECEIVED_HM03",
|
||||
"NPC_GIFT_RECEIVED_HM04",
|
||||
"NPC_GIFT_RECEIVED_HM05",
|
||||
"NPC_GIFT_RECEIVED_HM06",
|
||||
"NPC_GIFT_RECEIVED_HM07",
|
||||
"NPC_GIFT_RECEIVED_HM08",
|
||||
"NPC_GIFT_RECEIVED_HM_CUT",
|
||||
"NPC_GIFT_RECEIVED_HM_FLY",
|
||||
"NPC_GIFT_RECEIVED_HM_SURF",
|
||||
"NPC_GIFT_RECEIVED_HM_STRENGTH",
|
||||
"NPC_GIFT_RECEIVED_HM_FLASH",
|
||||
"NPC_GIFT_RECEIVED_HM_ROCK_SMASH",
|
||||
"NPC_GIFT_RECEIVED_HM_WATERFALL",
|
||||
"NPC_GIFT_RECEIVED_HM_DIVE",
|
||||
"NPC_GIFT_RECEIVED_ACRO_BIKE",
|
||||
"NPC_GIFT_RECEIVED_WAILMER_PAIL",
|
||||
"NPC_GIFT_RECEIVED_DEVON_GOODS_RUSTURF_TUNNEL",
|
||||
@@ -70,7 +79,7 @@ KEY_LOCATION_FLAGS = [
|
||||
"HIDDEN_ITEM_ABANDONED_SHIP_RM_1_KEY",
|
||||
"HIDDEN_ITEM_ABANDONED_SHIP_RM_4_KEY",
|
||||
"HIDDEN_ITEM_ABANDONED_SHIP_RM_6_KEY",
|
||||
"ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_4_SCANNER",
|
||||
"ITEM_ABANDONED_SHIP_HIDDEN_FLOOR_ROOM_2_SCANNER",
|
||||
"ITEM_ABANDONED_SHIP_CAPTAINS_OFFICE_STORAGE_KEY",
|
||||
"NPC_GIFT_RECEIVED_OLD_ROD",
|
||||
"NPC_GIFT_RECEIVED_GOOD_ROD",
|
||||
@@ -78,6 +87,24 @@ KEY_LOCATION_FLAGS = [
|
||||
]
|
||||
KEY_LOCATION_FLAG_MAP = {data.locations[location_name].flag: location_name for location_name in KEY_LOCATION_FLAGS}
|
||||
|
||||
LEGENDARY_NAMES = {
|
||||
"Groudon": "GROUDON",
|
||||
"Kyogre": "KYOGRE",
|
||||
"Rayquaza": "RAYQUAZA",
|
||||
"Latias": "LATIAS",
|
||||
"Latios": "LATIOS",
|
||||
"Regirock": "REGIROCK",
|
||||
"Regice": "REGICE",
|
||||
"Registeel": "REGISTEEL",
|
||||
"Mew": "MEW",
|
||||
"Deoxys": "DEOXYS",
|
||||
"Ho-oh": "HO_OH",
|
||||
"Lugia": "LUGIA",
|
||||
}
|
||||
|
||||
DEFEATED_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_DEFEATED_{name}"]: name for name in LEGENDARY_NAMES.values()}
|
||||
CAUGHT_LEGENDARY_FLAG_MAP = {data.constants[f"FLAG_CAUGHT_{name}"]: name for name in LEGENDARY_NAMES.values()}
|
||||
|
||||
|
||||
class PokemonEmeraldClient(BizHawkClient):
|
||||
game = "Pokemon Emerald"
|
||||
@@ -86,14 +113,31 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
local_checked_locations: Set[int]
|
||||
local_set_events: Dict[str, bool]
|
||||
local_found_key_items: Dict[str, bool]
|
||||
goal_flag: int
|
||||
local_defeated_legendaries: Dict[str, bool]
|
||||
goal_flag: Optional[int]
|
||||
|
||||
wonder_trade_update_event: asyncio.Event
|
||||
latest_wonder_trade_reply: dict
|
||||
wonder_trade_cooldown: int
|
||||
wonder_trade_cooldown_timer: int
|
||||
|
||||
death_counter: Optional[int]
|
||||
previous_death_link: float
|
||||
ignore_next_death_link: bool
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.local_checked_locations = set()
|
||||
self.local_set_events = {}
|
||||
self.local_found_key_items = {}
|
||||
self.goal_flag = IS_CHAMPION_FLAG
|
||||
self.local_defeated_legendaries = {}
|
||||
self.goal_flag = None
|
||||
self.wonder_trade_update_event = asyncio.Event()
|
||||
self.wonder_trade_cooldown = 5000
|
||||
self.wonder_trade_cooldown_timer = 0
|
||||
self.death_counter = None
|
||||
self.previous_death_link = 0
|
||||
self.ignore_next_death_link = False
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from CommonClient import logger
|
||||
@@ -123,88 +167,103 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
ctx.want_slot_data = True
|
||||
ctx.watcher_timeout = 0.125
|
||||
|
||||
self.death_counter = None
|
||||
self.previous_death_link = 0
|
||||
self.ignore_next_death_link = False
|
||||
|
||||
return True
|
||||
|
||||
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||
slot_name_bytes = (await bizhawk.read(ctx.bizhawk_ctx, [(data.rom_addresses["gArchipelagoInfo"], 64, "ROM")]))[0]
|
||||
ctx.auth = bytes([byte for byte in slot_name_bytes if byte != 0]).decode("utf-8")
|
||||
import base64
|
||||
auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(data.rom_addresses["gArchipelagoInfo"], 16, "ROM")]))[0]
|
||||
ctx.auth = base64.b64encode(auth_raw).decode("utf-8")
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
if ctx.slot_data is not None:
|
||||
if ctx.slot_data["goal"] == Goal.option_champion:
|
||||
self.goal_flag = IS_CHAMPION_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_steven:
|
||||
self.goal_flag = DEFEATED_STEVEN_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_norman:
|
||||
self.goal_flag = DEFEATED_NORMAN_FLAG
|
||||
if ctx.server is None or ctx.server.socket.closed or ctx.slot_data is None:
|
||||
return
|
||||
|
||||
if ctx.slot_data["goal"] == Goal.option_champion:
|
||||
self.goal_flag = DEFEATED_WALLACE_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_steven:
|
||||
self.goal_flag = DEFEATED_STEVEN_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_norman:
|
||||
self.goal_flag = DEFEATED_NORMAN_FLAG
|
||||
elif ctx.slot_data["goal"] == Goal.option_legendary_hunt:
|
||||
self.goal_flag = None
|
||||
|
||||
if ctx.slot_data["remote_items"] == RemoteItems.option_true and not ctx.items_handling & 0b010:
|
||||
ctx.items_handling = 0b011
|
||||
Utils.async_start(ctx.send_msgs([{
|
||||
"cmd": "ConnectUpdate",
|
||||
"items_handling": ctx.items_handling
|
||||
}]))
|
||||
|
||||
try:
|
||||
guards: Dict[str, Tuple[int, bytes, str]] = {}
|
||||
|
||||
# Checks that the player is in the overworld
|
||||
overworld_guard = (data.ram_addresses["gMain"] + 4, (data.ram_addresses["CB2_Overworld"] + 1).to_bytes(4, "little"), "System Bus")
|
||||
|
||||
# Read save block address
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(data.ram_addresses["gSaveBlock1Ptr"], 4, "System Bus")],
|
||||
[overworld_guard]
|
||||
guards["IN OVERWORLD"] = (
|
||||
data.ram_addresses["gMain"] + 4,
|
||||
(data.ram_addresses["CB2_Overworld"] + 1).to_bytes(4, "little"),
|
||||
"System Bus"
|
||||
)
|
||||
if read_result is None: # Not in overworld
|
||||
return
|
||||
|
||||
# Checks that the save block hasn't moved
|
||||
save_block_address_guard = (data.ram_addresses["gSaveBlock1Ptr"], read_result[0], "System Bus")
|
||||
|
||||
save_block_address = int.from_bytes(read_result[0], "little")
|
||||
|
||||
# Handle giving the player items
|
||||
read_result = await bizhawk.guarded_read(
|
||||
# Read save block addresses
|
||||
read_result = await bizhawk.read(
|
||||
ctx.bizhawk_ctx,
|
||||
[
|
||||
(save_block_address + 0x3778, 2, "System Bus"), # Number of received items
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 4, 1, "System Bus") # Received item struct full?
|
||||
],
|
||||
[overworld_guard, save_block_address_guard]
|
||||
(data.ram_addresses["gSaveBlock1Ptr"], 4, "System Bus"),
|
||||
(data.ram_addresses["gSaveBlock2Ptr"], 4, "System Bus"),
|
||||
]
|
||||
)
|
||||
if read_result is None: # Not in overworld, or save block moved
|
||||
return
|
||||
|
||||
num_received_items = int.from_bytes(read_result[0], "little")
|
||||
received_item_is_empty = read_result[1][0] == 0
|
||||
# Checks that the save data hasn't moved
|
||||
guards["SAVE BLOCK 1"] = (data.ram_addresses["gSaveBlock1Ptr"], read_result[0], "System Bus")
|
||||
guards["SAVE BLOCK 2"] = (data.ram_addresses["gSaveBlock2Ptr"], read_result[1], "System Bus")
|
||||
|
||||
# If the game hasn't received all items yet and the received item struct doesn't contain an item, then
|
||||
# fill it with the next item
|
||||
if num_received_items < len(ctx.items_received) and received_item_is_empty:
|
||||
next_item = ctx.items_received[num_received_items]
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 0, (next_item.item - BASE_OFFSET).to_bytes(2, "little"), "System Bus"),
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 2, (num_received_items + 1).to_bytes(2, "little"), "System Bus"),
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 4, [1], "System Bus"), # Mark struct full
|
||||
(data.ram_addresses["gArchipelagoReceivedItem"] + 5, [next_item.flags & 1], "System Bus"),
|
||||
])
|
||||
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
|
||||
sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
|
||||
|
||||
await self.handle_death_link(ctx, guards)
|
||||
await self.handle_received_items(ctx, guards)
|
||||
await self.handle_wonder_trade(ctx, guards)
|
||||
|
||||
# Read flags in 2 chunks
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(save_block_address + 0x1450, 0x96, "System Bus")], # Flags
|
||||
[overworld_guard, save_block_address_guard]
|
||||
[(sb1_address + 0x1450, 0x96, "System Bus")], # Flags
|
||||
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
|
||||
)
|
||||
if read_result is None: # Not in overworld, or save block moved
|
||||
return
|
||||
|
||||
flag_bytes = read_result[0]
|
||||
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(save_block_address + 0x14E6, 0x96, "System Bus")], # Flags
|
||||
[overworld_guard, save_block_address_guard]
|
||||
[(sb1_address + 0x14E6, 0x96, "System Bus")], # Flags continued
|
||||
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
|
||||
)
|
||||
if read_result is not None:
|
||||
flag_bytes += read_result[0]
|
||||
|
||||
# Read pokedex flags
|
||||
pokedex_caught_bytes = bytes(0)
|
||||
if ctx.slot_data["dexsanity"] == Toggle.option_true:
|
||||
# Read pokedex flags
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(sb2_address + 0x28, 0x34, "System Bus")],
|
||||
[guards["IN OVERWORLD"], guards["SAVE BLOCK 2"]]
|
||||
)
|
||||
if read_result is not None:
|
||||
pokedex_caught_bytes = read_result[0]
|
||||
|
||||
game_clear = False
|
||||
local_checked_locations = set()
|
||||
local_set_events = {flag_name: False for flag_name in TRACKER_EVENT_FLAGS}
|
||||
local_found_key_items = {location_name: False for location_name in KEY_LOCATION_FLAGS}
|
||||
defeated_legendaries = {legendary_name: False for legendary_name in LEGENDARY_NAMES.values()}
|
||||
caught_legendaries = {legendary_name: False for legendary_name in LEGENDARY_NAMES.values()}
|
||||
|
||||
# Check set flags
|
||||
for byte_i, byte in enumerate(flag_bytes):
|
||||
@@ -219,12 +278,45 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
if flag_id == self.goal_flag:
|
||||
game_clear = True
|
||||
|
||||
if flag_id in DEFEATED_LEGENDARY_FLAG_MAP:
|
||||
defeated_legendaries[DEFEATED_LEGENDARY_FLAG_MAP[flag_id]] = True
|
||||
|
||||
if flag_id in CAUGHT_LEGENDARY_FLAG_MAP:
|
||||
caught_legendaries[CAUGHT_LEGENDARY_FLAG_MAP[flag_id]] = True
|
||||
|
||||
if flag_id in EVENT_FLAG_MAP:
|
||||
local_set_events[EVENT_FLAG_MAP[flag_id]] = True
|
||||
|
||||
if flag_id in KEY_LOCATION_FLAG_MAP:
|
||||
local_found_key_items[KEY_LOCATION_FLAG_MAP[flag_id]] = True
|
||||
|
||||
# Check pokedex
|
||||
if ctx.slot_data["dexsanity"] == Toggle.option_true:
|
||||
for byte_i, byte in enumerate(pokedex_caught_bytes):
|
||||
for i in range(8):
|
||||
if byte & (1 << i) != 0:
|
||||
dex_number = (byte_i * 8 + i) + 1
|
||||
|
||||
location_id = dex_number + BASE_OFFSET + POKEDEX_OFFSET
|
||||
if location_id in ctx.server_locations:
|
||||
local_checked_locations.add(location_id)
|
||||
|
||||
# Count legendary hunt flags
|
||||
if ctx.slot_data["goal"] == Goal.option_legendary_hunt:
|
||||
# If legendary hunt doesn't require catching, add defeated legendaries to caught_legendaries
|
||||
if ctx.slot_data["legendary_hunt_catch"] == Toggle.option_false:
|
||||
for legendary, is_defeated in defeated_legendaries.items():
|
||||
if is_defeated:
|
||||
caught_legendaries[legendary] = True
|
||||
|
||||
num_caught = 0
|
||||
for legendary, is_caught in caught_legendaries.items():
|
||||
if is_caught and legendary in [LEGENDARY_NAMES[name] for name in ctx.slot_data["allowed_legendary_hunt_encounters"]]:
|
||||
num_caught += 1
|
||||
|
||||
if num_caught >= ctx.slot_data["legendary_hunt_count"]:
|
||||
game_clear = True
|
||||
|
||||
# Send locations
|
||||
if local_checked_locations != self.local_checked_locations:
|
||||
self.local_checked_locations = local_checked_locations
|
||||
@@ -232,14 +324,14 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
if local_checked_locations is not None:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationChecks",
|
||||
"locations": list(local_checked_locations)
|
||||
"locations": list(local_checked_locations),
|
||||
}])
|
||||
|
||||
# Send game clear
|
||||
if not ctx.finished_game and game_clear:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL
|
||||
"status": ClientStatus.CLIENT_GOAL,
|
||||
}])
|
||||
|
||||
# Send tracker event flags
|
||||
@@ -254,7 +346,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
"key": f"pokemon_emerald_events_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "or", "value": event_bitfield}]
|
||||
"operations": [{"operation": "or", "value": event_bitfield}],
|
||||
}])
|
||||
self.local_set_events = local_set_events
|
||||
|
||||
@@ -269,9 +361,313 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
"key": f"pokemon_emerald_keys_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "or", "value": key_bitfield}]
|
||||
"operations": [{"operation": "or", "value": key_bitfield}],
|
||||
}])
|
||||
self.local_found_key_items = local_found_key_items
|
||||
|
||||
if ctx.slot_data["goal"] == Goal.option_legendary_hunt:
|
||||
if caught_legendaries != self.local_defeated_legendaries and ctx.slot is not None:
|
||||
legendary_bitfield = 0
|
||||
for i, legendary_name in enumerate(LEGENDARY_NAMES.values()):
|
||||
if caught_legendaries[legendary_name]:
|
||||
legendary_bitfield |= 1 << i
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"pokemon_emerald_legendaries_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "or", "value": legendary_bitfield}],
|
||||
}])
|
||||
self.local_defeated_legendaries = caught_legendaries
|
||||
except bizhawk.RequestFailedError:
|
||||
# Exit handler and return to main loop to reconnect
|
||||
pass
|
||||
|
||||
async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
|
||||
"""
|
||||
Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game
|
||||
if a new one has been received.
|
||||
"""
|
||||
if ctx.slot_data.get("death_link", Toggle.option_false) == Toggle.option_true:
|
||||
if "DeathLink" not in ctx.tags:
|
||||
await ctx.update_death_link(True)
|
||||
self.previous_death_link = ctx.last_death_link
|
||||
|
||||
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
|
||||
sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
|
||||
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx, [
|
||||
(sb1_address + 0x177C + (52 * 4), 4, "System Bus"), # White out stat
|
||||
(sb1_address + 0x177C + (22 * 4), 4, "System Bus"), # Canary stat
|
||||
(sb2_address + 0xAC, 4, "System Bus"), # Encryption key
|
||||
],
|
||||
[guards["SAVE BLOCK 1"], guards["SAVE BLOCK 2"]]
|
||||
)
|
||||
if read_result is None: # Save block moved
|
||||
return
|
||||
|
||||
encryption_key = int.from_bytes(read_result[2], "little")
|
||||
times_whited_out = int.from_bytes(read_result[0], "little") ^ encryption_key
|
||||
|
||||
# Canary is an unused stat that will always be 0. There is a low chance that we've done this read on
|
||||
# a frame where the user has just entered a battle and the encryption key has been changed, but the data
|
||||
# has not yet been encrypted with the new key. If `canary` is 0, `times_whited_out` is correct.
|
||||
canary = int.from_bytes(read_result[1], "little") ^ encryption_key
|
||||
|
||||
# Skip all deathlink code if save is not yet loaded (encryption key is zero) or white out stat not yet
|
||||
# initialized (starts at 100 as a safety for subtracting values from an unsigned int).
|
||||
if canary == 0 and encryption_key != 0 and times_whited_out >= 100:
|
||||
if self.previous_death_link != ctx.last_death_link:
|
||||
self.previous_death_link = ctx.last_death_link
|
||||
if self.ignore_next_death_link:
|
||||
self.ignore_next_death_link = False
|
||||
else:
|
||||
await bizhawk.write(
|
||||
ctx.bizhawk_ctx,
|
||||
[(data.ram_addresses["gArchipelagoDeathLinkQueued"], [1], "System Bus")]
|
||||
)
|
||||
|
||||
if self.death_counter is None:
|
||||
self.death_counter = times_whited_out
|
||||
elif times_whited_out > self.death_counter:
|
||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} is out of usable POKéMON! "
|
||||
f"{ctx.player_names[ctx.slot]} whited out!")
|
||||
self.ignore_next_death_link = True
|
||||
self.death_counter = times_whited_out
|
||||
|
||||
async def handle_received_items(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
|
||||
"""
|
||||
Checks the index of the most recently received item and whether the item queue is full. Writes the next item
|
||||
into the game if necessary.
|
||||
"""
|
||||
received_item_address = data.ram_addresses["gArchipelagoReceivedItem"]
|
||||
|
||||
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
|
||||
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[
|
||||
(sb1_address + 0x3778, 2, "System Bus"), # Number of received items
|
||||
(received_item_address + 4, 1, "System Bus") # Received item struct full?
|
||||
],
|
||||
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
|
||||
)
|
||||
if read_result is None: # Not in overworld, or save block moved
|
||||
return
|
||||
|
||||
num_received_items = int.from_bytes(read_result[0], "little")
|
||||
received_item_is_empty = read_result[1][0] == 0
|
||||
|
||||
# If the game hasn't received all items yet and the received item struct doesn't contain an item, then
|
||||
# fill it with the next item
|
||||
if num_received_items < len(ctx.items_received) and received_item_is_empty:
|
||||
next_item = ctx.items_received[num_received_items]
|
||||
should_display = 1 if next_item.flags & 1 or next_item.player == ctx.slot else 0
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [
|
||||
(received_item_address + 0, (next_item.item - BASE_OFFSET).to_bytes(2, "little"), "System Bus"),
|
||||
(received_item_address + 2, (num_received_items + 1).to_bytes(2, "little"), "System Bus"),
|
||||
(received_item_address + 4, [1], "System Bus"),
|
||||
(received_item_address + 5, [should_display], "System Bus"),
|
||||
])
|
||||
|
||||
async def handle_wonder_trade(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
|
||||
"""
|
||||
Read wonder trade status from save data and either send a queued pokemon to data storage or attempt to retrieve
|
||||
one from data storage and write it into the save.
|
||||
"""
|
||||
from CommonClient import logger
|
||||
|
||||
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
|
||||
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[
|
||||
(sb1_address + 0x377C, 0x50, "System Bus"), # Wonder trade data
|
||||
(sb1_address + 0x37CC, 1, "System Bus"), # Is wonder trade sent
|
||||
],
|
||||
[guards["IN OVERWORLD"], guards["SAVE BLOCK 1"]]
|
||||
)
|
||||
|
||||
if read_result is not None:
|
||||
wonder_trade_pokemon_data = read_result[0]
|
||||
trade_is_sent = read_result[1][0]
|
||||
|
||||
if trade_is_sent == 0 and wonder_trade_pokemon_data[19] == 2:
|
||||
# Game has wonder trade data to send. Send it to data storage, remove it from the game's memory,
|
||||
# and mark that the game is waiting on receiving a trade
|
||||
Utils.async_start(self.wonder_trade_send(ctx, pokemon_data_to_json(wonder_trade_pokemon_data)))
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [
|
||||
(sb1_address + 0x377C, bytes(0x50), "System Bus"),
|
||||
(sb1_address + 0x37CC, [1], "System Bus"),
|
||||
])
|
||||
elif trade_is_sent != 0 and wonder_trade_pokemon_data[19] != 2:
|
||||
# Game is waiting on receiving a trade. See if there are any available trades that were not
|
||||
# sent by this player, and if so, try to receive one.
|
||||
if self.wonder_trade_cooldown_timer <= 0 and f"pokemon_wonder_trades_{ctx.team}" in ctx.stored_data:
|
||||
if any(item[0] != ctx.slot
|
||||
for key, item in ctx.stored_data.get(f"pokemon_wonder_trades_{ctx.team}", {}).items()
|
||||
if key != "_lock" and orjson.loads(item[1])["species"] <= 386):
|
||||
received_trade = await self.wonder_trade_receive(ctx)
|
||||
if received_trade is None:
|
||||
self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown
|
||||
self.wonder_trade_cooldown *= 2
|
||||
self.wonder_trade_cooldown += random.randrange(0, 500)
|
||||
else:
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [
|
||||
(sb1_address + 0x377C, json_to_pokemon_data(received_trade), "System Bus"),
|
||||
])
|
||||
logger.info("Wonder trade received!")
|
||||
self.wonder_trade_cooldown = 5000
|
||||
|
||||
else:
|
||||
# Very approximate "time since last loop", but extra delay is fine for this
|
||||
self.wonder_trade_cooldown_timer -= int(ctx.watcher_timeout * 1000)
|
||||
|
||||
async def wonder_trade_acquire(self, ctx: "BizHawkClientContext", keep_trying: bool = False) -> Optional[dict]:
|
||||
"""
|
||||
Acquires a lock on the `pokemon_wonder_trades_{ctx.team}` key in
|
||||
datastorage. Locking the key means you have exclusive access
|
||||
to modifying the value until you unlock it or the key expires (5
|
||||
seconds).
|
||||
|
||||
If `keep_trying` is `True`, it will keep trying to acquire the lock
|
||||
until successful. Otherwise it will return `None` if it fails to
|
||||
acquire the lock.
|
||||
"""
|
||||
while not ctx.exit_event.is_set():
|
||||
lock = int(time.time_ns() / 1000000)
|
||||
message_uuid = str(uuid.uuid4())
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"pokemon_wonder_trades_{ctx.team}",
|
||||
"default": {"_lock": 0},
|
||||
"want_reply": True,
|
||||
"operations": [{"operation": "update", "value": {"_lock": lock}}],
|
||||
"uuid": message_uuid,
|
||||
}])
|
||||
|
||||
self.wonder_trade_update_event.clear()
|
||||
try:
|
||||
await asyncio.wait_for(self.wonder_trade_update_event.wait(), 5)
|
||||
except asyncio.TimeoutError:
|
||||
if not keep_trying:
|
||||
return None
|
||||
continue
|
||||
|
||||
reply = copy.deepcopy(self.latest_wonder_trade_reply)
|
||||
|
||||
# Make sure the most recently received update was triggered by our lock attempt
|
||||
if reply.get("uuid", None) != message_uuid:
|
||||
if not keep_trying:
|
||||
return None
|
||||
await asyncio.sleep(self.wonder_trade_cooldown)
|
||||
continue
|
||||
|
||||
# Make sure the current value of the lock is what we set it to
|
||||
# (I think this should theoretically never run)
|
||||
if reply["value"]["_lock"] != lock:
|
||||
if not keep_trying:
|
||||
return None
|
||||
await asyncio.sleep(self.wonder_trade_cooldown)
|
||||
continue
|
||||
|
||||
# Make sure that the lock value we replaced is at least 5 seconds old
|
||||
# If it was unlocked before our change, its value was 0 and it will look decades old
|
||||
if lock - reply["original_value"]["_lock"] < 5000:
|
||||
# Multiple clients trying to lock the key may get stuck in a loop of checking the lock
|
||||
# by trying to set it, which will extend its expiration. So if we see that the lock was
|
||||
# too new when we replaced it, we should wait for increasingly longer periods so that
|
||||
# eventually the lock will expire and a client will acquire it.
|
||||
self.wonder_trade_cooldown *= 2
|
||||
self.wonder_trade_cooldown += random.randrange(0, 500)
|
||||
|
||||
if not keep_trying:
|
||||
self.wonder_trade_cooldown_timer = self.wonder_trade_cooldown
|
||||
return None
|
||||
await asyncio.sleep(self.wonder_trade_cooldown)
|
||||
continue
|
||||
|
||||
# We have the lock, reset the cooldown and return
|
||||
self.wonder_trade_cooldown = 5000
|
||||
return reply
|
||||
|
||||
async def wonder_trade_send(self, ctx: "BizHawkClientContext", data: str) -> None:
|
||||
"""
|
||||
Sends a wonder trade pokemon to data storage
|
||||
"""
|
||||
from CommonClient import logger
|
||||
|
||||
reply = await self.wonder_trade_acquire(ctx, True)
|
||||
|
||||
wonder_trade_slot = 0
|
||||
while str(wonder_trade_slot) in reply["value"]:
|
||||
wonder_trade_slot += 1
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"pokemon_wonder_trades_{ctx.team}",
|
||||
"default": {"_lock": 0},
|
||||
"operations": [{"operation": "update", "value": {
|
||||
"_lock": 0,
|
||||
str(wonder_trade_slot): (ctx.slot, data),
|
||||
}}],
|
||||
}])
|
||||
|
||||
logger.info("Wonder trade sent! We'll notify you here when a trade has been found.")
|
||||
|
||||
async def wonder_trade_receive(self, ctx: "BizHawkClientContext") -> Optional[str]:
|
||||
"""
|
||||
Tries to pop a pokemon out of the wonder trades. Returns `None` if
|
||||
for some reason it can't immediately remove a compatible pokemon.
|
||||
"""
|
||||
reply = await self.wonder_trade_acquire(ctx)
|
||||
|
||||
if reply is None:
|
||||
return None
|
||||
|
||||
candidate_slots = [
|
||||
int(slot)
|
||||
for slot in reply["value"]
|
||||
if slot != "_lock" \
|
||||
and reply["value"][slot][0] != ctx.slot \
|
||||
and orjson.loads(reply["value"][slot][1])["species"] <= 386
|
||||
]
|
||||
|
||||
if len(candidate_slots) == 0:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"pokemon_wonder_trades_{ctx.team}",
|
||||
"default": {"_lock": 0},
|
||||
"operations": [{"operation": "update", "value": {"_lock": 0}}],
|
||||
}])
|
||||
return None
|
||||
|
||||
wonder_trade_slot = max(candidate_slots)
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"pokemon_wonder_trades_{ctx.team}",
|
||||
"default": {"_lock": 0},
|
||||
"operations": [
|
||||
{"operation": "update", "value": {"_lock": 0}},
|
||||
{"operation": "pop", "value": str(wonder_trade_slot)},
|
||||
]
|
||||
}])
|
||||
|
||||
return reply["value"][str(wonder_trade_slot)][1]
|
||||
|
||||
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
|
||||
if cmd == "Connected":
|
||||
Utils.async_start(ctx.send_msgs([{
|
||||
"cmd": "SetNotify",
|
||||
"keys": [f"pokemon_wonder_trades_{ctx.team}"],
|
||||
}, {
|
||||
"cmd": "Get",
|
||||
"keys": [f"pokemon_wonder_trades_{ctx.team}"],
|
||||
}]))
|
||||
elif cmd == "SetReply":
|
||||
if args.get("key", "") == f"pokemon_wonder_trades_{ctx.team}":
|
||||
self.latest_wonder_trade_reply = args
|
||||
self.wonder_trade_update_event.set()
|
||||
|
Reference in New Issue
Block a user