From 442eeb9cbc94eb6f9312e0084b90fd121bc2e9cb Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 2 Dec 2025 15:24:48 -0500 Subject: [PATCH] fix lag of casting and channeling conditionals. --- Conditionals.lua | 221 +++++++++++++++++++++++++++++++++-------------- Core.lua | 138 ++++++++++++++++++++++++++--- Init.lua | 10 ++- 3 files changed, 287 insertions(+), 82 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index a5d0557..d1b49b2 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -351,18 +351,71 @@ function CleveRoids.GetSpammableConditional(name) return CleveRoids.spamConditions[name] or "nomybuff" end +-- Checks whether or not we're currently casting a spell with cast time +-- Returns TRUE if we should allow the cast (not casting, or not casting the specified spell) +-- Returns FALSE if we should block the cast (currently casting) +function CleveRoids.CheckCasting(castingSpell) + -- No parameter: check if we're casting ANYTHING + if not castingSpell or castingSpell == "" then + -- Time-based prediction: if we know cast duration, check if it should be done + if CleveRoids.CurrentSpell.type == "cast" and CleveRoids.castStartTime and CleveRoids.castDuration then + local elapsed = GetTime() - CleveRoids.castStartTime + local remaining = CleveRoids.castDuration - elapsed + + -- If cast should be done (with 0.1s grace period), treat as not casting + if remaining <= 0.1 then + CleveRoids.CurrentSpell.type = "" + return true + end + end + + return CleveRoids.CurrentSpell.type ~= "cast" + end + + -- With parameter: check if we're casting a specific spell + local spellName = string.gsub(CleveRoids.CurrentSpell.spellName or "", "%(.-%)%s*", "") + local casting = string.gsub(castingSpell, "%(.-%)%s*", "") + + -- If we're casting this specific spell, block the recast + if CleveRoids.CurrentSpell.type == "cast" and spellName == casting then + return false + end + + -- Not casting the specified spell, allow the cast + return true +end + -- Checks whether or not we're currently casting a channeled spell +-- Returns TRUE if we should allow the cast (not channeling, or not channeling the specified spell) +-- Returns FALSE if we should block the cast (currently channeling) function CleveRoids.CheckChanneled(channeledSpell) - if not channeledSpell then return false end + -- No parameter: check if we're channeling ANYTHING + if not channeledSpell or channeledSpell == "" then + -- Time-based prediction: if we know channel duration, check if it should be done + if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then + local elapsed = GetTime() - CleveRoids.channelStartTime + local remaining = CleveRoids.channelDuration - elapsed + + -- If channel should be done (with 0.1s grace period), treat as not channeling + if remaining <= 0.1 then + CleveRoids.CurrentSpell.type = "" + return true + end + end + + return CleveRoids.CurrentSpell.type ~= "channeled" + end -- Remove the "(Rank X)" part from the spells name in order to allow downranking - local spellName = string.gsub(CleveRoids.CurrentSpell.spellName, "%(.-%)%s*", "") + local spellName = string.gsub(CleveRoids.CurrentSpell.spellName or "", "%(.-%)%s*", "") local channeled = string.gsub(channeledSpell, "%(.-%)%s*", "") + -- If we're channeling this specific spell, block the recast if CleveRoids.CurrentSpell.type == "channeled" and spellName == channeled then return false end + -- Special cases for auto-attacks if channeled == CleveRoids.Localized.Attack then return not CleveRoids.CurrentSpell.autoAttack end @@ -375,7 +428,7 @@ function CleveRoids.CheckChanneled(channeledSpell) return not CleveRoids.CurrentSpell.wand end - CleveRoids.CurrentSpell.spellName = channeled + -- If none of the special cases matched, allow the cast (not channeling the specified spell) return true end @@ -1786,29 +1839,45 @@ CleveRoids.Keywords = { end, conditionals, "nocasting") end, - -- NEW: Direct player casting check using Nampower's GetCurrentCastingInfo - -- More reliable than [nocasting @player] for checking if YOU are casting + -- NEW: Direct player casting check with time-based prediction + -- Uses our accurate state tracking instead of GetCurrentCastingInfo polling selfcasting = function(conditionals) - if not GetCurrentCastingInfo then return false end - local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo() - -- Explicit check: casting or channeling must equal 1 (active) - if casting and casting == 1 then return true end - if channeling and channeling == 1 then return true end - return false + -- Check for cast with time-based prediction + if CleveRoids.CurrentSpell.type == "cast" and CleveRoids.castStartTime and CleveRoids.castDuration then + local remaining = CleveRoids.castDuration - (GetTime() - CleveRoids.castStartTime) + if remaining <= 0.1 then + return false -- Cast is done + end + end + + -- Check for channel with time-based prediction + if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then + local remaining = CleveRoids.channelDuration - (GetTime() - CleveRoids.channelStartTime) + if remaining <= 0.1 then + return false -- Channel is done + end + end + + return CleveRoids.CurrentSpell.type == "cast" or CleveRoids.CurrentSpell.type == "channeled" end, noselfcasting = function(conditionals) - -- If GetCurrentCastingInfo not available (shouldn't happen with Nampower), assume not casting - if not GetCurrentCastingInfo then return true end + -- Inverse of selfcasting with same prediction logic + if CleveRoids.CurrentSpell.type == "cast" and CleveRoids.castStartTime and CleveRoids.castDuration then + local remaining = CleveRoids.castDuration - (GetTime() - CleveRoids.castStartTime) + if remaining <= 0.1 then + return true -- Cast is done + end + end - local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo() + if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then + local remaining = CleveRoids.channelDuration - (GetTime() - CleveRoids.channelStartTime) + if remaining <= 0.1 then + return true -- Channel is done + end + end - -- Return TRUE if player is NOT casting AND NOT channeling - -- Explicit checks for safety - local isCasting = (casting and casting == 1) or false - local isChanneling = (channeling and channeling == 1) or false - - return not isCasting and not isChanneling + return CleveRoids.CurrentSpell.type ~= "cast" and CleveRoids.CurrentSpell.type ~= "channeled" end, zone = function(conditionals) @@ -2017,9 +2086,27 @@ CleveRoids.Keywords = { end, checkchanneled = function(conditionals) - return Or(conditionals.checkchanneled, function(channeledSpells) - return CleveRoids.CheckChanneled(channeledSpells) - end) + if conditionals.checkchanneled == true then + -- Boolean form [checkchanneled] - check if NOT channeling anything + return CleveRoids.CheckChanneled(nil) + else + -- String form [checkchanneled:SpellName] - check if NOT channeling that spell + return Or(conditionals.checkchanneled, function(channeledSpells) + return CleveRoids.CheckChanneled(channeledSpells) + end) + end + end, + + checkcasting = function(conditionals) + if conditionals.checkcasting == true then + -- Boolean form [checkcasting] - check if NOT casting anything + return CleveRoids.CheckCasting(nil) + else + -- String form [checkcasting:SpellName] - check if NOT casting that spell + return Or(conditionals.checkcasting, function(castingSpells) + return CleveRoids.CheckCasting(castingSpells) + end) + end end, buff = function(conditionals) @@ -2361,45 +2448,38 @@ CleveRoids.Keywords = { end, channeled = function(conditionals) - if GetCurrentCastingInfo then - local _, _, _, _, channeling = GetCurrentCastingInfo() - return channeling == 1 + -- Use time-based prediction for accuracy + if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then + local remaining = CleveRoids.channelDuration - (GetTime() - CleveRoids.channelStartTime) + if remaining <= 0.1 then + return false -- Channel is done + end end return CleveRoids.CurrentSpell.type == "channeled" end, nochanneled = function(conditionals) - if GetCurrentCastingInfo then - local _, _, _, _, channeling = GetCurrentCastingInfo() - return channeling ~= 1 + -- Use time-based prediction for accuracy + if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then + local remaining = CleveRoids.channelDuration - (GetTime() - CleveRoids.channelStartTime) + if remaining <= 0.1 then + return true -- Channel is done + end end return CleveRoids.CurrentSpell.type ~= "channeled" end, channeltime = function(conditionals) - -- Try both "player" literal and playerGuid for tracking lookup - local playerCast = CleveRoids.spell_tracking["player"] or CleveRoids.spell_tracking[CleveRoids.playerGuid] - if not playerCast or playerCast.type ~= "CHANNEL" then - if CleveRoids.ChannelTimeDebug then - if not playerCast then - DEFAULT_CHAT_FRAME:AddMessage("|cffffff00[channeltime]|r No tracking data for player or playerGuid: " .. tostring(CleveRoids.playerGuid)) - -- Show what keys exist - local keys = "" - for k,v in pairs(CleveRoids.spell_tracking) do - keys = keys .. tostring(k) .. " " - end - if keys ~= "" then - DEFAULT_CHAT_FRAME:AddMessage("|cffffff00[channeltime]|r Tracking keys: " .. keys) - end - else - DEFAULT_CHAT_FRAME:AddMessage("|cffffff00[channeltime]|r Wrong type: " .. tostring(playerCast.type)) - end - end - return false + -- Calculate remaining time (0 if not channeling) + local timeLeft = 0 + + if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then + local elapsed = GetTime() - CleveRoids.channelStartTime + timeLeft = CleveRoids.channelDuration - elapsed + -- Don't allow negative time + if timeLeft < 0 then timeLeft = 0 end end - -- FOUND TRACKING - Debug this path too! - local timeLeft = playerCast.expires - GetTime() local check = conditionals.channeltime -- channeltime is stored as an array by the parser, get the first element @@ -2407,26 +2487,37 @@ CleveRoids.Keywords = { check = check[1] end - if CleveRoids.ChannelTimeDebug then - DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[channeltime FOUND]|r expires=%.2f, timeLeft=%.2f", playerCast.expires, timeLeft)) - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[channeltime FOUND]|r check type=" .. type(check) .. ", value=" .. tostring(check)) - if type(check) == "table" then - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[channeltime FOUND]|r operator=" .. tostring(check.operator) .. ", amount=" .. tostring(check.amount)) - end + if type(check) == "table" and check.operator and check.amount then + -- Now compare: if not channeling, timeLeft is 0, so [channeltime:<0.5] returns true + return CleveRoids.comparators[check.operator](timeLeft, check.amount) + end + + return false + end, + + casttime = function(conditionals) + -- Calculate remaining time (0 if not casting) + local timeLeft = 0 + + if CleveRoids.CurrentSpell.type == "cast" and CleveRoids.castStartTime and CleveRoids.castDuration then + local elapsed = GetTime() - CleveRoids.castStartTime + timeLeft = CleveRoids.castDuration - elapsed + -- Don't allow negative time + if timeLeft < 0 then timeLeft = 0 end + end + + local check = conditionals.casttime + + -- casttime is stored as an array by the parser, get the first element + if type(check) == "table" and type(check[1]) == "table" then + check = check[1] end if type(check) == "table" and check.operator and check.amount then - local result = CleveRoids.comparators[check.operator](timeLeft, check.amount) - if CleveRoids.ChannelTimeDebug then - DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[channeltime RESULT]|r %.2fs %s %.2f = %s", - timeLeft, check.operator, check.amount, result and "TRUE" or "FALSE")) - end - return result + -- Now compare: if not casting, timeLeft is 0, so [casttime:<0.5] returns true + return CleveRoids.comparators[check.operator](timeLeft, check.amount) end - if CleveRoids.ChannelTimeDebug then - DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[channeltime ERROR]|r check structure invalid, returning false") - end return false end, diff --git a/Core.lua b/Core.lua index 2a8efd0..292f397 100644 --- a/Core.lua +++ b/Core.lua @@ -1332,6 +1332,8 @@ function CleveRoids.ParseMsg(msg) nostealth = true, channeled = true, nochanneled = true, + checkchanneled = true, + checkcasting = true, dead = true, alive = true, help = true, @@ -3058,6 +3060,15 @@ CleveRoids.Frame:RegisterEvent("START_AUTOREPEAT_SPELL") CleveRoids.Frame:RegisterEvent("STOP_AUTOREPEAT_SPELL") CleveRoids.Frame:RegisterEvent("SPELLCAST_CHANNEL_START") CleveRoids.Frame:RegisterEvent("SPELLCAST_CHANNEL_STOP") +CleveRoids.Frame:RegisterEvent("SPELLCAST_START") +CleveRoids.Frame:RegisterEvent("SPELLCAST_STOP") +CleveRoids.Frame:RegisterEvent("SPELLCAST_FAILED") +CleveRoids.Frame:RegisterEvent("SPELLCAST_INTERRUPTED") + +-- Nampower SPELL_CAST_EVENT for reliable channel tracking +if GetCurrentCastingInfo then + CleveRoids.Frame:RegisterEvent("SPELL_CAST_EVENT") +end @@ -3170,6 +3181,18 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time -- ALSO store under "player" literal for easier lookup if caster == CleveRoids.playerGuid then CleveRoids.spell_tracking["player"] = CleveRoids.spell_tracking[caster] + + -- For CHANNEL events, capture duration for checkchanneled conditional + if action == "CHANNEL" then + CleveRoids.channelStartTime = GetTime() + CleveRoids.channelDuration = cast_time / 1000 -- Convert ms to seconds + end + + -- For START events, capture cast time for checkcasting conditional + if action == "START" then + CleveRoids.castStartTime = GetTime() + CleveRoids.castDuration = cast_time / 1000 -- Convert ms to seconds + end end if CleveRoids.ChannelTimeDebug and caster == CleveRoids.playerGuid then @@ -3237,26 +3260,115 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time end end -function CleveRoids.Frame:SPELLCAST_CHANNEL_START() - CleveRoids.CurrentSpell.type = "channeled" - -- BUGFIX: Update full casting state (for [casting] conditional) - if CleveRoids.UpdateCastingState then - CleveRoids.UpdateCastingState() - end - if CleveRoidMacros.realtime == 0 then - CleveRoids.QueueActionUpdate() +-- Nampower SPELL_CAST_EVENT handler for reliable channel tracking +-- This is the PRIMARY source of truth for channel state (not GetCurrentCastingInfo polling) +function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGuid, itemId) + DEFAULT_CHAT_FRAME:AddMessage(string.format("[SPELL_CAST_EVENT] success=%s, spellId=%s, castType=%s", + tostring(success), tostring(spellId), tostring(castType))) + + local CHANNEL = 4 + + if castType == CHANNEL and success == 1 then + -- Channel started successfully + CleveRoids.CurrentSpell.type = "channeled" + CleveRoids.CurrentSpell.castingSpellId = spellId + + local spellName = GetSpellInfo(spellId) + if spellName then + CleveRoids.CurrentSpell.spellName = spellName + DEFAULT_CHAT_FRAME:AddMessage(string.format("[SPELL_CAST_EVENT] Channel START: %s", spellName)) + end + + -- Force immediate action update + CleveRoids.TestForAllActiveActions() end end +function CleveRoids.Frame:SPELLCAST_CHANNEL_START() + -- Set channel state immediately when event fires + -- Duration is captured by UNIT_CASTEVENT which fires earlier + CleveRoids.CurrentSpell.type = "channeled" + + -- Try to get spell name from GetCurrentCastingInfo if available + if GetCurrentCastingInfo then + local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo() + if visId and visId > 0 then + CleveRoids.CurrentSpell.castingSpellId = visId + local spellName = GetSpellInfo(visId) + if spellName then + CleveRoids.CurrentSpell.spellName = spellName + end + end + end + + -- Force immediate action update + CleveRoids.TestForAllActiveActions() +end + function CleveRoids.Frame:SPELLCAST_CHANNEL_STOP() + -- Channel ended - clear state immediately CleveRoids.CurrentSpell.type = "" CleveRoids.CurrentSpell.spellName = "" - -- BUGFIX: Update full casting state (for [casting] conditional) - if CleveRoids.UpdateCastingState then - CleveRoids.UpdateCastingState() + CleveRoids.CurrentSpell.castingSpellId = nil + + -- Force immediate action update + CleveRoids.TestForAllActiveActions() +end + +function CleveRoids.Frame:SPELLCAST_START() + -- Cast-time spell started + -- Duration is captured by UNIT_CASTEVENT which fires earlier + CleveRoids.CurrentSpell.type = "cast" + + -- Try to get spell name from GetCurrentCastingInfo if available + if GetCurrentCastingInfo then + local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo() + if castId and castId > 0 then + CleveRoids.CurrentSpell.castingSpellId = castId + local spellName = GetSpellInfo(castId) + if spellName then + CleveRoids.CurrentSpell.spellName = spellName + end + end end - if CleveRoidMacros.realtime == 0 then - CleveRoids.QueueActionUpdate() + + -- Force immediate action update + CleveRoids.TestForAllActiveActions() +end + +function CleveRoids.Frame:SPELLCAST_STOP() + -- Cast finished - clear state immediately + if CleveRoids.CurrentSpell.type == "cast" then + CleveRoids.CurrentSpell.type = "" + CleveRoids.CurrentSpell.spellName = "" + CleveRoids.CurrentSpell.castingSpellId = nil + + -- Force immediate action update + CleveRoids.TestForAllActiveActions() + end +end + +function CleveRoids.Frame:SPELLCAST_FAILED() + -- Cast failed - clear state immediately + if CleveRoids.CurrentSpell.type == "cast" then + CleveRoids.CurrentSpell.type = "" + CleveRoids.CurrentSpell.spellName = "" + CleveRoids.CurrentSpell.castingSpellId = nil + + -- Force immediate action update + CleveRoids.TestForAllActiveActions() + end +end + +function CleveRoids.Frame:SPELLCAST_INTERRUPTED() + -- Cast interrupted - clear state immediately + if CleveRoids.CurrentSpell.type == "cast" then + CleveRoids.CurrentSpell.type = "" + CleveRoids.CurrentSpell.spellName = "" + CleveRoids.CurrentSpell.castingSpellId = nil + + -- Force immediate action update + CleveRoids.TestForAllActiveActions() end end diff --git a/Init.lua b/Init.lua index ad40f2e..41d1bd6 100644 --- a/Init.lua +++ b/Init.lua @@ -63,17 +63,19 @@ CleveRoids.UpdateCastingState = function() local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo() -- Update CurrentSpell based on actual cast state + -- NOTE: Channel state is EXCLUSIVELY managed by SPELLCAST_CHANNEL_START/STOP events + -- This function NEVER touches channel state, only regular casts if casting == 1 then CleveRoids.CurrentSpell.type = "cast" CleveRoids.CurrentSpell.castingSpellId = castId - elseif channeling == 1 then - CleveRoids.CurrentSpell.type = "channeled" - CleveRoids.CurrentSpell.castingSpellId = visId - else + elseif CleveRoids.CurrentSpell.type == "cast" then + -- Only clear if we were in a regular cast (not channel) CleveRoids.CurrentSpell.type = "" CleveRoids.CurrentSpell.castingSpellId = nil end + -- DO NOT touch channel state here - events handle it + -- Always update metadata CleveRoids.CurrentSpell.autoAttack = (autoattack == 1) CleveRoids.CurrentSpell.onSwingPending = (onswing == 1) CleveRoids.CurrentSpell.visualSpellId = visId