Files
dockipelago/worlds/kh2/ClientStuff/Client.py
JaredWeakStrike a6740e7be3 KH2: Deathlink and ingame item popups (#5206)
---------

Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: Delilah <lindsaydiane@gmail.com>
2026-01-28 07:10:29 +01:00

755 lines
36 KiB
Python

from __future__ import annotations
import ModuleUpdate
import Utils
ModuleUpdate.update()
import os
import asyncio
import json
import requests
from pymem import pymem
from worlds.kh2 import item_dictionary_table, exclusion_item_table, CheckDupingItems, all_locations, exclusion_table, \
SupportAbility_Table, ActionAbility_Table, all_weapon_slot
from worlds.kh2.Names import ItemName
from .WorldLocations import *
from NetUtils import ClientStatus, NetworkItem
from CommonClient import gui_enabled, logger, get_base_parser, CommonContext, server_loop
from .CMDProcessor import KH2CommandProcessor
from .SendChecks import finishedGame
class KH2Context(CommonContext):
command_processor = KH2CommandProcessor
game = "Kingdom Hearts 2"
items_handling = 0b111 # Indicates you get items sent from other worlds.
def __init__(self, server_address, password):
super(KH2Context, self).__init__(server_address, password)
self.goofy_ability_to_slot = dict()
self.donald_ability_to_slot = dict()
self.all_weapon_location_id = None
self.sora_ability_to_slot = dict()
self.kh2_seed_save = None
self.kh2_local_items = None
self.growthlevel = None
self.kh2connected = False
self.kh2_finished_game = False
self.serverconnected = False
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
self.kh2_data_package = {}
self.kh2_loc_name_to_id = None
self.kh2_item_name_to_id = None
self.lookup_id_to_item = None
self.lookup_id_to_location = None
self.sora_ability_dict = {k: v.quantity for dic in [SupportAbility_Table, ActionAbility_Table] for k, v in
dic.items()}
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
self.slot_name = None
self.disconnect_from_server = False
self.sending = []
# queue for the strings to display on the screen
self.queued_puzzle_popup = []
self.queued_info_popup = []
self.queued_chest_popup = []
# special characters for printing in game
# A dictionary of all the special characters, which
# are hard to convert through a mathematical formula.
self.special_dict = {
' ': 0x01, '\n': 0x02, '-': 0x54, '!': 0x48, '?': 0x49, '%': 0x4A, '/': 0x4B,
'.': 0x4F, ',': 0x50, ';': 0x51, ':': 0x52, '\'': 0x57, '(': 0x5A, ')': 0x5B,
'[': 0x62, ']': 0x63, 'à': 0xB7, 'á': 0xB8, 'â': 0xB9, 'ä': 0xBA, 'è': 0xBB,
'é': 0xBC, 'ê': 0xBD, 'ë': 0xBE, 'ì': 0xBF, 'í': 0xC0, 'î': 0xC1, 'ï': 0xC2,
'ñ': 0xC3, 'ò': 0xC4, 'ó': 0xC5, 'ô': 0xC6, 'ö': 0xC7, 'ù': 0xC8, 'ú': 0xC9,
'û': 0xCA, 'ü': 0xCB, 'ç': 0xE8, 'À': 0xD0, 'Á': 0xD1, 'Â': 0xD2, 'Ä': 0xD3,
'È': 0xD4, 'É': 0xD5, 'Ê': 0xD6, 'Ë': 0xD7, 'Ì': 0xD8, 'Í': 0xD9, 'Î': 0xDA,
'Ï': 0xDB, 'Ñ': 0xDC, 'Ò': 0xDD, 'Ó': 0xDE, 'Ô': 0xDF, 'Ö': 0xE0, 'Ù': 0xE1,
'Ú': 0xE2, 'Û': 0xE3, 'Ü': 0xE4, '¡': 0xE5, '¿': 0xE6, 'Ç': 0xE7
}
# list used to keep track of locations+items player has. Used for disoneccting
self.kh2_seed_save_cache = {
"itemIndex": -1,
# back of soras invo is 0x25E2. Growth should be moved there
# Character: [back of invo, front of invo]
"SoraInvo": [0x25D8, 0x2546],
"DonaldInvo": [0x26F4, 0x2658],
"GoofyInvo": [0x2808, 0x276C],
"AmountInvo": {
"Ability": {},
"Amount": {
"Bounty": 0,
},
"Growth": {
"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
"Aerial Dodge": 0, "Glide": 0
},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": {}, # ItemName: Amount
"Magic": {
"Fire Element": 0,
"Blizzard Element": 0,
"Thunder Element": 0,
"Cure Element": 0,
"Magnet Element": 0,
"Reflect Element": 0
},
"StatIncrease": {
ItemName.MaxHPUp: 0,
ItemName.MaxMPUp: 0,
ItemName.DriveGaugeUp: 0,
ItemName.ArmorSlotUp: 0,
ItemName.AccessorySlotUp: 0,
ItemName.ItemSlotUp: 0,
},
},
}
self.kh2seedname = None
self.kh2_seed_save_path_join = None
self.kh2slotdata = None
self.mem_json = None
self.itemamount = {}
self.client_settings = {
"send_truncate_first": "playername", # there is no need to truncate item names for info popup
"receive_truncate_first": "playername", # truncation order. Can be PlayerName or ItemName
"send_popup_type": "chest", # type of popup when you receive an item
"receive_popup_type": "chest", # can be Puzzle, Info, Chest or None
}
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
self.kh2_client_settings = f"kh2_client_settings.json"
self.kh2_client_settings_join = os.path.join(self.game_communication_path, self.kh2_client_settings)
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
if not os.path.exists(self.kh2_client_settings_join):
# make the json with the settings
with open(self.kh2_client_settings_join, "wt") as f:
f.close()
elif os.path.exists(self.kh2_client_settings_join):
with open(self.kh2_client_settings_join) as f:
# if the file isnt empty load it
# this is the best I could fine to valid json stuff https://stackoverflow.com/questions/23344948/validate-and-format-json-files
try:
self.kh2_seed_save = json.load(f)
except json.decoder.JSONDecodeError:
pass
# this is what is effectively doing on
# self.client_settings = default
f.close()
self.hitlist_bounties = 0
# hooked object
self.kh2 = None
self.final_xemnas = False
self.worldid_to_locations = {
# 1: {}, # world of darkness (story cutscenes)
2: TT_Checks,
# 3: {}, # destiny island doesn't have checks
4: HB_Checks,
5: BC_Checks,
6: Oc_Checks,
7: AG_Checks,
8: LoD_Checks,
9: HundredAcreChecks,
10: PL_Checks,
11: Atlantica_Checks,
12: DC_Checks,
13: TR_Checks,
14: HT_Checks,
15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb
16: PR_Checks,
17: SP_Checks,
18: TWTNW_Checks,
# 255: {}, # starting screen
}
#Sora,Donald and Goofy are always in your party
self.WorldIDtoParty = {
4: "Beast",
6: "Auron",
7: "Aladdin",
8: "Mulan",
10: "Simba",
14: "Jack Skellington",
16: "Jack Sparrow",
17: "Tron",
18: "Riku"
}
self.last_world_int = -1
# PC Address anchors
# epic .10 addresses
self.Now = 0x0716DF8
self.Save = 0x9A9330
self.Journal = 0x743260
self.Shop = 0x743350
self.Slot1 = 0x2A23018
self.InfoBarPointer = 0xABE2A8
self.isDead = 0x0BEEF28
self.FadeStatus = 0xABAF38
self.PlayerGaugePointer = 0x0ABCCC8
self.kh2_game_version = None # can be egs or steam
self.kh2_seed_save_path = None
self.chest_set = set(exclusion_table["Chests"])
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
self.shield_set = set(CheckDupingItems["Weapons"]["Shields"])
self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set)
self.equipment_categories = CheckDupingItems["Equipment"]
self.armor_set = set(self.equipment_categories["Armor"])
self.accessories_set = set(self.equipment_categories["Accessories"])
self.all_equipment = self.armor_set.union(self.accessories_set)
self.CharacterAnchors = {
"Sora": 0x24F0,
"Donald": 0x2604,
"Goofy": 0x2718,
"Auron": 0x2940,
"Mulan": 0x2A54,
"Aladdin": 0x2B68,
"Jack Sparrow": 0x2C7C,
"Beast": 0x2D90,
"Jack Skellington": 0x2EA4,
"Simba": 0x2FB8,
"Tron": 0x30CC,
"Riku": 0x31E0
}
self.Equipment_Anchor_Dict = {
#Sora, Donald, Goofy in that order
# each slot is a short, Sora Anchor:0x24F0, Donald Anchor: 0x2604, Goofy Anchor: 0x2718
# Each of these has 8 slots that could have them no matter how many slots are unlocked
# If Ability Ring on slot 5 of sora
# ReadShort(Save+CharacterAnchors["Sora"]+Equiptment_Anchor["Accessories][4 (index 5)]) == self.item_name_to_data[item_name].memaddr
"Armor": [0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22],
"Accessories": [0x24, 0x26, 0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32]
}
self.AbilityQuantityDict = {}
self.ability_categories = CheckDupingItems["Abilities"]
self.sora_ability_set = set(self.ability_categories["Sora"])
self.donald_ability_set = set(self.ability_categories["Donald"])
self.goofy_ability_set = set(self.ability_categories["Goofy"])
self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set)
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
# Growth:[level 1,level 4,slot]
self.growth_values_dict = {
"High Jump": [0x05E, 0x061, 0x25DA],
"Quick Run": [0x62, 0x65, 0x25DC],
"Dodge Roll": [0x234, 0x237, 0x25DE],
"Aerial Dodge": [0x66, 0x069, 0x25E0],
"Glide": [0x6A, 0x6D, 0x25E2]
}
self.ability_code_list = None
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
self.base_hp = 20
self.base_mp = 100
self.base_drive = 5
self.base_accessory_slots = 1
self.base_armor_slots = 1
self.base_item_slots = 3
self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E,
0x2770, 0x2772]
self.deathlink_toggle = False
self.deathlink_blacklist = []
from .ReadAndWrite import kh2_read_longlong, kh2_read_int, kh2_read_string, kh2_read_byte, kh2_write_bytes, kh2_write_int, kh2_write_short, kh2_write_byte, kh2_read_short, kh2_return_base_address
from .SendChecks import checkWorldLocations, checkSlots, checkLevels, verifyChests, verifyLevel
from .RecieveItems import displayPuzzlePieceTextinGame, displayInfoTextinGame, displayChestTextInGame, verifyItems, give_item, IsInShop, to_khscii
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested)
await self.get_username()
# if slot name != first time login or previous name
# and seed name is none or saved seed name
if not self.slot_name and not self.kh2seedname:
await self.send_connect()
elif self.slot_name == self.auth and self.kh2seedname:
await self.send_connect()
else:
logger.info(f"You are trying to connect with data still cached in the client. Close client or connect to the correct slot: {self.slot_name}")
self.serverconnected = False
self.disconnect_from_server = True
# to not softlock the client when you connect to the wrong slot/game
def event_invalid_slot(self):
self.kh2seedname = None
CommonContext.event_invalid_slot(self)
def event_invalid_game(self):
self.kh2seedname = None
CommonContext.event_invalid_slot(self)
async def connection_closed(self):
self.kh2connected = False
self.serverconnected = False
if self.kh2seedname is not None and self.auth is not None:
with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4))
f.close()
await super(KH2Context, self).connection_closed()
async def disconnect(self, allow_autoreconnect: bool = False):
self.kh2connected = False
self.serverconnected = False
self.locations_checked = []
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4))
f.close()
await super(KH2Context, self).disconnect()
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(self.kh2_seed_save_path_join, 'w') as f:
f.write(json.dumps(self.kh2_seed_save, indent=4))
f.close()
with open(self.kh2_client_settings_join, 'w') as f2:
f2.write(json.dumps(self.client_settings, indent=4))
f2.close()
await super(KH2Context, self).shutdown()
def on_package(self, cmd: str, args: dict):
if cmd == "RoomInfo":
if not self.kh2seedname:
self.kh2seedname = args['seed_name']
elif self.kh2seedname != args['seed_name']:
self.disconnect_from_server = True
self.serverconnected = False
self.kh2connected = False
logger.info("Connection to the wrong seed, connect to the correct seed or close the client.")
return
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, Utils.get_file_safe_name(self.kh2_seed_save_path))
if not os.path.exists(self.kh2_seed_save_path_join):
self.kh2_seed_save = {
"Levels": {
"SoraLevel": 0,
"ValorLevel": 0,
"WisdomLevel": 0,
"LimitLevel": 0,
"MasterLevel": 0,
"FinalLevel": 0,
"SummonLevel": 0,
},
# Item: Amount of them sold
"SoldEquipment": dict(),
}
with open(self.kh2_seed_save_path_join, 'wt') as f:
f.close()
elif os.path.exists(self.kh2_seed_save_path_join):
with open(self.kh2_seed_save_path_join) as f:
try:
self.kh2_seed_save = json.load(f)
except json.decoder.JSONDecodeError:
self.kh2_seed_save = None
if self.kh2_seed_save is None or self.kh2_seed_save == {}:
self.kh2_seed_save = {
"Levels": {
"SoraLevel": 0,
"ValorLevel": 0,
"WisdomLevel": 0,
"LimitLevel": 0,
"MasterLevel": 0,
"FinalLevel": 0,
"SummonLevel": 0,
},
# Item: Amount of them sold
"SoldEquipment": dict(),
}
f.close()
if cmd == "Connected":
self.kh2slotdata = args['slot_data']
self.kh2_data_package = Utils.load_data_package_for_checksum(
"Kingdom Hearts 2", self.checksums["Kingdom Hearts 2"])
if "location_name_to_id" in self.kh2_data_package:
self.data_package_kh2_cache(
self.kh2_data_package["location_name_to_id"], self.kh2_data_package["item_name_to_id"])
self.connect_to_game()
else:
asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}]))
self.locations_checked = set(args["checked_locations"])
if cmd == "ReceivedItems":
# Sora Front of Ability List:0x2546
# Donald Front of Ability List:0x2658
# Goofy Front of Ability List:0x276A
start_index = args["index"]
if start_index == 0:
self.kh2_seed_save_cache = {
"itemIndex": -1,
# back of soras invo is 0x25E2. Growth should be moved there
# Character: [back of invo, front of invo]
"SoraInvo": [0x25D8, 0x2546],
"DonaldInvo": [0x26F4, 0x2658],
"GoofyInvo": [0x2808, 0x276C],
"AmountInvo": {
"Ability": {},
"Amount": {
"Bounty": 0,
},
"Growth": {
"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
"Aerial Dodge": 0, "Glide": 0
},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": {}, # ItemName: Amount
"Magic": {
"Fire Element": 0,
"Blizzard Element": 0,
"Thunder Element": 0,
"Cure Element": 0,
"Magnet Element": 0,
"Reflect Element": 0
},
"StatIncrease": {
ItemName.MaxHPUp: 0,
ItemName.MaxMPUp: 0,
ItemName.DriveGaugeUp: 0,
ItemName.ArmorSlotUp: 0,
ItemName.AccessorySlotUp: 0,
ItemName.ItemSlotUp: 0,
},
},
}
if start_index > self.kh2_seed_save_cache["itemIndex"] and self.serverconnected:
self.kh2_seed_save_cache["itemIndex"] = start_index
for item in args['items']:
networkItem = NetworkItem(*item)
# actually give player the item
asyncio.create_task(self.give_item(networkItem.item, networkItem.location))
if cmd == "RoomUpdate":
if "checked_locations" in args:
new_locations = set(args["checked_locations"])
self.locations_checked |= new_locations
if cmd == "DataPackage":
if "Kingdom Hearts 2" in args["data"]["games"]:
self.data_package_kh2_cache(
args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"],
args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"])
self.connect_to_game()
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
if cmd == "PrintJSON":
# shamelessly stolen from kh1
if args.get("type") == "ItemSend":
item = args["item"]
networkItem = NetworkItem(*item)
itemId = networkItem.item
receiverID = args["receiving"]
senderID = networkItem.player
receive_popup_type = self.client_settings["receive_popup_type"].lower()
send_popup_type = self.client_settings["send_popup_type"].lower()
receive_truncate_first = self.client_settings["receive_truncate_first"].lower()
send_truncate_first = self.client_settings["send_truncate_first"].lower()
# checking if sender is the kh2 player, and you aren't sending yourself the item
if receiverID == self.slot and senderID != self.slot: # item is sent to you and is not from yourself
itemName = self.item_names.lookup_in_game(itemId)
playerName = self.player_names[networkItem.player] # player that sent you the item
totalLength = len(itemName) + len(playerName)
if receive_popup_type == "info": # no restrictions on size here
temp_length = f"Obtained {itemName} from {playerName}"
if totalLength > 90:
self.queued_info_popup += [temp_length[:90]] # slice it to be 90
else:
self.queued_info_popup += [temp_length]
else: # either chest or puzzle. they are handled the same length wise
totalLength = len(itemName) + len(playerName)
while totalLength > 25:
if receive_truncate_first == "playername":
if len(playerName) > 5:
playerName = playerName[:-1]
else:
itemName = itemName[:-1]
else:
if len(ItemName) > 15:
itemName = itemName[:-1]
else:
playerName = playerName[:-1]
totalLength = len(itemName) + len(playerName)
# from =6. totalLength of the string cant be over 31 or game crash
if receive_popup_type == "puzzle": # sanitize ItemName and receiver name
self.queued_puzzle_popup += [f"{itemName} from {playerName}"]
else:
self.queued_chest_popup += [f"{itemName} from {playerName}"]
if receiverID != self.slot and senderID == self.slot: #item is sent to other players
itemName = self.item_names.lookup_in_slot(itemId, receiverID)
playerName = self.player_names[receiverID]
totalLength = len(itemName) + len(playerName)
if send_popup_type == "info":
if totalLength > 90:
temp_length = f"Sent {itemName} to {playerName}"
self.queued_info_popup += [temp_length[:90]] #slice it to be 90
else:
self.queued_info_popup += [f"Sent {itemName} to {playerName}"]
else: # else chest or puzzle. they are handled the same length wise
while totalLength > 27:
if send_truncate_first == "playername":
if len(playerName) > 5: #limit player name to at least be 5 characters
playerName = playerName[:-1]
else:
itemName = itemName[:-1]
else:
if len(ItemName) > 15: # limit item name to at least be 15 characters
itemName = itemName[:-1]
else:
playerName = playerName[:-1]
totalLength = len(itemName) + len(playerName)
if send_popup_type == "puzzle":
# to = 4 totalLength of the string cant be over 31 or game crash
self.queued_puzzle_popup += [f"{itemName} to {playerName}"]
else:
self.queued_chest_popup += [f"{itemName} to {playerName}"]
def connect_to_game(self):
if "KeybladeAbilities" in self.kh2slotdata.keys():
# sora ability to slot
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
# itemid:[slots that are available for that item]
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"])
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"])
self.all_weapon_location_id = {self.kh2_loc_name_to_id[loc] for loc in all_weapon_slot}
try:
if not self.kh2:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
self.get_addresses()
except Exception as e:
if self.kh2connected:
self.kh2connected = False
logger.info("Game is not open. If it is open run the launcher/client as admin.")
self.serverconnected = True
self.slot_name = self.auth
def data_package_kh2_cache(self, loc_to_id, item_to_id):
self.kh2_loc_name_to_id = loc_to_id
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
self.kh2_item_name_to_id = item_to_id
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
"""Gets dispatched when a new DeathLink is triggered by another linked player."""
if data["source"] not in self.deathlink_blacklist:
self.last_death_link = max(data["time"], self.last_death_link)
text = data.get("cause", "")
if text:
logger.info(f"DeathLink: {text}")
else:
logger.info(f"DeathLink: Received from {data['source']}")
# kills sora by setting flag for the lua to read
self.kh2_write_byte(0x810000, 1)
async def is_dead(self):
# General Death link logic: if hp is 0 and sora has 5 drive gauge and deathlink flag isnt set
# if deathlink is on and script is hasnt killed sora and sora isnt dead
if self.deathlink_toggle and self.kh2_read_byte(0x810000) == 0 and self.kh2_read_byte(0x810001) != 0:
# set deathlink flag so it doesn't send out bunch
# basically making the game think it got its death from a deathlink instead of from the game
self.kh2_write_byte(0x810000, 0)
# 0x810001 is set to 1 when you die via the goa script. This is done because the polling rate for the client can miss a death
# but the lua script runs eveery frame so we cant miss them now
self.kh2_write_byte(0x810001, 0)
#todo: read these from the goa lua instead since the deathlink is after they contiune which means that its just before they would've gotten into the fight
Room = self.kh2_read_byte(0x810002)
Event = self.kh2_read_byte(0x810003)
World = self.kh2_read_byte(0x810004)
if (World, Room, Event) in DeathLinkPair.keys():
logger.info(f"Deathlink: {self.player_names[self.slot]} died to {DeathLinkPair[(World,Room, Event)]}.")
await self.send_death(death_text=f"{self.player_names[self.slot]} died to {DeathLinkPair[(World,Room, Event)]}.")
else:
logger.info(f"Deathlink: {self.player_names[self.slot]} lost their heart to darkness.")
await self.send_death(death_text=f"{self.player_names[self.slot]} lost their heart to darkness.")
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager
class KH2Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago KH2 Client"
self.ui = KH2Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_addresses(self):
if not self.kh2connected and self.kh2 is not None:
if self.kh2_game_version is None:
# current verions is .10 then runs the get from github stuff
if self.kh2_read_string(0x9A98B0, 4) == "KH2J":
self.kh2_game_version = "STEAM"
self.Now = 0x0717008
self.Save = 0x09A98B0
self.Slot1 = 0x2A23598
self.Journal = 0x7434E0
self.Shop = 0x7435D0
self.InfoBarPointer = 0xABE828
self.isDead = 0x0BEF4A8
self.FadeStatus = 0xABB4B8
self.PlayerGaugePointer = 0x0ABD248
elif self.kh2_read_string(0x9A9330, 4) == "KH2J":
self.kh2_game_version = "EGS"
else:
if self.game_communication_path:
logger.info("Checking with most up to date addresses from the addresses json.")
# if mem addresses file is found then check version and if old get new one
kh2memaddresses_path = os.path.join(self.game_communication_path, "kh2memaddresses.json")
if not os.path.exists(kh2memaddresses_path):
logger.info("File is not found. Downloading json with memory addresses. This might take a moment")
mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json")
if mem_resp.status_code == 200:
self.mem_json = json.loads(mem_resp.content)
with open(kh2memaddresses_path, 'w') as f:
f.write(json.dumps(self.mem_json, indent=4))
f.close()
else:
with open(kh2memaddresses_path) as f:
self.mem_json = json.load(f)
f.close()
if self.mem_json:
for key in self.mem_json.keys():
if self.kh2_read_string(int(self.mem_json[key]["GameVersionCheck"], 0), 4) == "KH2J":
self.Now = int(self.mem_json[key]["Now"], 0)
self.Save = int(self.mem_json[key]["Save"], 0)
self.Slot1 = int(self.mem_json[key]["Slot1"], 0)
self.Journal = int(self.mem_json[key]["Journal"], 0)
self.Shop = int(self.mem_json[key]["Shop"], 0)
self.InfoBarPointer = int(self.mem_json[key]["InfoBarPointer"], 0)
self.isDead = int(self.mem_json[key]["isDead"], 0)
self.FadeStatus = int(self.mem_json[key]["FadeStatus"], 0)
self.PlayerGaugePointer = int(self.mem_json[key]["PlayerGaugePointer"], 0)
self.kh2_game_version = key
if self.kh2_game_version is not None:
logger.info(f"You are now auto-tracking {self.kh2_game_version}")
self.kh2connected = True
else:
logger.info("Your game version does not match what the client requires. Check in the "
"kingdom-hearts-2-final-mix channel for more information on correcting the game "
"version.")
self.kh2connected = False
async def kh2_watcher(ctx: KH2Context):
while not ctx.exit_event.is_set():
try:
if ctx.kh2connected and ctx.serverconnected:
ctx.sending = []
await asyncio.create_task(ctx.checkWorldLocations())
await asyncio.create_task(ctx.checkLevels())
await asyncio.create_task(ctx.checkSlots())
await asyncio.create_task(ctx.verifyChests())
await asyncio.create_task(ctx.verifyItems())
await asyncio.create_task(ctx.verifyLevel())
await asyncio.create_task(ctx.is_dead())
if (ctx.deathlink_toggle and "DeathLink" not in ctx.tags) or (not ctx.deathlink_toggle and "DeathLink" in ctx.tags):
await ctx.update_death_link(ctx.deathlink_toggle)
if finishedGame(ctx) and not ctx.kh2_finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.kh2_finished_game = True
if ctx.sending:
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
await ctx.send_msgs(message)
if ctx.queued_puzzle_popup:
await asyncio.create_task(ctx.displayPuzzlePieceTextinGame(ctx.queued_puzzle_popup[0])) # send the num 1 index of whats in the queue
if ctx.queued_info_popup:
await asyncio.create_task(ctx.displayInfoTextinGame(ctx.queued_info_popup[0]))
if ctx.queued_chest_popup:
await asyncio.create_task(ctx.displayChestTextInGame(ctx.queued_chest_popup[0]))
elif not ctx.kh2connected and ctx.serverconnected:
logger.info("Game Connection lost. trying to reconnect.")
ctx.kh2 = None
#todo: change this to be an option for the client to auto reconnect with the default being yes
# reason is because the await sleep causes the client to hang if you close the game then the client without disconnecting.
while not ctx.kh2connected and ctx.serverconnected:
try:
ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
ctx.get_addresses()
logger.info("Game Connection Established.")
except Exception as e:
await asyncio.sleep(5)
if ctx.disconnect_from_server:
ctx.disconnect_from_server = False
await ctx.disconnect()
except Exception as e:
if ctx.kh2connected:
ctx.kh2connected = False
logger.info(e)
logger.info("line 940")
await asyncio.sleep(0.5)
def launch():
async def main(args):
ctx = KH2Context(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
kh2_watcher(ctx), name="KH2ProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="KH2 Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()