388 lines
11 KiB
C#
388 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
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);
|
|
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)
|
|
{
|
|
if (token is JObject obj)
|
|
{
|
|
{
|
|
foreach (JProperty property in obj.Properties())
|
|
{
|
|
if (property.Name == "ReturnTime" && property.Value.Type == JTokenType.Integer)
|
|
{
|
|
returnTimes.Add(property.Value.Value<long>());
|
|
}
|
|
else
|
|
{
|
|
FindReturnTimes(property.Value, returnTimes);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (!(token is JArray array))
|
|
{
|
|
return;
|
|
}
|
|
foreach (JToken item in array)
|
|
{
|
|
FindReturnTimes(item, returnTimes);
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|