Skip to content

Property Implementation Reference

Early WA1 shipped nearly a hundred preset types in the property dropdown (removed in 2026-08, when the property system switched to “custom code + property library”). This document collects every old implementation from earlier releases and adapts it to the current WA1 as paste-ready custom code, for module authors’ reference. Client: both retail (Midnight) and Classic are supported and share one code path. The code below is written against the retail API surface and secret rules. Secret sanitizing (SuperDPS.SN) is a side-effect-free no-op on Classic, so write it as-is; but retail-only interfaces (such as C_AssistedCombat) return nothing on Classic, and a few C_Spell struct fields may differ between the two, so verify in-game once before moving code to Classic.

Sources: the property table and the addon query function library of earlier WA1 releases, compiled 2026-08-19.

  1. Module editor → property card → choose “Custom code” in the property dropdown (or pick a ready-made entry from the property library).
  2. Click “Edit” and paste the code from this document. Fill the Parameters column with comma-separated values (up to 3 segments); the code reads them as arg1/arg2/arg3. Purely numeric segments are passed as numbers, everything else as strings, and empty segments are nil.
  3. The code’s return value is sent back to the desktop side over the strip, the same as any other property.

One piece of code can be reused by several property rows (write arg1 in the code, then fill player / target as the parameter on two different rows).

Property code vs. init code (performance rules)

Section titled “Property code vs. init code (performance rules)”
  • Property code is a high-frequency hot path: it runs every 0.02 s, once per property row. Only do reads and the necessary queries there. Do not put one-time work in it (CreateFrame/RegisterEvent, building shared tables, defining shared functions, and so on). Even behind an if not ... guard the check still runs every frame for nothing, and forgetting the guard leaks one frame object per tick.
  • One-time work belongs in the module’s “init code” (module editor → Init code button). It runs once when the addon loads, before property registration, in the same environment as property code (same chunk, with the compatibility layer and the SuperDPS.* interfaces available). It is the right place to create event frames, initialize shared tables and define shared functions. Every entry in this document that needs event tracking (codes 1/6/12) or shares heavy logic across several rows (code 22) is given as two parts: “init code + property code”.
  • Keeping the if not SuperDPS.XxxFrame guard in init code is deliberate: init code runs again when the addon is reinstalled or reloaded, frame objects cannot be destroyed, and the guard prevents duplicate creation and double counting. That check runs only once at load time, with no hot-path cost.
  • An error thrown in init code aborts the whole addon startup (it is not inside the pcall protection that wraps properties), so watch for nil guards inside event callbacks and keep the logic robust.

Migrating old modules: in old .wa1 files, the "p" key of a property row is the “Old Key” in the tables below. After loading in the new version these rows are classified as custom code, but their value is still the old “parameter” rather than code. Add the code from this document and move the old value into the Parameters column.

  • Same chunk as the addon, so WoW APIs can be called directly. GetSpellInfo/GetSpellCooldown/GetSpellCharges/UnitAura/UnitBuff/UnitDebuff are compatibility wrappers (old multi-return signatures) over the new C_Spell/C_UnitAuras interfaces.
  • Reserved interfaces: SuperDPS.GetUnitAura/GetUnitBuff/GetUnitDebuff (look up an aura by name or spell ID; duration/expirationTime/count already sanitized), SuperDPS.SN(v, "tag") (protected-value sanitizing, protected values become 0), SuperDPS.HekeliPrimary, SuperDPS.Prop (shared value table).
  • Inside the function body, UnitHealth/UnitHealthMax/UnitPower/UnitPowerMax are the native raw versions and may return protected values in combat.

The three Midnight value-protection rules (the main reason old implementations broke)

Section titled “The three Midnight value-protection rules (the main reason old implementations broke)”
  1. Pass-through, passing as arguments and truthiness checks are fine: a protected value can be returned directly (the strip carries it back as usual), used in X or 0, or tested with if not X (non-boolean).
  2. Comparison and arithmetic require sanitizing first: >, == and the four arithmetic operators throw immediately on a protected value. Run it through SuperDPS.SN(v, "tag") first (protected values become 0).
  3. Exception: isActive/isOnGCD from C_Spell.GetSpellCooldown() are officially guaranteed never to be protected and are safe for logic.

The many %255, ×100, /100 scalings in the old implementations are leftovers from the old single-byte “pixel color block” channel. The new strip carries full values, so they have all been removed. When reusing thresholds from old modules, mind the unit conversion (noted per entry).

Legend: 📦 = a preset entry already exists in the new property library, use it directly; ✅ = use the numbered code below; ❌ = cannot be implemented under Midnight, reason noted.


Old property Old Key Old implementation New version
In Combat PropC4Ca SuperDPS.InCombatLockdown() 📦 preset “In Combat”
Behind Target PropC4Cb getBehind() (UI_ERROR_MESSAGE event tracking) ✅ Code 1
Global Cooldown PropC81E GCD() (cooldown of 61304 ×100, >255 becomes 0) 📦 preset “Global Cooldown” (unit changed to seconds)
WA global property PropC81F WaProp(name) reads SuperDPS.Prop 📦 preset “Global Property”
Hekili recommendation PropC82F Dedicated client-side channel (removed) 📦 preset “Hekili Recommendation”; there is also the zero-dependency “Assisted Combat Recommendation”

Code 1: Behind Target (parameters: none). The old version relied on the addon’s main event frame recording the “must be behind the target” error; the new addon no longer registers that event, so the module creates its own event frame. Semantics unchanged (treated as not behind for 0.6 s after the error).

Put this in the init code:

if not SuperDPS.BehindFrame then
SuperDPS.BehindAt = 0
local f = CreateFrame("Frame")
f:RegisterEvent("UI_ERROR_MESSAGE")
f:SetScript("OnEvent", function(_, _, _, msg)
if msg == SPELL_FAILED_NOT_BEHIND then
SuperDPS.BehindAt = GetTime()
end
end)
SuperDPS.BehindFrame = f
end

Property code:

if GetTime() - SuperDPS.BehindAt > 0.6 then return 1 end
return 0

The old Global CD returned “centiseconds” (remaining seconds ×100, values above 255 became 0); the preset “Global Cooldown” returns seconds. Divide old thresholds by 100 to reuse them.

Old property Old Key Old implementation New version
own health % PropEccb getHeal("player") (Health/Max×100) 📦 preset “Health Percentage” (0~1 fraction) or ✅ Code 2
target health % PropA87F getHeal("target") Same, parameter target
pet health % Prop1679 getHeal("pet") Same, parameter pet
target actual health, thousands PropE4Da getHeal2 = UnitHealth/1000 ✅ Code 3 (pass the raw value through; division hits protection)
target actual health, ten-thousands PropE4Db getHeal3 = UnitHealth/10000 Same as Code 3

The old UnitHealth/UnitHealthMax division throws outright in combat on Midnight (protected values cannot be used in arithmetic).

Code 2: Health percent (old 0~100 scale) (parameters: player / target / pet). The value can only be multiplied by 100 after SN sanitizing; if it is protected in combat it counts as 0. If a 0~1 scale is acceptable, prefer the preset “Health Percentage” (pass-through, not subject to the sanitizing downgrade):

local g = UnitGUID(arg1)
if not g then return 0 end
local v = SuperDPS.SN(UnitPercentHealthFromGUID(g), "healpct", arg1)
return v * 100

Code 3: Actual health (parameter: target). Returns the raw value directly (protected values can still ride the strip); do the thousands/ten-thousands conversion on the desktop side:

return UnitHealth(arg1)
Old property Old Key Old implementation New version
own primary power Prop8F14 getPower("player") (percent) ✅ Code 4 (percent, downgrades) or 📦 “Energy” (pass-through)
own power by type PropC9F0 UnitPower("player", type) 📦 preset “Energy” / “Combo Points”, parameters player,type
target power by type Prop45C4 UnitPower("target", type) Same, parameters target,type
own power % PropD3D9 getPowerById("player", type) ✅ Code 4
target power % Prop6512 getPowerById("target", type) ✅ Code 4
target range Prop45C5 getTargetRange() (probes several APIs in turn) ✅ Code 5 (downgrades) or 📦 “In Spell Range”
target castable, line of sight Prop45C6 UnitInLos2() (error event tracking) ✅ Code 6

Power type mapping (from the old dropdown hint): -2 HealthCost, 0 Mana, 1 Rage, 2 Focus, 3 Energy, 4 Combo Points, 5 Runes, 6 Runic Power, 7 Soul Shards, 8 Lunar Power, 9 Holy Power, 11 Maelstrom, 12 Chi, 13 Insanity, 16 Arcane Charges, 17 Fury, 18 Pain; leave empty = current primary resource.

Code 4: Power percent (parameter 1: unit; parameter 2: type, optional). If both values are protected in combat the result is 0. The new property library already has a preset “Power Percentage” 📦: on retail it passes UnitPowerPercent through when available and only falls back to the division below otherwise. Prefer the pass-through version and do the comparison on the desktop side:

local p = SuperDPS.SN(UnitPower(arg1, arg2), "powpct", arg1)
local m = SuperDPS.SN(UnitPowerMax(arg1, arg2), "powpct", arg1)
if m == 0 then return 0 end
return p / m * 100

Code 5: Target range (parameters: none; returns 255 when there is no target or the value cannot be read, same as the old semantics). UnitDistanceSquared may be restricted for hostile units on Midnight, so availability depends on in-game testing. To check range for a specific spell, use the preset “In Spell Range” instead:

if not UnitExists("target") then return 255 end
local ok, d2 = pcall(UnitDistanceSquared, "target")
if ok then
d2 = SuperDPS.SN(d2, "range")
if d2 > 0 then return math.sqrt(d2) end
end
return 255

Code 6: Target castable (line of sight / out of range) (parameters: none). Old semantics: the target exists and is visible, and no “out of range / line of sight” error has been reported against it within the last 1.5 s.

Put this in the init code (sharing one frame with Code 1 is fine too: one frame can register several events and branch in the callback; when merging the two, create only one frame):

if not SuperDPS.LosFrame then
SuperDPS.LosSeen = {}
local f = CreateFrame("Frame")
f:RegisterEvent("UI_ERROR_MESSAGE")
f:SetScript("OnEvent", function(_, _, _, msg)
if msg == ERR_OUT_OF_RANGE or msg == SPELL_FAILED_LINE_OF_SIGHT then
local g = UnitGUID("target")
if g then SuperDPS.LosSeen[g] = GetTime() end
end
end)
SuperDPS.LosFrame = f
end

Property code:

local g = UnitGUID("target")
if not g or not UnitIsVisible("target") then return 0 end
local t = SuperDPS.LosSeen[g]
if not t or GetTime() - t >= 1.5 then return 1 end
return 0
Old property Old Key Old implementation New version
Combo Points PropC20A GetComboPoints("player","target") 📦 preset “Combo Points” (UnitPower(unit,4))
DK runes available PropC20B GetRune() ✅ Code 7
DK rune slot cooldown PropC20B2 GetRuneByType2(slot) ✅ Code 8
DK Blood runes PropC20C GetRuneByType(1) ❌ Retail has had no rune types since Legion; Classic clients still have types, but the old implementation relied on internal fields of Blizzard’s rune frame and must be rewritten and verified by you
DK Frost runes PropC20D GetRuneByType(3) ❌ Same as above
DK Unholy runes PropC20E GetRuneByType(2) ❌ Same as above
DK Death runes PropC20F GetRuneByType(4) ❌ Same as above
has summoned pet PropC20G hasPet() (HasPetUI) 📦 preset “Has Pet”; to distinguish hunter pets use ✅ Code 9

Rune cooldowns risk being protected in combat (which is why no official preset was ported). The implementations below are sanitized and yield 0 when protected:

Code 7: Total available runes (parameters: none):

local n = 0
for i = 1, 6 do
local _, _, ready = GetRuneCooldown(i)
if SuperDPS.SN(ready, "rune") == true then n = n + 1 end
end
return n

Code 8: Rune slot cooldown remaining (parameter: 1~6):

local start, duration = GetRuneCooldown(arg1)
start = SuperDPS.SN(start, "runecd")
duration = SuperDPS.SN(duration, "runecd")
if start == 0 or duration == 0 then return 0 end
local remain = start + duration - GetTime()
if remain > 0 then return remain end
return 0

Code 9: Has summoned pet (old three-state semantics) (parameters: none; 2 = hunter pet, 1 = other pet, 0 = none):

local hasUI, isHunterPet = HasPetUI()
if hasUI and isHunterPet then return 2 end
if hasUI then return 1 end
return 0
Old property Old Key Old implementation New version
ranged weapon speed PropC51C UnitRangedDamage("player")*100 ❌ The attack-speed family returns protected values on Midnight (the same thing that killed the swing timer)
spell cooldown PropAab3 GetSpellTime(id) (start+duration−now) 📦 preset “Spell Cooldown” / “Spell Ready” (the old formula hits protection in combat)
spell charge cooldown PropAab4 GetSpellTime2(id) ✅ Code 10
spell cast time Prop9Bf3 SkillCastingTime(id) (measured and cached) ✅ Code 11
time since cast Prop9Bf4 getSpellCastTime(id) (event tracking) ✅ Code 12
time since cast, per target Prop9Bf5 getSpellCastTime2(id) ✅ Code 12 (toggle the commented line)
DoT tick timer, per target Prop9Bf6 getSpellCastDamage (already a debuff approximation after CLEU was blocked) 📦 preset “Own Debuff Remaining”
spell charges PropC74D GetSpellCharges(id) 📦 preset “Spell Charges”
spell cast count Prop70Ef GetSpellCount(id) ✅ Code 13
item count Prop6F49 GetItemCount(id) 📦 preset “Item Count”
raid buff count Prop1F0E getRaidBuffCount(id) ✅ Code 14
raid buff count, own Prop98F1 getRaidBuffCountByPlayer(id) ✅ Code 14, append ,1 to the parameters

Code 10: Spell charge cooldown remaining (parameter: spell ID):

local _, _, start, duration = GetSpellCharges(arg1)
start = SuperDPS.SN(start, "chargecd")
duration = SuperDPS.SN(duration, "chargecd")
if start == 0 or duration == 0 then return 0 end
local remain = start + duration - GetTime()
if remain > 0 then return remain end
return 0

Code 11: Spell cast time (parameter: spell ID; returns seconds. The old unit was milliseconds/10, i.e. centiseconds ×10, so convert old thresholds accordingly). Uses static spell data instead of measuring and caching:

local info = C_Spell.GetSpellInfo(arg1)
if not info then return 0 end
return SuperDPS.SN(info.castTime, "casttime") / 1000

Code 12: Time since cast (parameter: spell ID; capped at 120, returns 120 if never cast, same as the old semantics). The old version relied on the addon’s main event frame; the new version has the module create its own. Note the key changed from spell name to spell ID.

Put this in the init code:

if not SuperDPS.CastLogFrame then
SuperDPS.CastAt = {}
SuperDPS.CastAtTarget = {}
local f = CreateFrame("Frame")
f:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
f:SetScript("OnEvent", function(_, _, unit, _, spellID)
if unit == "player" and spellID then
SuperDPS.CastAt[spellID] = GetTime()
SuperDPS.CastAtTarget[spellID .. "_" .. (UnitGUID("target") or "")] = GetTime()
end
end)
SuperDPS.CastLogFrame = f
end

Property code:

local t = SuperDPS.CastAt[arg1]
-- Per-target version: use the next line instead (times the same spell separately for each target)
-- local t = SuperDPS.CastAtTarget[arg1 .. "_" .. (UnitGUID("target") or "")]
if not t then return 120 end
return math.min(GetTime() - t, 120)

Code 13: Spell cast count (parameter: spell ID; “how many more casts are available”, as with Kill Shot-style spells):

return C_Spell.GetSpellCastCount and C_Spell.GetSpellCastCount(arg1) or 0

Code 14: Raid buff count (parameter 1: aura ID or name; parameter 2: set to 1 to count only auras cast by yourself):

local n = 0
for i = 1, 40 do
local u = "raid" .. i
if UnitExists(u) and not UnitIsDeadOrGhost(u) and UnitIsPlayer(u) then
local name, _, _, _, _, _, source = SuperDPS.GetUnitBuff(u, arg1)
if name and (not arg2 or source == "player") then n = n + 1 end
end
end
return n

The old version had 31 properties in this family, all combinations of “unit × buff/debuff × remaining time/stacks × own-cast only”. The new version covers every combination with 4 generic pieces of code plus parameters. Unit is player/pet/target/focus; aura is a spell ID (recommended, avoids language differences) or a name.

Old property (by group) Old Key New version
own/pet buff remaining Prop6Ea9 / Prop3417 📦 preset “Buff Remaining”, parameters player,ID / pet,ID
own buff stacks PropC16A 📦 preset “Buff Stacks”
own buff remaining/stacks, own-cast Prop6364 / Prop182B ✅ Code 15/16, append ,1 to the parameters
own debuff remaining/stacks PropE369 / Prop1C38 📦 “Debuff Remaining” / “Debuff Stacks”, parameters player,ID
target buff remaining/stacks, ± own-cast Prop19Ca / PropA5Bf / PropA577 / PropD67D ✅ Code 15/16, unit target
focus buff remaining/stacks, ± own-cast Prop19Ca1 / PropA5Bf1 / PropA5771 / PropD67D1 ✅ Code 15/16, unit focus
target debuff remaining/stacks, ± own-cast PropD645 / Prop3416 / PropA1D0 / Prop17E6 📦 “Debuff Remaining” “Debuff Stacks” “Own Debuff Remaining” or ✅ Code 17
focus debuff remaining/stacks, ± own-cast PropD6451 / Prop34161 / PropA1D01 / Prop17E61 Same, unit focus
aura icon remaining/stacks, ± own-cast Prop3C59 / PropB6D7 / Prop3769 / Prop1Ff1 (self)
Prop8E29 / Prop4E73 / Prop02E7 / Prop33E7 (target)
✅ Code 18 (full search over buffs + debuffs)

Code 15: Buff remaining time (generic) (parameters: unit,aura[,1 for own-cast only]). Keeps the old semantics: permanent buffs and buffs that just expired return 1, absent returns 0. The preset “Buff Remaining” returns 0 for permanent buffs; old modules whose logic depends on this difference should use this version:

local name, _, _, _, _, expire, source = SuperDPS.GetUnitBuff(arg1, arg2)
if not name or (arg3 and source ~= "player") then return 0 end
expire = SuperDPS.SN(expire, "bufftime", arg1)
local remain = expire - GetTime()
if remain < 0 then return 1 end
return remain

Code 16: Buff stacks (generic) (parameters: unit,aura[,1 for own-cast only]):

local name, _, count, _, _, _, source = SuperDPS.GetUnitBuff(arg1, arg2)
if not name or (arg3 and source ~= "player") then return 0 end
return count or 0

Code 17: Debuff remaining/stacks (generic) (parameters: unit,aura[,1 for own-cast only]). Debuffs have no “expired → 1” clamp (historical behavior, deliberately different from the buff version):

local name, _, count, _, _, expire, source = SuperDPS.GetUnitDebuff(arg1, arg2)
if not name or (arg3 and source ~= "player") then return 0 end
expire = SuperDPS.SN(expire, "debufftime", arg1)
if expire == 0 then return 0 end
return expire - GetTime()
-- Stacks version: delete the three lines above and return count or 0 instead

Code 18: Aura icon remaining (full search over buffs + debuffs) (parameters: unit,aura[,1 for own-cast only]). The old “icon” family did not distinguish buffs from debuffs and searched everything by name or ID, which maps to the new GetUnitAura without a filter:

local name, _, count, _, _, expire, source = SuperDPS.GetUnitAura(arg1, arg2)
if not name or (arg3 and source ~= "player") then return 0 end
expire = SuperDPS.SN(expire, "auratime", arg1)
local remain = expire - GetTime()
if remain < 0 then return 1 end
return remain
-- Stacks version: delete the four lines above and return count or 0 instead
Old property Old Key Old implementation New version
nameplates with own debuff Prop17E7 getNamePlateDebuffCountByPlayer(id) ✅ Code 19
nameplates within range Prop17E8 getNamePlateRangeCount(yards) ✅ Code 20 (the distance API is risky) or 📦 “Enemy Count” (no range limit)

Nameplate units are limited by the in-game “enemy nameplates” toggle (V key by default) and the nameplate display distance.

Code 19: Number of nameplates carrying my DoT (parameter: debuff ID; for AoE DoT-refresh checks):

local n = 0
for i = 1, 40 do
local u = "nameplate" .. i
if UnitExists(u) then
local name, _, _, _, _, _, source = SuperDPS.GetUnitDebuff(u, arg1)
if name and source == "player" then n = n + 1 end
end
end
return n

Code 20: Number of nameplates within N yards (parameter: yards, e.g. 8). Relies on UnitDistanceSquared; availability for hostile units on Midnight depends on in-game testing, and units that cannot be read are not counted:

local n = 0
local r2 = arg1 * arg1
for i = 1, 40 do
local u = "nameplate" .. i
if UnitExists(u) then
local ok, d2 = pcall(UnitDistanceSquared, u)
if ok then
d2 = SuperDPS.SN(d2, "nprange")
if d2 > 0 and d2 <= r2 then n = n + 1 end
end
end
end
return n
Old property Old Key Old implementation New version
target cast code PropF717 CastingInfo("target") (ID%255) 📦 preset “Target Cast” (full ID, no more %255)
focus cast code PropF718 CastingInfo("focus") Same, parameter focus
own cast code Prop6C83 CastingInfo("player") Same, parameter player
own cast progress Prop7F39 CastingTime("player") ((end−now)/100) 📦 preset “Cast Remaining” (in seconds), parameter player
target cast progress Prop7F38 CastingTime("target") Same, parameter target

The old “cast code” took the spell ID modulo 255 (single-byte channel limit); conditions in old modules written against the modulo value must be changed to the full spell ID. The old “cast progress” unit was 0.1 s (milliseconds/100); the preset returns seconds.

Old property Old Key Old implementation New version
target in spell range PropD9D4 IsSpellInRange(id) 📦 preset “In Spell Range”
party dispel type check Prop67C6 GetDeBuffType(type) ✅ Code 21
lowest-health member index Prop642E getDamagePlayer() ✅ Code 22
lowest-health member damage % PropF457 getDamagePlayerHealth() ✅ Code 22 (switch the return value)
lowest-health index, with buff PropC0C7 getDamagePlayerBuff(id) ✅ Code 22, mode hasBuff
lowest-health damage %, with buff Prop2838 getDamagePlayerHealthBuff(id) Same
lowest-health index, without buff Prop9A11 getDamagePlayerNoBuff(id) ✅ Code 22, mode noBuff
lowest-health damage %, without buff PropD82C getDamagePlayerHealthNoBuff(id) Same
lowest-health index, without debuff PropA684 getDamagePlayerNoDeBuff(id) ✅ Code 22, mode noDebuff
lowest-health damage %, without debuff PropB53B getDamagePlayerHealthNoDeBuff(id) Same

Index convention (same as the old version, used on the desktop side to pick a macro target by index): 1 = yourself, 25 = party14, 645 = raid140. Solo counts only yourself, a party counts yourself + party, a raid counts only raid units.

Code 21: Party dispel type check (parameter: Curse/Disease/Magic/Poison; returns the index of the first affected member, or 0 if nobody is affected):

local function hit(unit)
if UnitIsDeadOrGhost(unit) or not UnitIsPlayer(unit) then return false end
for j = 1, 40 do
local name, _, _, dispelType = UnitDebuff(unit, j)
if not name then return false end
if dispelType == arg1 then return true end
end
return false
end
if UnitPlayerOrPetInRaid("player") then
for i = 1, 40 do
local u = "raid" .. i
if UnitExists(u) and hit(u) then return i + 5 end
end
elseif UnitPlayerOrPetInParty("player") then
if hit("player") then return 1 end
for i = 1, 4 do
local u = "party" .. i
if UnitExists(u) and hit(u) then return i + 1 end
end
elseif hit("player") then
return 1
end
return 0

Code 22: Lowest-health party member (whole family unified) (parameter 1: mode, empty/hasBuff/noBuff/noDebuff; parameter 2: aura ID, may be omitted when mode is empty). All 8 old properties share one scan, organized as “shared function defined in init code + thin call in the property row”, so multiple rows reuse it with zero duplication. Important on Midnight: health percent is sanitized, so members whose value is protected in combat are treated as full health and skipped; in heavy combat this whole family may degrade. The official presets deliberately did not port it, so healing modules should rely on it with care. The old version also applied a line-of-sight filter (error events); if you need it, add a section based on the LosSeen table from Code 6. One more semantic tweak: the old damage baseline was -1 (with everyone at full health it still picked the first qualifying member); this version’s baseline is 0 (returns 0 when nobody is damaged).

Put this in the init code:

-- Lowest-health group member scan, shared by several property rows. Returns: index, missing health percent (0~100)
function SuperDPS.ScanLowHp(mode, aura)
local best, bestDamage = 0, 0
local function consider(idx, unit)
if not UnitExists(unit) or UnitIsDeadOrGhost(unit) or not UnitIsPlayer(unit) then return end
if mode == "hasBuff" or mode == "noBuff" then
local name = SuperDPS.GetUnitBuff(unit, aura)
if (mode == "hasBuff") ~= (name ~= nil) then return end
elseif mode == "noDebuff" then
if SuperDPS.GetUnitDebuff(unit, aura) then return end
end
local g = UnitGUID(unit)
if not g then return end
local hp = SuperDPS.SN(UnitPercentHealthFromGUID(g), "lowhp", unit)
if hp == 0 then return end -- dead / unreadable / protected → skip
local damage = (1 - hp) * 100
if damage > bestDamage then best, bestDamage = idx, damage end
end
if UnitPlayerOrPetInRaid("player") then
for i = 1, 40 do consider(i + 5, "raid" .. i) end
else
consider(1, "player")
if UnitPlayerOrPetInParty("player") then
for i = 1, 4 do consider(i + 1, "party" .. i) end
end
end
return best, bestDamage
end

Property code (“index” version):

local idx = SuperDPS.ScanLowHp(arg1, arg2)
return idx

Property code (“damage %” version):

local _, damage = SuperDPS.ScanLowHp(arg1, arg2)
return damage
Old property Old Key Old implementation New version
main-hand weapon CD Prop9F61 getSwingTimer("main") ❌ UnitAttackSpeed returns a protected value on Midnight, so swing timing cannot be implemented (confirmed in-game; the whole subsystem was removed for this reason)
main-hand weapon CD, positive Prop9F62 getSwingTimer2("main") ❌ Same as above
off-hand weapon CD Prop72B3 getSwingTimer("off") ❌ Same as above
ranged weapon CD Prop072B getSwingTimer("ranged") ❌ Same as above
main-hand temporary enchant ID Prop66F0 GetWeaponEnchantInfoMain() (%255) ✅ Code 23
off-hand temporary enchant ID Prop093F GetWeaponEnchantInfoOff() ✅ Code 23

Code 23: Weapon temporary enchant ID (parameter: 4 main hand / 8 off hand; full ID, no more %255, so conditions in old modules written against the modulo value must be changed to the full enchant ID):

local id = select(arg1, GetWeaponEnchantInfo())
return id or 0
Old property Old Key Old implementation New version
button glow check Prop44F6 checkButtonOverlay(button) ✅ Code 24
UI visibility check Prop44F7 UiCheck(frame) ✅ Code 25
current map ID Prop44F8 MapId() (%255) ✅ Code 26
raid total damage % Prop03Af getAllHealth() (average damage %) ✅ Code 27 (same degradation note as Code 22)
spell power Prop0311 GetSpellPower(divisor) ✅ Code 28

Code 24: Button glow check (parameter: ActionButton1~ActionButton12). The old version checked the overlay field; the spell activation glow framework in newer clients renamed the field, so both are checked:

local b = _G[arg1]
if b and (b.overlay or b.SpellActivationAlert) then return 1 end
return 0

Code 25: UI visibility check (parameter: the frame’s global name, e.g. PVPFrame):

local f = _G[arg1]
if f and f.IsVisible and f:IsVisible() then return 1 end
return 0

Code 26: Current map ID (parameters: none; full ID, no more %255):

return C_Map.GetBestMapForUnit("player") or 0

Code 27: Raid average damage percent (parameters: none; 0~100):

local total, n = 0, 0
local function acc(unit)
if not UnitExists(unit) or UnitIsDeadOrGhost(unit) then return end
local g = UnitGUID(unit)
if not g then return end
local hp = SuperDPS.SN(UnitPercentHealthFromGUID(g), "allhp", unit)
if hp == 0 then return end
total = total + (1 - hp)
n = n + 1
end
if UnitPlayerOrPetInRaid("player") then
for i = 1, 40 do acc("raid" .. i) end
else
acc("player")
if UnitPlayerOrPetInParty("player") then
for i = 1, 4 do acc("party" .. i) end
end
end
if n == 0 then return 0 end
return total / n * 100

Code 28: Spell power (parameter: divisor. The old version used it to squeeze large values into the single-byte channel; in the new version fill 1 to get the raw value). Stat values may be protected in combat and become 0 after sanitizing:

local v = SuperDPS.SN(GetSpellBonusDamage(7), "spellpower")
return v / (arg1 or 1)

Old property Reason
The whole swing timer family (main-hand/off-hand/ranged CD, ranged weapon speed) UnitAttackSpeed/UnitRangedDamage return protected values on Midnight and throw on comparison (confirmed in-game 2026-08; the old addon’s swing subsystem was removed entirely because of this)
DK typed runes (Blood/Frost/Unholy/Death) Retail has had no rune types since Legion, so this only makes sense on Classic; the old implementation also relied on the internal field _G["Rune"..i].rune.runeType of Blizzard’s rune frame (not a public API, breaks easily between versions). To use it, rewrite it yourself and verify on the target client
CLEU-based DoT tick timing Combat log events are hard-blocked for addons (the late old version had already degraded to a debuff-presence approximation; use the preset “Own Debuff Remaining” directly)

Degraded-but-usable list (may be unreadable in combat, treated as 0)

Section titled “Degraded-but-usable list (may be unreadable in combat, treated as 0)”
  • Percent types (codes 2/4/22/27): division/comparison must be sanitized, protected values become 0;
  • Rune types (codes 7/8): cooldown values risk being protected;
  • Range types (codes 5/20): availability of UnitDistanceSquared for hostile units depends on in-game testing;