From f05641a8b97a95e5dc05cc4c6e353598bd716dac Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 8 Mar 2023 22:30:33 -0800 Subject: [PATCH] fix BB shops + sale prices --- src/ItemCreator.cc | 8 +-- src/ItemData.cc | 58 ++++++++++++++++++- src/ItemData.hh | 8 ++- src/ItemParameterTable.cc | 117 +++++++++++++++++++++++++++++++++++--- src/ItemParameterTable.hh | 5 +- src/Player.hh | 3 +- src/ReceiveSubcommands.cc | 84 ++++++++++++++++++++------- src/SendCommands.cc | 18 +++--- 8 files changed, 253 insertions(+), 48 deletions(-) diff --git a/src/ItemCreator.cc b/src/ItemCreator.cc index 58868a64..129982b0 100644 --- a/src/ItemCreator.cc +++ b/src/ItemCreator.cc @@ -797,18 +797,18 @@ void ItemCreator::generate_common_unit_variances(uint8_t det, ItemData& item) { const auto& def = this->item_parameter_table->get_unit(item.data1[2]); switch (z % 5) { case 0: - item.set_item_unit_bonus(-(def.modifier_amount * 2)); + item.set_unit_bonus(-(def.modifier_amount * 2)); break; case 1: - item.set_item_unit_bonus(-def.modifier_amount); + item.set_unit_bonus(-def.modifier_amount); break; case 2: break; case 3: - item.set_item_unit_bonus(def.modifier_amount); + item.set_unit_bonus(def.modifier_amount); break; case 4: - item.set_item_unit_bonus(def.modifier_amount * 2); + item.set_unit_bonus(def.modifier_amount * 2); break; } } diff --git a/src/ItemData.cc b/src/ItemData.cc index 682b274e..d16f6f09 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -109,6 +109,10 @@ void ItemData::set_unidentified_or_present_flag(uint16_t v) { this->data1[11] = v; } +uint8_t ItemData::get_tool_item_amount() const { + return this->is_stackable() ? this->data1[5] : 1; +} + void ItemData::set_tool_item_amount(uint8_t amount) { if (this->is_stackable()) { this->data1[5] = amount; @@ -117,24 +121,72 @@ void ItemData::set_tool_item_amount(uint8_t amount) { } } - +int16_t ItemData::get_armor_or_shield_defense_bonus() const { + return this->data1w[3]; +} void ItemData::set_armor_or_shield_defense_bonus(int16_t bonus) { this->data1w[3] = bonus; } +int16_t ItemData::get_common_armor_evasion_bonus() const { + return this->data1w[4]; +} + void ItemData::set_common_armor_evasion_bonus(int16_t bonus) { this->data1w[4] = bonus; } +int16_t ItemData::get_unit_bonus() const { + return this->data1w[3]; +} - -void ItemData::set_item_unit_bonus(int16_t bonus) { +void ItemData::set_unit_bonus(int16_t bonus) { this->data1w[3] = bonus; } +bool ItemData::has_bonuses() const { + switch (this->data1[0]) { + case 0: + for (size_t z = 6; z <= 10; z += 2) { + if (this->data1[z] != 0) { + return true; + } + } + return false; + case 1: + switch (this->data1[1]) { + case 1: + if (this->data1[5] != 0) { + return true; + } + [[fallthrough]]; + case 2: + return ((this->get_armor_or_shield_defense_bonus() > 0) || + (this->get_common_armor_evasion_bonus() > 0)); + case 3: + return (this->get_unit_bonus() > 0); + default: + throw runtime_error("invalid item"); + } + case 2: + if (this->data1[1] < 0x23) { + return ((this->data1[1] == 0x1D) || (this->data1[1] == 0x22)); + } else { + return (this->data1[1] == 0x27); + } + case 3: + case 4: + return false; + default: + throw runtime_error("invalid item"); + } +} + + + bool ItemData::compare_for_sort(const ItemData& a, const ItemData& b) { for (size_t z = 0; z < 12; z++) { if (a.data1[z] < b.data1[z]) { diff --git a/src/ItemData.hh b/src/ItemData.hh index 45c30731..46c022ea 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -81,10 +81,16 @@ struct ItemData { // 0x14 bytes void clear_mag_stats(); void set_unidentified_or_present_flag(uint16_t v); + uint8_t get_tool_item_amount() const; void set_tool_item_amount(uint8_t amount); + int16_t get_armor_or_shield_defense_bonus() const; void set_armor_or_shield_defense_bonus(int16_t bonus); + int16_t get_common_armor_evasion_bonus() const; void set_common_armor_evasion_bonus(int16_t bonus); - void set_item_unit_bonus(int16_t bonus); + int16_t get_unit_bonus() const; + void set_unit_bonus(int16_t bonus); + + bool has_bonuses() const; bool empty() const; diff --git a/src/ItemParameterTable.cc b/src/ItemParameterTable.cc index ed189eb5..f4e4611d 100644 --- a/src/ItemParameterTable.cc +++ b/src/ItemParameterTable.cc @@ -71,7 +71,7 @@ pair ItemParameterTable::find_tool_by_class( } } } - throw out_of_range("invalid tool class"); + throw runtime_error("invalid tool class"); } const ItemParameterTable::Mag& ItemParameterTable::get_mag( @@ -131,10 +131,10 @@ uint8_t ItemParameterTable::get_special_stars(uint8_t det) const { uint8_t ItemParameterTable::get_max_tech_level(uint8_t char_class, uint8_t tech_num) const { if (char_class >= 12) { - throw logic_error("invalid character class"); + throw runtime_error("invalid character class"); } if (tech_num >= 19) { - throw logic_error("invalid technique number"); + throw runtime_error("invalid technique number"); } return r.pget_u8(this->offsets->max_tech_level_table + tech_num * 12 + char_class); } @@ -152,7 +152,7 @@ const ItemParameterTable::ItemBase& ItemParameterTable::get_item_definition( } else if ((item.data1[1] == 1) || (item.data1[1] == 2)) { return this->get_armor_or_shield(item.data1[1], item.data1[2]).base; } - throw logic_error("invalid item"); + throw runtime_error("invalid item"); case 2: return this->get_mag(item.data1[1]).base; case 3: @@ -163,13 +163,13 @@ const ItemParameterTable::ItemBase& ItemParameterTable::get_item_definition( } throw logic_error("this should be impossible"); case 4: - throw logic_error("item is meseta and therefore has no definition"); + throw runtime_error("item is meseta and therefore has no definition"); default: - throw logic_error("invalid item"); + throw runtime_error("invalid item"); } } -uint8_t ItemParameterTable::get_item_stars(const ItemData& item) const { +uint8_t ItemParameterTable::get_item_base_stars(const ItemData& item) const { if (item.data1[0] == 2) { return (item.data1[1] > 0x27) ? 12 : 0; } else if (item.data1[0] < 2) { @@ -184,8 +184,31 @@ uint8_t ItemParameterTable::get_item_stars(const ItemData& item) const { } } +uint8_t ItemParameterTable::get_item_adjusted_stars(const ItemData& item) const { + uint8_t ret = this->get_item_base_stars(item); + if (item.data1[0] == 0) { + if (ret < 9) { + if (!(item.data1[4] & 0x80)) { + ret += this->get_special_stars(item.data1[4]); + } + } else if (item.data1[4] & 0x80) { + ret = 0; + } + } else if (item.data1[0] == 1) { + if (item.data1[1] == 3) { + int16_t unit_bonus = item.get_unit_bonus(); + if (unit_bonus < 0) { + ret--; + } else if (unit_bonus > 0) { + ret++; + } + } + } + return min(ret, 12); +} + bool ItemParameterTable::is_item_rare(const ItemData& item) const { - return (this->get_item_stars(item) >= 9); + return (this->get_item_base_stars(item) >= 9); } bool ItemParameterTable::is_unsealable_item(const ItemData& item) const { @@ -202,3 +225,81 @@ bool ItemParameterTable::is_unsealable_item(const ItemData& item) const { return false; } + + +size_t ItemParameterTable::price_for_item(const ItemData& item) const { + switch (item.data1[0]) { + case 0: { + if (item.data1[4] & 0x80) { + return 8; + } + if (this->is_item_rare(item)) { + return 80; + } + + float sale_divisor = this->get_sale_divisor(item.data1[0], item.data1[1]); + if (sale_divisor == 0.0) { + throw runtime_error("item sale divisor is zero"); + } + + const auto& def = this->get_weapon(item.data1[1], item.data1[2]); + double atp_max = def.atp_max + item.data1[3]; + double atp_factor = ((atp_max * atp_max) / sale_divisor); + + double bonus_factor = 0.0; + for (size_t bonus_index = 0; bonus_index < 3; bonus_index++) { + uint8_t bonus_type = item.data1[(2 * bonus_index) + 6]; + if ((bonus_type > 0) && (bonus_type < 6)) { + bonus_factor += item.data1[(2 * bonus_index) + 7]; + } + bonus_factor += 100.0; + } + + size_t special_stars = this->get_special_stars(item.data1[4]); + double special_stars_factor = 1000.0 * special_stars * special_stars; + + return special_stars_factor + (atp_factor * (bonus_factor / 100.0)); + } + + case 1: { + if (this->is_item_rare(item)) { + return 80; + } + + if (item.data1[1] == 3) { // Unit + return this->get_item_adjusted_stars(item) * this->get_sale_divisor(item.data1[0], 3); + } + + double sale_divisor = (double)this->get_sale_divisor(item.data1[0], item.data1[1]); + if (sale_divisor == 0.0) { + throw runtime_error("item sale divisor is zero"); + } + + int16_t def_bonus = item.get_armor_or_shield_defense_bonus(); + int16_t evp_bonus = item.get_common_armor_evasion_bonus(); + + const auto& def = this->get_armor_or_shield(item.data1[1], item.data1[2]); + double power_factor = def.dfp + def.evp + def_bonus + evp_bonus; + double power_factor_floor = static_cast((power_factor * power_factor) / sale_divisor); + return power_factor_floor + ( + 70.0 * + static_cast(item.data1[5] + 1) * + static_cast(def.required_level + 1)); + } + + case 2: + return (item.data1[2] + 1) * this->get_sale_divisor(2, item.data1[1]); + + case 3: { + const auto& def = this->get_tool(item.data1[1], item.data1[2]); + return def.cost * ((item.data1[1] == 2) ? (item.data1[2] + 1) : 1); + } + + case 4: + return item.data2d; + + default: + throw runtime_error("invalid item"); + } + throw logic_error("this should be impossible"); +} diff --git a/src/ItemParameterTable.hh b/src/ItemParameterTable.hh index 66e67d16..b7f09f91 100644 --- a/src/ItemParameterTable.hh +++ b/src/ItemParameterTable.hh @@ -229,10 +229,13 @@ public: uint8_t get_max_tech_level(uint8_t char_class, uint8_t tech_num) const; const ItemBase& get_item_definition(const ItemData& item) const; - uint8_t get_item_stars(const ItemData& item) const; + uint8_t get_item_base_stars(const ItemData& item) const; + uint8_t get_item_adjusted_stars(const ItemData& item) const; bool is_item_rare(const ItemData& item) const; bool is_unsealable_item(const ItemData& param_1) const; + size_t price_for_item(const ItemData& item) const; + private: std::shared_ptr data; StringReader r; diff --git a/src/Player.hh b/src/Player.hh index 6dd093b4..a415a337 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -512,7 +513,7 @@ public: std::string bb_username; size_t bb_player_index; PlayerInventoryItem identify_result; - std::vector shop_contents; + std::array, 3> shop_contents; bool should_save; ClientGameData(); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index e0b5a4ce..b0adf6b9 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -838,19 +838,20 @@ static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr s, size_t level = c->game_data.player()->disp.level + 1; switch (cmd.shop_type) { case 0: - c->game_data.shop_contents = l->item_creator->generate_tool_shop_contents(level); + c->game_data.shop_contents[0] = l->item_creator->generate_tool_shop_contents(level); break; case 1: - c->game_data.shop_contents = l->item_creator->generate_weapon_shop_contents(level); + c->game_data.shop_contents[1] = l->item_creator->generate_weapon_shop_contents(level); break; case 2: - c->game_data.shop_contents = l->item_creator->generate_armor_shop_contents(level); + c->game_data.shop_contents[2] = l->item_creator->generate_armor_shop_contents(level); break; default: throw runtime_error("invalid shop type"); } - for (auto& item : c->game_data.shop_contents) { + for (auto& item : c->game_data.shop_contents[cmd.shop_type]) { item.id = l->generate_item_id(c->lobby_client_id); + item.data2d = s->item_parameter_table->price_for_item(item); } send_shop(c, cmd.shop_type); @@ -1188,6 +1189,11 @@ static void on_destroy_inventory_item(shared_ptr, auto name = item.data.name(false); l->log.info("Inventory item %hu:%08" PRIX32 " destroyed (%s)", cmd.header.client_id.load(), cmd.item_id.load(), name.c_str()); + if (c->options.debug) { + string name = item.data.name(true); + send_text_message_printf(c, "$C5Items: destroy %08" PRIX32 "\n%s", + cmd.item_id.load(), name.c_str()); + } c->game_data.player()->print_inventory(stderr); forward_subcommand(l, c, command, flag, data); } @@ -1205,6 +1211,11 @@ static void on_destroy_ground_item(shared_ptr, auto name = item.data.name(false); l->log.info("Ground item %08" PRIX32 " destroyed (%s)", cmd.item_id.load(), name.c_str()); + if (c->options.debug) { + string name = item.data.name(true); + send_text_message_printf(c, "$C5Items: destroy/ground %08" PRIX32 "\n%s", + cmd.item_id.load(), name.c_str()); + } forward_subcommand(l, c, command, flag, data); } } @@ -1269,37 +1280,72 @@ static void on_accept_identify_item_bb(shared_ptr, } } -static void on_sell_item_at_shop_bb(shared_ptr, - shared_ptr l, shared_ptr, uint8_t, uint8_t, const string&) { +static void on_sell_item_at_shop_bb(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (l->version == GameVersion::BB) { - // const auto& cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { throw logic_error("item tracking not enabled in BB game"); } - // TODO: We should subtract the appropriate amount of meseta and do an - // appropriate send_create_inventory_item call here. Shop prices are not - // implemented yet, though, which is why this is difficult. - throw logic_error("shop actions are not yet implemented"); + auto item = c->game_data.player()->remove_item( + cmd.item_id, cmd.amount, c->version() != GameVersion::BB); + size_t price = (s->item_parameter_table->price_for_item(item.data) >> 3) * cmd.amount; + c->game_data.player()->disp.meseta = min( + c->game_data.player()->disp.meseta + price, 999999); + + auto name = item.data.name(false); + l->log.info("Inventory item %hu:%08" PRIX32 " destroyed via sale (%s)", + c->lobby_client_id, cmd.item_id.load(), name.c_str()); + c->game_data.player()->print_inventory(stderr); + if (c->options.debug) { + string name = item.data.name(true); + send_text_message_printf(c, "$C5Items: destroy/sale %08" PRIX32 "\n+%zu Meseta\n%s", + cmd.item_id.load(), price, name.c_str()); + } + + forward_subcommand(l, c, command, flag, data); } } static void on_buy_shop_item_bb(shared_ptr, - shared_ptr l, shared_ptr, uint8_t, uint8_t, const string&) { - + shared_ptr l, shared_ptr c, uint8_t, uint8_t, const string& data) { if (l->version == GameVersion::BB) { - // const auto& cmd = check_size_sc(data); - + const auto& cmd = check_size_sc(data); if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { throw logic_error("item tracking not enabled in BB game"); } - // TODO: We should subtract the appropriate amount of meseta and do an - // appropriate send_create_inventory_item call here. Shop prices are not - // implemented yet, though, which is why this is difficult. - throw logic_error("shop actions are not yet implemented"); + PlayerInventoryItem item; + item.data = c->game_data.shop_contents.at(cmd.shop_type).at(cmd.item_index); + if (item.data.is_stackable()) { + item.data.data1[5] = cmd.amount; + } else if (cmd.amount != 1) { + throw runtime_error("item is not stackable"); + } + + size_t price = item.data.data2d * cmd.amount; + item.data.data2d = 0; + if (c->game_data.player()->disp.meseta < price) { + throw runtime_error("player does not have enough money"); + } + c->game_data.player()->disp.meseta -= price; + + item.data.id = cmd.inventory_item_id; + c->game_data.player()->add_item(item); + send_create_inventory_item(l, c, item.data); + + auto name = item.data.name(false); + l->log.info("Inventory item %hu:%08" PRIX32 " created via purchase (%s) for %zu meseta", + c->lobby_client_id, cmd.inventory_item_id.load(), name.c_str(), price); + c->game_data.player()->print_inventory(stderr); + if (c->options.debug) { + string name = item.data.name(true); + send_text_message_printf(c, "$C5Items: create/purchase %08" PRIX32 "\n-%zu Meseta\n%s", + cmd.inventory_item_id.load(), price, name.c_str()); + } } } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 2c6c1e73..32867628 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1870,24 +1870,20 @@ void send_bank(shared_ptr c) { // sends the player a shop's contents void send_shop(shared_ptr c, uint8_t shop_type) { + const auto& contents = c->game_data.shop_contents.at(shop_type); + G_ShopContents_BB_6xB6 cmd = { - {0xB6, 0x2C, 0x037F}, + {0xB6, static_cast(2 + (sizeof(ItemData) >> 2) * contents.size()), 0x0000}, shop_type, - static_cast(c->game_data.shop_contents.size()), + static_cast(contents.size()), 0, {}, }; - - size_t count = c->game_data.shop_contents.size(); - if (count > sizeof(cmd.entries) / sizeof(cmd.entries[0])) { - throw logic_error("too many items in shop"); + for (size_t x = 0; x < contents.size(); x++) { + cmd.entries[x] = contents[x]; } - for (size_t x = 0; x < count; x++) { - cmd.entries[x] = c->game_data.shop_contents[x]; - } - - send_command(c, 0x6C, 0x00, &cmd, sizeof(cmd) - sizeof(cmd.entries[0]) * (20 - count)); + send_command(c, 0x60, 0x00, &cmd, sizeof(cmd) - sizeof(cmd.entries[0]) * (20 - contents.size())); } // notifies players about a level up