A unit's reaction caps the reputation rank at Revered, as in 1.12.1, so an Exalted faction's NPC no longer breaks the target frame (#455)
Targeting an NPC whose faction you are Exalted with no longer opens a script error. `UnitReaction` answered 8 there, and stock `TargetFrame_CheckFaction` indexes `UnitReactionColor[reaction]`, which has seven entries (`TargetFrame.lua:6-14`, `:183`). - **The cap.** `UnitReaction 0x6061e0` takes `0x606530`'s answer through `min(rank, 6)` at `0x606439` (`cmp esi,6` / `jl` / `mov esi,6`), so Revered and Exalted both read 6, and the Lua binding adds one (`0x51683e`), so Lua sees at most 7. `ring_reaction` now caps its answer there. - **The ladder stays whole.** `reputation_rank` is untouched: the reputation pane and the item requirement lines still read Exalted. - **Every caller** of `ring_reaction` models `0x6061e0` and tests at 1, 3, 4 or 5, so none changes but the four Lua unit feeds (target, targettarget, npc, mouseover), which now share one `unit_reaction` helper. The vendor discount's Exalted row reads as Revered, as in the reference, and still takes 10%. - **Comments** that gave the scale as 0..7 or 1..8 now say 0..=6 and 1..=7. Checked: `the_reputation_rank_is_capped_at_revered` walks both sides of the Honored, Revered and Exalted thresholds; `an_exalted_npc_target_reads_revered_and_its_plate_is_green` runs stock `TargetFrame` on an Exalted Stormwind unit. With the cap loosened to 7 both fail, the second with the issue's `TargetFrame.lua:183: attempt to index field '?'`; restored, both pass. Gates green on macOS; no platform seam touched. Fixes #440 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
79702e8921
commit
5f05465c8e
@@ -443,8 +443,8 @@ pub(super) fn inspect_ui(
|
||||
3 => "neutral",
|
||||
4 => "friendly",
|
||||
5 => "honored",
|
||||
6 => "revered",
|
||||
_ => "exalted",
|
||||
// `0x606439` caps the rank at 6: Exalted reads here too.
|
||||
_ => "revered+",
|
||||
};
|
||||
// A faction with a reputation slot reacts by our standing (`0x605fc0` -> `0x4d63a0`),
|
||||
// before any template comparison; everything else by the template comparator.
|
||||
|
||||
@@ -119,8 +119,9 @@ mod tests {
|
||||
(5, UNIT_FLAG_PVP, 7, D15),
|
||||
(5, UNIT_FLAG_PVP, 8, D20),
|
||||
(5, UNIT_FLAG_PVP, 19, D20),
|
||||
// Revered and up pass the same signed `cmp eax,5`; Neutral does not.
|
||||
(7, UNIT_FLAG_PVP, 19, D20),
|
||||
// Revered, which Exalted reads too (`0x606439`), passes the same signed `cmp eax,5`;
|
||||
// Neutral does not.
|
||||
(6, UNIT_FLAG_PVP, 19, D20),
|
||||
(3, UNIT_FLAG_PVP, 19, D10),
|
||||
// Every other flag bit set but the PvP one: no honor step.
|
||||
(5, !UNIT_FLAG_PVP, 19, D10),
|
||||
|
||||
@@ -118,11 +118,12 @@ impl RingVariant {
|
||||
}
|
||||
}
|
||||
|
||||
/// The selector's branch (`0x605960`) on the raw reaction rank `0..=7`. A player never grays: rank
|
||||
/// 0-1 reads red, standing in for the reference's attackability matrix, else green when PvP-flagged
|
||||
/// and blue when not, paler for a party member; self is not in the party table. An NPC reads gray
|
||||
/// when dead, else the rank palette. The reference takes the player branch on `UNIT_FIELD_FLAGS`
|
||||
/// bit 3 (`0x605baa`), which a player's pet carries too; callers pass the object type.
|
||||
/// The selector's branch (`0x605960`) on [`ring_reaction`]'s rank `0..=6`. A player never grays:
|
||||
/// rank 0-1 reads red, standing in for the reference's attackability matrix, else green when
|
||||
/// PvP-flagged and blue when not, paler for a party member; self is not in the party table. An NPC
|
||||
/// reads gray when dead, else the rank palette. The reference takes the player branch on
|
||||
/// `UNIT_FIELD_FLAGS` bit 3 (`0x605baa`), which a player's pet carries too; callers pass the object
|
||||
/// type.
|
||||
pub(crate) fn ring_variant(
|
||||
rank: u8,
|
||||
is_player: bool,
|
||||
@@ -400,13 +401,14 @@ pub(super) fn push_ring(
|
||||
batch.tris();
|
||||
}
|
||||
|
||||
/// The target's reaction toward our player as a raw rank `0..=7`, the direction every NPC colour
|
||||
/// The target's reaction toward our player as a rank `0..=6`, the direction every NPC colour
|
||||
/// uses (`0x605960` and `0x7cbaa0` ask `UnitReaction 0x6061e0` with the unit as `this`). When
|
||||
/// both carry `UNIT_FIELD_FLAGS` bit 3, a duel (`0x606296`) or mutual FFA decides first. Then
|
||||
/// `0x606530`: a faction with a reputation slot (`0x605fc0`) answers with our reputation rank
|
||||
/// (`0x4d63a0`), even in GM mode; any other goes to the template comparator (`0x606640`). Neutral
|
||||
/// when anything is missing. Not applied: the party rung (`0x6062b0`), the contested guard,
|
||||
/// forced reactions and the summon tail.
|
||||
/// when anything is missing. `0x606439` caps that answer at 6, so Revered and Exalted both read 6.
|
||||
/// Not applied: the party rung (`0x6062b0`), the contested guard, forced reactions and the summon
|
||||
/// tail.
|
||||
pub(crate) fn ring_reaction(
|
||||
factions: Option<&Factions>,
|
||||
reputations: &Reputations,
|
||||
@@ -439,9 +441,13 @@ pub(crate) fn ring_reaction(
|
||||
let self_tpl = catalog.template(self_store.0.unit_faction_template()?)?;
|
||||
Some(target_tpl.reaction_toward(self_tpl) as u8)
|
||||
})();
|
||||
resolved.unwrap_or(Reaction::Neutral as u8)
|
||||
// `0x606439` (`cmp esi,6` / `jl` / `mov esi,6`) caps `0x606530`'s answer: Exalted's 7 reads 6.
|
||||
resolved.map_or(Reaction::Neutral as u8, |rank| rank.min(REACTION_CAP))
|
||||
}
|
||||
|
||||
/// Revered, the highest rank `UnitReaction`'s generic leg answers (`0x606439`).
|
||||
const REACTION_CAP: u8 = 6;
|
||||
|
||||
/// `UNIT_FIELD_FLAGS` bit 3, player-controlled in behaviour: players and their pets carry it, wild
|
||||
/// creatures do not. `UnitReaction` needs it on both sides before any player-vs-player rung
|
||||
/// (`0x606217`, `0x60622f`), and `CanAttack` picks its reaction arm by it (`0x606a13`).
|
||||
@@ -858,6 +864,49 @@ mod tests {
|
||||
assert_eq!(rank(&unit(12), &quiet), 4);
|
||||
}
|
||||
|
||||
/// The reputation leg walks the ladder up to Revered and stops: `0x606439` caps the rank at 6,
|
||||
/// so a Stormwind unit reads the same to a Revered and an Exalted human, while the ladder
|
||||
/// itself, which the reputation pane reads, still reaches 7.
|
||||
#[test]
|
||||
fn the_reputation_rank_is_capped_at_revered() {
|
||||
use crate::net::ObjectStore;
|
||||
use crate::target::{stormwind_fixture, HUMAN_WARRIOR};
|
||||
use benilla_protocol::field::FIELD_UNIT_FACTIONTEMPLATE;
|
||||
use benilla_protocol::ObjectFields;
|
||||
|
||||
/// `UNIT_FIELD_BYTES_0`, absolute descriptor index.
|
||||
const BYTES_0: u16 = 36;
|
||||
let data = benilla_formats::wow_data_or_skip!();
|
||||
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
|
||||
let me = ObjectStore(ObjectFields::from_pairs(&[(BYTES_0, HUMAN_WARRIOR)]));
|
||||
// Both sides of the Honored, Revered and Exalted thresholds, and the wire's own ceiling.
|
||||
for (total, want) in [
|
||||
(8_999, 4),
|
||||
(9_000, 5),
|
||||
(20_999, 5),
|
||||
(21_000, 6),
|
||||
(41_999, 6),
|
||||
(42_000, 6),
|
||||
(42_999, 6),
|
||||
] {
|
||||
let (factions, template, reps) = stormwind_fixture(&mut chain, total);
|
||||
let unit = ObjectStore(ObjectFields::from_pairs(&[(
|
||||
FIELD_UNIT_FACTIONTEMPLATE,
|
||||
template,
|
||||
)]));
|
||||
assert_eq!(
|
||||
ring_reaction(Some(&factions), &reps, Some(&unit), Some(&me)),
|
||||
want,
|
||||
"standing {total}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
benilla_formats::reputation_rank(42_000),
|
||||
7,
|
||||
"the ladder is not capped"
|
||||
);
|
||||
}
|
||||
|
||||
/// Cenarion Circle, faction 609 with reputation slot 36: not at war, its NPCs are neither
|
||||
/// attackable nor, without a service bit, interactable, while the rank stays neutral.
|
||||
#[test]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! The wire standing excludes the race/class base, so the pane's total is base plus wire: vmangos
|
||||
//! stores `standing - BaseRep` (`ReputationMgr.cpp:261`) and reports the sum
|
||||
//! (`ReputationMgr.cpp:82`). The rank is [`benilla_formats::reputation_rank`], shared with the
|
||||
//! unit-reaction decode so the pane and the nameplate agree.
|
||||
//! unit-reaction decode, which caps it at 6, Revered (`0x606439`); the pane shows it uncapped.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
|
||||
@@ -834,6 +834,22 @@ fn pvp_icon_follows_the_three_branch_law() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The target frame's name plate tint, `TargetFrameNameBackground`'s vertex colour as drawn.
|
||||
fn plate_color(s: &mut UiScript) -> [f32; 4] {
|
||||
s.resolve();
|
||||
s.extract()
|
||||
.into_iter()
|
||||
.find_map(|q| match q.content {
|
||||
QuadContent::Texture {
|
||||
path: Some(p),
|
||||
color: Some(c),
|
||||
..
|
||||
} if p.contains("LevelBackground") => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
.expect("target name-plate quad present")
|
||||
}
|
||||
|
||||
/// A friendly player's plate is green when PvP-flagged, else blue (`TargetFrame.lua:163-172`).
|
||||
#[test]
|
||||
fn flagged_friendly_player_plate_is_green() {
|
||||
@@ -842,20 +858,6 @@ fn flagged_friendly_player_plate_is_green() {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_unit_frames(&s);
|
||||
|
||||
let plate_color = |s: &mut UiScript| -> [f32; 4] {
|
||||
s.resolve();
|
||||
s.extract()
|
||||
.into_iter()
|
||||
.find_map(|q| match q.content {
|
||||
QuadContent::Texture {
|
||||
path: Some(p),
|
||||
color: Some(c),
|
||||
..
|
||||
} if p.contains("LevelBackground") => Some(c),
|
||||
_ => None,
|
||||
})
|
||||
.expect("target name-plate quad present")
|
||||
};
|
||||
let friendly_player = |pvp: bool| UnitState {
|
||||
exists: true,
|
||||
name: Some("Guildmate".into()),
|
||||
@@ -889,6 +891,65 @@ fn flagged_friendly_player_plate_is_green() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// An NPC of a faction we are Exalted with reads as Revered: `0x606439` caps the rank at 6 and the
|
||||
/// binding adds one (`0x51683e`), so `UnitReaction` answers 7 and stock `TargetFrame_CheckFaction`
|
||||
/// finds the last of `UnitReactionColor`'s seven entries (`TargetFrame.lua:6-14`, `:183`).
|
||||
#[test]
|
||||
fn an_exalted_npc_target_reads_revered_and_its_plate_is_green() {
|
||||
use crate::net::ObjectStore;
|
||||
use crate::target::{stormwind_fixture, HUMAN_WARRIOR};
|
||||
use benilla_protocol::field::FIELD_UNIT_FACTIONTEMPLATE;
|
||||
use benilla_protocol::ObjectFields;
|
||||
|
||||
/// `UNIT_FIELD_BYTES_0`, absolute descriptor index.
|
||||
const BYTES_0: u16 = 36;
|
||||
let data = benilla_formats::wow_data_or_skip!();
|
||||
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_unit_frames(&s);
|
||||
let me = ObjectStore(ObjectFields::from_pairs(&[(BYTES_0, HUMAN_WARRIOR)]));
|
||||
|
||||
for total in [21_000, 42_000] {
|
||||
let (factions, template, reps) = stormwind_fixture(&mut chain, total);
|
||||
let guard = ObjectStore(ObjectFields::from_pairs(&[(
|
||||
FIELD_UNIT_FACTIONTEMPLATE,
|
||||
template,
|
||||
)]));
|
||||
let reaction = crate::ui_unit::unit_reaction(Some(&factions), &reps, &guard, Some(&me));
|
||||
s.set_unit(
|
||||
"target",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
name: Some("Stormwind Guard".into()),
|
||||
health: 100,
|
||||
max_health: 100,
|
||||
level: 55,
|
||||
reaction,
|
||||
is_connected: true,
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
||||
assert!(
|
||||
s.errors().is_empty(),
|
||||
"standing {total}: script errors: {:?}",
|
||||
s.errors()
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>(r#"return UnitReaction("target", "player")"#)
|
||||
.unwrap(),
|
||||
7,
|
||||
"standing {total}"
|
||||
);
|
||||
let green = plate_color(&mut s);
|
||||
assert!(
|
||||
green[0].abs() < 1e-6 && (green[1] - 1.0).abs() < 1e-6 && green[2].abs() < 1e-6,
|
||||
"standing {total}: the plate is UnitReactionColor[7], green, got {green:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stock `TargetFrame_CheckClassification` (`TargetFrame.lua:205-218`), asserted on the drawn
|
||||
/// quads: elite, rare-elite and world boss share the Elite art (1.12 ships no rare-elite border),
|
||||
/// and `UNIT_CLASSIFICATION_CHANGED` alone repaints it.
|
||||
|
||||
@@ -12,13 +12,11 @@ use benilla_ui::strings::Arg;
|
||||
use crate::items::Items;
|
||||
use crate::names::NameCache;
|
||||
use crate::net::{NetCommands, ObjectStore, Objects, SelfPlayer};
|
||||
use crate::target::{
|
||||
go_is_nearest, ring_reaction, Hovered, HoveredObject, GO_FLAG_LOCKED, GO_TYPE_GENERIC,
|
||||
};
|
||||
use crate::target::{go_is_nearest, Hovered, HoveredObject, GO_FLAG_LOCKED, GO_TYPE_GENERIC};
|
||||
use crate::ui_action::{PlayerActions, Spells};
|
||||
use crate::ui_script::UiFeed;
|
||||
use crate::ui_trainer::{TrainerFeed, TrainerTooltipSubjects};
|
||||
use crate::ui_unit::{enrich_unit, snapshot, UnitFeed};
|
||||
use crate::ui_unit::{enrich_unit, snapshot, unit_reaction, UnitFeed};
|
||||
|
||||
pub struct UiTooltipPlugin;
|
||||
|
||||
@@ -629,12 +627,7 @@ fn drive_mouseover_tooltip(
|
||||
let name = names
|
||||
.resolve_unit(guid, Some(store), &commands)
|
||||
.map(str::to_string);
|
||||
let reaction = ring_reaction(
|
||||
rx.factions.as_deref(),
|
||||
&rx.reputations,
|
||||
Some(store),
|
||||
self_store,
|
||||
) + 1;
|
||||
let reaction = unit_reaction(rx.factions.as_deref(), &rx.reputations, store, self_store);
|
||||
let mut s = snapshot(store, name, reaction, chr);
|
||||
enrich_unit(
|
||||
&mut s,
|
||||
|
||||
@@ -743,8 +743,20 @@ pub(crate) struct UnitStores<'w, 's> {
|
||||
edges: MessageReader<'w, 's, FieldChanged>,
|
||||
}
|
||||
|
||||
/// `UnitReaction(unit, "player")` (`0x5167e0`): [`ring_reaction`] plus one (`0x51683e`), so
|
||||
/// `1..=7`, Hated to Revered, and Exalted reads 7 too (`0x606439`). Stock `UnitReactionColor`
|
||||
/// has seven entries (`TargetFrame.lua:6-14`), one per value.
|
||||
pub(crate) fn unit_reaction(
|
||||
factions: Option<&Factions>,
|
||||
reputations: &Reputations,
|
||||
store: &ObjectStore,
|
||||
self_store: Option<&ObjectStore>,
|
||||
) -> u8 {
|
||||
ring_reaction(factions, reputations, Some(store), self_store) + 1
|
||||
}
|
||||
|
||||
/// Build a unit snapshot from a streamed descriptor, its cached name and its `UnitReaction`
|
||||
/// (`1..8`, or `0` where none is resolved, as for `"player"`); `classes` feeds the relic column.
|
||||
/// (`1..=7`, or `0` where none is resolved, as for `"player"`); `classes` feeds the relic column.
|
||||
pub(crate) fn snapshot(
|
||||
store: &ObjectStore,
|
||||
name: Option<String>,
|
||||
@@ -1193,13 +1205,12 @@ fn feed_units(
|
||||
let name = names
|
||||
.resolve_unit(guid, Some(store), &commands)
|
||||
.map(str::to_string);
|
||||
// `UnitReaction` (1..8) is the selection ring's 0..7 rank plus one.
|
||||
let reaction = ring_reaction(
|
||||
let reaction = unit_reaction(
|
||||
factions.as_deref(),
|
||||
&reputations,
|
||||
Some(store),
|
||||
store,
|
||||
self_pair.map(|(s, _)| s),
|
||||
) + 1;
|
||||
);
|
||||
let mut s = snapshot(store, name, reaction, chr);
|
||||
s.guid = guid;
|
||||
s.is_connected = true;
|
||||
@@ -1238,12 +1249,12 @@ fn feed_units(
|
||||
let name = names
|
||||
.resolve_unit(guid, Some(store), &commands)
|
||||
.map(str::to_string);
|
||||
let reaction = ring_reaction(
|
||||
let reaction = unit_reaction(
|
||||
factions.as_deref(),
|
||||
&reputations,
|
||||
Some(store),
|
||||
store,
|
||||
self_pair.map(|(s, _)| s),
|
||||
) + 1;
|
||||
);
|
||||
let mut s = snapshot(store, name, reaction, chr);
|
||||
s.guid = guid;
|
||||
s.is_connected = true;
|
||||
@@ -1306,12 +1317,12 @@ fn feed_units(
|
||||
let name = names
|
||||
.resolve_unit(guid, Some(store), &commands)
|
||||
.map(str::to_string);
|
||||
let reaction = ring_reaction(
|
||||
let reaction = unit_reaction(
|
||||
factions.as_deref(),
|
||||
&reputations,
|
||||
Some(store),
|
||||
store,
|
||||
self_pair.map(|(s, _)| s),
|
||||
) + 1;
|
||||
);
|
||||
let mut s = snapshot(store, name, reaction, chr);
|
||||
s.guid = guid;
|
||||
s.is_connected = true;
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct FactionTemplate {
|
||||
}
|
||||
|
||||
/// A unit's base reaction toward another on the reference's scale: `0x606640` returns these three,
|
||||
/// and the reputation path widens the scale to 0..7, hence the gaps.
|
||||
/// and the reputation path widens the scale to 0..=6, hence the gaps.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Reaction {
|
||||
Hostile = 1,
|
||||
@@ -154,9 +154,9 @@ impl FactionInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Total standing (base + wire) to rank 0..=7, hated to exalted, which is also the reference's
|
||||
/// extended reaction scale. Widths from −42000: 36000, 3000, 3000, 3000, 6000, 12000, 21000, 1000
|
||||
/// (vmangos `PointsInRank`).
|
||||
/// Total standing (base + wire) to rank 0..=7, hated to exalted, the reference's ladder
|
||||
/// (`0x4d63a0`); the unit reaction takes it capped at 6, Revered (`0x606439`). Widths from −42000:
|
||||
/// 36000, 3000, 3000, 3000, 6000, 12000, 21000, 1000 (vmangos `PointsInRank`).
|
||||
pub fn reputation_rank(total_standing: i32) -> u8 {
|
||||
match total_standing {
|
||||
i32::MIN..=-6001 => 0, // hated
|
||||
|
||||
@@ -91,8 +91,9 @@ pub struct UnitState {
|
||||
pub charmed: bool,
|
||||
/// `UnitIsGhost`: `PLAYER_FLAGS` bit `0x10`, so players only.
|
||||
pub ghost: bool,
|
||||
/// The reaction toward the player on `UnitReaction`'s scale, 1 hated to 8 exalted, 0 unknown
|
||||
/// (nil). Fed for the tokens naming another unit; `"player"` and `"pet"` stay 0.
|
||||
/// The reaction toward the player on `UnitReaction`'s scale, 1 hated to 7 revered, which
|
||||
/// Exalted reads too (`0x606439` caps the rank), 0 unknown (nil). Fed for the tokens naming
|
||||
/// another unit; `"player"` and `"pet"` stay 0.
|
||||
pub reaction: u8,
|
||||
/// `UnitRace`'s localized first return, from `UNIT_FIELD_BYTES_0` byte 0; `None` answers
|
||||
/// `nil, nil`.
|
||||
|
||||
Reference in New Issue
Block a user