qstbak/QuestionableCompanion/QuestionableCompanion.Services/QuestPreCheckService.cs
2025-12-04 04:39:08 +10:00

301 lines
9.9 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Newtonsoft.Json;
namespace QuestionableCompanion.Services;
public class QuestPreCheckService : IDisposable
{
private readonly IPluginLog log;
private readonly IClientState clientState;
private readonly Configuration config;
private readonly AutoRetainerIPC autoRetainerIPC;
private Dictionary<string, bool> preCheckResults = new Dictionary<string, bool>();
private Dictionary<string, Dictionary<uint, bool>> questDatabase = new Dictionary<string, Dictionary<uint, bool>>();
private Dictionary<string, DateTime> lastRefreshByCharacter = new Dictionary<string, DateTime>();
private readonly TimeSpan refreshInterval = TimeSpan.FromMinutes(30L);
private string QuestDatabasePath
{
get
{
global::_003C_003Ey__InlineArray5<string> buffer = default(global::_003C_003Ey__InlineArray5<string>);
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 0) = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 1) = "XIVLauncher";
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 2) = "pluginConfigs";
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 3) = "QuestionableCompanion";
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 4) = "QuestDatabase.json";
return Path.Combine(global::_003CPrivateImplementationDetails_003E.InlineArrayAsReadOnlySpan<global::_003C_003Ey__InlineArray5<string>, string>(in buffer, 5));
}
}
public QuestPreCheckService(IPluginLog log, IClientState clientState, Configuration config, AutoRetainerIPC autoRetainerIPC)
{
this.log = log;
this.clientState = clientState;
this.config = config;
this.autoRetainerIPC = autoRetainerIPC;
LoadQuestDatabase();
}
private void LoadQuestDatabase()
{
try
{
EnsureQuestDatabasePath();
if (!File.Exists(QuestDatabasePath))
{
log.Information("[QuestPreCheck] Creating new quest database...");
questDatabase = new Dictionary<string, Dictionary<uint, bool>>();
return;
}
string json = File.ReadAllText(QuestDatabasePath);
if (string.IsNullOrEmpty(json))
{
questDatabase = new Dictionary<string, Dictionary<uint, bool>>();
return;
}
questDatabase = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<uint, bool>>>(json) ?? new Dictionary<string, Dictionary<uint, bool>>();
log.Information($"[QuestPreCheck] Loaded quest database for {questDatabase.Count} characters");
}
catch (Exception ex)
{
log.Error("[QuestPreCheck] Error loading quest database: " + ex.Message);
questDatabase = new Dictionary<string, Dictionary<uint, bool>>();
}
}
private void SaveQuestDatabase()
{
try
{
EnsureQuestDatabasePath();
string json = JsonConvert.SerializeObject(questDatabase, Formatting.Indented);
File.WriteAllText(QuestDatabasePath, json);
log.Information($"[QuestPreCheck] Quest database saved ({questDatabase.Count} characters)");
}
catch (Exception ex)
{
log.Error("[QuestPreCheck] Error saving quest database: " + ex.Message);
}
}
private void EnsureQuestDatabasePath()
{
string directory = Path.GetDirectoryName(QuestDatabasePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
public unsafe void ScanCurrentCharacterQuestStatus(bool verbose = false)
{
if (clientState.LocalPlayer == null)
{
log.Warning("[QuestPreCheck] No local player found");
return;
}
string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
string charName = $"{clientState.LocalPlayer.Name}@{worldName}";
if (verbose)
{
log.Information("[QuestPreCheck] Scanning quest status for: " + charName);
}
if (!questDatabase.ContainsKey(charName))
{
questDatabase[charName] = new Dictionary<uint, bool>();
}
if (QuestManager.Instance() == null)
{
log.Error("[QuestPreCheck] QuestManager not available");
return;
}
int questsScanned = 0;
int questsCompleted = 0;
int questsChanged = 0;
List<uint> newlyCompleted = new List<uint>();
List<uint> questsToScan = config.QuestPreCheckRange ?? new List<uint>();
if (questsToScan.Count == 0)
{
for (uint questId = 1u; questId <= 4500; questId++)
{
questsToScan.Add(questId);
}
}
foreach (uint questId2 in questsToScan)
{
try
{
bool num = QuestManager.IsQuestComplete((ushort)(questId2 % 65536));
questsScanned++;
if (num)
{
questsCompleted++;
if (!questDatabase[charName].GetValueOrDefault(questId2, defaultValue: false))
{
questDatabase[charName][questId2] = true;
questsChanged++;
newlyCompleted.Add(questId2);
if (verbose)
{
log.Debug($"[QuestPreCheck] {charName} - Quest {questId2}: ✓ NEWLY COMPLETED");
}
}
else
{
questDatabase[charName][questId2] = true;
}
}
if (verbose && questId2 % 500 == 0)
{
log.Debug($"[QuestPreCheck] Progress: {questId2}/{questsToScan.Count} quests scanned...");
}
}
catch (Exception ex)
{
log.Error($"[QuestPreCheck] Error checking quest {questId2}: {ex.Message}");
}
}
if (verbose)
{
log.Information($"[QuestPreCheck] Scan complete: {questsScanned} checked, {questsCompleted} completed, {questsChanged} changed");
if (newlyCompleted.Count > 0)
{
log.Information("[QuestPreCheck] NEWLY COMPLETED: " + string.Join(", ", newlyCompleted));
}
}
lastRefreshByCharacter[charName] = DateTime.Now;
SaveQuestDatabase();
}
public void RefreshQuestDatabasePeriodic()
{
if (clientState.LocalPlayer != null && clientState.IsLoggedIn)
{
string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
string charName = $"{clientState.LocalPlayer.Name}@{worldName}";
if (!lastRefreshByCharacter.TryGetValue(charName, out var lastRefresh) || DateTime.Now - lastRefresh >= refreshInterval)
{
log.Information("[QuestDB] === 30-MINUTE REFRESH TRIGGERED ===");
log.Information("[QuestDB] Updating quest status for: " + charName);
ScanCurrentCharacterQuestStatus(verbose: true);
log.Information("[QuestDB] === 30-MINUTE REFRESH COMPLETE ===");
}
}
}
public void LogCompletedQuestsBeforeLogout()
{
if (clientState.LocalPlayer != null)
{
string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
string charName = $"{clientState.LocalPlayer.Name}@{worldName}";
log.Information("[QuestDB] Logging final quest status before logout: " + charName);
ScanCurrentCharacterQuestStatus();
log.Information("[QuestDB] Final quest state saved for: " + charName);
}
}
public Dictionary<string, bool> PerformPreRotationCheck(uint stopQuestId, List<string> characters)
{
log.Information("[QuestPreCheck] === STARTING PRE-ROTATION QUEST VERIFICATION ===");
log.Information($"[QuestPreCheck] Checking {characters.Count} characters for quest {stopQuestId}...");
preCheckResults.Clear();
foreach (string character in characters)
{
try
{
if (questDatabase.ContainsKey(character) && questDatabase[character].ContainsKey(stopQuestId))
{
bool isCompleted = questDatabase[character][stopQuestId];
preCheckResults[character] = isCompleted;
string status = (isCompleted ? "✓ COMPLETED" : "○ PENDING");
log.Information($"[QuestPreCheck] {character}: {status} (from database)");
}
else
{
log.Debug("[QuestPreCheck] " + character + ": Not in database, will check during rotation");
preCheckResults[character] = false;
}
}
catch (Exception ex)
{
log.Error("[QuestPreCheck] Error checking " + character + ": " + ex.Message);
preCheckResults[character] = false;
}
}
log.Information("[QuestPreCheck] === PRE-ROTATION CHECK COMPLETE ===");
return preCheckResults;
}
public bool ShouldSkipCharacter(string characterName, uint questId)
{
if (preCheckResults.TryGetValue(characterName, out var isCompleted) && isCompleted)
{
log.Information($"[QuestPreCheck] Character {characterName} already completed quest {questId} - SKIPPING");
return true;
}
bool completed = default(bool);
if (questDatabase.TryGetValue(characterName, out Dictionary<uint, bool> quests) && quests.TryGetValue(questId, out completed) && completed)
{
log.Information($"[QuestPreCheck] Character {characterName} already completed quest {questId} (from DB) - SKIPPING");
return true;
}
return false;
}
public bool? GetQuestStatus(string characterName, uint questId)
{
if (questDatabase.TryGetValue(characterName, out Dictionary<uint, bool> quests) && quests.TryGetValue(questId, out var isCompleted))
{
return isCompleted;
}
return null;
}
public List<uint> GetCompletedQuests(string characterName)
{
if (!questDatabase.TryGetValue(characterName, out Dictionary<uint, bool> quests))
{
return new List<uint>();
}
return (from kvp in quests
where kvp.Value
select kvp.Key).ToList();
}
public void MarkQuestCompleted(string characterName, uint questId)
{
if (!questDatabase.ContainsKey(characterName))
{
questDatabase[characterName] = new Dictionary<uint, bool>();
}
questDatabase[characterName][questId] = true;
SaveQuestDatabase();
log.Information($"[QuestPreCheck] Marked quest {questId} as completed for {characterName}");
}
public void ClearPreCheckResults()
{
preCheckResults.Clear();
log.Information("[QuestPreCheck] Pre-check results cleared");
}
public void Dispose()
{
SaveQuestDatabase();
log.Information("[QuestPreCheck] Service disposed");
}
}