local DEBUG = -- function() end d local function _tr(str) return str end if not InventoryManager then InventoryManager = {} end local IM = InventoryManager local RevCaches = nil local Empties = nil local Moves = nil local function CreateReverseCache(bagType) local _revCache = { } local _empties = { } for i = 0, GetBagUseableSize(bagType)-1, 1 do local curStack, maxStack = GetSlotStackSize(bagType, i) if curStack > 0 then local id = GetItemInstanceId(bagType, i) if curStack < maxStack then if not _revCache[id] then _revCache[id] = { } end local entry = _revCache[id] entry[i] = { curStack, maxStack } end else _empties[#_empties + 1] = i end end return _revCache, _empties end local function CreateReverseCaches() RevCaches = { } Empties = { } RevCaches[BAG_SUBSCRIBER_BANK], Empties[BAG_SUBSCRIBER_BANK] = CreateReverseCache(BAG_SUBSCRIBER_BANK) RevCaches[BAG_BACKPACK], Empties[BAG_BACKPACK] = CreateReverseCache(BAG_BACKPACK) RevCaches[BAG_BANK], Empties[BAG_BANK] = CreateReverseCache(BAG_BANK) end local function UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, count) local id = GetItemInstanceId(srcBagType, srcSlotId) local stack stack = RevCaches[srcBagType][id] and RevCaches[srcBagType][id][srcSlotId] if stack then stack[1] = stack[1] - count if stack[1] == 0 then local newempties = Empties[srcBagType] newempties[#newempties + 1] = srcSlotId RevCaches[srcBagType][id][srcSlotId] = nil end else local newempties = Empties[srcBagType] newempties[#newempties + 1] = srcSlotId end stack = RevCaches[tgtBagType][id] and RevCaches[tgtBagType][id][tgtSlotId] if stack then stack[1] = stack[1] + count if stack[1] == stack[2] then RevCaches[tgtBagType][id][tgtSlotId] = nil end else local _, maxStack = GetSlotStackSize(srcBagType, srcSlotId) if count < maxStack then -- We ended up with a new incomplete stack. if not RevCaches[tgtBagType][id] then RevCaches[tgtBagType][id] = { } end local newstack = RevCaches[tgtBagType][id] newstack[tgtSlotId] = { count, maxStack } end end end -- Returns tgtSlotId, count local function FindStackToFill(id, count, tgtBagType) local stacks = RevCaches[tgtBagType][id] if not stacks then return nil end local tgtSlotId = nil local tgtCount = 0 for slotId, v in pairs(stacks) do local missing = v[2] - v[1] if missing >= count then return slotId, count end if missing > tgtCount then tgtSlotId = slotId tgtCount = missing end end return tgtSlotId, tgtCount end -- Returns (empties source?), tgtSlotId, transferCount, tgtBagType -- We try the subscriber bank before the regular one, to keep the slots of the regular one free in case -- someone unsubs. local function FindTargetSlot(srcBagType, srcSlotId, tgtBagType) local curStack, _ = GetSlotStackSize(srcBagType, srcSlotId) local id = GetItemInstanceId(srcBagType, srcSlotId) local tgtSlotId = nil local transferCount = nil -- Try the subscriber bank before the regular one if we target the bank if tgtBagType == BAG_BANK then tgtSlotId, transferCount = FindStackToFill(id, curStack, BAG_SUBSCRIBER_BANK) end if tgtSlotId ~= nil then tgtBagType = BAG_SUBSCRIBER_BANK else tgtSlotId, transferCount = FindStackToFill(id, curStack, tgtBagType) end if tgtSlotId ~= nil then UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, transferCount) return ( transferCount == curStack ), tgtSlotId, transferCount, tgtBagType end -- All the stacks we might have found are already full, we need to find a free slot local empties = { } -- Again, try the subscriber bank before the regular one if we target the bank if tgtBagType == BAG_BANK then empties = Empties[BAG_SUBSCRIBER_BANK] end if #empties ~= 0 then tgtBagType = BAG_SUBSCRIBER_BANK else empties = Empties[tgtBagType] end -- No such luck? if #empties == 0 then return false, -1, 0, tgtBagType end -- It's a complete move, remove the empty slot from the target list tgtSlotId = empties[#empties] empties[#empties] = nil UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, curStack) return true, tgtSlotId, curStack, tgtBagType end local function CollectSingleDirection(action, bagType) local _moveSlots = { } InventoryManager:SetCurrentInventory(bagType) for i,_ in pairs(InventoryManager.currentInventory) do local data = InventoryManager:GetItemData(i) if action == InventoryManager.currentRuleset:Match(data, action) then _moveSlots[#_moveSlots + 1] = i end end return _moveSlots end local function CalculateSingleMove(srcBagType, tgtBagType) local srcSlotRepo = Moves[srcBagType] -- If we draw from the pending moves from the regular bank and there are none, -- try the subscriber bank. if #srcSlotRepo == 0 and srcBagType == BAG_BANK then srcBagType = BAG_SUBSCRIBER_BANK srcSlotRepo = Moves[srcBagType] end if #srcSlotRepo == 0 then return "src_empty" end local srcSlotId = srcSlotRepo[#srcSlotRepo] -- if we target the bank, FindTargetSlot returns the subscriber bank first, if possible local empties, tgtSlotId, count empties, tgtSlotId, count, tgtBagType = FindTargetSlot(srcBagType, srcSlotId, tgtBagType) if count == 0 then return "tgt_full" end if empties then srcSlotRepo[#srcSlotRepo] = nil end return "ok", { ["srcbag"] = srcBagType, ["srcslot"] = srcSlotId, ["tgtbag"] = tgtBagType, ["tgtslot"] = tgtSlotId, ["count"] = count } end local function CalculateMoves() -- Prepare an overview of the inventories and the pending transfers in both directions CreateReverseCaches() local continue = true local _moveStack = { } InventoryManager.currentRuleset:ResetCounters() Moves = { [BAG_BACKPACK] = CollectSingleDirection(IM.ACTION_STASH, BAG_BACKPACK), [BAG_BANK] = CollectSingleDirection(IM.ACTION_RETRIEVE, BAG_BANK), [BAG_SUBSCRIBER_BANK] = CollectSingleDirection(IM.ACTION_RETRIEVE, BAG_SUBSCRIBER_BANK), } -- We alternate between stashing and retrieving to minimize the chance of one -- of the inventories running full. while continue do local leftres, leftentry = CalculateSingleMove(BAG_BACKPACK, BAG_BANK) local rightres, rightentry = CalculateSingleMove(BAG_BANK, BAG_BACKPACK) -- ZOS Spam limitation if #_moveStack > 95 then return "limited", _moveStack end -- Both inventories full, can't move anything if leftres == "tgt_full" and rightres == "tgt_full" then return "deadlock", _moveStack end -- We completed all we were set out to do if leftres == "src_empty" and rightres == "src_empty" then return "ok", _moveStack end -- We filled up one side, but we can't empty it out if leftres ~= "ok" and rightres ~= "ok" then return "partial", _moveStack end if leftres == "ok" then _moveStack[#_moveStack + 1] = leftentry end if rightres == "ok" then _moveStack[#_moveStack + 1] = rightentry end end -- NOTREACHED end InventoryManager.moveStatus = nil local function ProcessMove(move) local bagIdFrom = move["srcbag"] local slotIdFrom = move["srcslot"] local bagIdTo = move["tgtbag"] local slotIdTo = move["tgtslot"] local qtyToMove = move["count"] local action = (bagIdFrom == BAG_BACKPACK and InventoryManager.ACTION_STASH) or InventoryManager.ACTION_RETRIEVE InventoryManager.currentBagType = bagIdFrom local data = InventoryManager:GetItemData(slotIdFrom, SHARED_INVENTORY:GetOrCreateBagCache(bagIdFrom)) InventoryManager:ReportAction(data, false, action, "", "") if IsProtectedFunction("RequestMoveItem") then CallSecureProtected("RequestMoveItem", bagIdFrom, slotIdFrom, bagIdTo, slotIdTo, qtyToMove) else RequestMoveItem(bagIdFrom, slotIdFrom, bagIdTo, slotIdTo, qtyToMove) end end function InventoryManager:FinishMoves() local result if self.moveStatus == "limited" then result = GetString(IM_BANK_LIMITED) elseif self.moveStatus == "deadlock" then result = GetString(IM_BANK_DEADLOCK) elseif self.moveStatus == "partial" then result = GetString(IM_BANK_PARTIAL) elseif self.moveStatus == "ok" then result = GetString(IM_BANK_OK) end if result ~= "" then CHAT_SYSTEM:AddMessage(result) end end function InventoryManager:BalanceCurrency(currencyType, minCur, maxCur, bankCur) local carried local banked if bankCur then carried = GetBankedCurrencyAmount(currencyType) banked = GetCarriedCurrencyAmount(currencyType) else carried = GetCarriedCurrencyAmount(currencyType) banked = GetBankedCurrencyAmount(currencyType) end if minCur < 0 then minCur = 0 end if maxCur < 0 then maxCur = 0 end local move = 0 if(carried < minCur) then move = carried - minCur elseif(carried > maxCur) then move = carried - maxCur end if bankCur then move = -move end carried = GetCarriedCurrencyAmount(currencyType) banked = GetBankedCurrencyAmount(currencyType) if move == 0 then return elseif move > 0 then if move > carried then move = carried end CHAT_SYSTEM:AddMessage( zo_strformat(GetString(IM_CUR_DEPOSIT), ZO_Currency_FormatPlatform(currencyType, move, ZO_CURRENCY_FORMAT_AMOUNT_ICON))) DepositCurrencyIntoBank(currencyType, move) else move = -move if move > banked then move = banked end if move == 0 then return end CHAT_SYSTEM:AddMessage( zo_strformat(GetString(IM_CUR_WITHDRAW), ZO_Currency_FormatPlatform(currencyType, move, ZO_CURRENCY_FORMAT_AMOUNT_ICON))) WithdrawCurrencyFromBank(currencyType, move) end end local function event_filter_fn(eventCode, bagId, slotId, isNewItem, itemSoundCategory, inventoryUpdateReason, stackCountChange) -- Bank moves fire two events, react only if we got the receiver side. if not stackCountChange or stackCountChange > 0 then return true end return false end function InventoryManager:OnBankOpened() local moves self.moveStatus, moves = CalculateMoves() -- Flip the list. The processors start from the end, and the sequence is important here. for i = 1, #moves / 2, 1 do local tmp = moves[i] moves[i] = moves[(#moves+1) - i] moves[(#moves+1) - i] = tmp end self:BalanceCurrency(CURT_MONEY, self.settings.minGold, self.settings.maxGold, self.settings.bankGold) self:BalanceCurrency(CURT_TELVAR_STONES, self.settings.minTV, self.settings.maxTV, self.settings.bankTV) self:BalanceCurrency(CURT_ALLIANCE_POINTS, self.settings.minAP, self.settings.maxAP, self.settings.bankAP) self:BalanceCurrency(CURT_WRIT_VOUCHERS, self.settings.minVW, self.settings.maxVW, self.settings.bankVW) zo_callLater( function() IM:DoEventProcessing(moves, ProcessMove, function() IM:FinishMoves() end, EVENT_INVENTORY_SINGLE_SLOT_UPDATE, EVENT_CLOSE_BANK, InventoryManager.settings.bankMoveDelay, event_filter_fn) end, InventoryManager.settings.bankInitDelay) end EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_OPEN_BANK, function() InventoryManager:OnBankOpened() end)