---@class DBMCoreNamespace
local private = select ( 2 , ... )
local twipe = table.wipe
local UnitExists , UnitPlayerOrPetInRaid , UnitGUID , Ambiguate =
UnitExists , UnitPlayerOrPetInRaid , UnitGUID , Ambiguate
---@class TargetScanningModule: DBMModule
local module = private : NewModule ( " TargetScanningModule " )
--Traditional loop scanning method tables
local targetScanCount = { }
local filteredTargetCache = { }
local bossuIdCache = { }
--UNIT_TARGET scanning method table
local unitScanCount = 0
local unitMonitor = { }
---@class DBMMod
local bossModPrototype = private : GetPrototype ( " DBMMod " )
local test = private : GetPrototype ( " DBMTest " )
function module : OnModuleEnd ( )
twipe ( targetScanCount )
twipe ( filteredTargetCache )
twipe ( bossuIdCache )
unitScanCount = 0
twipe ( unitMonitor )
end
do
local CL = DBM_COMMON_L
local bossTargetuIds = {
" boss1 " , " boss2 " , " boss3 " , " boss4 " , " boss5 " , " boss6 " , " boss7 " , " boss8 " , " boss9 " , " boss10 " , " focus " , " target "
}
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 "
}
local function debugLogBossTarget ( bossGuid , targetUid )
-- Used for more accurate target reconstruction in tests
DBM : Debug ( ( " GetBossTarget: %s#%s " ) : format ( tostring ( bossGuid ) , tostring ( UnitGUID ( targetUid ) ) , 3 , false , true ) )
end
local function getBossTarget ( guid , scanOnlyBoss )
local name , uid , bossuid
local cacheuid = bossuIdCache [ guid ] or " boss1 "
--Try to check last used unit token cache before iterating again.
if UnitGUID ( cacheuid ) == guid then
bossuid = cacheuid
name = DBM : GetUnitFullName ( cacheuid .. " target " )
uid = cacheuid .. " target "
bossuIdCache [ guid ] = bossuid
debugLogBossTarget ( guid , uid )
end
if name then
return name , uid , bossuid
end
--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
debugLogBossTarget ( guid , uid )
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 )
local name , uid , bossuid
DBM : Debug ( " GetBossTarget firing for : " .. tostring ( self ) .. " " .. tostring ( cidOrGuid ) .. " " .. tostring ( scanOnlyBoss ) , 3 )
if type ( cidOrGuid ) == " number " then --CID passed, slower and slighty more hacky scan
cidOrGuid = cidOrGuid or self.creatureId
local cacheuid = bossuIdCache [ cidOrGuid ] or " boss1 "
if self : GetUnitCreatureId ( cacheuid ) == cidOrGuid then
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
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 )
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 )
targetScanCount [ cidOrGuid ] = nil --Reset count for later use.
self : UnscheduleMethod ( " BossTargetScanner " , cidOrGuid , returnFunc )
DBM : Debug ( " Boss target scan for " .. cidOrGuid .. " should be aborting. " , 2 )
filteredTargetCache [ cidOrGuid ] = nil
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 )
--Increase scan count
cidOrGuid = cidOrGuid or self.creatureId
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 )
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 targetFilter then
--Chinese Wrath client seems to always have realm name in combat log, even if player is from same realm.
--This should do no harm when combat log is correct but fix it when it isn't
targetFilter = Ambiguate ( targetFilter , " none " )
end
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
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
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)
else --Go final scan.
self : BossTargetScanner ( cidOrGuid , returnFunc , scanInterval , scanTimes , scanOnlyBoss , isEnemyScan , true , targetFilter , tankFilter , onlyPlayers , filterFallback )
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
local scanningTime = ( targetScanCount [ cidOrGuid ] or 1 ) * scanInterval
self [ returnFunc ] ( self , targetname , targetuid , bossuid , scanningTime ) --Return results to warning function with all variables.
DBM : Debug ( " BossTargetScanner has ended for " .. cidOrGuid , 2 )
filteredTargetCache [ cidOrGuid ] = nil
end
--target was nil, lets schedule a rescan here too.
else
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 )
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
end
end
end
end
do
--UNIT_TARGET Target scanning method
local eventsRegistered = false
--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
function module : UNIT_TARGET_UNFILTERED ( uId )
--Active BossUnitTargetScanner
DBM : Debug ( " UNIT_TARGET_UNFILTERED fired for : " .. uId , 3 )
if unitMonitor [ uId ] and UnitExists ( uId .. " target " ) and validateGroupTarget ( uId .. " target " ) then
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 )
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.
if unitMonitor [ uId ] and ( unitMonitor [ uId ] . allowTank or not IsInGroup ( ) ) and validateGroupTarget ( uId .. " target " ) then
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.
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 )
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
if not eventsRegistered then
eventsRegistered = true
module : RegisterShortTermEvents ( " UNIT_TARGET_UNFILTERED " )
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?
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
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 )
repeatedScanEnabled [ returnFunc ] = true
repeatedScanner ( self , cidOrGuid , returnFunc , scanInterval , scanOnlyBoss , includeTank )
end
function bossModPrototype : StopRepeatedScan ( returnFunc )
repeatedScanEnabled [ returnFunc ] = nil
end
end
test : RegisterLocalHook ( " UnitGUID " , function ( val )
local old = UnitGUID
UnitGUID = val
return old
end )