GetTrainerServiceInfo answers "" for a rankless service's subText (#1947)
`GetTrainerServiceInfo` answered nil for a rankless service's subText. In 1.12.1 the subText is the Spell record's NameSubtext (`0x4d8b50`, read at `0x4d8b84`), the empty string for a spell with no rank; only a service with no Spell record answers nil (`0x4d8b58`). The resolver now keeps an empty rank as `""` and the binding pushes None as nil. Tests cover both rows and the install-backed Apprentice Blacksmith trainer. Supersedes #1935. Co-authored-by: kalistoking <148718830+kalistoking@users.noreply.github.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit fdbb4bb070382a6cf6b30bdaf33214c972cd02f4) # Conflicts: # crates/benilla-ui/src/script/trainer/tests.rs # docs/DEVIATIONS.md Absorbed into the octo fork. Mostly already true here: this fork's GetTrainerServiceInfo already answered "" for a rankless service, with its own note on the stock window's unguarded format. What is kept is upstream's test for it and the field doc. Two tests the conflict hunk dragged in from other upstream commits are dropped: a_service_row_answers_one_for_its_fourth_value and service_description_is_nil_without_text need struct shapes this fork lacks (an Option description). A third, a_service_without_a_spell_record_answers_nil_for_its_subtext, is dropped because it contradicts our deliberate choice — we answer "" for a missing Spell record too, and that is now recorded as a Deviation on the field rather than left as an undocumented difference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
co-authored by
Claude Opus 5
parent
5fa31a073b
commit
f0adc9a3ba
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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<String>,
|
||||
/// 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<String>,
|
||||
pub texture: Option<String>,
|
||||
pub description: String,
|
||||
|
||||
@@ -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::<String>("local _,sub = GetTrainerServiceInfo(2) return format(\"(%s)\", sub)")
|
||||
.unwrap(),
|
||||
"()"
|
||||
);
|
||||
// A ranked service is unchanged.
|
||||
assert_eq!(
|
||||
s.eval::<String>("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();
|
||||
|
||||
Reference in New Issue
Block a user