make quest item exchange implementations more complete

This commit is contained in:
Martin Michelsen
2025-12-26 19:54:22 -08:00
parent cd0d13e98c
commit 7ab3175f80
5 changed files with 85 additions and 54 deletions
+23 -20
View File
@@ -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
+7 -3
View File
@@ -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
// <meseta_slot_prize> replacement token in message strings. The status of this can be checked with
+31 -31
View File
@@ -5058,16 +5058,9 @@ static asio::awaitable<void> on_secret_lottery_ticket_exchange_bb(shared_ptr<Cli
uint32_t slt_item_id = p->inventory.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<void> on_quest_F95F_result_bb(shared_ptr<Client> 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<void> on_momoka_item_exchange_bb(shared_ptr<Client> 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);
+16
View File
@@ -3307,6 +3307,22 @@ void send_quest_function_call(shared_ptr<Client> c, uint16_t label) {
send_quest_function_call(c->channel, label);
}
void send_gallon_plan_result(
shared_ptr<Client> 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<Client> c) {
if (!c->check_flag(Client::Flag::HAS_EP3_CARD_DEFS)) {
auto s = c->require_server_state();
+8
View File
@@ -380,6 +380,14 @@ void send_rare_enemy_index_list(std::shared_ptr<Client> c, const std::vector<siz
void send_quest_function_call(std::shared_ptr<Channel> ch, uint16_t label);
void send_quest_function_call(std::shared_ptr<Client> c, uint16_t label);
void send_gallon_plan_result(
std::shared_ptr<Client> 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<Client> c);
void send_ep3_media_update(
std::shared_ptr<Client> c, uint32_t type, uint32_t which, const std::string& compressed_data);