added libasync to reduce lag, hooked up bagslotindex

git [02-10-18 - 16:04]
added libasync to reduce lag, hooked up bagslotindex
Filename
IIfA/IIfA.lua
IIfA/IIfA.txt
IIfA/IIfADataCollection.lua
IIfA/IIfAEvents.lua
IIfA/libs/LibAsync/LibAsync.lua
diff --git a/IIfA/IIfA.lua b/IIfA/IIfA.lua
index bc65109..d9cc03d 100644
--- a/IIfA/IIfA.lua
+++ b/IIfA/IIfA.lua
@@ -469,12 +469,18 @@ function IIfA_onLoad(eventCode, addOnName)
 	if not ObjSettings.frameSettings.hud.hidden then
 		IIfA:ProcessSceneChange("hud", "showing", "shown")
 	end
-	--	IIfA:MakeBSI()

 	IIfA:RegisterForEvents()
 	IIfA:RegisterForSceneChanges() -- register for callbacks on scene statechanges using user preferences or defaults
-	IIfA:ScanCurrentCharacter()
+
 	IIfA:ScanBank()
+	IIfA:ScanCurrentCharacter()
+	zo_callLater(function()
+		IIfA:MakeBSI()
+	end, 5000)
+
+
+
 end

 EVENT_MANAGER:RegisterForEvent("IIfALoaded", EVENT_ADD_ON_LOADED, IIfA_onLoad)
diff --git a/IIfA/IIfA.txt b/IIfA/IIfA.txt
index 4a373d1..3f80d11 100644
--- a/IIfA/IIfA.txt
+++ b/IIfA/IIfA.txt
@@ -26,6 +26,7 @@ libs\LibAddonMenu-2.0\controls\texture.lua
 libs\LibAddonMenu-2.0\controls\iconpicker.lua
 libs\LibAddonMenu-2.0\controls\divider.lua
 libs\LibCustomMenu\LibCustomMenu.lua
+libs\LibAsync\LibAsync.lua


 IIfA_Preload.lua
diff --git a/IIfA/IIfADataCollection.lua b/IIfA/IIfADataCollection.lua
index 99303b2..4b7c2f9 100644
--- a/IIfA/IIfADataCollection.lua
+++ b/IIfA/IIfADataCollection.lua
@@ -1,7 +1,8 @@
-local IIfA = IIfA
-local EMPTY_STRING = ""
+local IIfA 			= IIfA
+local EMPTY_STRING 	= ""

-
+local task 			= LibStub("LibAsync"):Create("IIfA_DataCollection")
+IIfA.task			= task
 local function grabBagContent(bagId)
 	local bagItems = GetBagSize(bagId)
 	for slotNum=0, bagItems, 1 do
@@ -64,15 +65,19 @@ function IIfA:CollectGuildBank()
 		IIfA.data.guildBanks[curGuild] = {}
 		IIfA.data.guildBanks[curGuild].bCollectData = true		-- default to true just so it's here and ok
 	end
-	local guildData = IIfA.data.guildBanks[curGuild]
-	guildData.items = #ZO_GuildBankBackpack.data
-	guildData.lastCollected = GetDate() .. "@" .. GetFormattedTime();
-	IIfA:ClearLocationData(curGuild)
-	IIfA:DebugOut(" - " .. #ZO_GuildBankBackpack.data .. " items")
-	for i=1, #ZO_GuildBankBackpack.data do
-		local slotIndex = ZO_GuildBankBackpack.data[i].data.slotIndex
-		IIfA:EvalBagItem(BAG_GUILDBANK, slotIndex)
-	end
+
+	-- call with libAsync to avoid lags
+	task:Call(function()
+		local guildData = IIfA.data.guildBanks[curGuild]
+		guildData.items = #ZO_GuildBankBackpack.data
+		guildData.lastCollected = GetDate() .. "@" .. GetFormattedTime();
+		IIfA:ClearLocationData(curGuild)
+		IIfA:DebugOut(" - " .. #ZO_GuildBankBackpack.data .. " items")
+		for i=1, #ZO_GuildBankBackpack.data do
+			local slotIndex = ZO_GuildBankBackpack.data[i].data.slotIndex
+			IIfA:EvalBagItem(BAG_GUILDBANK, slotIndex)
+		end
+	end)
 --	d("IIfA - Guild Bank Collected - " .. curGuild)
 end

@@ -87,11 +92,20 @@ local function scanBags()
 	IIfA:ClearLocationData(IIfA.currentCharacterId)

 	if not IIfA:IsCharacterEquipIgnored(playerName) then
-		grabBagContent(BAG_WORN)
+		-- call with libAsync to avoid lags
+		task:Call(function()
+			grabBagContent(BAG_WORN)
+		end)
 	end
 	if not IIfA:IsCharacterInventoryIgnored(playerName) then
-		grabBagContent(BAG_BACKPACK)
-	end
+		-- call with libAsync to avoid lags
+		task:Call(function()
+			grabBagContent(BAG_BACKPACK)
+		end)
+	end
+	task:Call(function()
+		IIfA:MakeBSI()
+	end)
 end
 IIfA.ScanCurrentCharacter = scanBags

@@ -105,8 +119,12 @@ local function tryScanHouseBank()
 	if IsCollectibleUnlocked(collectibleId) then
 		local collectibleName = GetCollectibleNickname(collectibleId)
 		if collectibleName == EMPTY_STRING then collectibleName = GetCollectibleName(collectibleId) end
-		IIfA:ClearLocationData(bagId)
-		grabBagContent(bagId)
+		-- call with libAsync to avoid lags
+		task:Call(function()
+			IIfA:ClearLocationData(bagId)
+		end):Then(function()
+			grabBagContent(bagId)
+		end)
 	end

 	return true
@@ -114,25 +132,46 @@ end

 function IIfA:ScanBank()
 	if tryScanHouseBank() then return end
-	IIfA:ClearLocationData(GetString(IIFA_BAG_BANK))
-	grabBagContent(BAG_BANK)
+	-- call with libAsync to avoid lags
+	task:Call(function()
+		IIfA:ClearLocationData(GetString(IIFA_BAG_BANK))
+	end):Then(function()
+		grabBagContent(BAG_BANK)
+	end)

 	local slotNum = nil
 	if HasCraftBagAccess() then
-		grabBagContent(BAG_SUBSCRIBER_BANK)
-		IIfA:ClearLocationData(GetString(IIFA_BAG_CRAFTBAG))
-		slotNum = GetNextVirtualBagSlotId(slotNum)
-		while slotNum ~= nil do
-			IIfA:EvalBagItem(BAG_VIRTUAL, slotNum)
+		task:Call(function()
+			grabBagContent(BAG_SUBSCRIBER_BANK)
+		end):Then(function()
+			IIfA:ClearLocationData(GetString(IIFA_BAG_CRAFTBAG))
+		end):Then(function()
 			slotNum = GetNextVirtualBagSlotId(slotNum)
-		end
+			while slotNum ~= nil do
+				IIfA:EvalBagItem(BAG_VIRTUAL, slotNum)
+				slotNum = GetNextVirtualBagSlotId(slotNum)
+			end
+		end)
 	end
+
 end

+function IIfA:UpdateBSI(bagId, slotId, locationId)
+	if locationId then
+		IIfA:MakeBSI()
+	elseif  nil ~= bagId and  nil ~= slotId and nil ~= IIfA.BagSlotInfo[bagId] then
+		IIfA.BagSlotInfo[bagId][slotId] = GetItemLink(bagId, slotId)
+	end
+end
+
+
 -- only grabs the content of bagpack and worn on the first login - hence we set the function to insta-return below.
 function IIfA:OnFirstInventoryOpen()
 	scanBags()
-	IIfA:ScanBank()
+	-- call with libAsync to avoid lags
+	task:Call(function()
+		IIfA:ScanBank()
+	end)
 	IIfA.OnFirstInventoryOpen = function() return end
 end

@@ -183,37 +222,49 @@ function IIfA:CleanEmptyGuildBug()
 end

 function IIfA:GuildBankReady()
-	IIfA:DebugOut("GuildBankReady...")
-	IIfA.isGuildBankReady = false
-	IIfA:UpdateGuildBankData()
-	IIfA:CleanEmptyGuildBug()
-	IIfA:CollectGuildBank()
+	-- call with libAsync to avoid lags
+	task:Call(function()
+		IIfA:DebugOut("GuildBankReady...")
+		IIfA.isGuildBankReady = false
+		IIfA:UpdateGuildBankData()
+	end):Then(function()
+		IIfA:CleanEmptyGuildBug()
+	end):Then(function()
+		IIfA:CollectGuildBank()
+	end)
 end

 function IIfA:GuildBankDelayReady()
 	IIfA:DebugOut("GuildBankDelayReady...")
 	if not IIfA.isGuildBankReady then
 		IIfA.isGuildBankReady = true
-		zo_callLater(function() IIfA:GuildBankReady() end, 1750)
+		-- call with libAsync to avoid lags
+		task:Call(function()
+			IIfA:GuildBankReady()
+		end)
 	end
 end

 function IIfA:GuildBankAddRemove(eventID, slotNum)
 	IIfA:DebugOut("Guild Bank Add or Remove...")
-	IIfA:UpdateGuildBankData()
-	IIfA:CleanEmptyGuildBug()
+	-- call with libAsync to avoid lags
+	task:Call(function()
+		IIfA:UpdateGuildBankData()
+		IIfA:CleanEmptyGuildBug()
+	end):Then(function()
 	--IIfA:CollectGuildBank()
-	local dbItem, itemKey
-	if eventID == EVENT_GUILD_BANK_ITEM_ADDED then
---		d("GB Add")
-		dbItem, itemKey = IIfA:EvalBagItem(BAG_GUILDBANK, slotNum, true, 0)
-		IIfA:ValidateItemCounts(BAG_GUILDBANK, slotNum, dbItem, itemKey)
-	else
---		d("GB Remove")
---		d(GetItemLink(BAG_GUILDBANK, slotNum))
---		dbItem, itemKey = IIfA:EvalBagItem(BAG_BACKPACK, slotNum)
---		IIfA:ValidateItemCounts(BAG_GUILDBANK, slotNum, dbItem, itemKey)
-	end
+		local dbItem, itemKey
+		if eventID == EVENT_GUILD_BANK_ITEM_ADDED then
+	--		d("GB Add")
+			dbItem, itemKey = IIfA:EvalBagItem(BAG_GUILDBANK, slotNum, true, 0)
+			IIfA:ValidateItemCounts(BAG_GUILDBANK, slotNum, dbItem, itemKey)
+		else
+	--		d("GB Remove")
+	--		d(GetItemLink(BAG_GUILDBANK, slotNum))
+	--		dbItem, itemKey = IIfA:EvalBagItem(BAG_BACKPACK, slotNum)
+	--		IIfA:ValidateItemCounts(BAG_GUILDBANK, slotNum, dbItem, itemKey)
+		end
+	end)
 end

 function IIfA:ActionLayerInventoryUpdate()
@@ -244,7 +295,8 @@ function IIfA:InventorySlotUpdate(eventCode, bagId, slotNum, isNewItem, itemSoun
 		isNewItem = "False"
 	end

-	IIfA:DebugOut(zo_strformat("Inv Slot Upd - <<1>>, <<2>>, <<3>>, <<4>>, <<5>>, <<6>>, <<7>>", GetItemLink(bagId, slotNum, LINK_STYLE_NORMAL), eventCode, bagId, slotNum, inventoryUpdateReason, qty, isNewItem))
+	IIfA:DebugOut(zo_strformat("Inv Slot Upd - <<1>>, <<2>>, <<3>>, <<4>>, <<5>>, <<6>>, <<7>>",
+		GetItemLink(bagId, slotNum, LINK_STYLE_NORMAL), eventCode, bagId, slotNum, inventoryUpdateReason, qty, isNewItem))
 	local dbItem, itemKey
 	dbItem, itemKey = self:EvalBagItem(bagId, slotNum, true, qty)
 --	if dbItem ~= nil and (bagId == BAG_BANK or bagId == BAG_SUBSCRIBER_BANK or bagId == BAG_BACKPACK) then
@@ -261,8 +313,7 @@ function IIfA:RescanHouse(houseCollectibleId)

 	if not IIfA:GetTrackedBags()[houseCollectibleId] then return end

-	-- it's easier to throw everything away and re-scan than to conditionally update
-	IIfA:ClearLocationData(houseCollectibleId)
+

 	local function getAllPlacedFurniture()
 		local ret = {}
@@ -278,10 +329,16 @@ function IIfA:RescanHouse(houseCollectibleId)
 		end
 	end

-	local items = getAllPlacedFurniture()
-	for itemLink, itemCount in pairs(items) do
-		IIfA:AddFurnitureItem(itemLink, itemCount, houseCollectibleId, true)
-	end
+	-- call with libAsync to avoid lags
+	task:Call(function()
+		-- it's easier to throw everything away and re-scan than to conditionally update
+		IIfA:ClearLocationData(houseCollectibleId)
+	end):Then(function()
+		local items = getAllPlacedFurniture()
+		for itemLink, itemCount in pairs(items) do
+			IIfA:AddFurnitureItem(itemLink, itemCount, houseCollectibleId, true)
+		end
+	end)

 end

@@ -330,8 +387,15 @@ function IIfA:EvalBagItem(bagId, slotNum, fromXfer, itemCount, itemLink, itemNam
 	end

 	itemName = itemName or GetItemName(bagId, slotNum) or EMPTY_STRING
+	if itemName == "" then
+		if nil ~= IIfA.BagSlotInfo[bagId] and nil ~= IIfA.BagSlotInfo[bagId][slotNum] then
+			itemLink = IIfA.BagSlotInfo[bagId][slotNum]
+			itemName = GetItemLinkItemName(bagId, slotNum)
+		end
+	end
+

-	IIfA:DebugOut(zo_strformat("EvalBagItem - <<1>>/<<2>> <<3>>", bagId, slotNum, itemName))
+	IIfA:DebugOut(zo_strformat("EvalBagItem bagId/slotId: <<1>>/<<2>> itemName: <<3>>", bagId, slotNum, itemName))

 	if itemName > EMPTY_STRING then
 		itemLink = itemLink or GetItemLink(bagId, slotNum, LINK_STYLE_BRACKETS)
@@ -428,7 +492,7 @@ function IIfA:ValidateItemCounts(bagID, slotNum, dbItem, itemKey, itemLinkOverri
 	else
 		itemLink = itemKey
 	end
-	IIfA:DebugOut(zo_strformat("ValidateItemCounts: <<1>>x<<3>> in <<2>>", itemLink, bagID, slotNum))
+	IIfA:DebugOut(zo_strformat("ValidateItemCounts: <<1>> in bag <<2>>/<<3>>", itemLink, bagID, slotNum))

 	for locName, data in pairs(dbItem.locations) do
 --		if data.bagID ~= nil then	-- it's an item, not attribute
@@ -457,6 +521,8 @@ function IIfA:ValidateItemCounts(bagID, slotNum, dbItem, itemKey, itemLinkOverri
 			end
 --		end
 	end
+
+	IIfA:UpdateBSI(bagID, slotNum)
 end


@@ -468,35 +534,39 @@ function IIfA:CollectAll()
 	local BagList = IIfA:GetTrackedBags() -- 20.1. mana: Iterating over a list now

 	for bagId, tracked in ipairs(BagList) do
+		-- call with libAsync to avoid lags
+		task:Call(function()

-		bagItems = GetBagSize(bagId)
-		if(bagId == BAG_WORN)then	--location for BAG_BACKPACK and BAG_WORN is the same so only reset once
-			IIfA:ClearLocationData(IIfA.currentCharacterId)
-		elseif(bagId == BAG_BANK) then	-- do NOT add BAG_SUBSCRIBER_BANK here, it'll wipe whatever already got put into the bank on first hit
-			IIfA:ClearLocationData(GetString(IIFA_BAG_BANK))
-		elseif(bagId == BAG_VIRTUAL)then
-			IIfA:ClearLocationData(GetString(IIFA_BAG_CRAFTBAG))
-		elseif GetAPIVersion() >= 100022 then -- 20.1. mana: bag bag bag
-			local collectibleId = GetCollectibleForHouseBankBag(bagId)
-			if IsCollectibleUnlocked(collectibleId) then
-				local name = GetCollectibleNickname(collectibleId) or GetCollectibleName(collectibleId)
-				IIfA:ClearLocationData(name)
-			end
-		end
---		d("  BagItemCount=" .. bagItems)
-		if bagId ~= BAG_VIRTUAL and tracked then
-			for slotNum=0, bagItems, 1 do
-				dbItem, itemKey = IIfA:EvalBagItem(bagId, slotNum)
+			bagItems = GetBagSize(bagId)
+			if(bagId == BAG_WORN)then	--location for BAG_BACKPACK and BAG_WORN is the same so only reset once
+				IIfA:ClearLocationData(IIfA.currentCharacterId)
+			elseif(bagId == BAG_BANK) then	-- do NOT add BAG_SUBSCRIBER_BANK here, it'll wipe whatever already got put into the bank on first hit
+				IIfA:ClearLocationData(GetString(IIFA_BAG_BANK))
+			elseif(bagId == BAG_VIRTUAL)then
+				IIfA:ClearLocationData(GetString(IIFA_BAG_CRAFTBAG))
+			elseif GetAPIVersion() >= 100022 then -- 20.1. mana: bag bag bag
+				local collectibleId = GetCollectibleForHouseBankBag(bagId)
+				if IsCollectibleUnlocked(collectibleId) then
+					local name = GetCollectibleNickname(collectibleId) or GetCollectibleName(collectibleId)
+					IIfA:ClearLocationData(name)
+				end
 			end
-		else
-			if HasCraftBagAccess() then
-				slotNum = GetNextVirtualBagSlotId(nil)
-				while slotNum ~= nil do
-					IIfA:EvalBagItem(bagId, slotNum)
-					slotNum = GetNextVirtualBagSlotId(slotNum)
+	--		d("  BagItemCount=" .. bagItems)
+			if bagId ~= BAG_VIRTUAL and tracked then
+				for slotNum=0, bagItems, 1 do
+					dbItem, itemKey = IIfA:EvalBagItem(bagId, slotNum)
+				end
+			else
+				if HasCraftBagAccess() then
+					slotNum = GetNextVirtualBagSlotId(nil)
+					while slotNum ~= nil do
+						IIfA:EvalBagItem(bagId, slotNum)
+						slotNum = GetNextVirtualBagSlotId(slotNum)
+					end
 				end
 			end
-		end
+
+		end)
 	end

 	-- 6-3-17 AM - need to clear unowned items when deleting char/guildbank too
diff --git a/IIfA/IIfAEvents.lua b/IIfA/IIfAEvents.lua
index 948fb53..f53c4aa 100644
--- a/IIfA/IIfAEvents.lua
+++ b/IIfA/IIfAEvents.lua
@@ -16,18 +16,6 @@ local function IIfA_InventorySlotUpdate(...)
 	IIfA:InventorySlotUpdate(...)
 end

-local function IIfA_CollectibleUpdate(eventCode, collectibleId, justUnlocked)
-	if justUnlocked then return end
-	if GetAPIVersion() < 100022 then return end
-
-	local collectibleData = ZO_COLLECTIBLE_DATA_MANAGER:GetCollectibleDataById(collectibleId)
-    if not collectibleData:IsCategoryType(COLLECTIBLE_CATEGORY_TYPE_HOUSE_BANK) then return end
-	local name 		= GetCollectibleName(collectibleId)
-	local nickName 	= GetCollectibleNickname(collectibleId)
-
-
-end
-
 local function IIfA_ScanHouse(eventCode, oldMode, newMode)
 	if newMode == "showing" or newMode == "shown" then return end
 	-- are we listening?
@@ -117,9 +105,6 @@ function IIfA:RegisterForEvents()
 -- not helpful, no link at all on this callback
 --	SHARED_INVENTORY:RegisterCallback("SlotRemoved", IIfA_EventDump)
 --	SHARED_INVENTORY:RegisterCallback("SingleSlotInventoryUpdate", IIfA_EventDump)
-
-	-- react to players possibly renaming their storage chests
-	em:RegisterForEvent("IIFA_Collectible_Updated", 	EVENT_COLLECTIBLE_UPDATED, IIfA_CollectibleUpdate)


 	-- Events for data collection
diff --git a/IIfA/libs/LibAsync/LibAsync.lua b/IIfA/libs/LibAsync/LibAsync.lua
new file mode 100644
index 0000000..77100f0
--- /dev/null
+++ b/IIfA/libs/LibAsync/LibAsync.lua
@@ -0,0 +1,340 @@
+local MAJOR, MINOR = "LibAsync", 1.4
+local async, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+if not async then return end -- the same or newer version of this lib is already loaded into memory
+
+if async.Unload then
+	async:Unload()
+end
+
+local em = GetEventManager()
+local remove, min = table.remove, math.min
+
+local function RemoveCall(job, callstackIndex)
+	remove(job.callstack, callstackIndex)
+	job.lastCallIndex = min(job.lastCallIndex, #job.callstack)
+end
+
+local current, call
+local function safeCall() return call(current) end
+
+local function DoCallback(job, callstackIndex)
+	local success, shouldContinue = pcall(safeCall)
+	if success then
+		-- If the call returns true, the call wants to be called again
+		if not shouldContinue then RemoveCall(job, callstackIndex) end
+	else
+		-- shouldContinue is the value returned by error or assert
+		job.Error = shouldContinue
+		RemoveCall(job, callstackIndex)
+
+		call = job.onError
+		if call then
+			pcall(safeCall)
+		else
+			job:Suspend()
+			error(job.Error)
+		end
+	end
+end
+
+local jobs = async.jobs or { }
+async.jobs = jobs
+-- async.registered = { }
+
+local function DoJob(job)
+	current = job
+	local index = #job.callstack
+	call = job.callstack[index]
+	if call then
+		DoCallback(job, index)
+	else
+		assert(index == 0, "No call on non-empty stack?!")
+		jobs[job.name] = nil
+		call = job.finally
+		if call then pcall(safeCall) end
+	end
+	current, call = nil, nil
+end
+
+-- time we can spend until the next frame must be shown
+local frameTimeTarget = 13
+-- we allow a function to use 25% of the frame time before it gets critical
+local spendTimeDef = frameTimeTarget * 0.75
+local spendTimeDefNoHUD = spendTimeDef * 1.54
+local spendTime = spendTimeDef
+
+local debug = false
+
+local GetFrameTimeMilliseconds, GetGameTimeMilliseconds = GetFrameTimeMilliseconds, GetGameTimeMilliseconds
+local identifier = "ASYNCTASKS_JOBS"
+
+local function GetThreshold()
+	return(HUD_SCENE:IsShowing() or HUD_UI_SCENE:IsShowing()) and spendTimeDef or spendTimeDefNoHUD
+end
+
+local job = nil
+local function Scheduler()
+	local start = GetFrameTimeMilliseconds()
+	local runTime = start
+	if (GetGameTimeMilliseconds() - start) > spendTime then
+		spendTime = 750 / GetFramerate()
+		if debug then
+			df("initial gap: %ims. skip. new threshold: %ims", GetGameTimeMilliseconds() - start, spendTime)
+		end
+		return
+	end
+	if debug then
+		df("initial gap: %ims", GetGameTimeMilliseconds() - start)
+	end
+	local name
+	while (GetGameTimeMilliseconds() - start) <= spendTime do
+		name, job = next(jobs)
+		if job then
+			runTime = GetGameTimeMilliseconds()
+			DoJob(job)
+		else
+			-- Finished
+			em:UnregisterForUpdate(identifier)
+			spendTime = GetThreshold()
+		return
+		end
+	end
+	spendTime = GetThreshold()
+	if debug and job then
+		local now = GetGameTimeMilliseconds()
+		local freezeTime = now - start
+		if freezeTime >= 16 then
+			runTime = now - runTime
+			df("%s freeze. used %ims, resulting frametime %ims.", job.name, runTime, freezeTime)
+		end
+	end
+end
+
+function async:GetDebug()
+	return debug
+end
+
+function async:SetDebug(enabled)
+	debug = enabled
+end
+
+-- Class task
+
+local task = async.task or ZO_Object:Subclass()
+async.task = task
+
+-- Called from async:Create()
+function task:New(name)
+	local instance = ZO_Object.New(self)
+	instance.name = name or tostring(instance)
+	instance:Initialize()
+	return instance
+end
+
+function task:Initialize()
+	self.callstack = { }
+	self.lastCallIndex = 0
+	-- async.registered[#async.registered + 1] = self
+end
+
+-- Resume the execution context.
+function task:Resume()
+	jobs[self.name] = self
+	em:RegisterForUpdate(identifier, 0, Scheduler)
+	return self
+end
+
+-- Suspend the execution context and allow to resume anytime later.
+function task:Suspend()
+	jobs[self.name] = nil
+	return self
+end
+
+-- Interupt and fully stop the execution context. Can be called from outside to stop everything.
+function task:Cancel()
+	ZO_ClearNumericallyIndexedTable(self.callstack)
+	self.lastCallIndex = 0
+	if jobs[self.name] then
+		if not self.finally then
+			jobs[self.name] = nil
+			-- else run job with empty callstack to run finalizer
+		end
+	end
+	return self
+end
+
+do
+	-- Run the given FuncOfTask in your task context execution.
+	function task:Call(funcOfTask)
+		self.lastCallIndex = #self.callstack + 1
+		self.callstack[self.lastCallIndex] = funcOfTask
+		return self:Resume()
+	end
+
+	local insert = table.insert
+	-- Continue your task context execution with the given FuncOfTask after the previous as finished.
+	function task:Then(funcOfTask)
+		assert(self.lastCallIndex > 0 and self.lastCallIndex <= #self.callstack, "cap!")
+		insert(self.callstack, self.lastCallIndex, funcOfTask)
+		return self
+	end
+end
+
+-- Start an interruptible for-loop.
+function task:For(p1, p2, p3)
+	-- If called as a normal job, false will prevent it is kept in callstack doing an endless loop
+	self.callstack[#self.callstack + 1] = function() return false, p1, p2, p3 end
+	return self
+end
+
+do
+	local function ForConditionAlreadyFalse() end
+	local function ContinueForward(index, endIndex) return index <= endIndex end
+	local function ContinueBackward(index, endIndex) return index >= endIndex end
+
+	local function asyncForWithStep(self, func, index, endIndex, step)
+		step = step or 1
+		if step == 0 then error("step is zero") end
+
+		local ShouldContinue
+		if step > 0 then
+			if index > endIndex then return ForConditionAlreadyFalse end
+			ShouldContinue = ContinueForward
+		else
+			if index < endIndex then return ForConditionAlreadyFalse end
+			ShouldContinue = ContinueBackward
+		end
+		return function()
+			if func(index) ~= async.BREAK then
+				index = index + step
+				return ShouldContinue(index, endIndex)
+			end
+		end
+	end
+
+	local function asyncForPairs(self, func, iter, list, key)
+		return function()
+			local value
+			key, value = iter(list, key)
+			return key and func(key, value) ~= async.BREAK
+		end
+	end
+
+	-- Execute the async-for with the given step-function. The parameters of the step-function are those you would use in your for body.
+	function task:Do(func)
+		local callstackIndex = #self.callstack
+		local shouldBeFalse, p1, p2, p3 = self.callstack[callstackIndex]()
+		assert(shouldBeFalse == false and p1, "Do without For")
+		remove(self.callstack, callstackIndex)
+
+		local DoLoop = type(p1) == "number" and
+		asyncForWithStep(self, func, p1, p2, p3) or
+		asyncForPairs(self, func, p1, p2, p3)
+
+		if current or #self.callstack == 0 then return self:Call(DoLoop) else return self:Then(DoLoop) end
+	end
+end
+
+-- Suspend the execution of your task context for the given delay in milliseconds and then call the given FuncOfTask to continue.
+function task:Delay(delay, funcOfTask)
+	self:StopTimer()
+	if delay < 10 then return self:Call(funcOfTask) end
+	self:Suspend()
+	em:RegisterForUpdate(self.name, delay, function()
+		em:UnregisterForUpdate(self.name)
+		self:Call(funcOfTask)
+	end )
+	return self
+end
+
+-- Stop the delay created by task:Delay or task:Interval.
+function task:StopTimer()
+	em:UnregisterForUpdate(self.name)
+	return self
+end
+
+-- Set a FuncOfTask as a final handler. If you call Called if something went wrong in your context.
+function task:Finally(funcOfTask)
+	self.finally = funcOfTask
+	return self
+end
+
+-- Set a FuncOfTask as an error handler. Called if something went wrong in your context.
+function task:OnError(funcOfTask)
+	self.onError = funcOfTask
+	return self
+end
+
+do
+	-- Thanks to: https://de.wikipedia.org/wiki/Quicksort
+
+	local function simpleCompare(a, b) return a < b end
+	local function sort(task, array, compare)
+		local function quicksort(left, right)
+			if left >= right then return end
+
+			-- partition
+			local i, j, pivot = left, right - 1, array[right]
+
+			task:Call( function()
+				while i < right and compare(array[i], pivot) do i = i + 1 end
+				while j > left and not compare(array[j], pivot) do j = j - 1 end
+				if i < j then
+					array[i], array[j] = array[j], array[i]
+					-- repeatly call this function until i >= j
+					return true
+				end
+			end )
+			task:Then( function()
+				if compare(pivot, array[i]) then array[i], array[right] = array[right], array[i] end
+				quicksort(left, i - 1)
+				quicksort(i + 1, right)
+			end )
+		end
+		quicksort(1, #array)
+	end
+
+	-- This sort function works like table.sort(). The compare function is optional.
+	function task:Sort(array, compare)
+		local sortJob = function(task) sort(task, array, compare or simpleCompare) end
+		if current or #self.callstack == 0 then return self:Call(sortJob) else return self:Then(sortJob) end
+	end
+end
+
+-- Class async
+
+-- Get the current context, if you are within a FuncOfTask or nil.
+function async:GetCurrent()
+	return current
+end
+
+-- Create an interruptible task context.
+function async:Create(name)
+	return task:New(name)
+end
+
+do
+	local Default = task:New("*Default Task*")
+	function Default:Cancel() error("Not allowed on default task. Use your_lib_var:Create(optional_name) for an interruptible task context.") end
+	Default.Finally = Default.Cancel
+	Default.OnError = Default.Cancel
+
+	-- Start a non-interruptible task or start a nested call in the current context.
+	function async:Call(funcOfTask)
+		-- if async:Call is called from within a task callback (the moment where GetCurrent() is not nil) use it for nested calls
+		return(async:GetCurrent() or Default):Call(funcOfTask)
+	end
+	-- Start a non-interruptible for-loop or start a nested for-loop in the current context.
+	function async:For(p1, p2, p3)
+		return(self:GetCurrent() or Default):For(p1, p2, p3)
+	end
+
+	-- Start a non-interruptible sort or start a nested sort in the current context.
+	function async:Sort(array, compare)
+		return(self:GetCurrent() or Default):Sort(array, compare)
+	end
+end
+
+-- async.BREAK is the new 'break' for breaking loops. As Lua would not allowed the keyword 'break' in that context.
+-- To break a for-loop, return async.BREAK
+async.BREAK = true