Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions HeavenClient/IO/UITypes/UIBuffList.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,18 @@ namespace ms
void UIBuffList::rightclick(Point<int16_t> cursorpos)
{
// FUN_0045b9fc. The icon goes when the server cancels the buff.
// Dragon Roar's can't be cancelled. Item buffs would send
// CANCEL_ITEM_EFFECT, which the server doesn't handle.
// Dragon Roar's can't be cancelled. An item buff (negative id) sends
// CANCEL_ITEM_EFFECT; v83 instead refuses a morph item's while
// morphed (notice 0x10fc), which this client doesn't track.
constexpr int32_t DRAGON_ROAR = 1311006;

if (auto entry = entry_at(cursorpos))
if (entry->id >= 0 && entry->id != DRAGON_ROAR)
{
if (entry->id < 0)
CancelItemEffectPacket(entry->id).dispatch();
else if (entry->id != DRAGON_ROAR)
CancelBuffPacket(entry->id).dispatch();
}
}

UIElement::Type UIBuffList::get_type() const
Expand Down
1 change: 1 addition & 0 deletions HeavenClient/Net/OutPacket.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ namespace ms
SORT_ITEMS = 70,
MOVE_ITEM = 71,
USE_ITEM = 72,
CANCEL_ITEM_EFFECT = 73,
USE_SKILL_BOOK = 82,
MESO_DROP = 94,
HEAL_OVER_TIME = 89,
Expand Down
12 changes: 12 additions & 0 deletions HeavenClient/Net/Packets/AttackAndSkillPackets.h
Original file line number Diff line number Diff line change
Expand Up @@ -387,4 +387,16 @@ namespace ms
write_int(skillid);
}
};

// Ends one of the player's item buffs, as right-clicking its icon does
// (FUN_00a096af). The id is the buff's, minus the item id.
// Opcode: CANCEL_ITEM_EFFECT(73)
class CancelItemEffectPacket : public OutPacket
{
public:
CancelItemEffectPacket(int32_t buffid) : OutPacket(OutPacket::Opcode::CANCEL_ITEM_EFFECT)
{
write_int(buffid);
}
};
}
54 changes: 54 additions & 0 deletions game-data/src/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! mapped and each lookup is a few binary searches.

use crate::{GameDataError, NxDataFile};
use game::buff::BuffStat;
use std::path::Path;

/// Stack size when `info/slotMax` is absent (v83: 1 for equipment, else 100).
Expand Down Expand Up @@ -52,8 +53,37 @@ pub struct ConsumeSpec {
/// Percent of max HP/MP.
pub hp_r: i32,
pub mp_r: i32,
/// How long `stats` last, in ms (`time`).
pub time: i32,
/// The stats it raises for `time` (`pad`, `pdd`, `mad`, `mdd`, `acc`,
/// `eva`, `speed`, `jump`).
pub stats: Vec<(BuffStat, i16)>,
/// The diseases it cures (`poison`, `darkness`, `weakness`, `seal`,
/// `curse`).
pub cures: Vec<BuffStat>,
}

/// The `spec` keys of the stats a consumable raises.
const SPEC_STATS: [(&str, BuffStat); 8] = [
("pad", BuffStat::Pad),
("pdd", BuffStat::Pdd),
("mad", BuffStat::Mad),
("mdd", BuffStat::Mdd),
("acc", BuffStat::Acc),
("eva", BuffStat::Eva),
("speed", BuffStat::Speed),
("jump", BuffStat::Jump),
];

/// The `spec` keys of the diseases a consumable cures.
const SPEC_CURES: [(&str, BuffStat); 5] = [
("poison", BuffStat::Poison),
("darkness", BuffStat::Darkness),
("weakness", BuffStat::Weakness),
("seal", BuffStat::Seal),
("curse", BuffStat::Curse),
];

/// An equipment template (`Character.nx/<category>/<id>.img/info`).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct EquipInfo {
Expand Down Expand Up @@ -201,6 +231,23 @@ impl ItemData {
mp: nx.int_child(spec, "mp")?.unwrap_or(0),
hp_r: nx.int_child(spec, "hpR")?.unwrap_or(0),
mp_r: nx.int_child(spec, "mpR")?.unwrap_or(0),
time: nx.int_child(spec, "time")?.unwrap_or(0),
stats: SPEC_STATS
.iter()
.filter_map(|&(key, stat)| match nx.int_child(spec, key) {
Ok(Some(0)) | Ok(None) => None,
Ok(Some(value)) => Some(Ok((stat, value as i16))),
Err(error) => Some(Err(error)),
})
.collect::<Result<_, _>>()?,
cures: SPEC_CURES
.iter()
.filter_map(|&(key, stat)| match nx.int_child(spec, key) {
Ok(Some(0)) | Ok(None) => None,
Ok(Some(_)) => Some(Ok(stat)),
Err(error) => Some(Err(error)),
})
.collect::<Result<_, _>>()?,
});
}
Ok(Some(info))
Expand Down Expand Up @@ -312,6 +359,13 @@ mod tests {
(subi.price, subi.unit_price, subi.slot_max),
(250, 0.3, 500)
);
let all_cure = data.info(2050004).unwrap().unwrap().spec.unwrap();
assert_eq!(all_cure.cures.len(), 5);
let warrior_pill = data.info(2002004).unwrap().unwrap().spec.unwrap();
assert_eq!(
(warrior_pill.stats, warrior_pill.time),
(vec![(BuffStat::Pad, 5)], 180_000)
);
let relaxer = data.info(3010000).unwrap().unwrap();
assert_eq!((relaxer.recovery_hp, relaxer.recovery_mp), (50, 0));
}
Expand Down
9 changes: 9 additions & 0 deletions integration-harness/src/hunting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ pub fn cancel_buff(skill_id: i32) -> Packet {
p
}

/// CANCEL_ITEM_EFFECT: right-clicking an item buff's icon (its id is the
/// buff's, `-item_id`).
pub fn cancel_item_effect(buff_id: i32) -> Packet {
let mut p = Packet::new_empty();
p.write_short(RecvOpcode::CancelItemEffect as i16).unwrap();
p.write_int(buff_id).unwrap();
p
}

/// MOVE_PLAYER: one absolute movement to (x, y), standing.
pub fn walk_to(x: i16, y: i16) -> Packet {
let mut movement = vec![1u8, 0];
Expand Down
48 changes: 48 additions & 0 deletions integration-harness/tests/item_buffs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! A pill raises a stat for its `time` as a buff of id `-item_id`, which
//! right-clicking its icon (CANCEL_ITEM_EFFECT) ends; All Cure is spent
//! with nothing to cure.

use integration_harness::database::grant_stack;
use integration_harness::hunting::{cancel_item_effect, use_item};
use integration_harness::preconditions::load_harness_config_or_fail;
use integration_harness::trace::step;
use integration_harness::{login_to_world_session, relogin};
use tokio::time::Duration;

const USE: i16 = 2;
/// Warrior Pill: +5 weapon attack for three minutes.
const WARRIOR_PILL: i32 = 2_002_004;
const ALL_CURE: i32 = 2_050_004;
/// Weapon attack's CANCEL_BUFF bit (second mask).
const PAD_MASK: i64 = 0x1_0000_0000;

#[tokio::test]
async fn a_pill_buffs_until_its_icon_is_right_clicked() {
let config = load_harness_config_or_fail().await;
let id = login_to_world_session(&config)
.await
.expect("first login failed")
.character_id;
drop(relogin(&config).await);
tokio::time::sleep(Duration::from_millis(500)).await;
grant_stack(id, USE, 20, WARRIOR_PILL, 2)
.await
.expect("grant pills");
grant_stack(id, USE, 21, ALL_CURE, 1)
.await
.expect("grant All Cure");
let mut player = relogin(&config).await;
let c = &mut player.connection;

let t = step(c, "pill", use_item(20, WARRIOR_PILL)).await;
assert_eq!(t.buffs, [(-WARRIOR_PILL, 5, 180_000)]);

let t = step(c, "cancel", cancel_item_effect(-WARRIOR_PILL)).await;
assert_eq!(t.cancelled_buffs, [PAD_MASK]);
let t = step(c, "cancel again", cancel_item_effect(-WARRIOR_PILL)).await;
assert!(t.cancelled_buffs.is_empty());

let t = step(c, "all cure", use_item(21, ALL_CURE)).await;
assert!(t.buffs.is_empty() && t.cancelled_buffs.is_empty());
assert_eq!(t.removed, [(USE as u8, 21)]);
}
3 changes: 2 additions & 1 deletion net/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ fn world_routes() -> &'static HashMap<i16, WorldRoute> {
(Op::SpendSp, InGame(&world::SpendSpHandler)),
(Op::UseSkill, InGame(&world::UseSkillHandler)),
(Op::CancelBuff, InGame(&world::CancelBuffHandler)),
(Op::CancelItemEffect, InGame(&world::CancelBuffHandler)),
(Op::SkillEffect, InGame(&world::SkillEffectHandler)),
(Op::ThrowGrenade, InGame(&world::ThrowGrenadeHandler)),
(Op::MesoDrop, InGame(&world::MesoDropHandler)),
Expand Down Expand Up @@ -617,7 +618,7 @@ mod tests {
fn every_registered_opcode_is_unique_per_server() {
// The maps would silently drop duplicates; check the table sizes match.
assert_eq!(login_routes().len(), 13);
assert_eq!(world_routes().len(), 55);
assert_eq!(world_routes().len(), 56);
}

#[test]
Expand Down
Loading
Loading