ui: ClassicAPI's GetCraftReagentItemID

The one column 1.12's `GetCraftReagentInfo` does not answer: it gives the
reagent's name, icon and the needed/held counts, and no item id at all,
so nothing that keys a table on an item — pfUI's reagent counter — can
use the stock verb.

Same `(craftIndex, reagentIndex)` pair and the same rows that verb walks,
so the two cannot disagree about which reagent is meant. Out of range on
either index is nil, matching the stock verb's own nil-first return.

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:53:10 -06:00
co-authored by Claude Opus 5
parent de3a3411e7
commit 7861ab2710
3 changed files with 96 additions and 1 deletions
@@ -41,6 +41,31 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
})?,
)?;
// `GetCraftReagentItemID(craftIndex, reagentIndex)` -> the reagent's item id. 1.12's
// `GetCraftReagentInfo` answers its name, icon, needed and held counts and no id at all, so
// nothing that keys a table on an item — pfUI's reagent counter does — can use the stock verb.
//
// The same `(index, reagentIndex)` pair and the same rows that verb walks
// (`script/craft.rs`), so the two cannot disagree about which reagent is meant. Out of range
// on either index is nil, as the stock verb's own nil-first return is.
g.set(
"GetCraftReagentItemID",
lua.create_function(
|lua, (index, reagent_index): (Option<usize>, Option<usize>)| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(index
.zip(reagent_index)
.and_then(|(i, r)| {
crate::script::craft::recipe(&model, i)
.zip(r.checked_sub(1))
.and_then(|(recipe, r)| recipe.reagents.get(r))
})
.map(|reagent| reagent.item)
.filter(|&id| id != 0))
},
)?,
)?;
// `CastSpellNoToggle(spellId)`: cast without the stock toggle-off, for a spell whose aura is
// already up. The toggle lives in the client's cast path (`0x6e5100`'s autorepeat and aura
// tests), and benilla's cast queue does not toggle anything — a press is a send — so this is
@@ -135,6 +160,72 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
mod tests {
use crate::script::{UiScript, UnitState};
/// `GetCraftReagentItemID`: the one column 1.12's `GetCraftReagentInfo` does not answer. It
/// reads the same rows that verb walks, so the two agree about which reagent is meant — and
/// anything keying a table on an item (pfUI's reagent counter) needs the id, not the name.
#[test]
fn get_craft_reagent_item_id_matches_the_stock_reagent_row() {
let mut s = UiScript::new().unwrap();
assert_eq!(
s.eval::<Option<u32>>("return GetCraftReagentItemID(1, 1)")
.unwrap(),
None,
"no craft window open"
);
s.set_craft(Some(crate::script::CraftState {
name: "Enchanting".into(),
rank: 57,
max_rank: 75,
craft_type: 2,
recipes: vec![crate::script::CraftRecipe {
spell_id: 683,
tooltip: crate::script::CraftTooltip::Spell(683),
name: "Enchant Weapon - Minor Health".into(),
sub_name: String::new(),
difficulty: crate::script::TradeSkillDifficulty::Optimal,
num_available: 1,
icon: None,
description: None,
needs_item_target: true,
reagents: vec![crate::script::CraftReagent {
item: 10940,
name: Some("Illusion Dust".into()),
icon: None,
need: 1,
have: 3,
}],
tools: Vec::new(),
spell_level: 0,
}],
}));
assert_eq!(
s.eval::<Option<u32>>("return GetCraftReagentItemID(1, 1)")
.unwrap(),
Some(10940)
);
// The stock verb names the same reagent at the same pair.
assert_eq!(
s.eval::<String>("return (GetCraftReagentInfo(1, 1))")
.unwrap(),
"Illusion Dust"
);
// Out of range on either index, and a missing argument, are nil.
for call in [
"GetCraftReagentItemID(1, 2)",
"GetCraftReagentItemID(2, 1)",
"GetCraftReagentItemID(1, 0)",
"GetCraftReagentItemID(1)",
"GetCraftReagentItemID()",
] {
assert_eq!(
s.eval::<Option<u32>>(&format!("return {call}")).unwrap(),
None,
"{call}"
);
}
}
/// `GetPlayerInfoByGUID`'s seven, for a unit this client holds. 1.12 has no verb that takes a
/// GUID at all, so every column here comes from the snapshot the unit surface already carries
/// — and the GUID resolves through the same `Model::unit` as every other token spelling, so a
+1 -1
View File
@@ -161,7 +161,7 @@ impl super::UiScript {
}
/// The recipe at a 1-based index.
fn recipe(model: &Model, index: usize) -> Option<&CraftRecipe> {
pub(in crate::script) fn recipe(model: &Model, index: usize) -> Option<&CraftRecipe> {
let n = index.checked_sub(1)?;
model.craft.as_ref()?.recipes.get(n)
}
@@ -323,6 +323,10 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> {
// (script/compat/classic_api/unit.rs). 1.12 has no verb that takes a GUID at all, and no
// cast path that skips its own toggle — benilla's queue does not toggle, so the second is
// the plain cast by id, the same reasoning nampower's `CastSpellNoQueue` carries.
// ClassicAPI's `GetCraftReagentItemID(craftIndex, reagentIndex)` (same file): the one
// column 1.12's `GetCraftReagentInfo` does not answer — it gives the name, icon and the
// two counts and no id at all, so nothing keying a table on an item can use it.
"GetCraftReagentItemID",
"CastSpellNoToggle",
"GetPlayerInfoByGUID",
"GetQuestIDForLogIndex",