diff --git a/src/Client.cc b/src/Client.cc index f2836cfd..fdecf5a6 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -859,6 +859,13 @@ void Client::load_all_files() { if (this->character_data) { // Clear legacy play_time field this->character_data->disp.name.clear_after_bytes(0x18); + + // Enforce item stack limits, in case they've changed + auto s = this->require_server_state(); + auto stack_limits = s->item_stack_limits(this->version()); + this->character_data->inventory.enforce_stack_limits(stack_limits); + this->character_data->bank.enforce_stack_limits(stack_limits); + this->login->account->auto_reply_message = this->character_data->auto_reply.decode(); this->login->account->save(); this->last_play_time_update = phosg::now(); diff --git a/src/ItemData.cc b/src/ItemData.cc index 8edadbd3..200ae8ae 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -254,9 +254,14 @@ size_t ItemData::max_stack_size(const StackLimits& limits) const { return limits.get(this->data1[0], this->data1[1]); } -void ItemData::enforce_min_stack_size(const StackLimits& limits) { - if (this->stack_size(limits) == 0) { - this->data1[5] = 1; +void ItemData::enforce_stack_size_limits(const StackLimits& limits) { + if (this->data1[0] == 0x03) { + size_t max_stack_size = this->max_stack_size(limits); + if (max_stack_size > 1) { + this->data1[5] = std::clamp(this->data1[5], 1, max_stack_size); + } else { + this->data1[5] = 0; + } } } diff --git a/src/ItemData.hh b/src/ItemData.hh index e9412e13..1a61d9a8 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -163,7 +163,7 @@ struct ItemData { bool is_stackable(const StackLimits& limits) const; size_t stack_size(const StackLimits& limits) const; size_t max_stack_size(const StackLimits& limits) const; - void enforce_min_stack_size(const StackLimits& limits); + void enforce_stack_size_limits(const StackLimits& limits); static bool is_common_consumable(uint32_t primary_identifier); bool is_common_consumable() const; diff --git a/src/ItemNameIndex.cc b/src/ItemNameIndex.cc index ee7b0ff1..c3b4f263 100644 --- a/src/ItemNameIndex.cc +++ b/src/ItemNameIndex.cc @@ -396,7 +396,7 @@ ItemData ItemNameIndex::parse_item_description(const std::string& desc) const { } } } - ret.enforce_min_stack_size(*this->limits); + ret.enforce_stack_size_limits(*this->limits); return ret; } diff --git a/src/PlayerInventory.hh b/src/PlayerInventory.hh index 4237595f..e4197a8a 100644 --- a/src/PlayerInventory.hh +++ b/src/PlayerInventory.hh @@ -287,6 +287,12 @@ struct PlayerInventoryT { } } + void enforce_stack_limits(std::shared_ptr stack_limits) { + for (size_t z = 0; z < std::min(this->num_items, this->items.size()); z++) { + this->items[z].data.enforce_stack_size_limits(*stack_limits); + } + } + operator PlayerInventoryT() const { PlayerInventoryT ret; ret.num_items = this->num_items; @@ -410,6 +416,12 @@ struct PlayerBankT { } } + void enforce_stack_limits(std::shared_ptr stack_limits) { + for (size_t z = 0; z < std::min(this->num_items, this->items.size()); z++) { + this->items[z].data.enforce_stack_size_limits(*stack_limits); + } + } + template operator PlayerBankT() const { PlayerBankT ret; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 9f1f12dd..e354902b 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -4056,7 +4056,7 @@ static asio::awaitable on_item_reward_request_bb(shared_ptr c, Sub ItemData item; item = cmd.item_data; - item.enforce_min_stack_size(limits); + item.enforce_stack_size_limits(limits); item.id = l->generate_item_id(c->lobby_client_id); // The logic for the item_create and item_create2 opcodes (B3 and B4) @@ -4717,7 +4717,7 @@ static asio::awaitable on_quest_exchange_item_bb(shared_ptr c, Sub // TODO: We probably should use an allow-list here to prevent the client // from creating arbitrary items if cheat mode is disabled. ItemData new_item = cmd.replace_item; - new_item.enforce_min_stack_size(limits); + new_item.enforce_stack_size_limits(limits); new_item.id = l->generate_item_id(c->lobby_client_id); p->add_item(new_item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); @@ -4775,7 +4775,7 @@ static asio::awaitable on_photon_drop_exchange_for_item_bb(shared_ptrgenerate_item_id(c->lobby_client_id); p->add_item(new_item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); @@ -4875,7 +4875,7 @@ static asio::awaitable on_secret_lottery_ticket_exchange_bb(shared_ptrsecret_lottery_results.size() == 1) ? s->secret_lottery_results[0] : s->secret_lottery_results[l->rand_crypt->next() % s->secret_lottery_results.size()]; - item.enforce_min_stack_size(limits); + item.enforce_stack_size_limits(limits); item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, item); @@ -4950,7 +4950,7 @@ static asio::awaitable on_quest_F95E_result_bb(shared_ptr c, Subco } else if (item.data1[0] == 0x00) { item.data1[4] |= 0x80; // Unidentified } else { - item.enforce_min_stack_size(*s->item_stack_limits(c->version())); + item.enforce_stack_size_limits(*s->item_stack_limits(c->version())); } item.id = l->generate_item_id(0xFF); @@ -4996,7 +4996,7 @@ static asio::awaitable on_quest_F95F_result_bb(shared_ptr c, Subco send_command_t(c, 0x60, 0x00, cmd_6xDB); ItemData new_item = result.second; - new_item.enforce_min_stack_size(limits); + new_item.enforce_stack_size_limits(limits); new_item.id = l->generate_item_id(c->lobby_client_id); p->add_item(new_item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); @@ -5116,7 +5116,7 @@ static asio::awaitable on_momoka_item_exchange_bb(shared_ptr c, Su // TODO: We probably should use an allow-list here to prevent the client // from creating arbitrary items if cheat mode is disabled. ItemData new_item = cmd.replace_item; - new_item.enforce_min_stack_size(limits); + new_item.enforce_stack_size_limits(limits); new_item.id = l->generate_item_id(c->lobby_client_id); p->add_item(new_item, limits); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item);