Now works on passing npcs. It'll let you hear it once, then mute all further repeats.

Wobin [04-14-14 - 11:58]
Now works on passing npcs. It'll let you hear it once, then mute all further repeats.
Filename
BorrowerAndLender.lua
BorrowerAndLender.txt
libs/AceTimer-3.0/AceTimer-3.0.lua
libs/AceTimer-3.0/AceTimer-3.0.txt
libs/LibAddonMenu-1.0/LibAddonMenu-1.0.lua
libs/LibStub/LibStub.lua
diff --git a/BorrowerAndLender.lua b/BorrowerAndLender.lua
index 4554eee..06d44f1 100644
--- a/BorrowerAndLender.lua
+++ b/BorrowerAndLender.lua
@@ -1,39 +1,80 @@
 BorrowerAndLender = {}
+LibStub("AceTimer-3.0"):Embed(BorrowerAndLender)
+local LAM = LibStub:GetLibrary("LibAddonMenu-1.0")
+
+local originalLevel
+local inBank
+local control = ZO_OptionsWindow.controlTable[2][9]
+local currentLevels
+local currentWait = nil
+local settings
+
 function EndsWith(String,End)
    return End=='' or string.sub(String,-string.len(End))==End
 end

-local originalLevel = 1
-local inBank
+local function Hush()
+	if control.currentChoice ~= 0 then
+		currentLevels = control.currentChoice or control.value
+		SetSetting(control.system, control.settingId, 0)
+	end
+end
+
+local function SpeakUpLad()
+	if currentWait then BorrowerAndLender:CancelTimer(currentWait) end
+	currentWait = nil
+	control.currentChoice = currentLevels or settings.defaultSoundLevel
+	SetSetting(control.system, control.settingId, control.currentChoice)
+end

 local function WhoAmI(eventCode, options)
-	if EndsWith(ZO_ChatterOption1:GetText(), GetString(SI_INTERACT_OPTION_BANK)) then
-		inBank = true
-		local control = ZO_OptionsWindow.controlTable[2][9]
-		originalLevel = control.currentChoice
-		control.value = 1
-		SetSetting(control.system, control.settingId, 1)
-		ZO_Options_UpdateOption(control)
+	if EndsWith(ZO_ChatterOption1:GetText(), GetString(SI_INTERACT_OPTION_BANK)) then
+		if currentWait then BorrowerAndLender:CancelTimer(currentWait) end
+		currentWait = BorrowerAndLender:ScheduleTimer(SpeakUpLad, (#ZO_InteractWindowTargetAreaBodyText:GetText()/15) + 5)
+		Hush()
 	end
 end

-local function WhoIWas(eventCode)
-	if inBank then
-		local control = ZO_OptionsWindow.controlTable[2][9]
-		control.value = originalLevel
-		SetSetting(control.system, control.settingId, originalLevel)
-		ZO_Options_UpdateOption(control)
-		inBank = false
-	end
+local function FilterNPC(eventCode, channel, npc, chat)
+	if channel ~= CHAT_CHANNEL_MONSTER_SAY and channel ~= CHAT_CHANNEL_MONSTER_YELL then return end
+
+	if not settings.chats[npc] then settings.chats[npc] = {} end
+	if not settings.chats[npc][chat] then settings.chats[npc][chat] = 0 return end
+
+	settings.chats[npc][chat] =  settings.chats[npc][chat] + 1
+
+	if currentWait then BorrowerAndLender:CancelTimer(currentWait) end
+
+	currentWait = BorrowerAndLender:ScheduleTimer(SpeakUpLad, (#chat/15) + 2)
+	Hush()
 end
+
 local function BorrowerAndLenderLoaded(eventCode, addOnName)

 	if(addOnName ~= "BorrowerAndLender") then
         return
     end

+	local defaults = {
+		chats = {},
+		defaultSoundLevel = control.currentChoice or control.value or 0
+	}
+
+	settings = ZO_SavedVars:NewAccountWide("BorrowerAndLender_Settings", 1, nil, defaults)
+
+	local panel = LAM:CreateControlPanel("BAL", "Borrower And Lender")
+
+	LAM:AddHeader(panel, "BAL_General", "Settings")
+
+  	LAM:AddSlider(panel, "defaultSound", "Set the standard voice over volume ",
+						"Set this value to your standard voice over volume",
+						0, 100, 1, function() return settings.defaultSoundLevel end,
+						function(value) settings.defaultSoundLevel = value end)
+
+
 	EVENT_MANAGER:RegisterForEvent("BALWho", EVENT_CHATTER_BEGIN, WhoAmI)
-	EVENT_MANAGER:RegisterForEvent("BALWas", EVENT_CHATTER_END, WhoIWas)
+	EVENT_MANAGER:RegisterForEvent("BALWho", EVENT_CHATTER_END, SpeakUpLad)
+	EVENT_MANAGER:RegisterForEvent("BALChat", EVENT_CHAT_MESSAGE_CHANNEL, FilterNPC)
 end


diff --git a/BorrowerAndLender.txt b/BorrowerAndLender.txt
index 1e70c5b..538a670 100644
--- a/BorrowerAndLender.txt
+++ b/BorrowerAndLender.txt
@@ -2,5 +2,10 @@
 ## Author: Wobin
 ## Version: @project-version@
 ## APIVersion: 100003
+## SavedVariables: BorrowerAndLender_Settings
+
+libs/LibStub/LibStub.lua
+libs/AceTimer-3.0/AceTimer-3.0.lua
+libs\LibAddonMenu-1.0\LibAddonMenu-1.0.lua

 BorrowerAndLender.lua
\ No newline at end of file
diff --git a/libs/AceTimer-3.0/AceTimer-3.0.lua b/libs/AceTimer-3.0/AceTimer-3.0.lua
new file mode 100644
index 0000000..7af967c
--- /dev/null
+++ b/libs/AceTimer-3.0/AceTimer-3.0.lua
@@ -0,0 +1,277 @@
+--- **AceTimer-3.0** provides a central facility for registering timers.
+-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
+-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered
+-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
+-- AceTimer is currently limited to firing timers at a frequency of 0.01s. This constant may change
+-- in the future, but for now it's required as animations with lower frequencies are buggy.
+--
+-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
+-- need to cancel the timer you just registered.
+--
+-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
+-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
+-- and can be accessed directly, without having to explicitly call AceTimer itself.\\
+-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
+-- make into AceTimer.
+-- @class file
+-- @name AceTimer-3.0
+-- @release $Id: AceTimer-3.0.lua 1079 2013-02-17 19:56:06Z funkydude $
+
+local MAJOR, MINOR = "AceTimer-3.0", 16 -- Bump minor on changes
+local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not AceTimer then return end -- No upgrade needed
+
+AceTimer.inactiveTimers = AceTimer.inactiveTimers or {}                    -- Timer recycling storage
+AceTimer.activeTimers = AceTimer.activeTimers or {}                        -- Active timer list
+
+-- Lua APIs
+local type, unpack, next, error, pairs, tostring, select = type, unpack, next, error, pairs, tostring, select
+
+-- Upvalue our private data
+local inactiveTimers = AceTimer.inactiveTimers
+local activeTimers = AceTimer.activeTimers
+
+local function OnFinished(self)
+	local id = self.id
+	if type(self.func) == "string" then
+		-- We manually set the unpack count to prevent issues with an arg set that contains nil and ends with nil
+		-- e.g. local t = {1, 2, nil, 3, nil} print(#t) will result in 2, instead of 5. This fixes said issue.
+		self.object[self.func](self.object, unpack(self.args, 1, self.argsCount))
+	else
+		self.func(unpack(self.args, 1, self.argsCount))
+	end
+
+	-- If the id is different it means that the timer was already cancelled
+	-- and has been used to create a new timer during the OnFinished callback.
+	if not self.looping and id == self.id then
+		activeTimers[self.id] = nil
+		self.args = nil
+		inactiveTimers[self] = true
+	end
+end
+
+local function new(self, loop, func, delay, ...)
+	local timer = next(inactiveTimers)
+	if timer then
+		inactiveTimers[timer] = nil
+	else
+		local anim = CreateSimpleAnimation()
+		timer = anim:GetTimeline()
+		anim:SetHandler("OnStop", function(me) OnFinished(timer) end)
+	end
+
+	-- Very low delays cause the animations to fail randomly.
+	-- A limited resolution of 0.01 seems reasonable.
+	if delay < 0.01 then
+		delay = 0.01
+	end
+
+	timer.object = self
+	timer.func = func
+	timer.looping = loop
+	timer.args = {...}
+	timer.argsCount = select("#", ...)
+
+	local anim = timer:GetAnimation()
+	if loop then
+		timer:SetPlaybackType(ANIMATION_PLAYBACK_LOOP,LOOP_INDEFINITELY)
+	else
+		timer:SetPlaybackType(ANIMATION_PLAYBACK_ONE_SHOT)
+	end
+	anim:SetDuration(delay)
+
+	local id = tostring(timer.args)
+	timer.id = id
+	activeTimers[id] = timer
+
+	timer:PlayFromStart()
+
+	return id
+end
+
+--- Schedule a new one-shot timer.
+-- The timer will fire once in `delay` seconds, unless canceled before.
+-- @param callback Callback function for the timer pulse (funcref or method name).
+-- @param delay Delay for the timer, in seconds.
+-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
+-- @usage
+-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
+--
+-- function MyAddOn:OnEnable()
+--   self:ScheduleTimer("TimerFeedback", 5)
+-- end
+--
+-- function MyAddOn:TimerFeedback()
+--   print("5 seconds passed")
+-- end
+function AceTimer:ScheduleTimer(func, delay, ...)
+	if not func or not delay then
+		error(MAJOR..": ScheduleTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
+	end
+	if type(func) == "string" then
+		if type(self) ~= "table" then
+			error(MAJOR..": ScheduleTimer(callback, delay, args...): 'self' - must be a table.", 2)
+		elseif not self[func] then
+			error(MAJOR..": ScheduleTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
+		end
+	end
+
+	delay = delay * 1000	-- Convert delay from seconds to milliseconds
+
+	return new(self, nil, func, delay, ...)
+end
+
+--- Schedule a repeating timer.
+-- The timer will fire every `delay` seconds, until canceled.
+-- @param callback Callback function for the timer pulse (funcref or method name).
+-- @param delay Delay for the timer, in seconds.
+-- @param ... An optional, unlimited amount of arguments to pass to the callback function.
+-- @usage
+-- MyAddOn = LibStub("AceAddon-3.0"):NewAddon("MyAddOn", "AceTimer-3.0")
+--
+-- function MyAddOn:OnEnable()
+--   self.timerCount = 0
+--   self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
+-- end
+--
+-- function MyAddOn:TimerFeedback()
+--   self.timerCount = self.timerCount + 1
+--   print(("%d seconds passed"):format(5 * self.timerCount))
+--   -- run 30 seconds in total
+--   if self.timerCount == 6 then
+--     self:CancelTimer(self.testTimer)
+--   end
+-- end
+function AceTimer:ScheduleRepeatingTimer(func, delay, ...)
+	if not func or not delay then
+		error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'callback' and 'delay' must have set values.", 2)
+	end
+	if type(func) == "string" then
+		if type(self) ~= "table" then
+			error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): 'self' - must be a table.", 2)
+		elseif not self[func] then
+			error(MAJOR..": ScheduleRepeatingTimer(callback, delay, args...): Tried to register '"..func.."' as the callback, but it doesn't exist in the module.", 2)
+		end
+	end
+
+	delay = delay * 1000	-- Convert delay from seconds to milliseconds
+
+	return new(self, true, func, delay, ...)
+end
+
+--- Cancels a timer with the given id, registered by the same addon object as used for `:ScheduleTimer`
+-- Both one-shot and repeating timers can be canceled with this function, as long as the `id` is valid
+-- and the timer has not fired yet or was canceled before.
+-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
+function AceTimer:CancelTimer(id)
+	local timer = activeTimers[id]
+	if not timer then return false end
+
+	timer:Stop()
+
+	activeTimers[id] = nil
+	timer.args = nil
+	inactiveTimers[timer] = true
+	return true
+end
+
+--- Cancels all timers registered to the current addon object ('self')
+function AceTimer:CancelAllTimers()
+	for k,v in pairs(activeTimers) do
+		if v.object == self then
+			AceTimer.CancelTimer(self, k)
+		end
+	end
+end
+
+--- Returns the time left for a timer with the given id, registered by the current addon object ('self').
+-- This function will return 0 when the id is invalid.
+-- @param id The id of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
+-- @return The time left on the timer.
+function AceTimer:TimeLeft(id)
+	local timer = activeTimers[id]
+	if not timer then return 0 end
+	return timer:GetDuration() * timer:GetProgress() / 1000		-- Time left / 1000 to return seconds
+end
+
+
+-- ---------------------------------------------------------------------
+-- Upgrading
+
+-- Upgrade from old hash-bucket based timers to animation timers
+if oldminor and oldminor < 10 then
+	-- disable old timer logic
+	-- TODO? ~Errc
+	-- convert timers
+	for object,timers in pairs(AceTimer.selfs) do
+		for handle,timer in pairs(timers) do
+			if type(timer) == "table" and timer.callback then
+				local id
+				if timer.delay then
+					id = AceTimer.ScheduleRepeatingTimer(timer.object, timer.callback, timer.delay, timer.arg)
+				else
+					id = AceTimer.ScheduleTimer(timer.object, timer.callback, timer.when - GetTime(), timer.arg)
+				end
+				-- change id to the old handle
+				local t = activeTimers[id]
+				activeTimers[id] = nil
+				activeTimers[handle] = t
+				t.id = handle
+			end
+		end
+	end
+	AceTimer.selfs = nil
+	AceTimer.hash = nil
+	AceTimer.debug = nil
+elseif oldminor and oldminor < 13 then
+	for handle, id in pairs(AceTimer.hashCompatTable) do
+		local t = activeTimers[id]
+		if t then
+			activeTimers[id] = nil
+			activeTimers[handle] = t
+			t.id = handle
+		end
+	end
+	AceTimer.hashCompatTable = nil
+end
+
+-- upgrade existing timers to the latest OnFinished
+for timer in pairs(inactiveTimers) do
+	timer:SetScript("OnStop", OnFinished)
+end
+
+for _,timer in pairs(activeTimers) do
+	timer:SetScript("OnStop", OnFinished)
+end
+
+-- ---------------------------------------------------------------------
+-- Embed handling
+
+AceTimer.embeds = AceTimer.embeds or {}
+
+local mixins = {
+	"ScheduleTimer", "ScheduleRepeatingTimer",
+	"CancelTimer", "CancelAllTimers",
+	"TimeLeft"
+}
+
+function AceTimer:Embed(target)
+	AceTimer.embeds[target] = true
+	for _,v in pairs(mixins) do
+		target[v] = AceTimer[v]
+	end
+	return target
+end
+
+-- AceTimer:OnEmbedDisable(target)
+-- target (object) - target object that AceTimer is embedded in.
+--
+-- cancel all timers registered for the object
+function AceTimer:OnEmbedDisable(target)
+	target:CancelAllTimers()
+end
+
+for addon in pairs(AceTimer.embeds) do
+	AceTimer:Embed(addon)
+end
diff --git a/libs/AceTimer-3.0/AceTimer-3.0.txt b/libs/AceTimer-3.0/AceTimer-3.0.txt
new file mode 100644
index 0000000..3a4abca
--- /dev/null
+++ b/libs/AceTimer-3.0/AceTimer-3.0.txt
@@ -0,0 +1,6 @@
+## APIVersion: 100000
+## Title: AceTimer-3.0
+## Credits: Kaelten, ported to ESO by Errc
+## DependsOn: LibStub
+
+AceTimer/AceTimer-3.0.lua
\ No newline at end of file
diff --git a/libs/LibAddonMenu-1.0/LibAddonMenu-1.0.lua b/libs/LibAddonMenu-1.0/LibAddonMenu-1.0.lua
new file mode 100644
index 0000000..7e382b1
--- /dev/null
+++ b/libs/LibAddonMenu-1.0/LibAddonMenu-1.0.lua
@@ -0,0 +1,373 @@
+local MAJOR, MINOR = "LibAddonMenu-1.0", 6
+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
+
+--UPVALUES--
+lam.lastAddedControl = {}
+local lastAddedControl = lam.lastAddedControl
+local wm = GetWindowManager()
+local strformat = string.format
+local tostring = tostring
+local round = zo_round
+local optionsWindow = ZO_OptionsWindowSettingsScrollChild
+
+--maybe return the controls from the creation functions?
+
+function lam:CreateControlPanel(controlPanelID, controlPanelName)
+	local panelID
+
+	if _G[controlPanelID] then
+		panelID = _G[controlPanelID]
+		return panelID
+	end
+
+	ZO_OptionsWindow_AddUserPanel(controlPanelID, controlPanelName)
+
+	--disables Defaults button because we don't need it, but keybind still works :/ ...
+	panelID = _G[controlPanelID]
+	ZO_PreHook("ZO_OptionsWindow_ChangePanels", function(panel)
+			local enable = (panel ~=  panelID)
+			ZO_OptionsWindowResetToDefaultButton:SetEnabled(enable)
+			ZO_OptionsWindowResetToDefaultButton:SetKeybindEnabled(enable)
+		end)
+
+	return panelID
+end
+
+function lam:AddHeader(panelID, controlName, text)
+	local header = wm:CreateControlFromVirtual(controlName, optionsWindow, lastAddedControl[panelID] and "ZO_Options_SectionTitle_WithDivider" or "ZO_Options_SectionTitle")
+	if lastAddedControl[panelID] then
+		header:SetAnchor(TOPLEFT, lastAddedControl[panelID], BOTTOMLEFT, 0, 15)
+	else
+		header:SetAnchor(TOPLEFT)
+	end
+	header.controlType = OPTIONS_SECTION_TITLE
+	header.panel = panelID
+	header.text = text
+
+	ZO_OptionsWindow_InitializeControl(header)
+
+	lastAddedControl[panelID] = header
+
+	return header
+end
+
+
+--To-Do list:
+--extra sub-options window out to the right?? (or maybe addon list?)
+--find alternatives to handler hooks
+
+function lam:AddSlider(panelID, controlName, text, tooltip, minValue, maxValue, step, getFunc, setFunc, warning, warningText)
+	local slider = wm:CreateControlFromVirtual(controlName, optionsWindow, "ZO_Options_Slider")
+	slider:SetAnchor(TOPLEFT, lastAddedControl[panelID], BOTTOMLEFT, 0, 6)
+	slider.controlType = OPTIONS_SLIDER
+	slider.system = SETTING_TYPE_UI
+	slider.panel = panelID
+	slider.text = text
+	slider.tooltipText = tooltip
+	slider.showValue = true
+	slider.showValueMin = minValue
+	slider.showValueMax = maxValue
+	local range = maxValue - minValue
+	local slidercontrol = slider:GetNamedChild("Slider")
+	local slidervalue = slider:GetNamedChild("ValueLabel")
+	slidercontrol:SetValueStep(1/range * step)
+	slider:SetHandler("OnShow", function()
+			local curValue = getFunc()
+			slidercontrol:SetValue((curValue - minValue)/range)
+			slidervalue:SetText(tostring(curValue))
+		end)
+	slidercontrol:SetHandler("OnValueChanged", function (self, value)
+			self:SetValue(value)
+			value = round(value*range + minValue)
+			slidervalue:SetText(strformat("%d", value))
+		end)
+	slidercontrol:SetHandler("OnSliderReleased", function(self, value)
+			value = round(value*range + minValue)
+			setFunc(value)
+		end)
+
+	if warning then
+		slider.warning = wm:CreateControlFromVirtual(controlName.."WarningIcon", slider, "ZO_Options_WarningIcon")
+		slider.warning:SetAnchor(RIGHT, slidercontrol, LEFT, -5, 0)
+		slider.warning.tooltipText = warningText
+	end
+
+	ZO_OptionsWindow_InitializeControl(slider)
+
+	lastAddedControl[panelID] = slider
+
+	return slider
+end
+
+function lam:AddDropdown(panelID, controlName, text, tooltip, validChoices, getFunc, setFunc, warning, warningText)
+	local dropdown = wm:CreateControlFromVirtual(controlName, optionsWindow, "ZO_Options_Dropdown")
+	dropdown:SetAnchor(TOPLEFT, lastAddedControl[panelID], BOTTOMLEFT, 0, 6)
+	dropdown.controlType = OPTIONS_DROPDOWN
+	dropdown.system = SETTING_TYPE_UI
+	dropdown.panel = panelID
+	dropdown.text = text
+	dropdown.tooltipText = tooltip
+	dropdown.valid = validChoices
+	local dropmenu = ZO_ComboBox_ObjectFromContainer(GetControl(dropdown, "Dropdown"))
+	local setText = dropmenu.m_selectedItemText.SetText
+	local selectedName
+	ZO_PreHookHandler(dropmenu.m_selectedItemText, "OnTextChanged", function(self)
+			if dropmenu.m_selectedItemData then
+				selectedName = dropmenu.m_selectedItemData.name
+				setText(self, selectedName)
+				setFunc(selectedName)
+			end
+		end)
+	dropdown:SetHandler("OnShow", function()
+			dropmenu:SetSelectedItem(getFunc())
+		end)
+
+	if warning then
+		dropdown.warning = wm:CreateControlFromVirtual(controlName.."WarningIcon", dropdown, "ZO_Options_WarningIcon")
+		dropdown.warning:SetAnchor(RIGHT, dropdown:GetNamedChild("Dropdown"), LEFT, -5, 0)
+		dropdown.warning.tooltipText = warningText
+	end
+
+	ZO_OptionsWindow_InitializeControl(dropdown)
+
+	lastAddedControl[panelID] = dropdown
+
+	return dropdown
+end
+
+function lam:AddCheckbox(panelID, controlName, text, tooltip, getFunc, setFunc, warning, warningText)
+	local checkbox = wm:CreateControlFromVirtual(controlName, optionsWindow, "ZO_Options_Checkbox")
+	checkbox:SetAnchor(TOPLEFT, lastAddedControl[panelID], BOTTOMLEFT, 0, 6)
+	checkbox.controlType = OPTIONS_CHECKBOX
+	checkbox.system = SETTING_TYPE_UI
+	checkbox.settingId = _G[strformat("SETTING_%s", controlName)]
+	checkbox.panel = panelID
+	checkbox.text = text
+	checkbox.tooltipText = tooltip
+
+	local checkboxButton = checkbox:GetNamedChild("Checkbox")
+
+	ZO_PreHookHandler(checkbox, "OnShow", function()
+			checkboxButton:SetState(getFunc() and 1 or 0)
+			checkboxButton:toggleFunction(getFunc())
+		end)
+	ZO_PreHookHandler(checkboxButton, "OnClicked", function() setFunc(not getFunc()) end)
+
+	if warning then
+		checkbox.warning = wm:CreateControlFromVirtual(controlName.."WarningIcon", checkbox, "ZO_Options_WarningIcon")
+		checkbox.warning:SetAnchor(RIGHT, checkboxButton, LEFT, -5, 0)
+		checkbox.warning.tooltipText = warningText
+	end
+
+	ZO_OptionsWindow_InitializeControl(checkbox)
+
+	lastAddedControl[panelID] = checkbox
+
+	return checkbox
+end
+
+function lam:AddColorPicker(panelID, controlName, text, tooltip, getFunc, setFunc, warning, warningText)
+	local colorpicker = wm:CreateTopLevelWindow(controlName)
+	colorpicker:SetParent(optionsWindow)
+	colorpicker:SetAnchor(TOPLEFT, lastAddedControl[panelID], BOTTOMLEFT, 0, 10)
+	colorpicker:SetResizeToFitDescendents(true)
+	colorpicker:SetWidth(510)
+	colorpicker:SetMouseEnabled(true)
+
+	colorpicker.label = wm:CreateControl(controlName.."Label", colorpicker, CT_LABEL)
+	local label = colorpicker.label
+	label:SetDimensions(300, 26)
+	label:SetAnchor(TOPLEFT)
+	label:SetFont("ZoFontWinH4")
+	label:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS)
+	label:SetText(text)
+
+	colorpicker.color = wm:CreateControl(controlName.."Color", colorpicker, CT_CONTROL)
+	local color = colorpicker.color
+	color:SetDimensions(200,26)
+	color:SetAnchor(RIGHT)
+
+	color.thumb = wm:CreateControl(controlName.."ColorThumb", color, CT_TEXTURE)
+	local thumb = color.thumb
+	thumb:SetDimensions(36, 18)
+	thumb:SetAnchor(LEFT, color, LEFT, 4, 0)
+	local r, g, b, a = getFunc()
+	thumb:SetColor(r, g, b, a or 1)
+
+	color.border = wm:CreateControl(controlName.."ColorBorder", 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 ColorPickerCallback
+	if not ColorPickerCallback then
+		ColorPickerCallback = function(r, g, b, a)
+			thumb:SetColor(r, g, b, a or 1)
+			setFunc(r, g, b, a)
+		end
+	end
+
+	colorpicker.controlType = OPTIONS_CUSTOM
+	colorpicker.customSetupFunction = function(colorpicker)
+			colorpicker:SetHandler("OnMouseUp", function(self, btn, upInside)
+					if upInside then
+						local r, g, b, a = getFunc()
+						COLOR_PICKER:Show(ColorPickerCallback, r, g, b, a, text)
+					end
+				end)
+		end
+	colorpicker.panel = panelID
+	colorpicker.tooltipText = tooltip
+	colorpicker:SetHandler("OnMouseEnter", ZO_Options_OnMouseEnter)
+	colorpicker:SetHandler("OnMouseExit", ZO_Options_OnMouseExit)
+
+	if warning then
+		colorpicker.warning = wm:CreateControlFromVirtual(controlName.."WarningIcon", colorpicker, "ZO_Options_WarningIcon")
+		colorpicker.warning:SetAnchor(RIGHT, colorpicker:GetNamedChild("Color"), LEFT, -5, 0)
+		colorpicker.warning.tooltipText = warningText
+	end
+
+	ZO_OptionsWindow_InitializeControl(colorpicker)
+
+	lastAddedControl[panelID] = colorpicker
+
+	return colorpicker
+end
+
+function lam:AddEditBox(panelID, controlName, text, tooltip, isMultiLine, getFunc, setFunc, warning, warningText)
+	local editbox = wm:CreateTopLevelWindow(controlName)
+	editbox:SetParent(optionsWindow)
+	editbox:SetAnchor(TOPLEFT, lastAddedControl[panelID], BOTTOMLEFT, 0, 10)
+	editbox:SetResizeToFitDescendents(true)
+	editbox:SetWidth(510)
+	editbox:SetMouseEnabled(true)
+
+	editbox.label = wm:CreateControl(controlName.."Label", editbox, CT_LABEL)
+	local label = editbox.label
+	label:SetDimensions(300, 26)
+	label:SetAnchor(TOPLEFT)
+	label:SetFont("ZoFontWinH4")
+	label:SetWrapMode(TEXT_WRAP_MODE_ELLIPSIS)
+	label:SetText(text)
+
+	editbox.bg = wm:CreateControlFromVirtual(controlName.."BG", editbox, "ZO_EditBackdrop")
+	local bg = editbox.bg
+	bg:SetDimensions(200,isMultiLine and 100 or 24)
+	bg:SetAnchor(RIGHT)
+	editbox.edit = wm:CreateControlFromVirtual(controlName.."Edit", bg, isMultiLine and "ZO_DefaultEditMultiLineForBackdrop" or "ZO_DefaultEditForBackdrop")
+	editbox.edit:SetText(getFunc())
+	editbox.edit:SetHandler("OnFocusLost", function(self) setFunc(self:GetText()) end)
+
+
+	editbox.panel = panelID
+	editbox.tooltipText = tooltip
+	editbox:SetHandler("OnMouseEnter", ZO_Options_OnMouseEnter)
+	editbox:SetHandler("OnMouseExit", ZO_Options_OnMouseExit)
+
+	if warning then
+		editbox.warning = wm:CreateControlFromVirtual(controlName.."WarningIcon", editbox, "ZO_Options_WarningIcon")
+		editbox.warning:SetAnchor(TOPRIGHT, editbox:GetNamedChild("BG"), TOPLEFT, -5, 0)
+		editbox.warning.tooltipText = warningText
+	end
+
+	ZO_OptionsWindow_InitializeControl(editbox)
+
+	lastAddedControl[panelID] = editbox
+
+	return editbox
+end
+
+function lam:AddButton(panelID, controlName, text, tooltip, onClick, warning, warningText)
+	local button = wm:CreateTopLevelWindow(controlName)
+	button:SetParent(optionsWindow)
+	button:SetAnchor(TOPLEFT, lastAddedControl[panelID], BOTTOMLEFT, 0, 6)
+	button:SetDimensions(510, 28)
+	button:SetMouseEnabled(true)
+
+	button.btn = wm:CreateControlFromVirtual(controlName.."Button", button, "ZO_DefaultButton")
+	local btn = button.btn
+	btn:SetAnchor(TOPRIGHT)
+	btn:SetWidth(200)
+	btn:SetText(text)
+	btn:SetHandler("OnClicked", onClick)
+
+	button.controlType = OPTIONS_CUSTOM
+	button.customSetupFunction = function() end	--move handlers into this function? (since I created a function...)
+	button.panel = panelID
+	btn.tooltipText = tooltip
+	btn:SetHandler("OnMouseEnter", ZO_Options_OnMouseEnter)
+	btn:SetHandler("OnMouseExit", ZO_Options_OnMouseExit)
+
+	if warning then
+		button.warning = wm:CreateControlFromVirtual(controlName.."WarningIcon", button, "ZO_Options_WarningIcon")
+		button.warning:SetAnchor(RIGHT, btn, LEFT, -5, 0)
+		button.warning.tooltipText = warningText
+	end
+
+	ZO_OptionsWindow_InitializeControl(button)
+
+	lastAddedControl[panelID] = button
+
+	return button
+end
+
+function lam:AddDescription(panelID, controlName, text, titleText)
+	local textBox = wm:CreateTopLevelWindow(controlName)
+	textBox:SetParent(optionsWindow)
+	textBox:SetAnchor(TOPLEFT, lastAddedControl[panelID], BOTTOMLEFT, 0, 10)
+	textBox:SetResizeToFitDescendents(true)
+	textBox:SetWidth(510)
+
+	if titleText then
+		textBox.title = wm:CreateControl(controlName.."Title", textBox, CT_LABEL)
+		local title = textBox.title
+		title:SetWidth(510)
+		title:SetAnchor(TOPLEFT, textBox, TOPLEFT)
+		title:SetFont("ZoFontWinH4")
+		title:SetText(titleText)
+	end
+
+	textBox.desc = wm:CreateControl(controlName.."Text", textBox, CT_LABEL)
+	local desc = textBox.desc
+	desc:SetWidth(510)
+	if titleText then
+		desc:SetAnchor(TOPLEFT, textBox.title, BOTTOMLEFT)
+	else
+		desc:SetAnchor(TOPLEFT)
+	end
+	desc:SetVerticalAlignment(TEXT_ALIGN_TOP)
+	desc:SetFont("ZoFontGame")
+	desc:SetText(text)
+
+	textBox.controlType = OPTIONS_CUSTOM
+	textBox.panel = panelID
+
+	ZO_OptionsWindow_InitializeControl(textBox)
+
+	lastAddedControl[panelID] = textBox
+
+	return textBox
+end
+
+
+--test controls & examples--
+--[[local controlPanelID = lam:CreateControlPanel("ZAM_ADDON_OPTIONS", "ZAM Addons")
+lam:AddHeader(controlPanelID, "ZAM_Addons_TESTADDON", "TEST ADDON")
+lam:AddDescription(controlPanelID, "ZAM_Addons_TESTDESC", "This is a test description.", "Header")
+lam:AddSlider(controlPanelID, "ZAM_TESTSLIDER", "Test slider", "Adjust the slider.", 1, 10, 1, function() return 7 end, function(value) end, true, "needs UI reload")
+lam:AddDropdown(controlPanelID, "ZAM_TESTDROPDOWN", "Test Dropdown", "Pick something!", {"thing 1", "thing 2", "thing 3"}, function() return "thing 2" end, function(self,valueString) print(valueString) end)
+local checkbox1 = true
+lam:AddCheckbox(controlPanelID, "ZAM_TESTCHECKBOX", "Test Checkbox", "On or off?", function() return checkbox1 end, function(value) checkbox1 = not checkbox1 print(value, checkbox1) end)
+lam:AddColorPicker(controlPanelID, "ZAM_TESTCOLORPICKER", "Test color picker", "What's your favorite color?", function() return 1, 1, 0 end, function(r,g,b) print(r,g,b) end)
+lam:AddEditBox(controlPanelID, "ZAM_TESTEDITBOX", "Test Edit Box", "This is a tooltip!", false, function() return "hi" end, function(text) print(text) end)
+lam:AddHeader(controlPanelID, "ZAM_Addons_TESTADDON2", "TEST ADDON 2")
+local checkbox2 = false
+lam:AddCheckbox(controlPanelID, "ZAM_TESTCHECKBOX2", "Test Checkbox 2", "On or off?", function() return checkbox2 end, function(value) checkbox2 = not checkbox2 print(value, checkbox2) end)
+lam:AddButton(controlPanelID, "ZAM_TESTBUTTON", "Test Button", "Click me", function() print("hi") end, true, "oh noez!")
+lam:AddEditBox(controlPanelID, "ZAM_TESTEDITBOX2", "Test Edit Box 2", "This is a tooltip!", true, function() return "hi" end, function(text) print(text) end, true, "warning text")
+lam:AddSlider(controlPanelID, "ZAM_TESTSLIDER2", "Test slider 2", "Adjust the slider.", 50, 100, 10, function() return 80 end, function(value) end)
+lam:AddDropdown(controlPanelID, "ZAM_TESTDROPDOWN2", "Test Dropdown 2", "Pick something!", {"thing 4", "thing 5", "thing 6"}, function() return "thing 6" end, function(self,valueString) print(valueString) end)
+]]--
\ No newline at end of file
diff --git a/libs/LibStub/LibStub.lua b/libs/LibStub/LibStub.lua
new file mode 100644
index 0000000..bfd96df
--- /dev/null
+++ b/libs/LibStub/LibStub.lua
@@ -0,0 +1,34 @@
+-- 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", 1  -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
+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)")
+		minor = assert(tonumber(zo_strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
+
+		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(("Cannot find a library instance of %q."):strformat(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