fix: send no repair-all at a vendor that does not repair (#561)
1.12.1's RepairAllItems (0x4fbfe0) calls 0x4fadb0, which resolves the open merchant's guid (0xbddfa0) to its unit and tests UNIT_NPC_FLAGS bit 14, and returns at 0x4fbff0 before the purse test when the bit is clear. The binding now queues the repair-all only when the merchant state's can_repair is set, the value CanMerchantRepair reads, so a macro or addon at an ordinary vendor sends no CMSG_REPAIR_ITEM and shows no "You don't have enough money.". Verified live on the local LyraCore server (macOS build): at Brother Danil (npc_flags 4) main sends 0x02a8 CMSG_REPAIR_ITEM and the server refuses it, or shows the purse refusal with an empty purse; with this change neither happens (out trace live to 30 s). At Dermot Johns (0x4004) the repair-all still goes out and repairs, and an empty purse still refuses. scripts/check.sh green (benilla, benilla-app, benilla-ui); the new tests fail with the check removed. 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
34b7c30af2
commit
5757ab2334
@@ -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<ClientCommand>, Vec<String>, 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::<Items>()
|
||||
.init_resource::<crate::net::GuidIndex>()
|
||||
.init_resource::<MerchantOpen>()
|
||||
.init_resource::<MerchantErrors>()
|
||||
.init_resource::<NameCache>()
|
||||
.init_resource::<crate::net::Reputations>()
|
||||
.init_resource::<crate::ui_chat::ChatLog>()
|
||||
.init_resource::<crate::sound::MessageSounds>()
|
||||
.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::<UiScript>()
|
||||
.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::<UiScript>());
|
||||
(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::<Items>()
|
||||
.init_resource::<crate::net::GuidIndex>()
|
||||
.init_resource::<MerchantOpen>()
|
||||
.init_resource::<crate::net::Reputations>()
|
||||
.init_resource::<crate::ui_chat::ChatLog>()
|
||||
.init_resource::<crate::sound::MessageSounds>()
|
||||
.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<ClientCommand> = rx.try_iter().collect();
|
||||
let lines = shown(app.world().non_send_resource::<UiScript>());
|
||||
(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(),
|
||||
|
||||
@@ -431,7 +431,11 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
"RepairAllItems",
|
||||
lua.create_function(|lua, ()| {
|
||||
let mut model = lua.app_data_mut::<Model>().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();
|
||||
|
||||
Reference in New Issue
Block a user