Housing Decor Guide

All 1,690 housing decorations in one addon. Browse every acquirable item, plan vendor shopping routes with built-in waypoints, queue crafting recipes with material tracking, and monitor profit margins — all with account-wide alt awareness.

File Details

Housing Decor Guide v2.43.0

  • R
  • May 10, 2026
  • 1.99 MB
  • 10.0K
  • 12.0.5+2
  • Retail

File Name

HousingDecorGuide-2.43.0.zip

Supported Versions

  • 12.0.5
  • 12.0.1
  • 12.0.0

User-facing additions to the HouseEditor companion plus a heavy
internal-hygiene pass. Smoke-tested across all queue-add paths,
Acquisition + Recipes filters, post-/reload state restoration, and
the new HouseEditor strip + filter controls.

* HouseEditor companion: header search box (under the title) filters
  the grid by item name across whatever view is currently selected.
  Persists across mode switches; Esc clears; clear-X button to the
  right of the box clears on click.

* HouseEditor companion: indoor/outdoor filter button (cycles
  Indoor + Outdoor / Indoor only / Outdoor only) right of the
  placement-cost icon. Filters the visible grid by each item's
  catalog isAllowedIndoors / isAllowedOutdoors fields.

* HouseEditor companion: per-cell placement-cost badge in the
  bottom-left of every grid card, using the same budget-icon atlas
  as the Decor Preview detail panel. Toggle on/off via the icon to
  the right of the title; greyed out when off.

* HouseEditor companion: cards for items with 0 copies in storage
  (everything you own is already placed) are now greyed out + slightly
  desaturated, so "you own this but cannot place another" is visible
  at a glance.

* HouseEditor companion: "By Placement Cost" virtual collections
  appear at the bottom of the Collections sidebar -- Small (1-2),
  Medium (3-4), Large (5), X-Large (6+). Click any bucket to see all
  decor in that cost range. Header rows in Themes and Collections
  also picked up an underline for cleaner section divides.

* HouseEditor companion: "Recent placements" strip across the bottom
  shows your last 8 placed items. Newest enters from the right; older
  shifts left as new placements come in. Reuses the same card chrome
  as the main grid (tooltips, badges, dye markers all work). Refreshes
  live as placements happen in the editor.

* HouseEditor companion: window background is now opaque (chat /
  world no longer bleed through behind the chest atlas).

* Acquisition tab: collapsible Advanced Filters panel + preset
  buttons. Reduces visual noise on the left filter row and gives
  one-click access to common filter combos.

* Fix: crafting-queue items now persist their `profession` field through
  the Store on insert. Previously HDG_PowerCrafting.MakeQueueItem set
  it but Store:AddToCraftingQueue dropped it on the way to
  SavedVariables, so post-/reload `GetCharactersWhoKnowRecipe(spellID,
  profession)` calls passed nil and crafter-grouping in the queue
  picked from any profession that knew the spell instead of the
  recipe's actual profession. Row icons recovered via the itemID
  fallback so this was visible-only after a reload, but the grouping
  was less precise. Single-line fix at HDG_Store.lua:725.

* Refactor: crafting-queue mutations now have a single canonical
  refresh path instead of two. Previously every Add/Remove/Update/
  Clear dispatched through Store -> EventBus (which refreshed
  QueuePanel + MaterialsPanel) AND then explicitly called
  HDG_UI.RefreshUI() (which refreshed QueuePanel + MaterialsPanel
  again, plus RecipesTab). The materials aggregation is the
  expensive bit -- it walks the whole queue summing reagents -- so
  the duplication was meaningful per click. EventBus is now
  authoritative: it refreshes queue + materials + recipes (added
  recipe refresh so in-queue badges still update); the post-dispatch
  HDG_UI.RefreshUI() calls were dropped from
  HDG_RecipesTab:AddToCraftingQueue and the four queue-mutation
  paths in HDG_QueuePanel (remove, shift-remove, decrement, clear).
  Expand/collapse paths in QueuePanel still call RefreshUI directly
  -- they don't dispatch and so don't trip the EventBus.

* Refactor: HDG_AcquisitionTab:SelectItem split into a small
  orchestrator + three mode-specific helpers. Was a 75-line
  if/elseif/else with three branches that each repeated the same
  4-line detailPanel anchor reset, the same UpdateHeaderPanel call,
  and (for the crafted/non-vendor cases) the same vendor-state
  clear + waypoint/map hide + cell release. Now SelectItem owns
  the shared prelude (selectedItemID, headerPanel/detailPanel show,
  detailPanel anchor reset), data probe (vendors / hasVendors /
  isCrafted), single dispatch (vendor wins over crafted, matching
  original priority), and shared postlude (UpdateHeaderPanel,
  UpdateNoteDisplay, vendor-note widgets, cart buttons,
  UpdatePreviewModel, UpdateRowHighlights). The three new methods
  -- :SelectVendorItem, :SelectCraftedItem, :SelectNonVendorItem --
  each own only the work that actually differs by mode. ~30 lines
  of duplication gone; behaviour preserved.

* Refactor: same surgery on the Acquisition tab. HDG_AcqData.filters
  was a public mutable table on a "data" module that the Acquisition
  UI mutated directly (HDG_AcqData.filters.search = val,
  .zone = val, .source = val etc.) -- worse smell than the Recipes
  side because there weren't even setter wrappers; the UI just grabbed
  the table and wrote to it. HDG_AcqData.sortMode same shape.
  Both now live as module-locals (acqFilters, acqSortMode) on
  HDG_AcquisitionTab where they belong. HDG_AcqData's three filter
  consumers (RebuildFilteredItems, RebuildFilteredVendors,
  ItemPassesFilters) take filters as a parameter -- pure data-layer
  logic, no shared mutable globals. Signatures changed:
    RebuildFilteredItems(CONFIG)   -> RebuildFilteredItems(filters, sortMode)
    RebuildFilteredVendors(CONFIG) -> RebuildFilteredVendors(filters)
    ItemPassesFilters(itemID, CONFIG) -> ItemPassesFilters(itemID, filters)
  CONFIG was dead in all three after the itemFacts refactor (used to
  feed BuildSourceMatchers which is now data-driven). Behavior
  preserved -- filters still session-only, reset on /reload, same
  defaults. ~30 mutation sites in HDG_AcquisitionTab updated;
  external API surface (BuildCaches still takes CONFIG) unchanged.

* Refactor: pulled recipe filter state out of HDG_Data and wired
  consumers to read Store directly. The 14 thin filter accessors
  in HDG_Data (GetSearchFilter / IsUncollectedFilterEnabled /
  IsCraftableFilterEnabled / IsKnownFilterEnabled / Get/SetExpansion
  Filter / IsSearchReagentsEnabled / Get/SetProfessionFilter and
  their Set* siblings) were zero-abstraction passthroughs to
  HDG.Store:GetConfig / Dispatch -- they didn't validate, transform,
  log, or hide anything; they just relocated the call into a "data"
  module that has nothing to do with UI filter state. Side-effect:
  RecipeMatchesFilters used to read those globals plus a module-local
  `recipeSearchFilter` that lived in HDG_Data only because the Edit
  Box and the matcher happened to share the file. The matcher now
  takes a filterContext (search / searchReagents / uncollectedOnly /
  expansion / knownOnly); HDG_RecipesTab owns the search-box state as
  its own module-local + a BuildFilterContext() helper that pulls the
  rest from Store. HDG.Store gets two new methods --
  GetCharacterProfessionFilter() / SetCharacterProfessionFilter() --
  that hide the per-character key lookup so callers don't duplicate
  HDG_Data.GetCharacterKey() + nil-guard chains. Net: HDG_Data shrank
  by ~14 functions + the `recipeSearchFilter` local + two unused
  GetConfig/GetUIState helpers; data-layer logic is now pure (takes
  filter state as parameter); state ownership is unambiguous (Store
  for persisted toggles, RecipesTab for transient search text).
  Sets up future HDG_Data domain split with no cross-module filter
  entanglement to untangle.

* Perf: scoped style-cache invalidation. The EventBus handler at
  the bottom of HDG_StyleEngine.lua used to call
  InvalidateCache() (full nuke -- vocab, facetStore, reverseIndex,
  every per-style bucket) on every HDG_COLLECTION_UPSERTED /
  DELETED / DECOR_ADDED / DECOR_REMOVED / FIELD_CHANGED event with
  payload.type == "style". So adding a single decor to a single
  curated style burned ~237ms (60ms steps 1-6 + 177ms step 7) on
  the next StylesTab:Show even though steps 1-6 don't depend on
  style definitions at all and ~99% of the per-style buckets were
  untouched. Now: derive the styleId from payload.collID
  ("style:..." -> "...") and dispatch to the existing scoped
  helpers -- InvalidateCustomStyle for mutations (rebuilds just
  that style's bucket + clears its compatCache slice),
  RemoveCustomStyle for deletes. Steps 1-6 stay valid forever
  (until /reload), the other ~100 styles' buckets stay built,
  next Show is instant. Guarded with `if not cacheBuilt then
  return end` so events fired before the first StylesTab open
  are no-ops (BuildCache picks up fresh state on first show
  anyway via RebuildAllStyleDefs).

* Perf: per-refresh memoization for HDG_Data.IsItemCollected on
  the Acquisition rebuild path. Item collection state is volatile
  (changes on collect events) so it can't go in itemFacts, but
  within a single RefreshList cycle it's called multiple times
  per item: ItemPassesFilters (when uncollectedOnly is on),
  RebuildFilteredItems' filteredUncollectedCount walk,
  RebuildFilteredVendors' per-vendor counters (an item sold by
  N vendors used to do N live calls), and pushItem's
  uncollectedCount in RefreshVendorList. Cache is wiped at the
  top of each Rebuild* call so each refresh sees fresh state;
  downstream consumers in the same cycle hit warm cache.
  Tooltip path (HDG_AcqRows row 668) intentionally left on the
  live API -- hover-time should reflect current state.
  HDG_AcqData.IsCollectedCached / ResetCollectedCache are the
  helpers; cache is private to HDG_AcqData.

* Perf: ItemPassesFilters reduced to mostly indexed reads. New
  HDG_AcqData.itemFacts table populated by BuildItemFacts (called
  at the end of BuildCaches) precomputes per-item static flags
  that don't change during a session: hasVendor, isCrafted,
  hasSource, isDyeable, releasedOK, srcType/altSrcType, and the
  per-source-category booleans (isAchievement / isQuest / isDrop
  / isTreasure / isPromotional / isShop / isReputation /
  isEndeavor / isGoldOnly). The rewritten ItemPassesFilters reads
  flags from the cache for everything except the inherently
  dynamic checks (IsItemCollected, search match, vendor-side
  expansion/zone/faction match -- those depend on filter values
  so can't be pre-baked). The rewritten SOURCE_MATCHERS each
  collapse to `function(f) return f.isX end`. The biggest wins:
  Reputation / Endeavor / Gold Only source filters were doing a
  per-item npcID walk on every call; releasedOnly was doing a
  per-item zero-cost vendor walk on every call; both are now
  single boolean reads. Trade-off: BuildItemFacts adds a one-time
  pass at BuildCaches time. Names that haven't loaded by that
  point fall through the legacy "Unreleased / Loading..."
  heuristic at snapshot time, which means new releases between
  patches don't update releasedOK until /reload -- accepted
  because retail item-release cadence is patch-day, not daily.
  Measured before/after on retail with 1676 items in scope:
  vendor mode RebuildFilteredVendors dropped 30-50%
  (full-scope 2.86 -> 1.76ms; heavy-filter 1.5-2.0 -> 0.6-0.75ms).

* Perf: vendor-mode RefreshList no longer double-filters every
  item. Was running both RebuildFilteredItems (full universe-wide
  pass: ItemPassesFilters * allItems + a sort) AND
  RebuildFilteredVendors (which already runs ItemPassesFilters on
  every item-per-vendor and populates vendorFilteredItems) on
  every keystroke / filter toggle / search update in vendor mode.
  Vendor mode now skips the universe-wide pass and derives the
  status-bar / header item count from a post-loop distinct walk
  over vendorFilteredItems (set as HDG_AcqData.filteredItemCount).
  By Item mode is unchanged. Map All button now calls
  RebuildFilteredItems(CONFIG) on click to keep its waypoint
  iteration correct in vendor mode where filteredItems is now
  intentionally stale -- one-time cost on a rare click. Added
  Acq.Rebuild.FilteredVendors to HDG_Debug.Timed for before/after
  measurement of this change and an upcoming itemFacts precompute
  refactor.

* Refactor: code-smell sweep prompted by the 2.42.1 reagent-quantity bug.
  Renamed in-memory recipe objects' `id`/`spell` fields to `itemID`/`spellID`
  for readability (~40 sites). Renamed crafting-queue payload fields to
  match (`{id, spell}` -> `{itemID, spellID}`) with a one-shot SavedVars
  migration on load -- existing queued items are rewritten in place,
  no data loss. Stripped dead `or 1` and `or mat.total` fallbacks that
  were hiding bugs behind silent defaults. Consolidated six queue-item
  builder sites (Recipes, Mogul, Acquire, Preview, Vendor List, Styles)
  through a single `HDG_PowerCrafting.MakeQueueItem` helper. Mogul tests
  rewritten to match new field names; 28/28 still green.

* Refactor: HousingDecorGuide.lua slimmed from 1806 to ~370 lines (-79%).
  Per-domain Blizzard event hooks now live in their owning modules'
  `RegisterEvents()` on their own event frames instead of being routed
  through one giant OnEvent dispatcher in the main file:
    - shopping-list `BuyMerchantItem` decrement -> HDG_VendorShoppingList
    - mail/merchant/bank/trade UI suspend + lumber achievement + logout
      session save + PEW lumber init -> HDG_LumberTracker
    - tooltip "Housing Decor reagent" tag -> HDG_PowerCrafting
    - crafting queue auto-decrement on craft -> HDG_PowerCrafting
    - profession-cache invalidation + recipe-learn UI refresh + craft
      acquisition log -> HDG_Data
    - HOUSING_STORAGE_ENTRY_UPDATED repaint + QueueInventoryUpdate +
      QueueRecipeUpdate fan-outs -> HDG_UI
    - HOUSE_DECOR_ADDED_TO_CHEST log + housing-achievement display
      refresh -> HDG_DataTab
    - chattiness-gated talking-head craft quote + counter -> HDG_Vamoose
    - ZONE_CHANGED_NEW_AREA: main file debounces and broadcasts
      HDG_ZONE_CHANGED on the bus; LumberTracker, LumberHUD, and
      ZoneMode subscribe themselves
  Main file now only handles lifecycle/coordination events
  (ADDON_LOADED bootstrap, PEW login-restore, BAG_UPDATE inventory
  fan-out, PLAYER_LOGOUT Store flush) and orchestrator init calls.

* Refactor: slash command cleanup. /hdg is the user-facing slash
  exclusively; all 12 dev/diagnostic commands (collection, featuredtest,
  costtest, tsm, debuglumber, debugprof, exportreagents, testquote,
  scantags (was tags), order, plus /lumberhud test/show/testdist
  aliases) live exclusively on /hdgdebug now. Bodies fully extracted
  from main file into HDG_Debug.lua; no delegation. /hdg help banner
  added listing user commands. /hdgdebug exportreagents reuses the
  existing HDG_Debug copy-window infrastructure -- the bespoke 50-line
  popup frame and three associated globals (HDG_ExportReagents,
  HDG_ShowExportWindow, HDG_ReagentExportFrame) are gone.

* Refactor: privateDB.pendingCraftInfo (multi-craft tracking, used by
  RecipesTab when initiating a craft and by HDG_PowerCrafting's queue
  auto-decrement when crafts complete) initialised in HDG_PowerCrafting
  instead of the main file. Dead duplicate _runtime.pendingCraftInfo
  removed from the Store state defaults.

* Refactor: dead bridge functions HDG:RefreshUI, HDG:RefreshCraftingQueue,
  HDG:RefreshMaterials, HDG:RefreshLumberTracking, HDG:Toggle removed.
  Zero internal callers; aspirational "for legacy macros" comment.
  Macro authors should bind /hdg or /click an addon button instead.

* Refactor: HDG_RecipesTab.lua slimmed from 3876 to 1109 lines (-71%).
  Four self-contained chunks lifted out into their own modules; what
  remains is the recipe list (left column), the lifecycle methods
  (Init/Create/Refresh/Show/Hide) and the EventBus glue:
    - UI/Tabs/HDG_CraftDialog.lua (282 lines) -- the [Craft] button's
      confirmation modal (frame creation + Show/Execute). Internal
      caller updated to privateDB.HDG_CraftDialog.Show; no third-party
      hooks exist so the old HDG_RecipesTab.ShowCraftConfirmation /
      .ExecuteCraft surface was deleted, not aliased.
    - UI/Tabs/HDG_ProfessionSidebar.lua (380 lines) -- left rail of the
      Recipes tab: expansion-filter dropdown, "All Professions" button,
      per-profession buttons with collection + recipes-known badges +
      open-prof-window icon. Was HDG_RecipesTab:RefreshProfessionTabs
      and the local EnsureProfessionTabFrames; now exposes Init(frame)
      + Refresh(). HDG_UI.RefreshProfessionTabs delegates to the new
      module instead of RecipesTab.
    - UI/Tabs/HDG_QueuePanel.lua (917 lines) -- middle column of the
      Recipes tab: queue ScrollBox + char-grouping + expand/collapse
      subsidiary recipes + the embedded Lumber widget at the bottom of
      the panel. Was HDG_RecipesTab:CreateQueuePanel +
      :RefreshCraftingQueue + ~580 lines of element kinds, binders,
      DataProvider push helpers, plus several queue-only locals
      (OpenRecipeAndThen, IsPSLAvailable, GetQueueItemColor,
      GetProfessionForItem, expandedQueueItems,
      pendingTooltipRebuilds + GET_ITEM_INFO_RECEIVED listener,
      SUB_INDENT/SUB_HEIGHT). Exposes Init(frame), Create(), Refresh().
      HDG_UI.RefreshCraftingQueue delegates to the new module; the
      EventBus HDG_STATE_CHANGED handler that fans out queue refreshes
      now calls privateDB.HDG_QueuePanel.Refresh() directly.
    - UI/Tabs/HDG_MaterialsPanel.lua (1217 lines) -- right column of
      the Recipes tab: materials ScrollBox + Totals/By-Recipe sort
      toggle + Direct/Raw mode toggle + cost rollup + "Add All to
      Auctionator" CTA. Was HDG_RecipesTab:CreateMaterialsPanel +
      :RefreshMaterials + :AddAllMaterialsToAuctionator + 7 binders
      + 3 push helpers + the orphan UpdateCostDisplay local. Exposes
      Init(frame), Create(), Refresh(), AddAllToAuctionator().
      HDG_UI.RefreshMaterials and HDG_UI.AddAllMaterialsToAuctionator
      delegate to the new module. Stripping these helper sites also
      surfaced four genuinely-dead locals in HDG_RecipesTab
      (GetConfig, GetUIState, GetCraftingQueue, GetRecipeByItemID --
      now read only by the new sub-panels) which were removed.

* Refactor: helper de-duplication across the recipes-tab modules.
  CreatePanelHeader (the 22px textured strip + title FontString) was
  copy-pasted in three sub-panels after the split; lifted into
  HDG_UIHelpers.CreatePanelHeader and the three local copies removed
  (~90 lines of pure duplication gone). Three new read accessors on
  HDG.Store (:GetConfig, :GetUIState, :GetCraftingQueue) replace the
  hand-rolled "state and state.config and state.config[key]" walks
  that every UI module had been duplicating; combined with calling
  HDG.Store:Dispatch directly, the four recipes-tab modules dropped
  ~70 more lines of boilerplate Get/Set wrappers. CraftDialog: 282 ->
  269. QueuePanel: 917 -> 874. MaterialsPanel: 1217 -> 1166.
  RecipesTab: 1109 -> 1064. ConfigTab/DataTab/MogulTab finished in
  the same style: wrapper preambles dropped, callers use HDG.Store
  directly (-54 more lines, -2 unused-local lints).

* Refactor: new HDG_UIHelpers.CreateAtlasButton(parent, atlasBase,
  size, opts) factory for the "frame + Normal/Pushed/Disabled atlas"
  pattern that recurs ~30 times across the UI. Auto-suffixes the
  Pressed and Disabled variants from atlasBase by default; opts let
  callers override per-button. Retrofitted across the UI:
  DecorPreviewTab destroy/minus/plus buttons, VendorShoppingList
  cart-plus/cart-minus, TrainersTab + AchievementSection collapse
  arrows, ZoneMode gear + clear-X icons, AcquisitionTab vendor + item
  note pencils. Sites that don't fit (bind-time atlas swaps, custom
  suffix conventions like HouseTab's -default/-active, or icons whose
  Normal atlas is set later) left inline.

* Refactor: materials-panel toggles now use the canonical
  HDG_UIHelpers.CreateFilterButton / UpdateFilterButtonState helpers
  instead of inline backdrop+text-color juggling. Affects the Sort
  Mode (Totals / By Recipe) chip and the Direct / Raw mode chip-pair.
  Two side benefits: the active/inactive state colours now match
  every other filter chip in the addon (was using bespoke
  button_hover/panel + accent-border instead of the addon-wide
  button_active/button_inactive theme tokens), and the panel shed
  another ~56 lines (1166 -> 1110) of inline frame setup.

* Memory: feedback_no_defensive_or_cascades.md sharpened with
  Vamoose's directive "things set to 1 hide bugs" -- default to no
  `or N` defaults at internal boundaries; only keep them at genuine
  external boundaries (WoW API returns, optional user config).