ui: six of nampower's reads, and the item-icon probe behind two of them

First tranche off the DLL work list. The method that produced it is worth
recording with the code: `SuperCleveRoidMacros`'s `NampowerAPI.lua` is a
capability registry — a `VERSION_REQUIREMENTS` table of every nampower
name with its minimum version, reached through `HasFeature`, which tests
the version **and** `_G[name] ~= nil`. So none of this was broken, and
raising `GetNampowerVersion` would not have helped: the only honest way
to make one of those features live is to define the global.

- `PlayerIsMoving()`, `PlayerIsRooted()`, `PlayerIsSwimming()` — `1` or
  nil, as upstream's own doc lines state, never a boolean. All three read
  the per-tick posture snapshot `SelfMovement` already carries, which
  gains `rooted` (`MOVEFLAG_ROOT`) and `moving` (any of the six movement
  bits, the keyboard turns included — they are input to the same word the
  client tests, and a turn in place does interrupt a cast).
- `IsAuraHidden(spellId)` — from a new `SpellLookup::hidden_aura`, filled
  with exactly `ui_aura::shown_in_aura_ui`'s predicate inverted, so it
  cannot contradict the aura list an addon reads beside it. An unknown
  spell is nil: "not hidden" is the honest answer about an aura this
  client knows nothing about, and the lists show a catalog miss too.
- `GetItemIconTexture(itemId)` — two hops, because an item template
  carries a display id and not an icon: the cache row's `displayInfoID`,
  then `ItemDisplayInfo`'s `Icon` through a new `ItemIconProbe`. A known
  item with no display row is nil (a data gap); an item the cache has
  never seen raises and records the ask, as every `item_record` verb here
  does. ClassicAPI's `GetItemIcon` wants the same probe, which is why it
  is a probe and not a closure.
- `GetTrinkets()` — slots 13 and 14 in `GetEquippedItem`'s own row shape,
  an empty slot absent rather than nil-valued, since the consumer walks
  it with `if t[i] then`.

Each is in `reference_surface.rs`'s `allowed_beyond_1_12` with why 1.12
has no spelling to prefer — the client reads its own movement word and
tells Lua nothing, applies the aura attribute test inside
`UnitBuff`/`UnitDebuff` without exposing either, and can only reach an
item icon through a bag slot it already holds.

All nine gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-23 21:06:41 -06:00
co-authored by Claude Opus 5
parent ada08720bb
commit f120e8003d
11 changed files with 339 additions and 2 deletions
@@ -37,6 +37,7 @@ fn install_boot_vm(world: &mut World) {
seed_vm_clock(world, &mut script);
install_addon_asset_resolvers(world, &mut script);
install_spell_catalog_probe(world, &mut script);
install_item_icon_probe(world, &mut script);
seed_creature_info(world, &mut script);
load_global_strings(world, &script);
load_emote_tokens(world, &script);
@@ -174,10 +175,34 @@ pub(crate) fn install_spell_probe(script: &mut UiScript, catalog: &benilla_forma
.filter(|&(item, count)| item != 0 && count != 0)
.collect(),
dispel: d.dispel,
// The same predicate `ui_aura::shown_in_aura_ui` applies to the lists, inverted:
// a catalog hit that is hidden there must read hidden here, or `IsAuraHidden`
// would disagree with the aura list an addon reads beside it.
hidden_aura: d.hidden_from_aura_bar() || d.tracking_aura(),
})
}));
}
/// `ItemDisplayInfo`'s inventory icons, behind nampower's `GetItemIconTexture` and ClassicAPI's
/// `GetItemIcon`. An item template carries a display id, not an icon, so both verbs need this
/// second hop and neither can be answered from the item cache alone.
///
/// The `(displayId, icon)` pairs are copied into the closure rather than sharing the catalog: the
/// icon is the only column either verb wants, the rest of an `ItemDisplay` is model and texture
/// paths for the renderer, and a VM build is rare (boot, world entry, `/reload`). Rows without an
/// icon are left out, so a hit is always a path.
fn install_item_icon_probe(world: &World, script: &mut UiScript) {
let Some(displays) = world.get_resource::<crate::entities::ItemDisplays>() else {
return;
};
let icons: std::collections::HashMap<u32, String> = displays
.catalog
.iter()
.filter_map(|(id, d)| d.icon.clone().map(|icon| (id, icon)))
.collect();
script.set_item_icon_probe(Box::new(move |display_id| icons.get(&display_id).cloned()));
}
/// Install [`install_spell_probe`] from the world's catalog, if it has one. The boot VM built at
/// `Startup` runs before `ui_action`'s loader has inserted [`crate::ui_action::Spells`] (its
/// `insert_resource` is a command, applied at the end of that schedule), so the character-screen
+9
View File
@@ -775,6 +775,15 @@ fn feed_self_movement(
mounted: store.is_some_and(|s| s.0.unit_mount_display_id() > 0),
indoors: world.is_some_and(|w| w.area_interior().is_some()),
falling: flags & move_flags::FALLING != 0,
rooted: flags & move_flags::ROOT != 0,
moving: flags
& (move_flags::FORWARD
| move_flags::BACKWARD
| move_flags::STRAFE_LEFT
| move_flags::STRAFE_RIGHT
| move_flags::TURN_LEFT
| move_flags::TURN_RIGHT)
!= 0,
shapeshift_form: store.map_or(0, |s| s.0.unit_shapeshift_form()),
mouselooking: rig.is_some_and(|r| r.mouselooking()),
});
@@ -902,6 +902,7 @@ mod tests {
icon: None,
reagents: Vec::new(),
dispel,
hidden_aura: false,
})
}));
@@ -944,6 +945,7 @@ mod tests {
icon: Some("Interface\\Icons\\Spell_Nature_Rejuvenation".into()),
reagents: Vec::new(),
dispel: 0,
hidden_aura: false,
})
}));
@@ -998,6 +1000,7 @@ mod tests {
icon: Some("Interface\\Icons\\WRONG".into()),
reagents: Vec::new(),
dispel: 0,
hidden_aura: false,
})
}));
assert_eq!(
@@ -1091,6 +1094,7 @@ mod tests {
icon: None,
reagents: vec![(17056, 1)],
dispel: 0,
hidden_aura: false,
}),
// A real spell that costs nothing.
133 => Some(crate::script::SpellLookup {
@@ -1099,6 +1103,7 @@ mod tests {
icon: None,
reagents: Vec::new(),
dispel: 0,
hidden_aura: false,
}),
_ => None,
}));
@@ -1141,6 +1146,7 @@ mod tests {
icon: None,
reagents: vec![(17056, 1)],
dispel: 0,
hidden_aura: false,
})
}));
s.set_spellbook(crate::script::SpellBookState {
@@ -110,6 +110,8 @@ mod tests {
mounted: true,
indoors: true,
falling: false,
rooted: false,
moving: false,
shapeshift_form: 3,
});
@@ -177,6 +177,9 @@ fn equipped_table(lua: &Lua, view: &crate::script::InvSlotView) -> mlua::Result<
/// expect — BetterCharacterPanel wraps exactly these three getters in `pcall`
/// (`BetterCharacterPanel-lib.lua:177`, `:198`, `:252`) while calling `GetEquippedItem` bare, which
/// is only worth doing for a function that can raise.
/// `INVSLOT_TRINKET1`/`INVSLOT_TRINKET2`, the two slots `GetTrinkets` reports.
const TRINKET_SLOTS: [usize; 2] = [13, 14];
fn item_record(lua: &Lua, item_id: Option<u32>) -> mlua::Result<crate::script::ItemTemplateView> {
let found = item_id.filter(|&id| id != 0).and_then(|id| {
let mut model = lua
@@ -735,6 +738,108 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// `GetItemIconTexture(itemId)` (v2.14+): the item's inventory icon as an extensionless
// `Interface\Icons\…` path, the sibling of `GetSpellIconTexture` above. Two hops, because an
// item template carries a *display* id and not an icon: the cache row's `displayInfoID`, then
// `ItemDisplayInfo`'s own `Icon` column through `Model::item_icon_probe`.
//
// An item the cache has never seen raises "Item not found in DBCache", exactly as the other
// `item_record` verbs here do — the caller's `pcall`-or-nil-check is the same either way, and
// the ask is recorded so the next call can answer. A known item whose display row is missing
// is **nil**: that is a gap in the data, not an error in the call.
lua.globals().set(
"GetItemIconTexture",
lua.create_function(|lua, item_id: Option<u32>| {
let display_id = item_record(lua, item_id)?.display_info_id;
let model = lua
.app_data_ref::<crate::script::Model>()
.expect("model app_data");
Ok(model
.item_icon_probe
.as_ref()
.filter(|_| display_id != 0)
.and_then(|probe| probe(display_id)))
})?,
)?;
// `GetTrinkets()` (v2.14+) -> the two trinket slots as a table keyed `1` and `2`, the same
// row shape `GetEquippedItem` hands back, with an empty slot absent rather than nil-valued
// (a nil value in a Lua table *is* absence, and `SuperCleveRoidMacros` walks it with `for i =
// 1, 2 do if t[i] then`). Slots 13 and 14, the reference's `INVSLOT_TRINKET1`/`2`.
lua.globals().set(
"GetTrinkets",
lua.create_function(|lua, ()| {
let t = lua.create_table()?;
{
let model = lua
.app_data_ref::<crate::script::Model>()
.expect("model app_data");
let token = inv_token(&model, None);
for (i, slot) in [TRINKET_SLOTS[0], TRINKET_SLOTS[1]].into_iter().enumerate() {
if let Some(view) = model.inv_slot(&token, slot).filter(|v| v.item_id != 0) {
t.set(i as i64 + 1, equipped_table(lua, &view)?)?;
}
}
}
Ok(t)
})?,
)?;
// `PlayerIsMoving()` / `PlayerIsRooted()` / `PlayerIsSwimming()` (v2.36+): `1` or **nil**, as
// upstream's own doc lines say ("Returns 1 if player is moving, nil if stationary"), not a
// boolean — `SuperCleveRoidMacros` branches on truthiness, but an addon printing the value
// would see `1` on the reference and `true` here.
//
// All three read the one per-tick posture snapshot `model.self_movement` already carries
// (`compat/classic_api/state.rs` feeds the same struct), so none of them costs a new feed.
// `PlayerIsSwimming` is `IsSwimming`'s bit under nampower's name and return shape; both exist
// because both mods publish their own, and an addon feature-detects whichever it was written
// against.
for (name, read) in [
("PlayerIsMoving", 0usize),
("PlayerIsRooted", 1),
("PlayerIsSwimming", 2),
] {
lua.globals().set(
name,
lua.create_function(move |lua, ()| {
let model = lua
.app_data_ref::<crate::script::Model>()
.expect("model app_data");
let m = model.self_movement;
let set = match read {
0 => m.moving,
1 => m.rooted,
_ => m.swimming,
};
Ok(set.then_some(1i64))
})?,
)?;
}
// `IsAuraHidden(spellId)` (v3.3+): `1` when that spell's aura is kept out of the Lua aura
// functions, nil otherwise — upstream's own wording, and the same wording as the `luaSlot` of
// `0` the buff/debuff events report for such an aura.
//
// Answered from `SpellLookup::hidden_aura`, which is `benilla-app`'s own
// `ui_aura::shown_in_aura_ui` predicate inverted, so this cannot disagree with the aura lists
// an addon reads beside it. An unknown spell id is **nil**, not `1`: "not hidden" is the
// honest answer about an aura this client knows nothing about, and the aura lists treat a
// catalog miss the same way — they show it.
lua.globals().set(
"IsAuraHidden",
lua.create_function(|lua, spell_id: Option<u32>| {
let model = lua
.app_data_ref::<crate::script::Model>()
.expect("model app_data");
Ok(spell_id
.filter(|&id| id != 0)
.and_then(|id| model.spell_probe.as_ref().and_then(|probe| probe(id)))
.is_some_and(|d| d.hidden_aura)
.then_some(1i64))
})?,
)?;
lua.globals().set(
"GetUnitGUID",
lua.create_function(|lua, token: Option<String>| {
@@ -754,6 +859,142 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
mod tests {
use crate::script::UiScript;
/// `GetItemIconTexture` takes the two hops an item icon needs — the cache row's display id,
/// then `ItemDisplayInfo` — and tells a data gap apart from an unknown item: a known item
/// whose display row is missing answers nil, an item the cache has never seen raises, as the
/// other `item_record` verbs here do.
#[test]
fn get_item_icon_texture_resolves_through_the_display_id() {
let mut s = UiScript::new().unwrap();
s.set_item_icon_probe(Box::new(|display_id| {
(display_id == 7777).then(|| "Interface\\Icons\\INV_Sword_04".to_string())
}));
// Nothing cached: the same raise every item verb here makes.
assert!(s
.eval::<Option<String>>("return GetItemIconTexture(123)")
.is_err());
for (id, display) in [(123u32, 7777u32), (124, 4242), (125, 0)] {
s.set_item_template(
id,
crate::script::ItemTemplateView {
display_info_id: display,
..Default::default()
},
);
}
assert_eq!(
s.eval::<Option<String>>("return GetItemIconTexture(123)")
.unwrap(),
Some("Interface\\Icons\\INV_Sword_04".to_string())
);
assert_eq!(
s.eval::<Option<String>>("return GetItemIconTexture(124)")
.unwrap(),
None,
"known item, missing display row: a data gap, not an error"
);
assert_eq!(
s.eval::<Option<String>>("return GetItemIconTexture(125)")
.unwrap(),
None,
"display id 0 is not a row"
);
}
/// nampower's three posture reads and `IsAuraHidden`, in the shapes its own doc lines give:
/// `1` or **nil**, never a boolean. `SuperCleveRoidMacros`'s `NampowerAPI.lua` gates each on
/// `HasFeature`, which checks the version *and* `_G[name] ~= nil`, so a wrong return shape
/// would not stop it calling — it would just make the value print as `true`.
#[test]
fn the_posture_reads_answer_one_or_nil_and_follow_the_feed() {
let mut s = UiScript::new().unwrap();
// Nothing fed yet: the one frame before the posture feed's first run.
for call in ["PlayerIsMoving()", "PlayerIsRooted()", "PlayerIsSwimming()"] {
assert_eq!(
s.eval::<Option<i64>>(&format!("return {call}")).unwrap(),
None,
"{call} before the first feed"
);
}
s.set_self_movement(crate::script::SelfMovement {
swimming: true,
rooted: true,
moving: false,
..Default::default()
});
assert_eq!(
s.eval::<Option<i64>>("return PlayerIsSwimming()").unwrap(),
Some(1)
);
assert_eq!(
s.eval::<Option<i64>>("return PlayerIsRooted()").unwrap(),
Some(1)
);
assert_eq!(
s.eval::<Option<i64>>("return PlayerIsMoving()").unwrap(),
None
);
// `IsSwimming` is ClassicAPI's name for the same bit; both mods publish one, and an addon
// feature-detects whichever it was written against, so they must agree.
assert!(s.eval::<bool>("return IsSwimming()").unwrap());
s.set_self_movement(crate::script::SelfMovement {
moving: true,
..Default::default()
});
assert_eq!(
s.eval::<Option<i64>>("return PlayerIsMoving()").unwrap(),
Some(1)
);
assert_eq!(
s.eval::<Option<i64>>("return PlayerIsRooted()").unwrap(),
None
);
}
/// `IsAuraHidden` answers for the same auras the buff and debuff lists omit, and **nil** for a
/// spell this client knows nothing about — "not hidden" is the honest answer there, and it is
/// what the aura lists do with a catalog miss as well.
#[test]
fn is_aura_hidden_follows_the_aura_lists_own_predicate() {
let mut s = UiScript::new().unwrap();
assert_eq!(
s.eval::<Option<i64>>("return IsAuraHidden(123)").unwrap(),
None,
"no probe at all"
);
s.set_spell_probe(Box::new(|id| {
Some(crate::script::SpellLookup {
name: format!("Spell {id}"),
rank: None,
icon: None,
reagents: Vec::new(),
dispel: 0,
hidden_aura: id == 999,
})
}));
assert_eq!(
s.eval::<Option<i64>>("return IsAuraHidden(999)").unwrap(),
Some(1)
);
assert_eq!(
s.eval::<Option<i64>>("return IsAuraHidden(123)").unwrap(),
None
);
assert_eq!(
s.eval::<Option<i64>>("return IsAuraHidden(0)").unwrap(),
None,
"id 0 is not a spell"
);
assert_eq!(
s.eval::<Option<i64>>("return IsAuraHidden()").unwrap(),
None
);
}
/// BetterCharacterPanel's four reads, in its own shapes
/// (`BetterCharacterPanel-lib.lua:93`, `:177`, `:193`, `:252`): the enchant check, the item
/// level, the quality colour and the two-hand test. All four were dead — `GetEquippedItem` was
@@ -291,6 +291,7 @@ mod option_verbs {
icon: Some("Interface\\Icons\\Spell_Nature_Rejuvenation".into()),
reagents: Vec::new(),
dispel: 0,
hidden_aura: false,
})
}));
+2 -2
View File
@@ -241,8 +241,8 @@ pub use model::{
RaceInfo,
};
pub use model::{
FontProbe, SpellDbcExtras, SpellIconProbe, SpellLookup, SpellMechanicProbe, SpellProbe,
SpellUsable, TextureProbe, TextureSizeProbe,
FontProbe, ItemIconProbe, SpellDbcExtras, SpellIconProbe, SpellLookup, SpellMechanicProbe,
SpellProbe, SpellUsable, TextureProbe, TextureSizeProbe,
};
pub(crate) use model::{Model, PendingTimer};
pub use model::{PlayerCacheEntry, SelfMovement, ServerClock};
+20
View File
@@ -51,6 +51,11 @@ pub struct SpellLookup {
/// (pfUI keys a `dispelTypeMap` on the number, `libs/libdebuff.lua:379`), and a name would
/// make a localized realm answer something its table cannot match.
pub dispel: u32,
/// Whether this spell's aura is kept out of the aura UI: `ATTR_DO_NOT_DISPLAY`,
/// `ATTR_EX_NO_AURA_ICON`, or a tracking aura. Exactly the predicate `benilla-app`'s
/// `ui_aura::shown_in_aura_ui` applies to the buff and debuff lists, so this answers for the
/// same auras those omit. Backs nampower's `IsAuraHidden`.
pub hidden_aura: bool,
}
/// The host's answer for *any* spell id, learned or not: `Spell.dbc`'s whole display table, which
@@ -60,6 +65,12 @@ pub struct SpellLookup {
/// back to the spellbook.
pub type SpellProbe = Box<dyn Fn(u32) -> Option<SpellLookup>>;
/// The host's answer for an `ItemDisplayInfo` row id: its inventory icon as an extensionless
/// `Interface\Icons\…` path. Keyed on the **display** id, not the item id, because that is the
/// column an item template carries and the one the reference's own vendor-cursor path resolves
/// through (`benilla-app`'s `cursor.rs`). `None` where the row or its icon is absent.
pub type ItemIconProbe = Box<dyn Fn(u32) -> Option<String>>;
/// A spell id to its `Spell.dbc` `Mechanic` column and that mechanic's enUS name from
/// `SpellMechanic.dbc`, for ClassicAPI's `C_Spell.GetSpellMechanicByID`.
///
@@ -258,6 +269,13 @@ pub struct SelfMovement {
pub indoors: bool,
/// `MOVEFLAG_FALLING`, set for the whole airborne arc.
pub falling: bool,
/// `MOVEFLAG_ROOT`, for nampower's `PlayerIsRooted()`. Not 1.12 data either: the client reads
/// its own movement word, and no stock binding exposes it.
pub rooted: bool,
/// Any of the six movement bits set — forward, backward, the two strafes, the two keyboard
/// turns — for nampower's `PlayerIsMoving()`. The turn keys count because they are movement
/// input to the same word the client tests, and a turn-in-place does interrupt a cast.
pub moving: bool,
/// `UNIT_FIELD_BYTES_2` byte 3, `0` not shapeshifted (`ShapeshiftForm.dbc` row otherwise).
pub shapeshift_form: u8,
/// `IsMouselooking()`: the camera rig's own mouse-look flag, the reference's `[cam+0x90] & 1`
@@ -699,6 +717,7 @@ pub(crate) struct Model {
pub(crate) spell_usable: std::collections::HashMap<u32, SpellUsable>,
/// `SpellIcon.dbc` lookup ([`SpellIconProbe`]).
pub(crate) spell_icon_probe: Option<SpellIconProbe>,
pub(crate) item_icon_probe: Option<ItemIconProbe>,
/// Creature display id to model path ([`CreatureModelProbe`]).
pub(crate) creature_model_probe: Option<CreatureModelProbe>,
/// The pet's spellbook, a second array as in the reference (`0xb700f0`, `0xb6f098`).
@@ -1597,6 +1616,7 @@ impl Model {
measurer: None,
texture_probe: None,
spell_probe: None,
item_icon_probe: None,
spell_mechanic_probe: None,
player_cache: HashMap::new(),
player_cache_enabled: [true, true],
+10
View File
@@ -89,6 +89,12 @@ impl super::UiScript {
}
/// Install the `SpellIcon.dbc` lookup behind `GetSpellIconTexture`.
/// Install [`super::ItemIconProbe`], the `ItemDisplayInfo` icon lookup behind nampower's
/// `GetItemIconTexture` and ClassicAPI's `GetItemIcon`.
pub fn set_item_icon_probe(&mut self, probe: super::ItemIconProbe) {
self.model_mut().item_icon_probe = Some(probe);
}
pub fn set_spell_icon_probe(&mut self, probe: super::SpellIconProbe) {
self.model_mut().spell_icon_probe = Some(probe);
}
@@ -278,6 +284,10 @@ pub(super) fn spell_display(model: &Model, spell_id: u32) -> Option<super::Spell
.map(|d| d.reagents.clone())
.unwrap_or_default(),
dispel: probed.as_ref().map_or(0, |d| d.dispel),
// Also the probe's: a spellbook slot carries no attribute columns, and `false` here
// would claim every known spell's aura is shown. A probe miss keeps `false`, which is
// what the aura lists do with a catalog miss too.
hidden_aura: probed.as_ref().is_some_and(|d| d.hidden_aura),
});
}
model.spell_probe.as_ref().and_then(|probe| probe(spell_id))
@@ -54,6 +54,7 @@ fn harness() -> UiScript {
icon: Some("Interface\\Icons\\Spell_Shadow_SiphonMana".into()),
reagents: Vec::new(),
dispel: 0,
hidden_aura: false,
})
}));
for (token, guid, name) in [
@@ -295,6 +295,28 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
// addon branches on to decide which of its rows to show. Not a 1.12 verb, and not
// namreeb/nampower's either — that fork has no Lua surface at all.
"GetNampowerVersion",
// nampower v2's three posture reads (script/compat/nampower.rs). 1.12 publishes no
// movement getter of any kind — the client reads its own movement word and tells Lua
// nothing — so there is no 1.12 spelling to prefer. `1`-or-nil, as upstream's own doc
// lines state. `SuperCleveRoidMacros`'s `NampowerAPI.lua` registers each in its
// `VERSION_REQUIREMENTS` table and reaches them through `HasFeature`, which tests the
// version *and* `_G[name] ~= nil`, so the honest way to make that feature live is to
// define the global rather than to raise the version claim.
// nampower v2's `GetItemIconTexture(itemId)` and `GetTrinkets()` (same file). 1.12 has
// neither: its only icon path for an item is `GetContainerItemInfo` on a bag slot you
// already hold, and it has no way at all to ask about an arbitrary item id or to read the
// two trinket slots as a pair. Both are in `SuperCleveRoidMacros`'s `HasFeature` table.
"GetItemIconTexture",
"GetTrinkets",
"PlayerIsMoving",
"PlayerIsRooted",
"PlayerIsSwimming",
// nampower v2's `IsAuraHidden(spellId)` (same file): whether an aura is kept out of the
// Lua aura functions. 1.12 has no way to ask — the client applies the attribute test
// inside `UnitBuff`/`UnitDebuff` and exposes neither the test nor the attributes. Answers
// from the same predicate `benilla-app`'s `ui_aura::shown_in_aura_ui` applies to the
// lists, so it cannot contradict the aura list an addon reads beside it.
"IsAuraHidden",
// ClassicAPI's Enum (script/compat/classic_api/enums.rs): the modern client's named
// constants, so an addon can write `Enum.PowerType.Mana` for 0. Pure data, no 1.12
// counterpart — 1.12 spells the same numbers as bare literals or `LE_*`/`ITEM_QUALITY_*`