919b72051f
Co-authored-by: Cursor <cursoragent@cursor.com>
195 lines
6.8 KiB
C#
195 lines
6.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Text.Json;
|
|
using Dalamud.Plugin.Services;
|
|
using Lumina.Excel.Sheets;
|
|
|
|
namespace MSQProgress;
|
|
|
|
/// <summary>
|
|
/// Maps ContentFinderCondition RowId to the Quest RowId that unlocks that duty.
|
|
/// Loaded from embedded DutyUnlockData.json (fallback) and then filled from the game's ContentFinderCondition sheet (UnlockQuest column) when DataManager is available, so all duties are covered.
|
|
/// </summary>
|
|
public static class DutyUnlockMap
|
|
{
|
|
private static Dictionary<uint, uint>? _cfcToQuest;
|
|
private static bool _filledFromGame;
|
|
private static readonly object Lock = new();
|
|
|
|
public static bool TryGetUnlockQuest(uint contentFinderConditionRowId, out uint questRowId, IDataManager? dataManager = null)
|
|
{
|
|
EnsureLoaded(dataManager);
|
|
if (_cfcToQuest != null && _cfcToQuest.TryGetValue(contentFinderConditionRowId, out var q) && q != 0)
|
|
{
|
|
questRowId = q;
|
|
return true;
|
|
}
|
|
questRowId = 0;
|
|
return false;
|
|
}
|
|
|
|
private static void EnsureLoaded(IDataManager? dataManager = null)
|
|
{
|
|
lock (Lock)
|
|
{
|
|
if (_cfcToQuest == null)
|
|
{
|
|
_cfcToQuest = new Dictionary<uint, uint>();
|
|
try
|
|
{
|
|
var asm = Assembly.GetExecutingAssembly();
|
|
var name = asm.GetName().Name + ".DutyUnlockData.json";
|
|
using var stream = asm.GetManifestResourceStream(name);
|
|
if (stream != null)
|
|
{
|
|
using var reader = new StreamReader(stream);
|
|
var json = reader.ReadToEnd();
|
|
var entries = JsonSerializer.Deserialize<List<CfcQuestEntry>>(json);
|
|
if (entries != null)
|
|
{
|
|
foreach (var e in entries)
|
|
{
|
|
if (e.QuestRowId != 0)
|
|
_cfcToQuest[e.CfcRowId] = e.QuestRowId;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
if (!_filledFromGame && dataManager != null && _cfcToQuest != null)
|
|
{
|
|
_filledFromGame = true;
|
|
try
|
|
{
|
|
var sheet = dataManager.GetExcelSheet<ContentFinderCondition>();
|
|
if (sheet != null)
|
|
{
|
|
foreach (var row in sheet)
|
|
{
|
|
if (row.RowId == 0) continue;
|
|
var q = GetUnlockQuestFromRow(row);
|
|
if (q != 0)
|
|
_cfcToQuest[row.RowId] = q;
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
_filledFromGame = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Export current CFC→Quest map to a JSON file (e.g. after loading from game). Use to regenerate DutyUnlockData.json.</summary>
|
|
public static string? ExportToJsonFile(IDataManager dataManager, string filePath)
|
|
{
|
|
EnsureLoaded(dataManager);
|
|
if (_cfcToQuest == null || _cfcToQuest.Count == 0) return null;
|
|
try
|
|
{
|
|
var entries = new List<CfcQuestEntry>();
|
|
foreach (var kv in _cfcToQuest)
|
|
{
|
|
if (kv.Value != 0)
|
|
entries.Add(new CfcQuestEntry { cfc = kv.Key, q = kv.Value });
|
|
}
|
|
entries.Sort((a, b) => a.CfcRowId.CompareTo(b.CfcRowId));
|
|
var json = JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = false });
|
|
File.WriteAllText(filePath, json);
|
|
return filePath;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
internal static uint GetUnlockQuestFromRow(ContentFinderCondition row)
|
|
{
|
|
var rowType = row.GetType();
|
|
// Prefer known column names
|
|
foreach (var propName in new[] { "UnlockQuest", "UnlockCriteria" })
|
|
{
|
|
var prop = rowType.GetProperty(propName);
|
|
if (prop == null) continue;
|
|
try
|
|
{
|
|
var val = prop.GetValue(row);
|
|
if (val == null) continue;
|
|
var id = ExtractQuestRowIdFromValue(val);
|
|
if (id != 0) return id;
|
|
}
|
|
catch { /* ignore */ }
|
|
}
|
|
// Fallback: any property whose name suggests unlock/quest (Lumina may use different names)
|
|
foreach (var prop in rowType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
var name = prop.Name;
|
|
if (!name.Contains("Unlock", StringComparison.OrdinalIgnoreCase) && !name.Contains("Quest", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
try
|
|
{
|
|
var val = prop.GetValue(row);
|
|
if (val == null) continue;
|
|
var id = ExtractQuestRowIdFromValue(val);
|
|
if (id != 0) return id;
|
|
}
|
|
catch { /* ignore */ }
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>Try to get a Quest row ID from a value (LazyRow/RowRef/ExcelRow). Handles Lumina 5 structs.</summary>
|
|
private static uint ExtractQuestRowIdFromValue(object val)
|
|
{
|
|
var t = val.GetType();
|
|
// Row.RowId (e.g. LazyRow<Quest>.Row)
|
|
if (t.GetProperty("Row", BindingFlags.Public | BindingFlags.Instance) is PropertyInfo rowProp)
|
|
{
|
|
var inner = rowProp.GetValue(val);
|
|
if (inner != null)
|
|
{
|
|
var innerId = GetRowIdFromObject(inner);
|
|
if (innerId != 0) return innerId;
|
|
}
|
|
}
|
|
return GetRowIdFromObject(val);
|
|
}
|
|
|
|
private static uint GetRowIdFromObject(object obj)
|
|
{
|
|
if (obj == null) return 0;
|
|
var t = obj.GetType();
|
|
if (t.GetProperty("RowId", BindingFlags.Public | BindingFlags.Instance) is PropertyInfo idProp)
|
|
{
|
|
var v = idProp.GetValue(obj);
|
|
if (v is uint u && u != 0) return u;
|
|
if (v is int i && i > 0) return (uint)i;
|
|
}
|
|
var keyProp = t.GetProperty("Key", BindingFlags.Public | BindingFlags.Instance);
|
|
if (keyProp != null)
|
|
{
|
|
var v = keyProp.GetValue(obj);
|
|
if (v is uint u && u != 0) return u;
|
|
if (v is int i && i > 0) return (uint)i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private sealed class CfcQuestEntry
|
|
{
|
|
public uint cfc { get; set; }
|
|
public uint q { get; set; }
|
|
public uint CfcRowId => cfc;
|
|
public uint QuestRowId => q;
|
|
}
|
|
}
|