f72031ae60
Co-authored-by: Cursor <cursoragent@cursor.com>
591 lines
24 KiB
C#
591 lines
24 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using Dalamud.Game;
|
|
using Dalamud.Plugin.Services;
|
|
using Lumina.Excel.Sheets;
|
|
using Action = Lumina.Excel.Sheets.Action;
|
|
|
|
namespace ConfigurableCombo;
|
|
|
|
/// <summary>Job list filter: combat (DoW/DoM) or crafting & gathering (DoH/DoL).</summary>
|
|
public enum JobListKind
|
|
{
|
|
Combat,
|
|
CraftingGathering,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches job list and actions by category for the config.
|
|
/// Uses dynamic to tolerate different Lumina API versions.
|
|
/// </summary>
|
|
internal static class JobActionsHelper
|
|
{
|
|
private const uint ActionCategoryItem = 2;
|
|
/// <summary>Max Action RowId to scan (avoids relying on foreach which may skip rows in some Lumina versions).</summary>
|
|
private const uint MaxActionRowId = 60000u;
|
|
|
|
/// <summary>FFXIV ClassJob RowIds: 8-18 are DoH/DoL (CRP..FSH). 1-7 and 19+ are combat.</summary>
|
|
private static bool IsPvP(dynamic r)
|
|
{
|
|
try
|
|
{
|
|
if (r.IsPvP == null) return false;
|
|
if (r.IsPvP is bool b) return b;
|
|
return Convert.ToInt32(r.IsPvP) != 0;
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
private static uint GetActionCategoryRow(dynamic r)
|
|
{
|
|
try
|
|
{
|
|
dynamic cat;
|
|
try { cat = r.ActionCategory; } catch { return 0; }
|
|
try { return (uint)Convert.ToUInt32(cat.RowId); } catch { }
|
|
try { return (uint)Convert.ToUInt32(cat.Row); } catch { }
|
|
try { dynamic v = cat.Value; if (v != null) return (uint)Convert.ToUInt32(v.RowId); } catch { }
|
|
try { dynamic v = cat.Value; if (v != null) return (uint)Convert.ToUInt32(v.Row); } catch { }
|
|
try { dynamic v = cat.ValueNullable; if (v != null) return (uint)Convert.ToUInt32(v.RowId); } catch { }
|
|
}
|
|
catch { }
|
|
return 0;
|
|
}
|
|
|
|
private static uint GetRowId(dynamic r)
|
|
{
|
|
try { return (uint)(r.RowId ?? r.Key ?? 0); } catch { return 0; }
|
|
}
|
|
|
|
/// <summary>True if the name is a real display name (not #rowId fallback or _rsv_ placeholder).</summary>
|
|
private static bool HasProperName(string name, uint rowId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name)) return false;
|
|
if (name.Trim() == $"#{rowId}") return false;
|
|
if (name.Contains("_rsv_", StringComparison.OrdinalIgnoreCase)) return false;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>True if the name uses only English-friendly characters (ASCII printable + Latin-1 supplement). Filters out CJK and other scripts.</summary>
|
|
private static bool IsLikelyEnglish(string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name)) return false;
|
|
foreach (char c in name)
|
|
{
|
|
if (c >= 0x20 && c <= 0x7E) continue; // ASCII printable
|
|
if (c >= 0xA0 && c <= 0xFF) continue; // Latin-1 supplement (é, ñ, ü, etc.)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static string GetActionName(dynamic r, uint rowId)
|
|
{
|
|
try
|
|
{
|
|
// Support both reference-type Name and value-type (e.g. ReadOnlySeString)
|
|
object? nameObj = null;
|
|
try { nameObj = r.Name; } catch { }
|
|
if (nameObj != null)
|
|
{
|
|
var n = nameObj.ToString();
|
|
if (!string.IsNullOrWhiteSpace(n)) return n.Trim();
|
|
}
|
|
try { nameObj = r.Singular; } catch { }
|
|
if (nameObj != null)
|
|
{
|
|
var n = nameObj.ToString();
|
|
if (!string.IsNullOrWhiteSpace(n)) return n.Trim();
|
|
}
|
|
}
|
|
catch { }
|
|
return $"#{rowId}";
|
|
}
|
|
|
|
private static uint GetIconId(dynamic r)
|
|
{
|
|
try { return (uint)(r.Icon ?? 0); } catch { return 0; }
|
|
}
|
|
private const uint CraftingGatheringMin = 8;
|
|
private const uint CraftingGatheringMax = 18;
|
|
|
|
private static readonly Dictionary<uint, List<(uint ActionId, string Name, uint IconId)>> _jobActionsCache = new();
|
|
private static List<(uint JobId, string Name)>? _combatJobListCache;
|
|
private static List<(uint JobId, string Name)>? _craftingGatheringJobListCache;
|
|
private static readonly Dictionary<uint, string> _jobNameByRowId = new();
|
|
private static readonly Dictionary<uint, List<(uint ActionId, string Name, uint IconId)>> _pvpActionsCacheByJob = new();
|
|
private static readonly Dictionary<uint, List<(uint ActionId, string Name, uint IconId)>> _roleActionsCacheByJob = new();
|
|
private static List<(uint ActionId, string Name, uint IconId)>? _consumableActionsCache;
|
|
|
|
/// <summary>Jobs for the dropdown. Combat = DoW/DoM (RowId 1-7, 19+). CraftingGathering = DoH/DoL (RowId 8-18). Includes Unspecified. Cached per kind.</summary>
|
|
public static List<(uint JobId, string Name)> GetJobList(JobListKind kind)
|
|
{
|
|
if (kind == JobListKind.Combat && _combatJobListCache != null)
|
|
return _combatJobListCache;
|
|
if (kind == JobListKind.CraftingGathering && _craftingGatheringJobListCache != null)
|
|
return _craftingGatheringJobListCache;
|
|
|
|
var result = new List<(uint JobId, string Name)> { (0, "Unspecified") };
|
|
try
|
|
{
|
|
var sheet = Service.DataManager.GetExcelSheet<Lumina.Excel.Sheets.ClassJob>(ClientLanguage.English);
|
|
if (sheet != null)
|
|
{
|
|
foreach (var row in sheet)
|
|
{
|
|
try
|
|
{
|
|
dynamic r = row;
|
|
uint rowId = (uint)r.RowId;
|
|
if (rowId == 0) continue;
|
|
if (kind == JobListKind.Combat && (rowId == 43 || rowId == 44 || rowId == 45)) continue;
|
|
bool isCraftGather = rowId >= CraftingGatheringMin && rowId <= CraftingGatheringMax;
|
|
if (kind == JobListKind.Combat && isCraftGather) continue;
|
|
if (kind == JobListKind.CraftingGathering && !isCraftGather) continue;
|
|
string name = r.Name?.ToString() ?? r.NameEnglish?.ToString() ?? r.Abbreviation?.ToString() ?? $"#{rowId}";
|
|
if (string.IsNullOrWhiteSpace(name)) name = $"#{rowId}";
|
|
name = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(name.Trim().ToLowerInvariant());
|
|
if (!HasProperName(name, rowId) || !IsLikelyEnglish(name)) continue;
|
|
result.Add((rowId, name));
|
|
_jobNameByRowId[rowId] = name;
|
|
}
|
|
catch { }
|
|
}
|
|
result = result.OrderBy(x => x.Name).ToList();
|
|
result.RemoveAll(x => x.JobId == 0);
|
|
result.Insert(0, (0, "Unspecified"));
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
if (kind == JobListKind.Combat)
|
|
_combatJobListCache = result;
|
|
else
|
|
_craftingGatheringJobListCache = result;
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Job-related PvE actions for the given job. Cached per job.</summary>
|
|
public static List<(uint ActionId, string Name, uint IconId)> GetActionsForJob(uint jobId)
|
|
{
|
|
if (jobId == 0) return new List<(uint, string, uint)>();
|
|
lock (_jobActionsCache)
|
|
{
|
|
if (_jobActionsCache.TryGetValue(jobId, out var cached))
|
|
return cached;
|
|
}
|
|
var list = BuildJobActions(jobId);
|
|
lock (_jobActionsCache)
|
|
{
|
|
_jobActionsCache[jobId] = list;
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/// <summary>PvP actions for the given job (or all PvP if jobId 0). Cached per job.</summary>
|
|
public static List<(uint ActionId, string Name, uint IconId)> GetPvPActions(uint jobId)
|
|
{
|
|
lock (_pvpActionsCacheByJob)
|
|
{
|
|
if (_pvpActionsCacheByJob.TryGetValue(jobId, out var cached))
|
|
return cached;
|
|
}
|
|
var list = BuildPvPActions(jobId);
|
|
lock (_pvpActionsCacheByJob)
|
|
{
|
|
_pvpActionsCacheByJob[jobId] = list;
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/// <summary>Role actions for the given job's category (shared actions like Second Wind, Leg Sweep). Cached per job.</summary>
|
|
public static List<(uint ActionId, string Name, uint IconId)> GetRoleActions(uint jobId)
|
|
{
|
|
lock (_roleActionsCacheByJob)
|
|
{
|
|
if (_roleActionsCacheByJob.TryGetValue(jobId, out var cached))
|
|
return cached;
|
|
}
|
|
var list = BuildRoleActions(jobId);
|
|
lock (_roleActionsCacheByJob)
|
|
{
|
|
_roleActionsCacheByJob[jobId] = list;
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/// <summary>Consumable items (ActionCategory Item). Cached.</summary>
|
|
public static List<(uint ActionId, string Name, uint IconId)> GetConsumableActions()
|
|
{
|
|
if (_consumableActionsCache != null)
|
|
return _consumableActionsCache;
|
|
_consumableActionsCache = BuildConsumableActions();
|
|
return _consumableActionsCache;
|
|
}
|
|
|
|
private static List<(uint ActionId, string Name, uint IconId)> BuildPvPActions(uint jobId)
|
|
{
|
|
var result = new List<(uint, string, uint)>();
|
|
try
|
|
{
|
|
var sheet = Service.DataManager.GetExcelSheet<Action>(ClientLanguage.English);
|
|
if (sheet == null)
|
|
{
|
|
Service.PluginLog.Warning("[ConfigurableCombo] Action sheet is null (PvP).");
|
|
return result;
|
|
}
|
|
byte jobByte = jobId == 0 ? (byte)0 : (byte)Math.Clamp(jobId, 0, 255);
|
|
foreach (var row in sheet)
|
|
{
|
|
try
|
|
{
|
|
dynamic r = row;
|
|
if (!IsPvP(r)) continue;
|
|
uint rowId = GetRowId(r);
|
|
if (rowId == 0) continue;
|
|
if (jobId != 0 && !IsActionForJob(r, jobId, jobByte))
|
|
continue;
|
|
var name = GetActionName(r, rowId);
|
|
if (!HasProperName(name, rowId) || !IsLikelyEnglish(name)) continue;
|
|
result.Add((rowId, name, GetIconId(r)));
|
|
}
|
|
catch { }
|
|
}
|
|
result = result.OrderBy(x => x.Item2).ToList();
|
|
if (result.Count == 0)
|
|
Service.PluginLog.Debug("[ConfigurableCombo] BuildPvPActions: 0 actions for jobId={JobId}.", jobId);
|
|
}
|
|
catch (Exception ex) { Service.PluginLog.Warning(ex, "[ConfigurableCombo] BuildPvPActions failed."); }
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Known PvE role action RowIds (from game data / WrathCombo). Displayed regardless of selected job.</summary>
|
|
private static readonly uint[] KnownRoleActionIds =
|
|
{
|
|
197, 198, 200, 201, 203, 204, 206, 207,
|
|
7531, 7533, 7535, 7537, 7538, 7540,
|
|
7541, 7542, 7546, 7548, 7549,
|
|
7551, 7553, 7554, 7557,
|
|
7559, 7560, 7561, 7562, 7568, 7571,
|
|
7863,
|
|
16560, 25880,
|
|
4238, 4239,
|
|
};
|
|
|
|
/// <summary>Role actions: built from known role action IDs so they always display (Tank, Healer, Melee, PhysRanged, Caster, Magic).</summary>
|
|
private static List<(uint ActionId, string Name, uint IconId)> BuildRoleActions(uint jobId)
|
|
{
|
|
var result = new List<(uint, string, uint)>();
|
|
try
|
|
{
|
|
var sheet = Service.DataManager.GetExcelSheet<Action>(ClientLanguage.English);
|
|
if (sheet == null)
|
|
{
|
|
Service.PluginLog.Warning("[ConfigurableCombo] Action sheet is null (Role Actions).");
|
|
return result;
|
|
}
|
|
foreach (uint id in KnownRoleActionIds)
|
|
{
|
|
try
|
|
{
|
|
if (!sheet.TryGetRow(id, out var row)) continue;
|
|
dynamic r = row;
|
|
if (IsPvP(r)) continue;
|
|
var name = GetActionName(r, id);
|
|
if (!HasProperName(name, id) || !IsLikelyEnglish(name)) continue;
|
|
result.Add((id, name, GetIconId(r)));
|
|
}
|
|
catch { }
|
|
}
|
|
result = result.OrderBy(x => x.Item2).ToList();
|
|
}
|
|
catch (Exception ex) { Service.PluginLog.Warning(ex, "[ConfigurableCombo] BuildRoleActions failed."); }
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Get action's ClassJobCategory RowId via reflection (same pattern as GetJobCategoryRowId so Role Actions work).</summary>
|
|
private static uint GetActionClassJobCategoryRowId(object? actionRow)
|
|
{
|
|
if (actionRow == null) return 0;
|
|
var cat = GetRowRefPropertyValue(actionRow, "ClassJobCategory");
|
|
return GetRowRefRowId(cat);
|
|
}
|
|
|
|
/// <summary>Get a RowRef-like property value from a row object, trying exact and case-insensitive property names.</summary>
|
|
private static object? GetRowRefPropertyValue(object? row, string propertyName)
|
|
{
|
|
if (row == null) return null;
|
|
try
|
|
{
|
|
var type = row.GetType();
|
|
var p = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
|
|
if (p != null) return p.GetValue(row);
|
|
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
if (string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase))
|
|
return prop.GetValue(row);
|
|
}
|
|
}
|
|
catch { }
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Consumables: usable items from the Item sheet (ItemAction set). IDs are Item RowIds; game uses these for item hotbar slots.</summary>
|
|
private static List<(uint ActionId, string Name, uint IconId)> BuildConsumableActions()
|
|
{
|
|
var result = new List<(uint, string, uint)>();
|
|
try
|
|
{
|
|
// Build from Item sheet: items with a use action (ItemAction valid) are consumables
|
|
var itemSheet = Service.DataManager.GetExcelSheet<Lumina.Excel.Sheets.Item>(ClientLanguage.English);
|
|
if (itemSheet == null)
|
|
{
|
|
Service.PluginLog.Warning("[ConfigurableCombo] Item sheet is null (Consumable).");
|
|
return result;
|
|
}
|
|
foreach (var row in itemSheet)
|
|
{
|
|
try
|
|
{
|
|
dynamic r = row;
|
|
uint rowId = GetRowId(r);
|
|
if (rowId == 0) continue;
|
|
if (!ItemHasUseAction(r)) continue;
|
|
string name = GetItemName(r, rowId);
|
|
if (!HasProperName(name, rowId) || !IsLikelyEnglish(name)) continue;
|
|
result.Add((rowId, name, GetIconId(r)));
|
|
}
|
|
catch { }
|
|
}
|
|
result = result.OrderBy(x => x.Item2).ToList();
|
|
if (result.Count == 0)
|
|
Service.PluginLog.Debug("[ConfigurableCombo] BuildConsumableActions: 0 usable items found.");
|
|
}
|
|
catch (Exception ex) { Service.PluginLog.Warning(ex, "[ConfigurableCombo] BuildConsumableActions failed."); }
|
|
return result;
|
|
}
|
|
|
|
private static bool ItemHasUseAction(dynamic r)
|
|
{
|
|
try
|
|
{
|
|
dynamic ia;
|
|
try { ia = r.ItemAction; } catch { return false; }
|
|
try { if (Convert.ToUInt32(ia.RowId) != 0) return true; } catch { }
|
|
try { if (Convert.ToUInt32(ia.Row) != 0) return true; } catch { }
|
|
try { if (ia.IsValid == true) return true; } catch { }
|
|
}
|
|
catch { }
|
|
return false;
|
|
}
|
|
|
|
private static string GetItemName(dynamic r, uint rowId)
|
|
{
|
|
try
|
|
{
|
|
object? nameObj = null;
|
|
try { nameObj = r.Name; } catch { }
|
|
if (nameObj != null)
|
|
{
|
|
var n = nameObj.ToString();
|
|
if (!string.IsNullOrWhiteSpace(n)) return n.Trim();
|
|
}
|
|
try { nameObj = r.Singular; } catch { }
|
|
if (nameObj != null)
|
|
{
|
|
var n = nameObj.ToString();
|
|
if (!string.IsNullOrWhiteSpace(n)) return n.Trim();
|
|
}
|
|
}
|
|
catch { }
|
|
return $"#{rowId}";
|
|
}
|
|
|
|
/// <summary>True if the action belongs to the given job (ClassJob match, parent class match, or ClassJobCategory). Uses jobCategoryRowId as fallback when IsJobInCategory is unavailable.</summary>
|
|
private static bool IsActionForJob(dynamic r, uint jobId, byte jobByte, uint? jobCategoryRowId = null, uint? parentClassJobId = null)
|
|
{
|
|
try
|
|
{
|
|
uint actionJob = GetClassJobRowId(r);
|
|
if (actionJob == jobId) return true;
|
|
if (parentClassJobId != null && parentClassJobId.Value != 0 && actionJob == parentClassJobId.Value) return true;
|
|
dynamic? cat = null;
|
|
try { cat = r.ClassJobCategory; } catch { }
|
|
if (cat == null) return false;
|
|
uint catRowId = 0;
|
|
try { catRowId = (uint)Convert.ToUInt32(cat.RowId); } catch { }
|
|
try { if (catRowId == 0) catRowId = (uint)Convert.ToUInt32(cat.Row); } catch { }
|
|
if (catRowId == 1) return false;
|
|
dynamic? val = null;
|
|
try { val = cat.Value; } catch { }
|
|
try { if (val == null) val = cat.ValueNullable; } catch { }
|
|
if (val != null)
|
|
{
|
|
try { return (bool)val.IsJobInCategory(jobByte); } catch { }
|
|
try { return (bool)val.IsJobInCategory((byte)jobId); } catch { }
|
|
}
|
|
// Fallback: include if action's category matches the job's category (role actions)
|
|
if (jobCategoryRowId != null && jobCategoryRowId.Value != 0 && catRowId == jobCategoryRowId.Value)
|
|
return true;
|
|
}
|
|
catch { }
|
|
return false;
|
|
}
|
|
|
|
/// <summary>Gets the ClassJobCategory RowId for the given job from the ClassJob sheet (for role-action fallback).</summary>
|
|
private static uint GetJobCategoryRowId(uint jobId)
|
|
{
|
|
if (jobId == 0) return 0;
|
|
try
|
|
{
|
|
var sheet = Service.DataManager.GetExcelSheet<Lumina.Excel.Sheets.ClassJob>(ClientLanguage.English);
|
|
if (sheet == null) return 0;
|
|
if (!sheet.TryGetRow(jobId, out var row)) return 0;
|
|
var cat = GetRowRefPropertyValue((object)row, "ClassJobCategory");
|
|
return GetRowRefRowId(cat);
|
|
}
|
|
catch { }
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>Gets the parent class RowId for a job (e.g. NIN 30 -> ROG 29). Actions like Spinning Edge are tied to the class, not the job.</summary>
|
|
private static uint GetParentClassJobId(uint jobId)
|
|
{
|
|
if (jobId == 0) return 0;
|
|
try
|
|
{
|
|
var sheet = Service.DataManager.GetExcelSheet<Lumina.Excel.Sheets.ClassJob>(ClientLanguage.English);
|
|
if (sheet == null) return 0;
|
|
if (!sheet.TryGetRow(jobId, out var row)) return 0;
|
|
object boxed = row;
|
|
var type = boxed.GetType();
|
|
var parentProp = type.GetProperty("ClassJobParent", BindingFlags.Public | BindingFlags.Instance);
|
|
if (parentProp == null) return 0;
|
|
var parent = parentProp.GetValue(boxed);
|
|
if (parent == null) return 0;
|
|
try { return (uint)Convert.ToUInt32(((dynamic)parent).RowId); } catch { }
|
|
try { return (uint)Convert.ToUInt32(((dynamic)parent).Row); } catch { }
|
|
return GetRowRefRowId(parent);
|
|
}
|
|
catch { }
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>Get a uint row id from any RowRef-like object (Lumina 5 struct) via reflection when dynamic fails.</summary>
|
|
private static uint GetRowRefRowId(object? rowRef)
|
|
{
|
|
if (rowRef == null) return 0;
|
|
try
|
|
{
|
|
var t = rowRef.GetType();
|
|
foreach (var name in new[] { "RowId", "Row", "Key" })
|
|
{
|
|
var p = t.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
|
|
if (p == null) continue;
|
|
var v = p.GetValue(rowRef);
|
|
if (v == null) continue;
|
|
if (v is uint u) return u;
|
|
if (v is int i && i >= 0) return (uint)i;
|
|
try { return Convert.ToUInt32(v); } catch { }
|
|
}
|
|
foreach (var f in t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
|
|
{
|
|
if (f.FieldType != typeof(uint) && f.FieldType != typeof(int)) continue;
|
|
var v = f.GetValue(rowRef);
|
|
if (v == null) continue;
|
|
try { var u = Convert.ToUInt32(v); if (u != 0) return u; } catch { }
|
|
}
|
|
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
if (p.PropertyType != typeof(uint) && p.PropertyType != typeof(int)) continue;
|
|
try
|
|
{
|
|
var v = p.GetValue(rowRef);
|
|
if (v == null) continue;
|
|
var u = Convert.ToUInt32(v);
|
|
if (u != 0) return u;
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
catch { }
|
|
return 0;
|
|
}
|
|
|
|
private static uint GetClassJobRowId(dynamic r)
|
|
{
|
|
try
|
|
{
|
|
dynamic cj;
|
|
try { cj = r.ClassJob; } catch { return 0; }
|
|
try { return (uint)Convert.ToUInt32(cj.RowId); } catch { }
|
|
try { return (uint)Convert.ToUInt32(cj.Row); } catch { }
|
|
try { return (uint)Convert.ToUInt32(cj.Key); } catch { }
|
|
try { dynamic v = cj.Value; if (v != null) return (uint)Convert.ToUInt32(v.RowId); } catch { }
|
|
try { dynamic v = cj.Value; if (v != null) return (uint)Convert.ToUInt32(v.Row); } catch { }
|
|
try { dynamic v = cj.ValueNullable; if (v != null) return (uint)Convert.ToUInt32(v.RowId); } catch { }
|
|
return GetRowRefRowId((object?)cj);
|
|
}
|
|
catch { }
|
|
return 0;
|
|
}
|
|
|
|
private static List<(uint ActionId, string Name, uint IconId)> BuildJobActions(uint jobId)
|
|
{
|
|
var result = new List<(uint, string, uint)>();
|
|
try
|
|
{
|
|
var sheet = Service.DataManager.GetExcelSheet<Action>(ClientLanguage.English);
|
|
if (sheet == null)
|
|
{
|
|
Service.PluginLog.Warning("[ConfigurableCombo] Action sheet is null (Job).");
|
|
return result;
|
|
}
|
|
|
|
byte jobByte = (byte)Math.Clamp(jobId, 0, 255);
|
|
uint jobCategoryRowId = GetJobCategoryRowId(jobId);
|
|
uint parentClassJobId = GetParentClassJobId(jobId);
|
|
for (uint id = 1; id <= MaxActionRowId; id++)
|
|
{
|
|
try
|
|
{
|
|
if (!sheet.TryGetRow(id, out var row)) continue;
|
|
dynamic r = row;
|
|
if (IsPvP(r)) continue;
|
|
int classJobLevel = 0;
|
|
try { classJobLevel = (int)(r.ClassJobLevel ?? 0); } catch { }
|
|
|
|
bool inCategory = IsActionForJob(r, jobId, jobByte, jobCategoryRowId, parentClassJobId);
|
|
if (classJobLevel <= 0 && !inCategory) continue;
|
|
if (!inCategory) continue;
|
|
|
|
var name = GetActionName(r, id);
|
|
if (!HasProperName(name, id) || !IsLikelyEnglish(name)) continue;
|
|
result.Add((id, name, GetIconId(r)));
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
result = result.OrderBy(x => x.Item2).ToList();
|
|
if (result.Count == 0)
|
|
Service.PluginLog.Debug("[ConfigurableCombo] BuildJobActions: 0 actions for jobId={JobId}. Select a job and ensure game data is loaded.", jobId);
|
|
}
|
|
catch (Exception ex) { Service.PluginLog.Warning(ex, "[ConfigurableCombo] BuildJobActions failed."); }
|
|
|
|
return result;
|
|
}
|
|
|
|
public static string GetJobName(uint jobId)
|
|
{
|
|
if (jobId == 0) return "Unspecified";
|
|
if (_jobNameByRowId.TryGetValue(jobId, out var name) && !string.IsNullOrEmpty(name))
|
|
return name;
|
|
GetJobList(JobListKind.Combat);
|
|
GetJobList(JobListKind.CraftingGathering);
|
|
return _jobNameByRowId.TryGetValue(jobId, out name) ? name : $"#{jobId}";
|
|
}
|
|
}
|