qstbak/QuestionableCompanion/QuestionableCompanion.Services/LANHelperServer.cs
2025-12-07 10:54:53 +10:00

577 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Plugin.Services;
using Newtonsoft.Json;
using QuestionableCompanion.Models;
namespace QuestionableCompanion.Services;
public class LANHelperServer : IDisposable
{
private readonly IPluginLog log;
private readonly IClientState clientState;
private readonly IFramework framework;
private readonly Configuration config;
private readonly PartyInviteAutoAccept partyInviteAutoAccept;
private readonly ICommandManager commandManager;
private readonly Plugin plugin;
private TcpListener? listener;
private CancellationTokenSource? cancellationTokenSource;
private readonly List<TcpClient> connectedClients = new List<TcpClient>();
private readonly Dictionary<string, TcpClient> activeConnections = new Dictionary<string, TcpClient>();
private readonly Dictionary<string, DateTime> knownQuesters = new Dictionary<string, DateTime>();
private bool isRunning;
private string? cachedPlayerName;
private ushort cachedWorldId;
private DateTime lastCacheRefresh = DateTime.MinValue;
private const int CACHE_REFRESH_SECONDS = 30;
public bool IsRunning => isRunning;
public int ConnectedClientCount => connectedClients.Count;
public List<string> GetConnectedClientNames()
{
DateTime now = DateTime.Now;
foreach (string s in (from kvp in knownQuesters
where (now - kvp.Value).TotalSeconds > 60.0
select kvp.Key).ToList())
{
knownQuesters.Remove(s);
}
return knownQuesters.Keys.ToList();
}
public LANHelperServer(IPluginLog log, IClientState clientState, IFramework framework, Configuration config, PartyInviteAutoAccept partyInviteAutoAccept, ICommandManager commandManager, Plugin plugin)
{
this.log = log;
this.clientState = clientState;
this.framework = framework;
this.config = config;
this.partyInviteAutoAccept = partyInviteAutoAccept;
this.commandManager = commandManager;
this.plugin = plugin;
}
public void Start()
{
if (isRunning)
{
log.Warning("[LANServer] Server already running");
return;
}
Task.Run(async delegate
{
try
{
framework.Update += OnFrameworkUpdate;
int retries = 5;
while (retries > 0)
{
try
{
listener = new TcpListener(IPAddress.Any, config.LANServerPort);
listener.Start();
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
retries--;
if (retries == 0)
{
throw;
}
log.Warning($"[LANServer] Port {config.LANServerPort} in use, retrying in 1s... ({retries} retries left)");
await Task.Delay(1000);
continue;
}
break;
}
cancellationTokenSource = new CancellationTokenSource();
isRunning = true;
log.Information("[LANServer] ===== LAN HELPER SERVER STARTED (v2-DEBUG) =====");
log.Information($"[LANServer] Listening on port {config.LANServerPort}");
log.Information("[LANServer] Waiting for player info cache... (via framework update)");
Task.Run(() => AcceptClientsAsync(cancellationTokenSource.Token));
Task.Run(() => BroadcastPresenceAsync(cancellationTokenSource.Token));
}
catch (Exception ex2)
{
log.Error("[LANServer] Failed to start server: " + ex2.Message);
isRunning = false;
framework.Update -= OnFrameworkUpdate;
}
});
}
private void OnFrameworkUpdate(IFramework framework)
{
if (isRunning)
{
DateTime now = DateTime.Now;
if ((now - lastCacheRefresh).TotalSeconds >= 30.0)
{
log.Debug($"[LANServer] Framework.Update triggered cache refresh (last: {(now - lastCacheRefresh).TotalSeconds:F1}s ago)");
RefreshPlayerCache();
}
}
}
private void RefreshPlayerCache()
{
try
{
log.Debug("[LANServer] RefreshPlayerCache called");
IPlayerCharacter player = clientState.LocalPlayer;
if (player != null)
{
string newName = player.Name.ToString();
ushort newWorldId = (ushort)player.HomeWorld.RowId;
if (cachedPlayerName != newName || cachedWorldId != newWorldId)
{
if (cachedPlayerName == null)
{
log.Information($"[LANServer] ✓ Player info cached: {newName}@{newWorldId}");
}
else
{
log.Information($"[LANServer] Player info updated: {newName}@{newWorldId}");
}
cachedPlayerName = newName;
cachedWorldId = newWorldId;
}
lastCacheRefresh = DateTime.Now;
}
else
{
log.Warning("[LANServer] RefreshPlayerCache: LocalPlayer is NULL!");
}
lastCacheRefresh = DateTime.Now;
}
catch (Exception ex)
{
log.Error("[LANServer] RefreshPlayerCache ERROR: " + ex.Message);
log.Error("[LANServer] Stack: " + ex.StackTrace);
}
}
private async Task BroadcastPresenceAsync(CancellationToken cancellationToken)
{
_ = 3;
try
{
using UdpClient udpClient = new UdpClient();
udpClient.EnableBroadcast = true;
IPEndPoint broadcastEndpoint = new IPEndPoint(IPAddress.Broadcast, 47789);
if (cachedPlayerName == null)
{
return;
}
string json = JsonConvert.SerializeObject(new
{
Type = "HELPER_ANNOUNCE",
Name = cachedPlayerName,
WorldId = cachedWorldId,
Port = config.LANServerPort
});
byte[] bytes = Encoding.UTF8.GetBytes(json);
for (int i = 0; i < 3; i++)
{
await udpClient.SendAsync(bytes, bytes.Length, broadcastEndpoint);
log.Information($"[LANServer] \ud83d\udce1 Broadcast announcement sent ({i + 1}/3)");
await Task.Delay(500, cancellationToken);
}
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(30000, cancellationToken);
await udpClient.SendAsync(bytes, bytes.Length, broadcastEndpoint);
log.Debug("[LANServer] Broadcast presence updated");
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex2)
{
log.Error("[LANServer] UDP broadcast error: " + ex2.Message);
}
}
public void Stop()
{
if (!isRunning && listener == null)
{
return;
}
log.Information("[LANServer] Stopping server...");
isRunning = false;
cancellationTokenSource?.Cancel();
framework.Update -= OnFrameworkUpdate;
lock (connectedClients)
{
foreach (TcpClient client in connectedClients.ToList())
{
try
{
if (client.Connected)
{
try
{
NetworkStream stream = client.GetStream();
if (stream.CanWrite)
{
string json = JsonConvert.SerializeObject(new LANMessage(LANMessageType.DISCONNECT));
byte[] bytes = Encoding.UTF8.GetBytes(json + "\n");
stream.Write(bytes, 0, bytes.Length);
}
}
catch
{
}
}
client.Close();
client.Dispose();
}
catch
{
}
}
connectedClients.Clear();
}
try
{
listener?.Stop();
}
catch (Exception ex)
{
log.Warning("[LANServer] Error stopping listener: " + ex.Message);
}
listener = null;
log.Information("[LANServer] Server stopped");
}
private async Task AcceptClientsAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested && isRunning)
{
try
{
TcpClient client = await listener.AcceptTcpClientAsync(cancellationToken);
string clientIP = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString();
log.Information("[LANServer] Client connected from " + clientIP);
lock (connectedClients)
{
connectedClients.Add(client);
}
Task.Run(() => HandleClientAsync(client, cancellationToken), cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex2)
{
log.Error("[LANServer] Error accepting client: " + ex2.Message);
await Task.Delay(1000, cancellationToken);
}
}
}
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
{
string clientIP = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString();
try
{
using NetworkStream stream = client.GetStream();
using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
while (!cancellationToken.IsCancellationRequested && client.Connected)
{
string line = await reader.ReadLineAsync(cancellationToken);
if (string.IsNullOrEmpty(line))
{
break;
}
try
{
LANMessage message = JsonConvert.DeserializeObject<LANMessage>(line);
if (message != null)
{
await HandleMessageAsync(client, message, clientIP);
}
}
catch (JsonException ex)
{
log.Error("[LANServer] Invalid message from " + clientIP + ": " + ex.Message);
}
}
}
catch (Exception ex2)
{
log.Error("[LANServer] Client " + clientIP + " error: " + ex2.Message);
}
finally
{
log.Information("[LANServer] Client " + clientIP + " disconnected");
lock (connectedClients)
{
connectedClients.Remove(client);
}
client.Close();
}
}
private async Task HandleMessageAsync(TcpClient client, LANMessage message, string clientIP)
{
log.Debug($"[LANServer] Received {message.Type} from {clientIP}");
switch (message.Type)
{
case LANMessageType.REQUEST_HELPER:
await HandleHelperRequest(client, message);
break;
case LANMessageType.HELPER_STATUS:
await HandleStatusRequest(client);
break;
case LANMessageType.INVITE_NOTIFICATION:
await HandleInviteNotification(client, message);
break;
case LANMessageType.FOLLOW_COMMAND:
await HandleFollowCommand(client, message);
break;
case LANMessageType.CHAUFFEUR_PICKUP_REQUEST:
await HandleChauffeurSummon(message);
break;
case LANMessageType.HEARTBEAT:
{
LANHeartbeat heartbeatData = message.GetData<LANHeartbeat>();
if (heartbeatData != null && heartbeatData.ClientRole == "Quester" && !string.IsNullOrEmpty(heartbeatData.ClientName))
{
string questerKey = $"{heartbeatData.ClientName}@{heartbeatData.ClientWorldId}";
knownQuesters[questerKey] = DateTime.Now;
}
SendMessage(client, new LANMessage(LANMessageType.HEARTBEAT));
break;
}
default:
log.Debug($"[LANServer] Unhandled message type: {message.Type}");
break;
}
}
private async Task HandleHelperRequest(TcpClient client, LANMessage message)
{
LANHelperRequest request = message.GetData<LANHelperRequest>();
if (request != null)
{
log.Information("[LANServer] Helper requested by " + request.QuesterName + " for duty: " + request.DutyName);
await SendCurrentStatus(client);
partyInviteAutoAccept.EnableForQuester(request.QuesterName);
log.Information("[LANServer] Auto-accept enabled for " + request.QuesterName);
}
}
private async Task HandleStatusRequest(TcpClient client)
{
await SendCurrentStatus(client);
}
private async Task SendCurrentStatus(TcpClient client)
{
try
{
log.Debug("[LANServer] SendCurrentStatus: Start");
if (cachedPlayerName == null)
{
log.Warning("[LANServer] SendCurrentStatus: Player info not cached! Sending NotReady status.");
LANHelperStatusResponse notReadyStatus = new LANHelperStatusResponse
{
Name = "Unknown",
WorldId = 0,
Status = LANHelperStatus.Offline,
CurrentActivity = "Waiting for character login..."
};
SendMessage(client, new LANMessage(LANMessageType.HELPER_STATUS, notReadyStatus));
return;
}
log.Debug($"[LANServer] SendCurrentStatus: Cached Name={cachedPlayerName}, World={cachedWorldId}");
LANHelperStatusResponse status = new LANHelperStatusResponse
{
Name = cachedPlayerName,
WorldId = cachedWorldId,
Status = LANHelperStatus.Available,
CurrentActivity = "Ready"
};
log.Debug("[LANServer] SendCurrentStatus: Status object created");
LANMessage msg = new LANMessage(LANMessageType.HELPER_STATUS, status);
log.Debug("[LANServer] SendCurrentStatus: LANMessage created");
SendMessage(client, msg);
log.Debug("[LANServer] SendCurrentStatus: Message sent");
}
catch (Exception ex)
{
log.Error("[LANServer] SendCurrentStatus CRASH: " + ex.Message);
log.Error("[LANServer] Stack: " + ex.StackTrace);
}
}
private async Task HandleInviteNotification(TcpClient client, LANMessage message)
{
string questerName = message.GetData<string>();
log.Information("[LANServer] Invite notification from " + questerName);
SendMessage(client, new LANMessage(LANMessageType.INVITE_ACCEPTED));
}
private async Task HandleFollowCommand(TcpClient client, LANMessage message)
{
LANFollowCommand followCmd = message.GetData<LANFollowCommand>();
if (followCmd != null)
{
ChauffeurModeService chauffeurSvc = plugin.GetChauffeurMode();
if (chauffeurSvc == null)
{
log.Warning("[LANServer] No ChauffeurModeService available for position update");
return;
}
if (chauffeurSvc.IsTransportingQuester)
{
log.Debug("[LANServer] Ignoring FOLLOW_COMMAND - Chauffeur Mode is actively transporting");
return;
}
string questerName = config.AssignedQuesterForFollowing ?? "LAN Quester";
chauffeurSvc.UpdateQuesterPositionFromLAN(followCmd.X, followCmd.Y, followCmd.Z, followCmd.TerritoryId, questerName);
log.Debug($"[LANServer] Updated quester position: ({followCmd.X:F2}, {followCmd.Y:F2}, {followCmd.Z:F2}) Zone={followCmd.TerritoryId}");
SendMessage(client, new LANMessage(LANMessageType.FOLLOW_STARTED));
}
}
private void SendMessage(TcpClient client, LANMessage message)
{
try
{
if (client.Connected)
{
string json = JsonConvert.SerializeObject(message);
byte[] bytes = Encoding.UTF8.GetBytes(json + "\n");
client.GetStream().Write(bytes, 0, bytes.Length);
}
}
catch (Exception ex)
{
log.Error("[LANServer] Failed to send message: " + ex.Message);
}
}
public void BroadcastMessage(LANMessage message)
{
lock (connectedClients)
{
foreach (TcpClient client in connectedClients.ToList())
{
SendMessage(client, message);
}
}
}
private async Task HandleChauffeurSummon(LANMessage message)
{
LANChauffeurSummon summonData = message.GetData<LANChauffeurSummon>();
if (summonData == null)
{
log.Error("[LANServer] HandleChauffeurSummon: Failed to deserialize summon data!");
return;
}
log.Information("[LANServer] =========================================");
log.Information("[LANServer] *** CHAUFFEUR PICKUP REQUEST RECEIVED ***");
log.Information("[LANServer] =========================================");
log.Information($"[LANServer] Quester: {summonData.QuesterName}@{summonData.QuesterWorldId}");
log.Information($"[LANServer] Zone: {summonData.ZoneId}");
log.Information($"[LANServer] Target: ({summonData.TargetX:F2}, {summonData.TargetY:F2}, {summonData.TargetZ:F2})");
log.Information($"[LANServer] AttuneAetheryte: {summonData.IsAttuneAetheryte}");
ChauffeurModeService chauffeur = plugin.GetChauffeurMode();
if (chauffeur != null)
{
Vector3 targetPos = new Vector3(summonData.TargetX, summonData.TargetY, summonData.TargetZ);
Vector3 questerPos = new Vector3(summonData.QuesterX, summonData.QuesterY, summonData.QuesterZ);
log.Information("[LANServer] Calling ChauffeurModeService.StartHelperWorkflow...");
await framework.RunOnFrameworkThread(delegate
{
chauffeur.StartHelperWorkflow(summonData.QuesterName, summonData.QuesterWorldId, summonData.ZoneId, targetPos, questerPos, summonData.IsAttuneAetheryte);
});
log.Information("[LANServer] StartHelperWorkflow dispatched to framework thread");
}
else
{
log.Error("[LANServer] ChauffeurModeService is null! Cannot start helper workflow.");
}
}
public void SendChauffeurMountReady(string questerName, ushort questerWorldId)
{
LANChauffeurResponse response = new LANChauffeurResponse
{
QuesterName = questerName,
QuesterWorldId = questerWorldId
};
LANMessage message = new LANMessage(LANMessageType.CHAUFFEUR_HELPER_READY_FOR_MOUNT, response);
log.Information($"[LANServer] Sending Chauffeur Mount Ready to connected clients for {questerName}@{questerWorldId}");
lock (connectedClients)
{
foreach (TcpClient client in connectedClients.ToList())
{
if (client.Connected)
{
SendMessage(client, message);
}
}
}
}
public void SendChauffeurArrived(string questerName, ushort questerWorldId)
{
LANChauffeurResponse response = new LANChauffeurResponse
{
QuesterName = questerName,
QuesterWorldId = questerWorldId
};
LANMessage message = new LANMessage(LANMessageType.CHAUFFEUR_HELPER_ARRIVED_DEST, response);
log.Information($"[LANServer] Sending Chauffeur Arrived to connected clients for {questerName}@{questerWorldId}");
lock (connectedClients)
{
foreach (TcpClient client in connectedClients.ToList())
{
if (client.Connected)
{
SendMessage(client, message);
}
}
}
}
public void Dispose()
{
Stop();
}
}