qstbak/QuestionableCompanion/QuestionableCompanion.Services/SubmarineManager.cs
2025-12-04 04:40:50 +10:00

420 lines
12 KiB
C#

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<string> buffer = default(_003C_003Ey__InlineArray7<string>);
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 0) = userProfile;
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 1) = "AppData";
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 2) = "Roaming";
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 3) = "XIVLauncher";
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 4) = "pluginConfigs";
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 5) = "AutoRetainer";
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 6) = "DefaultConfig.json";
return Path.Combine(global::_003CPrivateImplementationDetails_003E.InlineArrayAsReadOnlySpan<_003C_003Ey__InlineArray7<string>, 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<long> 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<long> 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<long> ParseReturnTimes(string jsonContent)
{
List<long> returnTimes = new List<long>();
try
{
JObject json = JObject.Parse(jsonContent);
HashSet<string> enabledSubs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (json.SelectTokens("$..EnabledSubs").FirstOrDefault() is JArray enabledSubsArray)
{
foreach (JToken item in enabledSubsArray)
{
string subName = item.Value<string>();
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<long> returnTimes, HashSet<string>? enabledSubs = null)
{
if (token is JObject obj)
{
if (obj.TryGetValue("Name", out JToken nameToken) && obj.TryGetValue("ReturnTime", out JToken returnTimeToken))
{
string submarineName = nameToken.Value<string>();
if ((enabledSubs == null || (submarineName != null && enabledSubs.Contains(submarineName))) && returnTimeToken.Type == JTokenType.Integer)
{
long returnTime = returnTimeToken.Value<long>();
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");
}
}