diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 090b7e47..e458a819 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -2483,7 +2483,7 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server // Note: PSO GC sends uninitialized data in the unused entries of this // command. newserv parses and regenerates the item data when sending D3, // which effectively erases the uninitialized data. - parray items; + parray item_datas; } __packed__; // D1 (S->C): Advance trade state (V3/BB) @@ -3750,11 +3750,10 @@ struct G_DropItem_6x2A { struct G_CreateInventoryItem_DC_6x2B { G_ClientIDHeader header; - ItemData item; + ItemData item_data; } __packed__; -struct G_CreateInventoryItem_PC_V3_BB_6x2B { - G_CreateInventoryItem_DC_6x2B basic_cmd; +struct G_CreateInventoryItem_PC_V3_BB_6x2B : G_CreateInventoryItem_DC_6x2B { uint8_t unused1; uint8_t unknown_a2; le_uint16_t unused2; @@ -4123,11 +4122,10 @@ struct G_DropStackedItem_DC_6x5D { le_uint16_t unused2; le_float x; le_float z; - ItemData data; + ItemData item_data; } __packed__; -struct G_DropStackedItem_PC_V3_BB_6x5D { - G_DropStackedItem_DC_6x5D basic_cmd; +struct G_DropStackedItem_PC_V3_BB_6x5D : G_DropStackedItem_DC_6x5D { le_uint32_t unused3; } __packed__; @@ -4135,7 +4133,7 @@ struct G_DropStackedItem_PC_V3_BB_6x5D { struct G_BuyShopItem_6x5E { G_ClientIDHeader header; - ItemData item; + ItemData item_data; } __packed__; // 6x5F: Drop item from box/enemy @@ -4149,11 +4147,10 @@ struct G_DropItem_DC_6x5F { le_float z; le_uint16_t unknown_a1; le_uint16_t unknown_a2; - ItemData data; + ItemData item_data; } __packed__; -struct G_DropItem_PC_V3_BB_6x5F { - G_DropItem_DC_6x5F basic_cmd; +struct G_DropItem_PC_V3_BB_6x5F : G_DropItem_DC_6x5F { le_uint32_t unused3; } __packed__; @@ -4307,10 +4304,9 @@ struct G_SyncItemState_6x6D_Decompressed { // item is dropped. The last item dropped in each area has drop_number equal // to total_items_dropped_per_area[area - 1] - 1. le_uint16_t drop_number; - ItemData data; + ItemData item_data; } __packed__; - // Variable-length field follows: - // FloorItem items[sum(floor_item_count_per_area)]; + FloorItem items[0]; // sum(floor_item_count_per_area) of them, actually } __packed__; // 6x6E: Sync flag state (used while loading into game) @@ -4340,7 +4336,7 @@ struct G_Unknown_6x6F { // Annoyingly, they didn't use the same format as the 65/67/68 commands here, // and instead rearranged a bunch of things. -struct G_SyncPlayerDispAndInventory_6x70 { +struct G_SyncPlayerDispAndInventory_V3_6x70 { G_ExtendedHeader header; // Offsets in this struct are relative to the overall command header /* 000C */ parray unknown_a1; @@ -5025,7 +5021,7 @@ struct G_ShopContents_BB_6xB6 { uint8_t num_items; le_uint16_t unused; // Note: data2d of these entries should be the price - ItemData entries[20]; + ItemData item_datas[20]; } __packed__; // 6xB7: Alias for 6xB3 (Episode 3 Trial Edition) @@ -5062,7 +5058,7 @@ struct G_AcceptItemIdentification_BB_6xB8 { struct G_IdentifyResult_BB_6xB9 { G_ClientIDHeader header; - ItemData item; + ItemData item_data; } __packed__; // 6xBA: Sync card trade state (Episode 3) @@ -5165,7 +5161,7 @@ struct G_SoundChat_GC_Ep3_6xBE { struct G_CreateInventoryItem_BB_6xBE { G_ClientIDHeader header; - ItemData item; + ItemData item_data; le_uint32_t unused; } __packed__; @@ -5344,7 +5340,7 @@ struct G_SetEXPMultiplier_BB_6xDD { struct G_Unknown_BB_6xE3 { G_ClientIDHeader header; - ItemData unknown_a1; + ItemData item_data; } __packed__; // 6xE4: Invalid subcommand diff --git a/src/Episode3/BattleRecord.cc b/src/Episode3/BattleRecord.cc index 82a514c7..193bcb4b 100644 --- a/src/Episode3/BattleRecord.cc +++ b/src/Episode3/BattleRecord.cc @@ -314,7 +314,7 @@ void BattleRecordPlayer::schedule_events() { if (this->event_it == this->record->events.end()) { if (relative_ts >= this->record->battle_end_timestamp) { - // If the record is complete and the end timestamp has been reached, so + // If the record is complete and the end timestamp has been reached, // send exit commands to all players in the lobby, and don't reschedule // the event (it will be deleted along with the Player when the lobby is // destroyed, when the last client leaves) diff --git a/src/ItemData.cc b/src/ItemData.cc index 569263fe..77f24c8d 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -41,6 +41,12 @@ void ItemData::clear() { this->data2d = 0; } +void ItemData::bswap_data2_if_mag() { + if (this->data1[0] == 0x02) { + this->data2d = bswap32(this->data2d); + } +} + bool ItemData::empty() const { return (this->data1d[0] == 0) && (this->data1d[1] == 0) && diff --git a/src/ItemData.hh b/src/ItemData.hh index 8cb5ef7a..6abd7f76 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -78,6 +78,10 @@ struct ItemData { // 0x14 bytes // W = photon blasts // Y = mag synchro // Z = item ID + // Note: PSO GC erroneously byteswaps data2 even when the item is a mag. This + // makes it incompatible with little-endian versions of PSO (i.e. all other + // versions). We manually byteswap data2 upon receipt and immediately before + // sending where needed. union { parray data1; @@ -101,6 +105,8 @@ struct ItemData { // 0x14 bytes void clear(); + void bswap_data2_if_mag(); + std::string hex() const; std::string name(bool include_color_codes) const; uint32_t primary_identifier() const; diff --git a/src/Player.cc b/src/Player.cc index 99857564..71095433 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -522,10 +522,15 @@ void ClientGameData::import_player(const PSOPlayerDataDCPC& pd) { // auto_reply = pd.auto_reply; } -void ClientGameData::import_player(const PSOPlayerDataV3& gc) { +void ClientGameData::import_player(const PSOPlayerDataV3& gc, bool is_gc) { auto account = this->account(); auto player = this->player(); player->inventory = gc.inventory; + if (is_gc) { + for (size_t z = 0; z < 30; z++) { + player->inventory.items[z].data.bswap_data2_if_mag(); + } + } player->disp = gc.disp.to_bb(); player->info_board = gc.info_board; account->blocked_senders = gc.blocked_senders; diff --git a/src/Player.hh b/src/Player.hh index bf186391..45614ffa 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -524,7 +524,7 @@ public: void save_player_data(); void import_player(const PSOPlayerDataDCPC& pd); - void import_player(const PSOPlayerDataV3& pd); + void import_player(const PSOPlayerDataV3& pd, bool is_gc); void import_player(const PSOPlayerDataBB& pd); // Note: this function is not const because it can cause player and account // data to be loaded diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 3bc22203..2c1625eb 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2461,7 +2461,7 @@ static void on_61_98(shared_ptr s, shared_ptr c, pd = &check_size_t(data, sizeof(PSOPlayerDataV3) + c->game_data.player()->auto_reply.bytes()); } - c->game_data.import_player(*pd); + c->game_data.import_player(*pd, c->version() == GameVersion::GC); break; } case GameVersion::BB: { @@ -3442,7 +3442,7 @@ static void on_6F(shared_ptr s, shared_ptr c, static void on_D0_V3_BB(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { - auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); if (c->game_data.pending_item_trade) { throw runtime_error("player started a trade when one is already pending"); @@ -3463,7 +3463,10 @@ static void on_D0_V3_BB(shared_ptr s, shared_ptr c, c->game_data.pending_item_trade.reset(new PendingItemTrade()); c->game_data.pending_item_trade->other_client_id = cmd.target_client_id; for (size_t x = 0; x < cmd.item_count; x++) { - c->game_data.pending_item_trade->items.emplace_back(cmd.items[x]); + auto& item = c->game_data.pending_item_trade->items.emplace_back(cmd.item_datas[x]); + if (c->version() == GameVersion::GC) { + item.bswap_data2_if_mag(); + } } // If the other player has a pending trade as well, assume this is the second diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 3f245eac..b0897f72 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -131,7 +131,7 @@ static void on_forward_check_game(shared_ptr, forward_subcommand(l, c, command, flag, data, size); } -static void on_forward_sync_game_state(shared_ptr, +static void on_forward_sync_joining_player_state(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { if (!l->is_game() || !l->any_client_loading()) { @@ -153,6 +153,137 @@ static void on_forward_sync_game_state(shared_ptr, forward_subcommand(l, c, command, flag, data, size); } +static void on_sync_joining_player_item_state(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const void* data, size_t size) { + if (!l->is_game() || !l->any_client_loading()) { + return; + } + + // I'm lazy and this should never happen (this command should always be + // private to the joining player) + if (!command_is_private(command)) { + throw runtime_error("6x70 sent via public command"); + } + + // 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 are + // PSO GC + bool sender_is_gc = (c->version() == GameVersion::GC); + if (!sender_is_gc && (c->version() != GameVersion::XB)) { + forward_subcommand(l, c, command, flag, data, size); + + } else { + if (flag >= l->max_clients) { + return; + } + auto target = l->clients[flag]; + if (!target) { + return; + } + bool target_is_gc = (target->version() == GameVersion::GC); + + if (target_is_gc == sender_is_gc) { + send_command(target, command, flag, data, size); + + } else { + const auto& cmd = check_size_t(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(cmd.data, cmd.compressed_size); + if (c->options.debug) { + 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(decompressed.data()); + + size_t num_floor_items = 0; + for (size_t z = 0; z < decompressed_cmd->floor_item_count_per_area.size(); z++) { + num_floor_items += decompressed_cmd->floor_item_count_per_area[z]; + } + + size_t required_size = sizeof(G_SyncItemState_6x6D_Decompressed) + num_floor_items * sizeof(G_SyncItemState_6x6D_Decompressed::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)); + } + + for (size_t z = 0; z < num_floor_items; z++) { + decompressed_cmd->items[z].item_data.bswap_data2_if_mag(); + } + + 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(); + + // TODO: It'd be nice to not copy the data so many times here. + StringWriter out_w; + out_w.put(out_cmd); + out_w.write(out_compressed_data); + + send_command(target, command, flag, out_w.str()); + } + } +} + +static void on_sync_joining_player_disp_and_inventory(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const void* data, size_t size) { + if (!l->is_game() || !l->any_client_loading()) { + return; + } + + // I'm lazy and this should never happen (this command should always be + // private to the joining player) + if (!command_is_private(command)) { + throw runtime_error("6x70 sent via public command"); + } + + // 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 are + // PSO GC + bool sender_is_gc = (c->version() == GameVersion::GC); + if (!sender_is_gc && (c->version() != GameVersion::XB)) { + forward_subcommand(l, c, command, flag, data, size); + + } else { + if (flag >= l->max_clients) { + return; + } + auto target = l->clients[flag]; + if (!target) { + return; + } + bool target_is_gc = (target->version() == GameVersion::GC); + + if (target_is_gc == sender_is_gc) { + send_command(target, command, flag, data, size); + } else { + auto out_cmd = check_size_t(data, size); + for (size_t z = 0; z < 30; z++) { + out_cmd.inventory.items[z].data.bswap_data2_if_mag(); + } + send_command_t(target, command, flag, out_cmd); + } + } +} + static void on_forward_check_game_loading(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { @@ -527,11 +658,32 @@ static void on_player_drop_item(shared_ptr, forward_subcommand(l, c, command, flag, data, size); } -static void on_create_inventory_item(shared_ptr, +template +void forward_subcommand_with_mag_bswap_t( + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const CmdT& cmd) { + // I'm lazy and this should never happen for item commands (since all players + // need to stay in sync) + if (command_is_private(command)) { + throw runtime_error("6x2B sent via private command"); + } + + for (auto& other_c : l->clients) { + if (!other_c || other_c == c) { + continue; + } + CmdT out_cmd = cmd; + if ((c->version() == GameVersion::GC) != (other_c->version() == GameVersion::GC)) { + out_cmd.item_data.bswap_data2_if_mag(); + } + send_command_t(other_c, command, flag, out_cmd); + } +} + +template +static void on_create_inventory_item_t(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { - const auto& cmd = check_size_t( - data, size, sizeof(G_CreateInventoryItem_PC_V3_BB_6x2B)); + const auto& cmd = check_size_t(data, size); if ((cmd.header.client_id != c->lobby_client_id)) { return; @@ -546,28 +698,43 @@ static void on_create_inventory_item(shared_ptr, PlayerInventoryItem item; item.present = 1; item.flags = 0; - item.data = cmd.item; + item.data = cmd.item_data; + if (c->version() == GameVersion::GC) { + item.data.bswap_data2_if_mag(); + } c->game_data.player()->add_item(item); auto name = item.data.name(false); l->log.info("Player %hu created inventory item %08" PRIX32 " (%s)", - cmd.header.client_id.load(), cmd.item.id.load(), name.c_str()); + cmd.header.client_id.load(), cmd.item_data.id.load(), name.c_str()); if (c->options.debug) { string name = item.data.name(true); send_text_message_printf(c, "$C5CREATE %08" PRIX32 "\n%s", - cmd.item.id.load(), name.c_str()); + cmd.item_data.id.load(), name.c_str()); } c->game_data.player()->print_inventory(stderr); } - forward_subcommand(l, c, command, flag, data, size); + forward_subcommand_with_mag_bswap_t(l, c, command, flag, cmd); } -static void on_drop_partial_stack(shared_ptr, +static void on_create_inventory_item(shared_ptr s, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { - const auto& cmd = check_size_t( - data, size, sizeof(G_DropStackedItem_PC_V3_BB_6x5D)); + if (size == sizeof(G_CreateInventoryItem_PC_V3_BB_6x2B)) { + on_create_inventory_item_t(s, l, c, command, flag, data, size); + } else if (size == sizeof(G_CreateInventoryItem_DC_6x2B)) { + on_create_inventory_item_t(s, l, c, command, flag, data, size); + } else { + throw runtime_error("invalid size for 6x2B command"); + } +} + +template +static void on_drop_partial_stack_t(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const void* data, size_t size) { + const auto& cmd = check_size_t(data, size); // TODO: Should we check the client ID here too? if (!l->is_game()) { @@ -583,7 +750,10 @@ static void on_drop_partial_stack(shared_ptr, PlayerInventoryItem item; item.present = 1; item.flags = 0; - item.data = cmd.data; + item.data = cmd.item_data; + if (c->version() == GameVersion::GC) { + item.data.bswap_data2_if_mag(); + } l->add_item(item, cmd.area, cmd.x, cmd.z); auto name = item.data.name(false); @@ -598,7 +768,19 @@ static void on_drop_partial_stack(shared_ptr, c->game_data.player()->print_inventory(stderr); } - forward_subcommand(l, c, command, flag, data, size); + forward_subcommand_with_mag_bswap_t(l, c, command, flag, cmd); +} + +static void on_drop_partial_stack(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const void* data, size_t size) { + if (size == sizeof(G_DropStackedItem_PC_V3_BB_6x5D)) { + on_drop_partial_stack_t(s, l, c, command, flag, data, size); + } else if (size == sizeof(G_DropStackedItem_DC_6x5D)) { + on_drop_partial_stack_t(s, l, c, command, flag, data, size); + } else { + throw runtime_error("invalid size for 6x5D command"); + } } static void on_drop_partial_stack_bb(shared_ptr, @@ -665,7 +847,10 @@ static void on_buy_shop_item(shared_ptr, PlayerInventoryItem item; item.present = 1; item.flags = 0; - item.data = cmd.item; + item.data = cmd.item_data; + if (c->version() == GameVersion::GC) { + item.data.bswap_data2_if_mag(); + } c->game_data.player()->add_item(item); auto name = item.data.name(false); @@ -679,14 +864,14 @@ static void on_buy_shop_item(shared_ptr, c->game_data.player()->print_inventory(stderr); } - forward_subcommand(l, c, command, flag, data, size); + forward_subcommand_with_mag_bswap_t(l, c, command, flag, cmd); } -static void on_box_or_enemy_item_drop(shared_ptr, +template +static void on_box_or_enemy_item_drop_t(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { - const auto& cmd = check_size_t( - data, size, sizeof(G_DropItem_PC_V3_BB_6x5F)); + const auto& cmd = check_size_t(data, size); if (!l->is_game() || (c->lobby_client_id != l->leader_id)) { return; @@ -699,7 +884,10 @@ static void on_box_or_enemy_item_drop(shared_ptr, PlayerInventoryItem item; item.present = 1; item.flags = 0; - item.data = cmd.data; + item.data = cmd.item_data; + if (c->version() == GameVersion::GC) { + item.data.bswap_data2_if_mag(); + } l->add_item(item, cmd.area, cmd.x, cmd.z); auto name = item.data.name(false); @@ -712,7 +900,19 @@ static void on_box_or_enemy_item_drop(shared_ptr, } } - forward_subcommand(l, c, command, flag, data, size); + forward_subcommand_with_mag_bswap_t(l, c, command, flag, cmd); +} + +static void on_box_or_enemy_item_drop(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const void* data, size_t size) { + if (size == sizeof(G_DropItem_DC_6x5F)) { + on_box_or_enemy_item_drop_t(s, l, c, command, flag, data, size); + } else if (size == sizeof(G_DropItem_PC_V3_BB_6x5F)) { + on_box_or_enemy_item_drop_t(s, l, c, command, flag, data, size); + } else { + throw runtime_error("invalid size for 6x5F command"); + } } static void on_pick_up_item(shared_ptr, @@ -1375,7 +1575,7 @@ static void on_identify_item_bb(shared_ptr, res.header.subcommand = 0xB9; res.header.size = sizeof(res) / 4; res.header.client_id = c->lobby_client_id; - res.item = c->game_data.identify_result.data; + res.item_data = c->game_data.identify_result.data; send_command_t(l, 0x60, 0x00, res); } else { @@ -1606,12 +1806,12 @@ subcommand_handler_t subcommand_handlers[0x100] = { /* 6x68 */ on_forward_check_size_game, /* 6x69 */ on_forward_check_size_game, /* 6x6A */ on_forward_check_size_game, - /* 6x6B */ on_forward_sync_game_state, - /* 6x6C */ on_forward_sync_game_state, - /* 6x6D */ on_forward_sync_game_state, - /* 6x6E */ on_forward_sync_game_state, + /* 6x6B */ on_forward_sync_joining_player_state, + /* 6x6C */ on_forward_sync_joining_player_state, + /* 6x6D */ on_sync_joining_player_item_state, + /* 6x6E */ on_forward_sync_joining_player_state, /* 6x6F */ on_forward_check_game_loading, - /* 6x70 */ on_forward_check_game_loading, + /* 6x70 */ on_sync_joining_player_disp_and_inventory, /* 6x71 */ on_forward_check_game_loading, /* 6x72 */ on_forward_check_game_loading, /* 6x73 */ on_invalid, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 52a32030..dee8e46f 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1374,23 +1374,31 @@ static void send_join_spectator_team(shared_ptr c, shared_ptr l) if (watched_lobby) { // Live spectating for (size_t z = 0; z < 4; z++) { - if (watched_lobby->clients[z]) { - cmd.players[z].lobby_data.player_tag = 0x00010000; - cmd.players[z].lobby_data.guild_card = watched_lobby->clients[z]->license->serial_number; - cmd.players[z].lobby_data.client_id = watched_lobby->clients[z]->lobby_client_id; - cmd.players[z].lobby_data.name = watched_lobby->clients[z]->game_data.player()->disp.name; - remove_language_marker_inplace(cmd.players[z].lobby_data.name); - cmd.players[z].inventory = watched_lobby->clients[z]->game_data.player()->inventory; - cmd.players[z].disp = watched_lobby->clients[z]->game_data.player()->disp.to_dcpcv3(); - remove_language_marker_inplace(cmd.players[z].disp.name); - cmd.entries[z].player_tag = 0x00010000; - cmd.entries[z].guild_card_number = watched_lobby->clients[z]->license->serial_number; - cmd.entries[z].name = watched_lobby->clients[z]->game_data.player()->disp.name; - remove_language_marker_inplace(cmd.entries[z].name); - cmd.entries[z].present = 1; - cmd.entries[z].level = watched_lobby->clients[z]->game_data.player()->disp.level.load(); - player_count++; + if (!watched_lobby->clients[z]) { + continue; } + auto& p = cmd.players[z]; + p.lobby_data.player_tag = 0x00010000; + p.lobby_data.guild_card = watched_lobby->clients[z]->license->serial_number; + p.lobby_data.client_id = watched_lobby->clients[z]->lobby_client_id; + p.lobby_data.name = watched_lobby->clients[z]->game_data.player()->disp.name; + remove_language_marker_inplace(p.lobby_data.name); + p.inventory = watched_lobby->clients[z]->game_data.player()->inventory; + for (size_t y = 0; y < 30; y++) { + p.inventory.items[y].data.bswap_data2_if_mag(); + } + p.disp = watched_lobby->clients[z]->game_data.player()->disp.to_dcpcv3(); + remove_language_marker_inplace(p.disp.name); + + auto& e = cmd.entries[z]; + e.player_tag = 0x00010000; + e.guild_card_number = watched_lobby->clients[z]->license->serial_number; + e.name = watched_lobby->clients[z]->game_data.player()->disp.name; + remove_language_marker_inplace(e.name); + e.present = 1; + e.level = watched_lobby->clients[z]->game_data.player()->disp.level.load(); + + player_count++; } } else if (l->battle_player) { @@ -1410,6 +1418,9 @@ static void send_join_spectator_team(shared_ptr c, shared_ptr l) cmd.players[client_id].lobby_data = entry.lobby_data; remove_language_marker_inplace(cmd.players[client_id].lobby_data.name); cmd.players[client_id].inventory = entry.inventory; + for (size_t z = 0; z < 30; z++) { + cmd.players[client_id].inventory.items[z].data.bswap_data2_if_mag(); + } cmd.players[client_id].disp = entry.disp; remove_language_marker_inplace(cmd.players[client_id].disp.name); cmd.entries[client_id].player_tag = 0x00010000; @@ -1481,6 +1492,9 @@ void send_join_game_t(shared_ptr c, shared_ptr l) { cmd->lobby_data[x].name = l->clients[x]->game_data.player()->disp.name; if (cmd_ep3) { cmd_ep3->players_ep3[x].inventory = l->clients[x]->game_data.player()->inventory; + for (size_t z = 0; z < 30; z++) { + cmd_ep3->players_ep3[x].inventory.items[z].data.bswap_data2_if_mag(); + } cmd_ep3->players_ep3[x].disp = convert_player_disp_data( l->clients[x]->game_data.player()->disp); } @@ -1612,6 +1626,11 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, e.lobby_data.client_id = lc->lobby_client_id; e.lobby_data.name = lc->game_data.player()->disp.name; e.inventory = lc->game_data.player()->inventory; + if (c->version() == GameVersion::GC) { + for (size_t z = 0; z < 30; z++) { + e.inventory.items[z].data.bswap_data2_if_mag(); + } + } e.disp = convert_player_disp_data(lc->game_data.player()->disp); if ((c->version() == GameVersion::PC) || (c->version() == GameVersion::DC)) { e.disp.enforce_v2_limits(); @@ -1782,13 +1801,16 @@ void send_get_player_info(shared_ptr c) { void send_execute_item_trade(shared_ptr c, const vector& items) { SC_TradeItems_D0_D3 cmd; - if (items.size() > sizeof(cmd.items) / sizeof(cmd.items[0])) { + if (items.size() > cmd.item_datas.size()) { throw logic_error("too many items in execute trade command"); } cmd.target_client_id = c->lobby_client_id; cmd.item_count = items.size(); for (size_t x = 0; x < items.size(); x++) { - cmd.items[x] = items[x]; + cmd.item_datas[x] = items[x]; + if (c->version() == GameVersion::GC) { + cmd.item_datas[x].bswap_data2_if_mag(); + } } send_command_t(c, 0xD3, 0x00, cmd); } @@ -1914,31 +1936,43 @@ void send_set_player_visibility(shared_ptr l, shared_ptr c, // BB game commands void send_drop_item(Channel& ch, const ItemData& item, - bool from_enemy, uint8_t area, float x, float z, uint16_t request_id) { + bool from_enemy, uint8_t area, float x, float z, uint16_t entity_id) { G_DropItem_PC_V3_BB_6x5F cmd = { - {{0x5F, 0x0B, 0x0000}, area, from_enemy, request_id, x, z, 0, 0, item}, 0}; + {{0x5F, 0x0B, 0x0000}, area, from_enemy, entity_id, x, z, 0, 0, item}, 0}; + if (ch.version == GameVersion::GC) { + cmd.item_data.bswap_data2_if_mag(); + } ch.send(0x60, 0x00, &cmd, sizeof(cmd)); } void send_drop_item(shared_ptr l, const ItemData& item, - bool from_enemy, uint8_t area, float x, float z, uint16_t request_id) { - G_DropItem_PC_V3_BB_6x5F cmd = { - {{0x5F, 0x0B, 0x0000}, area, from_enemy, request_id, x, z, 0, 0, item}, 0}; - send_command_t(l, 0x60, 0x00, cmd); + bool from_enemy, uint8_t area, float x, float z, uint16_t entity_id) { + for (auto& c : l->clients) { + if (!c) { + continue; + } + send_drop_item(c->channel, item, from_enemy, area, x, z, entity_id); + } } void send_drop_stacked_item(Channel& ch, const ItemData& item, uint8_t area, float x, float z) { G_DropStackedItem_PC_V3_BB_6x5D cmd = { {{0x5D, 0x0A, 0x0000}, area, 0, x, z, item}, 0}; + if (ch.version == GameVersion::GC) { + cmd.item_data.bswap_data2_if_mag(); + } ch.send(0x60, 0x00, &cmd, sizeof(cmd)); } void send_drop_stacked_item(shared_ptr l, const ItemData& item, uint8_t area, float x, float z) { - G_DropStackedItem_PC_V3_BB_6x5D cmd = { - {{0x5D, 0x0A, 0x0000}, area, 0, x, z, item}, 0}; - send_command_t(l, 0x60, 0x00, cmd); + for (auto& c : l->clients) { + if (!c) { + continue; + } + send_drop_stacked_item(c->channel, item, area, x, z); + } } void send_pick_up_item(shared_ptr l, shared_ptr c, @@ -1949,16 +1983,16 @@ void send_pick_up_item(shared_ptr l, shared_ptr c, send_command_t(l, 0x60, 0x00, cmd); } -// Creates an item in a player's inventory (used for withdrawing items from the -// bank) void send_create_inventory_item(shared_ptr l, shared_ptr c, const ItemData& item) { + if (c->version() != GameVersion::BB) { + throw logic_error("6xBE can only be sent to BB clients"); + } 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); } -// destroys an item void send_destroy_item(shared_ptr l, shared_ptr c, uint32_t item_id, uint32_t amount) { uint16_t client_id = c->lobby_client_id; @@ -1966,8 +2000,11 @@ void send_destroy_item(shared_ptr l, shared_ptr c, send_command_t(l, 0x60, 0x00, cmd); } -// sends the player their bank data void send_bank(shared_ptr c) { + if (c->version() != GameVersion::BB) { + throw logic_error("6xBC can only be sent to BB clients"); + } + vector items(c->game_data.player()->bank.items, &c->game_data.player()->bank.items[c->game_data.player()->bank.num_items]); @@ -1981,8 +2018,11 @@ void send_bank(shared_ptr c) { send_command_t_vt(c, 0x6C, 0x00, cmd, items); } -// sends the player a shop's contents void send_shop(shared_ptr c, uint8_t shop_type) { + if (c->version() != GameVersion::BB) { + throw logic_error("6xB6 can only be sent to BB clients"); + } + const auto& contents = c->game_data.shop_contents.at(shop_type); G_ShopContents_BB_6xB6 cmd = { @@ -1993,10 +2033,10 @@ void send_shop(shared_ptr c, uint8_t shop_type) { {}, }; for (size_t x = 0; x < contents.size(); x++) { - cmd.entries[x] = contents[x]; + cmd.item_datas[x] = contents[x]; } - send_command(c, 0x60, 0x00, &cmd, sizeof(cmd) - sizeof(cmd.entries[0]) * (20 - contents.size())); + send_command(c, 0x60, 0x00, &cmd, sizeof(cmd) - sizeof(cmd.item_datas[0]) * (20 - contents.size())); } // notifies players about a level up @@ -2029,6 +2069,10 @@ void send_level_up(shared_ptr l, shared_ptr c) { // gives a player EXP void send_give_experience(shared_ptr l, shared_ptr c, uint32_t amount) { + if (c->version() != GameVersion::BB) { + throw logic_error("6xBF can only be sent to BB clients"); + } + uint16_t client_id = c->lobby_client_id; G_GiveExperience_BB_6xBF cmd = { {0xBF, sizeof(G_GiveExperience_BB_6xBF) / 4, client_id}, amount};