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