fix DC NTE lobby interactions

This commit is contained in:
Martin Michelsen
2023-11-16 23:22:26 -08:00
parent 9cbcd09be0
commit 132395a53a
6 changed files with 218 additions and 76 deletions
+5 -1
View File
@@ -95,8 +95,12 @@ struct Client : public std::enable_shared_from_this<Client> {
Config() = default;
[[nodiscard]] static inline bool check_flag(uint64_t enabled_flags, Flag flag) {
return !!(enabled_flags & static_cast<uint64_t>(flag));
}
[[nodiscard]] inline bool check_flag(Flag flag) const {
return !!(this->enabled_flags & static_cast<uint64_t>(flag));
return this->check_flag(this->enabled_flags, flag);
}
inline void set_flag(Flag flag) {
this->enabled_flags |= static_cast<uint64_t>(flag);
+22 -9
View File
@@ -9,10 +9,12 @@
#include <phosg/Random.hh>
#include <stdexcept>
#include "Client.hh"
#include "ItemData.hh"
#include "ItemParameterTable.hh"
#include "Loggers.hh"
#include "PSOEncryption.hh"
#include "ServerState.hh"
#include "StaticGameData.hh"
#include "Text.hh"
#include "Version.hh"
@@ -33,7 +35,7 @@ void PlayerVisualConfig::compute_name_color_checksum() {
this->name_color_checksum = this->compute_name_color_checksum(this->name_color);
}
void PlayerDispDataDCPCV3::enforce_lobby_join_limits(GameVersion target_version) {
void PlayerDispDataDCPCV3::enforce_lobby_join_limits_for_client(shared_ptr<Client> c) {
struct ClassMaxes {
uint16_t costume;
uint16_t skin;
@@ -79,7 +81,7 @@ void PlayerDispDataDCPCV3::enforce_lobby_join_limits(GameVersion target_version)
{0x0000, 0x0000, 0x0000, 0x0000, 0x0000}};
const ClassMaxes* maxes;
if ((target_version == GameVersion::PC) || (target_version == GameVersion::DC)) {
if ((c->version() == GameVersion::PC) || (c->version() == GameVersion::DC)) {
// V1/V2 have fewer classes, so we'll substitute some here
switch (this->visual.char_class) {
case 0: // HUmar
@@ -115,7 +117,7 @@ void PlayerDispDataDCPCV3::enforce_lobby_join_limits(GameVersion target_version)
}
maxes = &v1_v2_class_maxes[this->visual.char_class];
this->visual.version = 2;
this->visual.version = c->config.check_flag(Client::Flag::IS_DC_V1) ? 1 : 2;
} else {
if (this->visual.char_class >= 19) {
@@ -139,8 +141,8 @@ void PlayerDispDataDCPCV3::enforce_lobby_join_limits(GameVersion target_version)
}
}
void PlayerDispDataBB::enforce_lobby_join_limits(GameVersion version) {
if (version != GameVersion::BB) {
void PlayerDispDataBB::enforce_lobby_join_limits_for_client(shared_ptr<Client> c) {
if (c->version() != GameVersion::BB) {
throw logic_error("PlayerDispDataBB being sent to non-BB client");
}
this->play_time = 0;
@@ -631,15 +633,26 @@ size_t PlayerInventory::remove_all_items_of_type(uint8_t data1_0, int16_t data1_
return ret;
}
void PlayerInventory::decode_for_version(GameVersion version) {
void PlayerInventory::decode_from_client(shared_ptr<Client> c) {
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.decode_for_version(version);
this->items[z].data.decode_for_version(c->version());
}
}
void PlayerInventory::encode_for_version(GameVersion version, shared_ptr<const ItemParameterTable> item_parameter_table) {
void PlayerInventory::encode_for_client(shared_ptr<Client> c) {
if (c->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
// DC NTE has the item count as a 32-bit value here, whereas every other
// version uses a single byte. To stop DC NTE from crashing by trying to
// construct far more than 30 TItem objects, we clear the fields DC NTE
// doesn't know about.
this->hp_from_materials = 0;
this->tp_from_materials = 0;
this->language = 0;
}
auto item_parameter_table = c->require_server_state()->item_parameter_table_for_version(c->version());
for (size_t z = 0; z < this->items.size(); z++) {
this->items[z].data.encode_for_version(version, item_parameter_table);
this->items[z].data.encode_for_version(c->version(), item_parameter_table);
}
}
+8 -4
View File
@@ -16,6 +16,7 @@
#include "Text.hh"
#include "Version.hh"
struct Client;
class ItemParameterTable;
// PSO V2 stored some extra data in the character structs in a format that I'm
@@ -68,6 +69,8 @@ struct PlayerInventory {
/* 0004 */ parray<PlayerInventoryItem, 30> items;
/* 034C */
void clear_dc_nte_unused_fields();
size_t find_item(uint32_t item_id) const;
size_t find_item_by_primary_identifier(uint32_t primary_identifier) const;
@@ -78,8 +81,8 @@ struct PlayerInventory {
size_t remove_all_items_of_type(uint8_t data0, int16_t data1 = -1);
void decode_for_version(GameVersion version);
void encode_for_version(GameVersion version, std::shared_ptr<const ItemParameterTable> item_parameter_table);
void decode_from_client(std::shared_ptr<Client> c);
void encode_for_client(std::shared_ptr<Client> c);
} __attribute__((packed));
struct PlayerBank {
@@ -142,7 +145,8 @@ struct PlayerDispDataDCPCV3 {
/* 74 */ parray<uint8_t, 0x48> config;
/* BC */ parray<uint8_t, 0x14> technique_levels_v1;
/* D0 */
void enforce_lobby_join_limits(GameVersion target_version);
void enforce_lobby_join_limits_for_client(std::shared_ptr<Client> c);
PlayerDispDataBB to_bb(uint8_t to_language, uint8_t from_language) const;
} __attribute__((packed));
@@ -168,7 +172,7 @@ struct PlayerDispDataBB {
/* 017C */ parray<uint8_t, 0x14> technique_levels_v1;
/* 0190 */
void enforce_lobby_join_limits(GameVersion target_version);
void enforce_lobby_join_limits_for_client(std::shared_ptr<Client> c);
PlayerDispDataDCPCV3 to_dcpcv3(uint8_t to_language, uint8_t from_language) const;
PlayerDispDataBBPreview to_preview() const;
void apply_preview(const PlayerDispDataBBPreview&);
+1 -1
View File
@@ -2878,7 +2878,7 @@ static void on_61_98(shared_ptr<Client> c, uint16_t command, uint32_t flag, stri
default:
throw logic_error("player data command not implemented for version");
}
player->inventory.decode_for_version(c->version());
player->inventory.decode_from_client(c);
c->channel.language = player->inventory.language;
string name_str = player->disp.name.decode(c->language());
+169 -48
View File
@@ -23,6 +23,19 @@ using namespace std;
// The functions in this file are called when a client sends a game command
// (60, 62, 6C, 6D, C9, or CB).
// There are three different sets of subcommand numbers: the DC NTE set, the
// November 2000 prototype set, and the set used by all other versions of the
// game (starting from the December 2000 prototype, all the way through BB).
// Currently we do not support the November 2000 prototype, but we do support
// DC NTE. In general, DC NTE clients can only interact with non-NTE players in
// very limited ways, since most subcommand-based actions take place in games,
// and non-NTE players cannot join NTE games. Commands sent by DC NTE clients
// are not handled by the functions defined in subcommand_handlers, but are
// instead handled by handle_subcommand_dc_nte. This means we only have to
// consider sending to DC NTE clients in a small subset of the command handlers
// (those that can occur in the lobby), and we can skip sending most
// subcommands to DC NTE by default.
bool command_is_private(uint8_t command) {
return (command == 0x62) || (command == 0x6D);
}
@@ -39,7 +52,8 @@ static void forward_subcommand(
uint8_t command,
uint8_t flag,
const void* data,
size_t size) {
size_t size,
uint8_t dc_nte_subcommand = 0x00) {
// If the command is an Ep3-only command, make sure an Ep3 client sent it
bool command_is_ep3 = (command & 0xF0) == 0xC0;
@@ -56,7 +70,17 @@ static void forward_subcommand(
if (!target) {
return;
}
send_command(target, command, flag, data, size);
if (target->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
if (dc_nte_subcommand) {
string nte_data(reinterpret_cast<const char*>(data), size);
nte_data[0] = dc_nte_subcommand;
send_command(target, command, flag, nte_data);
} else {
c->log.warning("Attempted to send unsupported target command to DC NTE client; dropping command");
}
} else {
send_command(target, command, flag, data, size);
}
} else {
if (command_is_ep3) {
@@ -66,8 +90,25 @@ static void forward_subcommand(
}
send_command(target, command, flag, data, size);
}
} else {
send_command_excluding_client(l, c, command, flag, data, size);
string nte_data;
for (auto& lc : l->clients) {
if (!lc || (lc == c)) {
continue;
}
if (lc->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
if (dc_nte_subcommand) {
if (nte_data.empty()) {
nte_data.assign(reinterpret_cast<const char*>(data), size);
nte_data[0] = dc_nte_subcommand;
}
send_command(lc, command, flag, nte_data);
}
} else {
send_command(lc, command, flag, data, size);
}
}
}
// Before battle, forward only chat commands to watcher lobbies; during
@@ -590,13 +631,21 @@ static void on_word_select(shared_ptr<Client> c, uint8_t command, uint8_t flag,
}
}
static void on_set_player_visibility(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
static void on_set_player_invisible(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_SetPlayerVisibility_6x22_6x23>(data, size);
if (cmd.header.client_id != c->lobby_client_id) {
return;
}
forward_subcommand(c, command, flag, data, size, 0x1E);
}
static void on_set_player_visible(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_SetPlayerVisibility_6x22_6x23>(data, size);
if (cmd.header.client_id == c->lobby_client_id) {
auto l = c->require_lobby();
forward_subcommand(c, command, flag, data, size, 0x1F);
forward_subcommand(c, command, flag, data, size);
auto l = c->require_lobby();
if (!l->is_game() && !c->config.check_flag(Client::Flag::IS_DC_V1)) {
send_arrow_update(l);
}
@@ -610,13 +659,18 @@ static void on_set_player_visibility(shared_ptr<Client> c, uint8_t command, uint
////////////////////////////////////////////////////////////////////////////////
// Game commands used by cheat mechanisms
template <typename CmdT>
static void on_change_floor(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<CmdT>(data, size);
static void on_change_floor_6x1F(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_SetPlayerArea_6x1F>(data, size);
c->floor = cmd.floor;
forward_subcommand(c, command, flag, data, size);
}
static void on_change_floor_6x21(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_InterLevelWarp_6x21>(data, size);
c->floor = cmd.floor;
forward_subcommand(c, command, flag, data, size, 0x1D);
}
// When a player dies, decrease their mag's synchro
static void on_player_died(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<G_ClientIDHeader>(data, size, 0xFFFF);
@@ -722,7 +776,7 @@ static void on_switch_state_changed(shared_ptr<Client> c, uint8_t command, uint8
////////////////////////////////////////////////////////////////////////////////
template <typename CmdT>
template <typename CmdT, uint8_t DCNTESubcommand>
void on_movement(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<CmdT>(data, size);
if (cmd.header.client_id != c->lobby_client_id) {
@@ -732,10 +786,10 @@ void on_movement(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void
c->x = cmd.x;
c->z = cmd.z;
forward_subcommand(c, command, flag, data, size);
forward_subcommand(c, command, flag, data, size, DCNTESubcommand);
}
template <typename CmdT>
template <typename CmdT, uint8_t DCNTESubcommand>
void on_movement_with_floor(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
const auto& cmd = check_size_t<CmdT>(data, size);
if (cmd.header.client_id != c->lobby_client_id) {
@@ -746,7 +800,12 @@ void on_movement_with_floor(shared_ptr<Client> c, uint8_t command, uint8_t flag,
c->z = cmd.z;
c->floor = cmd.floor;
forward_subcommand(c, command, flag, data, size);
forward_subcommand(c, command, flag, data, size, DCNTESubcommand);
}
static void on_toggle_counter_interaction(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
check_size_t<G_ToggleCounterInteraction_6x52>(data, size, 0xFFFF);
forward_subcommand(c, command, flag, data, size, 0x46);
}
////////////////////////////////////////////////////////////////////////////////
@@ -2544,6 +2603,69 @@ static void on_write_quest_global_flag_bb(shared_ptr<Client> c, uint8_t, uint8_t
////////////////////////////////////////////////////////////////////////////////
static void handle_subcommand_dc_nte(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size) {
auto l = c->require_lobby();
if (l->is_game()) {
// In a game, assume all other clients are DC NTE as well and forward the
// subcommand without any processing
forward_subcommand(c, command, flag, data, size);
} else {
// In a lobby, we have to deal with all other versions of the game having
// different subcommand numbers than DC NTE. We'll forward the command
// verbatim to other DC NTE clients, but will have to translate it for
// non-NTE clients. Some subcommands may not map cleanly; for those, we
// don't send anything at all to non-NTE clients.
auto& header = check_size_t<G_UnusedHeader>(data, size, 0xFFFF);
uint8_t nte_subcommand = header.subcommand;
uint8_t non_nte_subcommand = 0x00;
switch (nte_subcommand) {
case 0x1D:
non_nte_subcommand = 0x21;
break;
case 0x1E:
non_nte_subcommand = 0x22;
break;
case 0x1F:
non_nte_subcommand = 0x23;
break;
case 0x36:
non_nte_subcommand = 0x3F;
break;
case 0x37:
non_nte_subcommand = 0x40;
break;
case 0x39:
non_nte_subcommand = 0x42;
break;
case 0x46:
non_nte_subcommand = 0x52;
break;
default:
non_nte_subcommand = 0x00;
}
string non_nte_data;
for (auto lc : l->clients) {
if (!lc || (lc == c)) {
continue;
}
if (lc->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
send_command(lc, command, flag, data, size);
} else if (non_nte_subcommand != 0x00) {
if (non_nte_data.empty()) {
non_nte_data.assign(reinterpret_cast<const char*>(data), size);
non_nte_data[0] = non_nte_subcommand;
}
send_command(lc, command, flag, non_nte_data);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
typedef void (*subcommand_handler_t)(shared_ptr<Client> c, uint8_t command, uint8_t flag, const void* data, size_t size);
subcommand_handler_t subcommand_handlers[0x100] = {
@@ -2578,11 +2700,11 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6x1C */ on_forward_check_size_game,
/* 6x1D */ nullptr,
/* 6x1E */ nullptr,
/* 6x1F */ on_change_floor<G_SetPlayerArea_6x1F>,
/* 6x20 */ on_movement_with_floor<G_SetPosition_6x20>,
/* 6x21 */ on_change_floor<G_InterLevelWarp_6x21>,
/* 6x22 */ on_forward_check_size_client,
/* 6x23 */ on_set_player_visibility,
/* 6x1F */ on_change_floor_6x1F,
/* 6x20 */ on_movement_with_floor<G_SetPosition_6x20, 0x00>,
/* 6x21 */ on_change_floor_6x21,
/* 6x22 */ on_set_player_invisible,
/* 6x23 */ on_set_player_visible,
/* 6x24 */ on_forward_check_size_game,
/* 6x25 */ on_equip_item,
/* 6x26 */ on_unequip_item,
@@ -2609,11 +2731,11 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6x3B */ on_forward_check_size,
/* 6x3C */ nullptr,
/* 6x3D */ nullptr,
/* 6x3E */ on_movement_with_floor<G_StopAtPosition_6x3E>,
/* 6x3F */ on_movement_with_floor<G_SetPosition_6x3F>,
/* 6x40 */ on_movement<G_WalkToPosition_6x40>,
/* 6x3E */ on_movement_with_floor<G_StopAtPosition_6x3E, 0x00>,
/* 6x3F */ on_movement_with_floor<G_SetPosition_6x3F, 0x36>,
/* 6x40 */ on_movement<G_WalkToPosition_6x40, 0x37>,
/* 6x41 */ nullptr,
/* 6x42 */ on_movement<G_RunToPosition_6x42>,
/* 6x42 */ on_movement<G_RunToPosition_6x42, 0x39>,
/* 6x43 */ on_forward_check_size_client,
/* 6x44 */ on_forward_check_size_client,
/* 6x45 */ on_forward_check_size_client,
@@ -2629,7 +2751,7 @@ subcommand_handler_t subcommand_handlers[0x100] = {
/* 6x4F */ on_forward_check_size_client,
/* 6x50 */ on_forward_check_size_client,
/* 6x51 */ nullptr,
/* 6x52 */ on_forward_check_size,
/* 6x52 */ on_toggle_counter_interaction,
/* 6x53 */ on_forward_check_size_game,
/* 6x54 */ nullptr,
/* 6x55 */ on_forward_check_size_client,
@@ -2809,32 +2931,31 @@ void on_subcommand_multi(shared_ptr<Client> c, uint8_t command, uint8_t flag, co
if (data.empty()) {
throw runtime_error("game command is empty");
}
if (c->version() == GameVersion::DC &&
(c->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION) || c->config.check_flag(Client::Flag::IS_DC_V1_PROTOTYPE))) {
// TODO: We should convert these to non-trial formats and vice versa
forward_subcommand(c, command, flag, data.data(), data.size());
} else {
StringReader r(data);
while (!r.eof()) {
size_t size;
const auto& header = r.get<G_UnusedHeader>(false);
if (header.size != 0) {
size = header.size << 2;
} else {
const auto& ext_header = r.get<G_ExtendedHeader<G_UnusedHeader>>(false);
size = ext_header.size;
if (size < 8) {
throw runtime_error("extended subcommand header has size < 8");
}
if (size & 3) {
throw runtime_error("extended subcommand size is not a multiple of 4");
}
}
if (size == 0) {
throw runtime_error("invalid subcommand size");
}
const void* data = r.getv(size);
StringReader r(data);
while (!r.eof()) {
size_t size;
const auto& header = r.get<G_UnusedHeader>(false);
if (header.size != 0) {
size = header.size << 2;
} else {
const auto& ext_header = r.get<G_ExtendedHeader<G_UnusedHeader>>(false);
size = ext_header.size;
if (size < 8) {
throw runtime_error("extended subcommand header has size < 8");
}
if (size & 3) {
throw runtime_error("extended subcommand size is not a multiple of 4");
}
}
if (size == 0) {
throw runtime_error("invalid subcommand size");
}
const void* data = r.getv(size);
if (c->config.check_flag(Client::Flag::IS_DC_TRIAL_EDITION)) {
handle_subcommand_dc_nte(c, command, flag, data, size);
} else {
auto fn = subcommand_handlers[header.subcommand];
if (fn) {
fn(c, command, flag, data, size);
+13 -13
View File
@@ -1473,9 +1473,9 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
p.lobby_data.client_id = wc->lobby_client_id;
p.lobby_data.name.encode(wc_p->disp.name.decode(wc_p->inventory.language), c->language());
p.inventory = wc_p->inventory;
p.inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
p.inventory.encode_for_client(c);
p.disp = wc_p->disp.to_dcpcv3(c->language(), p.inventory.language);
p.disp.enforce_lobby_join_limits(c->version());
p.disp.enforce_lobby_join_limits_for_client(c);
auto& e = cmd.entries[z];
e.player_tag = 0x00010000;
@@ -1511,9 +1511,9 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
auto& p = cmd.players[client_id];
p.lobby_data = entry.lobby_data;
p.inventory = entry.inventory;
p.inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
p.inventory.encode_for_client(c);
p.disp = entry.disp;
p.disp.enforce_lobby_join_limits(c->version());
p.disp.enforce_lobby_join_limits_for_client(c);
auto& e = cmd.entries[client_id];
e.player_tag = 0x00010000;
@@ -1542,7 +1542,7 @@ static void send_join_spectator_team(shared_ptr<Client> c, shared_ptr<Lobby> l)
cmd_p.lobby_data.name.encode(other_p->disp.name.decode(other_p->inventory.language), c->language());
cmd_p.inventory = other_p->inventory;
cmd_p.disp = other_p->disp.to_dcpcv3(c->language(), cmd_p.inventory.language);
cmd_p.disp.enforce_lobby_join_limits(c->version());
cmd_p.disp.enforce_lobby_join_limits_for_client(c);
cmd_e.player_tag = 0x00010000;
cmd_e.guild_card_number = other_c->license->serial_number;
@@ -1648,9 +1648,9 @@ void send_join_game(shared_ptr<Client> c, shared_ptr<Lobby> l) {
if (l->clients[x]) {
auto other_p = l->clients[x]->game_data.character();
cmd.players_ep3[x].inventory = other_p->inventory;
cmd.players_ep3[x].inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
cmd.players_ep3[x].inventory.encode_for_client(c);
cmd.players_ep3[x].disp = convert_player_disp_data<PlayerDispDataDCPCV3>(other_p->disp, c->language(), other_p->inventory.language);
cmd.players_ep3[x].disp.enforce_lobby_join_limits(c->version());
cmd.players_ep3[x].disp.enforce_lobby_join_limits_for_client(c);
}
}
send_command_t(c, 0x64, player_count, cmd);
@@ -1778,9 +1778,9 @@ void send_join_lobby_t(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cli
e.lobby_data.name.encode(lp->disp.name.decode(lp->inventory.language), c->language());
}
e.inventory = lp->inventory;
e.inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
e.inventory.encode_for_client(c);
e.disp = convert_player_disp_data<DispDataT>(lp->disp, c->language(), lp->inventory.language);
e.disp.enforce_lobby_join_limits(c->version());
e.disp.enforce_lobby_join_limits_for_client(c);
}
send_command(c, command, used_entries, &cmd, cmd.size(used_entries));
@@ -1851,9 +1851,9 @@ void send_join_lobby_xb(shared_ptr<Client> c, shared_ptr<Lobby> l, shared_ptr<Cl
e.lobby_data.client_id = lc->lobby_client_id;
e.lobby_data.name.encode(lp->disp.name.decode(lp->inventory.language), c->language());
e.inventory = lp->inventory;
e.inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
e.inventory.encode_for_client(c);
e.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(lp->disp, c->language(), lp->inventory.language);
e.disp.enforce_lobby_join_limits(c->version());
e.disp.enforce_lobby_join_limits_for_client(c);
}
send_command(c, command, used_entries, &cmd, cmd.size(used_entries));
@@ -1899,9 +1899,9 @@ void send_join_lobby_dc_nte(shared_ptr<Client> c, shared_ptr<Lobby> l,
e.lobby_data.client_id = lc->lobby_client_id;
e.lobby_data.name.encode(lp->disp.name.decode(lp->inventory.language), c->language());
e.inventory = lp->inventory;
e.inventory.encode_for_version(c->version(), s->item_parameter_table_for_version(c->version()));
e.inventory.encode_for_client(c);
e.disp = convert_player_disp_data<PlayerDispDataDCPCV3>(lp->disp, c->language(), lp->inventory.language);
e.disp.enforce_lobby_join_limits(c->version());
e.disp.enforce_lobby_join_limits_for_client(c);
}
send_command(c, command, used_entries, &cmd, cmd.size(used_entries));