· 8 years ago · Mar 25, 2018, 09:08 PM
1local frame = CreateFrame("Frame");
2frame:RegisterEvent("ADDON_LOADED");
3frame:RegisterEvent("PLAYER_ALIVE");
4frame:RegisterEvent("GROUP_ROSTER_UPDATE");
5frame:RegisterEvent("PLAYER_REGEN_ENABLED");
6frame:RegisterEvent("PLAYER_ENTERING_WORLD");
7
8-- Slash Command
9SLASH_WHOTFPULLED1 = "/pullreport";
10
11-- Cache frequently used globals in locals.
12local strfind = string.find;
13local bitband = bit.band;
14
15local debug = false;
16local bossOnly = true;
17local segmentID;
18local isBossCache, deadGUIDs, pulledGUIDs, petOwners, activeNamePlates, cleuHistory = {}, {}, {}, {}, {}, {};
19local pullMsgColor, npcPullMsgColor = "FF6600", "B30000";
20local classColors = {
21 ["DEATHKNIGHT"] = "C41F3B",
22 ["DEMONHUNTER"] = "A330C9",
23 ["DRUID"] = "FF7D0A",
24 ["HUNTER"] = "ABD473",
25 ["MAGE"] = "69CCF0",
26 ["MONK"] = "00FF96",
27 ["PALADIN"] = "F58CBA",
28 ["PRIEST"] = "FFFFFF",
29 ["ROGUE"] = "FFF569",
30 ["SHAMAN"] = "0070DE",
31 ["WARLOCK"] = "9482C9",
32 ["WARRIOR"] = "C79C6E"
33};
34local ignoredSpells = {
35 -- Player spells
36 [51399] = true, -- Death Grip
37 [49576] = true, -- Death Grip
38 [217832] = true, -- Imprison
39 [48438] = true, -- Wild Growth (needs testing)
40 [185987] = true, -- Hunter's Mark
41 [185365] = true, -- Hunter's Mark
42 [1462] = true, -- Best Lore
43 [1543] = true, -- Flare
44 [132950] = true, -- Flare
45 [32599] = true, -- Soothe
46 [32641] = true, -- Soothe
47 [35352] = true, -- Soothe
48 [137619] = true, -- Marked for Death
49 [140149] = true, -- Marked for Death
50 [190529] = true, -- Marked for Death
51 [36554] = true, -- Shadowstep
52 [2094] = true, -- Blind
53 [6770] = true, -- Sap
54 [51514] = true, -- Hex
55 [196942] = true, -- Hex
56 [210873] = true, -- Hex
57 [211004] = true, -- Hex
58 [211010] = true, -- Hex
59 [211015] = true, -- Hex
60 -- Pet spells
61 [212468] = true -- Hook (Abomination pet)
62};
63
64local function getClassColor(playerGUID)
65 local _, classID = GetPlayerInfoByGUID(playerGUID);
66 return classColors[classID];
67end
68
69local function bossExists()
70 for i = 1, MAX_BOSS_FRAMES do
71 if UnitExists("boss" .. i) then
72 return true;
73 end
74 end
75 return false;
76end
77
78local function isBoss(npcGUID)
79 if isBossCache[npcGUID] ~= nil then
80 return isBossCache[npcGUID];
81 end
82 local _, _, _, _, _, mobID = strsplit("-", npcGUID);
83 if LibStub("LibBossIDs-1.0").BossIDs[tonumber(mobID)] then
84 isBossCache[npcGUID] = true;
85 return true;
86 end
87 isBossCache[npcGUID] = false;
88 return false;
89end
90
91local function getPetOwnerInfo(petGUID)
92 if petGUID then
93 if petOwners[petGUID] then
94 return petOwners[petGUID].ownerGUID, petOwners[petGUID].ownerName;
95 end
96 if UnitInRaid("player") then
97 for i = 1, 40 do
98 if UnitGUID("raidpet" .. i) == petGUID then
99 return UnitGUID("raid" .. i), UnitName("raid" .. i);
100 end
101 end
102 else -- Player is alone or in party.
103 if UnitGUID("pet") == petGUID then -- Access to player's pet if alone or in a party the same way.
104 return UnitGUID("player"), UnitName("player");
105 end
106 for i = 1, 4 do -- 4 members of the group.
107 if UnitGUID("partypet" .. i) == petGUID then
108 return UnitGUID("party" .. i), UnitName("party" .. i);
109 end
110 end
111 end
112 end
113end
114
115local function newPullMsgHistoryEntry(msg)
116 if not WHOTFPULLED_MSG then
117 WHOTFPULLED_MSG = {};
118 end
119 if not WHOTFPULLED_MSG[segmentID] then
120 WHOTFPULLED_MSG[segmentID] = {};
121 end
122 table.insert(WHOTFPULLED_MSG[segmentID], msg);
123end
124
125local function newCleuHistoryEntry(event, isPet, isBodyPull, npcGUID, srcGUID, srcName, spellLink, dmg)
126 if not cleuHistory[npcGUID] then
127 cleuHistory[npcGUID] = {};
128 cleuHistory[npcGUID].idMax = 0;
129 end
130 local newLogID = cleuHistory[npcGUID].idMax + 1;
131 if newLogID == 1 then
132 cleuHistory[npcGUID][newLogID] = {};
133 cleuHistory[npcGUID].idMax = cleuHistory[npcGUID].idMax + 1;
134 cleuHistory[npcGUID][newLogID].isPet = isPet;
135 cleuHistory[npcGUID][newLogID].isBodyPull = isBodyPull;
136 cleuHistory[npcGUID][newLogID].srcGUID = srcGUID;
137 cleuHistory[npcGUID][newLogID].srcName = srcName;
138 if isPet then
139 local petOwnerGUID, petOwnerName = getPetOwnerInfo(srcGUID);
140 cleuHistory[npcGUID][newLogID].petOwnerGUID = petOwnerGUID;
141 cleuHistory[npcGUID][newLogID].petOwnerName = petOwnerName;
142 end
143 if not isBodyPull then
144 cleuHistory[npcGUID][newLogID].spellLink = spellLink;
145 cleuHistory[npcGUID][newLogID].dmg = dmg;
146 end
147 elseif strfind(event, "_DAMAGE") and not isBodyPull then
148 cleuHistory[npcGUID][newLogID] = {};
149 cleuHistory[npcGUID].idMax = cleuHistory[npcGUID].idMax + 1;
150 cleuHistory[npcGUID][newLogID].dmg = dmg;
151 end
152end
153
154local function getTotalDamageDealt(npcGUID)
155 local totalDmgDealt = 0;
156 for id = 1, cleuHistory[npcGUID].idMax do
157 if id == 1 then
158 if not cleuHistory[npcGUID][id].isBodyPull then
159 totalDmgDealt = totalDmgDealt + cleuHistory[npcGUID][id].dmg;
160 end
161 else
162 totalDmgDealt = totalDmgDealt + cleuHistory[npcGUID][id].dmg;
163 end
164 end
165 return totalDmgDealt;
166end
167
168local function getNpcHealthInfo(npcGUID)
169 if npcGUID then
170 for namePlate in pairs(activeNamePlates) do
171 if UnitGUID(namePlate) == npcGUID then
172 return UnitHealth(namePlate), UnitHealthMax(namePlate);
173 end
174 end
175 if UnitInRaid("player") then
176 for i = 1, 40 do
177 if UnitGUID("raid" .. i .. "target") == npcGUID then
178 return UnitHealth("raid" .. i .. "target"), UnitHealthMax("raid" .. i .. "target");
179 elseif UnitGUID("raidpet" .. i .. "target") == npcGUID then
180 return UnitHealth("raidpet" .. i .. "target"), UnitHealthMax("raidpet" .. i .. "target");
181 end
182 end
183 else
184 if UnitGUID("playertarget") == npcGUID then
185 return UnitHealth("playertarget"), UnitHealthMax("playertarget");
186 end
187 for i = 1, 4 do
188 if UnitGUID("party" .. i .. "target") == npcGUID then
189 return UnitHealth("party" .. i .. "target"), UnitHealthMax("party" .. i .. "target");
190 elseif UnitGUID("partypet" .. i .. "target") == npcGUID then
191 return UnitHealth("partypet" .. i .. "target"), UnitHealthMax("partypet" .. i .. "target");
192 end
193 end
194 end
195 for i = 1, MAX_BOSS_FRAMES do
196 if UnitGUID("boss" .. i) == npcGUID then
197 return UnitHealth("boss" .. i), UnitHealthMax("boss" .. i);
198 end
199 end
200 end
201end
202
203local function setColor(msg, color)
204 if not color then
205 return msg;
206 end
207 return string.format("|cFF" .. color .. "%s|r", msg);
208end
209
210local function buildPullMsg(npcGUID, npcName)
211 local msg;
212 local isPet = cleuHistory[npcGUID][1].isPet;
213 local isBodyPull = cleuHistory[npcGUID][1].isBodyPull;
214 local unitName = cleuHistory[npcGUID][1].srcName;
215 local coloredNpcName = setColor(npcName, npcPullMsgColor);
216 if isPet then -- unitName == petName
217 local petOwnerGUID = cleuHistory[npcGUID][1].petOwnerGUID;
218 local petOwnerName = cleuHistory[npcGUID][1].petOwnerName;
219 if petOwnerName then
220 local coloredPetOwnerName = setColor(petOwnerName, getClassColor(petOwnerGUID));
221 if isBodyPull then
222 msg = string.format("%s's %s body pulled %s.", coloredPetOwnerName, unitName, coloredNpcName);
223 newPullMsgHistoryEntry(string.format("%s's %s body pulled %s.", petOwnerName, unitName, npcName));
224 else
225 local spellLink = cleuHistory[npcGUID][1].spellLink;
226 msg = string.format("%s's %s pulled %s with " .. spellLink .. ".", coloredPetOwnerName, unitName, coloredNpcName);
227 newPullMsgHistoryEntry(string.format("%s's %s pulled %s with " .. spellLink .. ".", petOwnerName, unitName, npcName));
228 end
229 else
230 if isBodyPull then
231 msg = string.format("%s (pet) body pulled %s.", unitName, coloredNpcName);
232 newPullMsgHistoryEntry(string.format("%s (pet) body pulled %s.", unitName, npcName));
233 else
234 local spellLink = cleuHistory[npcGUID][1].spellLink;
235 msg = string.format("%s (pet) pulled %s with " .. spellLink .. ".", unitName, coloredNpcName);
236 newPullMsgHistoryEntry(string.format("%s (pet) pulled %s with " .. spellLink .. ".", unitName, npcName));
237 end
238 end
239 else -- unitName == playerName
240 local unitGUID = cleuHistory[npcGUID][1].srcGUID;
241 local coloredUnitName = setColor(unitName, getClassColor(unitGUID));
242 if isBodyPull then
243 msg = string.format("%s body pulled %s.", coloredUnitName, coloredNpcName);
244 newPullMsgHistoryEntry(string.format("%s body pulled %s.", unitName, npcName));
245 else
246 local spellLink = cleuHistory[npcGUID][1].spellLink;
247 msg = string.format("%s pulled %s with " .. spellLink .. ".", coloredUnitName, coloredNpcName);
248 newPullMsgHistoryEntry(string.format("%s pulled %s with " .. spellLink .. ".", unitName, npcName));
249 end
250 end
251 return msg;
252end
253
254local function hexToRgbPercent(hex)
255 local hex = hex:gsub("#", "");
256 if hex:len() == 3 then
257 return (tonumber("0x" .. hex:sub(1, 1)) * 17) / 255, (tonumber("0x" .. hex:sub(2, 2)) * 17) / 255, (tonumber("0x" .. hex:sub(3, 3)) * 17) / 255;
258 else
259 return tonumber("0x" .. hex:sub(1, 2)) / 255, tonumber("0x" .. hex:sub(3, 4)) / 255, tonumber("0x" .. hex:sub(5, 6)) / 255;
260 end
261end
262
263local function printAllChatFrames(msg, color)
264 if msg then
265 local r, g, b = hexToRgbPercent(color);
266 for i = 1, #CHAT_FRAMES do
267 local chatFrame = _G["ChatFrame" .. i];
268 local _, _, _, _, _, _, shown, _, docked = FCF_GetChatWindowInfo(i);
269 if chatFrame and (shown or docked) then
270 if not IsCombatLog(chatFrame) then
271 chatFrame:AddMessage(msg, r, g, b);
272 end
273 end
274 end
275 end
276end
277
278local function isPull(event, isPet, isBodyPull, npcGUID, unitID, unitName, spellID, dmg)
279 if not pulledGUIDs[npcGUID] then
280 spellID = strfind(event, "SWING") and 6603 or spellID;
281 if not deadGUIDs[npcGUID] and (not ignoredSpells[spellID] or isBodyPull) then
282 if isBoss(npcGUID) or not bossOnly then
283 newCleuHistoryEntry(event, isPet, isBodyPull, npcGUID, unitID, unitName, GetSpellLink(spellID), dmg);
284 local npcHealth, npcHealthMax = getNpcHealthInfo(npcGUID);
285 if npcHealth and npcHealthMax and npcHealth ~= 0 and npcHealthMax ~= 0 then
286 pulledGUIDs[npcGUID] = true;
287 local totalDmgDealt = getTotalDamageDealt(npcGUID);
288 if totalDmgDealt + npcHealth >= npcHealthMax then
289 return true;
290 end
291 end
292 end
293 end
294 end
295 return false;
296end
297
298function frame:ADDON_LOADED(addonName)
299 if addonName == "WhoTFPulled" then
300 if not (type(WHOTFPULLED_MSG) == "table") then
301 WHOTFPULLED_MSG = {};
302 end
303 segmentID = #WHOTFPULLED_MSG + 1;
304 if debug then
305 self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
306 self:RegisterEvent("NAME_PLATE_UNIT_ADDED");
307 self:RegisterEvent("NAME_PLATE_UNIT_REMOVED");
308 self:RegisterEvent("FORBIDDEN_NAME_PLATE_UNIT_ADDED");
309 self:RegisterEvent("FORBIDDEN_NAME_PLATE_UNIT_REMOVED");
310 end
311 end
312end
313
314function frame:PLAYER_ENTERING_WORLD()
315 if not debug then
316 local inInstance, instanceType = IsInInstance();
317 if inInstance and (instanceType == "party" or instanceType == "raid") then
318 if GetNumGroupMembers(LE_PARTY_CATEGORY_HOME) > 0 or GetNumGroupMembers(LE_PARTY_CATEGORY_INSTANCE) > 0 then
319 if not self:IsEventRegistered("COMBAT_LOG_EVENT_UNFILTERED") then
320 self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
321 self:RegisterEvent("NAME_PLATE_UNIT_ADDED");
322 self:RegisterEvent("NAME_PLATE_UNIT_REMOVED");
323 self:RegisterEvent("FORBIDDEN_NAME_PLATE_UNIT_ADDED");
324 self:RegisterEvent("FORBIDDEN_NAME_PLATE_UNIT_REMOVED");
325 end
326 end
327 else
328 if self:IsEventRegistered("COMBAT_LOG_EVENT_UNFILTERED") then
329 self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
330 self:UnregisterEvent("NAME_PLATE_UNIT_ADDED");
331 self:UnregisterEvent("NAME_PLATE_UNIT_REMOVED");
332 self:UnregisterEvent("FORBIDDEN_NAME_PLATE_UNIT_ADDED");
333 self:UnregisterEvent("FORBIDDEN_NAME_PLATE_UNIT_REMOVED");
334 end
335 end
336 end
337end
338
339function frame:GROUP_ROSTER_UPDATE()
340 if not debug then
341 if not (GetNumGroupMembers(LE_PARTY_CATEGORY_HOME) > 0) and not (GetNumGroupMembers(LE_PARTY_CATEGORY_INSTANCE) > 0) then
342 wipe(isBossCache);
343 wipe(deadGUIDs);
344 wipe(petOwners);
345 wipe(WHOTFPULLED_MSG);
346 if self:IsEventRegistered("COMBAT_LOG_EVENT_UNFILTERED") then
347 self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
348 self:UnregisterEvent("NAME_PLATE_UNIT_ADDED");
349 self:UnregisterEvent("NAME_PLATE_UNIT_REMOVED");
350 self:UnregisterEvent("FORBIDDEN_NAME_PLATE_UNIT_ADDED");
351 self:UnregisterEvent("FORBIDDEN_NAME_PLATE_UNIT_REMOVED");
352 end
353 end
354 end
355end
356
357function frame:COMBAT_LOG_EVENT_UNFILTERED(...) -- Called only inside PVE instances whithin a group.
358 local _, event, _, srcGUID, srcName, srcFlags, _, destGUID, destName, destFlags, _, spellID = ...;
359 if event == "UNIT_DIED" then
360 deadGUIDs[destGUID] = true;
361 isBossCache[destGUID] = nil;
362 return;
363 end
364 if strfind(event, "_SUMMON") then -- Cache pet owners.
365 petOwners[destGUID] = {};
366 petOwners[destGUID].ownerGUID = srcGUID;
367 petOwners[destGUID].ownerName = srcName;
368 return;
369 end
370 if srcName and destName then
371 if srcName ~= destName then
372 if strfind(event, "SPELL") or strfind(event, "SWING") or strfind(event, "RANGE") then -- Check Prefix
373 if not strfind(event, "_RESURRECT") and not strfind(event, "_CREATE") then -- Check Suffix
374 -- A player is attacking a mob. (isPet = false; isBodyPull = false)
375 if bitband(srcFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0 and bitband(destFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0 then
376 local dmg = strfind(event, "_DAMAGE") and (strfind(event, "SWING") and select(12, ...) or select(15, ...)) or 0;
377 if isPull(event, false, false, destGUID, srcGUID, srcName, spellID, dmg) then
378 printAllChatFrames(buildPullMsg(destGUID, destName), pullMsgColor);
379 end
380 -- A player's pet attacks a mob. (isPet = true; isBodyPull = false)
381 elseif bitband(srcFlags, COMBATLOG_OBJECT_CONTROL_PLAYER) ~= 0 and bitband(destFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0 then
382 local dmg = strfind(event, "_DAMAGE") and (strfind(event, "SWING") and select(12, ...) or select(15, ...)) or 0;
383 if isPull(event, true, false, destGUID, srcGUID, srcName, spellID, dmg) then
384 printAllChatFrames(buildPullMsg(destGUID, destName), pullMsgColor);
385 end
386 -- A mob is attacking a player. (isPet = false; isBodyPull = true)
387 elseif bitband(destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0 and bitband(srcFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0 then
388 local dmg = strfind(event, "_DAMAGE") and (strfind(event, "SWING") and select(12, ...) or select(15, ...)) or 0;
389 if isPull(event, false, true, srcGUID, destGUID, destName) then
390 printAllChatFrames(buildPullMsg(srcGUID, srcName), pullMsgColor);
391 end
392 -- A mob attacks a player's pet. (isPet = true; isBodyPull = true)
393 elseif bitband(destFlags, COMBATLOG_OBJECT_CONTROL_PLAYER) ~= 0 and bitband(srcFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0 then
394 local dmg = strfind(event, "_DAMAGE") and (strfind(event, "SWING") and select(12, ...) or select(15, ...)) or 0;
395 if isPull(event, true, true, srcGUID, destGUID, destName) then
396 printAllChatFrames(buildPullMsg(srcGUID, srcName), pullMsgColor);
397 end
398 end
399 end
400 end
401 end
402 end
403end
404
405function frame:PLAYER_REGEN_ENABLED()
406 -- Event also fires on death and feigth death.
407 if not UnitIsDeadOrGhost("player") and not UnitIsFeignDeath("player") and not UnitAffectingCombat("pet") then
408 -- Edge case where you could go out of combat in a middle of a boss fight.
409 if not bossExists() and not IsEncounterInProgress() then
410 wipe(cleuHistory);
411 wipe(pulledGUIDs);
412 segmentID = (WHOTFPULLED_MSG and (#WHOTFPULLED_MSG + 1)) or 1;
413 end
414 end
415end
416
417function frame:PLAYER_ALIVE()
418 if not bossExists() and not IsEncounterInProgress() and not InCombatLockdown() and not UnitAffectingCombat("player") then
419 wipe(cleuHistory);
420 wipe(pulledGUIDs);
421 segmentID = (WHOTFPULLED_MSG and (#WHOTFPULLED_MSG + 1)) or 1;
422 end
423end
424
425function frame:NAME_PLATE_UNIT_ADDED(namePlate)
426 activeNamePlates[namePlate] = true;
427end
428
429function frame:NAME_PLATE_UNIT_REMOVED(namePlate)
430 activeNamePlates[namePlate] = nil;
431end
432
433-- Create aliases to avoid duplicating those functions.
434frame.FORBIDDEN_NAME_PLATE_UNIT_ADDED = frame.NAME_PLATE_UNIT_ADDED;
435frame.FORBIDDEN_NAME_PLATE_UNIT_REMOVED = frame.NAME_PLATE_UNIT_REMOVED;
436
437function SlashCmdList.WHOTFPULLED(msg, editbox)
438 if WHOTFPULLED_MSG and WHOTFPULLED_MSG[#WHOTFPULLED_MSG] then
439 local chatType = (UnitInRaid("player") and "RAID") or (UnitInParty("player") and "PARTY") or "SAY";
440 SendChatMessage("WhoTFPulled report:", chatType);
441 for i = 1, #WHOTFPULLED_MSG[#WHOTFPULLED_MSG] do
442 SendChatMessage(WHOTFPULLED_MSG[#WHOTFPULLED_MSG][i], chatType);
443 end
444 else
445 printAllChatFrames("No pull information available.", pullMsgColor);
446 end
447end
448
449frame:SetScript("OnEvent", function(self, event, ...)
450 return self[event] and self[event](self, ...); -- Automatically call the method for this event, if it exists.
451end);