From 5a30272869e2b111a097b79e9b8770f40a3235c5 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Thu, 19 Oct 2023 15:34:06 -0700 Subject: [PATCH] implement some BB quest commands --- src/ChatCommands.cc | 8 +- src/CommandFormats.hh | 49 +++++---- src/ItemData.cc | 21 ++++ src/ItemData.hh | 1 + src/LevelTable.cc | 22 ++++ src/LevelTable.hh | 5 + src/Player.cc | 49 ++------- src/PlayerSubordinates.cc | 9 ++ src/PlayerSubordinates.hh | 1 + src/QuestScript.cc | 16 +-- src/ReceiveCommands.cc | 4 +- src/ReceiveSubcommands.cc | 205 ++++++++++++++++++++++++++++++++++---- src/SendCommands.cc | 10 ++ src/SendCommands.hh | 4 + 14 files changed, 307 insertions(+), 97 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index a16794e4..530187e7 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -396,9 +396,7 @@ static void proxy_command_exit(shared_ptr ses, const static void server_command_call(shared_ptr c, const std::u16string& args) { auto l = c->require_lobby(); if (l->is_game() && l->quest) { - S_ConfirmQuestStatistic_V3_BB_AB cmd; - cmd.function_id = stoul(encode_sjis(args), nullptr, 0); - send_command_t(c, 0xAB, 0x00, cmd); + send_quest_function_call(c, stoul(encode_sjis(args), nullptr, 0)); } else { send_text_message(c, u"$C6You must be in\nquest to use this\ncommand"); } @@ -406,9 +404,7 @@ static void server_command_call(shared_ptr c, const std::u16string& args static void proxy_command_call(shared_ptr ses, const std::u16string& args) { if (ses->is_in_game && ses->is_in_quest) { - S_ConfirmQuestStatistic_V3_BB_AB cmd; - cmd.function_id = stoul(encode_sjis(args), nullptr, 0); - ses->client_channel.send(0xAB, 0x00, &cmd, sizeof(cmd)); + send_quest_function_call(ses->client_channel, stoul(encode_sjis(args), nullptr, 0)); } else { send_text_message(ses->client_channel, u"$C6You must be in\nquest to use this\ncommand"); } diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 31f2426a..19f5b321 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -2063,14 +2063,14 @@ struct C_SendQuestStatistic_V3_BB_AA { parray params; } __packed__; -// AB (S->C): Confirm quest statistic (V3/BB) +// AB (S->C): Call quest function (V3/BB) // This command is not valid on PSO GC Episodes 1&2 Trial Edition. // Upon receipt, the client starts a quest thread running the given function. // Probably this is supposed to be one of the function IDs previously sent in // the AA command, but the client does not check for this. The server can // presumably use this command to call any function at any time during a quest. -struct S_ConfirmQuestStatistic_V3_BB_AB { +struct S_CallQuestFunction_V3_BB_AB { le_uint16_t function_id; parray unused; } __packed__; @@ -5534,17 +5534,17 @@ struct G_Unknown_BB_6xD4 { struct G_ExchangeItemInQuest_BB_6xD5 { G_ClientIDHeader header; - ItemData unknown_a1; // Only data1[0]-[2] are used - ItemData unknown_a2; // Only data1[0]-[2] are used - le_uint16_t unknown_a3; - le_uint16_t unknown_a4; + ItemData find_item; // Only data1[0]-[2] are used + ItemData replace_item; // Only data1[0]-[2] are used + le_uint16_t success_function_id; + le_uint16_t failure_function_id; } __packed__; // 6xD6: Wrap item (BB; handled by server) struct G_WrapItem_BB_6xD6 { G_ClientIDHeader header; - ItemData item_data; + ItemData item; uint8_t unknown_a1; parray unused; } __packed__; @@ -5554,9 +5554,9 @@ struct G_WrapItem_BB_6xD6 { struct G_PaganiniPhotonDropExchange_BB_6xD7 { G_ClientIDHeader header; - ItemData unknown_a1; // Only data1[0]-[2] are used - le_uint16_t request_id; - le_uint16_t unknown_a3; + ItemData new_item; // Only data1[0]-[2] are used + le_uint16_t success_function_id; + le_uint16_t failure_function_id; } __packed__; // 6xD8: Add S-rank weapon special (BB; handled by server) @@ -5576,8 +5576,8 @@ struct G_AddSRankWeaponSpecial_BB_6xD8 { struct G_MomokaItemExchange_BB_6xD9 { G_ClientIDHeader header; - ItemData unknown_a1; - ItemData unknown_a2; + ItemData find_item; // Only data1[0]-[2] are used + ItemData replace_item; // Only data1[0]-[2] are used le_uint32_t unknown_a3; le_uint32_t unknown_a4; le_uint16_t unknown_a5; @@ -5589,13 +5589,13 @@ struct G_MomokaItemExchange_BB_6xD9 { struct G_UpgradeWeaponAttribute_BB_6xDA { G_ClientIDHeader header; - ItemData unknown_a1; // Only data1[0]-[2] are used - le_uint32_t item_id; - le_uint32_t attribute; - le_uint32_t unknown_a4; // 0 or 1 - le_uint32_t unknown_a5; - le_uint16_t request_id; - le_uint16_t unknown_a7; + ItemData item; // Only data1[0-2] are used (argsA[1-3]) + le_uint32_t item_id; // argsA[0] + le_uint32_t attribute; // argsA[4] + le_uint32_t payment_count; // Number of PD or PS (argsA[5]) + le_uint32_t payment_type; // 0 = Photon Drops, 1 = Photon Spheres + le_uint16_t success_function_id; // argsA[6] + le_uint16_t failure_function_id; // argsA[7] } __packed__; // 6xDB: Exchange item in quest (BB) @@ -5633,10 +5633,10 @@ struct G_GoodLuckQuestActions_BB_6xDE { le_uint16_t unknown_a3; } __packed__; -// 6xDF: Black Paper's Deal Photon Drop exchange (BB; handled by server) +// 6xDF: Black Paper's Deal Photon Crystal exchange (BB; handled by server) // The client sends this when it executes an F95D quest opcode. -struct G_BlackPaperDealPhotonDropExchange_BB_6xE0 { +struct G_BlackPaperDealPhotonCrystalExchange_BB_6xDF { G_ClientIDHeader header; } __packed__; @@ -5645,7 +5645,12 @@ struct G_BlackPaperDealPhotonDropExchange_BB_6xE0 { struct G_BlackPaperDealRewards_BB_6xE0 { G_ClientIDHeader header; - parray unknown_a1; // TODO: There might be uint16_ts and uint32_ts in here. + uint8_t unknown_a1; + uint8_t unknown_a2; + uint8_t unknown_a3; + uint8_t unknown_a4; + le_uint32_t unknown_a5; + le_uint32_t unknown_a6; } __packed__; // 6xE1: Gallon's Plan quest (BB; handled by server) diff --git a/src/ItemData.cc b/src/ItemData.cc index 5ca9c46b..b0380899 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -125,6 +125,27 @@ bool ItemData::is_wrapped() const { } } +void ItemData::wrap() { + switch (this->data1[0]) { + case 0: + case 1: + this->data1[4] |= 0x40; + break; + case 2: + this->data2[2] |= 0x40; + break; + case 3: + if (!this->is_stackable()) { + this->data1[3] |= 0x40; + } + break; + case 4: + break; + default: + throw runtime_error("invalid item data"); + } +} + void ItemData::unwrap() { switch (this->data1[0]) { case 0: diff --git a/src/ItemData.hh b/src/ItemData.hh index 8c732140..2276bed1 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -114,6 +114,7 @@ struct ItemData { // 0x14 bytes uint32_t primary_identifier() const; bool is_wrapped() const; + void wrap(); void unwrap(); bool is_stackable() const; diff --git a/src/LevelTable.cc b/src/LevelTable.cc index 456baf0a..b3391475 100644 --- a/src/LevelTable.cc +++ b/src/LevelTable.cc @@ -8,6 +8,28 @@ using namespace std; +void PlayerStats::reset_to_base(uint8_t char_class, shared_ptr level_table) { + this->level = 0; + this->char_stats = level_table->base_stats_for_class(char_class); +} + +void PlayerStats::advance_to_level(uint8_t char_class, uint32_t level, shared_ptr level_table) { + for (; this->level < level; this->level++) { + const auto& level_stats = level_table->stats_delta_for_level(char_class, this->level + 1); + // The original code clamps the resulting stat values to [0, max_stat]; we + // don't have max_stat handy so we just allow them to be unbounded + this->char_stats.atp += level_stats.atp; + this->char_stats.mst += level_stats.mst; + this->char_stats.evp += level_stats.evp; + this->char_stats.hp += level_stats.hp; + this->char_stats.dfp += level_stats.dfp; + this->char_stats.ata += level_stats.ata; + // Note: It is not a bug that lck is ignored here; the original code + // ignores it too. + this->experience = level_stats.experience; + } +} + LevelTable::LevelTable(shared_ptr data, bool compressed) { if (compressed) { this->data.reset(new string(prs_decompress(*data))); diff --git a/src/LevelTable.hh b/src/LevelTable.hh index af686d1a..62ff4f5d 100644 --- a/src/LevelTable.hh +++ b/src/LevelTable.hh @@ -6,6 +6,8 @@ #include #include +class LevelTable; + struct CharacterStats { le_uint16_t atp = 0; le_uint16_t mst = 0; @@ -25,6 +27,9 @@ struct PlayerStats { /* 1C */ le_uint32_t experience = 0; /* 20 */ le_uint32_t meseta = 0; /* 24 */ + + void reset_to_base(uint8_t char_class, std::shared_ptr level_table); + void advance_to_level(uint8_t char_class, uint32_t level, std::shared_ptr level_table); } __attribute__((packed)); class LevelTable { // from PlyLevelTbl.prs diff --git a/src/Player.cc b/src/Player.cc index 8278e76a..f0b34305 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -56,37 +56,19 @@ void ClientGameData::create_battle_overlay(shared_ptr rules, this->overlay_player_data->inventory.remove_all_items_of_type(3); } if (rules->replace_char) { + // TODO: Shouldn't we clear other material usage here? It looks like the + // original code doesn't, but that seems wrong. this->overlay_player_data->inventory.hp_materials_used = 0; this->overlay_player_data->inventory.tp_materials_used = 0; uint32_t target_level = clamp(rules->char_level, 0, 199); uint8_t char_class = this->overlay_player_data->disp.visual.char_class; - const auto& base_stats = level_table->base_stats_for_class(char_class); auto& stats = this->overlay_player_data->disp.stats; - stats.char_stats.atp = base_stats.atp; - stats.char_stats.mst = base_stats.mst; - stats.char_stats.evp = base_stats.evp; - stats.char_stats.hp = base_stats.hp; - stats.char_stats.dfp = base_stats.dfp; - stats.char_stats.ata = base_stats.ata; - stats.char_stats.lck = base_stats.lck; - for (this->overlay_player_data->disp.stats.level = 0; - this->overlay_player_data->disp.stats.level < target_level; - this->overlay_player_data->disp.stats.level++) { - const auto& level_stats = level_table->stats_delta_for_level(char_class, this->overlay_player_data->disp.stats.level + 1); - // The original code clamps the resulting stat values to [0, max_stat]; - // we don't have max_stat handy so we just allow them to be unbounded - stats.char_stats.atp += level_stats.atp; - stats.char_stats.mst += level_stats.mst; - stats.char_stats.evp += level_stats.evp; - stats.char_stats.hp += level_stats.hp; - stats.char_stats.dfp += level_stats.dfp; - stats.char_stats.ata += level_stats.ata; - // Note: It is not a bug that lck is ignored here; the original code - // ignores it too. - } + + stats.reset_to_base(char_class, level_table); + stats.advance_to_level(char_class, target_level, level_table); + stats.unknown_a1 = 40; - stats.experience = level_table->stats_delta_for_level(char_class, stats.level).experience; stats.meseta = 300; } if (rules->tech_disk_mode == BattleRules::TechDiskMode::LIMIT_LEVEL) { @@ -127,23 +109,8 @@ void ClientGameData::create_challenge_overlay(size_t template_index, shared_ptr< overlay->inventory.items[13].extension_data2 = 1; - overlay->disp.stats.char_stats = level_table->base_stats_for_class(overlay->disp.visual.char_class); - for (overlay->disp.stats.level = 0; - overlay->disp.stats.level < tpl.level; - overlay->disp.stats.level++) { - const auto& level_stats = level_table->stats_delta_for_level( - overlay->disp.visual.char_class, overlay->disp.stats.level + 1); - // The original code clamps the resulting stat values to [0, max_stat]; we - // don't have max_stat handy so we just allow them to be unbounded - overlay->disp.stats.char_stats.atp += level_stats.atp; - overlay->disp.stats.char_stats.mst += level_stats.mst; - overlay->disp.stats.char_stats.evp += level_stats.evp; - overlay->disp.stats.char_stats.hp += level_stats.hp; - overlay->disp.stats.char_stats.dfp += level_stats.dfp; - overlay->disp.stats.char_stats.ata += level_stats.ata; - // Note: It is not a bug that lck is ignored here; the original code - // ignores it too. - } + overlay->disp.stats.reset_to_base(overlay->disp.visual.char_class, level_table); + overlay->disp.stats.advance_to_level(overlay->disp.visual.char_class, tpl.level, level_table); overlay->disp.stats.unknown_a1 = 40; overlay->disp.stats.unknown_a3 = 10.0; diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index 19f8a4a5..6f33de0c 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -380,6 +380,15 @@ size_t PlayerInventory::find_item(uint32_t item_id) const { throw out_of_range("item not present"); } +size_t PlayerInventory::find_item_by_primary_identifier(uint32_t primary_identifier) const { + for (size_t x = 0; x < this->num_items; x++) { + if (this->items[x].data.primary_identifier() == primary_identifier) { + return x; + } + } + throw out_of_range("item not present"); +} + size_t PlayerInventory::find_equipped_weapon() const { ssize_t ret = -1; for (size_t y = 0; y < this->num_items; y++) { diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index 6e9aebde..640eb404 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -70,6 +70,7 @@ struct PlayerInventory { PlayerInventory(); size_t find_item(uint32_t item_id) const; + size_t find_item_by_primary_identifier(uint32_t primary_identifier) const; size_t find_equipped_weapon() const; size_t find_equipped_armor() const; diff --git a/src/QuestScript.cc b/src/QuestScript.cc index 7e3be3b1..badf11ba 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -669,11 +669,11 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF8B2, "read4", {REG, REG}, F_V2}, {0xF8B2, "read4", {REG, INT32}, F_V3_V4 | F_ARGS}, {0xF8B3, "write1", {REG, REG}, F_V2}, - {0xF8B3, "write1", {INT32, REG}, F_V3_V4 | F_ARGS}, + {0xF8B3, "write1", {INT32, INT32}, F_V3_V4 | F_ARGS}, {0xF8B4, "write2", {REG, REG}, F_V2}, - {0xF8B4, "write2", {INT32, REG}, F_V3_V4 | F_ARGS}, + {0xF8B4, "write2", {INT32, INT32}, F_V3_V4 | F_ARGS}, {0xF8B5, "write4", {REG, REG}, F_V2}, - {0xF8B5, "write4", {INT32, REG}, F_V3_V4 | F_ARGS}, + {0xF8B5, "write4", {INT32, INT32}, F_V3_V4 | F_ARGS}, {0xF8B6, "check_for_hacking", {REG}, F_V2}, // Returns a bitmask of 5 different types of detectable hacking. But it only works on DCv2 - it crashes on all other versions. {0xF8B7, nullptr, {REG}, F_V2_V4}, // TODO (DX) - Challenge mode. Appears to be timing-related; regA is expected to be in [60, 3600]. Encodes the value with encrypt_challenge_time even though it's never sent over the network and is only decrypted locally. {0xF8B8, "disable_retry_menu", {}, F_V2_V4}, @@ -829,13 +829,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF952, "bb_get_number_in_pack", {REG}, F_V4}, {0xF953, "bb_swap_item", {INT32, INT32, INT32, INT32, INT32, INT32, SCRIPT16, SCRIPT16}, F_V4 | F_ARGS}, // Sends 6xD5 {0xF954, "bb_check_wrap", {INT32, REG}, F_V4 | F_ARGS}, - {0xF955, "bb_exchange_pd_item", {INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xD7 + {0xF955, "bb_exchange_pd_item", {INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xD7 {0xF956, "bb_exchange_pd_srank", {INT32, INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xD8 - {0xF957, "bb_exchange_pd_special", {INT32, INT32, INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xDA - {0xF958, "bb_exchange_pd_percent", {INT32, INT32, INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xDA + {0xF957, "bb_exchange_pd_percent", {INT32, INT32, INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xDA + {0xF958, "bb_exchange_ps_percent", {INT32, INT32, INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xDA {0xF959, "bb_set_ep4_boss_can_escape", {INT32}, F_V4 | F_ARGS}, {0xF95A, "bb_is_ep4_boss_dying", {REG}, F_V4}, - {0xF95B, "bb_send_6xD9", {INT32, INT32, INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xD9 + {0xF95B, "bb_send_6xD9", {INT32, INT32, INT32, INT32, LABEL16, LABEL16}, F_V4 | F_ARGS}, // Sends 6xD9 {0xF95C, "bb_exchange_slt", {INT32, INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xDE {0xF95D, "bb_exchange_pc", {}, F_V4}, // Sends 6xDF {0xF95E, "bb_box_create_bp", {INT32, INT32, INT32}, F_V4 | F_ARGS}, // Sends 6xE0 @@ -1498,7 +1498,7 @@ std::string disassemble_quest_script(const void* data, size_t size, QuestScriptV StringReader r = cmd_r.sub(l->offset, size); lines.emplace_back(" // As F8F2 entries"); while (r.remaining() >= sizeof(UnknownF8F2Entry)) { - size_t offset = r.where() + cmd_r.where(); + size_t offset = l->offset + cmd_r.where(); const auto& e = r.get(); lines.emplace_back(string_printf(" %04zX entry %g, %g, %g, %g", offset, e.unknown_a1[0].load(), e.unknown_a1[1].load(), e.unknown_a1[2].load(), e.unknown_a1[3].load())); } diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 3d8870b2..b9eaa0a6 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2520,9 +2520,7 @@ static void on_AA(shared_ptr c, uint16_t, uint32_t, const string& data) } // TODO: Send the right value here. (When should we send function_id2?) - S_ConfirmQuestStatistic_V3_BB_AB response; - response.function_id = cmd.function_id1; - send_command_t(c, 0xAB, 0x00, response); + send_quest_function_call(c, cmd.function_id1); } static void on_D7_GC(shared_ptr c, uint16_t, uint32_t, const string& data) { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index ee9ac769..cddf7fe4 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1743,9 +1743,9 @@ static void on_battle_restart_bb(shared_ptr c, uint8_t, uint8_t, const v auto s = c->require_server_state(); auto l = c->require_lobby(); if (l->is_game() && + (l->base_version == GameVersion::BB) && (l->mode == GameMode::BATTLE) && - (l->flags & Lobby::Flag::QUEST_IN_PROGRESS) && - (l->base_version == GameVersion::BB)) { + (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) { const auto& cmd = check_size_t(data, size); shared_ptr new_rules(new BattleRules(cmd.rules)); @@ -1763,19 +1763,190 @@ static void on_battle_restart_bb(shared_ptr c, uint8_t, uint8_t, const v } } -static void on_battle_level_up_bb(shared_ptr c, uint8_t, uint8_t, const void*, size_t) { +static void on_battle_level_up_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { auto l = c->require_lobby(); if (l->is_game() && + (l->base_version == GameVersion::BB) && (l->mode == GameMode::BATTLE) && - (l->flags & Lobby::Flag::QUEST_IN_PROGRESS) && - (l->base_version == GameVersion::BB)) { - // Requests the client to be leveled up by num_levels levels. The server should - // respond with a 6x30 command. + (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) { + const auto& cmd = check_size_t(data, size); + auto lc = l->clients[cmd.header.client_id]; + if (lc) { + auto s = c->require_server_state(); + auto lp = lc->game_data.player(); + uint32_t target_level = lp->disp.stats.level + cmd.num_levels; + uint32_t before_exp = lp->disp.stats.experience; + lp->disp.stats.advance_to_level(lp->disp.visual.char_class, target_level, s->level_table); + send_give_experience(lc, lp->disp.stats.experience - before_exp); + send_level_up(lc); + } + } +} - struct G_BattleModeLevelUp_BB_6xD0 { - G_ClientIDHeader header; - le_uint32_t num_levels; - } __packed__; +static void on_quest_exchange_item_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { + auto l = c->require_lobby(); + if (l->is_game() && + (l->base_version == GameVersion::BB) && + (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) { + const auto& cmd = check_size_t(data, size); + + try { + auto p = c->game_data.player(); + + size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier()); + auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false); + send_destroy_item(c, found_item.id, 1); + + // 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.id = l->generate_item_id(c->lobby_client_id); + p->add_item(new_item); + send_create_inventory_item(c, new_item); + + send_quest_function_call(c, cmd.success_function_id); + + } catch (const exception& e) { + c->log.warning("Quest item exchange failed: %s", e.what()); + send_quest_function_call(c, cmd.failure_function_id); + } + } +} + +static void on_wrap_item_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { + auto l = c->require_lobby(); + if (l->is_game() && (l->base_version == GameVersion::BB)) { + const auto& cmd = check_size_t(data, size); + + auto p = c->game_data.player(); + auto item = p->remove_item(cmd.item.id, 1, false); + send_destroy_item(c, item.id, 1); + item.wrap(); + p->add_item(cmd.item); + send_create_inventory_item(c, item); + } +} + +static void on_photon_drop_exchange_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { + auto l = c->require_lobby(); + if (l->is_game() && (l->base_version == GameVersion::BB)) { + const auto& cmd = check_size_t(data, size); + + try { + auto p = c->game_data.player(); + + size_t found_index = p->inventory.find_item_by_primary_identifier(0x031000); + auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 0, false); + send_destroy_item(c, found_item.id, found_item.stack_size()); + + // 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.new_item; + new_item.id = l->generate_item_id(c->lobby_client_id); + p->add_item(new_item); + send_create_inventory_item(c, new_item); + + send_quest_function_call(c, cmd.success_function_id); + + } catch (const exception& e) { + c->log.warning("Quest Photon Drop exchange failed: %s", e.what()); + send_quest_function_call(c, cmd.failure_function_id); + } + } +} + +static void on_photon_crystal_exchange_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { + auto l = c->require_lobby(); + if (l->is_game() && (l->base_version == GameVersion::BB) && (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) { + check_size_t(data, size); + auto p = c->game_data.player(); + size_t index = p->inventory.find_item_by_primary_identifier(0x031002); + auto item = p->remove_item(p->inventory.items[index].data.id, 1, false); + send_destroy_item(c, item.id, 1); + // TODO: Should we disable drops here? + } +} + +static void on_momoka_item_exchange_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { + auto l = c->require_lobby(); + if (l->is_game() && (l->base_version == GameVersion::BB) && (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) { + const auto& cmd = check_size_t(data, size); + auto p = c->game_data.player(); + try { + size_t found_index = p->inventory.find_item_by_primary_identifier(cmd.find_item.primary_identifier()); + auto found_item = p->remove_item(p->inventory.items[found_index].data.id, 1, false); + + G_ExchangeItemInQuest_BB_6xDB cmd_6xDB = {{0xDB, 0x04, c->lobby_client_id}, 1, found_item.id, 1}; + send_command_t(c, 0x60, 0x00, cmd_6xDB); + + send_destroy_item(c, found_item.id, 1); + + // 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.id = l->generate_item_id(c->lobby_client_id); + p->add_item(new_item); + send_create_inventory_item(c, new_item); + + send_command(c, 0x23, 0x00); + } catch (const exception& e) { + c->log.warning("Momoka item exchange failed: %s", e.what()); + send_command(c, 0x23, 0x01); + } + } +} + +static void on_upgrade_weapon_attribute_bb(shared_ptr c, uint8_t, uint8_t, const void* data, size_t size) { + auto l = c->require_lobby(); + if (l->is_game() && (l->base_version == GameVersion::BB) && (l->flags & Lobby::Flag::QUEST_IN_PROGRESS)) { + const auto& cmd = check_size_t(data, size); + auto p = c->game_data.player(); + try { + size_t item_index = p->inventory.find_item(cmd.item_id); + auto& item = p->inventory.items[item_index].data; + + uint32_t payment_primary_identifier = cmd.payment_type ? 0x031001 : 0x031000; + size_t payment_index = p->inventory.find_item_by_primary_identifier(payment_primary_identifier); + auto& payment_item = p->inventory.items[payment_index].data; + if (payment_item.stack_size() < cmd.payment_count) { + throw runtime_error("not enough payment items present"); + } + p->remove_item(payment_item.id, cmd.payment_count, false); + send_destroy_item(c, payment_item.id, cmd.payment_count); + + uint8_t attribute_amount = 0; + if (cmd.payment_type == 1 && cmd.payment_count == 1) { + attribute_amount = 30; + } else if (cmd.payment_type == 0 && cmd.payment_count == 4) { + attribute_amount = 1; + } else if (cmd.payment_type == 1 && cmd.payment_count == 20) { + attribute_amount = 5; + } else { + throw runtime_error("unknown PD/PS expenditure"); + } + + size_t attribute_index = 0; + for (size_t z = 6; z <= 10; z += 2) { + if (!(item.data1[z] & 0x80) && (item.data1[z] == cmd.attribute)) { + attribute_index = z; + } else if (item.data1[z] == 0) { + attribute_index = z; + } + } + if (attribute_index == 0) { + throw runtime_error("no available attribute slots"); + } + item.data1[attribute_index] = cmd.attribute; + item.data1[attribute_index] += attribute_amount; + + send_destroy_item(c, item.id, 1); + send_create_inventory_item(c, item); + send_quest_function_call(c, cmd.success_function_id); + + } catch (const exception& e) { + c->log.warning("Weapon attribute upgrade failed: %s", e.what()); + send_quest_function_call(c, cmd.failure_function_id); + } } } @@ -1997,17 +2168,17 @@ subcommand_handler_t subcommand_handlers[0x100] = { /* 6xD2 */ nullptr, /* 6xD3 */ nullptr, /* 6xD4 */ nullptr, - /* 6xD5 */ nullptr, - /* 6xD6 */ nullptr, - /* 6xD7 */ nullptr, + /* 6xD5 */ on_quest_exchange_item_bb, + /* 6xD6 */ on_wrap_item_bb, + /* 6xD7 */ on_photon_drop_exchange_bb, /* 6xD8 */ nullptr, - /* 6xD9 */ nullptr, - /* 6xDA */ nullptr, + /* 6xD9 */ on_momoka_item_exchange_bb, + /* 6xDA */ on_upgrade_weapon_attribute_bb, /* 6xDB */ nullptr, /* 6xDC */ nullptr, /* 6xDD */ nullptr, /* 6xDE */ nullptr, - /* 6xDF */ nullptr, + /* 6xDF */ on_photon_crystal_exchange_bb, /* 6xE0 */ nullptr, /* 6xE1 */ nullptr, /* 6xE2 */ nullptr, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 66b6d1cd..f8a83379 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2209,6 +2209,16 @@ void send_rare_enemy_index_list(shared_ptr c, const vector& inde send_command_t(c, 0xDE, 0x00, cmd); } +void send_quest_function_call(Channel& ch, uint16_t function_id) { + S_CallQuestFunction_V3_BB_AB cmd; + cmd.function_id = function_id; + ch.send(0xAB, 0x00, &cmd, sizeof(cmd)); +} + +void send_quest_function_call(shared_ptr c, uint16_t function_id) { + send_quest_function_call(c->channel, function_id); +} + //////////////////////////////////////////////////////////////////////////////// // ep3 only commands diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 296d455a..3abf70de 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -307,6 +307,10 @@ void send_level_up(std::shared_ptr c); void send_give_experience(std::shared_ptr c, uint32_t amount); void send_set_exp_multiplier(std::shared_ptr l); void send_rare_enemy_index_list(std::shared_ptr c, const std::vector& indexes); + +void send_quest_function_call(Channel& ch, uint16_t function_id); +void send_quest_function_call(std::shared_ptr c, uint16_t function_id); + void send_ep3_card_list_update(std::shared_ptr c); void send_ep3_media_update( std::shared_ptr c,