From 808999f6823ed8ce2fc093cce43643e76aa32ac0 Mon Sep 17 00:00:00 2001 From: Vati Date: Wed, 18 Mar 2026 02:23:53 +0400 Subject: [PATCH] marker position --- Assets/pin.blp | Bin 0 -> 6676 bytes Core/Database.lua | 36 +++++++++++++ Sorting/SortEngine.lua | 46 +++++++++++------ UI/ItemButton.lua | 115 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 176 insertions(+), 21 deletions(-) create mode 100644 Assets/pin.blp diff --git a/Assets/pin.blp b/Assets/pin.blp new file mode 100644 index 0000000000000000000000000000000000000000..e6ee043620c8cc28a4c310da213963014cf2a586 GIT binary patch literal 6676 zcmeHL4{Q_X6@Pb$;hej~7>CqKutOa!$aU$2)W0){%awl1|QDsYTes{sv( ze{QKlL{oT`AbzK4ZV~4YA-1a5TRVLHV-;ds)^D5x)GwWEzZYFq!a|0!~>&3SJLdy1`z)R(d?X+Oyo$0U&4GJ z;^(014Z0!OKg3U`xLplJ7ME_ieMLQoeC#S*95Z$;o%{Ur$VZE~mR+=16}S?M)jH95 z#=3uETi#CtJ6=y0RN%{`_w}SAJ_>?eHAPkqoajY-owObrqsqm8N9C`F@teAg9h;i9 zN8{>JWHEwpz5Y_S5ySCg8g$^K6vX6a45H`A9VM z2wdn@Nzzst7GB1B0|P)A3hZcjM!Qb+3+s1Czw{F7m$sh85=Q?)%P`Dve7;m=l|&;o z)c;?Z_lRF$y*8I@Y9W>7NwlHZQO`=Qk>-A~0_)o;{#gx0qn}^<7H{Y8(^RE3biZFE zWdJgy%rmtac~Fe4aIYpcoVPpTUSZco{#v$QB%;}+-v*3f} zmk97Xqq`FpnK}fn$eQ{;SHC0qA)w(|ZT&WbYD{IdaXy@4nG@r$M5>#kLwbev9FDpp ze>$=AEsB4?c{kejp!I%l6WX1kd6(GOt(^JlL{k&ydxC=bQWb!2_@5tvT7T~<*u`hrQ z#Q&0{FHmpPh;OPl!`B6#0YxErwr?uZ6~IZE&ytIZQLl_lp{qOGEpU)m@l!#t7+dE|*@&mFgJ4QbLN!u@~o^|PYKkr6wL9fv;#0a2( zF5h$p(V$>!-fevO%5hx)P``?$l9G}BYMlIPIv;sg0AymnzYeV9kC-Bg6MrxHr3=7c zEkGMS5P0ij(DRYcAFG{}gU({USGCf9NAum_fW)lBRTqpHZ?EG9xR3vEOP?4zCUC_i z3o@{|3yvXSBaBR<#P;cQxB3jrTkHDKoM!+s_D`)gnonq@$Fd0PF@p_)olF zJ#IeeJWuh(=lTCbe53ItWDMoMFW!$Nf5Ap-pPd>yWTg6E&FiVH3&{EL`ml|*-&n8r zLHm2Dwe@R!cz({!$;rJlu<;%}A+)REsI;0mwnz2qp>PLD=AS_m=m=`MjGrFB(n>Tocw~8`OQBi4=r`0=Mcv$k(&bUcVpj zxA!2wE}XA*xIK2$)?qbA7T8^(7-WM<+f&e%CFx+oN(8uyZb*qBi->jqqLhIKC%1G60*|%(d0C z8nM4xY_>7y-?QSk`HkHe{dcMG+#8LW(~GL``L(SZY-Lc{N9T7%0HFH52R^Bb+php5 z;}4h}(^Ba9LpB50&>oIAjZ*wG%KMww@O-?D^8Xg)-!CzrPFr{xB{lR5?4SRIe!{h&pPtxmkF;9-q94$e_vM}Gand?M1%{RYGVp`umf29WX2j_=nxc@Tv zB7WXdFg}|f5s&199{XlbS+40YtvA1pxl!Hlm;8F|wGQlG4%tTFv$|sQM+-|MMqcxW zUrJY@lFi(T^>ot66=Q%;K>o8WUp`al+b8p7?RII-kGXj?&TexkYu~}<=FIG&uvg)n Jcc}Sc`=2XW46^_L literal 0 HcmV?d00001 diff --git a/Core/Database.lua b/Core/Database.lua index bedc744..834eb4f 100644 --- a/Core/Database.lua +++ b/Core/Database.lua @@ -162,6 +162,11 @@ function DB:Initialize() Guda_CharDB.setProtectionExceptions = {} end + -- Initialize pinned slots storage + if not Guda_CharDB.pinnedSlots then + Guda_CharDB.pinnedSlots = {} + end + -- Initialize CategoryManager for custom categories if addon.Modules.CategoryManager then addon.Modules.CategoryManager:Initialize() @@ -540,6 +545,37 @@ function DB:IsItemProtected(itemID) return false end +------------------------------------------------- +-- Pinned Slots (per-character, slot-based) +-- Slots pinned by the user are skipped during sorting. +-- Key format: bagID * 1000 + slot +------------------------------------------------- + +function DB:IsPinnedSlot(bagID, slot) + if not bagID or not slot or not Guda_CharDB or not Guda_CharDB.pinnedSlots then return false end + return Guda_CharDB.pinnedSlots[bagID * 1000 + slot] and true or false +end + +function DB:TogglePinnedSlot(bagID, slot) + if not bagID or not slot or not Guda_CharDB then return false end + if not Guda_CharDB.pinnedSlots then + Guda_CharDB.pinnedSlots = {} + end + local key = bagID * 1000 + slot + if Guda_CharDB.pinnedSlots[key] then + Guda_CharDB.pinnedSlots[key] = nil + return false -- unpinned + else + Guda_CharDB.pinnedSlots[key] = true + return true -- pinned + end +end + +function DB:GetPinnedSlotSet() + if not Guda_CharDB or not Guda_CharDB.pinnedSlots then return {} end + return Guda_CharDB.pinnedSlots +end + -- Cleanup old characters (not updated in 90 days) function DB:CleanupOldCharacters() local cutoff = time() - (90 * 24 * 60 * 60) -- 90 days diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 2837ada..9c00dd3 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -1147,7 +1147,7 @@ end -- PHASE 5: Empty Slot Management & Apply Sort --=========================================================================== -local function CollectItems(bagIDs) +local function CollectItems(bagIDs, pinnedSlots) local items = {} local sequence = 0 -- Add sequence number for stable sort @@ -1156,6 +1156,10 @@ local function CollectItems(bagIDs) if addon.Modules.Utils:IsBagValid(bagID) then for slot = 1, numSlots do + -- Skip pinned slots — they are not touched by sorting + if pinnedSlots and pinnedSlots[bagID * 1000 + slot] then + -- do nothing, slot is pinned + else -- Scan directly from game API instead of cached data local texture, itemCount, locked = GetContainerItemInfo(bagID, slot) local itemLink = GetContainerItemLink(bagID, slot) @@ -1194,6 +1198,7 @@ local function CollectItems(bagIDs) class = category or "", }) end + end -- if pinnedSlots end end end @@ -1201,7 +1206,7 @@ local function CollectItems(bagIDs) return items end -local function BuildTargetPositions(bagIDs, itemCount) +local function BuildTargetPositions(bagIDs, itemCount, pinnedSlots) local positions = {} local index = 1 @@ -1234,8 +1239,11 @@ local function BuildTargetPositions(bagIDs, itemCount) if addon.Modules.Utils:IsBagValid(bagID) then for slot = 1, numSlots do if index <= itemCount then - positions[index] = {bag = bagID, slot = slot} - index = index + 1 + -- Skip pinned slots + if not (pinnedSlots and pinnedSlots[bagID * 1000 + slot]) then + positions[index] = {bag = bagID, slot = slot} + index = index + 1 + end else break end @@ -1402,7 +1410,7 @@ end -- Build tail positions (end-to-start) for a given count across the provided bags, -- starting from the "last" regular bag (lowest priority, then highest bagID), -- and spilling into previous bags when needed. -local function BuildGreyTailPositions(bagIDs, greyCount) +local function BuildGreyTailPositions(bagIDs, greyCount, pinnedSlots) local positions = {} if greyCount <= 0 then return positions end @@ -1439,7 +1447,7 @@ local function BuildGreyTailPositions(bagIDs, greyCount) local tailSlots = {} for _, info in ipairs(ordered) do for slot = info.numSlots, 1, -1 do - if table.getn(tailSlots) < greyCount then + if table.getn(tailSlots) < greyCount and not (pinnedSlots and pinnedSlots[info.bagID * 1000 + slot]) then table.insert(tailSlots, { bag = info.bagID, slot = slot }) else break @@ -1611,6 +1619,9 @@ end function SortEngine:SortBagsPass() local bagIDs = addon.Constants.BAGS + -- Cache pinned slots for this pass + local pinnedSlots = addon.Modules.DB and addon.Modules.DB:GetPinnedSlotSet() or {} + -- Phase 1: Detect specialized bags local containers = DetectSpecializedBags(bagIDs) @@ -1625,10 +1636,10 @@ function SortEngine:SortBagsPass() for _, bagType in ipairs({"enchant", "herb", "soul", "quiver", "ammo"}) do local specialBags = containers[bagType] for _, bagID in ipairs(specialBags) do - local items = CollectItems({bagID}) + local items = CollectItems({bagID}, pinnedSlots) if table.getn(items) > 0 then items = SortItems(items) - local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) + local targetPositions = BuildTargetPositions({bagID}, table.getn(items), pinnedSlots) local moveCount = ApplySort({bagID}, items, targetPositions) specializedMoves = specializedMoves + moveCount end @@ -1642,7 +1653,7 @@ function SortEngine:SortBagsPass() local regularMoves = 0 local regularBagIDs = containers.regular if table.getn(regularBagIDs) > 0 then - local allItems = CollectItems(regularBagIDs) + local allItems = CollectItems(regularBagIDs, pinnedSlots) local nonGreys, greys = SplitGreyItems(allItems) local combinedItems = {} @@ -1651,7 +1662,7 @@ function SortEngine:SortBagsPass() -- Non-greys: sorted to front positions if table.getn(nonGreys) > 0 then local sortedNonGreys = SortItems(nonGreys) - local frontPositions = BuildTargetPositions(regularBagIDs, table.getn(sortedNonGreys)) + local frontPositions = BuildTargetPositions(regularBagIDs, table.getn(sortedNonGreys), pinnedSlots) for i, item in ipairs(sortedNonGreys) do table.insert(combinedItems, item) table.insert(combinedPositions, frontPositions[i]) @@ -1660,7 +1671,7 @@ function SortEngine:SortBagsPass() -- Greys: to tail positions (end->start) if table.getn(greys) > 0 then - local tailPositions = BuildGreyTailPositions(regularBagIDs, table.getn(greys)) + local tailPositions = BuildGreyTailPositions(regularBagIDs, table.getn(greys), pinnedSlots) for i, item in ipairs(greys) do table.insert(combinedItems, item) table.insert(combinedPositions, tailPositions[i]) @@ -1764,6 +1775,9 @@ function SortEngine:SortBankPass() local bagIDs = addon.Constants.BANK_BAGS + -- Cache pinned slots for this pass + local pinnedSlots = addon.Modules.DB and addon.Modules.DB:GetPinnedSlotSet() or {} + -- Phase 1: Detect specialized bags local containers = DetectSpecializedBags(bagIDs) @@ -1778,10 +1792,10 @@ function SortEngine:SortBankPass() for _, bagType in ipairs({"enchant", "herb", "soul", "quiver", "ammo"}) do local specialBags = containers[bagType] for _, bagID in ipairs(specialBags) do - local items = CollectItems({bagID}) + local items = CollectItems({bagID}, pinnedSlots) if table.getn(items) > 0 then items = SortItems(items) - local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) + local targetPositions = BuildTargetPositions({bagID}, table.getn(items), pinnedSlots) local moved = ApplySort({bagID}, items, targetPositions) specializedMoves = specializedMoves + moved end @@ -1792,7 +1806,7 @@ function SortEngine:SortBankPass() local regularBagIDs = containers.regular local regularMoves = 0 if table.getn(regularBagIDs) > 0 then - local allItems = CollectItems(regularBagIDs) + local allItems = CollectItems(regularBagIDs, pinnedSlots) local nonGreys, greys = SplitGreyItems(allItems) local combinedItems = {} @@ -1800,7 +1814,7 @@ function SortEngine:SortBankPass() if table.getn(nonGreys) > 0 then local sortedNonGreys = SortItems(nonGreys) - local frontPositions = BuildTargetPositions(regularBagIDs, table.getn(sortedNonGreys)) + local frontPositions = BuildTargetPositions(regularBagIDs, table.getn(sortedNonGreys), pinnedSlots) for i, item in ipairs(sortedNonGreys) do table.insert(combinedItems, item) table.insert(combinedPositions, frontPositions[i]) @@ -1808,7 +1822,7 @@ function SortEngine:SortBankPass() end if table.getn(greys) > 0 then - local tailPositions = BuildGreyTailPositions(regularBagIDs, table.getn(greys)) + local tailPositions = BuildGreyTailPositions(regularBagIDs, table.getn(greys), pinnedSlots) for i, item in ipairs(greys) do table.insert(combinedItems, item) table.insert(combinedPositions, tailPositions[i]) diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index 8a500f5..3a2291f 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -603,6 +603,84 @@ local function HideLockIcon(button) end end +--===================================================== +-- Pin Icon Pool (indicates slot is pinned / sort-protected) +--===================================================== +local pinIconPool = {} + +local function AcquirePinIcon() + local icon = table.remove(pinIconPool) + if not icon then + icon = CreateFrame("Frame", nil, UIParent) + icon:SetFrameStrata("HIGH") + icon:SetWidth(13) + icon:SetHeight(13) + + -- Shadow (behind) + local shadow = icon:CreateTexture(nil, "BACKGROUND") + shadow:SetWidth(15) + shadow:SetHeight(15) + shadow:SetPoint("CENTER", icon, "CENTER", 0, 0) + shadow:SetTexture("Interface\\AddOns\\Guda\\Assets\\pin") + shadow:SetVertexColor(0, 0, 0, 0.9) + icon.shadow = shadow + + -- Icon (front) + local texture = icon:CreateTexture(nil, "OVERLAY") + texture:SetAllPoints(icon) + texture:SetTexture("Interface\\AddOns\\Guda\\Assets\\pin") + icon.texture = texture + end + return icon +end + +local function ReleasePinIcon(icon) + if icon then + icon:Hide() + icon:ClearAllPoints() + table.insert(pinIconPool, icon) + end +end + +local function UpdatePinIcon(button, iconSize) + local DB = addon.Modules.DB + if not DB then return end + + local isPinned = false + if button.bagID and button.slotID and not button.otherChar and not button.isReadOnly then + isPinned = DB:IsPinnedSlot(button.bagID, button.slotID) + end + + if isPinned then + if not button.pinIcon then + button.pinIcon = AcquirePinIcon() + end + local pinSize = math.max(10, math.min(14, iconSize * 0.35)) + button.pinIcon:SetWidth(pinSize) + button.pinIcon:SetHeight(pinSize) + if button.pinIcon.shadow then + button.pinIcon.shadow:SetWidth(pinSize + 2) + button.pinIcon.shadow:SetHeight(pinSize + 2) + end + button.pinIcon:ClearAllPoints() + button.pinIcon:SetPoint("TOPLEFT", button, "TOPLEFT", -2, 2) + button.pinIcon:SetFrameLevel(button:GetFrameLevel() + 5) + button.pinIcon:Show() + else + if button.pinIcon then + ReleasePinIcon(button.pinIcon) + button.pinIcon = nil + end + end +end + +local function HidePinIcon(button) + if button.pinIcon then + ReleasePinIcon(button.pinIcon) + button.pinIcon = nil + end +end + -- Hook UseContainerItem to prevent selling/disenchanting protected items local OriginalUseContainerItem = UseContainerItem UseContainerItem = function(bag, slot, ...) @@ -988,6 +1066,7 @@ function Guda_ItemButton_OnLoad(self) self:SetScript("OnHide", function() HideJunkIcon(this) HideLockIcon(this) + HidePinIcon(this) end) -- Track cursor item on drag start for category drag-drop @@ -1043,6 +1122,30 @@ function Guda_ItemButton_OnLoad(self) return end + -- Pin/unpin slot with Alt+Right-Click + if IsAltKeyDown() and arg1 == "RightButton" and not this.otherChar and not this.isReadOnly then + if this.bagID ~= nil and this.slotID and addon.Modules.DB then + local isNowPinned = addon.Modules.DB:TogglePinnedSlot(this.bagID, this.slotID) + local itemName = "" + if this.hasItem and this.itemData and this.itemData.link then + itemName = " " .. this.itemData.link + end + if isNowPinned then + addon:Print("Slot pinned" .. itemName .. " (skipped during sort)") + else + addon:Print("Slot unpinned" .. itemName) + end + -- Refresh to show/hide pin icon + if addon.Modules.BagFrame and addon.Modules.BagFrame.Update then + addon.Modules.BagFrame:Update() + end + if addon.Modules.BankFrame and addon.Modules.BankFrame.Update then + addon.Modules.BankFrame:Update() + end + end + return + end + if IsAltKeyDown() and arg1 == "LeftButton" and this.hasItem and not this.otherChar and not this.isReadOnly then local link = GetContainerItemLink(this.bagID, this.slotID) if link and addon and addon.Modules and addon.Modules.Utils then @@ -1504,8 +1607,9 @@ local function ClearItemButton(self, emptySlotBg, countText, bagID) -- Hide junk icon HideJunkIcon(self) - -- Hide lock icon + -- Hide lock and pin icons HideLockIcon(self) + HidePinIcon(self) -- Hide normal texture self:SetNormalTexture("") @@ -1647,8 +1751,9 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha end end - -- Update lock icon + -- Update lock and pin icons UpdateLockIcon(self, iconSize) + UpdatePinIcon(self, iconSize) -- Update empty slot bg anchors/texture based on slot style UpdateEmptySlotBackground(self, emptySlotBg, iconSize) @@ -1838,13 +1943,13 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha if not self.categoryMarkIcon then self.categoryMarkIcon = self:CreateTexture(nil, "OVERLAY", 7) end - local markSize = math.max(10, math.floor(iconSize * 0.3)) + 6 + local markSize = math.max(10, math.floor(iconSize * 0.3)) + 3 self.categoryMarkIcon:SetWidth(markSize) self.categoryMarkIcon:SetHeight(markSize) self.categoryMarkIcon:ClearAllPoints() - local markOffX, markOffY = 2, 2 + local markOffX, markOffY = 1, 2 if addon.Modules and addon.Modules.Theme and addon.Modules.Theme:GetSlotStyle() == "square" then - markOffX, markOffY = -2, -2 + markOffX, markOffY = -1, -1 end self.categoryMarkIcon:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", markOffX, markOffY) self.categoryMarkIcon:SetTexture(categoryMarkTexture)