Files
OctoLatency/OctoLatency.lua
T
kompolompoandClaude Sonnet 5 5e1f373b75 Fix drag getting stuck to cursor on release, bump to 1.2
OnMouseUp only fires while the cursor is still over the frame at the
instant the button is released, which a quick flick of this small
frame easily misses, leaving it stuck following the mouse forever.
OnUpdate now polls IsMouseButtonDown every frame and stops the drag
as soon as the button is released, independent of cursor position.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 12:31:35 +02:00

243 lines
8.0 KiB
Lua

--------------------------------------------------------------------------------
-- OctoLatency for Vanilla WoW (1.12.1) - Author: Palladinpala
--------------------------------------------------------------------------------
-- Throttle timer to reduce CPU/performance overhead
local elapsedTimer = 0
local UPDATE_INTERVAL = 2.0 -- update every 2 seconds
local MAX_ERROR_LOG = 20
local function OctoLatency_EnsureDB()
if (not OctoLatencyDB) then
OctoLatencyDB = {}
end
if (not OctoLatencyDB.errorLog) then
OctoLatencyDB.errorLog = {}
end
return OctoLatencyDB
end
-- Records an error to SavedVariables (visible in-game via /octolat errors,
-- and readable straight out of the WTF SavedVariables file after logout)
-- and pings the chat frame so it isn't silently swallowed.
function OctoLatency_LogError(context, err)
local db = OctoLatency_EnsureDB()
local entry = {
time = date("%Y-%m-%d %H:%M:%S"),
context = context,
message = tostring(err),
}
table.insert(db.errorLog, entry)
while (table.getn(db.errorLog) > MAX_ERROR_LOG) do
table.remove(db.errorLog, 1)
end
DEFAULT_CHAT_FRAME:AddMessage("|cffff5555OctoLatency error|r in " .. context .. ": " .. tostring(err) .. " (type /octolat errors)")
end
-- Runs func in protected mode so one bad tick can't break the addon,
-- and captures the error for later reporting instead of losing it.
local function OctoLatency_SafeCall(context, func, ...)
local ok, err = pcall(func, ...)
if (not ok) then
OctoLatency_LogError(context, err)
end
return ok
end
function OctoLatency_OnLoad(self)
-- Tooltip on hover
self:SetScript("OnEnter", function()
OctoLatency_SafeCall("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:AddLine("OctoLatency", 0, 1, 1)
local bandwidth, latency, homeLatency, worldLatency = GetNetStats()
bandwidth = bandwidth or 0
homeLatency = homeLatency or 0
worldLatency = worldLatency or 0
GameTooltip:AddLine("Home Latency: " .. homeLatency .. " ms", 1, 1, 1)
GameTooltip:AddLine("World Latency: " .. worldLatency .. " ms", 1, 1, 1)
GameTooltip:AddLine("Bandwidth: " .. string.format("%.2f", bandwidth) .. " KB/s", 1, 1, 1)
GameTooltip:AddLine("Left-click and drag to move.", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
end)
self:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
end
local function OctoLatency_UpdateText()
-- GetNetStats returns: bandwidth, latency, homeLatency, worldLatency
local _, _, homeLatency, worldLatency = GetNetStats()
-- We'll display world latency (or max of home/world)
-- Some servers never populate world latency (always 0), and 0 is
-- truthy in Lua, so treat a zero world latency as "unavailable" too.
local latency = homeLatency or 0
if (worldLatency and worldLatency > 0) then
latency = worldLatency
end
-- Color thresholds: Green < 150ms, Yellow 150-350ms, Red > 350ms
local colorHex
if (latency < 150) then
colorHex = "|cff00ff00" -- Green
elseif (latency <= 350) then
colorHex = "|cffffff00" -- Yellow
else
colorHex = "|cffff0000" -- Red
end
OctoLatencyText:SetText(colorHex .. latency .. " ms|r")
end
function OctoLatency_OnUpdate(elapsed)
-- OnMouseUp only fires while the cursor is still over the frame at
-- release, which a quick flick of this small frame easily misses,
-- leaving it stuck following the cursor forever. Poll the actual
-- button state every frame so it lets go regardless of where the
-- mouse ends up.
if (this.isMoving and not IsMouseButtonDown("LeftButton")) then
OctoLatency_StopMoving(this)
end
elapsedTimer = elapsedTimer + elapsed
if (elapsedTimer < UPDATE_INTERVAL) then
return
end
elapsedTimer = 0
OctoLatency_SafeCall("OnUpdate", OctoLatency_UpdateText)
end
function OctoLatency_StopMoving(self)
if (self.isMoving) then
self:StopMovingOrSizing()
self.isMoving = false
OctoLatency_SavePosition()
end
end
function OctoLatency_SavePosition()
if (not OctoLatencyDB) then
OctoLatencyDB = {}
end
local point, relativeTo, relativePoint, xOfs, yOfs = OctoLatencyFrame:GetPoint()
OctoLatencyDB.point = point
OctoLatencyDB.relativePoint = relativePoint
OctoLatencyDB.xOfs = xOfs
OctoLatencyDB.yOfs = yOfs
end
--------------------------------------------------------------------------------
-- Copy Errors window: vanilla WoW chat text can't be selected or copied, so
-- this shows the log in a multi-line EditBox that's auto-highlighted for Ctrl+C.
--------------------------------------------------------------------------------
local copyFrame, copyEditBox
local function OctoLatency_BuildErrorText()
local db = OctoLatency_EnsureDB()
if (table.getn(db.errorLog) == 0) then
return "No errors logged."
end
local lines = {}
for i, entry in ipairs(db.errorLog) do
table.insert(lines, "[" .. entry.time .. "] " .. entry.context .. ": " .. entry.message)
end
return table.concat(lines, "\n")
end
local function OctoLatency_CreateCopyFrame()
local f = CreateFrame("Frame", "OctoLatencyCopyFrame", UIParent)
f:SetFrameStrata("DIALOG")
f:SetWidth(420)
f:SetHeight(260)
f:SetPoint("CENTER")
f:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 },
})
f:EnableMouse(true)
f:SetMovable(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
f:Hide()
local title = f:CreateFontString(nil, "ARTWORK", "GameFontNormal")
title:SetPoint("TOP", f, "TOP", 0, -16)
title:SetText("OctoLatency Errors (Ctrl+C to copy, Esc to close)")
local scroll = CreateFrame("ScrollFrame", "OctoLatencyCopyScroll", f, "UIPanelScrollFrameTemplate")
scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -40)
scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -34, 44)
local editBox = CreateFrame("EditBox", "OctoLatencyCopyEditBox", scroll)
editBox:SetMultiLine(true)
editBox:SetFontObject(ChatFontNormal)
editBox:SetWidth(360)
editBox:SetAutoFocus(false)
editBox:SetScript("OnEscapePressed", function() copyFrame:Hide() end)
scroll:SetScrollChild(editBox)
copyEditBox = editBox
local closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -4)
closeBtn:SetScript("OnClick", function() f:Hide() end)
copyFrame = f
end
function OctoLatency_ShowCopyErrors()
if (not copyFrame) then
OctoLatency_CreateCopyFrame()
end
copyEditBox:SetText(OctoLatency_BuildErrorText())
copyFrame:Show()
copyEditBox:SetFocus()
copyEditBox:HighlightText()
end
-- Load saved position if available on PLAYER_LOGIN
local loadFrame = CreateFrame("Frame")
loadFrame:RegisterEvent("PLAYER_LOGIN")
loadFrame:SetScript("OnEvent", function()
OctoLatency_SafeCall("PLAYER_LOGIN", function()
if (OctoLatencyDB and OctoLatencyDB.point) then
OctoLatencyFrame:ClearAllPoints()
OctoLatencyFrame:SetPoint(OctoLatencyDB.point, UIParent, OctoLatencyDB.relativePoint, OctoLatencyDB.xOfs, OctoLatencyDB.yOfs)
end
end)
end)
--------------------------------------------------------------------------------
-- Slash command: /octolat errors | copyerrors | clearerrors
--------------------------------------------------------------------------------
SLASH_OCTOLATENCY1 = "/octolat"
SlashCmdList["OCTOLATENCY"] = function(msg)
local db = OctoLatency_EnsureDB()
msg = string.lower(msg or "")
if (msg == "errors") then
if (table.getn(db.errorLog) == 0) then
DEFAULT_CHAT_FRAME:AddMessage("OctoLatency: no errors logged.")
return
end
DEFAULT_CHAT_FRAME:AddMessage("OctoLatency: last " .. table.getn(db.errorLog) .. " error(s):")
for i, entry in ipairs(db.errorLog) do
DEFAULT_CHAT_FRAME:AddMessage("[" .. entry.time .. "] " .. entry.context .. ": " .. entry.message)
end
elseif (msg == "copyerrors") then
OctoLatency_ShowCopyErrors()
elseif (msg == "clearerrors") then
db.errorLog = {}
DEFAULT_CHAT_FRAME:AddMessage("OctoLatency: error log cleared.")
else
DEFAULT_CHAT_FRAME:AddMessage("OctoLatency commands: /octolat errors, /octolat copyerrors, /octolat clearerrors")
end
end