Version 1.3.0

willneedit [04-03-17 - 14:39]
Version 1.3.0
 * Added command /im settings
 * Fixed LUA error in RuleEdit when FCOIS is not present
 * Added maximum execution count for rules
 * Revamped Banker - more robust, better performance
Filename
FCOISLink.lua
InventoryManager.lua
InventoryManager.txt
Modules/Banking.lua
Modules/DelayedProcessor.lua
Modules/Extractor.lua
Modules/Junker.lua
Modules/Seller.lua
Rulesets.lua
UI/RuleEdit.lua
lang/de.lua
lang/en.lua
diff --git a/FCOISLink.lua b/FCOISLink.lua
index e1abb2b..5748d0c 100644
--- a/FCOISLink.lua
+++ b/FCOISLink.lua
@@ -146,13 +146,13 @@ function FCOISL:IsAnyMark(mark) return mark == I_ANY_MARK end
 function FCOISL:GetDynamicIconChoices()
 	if DIChoices then return DIChoices end

+	if not self:hasAddon() then return { TXT_NO_CARE } end
+
 	DIChoices = { TXT_NO_CARE, TXT_NO_MARK, TXT_ANY_MARK }
 	for _, v in pairs(staticIconList) do
 		DIChoices[#DIChoices + 1] = FCOISL:GetIconText(v)
 	end

-	if not self:hasAddon() then return DIChoices end
-
 	local totalNumberOfDynamicIcons, numberToDynamicIconNr = FCOGetDynamicInfo()
 	for index, dynamicIconNr in pairs(numberToDynamicIconNr) do
         local dynIconName = FCOISL:GetIconText(dynamicIconNr)
diff --git a/InventoryManager.lua b/InventoryManager.lua
index 957dfb8..e7695d3 100644
--- a/InventoryManager.lua
+++ b/InventoryManager.lua
@@ -150,6 +150,10 @@ function InventoryManager:run()
 	self:WorkBackpack(false)
 end

+function InventoryManager:OpenSettings()
+	self.LAM:OpenToPanel(self.UI.panel)
+end
+
 function InventoryManager:help()
 	-- self:SetCurrentInventory(BAG_BACKPACK)
 	-- for i, entry in pairs(self.currentInventory) do
@@ -166,6 +170,7 @@ function InventoryManager:help()
 	CHAT_SYSTEM:AddMessage("/im listrules - list the rules currently defined")
 	CHAT_SYSTEM:AddMessage("/im dryrun    - show what the currently defined rules would do to your inventory")
 	CHAT_SYSTEM:AddMessage("/im run       - make a pass of the filters over your inventory")
+	CHAT_SYSTEM:AddMessage("/im settings  - Open up the settings menu")
 end

 function InventoryManager:SlashCommand(argv)
@@ -185,6 +190,8 @@ function InventoryManager:SlashCommand(argv)
 		self:dryrun()
 	elseif options[1] == "run" then
 		self:run()
+	elseif options[1] == "settings" then
+		self:OpenSettings()
 	else
 		CHAT_SYSTEM:AddMessage("Unknown parameter '" .. argv .. "'")
 	end
diff --git a/InventoryManager.txt b/InventoryManager.txt
index f9a828d..479fcf4 100644
--- a/InventoryManager.txt
+++ b/InventoryManager.txt
@@ -2,7 +2,7 @@
 ## APIVersion: 100018
 ## OptionalDependsOn: LibAddonMenu-2.0
 ## SavedVariables: IMSavedVars
-## Version: 1.2.2
+## Version: 1.3.0
 ## Author: iwontsay
 ## Description: iwontsay's Inventory Manager

diff --git a/Modules/Banking.lua b/Modules/Banking.lua
index d69d940..b5fb1f3 100644
--- a/Modules/Banking.lua
+++ b/Modules/Banking.lua
@@ -6,73 +6,110 @@ local function _tr(str)
 	return str
 end

-local ST_OK = 0
-local ST_TGTFULL = 1
-local ST_SPAM = 2
+local IM = InventoryManager

-local InvCache = nil
+local RevCaches = nil
+local Empties = nil

-local function ScanInventory(bagType)
+local function CreateReverseCache(bagType)
+	local _revCache = { }
 	local _empties = { }
-	local _stackCount = { }
-	local inv = SHARED_INVENTORY:GetOrCreateBagCache(bagType)
+
 	for i = 0, GetBagSize(bagType)-1, 1 do
-		if not inv[i] then
-			_empties[#_empties + 1] = i
+		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
-			local curStack, maxStack = GetSlotStackSize(bagType, i)
-			_stackCount[i] = {
-				["id"] = inv[i].itemInstanceId,
-				["current"] = curStack,
-				["max"] = maxStack
-			}
+			_empties[#_empties + 1] = i
 		end
 	end
-	-- Empties is a list of empty slots, items an overview over stack counts in specific slots
-	return { ["empties"] = _empties, ["items"] = _stackCount }
+	return _revCache, _empties
 end

-local function RebuildStackCache(tgtBagType)
-	InvCache["tgtStackCache"][tgtBagType] = { }
-
-	local tgtInv = InvCache[tgtBagType]
-	local tgtStackCache = InvCache["tgtStackCache"][tgtBagType]
+local function CreateReverseCaches()
+	RevCaches = { }
+	Empties = { }
+	RevCaches[BAG_BACKPACK], 	Empties[BAG_BACKPACK] 	= CreateReverseCache(BAG_BACKPACK)
+	RevCaches[BAG_BANK], 		Empties[BAG_BANK] 		= CreateReverseCache(BAG_BANK)
+end

-	for k,v in pairs(tgtInv["items"]) do
-		local missing = v["max"] - v["current"]
-		if missing > 0 then
-			tgtStackCache[v["id"]] = { missing, k }
+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 (empties source?), tgtSlotId, transferCount
 local function FindTargetSlot(srcBagType, srcSlotId, tgtBagType)
-	local srcInv = InvCache[srcBagType]
-	local tgtInv = InvCache[tgtBagType]
-	local tgtStackCache = InvCache["tgtStackCache"][tgtBagType]
-
-	local iId = srcInv["items"][srcSlotId]["id"]
-	local count = srcInv["items"][srcSlotId]["current"]
-
-	if tgtStackCache[iId] then
-		local missing = tgtStackCache[iId][1]
+	local curStack, maxStack = GetSlotStackSize(srcBagType, srcSlotId)
+	local id = GetItemInstanceId(srcBagType, srcSlotId)

-		if missing > 0 then
-			local k = tgtStackCache[iId][2]
-			local empties = missing >= count
-			return empties, false, k, (empties and count) or missing
+	local stacks = RevCaches[tgtBagType][id]
+	if stacks then
+		-- First, try to find a stack small enough to hold the entirety of the source
+		for tgtSlotId, v in pairs(stacks) do
+			if v[2]-v[1] >= curStack then
+				UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, curStack)
+				return true, tgtSlotId, curStack
+			end
 		end
-	end
-
-	-- No incomplete stack found, return an empty slot or a failure
-	local emptyslots = tgtInv["empties"]

-	if #emptyslots == 0 then
-		return false, false, -1, 0
+		-- Now, fill any incomplete stack we might have, splitting the source stack
+		for tgtSlotId, v in pairs(stacks) do
+			local missing = v[2] - v[1]
+			if missing > 0 then
+				UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, missing)
+				return false, tgtSlotId, missing
+			end
+		end
 	end
+
+	-- All the stacks we might have found are already full, we need to find a free slot
+	local empties = Empties[tgtBagType]
+
+	-- No such luck?
+	if #empties == 0 then return false, -1, 0 end
+
+	-- It's a complete move, remove the empty slot from the target list, and create a new one on the source list
+	local tgtSlotId = empties[#empties]
+	empties[#empties] = nil

-	-- We'd start another stack, but we're sure it'll be a complete transfer
-	return true, true, emptyslots[#emptyslots], count
+	UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, curStack)
+	return true, tgtSlotId, curStack
+
 end


@@ -91,86 +128,19 @@ local function CollectSingleDirection(action)
 	return _moveSlots
 end

-local function PrepareMoveCaches()
-	InvCache = {
-		[BAG_BACKPACK] = ScanInventory(BAG_BACKPACK),
-		[BAG_BANK] = ScanInventory(BAG_BANK),
-		["tgtStackCache"] = { [BAG_BACKPACK] = { }, [BAG_BANK] = { } }
-	}
-
-	RebuildStackCache(BAG_BACKPACK)
-	RebuildStackCache(BAG_BANK)
-
-	Moves = {
-		["stash"] = CollectSingleDirection(InventoryManager.ACTION_STASH),
-		["retrieve"] = CollectSingleDirection(InventoryManager.ACTION_RETRIEVE)
-	}
-
-end
-
 local function CalculateSingleMove(direction)
-	local IMR = InventoryManager.IM_Ruleset
 	local srcBagType = (direction == 1 and BAG_BACKPACK) or BAG_BANK
 	local tgtBagType = (direction == 1 and BAG_BANK) or BAG_BACKPACK
-	local tgtStackCache = InvCache["tgtStackCache"][tgtBagType]

 	local srcSlotRepo = Moves[(direction == 1 and "stash") or "retrieve"]
-	if #srcSlotRepo == 0 then
-		return "src_empty"
-	end
+	if #srcSlotRepo == 0 then return "src_empty" end

 	local srcSlotId = srcSlotRepo[#srcSlotRepo]

-	local empties, newSlot, tgtSlotId, count = FindTargetSlot(srcBagType, srcSlotId, tgtBagType)
-	if count == 0 then
-		return "tgt_full"
-	end
+	local empties, tgtSlotId, count = FindTargetSlot(srcBagType, srcSlotId, tgtBagType)
+	if count == 0 then return "tgt_full" end

-	InventoryManager:SetCurrentInventory(srcBagType)
-	local data = InventoryManager:GetItemData(srcSlotId)
-
-	if count > 0 then
-	end
-
-	if empties then
-		-- Empties source slot, remove from pending moves
-		InvCache[srcBagType]["items"][srcSlotId] = nil
-
-		local emptyslots = InvCache[srcBagType]["empties"]
-		emptyslots[#emptyslots + 1] = srcSlotId
-
-		srcSlotRepo[#srcSlotRepo] = nil
-	else
-		-- Incomplete move, deduce count in source cache
-		local srcslot = InvCache[srcBagType]["items"][srcSlotId]
-		srcslot["current"] = srcslot["current"] - count
-	end
-
-	if newSlot then
-		-- Filled up a new slot in the target
-		InvCache[tgtBagType]["items"][tgtSlotId] = {
-				["id"] = data.itemInstanceId,
-				["current"] = count,
-				["max"] = data.maxCount
-		}
-
-		local emptyslots = InvCache[tgtBagType]["empties"]
-		emptyslots[#emptyslots] = nil
-
-		local tgtslot = InvCache[tgtBagType]["items"][tgtSlotId]
-		tgtStackCache[data.itemInstanceId] = { tgtslot["max"] - tgtslot["current"], tgtSlotId }
-	else
-		-- Stashed onto existing stack, increase count
-		local tgtslot = InvCache[tgtBagType]["items"][tgtSlotId]
-		tgtslot["current"] = tgtslot["current"] + count
-		tgtStackCache[data.itemInstanceId] = { tgtslot["max"] - tgtslot["current"], tgtSlotId }
-
-		-- If we filled a stack, rescan, maybe there's another incomplete stack.
-		if tgtStackCache[data.itemInstanceId][1] == 0 then
-			RebuildStackCache(tgtBagType)
-		end
-	end
-
+	if empties then srcSlotRepo[#srcSlotRepo] = nil end

 	return "ok", {
 		["srcbag"] = srcBagType,
@@ -183,9 +153,16 @@ end

 local function CalculateMoves()
 	-- Prepare an overview of the inventories and the pending transfers in both directions
-	PrepareMoveCaches()
+	CreateReverseCaches()
 	local continue = true
 	local _moveStack = { }
+
+	InventoryManager.currentRuleset:ResetCounters()
+
+    Moves = {
+		["stash"] = CollectSingleDirection(IM.ACTION_STASH),
+		["retrieve"] = CollectSingleDirection(IM.ACTION_RETRIEVE)
+    }

 	-- We alternate between stashing and retrieving to minimize the chance of one
 	-- of the inventories running full.
@@ -226,9 +203,7 @@ end

 InventoryManager.moveStatus = nil

-function ProcessMove(move)
-	local IMR = InventoryManager.IM_Ruleset
-
+function ProcessMove(move)
 	local bagIdFrom = move["srcbag"]
 	local slotIdFrom = move["srcslot"]
 	local bagIdTo = move["tgtbag"]
@@ -290,22 +265,39 @@ function InventoryManager:BalanceCurrency(currencyType, minCur, maxCur, curName)
 	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, GetString(IM_CUR_GOLD))
 	self:BalanceCurrency(CURT_TELVAR_STONES, self.settings.minTV, self.settings.maxTV, GetString(IM_CUR_TVSTONES))

-	self:DoDelayedProcessing(moves,
-		ProcessMove,
-		function() InventoryManager:FinishMoves() end,
-		InventoryManager.settings.bankMoveDelay,
+	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

-local function OnBankOpened(eventCode)
-	InventoryManager:OnBankOpened()
-end
-
-EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_OPEN_BANK, OnBankOpened)
+EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_OPEN_BANK, function() InventoryManager:OnBankOpened() end)
diff --git a/Modules/DelayedProcessor.lua b/Modules/DelayedProcessor.lua
index b53c54b..b26d27b 100644
--- a/Modules/DelayedProcessor.lua
+++ b/Modules/DelayedProcessor.lua
@@ -15,6 +15,7 @@ local _Finish_fn = nil

 local _Event_Next = nil
 local _Event_Abort = nil
+local _Event_Filter_fn = nil

 -- Simple processing loop, call next element after delay
 local function ProcessLoop()
@@ -33,6 +34,11 @@ end
 -- Event driven processing loop, called directly for first element,
 -- then is fired by _Loop_fn's completion for the subsequent ones.
 local function EventProcessLoop(eventCode, a1, a2, a3, a4, a5, a6, a7, a8)
+	-- Bail out if this event is not meant for us.
+	if _Event_Filter_fn then
+		if not _Event_Filter_fn(eventCode, a1, a2, a3, a4, a5, a6, a7, a8) then return end
+	end
+
 	if not _Pending or #_Pending == 0 then
 		if _Event_Next then
 			EVENT_MANAGER:UnregisterForEvent("IMEventProcessLoop", _Event_Next)
@@ -62,13 +68,14 @@ local function EventProcessLoopAbort()
 	if _Finish_fn then _Finish_fn(false) end
 end

-function IM:DoEventProcessing(list, loop_fn, finish_fn, loop_event, abort_event, run_delay)
+function IM:DoEventProcessing(list, loop_fn, finish_fn, loop_event, abort_event, run_delay, event_filter_fn)
 	_Pending = list
 	_Loop_fn = loop_fn
 	_Finish_fn = finish_fn
 	_Delay = run_delay or 1
 	_Event_Next = loop_event
 	_Event_Abort = abort_event
+	_Event_Filter_fn = event_filter_fn

 	if _Event_Next then
 		EVENT_MANAGER:RegisterForEvent("IMEventProcessLoop", _Event_Next, EventProcessLoop)
@@ -127,10 +134,10 @@ function IM:ProcessBag(bagId, filter_fn, loop_fn, finish_fn, run_delay, init_del
 	self:DoDelayedProcessing(list, loop_fn, finish_fn, run_delay, init_delay)
 end

-function IM:EventProcessBag(bagId, filter_fn, loop_fn, finish_fn, loop_event, abort_event, run_delay)
+function IM:EventProcessBag(bagId, filter_fn, loop_fn, finish_fn, loop_event, abort_event, run_delay, event_filter_fn)
 	local list = IM:CreateInventoryList(bagId, filter_fn)

-	self:DoEventProcessing(list, loop_fn, finish_fn, loop_event, abort_event, run_delay)
+	self:DoEventProcessing(list, loop_fn, finish_fn, loop_event, abort_event, run_delay, event_filter_fn)
 end

 function IM:AbortProcessing()
diff --git a/Modules/Extractor.lua b/Modules/Extractor.lua
index dee47a7..94b96b5 100644
--- a/Modules/Extractor.lua
+++ b/Modules/Extractor.lua
@@ -92,6 +92,8 @@ local function extract_single_item(tradeskill, data)
 end

 local function InitDeconstruction(tradeskill)
+	InventoryManager.currentRuleset:ResetCounters()
+
 	local list = IM:CreateInventoryList(BAG_BACKPACK,
 		function(data) return filter_for_deconstruction(tradeskill, data) end)

diff --git a/Modules/Junker.lua b/Modules/Junker.lua
index c3e886c..2f6574e 100644
--- a/Modules/Junker.lua
+++ b/Modules/Junker.lua
@@ -10,6 +10,8 @@ function IM:CheckAndDestroy()
 		return
 	end

+	InventoryManager.currentRuleset:ResetCounters()
+
 	self:SetCurrentInventory(BAG_BACKPACK)
 	for i,_ in pairs(self.currentInventory) do
 		local data = self:GetItemData(i)
@@ -39,6 +41,7 @@ local function filter_for_backpack_action(dryrun, data)
 end

 function IM:WorkBackpack(dryrun)
+	InventoryManager.currentRuleset:ResetCounters()
 	self:ProcessBag(BAG_BACKPACK,
 		function(data) return filter_for_backpack_action(dryrun, data) end,
 		function(data) IM:ProcessSingleItem(dryrun, data) end,
diff --git a/Modules/Seller.lua b/Modules/Seller.lua
index f0bc21e..8ebacf2 100644
--- a/Modules/Seller.lua
+++ b/Modules/Seller.lua
@@ -53,6 +53,7 @@ function InventoryManager:SellItems(stolen)
 		if eventCode ~= nil then
 			_Gain = _Gain + money
 		end
+		InventoryManager.currentRuleset:ResetCounters()
 		InventoryManager:EventProcessBag(BAG_BACKPACK,
 			filter_for_launder,
 			function(data) InventoryManager:ProcessSingleItem(false, data) end,
@@ -63,6 +64,7 @@ function InventoryManager:SellItems(stolen)
 		end

 	_Gain = 0
+	InventoryManager.currentRuleset:ResetCounters()
 	self:EventProcessBag(BAG_BACKPACK,
 		function(data) return filter_for_sale(stolen, data) end,
 		do_sell,
diff --git a/Rulesets.lua b/Rulesets.lua
index 42e4bc9..93140f4 100644
--- a/Rulesets.lua
+++ b/Rulesets.lua
@@ -40,6 +40,10 @@ function IM_Rule:ToString()
 			" " .. GetString(self.filterType))


+	if self.maxCount then
+		actionText = actionText .. " " .. zo_strformat(GetString(IM_RULETXT_EXECOUNT), self.maxCount)
+	end
+
 	if self.traitType then
 		local which = (self.filterType == "IM_FILTER_CONSUMABLE" and 1) or 0
 		if self.traitType < 0 then
@@ -188,7 +192,15 @@ function IM_Ruleset:New()
 	return _new
 end

+local ExecCounters = nil
+
+function IM_Ruleset:ResetCounters()
+	ExecCounters = nil
+end
+
 function IM_Ruleset:Match(data)
+	if not ExecCounters then ExecCounters = { } end
+
 	for k, v in pairs(self.rules) do
 		local res = v:Filter(data)

@@ -201,7 +213,13 @@ function IM_Ruleset:Match(data)
 			end
 		end

+		-- If we reached the max execution count for that particular rule, skip it.
+		if res and v.maxCount and ExecCounters[k] and ExecCounters[k] >= v.maxCount then
+			res = false
+		end
+
 		if res then
+			ExecCounters[k] = (ExecCounters[k] or 0) + 1
 			return v.action, k, v:ToString()
 		end
 	end
diff --git a/UI/RuleEdit.lua b/UI/RuleEdit.lua
index 074dc9e..6524281 100644
--- a/UI/RuleEdit.lua
+++ b/UI/RuleEdit.lua
@@ -167,9 +167,16 @@ function RE:GetControls()
 			setFunc = function(value) RE.editingRule.action = RE.actionList["reverse"][value] end,
 		},
 		{
-			type = "description",
-			text = "",
-			width = "half",
+			type = "slider",
+			name = GetString(IM_SET_EXECOUNT),
+			tooltip = GetString(IM_SET_EXECOUNT_TT),
+			min = 0,
+			max = 500,
+			getFunc = function() return RE.editingRule.maxCount or 0 end,
+			setFunc = function(value)
+				RE.editingRule.maxCount = (value ~= 0 and value) or nil
+			end,
+			width = "half",	--or "half" (optional)
 		},
 		{
 			type = "dropdown",
diff --git a/lang/de.lua b/lang/de.lua
index b6e2b33..695a10d 100644
--- a/lang/de.lua
+++ b/lang/de.lua
@@ -11,6 +11,7 @@ local lang = {
 	IM_RULETXT_JUNKED			= "weggeworfene(s)",
 	IM_RULETXT_QUALITY1			= "mit Qualität <<1>>",
 	IM_RULETXT_QUALITY2			= "mit Qualität von <<1>> bis <<2>>",
+	IM_RULETXT_EXECOUNT 		= "(max. <<1>>-mal)",

 	IM_ACTIONTXT0				= "Behalten",
 	IM_ACTIONTXT1				= "Zum Müll stecken",
@@ -143,6 +144,8 @@ local lang = {
 	IM_SET_START_BM_TT 			= "Setzt die Verzögerung, bevor mit Bankbewegungen angefangen wird. Es ist ratsam, bei hochvolumigen Addons wie Inventory Grid View einen höheren Wert anzusetzen.",
 	IM_SET_INV 					= "Verzögerung zw. Inv.-Änderung",
 	IM_SET_INV_TT 				= "Setzt die Verzögerung zwischen Änderungen im Inventar wie Sperren/Entsperren usw.",
+	IM_SET_EXECOUNT 			= "Maximale Anzahl Ausführungen",
+	IM_SET_EXECOUNT_TT 			= "Wie oft diese Regel maximal in einem Lauf ausgeführt werden darf. 0 bedeutet 'unbegrenzt'",

 	IM_PM_PROFILENAME_TOOLTIP	= "Namen vom Profil hier eingeben",
 	IM_RM_PRESETRULES			= "--- Voreingestellte Profile ---",
diff --git a/lang/en.lua b/lang/en.lua
index 51b1388..23137dc 100644
--- a/lang/en.lua
+++ b/lang/en.lua
@@ -11,6 +11,7 @@ local lang = {
 	IM_RULETXT_JUNKED			= "junked",
 	IM_RULETXT_QUALITY1			= "with quality <<1>>",
 	IM_RULETXT_QUALITY2			= "with quality from <<1>> to <<2>>",
+	IM_RULETXT_EXECOUNT 		= "(max. <<1>> times)",

 	IM_ACTIONTXT0				= "Keep",
 	IM_ACTIONTXT1				= "Put to junk",
@@ -143,6 +144,8 @@ local lang = {
 	IM_SET_START_BM_TT 			= "Sets the delay before starting bank moves. It's advisable to set a higher delay if you use high-impact addons like Inventory Grid View.",
 	IM_SET_INV 					= "Delay between inv changes",
 	IM_SET_INV_TT 				= "Sets the delay between inventory status changes like junk/unjunk lock/unlock and so on.",
+	IM_SET_EXECOUNT 			= "Maximum execution count",
+	IM_SET_EXECOUNT_TT 			= "How often this rule may be executed in a single run. 0 means 'unlimited'",

 	IM_PM_PROFILENAME_TOOLTIP	= "Enter the name of the new profile here",
 	IM_RM_PRESETRULES			= "--- Preset profiles ---",