Initial commit: MSQ Progress plugin v1.0.0
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user