Quantcast

-- cross-module access
local	KarmaObj = KarmaAvEnK;
local	KOH = KarmaObj.Helpers;
local	KCfg = KarmaObj.Cfg;

local GfTt =
	{
		GetNumPartyMembersTf = GetNumPartyMembers or GetNumSubgroupMembers,
		GetNumRaidMembersTf = GetNumRaidMembers or GetNumGroupMembers
	};

local	AchievementCategoryUnknown = "UNKNOWN";

local	KarmaModuleLocal =
		{
			-- criteria update cache:
				-- { [1] =
				--		{ [CatID] =
				--			{ [AchID] = { [1] = true, [2] = false, Count = 2, Completed = false, Category = CatID },
				--			... },
				--		... },
				--   [2] =
				--		{ [AchID] = { [1] = true, [2] = false, Count = 2, Completed = false, Category = CatID },
				--		... },
				-- }
			AchievementCache = { [1] = {}, [2] = {} },

			-- 165 is actually PvP/Arena, but why make it more difficult than necessary...
			AchievementCategoriesCheck = true,
			AchievementCategories =
				{
					[   165 ] = "GROUP",	-- pvp/arena

					[ 14808 ] = "GROUP",	-- dungeon/classic
					[ 14805 ] = "GROUP",	-- dungeon/BC

					[ 14806 ] = "GROUP",	-- dungeon/LK (Cata)
					[ 14922 ] = "RAID",	-- raid/LK (Cata)

					-- Cataclysm
					[ 15067 ] = "GROUP",	-- dungeon/Cataclysm
					[ 15068 ] = "RAID",	-- raid/Cataclysm

					-- Pandaria
					[ 15106 ] = "GROUP",	-- dungeon/Pandaria
					[ 15107 ] = "RAID",	-- raid/Pandaria

					[ 15115 ] = "GROUP",	-- dungeon/Pandaria: challenge modes
				},

				-- handpicked candidates & list
			AchievementListSeasonal =
				{
					-- World events:
						-- Winterveil (156):
							-- 273/279: Save Metzen / Questline for Grinch
								-- not considered (barely anyone does this as group)
							-- 1687: Throw snow on race/class
								-- requires: spell cast tracking
								-- not considered (but would like to...)
							-- 1690: dance as snowman with another snowman
								-- requires: emote tracking
								-- not considered (but would like to...)

						-- Hallow's End (158):
							-- 238: get transmogrified via staves
								-- requires: spell cast tracking
								-- not considered (but would like to...)
							-- 255: kill the headless horseman in scarlet monastery
								-- mono-criteria: mob kill
								-- not for cache, but for accept
					[  255 ] = { CatType = "GROUP" },
							-- 291: convert races into pumpkin heads
								-- requires: spell cast tracking
								-- not considered (but would like to...)
							-- 292: collect the two rare drops of the headless horseman
								-- criteria: 2 items (pet, helm)
					[  292 ] = { CatType = "GROUP" },

						-- Noblegarden (159):
							-- 2416: Un'Goro, lay egg as bunny
								-- requires: check who gave the bunny buff when gaining achievement
								-- not considered (but would like to...)

						-- Lunar Festival (160):
							-- 937: Complete Elune's Blessing Q (which is: kill Omen)
								-- does this complete at kill or at completing quest at NPC?
								-- requires: potentially track if mob killed == Omen, save raid, on quest completion use saved raid to add achievement
								-- not considered, need more info

						-- Midsummer (161):
							-- 263: kill Ahune in Slave Pens, also as q 11972
								-- mono-criteria: mob kill
								-- not for cache, but for accept
					[  263 ] = { CatType = "GROUP" },

						-- Brewfest (162):
							-- 295: kill Coren Direbrew in blackrock depths
								-- mono-criteria: mob kill
								-- not for cache, but for accept
					[  295 ] = { CatType = "GROUP" },

						-- Children's Week (163):
							-- 1786: the most idiotic achievement of assholiness in the current World of Warcraft (as of 3.1.2)
								-- not considered
							-- 1790: kill Ymiron with your orphan out (requires *you* surviving)
								-- mono-criteria: mob kill
								-- not for cache, but for accept
					[ 1790 ] = { CatType = "GROUP" },

						-- Love is in the Air (187): (my favorite!)
							-- 260: mend the hearts of 20 characters
								-- not considered (for fairness reasons would have to track *all* mended hearts...)
							-- 1188: shoot people a ugly flying goblin pet
								-- requires: spell cast tracking
								-- not considered (for fairness reasons would have to track *all* petified chars...)
							-- 1291: romantic picnic in Dalaran with someone else
								-- requires: open item (basket) tracking, check all people in the area for the heart buff
								-- not considered (but would like to...)
							-- 1703: get a rose petal clicky
								-- mono-criteria: loot
								-- not for cache, but for accept
					[ 1703 ] = { CatType = "GROUP" },

					-- General: none of this goes into cache, as they are all 'one-shot'
						-- level 10, 20, 30, 40, 50, 60, 70, 80
					[    6 ] = { CatType = AchievementCategoryUnknown },
					[    7 ] = { CatType = AchievementCategoryUnknown },
					[    8 ] = { CatType = AchievementCategoryUnknown },
					[    9 ] = { CatType = AchievementCategoryUnknown },
					[   10 ] = { CatType = AchievementCategoryUnknown },
					[   11 ] = { CatType = AchievementCategoryUnknown },
					[   12 ] = { CatType = AchievementCategoryUnknown },
					[   13 ] = { CatType = AchievementCategoryUnknown },

						-- won a need/greed purple with 100
					[  558 ] = { CatType = AchievementCategoryUnknown },
					[  559 ] = { CatType = AchievementCategoryUnknown },

						-- money, money, money...
					[ 1176 ] = { CatType = AchievementCategoryUnknown },	--    100g
					[ 1177 ] = { CatType = AchievementCategoryUnknown },	--  1.000g
					[ 1178 ] = { CatType = AchievementCategoryUnknown },	--  5.000g
					[ 1180 ] = { CatType = AchievementCategoryUnknown },	-- 10.000g
					[ 1181 ] = { CatType = AchievementCategoryUnknown },	-- 25.000g
				},
			AchievementsListPerCategoryOnce = false,
		};

				--[[
					[ 14806 ] = "GROUP",	-- dungeon/LK/normal - Cata
					[ 14921 ] = "GROUP",	-- dungeon/LK/heroic
				]]--

local	AchievementCategories_3_x_x =
	{
		[ 14806 ] = "GROUP",	-- dungeon/LK/normal - Cata
		[ 14921 ] = "GROUP",	-- dungeon/LK/heroic

		[ 14922 ] = "RAID10",	-- raid/LK/normal - Cata
		[ 14923 ] = "RAID25",	-- raid/LK/heroic
		[ 14961 ] = "RAID10",	-- raid/LK/Ulduar/normal
		[ 14962 ] = "RAID25",	-- raid/LK/Ulduar/heroic
		[ 15001 ] = "RAID10",	-- raid/LK/CotC/normal
		[ 15002 ] = "RAID25",	-- raid/LK/CotC/heroic

		[ 15041 ] = "RAID10",	-- raid/LK-3.3/IcecrownCitadel
		[ 15042 ] = "RAID25",	-- raid/LK-3.3/IcecrownCitadel
	};


local	KARMA_DB_L5_RRFFM_TERROR = "TERROR";

-- function	Karma_CreateAchievementsCache()
function	KarmaObj.Achievements.CacheCreate()
	local	bDebug = KCfg.Get("DEBUG_ENABLED") == 1;

	if (KarmaModuleLocal.AchievementCategoriesCheck) then
		if (KarmaObj.Const.iTOC < 40000) then
			local	oTable, k, v = KarmaModuleLocal.AchievementCategories;
			for k, v in pairs(AchievementCategories_3_x_x) do
				oTable[k] = v;
			end
		end

		-- check if we got any *new* categories in dungeon/raid (only done once at startup)
		local	lCategoryAll = GetCategoryList();
		local	iKey, iCategoryID;
		for iKey, iCategoryID in pairs(lCategoryAll) do
			local	sCategoryTitle, iParentCategoryID = GetCategoryInfo(iCategoryID);
			if (iParentCategoryID == 168) then
				if (KarmaModuleLocal.AchievementCategories[iCategoryID] == nil) then
					KarmaChatSecondaryFallbackDefault("Oops! Missing a whole *category* from achievement tracking: " .. sCategoryTitle .. " (" .. iCategoryID .. ")");
					KarmaModuleLocal.AchievementCategories[iCategoryID] = AchievementCategoryUnknown;
				else
					KarmaChatDebug("[" .. iCategoryID .. "] " .. sCategoryTitle .. ": " .. KarmaModuleLocal.AchievementCategories[iCategoryID]);
				end
			end
		end

		KarmaModuleLocal.AchievementCategoriesCheck = false;
		KarmaModuleLocal.AchievementsListPerCategoryOnce = false;	-- spammy debug output when true
	end

	local	Store = function(iAchID, iCatID, bIsRaid)
			local	iID, sAchievementName, iPoints, bAchievementCompleted = GetAchievementInfo(iAchID);
			local	iMax = GetAchievementNumCriteria(iAchID);
			if ((not bAchievementCompleted) and iMax and (iMax > 1)) then
				local	oCat;
				if (iCatID) then
					KOH.TableInit(KarmaModuleLocal.AchievementCache[1], iCatID);
					oCat = KarmaModuleLocal.AchievementCache[1][iCatID];
				else
					oCat = KarmaModuleLocal.AchievementCache[2];
					iCatID = GetAchievementCategory(iAchID);
				end

				oCat[iAchID] = {};
				local	oAch = oCat[iAchID];

				local	i;
				for i = 1, iMax do
					local	sName, iType, bCriteriaCompleted = GetAchievementCriteriaInfo(iAchID, i);
					oAch[i] = bCriteriaCompleted;
				end

				oAch.Completed = bAchievementCompleted;
				oAch.Count = iMax;
				oAch.Category = iCatID;
				if (bIsRaid) then
					if (strfind(sAchievementName, "10")) then
						oAch.RaidSize = 10;
					elseif (strfind(sAchievementName, "25")) then
						oAch.RaidSize = 25;
					end
				end

				return true;
			end

			return false;
		end

	-- TODO:
	--  97 (Group) : EXPLORATION

	--  95 (Group) : PvP -> 165 : Arena
	-- 168 (Group) : Dungeons & Raids
	local	iCategoryID, sCategoryType, bIsRaid;
	for iCategoryID, sCategoryType in pairs(KarmaModuleLocal.AchievementCategories) do
		bIsRaid = strsub(sCategoryType, 1, 4) == "RAID";
		local	sCategoryTitle, iParentCategoryID = GetCategoryInfo(iCategoryID);
		if (sCategoryTitle) then
			if (KarmaModuleLocal.AchievementsListPerCategoryOnce) then
				KarmaChatDebug("Category #" .. iCategoryID .. " (" .. sCategoryType .. "): " .. sCategoryTitle .. " -->");
			end

			local	iMax = GetCategoryNumAchievements(iCategoryID);
			if (iMax and (iMax > 0)) then
				local	sAll, iAll = "", 0;
				local	i, iAchievementID, sAchievementName, _, bCompleted;
				for i = 1, iMax do
					iAchievementID, sAchievementName, _, bCompleted = GetAchievementInfo(iCategoryID, i);
					local	bStored = Store(iAchievementID, iCategoryID, bIsRaid);
					if (bStored or bDebug) then
						if (KarmaModuleLocal.AchievementsListPerCategoryOnce) then
							if (bCompleted) then
								sAll = sAll .. " |cFF00FF00(" .. sAchievementName .. ")|r";
							elseif (bStored) then
								sAll = sAll .. " <" .. sAchievementName .. ">";
							else
								sAll = sAll .. " [" .. sAchievementName .. "]";
							end
							iAll = iAll + 1;
							if ((iAll % 10) == 0) then
								KarmaChatDebug(format("[%d]", (iAll / 10)) .. sAll);
								sAll = "";
							end
						end
					end
				end
				if (KarmaModuleLocal.AchievementsListPerCategoryOnce) then
					if (sAll ~= "") then
						KarmaChatDebug(format("[%d]", ((iAll + 9) / 10)) .. sAll);
					end
				end
			else
				KarmaChatDebug("Achievement category without achievements? #" .. iCategoryID);
			end
			if (KarmaModuleLocal.AchievementsListPerCategoryOnce) then
				KarmaChatDebug("<--");
			end
		end
	end

	local	iCategoryGeneralID, iCategoryWorldEventsID = 92, 155;
	local	sCategoryGeneralID     = GetCategoryInfo(iCategoryGeneralID);
	local	sCategoryWorldEventsID = GetCategoryInfo(iCategoryWorldEventsID);
	if (KarmaModuleLocal.AchievementsListPerCategoryOnce) then
		KarmaChatDebug("Category groups #" .. iCategoryGeneralID .. "/" .. iCategoryWorldEventsID .. " (VARIOUS): "
						   .. sCategoryGeneralID .. "/" .. sCategoryWorldEventsID .. " -->");
	end

	local	sAll, iAll = "", 0;
	local	iAchievementID, oAchInfo;
	for iAchievementID, oAchInfo in pairs(KarmaModuleLocal.AchievementListSeasonal) do
		local	bStored = Store(iAchievementID);
		if (bStored or bDebug) then
			if (KarmaModuleLocal.AchievementsListPerCategoryOnce) then
				local	_, sAchievementName, _, bCompleted = GetAchievementInfo(iAchievementID);
				if (bCompleted) then
					sAll = sAll .. " |cFF00FF00(" .. sAchievementName .. ")|r";
				elseif (bStored) then
					sAll = sAll .. " <" .. sAchievementName .. ">";
				else
					sAll = sAll .. " [" .. sAchievementName .. "]";
				end
				iAll = iAll + 1;
				if ((iAll % 10) == 0) then
					KarmaChatDebug(format("[%d]", (iAll / 10)) .. sAll);
					sAll = "";
				end
			end
		end
	end
	if (KarmaModuleLocal.AchievementsListPerCategoryOnce) then
		if (sAll ~= "") then
			KarmaChatDebug(format("[%d]", ((iAll + 9) / 10)) .. sAll);
		end
	end

	if (KarmaModuleLocal.AchievementsListPerCategoryOnce) then
		KarmaChatDebug("<--");
	end

	KarmaModuleLocal.AchievementsListPerCategoryOnce = false;
end

local	AchievementsProgressExecute = function(oParty, sPlayer, sEvent, iAchievementID, bSpecial)
	if (KCfg.Get("TRACK_DISABLEACHIEVEMENT") == 1) then
		KarmaChatDebug("Achievement tracking is disabled.");
		return
	end

	if (WhoAmI == nil) then
		WhoAmI = UnitName("player");
	end

	local	DoAchievementAdd = function(iAchID, iCritIx, iCritNum)
			local	sName, sContainer;
			for sName, sContainer in pairs(oParty) do
				if (sName ~= WhoAmI) then
					local	oMember = Karma_MemberList_GetObject(sName);
					if (oMember) then
						if (iCritIx == nil) then
							KarmaChatDebug("Achievement #" .. iAchID .. " (completion) -> " .. sName);
						else
							KarmaChatDebug("Achievement #" .. iAchID .. "." .. iCritIx .. " -> " .. sName);
						end
						KarmaObj.DB.MC.AchievementAdd(oMember, sPlayer, iAchID, iCritIx, iCritNum);
					end
				end
			end
		end;

	KarmaChatDebug("Achievement(" .. sEvent .. ") progressed or completed: " .. iAchievementID);

	-- is there any 'hidden until other achievement reached' in Arena/Dungeon/Raid? dunno. better be on the safe side:
	local	oAch;
	if (bSpecial) then
		oAch = KarmaModuleLocal.AchievementCache[2][iAchievementID];
		if (oAch == nil) then
			KarmaChatDebug("IEKS! Achievement tracking: Internal inconsistency with cache for world events. :(");
			return
		end
	else
		local	iCategoryID = GetAchievementCategory(iAchievementID);
		KOH.TableInit(KarmaModuleLocal.AchievementCache[1], iCategoryID);
		KOH.TableInit(KarmaModuleLocal.AchievementCache[1][iCategoryID], iAchievementID);
		oAch = KarmaModuleLocal.AchievementCache[1][iCategoryID][iAchievementID];
	end

	local	iMax, iCriteriaIndex = GetAchievementNumCriteria(iAchievementID);
	if (iMax > 1) then
		local	i;
		for i = 1, iMax do
			local	sName, iType, bCriteriaCompleted = GetAchievementCriteriaInfo(iAchievementID, i);
			if (oAch[i] ~= bCriteriaCompleted) then
				iCriteriaIndex = i;
				KarmaChatDebug("Achieved criteria #" .. iCriteriaIndex .. " in achievement " .. iAchievementID .. " completed.");
				DoAchievementAdd(iAchievementID, iCriteriaIndex, iMax);
				oAch[i] = bCriteriaCompleted;
			end
		end
	end

	local	_, _, _, bAchievementCompleted = GetAchievementInfo(iAchievementID);
	if (oAch.Completed ~= bAchievementCompleted) then
		DoAchievementAdd(iAchievementID);
		oAch.Completed = bAchievementCompleted;
		KarmaChatDebug("Achievement completed: " .. iAchievementID);
	end
end

-- function	Karma_AchievementProgress(sEvent, iAchievementID, ...)
function	KarmaObj.Achievements.Progress(oParty, sPlayer, sEvent, iAchievementID, ...)
	local	arg2, arg3, arg4, arg5 = ...;
	KarmaChatDebug("Achievement: " .. sEvent .. ", " .. KOH.AllToString(iAchievementID) .. ", " .. KOH.AllToString(arg2) .. ", "
			.. KOH.AllToString(arg3) .. ", " .. KOH.AllToString(arg4) .. ", " .. KOH.AllToString(arg5) .. ", ...");

	local	iID, sAchievementName = GetAchievementInfo(iAchievementID);
	KarmaChatDebug("Earned new achievement #" .. iAchievementID .. ": <" .. Karma_NilToString(sAchievementName) .. ">");
	if (sAchievementName == nil) then
		-- uuh... huh?!
		return
	end

	local	iCategoryID = GetAchievementCategory(iAchievementID);
	local	sCategoryTitle, iParentCategoryID = GetCategoryInfo(iCategoryID);
	KarmaChatDebug("In category #" .. iCategoryID .. ": <" .. Karma_NilToString(sCategoryTitle) .. ">");

	--  95 (Group) : PvP -> 165 : Arena
	-- 168 (Group) : Dungeons & Raids
	local	bValid, bSpecial = false, false
	if ((iParentCategoryID == 168) or (iCategoryID == 165)) then
		bValid = true;
	elseif (KarmaModuleLocal.AchievementListSeasonal[iAchievementID] ~= nil) then
		bValid = true;
		bSpecial = true;
	end

	if (bValid) then
		KarmaChatDebug("Valid achievement for (group) tracking, checking further.");
		AchievementsProgressExecute(oParty, sPlayer, sEvent, iAchievementID, bSpecial);
	end
end

-- function	Karma_AchievementTest()
function	KarmaObj.Achievements.UpdateTest(oParty, sPlayer)
	if (KCfg.Get("TRACK_DISABLEACHIEVEMENT") == 1) then
		return
	end

	local	sCatTypeReq = "SPECIAL";	-- tbd (SPELL, BUFF, ... + SELFTOTARGET, TARGETTOSELF
	local	iRaidSize = GfTt.GetNumRaidMembersTf();
	local	iPartySize = GfTt.GetNumPartyMembersTf();
	if (KarmaObj.Const.iTOC >= 50000) then
		if (not IsInRaid()) then
			iRaidSize = 0;
		end
		if (not IsInGroup()) then
			iPartySize = 0;
		end
	end
	local	_, _, _, _, iPlayersMax = GetInstanceInfo();
	if (iPlayersMax < 10 and iRaidSize > 0) then
		iPlayersMax = 10;
		if (iRaidSize > 15) then
			iPlayersMax = 25;
		end
	end

	if (iPlayersMax >= 10) then
		sCatTypeReq = "RAID";
		if (KarmaObj.Const.iTOC < 40000) then
			sCatTypeReq = "RAID" .. iPlayersMax;
		end
	elseif (iPartySize > 0) then
		sCatTypeReq = "GROUP";
	end

	-- -----------------------------------------------------------
	-- TODO:
	-- if the number of achievements changed (new one added because old completed,
	-- we *might* need to check what was added!
	-- it would require a new achievement, that
	-- * has partial progress
	-- * only shows after completing another one
	-- * is relevant for group/raid (i.e. kill mob, loot boss mob item)
	-- as soon as more is tracked (spell cast/item used on target or from target),
	-- this might have to be reviewed
	-- currently I know of no case where it is relevant for this function
	-- -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
	--	local	oCacheOld = KarmaModuleLocal.AchievementCache;
	--	KarmaModuleLocal.AchievementCache = {};
	--	KarmaObj.Achievements.CacheCreate();
	--	local	oCacheNew = KarmaModuleLocal.AchievementCache;
	--	KarmaModuleLocal.AchievementCache = oCacheOld;
	-- -----------------------------------------------------------

	local	CheckAch = function(iAch, oAch)
			if (oAch.Count == nil) then
				-- added via event to cache: -> should be handled correctly there
				return false;
			end

			if (oAch.RaidSize and (iPlayersMax ~= oAch.RaidSize)) then
				-- Achievement is for other raid size (10/25)
				return false;
			end

			local	bChange, i = false;
			for i = 1, oAch.Count do
				local	sName, iType, bCriteriaCompleted = GetAchievementCriteriaInfo(iAch, i);
				if (oAch[i] ~= bCriteriaCompleted) then
					bChange = true;
					break;
				end
			end

			return bChange;
		end

	local	sSkipped, iCatID, oCat = "";
	for iCatID, oCat in pairs(KarmaModuleLocal.AchievementCache[1]) do
		-- only check applicable categories:
		local	bCatValid = true;
		if (KarmaModuleLocal.AchievementCategories[iCatID] ~= nil) then
			if ((KarmaModuleLocal.AchievementCategories[iCatID] ~= sCatTypeReq) and
			    (KarmaModuleLocal.AchievementCategories[iCatID] ~= AchievementCategoryUnknown)) then
				bCatValid = false;

				sSkipped = sSkipped .. ", " .. iCatID .. " (" .. GetCategoryInfo(iCatID) .. ")";
			end
		end

		if (bCatValid) then
			local	iAch, oAch;
			for iAch, oAch in pairs(oCat) do
				if (CheckAch(iAch, oAch)) then
					KarmaChatDebug("Achievement updating (1): " .. iAch);
					AchievementsProgressExecute(oParty, sPlayer, "ACHIEVEMENT_PROGRESS_DEDUCED", iAch, false);
				end
			end
		end
	end
	if (sSkipped ~= "") then
		KarmaChatDebug("Skipped categories: " .. strsub(sSkipped, 3));
	end

	for iAch, oAch in pairs(KarmaModuleLocal.AchievementCache[2]) do
		local	sCatType = KarmaModuleLocal.AchievementListSeasonal[iAch].CatType;
		if ((sCatType == sCatTypeReq) or
		    (sCatType == AchievementCategoryUnknown)) then
			if (CheckAch(iAch, oAch)) then
				KarmaChatDebug("Achievement updating (2): " .. iAch);
				AchievementsProgressExecute(oParty, sPlayer, "ACHIEVEMENT_PROGRESS_DEDUCED", iAch, false);
			end
		end
	end
end

function	KarmaObj.Achievements.LinesGet(oMember, sChar)
	local	lAchievements = KarmaObj.DB.MC.AchievementListGet(oMember, sChar);
	if (type(lAchievements) ~= "table") then
		return;
	end

	local	TotalKeysCount = 0;	-- Cats + Achs

	local	lCats = {};		-- CatIDs
	local	lCat2AchID = {};	-- CatID, AchL

	local	sAKey, oValues;
	for sAKey, oValues in pairs(lAchievements) do
		oValues.AchievementID = tonumber(strsub(sAKey, 2));
		local	_, sTitle = GetAchievementInfo(oValues.AchievementID);
		oValues.AchievementTitle = sTitle;
		oValues.CategoryID = GetAchievementCategory(oValues.AchievementID);
		if (lCat2AchID[oValues.CategoryID] == nil) then
			tinsert(lCats, oValues.CategoryID);
			lCat2AchID[oValues.CategoryID] = {};
			TotalKeysCount = TotalKeysCount + 1;
		end
		tinsert(lCat2AchID[oValues.CategoryID], oValues.AchievementID);
		TotalKeysCount = TotalKeysCount + 1;
	end
	table.sort(lCats);
	local	iCat;
	for iCat, oValues in pairs(lCat2AchID) do
		table.sort(oValues);
	end

	local	sDbg = "";

	local	iKey, lKeys, iDummy, iSubDummy, iAch = 0, {};
	for iDummy, iCat in pairs(lCats) do
		iKey = iKey + 1;
		lKeys[iKey] = "C" .. iCat;
		sDbg = "Cat key " .. lKeys[iKey] .. ":";
		for iSubDummy, iAch in pairs(lCat2AchID[iCat]) do
			iKey = iKey + 1;
			lKeys[iKey] = "A" .. iAch;
			sDbg = sDbg .. " " .. lKeys[iKey];
		end
		KarmaChatDebug(sDbg);
	end
	lKeys.__Count = iKey;

	return lKeys, lAchievements;
end

function	KarmaObj.Achievements.ListTip(self)
	local	id = self:GetID();
	local	btnText = getglobal("AchievementList_GlobalButton" .. id .. "_Text");
	local	sName = btnText:GetText();
	if (sName ~= nil) and (sName ~= "") then
		GameTooltip:SetOwner(self, "ANCHOR_RIGHT");
		local	btnObj = getglobal("AchievementList_GlobalButton" .. id);
		if (btnObj.AchievementData == nil) then
			GameTooltip:SetText(sName, 1, 1, 0);
		else
			local	oAch = btnObj.AchievementData;
			GameTooltip:AddLine(oAch.AchievementTitle .. " (" .. oAch.AchievementID .. ")", 1, 1, 0);
			local	iMax = oAch["CMAX"];
			if (iMax) then
				local	iAch, i = 0;
				for i = 1, iMax do
					local	tsAch = oAch["C" .. i];
					if (tsAch) then
						iAch = iAch + 1;
						GameTooltip:AddLine("Criteria achieved on " .. date(KARMA_DATEFORMAT .. " %H:%M", tsAch) .. ":", 1, 1, 1);
						GameTooltip:AddLine("-- " .. GetAchievementCriteriaInfo(oAch.AchievementID, i), 1, 1, 1);
					end
				end
				if (iAch == iMax) then
					GameTooltip:AddLine("Completed on " .. date(KARMA_DATEFORMAT .. " %H:%M", oAch.At), 1, 1, 1);
				else
					GameTooltip:AddLine(format("%d%% achieved with this player", (100 * iAch) / iMax), 1, 1, 1);
				end
			else
				GameTooltip:AddLine("Completed on " .. date(KARMA_DATEFORMAT .. " %H:%M", oAch.At), 1, 1, 1);
			end

			GameTooltip:AddLine("Shift-click to create a link in the chatbox.", 0, 1, 1);
			GameTooltip:AddLine("Middle-click to show the achievement UI with this achievement selected.", 0, 1, 1);
		end

		GameTooltip:Show();
	end
end

function	KarmaObj.Achievements.OnClick(oAchBtn, sMouse)
	if (oAchBtn.AchievementData == nil) then
		KarmaChatDebug("Clicked: " .. sMouse .. " on achievement button, having no infos.");
		GameTooltip:SetText(sName, 1, 1, 0);
	else
		local	oAch = oAchBtn.AchievementData;
		KarmaChatDebug("Clicked: " .. sMouse .. " on achievement button, achievement id = " .. oAch.AchievementID);
		if ((sMouse == "LeftButton") and (IsShiftKeyDown())) then
			ChatFrameEditBox:Insert(GetAchievementLink(oAch.AchievementID));
		elseif (sMouse == "MiddleButton") then
			if (not IsAddOnLoaded("Blizzard_AchievementUI")) then
				LoadAddOn("Blizzard_AchievementUI");
				KarmaChatSecondaryFallbackDefault("Had to load achievement UI first, please click again...");
			else
				if (AchievementFrame == nil) then
					KarmaChatDefault("Loading the achievement UI failed for unknown reasons. Can't display...");
				else
					if (not AchievementFrame:IsShown()) then
						AchievementFrame_ToggleAchievementFrame();
						-- ShowUIPanel(AchievementFrame);
						-- AchievementFrameTab_OnClick(1);
					end
					AchievementFrame_SelectAchievement(oAch.AchievementID);
				end
			end
		end
	end
end

local	bInit = false;

local	CheckTerrorUnitNameReq;
local	CheckTerrorUnitNameIs;
local	CheckTerrorUnitNameNow;
local	CheckTerrorUnitSetAt = 0;
local	CheckTerrorUnitIsMine;

function	ClearAchievementComparisonUnitHook()
	CheckTerrorUnitNameNow = nil;
	CheckTerrorUnitSetAt = 0;
end

function	SetAchievementComparisonUnitHook(sUnit)
	CheckTerrorUnitNameNow = sUnit;
	CheckTerrorUnitSetAt = GetTime();
end

function	KarmaObj.Achievements.CheckTerrorQuery(oList)
	-- list of missing terror checks, key = unit, value = name(@server)
	if (not bInit) then
		bInit = true;
		hooksecurefunc("ClearAchievementComparisonUnit", ClearAchievementComparisonUnitHook);
		hooksecurefunc("SetAchievementComparisonUnit", SetAchievementComparisonUnitHook);
	end

	-- first check if frame is defined: AchUI is LOD
	if (InCombatLockdown() or AchievementFrame and AchievementFrame:IsShown()) then
		return
	end

	local	iNow = GetTime();
	if (iNow - CheckTerrorUnitSetAt < 30) then
		return
	end

	if (oList[CheckTerrorUnitNameNow]) then
		if (CheckTerrorUnitNameReq ~= CheckTerrorUnitNameNow) then
			if (CheckTerrorUnitIsMine and (CheckTerrorUnitIsMine < CheckTerrorUnitSetAt)) then
				-- user/other addon requested other unit
				return
			end
		elseif (CheckTerrorUnitIsMine and (iNow - CheckTerrorUnitIsMine < 30)) then
			-- requested less than 30s ago
			return
		end
	elseif (CheckTerrorUnitSetAt and (iNow - CheckTerrorUnitSetAt < 30)) then
		return
	end

	local	k, v;
	for k, v in pairs(oList) do
		if (CheckInteractDistance(k, 1)) then
			CheckTerrorUnitNameReq = k;
			break;
		end
	end

	-- nothing found in range?
	if (CheckTerrorUnitNameReq == nil) then
		-- check if the units are still valid
		local	oRemove, k, v;
		for k, v in pairs(oList) do
			if (not UnitExists(k)) then
				oRemove = oRemove or {};
				tinsert(oRemove, k);
			end
		end

		if (oRemove) then
			for k, v in pairs(oRemove) do
				oList[v] = nil;
			end
		end
	end

	if (CheckTerrorUnitNameReq) then
		local	sServer;
		CheckTerrorUnitNameIs, sServer = UnitName(CheckTerrorUnitNameReq);
		if (sServer and (sServer ~= "")) then
			CheckTerrorUnitNameIs = CheckTerrorUnitNameIs .. "@" .. sServer;
		end

		Karma:RegisterEvent("INSPECT_ACHIEVEMENT_READY");

		local	sOut = "Checking unit " .. CheckTerrorUnitNameReq .. " for terror achievements...";
		if (AchievementFrame and AchievementFrameComparison) then
			AchievementFrameComparison:UnregisterEvent("INSPECT_ACHIEVEMENT_READY");
			sOut = sOut .. " (event unset in AchUI to STFU)";
		end
		KarmaChatDebug(sOut);

		ClearAchievementComparisonUnit();
		SetAchievementComparisonUnit(CheckTerrorUnitNameReq);
		CheckTerrorUnitIsMine = GetTime();
	end
end

local	oIDAlliance = { 610, 611, 612, 613, 614, };
local	oIDHorde = { 615, 616, 617, 618, 619, };

function	KarmaObj.Achievements.CheckTerrorReady()
	if ((CheckTerrorUnitNameNow ~= nil) and (CheckTerrorUnitNameReq == CheckTerrorUnitNameNow) and
	    (math.abs(CheckTerrorUnitIsMine - CheckTerrorUnitSetAt) < 1)) then
		local	sName, sServer = UnitName(CheckTerrorUnitNameReq);
		if (sServer and (sServer ~= "")) then
			sName = sName .. "@" .. sServer;
		end
		if (sName ~= CheckTerrorUnitNameIs) then
			CheckTerrorUnitNameReq = nil;
			CheckTerrorUnitIsMine = nil;
			return
		end

		local	oMember = Karma_MemberList_GetObject(sName);
		if (oMember == nil) then
			CheckTerrorUnitNameReq = nil;
			CheckTerrorUnitIsMine = nil;
			return
		end

		KOH.TableInit(oMember, KARMA_DB_L5_RRFFM_TERROR);
		local	oStore = oMember[KARMA_DB_L5_RRFFM_TERROR];
		oStore.Updated = time();

		local	oIDs;
		if (UnitFactionGroup(CheckTerrorUnitNameReq) == "Alliance") then
			oIDs = oIDAlliance;
		else
			oIDs = oIDHorde;
		end

		local	iTotal = 0;
		if (oIDs) then
			local	Index, ID;
			for Index, ID in pairs(oIDs) do
				local	bCompleted, iMonth, iDay, iYear = GetAchievementComparisonInfo(ID);
				local	sOut = "?";
				if (bCompleted) then
					iTotal = iTotal + 1;
					oStore[ID] = iYear * 512 + iMonth * 32 + iDay;
					sOut = "YES. :-(";
				else
					sOut = "No. :-)";
				end
				KarmaChatDebug("Checked unit " .. CheckTerrorUnitNameReq .. " for terror achievement " .. ID .. ": " .. sOut);
			end
		end

		if (not Karma_PlayerIsInRaid()) then
			if (iTotal == 5) then
				KarmaChatSecondaryFallbackDefault("|cFFFF6060" .. (UnitName(CheckTerrorUnitNameReq) or CheckTerrorUnitNameReq) .. " completed For The Alliance/For The Horde. :-( |r");
			elseif (iTotal > 0) then
				KarmaChatSecondaryFallbackDefault("|cFFFFFF60" .. (UnitName(CheckTerrorUnitNameReq) or CheckTerrorUnitNameReq) .. " completed " .. (iTotal * 25) .. "% For The Alliance/For The Horde. :-| |r");
			else
				KarmaChatSecondaryFallbackDefault("|cFF60FF60" .. (UnitName(CheckTerrorUnitNameReq) or CheckTerrorUnitNameReq) .. " has not participated in For The Alliance/For The Horde. :-) |r");
			end
		end

		Karma:UnregisterEvent("INSPECT_ACHIEVEMENT_READY");
		if (AchievementFrame and AchievementFrameComparison) then
			AchievementFrameComparison:RegisterEvent("INSPECT_ACHIEVEMENT_READY");
		end

		local	sUnit = CheckTerrorUnitNameReq;
		CheckTerrorUnitNameReq = nil;
		CheckTerrorUnitIsMine = nil;

		return sUnit;
	end
end

function	KarmaObj.Achievements.CheckTerrorDone(oPartyNames)
	local	oIDs;
	if (UnitFactionGroup("player") == "Alliance") then
		oIDs = oIDAlliance;
	else
		oIDs = oIDHorde;
	end

	-- oPartyNames: name@server = oMember
	local	iTotalNo, iTotalPartial, k, v = 0, 0;
	for k, v in pairs(oPartyNames) do
		local	oState = v[KARMA_DB_L5_RRFFM_TERROR];
		if (oState and next(oState)) then
			local	iProgress, Index, ID = 0;
			for Index, ID in pairs(oIDs) do
				if (oState[ID]) then
					iProgress = iProgress + 1;
				end
			end
			if (iProgress == 5) then
				iTotalPartial = iTotalPartial + 1;
			elseif (iProgress > 0) then
				iTotalPartial = iTotalPartial + iProgress / 4;
			else
				iTotalNo = iTotalNo + 1;
			end
		else
			iTotalNo = iTotalNo + 1;
		end
	end

	local	iNum = GfTt.GetNumRaidMembersTf() - 1;
	if (iNum > 0) then
		local	iTerrorists = iNum - iTotalNo;
		if (iTerrorists > 0) then
			KarmaChatSecondaryFallbackDefault("Mob players in this raid: " .. iTerrorists .. " averaging a cowardice level of " .. format("%.2d", 100 * iTotalPartial / iTerrorists) .. "%.");
		end
	end
end