LibArmorInsulation (2.7.5)
# LibArmorInsulation
**Version 2.7.5** ESO API 101049 101050 License: MIT
*by @Kreksar5 and Claude.ai*
> **AI-ASSISTED ADDON.** Large portions of this addon’s code (the costume/outfit
> scanning commands, the resolver priority logic, and several data-table
> structures) and its UESP-sourced costume/style insulation data were written
> with Claude.ai, and **have not yet been independently validated in-game**
> for correctness or performance. Material/insulation assignments carry an
> explicit “ tag in `LibArmorInsulation_StyleData.lua`
> where applicable `low`-confidence entries in particular are thematic
> inferences, not verified facts, and are good candidates for manual
> re-checking against UESP or in-game testing before being trusted as accurate.
> This notice will be removed once the addon has seen real play-testing.
A reusable ESO addon library that calculates a **staggered-tier thermal insulation score (-10 to 90, in steps of 10)** for the player based on their current visual appearance taking into account armor style, material composition, body-slot coverage, and flavor-text bonuses or penalties from the style database. The score mirrors ESO’s own visual-priority rules: polymorphs beat costumes, costumes beat outfits, outfits beat worn armor.
—
## Table of Contents
1. (#installation)
2. (#dependencies)
3. (#insulation-model)
4. (#visual-priority-cascade)
5. (#public-api)
6. (#slash-commands)
7. (#settings-panel)
8. (#adding–editing-style-data)
9. (#eso-api-functions-used)
10. (#design-notes–limitations)
11. (#changelog)
—
## Installation
1. Drop the `LibArmorInsulation` folder into `Elder Scrolls OnlineliveAddOns`.
2. Ensure **LibAddonMenu-2.0 (v33+)** is also installed ((https://www.esoui.com/)).
3. Enable the addon in the ESO Addon Manager.
—
## Dependencies
| Library | Required | Notes |
|—|—|—|
| LibAddonMenu-2.0 ≥ 33 | **Yes** | Settings panel only. The calculation API works without LAM, but the settings panel will be unavailable. |
| LibStyleInfo | No | Not used. Its confirmed public API exposes style IDtoname lookups and light/medium/heavy weight flags, but not material composition, body-coverage percentage, or flavor text. |
| LibMotifCategories | No | Not used. Motif category data (crafted/overland/dungeon/etc.) does not map to thermal properties. |
> **Why maintain an internal style database?**
> Both evaluated libraries were ruled out because neither provides the material or coverage data needed for insulation math. LibArmorInsulation therefore ships its own hand-authored table keyed on `ITEM_STYLE_*` integer constants, which every ESO item carries natively.
—
## Insulation Model
### Scale
All values resolve to one of **eleven fixed tiers**, staggered in increments of 10 from **-10 to 90**:
| Tier | Label | Meaning |
|—|—|—|
| -10 | Magically Cooled | Magically cooled equipment (e.g. refrigeration) |
| 0 | No Insulation | No insulation |
| 10 | Minimal | Underwear/swimwear |
| 20 | Light | Light silks, breathable thin fabrics, or minimal coverage |
| 30 | Coarse Light | Linens, coarser fabrics, or small amounts of leather with light coverage |
| 40 | Layered/Leather | Thick or many-layered fabrics, or majority-coverage leathers |
| 50 | Full Leather/Light Metal | Full coverage leathers, mild coverage metals, or full-coverage multi-layered thick fabrics |
| 60 | Heavy Leather/Fur | Full coverage heavy/thick leathers, mild coverage furs, or moderate coverage metals |
| 70 | Full Fur/Metal | Full coverage furs or full coverage metals |
| 80 | Heavy Fur/Metal | Full coverage heavy furs and/or metals |
| 90 | Magically Heated | Magically heated equipment |
`-10` and `90` are reserved for entries explicitly flagged `magical = true` in the data tables (Flame Atronach, Ice Wraith, etc.). Every other entry is clamped to the mundane **080** range before being snapped to the nearest tier, so ordinary armor and clothing can never accidentally read as magically hot or cold.
> **Why a tier ladder instead of a continuous score?** It’s a much better fit for anything that has to *display* insulation a UI can show one of 11 discrete states/icons instead of trying to render 101 shades of meaning. It’s also easier to reason about and balance by hand: every entry in the data tables is describable in one of eleven plain-English buckets rather than an arbitrary integer.
### How an entry becomes a tier
Armor styles and outfit style pieces keep their existing `baseMaterial` + `coverage` + `flavorBonus` authoring fields nothing in the data tables needed to be hand-rewritten for the tier system. The Calculator computes the same full-body reference score it always has, then **snaps** that score onto the nearest tier:
“`
raw = 0.90 styleCoverage materialCoeff SCALE_FACTOR + flavorBonus
tier = SnapToTier(raw, magical) — nearest of {-10,0,10,…,80,90}
— clamped to first unless the entry has `magical = true`
“`
`SCALE_FACTOR = 55.556` anchors a full set of standard leather armor at coverage 1.0 to exactly tier 50.
Costumes and polymorphs keep their existing hand-authored `totalInsulation` field, which is likewise snapped to the nearest tier at lookup time (again clamped to 080 unless `magical = true`).
### Per-slot percentage adjustment (unless it’s a full costume)
A style/outfit piece’s tier is its **full-body reference value**. Individual armor/outfit slots then contribute a **percentage** of that tier based on how much of the body they cover:
“`
slot_contribution = round(tier slotPercentage)
total = sum(slot_contribution for every worn slot)
“`
Slot percentages now sum to exactly **1.00** across the 7 thermally-relevant slots (previously 0.90, compensated by `SCALE_FACTOR`), so a full matching set (every slot the same tier) reproduces that tier as the total. Missing slots count as bare skin (tier 0) for that percentage of the body small independent-rounding artifacts of a point or two are possible when summing per-slot contributions, but the total will never drift outside the -10..90 tier range.
**Costumes and polymorphs are NOT slot-adjusted.** They represent the whole body at once, so their snapped tier *is* the total, unadjusted there’s no percentage to apply.
### Slot coverage weights
| Slot | Percentage |
|—|—|
| Chest | 33% |
| Legs | 28% |
| Head | 11% |
| Shoulders | 11% |
| Hands | 6% |
| Feet | 6% |
| Waist | 5% |
| Rings, Neck, Weapons | 0% |
### Material coefficients
| Material | Coefficient | Full-set reference (pre-snap) | Snapped tier |
|—|—|—|—|
| fur | 1.8 | 90 | 80 (mundane cap) |
| wool | 1.5 | 75 | 70 (tie rounds down) |
| heavy_leather | 1.3 | 65 | 60 (tie rounds down) |
| leather | 1.0 | 50 (baseline) | 50 |
| scale | 0.8 | 40 | 40 |
| runic | 0.8 | 40 | 40 |
| bone | 0.7 | 35 | 30 (tie rounds down) |
| cloth | 0.7 | 35 | 30 (tie rounds down) |
| chitin | 0.6 | 30 | 30 |
| hide | 0.6 | 30 | 30 |
| iron | 0.5 | 25 | 20 (tie rounds down) |
| steel | 0.5 | 25 | 20 (tie rounds down) |
| silk | 0.5 | 25 | 20 (tie rounds down) |
| linen | 0.4 | 20 | 20 |
| aetherial | 0.4 | 20 | 20 |
| daedric | 0.3 | 15 | 10 (tie rounds down) |
| crystal | 0.2 | 10 | 10 |
| none | 0.0 | 0 | 0 |
Ties (raw score exactly halfway between two tiers, e.g. 75 between 70 and 80) round **down** to the lower tier `Calc.SnapToTier()` picks the first tier with the smallest difference while scanning the ladder ascending, so the lower of two equidistant tiers wins.
“Full-set reference” assumes coverage 1.0 and no flavor bonus; per-style `coverage`/`flavorBonus` modifiers shift the raw score up or down before it’s snapped, so an individual style’s tier can land a step away from its base material’s own tier (e.g. Nord’s fur base plus its +8 flavor bonus and 1.1 coverage pushes it to the mundane ceiling of tier 80).
### Costume & polymorph scoring
Costumes and polymorphs bypass per-slot math entirely. They return a single snapped tier looked up from `CostumeInsulationById` (by collectible ID), then `CostumeInsulation` (by normalized lowercase name), then the appropriate default:
| Fallback | Raw default | Snapped tier |
|—|—|—|
| `DEFAULT_COSTUME` | 50 | 50 |
| `DEFAULT_POLYMORPH` | 38 | 40 |
**Costume lookup priority.** Order is: user override (already a tier used as-is) → `CostumeInsulationById` (by collectible ID) → `CostumeInsulation` (by name, manually curated see Changelog 2.5.0) → `sv.costumeCache` (populated by `/scancostumes`) → `DEFAULT_COSTUME`. Run `/scancostumes` to populate the cache with keyword-derived auto-ratings for anything not already in the manual tables.
—
## Visual Priority Cascade
`GetTotalInsulation()` and `GetInsulationBreakdown()` mirror ESO’s own visual priority rules, evaluated top-to-bottom:
1. **Polymorph** detected via `GetActiveCollectibleByType(COLLECTIBLE_CATEGORY_TYPE_COSTUME)` combined with keyword/ID matching against polymorph entries. Returns a flat body-wide score; slot math is skipped.
2. **Costume** same detection mechanism. Returns a flat body-wide score; slot math is skipped.
3. **Active Outfit** detected via `GetEquippedOutfitIndex()`. Slot insulation is calculated from the collectible ID on each `OUTFIT_SLOT_*` using `ZO_OUTFIT_MANAGER`. Only slots with a non-zero style override contribute; if *no* outfit slots are overridden, falls through to armor.
4. **Worn Armor** calls `GetItemLink(BAG_WORN, equipSlot)` then `GetItemLinkItemStyle(itemLink)` for each thermally relevant `EQUIP_SLOT_*`. `GetItemLinkItemStyle` is used (not `GetItemStyle`) because it returns the correct visual style for all item origins crafted, dropped, set, and Crown Store items alike.
5. **Naked** returns 0.
—
## Public API
“`lua
— Total insulation score, one of the 11 tiers for costume/polymorph;
— a composite (-10..90) for armor/outfit made of per-slot tier contributions
local total = LibArmorInsulation.GetTotalInsulation()
— Full breakdown table
local info = LibArmorInsulation.GetInsulationBreakdown()
— info.source : “polymorph” | “costume” | “outfit” | “armor” | “naked”
— info.total : integer, -10..90
— info.slots : table {
— = {
— styleId, — ITEM_STYLE_* (armor path) or nil
— collectibleId, — collectible ID (outfit/costume path) or nil
— insulation, — integer slot contribution (== tier for costume/polymorph)
— tier, — the style/costume’s own full-body tier (-10..90)
— slotPercentage, — 0-1 weight of this slot; nil for costume/polymorph
— material, — string material key
— flavorNote, — string human-readable note, or “”
— armorFallback, — boolean: true if outfit slot fell back to worn armor
— }
— }
— info.costumeId : number | nil (collectible ID of active costume/polymorph)
— info.costumeName : string | nil (lower-cased collectible name)
— Full-set reference tier for a given ITEM_STYLE_* id (respects overrides)
— Returns one of {-10,0,10,20,30,40,50,60,70,80,90}
local tier = LibArmorInsulation.GetInsulationForStyle(styleId)
— Set a persisted insulation override one of the 11 tier values
— idType : “style” → ITEM_STYLE_* integer (worn armor)
— “outfit” → collectible ID (outfit slot piece)
— “costume” → collectible ID (costume or polymorph)
— value = nil removes the override. Non-tier numbers are snapped to the
— nearest tier rather than rejected. In the Settings panel this is always a
— dropdown, so it can’t drift off-tier.
LibArmorInsulation.SetOverride(idType, id, value)
— Remove all user overrides (does not clear the costume cache)
LibArmorInsulation.ResetOverrides()
“`
### Constants
“`lua
LibArmorInsulation.VERSION — “2.7.0”
LibArmorInsulation.ADDON_NAME — “LibArmorInsulation”
“`
### Saved variables schema (account-wide)
“`lua
sv.overrides — { = tier integer (-10..90) }
sv.costumeCache — { = { name, insulation, autoRated } }
— Populated by /scancostumes; persists across sessions.
— `insulation` here is a pre-snap raw value, snapped to the
— nearest tier at lookup time.
“`
Override keys follow the pattern `_` (e.g. `style_42`, `outfit_1234567`, `costume_9876543`). This unified key space means a single `SetOverride` call handles all three entity types without ambiguity.
—
## Slash Commands
### General
| Command | Description |
|—|—|
| `/insulation` | Prints the current source, total score with its nearest tier label, and per-slot breakdown style/collectible ID, insulation contribution, tier, slot percentage, material to chat. |
| `/styleids` | Prints the `ITEM_STYLE_*` ID worn in each armor slot. Use this to find the ID to enter in the override editor when working with worn armor. |
### Outfit commands
| Command | Description |
|—|—|
| `/outfitids` | Prints the collectible ID and name applied to each slot of every styled outfit. Use this to find the collectible ID to enter as an `outfit_` override. |
| `/outfitactive` | Diagnostic probes free functions and `outfitManipulator` methods to determine whether each outfit is actively equipped versus merely stored. Run with an outfit equipped, then unequipped, and compare output. |
| `/scanoutfitstyles` | Walks every equipped slot across all unlocked outfits and, for any collectible ID not already in the hand-curated `OutfitStyles` table, keyword-rates it (by name, then description) and saves a guessed `{baseMaterial, coverage, flavorBonus}` to `sv.outfitStyleCache`. Run this after unlocking/equipping a new style page so it gets a value other than `OutfitStyles`. |
### Costume & polymorph commands
| Command | Description |
|—|—|
| `/costumeids` | Prints the collectible ID, display name, and current lookup method (user override / ID table / name table / DEFAULT_COSTUME) for the active costume or polymorph. Also prints the override key (`costume_`) to use in the settings panel. |
| `/scancostumes` | Walks every owned costume and polymorph in the Collections system and assigns a keyword-derived auto-rating to each (checking the name first, then the in-game description if the name has no match), saving the results in the costume cache (`sv.costumeCache`). Run this once and again after acquiring new costumes to ensure all owned costumes receive a meaningful score rather than the default. |
| `/clearcostumecache` | Removes all entries from `sv.costumeCache`. Re-run `/scancostumes` afterward to repopulate. |
### Review commands
| Command | Description |
|—|—|
| `/costumesneedreview` | Filters `sv.costumeCache` down to entries where the keyword scan found no match in either the name or description (sitting on the generic 50 default). Use this as a worklist for manual UESP-backed `CostumeInsulationById` entries instead of scrolling the full `/scancostumes` output. |
| `/outfitstylesneedreview` | Same as `/costumesneedreview`, but for `sv.outfitStyleCache` entries with no keyword match. Use as a worklist for manual `OutfitStyles` entries. |
### Diagnostic commands
| Command | Description |
|—|—|
| `/diagcollectibles` | Three-phase diagnostic that enumerates every collectible category and subcategory, identifies which one ESO marks as `COLLECTIBLE_CATEGORY_TYPE_COSTUME`, and reports the first reachable collectible per category. Run this and share the output if `/scancostumes` finds 0 costumes. |
—
## Settings Panel
Accessible via **Addon Settings** in the ESO main menu.
| Section | What it does |
|—|—|
| **Current Insulation** | Live breakdown of every slot’s contribution, tier, slot percentage, the source (outfit / armor / costume / polymorph), material, and any flavor note. Press **Refresh** to recalculate. |
| **Manual Overrides** | Pick a **Layer** (Costume/Polymorph, Outfit, or Armor) and, for Outfit/Armor, a **Slot** each selection does a fresh live lookup of that specific layer/slot (independent of what’s actually visually showing) and auto-fills Override Type/ID/Tier with it, or with a placeholder if nothing’s active there. Use **Refresh Preview** if you changed gear without touching the Layer/Slot dropdowns. Adjust the Tier if you like, then press **Apply Override**. Choosing “(no override)” removes an existing override. |
| **Active Overrides** | Lists all currently active overrides with their keys, tier values, and tier labels. |
| **Reset** | **Reset All Overrides** removes every override and reverts to database defaults. The costume cache is not affected. |
> **Tip:** Use `/styleids` to find worn armor style IDs, `/outfitids` for outfit collectible IDs, and `/costumeids` for the active costume’s collectible ID before entering values in the override editor.
—
## Adding / Editing Style Data
All style data lives in `LibArmorInsulation_StyleData.lua`.
### Armor styles (`LibArmorInsulationData.Styles`)
Keys are `ITEM_STYLE_*` integer constants. Full list:
“`lua
= {
baseMaterial = “leather”, — string key from MaterialCoefficient table
coverage = 1.0, — style-level coverage modifier
flavorBonus = 0, — flat integer bonus/penalty (can be negative)
flavorNote = “”, — human-readable reason shown in settings & /insulation
},
“`
Any style not in the table falls back to “ (leather, coverage 1.0, no bonus → score 50).
### Outfit styles (`LibArmorInsulationData.OutfitStyles`)
Keys are **collectible IDs** (Collections system integers), not `ITEM_STYLE_*` values. The two number spaces are unrelated. Use `/outfitids` to find collectible IDs.
“`lua
= {
baseMaterial = “leather”,
coverage = 1.0,
flavorBonus = 0,
flavorNote = “”,
},
“`
Falls back to “ (leather, coverage 0.9).
### Costume & polymorph entries (`LibArmorInsulationData.CostumeInsulationById` and `CostumeInsulation`)
`CostumeInsulationById` is keyed by collectible ID (preferred); `CostumeInsulation` is keyed by lowercase display name (legacy fallback). Both use flat `totalInsulation` values.
“`lua
= { totalInsulation = 72, flavorNote = “Heavy bear-fur costume” },
— or by name:
= { totalInsulation = 30, flavorNote = “Thin ceremonial cloth” },
“`
Special keys “ (50) and “ (38) are the last-resort fallbacks.
—
## ESO API Functions Used
| Function | Source | Purpose |
|—|—|—|
| `GetActiveCollectibleByType(type)` | ESO | Detect active costume / polymorph |
| `GetCollectibleName(id)` | ESO | Resolve collectible display name for data lookup |
| `GetCollectibleId(catIndex, subIndex, index)` | ESO | Iterate collectibles for costume scanning |
| `GetCollectibleInfo(id)` | ESO | Read unlock state during `/scancostumes` |
| `GetNumCollectibleCategories()` | ESO | Enumerate categories in `/diagcollectibles` |
| `GetCollectibleCategoryInfo(catIndex)` | ESO | Get category name in `/diagcollectibles` |
| `GetEquippedOutfitIndex()` | ESO | Detect whether an outfit is actively worn (confirmed in-game) |
| `GetNumUnlockedOutfits()` | ESO | Count unlocked outfits (takes **no** arguments) |
| `GetOutfitName(outfitIndex)` | ESO | Outfit display name for `/outfitids` |
| `ZO_OUTFIT_MANAGER:GetOutfitManipulator(actorCategory, outfitIndex)` | ESO | Access outfit slot data (actorCategory is required) |
| `outfitManipulator:GetSlotManipulator(OUTFIT_SLOT_*)` | ESO | Access individual outfit slots |
| `slotManipulator:GetCurrentCollectibleId()` | ESO | Collectible ID on a given outfit slot |
| `GetItemLink(bagId, slotIndex, linkStyle)` | ESO | Item link string for a worn equipment slot |
| `GetItemLinkItemStyle(itemLink)` | ESO | Visual style ID for any item regardless of origin |
| `BAG_WORN` | ESO | Constant the equipped item bag |
| `EQUIP_SLOT_*` | ESO | Equipment slot constants |
| `OUTFIT_SLOT_*` | ESO | Outfit slot constants |
| `COLLECTIBLE_CATEGORY_TYPE_COSTUME` | ESO | Collectible category constant |
| `ZO_SavedVars:NewAccountWide(…)` | ESO | Account-wide saved variables |
| `EVENT_MANAGER:RegisterForEvent(…)` | ESO | Addon load event |
| `CHAT_SYSTEM:AddMessage(…)` | ESO | Chat output |
| `SLASH_COMMANDS` | ESO | Slash command registration |
| `LibAddonMenu2:RegisterAddonPanel(…)` | LAM | Settings panel registration |
| `LibAddonMenu2:RegisterOptionControls(…)` | LAM | Settings controls |
> **`GetItemLinkItemStyle` vs `GetItemStyle`:** `GetItemStyle()` only returns non-zero for player-crafted items; every dropped, set, or Crown Store piece returns `ITEM_STYLE_NONE` (0). `GetItemLinkItemStyle(itemLink)` returns the correct visual style for all item origins and is the correct choice here.
>
> **`GetNumUnlockedOutfits()`:** Takes no arguments. A `GameplayActorCategory` parameter does not exist on this function. `GetNumOutfits()` is not a real ESO API function and will crash with “function expected instead of nil”.
>
> **`GetEquippedOutfitIndex()`:** The correct gate for the outfit path. Confirmed working in-game; returns the 1-based index of the worn outfit, or nil if none.
—
## Design Notes & Limitations
**No per-item flavor text scraping.** ESO does not provide a Lua API to read raw item flavor text at runtime (the tooltip system renders it as formatted strings, not structured data). The `flavorBonus` values in the style database are therefore authored by hand based on in-game style descriptions and motif book lore.
**Item set names are ignored.** The library uses only the base `ITEM_STYLE_*` of each piece, not the set it belongs to. Set bonuses are mechanical gameplay systems unrelated to visual insulation.
**Outfit “no override” slots fall through.** If a player has an outfit active but has not overridden every slot, only overridden slots contribute from the outfit source. If *no* outfit slots are overridden, the library falls through to worn armor styles entirely.
**Costume vs. outfit visual conflict.** If a player has both a costume collectible active and an outfit applied, the costume wins visually in-game (costumes always take priority). LibArmorInsulation mirrors this by detecting costumes before outfits.
**Polymorphs override costumes.** A polymorph replaces the entire character model. LibArmorInsulation checks for polymorphs before costumes, consistent with ESO’s in-game visual priority.
**Outfit collectible IDs ≠ ITEM_STYLE_* IDs.** Outfit slots yield Collections-system collectible IDs, which are an entirely separate number space from `ITEM_STYLE_*` integers. The library maintains separate lookup tables (`OutfitStyles` and `Styles`) for this reason, and the override key prefix (`outfit_` vs `style_`) distinguishes them in saved variables.
**Costume cache is opt-in.** The `sv.costumeCache` table is only populated when you run `/scancostumes`. Until then, unrecognized costumes fall back to `DEFAULT_COSTUME` (50). Cache entries persist across sessions.
**Saved variables version 4.** The upgrade path from v3 preserves existing overrides and `costumeCache`, and adds the missing `outfitStyleCache` table. The upgrade path from v2 preserves overrides and adds both cache tables. Saves from v1 are reset to defaults to avoid schema conflicts.
—
## Changelog
### 2.7.5
– **Fixed the settings panel’s author/version fields**, which were still
the literal placeholder text `”YourName”` and a hardcoded `”1.0.0″`
never actually updated even after the `## Author` manifest field itself
was corrected. The panel now reads `author = “@Kreksar5 and Claude.ai”`
and `version = LibArmorInsulation.VERSION`, and the library’s internal
`ADDON_VERSION` constant (previously stale at `”2.7.0″`) is kept in sync
with the manifest `## Version` going forward.
### 2.7.4
– **Promoted 4 costumes and 11 outfit styles from verified player overrides**
into the hard-coded data tables (`CostumeInsulationById` and
`OutfitStyles`), so these no longer rely on a per-account manual override
or auto-rated cache guess:
– `CostumeInsulationById` gained 4 entries (collectible IDs 12717, 1101,
12719, 286) with their confirmed `totalInsulation` values, each
superseding a previous auto-rated cache guess.
– `OutfitStyles` gained 11 entries (collectible IDs 4804, 4798, 4800,
9746, 7195, 8786, 11819, 11818, 11820, 11851, 5309). The final tier for
each is confirmed correct (set manually by a player after observing the
outfit in-game), but since this table stores `baseMaterial`/`coverage`/
`flavorBonus` rather than a direct tier, those fields were chosen only
as the minimal combination that reproduces the confirmed tier they
are **not** an independent material assessment, and the collectibles’
actual names/appearances haven’t been looked up yet. Flagged
accordingly in each entry’s `flavorNote`.
### 2.7.3
– **Standardized the author credit** to `@Kreksar5 and Claude.ai` in the
`## Author` manifest field and this README’s byline, and named Claude.ai
specifically (rather than generic “AI assistance”) in the manifest
description and the AI-assistance disclosure above, matching the crediting
convention used across this addon’s companion libraries.
### 2.7.2
– **Corrected the `## Author` manifest field**, which was still the literal
placeholder text “YourName” left over from the manifest template never
actually filled in. Fixed to the real author.
### 2.7.1
– **Full API audit against the official ESOUI API 101050 documentation.**
Every function call, method call, and constant referenced across the
library was cross-checked against the API doc and, where the doc didn’t
cover it (manager singletons, third-party libraries), against the live
ESOUI source. No invalid or deprecated API usage found `ZO_OUTFIT_MANAGER`
and its `GetOutfitManipulator`/`GetSlotManipulator`/`GetCurrentCollectibleId`
chain, and the `LibAddonMenu`/`LibSavedVars` calls, are all legitimate and
correctly used. No code changes required.
### 2.7.0
– **Override editor redesigned around explicit Layer/Slot dropdowns.**
Replaced the auto-detected “currently active item” list (and the 2.6.3
background poll that tried to keep it in sync) with two small, permanently
static dropdowns: **Layer** (Costume/Polymorph, Outfit, Armor) and **Slot**
(Head/Shoulders/Chest/Hands/Waist/Legs/Feet, disabled for Costume/Polymorph
since that’s whole-body). Picking either does a FRESH live lookup at that
exact moment via three new independent Calculator functions
`Calc.ResolveCostumeOrPolymorph()`, `Calc.ResolveOutfitSlot()`,
`Calc.ResolveArmorSlot()` each of which reads live game state directly
and completely ignores ESO’s usual visual precedence. This means you can
now inspect (and override) your Armor style on a slot even while a costume
is actively displayed instead, or check what your Outfit has set for a
slot regardless of what’s currently showing. If a layer/slot has nothing
active (e.g. Costume picked while no costume is worn, or an empty armor
slot), a placeholder tier and an explanatory note are shown instead of
leaving the fields blank. A **Refresh Preview** button re-runs the same
live lookup for whenever gear changes without touching either dropdown.
– **Root-cause bug fix: Settings panel re-registration was silently a no-op.**
The options table powering the whole Settings panel was declared as
`local optionsTable = { …, func = function() … optionsTable … end, … }`.
In Lua, a local variable’s scope begins only AFTER its declaration
statement finishes, so any closure written INSIDE that table constructor
which refers to `optionsTable` which is exactly what the Refresh/Apply
Override/Reset buttons did to push a rebuilt panel back to LibAddonMenu
actually resolved to a global of that name (`nil`), not the local table
being built. Every one of those buttons had therefore been calling
`LAM:RegisterOptionControls(Settings.panelId, nil)` this whole time. This
is almost certainly the real reason the Settings panel never seemed to
refresh, going back to before the tier-system rewrite. Fixed by
forward-declaring `local optionsTable` on its own line before assigning the
table to it, which is the standard Lua idiom for self-referencing
structures; verified with a minimal isolated reproduction before and after
the fix, and with an end-to-end test through the actual button handlers.
### 2.6.3
– **Background auto-refresh for the “currently active item” dropdown.**
Previously the list only reflected equipped gear/costume/outfit at the
moment the addon loaded, or whenever Refresh/Apply Override/Reset was
pressed changing costume or outfit mid-session left it stale until one
of those was clicked. Added a lightweight poll (`EVENT_MANAGER:RegisterForUpdate`,
every 2 seconds) that compares a cheap fingerprint of “what’s currently
resolving” (source + costume/polymorph ID + each slot’s collectible/style
ID) against the last-seen one, and only pushes a rebuilt options table back
to LibAddonMenu when something actually changed. This deliberately avoids
hooking any specific ESO gear/costume-changed event, since there’s no single
confirmed event that reliably fires for every costume/outfit/armor change
alike and this addon’s data tables hold to a no-unverified-API-constants
standard polling a signature built from data this file already computes
gets the same result using only a bedrock-stable API. Updates land within
~2 seconds of a costume/outfit/armor change, whether or not the Settings
panel happens to be open at the time.
### 2.6.2
– **Bug fix: costume/polymorph misclassification after applying an override.**
Costumes and polymorphs share the same override key namespace
(`costume_`) and the same detection path (`GetActiveCollectibleByType`),
since ESO has no separate `COLLECTIBLE_CATEGORY_TYPE_POLYMORPH`. A prior fix
that let polymorph overrides win over the data table accidentally checked
`if polymorphOverride or polymorphEntry then` meaning applying an override
to a *plain costume* made it get misclassified as a polymorph on the very
next lookup (`result.source` flipped from `”costume”` to `”polymorph”`, and
the breakdown slot key flipped from `””` to `””`). This
is what caused the “Prefill from currently active item” dropdown in Settings
to mislabel a costume as a “Polymorph” once you’d set an override on it.
Classification now comes ONLY from table membership
(`PolymorphInsulationById`/`PolymorphInsulation`), independent of whether an
override exists; the override still applies to the resolved tier either way.
The insulation *value* was never wrong only the source/label but the
label feeds directly into the prefill dropdown and `/insulation`/`/costumeids`
output, so this is worth updating for.
### 2.6.1
– **Override editor prefill.** Added a “Prefill from currently active item”
dropdown above the Override Type/ID fields, listing whatever costume,
polymorph, outfit piece, or armor style is *actually resolving right now*
using the exact same precedence `GetInsulationBreakdown()` uses (polymorph/
costume beats outfit beats worn armor; an outfit slot with no style set
falls back to the armor style showing through). Picking an entry auto-fills
Override Type, the ID field, and the Insulation Tier dropdown with that
item’s current resolved values, and shows the game’s own display name
(`GetCollectibleName()` for outfits/costumes, the resolved English style
name for armor) so there’s no need to cross-reference `/styleids`,
`/outfitids`, or `/costumeids` by hand. “(manual entry)” remains available
as the first choice for typing an arbitrary ID. The list refreshes whenever
the panel opens, when **Refresh** is pressed, when an override is applied,
or when overrides are reset.
### 2.6.0
– **Staggered tier system.** Replaced the free-floating 0100 continuous scale
with eleven fixed tiers, staggered in increments of 10 from -10 to 90
(Magically Cooled, No Insulation, Minimal, Light, Coarse Light,
Layered/Leather, Full Leather/Light Metal, Heavy Leather/Fur, Full
Fur/Metal, Heavy Fur/Metal, Magically Heated). Every style, outfit piece,
costume, and polymorph now resolves to one of these tiers via
`Calc.SnapToTier()`. `-10`/`90` are reserved for entries flagged
`magical = true` (Flame Atronach, Ice Wraith); everything else is clamped
to the mundane 080 range before snapping.
– **Per-slot percentage adjustment.** A style/outfit piece’s tier is now a
full-body reference value; individual slots contribute a *percentage* of
that tier based on body coverage (`slot_contribution = round(tier
slotPercentage)`). `SlotCoverage`/`OutfitSlotCoverage` were rebalanced from
summing to 0.90 up to exactly 1.00, so a full matching set reproduces its
tier exactly as the total. Costumes and polymorphs are **not** slot-adjusted
their snapped tier is the total, unadjusted, since they’re whole-body.
– **Dropdown-based override editor.** The Settings panel’s “Insulation value”
free-text editbox was replaced with an “Insulation tier” dropdown listing
the eleven tiers by name, so an override can never drift off-tier.
Programmatic `SetOverride()` calls snap any value passed in to the nearest
tier instead of clamping 0100; `nil` (not `0`) now clears an override,
since `0` is a legitimate tier (“No Insulation”).
– **Saved-variable migration (v4 → v5).** Existing overrides from the old
0100 scale are snapped onto the nearest tier on first load rather than
being discarded, preserving prior manual tuning as closely as the new scale
allows.
– **Bug fix:** `NearestMaterialForTarget()` (used by `/scanoutfitstyles` to
guess a base material for unrecognized outfit pieces) was missing the 0.90
slot-weight-sum factor present in every other full-body reference
calculation in this file, so its reference numbers (fur=100, leather=55.6,
…) didn’t match the documented full-set reference table (fur=90,
leather=50, …). Fixed to multiply by 0.90 like everywhere else.
– `GetInsulationForStyle()` now simply delegates to the new
`Calc.GetStyleTier()` instead of duplicating the formula.
– Version bumped to 2.6.0 (`AddOnVersion: 8`), `SAVED_VARS_VERSION` to 5.
### 2.5.3
– **Corrected `LibAddonMenu-2.0` dependency floor**: was mistakenly bumped to
`>=45` in 2.5.1, but LibAddonMenu’s own manifest documentation shows `43`
as the latest available version, not `45`. Fixed to `>=43`. The 2.5.1 entry
below has also been corrected to reflect the right number.
### 2.5.2
– **”What addons cannot do” compliance verified** against the actual ESOUI
rules text (previously unverifiable due to a fetch issue with that specific
thread see 2.5.1’s note). Checked all 24 prohibited-behavior items: this
addon doesn’t touch the character-select/crown-store/scrying/Tribute UI,
doesn’t read other players’ or NPCs’ positions, doesn’t change any visual
effect/texture/sound/nameplate/camera/tooltip, doesn’t move/cast/interact
with anything, doesn’t touch quickslot items, doesn’t write outside
`SavedVariables`, doesn’t talk to external web services, and doesn’t share
data between players. It’s a pure read-only calculator using documented API
functions (the explicitly sanctioned “what addons could do” item #7), plus a
settings panel and slash commands. **No code changes were required.**
– Specifically confirmed: `CHAT_SYSTEM:AddMessage()`, used throughout this
addon’s slash commands, is the standard local-only system-message print
and is **not** the prohibited “send messages to chat directly” behavior
(that rule targets auto-sending player-authored text into channels other
players see, e.g. simulating Enter on `/say` or guild chat this addon
never does that).
### 2.5.1
– **ESOUI release-rules compliance pass**, checked against the ESOUI “Please
read: Before you release a new/update your addons” guidelines:
– **AI-code disclosure added.** Per the rule requiring AI involvement to be
named at the top of the addon description when the AI-written code/data
hasn’t been independently validated, added a disclosure to both the
manifest `## Description` and the top of this README. Most of this
addon’s commands and data tables were built with AI assistance and are
not yet confirmed working/performant in actual gameplay.
– **Added the missing `## AddOnVersion` manifest tag.** Libraries need this
so dependent addons can use `DependsOn: LibArmorInsulation>=N` version
checks (the same mechanism this addon already uses for its own
`LibAddonMenu-2.0` dependency). Starting value: `6` (one per tracked
release so far); increment by 1 on every future release regardless of the
semantic `## Version` string.
– **`## APIVersion` updated** from the stale `101040` to `101049 101050`
(current Live and PTS as of this pass), so the addon no longer shows as
“Outdated” in-game.
– **`LibAddonMenu-2.0` dependency floor bumped** from `>=33` to `>=43`,
matching the version explicitly cited as current in the ESOUI rules
thread at time of this pass. `>=` (rather than no version check) is
required so the addon doesn’t silently load against a pre-Summerset
LibStub-only build of LAM.
– **Reviewed for global variable leaks** none found. The codebase already
follows the recommended single-global-table pattern (`LibArmorInsulation`,
`LibArmorInsulationData`, `LibArmorInsulationCalc`,
`LibArmorInsulationSettings`), with all other identifiers declared `local`.
– **Not independently verified this pass:** the “What AddOns must not do”
rules thread (linked from the same guidelines post) could not be fetched
to confirm compliance line-by-line at the time. **Resolved in 2.5.2** once
the actual rules text was provided directly see that entry for the
confirmed result.
### 2.5.0
– **152 manually-researched costume entries added to `CostumeInsulation`.**
Sourced from UESP search-result snippets (direct page fetches are blocked by
UESP’s bot detection, so coverage is best-effort rather than exhaustive
roughly 134 of ~308 total ESO costumes were found with usable description
text; the rest were left for `/scancostumes` to auto-rate). Each entry’s
`flavorNote` carries a “ tag:
– **high** the source text stated an explicit material (e.g. “studded
leather doublet,” “Scaly Cloth Scraps,” “Fighters Guild Cool-Weather Gear”).
– **medium** a strong contextual/regional inference (e.g. a scout outfit
explicitly tied to a named cold-climate zone).
– **low** a thematic guess with no explicit material cue in the source;
these are reasonable starting points but are good candidates for manual
re-verification, and `/scancostumes` will not override them since
`CostumeInsulation` now outranks the scanner cache (see below).
– One entry (**Old Orsinium Sentry**) corrects an earlier same-session guess
after better source text was found later in the research pass.
– **Costume resolver priority reordered.** `CostumeInsulation` (this
manually-curated table) now outranks `sv.costumeCache` (the `/scancostumes`
runtime cache) in `GetInsulationBreakdown()`. New priority order: user
override → `CostumeInsulationById` → `CostumeInsulation` →
`sv.costumeCache` → `DEFAULT_COSTUME`. Previously the scanner cache outranked
this table, which meant a manually-verified entry could be silently shadowed
by a coarser auto-rated guess.
– **`OutfitStyles` gains a `Styles` fallback.** Most outfit-style
collectibles are account-wide unlockable versions of existing crafting
motifs, just reached through a different ID space. `GetOutfitStyleData()`
now resolves an unrecognized collectible ID’s name via `GetCollectibleName()`
(stripping a trailing `” Style”` suffix) and checks `Styles` before
falling back to the `/scanoutfitstyles` cache so a style whose name matches
an existing motif entry needs no new per-ID data at all. `/scanoutfitstyles`
reports these as “ in its output.
– **9 new motifs added to `Styles`** to close a coverage gap found while
researching outfit styles: Swordthane, Dark Executioner, Necrom Armiger,
Order of the Lamp, Ancient Mirrormoor, Companion Revelry, Fharun Moonlight,
Chosen of Anu, Chosen of Padomay. These are Crown Crate/Collector’s Edition
unlocks rather than world-found motif books, so they postdate the original
“Motifs 1129” coverage. Material assignments are UESP-sourced where the
style explicitly resembles an existing entry (e.g. Dark Executioner →
resembles Dark Brotherhood armor); the rest are lower-confidence thematic
inferences from cosmic-duality/regional lore with no explicit material
description available.
### 2.4.0
– **`/scancostumes` now scans description text, not just name.** `AutoRate`
checks the costume’s name against the keyword list first (names are usually
more deliberately worded), then falls back to `GetCollectibleDescription()`
flavor text if the name has no hit. Each cache entry now records a
`matchSource` (`”name”`, `”description”`, or `”none”`) so genuinely unmatched
entries sitting on the generic 50 fallback can be identified.
– **New `/scanoutfitstyles` command.** Outfit style pages had no auto-detection
path at all (`OutfitStyles` was a hand-only stub table). This walks every
unlocked outfit’s 7 slots (same enumeration as `/outfitids`) and, for any
collectible ID not already in `OutfitStyles`, keyword-rates it via name +
description and writes a guessed `{baseMaterial, coverage, flavorBonus}`
into the new `sv.outfitStyleCache`. `GetOutfitStyleData()` now checks this
cache as a fallback before `OutfitStyles`.
– **New `/costumesneedreview` and `/outfitstylesneedreview` commands.** Filter
each respective cache down to entries where the keyword scan found no match
in either name or description a short worklist for manual UESP-backed
overrides instead of scrolling the full scan output.
– **Shared keyword table.** `KEYWORD_RATINGS` and `AutoRate` were hoisted out
of `/scancostumes` to file scope so `/scanoutfitstyles` draws from the same
list a new pattern only needs adding once.
– **Removed an unused dynamic costume-discovery path.** An earlier in-progress
approach (`BuildCostumeKeywordMap()`, brute-force probing every collectible
ID 1→N) was removed before release it would have silently outranked the
better-designed `/scancostumes` + `sv.costumeCache` resolution order. Not
shipped in any prior version; mentioned here for the record.
– **Saved variables bumped to version 4;** v2/v3 saves are migrated
automatically, preserving existing overrides and caches.
### 2.3.0
– **Name-keyed style table.** `LibArmorInsulationData.Styles` is now keyed by the
English name returned by `GetItemStyleName()` (e.g. “, “)
instead of raw `StyleItemIndex` integers. This mirrors the zone-ID lesson: style
integers can be reassigned between ESO API updates, but the English name is stable.
– **Runtime ID maps built at login.** `LibArmorInsulationCalc.BuildStyleIdMaps()` is
called during `EVENT_ADD_ON_LOADED`. It iterates `GetItemStyleName(i)` for
`i = 1, GetHighestItemStyleId()` and populates two tables:
– `LibArmorInsulationData.StyleIdByName` `{ = 5, }`
– `LibArmorInsulationData.StyleNameById` `{ = “Nord”, }`
These maps are rebuilt every login from live game data, so any future ZOS
reassignment of integer IDs is handled automatically without a data-table update.
– **Lookup chain in Calculator.** All style lookups now follow:
`styleId → StyleNameById → Styles → DEFAULT`.
The integer from `GetItemLinkItemStyle()` is still accepted everywhere in the
public API; it is resolved to a name internally and never stored as a persistent
key.
– **`styleName` added to breakdown results.** Each slot entry in
`GetInsulationBreakdown().slots` now includes a `styleName` field (string or nil)
alongside the existing `styleId`, so callers can display a human-readable name
without performing their own lookup.
– **Override keys unchanged.** Saved-variable override keys remain `”style_N”`
(integer-based) because the player set them against a specific integer at a known
point in time. They continue to work the integer is used only as a unique
token for the saved-variable key, not for a data-table lookup.
### 2.2.0
– **Full style coverage:** `LibArmorInsulationData.Styles` now contains entries for all
crafting-motif style IDs known as of ESO Update 49 (motifs 1129, IDs 1129).
Previously only ~67 IDs were populated; ~62 motifs from the Thieves Guild, Dark
Brotherhood, Morrowind, Summerset, Elsweyr, Greymoor, Blackwood, High Isle, Necrom,
and Gold Road chapters were missing entirely.
– **Corrected several ID-to-name mappings** that were shifted by one due to a
miscount of the gap between the racial styles (110) and the first post-racial motifs:
– Telvanni was “; corrected to “ (Motif 50).
– Dragonscale was “; ID 36 is Dark Brotherhood (Motif 36). Dragonscale is not a
craftable motif removed; dragon-scale aesthetics are covered by individual
dungeon-set pieces with no motif ID.
– Snowhawk “ was an invention with no confirmed ITEM_STYLE_* counterpart;
Snowhawk is an **outfit-style collectible only** (not a motif). Entry removed from
`Styles`; use the `OutfitStyles` collectible table instead.
– IDs 8089 ancestral styles renumbered to match confirmed Motifs 8795.
– IDs 90105 updated to match Greymoor / Blackwood / High Isle motif numbering.
– **Zone ID statement evaluated:** The claim that “ESO zone IDs are large non-sequential
integers the only stable identifier is the English zone name `BuildZoneIdMaps()`
inverts the name→ID mapping” does **not apply to style IDs** and has **no relevance
to this library**. Style IDs (`StyleItemIndex` / `ITEM_STYLE_*`) are small sequential
integers starting at 1 and are stable across API updates the exact opposite of zone
IDs. This library does not use `LibZone`, `BuildZoneIdMaps()`, or any zone-ID
machinery. No changes were required.
### 2.1.0
– Outfit slots now look up insulation by **collectible ID** via `ZO_OUTFIT_MANAGER` rather than by `ITEM_STYLE_*` ID, matching how ESO actually stores outfit style data internally.
– Added `LibArmorInsulationData.OutfitStyles` table (keyed by collectible ID) separate from `Styles` (keyed by `ITEM_STYLE_*`).
– `SetOverride` now accepts an `idType` parameter (`”style”`, `”outfit”`, `”costume”`) to disambiguate the three ID spaces.
– Added `sv.costumeCache` a persistent per-account cache populated by `/scancostumes`.
– New slash commands: `/costumeids`, `/scancostumes`, `/clearcostumecache`, `/outfitids`, `/outfitactive`, `/diagcollectibles`.
– Added `LibArmorInsulationData.CostumeInsulationById` (collectible ID keyed) alongside the existing name-keyed `CostumeInsulation` table.
– Saved variables bumped to version 3; v2 saves are migrated automatically.
– Detection of active outfit changed from `GetActiveOutfitIndex()` to `GetEquippedOutfitIndex()` (confirmed correct ESO API function).
### 1.0.0
– Initial release.
—
## License
MIT free to use, modify, and redistribute with attribution.

