fix item ID inconsistencies between server and client due to shops and tekker in BB
This commit is contained in:
@@ -5312,7 +5312,7 @@ struct G_ShopContents_BB_6xB6 {
|
||||
|
||||
struct G_BuyShopItem_BB_6xB7 {
|
||||
G_UnusedHeader header;
|
||||
le_uint32_t inventory_item_id = 0;
|
||||
le_uint32_t shop_item_id = 0;
|
||||
uint8_t shop_type = 0;
|
||||
uint8_t item_index = 0;
|
||||
uint8_t amount = 0;
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ void player_use_item(shared_ptr<Client> c, size_t item_index) {
|
||||
|
||||
auto l = c->lobby.lock();
|
||||
if (l) {
|
||||
send_create_inventory_item(c, item.data);
|
||||
send_create_inventory_item(c, item.data, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
+1
-9
@@ -196,15 +196,7 @@ void Lobby::add_client(shared_ptr<Client> c, ssize_t required_client_id) {
|
||||
// If the lobby is a game and item tracking is enabled, assign the inventory's
|
||||
// item IDs
|
||||
if (this->is_game() && this->check_flag(Lobby::Flag::ITEM_TRACKING_ENABLED)) {
|
||||
auto s = this->require_server_state();
|
||||
auto p = c->game_data.character();
|
||||
auto& inv = p->inventory;
|
||||
size_t count = min<uint8_t>(inv.num_items, 30);
|
||||
for (size_t x = 0; x < count; x++) {
|
||||
inv.items[x].data.id = this->generate_item_id(c->lobby_client_id);
|
||||
}
|
||||
this->log.info("Assigned item IDs for joining player %zd", index);
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
this->assign_inventory_item_ids(c);
|
||||
}
|
||||
|
||||
// If the lobby is recording a battle record, add the player join event
|
||||
|
||||
@@ -472,7 +472,6 @@ struct DATParserRandomState {
|
||||
size_t rand_int_biased(size_t min_v, size_t max_v) {
|
||||
float max_f = static_cast<float>(max_v + 1);
|
||||
uint32_t crypt_v = this->random.next();
|
||||
fprintf(stderr, "(global) => %08" PRIX32 "\n", crypt_v);
|
||||
float det_f = static_cast<float>(crypt_v);
|
||||
return max<size_t>(floorf((max_f * det_f) / UINT32_MAX_AS_FLOAT), min_v);
|
||||
}
|
||||
@@ -517,7 +516,6 @@ struct DATParserRandomState {
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
for (size_t x = 0; x < sec.count; x++) {
|
||||
uint32_t crypt_v = this->location_table_random.next();
|
||||
fprintf(stderr, "(local?) => %08" PRIX32 "\n", crypt_v);
|
||||
size_t choice = floorf((static_cast<float>(sec.count) * static_cast<float>(crypt_v)) / UINT32_MAX_AS_FLOAT);
|
||||
uint32_t t = this->location_index_table[x];
|
||||
this->location_index_table[x] = this->location_index_table[choice];
|
||||
|
||||
+90
-86
@@ -228,78 +228,82 @@ static void on_sync_joining_player_item_state(shared_ptr<Client> c, uint8_t comm
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-V3 versions, just forward the data verbatim. For V3, we need to
|
||||
// byteswap mags' data2 fields if exactly one of the sender and recipient is
|
||||
// PSO GC
|
||||
bool sender_is_gc = (c->version() == GameVersion::GC);
|
||||
if (!sender_is_gc && (c->version() != GameVersion::XB)) {
|
||||
const auto& cmd = check_size_t<G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E>(data, size, 0xFFFF);
|
||||
if (cmd.compressed_size > size - sizeof(cmd)) {
|
||||
throw runtime_error("compressed end offset is beyond end of command");
|
||||
}
|
||||
|
||||
string decompressed = bc0_decompress(reinterpret_cast<const char*>(data) + sizeof(cmd), cmd.compressed_size);
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
c->log.info("Decompressed item sync data (%" PRIX32 " -> %zX bytes; expected %" PRIX32 "):",
|
||||
cmd.compressed_size.load(), decompressed.size(), cmd.decompressed_size.load());
|
||||
print_data(stderr, decompressed);
|
||||
}
|
||||
|
||||
if (decompressed.size() < sizeof(G_SyncItemState_6x6D_Decompressed)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed 6x6D data (0x%zX bytes) is too short for header (0x%zX bytes)",
|
||||
decompressed.size(), sizeof(G_SyncItemState_6x6D_Decompressed)));
|
||||
}
|
||||
auto* decompressed_cmd = reinterpret_cast<G_SyncItemState_6x6D_Decompressed*>(decompressed.data());
|
||||
|
||||
size_t num_floor_items = 0;
|
||||
for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_floor.size(); z++) {
|
||||
num_floor_items += decompressed_cmd->floor_item_count_per_floor[z];
|
||||
}
|
||||
|
||||
size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(FloorItem);
|
||||
if (decompressed.size() < required_size) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed 6x6D data (0x%zX bytes) is too short for all floor items (0x%zX bytes)",
|
||||
decompressed.size(), required_size));
|
||||
}
|
||||
auto* floor_items = reinterpret_cast<FloorItem*>(decompressed.data() + sizeof(G_SyncItemState_6x6D_Decompressed));
|
||||
|
||||
size_t target_num_items = target->game_data.character()->inventory.num_items;
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
uint32_t client_next_id = decompressed_cmd->next_item_id_per_player[z];
|
||||
uint32_t server_next_id = l->next_item_id[z];
|
||||
if (client_next_id == server_next_id) {
|
||||
l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value", z, l->next_item_id[z]);
|
||||
} else if ((z == target->lobby_client_id) && (client_next_id == server_next_id - target_num_items)) {
|
||||
l->log.info("Next item ID for player %zu (%08" PRIX32 ") matches expected value before inventory item ID assignment (%08" PRIX32 ")", z, l->next_item_id[z], static_cast<uint32_t>(server_next_id - target_num_items));
|
||||
} else {
|
||||
l->log.warning("Next item ID for player %zu (%08" PRIX32 ") does not match expected value (%08" PRIX32 ")",
|
||||
z, decompressed_cmd->next_item_id_per_player[z].load(), l->next_item_id[z]);
|
||||
}
|
||||
}
|
||||
|
||||
// We need to byteswap mags' data2 fields if exactly one of the sender and
|
||||
// recipient is PSO GC
|
||||
if ((c->version() == GameVersion::GC) == (target->version() == GameVersion::GC)) {
|
||||
send_or_enqueue_joining_player_command(target, command, flag, data, size);
|
||||
|
||||
} else {
|
||||
bool target_is_gc = (target->version() == GameVersion::GC);
|
||||
|
||||
if (target_is_gc == sender_is_gc) {
|
||||
send_or_enqueue_joining_player_command(target, command, flag, data, size);
|
||||
|
||||
} else {
|
||||
const auto& cmd = check_size_t<G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E>(data, size, 0xFFFF);
|
||||
if (cmd.compressed_size > size - sizeof(cmd)) {
|
||||
throw runtime_error("compressed end offset is beyond end of command");
|
||||
}
|
||||
|
||||
string decompressed = bc0_decompress(reinterpret_cast<const char*>(data) + sizeof(cmd), cmd.compressed_size);
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
c->log.info("Decompressed item sync data (%" PRIX32 " -> %zX bytes; expected %" PRIX32 "):",
|
||||
cmd.compressed_size.load(), decompressed.size(), cmd.decompressed_size.load());
|
||||
print_data(stderr, decompressed);
|
||||
}
|
||||
|
||||
if (decompressed.size() < sizeof(G_SyncItemState_6x6D_Decompressed)) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed 6x6D data (0x%zX bytes) is too short for header (0x%zX bytes)",
|
||||
decompressed.size(), sizeof(G_SyncItemState_6x6D_Decompressed)));
|
||||
}
|
||||
auto* decompressed_cmd = reinterpret_cast<G_SyncItemState_6x6D_Decompressed*>(decompressed.data());
|
||||
|
||||
size_t num_floor_items = 0;
|
||||
for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_floor.size(); z++) {
|
||||
num_floor_items += decompressed_cmd->floor_item_count_per_floor[z];
|
||||
}
|
||||
|
||||
size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(FloorItem);
|
||||
if (decompressed.size() < required_size) {
|
||||
throw runtime_error(string_printf(
|
||||
"decompressed 6x6D data (0x%zX bytes) is too short for all items (0x%zX bytes)",
|
||||
decompressed.size(), required_size));
|
||||
}
|
||||
auto* floor_items = reinterpret_cast<FloorItem*>(decompressed.data() + sizeof(G_SyncItemState_6x6D_Decompressed));
|
||||
|
||||
for (size_t z = 0; z < num_floor_items; z++) {
|
||||
// NOTE: If we use this codepath for non-V3 in the future, we'll need to
|
||||
// change this hardcoded version. This only works because GC's mag
|
||||
// encoding/decoding is symmetric (encode and decode do the same thing).
|
||||
floor_items[z].item.decode_for_version(GameVersion::GC);
|
||||
}
|
||||
|
||||
string out_compressed_data = bc0_compress(decompressed);
|
||||
|
||||
G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E out_cmd;
|
||||
out_cmd.header.basic_header.subcommand = 0x6D;
|
||||
out_cmd.header.basic_header.size = 0x00;
|
||||
out_cmd.header.basic_header.unused = 0x0000;
|
||||
out_cmd.header.size = ((out_compressed_data.size() + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E)) + 3) & (~3);
|
||||
out_cmd.decompressed_size = decompressed.size();
|
||||
out_cmd.compressed_size = out_compressed_data.size();
|
||||
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
c->log.info("Byteswapped and recompressed item sync data (%zX bytes)", out_compressed_data.size());
|
||||
}
|
||||
|
||||
StringWriter w;
|
||||
w.write(&out_cmd, sizeof(out_cmd));
|
||||
w.write(out_compressed_data);
|
||||
send_or_enqueue_joining_player_command(target, command, flag, std::move(w.str()));
|
||||
auto s = target->require_server_state();
|
||||
for (size_t z = 0; z < num_floor_items; z++) {
|
||||
floor_items[z].item.decode_for_version(c->version());
|
||||
floor_items[z].item.encode_for_version(target->version(), s->item_parameter_table_for_version(target->version()));
|
||||
}
|
||||
|
||||
string out_compressed_data = bc0_compress(decompressed);
|
||||
|
||||
G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E out_cmd;
|
||||
out_cmd.header.basic_header.subcommand = 0x6D;
|
||||
out_cmd.header.basic_header.size = 0x00;
|
||||
out_cmd.header.basic_header.unused = 0x0000;
|
||||
out_cmd.header.size = ((out_compressed_data.size() + sizeof(G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E)) + 3) & (~3);
|
||||
out_cmd.decompressed_size = decompressed.size();
|
||||
out_cmd.compressed_size = out_compressed_data.size();
|
||||
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
c->log.info("Transcoded and recompressed item sync data (%zX bytes)", out_compressed_data.size());
|
||||
}
|
||||
|
||||
StringWriter w;
|
||||
w.write(&out_cmd, sizeof(out_cmd));
|
||||
w.write(out_compressed_data);
|
||||
send_or_enqueue_joining_player_command(target, command, flag, std::move(w.str()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1307,7 +1311,7 @@ static void on_open_shop_bb_or_ep3_battle_subs(shared_ptr<Client> c, uint8_t com
|
||||
throw runtime_error("invalid shop type");
|
||||
}
|
||||
for (auto& item : c->game_data.shop_contents[cmd.shop_type]) {
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
item.id = 0xFFFFFFFF;
|
||||
item.data2d = s->item_parameter_table_for_version(c->version())->price_for_item(item);
|
||||
}
|
||||
|
||||
@@ -1383,9 +1387,9 @@ static void on_ep3_private_word_select_bb_bank_action(shared_ptr<Client> c, uint
|
||||
|
||||
} else { // Take item
|
||||
auto item = p->bank.remove_item(cmd.item_id, cmd.item_amount);
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(item);
|
||||
send_create_inventory_item(c, item);
|
||||
send_create_inventory_item(c, item, true);
|
||||
|
||||
string name = s->item_name_index->describe_item(GameVersion::BB, item);
|
||||
l->log.info("Player %hu withdrew item %08" PRIX32 " (x%hhu) (%s) from the bank",
|
||||
@@ -1996,9 +2000,9 @@ void on_meseta_reward_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, const v
|
||||
ItemData item;
|
||||
item.data1[0] = 0x04;
|
||||
item.data2d = cmd.amount.load();
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(item);
|
||||
send_create_inventory_item(c, item);
|
||||
send_create_inventory_item(c, item, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2008,9 +2012,9 @@ void on_item_reward_request_bb(shared_ptr<Client> c, uint8_t, uint8_t, const voi
|
||||
|
||||
ItemData item;
|
||||
item = cmd.item_data;
|
||||
item.id = l->generate_item_id(0xFF);
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
c->game_data.character()->add_item(item);
|
||||
send_create_inventory_item(c, item);
|
||||
send_create_inventory_item(c, item, true);
|
||||
}
|
||||
|
||||
static void on_destroy_inventory_item(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
|
||||
@@ -2111,7 +2115,7 @@ static void on_accept_identify_item_bb(shared_ptr<Client> c, uint8_t command, ui
|
||||
throw runtime_error("accepted item ID does not match previous identify request");
|
||||
}
|
||||
c->game_data.character()->add_item(c->game_data.identify_result);
|
||||
send_create_inventory_item(c, c->game_data.identify_result);
|
||||
send_create_inventory_item(c, c->game_data.identify_result, false);
|
||||
c->game_data.identify_result.clear();
|
||||
|
||||
} else {
|
||||
@@ -2170,19 +2174,19 @@ static void on_buy_shop_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, const vo
|
||||
auto p = c->game_data.character();
|
||||
p->remove_meseta(price, false);
|
||||
|
||||
item.id = cmd.inventory_item_id;
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(item);
|
||||
send_create_inventory_item(c, item);
|
||||
send_create_inventory_item(c, item, true);
|
||||
|
||||
auto s = c->require_server_state();
|
||||
auto name = s->describe_item(c->version(), item, false);
|
||||
l->log.info("Player %hhu purchased item %08" PRIX32 " (%s) for %zu meseta",
|
||||
c->lobby_client_id, cmd.inventory_item_id.load(), name.c_str(), price);
|
||||
c->lobby_client_id, item.id.load(), name.c_str(), price);
|
||||
p->print_inventory(stderr, c->version(), s->item_name_index);
|
||||
if (c->config.check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
string name = s->describe_item(c->version(), item, true);
|
||||
send_text_message_printf(c, "$C5CREATE/BUY %08" PRIX32 "\n-%zu Meseta\n%s",
|
||||
cmd.inventory_item_id.load(), price, name.c_str());
|
||||
item.id.load(), price, name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2261,7 +2265,7 @@ static void on_quest_exchange_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, co
|
||||
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_create_inventory_item(c, new_item, true);
|
||||
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
|
||||
@@ -2282,7 +2286,7 @@ static void on_wrap_item_bb(shared_ptr<Client> c, uint8_t, uint8_t, const void*
|
||||
send_destroy_item(c, item.id, 1);
|
||||
item.wrap();
|
||||
p->add_item(cmd.item);
|
||||
send_create_inventory_item(c, item);
|
||||
send_create_inventory_item(c, item, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2303,7 +2307,7 @@ static void on_photon_drop_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, c
|
||||
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_create_inventory_item(c, new_item, true);
|
||||
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
|
||||
@@ -2350,7 +2354,7 @@ static void on_secret_lottery_ticket_exchange_bb(shared_ptr<Client> c, uint8_t,
|
||||
: s->secret_lottery_results[random_object<uint32_t>() % s->secret_lottery_results.size()];
|
||||
item.id = l->generate_item_id(c->lobby_client_id);
|
||||
p->add_item(item);
|
||||
send_create_inventory_item(c, item);
|
||||
send_create_inventory_item(c, item, true);
|
||||
}
|
||||
|
||||
S_ExchangeSecretLotteryTicketResult_BB_24 out_cmd;
|
||||
@@ -2430,7 +2434,7 @@ static void on_momoka_item_exchange_bb(shared_ptr<Client> c, uint8_t, uint8_t, c
|
||||
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_create_inventory_item(c, new_item, true);
|
||||
|
||||
send_command(c, 0x23, 0x00);
|
||||
} catch (const exception& e) {
|
||||
@@ -2484,7 +2488,7 @@ static void on_upgrade_weapon_attribute_bb(shared_ptr<Client> c, uint8_t, uint8_
|
||||
item.data1[attribute_index] += attribute_amount;
|
||||
|
||||
send_destroy_item(c, item.id, 1);
|
||||
send_create_inventory_item(c, item);
|
||||
send_create_inventory_item(c, item, false);
|
||||
send_quest_function_call(c, cmd.success_function_id);
|
||||
|
||||
} catch (const exception& e) {
|
||||
|
||||
+8
-1
@@ -2180,11 +2180,18 @@ void send_pick_up_item(shared_ptr<Client> c, uint32_t item_id, uint8_t floor) {
|
||||
send_command_t(l, 0x60, 0x00, cmd);
|
||||
}
|
||||
|
||||
void send_create_inventory_item(shared_ptr<Client> c, const ItemData& item) {
|
||||
void send_create_inventory_item(shared_ptr<Client> c, const ItemData& item, bool has_newest_item_id) {
|
||||
auto l = c->require_lobby();
|
||||
if (c->version() != GameVersion::BB) {
|
||||
throw logic_error("6xBE can only be sent to BB clients");
|
||||
}
|
||||
// This command consumes an item ID on the client even though it's never used,
|
||||
// because the passed-in item's ID overwrites it. If the passed-in ID was just
|
||||
// generated by calling l->generate_item_id, then we shouldn't waste one here,
|
||||
// but if not, we should (to keep our state in sync with the client).
|
||||
if (!has_newest_item_id) {
|
||||
l->generate_item_id(c->lobby_client_id);
|
||||
}
|
||||
uint16_t client_id = c->lobby_client_id;
|
||||
G_CreateInventoryItem_BB_6xBE cmd = {{0xBE, 0x07, client_id}, item, 0};
|
||||
send_command_t(l, 0x60, 0x00, cmd);
|
||||
|
||||
+1
-1
@@ -301,7 +301,7 @@ void send_drop_stacked_item(std::shared_ptr<ServerState> s, Channel& ch, const I
|
||||
void send_drop_stacked_item(std::shared_ptr<Lobby> l, const ItemData& item,
|
||||
uint8_t floor, float x, float z);
|
||||
void send_pick_up_item(std::shared_ptr<Client> c, uint32_t id, uint8_t floor);
|
||||
void send_create_inventory_item(std::shared_ptr<Client> c, const ItemData& item);
|
||||
void send_create_inventory_item(std::shared_ptr<Client> c, const ItemData& item, bool has_newest_item_id);
|
||||
void send_destroy_item(std::shared_ptr<Client> c, uint32_t item_id, uint32_t amount);
|
||||
void send_item_identify_result(std::shared_ptr<Client> c);
|
||||
void send_bank(std::shared_ptr<Client> c);
|
||||
|
||||
Reference in New Issue
Block a user