Files
dockipelago/worlds/khddd/Socket.py
Jonathan Tinney 7971961166
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
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

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()