874cadc0f7
- Fix OnEnter tooltip crash: attempt to concatenate local 'worldLatency' (a nil value) when GetNetStats() hasn't populated latency yet (e.g. right after login/loading screen); fall back to 0 like OctoLatency_UpdateText already does - Add /octolat copyerrors and a minimap button opening a copy-errors window (auto-highlighted EditBox), since vanilla WoW chat text can't be selected/copied directly - Minimap button also right-click toggles the latency frame and can be dragged to reposition - Color the addon's title in the AddOns list with a per-letter dark-to-light green gradient - Add CHANGELOG.md Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
307 lines
10 KiB
Lua
307 lines
10 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)
|
|
local latency = worldLatency or homeLatency or 0
|
|
|
|
-- 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)
|
|
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
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Minimap button: left-click opens the copy-errors window, right-click toggles
|
|
-- the latency frame, drag to reposition around the minimap.
|
|
--------------------------------------------------------------------------------
|
|
local function OctoLatency_UpdateMinimapButtonPosition(button)
|
|
local db = OctoLatency_EnsureDB()
|
|
local angle = db.minimapAngle or 220
|
|
local rad = math.rad(angle)
|
|
local radius = 80
|
|
button:ClearAllPoints()
|
|
button:SetPoint("CENTER", Minimap, "CENTER", radius * math.cos(rad), radius * math.sin(rad))
|
|
end
|
|
|
|
local function OctoLatency_CreateMinimapButton()
|
|
local button = CreateFrame("Button", "OctoLatencyMinimapButton", Minimap)
|
|
button:SetWidth(31)
|
|
button:SetHeight(31)
|
|
button:SetFrameStrata("MEDIUM")
|
|
button:SetFrameLevel(8)
|
|
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
button:RegisterForDrag("LeftButton")
|
|
|
|
local icon = button:CreateTexture(nil, "BACKGROUND")
|
|
icon:SetWidth(20)
|
|
icon:SetHeight(20)
|
|
icon:SetPoint("CENTER", 0, 0)
|
|
icon:SetTexture("Interface\\Icons\\INV_Misc_PocketWatch_01")
|
|
|
|
local overlay = button:CreateTexture(nil, "OVERLAY")
|
|
overlay:SetWidth(53)
|
|
overlay:SetHeight(53)
|
|
overlay:SetPoint("TOPLEFT", 0, 0)
|
|
overlay:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
|
|
|
|
button:SetScript("OnClick", function()
|
|
OctoLatency_SafeCall("MinimapButtonClick", function()
|
|
if (arg1 == "RightButton") then
|
|
if (OctoLatencyFrame:IsShown()) then
|
|
OctoLatencyFrame:Hide()
|
|
else
|
|
OctoLatencyFrame:Show()
|
|
end
|
|
else
|
|
OctoLatency_ShowCopyErrors()
|
|
end
|
|
end)
|
|
end)
|
|
|
|
button:SetScript("OnEnter", function()
|
|
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
|
|
GameTooltip:AddLine("OctoLatency", 0, 1, 1)
|
|
GameTooltip:AddLine("Left-click: copy error log", 1, 1, 1)
|
|
GameTooltip:AddLine("Right-click: toggle latency frame", 1, 1, 1)
|
|
GameTooltip:AddLine("Drag: move this button", 0.7, 0.7, 0.7)
|
|
GameTooltip:Show()
|
|
end)
|
|
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
|
|
button:SetScript("OnDragStart", function()
|
|
this:SetScript("OnUpdate", function()
|
|
local mx, my = GetCursorPosition()
|
|
local scale = Minimap:GetEffectiveScale()
|
|
mx, my = mx / scale, my / scale
|
|
local cx, cy = Minimap:GetCenter()
|
|
local db = OctoLatency_EnsureDB()
|
|
db.minimapAngle = math.deg(math.atan2(my - cy, mx - cx))
|
|
OctoLatency_UpdateMinimapButtonPosition(this)
|
|
end)
|
|
end)
|
|
button:SetScript("OnDragStop", function()
|
|
this:SetScript("OnUpdate", nil)
|
|
end)
|
|
|
|
OctoLatency_UpdateMinimapButtonPosition(button)
|
|
return button
|
|
end
|
|
|
|
-- Load saved position and create the minimap button 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
|
|
OctoLatency_CreateMinimapButton()
|
|
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
|