v0.5.7
What's new
AppearanceMatic Change Log
[v0.5.7-release] - 09/02/2026
In-Game
- Adding or removing a route entry updates the panel straight away. It was trailing your click by a second and a half, sometimes nearly three, and the bigger your list the longer the wait.
Fixed
- The panel rested after a click as though it were an event storm.
RebuildWaitislastRebuildCost * REBUILD_DUTY(19) clamped to 3 s, which is right for a burst of loot or lockout updates but was also applied to direct actions, so a 78 ms draw put a 1.5 s gap between a route toggle and the redraw, and a 142 ms draw put 2.7 s. Confirmed from a live/am diag: three route adds atswitch=1227376.8, 1227415.9 and 1227563.1 redrew after 1.1 s, 0.6 s and 2.7 s, each exactly 19 times the preceding draw's 60 ms, 30 ms and 142 ms. The draws themselves were 23 ms to 142 ms; nothing was ever slow.ScheduleRebuild(urgent)now takes a direct action on the next frame (REBUILD_URGENT_WAIT, 0.05 s) while event churn keeps the duty cycle; an urgent ask pulls a resting draw forward and a token makes sure only the newest timer draws. Marked urgent: route item and raid toggles, clear route, pause and resume, the difficulty pick, waypoint and track row clicks, and the ignore-characters list.ToggleRouteItemhad no rebuild ask of its own at all and rode the star listener's background one; it now asks urgently like the raid header.tests/rebuild_loop_spec.luaREBUILD_LOOP_URGENT_PASS, REBUILD_LOOP_URGENT_PULLS_FORWARD_PASS, REBUILD_LOOP_URGENT_FOLDS_PASS.
[v0.5.6-release] - 09/02/2026
In-Game
- Every raid and dungeon header has a difficulty control: pick Normal, Heroic, Mythic or Raid Finder (the sizes for older raids) and the panel reads this week for that difficulty only. Status lines, the red down tag and the "everything done" line follow the pick. Leave it on All difficulties to count every difficulty an item drops on, as before.
- Bosses already down this week sink to the bottom of their raid's list and carry a red down tag, so what is still up sits on top.
- Dungeon lockouts count now: a Heroic or Mythic dungeon boss you killed this week shows as down like a raid boss.
- The "you are here" tag on the raid you are standing in is gone.
- The guide arrow stays outside. Step into a dungeon, raid, scenario or battleground and it goes away instead of hanging at the top of your screen pointing at the door you just walked through; it comes back when you step out.
- The arrow's arrival animation spins downward again. It was drawing the bottom of one arrow above the top of another after you reached a spot, lost your bearings and got them back.
- Blackhand tells you how to reach him: Blast Furnace, Kromog and the Iron Maidens must be down on the same difficulty this lockout, the bridge over the slag pool burns until then, and the flaming gate at the entrance is the Sigil of the Black Hand skip, not the way in. Once the sigil quest is done on any character, the line says to click a Blackrock Artifact beside that gate instead. With a difficulty picked for the raid the line answers for that difficulty; without one it names whichever difficulty already has the way open.
Fixed
- The guide arrow drew two half arrows in its arrival state.
state.arrivingwas a claim about which sprite sheet was on the texture, but three other paths swapped the texture without touching it (setSpriteModeleaving icon mode, the no-bearing branch,SetHop), so arrival cell maths (56 px cells) ran on the heading sheet (42 px cells) and the tex coords straddled two rows. The sheet is now recorded on the frame beside the texture that set it (setSheet), so the two cannot disagree. Repro, and the newtests/guide_arrow_timing_spec.luaGUIDE_ARROW_SHEET_SYNC_PASS: arrive on the frame the hop is set (never settingseenVector), lose the bearing, get it back. - The guide arrow followed the player into instances, where HereBeDragons has no bearing to give and there is nothing to walk to, so it froze green on "Arrived" for the hop outside.
Arrow:OnUpdatenow hides onIsInInstance()with a non-"none" instance type and logsinside an instance; becauseHidestopsOnUpdate, the arrow's watcher frame also takesPLAYER_ENTERING_WORLDandZONE_CHANGED_NEW_AREAand callsRefreshChrome, which no longer shows the frame while inside. GUIDE_ARROW_INSTANCE_HIDDEN_PASS. - The Raid Finder hint names the right wing. Three defects fed one wrong answer: the scan filtered on
subtypeID == LFG_SUBTYPEID_RAID(3), but Blizzard's own code accepts raid or flex-raid and the client table gives current Raid Finder wings Subtype 2, so difficulty (17 or 7) is now the only discriminator; the index kept only the first wing a boss name appeared in, so a whole-raid entry or another raid's wing could win; and the lookup ignored the raid asking. It now keeps every wing a name appears in, restricts them to the asking raid's own bosses, and picks the smallest, so a wing beats a whole-raid entry.tests/raid_finder_npc_spec.luaRAID_FINDER_WING_RAID_PASS, RAID_FINDER_WING_SMALLEST_PASS. /am diagprints anlfrwings:line naming every wing the game offers per boss of the guided raid, with each wing's boss count, so a wrong hint can be read off the client instead of guessed at.- The Legion Raid Finder hint pointed at the wrong city: Archmage Timear stands in Legion's Dalaran above the Broken Isles, not Dalaran in Northrend. Verified against warcraft.wiki.gg and wowhead; the other eight queue NPC rows were checked the same way and are correct, coordinates included.
Added
Lockouts:RaidDifficulty/SetRaidDifficulty(saved inAppearanceMaticDB.raidDifficulty[raidName]),RaidDifficultyOptions(union of the bosses' drop difficulties in difficulty order),EffectiveDifficulties(an item's difficulties narrowed to the pick when it drops on it),BossDown,CurrentSnapshot,SinkDownedBosses(stable partition ofbossOrder, down last),RaidDifficultyLink(w2tsdiff:<raid>, hidden with fewer than two options),IsDifficultyLink,HandleDifficultyLink(aMenuUtil.CreateContextMenuof radios, or a step to the next option where the menu system is missing).tests/lockout_group_spec.luaLOCKOUT_DIFFICULTY_PICK_PASS.- Locale keys
lock_diff_all,lock_diff_menu_title,lock_diff_pick_hint,lock_boss_downed(enUS/enGB now, the other locales at the release gate). - Boss gates of a new kind in
Data/Gates.luaBOSS_GATES:bosses(encounter ids that must be down on the same lock this lockout, English fallback names, journal names in game),skipQuests(any done, character or account, opens the skip),note/skipNotelocale keys. Blackhand (959) requires 1154 Blast Furnace, 1162 Kromog, 1203 Iron Maidens; skip quests 37029/37030/37031 (Sigil of the Black Hand N/H/M, account-wide since 11.0.5).Lockouts:BossesGateTextreads the current character's locks per lock group, honours the raid's difficulty pick, and lists the bosses still up on the closest lock. Locale keysgate_boss_bosses_blocked_fmt,gate_boss_bosses_open_fmt,gate_boss_skip_fmt,gate_boss_note_generic,gate_boss_skip_generic,gate_boss_note_blackhand,gate_boss_skip_blackhand.tests/gate_state_spec.luaGATE_BOSS_BOSSES_PASS.
Changed
RaidDoneForCurrentChargoes throughBossDown, so the raid's pick decides the done state.BuildContentsinks down bosses afterSortBossOrderand keeps the drawn raids inself.panelRaidsfor the difficulty control's click; the boss line gets the down tag;AddCharacterStatusand the single-drop route rows pass effective difficulties toBossStatusLines.SnapshotCharacterrecords every locked instance, not only raids, with anisRaidflag;LockoutGroupmaps dungeon ids 1/2/23 and raid ids 14/15/16 to the same normal/heroic/mythic tiers so a dungeon lock answers for an item's difficulty name.
Removed
lock_you_are_hereand the label on the header; the raid you stand in still leads the list and never sinks.
[v0.5.5-release] - 09/02/2026
In-Game
- The guide speaks each hop once: the same portal is no longer announced as an emote and then printed again a moment later. A line the guide holds back to keep the room quiet is not printed instead; the arrow and the map pin carry the change.
- Finishing the raid you are guided to now moves the guide on to the next raid on your route, even when that raid had already dropped to the bottom of the Routing list.
Fixed
- Route hop identity:
sameHoptreats two hops with identical text as one hop regardless of distance. The table's provisional spot and the game's confirmed spot for the Pathfinder's Den portal sat more thanSAME_HOP_YARDSapart, so the confirmed answer counted as a new hop and, inside the 30 s emote window, went out as a private print right after the emote (his screenshot).tests/route_tick_spec.luaROUTE_TICK_SAME_TEXT_SILENT_PASS. - Route voice: an aloud-worthy hop change held back by the 30 s emote limiter is no longer printed privately; it is one voice or the other, never both. ROUTE_TICK_EMOTE_RATE_LIMIT_PASS updated.
Lockouts:RouteAdvanceIfDoneonly inspectedrouteOrder[1]for a guided-and-done entry, but a raid that goes done sinks to the bottom of the Routing order, so the guide never moved on (Black Temple done, still guiding to Black Temple while Blackrock Foundry waited). It now scans the whole order.tests/route_list_spec.luaROUTE_ADVANCE_DONE_ANYWHERE_PASS.
[v0.5.4-release] - 09/02/2026
In-Game
- The try-on preview beside the wardrobe no longer stands in shadow: the character is lit evenly whichever way you turn it and wherever you happen to be standing.
- The star on wardrobe appearances and mount journal rows is bigger and easier to hit.
Fixed
- The wardrobe try-on panel's
DressUpModelnever set its own light, so it took the world's directional light with it and the character rendered dark at some facings and locations. It now installs the same ambient-only light Blizzard's wardrobe models use (SetLight(true, { omnidirectional = false, point = (-1, 1, -1), ambientIntensity = 1.05, diffuseIntensity = 0 }), fromWardrobeItemsModelMixin:OnLoad), once, when the model is built.tests/preview_light_spec.luaPREVIEW_LIGHT_PASS.
Changed
- Wardrobe star button 18 -> 31, its route icon 18 -> 25; mount journal star 22 -> 39, its route icon 22 -> 31.
[v0.5.3-release] - 08/31/2026
In-Game
- The Dalaran and Garrison stones speak only when truly needed: standing near a decent portal chain they stay quiet, and they step in only when using one saves serious travel or no portal chain exists at all. Your own Hearthstone and class recall behave as before.
- Standing in the city your hearth is set to never hears "use your Hearthstone" anymore; the stone only speaks up when it would actually take you somewhere.
- The moment your hearth lands, "Use your Hearthstone" leaves the guide and the route re-checks right away, instead of showing the stone for a few more seconds; a canceled cast just carries on.
- The last of the frame skips: the world-boss check no longer sweeps every zone in one gulp; it pays a few zones per frame, so the occasional single-frame hitch it caused (about 157ms every 30 seconds while a route ran) is gone.
- Stopping a route now takes its map pin with it; no more waypoint left behind that you cannot get rid of.
- The guide stops flip-flopping between flight points: a chosen "pick it up on the way" stop stays chosen while it is on your way, an announced flight keeps its landing while it still pays, and a detour never costs more than a stone's throw extra however long the ride.
Fixed
- Special stones (prio 2/3) now need a real win to be suggested: their score must beat the chain by more than three chain hops' worth (
R.SPECIAL_HEARTH_MARGIN, default 3 xCHAIN_HOP_YARDS), or there must be no chain at all; his diag case (Garrison 500 vs chain 5073 while beside Silvermoon's portals) goes silent, the stranded ambiguous-bind case still rescues. The bag stone and class recall keep the plain comparison.tests/route_spec.luaHEARTH_SPECIAL_ONLY_WHEN_NEEDED_PASS. - The hearth's same-continent suppression compared only physical world instances, and zones like Quel'Thalas live on their own instance (530) while the bind area logs the logical continent (Eastern Kingdoms), so a Silvermoon-bound player standing in Silvermoon was offered the stone:
HearthHopnow also walks the player's map parents to its continent map and excludes candidates matching that logical identity.tests/route_spec.luaHEARTH_HOME_CITY_QUIET_PASS. - The hearth-cast hold remembers the map the cast started on: ending on a different map (landed) clears the hearth hop, the arrow and the query throttles so the same tick recomputes fresh; ending on the same map (canceled) resumes without a flap.
tests/route_tick_spec.luaROUTE_TICK_HEARTH_LANDING_PASS. - The
ActiveWorldBossSetsweep is sliced across frames: with a profiler clock present it processes zones under a 4ms budget per frame (one slice perGetTimetimestamp), the last committed answer standing mid-scan and everything (vignettes, continent skulls, network union) committing atomically at the end; withoutdebugprofilestop(test harness) it stays fully synchronous.InvalidateIndexesdrops an in-flight scan. His second capture had it at 157ms worst inside a single frame.tests/route_untracked_spec.luaWORLDBOSS_SLICED_SWEEP_PASS. /am stopleft the Blizzard user waypoint standing (only/am resetcleared it):Route:PlacePinmarks the pin as the route's own andRoute:Stopclears it (C_Map.ClearUserWaypoint, super-track flag off).tests/route_tick_spec.luaROUTE_TICK_STOP_CLEARS_PIN_PASS.- The learn detour and the taxi landing re-optimized every 0.3s tick with no hysteresis (his log: 12 hop changes in 6 minutes, each a fresh pin and chat line, including "Dragonscale Basecamp, The Waking Shores on the way" from Ohn'ahran Plains): the chosen learn stop is sticky while still on the way (
learnNodeon the hop), the announcedtaxiDestholds while it still beats the gate, and detour extra is capped at 1000 yards absolute on top of the 15% (LEARN_DETOUR_MAX_EXTRA).tests/route_spec.luaTAXI_LEARN_STICKY_PASS, TAXI_DEST_STICKY_PASS.
[v0.5.2-release] - 08/31/2026
In-Game
- Riding somewhere with no useful flight point learned, the guide swings you past an unlearned one that lies on the way, so you pick it up and the next trip flies.
- The flight assist no longer aims at a flight point in another zone's corner of the world: with few flight points learned it now says plainly to ride to the entrance instead of inventing a detour like flying to Bel'ameth for an Azure Span raid.
- Flying no longer stutters with the panel open: the panel used to redraw itself for every zone's map pins streaming past, a visible frame skip each time; now it redraws only when an access line truly changed.
- While your Hearthstone cast is running, the guide holds still instead of flashing the flight-master route for those ten seconds; it picks up again where you land.
- The guide's spoken portal and flight emotes are limited to one every 30 seconds; a quick change of answer is still told to you privately, so the room never hears it twice.
Fixed
- Learn-on-the-way detours: when
SameContinentHopanswers "ride there",Route:LearnDetourchecks the undiscovered flight points of both the player's and the destination's maps and, if going through one costs at most 15% extra (LEARN_DETOUR_RATIO), makes it ataxi-learnstop named in the hop (route_hop_learn_fp_fmt, enUS/enGB, remaining locales at the release gate); passing it or learning it recomputes naturally, and an undiscovered node is still never boarded.tests/route_spec.luaTAXI_LEARN_DETOUR_PASS. - Off-map taxi nodes are dropped:
C_TaxiMap.GetTaxiNodesForMapreturns the whole network in the asked map's coordinate space (positions past 0..1 for other zones' nodes), andworldFromMapextrapolated outside the map region is unreliable, which made a Waking Shores flight master score as "beside the player" in Ohn'ahran Plains and Bel'ameth as the landing for a Brackenhide Hollow route (his diag: lastHop at -0.303,1.332).Route:TaxiNodeskeeps only nodes with map coordinates inside 0..1.tests/route_spec.luaTAXI_OFFMAP_DROPPED_PASS. - The in-game profiler named the stutter:
BuildContentat 141ms mean / 370ms worst, asked for every ~5s by GATE_EVENTS (AREA_POIS_UPDATEDand kin fire for every zone a flight streams). The events now callLockouts:RefreshGates(), which recomputes the handful of cached gate answers and reports whether any moved (state, text or textRich), and the rebuild is scheduled only on a true change.tests/gate_state_spec.luaGATE_REFRESH_PASS. - Mid-cast the route no longer recomputes: the stone's own cooldown starts with the cast, which made
Route:HearthHopnon-viable and flipped the answer to the flight master until the hearth landed;Route:Ticknow holds whileUnitCastingInfo("player")reports a cast and the shown hop is the hearth, the same hold a taxi gets.tests/route_tick_spec.luaROUTE_TICK_HEARTH_CAST_HOLD_PASS. - Aloud hop announcements rate limit to one per 30 seconds (
Route.lastSpokeAt): an aloud-worthy change inside the window downgrades to the private tell, the arrow and pin still move; past the window the emote returns.tests/route_tick_spec.luaROUTE_TICK_EMOTE_RATE_LIMIT_PASS.
[v0.5.1-release] - 08/31/2026
In-Game
- A world boss group now routes to the boss that is actually up this week, straight to its world quest on the map, and the guide names that boss when it starts. In the panel the active boss leads its group with an "up this week" tag beside its name, so one look says which boss the week gives you.
- Adding something to the route while searching clears the search box, so the panel and its Routing section come straight back.
- Adding a world boss group to the route looks for the live boss right then, no waiting.
- Boss quests titled with the boss's name ("DEADLY: ...") are found even when the game gives them no world boss tag, which is how the older weekly bosses actually surface.
- A route brought back by a reload while parked at a lair moves onto the live boss by itself, same as a fresh one.
- When the week turns, a world boss route follows the rotation to the new boss on its own; nothing to clear or re-add.
- A world boss group that cannot drop anything you starred this week steps aside like a finished raid, and steps back in when its week comes.
- Access lines for anything already unlocked show in the same cream white as the rest of the panel; orange now always means something still stands in the way.
- Searching puts the best hits first: names that start with what you typed lead, names that merely contain it follow, and groups that only hold a matching item come last.
- Searching "cloth", "plate" or "mount" shows just those rows inside each group, not everything the group drops.
- Search by what a thing is: type "plate" or "cloth" for armor of that kind, a weapon type like "sword", or "mount" for every mount, in your own language.
- While a Hearthstone hop is up, the stone itself shows on the guide instead of a direction arrow, and the guide arrow is a touch bigger.
- Drag the guide arrow anywhere and it stays there, through new hops and reloads alike; an arrow never moved starts just right of top center, out of the way of the action.
- A far stop on your own continent sends you to the nearest flight master first, even when the game's own tracker picked the stop.
- The guide can now tell you to use your Hearthstone (or the Dalaran and Garrison stones) when it is ready and clearly faster than the portals, and a new toggle under the Guide arrow settings turns that off. On cooldown, the portals and flight masters guide as always.
- The skull the map draws for a rotating boss is read directly, so bosses like Calamir route by name even when the game gives them no quest or journal pin.
- A route that had to start at a known lair moves itself onto the live boss as soon as the game shows its skull, naming the boss in chat as it does.
- When no world boss is visible to your character at all, the guide says so as it heads to a known lair, instead of walking you somewhere without a word.
- The boss the week gives you is read straight off the continent map, the same skull you would check yourself, so older groups like the Broken Isles crown their live boss too.
- Ordos on the Timeless Isle shows why he is out of reach: a red line under his name names the legendary cloak quest this character still needs, and disappears once it is done.
- Baradin Hold and Vault of Archavon now say who holds their zone at a glance: the access line shows the map's own control icon for Tol Barad or Wintergrasp and the ruling faction's name painted in that faction's color, updating live as control flips.
- The game only tells anyone who holds Tol Barad or Wintergrasp while they are near it, so the addon now remembers the last holder it saw: visit the zone (or catch its map pin) once and the faction badge shows from anywhere, counting down to the next battle; once a battle could have flipped control, the line says who held it when last seen instead of guessing.
- Until a holder has been seen, the access line says where the answer starts filling in: get near the zone once and it shows from anywhere.
- The control line counts down to the next battle even while the zone is yours, and while a battle is running (up to 30 minutes past its start) it says the battle is underway right now and who held the zone going in.
- AppearanceMatic players now help each other see: guildmates, groupmates and battleground raid mates running the addon quietly trade what they saw, so one of them passing Tol Barad or Wintergrasp lights the faction badge and battle timer for everyone, and a boss scan from any of them fills in the week's "up this week" tag even for a character who cannot see the quest yet. Nothing to set up, it just works between addon users.
- A world boss you cannot see yet says why: groups whose weekly boss needs that expansion's world quests unlocked first (Broken Isles and later) show the unlock quest on the group, in its tooltip, and in chat when the route starts, so the quest to do is always named.
Added
- The Hearthstone joins the route options: when a trip crosses continents, the guide weighs the player's bound Hearthstone, the Dalaran Hearthstone and the Garrison Hearthstone against the best portal chain (
Route:HearthHop, scored besideBestGatewayToward); a stone that is owned, off cooldown and clearly cheaper becomes the hop ("Use your Hearthstone", localized stone names viaC_Item.GetItemInfo), and on cooldown or with the new Guide arrow toggle off (useHearth, default on) the portals and flight masters stand exactly as before. Bind locations resolve by zone name; a bind the map index cannot name simply leaves the regular stone out. Three new keys (toggle_hearth_hops,toggle_hearth_hops_desc,route_hop_hearth_fmt) in all 12 locales.tests/route_spec.luaHEARTH_HOP_PASS. - Search understands what a row is: gear answers its own localized type and subtype straight from the item data ("plate", "cloth", "sword", no list to maintain, works in every language), and every mount row answers the localized word for mount ("mount", "reittier", "montura";
lock_search_type_mount, all 12 locales).tests/show_filter_spec.luaSHOW_FILTER_GEAR_TYPE_PASS, SHOW_FILTER_MOUNT_KEYWORD_PASS. - The control gate line carries the controlling faction's badge: the live Tol Barad control pin's own icon (
C_AreaPoiInfo.GetAreaPOIInfoatlas when present, else itstextureIndexcell onInterface\Minimap\POIIconsviaC_Minimap.GetPOITextureCoords) plus the holder's name in Blizzard's faction colors; Wintergrasp, which has no control pin in AreaPOI.db2, uses the same shipped keep-icon indexes (46/48,controlIcononData/Gates.luacontrol rows).GateStatekeepstextescape-free for route titles and addstextRichfor the panel, route section and wardrobe tooltip (Lockouts:ControlBadge).tests/gate_state_spec.luaGATE_CONTROL_BADGE_PASS. - Live probes proved Blizzard serves the Tol Barad control pins,
GetAreaPOIInfoby ID andGetOutdoorPvPWaitTimeonly near the zone (all nil from another continent, map open or not), so control sightings persist account-wide indb.global.zoneControl(holder,seenAt,battleAtfrom the observed wait): a remembered holder answers as fact untilbattleAtpasses (control flips only when a battle ends), then the line falls to a last-seen wording (gate_control_last_seen_fmt, enUS/enGB now, remaining locales at the release gate).tests/gate_state_spec.luaGATE_CONTROL_MEMORY_PASS. - A player network over addon messages (
Network.lua, prefixAPPRMATICNET, rawC_ChatInfosince retail carries addon data only on GUILD/RAID/PARTY/INSTANCE_CHAT, no YELL/SAY/CHANNEL): each client broadcasts only its own sightings (control stones:zoneControlentries without a peervia; boss scans:activeBossSetids) on login and a 60s changed-only ticker, tiny pipe-format messages under the 255-byte and burst limits; instance groups (battlegrounds, dungeon and raid finder,IsInGroup(LE_PARTY_CATEGORY_INSTANCE)) ride INSTANCE_CHAT; receivers merge newest-wins intodb.global.zoneControl(gates invalidated, panel rebuilt) and week-scopeddb.global.bossNetwork(unioned byActiveWorldBossSet, expiring at the weekly reset viaC_DateAndTime.GetSecondsUntilWeeklyReset). Peer entries never rebroadcast, so the mesh cannot echo./am diagprints the network line.tests/network_spec.luaNETWORK_SPEC_PASS,tests/route_untracked_spec.luaNETWORK_BOSS_UNION_PASS.
Fixed
- A group kept in the results only by its contents shows only the rows that answer: searching "plate" brings the plate pieces without the raid's whole wardrobe, "mount" brings the mount rows alone, and a boss whose own name answers keeps all of its rows. Name-matched groups still render whole.
tests/show_filter_spec.luaSHOW_FILTER_ROW_LEVEL_PASS. - Search reads what was typed the way a person means it: a query that starts a word in the raid's own name ranks first ("und" leads with Liberation of Undermine), a name it merely sits inside (Blackrock Foundry) comes after, then zone and expansion hits, and content-only matches last; nothing is hidden, only ordered.
tests/show_filter_spec.luaSHOW_FILTER_WORD_START_PASS. - The stones line up by succession: the bag Hearthstone leads whenever it is viable, the class recall (Astral Recall, known and off cooldown) follows, and the Dalaran and Garrison stones come last; score only breaks ties on a rung. Spell hops carry
hearthSpellID, say the spell's own name and show its icon. The guide icon draws at 85% of the frame (Arrow.IconInset) so it reads as an item, not a fill, and the sprite swaps cleanly between icon and arrow modes.tests/route_spec.luaHEARTH_PRIORITY_PASS;tests/guide_arrow_spec.luaGUIDE_ARROW_ICON_INSET_PASS. - The bag stone leads from an inn bind too:
GetBindLocation()names the inn's area (his diag: "Wayfarer's Rest", the Silvermoon City inn), which is no uiMap, so the bind rungs were never built and the Garrison stone won by default. A shipped area table (Data/BindAreas.lua, generated bytools/gen-bind-areas.pyfrom Blizzard'sAreaTableandMaptables, areaIDs per world continent) resolves the bind name to its continent through the client's own localizedC_Map.GetAreaInfo(Route:BindAreaContinent, scanned once per bind name); a name two continents share stays unresolved and the line moves on to the next stone. Map-name binds match in any casing now (Route:ResolveBindMaps), and/am diaggained ahearth:line telling the bind, its resolution and every stone's verdict from the last routing pass.tests/route_spec.luaHEARTH_BIND_CASE_PASS, HEARTH_BIND_AREA_PASS. - A moved guide arrow stays put: the drag handler saved only the anchor's offsets while dropping which corner the client re-anchored by, then restored them against CENTER, so the frame landed somewhere else after a reload.
savePointkeeps the whole anchor (point,relPoint, offsets) andrestorePointre-applies it exactly; the default position is the operator's own tested spot (TOP anchor, x 30, y -4, read from his live layout) inCore.lua, the restore fallbacks andArrow:ResetPosition.tests/guide_arrow_spec.luaGUIDE_ARROW_POINT_PASS. - A hearth hop shows the stone, not a bearing: the guide swaps the arrow for the chosen stone's own item icon (
Arrow.ItemIconForHop,hearthItemIDon the hop) with no rotation and no "no bearing from here" line, and the arrow frame grew 15% (56 to 64) for readability.tests/guide_arrow_spec.luaGUIDE_ARROW_ITEM_HOP_PASS. - The stone competes with the game's tracker too: HearthHop lived only in the table router, and a tracked route (his Brackenhide case) bypassed it entirely, so a ready Hearthstone was never weighed; a hearth that beats the whole portal chain toward the final target now outranks the tracker's hop as well.
tests/route_spec.luaTRACKED_HEARTH_PASS. - The game's tracked answer flies too: a tracked hop thousands of yards away on the player's continent runs through the same flight-master assist as the table's hops ("Flight master: X, then fly to Y" instead of a four-thousand-yard walk to a portal); near hops, taxi answers and cross-continent answers stand untouched.
tests/route_spec.luaTRACKED_TAXI_ASSIST_PASS. - Search ranks by relevance: a raid whose own name, zone or expansion carries the query renders above raids that only match through a boss or item inside, so searching "under" leads with Liberation of Undermine instead of a raid that merely drops a Thunder-something. No fuzzy matching involved: the item search was and stays plain substring; only the order changed.
tests/show_filter_spec.luaSHOW_FILTER_NAME_FIRST_PASS. - An open gate reads calm: unlocked access lines render in the panel's own cream white, only blocked and unknown gates keep the legendary orange (post-0.5.0-final).
tests/gate_state_spec.luaGATE_OPEN_CREAM_PASS. - A no-chance week acts like a done lockout: when the group's active boss is positively known and none of the starred bosses is it, the group sinks with the done raids, the route walk passes over it (
Lockouts:WorldBossNoChance,noChanceon the route record), and it comes back alive when the rotation turns or the active boss is unknown. Single-drop rows keep their behavior when their group cannot be judged.tests/route_untracked_spec.luaWORLDBOSS_NOCHANCE_VERDICT_PASS, WORLDBOSS_NOCHANCE_SKIPPED_PASS;tests/route_panel_spec.luaROUTE_PANEL_NOCHANCE_SINKS_PASS. - The weekly turn is followed: a world-boss route recognized by its group (route.target.raidName) retargets onto the new live boss when the rotation moves, whether it stood on the shipped anchor or on last week's boss spot; a route already standing on the live boss is never restarted.
tests/route_untracked_spec.luaWORLDBOSS_WEEKLY_TURN_PASS. - A route resumed onto a group's anchor upgrades too: the anchor flag does not survive a reload, so the route tick now recognizes a target standing on a world-boss group's own shipped anchor and moves the trip onto the live boss the moment the scan finds one.
tests/route_untracked_spec.luaWORLDBOSS_RESUME_UPGRADE_PASS. - The boss quest that answers no tag is caught by its title: the map-630 probe showed the live boss's quest sitting in the task list ("DEADLY: Withered J'im") with a nil tag that even
GetQuestTagInfonever resolves, so the sweep kept discarding it. Every titled task quest is a candidate now; the boss-name containment match decides, and the WorldBoss tag keeps its pin-proximity fast path.tests/route_untracked_spec.luaWORLDBOSS_TAGLESS_QUEST_PASS. - The vignette layer joins the hunt: the full pin dump proved journal pins exist only for the permanently-up bosses (Sha of Anger through Kazzak and the BfA statics), rotating bosses never get one, and the skull the zone map draws for them is a vignette. The active scan now keeps every world-map vignette by name (
C_VignetteInfo.GetVignettes/GetVignetteInfo, live and on the world map only); a vignette named for a group's boss marks it live andWorldBossSpotresolves its position across the zone maps (GetVignettePosition) and routes to it by name./am diagworldboss line showsvign=Nwith the names.tests/route_untracked_spec.luaWORLDBOSS_VIGNETTE_PASS. - A route action scans fresh:
RouteUntrackeddrops the pin and quest scan throttles before asking, so adding or starting a world-boss route sees the live boss at click time instead of waiting out a timer.tests/route_untracked_spec.luaWORLDBOSS_FRESH_ON_CLICK_PASS. - A partial pin index is never final: the wbindex diag proved the boss index perfect (all 15 Broken Isles encounters) while the pin index held 22 current-content pins cached for good, built before old-expansion skulls streamed in, so Calamir's Azsuna skull (visible on the player's own map) never entered it.
EncounterPinIndexnow rescans every minute once built (empty asks keep the 30 s retry), and a rescan that answers empty keeps what it had. A group with exactly one pinned boss and no quest signal treats that boss as the live one (only the active boss of an old rotating group gets a skull), so it leads the group tagged "up this week". And an anchored world-boss trip upgrades itself:RouteAdvanceIfDoneretargets the route onto the live boss the moment its pin streams in, telling the boss's name.tests/route_untracked_spec.luaWORLDBOSS_PIN_RESCAN_PASS, WORLDBOSS_LONE_PIN_ACTIVE_PASS, WORLDBOSS_ANCHOR_UPGRADE_PASS. - The world-quest sweep reads continent maps too: Blizzard aggregates zone world quests onto the continent view, and for some old groups that is the only place the boss quest surfaces, so
ActiveWorldBossSetsweeps continent-type maps after the zones (deduped by questID). A group with no pin, no quest and an open gate no longer anchors wordlessly: the guide says no world boss is showing for this character (lock_worldboss_none_fmt, all 12 locales). His diag also proved the gate side correct: char=true on Uniting the Isles, gate open, line rightly silent.tests/route_untracked_spec.luaWORLDBOSS_CONTINENT_QUEST_PASS, WORLDBOSS_NONE_TOLD_PASS. - The continent map's skull crowns the live boss: old expansions carry no zone-map encounter pins at all (his diag: 22 pins, none Legion), but the game draws the week's active world boss on the continent map itself, so
EncounterPinIndexnow sweeps continent maps too and every continent-pinned encounter is marked the live one (liveContinentBossesfolded intoActiveWorldBossSet). Standing at Nithogg now means Nithogg leads the group tagged "up this week" and the route aims at his skull./am diaggains agate:line (gate kind/state plus each unlock quest's char and account flags) andliveCont=N.tests/route_untracked_spec.luaWORLDBOSS_CONTINENT_SKULL_PASS. - A per-character unlock is judged by this character alone:
questDonefell through toIsQuestFlaggedCompletedOnAccount, so another character's Uniting the Isles opened the Broken Isles gate for a character that personally cannot see Legion world quests, the line went quiet-open and no tip showed anywhere. Legion, BfA and Dragon Isles worldquest rows now carryperCharacter = trueand check only the character's own flag; Khaz Algar's Surface Bound grants warband-wide World Quests and stays account-checked.tests/gate_state_spec.luacovers the account-flagged-but-blocked case. - Ordos carries his cloak gate now: the one world boss AllTheThings records a real prerequisite for (and the wiki confirms still holds on retail), a red line under his name in the Pandaria group says A Pandaren Legend must be done on this character, gone once it is. New
R.BOSS_GATEStable (per-encounter,Lockouts:BossGateText) for a single gated boss inside an otherwise free group;gate_boss_quest_fmttranslated in all 12 locales. The full ATT audit found no other live gate missing: every other world boss carries no prerequisite in ATT's data, and the old raid attunements it records are all flagged removed by Blizzard.tests/gate_state_spec.luaGATE_BOSS_QUEST_PASS;tests/route_panel_spec.luaROUTE_PANEL_BOSS_GATE_PASS. - An unlocked world-quest gate goes silent: world-boss groups sit in the panel for good, so the access line shows only while the unlock quest is actually missing and leaves once it is done (
quietOpenon the open worldquest state; blocked lines are untouched). GATE_WORLDQUEST_PASS covers both states. - A gated world-boss group says the gate in chat too: when the route can find no live boss (the unlock quest is missing, so the character cannot see it), the guide tells the access line ("the weekly world boss stays hidden ... until Uniting the Isles is done") before anchoring, so the fix is named right where the player is looking.
tests/route_untracked_spec.luaWORLDBOSS_GATE_TOLD_PASS. - Boss world quests titled with a prefix ("Lair: Nymrissa Wavecaller") still place their boss: the quest-title match accepts a title that contains the boss's name, not only an exact match (seen live in /am diag).
tests/route_untracked_spec.luaWORLDBOSS_TITLE_PREFIX_PASS. - World-boss groups gated behind a world-quest unlock say so now: a new
worldquestgate kind inData/Gates.luacovers Broken Isles (Uniting the Isles, 43341), Battle for Azeroth (Uniting Kul Tiras 51916 / Uniting Zandalar 51918), Dragon Isles (Moving On, 66221) and Khaz Algar (Surface Bound, 79197); without the unlock the character cannot even see the weekly boss, so the panel's access line and the raid tooltip both carry the requirement in red, green once unlocked (gate_worldquest_blocked_fmt/gate_worldquest_open_fmt). Shadowlands names no unlock quest on the wiki (level + covenant) and carries no row.Lockouts:FillRaidTooltipnow appends the gate line for every gated raid.tests/gate_state_spec.luaGATE_WORLDQUEST_PASS;tests/route_panel_spec.luaROUTE_PANEL_GATE_TOOLTIP_PASS. - The world quest is the boss's own address now: old world-boss groups (Broken Isles and every rotating group before the current content) get no journal pin at all, so the active scan sweeps every zone for world-boss quests (the tag read from
C_QuestLog.GetQuestTagInfo(questID).worldQuestType, the field Blizzard's own map pins use; the task POI'squestTagTyperides along nil on the live client), reads each quest's title (C_TaskQuest.GetQuestInfoByQuestID) and routes to the quest's own map position when its title names one of the group's bosses; the pin match and the shipped anchor stay as the floor below it./am diaggains aworldboss:line showing every world-boss quest the scan sees (and the total task quests swept, so an empty sweep and an empty tag match read differently) and the spot the first open route group resolves to.tests/route_untracked_spec.luaWORLDBOSS_QUEST_SPOT_PASS. - Adding a raid or an item to the route from a search clears the search box (
Lockouts:ClearSearch), so the panel comes straight back whole with the Routing section on top instead of staying filtered.tests/route_panel_spec.luaROUTE_PANEL_SEARCH_CLEARS_PASS. Lockouts:ActiveWorldBossSet(new): the week's active world boss is the encounter pin carrying a live world quest taggedEnum.QuestTagType.WorldBoss(C_TaskQuest.GetQuestsOnMapper pinned zone, matched to the pin within 0.05); the scan re-asks every 30 s so an empty early answer is never kept and a mid-session weekly reset is seen.WorldBossSpotprefers the active boss's pin over the first pinned boss and returns the boss's name (EJ_GetEncounterInfo),RouteUntrackedsays and titles that name,SortBossOrderleads with the active boss, and the panel's boss line carries a greenlock_boss_activetag (new key, translated in all 12 locales).InvalidateIndexesclears the set.tests/route_untracked_spec.luaWORLDBOSS_ACTIVE_PASS, WORLDBOSS_ACTIVE_RETRY_PASS;tests/route_panel_spec.luaROUTE_PANEL_ACTIVE_BOSS_PASS.
[v0.5.0-release] - 08/30/2026
In-Game
AppearanceMatic finds the looks you are missing and walks you to them. Star any appearance in the Wardrobe or any mount in the Mount Journal; the panel (/am) gathers every starred item by the raid, dungeon, vendor or zone it comes from, with each character's lockouts, the cost, the location and what must be done first. Add an entry to the route and the guide takes over: the arrow and the game's yellow waypoint lead you portal by portal, boat by boat, flight by flight, never through a gateway your character has not unlocked. Collect the item and the star comes off by itself, the alert fires with your own message and sound, and the route moves on to the next stop. A try-on model on item tooltips shows a look before you chase it, and the FAQ tab in the settings answers the common questions.
This first public version is a beta: reports shape it. Bugs and ideas belong on the CurseForge page or in the Discord (the button sits on the General settings tab). If a direction from the arrow ever leaves you unsure where to go next, say what it said and what would have made it clear: your wording becomes the guide's wording for every player after you. And if a line in your language reads wrong, post the language, the line and the words a native speaker would use.
Added
- An FAQ tab in the settings: what the addon does best, starting a route, what the arrow and the yellow waypoint mean, how travel is chosen, where beta reports go, how to suggest clearer directions, how to report a translation, and how the guide behaves beside TomTom and Blizzard's own tracking. The changelog popup opens on the beta welcome.
- Collected announcements go to your guild from the start, paced to one guild line a minute with the rest following in order so a lucky run never floods guild chat; party, raid, battleground and the emote are on too, and every Social box stays the player's to change.
Changed
Core.luasocial defaults:guild = trueagain; a one-time_socialGuildRejoinedmigration turns the box back on for profiles carrying the 0.5.0 opt-out write, and the 0.5.0_socialGuildOptInshim is gone. The box stays the player's to change.Social:Announceguild lines pass throughSocial:PushGuild, a one-a-minute queue (GUILD_GAP_SECONDS, 60): the first goes at once, the rest drain in order throughC_Timer.After, none dropped; with no clock or timer available lines go straight out.tests/social_spec.luaSOCIAL_GUILD_SPACING_PASS.- Route chat splits by hop kind: only portal and flight hops go through
say(the emote path); route starts, plain next stops, arrivals, inside-the-instance lines, pin rejections,lock_route_nextandlock_collected_guide_endedgo throughtell.tests/route_tick_spec.luaROUTE_TICK_EMOTE_GATE_PASS plus the reworked chat assertions.
Fixed
Route:TableHop: chain selection minimized continent hops alone (GatewaysToward's shortest-chain set), then took the nearest of that set, so from Silvermoon a discovered EK-to-Outland gateway 11,000 yd away beat the Orgrimmar portal 60 yd away (Horde ships no EK-to-Outland row; the 2-hop Silvermoon -> Orgrimmar -> Shattrath chain never entered the candidate set). NewRoute:BestGatewayTowardscores every unlocked same-continent gateway with a live chain by yards-to-reach-it plusR.CHAIN_HOP_YARDS(2500) per remaining continent hop, andTableHoproutes to the cheapest. The unreachableroute_reach_first_fmtbranch (candidates are always on the player's continent) is gone.tests/route_spec.luaCHAIN_COST_PASS.Route:TrackedNextHop(new): a route carrying a tracked item now asksC_ContentTracking.GetNextWaypointForTrackable(viaTracking:NextWaypoint, throttled to 1 s) every tick, settle window included: content tracking is asked by the player's current map and answers nothing while pending, so waiting outMarkSettling's 3 s (ZONE_CHANGED_NEW_AREA) / 5 s (PLAYER_ENTERING_WORLD) parked the pin on the far destination after every loading screen, the "takes 3 to 5 secs to update" report.tests/route_spec.luaTRACKED_NO_SETTLE_WAIT_PASS. It is asked and feeds the answer in as the game hop.GameNextHop'sC_SuperTrack.GetNextWaypointForMapquery is blinded by the addon's own super-tracked user pin outside the 3-tick window, so Blizzard's correct "Take the portal from Silvermoon to Orgrimmar" was computed and never heard. Wordless points are no hop (the table guides), and an off-world hop for a same-continent target is refused like the super-track path's. Cleared on map change, on reaching a flown-to node, and inStop.tests/route_spec.luaTRACKED_GAME_ROUTE_PASS.Route:OnTaxiMapOpened: with an off-continent target it compared taxi-node world coordinates against the target's coordinates on another continent's grid (instance 530 yards vs instance 0 yards), picking a nonsense "closest" node and storing its position relative to the player's map (the out-of-rangemap 2393 @ -1.772,5.710hop). Cross-continent it now aims atBestGatewayToward's first gateway, and gives up the override rather than guess when no chain exists.tests/route_spec.luaTAXI_CROSS_CONTINENT_PASS.Route:Tickreturns whileUnitOnTaxi("player"): every zone crossed under a taxi re-queried the route (mapChanged), re-placed the pin and re-set the arrow's hop at the node under the player, ringing the arrow's 40 yd arrival ping at each flight point;GuideArrow'splayArrivalPingis taxi-guarded too, so the ping waits for touchdown. The landing tick recomputes and announces once.tests/route_tick_spec.luaROUTE_TICK_TAXI_SILENT_PASS.Route:SameContinentHop: near a flight master, any flight whose landing node was even one yard closer than the straight ride was boarded (destNodeD > straightwas the only gate), which chained a second flight after every landing. A flight now has to landR.TAXI_GAIN_YARDS(1000) closer than riding straight.tests/route_spec.luaTAXI_GAIN_PASS.LockoutsEvents.luaPLAYER_ENTERING_WORLD:Route:ResumeandLockouts:ResumeGuidesat behind a flatC_Timer.After(6), so every /reload left the route, arrow and pin dead for six seconds (portal loading screens re-entered the same wait, harmlessly but pointlessly). The resume now runs synchronously on reloads and portal transitions, with a 2 s wait only on a cold login (isInitialLoginper the event payload); the chained 6 sRouteAdvanceIfDonekick is unchanged. Newtests/resume_start_spec.luaRESUME_START_PASS.Route:NextHop/Route:Tick: with a tracked item aboard, the pin-query dance is skipped entirely.C_ContentTrackinganswers no matter where the pin sits, and Blizzard's router needs ~3 s after a loading screen (the guide path's old DataPending retry already measured this), so the provisional window parked the pin on the far destination exactly that long after every portal. The table's hop is now final at once, pin included;TrackedNextHopre-asks a pending answer every tick instead of throttling nothing for a second; and a hop change on the tick the player's map changed skips the two-tick flap gate, so the hop after a portal announces and pins immediately./am diagprints the tracked hop and the age of the last ask.tests/route_tick_spec.luaROUTE_TICK_TRACKED_NO_PIN_DANCE_PASS.Route:GatewaysTowardnever wrote its computed list back totowardCache(the cache was read-only since it was added), so the BFS re-ran every 0.3 s tick. The hop map is factored intoRoute:ContinentHops(shared withBestGatewayToward) and both now cache beside the toward lists, cleared by the same invalidations.AnnounceRoute(both branches),ToggleRoutePause, the/am stophandler inLockoutsEvents.lua(which gained its owntelllocal),HandleTrackLink's limit notice,GuideRouteNext's no-entrance notice andFavorites:GuideToVisual's twofav_print_no_locationreplies now go throughpluginShared.tellinstead ofsay. Newtests/announce_voice_spec.luaasserts route adds, removals and both switch replies reachtelland neversay.Lockouts:NoteRouteLink/routeKey: the route-target store was keyed by visualID alone, and every raid dropping that visual overwrote it (rows render for each raid that has a source of a starred model);GuideRouteNextthen followed the last writer's link, raid name, source and entrance. Raid rows now store and look up under<kind>:<id>:<raidName>(the routed raid's own row wins), single-item records carry theirraidName, anddropTargetlinks pass the raid through.tests/route_panel_spec.luaROUTE_PANEL_SHARED_VISUAL_PASS covers two raids sharing one visualID.Route:GameNextHop:C_SuperTrack.GetNextWaypointForMapreturning coordinates with an empty description was wrapped as a "game" hop titled with the final target; the answer coming and going (result 2 failures) flapped the hop against the table's flight-master answer and announced every swing. A wordless point is now no hop at all; the table guides.tests/route_tick_spec.luaROUTE_TICK_WORDLESS_GAME_PASS.Route:Startsaysroute_print_routing_fmtbefore its first tick (the tick used to say "next" first), andTickannounces a hop only once a first hop was seen (Route.hopSeen).NextHop's "reached the cached gateway" reset no longer applies to a hop that leaves the world (LeavesWorld): standing at a portal at route start wipedlastHopand re-announced the same hop.sameHoptreats two hops on the same map withinSAME_HOP_YARDS(15) as one; the game's spot for a gateway replaces the table's silently (arrow and pin move, no chat line). A route's first hop and provisional hops skip the two-tick confirmation.Route:CanFlyToTarget:IsFlying()no longer chooses a straight line; onlyguideArrow.preferOwnMountdoes. Being airborne at the flight master flipped the hop to the far target and landing flipped it back.Route:Gateways(): the TravelGuide sweep ships every hand-written hub portal again as one room node per map (Portal to X,hubName= the map's name, no floor;Data/Routes.lua:280and the map-85 rows), and Blizzard's own map links do the same at runtime.NextHoptook the nearest candidate andDescribeGatewaythe nearest row within 60 yd, so the twin won and the hub row's label, name and floor never showed. A generated, game-reported or learned row for the same destination withinHUB_TWIN_YARDS(150) of a hub portal now folds into the hub row, which keeps its words and takes the twin'sreqwhen it has none (the Zuldazar portal now carries quest 46931 account-wide from its twin). The old same-map floor borrow is gone.DescribeGatewayprefers a hub row within 60 yd; a sweep row whose destination no hub row names (Caverns of Time by its own map, Warspear, the newer Razorwind Shores and Founder's Point portals) is placed byRoute:HubNearas "(Pathfinder's Den)" rather than "(Orgrimmar)".Route:TableHop(the table half ofNextHop): the hop verb comes from the row's kind, so generated labels lose their own gateway word (placeName: "Portal to", "Boat to", "Zeppelin to", "Deeprun Tram to", "Rootway to", "Return to" and the rest of the sweep's vocabulary).Route:PlacePin/Route:PinIsAttrack where the route last put the game's user waypoint.NextHopre-asks Blizzard's router every 60 s by moving the pin onto the destination; when the answer was the same hop,Tickonly calledEnsureGameArrow, which returns while any pin is tracked, so the pin stayed on the destination for good. An unchanged hop now moves the pin back onto it, and a table gateway hop (portal, boat, zeppelin, flight master) is not re-asked at all: the game would name the same gateway.Route:NextHop: while the game's three query ticks run, the table's answer is returned as a provisional hop thatTicksets on the arrow at once (announced once, no two-tick confirmation) while the game's pin stays on the destination for the game to route from; the table's answer standing after the window moves the pin onto it without a second chat line. A tick with no player position (a loading screen, the moment after a portal) returnspendinginstead of a final hop to the far destination the arrow could not bear on.Tickno longer sits out the 5 s settle afterPLAYER_ENTERING_WORLD; only the game query waits for it.Tests:
tests/route_spec.lua(OWN_MOUNT_OPTION_PASS; hub twins fold, requirement adoption, room-node hops,DescribeGateway, label cleanup, spec maps for Durotar, Zandalar, Zuldazar and old Silvermoon) and the newtests/route_tick_spec.lua(provisional hop at once, pin waits on the destination then moves onto the hop, no re-ask for a table gateway, pin returns after a re-ask, no hop without a position, settling gates only the game query, one chat line per start, the game's same-gateway answer taken over silently, standing at the portal changes nothing, a hop change announced once).World-boss groups always have somewhere to route: Data/Entrances.lua now ships a curated anchor for Broken Isles, Azeroth (BfA), Shadowlands, Dragon Isles and Khaz Algar (Blizzard's JournalEncounter table carries junk coordinates for every post-WoD world-boss group, so these are wiki-read spawn spots of one boss each), plus the Return to Karazhan doorway. Shipped world-boss rows carry worldBoss = true; RouteUntracked prefers the week's live boss pin over the shipped anchor and falls back to the anchor when no pin is up, so the route exists from shipped data on login instead of depending on the Encounter Journal waking. /am diag route line shows the live pin count (pins=N).
The whole empty-cache class swept, every once-per-session index that could be built too early and keep a hollow answer forever:
Route:MapIndex(an empty map index broke every name-resolved gateway all session),Route:DiscoverGateways(a sweep that found nothing stuck at nothing), andLockouts:BuildBossOrdinalIndex(an unloaded journal cached an empty boss index, and the entrance retry never cleared it; it does now). All three follow the same rule as the entrance and pin indexes: an empty answer is never kept for good.Lockouts:CurrentExpansionNameand the npc-name cache were checked and already never store an empty answer.tests/route_spec.luaMAP_INDEX_EMPTY_RETRY_PASS, DISCOVER_EMPTY_RETRY_PASS;tests/route_untracked_spec.luaBOSS_INDEX_EMPTY_RETRY_PASS.Lockouts:EncounterPinIndexcached an empty scan forever when it first ran before the journal data loaded (the live reason Broken Isles still failed after the pin fix); an empty result now retries every 30 s until pins arrive, and only a non-empty scan is kept for the session.tools/gen-entrances.pyalso ships world-boss spots fromJournalEncounterwhere Blizzard's data is trustworthy (an outdoor zone under a real continent; Legion and BfA rows point at a battleground map and are refused), adding Pandaria and Draenor rows additively, zero removals.LockoutsUI:BuildContentgainsrouteHidden: any non-routing search skips the Routing section render whilerouteOrderstill builds for the guide, routed raids and pulled rows rejoin the normal lists as search results.tests/show_filter_spec.luaSHOW_FILTER_ROUTE_SEARCH_PASS.The full dead-end sweep, every route-entry shape that could fail with nowhere to point: world-boss groups (Broken Isles and kin) have no entrance anywhere, so
Lockouts:EncounterPinIndexreads the game's own zone-map boss pins (C_EncounterJournal.GetEncountersOnMap, keyed by the journal encounter idsBuildBossOrdinalIndexnow keeps) andWorldBossSpotroutes to the boss's skull; raid-drop links now carry their shipped spot when the record knows one (TrackTargetno longer strips the fallback when a raidName rides along,dropTargetpasses the acq coordinates), so an entrance-less group like an old Acquisition-named raid still routes; and when truly nothing is routableGuideRouteNextsays "no route and no entrance" once instead of failing silently./am diagprints the tracker's last start error.tests/route_untracked_spec.luacovers all six shapes.Lockouts:HandleTrackLinkreturned true whenTracking:Togglefailed (WoW's tracker refusing the item or at its limit), soFollowTarget,GuideRouteNextandResumeRouteall believed a guide started while nothing did: a Broken Isles world-boss entry sat first on the route with no guide, no route, no arrow and no word. The failed branch reports false now.GuideRouteNextthen falls back to the newLockouts:RouteUntracked, a plain route to the entry's entrance (LegacyEntrance) or the link's shipped spot with no tracked item aboard, and the tracking watchdog (trackMisses >= 3, sustained not-tracked) takes the same fallback instead of a silentStopGuide. Each fallback tells the player it is guiding untracked.tests/route_untracked_spec.lua.
[v1.1.2-release] - 08/30/2026
In-Game
- Removing something from the route, and a star coming off (when you collect the item, or when the addon clears stars from things you already have), are now told to you alone in chat, never as an emote. Adding to the route and the routing lines still go out as an emote while the Emote box is on.
Changed
Social:Tell(text): the printed form ofSocial:Line(star texture, pink name,chat_line_fmt), never broadcast.Favorites.luaexports it aspluginShared.tellbesidesay;LockoutsRoute.luauses it forlock_route_removed_fmt(AnnounceRoutewithadded = false),lock_stars_removed,lock_mount_collected_star_removedandlock_appearance_collected_star_removed.tests/social_spec.luaasserts Tell prints and never broadcasts with the Emote box on.
[v1.1.1-release] - 08/30/2026
In-Game
- The widget no longer errors five times over while it opens when the game withholds a vendor's or creature's name (in combat and inside some instances). The row shows without the name for now and picks it up on a later refresh.
Fixed
Acquisition:NpcNamecomparedline.leftTextfromC_TooltipInfo.GetHyperlink("unit:Creature-...")before checking whether the client had handed back a secret string, so the panel's Other section raisedattempt to compare local 'name' (a secret string value)on every row with a creature source, five times per Init. A secret name is now dropped withissecretvalue(the same guardPlaceTitlealready used for zone names), recorded as a miss and asked for again after the retry window.tests/acquisition_access_spec.luacovers it (red before, green after).
[v1.1.0-release] - 08/30/2026
Data
Data/Acquisition.luarefreshed from AllTheThings 5.3.7 (the GitHub release of 2026-08-30, which carries the "usual sunday parse" commit 0031c1c4; the installed client copy was still 5.2.4, so the dump ran against the release zip). Additive: vendors 112700 to 114670 (+1970, 0 removed), located vendor rows 96476 to 98070, mounts by spell 1080 to 1102 (+22), notes 7407 to 7783, gates 64989 to 66219, 39 event names, 9 availability codes.tools/check-acquisition-diff.luaADDITIVE_OK, no dashes,tests/acquisition_ensemble_spec.luaandtests/acquisition_access_spec.luapass.
In-Game
- Routing and alert lines no longer say the addon's name twice. The emote and the chat line read "AppearanceMatic: routing to Ulduar" once, with the star in front, instead of "AppearanceMatic: AppearanceMatic: routing to Ulduar". /am stop says it the same way as the Routing switch now.
- The try-on model now shows on item tooltips for gear in your bags, the same as it always did when you hover a row in the widget. The tooltip had an extra check in front of it that the widget does not have, and that check was rejecting gear the widget shows perfectly well. Both now go through exactly the same code, so anything the widget can preview, a tooltip can preview.
- Starring something in the middle of a fight no longer puts an error on the widget. While you are in combat the game deliberately withholds some of the information the panel reads, and the panel was tripping over it. It now skips what it cannot see, keeps showing everything it can, and adds one line telling you plainly that some of it is hidden until you leave combat. The moment the fight ends it works out what was missing and fills itself in, with no action from you.
- Starred gear that only drops in this season's Raid Finder now tells you where to queue. Those raids have no Raid Finder NPC to talk to (older ones do), so the row used to say nothing at all, which read as though there was no way in. It now reads "Raid Finder: queue from [Group Finder]", and clicking that opens the Group Finder straight to the Raid Finder tab. Legacy raids still name the NPC to talk to, as before.
- The panel now leaves alone any line that already reads exactly the way it should. A refresh used to redraw all of its lines, well over a thousand of them with a few hundred starred items, even when the list was identical; now only the lines that actually changed are touched. Each starred item's name and icon is also looked up once instead of on every refresh. Together these are the bulk of what the panel cost while open, and it shows most in a raid, where a stream of loot was making it refresh again and again.
- The panel also limits how much of your frame time it will take. If a refresh turns out to be expensive it waits proportionally longer before the next one, so a burst of activity (a raid full of loot, a run of lockout updates) can no longer make the panel the most expensive thing running. A quick refresh still happens as fast as it ever did.
- The addon costs much less while you play, whether the panel is open or closed. The list of your starred mounts is no longer worked out by walking every mount in the game each time anything refreshes; the Mount Journal no longer redraws every star and route pin several times a second when nothing about them has changed; an item tooltip works out only once whether a piece can be tried on, instead of every time a tooltip opens or refreshes itself; and a vendor the game cannot name yet is no longer looked up again for every row.
- The Raid Lockouts panel no longer redraws itself over and over while it is open. With a few hundred starred items it was rebuilding the whole list several times a second for as long as the panel was up, which ate a real slice of your frame rate and made the game feel choppy while travelling. The panel now redraws only when something it shows has actually changed, and each redraw does far less work: starred items whose drop information has not arrived yet are no longer re-checked from scratch every time, and the route order, the panel font and the Raid Finder lookups are worked out once instead of once per row. Routing is lighter on the way as well. Nothing on the panel changes; it just costs a fraction of what it did.
- The widget shows a turning loader over itself while it is still gathering the drop data for your starred appearances, so an opening panel is never a half-filled list. It clears the moment the list is complete, and once the panel has shown you a full list it never covers it again while you play.
- The guide arrow appears as soon as the waypoint is set. When a bearing is not available yet (another continent, or a spot the map cannot place), the arrow now says so within a third of a second instead of sitting invisible for seconds, and it retries straight away rather than waiting; the wait no longer stretches out when your frame rate is low.
- New option, on by default (General > Tooltip): every item tooltip for gear with an appearance, in bags, at vendors, on loot, chat links and quest rewards, shows the same try-on model beside it that the widget shows when you hover a gear row (flipping to the tooltip's left when it would run off the screen). Turn it off to keep the model to the widget rows.
- Starring an appearance or mount you already have no longer sticks: the star comes straight off (a leftover star on something you own goes too) and a chat line tells you why; the wardrobe star reads "Already collected" on hover; the route pin refuses those items the same way.
- The currency and item icons in front of vendor prices in the widget are 40 percent smaller, sitting at the text height.
- New: the Route, one routing for the whole panel. Every raid and dungeon in the Raid Lockouts panel has [Add to route] beside its title: click it and the whole raid goes on your route. Vendor, mount and world-drop rows end with their own [Add to route]; the gold pin next to the star in the Appearances tab and the Mount Journal adds a single item too. Everything on the route sits under a Routing header with the big gold pin at the very top of the panel (the only icon in the panel), nearest first from where you stand, routed raids shown whole with their bosses and rows; [Remove all routes] at the bottom empties it (after a Yes). The route is the only thing that drives the guide: it starts on the nearest route entry as soon as you add one (or log in with a route), moves to the next entry when an item is collected or when the raid you just cleared is done for the week, and raids stay on the route through weekly resets until you take them off or every starred item in them is collected. Nothing else moves the list any more: raids below the Routing section sit in one fixed order by name.
- New option, Hide unattainable items (General): starred appearances and mounts that were removed from the game or never made it in stay out of the Raid Lockouts panel; the panel refreshes the moment you flip it.
- The [Guide me] and [Guiding] links after each row are gone (the route guides); every starred row ends with its star: clicking the star asks "Remove the star from this item?" and removes it on Yes (and, if the guide was leading there, moves it to the next route entry), the row disappears.
- The little map-pin icon after location links in the panel is gone; the link itself is the guide.
- New: General > Lockouts panel font. One font face, size and outline for everything in the Raid Lockouts panel: rows, boss lines, headers, the title and the toolbar. Boss lines and headers sit a step above the chosen size; the title and the toolbar labels shrink to fit their space. The face list is the same shared-media list the alerts use; on a non-English client the panel draws with the game's own font for your language (the bundled faces only carry Latin letters) and the size and outline still apply. Reset font puts the defaults back.
- A raid cleared on this character reads "Everything done this lockout" on its own line under the title instead of a tag on the title line.
- Each character's lockout state now sits directly under the item it applies to, and only for the difficulties that item drops on: an item tagged Heroic has "Cryptstep [Heroic]: Available" under it, an item tagged Normal and Looking For Raid has those two lines. A boss no longer ends in one block covering every difficulty. A single drop on the route shows the same under its row in the Routing section.
- The character picker still defaults to This character, but its menu now also lets you tick other characters to show alongside it; All characters still shows everyone. Which characters you ticked is remembered between sessions.
- New Ignore Characters tab in settings: check off characters you never want to see and they drop out of the character picker, All characters and search by name, everywhere in the panel. One click ignores every character except the ones on your current server. The character you are playing is never hidden, even if it is checked.
- Every Social box is on by default, the emote included: a profile saved while one of them was off gets them all switched on once, and from there your own choice stands.
- In the Routing section, a raid or dungeon that reads "Everything done this lockout" always sits at the bottom of the Routing list, whether or not it is the one being guided. It stays on the route and inside the section; it just goes last, since there is nothing to go there for until the reset.
- Access lines under raids (and in the wardrobe tooltip) draw in legendary orange, whatever their state says.
- Every route change answers in chat ("Tomb of Sargeras added to the route", "removed from the route"), so a click is never silent.
- Item icons on the rows are a quarter bigger; currency icons on cost lines are half again as big.
- Raid and dungeon headers are just the name with [Add to route] / [Remove from route] beside it; hover the name for where the raid is (its zone) and which expansion it belongs to. Zone and expansion still match the search box.
- Searching "routing" (or "route") in the panel shows only the route. /am stop rests the guide until the route changes; the route itself stays.
- A gold switch sits beside the Routing header: flip it off to rest the route's guide for now, so TomTom's own waypoints and arrow show again; flip it on to resume. The route keeps its entries, the switch stays where you left it through reloads and logouts, and the guide comes back on its own when you change the route (/am stop flips it off too). The Routing pin is a touch smaller.
- Event-only vendors no longer show a bare "acq_note_limited_event" / "acq_note_timewalking" line: the note reads "Only present during a limited-time event." or "Only sold during the Timewalking event." in your language.
- Access lines name every holiday (Brewfest, Hallow's End, the Darkmoon Faire and the rest) from the game's own calendar instead of "event 1719", on every region.
- A quest requirement names the quest when the game knows it ("Requires the quest "The Tabiqa" first") and otherwise reads plain "Quested", never "quest 9121".
- The Raid Lockouts panel wears the same window as the other Infinite Loop Alchemist addons: dark backdrop, the AppearanceMatic medallion in the corner, a big pink title, a divider, and a resize grip in the bottom-right corner (drag it to make the panel wider or taller; the lines re-wrap and the size is remembered). Search and character picker sit on one toolbar strip under the divider, each taking half of the panel's width (they grow with the panel); the minimize button stays next to the close button.
- New Detection tab right after Alerts: it shows the other Infinite Loop Alchemist addons AppearanceMatic looks for on your client. GearMatic has its card (icon, description, link) with a switch that sits off until GearMatic is installed, what its gear auto-sell (legacy gear, or gear under the item level you set) and auto repair are set to right now, and the note that while GearMatic is installed AppearanceMatic keeps every item whose appearance you have not collected yet out of that auto-sell (needs the GearMatic release that carries the keep hook).
- The Social tab does something now. Collecting a starred appearance or mount posts one line: to your guild (Guild box), and to your party, raid or instance chat when you are grouped (their boxes), or as an emote when you are not grouped or Emote is the only box on. All five boxes start on; profiles saved before this get Guild switched on once.
- Options: the guide arrow's settings moved from General to their own Guide arrow tab right after Sound, opening with a note on what the arrow does.
- Options: the panel's settings (window and font) moved from General to their own Widget Panel tab right after Alerts, opening with a note on what the panel is and is for; the login line option is now Welcome message (its tooltip says it carries the version and the help command); Discord, Buy Me a Coffee and Follow on X in General now show their icons, and the Infinite Loop Alchemist line shows its profile picture.
- /am help lists every command: /am, /am settings, /am test [appearance|mount], /am stop, /am reset, /am here, /am diag, /am changelog and /am help. New: /am changelog opens the change log.
- Hover the panel's medallion for a hint, right-click it for a menu: Wardrobe, Mounts, Change log, Settings. A new minimap button with the AppearanceMatic medallion opens the panel on left-click and the settings on right-click; drag it around the minimap ring, or hide it with the new Show minimap button option (General).
- The panel is titled AppearanceMatic, the name as written. Its text is a cream white throughout (title, rows, boss lines, the Routing header, [Add to route], the search box and the character picker); [Remove from route] is red.
- The expansion filter button is gone. A row of tick boxes under the search box (they stay put while you scroll) picks what the panel shows: the current expansion (Midnight), Legacy (earlier expansions), Vendors, Mounts and Other. All are on to start; untick one and that part leaves the list, with a count of what is hidden at the bottom. Your choices are remembered.
- The star's Yes/No prompt now says the star also comes off your favorites in the Appearances tab or the Mount Journal.
- The guide no longer sends you through a portal, boat or rootway your character has not unlocked yet. When the only way across needs a quest, level, spell, reputation or race you do not have, the guide names it ("Portal to Undermine is not unlocked on this character yet, travel manually") instead of pointing at a door that will not open. Unlocks are picked up the moment a quest turns in or you level.
- Vendor and world items now route with the guide's own directions to their known location; Blizzard's tracker keeps the map pin, the objective and the collected alert. Items whose zone is known but not the exact spot are guided to the zone first, then to Blizzard's exact waypoint inside it.
- Flight hops now tell you when an unlearned flight point would shorten the trip: "Fly to X (discover Y for a shorter route)".
- New "Access:" line under a raid, on guided rows and in the wardrobe tooltip when something must happen first: which faction controls Tol Barad or Wintergrasp and when the next battle starts (Baradin Hold, Vault of Archavon), or which campaign unlocks travel to Undermine, K'aresh, Azj-Kahet, Voidstorm, the Isle of Quel'Danas, Harandar, Zereth Mortis, The Maw and the Isle of Thunder. The guide's directions carry the same note while the gate is closed.
- Guides resume cleanly after a reload: the saved route and the saved guide join up instead of starting twice.
- Vendor, mount and world-drop rows now say what stands between you and them, in the panel and in the wardrobe tooltip: a quest to finish first, a reputation standing to reach, a faction or race the source is limited to, an event it only appears during, or that it was removed from the game (with the patch). Red when it blocks your character, grey when it is only good to know.
- Manaforge Omega's access line names its door quest, "The Tabiqa", once travel to K'aresh is open.
- Access lines now also cover Antorus, the Burning Throne (the Argus campaign up to the Antoran Wastes beacon), The Eternal Palace (the Nazjatar storyline), Amirdrassil, the Dream's Hope (the Emerald Dream portal from the Guardians of the Dream campaign) and Sporefall (the rootway to Harandar).
- The Eternal Palace can now be guided to: its entrance at the Gate of the Queen in Nazjatar was missing from the guide.
- Crucible of Storms and Ny'alotha, the Waking City show what opens their doors: the Azsharan Medallion questline ("Twist the Knife") and the Black Empire campaign up to "Network Diagnostics", named by the quest title once the client has it.
- When every way to a raid runs through a portal you have not unlocked, the guide now names the portal that is actually closed (the K'aresh portal in Dornogal), not the open Dornogal portal that merely starts the trip.
- Amirdrassil's access line accepts either Emerald Dream portal quest and Liberation of Undermine's accepts the Zandalar rocket drill, so a character who unlocked the zone another way is no longer told travel is blocked.
- The guide arrow and the lockouts panel do less work every frame and on every event: the arrow only rewrites its distance when the number changes, the panel builds each starred row once instead of twice, back-to-back lockout updates fold into one rebuild, and wardrobe tooltips remember an appearance's source and access lines until something about your character changes.
- The Bel'ameth boat to Gilneas is routable again (both directions).
- Antorus, the Burning Throne (and anything else in the Antoran Wastes) is routable: the guide leads to the Vindicaar beacon on Krasus' Landing in Dalaran, then points at the target below once you are aboard.
- Vendor and mount facts come only from the data shipped with the addon (and what your own panel already remembered), so the same items show the same information for everyone, with no other addon loaded.
- The /am harvest command is gone: everything it used to gather in game now ships with the addon.
Added
Data/Routes.luagateway rows carryreq = { quest, accquest, level, spell, reputation, race }generated from the TravelGuiderequirementsblock;Route:GatewayUsableevaluates them (fail closed),Route:GatewaysTowardskips locked rows,Route:NextHopnames the locked gateway (route_gateway_locked_fmt); locks re-evaluate on QUEST_TURNED_IN, PLAYER_LEVEL_UP, SPELLS_CHANGED, UPDATE_FACTION.Data/Gates.lua+LockoutsGates.lua: access gates keyed by journal instance ID (controlvia Blizzard's per-faction AreaPOI pins 2485/2486 andC_PvP.GetZonePVPInfo/GetOutdoorPvPWaitTime;travelvia the zone portal quests, character then account-wide), rendered byLockouts:GateLinein the panel, tooltip and route title; re-read on AREA_POIS_UPDATED, PVP_WORLDSTATE_UPDATE, QUEST_TURNED_IN, zone changes.R.GATEWAYS_STAMP(TravelGuide commit) andR.ENTRANCES_STAMP(WoW build);tools/refresh-travel.sh --check(exit 0 / 3 NEWER / 2 CHECK_FAILED); gen-gateways aborts when a regeneration would drop a shipped row unless--allow-removals;/am diagprints the stamps and locked gateways.Route:DiscoverHintandRoute:TaxiNodes(..., includeUndiscovered);route_discover_hint_fmt.- Tests:
tests/gate_state_spec.lua,tests/gen_gateways_req_test.py(+ fixture), lock/hint cases intests/route_spec.lua, shipped-route and resume cases intests/track_link_fallback_spec.lua, stamp and req-shape checks intests/gateway_coverage_spec.lua. tools/webgrab-extension/(Firefox WebExtension, MV2): reads wiki/wowhead pages in the user's own browser at a person's pace (one tab, 8-20 s between pages, breaks, 60 pages per run, an hour off on a block screen) and saves page digests;tools/webgrab-jobs.py gates|urlsbuilds the page list,tools/webgrab-import.pyreports door quests with IDs, access sentences and campaign mentions per gated raid;tests/webgrab_import_test.py.Acquisition:GateFromGroupreads the gating facts recorded up a source's group chain (availability code, event, removed-with-patch, prerequisite quests, minimum reputation standing, faction or race restriction; a header's value applies to everything under it) intogate = {...}on every vendor/mount/drop row; the dump and export tools ship it, plusD.events(event names in all 11 client languages) andD.phases(availability codes and states).Acquisition:AccessLines(rec)checks them live (C_QuestLogcharacter/account,C_Reputation.GetFactionDataByID().currentStandingagainst the shipped standing value,UnitFactionGroup,UnitRace/C_CreatureInfo.GetRaceInfo,GetBuildInfoagainst the removal patch) and the panel (addSourceLines) and wardrobe tooltip render them as Access lines;acq_access_*strings in 12 locales;tests/acquisition_access_spec.lua.Data/Gates.luarows for 946 (Antoran Wastes, quest 48200), 1179 (Nazjatar, 54972/55053), 1207 (Emerald Dream, 76318) and 1305 (Harandar), read from the WebGrab sweep of every raid's wiki page against the TravelGuide portal requirements;gate_campaign_argus,gate_campaign_nazjatar,gate_campaign_guardians_of_the_dreamin all 12 locales;tests/gate_state_spec.luacoverskind = "quest"rows.tools/gen-entrances.pycarries aSUPPLEMENTtable for raids Blizzard draws no entrance pin for (The Eternal Palace, 1179, at the Gate of the Queen per the wiki), used only while the pin table still lacks the instance;tools/webgrab-jobs.pyfollows up to 12 quest links per raid page (was 4/8) so a sweep reads whole questlines.Data/Routes.luaTRANSITS: Dalaran (627) -> The Vindicaar (887) at the Krasus' Landing flight master position from Blizzard's TaxiNodes row 1774 (req = { quest = 48200 });tests/gateway_coverage_spec.luaasserts the row.Data/Gates.luakind = "quest"rows for Crucible of Storms 1177 (doorQuests 53763 Alliance / 54126 Horde) and Ny'alotha 1180 (58506), quest IDs read from the wiki quest pages by the follow-up WebGrab job..pkgmeta(package-as AppearanceMatic, manual changelog, LibMatic as afile://external,tools/tests/docs/release.shignored) andrelease.sh(localization parity gate, in-game changelog gate, BigWigs packager in.release/, zip deleted after a clean upload);Libs/LibMaticis no longer tracked (local symlink, packager external),.release/and*.zipignored. Verified with a-d -zdry run: the package holds only the addon files and libraries.- Gate rows accept
doorQuests = { id }(andkind = "quest"): once travel is unlocked the line names the raid-door quest by its client-side title (C_QuestLog.GetTitleForQuestID, loaded viaRequestLoadQuestByID, refreshed on QUEST_DATA_LOAD_RESULT);gate_quest_*strings in all 12 locales.
Fixed
- Doubled addon name on every say() line:
Social:Line(43ecddd) prepends the star andAppearanceMatic:to each routing and alert line, but the 17 localized strings that flow throughsay()(route_print_*,lock_route_*,lock_routing_*,lock_*_collected_*,lock_stars_removed,lock_no_route_no_entrance,lock_track_limit_reached,fav_print_no_location) still opened with their own|cffffb6d9AppearanceMatic|r:, so the emote went out as{rt1} AppearanceMatic: AppearanceMatic: .... The prefix is stripped from those keys in all 12 locales;Social:Lineis the only place the name is added./am stopinLockoutsEvents.luaprintedlock_routing_stoppeddirectly and now goes throughsaylike the Routing switch.tests/locale_load_spec.luaasserts none of the say() keys names the addon, in any locale. - Rebuild storm: the panel drove its own redraw.
Lockouts:ScheduleRebuildclearedpendingRebuildbefore callingRebuild, so any ask raised from inside the draw armed a fresh 0.1 s timer instead of coalescing;Lockouts:ResolveItemDisplaycallsRequestItemLoadfor every source whoseitemLinkcarries no name, on every rebuild, andItemMixin:ContinueOnItemLoadruns its callback immediately for an item the client already has (wow-api MCP), so that callback fired inside the draw and unconditionally calledScheduleRebuild. Measured in game at 280 rebuilds in 118 s, 2.37/s, 20.06 s of CPU, 17 percent of frame time, withCollectStarredDropsre-scanning all 400 favourites each time (112000VisualDropscalls = 280 x 400). Fix at the source:RequestItemLoadrecords each item inLockouts.loadedItemsand never asks twice, and only schedules a redraw whenC_Item.GetItemNameByIDshows a name actually arrived;Lockouts:ForgetNamelessItemsreopens them onTRANSMOG_COLLECTION_UPDATEDand onBlizzard_EncounterJournalload. Defence in depth:pendingRebuildis now held across the draw behind arebuildingflag, so any number of asks raised during a draw coalesce into one follow-up, and a draw that errors still clears both flags and re-raises.tests/rebuild_loop_spec.lua. Lockouts:VisualDropsnever cached anincompleteresult, so a starred appearance whose drop data had not arrived (encounter journal not loaded, secret values inside an instance) was re-derived fromC_TransmogCollectionon every rebuild forever, about 21 ms of every rebuild. It is now cached with aretryAtstamp and re-asked afterDROP_RETRY_SECONDS(20 s) instead; the item stays listed anddropsIncompletestill drives the loader exactly as before.tests/drop_cache_retry_spec.lua.
Fixed (tooltip preview)
TooltipPreview:OnItemTooltipgated on its ownTransmoggable(link)before callingLockouts:ShowHoverPreview, while the widget's own hover (LockoutsUI.lua:1536) callsShowHoverPreview(link)with no gate at all. The gate was strictly more restrictive than the function it guarded, in three separate ways, so bag gear the widget previews fine showed nothing on its tooltip:TransmoggabletestedC_TransmogCollection.GetItemInfo's FIRST return,itemAppearanceID, whileShowHoverPreviewuses the SECOND,itemModifiedAppearanceID(LockoutsUI.lua:1753). Confirmed return order via the wow-api MCP. Two different questions.ShowHoverPreviewfalls back to the wrapped raw link when there is no source ID at all (sourceID or ("|H" .. link .. "|h[]|h"),LockoutsUI.lua:1755), so it can show items the gate rejected outright.- The
moggablememo added earlier this session cached a negative per itemID, andC_Item.IsItemDataCachedByIDcan report an item cached before its appearance data has arrived, making a false answer permanent for the session. The gate and its memo are deleted.OnItemTooltipnow makes the identical call the widget makes and letsShowHoverPreviewdecide, which it already did: a failedTryOnsetsshown = falseand hides the frame (LockoutsUI.lua:1756-1766).
ShowHoverPreviewnow remembers a refusal on the frame (previewRefused) alongsidepreviewKey, so an item tooltip refreshing over something that cannot be worn does not redo the model work on every refresh. That was the only thing the gate was buying, and it belongs in the shared function where the widget path gets it too.tests/tooltip_memo_spec.luadeleted with the memo it covered.
Fixed (error reporting)
Every swallowed error was classified, and the ones that mean this addon is broken now reach the game's error handler. New
pluginShared.reported(fn, ...)inFavorites.luaisguarded's twin: same return shape, but it runsxpcall(fn, handleError, ...)so the error still reachesCallErrorHandler(falling back togeterrorhandler()) with a traceback taken where the error happened, while the addon carries on. 35 call sites across 12 files converted, including:Favorites:Notify's listener dispatch (a listener throwing is this addon breaking, and swallowing it is how the combat bug stayed hidden); every widget setter in the wardrobe and Mount Journal star refresh; the model-scene calls in the hover preview; the loading spinner's animation group;Lockouts:VisualDrops' collection scan;Acquisition:PlaceTitle'sC_Map.GetMapInfoon a shipped mapID;Route:GatewayUsable's six requirement checks against shippedData/Routes.luarows; andLockouts:DryBuild, which runsBuildContentwith the panel closed and was the one path where a broken draw could still go unseen.Lockouts:ScheduleRebuildusedpcallthenerror(err, 0)to rethrow. That reported, but the traceback pointed at the rethrow rather than at whatever actually broke. It now usesxpcall(Lockouts.Rebuild, handleError, Lockouts), so the trace is the real failure site.Tracking:Toggle's untrack branch discarded the result ofC_ContentTracking.StopTrackingand returned true regardless, telling the caller it had worked when it may not have. It now reports a genuine failure and hands the real answer back. The bulkStopAllloop got the same treatment.Deliberately left quiet, each for a named reason rather than "defensive":
GameTooltip:SetHyperlink's catch-all fallback andModelSceneActor:TryOn(Blizzard's designed "cannot preview this" channel, both fire constantly on ordinary hovers);C_QuestLog.GetTitleForQuestID(answers empty untilQUEST_DATA_LOAD_RESULTarrives, by design, withRequestLoadQuestByIDas the deliberate follow-up);C_Map.CanSetUserWaypointOnMap(legitimately false in instances and sanctuaries, and the caller already retries up to five parent maps);worldFromMap(many uiMapIDs genuinely have no world position, the canonical case);C_SuperTrack.GetNextWaypointForMapandC_ContentTracking.GetNextWaypointForTrackable/IsTrackable/IsTracking(a "no" is the everyday answer, called per row on every build); the TomTom cleanup calls inRoute:StopandGuideArrow's pin hiding (a third-party addon owns that state and a uid can be stale); the optional-addon probes inLockoutsEvents,FavoritesMountsandDetection; andTooltipPreview:Transmoggable'sGetItemInfo(item-not-cached is the expected outcome, as the code's own comment says). Reporting any of these would push hundreds of entries an hour into BugSack and bury the errors worth seeing.A broken panel draw no longer disappears.
Lockouts:Rebuildswallowed it withpcalland printed it into the panel, which is exactly why the combat bug above never reached BugGrabber. It now usesxpcall(self.BuildContent, handler, self)where the handler passes the error toCallErrorHandler(falling back togeterrorhandler()), so BugGrabber and friends record it with a traceback taken at the point of the error. The panel still draws every row it managed, still shows the friendly line, and still recordsAppearanceMaticDB.diag.rebuildError.xpcallwith extra arguments is confirmed supported from Blizzard's own use of the same shape atBlizzard_TransformTree/TransformTreeBaseNodeMixin.lua:195andBlizzard_Settings_Shared/Blizzard_SettingsPanel.lua:525, read through the FrameXML index rather than recalled.
Fixed (secret values in combat)
- Starring anything mid-fight raised a Lua error that the panel printed into itself.
Lockouts:RebuildwrapsBuildContentinpcall(LockoutsUI.lua:970) and renders the error as a line, which is why it never reached BugSack. Root cause: WoW 12.x returns "secret" values from several APIs in combat and inside instances, and touching one raises, including comparing it, concatenating it, running a string method on it, sorting it, and using it as a table key.type()on a secret string still returns"string", which is precisely why this codebase's working guards (Favorites.lua:463,:557,Lockouts.luaSnapshotCharacter) always pair the type check withissecretvalue. Several sites checked only the type. - Shared
pluginShared.plain(value)added inFavorites.lua(first in the TOC), with a local fallback inLockouts.luaso that file still stands alone under the specs.Lockouts:Plain(value)wraps it and recordscombatHidden/hiddenCountso a rebuild can explain itself. - Sites fixed:
linkHasName(Lockouts.lua:83, didlink == ""thenlink:matchonC_TransmogCollection.GetAppearanceSourceInfo'sitemLink, reached fromResolveItemDisplayfor every source of every starred item on every rebuild, the prime suspect);itemHasName(name ~= ""onC_Item.GetItemNameByID);ResolveItemDisplay'siconanditemLink;VisualDrops'info.isCollected,info.sourceType(compared againstBOSS_DROP_SOURCEandVENDOR_SOURCE),info.itemID,info.icon, and every entry ofdrop.difficulties, which become table keys inCollectStarredDropsand are then sorted andtable.concated inrenderDropRow; and thefor index = 1, GetNumSavedInstances()loop bound inSnapshotCharacter, the one value in that function that was not already run through its localplain. - A second sweep found five more, all reachable from a rebuild:
ResolveItemDisplaystoredC_TransmogCollection.GetSourceItemID's return unguarded and passed it toRequestItemLoad, which uses it as a key intoloadedItemsandpendingItems, and a secret value used as a table key raises (Blizzard's ownBlizzard_SharedXMLBase/SecureTypes.lua:34assertsnot issecretvalue(key)for exactly this);Lockouts:MountLinkcomparedC_MountJournal.GetMountLink's return against"";Favorites:FavoritedUncollectedMountsstoredGetMountInfoByID's name, spellID and icon raw, and the name is concatenated into a row inLockoutsUI.lua;Acquisition:PlaceTitlecompared and then concatenatedC_Map.GetMapInfo'sname; andLockouts:VisualOfSourcestoredGetSourceInfo'svisualIDraw, then compared it and used it as a table key.RequestItemLoadis now guarded at the point of use as well as at its caller, since it is the one that holds the table keys. - A
VisualDropsscan during which the game withheld anything now setsresult.incomplete, so a partial answer is never cached as final. - The panel now explains itself instead of showing Lua:
Rebuildadds onelock_combat_hiddenline when anything was withheld, and if a draw does still fail whileInCombatLockdown()is true the player gets that same line rather than the raw error. The error text still goes toAppearanceMaticDB.diag.rebuildErrorfor bug reports. PLAYER_REGEN_ENABLEDis registered: leaving combat drops the drop and display caches and rebuilds, so the withheld rows fill in on their own. New keylock_combat_hiddenin all 12 locales, 267 keys x 12.tests/combat_hidden_spec.lua.
Added
Lockouts:RaidFinderHintanswers for a current-season raid instead of returning an empty string.Lockouts:RaidFinderNPCdeliberately returns nil whenIsCurrentSeasonInstanceis true (LockoutsRoute.lua:1795, current-season raids queue from the Group Finder rather than an NPC), and the hint's only caller silently dropped the row's line as a result, so an LFR-only drop in the current tier showed no route information whatsoever. The hint now names the Group Finder and carries aw2tsgroupfinderlink, handled inLockouts:HandleRouteLink, that callsLockouts:OpenRaidFinder:C_AddOns.LoadAddOn("Blizzard_GroupFinder")thenPVEFrame_ShowFrame("GroupFinderFrame", RaidFinderFrame), matching the call Blizzard's own Raid Finder makes atBlizzard_GroupFinder/Shared/RaidFinder.lua:20(read through the wow-api FrameXML index, not from memory). The link label prefers the game's ownDUNGEONS_BUTTONso it reads exactly like the button the player clicks, falling back tolock_group_finder. New keyslock_raid_finder_currentandlock_group_finderin all 12 locales.tests/group_finder_hint_spec.lua.
Changed (performance)
Measured from a live whole-addon run (344 s, LFR with constant loot, panel open, ~400 starred items): Lockouts:BuildContent totalled 30954.7 ms, about 9 percent of wall time, across 429 calls at 72.155 ms each.
Lockouts:AddLinewas the largest single cost: 727159 calls, 7529.0 ms self, roughly 1695 lines per rebuild, every one of them re-fonted, re-placed, re-coloured, re-texted and re-measured even when the rebuild produced an identical list. A line now records what it was drawn as (text, font object, indent, width, vertical offset, colour) plus its measured height, and a line that would be drawn the same way is onlyShow()n. This also removes the font chain that rode on top of it:Lockouts:ApplyFont4799.9 ms total,Lockouts:FontSettings2182767 calls,PanelFontPath/PanelFontSize/PanelFontOutline/CustomFontsAllowed727589 calls each.Lockouts:ForgetLineCache, called fromRefreshFonts, drops the record when a font change makes the measured heights wrong; a secret string never takes the fast path since it cannot be compared.tests/line_reuse_spec.lua.Lockouts:ResolveItemDisplaywas second: 220672 calls, 6190.4 ms self, two Blizzard lookups per source per rebuild for an answer that never changes once the game has it. Now cached per sourceID, with the sameretryAttreatment as the drop cache for a source the game cannot name yet, cleared byInvalidateDropCache.tests/resolve_display_cache_spec.lua.Lockouts:RaidFinderNPCran 357786 times, about 834 per rebuild. Past the difficulty test its answer turns only on the raid name, so each raid's queue NPC is worked out once.Rebuild rate is now self-limiting. 429 rebuilds in 344 s is 1.25/s, driven by event bursts rather than by anything the player did.
Lockouts:RebuildWaittimes the previous draw withdebugprofilestopand waitsREBUILD_DUTY(19) times that long before the next, floored at 0.1 s and capped at 3 s, so the panel takes at most about a twentieth of wall time no matter what storms the event queue. A cheap draw still waits the old tenth of a second.tests/rebuild_loop_spec.lua.Favorites:FavoritedUncollectedMountswalkedC_MountJournal.GetMountIDs(), every mount in the client, on everyBuildContent. That is an O(all mounts) floor per rebuild that does not shrink when the player has few starred mounts, and it was paid again byLockouts:DryBuildon every route check with the panel closed. It is now built once and kept, invalidated byFavorites:Notify(which every star mutator already calls), byNEW_MOUNT_ADDEDinAlert.lua, and byTRANSMOG_COLLECTION_UPDATED.tests/mount_scan_cache_spec.lua.TooltipPreview:TransmoggableranC_TransmogCollection.GetItemInfoandC_Item.IsDressableItemByIDon every firing of a hook that fires for every item tooltip client-wide and re-fires while a tooltip stays open. Both answers are fixed per item, so they are memoised by itemID. A negative reached before the client had the item's data is deliberately not kept: it is only stored onceC_Item.IsItemDataCachedByIDconfirms the answer was real.tests/tooltip_memo_spec.lua.tests/tooltip_preview_spec.luagained a separate undressable item, since flipping one item's dressability mid-session is not a state the game can produce.Acquisition:NpcNamehad a positive cache but no negative one, so an NPC the client could not name re-issued a fullC_TooltipInfo.GetHyperlinktooltip build on every row of every rebuild. Misses are now held forNPC_RETRY_SECONDS(30) in a session table, cleared when the name finally arrives.The Mount Journal star poller (
Favorites:EnsureMountStar/EnsureMountRoute, driven at 0.15 s byFavoritesMounts.lua) re-appliedSetDesaturated,SetAlphaandSetTextureto every visible row on every tick regardless of whether anything had changed, which is the common case. Each row now remembers what it was last drawn as, keyed by mountID, and skips the setters when they match.Not changed, and why: the 0.3 s guide ticker was reported as uncapped per-tick API work, but the live profile measured
Lockouts:RouteGuideToTomTomat 9.1 ms across 381 calls, 0.024 ms each including everything it calls, which is 0.008 percent of the run.Lockouts:DryBuildgenuinely runs a fullBuildContentwith no panel-visible guard (LockoutsRoute.lua:1213-1228), throttled to 5 s while guided and 60 s while idle; its per-call cost drops with the mount and drop caches above, and it is now its own profiler row, so it is measured before the routing core is restructured on an estimate.The profiler measures the whole addon instead of a hand-kept list of 16 methods, which is what let the rebuild storm's neighbours hide.
Profiler:Targets()walkspluginSharedand wraps every method of every module it owns (Module:Methodlabels), skipping onlyProfileritself, Ace'sdb, and the addon object whose methods are Ace's rather than this addon's. The wrapper is allocation free and passes through every return value via a tail call into afinish(rec, started, ...)helper, replacing the old five-value truncation that was only safe because the target list was curated. Nesting is tracked with a depth counter and a per-level child-time table, so the report can order by time a method spent in its OWN body rather than by a total that is inflated by everything it called; a frameOnUpdateresets the depth each frame so a method that throws cannot leave the counter skewed. The report caps at 20 rows and sums the rest into one line, adds the game's ownGetAddOnCPUUsagefigure when script profiling is on so the numbers can be compared against an external CPU meter, and states that measuring everything costs time of its own. New locale keysprofile_more_fmt,profile_cpu_fmt,profile_overheadin all 12 locales;profile_row_fmtgained a self-time column.tests/profiler_spec.lua.RouteList:Orderedis built once and kept until the route changes (cleared innotify, which every mutator already calls), and carries a position index soRouteList:Position/RaidPositionare table reads instead of a rebuild-plus-sort-plus-linear-scan.BuildContentasked for a position once per starred row, several hundred times per rebuild.tests/route_order_cache_spec.lua.Lockouts:PanelFontPathkeeps its answer against the client locale and the chosen face instead of callingLibStubandLibSharedMedia:Fetchon everyAddLine, roughly a thousand times per rebuild.tests/panel_font_spec.lua.raidFinderNames()andLockouts:ExpansionLevelByNamememoise (LFR difficulty names andEXPANSION_NAME*are fixed for the session); both were re-derived per starred raid row,ExpansionLevelByNamewalking 21 globals each time.Route:ZoneMapmemoises per mapID (up to 6C_Map.GetMapInfopcalls per route tick for the same map) andRoute:GatewaysTowardmemoises its BFS per continent pair, cleared byRoute:InvalidateGatewaysand byRoute:RefreshGatewayLockssince that mutatesg.lockedin place.GatewaysTowardwas 464 ms of the run, the bulk ofRoute:NextHop.GuideArrowcounts the wait for a bearing in seconds, not frames:ATTACH_RETRY1.0 s,NO_BEARING_AFTER0.35 s,STALL_GIVE_UP5.0 s replace the 60 / 90 / 300 / 600 tick gates (at 30 fps those were 2 s / 3 s / 10 s / 20 s), and the pin re-attaches on the first missed read instead of 60 frames later;tests/guide_arrow_timing_spec.lua.Lockouts:ToggleRouteItemtells a removal apart from a refusal (RouteList:Hasbefore the toggle), so clicking the route control on something already collected no longer prints "removed from the route";Lockouts:PlaceHoverPreview(frame, anchor)splits the placement out ofShowHoverPreviewand runs on every show, so a model that keeps its scene still moves clear of a comparison tooltip that opened since.The show-box strip shrinks its labels together (
Lockouts:FitFilterBoxes, floor 8 px) when five labels at the panel font size would pass the panel's right edge (narrow panel, long localized names); re-fit on resize, on a label change and after a font change. Vendor rows that are mounts skip the current/legacy expansion test (a mount has no expansion of its own) instead of always counting as legacy.tests/filter_fit_spec.lua,tests/show_filter_spec.lua.Panel gate lines drew white:
local gateText, gr, gg, gb = self.GateLine and self:GateLine(raidName)kept only the first return (x and f()truncates to one value), so the colour never reachedAddLine; both draw sites callGateLineplainly now.tests/gate_line_color_spec.luarecords line colours throughAddLineunder a listed raid and under a routed drop.Localization/enUS.luaandenGB.luacarried an unescaped quote ingate_quest_blocked_fmt, which raised at load and dropped the 14 keys after it (blank campaign names, a Lua error on every door-quest access line);tests/locale_load_spec.luanow loads every locale file the way AceLocale does and checks parity and non-empty strings.Route:BlockingGatewaywalks the locked-inclusive chain and names the first locked hop;Route:NextHopused the nearest first-hop gateway even when it was open (tests/route_spec.lua).Route:Gatewaysrecords rows whose map the client cannot name inRoute.unresolvedGateways(/am diagprints them) instead of dropping them silently; the twoData/Routes.luaMoonlit Rose rows now carrymapID = 217.Data/Gates.lua: 1207 adds 76326, 1296 adds 83933, Harandar rows drop 86897 (a TravelGuidehideAfterQuest, not a requirement).GuideArrow.luaOnUpdate: locale strings cached, colour table reused, yards text formatted only when the rounded value changes, stall text formatted once;LockoutsUI.luadecides the guided rows fromIsGuided/the route title instead of rendering every row into a capture buffer first;UPDATE_INSTANCE_INFOgoes throughScheduleRebuild;Favorites.luacaches the tooltip record and access lines per source (invalidated on the lock and gate events) and shows "Location" instead of a raw map ID when the zone name is unavailable.
Changed
Profiler:Elapsed()freezes the measured window whereStop()left it instead of counting idle time into a later/am profile report;/am profile resetsays whether a run is still going (profile_reset_idle, 12 locales);Lockouts:HideLoadingCover()forgets an unfinished wait so the loader's 15 second ceiling only counts time it was actually on screen, not time the panel spent closed.LockoutsLoading.lua(new):Lockouts:PanelLoading()reads the panel's owndropsIncompletesignal,EnsureLoadingCover()builds a DIALOG-strata cover on the panel (a 25 percent black scrim, a 48 pxInterface\COMMON\StreamCircledisc falling back to the addon's own icon, a looping Rotation of -360 degrees over 1.1 s, a GameFontNormal label),UpdateLoadingCover()holds it for a 0.4 s floor so it never flashes and reveals at a 15 s ceiling whatever the data does, polling at 0.2 s while up, and setspanelRevealedso a later background refresh never covers a list in use;HideLoadingCover()on the panel's hide; called from the end ofLockouts:Rebuild;lock_loadingin 12 locales;tests/panel_loading_spec.lua.- Performance:
Route:TaxiNodeskeeps each map's flight-point list for 3 seconds instead of rebuilding it fromC_TaxiMap.GetTaxiNodesForMaptwice on every 0.3 s route tick (about 7 rebuilds a second while guiding), cleared at once byRoute:InvalidateTaxiNodes()on TAXIMAP_OPENED and on a gateway rebuild, and never kept when there is no clock to age it by;Acquisition:WithCostbuilds a record'scostTextonce the game has every name in it (FormatCost/ItemName/CurrencyNamenow return whether the name came from the game, and a line still holding a placeholder is rebuilt next time rather than kept for the session) instead of reformatting it on every panel rebuild;tests/route_taxi_cache_spec.lua. Profiler.lua(new) with/am profile:Wrap(owner, key, label)swaps a method for a timing wrapper readingdebugprofilestop(),Start()wraps sixteen named hot paths (guide arrow OnUpdate, wardrobe preview poll, route tick / next hop / flight nodes / gateway search, guide router, panel rebuild, starred drop scan, appearance drop lookup, try-on model, item tooltip hook, wardrobe and mount stars, tooltip acquisition lines, acquisition lookup),Rows()reports count, total, mean and worst milliseconds slowest first,Memory()readsUpdateAddOnMemoryUsage/GetAddOnMemoryUsage,Stop()restores every real method; nothing is wrapped until a run starts./am profiletoggles a run and prints the report,/am profile reportprints without stopping,/am profile resetzeroes the counters;profile_*andcore_cmd_profilein 12 locales;tests/profiler_spec.lua.TooltipPreview.lua(new):TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item)on GameTooltip and ItemRefTooltip only, link fromTooltipUtil.GetDisplayedItem(tooltipData.hyperlink fallback), transmoggable =C_TransmogCollection.GetItemInfoappearance > 0 andC_Item.IsDressableItemByID;Lockouts:EnsureHoverPreview(anchor)keeps one model frame per tooltip (self.hoverPreviews), clamped to the screen;ShowHoverPreview(link, anchor)keeps the model it has when the same item comes back on the same tooltip with the same dress setting (item tooltips re-run their post call on every refresh) and places it past any shown ShoppingTooltip1/2 throughPreviewSideOffset(anchor, toRight), flipping left at the screen edge;HideHoverPreview(tooltip)hides that tooltip's model, no argument hides all; aSetItemRefhook drops the chat-link tooltip's model when it is reused for a non-item link (dungeon score, PvP rating); OnHide hooks on both tooltips;generalSettings.itemTooltipPreviewdefault true, toggle order 45 in the tooltip group;item_tooltip_preview_*in 12 locales;tests/tooltip_preview_spec.lua,tests/hover_preview_repeat_spec.lua.Favorites:AppearanceCollected(visualID)(C_TransmogCollection.GetAppearanceSourcesanyisCollected),Favorites:MountCollected(mountID)(GetMountInfoByID11th return),AlreadyCollected(isMount, id),SayCollected();Favorites:Set/MountSetrefuseonfor collected things (clearing a stale star, notifying only then) and return the final state;RouteList:Addrefuses collected items before the star side effect andRouteList:Togglereturns its answer (a refusal used to read as a route add, announcing it and starting a guide); wardrobe star tooltip showsfav_tooltip_collected;fav_print_already_collectedin 12 locales;tests/star_collected_spec.lua.Detection.lua(new):Installed(name)/Version(name)(C_AddOns.IsAddOnLoaded,GetAddOnMetadata),SwitchTexture(name)(the route switch art),StatusText(name),GearMaticState(missing / hookless / installed),GearMaticAutomation(readsGearMatic.db.profile.autoSellGear/autoSellGearRule/autoSellGearIlvl/autoRepair),AutoSellText/AutoRepairText,KeepUncollected(link)(C_TransmogCollection.GetItemInfosource,PlayerHasTransmogItemModifiedAppearance),AttachGuard()registering withGearMatic.VendorAutomation.RegisterSellGuardon ADDON_LOADED(GearMatic) and PLAYER_LOGIN;BuildDetectionPage(order 2.2) with the roster card, live automation lines and the keep note;opt_detection_*,detect_*in 12 locales;tests/detection_spec.lua. GearMatic side:VendorAutomation.RegisterSellGuard(GearMatic, unreleased).BuildArrowPage(order 3.5): the guide arrow frame controls and toggles under anintrodescription (opt_arrow_tab_desc, 12 locales) as their own tab.Social.lua(new):Social:Grouping(IsInGroup/IsInRaid/IsPartyLFG/IsInInstancepvp/IsInGuild),Social:ChannelsFor(social, grouping)(one group chat type, GUILD, EMOTE only without a group line),Social:Send(group/guild throughLibMatic-Broadcast-1.0:Broadcastmarker 8, emotes throughC_ChatInfo.SendChatMessage),Social:Announce(kind, link)called fromAlert.lua'sOnSourceAdded(item link) andOnMountAdded(C_MountJournal.GetMountLink);social.guilddefaults to true with a one-time_socialAnnounceWiredflip;social_collected_*_fmt/social_emote_*_fmtin 12 locales;tests/social_spec.lua. Thenew-matic-addonskill now requires a sender behindBuildSocialPage.BuildPanelPage(order 2.5) holdslockoutsFrameGroupandlockoutsFontGroupunder anintrodescription (opt_panel_tab_name/desc); General overrides the extras'loginBannerlabel withtoggle_welcome_message*;support.discordIcon/coffeeIconandnav.extraLinks(X,opt_x_link) withMedia/Textures/discord.tga,bmc-logo.tga,x-logo.tga; the Startup command list carries every slash branch (core_cmd_test/stop/reset/here/diag/changelog);/am changelogbranch. Thenew-matic-addonskill now requires the support icons and the X link.Launcher.lua(new):Launcher:AttachMedallion(emblem hover hint, right-clickMenuUtil.CreateContextMenuwithMenuEntries, drag forwards to the panel),Launcher:Setup(LibDataBroker launcher object shown through LibDBIcon, state inprofile.minimap),SetMinimapShown/MinimapShown; Core re-runsSetupon profile change/copy/reset;Libs/LibDataBroker-1.1andLibs/LibDBIcon-1.0embedded;menu_*,medallion_hint,minimap_*,toggle_minimap_button*in 12 locales;tests/launcher_spec.lua.Lockouts:ShowFilter/SetShow/ExpansionFilterActive(LockoutsUI.lua) hold the show boxes inAppearanceMaticDB.widget.show(missing = on);PassesExpansionFilterreads them (currentcovers the current expansion and season instances,legacythe rest);ExpansionFilter/CycleExpansionFilter/ExpansionFilterLabeland the filter button are gone, withlock_filter_label,lock_hide_expansion,lock_all_expansions,lock_expansion_only,lock_expansion_filter_title/desc. FiveUICheckButtonTemplateboxes (FILTER_LEFT/TOP,CONTENT_TOP104) withlock_filter_legacy/otherandlock_filter_*_hintin 12 locales; toolbar rows under the medallion (TOOLBAR_TOP-64,FILTER_TOP-90,CONTENT_TOP118) spanning the full width;lock_panel_titleis AppearanceMatic (was APPEARANCEMATIC until 2026-08-30);lock_unstar_confirmreworded.tests/lockout_panel_spec.lua,tests/show_filter_spec.lua.Lockouts:PauseRoute/ResumeRoute/ToggleRoutePauseown the guide's rest state (routePaused);/am stopgoes throughPauseRoute. The Routing header places oneButton(Lockouts:EnsureRouteToggle/PlaceRouteToggle,GetUnboundedStringWidthafter the text) drawn fromMedia/Textures/route-toggle-on.tga/route-toggle-off.tga(256x128, both switches fit the same 244x116 box); the header pin is 18 px.lock_routing_resumed,lock_route_toggle_on/off/hintin 12 locales;tests/route_pause_spec.lua.- Route rework:
RouteListgainsraids(HasRaid/AddRaid/RemoveRaid/ToggleRaid/RaidPosition) andClear;Lockouts:TrackTarget/EntranceTarget/RaidFinderTargetreturn raww2ts*targets (the oldTrackLinklabels are gone) thatNoteRouteLinkrecords for the route;BuildContentrenders a Routing section fromLockouts:TravelOrder(nearest first, the guided entry anchored,MapWorldfor vendor and world spots) withrenderRaidshared by the section and the list, recordsrouteOrderfor the guide, prunes routed raids with nothing left oncerouteSettled(set byPruneCollectedFavorites) anddropsIncompleteallow, and keeps the list itself sorted by name (BuildRaidRouteremoved,PriorityRaidonly pins the instance you stand in).HandleRouteLinkhandlesw2tsrouteraid:andw2tsrouteclear(APPEARANCEMATIC_ROUTE_CLEARpopup);ToggleRouteItem/ToggleRouteRaid/ClearRoute/EndGuide/GuideRouteNext(announce)/RouteAdvanceIfDonedrive the guide from the route (login +12 s, ZONE_CHANGED_NEW_AREA +3.5 s, UPDATE_INSTANCE_INFO);ResumeGuideno longer adopts Blizzard-tracked items;IsGuidingQueryisIsRoutingQuery;Favorites:GuideToVisualfollowsTrackTarget. Stale route entries (an unstarred item; a raid with nothing starred left once settled, never one the unattainable filter hid) are pruned before the empty-panel return, which also keeps the clear link; single routed drops leave their raid block below; the guided entry is anchored whether it is tracked, entrance-routed or Raid Finder-routed (routeOrder[i].guided), andRouteAdvanceIfDonereads that flag; a collected item only advances the guide when it was on the route (or was the guided item) and/am stopis not holding it. Locale keyslock_routing_header,lock_nothing_routing,lock_route_clear,lock_route_clear_confirm,lock_route_clear_hint,lock_route_raid_hintadded andlock_guide_me,lock_guiding,lock_guiding_marker,lock_nothing_guiding,lock_guide_hint_track,lock_route_headerremoved in all 12 locales;tests/route_panel_spec.lua(new),tests/route_list_spec.lua,tests/routing_query_spec.lua(was guiding_query_spec),tests/track_link_fallback_spec.lua. LockoutsFont.lua:Lockouts:ApplyFont(fs, role, fixedSize)/RefreshFonts/ResetFontSettings/PanelFontPath(LibSharedMedia face on enUS/enGB, the locale font file on koKR/zhCN/zhTW/ruRU,STANDARD_TEXT_FONTelsewhere, the GearMatic handling) driven byprofile.lockoutsFont = { font, fontSize, fontOutline };AddLinefonts every line by role (lines keep wrapping: the client's hyperlink hit-testing ignoresSetTextScale, so a FitText-shrunk header answered clicks only where scaled and unscaled text overlapped); the RosterFrame title (applyFont), search box, character dropdown, filter and minimize buttons take the same font; options grouplockoutsFontGroupbuilt withSettings:BuildAlertFontControls;lockfont_*keys in 12 locales;tests/panel_font_spec.lua. The derived-fontTEXT_SCALEapproach is gone.Acquisition:EventNotereturns a locale key and the row builders store it asnoteKey(the offline dump has no locale, so the key used to be baked intonote);Acquisition:NoteText(rec)resolves it at render (panel, mount rows, wardrobe tooltip);tools/export-acquisition.luaemitsnoteKeyand migrates legacynotekeys on regeneration;Data/Acquisition.luaregenerated.Acquisition:CalendarEventNamesscans the client calendar once per session (18 months back, 12 ahead,C_Calendar.GetDayEventholidays) with the region aliases (EU/KR/TW IDs onto the shipped US ID, 237 onto 239);EventNamefalls back to it after the shippedD.events.Acquisition:QuestTitlereturns nil until the client names the quest; the access line then readsacq_access_quested("Quested", 12 locales) instead of a number (acq_access_quest_id_fmtremoved).RouteList.lua(AppearanceMaticDB.routeList, sequence-orderedappearances[visualID]/mounts[mountID],Has/Add/Remove/Toggle/Ordered/Position/First, adding stars the item); wardrobe and mount-journal route buttons (Favorites:EnsureRouteButton,EnsureMountRoute) withMedia/Textures/add-to-route.tga/add-to-route-dim.tga(256x256 uncompressed 32-bit);Lockouts:RouteIconLink/HandleRouteLink/ToggleRouteItem(w2tsroute:<a|m>:<id>:<sourceID>),NoteRouteLinkrecords each row's own track link during a build andGuideRouteItemfollows it after a dry build,GuideRouteNextafterOnTrackedCollecteddrops the collected item;BuildContentrenders the Route section above Guiding and hoists route raids to the front;lock_route_header,route_tooltip_*,lock_route_nextin 12 locales;tests/route_list_spec.lua.Acquisition:IsUnattainable(rec)(availability state 1, orrwpat or below the client build);generalSettings.hideUnattainable(default off) withtoggle_hide_unattainable*strings;Lockouts:AttainableOnly/PruneUnattainableDropsfilter vendors, other items, mounts and raid-drop mounts inBuildContent(mount entries now carryacq);Lockouts:UnstarLink/HandleUnstarLink(w2tsunstar:<a|m>:<id>:<sourceID>, first in the click chain,lock_unstar*strings) callFavorites:Set/MountSetand stop a guide aimed at the item.Lockouts:EnsurePanelbuilds the panel withLibMatic-RosterFrame-1.0:New(skipRows, the library's scroll/content pair hosts the lines;onRefreshre-wraps only when the content width changed, tracked byLockouts.builtWidth); drag and the resize grip respect the lock option;AppearanceMaticDB.widgetalso storesw/h;ApplyMinimizedhides the toolbar and grip at 60 px;embeds.xmlloads the library after FitText;tests/lockout_panel_spec.lua./am harvest,Acquisition:Harvestand thelock_harvest_*/core_cmd_harveststrings are removed; shipped data comes from the offline refresh.Acquisition:GetMount/GetVendorread shipped rows and the SavedVariables cache only; the live_G.AllTheThingslookup (SourceDB,HasSourceDB,NeedsRefresh,MergeWalk) is gone from the client.WalkVendor/WalkMounttake groups fromAcquisition.SearchGroups, which onlytools/dump-source.luasets while it buildsData/Acquisition.lua(the data itself is derived from the AllTheThings database, MIT licensed).Lockouts:RouteGuideToTomTomroutes vendor/world items fromLockouts:ShippedTargetbefore asking Blizzard's tracker (ShippedHopOverrideremoved); the track link carries a zone-only flag (:map:x:y:z), saved asguide.fallbackZoneOnly;Favorites:GuideToVisualgoes throughTrackLink/HandleTrackLink.- PLAYER_ENTERING_WORLD resumes the saved route and then the saved guide;
ResumeGuidemarks the route as already owned so nothing starts twice. Route:InvalidateGatewaysalso drops the discovered-pin cache (/am resetrescans).- Localization:
route_gateway_locked_fmt,route_discover_hint_fmt,lock_access_label,gate_*in all 12 locales.
In-Game
- AppearanceMatic now speaks your language. Options, tooltips, alerts, the lockouts panel, guide messages, and the what's-new popup are fully translated into German, Spanish, Mexican Spanish, French, Italian, Korean, Brazilian Portuguese, Russian, and both Chinese scripts. New collected-item alerts also use your language's wording by default.
Changed
- Full localization pass: every user-facing string across Core, Alert, Acquisition, Favorites, Bookmark, Preview, GuideArrow, Route, the Lockouts UI, and the options panel moved into AceLocale keys (157 enUS keys, 12-locale parity); the changelog popup chrome now resolves through clp_* keys; AceDB alert-format defaults read the locale at runtime.
In-Game
- Fixed the custom font choices showing up on non-English game clients. The decorative fonts only contain Latin letters and render as empty boxes in other languages, so the custom fonts are now offered on English clients only; every other language keeps the game's own fonts.
Fixed
- Vendored LibMatic-FontPack-1.0 (MINOR 2) and LibMatic-CustomMedia-1.0 (MINOR 3) now refuse to register fonts on non-English clients, so the shared-lib path can no longer put Latin-only fonts into the font dropdowns.
Added
- Retrofitted the addon onto the shared LibMatic framework base (the same base CritMatic, GearMatic, RepMatic, and XPMatic use), bringing full options tabs, alert stacking, bounce, sound, social broadcast, changelog popup, and AceDB profiles.
- New Core.lua (AceAddon) and Alert.lua (stacked alert engine) replace the old hand-rolled Alerts.lua. Alerts now fire through LibMatic-AlertStack-1.0, LibMatic-CombatTextBounce-1.0, LibMatic-AlertShadow-1.0, LibMatic-SoundPack-1.0/SoundThrottle-1.0, and LibMatic-OverlayGuard-1.0 instead of a raw RaidWarning-style popup and PlaySound call.
- New Modules/AppearanceMaticOptions.lua builds a full LibMatic-Settings-1.0 panel: General (alerts toggle, login banner/update notify/chat notifications/debug, tooltip expansion toggle, Lockouts panel frame controls), Alerts (per-type message formats with preview, globals, animation, icon, font, bounce style, position), Sound (per-slot sound pickers, mute, channel/rate limit/once-per-login/in-combat, custom sound scan), Social (party/raid/guild/battleground/emote), Change Log, and Profiles.
- New Modules/ChangeLogPopUp.lua wired through LibMatic-ChangeLog-1.0 with a v1.1.0 entry.
- New
AppearanceMaticSettingsDBAceDB SavedVariable (profiles) holds all new settings, kept fully separate from the existingAppearanceMaticDBdata table (favorites, mountFavorites, preview, widget, lockouts, acquisition, alerts, diag) so no player data could be lost in the migration. - The legacy
AppearanceMaticDB.alerts.enabled/.soundvalues are migrated once into the new settings profile on first load (sound kit 888 maps to the "Level Up" LibSharedMedia sound, kit 0 maps to muting sound). The old table is left in place untouched. - Lockouts panel gained Show, Lock Position, Scale, Opacity, and Reset Position controls (
LockoutsUI.lua:Lockouts:IsLocked/SetLocked/SetScale/GetScale/SetAlpha/GetAlpha/ResetPosition), driven from the new options panel and persisted inAppearanceMaticSettingsDB.profile.lockoutsFrame. - Full Ace3 + LibSharedMedia-3.0 library stack vendored under
Libs/(matching CritMatic's set);Libs/LibMaticremains a symlink to the shared libmatic repo. embeds.xmlrewritten to load the full LibMatic module set (CombatTextBounce, IconPicker, CountUp, ChangeLog, Settings, AlertPositioner, AlertShadow, AlertStack, Startup, Broadcast, OverlayGuard, SoundPack, SoundThrottle, CustomMedia, VersionCheck, FontPack, FitText) plus the 12-locale Localization set.- Full localization:
Localization/enUS.luaplus enGB, deDE, frFR, esES, esMX, itIT, ruRU, koKR, ptBR, zhCN, zhTW, all translated with the same key set. tests/settings_migration_spec.lua: pure-Lua coverage of the legacy sound-kit-to-LibSharedMedia-name migration.
Changed
/am settings,/am options, and/am confignow open the new LibMatic-Settings options panel instead of the old Blizzard Settings panel. Added/am help.Favorites.lua's expansion-in-tooltip hook is now gated by the new "Show Expansion in Tooltip" option (generalSettings.expansionInTooltip, default on).AppearanceMatic.toc:embeds.xmlfirst, addedCore.lua,Alert.lua,Modules\AppearanceMaticOptions.lua,Modules\ChangeLogPopUp.lua; added## SavedVariables: AppearanceMaticDB, AppearanceMaticSettingsDBand## IconTexture; version bumped to 1.1.0.
Removed
Alerts.lua(hand-rolled Blizzard Settings panel + rawPlaySoundkit 888 alert) replaced byCore.lua/Alert.lua/Modules/AppearanceMaticOptions.lua.
1.0.0
- Initial release: favorites, mount favorites, preview panel, bookmark, tracking, raid lockout tracking, acquisition data, and alert notifications.
This mod has no additional files