/clearcasting.lua (fa1d1ddf2c60228a5fed0b39f47b9f7430b3c9fc) (40051 bytes) (mode 100644) (type blob)

--[[--
Clearcasting addon.

TODO Show tooltip hint on mouseover.
TODO Separate profiles for different character classes.
TODO Sort debuff by time remaining and importance instead of first any.

@script clearcasting
]]

local function debug(...)
	if true == ClearcastingDebugFlag then
		print('[Clearcasting]: ', ...)
	end
end

local function getIndicatorArtworkSize()
	return 24
end

local function getIndicatorFooterSize()
	return 16
end

local function getIndicatorPadding()
	return 4
end

local function findFirstFilterName(unitDesignation, filterDescriptor, eitherTargetNameOrId)
	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 32)

	assert (filterDescriptor ~= nil)
	assert ('string' == type(filterDescriptor))
	assert (string.len(filterDescriptor) >= 4)
	assert (string.len(filterDescriptor) <= 64)

	assert (eitherTargetNameOrId ~= nil)
	if 'string' == type(eitherTargetNameOrId) then
		local targetName = eitherTargetNameOrId
		assert (targetName ~= nil)
		assert ('string' == type(targetName))
		--[[ The shortest spell name in English is "Hex" ]]--
		assert (string.len(targetName) >= 2)
		assert (string.len(targetName) <= 256)
	elseif 'number' == type(eitherTargetNameOrId) then
		local targetId = eitherTargetNameOrId
		assert (targetId ~= nil)
		assert (targetId > 0)
	else
		return nil
		--[[error('illegal argument')]]--
	end

	local i = 0
	while (i < 64) do
		i = i + 1
		local name, rank, pictureFile, stackQuantity, category,
		duration, expirationInstance,
		caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, i, filterDescriptor)
		if not name then
			break
		end
		if eitherTargetNameOrId == name or eitherTargetNameOrId == id then
			return name, rank, pictureFile, stackQuantity, category,
			duration, expirationInstance,
			caster, stealableFlag, consolidateFlag, id
		end
	end

	return nil
end

local function findFirstFilterCategory(unitDesignation, filterDescriptor, targetCategory)
	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 32)

	assert (filterDescriptor ~= nil)
	assert ('string' == type(filterDescriptor))
	assert (string.len(filterDescriptor) >= 4)
	assert (string.len(filterDescriptor) <= 64)

	assert (targetCategory ~= nil)
	if 'string' == type(targetCategory) then
		assert (string.len(targetCategory) >= 2)
		assert (string.len(targetCategory) <= 64)
	else
		return nil
		--[[error('illegal argument')]]--
	end

	local i = 0
	while (i < 64) do
		i = i + 1
		local name, rank, pictureFile, stackQuantity, category,
		duration, expirationInstance,
		caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, i, filterDescriptor)
		if not name then
			break
		end
		if targetCategory == category then
			return name, rank, pictureFile, stackQuantity, category,
			duration, expirationInstance,
			caster, stealableFlag, consolidateFlag, id
		end
	end

	return nil
end

local function findAnyFilterName(unitDesignation, filterDescriptor, targetSet)
	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 32)

	assert (filterDescriptor ~= nil)
	assert ('string' == type(filterDescriptor))
	assert (string.len(filterDescriptor) >= 4)
	assert (string.len(filterDescriptor) <= 64)

	if 'table' == type(targetSet) then
		assert (targetSet ~= nil)
		assert (#targetSet >= 1)
	else
		return nil
	end

	local i = 0
	debug('#begin findAny')
	while (i < 64) do
		i = i + 1
		local name, rank, pictureFile, stackQuantity, category,
		duration, expirationInstance,
		caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, i, filterDescriptor)
		local j = 0
		while (j < #targetSet) do
			j = j + 1
			local eitherTargetNameOrId = targetSet[j]
			debug('expected: ', eitherTargetNameOrId, ', actual: ', name)
			if eitherTargetNameOrId == name or eitherTargetNameOrId == id then
				debug('return ', name)
				return name, rank, pictureFile, stackQuantity, category,
				duration, expirationInstance,
				caster, stealableFlag, consolidateFlag, id
			end
		end
	end
	debug('-end findAny')

	return nil
end

local function findAnyHarmful(unitDesignation, filterDescriptor, targetCategory)
	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 32)

	assert (filterDescriptor ~= nil)
	assert ('string' == type(filterDescriptor))
	assert (string.len(filterDescriptor) >= 4)
	assert (string.len(filterDescriptor) <= 64)

	assert (targetCategory ~= nil)
	if 'string' == type(targetCategory) then
		assert (string.len(targetCategory) >= 2)
		assert (string.len(targetCategory) <= 64)
		if 'HARMFUL' ~= targetCategory then
			return nil
		end
	else
		return nil
		--[[error('illegal argument')]]--
	end


	local i = 0
	while (i < 64) do
		i = i + 1
		local name, rank, pictureFile, stackQuantity, category,
		duration, expirationInstance,
		caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, i, filterDescriptor)
		if name and nil == category then
			return name, rank, pictureFile, stackQuantity, category,
			duration, expirationInstance,
			caster, stealableFlag, consolidateFlag, id
		end
	end

	return nil
end

local function applyBackground(f, pictureFile)
	assert (f ~= nil)

	f:SetBackdrop({bgFile = pictureFile,
	               edgeFile = "Interface\\AddOns\\clearcasting\\share\\2px_tooltip_border",
	               tile = false, tileSize = 24, edgeSize = 8,
	               insets = { left = 2, right = 2, top = 2, bottom = 2 }})

	--f:SetBackdropColor(0.5, 0.5, 0.5, 0.5)
end

local function applyBorder(f, category, caster)
	local r, g, b, a
	if 'Magic' == category then
		r = 0 / 255
		g = 153 / 255
		b = 255 / 255
		a = 255 / 255
	elseif 'Disease' == category then
		r = 204 / 255
		g = 255 / 255
		b = 0 / 255
		a = 255 / 255
	elseif 'Curse' == category then
		r = 255 / 255
		g = 51 / 255
		b = 255 / 255
		a = 255 / 255
	elseif 'Poison' == category then
		r = 0 / 255
		g = 204 / 255
		b = 51 / 255
		a = 255 / 255
	elseif 'player' == caster then
		r = 255 / 255
		g = 255 / 255
		b = 255 / 255
		a = 255 / 255
	else
		r = 255 / 255
		g = 51 / 255
		b = 0 / 255
		a = 255 / 255
	end
	f:SetBackdropBorderColor(r, g, b, a)
end

local function formatIndicatorText(duration, expirationInstance, stackQuantity)
	local now = GetTime()
	duration = duration or 0
	expirationInstance = expirationInstance or now
	local remainingDurationSec = expirationInstance - now

	local t
	if remainingDurationSec <= 0 or remainingDurationSec >= 60 or 0 == duration then
		t = nil
	else
		t = string.format("%.0f", remainingDurationSec)

		stackQuantity = stackQuantity or 0
		if stackQuantity >= 2 and stackQuantity <= 9 then
			t = tostring(math.ceil(stackQuantity)) .. '*' .. t
		end
	end

	return t
end

local function applyDuration(f, duration, expirationInstance, stackQuantity)
	assert (f ~= nil)

	local now = GetTime()
	duration = duration or 0
	expirationInstance = expirationInstance or now

	local t = formatIndicatorText(duration, expirationInstance, stackQuantity)

	local textHandle = f.text
	textHandle:SetText(t)
	f:Show()
	f.expirationInstance = expirationInstance
	f.duration = duration
	f.stackQuantity = stackQuantity
end

local function applyIndicatorUpdate(f)
	applyDuration(f, f.duration, f.expirationInstance, f.stackQuantity)
end

local function indicatorUpdateProcessor(rootFrame, elapsedSecs)
	assert (rootFrame ~= nil)
	assert (elapsedSecs ~= nil)
	assert (type(elapsedSecs) == 'number')
	elapsedSecs = math.min(math.max(0, elapsedSecs), 999)

	local duration = rootFrame.elapsedSecs
	if duration == nil then
		duration = 0.0
	elseif type(duration) ~= 'number' then
		duration = 0.0
	end
	duration = math.min(math.max(0, duration + elapsedSecs), 999)
	rootFrame.elapsedSecs = duration
	if duration >= 0.08 then
		applyIndicatorUpdate(rootFrame)
		rootFrame.elapsedSecs = 0.0
	end
end

local function attemptToApply(f, name, rank, pictureFile, stackQuantity, category,
                              duration, expirationInstance,
                              caster, stealableFlag, consolidateFlag, id)
	assert (f ~= nil)

	if not name then
		f.spellId = nil
		f.spellName = nil
		f:Hide()
		f:SetScript('OnUpdate', nil)
		return
	end

	assert (name ~= nil)
	assert ('string' == type(name))
	assert (string.len(name) >= 2)
	assert (string.len(name) <= 256)
	f.spellName = name

	assert (id ~= nil)
	assert ('number' == type(id))
	assert (id >= 1)
	f.spellId = math.floor(id)

	applyBackground(f, pictureFile)
	applyBorder(f, category, caster)
	applyDuration(f, duration, expirationInstance, stackQuantity)

	f:Show()
	f:SetScript('OnUpdate', indicatorUpdateProcessor)
end

local function indicatorEventProcessor(f)
	assert (f ~= nil)

	local unitDesignation = f.unit or 'player'
	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 32)

	local filterDescriptor = f.filter or 'HELPFUL'
	assert (filterDescriptor ~= nil)
	assert ('string' == type(filterDescriptor))
	assert (string.len(filterDescriptor) >= 4)
	assert (string.len(filterDescriptor) <= 64)

	local target = f.target
	assert (target ~= nil)

	local name, rank, pictureFile, stackQuantity, category,
	duration, expirationInstance,
	caster, stealableFlag, consolidateFlag, id

	name, rank, pictureFile, stackQuantity, category,
	duration, expirationInstance,
	caster, stealableFlag, consolidateFlag, id = findFirstFilterName(unitDesignation, filterDescriptor, target)

	if not name then
		name, rank, pictureFile, stackQuantity, category,
		duration, expirationInstance,
		caster, stealableFlag, consolidateFlag, id = findFirstFilterCategory(unitDesignation, filterDescriptor, target)
	end

	if not name then
		name, rank, pictureFile, stackQuantity, category,
		duration, expirationInstance,
		caster, stealableFlag, consolidateFlag, id = findAnyFilterName(unitDesignation, filterDescriptor, target)
	end

	if not name then
		name, rank, pictureFile, stackQuantity, category,
		duration, expirationInstance,
		caster, stealableFlag, consolidateFlag, id = findAnyHarmful(unitDesignation, filterDescriptor, target)
	end

	attemptToApply(f, name, rank, pictureFile, stackQuantity, category,
	               duration, expirationInstance,
	               caster, stealableFlag, consolidateFlag, id)
end

local function tooltipOverlayEventProcessor(tooltipOverlay)
	assert (tooltipOverlay ~= nil)

	GameTooltip:SetOwner(tooltipOverlay, 'ANCHOR_BOTTOMRIGHT')

	local button = tooltipOverlay:GetParent()
	assert (button ~= nil)
	GameTooltip:SetText('spell description could not be found')
	if button.unit and button.index and button.filter then
		GameTooltip:SetUnitAura(button.unit, button.index, button.filter);
	elseif button.spellId then
		local t = GetSpellLink(button.spellId)
		GameTooltip:SetHyperlink(t)
	end
end

local function createTooltipOverlay(indicator)
	assert (indicator ~= nil)

	local p = indicator:GetName() or 'Clearcasting'
	local n = p .. 'TooltipOverlay'
	local o = CreateFrame('FRAME', n, indicator)
	o:SetAllPoints()

	--[[ It is critical to call EnableMouse method on a tooltip overlay frame ]]--
	o:EnableMouse(true)

	o:SetScript('OnEnter', tooltipOverlayEventProcessor)
	o:SetScript('OnLeave', function() GameTooltip:Hide(); end)

	return o
end

local function createIndicator(parentFrame, target, unitDesignation, filterDescriptor)
	assert (parentFrame ~= nil)

	assert (target ~= nil)

	local maxColumnQuantity = 4
	local maxRowQuantity = 8

	local p = parentFrame:GetName() or 'Clearcasting'
	local siblingSet = {parentFrame:GetChildren()}
	local siblingQuantity = #siblingSet or 0
	assert (siblingQuantity >= 0)
	local maxIndicatorQuantity = math.min(64, maxRowQuantity * maxColumnQuantity)
	assert (siblingQuantity < maxIndicatorQuantity, 'too many indicators ' .. tostring(siblingQuantity))
	local i = siblingQuantity + 1
	local n = p .. 'SpellActivationOverlay' .. tostring(i)

	local f = CreateFrame('FRAME', n, parentFrame)
	local size = 24
	local padding = 4
	local paddedSize = size + padding
	local y = math.floor(siblingQuantity / maxColumnQuantity)
	local x = siblingQuantity - (maxColumnQuantity * y)
	f:SetSize(size, size)
	f:SetPoint('BOTTOMLEFT', x * paddedSize, y * (paddedSize + size * 2 / 3))

	local t = f:CreateFontString(n .. 'Text', 'OVERLAY')
	local fontObject = NumberFont_OutlineThick_Mono_Small
	assert (fontObject ~= nil)
	t:SetFontObject(fontObject)
	t:SetPoint('TOPRIGHT', f, 'TOPRIGHT', 6, -f:GetHeight())
	t:SetPoint('TOPLEFT', f, 'TOPLEFT', -6, -f:GetHeight())
	t:SetPoint('BOTTOMLEFT', f, 'BOTTOMLEFT', 0, -f:GetHeight() * 2 / 3)
	t:SetPoint('BOTTOMRIGHT', f, 'BOTTOMRIGHT', 0, -f:GetHeight() * 2 / 3)
	t:SetText('?')
	f.text = t

	local a = f:CreateTexture(n .. 'Background', 'ARTWORK')
	a:SetAllPoints()
	f.background = a

	f.unit = unitDesignation
	f.filter = string.upper(filterDescriptor or 'HELPFUL')
	f.target = target
	f.spellId = nil
	f.spellName = nil

	f.tooltipOverlay = createTooltipOverlay(f)

	f:SetScript('OnEvent', indicatorEventProcessor)
	f:SetScript('OnUpdate', indicatorUpdateProcessor)
	f:RegisterEvent('PLAYER_ENTERING_WORLD')
	f:RegisterEvent('PLAYER_FOCUS_CHANGED')
	f:RegisterEvent('PLAYER_TARGET_CHANGED')
	f:RegisterEvent('UNIT_AURA')

	return f
end

local function sectionEventProcessor(section)
	assert (section ~= nil)

	local t = {section:GetChildren()}
	if #t < 1 then
		return
	end

	local width = section:GetWidth()

	local rowHeight = 44
	local columnWidth = 28
	local columnQuantitiy = math.floor(width / columnWidth)

	local x = 0
	local y = 0


	local i = 0
	local j = math.min(math.max(0, #t), 64)
	while (i < j) do
		i = i + 1
		local f = t[i]
		assert (f ~= nil)
		indicatorEventProcessor(f)
		if 1 == f:IsShown() then
			f:SetPoint('BOTTOMLEFT', x * columnWidth, y * rowHeight)
			x = x + 1
			if x >= columnQuantitiy then
				x = 0
				y = y + 1
			end
		end
	end
end

local function createSection(name, parent, width, height)
	assert (name ~= nil)
	name = strtrim(name)
	assert ('string' == type(name))
	assert (string.len(name) >= 4)
	assert (string.len(name) <= 128)

	assert (parent ~= nil)

	assert (width ~= nil)
	assert ('number' == type(width))
	assert (width >= 12)
	assert (width <= 1024)

	assert (height ~= nil)
	assert ('number' == type(height))
	assert (height >= 12)
	assert (height <= 1024)

	local section = CreateFrame('FRAME', name, parent)
	section:SetSize(width, height)

	local b = section:CreateTexture(name .. 'Background', 'BACKGROUND')
	b:SetAllPoints()
        b:SetTexture(0.02, 0.02, 0.02, 0.12)
	section.background = b

	section:SetScript('OnEvent', sectionEventProcessor)
	section:RegisterEvent('PLAYER_FOCUS_CHANGED')
	section:RegisterEvent('PLAYER_TARGET_CHANGED')
	section:RegisterEvent('UNIT_AURA')
	section:RegisterEvent('UNIT_DEATH')

	assert (section ~= nil)
	return section
end

local function unpackAuraTable(auraTable)
	assert(auraTable ~= nil)
	assert("table" == type(auraTable))

	local spellName = auraTable[1]
	local spellRank = auraTable[2]
	local pictureFile = auraTable[3]
	local stackQuantity = auraTable[4]
	local category = auraTable[5]
	local durationSec = auraTable[6]
	local expirationInstance = auraTable[7]
	local casterUnitDesignation = auraTable[8]
	local stealableFlag = auraTable[9]
	local consolidationFlag = auraTable[10]
	local spellId = auraTable[11]

	return spellName, spellRank, pictureFile, stackQuantity, category, durationSec, expirationInstance,
	       casterUnitDesignation, stealableFlag, consolidationFlag, spellId
end

local function getAuraWeight(
	eitherAuraTableOrSpellName,
	spellRank,
	pictureFile,
	stackQuantity,
	category,
	durationSec,
	expirationInstance,
	casterUnitDesignation,
	stealableFlag,
	consolidationFlag,
	spellId)

	local spellName
	if "table" == type(eitherAuraTableOrSpellName) then
		local auraTable = eitherAuraTableOrSpellName

		spellName,
		spellRank,
		pictureFile,
		stackQuantity,
		category,
		durationSec,
		expirationInstance,
		casterUnitDesignation,
		stealableFlag,
		consolidationFlag,
		spellId = unpackAuraTable(auraTable)
	elseif "string" == type(eitherAuraTableOrSpellName) then
		spellName = eitherAuraTableOrSpellName
	else
		error("invalid argument")
	end

	durationSec = durationSec or 1
	durationSec = math.max(1, durationSec)
	local now = GetTime()
	expirationInstance = expirationInstance or now
	local weight = 0
	if "player" == casterUnitDesignation then
		weight = weight + 10000
	end

	local categoryMap = {["Magic"] = 4000, ["Poison"] = 3000, ["Disease"] = 2000, ["Curse"] = 1000}
	local categoryWeight = 0
	if category then
		categoryWeight = categoryMap[category] or 0
	end
	weight = weight + categoryWeight

	--[[ FIXME Sorting by remained duraiton does not work for all spells for some reason ]]--
	local durationRemainingSec = expirationInstance - now
	local durationElapsedSec = durationSec - durationRemainingSec
	local durationWeight = durationElapsedSec - durationSec
	weight = weight + math.ceil(durationWeight)

	return weight
end


local function sortUnitAuraTable(a, b)
	return getAuraWeight(a) > getAuraWeight(b)
end

local function requestUnitAuraTable(unitDesignation, filterDescriptor)
	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	unitDesignation = strtrim(unitDesignation)
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 64)

	assert (filterDescriptor ~= nil)
	assert ('string' == type(filterDescriptor))
	filterDescriptor = strtrim(filterDescriptor)
	assert (string.len(filterDescriptor) >= 4)
	assert (string.len(filterDescriptor) <= 128)

	local j = 0
	local q = 0
	local e = {}
	while (j < 144) do
		j = j + 1
		local name, rank, pictureFile, stackQuantity, category,
		duration, expirationInstance,
		caster, stealableFlag, consolidateFlag, id = UnitAura(unitDesignation, j, filterDescriptor)
		if name then
			local auraTable = {name, rank, pictureFile, stackQuantity, category,
			                   duration, expirationInstance,
			                   caster, stealableFlag, consolidateFlag, id, j}
			q = q + 1
			e[q] = auraTable
		else
			break
		end
	end
	table.sort(e, sortUnitAuraTable)

	return e
end

local function subsetEventProcessor(subsetFrame)
	assert (subsetFrame ~= nil)

	local unitDesignation = subsetFrame.unit or 'player'
	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	unitDesignation = strtrim(unitDesignation)
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 64)

	local filterDescriptor = subsetFrame.filter or 'HELPFUL'
	assert (filterDescriptor ~= nil)
	assert ('string' == type(filterDescriptor))
	filterDescriptor = strtrim(filterDescriptor)
	assert (string.len(filterDescriptor) >= 4)
	assert (string.len(filterDescriptor) <= 128)

	local e = requestUnitAuraTable(unitDesignation, filterDescriptor)

	local i = 0
	local t = {subsetFrame:GetChildren()}
	while (i < math.min(#e, #t)) do
		i = i + 1
		local b = t[i]
		assert (b ~= nil)

		local auraTable = e[i]
		assert (auraTable ~= nil)

		local pictureFile = auraTable[3]
		assert (pictureFile ~= nil)
		local artwork = b.artwork
		assert (artwork ~= nil, b:GetName() .. ' requires artwork field')
		artwork:SetTexture(pictureFile)

		local stackQuantity = auraTable[4]
		local durationSec = auraTable[6]
		local expirationInstance = auraTable[7]
		applyDuration(b, durationSec, expirationInstance, stackQuantity)

		local spellName = auraTable[1]
		assert (spellName ~= nil)
		b.spell = spellName

		local index = auraTable[12]
		assert (index ~= nil)
		b.index = index

		b.filter = filterDescriptor
		b.unit = unitDesignation

		b:Show()
	end

	local k = #e
	while (k < #t) do
		k = k + 1
		local b = t[k]
		assert (b ~= nil)

		local artwork = b.artwork
		assert (artwork ~= nil, b:GetName() .. ' requires artwork field')
		artwork:SetTexture("Interface\\Icons\\spell_nature_wispsplode")

		b.spell = nil
		b.index = nil

		b.filter = filterDescriptor
		b.unit = unitDesignation

		b:Hide()
	end
end

local function subsetButtonUpdateProcessor(subsetButton)
	local unit = subsetButton.unit
	if not unit then
		return
	end

	local spell = subsetButton.spell
	if not spell then
		return
	end

	local filter = subsetButton.filter

	local n, _, _, stackQuantity, _, durationSec, expirationInstance = UnitAura(unit, spell, nil, filter)
	if not n then
		return
	end

	applyDuration(subsetButton, durationSec, expirationInstance, stackQuantity)
end

local function createSubsetButtonArtwork(subsetButton)
	assert (subsetButton ~= nil)

	local buttonWidth = subsetButton:GetWidth()
	local buttonHeight = subsetButton:GetHeight()

	local marginBottom = math.max(buttonWidth, buttonHeight) - math.min(buttonWidth, buttonHeight)
	local artwork = subsetButton:CreateTexture(subsetButton:GetName() .. 'Artwork', 'ARTWORK')
	artwork:SetPoint('TOPLEFT', 0, 0)
	artwork:SetPoint('TOPRIGHT', 0, 0)
	artwork:SetPoint('BOTTOMLEFT', 0, marginBottom)
	artwork:SetPoint('BOTTOMRIGHT', 0, marginBottom)
	artwork:SetTexture("Interface\\Icons\\spell_nature_wispsplode")

	return artwork
end

local function createSubsetButtonText(subsetButton)
	assert (subsetButton ~= nil)

	local buttonHeight = subsetButton:GetHeight()

	local t = subsetButton:CreateFontString(subsetButton:GetName() .. 'Text', 'OVERLAY')
	local fontObject = NumberFont_OutlineThick_Mono_Small
	assert (fontObject ~= nil)
	t:SetFontObject(fontObject)
	t:SetPoint('BOTTOMLEFT', -4, 0)
	t:SetPoint('BOTTOMRIGHT', 4, 0)
	t:SetPoint('TOPRIGHT', 4, buttonHeight / -2)
	t:SetPoint('TOPLEFT', -4, buttonHeight / -2)
	t:SetText('?')

	return t
end

local function createSubsetButton(subsetFrame, buttonDesignation, unitDesignation, spellName, filterDescriptor,
                                  buttonWidth, buttonHeight)
	assert (buttonDesignation ~= nil)
	assert ('string' == type(buttonDesignation))
	buttonDesignation = strtrim(buttonDesignation)
	assert (string.len(buttonDesignation) >= 4)
	assert (string.len(buttonDesignation) <= 256)

	assert (subsetFrame ~= nil)

	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	unitDesignation = strtrim(unitDesignation)
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 64)

	if spellName then
		assert ('string' == type(spellName))
		spellName = strtrim(spellName)
		assert (string.len(spellName) >= 2)
		assert (string.len(spellName) <= 256)
	end

	if filterDescriptor then
		assert ('string' == type(filterDescriptor))
		filterDescriptor = strtrim(filterDescriptor)
		assert (string.len(filterDescriptor) >= 4)
		assert (string.len(filterDescriptor) <= 128)
	end

	if not buttonWidth then
		buttonWidth = 24
	end
	buttonWidth = math.ceil(buttonWidth)
	assert (buttonWidth ~= nil)
	assert ('number' == type(buttonWidth))
	assert (buttonWidth >= 8)
	assert (buttonWidth <= 40)

	if not buttonHeight then
		buttonHeight = buttonWidth + buttonWidth * 2 / 3
	end
	buttonHeight = math.ceil(buttonHeight)
	assert (buttonHeight ~= nil)
	assert ('number' == type(buttonHeight))
	assert (buttonHeight >= 8)
	assert (buttonHeight <= 40)

	assert (buttonWidth <= buttonHeight)

	local b = CreateFrame('FRAME', buttonDesignation, subsetFrame)
	b:SetSize(buttonWidth, buttonHeight)

	b.artwork = createSubsetButtonArtwork(b)
	b.text = createSubsetButtonText(b)

	b.unit = unitDesignation
	b.spell = spellName or nil
	b.filter = filterDescriptor or nil
	b.index = nil

	--[[ TODO Add aura category (magic, disease, physical etc) border color indicator ]]--
	b.tooltipOverlay = createTooltipOverlay(b)

	b:SetScript('OnUpdate', subsetButtonUpdateProcessor)

	return b
end

local function createSubset(parentFrame, frameDesignation, unitDesignation, filterDescriptor,
			    columnQuantity, rowQuantity,
                            buttonWidth, buttonHeight)
	assert (frameDesignation ~= nil)
	assert ('string' == type(frameDesignation))
	frameDesignation = strtrim(frameDesignation)
	assert (string.len(frameDesignation) >= 4)
	assert (string.len(frameDesignation) <= 256)

	assert (parentFrame ~= nil)

	assert (unitDesignation ~= nil)
	assert ('string' == type(unitDesignation))
	unitDesignation = strtrim(unitDesignation)
	assert (string.len(unitDesignation) >= 4)
	assert (string.len(unitDesignation) <= 64)

	assert (filterDescriptor ~= nil)
	assert ('string' == type(filterDescriptor))
	filterDescriptor = strtrim(filterDescriptor)
	assert (string.len(filterDescriptor) >= 4)
	assert (string.len(filterDescriptor) <= 128)

	if not buttonWidth then
		buttonWidth = 24
	end
	buttonWidth = math.ceil(buttonWidth)
	assert (buttonWidth ~= nil)
	assert ('number' == type(buttonWidth))
	assert (buttonWidth >= 8)
	assert (buttonWidth <= 40)

	if not buttonHeight then
		buttonHeight = buttonWidth + buttonWidth * 2 / 3
	end
	buttonHeight = math.ceil(buttonHeight)
	assert (buttonHeight ~= nil)
	assert ('number' == type(buttonHeight))
	assert (buttonHeight >= 8)
	assert (buttonHeight <= 40)

	if not columnQuantity then
		columnQuantity = 1
	end
	columnQuantity = math.min(math.max(1, math.ceil(columnQuantity)), 12)
	assert (columnQuantity ~= nil)
	assert ('number' == type(columnQuantity))
	assert (columnQuantity >= 1)
	assert (columnQuantity <= 12)

	if not rowQuantity then
		rowQuantity = 1
	end
	rowQuantity = math.min(math.max(1, math.ceil(rowQuantity)), 12)
	assert (rowQuantity ~= nil)
	assert ('number' == type(rowQuantity))
	assert (rowQuantity >= 1)
	assert (rowQuantity <= 12)

	local padding = math.ceil(math.min(buttonWidth, buttonHeight) / 10)

	local subsetFrame = CreateFrame('FRAME', frameDesignation, parentFrame)
	subsetFrame:SetSize(buttonWidth * columnQuantity + padding * (columnQuantity + 1),
	                    buttonHeight * rowQuantity + padding * (rowQuantity + 1))

	local nameFormat = subsetFrame:GetName() .. 'Button%03d'
	local k = 0
	local i = 0
	while (i < columnQuantity) do
		i = i + 1
		local j = 0
		while (j < rowQuantity) do
			j = j + 1
			k = k + 1
			local n = string.format(nameFormat, k)
			local emptySpellName = nil
			local b = createSubsetButton(subsetFrame, n, unitDesignation, emptySpellName, filterDescriptor,
			                             buttonWidth, buttonHeight)
			b:SetPoint('BOTTOMLEFT', buttonWidth * (i - 1) + padding * i, buttonHeight * (j - 1) + padding * j)
		end
	end

	subsetFrame.unit = unitDesignation
	subsetFrame.filter = filterDescriptor
	subsetFrame:RegisterEvent('PARTY_CONVERTED_TO_RAID')
	subsetFrame:RegisterEvent('PARTY_MEMBERS_CHANGED')
	subsetFrame:RegisterEvent('PLAYER_FOCUS_CHANGED')
	subsetFrame:RegisterEvent('PLAYER_LOGIN')
	subsetFrame:RegisterEvent('PLAYER_TARGET_CHANGED')
	subsetFrame:RegisterEvent('PLAYER_SPECIALIZATION_CHANGED')
	subsetFrame:RegisterEvent('RAID_ROSTER_UPDATE')
	subsetFrame:RegisterEvent('UNIT_AURA')
	subsetFrame:RegisterEvent('UNIT_HEALTH')
	subsetFrame:RegisterEvent('UPDATE_BATTLEFIELD_SCORE')
	subsetFrame:SetScript('OnEvent', subsetEventProcessor)

	return subsetFrame
end

local function initSpellActivationOverlayAny(rootFrame)
	local margin = rootFrame:GetWidth() / 2 - 28 * 5 / 2
	local d0 = createIndicator(rootFrame, 'Magic', 'player', 'HARMFUL')
	d0:SetPoint('BOTTOMLEFT', margin + 28 * 0, 64)
	local d1 = createIndicator(rootFrame, 'Poison', 'player', 'HARMFUL')
	d1:SetPoint('BOTTOMLEFT', margin + 28 * 1, 64)
	local d2 = createIndicator(rootFrame, 'Disease', 'player', 'HARMFUL')
	d2:SetPoint('BOTTOMLEFT', margin + 28 * 2, 64)
	local d3 = createIndicator(rootFrame, 'Curse', 'player', 'HARMFUL')
	d3:SetPoint('BOTTOMLEFT', margin + 28 * 3, 64)
	local d4 = createIndicator(rootFrame, 'HARMFUL', 'player', 'HARMFUL')
	d4:SetPoint('BOTTOMLEFT', margin + 28 * 4, 64)

	local subset0 = createSubset(rootFrame, 'ClearcastingPlayerSubset', 'player', 'PLAYER HELPFUL',
	             6, 1)
	subset0:SetPoint('CENTER', 144 * 2, 144 * 2)

	local subset1 = createSubset(rootFrame, 'ClearcastingPlayerSubset', 'target', 'PLAYER HELPFUL',
	             6, 1)
	subset1:SetPoint('CENTER', -144 * 2, 144 * 2)

	local subset2 = createSubset(rootFrame, 'ClearcastingPlayerSubset', 'target', 'HARMFUL',
	             6, 1)
	subset2:SetPoint('CENTER', 0, 144 * 2)
end

local function initSpellActivationOverlayDeathKnight(rootFrame)
	local _, classDesignation = UnitClass('player')
	if 'DEATHKNIGHT' ~= classDesignation then
		return
	end

	local sectionWidth = 288
	local sectionHeight = 36

	local s0 = createSection('ClearcastingDeathKnightFrame1', rootFrame, sectionWidth, sectionHeight)
	s0:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 144)
	createIndicator(s0, 'Icebound Fortitude')
	createIndicator(s0, 'Anti-Magic Shell')
	createIndicator(s0, 'Anti-Magic Zone')
	createIndicator(s0, 'Vampiric Blood')
	createIndicator(s0, 'Icy Talons')
	createIndicator(s0, 'Killing Machine')
	createIndicator(s0, 'Freezing Fog')
	createIndicator(s0, 'Blade Barrier')
	createIndicator(s0, 'Horn of Winter')
	createIndicator(s0, 'Lichborne')

	local s1 = createSection('ClearcastingDeathKnightFrame2', rootFrame, sectionWidth, sectionHeight)
	s1:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 144 * 4)
	createIndicator(s1, 'Unholy Blight', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Frost Fever', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Blood Plague', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Heart Strike', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Chains of Ice', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Unholy Blight', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Icy Clutch', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Mark of Blood', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Strangulate', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Gnaw', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Death Grip', 'target', 'PLAYER HARMFUL')
	createIndicator(s1, 'Dark Command', 'target', 'PLAYER HARMFUL')

	--[[createIndicator(s0, 'Hysteria')]]--
end

local function initSpellActivationOverlayPaladin(rootFrame)
	local _, classDesignation = UnitClass('player')
	if 'PALADIN' ~= classDesignation then
		return
	end

	--[[ row 1 ]]--
	local presenceSet = {
		'Concentration Aura',
		'Crusader Aura',
		'Devotion Aura',
		'Fire Resistance Aura',
		'Frost Resistance Aura',
		'Retribution Aura',
		'Shadow Resistance Aura',
	}
	createIndicator(rootFrame, presenceSet, 'player', 'PLAYER HELPFUL')

	local sealSet = {
		'Seal of Command',
		'Seal of Corruption',
		'Seal of Light',
		'Seal of Righteousness',
		'Seal of Vengeance',
		'Seal of Wisdom',
	}
	createIndicator(rootFrame, sealSet, 'player', 'PLAYER HELPFUL')

	local blessingSet = {
		'Blessing of Kings',
		'Blessing of Might',
		'Blessing of Sanctuary',
		'Blessing of Wisdom',
		'Greater Blessing of Kings',
		'Greater Blessing of Might',
		'Greater Blessing of Sanctuary',
		'Greater Blessing of Wisdom',
	}
	createIndicator(rootFrame, blessingSet, 'player', 'PLAYER HELPFUL')

	createIndicator(rootFrame, 'Righteous Fury')

	--[[ row 2 ]]--
	createIndicator(rootFrame, 'Divine Shield')
	createIndicator(rootFrame, 'Divine Protection')
	createIndicator(rootFrame, 'Hand of Protection')
	createIndicator(rootFrame, 'Avenging Wrath')
	--[[ row 3 ]]--
	createIndicator(rootFrame, 'Divine Favor')
	createIndicator(rootFrame, 'Divine Plea')
	createIndicator(rootFrame, 'Divine Illumination')
	createIndicator(rootFrame, 'Hand of Sacrifice')
	--[[ row 4 ]]--
	createIndicator(rootFrame, 'Judgements of the Pure')
	createIndicator(rootFrame, 'Light\'s Grace')
	createIndicator(rootFrame, 'Infusion of Light')
	createIndicator(rootFrame, 'Holy Shield')
	--[[ row 5 ]]--
	createIndicator(rootFrame, 53601, 'player', 'PLAYER HELPFUL')
	createIndicator(rootFrame, 58597, 'player', 'PLAYER HELPFUL')
	createIndicator(rootFrame, 'Flash of Light', 'player', 'PLAYER HELPFUL')

	createIndicator(rootFrame, 58597, 'focus', 'HELPFUL')
end

local function initSpellActivationOverlayWarlock(rootFrame)
	assert (rootFrame ~= nil)

	local _, classDesignation = UnitClass('player')
	if 'WARLOCK' ~= classDesignation then
		return
	end

	local sectionWidth = 128
	local sectionHeight = 64

	local wf = createSection('ClearcastingWarlockFrame', rootFrame, sectionWidth, sectionHeight)
	wf:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 0)

	createIndicator(wf, 'Blood Fury')
	createIndicator(wf, 'Sacrifice')
	createIndicator(wf, 'Backlash')
	createIndicator(wf, 'Decimation')
	createIndicator(wf, 'Eradication')
	createIndicator(wf, 'Shadow Trance')
	createIndicator(wf, 'Nether Protection')
	createIndicator(wf, 'Shadow Ward')

	--[[ Cumulative effects. Show effects that belong to user only. ]]--
	local t = {
		'Drain Soul',
		'Drain Life',
		'Corruption',
		'Haunt',
		'Immolate',
		'Seed of Corruption',
		'Unstable Affliction',
		'Shadow Embrace',
	}

	--[[ Conflicting or overlapping effects. Show effects that belong to any player. ]]--
	local u = {
		--[[ Curse ]]--
		'Curse of Agony',
		'Curse of Exhaustion',
		'Curse of Weakness',
		'Curse of the Elements',
		'Curse of Tongues',
		'Hex',
		--[[ Magic ]]--
		'Shadow Mastery',
		'Entangling Roots',
		'Fear',
		'Polymorph',
		'Psychic Scream',
		'Spell Lock',
		'Counterspell',
		'Death Coil',
		'Howl of Terror',
		'Psychic Scream',
		--[[ Other ]]--
		'Concussion Blow',
		'Cyclone',
	}

	local sft = createSection('ClearcastingWarlockFocusPlayerHarmful', rootFrame, sectionWidth, sectionHeight)
	local sfu = createSection('ClearcastingWarlockFocusHarmful', rootFrame, sectionWidth, sectionHeight)
	local spt = createSection('ClearcastingWarlockPetTargetPlayerHarmful', rootFrame, sectionWidth, sectionHeight)
	local spu = createSection('ClearcastingWarlockPetTargetHarmful', rootFrame, sectionWidth, sectionHeight)
	local stt = createSection('ClearcastingWarlockTargetPlayerHarmful', rootFrame, sectionWidth, sectionHeight)
	local stu = createSection('ClearcastingWarlockTargetHarmful', rootFrame, sectionWidth, sectionHeight)

	local marginBottom = rootFrame:GetHeight() * 2 / 3
	local marginLeft = rootFrame:GetWidth()
	sft:SetPoint('BOTTOMLEFT', marginLeft - sectionWidth * 2, marginBottom + sectionHeight * 0)
	sfu:SetPoint('BOTTOMLEFT', marginLeft - sectionWidth * 2, marginBottom + sectionHeight * 1)
	spt:SetPoint('BOTTOMLEFT', marginLeft - sectionWidth * 1, marginBottom + sectionHeight * 0)
	spu:SetPoint('BOTTOMLEFT', marginLeft - sectionWidth * 1, marginBottom + sectionHeight * 1)
	stt:SetPoint('BOTTOMLEFT', sectionWidth, marginBottom + sectionHeight * 0)
	stu:SetPoint('BOTTOMLEFT', sectionWidth, marginBottom + sectionHeight * 1)

	local p = 0
	while (p < #t) do
		p = p + 1
		local spellName = t[p]
		createIndicator(stt, spellName, 'target', 'PLAYER HARMFUL')
		createIndicator(sft, spellName, 'focus', 'PLAYER HARMFUL')
		createIndicator(spt, spellName, 'pettarget', 'PLAYER HARMFUL')
	end

	local q = 0
	while (q < #u) do
		q = q + 1
		local spellName = u[q]
		createIndicator(stu, spellName, 'target', 'HARMFUL')
		createIndicator(sfu, spellName, 'focus', 'HARMFUL')
		createIndicator(spu, spellName, 'pettarget', 'HARMFUL')
	end
end

local function initSpellActivationOverlayWarrior(rootFrame)
	local _, classDesignation = UnitClass('player')
	if 'WARRIOR' ~= classDesignation then
		return
	end

	createIndicator(rootFrame, 'Shield Wall')
	createIndicator(rootFrame, 'Last Stand')
	createIndicator(rootFrame, 'Enraged Regeneration')
	createIndicator(rootFrame, 'Shield Block')

	createIndicator(rootFrame, 'Recklessness')
	createIndicator(rootFrame, 'Retaliation')
	createIndicator(rootFrame, 'Berserker Rage')
	createIndicator(rootFrame, 'Bloodrage')

	createIndicator(rootFrame, 'Blood Fury')
	createIndicator(rootFrame, 'Enrage')
	createIndicator(rootFrame, 'Glyph of Blocking')
	createIndicator(rootFrame, 'Sword and Board')

	local x = UIParent:GetWidth() / 2 - 28 * 10 / 2
	local y = 640

	local f0 = createIndicator(rootFrame, 'Concussion Blow', 'target', 'PLAYER HARMFUL')
	f0:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 0, y)

	local f1 = createIndicator(rootFrame, 'Shockwave', 'target', 'PLAYER HARMFUL')
	f1:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 1, y)

	local f2 = createIndicator(rootFrame, 'Hamstring', 'target', 'PLAYER HARMFUL')
	f2:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 2, y)

	local f3 = createIndicator(rootFrame, 'Piercing Howl', 'target', 'PLAYER HARMFUL')
	f3:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 3, y)

	local f4 = createIndicator(rootFrame, 'Gag Order', 'target', 'PLAYER HARMFUL')
	f4:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 4, y)

	local f5 = createIndicator(rootFrame, 'Disarm', 'target', 'PLAYER HARMFUL')
	f5:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 5, y)

	local f6 = createIndicator(rootFrame, 'Intimidating Shout', 'target', 'PLAYER HARMFUL')
	f6:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 6, y)

	local f7 = createIndicator(rootFrame, 'Sunder Armor', 'target', 'PLAYER HARMFUL')
	f7:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 7, y)

	local f8 = createIndicator(rootFrame, 'Taunt', 'target', 'PLAYER HARMFUL')
	f8:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 8, y)

	local f9 = createIndicator(rootFrame, 'Mocking Blow', 'target', 'PLAYER HARMFUL')
	f9:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 9, y)

	local f10 = createIndicator(rootFrame, 'Challenging Shout', 'target', 'PLAYER HARMFUL')
	f10:SetPoint('BOTTOMLEFT', UIParent, 'BOTTOMLEFT', x + 28 * 10, y)
end

local function initSpellActivationOverlayPriest(rootFrame)
	assert (rootFrame ~= nil)

	local sectionWidth = 288
	local sectionHeight = 36

	local s0 = createSection('ClearcastingPriestFrame1', rootFrame, sectionWidth, sectionHeight)
	s0:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 144)
	local s1 = createSection('ClearcastingPriestFrame2', rootFrame, sectionWidth, sectionHeight)
	s1:SetPoint('BOTTOMLEFT', rootFrame:GetWidth() / 2 - sectionWidth / 2, 144 * 4)

	local s = {s0, s1}
	local t = {'player', 'target'}
	local i = 0
	while (i < #t) do
		i = i + 1
		local section = s[i]
		assert (section ~= nil)
		local unitDesignation = t[i]
		assert (unitDesignation ~= nil)
		createIndicator(section, 'Power Word: Shield', unitDesignation, 'PLAYER HELPFUL')
		createIndicator(section, 'Renew', unitDesignation, 'PLAYER HELPFUL')
		createIndicator(section, 'Weakened Soul', unitDesignation, 'HARMFUL')
		createIndicator(section, 'Psychic Scream', unitDesignation, 'HARMFUL')
	end
	createIndicator(s0, 'Surge of Light', 'player', 'PLAYER HELPFUL')
	createIndicator(s0, 'Serendipity', 'player', 'PLAYER HELPFUL')
	createIndicator(s0, 'Borrowed Time', 'player', 'PLAYER HELPFUL')
end

local function initSpellActivationOverlay(rootFrame)
	initSpellActivationOverlayDeathKnight(rootFrame)
	initSpellActivationOverlayPaladin(rootFrame)
	initSpellActivationOverlayPriest(rootFrame)
	initSpellActivationOverlayWarlock(rootFrame)
	initSpellActivationOverlayWarrior(rootFrame)
	initSpellActivationOverlayAny(rootFrame)

	return {rootFrame:GetChildren()}
end

local function init(rootFrame)
	rootFrame:UnregisterAllEvents()

	local t = initSpellActivationOverlay(rootFrame)
	assert (t ~= nil)
	assert (#t >= 1)


	--rootFrame:SetScript('OnEvent', eventProcessor)

	rootFrame.elapsedSecs = 0.0
	--rootFrame:SetScript('OnUpdate', updateProcessor)

	--rootFrame:RegisterEvent('UNIT_AURA')
	--rootFrame:RegisterEvent('SPELLS_CHANGED')
	rootFrame.createIndicator = createIndicator
end

local function main()
	assert ('enGB' == GetLocale() or 'enUS' == GetLocale())
	local rootFrame = CreateFrame('FRAME', 'ClearcastingFrame', UIParent)
	rootFrame:SetSize(1024, 768)
	rootFrame:SetPoint('CENTER', UIParent, 'CENTER', 0, 0)

	rootFrame:SetScript('OnEvent', init)
	rootFrame:RegisterEvent('VARIABLES_LOADED')
end
main()


Mode Type Size Ref File
100644 blob 2228 9b16a88194e199a3ba859cc528d4a0e80fe19879 .luacheckrc
100644 blob 40051 fa1d1ddf2c60228a5fed0b39f47b9f7430b3c9fc clearcasting.lua
100644 blob 206 f9e44cd8f77d5a6b66a36def31ba0b404f376f2d clearcasting.toc
100644 blob 469 a2ba6e60655abe1e6bed39113ee3cd957a902bc4 clearcasting.xml
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/vrtc/wowaddons

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/vrtc/wowaddons

Clone this repository using git:
git clone git://git.rocketgit.com/user/vrtc/wowaddons

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main