diff --git a/Chat.lua b/Chat.lua index 939e440..0be350a 100644 --- a/Chat.lua +++ b/Chat.lua @@ -374,12 +374,23 @@ function SFrames:RefreshClassColorCache() PersistClassCache() end +SFrames._classMissCache = {} +SFrames._classMissCacheTime = 0 + function SFrames:GetClassHexForName(name) if not name or name == "" then return nil end local cache = self.PlayerClassColorCache if cache[name] then return cache[name] end + if self._classMissCache[name] then return nil end self:RefreshClassColorCache() - return cache[name] + if cache[name] then return cache[name] end + self._classMissCache[name] = true + local now = GetTime() + if now - self._classMissCacheTime > 30 then + self._classMissCacheTime = now + self._classMissCache = {} + end + return nil end function SFrames:GetLevelForName(name) @@ -730,27 +741,24 @@ end local function ParseHardcoreDeathMessage(text) if type(text) ~= "string" or text == "" then return nil end + if not string.find(text, "硬核") and not string.find(text, "死亡") then + local lower = string.lower(text) + if not string.find(lower, "hc news") and not string.find(lower, "has fallen") + and not string.find(lower, "died") and not string.find(lower, "slain") then + return nil + end + end local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") clean = string.gsub(clean, "|r", "") - - -- Check for Hardcore death signatures - if string.find(string.lower(clean), "hc news") or string.find(clean, "硬核") or string.find(clean, "死亡") or string.find(string.lower(clean), "has fallen") or string.find(string.lower(clean), "died") or string.find(string.lower(clean), "slain") then - -- Turtle English "Level 14" - local _, _, lvlStr = string.find(clean, "Level%s+(%d+)") - if lvlStr then return tonumber(lvlStr) end - - -- Chinese "14级" - local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级") - if lvlStr2 then return tonumber(lvlStr2) end - - -- Fallback - local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)") - if lvlStr3 then return tonumber(lvlStr3) end - - -- If it matches death signatures but no level is found, return 1 as a baseline to trigger the filter. - if string.find(string.lower(clean), "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(clean, "has fallen"))) then - return 1 - end + local _, _, lvlStr = string.find(clean, "Level%s+(%d+)") + if lvlStr then return tonumber(lvlStr) end + local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级") + if lvlStr2 then return tonumber(lvlStr2) end + local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)") + if lvlStr3 then return tonumber(lvlStr3) end + local lower = string.lower(clean) + if string.find(lower, "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(lower, "has fallen"))) then + return 1 end return nil end @@ -1527,6 +1535,8 @@ local function EnsureDB() if type(db.editBoxPosition) ~= "string" then db.editBoxPosition = DEFAULTS.editBoxPosition end if type(db.editBoxX) ~= "number" then db.editBoxX = DEFAULTS.editBoxX end if type(db.editBoxY) ~= "number" then db.editBoxY = DEFAULTS.editBoxY end + if db.translateEnabled == nil then db.translateEnabled = true end + if db.chatMonitorEnabled == nil then db.chatMonitorEnabled = true end if type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end if db.layoutVersion < 2 then db.topPadding = DEFAULTS.topPadding @@ -3137,6 +3147,7 @@ function SFrames.Chat:ToggleConfigFrame() end local CONFIG_PAGE_ORDER = { + { key = "general", label = "通用", title = "通用设置", desc = "翻译引擎和聊天消息监控的总开关。", icon = "settings" }, { key = "window", label = "窗口", title = "聊天窗口", desc = "尺寸、缩放、边框和输入框位置。", icon = "settings" }, { key = "tabs", label = "标签", title = "标签管理", desc = "切换、重命名、新建和删除聊天标签。", icon = "chat" }, { key = "filters", label = "过滤", title = "消息过滤", desc = "为当前标签设置消息类型和频道接收规则。", icon = "settings" }, @@ -3442,6 +3453,57 @@ function SFrames.Chat:EnsureConfigFrame() return page end + local generalPage = CreatePage("general") + do + local engineSection = CreateCfgSection(generalPage, "翻译引擎", 0, 0, 584, 120, fontPath) + + AddControl(CreateCfgCheck(engineSection, "启用 AI 翻译引擎", 16, -30, + function() return EnsureDB().translateEnabled ~= false end, + function(checked) + EnsureDB().translateEnabled = (checked == true) + end, + function() + SFrames.Chat:RefreshConfigFrame() + end + )) + + local transDesc = engineSection:CreateFontString(nil, "OVERLAY") + transDesc:SetFont(fontPath, 10, "OUTLINE") + transDesc:SetPoint("TOPLEFT", engineSection, "TOPLEFT", 38, -50) + transDesc:SetWidth(520) + transDesc:SetJustifyH("LEFT") + transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。") + transDesc:SetTextColor(0.7, 0.7, 0.74) + + local monitorSection = CreateCfgSection(generalPage, "聊天消息监控", 0, -136, 584, 160, fontPath) + + AddControl(CreateCfgCheck(monitorSection, "启用聊天消息监控与收集", 16, -30, + function() return EnsureDB().chatMonitorEnabled ~= false end, + function(checked) + EnsureDB().chatMonitorEnabled = (checked == true) + end, + function() + SFrames.Chat:RefreshConfigFrame() + end + )) + + local monDesc = monitorSection:CreateFontString(nil, "OVERLAY") + monDesc:SetFont(fontPath, 10, "OUTLINE") + monDesc:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -50) + monDesc:SetWidth(520) + monDesc:SetJustifyH("LEFT") + monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、频道翻译触发等功能。\n关闭后消息将原样通过,不做任何处理(翻译、复制等功能不可用)。") + monDesc:SetTextColor(0.7, 0.7, 0.74) + + local reloadHint = monitorSection:CreateFontString(nil, "OVERLAY") + reloadHint:SetFont(fontPath, 10, "OUTLINE") + reloadHint:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -86) + reloadHint:SetWidth(520) + reloadHint:SetJustifyH("LEFT") + reloadHint:SetText("提示:更改监控开关后建议 /reload 以确保完全生效。") + reloadHint:SetTextColor(0.9, 0.75, 0.5) + end + local windowPage = CreatePage("window") do local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath) @@ -6562,26 +6624,42 @@ function SFrames.Chat:Initialize() end end - -- Helper: save a message to the persistent cache ring buffer + local _persistWriteIdx = table.getn(db.messageCache) local function PersistMessage(msgID, text, r, g, b, frameIndex) local cache = EnsureDB().messageCache if not cache then EnsureDB().messageCache = {} cache = EnsureDB().messageCache end - table.insert(cache, { - id = msgID, - text = text, - r = r, - g = g, - b = b, - frame = frameIndex or 1, - time = date("%H:%M:%S"), - }) - -- Trim to MAX_CACHE - while table.getn(cache) > MAX_CACHE do - table.remove(cache, 1) + _persistWriteIdx = _persistWriteIdx + 1 + if _persistWriteIdx > MAX_CACHE then + _persistWriteIdx = 1 end + local entry = cache[_persistWriteIdx] + if entry then + entry.id = msgID + entry.text = text + entry.r = r + entry.g = g + entry.b = b + entry.frame = frameIndex or 1 + entry.time = date("%H:%M:%S") + else + cache[_persistWriteIdx] = { + id = msgID, + text = text, + r = r, + g = g, + b = b, + frame = frameIndex or 1, + time = date("%H:%M:%S"), + } + end + end + + local _cfTabCache = {} + for i = 1, 7 do + _cfTabCache[_G["ChatFrame" .. i]] = i end for i = 1, 7 do @@ -6594,13 +6672,28 @@ function SFrames.Chat:Initialize() origAddMessage(self, text, r, g, b, alpha, holdTime) return end - if string.sub(text, 1, 10) ~= "|Hsfchat:" and string.sub(text, 1, 12) ~= "|cff888888|H" then + if EnsureDB().chatMonitorEnabled == false then + origAddMessage(self, text, r, g, b, alpha, holdTime) + return + end + local b1 = string.byte(text, 1) + if b1 == 124 then + local b2 = string.byte(text, 2) + if b2 == 72 and string.find(text, "^|Hsfchat:") then + origAddMessage(self, text, r, g, b, alpha, holdTime) + return + end + if b2 == 99 and string.find(text, "^|cff888888|H") then + origAddMessage(self, text, r, g, b, alpha, holdTime) + return + end + end + + do local db = EnsureDB() - -- Universal catch for Turtle WoW custom chat channels like [硬核] local chanName = GetChannelNameFromChatLine(text) if chanName and IsIgnoredChannelByDefault(chanName) then - -- Global HC kill switch override check if db.hcGlobalDisable then local lowerChan = string.lower(chanName) if string.find(lowerChan, "hc") or string.find(chanName, "硬核") or string.find(lowerChan, "hardcore") then @@ -6608,46 +6701,32 @@ function SFrames.Chat:Initialize() end end - local frameName = self:GetName() - local matchedTabIdx = nil - if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then - matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(self) - if not matchedTabIdx then - local _, _, frameNumStr = string.find(frameName, "^ChatFrame(%d+)$") - local frameNum = tonumber(frameNumStr) - if frameNum then - local db = EnsureDB() - if frameNum <= table.getn(db.tabs) then - matchedTabIdx = frameNum - end - end - end + local matchedTabIdx = _cfTabCache[self] + or SFrames.Chat:GetTabIndexForChatFrame(self) + if not matchedTabIdx then + return end - if matchedTabIdx then - local tab = EnsureDB().tabs[matchedTabIdx] - if tab then - if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then - return + local tabs = db.tabs + local tab = tabs and tabs[matchedTabIdx] + if tab then + if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then + return + end + local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName) + if shouldTranslate then + local cleanText = CleanTextForTranslation(text) + cleanText = string.gsub(cleanText, "^%[.-%]%s*", "") + local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*") + if senderName then + cleanText = string.gsub(cleanText, "^%[[^%]]+%]:%s*", "") end - -- If it passed channel filter, allow translation for these bypassed channels - local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName) - if shouldTranslate then - local cleanText = CleanTextForTranslation(text) - -- Remove the channel prefix from translation text to be clean - cleanText = string.gsub(cleanText, "^%[.-%]%s*", "") - -- It might also have [PlayerName]: - local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*") - if senderName then - cleanText = string.gsub(cleanText, "^%[[^%]]+%]:%s*", "") - end - if cleanText ~= "" then - local tabId = tab.id - SFrames.Chat:RequestAutoTranslation(cleanText, function(result) - if result and result ~= "" then - SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName) - end - end) - end + if cleanText ~= "" then + local tabId = tab.id + SFrames.Chat:RequestAutoTranslation(cleanText, function(result) + if result and result ~= "" then + SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName) + end + end) end end else @@ -6655,7 +6734,6 @@ function SFrames.Chat:Initialize() end end - -- Hardcore Death Event Overrides if db.hcDeathDisable or (db.hcDeathLevelMin and db.hcDeathLevelMin > 1) then local deathLvl = ParseHardcoreDeathMessage(text) if deathLvl then @@ -6667,47 +6745,13 @@ function SFrames.Chat:Initialize() SFrames.Chat.MessageIndex = SFrames.Chat.MessageIndex + 1 local msgID = SFrames.Chat.MessageIndex - -- Store in runtime lookup SFrames.Chat.MessageHistory[msgID] = text - -- Persist to SavedVariables - local frameIdx = nil - for fi = 1, 7 do - if self == _G["ChatFrame" .. fi] then - frameIdx = fi - break - end - end - PersistMessage(msgID, text, r, g, b, frameIdx) + PersistMessage(msgID, text, r, g, b, _cfTabCache[self]) - -- Apply class color to player names in message local coloredText = ColorPlayerNamesInText(text) - -- Insert the clickable button [+] at the beginning local modifiedText = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. coloredText origAddMessage(self, modifiedText, r, g, b, alpha, holdTime) - - -- Legacy auto-translate disabled; handled by per-tab routing above. - if false then - if self == ChatFrame1 and not string.find(text, "%[翻译%]") then - if string.find(text, "%[.-硬核.-%]") or string.find(string.lower(text), "%[.-hc.-%]") or string.find(string.lower(text), "%[.-hardcore.-%]") then - local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") - cleanText = string.gsub(cleanText, "|r", "") - cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1") - - if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then - _G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta) - if result then - DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r") - end - end, "Nanami-UI") - elseif _G.STranslate and _G.STranslate.SendIO then - _G.STranslate:SendIO(cleanText, "IN", "auto", "zh") - end - end - end - end - else - origAddMessage(self, text, r, g, b, alpha, holdTime) end end end diff --git a/ConfigUI.lua b/ConfigUI.lua index b0f5edb..6ab2b24 100644 --- a/ConfigUI.lua +++ b/ConfigUI.lua @@ -509,7 +509,6 @@ local function EnsureDB() if type(SFramesDB.Tweaks) ~= "table" then SFramesDB.Tweaks = {} end if SFramesDB.Tweaks.autoStance == nil then SFramesDB.Tweaks.autoStance = true end - if SFramesDB.Tweaks.autoDismount == nil then SFramesDB.Tweaks.autoDismount = true end if SFramesDB.Tweaks.superWoW == nil then SFramesDB.Tweaks.superWoW = true end if SFramesDB.Tweaks.turtleCompat == nil then SFramesDB.Tweaks.turtleCompat = true end if SFramesDB.Tweaks.cooldownNumbers == nil then SFramesDB.Tweaks.cooldownNumbers = true end @@ -1090,14 +1089,7 @@ function SFrames.ConfigUI:BuildUIPage() CreateDesc(tweaksSection, "施法需要特定姿态时自动切换(如人形按熊掌→自动变熊)", 36, -50, font, 218) table.insert(controls, CreateCheckBox(tweaksSection, - "自动取消变形/下马", 270, -34, - function() return SFramesDB.Tweaks.autoDismount ~= false end, - function(checked) SFramesDB.Tweaks.autoDismount = checked end - )) - CreateDesc(tweaksSection, "变形/骑马时施法自动取消(如熊形按治疗→自动回人形)", 292, -50, font, 218) - - table.insert(controls, CreateCheckBox(tweaksSection, - "乌龟服兼容修改", 14, -80, + "乌龟服兼容修改", 270, -34, function() return SFramesDB.Tweaks.turtleCompat ~= false end, function(checked) SFramesDB.Tweaks.turtleCompat = checked end )) diff --git a/Core.lua b/Core.lua index f935cc3..b49b294 100644 --- a/Core.lua +++ b/Core.lua @@ -83,9 +83,11 @@ end -- Event Dispatcher SFrames.eventFrame:SetScript("OnEvent", function() - if SFrames.events[event] then - for i, func in ipairs(SFrames.events[event]) do - func(event) + local handlers = SFrames.events[event] + if handlers then + local n = table.getn(handlers) + for i = 1, n do + handlers[i](event) end end end) @@ -100,8 +102,8 @@ end function SFrames:UnregisterEvent(event, func) if self.events[event] then - for i, f in ipairs(self.events[event]) do - if f == func then + for i = 1, table.getn(self.events[event]) do + if self.events[event][i] == func then table.remove(self.events[event], i) break end diff --git a/Tooltip.lua b/Tooltip.lua index aa4e51d..76b62c8 100644 --- a/Tooltip.lua +++ b/Tooltip.lua @@ -147,7 +147,11 @@ function SFrames.FloatingTooltip:Initialize() bg:SetAllPoints(bgFrame) GameTooltip._nanamiBGTex = bg + bgFrame._ttVisCheck = 0 bgFrame:SetScript("OnUpdate", function() + this._ttVisCheck = this._ttVisCheck + arg1 + if this._ttVisCheck < 0.2 then return end + this._ttVisCheck = 0 if not GameTooltip:IsVisible() then this:Hide() end diff --git a/TrainerUI.lua b/TrainerUI.lua index 1f9bdd3..f282704 100644 --- a/TrainerUI.lua +++ b/TrainerUI.lua @@ -812,10 +812,20 @@ local function UpdateFilters() MainFrame.filterUsed:SetActive(currentFilter == "used") end +local _isUpdating = false + local function FullUpdate() - UpdateFilters() - UpdateList() - UpdateDetail() + if _isUpdating then return end + _isUpdating = true + local ok, err = pcall(function() + UpdateFilters() + UpdateList() + UpdateDetail() + end) + _isUpdating = false + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI TrainerUI: FullUpdate error: " .. tostring(err) .. "|r") + end end local function SelectService(index) @@ -1181,12 +1191,6 @@ function TUI:Initialize() ClassTrainerFrame:EnableMouse(false) end - if SetTrainerServiceTypeFilter then - SetTrainerServiceTypeFilter("available", 1) - SetTrainerServiceTypeFilter("unavailable", 1) - SetTrainerServiceTypeFilter("used", 1) - end - selectedIndex = nil currentFilter = "all" collapsedCats = {} @@ -1196,6 +1200,15 @@ function TUI:Initialize() npcName = npcName .. " - 专业训练" end MainFrame.npcNameFS:SetText(npcName) + + _isUpdating = true + if SetTrainerServiceTypeFilter then + pcall(SetTrainerServiceTypeFilter, "available", 1) + pcall(SetTrainerServiceTypeFilter, "unavailable", 1) + pcall(SetTrainerServiceTypeFilter, "used", 1) + end + _isUpdating = false + MainFrame:Show() BuildDisplayList() for _, entry in ipairs(displayList) do diff --git a/Tweaks.lua b/Tweaks.lua index 903e598..124ae8a 100644 --- a/Tweaks.lua +++ b/Tweaks.lua @@ -6,8 +6,7 @@ -- 4. Cooldown Numbers - show remaining cooldown time as text overlay -- 5. Dark UI - darken the entire interface -- 6. WorldMap Window - turn fullscreen map into a movable/scalable window --- 7. Auto Dismount - cancel shapeshift/mount when casting incompatible spells --- 8. Hunter Aspect Guard - auto switch to Hawk when taking damage with Cheetah/Pack +-- 7. Hunter Aspect Guard - cancel Cheetah/Pack when taking damage in combat (avoid OOC false positives) -------------------------------------------------------------------------------- SFrames.Tweaks = SFrames.Tweaks or {} @@ -75,120 +74,6 @@ local function InitAutoStance() end) end --------------------------------------------------------------------------------- --- Auto Dismount / Cancel Shapeshift --- When casting a spell that fails because you are mounted or shapeshifted, --- automatically cancel the mount/shapeshift buff so the next cast succeeds. --------------------------------------------------------------------------------- -local function InitAutoDismount() - local dismount = CreateFrame("Frame", "NanamiAutoDismount") - local _, playerClass = UnitClass("player") - - local scanner = CreateFrame("GameTooltip", "NanamiDismountScan", nil, "GameTooltipTemplate") - scanner:SetOwner(WorldFrame, "ANCHOR_NONE") - scanner:SetAlpha(0) - scanner:Hide() - - local mountStrings = { - "^Increases speed by (.+)%%", - "^Erhöht Tempo um (.+)%%", - "^Aumenta la velocidad en un (.+)%%", - "^Augmente la vitesse de (.+)%%", - "^Скорость увеличена на (.+)%%", - "^이동 속도 (.+)%%만큼 증가", - "^速度提高(.+)%%", "^移动速度提高(.+)%%", - "speed based on", "Slow and steady...", "Riding", - "Lento y constante...", "Aumenta la velocidad según tu habilidad de Montar.", - "根据您的骑行技能提高速度。", "根据骑术技能提高速度。", "又慢又稳......", - } - - local shapeshiftIcons = { - "ability_racial_bearform", "ability_druid_catform", - "ability_druid_travelform", "spell_nature_forceofnature", - "ability_druid_aquaticform", "spell_nature_spiritwolf", - "ability_druid_treeoflife", "ability_druid_stagform", - } - - local hunterAspectIcons = { - "ability_mount_jungletiger", - "ability_mount_packhorse", - } - - local errorStrings = {} - local errorGlobals = { - "SPELL_FAILED_NOT_MOUNTED", "ERR_ATTACK_MOUNTED", "ERR_TAXIPLAYERALREADYMOUNTED", - "ERR_NOT_WHILE_MOUNTED", - "SPELL_FAILED_NOT_SHAPESHIFT", "SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED", - "SPELL_NOT_SHAPESHIFTED", "SPELL_NOT_SHAPESHIFTED_NOSPACE", - "ERR_CANT_INTERACT_SHAPESHIFTED", "ERR_NOT_WHILE_SHAPESHIFTED", - "ERR_NO_ITEMS_WHILE_SHAPESHIFTED", "ERR_TAXIPLAYERSHAPESHIFTED", - "ERR_MOUNT_SHAPESHIFTED", - } - for _, name in pairs(errorGlobals) do - local val = getfenv(0)[name] - if val then table.insert(errorStrings, val) end - end - - dismount:RegisterEvent("UI_ERROR_MESSAGE") - dismount:SetScript("OnEvent", function() - if arg1 == SPELL_FAILED_NOT_STANDING then - SitOrStand() - return - end - - local matched = false - for _, err in pairs(errorStrings) do - if arg1 == err then matched = true break end - end - if not matched then return end - - for i = 0, 31 do - local buff = GetPlayerBuffTexture(i) - if buff then - local lowerBuff = string.lower(buff) - - local skip = false - if playerClass == "HUNTER" then - for _, tex in pairs(hunterAspectIcons) do - if string.find(lowerBuff, tex) then - skip = true - break - end - end - end - - if not skip then - scanner:ClearLines() - scanner:SetPlayerBuff(i) - for line = 1, scanner:NumLines() do - local text = getfenv(0)["NanamiDismountScanTextLeft" .. line] - if text and text:GetText() then - for _, str in pairs(mountStrings) do - if string.find(text:GetText(), str) then - CancelPlayerBuff(i) - return - end - end - end - end - - for _, icon in pairs(shapeshiftIcons) do - if string.find(lowerBuff, icon) then - CancelPlayerBuff(i) - return - end - end - - if string.find(lowerBuff, "ability_mount_") then - CancelPlayerBuff(i) - return - end - end - end - end - end) -end - -------------------------------------------------------------------------------- -- SuperWoW Compatibility -- Provides GUID-based cast/channel data when SuperWoW client mod is active. @@ -389,6 +274,78 @@ local function TimeConvert(remaining) end end +local _activeCooldowns = {} +local _cdTickTimer = 0 +local _cdUpdaterFrame + +local function CooldownSharedUpdate() + _cdTickTimer = _cdTickTimer + arg1 + if _cdTickTimer < 0.1 then return end + _cdTickTimer = 0 + + local now = GetTime() + local sysTime = time() + + local n = table.getn(_activeCooldowns) + local i = 1 + while i <= n do + local cdFrame = _activeCooldowns[i] + if cdFrame and cdFrame:IsShown() then + local parent = cdFrame:GetParent() + if parent then + cdFrame:SetAlpha(parent:GetAlpha()) + end + if cdFrame.start < now then + local remaining = cdFrame.duration - (now - cdFrame.start) + if remaining > 0 then + cdFrame.text:SetText(TimeConvert(remaining)) + else + cdFrame:Hide() + _activeCooldowns[i] = _activeCooldowns[n] + _activeCooldowns[n] = nil + n = n - 1 + i = i - 1 + end + else + local startupTime = sysTime - now + local cdTime = (2 ^ 32) / 1000 - cdFrame.start + local cdStartTime = startupTime - cdTime + local cdEndTime = cdStartTime + cdFrame.duration + local remaining = cdEndTime - sysTime + if remaining >= 0 then + cdFrame.text:SetText(TimeConvert(remaining)) + else + cdFrame:Hide() + _activeCooldowns[i] = _activeCooldowns[n] + _activeCooldowns[n] = nil + n = n - 1 + i = i - 1 + end + end + i = i + 1 + else + _activeCooldowns[i] = _activeCooldowns[n] + _activeCooldowns[n] = nil + n = n - 1 + end + end + if n == 0 and _cdUpdaterFrame then + _cdUpdaterFrame:Hide() + end +end + +local function RegisterCooldownFrame(cdFrame) + for i = 1, table.getn(_activeCooldowns) do + if _activeCooldowns[i] == cdFrame then return end + end + table.insert(_activeCooldowns, cdFrame) + if not _cdUpdaterFrame then + _cdUpdaterFrame = CreateFrame("Frame", "NanamiCDSharedUpdater", UIParent) + _cdUpdaterFrame:SetScript("OnUpdate", CooldownSharedUpdate) + end + _cdUpdaterFrame:Show() +end + local function CooldownOnUpdate() local parent = this:GetParent() if not parent then this:Hide() return end @@ -494,7 +451,7 @@ local function CreateCoolDown(cooldown, start, duration) cooldown.cooldowntext.text:SetPoint("CENTER", cooldown.cooldowntext, "CENTER", 0, 0) end - cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate) + RegisterCooldownFrame(cooldown.cooldowntext) end local function SetCooldown(frame, start, duration, enable) @@ -520,12 +477,13 @@ local function SetCooldown(frame, start, duration, enable) if start > 0 and duration > 0 and (not enable or enable > 0) then if frame.cooldownmask then frame.cooldownmask:Show() end frame.cooldowntext:Show() + frame.cooldowntext.start = start + frame.cooldowntext.duration = duration + RegisterCooldownFrame(frame.cooldowntext) else if frame.cooldownmask then frame.cooldownmask:Hide() end frame.cooldowntext:Hide() end - frame.cooldowntext.start = start - frame.cooldowntext.duration = duration end end @@ -1061,8 +1019,8 @@ end -------------------------------------------------------------------------------- -- Hunter Aspect Guard --- When a Hunter takes damage with Aspect of the Cheetah or Aspect of the Pack --- active, automatically cancel the aspect to prevent repeated dazing. +-- When a Hunter takes damage in combat with Aspect of the Cheetah or Pack +-- active, cancel the aspect to reduce daze chains. OOC HP changes are ignored. -------------------------------------------------------------------------------- local function InitHunterAspectGuard() local _, playerClass = UnitClass("player") @@ -1104,7 +1062,7 @@ local function InitHunterAspectGuard() return end - if lastHP > 0 and hp < lastHP then + if lastHP > 0 and hp < lastHP and UnitAffectingCombat("player") then if GetTime() - lastCancel >= 1.0 then if CancelDangerousAspect() then lastCancel = GetTime() @@ -1129,13 +1087,6 @@ function Tweaks:Initialize() end end - if cfg.autoDismount ~= false then - local ok, err = pcall(InitAutoDismount) - if not ok then - DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: AutoDismount init failed: " .. tostring(err) .. "|r") - end - end - if cfg.superWoW ~= false then local ok, err = pcall(InitSuperWoW) if not ok then diff --git a/Units/Party.lua b/Units/Party.lua index 9d4a1b4..f300318 100644 --- a/Units/Party.lua +++ b/Units/Party.lua @@ -493,7 +493,7 @@ function SFrames.Party:Initialize() if not self._globalUpdateFrame then self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent) - self._globalUpdateFrame:SetScript("OnUpdate", function() + self._globalUpdateFrame._onUpdateFunc = function() if SFrames.Party.testing then return end local dt = arg1 local frames = SFrames.Party.frames @@ -533,7 +533,8 @@ function SFrames.Party:Initialize() end end end - end) + end + self._globalUpdateFrame:Hide() end self:ApplyConfig() @@ -745,20 +746,35 @@ function SFrames.Party:UpdateAll() self.frames[i].frame:Hide() end end + if self._globalUpdateFrame then + self._globalUpdateFrame:SetScript("OnUpdate", nil) + self._globalUpdateFrame:Hide() + end return end if self.testing then return end local numParty = GetNumPartyMembers() + local hasVisible = false for i = 1, 4 do local data = self.frames[i] local f = data.frame if i <= numParty then f:Show() self:UpdateFrame(data.unit) + hasVisible = true else f:Hide() end end + if self._globalUpdateFrame then + if hasVisible then + self._globalUpdateFrame:SetScript("OnUpdate", self._globalUpdateFrame._onUpdateFunc) + self._globalUpdateFrame:Show() + else + self._globalUpdateFrame:SetScript("OnUpdate", nil) + self._globalUpdateFrame:Hide() + end + end end function SFrames.Party:UpdateFrame(unit) diff --git a/Units/Raid.lua b/Units/Raid.lua index 35c7476..6aa5300 100644 --- a/Units/Raid.lua +++ b/Units/Raid.lua @@ -539,11 +539,13 @@ function SFrames.Raid:EnsureFrames() if not self._globalUpdateFrame then self._globalUpdateFrame = CreateFrame("Frame", nil, UIParent) - self._globalUpdateFrame:SetScript("OnUpdate", function() + self._globalUpdateFrame._onUpdateFunc = function() if SFrames.Raid.testing then return end local dt = arg1 local frames = SFrames.Raid.frames if not frames then return end + local visibleCount = SFrames.Raid._visibleCount or 0 + if visibleCount == 0 then return end for i = 1, 40 do local entry = frames[i] if entry then @@ -573,7 +575,8 @@ function SFrames.Raid:EnsureFrames() end end end - end) + end + self._globalUpdateFrame:Hide() end self:ApplyLayout() @@ -602,6 +605,7 @@ function SFrames.Raid:UpdateAll() groupSlots[g] = 0 end + local visibleCount = 0 for i = 1, 40 do local name, rank, subgroup = GetRaidRosterInfo(i) if name and subgroup and subgroup >= 1 and subgroup <= 8 then @@ -615,9 +619,16 @@ function SFrames.Raid:UpdateAll() self.frames[frameIndex].unit = "raid" .. i f:Show() self:UpdateFrame("raid" .. i) + visibleCount = visibleCount + 1 end end end + self._visibleCount = visibleCount + + if self._globalUpdateFrame then + self._globalUpdateFrame:SetScript("OnUpdate", self._globalUpdateFrame._onUpdateFunc) + self._globalUpdateFrame:Show() + end -- Show/hide group labels based on whether each group has members local showLabel = SFramesDB and SFramesDB.raidShowGroupLabel ~= false @@ -632,6 +643,11 @@ function SFrames.Raid:UpdateAll() end end else + self._visibleCount = 0 + if self._globalUpdateFrame then + self._globalUpdateFrame:SetScript("OnUpdate", nil) + self._globalUpdateFrame:Hide() + end if not self.testing and self._framesBuilt then for i = 1, 40 do self.frames[i].frame:Hide() diff --git a/docs/AutoDismount-技术总结.md b/docs/AutoDismount-技术总结.md new file mode 100644 index 0000000..cc3ae4b --- /dev/null +++ b/docs/AutoDismount-技术总结.md @@ -0,0 +1,89 @@ +# 自动下马 / 取消变形(Auto Dismount)技术总结 + +## 功能概述 + +骑乘或变形状态下尝试施法时,自动取消坐骑 / 变形 buff,使下次施法直接生效。 + +## 检测架构(三层) + +### 第 1 层 — 错误消息匹配 + +监听 `UI_ERROR_MESSAGE` 事件: + +| 方式 | 说明 | +|---|---| +| 哈希表精确匹配 | `ERR_ATTACK_MOUNTED`, `ERR_NOT_WHILE_MOUNTED`, `ERR_MOUNT_SHAPESHIFTED` 等标准全局变量 | +| 关键词模糊匹配 | `mounted`, `shapeshifted`, `坐骑`, `骑乘`, `变形`, `形态`(兜底自定义错误消息) | + +> Turtle WoW 使用自定义错误 `"你正在骑乘状态"`,不在标准全局变量中,通过关键词 `"骑乘"` 命中。 + +### 第 2 层 — Dismount() API + +客户端若提供 `Dismount()` 函数则先调用(`pcall` 包裹),**不 early return**,继续 buff 扫描作为后备。 + +### 第 3 层 — Buff 扫描(0-39) + +遍历玩家 buff,逐个按优先级检测: + +| 优先级 | 检测方式 | 目标 | 示例 | +|---|---|---|---| +| 1 | 变形图标匹配 | 德鲁伊 / 萨满形态 | `ability_druid_catform`, `spell_nature_spiritwolf` | +| 2 | 坐骑图标模式 | 坐骑 buff 图标 | `ability_mount_*`, `inv_pet_speedy` | +| 3 | Tooltip 速度文本 | 坐骑速度描述(多语言) | `"又慢又稳"`, `"Increases speed by"` | +| 4 | Buff 名称关键词 | Tooltip 首行含坐骑关键词 | `"骑乘"`, `"riding"` | + +### 猎人守护跳过 + +猎人的猎豹 / 豹群守护使用 `ability_mount_*` 图标,需特殊跳过以免误取消。 + +## 调试关键发现 + +### Tooltip 扫描在 Turtle WoW 中失效 + +`GameTooltip:SetPlayerBuff(index)` 对所有 buff 均返回 **0 行**。 +即使重新 `SetOwner`、尝试 `GetPlayerBuff` 返回的 buffId,结果相同。 + +**影响**:tooltip 检测路径完全失效,必须依赖图标模式匹配。 + +### 非标准坐骑图标 + +| 坐骑 | 实际图标纹理 | +|---|---| +| 骑乘乌龟 | `inv_pet_speedy`(不匹配 `ability_mount_*`) | +| 标准坐骑 | `Ability_Mount_*` | +| AQ 坐骑 | `inv_misc_qirajicrystal_*` | + +## 完整图标模式清单 + +```lua +-- 坐骑 +mountIconPatterns = { + "ability_mount_", + "inv_misc_qirajicrystal", + "inv_pet_speedy", +} +-- 变形 +shapeshiftIcons = { + "ability_racial_bearform", -- 熊形态 + "ability_druid_catform", -- 猫形态 + "ability_druid_travelform", -- 旅行形态 + "spell_nature_forceofnature", -- 枭兽形态 + "ability_druid_aquaticform", -- 水栖形态 + "spell_nature_spiritwolf", -- 幽魂之狼 + "ability_druid_treeoflife", -- 生命之树 + "ability_druid_stagform", -- 鹿形态 + "ability_druid_flightform", -- 飞行形态 +} +-- 猎人守护(跳过) +hunterAspectIcons = { + "ability_mount_jungletiger", -- 猎豹守护 + "ability_mount_packhorse", -- 豹群守护 +} +``` + +## 扩展方法 + +遇到新坐骑无法自动下马时: +1. 在 `InitAutoDismount` 中将 `_debug` 设为 `true` +2. 骑上坐骑施法,查看聊天窗口中的 `[Nanami-DBG] buff[x]=` 输出 +3. 将新坐骑的图标关键字添加到 `mountIconPatterns`