diff --git a/crates/benilla-app/src/ui_merchant/mod.rs b/crates/benilla-app/src/ui_merchant/mod.rs index c097a34d..5348a17a 100644 --- a/crates/benilla-app/src/ui_merchant/mod.rs +++ b/crates/benilla-app/src/ui_merchant/mod.rs @@ -1,6 +1,6 @@ //! The merchant window's app side: [`MerchantOpen`] holds the `SMSG_LIST_INVENTORY` rows, -//! [`feed_merchant`] pushes them with the buyback and repair rows, the purse and the refusals, and -//! [`drain_merchant`] sends the Lua intents. A bag click sells through [`crate::ui_items`]. +//! [`feed_merchant`] pushes them with the buyback and repair rows, the purse and the refusals, +//! [`feed_repair_all_cost`] the repair-all total, and [`drain_merchant`] sends the Lua intents. A bag click sells through [`crate::ui_items`]. use benilla_protocol::messages::{buy_result, sell_result, VendorItem}; use bevy::prelude::*; @@ -111,6 +111,11 @@ impl Plugin for UiMerchantPlugin { // player-requirement stores for `GetMerchantItemInfo`'s `isUsable`. close_npc_session_out_of_range::.before(feed_merchant), feed_merchant.after(crate::ui_unit::UnitFeed).in_set(UiFeed), + // Before `feed_char`: the `UNIT_INVENTORY_CHANGED` a repair fires refreshes the + // Repair All button from this total (`PaperDollFrame.lua:717-724`). + feed_repair_all_cost + .in_set(UiFeed) + .before(crate::ui_char::feed_char), drain_merchant.after(UiInput), ), ); @@ -332,6 +337,45 @@ fn repair_all_cost( total.min(u64::from(u32::MAX)) as u32 } +/// The open vendor's `UNIT_NPC_FLAGS`, 0 with none open or its unit not streamed. +fn vendor_npc_flags( + open: &MerchantOpen, + units: &Query<(&Guid, &ObjectStore), Without>, +) -> u32 { + open.vendor + .and_then(|g| units.iter().find(|(guid, _)| guid.0 == g)) + .map(|(_, store)| store.0.unit_npc_flags()) + .unwrap_or(0) +} + +/// Push `GetRepairAllCost`'s total, which the reference sweeps at the call (`0x4fbd60`): every +/// frame a repairing vendor is open, 0 otherwise. +fn feed_repair_all_cost( + script: Option>, + open: Res, + objects: Objects, + items: Res, + commands: Res, + self_q: Query<&ObjectStore, With>, + units: Query<(&Guid, &ObjectStore), Without>, + tables: Option>, + mut last: Local>>, +) { + let Some(mut script) = script else { + return; + }; + let last = last.get(&script); + let can_repair = vendor_npc_flags(&open, &units) & NPC_FLAG_REPAIR != 0; + let total = match (can_repair, self_q.iter().next(), tables.as_deref()) { + (true, Some(store), Some(t)) => repair_all_cost(&store.0, &objects, &items, t, &commands), + _ => 0, + }; + if *last != Some(total) { + script.set_repair_all_cost(total); + *last = Some(total); + } +} + fn snapshot( open: &MerchantOpen, objects: &Objects, @@ -340,7 +384,6 @@ fn snapshot( commands: &NetCommands, player: Option<&benilla_protocol::ObjectFields>, vendor_npc_flags: u32, - tables: Option<&RepairTables>, ) -> Option { open.vendor?; let buyback = player @@ -351,11 +394,6 @@ fn snapshot( .collect() }) .unwrap_or_default(); - let can_repair = vendor_npc_flags & NPC_FLAG_REPAIR != 0; - let repair_cost = match (can_repair, player, tables) { - (true, Some(store), Some(t)) => repair_all_cost(store, objects, items, t, commands), - _ => 0, - }; Some(MerchantState { items: open .items @@ -363,8 +401,7 @@ fn snapshot( .map(|it| resolve_item(it, items, icons, commands)) .collect(), buyback, - can_repair, - repair_all_cost: repair_cost, + can_repair: vendor_npc_flags & NPC_FLAG_REPAIR != 0, }) } @@ -378,8 +415,7 @@ fn feed_merchant( icons: Option>, commands: Res, self_q: Query<&ObjectStore, With>, - units: Query<(&crate::net::Guid, &ObjectStore), Without>, - tables: Option>, + units: Query<(&Guid, &ObjectStore), Without>, names: Res, mut errors: ResMut, mut last: Local>>, @@ -422,11 +458,6 @@ fn feed_merchant( } // The vendor's service bits gate the repair pair; the player's descriptor carries buyback. - let vendor_npc_flags = open - .vendor - .and_then(|g| units.iter().find(|(guid, _)| guid.0 == g)) - .map(|(_, store)| store.0.unit_npc_flags()) - .unwrap_or(0); let player = self_q.iter().next().map(|s| &s.0); let fresh = snapshot( &open, @@ -435,8 +466,7 @@ fn feed_merchant( icons.as_deref(), &commands, player, - vendor_npc_flags, - tables.as_deref(), + vendor_npc_flags(&open, &units), ); // The vendor's name rides `MERCHANT_SHOW` and `MERCHANT_UPDATE` as arg1, and its landing alone // re-fires `MERCHANT_UPDATE`; the reference fires both bare (`0x4fad92`, `0x4facaa`), and @@ -604,6 +634,18 @@ fn drain_merchant( mod tests { use super::*; + /// A repair's `UNIT_INVENTORY_CHANGED` reads this frame's total: the stock handler refreshes + /// the Repair All button from `GetRepairAllCost` (`PaperDollFrame.lua:717-724`). + #[test] + fn the_repair_all_total_is_swept_before_the_inventory_event() { + let mut app = crate::game_plugins::schedule_tests::headless_client(); + assert!(crate::test_support::runs_before( + &mut app, + feed_repair_all_cost, + crate::ui_char::feed_char + )); + } + fn row(entry: u32, slot: u32, count: u32) -> VendorItem { VendorItem { slot, diff --git a/crates/benilla-app/src/ui_script/merchant_tests.rs b/crates/benilla-app/src/ui_script/merchant_tests.rs index 994b919c..4275bd72 100644 --- a/crates/benilla-app/src/ui_script/merchant_tests.rs +++ b/crates/benilla-app/src/ui_script/merchant_tests.rs @@ -732,8 +732,8 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() { buyback_item("Cracked Sword", 20), // the latest sale: the merchant page's slot ], can_repair: true, - repair_all_cost: 76, })); + s.set_repair_all_cost(76); s.fire_event( "MERCHANT_SHOW", vec![ScriptValue::Str("Kurdram Stonehammer".into())], diff --git a/crates/benilla-ui/src/script/merchant.rs b/crates/benilla-ui/src/script/merchant.rs index d657af4a..0537d6e1 100644 --- a/crates/benilla-ui/src/script/merchant.rs +++ b/crates/benilla-ui/src/script/merchant.rs @@ -76,8 +76,6 @@ pub struct MerchantState { pub buyback: Vec, /// Whether this vendor repairs (the `UNIT_NPC_FLAGS` repair bit). pub can_repair: bool, - /// The repair-all cost in copper; 0 disables the button (`MerchantFrame_OnShow`). - pub repair_all_cost: u32, } impl super::UiScript { @@ -91,6 +89,11 @@ impl super::UiScript { } } + /// Push `GetRepairAllCost`'s total in copper, swept by the app ahead of the events that read it. + pub fn set_repair_all_cost(&mut self, copper: u32) { + self.model_mut().repair_all_cost = copper; + } + /// Drain the `(row, quantity)` buys `BuyMerchantItem` queued, the row 1-based. pub fn take_merchant_buys(&mut self) -> Vec<(u32, u32)> { std::mem::take(&mut self.model_mut().merchant_buys) @@ -446,13 +449,17 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { )?; // GetRepairAllCost() → cost, canRepair: whether there is damage to pay for, which enables the - // repair-all button (`MerchantFrame.lua:38`). + // repair-all button (`MerchantFrame.lua:38`); 0 away from a vendor that repairs (`0x4fbd60`). g.set( "GetRepairAllCost", lua.create_function(|lua, ()| { let cost = { let model = lua.app_data_ref::().expect("model app_data"); - model.merchant.as_ref().map_or(0, |m| m.repair_all_cost) + if model.merchant.as_ref().is_some_and(|m| m.can_repair) { + model.repair_all_cost + } else { + 0 + } }; Ok(MultiValue::from_vec(vec![ Value::Integer(i64::from(cost)), @@ -801,6 +808,22 @@ mod tests { assert!(s.take_merchant_buybacks().is_empty(), "drained"); } + /// Away from a vendor that repairs, the total reads 0, as `0x4fbd60` pushes there. + #[test] + fn the_repair_all_total_reads_zero_away_from_a_repairer() { + let mut s = UiScript::new().unwrap(); + s.set_repair_all_cost(1234); + assert!( + s.eval::("return GetRepairAllCost() == 0").unwrap(), + "no vendor" + ); + s.set_merchant(Some(stock())); + assert!( + s.eval::("return GetRepairAllCost() == 0").unwrap(), + "a vendor that does not repair" + ); + } + #[test] fn repair_reads_intents_and_mode_latch() { let mut s = UiScript::new().unwrap(); @@ -808,8 +831,8 @@ mod tests { let mut state = stock(); state.can_repair = true; - state.repair_all_cost = 1234; s.set_merchant(Some(state)); + s.set_repair_all_cost(1234); assert!(s.eval::("return CanMerchantRepair() == 1").unwrap()); assert!(s .eval::("local c, can = GetRepairAllCost()\nreturn c == 1234 and can == true",) diff --git a/crates/benilla-ui/src/script/model.rs b/crates/benilla-ui/src/script/model.rs index eaa49e8f..5a0eb9f6 100644 --- a/crates/benilla-ui/src/script/model.rs +++ b/crates/benilla-ui/src/script/model.rs @@ -592,6 +592,8 @@ pub(crate) struct Model { /// The open vendor's stock, the `BuyMerchantItem` calls and whether `CloseMerchant` ran. pub(crate) merchant: Option, + /// `GetRepairAllCost`'s total, which the app pushes every frame a vendor is open. + pub(crate) repair_all_cost: u32, pub(crate) merchant_buys: Vec<(u32, u32)>, /// The held `(bag, slot)` when `PickupMerchantItem` sells, sent as `CMSG_SELL_ITEM`. pub(crate) merchant_cursor_sells: Vec<(i64, u32)>, @@ -1223,6 +1225,7 @@ impl Model { gossip_close: false, gossip_quest_selects: Vec::new(), merchant: None, + repair_all_cost: 0, merchant_buys: Vec::new(), merchant_cursor_sells: Vec::new(), merchant_slot_buys: Vec::new(),