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; /// /// 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. /// public static class DutyUnlockMap { private static Dictionary? _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(); 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>(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(); 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; } } } } /// Export current CFC→Quest map to a JSON file (e.g. after loading from game). Use to regenerate DutyUnlockData.json. public static string? ExportToJsonFile(IDataManager dataManager, string filePath) { EnsureLoaded(dataManager); if (_cfcToQuest == null || _cfcToQuest.Count == 0) return null; try { var entries = new List(); 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; } /// Try to get a Quest row ID from a value (LazyRow/RowRef/ExcelRow). Handles Lumina 5 structs. private static uint ExtractQuestRowIdFromValue(object val) { var t = val.GetType(); // Row.RowId (e.g. LazyRow.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; } }