7c174efa70
- Per-combo reset when: Game timer (default), Never, On target change, After X seconds. Never = cycle sequence without time/target reset. After seconds uses configurable 0.5–300s window. - Per-combo 'Preserve position when using other actions' (default on): when on, using another combo or any ability never resets this combo's step; it only resets per its own 'Reset when' setting. When off (original), using an action not in this combo resets it to step 1. - Optional ITargetManager via constructor for On target change; plugin loads if service unavailable (On target change no-op when null). Made-with: Cursor
451 lines
18 KiB
C#
451 lines
18 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();
|
||
}
|
||
|
||
// 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<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();
|
||
}
|
||
}
|