Sku v43.2 - patch notes

Overview

New

Changes

Bugfixes

Swimming and flying: the pitch lock ends the unwanted diving

Simple: While swimming or flying, every automatic turn towards the beacon pulled you a little bit downwards - over a longer stretch that added up until you were under water or on the ground without noticing. Now Sku locks your pitch as soon as you swim or fly: you move exactly level, and Space and X still carry you up and down. And if something does tilt you - such as the automatic approach when interacting with an NPC - Sku actively straightens you out again: when the NPC window opens, shortly after every turn, and whenever you press ascend or descend. On by default; anyone who wants to dive by pitching with the mouse while swimming (residual sight) switches it off under Settings -> General. On top of that, the new keybind "pitch lock" (Ctrl+Shift+N) locks and releases manually.

Technical: Three building blocks. First, the console variable pitchlimit 0 caps the character's movement pitch (the technique of the LevelFlight and FlightHUD addons) - set persistently rather than around each turn impulse, because exactly that toggle race is what made earlier attempts fail. pitchlimit is writable but not readable; every login therefore blindly restores the default of 88 so a lock can never get stuck. Second, the lock only holds what is there: engine-driven movement such as the interact approach still tilts the character, and afterwards no keyboard input exists that raises the pitch again. The active re-leveling uses the very transfer that originally caused the bug: ResetView/SetView snaps the camera to the known, almost level default preset, a mouselook impulse transfers it onto the character, and the lock caps the small remainder to zero. Triggers are the NPC window, every turn (delayed by 0.5 s, because the engine applies the yaw transfer a frame later - an immediate second impulse would undo the turn) and, via hooksecurefunc, the ascend/descend functions JumpOrAscendStart, AscendStop, SitStandOrDescendStart and DescendStop - independent of which keys they are bound to. Third, a pitch gauge for the log: measured horizontal speed divided by GetUnitSpeed is the cosine of the trajectory angle - the only measurement this client offers (GetUnitPitch was removed in patch 7.1 and is missing here too). It measures the trajectory, not the body pitch, and serves diagnosis only.

The cursor stays on the item in the bag

Simple: Right-clicking an item in the bag list - use it, equip it, sell it to a vendor, apply an enchantment - dropped you onto the first entry of the whole list. For items that vanish straight away that barely showed, because the list re-sorts anyway; for everything else it was simply a lost position. Now the cursor stays on the item you worked on, and when the item leaves the bag you land on its slot - that is, on whatever moved up into it.

Technically: The cause was never shifting indices but the third line of the right-click macro, "/script SkuCore:CheckFrames()". It dated from before the event-driven confirm existed and ran AT THE KEYPRESS - before the action had changed anything at all, and for an action with a cast time seconds before. It therefore rebuilt stale data and cost the cursor for nothing. Removed; the refresh belongs to the BAG_UPDATE-driven confirm alone, which rebuilds only once the bags have really settled and then re-pins the cursor by bag slot and item id. In addition, CheckFrames no longer remembers only the row number of the focused entry across a rebuild but its name as well, and looks that up in the rebuilt list - the row number alone stops being right as soon as the list had a different shape in between.

Enchants, lures and oils: the list refreshes after the cast

Simple: An enchantment, a fishing lure, a weapon oil, an armor kit or disenchanting only change the bags when the cast finishes - often five seconds after the click. Sku had long given up by then: the list stayed stale and the cursor sat somewhere. Now Sku waits the cast out, announces the item once afterwards and leaves the cursor on it. If you navigate on yourself in the meantime, you keep your own position - Sku then stays out of it.

Technically: The confirm window opened by the right-click expired after 2.5 seconds. UNIT_SPELLCAST_START/-CHANNEL_START now push its deadline out to the end of the cast while keeping the anchor captured at the click, so the re-pin afterwards lands on exactly the item that was worked on. Deliberately without a spell list: an open window is itself the proof that this cast belongs to the action. Two hard guards, both learned from a real case: the cast has to follow the click within one second ("/use" starts it in the same frame), and the extension is a one-shot. Without them a 22 second fishing cast kept pushing the deadline ahead of itself and the window never died.

The confirmation dialog no longer costs the position

Simple: Applying an enchantment to an already enchanted item gets you Blizzard's "Replace existing enchantment?" question. After "Yes" or "No" you ended up at the very top of the bag hierarchy, on "Bag 1" instead of on the item. Now both answers lead back to the item, and it is announced once.

Technically: The dialog is a modal window between the click and its effect, and it broke the position twice over: the 2.5 second deadline expired while it was up, and Sku anchors the menu INSIDE the dialog, so on closing there was nothing left to return to

list. SkuBagPostActionPopupGate holds the window open for as long as the dialog stands (up to 30 seconds) and triggers the confirm when it closes. That is the right tool precisely because it covers both answers: "No" produces neither a cast nor a bag change, so nothing else could ever put the cursor back. While the dialog stands the confirm holds off entirely - otherwise an unrelated bag change (loot landing, a trade settling) would tear the cursor out of the dialog.

The type-ahead filter in the bag list clears again

Simple: Typing a name in the bag list filters it down to the matches. That filter used to stop clearing as soon as you had used the filtered item: even arrow left and right again did not bring the full list back, only closing the bags helped. Now every one of those keys clears the filter again, the way you are used to. When nothing matches the filter at all, the list stays empty as before and the arrow keys only repeat the typed filter - which is exactly how you hear that there is no hit.

Technically: SkuOptions:ClearFilter only forgot the typed string; the filtered child list ApplyFilter had installed stayed in the tree until something replaced it wholesale - and a level built from game frames, such as the bags, cannot refresh in place, it needs a full rebuild. Masked for years by the forced CheckFrames in the macro (see above), it showed the moment an action does not change the bags at all. ClearFilter now genuinely restores the list, including the forward and backward links, and does so on the node it filtered rather than the currently focused one.

Escape did not close the menu while standing right on a quest object

Simple: Standing at roughly zero metres in front of a quest object and closing the menu with Escape pulled the quest window straight back up - endlessly. In that spot the menu could barely be closed at all. Now the open game windows are closed first and the menu itself afterwards.

Technically: The menu hid itself first and worked through the open interact windows after that. The quest frame was therefore closed while the game still reported the object in range, and it reopened immediately. The order is reversed now and secured by a counter that "/skucheck menu" reads out: a violation of that order is noticed without anyone having to be standing on exactly that spot by chance.

Fewer duplicate rebuilds when closing a window

Simple: A single Escape used to trigger up to six complete rebuilds of the menu data, because several windows close at once and Blizzard's quest window alone hides four sub-panels. That cost noticeable time. Now exactly one rebuild runs per frame. Also, the bag list held for combat could go stale when a quest window was closed while the bags were open - a fault that would only have surfaced later, in combat.

Technically: The hide paths run through SkuCore:CheckFramesCoalesced, whose gate opens once per frame; the actual pass is deferred by 0.01 s anyway and reads live state, so it sees exactly what the last call of the burst would have seen. The OnShow side is deliberately left out. The bookkeeping in GENERIC_OnClose has been given its own gate: it previously hung on the return value of that same shared gate, and since the quest sub-panels fire first they consumed it and the bookkeeping - the combat bag list refresh among it - was skipped.

Speech: announcements no longer fall silent

Simple: While navigating, speech kept going quiet for a while - an arrow key simply did not read the entry out, in a quiet moment with nothing else going on. It was most reliable when you filtered a list down to a single hit and then moved quickly between the filter and that one hit: after a few passes the hit stayed silent for good, even if you waited a second. Both are fixed.

Technically: Two independent causes, both below the queue - Sku itself had handed over every announcement cleanly; in a 95-minute capture every keypress produced exactly one SpeakText with a STARTED.

First, the same line was re-issued up to four times inside one second, because a redundant window check announces the focused menu entry again; 95 of 1512 utterances were such repeats. Each carries its own "queuereset", i.e. a StopSpeakingText, and that does reach the screen reader - so the line was cut mid-word and restarted, and whenever the resend was lost along the way nothing was left. The existing duplicate guard could not catch this: it hangs off VOICE_CHAT_TTS_PLAYBACK_FINISHED, and across the screen-reader bridge STARTED and FINISHED both come back in the same frame - so there the guard is always empty and can never fire. There is now a time-based guard that drops an immediately following identical line within one second, together with its queuereset; dropping only the text would still cancel the line being spoken and put nothing in its place. It compares against the line handed over immediately before and nothing else, so it cannot swallow a genuine re-read, and it only applies when the first copy really began to play.

Second, the cache-buster had stopped working. The client stores synthesized speech by text and replays it on a repeat without invoking the voice again - across a bridge that replay is silent. The counter-measure so far was a cycling <bookmark>, but the client parses markup off and keys only on the spoken text: in the capture every single utterance carried its own marker and the hit still went silent. The original buster varied real characters and did work; switching it to a marker was meant to protect prosody and quietly brought back the very bug it had been written against. Real text is appended again: a cycling run of 1 to 64 no-break spaces after the last word, where it is neither audible nor able to colour the prosody.

Well Fed: only one entry left in the spell picker

Simple: Anyone building an aura on the Well Fed buff found two entries in the spell picker that were both spoken as "Well Fed". Only one of them could ever fire, the other was a dead record - which one you hit was pure luck and not audible. There is only one entry now, and it is the right one. An already saved aura pointing at the wrong one is moved over once at the next login and works afterwards.

Technically: Since v43.0 auras are keyed on a spell's English identity, so the same aura fits in every language. In the shipped spell database, however, ten rows carry the English name IN QUOTES - 44097 to 44106 plus 69559 and 65365 carry the name with two quote characters wrapped around it, where the other 65 rows carry it without. That created a second identity behind

one and the same localized name, and the menu disambiguation appends the English identity in such a case: one entry read "Well Fed (Well Fed)", the other the same but with two extra quote characters around Well Fed.

Speech does not read quotes, so the two entries sounded identical. Well Fed is the only one of the 26,650 localized names in the data set whose two identities differ by nothing but those characters. The name is now unquoted when the identity is formed, and on BOTH sides - in the value list and in every resolution of a live event - because changing one side only would produce a stored value no event can match any more. Only names quoted at BOTH ends are unquoted; the 17 remaining rows with quotes inside the name (Goblin "Boom" Box, "VICTORY" Perfume) keep them, because there they are part of the name. A one-time migration moves saved auras over.

Weapon enchants are recognisable in the spell picker

Simple: A fishing lure, a sharpening stone or a weapon oil is not a buff but an enchant on the weapon. It appears in no buff list of the game and fires no aura event - an aura on "event = aura lost and spell name = Fishing Skill +75" could therefore never fire, even though the entry is in the picker and sounds perfectly normal. Such entries now carry the suffix "(weapon enchant)" in every picker, so it is audible what they are. To react to one, use either the event "Weapon enchant expired" or the condition "Your weapon enchant main hand". The suffix is not read out in the announcement of a fired aura.

Technically: The value list of the "spell name" attribute is the entire spell database, including rows that only ever reach the player as a temporary weapon enchant - the lure is spell 8084 behind enchant id 265. The affected identities are now determined while the lists are built (every spell id of the identity is referenced by the enchant database) and get the suffix. Deliberately MARKED rather than removed: there are 395 of them and they are not all inert - Fiery Weapon, Executioner and Unholy Strength are enchant procs that do appear in the combat log under exactly that name. Removing them would have taken working auras their only way to name them. The suffix rides the same mechanism as the English disambiguation, so it drops out again in the speech of a fired aura.

Weapon enchants are addressed like spells

Simple: For a damage-over-time spell you write "event equals aura lost" and "spell name equals <spell>" - the event itself says what it is about. For a weapon enchant that was not possible: the fishing lure could only be named through "your weapon enchant main hand", and that is a question about state, not about the event. In the "contains" form such an aura could not fire at all - by the time the event arrives the enchant is already gone, so the list no longer contains it. Only the inverted "contains not" form worked. The event now carries the enchant's name itself, and the aura is written exactly like a spell one: "event equals Weapon enchant expired" and "spell name equals Fishing Skill +75". "contains not" keeps working.

Technically: The two synthesised events WEAPON_ENCHANT_REMOVED and

WEAPON_ENCHANT_UPDATE put the HAND name in field 13 - and field 13 is the spell name field. So a "spell name" condition compared against "Main Hand", and a "spell name" output announced the hand instead of the enchant. The enchant now sits there: field 12 gets its spell id, field 13 its name, both through the same resolvers the value list is built from - the stored value is the same one the weapon-enchant condition uses. A removal reports the enchant that WAS there, which is the identity meant.

Existing auras cannot go silent from this: "Main Hand", "Off hand", "Haupthand" and "Nebenhand" do not occur once as a spell name across all 49,117 rows of the spell database, so a "spell name" condition could never have matched on this event anyway.

The hand is not lost, it moves to its own field with its own condition and output ("weapon enchant hand"), stored as a language-neutral token so a shared aura means the same hand in every language. What was deliberately NOT done is the other obvious route - filling the state list on a removal with the outgoing enchant: the list is shared, so it would lie to every other aura on that pass, re-arm a "contains not" gate and thereby produce double announcements, and it would contradict the remaining duration, which is deliberately forced to 0 on a removal. In addition, /skucheck auras now reports the "removal event plus contains" combination as an aura that can never fire. Fixed along the way: when only the off hand changed, the event was still reported as the main hand.

The end of a weapon enchant is detected at once

Simple: The game does not announce that a fishing lure or a poison has run out - Sku looks every 0.25 seconds. The announcement was therefore up to a quarter second late. It now arrives at the moment it expires.

Technically: How long an enchant still lasts is fixed the moment you see it - GetWeaponEnchantInfo returns the remaining milliseconds. The ticker records the next expiry from it, and the frame driver compares one number per frame and then calls exactly the ordinary player ticker. That is the same shape as the duration deadline scheduler introduced in v43.0 and costs no extra polling. Double announcements are impossible because the ticker only emits when its stored snapshot really changed. Deliberately NOT done: putting the ticker on every frame - that would be fifteen times the polling for the same gain.

Toggling in a filtered list reads the new state back again

Simple: Narrowing a list with the type-ahead filter and then pressing Enter on an on/off entry removes the filter - that part is intended. The entry was toggled correctly too, you just could not hear it: the cursor jumped to the top of the list, and what was announced was the first entry rather than the new state. In the aura outputs the first entry's sound preview played on top of that. You now stay on the entry you toggled and hear its new state.

Technically: Enter removes the filter BEFORE it dispatches the action - the menu node calls ApplyFilter("") for that. Its cleanup ended in a blanket OnFirst() on the level, so the cursor moved to the top of the list in EVERY case. The toggle itself was never affected: it acts on the node (actionInPlace), not on the cursor. Only what got read back afterwards was wrong - and OnFirst additionally fired the first entry's OnEnter on landing, which for the aura sounds is the audition. ApplyFilter("") now follows the same rule ClearFilter already follows: the restored list holds the SAME node objects, so a cursor on a real entry stays valid; only the synthetic "Filter;<typed>" row has no home in it, and for that row alone the full OnFirst() including OnEnter still runs, so the audition on landing is not lost. With no filter active ApplyFilter("") always was a no-op - which is why toggling in an unfiltered list never misbehaved; both cases now behave the same. Side effect of the same rule: Backspace now leaves the cursor on the entry it was on instead of jumping to the top of the list.

Dungeon browser: accepting an invitation no longer lands you in the browser

Simple: If you were listed in the dungeon browser and then accepted a group invitation, you ended up in the dungeon browser without pressing a single key: the menu reopened by itself and the cursor sat on "Not listed". For sighted players nothing happens at that moment - Blizzard's group finder window never opens when an invitation is accepted. Now Sku stays quiet too: you stay where you were.

Technically: Two paths led there, both are closed now. (1) The moment the server drops your listing - which is exactly what it does when you join a group - LFG_LIST_ACTIVE_ENTRY_UPDATE arrives, and the browser rebuilt its menu entry in response. That rebuild ended in a SlashFunc onto its own menu path, and that reopens a CLOSED menu and descends into it. It now only runs while the menu is open AND the cursor actually stands inside the dungeon browser - only then do the freshly built entries have to be put back under the cursor. Skipping it costs nothing: the menu entry is "dynamic" and rebuilds on the next descend anyway. As a result no listing change you did not cause yourself (expiry, an edit by the party leader) can yank you out of your bags into the browser either. (2) A third of a second after the invitation dialog has disappeared, the browser checks whether it should reopen. It only asked "is any group finder window open right now?" and turned that into an open. Now the state present when the invitation ARRIVES is remembered, and only exactly that is restored. If the invitation put you into a group, nothing happens at all - neither open nor close.

New keybind: read the current target's full tooltip

Simple: On a target change Sku announces a fixed, short set - name, level, elite or rare, plus the second tooltip line with the creature type. Everything below that in the tooltip has never been reachable: the faction line, the PvP marker, and above all what the Hunter ability Beast Lore reveals about a beast - its family, its diet and whether it can be tamed. The new keybind (default: Ctrl+Shift+V) reads the complete tooltip out loud on request. It acts only when pressed: the normal target announce stays exactly as short as it was, and anyone who does not want the extra detail never hears it. That is also why there is no setting for it - pressing the key IS the request. With no hard target the key describes the current soft target instead, so it also works on something you have not committed to yet. Rebindable under Sku key bindings, target and soft targeting.

Technically: The idea, the proof and the reference implementation come from ZenqFR, in the companion addon SkuBeastLore (github.com/ZenqFR/Sku-BeastLore); the Beast Lore output was tested there on a real Hunter. Here the feature is rebuilt natively against Sku's own interfaces, so no extra addon is needed. The Beast Lore lines are produced by the client itself inside the unit tooltip; no Blizzard interface Lua builds them and no API returns them separately - reading the tooltip is the only way to reach them. That is also why a separate option for just the Hunter information is not possible: those lines could only be isolated by locale-dependent text matching.

SkuMob:OutputTargetTooltip resolves target, softenemy, softfriend and softinteract in turn and reads through Sku's shared SkuScanningTooltip. Because that frame is shared - bags, auction house, chat links and quest text all read the same one - it is cleared before AND after, and its owner restored exactly as SkuCore sets it at login.

Track and untrack quests

Simple: Every quest menu now ends with a "Track quest" entry. ENTER flips it, the cursor stays where it is, and the new state is spoken straight back - "Track quest;On" or "Track quest;Off". Tracked means WoW's own marker on the minimap and the world map. Anyone with some remaining sight finds quest objectives faster that way; anyone who sees nothing gets nothing out of the markers. So the entry can be switched off entirely - under Sku settings, SkuQuest, "show track quest entry"; it is on by default. It only appears for quests in your own quest log, because a quest from the quest database is not one you have yet. Two limits come from the game itself: The Burning Crusade allows at most five tracked quests at a time, and a quest with no objectives at all cannot be tracked. In both cases Sku speaks the game's own message and the state stays as it was.

Technically: The idea and the reference implementation come from ZenqFR, in the companion addon SkuQuestNearby (github.com/ZenqFR/Sku-QuestNearby); with ZenqFR's agreement feature is rebuilt here natively against Sku's own interfaces, so no extra addon is needed. The entry lives in the chunk-local CreateQuestSubmenu rather than on a hook over the public wrapper - which is why every quest view gets it: quest log, quest database and SkuChat's quest menus. There is no C_QuestLog on 2.5.6: AddQuestWatch, RemoveQuestWatch and IsQuestWatched take a quest LOG INDEX, not a quest ID, and that index shifts whenever a quest is accepted or handed in. Sku therefore keeps a quest ID to index map, re-verifies the remembered index on every read, and drops the map on the quest events; without that cache the "quest database, all" list would run a full quest-log scan per quest. Turning tracking on goes through AutoQuestWatch_Insert with QUEST_WATCH_NO_EXPIRE so the quest lands in QUEST_WATCH_LIST and no expiry timer can remove it again; turning it off removes the QUEST_WATCH_LIST row first and only then calls RemoveQuestWatch - exactly like Blizzard's own click path, or a long-untracked quest keeps counting against the limit of five. QuestWatch_Update only runs outside combat, because it ends up moving protected frames; the next QUEST_LOG_UPDATE runs it anyway.

New keybind "Target nearest quest mob" (Alt+H)

Simple: One press targets a creature that belongs to a quest objective you still have open - including one you do not have to kill at all, but which merely drops an item you need. Example: for "Essence for the Engines" the quest log only names the item; the key still targets the Mana Wraith that drops it. If nothing matches in range Sku says "no quest target in range"; if the quest log holds nothing suitable at all, "no quest target in your log". The key is bound to Alt+H out of the box and can be rebound under Sku keybindings, "Target and soft targeting".

Technically: The idea and the reference implementation come from ZenqFR, in the companion addon SkuQuestTarget (github.com/ZenqFR/Sku-QuestTarget); here the feature was rebuilt natively with ZenqFR's agreement, so no extra addon is needed. Candidates come from your own quest log via SkuQuest:GetQuestTargetIds; item objectives are traced back through the drop source (npcDrops, and one level deep also itemDrops) to the creatures that drop them. The search looks in the zone you are standing in first - a creature with no spawn there cannot possibly be in targeting range and must not take a slot in the macro; only when nothing is recorded for your own zone does it sort across the whole continent. Distance uses the nearest recorded spawn point, not the first one in the list. The macro is multi-line, and one quirk of the game decides the outcome: every matching line fires and each match overwrites the previous one, so the LAST one wins. Candidates are therefore included near to far, so the length budget can only cut distant ones, but written out reversed, so the closest candidate is the final line. It is rebuilt on every press out of combat; in combat the last built text fires.

"Next enemy in combat" now finds things you are not facing (Ctrl+H)

Simple: The key could only ever find what you were already looking at - with the same enemy five metres behind you it reported nothing. That is not a setting, it is how the game's tab targeting works. So Sku now remembers the names of hostile creatures you come across and additionally tries them by name - and a name works in every direction. In practice: anything you have seen, fought or targeted once stays reachable from anywhere for two minutes, including with your back to it. The search now also restarts at the nearest enemy on every press instead of cycling onwards. The key is bound to Ctrl+H out of the box.

Technically: The approach is borrowed from ZenqFR's SkuQuestTarget, which uses the same route for its secondary function. /targetenemy is tab and searches the view cone - nothing can change that. "/tar name" instead searches the client's name list and is direction-independent, but it needs a name, and there is no interface that enumerates units behind you. Sku therefore collects names continuously from four sources: hostile nameplates, your own target, the unit under the mouse, and the combat monitor's enemy list. Collection runs on a one-second tick while the key is bound, because a nameplate only exists while the creature is in view - which is exactly when you do not need the key. Only names are collected there; the expensive range measurement runs once per key press. Remembered names last two minutes. The macro is "/cleartarget", then "/targetenemy", then the remembered names far to near - the last matching line wins, so the nearest one.

New "Nearby quests" list

Simple: The quest menu has a third entry now, after "Current Quests" and "Quest database": "Nearby quests". It holds your whole quest log in ONE list - started and finished mixed together - sorted by how far away the thing that currently matters for each quest is. Every entry names the distance first, then the compass direction, then the kind, then the quest, so you hear "180 Meters; north-east; quest target; Thieving Kobolds" or "340 Meters; south; Turn in; The Wolf Pack Leader". For a quest with open objectives that

is the distance to the nearest objective; for one reported complete, the distance to the turn-in. "Current Quests" groups by the quest log's own headers and therefore sorts by zone - this list answers the other question: what is nearest from where I stand. Enter opens the same quest menu as everywhere else, waypoints and the "Track quest" entry included. The list is OFF out of the box and is switched on under Sku settings, SkuQuest, "show nearby quests entry": it recomputes the whole quest log against Sku's spawn database every time it opens, and only someone who uses it should pay for that. Where Sku's data yields no location the entry reads "Location unknown" instead of a distance and sorts to the end - it never disappears.

Technically: Idea and reference implementation by ZenqFR, from the companion addon SkuQuestNearby (github.com/ZenqFR/Sku-QuestNearby); rebuilt natively here with ZenqFR's permission so no extra addon is needed - in particular without Questie and without GatherMate2. The maths lives in the new, stateless file SkuQuest/Proximity.lua. Two tiers, and the order is the point: first the nearest recorded spawn point IN the zone you are standing in - the selection runs cheaply in map coordinates and only the winner is converted to world coordinates once; only if that yields nothing, the minimum across all zones of the same continent. A different continent is not an imprecise distance, it is no distance, and stays out. Item objectives are traced back to the creatures and world objects that drop them, explore objectives to their recorded coordinates. Unlike the waypoint path, ALL of a quest's objective lists are read, not just the first non-empty one - a quest with "kill eight wolves AND collect five pelts" would otherwise only ever show the wolves. Filtering is by objective KIND only: which kinds are still open comes from Blizzard's own progress display ("monster", "item", "object"), and that maps one-to-one onto the three lists; matching a progress line against an individual database entry would be guesswork, because neither order is bound to the other. At most twelve ids are evaluated per quest and kind - the limit here is the hardcore realms' script budget, not the last decimal. The compass direction comes from the same world coordinates through the same helper the waypoint lists and the minimap scanner use. Deliberately a compass bearing and not a clock position: a clock position is relative to where you are facing and would be wrong the moment you turn - but the list is built once and then read through.

Quest database: "Starts in zone" without the extra level

Simple: "Quest database, Starts in zone" held exactly one entry, "By distance", and only inside it the quests. A level with nothing to choose is just a keypress. The list now sits directly under "Starts in zone". That list was also broken: it broke off after the first quest, so you got exactly one entry instead of all the zone's quests.

Technically: The extra level existed because a second sort ("by difficulty") was once meant to sit beside it; that construction site sat commented out next to it for years and was never built. The break came from a line "tcount = tcount + 1" in the list builder: tcount was not a local there but a global that is never set, so incrementing it raised on nil. An error inside a menu's child build is swallowed - the build therefore ended silently after the first entry. Nothing ever read the counter; it is removed rather than initialised.

Everything in the bank in one list, and the bank is recognised under Bagnon too

Simple: The bags menu had "all bags" - one flat list across every bag, sorted by name. The bank had no such thing: at an open bank the menu did show "Bank" and "Bank Bag 1" through "Bank Bag 7", but looking for something meant walking into each compartment in turn. Now, while the bank is open, a new "all bank" entry sits directly after "all bags": everything lying in the bank in one list, with the same type-ahead filter. Ctrl+Enter moves an item from there into your bags - exactly as it already did inside the individual bank bags. On top of that, Sku now works out whether the bank is open from the game's own messages rather than from Blizzard's window. Anyone using a bag-replacement addon such as Bagnon, which hides that window, had no bank in the menu at all until now - Sku considered it closed and let every bank container disappear. No bridge addon is needed for that any more.

Technically: The suggestion comes from ZenqFR's companion addon SkuBagnonBridge (github.com/ZenqFR/Sku-BagnonBridge), which closes the same gap in its own way and likewise recognises the bank from the game's events; with ZenqFR's agreement the part that needs no further addon is built natively here. The flat list was limited to bags 0 to 4; the bank containers (-1, -3, 5 to 11) were never copied into it. They now collect in a list of their own, deliberately kept separate from the bag list: SkuCore.combatBagOrder derives from that one - the mirror used in combat - and it can only ever use bag slots. The entries in the new list are copies of the same per-slot entries and carry bag and slot with them, so every existing action works unchanged. The bank's state now hangs on BANKFRAME_OPENED and BANKFRAME_CLOSED: both come from the interaction with the banker itself and are independent of who draws the window. It is reset on load and after /reload, and the old window check is kept alongside - so the detection can only ever get more cases right than before, never fewer.

Deleting a custom waypoint cleared the wrong entry

Simple: Deleting a waypoint you had created yourself did not remove its own id from an internal lookup table but that of a different waypoint. And "/skucheck wp" reported four errors on the four quick waypoints although nothing was wrong with them.

Technically: A waypoint's wpId is its identity - the link table is keyed by it - not a description of where it is. The area id packed into it dates from the moment the waypoint cache was built and stays put when a custom waypoint moves; that is exactly what happens to the four quick waypoints at every loading screen, because they are re-set to the player position then. The check rebuilt the id from the record's current areaId and compared - it now uses the area packed into the id itself. The delete path built the id from ".areaid" (the field is areaId), so it computed with area 1 instead of the real one and cleared the entry of whichever record really owns that key; it now uses the record's stored id.

The chat input has become usable

Simple: The chat line was the only input field in Sku you typed into blind. There was a sound on opening and one on closing, and nothing else - no character echo, no reading under the cursor, and no word about where the message would actually go. It now behaves like every other Sku input field. Every typed and deleted character is spoken, arrow left and right read the character under the cursor and with Ctrl the whole word, arrow up recalls the messages you sent last - to send again or to change

where it would go. The channel is also announced on opening and on every change, but deliberately not for "Say": that is the harmless case, and saying it every time would be noise. If the announcement does not suit your workflow, turn it off in the chat settings under "announce the chat input channel"; it is on by default. Turning it off also drops the channel in front of a line recalled with arrow up.

An item in the chat counts as a single character when reading instead of sixty-five characters of markup. And a mistyped command such as "/tets" now says "unknown command" instead of simply doing nothing.

Technical: The keyboard echo of Sku's own fields now attaches to foreign ones through SkuOptions:AttachInputEcho. Three things differ there: it attaches with HookScript rather than SetScript, because Blizzard's field carries its own handlers for history and name completion; the template sets ignoreArrows, which is why plain arrows went to the game and the text cursor needed ALT - that is turned back; and it arms on focus, because a foreign field has no open-and-close pair for the echo to hang off. The channel is read from the input's header rather than rebuilt from chatType - the header is already translated and covers whisper targets, numbered channels and Blizzard's own switches mid-call. Its announcement runs deferred and in a single slot, because every channel change is triggered by a keystroke, and that keystroke's character echo used to throw the announcement out of the queue every time before it was ever spoken.

Item links are read as their name, not as colour codes

Simple: An item - in chat, in the mail, in loot - was read out as a long run of colour codes, identifiers and colons instead of simply as its name.

Technical: The voice output first replaced every vertical bar with a space and THEN called the function that strips colour codes, links and textures. All of its patterns look for a vertical bar, which no longer existed by that point - so they had been matching nothing all along. Order swapped; the blanket replacement is now only a safety net behind it.

Quest database: "Starts in zone" also names the compass direction

Simple: The list of quests that start in this zone used to say only how far away the quest giver is. Now, like the "Nearby quests" list, it also says which direction that is - distance first, then the compass direction.

Technically: The quest giver's world coordinates were already in this list; the direction comes from the same function the waypoint lists use. Deliberately the compass direction and not a clock direction: a clock bearing is relative to where you are facing and would be wrong the moment you turn, while this list is built once and then read through. An entry's quest id now comes straight off the row instead of a lookup table whose key was the old label text without the direction.

Keyboard echo: characters are spoken instantly

Simple: While typing into an input field the letters did not keep up with your fingers: some were swallowed, others arrived noticeably late, and doubled letters as in "letter" or "all" were regularly heard only once. Every typed character now arrives immediately, and a key pressed twice is spoken twice.

Technically: The echo used to run through the same queue as every menu announcement, and that queue is built for announcements, not for keystrokes. It collected characters for a tenth of a second and spoke them with overwrite - and overwrite here means: first StopSpeakingText, then wait another tenth of a second so that stop does not wipe out the very line it was meant to make room for. Those two in series are about 0.2 seconds per keystroke, and one keystroke's stop regularly landed on the next one. On top of that came this version's new back-to-back duplicate guard, for which a single character is an utterance identical to itself - hence the swallowed doubled letters. Typed characters now take their own fast lane: one slot in which the newest keystroke wins, handover on the same frame, the stop issued once at the start of a typing burst instead of once per keystroke, and the duplicate guard no longer sees typed characters at all. Background: a screen reader never needs any of this, because its own interface offers cancelling as part of the speak call - one single operation. The game's interface cannot do that, so Sku has to rebuild it from two calls that can overtake each other. What is new is doing that only once per typing burst.

A line asked for again by keypress is spoken again

Simple: Asking to hear a menu line again with a keypress sometimes produced silence - the speech output considered the repeat redundant.

Technically: The duplicate guard discards a line that was already spoken identically within one second. That is right for the redundant echo of a menu rebuild and wrong for the same text the user just asked for with a key. The two cannot be told apart by their text, and no time window separates them - only what triggered them. The menu's key handler therefore now marks its announcement as key-triggered, and the guard always lets marked lines through. Redundant repeats are still discarded, now regardless of how fast they arrive.

Quests with mixed objectives show every objective kind

Simple: For a quest like "kill 8 wolves AND collect 5 pelts", the "Quest target" entry in the quest menu only showed the first objective kind - usually the enemies; the places to find the collectible item, or an exploration point, were silently missing. 95 quests were affected. All objective kinds now sit side by side in one list, and the "Target nearest quest mob" keybind also knows both halves of such quests. Fixed along the way: a quest that has only an exploration point and no other objective data now gets a "Quest target" entry at all, and an unresolvable exploration point no longer produces an entry that just announces "empty".

Technically: SkuQuest:GetQuestTargetIds was an elseif chain over the quest database's objective lists (creatures, else objects, else items, ...) and could only ever return ONE list with ONE type by its very signature. Replaced by GetQuestTargetGroups: an array of {targets, type} groups - creatures merged with deduplicated kill credits, objects, items and, for the objectives callers only, the quest's triggerEnd point. All seven call sites are converted (quest menu, waypoint cache, quest markers, quest-target key); the old name remains as a first-group wrapper for the WowVision port. The item branch of

GetResultingWps is also guarded against unknown item ids now - it indexed unchecked so far and let the swallowed menu build die as a silent "list empty".

Quest database brought up to Questie's current data

Simple: Sku's built-in quest database comes from Questie and was about two years old. After the sync, 33 quests whose "Quest target" used to stay empty have real targets (the Darkmoon card decks, the repeatable Ogri'la and Violet Eye quests, "Finding the Keymaster"), the six "Investigate the Scourge" quests now have exploration points, five missing quests were added, and about 2000 records carry smaller corrections to names, levels, quest givers and prequest chains. Verified: the corrections are identical for Era and TBC, the update is safe on both.

Technically: A semantic merge, not a byte copy (new tool

Event quests no longer show up right after loading

Simple: In the first seconds after a /reload, quests of inactive world events could appear in "Starts in zone" - Midsummer quests in August, say. Once Questie had finished loading they vanished again by themselves; they are now hidden in that window too.

Technically: The two availability gates only consulted Questie from Questie.started on, and that flag flips at the very END of Questie's initialization. Questie's static blacklist QuestieCorrections.hiddenQuests - removed quests plus ALL event quests; the active ones are only unhidden after the server's calendar answer - exists much earlier though. IsQuestieUnavailable now reads it directly before started: a plain table read; the memoize-poisoning hazard behind the started gate is specific to IsDoable. The value "HIDE_ON_MAP" is skipped just like Questie itself does, and every pre-start hide writes a dprint line to the log.

Quests in dungeons lead to the entrance instead of nowhere

Simple: For about 300 quests the quest giver, the target or the turn-in sits inside a dungeon - "Vorrel's Revenge" starts in the Scarlet Monastery, "Willix the Importer" ends in Razorfen Kraul. Quest start, Quest target and Delivery used to be empty there, because instances have no world map. Those entries now read "NPC, entrance, dungeon name, zone" with a route to the dungeon entrance - battleground quests included (Alterac Valley picks the right entrance per faction). Drop sources of an ITEM inside the same dungeon collapse into a single "dungeon name, entrance, zone" entry - "The Fate of Ramaladni" otherwise named 29 Naxxramas trash mobs one by one, all leading to the same gate; kill objectives keep their individual mob names.

Technically: New generated table SkuDB.InstanceEntrances (from Questie's dungeon table, tool dev/rework-docs/_gen_instance_entrances.py): instance areaId -> entrances {parent zone, x, y, optional faction}, including the wing sub-ids and a GetBuildInfo gate for the WotLK entrance moves. The spawn resolvers (CreatureIdHelper, the object and item branches of GetResultingWps) no longer skip map-less areas; they answer like the v43.2 triggerEnd detour: convert the entrance coordinates to world coordinates, take the nearest route waypoint. Memoised per waypoint-cache generation; the faction filter reads UnitFactionGroup.

77 field-objective quests now have a real quest target

Simple: 77 quests whose objective is a place or object out in the field (the Mulgore water wells, the druids' Moonkin Stone, the Crystal Pylons of Un'Goro Crater, Rolf's corpse, the four draenei totems, the hunters' Taming the Beast chain and many more) had no target data at all - the "Quest target" entry was simply missing. All of them now have a real, routable quest target. Important: the objects and creatures come from Sku's own spawn data, but 26 locations come from web sources (warcraft.wiki.gg, Wowhead) and could not all be checked in game. They were chosen with care but may be off in individual cases - a report on whether such a target is right or wrong helps a lot. Even an imprecise point beats none at all: the route leads into the right area.

Technically: Result of the review list dev/rework-docs/QUEST-TARGET-PROPOSALS.txt (sections 1-3, reviewed by hand): 27 object and 24 creature targets as objectives entries in quests_fixes.lua (ids validated against the own databases, spawn zones verified, no -1,-1 positions in the open world) plus 26 triggerEnd coordinates from the web research. Two quests (9410, 10166) already carried the right object id in extraObjectives, which the target menus do not read - the objectives field was added there.

Thirteen talk-to quests have a turn-in again

Simple: Thirteen quests - above all the old priest spell quests (Stars of Elune, Elune's Grace, A Lack of Fear, Hex of Weakness), plus among others "The Tome of Nobility" and "Ally of the Netherwing" - shipped without a turn-in in the data: neither a target nor a delivery point, nothing was routable. The Delivery entry now leads to the NPC the quest text itself names.

Technically: finishedBy entries in quests_fixes.lua; the creature ids were resolved from the quest text against the creature database (every name is unique there, and the spawns lie in the zone the text names). The candidates come from the new tool dev/rework-docs/_quest_target_candidates.py, which classifies every target-less quest and writes the review list QUEST-TARGET-CANDIDATES.txt for the remaining ones (real field objectives without a coordinate source).

Experimental: gather routes

Simply: You pick a resource - an ore, a herb, a gas cloud or a kind of chest - and Sku walks you through the zone node by node, always the nearest one first. Where the path data covers the node it takes a real route; where it does not you get a straight beacon and Sku says so - a beacon can point through a wall, a route cannot. While flying it never routes: up there the straight line is the right answer, not the fallback.

At the node Sku waits briefly while you gather, then moves on. The keybind "next route waypoint" (Ctrl+Shift+W by default) skips the current node while a gather route is running, so no new key is needed.

With the matching tracking active (Find Minerals, Find Herbs) the minimap scanner checks on approach whether the node is actually still there, and skips mined-out ones with an announcement of its own. Without tracking the route still runs, just without that check.

Found under Settings -> Scan -> resource scanning -> Gather routes, together with the check radius, the number of nodes per route and the switch for the check itself. For ore, herbs and chests "Show herb and mining node waypoints" has to be on in the navigation settings - otherwise those nodes do not exist as waypoints at all, and Sku says exactly that on start rather than wrongly declaring the zone empty. Gas clouds do not need the setting.

Important: This feature is explicitly experimental. It has only been tried in game in outline, and the "is the node still there" check could not be exercised under real conditions at all, because no character here has a gathering profession - without Find Minerals or Find Herbs there are no blips on the minimap for the scanner to read. If you play Mining or Herbalism: please try it and report what goes wrong.

Technically: The idea and the reference implementation come from ZenqFR, from the companion addon SkuGatherRoute (github.com/ZenqFR/Sku-GatherRoute); the route mechanics were devised and demonstrated there - a waypoint family under one shared base name, every node reached over a close route, a distinct announcement for every transition, and the existing keybind as the skip key. Here the feature is rebuilt natively with ZenqFR's agreement, so that no additional addon is required.

One difference is deliberate: the original requires GatherMate2, this version does not. We see no advantage in that dependency - the nodes have long been in SkuNav's waypoint cache complete with world coordinates, and named so that the resource IS the waypoint's base name; a second data source and a second addon would add nothing to that while costing every user another install. Should GatherMate2 do something we have overlooked - self-recording of nodes actually gathered would be the candidate - then please tell us what for, and we will look at it.

The presence check hooks MinimapScanner:MinimapScanFastStop, the one point every scan funnels through. With "notify on resources" on, the scan that is running anyway is enough; otherwise the route scans by itself, throttled and only within range of the target, and those scans stay silent. A hit confirms, a miss proves nothing - a scan only ever reports one name and may have caught a different blip. Only after several unsuccessful scans in range does a node count as gone. When in doubt it never skips: a node wrongly kept costs a few steps, a node wrongly skipped is never noticed at all.

The menu stays usable while you walk

Simple: If you opened the menu while walking, it came up and said "menu opened", but no key did anything any more - no arrow, no letter, no Enter. Only Escape got through. It only became usable once you stood still. That is fixed: an open menu now accepts keys whether you are walking, running or auto-running.

Technically: There were two movement gates, not one. The first prevents the menu from OPENING while a movement key is held down - that one stays, and it is needed: the menu's bindings would swallow the key-up and you would run on forever. Auto-run has been exempt from it since v42.00 (its own AutoRun flag, which IsPlayerMoving deliberately does not read). The second sat in the menu's own key handler and, as long as a movement latch was set, let every keypress except Escape expire. That second gate buys nothing: once the menu is open the bindings already own the keys, so discarding the press stops no movement - it only deadens the menu, and on top of that stamps "open later" on a menu that is already open, which the reopen loop then has to clear again. It now applies only when the menu is NOT open. The four remaining open-gates also write a line to the debug log naming the movement key that is holding them - before, they were silent, and a report of "the menu would not open while walking" left no trace at all.

Navigation: "Current map by distance" now comes first

Simple: Under "Direct waypoint", "Recent" was first and "Current map by distance" second. The two are swapped - the list of waypoints in the current zone is the more common way in, and it now sits directly under the cursor.

Technically: Purely the order of the injections in SkuNav:MenuBuilder. "Recent" remains tied to a non-empty list of recently used waypoints; when that list is empty, "Current map by distance" is first anyway, as it was before.

The type-ahead filter ignores accents

Simple: In every list you can narrow down by typing, accented letters were treated as characters of a different language. On a French client "general" found nothing even though "Fournitures generales" was right there, and "ingenieur" did not find "Ingenieur" - while "ing" did. German was hit too: an entry starting with a capital umlaut could never be reached through the filter. Now the letter counts, not the accent. On top of that, the u key on a French AZERTY keyboard now types into the filter as well - it is the only accented letter that keyboard delivers as a real key event.

Technically: string.lower in Lua is byte-wise and maps A-Z and nothing else, so it never folds an accented character on either side of the comparison. SkuMenuFilterMatch (used by both ApplyFilter and JumpToFilterMatch) and SettingsSearchMatch now fold the entry name and the typed query to unaccented ASCII before comparing; only 2-byte Latin-1 Supplement and Latin Extended-A sequences are touched, and pure ASCII takes a fast path. Strictly more permissive than before: nothing that matched stops matching. The two string.find calls also pass plain=true now - the typed query was being read as a Lua pattern, so an input like "50%" or "(long" found nothing or threw. The accent folding comes from PR #9 (ZenqFR), the u key from PR #13 (Naxedim).

Action bars: the spell tooltip is read out again

Simple: In the action bar menu (Shift-F11), Shift-Down on a spell in the assignment list read out nothing at all. The tooltip is back - as it is for a key's current content and for the two pet ability reads next to it, which had the same weakness.

Technically: Two independent causes, both addressed because neither could be ruled out from the code alone. First, SkuScanningTooltip is created once at login with no template and shared by every module; whatever used it last can leave it without a usable owner, after which every Set* call populates nothing - silently, with no error. It is now re-owned immediately before being populated. Second, SetSpellByID was handed the third return of GetSpellBookItemName, which on this client follows the Classic signature and returns only (name, rank) - the id is a retail addition and was nil. The id now comes from GetSpellBookItemInfo, and the tooltip is populated with SetSpellBookItem, which needs no id at all. The read is also pcall'd, since it runs on every cursor move. From PR #10 (ZenqFR).

Quest target: the place was in the data, but never found

Simple: 174 quests that ask you to explore a spot or use something there answered nothing but "Empty" under "Quest target". The entry now leads to the nearest route waypoint at that place.

Technically: GetTriggerEndWps composed a waypoint NAME

French: the waypoint comments speak again - and now in French

Simple: The comments on waypoints - among them the safety-critical ones such as "careful, from here the route runs along the edge of a gorge", "wait here for the zeppelin" or "this NPC moves" - had been completely silent on a French client since v42.11. They speak again, and they now speak French: 207 texts sitting on 546 waypoints.

Technically: The route data carries lComments for enUS and deDE only (~296 waypoints). SkuNav:PlayWpComments read comments[Sku.Loc] with no fallback. Until v42.11 Sku.Loc was "enUS" on a French client because no frFR locale existed, and you heard the English texts; since locales/frFR.lua ships, Sku.Loc is "frFR", the lookup is nil, and the function returns without a word. New Sku.locList (SkuUtil.lua), the list counterpart of Sku.locStr, returns the first NON-EMPTY list among Sku.Loc / enUS / deDE. Non-empty matters, because SkuMM.lua materialises an empty comments[Sku.Loc] on every waypoint it draws - a plain or chain would latch onto that. The translation is made from the GERMAN original, not via the English: the English lost detail on exactly these safety texts (the gorge warning without "you walk on the edge" and without the direction of travel, "extremely narrow bridge with a ravine" reduced to "extremely narrow bridge"), and several English entries are empty, including the lethal-elevator warning that sits on eight waypoints.

French: resource names and the Cave of the Mists

Simple: Two faults that hit a French client only. First, 22 of the 94 minimap scanner resource names were wrong - the scanner matches them against the game's tooltip, so a wrong name means that node is never announced, with no error. Second, inside the Cave of the Mists the waypoint list stayed empty, while a route started outside kept running.

Technically: Each of the 22 names was adjudicated against a source independent of both addons: 19 through SkuDB's own frFR tables (imported from Questie's Blizzard strings), three through Wowhead's tooltip API. For the cave, Geo.lua compares GetMinimapZoneText() verbatim against L["Cave of the Mists"]; the generated frFR locale held "Grotte des Brumes", a free translation - the client says "Caverne des Brumes". The compare was therefore dead, uiMap stayed on the continent, GetAreaIdFromUiMapId answered -1 and GetCurrentAreaId nil, so ListWaypoints2 bailed out. The value was read with /szp in the zone, not translated, and now sits both in the generated file and in the TSV store it is assembled from - together with the note that it must not be translated freely. From PRs #14 and #16 (Naxedim).

Turn to beacon: more accurate, and with lead

Simple: The automatic turn to the beacon (T) now lands much more accurately. It used to run so fast that stopping it on a frame boundary alone could cost twenty degrees of overshoot or more, depending on your frame rate. The speed now follows the size of the turn, and no turn takes longer than a quarter second. While you are moving it also leads: it aims at where the waypoint will be when the turn ends - which ends the circling around nearby waypoints that showed up mostly when mounted and in flight. And a press during a turn that is already running is discarded instead of aborting that turn halfway; pressing repeatedly still refines as it always did.

Technically: Four parts, tested together. The snap to the default camera before the turn stays - it is load-bearing for the yaw, without it the angle is wrong. The turn speed is capped from the remaining angle instead of running flat out. The lead computes from the current speed and the expected duration of the turn where the waypoint will be at the end. Plus a guard against the fault where, after turns triggered in quick succession, the camera keys' turn speed could stay quadrupled for good.

The installer remembers where your WoW is

Simple: The installer looks for the game in the usual places - Program Files, plus a handful of likely folders on C, D and E. Anyone whose WoW sits anywhere else had to browse to the folder by hand, and not once but on EVERY update: the installer forgot the answer the moment it closed. It now writes the folder down after an install has gone through, and starts there the next time. Showing it one client also finds the other one beside it, so a Classic Era next to an Anniversary on the same unusual drive is picked up by itself. If the game later moves or is uninstalled, the note is dropped and the search runs as it did before. The installer is version 4.5 with this.

Technical: New PathMemory.cs. The install manifest could not serve for this - SkuInstall.json lives INSIDE the AddOns folder, so reading it presupposes having already found the folder we are trying to remember. Each client's folder is therefore written as a plain "product=path" line into SkuPaths.txt, in two places: %LOCALAPPDATA%\SkuUpdater, beside the persistent updater copy the "Sku Updater" shortcut launches, and %ProgramData%\SkuUpdater as a machine-wide mirror. The mirror is not redundancy for its own sake: the installer requests elevation, so a standard user who answers the UAC prompt with someone else's admin credentials runs the whole program under that other profile and would otherwise write the note into a LOCALAPPDATA they never see again. On the next run a remembered folder outranks detection - it is a decision, while detection is a guess over a short list of drives - and its base directory is prepended to the probe list, which is what makes remembering one client pay off for the other. Guards against a stale note: the folder must still exist, and its .flavor.info must not now report a different client; either way the note is ignored rather than sending an install somewhere dead. Written only after a client has installed cleanly, so a cancelled or failed run leaves nothing on record. Plain text on purpose - it can be read out and corrected in Notepad, and deleting a line restores auto-detection for that client. New headless check "SkuSelfTest pathmemory": it prints what the machine has on record and round-trips the write, read and stale cases against a throwaway store, never touching the real one.

The reading window can be configured

Simple: Quest text, tooltips, the chat history and the wiki pages all end up in the same reading window. How that window looked was fixed in the code: Playfair Display at 12 point, a thin serif face at the smallest size the addon uses anywhere. For someone with residual vision that was the most important surface in the addon and at the same time the hardest to read. Settings -> Visual aids -> Text window now offers font sizes from 14 to 44 point, four fonts, four color pairs (white on black, yellow on black, black on white, white on blue), the background opacity and an outline. Out of the box the window now runs in a sans serif face at 18 point, so even without changing anything it is considerably easier to read than before.

Technically: The surface itself belongs to Libs/SkuTTS-1.0 (frame SkuTTSMainFrame with its FontString .FS), where the appearance was a single hardcoded SetFont call. The settings now live in SkuCore VisualAids under visualAids.textWindow, and SkuTTS:Output calls VisualAids:TextWindowLayout before each show - pcall-guarded and in the same direction in which SkuZOptions already feeds the reading bar. That way the lib needs to know nothing about the options, and a disabled Visual aids module simply leaves the window looking as it did. Every font assignment is checked and falls back to the client's standard font rather than leaving a blank surface - the two bundled families Raleway and Playfair live under Libs/SkuTTS-1.0/fonts and ship only in the release ZIP. The size steps are deliberately smaller than the reading bar's: a whole quest text goes here, and the window has no scrollbar, so it clips at the bottom sooner the larger the font gets. Everything is still spoken in full; the display is the second track.

The reading bar shows what Sku says - and it works in combat

Simple: Two things at once. First, the bar stayed empty in combat, which is exactly when reading along matters most. Second, it showed only the name of a menu entry: on an on/off switch you got the name but not the state, on a bag line the name but not the count - everything Sku additionally reads out was missing from the bar. Both are fixed; the bar now shows the same line Sku speaks, and it does so in the middle of combat too. On top of that its font and color pair are now selectable as well, with values of its own alongside the text window's.

Technically: The combat lockout was a confusion of visible with open: the check hung off SkuOptions:IsMenuOpen(), which tests OnSkuOptionsMain:IsVisible(). That frame is an ancestor of the secure ENTER buttons and therefore protected, so its Show() is blocked in combat - the combat menu deliberately runs headless and reports through SkuOptions.combatMenuActive instead. templates.lua and SkuZOptions already check exactly that pair; the reading bar simply did not know about it. The bar itself is an ordinary frame with no secure children, so its Show() was never the problem in combat. Because the combat path only closes the menu logically (the frame Hide is protected there) and does so at six different sites, the bar now hides itself: an OnUpdate checks every 0.25 s whether the menu is still open - WoW runs OnUpdate only on visible frames, so it costs nothing while hidden. The content was simply a discarded parameter: the caller always passed the complete spoken string, yet currentMenuPosition.name was displayed instead. The passed string now takes precedence, and the semicolons of the speech separator become dashes for display.

Nameplates: the settings the game hides

Simple: The client has long been able to draw its nameplates bigger, more colorful and with more contrast - it just never offers it. Blizzard's own options window shows not one of these settings in Classic; the checkbox for larger nameplates that current WoW has is explicitly left empty in Classic. Without Sku you could only reach them through console commands. The new menu under Settings -> Visual aids -> Nameplates makes them available: nameplate size, target emphasis, dimming of all other plates, class colors for enemies and allies, and names only for friendly players. Unlike the older nameplate coloring this does not paint something behind the plate but changes the real plate itself. Out of the box everything sits exactly where the client ships it - choose nothing and nothing changes.

Technically: Pure CVar settings, no frame of our own, no taint. Nameplate size sets nameplateMinScale and nameplateMaxScale together (setting them apart would produce distance-dependent scaling, which nobody wants here), emphasis sets nameplateSelectedScale, dimming nameplateMinAlpha, plus nameplateShowClassColor, nameplateShowFriendlyClassColor and nameplateShowOnlyNameForFriendlyPlayerUnits. Every value and the existence of each individual CVar was measured against the live client 2.5.6.69546, and the 2.0 ceiling checked against the client's clamping. Important for future work: the extracted BlizzardInterfaceCode in the client folder is a March/April 2026 dump and therefore older than the July nameplate change; it names retail CVars such as NamePlateVerticalScale or ShowClassColorInNameplate that do not exist in this exe at all. The exe strings are what counts. CVar writes are blocked in combat and fail silently, so a change made there is remembered and applied at PLAYER_REGEN_ENABLED. Sku re-applies the values at login, but only once something has actually been chosen in the menu - anyone who set their nameplates by hand through the console keeps them untouched.

Nameplate coloring survives a restart

Simple: If you switched nameplate coloring on, you had it exactly until you logged in again or reloaded the interface. After that the switch still read as on, but nothing was colored any more. The only way to get it going again was to flip the switch off and on by hand - once per session, every session. That is fixed; the setting now simply works.

Technically: Arming the event driver (NAME_PLATE_UNIT_ADDED, _REMOVED and PLAYER_TARGET_CHANGED) hung exclusively off the menu switch's OnAction. The stored value was read at load but applied nowhere, so after every login not a single event was registered. Arming now sits in VisualAids:OnEnable, where the follow warning and the next-enemy button have always been armed, with a delayed second attempt in case the profile is not up yet on the first pass.

A closed window is no longer announced after the fact

Simple: Learning several spells at a trainer, closing the window quickly and then opening your profession window brought the trainer window back after a while - sometimes only after you had read a tooltip. Sku remembered where in the menu it still meant to jump and did it later, without checking whether that window still existed.

Technically: The deferred menu jump is three fields (openMenuAfterCombat, openMenuAfterMoving, openMenuAfterPath) that used to be written independently. SlashFunc arms flag AND path together; other sites armed only the flag - and so inherited whatever path was still lying around. The path was also only cleared in the deferred part of CheckFrames, which bails out while you are moving and retries half a second later, whereas the release runs per frame and fires the instant you stop - a race the release won. The three fields are one unit now

(SkuCore:ClearDeferredMenuOpen), arming a flag drops the path, and the clearing happens in the window's Hide hook (SkuCore:GENERIC_OnClose) - exactly when the jump becomes invalid. Deliberately without a timeout and without a visibility probe: the state is cleared where it goes wrong, rather than outrun.

The menu no longer opens its top level by itself

Simple: Now and then the menu opened on its own - and at its very top, instead of where you wanted to go.

Technically: SkuCoreControlOption1 holds keybinds (target distance, panic mode, minimap scan, cancel flight, turn to unit) and has nothing to do with the menu. Its OnShow bails while you are moving or in combat because SetOverrideBindingClick is not allowed there and would swallow the key-up of a held movement key - but it bailed by setting openMenuAfterMoving, and the release, finding no path, turned that into a menu opening at its root. The frame has its own flag now (rebindControlKeysAfterBlock) and only gets its keys back.

Closing a second open window is noticed again

Simple: With two windows open at once - a vendor and the bags, say - only the first close was noticed. On the second the menu neither rebuilt nor closed.

Technically: One of the two hook installers wrapped Show and Hide around a single shared switch (SkuDropdownlistGenericFlag): any Show set it, and only the first Hide after that consumed it. Which installer got a window was pure timing - this one takes everything that exists one second after entering the world, while windows such as the trainer, the tradeskill and the auction house are created later and fall to the other. The switch is gone; GENERIC_OnClose coalesces on its own anyway.

No contents of a closed window under "Local" any more

Simple: After a window was closed its entries could stay under "Local" and turn up again the next time the menu was opened.

Technically: SkuCore.GossipList is the Local menu's data source, but it was only emptied when the menu happened to be open at that moment. On the Escape path it never is: the menu frame deliberately goes down before the windows, and the code responsible runs a frame later. It is now emptied as soon as no window is open.

Trainer: quiet once it is closed

Simple: Learning several spells in quick succession and closing the window right after left the trainer's announcements still coming.

Technically: Every click on "Train" queued a rescan after half a second and an announcement after 0.85 seconds, with no way to cancel them. Both steps now check whether ClassTrainerFrame is still visible at all. The v43.1 guard against speaking into a closed menu did not help here: as soon as the next window reopened the menu, it was open. The retry for silent rescans while moving also lost its silent flag and was scheduled once per click - it now carries its arguments along and runs only once.

Auction house: the result list really is sorted

Simple: Sorting by "Level descending" still gave you a list that started with low-level junk. The sort only ever worked inside one page - and one page is the 50 cheapest auctions. What stood at the top was the highest level among the cheapest grey goods, while the level 70 items sat hundreds of entries further down. Even after the finished sound, because nothing was ever re-sorted. Now the server sorts the result list by level itself, and equal levels are ordered by the price per item.

Technically: Every query went out with the server sort hard-wired to "unitprice", and the result list is built page by page, append-only (new items go to the end so the user's cursor never jumps). The order of the list is therefore always the server's, and our own sort only ever ordered one page. For "Level ascending" and "descending" the server answer is now sorted by the "level" column - the same column Blizzard's own level sort button sets. Buy and strategy buy stay on "unitprice": they depend on the cheapest offer being on page 0. So does a name search, where every hit is the same item: the server would have sorted by a field in which nothing differs and handed back its pages in its own order instead of cheapest first. On top of that, the level comparators now use the price per item as their second key; bid-only auctions count with their bid per item instead of a buyout of 0.

Auction house: "level" means the required level everywhere

Simple: The level in the results was sometimes the required level and sometimes the item level, depending on which list it came from and whether the item has a requirement at all. A stack of Netherweave Cloth (requirement 0, item level 55) therefore outranked a level 40 sword. It is now always the required level, the column the auction house itself calls "level". And it is only read out where it tells the entries apart: in a category or in the full-scan list. A search for an item name returns auctions of one and the same item - there it was the same number 800 times over. If you filter by level, you still get it announced everywhere.

Technically: One shared reader for both result lists: field 6 of the server answer, else the value from SkuDB. Field 7 is honoured - for recipes field 6 holds the required profession skill, for containers the number of bag slots, neither of which is a level. The item level is never substituted again. 0 now means "no level requirement" instead of "unknown", which is why no "Level 0" is announced any more (0 is true in Lua, which is how it got spoken in the first place).

Auction house: "usable only" no longer hides anything

Simple: In the full-scan list this filter left nothing but low-level goods in some categories - exactly the high-level items you were shopping for were gone. The normal search never behaved that way.

Technically: In the live list the server filters. The full scan fetches everything unfiltered and Sku decides for itself - until now strictly by the canUse field from the scan answer. The client can only fill that field when it already had the item data for the row; during a full scan it does not have it for the majority of rows. They are the very same rows for which name, level and quality have to be repaired - only canUse had no repair. So what disappeared was what the player has never seen, and that is mostly the high-level things. A "not usable" now only counts on a complete row; otherwise the required level from SkuDB decides. The database knows no class or race restrictions, so in case of doubt one item too many is shown rather than the wanted one hidden.

Auction house: no filter means no filtering

Simple: Even with no filter set, the full-scan list dropped everything without a level requirement - trade goods, materials, many recipes - and all grey items on top of that, although the quality menu kept displaying "Poor", i.e. no restriction.

Technically: The defaults of the unset filters were a 1 each instead of a 0, and the level was read raw from the scan answer. A row whose level the client did not know during the scan came back as 0 and thus fell below the minimum level of 1; the repair from the database only fired on a missing value, not on a 0. Both defaults are 0 now, the level comes from the shared reader, and the repair during ingest treats 0 as "missing". The user's own minimum level as a last-resort fallback is gone: a row stamped with it always passed the user's own filter and was then announced with that level as well.

Auction house: categories without subcategories list their items again

Simple: Under "Auctions by item" there were categories that contained nothing but "All" - quest items, for instance.

Technically: The item list compared the subclass even where there is none. A category without subcategories is built without a subclass, and the comparison is then false for every item. The full-scan twin has carried the guard all along; the item list does now too.

Auction house: a cancelled purchase stays cancelled

Simple: Cancel a buy, do something else, and out of nowhere the buy dialog could come back - and the next Enter, meant to open the search field, bought. At least it bought the right thing, but nobody had asked for it.

Technically: Cancelling dropped the timers and the key bindings, but not the buy target. The query function reads its mode off exactly that - there is a buy target, so this is a buy query - and so the user's next, completely unrelated query ran into the buy path: it found the old target in the fresh list, built the dialog and armed Enter. In the log of 2026-09-01 the cancel (18:45:40), the new search (18:46:12, immediately "MATCH FOUND, showing popup") and the purchase (18:46:16, "bid accepted") are four menu keypresses apart. The buy target is now cleared at the three points where the intent to buy ends: cancelling the dialog (including through the 30 second safety net), starting a new search or list, and closing the auction house. The buy path itself is untouched - it does not go through the search entry point, and the retry after a lost race keeps its target.

Auction house: an aborted search says so

Simple: When a search is cut short because the server stops answering, the half-filled result list stayed on screen and looked like a finished result. Now you hear "Search aborted, list incomplete".

Technically: The watchdog ends a paged search after 60 seconds without a server answer, or after 180 seconds in total - silently, until now. The finished sound is the signal for "complete", but its absence is not something you hear. It is only announced when the user is actually inside that result list; a search running in the background stays quiet.