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 connectedClients = new List(); private readonly Dictionary activeConnections = new Dictionary(); private readonly Dictionary knownQuesters = new Dictionary(); 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 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(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(); 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(); 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(); 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(); 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(); 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(); } }