diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 0b7b57f9..75c9afb8 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -814,19 +814,20 @@ struct SC_GameGuardCheck_BB_0022 { // used in the case described above; there are no other conditions that cause it to be sent. // 23 (S->C): Momoka Item Exchange result (BB) -// Sent in response to a 6xD9 command from the client. This command is not valid on BB Trial Edition. header.flag -// indicates the result code: +// This command is not valid on BB Trial Edition. +// Sent in response to a 6xD9 command from the client. header.flag indicates the result code: // 0 = success // 1 = currency item not found // 2 = inventory is full // Anything else = generic failure // 24 (S->C): Secret Lottery Ticket exchange result (BB) +// This command is not valid on BB Trial Edition. // Sent in response to a 6xDE command from the client. The client sets 8 sequential quest registers, starting with // start_reg_num, to the values specified in reg_values. Then it starts a new quest thread at the specified label. -// header.flag indicates whether the client had any Secret Lottery Tickets in their inventory (and hence could -// participate): 0 means success, 1 means failure. However, this value is unused by the client. -// This command is not valid on BB Trial Edition. +// According to logs from Sega's servers, header.flag indicates whether the client had any Secret Lottery Tickets in +// their inventory (and hence could participate): 0 means success, 1 means failure. However, this value is unused by +// the client. struct S_ExchangeSecretLotteryTicketResult_BB_24 { le_uint16_t label = 0; @@ -836,16 +837,17 @@ struct S_ExchangeSecretLotteryTicketResult_BB_24 { } __packed_ws__(S_ExchangeSecretLotteryTicketResult_BB_24, 0x24); // 25 (S->C): Gallon's Plan result (BB) -// Sent in response to a 6xE1 command from the client. The client sets the quest registers reg_num1 and reg_num2 to -// value1 and value2 respectively, then starts a new quest thread at the specified label. // This command is not valid on BB Trial Edition. +// Sent in response to a 6xE1 command from the client. The client sets the quest registers result_code_reg and +// result_index_reg to result_code_value and result_index_value respectively, then starts a new quest thread at the +// specified label. struct S_GallonPlanResult_BB_25 { le_uint16_t label = 0; - uint8_t reg_num1 = 0; - uint8_t reg_num2 = 0; - uint8_t value1 = 0; - uint8_t value2 = 0; + uint8_t result_code_reg = 0; + uint8_t result_index_reg = 0; + uint8_t result_code_value = 0; // See description of F95F (bb_exchange_pt) in QuestScript.cc for values here + uint8_t result_index_value = 0; le_uint16_t unused = 0; } __packed_ws__(S_GallonPlanResult_BB_25, 8); @@ -3969,7 +3971,7 @@ struct G_FeedMag_6x28 { le_uint32_t fed_item_id = 0; } __packed_ws__(G_FeedMag_6x28, 0x0C); -// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) (protected on GC NTE/V3 but not on V4) +// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) (protected on GC NTE/V3 but not on BB) // This subcommand is also used for reducing the size of stacks - if amount is less than the stack count, the item is // not deleted and its ID remains valid. @@ -5957,16 +5959,17 @@ struct G_UpgradeWeaponAttribute_BB_6xDA { le_uint16_t failure_label = 0; // labelH } __packed_ws__(G_UpgradeWeaponAttribute_BB_6xDA, 0x2C); -// 6xDB: Exchange item in quest (BB) +// 6xDB: Extended delete inventory item (BB) -struct G_ExchangeItemInQuest_BB_6xDB { +struct G_ExtendedDeleteInventoryItem_BB_6xDB { G_ClientIDHeader header; - // If this is 0, the command is identical to 6x29. If this is 1, a function similar to find_item_by_id is called - // instead of find_item_by_id, but I don't yet know what exactly the logic differences are (TODO). - le_uint32_t unknown_a1 = 0; + // If exclude_wrapped is 0, the command is identical to 6x29; if this is 1, the command won't delete any item which + // is wrapped. This seems like an odd feature; shouldn't we expect that the server and client have the same item + // state, so the server would already know if the item was wrapped or not before sending this? + le_uint32_t exclude_wrapped = 0; le_uint32_t item_id = 0; le_uint32_t amount = 0; -} __packed_ws__(G_ExchangeItemInQuest_BB_6xDB, 0x10); +} __packed_ws__(G_ExtendedDeleteInventoryItem_BB_6xDB, 0x10); // 6xDC: Saint-Milion/Shambertin/Kondrieu boss actions (BB) @@ -6027,8 +6030,8 @@ struct G_RequestItemDropFromQuest_BB_6xE0 { struct G_ExchangePhotonTickets_BB_6xE1 { G_ClientIDHeader header; - uint8_t unknown_a1 = 0; // valueA - uint8_t unknown_a2 = 0; // valueB + uint8_t result_code_reg = 0; // valueA + uint8_t result_index_reg = 0; // valueB uint8_t result_index = 0; // valueC uint8_t unused = 0; le_uint16_t success_label = 0; // valueD diff --git a/src/QuestScript.cc b/src/QuestScript.cc index 934d32c4..6545916f 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -2587,12 +2587,16 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF95E, "bb_box_create_bp", "BB_box_create_BP", {I32, FLOAT32, FLOAT32}, F_V4 | F_ARGS}, // Requests an exchange of Photon Tickets for items. Sends 6xE1. - // valueA = unknown_a1 - // valueB = unknown_a2 + // regA = result code reg (set to result code upon server response); the result codes are: + // 0 = success + // 1 = player doesn't have enough Photon Tickets + // 2 = inventory is full + // 3, 4, or 5 = "server send error" (from Gallon's Plan script; newserv never sends these) + // regB = result index reg (set to valueC upon server response) // valueC = result index (index into QuestF95FResultItems in config.json) // labelD = label to call on success // labelE = label to call on failure - {0xF95F, "bb_exchange_pt", "BB_exchage_PT", {I32, I32, I32, I32, I32}, F_V4 | F_ARGS}, + {0xF95F, "bb_exchange_pt", "BB_exchage_PT", {W_REG32, W_REG32, I32, I32, I32}, F_V4 | F_ARGS}, // Requests a prize from the Meseta gambling prize list. Sends 6xE2. The server responds with 6xE3, which sets the // replacement token in message strings. The status of this can be checked with diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 2c52a035..5f9aa3fc 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -5058,16 +5058,9 @@ static asio::awaitable on_secret_lottery_ticket_exchange_bb(shared_ptrinventory.items[currency_index].data.id; - G_ExchangeItemInQuest_BB_6xDB exchange_cmd; - exchange_cmd.header.subcommand = 0xDB; - exchange_cmd.header.size = 4; - exchange_cmd.header.client_id = c->lobby_client_id; - exchange_cmd.unknown_a1 = 1; - exchange_cmd.item_id = slt_item_id; - exchange_cmd.amount = 1; - send_command_t(c, 0x60, 0x00, exchange_cmd); - + // Note: It seems Sega used 6xDB here; we use 6x29 instead. p->remove_item(slt_item_id, 1, limits); + send_destroy_item_to_lobby(c, slt_item_id, 1); item.id = l->generate_item_id(c->lobby_client_id); p->add_item(item, limits); @@ -5163,30 +5156,38 @@ static asio::awaitable on_quest_F95F_result_bb(shared_ptr c, Subco } const auto& limits = *s->item_stack_limits(c->version()); - size_t index = p->inventory.find_item_by_primary_identifier(0x03100400); // Photon Ticket - auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, limits); - // TODO: Shouldn't we send a 6x29 here? Check if this causes desync in an actual game - G_ExchangeItemInQuest_BB_6xDB cmd_6xDB; - cmd_6xDB.header = {0xDB, 0x04, c->lobby_client_id}; - cmd_6xDB.unknown_a1 = 1; - cmd_6xDB.item_id = ticket_item.id; - cmd_6xDB.amount = result.first; - send_command_t(c, 0x60, 0x00, cmd_6xDB); + bool failed = false; + ItemData ticket_item; + try { + size_t index = p->inventory.find_item_by_primary_identifier(0x03100400); // Photon Ticket + ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, limits); + } catch (const out_of_range&) { + failed = true; + } + if (failed) { + send_gallon_plan_result(c, cmd.failure_label, cmd.result_code_reg, 1, cmd.result_index_reg, cmd.result_index); + co_return; + } ItemData new_item = result.second; - 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); + try { + new_item.enforce_stack_size_limits(limits); + new_item.id = l->generate_item_id(c->lobby_client_id); + p->add_item(new_item, limits); + } catch (const out_of_range&) { + failed = true; + } + if (failed) { + p->add_item(ticket_item, limits); + send_gallon_plan_result(c, cmd.failure_label, cmd.result_code_reg, 2, cmd.result_index_reg, cmd.result_index); + co_return; + } - S_GallonPlanResult_BB_25 out_cmd; - out_cmd.label = cmd.success_label; - out_cmd.reg_num1 = 0x3C; - out_cmd.reg_num2 = 0x08; - out_cmd.value1 = 0x00; - out_cmd.value2 = cmd.result_index; - send_command_t(c, 0x25, 0x00, out_cmd); + // Note: It seems Sega used 6xDB here; we use 6x29 instead. + send_destroy_item_to_lobby(c, ticket_item.id, result.first); + send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); + send_gallon_plan_result(c, cmd.success_label, cmd.result_code_reg, 0, cmd.result_index_reg, cmd.result_index); co_return; } @@ -5325,8 +5326,7 @@ static asio::awaitable on_momoka_item_exchange_bb(shared_ptr c, Su co_return; } - 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); + // Note: It seems Sega used 6xDB here; we use 6x29 instead. send_destroy_item_to_lobby(c, found_item.id, 1); send_create_inventory_item_to_lobby(c, c->lobby_client_id, new_item); send_command(c, 0x23, 0x00); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 9f0a2b3d..9268de87 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -3307,6 +3307,22 @@ void send_quest_function_call(shared_ptr c, uint16_t label) { send_quest_function_call(c->channel, label); } +void send_gallon_plan_result( + shared_ptr c, + uint16_t label, + uint8_t result_code_reg, + uint32_t result_code, + uint8_t result_index_reg, + uint32_t result_index) { + S_GallonPlanResult_BB_25 cmd; + cmd.label = label; + cmd.result_code_reg = result_code_reg; + cmd.result_index_reg = result_index_reg; + cmd.result_code_value = result_code; + cmd.result_index_value = result_index; + send_command_t(c, 0x25, 0x00, cmd); +} + void send_ep3_card_list_update(shared_ptr c) { if (!c->check_flag(Client::Flag::HAS_EP3_CARD_DEFS)) { auto s = c->require_server_state(); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index e6553fcd..2acc8dce 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -380,6 +380,14 @@ void send_rare_enemy_index_list(std::shared_ptr c, const std::vector ch, uint16_t label); void send_quest_function_call(std::shared_ptr c, uint16_t label); +void send_gallon_plan_result( + std::shared_ptr c, + uint16_t label, + uint8_t result_code_reg, + uint32_t result_code, + uint8_t result_index_reg, + uint32_t result_index); + void send_ep3_card_list_update(std::shared_ptr c); void send_ep3_media_update( std::shared_ptr c, uint32_t type, uint32_t which, const std::string& compressed_data);