dpslog: CLEU rename, DPSMate absorb pipeline, WSBT adapter, module version API

- Rename COMBAT_LOG_EVENT -> COMBAT_LOG_EVENT_UNFILTERED across Zig/Lua
- DPSMate CLEU adapter: absorb pipeline (SetUnregisterVariables, Absorb,
  ConfirmAbsorbApplication, UnregisterAbsorb), BuildFail type 2/3,
  "(Periodic)" suffix, removed dead handler
- WSBT (Weird Scrolling Battle Text): CLEU adapter fork of MSBT with
  dispatch table, A/B combat profiling, selective event unregistration
- Module version API: GetWeirdUtilsVersion(name?) Lua function + WeirdUtils
  global table, additive across independent DLLs. Build system passes
  per-module version strings via build_options.
- Hook Glue_LoadScriptFunctions (0x46ABB0) for login screen availability
- Rename lsf_hook -> register_commands_hook (real name: Player_LoadScriptFunctions)
- lua.zig: add setglobal/getglobal via LUA_GLOBALSINDEX (-10001)
This commit is contained in:
MarcelineVQ
2026-03-18 14:20:16 -07:00
parent 86da3bcc03
commit fc762ac200
14 changed files with 6672 additions and 402 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
local L = AceLibrary("AceLocale-2.2"):new("WSBT")
L:RegisterTranslations("deDE", function()
return {
["Debug mode has been enabled."] = "Debug mode has been enabled.",
["Debug mode has been disabled."] = "Debug mode has been disabled.",
["Event search mode has been enabled. Searching for: "] = "Event search mode has been enabled. Searching for: ",
["Event search mode has been disabled."] = "Event search mode has been disabled.",
["The mod is now disabled."] = "The mod is now disabled.",
["The mod is now enabled."] = "The mod is now enabled.",
["Hits"] = "Treffer",
["Crit"] = "Krit",
["Crits"] = "Krits",
["Multiple"] = "Mehrere",
[" (%d vulnerability)"] = " (%d vulnerability)",
[" <\124cff00b37e\124h%d\124h\124r>"] = " <\124cff00b37e\124h%d\124h\124r>",
["Profile Reset"] = "Profile Reset",
["Drowning"] = "Drowning",
["Falling"] = "Falling",
["Fatigue"] = "Fatigue",
["Fire"] = "Fire",
["Lava"] = "Lava",
["Slime"] = "Slime",
}
end)
@@ -0,0 +1,25 @@
local L = AceLibrary("AceLocale-2.2"):new("WSBT")
L:RegisterTranslations("enUS", function()
return {
["Debug mode has been enabled."] = true,
["Debug mode has been disabled."] = true,
["Event search mode has been enabled. Searching for: "] = true,
["Event search mode has been disabled."] = true,
["The mod is now disabled."] = true,
["The mod is now enabled."] = true,
["Hits"] = true,
["Crit"] = true,
["Crits"] = true,
["Multiple"] = true,
[" (%d vulnerability)"] = true,
[" <\124cff00b37e\124h%d\124h\124r>"] = true,
["Profile Reset"] = true,
["Drowning"] = true,
["Falling"] = true,
["Fatigue"] = true,
["Fire"] = true,
["Lava"] = true,
["Slime"] = true,
}
end)
@@ -0,0 +1,25 @@
local L = AceLibrary("AceLocale-2.2"):new("WSBT")
L:RegisterTranslations("frFR", function()
return {
["Debug mode has been enabled."] = "Debug mode has been enabled.",
["Debug mode has been disabled."] = "Debug mode has been disabled.",
["Event search mode has been enabled. Searching for: "] = "Event search mode has been enabled. Searching for: ",
["Event search mode has been disabled."] = "Event search mode has been disabled.",
["The mod is now disabled."] = "The mod is now disabled.",
["The mod is now enabled."] = "The mod is now enabled.",
["Hits"] = "Coups",
["Crit"] = "Crit",
["Crits"] = "Crits",
["Multiple"] = "Multiple",
[" (%d vulnerability)"] = " (%d vulnérabilité)",
[" <\124cff00b37e\124h%d\124h\124r>"] = " <\124cff00b37e\124h%d\124h\124r>",
["Profile Reset"] = "Profile Reset",
["Drowning"] = "Noyade",
["Falling"] = "Chute",
["Fatigue"] = "Fatigue",
["Fire"] = "Feu",
["Lava"] = "Lave",
["Slime"] = "Gelée",
}
end)
+22
View File
@@ -0,0 +1,22 @@
## Interface: 11200
## Version: 4.43-WSBT
## Title: Weird Scrolling Battle Text
## Notes: Scrolls battle information around the character model. CLEU-powered fork of MSBT.
## OptionalDeps: SW_FixLogStrings
## SavedVariables: MikSBT_Save
Libs\AceLibrary\AceLibrary.lua
Libs\AceLocale-2.2\AceLocale-2.2.lua
Libs\BabbleSpell-2.2\Babble-Spell-2.2.lua
MikScrollingBattleText-enUS.lua
MikScrollingBattleText-frFR.lua
MikScrollingBattleText-deDE.lua
localization.lua
MikTableRecyclerObject.lua
MikCombatEventHelper.lua
MikScrollingBattleText.lua
MikCombatEventHelper.xml
MikScrollingBattleText.xml
WSBT_CLEUAdapter.lua
+578
View File
@@ -0,0 +1,578 @@
-- WSBT CLEU Adapter
-- Replaces MikCEH's CHAT_MSG_* string parsing with structured COMBAT_LOG_EVENT_UNFILTERED.
-- Toggle: /wsbtcleu (switches between CLEU adapter and original string parser)
-- Requires: DPSLog module (provides COMBAT_LOG_EVENT_UNFILTERED)
-- Wait for MikCEH to be initialized
if not MikCEH or not MikCEH.SendEvent then return end
local CEH = MikCEH
local SendEvent = MikCEH.SendEvent
local GetDamageData = MikCEH.GetDamageEventData
local GetHealData = MikCEH.GetHealEventData
local GetNotifData = MikCEH.GetNotificationEventData
-- ============================================================================
-- Constants (local refs for speed)
-- ============================================================================
local INCOMING = CEH.DIRECTIONTYPE_PLAYER_INCOMING
local OUTGOING = CEH.DIRECTIONTYPE_PLAYER_OUTGOING
local PET_OUT = CEH.DIRECTIONTYPE_PET_OUTGOING
local PET_IN = CEH.DIRECTIONTYPE_PET_INCOMING
local HIT = CEH.ACTIONTYPE_HIT
local MISS = CEH.ACTIONTYPE_MISS
local DODGE = CEH.ACTIONTYPE_DODGE
local PARRY = CEH.ACTIONTYPE_PARRY
local BLOCK = CEH.ACTIONTYPE_BLOCK
local RESIST = CEH.ACTIONTYPE_RESIST
local ABSORB = CEH.ACTIONTYPE_ABSORB
local IMMUNE = CEH.ACTIONTYPE_IMMUNE
local EVADE = CEH.ACTIONTYPE_EVADE
local REFLECT = CEH.ACTIONTYPE_REFLECT
local HIT_NORMAL = CEH.HITTYPE_NORMAL
local HIT_CRIT = CEH.HITTYPE_CRIT
local HIT_DOT = CEH.HITTYPE_OVER_TIME
local HEAL_NORMAL = CEH.HEALTYPE_NORMAL
local HEAL_CRIT = CEH.HEALTYPE_CRIT
local HEAL_HOT = CEH.HEALTYPE_OVER_TIME
local DMG_PHYSICAL = CEH.DAMAGETYPE_PHYSICAL
local DMG_UNKNOWN = CEH.DAMAGETYPE_UNKNOWN
local PARTIAL_ABSORB = CEH.PARTIALACTIONTYPE_ABSORB
local PARTIAL_BLOCK = CEH.PARTIALACTIONTYPE_BLOCK
local PARTIAL_RESIST = CEH.PARTIALACTIONTYPE_RESIST
local PARTIAL_CRUSHING = CEH.PARTIALACTIONTYPE_CRUSHING
local PARTIAL_GLANCING = CEH.PARTIALACTIONTYPE_GLANCING
local PARTIAL_OVERHEAL = CEH.PARTIALACTIONTYPE_OVERHEAL
local NOTIF_DEBUFF = CEH.NOTIFICATIONTYPE_DEBUFF
local NOTIF_BUFF = CEH.NOTIFICATIONTYPE_BUFF
local NOTIF_BUFF_FADE = CEH.NOTIFICATIONTYPE_BUFF_FADE
local NOTIF_POWER_GAIN = CEH.NOTIFICATIONTYPE_POWER_GAIN
local NOTIF_POWER_LOSS = CEH.NOTIFICATIONTYPE_POWER_LOSS
-- Miss type string → MikCEH action type
local missActionMap = {
MISS = MISS,
DODGE = DODGE,
PARRY = PARRY,
BLOCK = BLOCK,
RESIST = RESIST,
ABSORB = ABSORB,
IMMUNE = IMMUNE,
EVADE = EVADE,
REFLECT = REFLECT,
DEFLECT = MISS, -- no DEFLECT in MSBT, treat as miss
}
-- WoW spell school bitmask → MSBT damage type
-- 1=Physical, 2=Holy, 4=Fire, 8=Nature, 16=Frost, 32=Shadow, 64=Arcane
local schoolMap = {
[1] = 1, -- Physical
[2] = 2, -- Holy
[4] = 4, -- Fire
[8] = 3, -- Nature
[16] = 5, -- Frost
[32] = 6, -- Shadow
[64] = 7, -- Arcane
}
local function schoolToDamageType(school)
if not school or school == 0 then return DMG_PHYSICAL end
return schoolMap[school] or DMG_UNKNOWN
end
-- Power type ID → localized string
local powerTypeNames = {
[0] = MANA or "Mana",
[1] = RAGE or "Rage",
[2] = "Focus",
[3] = ENERGY or "Energy",
}
-- ============================================================================
-- Player/pet identity
-- ============================================================================
local playerName = UnitName("player")
local petName = UnitName("pet")
local identityFrame = CreateFrame("Frame")
identityFrame:RegisterEvent("UNIT_NAME_UPDATE")
identityFrame:RegisterEvent("PLAYER_PET_CHANGED")
identityFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
identityFrame:SetScript("OnEvent", function()
playerName = UnitName("player")
petName = UnitName("pet")
end)
local function getDirection(srcName, dstName)
-- Returns direction, counterpartName
if srcName == playerName then
return OUTGOING, dstName
elseif dstName == playerName then
return INCOMING, srcName
elseif petName and srcName == petName then
return PET_OUT, dstName
elseif petName and dstName == petName then
return PET_IN, srcName
end
return nil, nil -- not relevant to player/pet
end
-- ============================================================================
-- Toggle state
-- ============================================================================
local cleuActive = false
local cleuFrame = CreateFrame("Frame")
local function enableCLEU()
-- Only unregister CHAT_MSG_* events; keep core events (PLAYER_REGEN_*, UNIT_HEALTH, etc.)
MikCEH.UnregisterCombatParseEvents()
cleuFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
cleuActive = true
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00WSBT CLEU|r: |cff00ff00ON|r (structured events)")
end
local function enableOriginal()
cleuFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
MikCEH.RegisterCombatParseEvents()
cleuActive = false
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00WSBT CLEU|r: |cffff4444OFF|r (string parser)")
end
local function toggle()
if cleuActive then enableOriginal() else enableCLEU() end
end
SLASH_WSBTCLEU1 = "/wsbtcleu"
SlashCmdList["WSBTCLEU"] = function(msg)
if msg == "on" then
if not cleuActive then enableCLEU() end
elseif msg == "off" then
if cleuActive then enableOriginal() end
elseif msg == "status" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00WSBT CLEU|r: " .. (cleuActive and "|cff00ff00ON|r" or "|cffff4444OFF|r"))
else
toggle()
end
end
-- ============================================================================
-- Partial action helpers
-- ============================================================================
local function applyPartials(eventData, resisted, blocked, absorbed, glancing, crushing)
-- Only one partial action per event in MSBT's model
if crushing and crushing == 1 then
eventData.PartialActionType = PARTIAL_CRUSHING
elseif glancing and glancing == 1 then
eventData.PartialActionType = PARTIAL_GLANCING
elseif absorbed and absorbed > 0 then
eventData.PartialActionType = PARTIAL_ABSORB
eventData.PartialAmount = absorbed
elseif blocked and blocked > 0 then
eventData.PartialActionType = PARTIAL_BLOCK
eventData.PartialAmount = blocked
elseif resisted and resisted > 0 then
eventData.PartialActionType = PARTIAL_RESIST
eventData.PartialAmount = resisted
end
end
-- ============================================================================
-- CLEU handler
-- ============================================================================
local envActionMap = {
DROWNING = CEH.ACTIONTYPE_DROWNING,
FALLING = CEH.ACTIONTYPE_FALLING,
FATIGUE = CEH.ACTIONTYPE_FATIGUE,
FIRE = CEH.ACTIONTYPE_FIRE,
LAVA = CEH.ACTIONTYPE_LAVA,
SLIME = CEH.ACTIONTYPE_SLIME,
EXHAUSTED = CEH.ACTIONTYPE_FATIGUE,
}
local cleuHandler
cleuHandler = function()
local sub = arg1
if not sub then return end
local srcName = arg3
local dstName = arg5
if not srcName or srcName == "" then srcName = "Unknown" end
if not dstName or dstName == "" then dstName = "Unknown" end
-- ========================================================================
-- SWING_DAMAGE
-- args: amount(6), overkill(7), school(8), resisted(9), blocked(10),
-- absorbed(11), critical(12), glancing(13), crushing(14)
-- ========================================================================
if sub == "SWING_DAMAGE" then
local dir, name = getDirection(srcName, dstName)
if not dir then return end
local amount = arg6 or 0
local resisted = arg9 or 0
local blocked = arg10 or 0
local absorbed = arg11 or 0
local critical = arg12 == 1
local glancing = arg13 == 1 and 1 or 0
local crushing = arg14 == 1 and 1 or 0
local hitType = critical and HIT_CRIT or HIT_NORMAL
local data = GetDamageData(dir, HIT, hitType, DMG_PHYSICAL, amount, nil, name)
applyPartials(data, resisted, blocked, absorbed, glancing, crushing)
SendEvent(data)
-- ========================================================================
-- SWING_MISSED
-- args: missType(6), amountMissed(7)
-- ========================================================================
elseif sub == "SWING_MISSED" then
local dir, name = getDirection(srcName, dstName)
if not dir then return end
local action = missActionMap[arg6] or MISS
local data = GetDamageData(dir, action, nil, nil, nil, nil, name)
SendEvent(data)
-- ========================================================================
-- SPELL_DAMAGE / RANGE_DAMAGE / SPELL_PERIODIC_DAMAGE / DAMAGE_SHIELD / DAMAGE_SPLIT
-- prefix: spellId(6), spellName(7), spellSchool(8)
-- suffix: amount(9), overkill(10), school(11), resisted(12), blocked(13),
-- absorbed(14), critical(15), glancing(16), crushing(17)
-- ========================================================================
elseif sub == "SPELL_DAMAGE" or sub == "RANGE_DAMAGE" or sub == "DAMAGE_SHIELD"
or sub == "DAMAGE_SPLIT" then
local dir, name = getDirection(srcName, dstName)
if not dir then return end
local spellName = arg7
local amount = arg9 or 0
local school = arg11
local resisted = arg12 or 0
local blocked = arg13 or 0
local absorbed = arg14 or 0
local critical = arg15 == 1
local crushing = arg17 == 1 and 1 or 0
local hitType = critical and HIT_CRIT or HIT_NORMAL
local dmgType = schoolToDamageType(school)
local data = GetDamageData(dir, HIT, hitType, dmgType, amount, spellName, name)
applyPartials(data, resisted, blocked, absorbed, 0, crushing)
SendEvent(data)
elseif sub == "SPELL_PERIODIC_DAMAGE" then
local dir, name = getDirection(srcName, dstName)
if not dir then return end
local spellName = arg7
local amount = arg9 or 0
local school = arg11
local resisted = arg12 or 0
local absorbed = arg14 or 0
local dmgType = schoolToDamageType(school)
local data = GetDamageData(dir, HIT, HIT_DOT, dmgType, amount, spellName, name)
applyPartials(data, resisted, 0, absorbed, 0, 0)
SendEvent(data)
-- ========================================================================
-- SPELL_MISSED / RANGE_MISSED / SPELL_PERIODIC_MISSED / DAMAGE_SHIELD_MISSED
-- prefix: spellId(6), spellName(7), spellSchool(8)
-- suffix: missType(9), amountMissed(10)
-- ========================================================================
elseif sub == "SPELL_MISSED" or sub == "RANGE_MISSED"
or sub == "SPELL_PERIODIC_MISSED" or sub == "DAMAGE_SHIELD_MISSED" then
local dir, name = getDirection(srcName, dstName)
if not dir then return end
local spellName = arg7
local action = missActionMap[arg9] or MISS
local data = GetDamageData(dir, action, nil, nil, nil, spellName, name)
SendEvent(data)
-- ========================================================================
-- ENVIRONMENTAL_DAMAGE
-- args: envType(6), amount(7), overkill(8), school(9), resisted(10),
-- blocked(11), absorbed(12), critical(13), glancing(14), crushing(15)
-- ========================================================================
elseif sub == "ENVIRONMENTAL_DAMAGE" then
if dstName ~= playerName then return end
local envType = arg6
local amount = arg7 or 0
local action = envActionMap[envType] or HIT
local data = GetDamageData(INCOMING, action, HIT_NORMAL, DMG_PHYSICAL, amount, nil, envType or "Environment")
SendEvent(data)
-- ========================================================================
-- SPELL_HEAL / SPELL_PERIODIC_HEAL
-- prefix: spellId(6), spellName(7), spellSchool(8)
-- suffix: amount(9), overheal(10), absorbed(11), critical(12)
-- ========================================================================
elseif sub == "SPELL_HEAL" or sub == "SPELL_PERIODIC_HEAL" then
local dir, name = getDirection(srcName, dstName)
if not dir then return end
local spellName = arg7
local amount = arg9 or 0
local overheal = arg10 or 0
local critical = arg12 == 1
local isPeriodic = (sub == "SPELL_PERIODIC_HEAL")
local healType
if isPeriodic then
healType = HEAL_HOT
elseif critical then
healType = HEAL_CRIT
else
healType = HEAL_NORMAL
end
-- For outgoing heals, name = target (dstName); for incoming, name = healer (srcName)
local data = GetHealData(dir, healType, amount, spellName, name)
-- Overheal partial
if overheal > 0 then
data.PartialActionType = PARTIAL_OVERHEAL
data.PartialAmount = overheal
end
SendEvent(data)
-- ========================================================================
-- SPELL_ENERGIZE / SPELL_PERIODIC_ENERGIZE
-- prefix: spellId(6), spellName(7), spellSchool(8)
-- suffix: amount(9), powerType(10)
-- ========================================================================
elseif sub == "SPELL_ENERGIZE" or sub == "SPELL_PERIODIC_ENERGIZE" then
if dstName ~= playerName then return end
local spellName = arg7
local amount = arg9 or 0
local powerType = arg10 or 0
local powerName = powerTypeNames[powerType] or "Mana"
local data = GetNotifData(NOTIF_POWER_GAIN, amount .. " " .. powerName, spellName)
SendEvent(data)
-- ========================================================================
-- SPELL_PERIODIC_DRAIN / SPELL_PERIODIC_LEECH
-- prefix: spellId(6), spellName(7), spellSchool(8)
-- suffix: amount(9), powerType(10), extraAmount(11)
-- ========================================================================
elseif sub == "SPELL_PERIODIC_DRAIN" then
if dstName ~= playerName then return end
local spellName = arg7
local amount = arg9 or 0
local powerType = arg10 or 0
local powerName = powerTypeNames[powerType] or "Mana"
local data = GetNotifData(NOTIF_POWER_LOSS, amount .. " " .. powerName, spellName)
SendEvent(data)
elseif sub == "SPELL_PERIODIC_LEECH" then
-- Leech = damage to target + heal to source
-- Show as damage taken if we're the target
local dir, name = getDirection(srcName, dstName)
if not dir then return end
local spellName = arg7
local amount = arg9 or 0
local dmgType = schoolToDamageType(arg8)
local data = GetDamageData(dir, HIT, HIT_DOT, dmgType, amount, spellName, name)
SendEvent(data)
-- ========================================================================
-- AURA events
-- prefix: spellId(6), spellName(7), spellSchool(8)
-- suffix: auraType(9)
-- ========================================================================
elseif sub == "SPELL_AURA_APPLIED" then
if dstName ~= playerName then return end
local spellName = arg7
local auraType = arg9
if auraType == "DEBUFF" then
local data = GetNotifData(NOTIF_DEBUFF, nil, spellName)
SendEvent(data)
else
local data = GetNotifData(NOTIF_BUFF, nil, spellName)
SendEvent(data)
end
elseif sub == "SPELL_AURA_REMOVED" then
if dstName ~= playerName then return end
local spellName = arg7
local data = GetNotifData(NOTIF_BUFF_FADE, nil, spellName)
SendEvent(data)
-- ========================================================================
-- PARTY_KILL — killing blow notification
-- ========================================================================
elseif sub == "PARTY_KILL" then
if srcName ~= playerName then return end
local notifType = CEH.NOTIFICATIONTYPE_NPC_KILLING_BLOW
if CEH.recentlySelectedPlayers[dstName] then
notifType = CEH.NOTIFICATIONTYPE_PC_KILLING_BLOW
end
local data = GetNotifData(notifType, nil, dstName)
SendEvent(data)
end
end
-- ============================================================================
-- Performance profiling (A/B per-combat)
-- ============================================================================
local profiling = false
local profCLEU = { events = 0, totalMs = 0, gcStart = 0 }
local profOrig = { events = 0, totalMs = 0, gcStart = 0 }
local profCurrent = nil
-- Hook the original MikCEH.OnEvent to measure original mode
-- Only count events that CLEU replaces (combat parse), not honor/XP/rep/health/etc.
local origOnEvent = MikCEH.OnEvent
local cleuReplacedLookup = CEH.cleuReplacedLookup
local function measuredOrigOnEvent()
if profiling and profCurrent == profOrig and cleuReplacedLookup[event] then
local before = debugprofilestop()
origOnEvent()
local after = debugprofilestop()
profOrig.totalMs = profOrig.totalMs + (after - before)
profOrig.events = profOrig.events + 1
else
origOnEvent()
end
end
-- Patch MikCEH.OnEvent so the XML OnEvent handler calls the measured version
MikCEH.OnEvent = measuredOrigOnEvent
local cleuEventsSkipped = 0
local function measuredCLEUHandler()
if profiling and profCurrent == profCLEU then
-- Quick relevance check: is player or pet involved?
local srcName = arg3 or ""
local dstName = arg5 or ""
local relevant = (srcName == playerName or dstName == playerName
or (petName and (srcName == petName or dstName == petName)))
local before = debugprofilestop()
cleuHandler()
local after = debugprofilestop()
if relevant then
profCLEU.totalMs = profCLEU.totalMs + (after - before)
profCLEU.events = profCLEU.events + 1
else
cleuEventsSkipped = cleuEventsSkipped + 1
end
else
cleuHandler()
end
end
cleuFrame:SetScript("OnEvent", measuredCLEUHandler)
local function profReset(tbl)
tbl.events = 0
tbl.totalMs = 0
tbl.gcStart = gcinfo()
end
local lastCLEUAvg = nil
local lastOrigAvg = nil
local function profReport(label, tbl)
local gcEnd = gcinfo()
local gcDelta = gcEnd - tbl.gcStart
local totalMs = tbl.totalMs / 1000
local avgUs = tbl.events > 0 and (tbl.totalMs / tbl.events) or 0
if label == "CLEU" then
lastCLEUAvg = avgUs
else
lastOrigAvg = avgUs
end
local skipMsg = ""
if label == "CLEU" then
skipMsg = string.format(" (%d skipped)", cleuEventsSkipped)
cleuEventsSkipped = 0
end
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff00ff00[WSBT %s]|r %d events%s, %.1fms total, %.1f us/event, %+.1f KB gc",
label, tbl.events, skipMsg, totalMs, avgUs, gcDelta))
if lastCLEUAvg and lastOrigAvg and lastOrigAvg > 0 then
local pct = ((lastCLEUAvg - lastOrigAvg) / lastOrigAvg) * 100
local sign = pct < 0 and "" or "+"
local color = pct < 0 and "|cff00ff00" or "|cffff4444"
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"%s[WSBT CLEU vs ORIGINAL]|r %.1f vs %.1f us/event = %s%.1f%%|r %s",
color, lastCLEUAvg, lastOrigAvg, sign, pct,
pct < 0 and "(CLEU faster)" or "(ORIGINAL faster)"))
end
end
local benchActive = true
local benchFrame = CreateFrame("Frame")
local function benchCombatStart()
if not benchActive then return end
profCurrent = cleuActive and profCLEU or profOrig
profReset(profCurrent)
debugprofilestart()
profiling = true
local label = cleuActive and "CLEU" or "ORIGINAL"
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[WSBT Bench]|r combat started, measuring %s", label))
end
local function benchCombatEnd()
if not benchActive or not profiling then return end
profiling = false
local label = cleuActive and "CLEU" or "ORIGINAL"
profReport(label, profCurrent)
toggle()
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff00ff00[WSBT Bench]|r next combat will use: %s", cleuActive and "CLEU" or "ORIGINAL"))
end
benchFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
benchFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
benchFrame:SetScript("OnEvent", function()
if event == "PLAYER_REGEN_DISABLED" then
benchCombatStart()
elseif event == "PLAYER_REGEN_ENABLED" then
benchCombatEnd()
end
end)
SLASH_WSBTBENCH1 = "/wsbtbench"
SlashCmdList["WSBTBENCH"] = function()
benchActive = not benchActive
if benchActive then
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff00ff00[WSBT Bench]|r enabled. Current mode: %s. Enter combat to begin.",
cleuActive and "CLEU" or "ORIGINAL"))
else
profiling = false
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[WSBT Bench]|r disabled.")
end
end
-- ============================================================================
-- Start in CLEU mode by default
-- ============================================================================
enableCLEU()
File diff suppressed because it is too large Load Diff
+98 -183
View File
@@ -1,10 +1,10 @@
-- DPSMate CLEU Adapter
-- Replaces the string-parsing CHAT_MSG_* event system with structured COMBAT_LOG_EVENT
-- data from the DPSLog module. Registers for COMBAT_LOG_EVENT and calls DPSMate.DB
-- Replaces the string-parsing CHAT_MSG_* event system with structured COMBAT_LOG_EVENT_UNFILTERED
-- data from the DPSLog module. Registers for COMBAT_LOG_EVENT_UNFILTERED and calls DPSMate.DB
-- functions directly with extracted values.
--
-- Toggle: /dpscleu (switches between CLEU adapter and original string parser)
-- Requires: DPSLog module (provides COMBAT_LOG_EVENT + GetSpellInfo)
-- Requires: DPSLog module (provides COMBAT_LOG_EVENT_UNFILTERED + GetSpellInfo)
if not DPSMate or not DPSMate.DB then return end
@@ -75,14 +75,14 @@ local function enableCLEU()
Parser:UnregisterEvent(ev)
end
-- Enable CLEU
cleuFrame:RegisterEvent("COMBAT_LOG_EVENT")
cleuFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
cleuActive = true
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSMate CLEU Adapter|r: |cff00ff00ON|r (structured events)")
end
local function enableOriginal()
-- Disable CLEU
cleuFrame:UnregisterEvent("COMBAT_LOG_EVENT")
cleuFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
-- Re-enable string parser
for _, ev in ipairs(chatEvents) do
Parser:RegisterEvent(ev)
@@ -129,177 +129,9 @@ local function isGroupMember(name)
end
-- ============================================================================
-- CLEU event handler
-- (dead handler removed — real handler is cleuHandler below, wrapped by profiling)
-- ============================================================================
cleuFrame:SetScript("OnEvent", function()
local sub = arg1
if not sub then return end
local srcGUID = arg2
local srcName = arg3
local dstGUID = arg4
local dstName = arg5
if not srcName or srcName == "" then srcName = "Unknown" end
if not dstName or dstName == "" then dstName = "Unknown" end
-- ========================================================================
-- DAMAGE events
-- ========================================================================
if sub == "SWING_DAMAGE" then
local amount = arg6 or 0
local critical = arg12 == 1 and 1 or 0
local glancing = arg13 == 1 and 1 or 0
local crushing = arg14 == 1 and 1 or 0
local hit = (critical == 0 and glancing == 0 and crushing == 0) and 1 or 0
DB:DamageDone(srcName, AAttack, hit, critical, 0, 0, 0, 0, amount, glancing, 0)
DB:DamageTaken(dstName, AAttack, hit, critical, 0, 0, 0, 0, amount, srcName, crushing, 0)
DB:EnemyDamage(1, DPSMateEDT, dstName, AAttack, hit, critical, 0, 0, 0, 0, amount, srcName, 0, crushing)
DB:EnemyDamage(2, DPSMateEDD, srcName, AAttack, hit, critical, 0, 0, 0, 0, amount, dstName, 0, 0)
DB:DeathHistory(dstName, srcName, AAttack, amount, hit, critical, "hit", crushing)
elseif sub == "SWING_MISSED" then
local missType = arg6
local miss = (missType == "MISS") and 1 or 0
local parry = (missType == "PARRY") and 1 or 0
local dodge = (missType == "DODGE") and 1 or 0
local resist = (missType == "RESIST" or missType == "IMMUNE") and 1 or 0
local block = (missType == "BLOCK") and 1 or 0
local absorb = (missType == "ABSORB") and 1 or 0
DB:DamageDone(srcName, AAttack, 0, 0, miss + absorb, parry, dodge, resist, 0, 0, block)
DB:DamageTaken(dstName, AAttack, 0, 0, miss + absorb, parry, dodge, resist, 0, srcName, 0, block)
elseif sub == "SPELL_DAMAGE" or sub == "RANGE_DAMAGE" or sub == "SPELL_PERIODIC_DAMAGE"
or sub == "DAMAGE_SHIELD" or sub == "DAMAGE_SPLIT" then
local spellName = arg7 or "Unknown"
local amount = arg9 or 0
local school = arg11
local critical = arg15 == 1 and 1 or 0
local glancing = arg16 == 1 and 1 or 0
local crushing = arg17 == 1 and 1 or 0
local hit = (critical == 0 and glancing == 0 and crushing == 0) and 1 or 0
DB:DamageDone(srcName, spellName, hit, critical, 0, 0, 0, 0, amount, glancing, 0)
DB:DamageTaken(dstName, spellName, hit, critical, 0, 0, 0, 0, amount, srcName, crushing, 0)
DB:EnemyDamage(1, DPSMateEDT, dstName, spellName, hit, critical, 0, 0, 0, 0, amount, srcName, 0, crushing)
DB:EnemyDamage(2, DPSMateEDD, srcName, spellName, hit, critical, 0, 0, 0, 0, amount, dstName, 0, 0)
DB:DeathHistory(dstName, srcName, spellName, amount, hit, critical, "hit", crushing)
if school then DB:AddSpellSchool(spellName, school) end
elseif sub == "SPELL_MISSED" or sub == "RANGE_MISSED"
or sub == "SPELL_PERIODIC_MISSED" or sub == "DAMAGE_SHIELD_MISSED" then
local spellName = arg7 or "Unknown"
local missType = arg9
local miss = (missType == "MISS") and 1 or 0
local parry = (missType == "PARRY") and 1 or 0
local dodge = (missType == "DODGE") and 1 or 0
local resist = (missType == "RESIST" or missType == "IMMUNE") and 1 or 0
local block = (missType == "BLOCK") and 1 or 0
local absorb = (missType == "ABSORB") and 1 or 0
DB:DamageDone(srcName, spellName, 0, 0, miss + absorb, parry, dodge, resist, 0, 0, block)
DB:DamageTaken(dstName, spellName, 0, 0, miss + absorb, parry, dodge, resist, 0, srcName, 0, block)
elseif sub == "ENVIRONMENTAL_DAMAGE" then
local envType = arg6
local amount = arg7 or 0
DB:DamageTaken(dstName, envType or "Environment", 1, 0, 0, 0, 0, 0, amount, envType or "Environment", 0, 0)
DB:DeathHistory(dstName, envType or "Environment", envType or "Environment", amount, 1, 0, "hit", 0)
-- ========================================================================
-- HEAL events
-- ========================================================================
elseif sub == "SPELL_HEAL" or sub == "SPELL_PERIODIC_HEAL" then
local spellName = arg7 or "Unknown"
local amount = arg9 or 0
local overheal = arg10 or 0
local critical = arg12 == 1 and 1 or 0
local hit = critical == 0 and 1 or 0
local effective = amount - overheal
if effective < 0 then effective = 0 end
DB:Healing(1, DPSMateHealingTaken, srcName, spellName, hit, critical, effective)
DB:Healing(2, DPSMateOverhealing, srcName, spellName, hit, critical, overheal)
DB:HealingTaken(1, DPSMateHealingTaken, srcName, spellName, hit, critical, effective, dstName)
DB:DeathHistory(dstName, srcName, spellName, effective, hit, critical, "heal", 0)
-- ========================================================================
-- AURA events
-- ========================================================================
elseif sub == "SPELL_AURA_APPLIED" then
local spellName = arg7 or "Unknown"
local auraType = arg9
if auraType == "DEBUFF" then
DB:BuildBuffs(srcName, dstName, spellName, false)
if Parser.CC[spellName] then
DB:BuildActiveCC(dstName, spellName)
end
else
DB:BuildBuffs(srcName, dstName, spellName, true)
end
elseif sub == "SPELL_AURA_REMOVED" then
local spellName = arg7 or "Unknown"
DB:DestroyBuffs(dstName, spellName)
local auraType = arg9
if auraType == "DEBUFF" then
DB:RemoveActiveCC(dstName, spellName)
end
elseif sub == "SPELL_AURA_BROKEN_SPELL" or sub == "SPELL_AURA_BROKEN" then
local spellName = arg7 or "Unknown"
DB:RemoveActiveCC(dstName, spellName)
if Parser.CC[spellName] then
DB:CCBreaker(dstName, spellName, srcName)
end
-- ========================================================================
-- CAST events
-- ========================================================================
elseif sub == "SPELL_CAST_SUCCESS" then
local spellName = arg7 or "Unknown"
if Parser.Kicks and Parser.Kicks[spellName] then
DB:RegisterPotentialKick(srcName, spellName, GT())
end
-- ========================================================================
-- INTERRUPT / DISPEL events
-- ========================================================================
elseif sub == "SPELL_INTERRUPT" then
local spellName = arg7 or "Unknown"
local extraSpellName = arg10 or "Unknown"
DB:Kick(srcName, dstName, spellName, extraSpellName)
elseif sub == "SPELL_DISPEL" then
local spellName = arg7 or "Unknown"
local extraSpellName = arg10 or "Unknown"
if isGroupMember(srcName) then
DB:Dispels(srcName, spellName, dstName, extraSpellName)
end
-- ========================================================================
-- DEATH events
-- ========================================================================
elseif sub == "UNIT_DIED" or sub == "UNIT_DESTROYED" then
DB:UnregisterDeath(dstName)
elseif sub == "SPELL_SUMMON" then
local spellName = arg7 or "Unknown"
if not Parser.petToOwnerMap then Parser.petToOwnerMap = {} end
if not Parser.petToOwnerMap[dstName] then Parser.petToOwnerMap[dstName] = {} end
Parser.petToOwnerMap[dstName][srcName] = true
end
end)
-- ============================================================================
-- Performance profiling
-- ============================================================================
@@ -441,6 +273,10 @@ cleuFrame:SetScript("OnEvent", function()
end)
-- The actual CLEU handler (extracted so we can call it directly or measured)
local ShieldFlags = DB.ShieldFlags
local FailDT = DPSMate.Parser.FailDT
local FailDB = DPSMate.Parser.FailDB
cleuHandler = function()
local sub = arg1
if not sub then return end
@@ -453,17 +289,30 @@ cleuHandler = function()
if not srcName or srcName == "" then srcName = "Unknown" end
if not dstName or dstName == "" then dstName = "Unknown" end
-- ========================================================================
-- DAMAGE events
-- ========================================================================
-- SWING_DAMAGE args: amount(6), overkill(7), school(8), resisted(9),
-- blocked(10), absorbed(11), critical(12), glancing(13), crushing(14)
if sub == "SWING_DAMAGE" then
local amount = arg6 or 0
local absorbed = arg11 or 0
local critical = arg12 == 1 and 1 or 0
local glancing = arg13 == 1 and 1 or 0
local crushing = arg14 == 1 and 1 or 0
local hit = (critical == 0 and glancing == 0 and crushing == 0) and 1 or 0
DB:DamageDone(srcName, AAttack, hit, critical, 0, 0, 0, 0, amount, glancing, 0)
DB:DamageTaken(dstName, AAttack, hit, critical, 0, 0, 0, 0, amount, srcName, crushing, 0)
DB:EnemyDamage(1, DPSMateEDT, dstName, AAttack, hit, critical, 0, 0, 0, 0, amount, srcName, 0, crushing)
DB:EnemyDamage(2, DPSMateEDD, srcName, AAttack, hit, critical, 0, 0, 0, 0, amount, dstName, 0, 0)
DB:DeathHistory(dstName, srcName, AAttack, amount, hit, critical, "hit", crushing)
-- Partial absorb on a hit
if absorbed > 0 then
DB:SetUnregisterVariables(absorbed, AAttack, srcName)
DB:Absorb(AAttack, dstName, srcName)
end
elseif sub == "SWING_MISSED" then
local missType = arg6
@@ -473,24 +322,44 @@ cleuHandler = function()
local resist = (missType == "RESIST" or missType == "IMMUNE") and 1 or 0
local block = (missType == "BLOCK") and 1 or 0
local absorb = (missType == "ABSORB") and 1 or 0
DB:DamageDone(srcName, AAttack, 0, 0, miss + absorb, parry, dodge, resist, 0, 0, block)
DB:DamageTaken(dstName, AAttack, 0, 0, miss + absorb, parry, dodge, resist, 0, srcName, 0, block)
-- Full absorb
if absorb == 1 then
DB:Absorb(AAttack, dstName, srcName)
end
-- SPELL_DAMAGE args (after prefix): amount(9), overkill(10), school(11),
-- resisted(12), blocked(13), absorbed(14), critical(15), glancing(16), crushing(17)
elseif sub == "SPELL_DAMAGE" or sub == "RANGE_DAMAGE" or sub == "SPELL_PERIODIC_DAMAGE"
or sub == "DAMAGE_SHIELD" or sub == "DAMAGE_SPLIT" then
local spellName = arg7 or "Unknown"
local amount = arg9 or 0
local school = arg11
local absorbed = arg14 or 0
local critical = arg15 == 1 and 1 or 0
local glancing = arg16 == 1 and 1 or 0
local crushing = arg17 == 1 and 1 or 0
local hit = (critical == 0 and glancing == 0 and crushing == 0) and 1 or 0
DB:DamageDone(srcName, spellName, hit, critical, 0, 0, 0, 0, amount, glancing, 0)
DB:DamageTaken(dstName, spellName, hit, critical, 0, 0, 0, 0, amount, srcName, crushing, 0)
DB:EnemyDamage(1, DPSMateEDT, dstName, spellName, hit, critical, 0, 0, 0, 0, amount, srcName, 0, crushing)
DB:EnemyDamage(2, DPSMateEDD, srcName, spellName, hit, critical, 0, 0, 0, 0, amount, dstName, 0, 0)
DB:DeathHistory(dstName, srcName, spellName, amount, hit, critical, "hit", crushing)
if school then DB:AddSpellSchool(spellName, school) end
local abilityName = (sub == "SPELL_PERIODIC_DAMAGE") and (spellName .. "(Periodic)") or spellName
DB:DamageDone(srcName, abilityName, hit, critical, 0, 0, 0, 0, amount, glancing, 0)
DB:DamageTaken(dstName, abilityName, hit, critical, 0, 0, 0, 0, amount, srcName, crushing, 0)
DB:EnemyDamage(1, DPSMateEDT, dstName, abilityName, hit, critical, 0, 0, 0, 0, amount, srcName, 0, crushing)
DB:EnemyDamage(2, DPSMateEDD, srcName, abilityName, hit, critical, 0, 0, 0, 0, amount, dstName, 0, 0)
DB:DeathHistory(dstName, srcName, abilityName, amount, hit, critical, "hit", crushing)
if school then DB:AddSpellSchool(abilityName, school) end
-- Partial absorb on a hit
if absorbed > 0 then
DB:SetUnregisterVariables(absorbed, abilityName, srcName)
DB:Absorb(abilityName, dstName, srcName)
end
-- Avoidable damage taken (fire, void zones, etc.)
if FailDT and FailDT[spellName] then
DB:BuildFail(2, srcName, dstName, abilityName, amount)
end
elseif sub == "SPELL_MISSED" or sub == "RANGE_MISSED"
or sub == "SPELL_PERIODIC_MISSED" or sub == "DAMAGE_SHIELD_MISSED" then
@@ -502,8 +371,15 @@ cleuHandler = function()
local resist = (missType == "RESIST" or missType == "IMMUNE") and 1 or 0
local block = (missType == "BLOCK") and 1 or 0
local absorb = (missType == "ABSORB") and 1 or 0
DB:DamageDone(srcName, spellName, 0, 0, miss + absorb, parry, dodge, resist, 0, 0, block)
DB:DamageTaken(dstName, spellName, 0, 0, miss + absorb, parry, dodge, resist, 0, srcName, 0, block)
local isPeriodic = (sub == "SPELL_PERIODIC_MISSED")
local abilityName = isPeriodic and (spellName .. "(Periodic)") or spellName
DB:DamageDone(srcName, abilityName, 0, 0, miss + absorb, parry, dodge, resist, 0, 0, block)
DB:DamageTaken(dstName, abilityName, 0, 0, miss + absorb, parry, dodge, resist, 0, srcName, 0, block)
-- Full absorb
if absorb == 1 then
DB:Absorb(abilityName, dstName, srcName)
end
elseif sub == "ENVIRONMENTAL_DAMAGE" then
local envType = arg6
@@ -511,6 +387,10 @@ cleuHandler = function()
DB:DamageTaken(dstName, envType or "Environment", 1, 0, 0, 0, 0, 0, amount, envType or "Environment", 0, 0)
DB:DeathHistory(dstName, envType or "Environment", envType or "Environment", amount, 1, 0, "hit", 0)
-- ========================================================================
-- HEAL events
-- ========================================================================
elseif sub == "SPELL_HEAL" or sub == "SPELL_PERIODIC_HEAL" then
local spellName = arg7 or "Unknown"
local amount = arg9 or 0
@@ -519,11 +399,16 @@ cleuHandler = function()
local hit = critical == 0 and 1 or 0
local effective = amount - overheal
if effective < 0 then effective = 0 end
DB:Healing(1, DPSMateHealingTaken, srcName, spellName, hit, critical, effective)
DB:Healing(2, DPSMateOverhealing, srcName, spellName, hit, critical, overheal)
DB:HealingTaken(1, DPSMateHealingTaken, srcName, spellName, hit, critical, effective, dstName)
DB:DeathHistory(dstName, srcName, spellName, effective, hit, critical, "heal", 0)
-- ========================================================================
-- AURA events (+ absorb shield lifecycle)
-- ========================================================================
elseif sub == "SPELL_AURA_APPLIED" then
local spellName = arg7 or "Unknown"
local auraType = arg9
@@ -532,15 +417,29 @@ cleuHandler = function()
if Parser.CC[spellName] then
DB:BuildActiveCC(dstName, spellName)
end
-- Avoidable debuff application
if FailDB and FailDB[spellName] then
DB:BuildFail(3, "Environment", dstName, spellName, 0)
end
else
DB:BuildBuffs(srcName, dstName, spellName, true)
-- Absorb shield applied
if ShieldFlags[spellName] then
DB:ConfirmAbsorbApplication(spellName, dstName, GT())
end
end
elseif sub == "SPELL_AURA_REMOVED" then
local spellName = arg7 or "Unknown"
DB:DestroyBuffs(dstName, spellName)
if arg9 == "DEBUFF" then
local auraType = arg9
if auraType == "DEBUFF" then
DB:RemoveActiveCC(dstName, spellName)
else
-- Absorb shield removed
if ShieldFlags[spellName] then
DB:UnregisterAbsorb(spellName, dstName)
end
end
elseif sub == "SPELL_AURA_BROKEN_SPELL" or sub == "SPELL_AURA_BROKEN" then
@@ -550,11 +449,23 @@ cleuHandler = function()
DB:CCBreaker(dstName, spellName, srcName)
end
-- ========================================================================
-- CAST events
-- ========================================================================
elseif sub == "SPELL_CAST_SUCCESS" then
local spellName = arg7 or "Unknown"
if Parser.Kicks and Parser.Kicks[spellName] then
DB:RegisterPotentialKick(srcName, spellName, GT())
end
-- Shield cast → await confirmation via SPELL_AURA_APPLIED
if ShieldFlags[spellName] then
DB:AwaitingAbsorbConfirmation(srcName, spellName, dstName, GT())
end
-- ========================================================================
-- INTERRUPT / DISPEL events
-- ========================================================================
elseif sub == "SPELL_INTERRUPT" then
local spellName = arg7 or "Unknown"
@@ -568,6 +479,10 @@ cleuHandler = function()
DB:Dispels(srcName, spellName, dstName, extraSpellName)
end
-- ========================================================================
-- DEATH events
-- ========================================================================
elseif sub == "UNIT_DIED" or sub == "UNIT_DESTROYED" then
DB:UnregisterDeath(dstName)
+1 -1
View File
@@ -591,7 +591,7 @@ end)
-- Expand args table to handle up to 17 args (spell damage has the most: 5 base + 3 prefix + 9 suffix)
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("COMBAT_LOG_EVENT")
eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
eventFrame:SetScript("OnEvent", function()
local subevent = arg1
+437 -210
View File
@@ -1,6 +1,6 @@
//! DPS log module — TBC/WotLK-style COMBAT_LOG_EVENT for vanilla 1.12.1.
//! DPS log module — TBC/WotLK-style COMBAT_LOG_EVENT_UNFILTERED for vanilla 1.12.1.
//!
//! Fires a unified COMBAT_LOG_EVENT with WotLK-style subevent names and
//! Fires a unified COMBAT_LOG_EVENT_UNFILTERED with WotLK-style subevent names and
//! structured args. All subevents share: arg1=subevent, arg2=sourceGUID,
//! arg3=destGUID. Remaining args are subevent-specific.
//!
@@ -87,12 +87,12 @@ pub fn isActive() bool {
}
// =============================================================================
// COMBAT_LOG_EVENT — slot assigned dynamically in createEventsDetour
// COMBAT_LOG_EVENT_UNFILTERED — slot assigned dynamically in createEventsDetour
// =============================================================================
var g_event_combat_log: u32 = 0;
const EVENT_TABLE_MAIN: u32 = 0xBE1198;
const event_name: [*:0]const u8 = "COMBAT_LOG_EVENT";
const event_name: [*:0]const u8 = "COMBAT_LOG_EVENT_UNFILTERED";
// =============================================================================
// Subevent name strings (WotLK naming)
@@ -119,6 +119,7 @@ const SUB_SPELL_PERIODIC_HEAL: [*:0]const u8 = "SPELL_PERIODIC_HEAL";
// Energize / Drain
const SUB_SPELL_ENERGIZE: [*:0]const u8 = "SPELL_ENERGIZE";
const SUB_SPELL_DRAIN: [*:0]const u8 = "SPELL_DRAIN";
const SUB_SPELL_PERIODIC_ENERGIZE: [*:0]const u8 = "SPELL_PERIODIC_ENERGIZE";
const SUB_SPELL_PERIODIC_DRAIN: [*:0]const u8 = "SPELL_PERIODIC_DRAIN";
const SUB_SPELL_PERIODIC_LEECH: [*:0]const u8 = "SPELL_PERIODIC_LEECH";
@@ -296,6 +297,94 @@ fn guidToString(guid: u64) [*:0]const u8 {
return @ptrCast(buf[0..18 :0]);
}
// =============================================================================
// Handler Table Swap — replace NetClient opcode dispatch entries
// =============================================================================
//
// NetClient__ProcessMessage (0x537AA0) dispatches packets via:
// handler = NetClient[opcode * 4 + 0x74]
// call handler(ECX=context, EDX=opcode, stack: timestamp, CDataStore*)
//
// We swap pointers in the heap-allocated NetClient object (no code patching).
// Original handler is saved and called after our processing.
const NETCLIENT_PTR: u32 = 0xC28128;
const HANDLER_TABLE_BASE: u32 = 0x74;
const HandlerSwap = struct {
opcode: u16,
original: u32 = 0,
active: bool = false,
};
// Max swaps we'll ever need (one per opcode we intercept)
const MAX_SWAPS = 16;
var handler_swaps: [MAX_SWAPS]HandlerSwap = [_]HandlerSwap{.{ .opcode = 0 }} ** MAX_SWAPS;
var swap_count: u32 = 0;
fn getNetClient() ?u32 {
const nc = hook.readMem(u32, NETCLIENT_PTR);
return if (nc != 0) nc else null;
}
fn handlerSlotAddr(net_client: u32, opcode: u16) u32 {
return net_client + @as(u32, opcode) * 4 + HANDLER_TABLE_BASE;
}
fn swapHandler(opcode: u16, replacement: u32) bool {
const nc = getNetClient() orelse {
log.fmt("swapHandler: NetClient is NULL, cannot swap opcode 0x{X}\n", .{opcode});
return false;
};
if (swap_count >= MAX_SWAPS) {
log.fmt("swapHandler: swap table full\n", .{});
return false;
}
const slot = handlerSlotAddr(nc, opcode);
const original = hook.readMem(u32, slot);
if (original == 0) {
log.fmt("swapHandler: no handler registered for opcode 0x{X}\n", .{opcode});
return false;
}
handler_swaps[swap_count] = .{
.opcode = opcode,
.original = original,
.active = true,
};
swap_count += 1;
// Write our replacement into the table (heap memory, writable)
@as(*align(1) u32, @ptrFromInt(slot)).* = replacement;
log.fmt("swapHandler: opcode 0x{X} swapped (original=0x{X})\n", .{ opcode, original });
return true;
}
fn getOriginalHandler(opcode: u16) ?u32 {
for (handler_swaps[0..swap_count]) |entry| {
if (entry.opcode == opcode and entry.active) return entry.original;
}
return null;
}
fn callOriginalHandler(opcode: u16, unk: u32, opc: u32, unk2: u32, cds: u32) u32 {
const original = getOriginalHandler(opcode) orelse return 0;
return @call(.auto, @as(*const FastCallPacketHandlerFn, @ptrFromInt(original)), .{ unk, opc, unk2, cds });
}
fn restoreAllHandlers() void {
const nc = getNetClient() orelse return;
for (handler_swaps[0..swap_count]) |*entry| {
if (entry.active) {
const slot = handlerSlotAddr(nc, entry.opcode);
@as(*align(1) u32, @ptrFromInt(slot)).* = entry.original;
entry.active = false;
log.fmt("restoreHandler: opcode 0x{X} restored\n", .{entry.opcode});
}
}
swap_count = 0;
}
// =============================================================================
// SignalEventParam fire functions — one per format pattern
// =============================================================================
@@ -406,6 +495,21 @@ fn fireBase(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst
@call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s", sub, src, src_name, dst, dst_name });
}
// =============================================================================
// Hook: InitializeGameEngine (0x401570)
// __thiscall(ECX=this), RET 0xC (3 stack params)
// Registers all packet handlers. We install table swaps after it returns.
// =============================================================================
const InitGameEngineFn = fn (u32, u32, u32, u32) callconv(hook.cc.thiscall) void;
var init_engine_hook: hook.Detour(InitGameEngineFn) = .{};
fn initGameEngineDetour(this: u32, p1: u32, p2: u32, p3: u32) callconv(hook.cc.thiscall) void {
init_engine_hook.callOriginal(.{ this, p1, p2, p3 });
// All packet handlers are now registered — install our table swaps
installHandlerSwaps();
}
// =============================================================================
// Hook: FrameScript_CreateEvents (0x703D90)
// After the chain runs, write our event into the internal table.
@@ -475,10 +579,76 @@ fn createEventsDetour(param1: u32, max_event_id: u32) callconv(hook.cc.fastcall)
hook.writeMem(internal_array + slot * 16, std.mem.asBytes(&name_ptr));
g_event_combat_log = slot;
log.fmt("createEventsDetour: COMBAT_LOG_EVENT at slot {d}, capacity={d}\n", .{ slot, capacity });
log.fmt("createEventsDetour: COMBAT_LOG_EVENT_UNFILTERED at slot {d}, capacity={d}\n", .{ slot, capacity });
}
}
fn installHandlerSwaps() void {
if (swap_count > 0) return; // already installed
// Packet handler table swaps (no code patching, heap pointer writes only)
if (!swapHandler(0x250, @intFromPtr(&spellNonMeleeDmgLogDetour)))
log.print("FAILED to swap SPELLNONMELEEDAMAGELOG (0x250)\n")
else
log.print("Swapped SPELLNONMELEEDAMAGELOG (0x250)\n");
if (!swapHandler(0x24E, @intFromPtr(&periodicAuraLogDetour)))
log.print("FAILED to swap PERIODICAURALOG (0x24E)\n")
else
log.print("Swapped PERIODICAURALOG (0x24E)\n");
if (!swapHandler(0x150, @intFromPtr(&healLogDetour)))
log.print("FAILED to swap SPELLHEALLOG (0x150)\n")
else
log.print("Swapped SPELLHEALLOG (0x150)\n");
if (!swapHandler(0x14A, @intFromPtr(&meleeDispatcherDetour)))
log.print("FAILED to swap ATTACKERSTATEUPDATE (0x14A)\n")
else
log.print("Swapped ATTACKERSTATEUPDATE (0x14A)\n");
if (!swapHandler(0x1F5, @intFromPtr(&partyKillLogDetour)))
log.print("FAILED to swap PARTYKILLLOG (0x1F5)\n")
else
log.print("Swapped PARTYKILLLOG (0x1F5)\n");
if (!swapHandler(0x131, @intFromPtr(&spellStartDetour)))
log.print("FAILED to swap SPELL_START (0x131)\n")
else
log.print("Swapped SPELL_START (0x131)\n");
if (!swapHandler(0x132, @intFromPtr(&spellStartDetour)))
log.print("FAILED to swap SPELL_GO (0x132)\n")
else
log.print("Swapped SPELL_GO (0x132)\n");
if (!swapHandler(0x130, @intFromPtr(&castResultDetour)))
log.print("FAILED to swap CAST_RESULT (0x130)\n")
else
log.print("Swapped CAST_RESULT (0x130)\n");
if (!swapHandler(0x24B, @intFromPtr(&spellMissedDetour)))
log.print("FAILED to swap SPELLLOGMISS (0x24B)\n")
else
log.print("Swapped SPELLLOGMISS (0x24B)\n");
if (!swapHandler(0x24F, @intFromPtr(&damageShieldDetour)))
log.print("FAILED to swap SPELLDAMAGESHIELD (0x24F)\n")
else
log.print("Swapped SPELLDAMAGESHIELD (0x24F)\n");
if (!swapHandler(0x24C, @intFromPtr(&spellLogExecuteDetour)))
log.print("FAILED to swap SPELLLOGEXECUTE (0x24C)\n")
else
log.print("Swapped SPELLLOGEXECUTE (0x24C)\n");
if (!swapHandler(0x32F, @intFromPtr(&instaKillDetour)))
log.print("FAILED to swap SPELLINSTAKILLLOG (0x32F)\n")
else
log.print("Swapped SPELLINSTAKILLLOG (0x32F)\n");
log.fmt("installHandlerSwaps: {d} handlers swapped\n", .{swap_count});
}
// =============================================================================
// Hook: SpellNonMeleeDmgLogHandler (0x5E85E0)
// Packet: SMSG_SPELLNONMELEEDAMAGELOG
@@ -487,8 +657,6 @@ fn createEventsDetour(param1: u32, max_event_id: u32) callconv(hook.cc.fastcall)
const FastCallPacketHandlerFn = fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) u32;
var spell_dmg_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn spellNonMeleeDmgLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
@@ -548,7 +716,7 @@ fn spellNonMeleeDmgLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callcon
}
}
return spell_dmg_hook.callOriginal(.{ unk, opcode, unk2, cds });
return callOriginalHandler(0x250, unk, opcode, unk2, cds);
}
// =============================================================================
@@ -558,8 +726,6 @@ fn spellNonMeleeDmgLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callcon
// SPELL_PERIODIC_DRAIN, SPELL_PERIODIC_LEECH
// =============================================================================
var periodic_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn periodicAuraLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
@@ -650,45 +816,44 @@ fn periodicAuraLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(ho
}
cdsSetRead(cds, saved_read);
return periodic_hook.callOriginal(.{ unk, opcode, unk2, cds });
return callOriginalHandler(0x24E, unk, opcode, unk2, cds);
}
// =============================================================================
// Hook: ProcessSpellPowerDrainMessage (0x62C770) — downstream of SMSG_SPELLHEALLOG
// __fastcall(ECX=casterGuid_ptr, EDX=targetGuid_ptr, stack: spellId, healAmount, isCrit)
// RET 0xC (3 stack params). Ghidra mislabel — actually heal display function.
// Hook: SMSG_SPELLHEALLOG (opcode 0x150) — handler table swap
// Packet: victimPackGUID, casterPackGUID, uint32 spellId, uint32 healAmount, uint8 isCrit
// Fires: SPELL_HEAL
// =============================================================================
const HealDisplayFn = fn (u32, u32, u32, u32, u32) callconv(hook.cc.fastcall) void;
var heal_hook: hook.Detour(HealDisplayFn) = .{};
fn healDisplayDetour(caster_ptr: u32, target_ptr: u32, spell_id: u32, heal_amount: u32, is_crit: u32) callconv(hook.cc.fastcall) void {
fn healLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
if (caster_ptr != 0 and target_ptr != 0 and spell_id != 0) {
const caster_lo = hook.readMem(u32, caster_ptr);
const caster_hi = hook.readMem(u32, caster_ptr + 4);
const target_lo = hook.readMem(u32, target_ptr);
const target_hi = hook.readMem(u32, target_ptr + 4);
const caster_guid: u64 = @as(u64, caster_hi) << 32 | @as(u64, caster_lo);
const target_guid: u64 = @as(u64, target_hi) << 32 | @as(u64, target_lo);
const saved_read = cdsGetRead(cds);
if (caster_guid != 0 and target_guid != 0 and !isPeriodicLeechSpell(spell_id)) {
// Suppress heal for periodic leech spells — already covered by SPELL_PERIODIC_LEECH
const src_str = guidToString(caster_guid);
const dst_str = guidToString(target_guid);
const src_name = wow.getNameByGUID(caster_guid);
const dst_name = wow.getNameByGUID(target_guid);
const critical: u32 = if (is_crit != 0) 1 else 0;
const overheal = computeOverheal(target_guid, heal_amount);
// _HEAL: spellId, amount, overheal, absorbed(0), critical
fireSpellHeal(SUB_SPELL_HEAL, src_str, src_name, dst_str, dst_name, spell_id, getSpellName(spell_id), getSpellSchool(spell_id), heal_amount, overheal, 0, critical);
}
const target_guid = cdsGetPackedGuid(cds);
const caster_guid = cdsGetPackedGuid(cds);
const spell_id = cdsGet(u32, cds);
const heal_amount = cdsGet(u32, cds);
const is_crit = cdsGet(u8, cds);
cdsSetRead(cds, saved_read);
if (target_guid != null and caster_guid != null and spell_id != null and
heal_amount != null and is_crit != null and
caster_guid.? != 0 and target_guid.? != 0 and !isPeriodicLeechSpell(spell_id.?))
{
// Suppress heal for periodic leech spells — already covered by SPELL_PERIODIC_LEECH
const src_str = guidToString(caster_guid.?);
const dst_str = guidToString(target_guid.?);
const src_name = wow.getNameByGUID(caster_guid.?);
const dst_name = wow.getNameByGUID(target_guid.?);
const critical: u32 = if (is_crit.? != 0) 1 else 0;
const overheal = computeOverheal(target_guid.?, heal_amount.?);
// _HEAL: spellId, amount, overheal, absorbed(0), critical
fireSpellHeal(SUB_SPELL_HEAL, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), getSpellSchool(spell_id.?), heal_amount.?, overheal, 0, critical);
}
heal_hook.callOriginal(.{ caster_ptr, target_ptr, spell_id, heal_amount, is_crit });
return callOriginalHandler(0x150, unk, opcode, unk2, cds);
}
// =============================================================================
@@ -700,8 +865,6 @@ fn healDisplayDetour(caster_ptr: u32, target_ptr: u32, spell_id: u32, heal_amoun
const OPCODE_ATTACKERSTATEUPDATE: u32 = 0x14A;
var melee_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn meleeDispatcherDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
@@ -709,7 +872,7 @@ fn meleeDispatcherDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(ho
parseMeleePacket(cds);
}
return melee_hook.callOriginal(.{ unk, opcode, unk2, cds });
return callOriginalHandler(0x14A, unk, opcode, unk2, cds);
}
fn parseMeleePacket(cds: u32) void {
@@ -851,81 +1014,87 @@ fn getActivePlayerGuid() u64 {
}
// =============================================================================
// Hook: ProcessSpellCombatResult (0x62BAB0) — downstream of SMSG_SPELLLOGMISS
// __fastcall(ECX=missType, EDX=spellId, stack: casterGuidLo, casterGuidHi,
// targetGuidLo, targetGuidHi, isFromSpellLogMiss)
// RET 0x14 (5 stack params)
// Hook: SMSG_SPELLLOGMISS (opcode 0x24B) — handler table swap
// Packet: uint32 spellId, casterGUID(8 raw), uint8 unk, uint32 targetCount,
// [targetGUID(8 raw), uint8 missInfo] x count
// Fires: SPELL_MISSED, RANGE_MISSED, SPELL_PERIODIC_MISSED, DAMAGE_SHIELD_MISSED
// =============================================================================
const SpellMissedFn = fn (u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.fastcall) void;
var spell_missed_hook: hook.Detour(SpellMissedFn) = .{};
fn spellMissedDetour(miss_type: u32, spell_id: u32, caster_lo: u32, caster_hi: u32, target_lo: u32, target_hi: u32, is_spell_log_miss: u32) callconv(hook.cc.fastcall) void {
fn spellMissedDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
const caster_guid: u64 = @as(u64, caster_hi) << 32 | @as(u64, caster_lo);
const target_guid: u64 = @as(u64, target_hi) << 32 | @as(u64, target_lo);
const saved_read = cdsGetRead(cds);
if (caster_guid != 0 and target_guid != 0 and spell_id != 0) {
const src_str = guidToString(caster_guid);
const dst_str = guidToString(target_guid);
const src_name = wow.getNameByGUID(caster_guid);
const dst_name = wow.getNameByGUID(target_guid);
const miss_str = missInfoToString(@truncate(miss_type));
const school = getSpellSchool(spell_id);
// Classify miss subevent by spell type
const sub = if (spell_id == 75 or spell_id == 5019)
const spell_id = cdsGet(u32, cds);
const caster_guid = cdsGet(u64, cds);
_ = cdsGet(u8, cds); // unknown byte
const target_count = cdsGet(u32, cds);
if (spell_id != null and caster_guid != null and target_count != null and
caster_guid.? != 0 and spell_id.? != 0)
{
const src_str = guidToString(caster_guid.?);
const src_name = wow.getNameByGUID(caster_guid.?);
const school = getSpellSchool(spell_id.?);
const sub = if (spell_id.? == 75 or spell_id.? == 5019)
SUB_RANGE_MISSED
else if (isDamageShieldSpell(spell_id))
else if (isDamageShieldSpell(spell_id.?))
SUB_DAMAGE_SHIELD_MISSED
else if (isPeriodicSpell(spell_id))
else if (isPeriodicSpell(spell_id.?))
SUB_SPELL_PERIODIC_MISSED
else
SUB_SPELL_MISSED;
log.fmt("{s}: [{d}]{s} miss={s} logmiss={d}\n", .{ std.mem.span(sub), spell_id, std.mem.span(getSpellName(spell_id)), std.mem.span(miss_str), is_spell_log_miss });
fireSpellMissed(sub, src_str, src_name, dst_str, dst_name, spell_id, getSpellName(spell_id), school, miss_str, 0);
var i: u32 = 0;
while (i < target_count.?) : (i += 1) {
const target_guid = cdsGet(u64, cds) orelse break;
const miss_info = cdsGet(u8, cds) orelse break;
if (target_guid == 0) continue;
const dst_str = guidToString(target_guid);
const dst_name = wow.getNameByGUID(target_guid);
const miss_str = missInfoToString(miss_info);
log.fmt("{s}: [{d}]{s} miss={s}\n", .{ std.mem.span(sub), spell_id.?, std.mem.span(getSpellName(spell_id.?)), std.mem.span(miss_str) });
fireSpellMissed(sub, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), school, miss_str, 0);
}
}
spell_missed_hook.callOriginal(.{ miss_type, spell_id, caster_lo, caster_hi, target_lo, target_hi, is_spell_log_miss });
cdsSetRead(cds, saved_read);
return callOriginalHandler(0x24B, unk, opcode, unk2, cds);
}
// =============================================================================
// Hook: ProcessSpellDrainEffectMessage (0x62CA20) — downstream of SMSG_SPELLDAMAGESHIELD
// __fastcall(ECX=victimGuid_ptr, EDX=casterGuid_ptr, stack: damage, school)
// RET 0x8 (2 stack params)
// Packet: victimGUID(8 raw), attackerGUID(8 raw), uint32 damage, uint32 school
// Fires: DAMAGE_SHIELD
// =============================================================================
const DamageShieldFn = fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) void;
var damage_shield_hook: hook.Detour(DamageShieldFn) = .{};
fn damageShieldDetour(victim_ptr: u32, caster_ptr: u32, damage: u32, school: u32) callconv(hook.cc.fastcall) void {
fn damageShieldDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
if (victim_ptr != 0 and caster_ptr != 0) {
const victim_lo = hook.readMem(u32, victim_ptr);
const victim_hi = hook.readMem(u32, victim_ptr + 4);
const caster_lo = hook.readMem(u32, caster_ptr);
const caster_hi = hook.readMem(u32, caster_ptr + 4);
const victim_guid: u64 = @as(u64, victim_hi) << 32 | @as(u64, victim_lo);
const caster_guid: u64 = @as(u64, caster_hi) << 32 | @as(u64, caster_lo);
const saved_read = cdsGetRead(cds);
if (victim_guid != 0 and caster_guid != 0) {
const src_str = guidToString(caster_guid);
const dst_str = guidToString(victim_guid);
const src_name = wow.getNameByGUID(caster_guid);
const dst_name = wow.getNameByGUID(victim_guid);
const overkill = computeOverkill(victim_guid, damage);
log.fmt("DAMAGE_SHIELD: dmg={d} school={d}\n", .{ damage, school });
// No spellId available in SMSG_SPELLDAMAGESHIELD — pass 0
fireSpellDamage(SUB_DAMAGE_SHIELD, src_str, src_name, dst_str, dst_name, 0, getSpellName(0), school, damage, overkill, school, 0, 0, 0, 0, 0, 0);
}
const victim_guid = cdsGet(u64, cds);
const attacker_guid = cdsGet(u64, cds);
const damage = cdsGet(u32, cds);
const school = cdsGet(u32, cds);
cdsSetRead(cds, saved_read);
if (victim_guid != null and attacker_guid != null and damage != null and school != null and
victim_guid.? != 0 and attacker_guid.? != 0)
{
const src_str = guidToString(attacker_guid.?);
const dst_str = guidToString(victim_guid.?);
const src_name = wow.getNameByGUID(attacker_guid.?);
const dst_name = wow.getNameByGUID(victim_guid.?);
const overkill = computeOverkill(victim_guid.?, damage.?);
log.fmt("DAMAGE_SHIELD: dmg={d} school={d}\n", .{ damage.?, school.? });
// No spellId in SMSG_SPELLDAMAGESHIELD packet — pass 0
fireSpellDamage(SUB_DAMAGE_SHIELD, src_str, src_name, dst_str, dst_name, 0, getSpellName(0), school.?, damage.?, overkill, school.?, 0, 0, 0, 0, 0, 0);
}
damage_shield_hook.callOriginal(.{ victim_ptr, caster_ptr, damage, school });
return callOriginalHandler(0x24F, unk, opcode, unk2, cds);
}
// =============================================================================
@@ -1001,35 +1170,177 @@ fn spellInterruptDetour(caster_ptr: u32, target_ptr: u32, interrupted_spell_id:
}
// =============================================================================
// Hook: ProcessInstaKillSpellMessage (0x62CBE0) — downstream of SMSG_SPELLINSTAKILLLOG
// __fastcall(ECX=casterGuid_ptr, EDX=spellId)
// Plain RET (0 stack params)
// Fires: SPELL_INSTAKILL
// Hook: SMSG_SPELLLOGEXECUTE (opcode 0x24C) — handler table swap
// Packet: casterPackGUID, uint32 spellId, uint32 effectCount,
// per effect: uint32 effectType, uint32 logCount,
// per log entry: varies by effectType (see SpellDefines.h)
// Fires: SPELL_INTERRUPT, SPELL_ENERGIZE, SPELL_EXTRA_ATTACKS,
// SPELL_SUMMON, SPELL_RESURRECT
// Replaces 4 downstream hooks: 0x626A10, 0x62D9F0, 0x62CA00, 0x62ACE0
// =============================================================================
const InstaKillFn = fn (u32, u32) callconv(hook.cc.fastcall) void;
// Vanilla spell effect type constants
const EFFECT_INSTAKILL: u32 = 1;
const EFFECT_POWER_DRAIN: u32 = 8;
const EFFECT_HEAL: u32 = 10;
const EFFECT_ADD_EXTRA_ATTACKS: u32 = 19;
const EFFECT_CREATE_ITEM: u32 = 24;
const EFFECT_ENERGIZE: u32 = 30;
const EFFECT_DISPEL: u32 = 38;
const EFFECT_SUMMON_PET: u32 = 56;
const EFFECT_HEAL_MAX_HEALTH: u32 = 67;
const EFFECT_INTERRUPT_CAST: u32 = 68;
const EFFECT_FEED_PET: u32 = 101;
const EFFECT_DURABILITY_DAMAGE: u32 = 111;
const EFFECT_RESURRECT_NEW: u32 = 113;
var instakill_hook: hook.Detour(InstaKillFn) = .{};
fn instaKillDetour(caster_ptr: u32, spell_id: u32) callconv(hook.cc.fastcall) void {
fn spellLogExecuteDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
if (caster_ptr != 0 and spell_id != 0) {
const caster_lo = hook.readMem(u32, caster_ptr);
const caster_hi = hook.readMem(u32, caster_ptr + 4);
const caster_guid: u64 = @as(u64, caster_hi) << 32 | @as(u64, caster_lo);
const saved_read = cdsGetRead(cds);
if (caster_guid != 0) {
const src_str = guidToString(caster_guid);
const src_name = wow.getNameByGUID(caster_guid);
const school = getSpellSchool(spell_id);
log.fmt("SPELL_INSTAKILL: [{d}]{s} caster=0x{x}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), caster_guid });
// No victim GUID in SMSG_SPELLINSTAKILLLOG — pass zero GUID
fireSpell(SUB_SPELL_INSTAKILL, src_str, src_name, GUID_ZERO, "", spell_id, getSpellName(spell_id), school);
const caster_guid = cdsGetPackedGuid(cds);
const spell_id = cdsGet(u32, cds);
const effect_count = cdsGet(u32, cds);
if (caster_guid != null and spell_id != null and effect_count != null and
caster_guid.? != 0 and spell_id.? != 0)
{
const src_str = guidToString(caster_guid.?);
const src_name = wow.getNameByGUID(caster_guid.?);
const spell_school = getSpellSchool(spell_id.?);
const spell_name = getSpellName(spell_id.?);
var eff_i: u32 = 0;
while (eff_i < effect_count.?) : (eff_i += 1) {
const effect_type = cdsGet(u32, cds) orelse break;
const log_count = cdsGet(u32, cds) orelse break;
var log_i: u32 = 0;
while (log_i < log_count) : (log_i += 1) {
switch (effect_type) {
EFFECT_POWER_DRAIN => {
// targetGUID(8), amount(4), powerType(4), multiplier(float 4)
const target = cdsGet(u64, cds) orelse break;
const amount = cdsGet(u32, cds) orelse break;
const power_type = cdsGet(u32, cds) orelse break;
_ = cdsGet(u32, cds) orelse break; // multiplier float, skip
if (target != 0) {
const dst_str = guidToString(target);
const dst_name = wow.getNameByGUID(target);
log.fmt("SPELL_DRAIN: [{d}]{s} amt={d} power={d}\n", .{ spell_id.?, std.mem.span(spell_name), amount, power_type });
fireSpellEnergize(SUB_SPELL_DRAIN, src_str, src_name, dst_str, dst_name, spell_id.?, spell_name, spell_school, amount, power_type);
}
},
EFFECT_ENERGIZE => {
// targetGUID(8), amount(4), powerType(4)
const target = cdsGet(u64, cds) orelse break;
const amount = cdsGet(u32, cds) orelse break;
const power_type = cdsGet(u32, cds) orelse break;
if (target != 0) {
const dst_str = guidToString(target);
const dst_name = wow.getNameByGUID(target);
log.fmt("SPELL_ENERGIZE: [{d}]{s} amt={d} power={d}\n", .{ spell_id.?, std.mem.span(spell_name), amount, power_type });
fireSpellEnergize(SUB_SPELL_ENERGIZE, src_str, src_name, dst_str, dst_name, spell_id.?, spell_name, spell_school, amount, power_type);
}
},
EFFECT_ADD_EXTRA_ATTACKS => {
// targetGUID(8), count(4)
const target = cdsGet(u64, cds) orelse break;
const count = cdsGet(u32, cds) orelse break;
_ = target;
log.fmt("SPELL_EXTRA_ATTACKS: [{d}]{s} count={d}\n", .{ spell_id.?, std.mem.span(spell_name), count });
fireSpellExtraAttacks(src_str, src_name, GUID_ZERO, "", spell_id.?, spell_name, spell_school, count);
},
EFFECT_INTERRUPT_CAST => {
// targetGUID(8), interruptedSpellId(4)
const target = cdsGet(u64, cds) orelse break;
const interrupted_id = cdsGet(u32, cds) orelse break;
if (target != 0 and interrupted_id != 0) {
const dst_str = guidToString(target);
const dst_name = wow.getNameByGUID(target);
const extra_school = getSpellSchool(interrupted_id);
log.fmt("SPELL_INTERRUPT: [{d}]{s} interrupted=[{d}]{s}\n", .{
spell_id.?, std.mem.span(spell_name), interrupted_id, std.mem.span(getSpellName(interrupted_id)),
});
// Now we have the interrupting spell ID (spell_id) — previously was 0
fireSpellInterrupt(SUB_SPELL_INTERRUPT, src_str, src_name, dst_str, dst_name, spell_id.?, spell_name, spell_school, interrupted_id, getSpellName(interrupted_id), extra_school);
}
},
EFFECT_HEAL, EFFECT_HEAL_MAX_HEALTH => {
// targetGUID(8), amount(4), critical(4)
_ = cdsGet(u64, cds) orelse break;
_ = cdsGet(u32, cds) orelse break;
_ = cdsGet(u32, cds) orelse break;
// Heals from SPELLLOGEXECUTE are handled by SMSG_SPELLHEALLOG — skip here
},
EFFECT_CREATE_ITEM => {
// itemEntry(4)
_ = cdsGet(u32, cds) orelse break;
},
EFFECT_FEED_PET => {
// itemEntry(4)
_ = cdsGet(u32, cds) orelse break;
},
EFFECT_DURABILITY_DAMAGE => {
// targetGUID(8), itemEntry(4), unk(4)
_ = cdsGet(u64, cds) orelse break;
_ = cdsGet(u32, cds) orelse break;
_ = cdsGet(u32, cds) orelse break;
},
else => {
// Most other effect types: just targetGUID(8)
// This covers INSTAKILL, RESURRECT, DISPEL, SUMMON variants, etc.
const target = cdsGet(u64, cds) orelse break;
if (target != 0) {
const dst_str = guidToString(target);
const dst_name = wow.getNameByGUID(target);
if (isSummonEffect(effect_type)) {
log.fmt("SPELL_SUMMON: [{d}]{s}\n", .{ spell_id.?, std.mem.span(spell_name) });
fireSpell(SUB_SPELL_SUMMON, src_str, src_name, dst_str, dst_name, spell_id.?, spell_name, spell_school);
} else if (isResurrectEffect(effect_type)) {
log.fmt("SPELL_RESURRECT: [{d}]{s}\n", .{ spell_id.?, std.mem.span(spell_name) });
fireSpell(SUB_SPELL_RESURRECT, src_str, src_name, dst_str, dst_name, spell_id.?, spell_name, spell_school);
}
}
},
}
}
}
}
instakill_hook.callOriginal(.{ caster_ptr, spell_id });
cdsSetRead(cds, saved_read);
return callOriginalHandler(0x24C, unk, opcode, unk2, cds);
}
// =============================================================================
// Hook: SMSG_SPELLINSTAKILLLOG (opcode 0x32F) — handler table swap
// Packet: victimGUID(8 raw), uint32 spellId
// Fires: SPELL_INSTAKILL
// =============================================================================
fn instaKillDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
const saved_read = cdsGetRead(cds);
const victim_guid = cdsGet(u64, cds);
const spell_id = cdsGet(u32, cds);
cdsSetRead(cds, saved_read);
if (victim_guid != null and spell_id != null and victim_guid.? != 0) {
const dst_str = guidToString(victim_guid.?);
const dst_name = wow.getNameByGUID(victim_guid.?);
const school = getSpellSchool(spell_id.?);
log.fmt("SPELL_INSTAKILL: [{d}]{s} victim=0x{x}\n", .{ spell_id.?, std.mem.span(getSpellName(spell_id.?)), victim_guid.? });
// Caster not in packet — src is unknown
fireSpell(SUB_SPELL_INSTAKILL, GUID_ZERO, "", dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), school);
}
return callOriginalHandler(0x32F, unk, opcode, unk2, cds);
}
// =============================================================================
@@ -1038,8 +1349,6 @@ fn instaKillDetour(caster_ptr: u32, spell_id: u32) callconv(hook.cc.fastcall) vo
// Fires: PARTY_KILL
// =============================================================================
var party_kill_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn partyKillLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
@@ -1059,7 +1368,7 @@ fn partyKillLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.
fireBase(SUB_PARTY_KILL, src_str, src_name, dst_str, dst_name);
}
return party_kill_hook.callOriginal(.{ unk, opcode, unk2, cds });
return callOriginalHandler(0x1F5, unk, opcode, unk2, cds);
}
// =============================================================================
@@ -1071,8 +1380,6 @@ fn partyKillLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.
const OPCODE_SPELL_START: u32 = 0x131;
const OPCODE_SPELL_GO: u32 = 0x132;
var spell_start_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn spellStartDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
@@ -1126,7 +1433,8 @@ fn spellStartDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc
cdsSetRead(cds, saved_read);
}
return spell_start_hook.callOriginal(.{ unk, opcode, unk2, cds });
// Both 0x131 and 0x132 share this handler; callOriginal uses the opcode passed in EDX
return callOriginalHandler(@intCast(opcode), unk, opcode, unk2, cds);
}
// =============================================================================
@@ -1135,8 +1443,6 @@ fn spellStartDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc
// Fires: SPELL_CAST_FAILED (when status != 0)
// =============================================================================
var cast_result_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
fn castResultDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
@@ -1158,7 +1464,7 @@ fn castResultDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc
}
}
return cast_result_hook.callOriginal(.{ unk, opcode, unk2, cds });
return callOriginalHandler(0x130, unk, opcode, unk2, cds);
}
// =============================================================================
@@ -1906,28 +2212,10 @@ pub fn installHooks() void {
log.print("Hooked FrameScript_CreateEvents\n");
}
if (spell_dmg_hook.attach(0x5E85E0, &spellNonMeleeDmgLogDetour) != .ok) {
log.print("FAILED to hook SpellNonMeleeDmgLogHandler\n");
if (init_engine_hook.attach(0x401570, &initGameEngineDetour) != .ok) {
log.print("FAILED to hook InitializeGameEngine\n");
} else {
log.print("Hooked SpellNonMeleeDmgLogHandler\n");
}
if (periodic_hook.attach(0x626DD0, &periodicAuraLogDetour) != .ok) {
log.print("FAILED to hook PeriodicAuraLogHandler\n");
} else {
log.print("Hooked PeriodicAuraLogHandler\n");
}
if (heal_hook.attach(0x62C770, &healDisplayDetour) != .ok) {
log.print("FAILED to hook ProcessSpellPowerDrainMessage\n");
} else {
log.print("Hooked ProcessSpellPowerDrainMessage (SPELL_HEAL)\n");
}
if (melee_hook.attach(0x6255B0, &meleeDispatcherDetour) != .ok) {
log.print("FAILED to hook MeleeDispatcher\n");
} else {
log.print("Hooked MeleeDispatcher\n");
log.print("Hooked InitializeGameEngine (handler table swaps after return)\n");
}
if (env_dmg_hook.attach(0x62AAC0, &envDamageDetour) != .ok) {
@@ -1936,23 +2224,6 @@ pub fn installHooks() void {
log.print("Hooked ProcessEnvironmentalDamage\n");
}
if (party_kill_hook.attach(0x628890, &partyKillLogDetour) != .ok) {
log.print("FAILED to hook PartyKillLogHandler\n");
} else {
log.print("Hooked PartyKillLogHandler\n");
}
if (spell_start_hook.attach(0x6E7640, &spellStartDetour) != .ok) {
log.print("FAILED to hook SpellStartHandler\n");
} else {
log.print("Hooked SpellStartHandler\n");
}
if (cast_result_hook.attach(0x6E7330, &castResultDetour) != .ok) {
log.print("FAILED to hook CastResultHandler\n");
} else {
log.print("Hooked CastResultHandler (SPELL_CAST_FAILED self)\n");
}
if (spell_failed_other_hook.attach(0x6E75F0, &spellFailedOtherDetour) != .ok) {
log.print("FAILED to hook HandleSpellInterruptUpdate\n");
@@ -1960,35 +2231,6 @@ pub fn installHooks() void {
log.print("Hooked HandleSpellInterruptUpdate (SPELL_CAST_FAILED others)\n");
}
if (spell_missed_hook.attach(0x62BAB0, &spellMissedDetour) != .ok) {
log.print("FAILED to hook ProcessSpellCombatResult\n");
} else {
log.print("Hooked ProcessSpellCombatResult (SPELL_MISSED)\n");
}
if (damage_shield_hook.attach(0x62CA20, &damageShieldDetour) != .ok) {
log.print("FAILED to hook ProcessSpellDrainEffectMessage\n");
} else {
log.print("Hooked ProcessSpellDrainEffectMessage (DAMAGE_SHIELD)\n");
}
if (energize_hook.attach(0x62CA00, &energizeDetour) != .ok) {
log.print("FAILED to hook ProcessStandardPowerGainMessage\n");
} else {
log.print("Hooked ProcessStandardPowerGainMessage (SPELL_ENERGIZE)\n");
}
if (spell_interrupt_hook.attach(0x626A10, &spellInterruptDetour) != .ok) {
log.print("FAILED to hook DisplaySpellInterruptMessage\n");
} else {
log.print("Hooked DisplaySpellInterruptMessage (SPELL_INTERRUPT)\n");
}
if (instakill_hook.attach(0x62CBE0, &instaKillDetour) != .ok) {
log.print("FAILED to hook ProcessInstaKillSpellMessage\n");
} else {
log.print("Hooked ProcessInstaKillSpellMessage (SPELL_INSTAKILL)\n");
}
if (unit_death_hook.attach(0x605860, &unitDeathDetour) != .ok) {
log.print("FAILED to hook HandleUnitDeath\n");
@@ -2014,11 +2256,7 @@ pub fn installHooks() void {
log.print("Hooked ValidateSpellSlot (SPELL_AURA_*_DOSE)\n");
}
if (extra_attacks_hook.attach(0x62D9F0, &extraAttacksDetour) != .ok) {
log.print("FAILED to hook ProcessExtraAttacksSpellMessage\n");
} else {
log.print("Hooked ProcessExtraAttacksSpellMessage (SPELL_EXTRA_ATTACKS)\n");
}
// extra_attacks_hook — now handled by SPELLLOGEXECUTE table swap (0x24C)
if (dispel_hook.attach(0x62D480, &dispelDetour) != .ok) {
log.print("FAILED to hook ProcessAuraDispelMessage\n");
@@ -2026,11 +2264,7 @@ pub fn installHooks() void {
log.print("Hooked ProcessAuraDispelMessage (SPELL_DISPEL)\n");
}
if (spell_effect_hook.attach(0x62ACE0, &spellEffectDetour) != .ok) {
log.print("FAILED to hook ProcessSpellEffect\n");
} else {
log.print("Hooked ProcessSpellEffect (SPELL_SUMMON/RESURRECT/ENERGIZE)\n");
}
// spell_effect_hook — now handled by SPELLLOGEXECUTE table swap (0x24C)
if (aura_duration_hook.attach(0x4E4390, &auraDurationDetour) != .ok) {
log.print("FAILED to hook SetActionCooldownTimer\n");
@@ -2047,28 +2281,21 @@ pub fn installHooks() void {
pub fn removeHooks() void {
if (g_is_hook_owner) {
// Restore handler table swaps (no code to unpatch)
restoreAllHandlers();
// Detach remaining JMP-patching detours (non-packet hooks)
spell_failed_other_hook.detach();
dispel_failed_hook.detach();
aura_duration_hook.detach();
spell_effect_hook.detach();
dispel_hook.detach();
extra_attacks_hook.detach();
aura_dose_hook.detach();
aura_applied_hook.detach();
aura_removed_hook.detach();
unit_death_hook.detach();
instakill_hook.detach();
spell_interrupt_hook.detach();
damage_shield_hook.detach();
spell_missed_hook.detach();
cast_result_hook.detach();
spell_start_hook.detach();
party_kill_hook.detach();
// spell_interrupt_hook — now in SPELLLOGEXECUTE table swap
env_dmg_hook.detach();
melee_hook.detach();
heal_hook.detach();
periodic_hook.detach();
spell_dmg_hook.detach();
// energize_hook — now in SPELLLOGEXECUTE table swap
init_engine_hook.detach();
create_events_hook.detach();
resize_events_hook.detach();
log.close();
+14
View File
@@ -144,3 +144,17 @@ pub fn checknumber(L: State, index: i32) f64 {
const f: *const fn (State, i32) callconv(hook.cc.fastcall) f64 = @ptrFromInt(0x6F4C80);
return f(L, index);
}
// Lua 5.0 pseudo-index for globals table
pub const GLOBALS_INDEX: i32 = -10001;
pub fn setglobal(L: State, name: [*:0]const u8) void {
pushstring(L, name);
insert(L, -2); // swap: value is now at -1, name at -2 → after insert: name at -2, value at -1
settable(L, GLOBALS_INDEX);
}
pub fn getglobal(L: State, name: [*:0]const u8) void {
pushstring(L, name);
gettable(L, GLOBALS_INDEX);
}
+88 -6
View File
@@ -137,6 +137,74 @@ fn registerLuaFunctions() void {
}
}
// =============================================================================
// Module version registry — GetWeirdUtilsVersion(name?) from Lua
// =============================================================================
//
// Populates a global "WeirdUtils" table with { module_name = "version", ... }
// for each enabled module. If the table already exists (from another DLL),
// new entries are merged in additively.
//
// GetWeirdUtilsVersion() → returns the full WeirdUtils table
// GetWeirdUtilsVersion("name") → returns version string for that module, or nil
const all_module_names = @import("build_options").all_module_names;
const all_module_versions = @import("build_options").all_module_versions;
fn registerModuleVersions() void {
const L = lua.getContext();
// Get or create the WeirdUtils global table
lua.getglobal(L, "WeirdUtils");
if (lua.typeOf(L, -1) != 5) { // 5 = LUA_TTABLE
lua.pop(L, 1);
lua.newtable(L);
}
// For each enabled module, set WeirdUtils[name] = version
inline for (all_module_names, 0..) |name, i| {
const enabled = @field(@import("build_options"), "enable_" ++ name);
if (enabled) {
lua.pushstring(L, @ptrCast(name.ptr));
lua.pushstring(L, @ptrCast(all_module_versions[i].ptr));
lua.settable(L, -3);
}
}
// Set as global
lua.setglobal(L, "WeirdUtils");
// Register query function
registerFunction("GetWeirdUtilsVersion", @intFromPtr(&luaGetWeirdUtilsVersion));
}
fn luaGetWeirdUtilsVersion(L_ecx: usize) callconv(hook.cc.fastcall) u32 {
const L: lua.State = @ptrFromInt(L_ecx);
const nargs = lua.gettop(L);
if (nargs >= 1 and lua.isstring(L, 1)) {
// GetWeirdUtilsVersion("name") → return version or nil
const name = lua.tostring(L, 1) orelse {
lua.pushnil(L);
return 1;
};
lua.getglobal(L, "WeirdUtils");
if (lua.typeOf(L, -1) != 5) {
lua.pop(L, 1);
lua.pushnil(L);
return 1;
}
lua.pushstring(L, name);
lua.gettable(L, -2);
lua.remove(L, -2); // remove WeirdUtils table, leave value
return 1;
}
// GetWeirdUtilsVersion() → return the whole table
lua.getglobal(L, "WeirdUtils");
return 1;
}
// =============================================================================
// Embedded addon files
// =============================================================================
@@ -661,11 +729,22 @@ fn removeFileHooks() void {
// Hook: LoadScriptFunctions (0x490250)
// =============================================================================
var lsf_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{};
var register_commands_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{};
var glue_commands_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{};
fn loadScriptFunctionsDetour() callconv(hook.cc.stdcall) void {
lsf_hook.callOriginal(.{});
/// Hook for Player_LoadScriptFunctions (0x490250).
/// Fires after login/reload — registers gameplay Lua functions + version table.
fn registerAllSystemCommandsDetour() callconv(hook.cc.stdcall) void {
register_commands_hook.callOriginal(.{});
registerLuaFunctions();
registerModuleVersions();
}
/// Hook for Glue_LoadScriptFunctions (0x46ABB0).
/// Fires at the login/glue screen — registers version table so addons can query early.
fn glueLoadScriptFunctionsDetour() callconv(hook.cc.stdcall) void {
glue_commands_hook.callOriginal(.{});
registerModuleVersions();
}
// =============================================================================
@@ -793,7 +872,8 @@ fn install() void {
_ = protection_hook.attach(0x42a320, &luaProtectionDetour);
installFileHooks();
_ = file_hook.attach(0x648620, &loadFileDetour);
_ = lsf_hook.attach(0x490250, &loadScriptFunctionsDetour);
_ = register_commands_hook.attach(0x490250, &registerAllSystemCommandsDetour);
_ = glue_commands_hook.attach(0x46ABB0, &glueLoadScriptFunctionsDetour);
inline for (modules) |m| {
if (m.install) |inst| inst();
@@ -822,7 +902,8 @@ fn uninstall() void {
}
addons.uninstall();
lsf_hook.detach();
register_commands_hook.detach();
glue_commands_hook.detach();
file_hook.detach();
removeFileHooks();
protection_hook.detach();
@@ -900,7 +981,8 @@ fn disableAll() callconv(.c) i32 {
logout_hook.detach();
engine_init_hook.detach();
addons.uninstall();
lsf_hook.detach();
register_commands_hook.detach();
glue_commands_hook.detach();
file_hook.detach();
removeFileHooks();
protection_hook.detach();