LevelTimer-v2.8.1.zip
What's new
Level Time
v2.8.1 (2026-10-03)
Full Changelog Previous Releases
- Merge pull request #14 from Noobygames/fix/v2.9-stability
v2.8.1: stability fixes from the deep review (roadmap 95-144) - Note roadmap items released in v2.8.1
- Ask WoW Forever whether the character uses ammo (roadmap 144)
The ammo warning assumed every hunter outside Retail uses ammo. WoW Forever documents UnitUsesAmmo, which its own character frame uses to show the ammo slot; where it exists the warning now follows it, elsewhere it still checks for hunters. - Scroll the profile list in the options (roadmap 143)
The Profiles tab showed only the first six profiles; further ones could not be selected or deleted there. The list now scrolls with the mouse wheel and a scroll bar that appears once there are more profiles than rows. - Fit localized buttons and keep sort markers visible (roadmap 142)
The history window's delete button and the options' save-profile button had fixed widths that French, Spanish and German labels overflow. Widgets.SetButtonText now widens a button to its text (Button:GetTextWidth on every flavor) with the old width as minimum. Table headers show the sort marker before the label, so narrow localized headers lose their end but never the sort direction. - Show the count footer in a column wide enough (roadmap 141)
Count footers were always written into column 1, which in the Speedrun tables is the 18 px favorite column, so the count was unreadable. The count now goes into the first column at least 80 px wide. - Sort sessions by level numerically (roadmap 140)
The sessions table sorted its Level column by the shown text, mixing numbers and ranges like '9-10', so the order was wrong. The column now sorts by start level, then end level, as a number. - Widen the timer text for 100 days and more (roadmap 139)
The time display reserved width for two day digits, so a level time of 100 days or more (long-lived max-level characters) was cut off or wrapped over the XP bar. The reserved width now grows with the number of day digits and the window re-runs its layout when that happens. - Format money through C_CurrencyInfo (roadmap 138)
Format.Money used the global GetCoinTextureString, which every client only defines as a deprecation alias when loadDeprecationFallbacks is on (WoW Forever has no alias at all), so money fell back to plain text. It now uses C_CurrencyInfo.GetCoinTextureString, documented on every flavor, and the stub only provides that. - Re-layout the timer window when leveling stops or resumes (roadmap 137)
XP bar visibility was only recomputed when settings were applied, so the bar stayed after reaching max level or turning XP off at the experience eliminator. The window now re-runs its layout on OnLevelStarted and on ENABLE_XP_GAIN/DISABLE_XP_GAIN (documented on every flavor). - Sell junk first when only its proceeds cover the repair (roadmap 136)
Auto repair ran before selling junk and reported 'not enough money' although the junk sold right afterwards would have paid for it. When the repair costs more than the character's gold but not more than gold plus the expected junk proceeds, junk is sold first and the repair runs a second later, after the server has credited the sale; otherwise the old order (repair, then sell) stays so repair debits remain separate. - Decline duels to the death as well (roadmap 135)
declineDuels only listened to DUEL_REQUESTED; Hardcore 'duel to the death' requests come as DUEL_TO_THE_DEATH_REQUESTED with their own dialog, whose decline button calls CancelDuel (Classic Era, TBC Anniversary). The same setting now declines them too. - Close the role invite popup when declining group invites (roadmap 134)
Retail and WoW Forever show group invites that ask for a role in LFGInvitePopup instead of the PARTY_INVITE dialog, so a declined invite left that window on screen. The group decline now also hides it via StaticPopupSpecial_Hide when both exist. - Leave auto-selected gossip options to Blizzard (roadmap 133)
Blizzard's GossipFrame already selects the only option itself when it has selectOptionWhenOnlyOption (HandleShow on every flavor), and skipGossip selected it a second time. Such options are now left to the frame; skipGossip covers only the options Blizzard leaves for manual selection. - Strip escape sequences from imported run names (roadmap 132)
Imported run names were shown as they came, so a pasted name with |c, |H or |T sequences rendered colors, links or textures in the history and split list. Imported names, realms and classes now drop every '|'. - Respect streamer privacy in run exports (roadmap 131)
Run export and backup printed the real name and realm into the copy window even with streamerPrivacy on, which is visible on stream. Exports now drop the realm and use the history's display name for own characters while privacy is on. - Import old attempts of own characters (roadmap 130)
Run import treated any run with the name and realm of an own character as already known, so the exported old attempt of a character whose stats were reset or that was recreated could not be imported back. A run now only counts as known when its level times match too. - Show no time to level for an empty level span (roadmap 129)
Runs.SumOfLevels returned 0 for an empty span, so the Runs view showed 0s for every run while the logged-in character had no completed level. An empty span now returns nil and the cell shows '-'. - Keep the split list in place when it is rescaled (roadmap 128)
The split list re-anchored with its saved offsets after every scale change, but offsets are measured in the frame's own scale, so it jumped. Like the main window it now scales around its top-left corner and saves the new position; a new position table (login, profile, reset) is still restored. The corner-keeping scale helper moved to Widgets for both frames. - Compare against best times when the chosen run is the logged-in character (roadmap 127)
The chosen comparison run is stored account-wide, so logging into that character compared it against its own frozen copy. In that case the splits now use the best time per level. - Rate summed XP only over entries with a known duration (roadmap 126)
History.Summarize added the XP of levels whose duration was unknown (/played had not answered) but not their time, so the total XP/h row and the character comparison were too high. Summaries now carry the XP and AFK time of entries with a duration, which Experience.RecordRate uses. - Re-read the current instance after a login restart (roadmap 125)
Deleting the logged-in character's stats while inside an instance restarts the login, which cleared the current stay; nothing was tracked until the next zone change. InstanceCopy now re-reads the current instance right after the login callbacks (C_Timer.After 0); on a normal login the following PLAYER_ENTERING_WORLD finds the same copy and does nothing. - Report today's count when the server refuses an instance (roadmap 124)
When the server rejected an instance while the addon counted free slots, the message only showed the hourly count, which looks wrong if the refusal came from another limit (a daily cap is rumored but not documented). The message now also shows today's count and says that the server may apply another limit. - Tell Normal and Heroic copies of an instance apart (roadmap 123)
Copies were tracked by instance name only, so entering the Heroic version of a dungeon within 30 minutes of the Normal one counted as the same copy: no limit entry and both runs merged. Visits are now keyed by name and difficultyID (third return of GetInstanceInfo on every flavor); a reset message clears every difficulty of that name and visits saved by v2.8 are taken over. - Keep the copy guess across reloads until it is confirmed (roadmap 122)
When a new copy was guessed, the old zoneUID only lived in memory; a /reload or a quick exit before two enemies were seen lost it, so a later confirmation of the old copy could no longer undo the extra limit entry or merge the split run. The visit now keeps the pending guess (guess, expected zoneUID, entry time) until confirmation, and re-entering the unconfirmed copy continues with it. - Ignore resets of the instance the player is standing in (roadmap 121)
A reset message for the instance the player is in (a leader resetting while others are still inside) deleted the visit, ended the run mid-dungeon and made the next confirmation index a missing visit (Lua error). Such resets are now ignored, since they cannot affect an occupied copy, and confirm() creates the visit if it is missing. - Ignore low health right after a resurrection (roadmap 120)
Resurrections often restore little health (spirit healer, soulstone, low-rank spells), so recovering afterwards counted as a near death. For 15 seconds after the player is alive again (PLAYER_ALIVE/PLAYER_UNGHOST) low health no longer starts a near-death episode. - Read combat log events through C_CombatLog (roadmap 119)
Death causes from the combat log used the global CombatLogGetCurrentEventInfo, which every targeted client only defines as a deprecation alias when the CVar loadDeprecationFallbacks is on, so without it Classic Era and TBC recorded no killer. The addon now uses C_CombatLog.GetCurrentEventInfo (falling back to the global) and the stub only provides the C_CombatLog variant. - Use Blizzard's effective max level for leveling checks (roadmap 118)
IsLeveling compared against GetMaxPlayerLevel only, so at the account's expansion level cap in Retail XP/h, forecasts, the XP bar and the Well Fed reminder stayed on. It now asks GameRulesUtil.IsPlayerAtEffectiveMaxLevel (Retail, WoW Forever) or IsPlayerAtEffectiveMaxLevel (Classic), like Blizzard's XP bar, and falls back to GetMaxPlayerLevel. - Accept only whole levels as goal (roadmap 117)
/lt goal 30.5 was accepted; the confirmation showed one level and the goal was reached at another. Goal.Set now rejects non-integer levels. - Rebuild the split reference at every login (roadmap 116)
The reference cache key counts the characters, so deleting the logged-in character (which restarts the login with the same count) kept the old personal-best reference until the next level-up. The cache is now cleared at every login. Split test fixtures carry a schema version, since all stored characters are migrated since roadmap 104. - Reject deeply nested serialized text (roadmap 115)
Serializer.Decode recursed once per nesting level, so pasted text with thousands of opening braces overflowed the stack instead of returning nil. Decoding now stops at 20 levels, far more than runs or profiles use. - Accept the shown name of the default profile in commands (roadmap 114)
The default profile is listed under its localized name (Standard, Par défaut, Predeterminado), but /lt profile only knew the stored key Default, so the listed name could not be used and saving under it created a second profile. Switch, SaveAs and Delete now map the localized name to the default profile. - Match debug alert kinds case-insensitively (roadmap 113)
C:/Program Files/Git/lt debug lowercases its argument, so the levelUp and nearDeath sample alerts could never be shown. Kinds are now matched case-insensitively. - Never lower the saved schema version (roadmap 112)
migrate() always stamped the target version, so running an older addon version lowered it and the next upgrade ran migrations again; v4/v5 add to the daily stats and doubled them. Data at or above the target version is now left alone. - Load the default profile for characters without their own choice (roadmap 111)
A character that never picked a profile kept whatever profile was active last, and its changes were stored into that other character's profile. Characters without an assignment (or whose profile was deleted) now load the default profile at login. - Validate the settings of imported profiles (roadmap 110)
Profiles.Import only checked that keys were strings, so a value of the wrong type broke ApplySettings at every login once the profile was active. Database.SanitizeSettings now keeps only known settings whose type matches the default (nested tables checked per key) plus the optional window positions, split reference and stream backup in their expected shape; empty profile names are rejected. - Prepare table sort keys once per row (roadmap 109)
The history tables refresh every second and the text comparator stripped color codes (five gsub calls) and lowercased both keys in every comparison, about 60k comparisons for a full kill journal. Sort keys are now prepared once per row, and mixed number/text columns get a fixed order (numbers first) instead of a non-transitive comparison. - Leave repeatable quests to the player (roadmap 108)
Auto accept and turn-in ignored the repeatable flag, so a repeatable item turn-in reopened the NPC dialog after every hand-in and the automation handed in all of the player's items. Repeatable quests are now skipped in the gossip lists (GossipQuestUIInfo.repeatable) and in the old quest greeting (third return of GetAvailableQuestInfo, isRepeatable on every flavor). - Try guild repairs first and pay the rest personally (roadmap 107)
Guild repair was gated on GetGuildBankMoney(), which is 0 until the guild bank was opened in the session (or stale afterwards), so the guild almost never paid and a stale value could print a guild repair that failed. Like Blizzard's dialog, the addon now just calls RepairAllItems(true), checks the remaining cost a second later, reports what the guild paid and repairs the rest with the character's money. The stub models a guild repair that the server refuses. - Start fresh when a deleted character's name is reused (roadmap 106)
Stats are keyed by name and realm, so a character recreated with the same name inherited the old attempt's level history, and the total split, PB, runs and export mixed two attempts. A saved level above the current one (levels never drop) now marks the data as replaced: Runs keeps the old attempt as an imported run and the new character starts empty. Split totals also ignore levels at or above the current one. - Migrate every character's data, not only the logged-in one (roadmap 104)
Migrations and defaults only ran for the logged-in character, so alts saved by v1.x lacked the journals, zone and daily stats, and the history views and charts raised Lua errors when one of them was selected. Database.Load now migrates and fills defaults for every stored character. - Hide near deaths where the client keeps health secret (roadmap 103)
The generated docs mark UnitHealth with SecretReturns on Retail and WoW Forever, so near deaths can never be detected there and the stat stayed at 0. NearDeath.IsAvailable now hides the window row, its options toggle and the near-death alert toggle on those clients. Stat lines can declare available() for this; the window, the data broker and the options honor it via ns.IsStatShown. - Book half of the rested pool decrease as rested bonus (roadmap 102)
GetXPExhaustion is the XP range gained at double rate, so each gain inside it lowers the pool by base plus bonus XP (warcraft.wiki.gg API_GetXPExhaustion). The whole decrease was booked as bonus, doubling the rested XP shown. Only half is booked now; the test models the pool like the client. - Keep the addon's own /played requests out of chat (roadmap 101)
PlayedTime wrapped ChatFrame_DisplayTimePlayed, but every targeted client calls ChatFrameUtil.DisplayTimePlayed from the chat frames' TIME_PLAYED_MSG handler, so each addon request printed to chat. The chat frames now stop listening to TIME_PLAYED_MSG while the addon's own request is pending (no replaced Blizzard function, so no taint) and listen again after the answer or a timeout. The stub models the chat frames instead of a no-op alias. - Count session XP correctly across level-ups (roadmap 100)
A level-up was only detected when UnitXP went down, so a gain that left more XP on the new level than before on the old one lost the rest of the old level, and a gain over two levels lost a whole level. Level-ups are now detected by a changed XP requirement, falling XP or a higher level, skipped levels come from the XP table where it is known, and the order of PLAYER_XP_UPDATE and PLAYER_LEVEL_UP no longer matters. - Move the timer window to the profile's position on profile switch (roadmap 99)
Switching profiles only rescaled the window around its current corner and then saved that spot into the new profile, overwriting its position. The apply callback now notices that db.pos is a different table than the last applied one and restores the profile's position and scale. - Isolate errors of event, login, logout, level-up and apply callbacks (roadmap 98)
runAll called every callback directly, so one failing module skipped the rest: an error in an OnLevelCompleted callback left ns.level and every level reset undone, an error at logout lost the session's seconds and lastSeen. Each callback now runs through ns.SafeCall (xpcall with the client's error handler), like ns.Every. The test harness fails a scenario for every error caught this way unless the scenario clears wow.errors. - Capture loot links regardless of the format's ending (roadmap 97)
ChatPatterns compiled a trailing %s to a lazy capture that matched the empty string, so with Retail's loot formats (no trailing period, likely also WoW Forever) single items were never logged. A trailing %s now takes the rest of the line, and loot formats capture the item link itself (ChatPatterns.LINK), so dots in item names no longer cut it either. - Check for secret values before testing unit flags (roadmap 96)
UnitIsAFK is secret during chat messaging lockdown in Retail and WoW Forever. TimeBreakdown tested the value for truth before checking it, which addons may not do with secrets, raising an error every second. The secret check now runs first. The test stub gains issecretvalue and wow.SECRET. - Step day and week charts by calendar days (roadmap 105)
The day and week charts counted back in steps of 86400 s from midnight. Across the spring DST change that skipped a day and moved older weeks to start on Sunday. They now use Daily.AddDays. - Book daily play time across DST changes without hanging (roadmap 95)
Daily.BookPlayTime added 86400 s to midnight to find the next day. On the 25-hour day at the end of daylight saving time that lands at 23:00 of the same day, so the loop never advanced and the client hung on logout, /reload or when a daily chart opened. Days now step by calendar (Daily.AddDays) and the loop stops if it cannot advance. - Add stability items from the deep review (roadmap 95-144)
Fifty findings from a review of every feature, each confirmed by two
independent checks against the code and Blizzard's UI source for Retail,
Classic Era, TBC Anniversary and WoW Forever. In-game checks for the
items that depend on live client behaviour go to ManualToCheck.md.
This mod has no additional files

