Settings window (/ob options): window look, fixed corner, routes, alerts; footer only after a restart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyQKCL92ZLwPi9qmYQtNdc
This commit is contained in:
Claude
2026-10-12 15:24:10 -04:00
parent 695371d5b2
commit 06ae0d50b9
5 changed files with 258 additions and 43 deletions
+3
View File
@@ -5,8 +5,11 @@
### New
- **Accurate without the DLL.** OctoBus now ships with each route's restart offset (how many seconds into its trip it docks, counted from the last server restart), measured on 12 October. At every login it asks the server how long it has been up and sets every route it has not seen since that restart from the restart time plus the offset. A brand new copy with no DLL and no sightings gets every timer right from the first login, and stays right after later restarts.
- **Settings window**: `/ob options`, or right-click the minimap icon. Background opacity, window opacity, text size, window size, lock, which corner stays put as the list grows or shrinks, auto-show, which routes to show, and bell alerts. Changes apply at once.
### Changed
- The "no DLL: estimates only" note is gone from the panel: without the DLL the timers are now accurate. `/ob debug` still shows whether the DLL is loaded.
- The footer no longer names the route with the oldest data (the restart correction makes it unnecessary). It only appears after a server restart, while a route in the window has not been seen since.
## 1.3.4 (12 October 2026)
+81 -42
View File
@@ -22,6 +22,7 @@
SLASH COMMANDS (/ob, also /transit and /zep)
/ob show / hide the window (works anywhere)
/ob options settings window (also: right-click the minimap icon)
/ob list print all timers in chat
/ob alert <sec> bell alerts fire this many seconds before arrival (default 45)
/ob sound|chat|flash toggle that alert type
@@ -362,29 +363,6 @@ local OB_OK, OB_ERR = pcall(function ()
return string.format("%.1f days", sec / 86400)
end
-- footer text, and whether it should be highlighted: names the transport whose latest
-- sighting is the oldest, and how long ago that was. Orange after 5 days.
local function CalAgeText()
local oldR, oldT = nil, nil
for i = 1, table.getn(ROUTES) do
local r = ROUTES[i]
if Visible(r) then
local t = math.max(DB.lastObs[r.entry] or 0, DB.anchor[r.entry] or r.anchor or 0)
if not oldT or t < oldT then oldR, oldT = r, t end
end
end
if not oldR then return "", false end
if oldT <= 0 then return "oldest data: " .. RouteName(oldR) .. ", never seen", true end
local gone = time() - oldT
local ago
if gone < 120 then ago = "just now"
elseif gone < 3600 then ago = math.floor(gone / 60 + 0.5) .. " min ago"
elseif gone < 86400 then ago = string.format("%.0f", gone / 3600) .. " h ago"
else ago = math.floor(gone / 86400 + 0.5) .. " days ago" end
local stale = (gone >= 5 * 86400)
return "oldest data: " .. RouteName(oldR) .. ", seen " .. ago .. (stale and ", go see it" or ""), stale
end
local function CalFinish(complete)
if not cal then return end
Say(complete and "calibration complete." or "calibration stopped.")
@@ -398,9 +376,6 @@ local OB_OK, OB_ERR = pcall(function ()
(done and "" or " - not seen, still using the old value"))
end
end
if complete then
Say("done. The window footer tracks how fresh this is; it nags after 5 days.")
end
cal = nil
end
@@ -916,7 +891,7 @@ local OB_OK, OB_ERR = pcall(function ()
btn:SetFrameLevel(Minimap:GetFrameLevel() + 8)
btn:SetWidth(31)
btn:SetHeight(31)
btn:RegisterForClicks("LeftButtonUp")
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:RegisterForDrag("LeftButton")
btn.icon = btn:CreateTexture(nil, "ARTWORK")
btn.icon:SetWidth(20)
@@ -961,9 +936,11 @@ local OB_OK, OB_ERR = pcall(function ()
end)
-- the timer window
local WIDTH = 280
local ROW_H = 18
local TOP = 22
-- sizes at text size 100%; ApplyLook scales them with the text size setting
local textScale = CFG.textScale or 1
local WIDTH = math.floor(280 * textScale + 0.5)
local ROW_H = math.floor(18 * textScale + 0.5)
local TOP = math.floor(22 * textScale + 0.5)
local panel = CreateFrame("Frame", "OctoBusPanel", UIParent)
panel:SetFrameStrata("MEDIUM")
@@ -990,9 +967,17 @@ local OB_OK, OB_ERR = pcall(function ()
panel.foot:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 8, 4)
panel.foot:SetJustifyH("LEFT")
-- The window is pinned by one corner (CFG.anchor, set in the settings window; default
-- bottom-left), so when the list gets longer or shorter the other edges move.
local function SavePos()
local x, y = panel:GetLeft(), panel:GetBottom()
if x and y then CFG.pos = { x = x, y = y } end
local a = CFG.anchor or "BOTTOMLEFT"
local x = string.find(a, "RIGHT") and panel:GetRight() or panel:GetLeft()
local y = string.find(a, "TOP") and panel:GetTop() or panel:GetBottom()
if x and y then
CFG.pos = { x = x, y = y, a = a }
panel:ClearAllPoints()
panel:SetPoint(a, UIParent, "BOTTOMLEFT", x, y)
end
end
local function StartDrag()
@@ -1091,11 +1076,20 @@ local OB_OK, OB_ERR = pcall(function ()
local rows = {}
local function CreateRow(i)
local row = CreateFrame("Button", nil, panel)
local function PlaceRow(row, i)
row:SetHeight(ROW_H)
row:ClearAllPoints()
row:SetPoint("TOPLEFT", panel, "TOPLEFT", 4, -(TOP + (i - 1) * ROW_H))
row:SetPoint("RIGHT", panel, "RIGHT", -4, 0)
local fs = math.floor(10 * textScale + 0.5)
if row.name then
row.name:SetFont(font, fs, "OUTLINE")
row.time:SetFont(font, fs, "OUTLINE")
end
end
local function CreateRow(i)
local row = CreateFrame("Button", nil, panel)
row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
row:RegisterForDrag("LeftButton")
row:SetScript("OnDragStart", StartDrag)
@@ -1199,10 +1193,30 @@ local OB_OK, OB_ERR = pcall(function ()
GameTooltip:Show()
end)
row:SetScript("OnLeave", function() GameTooltip:Hide() end)
PlaceRow(row, i)
rows[i] = row
return row
end
-- Appearance (set from the settings window, /ob options): background opacity, window
-- opacity and text size. Text size scales the fonts and the row height and width with them.
local function ApplyLook()
local bg = CFG.bgAlpha or 0.75
panel:SetBackdropColor(0.02, 0.02, 0.02, bg)
panel:SetBackdropBorderColor(0.22, 0.22, 0.22, math.min(1, bg + 0.25))
panel:SetAlpha(CFG.alpha or 1)
textScale = CFG.textScale or 1
WIDTH = math.floor(280 * textScale + 0.5)
ROW_H = math.floor(18 * textScale + 0.5)
TOP = math.floor(22 * textScale + 0.5)
panel:SetWidth(WIDTH)
panel.title:SetFont(font, math.floor(10 * textScale + 0.5), "OUTLINE")
panel.status:SetFont(font, math.floor(9 * textScale + 0.5), "OUTLINE")
panel.foot:SetFont(font, math.floor(9 * textScale + 0.5), "OUTLINE")
for i = 1, table.getn(rows) do PlaceRow(rows[i], i) end
end
ApplyLook()
-- returns state ("docked" / "wait" / nil), seconds (may be nil if unknown)
local function Status(r)
if Flipped(r) then return Predict(r, true) end
@@ -1329,7 +1343,7 @@ local OB_OK, OB_ERR = pcall(function ()
end
end
for i = n + 1, table.getn(rows) do rows[i]:Hide() end
panel:SetHeight(TOP + n * ROW_H + 18)
panel:SetHeight(TOP + n * ROW_H + ((cal or (panel.foot:GetText() or "") ~= "") and math.floor(18 * textScale + 0.5) or 10))
panel.title:SetText("|cff33ffccTransports|r" .. (CFG.full and " |cff777777- all|r" or ""))
if recOn then
panel.status:SetText("|cffff4444REC|r " .. (type(DB.rec) == "table" and table.getn(DB.rec) or 0))
@@ -1353,22 +1367,22 @@ local OB_OK, OB_ERR = pcall(function ()
panel.foot:SetText("|cffffaa33calibrating " .. d .. "/" .. t .. ": waiting for arrivals|r")
end
else
local txt, stale = CalAgeText()
-- the footer only appears after a server restart, while a shown route is unseen since
local txt = ""
for i = 1, table.getn(ROUTES) do
if Visible(ROUTES[i]) and Stale(ROUTES[i]) then
txt = "server restarted " .. FormatSpan(time() - DB.restartAt) .. " ago: ? = not seen since"
stale = true
txt = "|cffffaa33server restarted " .. FormatSpan(time() - DB.restartAt) .. " ago: ? = not seen since|r"
break
end
end
panel.foot:SetText((stale and "|cffffaa33" or "|cff777777") .. txt .. "" .. "|r")
panel.foot:SetText(txt)
end
end
local function PlacePanel()
panel:ClearAllPoints()
if CFG.pos then
panel:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", CFG.pos.x, CFG.pos.y)
panel:SetPoint(CFG.pos.a or "BOTTOMLEFT", UIParent, "BOTTOMLEFT", CFG.pos.x, CFG.pos.y)
else
panel:SetPoint("TOPRIGHT", anchorTo, "BOTTOMRIGHT", 0, -8)
end
@@ -1424,11 +1438,14 @@ local OB_OK, OB_ERR = pcall(function ()
ApplyVisibility()
end
btn:SetScript("OnClick", function() ToggleShown() end)
btn:SetScript("OnClick", function()
if arg1 == "RightButton" then OctoBus_Options() else ToggleShown() end
end)
btn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:AddLine("Transports", 0.2, 1, 0.8)
GameTooltip:AddLine("Click: " .. (panel:IsShown() and "hide" or "show") .. " the window (works anywhere)", 1, 1, 1)
GameTooltip:AddLine("Right-click: settings", 1, 1, 1)
GameTooltip:AddLine("Opens by itself at either end of any route.", 0.7, 0.7, 0.7)
GameTooltip:AddLine("Drag: move this icon around the minimap", 0.7, 0.7, 0.7)
GameTooltip:Show()
@@ -1611,6 +1628,9 @@ local OB_OK, OB_ERR = pcall(function ()
elseif cmd == "boat" then
CFG.hideBoat = not CFG.hideBoat
Say("Sparkwater Port boat " .. (CFG.hideBoat and "hidden" or "shown") .. ".")
elseif cmd == "options" or cmd == "config" or cmd == "settings" then
OctoBus_Options()
elseif cmd == "lock" then
Toggle("window locked", "locked")
if CFG.locked then grip:Hide() else grip:Show() end
@@ -1735,10 +1755,29 @@ local OB_OK, OB_ERR = pcall(function ()
if recOn then Say("recording: on, " .. table.getn(DB.rec) .. " lines so far.") end
else
Say("/ob (window on/off) | list | alert <s> | all|sound|chat|flash|zone|boat|lock | scale <n> | where | shape <square|round|auto> | allow|disallow <name> | resetpos | name <n> <text> | period <n> <s> | travel <s> | calibrate | reset | debug [on|off]")
Say("/ob (window on/off) | options | list | alert <s> | all|sound|chat|flash|zone|boat|lock | scale <n> | where | shape <square|round|auto> | allow|disallow <name> | resetpos | name <n> <text> | period <n> <s> | travel <s> | calibrate | reset | debug [on|off]")
end
end
-- for the settings window (OctoBusOptions.lua)
OctoBus = {
cfg = CFG,
ApplyLook = function() ApplyLook() Refresh() end,
Refresh = function() Refresh() end,
Show = function() override = "show" ApplyVisibility() end,
SetAnchor = function(a)
CFG.anchor = a
if panel:GetLeft() then SavePos() end -- keeps the window where it is, pinned by the new corner
end,
SetScale = function(v) ApplyScale(v) end,
scaleMin = SCALE_MIN,
scaleMax = SCALE_MAX,
SetLocked = function(v)
CFG.locked = v and true or false
if CFG.locked then grip:Hide() else grip:Show() end
end,
}
end)
if not OB_OK then
OB_STAGE = "start-up error: " .. tostring(OB_ERR)
+1
View File
@@ -6,3 +6,4 @@
## SavedVariables: OctoBusDB
OctoBus.lua
OctoBusOptions.lua
+170
View File
@@ -0,0 +1,170 @@
--[[
OctoBus settings window: /ob options, or right-click the minimap icon.
Changes apply at once and are saved with the rest of OctoBus's settings.
]]
local font = STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF"
local W = 300
local frame, controls = nil, {}
local loading = false
local function Backdrop(f)
f:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
f:SetBackdropColor(0.02, 0.02, 0.02, 0.92)
f:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
end
local function Heading(text, y)
local fs = frame:CreateFontString(nil, "OVERLAY")
fs:SetFont(font, 11, "OUTLINE")
fs:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, y)
fs:SetText("|cff33ffcc" .. text .. "|r")
end
local sliderCount = 0
-- get() returns the setting's current value; set(v) applies it; fmt(v) is the label text
local function Slider(label, y, lo, hi, step, get, set, fmt)
sliderCount = sliderCount + 1
local name = "OctoBusOptionsSlider" .. sliderCount
local s = CreateFrame("Slider", name, frame, "OptionsSliderTemplate")
s:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, y)
s:SetWidth(W - 36)
s:SetMinMaxValues(lo, hi)
s:SetValueStep(step)
getglobal(name .. "Low"):SetText("")
getglobal(name .. "High"):SetText("")
local text = getglobal(name .. "Text")
text:SetFont(font, 10)
s.Update = function()
loading = true
s:SetValue(get())
loading = false
text:SetText(label .. ": |cffffffff" .. fmt(get()) .. "|r")
end
s:SetScript("OnValueChanged", function()
if loading then return end
local v = math.floor(this:GetValue() / step + 0.5) * step
set(v)
text:SetText(label .. ": |cffffffff" .. fmt(v) .. "|r")
end)
table.insert(controls, s)
return s
end
local checkCount = 0
local function Check(label, x, y, get, set)
checkCount = checkCount + 1
local name = "OctoBusOptionsCheck" .. checkCount
local c = CreateFrame("CheckButton", name, frame, "OptionsCheckButtonTemplate")
c:SetWidth(24)
c:SetHeight(24)
c:SetPoint("TOPLEFT", frame, "TOPLEFT", x, y)
local text = getglobal(name .. "Text")
text:SetFont(font, 10)
text:SetText(label)
c.Update = function() c:SetChecked(get() and 1 or nil) end
c:SetScript("OnClick", function() set(this:GetChecked() and true or false) end)
table.insert(controls, c)
return c
end
local function pct(v) return math.floor(v * 100 + 0.5) .. "%" end
local function Build()
local OB = OctoBus
local C = OB.cfg
frame = CreateFrame("Frame", "OctoBusOptions", UIParent)
frame:SetWidth(W)
frame:SetHeight(538)
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 60)
frame:SetFrameStrata("DIALOG")
frame:SetClampedToScreen(true)
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function() this:StartMoving() end)
frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
Backdrop(frame)
table.insert(UISpecialFrames, "OctoBusOptions") -- Escape closes it
local title = frame:CreateFontString(nil, "OVERLAY")
title:SetFont(font, 12, "OUTLINE")
title:SetPoint("TOP", frame, "TOP", 0, -10)
title:SetText("|cff33ffccOcto|rBus settings")
local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 2, 2)
local function look(key) return function(v) C[key] = v OB.ApplyLook() end end
Heading("Window", -34)
Slider("Background opacity", -64, 0, 1, 0.05, function() return C.bgAlpha or 0.75 end, look("bgAlpha"), pct)
Slider("Window opacity", -102, 0.2, 1, 0.05, function() return C.alpha or 1 end, look("alpha"), pct)
Slider("Text size", -140, 0.7, 1.5, 0.05, function() return C.textScale or 1 end, look("textScale"), pct)
Slider("Window size", -178, OB.scaleMin, OB.scaleMax, 0.05, function() return C.scale or 1 end,
function(v) OB.SetScale(v) end, pct)
Check("Lock position and size", 14, -204, function() return C.locked end, function(v) OB.SetLocked(v) end)
-- which corner stays in place when the list gets longer or shorter
local corner = frame:CreateFontString(nil, "OVERLAY")
corner:SetFont(font, 10)
corner:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -234)
corner:SetText("Corner that stays put as the list grows or shrinks:")
corner:SetTextColor(1, 0.82, 0)
local CORNERS = {
{ "TOPLEFT", "Top left", 14, -248 }, { "TOPRIGHT", "Top right", 150, -248 },
{ "BOTTOMLEFT", "Bottom left", 14, -272 }, { "BOTTOMRIGHT", "Bottom right", 150, -272 },
}
for i = 1, table.getn(CORNERS) do
local a = CORNERS[i][1]
Check(CORNERS[i][2], CORNERS[i][3], CORNERS[i][4], function() return (C.anchor or "BOTTOMLEFT") == a end,
function()
OB.SetAnchor(a)
for j = 1, table.getn(controls) do controls[j].Update() end -- one corner at a time
end)
end
Heading("Routes", -302)
Check("Open by itself only at route ends", 14, -320, function() return C.zoneOnly end,
function(v) C.zoneOnly = v end)
Check("Show all routes (not just for where you are)", 14, -344, function() return C.full end,
function(v) C.full = v OB.Refresh() end)
Check("Show the Sparkwater Port boat", 14, -368, function() return not C.hideBoat end,
function(v) C.hideBoat = not v OB.Refresh() end)
Heading("Alerts (click a row to put a bell on it)", -400)
Slider("Warn before arrival", -430, 10, 120, 5, function() return C.alert or 45 end,
function(v) C.alert = v end, function(v) return math.floor(v + 0.5) .. " s" end)
Check("Chat message", 14, -452, function() return C.chat end, function(v) C.chat = v end)
Check("Sound", 120, -452, function() return C.sound end, function(v) C.sound = v end)
Check("Flash the minimap icon", 14, -476, function() return C.flash end, function(v) C.flash = v end)
local reset = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
reset:SetWidth(150)
reset:SetHeight(20)
reset:SetPoint("BOTTOM", frame, "BOTTOM", 0, 12)
reset:SetText("Reset window look")
reset:SetScript("OnClick", function()
C.bgAlpha, C.alpha, C.textScale = nil, nil, nil
OB.SetScale(1)
OB.ApplyLook()
for i = 1, table.getn(controls) do controls[i].Update() end
end)
end
function OctoBus_Options()
if not OctoBus then
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccOctoBus:|r not running, so its settings are unavailable.")
return
end
if not frame then Build() end
if frame:IsShown() then frame:Hide() return end
for i = 1, table.getn(controls) do controls[i].Update() end
OctoBus.Show() -- show the timer window so changes can be seen
frame:Show()
end
+3 -1
View File
@@ -24,7 +24,7 @@ OctoBus needs one extra file, `ZepSense.dll`, which you install by hand in the g
| Row order | Soonest first: whatever docks, leaves or needs you to fly next is at the top. Rows move as the timers change. |
| Bell on a row | Alerts are on for that route (click a row to toggle). |
| Title | Click it to switch between the routes for where you are and all routes (`Transports - all`). |
| Footer | The route whose data is oldest, e.g. `oldest data: Orgrimmar -> Undercity, seen 3 h ago`. It turns orange after 5 days, as a hint to go and see it again. |
| Footer | Only shown after a server restart, while a route in the window has not been seen since: `server restarted 20 min ago: ? = not seen since`. |
| Dots, bottom-right corner | Drag to make the window bigger or smaller. |
| Hover over a row | Cycle, stay at the dock, how the far end was measured, and what left- and right-click do. |
@@ -81,6 +81,7 @@ Saved data lives in `WTF\...\SavedVariables\OctoBus.lua`.
- **Auto-show**: the window opens by itself at either end of any route (the zones in the table above), and while a tracked transport is in view, such as on a crossing. It hides elsewhere. The icon toggle always works manually. Add more auto-show places with `/ob allow <zone or sub-zone>` (capitals do not matter; `/ob where` shows the names), remove one with `/ob disallow <name>`, or turn the behaviour off with `/ob zone`.
- **Bells**: click a row to toggle a bell. Belled routes print a chat message `/ob alert` seconds (default 45) before the transport docks at the row's start, e.g. `Orgrimmar -> Undercity: docks at Orgrimmar in 0:45`. Nothing is printed to chat for routes without a bell.
- **Reverse a row**: right-click it.
- **Settings window**: `/ob options`, or right-click the minimap icon. Background opacity, window opacity, text size, window size, lock, and which corner stays put as the list grows or shrinks, plus auto-show, which routes to show, and bell alerts. Changes apply at once.
- **Move and size the window**: drag it to move it; drag the dots in the bottom-right corner (or use `/ob scale`) to resize it. `/ob lock` locks position and size; `/ob resetpos` puts the window and icon back at their default place and size.
## How the timing works
@@ -108,6 +109,7 @@ The Ratchet boat cannot be seen from Orgrimmar, so its row is always an estimate
| Command | What it does |
|---|---|
| `/ob` | Show / hide the window (works anywhere) |
| `/ob options` | Settings window (also: right-click the minimap icon) |
| `/ob list` | Print all timers in chat |
| `/ob alert <sec>` | Bell alerts fire this many seconds before arrival (default 45, `0` = off) |
| `/ob chat`, `/ob sound`, `/ob flash` | Toggle that alert type (chat on by default; sound and screen flash off) |