diff --git a/crates/benilla-app/src/ui_merchant/mod.rs b/crates/benilla-app/src/ui_merchant/mod.rs index fa8207fe..380b73e3 100644 --- a/crates/benilla-app/src/ui_merchant/mod.rs +++ b/crates/benilla-app/src/ui_merchant/mod.rs @@ -1082,6 +1082,84 @@ mod tests { assert_eq!(got, i64::from(want), "the undiscounted total is {full}"); } + /// `RepairAllItems()` through the feed and the drain, at [`purse_fixture`]'s vendor carrying + /// `npc_flags`, with the sword at `slot_field` and `money` copper: the packets, the + /// `UI_ERROR_MESSAGE` lines and the message rows queued for their sound. + fn repair_all( + npc_flags: u32, + slot_field: u16, + money: u32, + ) -> (Vec, Vec, Vec<&'static str>) { + use benilla_protocol::field::FIELD_UNIT_NPC_FLAGS; + use benilla_protocol::ObjectFields; + use bevy::ecs::system::RunSystemOnce; + use purse_fixture::*; + + let (tx, rx) = crossbeam_channel::unbounded(); + let mut app = App::new(); + app.init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .insert_resource(NetCommands(tx)); + seat(app.world_mut()); + let mut units = app.world_mut().query::<(&Guid, &mut ObjectStore)>(); + for (guid, mut store) in units.iter_mut(app.world_mut()) { + if guid.0 == VENDOR { + let flags = ObjectFields::from_pairs(&[(FIELD_UNIT_NPC_FLAGS, npc_flags)]); + store.0.merge(flags); + } + } + app.world_mut() + .spawn((SelfPlayer, Guid(0x5e1f), player(slot_field, money))); + let script = UiScript::new().unwrap(); + record_errors(&script); + app.insert_non_send_resource(script); + app.world_mut().run_system_once(feed_merchant).unwrap(); + while rx.try_recv().is_ok() {} // the feed's name query for the vendor + app.world() + .non_send_resource::() + .run("RepairAllItems()") + .unwrap(); + app.world_mut().run_system_once(drain_merchant).unwrap(); + let sent = rx.try_iter().collect(); + let lines = shown(app.world().non_send_resource::()); + (sent, lines, sounded(app.world())) + } + + /// At a vendor that does not repair, `RepairAllItems` sends nothing and says nothing: it + /// returns (`0x4fbff0`) unless `0x4fadb0` finds the repair flag on the open merchant, before + /// its purse test. The same worn sword at a repairer goes out. + #[test] + fn repair_all_does_nothing_at_a_vendor_that_does_not_repair() { + use crate::target::cursor_mode::npc_flags::VENDOR as NPC_FLAG_VENDOR; + use benilla_protocol::field::FIELD_PLAYER_INV_SLOT_HEAD; + use purse_fixture::*; + + let (_, discounted) = sword_prices(); + let main_hand = FIELD_PLAYER_INV_SLOT_HEAD + 2 * 15; + for money in [discounted, discounted - 1] { + let (sent, lines, rows) = repair_all(NPC_FLAG_VENDOR, main_hand, money); + assert!(sent.is_empty(), "{money} copper: no repair goes out"); + assert!( + lines.is_empty() && rows.is_empty(), + "{money} copper: no purse refusal" + ); + } + let (sent, ..) = repair_all(NPC_FLAG_VENDOR | NPC_FLAG_REPAIR, main_hand, discounted); + assert!(matches!( + sent.as_slice(), + [ClientCommand::RepairItem { + vendor: VENDOR, + item_guid: 0 + }] + )); + } + /// `RepairAllItems` prices only the equipped slots for its purse test (`0x4fc026`–`0x4fc063`, /// `0x4fc08e`): the sword worn and over the purse is refused with `ERR_NOT_ENOUGH_MONEY` and /// no send; the same sword in the backpack, which `GetRepairAllCost` would count, leaves a worn @@ -1089,35 +1167,13 @@ mod tests { #[test] fn repair_all_tests_the_purse_against_the_worn_gear_alone() { use benilla_protocol::field::FIELD_PLAYER_INV_SLOT_HEAD; - use bevy::ecs::system::RunSystemOnce; use purse_fixture::*; let (_, discounted) = sword_prices(); - let repair_all = |slot_field: u16| { - let (tx, rx) = crossbeam_channel::unbounded(); - let mut app = App::new(); - app.init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .insert_resource(NetCommands(tx)); - seat(app.world_mut()); - app.world_mut() - .spawn((SelfPlayer, Guid(0x5e1f), player(slot_field, discounted - 1))); - let script = UiScript::new().unwrap(); - record_errors(&script); - script.run("RepairAllItems()").unwrap(); - app.insert_non_send_resource(script); - app.world_mut().run_system_once(drain_merchant).unwrap(); - let sent: Vec = rx.try_iter().collect(); - let lines = shown(app.world().non_send_resource::()); - (sent, lines, sounded(app.world())) - }; // The main hand (slot 15). - let (sent, lines, rows) = repair_all(FIELD_PLAYER_INV_SLOT_HEAD + 2 * 15); + let main_hand = FIELD_PLAYER_INV_SLOT_HEAD + 2 * 15; + let (sent, lines, rows) = repair_all(NPC_FLAG_REPAIR, main_hand, discounted - 1); assert!(sent.is_empty(), "worn, one copper short: nothing goes out"); assert_eq!(lines, [NOT_ENOUGH_MONEY]); assert_eq!( @@ -1127,7 +1183,8 @@ mod tests { ); // Backpack slot 1, inventory slot 23. - let (sent, lines, rows) = repair_all(FIELD_PLAYER_INV_SLOT_HEAD + 2 * 23); + let backpack_1 = FIELD_PLAYER_INV_SLOT_HEAD + 2 * 23; + let (sent, lines, rows) = repair_all(NPC_FLAG_REPAIR, backpack_1, discounted - 1); assert!( matches!( sent.as_slice(), diff --git a/crates/benilla-ui/src/script/merchant.rs b/crates/benilla-ui/src/script/merchant.rs index f4569788..3ac0d1f8 100644 --- a/crates/benilla-ui/src/script/merchant.rs +++ b/crates/benilla-ui/src/script/merchant.rs @@ -431,7 +431,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { "RepairAllItems", lua.create_function(|lua, ()| { let mut model = lua.app_data_mut::().expect("model app_data"); - model.repair_all = true; + // `0x4fbff0`: nothing, not even the purse test, unless the open merchant repairs + // (`0x4fadb0`, `UNIT_NPC_FLAGS` bit 14). + if model.merchant.as_ref().is_some_and(|m| m.can_repair) { + model.repair_all = true; + } Ok(()) })?, )?; @@ -745,6 +749,19 @@ mod tests { ); } + /// `RepairAllItems` returns before anything else unless the open merchant repairs + /// (`0x4fadb0`, tested at `0x4fbfe9`–`0x4fbff0`): no vendor, or one without the repair flag, + /// queues nothing. + #[test] + fn repair_all_queues_nothing_away_from_a_repairer() { + let mut s = UiScript::new().unwrap(); + s.run("RepairAllItems()").unwrap(); + assert!(!s.take_repair_all(), "no vendor"); + s.set_merchant(Some(stock())); + s.run("RepairAllItems()").unwrap(); + assert!(!s.take_repair_all(), "a vendor that does not repair"); + } + #[test] fn repair_reads_intents_and_mode_latch() { let mut s = UiScript::new().unwrap();