f72031ae60
Co-authored-by: Cursor <cursoragent@cursor.com>
409 lines
16 KiB
C#
409 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Game;
|
|
using Dalamud.Interface.Textures;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using Dalamud.Interface.Windowing;
|
|
using Lumina.Excel.Sheets;
|
|
using Action = Lumina.Excel.Sheets.Action;
|
|
|
|
namespace ConfigurableCombo;
|
|
|
|
public class ConfigWindow : Window
|
|
{
|
|
private const float ActionIconSize = 24f;
|
|
|
|
private uint _selectedJobId;
|
|
private JobListKind _jobListKind = JobListKind.Combat;
|
|
private string _newComboName = "New Combo";
|
|
private UserComboDefinition? _editingCombo;
|
|
private int _editingActionIndex = -1;
|
|
private string _editingActionIdInput = "";
|
|
private UserComboDefinition? _addFromJobTargetCombo;
|
|
private string _jobActionFilter = "";
|
|
private bool _windowWasOpen;
|
|
|
|
public ConfigWindow() : base("ConfigurableCombo")
|
|
{
|
|
Size = new Vector2(500, 450);
|
|
SizeCondition = ImGuiCond.FirstUseEver;
|
|
}
|
|
|
|
/// <summary>Set job dropdown to player's current job if it's in the current job-type list; otherwise keep selection if valid or Unspecified.</summary>
|
|
private void SyncJobDropdownToPlayerOrSelection()
|
|
{
|
|
var list = JobActionsHelper.GetJobList(_jobListKind);
|
|
var playerJobId = Service.ClientState.LocalPlayer?.ClassJob.RowId ?? 0u;
|
|
if (playerJobId != 0 && list.Any(x => x.JobId == playerJobId))
|
|
_selectedJobId = playerJobId;
|
|
else if (list.All(x => x.JobId != _selectedJobId))
|
|
_selectedJobId = 0;
|
|
}
|
|
|
|
public override void Draw()
|
|
{
|
|
if (!Service.Configuration.EnablePlugin)
|
|
{
|
|
ImGui.TextColored(new Vector4(1, 0.4f, 0.4f, 1), "Plugin is disabled. Enable below to use combo replacement.");
|
|
}
|
|
|
|
bool enablePlugin = Service.Configuration.EnablePlugin;
|
|
if (ImGui.Checkbox("Enable plugin", ref enablePlugin))
|
|
{
|
|
Service.Configuration.EnablePlugin = enablePlugin;
|
|
Service.Configuration.Save();
|
|
}
|
|
ImGui.SameLine();
|
|
ImGui.TextColored(new Vector4(0.6f, 0.7f, 0.8f, 1f), "Tip: Use /ccombo to open this window anytime.");
|
|
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
// When window just opened, select the player's current job so their combos are shown by default
|
|
if (IsOpen && !_windowWasOpen)
|
|
{
|
|
_windowWasOpen = true;
|
|
var player = Service.ClientState.LocalPlayer;
|
|
var currentJobId = player?.ClassJob.RowId ?? 0u;
|
|
if (currentJobId != 0)
|
|
{
|
|
var combatList = JobActionsHelper.GetJobList(JobListKind.Combat);
|
|
var craftList = JobActionsHelper.GetJobList(JobListKind.CraftingGathering);
|
|
if (combatList.Any(x => x.JobId == currentJobId))
|
|
{
|
|
_jobListKind = JobListKind.Combat;
|
|
_selectedJobId = currentJobId;
|
|
}
|
|
else if (craftList.Any(x => x.JobId == currentJobId))
|
|
{
|
|
_jobListKind = JobListKind.CraftingGathering;
|
|
_selectedJobId = currentJobId;
|
|
}
|
|
}
|
|
}
|
|
else if (!IsOpen)
|
|
{
|
|
_windowWasOpen = false;
|
|
}
|
|
|
|
// Job type toggle: Combat vs Crafting & Gathering
|
|
ImGui.Text("Job type:");
|
|
ImGui.SameLine();
|
|
bool showCombatJobs = _jobListKind == JobListKind.Combat;
|
|
if (ImGui.Checkbox("Combat (DoW/DoM)", ref showCombatJobs))
|
|
{
|
|
_jobListKind = showCombatJobs ? JobListKind.Combat : JobListKind.CraftingGathering;
|
|
SyncJobDropdownToPlayerOrSelection();
|
|
}
|
|
ImGui.SameLine();
|
|
bool showCraftGather = _jobListKind == JobListKind.CraftingGathering;
|
|
if (ImGui.Checkbox("Crafting & Gathering (DoH/DoL)", ref showCraftGather))
|
|
{
|
|
_jobListKind = showCraftGather ? JobListKind.CraftingGathering : JobListKind.Combat;
|
|
SyncJobDropdownToPlayerOrSelection();
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Switch between battle jobs and crafters/gatherers. Combos are per job.");
|
|
|
|
ImGui.Text("Job:");
|
|
ImGui.SameLine();
|
|
var jobList = JobActionsHelper.GetJobList(_jobListKind);
|
|
var currentJobIndex = jobList.FindIndex(x => x.JobId == _selectedJobId);
|
|
if (currentJobIndex < 0) currentJobIndex = 0;
|
|
ImGui.SetNextItemWidth(180);
|
|
if (ImGui.BeginCombo("##job", jobList[currentJobIndex].Name))
|
|
{
|
|
for (int j = 0; j < jobList.Count; j++)
|
|
{
|
|
var (jobId, name) = jobList[j];
|
|
if (ImGui.Selectable(name, _selectedJobId == jobId))
|
|
{
|
|
_selectedJobId = jobId;
|
|
}
|
|
}
|
|
ImGui.EndCombo();
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Combos below are for this job. New combos will be saved to this job.");
|
|
|
|
ImGui.Spacing();
|
|
ImGui.Text("Define action sequences. Put the first action on your hotbar; each press advances the sequence.");
|
|
ImGui.TextColored(new Vector4(0.7f, 0.8f, 1f, 1f),
|
|
"Sequence resets to step 1 after the last step or after any other action.");
|
|
ImGui.Spacing();
|
|
ImGui.TextWrapped("Example (NIN 1-2 with 3a/3b on separate keys): Add a combo with actions 2240, 2242 (Spinning Edge, Gust Slash). Put 2240 on one hotbar slot. Use that slot for 1->2; put Aeolian Edge (2255) and Armor Crush (3563) on other keybinds.");
|
|
ImGui.Spacing();
|
|
|
|
// Add new combo (for selected job)
|
|
ImGui.SetNextItemWidth(180);
|
|
ImGui.InputTextWithHint("##name", "Combo name", ref _newComboName, 64);
|
|
ImGui.SameLine();
|
|
if (ImGui.Button("Add combo"))
|
|
{
|
|
Service.Configuration.UserCombos.Add(new UserComboDefinition { Name = _newComboName, JobId = _selectedJobId });
|
|
Service.Configuration.Save();
|
|
_newComboName = "New Combo";
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
ImGui.Separator();
|
|
ImGui.Spacing();
|
|
|
|
// List combos for selected job only
|
|
for (int i = 0; i < Service.Configuration.UserCombos.Count; i++)
|
|
{
|
|
var combo = Service.Configuration.UserCombos[i];
|
|
if (combo.JobId != _selectedJobId) continue;
|
|
DrawCombo(combo, i);
|
|
}
|
|
}
|
|
|
|
private void DrawCombo(UserComboDefinition combo, int index)
|
|
{
|
|
using (var id = ImRaii.PushId(index))
|
|
{
|
|
bool enabled = combo.Enabled;
|
|
if (ImGui.Checkbox("##enabled", ref enabled))
|
|
{
|
|
combo.Enabled = enabled;
|
|
Service.Configuration.Save();
|
|
}
|
|
ImGui.SameLine();
|
|
|
|
bool open = ImGui.TreeNodeEx($"{combo.Name} ({combo.ActionIds.Count} actions)", ImGuiTreeNodeFlags.DefaultOpen);
|
|
ImGui.SameLine(ImGui.GetContentRegionAvail().X - 80);
|
|
if (ImGui.SmallButton("Delete"))
|
|
{
|
|
Service.Configuration.UserCombos.RemoveAt(index);
|
|
Service.Configuration.Save();
|
|
return;
|
|
}
|
|
|
|
if (open)
|
|
{
|
|
ImGui.Indent();
|
|
|
|
// Name
|
|
string name = combo.Name;
|
|
if (ImGui.InputText("Name", ref name, 64))
|
|
{
|
|
combo.Name = name;
|
|
Service.Configuration.Save();
|
|
}
|
|
|
|
// Action list
|
|
ImGui.Text("Action sequence (first = hotbar slot; order: 1, 2, 3a/3b on other keys...)");
|
|
ImGui.TextColored(new Vector4(0.9f, 0.85f, 0.4f, 1f), "First action must be the combo starter (e.g. Spinning Edge for NIN), or the icon/state won't advance.");
|
|
for (int a = 0; a < combo.ActionIds.Count; a++)
|
|
{
|
|
uint aid = combo.ActionIds[a];
|
|
string label = GetActionName(aid);
|
|
if (ImGui.SmallButton($"{label} (##{a})"))
|
|
{
|
|
_editingCombo = combo;
|
|
_editingActionIndex = a;
|
|
_editingActionIdInput = aid.ToString();
|
|
}
|
|
ImGui.SameLine();
|
|
if (ImGui.SmallButton($"X##del{a}"))
|
|
{
|
|
combo.ActionIds.RemoveAt(a);
|
|
Service.Configuration.Save();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_editingCombo == combo && _editingActionIndex >= 0)
|
|
{
|
|
ImGui.SetNextItemWidth(120);
|
|
if (ImGui.InputText("Action ID", ref _editingActionIdInput, 16, ImGuiInputTextFlags.CharsDecimal))
|
|
{ }
|
|
ImGui.SameLine();
|
|
if (ImGui.Button("OK"))
|
|
{
|
|
if (uint.TryParse(_editingActionIdInput, out uint newId))
|
|
{
|
|
if (_editingActionIndex < combo.ActionIds.Count)
|
|
combo.ActionIds[_editingActionIndex] = newId;
|
|
else
|
|
combo.ActionIds.Add(newId);
|
|
Service.Configuration.Save();
|
|
}
|
|
_editingCombo = null;
|
|
_editingActionIndex = -1;
|
|
}
|
|
ImGui.SameLine();
|
|
if (ImGui.Button("Cancel"))
|
|
{
|
|
_editingCombo = null;
|
|
_editingActionIndex = -1;
|
|
}
|
|
}
|
|
|
|
ImGui.Spacing();
|
|
ImGui.Text("Add action:");
|
|
ImGui.SameLine();
|
|
if (ImGui.Button("Action browser..."))
|
|
{
|
|
_addFromJobTargetCombo = combo;
|
|
}
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Browse Job-Related, PvP, or Pet/Other actions. Click an action to add to sequence.");
|
|
ImGui.SameLine();
|
|
if (ImGui.Button("Add by ID"))
|
|
{
|
|
_editingCombo = combo;
|
|
_editingActionIndex = combo.ActionIds.Count;
|
|
_editingActionIdInput = "";
|
|
}
|
|
|
|
if (_addFromJobTargetCombo == combo)
|
|
{
|
|
DrawActionBrowser(combo);
|
|
}
|
|
|
|
ImGui.Unindent();
|
|
ImGui.TreePop();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawActionBrowser(UserComboDefinition targetCombo)
|
|
{
|
|
ImGui.Spacing();
|
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f))
|
|
using (ImRaii.Child("ActionBrowser", new Vector2(-1, 340), true))
|
|
{
|
|
ImGui.TextColored(new Vector4(0.7f, 0.9f, 1f, 1f), "Action browser — select a category, then click an action to add.");
|
|
ImGui.SameLine();
|
|
if (ImGui.SmallButton("Close"))
|
|
{
|
|
_addFromJobTargetCombo = null;
|
|
return;
|
|
}
|
|
ImGui.SetNextItemWidth(220f);
|
|
ImGui.InputTextWithHint("##filter", "Filter by name or ID...", ref _jobActionFilter, 64);
|
|
ImGui.Spacing();
|
|
|
|
var filter = _jobActionFilter.AsSpan().Trim().ToString();
|
|
bool hasFilter = !string.IsNullOrEmpty(filter);
|
|
|
|
if (ImGui.BeginTabBar("ActionBrowserTabs"))
|
|
{
|
|
if (ImGui.BeginTabItem("Job-Related"))
|
|
{
|
|
var actions = JobActionsHelper.GetActionsForJob(_selectedJobId);
|
|
DrawActionTable(actions, targetCombo, filter, hasFilter);
|
|
ImGui.EndTabItem();
|
|
}
|
|
if (ImGui.BeginTabItem("PvP Actions"))
|
|
{
|
|
var actions = JobActionsHelper.GetPvPActions(_selectedJobId);
|
|
DrawActionTable(actions, targetCombo, filter, hasFilter);
|
|
ImGui.EndTabItem();
|
|
}
|
|
if (ImGui.BeginTabItem("Role Actions"))
|
|
{
|
|
var actions = JobActionsHelper.GetRoleActions(_selectedJobId);
|
|
DrawActionTable(actions, targetCombo, filter, hasFilter);
|
|
ImGui.EndTabItem();
|
|
}
|
|
if (ImGui.BeginTabItem("Consumable Items"))
|
|
{
|
|
var actions = JobActionsHelper.GetConsumableActions();
|
|
DrawActionTable(actions, targetCombo, filter, hasFilter);
|
|
ImGui.EndTabItem();
|
|
}
|
|
ImGui.EndTabBar();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawActionTable(
|
|
List<(uint ActionId, string Name, uint IconId)> actions,
|
|
UserComboDefinition targetCombo,
|
|
string filter,
|
|
bool hasFilter)
|
|
{
|
|
if (actions.Count == 0)
|
|
{
|
|
ImGui.TextDisabled("No actions in this category.");
|
|
return;
|
|
}
|
|
if (ImGui.BeginTable("ActionBrowserTable", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingStretchProp, new Vector2(-1, -1)))
|
|
{
|
|
ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthFixed, ActionIconSize + 8);
|
|
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch);
|
|
ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed, 48);
|
|
ImGui.TableHeadersRow();
|
|
|
|
foreach (var (actionId, name, iconId) in actions)
|
|
{
|
|
if (hasFilter)
|
|
{
|
|
if (name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0 &&
|
|
actionId.ToString().IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
|
|
continue;
|
|
}
|
|
|
|
ImGui.TableNextRow();
|
|
ImGui.TableNextColumn();
|
|
if (iconId > 0)
|
|
{
|
|
try
|
|
{
|
|
var tex = Service.TextureProvider.GetFromGameIcon(new GameIconLookup(iconId));
|
|
var wrap = tex.GetWrapOrEmpty();
|
|
if (wrap != null)
|
|
ImGui.Image(wrap.Handle, new Vector2(ActionIconSize, ActionIconSize));
|
|
}
|
|
catch { }
|
|
}
|
|
ImGui.TableNextColumn();
|
|
if (ImGui.Selectable($"{name}##{actionId}", false))
|
|
{
|
|
targetCombo.ActionIds.Add(actionId);
|
|
Service.Configuration.Save();
|
|
_addFromJobTargetCombo = null;
|
|
ImGui.EndTable();
|
|
return;
|
|
}
|
|
ImGui.TableNextColumn();
|
|
ImGui.TextUnformatted(actionId.ToString());
|
|
}
|
|
ImGui.EndTable();
|
|
}
|
|
}
|
|
|
|
private static string GetActionName(uint actionId)
|
|
{
|
|
if (actionId == 0) return "0";
|
|
try
|
|
{
|
|
var sheet = Service.DataManager.GetExcelSheet<Action>(ClientLanguage.English);
|
|
var row = sheet?.GetRow(actionId);
|
|
if (row != null)
|
|
{
|
|
var nameStr = (row as dynamic)?.Name?.ToString() ?? (row as dynamic)?.Singular?.ToString();
|
|
if (!string.IsNullOrEmpty(nameStr))
|
|
return $"{nameStr} ({actionId})";
|
|
}
|
|
// Fallback: may be an item ID (e.g. from Consumable Items tab)
|
|
var itemSheet = Service.DataManager.GetExcelSheet<Lumina.Excel.Sheets.Item>(ClientLanguage.English);
|
|
var itemRow = itemSheet?.GetRow(actionId);
|
|
if (itemRow != null)
|
|
{
|
|
var itemName = (itemRow as dynamic)?.Name?.ToString() ?? (itemRow as dynamic)?.Singular?.ToString();
|
|
if (!string.IsNullOrEmpty(itemName))
|
|
return $"{itemName} [Item] ({actionId})";
|
|
}
|
|
}
|
|
catch { }
|
|
return actionId.ToString();
|
|
}
|
|
}
|