diff --git a/HeavenClient/Character/Inventory/Inventory.cpp b/HeavenClient/Character/Inventory/Inventory.cpp index 0b79e2072..6886bf225 100644 --- a/HeavenClient/Character/Inventory/Inventory.cpp +++ b/HeavenClient/Character/Inventory/Inventory.cpp @@ -321,6 +321,18 @@ namespace ms } } + int16_t Inventory::equip_position(int16_t slot, EquipSlot::Id part) const + { + return get_cash_id(InventoryType::Id::EQUIP, slot) ? part + CASH_EQUIP : part; + } + + int16_t Inventory::shown_equip(EquipSlot::Id part) const + { + int16_t cash = part + CASH_EQUIP; + + return inventories[InventoryType::Id::EQUIPPED].count(cash) ? cash : part; + } + int16_t Inventory::find_free_slot(InventoryType::Id type) const { int16_t counter = 1; diff --git a/HeavenClient/Character/Inventory/Inventory.h b/HeavenClient/Character/Inventory/Inventory.h index c32d20735..f20cb2da9 100644 --- a/HeavenClient/Character/Inventory/Inventory.h +++ b/HeavenClient/Character/Inventory/Inventory.h @@ -57,6 +57,9 @@ namespace ms ADDCOUNT }; + // How far above its slot a cash item is worn. + static constexpr int16_t CASH_EQUIP = 100; + // Return the move type by value static Movement movementbyvalue(int8_t value); @@ -121,6 +124,15 @@ namespace ms // Find a free slot for the specified equip. EquipSlot::Id find_equipslot(int32_t itemid) const; + // The worn position for the equip in Equip tab `slot` at `part`. Like + // v83 (FUN_004f2cbb), an item with a Cash Shop serial is worn 100 + // above its slot, over whatever regular item is there. + int16_t equip_position(int16_t slot, EquipSlot::Id part) const; + + // The worn position whose item is shown at `part`: the cash item's + // if there is one (FUN_007fec81), else the regular one's. + int16_t shown_equip(EquipSlot::Id part) const; + // Find a free slot in the specified inventory. int16_t find_free_slot(InventoryType::Id type) const; diff --git a/HeavenClient/Character/Player.cpp b/HeavenClient/Character/Player.cpp index e2906a7f2..47e50f82c 100644 --- a/HeavenClient/Character/Player.cpp +++ b/HeavenClient/Character/Player.cpp @@ -193,10 +193,19 @@ namespace ms void Player::change_equip(int16_t slot) { - if (int32_t itemid = inventory.get_item_id(InventoryType::Id::EQUIPPED, slot)) + EquipSlot::Id part = EquipSlot::by_id(slot % Inventory::CASH_EQUIP); + + if (part == EquipSlot::Id::NONE) + return; + + // The look wears a cash item over the regular one, but has no cash + // weapons (v83 draws them over the regular weapon's stances). + int16_t shown = part == EquipSlot::Id::WEAPON ? part : inventory.shown_equip(part); + + if (int32_t itemid = inventory.get_item_id(InventoryType::Id::EQUIPPED, shown)) look.add_equip(itemid); else - look.remove_equip(EquipSlot::by_id(slot)); + look.remove_equip(part); } void Player::use_item(int32_t itemid) diff --git a/HeavenClient/IO/UI.cpp b/HeavenClient/IO/UI.cpp index eb590892b..b7e6a8763 100644 --- a/HeavenClient/IO/UI.cpp +++ b/HeavenClient/IO/UI.cpp @@ -129,7 +129,7 @@ namespace ms Music::set_bgmvolume(Music::configured_volume()); // v83 checks the balances on coming back to the Cash Shop. - if (get_element()) + if (auto cashshop = get_element(); cashshop && cashshop->request()) CheckCashPacket().dispatch(); } else { diff --git a/HeavenClient/IO/UIStateCashShop.cpp b/HeavenClient/IO/UIStateCashShop.cpp index 21656ad17..a3fbc0f47 100644 --- a/HeavenClient/IO/UIStateCashShop.cpp +++ b/HeavenClient/IO/UIStateCashShop.cpp @@ -90,26 +90,20 @@ namespace ms return focusedelement->send_cursor(clicked, cursorpos); } - else - { - focused = UIElement::NONE; - return cursorstate; - } + // A closed dialog lets the window under it have the cursor at + // once, or the first click after it would find no button hovered. + focused = UIElement::NONE; } - else + + if (auto front = get_front()) { - if (auto front = get_front()) - { - remove_cursor(front->get_type()); + remove_cursor(front->get_type()); - return front->send_cursor(clicked, cursorpos); - } - else - { - return Cursor::State::IDLE; - } + return front->send_cursor(clicked, cursorpos); } + + return Cursor::State::IDLE; } UIState::Iterator UIStateCashShop::pre_add(UIElement::Type type, bool toggled, bool is_focused) diff --git a/HeavenClient/IO/UITypes/UICashBuy.cpp b/HeavenClient/IO/UITypes/UICashBuy.cpp index 480359282..2cf87844b 100644 --- a/HeavenClient/IO/UITypes/UICashBuy.cpp +++ b/HeavenClient/IO/UITypes/UICashBuy.cpp @@ -16,8 +16,10 @@ // along with this program. If not, see . // ////////////////////////////////////////////////////////////////////////////////// #include "UICashBuy.h" +#include "UICashShop.h" #include "../KeyAction.h" +#include "../UI.h" #include "../Components/MapleButton.h" @@ -164,15 +166,19 @@ namespace ms void UICashBuy::buy() { CashType cash_type = PAYMENTS[payment]; + auto cashshop = UI::get().get_element(); - if (is_package(item_id)) - BuyCashPackagePacket(cash_type, sn).dispatch(); - else if (is_expansion(item_id) && sn == 0) - ExpandSlotsPacket(cash_type, expanded(item_id)).dispatch(); - else if (is_expansion(item_id)) - ExpandSlotsPacket(cash_type, expanded(item_id), sn).dispatch(); - else - BuyCashItemPacket(cash_type, sn).dispatch(); + if (cashshop && cashshop->request()) + { + if (is_package(item_id)) + BuyCashPackagePacket(cash_type, sn).dispatch(); + else if (is_expansion(item_id) && sn == 0) + ExpandSlotsPacket(cash_type, expanded(item_id)).dispatch(); + else if (is_expansion(item_id)) + ExpandSlotsPacket(cash_type, expanded(item_id), sn).dispatch(); + else + BuyCashItemPacket(cash_type, sn).dispatch(); + } deactivate(); } diff --git a/HeavenClient/IO/UITypes/UICashGift.cpp b/HeavenClient/IO/UITypes/UICashGift.cpp index 1008f52f9..6a875d9af 100644 --- a/HeavenClient/IO/UITypes/UICashGift.cpp +++ b/HeavenClient/IO/UITypes/UICashGift.cpp @@ -16,6 +16,7 @@ // along with this program. If not, see . // ////////////////////////////////////////////////////////////////////////////////// #include "UICashGift.h" +#include "UICashShop.h" #include "../KeyAction.h" #include "../UI.h" @@ -181,7 +182,9 @@ namespace ms if (asking_birthday) { // The server answers a wrong code with its own notice. - GiftCashItemPacket(birthday_code(birthday.get_text()), sn, to.get_text(), message.get_text()).dispatch(); + if (auto cashshop = UI::get().get_element(); cashshop && cashshop->request()) + GiftCashItemPacket(birthday_code(birthday.get_text()), sn, to.get_text(), message.get_text()).dispatch(); + close(); return; diff --git a/HeavenClient/IO/UITypes/UICashShop.cpp b/HeavenClient/IO/UITypes/UICashShop.cpp index a2930c5a5..7d7d91142 100644 --- a/HeavenClient/IO/UITypes/UICashShop.cpp +++ b/HeavenClient/IO/UITypes/UICashShop.cpp @@ -96,7 +96,7 @@ namespace ms } } - UICashShop::UICashShop() : tab(EVENT_TAB), subtab(0), page(0), preview_index(0), female(false), credit(0), points(0), prepaid(0), cash_row(0), item_tab(InventoryType::Id::EQUIP), item_row(0) + UICashShop::UICashShop() : tab(EVENT_TAB), subtab(0), page(0), preview_index(0), female(false), credit(0), points(0), prepaid(0), pending(false), cash_row(0), item_tab(InventoryType::Id::EQUIP), item_row(0) { nl::node CashShop = NxFiles::ui_classic["CashShop.img"]; nl::node Base = CashShop["Base"]; @@ -430,7 +430,8 @@ namespace ms return Button::State::NORMAL; } case Buttons::BT_CHECK: - CheckCashPacket().dispatch(); + if (request()) + CheckCashPacket().dispatch(); return Button::State::NORMAL; default: @@ -448,18 +449,21 @@ namespace ms { const Commodity& commodity = listed[index]; - if (remove) + if (pending) + return Button::State::NORMAL; + + if (remove && request()) SetWishListPacket(unwish(wish_list, commodity.sn)).dispatch(); else if (list_button % 3 == 0) UI::get().emplace(commodity, credit, points, prepaid); else if (list_button % 3 == 1) UI::get().emplace(commodity); - else if (list_button % 3 == 2) + else if (list_button % 3 == 2 && request()) SetWishListPacket(wish(wish_list, commodity.sn)).dispatch(); } } - if (buttonid >= Buttons::BT_EX_EQUIP && buttonid <= Buttons::BT_EX_TRUNK) + if (buttonid >= Buttons::BT_EX_EQUIP && buttonid <= Buttons::BT_EX_TRUNK && !pending) { // Four slots for 4,000 NX, shown as the expansion item: 9111000 // to 9114000 for the bags, 9110000 for the storage. @@ -585,7 +589,8 @@ namespace ms InventoryType::Id type = InventoryType::by_item_id(item.item_id); // The server picks another slot, or refuses, if this one is taken. - TakeOutCashItemPacket(item.cash_id, type, inventory.find_free_slot(type)).dispatch(); + if (request()) + TakeOutCashItemPacket(item.cash_id, type, inventory.find_free_slot(type)).dispatch(); return; } @@ -595,11 +600,26 @@ namespace ms { int16_t slot = static_cast(item_row * ITEM_COLUMNS + item_cell + 1); - if (int64_t cash_id = inventory.get_cash_id(item_tab, slot)) + if (int64_t cash_id = inventory.get_cash_id(item_tab, slot); cash_id && request()) PutBackCashItemPacket(cash_id, item_tab).dispatch(); } } + bool UICashShop::request() + { + if (pending) + return false; + + pending = true; + + return true; + } + + void UICashShop::answered() + { + pending = false; + } + int16_t UICashShop::grid_cell(Point cursor, Point grid, size_t columns, size_t rows) const { Point in_grid = cursor - grid; diff --git a/HeavenClient/IO/UITypes/UICashShop.h b/HeavenClient/IO/UITypes/UICashShop.h index e3a3f1799..bc2614606 100644 --- a/HeavenClient/IO/UITypes/UICashShop.h +++ b/HeavenClient/IO/UITypes/UICashShop.h @@ -76,6 +76,13 @@ namespace ms void remove_cash_item(int64_t cash_id); void set_wish_list(const std::vector& sns); + // v83 (CCashShop+0x18) has one request out at a time and ignores the + // buttons until its answer. Returns whether one may be sent, counting + // it as sent. + bool request(); + // A balance or an operation result came. + void answered(); + private: void change_tab(cash_catalog::Tab tab); void change_subtab(size_t subtab); @@ -165,6 +172,7 @@ namespace ms int32_t credit; int32_t points; int32_t prepaid; + bool pending; std::vector cash_items; Slider cash_slider; diff --git a/HeavenClient/IO/UITypes/UIEquipInventory.cpp b/HeavenClient/IO/UITypes/UIEquipInventory.cpp index 9fd6da444..93284e628 100644 --- a/HeavenClient/IO/UITypes/UIEquipInventory.cpp +++ b/HeavenClient/IO/UITypes/UIEquipInventory.cpp @@ -219,12 +219,14 @@ namespace ms void UIEquipInventory::update_slot(EquipSlot::Id slot) { - if (int32_t item_id = inventory.get_item_id(InventoryType::Id::EQUIPPED, slot)) + int16_t shown = inventory.shown_equip(slot); + + if (int32_t item_id = inventory.get_item_id(InventoryType::Id::EQUIPPED, shown)) { const Texture &texture = ItemData::get(item_id).get_icon(false); icons[slot] = std::make_unique( - std::make_unique(slot), + std::make_unique(shown), texture, -1 ); @@ -270,7 +272,7 @@ namespace ms return Cursor::State::GRABBING; } else { - show_equip(slot); + show_equip(inventory.shown_equip(slot)); return Cursor::State::CANGRAB; } @@ -299,7 +301,8 @@ namespace ms if (icons[slot]) if (int16_t freeslot = inventory.find_free_slot(InventoryType::Id::EQUIP)) - Stage::get().get_player().request(UnequipItemPacket(slot, freeslot), ExclRequest::WAIT); + Stage::get().get_player().request(UnequipItemPacket(inventory.shown_equip(slot), freeslot), + ExclRequest::WAIT); } bool UIEquipInventory::is_in_range(Point cursorpos) const @@ -334,8 +337,8 @@ namespace ms void UIEquipInventory::modify(int16_t pos, int8_t mode, int16_t arg) { - EquipSlot::Id eqpos = EquipSlot::by_id(pos); - EquipSlot::Id eqarg = EquipSlot::by_id(arg); + EquipSlot::Id eqpos = EquipSlot::by_id(pos % Inventory::CASH_EQUIP); + EquipSlot::Id eqarg = EquipSlot::by_id(arg % Inventory::CASH_EQUIP); switch (mode) { @@ -350,7 +353,7 @@ namespace ms } } - void UIEquipInventory::show_equip(EquipSlot::Id slot) + void UIEquipInventory::show_equip(int16_t slot) { UI::get().show_equip(Tooltip::Parent::EQUIPINVENTORY, slot); } @@ -386,7 +389,7 @@ namespace ms void UIEquipInventory::EquipIcon::drop_on_equips(EquipSlot::Id slot) const { - if (source == slot) + if (source % Inventory::CASH_EQUIP == slot) Sound(Sound::Name::DRAGEND).play(); } @@ -406,8 +409,11 @@ namespace ms if (equip) { - if (eqslot == source) - Stage::get().get_player().request(EquipItemPacket(slot, eqslot), ExclRequest::WAIT); + // Only an item worn at the same position swaps with this one. + int16_t worn = Stage::get().get_player().get_inventory().equip_position(slot, eqslot); + + if (worn == source) + Stage::get().get_player().request(EquipItemPacket(slot, worn), ExclRequest::WAIT); } else { Stage::get().get_player().request(UnequipItemPacket(source, slot), ExclRequest::WAIT); diff --git a/HeavenClient/IO/UITypes/UIEquipInventory.h b/HeavenClient/IO/UITypes/UIEquipInventory.h index 1dabfcae7..5b42ce749 100644 --- a/HeavenClient/IO/UITypes/UIEquipInventory.h +++ b/HeavenClient/IO/UITypes/UIEquipInventory.h @@ -54,7 +54,8 @@ namespace ms Button::State button_pressed(uint16_t buttonid) override; private: - void show_equip(EquipSlot::Id slot); + // Shows the tooltip for worn position `slot`. + void show_equip(int16_t slot); void clear_tooltip(); void load_icons(); @@ -66,6 +67,7 @@ namespace ms class EquipIcon : public Icon::Type { public: + // For the item at worn position `source`. EquipIcon(int16_t source); void drop_on_stage() const override; diff --git a/HeavenClient/IO/UITypes/UIItemInventory.cpp b/HeavenClient/IO/UITypes/UIItemInventory.cpp index 1f633eac3..12e4054db 100644 --- a/HeavenClient/IO/UITypes/UIItemInventory.cpp +++ b/HeavenClient/IO/UITypes/UIItemInventory.cpp @@ -676,7 +676,11 @@ namespace ms { case InventoryType::Id::EQUIP: if (can_wear_equip(slot)) - Stage::get().get_player().request(EquipItemPacket(slot, inventory.find_equipslot(item_id)), ExclRequest::WAIT); + { + int16_t worn = inventory.equip_position(slot, inventory.find_equipslot(item_id)); + + Stage::get().get_player().request(EquipItemPacket(slot, worn), ExclRequest::WAIT); + } break; case InventoryType::Id::USE: @@ -1269,8 +1273,12 @@ namespace ms { case InventoryType::Id::EQUIP: if (eqsource == eqslot) + { + int16_t worn = Stage::get().get_player().get_inventory().equip_position(source, eqslot); + if (parent.can_wear_equip(source)) - Stage::get().get_player().request(EquipItemPacket(source, eqslot), ExclRequest::WAIT); + Stage::get().get_player().request(EquipItemPacket(source, worn), ExclRequest::WAIT); + } Sound(Sound::Name::DRAGEND).play(); @@ -1300,13 +1308,15 @@ namespace ms const Inventory &inventory = player.get_inventory(); - // Also refuses an empty slot. - if (!scroll_rules::fits(item_id, inventory.get_item_id(InventoryType::Id::EQUIPPED, eqslot))) + // The scroll goes on the item shown there. Also refuses an empty slot. + int16_t target = inventory.shown_equip(eqslot); + + if (!scroll_rules::fits(item_id, inventory.get_item_id(InventoryType::Id::EQUIPPED, target))) break; - auto send = [slot = source, eqslot](bool whitescroll) + auto send = [slot = source, target](bool whitescroll) { - Stage::get().get_player().request(ScrollEquipPacket(slot, eqslot, whitescroll), ExclRequest::WAIT); + Stage::get().get_player().request(ScrollEquipPacket(slot, target, whitescroll), ExclRequest::WAIT); }; // Either answer scrolls; only Yes uses the White Scroll. diff --git a/HeavenClient/Net/Handlers/CashShopHandlers.cpp b/HeavenClient/Net/Handlers/CashShopHandlers.cpp index 5b74875f4..5cb0e79aa 100644 --- a/HeavenClient/Net/Handlers/CashShopHandlers.cpp +++ b/HeavenClient/Net/Handlers/CashShopHandlers.cpp @@ -113,7 +113,10 @@ namespace ms int32_t prepaid = recv.read_int(); if (auto cashshop = UI::get().get_element()) + { + cashshop->answered(); cashshop->set_balance(credit, points, prepaid); + } } namespace @@ -222,6 +225,8 @@ namespace ms if (!cashshop) return; + cashshop->answered(); + uint8_t mode = static_cast(recv.read_byte()); switch (mode) diff --git a/HeavenClient/Net/Handlers/Helpers/CharacterParser.cpp b/HeavenClient/Net/Handlers/Helpers/CharacterParser.cpp index d44ed8705..226470e5f 100644 --- a/HeavenClient/Net/Handlers/Helpers/CharacterParser.cpp +++ b/HeavenClient/Net/Handlers/Helpers/CharacterParser.cpp @@ -32,14 +32,16 @@ namespace ms recv.skip(8); + // Worn regular items, worn cash items, and the Equip tab. Like v83, the + // cash items are kept 100 above their slot. for (size_t i = 0; i < 3; i++) { - InventoryType::Id inv = (i == 0) ? InventoryType::EQUIPPED : InventoryType::EQUIP; + InventoryType::Id inv = (i < 2) ? InventoryType::EQUIPPED : InventoryType::EQUIP; int16_t pos = recv.read_short(); while (pos != 0) { - int16_t slot = (i == 1) ? -pos : pos; + int16_t slot = (i == 1) ? pos + Inventory::CASH_EQUIP : pos; ItemParser::parse_item(recv, inv, slot, invent); pos = recv.read_short(); } diff --git a/HeavenClient/Net/Handlers/SetFieldHandlers.cpp b/HeavenClient/Net/Handlers/SetFieldHandlers.cpp index 85b1c1510..e7f9cc647 100644 --- a/HeavenClient/Net/Handlers/SetFieldHandlers.cpp +++ b/HeavenClient/Net/Handlers/SetFieldHandlers.cpp @@ -123,6 +123,10 @@ namespace ms if (auto room = UI::get().get_element()) room->close(); + // v83 (FUN_00a03eba, from CField::OnSetField) sends this on every + // field change, not only on entering the game. + PlayerUpdatePacket().dispatch(); + transition(mapid, portalid, chase); } diff --git a/HeavenClient/Net/Packets/InventoryPackets.h b/HeavenClient/Net/Packets/InventoryPackets.h index 87af6e025..7bcfcd78f 100644 --- a/HeavenClient/Net/Packets/InventoryPackets.h +++ b/HeavenClient/Net/Packets/InventoryPackets.h @@ -76,12 +76,13 @@ namespace ms } }; - // Packet which requests that an item is equipped + // Packet which requests that an item is equipped at worn position `dest` + // (Inventory::equip_position). // Opcode: MOVE_ITEM(71) class EquipItemPacket : public MoveItemPacket { public: - EquipItemPacket(int16_t src, EquipSlot::Id dest) : MoveItemPacket(InventoryType::Id::EQUIP, src, -dest, 1) {} + EquipItemPacket(int16_t src, int16_t dest) : MoveItemPacket(InventoryType::Id::EQUIP, src, -dest, 1) {} }; // Packet which requests that an item is unequipped @@ -273,7 +274,7 @@ namespace ms class ScrollEquipPacket : public OutPacket { public: - ScrollEquipPacket(int16_t source, EquipSlot::Id target, bool whitescroll) : OutPacket(OutPacket::Opcode::SCROLL_EQUIP) + ScrollEquipPacket(int16_t source, int16_t target, bool whitescroll) : OutPacket(OutPacket::Opcode::SCROLL_EQUIP) { write_time(); write_short(source); diff --git a/game/src/inventory.rs b/game/src/inventory.rs index 1b61d2502..3a244fe4f 100644 --- a/game/src/inventory.rs +++ b/game/src/inventory.rs @@ -109,8 +109,14 @@ pub mod equip_slot { } } - /// Whether `item_id` may be worn at `position`. - pub fn fits(item_id: i32, position: i16) -> bool { + /// Whether `item_id` may be worn at `position`. As in v83, an item with + /// a cash serial is worn at its slot less 100, and any other at its slot. + pub fn fits(item_id: i32, cash: bool, position: i16) -> bool { + let position = match (cash, position) { + (true, -199..=-101) => position + 100, + (false, -99..=-1) => position, + _ => return false, + }; match for_item(item_id) { Some(slot) if RINGS.contains(&slot) => RINGS.contains(&position), Some(slot) => slot == position, @@ -570,7 +576,7 @@ impl Inventory { .get(InventoryType::Equip, from) .ok_or(InventoryError::InvalidSlot)? .clone(); - if item.equip.is_none() || !equip_slot::fits(item.item_id, to) { + if item.equip.is_none() || !equip_slot::fits(item.item_id, item.cash_serial.is_some(), to) { return Err(InventoryError::CannotEquip); } let worn = |position: i16| self.equipped.get(&position).map(|i| i.item_id); @@ -981,6 +987,27 @@ mod tests { ); } + #[test] + fn cash_equips_are_worn_a_hundred_below_their_slot() { + let mut inventory = Inventory::default(); + let mut cash_cap = equip(1002186); + cash_cap.cash_serial = Some(7); + inventory.insert(InventoryType::Equip, 1, cash_cap); + inventory.insert(InventoryType::Equip, 2, equip(1002008)); + assert_eq!( + inventory.equip(1, equip_slot::CAP), + Err(InventoryError::CannotEquip) + ); + assert_eq!( + inventory.equip(2, equip_slot::CAP - 100), + Err(InventoryError::CannotEquip) + ); + inventory.equip(1, equip_slot::CAP - 100).unwrap(); + inventory.equip(2, equip_slot::CAP).unwrap(); + assert_eq!(inventory.equipped_regular().count(), 1); + assert_eq!(inventory.equipped_cash().count(), 1); + } + #[test] fn overalls_and_pants_take_each_other_off() { let mut inventory = Inventory::default();