Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
- run: lua${{ matrix.lua }} scripts/test.lua . reverse
- run: lua${{ matrix.lua }} scripts/smoke.lua . retail
- run: lua${{ matrix.lua }} scripts/smoke.lua . forever
- run: lua${{ matrix.lua }} scripts/smoke.lua . era
- run: lua${{ matrix.lua }} scripts/smoke.lua . tbc
- run: lua${{ matrix.lua }} scripts/smoke.lua . mists
- run: lua${{ matrix.lua }} scripts/smoke.lua . titan
- run: python3 ../library/scripts/vendor.py . --check
- run: python3 scripts/package.py --library-source ../library --output /tmp/NoPoizen.zip
- run: python3 -B -m unittest discover -s scripts -p 'test_*.py'
Expand Down
2 changes: 2 additions & 0 deletions AUDIT_2026-09-24.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# NoPoizen audit — September 24, 2026

Historical report: Forever monitoring and Classic adapters have since been implemented. See [CLIENT_COMPATIBILITY.md](CLIENT_COMPATIBILITY.md) for current behavior, evidence and remaining live checks.

Baseline: **1.0.7**, `71412d6`, clean `main` synchronized with `origin/main` before edits. Initial test build: **1.0.8-beta.2** on `fix/retail-forever-audit`. The work is now in PR #1, with **1.1.0-beta.1** prepared for coordinated release. Publication remains gated. Existing save files and original audio sources were not modified.

Installed clients rechecked: **Retail 12.1.0.69933**, **Forever beta 1.60.1.69977**. TOC adds interfaces **120100** and **16001**, retaining previous Retail metadata. This is permission to load the test build, not certification of live compatibility.
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# NoPoizen changelog

## 1.2.0 — 2026-09-24

Enable real Forever weapon-poison monitoring using verified client spell/enchant data. Add Era/Hardcore/SoD, TBC, Mists and Titan adapters, hand-aware warnings, charge/expiry polling, Classic load metadata and six-client offline coverage. Preserve Retail aura/talent behavior and shared diagnostics. Live validation remains pending.

Validation: 140 tests pass in both orders on Lua 5.1 and 5.2, with six client profiles, Lua parsing and exact private-library vendor checks. NoPoizen client smoke checks and package verification also pass. Live validation of the new adapters remains pending.

See [CLIENT_COMPATIBILITY.md](CLIENT_COMPATIBILITY.md) for source evidence, scope and validation limits.

## 1.1.1 — 2026-09-24

Keep overlapping debug consoles and their controls in one native stacking group through private libchev 1.1.3. Category menus stay with their owning console.
Expand Down
44 changes: 44 additions & 0 deletions CLIENT_COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# NoPoizen client compatibility

One source tree supports these current client families. Interface metadata permits loading; API capabilities still decide which adapter can run. PTR/beta builds in the same family use those adapters when their contracts match. This does not claim support for arbitrary historical/private-server clients.

| Client family | Source snapshot | Interface targets |
| --- | --- | --- |
| Retail | Installed 12.1.0.69933 UI export | 120100 (existing older Retail targets retained) |
| Forever | Installed 1.60.1.69977 UI export; 1.60.1.70009 spell data | 16001 |
| Era / Hardcore / Season of Discovery | [1.15.9.69722](https://github.com/Gethe/wow-ui-source/tree/33e177d9bf38d76d5c6c6e05d5da78db1899659a) | 11509 |
| Anniversary / Burning Crusade | [2.5.6.69795](https://github.com/Gethe/wow-ui-source/tree/1463c686270b6c64e2c5c228f447c4597c0f8ba6) | 20506 |
| Mists Classic | [5.5.4.69934](https://github.com/Gethe/wow-ui-source/tree/cde55d0033e89b246381385b2f063cd6c6047ef8) | 50504 |
| Titan Reforged (China) | [3.80.2.69874](https://github.com/Gethe/wow-ui-source/tree/84ef503f0d2617494db84cc9c7e7b530e976f6e7) | 38001, 38002 |

The Classic references are mirrored Blizzard UI/API source. Source inspection and offline fixtures establish expected contracts, not engine-level taint safety, rendering or gameplay validation. The user requested source-backed implementation without waiting to level a Forever rogue. Live poison application, expiry, charge exhaustion and audio checks remain pending; no live test result is inferred from these fixtures. No SavedVariables were changed externally.

## Monitoring behavior

- Retail retains learned lethal/nonlethal aura checks and Dragon-Tempered Blades requirements.
- Forever uses **all** accessible rows (including sparse or zero-based tables) from `C_Item.GetWeaponEnchantInfo(Enum.WeaponSlot)`, requiring one verified poison on each equipped melee weapon. It recognizes both poison training spells (`2842`, `1298494`), reads time in milliseconds, and checks charges only for poisons whose client data defines charges. Other coatings do not satisfy poison requirements. A valid poison can coexist with another coating.
- Era/Hardcore/SoD and TBC use per-weapon temporary enchants. The exact client catalog controls rank and charge handling. SoD-specific Sebacious, Numbing, Atrophic and Occult poisons are in the Era catalog only. TBC has its additional ranks and Anesthetic Poison, with no charge requirement.
- Titan uses its Wrath-derived weapon poison catalog, including ranks VIII/IX. Vendor poisons become usable at level 20; eligibility does not require the retired crafting skill. `ItemSparse` item `6947` explicitly requires level 20, rogue class mask 8, and no required skill.
- Mists uses its own lethal/nonlethal aura catalog, including Leeching and Paralytic Poison, without Retail's Dragon-Tempered Blades rules.

Empty hands, shields and holdables do not need poison. A missing item query, inaccessible enchant result or failed restriction check stays **unknown**, with no missing alert. Weapon reads pause during combat/restrictions; recovery establishes a quiet audio baseline. Enchant/equipment events refresh immediately, with a one-second poll covering expiry and charge changes. Disable removes polling; loading/logout prevent further observations.

HUD hand/category rows, settings, startup grace, transition audio, restriction policy and the private libchev debug console remain shared across clients. `/np edit` works independently of native Edit Mode; the native callback bridge is optional.

## Poison identity evidence

The generated `PoisonData.lua` comes from the actual client DB2 records, mirrored by Wago, rather than copying a Classic lookup into Forever:

- [Forever SpellItemEnchantment](https://wago.tools/db2/SpellItemEnchantment?build=1.60.1.70009), [SpellEffect](https://wago.tools/db2/SpellEffect?build=1.60.1.70009), [SpellName](https://wago.tools/db2/SpellName?build=1.60.1.70009).
- Matching tables for Era `1.15.9.69722`, TBC `2.5.6.69795`, and Titan `3.80.2.69874` are pinned in `scripts/poison-data-sources.json`, including SHA-256 digests and selected spell/enchant pairs.
- Forever's actual application records use effect **360** (for example spell `8679` -> enchant `323`); Era/TBC/Titan use effect **54**. Only explicitly named rogue poison families with matching enchant records enter the catalog. Generic poison, NPC toxin, rune engraving, sharpening stone, oil and shaman imbue records are excluded. This also prevents dormant SoD spell records in Forever's data from being mistaken for active Forever coatings.
- Enchantment `Charges` is positive for charged Forever/Era poisons, zero for Crippling and for TBC/Titan poisons. Presence, icon or enchant type alone never establishes poison identity.
- Forever `Spell`/`SpellEffect` records `2842` and `1298494` describe poison crafting and skill lines 40/2988. Mists `Spell`/`SpellEffect` records `2823`, `8679`, `3408`, `5761`, `108211`, `108215` explicitly describe lethal/nonlethal coatings implemented as player auras.

To reproduce the catalog, download `https://wago.tools/db2/<table>/csv?build=<build>` as `<build>-<table>.csv`, run `python3 scripts/build_poison_catalog.py /path/to/csv-cache`, then format `PoisonData.lua` with StyLua. Raw CSV files are research inputs, not runtime payloads. The generated Lua and provenance JSON are committed.

## Validation

Run `lua scripts/test.lua` and `lua scripts/test.lua . reverse`. Run `lua scripts/smoke.lua . <client>` for `retail`, `forever`, `era`, `tbc`, `mists`, `titan` under Lua 5.1 and 5.2. The smoke checks load the real TOC and exercise startup, warning/satisfied transitions, restriction recovery, polling, settings/editing, console isolation, loading, disable/re-enable and logout. Private in-game fixtures cover secrets, malformed results, wrong coatings, charge policy, hand swaps, rank catalogs and Mists talent differences.

Follow up with actual poison application on each relevant client when characters are available. In particular, confirm Forever exposes trained poison abilities through the spell-knowledge API and returns the documented weapon rows. Any unexpected runtime shape fails quietly and appears in `/np diagnostics`; it must not be treated as a successful live observation.
2 changes: 2 additions & 0 deletions CLIENT_RESEARCH_2026-09-24.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# NoPoizen client/API evidence — September 24, 2026

Historical report: Forever monitoring and Classic adapters have since been implemented. See [CLIENT_COMPATIBILITY.md](CLIENT_COMPATIBILITY.md) for current behavior, evidence and remaining live checks.

Re-read the installed `.build.info`: Retail is **12.1.0.69933** (`wow`), Forever beta is **1.60.1.69977** (`wow_classic_beta`). Interface targets are **120100** and **16001** respectively. Plater's inspected current TOC independently declares `Interface-Mainline: 120100, 120105` and `Interface-Camelot: 16001`; the uninstalled 12.1.5 target does not establish NoPoizen compatibility.

Both client exports were read directly, under:
Expand Down
23 changes: 9 additions & 14 deletions CURSEFORGE_DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,18 @@

NoPoizen is a focused quality-of-life addon for **Rogues** that helps you avoid fighting without proper poisons active.

On supported Retail clients, it watches your current poison state and warns when you are missing required poisons. Unreadable or restricted observations do not trigger missing-poison warnings.
On Retail and Mists it watches your poison auras; on Forever, Era/Hardcore/SoD, TBC and Titan Reforged it checks poison coverage on your equipped weapons. It warns when required poisons are missing. Unreadable or restricted observations do not trigger missing-poison warnings.

## What It Does

- Monitors **lethal** and **non-lethal** poison categories.
- Shows a center-screen reminder widget when your poison setup is incomplete.
- Supports talent-based poison limits:
- Default: up to `1 lethal + 1 non-lethal`, limited to learned poison categories
- With `Dragon-Tempered Blades`: `2 lethal + 2 non-lethal`
- Uses a two-row display:
- Row 1: Lethal Poisons
- Row 2: Non-Lethal Poisons
- Removes already-applied poisons from each row.
- Hides an entire row once that category is fully satisfied.
- Retail/Mists: monitors learned **lethal** and **non-lethal** poison categories; Retail also accounts for **Dragon-Tempered Blades**.
- Forever and weapon-based Classic clients: checks each equipped melee weapon and labels missing coverage **Main Hand** or **Off Hand**.
- Recognizes actual poison enchants; other weapon coatings do not count.
- Shows a movable reminder while an observable poison requirement is missing and hides satisfied rows.

## Indicator Position and Scale

Open Blizzard **Edit Mode** to see NoPoizen alongside your other HUD elements. Drag its preview or click it to open position and scale controls. `/np edit` also opens those controls directly.
Where available, open Blizzard **Edit Mode** to see NoPoizen alongside your other HUD elements. Drag its preview or click it to open position and scale controls. `/np edit` also opens those controls directly.

- Drag the indicator to place it where you want.
- Scale the indicator up/down in real time.
Expand Down Expand Up @@ -83,8 +77,9 @@ All settings are **per-character**, including:

## Compatibility Notes

- Retail WoW: updated for the installed 12.1 client; live validation of this beta is pending.
- Forever beta: experimental loading and diagnostics only. Poison/coating identifiers are not yet verified, so poison warnings remain disabled there.
- Targets Retail, Forever, Era/Hardcore/SoD, Anniversary/TBC, Mists Classic and Titan Reforged.
- Forever monitoring is implemented using client spell/enchant data, including multiple coatings and poison charges.
- Source and offline validation are complete; in-game validation of the new client adapters remains pending.
- Rogue-only behavior by design
- Indicator logic automatically reacts to spec/talent changes and aura updates

Expand Down
6 changes: 2 additions & 4 deletions ClientDiagnostics.lua
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,8 @@ local function BuildClientCapabilityLines(api)
lines[#lines + 1] = "Weapon samples: modern multi-enchant API unavailable"
return lines
end
-- Samples are observations only. No row is classified as poison by its type,
-- icon, presence, or charges; Forever poison IDs require client evidence.
lines[#lines + 1] =
"Forever poison spell/enchant mapping: unverified; weapon samples do not establish poison coverage"
-- Raw samples remain separate from the detector's verified enchant catalog.
lines[#lines + 1] = "Forever poison spell/enchant mapping: client data 1.60.1.70009; live validation pending"
local safeOK, safe, reason = pcall(api.CanSample)
if not safeOK or not api.CanAccessValue(safe) or safe ~= true then
lines[#lines + 1] = "Weapon samples: skipped ("
Expand Down
4 changes: 2 additions & 2 deletions ClientDiagnosticsTests.lua
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ NoPoizen:RegisterTest("client diagnostics use weapon enums and preserve empty sa
Contains(report, "interface=16001")
Contains(report, "MainHand: no enchant rows")
Contains(report, "OffHand: no enchant rows")
Contains(report, "mapping: unverified")
Contains(report, "mapping: client data 1.60.1.70009")
end)

NoPoizen:RegisterTest("client diagnostics skip samples under restrictions", function()
Expand Down Expand Up @@ -144,7 +144,7 @@ NoPoizen:RegisterTest("client diagnostics retain multiple coating rows without p
local report = Report(api)
Contains(report, "id=100 ms=1000 charges=0")
Contains(report, "id=200 ms=2000 charges=40")
Contains(report, "weapon samples do not establish poison coverage")
Contains(report, "live validation pending")
end)

NoPoizen:RegisterTest("client diagnostics bound oversized enchant observations", function()
Expand Down
20 changes: 20 additions & 0 deletions Core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ NoPoizen.runtimeEvents = {
"PLAYER_ALIVE",
"PLAYER_EQUIPMENT_CHANGED",
"UNIT_INVENTORY_CHANGED",
"WEAPON_ENCHANT_CHANGED",
"WEAPON_SLOT_CHANGED",
"ADDON_RESTRICTION_STATE_CHANGED",
}

Expand Down Expand Up @@ -571,6 +573,9 @@ function NoPoizen:Enable()
self.postLoadRefreshAt = 0
LibChev.Advance(self, "postLoadRefreshToken")
self:ResetAudioTransitionArming()
if self.StartWeaponPoisonPolling then
self:StartWeaponPoisonPolling()
end

if self.EnsurePoisonIndicatorWidget then
self:EnsurePoisonIndicatorWidget()
Expand All @@ -594,6 +599,9 @@ function NoPoizen:Disable()
self.db.enabled = false

self.isEnabled = false
if self.StopWeaponPoisonPolling then
self:StopWeaponPoisonPolling()
end
if self.UnregisterEditModeCallbacks then
self:UnregisterEditModeCallbacks()
end
Expand Down Expand Up @@ -807,6 +815,18 @@ function NoPoizen:UNIT_INVENTORY_CHANGED(_, unit)
end
end

function NoPoizen:WEAPON_ENCHANT_CHANGED()
if self.isEnabled then
self:RefreshPoisonState("WEAPON_ENCHANT_CHANGED")
end
end

function NoPoizen:WEAPON_SLOT_CHANGED()
if self.isEnabled then
self:RefreshPoisonState("WEAPON_SLOT_CHANGED")
end
end

function NoPoizen:ADDON_RESTRICTION_STATE_CHANGED()
-- Restriction queries report false during event dispatch: defer the observation.
if not self.isEnabled then
Expand Down
14 changes: 11 additions & 3 deletions Diagnostics.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ function NoPoizen:RecordPoisonObservation(reason, state)
self:SafeToString(state.activeCounts and state.activeCounts.nonLethal),
self:SafeToString(state.requiredCounts and state.requiredCounts.lethal),
self:SafeToString(state.requiredCounts and state.requiredCounts.nonLethal),
self:SafeToString(state.activeCounts and state.activeCounts.mainHand),
self:SafeToString(state.activeCounts and state.activeCounts.offHand),
self:SafeToString(state.requiredCounts and state.requiredCounts.mainHand),
self:SafeToString(state.requiredCounts and state.requiredCounts.offHand),
}, "/")
self.observationCount = (self.observationCount or 0) + 1
if signature ~= self.lastObservationSignature then
Expand All @@ -43,8 +47,10 @@ function NoPoizen:BuildDiagnosticReport()
Add("reason", state.reason)
Add("mode", state.detectionMode)
Add("observable", state.observable)
Add("knownPoisonCount", state.knownPoisonCount)
Add("dragonTemperedBlades", state.hasDragonTemperedBlades)
if not state.activeEnchantIDs then
Add("knownPoisonCount", state.knownPoisonCount)
Add("dragonTemperedBlades", state.hasDragonTemperedBlades)
end
local function SpellIDs(ids)
local parts = {}
for index, spellID in ipairs(ids or {}) do
Expand All @@ -57,7 +63,9 @@ function NoPoizen:BuildDiagnosticReport()
end
Add("knownSpellIDs", SpellIDs(state.knownSpellIDs))
Add("activeSpellIDs", SpellIDs(state.activeSpellIDs))
for _, category in ipairs({ "lethal", "nonLethal" }) do
Add("activeEnchantIDs", SpellIDs(state.activeEnchantIDs))
Add("trainingSpellIDs", SpellIDs(state.trainingSpellIDs))
for _, category in ipairs({ "lethal", "nonLethal", "mainHand", "offHand" }) do
Add(category .. "Required", state.requiredCounts and state.requiredCounts[category])
Add(category .. "Active", state.activeCounts and state.activeCounts[category])
end
Expand Down
30 changes: 28 additions & 2 deletions HUDWidget.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@ end
-- These wrappers belong to the addon. Tests use private fixtures, never client globals.
NoPoizen.HUDAPI = {
IsRestricted = function()
if not NoPoizen:CanAccessValue(C_RestrictedActions) then
return true
end
if type(InCombatLockdown) == "function" then
local combat = InCombatLockdown()
if not NoPoizen:CanAccessValue(combat) or (combat ~= false and combat ~= nil) then
return true
end
end
if C_RestrictedActions and Enum and Enum.AddOnRestrictionType then
if C_RestrictedActions ~= nil then
if
not NoPoizen:CanAccessTable(C_RestrictedActions)
or not NoPoizen:CanAccessTable(Enum)
or not NoPoizen:CanAccessTable(Enum.AddOnRestrictionType)
then
return true
end
if
type(C_RestrictedActions.GetAddOnRestrictionState) ~= "function"
and type(C_RestrictedActions.IsAddOnRestrictionActive) ~= "function"
then
return true
end
for _, name in ipairs({ "Combat", "Encounter", "ChallengeMode", "PvPMatch", "Map" }) do
local restrictionType = Enum.AddOnRestrictionType[name]
if not NoPoizen:CanAccessValue(restrictionType) then
Expand Down Expand Up @@ -149,9 +165,19 @@ end

local function GetDefaultEditModeRows(owner)
local rows = {}
if owner.GetPoisonClient and owner.weaponPoisonCatalogs and owner.weaponPoisonCatalogs[owner:GetPoisonClient()] then
return {
{ category = "mainHand", icons = { { icon = 132273 } } },
{ category = "offHand", icons = { { icon = 132273 } } },
}
end
local catalog = owner.poisonCatalog or {}
if owner.GetPoisonClient and owner:GetPoisonClient() == "mists" then
catalog = owner.mistsPoisonCatalog or catalog
end
for _, category in ipairs({ "lethal", "nonLethal" }) do
local row = { category = category, icons = {} }
for _, spell in ipairs((owner.poisonCatalog or {})[category] or {}) do
for _, spell in ipairs(catalog[category] or {}) do
-- A static preview does not need to read auras or spell APIs.
table.insert(row.icons, { icon = spell.icon or 134400 })
end
Expand Down
2 changes: 2 additions & 0 deletions LIBCHEV_INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Shared library integration

Historical report: Forever monitoring and Classic adapters have since been implemented. See [CLIENT_COMPATIBILITY.md](CLIENT_COMPATIBILITY.md) for current behavior, evidence and remaining live checks.

NoPoizen privately embeds **libchev 1.1.3**, pinned to immutable commit [`445c0cf5b3dd40d36461f6e0b9adc2feb8dfb16b`](https://github.com/AlexAllocated/libchev/commit/445c0cf5b3dd40d36461f6e0b9adc2feb8dfb16b). The vendored bytes and SHA-256 manifest live in `Libs/libchev/`; the loader binds `namespace.LibChev`. There is no global library registry or compatibility alias.

## Shared behavior and addon policy
Expand Down
Loading
Loading