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;
}
/// Set job dropdown to player's current job if it's in the current job-type list; otherwise keep selection if valid or Unspecified.
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();
}
// Reset behavior (per-combo)
ImGui.Text("Reset combo when:");
ImGui.SameLine();
var resetMode = combo.ResetMode;
ImGui.SetNextItemWidth(180);
if (ImGui.BeginCombo("##resetMode", resetMode.ToString().Replace("_", " ")))
{
foreach (ComboResetMode mode in Enum.GetValues(typeof(ComboResetMode)))
{
if (ImGui.Selectable(mode.ToString().Replace("_", " "), resetMode == mode))
{
combo.ResetMode = mode;
Service.Configuration.Save();
}
}
ImGui.EndCombo();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Game timer: reset when game combo timer expires (~15s).\nNever: always run through sequence (cycle after last step).\nOn target change: reset when you change target.\nAfter seconds: reset after the time below with no combo use.");
if (combo.ResetMode == ComboResetMode.AfterSeconds)
{
ImGui.SameLine();
float sec = combo.ResetAfterSeconds;
ImGui.SetNextItemWidth(80);
if (ImGui.InputFloat("##resetSec", ref sec, 0.5f, 1f, "%.1fs", ImGuiInputTextFlags.CharsDecimal))
{
combo.ResetAfterSeconds = Math.Clamp(sec, 0.5f, 300f);
Service.Configuration.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Seconds of no combo use before reset (0.5–300).");
}
bool preservePosition = combo.PreservePositionWhenUsingOtherActions;
if (ImGui.Checkbox("Preserve position when using other actions", ref preservePosition))
{
combo.PreservePositionWhenUsingOtherActions = preservePosition;
Service.Configuration.Save();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("On (default): Using another combo or any ability never resets this combo's step; it only resets per \"Reset when\" above.\nOff (original): Using an action not in this combo resets this combo to step 1.");
// 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(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(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();
}
}