diff --git a/TODO.md b/TODO.md index 149422b1..cdbddeb4 100644 --- a/TODO.md +++ b/TODO.md @@ -35,10 +35,9 @@ - Find any remaining mismatches in enemy indexes / experience - Fix some edge cases on the BB proxy server (e.g. Change Ship) - Implement less-common subcommands - - 6xAC: Sort inventory - - 6xC1, 6xC2, 6xCD, 6xCE - - 6xCC: Exchange item for team points - 6xD8: Add S-rank weapon special - - 6xE1: Gallon's Plan quest - Implement teams + - All EA subcommands + - 6xC1, 6xC2, 6xCD, 6xCE: Team invites/administration + - 6xCC: Exchange item for team points - Implement story progress flags for unlocking quests diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 15aa177d..7692b084 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -858,8 +858,9 @@ struct SC_GameGuardCheck_BB_0022 { struct S_ExchangeSecretLotteryTicketResult_BB_24 { // These fields map to unknown_a1 and unknown_a2 in the 6xDE command (but // their order is swapped here). - le_uint16_t unknown_a2 = 0; - le_uint16_t unknown_a1 = 0; + le_uint16_t function_id = 0; + uint8_t start_index = 0; + uint8_t unused = 0; parray unknown_a3; } __packed__; @@ -867,7 +868,7 @@ struct S_ExchangeSecretLotteryTicketResult_BB_24 { // Sent in response to a 6xE1 command from the client. struct S_GallonPlanResult_BB_25 { - le_uint16_t unknown_a1 = 0; + le_uint16_t function_id = 0; uint8_t offset1 = 0; uint8_t offset2 = 0; uint8_t value1 = 0; @@ -5695,7 +5696,7 @@ struct G_ExchangeItemInQuest_BB_6xDB { G_ClientIDHeader header; le_uint32_t unknown_a1 = 0; le_uint32_t item_id = 0; - le_uint32_t unknown_a3 = 0; + le_uint32_t amount = 0; } __packed__; // 6xDC: Saint-Million boss actions (BB) @@ -5719,9 +5720,9 @@ struct G_SetEXPMultiplier_BB_6xDD { struct G_ExchangeSecretLotteryTicket_BB_6xDE { G_ClientIDHeader header; - uint8_t unknown_a1 = 0; - uint8_t unknown_a2 = 0; - le_uint16_t unknown_a3 = 0; + uint8_t index = 0; + uint8_t function_id1 = 0; + le_uint16_t function_id2 = 0; } __packed__; // 6xDF: Exchange Photon Crystals (BB; handled by server) @@ -5744,17 +5745,17 @@ struct G_RequestItemDropFromQuest_BB_6xE0 { le_float z = 0.0f; // argsA[2] } __packed__; -// 6xE1: Gallon's Plan quest (BB; handled by server) +// 6xE1: Exchange Photon Tickets (BB; handled by server) // The client sends this when it executes an F95F quest opcode. -struct G_GallonsPlanQuestActions_BB_6xE1 { +struct G_ExchangePhotonTickets_BB_6xE1 { G_ClientIDHeader header; - uint8_t unknown_a1 = 0; - uint8_t unknown_a2 = 0; - uint8_t unknown_a3 = 0; + uint8_t unknown_a1 = 0; // argsA[0] + uint8_t unknown_a2 = 0; // argsA[1] + uint8_t result_index = 0; // argsA[2] uint8_t unused = 0; - le_uint16_t unknown_a4 = 0; - le_uint16_t unknown_a5 = 0; + le_uint16_t function_id1 = 0; // argsA[3] + le_uint16_t unknown_a5 = 0; // argsA[4] } __packed__; // 6xE2: Coren actions (BB) diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 68289561..a6e818d9 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2344,7 +2344,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr c, uint8_t, exchange_cmd.header.client_id = c->lobby_client_id; exchange_cmd.unknown_a1 = 1; exchange_cmd.item_id = slt_item_id; - exchange_cmd.unknown_a3 = 1; + exchange_cmd.amount = 1; send_command_t(c, 0x60, 0x00, exchange_cmd); send_destroy_item(c, slt_item_id, 1); @@ -2358,8 +2358,8 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr c, uint8_t, } S_ExchangeSecretLotteryTicketResult_BB_24 out_cmd; - out_cmd.unknown_a1 = cmd.unknown_a1; - out_cmd.unknown_a2 = cmd.unknown_a2; + out_cmd.start_index = cmd.index; + out_cmd.function_id = cmd.function_id1; if (s->secret_lottery_results.empty()) { out_cmd.unknown_a3.clear(0); } else if (s->secret_lottery_results.size() == 1) { @@ -2415,6 +2415,45 @@ static void on_quest_F95E_result_bb(shared_ptr c, uint8_t, uint8_t, cons } } +static void on_quest_F95F_result_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->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { + const auto& cmd = check_size_t(data, size); + auto s = c->require_server_state(); + auto p = c->game_data.character(); + + const auto& result = s->quest_F95F_results.at(cmd.result_index); + if (result.second.empty()) { + throw runtime_error("invalid result index"); + } + + size_t index = p->inventory.find_item_by_primary_identifier(0x031004); // Photon Ticket + auto ticket_item = p->remove_item(p->inventory.items[index].data.id, result.first, false); + // 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); + + ItemData new_item = result.second; + new_item.id = l->generate_item_id(c->lobby_client_id); + p->add_item(new_item); + send_create_inventory_item(c, new_item); + + S_GallonPlanResult_BB_25 out_cmd; + out_cmd.function_id = cmd.function_id1; + out_cmd.offset1 = 0x3C; + out_cmd.offset2 = 0x08; + out_cmd.value1 = 0x00; + out_cmd.value2 = cmd.result_index; + send_command_t(c, 0x25, 0x00, out_cmd); + } +} + 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->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)) { @@ -2733,7 +2772,7 @@ subcommand_handler_t subcommand_handlers[0x100] = { /* 6xDE */ on_secret_lottery_ticket_exchange_bb, /* 6xDF */ on_photon_crystal_exchange_bb, /* 6xE0 */ on_quest_F95E_result_bb, - /* 6xE1 */ nullptr, + /* 6xE1 */ on_quest_F95F_result_bb, /* 6xE2 */ nullptr, /* 6xE3 */ nullptr, /* 6xE4 */ nullptr, diff --git a/src/ServerState.cc b/src/ServerState.cc index 21f59bb9..c9fe8468 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -703,7 +703,7 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { try { this->quest_F95E_results.clear(); - for (const auto& type_it : json.get_list("QuestF95ERewardItems")) { + for (const auto& type_it : json.get_list("QuestF95EResultItems")) { auto& type_res = this->quest_F95E_results.emplace_back(); for (const auto& difficulty_it : type_it->as_list()) { auto& difficulty_res = type_res.emplace_back(); @@ -715,7 +715,16 @@ void ServerState::parse_config(const JSON& json, bool is_reload) { } } catch (const out_of_range&) { } - + try { + this->quest_F95F_results.clear(); + for (const auto& it : json.get_list("QuestF95FResultItems")) { + auto& list = it->as_list(); + size_t price = list.at(0)->as_int(); + string data = parse_data_string(list.at(1)->as_string()); + this->quest_F95F_results.emplace_back(make_pair(price, ItemData::from_data(data))); + } + } catch (const out_of_range&) { + } try { this->secret_lottery_results.clear(); for (const auto& it : json.get_list("SecretLotteryResultItems")) { diff --git a/src/ServerState.hh b/src/ServerState.hh index eb8307de..9423ff77 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -115,6 +115,7 @@ struct ServerState : public std::enable_shared_from_this { // Indexed as [type][difficulty][random_choice] std::vector>> quest_F95E_results; + std::vector> quest_F95F_results; // [(num_photon_tickets, item)] std::vector secret_lottery_results; uint16_t bb_global_exp_multiplier; diff --git a/system/config.example.json b/system/config.example.json index 184a7e19..1403605d 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -507,14 +507,14 @@ [0x40, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"], ], - // Quest reward item definitions for opcode F95E (used in Black Paper's + // Quest result item definitions for opcode F95E (used in Black Paper's // Dangerous Deal 1 and 2). This list is indexed as [reward_type][difficulty]. // Reward types 0, 1, 2, and 4 are used by vanilla PSOBB; other reward types // can be specified when using the F95E quest opcode in custom quests. Rewards // are chosen uniformly at random from the list corresponding to the reward // location and difficulty level. All items in these lists must be hex item // codes (1-16 bytes); textual descriptions are not supported here. - "QuestF95ERewardItems": [ + "QuestF95EResultItems": [ [ ["009000", "009001", "009002", "009003", "009004", "009005", "009006", "009007", "009008", "00B400", "01014E", "010307", "010341", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000"], ["00B900", "003400", "000901", "009002", "009007", "002C00", "002D00", "010235", "000106", "000105", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000"], @@ -537,7 +537,16 @@ ["00BA00", "00B400", "000D03", "00B600", "00B300", "000708", "004301", "00C900", "010136", "01028A", "010299", "010285", "010348", "010351", "01035B", "010352"], ], ], - // Reward item definitions for Secret Lottery Ticket exchange (quest opcode + // Results for quest opcode F95F (used in Gallon's Plan). This list is indexed + // by type (which is the third argument to the opcode). The entries here are + // [num_photon_tickets, item_hex]. + "QuestF95FResultItems": [ + [0, ""], // Unused + [10, "00D500"], + [15, "000A07"], + [20, "010157"], + ], + // Result item definitions for Secret Lottery Ticket exchange (quest opcode // F95C, used in the Good Luck quest). "SecretLotteryResultItems": [ "000106", "000107", "000206", "000407", "000606", "000807", "000D01", diff --git a/tests/config.json b/tests/config.json index d06f9840..ab36e551 100644 --- a/tests/config.json +++ b/tests/config.json @@ -146,7 +146,7 @@ [0x40, "download-ep3", "Download", "$E$C6Quests to download\nto your Memory Card"], ], - "QuestF95ERewardItems": [ + "QuestF95EResultItems": [ [ ["009000", "009001", "009002", "009003", "009004", "009005", "009006", "009007", "009008", "00B400", "01014E", "010307", "010341", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000"], ["00B900", "003400", "000901", "009002", "009007", "002C00", "002D00", "010235", "000106", "000105", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000", "040000"], @@ -169,6 +169,12 @@ ["00BA00", "00B400", "000D03", "00B600", "00B300", "000708", "004301", "00C900", "010136", "01028A", "010299", "010285", "010348", "010351", "01035B", "010352"], ], ], + "QuestF95FResultItems": [ + [0, ""], // Unused + [10, "00D500"], + [15, "000A07"], + [20, "010157"], + ], "SecretLotteryResultItems": [ "000106", "000107", "000206", "000407", "000606", "000807", "000D01", "001300", "002000", "002700", "002C00", "003400", "003900", "003C00",