using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Dalamud.Plugin.Services; using Newtonsoft.Json.Linq; namespace QuestionableCompanion.Services; public class SubmarineManager : IDisposable { private readonly IPluginLog log; private readonly AutoRetainerIPC autoRetainerIPC; private readonly Configuration config; private readonly ICommandManager? commandManager; private readonly IFramework? framework; private DateTime lastSubmarineCheck = DateTime.MinValue; private DateTime submarineReloginCooldownEnd = DateTime.MinValue; private DateTime submarineNoAvailableWaitEnd = DateTime.MinValue; private bool submarinesPaused; private bool submarinesWaitingForSeq0; private bool submarineReloginInProgress; private bool submarineJustCompleted; private string? originalCharacterForSubmarines; public bool IsSubmarinePaused => submarinesPaused; public bool IsWaitingForSequence0 => submarinesWaitingForSeq0; public bool IsReloginInProgress => submarineReloginInProgress; public bool IsSubmarineJustCompleted => submarineJustCompleted; public SubmarineManager(IPluginLog log, AutoRetainerIPC autoRetainerIPC, Configuration config, ICommandManager? commandManager = null, IFramework? framework = null) { this.log = log; this.autoRetainerIPC = autoRetainerIPC; this.config = config; this.commandManager = commandManager; this.framework = framework; log.Information("[SubmarineManager] Service initialized"); } private string? GetConfigPath() { try { string userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); if (string.IsNullOrEmpty(userProfile)) { string username = Environment.GetEnvironmentVariable("USERNAME"); if (string.IsNullOrEmpty(username)) { log.Warning("[SubmarineManager] Could not resolve USERPROFILE or USERNAME"); return null; } userProfile = "C:\\Users\\" + username; } _003C_003Ey__InlineArray7 buffer = default(_003C_003Ey__InlineArray7); global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7, string>(ref buffer, 0) = userProfile; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7, string>(ref buffer, 1) = "AppData"; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7, string>(ref buffer, 2) = "Roaming"; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7, string>(ref buffer, 3) = "XIVLauncher"; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7, string>(ref buffer, 4) = "pluginConfigs"; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7, string>(ref buffer, 5) = "AutoRetainer"; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7, string>(ref buffer, 6) = "DefaultConfig.json"; return Path.Combine(global::_003CPrivateImplementationDetails_003E.InlineArrayAsReadOnlySpan<_003C_003Ey__InlineArray7, string>(in buffer, 7)); } catch (Exception ex) { log.Error("[SubmarineManager] Error resolving config path: " + ex.Message); return null; } } public bool CheckSubmarines() { if (!config.EnableSubmarineCheck) { return false; } string configPath = GetConfigPath(); if (string.IsNullOrEmpty(configPath)) { log.Warning("[SubmarineManager] Could not resolve config path"); return false; } if (!File.Exists(configPath)) { log.Debug("[SubmarineManager] Config file not found: " + configPath); return false; } try { string content = File.ReadAllText(configPath); if (string.IsNullOrEmpty(content)) { log.Warning("[SubmarineManager] Config file is empty"); return false; } List returnTimes = ParseReturnTimes(content); if (returnTimes.Count == 0) { return false; } long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); int available = 0; long? minDelta = null; foreach (long item in returnTimes) { long delta = item - now; if (delta <= 0) { available++; } else if (!minDelta.HasValue || delta < minDelta.Value) { minDelta = delta; } } if (available > 0) { string plural = ((available == 1) ? "Sub" : "Subs"); log.Information($"[SubmarineManager] {available} {plural} available - pausing quest rotation!"); return true; } if (minDelta.HasValue && minDelta.Value > 0) { int minutes = Math.Max(0, (int)Math.Ceiling((double)minDelta.Value / 60.0)); string plural2 = ((minutes == 1) ? "minute" : "minutes"); log.Debug($"[SubmarineManager] Next submarine in {minutes} {plural2}"); } return false; } catch (Exception ex) { log.Error("[SubmarineManager] Error checking submarines: " + ex.Message); return false; } } public int CheckSubmarinesSoon() { if (!config.EnableSubmarineCheck) { return 0; } string configPath = GetConfigPath(); if (string.IsNullOrEmpty(configPath) || !File.Exists(configPath)) { return 0; } try { string content = File.ReadAllText(configPath); if (string.IsNullOrEmpty(content)) { return 0; } List returnTimes = ParseReturnTimes(content); if (returnTimes.Count == 0) { return 0; } long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); long? minDelta = null; int availableNow = 0; foreach (long item in returnTimes) { long delta = item - now; if (delta <= 0) { availableNow++; } else if (delta <= 120 && (!minDelta.HasValue || delta < minDelta.Value)) { minDelta = delta; } } if (availableNow > 0) { log.Debug($"[SubmarineManager] {availableNow} submarines ready NOW - continue Multi-Mode"); return 999; } if (minDelta.HasValue) { int minutes = (int)Math.Ceiling((double)minDelta.Value / 60.0); log.Debug($"[SubmarineManager] Submarine will be ready in {minDelta.Value} seconds ({minutes} min) - waiting before character switch"); return (int)minDelta.Value; } if (submarineNoAvailableWaitEnd == DateTime.MinValue) { submarineNoAvailableWaitEnd = DateTime.Now.AddSeconds(60.0); log.Information("[SubmarineManager] No submarines available - waiting 60 seconds before relog"); return 60; } if (DateTime.Now < submarineNoAvailableWaitEnd) { int remaining = (int)(submarineNoAvailableWaitEnd - DateTime.Now).TotalSeconds; log.Debug($"[SubmarineManager] Waiting {remaining}s before relog..."); return remaining; } submarineNoAvailableWaitEnd = DateTime.MinValue; return 0; } catch (Exception ex) { log.Error("[SubmarineManager] Error checking submarines soon: " + ex.Message); return 0; } } private List ParseReturnTimes(string jsonContent) { List returnTimes = new List(); try { JObject json = JObject.Parse(jsonContent); HashSet enabledSubs = new HashSet(StringComparer.OrdinalIgnoreCase); if (json.SelectTokens("$..EnabledSubs").FirstOrDefault() is JArray enabledSubsArray) { foreach (JToken item in enabledSubsArray) { string subName = item.Value(); if (!string.IsNullOrEmpty(subName)) { enabledSubs.Add(subName); } } if (enabledSubs.Count > 0) { log.Information($"[SubmarineManager] Found {enabledSubs.Count} enabled submarines: {string.Join(", ", enabledSubs)}"); } else { log.Information("[SubmarineManager] EnabledSubs array found but empty - NO submarines will be checked"); } FindReturnTimes(json, returnTimes, enabledSubs); } else { log.Information("[SubmarineManager] No EnabledSubs found in config - checking all submarines"); FindReturnTimes(json, returnTimes); } } catch { string pattern = "\"ReturnTime\"\\s*:\\s*(\\d+)"; foreach (Match match in Regex.Matches(jsonContent, pattern)) { if (match.Groups.Count > 1 && long.TryParse(match.Groups[1].Value, out var timestamp)) { returnTimes.Add(timestamp); } } } return returnTimes; } private void FindReturnTimes(JToken token, List returnTimes, HashSet? enabledSubs = null) { if (token is JObject obj) { if (obj.TryGetValue("Name", out JToken nameToken) && obj.TryGetValue("ReturnTime", out JToken returnTimeToken)) { string submarineName = nameToken.Value(); if ((enabledSubs == null || (submarineName != null && enabledSubs.Contains(submarineName))) && returnTimeToken.Type == JTokenType.Integer) { long returnTime = returnTimeToken.Value(); returnTimes.Add(returnTime); if (enabledSubs != null) { log.Debug($"[SubmarineManager] Including submarine '{submarineName}' (ReturnTime: {returnTime})"); } } } { foreach (JProperty property in obj.Properties()) { FindReturnTimes(property.Value, returnTimes, enabledSubs); } return; } } if (!(token is JArray array)) { return; } foreach (JToken item in array) { FindReturnTimes(item, returnTimes, enabledSubs); } } public void StartSubmarineWait(string currentCharacter) { submarinesWaitingForSeq0 = true; originalCharacterForSubmarines = currentCharacter; log.Information("[SubmarineManager] Waiting for Sequence 0 completion before enabling Multi-Mode"); } public void EnableMultiMode() { if (!autoRetainerIPC.IsAvailable) { log.Warning("[SubmarineManager] AutoRetainer not available - cannot enable Multi-Mode"); return; } try { if (autoRetainerIPC.GetMultiModeEnabled()) { log.Information("[SubmarineManager] Multi-Mode is already enabled - skipping activation"); submarinesPaused = true; submarinesWaitingForSeq0 = false; return; } if (commandManager != null && framework != null) { log.Information("[SubmarineManager] Sending /ays multi e command..."); framework.RunOnFrameworkThread(delegate { commandManager.ProcessCommand("/ays multi e"); }).Wait(); log.Information("[SubmarineManager] ✓ /ays multi e command sent"); } autoRetainerIPC.SetMultiModeEnabled(enabled: true); submarinesPaused = true; submarinesWaitingForSeq0 = false; log.Information("[SubmarineManager] Multi-Mode enabled - quest automation paused"); } catch (Exception ex) { log.Error("[SubmarineManager] Failed to enable Multi-Mode: " + ex.Message); } } public void DisableMultiModeAndReturn() { if (!autoRetainerIPC.IsAvailable) { log.Warning("[SubmarineManager] AutoRetainer not available"); return; } try { if (commandManager != null && framework != null) { log.Information("[SubmarineManager] Sending /ays multi d command..."); framework.RunOnFrameworkThread(delegate { commandManager.ProcessCommand("/ays multi d"); }).Wait(); log.Information("[SubmarineManager] ✓ /ays multi d command sent"); } autoRetainerIPC.SetMultiModeEnabled(enabled: false); log.Information("[SubmarineManager] Multi-Mode disabled - starting return to original character"); submarineNoAvailableWaitEnd = DateTime.MinValue; if (!string.IsNullOrEmpty(originalCharacterForSubmarines)) { submarineReloginInProgress = true; log.Information("[SubmarineManager] Returning to original character: " + originalCharacterForSubmarines); } } catch (Exception ex) { log.Error("[SubmarineManager] Failed to disable Multi-Mode: " + ex.Message); } } public void CompleteSubmarineRelog() { submarineReloginInProgress = false; submarinesPaused = false; submarineJustCompleted = true; submarineReloginCooldownEnd = DateTime.Now.AddSeconds(config.SubmarineReloginCooldown); log.Information($"[SubmarineManager] Submarine rotation complete - cooldown active for {config.SubmarineReloginCooldown} seconds"); } public bool IsSubmarineCooldownActive() { return DateTime.Now < submarineReloginCooldownEnd; } public void ClearSubmarineJustCompleted() { submarineJustCompleted = false; log.Information("[SubmarineManager] Cooldown expired - submarine checks re-enabled"); } public void Reset() { submarinesPaused = false; submarinesWaitingForSeq0 = false; submarineReloginInProgress = false; submarineJustCompleted = false; originalCharacterForSubmarines = null; submarineReloginCooldownEnd = DateTime.MinValue; log.Information("[SubmarineManager] State reset"); } public void Dispose() { Reset(); log.Information("[SubmarineManager] Service disposed"); } }