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
+32 -1
View File
@@ -165,7 +165,7 @@ No other configuration needed, install and forget.
### DPSLog (Combat Log Events)
Provides WotLK 3.3.5-style `COMBAT_LOG_EVENT` for the vanilla client. Fires a single unified event with structured arguments instead of vanilla's fragmented localized text events. Enables modern DPS meter addons without expensive string parsing.
Provides WotLK 3.3.5-style `COMBAT_LOG_EVENT_UNFILTERED` for the vanilla client. Fires a single unified event with structured arguments instead of vanilla's fragmented localized text events. Enables modern DPS meter addons without expensive string parsing.
37 subevents covering all combat interactions: damage (spell, melee, periodic, environmental, damage shield, damage split), healing (direct, periodic, overheal tracking), misses (all types), auras (applied, removed, refreshed, broken, dose changes), casts (start, success, failed, interrupted), power (energize, drain, leech), dispels, extra attacks, deaths, and kills.
@@ -297,6 +297,37 @@ if (hMod) {
}
```
### Version Query API
WeirdUtils registers a Lua global table and query function for addon developers to detect which modules are loaded and their versions. Available from the login screen onward.
#### `GetWeirdUtilsVersion()`
Returns the `WeirdUtils` table containing all enabled modules and their version strings:
```lua
local modules = GetWeirdUtilsVersion()
for name, version in pairs(modules) do
print(name .. " v" .. version) -- e.g. "dpslog v1.0"
end
```
#### `GetWeirdUtilsVersion("modulename")`
Returns the version string for a specific module, or `nil` if not loaded:
```lua
if GetWeirdUtilsVersion("dpslog") then
-- DPSLog is available, register for COMBAT_LOG_EVENT_UNFILTERED
end
local ver = GetWeirdUtilsVersion("minimapicons") -- "1.0" or nil
```
The `WeirdUtils` table is additive -- if multiple independent DLLs are loaded (e.g. `dpslog.dll` and `minimapicons.dll` separately), each adds its own modules to the shared table.
---
### Module Mutexes
Each module also holds a named mutex while active: `Local\WeirdUtils_<name>_<PID>` (e.g. `Local\WeirdUtils_framecrash_12345`). The exception is transmogfix, which uses `Local\TransmogCoalesceHook_<PID>` for legacy reasons.
+9 -1
View File
@@ -3,6 +3,7 @@ const std = @import("std");
const ModuleDesc = struct {
name: []const u8,
desc: []const u8,
version: []const u8 = "1.0",
default: bool = true,
/// Source directory under src/ (defaults to name if null).
src_dir: ?[]const u8 = null,
@@ -255,7 +256,7 @@ fn addModuleOptions(b: *std.Build, opts: *std.Build.Step.Options) void {
inline for (module_list) |mod| {
opts.addOption(bool, "enable_" ++ mod.name, b.option(bool, mod.name, mod.desc) orelse mod.default);
}
// Pass full module name list so addons.zig doesn't need a hardcoded copy
// Pass full module name/version lists so main.zig/addons.zig can use them at comptime
const names: []const []const u8 = comptime blk: {
var n: [module_list.len][]const u8 = undefined;
for (module_list, 0..) |mod, i| n[i] = mod.name;
@@ -263,6 +264,13 @@ fn addModuleOptions(b: *std.Build, opts: *std.Build.Step.Options) void {
break :blk &final;
};
opts.addOption([]const []const u8, "all_module_names", names);
const versions: []const []const u8 = comptime blk: {
var v: [module_list.len][]const u8 = undefined;
for (module_list, 0..) |mod, i| v[i] = mod.version;
const final = v;
break :blk &final;
};
opts.addOption([]const []const u8, "all_module_versions", versions);
addFileListOptions(b, opts);
}
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();