You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

322 lines
17 KiB

---@class DBMCoreNamespace
local private = select(2, ...)
4 years ago
local twipe = table.wipe
local UnitExists, UnitPlayerOrPetInRaid, UnitGUID =
UnitExists, UnitPlayerOrPetInRaid, UnitGUID
---@class TargetScanningModule: DBMModule
local module = private:NewModule("TargetScanningModule")
4 years ago
--Traditional loop scanning method tables
local targetScanCount = {}
local filteredTargetCache = {}
4 years ago
local bossuIdCache = {}
--UNIT_TARGET scanning method table
local unitScanCount = 0
local unitMonitor = {}
---@class DBMMod
local bossModPrototype = private:GetPrototype("DBMMod")
local test = private:GetPrototype("DBMTest")
4 years ago
function module:OnModuleEnd()
twipe(targetScanCount)
twipe(filteredTargetCache)
4 years ago
twipe(bossuIdCache)
unitScanCount = 0
twipe(unitMonitor)
end
do
local CL = DBM_COMMON_L
3 years ago
4 years ago
local bossTargetuIds = {
"boss1", "boss2", "boss3", "boss4", "boss5", "boss6", "boss7", "boss8", "boss9", "boss10", "focus", "target"
4 years ago
}
local fullUids = {
"boss1", "boss2", "boss3", "boss4", "boss5", "boss6", "boss7", "boss8", "boss9", "boss10",
"mouseover", "target", "focus", "focustarget", "targettarget", "mouseovertarget",
"party1target", "party2target", "party3target", "party4target",
"raid1target", "raid2target", "raid3target", "raid4target", "raid5target", "raid6target", "raid7target", "raid8target", "raid9target", "raid10target",
"raid11target", "raid12target", "raid13target", "raid14target", "raid15target", "raid16target", "raid17target", "raid18target", "raid19target", "raid20target",
"raid21target", "raid22target", "raid23target", "raid24target", "raid25target", "raid26target", "raid27target", "raid28target", "raid29target", "raid30target",
"raid31target", "raid32target", "raid33target", "raid34target", "raid35target", "raid36target", "raid37target", "raid38target", "raid39target", "raid40target",
"nameplate1", "nameplate2", "nameplate3", "nameplate4", "nameplate5", "nameplate6", "nameplate7", "nameplate8", "nameplate9", "nameplate10",
"nameplate11", "nameplate12", "nameplate13", "nameplate14", "nameplate15", "nameplate16", "nameplate17", "nameplate18", "nameplate19", "nameplate20",
"nameplate21", "nameplate22", "nameplate23", "nameplate24", "nameplate25", "nameplate26", "nameplate27", "nameplate28", "nameplate29", "nameplate30",
"nameplate31", "nameplate32", "nameplate33", "nameplate34", "nameplate35", "nameplate36", "nameplate37", "nameplate38", "nameplate39", "nameplate40"
}
4 years ago
local function getBossTarget(guid, scanOnlyBoss)
local name, uid, bossuid
local cacheuid = bossuIdCache[guid] or "boss1"
3 years ago
--Try to check last used unit token cache before iterating again.
4 years ago
if UnitGUID(cacheuid) == guid then
bossuid = cacheuid
name = DBM:GetUnitFullName(cacheuid.."target")
uid = cacheuid.."target"
bossuIdCache[guid] = bossuid
end
if name then return name, uid, bossuid end
3 years ago
--Else, perform iteration again
local unitID = DBM:GetUnitIdFromGUID(guid, scanOnlyBoss)
if unitID then
bossuid = unitID
name = DBM:GetUnitFullName(unitID.."target")
uid = unitID.."target"
bossuIdCache[guid] = bossuid
4 years ago
end
return name, uid, bossuid
end
---@param cidOrGuid number|string
---@param scanOnlyBoss boolean?
---@return string? name, string? uid, string? bossuid
function bossModPrototype:GetBossTarget(cidOrGuid, scanOnlyBoss)
4 years ago
local name, uid, bossuid
DBM:Debug("GetBossTarget firing for :"..tostring(self).." "..tostring(cidOrGuid).." "..tostring(scanOnlyBoss), 3)
4 years ago
if type(cidOrGuid) == "number" then--CID passed, slower and slighty more hacky scan
cidOrGuid = cidOrGuid or self.creatureId
4 years ago
local cacheuid = bossuIdCache[cidOrGuid] or "boss1"
if self:GetUnitCreatureId(cacheuid) == cidOrGuid then
4 years ago
bossuIdCache[cidOrGuid] = cacheuid
bossuIdCache[UnitGUID(cacheuid)] = cacheuid
name, uid, bossuid = getBossTarget(UnitGUID(cacheuid), scanOnlyBoss)
else
local usedTable = scanOnlyBoss and bossTargetuIds or fullUids
for _, uId in ipairs(usedTable) do
if self:GetUnitCreatureId(uId) == cidOrGuid then
4 years ago
bossuIdCache[cidOrGuid] = uId
bossuIdCache[UnitGUID(uId)] = uId
name, uid, bossuid = getBossTarget(UnitGUID(uId), scanOnlyBoss)
break
end
end
end
else
name, uid, bossuid = getBossTarget(cidOrGuid, scanOnlyBoss)
end
if uid then
local cid = self:GetUnitCreatureId(uid)
4 years ago
if cid == 24207 or cid == 80258 or cid == 87519 then--Filter useless units, like "Army of the Dead", that would otherwise throw off the target scan
return
end
end
return name, uid, bossuid
end
---Manually aborts BossTargetScanner
---@param cidOrGuid string|number
---@param returnFunc string
function bossModPrototype:BossTargetScannerAbort(cidOrGuid, returnFunc)
4 years ago
targetScanCount[cidOrGuid] = nil--Reset count for later use.
self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)
4 years ago
DBM:Debug("Boss target scan for "..cidOrGuid.." should be aborting.", 2)
filteredTargetCache[cidOrGuid] = nil
4 years ago
end
---All purpose boss target scanner with many filter and scan options.
---@param cidOrGuid string|number?
---@param returnFunc string name of the function mod scanner runs on completion that's within boss mod
---@param scanInterval number? frequency of scan
---@param scanTimes number? number of times to scan before running final scan
---@param scanOnlyBoss boolean? used to scope scan to only scan "boss" unitIDs
---@param isEnemyScan boolean? used to filter friendly targets from scan results. Useful if scanning for a boss targetting another enemy
---@param isFinalScan boolean? don't manually use this, it's auto used on final scan. This should be nil in boss mods
---@param targetFilter string? used to filter specific targets froms can results (useful when boss rapid casts and want to avoid previous mechanics target)
---@param tankFilter boolean? used to filter players with tank role from scan results.
---@param onlyPlayers boolean? used to ignore npcs and pets
---@param filterFallback boolean? if true, tells function to allow person defined in targetFilter to be used in final scan if no other target found
function bossModPrototype:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, isFinalScan, targetFilter, tankFilter, onlyPlayers, filterFallback)
4 years ago
--Increase scan count
cidOrGuid = cidOrGuid or self.creatureId
4 years ago
if not cidOrGuid then return end
if not targetScanCount[cidOrGuid] then
targetScanCount[cidOrGuid] = 0
DBM:Debug("Boss target scan started for "..cidOrGuid, 2)
end
targetScanCount[cidOrGuid] = targetScanCount[cidOrGuid] + 1
--Set default values
scanInterval = scanInterval or 0.05
scanTimes = scanTimes or 16
local targetname, targetuid, bossuid = self:GetBossTarget(cidOrGuid, scanOnlyBoss)
4 years ago
DBM:Debug("Boss target scan "..targetScanCount[cidOrGuid].." of "..scanTimes..", found target "..(targetname or "nil").." using "..(bossuid or "nil"), 3)--Doesn't hurt to keep this, as level 3
--Do scan
--Cache the filtered target if using a filter target fallback
--so when scan ends we can return that instead of tank when scan ends
--(because boss might have already swapped back to aggro target by then)
if targetname and targetname ~= CL.UNKNOWN and filterFallback and targetFilter and targetFilter == targetname then
filteredTargetCache[cidOrGuid] = {}
filteredTargetCache[cidOrGuid].target = targetname
filteredTargetCache[cidOrGuid].targetuid = targetuid
end
--Hard return filter target, with no other checks like tank or hostility if final scan and cache exists
if filteredTargetCache[cidOrGuid] and isFinalScan then
targetScanCount[cidOrGuid] = nil
self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
local scanningTime = (targetScanCount[cidOrGuid] or 1) * scanInterval
self[returnFunc](self, filteredTargetCache[cidOrGuid].targetname, filteredTargetCache[cidOrGuid].targetuid, bossuid, scanningTime)--Return results to warning function with all variables.
DBM:Debug("BossTargetScanner has ended for "..cidOrGuid, 2)
filteredTargetCache[cidOrGuid] = nil
--Perform normal scan criteria matching
elseif targetname and targetuid and targetname ~= CL.UNKNOWN and (not targetFilter or (targetFilter and targetFilter ~= targetname)) then
4 years ago
if not IsInGroup() then scanTimes = 1 end--Solo, no reason to keep scanning, give faster warning. But only if first scan is actually a valid target, which is why i have this check HERE
if (isEnemyScan and UnitIsFriend("player", targetuid) or (onlyPlayers and DBM:IsNonPlayableGUID(UnitGUID(targetuid))) or self:IsTanking(targetuid, bossuid, nil, true)) and not isFinalScan then--On player scan, ignore tanks. On enemy scan, ignore friendly player. On Only player, ignore npcs and pets
4 years ago
if targetScanCount[cidOrGuid] < scanTimes then--Make sure no infinite loop.
self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter, onlyPlayers, filterFallback)--Scan multiple times to be sure it's not on something other then tank (or friend on enemy scan, or npc/pet on only person)
4 years ago
else--Go final scan.
self:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, true, targetFilter, tankFilter, onlyPlayers, filterFallback)
4 years ago
end
else--Scan success. (or failed to detect right target.) But some spells can be used on tanks, anyway warns tank if player scan. (enemy scan block it)
targetScanCount[cidOrGuid] = nil--Reset count for later use.
self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
if (tankFilter and self:IsTanking(targetuid, bossuid, nil, true)) or (isFinalScan and isEnemyScan) or onlyPlayers and DBM:IsNonPlayableGUID(UnitGUID(targetuid)) then return end--If enemyScan and playerDetected, return nothing
4 years ago
local scanningTime = (targetScanCount[cidOrGuid] or 1) * scanInterval
self[returnFunc](self, targetname, targetuid, bossuid, scanningTime)--Return results to warning function with all variables.
4 years ago
DBM:Debug("BossTargetScanner has ended for "..cidOrGuid, 2)
filteredTargetCache[cidOrGuid] = nil
4 years ago
end
--target was nil, lets schedule a rescan here too.
else
4 years ago
if targetScanCount[cidOrGuid] < scanTimes then--Make sure not to infinite loop here as well.
self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter, onlyPlayers, filterFallback)
elseif not isFinalScan then--Still trigger a final scan check, even if the target was nil/unknown on final scan, to make sure isFinalScan+filterFallback run if it exists and final scan was a failure
self:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, true, targetFilter, tankFilter, onlyPlayers, filterFallback)
4 years ago
else
targetScanCount[cidOrGuid] = nil--Reset count for later use.
self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
filteredTargetCache[cidOrGuid] = nil
4 years ago
end
end
end
end
do
--UNIT_TARGET Target scanning method
local eventsRegistered = false
3 years ago
--Validate target is in group (I'm not sure why i actually required this since I didn't comment code when I added requirement, so I leave it for now)
--If I determine this check isn't needed and it continues to be a problem i'll kill it off.
--For now, I'll have check be smart and switch between raid and party and just disable when solo
local function validateGroupTarget(unit)
if IsInGroup() then
if UnitPlayerOrPetInRaid(unit) or UnitPlayerOrPetInParty(unit) then
return true
end
else--Solo
return true
end
end
4 years ago
function module:UNIT_TARGET_UNFILTERED(uId)
--Active BossUnitTargetScanner
3 years ago
DBM:Debug("UNIT_TARGET_UNFILTERED fired for :"..uId, 3)
if unitMonitor[uId] and UnitExists(uId.."target") and validateGroupTarget(uId.."target") then
4 years ago
DBM:Debug("unitMonitor for this unit exists, target exists in group", 2)
local modId, returnFunc = unitMonitor[uId].modid, unitMonitor[uId].returnFunc
DBM:Debug("unitMonitor: "..modId..", "..uId..", "..returnFunc, 2)
if not unitMonitor[uId].allowTank then
local tanking, status = UnitDetailedThreatSituation(uId, uId.."target")--Tanking may return 0 if npc is temporarily looking at an NPC (IE fracture) but status will still be 3 on true tank
if tanking or (status == 3) then
DBM:Debug("unitMonitor ending for unit without 'allowTank', ignoring target", 2)
return
end
end
local mod = DBM:GetModByName(modId)--The whole reason we store modId in unitMonitor,
DBM:Debug("unitMonitor success for this unit, a valid target for returnFunc", 2)
mod[returnFunc](mod, DBM:GetUnitFullName(uId.."target"), uId.."target", uId)--Return results to warning function with all variables.
unitMonitor[uId] = nil
unitScanCount = unitScanCount - 1
end
if unitScanCount == 0 then--Out of scans
eventsRegistered = false
self:UnregisterShortTermEvents()
DBM:Debug("All target scans complete, unregistering events", 2)
end
end
---Used to abort BossUnitTargetScanner on specified unit
---@param uId string?
function bossModPrototype:BossUnitTargetScannerAbort(uId)
4 years ago
if not uId then--Not called with unit, means mod requested to clear all used units
DBM:Debug("BossUnitTargetScannerAbort called without unit, clearing all unitMonitor units", 2)
twipe(unitMonitor)
unitScanCount = 0
return
end
--If tank is allowed, return current target when scan ends no matter what.
3 years ago
if unitMonitor[uId] and (unitMonitor[uId].allowTank or not IsInGroup()) and validateGroupTarget(uId.."target") then
4 years ago
DBM:Debug("unitMonitor unit exists, allowTank target exists", 2)
local modId, returnFunc = unitMonitor[uId].modid, unitMonitor[uId].returnFunc
DBM:Debug("unitMonitor: "..modId..", "..uId..", "..returnFunc, 2)
DBM:Debug("unitMonitor found a target that probably is a tank", 2)
self[returnFunc](self, DBM:GetUnitFullName(uId.."target"), uId.."target", uId)--Return results to warning function with all variables.
4 years ago
end
unitMonitor[uId] = nil
unitScanCount = unitScanCount - 1
DBM:Debug("Boss unit target scan should be aborting for "..uId, 2)
end
---UNIT_TARGET event monitor target scanner. Will instantly detect a target change of a registered Unit
---<br>If target change occurs before this method is called (or if boss doesn't change target because cast ends up actually being on the tank, target scan will fail completely
---@param uId string
---@param returnFunc string
---@param scanTime number?
---@param allowTank boolean? If allowTank is passed, it basically tells this scanner to return current target of unitId at time of failure/abort when scanTime is complete
function bossModPrototype:BossUnitTargetScanner(uId, returnFunc, scanTime, allowTank)
4 years ago
unitMonitor[uId] = {}
unitScanCount = unitScanCount + 1
unitMonitor[uId].modid, unitMonitor[uId].returnFunc, unitMonitor[uId].allowTank = self.id, returnFunc, allowTank
self:ScheduleMethod(scanTime or 1.5, "BossUnitTargetScannerAbort", uId)--In case of BossUnitTargetScanner firing too late, and boss already having changed target before monitor started, it needs to abort after x seconds
4 years ago
if not eventsRegistered then
eventsRegistered = true
module:RegisterShortTermEvents("UNIT_TARGET_UNFILTERED")
4 years ago
DBM:Debug("Registering UNIT_TARGET event for BossUnitTargetScanner", 2)
end
end
end
do
local repeatedScanEnabled = {}
---@param mod DBMMod
---@param cidOrGuid number|string?
---@param returnFunc string
---@param scanInterval number?
---@param scanOnlyBoss boolean?
---@param includeTank boolean?
4 years ago
local function repeatedScanner(mod, cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank)
if repeatedScanEnabled[returnFunc] then
cidOrGuid = cidOrGuid or mod.creatureId
scanInterval = scanInterval or 0.1
local targetname, targetuid, bossuid = mod:GetBossTarget(cidOrGuid, scanOnlyBoss)
if targetname and (includeTank or not mod:IsTanking(targetuid, bossuid, nil, true)) then
4 years ago
mod[returnFunc](mod, targetname, targetuid, bossuid)
end
mod:Schedule(scanInterval, repeatedScanner, mod, cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank)
end
end
---A scan method that uses an infinite loop for VERY specific niche situations like Hanz and Franz
---@param self DBMMod
---@param cidOrGuid number|string?
---@param returnFunc string
---@param scanInterval number?
---@param scanOnlyBoss boolean?
---@param includeTank boolean?
function bossModPrototype:StartRepeatedScan(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank)
4 years ago
repeatedScanEnabled[returnFunc] = true
repeatedScanner(self, cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank)
4 years ago
end
function bossModPrototype:StopRepeatedScan(returnFunc)
4 years ago
repeatedScanEnabled[returnFunc] = nil
end
end
test:RegisterLocalHook("UnitGUID", function(val)
local old = UnitGUID
UnitGUID = val
return old
end)