Version 1.0.0

willneedit [03-15-17 - 16:47]
Version 1.0.0
Filename
CraftStoreLink.lua
InventoryManager.lua
InventoryManager.txt
Modules/Banking.lua
Modules/Data.lua
Modules/Junker.lua
Modules/Seller.lua
README.md
Rulesets.lua
TODO
UI/ProfileEdit.lua
UI/RuleEdit.lua
UI/Settings.lua
ZoS Disclosure
lang/de.lua
lang/en.lua
libs/LibAddonMenu-2.0/LICENSE
libs/LibAddonMenu-2.0/LibAddonMenu-2.0.lua
libs/LibAddonMenu-2.0/controls/button.lua
libs/LibAddonMenu-2.0/controls/checkbox.lua
libs/LibAddonMenu-2.0/controls/colorpicker.lua
libs/LibAddonMenu-2.0/controls/custom.lua
libs/LibAddonMenu-2.0/controls/description.lua
libs/LibAddonMenu-2.0/controls/divider.lua
libs/LibAddonMenu-2.0/controls/dropdown.lua
libs/LibAddonMenu-2.0/controls/editbox.lua
libs/LibAddonMenu-2.0/controls/header.lua
libs/LibAddonMenu-2.0/controls/iconpicker.lua
libs/LibAddonMenu-2.0/controls/panel.lua
libs/LibAddonMenu-2.0/controls/slider.lua
libs/LibAddonMenu-2.0/controls/submenu.lua
libs/LibAddonMenu-2.0/controls/texture.lua
libs/LibItemInfo-1.0/LibItemInfo-1.0.lua
libs/LibItemInfo-1.0/LibItemInfo-1.0.txt
libs/LibLoadedAddons/LibLoadedAddons.lua
libs/LibLoadedAddons/LibLoadedAddons.txt
libs/LibMsgWin-1.0/LibMsgWin-1.0.lua
libs/LibMsgWin-1.0/LibMsgWin-1.0.txt
libs/LibNeed4Research/LibNeed4Research.lua
libs/LibNeed4Research/LibNeed4Research.txt
libs/LibStub/LibStub.lua
diff --git a/CraftStoreLink.lua b/CraftStoreLink.lua
new file mode 100644
index 0000000..84258f1
--- /dev/null
+++ b/CraftStoreLink.lua
@@ -0,0 +1,90 @@
+local DEBUG =
+function() end
+-- d
+
+local CSL = {}
+
+InventoryManager.CSL = CSL
+
+local function SplitLink(link,nr)
+	local split = {SplitString(':', link)}
+	if split[nr] then return tonumber(split[nr]) else return false end
+end
+
+function CSL:hasCSAddon()
+	return CS and CS.GetTrait and CS.account and CS.account.crafting and true
+end
+
+function CSL:IsTraitNeeded(itemLink)
+	local need = { }
+	local craft, row, trait = CS.GetTrait(itemLink)
+	-- Loop all chars known by CS
+	for char, data in pairs(CS.account.crafting.studies) do
+		--if a char study this item
+		if data[craft] and data[craft][row] and (data[craft][row]) then
+			-- If this char didn't yet researched this item
+			local csr = CS.account.crafting.research
+			if csr[char][craft] and csr[char][craft][row] and csr[char][craft][row][trait] == false then
+				need[char] = true
+				need[#need + 1] = char
+			end
+		end
+	end
+	return need
+end
+
+local CURRENT_PLAYER = GetUnitName("player")
+
+function CSL:IsStyleNeeded(link)
+	local id, need = SplitLink(link,3), { }
+	if id then
+		for _, char in pairs(CS.GetCharacters()) do
+			if CS.account.style.tracking[char] and not CS.account.style.knowledge[char][id] then
+				need[char] = true
+				need[#need + 1] = char
+			end
+		end
+	end
+	return need
+end
+
+function CSL:IsRecipeNeeded(link)
+	local id, need = SplitLink(link,3), { }
+	if id then
+		for char,data in pairs(CS.account.cook.knowledge) do
+			if not data[id] and CS.account.cook.tracking[char] then
+				need[char] = true
+				need[#need + 1] = char
+			end
+		end
+	end
+	return need
+end
+
+function CSL:isUnknown(itemLink)
+	local chars
+	local itemType
+
+	if not CSL:hasCSAddon() then
+		return false, false
+	end
+
+	itemType, _ = GetItemLinkItemType(itemLink)
+	if itemType == ITEMTYPE_RECIPE then
+		chars = CSL:IsRecipeNeeded(itemLink)
+	elseif itemType == ITEMTYPE_RACIAL_STYLE_MOTIF then
+		chars = CSL:IsStyleNeeded(itemLink)
+	elseif itemType == ITEMTYPE_WEAPON or itemType == ITEMTYPE_ARMOR then
+		chars = CSL:IsTraitNeeded(itemLink)
+	end
+
+	if not chars then
+		return false, false
+	end
+
+	local oneself = (chars[CURRENT_PLAYER] or false)
+	local numothers = #chars - ((oneself and 1) or 0)
+	local others = numothers > 0
+	DEBUG(oneself, #chars, numothers, others)
+	return oneself, others
+end
diff --git a/InventoryManager.lua b/InventoryManager.lua
new file mode 100644
index 0000000..74aff0d
--- /dev/null
+++ b/InventoryManager.lua
@@ -0,0 +1,300 @@
+local DEBUG =
+function() end
+-- d
+
+local function _tr(str)
+	return str
+end
+
+InventoryManager = {}
+
+InventoryManager.LAM = LibStub:GetLibrary("LibAddonMenu-2.0")
+
+InventoryManager.name = "InventoryManager"
+InventoryManager.loadedAddons = {}
+
+-- The current inventory we're working on
+InventoryManager.currentInventory = nil
+InventoryManager.currentBagType = nil
+
+-- The current ruleset we're working with
+InventoryManager.currentRuleset = { }
+
+function InventoryManager:ReportAction(data, dryrun, action, rIndex, rString)
+	local index = (dryrun and 0) or 1
+	CHAT_SYSTEM:AddMessage(zo_strformat(GetString("IM_TAKENACTION", index),
+			GetString("IM_ACTIONTXT",action),
+			data.icon,
+			data.lnk,
+			rIndex or "",
+			rString or ""))
+end
+
+function InventoryManager:SetCurrentInventory(bagType)
+	self.currentInventory = SHARED_INVENTORY:GetOrCreateBagCache(bagType)
+
+	self.currentBagType = bagType
+
+end
+
+function InventoryManager:SetShownInventory()
+	local bagType = nil
+
+	if SCENE_MANAGER.currentScene == SCENE_MANAGER.scenes.inventory then
+		bagType = BAG_BACKPACK
+	elseif SCENE_MANAGER.currentScene == SCENE_MANAGER.scenes.bank then
+		if INVENTORY_FRAGMENT:IsShowing() then
+			bagType = BAG_BACKPACK
+		elseif BANK_FRAGMENT:IsShowing() then
+			bagType = BAG_BANK
+		end
+	end
+
+	if not bagType then
+		return nil
+	end
+	self:SetCurrentInventory(bagType)
+	return bagType
+end
+
+function InventoryManager:GetItemData(slotId, _inv)
+	local data = {}
+	local inv = nil
+
+	if _inv then
+		inv = _inv
+	else
+		inv = self.currentInventory
+	end
+
+	if not inv or not inv[slotId] then
+		return nil
+	end
+
+	local itemLink = inv[slotId].lnk
+
+	data.name = inv[slotId].name
+	data.lnk = itemLink
+	data.itemInstanceId = inv[slotId].itemInstanceId
+
+	data.count, data.maxCount = GetSlotStackSize(self.currentBagType, slotId)
+	data.locked = IsItemPlayerLocked(self.currentBagType, slotId)
+	data.junk = IsItemJunk(self.currentBagType, slotId)
+
+	data.itemType, data.specialitemtype = GetItemLinkItemType(itemLink)
+	data.value = GetItemLinkValue(itemLink, false)
+
+	data.icon, _, _, data.equipType, data.itemStyle = GetItemLinkInfo(itemLink)
+
+	if data.itemType == ITEMTYPE_ARMOR then
+		data.armorType = GetItemLinkArmorType(itemLink)
+	elseif data.itemType == ITEMTYPE_WEAPON then
+		data.weaponType = GetItemLinkWeaponType(itemLink)
+	end
+
+	data.traitType = GetItemLinkTraitInfo(itemLink)
+
+	data.isSet, data.set = GetItemLinkSetInfo(itemLink)
+
+	data.quality = GetItemLinkQuality(itemLink)
+	data.stolen = IsItemLinkStolen(itemLink)
+
+	data.unknownself, data.unknownothers = self.CSL:isUnknown(itemLink)
+	return data
+end
+
+function InventoryManager:listrules()
+	CHAT_SYSTEM:AddMessage(GetString("IM_LIST_NUM_RULES") .. #InventoryManager.currentRuleset.rules)
+
+	for i = 1, #InventoryManager.currentRuleset.rules, 1 do
+		if not InventoryManager.currentRuleset.rules[i] then
+			break
+		end
+		CHAT_SYSTEM:AddMessage(GetString("IM_LIST_RULE") .. i .. ":" .. InventoryManager.currentRuleset.rules[i]:ToString())
+	end
+end
+
+function InventoryManager:dryrun()
+	self:WorkBackpack(true)
+end
+
+function InventoryManager:help()
+	-- self:SetCurrentInventory(BAG_BACKPACK)
+	-- for i, entry in pairs(self.currentInventory) do
+		-- local knownString = ""
+		-- local oneself, others = self.CSL:isUnknown(entry.lnk)
+		-- if oneself then
+			-- knownString = " (unknown to you)"
+		-- end
+		-- if others then
+			-- knownString = knownString .. " (unknown to others)"
+		-- end
+		-- CHAT_SYSTEM:AddMessage("  Item " .. i .. ": " .. entry.lnk .. knownString);
+	-- end
+	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")
+end
+
+function InventoryManager:SlashCommand(argv)
+    local options = {}
+    local searchResult = { string.match(argv,"^(%S*)%s*(.-)$") }
+    for i,v in pairs(searchResult) do
+        if (v ~= nil and v ~= "") then
+            options[i] = string.lower(v)
+        end
+    end
+
+	if #options == 0 or options[1] == "help" then
+		self:help()
+	elseif options[1] == "listrules" then
+		self:listrules()
+	elseif options[1] == "dryrun" then
+		self:dryrun()
+	else
+		CHAT_SYSTEM:AddMessage("Unknown parameter '" .. argv .. "'")
+	end
+end
+
+InventoryManager.UI = { }
+InventoryManager.UI.RuleEdit = { }
+InventoryManager.UI.ProfileEdit = { }
+InventoryManager.UI.Settings = { }
+
+local RuleEdit = InventoryManager.UI.RuleEdit
+local ProfileEdit = InventoryManager.UI.ProfileEdit
+local Settings = InventoryManager.UI.Settings
+
+function InventoryManager:InitializeUI()
+	local panelData = {
+		type = "panel",
+		name = "InventoryManager",
+		registerForRefresh = true,	--boolean (optional) (will refresh all options controls when a setting is changed and when the panel is shown)
+	}
+
+	local mainPanel = {
+		{
+			type = "submenu",
+			name = GetString("IM_UI_PM"),
+			tooltip = GetString("IM_UI_PM_TOOLTIP"),	--(optional)
+			controls = ProfileEdit:GetControls(),
+		},
+		{
+			type = "submenu",
+			name = GetString("IM_UI_RM"),
+			tooltip = GetString("IM_UI_RM_TOOLTIP"),	--(optional)
+			controls = RuleEdit:GetControls(),
+		},
+		{
+			type = "submenu",
+			name = GetString("IM_UI_SETTINGS"),
+			tooltip = GetString("IM_UI_SETTINGS_TOOLTIP"),	--(optional)
+			controls = Settings:GetControls(),
+		},
+
+	}
+
+	self.UI.panel = self.LAM:RegisterAddonPanel("iwontsayInventoryManager", panelData)
+	self.LAM:RegisterOptionControls("iwontsayInventoryManager", mainPanel)
+end
+
+local function ctorandload(ctor, ctorob, data)
+	local _new = ctor(ctorob)
+	for k,v in pairs(data) do
+		if v then _new[k] = v end
+	end
+	return _new
+end
+
+local function loadRule(ruleData)
+	return ctorandload(InventoryManager.IM_Ruleset.NewRule, InventoryManager.IM_Ruleset, ruleData)
+end
+
+local function loadRulelist(rulelistData)
+	local _new = { }
+	for k,v in pairs(rulelistData) do
+		_new[k] = loadRule(v)
+	end
+	return _new
+end
+
+local function loadProfile(profileData)
+	local _new = { }
+	for k,v in pairs(profileData) do
+		_new[k] = { }
+		_new[k]["name"] = v["name"]
+		_new[k]["rules"] = loadRulelist(v["rules"])
+	end
+	return _new
+end
+
+function InventoryManager:Init()
+	self.currentRuleset			= self.IM_Ruleset:New()
+
+	self.charDefaults = {
+		["currentRules"]	= self.currentRuleset["rules"],
+		["settings"]		= {
+			["destroyThreshold"]	= 5,
+			["bankMoveDelay"]		= 20,
+			["maxGold"]				= 5000,
+			["minGold"]				= 1000,
+			["maxTV"]				= 10,
+			["minTV"]				= 0
+		}
+	}
+
+	self.accDefaults = {
+		["Profiles"] = { }
+	}
+
+	self.accVariables = ZO_SavedVars:NewAccountWide(
+		"IMSavedVars",
+		1,
+		nil,
+		self.accDefaults)
+
+	self.charVariables = ZO_SavedVars:New(
+		"IMSavedVars",
+		1,
+		nil,
+		self.charDefaults)
+
+	self.Profiles 				= loadProfile(self.accVariables.Profiles)
+	self.currentRuleset.rules	= loadRulelist(self.charVariables.currentRules)
+	self.settings				= self.charVariables.settings
+
+	self.presetProfiles			= loadProfile(self.presetProfiles)
+
+	self:InitializeUI()
+
+	CHAT_SYSTEM:AddMessage(self.name .. " Addon Loaded.")
+	CHAT_SYSTEM:AddMessage("Use /im help for an overview")
+end
+
+function InventoryManager:Save()
+	self.charVariables.settings		= self.settings
+	self.charVariables.currentRules	= self.currentRuleset.rules
+	self.charVariables.Profiles		= nil
+
+	self.accVariables.Profiles 		= self.Profiles
+	self.accVariables.currentRules	= nil
+end
+
+local function OnAddOnLoaded(eventCode, addonName)
+	if addonName == InventoryManager.name then
+		InventoryManager:Init()
+	else
+		InventoryManager.loadedAddons[addonName] = true
+	end
+end
+
+local function OnPCCreated()
+	RuleEdit:PopulateUI()
+	ProfileEdit:PopulateUI()
+	Settings:PopulateUI()
+end
+
+EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_ADD_ON_LOADED, OnAddOnLoaded)
+
+SLASH_COMMANDS["/im"] = function(argv) InventoryManager:SlashCommand(argv) end
+
+CALLBACK_MANAGER:RegisterCallback("LAM-PanelControlsCreated", OnPCCreated)
diff --git a/InventoryManager.txt b/InventoryManager.txt
new file mode 100644
index 0000000..ae32cd3
--- /dev/null
+++ b/InventoryManager.txt
@@ -0,0 +1,40 @@
+## Title: InventoryManager
+## APIVersion: 100018
+## OptionalDependsOn: LibAddonMenu-2.0
+## SavedVariables: IMSavedVars
+## Version: 1.0.0
+## Author: iwontsay
+## Description: iwontsay's Inventory Manager
+
+libs/LibStub/LibStub.lua
+
+libs/LibLoadedAddons/LibLoadedAddons.lua
+
+libs/LibAddonMenu-2.0/LibAddonMenu-2.0.lua
+libs/LibAddonMenu-2.0/controls/panel.lua
+libs/LibAddonMenu-2.0/controls/submenu.lua
+libs/LibAddonMenu-2.0/controls/button.lua
+libs/LibAddonMenu-2.0/controls/checkbox.lua
+libs/LibAddonMenu-2.0/controls/colorpicker.lua
+libs/LibAddonMenu-2.0/controls/custom.lua
+libs/LibAddonMenu-2.0/controls/description.lua
+libs/LibAddonMenu-2.0/controls/dropdown.lua
+libs/LibAddonMenu-2.0/controls/editbox.lua
+libs/LibAddonMenu-2.0/controls/header.lua
+libs/LibAddonMenu-2.0/controls/slider.lua
+libs/LibAddonMenu-2.0/controls/texture.lua
+
+InventoryManager.lua
+Modules/Data.lua
+Modules/Banking.lua
+Modules/Junker.lua
+Modules/Seller.lua
+CraftStoreLink.lua
+Rulesets.lua
+UI/RuleEdit.lua
+UI/ProfileEdit.lua
+UI/Settings.lua
+
+lang/en.lua
+lang/$(language).lua
+
diff --git a/Modules/Banking.lua b/Modules/Banking.lua
new file mode 100644
index 0000000..849b038
--- /dev/null
+++ b/Modules/Banking.lua
@@ -0,0 +1,290 @@
+local DEBUG =
+-- function() end
+d
+
+local function _tr(str)
+	return str
+end
+
+local ST_OK = 0
+local ST_TGTFULL = 1
+local ST_SPAM = 2
+
+local InvCache = nil
+local Moves = nil
+
+local function ScanInventory(bagType)
+	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
+		else
+			local curStack, maxStack = GetSlotStackSize(bagType, i)
+			_stackCount[i] = {
+				["id"] = inv[i].itemInstanceId,
+				["current"] = curStack,
+				["max"] = maxStack
+			}
+		end
+	end
+	-- Empties is a list of empty slots, items an overview over stack counts in specific slots
+	return { ["empties"] = _empties, ["items"] = _stackCount }
+end
+
+local function FindTargetSlot(srcBagType, srcSlotId, tgtBagType)
+	local srcInv = InvCache[srcBagType]
+	local tgtInv = InvCache[tgtBagType]
+
+	local iId = srcInv["items"][srcSlotId]["id"]
+	local count = srcInv["items"][srcSlotId]["current"]
+
+	-- Try to fill up existing stacks, return even incomplete transfers doing so
+	for k,v in pairs(tgtInv["items"]) do
+		local missing = v["max"] - v["current"]
+		if iId == v["id"] and missing > 0 then
+			local empties = missing >= count
+			return empties, false, k, (empties and count) or missing
+		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
+	end
+
+	-- We'd start another stack, but we're sure it'll be a complete transfer
+	return true, true, emptyslots[#emptyslots], count
+end
+
+
+local function CollectSingleDirection(action)
+	local bagType = (action == InventoryManager.IM_Ruleset.ACTION_STASH and BAG_BACKPACK) or BAG_BANK
+	local _moveSlots = { }
+
+	InventoryManager:SetCurrentInventory(bagType)
+
+	for i,_ in pairs(InventoryManager.currentInventory) do
+		local data = InventoryManager:GetItemData(i)
+		if action == InventoryManager.currentRuleset:Match(data) then
+			_moveSlots[#_moveSlots + 1] = i
+		end
+	end
+	return _moveSlots
+end
+
+local function PrepareMoveCaches()
+	InvCache = {
+		[BAG_BACKPACK] = ScanInventory(BAG_BACKPACK),
+		[BAG_BANK] = ScanInventory(BAG_BANK)
+	}
+
+	Moves = {
+		["stash"] = CollectSingleDirection(InventoryManager.IM_Ruleset.ACTION_STASH),
+		["retrieve"] = CollectSingleDirection(InventoryManager.IM_Ruleset.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 srcSlotRepo = Moves[(direction == 1 and "stash") or "retrieve"]
+	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
+
+	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
+	else
+		-- Stashed onto existing stack, increase count
+		local tgtslot = InvCache[tgtBagType]["items"][tgtSlotId]
+		tgtslot["current"] = tgtslot["current"] + count
+	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
+	PrepareMoveCaches()
+	local continue = true
+	local _moveStack = { }
+
+	-- We alternate between stashing and retrieving to minimize the chance of one
+	-- of the inventories running full.
+	while continue do
+		local leftres, leftentry = CalculateSingleMove(1)
+		local rightres, rightentry = CalculateSingleMove(-1)
+
+		-- 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
+
+-- Pending slots for time delayed actions
+InventoryManager.pendingMoves = nil
+InventoryManager.moveStatus = nil
+
+function InventoryManager:ProcessMoves()
+	if not self.pendingMoves or self.currentMove > #self.pendingMoves then
+		return self:FinishMoves()
+	end
+
+	local IMR = InventoryManager.IM_Ruleset
+
+	local move = self.pendingMoves[self.currentMove]
+
+	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 IMR.ACTION_STASH) or IMR.ACTION_RETRIEVE
+
+	local data = self:GetItemData(slotIdFrom, SHARED_INVENTORY:GetOrCreateBagCache(bagIdFrom))
+	self:ReportAction(data, false, action)
+
+	if IsProtectedFunction("RequestMoveItem") then
+		CallSecureProtected("RequestMoveItem", bagIdFrom, slotIdFrom, bagIdTo, slotIdTo, qtyToMove)
+	else
+		RequestMoveItem(bagIdFrom, slotIdFrom, bagIdTo, slotIdTo, qtyToMove)
+	end
+
+	self.currentMove = self.currentMove + 1
+	zo_callLater(
+		function() InventoryManager:ProcessMoves() end,
+		InventoryManager.settings.bankMoveDelay)
+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, curName)
+	local carried = GetCarriedCurrencyAmount(currencyType)
+	local banked = GetBankedCurrencyAmount(currencyType)
+
+	local move = 0
+	if(carried < minCur) then
+		move = carried - minCur
+	elseif(carried > maxCur) then
+		move = carried - maxCur
+	end
+
+	if move == 0 then
+		return
+	elseif move > 0 then
+		CHAT_SYSTEM:AddMessage(
+			zo_strformat(GetString("IM_CUR_DEPOSIT"), move, curName))
+		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"), move, curName))
+		WithdrawCurrencyFromBank(currencyType, move)
+	end
+end
+
+function InventoryManager:OnBankOpened()
+	self.moveStatus, self.pendingMoves = CalculateMoves()
+	self.currentMove = 1
+
+	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"))
+
+	zo_callLater(function() InventoryManager:ProcessMoves() end, 100)
+end
+
+local function OnBankOpened(eventCode)
+	InventoryManager:OnBankOpened()
+end
+
+EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_OPEN_BANK, OnBankOpened)
diff --git a/Modules/Data.lua b/Modules/Data.lua
new file mode 100644
index 0000000..65ac4ff
--- /dev/null
+++ b/Modules/Data.lua
@@ -0,0 +1,314 @@
+
+local function generateFiltertypes()
+	local _new = { }
+	for _, f in pairs(InventoryManager.filterorder) do
+		local _innernew = { }
+		for _, ff in pairs(f[2]) do
+			_innernew[ff[1]] = ff[2]
+		end
+		_new[f[1]] = _innernew
+	end
+	return _new
+end
+
+InventoryManager.IM_Ruleset = { }
+
+local IM_Ruleset = InventoryManager.IM_Ruleset
+
+function InventoryManager:getIQString(itemQuality)
+	return GetItemQualityColor(itemQuality):Colorize(GetString("SI_ITEMQUALITY", itemQuality))
+end
+
+IM_Ruleset.ACTION_KEEP		=  0
+IM_Ruleset.ACTION_JUNK		=  1
+IM_Ruleset.ACTION_DESTROY 	=  2
+IM_Ruleset.ACTION_STASH		= 10
+IM_Ruleset.ACTION_RETRIEVE	= 20
+
+IM_Ruleset.ITEM_TRAIT_TYPE_ANY				= -1
+IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKOTHERS		= -2
+IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKNOWN		= -3
+
+
+InventoryManager.filterorder = {
+		{ "IM_FILTER_ANY" 			, {
+			{ "IM_FILTERSPEC_ANY"			, { } },
+		} },
+		{ "IM_FILTER_WEAPON" 		, {
+			{ "IM_FILTERSPEC_ANY"			, { "itemType", ITEMTYPE_WEAPON } },
+			{ "IM_FILTERSPEC_1H"			, { "weaponType", WEAPONTYPE_AXE, WEAPONTYPE_HAMMER, WEAPONTYPE_SWORD, WEAPONTYPE_DAGGER } },
+			{ "IM_FILTERSPEC_2H"			, { "weaponType", WEAPONTYPE_TWO_HANDED_AXE, WEAPONTYPE_TWO_HANDED_HAMMER, WEAPONTYPE_TWO_HANDED_SWORD } },
+			{ "IM_FILTERSPEC_BOW"			, { "weaponType", WEAPONTYPE_BOW } },
+			{ "IM_FILTERSPEC_STAFF_DEST"	, { "weaponType", WEAPONTYPE_FIRE_STAFF, WEAPONTYPE_FROST_STAFF, WEAPONTYPE_LIGHTNING_STAFF } },
+			{ "IM_FILTERSPEC_STAFF_HEAL"	, { "weaponType", WEAPONTYPE_HEALING_STAFF } }
+		} },
+		{ "IM_FILTER_APPAREL"		, {
+			{ "IM_FILTERSPEC_ANY"			, { "itemType", ITEMTYPE_ARMOR } },
+			{ "IM_FILTERSPEC_ANY_BODY"		, { "armorType", ARMORTYPE_HEAVY, ARMORTYPE_MEDIUM, ARMORTYPE_LIGHT } },
+			{ "IM_FILTERSPEC_HEAVY" 		, { "armorType", ARMORTYPE_HEAVY } },
+			{ "IM_FILTERSPEC_MEDIUM"		, { "armorType", ARMORTYPE_MEDIUM } },
+			{ "IM_FILTERSPEC_LIGHT"			, { "armorType", ARMORTYPE_LIGHT } },
+			{ "IM_FILTERSPEC_SHIELD"		, { "equipType", EQUIP_TYPE_OFF_HAND } },
+			{ "IM_FILTERSPEC_JEWELRY"		, { "equipType", EQUIP_TYPE_RING, EQUIP_TYPE_NECK } },
+			{ "IM_FILTERSPEC_VANITY"		, { "equipType", EQUIP_TYPE_DISGUISE, EQUIP_TYPE_COSTUME  }}
+		} },
+		{ "IM_FILTER_CONSUMABLE"	, {
+			{ "IM_FILTERSPEC_ANY"			, { "itemType", ITEMTYPE_CROWN_ITEM, ITEMTYPE_FOOD, ITEMTYPE_DRINK, ITEMTYPE_RECIPE, ITEMTYPE_POTION, ITEMTYPE_POISON, ITEMTYPE_RACIAL_STYLE_MOTIF, ITEMTYPE_MASTER_WRIT, ITEMTYPE_CONTAINER, ITEMTYPE_AVA_REPAIR, ITEMTYPE_TOOL, ITEMTYPE_CROWN_REPAIR, ITEMTYPE_FISH, ITEMTYPE_TROPHY } },
+			{ "IM_FILTERSPEC_CROWN_ITEM"	, { "itemType", ITEMTYPE_CROWN_ITEM } },
+			{ "IM_FILTERSPEC_FOOD"			, { "itemType", ITEMTYPE_FOOD } },
+			{ "IM_FILTERSPEC_DRINK"			, { "itemType", ITEMTYPE_DRINK } },
+			{ "IM_FILTERSPEC_RECIPE"		, { "itemType", ITEMTYPE_RECIPE } },
+			{ "IM_FILTERSPEC_POTION"		, { "itemType", ITEMTYPE_POTION } },
+			{ "IM_FILTERSPEC_POISON"		, { "itemType", ITEMTYPE_POISON } },
+			{ "IM_FILTERSPEC_MOTIF"			, { "itemType", ITEMTYPE_RACIAL_STYLE_MOTIF } },
+			{ "IM_FILTERSPEC_MASTER_WRIT"	, { "itemType", ITEMTYPE_MASTER_WRIT } },
+			{ "IM_FILTERSPEC_CONTAINER"		, { "itemType", ITEMTYPE_CONTAINER } },
+			{ "IM_FILTERSPEC_REPAIR"		, { "itemType", ITEMTYPE_AVA_REPAIR, ITEMTYPE_TOOL, ITEMTYPE_CROWN_REPAIR } },
+			{ "IM_FILTERSPEC_FISH"			, { "itemType", ITEMTYPE_FISH } },
+			{ "IM_FILTERSPEC_TROPHY"		, { "itemType", ITEMTYPE_TROPHY } },
+		} },
+		{ "IM_FILTER_MATERIAL"		, {
+			{ "IM_FILTERSPEC_ANY"			, { "itemType", ITEMTYPE_BLACKSMITHING_MATERIAL, ITEMTYPE_BLACKSMITHING_RAW_MATERIAL, ITEMTYPE_BLACKSMITHING_BOOSTER, ITEMTYPE_CLOTHIER_MATERIAL, ITEMTYPE_CLOTHIER_RAW_MATERIAL, ITEMTYPE_CLOTHIER_BOOSTER, ITEMTYPE_WOODWORKING_MATERIAL, ITEMTYPE_WOODWORKING_RAW_MATERIAL, ITEMTYPE_WOODWORKING_BOOSTER, ITEMTYPE_REAGENT, ITEMTYPE_POTION_BASE, ITEMTYPE_POISON_BASE, ITEMTYPE_ENCHANTING_RUNE_ASPECT, ITEMTYPE_ENCHANTING_RUNE_ESSENCE, ITEMTYPE_ENCHANTING_RUNE_POTENCY, ITEMTYPE_INGREDIENT, ITEMTYPE_STYLE_MATERIAL, ITEMTYPE_RAW_MATERIAL, ITEMTYPE_WEAPON_TRAIT, ITEMTYPE_ARMOR_TRAIT } },
+			{ "IM_FILTERSPEC_BLACKSMITHING"	, { "itemType", ITEMTYPE_BLACKSMITHING_MATERIAL, ITEMTYPE_BLACKSMITHING_RAW_MATERIAL, ITEMTYPE_BLACKSMITHING_BOOSTER } },
+			{ "IM_FILTERSPEC_CLOTHIER"		, { "itemType", ITEMTYPE_CLOTHIER_MATERIAL, ITEMTYPE_CLOTHIER_RAW_MATERIAL, ITEMTYPE_CLOTHIER_BOOSTER } },
+			{ "IM_FILTERSPEC_WOODWORKING"	, { "itemType", ITEMTYPE_WOODWORKING_MATERIAL, ITEMTYPE_WOODWORKING_RAW_MATERIAL, ITEMTYPE_WOODWORKING_BOOSTER } },
+			{ "IM_FILTERSPEC_ALCHEMY"		, { "itemType", ITEMTYPE_REAGENT, ITEMTYPE_POTION_BASE, ITEMTYPE_POISON_BASE } },
+			{ "IM_FILTERSPEC_ENCHANTING"	, { "itemType", ITEMTYPE_ENCHANTING_RUNE_ASPECT, ITEMTYPE_ENCHANTING_RUNE_ESSENCE, ITEMTYPE_ENCHANTING_RUNE_POTENCY } },
+			{ "IM_FILTERSPEC_PROVISIONING"	, { "itemType", ITEMTYPE_INGREDIENT } },
+			{ "IM_FILTERSPEC_STYLE_MATERIAL", { "itemType", ITEMTYPE_STYLE_MATERIAL, ITEMTYPE_RAW_MATERIAL } },
+			{ "IM_FILTERSPEC_ARMOR_TRAIT"	, { "itemType", ITEMTYPE_WEAPON_TRAIT } },
+			{ "IM_FILTERSPEC_WEAPON_TRAIT"	, { "itemType", ITEMTYPE_ARMOR_TRAIT } },
+		} },
+		{ "IM_FILTER_FURNISHING"	, {
+			{ "IM_FILTERSPEC_ANY"			, { "itemType", ITEMTYPE_FURNISHING } },
+		} },
+		{ "IM_FILTER_MISC"			, {
+			{ "IM_FILTERSPEC_ANY"			, { "itemType", ITEMTYPE_GLYPH_ARMOR, ITEMTYPE_GLYPH_JEWELRY, ITEMTYPE_GLYPH_WEAPON, ITEMTYPE_SOUL_GEM, ITEMTYPE_SIEGE, ITEMTYPE_LURE, ITEMTYPE_TOOL, ITEMTYPE_TRASH, ITEMTYPE_COLLECTIBLE, ITEMTYPE_TREASURE } },
+			{ "IM_FILTERSPEC_GLYPH"			, { "itemType", ITEMTYPE_GLYPH_ARMOR, ITEMTYPE_GLYPH_JEWELRY, ITEMTYPE_GLYPH_WEAPON } },
+			{ "IM_FILTERSPEC_SOUL_GEM"		, { "itemType", ITEMTYPE_SOUL_GEM } },
+			{ "IM_FILTERSPEC_SIEGE"			, { "itemType", ITEMTYPE_SIEGE } },
+			{ "IM_FILTERSPEC_BAIT"			, { "itemType", ITEMTYPE_LURE } },
+			{ "IM_FILTERSPEC_TOOL"			, { "itemType", ITEMTYPE_TOOL } },
+			{ "IM_FILTERSPEC_TRASH"			, { "itemType", ITEMTYPE_TRASH } },
+			{ "IM_FILTERSPEC_TREASURE"		, { "itemType", ITEMTYPE_COLLECTIBLE, ITEMTYPE_TREASURE } },
+		} },
+}
+
+InventoryManager.filtertypes = generateFiltertypes()
+
+InventoryManager.qualityorder = {
+	{ InventoryManager:getIQString(ITEM_QUALITY_TRASH), 		ITEM_QUALITY_TRASH },
+	{ InventoryManager:getIQString(ITEM_QUALITY_NORMAL), 	ITEM_QUALITY_NORMAL },
+	{ InventoryManager:getIQString(ITEM_QUALITY_MAGIC), 		ITEM_QUALITY_MAGIC },
+	{ InventoryManager:getIQString(ITEM_QUALITY_ARCANE), 	ITEM_QUALITY_ARCANE },
+	{ InventoryManager:getIQString(ITEM_QUALITY_ARTIFACT), 	ITEM_QUALITY_ARTIFACT },
+	{ InventoryManager:getIQString(ITEM_QUALITY_LEGENDARY), 	ITEM_QUALITY_LEGENDARY }
+}
+
+InventoryManager.actionorder = {
+	{ IM_Ruleset.ACTION_KEEP },
+	{ IM_Ruleset.ACTION_JUNK },
+	{ IM_Ruleset.ACTION_DESTROY },
+	{ IM_Ruleset.ACTION_STASH },
+	{ IM_Ruleset.ACTION_RETRIEVE },
+}
+
+InventoryManager.traitsorder = {
+	["IM_FILTER_ANY"] = {
+		0, -- Redefined to "don't care about traits"
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANY,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKOTHERS,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKNOWN,
+		ITEM_TRAIT_TYPE_WEAPON_INTRICATE,
+		ITEM_TRAIT_TYPE_WEAPON_ORNATE,
+	},
+	["IM_FILTER_WEAPON"] = {
+		0, -- Redefined to "don't care about traits"
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANY,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKOTHERS,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKNOWN,
+		ITEM_TRAIT_TYPE_WEAPON_CHARGED,
+		ITEM_TRAIT_TYPE_WEAPON_DECISIVE,
+		ITEM_TRAIT_TYPE_WEAPON_DEFENDING,
+		ITEM_TRAIT_TYPE_WEAPON_INFUSED,
+		ITEM_TRAIT_TYPE_WEAPON_POWERED,
+		ITEM_TRAIT_TYPE_WEAPON_PRECISE,
+		ITEM_TRAIT_TYPE_WEAPON_SHARPENED,
+		ITEM_TRAIT_TYPE_WEAPON_TRAINING,
+		ITEM_TRAIT_TYPE_WEAPON_NIRNHONED,
+		ITEM_TRAIT_TYPE_WEAPON_INTRICATE,
+		ITEM_TRAIT_TYPE_WEAPON_ORNATE,
+ 	},
+	["IM_FILTER_APPAREL"] = {
+		0, -- Redefined to "don't care about traits"
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANY,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKOTHERS,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKNOWN,
+		ITEM_TRAIT_TYPE_ARMOR_DIVINES,
+		ITEM_TRAIT_TYPE_ARMOR_IMPENETRABLE,
+		ITEM_TRAIT_TYPE_ARMOR_INFUSED,
+		ITEM_TRAIT_TYPE_ARMOR_PROSPEROUS,
+		ITEM_TRAIT_TYPE_ARMOR_REINFORCED,
+		ITEM_TRAIT_TYPE_ARMOR_STURDY,
+		ITEM_TRAIT_TYPE_ARMOR_TRAINING,
+		ITEM_TRAIT_TYPE_ARMOR_WELL_FITTED,
+		ITEM_TRAIT_TYPE_WEAPON_NIRNHONED,
+		ITEM_TRAIT_TYPE_WEAPON_INTRICATE,
+		ITEM_TRAIT_TYPE_WEAPON_ORNATE,
+	},
+	["IM_FILTERSPEC_JEWELRY"] = {
+		0, -- Redefined to "don't care about traits"
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANY,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKOTHERS,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKNOWN,
+		ITEM_TRAIT_TYPE_JEWELRY_ARCANE,
+		ITEM_TRAIT_TYPE_JEWELRY_HEALTHY,
+		ITEM_TRAIT_TYPE_JEWELRY_ROBUST,
+		ITEM_TRAIT_TYPE_WEAPON_ORNATE,
+ 	},
+	["IM_FILTERSPEC_RECIPE"] = {
+		0, -- Redefined to "don't care about traits"
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKOTHERS,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKNOWN,
+	},
+	["IM_FILTERSPEC_MOTIF"] = {
+		0, -- Redefined to "don't care about traits"
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKOTHERS,
+		IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKNOWN,
+	},
+}
+
+InventoryManager.presetProfiles = {
+	[1] =
+	{
+		["rules"] =
+		{
+			[1] =
+			{
+				["minQuality"] = 0,
+				["filterType"] = "IM_FILTER_ANY",
+				["filterSubType"] = "IM_FILTERSPEC_ANY",
+				["action"] = 10,
+				["New"] = nil, -- invalid value type [function] used
+				["traitType"] = -2,
+				["maxQuality"] = 5,
+				["Filter"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+			},
+			[2] =
+			{
+				["minQuality"] = 0,
+				["filterType"] = "IM_FILTER_ANY",
+				["filterSubType"] = "IM_FILTERSPEC_ANY",
+				["action"] = 10,
+				["New"] = nil, -- invalid value type [function] used
+				["traitType"] = 9,
+				["maxQuality"] = 5,
+				["Filter"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+			},
+			[3] =
+			{
+				["minQuality"] = 0,
+				["filterType"] = "IM_FILTER_ANY",
+				["filterSubType"] = "IM_FILTERSPEC_ANY",
+				["action"] = 1,
+				["New"] = nil, -- invalid value type [function] used
+				["traitType"] = 10,
+				["maxQuality"] = 5,
+				["Filter"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+			},
+			[4] =
+			{
+				["minQuality"] = 0,
+				["filterType"] = "IM_FILTER_ANY",
+				["filterSubType"] = "IM_FILTERSPEC_ANY",
+				["action"] = 20,
+				["New"] = nil, -- invalid value type [function] used
+				["traitType"] = -3,
+				["maxQuality"] = 5,
+				["Filter"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+			},
+			[5] =
+			{
+				["minQuality"] = 0,
+				["Filter"] = nil, -- invalid value type [function] used
+				["action"] = 1,
+				["New"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+				["maxQuality"] = 5,
+				["filterSubType"] = "IM_FILTERSPEC_TRASH",
+				["filterType"] = "IM_FILTER_MISC",
+			},
+		},
+		["name"] = "Research Assistant",
+	},
+	[2] =
+	{
+		["rules"] =
+		{
+			[1] =
+			{
+				["minQuality"] = 0,
+				["filterType"] = "IM_FILTER_ANY",
+				["filterSubType"] = "IM_FILTERSPEC_ANY",
+				["action"] = 20,
+				["New"] = nil, -- invalid value type [function] used
+				["traitType"] = -3,
+				["maxQuality"] = 5,
+				["Filter"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+			},
+			[2] =
+			{
+				["minQuality"] = 0,
+				["filterType"] = "IM_FILTER_ANY",
+				["filterSubType"] = "IM_FILTERSPEC_ANY",
+				["action"] = 10,
+				["New"] = nil, -- invalid value type [function] used
+				["traitType"] = -2,
+				["maxQuality"] = 5,
+				["Filter"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+			},
+			[3] =
+			{
+				["minQuality"] = 0,
+				["filterType"] = "IM_FILTER_ANY",
+				["filterSubType"] = "IM_FILTERSPEC_ANY",
+				["action"] = 20,
+				["New"] = nil, -- invalid value type [function] used
+				["traitType"] = 9,
+				["maxQuality"] = 5,
+				["Filter"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+			},
+			[4] =
+			{
+				["minQuality"] = 0,
+				["filterType"] = "IM_FILTER_ANY",
+				["filterSubType"] = "IM_FILTERSPEC_ANY",
+				["action"] = 1,
+				["New"] = nil, -- invalid value type [function] used
+				["traitType"] = 10,
+				["maxQuality"] = 5,
+				["Filter"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+			},
+			[5] =
+			{
+				["minQuality"] = 0,
+				["Filter"] = nil, -- invalid value type [function] used
+				["action"] = 1,
+				["New"] = nil, -- invalid value type [function] used
+				["ToString"] = nil, -- invalid value type [function] used
+				["maxQuality"] = 5,
+				["filterSubType"] = "IM_FILTERSPEC_TRASH",
+				["filterType"] = "IM_FILTER_MISC",
+			},
+		},
+		["name"] = "Researcher",
+	},
+}
diff --git a/Modules/Junker.lua b/Modules/Junker.lua
new file mode 100644
index 0000000..8f8b0a2
--- /dev/null
+++ b/Modules/Junker.lua
@@ -0,0 +1,69 @@
+
+function InventoryManager:CheckAndDestroy()
+	if GetNumBagFreeSlots(BAG_BACKPACK) >= InventoryManager.settings.destroyThreshold then
+		return
+	end
+
+	self:SetCurrentInventory(BAG_BACKPACK)
+	for i,_ in pairs(self.currentInventory) do
+		local data = self:GetItemData(i)
+		local action, index, text = InventoryManager.currentRuleset:Match(data)
+		if action == self.IM_Ruleset.ACTION_DESTROY then
+			self:ReportAction(data, false, action, index, text)
+			DestroyItem(BAG_BACKPACK, i)
+		end
+	end
+end
+
+function InventoryManager:WorkBackpack(dryrun)
+	self:SetCurrentInventory(BAG_BACKPACK)
+	for i,_ in pairs(self.currentInventory) do
+		local data = self:GetItemData(i)
+		local action, index, text = self.currentRuleset:Match(data)
+
+		if action == self.IM_Ruleset.ACTION_JUNK or
+			action == self.IM_Ruleset.ACTION_DESTROY then
+			if not dryrun then
+				action = self.IM_Ruleset.ACTION_JUNK
+				SetItemIsJunk(BAG_BACKPACK, i, true)
+			end
+			self:ReportAction(data, dryrun, action, index, text)
+		end
+		if (action == self.IM_Ruleset.ACTION_STASH) and dryrun then
+			self:ReportAction(data, dryrun, action, index, text)
+		end
+	end
+	if not dryrun then
+		self:CheckAndDestroy()
+	end
+end
+
+function InventoryManager:UnJunk()
+	for i = 1, GetBagSize(BAG_BACKPACK), 1 do
+		SetItemIsJunk(BAG_BACKPACK, i, false)
+	end
+end
+
+function InventoryManager:OnInvSlotUpdate(bagId, slotId)
+	self:SetCurrentInventory(bagId)
+	local data = self:GetItemData(slotId)
+
+	if not data then return end
+
+	local action = self.currentRuleset:Match(data)
+	if action == self.IM_Ruleset.ACTION_JUNK or
+		action == self.IM_Ruleset.ACTION_DESTROY then
+		self:ReportAction(data, false, action)
+		SetItemIsJunk(bagId, slotId, true)
+	end
+
+	self:CheckAndDestroy()
+end
+
+local function OnInvSlotUpdate(eventCode, bagId, slotId, isNewItem, itemSoundCategory, inventoryUpdateReason, stackCountChange)
+	if not isNewItem or bagId ~= BAG_BACKPACK then return end
+
+	InventoryManager:OnInvSlotUpdate(bagId, slotId)
+end
+
+EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_INVENTORY_SINGLE_SLOT_UPDATE, OnInvSlotUpdate)
diff --git a/Modules/Seller.lua b/Modules/Seller.lua
new file mode 100644
index 0000000..ec76e08
--- /dev/null
+++ b/Modules/Seller.lua
@@ -0,0 +1,53 @@
+local DEBUG =
+-- function() end
+d
+
+local function _tr(str)
+	return str
+end
+
+local Sells = nil
+local StartGold = 0
+
+local function ProcessMoves()
+	if not Sells or #Sells == 0 then
+		local gain = GetCarriedCurrencyAmount(CURT_MONEY) - StartGold
+		CHAT_SYSTEM:AddMessage(zo_strformat(GetString("IM_CUR_SOLDJUNK"), gain))
+		return
+	end
+
+	local entry = Sells[#Sells]
+	local slotId = entry[1]
+	local count = entry[2]
+
+	SellInventoryItem(BAG_BACKPACK, slotId, count)
+	Sells[#Sells] = nil
+	zo_callLater(ProcessMoves, InventoryManager.settings.bankMoveDelay)
+end
+
+function InventoryManager:SellJunk(stolen)
+	Sells = { }
+	StartGold = GetCarriedCurrencyAmount(CURT_MONEY)
+	self:SetCurrentInventory(BAG_BACKPACK)
+	for i,_ in pairs(self.currentInventory) do
+		if #Sells > 90 then
+			break
+		end
+		local data = self:GetItemData(i)
+		if data.junk and data.stolen == stolen then
+			Sells[#Sells + 1] = { i, data.count }
+		end
+	end
+	zo_callLater(ProcessMoves, InventoryManager.settings.bankMoveDelay)
+end
+
+local function OnOpenStore(eventCode)
+	InventoryManager:SellJunk(false)
+end
+
+local function OnOpenFence(eventCode)
+	InventoryManager:SellJunk(true)
+end
+
+EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_OPEN_STORE, OnOpenStore)
+EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_OPEN_FENCE, OnOpenFence)
diff --git a/README.md b/README.md
index 8dc7139..c3d9f66 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,7 @@
 # InventoryManager
+
+Not yet another Junker or Bank Addon, this Inventory Manager takes care of your needs to automatically stash, retrieve and dispose of your items as you see fit.
+
+This addon works in conjunction with the CraftStore addon to determine which recipes, style motifs or traits are wanted by your current character or your alts and lets you determine whether to put the given recipes into the bank or even get them from the bank if your character deems them suitable.
+
+Rather than working with a preset list of actions and item groupings, this addon allows to set a list of rules which are applied on the items you loot, your inventory or the bank, as you wish. Think the mail filter in Outlook or Thunderbird and you get the idea.
diff --git a/Rulesets.lua b/Rulesets.lua
new file mode 100644
index 0000000..6bd8bba
--- /dev/null
+++ b/Rulesets.lua
@@ -0,0 +1,212 @@
+
+local IM_Rule = {}
+local IM_Ruleset = InventoryManager.IM_Ruleset
+
+IM_Rule.action		= IM_Ruleset.ACTION_KEEP
+IM_Rule.minQuality 	= ITEM_QUALITY_TRASH
+IM_Rule.maxQuality 	= ITEM_QUALITY_LEGENDARY
+
+IM_Rule.filterType 		= "IM_FILTER_ANY"
+IM_Rule.filterSubType 	= "IM_FILTERSPEC_ANY"
+
+function IM_Rule:New()
+	local _new = { }
+
+	for k,v in pairs(self) do
+		_new[k] = v
+	end
+
+	return _new
+end
+
+function IM_Rule:ToString()
+	local stolenText = ""
+	local traitText = ""
+	local worthlessText = ""
+	local qualityRangeText = ""
+	local isSetText = ""
+	local actionText = GetString("IM_ACTIONTXT", self.action)
+
+	local itemDescription = zo_strformat(
+		GetString(self.filterSubType),
+			" " .. GetString(self.filterType))
+
+
+	if self.traitType then
+		local which = (self.filterType == "IM_FILTER_CONSUMABLE" and 1) or 0
+		if self.traitType < 0 then
+			local str = (self.traitType == IM_Ruleset.ITEM_TRAIT_TYPE_ANY and "") or GetString("IM_META_TRAIT_TYPE", -self.traitType)
+			itemDescription = zo_strformat(
+				GetString("IM_META_TRAIT_TYPE_FORMAT", which),
+				itemDescription,
+				str)
+		else
+			itemDescription = GetString("SI_ITEMTRAITTYPE", self.traitType) .. " " .. itemDescription
+		end
+	end
+
+	if self.worthless then
+		itemDescription = GetString("IM_RULETXT_WORTHLESS") .. " " .. itemDescription
+	end
+
+	if self.stolen then
+		itemDescription = GetString("IM_RULETXT_STOLEN") .. " " .. itemDescription
+	end
+
+	if self.isSet then
+		isSetText = " " .. GetString("IM_RULETXT_ISSET")
+	end
+
+	colorMin = GetItemQualityColor(self.minQuality)
+	colorMax = GetItemQualityColor(self.maxQuality)
+
+	if self.minQuality == self.maxQuality then
+		qualityRangeText = " " .. zo_strformat(GetString("IM_RULETXT_QUALITY", 1),
+			InventoryManager:getIQString(self.minQuality))
+	elseif self.minQuality ~= ITEM_QUALITY_TRASH or self.maxQuality ~= ITEM_QUALITY_LEGENDARY then
+		qualityRangeText = " " .. zo_strformat(GetString("IM_RULETXT_QUALITY", 2),
+			InventoryManager:getIQString(self.minQuality),
+			InventoryManager:getIQString(self.maxQuality))
+	end
+
+	return zo_strformat(GetString("IM_RULETXTFORMAT"),
+		itemDescription,
+		qualityRangeText,
+		isSetText,
+		actionText)
+end
+
+function IM_Rule:Filter(data)
+
+	if data.junk then return false end
+
+	filterList = InventoryManager.filtertypes[self.filterType][self.filterSubType]
+
+	if #filterList > 0 then
+		attrName = filterList[1]
+
+		found = false
+		for i = 2, #filterList, 1 do
+			if data[attrName] == filterList[i] then
+				found = true
+				break
+			end
+		end
+
+		if not found then return false end
+	end
+
+	-- For sake of simplicity, translate 'intricate' and 'ornate' to a single value
+	local traitType = data.traitType
+
+	if traitType == ITEM_TRAIT_TYPE_ARMOR_NIRNHONED then
+		traitType = ITEM_TRAIT_TYPE_WEAPON_NIRNHONED
+	end
+
+	if traitType == ITEM_TRAIT_TYPE_ARMOR_INTRICATE then
+		traitType = ITEM_TRAIT_TYPE_WEAPON_INTRICATE
+	end
+
+	if traitType == ITEM_TRAIT_TYPE_ARMOR_ORNATE then
+		traitType = ITEM_TRAIT_TYPE_WEAPON_ORNATE
+	end
+
+	if traitType == ITEM_TRAIT_TYPE_JEWELRY_ORNATE then
+		traitType = ITEM_TRAIT_TYPE_WEAPON_ORNATE
+	end
+
+	-- Ornate, Intricate, ect.
+	if self.traitType then
+		if self.traitType == IM_Ruleset.ITEM_TRAIT_TYPE_ANY then
+			if traitType == ITEM_TRAIT_TYPE_NONE then return false end
+		elseif self.traitType == IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKOTHERS then
+			if not data.unknownothers then return false end
+		elseif self.traitType == IM_Ruleset.ITEM_TRAIT_TYPE_ANYUNKNOWN then
+			if not data.unknownself then return false end
+		elseif self.traitType ~= traitType then
+			return false
+		end
+	end
+
+	-- Part of a set?
+	if self.isSet and not data.isSet then return false end
+
+	-- stolen only?
+	if self.stolen and not data.stolen then return false end
+
+	-- worthless?
+	if self.worthless and data.value ~= 0 then return false end
+
+	-- outside wanted quality range?
+	if data.quality < self.minQuality or data.quality > self.maxQuality  then return false end
+
+	return true
+end
+
+function IM_Ruleset:New()
+	local _new = { }
+
+	for k,v in pairs(self) do
+		_new[k] = v
+	end
+
+	_new["rules"] = { }
+	if self.rules then
+		for k,v in pairs(self.rules) do
+			_new["rules"][k] = v:New()
+		end
+	end
+
+	return _new
+end
+
+function IM_Ruleset:Match(data)
+	for k, v in pairs(self.rules) do
+		local res = v:Filter(data)
+
+		-- Safeguards
+		-- If it's locked, don't touch.
+		-- If it's stolen, we can't put it in the bank.
+		if res then
+			if data.locked then res = false
+			elseif data.stolen and v.action == IM_Ruleset.ACTION_STASH then res = false end
+		end
+
+		if res then
+			return v.action, k, v:ToString()
+		end
+	end
+
+	return IM_Ruleset.ACTION_KEEP, nil, nil
+end
+
+function IM_Ruleset:NewRule()
+	return IM_Rule:New()
+end
+
+InventoryManager.IM_Ruleset = IM_Ruleset
+
+-- DEBUG CODE
+-- Ruleset = IM_Ruleset:New()
+
+-- local Rule1 = IM_Ruleset:NewRule()
+-- Rule1.filterType = "IM_FILTER_MISC"
+-- Rule1.filterSubType = "IM_FILTERSPEC_TRASH"
+-- Rule1.action = IM_Ruleset.ACTION_JUNK
+
+-- local Rule2 = IM_Ruleset:NewRule()
+-- Rule2.filterType = "IM_FILTER_WEAPON"
+-- Rule2.filterSubType = "IM_FILTERSPEC_2H"
+-- Rule2.minQuality = ITEM_QUALITY_MAGIC
+-- Rule2.action = IM_Ruleset.ACTION_RETRIEVE
+
+-- local Rule3 = IM_Ruleset:NewRule()
+-- Rule3.filterType = "IM_FILTER_APPAREL"
+-- Rule3.filterSubType = "IM_FILTERSPEC_MEDIUM"
+-- Rule3.stolen = true
+-- Rule3.action = IM_Ruleset.ACTION_DESTROY
+
+-- Ruleset.rules = { Rule1, Rule2, Rule3 }
+-- -- Ruleset.rules = { }
+
+-- InventoryManager.currentRuleset = Ruleset
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..e69de29
diff --git a/UI/ProfileEdit.lua b/UI/ProfileEdit.lua
new file mode 100644
index 0000000..88e5c2b
--- /dev/null
+++ b/UI/ProfileEdit.lua
@@ -0,0 +1,199 @@
+local DEBUG =
+function() end
+-- d
+
+local function _tr(str)
+	return str
+end
+
+local IM = InventoryManager
+local PE = IM.UI.ProfileEdit
+
+PE.profileList = { }
+PE.reverseProfileList = { }
+PE.selectedProfile = 0
+PE.selectedName = ""
+
+function PE:GetControls()
+	return {
+		{
+			type = "dropdown",
+			name = GetString("IM_PE_PROFILES"),
+			width = "half",
+			choices = {  },
+			getFunc = function() return PE:GetSelectedProfile() end,
+			setFunc = function(value) PE:SetSelectedProfile(value) end,
+			reference = "IWONTSAY_IM_CHO_PROFILES",
+		},
+		{
+			type = "button",
+			name = GetString("IM_PE_LOADPROFILE"),
+			width = "half",
+			disabled = function() return PE:GetBtnLoadDisabled() end,
+			func = function() return PE:BtnLoadClicked() end,
+		},
+		{
+			type = "button",
+			name = GetString("IM_PE_DELETEPROFILE"),
+			width = "half",
+			disabled = function() return PE:GetBtnDeleteDisabled() end,
+			func = function() return PE:BtnDeleteClicked() end,
+		},
+		{
+			type = "description",
+			text = "",
+			width = "half",
+		},
+		{
+			type = "editbox",
+			name = GetString("IM_PE_EDITPRNAME"),
+			tooltip = GetString("IM_PM_PROFILENAME_TOOLTIP"),
+			getFunc = function() return PE:GetProfileName() end,
+			setFunc = function(text) PE:SetProfileName(text) end,
+			isMultiline = false,
+			width = "half",
+		},
+		{
+			type = "button",
+			name = GetString("IM_PE_SAVEPROFILE"),
+			width = "half",
+			disabled = function() return PE:GetBtnSaveDisabled() end,
+			func = function() return PE:BtnSaveClicked() end,
+		},
+	}
+end
+
+function PE:GetSelectedProfile()
+	DEBUG("--- ProfileList:GetSelectedProfile()")
+	return PE.selectedName
+end
+
+function PE:SetSelectedProfile(value)
+	DEBUG("--- ProfileList:SetSelectedProfile()")
+	local profiles = IM.Profiles
+	PE.selectedProfile = PE.reverseProfileList[value]
+	PE.selectedName = (PE.selectedProfile and PE.selectedProfile > 0 and value) or ""
+end
+
+function PE:GetBtnLoadDisabled()
+	return #PE.profileList < 1 or (not PE.selectedProfile or PE.selectedProfile == 0)
+end
+
+function PE:GetBtnDeleteDisabled()
+	return #PE.profileList < 1 or (not PE.selectedProfile or PE.selectedProfile < 1)
+end
+
+function PE:BtnDeleteClicked()
+	local profiles = IM.Profiles
+	table.remove(profiles, PE.selectedProfile)
+	if PE.selectedProfile > #profiles then
+		PE.selectedProfile = #profiles
+	end
+	PE:UpdateProfileList(PE.selectedProfile)
+	IM:Save()
+end
+
+function PE:BtnLoadClicked()
+	local selProfile
+
+	if PE.selectedProfile > 0 then
+		selProfile = IM.Profiles[PE.selectedProfile]
+	else
+		selProfile = IM.presetProfiles[-PE.selectedProfile]
+	end
+
+	IM.currentRuleset.rules = selProfile["rules"]
+	IM.currentRuleset = IM.currentRuleset:New()
+
+	IM.settings = { }
+	for k,v in pairs(IM.charDefaults["settings"]) do
+		IM.settings[k] = v
+	end
+	for k,v in pairs(selProfile["settings"] or { }) do
+		IM.settings[k] = v
+	end
+
+	IM.UI.RuleEdit:UpdateRuleList()
+	IM:Save()
+end
+
+function PE:GetBtnSaveDisabled()
+	return PE.selectedName == ""
+end
+
+function PE:BtnSaveClicked()
+	local profiles = IM.Profiles
+	if not PE.reverseProfileList[PE.selectedName] then
+		PE.selectedProfile = #profiles + 1
+	end
+
+	profiles[PE.selectedProfile] = {
+		["name"] = PE.selectedName,
+		["rules"] = IM.currentRuleset:New()["rules"],
+		["settings"] = IM.settings,
+	}
+
+	PE:UpdateProfileList(PE.selectedProfile)
+	IM:Save()
+end
+
+function PE:GetProfileName()
+	return PE.selectedName or ""
+end
+
+function PE:SetProfileName(text)
+	PE.selectedName = text
+end
+
+function PE:UpdateProfileList(preselection)
+	DEBUG("--- UpdateProfileList()", preselection)
+
+	PE.profileList = { }
+	PE.reverseProfileList = { }
+
+	local _preselection = nil
+	local profiles
+	profiles = IM.presetProfiles
+
+	PE.profileList[1] = GetString("IM_RM_PRESETRULES")
+
+	if #profiles then
+		for i = 1, #profiles, 1 do
+			local tgt = #PE.profileList + 1
+			PE.profileList[tgt] = profiles[i]["name"]
+			PE.reverseProfileList[profiles[i]["name"]] = -i
+		end
+	end
+
+	PE.profileList[#PE.profileList + 1] = GetString("IM_RM_CUSTOMRULES")
+
+	profiles = IM.Profiles
+	if #profiles then
+		for i = 1, #profiles, 1 do
+			local tgt = #PE.profileList + 1
+			if preselection == i then _preselection = tgt end
+			PE.profileList[tgt] = profiles[i]["name"]
+			PE.reverseProfileList[profiles[i]["name"]] = i
+		end
+	end
+
+	IWONTSAY_IM_CHO_PROFILES:UpdateChoices(PE.profileList)
+
+	if #PE.profileList > 0 then
+		local seltxt = PE.profileList[_preselection or 1]
+		IWONTSAY_IM_CHO_PROFILES:UpdateValue(false, seltxt)
+		PE:SetSelectedProfile(seltxt)
+	end
+end
+
+
+-- Called whenever the panel is first created.
+function PE:PopulateUI()
+	DEBUG("--- PE:PopulateUI")
+
+	-- fired because of someone else?
+	if not IWONTSAY_IM_CHO_PROFILES then return end
+
+	PE:UpdateProfileList()
+end
+
diff --git a/UI/RuleEdit.lua b/UI/RuleEdit.lua
new file mode 100644
index 0000000..e91e0ac
--- /dev/null
+++ b/UI/RuleEdit.lua
@@ -0,0 +1,433 @@
+local DEBUG =
+function() end
+-- d
+
+local function _tr(str)
+	return str
+end
+
+local IM = InventoryManager
+local RE = IM.UI.RuleEdit
+
+
+local function getChoiceboxLists(structure, fun)
+	local _new = { }
+	local _reverse = { }
+	local _order = { }
+	for i = 1, #structure, 1 do
+		n = structure[i][1]
+		_new[n] = fun(n, i)
+		_order[i] = _new[n]
+		_reverse[_new[n]] = n
+	end
+	return {
+		["forward"] = _new,
+		["reverse"] = _reverse,
+		["order"] = _order,
+		["seltext"] = _order[1],
+		["selvalue"] = _reverse[_order[1]]
+	}
+end
+
+local function getChoiceboxListsAssoc(structure)
+	local _new = { }
+	local _reverse = { }
+	local _order = { }
+	for i = 1, #structure, 1 do
+		local entry = structure[i]
+		_new[entry[2]] = entry[1]
+		_reverse[entry[1]] = entry[2]
+		_order[#_order + 1] = entry[1]
+	end
+	return {
+		["forward"] = _new,
+		["reverse"] = _reverse,
+		["order"] = _order,
+		["seltext"] = _order[1],
+		["selvalue"] = _reverse[_order[1]]
+	}
+end
+
+local function getSpecificFilterTypes(whichFilter)
+	local found = nil
+	for i = 1, #IM.filterorder, 1 do
+		if IM.filterorder[i][1] == whichFilter then
+			found = IM.filterorder[i][2]
+			break
+		end
+	end
+
+	return getChoiceboxLists(found, function(n) return zo_strformat(GetString(n), GetString("IM_FILTER_RULE_ANY")) end)
+end
+
+local function getSpecificTraitTypes(whichFilter, whichSubFilter)
+	local rs = IM.IM_Ruleset
+	local ttlist = IM.traitsorder[whichSubFilter] or IM.traitsorder[whichFilter] or { }
+
+	local _new = { }
+	local _reverse = { }
+	local _order = { }
+	local str
+	for i = 1, #ttlist, 1 do
+		if ttlist[i] <= 0 then
+			str = GetString("IM_META_TRAIT_TYPE", -ttlist[i])
+		else
+			str = GetString("SI_ITEMTRAITTYPE", ttlist[i])
+		end
+		_new[ttlist[i]] = str
+		_reverse[str] = ttlist[i]
+		_order[#_order + 1] = str
+	end
+	return {
+		["forward"] = _new,
+		["reverse"] = _reverse,
+		["order"] = _order,
+		["seltext"] = _order[1],
+		["selvalue"] = _reverse[_order[1]]
+	}
+end
+
+function RE:GetControls()
+
+	RE.filterTypesList = getChoiceboxLists(IM.filterorder, function(n) return GetString(n) end)
+	RE.actionList = getChoiceboxLists(IM.actionorder, function(n) return GetString("IM_ACTIONTXT", n) end)
+	RE.qualityList = getChoiceboxListsAssoc(IM.qualityorder)
+
+	RE.editingRule = IM.IM_Ruleset:NewRule()
+	local rule = RE.editingRule
+	RE:UpdateFilterSpecList(rule.filterType, rule.filterSubType)
+	RE:UpdateTraitList(rule.filterType, rule.filterSubType)
+
+	return {
+		{
+			type = "dropdown",
+			name = GetString("IM_RE_CURRENTRULES"),
+			width = "half",
+			tooltip = GetString("IM_UI_LISTRULES_HEAD"),
+			choices = { "(invalid)" },
+			getFunc = function() return RE:GetSelectedRule() end,
+			setFunc = function(value) RE:SetSelectedRule(value) end,
+			reference = "IWONTSAY_IM_CHO_RULES",
+		},
+		{
+			type = "button",
+			name = GetString("IM_RE_DELETERULE"),
+			width = "half",
+			disabled = function() return RE:GetBtnDeleteDisabled() end,
+			func = function() return RE:BtnDeleteClicked() end,
+		},
+		{
+			type = "button",
+			name = GetString("IM_RE_MOVERULEUP"),
+			width = "half",
+			disabled = function() return RE:GetBtnMoveUpDisabled() end,
+			func = function() return RE:BtnMoveUpClicked() end,
+		},
+		{
+			type = "button",
+			name = GetString("IM_RE_ADDRULEBEFORE"),
+			width = "half",
+			func = function() return RE:BtnAddBeforeClicked() end,
+		},
+		{
+			type = "button",
+			name = GetString("IM_RE_MOVERULEDN"),
+			width = "half",
+			disabled = function() return RE:GetBtnMoveDownDisabled() end,
+			func = function() return RE:BtnMoveDownClicked() end,
+		},
+		{
+			type = "button",
+			name = GetString("IM_RE_ADDRULEAFTER"),
+			width = "half",
+			func = function() return RE:BtnAddAfterClicked() end,
+		},
+		{
+			type = "button",
+			name = GetString("IM_RE_REPLACERULE"),
+			width = "half",
+			disabled = function() return RE:GetBtnDeleteDisabled() end, -- Same condition as Delete
+			func = function() return RE:BtnReplaceClicked() end,
+		},
+		{
+			type = "description",
+			text = "",
+			width = "half",
+		},
+		{
+			type = "description",
+			text = GetString("IM_RE_DESC"),
+		},
+		{
+			type = "dropdown",
+			name = GetString("IM_RE_ACTION"),
+			width = "half",
+			choices = RE.actionList["order"],
+			getFunc = function() return RE.actionList["forward"][RE.editingRule.action] end,
+			setFunc = function(value) RE.editingRule.action = RE.actionList["reverse"][value] end,
+		},
+		{
+			type = "description",
+			text = "",
+			width = "half",
+		},
+		{
+			type = "dropdown",
+			name = GetString("IM_RE_GENTYPE"),
+			width = "half",
+			choices = RE.filterTypesList["order"],
+			getFunc = function() return RE.filterTypesList["forward"][RE.editingRule.filterType] end,
+			setFunc = function(value) RE:SetSelectedFilterType(value) end,
+		},
+		{
+			type = "dropdown",
+			name = GetString("IM_RE_SPECTYPE"),
+			width = "half",
+			choices = { "(invalid)" },
+			getFunc = function() return RE.filterSubTypesList["forward"][RE.editingRule.filterSubType] end,
+			setFunc = function(value) RE:SetSelectedFilterSubType(value) end,
+			reference = "IWONTSAY_IM_CHO_FILTERSPEC",
+		},
+		{
+			type = "dropdown",
+			name = GetString("IM_RE_TRAIT"),
+			width = "half",
+			choices = { "(invalid)" },
+			getFunc = function() return RE.traitList["forward"][RE.editingRule.traitType or 0] end,
+			setFunc = function(value) RE:SetSelectedTraitType(value) end,
+			reference = "IWONTSAY_IM_CHO_TRAIT",
+		},
+		{
+			type = "checkbox",
+			name = GetString("IM_RE_PARTOFSET"),
+			width = "half",
+			disabled = function() return RE:GetIsSetCheckDisabled() end,
+			getFunc = function() return RE.editingRule.isSet end,
+			setFunc = function(value) RE.editingRule.isSet = value end,
+		},
+		{
+			type = "dropdown",
+			name = GetString("IM_RE_MINQUAL"),
+			width = "half",
+			choices = RE.qualityList["order"],
+			getFunc = function() return RE.qualityList["forward"][RE.editingRule.minQuality] end,
+			setFunc = function(value) RE.editingRule.minQuality = RE.qualityList["reverse"][value] end,
+		},
+		{
+			type = "dropdown",
+			name = GetString("IM_RE_MAXQUAL"),
+			width = "half",
+			choices = RE.qualityList["order"],
+			getFunc = function() return RE.qualityList["forward"][RE.editingRule.maxQuality] end,
+			setFunc = function(value) RE.editingRule.maxQuality = RE.qualityList["reverse"][value] end,
+		},
+		{
+			type = "checkbox",
+			name = GetString("IM_RE_STOLEN"),
+			width = "half",
+			getFunc = function() return RE.editingRule.stolen end,
+			setFunc = function(value) RE.editingRule.stolen = value end,
+		},
+		{
+			type = "checkbox",
+			name = GetString("IM_RE_WORTHLESS"),
+			width = "half",
+			getFunc = function() return RE.editingRule.worthless end,
+			setFunc = function(value) RE.editingRule.worthless = value end,
+		},
+	}
+end
+
+function RE:Update()
+	CALLBACK_MANAGER:FireCallbacks("LAM-RefreshPanel", RE.panel)
+end
+
+function RE:BtnAddBeforeClicked()
+	DEBUG("--- OnBtnAddBefore")
+	local rs = IM.currentRuleset.rules
+	RE.selectedRule = RE.selectedRule or 1
+	table.insert(rs, RE.selectedRule, RE.editingRule)
+	RE:UpdateRuleList(RE.selectedRule)
+	IM:Save()
+end
+
+function RE:BtnAddAfterClicked()
+	DEBUG("--- OnBtnAddAfter")
+	local rs = IM.currentRuleset.rules
+	RE.selectedRule = RE.selectedRule or 0
+	RE.selectedRule = RE.selectedRule + 1
+	table.insert(rs, RE.selectedRule, RE.editingRule)
+	RE:UpdateRuleList(RE.selectedRule)
+	IM:Save()
+end
+
+function RE:BtnDeleteClicked()
+	DEBUG("--- OnBtnDelete")
+	local rs = IM.currentRuleset.rules
+	table.remove(rs, RE.selectedRule)
+	if RE.selectedRule > #rs then
+		RE.selectedRule = #rs
+	end
+	RE:UpdateRuleList(RE.selectedRule)
+	IM:Save()
+end
+
+function RE:BtnReplaceClicked()
+	DEBUG("--- OnBtnAddAfter")
+	local rs = IM.currentRuleset.rules
+	RE.selectedRule = RE.selectedRule or 1
+	rs[RE.selectedRule] = RE.editingRule
+	RE:UpdateRuleList(RE.selectedRule)
+	IM:Save()
+end
+
+local function moveRule(direction)
+	local rs = IM.currentRuleset.rules
+	local tmp = rs[RE.selectedRule]
+	rs[RE.selectedRule] = rs[RE.selectedRule+direction]
+	rs[RE.selectedRule+direction] = tmp
+	RE.selectedRule = RE.selectedRule+direction
+	RE:UpdateRuleList(RE.selectedRule)
+	IM:Save()
+end
+
+function RE:BtnMoveUpClicked()
+	DEBUG("--- OnBtnMoveUp")
+	moveRule(-1)
+end
+
+function RE:BtnMoveDownClicked()
+	DEBUG("--- OnBtnMoveDown")
+	moveRule(1)
+end
+
+function RE:GetBtnDeleteDisabled()
+	DEBUG("--- GetBtnDeleteDisabled")
+	return not RE.selectedRule
+end
+
+function RE:GetBtnMoveUpDisabled()
+	DEBUG("--- GetBtnDeleteDisabled")
+	return not RE.selectedRule or RE.selectedRule == 1
+end
+
+function RE:GetBtnMoveDownDisabled()
+	DEBUG("--- GetBtnDeleteDisabled")
+	return not RE.selectedRule or RE.selectedRule == #RE.ruleList
+end
+
+function RE:UpdateTraitList(filterType, filterSubType)
+	DEBUG("--- UpdateTraitList", filterType, filterSubType)
+	RE.traitList = getSpecificTraitTypes(filterType, filterSubType)
+
+	local traitTxt = (RE.editingRule.traitType and RE.traitList["forward"][RE.editingRule.traitType]) or RE.traitList["seltext"]
+
+	if not IWONTSAY_IM_CHO_TRAIT then return end
+
+	IWONTSAY_IM_CHO_TRAIT:UpdateChoices(RE.traitList["order"]);
+	IWONTSAY_IM_CHO_TRAIT:UpdateValue(false, traitTxt);
+end
+
+function RE:UpdateFilterSpecList(whichFilter, preselection)
+	DEBUG("--- UpdateFilterSpecList()", whichFilter, preselection)
+	RE.filterSubTypesList = getSpecificFilterTypes(whichFilter)
+	local fst = preselection or RE.filterSubTypesList["selvalue"]
+	RE.editingRule.filterSubType = fst
+	if not IWONTSAY_IM_CHO_FILTERSPEC then return end
+
+	-- Go the long way of updating, since the UI is present
+	RE.editingRule.filterSubType = nil
+
+	IWONTSAY_IM_CHO_FILTERSPEC:UpdateChoices(RE.filterSubTypesList["order"]);
+	IWONTSAY_IM_CHO_FILTERSPEC:UpdateValue(false, RE.filterSubTypesList["forward"][fst])
+end
+
+function RE:UpdateRuleList(preselection)
+	DEBUG("--- UpdateRuleList()", preselection)
+
+	local rules = IM.currentRuleset.rules
+	RE.ruleList = { }
+	RE.reverseRuleList = { }
+	if #rules then
+		for i = 1, #rules, 1 do
+			RE.ruleList[i] = zo_strformat("<<1>>: <<2>>", i, rules[i]:ToString())
+			RE.reverseRuleList[RE.ruleList[i]] = i
+		end
+	end
+
+	if #RE.ruleList > 0 then
+		IWONTSAY_IM_CHO_RULES:UpdateChoices(RE.ruleList)
+		IWONTSAY_IM_CHO_RULES:UpdateValue(false, RE.ruleList[preselection or 1])
+	else
+		DEBUG(" -- Setting (empty)...")
+		IWONTSAY_IM_CHO_RULES:UpdateChoices({ GetString("IM_RE_EMPTY") })
+		IWONTSAY_IM_CHO_RULES:UpdateValue(false, GetString("IM_RE_EMPTY"))
+	end
+end
+
+function RE:GetIsSetCheckDisabled()
+	DEBUG("--- GetIsSetCheckDisabled")
+	local disabled =
+		RE.editingRule.filterType ~= "IM_FILTER_ANY" and
+		RE.editingRule.filterType ~= "IM_FILTER_WEAPON" and
+		RE.editingRule.filterType ~= "IM_FILTER_APPAREL"
+
+	if disabled then RE.editingRule.isSet = false end
+
+	return disabled
+end
+
+function RE:GetSelectedRule()
+	DEBUG("--- GetSelectedRule")
+	return (RE.ruleList and RE.selectedRule and RE.ruleList[RE.selectedRule]) or "(empty)"
+end
+
+function RE:SetSelectedRule(whichRuleText)
+	DEBUG("--- SetSelectedRule", whichRuleText)
+	RE.selectedRule = RE.reverseRuleList[whichRuleText]
+	local rule = IM.currentRuleset.rules[RE.selectedRule]
+	RE.editingRule = (rule and rule:New()) or RE.editingRule
+	rule = RE.editingRule
+
+	RE:UpdateFilterSpecList(rule.filterType, rule.filterSubType)
+
+end
+
+function RE:SetSelectedFilterType(whichFilterText)
+	DEBUG("--- SetSelectedFilterType", whichFilterText)
+	local whichFilter = RE.filterTypesList["reverse"][whichFilterText]
+
+	if RE.editingRule.filterType ~= whichFilter then
+		RE.editingRule.filterType = whichFilter
+		RE:UpdateFilterSpecList(whichFilter)
+	end
+end
+
+function RE:SetSelectedFilterSubType(value)
+	DEBUG("--- SetSelectedFilterSubType", value)
+	local whichSubFilter = RE.filterSubTypesList["reverse"][value]
+
+	if RE.editingRule.filterSubType ~= whichSubFilter then
+		RE.editingRule.filterSubType = whichSubFilter
+		RE:UpdateTraitList(RE.editingRule.filterType, RE.editingRule.filterSubType)
+	end
+end
+
+function RE:SetSelectedTraitType(value)
+	DEBUG("--- SetSelectedTraitType", value)
+	RE.editingRule.traitType = RE.traitList["reverse"][value]
+	if RE.editingRule.traitType == 0 then
+		RE.editingRule.traitType = nil
+	end
+end
+
+-- Called whenever the panel is first created.
+function RE:PopulateUI()
+	DEBUG("--- PopulateUI")
+
+	-- fired because of someone else?
+	if not IWONTSAY_IM_CHO_FILTERSPEC then return end
+
+	RE:UpdateRuleList()
+end
+
diff --git a/UI/Settings.lua b/UI/Settings.lua
new file mode 100644
index 0000000..4ff1e86
--- /dev/null
+++ b/UI/Settings.lua
@@ -0,0 +1,105 @@
+local DEBUG =
+-- function() end
+d
+
+local function _tr(str)
+	return str
+end
+local IM = InventoryManager
+local SE = IM.UI.Settings
+
+function SE:GetControls()
+	return {
+		{
+			type = "slider",
+			name = GetString("IM_SET_MIN_GOLD"),
+			tooltip = GetString("IM_SET_MIN_GOLD_TOOLTIP"),
+			min = 0,
+			max = 100000,
+			getFunc = function() return IM.settings.minGold end,
+			setFunc = function(value) IM.settings.minGold = value end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "slider",
+			name = GetString("IM_SET_MAX_GOLD"),
+			tooltip = GetString("IM_SET_MAX_GOLD_TOOLTIP"),
+			min = 0,
+			max = 100000,
+			getFunc = function() return IM.settings.maxGold end,
+			setFunc = function(value) IM.settings.maxGold = value end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "slider",
+			name = GetString("IM_SET_MIN_TV"),
+			tooltip = GetString("IM_SET_MIN_TV_TOOLTIP"),
+			min = 0,
+			max = 100000,
+			getFunc = function() return IM.settings.minTV end,
+			setFunc = function(value) IM.settings.minTV = value end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "slider",
+			name = GetString("IM_SET_MAX_TV"),
+			tooltip = GetString("IM_SET_MAX_TV_TOOLTIP"),
+			min = 0,
+			max = 100000,
+			getFunc = function() return IM.settings.maxTV end,
+			setFunc = function(value) IM.settings.maxTV = value end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "slider",
+			name = GetString("IM_SET_BANK"),
+			tooltip = GetString("IM_SET_BANK_TOOLTIP"),
+			min = 2,
+			max = 200,
+			getFunc = function() return IM.settings.bankMoveDelay end,
+			setFunc = function(value) IM.settings.bankMoveDelay = value end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "slider",
+			name = GetString("IM_SET_DEST"),
+			tooltip = GetString("IM_SET_DEST_TOOLTIP"),
+			min = 0,
+			max = 500,
+			getFunc = function() return IM.settings.destroyThreshold end,
+			setFunc = function(value) IM.settings.destroyThreshold = value end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "button",
+			name = GetString("IM_SET_LIST"),
+			tooltip = GetString("IM_SET_LIST_TOOLTIP"),
+			func = function() IM:listrules() end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "button",
+			name = GetString("IM_SET_UNJUNK"),
+			tooltip = GetString("IM_SET_UNJUNK_TOOLTIP"),
+			func = function() IM:UnJunk() end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "button",
+			name = GetString("IM_SET_DRYRUN"),
+			tooltip = GetString("IM_SET_DRYRUN_TOOLTIP"),
+			func = function() IM:dryrun() end,
+			width = "half",	--or "half" (optional)
+		},
+		{
+			type = "button",
+			name = GetString("IM_SET_RUN"),
+			tooltip = GetString("IM_SET_RUN_TOOLTIP"),
+			func = function() IM:WorkBackpack(false) end,
+			width = "half",	--or "half" (optional)
+		},
+	}
+end
+
+function SE:PopulateUI()
+end
diff --git a/ZoS Disclosure b/ZoS Disclosure
new file mode 100644
index 0000000..ec6df7c
--- /dev/null
+++ b/ZoS Disclosure
@@ -0,0 +1 @@
+This Add-on is not created by, affiliated with or sponsored by ZeniMax Media Inc. or its affiliates. The Elder Scrolls and related logos are registered trademarks or trademarks of ZeniMax Media Inc. in the United States and/or other countries. All rights reserved."
\ No newline at end of file
diff --git a/lang/de.lua b/lang/de.lua
new file mode 100644
index 0000000..7a6baf8
--- /dev/null
+++ b/lang/de.lua
@@ -0,0 +1,153 @@
+
+-- Check Behavior of GetString
+local lang = {
+
+	-- parameters are itemDescription, qualityRangeText, isSetText, actionText
+	-- e.g. "put in trash any stolen worthless light armor with quality Trash to Normal"
+	IM_RULETXTFORMAT0			= "<<4>>: jeder <<z:1>><<z:2>><<z:3>>.",
+	IM_RULETXT_ISSET0			= "(Teil eines Sets)",
+	IM_RULETXT_STOLEN0			= "gestohlene(s)",
+	IM_RULETXT_WORTHLESS0		= "wertlos(es)",
+	IM_RULETXT_QUALITY1			= "mit Qualität <<1>>",
+	IM_RULETXT_QUALITY2			= "mit Qualität von <<1>> bis <<2>>",
+
+	IM_ACTIONTXT0				= "Behalten",
+	IM_ACTIONTXT1				= "Zum Müll stecken",
+	IM_ACTIONTXT2				= "Vernichten",
+	IM_ACTIONTXT10				= "Einlagern",
+	IM_ACTIONTXT20				= "Auslagern",
+
+	IM_TAKENACTION0				= "Würde <<z:1>>: |t16:16:<<2>>|t <<3>> wegen Regel <<4>>: <<5>>",
+	IM_TAKENACTION1				= "<<1>>: |t16:16:<<2>>|t <<3>>",
+
+	IM_FILTER_RULE_ANY0			= "Alle",
+	IM_FILTER_ANY0				= "Gegenstand",
+	IM_FILTERSPEC_ANY0			= "<<1>>",
+	IM_FILTER_WEAPON0			= "Waffe",
+	IM_FILTERSPEC_1H0			= "Einhandwaffe",
+	IM_FILTERSPEC_2H0			= "Zweihandwaffe",
+	IM_FILTERSPEC_BOW0			= "Bogen",
+	IM_FILTERSPEC_STAFF_DEST0 	= "Zerstörungsstab",
+	IM_FILTERSPEC_STAFF_HEAL0 	= "Heilstab",
+	IM_FILTER_APPAREL0			= "Tragbares",
+	IM_FILTERSPEC_ANY_BODY0		= "Schutzbekleidung",
+	IM_FILTERSPEC_HEAVY0		= "Schwere Rüstung",
+	IM_FILTERSPEC_MEDIUM0		= "Mittlere Rüstung",
+	IM_FILTERSPEC_LIGHT0		= "Leichte Rüstung",
+	IM_FILTERSPEC_SHIELD0		= "Schild",
+	IM_FILTERSPEC_JEWELRY0		= "Juwelen",
+	IM_FILTERSPEC_VANITY0		= "Kostüme",
+	IM_FILTER_CONSUMABLE0		= "Verbrauchsgüter",
+	IM_FILTERSPEC_CROWN_ITEM0 	= "Kronengegenstand",
+	IM_FILTERSPEC_FOOD0			= "Essen",
+	IM_FILTERSPEC_DRINK0		= "Trinken",
+	IM_FILTERSPEC_RECIPE0		= "Rezept",
+	IM_FILTERSPEC_POTION0		= "Zaubertrank",
+	IM_FILTERSPEC_POISON0		= "Gift",
+	IM_FILTERSPEC_MOTIF0		= "Stilbuch",
+	IM_FILTERSPEC_MASTER_WRIT0 	= "Meisterschieb",
+	IM_FILTERSPEC_CONTAINER0	= "Behälter",
+	IM_FILTERSPEC_REPAIR0		= "Reparatursatz",
+	IM_FILTERSPEC_FISH0			= "Fisch",
+	IM_FILTERSPEC_TROPHY0		= "Trophäe",
+	IM_FILTER_MATERIAL0			= "Material",
+	IM_FILTERSPEC_BLACKSMITHING0	= "Schmiedematerial",
+	IM_FILTERSPEC_CLOTHIER0		= "Schneidermaterial",
+	IM_FILTERSPEC_WOODWORKING0 	= "Holzhandwerkermasterial",
+	IM_FILTERSPEC_ALCHEMY0		= "Alchimiematerial",
+	IM_FILTERSPEC_ENCHANTING0 	= "Verzauberungsmaterial",
+	IM_FILTERSPEC_PROVISIONING0 = "Kochzutat",
+	IM_FILTERSPEC_STYLE_MATERIAL0 = "Stilmaterial",
+	IM_FILTERSPEC_ARMOR_TRAIT0 	= "Rüstungsaufwertung",
+	IM_FILTERSPEC_WEAPON_TRAIT0 = "Waffenaufwertung",
+	IM_FILTER_FURNISHING0		= "Möbel",
+	IM_FILTER_MISC0				= "Verschiedenes",
+	IM_FILTERSPEC_GLYPH0		= "Glyphe",
+	IM_FILTERSPEC_SOUL_GEM0 	= "Seelenstein",
+	IM_FILTERSPEC_SIEGE0		= "Belagerungsausrüstung",
+	IM_FILTERSPEC_BAIT0			= "Köder",
+	IM_FILTERSPEC_TOOL0			= "Werkzeug",
+	IM_FILTERSPEC_TRASH0		= "Müll",
+	IM_FILTERSPEC_TREASURE0		= "Schatz",
+
+	IM_META_TRAIT_TYPE_FORMAT0	= "<<1>> mit jeder Eigenschaft, die <<2>> ist",
+	IM_META_TRAIT_TYPE_FORMAT1	= "<<1>> welches <<2>> ist",
+	IM_META_TRAIT_TYPE0			= "(irrelevant)",
+	IM_META_TRAIT_TYPE1			= "jede Eigenschaft",
+	IM_META_TRAIT_TYPE2			= "unbekannt für andere",
+	IM_META_TRAIT_TYPE3			= "unbekannt",
+
+	IM_RE_CURRENTRULES0			= "Derzeitige Regeln",
+	IM_RE_DELETERULE0			= "Regel löschen",
+	IM_RE_MOVERULEUP0			= "Regel nach oben",
+	IM_RE_ADDRULEBEFORE0		= "Neue Regel vor dieser",
+	IM_RE_MOVERULEDN0			= "Regel nach unten",
+	IM_RE_ADDRULEAFTER0			= "Neue Regel nach dieser",
+	IM_RE_REPLACERULE0			= "Regel ersetzen",
+	IM_RE_DESC0					= "Verändern Sie diese Felder um die Regel zu definieren, die Sie hinzufügen wollen.",
+	IM_RE_ACTION0				= "Aktion",
+	IM_RE_GENTYPE0				= "Typ",
+	IM_RE_SPECTYPE0				= "Spezieller Typ",
+	IM_RE_TRAIT0				= "Eigenschaft",
+	IM_RE_PARTOFSET0			= "Teil eines Sets",
+	IM_RE_MINQUAL0				= "Minimale Qualität",
+	IM_RE_MAXQUAL0				= "Maximale Qualität",
+	IM_RE_STOLEN0				= "Gestohlen",
+	IM_RE_WORTHLESS0			= "Wertlos",
+	IM_RE_EMPTY0				= "(leer)",
+
+	IM_PE_PROFILES0				= "Profile",
+	IM_PE_LOADPROFILE0			= "Profil laden",
+	IM_PE_DELETEPROFILE0		= "Profil löschen",
+	IM_PE_EDITPRNAME0			= "Profilname",
+	IM_PE_SAVEPROFILE0			= "Profil speichern",
+
+	IM_BANK_LIMITED0			= "Transaktion unvollständig - ZOS Anti-Spam-Filterung",
+	IM_BANK_DEADLOCK0			= "Transaktion unvollständig - beide Lager voll",
+	IM_BANK_PARTIAL0			= "Transaktion unvollständig - eines der Lager voll",
+	IM_BANK_OK0					= "Transaktion abgeschlossen",
+
+	IM_UI_LISTRULES_HEAD0		= "Liste der Regeln",
+	IM_SET_MIN_GOLD0			= "Minimum Gold",
+	IM_SET_MIN_GOLD_TOOLTIP0	= "Wieviele Münzen mindestens beim Charakter behalten werden",
+	IM_SET_MAX_GOLD0			= "Maximum Gold",
+	IM_SET_MAX_GOLD_TOOLTIP0	= "Wieviele Münzen höchstens beim Charakter behalten werden",
+	IM_SET_MIN_TV0				= "Minimum Tel Var stones",
+	IM_SET_MIN_TV_TOOLTIP0		= "Wieviele Steine mindestens beim Charakter behalten werden",
+	IM_SET_MAX_TV0				= "Maximum Tel Var stones",
+	IM_SET_MAX_TV_TOOLTIP0		= "Wieviele Steine höchstens beim Charakter behalten werden",
+	IM_SET_BANK0				= "Bank-Verzögerung",
+	IM_SET_BANK_TOOLTIP0		= "Zeit in Millisekunden zwischen einzelnen Bankbewegungen",
+	IM_SET_DEST0				= "Zerstörungsschwelle",
+	IM_SET_DEST_TOOLTIP0		= "Zerstöre die durch Regeln angegebenen Gegenstände, wenn weniger Slots frei sind als hier angegeben",
+	IM_SET_LIST0				= "Regeln auflisten",
+	IM_SET_LIST_TOOLTIP0		= "Listet alle Regeln im Chatfenster auf",
+	IM_SET_UNJUNK0				= "Müll-Marker löschen",
+	IM_SET_UNJUNK_TOOLTIP0		= "Löscht alle Müll-Markierungen auf den Gegenständen im Inventar",
+	IM_SET_DRYRUN0				= "Probelauf",
+	IM_SET_DRYRUN_TOOLTIP0		= "Listet die Aktionen, die auf die Gegenstände im Inventar ausgeführt würden",
+	IM_SET_RUN0					= "Inventar bearbeiten",
+	IM_SET_RUN_TOOLTIP0			= "Führt die Wegwerf/Zerstörungsaktionen über die Gegenstände im Inventar aus",
+	IM_PM_PROFILENAME_TOOLTIP0	= "Namen vom Profil hier eingeben",
+	IM_RM_PRESETRULES0			= "--- Voreingestellte Profile ---",
+	IM_RM_CUSTOMRULES0			= "--- Eigene Profile ---",
+	IM_LIST_NUM_RULES0			= "Gefundene Regeln: ",
+	IM_LIST_RULE0				= "Regel ",
+	IM_UI_PM0					= "Profilmanagement",
+	IM_UI_PM_TOOLTIP0			= "Auswählen, Hinzufügen und Löschen von Profilen",
+	IM_UI_RM0					= "Regelmanagement",
+	IM_UI_RM_TOOLTIP0			= "Auswählen, Hinzufügen und Löschen von Regeln",
+	IM_UI_SETTINGS0				= "Einstellungen",
+	IM_UI_SETTINGS_TOOLTIP0		= "Generelles Verhalten anpassen",
+	IM_CUR_SOLDJUNK0			= "Müll verkauft, Erlös <<1>> Münzen.",
+	IM_CUR_DEPOSIT0				= "Zahle <<1>> <<2>> ein.",
+	IM_CUR_WITHDRAW0			= "Hebe <<1>> <<2>> ab.",
+	IM_CUR_GOLD0				= "Goldmnünzen",
+	IM_CUR_TVSTONES0			= "Tel Var Steine",
+
+}
+
+for stringId, stringValue in pairs(lang) do
+   ZO_CreateStringId(stringId, stringValue)
+   SafeAddVersion(stringId, 1)
+end
diff --git a/lang/en.lua b/lang/en.lua
new file mode 100644
index 0000000..8cf8124
--- /dev/null
+++ b/lang/en.lua
@@ -0,0 +1,153 @@
+
+-- Check Behavior of GetString
+local lang = {
+
+	-- parameters are itemDescription, qualityRangeText, isSetText, actionText
+	-- e.g. "put in trash any stolen worthless light armor with quality Trash to Normal"
+	IM_RULETXTFORMAT0			= "<<z:4>> any <<z:1>><<z:2>><<z:3>>.",
+	IM_RULETXT_ISSET0			= "which is part of a set",
+	IM_RULETXT_STOLEN0			= "stolen",
+	IM_RULETXT_WORTHLESS0		= "worthless",
+	IM_RULETXT_QUALITY1			= "with quality <<1>>",
+	IM_RULETXT_QUALITY2			= "with quality from <<1>> to <<2>>",
+
+	IM_ACTIONTXT0				= "Keep",
+	IM_ACTIONTXT1				= "Put to junk",
+	IM_ACTIONTXT2				= "Destroy",
+	IM_ACTIONTXT10				= "Put in bank",
+	IM_ACTIONTXT20				= "Pull from bank",
+
+	IM_TAKENACTION0				= "Would <<z:1>>: |t16:16:<<2>>|t <<3>> because of Rule <<4>>: <<5>>",
+	IM_TAKENACTION1				= "<<1>>: |t16:16:<<2>>|t <<3>>",
+
+	IM_FILTER_RULE_ANY0			= "Any",
+	IM_FILTER_ANY0				= "Item",
+	IM_FILTERSPEC_ANY0			= "<<1>>",
+	IM_FILTER_WEAPON0			= "Weapon",
+	IM_FILTERSPEC_1H0			= "One-Handed weapon",
+	IM_FILTERSPEC_2H0			= "Two-Handed weapon",
+	IM_FILTERSPEC_BOW0			= "Bow",
+	IM_FILTERSPEC_STAFF_DEST0 	= "Destruction staff",
+	IM_FILTERSPEC_STAFF_HEAL0 	= "Healing staff",
+	IM_FILTER_APPAREL0			= "Apparel",
+	IM_FILTERSPEC_ANY_BODY0		= "Protective vestment",
+	IM_FILTERSPEC_HEAVY0		= "Heavy Armor",
+	IM_FILTERSPEC_MEDIUM0		= "Medium Armor",
+	IM_FILTERSPEC_LIGHT0		= "Light Armor",
+	IM_FILTERSPEC_SHIELD0		= "Shield",
+	IM_FILTERSPEC_JEWELRY0		= "Jewelry",
+	IM_FILTERSPEC_VANITY0		= "Vanity clothing",
+	IM_FILTER_CONSUMABLE0		= "Consumable",
+	IM_FILTERSPEC_CROWN_ITEM0 	= "Crown Item",
+	IM_FILTERSPEC_FOOD0			= "Food",
+	IM_FILTERSPEC_DRINK0		= "Drink",
+	IM_FILTERSPEC_RECIPE0		= "Recipe",
+	IM_FILTERSPEC_POTION0		= "Potion",
+	IM_FILTERSPEC_POISON0		= "Poison",
+	IM_FILTERSPEC_MOTIF0		= "Style motif",
+	IM_FILTERSPEC_MASTER_WRIT0 	= "Master writ",
+	IM_FILTERSPEC_CONTAINER0	= "Container",
+	IM_FILTERSPEC_REPAIR0		= "Repair item",
+	IM_FILTERSPEC_FISH0			= "Fish",
+	IM_FILTERSPEC_TROPHY0		= "Trophy",
+	IM_FILTER_MATERIAL0			= "Material",
+	IM_FILTERSPEC_BLACKSMITHING0	= "Blacksmithing material",
+	IM_FILTERSPEC_CLOTHIER0		= "Clothier material",
+	IM_FILTERSPEC_WOODWORKING0 	= "Woodworking material",
+	IM_FILTERSPEC_ALCHEMY0		= "Alchemy material",
+	IM_FILTERSPEC_ENCHANTING0 	= "Enchanting material",
+	IM_FILTERSPEC_PROVISIONING0 = "Provisioning ingredient",
+	IM_FILTERSPEC_STYLE_MATERIAL0 = "Style material",
+	IM_FILTERSPEC_ARMOR_TRAIT0 	= "Armor trait",
+	IM_FILTERSPEC_WEAPON_TRAIT0 = "Weapon trait",
+	IM_FILTER_FURNISHING0		= "Furnishing",
+	IM_FILTER_MISC0				= "Miscellaneous",
+	IM_FILTERSPEC_GLYPH0		= "Glyph",
+	IM_FILTERSPEC_SOUL_GEM0 	= "Soul gem",
+	IM_FILTERSPEC_SIEGE0		= "Siege equipment",
+	IM_FILTERSPEC_BAIT0			= "Bait",
+	IM_FILTERSPEC_TOOL0			= "Tool",
+	IM_FILTERSPEC_TRASH0		= "Trash",
+	IM_FILTERSPEC_TREASURE0		= "Treasure",
+
+	IM_META_TRAIT_TYPE_FORMAT0	= "<<1>> with any trait <<2>>",
+	IM_META_TRAIT_TYPE_FORMAT1	= "<<1>> which is <<2>>",
+	IM_META_TRAIT_TYPE0			= "(irrelevant)",
+	IM_META_TRAIT_TYPE1			= "any trait",
+	IM_META_TRAIT_TYPE2			= "unknown to others",
+	IM_META_TRAIT_TYPE3			= "unknown",
+
+	IM_RE_CURRENTRULES0			= "Current Rules",
+	IM_RE_DELETERULE0			= "Delete Rule",
+	IM_RE_MOVERULEUP0			= "Move rule up",
+	IM_RE_ADDRULEBEFORE0		= "Add rule before this one",
+	IM_RE_MOVERULEDN0			= "Move rule down",
+	IM_RE_ADDRULEAFTER0			= "Add rule after this one",
+	IM_RE_REPLACERULE0			= "Replace Rule",
+	IM_RE_DESC0					= "Modify the contents of these fields to specify the rule to add.",
+	IM_RE_ACTION0				= "Action",
+	IM_RE_GENTYPE0				= "General type",
+	IM_RE_SPECTYPE0				= "Specific type",
+	IM_RE_TRAIT0				= "Trait",
+	IM_RE_PARTOFSET0			= "Part of a set",
+	IM_RE_MINQUAL0				= "Minimum Quality",
+	IM_RE_MAXQUAL0				= "Maximum Quality",
+	IM_RE_STOLEN0				= "stolen",
+	IM_RE_WORTHLESS0			= "worthless",
+	IM_RE_EMPTY0				= "(empty)",
+
+	IM_PE_PROFILES0				= "Profiles",
+	IM_PE_LOADPROFILE0			= "Load Profile",
+	IM_PE_DELETEPROFILE0		= "Delete Profile",
+	IM_PE_EDITPRNAME0			= "Edit Profile Name",
+	IM_PE_SAVEPROFILE0			= "Save Profile",
+
+	IM_BANK_LIMITED0			= "Incomplete transaction - avoiding anti-flood filtering",
+	IM_BANK_DEADLOCK0			= "Incomplete transaction - both inventories full",
+	IM_BANK_PARTIAL0			= "Incomplete transaction - one inventory full",
+	IM_BANK_OK0					= "All transactions sent",
+
+	IM_UI_LISTRULES_HEAD0		= "List of rules",
+	IM_SET_MIN_GOLD0			= "Minimum Gold",
+	IM_SET_MIN_GOLD_TOOLTIP0	= "Minimum amount of gold to keep on character",
+	IM_SET_MAX_GOLD0			= "Maximum Gold",
+	IM_SET_MAX_GOLD_TOOLTIP0	= "Maximum amount of gold to keep on character",
+	IM_SET_MIN_TV0				= "Minimum Tel Var stones",
+	IM_SET_MIN_TV_TOOLTIP0		= "Minimum amount of Tel Var stones to keep on character",
+	IM_SET_MAX_TV0				= "Maximum Tel Var stones",
+	IM_SET_MAX_TV_TOOLTIP0		= "Maximum amount of Tel Var stones to keep on character",
+	IM_SET_BANK0				= "Delay between bank moves",
+	IM_SET_BANK_TOOLTIP0		= "Time in milliseconds to wait between bank moves",
+	IM_SET_DEST0				= "Destroy Threshold",
+	IM_SET_DEST_TOOLTIP0		= "Destroy items when inventory space drops below this number of slots",
+	IM_SET_LIST0				= "List rules",
+	IM_SET_LIST_TOOLTIP0		= "List the current ruleset in the chat window",
+	IM_SET_UNJUNK0				= "UnJunk all",
+	IM_SET_UNJUNK_TOOLTIP0		= "Remove Junk markings on all of your items",
+	IM_SET_DRYRUN0				= "Dry run",
+	IM_SET_DRYRUN_TOOLTIP0		= "List the actions which would be performed on your inventory",
+	IM_SET_RUN0					= "Run over Inventory",
+	IM_SET_RUN_TOOLTIP0			= "Perform the junk/destroy options on your current inventory",
+	IM_PM_PROFILENAME_TOOLTIP0	= "Enter the name of the new profile here",
+	IM_RM_PRESETRULES0			= "--- Preset profiles ---",
+	IM_RM_CUSTOMRULES0			= "--- Custom profiles ---",
+	IM_LIST_NUM_RULES0			= "Rules found: ",
+	IM_LIST_RULE0				= "Rule ",
+	IM_UI_PM0					= "Profile Management",
+	IM_UI_PM_TOOLTIP0			= "Select, add or delete profiles",
+	IM_UI_RM0					= "Rule Management",
+	IM_UI_RM_TOOLTIP0			= "Add, modify and delete rules",
+	IM_UI_SETTINGS0				= "Settings",
+	IM_UI_SETTINGS_TOOLTIP0		= "Adapt the general behavior",
+	IM_CUR_SOLDJUNK0			= "Sold junk, Revenue is <<1>> gold coins.",
+	IM_CUR_DEPOSIT0				= "Depositing <<1>> <<2>>.",
+	IM_CUR_WITHDRAW0			= "Withdrawing <<1>> <<2>>.",
+	IM_CUR_GOLD0				= "gold coins",
+	IM_CUR_TVSTONES0			= "Tel Var stones",
+
+}
+
+for stringId, stringValue in pairs(lang) do
+   ZO_CreateStringId(stringId, stringValue)
+   SafeAddVersion(stringId, 1)
+end
diff --git a/libs/LibAddonMenu-2.0/LICENSE b/libs/LibAddonMenu-2.0/LICENSE
new file mode 100644
index 0000000..755f075
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/LICENSE
@@ -0,0 +1,201 @@
+               The Artistic License 2.0
+
+           Copyright (c) 2016 Ryan Lakanen (Seerah)
+
+     Everyone is permitted to copy and distribute verbatim copies
+      of this license document, but changing it is not allowed.
+
+Preamble
+
+This license establishes the terms under which a given free software
+Package may be copied, modified, distributed, and/or redistributed.
+The intent is that the Copyright Holder maintains some artistic
+control over the development of that Package while still keeping the
+Package available as open source and free software.
+
+You are always permitted to make arrangements wholly outside of this
+license directly with the Copyright Holder of a given Package.  If the
+terms of this license do not permit the full use that you propose to
+make of the Package, you should contact the Copyright Holder and seek
+a different licensing arrangement.
+
+Definitions
+
+    "Copyright Holder" means the individual(s) or organization(s)
+    named in the copyright notice for the entire Package.
+
+    "Contributor" means any party that has contributed code or other
+    material to the Package, in accordance with the Copyright Holder's
+    procedures.
+
+    "You" and "your" means any person who would like to copy,
+    distribute, or modify the Package.
+
+    "Package" means the collection of files distributed by the
+    Copyright Holder, and derivatives of that collection and/or of
+    those files. A given Package may consist of either the Standard
+    Version, or a Modified Version.
+
+    "Distribute" means providing a copy of the Package or making it
+    accessible to anyone else, or in the case of a company or
+    organization, to others outside of your company or organization.
+
+    "Distributor Fee" means any fee that you charge for Distributing
+    this Package or providing support for this Package to another
+    party.  It does not mean licensing fees.
+
+    "Standard Version" refers to the Package if it has not been
+    modified, or has been modified only in ways explicitly requested
+    by the Copyright Holder.
+
+    "Modified Version" means the Package, if it has been changed, and
+    such changes were not explicitly requested by the Copyright
+    Holder.
+
+    "Original License" means this Artistic License as Distributed with
+    the Standard Version of the Package, in its current version or as
+    it may be modified by The Perl Foundation in the future.
+
+    "Source" form means the source code, documentation source, and
+    configuration files for the Package.
+
+    "Compiled" form means the compiled bytecode, object code, binary,
+    or any other form resulting from mechanical transformation or
+    translation of the Source form.
+
+
+Permission for Use and Modification Without Distribution
+
+(1)  You are permitted to use the Standard Version and create and use
+Modified Versions for any purpose without restriction, provided that
+you do not Distribute the Modified Version.
+
+
+Permissions for Redistribution of the Standard Version
+
+(2)  You may Distribute verbatim copies of the Source form of the
+Standard Version of this Package in any medium without restriction,
+either gratis or for a Distributor Fee, provided that you duplicate
+all of the original copyright notices and associated disclaimers.  At
+your discretion, such verbatim copies may or may not include a
+Compiled form of the Package.
+
+(3)  You may apply any bug fixes, portability changes, and other
+modifications made available from the Copyright Holder.  The resulting
+Package will still be considered the Standard Version, and as such
+will be subject to the Original License.
+
+
+Distribution of Modified Versions of the Package as Source
+
+(4)  You may Distribute your Modified Version as Source (either gratis
+or for a Distributor Fee, and with or without a Compiled form of the
+Modified Version) provided that you clearly document how it differs
+from the Standard Version, including, but not limited to, documenting
+any non-standard features, executables, or modules, and provided that
+you do at least ONE of the following:
+
+    (a)  make the Modified Version available to the Copyright Holder
+    of the Standard Version, under the Original License, so that the
+    Copyright Holder may include your modifications in the Standard
+    Version.
+
+    (b)  ensure that installation of your Modified Version does not
+    prevent the user installing or running the Standard Version. In
+    addition, the Modified Version must bear a name that is different
+    from the name of the Standard Version.
+
+    (c)  allow anyone who receives a copy of the Modified Version to
+    make the Source form of the Modified Version available to others
+    under
+
+    (i)  the Original License or
+
+    (ii)  a license that permits the licensee to freely copy,
+    modify and redistribute the Modified Version using the same
+    licensing terms that apply to the copy that the licensee
+    received, and requires that the Source form of the Modified
+    Version, and of any works derived from it, be made freely
+    available in that license fees are prohibited but Distributor
+    Fees are allowed.
+
+
+Distribution of Compiled Forms of the Standard Version
+or Modified Versions without the Source
+
+(5)  You may Distribute Compiled forms of the Standard Version without
+the Source, provided that you include complete instructions on how to
+get the Source of the Standard Version.  Such instructions must be
+valid at the time of your distribution.  If these instructions, at any
+time while you are carrying out such distribution, become invalid, you
+must provide new instructions on demand or cease further distribution.
+If you provide valid instructions or cease distribution within thirty
+days after you become aware that the instructions are invalid, then
+you do not forfeit any of your rights under this license.
+
+(6)  You may Distribute a Modified Version in Compiled form without
+the Source, provided that you comply with Section 4 with respect to
+the Source of the Modified Version.
+
+
+Aggregating or Linking the Package
+
+(7)  You may aggregate the Package (either the Standard Version or
+Modified Version) with other packages and Distribute the resulting
+aggregation provided that you do not charge a licensing fee for the
+Package.  Distributor Fees are permitted, and licensing fees for other
+components in the aggregation are permitted. The terms of this license
+apply to the use and Distribution of the Standard or Modified Versions
+as included in the aggregation.
+
+(8) You are permitted to link Modified and Standard Versions with
+other works, to embed the Package in a larger work of your own, or to
+build stand-alone binary or bytecode versions of applications that
+include the Package, and Distribute the result without restriction,
+provided the result does not expose a direct interface to the Package.
+
+
+Items That are Not Considered Part of a Modified Version
+
+(9) Works (including, but not limited to, modules and scripts) that
+merely extend or make use of the Package, do not, by themselves, cause
+the Package to be a Modified Version.  In addition, such works are not
+considered parts of the Package itself, and are not subject to the
+terms of this license.
+
+
+General Provisions
+
+(10)  Any use, modification, and distribution of the Standard or
+Modified Versions is governed by this Artistic License. By using,
+modifying or distributing the Package, you accept this license. Do not
+use, modify, or distribute the Package, if you do not accept this
+license.
+
+(11)  If your Modified Version has been derived from a Modified
+Version made by someone other than you, you are nevertheless required
+to ensure that your Modified Version complies with the requirements of
+this license.
+
+(12)  This license does not grant you the right to use any trademark,
+service mark, tradename, or logo of the Copyright Holder.
+
+(13)  This license includes the non-exclusive, worldwide,
+free-of-charge patent license to make, have made, use, offer to sell,
+sell, import and otherwise transfer the Package with respect to any
+patent claims licensable by the Copyright Holder that are necessarily
+infringed by the Package. If you institute patent litigation
+(including a cross-claim or counterclaim) against any party alleging
+that the Package constitutes direct or contributory patent
+infringement, then this Artistic License to you shall terminate on the
+date that such litigation is filed.
+
+(14)  Disclaimer of Warranty:
+THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS
+IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
+NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL
+LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/libs/LibAddonMenu-2.0/LibAddonMenu-2.0.lua b/libs/LibAddonMenu-2.0/LibAddonMenu-2.0.lua
new file mode 100644
index 0000000..359802e
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/LibAddonMenu-2.0.lua
@@ -0,0 +1,1186 @@
+-- LibAddonMenu-2.0 & its files © Ryan Lakanen (Seerah)         --
+-- Distributed under The Artistic License 2.0 (see LICENSE)     --
+------------------------------------------------------------------
+
+
+--Register LAM with LibStub
+local MAJOR, MINOR = "LibAddonMenu-2.0", 23
+local lam, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+if not lam then return end --the same or newer version of this lib is already loaded into memory
+
+local messages = {}
+local MESSAGE_PREFIX = "[LAM2] "
+local function PrintLater(msg)
+    if CHAT_SYSTEM.primaryContainer then
+        d(MESSAGE_PREFIX .. msg)
+    else
+        messages[#messages + 1] = msg
+    end
+end
+
+local function FlushMessages()
+    for i = 1, #messages do
+        d(MESSAGE_PREFIX .. messages[i])
+    end
+    messages = {}
+end
+
+if LAMSettingsPanelCreated and not LAMCompatibilityWarning then
+    PrintLater("An old version of LibAddonMenu with compatibility issues was detected. For more information on how to proceed search for LibAddonMenu on esoui.com")
+    LAMCompatibilityWarning = true
+end
+
+--UPVALUES--
+local wm = WINDOW_MANAGER
+local em = EVENT_MANAGER
+local sm = SCENE_MANAGER
+local cm = CALLBACK_MANAGER
+local tconcat = table.concat
+local tinsert = table.insert
+
+local MIN_HEIGHT = 26
+local HALF_WIDTH_LINE_SPACING = 2
+local OPTIONS_CREATION_RUNNING = 1
+local OPTIONS_CREATED = 2
+local LAM_CONFIRM_DIALOG = "LAM_CONFIRM_DIALOG"
+local LAM_DEFAULTS_DIALOG = "LAM_DEFAULTS"
+local LAM_RELOAD_DIALOG = "LAM_RELOAD_DIALOG"
+
+local addonsForList = {}
+local addonToOptionsMap = {}
+local optionsState = {}
+lam.widgets = lam.widgets or {}
+local widgets = lam.widgets
+lam.util = lam.util or {}
+local util = lam.util
+lam.controlsForReload = lam.controlsForReload or {}
+local controlsForReload = lam.controlsForReload
+
+local function GetDefaultValue(default)
+    if type(default) == "function" then
+        return default()
+    end
+    return default
+end
+
+local function GetStringFromValue(value)
+    if type(value) == "function" then
+        return value()
+    elseif type(value) == "number" then
+        return GetString(value)
+    end
+    return value
+end
+
+local function CreateBaseControl(parent, controlData, controlName)
+    local control = wm:CreateControl(controlName or controlData.reference, parent.scroll or parent, CT_CONTROL)
+    control.panel = parent.panel or parent -- if this is in a submenu, panel is the submenu's parent
+    control.data = controlData
+
+    control.isHalfWidth = controlData.width == "half"
+    local width = 510 -- set default width in case a custom parent object is passed
+    if control.panel.GetWidth ~= nil then width = control.panel:GetWidth() - 60 end
+    control:SetWidth(width)
+    return control
+end
+
+local function CreateLabelAndContainerControl(parent, controlData, controlName)
+    local control = CreateBaseControl(parent, controlData, controlName)
+    local width = control:GetWidth()
+
+    local container = wm:CreateControl(nil, control, CT_CONTROL)
+    container:SetDimensions(width / 3, MIN_HEIGHT)
+    control.container = container
+
+    local label = wm:CreateControl(nil, control, CT_LABEL)
+    label:SetFont("ZoFontWinH4")
+    label:SetHeight(MIN_HEIGHT)
+    label:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS)
+    label:SetText(GetStringFromValue(controlData.name))
+    control.label = label
+
+    if control.isHalfWidth then
+        control:SetDimensions(width / 2, MIN_HEIGHT * 2 + HALF_WIDTH_LINE_SPACING)
+        label:SetAnchor(TOPLEFT, control, TOPLEFT, 0, 0)
+        label:SetAnchor(TOPRIGHT, control, TOPRIGHT, 0, 0)
+        container:SetAnchor(TOPRIGHT, control.label, BOTTOMRIGHT, 0, HALF_WIDTH_LINE_SPACING)
+    else
+        control:SetDimensions(width, MIN_HEIGHT)
+        container:SetAnchor(TOPRIGHT, control, TOPRIGHT, 0, 0)
+        label:SetAnchor(TOPLEFT, control, TOPLEFT, 0, 0)
+        label:SetAnchor(TOPRIGHT, container, TOPLEFT, 5, 0)
+    end
+
+    control.data.tooltipText = GetStringFromValue(control.data.tooltip)
+    control:SetMouseEnabled(true)
+    control:SetHandler("OnMouseEnter", ZO_Options_OnMouseEnter)
+    control:SetHandler("OnMouseExit", ZO_Options_OnMouseExit)
+    return control
+end
+
+local function GetTopPanel(panel)
+    while panel.panel and panel.panel ~= panel do
+        panel = panel.panel
+    end
+    return panel
+end
+
+local function IsSame(objA, objB)
+    if #objA ~= #objB then return false end
+    for i = 1, #objA do
+        if objA[i] ~= objB[i] then return false end
+    end
+    return true
+end
+
+local function RefreshReloadUIButton()
+    lam.requiresReload = false
+
+    for i = 1, #controlsForReload do
+        local reloadControl = controlsForReload[i]
+        if not IsSame(reloadControl.startValue, {reloadControl.data.getFunc()}) then
+            lam.requiresReload = true
+            break
+        end
+    end
+
+    lam.applyButton:SetHidden(not lam.requiresReload)
+end
+
+local function RequestRefreshIfNeeded(control)
+    -- if our parent window wants to refresh controls, then fire the callback
+    local panel = GetTopPanel(control.panel)
+    local panelData = panel.data
+    if panelData.registerForRefresh then
+        cm:FireCallbacks("LAM-RefreshPanel", control)
+    end
+    RefreshReloadUIButton()
+end
+
+local function RegisterForRefreshIfNeeded(control)
+    -- if our parent window wants to refresh controls, then add this to the list
+    local panel = GetTopPanel(control.panel)
+    local panelData = panel.data
+    if panelData.registerForRefresh or panelData.registerForDefaults then
+        tinsert(panel.controlsToRefresh or {}, control) -- prevent errors on custom panels
+    end
+end
+
+local function RegisterForReloadIfNeeded(control)
+    if control.data.requiresReload then
+        tinsert(controlsForReload, control)
+        control.startValue = {control.data.getFunc()}
+    end
+end
+
+local function GetConfirmDialog()
+    if(not ESO_Dialogs[LAM_CONFIRM_DIALOG]) then
+        ESO_Dialogs[LAM_CONFIRM_DIALOG] = {
+            canQueue = true,
+            title = {
+                text = "",
+            },
+            mainText = {
+                text = "",
+            },
+            buttons = {
+                [1] = {
+                    text = SI_DIALOG_CONFIRM,
+                    callback = function(dialog) end,
+                },
+                [2] = {
+                    text = SI_DIALOG_CANCEL,
+                }
+            }
+        }
+    end
+    return ESO_Dialogs[LAM_CONFIRM_DIALOG]
+end
+
+local function ShowConfirmationDialog(title, body, callback)
+    local dialog = GetConfirmDialog()
+    dialog.title.text = title
+    dialog.mainText.text = body
+    dialog.buttons[1].callback = callback
+    ZO_Dialogs_ShowDialog(LAM_CONFIRM_DIALOG)
+end
+
+local function GetDefaultsDialog()
+    if(not ESO_Dialogs[LAM_DEFAULTS_DIALOG]) then
+        ESO_Dialogs[LAM_DEFAULTS_DIALOG] = {
+            canQueue = true,
+            title = {
+                text = SI_INTERFACE_OPTIONS_RESET_TO_DEFAULT_TOOLTIP,
+            },
+            mainText = {
+                text = SI_OPTIONS_RESET_PROMPT,
+            },
+            buttons = {
+                [1] = {
+                    text = SI_OPTIONS_RESET,
+                    callback = function(dialog) end,
+                },
+                [2] = {
+                    text = SI_DIALOG_CANCEL,
+                }
+            }
+        }
+    end
+    return ESO_Dialogs[LAM_DEFAULTS_DIALOG]
+end
+
+local function ShowDefaultsDialog(panel)
+    local dialog = GetDefaultsDialog()
+    dialog.buttons[1].callback = function()
+        panel:ForceDefaults()
+        RefreshReloadUIButton()
+    end
+    ZO_Dialogs_ShowDialog(LAM_DEFAULTS_DIALOG)
+end
+
+local function DiscardChangesOnReloadControls()
+    for i = 1, #controlsForReload do
+        local reloadControl = controlsForReload[i]
+        if not IsSame(reloadControl.startValue, {reloadControl.data.getFunc()}) then
+            reloadControl:UpdateValue(false, unpack(reloadControl.startValue))
+        end
+    end
+    lam.requiresReload = false
+    lam.applyButton:SetHidden(true)
+end
+
+local function StorePanelForReopening()
+    local saveData = ZO_Ingame_SavedVariables["LAM"] or {}
+    saveData.reopenPanel = lam.currentAddonPanel:GetName()
+    ZO_Ingame_SavedVariables["LAM"] = saveData
+end
+
+local function RetrievePanelForReopening()
+    local saveData = ZO_Ingame_SavedVariables["LAM"]
+    if(saveData) then
+        ZO_Ingame_SavedVariables["LAM"] = nil
+        return _G[saveData.reopenPanel]
+    end
+end
+
+local function HandleReloadUIPressed()
+    StorePanelForReopening()
+    ReloadUI()
+end
+
+local function HandleLoadDefaultsPressed()
+    ShowDefaultsDialog(lam.currentAddonPanel)
+end
+
+local function GetReloadDialog()
+    if(not ESO_Dialogs[LAM_RELOAD_DIALOG]) then
+        ESO_Dialogs[LAM_RELOAD_DIALOG] = {
+            canQueue = true,
+            title = {
+                text = util.L["RELOAD_DIALOG_TITLE"],
+            },
+            mainText = {
+                text = util.L["RELOAD_DIALOG_TEXT"],
+            },
+            buttons = {
+                [1] = {
+                    text = util.L["RELOAD_DIALOG_RELOAD_BUTTON"],
+                    callback = function() ReloadUI() end,
+                },
+                [2] = {
+                    text = util.L["RELOAD_DIALOG_DISCARD_BUTTON"],
+                    callback = DiscardChangesOnReloadControls,
+                }
+            },
+            noChoiceCallback = DiscardChangesOnReloadControls,
+        }
+    end
+    return ESO_Dialogs[LAM_CONFIRM_DIALOG]
+end
+
+local function ShowReloadDialogIfNeeded()
+    if lam.requiresReload then
+        local dialog = GetReloadDialog()
+        ZO_Dialogs_ShowDialog(LAM_RELOAD_DIALOG)
+    end
+end
+
+local function UpdateWarning(control)
+    local warning
+    if control.data.warning ~= nil then
+        warning = util.GetStringFromValue(control.data.warning)
+    end
+
+    if control.data.requiresReload then
+        if not warning then
+            warning = string.format("|cff0000%s", util.L["RELOAD_UI_WARNING"])
+        else
+            warning = string.format("%s\n\n|cff0000%s", warning, util.L["RELOAD_UI_WARNING"])
+        end
+    end
+
+    if not warning then
+        control.warning:SetHidden(true)
+    else
+        control.warning.data = {tooltipText = warning}
+        control.warning:SetHidden(false)
+    end
+end
+
+local localization = {
+    en = {
+        PANEL_NAME = "Addons",
+        AUTHOR = string.format("%s: <<X:1>>", GetString(SI_ADDON_MANAGER_AUTHOR)), -- "Author: <<X:1>>"
+        VERSION = "Version: <<X:1>>",
+        WEBSITE = "Visit Website",
+        PANEL_INFO_FONT = "$(CHAT_FONT)|14|soft-shadow-thin",
+        RELOAD_UI_WARNING = "Changes to this setting require an UI reload in order to take effect.",
+        RELOAD_DIALOG_TITLE = "UI Reload required",
+        RELOAD_DIALOG_TEXT = "Some changes require an UI reload in order to take effect. Do you want to reload now or discard the changes?",
+        RELOAD_DIALOG_RELOAD_BUTTON = "Reload",
+        RELOAD_DIALOG_DISCARD_BUTTON = "Discard",
+    },
+    fr = { -- provided by Ayantir
+        PANEL_NAME = "Extensions",
+        WEBSITE = "Visiter le site Web",
+        RELOAD_UI_WARNING = "La modification de ce paramètre requiert un rechargement de l'UI pour qu'il soit pris en compte.",
+        RELOAD_DIALOG_TITLE = "Reload UI requis",
+        RELOAD_DIALOG_TEXT = "Certaines modifications requièrent un rechargement de l'UI pour qu'ils soient pris en compte. Souhaitez-vous recharger l'interface maintenant ou annuler les modifications ?",
+        RELOAD_DIALOG_RELOAD_BUTTON = "Recharger",
+        RELOAD_DIALOG_DISCARD_BUTTON = "Annuler",
+    },
+    de = { -- provided by sirinsidiator
+        PANEL_NAME = "Erweiterungen",
+        WEBSITE = "Webseite besuchen",
+        RELOAD_UI_WARNING = "Änderungen an dieser Option werden erst übernommen nachdem die Benutzeroberfläche neu geladen wird.",
+        RELOAD_DIALOG_TITLE = "Neuladen benötigt",
+        RELOAD_DIALOG_TEXT = "Einige Änderungen werden erst übernommen nachdem die Benutzeroberfläche neu geladen wird. Wollt Ihr sie jetzt neu laden oder die Änderungen verwerfen?",
+        RELOAD_DIALOG_RELOAD_BUTTON = "Neu laden",
+        RELOAD_DIALOG_DISCARD_BUTTON = "Verwerfen",
+    },
+    ru = { -- provided by TERAB1T
+        PANEL_NAME = "Дополнения",
+        VERSION = "Версия: <<X:1>>",
+        WEBSITE = "Посетить сайт",
+        PANEL_INFO_FONT = "RuESO/fonts/Univers57.otf|14|soft-shadow-thin",
+    },
+    es = { -- provided by silvereyes333
+        WEBSITE = "Vaya al sitio web",
+    },
+    jp = { -- provided by k0ta0uchi
+        PANEL_NAME = "アドオン設定",
+        WEBSITE = "ウェブサイトを見る",
+    },
+    zh = { -- provided by bssthu
+        PANEL_NAME = "插件",
+        VERSION = "版本: <<X:1>>",
+        WEBSITE = "访问网站",
+        PANEL_INFO_FONT = "EsoZh/fonts/univers57.otf|14|soft-shadow-thin",
+    },
+}
+
+util.L = ZO_ShallowTableCopy(localization[GetCVar("Language.2")], localization["en"])
+util.GetTooltipText = GetStringFromValue -- deprecated, use util.GetStringFromValue instead
+util.GetStringFromValue = GetStringFromValue
+util.GetDefaultValue = GetDefaultValue
+util.CreateBaseControl = CreateBaseControl
+util.CreateLabelAndContainerControl = CreateLabelAndContainerControl
+util.RequestRefreshIfNeeded = RequestRefreshIfNeeded
+util.RegisterForRefreshIfNeeded = RegisterForRefreshIfNeeded
+util.RegisterForReloadIfNeeded = RegisterForReloadIfNeeded
+util.GetTopPanel = GetTopPanel
+util.ShowConfirmationDialog = ShowConfirmationDialog
+util.UpdateWarning = UpdateWarning
+
+local ADDON_DATA_TYPE = 1
+local RESELECTING_DURING_REBUILD = true
+local USER_REQUESTED_OPEN = true
+
+
+--INTERNAL FUNCTION
+--scrolls ZO_ScrollList `list` to move the row corresponding to `data`
+-- into view (does nothing if there is no such row in the list)
+--unlike ZO_ScrollList_ScrollDataIntoView, this function accounts for
+-- fading near the list's edges - it avoids the fading area by scrolling
+-- a little further than the ZO function
+local function ScrollDataIntoView(list, data)
+    local targetIndex = data.sortIndex
+    if not targetIndex then return end
+
+    local scrollMin, scrollMax = list.scrollbar:GetMinMax()
+    local scrollTop = list.scrollbar:GetValue()
+    local controlHeight = list.controlHeight
+    local targetMin = controlHeight * (targetIndex - 1) - 64
+    -- subtracting 64 ain't arbitrary, it's the maximum fading height
+    -- (libraries/zo_templates/scrolltemplates.lua/UpdateScrollFade)
+
+    if targetMin < scrollTop then
+        ZO_ScrollList_ScrollAbsolute(list, zo_max(targetMin, scrollMin))
+    else
+        local listHeight = ZO_ScrollList_GetHeight(list)
+        local targetMax = controlHeight * targetIndex + 64 - listHeight
+
+        if targetMax > scrollTop then
+            ZO_ScrollList_ScrollAbsolute(list, zo_min(targetMax, scrollMax))
+        end
+    end
+end
+
+
+--INTERNAL FUNCTION
+--constructs a string pattern from the text in `searchEdit` control
+-- * metacharacters are escaped, losing their special meaning
+-- * whitespace matches anything (including empty substring)
+--if there is nothing but whitespace, returns nil
+--otherwise returns a filter function, which takes a `data` table argument
+-- and returns true iff `data.filterText` matches the pattern
+local function GetSearchFilterFunc(searchEdit)
+    local text = searchEdit:GetText():lower()
+    local pattern = text:match("(%S+.-)%s*$")
+
+    if not pattern then -- nothing but whitespace
+        return nil
+    end
+
+    -- escape metacharacters, e.g. "ESO-Datenbank.de" => "ESO%-Datenbank%.de"
+    pattern = pattern:gsub("[-*+?^$().[%]%%]", "%%%0")
+
+    -- replace whitespace with "match shortest anything"
+    pattern = pattern:gsub("%s+", ".-")
+
+    return function(data)
+        return data.filterText:lower():find(pattern) ~= nil
+    end
+end
+
+
+--INTERNAL FUNCTION
+--populates `addonList` with entries from `addonsForList`
+-- addonList = ZO_ScrollList control
+-- filter = [optional] function(data)
+local function PopulateAddonList(addonList, filter)
+    local entryList = ZO_ScrollList_GetDataList(addonList)
+    local numEntries = 0
+    local selectedData = nil
+
+    ZO_ScrollList_Clear(addonList)
+
+    for i, data in ipairs(addonsForList) do
+        if not filter or filter(data) then
+            local dataEntry = ZO_ScrollList_CreateDataEntry(ADDON_DATA_TYPE, data)
+            numEntries = numEntries + 1
+            data.sortIndex = numEntries
+            entryList[numEntries] = dataEntry
+            -- select the first panel passing the filter, or the currently
+            -- shown panel, but only if it passes the filter as well
+            if selectedData == nil or data.panel == lam.currentAddonPanel then
+                selectedData = data
+            end
+        else
+            data.sortIndex = nil
+        end
+    end
+
+    ZO_ScrollList_Commit(addonList)
+
+    if selectedData then
+        if selectedData.panel == lam.currentAddonPanel then
+            ZO_ScrollList_SelectData(addonList, selectedData, nil, RESELECTING_DURING_REBUILD)
+        else
+            ZO_ScrollList_SelectData(addonList, selectedData, nil)
+        end
+        ScrollDataIntoView(addonList, selectedData)
+    end
+end
+
+
+--METHOD: REGISTER WIDGET--
+--each widget has its version checked before loading,
+--so we only have the most recent one in memory
+--Usage:
+-- widgetType = "string"; the type of widget being registered
+-- widgetVersion = integer; the widget's version number
+LAMCreateControl = LAMCreateControl or {}
+local lamcc = LAMCreateControl
+
+function lam:RegisterWidget(widgetType, widgetVersion)
+    if widgets[widgetType] and widgets[widgetType] >= widgetVersion then
+        return false
+    else
+        widgets[widgetType] = widgetVersion
+        return true
+    end
+end
+
+-- INTERNAL METHOD: hijacks the handlers for the actions in the OptionsWindow layer if not already done
+local function InitKeybindActions()
+    if not lam.keybindsInitialized then
+        lam.keybindsInitialized = true
+        ZO_PreHook(KEYBOARD_OPTIONS, "ApplySettings", function()
+            if lam.currentPanelOpened then
+                if not lam.applyButton:IsHidden() then
+                    HandleReloadUIPressed()
+                end
+                return true
+            end
+        end)
+        ZO_PreHook("ZO_Dialogs_ShowDialog", function(dialogName)
+            if lam.currentPanelOpened and dialogName == "OPTIONS_RESET_TO_DEFAULTS" then
+                if not lam.defaultButton:IsHidden() then
+                    HandleLoadDefaultsPressed()
+                end
+                return true
+            end
+        end)
+    end
+end
+
+-- INTERNAL METHOD: fires the LAM-PanelOpened callback if not already done
+local function OpenCurrentPanel()
+    if lam.currentAddonPanel and not lam.currentPanelOpened then
+        lam.currentPanelOpened = true
+        lam.defaultButton:SetHidden(not lam.currentAddonPanel.data.registerForDefaults)
+        cm:FireCallbacks("LAM-PanelOpened", lam.currentAddonPanel)
+    end
+end
+
+-- INTERNAL METHOD: fires the LAM-PanelClosed callback if not already done
+local function CloseCurrentPanel()
+    if lam.currentAddonPanel and lam.currentPanelOpened then
+        lam.currentPanelOpened = false
+        cm:FireCallbacks("LAM-PanelClosed", lam.currentAddonPanel)
+    end
+end
+
+--METHOD: OPEN TO ADDON PANEL--
+--opens to a specific addon's option panel
+--Usage:
+-- panel = userdata; the panel returned by the :RegisterOptionsPanel method
+local locSettings = GetString(SI_GAME_MENU_SETTINGS)
+function lam:OpenToPanel(panel)
+
+    -- find and select the panel's row in addon list
+
+    local addonList = lam.addonList
+    local selectedData = nil
+
+    for _, addonData in ipairs(addonsForList) do
+        if addonData.panel == panel then
+            selectedData = addonData
+            ScrollDataIntoView(addonList, selectedData)
+            break
+        end
+    end
+
+    ZO_ScrollList_SelectData(addonList, selectedData)
+    ZO_ScrollList_RefreshVisible(addonList, selectedData)
+
+    local srchEdit = LAMAddonSettingsWindow:GetNamedChild("SearchFilterEdit")
+    srchEdit:Clear()
+
+    -- note that ZO_ScrollList doesn't require `selectedData` to be actually
+    -- present in the list, and that the list will only be populated once LAM
+    -- "Addon Settings" menu entry is selected for the first time
+
+    local function openAddonSettingsMenu()
+        local gameMenu = ZO_GameMenu_InGame.gameMenu
+        local settingsMenu = gameMenu.headerControls[locSettings]
+
+        if settingsMenu then -- an instance of ZO_TreeNode
+            local children = settingsMenu:GetChildren()
+            for i = 1, (children and #children or 0) do
+                local childNode = children[i]
+                local data = childNode:GetData()
+                if data and data.id == lam.panelId then
+                    -- found LAM "Addon Settings" node, yay!
+                    childNode:GetTree():SelectNode(childNode)
+                    break
+                end
+            end
+        end
+    end
+
+    if sm:GetScene("gameMenuInGame"):GetState() == SCENE_SHOWN then
+        openAddonSettingsMenu()
+    else
+        sm:CallWhen("gameMenuInGame", SCENE_SHOWN, openAddonSettingsMenu)
+        sm:Show("gameMenuInGame")
+    end
+end
+
+local TwinOptionsContainer_Index = 0
+local function TwinOptionsContainer(parent, leftWidget, rightWidget)
+    TwinOptionsContainer_Index = TwinOptionsContainer_Index + 1
+    local cParent = parent.scroll or parent
+    local panel = parent.panel or cParent
+    local container = wm:CreateControl("$(parent)TwinContainer" .. tostring(TwinOptionsContainer_Index),
+        cParent, CT_CONTROL)
+    container:SetResizeToFitDescendents(true)
+    container:SetAnchor(select(2, leftWidget:GetAnchor(0) ))
+
+    leftWidget:ClearAnchors()
+    leftWidget:SetAnchor(TOPLEFT, container, TOPLEFT)
+    rightWidget:SetAnchor(TOPLEFT, leftWidget, TOPRIGHT, 5, 0)
+
+    leftWidget:SetWidth( leftWidget:GetWidth() - 2.5 ) -- fixes bad alignment with 'full' controls
+    rightWidget:SetWidth( rightWidget:GetWidth() - 2.5 )
+
+    leftWidget:SetParent(container)
+    rightWidget:SetParent(container)
+
+    container.data = {type = "container"}
+    container.panel = panel
+    return container
+end
+
+--INTERNAL FUNCTION
+--creates controls when options panel is first shown
+--controls anchoring of these controls in the panel
+local function CreateOptionsControls(panel)
+    local addonID = panel:GetName()
+    if(optionsState[addonID] == OPTIONS_CREATED) then
+        return false
+    elseif(optionsState[addonID] == OPTIONS_CREATION_RUNNING) then
+        return true
+    end
+    optionsState[addonID] = OPTIONS_CREATION_RUNNING
+
+    local function CreationFinished()
+        optionsState[addonID] = OPTIONS_CREATED
+        cm:FireCallbacks("LAM-PanelControlsCreated", panel)
+        OpenCurrentPanel()
+    end
+
+    local optionsTable = addonToOptionsMap[addonID]
+    if optionsTable then
+        local function CreateAndAnchorWidget(parent, widgetData, offsetX, offsetY, anchorTarget, wasHalf)
+            local widget
+            local status, err = pcall(function() widget = LAMCreateControl[widgetData.type](parent, widgetData) end)
+            if not status then
+                return err or true, offsetY, anchorTarget, wasHalf
+            else
+                local isHalf = (widgetData.width == "half")
+                if not anchorTarget then -- the first widget in a panel is just placed in the top left corner
+                    widget:SetAnchor(TOPLEFT)
+                    anchorTarget = widget
+                elseif wasHalf and isHalf then -- when the previous widget was only half width and this one is too, we place it on the right side
+                    widget.lineControl = anchorTarget
+                    isHalf = false
+                    offsetY = 0
+                    anchorTarget = TwinOptionsContainer(parent, anchorTarget, widget)
+                else -- otherwise we just put it below the previous one normally
+                    widget:SetAnchor(TOPLEFT, anchorTarget, BOTTOMLEFT, 0, 15)
+                    offsetY = 0
+                    anchorTarget = widget
+                end
+                return false, offsetY, anchorTarget, isHalf
+            end
+        end
+
+        local THROTTLE_TIMEOUT, THROTTLE_COUNT = 10, 20
+        local fifo = {}
+        local anchorOffset, lastAddedControl, wasHalf
+        local CreateWidgetsInPanel, err
+
+        local function PrepareForNextPanel()
+            anchorOffset, lastAddedControl, wasHalf = 0, nil, false
+        end
+
+        local function SetupCreationCalls(parent, widgetDataTable)
+            fifo[#fifo + 1] = PrepareForNextPanel
+            local count = #widgetDataTable
+            for i = 1, count, THROTTLE_COUNT do
+                fifo[#fifo + 1] = function()
+                    CreateWidgetsInPanel(parent, widgetDataTable, i, zo_min(i + THROTTLE_COUNT - 1, count))
+                end
+            end
+            return count ~= NonContiguousCount(widgetDataTable)
+        end
+
+        CreateWidgetsInPanel = function(parent, widgetDataTable, startIndex, endIndex)
+            for i=startIndex,endIndex do
+                local widgetData = widgetDataTable[i]
+                if not widgetData then
+                    PrintLater("Skipped creation of missing entry in the settings menu of " .. addonID .. ".")
+                else
+                    local widgetType = widgetData.type
+                    local offsetX = 0
+                    local isSubmenu = (widgetType == "submenu")
+                    if isSubmenu then
+                        wasHalf = false
+                        offsetX = 5
+                    end
+
+                    err, anchorOffset, lastAddedControl, wasHalf = CreateAndAnchorWidget(parent, widgetData, offsetX, anchorOffset, lastAddedControl, wasHalf)
+                    if err then
+                        PrintLater(("Could not create %s '%s' of %s."):format(widgetData.type, widgetData.name or "unnamed", addonID))
+                    end
+
+                    if isSubmenu then
+                        if SetupCreationCalls(lastAddedControl, widgetData.controls) then
+                            PrintLater(("The sub menu '%s' of %s is missing some entries."):format(widgetData.name or "unnamed", addonID))
+                        end
+                    end
+                end
+            end
+        end
+
+        local function DoCreateSettings()
+            if #fifo > 0 then
+                local nextCall = table.remove(fifo, 1)
+                nextCall()
+                if(nextCall == PrepareForNextPanel) then
+                    DoCreateSettings()
+                else
+                    zo_callLater(DoCreateSettings, THROTTLE_TIMEOUT)
+                end
+            else
+                CreationFinished()
+            end
+        end
+
+        if SetupCreationCalls(panel, optionsTable) then
+            PrintLater(("The settings menu of %s is missing some entries."):format(addonID))
+        end
+        DoCreateSettings()
+    else
+        CreationFinished()
+    end
+
+    return true
+end
+
+--INTERNAL FUNCTION
+--handles switching between panels
+local function ToggleAddonPanels(panel) --called in OnShow of newly shown panel
+    local currentlySelected = lam.currentAddonPanel
+    if currentlySelected and currentlySelected ~= panel then
+        currentlySelected:SetHidden(true)
+        CloseCurrentPanel()
+    end
+    lam.currentAddonPanel = panel
+
+    -- refresh visible rows to reflect panel IsHidden status
+    ZO_ScrollList_RefreshVisible(lam.addonList)
+
+    if not CreateOptionsControls(panel) then
+        OpenCurrentPanel()
+    end
+
+    cm:FireCallbacks("LAM-RefreshPanel", panel)
+end
+
+local CheckSafetyAndInitialize
+
+--METHOD: REGISTER ADDON PANEL
+--registers your addon with LibAddonMenu and creates a panel
+--Usage:
+-- addonID = "string"; unique ID which will be the global name of your panel
+-- panelData = table; data object for your panel - see controls\panel.lua
+function lam:RegisterAddonPanel(addonID, panelData)
+    CheckSafetyAndInitialize(addonID)
+    local container = lam:GetAddonPanelContainer()
+    local panel = lamcc.panel(container, panelData, addonID) --addonID==global name of panel
+    panel:SetHidden(true)
+    panel:SetAnchorFill(container)
+    panel:SetHandler("OnShow", ToggleAddonPanels)
+
+    local function stripMarkup(str)
+        return str:gsub("|[Cc]%x%x%x%x%x%x", ""):gsub("|[Rr]", "")
+    end
+
+    local filterParts = {panelData.name, nil, nil}
+    -- append keywords and author separately, the may be nil
+    filterParts[#filterParts + 1] = panelData.keywords
+    filterParts[#filterParts + 1] = panelData.author
+
+    local addonData = {
+        panel = panel,
+        name = stripMarkup(panelData.name),
+        filterText = stripMarkup(tconcat(filterParts, "\t")):lower(),
+    }
+
+    tinsert(addonsForList, addonData)
+
+    if panelData.slashCommand then
+        SLASH_COMMANDS[panelData.slashCommand] = function()
+            lam:OpenToPanel(panel)
+        end
+    end
+
+    return panel --return for authors creating options manually
+end
+
+
+--METHOD: REGISTER OPTION CONTROLS
+--registers the options you want shown for your addon
+--these are stored in a table where each key-value pair is the order
+--of the options in the panel and the data for that control, respectively
+--see exampleoptions.lua for an example
+--see controls\<widget>.lua for each widget type
+--Usage:
+-- addonID = "string"; the same string passed to :RegisterAddonPanel
+-- optionsTable = table; the table containing all of the options controls and their data
+function lam:RegisterOptionControls(addonID, optionsTable) --optionsTable = {sliderData, buttonData, etc}
+    addonToOptionsMap[addonID] = optionsTable
+end
+
+--INTERNAL FUNCTION
+--creates LAM's Addon Settings entry in ZO_GameMenu
+local function CreateAddonSettingsMenuEntry()
+    local panelData = {
+        id = KEYBOARD_OPTIONS.currentPanelId,
+        name = util.L["PANEL_NAME"],
+    }
+
+    KEYBOARD_OPTIONS.currentPanelId = panelData.id + 1
+    KEYBOARD_OPTIONS.panelNames[panelData.id] = panelData.name
+
+    lam.panelId = panelData.id
+
+    local addonListSorted = false
+
+    function panelData.callback()
+        sm:AddFragment(lam:GetAddonSettingsFragment())
+        KEYBOARD_OPTIONS:ChangePanels(lam.panelId)
+
+        local title = LAMAddonSettingsWindow:GetNamedChild("Title")
+        title:SetText(panelData.name)
+
+        if not addonListSorted and #addonsForList > 0 then
+            local searchEdit = LAMAddonSettingsWindow:GetNamedChild("SearchFilterEdit")
+            --we're about to show our list for the first time - let's sort it
+            table.sort(addonsForList, function(a, b) return a.name < b.name end)
+            PopulateAddonList(lam.addonList, GetSearchFilterFunc(searchEdit))
+            addonListSorted = true
+        end
+    end
+
+    function panelData.unselectedCallback()
+        sm:RemoveFragment(lam:GetAddonSettingsFragment())
+        if SetCameraOptionsPreviewModeEnabled then -- available since API version 100011
+            SetCameraOptionsPreviewModeEnabled(false)
+        end
+    end
+
+    ZO_GameMenu_AddSettingPanel(panelData)
+end
+
+
+--INTERNAL FUNCTION
+--creates the left-hand menu in LAM's window
+local function CreateAddonList(name, parent)
+    local addonList = wm:CreateControlFromVirtual(name, parent, "ZO_ScrollList")
+
+    local function addonListRow_OnMouseDown(control, button)
+        if button == 1 then
+            local data = ZO_ScrollList_GetData(control)
+            ZO_ScrollList_SelectData(addonList, data, control)
+        end
+    end
+
+    local function addonListRow_OnMouseEnter(control)
+        ZO_ScrollList_MouseEnter(addonList, control)
+    end
+
+    local function addonListRow_OnMouseExit(control)
+        ZO_ScrollList_MouseExit(addonList, control)
+    end
+
+    local function addonListRow_Select(previouslySelectedData, selectedData, reselectingDuringRebuild)
+        if not reselectingDuringRebuild then
+            if previouslySelectedData then
+                previouslySelectedData.panel:SetHidden(true)
+            end
+            if selectedData then
+                selectedData.panel:SetHidden(false)
+                PlaySound(SOUNDS.MENU_SUBCATEGORY_SELECTION)
+            end
+        end
+    end
+
+    local function addonListRow_Setup(control, data)
+        control:SetText(data.name)
+        control:SetSelected(not data.panel:IsHidden())
+    end
+
+    ZO_ScrollList_AddDataType(addonList, ADDON_DATA_TYPE, "ZO_SelectableLabel", 28, addonListRow_Setup)
+    -- I don't know how to make highlights clear properly; they often
+    -- get stuck and after a while the list is full of highlighted rows
+    --ZO_ScrollList_EnableHighlight(addonList, "ZO_ThinListHighlight")
+    ZO_ScrollList_EnableSelection(addonList, "ZO_ThinListHighlight", addonListRow_Select)
+
+    local addonDataType = ZO_ScrollList_GetDataTypeTable(addonList, ADDON_DATA_TYPE)
+    local addonListRow_CreateRaw = addonDataType.pool.m_Factory
+
+    local function addonListRow_Create(pool)
+        local control = addonListRow_CreateRaw(pool)
+        control:SetHandler("OnMouseDown", addonListRow_OnMouseDown)
+        --control:SetHandler("OnMouseEnter", addonListRow_OnMouseEnter)
+        --control:SetHandler("OnMouseExit", addonListRow_OnMouseExit)
+        control:SetHeight(28)
+        control:SetFont("ZoFontHeader")
+        control:SetHorizontalAlignment(TEXT_ALIGN_LEFT)
+        control:SetVerticalAlignment(TEXT_ALIGN_CENTER)
+        control:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS)
+        return control
+    end
+
+    addonDataType.pool.m_Factory = addonListRow_Create
+
+    return addonList
+end
+
+
+--INTERNAL FUNCTION
+local function CreateSearchFilterBox(name, parent)
+    local boxControl = wm:CreateControl(name, parent, CT_CONTROL)
+
+    local srchButton =  wm:CreateControl("$(parent)Button", boxControl, CT_BUTTON)
+    srchButton:SetDimensions(32, 32)
+    srchButton:SetAnchor(LEFT, nil, LEFT, 2, 0)
+    srchButton:SetNormalTexture("EsoUI/Art/LFG/LFG_tabIcon_groupTools_up.dds")
+    srchButton:SetPressedTexture("EsoUI/Art/LFG/LFG_tabIcon_groupTools_down.dds")
+    srchButton:SetMouseOverTexture("EsoUI/Art/LFG/LFG_tabIcon_groupTools_over.dds")
+
+    local srchEdit = wm:CreateControlFromVirtual("$(parent)Edit", boxControl, "ZO_DefaultEdit")
+    srchEdit:SetAnchor(LEFT, srchButton, RIGHT, 4, 1)
+    srchEdit:SetAnchor(RIGHT, nil, RIGHT, -4, 1)
+    srchEdit:SetColor(ZO_NORMAL_TEXT:UnpackRGBA())
+
+    local srchBg = wm:CreateControl("$(parent)Bg", boxControl, CT_BACKDROP)
+    srchBg:SetAnchorFill()
+    srchBg:SetAlpha(0)
+    srchBg:SetCenterColor(0, 0, 0, 0.5)
+    srchBg:SetEdgeColor(ZO_DISABLED_TEXT:UnpackRGBA())
+    srchBg:SetEdgeTexture("", 1, 1, 0, 0)
+
+    -- search backdrop should appear whenever you hover over either
+    -- the magnifying glass button or the edit field (which is only
+    -- visible when it contains some text), and also while the edit
+    -- field has keyboard focus
+
+    local srchActive = false
+    local srchHover = false
+
+    local function srchBgUpdateAlpha()
+        if srchActive or srchEdit:HasFocus() then
+            srchBg:SetAlpha(srchHover and 0.8 or 0.6)
+        else
+            srchBg:SetAlpha(srchHover and 0.6 or 0.0)
+        end
+    end
+
+    local function srchMouseEnter(control)
+        srchHover = true
+        srchBgUpdateAlpha()
+    end
+
+    local function srchMouseExit(control)
+        srchHover = false
+        srchBgUpdateAlpha()
+    end
+
+    boxControl:SetMouseEnabled(true)
+    boxControl:SetHitInsets(1, 1, -1, -1)
+    boxControl:SetHandler("OnMouseEnter", srchMouseEnter)
+    boxControl:SetHandler("OnMouseExit", srchMouseExit)
+
+    srchButton:SetHandler("OnMouseEnter", srchMouseEnter)
+    srchButton:SetHandler("OnMouseExit", srchMouseExit)
+
+    local focusLostTime = 0
+
+    srchButton:SetHandler("OnClicked", function(self)
+        srchEdit:Clear()
+        if GetFrameTimeMilliseconds() - focusLostTime < 100 then
+            -- re-focus the edit box if it lost focus due to this
+            -- button click (note that this handler may run a few
+            -- frames later)
+            srchEdit:TakeFocus()
+        end
+    end)
+
+    srchEdit:SetHandler("OnMouseEnter", srchMouseEnter)
+    srchEdit:SetHandler("OnMouseExit", srchMouseExit)
+    srchEdit:SetHandler("OnFocusGained", srchBgUpdateAlpha)
+
+    srchEdit:SetHandler("OnFocusLost", function()
+        focusLostTime = GetFrameTimeMilliseconds()
+        srchBgUpdateAlpha()
+    end)
+
+    srchEdit:SetHandler("OnEscape", function(self)
+        self:Clear()
+        self:LoseFocus()
+    end)
+
+    srchEdit:SetHandler("OnTextChanged", function(self)
+        local filterFunc = GetSearchFilterFunc(self)
+        if filterFunc then
+            srchActive = true
+            srchBg:SetEdgeColor(ZO_SECOND_CONTRAST_TEXT:UnpackRGBA())
+            srchButton:SetState(BSTATE_PRESSED)
+        else
+            srchActive = false
+            srchBg:SetEdgeColor(ZO_DISABLED_TEXT:UnpackRGBA())
+            srchButton:SetState(BSTATE_NORMAL)
+        end
+        srchBgUpdateAlpha()
+        PopulateAddonList(lam.addonList, filterFunc)
+        PlaySound(SOUNDS.SPINNER_DOWN)
+    end)
+
+    return boxControl
+end
+
+
+--INTERNAL FUNCTION
+--creates LAM's Addon Settings top-level window
+local function CreateAddonSettingsWindow()
+    local tlw = wm:CreateTopLevelWindow("LAMAddonSettingsWindow")
+    tlw:SetHidden(true)
+    tlw:SetDimensions(1010, 914) -- same height as ZO_OptionsWindow
+
+    ZO_ReanchorControlForLeftSidePanel(tlw)
+
+    -- create black background for the window (mimic ZO_RightFootPrintBackground)
+
+    local bgLeft = wm:CreateControl("$(parent)BackgroundLeft", tlw, CT_TEXTURE)
+    bgLeft:SetTexture("EsoUI/Art/Miscellaneous/centerscreen_left.dds")
+    bgLeft:SetDimensions(1024, 1024)
+    bgLeft:SetAnchor(TOPLEFT, nil, TOPLEFT)
+    bgLeft:SetDrawLayer(DL_BACKGROUND)
+    bgLeft:SetExcludeFromResizeToFitExtents(true)
+
+    local bgRight = wm:CreateControl("$(parent)BackgroundRight", tlw, CT_TEXTURE)
+    bgRight:SetTexture("EsoUI/Art/Miscellaneous/centerscreen_right.dds")
+    bgRight:SetDimensions(64, 1024)
+    bgRight:SetAnchor(TOPLEFT, bgLeft, TOPRIGHT)
+    bgRight:SetDrawLayer(DL_BACKGROUND)
+    bgRight:SetExcludeFromResizeToFitExtents(true)
+
+    -- create gray background for addon list (mimic ZO_TreeUnderlay)
+
+    local underlayLeft = wm:CreateControl("$(parent)UnderlayLeft", tlw, CT_TEXTURE)
+    underlayLeft:SetTexture("EsoUI/Art/Miscellaneous/centerscreen_indexArea_left.dds")
+    underlayLeft:SetDimensions(256, 1024)
+    underlayLeft:SetAnchor(TOPLEFT, bgLeft, TOPLEFT)
+    underlayLeft:SetDrawLayer(DL_BACKGROUND)
+    underlayLeft:SetExcludeFromResizeToFitExtents(true)
+
+    local underlayRight = wm:CreateControl("$(parent)UnderlayRight", tlw, CT_TEXTURE)
+    underlayRight:SetTexture("EsoUI/Art/Miscellaneous/centerscreen_indexArea_right.dds")
+    underlayRight:SetDimensions(128, 1024)
+    underlayRight:SetAnchor(TOPLEFT, underlayLeft, TOPRIGHT)
+    underlayRight:SetDrawLayer(DL_BACKGROUND)
+    underlayRight:SetExcludeFromResizeToFitExtents(true)
+
+    -- create title bar (mimic ZO_OptionsWindow)
+
+    local title = wm:CreateControl("$(parent)Title", tlw, CT_LABEL)
+    title:SetAnchor(TOPLEFT, nil, TOPLEFT, 65, 70)
+    title:SetFont("ZoFontWinH1")
+    title:SetModifyTextType(MODIFY_TEXT_TYPE_UPPERCASE)
+
+    local divider = wm:CreateControlFromVirtual("$(parent)Divider", tlw, "ZO_Options_Divider")
+    divider:SetAnchor(TOPLEFT, nil, TOPLEFT, 65, 108)
+
+    -- create search filter box
+
+    local srchBox = CreateSearchFilterBox("$(parent)SearchFilter", tlw)
+    srchBox:SetAnchor(TOPLEFT, nil, TOPLEFT, 63, 120)
+    srchBox:SetDimensions(260, 30)
+
+    -- create scrollable addon list
+
+    local addonList = CreateAddonList("$(parent)AddonList", tlw)
+    addonList:SetAnchor(TOPLEFT, nil, TOPLEFT, 65, 160)
+    addonList:SetDimensions(285, 665)
+
+    lam.addonList = addonList -- for easy access from elsewhere
+
+    -- create container for option panels
+
+    local panelContainer = wm:CreateControl("$(parent)PanelContainer", tlw, CT_CONTROL)
+    panelContainer:SetAnchor(TOPLEFT, nil, TOPLEFT, 365, 120)
+    panelContainer:SetDimensions(645, 675)
+
+    local defaultButton = wm:CreateControlFromVirtual("$(parent)ResetToDefaultButton", tlw, "ZO_DialogButton")
+    ZO_KeybindButtonTemplate_Setup(defaultButton, "OPTIONS_LOAD_DEFAULTS", HandleLoadDefaultsPressed, GetString(SI_OPTIONS_DEFAULTS))
+    defaultButton:SetAnchor(TOPLEFT, panelContainer, BOTTOMLEFT, 0, 2)
+    lam.defaultButton = defaultButton
+
+    local applyButton = wm:CreateControlFromVirtual("$(parent)ApplyButton", tlw, "ZO_DialogButton")
+    ZO_KeybindButtonTemplate_Setup(applyButton, "OPTIONS_APPLY_CHANGES", HandleReloadUIPressed, GetString(SI_ADDON_MANAGER_RELOAD))
+    applyButton:SetAnchor(TOPRIGHT, panelContainer, BOTTOMRIGHT, 0, 2)
+    applyButton:SetHidden(true)
+    lam.applyButton = applyButton
+
+    return tlw
+end
+
+
+--INITIALIZING
+local safeToInitialize = false
+local hasInitialized = false
+
+local eventHandle = table.concat({MAJOR, MINOR}, "r")
+local function OnLoad(_, addonName)
+    -- wait for the first loaded event
+    em:UnregisterForEvent(eventHandle, EVENT_ADD_ON_LOADED)
+    safeToInitialize = true
+end
+em:RegisterForEvent(eventHandle, EVENT_ADD_ON_LOADED, OnLoad)
+
+local function OnActivated(_, initial)
+    em:UnregisterForEvent(eventHandle, EVENT_PLAYER_ACTIVATED)
+    FlushMessages()
+
+    local reopenPanel = RetrievePanelForReopening()
+    if not initial and reopenPanel then
+        lam:OpenToPanel(reopenPanel)
+    end
+end
+em:RegisterForEvent(eventHandle, EVENT_PLAYER_ACTIVATED, OnActivated)
+
+function CheckSafetyAndInitialize(addonID)
+    if not safeToInitialize then
+        local msg = string.format("The panel with id '%s' was registered before addon loading has completed. This might break the AddOn Settings menu.", addonID)
+        PrintLater(msg)
+    end
+    if not hasInitialized then
+        hasInitialized = true
+    end
+end
+
+
+--TODO documentation
+function lam:GetAddonPanelContainer()
+    local fragment = lam:GetAddonSettingsFragment()
+    local window = fragment:GetControl()
+    return window:GetNamedChild("PanelContainer")
+end
+
+
+--TODO documentation
+function lam:GetAddonSettingsFragment()
+    assert(hasInitialized or safeToInitialize)
+    if not LAMAddonSettingsFragment then
+        local window = CreateAddonSettingsWindow()
+        LAMAddonSettingsFragment = ZO_FadeSceneFragment:New(window, true, 100)
+        LAMAddonSettingsFragment:RegisterCallback("StateChange", function(oldState, newState)
+            if(newState == SCENE_FRAGMENT_SHOWN) then
+                InitKeybindActions()
+                PushActionLayerByName("OptionsWindow")
+                OpenCurrentPanel()
+            elseif(newState == SCENE_FRAGMENT_HIDDEN) then
+                CloseCurrentPanel()
+                RemoveActionLayerByName("OptionsWindow")
+                ShowReloadDialogIfNeeded()
+            end
+        end)
+        CreateAddonSettingsMenuEntry()
+    end
+    return LAMAddonSettingsFragment
+end
diff --git a/libs/LibAddonMenu-2.0/controls/button.lua b/libs/LibAddonMenu-2.0/controls/button.lua
new file mode 100644
index 0000000..be55dfc
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/button.lua
@@ -0,0 +1,91 @@
+--[[buttonData = {
+    type = "button",
+    name = "My Button", -- string id or function returning a string
+    func = function() end,
+    tooltip = "Button's tooltip text.", -- string id or function returning a string (optional)
+    width = "full", --or "half" (optional)
+    disabled = function() return db.someBooleanSetting end, --or boolean (optional)
+    icon = "icon\\path.dds", --(optional)
+    isDangerous = false, -- boolean, if set to true, the button text will be red and a confirmation dialog with the button label and warning text will show on click before the callback is executed (optional)
+    warning = "Will need to reload the UI.", --(optional)
+    reference = "MyAddonButton", -- unique global reference to control (optional)
+} ]]
+
+local widgetVersion = 11
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("button", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+local function UpdateDisabled(control)
+    local disable = control.data.disabled
+    if type(disable) == "function" then
+        disable = disable()
+    end
+    control.button:SetEnabled(not disable)
+end
+
+--controlName is optional
+local MIN_HEIGHT = 28 -- default_button height
+local HALF_WIDTH_LINE_SPACING = 2
+function LAMCreateControl.button(parent, buttonData, controlName)
+    local control = LAM.util.CreateBaseControl(parent, buttonData, controlName)
+    control:SetMouseEnabled(true)
+
+    local width = control:GetWidth()
+    if control.isHalfWidth then
+        control:SetDimensions(width / 2, MIN_HEIGHT * 2 + HALF_WIDTH_LINE_SPACING)
+    else
+        control:SetDimensions(width, MIN_HEIGHT)
+    end
+
+    if buttonData.icon then
+        control.button = wm:CreateControl(nil, control, CT_BUTTON)
+        control.button:SetDimensions(26, 26)
+        control.button:SetNormalTexture(buttonData.icon)
+        control.button:SetPressedOffset(2, 2)
+    else
+        --control.button = wm:CreateControlFromVirtual(controlName.."Button", control, "ZO_DefaultButton")
+        control.button = wm:CreateControlFromVirtual(nil, control, "ZO_DefaultButton")
+        control.button:SetWidth(width / 3)
+        control.button:SetText(LAM.util.GetStringFromValue(buttonData.name))
+        if buttonData.isDangerous then control.button:SetNormalFontColor(ZO_ERROR_COLOR:UnpackRGBA()) end
+    end
+    local button = control.button
+    button:SetAnchor(control.isHalfWidth and CENTER or RIGHT)
+    button:SetClickSound("Click")
+    button.data = {tooltipText = LAM.util.GetStringFromValue(buttonData.tooltip)}
+    button:SetHandler("OnMouseEnter", ZO_Options_OnMouseEnter)
+    button:SetHandler("OnMouseExit", ZO_Options_OnMouseExit)
+    button:SetHandler("OnClicked", function(...)
+        local args = {...}
+        local function callback()
+            buttonData.func(unpack(args))
+            LAM.util.RequestRefreshIfNeeded(control)
+        end
+
+        if(buttonData.isDangerous) then
+            local title = LAM.util.GetStringFromValue(buttonData.name)
+            local body = LAM.util.GetStringFromValue(buttonData.warning)
+            LAM.util.ShowConfirmationDialog(title, body, callback)
+        else
+            callback()
+        end
+    end)
+
+    if buttonData.warning ~= nil then
+        control.warning = wm:CreateControlFromVirtual(nil, control, "ZO_Options_WarningIcon")
+        control.warning:SetAnchor(RIGHT, button, LEFT, -5, 0)
+        control.UpdateWarning = LAM.util.UpdateWarning
+        control:UpdateWarning()
+    end
+
+    if buttonData.disabled ~= nil then
+        control.UpdateDisabled = UpdateDisabled
+        control:UpdateDisabled()
+    end
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/checkbox.lua b/libs/LibAddonMenu-2.0/controls/checkbox.lua
new file mode 100644
index 0000000..84710de
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/checkbox.lua
@@ -0,0 +1,142 @@
+--[[checkboxData = {
+    type = "checkbox",
+    name = "My Checkbox", -- or string id or function returning a string
+    getFunc = function() return db.var end,
+    setFunc = function(value) db.var = value doStuff() end,
+    tooltip = "Checkbox's tooltip text.", -- or string id or function returning a string (optional)
+    width = "full", -- or "half" (optional)
+    disabled = function() return db.someBooleanSetting end, --or boolean (optional)
+    warning = "May cause permanent awesomeness.", -- or string id or function returning a string (optional)
+    requiresReload = false, -- boolean, if set to true, the warning text will contain a notice that changes are only applied after an UI reload and any change to the value will make the "Apply Settings" button appear on the panel which will reload the UI when pressed (optional)
+    default = defaults.var, -- a boolean or function that returns a boolean (optional)
+    reference = "MyAddonCheckbox", -- unique global reference to control (optional)
+} ]]
+
+
+local widgetVersion = 14
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("checkbox", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+--label
+local enabledColor = ZO_DEFAULT_ENABLED_COLOR
+local enabledHLcolor = ZO_HIGHLIGHT_TEXT
+local disabledColor = ZO_DEFAULT_DISABLED_COLOR
+local disabledHLcolor = ZO_DEFAULT_DISABLED_MOUSEOVER_COLOR
+--checkbox
+local checkboxColor = ZO_NORMAL_TEXT
+local checkboxHLcolor = ZO_HIGHLIGHT_TEXT
+
+
+local function UpdateDisabled(control)
+    local disable
+    if type(control.data.disabled) == "function" then
+        disable = control.data.disabled()
+    else
+        disable = control.data.disabled
+    end
+
+    control.label:SetColor((disable and ZO_DEFAULT_DISABLED_COLOR or control.value and ZO_DEFAULT_ENABLED_COLOR or ZO_DEFAULT_DISABLED_COLOR):UnpackRGBA())
+    control.checkbox:SetColor((disable and ZO_DEFAULT_DISABLED_COLOR or ZO_NORMAL_TEXT):UnpackRGBA())
+    --control:SetMouseEnabled(not disable)
+    --control:SetMouseEnabled(true)
+
+    control.isDisabled = disable
+end
+
+local function ToggleCheckbox(control)
+    if control.value then
+        control.label:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+        control.checkbox:SetText(control.checkedText)
+    else
+        control.label:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+        control.checkbox:SetText(control.uncheckedText)
+    end
+end
+
+local function UpdateValue(control, forceDefault, value)
+    if forceDefault then --if we are forcing defaults
+        value = LAM.util.GetDefaultValue(control.data.default)
+        control.data.setFunc(value)
+    elseif value ~= nil then --our value could be false
+        control.data.setFunc(value)
+        --after setting this value, let's refresh the others to see if any should be disabled or have their settings changed
+        LAM.util.RequestRefreshIfNeeded(control)
+    else
+        value = control.data.getFunc()
+    end
+    control.value = value
+
+    ToggleCheckbox(control)
+end
+
+local function OnMouseEnter(control)
+    ZO_Options_OnMouseEnter(control)
+
+    if control.isDisabled then return end
+
+    local label = control.label
+    if control.value then
+        label:SetColor(ZO_HIGHLIGHT_TEXT:UnpackRGBA())
+    else
+        label:SetColor(ZO_DEFAULT_DISABLED_MOUSEOVER_COLOR:UnpackRGBA())
+    end
+    control.checkbox:SetColor(ZO_HIGHLIGHT_TEXT:UnpackRGBA())
+end
+
+local function OnMouseExit(control)
+    ZO_Options_OnMouseExit(control)
+
+    if control.isDisabled then return end
+
+    local label = control.label
+    if control.value then
+        label:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+    else
+        label:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+    end
+    control.checkbox:SetColor(ZO_NORMAL_TEXT:UnpackRGBA())
+end
+
+--controlName is optional
+function LAMCreateControl.checkbox(parent, checkboxData, controlName)
+    local control = LAM.util.CreateLabelAndContainerControl(parent, checkboxData, controlName)
+    control:SetHandler("OnMouseEnter", OnMouseEnter)
+    control:SetHandler("OnMouseExit", OnMouseExit)
+    control:SetHandler("OnMouseUp", function(control)
+        if control.isDisabled then return end
+        PlaySound(SOUNDS.DEFAULT_CLICK)
+        control.value = not control.value
+        control:UpdateValue(false, control.value)
+    end)
+
+    control.checkbox = wm:CreateControl(nil, control.container, CT_LABEL)
+    local checkbox = control.checkbox
+    checkbox:SetAnchor(LEFT, control.container, LEFT, 0, 0)
+    checkbox:SetFont("ZoFontGameBold")
+    checkbox:SetColor(ZO_NORMAL_TEXT:UnpackRGBA())
+    control.checkedText = GetString(SI_CHECK_BUTTON_ON):upper()
+    control.uncheckedText = GetString(SI_CHECK_BUTTON_OFF):upper()
+
+    if checkboxData.warning ~= nil or checkboxData.requiresReload then
+        control.warning = wm:CreateControlFromVirtual(nil, control, "ZO_Options_WarningIcon")
+        control.warning:SetAnchor(RIGHT, checkbox, LEFT, -5, 0)
+        control.UpdateWarning = LAM.util.UpdateWarning
+        control:UpdateWarning()
+    end
+
+    control.data.tooltipText = LAM.util.GetStringFromValue(checkboxData.tooltip)
+
+    control.UpdateValue = UpdateValue
+    control:UpdateValue()
+    if checkboxData.disabled ~= nil then
+        control.UpdateDisabled = UpdateDisabled
+        control:UpdateDisabled()
+    end
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+    LAM.util.RegisterForReloadIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/colorpicker.lua b/libs/LibAddonMenu-2.0/controls/colorpicker.lua
new file mode 100644
index 0000000..db21260
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/colorpicker.lua
@@ -0,0 +1,106 @@
+--[[colorpickerData = {
+    type = "colorpicker",
+    name = "My Color Picker", -- or string id or function returning a string
+    getFunc = function() return db.r, db.g, db.b, db.a end, --(alpha is optional)
+    setFunc = function(r,g,b,a) db.r=r, db.g=g, db.b=b, db.a=a end, --(alpha is optional)
+    tooltip = "Color Picker's tooltip text.", -- or string id or function returning a string (optional)
+    width = "full", --or "half" (optional)
+    disabled = function() return db.someBooleanSetting end, --or boolean (optional)
+    warning = "May cause permanent awesomeness.", -- or string id or function returning a string (optional)
+    requiresReload = false, -- boolean, if set to true, the warning text will contain a notice that changes are only applied after an UI reload and any change to the value will make the "Apply Settings" button appear on the panel which will reload the UI when pressed (optional)
+    default = {r = defaults.r, g = defaults.g, b = defaults.b, a = defaults.a}, --(optional) table of default color values (or default = defaultColor, where defaultColor is a table with keys of r, g, b[, a]) or a function that returns the color
+    reference = "MyAddonColorpicker" -- unique global reference to control (optional)
+} ]]
+
+
+local widgetVersion = 13
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("colorpicker", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+local function UpdateDisabled(control)
+    local disable
+    if type(control.data.disabled) == "function" then
+        disable = control.data.disabled()
+    else
+        disable = control.data.disabled
+    end
+
+    if disable then
+        control.label:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+    else
+        control.label:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+    end
+
+    control.isDisabled = disable
+end
+
+local function UpdateValue(control, forceDefault, valueR, valueG, valueB, valueA)
+    if forceDefault then --if we are forcing defaults
+        local color = LAM.util.GetDefaultValue(control.data.default)
+        valueR, valueG, valueB, valueA = color.r, color.g, color.b, color.a
+        control.data.setFunc(valueR, valueG, valueB, valueA)
+    elseif valueR and valueG and valueB then
+        control.data.setFunc(valueR, valueG, valueB, valueA or 1)
+        --after setting this value, let's refresh the others to see if any should be disabled or have their settings changed
+        LAM.util.RequestRefreshIfNeeded(control)
+    else
+        valueR, valueG, valueB, valueA = control.data.getFunc()
+    end
+
+    control.thumb:SetColor(valueR, valueG, valueB, valueA or 1)
+end
+
+function LAMCreateControl.colorpicker(parent, colorpickerData, controlName)
+    local control = LAM.util.CreateLabelAndContainerControl(parent, colorpickerData, controlName)
+
+    control.color = control.container
+    local color = control.color
+
+    control.thumb = wm:CreateControl(nil, color, CT_TEXTURE)
+    local thumb = control.thumb
+    thumb:SetDimensions(36, 18)
+    thumb:SetAnchor(LEFT, color, LEFT, 4, 0)
+
+    color.border = wm:CreateControl(nil, color, CT_TEXTURE)
+    local border = color.border
+    border:SetTexture("EsoUI\\Art\\ChatWindow\\chatOptions_bgColSwatch_frame.dds")
+    border:SetTextureCoords(0, .625, 0, .8125)
+    border:SetDimensions(40, 22)
+    border:SetAnchor(CENTER, thumb, CENTER, 0, 0)
+
+    local function ColorPickerCallback(r, g, b, a)
+        control:UpdateValue(false, r, g, b, a)
+    end
+
+    control:SetHandler("OnMouseUp", function(self, btn, upInside)
+        if self.isDisabled then return end
+
+        if upInside then
+            local r, g, b, a = colorpickerData.getFunc()
+            COLOR_PICKER:Show(ColorPickerCallback, r, g, b, a, LAM.util.GetStringFromValue(colorpickerData.name))
+        end
+    end)
+
+    if colorpickerData.warning ~= nil or colorpickerData.requiresReload then
+        control.warning = wm:CreateControlFromVirtual(nil, control, "ZO_Options_WarningIcon")
+        control.warning:SetAnchor(RIGHT, control.color, LEFT, -5, 0)
+        control.UpdateWarning = LAM.util.UpdateWarning
+        control:UpdateWarning()
+    end
+
+    control.data.tooltipText = LAM.util.GetStringFromValue(colorpickerData.tooltip)
+
+    control.UpdateValue = UpdateValue
+    control:UpdateValue()
+    if colorpickerData.disabled ~= nil then
+        control.UpdateDisabled = UpdateDisabled
+        control:UpdateDisabled()
+    end
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+    LAM.util.RegisterForReloadIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/custom.lua b/libs/LibAddonMenu-2.0/controls/custom.lua
new file mode 100644
index 0000000..5d6111c
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/custom.lua
@@ -0,0 +1,35 @@
+--[[customData = {
+    type = "custom",
+    reference = "MyAddonCustomControl", --(optional) unique name for your control to use as reference
+    refreshFunc = function(customControl) end, --(optional) function to call when panel/controls refresh
+    width = "full", --or "half" (optional)
+} ]]
+
+local widgetVersion = 7
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("custom", widgetVersion) then return end
+
+local function UpdateValue(control)
+    if control.data.refreshFunc then
+        control.data.refreshFunc(control)
+    end
+end
+
+local MIN_HEIGHT = 26
+function LAMCreateControl.custom(parent, customData, controlName)
+    local control = LAM.util.CreateBaseControl(parent, customData, controlName)
+    local width = control:GetWidth()
+    control:SetResizeToFitDescendents(true)
+
+    if control.isHalfWidth then --note these restrictions
+        control:SetDimensionConstraints(width / 2, MIN_HEIGHT, width / 2, MIN_HEIGHT * 4)
+    else
+        control:SetDimensionConstraints(width, MIN_HEIGHT, width, MIN_HEIGHT * 4)
+    end
+
+    control.UpdateValue = UpdateValue
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/description.lua b/libs/LibAddonMenu-2.0/controls/description.lua
new file mode 100644
index 0000000..27c7192
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/description.lua
@@ -0,0 +1,60 @@
+--[[descriptionData = {
+    type = "description",
+    text = "My description text to display.", -- or string id or function returning a string
+    title = "My Title", -- or string id or function returning a string (optional)
+    width = "full", --or "half" (optional)
+    reference = "MyAddonDescription" -- unique global reference to control (optional)
+} ]]
+
+
+local widgetVersion = 8
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("description", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+local function UpdateValue(control)
+    if control.title then
+        control.title:SetText(LAM.util.GetStringFromValue(control.data.title))
+    end
+    control.desc:SetText(LAM.util.GetStringFromValue(control.data.text))
+end
+
+function LAMCreateControl.description(parent, descriptionData, controlName)
+    local control = LAM.util.CreateBaseControl(parent, descriptionData, controlName)
+    local isHalfWidth = control.isHalfWidth
+    local width = control:GetWidth()
+    control:SetResizeToFitDescendents(true)
+
+    if isHalfWidth then
+        control:SetDimensionConstraints(width / 2, 0, width / 2, 0)
+    else
+        control:SetDimensionConstraints(width, 0, width, 0)
+    end
+
+    control.desc = wm:CreateControl(nil, control, CT_LABEL)
+    local desc = control.desc
+    desc:SetVerticalAlignment(TEXT_ALIGN_TOP)
+    desc:SetFont("ZoFontGame")
+    desc:SetText(LAM.util.GetStringFromValue(descriptionData.text))
+    desc:SetWidth(isHalfWidth and width / 2 or width)
+
+    if descriptionData.title then
+        control.title = wm:CreateControl(nil, control, CT_LABEL)
+        local title = control.title
+        title:SetWidth(isHalfWidth and width / 2 or width)
+        title:SetAnchor(TOPLEFT, control, TOPLEFT)
+        title:SetFont("ZoFontWinH4")
+        title:SetText(LAM.util.GetStringFromValue(descriptionData.title))
+        desc:SetAnchor(TOPLEFT, title, BOTTOMLEFT)
+    else
+        desc:SetAnchor(TOPLEFT)
+    end
+
+    control.UpdateValue = UpdateValue
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+
+    return control
+
+end
diff --git a/libs/LibAddonMenu-2.0/controls/divider.lua b/libs/LibAddonMenu-2.0/controls/divider.lua
new file mode 100644
index 0000000..b24e2fc
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/divider.lua
@@ -0,0 +1,45 @@
+--[[dividerData = {
+    type = "divider",
+    width = "full", --or "half" (optional)
+    height = 10, (optional)
+    alpha = 0.25, (optional)
+    reference = "MyAddonDivider" -- unique global reference to control (optional)
+} ]]
+
+
+local widgetVersion = 2
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("divider", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+local MIN_HEIGHT = 10
+local MAX_HEIGHT = 50
+local MIN_ALPHA = 0
+local MAX_ALPHA = 1
+local DEFAULT_ALPHA = 0.25
+
+local function GetValueInRange(value, min, max, default)
+    if not value or type(value) ~= "number" then
+        return default
+    end
+    return math.min(math.max(min, value), max)
+end
+
+function LAMCreateControl.divider(parent, dividerData, controlName)
+    local control = LAM.util.CreateBaseControl(parent, dividerData, controlName)
+    local isHalfWidth = control.isHalfWidth
+    local width = control:GetWidth()
+    local height = GetValueInRange(dividerData.height, MIN_HEIGHT, MAX_HEIGHT, MIN_HEIGHT)
+    local alpha = GetValueInRange(dividerData.alpha, MIN_ALPHA, MAX_ALPHA, DEFAULT_ALPHA)
+
+    control:SetDimensions(isHalfWidth and width / 2 or width, height)
+
+    control.divider = wm:CreateControlFromVirtual(nil, control, "ZO_Options_Divider")
+    local divider = control.divider
+    divider:SetWidth(isHalfWidth and width / 2 or width)
+    divider:SetAnchor(TOPLEFT)
+    divider:SetAlpha(alpha)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/dropdown.lua b/libs/LibAddonMenu-2.0/controls/dropdown.lua
new file mode 100644
index 0000000..29ad022
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/dropdown.lua
@@ -0,0 +1,211 @@
+--[[dropdownData = {
+    type = "dropdown",
+    name = "My Dropdown", -- or string id or function returning a string
+    choices = {"table", "of", "choices"},
+    choicesValues = {"foo", 2, "three"}, -- if specified, these values will get passed to setFunc instead (optional)
+    getFunc = function() return db.var end,
+    setFunc = function(var) db.var = var doStuff() end,
+    tooltip = "Dropdown's tooltip text.", -- or string id or function returning a string (optional)
+    choicesTooltips = {"tooltip 1", "tooltip 2", "tooltip 3"}, -- or array of string ids or array of functions returning a string (optional)
+    sort = "name-up", --or "name-down", "numeric-up", "numeric-down", "value-up", "value-down", "numericvalue-up", "numericvalue-down" (optional) - if not provided, list will not be sorted
+    width = "full", --or "half" (optional)
+    disabled = function() return db.someBooleanSetting end, --or boolean (optional)
+    warning = "May cause permanent awesomeness.", -- or string id or function returning a string (optional)
+    requiresReload = false, -- boolean, if set to true, the warning text will contain a notice that changes are only applied after an UI reload and any change to the value will make the "Apply Settings" button appear on the panel which will reload the UI when pressed (optional)
+    default = defaults.var, -- default value or function that returns the default value (optional)
+    reference = "MyAddonDropdown" -- unique global reference to control (optional)
+} ]]
+
+
+local widgetVersion = 16
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("dropdown", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+local SORT_BY_VALUE         = { ["value"] = {} }
+local SORT_BY_VALUE_NUMERIC = { ["value"] = { isNumeric = true } }
+local SORT_TYPES = {
+    name = ZO_SORT_BY_NAME,
+    numeric = ZO_SORT_BY_NAME_NUMERIC,
+    value = SORT_BY_VALUE,
+    numericvalue = SORT_BY_VALUE_NUMERIC,
+}
+local SORT_ORDERS = {
+    up = ZO_SORT_ORDER_UP,
+    down = ZO_SORT_ORDER_DOWN,
+}
+
+local function UpdateDisabled(control)
+    local disable
+    if type(control.data.disabled) == "function" then
+        disable = control.data.disabled()
+    else
+        disable = control.data.disabled
+    end
+
+    control.dropdown:SetEnabled(not disable)
+    if disable then
+        control.label:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+    else
+        control.label:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+    end
+end
+
+local function UpdateValue(control, forceDefault, value)
+    if forceDefault then --if we are forcing defaults
+        value = LAM.util.GetDefaultValue(control.data.default)
+        control.data.setFunc(value)
+        control.dropdown:SetSelectedItem(control.choices[value])
+    elseif value then
+        control.data.setFunc(value)
+        --after setting this value, let's refresh the others to see if any should be disabled or have their settings changed
+        LAM.util.RequestRefreshIfNeeded(control)
+    else
+        value = control.data.getFunc()
+        control.dropdown:SetSelectedItem(control.choices[value])
+    end
+end
+
+local function DropdownCallback(control, choiceText, choice)
+    choice.control:UpdateValue(false, choice.value or choiceText)
+end
+
+local function SetupTooltips(comboBox, choicesTooltips)
+    local function ShowTooltip(control)
+        InitializeTooltip(InformationTooltip, control, TOPLEFT, 0, 0, BOTTOMRIGHT)
+        SetTooltipText(InformationTooltip, LAM.util.GetStringFromValue(control.tooltip))
+        InformationTooltipTopLevel:BringWindowToTop()
+    end
+    local function HideTooltip(control)
+        ClearTooltip(InformationTooltip)
+    end
+
+    -- allow for tooltips on the drop down entries
+    local originalShow = comboBox.ShowDropdownInternal
+    comboBox.ShowDropdownInternal = function(comboBox)
+        originalShow(comboBox)
+        local entries = ZO_Menu.items
+        for i = 1, #entries do
+            local entry = entries[i]
+            local control = entries[i].item
+            control.tooltip = choicesTooltips[i]
+            entry.onMouseEnter = control:GetHandler("OnMouseEnter")
+            entry.onMouseExit = control:GetHandler("OnMouseExit")
+            ZO_PreHookHandler(control, "OnMouseEnter", ShowTooltip)
+            ZO_PreHookHandler(control, "OnMouseExit", HideTooltip)
+        end
+    end
+
+    local originalHide = comboBox.HideDropdownInternal
+    comboBox.HideDropdownInternal = function(self)
+        local entries = ZO_Menu.items
+        for i = 1, #entries do
+            local entry = entries[i]
+            local control = entries[i].item
+            control:SetHandler("OnMouseEnter", entry.onMouseEnter)
+            control:SetHandler("OnMouseExit", entry.onMouseExit)
+            control.tooltip = nil
+        end
+        originalHide(self)
+    end
+end
+
+local function UpdateChoices(control, choices, choicesValues, choicesTooltips)
+    control.dropdown:ClearItems() --remove previous choices --(need to call :SetSelectedItem()?)
+    ZO_ClearTable(control.choices)
+
+    --build new list of choices
+    local choices = choices or control.data.choices
+    local choicesValues = choicesValues or control.data.choicesValues
+    local choicesTooltips = choicesTooltips or control.data.choicesTooltips
+
+    if choicesValues then
+        assert(#choices == #choicesValues, "choices and choicesValues need to have the same size")
+    end
+
+    if choicesTooltips then
+        assert(#choices == #choicesTooltips, "choices and choicesTooltips need to have the same size")
+        SetupTooltips(control.dropdown, choicesTooltips)
+    end
+
+    for i = 1, #choices do
+        local entry = control.dropdown:CreateItemEntry(choices[i], DropdownCallback)
+        entry.control = control
+        if choicesValues then
+            entry.value = choicesValues[i]
+        end
+        control.choices[entry.value or entry.name] = entry.name
+        control.dropdown:AddItem(entry, not control.data.sort and ZO_COMBOBOX_SUPRESS_UPDATE) --if sort type/order isn't specified, then don't sort
+    end
+end
+
+local function GrabSortingInfo(sortInfo)
+    local t, i = {}, 1
+    for info in string.gmatch(sortInfo, "([^%-]+)") do
+        t[i] = info
+        i = i + 1
+    end
+
+    return t
+end
+
+function LAMCreateControl.dropdown(parent, dropdownData, controlName)
+    local control = LAM.util.CreateLabelAndContainerControl(parent, dropdownData, controlName)
+    control.choices = {}
+
+    local countControl = parent
+    local name = parent:GetName()
+    if not name or #name == 0 then
+        countControl = LAMCreateControl
+        name = "LAM"
+    end
+    local comboboxCount = (countControl.comboboxCount or 0) + 1
+    countControl.comboboxCount = comboboxCount
+    control.combobox = wm:CreateControlFromVirtual(zo_strjoin(nil, name, "Combobox", comboboxCount), control.container, "ZO_ComboBox")
+
+    local combobox = control.combobox
+    combobox:SetAnchor(TOPLEFT)
+    combobox:SetDimensions(control.container:GetDimensions())
+    combobox:SetHandler("OnMouseEnter", function() ZO_Options_OnMouseEnter(control) end)
+    combobox:SetHandler("OnMouseExit", function() ZO_Options_OnMouseExit(control) end)
+    control.dropdown = ZO_ComboBox_ObjectFromContainer(combobox)
+    local dropdown = control.dropdown
+    dropdown:SetSortsItems(false) -- need to sort ourselves in order to be able to sort by value
+
+    ZO_PreHook(dropdown, "UpdateItems", function(self)
+        assert(not self.m_sortsItems, "built-in dropdown sorting was reactivated, sorting is handled by LAM")
+        if control.m_sortOrder ~= nil and control.m_sortType then
+            local sortKey = next(control.m_sortType)
+            local sortFunc = function(item1, item2) return ZO_TableOrderingFunction(item1, item2, sortKey, control.m_sortType, control.m_sortOrder) end
+            table.sort(self.m_sortedItems, sortFunc)
+        end
+    end)
+
+    if dropdownData.sort then
+        local sortInfo = GrabSortingInfo(dropdownData.sort)
+        control.m_sortType, control.m_sortOrder = SORT_TYPES[sortInfo[1]], SORT_ORDERS[sortInfo[2]]
+    elseif dropdownData.choicesValues then
+        control.m_sortType, control.m_sortOrder = ZO_SORT_ORDER_UP, SORT_BY_VALUE
+    end
+
+    if dropdownData.warning ~= nil or dropdownData.requiresReload then
+        control.warning = wm:CreateControlFromVirtual(nil, control, "ZO_Options_WarningIcon")
+        control.warning:SetAnchor(RIGHT, combobox, LEFT, -5, 0)
+        control.UpdateWarning = LAM.util.UpdateWarning
+        control:UpdateWarning()
+    end
+
+    control.UpdateChoices = UpdateChoices
+    control:UpdateChoices(dropdownData.choices, dropdownData.choicesValues)
+    control.UpdateValue = UpdateValue
+    control:UpdateValue()
+    if dropdownData.disabled ~= nil then
+        control.UpdateDisabled = UpdateDisabled
+        control:UpdateDisabled()
+    end
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+    LAM.util.RegisterForReloadIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/editbox.lua b/libs/LibAddonMenu-2.0/controls/editbox.lua
new file mode 100644
index 0000000..bcc6a7c
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/editbox.lua
@@ -0,0 +1,156 @@
+--[[editboxData = {
+    type = "editbox",
+    name = "My Editbox", -- or string id or function returning a string
+    getFunc = function() return db.text end,
+    setFunc = function(text) db.text = text doStuff() end,
+    tooltip = "Editbox's tooltip text.", -- or string id or function returning a string (optional)
+    isMultiline = true, --boolean (optional)
+    isExtraWide = true, --boolean (optional)
+    width = "full", --or "half" (optional)
+    disabled = function() return db.someBooleanSetting end, --or boolean (optional)
+    warning = "May cause permanent awesomeness.", -- or string id or function returning a string (optional)
+    requiresReload = false, -- boolean, if set to true, the warning text will contain a notice that changes are only applied after an UI reload and any change to the value will make the "Apply Settings" button appear on the panel which will reload the UI when pressed (optional)
+    default = defaults.text, -- default value or function that returns the default value (optional)
+    reference = "MyAddonEditbox" -- unique global reference to control (optional)
+} ]]
+
+
+local widgetVersion = 14
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("editbox", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+local function UpdateDisabled(control)
+    local disable
+    if type(control.data.disabled) == "function" then
+        disable = control.data.disabled()
+    else
+        disable = control.data.disabled
+    end
+
+    if disable then
+        control.label:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+        control.editbox:SetColor(ZO_DEFAULT_DISABLED_MOUSEOVER_COLOR:UnpackRGBA())
+    else
+        control.label:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+        control.editbox:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+    end
+    --control.editbox:SetEditEnabled(not disable)
+    control.editbox:SetMouseEnabled(not disable)
+end
+
+local function UpdateValue(control, forceDefault, value)
+    if forceDefault then --if we are forcing defaults
+        value = LAM.util.GetDefaultValue(control.data.default)
+        control.data.setFunc(value)
+        control.editbox:SetText(value)
+    elseif value then
+        control.data.setFunc(value)
+        --after setting this value, let's refresh the others to see if any should be disabled or have their settings changed
+        LAM.util.RequestRefreshIfNeeded(control)
+    else
+        value = control.data.getFunc()
+        control.editbox:SetText(value)
+    end
+end
+
+local MIN_HEIGHT = 24
+local HALF_WIDTH_LINE_SPACING = 2
+function LAMCreateControl.editbox(parent, editboxData, controlName)
+    local control = LAM.util.CreateLabelAndContainerControl(parent, editboxData, controlName)
+
+    local container = control.container
+    control.bg = wm:CreateControlFromVirtual(nil, container, "ZO_EditBackdrop")
+    local bg = control.bg
+    bg:SetAnchorFill()
+
+    if editboxData.isMultiline then
+        control.editbox = wm:CreateControlFromVirtual(nil, bg, "ZO_DefaultEditMultiLineForBackdrop")
+        control.editbox:SetHandler("OnMouseWheel", function(self, delta)
+            if self:HasFocus() then --only set focus to new spots if the editbox is currently in use
+                local cursorPos = self:GetCursorPosition()
+                local text = self:GetText()
+                local textLen = text:len()
+                local newPos
+                if delta > 0 then --scrolling up
+                    local reverseText = text:reverse()
+                    local revCursorPos = textLen - cursorPos
+                    local revPos = reverseText:find("\n", revCursorPos+1)
+                    newPos = revPos and textLen - revPos
+                else --scrolling down
+                    newPos = text:find("\n", cursorPos+1)
+                end
+                if newPos then --if we found a new line, then scroll, otherwise don't
+                    self:SetCursorPosition(newPos)
+                end
+            end
+        end)
+    else
+        control.editbox = wm:CreateControlFromVirtual(nil, bg, "ZO_DefaultEditForBackdrop")
+    end
+    local editbox = control.editbox
+    editbox:SetText(editboxData.getFunc())
+    editbox:SetMaxInputChars(3000)
+    editbox:SetHandler("OnFocusLost", function(self) control:UpdateValue(false, self:GetText()) end)
+    editbox:SetHandler("OnEscape", function(self) self:LoseFocus() control:UpdateValue(false, self:GetText()) end)
+    editbox:SetHandler("OnMouseEnter", function() ZO_Options_OnMouseEnter(control) end)
+    editbox:SetHandler("OnMouseExit", function() ZO_Options_OnMouseExit(control) end)
+
+    local MIN_WIDTH = (parent.GetWidth and (parent:GetWidth() / 10)) or (parent.panel.GetWidth and (parent.panel:GetWidth() / 10)) or 0
+
+    control.label:ClearAnchors()
+    container:ClearAnchors()
+
+    control.label:SetAnchor(TOPLEFT, control, TOPLEFT, 0, 0)
+    container:SetAnchor(BOTTOMRIGHT, control, BOTTOMRIGHT, 0, 0)
+
+    if control.isHalfWidth then
+        container:SetAnchor(BOTTOMRIGHT, control, BOTTOMRIGHT, 0, 0)
+    end
+
+    if editboxData.isExtraWide then
+        container:SetAnchor(BOTTOMLEFT, control, BOTTOMLEFT, 0, 0)
+    else
+        container:SetWidth(MIN_WIDTH * 3.2)
+    end
+
+    if editboxData.isMultiline then
+        container:SetHeight(MIN_HEIGHT * 3)
+    else
+        container:SetHeight(MIN_HEIGHT)
+    end
+
+    if control.isHalfWidth ~= true and editboxData.isExtraWide ~= true then
+        control:SetHeight(container:GetHeight())
+    else
+        control:SetHeight(container:GetHeight() + control.label:GetHeight())
+    end
+
+    editbox:ClearAnchors()
+    editbox:SetAnchor(TOPLEFT, container, TOPLEFT, 2, 2)
+    editbox:SetAnchor(BOTTOMRIGHT, container, BOTTOMRIGHT, -2, -2)
+
+    if editboxData.warning ~= nil or editboxData.requiresReload then
+        control.warning = wm:CreateControlFromVirtual(nil, control, "ZO_Options_WarningIcon")
+        if editboxData.isExtraWide then
+            control.warning:SetAnchor(BOTTOMRIGHT, control.bg, TOPRIGHT, 2, 0)
+        else
+            control.warning:SetAnchor(TOPRIGHT, control.bg, TOPLEFT, -5, 0)
+        end
+        control.UpdateWarning = LAM.util.UpdateWarning
+        control:UpdateWarning()
+    end
+
+    control.UpdateValue = UpdateValue
+    control:UpdateValue()
+    if editboxData.disabled ~= nil then
+        control.UpdateDisabled = UpdateDisabled
+        control:UpdateDisabled()
+    end
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+    LAM.util.RegisterForReloadIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/header.lua b/libs/LibAddonMenu-2.0/controls/header.lua
new file mode 100644
index 0000000..3290c89
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/header.lua
@@ -0,0 +1,42 @@
+--[[headerData = {
+    type = "header",
+    name = "My Header", -- or string id or function returning a string
+    width = "full", --or "half" (optional)
+    reference = "MyAddonHeader" -- unique global reference to control (optional)
+} ]]
+
+
+local widgetVersion = 8
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("header", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+local function UpdateValue(control)
+    control.header:SetText(LAM.util.GetStringFromValue(control.data.name))
+end
+
+local MIN_HEIGHT = 30
+function LAMCreateControl.header(parent, headerData, controlName)
+    local control = LAM.util.CreateBaseControl(parent, headerData, controlName)
+    local isHalfWidth = control.isHalfWidth
+    local width = control:GetWidth()
+    control:SetDimensions(isHalfWidth and width / 2 or width, MIN_HEIGHT)
+
+    control.divider = wm:CreateControlFromVirtual(nil, control, "ZO_Options_Divider")
+    local divider = control.divider
+    divider:SetWidth(isHalfWidth and width / 2 or width)
+    divider:SetAnchor(TOPLEFT)
+
+    control.header = wm:CreateControlFromVirtual(nil, control, "ZO_Options_SectionTitleLabel")
+    local header = control.header
+    header:SetAnchor(TOPLEFT, divider, BOTTOMLEFT)
+    header:SetAnchor(BOTTOMRIGHT)
+    header:SetText(LAM.util.GetStringFromValue(headerData.name))
+
+    control.UpdateValue = UpdateValue
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/iconpicker.lua b/libs/LibAddonMenu-2.0/controls/iconpicker.lua
new file mode 100644
index 0000000..3485740
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/iconpicker.lua
@@ -0,0 +1,436 @@
+--[[iconpickerData = {
+    type = "iconpicker",
+    name = "My Icon Picker", -- or string id or function returning a string
+    choices = {"texture path 1", "texture path 2", "texture path 3"},
+    getFunc = function() return db.var end,
+    setFunc = function(var) db.var = var doStuff() end,
+    tooltip = "Color Picker's tooltip text.", -- or string id or function returning a string (optional)
+    choicesTooltips = {"icon tooltip 1", "icon tooltip 2", "icon tooltip 3"}, -- or array of string ids or array of functions returning a string (optional)
+    maxColumns = 5, -- number of icons in one row (optional)
+    visibleRows = 4.5, -- number of visible rows (optional)
+    iconSize = 28, -- size of the icons (optional)
+    defaultColor = ZO_ColorDef:New("FFFFFF"), -- default color of the icons (optional)
+    width = "full", --or "half" (optional)
+    beforeShow = function(control, iconPicker) return preventShow end, --(optional)
+    disabled = function() return db.someBooleanSetting end, --or boolean (optional)
+    warning = "May cause permanent awesomeness.", -- or string id or function returning a string (optional)
+    requiresReload = false, -- boolean, if set to true, the warning text will contain a notice that changes are only applied after an UI reload and any change to the value will make the "Apply Settings" button appear on the panel which will reload the UI when pressed (optional)
+    default = defaults.var, -- default value or function that returns the default value (optional)
+    reference = "MyAddonIconPicker" -- unique global reference to control (optional)
+} ]]
+
+local widgetVersion = 8
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("iconpicker", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+local IconPickerMenu = ZO_Object:Subclass()
+local iconPicker
+LAM.util.GetIconPickerMenu = function()
+    if not iconPicker then
+        iconPicker = IconPickerMenu:New("LAMIconPicker")
+        local sceneFragment = LAM:GetAddonSettingsFragment()
+        ZO_PreHook(sceneFragment, "OnHidden", function()
+            if not iconPicker.control:IsHidden() then
+                iconPicker:Clear()
+            end
+        end)
+    end
+    return iconPicker
+end
+
+function IconPickerMenu:New(...)
+    local object = ZO_Object.New(self)
+    object:Initialize(...)
+    return object
+end
+
+function IconPickerMenu:Initialize(name)
+    local control = wm:CreateTopLevelWindow(name)
+    control:SetDrawTier(DT_HIGH)
+    control:SetHidden(true)
+    self.control = control
+
+    local scrollContainer = wm:CreateControlFromVirtual(name .. "ScrollContainer", control, "ZO_ScrollContainer")
+    -- control:SetDimensions(control.container:GetWidth(), height) -- adjust to icon size / col count
+    scrollContainer:SetAnchorFill()
+    ZO_Scroll_SetUseFadeGradient(scrollContainer, false)
+    ZO_Scroll_SetHideScrollbarOnDisable(scrollContainer, false)
+    ZO_VerticalScrollbarBase_OnMouseExit(scrollContainer:GetNamedChild("ScrollBar")) -- scrollbar initialization seems to be broken so we force it to update the correct alpha value
+    local scroll = GetControl(scrollContainer, "ScrollChild")
+    self.scroll = scroll
+    self.scrollContainer = scrollContainer
+
+    local bg = wm:CreateControl(nil, scrollContainer, CT_BACKDROP)
+    bg:SetAnchor(TOPLEFT, scrollContainer, TOPLEFT, 0, -3)
+    bg:SetAnchor(BOTTOMRIGHT, scrollContainer, BOTTOMRIGHT, 2, 5)
+    bg:SetEdgeTexture("EsoUI\\Art\\Tooltips\\UI-Border.dds", 128, 16)
+    bg:SetCenterTexture("EsoUI\\Art\\Tooltips\\UI-TooltipCenter.dds")
+    bg:SetInsets(16, 16, -16, -16)
+
+    local mungeOverlay = wm:CreateControl(nil, bg, CT_TEXTURE)
+    mungeOverlay:SetTexture("EsoUI/Art/Tooltips/munge_overlay.dds")
+    mungeOverlay:SetDrawLevel(1)
+    mungeOverlay:SetAddressMode(TEX_MODE_WRAP)
+    mungeOverlay:SetAnchorFill()
+
+    local mouseOver = wm:CreateControl(nil, scrollContainer, CT_TEXTURE)
+    mouseOver:SetDrawLevel(2)
+    mouseOver:SetTexture("EsoUI/Art/Buttons/minmax_mouseover.dds")
+    mouseOver:SetHidden(true)
+
+    local function IconFactory(pool)
+        local icon = wm:CreateControl(name .. "Entry" .. pool:GetNextControlId(), scroll, CT_TEXTURE)
+        icon:SetMouseEnabled(true)
+        icon:SetDrawLevel(3)
+        icon:SetHandler("OnMouseEnter", function()
+            mouseOver:SetAnchor(TOPLEFT, icon, TOPLEFT, 0, 0)
+            mouseOver:SetAnchor(BOTTOMRIGHT, icon, BOTTOMRIGHT, 0, 0)
+            mouseOver:SetHidden(false)
+            if self.customOnMouseEnter then
+                self.customOnMouseEnter(icon)
+            else
+                self:OnMouseEnter(icon)
+            end
+        end)
+        icon:SetHandler("OnMouseExit", function()
+            mouseOver:ClearAnchors()
+            mouseOver:SetHidden(true)
+            if self.customOnMouseExit then
+                self.customOnMouseExit(icon)
+            else
+                self:OnMouseExit(icon)
+            end
+        end)
+        icon:SetHandler("OnMouseUp", function(control, ...)
+            PlaySound("Click")
+            icon.OnSelect(icon, icon.texture)
+            self:Clear()
+        end)
+        return icon
+    end
+
+    local function ResetFunction(icon)
+        icon:ClearAnchors()
+    end
+
+    self.iconPool = ZO_ObjectPool:New(IconFactory, ResetFunction)
+    self:SetMaxColumns(1)
+    self.icons = {}
+    self.color = ZO_DEFAULT_ENABLED_COLOR
+
+    EVENT_MANAGER:RegisterForEvent(name .. "_OnGlobalMouseUp", EVENT_GLOBAL_MOUSE_UP, function()
+        if self.refCount ~= nil then
+            local moc = wm:GetMouseOverControl()
+            if(moc:GetOwningWindow() ~= control) then
+                self.refCount = self.refCount - 1
+                if self.refCount <= 0 then
+                    self:Clear()
+                end
+            end
+        end
+    end)
+end
+
+function IconPickerMenu:OnMouseEnter(icon)
+    InitializeTooltip(InformationTooltip, icon, TOPLEFT, 0, 0, BOTTOMRIGHT)
+    SetTooltipText(InformationTooltip, LAM.util.GetStringFromValue(icon.tooltip))
+    InformationTooltipTopLevel:BringWindowToTop()
+end
+
+function IconPickerMenu:OnMouseExit(icon)
+    ClearTooltip(InformationTooltip)
+end
+
+function IconPickerMenu:SetMaxColumns(value)
+    self.maxCols = value ~= nil and value or 5
+end
+
+local DEFAULT_SIZE = 28
+function IconPickerMenu:SetIconSize(value)
+    local iconSize = DEFAULT_SIZE
+    if value ~= nil then iconSize = math.max(iconSize, value) end
+    self.iconSize = iconSize
+end
+
+function IconPickerMenu:SetVisibleRows(value)
+    self.visibleRows = value ~= nil and value or 4.5
+end
+
+function IconPickerMenu:SetMouseHandlers(onEnter, onExit)
+    self.customOnMouseEnter = onEnter
+    self.customOnMouseExit = onExit
+end
+
+function IconPickerMenu:UpdateDimensions()
+    local iconSize = self.iconSize
+    local width = iconSize * self.maxCols + 20
+    local height = iconSize * self.visibleRows
+    self.control:SetDimensions(width, height)
+
+    local icons = self.icons
+    for i = 1, #icons do
+        local icon = icons[i]
+        icon:SetDimensions(iconSize, iconSize)
+    end
+end
+
+function IconPickerMenu:UpdateAnchors()
+    local iconSize = self.iconSize
+    local col, maxCols = 1, self.maxCols
+    local previousCol, previousRow
+    local scroll = self.scroll
+    local icons = self.icons
+
+    for i = 1, #icons do
+        local icon = icons[i]
+        icon:ClearAnchors()
+        if i == 1 then
+            icon:SetAnchor(TOPLEFT, scroll, TOPLEFT, 0, 0)
+            previousRow = icon
+        elseif col == 1 then
+            icon:SetAnchor(TOPLEFT, previousRow, BOTTOMLEFT, 0, 0)
+            previousRow = icon
+        else
+            icon:SetAnchor(TOPLEFT, previousCol, TOPRIGHT, 0, 0)
+        end
+        previousCol = icon
+        col = col >= maxCols and 1 or col + 1
+    end
+end
+
+function IconPickerMenu:Clear()
+    self.icons = {}
+    self.iconPool:ReleaseAllObjects()
+    self.control:SetHidden(true)
+    self.color = ZO_DEFAULT_ENABLED_COLOR
+    self.refCount = nil
+    self.parent = nil
+    self.customOnMouseEnter = nil
+    self.customOnMouseExit = nil
+end
+
+function IconPickerMenu:AddIcon(texturePath, callback, tooltip)
+    local icon, key = self.iconPool:AcquireObject()
+    icon:SetTexture(texturePath)
+    icon:SetColor(self.color:UnpackRGBA())
+    icon.texture = texturePath
+    icon.tooltip = tooltip
+    icon.OnSelect = callback
+    self.icons[#self.icons + 1] = icon
+end
+
+function IconPickerMenu:Show(parent)
+    if #self.icons == 0 then return false end
+    if not self.control:IsHidden() then self:Clear() return false end
+    self:UpdateDimensions()
+    self:UpdateAnchors()
+
+    local control = self.control
+    control:ClearAnchors()
+    control:SetAnchor(TOPLEFT, parent, BOTTOMLEFT, 0, 8)
+    control:SetHidden(false)
+    control:BringWindowToTop()
+    self.parent = parent
+    self.refCount = 2
+
+    return true
+end
+
+function IconPickerMenu:SetColor(color)
+    local icons = self.icons
+    self.color = color
+    for i = 1, #icons do
+        local icon = icons[i]
+        icon:SetColor(color:UnpackRGBA())
+    end
+end
+
+-------------------------------------------------------------
+
+local function UpdateChoices(control, choices, choicesTooltips)
+    local data = control.data
+    if not choices then
+        choices, choicesTooltips = data.choices, data.choicesTooltips or {}
+    end
+    local addedChoices = {}
+
+    local iconPicker = LAM.util.GetIconPickerMenu()
+    iconPicker:Clear()
+    for i = 1, #choices do
+        local texture = choices[i]
+        if not addedChoices[texture] then -- remove duplicates
+            iconPicker:AddIcon(choices[i], function(self, texture)
+                control.icon:SetTexture(texture)
+                data.setFunc(texture)
+                LAM.util.RequestRefreshIfNeeded(control)
+            end, LAM.util.GetStringFromValue(choicesTooltips[i]))
+        addedChoices[texture] = true
+        end
+    end
+end
+
+local function IsDisabled(control)
+    if type(control.data.disabled) == "function" then
+        return control.data.disabled()
+    else
+        return control.data.disabled
+    end
+end
+
+local function SetColor(control, color)
+    local icon = control.icon
+    if IsDisabled(control) then
+        icon:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+    else
+        icon.color = color or control.data.defaultColor or ZO_DEFAULT_ENABLED_COLOR
+        icon:SetColor(icon.color:UnpackRGBA())
+    end
+
+    local iconPicker = LAM.util.GetIconPickerMenu()
+    if iconPicker.parent == control.container and not iconPicker.control:IsHidden() then
+        iconPicker:SetColor(icon.color)
+    end
+end
+
+local function UpdateDisabled(control)
+    local disable = IsDisabled(control)
+
+    control.dropdown:SetMouseEnabled(not disable)
+    control.dropdownButton:SetEnabled(not disable)
+
+    local iconPicker = LAM.util.GetIconPickerMenu()
+    if iconPicker.parent == control.container and not iconPicker.control:IsHidden() then
+        iconPicker:Clear()
+    end
+
+    SetColor(control, control.icon.color)
+    if disable then
+        control.label:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+    else
+        control.label:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+    end
+end
+
+local function UpdateValue(control, forceDefault, value)
+    if forceDefault then --if we are forcing defaults
+        value = LAM.util.GetDefaultValue(control.data.default)
+        control.data.setFunc(value)
+        control.icon:SetTexture(value)
+    elseif value then
+        control.data.setFunc(value)
+        --after setting this value, let's refresh the others to see if any should be disabled or have their settings changed
+        LAM.util.RequestRefreshIfNeeded(control)
+    else
+        value = control.data.getFunc()
+        control.icon:SetTexture(value)
+    end
+end
+
+local MIN_HEIGHT = 26
+local HALF_WIDTH_LINE_SPACING = 2
+local function SetIconSize(control, size)
+    local icon = control.icon
+    icon.size = size
+    icon:SetDimensions(size, size)
+
+    local height = size + 4
+    control.dropdown:SetDimensions(size + 20, height)
+    height = math.max(height, MIN_HEIGHT)
+    control.container:SetHeight(height)
+    if control.lineControl then
+        control.lineControl:SetHeight(MIN_HEIGHT + size + HALF_WIDTH_LINE_SPACING)
+    else
+        control:SetHeight(height)
+    end
+
+    local iconPicker = LAM.util.GetIconPickerMenu()
+    if iconPicker.parent == control.container and not iconPicker.control:IsHidden() then
+        iconPicker:SetIconSize(size)
+        iconPicker:UpdateDimensions()
+        iconPicker:UpdateAnchors()
+    end
+end
+
+function LAMCreateControl.iconpicker(parent, iconpickerData, controlName)
+    local control = LAM.util.CreateLabelAndContainerControl(parent, iconpickerData, controlName)
+
+    local function ShowIconPicker()
+        local iconPicker = LAM.util.GetIconPickerMenu()
+        if iconPicker.parent == control.container then
+            iconPicker:Clear()
+        else
+            iconPicker:SetMaxColumns(iconpickerData.maxColumns)
+            iconPicker:SetVisibleRows(iconpickerData.visibleRows)
+            iconPicker:SetIconSize(control.icon.size)
+            UpdateChoices(control)
+            iconPicker:SetColor(control.icon.color)
+            if iconpickerData.beforeShow then
+                if iconpickerData.beforeShow(control, iconPicker) then
+                    iconPicker:Clear()
+                    return
+                end
+            end
+            iconPicker:Show(control.container)
+        end
+    end
+
+    local iconSize = iconpickerData.iconSize ~= nil and iconpickerData.iconSize or DEFAULT_SIZE
+    control.dropdown = wm:CreateControl(nil, control.container, CT_CONTROL)
+    local dropdown = control.dropdown
+    dropdown:SetAnchor(LEFT, control.container, LEFT, 0, 0)
+    dropdown:SetMouseEnabled(true)
+    dropdown:SetHandler("OnMouseUp", ShowIconPicker)
+    dropdown:SetHandler("OnMouseEnter", function() ZO_Options_OnMouseEnter(control) end)
+    dropdown:SetHandler("OnMouseExit", function() ZO_Options_OnMouseExit(control) end)
+
+    control.icon = wm:CreateControl(nil, dropdown, CT_TEXTURE)
+    local icon = control.icon
+    icon:SetAnchor(LEFT, dropdown, LEFT, 3, 0)
+    icon:SetDrawLevel(2)
+
+    local dropdownButton = wm:CreateControlFromVirtual(nil, dropdown, "ZO_DropdownButton")
+    dropdownButton:SetDimensions(16, 16)
+    dropdownButton:SetHandler("OnClicked", ShowIconPicker)
+    dropdownButton:SetAnchor(RIGHT, dropdown, RIGHT, -3, 0)
+    control.dropdownButton = dropdownButton
+
+    control.bg = wm:CreateControl(nil, dropdown, CT_BACKDROP)
+    local bg = control.bg
+    bg:SetAnchor(TOPLEFT, dropdown, TOPLEFT, 0, -3)
+    bg:SetAnchor(BOTTOMRIGHT, dropdown, BOTTOMRIGHT, 2, 5)
+    bg:SetEdgeTexture("EsoUI/Art/Tooltips/UI-Border.dds", 128, 16)
+    bg:SetCenterTexture("EsoUI/Art/Tooltips/UI-TooltipCenter.dds")
+    bg:SetInsets(16, 16, -16, -16)
+    local mungeOverlay = wm:CreateControl(nil, bg, CT_TEXTURE)
+    mungeOverlay:SetTexture("EsoUI/Art/Tooltips/munge_overlay.dds")
+    mungeOverlay:SetDrawLevel(1)
+    mungeOverlay:SetAddressMode(TEX_MODE_WRAP)
+    mungeOverlay:SetAnchorFill()
+
+    if iconpickerData.warning ~= nil or iconpickerData.requiresReload then
+        control.warning = wm:CreateControlFromVirtual(nil, control, "ZO_Options_WarningIcon")
+        control.warning:SetAnchor(RIGHT, control.container, LEFT, -5, 0)
+        control.UpdateWarning = LAM.util.UpdateWarning
+        control:UpdateWarning()
+    end
+
+    control.UpdateChoices = UpdateChoices
+    control.UpdateValue = UpdateValue
+    control:UpdateValue()
+    control.SetColor = SetColor
+    control:SetColor()
+    control.SetIconSize = SetIconSize
+    control:SetIconSize(iconSize)
+
+    if iconpickerData.disabled ~= nil then
+        control.UpdateDisabled = UpdateDisabled
+        control:UpdateDisabled()
+    end
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+    LAM.util.RegisterForReloadIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/panel.lua b/libs/LibAddonMenu-2.0/controls/panel.lua
new file mode 100644
index 0000000..d6956d8
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/panel.lua
@@ -0,0 +1,126 @@
+--[[panelData = {
+    type = "panel",
+    name = "Window Title", -- or string id or function returning a string
+    displayName = "My Longer Window Title",  -- or string id or function returning a string (optional) (can be useful for long addon names or if you want to colorize it)
+    author = "Seerah",  -- or string id or function returning a string (optional)
+    version = "2.0",  -- or string id or function returning a string (optional)
+    website = "http://www.esoui.com/downloads/info7-LibAddonMenu.html", -- URL of website where the addon can be updated (optional)
+    keywords = "settings", -- additional keywords for search filter (it looks for matches in name..keywords..author) (optional)
+    slashCommand = "/myaddon", -- will register a keybind to open to this panel (don't forget to include the slash!) (optional)
+    registerForRefresh = true, --boolean (optional) (will refresh all options controls when a setting is changed and when the panel is shown)
+    registerForDefaults = true, --boolean (optional) (will set all options controls back to default values)
+    resetFunc = function() print("defaults reset") end, --(optional) custom function to run after settings are reset to defaults
+} ]]
+
+
+local widgetVersion = 13
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("panel", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+local cm = CALLBACK_MANAGER
+
+local function RefreshPanel(control)
+    local panel = LAM.util.GetTopPanel(control) --callback can be fired by a single control, by the panel showing or by a nested submenu
+    local panelControls = panel.controlsToRefresh
+
+    for i = 1, #panelControls do
+        local updateControl = panelControls[i]
+        if updateControl ~= control and updateControl.UpdateValue then
+            updateControl:UpdateValue()
+        end
+        if updateControl.UpdateDisabled then
+            updateControl:UpdateDisabled()
+        end
+        if updateControl.UpdateWarning then
+            updateControl:UpdateWarning()
+        end
+    end
+end
+
+local function ForceDefaults(panel)
+    local panelControls = panel.controlsToRefresh
+
+    for i = 1, #panelControls do
+        local updateControl = panelControls[i]
+        if updateControl.UpdateValue and updateControl.data.default ~= nil then
+            updateControl:UpdateValue(true)
+        end
+    end
+
+    if panel.data.resetFunc then
+        panel.data.resetFunc()
+    end
+
+    cm:FireCallbacks("LAM-RefreshPanel", panel)
+end
+
+local callbackRegistered = false
+LAMCreateControl.scrollCount = LAMCreateControl.scrollCount or 1
+local SEPARATOR = " - "
+local LINK_COLOR = ZO_ColorDef:New("5959D5")
+local LINK_MOUSE_OVER_COLOR = ZO_ColorDef:New("B8B8D3")
+
+function LAMCreateControl.panel(parent, panelData, controlName)
+    local control = wm:CreateControl(controlName, parent, CT_CONTROL)
+
+    control.label = wm:CreateControlFromVirtual(nil, control, "ZO_Options_SectionTitleLabel")
+    local label = control.label
+    label:SetAnchor(TOPLEFT, control, TOPLEFT, 0, 4)
+    label:SetText(LAM.util.GetStringFromValue(panelData.displayName or panelData.name))
+
+    if panelData.author or panelData.version then
+        control.info = wm:CreateControl(nil, control, CT_LABEL)
+        local info = control.info
+        info:SetFont(LAM.util.L["PANEL_INFO_FONT"])
+        info:SetAnchor(TOPLEFT, label, BOTTOMLEFT, 0, -2)
+
+        local output = {}
+        if panelData.author then
+            output[#output + 1] = zo_strformat(LAM.util.L["AUTHOR"], LAM.util.GetStringFromValue(panelData.author))
+        end
+        if panelData.version then
+            output[#output + 1] = zo_strformat(LAM.util.L["VERSION"], LAM.util.GetStringFromValue(panelData.version))
+        end
+        info:SetText(table.concat(output, SEPARATOR))
+    end
+
+    if panelData.website then
+        control.website = wm:CreateControl(nil, control, CT_BUTTON)
+        local website = control.website
+        website:SetClickSound("Click")
+        website:SetFont(LAM.util.L["PANEL_INFO_FONT"])
+        website:SetNormalFontColor(LINK_COLOR:UnpackRGBA())
+        website:SetMouseOverFontColor(LINK_MOUSE_OVER_COLOR:UnpackRGBA())
+        if(control.info) then
+            website:SetAnchor(TOPLEFT, control.info, TOPRIGHT, 0, 0)
+            website:SetText(string.format("|cffffff%s|r%s", SEPARATOR, LAM.util.L["WEBSITE"]))
+        else
+            website:SetAnchor(TOPLEFT, label, BOTTOMLEFT, 0, -2)
+            website:SetText(LAM.util.L["WEBSITE"])
+        end
+        website:SetDimensions(website:GetLabelControl():GetTextDimensions())
+        website:SetHandler("OnClicked", function()
+            RequestOpenUnsafeURL(panelData.website)
+        end)
+    end
+
+    control.container = wm:CreateControlFromVirtual("LAMAddonPanelContainer"..LAMCreateControl.scrollCount, control, "ZO_ScrollContainer")
+    LAMCreateControl.scrollCount = LAMCreateControl.scrollCount + 1
+    local container = control.container
+    container:SetAnchor(TOPLEFT, control.info or label, BOTTOMLEFT, 0, 20)
+    container:SetAnchor(BOTTOMRIGHT, control, BOTTOMRIGHT, -3, -3)
+    control.scroll = GetControl(control.container, "ScrollChild")
+    control.scroll:SetResizeToFitPadding(0, 20)
+
+    if panelData.registerForRefresh and not callbackRegistered then --don't want to register our callback more than once
+        cm:RegisterCallback("LAM-RefreshPanel", RefreshPanel)
+        callbackRegistered = true
+    end
+
+    control.ForceDefaults = ForceDefaults
+    control.data = panelData
+    control.controlsToRefresh = {}
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/slider.lua b/libs/LibAddonMenu-2.0/controls/slider.lua
new file mode 100644
index 0000000..7a85d57
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/slider.lua
@@ -0,0 +1,212 @@
+--[[sliderData = {
+    type = "slider",
+    name = "My Slider", -- or string id or function returning a string
+    getFunc = function() return db.var end,
+    setFunc = function(value) db.var = value doStuff() end,
+    min = 0,
+    max = 20,
+    step = 1, --(optional)
+    clampInput = true, -- boolean, if set to false the input won't clamp to min and max and allow any number instead (optional)
+    decimals = 0, -- when specified the input value is rounded to the specified number of decimals (optional)
+    autoSelect = false, -- boolean, automatically select everything in the text input field when it gains focus (optional)
+    inputLocation = "below", -- or "right", determines where the input field is shown. This should not be used within the addon menu and is for custom sliders (optional)
+    tooltip = "Slider's tooltip text.", -- or string id or function returning a string (optional)
+    width = "full", --or "half" (optional)
+    disabled = function() return db.someBooleanSetting end, --or boolean (optional)
+    warning = "May cause permanent awesomeness.", -- or string id or function returning a string (optional)
+    requiresReload = false, -- boolean, if set to true, the warning text will contain a notice that changes are only applied after an UI reload and any change to the value will make the "Apply Settings" button appear on the panel which will reload the UI when pressed (optional)
+    default = defaults.var, -- default value or function that returns the default value (optional)
+    reference = "MyAddonSlider" -- unique global reference to control (optional)
+} ]]
+
+local widgetVersion = 12
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("slider", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+local strformat = string.format
+
+local function RoundDecimalToPlace(d, place)
+    return tonumber(strformat("%." .. tostring(place) .. "f", d))
+end
+
+local function UpdateDisabled(control)
+    local disable
+    if type(control.data.disabled) == "function" then
+        disable = control.data.disabled()
+    else
+        disable = control.data.disabled
+    end
+
+    control.slider:SetEnabled(not disable)
+    control.slidervalue:SetEditEnabled(not disable)
+    if disable then
+        control.label:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+        control.minText:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+        control.maxText:SetColor(ZO_DEFAULT_DISABLED_COLOR:UnpackRGBA())
+        control.slidervalue:SetColor(ZO_DEFAULT_DISABLED_MOUSEOVER_COLOR:UnpackRGBA())
+    else
+        control.label:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+        control.minText:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+        control.maxText:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+        control.slidervalue:SetColor(ZO_DEFAULT_ENABLED_COLOR:UnpackRGBA())
+    end
+end
+
+local function UpdateValue(control, forceDefault, value)
+    if forceDefault then --if we are forcing defaults
+        value = LAM.util.GetDefaultValue(control.data.default)
+        control.data.setFunc(value)
+    elseif value then
+        if control.data.decimals then
+            value = RoundDecimalToPlace(value, control.data.decimals)
+        end
+        if control.data.clampInput ~= false then
+            value = math.max(math.min(value, control.data.max), control.data.min)
+        end
+        control.data.setFunc(value)
+        --after setting this value, let's refresh the others to see if any should be disabled or have their settings changed
+        LAM.util.RequestRefreshIfNeeded(control)
+    else
+        value = control.data.getFunc()
+    end
+
+    control.slider:SetValue(value)
+    control.slidervalue:SetText(value)
+end
+
+function LAMCreateControl.slider(parent, sliderData, controlName)
+    local control = LAM.util.CreateLabelAndContainerControl(parent, sliderData, controlName)
+    local isInputOnRight = sliderData.inputLocation == "right"
+
+    --skipping creating the backdrop...  Is this the actual slider texture?
+    control.slider = wm:CreateControl(nil, control.container, CT_SLIDER)
+    local slider = control.slider
+    slider:SetAnchor(TOPLEFT)
+    slider:SetHeight(14)
+    if(isInputOnRight) then
+        slider:SetAnchor(TOPRIGHT, nil, nil, -60)
+    else
+        slider:SetAnchor(TOPRIGHT)
+    end
+    slider:SetMouseEnabled(true)
+    slider:SetOrientation(ORIENTATION_HORIZONTAL)
+    --put nil for highlighted texture file path, and what look to be texture coords
+    slider:SetThumbTexture("EsoUI\\Art\\Miscellaneous\\scrollbox_elevator.dds", "EsoUI\\Art\\Miscellaneous\\scrollbox_elevator_disabled.dds", nil, 8, 16)
+    local minValue = sliderData.min
+    local maxValue = sliderData.max
+    slider:SetMinMax(minValue, maxValue)
+    slider:SetHandler("OnMouseEnter", function() ZO_Options_OnMouseEnter(control) end)
+    slider:SetHandler("OnMouseExit", function() ZO_Options_OnMouseExit(control) end)
+
+    slider.bg = wm:CreateControl(nil, slider, CT_BACKDROP)
+    local bg = slider.bg
+    bg:SetCenterColor(0, 0, 0)
+    bg:SetAnchor(TOPLEFT, slider, TOPLEFT, 0, 4)
+    bg:SetAnchor(BOTTOMRIGHT, slider, BOTTOMRIGHT, 0, -4)
+    bg:SetEdgeTexture("EsoUI\\Art\\Tooltips\\UI-SliderBackdrop.dds", 32, 4)
+
+    control.minText = wm:CreateControl(nil, slider, CT_LABEL)
+    local minText = control.minText
+    minText:SetFont("ZoFontGameSmall")
+    minText:SetAnchor(TOPLEFT, slider, BOTTOMLEFT)
+    minText:SetText(sliderData.min)
+
+    control.maxText = wm:CreateControl(nil, slider, CT_LABEL)
+    local maxText = control.maxText
+    maxText:SetFont("ZoFontGameSmall")
+    maxText:SetAnchor(TOPRIGHT, slider, BOTTOMRIGHT)
+    maxText:SetText(sliderData.max)
+
+    control.slidervalueBG = wm:CreateControlFromVirtual(nil, slider, "ZO_EditBackdrop")
+    if(isInputOnRight) then
+        control.slidervalueBG:SetDimensions(60, 26)
+        control.slidervalueBG:SetAnchor(LEFT, slider, RIGHT, 5, 0)
+    else
+        control.slidervalueBG:SetDimensions(50, 16)
+        control.slidervalueBG:SetAnchor(TOP, slider, BOTTOM, 0, 0)
+    end
+    control.slidervalue = wm:CreateControlFromVirtual(nil, control.slidervalueBG, "ZO_DefaultEditForBackdrop")
+    local slidervalue = control.slidervalue
+    slidervalue:ClearAnchors()
+    slidervalue:SetAnchor(TOPLEFT, control.slidervalueBG, TOPLEFT, 3, 1)
+    slidervalue:SetAnchor(BOTTOMRIGHT, control.slidervalueBG, BOTTOMRIGHT, -3, -1)
+    slidervalue:SetTextType(TEXT_TYPE_NUMERIC)
+    if(isInputOnRight) then
+        slidervalue:SetFont("ZoFontGameLarge")
+    else
+        slidervalue:SetFont("ZoFontGameSmall")
+    end
+
+    local isHandlingChange = false
+    local function HandleValueChanged(value)
+        if isHandlingChange then return end
+        if sliderData.decimals then
+            value = RoundDecimalToPlace(value, sliderData.decimals)
+        end
+        isHandlingChange = true
+        slider:SetValue(value)
+        slidervalue:SetText(value)
+        isHandlingChange = false
+    end
+
+    slidervalue:SetHandler("OnEscape", function(self)
+        HandleValueChanged(sliderData.getFunc())
+        self:LoseFocus()
+    end)
+    slidervalue:SetHandler("OnEnter", function(self)
+        self:LoseFocus()
+    end)
+    slidervalue:SetHandler("OnFocusLost", function(self)
+        local value = tonumber(self:GetText())
+        control:UpdateValue(false, value)
+    end)
+    slidervalue:SetHandler("OnTextChanged", function(self)
+        local input = self:GetText()
+        if(#input > 1 and not input:sub(-1):match("[0-9]")) then return end
+        local value = tonumber(input)
+        if(value) then
+            HandleValueChanged(value)
+        end
+    end)
+    if(sliderData.autoSelect) then
+        ZO_PreHookHandler(slidervalue, "OnFocusGained", function(self)
+            self:SelectAll()
+        end)
+    end
+
+    local range = maxValue - minValue
+    slider:SetValueStep(sliderData.step or 1)
+    slider:SetHandler("OnValueChanged", function(self, value, eventReason)
+        if eventReason == EVENT_REASON_SOFTWARE then return end
+        HandleValueChanged(value)
+    end)
+    slider:SetHandler("OnSliderReleased", function(self, value)
+        control:UpdateValue(false, value)
+    end)
+    slider:SetHandler("OnMouseWheel", function(self, value)
+        if(not self:GetEnabled()) then return end
+        local new_value = (tonumber(slidervalue:GetText()) or sliderData.min or 0) + ((sliderData.step or 1) * value)
+        control:UpdateValue(false, new_value)
+    end)
+
+    if sliderData.warning ~= nil or sliderData.requiresReload then
+        control.warning = wm:CreateControlFromVirtual(nil, control, "ZO_Options_WarningIcon")
+        control.warning:SetAnchor(RIGHT, slider, LEFT, -5, 0)
+        control.UpdateWarning = LAM.util.UpdateWarning
+        control:UpdateWarning()
+    end
+
+    control.UpdateValue = UpdateValue
+    control:UpdateValue()
+
+    if sliderData.disabled ~= nil then
+        control.UpdateDisabled = UpdateDisabled
+        control:UpdateDisabled()
+    end
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+    LAM.util.RegisterForReloadIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/submenu.lua b/libs/LibAddonMenu-2.0/controls/submenu.lua
new file mode 100644
index 0000000..94087cb
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/submenu.lua
@@ -0,0 +1,108 @@
+--[[submenuData = {
+    type = "submenu",
+    name = "Submenu Title", -- or string id or function returning a string
+    tooltip = "My submenu tooltip", -- -- or string id or function returning a string (optional)
+    controls = {sliderData, buttonData} --(optional) used by LAM
+    reference = "MyAddonSubmenu" --(optional) unique global reference to control
+} ]]
+
+local widgetVersion = 11
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("submenu", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+local am = ANIMATION_MANAGER
+
+local function UpdateValue(control)
+    control.label:SetText(LAM.util.GetStringFromValue(control.data.name))
+    if control.data.tooltip then
+        control.label.data.tooltipText = LAM.util.GetStringFromValue(control.data.tooltip)
+    end
+end
+
+local function AnimateSubmenu(clicked)
+    local control = clicked:GetParent()
+    control.open = not control.open
+
+    if control.open then
+        control.animation:PlayFromStart()
+    else
+        control.animation:PlayFromEnd()
+    end
+end
+
+function LAMCreateControl.submenu(parent, submenuData, controlName)
+    local width = parent:GetWidth() - 45
+    local control = wm:CreateControl(controlName or submenuData.reference, parent.scroll or parent, CT_CONTROL)
+    control.panel = parent
+    control.data = submenuData
+
+    control.label = wm:CreateControlFromVirtual(nil, control, "ZO_Options_SectionTitleLabel")
+    local label = control.label
+    label:SetAnchor(TOPLEFT, control, TOPLEFT, 5, 5)
+    label:SetDimensions(width, 30)
+    label:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS)
+    label:SetText(LAM.util.GetStringFromValue(submenuData.name))
+    label:SetMouseEnabled(true)
+    if submenuData.tooltip then
+        label.data = {tooltipText = LAM.util.GetStringFromValue(submenuData.tooltip)}
+        label:SetHandler("OnMouseEnter", ZO_Options_OnMouseEnter)
+        label:SetHandler("OnMouseExit", ZO_Options_OnMouseExit)
+    end
+
+    control.scroll = wm:CreateControl(nil, control, CT_SCROLL)
+    local scroll = control.scroll
+    scroll:SetParent(control)
+    scroll:SetAnchor(TOPLEFT, label, BOTTOMLEFT, 0, 10)
+    scroll:SetDimensionConstraints(width + 5, 0, width + 5, 0)
+
+    control.bg = wm:CreateControl(nil, label, CT_BACKDROP)
+    local bg = control.bg
+    bg:SetAnchor(TOPLEFT, label, TOPLEFT, -5, -5)
+    bg:SetAnchor(BOTTOMRIGHT, scroll, BOTTOMRIGHT, -7, 0)
+    bg:SetEdgeTexture("EsoUI\\Art\\Tooltips\\UI-Border.dds", 128, 16)
+    bg:SetCenterTexture("EsoUI\\Art\\Tooltips\\UI-TooltipCenter.dds")
+    bg:SetInsets(16, 16, -16, -16)
+
+    control.arrow = wm:CreateControl(nil, bg, CT_TEXTURE)
+    local arrow = control.arrow
+    arrow:SetDimensions(28, 28)
+    arrow:SetTexture("EsoUI\\Art\\Miscellaneous\\list_sortdown.dds") --list_sortup for the other way
+    arrow:SetAnchor(TOPRIGHT, bg, TOPRIGHT, -5, 5)
+
+    --figure out the cool animation later...
+    control.animation = am:CreateTimeline()
+    local animation = control.animation
+    animation:SetPlaybackType(ANIMATION_SIZE, 0) --2nd arg = loop count
+
+    control:SetResizeToFitDescendents(true)
+    control.open = false
+    label:SetHandler("OnMouseUp", AnimateSubmenu)
+    animation:SetHandler("OnStop", function(self, completedPlaying)
+        scroll:SetResizeToFitDescendents(control.open)
+        if control.open then
+            control.arrow:SetTexture("EsoUI\\Art\\Miscellaneous\\list_sortup.dds")
+            scroll:SetResizeToFitPadding(5, 20)
+        else
+            control.arrow:SetTexture("EsoUI\\Art\\Miscellaneous\\list_sortdown.dds")
+            scroll:SetResizeToFitPadding(5, 0)
+            scroll:SetHeight(0)
+        end
+    end)
+
+    --small strip at the bottom of the submenu that you can click to close it
+    control.btmToggle = wm:CreateControl(nil, control, CT_TEXTURE)
+    local btmToggle = control.btmToggle
+    btmToggle:SetMouseEnabled(true)
+    btmToggle:SetAnchor(BOTTOMLEFT, control.scroll, BOTTOMLEFT)
+    btmToggle:SetAnchor(BOTTOMRIGHT, control.scroll, BOTTOMRIGHT)
+    btmToggle:SetHeight(15)
+    btmToggle:SetAlpha(0)
+    btmToggle:SetHandler("OnMouseUp", AnimateSubmenu)
+
+    control.UpdateValue = UpdateValue
+
+    LAM.util.RegisterForRefreshIfNeeded(control)
+
+    return control
+end
diff --git a/libs/LibAddonMenu-2.0/controls/texture.lua b/libs/LibAddonMenu-2.0/controls/texture.lua
new file mode 100644
index 0000000..4604fea
--- /dev/null
+++ b/libs/LibAddonMenu-2.0/controls/texture.lua
@@ -0,0 +1,45 @@
+--[[textureData = {
+    type = "texture",
+    image = "file/path.dds",
+    imageWidth = 64, --max of 250 for half width, 510 for full
+    imageHeight = 32, --max of 100
+    tooltip = "Image's tooltip text.", -- or string id or function returning a string (optional)
+    width = "full", --or "half" (optional)
+    reference = "MyAddonTexture" --(optional) unique global reference to control
+} ]]
+
+--add texture coords support?
+
+local widgetVersion = 9
+local LAM = LibStub("LibAddonMenu-2.0")
+if not LAM:RegisterWidget("texture", widgetVersion) then return end
+
+local wm = WINDOW_MANAGER
+
+local MIN_HEIGHT = 26
+function LAMCreateControl.texture(parent, textureData, controlName)
+    local control = LAM.util.CreateBaseControl(parent, textureData, controlName)
+    local width = control:GetWidth()
+    control:SetResizeToFitDescendents(true)
+
+    if control.isHalfWidth then --note these restrictions
+        control:SetDimensionConstraints(width / 2, MIN_HEIGHT, width / 2, MIN_HEIGHT * 4)
+    else
+        control:SetDimensionConstraints(width, MIN_HEIGHT, width, MIN_HEIGHT * 4)
+    end
+
+    control.texture = wm:CreateControl(nil, control, CT_TEXTURE)
+    local texture = control.texture
+    texture:SetAnchor(CENTER)
+    texture:SetDimensions(textureData.imageWidth, textureData.imageHeight)
+    texture:SetTexture(textureData.image)
+
+    if textureData.tooltip then
+        texture:SetMouseEnabled(true)
+        texture.data = {tooltipText = LAM.util.GetStringFromValue(textureData.tooltip)}
+        texture:SetHandler("OnMouseEnter", ZO_Options_OnMouseEnter)
+        texture:SetHandler("OnMouseExit", ZO_Options_OnMouseExit)
+    end
+
+    return control
+end
diff --git a/libs/LibItemInfo-1.0/LibItemInfo-1.0.lua b/libs/LibItemInfo-1.0/LibItemInfo-1.0.lua
new file mode 100644
index 0000000..52ef9f9
--- /dev/null
+++ b/libs/LibItemInfo-1.0/LibItemInfo-1.0.lua
@@ -0,0 +1,492 @@
+
+--Register LAM with LibStub
+local MAJOR, MINOR = "LibItemInfo-1.0", 7
+local lii, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+if not lii then return end	--the same or newer version of this lib is already loaded into memory
+
+
+local tEquipTypes = {
+	[ITEMTYPE_ARMOR] = {
+		--[ARMORTYPE_NONE] 	= {}, -- jewelry is excluded --
+		[ARMORTYPE_LIGHT] 	= {
+			["CRAFTINGSKILLTYPE"] 	= CRAFTING_TYPE_CLOTHIER,
+			[EQUIP_TYPE_CHEST]		= 1,
+			[EQUIP_TYPE_FEET]		= 2,
+			[EQUIP_TYPE_HAND]		= 3,
+			[EQUIP_TYPE_HEAD]		= 4,
+			[EQUIP_TYPE_LEGS]		= 5,
+			[EQUIP_TYPE_SHOULDERS]	= 6,
+			[EQUIP_TYPE_WAIST]		= 7,
+		},
+		[ARMORTYPE_MEDIUM] 	= {
+			["CRAFTINGSKILLTYPE"] 	= CRAFTING_TYPE_CLOTHIER,
+			[EQUIP_TYPE_CHEST]		= 8,
+			[EQUIP_TYPE_FEET]		= 9,
+			[EQUIP_TYPE_HAND]		= 10,
+			[EQUIP_TYPE_HEAD]		= 11,
+			[EQUIP_TYPE_LEGS]		= 12,
+			[EQUIP_TYPE_SHOULDERS]	= 13,
+			[EQUIP_TYPE_WAIST]		= 14,
+		},
+		[ARMORTYPE_HEAVY] 	= {
+			["CRAFTINGSKILLTYPE"] 	= CRAFTING_TYPE_BLACKSMITHING,
+			[EQUIP_TYPE_CHEST]		= 8,
+			[EQUIP_TYPE_FEET]		= 9,
+			[EQUIP_TYPE_HAND]		= 10,
+			[EQUIP_TYPE_HEAD]		= 11,
+			[EQUIP_TYPE_LEGS]		= 12,
+			[EQUIP_TYPE_SHOULDERS]	= 13,
+			[EQUIP_TYPE_WAIST]		= 14,
+		},
+	},
+	[ITEMTYPE_WEAPON] = {
+		[WEAPONTYPE_SHIELD]				= {
+			[EQUIP_TYPE_OFF_HAND] = 6, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_AXE]				= {
+			[EQUIP_TYPE_ONE_HAND] = 1, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_DAGGER]				= {
+			[EQUIP_TYPE_ONE_HAND] = 7, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_HAMMER]				= {
+			[EQUIP_TYPE_ONE_HAND] = 2, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_SWORD]				= {
+			[EQUIP_TYPE_ONE_HAND] = 3, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_TWO_HANDED_AXE]		= {
+			[EQUIP_TYPE_TWO_HAND] = 4, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_TWO_HANDED_HAMMER]	= {
+			[EQUIP_TYPE_TWO_HAND] = 5, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_TWO_HANDED_SWORD]	= {
+			[EQUIP_TYPE_TWO_HAND] = 6, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_BOW]				= {
+			[EQUIP_TYPE_TWO_HAND] = 1, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_FIRE_STAFF]			= {
+			[EQUIP_TYPE_TWO_HAND] = 2, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_FROST_STAFF]		= {
+			[EQUIP_TYPE_TWO_HAND] = 3, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_LIGHTNING_STAFF] 	= {
+			[EQUIP_TYPE_TWO_HAND] = 4, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_HEALING_STAFF]		= {
+			[EQUIP_TYPE_TWO_HAND] = 5, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+	},
+}
+
+local tIsTraitResearchable = {
+	[ITEM_TRAIT_TYPE_ARMOR_DIVINES] 		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_EXPLORATION] 	= true,
+	[ITEM_TRAIT_TYPE_ARMOR_IMPENETRABLE] 	= true,
+	[ITEM_TRAIT_TYPE_ARMOR_INFUSED] 		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_INTRICATE] 		= false,
+	[ITEM_TRAIT_TYPE_ARMOR_ORNATE] 			= false,
+	[ITEM_TRAIT_TYPE_ARMOR_REINFORCED]		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_STURDY] 			= true,
+	[ITEM_TRAIT_TYPE_ARMOR_TRAINING] 		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_WELL_FITTED]		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_NIRNHONED] 		= true,
+
+	[ITEM_TRAIT_TYPE_WEAPON_CHARGED] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_DEFENDING] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_INFUSED] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_INTRICATE] 		= false,
+	[ITEM_TRAIT_TYPE_WEAPON_ORNATE] 		= false,
+	[ITEM_TRAIT_TYPE_WEAPON_POWERED] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_PRECISE] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_SHARPENED] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_TRAINING] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_WEIGHTED]	 	= true,
+	[ITEM_TRAIT_TYPE_WEAPON_NIRNHONED] 		= true,
+
+	[ITEM_TRAIT_TYPE_JEWELRY_ARCANE] 		= false,
+	[ITEM_TRAIT_TYPE_JEWELRY_HEALTHY]	 	= false,
+	[ITEM_TRAIT_TYPE_JEWELRY_ORNATE] 		= false,
+	[ITEM_TRAIT_TYPE_JEWELRY_ROBUST] 		= false,
+
+	[ITEM_TRAIT_TYPE_NONE] 					= false,
+}
+
+-- All functions may depend on lii:GetItemLink to function properly.
+-- Any other library dependent functions are marked with a preceding comment.
+
+------------------------------------------------------------------------
+-- 	General Functions  --
+------------------------------------------------------------------------
+-- Returns an unformatted link of the item
+function lii:GetItemLink(_BagIdOrLink, _iSlotId)
+	if _iSlotId then
+		return GetItemLink(_BagIdOrLink,_iSlotId)
+	end
+	return _BagIdOrLink
+end
+
+-- Returns a zo_strformat(..)'d Link --
+function lii:GetFormattedItemLink(_BagIdOrLink, _iSlotId)
+	if _iSlotId then
+		return zo_strformat("<<t:1>>", GetItemLink(_BagIdOrLink,_iSlotId))
+	end
+	return zo_strformat("<<t:1>>", _BagIdOrLink)
+end
+
+-- Returns the zo_strformat(..)'d ToolTipName of the item
+function lii:GetItemToolTipName(_BagIdOrLink, _iSlotId)
+	if _iSlotId then
+		return zo_strformat(SI_TOOLTIP_ITEM_NAME, GetItemName(_BagIdOrLink, _iSlotId))
+	end
+	return zo_strformat(SI_TOOLTIP_ITEM_NAME, GetItemLinkName(_BagIdOrLink))
+end
+
+local function GetMyLinkItemInfo(_lItemLink)
+	local sIcon, iSellPrice, bMeetsUsageRequirement, iEquipType, iItemStyle = GetItemLinkInfo(_lItemLink)
+	local iItemQuality = GetItemLinkQuality(_lItemLink)
+
+	return sIcon, iStack, iSellPrice, bMeetsUsageRequirement, bLocked, iEquipType, iItemStyle, iItemQuality
+end
+
+-- Same returns as the built in GetItemInfo
+-- sIcon, iStack, iSellPrice, bMeetsUsageRequirement, bLocked, iEquipType, iItemStyle, iItemQuality
+-- Links do not have stack sizes or a locked property
+-- If you pass in a link iStack & bLocked will return nil
+function lii:GetItemInfo(_BagIdOrLink, _iSlotId)
+	if _iSlotId then
+		return GetItemInfo(_BagIdOrLink, _iSlotId)
+    end
+	return GetMyLinkItemInfo(_BagIdOrLink)
+end
+
+
+
+------------------------------------------------------------------------
+-- 	Item SubType (ArmorType/WeaponType) Info  --
+------------------------------------------------------------------------
+-- GetSubType is depended on by several functions
+-- Returns the appropriate ArmorType, WeaponType,
+-- or 0 if not a piece of Armor or Weapon
+-- 0 is the same value as WEAPONTYPE_NONE & ARMORTYPE_NONE
+function lii:GetSubType(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+
+	if iItemType == ITEMTYPE_ARMOR then
+		return GetItemLinkArmorType(lLink)
+	elseif iItemType == ITEMTYPE_WEAPON then
+		return GetItemLinkWeaponType(lLink)
+	end
+	-- 0 is the same return as WEAPONTYPE_NONE & ARMORTYPE_NONE
+	return 0
+end
+
+
+
+------------------------------------------------------------------------
+-- 	ItemType Group Properties  --
+------------------------------------------------------------------------
+-- Returns: True/False if the item is jewelry
+function lii:IsJewelry(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+
+	if iItemType == ITEMTYPE_ARMOR then
+		local iArmorType = GetItemLinkArmorType(lLink)
+		if iArmorType == ARMORTYPE_NONE then
+			return true
+		end
+	end
+	return false
+end
+
+-- Returns true/false if the item is a one handed weapon
+function lii:IsWeaponOneHanded(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+	local iEquipType = GetItemLinkEquipType(lLink)
+
+	if ((iItemType == ITEMTYPE_WEAPON) and (iEquipType == EQUIP_TYPE_ONE_HAND)) then
+		return true
+	end
+	return false
+end
+
+-- Returns true/false if the item is a two handed weapon
+function lii:IsWeaponTwoHanded(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+	local iEquipType = GetItemLinkEquipType(lLink)
+
+	if ((iItemType == ITEMTYPE_WEAPON) and (iEquipType == EQUIP_TYPE_TWO_HAND)) then
+		return true
+	end
+	return false
+end
+
+-- Returns true/false if the item is a crafting mat
+function lii:IsCrafingMaterial(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+
+	if GetItemLinkCraftingSkillType(lLink) ~= CRAFTING_TYPE_INVALID then
+		return true
+	end
+	return false
+end
+
+-- returns true/false if the item is a glyph
+function lii:IsGlyph(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+
+	if ((iItemType == ITEMTYPE_GLYPH_ARMOR) or (iItemType == ITEMTYPE_GLYPH_WEAPON)
+	or (iItemType == ITEMTYPE_GLYPH_JEWELRY)) then
+		return true
+	end
+	return false
+end
+
+
+
+------------------------------------------------------------------------
+-- 	Crafting Info Functions --
+------------------------------------------------------------------------
+-- Returns a zo_strFormat(..)'d name for the CraftingSkillType in the games current language or CRAFTING_TYPE_INVALID
+-- In English it returns:
+-- CRAFTING_TYPE_ALCHEMY returns "Alchemy"
+-- CRAFTING_TYPE_BLACKSMITHING returns "Blacksmithing"
+-- CRAFTING_TYPE_CLOTHIER returns "Clothing"
+-- CRAFTING_TYPE_ENCHANTING returns "Enchanting"
+-- CRAFTING_TYPE_INVALID returns CRAFTING_TYPE_INVALID (NOT a formatted string, which is 0)
+-- CRAFTING_TYPE_PROVISIONING returns "Provisioning"
+-- CRAFTING_TYPE_WOODWORKING  returns "Woodworking"
+function lii:GetCraftingSkillTypeLabelName(_iCraftingSkillType)
+-- Returns Name of Crafting Skill Type. Used for labelling things --
+	-- 0 is CRAFTING_TYPE_INVALID & there are only 6 consecutively numbered crafting skill types --
+	if ((_iCraftingSkillType > 0) and (_iCraftingSkillType < 7)) then
+		local SkillType, skillIndex = GetCraftingSkillLineIndices(_iCraftingSkillType)
+		local name, rank = GetSkillLineInfo(SkillType, skillIndex)
+		return zo_strformat(SI_TOOLTIP_ITEM_NAME, name)
+	end
+	return CRAFTING_TYPE_INVALID
+end
+
+-- Dependence on  lii:GetSubType & IsResearchableItemType
+-- Possible Returns: CRAFTING_TYPE_CLOTHIER, CRAFTING_TYPE_BLACKSMITHING, CRAFTING_TYPE_WOODWORKING, or CRAFTING_TYPE_INVALID
+-- Returns: the CraftingSkillType of an item IF it is a researchable ItemType
+-- Returns: CRAFTING_TYPE_INVALID if it is not a researchable ItemType
+function lii:GetResearchableCraftingSkillType(_BagIdOrLink, _iSlotId)
+	if not self:IsResearchableItemType(_BagIdOrLink, _iSlotId) then return CRAFTING_TYPE_INVALID end
+
+	local lLink 	= self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iSubType 	= self:GetSubType(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+
+	return tEquipTypes[iItemType][iSubType]["CRAFTINGSKILLTYPE"]
+end
+
+-- Dependecy on: GetResearchableCraftingSkillType(...)
+-- RETURNS: the items corresponding crafting skill type or CRAFTING_TYPE_INVALID
+-- This is for fully created items like actual armor/weapon, food, drink, potions
+-- DOES NOT WORK ON CRAFTING MATERIALS, if you want that info just use the built in
+-- API function: GetItemLinkCraftingSkillType(string ItemLink)
+-- ** NOTE: just because it returns a CraftingSkillType does not mean it is a crafted, it may have been looted or bought.
+function lii:GetItemCraftingSkillType(_BagIdOrLink, _iSlotId)
+	local lLink 	= self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+
+	local allowedReturnTypes = {
+		[ITEMTYPE_FOOD] 	= CRAFTING_TYPE_PROVISIONING,
+		[ITEMTYPE_DRINK] 	= CRAFTING_TYPE_PROVISIONING,
+		[ITEMTYPE_POTION] 	= CRAFTING_TYPE_ALCHEMY,
+		[ITEMTYPE_GLYPH_ARMOR] 		= CRAFTING_TYPE_ENCHANTING,
+		[ITEMTYPE_GLYPH_WEAPON] 	= CRAFTING_TYPE_ENCHANTING,
+		[ITEMTYPE_GLYPH_JEWELRY] 	= CRAFTING_TYPE_ENCHANTING,
+		[ITEMTYPE_ENCHANTING_RUNE_ASPECT] 	= CRAFTING_TYPE_ENCHANTING,
+		[ITEMTYPE_ENCHANTING_RUNE_ESSENCE] 	= CRAFTING_TYPE_ENCHANTING,
+		[ITEMTYPE_ENCHANTING_RUNE_POTENCY] 	= CRAFTING_TYPE_ENCHANTING,
+	}
+
+	local iCraftingSkillType = allowedReturnTypes[iItemType]
+
+	if iCraftingSkillType then
+		return iCraftingSkillType
+	end
+
+	return self:GetResearchableCraftingSkillType(_BagIdOrLink, _iSlotId)
+end
+
+
+
+------------------------------------------------------------------------
+-- 	Research Specific Functions --
+------------------------------------------------------------------------
+-- Dependence on IsResearchableItem, GetResearchLineIndex, GetTraitIndex, GetResearchableCraftingSkillType
+-- Returns: bool IsResearchableItem, integer CraftingSkillType, Integer ResearchLineIndex, integer TraitIndex
+--  IsResearchableItem: Returns: True if itemType is researchable AND has a researchable trait on it, But that does not mean it is an unknown trait --
+-- CraftingSkillType: Returns the CraftingSkillType of an item IF it is a researchable ItemType or CRAFTING_TYPE_INVALID (item does not have to have a trait on it to return a CraftingSkillType)
+-- GetResearchLineIndex: Returns the ResearchLineIndex for an item if it is a researchable ItemType Or returns CRAFTING_TYPE_INVALID (item does not have to have a trait on it to return a ResearchLineIndex) --
+-- TraitIndex: Returns the TraitIndex of the trait on the item or CRAFTING_TYPE_INVALID (Armor/Weapon Trait Stones will return a TraitIndex)
+function lii:GetResearchInfo(_BagIdOrLink, _iSlotId)
+	local bIsResearchableItem 	= self:IsResearchableItem(_BagIdOrLink, _iSlotId)
+	local iCraftingSkillType 	= self:GetResearchableCraftingSkillType(_BagIdOrLink, _iSlotId)
+	local iResearchLineIndex 	= self:GetResearchLineIndex(_BagIdOrLink, _iSlotId)
+	local iTraitIndex 			= self:GetTraitIndex(_BagIdOrLink, _iSlotId)
+
+	return bIsResearchableItem, iCraftingSkillType, iResearchLineIndex, iTraitIndex
+end
+
+-- Dependence on lii:GetSubType
+-- Returns: True if the ItemType is Armor or Weapon and the subtype is  handled (in the equipTypes table)
+function lii:IsResearchableItemType(_BagIdOrLink, _iSlotId)
+	local lLink 	= self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+	if ((iItemType ~= ITEMTYPE_ARMOR) and (iItemType ~= ITEMTYPE_WEAPON)) then return false end
+
+	local iSubType 	= self:GetSubType(lLink)
+
+	if tEquipTypes[iItemType][iSubType] then
+		return true
+	end
+	return false
+end
+
+-- Dependence on  lii:GetSubType & IsResearchableItemType
+-- Returns: True if itemType is researchable AND has a researchable trait on it --
+-- That does not mean it is an unknown trait --
+-- Make sure you note the distinction between this & HasResearchableTrait, they are not the same --
+function lii:IsResearchableItem(_BagIdOrLink, _iSlotId)
+	if not self:IsResearchableItemType(_BagIdOrLink, _iSlotId) then return false end
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+
+	return tIsTraitResearchable[GetItemLinkTraitInfo(lLink)]
+end
+
+-- Dependence on GetResearchInfo
+-- Returns: True or False
+-- True if the item is a researchable ItemType, it has a researchable trait
+--   on it that is unknown, and that trait is not currently being researched
+function lii:NeedForResearch(_BagIdOrLink, _iSlotId)
+	local bIsResearchableItem, iCraftingSkillType, iResearchLineIndex, iTraitIndex = self:GetResearchInfo(_BagIdOrLink, _iSlotId)
+	local _, _, bIsTraitKnown = GetSmithingResearchLineTraitInfo(iCraftingSkillType, iResearchLineIndex, iTraitIndex)
+
+	if (bIsResearchableItem and (not bIsTraitKnown) and
+	(GetSmithingResearchLineTraitTimes(iCraftingSkillType, iResearchLineIndex, iTraitIndex) == nil)) then
+		return true
+	end
+	return false
+end
+
+-- Dependence on  lii:GetSubType & IsResearchableItemType
+-- Possible Returns: The Items ResearchLine Index or CRAFTING_TYPE_INVALID
+-- Returns: The ResearchLineIndex for an item if it is a researchable ItemType
+-- 		This does not mean it has a trait on it.
+-- Returns: CRAFTING_TYPE_INVALID if the item is not a Researchable ItemType
+function lii:GetResearchLineIndex(_BagIdOrLink, _iSlotId)
+	if not self:IsResearchableItemType(_BagIdOrLink, _iSlotId) then return CRAFTING_TYPE_INVALID end
+
+	local lLink 		= self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iSubType 		= self:GetSubType(_BagIdOrLink, _iSlotId)
+	local iItemType 	= GetItemLinkItemType(lLink)
+	local iEquipType 	= GetItemLinkEquipType(lLink)
+
+	return tEquipTypes[iItemType][iSubType][iEquipType]
+end
+
+
+
+------------------------------------------------------------------------
+-- 	Item Trait Specific Functions --
+------------------------------------------------------------------------
+-- Returns: the TraitIndex of the trait on an item or CRAFTING_TYPE_INVALID
+-- Does not mean the ItemType is researchable (Armor/Weapon Trait Stones will return a TraitIndex)
+function lii:GetTraitIndex(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iTraitType = GetItemLinkTraitInfo(lLink)
+	if not tIsTraitResearchable[iTraitType] then return CRAFTING_TYPE_INVALID end
+
+	if ((iTraitType == ITEM_TRAIT_TYPE_WEAPON_NIRNHONED)
+	or (iTraitType == ITEM_TRAIT_TYPE_ARMOR_NIRNHONED)) then
+		return 9
+	end
+	return (iTraitType % 10)
+end
+
+-- Dependence on GetResearchInfo
+-- Returns true if the item is researchable and has a known researchable trait
+function lii:HasKnownTrait(_BagIdOrLink, _iSlotId)
+	local bIsResearchableItem, iCraftingSkillType, iResearchLineIndex, iTraitIndex = self:GetResearchInfo(_BagIdOrLink, _iSlotId)
+	local _, _, bHasKnownTrait = GetSmithingResearchLineTraitInfo(iCraftingSkillType, iResearchLineIndex, iTraitIndex)
+
+	return bHasKnownTrait
+end
+
+-- Dependence on GetResearchInfo
+-- Returns true/false if the item is researchable and has an unknown researchable trait
+function lii:HasUnKnownTrait(_BagIdOrLink, _iSlotId)
+	local bIsResearchableItem, iCraftingSkillType, iResearchLineIndex, iTraitIndex = self:GetResearchInfo(_BagIdOrLink, _iSlotId)
+
+	if bIsResearchableItem then
+		local _, _, bIsKnown = GetSmithingResearchLineTraitInfo(iCraftingSkillType, iResearchLineIndex, iTraitIndex)
+		return not bIsKnown
+	end
+	return false
+end
+
+-- Returns true/false if the item has a researchable trait
+-- 		This does not mean the trait is unknown to you or that it is
+-- a researchable ItemType only that it is one of the researchable
+-- TraitTypes (Armor/Weapon Trait Stones will return true)
+-- Make sure you note the distinction between this & IsResearchableItem, they are not the same --
+function lii:HasResearchableTrait(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+	return tIsTraitResearchable[GetItemLinkTraitInfo(lLink)]
+end
+
+-- Dependence on GetResearchInfo
+-- If the TraitType on the item is currently being researched
+-- 		Returns: True, TotalResearchTime, timeLeftInSeconds
+-- If the TraitType on the item is not currently being researched
+-- 		Returns: False, nil, nil
+function lii:IsItemTraitBeingResearched(_BagIdOrLink, _iSlotId)
+	local bIsResearchableItem, iCraftingSkillType, iResearchLineIndex, iTraitIndex = self:GetResearchInfo(_BagIdOrLink, _iSlotId)
+	local iTotalResearchTime, iTimeLeftInSecs = GetSmithingResearchLineTraitTimes(iCraftingSkillType, iResearchLineIndex, iTraitIndex)
+
+	if iTotalResearchTime then
+		return true, iTotalResearchTime, iTimeLeftInSecs
+	end
+	return false
+end
+
+
+
+------------------------------------------------------------------------
+-- 	Recipe Functions  --
+------------------------------------------------------------------------
+
+-- Returns: bool IsKnownRecipe, string (ToolTipFormatted)RecipeName, integer RecipeListIndex, integer RecipeIndex
+-- Returns: false, nil, nil, nil if the recipe is unknown or if the item is not a recipe
+-- Returns: true, (ToolTipFormatted)RecipeName, RecipeListIndex, RecipeIndex if the recipe is known
+function lii:GetRecipeInfo(_BagIdOrLink, _iSlotId)
+	local lLink = self:GetItemLink(_BagIdOrLink, _iSlotId)
+	local iItemType = GetItemLinkItemType(lLink)
+	if iItemType ~= ITEMTYPE_RECIPE then return false end
+
+	local lRecipeResultItemLink = GetItemLinkRecipeResultItemLink(lLink)
+	local iNumRecipeLists = GetNumRecipeLists()
+
+	for iRecipeListIndex = 1, iNumRecipeLists do
+		local _, iNumRecipes = GetRecipeListInfo(iRecipeListIndex)
+
+		for iRecipeIndex = 1, iNumRecipes do
+			local sRecipeResultIndexLink = GetRecipeResultItemLink(iRecipeListIndex,iRecipeIndex)
+			if lRecipeResultItemLink == sRecipeResultIndexLink then
+				local _,sRecipeName = GetRecipeInfo(iRecipeListIndex,iRecipeIndex)
+				local sFormattedRecipeName = zo_strformat(SI_TOOLTIP_ITEM_NAME, sRecipeName)
+
+				return true, sFormattedRecipeName, iRecipeListIndex, iRecipeIndex
+			end
+		end
+	end
+	return false
+end
+
+
+
+
+
+
+
+
+
diff --git a/libs/LibItemInfo-1.0/LibItemInfo-1.0.txt b/libs/LibItemInfo-1.0/LibItemInfo-1.0.txt
new file mode 100644
index 0000000..ade03a4
--- /dev/null
+++ b/libs/LibItemInfo-1.0/LibItemInfo-1.0.txt
@@ -0,0 +1,10 @@
+## APIVersion: 100009
+## Title: LibItemInfo-1.0
+## Version: 1.0 r7
+## Author: Circonian
+## Description: A library used to aid in gathering item information.
+
+
+LibStub\LibStub.lua
+
+LibItemInfo-1.0.lua
diff --git a/libs/LibLoadedAddons/LibLoadedAddons.lua b/libs/LibLoadedAddons/LibLoadedAddons.lua
new file mode 100644
index 0000000..efc8e48
--- /dev/null
+++ b/libs/LibLoadedAddons/LibLoadedAddons.lua
@@ -0,0 +1,63 @@
+
+--Register LAM with LibStub
+local LIBRARY_NAME = "LibLoadedAddons"
+local MAJOR, MINOR = LIBRARY_NAME, 1
+local lla, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+if not lla then return end	--the same or newer version of this lib is already loaded into memory
+
+local loadedAddons = {}
+
+------------------------------------------------------------------------
+-- 	General Functions  --
+------------------------------------------------------------------------
+function lla:RegisterAddon(uniqueAddonName, versionNumber)
+	if type(versionNumber) ~= "number" then
+		return false, "Version number must be a number."
+	end
+
+	local version = loadedAddons[uniqueAddonName]
+
+	if version then
+		if version == 0 then
+			loadedAddons[uniqueAddonName] = versionNumber
+			return true
+		else
+			return false, "Version number already set for this addon"
+		end
+	end
+	return false, "Addon not loaded, addon name not found."
+end
+
+function lla:UnregisterAddon(uniqueAddonName)
+	if loadedAddons[uniqueAddonName] then
+		loadedAddons[uniqueAddonName] = nil
+		return true
+	end
+	return false, "Addon name was not registered"
+end
+
+function lla:IsAddonLoaded(uniqueAddonName)
+	if loadedAddons[uniqueAddonName] then
+		return true, loadedAddons[uniqueAddonName]
+	end
+	return false
+end
+
+local function OnPlayerActivated()
+	EVENT_MANAGER:UnregisterForEvent(LIBRARY_NAME, EVENT_ADD_ON_LOADED)
+end
+
+local function OnAddOnLoaded(_event, addonName)
+	loadedAddons[addonName] = 0
+end
+---------------------------------------------------------------------------------
+--  Register Events --
+---------------------------------------------------------------------------------
+EVENT_MANAGER:RegisterForEvent(LIBRARY_NAME, EVENT_ADD_ON_LOADED, OnAddOnLoaded)
+EVENT_MANAGER:RegisterForEvent(LIBRARY_NAME, EVENT_PLAYER_ACTIVATED, OnPlayerActivated)
+
+
+
+
+
+
diff --git a/libs/LibLoadedAddons/LibLoadedAddons.txt b/libs/LibLoadedAddons/LibLoadedAddons.txt
new file mode 100644
index 0000000..f72e3df
--- /dev/null
+++ b/libs/LibLoadedAddons/LibLoadedAddons.txt
@@ -0,0 +1,9 @@
+## APIVersion: 100013
+## Title: LibLoadedAddons
+## Version: 1.0
+## Author: Circonian
+## Description: A library used by addons to register that they are loaded. This is to make it easier for other addons to determine if your addon is running or not.
+
+
+LibStub\LibStub.lua
+LibLoadedAddons.lua
diff --git a/libs/LibMsgWin-1.0/LibMsgWin-1.0.lua b/libs/LibMsgWin-1.0/LibMsgWin-1.0.lua
new file mode 100644
index 0000000..4d80f63
--- /dev/null
+++ b/libs/LibMsgWin-1.0/LibMsgWin-1.0.lua
@@ -0,0 +1,236 @@
+
+--Register LAM with LibStub
+local MAJOR, MINOR = "LibMsgWin-1.0", 8
+local libmw, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+if not libmw then return end	--the same or newer version of this lib is already loaded into memory
+
+
+local function AdjustSlider(self)
+	local numHistoryLines = self:GetNamedChild("Buffer"):GetNumHistoryLines()
+	local numVisHistoryLines = self:GetNamedChild("Buffer"):GetNumVisibleLines()
+	local bufferScrollPos = self:GetNamedChild("Buffer"):GetScrollPosition()
+	local sliderMin, sliderMax = self:GetNamedChild("Slider"):GetMinMax()
+	local sliderValue = self:GetNamedChild("Slider"):GetValue()
+
+	self:GetNamedChild("Slider"):SetMinMax(0, numHistoryLines)
+
+	-- If the sliders at the bottom, stay at the bottom to show new text
+	if sliderValue == sliderMax then
+		self:GetNamedChild("Slider"):SetValue(numHistoryLines)
+	-- If the buffer is full start moving the slider up
+	elseif numHistoryLines == self:GetNamedChild("Buffer"):GetMaxHistoryLines() then
+		self:GetNamedChild("Slider"):SetValue(sliderValue-1)
+	end -- Else the slider does not move
+
+	-- If there are more history lines than visible lines show the slider
+	if numHistoryLines > numVisHistoryLines then
+		self:GetNamedChild("Slider"):SetHidden(false)
+	else
+		-- else hide the slider
+		self:GetNamedChild("Slider"):SetHidden(true)
+	end
+end
+
+function libmw:CreateMsgWindow(_UniqueName, _LabelText, _FadeDelay, _FadeTime)
+	-- Dimension Constraits
+	local minWidth = 200
+	local minHeight = 150
+
+	local tlw = WINDOW_MANAGER:CreateTopLevelWindow(_UniqueName)
+	tlw:SetMouseEnabled(true)
+	tlw:SetMovable(true)
+	tlw:SetHidden(false)
+	tlw:SetClampedToScreen(true)
+	tlw:SetDimensions(350, 400)
+	tlw:SetClampedToScreenInsets(-24)
+	tlw:SetAnchor(TOPLEFT, GuiRoot, TOPLEFT, 50,50)
+	tlw:SetDimensionConstraints(minWidth, minHeight)
+	tlw:SetResizeHandleSize(16)
+
+	-- Set Fade Delay/Times
+	tlw.fadeDelayWindow		= _FadeDelay or 0
+	tlw.fadeTimeWindow		= _FadeTime or 0
+	tlw.fadeDelayTextLines 	= tlw.fadeDelayWindow/1000
+	tlw.fadeTimeTextLines 	= tlw.fadeTimeWindow/1000
+
+	-- Create window fade timeline/animation
+	tlw.timeline = ANIMATION_MANAGER:CreateTimeline()
+	tlw.animation = tlw.timeline:InsertAnimation(ANIMATION_ALPHA, tlw, tlw.fadeDelayWindow)
+	tlw.animation:SetAlphaValues(1, 0)
+	tlw.animation:SetDuration(tlw.fadeTimeWindow)
+	tlw.timeline:PlayFromStart()
+
+
+	function tlw:AddText(_Message, _Red, _Green, _Blue)
+		local Red 	= _Red or 1
+		local Green = _Green or 1
+		local Blue 	= _Blue or 1
+
+		if not _Message then return end
+		-- Add message first
+		self:GetNamedChild("Buffer"):AddMessage(_Message, Red, Green, Blue)
+		-- Set new slider value & check visibility
+		AdjustSlider(self)
+		-- Reset Fade Timers
+		tlw:SetAlpha(1)
+		tlw.timeline:PlayFromStart()
+	end
+	function tlw:ChangeWinFade(_FadeDelay, _FadeTime)
+		if not (type(_FadeDelay) == "number" and type(_FadeTime) == "number") then return end
+		tlw.fadeDelayWindow		= _FadeDelay
+		tlw.fadeTimeWindow		= _FadeTime
+
+		tlw.timeline:SetAnimationOffset(tlw.animation, _FadeDelay)
+		tlw.animation:SetDuration(_FadeTime)
+	end
+	function tlw:ChangeTextFade(_FadeDelay, _FadeTime)
+		if not (type(_FadeDelay) == "number" and type(_FadeTime) == "number") then return end
+		tlw.fadeDelayTextLines 	= _FadeDelay/1000
+		tlw.fadeTimeTextLines 	= _FadeTime/1000
+		self:GetNamedChild("Buffer"):SetLineFade(_FadeDelay/1000, _FadeTime/1000)
+	end
+	function tlw:ClearText()
+		self:GetNamedChild("Buffer"):Clear()
+	end
+
+	local bg = WINDOW_MANAGER:CreateControl(_UniqueName.."Bg", tlw, CT_BACKDROP)
+	bg:SetAnchor(TOPLEFT, tlw, TOPLEFT, -8, -6)
+	bg:SetAnchor(BOTTOMRIGHT, tlw, BOTTOMRIGHT, 4, 4)
+	bg:SetEdgeTexture("EsoUI/Art/ChatWindow/chat_BG_edge.dds", 256, 256, 32)
+	bg:SetCenterTexture("EsoUI/Art/ChatWindow/chat_BG_center.dds")
+	bg:SetInsets(32, 32, -32, -32)
+	bg:SetDimensionConstraints(minWidth, minHeight)
+
+
+	local divider = WINDOW_MANAGER:CreateControl(_UniqueName.."Divider", tlw, CT_TEXTURE)
+	divider:SetDimensions(4, 8)
+	divider:SetAnchor(TOPLEFT, tlw, TOPLEFT, 20, 40)
+	divider:SetAnchor(TOPRIGHT, tlw, TOPRIGHT, -20, 40)
+	divider:SetTexture("EsoUI/Art/Miscellaneous/horizontalDivider.dds")
+	divider:SetTextureCoords(0.181640625, 0.818359375, 0, 1)
+
+
+	local buffer = WINDOW_MANAGER:CreateControl(_UniqueName.."Buffer", tlw, CT_TEXTBUFFER)
+	buffer:SetFont("ZoFontChat")
+	buffer:SetMaxHistoryLines(200)
+	buffer:SetMouseEnabled(true)
+	buffer:SetLinkEnabled(true)
+	buffer:SetAnchor(TOPLEFT, tlw, TOPLEFT, 20, 42)
+	buffer:SetAnchor(BOTTOMRIGHT, tlw, BOTTOMRIGHT, -35, -20)
+	buffer:SetLineFade(tlw.fadeDelayTextLines, tlw.fadeTimeTextLines)
+	buffer:SetHandler("OnLinkMouseUp", function(self, linkText, link, button)
+              --  ZO_PopupTooltip_SetLink(link)
+		ZO_LinkHandler_OnLinkMouseUp(link, button, self)
+	end)
+	---[[
+	buffer:SetHandler("OnMouseUp", function(self, linkText, link, button, temp1, temp2, temp3, temp4)
+        d("self: "..tostring(self:GetName()))
+        d("linkText: "..tostring(linkText))
+        d("link: "..tostring(link))
+        d("button: "..tostring(button))
+        d("temp1: "..tostring(temp1))
+        d("temp2: "..tostring(temp2))
+        d("temp3: "..tostring(temp3))
+        d("temp4: "..tostring(temp4))
+	end)
+
+
+	--]]
+
+	buffer:SetDimensionConstraints(minWidth-55, minHeight-62)
+
+	buffer:SetHandler("OnMouseWheel", function(self, delta, ctrl, alt, shift)
+		local offset = delta
+		local slider = buffer:GetParent():GetNamedChild("Slider")
+		if shift then
+			offset = offset * buffer:GetNumVisibleLines()
+		elseif ctrl then
+			offset = offset * buffer:GetNumHistoryLines()
+		end
+		buffer:SetScrollPosition(buffer:GetScrollPosition() + offset)
+		slider:SetValue(slider:GetValue() - offset)
+	end)
+
+	buffer:SetHandler("OnMouseEnter", function(...)
+		tlw.timeline:Stop()
+		buffer:SetLineFade(0, 0)
+		buffer:ShowFadedLines()
+		tlw:SetAlpha(1)
+	end)
+	buffer:SetHandler("OnMouseExit", function(...)
+		buffer:SetLineFade(tlw.fadeDelayTextLines, tlw.fadeTimeTextLines)
+		tlw.timeline:PlayFromStart()
+	end)
+
+	local slider = WINDOW_MANAGER:CreateControl(_UniqueName.."Slider", tlw, CT_SLIDER)
+	slider:SetDimensions(15, 32)
+	slider:SetAnchor(TOPRIGHT, tlw, TOPRIGHT, -25, 60)
+	slider:SetAnchor(BOTTOMRIGHT, tlw, BOTTOMRIGHT, -15, -80)
+	slider:SetMinMax(1, 1)
+	slider:SetMouseEnabled(true)
+	slider:SetValueStep(1)
+	slider:SetValue(1)
+	slider:SetHidden(true)
+	slider:SetThumbTexture("EsoUI/Art/ChatWindow/chat_thumb.dds", "EsoUI/Art/ChatWindow/chat_thumb_disabled.dds", nil, 8, 22, nil, nil, 0.6875, nil)
+	slider:SetBackgroundMiddleTexture("EsoUI/Art/ChatWindow/chat_scrollbar_track.dds")
+
+	slider:SetHandler("OnValueChanged", function(self,value, eventReason)
+		local numHistoryLines = self:GetParent():GetNamedChild("Buffer"):GetNumHistoryLines()
+		local sliderValue = slider:GetValue()
+
+		if eventReason == EVENT_REASON_HARDWARE then
+			buffer:SetScrollPosition(numHistoryLines-sliderValue)
+		end
+	end)
+
+
+	local scrollUp = WINDOW_MANAGER:CreateControlFromVirtual(_UniqueName.."SliderScrollUp", slider, "ZO_ScrollUpButton")
+	scrollUp:SetAnchor(BOTTOM, slider, TOP, -1, 0)
+	scrollUp:SetNormalTexture("EsoUI/Art/ChatWindow/chat_scrollbar_upArrow_up.dds")
+	scrollUp:SetPressedTexture("EsoUI/Art/ChatWindow/chat_scrollbar_upArrow_down.dds")
+	scrollUp:SetMouseOverTexture("EsoUI/Art/ChatWindow/chat_scrollbar_upArrow_over.dds")
+	scrollUp:SetDisabledTexture("EsoUI/Art/ChatWindow/chat_scrollbar_upArrow_disabled.dds")
+	scrollUp:SetHandler("OnMouseDown", function(...)
+		buffer:SetScrollPosition(buffer:GetScrollPosition()+1)
+		slider:SetValue(slider:GetValue()-1)
+	end)
+
+
+	local scrollDown = WINDOW_MANAGER:CreateControlFromVirtual(_UniqueName.."SliderScrollDown", slider, "ZO_ScrollDownButton")
+	scrollDown:SetAnchor(TOP, slider, BOTTOM, -1, 0)
+	scrollDown:SetNormalTexture("EsoUI/Art/ChatWindow/chat_scrollbar_downArrow_up.dds")
+	scrollDown:SetPressedTexture("EsoUI/Art/ChatWindow/chat_scrollbar_downArrow_down.dds")
+	scrollDown:SetMouseOverTexture("EsoUI/Art/ChatWindow/chat_scrollbar_downArrow_over.dds")
+	scrollDown:SetDisabledTexture("EsoUI/Art/ChatWindow/chat_scrollbar_downArrow_disabled.dds")
+	scrollDown:SetHandler("OnMouseDown", function(...)
+		buffer:SetScrollPosition(buffer:GetScrollPosition()-1)
+		slider:SetValue(slider:GetValue()+1)
+	end)
+
+
+	local scrollEnd = WINDOW_MANAGER:CreateControlFromVirtual(_UniqueName.."SliderScrollEnd", slider, "ZO_ScrollEndButton")
+	scrollEnd:SetDimensions(16, 16)
+	scrollEnd:SetAnchor(TOP, scrollDown, BOTTOM, 0, 0)
+	scrollEnd:SetHandler("OnMouseDown", function(...)
+		buffer:SetScrollPosition(0)
+		slider:SetValue(buffer:GetNumHistoryLines())
+	end)
+
+	if _LabelText and _LabelText ~= "" then
+		local label = WINDOW_MANAGER:CreateControl(_UniqueName.."Label", tlw, CT_LABEL)
+		label:SetText(_LabelText)
+		label:SetFont("$(ANTIQUE_FONT)|24")
+		label:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS)
+		local textHeight = label:GetTextHeight()
+		label:SetDimensionConstraints(minWidth-60, textHeight, nil, textHeight)
+		label:ClearAnchors()
+		label:SetAnchor(TOPLEFT, tlw, TOPLEFT, 30, (40-textHeight)/2+5)
+		label:SetAnchor(TOPRIGHT, tlw, TOPRIGHT, -30, (40-textHeight)/2+5)
+	end
+	return tlw
+end
+
+
+
+
+
diff --git a/libs/LibMsgWin-1.0/LibMsgWin-1.0.txt b/libs/LibMsgWin-1.0/LibMsgWin-1.0.txt
new file mode 100644
index 0000000..a13abe6
--- /dev/null
+++ b/libs/LibMsgWin-1.0/LibMsgWin-1.0.txt
@@ -0,0 +1,10 @@
+## APIVersion: 100010
+## Title: LibMsgWin-1.0
+## Version: 1.0 r3
+## Author: Circonian
+## Description: A library used to aid in creating message windows
+
+
+LibStub\LibStub.lua
+
+LibMsgWin-1.0.lua
diff --git a/libs/LibNeed4Research/LibNeed4Research.lua b/libs/LibNeed4Research/LibNeed4Research.lua
new file mode 100644
index 0000000..9bb9685
--- /dev/null
+++ b/libs/LibNeed4Research/LibNeed4Research.lua
@@ -0,0 +1,427 @@
+
+
+---[[
+--Register LAM with LibStub
+local MAJOR, MINOR = "LibNeed4Research", 5
+local ln4r, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+if not ln4r then return end	--the same or newer version of this lib is already loaded into memory
+--]]
+
+local ASV
+local LIBNEED4RESEARCHVARVERSION = 2  -- DO NOT CHANGE
+
+local varDefaults = {
+	KnownTraitTable = {},
+	KnownRecipeTable = {},
+	PlayerNames = {},
+}
+
+local tIsTraitResearchable = {
+	[ITEM_TRAIT_TYPE_ARMOR_DIVINES] 		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_EXPLORATION] 	= true,
+	[ITEM_TRAIT_TYPE_ARMOR_IMPENETRABLE] 	= true,
+	[ITEM_TRAIT_TYPE_ARMOR_INFUSED] 		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_INTRICATE] 		= false,
+	[ITEM_TRAIT_TYPE_ARMOR_ORNATE] 			= false,
+	[ITEM_TRAIT_TYPE_ARMOR_REINFORCED]		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_STURDY] 			= true,
+	[ITEM_TRAIT_TYPE_ARMOR_TRAINING] 		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_WELL_FITTED]		= true,
+	[ITEM_TRAIT_TYPE_ARMOR_NIRNHONED] 		= true,
+
+	[ITEM_TRAIT_TYPE_WEAPON_CHARGED] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_DEFENDING] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_INFUSED] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_INTRICATE] 		= false,
+	[ITEM_TRAIT_TYPE_WEAPON_ORNATE] 		= false,
+	[ITEM_TRAIT_TYPE_WEAPON_POWERED] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_PRECISE] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_SHARPENED] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_TRAINING] 		= true,
+	[ITEM_TRAIT_TYPE_WEAPON_WEIGHTED]	 	= true,
+	[ITEM_TRAIT_TYPE_WEAPON_NIRNHONED] 		= true,
+
+	[ITEM_TRAIT_TYPE_JEWELRY_ARCANE] 		= false,
+	[ITEM_TRAIT_TYPE_JEWELRY_HEALTHY]	 	= false,
+	[ITEM_TRAIT_TYPE_JEWELRY_ORNATE] 		= false,
+	[ITEM_TRAIT_TYPE_JEWELRY_ROBUST] 		= false,
+
+	[ITEM_TRAIT_TYPE_NONE] 					= false,
+}
+
+-- Used to find the crafting type & research Index for items
+local tEquipTypes = {
+	[ITEMTYPE_ARMOR] = {
+		--[ARMORTYPE_NONE] 	= {}, -- jewelry is excluded --
+		[ARMORTYPE_LIGHT] 	= {
+			["CRAFTINGSKILLTYPE"] 	= CRAFTING_TYPE_CLOTHIER,
+			[EQUIP_TYPE_CHEST]		= 1,
+			[EQUIP_TYPE_FEET]		= 2,
+			[EQUIP_TYPE_HAND]		= 3,
+			[EQUIP_TYPE_HEAD]		= 4,
+			[EQUIP_TYPE_LEGS]		= 5,
+			[EQUIP_TYPE_SHOULDERS]	= 6,
+			[EQUIP_TYPE_WAIST]		= 7,
+		},
+		[ARMORTYPE_MEDIUM] 	= {
+			["CRAFTINGSKILLTYPE"] 	= CRAFTING_TYPE_CLOTHIER,
+			[EQUIP_TYPE_CHEST]		= 8,
+			[EQUIP_TYPE_FEET]		= 9,
+			[EQUIP_TYPE_HAND]		= 10,
+			[EQUIP_TYPE_HEAD]		= 11,
+			[EQUIP_TYPE_LEGS]		= 12,
+			[EQUIP_TYPE_SHOULDERS]	= 13,
+			[EQUIP_TYPE_WAIST]		= 14,
+		},
+		[ARMORTYPE_HEAVY] 	= {
+			["CRAFTINGSKILLTYPE"] 	= CRAFTING_TYPE_BLACKSMITHING,
+			[EQUIP_TYPE_CHEST]		= 8,
+			[EQUIP_TYPE_FEET]		= 9,
+			[EQUIP_TYPE_HAND]		= 10,
+			[EQUIP_TYPE_HEAD]		= 11,
+			[EQUIP_TYPE_LEGS]		= 12,
+			[EQUIP_TYPE_SHOULDERS]	= 13,
+			[EQUIP_TYPE_WAIST]		= 14,
+		},
+	},
+	[ITEMTYPE_WEAPON] = {
+		[WEAPONTYPE_SHIELD]				= {
+			[EQUIP_TYPE_OFF_HAND] = 6, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_AXE]				= {
+			[EQUIP_TYPE_ONE_HAND] = 1, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_DAGGER]				= {
+			[EQUIP_TYPE_ONE_HAND] = 7, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_HAMMER]				= {
+			[EQUIP_TYPE_ONE_HAND] = 2, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_SWORD]				= {
+			[EQUIP_TYPE_ONE_HAND] = 3, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_TWO_HANDED_AXE]		= {
+			[EQUIP_TYPE_TWO_HAND] = 4, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_TWO_HANDED_HAMMER]	= {
+			[EQUIP_TYPE_TWO_HAND] = 5, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_TWO_HANDED_SWORD]	= {
+			[EQUIP_TYPE_TWO_HAND] = 6, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_BLACKSMITHING},
+		[WEAPONTYPE_BOW]				= {
+			[EQUIP_TYPE_TWO_HAND] = 1, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_FIRE_STAFF]			= {
+			[EQUIP_TYPE_TWO_HAND] = 2, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_FROST_STAFF]		= {
+			[EQUIP_TYPE_TWO_HAND] = 3, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_LIGHTNING_STAFF] 	= {
+			[EQUIP_TYPE_TWO_HAND] = 4, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+		[WEAPONTYPE_HEALING_STAFF]		= {
+			[EQUIP_TYPE_TWO_HAND] = 5, ["CRAFTINGSKILLTYPE"] = CRAFTING_TYPE_WOODWORKING},
+	},
+}
+
+
+------------------------------------------------------------------------
+-- Get Trait Index --
+------------------------------------------------------------------------
+local function GetTraitIndex(_BagIdOrLink, _iSlotId)
+	local lLink = _BagIdOrLink
+	if _iSlotId then
+		lLink = GetItemLink(_BagIdOrLink,_iSlotId)
+	end
+	local iTraitType = GetItemLinkTraitInfo(lLink)
+	if not tIsTraitResearchable[iTraitType] then return end
+
+	if ((iTraitType == ITEM_TRAIT_TYPE_WEAPON_NIRNHONED)
+	or (iTraitType == ITEM_TRAIT_TYPE_ARMOR_NIRNHONED)) then
+		return 9
+	end
+	return (iTraitType % 10)
+end
+
+local function GetResearchInfo(_BagIdOrLink, _iSlotId)
+	local lLink = _BagIdOrLink
+	if _iSlotId then
+		lLink = GetItemLink(_BagIdOrLink, _iSlotId)
+	end
+
+	local iTraitType = GetItemLinkTraitInfo(lLink)
+	if not tIsTraitResearchable[iTraitType] then return end
+
+	local iItemType = GetItemLinkItemType(lLink)
+	local iSubType = 0 -- 0 is the same return as WEAPONTYPE_NONE & ARMORTYPE_NONE
+
+	if iItemType == ITEMTYPE_ARMOR then
+		iSubType = GetItemLinkArmorType(lLink)
+	elseif iItemType == ITEMTYPE_WEAPON then
+		iSubType = GetItemLinkWeaponType(lLink)
+	else
+		return
+	end
+
+	if not (tEquipTypes[iItemType] and tEquipTypes[iItemType][iSubType]) then return end
+
+	local iEquipType 			= GetItemLinkEquipType(lLink)
+	local iCraftingSkillType 	= tEquipTypes[iItemType][iSubType]["CRAFTINGSKILLTYPE"]
+	local iResearchLineIndex 	= tEquipTypes[iItemType][iSubType][iEquipType]
+	local iTraitIndex 			= GetTraitIndex(_BagIdOrLink, _iSlotId)
+
+	return iCraftingSkillType, iResearchLineIndex, iTraitIndex
+end
+
+--[[
+ZO_CraftingUtils_IsTraitAppliedToWeapons(traitType)
+ZO_CraftingUtils_IsTraitAppliedToArmor(traitType)
+--]]
+
+local function IsTraitNeeded(_sPlayerName, _iCraftingSkillType, _iResearchLineIndex, _iTraitIndex)
+	--[[ checks to make sure were not getting called to early by an addon, before the libraries saved vars have loaded. Even though this is local, this is called from a function they can access.
+	--]]
+	if not (ASV and ASV.KnownTraitTable and ASV.KnownTraitTable[_sPlayerName]) then return true end
+	if not ASV.KnownTraitTable[_sPlayerName][_iCraftingSkillType] then return true end
+	if not ASV.KnownTraitTable[_sPlayerName][_iCraftingSkillType][_iResearchLineIndex] then return true end
+
+	if ASV.KnownTraitTable[_sPlayerName][_iCraftingSkillType][_iResearchLineIndex][_iTraitIndex] then
+		return false
+	end
+	return true
+end
+
+function ln4r:DoesPlayerNeedTrait(_sPlayerName, _iBagIdOrLink, _iSlotId)
+	local iCraftingSkillType, iResearchLineIndex, iTraitIndex = GetResearchInfo(_iBagIdOrLink, _iSlotId)
+
+	-- Its either not a researchable ItemType or doesn't have a researchable trait
+	if not (iCraftingSkillType and iResearchLineIndex and iTraitIndex) then return false end
+
+	if IsTraitNeeded(_sPlayerName, iCraftingSkillType, iResearchLineIndex, iTraitIndex) then
+		return true, iCraftingSkillType, iResearchLineIndex, iTraitIndex
+	end
+	return false
+end
+
+function ln4r:DoAnyPlayersNeedTrait(_iBagIdOrLink, _iSlotId)
+	local iCraftingSkillType, iResearchLineIndex, iTraitIndex = GetResearchInfo(_iBagIdOrLink, _iSlotId)
+
+	-- Its either not a researchable ItemType or doesn't have a researchable trait
+	if not (iCraftingSkillType and iResearchLineIndex and iTraitIndex) then return end
+
+	--[[ check to make sure were not getting called to early by an addon, before the libraries saved vars have loaded. Default to true, player needs, so nothing bad happens to the item.
+	--]]
+	if not (ASV and ASV.PlayerNames) then return end
+
+	local sCurrentPlayerName = GetUnitName("player")
+
+	local tPlayersThatNeed = {
+		CraftingSkillType	= iCraftingSkillType,
+		ResearchLineIndex 	= iResearchLineIndex,
+		TraitIndex 			= iTraitIndex,
+		PlayerCount 		= 0,
+		PlayerNames			= {},
+		PlayerNeeds			= false,
+		OtherNeeds			= false,
+	}
+
+	for sPlayerName,v in pairs(ASV.PlayerNames) do
+		if IsTraitNeeded(sPlayerName, iCraftingSkillType, iResearchLineIndex, iTraitIndex) then
+			tPlayersThatNeed.PlayerCount = tPlayersThatNeed.PlayerCount + 1
+
+			if sPlayerName == sCurrentPlayerName then
+				tPlayersThatNeed.PlayerNeeds = true
+			else
+				tPlayersThatNeed.OtherNeeds = true
+			end
+			tPlayersThatNeed.PlayerNames[sPlayerName] = true
+		end
+	end
+	if tPlayersThatNeed.PlayerCount > 0 then
+		return tPlayersThatNeed
+	end
+end
+--[[
+function ln4r:GetRecipeIndices(_iBagIdOrLink, _iSlotId)
+	local lLink = _iBagIdOrLink
+	if _iSlotId then
+		lLink = GetItemLink(_iBagIdOrLink, _iSlotId)
+	end
+
+	local iItemType = GetItemLinkItemType(lLink)
+	if iItemType ~= ITEMTYPE_RECIPE then return end
+
+	local lRecipeResultItemLink = GetItemLinkRecipeResultItemLink(lLink)
+	local iNumRecipeLists = GetNumRecipeLists()
+
+	for iRecipeListIndex = 1, iNumRecipeLists do
+		local _, iNumRecipes = GetRecipeListInfo(iRecipeListIndex)
+		for iRecipeIndex = 1, iNumRecipes do
+			local lRecipeResultTableLink = GetRecipeResultItemLink(iRecipeListIndex, iRecipeIndex)
+			if lRecipeResultItemLink == lRecipeResultTableLink then
+				return iRecipeListIndex, iRecipeIndex, lRecipeResultItemLink
+			end
+		end
+	end
+end
+--]]
+function ln4r:DoesPlayerNeedRecipe(_sPlayerName, _iBagIdOrLink, _iSlotId)
+	--[[ check to make sure were not getting called to early by an addon, before the libraries saved vars have loaded. --]]
+	if not (ASV and ASV.KnownRecipeTable and ASV.KnownRecipeTable[_sPlayerName]) then return end
+
+	local lLink = _iBagIdOrLink
+	if _iSlotId then
+		lLink = GetItemLink(_iBagIdOrLink, _iSlotId)
+	end
+
+	local iItemType = GetItemLinkItemType(lLink)
+	if iItemType ~= ITEMTYPE_RECIPE then return false end
+
+	local lRecipeResultItemLink = GetItemLinkRecipeResultItemLink(lLink)
+	local sItemId = select(4,ZO_LinkHandler_ParseLink(lRecipeResultItemLink))
+	local iItemId = tonumber(sItemId)
+
+	if type(iItemId) == "number" and ASV.KnownRecipeTable[_sPlayerName][iItemId] then
+		return false
+	end
+	return true
+end
+
+function ln4r:DoAnyPlayersNeedRecipe(_iBagIdOrLink, _iSlotId)
+	local lLink = _iBagIdOrLink
+	if _iSlotId then
+		lLink = GetItemLink(_iBagIdOrLink, _iSlotId)
+	end
+
+	local iItemType = GetItemLinkItemType(lLink)
+	if iItemType ~= ITEMTYPE_RECIPE then return end
+	-- check to make sure were not getting called to early by an addon, before the libraries saved
+	-- vars have loaded.
+	if not (ASV and ASV.PlayerNames) then return end
+
+	local sCurrentPlayerName = GetUnitName("player")
+
+	local tPlayersThatNeed = {
+		CraftingSkillType	= CRAFTING_TYPE_PROVISIONING,
+		PlayerCount 		= 0,
+		PlayerNames			= {},
+		PlayerNeeds			= false,
+		OtherNeeds			= false,
+	}
+	for sPlayerName,v in pairs(ASV.PlayerNames) do
+		local bPlayerNeedsRecipe = ln4r:DoesPlayerNeedRecipe(sPlayerName, _iBagIdOrLink, _iSlotId)
+		if bPlayerNeedsRecipe then
+			tPlayersThatNeed.PlayerCount = tPlayersThatNeed.PlayerCount + 1
+
+			if sPlayerName == sCurrentPlayerName then
+				tPlayersThatNeed.PlayerNeeds = true
+			else
+				tPlayersThatNeed.OtherNeeds = true
+			end
+			tPlayersThatNeed.PlayerNames[sPlayerName] = true
+		end
+	end
+	if tPlayersThatNeed.PlayerCount > 0 then
+		return tPlayersThatNeed
+	end
+end
+
+
+local function AddKnownTrait(_EventCode,  _iCraftingSkillType,  _iResearchLineIndex,  _iTraitIndex)
+	if _iCraftingSkillType == CRAFTING_TYPE_INVALID then return end
+	local sPlayerName = GetUnitName("player")
+
+	if not ASV.KnownTraitTable[sPlayerName][_iCraftingSkillType][_iResearchLineIndex] then
+		ASV.KnownTraitTable[sPlayerName][_iCraftingSkillType][_iResearchLineIndex] = {}
+	end
+
+    local iTraitType  = GetSmithingResearchLineTraitInfo(_iCraftingSkillType, _iResearchLineIndex, _iTraitIndex)
+
+	if iTraitType ~= ITEM_TRAIT_TYPE_NONE then
+		ASV.KnownTraitTable[sPlayerName][_iCraftingSkillType][_iResearchLineIndex][_iTraitIndex] = true
+	end
+end
+
+local function AddKnownRecipe(_EventCode, _iRecipeListIndex, _iRecipeIndex)
+	local sPlayerName = GetUnitName("player")
+    local lRecipeResultItemLink = GetRecipeResultItemLink(_iRecipeListIndex, _iRecipeIndex)
+	local sItemId = select(4,ZO_LinkHandler_ParseLink(lRecipeResultItemLink))
+	local iItemId = tonumber(sItemId)
+
+	if type(iItemId) == "number" then
+		ASV.KnownRecipeTable[sPlayerName][iItemId] = true
+	end
+end
+
+local function UpdateUnKnownRecipes()
+	for iRecipeListIndex = 1, GetNumRecipeLists()  do
+		local sRecipeListName, iNumRecipes = GetRecipeListInfo(iRecipeListIndex)
+		for iRecipeIndex = 1, iNumRecipes do
+			local bIsKnown, sRecipeName = GetRecipeInfo(iRecipeListIndex, iRecipeIndex)
+			if bIsKnown then
+				AddKnownRecipe(nil, iRecipeListIndex, iRecipeIndex)
+			end
+		end
+	end
+end
+
+local function UpdateCraftingSkillTraits(_iCraftingSkillType)
+	for iResearchLineIndex = 1, GetNumSmithingResearchLines(_iCraftingSkillType) do
+		local _,_, iNumTraits = GetSmithingResearchLineInfo(_iCraftingSkillType, iResearchLineIndex)
+		for iTraitIndex = 1, iNumTraits do
+			local iTraitType, _, bIsKnown = GetSmithingResearchLineTraitInfo(_iCraftingSkillType, iResearchLineIndex, iTraitIndex)
+			-- if its not known, check to see if we are researching it --
+			if bIsKnown or GetSmithingResearchLineTraitTimes(_iCraftingSkillType, iResearchLineIndex, iTraitIndex) ~= nil then
+				AddKnownTrait(nil,  _iCraftingSkillType,  iResearchLineIndex,  iTraitIndex)
+			end
+		end
+	end
+end
+local function UpdateUnKnownTraits()
+	UpdateCraftingSkillTraits(CRAFTING_TYPE_BLACKSMITHING)
+	UpdateCraftingSkillTraits(CRAFTING_TYPE_CLOTHIER)
+	UpdateCraftingSkillTraits(CRAFTING_TYPE_WOODWORKING)
+end
+
+---------------------------------------------------------------------------------
+---- Update Tables ----
+--	Called during initialization() --
+---------------------------------------------------------------------------------
+local function UpdateTables()
+	local sPlayerName = GetUnitName("player")
+
+	if not ASV.KnownTraitTable[sPlayerName] then
+		ASV.KnownTraitTable[sPlayerName] = {
+			[CRAFTING_TYPE_BLACKSMITHING] 	= {},
+			[CRAFTING_TYPE_CLOTHIER] 		= {},
+			[CRAFTING_TYPE_WOODWORKING] 	= {},
+		}
+	end
+	if not ASV.KnownRecipeTable[sPlayerName] then
+		ASV.KnownRecipeTable[sPlayerName] = {}
+	end
+	if not ASV.PlayerNames[sPlayerName] then
+		ASV.PlayerNames[sPlayerName] = true
+	end
+
+	UpdateUnKnownTraits()
+	UpdateUnKnownRecipes()
+end
+
+local function OnAddOnLoaded(_event, _sAddonName)
+	if _sAddonName == "ZO_Ingame" then
+		ln4r:Initialize()
+	end
+end
+function ln4r:Initialize()
+	ln4r.AccountSavedVariables = ZO_SavedVars:NewAccountWide("ZO_Ingame_SavedVariables", LIBNEED4RESEARCHVARVERSION, "LibNeed4ResearchVars", varDefaults)
+
+	ASV = ln4r.AccountSavedVariables
+	--Need4Research = ln4r.AccountSavedVariables
+	UpdateTables()
+
+	-- register for events
+	EVENT_MANAGER:RegisterForEvent("LibNeed4Research", EVENT_SMITHING_TRAIT_RESEARCH_STARTED, AddKnownTrait)
+	EVENT_MANAGER:RegisterForEvent("LibNeed4Research", EVENT_RECIPE_LEARNED, AddKnownRecipe)
+
+	-- Unregister events
+	EVENT_MANAGER:UnregisterForEvent("LibNeed4Research", EVENT_ADD_ON_LOADED)
+end
+
+EVENT_MANAGER:RegisterForEvent("LibNeed4Research", EVENT_ADD_ON_LOADED, OnAddOnLoaded)
+
+
+
+
+
+
diff --git a/libs/LibNeed4Research/LibNeed4Research.txt b/libs/LibNeed4Research/LibNeed4Research.txt
new file mode 100644
index 0000000..ee810e0
--- /dev/null
+++ b/libs/LibNeed4Research/LibNeed4Research.txt
@@ -0,0 +1,9 @@
+## APIVersion: 100010
+## Title: LibNeed4Research
+## Version: xxx
+## Author: Circonian
+## Description: A library used to track needed traits & recipes for all characters.
+
+
+LibStub\LibStub.lua
+LibNeed4Research.lua
diff --git a/libs/LibStub/LibStub.lua b/libs/LibStub/LibStub.lua
new file mode 100644
index 0000000..4c35014
--- /dev/null
+++ b/libs/LibStub/LibStub.lua
@@ -0,0 +1,38 @@
+-- LibStub is a simple versioning stub meant for use in Libraries.  http://www.wowace.com/wiki/LibStub for more info
+-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
+-- LibStub developed for World of Warcraft by above members of the WowAce community.
+-- Ported to Elder Scrolls Online by Seerah
+
+local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 4
+local LibStub = _G[LIBSTUB_MAJOR]
+
+local strformat = string.format
+if not LibStub or LibStub.minor < LIBSTUB_MINOR then
+	LibStub = LibStub or {libs = {}, minors = {} }
+	_G[LIBSTUB_MAJOR] = LibStub
+	LibStub.minor = LIBSTUB_MINOR
+
+	function LibStub:NewLibrary(major, minor)
+		assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
+		if type(minor) ~= "number" then
+			minor = assert(tonumber(zo_strmatch(minor, "%d+%.?%d*")), "Minor version must either be a number or contain a number.")
+		end
+
+		local oldminor = self.minors[major]
+		if oldminor and oldminor >= minor then return nil end
+		self.minors[major], self.libs[major] = minor, self.libs[major] or {}
+		return self.libs[major], oldminor
+	end
+
+	function LibStub:GetLibrary(major, silent)
+		if not self.libs[major] and not silent then
+			error(strformat("Cannot find a library instance of %q.", tostring(major)), 2)
+		end
+		return self.libs[major], self.minors[major]
+	end
+
+	function LibStub:IterateLibraries() return pairs(self.libs) end
+	setmetatable(LibStub, { __call = LibStub.GetLibrary })
+end
+
+LibStub.SILENT = true
\ No newline at end of file