Release v1.0.4

Add Shift+Right Click Trade window support with auto-fill max quantity.
This commit is contained in:
2026-01-26 22:20:18 -05:00
parent 57d2b8c6e2
commit dae9ea1be0
4 changed files with 82 additions and 14 deletions
+75 -7
View File
@@ -1148,7 +1148,7 @@ public sealed unsafe class Plugin : IDalamudPlugin
// Then we can recognize the correct InputNumeric without relying on prompt text. // Then we can recognize the correct InputNumeric without relying on prompt text.
private uint pendingSplitExpectedMax; private uint pendingSplitExpectedMax;
private long pendingSplitExpectedUntilMs; private long pendingSplitExpectedUntilMs;
private enum PendingNumericKind { None, Store, Remove, Move, Split } private enum PendingNumericKind { None, Store, Remove, Move, Split, Trade }
private PendingNumericKind pendingNumericKind; private PendingNumericKind pendingNumericKind;
private long lastShiftSeenMs; private long lastShiftSeenMs;
@@ -1287,6 +1287,7 @@ public sealed unsafe class Plugin : IDalamudPlugin
RemoveFromCompanyChest, RemoveFromCompanyChest,
Split, Split,
Sort, Sort,
Trade,
} }
private static readonly string[] ArmouryAddonNames = private static readonly string[] ArmouryAddonNames =
@@ -1899,6 +1900,23 @@ public sealed unsafe class Plugin : IDalamudPlugin
lastActionTickMs = now; lastActionTickMs = now;
if (Configuration.DebugMode) if (Configuration.DebugMode)
Log.Information($"[QuickTransfer] ({mode} + RClick) Selected context action '{chosenText}' (idx={chosenIndex}) via OpenForItemSlot."); Log.Information($"[QuickTransfer] ({mode} + RClick) Selected context action '{chosenText}' (idx={chosenIndex}) via OpenForItemSlot.");
// Set up Trade quantity auto-confirm if Trade was selected
if (mode == ModifierMode.Shift &&
chosenText.Length > 0 &&
ContextLabelMatches(AutoContextAction.Trade, chosenText) &&
IsTradeOpen())
{
pendingCompanyChestNumericConfirmUntilMs = now + 1500;
pendingCompanyChestNumericConfirmAttempts = 0;
pendingCompanyChestNumericArmed = true;
pendingNumericKind = PendingNumericKind.Trade;
pendingCompanyChestNumericValueSet = false;
pendingCompanyChestNumericValueSetAtMs = 0;
pendingCompanyChestNumericDesired = 0;
pendingCompanyChestNumericHalf = false;
ArmSuppressInputNumeric(now, 1500);
}
} }
else if (Configuration.DebugMode && mode == ModifierMode.Ctrl) else if (Configuration.DebugMode && mode == ModifierMode.Ctrl)
{ {
@@ -2014,7 +2032,12 @@ public sealed unsafe class Plugin : IDalamudPlugin
lastAltSeenMs = now; lastAltSeenMs = now;
// Quantity prompt auto-confirm (best effort). // Quantity prompt auto-confirm (best effort).
if (Configuration.AutoConfirmCompanyChestQuantity && // Trade and Split always auto-confirm; Company Chest respects the config setting.
var shouldAutoConfirm = pendingNumericKind == PendingNumericKind.Trade ||
pendingNumericKind == PendingNumericKind.Split ||
(Configuration.AutoConfirmCompanyChestQuantity && pendingNumericKind != PendingNumericKind.None);
if (shouldAutoConfirm &&
pendingNumericKind != PendingNumericKind.None && pendingNumericKind != PendingNumericKind.None &&
pendingCompanyChestNumericConfirmUntilMs > 0 && pendingCompanyChestNumericConfirmUntilMs > 0 &&
now <= pendingCompanyChestNumericConfirmUntilMs) now <= pendingCompanyChestNumericConfirmUntilMs)
@@ -2422,6 +2445,21 @@ public sealed unsafe class Plugin : IDalamudPlugin
? 3000 ? 3000
: 1500; : 1500;
ArmSuppressContextMenu(now, suppressMs); ArmSuppressContextMenu(now, suppressMs);
if (pending.Value.Mode == ModifierMode.Shift &&
chosenText.Length > 0 &&
ContextLabelMatches(AutoContextAction.Trade, chosenText))
{
// Trade: auto-confirm max quantity when InputNumeric appears
pendingCompanyChestNumericConfirmUntilMs = now + 1500;
pendingCompanyChestNumericConfirmAttempts = 0;
pendingCompanyChestNumericArmed = true;
pendingNumericKind = PendingNumericKind.Trade;
pendingCompanyChestNumericValueSet = false;
pendingCompanyChestNumericValueSetAtMs = 0;
pendingCompanyChestNumericDesired = 0;
pendingCompanyChestNumericHalf = false;
ArmSuppressInputNumeric(now, 1500);
}
if (Configuration.EnableCompanyChest && if (Configuration.EnableCompanyChest &&
pending.Value.Mode == ModifierMode.Shift && pending.Value.Mode == ModifierMode.Shift &&
chosenText.Length > 0 && chosenText.Length > 0 &&
@@ -2969,8 +3007,8 @@ public sealed unsafe class Plugin : IDalamudPlugin
// Single-pass: decode each label once, record first match per action. // Single-pass: decode each label once, record first match per action.
var foundAny = false; var foundAny = false;
int removeIdx = -1, addIdx = -1, placeIdx = -1, returnIdx = -1, entrustIdx = -1, retrieveIdx = -1, companyRemoveIdx = -1, splitIdx = -1; int removeIdx = -1, addIdx = -1, placeIdx = -1, returnIdx = -1, entrustIdx = -1, retrieveIdx = -1, companyRemoveIdx = -1, splitIdx = -1, tradeIdx = -1;
string? removeTxt = null, addTxt = null, placeTxt = null, returnTxt = null, entrustTxt = null, retrieveTxt = null, companyRemoveTxt = null, splitTxt = null; string? removeTxt = null, addTxt = null, placeTxt = null, returnTxt = null, entrustTxt = null, retrieveTxt = null, companyRemoveTxt = null, splitTxt = null, tradeTxt = null;
var max = Math.Min(agent->ContextItemCount, 64); var max = Math.Min(agent->ContextItemCount, 64);
for (var i = 0; i < max; i++) for (var i = 0; i < max; i++)
@@ -3039,6 +3077,13 @@ public sealed unsafe class Plugin : IDalamudPlugin
{ {
splitIdx = i; splitIdx = i;
splitTxt = text; splitTxt = text;
continue;
}
if (tradeIdx < 0 && ContextLabelMatches(AutoContextAction.Trade, text))
{
tradeIdx = i;
tradeTxt = text;
} }
} }
@@ -3048,6 +3093,7 @@ public sealed unsafe class Plugin : IDalamudPlugin
var saddlebagOpen = IsSaddlebagOpen(); var saddlebagOpen = IsSaddlebagOpen();
var retainerOpen = IsRetainerOpen(); var retainerOpen = IsRetainerOpen();
var companyChestOpen = IsCompanyChestOpen(); var companyChestOpen = IsCompanyChestOpen();
var tradeOpen = IsTradeOpen();
// Choose the best action that exists in the menu. // Choose the best action that exists in the menu.
// //
@@ -3071,6 +3117,11 @@ public sealed unsafe class Plugin : IDalamudPlugin
{ {
chosen = splitIdx >= 0 ? (splitIdx, splitTxt) : (-1, (string?)null); chosen = splitIdx >= 0 ? (splitIdx, splitTxt) : (-1, (string?)null);
} }
else if (mode == ModifierMode.Shift && tradeOpen)
{
// Trade window: prioritize Trade action when Trade window is open
chosen = tradeIdx >= 0 ? (tradeIdx, tradeTxt) : (-1, (string?)null);
}
else if (mode == ModifierMode.Shift && companyChestOpen && Configuration.EnableCompanyChest) else if (mode == ModifierMode.Shift && companyChestOpen && Configuration.EnableCompanyChest)
{ {
chosen = companyRemoveIdx >= 0 ? (companyRemoveIdx, companyRemoveTxt) : (-1, (string?)null); chosen = companyRemoveIdx >= 0 ? (companyRemoveIdx, companyRemoveTxt) : (-1, (string?)null);
@@ -3122,11 +3173,11 @@ public sealed unsafe class Plugin : IDalamudPlugin
GenerateCallback(contextMenuAddon, 0, chosen.idx, 0U, 0, 0); GenerateCallback(contextMenuAddon, 0, chosen.idx, 0U, 0, 0);
// Some actions (notably Split) can be cancelled if we close the menu immediately. // Some actions (notably Split and Trade) can be cancelled if we close the menu immediately.
// Delay the close slightly to allow the follow-up UI (InputNumeric) to spawn. // Delay the close slightly to allow the follow-up UI (InputNumeric) to spawn.
if (chosen.txt != null && ContextLabelMatches(AutoContextAction.Split, chosen.txt)) if (chosen.txt != null && (ContextLabelMatches(AutoContextAction.Split, chosen.txt) || ContextLabelMatches(AutoContextAction.Trade, chosen.txt)))
{ {
// Don't close immediately: on some setups this cancels Split before InputNumeric opens. // Don't close immediately: on some setups this cancels the action before InputNumeric opens.
// We'll keep the menu invisible (via suppression) and close it later as a cleanup. // We'll keep the menu invisible (via suppression) and close it later as a cleanup.
pendingCloseContextMenuAtMs = Environment.TickCount64 + 3000; pendingCloseContextMenuAtMs = Environment.TickCount64 + 3000;
} }
@@ -4457,6 +4508,15 @@ public sealed unsafe class Plugin : IDalamudPlugin
return false; return false;
if (kind == PendingNumericKind.Remove && !prompt.Contains("remove", StringComparison.OrdinalIgnoreCase)) if (kind == PendingNumericKind.Remove && !prompt.Contains("remove", StringComparison.OrdinalIgnoreCase))
return false; return false;
// Trade dialogs may be localized; if we're in Trade mode and Trade window is open, accept it
// (similar to how Split works - we trust the context rather than requiring exact prompt text)
if (kind == PendingNumericKind.Trade && !prompt.Contains("trade", StringComparison.OrdinalIgnoreCase))
{
// Fallback: if Trade window is open and we're expecting Trade, accept it anyway
// (prompt might be localized or say "How many would you like to trade?" etc.)
if (!IsTradeOpen())
return false;
}
if (minValue->Type != AtkValueType.UInt || maxValue->Type != AtkValueType.UInt || defaultValue->Type != AtkValueType.UInt) if (minValue->Type != AtkValueType.UInt || maxValue->Type != AtkValueType.UInt || defaultValue->Type != AtkValueType.UInt)
return false; return false;
@@ -5111,6 +5171,9 @@ public sealed unsafe class Plugin : IDalamudPlugin
private static bool IsCompanyChestOpen() private static bool IsCompanyChestOpen()
=> IsAddonVisibleAnyIndex(FreeCompanyChestAddonName); => IsAddonVisibleAnyIndex(FreeCompanyChestAddonName);
private static bool IsTradeOpen()
=> IsAddonVisibleAnyIndex("Trade") || IsAddonVisibleAnyIndex("TradeWindow");
private static bool IsCompanyChestType(FFXIVClientStructs.FFXIV.Client.Game.InventoryType inventoryType) private static bool IsCompanyChestType(FFXIVClientStructs.FFXIV.Client.Game.InventoryType inventoryType)
{ {
var name = Enum.GetName(typeof(FFXIVClientStructs.FFXIV.Client.Game.InventoryType), inventoryType); var name = Enum.GetName(typeof(FFXIVClientStructs.FFXIV.Client.Game.InventoryType), inventoryType);
@@ -5224,6 +5287,11 @@ public sealed unsafe class Plugin : IDalamudPlugin
t.Equals("Sort", StringComparison.OrdinalIgnoreCase) || t.Equals("Sort", StringComparison.OrdinalIgnoreCase) ||
t.StartsWith("Sort", StringComparison.OrdinalIgnoreCase), t.StartsWith("Sort", StringComparison.OrdinalIgnoreCase),
AutoContextAction.Trade =>
t.Equals("Trade", StringComparison.OrdinalIgnoreCase) ||
t.StartsWith("Trade", StringComparison.OrdinalIgnoreCase) ||
(Has(t, "Trade") && Has(t, "Item")),
_ => false, _ => false,
}; };
} }
+3 -3
View File
@@ -8,9 +8,9 @@
<RootNamespace>QuickTransfer</RootNamespace> <RootNamespace>QuickTransfer</RootNamespace>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<Version>1.0.3</Version> <Version>1.0.4</Version>
<AssemblyVersion>1.0.3.0</AssemblyVersion> <AssemblyVersion>1.0.4.0</AssemblyVersion>
<FileVersion>1.0.3.0</FileVersion> <FileVersion>1.0.4.0</FileVersion>
</PropertyGroup> </PropertyGroup>
<!-- Local builds: some setups have DALAMUD_HOME pointing at the XIVLauncher root, <!-- Local builds: some setups have DALAMUD_HOME pointing at the XIVLauncher root,
+2 -2
View File
@@ -2,8 +2,8 @@
"Author": "flick", "Author": "flick",
"Name": "QuickTransfer", "Name": "QuickTransfer",
"InternalName": "QuickTransfer", "InternalName": "QuickTransfer",
"AssemblyVersion": "1.0.3.0", "AssemblyVersion": "1.0.4.0",
"Description": "Automate inventory transfers with Shift/Ctrl/Alt + Right-Click.", "Description": "Automate inventory transfers with Shift/Ctrl/Alt + Right-Click. Includes Trade window support.",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"RepoUrl": "https://github.com/Knack117/QuickTransfer", "RepoUrl": "https://github.com/Knack117/QuickTransfer",
"Tags": [ "Tags": [
+2 -2
View File
@@ -3,8 +3,8 @@
"Author": "flick", "Author": "flick",
"Name": "QuickTransfer", "Name": "QuickTransfer",
"InternalName": "QuickTransfer", "InternalName": "QuickTransfer",
"AssemblyVersion": "1.0.3.0", "AssemblyVersion": "1.0.4.0",
"Description": "Automate inventory transfers with Shift/Ctrl/Alt + Right-Click.", "Description": "Automate inventory transfers with Shift/Ctrl/Alt + Right-Click. Includes Trade window support.",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"RepoUrl": "https://github.com/Knack117/QuickTransfer", "RepoUrl": "https://github.com/Knack117/QuickTransfer",
"DalamudApiLevel": 14, "DalamudApiLevel": 14,