comparison Main.lua @ 393:7d0ad2573092

Added support for collecting loot from item containers with no loot window and no spells cast.
author MMOSimca <MMOSimca@gmail.com>
date Thu, 18 Dec 2014 21:38:11 -0500
parents b00732fa9352
children 930da8078de7
comparison
equal deleted inserted replaced
392:f1952ed33a16 393:7d0ad2573092
191 y = nil, 191 y = nil,
192 zone_data = nil, 192 zone_data = nil,
193 } 193 }
194 194
195 195
196 -- Timer prototypes
197 local ClearKilledNPC, ClearKilledBossID, ClearLootToastContainerID, ClearLootToastData, ClearChatLootData
198
199
196 -- HELPERS ------------------------------------------------------------ 200 -- HELPERS ------------------------------------------------------------
197 201
198 local function Debug(message, ...) 202 local function Debug(message, ...)
199 if not DEBUGGING or not message then 203 if not DEBUGGING or not message then
200 return 204 return
210 else 214 else
211 _G.print(message) 215 _G.print(message)
212 end 216 end
213 end 217 end
214 218
215
216 local TradeSkillExecutePer
217 do
218 local header_list = {}
219
220 function TradeSkillExecutePer(iter_func)
221 if not _G.TradeSkillFrame or not _G.TradeSkillFrame:IsVisible() then
222 return
223 end
224 -- Clear the search box focus so the scan will have correct results.
225 local search_box = _G.TradeSkillFrameSearchBox
226 search_box:SetText("")
227
228 _G.TradeSkillSearch_OnTextChanged(search_box)
229 search_box:ClearFocus()
230 search_box:GetScript("OnEditFocusLost")(search_box)
231
232 table.wipe(header_list)
233
234 -- Save the current state of the TradeSkillFrame so it can be restored after we muck with it.
235 local have_materials = _G.TradeSkillFrame.filterTbl.hasMaterials
236 local have_skillup = _G.TradeSkillFrame.filterTbl.hasSkillUp
237
238 if have_materials then
239 _G.TradeSkillFrame.filterTbl.hasMaterials = false
240 _G.TradeSkillOnlyShowMakeable(false)
241 end
242
243 if have_skillup then
244 _G.TradeSkillFrame.filterTbl.hasSkillUp = false
245 _G.TradeSkillOnlyShowSkillUps(false)
246 end
247 _G.SetTradeSkillInvSlotFilter(0, true, true)
248 _G.TradeSkillUpdateFilterBar()
249 _G.TradeSkillFrame_Update()
250
251 -- Expand all headers so we can see all the recipes there are
252 for tradeskill_index = 1, _G.GetNumTradeSkills() do
253 local name, tradeskill_type, _, is_expanded = _G.GetTradeSkillInfo(tradeskill_index)
254
255 if tradeskill_type == "header" or tradeskill_type == "subheader" then
256 if not is_expanded then
257 header_list[name] = true
258 _G.ExpandTradeSkillSubClass(tradeskill_index)
259 end
260 elseif iter_func(name, tradeskill_index) then
261 break
262 end
263 end
264
265 -- Restore the state of the things we changed.
266 for tradeskill_index = 1, _G.GetNumTradeSkills() do
267 local name, tradeskill_type, _, is_expanded = _G.GetTradeSkillInfo(tradeskill_index)
268
269 if header_list[name] then
270 _G.CollapseTradeSkillSubClass(tradeskill_index)
271 end
272 end
273 _G.TradeSkillFrame.filterTbl.hasMaterials = have_materials
274 _G.TradeSkillOnlyShowMakeable(have_materials)
275 _G.TradeSkillFrame.filterTbl.hasSkillUp = have_skillup
276 _G.TradeSkillOnlyShowSkillUps(have_skillup)
277
278 _G.TradeSkillUpdateFilterBar()
279 _G.TradeSkillFrame_Update()
280 end
281 end -- do-block
282
283
284 local ActualCopperCost
285 do
286 local BARTERING_SPELL_ID = 83964
287
288 local STANDING_DISCOUNTS = {
289 HATED = 0,
290 HOSTILE = 0,
291 UNFRIENDLY = 0,
292 NEUTRAL = 0,
293 FRIENDLY = 0.05,
294 HONORED = 0.1,
295 REVERED = 0.15,
296 EXALTED = 0.2,
297 }
298
299
300 function ActualCopperCost(copper_cost, rep_standing)
301 if not copper_cost or copper_cost == 0 then
302 return 0
303 end
304 local modifier = 1
305
306 if _G.IsSpellKnown(BARTERING_SPELL_ID) then
307 modifier = modifier - 0.1
308 end
309
310 if rep_standing then
311 if PLAYER_RACE == "Goblin" then
312 modifier = modifier - STANDING_DISCOUNTS["EXALTED"]
313 elseif STANDING_DISCOUNTS[rep_standing] then
314 modifier = modifier - STANDING_DISCOUNTS[rep_standing]
315 end
316 end
317 return math.floor(copper_cost / modifier)
318 end
319 end -- do-block
320
321
322 local function InstanceDifficultyToken()
323 local _, instance_type, instance_difficulty, _, _, _, is_dynamic = _G.GetInstanceInfo()
324
325 if not instance_type or instance_type == "" then
326 instance_type = "NONE"
327 end
328 return ("%s:%d:%s"):format(instance_type:upper(), instance_difficulty, tostring(is_dynamic))
329 end
330
331
332 local function DBEntry(data_type, unit_id)
333 if not data_type or not unit_id then
334 return
335 end
336 local category = global_db[data_type]
337
338 if not category then
339 category = {}
340 global_db[data_type] = category
341 end
342 local unit = category[unit_id]
343
344 if not unit then
345 unit = {}
346 category[unit_id] = unit
347 end
348 return unit
349 end
350
351 private.DBEntry = DBEntry
352
353 local NPCEntry
354 do
355 local npc_prototype = {}
356 local npc_meta = {
357 __index = npc_prototype
358 }
359
360 function NPCEntry(identifier)
361 local npc = DBEntry("npcs", identifier)
362 return npc and _G.setmetatable(npc, npc_meta) or nil
363 end
364
365 function npc_prototype:EncounterData(difficulty_token)
366 self.encounter_data = self.encounter_data or {}
367 self.encounter_data[difficulty_token] = self.encounter_data[difficulty_token] or {}
368 self.encounter_data[difficulty_token].stats = self.encounter_data[difficulty_token].stats or {}
369
370 return self.encounter_data[difficulty_token]
371 end
372 end
373
374
375 local function CurrentLocationData()
376 if _G.GetCurrentMapAreaID() ~= current_area_id then
377 return _G.GetRealZoneText(), current_area_id, 0, 0, 0, InstanceDifficultyToken()
378 end
379 local map_level = _G.GetCurrentMapDungeonLevel() or 0
380 local x, y = _G.GetPlayerMapPosition("player")
381
382 x = x or 0
383 y = y or 0
384
385 if x == 0 and y == 0 then
386 for level_index = 1, _G.GetNumDungeonMapLevels() do
387 _G.SetDungeonMapLevel(level_index)
388 x, y = _G.GetPlayerMapPosition("player")
389
390 if x and y and (x > 0 or y > 0) then
391 _G.SetDungeonMapLevel(map_level)
392 map_level = level_index
393 break
394 end
395 end
396 end
397
398 if _G.DungeonUsesTerrainMap() then
399 map_level = map_level - 1
400 end
401 local x = _G.floor(x * 1000)
402 local y = _G.floor(y * 1000)
403
404 if x % 2 ~= 0 then
405 x = x + 1
406 end
407
408 if y % 2 ~= 0 then
409 y = y + 1
410 end
411 return _G.GetRealZoneText(), current_area_id, x, y, map_level, InstanceDifficultyToken()
412 end
413
414
415 local function CurrencyLinkToTexture(currency_link)
416 if not currency_link then
417 return
418 end
419 local _, _, texture_path = _G.GetCurrencyInfo(tonumber(currency_link:match("currency:(%d+)")))
420 return texture_path:match("[^\\]+$"):lower()
421 end
422
423
424 local function ItemLinkToID(item_link)
425 if not item_link then
426 return
427 end
428 return tonumber(item_link:match("item:(%d+)"))
429 end
430
431 private.ItemLinkToID = ItemLinkToID
432
433 local function UnitTypeIsNPC(unit_type)
434 return unit_type == private.UNIT_TYPES.NPC or unit_type == private.UNIT_TYPES.VEHICLE
435 end
436
437
438 local ParseGUID
439 do
440 local UNIT_TYPES = private.UNIT_TYPES
441
442 local NPC_ID_MAPPING = {
443 [62164] = 63191, -- Garalon
444 }
445
446
447 local function MatchUnitTypes(unit_type_name)
448 if not unit_type_name then
449 return UNIT_TYPES.UNKNOWN
450 end
451
452 for def, text in next, UNIT_TYPES do
453 if unit_type_name == text then
454 return UNIT_TYPES[def]
455 end
456 end
457 return UNIT_TYPES.UNKNOWN
458 end
459
460
461 function ParseGUID(guid)
462 if not guid then
463 return
464 end
465
466 -- We might want to use some of this new information later, but leaving the returns alone for now
467 local unit_type_name, unk_id1, server_id, instance_id, unk_id2, unit_idnum, spawn_id = ("-"):split(guid)
468
469 local unit_type = MatchUnitTypes(unit_type_name)
470 if unit_type ~= UNIT_TYPES.PLAYER and unit_type ~= UNIT_TYPES.PET and unit_type ~= UNIT_TYPES.ITEM then
471
472 local id_mapping = NPC_ID_MAPPING[unit_idnum]
473
474 if id_mapping and UnitTypeIsNPC(unit_type) then
475 unit_idnum = id_mapping
476 end
477 return unit_type, unit_idnum
478 end
479 return unit_type
480 end
481
482 private.ParseGUID = ParseGUID
483 end -- do-block
484
485
486 local UpdateDBEntryLocation
487 do
488 -- Fishing node coordinate code based on code in GatherMate2 with permission from Kagaro.
489 local function FishingCoordinates(x, y, yard_width, yard_height)
490 local facing = _G.GetPlayerFacing()
491
492 if not facing then
493 return x, y
494 end
495 local rad = facing + math.pi
496 return x + math.sin(rad) * 15 / yard_width, y + math.cos(rad) * 15 / yard_height
497 end
498
499
500 function UpdateDBEntryLocation(entry_type, identifier)
501 if not identifier then
502 return
503 end
504 local zone_name, area_id, x, y, map_level, difficulty_token = CurrentLocationData()
505 if not (zone_name and area_id and x and y and map_level) then
506 Debug("UpdateDBEntryLocation: Missing current location data - %s, %d, %d, %d, %d.", zone_name, area_id, x, y, map_level)
507 return
508 end
509 local entry = DBEntry(entry_type, identifier)
510 entry[difficulty_token] = entry[difficulty_token] or {}
511 entry[difficulty_token].locations = entry[difficulty_token].locations or {}
512
513 local zone_token = ("%s:%d"):format(zone_name, area_id)
514 local zone_data = entry[difficulty_token].locations[zone_token]
515
516 if not zone_data then
517 zone_data = {}
518 entry[difficulty_token].locations[zone_token] = zone_data
519 end
520
521 -- Special case for Fishing.
522 if current_action.spell_label == "FISHING" then
523 local yard_width, yard_height = MapData:MapArea(area_id, map_level)
524
525 if yard_width > 0 and yard_height > 0 then
526 x, y = FishingCoordinates(x, y, yard_width, yard_height)
527 current_action.x = x
528 current_action.y = y
529 end
530 end
531 local location_token = ("%d:%d:%d"):format(map_level, x, y)
532
533 zone_data[location_token] = zone_data[location_token] or true
534 return zone_data
535 end
536 end -- do-block
537
538
539 local function HandleItemUse(item_link, bag_index, slot_index)
540 if not item_link then
541 return
542 end
543 local item_id = ItemLinkToID(item_link)
544
545 if not bag_index or not slot_index then
546 for new_bag_index = 0, _G.NUM_BAG_FRAMES do
547 for new_slot_index = 1, _G.GetContainerNumSlots(new_bag_index) do
548 if item_id == ItemLinkToID(_G.GetContainerItemLink(new_bag_index, new_slot_index)) then
549 bag_index = new_bag_index
550 slot_index = new_slot_index
551 break
552 end
553 end
554 end
555 end
556
557 if not bag_index or not slot_index then
558 return
559 end
560 local _, _, _, _, _, is_lootable = _G.GetContainerItemInfo(bag_index, slot_index)
561
562 if not is_lootable then
563 return
564 end
565
566 table.wipe(current_action)
567 current_loot = nil
568 current_action.target_type = AF.ITEM
569 current_action.identifier = item_id
570 current_action.loot_label = "contains"
571
572 --[[DatamineTT:ClearLines()
573 DatamineTT:SetBagItem(bag_index, slot_index)
574
575 for line_index = 1, DatamineTT:NumLines() do
576 local current_line = _G["WDPDatamineTTTextLeft" .. line_index]
577
578 if not current_line then
579 Debug("HandleItemUse: Item with ID %d and link %s had an invalid tooltip.", item_id, item_link)
580 return
581 end
582
583 if current_line:GetText() == _G.ITEM_OPENABLE then
584 table.wipe(current_action)
585 current_loot = nil
586
587 current_action.target_type = AF.ITEM
588 current_action.identifier = item_id
589 current_action.loot_label = "contains"
590 return
591 end
592 end
593 Debug("HandleItemUse: Item with ID %d and link %s did not have a tooltip that contained the string %s.", item_id, item_link, _G.ITEM_OPENABLE)]]--
594 end
595
596
597 local UnitFactionStanding
598 local UpdateFactionData
599 do
600 local MAX_FACTION_INDEX = 1000
601
602 local STANDING_NAMES = {
603 "HATED",
604 "HOSTILE",
605 "UNFRIENDLY",
606 "NEUTRAL",
607 "FRIENDLY",
608 "HONORED",
609 "REVERED",
610 "EXALTED",
611 }
612
613
614 function UnitFactionStanding(unit)
615 local unit_name = _G.UnitName(unit)
616 UpdateFactionData()
617 DatamineTT:ClearLines()
618 DatamineTT:SetUnit(unit)
619
620 for line_index = 1, DatamineTT:NumLines() do
621 local faction_name = _G["WDPDatamineTTTextLeft" .. line_index]:GetText():trim()
622
623 if faction_name and faction_name ~= unit_name and faction_standings[faction_name] then
624 return faction_name, faction_standings[faction_name]
625 end
626 end
627 end
628
629
630 function UpdateFactionData()
631 for faction_index = 1, MAX_FACTION_INDEX do
632 local faction_name, _, current_standing, _, _, _, _, _, is_header = _G.GetFactionInfo(faction_index)
633
634 if faction_name then
635 faction_standings[faction_name] = STANDING_NAMES[current_standing]
636 elseif not faction_name then
637 break
638 end
639 end
640 end
641 end -- do-block
642
643
644 local GenericLootUpdate
645 do
646 local function LootTable(entry, loot_type, top_field)
647 if top_field then
648 entry[top_field] = entry[top_field] or {}
649 entry[top_field][loot_type] = entry[top_field][loot_type] or {}
650 return entry[top_field][loot_type]
651 end
652 entry[loot_type] = entry[loot_type] or {}
653 return entry[loot_type]
654 end
655
656 function GenericLootUpdate(data_type, top_field)
657 local loot_type = current_loot.label
658 local loot_count = ("%s_count"):format(loot_type)
659 local source_list = {}
660
661 if current_loot.sources then
662 for source_guid, loot_data in pairs(current_loot.sources) do
663 local source_id
664
665 if current_loot.target_type == AF.ITEM then
666 -- Items return the player as the source, so we need to use the item's ID (disenchant, milling, etc)
667 source_id = current_loot.identifier
668 else
669 local _, unit_ID = ParseGUID(source_guid)
670 if unit_ID then
671 if current_loot.target_type == AF.OBJECT then
672 source_id = ("%s:%s"):format(current_loot.spell_label, unit_ID)
673 else
674 source_id = unit_ID
675 end
676 end
677 end
678 local entry = DBEntry(data_type, source_id)
679
680 if entry then
681 local loot_table = LootTable(entry, loot_type, top_field)
682
683 if not source_list[source_id] then
684 if top_field then
685 entry[top_field][loot_count] = (entry[top_field][loot_count] or 0) + 1
686 elseif not container_loot_toasting then
687 entry[loot_count] = (entry[loot_count] or 0) + 1
688 end
689 source_list[source_id] = true
690 end
691 UpdateDBEntryLocation(data_type, source_id)
692
693 if current_loot.target_type == AF.ZONE then
694 for item_id, quantity in pairs(loot_data) do
695 table.insert(loot_table, ("%d:%d"):format(item_id, quantity))
696 end
697 else
698 for loot_token, quantity in pairs(loot_data) do
699 local label, currency_texture = (":"):split(loot_token)
700
701 if label == "currency" and currency_texture then
702 table.insert(loot_table, ("currency:%d:%s"):format(quantity, currency_texture))
703 elseif loot_token == "money" then
704 table.insert(loot_table, ("money:%d"):format(quantity))
705 else
706 table.insert(loot_table, ("%d:%d"):format(loot_token, quantity))
707 end
708 end
709 end
710 end
711 end
712 end
713
714 -- This is used for Gas Extractions.
715 if #current_loot.list <= 0 then
716 return
717 end
718 local entry
719
720 -- At this point we only have a name if it's an object.
721 -- (As of 5.x, the above statement is almost never true, but there are a few cases, like gas extractions.)
722 if current_loot.target_type == AF.OBJECT then
723 entry = DBEntry(data_type, ("%s:%s"):format(current_loot.spell_label, current_loot.object_name))
724 else
725 entry = DBEntry(data_type, current_loot.identifier)
726 end
727
728 if not entry then
729 return
730 end
731 local loot_table = LootTable(entry, loot_type, top_field)
732
733 if current_loot.identifier then
734 if not source_list[current_loot.identifier] then
735 if top_field then
736 entry[top_field][loot_count] = (entry[top_field][loot_count] or 0) + 1
737 else
738 entry[loot_count] = (entry[loot_count] or 0) + 1
739 end
740 source_list[current_loot.identifier] = true
741 end
742 end
743
744 for index = 1, #current_loot.list do
745 table.insert(loot_table, current_loot.list[index])
746 end
747 end
748 end -- do-block
749
750
751 local ReplaceKeywords
752 do
753 local KEYWORD_SUBSTITUTIONS = {
754 class = PLAYER_CLASS,
755 name = PLAYER_NAME,
756 race = PLAYER_RACE,
757 }
758
759
760 function ReplaceKeywords(text)
761 if not text or text == "" then
762 return ""
763 end
764
765 for category, lookup in pairs(KEYWORD_SUBSTITUTIONS) do
766 local category_format = ("<%s>"):format(category)
767 text = text:gsub(lookup, category_format):gsub(lookup:lower(), category_format)
768 end
769 return text
770 end
771 end -- do-block
772
773
774 -- Contains a dirty hack due to Blizzard's strange handling of Micro Dungeons; GetMapInfo() will not return correct information
775 -- unless the WorldMapFrame is shown.
776 do
777 -- MapFileName = MapAreaID
778 local MICRO_DUNGEON_IDS = {
779 ShrineofTwoMoons = 903,
780 ShrineofSevenStars = 905,
781 }
782
783 local function SetCurrentAreaID()
784 if private.in_combat then
785 private.set_area_id = true
786 return
787 end
788 local map_area_id = _G.GetCurrentMapAreaID()
789
790 if map_area_id == current_area_id then
791 return
792 end
793 local world_map = _G.WorldMapFrame
794 local map_visible = world_map:IsVisible()
795 local sfx_value = tonumber(_G.GetCVar("Sound_EnableSFX"))
796
797 if not map_visible then
798 _G.SetCVar("Sound_EnableSFX", 0)
799 world_map:Show()
800 end
801 local _, _, _, _, micro_dungeon_map_name = _G.GetMapInfo()
802 local micro_dungeon_id = MICRO_DUNGEON_IDS[micro_dungeon_map_name]
803
804 _G.SetMapToCurrentZone()
805
806 if micro_dungeon_id then
807 current_area_id = micro_dungeon_id
808 else
809 current_area_id = _G.GetCurrentMapAreaID()
810 end
811
812 if map_visible then
813 _G.SetMapByID(map_area_id)
814 else
815 world_map:Hide()
816 _G.SetCVar("Sound_EnableSFX", sfx_value)
817 end
818 end
819
820 function WDP:HandleZoneChange(event_name)
821 in_instance = _G.IsInInstance()
822 SetCurrentAreaID()
823 end
824 end
825 219
826 local function InitializeCurrentLoot() 220 local function InitializeCurrentLoot()
827 current_loot = { 221 current_loot = {
828 list = {}, 222 list = {},
829 sources = {}, 223 sources = {},
840 234
841 table.wipe(current_action) 235 table.wipe(current_action)
842 end 236 end
843 237
844 238
239 local TradeSkillExecutePer
240 do
241 local header_list = {}
242
243 function TradeSkillExecutePer(iter_func)
244 if not _G.TradeSkillFrame or not _G.TradeSkillFrame:IsVisible() then
245 return
246 end
247 -- Clear the search box focus so the scan will have correct results.
248 local search_box = _G.TradeSkillFrameSearchBox
249 search_box:SetText("")
250
251 _G.TradeSkillSearch_OnTextChanged(search_box)
252 search_box:ClearFocus()
253 search_box:GetScript("OnEditFocusLost")(search_box)
254
255 table.wipe(header_list)
256
257 -- Save the current state of the TradeSkillFrame so it can be restored after we muck with it.
258 local have_materials = _G.TradeSkillFrame.filterTbl.hasMaterials
259 local have_skillup = _G.TradeSkillFrame.filterTbl.hasSkillUp
260
261 if have_materials then
262 _G.TradeSkillFrame.filterTbl.hasMaterials = false
263 _G.TradeSkillOnlyShowMakeable(false)
264 end
265
266 if have_skillup then
267 _G.TradeSkillFrame.filterTbl.hasSkillUp = false
268 _G.TradeSkillOnlyShowSkillUps(false)
269 end
270 _G.SetTradeSkillInvSlotFilter(0, true, true)
271 _G.TradeSkillUpdateFilterBar()
272 _G.TradeSkillFrame_Update()
273
274 -- Expand all headers so we can see all the recipes there are
275 for tradeskill_index = 1, _G.GetNumTradeSkills() do
276 local name, tradeskill_type, _, is_expanded = _G.GetTradeSkillInfo(tradeskill_index)
277
278 if tradeskill_type == "header" or tradeskill_type == "subheader" then
279 if not is_expanded then
280 header_list[name] = true
281 _G.ExpandTradeSkillSubClass(tradeskill_index)
282 end
283 elseif iter_func(name, tradeskill_index) then
284 break
285 end
286 end
287
288 -- Restore the state of the things we changed.
289 for tradeskill_index = 1, _G.GetNumTradeSkills() do
290 local name, tradeskill_type, _, is_expanded = _G.GetTradeSkillInfo(tradeskill_index)
291
292 if header_list[name] then
293 _G.CollapseTradeSkillSubClass(tradeskill_index)
294 end
295 end
296 _G.TradeSkillFrame.filterTbl.hasMaterials = have_materials
297 _G.TradeSkillOnlyShowMakeable(have_materials)
298 _G.TradeSkillFrame.filterTbl.hasSkillUp = have_skillup
299 _G.TradeSkillOnlyShowSkillUps(have_skillup)
300
301 _G.TradeSkillUpdateFilterBar()
302 _G.TradeSkillFrame_Update()
303 end
304 end -- do-block
305
306
307 local ActualCopperCost
308 do
309 local BARTERING_SPELL_ID = 83964
310
311 local STANDING_DISCOUNTS = {
312 HATED = 0,
313 HOSTILE = 0,
314 UNFRIENDLY = 0,
315 NEUTRAL = 0,
316 FRIENDLY = 0.05,
317 HONORED = 0.1,
318 REVERED = 0.15,
319 EXALTED = 0.2,
320 }
321
322
323 function ActualCopperCost(copper_cost, rep_standing)
324 if not copper_cost or copper_cost == 0 then
325 return 0
326 end
327 local modifier = 1
328
329 if _G.IsSpellKnown(BARTERING_SPELL_ID) then
330 modifier = modifier - 0.1
331 end
332
333 if rep_standing then
334 if PLAYER_RACE == "Goblin" then
335 modifier = modifier - STANDING_DISCOUNTS["EXALTED"]
336 elseif STANDING_DISCOUNTS[rep_standing] then
337 modifier = modifier - STANDING_DISCOUNTS[rep_standing]
338 end
339 end
340 return math.floor(copper_cost / modifier)
341 end
342 end -- do-block
343
344
345 local function InstanceDifficultyToken()
346 local _, instance_type, instance_difficulty, _, _, _, is_dynamic = _G.GetInstanceInfo()
347
348 if not instance_type or instance_type == "" then
349 instance_type = "NONE"
350 end
351 return ("%s:%d:%s"):format(instance_type:upper(), instance_difficulty, tostring(is_dynamic))
352 end
353
354
355 local function DBEntry(data_type, unit_id)
356 if not data_type or not unit_id then
357 return
358 end
359 local category = global_db[data_type]
360
361 if not category then
362 category = {}
363 global_db[data_type] = category
364 end
365 local unit = category[unit_id]
366
367 if not unit then
368 unit = {}
369 category[unit_id] = unit
370 end
371 return unit
372 end
373
374 private.DBEntry = DBEntry
375
376 local NPCEntry
377 do
378 local npc_prototype = {}
379 local npc_meta = {
380 __index = npc_prototype
381 }
382
383 function NPCEntry(identifier)
384 local npc = DBEntry("npcs", identifier)
385 return npc and _G.setmetatable(npc, npc_meta) or nil
386 end
387
388 function npc_prototype:EncounterData(difficulty_token)
389 self.encounter_data = self.encounter_data or {}
390 self.encounter_data[difficulty_token] = self.encounter_data[difficulty_token] or {}
391 self.encounter_data[difficulty_token].stats = self.encounter_data[difficulty_token].stats or {}
392
393 return self.encounter_data[difficulty_token]
394 end
395 end
396
397
398 local function CurrentLocationData()
399 if _G.GetCurrentMapAreaID() ~= current_area_id then
400 return _G.GetRealZoneText(), current_area_id, 0, 0, 0, InstanceDifficultyToken()
401 end
402 local map_level = _G.GetCurrentMapDungeonLevel() or 0
403 local x, y = _G.GetPlayerMapPosition("player")
404
405 x = x or 0
406 y = y or 0
407
408 if x == 0 and y == 0 then
409 for level_index = 1, _G.GetNumDungeonMapLevels() do
410 _G.SetDungeonMapLevel(level_index)
411 x, y = _G.GetPlayerMapPosition("player")
412
413 if x and y and (x > 0 or y > 0) then
414 _G.SetDungeonMapLevel(map_level)
415 map_level = level_index
416 break
417 end
418 end
419 end
420
421 if _G.DungeonUsesTerrainMap() then
422 map_level = map_level - 1
423 end
424 local x = _G.floor(x * 1000)
425 local y = _G.floor(y * 1000)
426
427 if x % 2 ~= 0 then
428 x = x + 1
429 end
430
431 if y % 2 ~= 0 then
432 y = y + 1
433 end
434 return _G.GetRealZoneText(), current_area_id, x, y, map_level, InstanceDifficultyToken()
435 end
436
437
438 local function CurrencyLinkToTexture(currency_link)
439 if not currency_link then
440 return
441 end
442 local _, _, texture_path = _G.GetCurrencyInfo(tonumber(currency_link:match("currency:(%d+)")))
443 return texture_path:match("[^\\]+$"):lower()
444 end
445
446
447 local function ItemLinkToID(item_link)
448 if not item_link then
449 return
450 end
451 return tonumber(item_link:match("item:(%d+)"))
452 end
453
454 private.ItemLinkToID = ItemLinkToID
455
456 local function UnitTypeIsNPC(unit_type)
457 return unit_type == private.UNIT_TYPES.NPC or unit_type == private.UNIT_TYPES.VEHICLE
458 end
459
460
461 local ParseGUID
462 do
463 local UNIT_TYPES = private.UNIT_TYPES
464
465 local NPC_ID_MAPPING = {
466 [62164] = 63191, -- Garalon
467 }
468
469
470 local function MatchUnitTypes(unit_type_name)
471 if not unit_type_name then
472 return UNIT_TYPES.UNKNOWN
473 end
474
475 for def, text in next, UNIT_TYPES do
476 if unit_type_name == text then
477 return UNIT_TYPES[def]
478 end
479 end
480 return UNIT_TYPES.UNKNOWN
481 end
482
483
484 function ParseGUID(guid)
485 if not guid then
486 return
487 end
488
489 -- We might want to use some of this new information later, but leaving the returns alone for now
490 local unit_type_name, unk_id1, server_id, instance_id, unk_id2, unit_idnum, spawn_id = ("-"):split(guid)
491
492 local unit_type = MatchUnitTypes(unit_type_name)
493 if unit_type ~= UNIT_TYPES.PLAYER and unit_type ~= UNIT_TYPES.PET and unit_type ~= UNIT_TYPES.ITEM then
494
495 local id_mapping = NPC_ID_MAPPING[unit_idnum]
496
497 if id_mapping and UnitTypeIsNPC(unit_type) then
498 unit_idnum = id_mapping
499 end
500 return unit_type, unit_idnum
501 end
502 return unit_type
503 end
504
505 private.ParseGUID = ParseGUID
506 end -- do-block
507
508
509 local UpdateDBEntryLocation
510 do
511 -- Fishing node coordinate code based on code in GatherMate2 with permission from Kagaro.
512 local function FishingCoordinates(x, y, yard_width, yard_height)
513 local facing = _G.GetPlayerFacing()
514
515 if not facing then
516 return x, y
517 end
518 local rad = facing + math.pi
519 return x + math.sin(rad) * 15 / yard_width, y + math.cos(rad) * 15 / yard_height
520 end
521
522
523 function UpdateDBEntryLocation(entry_type, identifier)
524 if not identifier then
525 return
526 end
527 local zone_name, area_id, x, y, map_level, difficulty_token = CurrentLocationData()
528 if not (zone_name and area_id and x and y and map_level) then
529 Debug("UpdateDBEntryLocation: Missing current location data - %s, %d, %d, %d, %d.", zone_name, area_id, x, y, map_level)
530 return
531 end
532 local entry = DBEntry(entry_type, identifier)
533 entry[difficulty_token] = entry[difficulty_token] or {}
534 entry[difficulty_token].locations = entry[difficulty_token].locations or {}
535
536 local zone_token = ("%s:%d"):format(zone_name, area_id)
537 local zone_data = entry[difficulty_token].locations[zone_token]
538
539 if not zone_data then
540 zone_data = {}
541 entry[difficulty_token].locations[zone_token] = zone_data
542 end
543
544 -- Special case for Fishing.
545 if current_action.spell_label == "FISHING" then
546 local yard_width, yard_height = MapData:MapArea(area_id, map_level)
547
548 if yard_width > 0 and yard_height > 0 then
549 x, y = FishingCoordinates(x, y, yard_width, yard_height)
550 current_action.x = x
551 current_action.y = y
552 end
553 end
554 local location_token = ("%d:%d:%d"):format(map_level, x, y)
555
556 zone_data[location_token] = zone_data[location_token] or true
557 return zone_data
558 end
559 end -- do-block
560
561
562 local function HandleItemUse(item_link, bag_index, slot_index)
563 if not item_link then
564 return
565 end
566 local item_id = ItemLinkToID(item_link)
567
568 if not bag_index or not slot_index then
569 for new_bag_index = 0, _G.NUM_BAG_FRAMES do
570 for new_slot_index = 1, _G.GetContainerNumSlots(new_bag_index) do
571 if item_id == ItemLinkToID(_G.GetContainerItemLink(new_bag_index, new_slot_index)) then
572 bag_index = new_bag_index
573 slot_index = new_slot_index
574 break
575 end
576 end
577 end
578 end
579
580 if not bag_index or not slot_index then
581 return
582 end
583 local _, _, _, _, _, is_lootable = _G.GetContainerItemInfo(bag_index, slot_index)
584
585 if not is_lootable then
586 return
587 end
588
589 table.wipe(current_action)
590 current_loot = nil
591 current_action.target_type = AF.ITEM
592 current_action.identifier = item_id
593 current_action.loot_label = "contains"
594
595 -- For items that open instantly with no spell cast
596 if private.CONTAINER_ITEM_ID_LIST[item_id] == true then
597 ClearChatLootData()
598 Debug("HandleItemUse: Beginning chat-based loot timer for item with ID %d.", item_id)
599 chat_loot_timer_handle = C_Timer.NewTimer(1, ClearChatLootData)
600 InitializeCurrentLoot()
601 end
602
603 --[[DatamineTT:ClearLines()
604 DatamineTT:SetBagItem(bag_index, slot_index)
605
606 for line_index = 1, DatamineTT:NumLines() do
607 local current_line = _G["WDPDatamineTTTextLeft" .. line_index]
608
609 if not current_line then
610 Debug("HandleItemUse: Item with ID %d and link %s had an invalid tooltip.", item_id, item_link)
611 return
612 end
613
614 if current_line:GetText() == _G.ITEM_OPENABLE then
615 table.wipe(current_action)
616 current_loot = nil
617
618 current_action.target_type = AF.ITEM
619 current_action.identifier = item_id
620 current_action.loot_label = "contains"
621 return
622 end
623 end
624 Debug("HandleItemUse: Item with ID %d and link %s did not have a tooltip that contained the string %s.", item_id, item_link, _G.ITEM_OPENABLE)]]--
625 end
626
627
628 local UnitFactionStanding
629 local UpdateFactionData
630 do
631 local MAX_FACTION_INDEX = 1000
632
633 local STANDING_NAMES = {
634 "HATED",
635 "HOSTILE",
636 "UNFRIENDLY",
637 "NEUTRAL",
638 "FRIENDLY",
639 "HONORED",
640 "REVERED",
641 "EXALTED",
642 }
643
644
645 function UnitFactionStanding(unit)
646 local unit_name = _G.UnitName(unit)
647 UpdateFactionData()
648 DatamineTT:ClearLines()
649 DatamineTT:SetUnit(unit)
650
651 for line_index = 1, DatamineTT:NumLines() do
652 local faction_name = _G["WDPDatamineTTTextLeft" .. line_index]:GetText():trim()
653
654 if faction_name and faction_name ~= unit_name and faction_standings[faction_name] then
655 return faction_name, faction_standings[faction_name]
656 end
657 end
658 end
659
660
661 function UpdateFactionData()
662 for faction_index = 1, MAX_FACTION_INDEX do
663 local faction_name, _, current_standing, _, _, _, _, _, is_header = _G.GetFactionInfo(faction_index)
664
665 if faction_name then
666 faction_standings[faction_name] = STANDING_NAMES[current_standing]
667 elseif not faction_name then
668 break
669 end
670 end
671 end
672 end -- do-block
673
674
675 local GenericLootUpdate
676 do
677 local function LootTable(entry, loot_type, top_field)
678 if top_field then
679 entry[top_field] = entry[top_field] or {}
680 entry[top_field][loot_type] = entry[top_field][loot_type] or {}
681 return entry[top_field][loot_type]
682 end
683 entry[loot_type] = entry[loot_type] or {}
684 return entry[loot_type]
685 end
686
687 function GenericLootUpdate(data_type, top_field)
688 local loot_type = current_loot.label
689 local loot_count = ("%s_count"):format(loot_type)
690 local source_list = {}
691
692 if current_loot.sources then
693 for source_guid, loot_data in pairs(current_loot.sources) do
694 local source_id
695
696 if current_loot.target_type == AF.ITEM then
697 -- Items return the player as the source, so we need to use the item's ID (disenchant, milling, etc)
698 source_id = current_loot.identifier
699 else
700 local _, unit_ID = ParseGUID(source_guid)
701 if unit_ID then
702 if current_loot.target_type == AF.OBJECT then
703 source_id = ("%s:%s"):format(current_loot.spell_label, unit_ID)
704 else
705 source_id = unit_ID
706 end
707 end
708 end
709 local entry = DBEntry(data_type, source_id)
710
711 if entry then
712 local loot_table = LootTable(entry, loot_type, top_field)
713
714 if not source_list[source_id] then
715 if top_field then
716 entry[top_field][loot_count] = (entry[top_field][loot_count] or 0) + 1
717 elseif not container_loot_toasting then
718 entry[loot_count] = (entry[loot_count] or 0) + 1
719 end
720 source_list[source_id] = true
721 end
722 UpdateDBEntryLocation(data_type, source_id)
723
724 if current_loot.target_type == AF.ZONE then
725 for item_id, quantity in pairs(loot_data) do
726 table.insert(loot_table, ("%d:%d"):format(item_id, quantity))
727 end
728 else
729 for loot_token, quantity in pairs(loot_data) do
730 local label, currency_texture = (":"):split(loot_token)
731
732 if label == "currency" and currency_texture then
733 table.insert(loot_table, ("currency:%d:%s"):format(quantity, currency_texture))
734 elseif loot_token == "money" then
735 table.insert(loot_table, ("money:%d"):format(quantity))
736 else
737 table.insert(loot_table, ("%d:%d"):format(loot_token, quantity))
738 end
739 end
740 end
741 end
742 end
743 end
744
745 -- This is used for Gas Extractions.
746 if #current_loot.list <= 0 then
747 return
748 end
749 local entry
750
751 -- At this point we only have a name if it's an object.
752 -- (As of 5.x, the above statement is almost never true, but there are a few cases, like gas extractions.)
753 if current_loot.target_type == AF.OBJECT then
754 entry = DBEntry(data_type, ("%s:%s"):format(current_loot.spell_label, current_loot.object_name))
755 else
756 entry = DBEntry(data_type, current_loot.identifier)
757 end
758
759 if not entry then
760 return
761 end
762 local loot_table = LootTable(entry, loot_type, top_field)
763
764 if current_loot.identifier then
765 if not source_list[current_loot.identifier] then
766 if top_field then
767 entry[top_field][loot_count] = (entry[top_field][loot_count] or 0) + 1
768 else
769 entry[loot_count] = (entry[loot_count] or 0) + 1
770 end
771 source_list[current_loot.identifier] = true
772 end
773 end
774
775 for index = 1, #current_loot.list do
776 table.insert(loot_table, current_loot.list[index])
777 end
778 end
779 end -- do-block
780
781
782 local ReplaceKeywords
783 do
784 local KEYWORD_SUBSTITUTIONS = {
785 class = PLAYER_CLASS,
786 name = PLAYER_NAME,
787 race = PLAYER_RACE,
788 }
789
790
791 function ReplaceKeywords(text)
792 if not text or text == "" then
793 return ""
794 end
795
796 for category, lookup in pairs(KEYWORD_SUBSTITUTIONS) do
797 local category_format = ("<%s>"):format(category)
798 text = text:gsub(lookup, category_format):gsub(lookup:lower(), category_format)
799 end
800 return text
801 end
802 end -- do-block
803
804
805 -- Contains a dirty hack due to Blizzard's strange handling of Micro Dungeons; GetMapInfo() will not return correct information
806 -- unless the WorldMapFrame is shown.
807 do
808 -- MapFileName = MapAreaID
809 local MICRO_DUNGEON_IDS = {
810 ShrineofTwoMoons = 903,
811 ShrineofSevenStars = 905,
812 }
813
814 local function SetCurrentAreaID()
815 if private.in_combat then
816 private.set_area_id = true
817 return
818 end
819 local map_area_id = _G.GetCurrentMapAreaID()
820
821 if map_area_id == current_area_id then
822 return
823 end
824 local world_map = _G.WorldMapFrame
825 local map_visible = world_map:IsVisible()
826 local sfx_value = tonumber(_G.GetCVar("Sound_EnableSFX"))
827
828 if not map_visible then
829 _G.SetCVar("Sound_EnableSFX", 0)
830 world_map:Show()
831 end
832 local _, _, _, _, micro_dungeon_map_name = _G.GetMapInfo()
833 local micro_dungeon_id = MICRO_DUNGEON_IDS[micro_dungeon_map_name]
834
835 _G.SetMapToCurrentZone()
836
837 if micro_dungeon_id then
838 current_area_id = micro_dungeon_id
839 else
840 current_area_id = _G.GetCurrentMapAreaID()
841 end
842
843 if map_visible then
844 _G.SetMapByID(map_area_id)
845 else
846 world_map:Hide()
847 _G.SetCVar("Sound_EnableSFX", sfx_value)
848 end
849 end
850
851 function WDP:HandleZoneChange(event_name)
852 in_instance = _G.IsInInstance()
853 SetCurrentAreaID()
854 end
855 end
856
857
845 -- TIMERS ------------------------------------------------------------- 858 -- TIMERS -------------------------------------------------------------
846 859
847 local function ClearKilledNPC() 860 function ClearKilledNPC()
848 killed_npc_id = nil 861 killed_npc_id = nil
849 end 862 end
850 863
851 864
852 local function ClearKilledBossID() 865 function ClearKilledBossID()
853 if killed_boss_id_timer_handle then 866 if killed_boss_id_timer_handle then
854 killed_boss_id_timer_handle:Cancel() 867 killed_boss_id_timer_handle:Cancel()
855 killed_boss_id_timer_handle = nil 868 killed_boss_id_timer_handle = nil
856 end 869 end
857 870
858 table.wipe(boss_loot_toasting) 871 table.wipe(boss_loot_toasting)
859 raid_boss_id = nil 872 raid_boss_id = nil
860 end 873 end
861 874
862 875
863 local function ClearLootToastContainerID() 876 function ClearLootToastContainerID()
864 if loot_toast_container_timer_handle then 877 if loot_toast_container_timer_handle then
865 loot_toast_container_timer_handle:Cancel() 878 loot_toast_container_timer_handle:Cancel()
866 loot_toast_container_timer_handle = nil 879 loot_toast_container_timer_handle = nil
867 end 880 end
868 881
869 container_loot_toasting = false 882 container_loot_toasting = false
870 loot_toast_container_id = nil 883 loot_toast_container_id = nil
871 end 884 end
872 885
873 886
874 local function ClearLootToastData() 887 function ClearLootToastData()
875 if loot_toast_data_timer_handle then 888 if loot_toast_data_timer_handle then
876 loot_toast_data_timer_handle:Cancel() 889 loot_toast_data_timer_handle:Cancel()
877 loot_toast_data_timer_handle = nil 890 loot_toast_data_timer_handle = nil
878 end 891 end
879 892
881 table.wipe(loot_toast_data) 894 table.wipe(loot_toast_data)
882 end 895 end
883 end 896 end
884 897
885 898
886 local function ClearChatLootData() 899 function ClearChatLootData()
887 Debug("ClearChatLootData: Ending chat-based loot timer.")
888 if chat_loot_timer_handle then 900 if chat_loot_timer_handle then
901 Debug("ClearChatLootData: Ending chat-based loot timer.")
889 chat_loot_timer_handle:Cancel() 902 chat_loot_timer_handle:Cancel()
890 chat_loot_timer_handle = nil 903 chat_loot_timer_handle = nil
891 end 904
892 905 if current_loot and current_loot.identifier and (private.CONTAINER_ITEM_ID_LIST[current_loot.identifier] ~= nil) then
893 if current_loot and current_loot.identifier and (private.CONTAINER_ITEM_ID_LIST[current_loot.identifier] ~= nil) then 906 GenericLootUpdate("items")
894 GenericLootUpdate("items") 907 end
895 end 908 end
896 current_loot = nil 909 current_loot = nil
897 end 910 end
898 911
899 912
2705 end 2718 end
2706 private.tracked_line = spell_line 2719 private.tracked_line = spell_line
2707 end 2720 end
2708 2721
2709 2722
2723 -- Triggered by bonus roll prompts, disenchant prompts, and in a few other rare circumstances
2710 function WDP:SPELL_CONFIRMATION_PROMPT(event_name, spell_id, confirm_type, text, duration, currency_id_cost) 2724 function WDP:SPELL_CONFIRMATION_PROMPT(event_name, spell_id, confirm_type, text, duration, currency_id_cost)
2711 if private.RAID_BOSS_BONUS_SPELL_ID_TO_NPC_ID_MAP[spell_id] then 2725 if private.RAID_BOSS_BONUS_SPELL_ID_TO_NPC_ID_MAP[spell_id] then
2712 ClearKilledBossID() 2726 ClearKilledBossID()
2713 ClearLootToastContainerID() 2727 ClearLootToastContainerID()
2714 raid_boss_id = private.RAID_BOSS_BONUS_SPELL_ID_TO_NPC_ID_MAP[spell_id] 2728 raid_boss_id = private.RAID_BOSS_BONUS_SPELL_ID_TO_NPC_ID_MAP[spell_id]
2783 return 2797 return
2784 end 2798 end
2785 private.tracked_line = nil 2799 private.tracked_line = nil
2786 private.previous_spell_id = spell_id 2800 private.previous_spell_id = spell_id
2787 2801
2788 -- Handle Logging spell casts 2802 -- For spells cast when Logging
2789 if private.LOGGING_SPELL_ID_TO_OBJECT_ID_MAP[spell_id] then 2803 if private.LOGGING_SPELL_ID_TO_OBJECT_ID_MAP[spell_id] then
2790 last_timber_spell_id = spell_id 2804 last_timber_spell_id = spell_id
2791 UpdateDBEntryLocation("objects", ("OPENING:%s"):format(private.LOGGING_SPELL_ID_TO_OBJECT_ID_MAP[spell_id])) 2805 UpdateDBEntryLocation("objects", ("OPENING:%s"):format(private.LOGGING_SPELL_ID_TO_OBJECT_ID_MAP[spell_id]))
2792 return 2806 return
2793 end 2807 end
2794 2808
2795 -- Handle Loot Toast spell casts 2809 -- For spells cast by items that always trigger loot toasts
2796 if private.LOOT_TOAST_CONTAINER_SPELL_ID_TO_ITEM_ID_MAP[spell_id] then 2810 if private.LOOT_TOAST_CONTAINER_SPELL_ID_TO_ITEM_ID_MAP[spell_id] then
2797 ClearKilledBossID() 2811 ClearKilledBossID()
2798 ClearLootToastContainerID() 2812 ClearLootToastContainerID()
2799 ClearLootToastData() 2813 ClearLootToastData()
2800 2814
2801 loot_toast_container_id = private.LOOT_TOAST_CONTAINER_SPELL_ID_TO_ITEM_ID_MAP[spell_id] 2815 loot_toast_container_id = private.LOOT_TOAST_CONTAINER_SPELL_ID_TO_ITEM_ID_MAP[spell_id]
2802 loot_toast_container_timer_handle = C_Timer.NewTimer(1, ClearLootToastContainerID) -- we need to assign a handle here to cancel it later 2816 loot_toast_container_timer_handle = C_Timer.NewTimer(1, ClearLootToastContainerID) -- we need to assign a handle here to cancel it later
2803 return 2817 return
2804 end 2818 end
2805 2819
2806 -- For Crates of Salvage (and potentially other items based on spell casts in the future which need manual handling) 2820 -- For spells cast by items that don't usually trigger loot toasts
2807 if private.DELAYED_CONTAINER_SPELL_ID_TO_ITEM_ID_MAP[spell_id] then 2821 if private.DELAYED_CONTAINER_SPELL_ID_TO_ITEM_ID_MAP[spell_id] then
2808 -- Set up timer 2822 -- Set up timer
2809 Debug("%s: Beginning Salvage loot timer for spellID %d", event_name, spell_id) 2823 ClearChatLootData()
2824 Debug("%s: Beginning chat-based loot timer for spellID %d", event_name, spell_id)
2810 chat_loot_timer_handle = C_Timer.NewTimer(1, ClearChatLootData) 2825 chat_loot_timer_handle = C_Timer.NewTimer(1, ClearChatLootData)
2811 2826
2812 -- Standard item handling setup 2827 -- Standard item handling setup
2813 table.wipe(current_action) 2828 table.wipe(current_action)
2814 current_loot = nil 2829 current_loot = nil