promotional bannermobile promotional banner

Loothing

A modern loot council addon for WoW 12.0+ that enables guild masters and officers to manage, distribute, and track raid loot through collaborative council voting, powered by the Loolib framework.

File Details

Loothing v2.0.18

  • R
  • Apr 16, 2026
  • 803.61 KB
  • 26
  • 12.0.5+1
  • Retail

File Name

loothing_2.0.18.zip

Supported Versions

  • 12.0.5
  • 12.0.1

Fixed

  • Observer settings were silently overwritten by the Master Looter's config. When the ML broadcast their observer roster, non-ML receivers were persisting the ML's permissions, open-observation flag, and ML-as-observer flag into their own saved profile. After the raid, if you were later your own ML on a different toon, your observer settings were whatever the previous raid's ML had configured. The remote data is now held only in-memory for the duration of the session and never written to your saved variables; the authoritative MLDB session-settings apply-and-restore path still handles legitimate per-session application.
  • Replay-protection dedup now covers legacy v3 messages. The addon's replay-protection dedup (introduced with Protocol v4) keyed on a monotonic message ID that v3 senders don't include, which meant any v3-formatted message — including one a peer had crafted to bypass dedup — slipped past the dedup table entirely. v3 messages now dedup on a content hash (sender + Adler-32 of the encoded blob + length) within the same 120-second window that v4 uses.
  • Session hijack protection. A non-Master-Looter group member could previously spoof a SESSION_INIT addon message and force-end another player's active loot council session by sending one with a different session ID. Session-init is now restricted to the current Master Looter, or — only when the receiver doesn't yet know who the Master Looter is — a raid leader/assistant as a one-time bootstrap. Once the receiver knows the Master Looter, the bootstrap door closes.
  • Malformed addon messages can no longer slip through to session state. The combined SESSION_INIT message used to bypass the per-payload schema and authorization checks that direct broadcasts ran, and RESPONSE_BATCH didn't validate each item inside the batch. A buggy (or malicious) peer sending garbage inside either wrapper would propagate it straight into session state. All sub-payloads and each inner response are now validated individually; one bad item in a batch drops just that item instead of poisoning the rest.
  • Silent auto-pass against the wrong item, fixed. When Loothing's auto-pass had to wait for the game's item cache to warm up, the deferred retry could fire later against an item you'd already moved on from (or that had been removed from the session) and silently queue an auto-pass response for it. Switching to a new voting item now cancels every deferred retry, timer, and item-info hook from the prior item — including the OnItemInfoLoaded callback that used to linger until the cache warmed.
  • Heartbeat observer-sync spam, fixed. If the Master Looter had Loothing's observer system loaded with at least one observer and your client didn't have the observer module, your client was triggering an observer incremental sync once every 10 seconds that it couldn't actually apply. The observer hash compare now skips when the module isn't loaded locally.
  • Sync cooldown no longer suppresses a real mismatch after a false alarm. When a pending sync was cancelled because state converged before it fired, the 60-second cooldown watermark stayed set — a genuine state divergence arriving within that window was silently ignored. The cooldown is now reset when the cancellation was due to convergence.
  • Communication diagnostic stats (/lt diag stats) are now accurate. Sends that Loolib's queue dropped under heavy pressure were being counted as successful. Those are now counted as dropped with a reason attached. Test-mode traffic intercepts also match — a dropped send is no longer also recorded as outgoing.
  • Guild-channel drops under pressure now fire the same drop notification as group drops. Subscribers to the OnMessageDropped event saw group drops but not guild drops; parity restored.
  • PARTY-channel addon messages are now dropped client-side when you're in a raid instead of being handed to the game and bouncing back with GeneralError(9). Matches the same error-frame-spam protection the addon already has for raid/instance/guild/whisper channels.
  • Message sequence counter (used for replay protection) wraps strictly above 2,147,483,647 — previously the exact max value was skipped. Nothing user-visible; a cosmetic tightening.
  • Test-mode guard on history writes is now defence-in-depth. The check covered one of the two test-mode modules (ns.TestMode); a remote HISTORY_ENTRY arriving during test mode could theoretically slip through if the state tracker and harness were ever out of sync. Both sources are now consulted.
  • Council-table right-click menu is ordered, stable, and now shows custom responses. The "Award:" and "Change Response" submenus used pairs(ResponseInfo) to enumerate responses, so the Need / Greed / Offspec / Transmog / Pass items appeared in a different order every time you opened the menu. An earlier fix in this cycle iterated the static priority list instead, which silently hid any response the guild had customized (added a sixth response, renamed an ID, etc.). The menus now iterate your active button set via ResponseManager:GetSortedResponses() — every configured response is present, in the order you set. The menu also no longer relies on Blizzard's undocumented nop global.
  • Orphan auto-roll-on-submit locale strings cleaned. The auto-roll-on-submit toggle was removed in 2.0.8, but translations for it still shipped in ruRU, ptBR, esES, deDE, and the Brainrot locale. Now gone.
  • Cross-realm trade-in matching no longer fails on realm-name case drift. A player whose name arrived in chat as Jimbo-illidan (chat-server lowercasing) was being treated as a different player from Jimbo-Illidan (API casing) during tradability and time-remaining updates, so items would miss their owner in cross-realm raids. Name normalization now canonicalizes the realm segment to lowercase, and the four direct item.looter == comparisons in the session layer now route through Utils.IsSamePlayer. This tightens every cross-realm player comparison in the addon — council members, voting tally keys, comm senders, history lookups — not just the trade path.
  • Manually added items no longer escape the class-blacklist when the ML does the adding. The bag-scanner already rejected consumables, reagents, tradeskill items, quest items, and housing decor before adding to a session; the AddItem entry point shared by automatic paths was missing that same gate, so anything else flowing in without force set could still land in a voting session. The filter is now applied symmetrically. Explicit /lt add, sync restore, and reconnect restore still pass force=true and retain their intentional override.
  • History no longer keeps orphaned entries when the ML revotes a completed item — everywhere. Calling revote on an already-awarded item returned the item to voting state but left the original award row in history; a subsequent re-award wrote a second row with no link back. Revoting now cleans up the superseded history entry so the final award is the only one recorded. This works both locally on the ML and on every council member: the history layer is receiver-side idempotent on the item's session GUID, so when the re-award's history entry arrives over the wire, every receiver evicts the stale row before inserting the new one. No extra broadcast is required. Pre-existing history entries are unaffected — the back-reference only applies to awards made after this version is installed.
  • Majority-recommendation label now shows the ballot count as a fraction. The winner text reads Recommended: Jimbo (3/5 votes) instead of Recommended: Jimbo (3 votes) so it's clear that the majority denominator is "ballots submitted," not the total council size — non-voters are not counted. This matches what the voting engine has always done; we just now say it out loud. The voting-engine docstring and internal comments have been updated with the same clarification.
  • SESSION_INIT bootstrap tightened against assistant-impersonates-ML. The earlier session-hijack fix accepted SESSION_INIT from a raid leader or assistant when the receiver didn't yet know the Master Looter, and provisionally adopted that sender as ML pending the MLDB sub-payload's authoritative assignment. If the sub-payload was missing or malformed, the assistant stayed stuck as the receiver's ML until the next legitimate MLDB — long enough to forge council-roster changes, item adds, and votes as the supposed ML. Bootstrap now requires a structurally valid MLDB sub-payload up front; without it, SESSION_INIT is rejected and the receiver never promotes the sender.
  • Deferred auto-pass no longer fires against a torn-down frame. When an item's class/subclass info wasn't yet cached, the auto-pass check was scheduled behind an OnItemInfoLoaded callback + retry timer. If the frame was torn down (session ended, loot finished) between scheduling and firing, a nil self.items was silently treated as "item is still present" and auto-pass ran against stale state, queueing a phantom response. The nil case is now treated as "frame gone, bail out." The callback itself always unregisters on fire, so the item no longer leaks through the closure.

Removed (internal)

  • Deleted three unused comm-layer helpers: CommMixin:SendViaRelay (cross-realm relay wrapper — replaced by the XREALM envelope path, never called), CommMixin:BroadcastVoteUpdate (replaced by the batched QueueForBatch(VOTE_UPDATE, …) path), and SessionMixin:OnVoteTimeout (legacy per-session timeout — replaced by per-item timeouts in a prior refactor). All three were sitting as public-looking methods with zero callers.
  • Thirteen unused popup definitions and their orphan locale strings deleted across six locale files. LOOTHING_CONFIRM_USAGE, LOOTHING_CONFIRM_ABORT, LOOTHING_CONFIRM_AWARD_LATER, LOOTHING_TRADE_ADD_ITEM, LOOTHING_KEEP_ITEM, LOOTHING_CONFIRM_CLEAR_COUNCIL, LOOTHING_CONFIRM_SKIP, LOOTHING_CONFIRM_REVOTE, LOOTHING_CONFIRM_CLEAR_IGNORED, LOOTHING_CONFIRM_RESET_REASONS, LOOTHING_CONFIRM_DELETE_SET, LOOTHING_CONFIRM_REANNOUNCE, and LOOTHING_CONFIRM_PROFILE_OVERWRITE were registered but never invoked — either superseded by LOOTHING_CONFIRM_START_SESSION during the v2.0 UI overhaul, or declared for features that never shipped. Removing them trims the Popups surface and eliminates 132 dead translation lines.

Changed

  • TempTable leak tracking is now off by default for release builds. The per-Acquire stack capture the pool was doing was a measurable cost on hot paths (communication batching, event dispatch). The dev build (Loothing (Dev) TOC) re-enables leak tracking automatically, and the /lt diag panel now shows "(tracking off)" in the Health section when the tracking is off, instead of reporting a misleading 0 leaks.

Internal / Library (Loolib 2.1.2)

  • Callback registry hardened against mid-dispatch unregister. If a callback unregistered itself or any other callback while its event was still being dispatched, the pairs() iteration over the live callback table hit undefined behavior. The registry now marks unregistered entries with a tombstone sentinel instead of deleting them mid-iteration, and sweeps them after dispatch drains.
  • ThrottleWithTrailing and Debounce preserve the exact arguments of the last call. Previously, ThrottleWithTrailing called its wrapped function with no arguments on the trailing edge, and both helpers truncated varargs with trailing nils. The trailing fire now forwards the full last-call argument list.
  • CommMixin:Shutdown teardown hook added (test harnesses / debug reloads). A subsequent init rebuilds cleanly — the frame reference is nilled, queues/prefix tables/throttle state are reset, and the hooksecurefunc budget tracker is installed exactly once per Lua state to prevent stacking.
  • RESPONSE_BATCH item rejections and Loolib queue-full drops now both emit OnMessageDropped events with a reason code, suitable for rate-limit / alert subscribers.
  • Loolib.Locale:GetLocale no longer returns a bare {} when a locale lookup has no result. Both the "no locales registered for this application" and "no default locale found" paths now return a shared fallback table whose __index echoes the requested key back as a string. This prevents string.format(L["KEY"], …) from crashing on an unreachable edge case, and makes missing keys visually obvious during debugging. The fallback also rejects writes, so callers can't accidentally mutate shared state via L["FOO"] = ….
  • SendCommMessageInstant can no longer leave the bypass-tracking flag stuck on true. The instant-send path flipped an internal "this is our own send" flag before calling C_ChatInfo.SendAddonMessage and back off afterwards. If the C_ChatInfo call raised a Lua error (throttle lockdown, invalid state), the flag stuck, and the hooksecurefunc that audits bypass-traffic then ignored every subsequent bypass send for the rest of the session — silently poisoning the throttle-budget measurement in /lt diag stats. The call is now pcall-guarded; the flag always resets.

Desktop (Loothing Desktop 1.6.1)

Fixed

  • The app now recovers when configuration loading fails instead of silently landing you on the setup screen. Previously, a transient backend error while loading your saved WoW path would throw you back to the "Configure WoW" screen with no indication that anything had gone wrong; you'd assume your setup was lost. The app now shows a clear "Couldn't load configuration" card with the underlying error and a Retry button, so it's obvious when something is recoverable.
  • No more blank window when one page hits an unexpected error. Every major page (Dashboard, Roster, History, Wishlists, Changelog, Settings) is now wrapped in its own error boundary. If a page crashes because of unexpected data (e.g. a malformed wishlist payload from the server), you see a recovery card with "Try again" and "Back to dashboard" buttons — the rest of the app stays usable, and navigating tabs resets the crashed view.
  • "Try again" on an error now actually re-runs the page. The error-recovery button used to leave stale React state in place, so clicking it often just re-showed the same error. It now forces the view to fully remount, giving the code a clean slate.
  • Wishlists and roster no longer crash when the server sends an unexpected shape. If the wishlist API returned a byItemID entry as null or a string instead of the expected array, the Wishlists page and the Roster member-detail panel would throw and take the view down. Those sites now normalize defensively and keep rendering.
  • Roster tank/healer/DPS/flex counts now add up to total member count. Members whose role was missing or unknown were silently dropped from the role counts (making the sum lower than the member list), and they rendered with a broken role icon. Unknown roles now count as "Flex" and show the Flex icon.
  • Retrying a stuck boot no longer clobbers the fresh retry with stale data from the previous attempt. Clicking "Retry" while the previous load was still in flight could let the older response overwrite the newer one mid-retry. Each boot now gets its own generation id; any in-flight request whose id is stale silently drops its result. Same protection added to Dashboard stats/tier-data loads, so rapid filter/guild switches no longer race.
  • Retry buttons no longer fire duplicate requests when clicked rapidly. Retry, Skip, and warnings-banner retry buttons are now disabled while a boot is in progress.
  • Themed guild branding now clears correctly when you leave a guild. Switching from a themed guild to "no guild selected" used to keep the old theme applied; the reset now fires in the right place in the effect cycle.
  • Login errors are now human-readable. OAuth token exchange failures used to surface as raw Error: invalid_grant / [object Object] strings. Common patterns — expired tokens, timeouts, network errors — are now translated to friendly sentences. Pasted tokens are validated locally for basic format before being sent.
  • Sync status error messages no longer leak internal file paths and other backend-only detail to the Sync panel. Error strings are routed through a formatter that strips paths and truncates long messages.
  • Addon updates can no longer leave you with no installed addon if the install fails mid-extract. The updater used to delete the current Loothing/ folder before extracting the new one; a crash or disk error in between would leave you with no addon and a "manual reinstall" problem. The new version extracts into a staging directory and atomically swaps — the original is never touched until the new version is fully in place. If the swap itself fails, the old install is restored.
  • Addon zip extraction now rejects files that could compromise your system. Symlink entries (which would follow out of the addon directory), absolute paths, NUL bytes, leading-dot config files (.DS_Store, .git), and parent-directory traversal components are all refused with clear error messages.
  • SavedVariables writes are now atomic. The desktop app used to overwrite Loothing.lua in place; if it crashed mid-write, you could end up with a truncated file — and because the app re-serializes every top-level global in that file, a bad write could corrupt other addons' saved data too. The write now goes to a .tmp file first, is fsynced, then renamed into place. WoW never sees a partial file, other addons' data is safe.
  • NaN or infinity values in synced data no longer corrupt your SavedVariables. If a piece of synced data contained NaN or Infinity (e.g. from a divide-by-zero in a server stat), the old writer would emit NaN / inf — neither of which is valid Lua — and WoW would fail to load your settings entirely. Those values are now emitted as (0/0), (1/0), (-1/0), which parse and round-trip correctly.
  • Addon auto-update version detection now tolerates common TOC formatting variations (## Version: vs. ##Version:, different cases, extra spaces) so it no longer silently decides "no update available" because the addon author's build pipeline happened to format the version line slightly differently.
  • Pre-release version strings no longer falsely report "you're ahead" and suppress real updates. Versions like 1.2.3-beta used to silently compare as (1, 2, 0) — older than 1.2.2 — so the next update check would see "nothing newer" even when a legitimate newer version existed. The version parser now returns "unknown" on any unparseable component, and the update check treats that as "don't auto-prompt, don't falsely claim currency."
  • File watcher thread no longer leaks on reconfigure. Previously, each time you changed your WoW path or account in Setup, the prior SavedVariables file watcher leaked a thread that never exited. The app now signals the old watcher to stop before spawning a new one.
  • Authentication state no longer silently recovers from panics. The auth check previously tried to recover from a poisoned mutex (the internal signal that something panicked while holding the lock), which could return a stale "you're logged in" state after a crash. It now surfaces the condition so the UI can prompt for a restart instead of drifting.

Added

  • Safer WoW path handling. The path and account fields you type into Setup are now validated end-to-end — absolute path required, no .. traversal, no path separators or NUL bytes in account names, Windows reserved device names (CON, PRN, AUX, NUL, COM1-COM9, LPT1-LPT9) are refused. The app also canonicalizes the WoW path so symlinks inside WTF/Account/<name>/ can't escape the account directory.
  • SavedVariables parser now refuses oversized files (64MB cap) and deeply-nested tables (128-level cap). Real LoolibDB files are under 5MB with modest nesting; the caps prevent a corrupted or hand-crafted file from exhausting memory or crashing the app with a stack overflow during parse.
  • When "Couldn't load your guild list" happens at boot, you now see a warning banner with a retry link instead of the app just quietly moving on without guild data.
  • Multiple simultaneous warnings are now shown compactly in the banner rather than only showing the most recent one.

Changed

  • HTTP client initialization failures now crash the app immediately with a clear log line instead of silently falling back to a client with no timeouts (which would cause every network call to hang forever). If the TLS stack is unusable you learn about it at startup.
  • Event listener cleanup hardened across the app. The sync/auth/update hooks each register multiple Tauri event listeners; cleanup is now handled via Promise.allSettled so one listener failing doesn't leak the others, and crashes during unlisten are logged rather than swallowed.
  • Directory-scan errors during WoW auto-detect (on /mnt, /media, /run/media/$USER) now surface as debug log lines instead of being silently ignored, making installation-discovery issues diagnosable.

Internal

  • resolve_sv_path is now the single canonical entry point for resolving SavedVariables paths — all SV read and write sites route through it, closing a bypass where one read path had reimplemented the path-joining without the validation checks.
  • extract_existing_exchange_field no longer silently returns an empty object on read/parse failure. It now returns Result, and every caller (20+ sync paths) explicitly handles the failure case — if we can't read existing data we skip the SV write entirely rather than wiping the user's synced state with empty objects.
  • Addon downloads now share a unified HTTP client helper (http_client_with_timeout) instead of building bespoke reqwest::Client instances inline.
  • Lua writer hardening. Unicode line separators (U+2028, U+2029) and C0/C1 control characters are now escaped in the serialized output so the file stays parseable by both the desktop app and WoW's Lua runtime regardless of what Unicode crept into server-supplied strings.
  • Parser error paths are no longer silent. Trailing garbage after the last valid assignment now raises an explicit "failed to parse assignment near..." error instead of quietly returning partial data.
  • Mutex-poisoning recovery across the app is now consistent — poisoned auth state surfaces as an error with a log line instead of being silently recovered from, so panic-induced state drift is visible in support diagnostics.