v1.2: start -> end rows, routes for where you are, both ends timed

- Rows read 'start -> end' with the timer for the start; right-click
  reverses a row
- Default view shows only routes with an end in your zone, already
  facing your end; click the title or /ob all for every route
- New route: Grom'gol <-> Undercity zeppelin
- Far-end timings built in for every route, and learned automatically
  when you ride (no recording needed)
- Window auto-shows at either end of any route
- Built-in cycles from ~5 h of sightings
- Fixes: far-end learning while recording; baseline ignored when the
  first sighting was a departure
- Add CHANGELOG.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyQKCL92ZLwPi9qmYQtNdc
This commit is contained in:
Phridgey
2026-10-08 16:11:45 -04:00
co-authored by Claude Opus 5.5
parent 720534e06a
commit 11e9ccae55
4 changed files with 352 additions and 89 deletions
+38
View File
@@ -0,0 +1,38 @@
# Changelog
## 1.2 (8 October 2026)
### New
- **Rows read "start -> end"**, for example `Orgrimmar -> Undercity`, and the timer is for the start. The separate "zeppelin" / "boat" label is gone from the rows (it is still in the tooltip).
- **Right-click a row to reverse it**, for example `Undercity -> Orgrimmar`, timed at Undercity.
- **Routes for where you are.** By default the window only shows routes with an end in your zone, each already reversed to start from your end. Click the window title (or `/ob all`) to switch to all routes. Somewhere with no routes, opening the window by hand shows the last place's routes.
- **New route: Grom'gol <-> Undercity zeppelin.** Its cycle is worked out from earlier sightings; its Undercity end is learned on the first ride from Grom'gol.
- **Both ends of every route are timed.** Built-in far-end timings measured on rides: Undercity, Grom'gol, Kargath, Thunder Bluff, Revantusk and Booty Bay.
- **Far ends are learned by riding**, with no recording needed: a transport that stands still for 20 s away from its home dock is treated as docked at the far end.
- **The window opens by itself at either end of any route**, and while a tracked transport is in view (for example during a crossing).
- Built-in cycle lengths now come from about 5 hours of sightings, so a new copy's first sightings line up with a long baseline.
### Fixed
- Far-end learning failed while `/ob debug on` was recording.
- The built-in baseline was ignored when the first thing seen in a session was a transport leaving rather than arriving.
### Changed
- `/ob name` renames the far end of a route.
- `/ob zone` now means "auto-show only at route ends".
## 1.1 (8 October 2026)
### New
- **Ratchet <-> Booty Bay boat**, shown in Orgrimmar as when to fly (`fly 4:55 sails 6:40`) to make the next sailing. `/ob travel <seconds>` sets your travel time to the Ratchet dock (default 175 s).
- **Resizable window**: drag the dots in the bottom-right corner, or `/ob scale <0.6 to 2>`.
- **Footer names the route with the oldest data** and how long ago it was seen.
- **`/ob disallow <name>`** removes a place from the auto-show list. Place names now match regardless of capitals or quotes.
- **`/ob debug on` / `off`** records transport positions for mapping new routes. `REC` shows in the window while it runs.
### Changed
- A single sighting that does not fit the schedule no longer resets a route's history; a second agreeing sighting is needed.
- The addon is at the top of the repository, so it installs correctly from a launcher's git address. The DLL is a separate download.
## 1.0 (8 October 2026)
- First release, as TurtleTransit, renamed OctoBus the same day: arrival timers for the Orgrimmar zeppelins and the Sparkwater boat, with bells, a minimap icon and automatic calibration from sightings.
+275 -77
View File
@@ -1,9 +1,11 @@
--[[
OctoBus
-------
Arrival timers for the zeppelins at the Orgrimmar towers (plus the Sparkwater Port boat).
A small icon on the minimap edge shows / hides a timer window; the window opens by itself
in Orgrimmar and Durotar.
Arrival timers for the Horde zeppelins and boats: Orgrimmar to Undercity, Grom'gol, Kargath
and Thunder Bluff, Grom'gol to Undercity, Sparkwater to Revantusk, and Ratchet to Booty Bay.
Each row reads "start -> end" and times the transport at the start; right-click reverses
it. The window shows the routes for where you are (click its title for all of them) and
opens by itself at either end of any route. A small minimap icon shows / hides it.
HOW IT WORKS
Needs ZepSense.dll (listed in dlls.txt). The DLL adds one function to the client,
@@ -23,14 +25,15 @@
/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
/ob zone toggle "auto-show only in Orgrimmar / Durotar"
/ob zone toggle "auto-show only at route ends" (off = always show)
/ob all all routes / only routes for where you are (or click the title)
/ob boat show / hide the Sparkwater Port boat
/ob scale <n> window size, 0.6 to 2 (or drag the dots in the bottom-right corner)
/ob shape square|round|auto minimap icon placement (auto looks at your minimap)
/ob where print your zone / sub-zone and whether the window auto-shows
/ob allow <name> also auto-show in this zone or sub-zone
/ob disallow <name> take a place off that list
/ob name <n> <text> rename route number n (from /ob list)
/ob name <n> <text> rename the far end of route number n (from /ob list)
/ob period <n> <sec> set a route's cycle length by hand
/ob calibrate optional: watch every transport on purpose (stand between the towers).
Not required: any arrival you see near the towers calibrates automatically.
@@ -59,20 +62,55 @@ local OB_OK, OB_ERR = pcall(function ()
-- period/anchor are starting values measured from a 25 minute log; anchor is the
-- time() of one observed arrival. Real observations replace them automatically.
------------------------------------------------------------------------
-- Zones (or sub-zones) of each end, for the "where you are" view: homeZ = the home end,
-- awayZ = the far end, planZ = places where the route is shown for planning (Ratchet from
-- Orgrimmar, as "fly").
local ORG = { "Orgrimmar", "Durotar" }
local ROUTES = {
{ entry = 164871, name = "Undercity", kind = "zeppelin", x = 1318.1, y = -4658.0, period = 356.3, dwell = 63, anchor = 1791422404 },
{ entry = 175080, name = "Grom'gol", kind = "zeppelin", x = 1360.8, y = -4631.3, period = 303.5, dwell = 63, anchor = 1791422441 },
{ entry = 190552, name = "Kargath", kind = "zeppelin", x = 1206.1, y = -4158.7, period = 374.0, dwell = 63, anchor = 1791422484 },
{ entry = 190549, name = "Thunder Bluff", kind = "zeppelin", x = 1125.4, y = -4135.0, period = 567.0, dwell = 63, anchor = 1791422131 },
{ entry = 190550, name = "Sparkwater", kind = "boat", x = 876.1, y = -5204.1, period = 244.75, dwell = 43, anchor = 1791422323, boat = true },
-- Built-in starting values from Phridgey's sightings on 8 Oct 2026: cycle measured over
-- ~5 h (origin = first sighting, anchor = latest), stay at the dock, and where measured
-- on a ride, the far end (flipOffset = seconds after docking here that it docks there).
-- A new copy refines all of this from its own sightings.
{ entry = 164871, homeZ = ORG, awayZ = { "Tirisfal Glades", "Undercity" },
name = "Undercity", kind = "zeppelin", x = 1318.1, y = -4658.0,
period = 356.286, dwell = 64.0, origin = 1791471571.34, anchor = 1791489385.64,
flipFrom = "Undercity", flipTo = "Orgrimmar", flipOffset = 191.1, flipDwell = 64.0 },
{ entry = 175080, homeZ = ORG, awayZ = { "Stranglethorn Vale" },
name = "Grom'gol", kind = "zeppelin", x = 1360.8, y = -4631.3,
period = 303.458, dwell = 64.1, origin = 1791471600.26, anchor = 1791489504.28,
flipFrom = "Grom'gol", flipTo = "Orgrimmar", flipOffset = 138.2, flipDwell = 64.1 },
{ entry = 190552, homeZ = ORG, awayZ = { "Badlands" },
name = "Kargath", kind = "zeppelin", x = 1206.1, y = -4158.7,
period = 374.073, dwell = 63.9, origin = 1791471857.57, anchor = 1791489438.99,
flipFrom = "Kargath", flipTo = "Orgrimmar", flipOffset = 198.0, flipDwell = 64.2 },
{ entry = 190549, homeZ = ORG, awayZ = { "Mulgore", "Thunder Bluff" },
name = "Thunder Bluff", kind = "zeppelin", x = 1125.4, y = -4135.0,
period = 566.785, dwell = 64.2, origin = 1791472003.14, anchor = 1791490140.28,
flipFrom = "Thunder Bluff", flipTo = "Orgrimmar", flipOffset = 275.8, flipDwell = 64.0 },
{ entry = 190550, homeZ = ORG, awayZ = { "The Hinterlands" },
name = "Sparkwater", kind = "boat", x = 876.1, y = -5204.1, boat = true,
period = 245.030, dwell = 44.1, origin = 1791471573.69, anchor = 1791489460.91,
flipFrom = "Revantusk", flipTo = "Sparkwater", flipOffset = 123.2, flipDwell = 44.0 },
-- Grom'gol <-> Undercity zeppelin, timed at the Grom'gol dock (the only end measured).
-- Seen leaving Grom'gol 1791480459.89 and arriving 1791486390.25: 18 cycles of 333.02 s
-- (assuming the usual 64 s stay); an earlier glimpse docked fits too. The Undercity end
-- is learned on the first ride from Grom'gol.
{ entry = 176495, homeZ = { "Stranglethorn Vale" }, awayZ = { "Tirisfal Glades", "Undercity" },
name = "Undercity", kind = "zeppelin", x = -12406.9, y = 211.8,
period = 333.02, dwell = 64.0, origin = 1791480395.89, anchor = 1791486390.25,
flipFrom = "Undercity", flipTo = "Grom'gol" },
-- Ratchet -> Booty Bay boat, timed at the Ratchet dock. Never visible from Orgrimmar, so
-- the row shows when to fly out ("fly") rather than when it docks. Measured on two rides
-- (8 Oct 2026): Ratchet arrivals 1791479926.67 and 1791480654.81 are 2 cycles apart
-- (364.07 s; legs 64 s dock + 114.8 s crossing + 64.2 s Booty Bay + 121.1 s back).
-- origin seeds the baseline so later arrivals keep refining the cycle.
{ entry = 20808, name = "Ratchet", kind = "boat", x = -1005.6, y = -3841.6, period = 364.07,
dwell = 64, anchor = 1791480654.81, origin = 1791479926.67, far = true },
-- the row shows when to fly out ("fly") rather than when it docks. Ratchet arrivals
-- 1791479926.67 .. 1791490117.34 are 28 cycles apart (363.95 s). Booty Bay end seen on
-- two days' rides: docks 178.6 s after Ratchet, stays 64.2 s.
{ entry = 20808, homeZ = { "Ratchet" }, awayZ = { "Stranglethorn Vale" }, planZ = ORG,
name = "Ratchet", kind = "boat", x = -1005.6, y = -3841.6, far = true,
period = 363.952, dwell = 63.7, origin = 1791479926.67, anchor = 1791490117.34,
flipFrom = "Booty Bay", flipTo = "Ratchet", flipOffset = 178.6, flipDwell = 64.2 },
}
-- Right-click on a row reverses it: times for the far end (flipFrom, going to flipTo). Where the other end was never measured (flipOffset unset), it is assumed to
-- dock halfway through the cycle and to stay as long as at this end: a rough estimate.
local DOCK_RADIUS = 2.0
------------------------------------------------------------------------
@@ -98,6 +136,9 @@ local OB_OK, OB_ERR = pcall(function ()
t.origin = t.origin or {}
t.dwell = t.dwell or {}
t.lastObs = t.lastObs or {}
t.flip = t.flip or {}
t.farOff = t.farOff or {}
t.farDwell = t.farDwell or {}
t.cfg = t.cfg or {}
local c = t.cfg
if c.alert == nil then c.alert = 45 end
@@ -139,14 +180,66 @@ local OB_OK, OB_ERR = pcall(function ()
return string.format("%d:%02d", math.floor(s / 60), math.mod(s, 60))
end
-- "<place> <kind>": the place can be renamed with /ob name; in the window the kind is
-- shown in the same grey as the "docks" / "leaves" labels.
local function RouteName(r) return (DB.names[r.entry] or r.name) .. " " .. r.kind end
local function RowName(r) return (DB.names[r.entry] or r.name) .. " |cffaaaaaa" .. r.kind .. "|r" end
-- Rows read "start -> end". flipTo is the home end (Orgrimmar, Sparkwater, Ratchet),
-- flipFrom the far end; /ob name renames the far end.
local function Away(r) return DB.names[r.entry] or r.flipFrom end
local function RouteName(r) return r.flipTo .. " -> " .. Away(r) end
local function Period(r) return DB.period[r.entry] or r.period end
local function Anchor(r) return DB.anchor[r.entry] or r.anchor end
local function Watched(r) return DB.bell[r.entry] == true end
local function Visible(r) return (not r.boat) or (not CFG.hideBoat) end
-- Names are compared without capitals, quotes or outer spaces.
local function Norm(n)
n = string.lower(n or "")
n = string.gsub(n, "[\"']", "")
n = string.gsub(n, "^%s+", "")
n = string.gsub(n, "%s+$", "")
return n
end
local seenState = {} -- entry -> { atDock = bool, arrivedAt = epoch or nil }
-- Where the player is. LOC.z / LOC.sz = current zone / sub-zone; LOC.mz / LOC.msz = the
-- place the "where you are" view is built for: the current one, or the last place that
-- had routes (CFG.hub) when you are somewhere without any.
local LOC = { z = "", sz = "", mz = "", msz = "", key = nil }
local manualFlip = {} -- right-clicks in the "where you are" view; cleared on zone change
local function InListAt(t, z, sz)
if not t then return false end
for i = 1, table.getn(t) do
local a = Norm(t[i])
if a == z or a == sz then return true end
end
return false
end
local function AtHome(r) return InListAt(r.homeZ, LOC.mz, LOC.msz) end
local function AtAway(r) return InListAt(r.awayZ, LOC.mz, LOC.msz) end
local function AtPlan(r) return InListAt(r.planZ, LOC.mz, LOC.msz) end
local function UpdateLoc()
local z, sz = Norm(GetZoneText()), Norm(GetSubZoneText())
if z ~= LOC.key then LOC.key = z; manualFlip = {} end
LOC.z, LOC.sz = z, sz
local any = false
for i = 1, table.getn(ROUTES) do
local r = ROUTES[i]
if InListAt(r.homeZ, z, sz) or InListAt(r.awayZ, z, sz) or InListAt(r.planZ, z, sz) then any = true end
end
if any then
LOC.mz, LOC.msz = z, sz
CFG.hub = { z = z, sz = sz }
elseif CFG.hub then
LOC.mz, LOC.msz = CFG.hub.z, CFG.hub.sz
else
LOC.mz, LOC.msz = z, sz
end
end
-- Shown rows: "all routes" (CFG.full) or only the ones for where you are (either end,
-- a planning place, or the transport in view, e.g. while riding it).
local function Visible(r)
if r.boat and CFG.hideBoat then return false end
if CFG.full then return true end
return AtHome(r) or AtAway(r) or AtPlan(r) or seenState[r.entry] ~= nil
end
-- confirmed[entry] = true once the transport has been SEEN docking/leaving this session
local confirmed = {}
@@ -166,13 +259,38 @@ local OB_OK, OB_ERR = pcall(function ()
local function Dwell(r) return DB.dwell[r.entry] or r.dwell end
-- returns "docked", secondsUntilDeparture or "wait", secondsUntilArrival
local function Predict(r)
-- Reversed? "All routes": as set with right-click (kept). "Where you are": starts from
-- your end automatically; a right-click reverses it until you change zone.
local function Flipped(r)
if not r.flipFrom then return false end
if CFG.full then return DB.flip[r.entry] == true end
local auto = AtAway(r) and not AtHome(r) and not AtPlan(r)
if manualFlip[r.entry] then return not auto end
return auto
end
local function FlipName(r) return Away(r) .. " -> " .. r.flipTo end
local function ShownName(r) if Flipped(r) then return FlipName(r) end return RouteName(r) end
local function StartName(r) if Flipped(r) then return Away(r) end return r.flipTo end
local RowName = ShownName
-- Far end timing: learned on a ride (DB.farOff / DB.farDwell), else built in, else the
-- rough "halfway through the cycle, same stay" assumption. FarKnown = not a guess.
local function FarKnown(r) return (DB.farOff[r.entry] or r.flipOffset) ~= nil end
local function FarOff(r, P) return DB.farOff[r.entry] or r.flipOffset or P / 2 end
local function FarDwell(r) return DB.farDwell[r.entry] or r.flipDwell or Dwell(r) end
-- otherEnd = true: the same schedule seen from the far end of the route
local function Predict(r, otherEnd)
local a, P = Anchor(r), Period(r)
if not a or not P or P <= 0 then return nil end
local d = Dwell(r)
if otherEnd then
a = a + FarOff(r, P)
d = FarDwell(r)
end
local e = EpochNow() - a
local ph = math.mod(e, P)
if ph < 0 then ph = ph + P end
local d = Dwell(r)
if ph < d then return "docked", d - ph end
return "wait", P - ph
end
@@ -278,7 +396,6 @@ local OB_OK, OB_ERR = pcall(function ()
------------------------------------------------------------------------
-- Watching the DLL
------------------------------------------------------------------------
local seenState = {} -- entry -> { atDock = bool, arrivedAt = epoch or nil }
local dllOk = false
local lastGood = 0
@@ -344,6 +461,7 @@ local OB_OK, OB_ERR = pcall(function ()
end
else
-- never saw it arrive, but it leaves 'dwell' seconds after arriving
if not DB.origin[e] and r.origin then DB.origin[e] = r.origin end
DB.anchor[e] = t - Dwell(r)
if not DB.origin[e] then
DB.origin[e] = t - Dwell(r)
@@ -392,6 +510,66 @@ local OB_OK, OB_ERR = pcall(function ()
end
end
------------------------------------------------------------------------
-- Far-end learning. A tracked transport that stands still for 20 s somewhere other than
-- its home dock is docked at the far end. Its arrival there (when it first came within
-- DOCK_RADIUS of where it stopped) is stored as seconds into the cycle, and its stay is
-- stored when it leaves. Only learned when the home-end schedule was confirmed this
-- session, so the offset is measured against a fresh schedule. Nothing is printed.
------------------------------------------------------------------------
local FAR_STILL = 20
local farW = {} -- entry -> { hist = {{t,x,y}...}, sx, sy, st, arr }
local function FarWatch(r, now, x, y, atDock)
local e = r.entry
if not r.flipFrom or atDock or (x == 0 and y == 0) then farW[e] = nil; return end
local w = farW[e]
if not w then w = { hist = {} }; farW[e] = w end
-- keep only samples where it moved, so the approach is still in the history after
-- 20 s standing still (polling is 10 per second while recording)
local hn = table.getn(w.hist)
local last = w.hist[hn]
if not last or math.abs(last[2] - x) >= 0.05 or math.abs(last[3] - y) >= 0.05 then
table.insert(w.hist, { now, x, y })
if hn + 1 > 120 then table.remove(w.hist, 1) end
end
if w.sx and math.abs(x - w.sx) < 0.3 and math.abs(y - w.sy) < 0.3 then
if not w.arr and (now - w.st) >= FAR_STILL then
-- docked: arrival = first recent sample within DOCK_RADIUS of the stop point
local arr, approached = w.st, false
for i = table.getn(w.hist), 1, -1 do
local h = w.hist[i]
local dx, dy = h[2] - w.sx, h[3] - w.sy
if math.sqrt(dx * dx + dy * dy) > DOCK_RADIUS then approached = true; break end
arr = h[1]
end
w.arr = arr
local P = Period(r)
-- only when it was seen coming in (not already docked when it came into view)
if approached and confirmed[e] and P and P > 0 then
local off = math.mod(arr - Anchor(r), P)
if off < 0 then off = off + P end
local old = DB.farOff[e]
if old and math.abs(old - off) < 20 then off = old * 0.5 + off * 0.5 end
DB.farOff[e] = off
w.learn = true
end
end
return
end
if w.arr and w.sx then
local dx, dy = x - w.sx, y - w.sy
if math.sqrt(dx * dx + dy * dy) <= DOCK_RADIUS then return end -- still easing out
local d = now - w.arr
if w.learn and d > 20 and d < 150 then
local old = DB.farDwell[e]
DB.farDwell[e] = old and (old * 0.6 + d * 0.4) or d
end
w.arr = nil
w.learn = nil
end
w.sx, w.sy, w.st = x, y, now
end
local function Poll()
if type(ZepTransports) ~= "function" then dllOk = false; return end
local ok, str = pcall(ZepTransports)
@@ -414,6 +592,7 @@ local OB_OK, OB_ERR = pcall(function ()
present[de] = true
local dx, dy = x - r.x, y - r.y
local atDock = (math.sqrt(dx * dx + dy * dy) <= DOCK_RADIUS)
FarWatch(r, now, x, y, atDock)
local st = seenState[de]
if not st then
seenState[de] = { atDock = atDock }
@@ -437,6 +616,7 @@ local OB_OK, OB_ERR = pcall(function ()
for i = 1, table.getn(ROUTES) do
local e = ROUTES[i].entry
if seenState[e] and not present[e] then seenState[e] = nil end
if not present[e] then farW[e] = nil end
end
end
@@ -567,6 +747,24 @@ local OB_OK, OB_ERR = pcall(function ()
panel:SetScript("OnDragStart", StartDrag)
panel:SetScript("OnDragStop", StopDrag)
-- click the title: switch between all routes and routes for where you are
local titleBtn = CreateFrame("Button", nil, panel)
titleBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", 4, -2)
titleBtn:SetWidth(150)
titleBtn:SetHeight(16)
titleBtn:RegisterForClicks("LeftButtonUp")
titleBtn:RegisterForDrag("LeftButton")
titleBtn:SetScript("OnDragStart", StartDrag)
titleBtn:SetScript("OnDragStop", StopDrag)
titleBtn:SetScript("OnClick", function() CFG.full = not CFG.full end)
titleBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:AddLine(CFG.full and "Showing all routes" or "Showing routes for where you are", 1, 1, 1)
GameTooltip:AddLine("Click: " .. (CFG.full and "only routes for where you are" or "all routes") .. " (/ob all)", 0.2, 1, 0.8)
GameTooltip:Show()
end)
titleBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
-- Resize grip, bottom-right: drag to scale the whole window (0.6x to 2x). The top-left
-- corner stays where it is. Hidden while the window is locked.
local SCALE_MIN, SCALE_MAX = 0.6, 2.0
@@ -638,7 +836,7 @@ local OB_OK, OB_ERR = pcall(function ()
row:SetHeight(ROW_H)
row:SetPoint("TOPLEFT", panel, "TOPLEFT", 4, -(TOP + (i - 1) * ROW_H))
row:SetPoint("RIGHT", panel, "RIGHT", -4, 0)
row:RegisterForClicks("LeftButtonUp")
row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
row:RegisterForDrag("LeftButton")
row:SetScript("OnDragStart", StartDrag)
row:SetScript("OnDragStop", StopDrag)
@@ -658,9 +856,6 @@ local OB_OK, OB_ERR = pcall(function ()
row.name:SetPoint("LEFT", row.icon, "RIGHT", 4, 0)
row.name:SetJustifyH("LEFT")
row.kind = row:CreateFontString(nil, "OVERLAY")
row.kind:SetFont(font, 10, "OUTLINE")
row.kind:SetTextColor(0.667, 0.667, 0.667)
row.time = row:CreateFontString(nil, "OVERLAY")
row.time:SetFont(font, 10, "OUTLINE")
@@ -675,15 +870,25 @@ local OB_OK, OB_ERR = pcall(function ()
row.bell:Hide()
row:SetScript("OnClick", function()
if not this.route then return end
DB.bell[this.route.entry] = (not Watched(this.route)) or nil
if Watched(this.route) then this.bell:Show() else this.bell:Hide() end
local r = this.route
if not r then return end
if arg1 == "RightButton" then
if r.flipFrom then
if CFG.full then DB.flip[r.entry] = (not Flipped(r)) or nil
else manualFlip[r.entry] = (not manualFlip[r.entry]) or nil end
local f = this:GetScript("OnEnter")
if f and MouseIsOver and MouseIsOver(this) then GameTooltip:Hide(); f() end
end
return
end
DB.bell[r.entry] = (not Watched(r)) or nil
if Watched(r) then this.bell:Show() else this.bell:Hide() end
end)
row:SetScript("OnEnter", function()
local r = this.route
if not r then return end
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:AddLine(RouteName(r), 1, 1, 1)
GameTooltip:AddLine(ShownName(r) .. " " .. r.kind, 1, 1, 1)
GameTooltip:AddLine((Period(r) and ("Cycle " .. string.format("%.1f", Period(r)) .. "s, ") or "Cycle not measured, ") .. "stays " .. string.format("%.0f", Dwell(r)) .. "s (object " .. r.entry .. ")", 0.7, 0.7, 0.7)
if DB.origin[r.entry] and DB.anchor[r.entry] then
local span = DB.anchor[r.entry] - DB.origin[r.entry]
@@ -691,7 +896,15 @@ local OB_OK, OB_ERR = pcall(function ()
GameTooltip:AddLine("Cycle measured over " .. (span >= 86400 and string.format("%.1f days", span / 86400) or string.format("%.0f min", span / 60)) .. " of observations.", 0.7, 0.7, 0.7)
end
end
if r.far then
if Flipped(r) then
GameTooltip:AddLine("Times are for " .. Away(r) .. ": when it docks there and leaves for " .. r.flipTo .. ".", 0.7, 0.7, 0.7)
if FarKnown(r) then
GameTooltip:AddLine(Away(r) .. " end measured on a ride: docks there " .. string.format("%.0f", FarOff(r, Period(r))) .. "s into the cycle, stays " .. string.format("%.0f", FarDwell(r)) .. "s.", 0.7, 0.7, 0.7)
else
GameTooltip:AddLine(Away(r) .. " end not measured: assumed to dock halfway through the cycle, so treat it as rough. Ride it once (window open) to measure it.", 1, 0.6, 0.2)
end
GameTooltip:AddLine("Click: " .. (Watched(r) and "remove the bell (no chat alert)" or "add a bell: chat message " .. CFG.alert .. "s before it arrives at " .. Away(r)), 0.2, 1, 0.8)
elseif r.far then
local lo = DB.lastObs[r.entry] or DB.anchor[r.entry] or r.anchor
if r.periodGuess and not DB.period[r.entry] then
GameTooltip:AddLine("Cycle is a rough estimate until the boat is seen docking at Ratchet once more.", 1, 0.6, 0.2)
@@ -708,6 +921,9 @@ local OB_OK, OB_ERR = pcall(function ()
GameTooltip:AddLine(confirmed[r.entry] and "Seen this session: exact." or "Not seen this session: estimate (~).", 0.7, 0.7, 0.7)
GameTooltip:AddLine("Click: " .. (Watched(r) and "remove the bell (no chat alert)" or "add a bell: chat message " .. CFG.alert .. "s before it arrives"), 0.2, 1, 0.8)
end
if r.flipFrom then
GameTooltip:AddLine("Right-click: reverse to " .. (Flipped(r) and RouteName(r) or FlipName(r)), 0.2, 1, 0.8)
end
GameTooltip:Show()
end)
row:SetScript("OnLeave", function() GameTooltip:Hide() end)
@@ -717,6 +933,7 @@ local OB_OK, OB_ERR = pcall(function ()
-- returns state ("docked" / "wait" / nil), seconds (may be nil if unknown)
local function Status(r)
if Flipped(r) then return Predict(r, true) end
local ps, psecs = Predict(r)
local live = seenState[r.entry]
if live then
@@ -748,7 +965,7 @@ local OB_OK, OB_ERR = pcall(function ()
end
local function RowText(r)
if r.far and not seenState[r.entry] then
if r.far and not seenState[r.entry] and not Flipped(r) and not AtHome(r) then
local fly, sail = FlyBy(r)
if not fly then return RowName(r), nil, nil, "|cff888888not measured yet|r" end
local color = "|cffffffff"
@@ -758,8 +975,10 @@ local OB_OK, OB_ERR = pcall(function ()
end
local state, secs = Status(r)
local est = confirmed[r.entry] and "" or "|cff888888~|r"
local mark = ""
local nm = mark .. RowName(r)
local nm = RowName(r)
if Flipped(r) then
if not (confirmed[r.entry] and FarKnown(r)) then est = "|cff888888~|r" end
end
if state == "docked" then
if secs then
return nm, state, secs, est .. "|cff55ff55leaves|r |cffffffff" .. FormatTime(secs) .. "|r"
@@ -778,7 +997,6 @@ local OB_OK, OB_ERR = pcall(function ()
local function Refresh()
local n = 0
local kindL, kindR = nil, nil
for i = 1, table.getn(ROUTES) do
local r = ROUTES[i]
if Visible(r) then
@@ -787,7 +1005,7 @@ local OB_OK, OB_ERR = pcall(function ()
row:Show()
row.route = r
local _, state, secs, txt = RowText(r)
local nm = DB.names[r.entry] or r.name
local nm = ShownName(r)
if cal and cal.need[r.entry] then
if (cal.got[r.entry] or 0) >= cal.need[r.entry] then
nm = "|cff55ff55+|r " .. nm
@@ -797,14 +1015,6 @@ local OB_OK, OB_ERR = pcall(function ()
end
row.name:SetText(nm)
row.time:SetText(txt)
row.kind:SetText(r.kind)
local nw, tw = row.name:GetStringWidth(), row.time:GetStringWidth()
if type(nw) ~= "number" then nw = 60 end
if type(tw) ~= "number" then tw = 60 end
local nameRight = 2 + 14 + 4 + nw
local timeLeft = (WIDTH - 8) - 20 - tw
if not kindL or nameRight > kindL then kindL = nameRight end
if not kindR or timeLeft < kindR then kindR = timeLeft end
if Watched(r) then row.bell:Show() else row.bell:Hide() end
if state == "docked" then
row.icon:SetTexture(ICON_HERE)
@@ -821,17 +1031,9 @@ local OB_OK, OB_ERR = pcall(function ()
end
end
end
-- transport types in one column, centred between the longest place name and the
-- widest timer of all rows
if kindL and kindR then
local cx = math.floor((kindL + kindR) / 2 + 0.5)
for i = 1, n do
rows[i].kind:ClearAllPoints()
rows[i].kind:SetPoint("CENTER", rows[i], "LEFT", cx, 0)
end
end
for i = n + 1, table.getn(rows) do rows[i]:Hide() end
panel:SetHeight(TOP + n * ROW_H + 18)
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))
else
@@ -868,28 +1070,19 @@ local OB_OK, OB_ERR = pcall(function ()
end
end
-- Auto-show areas: Orgrimmar and Durotar (zone or sub-zone name), plus anything added with /ob allow.
-- Names are compared without capitals, quotes or outer spaces. The window also stays
-- open while a far route's transport (the Ratchet boat) is in view: on its dock and for
-- the whole crossing, which runs through zones that are not on the list.
local function Norm(n)
n = string.lower(n or "")
n = string.gsub(n, "[\"']", "")
n = string.gsub(n, "^%s+", "")
n = string.gsub(n, "%s+$", "")
return n
end
-- Auto-show: in any zone or sub-zone at either end of a route (or a planning place),
-- in a place added with /ob allow, or while a tracked transport is in view.
local function InOrg()
UpdateLoc()
if not CFG.zoneOnly then return true end
local z = Norm(GetZoneText())
local sz = Norm(GetSubZoneText())
if z == "orgrimmar" or sz == "orgrimmar" or z == "durotar" or sz == "durotar" then return true end
local z, sz = LOC.z, LOC.sz
for i = 1, table.getn(CFG.allow) do
local a = Norm(CFG.allow[i])
if a ~= "" and (z == a or sz == a) then return true end
end
for i = 1, table.getn(ROUTES) do
if ROUTES[i].far and seenState[ROUTES[i].entry] then return true end
local r = ROUTES[i]
if InListAt(r.homeZ, z, sz) or InListAt(r.awayZ, z, sz) or InListAt(r.planZ, z, sz) or seenState[r.entry] then return true end
end
return false
end
@@ -932,7 +1125,7 @@ local OB_OK, OB_ERR = pcall(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("Opens by itself in Orgrimmar and Durotar.", 0.7, 0.7, 0.7)
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()
end)
@@ -948,7 +1141,7 @@ local OB_OK, OB_ERR = pcall(function ()
local function RunAlerts()
for i = 1, table.getn(ROUTES) do
local r = ROUTES[i]
if Visible(r) and r.far then
if Visible(r) and r.far and not Flipped(r) then
local fly = FlyBy(r)
if fly and Watched(r) and CFG.alert > 0 and fly <= CFG.alert then
local stamp = math.floor(EpochNow() + fly + 0.5)
@@ -968,7 +1161,7 @@ local OB_OK, OB_ERR = pcall(function ()
if alerted[r.entry] ~= stamp then
alerted[r.entry] = stamp
if CFG.chat then
Say(RouteName(r) .. " arrives in " .. FormatTime(secs) .. ".")
Say(ShownName(r) .. ": docks at " .. StartName(r) .. " in " .. FormatTime(secs) .. ".")
end
if CFG.sound then PlaySound("RaidWarning") end
if CFG.flash then flashUntil = GetTime() + 8 end
@@ -1058,7 +1251,7 @@ local OB_OK, OB_ERR = pcall(function ()
elseif cmd == "sound" then Toggle("sound alert", "sound")
elseif cmd == "chat" then Toggle("chat alert", "chat")
elseif cmd == "flash" then Toggle("minimap icon flash", "flash")
elseif cmd == "zone" then Toggle("auto-show limited to Orgrimmar/Durotar (off = always show)", "zoneOnly")
elseif cmd == "zone" then Toggle("auto-show only at route ends (off = always show)", "zoneOnly")
elseif cmd == "where" then
Say("zone: \"" .. (GetZoneText() or "") .. "\" sub-zone: \"" .. (GetSubZoneText() or "") .. "\" auto-shows here: " .. tostring(InOrg()))
elseif cmd == "allow" then
@@ -1095,7 +1288,7 @@ local OB_OK, OB_ERR = pcall(function ()
elseif cmd == "allowclear" then
CFG.allow = {}
Say("extra places cleared (Orgrimmar and Durotar remain).")
Say("extra places cleared (route ends still open the window).")
elseif cmd == "shape" then
local v = string.lower(w[2] or "")
if v == "square" or v == "round" then
@@ -1108,6 +1301,9 @@ local OB_OK, OB_ERR = pcall(function ()
end
PlaceButton()
Say("minimap icon placement: " .. (CFG.shape or "auto") .. ".")
elseif cmd == "all" then
CFG.full = not CFG.full
Say(CFG.full and "showing all routes." or "showing routes for where you are.")
elseif cmd == "boat" then
CFG.hideBoat = not CFG.hideBoat
Say("Sparkwater Port boat " .. (CFG.hideBoat and "hidden" or "shown") .. ".")
@@ -1141,7 +1337,7 @@ local OB_OK, OB_ERR = pcall(function ()
DB.names[r.entry] = text
Say("route " .. w[2] .. " is now called: " .. RouteName(r))
else
Say("usage: /ob name <number from /ob list> <new place name> (the zeppelin / boat part stays)")
Say("usage: /ob name <number from /ob list> <new place name> (renames the far end; the home end stays)")
end
elseif cmd == "period" then
@@ -1179,6 +1375,8 @@ local OB_OK, OB_ERR = pcall(function ()
DB.period = {}
DB.origin = {}
DB.dwell = {}
DB.farOff = {}
DB.farDwell = {}
DB.lastCal = nil
DB.lastObs = {}
DB.pending = {}
@@ -1222,7 +1420,7 @@ 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> | 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) | 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
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: |cff33ffccOcto|rBus
## Notes: Zeppelin and boat arrival timers for Orgrimmar. Reads live positions from ZepSense.dll; minimap icon toggles the window.
## Author: Phridgey
## Version: 1.1
## Version: 1.2
## SavedVariables: OctoBusDB
OctoBus.lua
+38 -11
View File
@@ -1,8 +1,10 @@
# OctoBus
Arrival timers for the zeppelins at the Orgrimmar towers, the Sparkwater Port boat, and the Ratchet to Booty Bay boat, for the vanilla 1.12.1 client (tested on OctoWow / Turtle WoW).
Arrival timers for the Horde zeppelins and boats, for the vanilla 1.12.1 client (tested on OctoWow / Turtle WoW).
A small minimap icon opens a window listing every transport with a live timer: how long until it **leaves** if it is docked, or how long until it **docks** if it is away. For the Ratchet boat it shows when to **fly** from Orgrimmar to make the next sailing. Click a transport to put a bell on it and get a chat message shortly before it arrives (or, for the Ratchet boat, shortly before you need to fly).
Each row reads **start -> end**, for example `Orgrimmar -> Undercity`, with a live timer for the start: how long until it **leaves** if it is docked there, or how long until it **docks** there if it is away. Right-click a row to reverse it (`Undercity -> Orgrimmar`, timed at Undercity). For the Ratchet boat, seen from Orgrimmar, it shows when to **fly** to make the next sailing. Click a row to put a bell on it and get a chat message shortly before it arrives (or, for the Ratchet boat, shortly before you need to fly).
See the [changelog](CHANGELOG.md) for what changed in each version.
OctoBus needs one extra file, `ZepSense.dll`, which you install by hand in the game folder. Your launcher can install the addon but not the DLL. See [Install](#install).
@@ -15,10 +17,31 @@ OctoBus needs one extra file, `ZepSense.dll`, which you install by hand in the g
| `~` before a time | An estimate from stored data. It goes away once you see the transport dock or leave. |
| `fly 4:55 sails 6:40` | Ratchet boat only: leave Orgrimmar in 4:55 to be on the Ratchet dock before it sails in 6:40. If you can no longer make a sailing, it shows the next one. |
| Bell on a row | Alerts are on for that route (click a row to toggle). |
| Footer | The transport whose data is oldest, e.g. `oldest data: Undercity zeppelin, seen 3 h ago`. It turns orange after 5 days, as a hint to go and see it again. |
| 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. |
| 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. |
Routes tracked (with built-in starting values, which refine themselves as you play): the Undercity, Grom'gol, Kargath and Thunder Bluff zeppelins, the Sparkwater Port boat (hide it with `/ob boat`), and the Ratchet boat. You can rename the place part of any route with `/ob name`.
### Routes
| Route | Type | Ends (where it shows up) |
|---|---|---|
| Orgrimmar <-> Undercity | zeppelin | Orgrimmar / Durotar; Tirisfal Glades / Undercity |
| Orgrimmar <-> Grom'gol | zeppelin | Orgrimmar / Durotar; Stranglethorn Vale |
| Orgrimmar <-> Kargath | zeppelin | Orgrimmar / Durotar; Badlands |
| Orgrimmar <-> Thunder Bluff | zeppelin | Orgrimmar / Durotar; Mulgore / Thunder Bluff |
| Grom'gol <-> Undercity | zeppelin | Stranglethorn Vale; Tirisfal Glades / Undercity |
| Sparkwater <-> Revantusk | boat | Orgrimmar / Durotar; The Hinterlands (hide with `/ob boat`) |
| Ratchet <-> Booty Bay | boat | Ratchet; Stranglethorn Vale. Also shown in Orgrimmar / Durotar, for planning. |
Every route has built-in timings measured on rides (both ends, cycle and stay), which your copy keeps refining from what it sees. The far end of Grom'gol <-> Undercity is not measured yet; it is learned the first time you ride it from Grom'gol. You can rename the far end of a route with `/ob name`.
### Which routes are shown
- **Routes for where you are** (the default): only routes with an end in your zone, each reversed to start from your end. In Stranglethorn Vale, for example, you see `Grom'gol -> Orgrimmar`, `Grom'gol -> Undercity` and `Booty Bay -> Ratchet`. A right-click reverses a row until you change zone. A route whose transport is in view (for example while you ride it) is shown too. Somewhere with no routes, opening the window by hand shows the routes of the last place that had some.
- **All routes**: every route, in the direction you last set with right-click (kept between sessions).
Switch with a click on the window title, or `/ob all`.
## Requirements
@@ -49,8 +72,9 @@ Saved data lives in `WTF\...\SavedVariables\OctoBus.lua`.
## Using it
- **Minimap icon**: click to show/hide the window anywhere; drag to move it around the minimap edge.
- **Auto-show**: the window opens by itself in Orgrimmar and Durotar, and while the Ratchet boat is in view (on its dock and for the whole 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 transport row to toggle a bell. Belled routes print a chat message `/ob alert` seconds (default 45) before arrival. Nothing is printed to chat for routes without a bell.
- **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.
- **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
@@ -60,11 +84,12 @@ Each route runs on a fixed cycle. Once the addon sees one transport arrive, it c
- **Automatic calibration**: any arrival you watch near the towers calibrates that route. The first time takes two sightings, later refreshes take one. The cycle length is refined as more arrivals are seen, and the dock-time (how long a transport waits) is learned too.
- **`/ob calibrate`** (optional): watches every transport on purpose. Stand between the two towers where you can see the docks, do not `/reload`, and wait; rows are marked complete as each route is confirmed. A transport being out of view for a while is normal. If nothing at all has been seen after about 10 minutes, move closer to the towers. `/ob calibrate stop` cancels.
- Transports are not reported from deep inside the city (bank / auction house area), so calibrate near the towers.
- **Far ends are learned by riding.** When a transport stands still for 20 s away from its home dock, it is docked at the far end: the addon stores when it docked there (relative to its schedule) and how long it stayed. This needs the home end to have been seen earlier in the same session (for example, it leaving Orgrimmar with you on board), and the transport to be seen arriving. No recording is needed.
- **One odd sighting does not wipe a route's history.** A sighting that does not fit the schedule is set aside; the schedule only starts over (for example after a server restart) when a second sighting agrees with it.
### The Ratchet boat
The Ratchet boat cannot be seen from Orgrimmar, so its row is always an estimate (`~`) built from the last time you saw it at Ratchet, or from the built-in timing if you never have. The built-in timing was measured on two rides (cycle 364.07 s, 64 s at the dock). Every time you are at Ratchet with the boat in view (the DLL is needed for this), your copy corrects its timing and refines the cycle, so predictions stay accurate for longer the more often you have been there. Nothing is shared between players: each copy only learns from what its own player sees.
The Ratchet boat cannot be seen from Orgrimmar, so its row is always an estimate (`~`) built from the last time you saw it at Ratchet, or from the built-in timing if you never have. The built-in timing was measured over several rides (cycle 363.95 s, 64 s at each dock, docks at Booty Bay 178.6 s after Ratchet). At Ratchet itself the row shows plain docks / leaves times instead of `fly`. Every time you are at Ratchet with the boat in view (the DLL is needed for this), your copy corrects its timing and refines the cycle, so predictions stay accurate for longer the more often you have been there. Nothing is shared between players: each copy only learns from what its own player sees.
`fly` is the boat's departure from Ratchet minus your travel time from Orgrimmar to the dock. Set your own with `/ob travel <seconds>` (default 175: enough for a slower mount, with some time to spare; time your own trip from take-off at the Orgrimmar flight master to standing on the boat).
@@ -78,18 +103,19 @@ The Ratchet boat cannot be seen from Orgrimmar, so its row is always an estimate
| `/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) |
| `/ob zone` | Toggle "auto-show only in Orgrimmar / Durotar" |
| `/ob all` | Switch between routes for where you are and all routes (same as clicking the title) |
| `/ob zone` | Toggle "auto-show only at route ends" (off = always show) |
| `/ob allow <name>` | Also auto-show in this zone or sub-zone |
| `/ob disallow <name>` | Take a place off that list |
| `/ob allowclear` | Remove all the extra auto-show places |
| `/ob where` | Print your zone / sub-zone and whether the window auto-shows here |
| `/ob boat` | Show / hide the Sparkwater Port boat |
| `/ob boat` | Show / hide the Sparkwater <-> Revantusk boat |
| `/ob shape square\|round\|auto` | Minimap icon placement. `auto` reads your minimap shape (tell it explicitly if you use a square minimap addon and the icon sits wrong) |
| `/ob travel <sec>` | Your travel time from Orgrimmar to the Ratchet dock, used for `fly` |
| `/ob scale <n>` | Window size, 0.6 to 2 (same as dragging the bottom-right corner) |
| `/ob lock` | Lock / unlock the window position and size |
| `/ob resetpos` | Put the window and icon back in their default spots and size |
| `/ob name <n> <text>` | Rename route number `n` (numbers from `/ob list`) |
| `/ob name <n> <text>` | Rename the far end of route number `n` (numbers from `/ob list`) |
| `/ob period <n> <sec>` | Set a route's cycle length by hand |
| `/ob calibrate` | Watch all transports on purpose (see above); `/ob calibrate stop` cancels |
| `/ob reset` | Forget everything learned and go back to built-in values |
@@ -99,7 +125,8 @@ The Ratchet boat cannot be seen from Orgrimmar, so its row is always an estimate
## Troubleshooting
- **"no DLL: estimates only"**: the DLL is not loaded. Check that `ZepSense.dll` is in the game folder and listed in `dlls.txt`, then fully restart the game. `/ob debug` shows the state.
- **Times have a `~` and never confirm**: walk toward the towers and wait for an arrival, or run `/ob calibrate`.
- **Times have a `~` and never confirm**: go to that end of the route and wait for an arrival, or at the Orgrimmar towers run `/ob calibrate`.
- **A route I want is missing from the window**: you are in the "where you are" view and not at either end of it. Click the title to see all routes.
- **`/ob` does nothing or prints a start-up error**: the addon reports its own load errors in chat; copy the message when reporting a bug.
- **Window did not auto-show**: use `/ob where` to see your zone and sub-zone names, then `/ob allow <name>` if you want it there.
- **Ratchet times look wrong**: it has probably been a long time, or there was a server restart, since you last saw it. Visit Ratchet with the window open and wait for the boat to dock.