Quantcast
--[[
Name: Sink-2.0
Revision: $Rev: 113 $
Author(s): Funkydude
Description: Library that handles chat output.
Dependencies: LibStub, SharedMedia-3.0 (optional)
License: CC-BY-NC-SA 3.0
]]

--[[
Copyright (C) 2008-2015
For the attribution bit of the license, as long as you distribute the library unmodified,
no attribution is required.
If you derive from the library or change it in any way, you are required to contact the author(s).
]]

-----------------------------------------------------------------------
-- Sink-2.0

local SINK20 = "LibSink-2.0"
local SINK20_MINOR = 90103

local sink = LibStub:NewLibrary(SINK20, SINK20_MINOR)
if not sink then return end

-- Start upgrade
sink.storageForAddon = sink.storageForAddon or {}
sink.override = sink.override or {}
sink.msbt_registered_fonts = sink.msbt_registered_fonts or {}
sink.registeredScrollAreaFunctions = sink.registeredScrollAreaFunctions or {}
sink.handlers = sink.handlers or {}

sink.stickyAddons = sink.stickyAddons or {
	Blizzard = true,
	MikSBT = true,
	SCT = true,
}

-- Upgrade complete

local _G = _G
local format, gsub, wipe, next, select = string.format, string.gsub, wipe, next, select
local IsInRaid, IsInGroup, SendChatMessage = IsInRaid, IsInGroup, SendChatMessage

local L = {}
L.DEFAULT = _G.DEFAULT -- "Default"
L.CHAT = _G.CHAT -- "Chat"
L.NONE = _G.NONE -- "None"
L.RW = _G.RAID_WARNING -- "Raid Warning"
L.BLIZZARD = _G.FLOATING_COMBATTEXT_LABEL -- "Floating Combat Text"
L.CHANNEL = _G.CHANNEL -- "Channel"

L.DEFAULT_DESC = "Route output from this addon through the first available handler, preferring scrolling combat text addons if available."
L.ROUTE = "Route output from this addon through %s."
L.UIERROR = "Blizzard Error Frame"
L.OUTPUT = "Output"
L.OUTPUT_DESC = "Where to route the output from this addon."
L.SCROLL = "Sub section"
L.SCROLL_DESC = "Set the sub section where messages should appear.\n\nOnly available for some outputs."
L.STICKY = "Sticky"
L.STICKY_DESC = "Set messages from this addon to appear as sticky.\n\nOnly available for some outputs."
L.NONE_DESC = "Hide all messages from this addon."
L.NOTINCHANNEL = "LibSink: %s (Sending to channel '%s' failed, you're not in it)"

do
	-- These localization strings are translated on WoWAce: http://www.wowace.com/addons/libsink-2-0/localization/
	local l = GetLocale()
	if l == "koKR" then
L["DEFAULT_DESC"] = "처음으로 사용 가능한 트레이너를 통해 이 애드온으로부터 출력을 보냅니다." -- Needs review
L["NONE_DESC"] = "이 애드온의 모든 메시지를 숨김니다." -- Needs review
L["NOTINCHANNEL"] = "LibSink: %s (%s 채널로 전송 실패)" -- Needs review
L["OUTPUT"] = "출력" -- Needs review
L["OUTPUT_DESC"] = "어디에 이 애드온의 메시지를 출력할지 선택합니다." -- Needs review
L["ROUTE"] = "%s|1을;를; 통해 이 애드온의 메시지를 출력합니다." -- Needs review
L["SCROLL"] = "스크롤 영역" -- Needs review
L["SCROLL_DESC"] = "메시지를 출력할 스크룰 영역을 설정합니다." -- Needs review
L["STICKY"] = "점착" -- Needs review
L["STICKY_DESC"] = "달라붙는 것처럼 보일 이 애드온의 메시지를 설정합니다." -- Needs review
L["UIERROR"] = "블리자드 오류 창" -- Needs review

	elseif l == "frFR" then
L["DEFAULT_DESC"] = "Dirige la sortie de cet addon vers le premier gestionnaire disponible, de préférence les addons de texte de combat flottant si disponibles." -- Needs review
L["NONE_DESC"] = "Cache tous les messages de cet addon." -- Needs review
L["NOTINCHANNEL"] = "LibSink : %s (l'envoi vers le canal '%s' a échoué, car vous n'êtes pas dessus)" -- Needs review
L["OUTPUT"] = "Sortie" -- Needs review
L["OUTPUT_DESC"] = "Vers où diriger la sortie de cet addon." -- Needs review
L["ROUTE"] = "Dirige la sortie de cet addon vers %s" -- Needs review
L["SCROLL"] = "Sous-section" -- Needs review
L["SCROLL_DESC"] = [=[Définit la sous-section dans laquelle les messages doivent apparaître.

Disponible uniquement pour certaines sorties.]=] -- Needs review
L["STICKY"] = "Épinglé" -- Needs review
L["STICKY_DESC"] = [=[Fait apparaître les messages de cet addon comme épinglés.

Disponible uniquement pour certaines sorties.]=] -- Needs review
L["UIERROR"] = "Cadre des erreurs de Blizzard" -- Needs review

	elseif l == "deDE" then
L["DEFAULT_DESC"] = "Die Ausgaben dieses Addons werden durch den ersten verfügbaren Handler geleitet, es werden Schwebender-Kampftext-Addons bevorzugt, wenn diese vorhanden sind."
L["NONE_DESC"] = "Alle Meldungen dieses Addons verstecken."
L["NOTINCHANNEL"] = "LibSink : %s (Senden auf Channel \"%s\" gescheitert, da du nicht in ihm bist)"
L["OUTPUT"] = "Ausgabe"
L["OUTPUT_DESC"] = "Wohin die Ausgaben dieses Addons geleitet werden sollen."
L["ROUTE"] = "Die Ausgaben dieses Addons werden durch %s geleitet."
L["SCROLL"] = "Unterabschnitt"
L["SCROLL_DESC"] = [=[Stelle den Unterabschnitt ein, in dem die Nachrichten erscheinen sollen.

Dies ist nur für manche Ausgaben verfügbar.]=]
L["STICKY"] = "Fixiert"
L["STICKY_DESC"] = [=[Lässt Nachrichten dieses Addons als fixiert erscheinen, das heißt, dass die Ausgaben an einer festen Position auf dem Bildschirm erscheinen und dort wieder verschwinden.

Dies ist nur für manche Ausgaben verfügbar.]=]
L["UIERROR"] = "Blizzards Fehlerfenster"

	elseif l == "zhCN" then
L["DEFAULT_DESC"] = "从这个插件路由输出到第一个可用的处理程序,倾向于可用的滚动战斗文本插件。"
L["NONE_DESC"] = "隐藏此插件全部消息。"
L["NOTINCHANNEL"] = "LibSink:%s(发送到频道“%s”失败,不在此频道)"
L["OUTPUT"] = "输出"
L["OUTPUT_DESC"] = "从此插件路由输出。"
L["ROUTE"] = "从此插件通过%s路由输出。"
L["SCROLL"] = "子区段"
L["SCROLL_DESC"] = [=[设置子区段消息出现状态。

只在一些输出可用。]=]
L["STICKY"] = "固定"
L["STICKY_DESC"] = [=[设置信息从此插件出现状态为固定。

只在一些输出可用。]=]
L["UIERROR"] = "暴雪错误框体"

	elseif l == "zhTW" then
L["DEFAULT_DESC"] = "從這個插件路由輸出到第一個可用的處理程式,傾向於可用的滾動戰鬥文本插件。"
L["NONE_DESC"] = "隱藏此插件全部訊息。"
L["NOTINCHANNEL"] = "LibSink:%s(發送到頻道“%s”失敗,不在此頻道)"
L["OUTPUT"] = "輸出"
L["OUTPUT_DESC"] = "從此插件路由輸出。"
L["ROUTE"] = "從此插件通過%s路由輸出。"
L["SCROLL"] = "子區段"
L["SCROLL_DESC"] = [=[設置子區段訊息出現狀態。

只在一些輸出可用。 ]=]
L["STICKY"] = "固定"
L["STICKY_DESC"] = [=[設置訊息從此插件出現狀態為固定。

只在一些輸出可用。 ]=]
L["UIERROR"] = "暴雪錯誤框體"

	elseif l == "ruRU" then
L["DEFAULT_DESC"] = "Направлять вывод из этого аддона через первый доступный обработчик, предпочитая аддоны прокрутки журнала боя если они доступны."
L["NONE_DESC"] = "Скрыть все сообщения этого аддона"
L["NOTINCHANNEL"] = "LibSink: %s (Отправка в канал '%s' неудачна, вы не в нем)"
L["OUTPUT"] = "Вывод"
L["OUTPUT_DESC"] = "Куда направлять вывод из этого аддона."
L["ROUTE"] = "Направлять вывод из этого аддона через %s."
L["SCROLL"] = "Подразделы"
L["SCROLL_DESC"] = [=[Установить подраздел, где должны появляться сообщения.

Доступно только для некоторых выводов.]=]
L["STICKY"] = "Прикрепление"
L["STICKY_DESC"] = [=[Прикреплять сообщения из этого аддона

Доступно только для некоторых выводов.]=]
L["UIERROR"] = "Фрейм ошибок Blizzard."

	elseif l == "esES" then
L["DEFAULT_DESC"] = "Ruta de salida de este addon mediante el primer controlador disponible, prefiriendo el desplazamiento de texto de combate si está disponible." -- Needs review
L["NONE_DESC"] = "Oculta todos los mensajes de este addon." -- Needs review
L["NOTINCHANNEL"] = "LibSink: %s (Falló al enviar al canal '%s', no estás en el)" -- Needs review
L["OUTPUT"] = "Salida" -- Needs review
L["OUTPUT_DESC"] = "Donde se ajustará la ruta de salida de este addon." -- Needs review
L["ROUTE"] = "Ruta de salida de este addon mediante %s." -- Needs review
L["SCROLL"] = "Sub sección." -- Needs review
L["SCROLL_DESC"] = [=[Ajusta la sub sección donde los mensajes deben aparecer.

Disponible sólo para algunas salidas.]=] -- Needs review
L["STICKY"] = "Chincheta" -- Needs review
L["STICKY_DESC"] = [=[Ajusta los mensajes de este addon para que aparezcan como chincheta.

Disponible sólo para algunas salidas.]=] -- Needs review
L["UIERROR"] = "Marco de Errores de Blizzard" -- Needs review

	elseif l == "esMX" then
L["DEFAULT_DESC"] = "Ruta de salida de este addon mediante el primer controlador disponible, prefiriendo el desplazamiento de texto de combate si está disponible." -- Needs review
L["NONE_DESC"] = "Oculta todos los mensajes de este addon." -- Needs review
L["NOTINCHANNEL"] = "LibSink: %s (Falló al enviar al canal '%s', no estás en el)" -- Needs review
L["OUTPUT"] = "Salida" -- Needs review
L["OUTPUT_DESC"] = "Donde se ajustará la ruta de salida de este addon." -- Needs review
L["ROUTE"] = "Ruta de salida de este addon mediante %s." -- Needs review
L["SCROLL"] = "Sub sección." -- Needs review
L["SCROLL_DESC"] = [=[Ajusta la sub sección donde los mensajes deben aparecer.

Disponible sólo para algunas salidas.]=] -- Needs review
L["STICKY"] = "Chincheta" -- Needs review
L["STICKY_DESC"] = [=[Ajusta los mensajes de este addon para que aparezcan como chincheta.

Disponible sólo para algunas salidas.]=] -- Needs review
L["UIERROR"] = "Marco de Errores de Blizzard" -- Needs review

	elseif l == "ptBR" then

	elseif l == "itIT" then
L["DEFAULT_DESC"] = "Indirizza l'uscita da questo addon attraverso il primo metodo di uscita disponibile, preferibilmente un addon visivo a schermo se disponibile."
L["NONE_DESC"] = "Nasconti tutti i messaggi per questo addon."
L["NOTINCHANNEL"] = "LibSink: %s (Invio al canale '%s' non riuscito, non sei dentro)"
L["OUTPUT"] = "Uscita"
L["OUTPUT_DESC"] = "Dove indirizzare l'uscita da questo addon."
L["ROUTE"] = "Indirizza l'uscita da questo addon attraverso %s."
L["SCROLL"] = "Sotto sezione"
L["SCROLL_DESC"] = [=[Imposta la sotto sezione in cui i messaggi devono apparire.

Disponibile solo per alcune uscite.]=]
L["STICKY"] = "Importante"
L["STICKY_DESC"] = [=[Imposta i messaggi di questo addon di apparire come importanti.

Disponibile solo per alcune uscite.]=]
L["UIERROR"] = "Frame Errore Blizzard"

	end
end

local SML = LibStub("LibSharedMedia-3.0", true)

local sct_color = {}
local function sct(addon, text, r, g, b, font, size, outline, sticky, _, icon)
	sct_color.r, sct_color.g, sct_color.b = r, g, b
	local loc = sink.storageForAddon[addon] and sink.storageForAddon[addon].sink20ScrollArea or "Messages"
	local location = (loc == "Outgoing" and SCT.FRAME1) or (loc == "Incoming" and SCT.FRAME2) or SCT.MSG
	local s = sink.storageForAddon[addon] and sink.storageForAddon[addon].sink20Sticky or sticky
	SCT:DisplayCustomEvent(text, sct_color, s, location, nil, icon)
end

local msbt_outlines = {["NORMAL"] = 1, ["OUTLINE"] = 2, ["THICKOUTLINE"] = 3}
local function msbt(addon, text, r, g, b, font, size, outline, sticky, _, icon)
	if font and SML and not sink.msbt_registered_fonts[font] then
		MikSBT.RegisterFont(font, SML:Fetch("font", font))
		sink.msbt_registered_fonts[font] = true
	end
	local location = sink.storageForAddon[addon] and sink.storageForAddon[addon].sink20ScrollArea or MikSBT.DISPLAYTYPE_NOTIFICATION
	local s = sink.storageForAddon[addon] and sink.storageForAddon[addon].sink20Sticky or sticky
	MikSBT.DisplayMessage(text, location, s, r * 255, g * 255, b * 255, size, font, msbt_outlines[outline], icon)
end

local function blizzard(addon, text, r, g, b, font, size, outline, sticky, _, icon)
	if icon then text = "\124T"..icon..":15:15:0:0:64:64:4:60:4:60\124t "..text end
	if SHOW_COMBAT_TEXT == "1" then
		local s = sink.storageForAddon[addon] and sink.storageForAddon[addon].sink20Sticky or sticky
		if not CombatText_AddMessage then
			UIParentLoadAddOn("Blizzard_CombatText")
		end
		CombatText_AddMessage(text, CombatText_StandardScroll, r, g, b, s and "crit" or nil, false)
	else
		UIErrorsFrame:AddMessage(text, r, g, b, 1.0)
	end
end

sink.channelMapping = sink.channelMapping or {
	[_G.SAY] = "SAY",
	[_G.PARTY] = "PARTY",
	[_G.INSTANCE_CHAT] = "INSTANCE_CHAT",
	[_G.GUILD_CHAT] = "GUILD",
	[_G.OFFICER_CHAT] = "OFFICER",
	[_G.YELL] = "YELL",
	[_G.RAID] = "RAID",
	[_G.RAID_WARNING] = "RAID_WARNING",
	[_G.GROUP] = "GROUP",
}
sink.channelMappingIds = sink.channelMappingIds or {}
sink.frame = sink.frame or CreateFrame("Frame")
sink.frame:UnregisterAllEvents()
sink.frame:RegisterEvent("CHANNEL_UI_UPDATE")
sink.frame:RegisterEvent("PLAYER_ENTERING_WORLD")
do
	local function loop(...)
		wipe(sink.channelMappingIds)
		for i = 1, select("#", ...), 2 do
			local id, name = select(i, ...)
			sink.channelMappingIds[name] = id
		end
		for k, v in next, sink.channelMapping do
			if v == "CHANNEL" and not sink.channelMappingIds[k] then
				sink.channelMapping[k] = nil
			end
		end
		for k in next, sink.channelMappingIds do sink.channelMapping[k] = "CHANNEL" end
	end
	local function rescanChannels() loop(GetChannelList()) end
	sink.frame:SetScript("OnEvent", rescanChannels)
	rescanChannels()
end

local function color_strip(a, b, c)
	if b:sub(1,2) == "|H" then
		return a..b..c
	else
		return b
	end
end

local function channel(addon, text)
	-- Sanitize the text, remove all color codes & icons
	text = gsub(text, "(|c%x%x%x%x%x%x%x%x)(.-)(|r)", color_strip)
	text = gsub(text, "|T.-|t", "")
	local loc = sink.storageForAddon[addon] and sink.storageForAddon[addon].sink20ScrollArea
	local chan = sink.channelMapping[loc]
	if chan == "GROUP" then
		chan = (IsInGroup(2) and "INSTANCE_CHAT") or (IsInRaid() and "RAID") or (IsInGroup() and "PARTY") or "SAY"
	elseif chan == "CHANNEL" then
		local id, name = GetChannelName(sink.channelMappingIds[loc])
		if name then
			SendChatMessage(text, "CHANNEL", nil, id)
		else
			print(format(L.NOTINCHANNEL, text, loc))
		end
		return
	end
	SendChatMessage(text, chan or "SAY")
end

-- |TTexturePath:size1:size2:xoffset:yoffset:dimx:dimy:coordx1:coordx2:coordy1:coordy2:red:green:blue|t
local function chat(addon, text, r, g, b, _, _, _, _, _, icon)
	if icon then text = "\124T"..icon..":15:15:0:0:64:64:4:60:4:60\124t"..text end
	DEFAULT_CHAT_FRAME:AddMessage(text, r, g, b)
end

local function uierror(addon, text, r, g, b, _, _, _, _, _, icon)
	if icon then text = "\124T"..icon..":15:15:0:0:64:64:4:60:4:60\124t "..text end
	UIErrorsFrame:AddMessage(text, r, g, b, 1.0)
end

local rw
do
	local white = {r = 1, g = 1, b = 1}
	function rw(addon, text, r, g, b, _, _, _, _, _, icon)
		if r or g or b then
			text = format("\124cff%02x%02x%02x%s\124r", (r or 0) * 255, (g or 0) * 255, (b or 0) * 255, text)
		end
		if icon then text = "\124T"..icon..":15:15:0:0:64:64:4:60:4:60\124t "..text end
		RaidNotice_AddMessage(RaidWarningFrame, text, white)
	end
end

local function noop() --[[ noop! ]] end

local handlerPriority = { "SCT", "MikSBT" }
local customHandlersEnabled = {
	SCT = function()
		return _G.SCT and _G.SCT:IsEnabled()
	end,
	MikSBT = function()
		return _G.MikSBT and not _G.MikSBT.IsModDisabled()
	end,
}

local currentHandler = nil
local function getPrioritizedSink()
	if currentHandler then
		local check = customHandlersEnabled[currentHandler]
		if check and check() then
			return sink.handlers[currentHandler]
		end
	end
	for i, v in next, handlerPriority do
		local check = customHandlersEnabled[v]
		if check and check() then
			currentHandler = v
			return sink.handlers[v]
		end
	end
	if SHOW_COMBAT_TEXT and tostring(SHOW_COMBAT_TEXT) ~= "0" then
		return blizzard
	end
	return chat
end

local function pour(addon, text, r, g, b, ...)
	local func = sink.override and sink.handlers[sink.override] or nil
	if not func and sink.storageForAddon[addon] and sink.storageForAddon[addon].sink20OutputSink then
		local h = sink.storageForAddon[addon].sink20OutputSink
		func = sink.handlers[h]
		-- If this sink is not available now, find one manually.
		if customHandlersEnabled[h] and not customHandlersEnabled[h]() then
			func = nil
		end
	end
	if not func then
		func = getPrioritizedSink()
	end
	if not func then func = chat end
	func(addon, text, r or 1, g or 1, b or 1, ...)
end

function sink:Pour(textOrAddon, ...)
	local t = type(textOrAddon)
	if t == "string" then
		pour(self, textOrAddon, ...)
	elseif t == "number" then
		pour(self, tostring(textOrAddon), ...)
	elseif t == "table" then
		pour(textOrAddon, ...)
	else
		error("Invalid argument 2 to :Pour, must be either a string or a table.")
	end
end

local sinks
do
	-- Maybe we want to hide them instead of disable
	local function shouldDisableSCT()
		return not _G.SCT
	end
	local function shouldDisableMSBT()
		return not _G.MikSBT
	end
	local function shouldDisableFCT()
		return not SHOW_COMBAT_TEXT or tostring(SHOW_COMBAT_TEXT) == "0"
	end

	local sctFrames = {"Incoming", "Outgoing", "Messages"}
	local msbtFrames = nil
	local function getScrollAreasForAddon(addon)
		if type(addon) ~= "string" then return nil end
		if addon == "MikSBT" then
			if not msbtFrames then
				msbtFrames = {}
				for key, name in MikSBT.IterateScrollAreas() do
					msbtFrames[#msbtFrames+1] = name
				end
			end
			return msbtFrames
		elseif addon == "SCT" then
			return sctFrames
		elseif addon == "Channel" then
			local tmp = {}
			for k in next, sink.channelMapping do
				tmp[#tmp + 1] = k
			end
			return tmp
		elseif sink.registeredScrollAreaFunctions[addon] then
			return sink.registeredScrollAreaFunctions[addon]()
		end
		return nil
	end

	local emptyTable, args, options = {}, {}, {}
	sinks = {
		Default = {L.DEFAULT, L.DEFAULT_DESC},
		SCT = {"Scrolling Combat Text (SCT)", nil, shouldDisableSCT},
		MikSBT = {"MikSBT", nil, shouldDisableMSBT},
		Blizzard = {L.BLIZZARD, nil, shouldDisableFCT},
		RaidWarning = {L.RW},
		ChatFrame = {L.CHAT},
		Channel = {L.CHANNEL},
		UIErrorsFrame = {L.UIERROR},
		None = {L.NONE, L.NONE_DESC}
	}

	local function getAce2SinkOptions(key, opts)
		local name, desc, hidden = unpack(opts)
		args["Ace2"][key] = {
			type = "toggle",
			name = name,
			desc = desc or format(L.ROUTE, name),
			isRadio = true,
			hidden = hidden
		}
	end

	function sink.GetSinkAce2OptionsDataTable(addon)
		options["Ace2"][addon] = options["Ace2"][addon] or {
			output = {
				type = "group",
				name = L.OUTPUT,
				desc = L.OUTPUT_DESC,
				pass = true,
				get = function(key)
					if not sink.storageForAddon[addon] then
						return "Default"
					end
					if tostring(key) == "nil" then
						-- Means AceConsole wants to list the output option,
						-- so we should show which sink is currently used.
						return sink.storageForAddon[addon].sink20OutputSink or L.DEFAULT
					end
					if key == "ScrollArea" then
						return sink.storageForAddon[addon].sink20ScrollArea
					elseif key == "Sticky" then
						return sink.storageForAddon[addon].sink20Sticky
					else
						if sink.storageForAddon[addon].sink20OutputSink == key then
							local sa = getScrollAreasForAddon(key)
							options["Ace2"][addon].output.args.ScrollArea.validate = sa or emptyTable
							options["Ace2"][addon].output.args.ScrollArea.disabled = not sa
							options["Ace2"][addon].output.args.Sticky.disabled = not sink.stickyAddons[key]
						end
						return sink.storageForAddon[addon].sink20OutputSink and sink.storageForAddon[addon].sink20OutputSink == key or nil
					end
				end,
				set = function(key, value)
					if not sink.storageForAddon[addon] then return end
					if key == "ScrollArea" then
						sink.storageForAddon[addon].sink20ScrollArea = value
					elseif key == "Sticky" then
						sink.storageForAddon[addon].sink20Sticky = value
					elseif value then
						local sa = getScrollAreasForAddon(key)
						options["Ace2"][addon].output.args.ScrollArea.validate = sa or emptyTable
						options["Ace2"][addon].output.args.ScrollArea.disabled = not sa
						options["Ace2"][addon].output.args.Sticky.disabled = not sink.stickyAddons[key]
						sink.storageForAddon[addon].sink20OutputSink = key
					end
				end,
				args = args["Ace2"],
				disabled = function()
					return (type(addon.IsActive) == "function" and not addon:IsActive()) or nil
				end
			}
		}
		return options["Ace2"][addon]
	end

	-- Ace3 options data table format
	local function getAce3SinkOptions(key, opts)
		local name, desc, hidden = unpack(opts)
		args["Ace3"][key] = {
			type = "toggle",
			name = name,
			desc = desc or format(L.ROUTE, name),
			hidden = hidden
		}
	end

	function sink.GetSinkAce3OptionsDataTable(addon)
		if not options["Ace3"][addon] then
			options["Ace3"][addon] = {
				type = "group",
				name = L.OUTPUT,
				desc = L.OUTPUT_DESC,
				args = args["Ace3"],
				get = function(info)
					local key = info[#info]
					if not sink.storageForAddon[addon] then
						return "Default"
					end
					if tostring(key) == "nil" then
						-- Means AceConsole wants to list the output option,
						-- so we should show which sink is currently used.
						return sink.storageForAddon[addon].sink20OutputSink or L.DEFAULT
					end
					if key == "ScrollArea" then
						return sink.storageForAddon[addon].sink20ScrollArea
					elseif key == "Sticky" then
						return sink.storageForAddon[addon].sink20Sticky
					else
						if sink.storageForAddon[addon].sink20OutputSink == key then
							local sa = getScrollAreasForAddon(key)
							if sa then
								local tbl = {}
								for i = 1, #sa do
									local n = sa[i]
									tbl[n] = n
								end
								options["Ace3"][addon].args.ScrollArea.values = tbl
								options["Ace3"][addon].args.ScrollArea.disabled = nil
							else
								options["Ace3"][addon].args.ScrollArea.disabled = true
								options["Ace3"][addon].args.ScrollArea.values = emptyTable
							end
							options["Ace3"][addon].args.Sticky.disabled = not sink.stickyAddons[key]
						end
						return sink.storageForAddon[addon].sink20OutputSink and sink.storageForAddon[addon].sink20OutputSink == key or nil
					end
				end,
				set = function(info, v)
					local key = info[#info]
					if not sink.storageForAddon[addon] then return end
					if key == "ScrollArea" then
						sink.storageForAddon[addon].sink20ScrollArea = v
					elseif key == "Sticky" then
						sink.storageForAddon[addon].sink20Sticky = v
					elseif v then
						local sa = getScrollAreasForAddon(key)
						if sa then
							local tbl = {}
							for i = 1, #sa do
								local n = sa[i]
								tbl[n] = n
							end
							options["Ace3"][addon].args.ScrollArea.values = tbl
							options["Ace3"][addon].args.ScrollArea.disabled = nil
						else
							options["Ace3"][addon].args.ScrollArea.disabled = true
							options["Ace3"][addon].args.ScrollArea.values = emptyTable
						end
						options["Ace3"][addon].args.Sticky.disabled = not sink.stickyAddons[key]
						sink.storageForAddon[addon].sink20OutputSink = key
					end
				end,
				disabled = function()
					return (type(addon.IsEnabled) == "function" and not addon:IsEnabled()) or nil
				end,
			}
		end
		return options["Ace3"][addon]
	end

	local sinkOptionGenerators = {
		["Ace2"] = getAce2SinkOptions,
		["Ace3"] = getAce3SinkOptions
	}
	for generatorName, generator in next, sinkOptionGenerators do
		options[generatorName] = options[generatorName] or {}
		args[generatorName] = args[generatorName] or {}
		for name, opts in next, sinks do
			generator(name, opts)
		end
	end

	args["Ace2"].ScrollArea = {
		type = "text",
		name = L.SCROLL,
		desc = L.SCROLL_DESC,
		validate = emptyTable,
		order = -1,
		disabled = true
	}
	args["Ace2"].Sticky = {
		type = "toggle",
		name = L.STICKY,
		desc = L.STICKY_DESC,
		validate = emptyTable,
		order = -2,
		disabled = true
	}

	args["Ace3"].ScrollArea = {
		type = "select",
		name = L.SCROLL,
		desc = L.SCROLL_DESC,
		values = emptyTable,
		order = -1,
		disabled = true
	}
	args["Ace3"].Sticky = {
		type = "toggle",
		name = L.STICKY,
		desc = L.STICKY_DESC,
		order = -2,
		disabled = true
	}

	function sink:RegisterSink(shortName, name, desc, func, scrollAreaFunc, hasSticky)
		assert(type(shortName) == "string")
		assert(type(name) == "string")
		assert(type(desc) == "string" or desc == nil)
		assert(type(func) == "function" or type(func) == "string")
		assert(type(scrollAreaFunc) == "function" or type(scrollAreaFunc) == "string" or scrollAreaFunc == nil)
		assert(type(hasSticky) == "boolean" or hasSticky == nil)

		if sinks[shortName] or sink.handlers[shortName] then
			error(format("There's already a sink by the short name %q.", shortName))
		end
		sinks[shortName] = {name, desc}
		-- Save it for library upgrades.
		if not sink.registeredSinks then sink.registeredSinks = {} end
		sink.registeredSinks[shortName] = sinks[shortName]

		if type(func) == "function" then
			sink.handlers[shortName] = func
		else
			sink.handlers[shortName] = function(...)
				self[func](self, ...)
			end
		end
		if type(scrollAreaFunc) == "function" then
			sink.registeredScrollAreaFunctions[shortName] = scrollAreaFunc
		elseif type(scrollAreaFunc) == "string" then
			sink.registeredScrollAreaFunctions[shortName] = function(...)
				return self[scrollAreaFunc](self, ...)
			end
		end
		sink.stickyAddons[shortName] = hasSticky and true or nil

		for k, v in next, sinkOptionGenerators do
			v(shortName, sinks[shortName])
		end
	end
end

function sink.SetSinkStorage(addon, storage)
	assert(type(addon) == "table")
	assert(type(storage) == "table", "Storage must be a table")
	sink.storageForAddon[addon] = storage
end

-- Sets a sink override for -all- addons, librarywide.
function sink:SetSinkOverride(override)
	assert(type(override) == "string" or override == nil)
	if override and not sink.handlers[override] then
		error("There's no %q sink.", override)
	end
	sink.override = override
end

-- Put this at the bottom, because we need the local functions to exist first.
local handlers = {
	SCT = sct,
	MikSBT = msbt,
	ChatFrame = chat,
	Channel = channel,
	UIErrorsFrame = uierror,
	Blizzard = blizzard,
	RaidWarning = rw,
	None = noop,
}
-- Overwrite any handler functions from the old library
for k, v in next, handlers do
	sink.handlers[k] = v
end

-----------------------------------------------------------------------
-- Embed handling

sink.embeds = sink.embeds or {}

local mixins = {
	"Pour", "RegisterSink", "SetSinkStorage",
	"GetSinkAce2OptionsDataTable", "GetSinkAce3OptionsDataTable"
}

function sink:Embed(target)
	sink.embeds[target] = true
	for _,v in next, mixins do
		target[v] = sink[v]
	end
	return target
end

for addon in next, sink.embeds do
	sink:Embed(addon)
end