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
249 lines
8.2 KiB
Python
249 lines
8.2 KiB
Python
from enum import IntEnum
|
|
import asyncio
|
|
from re import S
|
|
import socket
|
|
from CommonClient import logger
|
|
|
|
class MessageType(IntEnum):
|
|
Invalid = -1
|
|
Test = 0
|
|
ChestChecked = 1
|
|
LevelChecked = 2
|
|
ReceiveAllItems = 3
|
|
RequestAllItems = 4
|
|
ReceiveSingleItem = 5
|
|
StoryChecked = 6
|
|
ClientCommand = 7
|
|
Deathlink = 8
|
|
PortalChecked = 9
|
|
SendSlotData = 10
|
|
Victory = 11
|
|
Handshake = 12
|
|
GetCurrentIndex = 13
|
|
ItemPrompt = 14
|
|
Closed = 20
|
|
|
|
class DDDCommand(IntEnum):
|
|
DROP = 0
|
|
UNSTUCK = 1
|
|
DEATH_LINK = 3
|
|
|
|
# using slot data keys for the enum values
|
|
class SlotDataType(IntEnum):
|
|
keyblade_stats = 0
|
|
character = 1
|
|
play_destiny_islands = 2
|
|
exp_multiplier = 3
|
|
skip_light_cycle = 4
|
|
fast_go_mode = 5
|
|
recipe_reqs = 6
|
|
win_con = 7
|
|
stat_bonus = 8
|
|
lord_kyroo = 9
|
|
local_item_notifs = 10
|
|
|
|
class KHDDDSocket():
|
|
@property
|
|
def isConnected(self) -> bool:
|
|
if self.client:
|
|
return self.client.connectedToDDD
|
|
else:
|
|
return False
|
|
@isConnected.setter
|
|
def isConnected(self, value: bool):
|
|
if self.client:
|
|
self.client.connectedToDDD = value
|
|
|
|
def __init__(self, client, host: str = "127.0.0.1", port:int = 13713):
|
|
self.client: KHDDDContext = client
|
|
self.host: str = host
|
|
self.port: int = port
|
|
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.client_socket = None
|
|
self.deathTime = ""
|
|
self.goaled = False
|
|
self.client_item_index = 0
|
|
|
|
async def start_server(self):
|
|
logger.debug("Starting server... waiting for game.")
|
|
self.loop = asyncio.get_event_loop()
|
|
self.server_socket.bind((self.host, self.port))
|
|
self.server_socket.listen(1)
|
|
await self._accept_client()
|
|
|
|
async def _accept_client(self):
|
|
"""Wait for a client to connect and start a listener task."""
|
|
logger.info("Waiting for KHDDD game connection...")
|
|
while True:
|
|
try:
|
|
self.client_socket, addr = await self.loop.sock_accept(self.server_socket)
|
|
logger.info("KHDDD game client connected.")
|
|
self.isConnected = True
|
|
self.loop.create_task(self.listen())
|
|
self.send_client_cmd(DDDCommand.DEATH_LINK, str(self.client.death_link))
|
|
# Reapply deathlink to game after ddd websocket reconnect
|
|
self.client.get_items()
|
|
|
|
# Queue up a request for slot data
|
|
self.client.get_slot_data()
|
|
# Resend all items to game after ddd websocket reconnect
|
|
return
|
|
except OSError as e:
|
|
logger.debug(f"Socket accept failed ({e}); retrying in 5s")
|
|
self.isConnected = False
|
|
await asyncio.sleep(5)
|
|
|
|
def _safe_close_client(self):
|
|
"""Close the current client socket without killing the server socket."""
|
|
try:
|
|
if self.client_socket:
|
|
self.client_socket.close()
|
|
finally:
|
|
self.client_socket = None
|
|
self.isConnected = False
|
|
|
|
|
|
async def listen(self):
|
|
while True:
|
|
try:
|
|
message = await self.loop.sock_recv(self.client_socket, 1024)
|
|
if not message:
|
|
raise ConnectionResetError("Client disconnected")
|
|
msgStr = message.decode("utf-8").replace("\n", "")
|
|
values = msgStr.split(";")
|
|
logger.debug("Received message: "+msgStr)
|
|
self.handle_message(values)
|
|
except (ConnectionResetError, OSError) as e:
|
|
logger.info(f"Connection to game lost, reconnecting...")
|
|
self._safe_close_client()
|
|
await self._accept_client()
|
|
return
|
|
|
|
def send(self, msgId: int, values: list):
|
|
if not self.isConnected or self.client_socket is None:
|
|
return
|
|
try:
|
|
msg = str(msgId)
|
|
for val in values:
|
|
msg += ";" + str(val)
|
|
msg += "\n"
|
|
self.client_socket.send(msg.encode("utf-8"))
|
|
logger.debug("Sent message: "+msg)
|
|
except (OSError, ConnectionResetError, BrokenPipeError) as e:
|
|
logger.debug(f"Error sending message {msgId}: {e}; connection may be lost")
|
|
self.isConnected = False
|
|
except Exception as e:
|
|
logger.debug(f"Error sending message {msgId}: {e}")
|
|
|
|
def handle_message(self, message: list[str]):
|
|
if message[0] == '':
|
|
return
|
|
|
|
logger.debug("Handling message: "+str(message))
|
|
msgType = MessageType(int(message[0]))
|
|
|
|
if msgType == MessageType.ChestChecked:
|
|
locid = int(message[1])
|
|
self.client.check_location_IDs.append(locid)
|
|
logger.debug("Chest location checked: "+str(locid))
|
|
|
|
elif msgType == MessageType.LevelChecked:
|
|
logger.debug("Level checked")
|
|
self.client.check_location_IDs.append(int(message[1]))
|
|
|
|
elif msgType == MessageType.StoryChecked:
|
|
for x in message:
|
|
if len(x) > 1:
|
|
locid = int(x)
|
|
self.client.check_location_IDs.append(locid)
|
|
logger.debug("Story location checked: " + str(locid))
|
|
|
|
elif msgType == MessageType.PortalChecked:
|
|
for x in message:
|
|
if len(x) > 1:
|
|
locid = int(x)
|
|
self.client.check_location_IDs.append(locid)
|
|
logger.debug("Secret portal location checked: " + str(locid))
|
|
|
|
elif msgType == MessageType.Deathlink:
|
|
self.deathTime = message[1]
|
|
|
|
elif msgType == MessageType.Victory:
|
|
self.goaled = True
|
|
|
|
elif msgType == MessageType.RequestAllItems:
|
|
self.client.get_items()
|
|
|
|
elif msgType == MessageType.Handshake:
|
|
logger.debug("Attempting to respond to handshake")
|
|
self.send(MessageType.Handshake, [str(self.client.connectedToAp)])
|
|
logger.debug("Responded to Handshake")
|
|
|
|
|
|
elif msgType == MessageType.GetCurrentIndex:
|
|
self.client_item_index = int(message[1])
|
|
|
|
def send_singleItem(self, id: int, itemCnt):
|
|
msgCont = [str(id), str(itemCnt)]
|
|
self.send(MessageType.ReceiveSingleItem, msgCont)
|
|
|
|
|
|
def send_multipleItems(self, items, itemCnt):
|
|
logger.debug(f"Sending multiple items {len(items)}")
|
|
values = []
|
|
|
|
msgLimit = 3 #Need to cap how long each message can be to prevent data from being lost
|
|
|
|
currItemCount = 0
|
|
currMsg = 0
|
|
|
|
sendCnt = 0
|
|
for item in items:
|
|
if currItemCount == 0:
|
|
values.append([])
|
|
values[currMsg].append(item.item)
|
|
currItemCount += 1
|
|
sendCnt += 1
|
|
if currItemCount > msgLimit:
|
|
currItemCount = 0
|
|
currMsg = currMsg + 1
|
|
|
|
|
|
sendMsg = 0
|
|
for msg in values:
|
|
msg.append(itemCnt-(sendCnt-(msgLimit*sendMsg)))
|
|
sendCnt -= 1
|
|
sendMsg += 1
|
|
self.send(MessageType.ReceiveAllItems, msg)
|
|
|
|
def send_slot_data(self, slotType, data):
|
|
if slotType == SlotDataType.keyblade_stats:
|
|
splitNums = data.split(",")
|
|
sendVal = [str(slotType)]
|
|
currStat = 1
|
|
|
|
sendLimit = 10
|
|
|
|
for x in splitNums:
|
|
sendVal.append(x)
|
|
if currStat >= sendLimit:
|
|
self.send(MessageType.SendSlotData, sendVal)
|
|
sendVal = [str(slotType)]
|
|
currStat = 0
|
|
currStat = currStat + 1
|
|
else:
|
|
self.send(MessageType.SendSlotData, [str(slotType), str(data)])
|
|
|
|
def send_client_cmd(self, cmdId, extParam):
|
|
values = [str(cmdId)]
|
|
if extParam != "":
|
|
values.append(extParam)
|
|
logger.debug(f"Sending client command to player: {cmdId}")
|
|
self.send(MessageType.ClientCommand, values)
|
|
|
|
def item_msg(self, itemName:str, owningPlayer:str, itemCategory:str):
|
|
self.send(MessageType.ItemPrompt, [itemName, owningPlayer, itemCategory])
|
|
|
|
def shutdown_server(self):
|
|
self.client_socket.close()
|
|
self.server_socket.close() |