diff --git a/crates/benilla-app/src/ui_trainer/mod.rs b/crates/benilla-app/src/ui_trainer/mod.rs index 203bdb4f..70e0307d 100644 --- a/crates/benilla-app/src/ui_trainer/mod.rs +++ b/crates/benilla-app/src/ui_trainer/mod.rs @@ -217,7 +217,9 @@ fn resolve_service( TrainerService { spell_id: wire.spell, name: display.map(|d| d.name.clone()), - subtext: display.and_then(|d| d.rank.clone()), + // `0x4d8b50` answers the record's NameSubtext, "" for a rankless spell (`0x4d8b84`), and + // NULL without a Spell record (`0x4d8b58`), which `0x6f3890` pushes as nil. + subtext: display.map(|d| d.rank.clone().unwrap_or_default()), // Over the wire spell, like the name and subtext ([`service_icon`]). texture: service_icon(wire.spell, trainer_type, spells, icons, items, commands), // Empty: the 1.12 `GetTrainerServiceDescription` returns `Spell.dbc`'s Description with diff --git a/crates/benilla-app/src/ui_trainer/tests.rs b/crates/benilla-app/src/ui_trainer/tests.rs index 1bcb7c0f..6da6a27d 100644 --- a/crates/benilla-app/src/ui_trainer/tests.rs +++ b/crates/benilla-app/src/ui_trainer/tests.rs @@ -328,7 +328,11 @@ fn display_name_is_the_wire_spell_not_the_taught_one() { &BTreeSet::new(), ); assert_eq!(svc.name.as_deref(), Some("Apprentice Blacksmith")); - assert_eq!(svc.subtext.as_deref(), None); + assert_eq!( + svc.subtext.as_deref(), + Some(""), + "a rankless spell's NameSubtext is \"\" (0x4d8b84)" + ); assert_ne!( spells.get(2020).map(|d| d.name.as_str()), spells.get(2018).map(|d| d.name.as_str()), diff --git a/crates/benilla-ui/src/script/trainer/mod.rs b/crates/benilla-ui/src/script/trainer/mod.rs index 91fb21aa..d875e97f 100644 --- a/crates/benilla-ui/src/script/trainer/mod.rs +++ b/crates/benilla-ui/src/script/trainer/mod.rs @@ -108,7 +108,12 @@ pub struct TrainerService { pub spell_id: u32, /// The wire spell's name; `None` (before `Spell.dbc` loads) answers nil, shown as `UNKNOWN`. pub name: Option, - /// The wire spell's rank text, such as "Rank 2". + /// The wire spell's NameSubtext, such as "Rank 2", "" for a rankless spell. + /// + /// Deviation: `None` — no Spell record at all — answers `""` here, where the reference pushes + /// nil (`0x4d8b50` returns NULL at `0x4d8b58`, which `0x6f3890` pushes as nil). The getter's + /// own comment says why: the stock window formats this value unguarded, so a nil raises inside + /// `ClassTrainerFrame_Show` and the window then shows nothing and trains nothing. pub subtext: Option, pub texture: Option, pub description: String, diff --git a/crates/benilla-ui/src/script/trainer/tests.rs b/crates/benilla-ui/src/script/trainer/tests.rs index 8aab4236..5d4340ab 100644 --- a/crates/benilla-ui/src/script/trainer/tests.rs +++ b/crates/benilla-ui/src/script/trainer/tests.rs @@ -108,6 +108,44 @@ fn tree_interleaves_headers_and_ordered_services() { assert_eq!(info(&mut s, 5), ("Bloodrage".into(), "unavailable".into())); } +/// A service row whose spell has no rank answers the empty string for its subText (the Spell +/// record's `NameSubtext`, `0x4d8b84`), never nil: a client's FrameXML may `format` it before its +/// nil guard, which throws on nil. Without a Spell record `0x4d8b50` returns NULL (`0x4d8b58`), +/// which `0x6f3890` pushes as nil. +#[test] +fn a_rankless_service_answers_an_empty_string_for_its_subtext() { + let mut s = UiScript::new().unwrap(); + let mut t = trainer(); + t.services[0].subtext = Some(String::new()); // Heroic Strike, row 2 + s.set_trainer(Some(t)); + + let (name, sub, ty) = s + .eval::<(String, mlua::Value, String)>( + "local n,s,t = GetTrainerServiceInfo(2) return n,s,t", + ) + .unwrap(); + assert_eq!((name.as_str(), ty.as_str()), ("Heroic Strike", "available")); + match sub { + mlua::Value::String(v) => assert_eq!(v.to_str().unwrap(), ""), + other => panic!("the rankless subText must be a string, got {other:?}"), + } + // A `format` over it must not throw. + assert_eq!( + s.eval::("local _,sub = GetTrainerServiceInfo(2) return format(\"(%s)\", sub)") + .unwrap(), + "()" + ); + // A ranked service is unchanged. + assert_eq!( + s.eval::("local _,sub = GetTrainerServiceInfo(3) return sub") + .unwrap(), + "Rank 1" + ); +} + + + + #[test] fn service_getters_read_the_row_at_a_visible_index() { let mut s = UiScript::new().unwrap();