317 lines
10 KiB
C#
317 lines
10 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 ClearCharacterData(string characterName)
|
|
{
|
|
if (questDatabase.ContainsKey(characterName))
|
|
{
|
|
int questCount = questDatabase[characterName].Count;
|
|
questDatabase.Remove(characterName);
|
|
SaveQuestDatabase();
|
|
log.Information($"[QuestPreCheck] Cleared {questCount} quests for {characterName}");
|
|
}
|
|
else
|
|
{
|
|
log.Information("[QuestPreCheck] No quest data found for " + characterName);
|
|
}
|
|
lastRefreshByCharacter.Remove(characterName);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
SaveQuestDatabase();
|
|
log.Information("[QuestPreCheck] Service disposed");
|
|
}
|
|
}
|