diff --git a/src/Client.hh b/src/Client.hh index 8a85f659..0c16b023 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -95,8 +95,12 @@ struct Client : public std::enable_shared_from_this { Config() = default; + [[nodiscard]] static inline bool check_flag(uint64_t enabled_flags, Flag flag) { + return !!(enabled_flags & static_cast(flag)); + } + [[nodiscard]] inline bool check_flag(Flag flag) const { - return !!(this->enabled_flags & static_cast(flag)); + return this->check_flag(this->enabled_flags, flag); } inline void set_flag(Flag flag) { this->enabled_flags |= static_cast(flag); diff --git a/src/PlayerSubordinates.cc b/src/PlayerSubordinates.cc index 1d1c2720..b980482d 100644 --- a/src/PlayerSubordinates.cc +++ b/src/PlayerSubordinates.cc @@ -9,10 +9,12 @@ #include #include +#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 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 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 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 item_parameter_table) { +void PlayerInventory::encode_for_client(shared_ptr 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); } } diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index fd614165..d51abb5c 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -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 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 item_parameter_table); + void decode_from_client(std::shared_ptr c); + void encode_for_client(std::shared_ptr c); } __attribute__((packed)); struct PlayerBank { @@ -142,7 +145,8 @@ struct PlayerDispDataDCPCV3 { /* 74 */ parray config; /* BC */ parray technique_levels_v1; /* D0 */ - void enforce_lobby_join_limits(GameVersion target_version); + + void enforce_lobby_join_limits_for_client(std::shared_ptr c); PlayerDispDataBB to_bb(uint8_t to_language, uint8_t from_language) const; } __attribute__((packed)); @@ -168,7 +172,7 @@ struct PlayerDispDataBB { /* 017C */ parray technique_levels_v1; /* 0190 */ - void enforce_lobby_join_limits(GameVersion target_version); + void enforce_lobby_join_limits_for_client(std::shared_ptr c); PlayerDispDataDCPCV3 to_dcpcv3(uint8_t to_language, uint8_t from_language) const; PlayerDispDataBBPreview to_preview() const; void apply_preview(const PlayerDispDataBBPreview&); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 1e901914..68c9ff06 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2878,7 +2878,7 @@ static void on_61_98(shared_ptr 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()); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index a6e818d9..fb440fa8 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -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(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(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 c, uint8_t command, uint8_t flag, } } -static void on_set_player_visibility(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { +static void on_set_player_invisible(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { + const auto& cmd = check_size_t(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 c, uint8_t command, uint8_t flag, const void* data, size_t size) { const auto& cmd = check_size_t(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 c, uint8_t command, uint //////////////////////////////////////////////////////////////////////////////// // Game commands used by cheat mechanisms -template -static void on_change_floor(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { - const auto& cmd = check_size_t(data, size); +static void on_change_floor_6x1F(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { + const auto& cmd = check_size_t(data, size); c->floor = cmd.floor; forward_subcommand(c, command, flag, data, size); } +static void on_change_floor_6x21(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { + const auto& cmd = check_size_t(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 c, uint8_t command, uint8_t flag, const void* data, size_t size) { const auto& cmd = check_size_t(data, size, 0xFFFF); @@ -722,7 +776,7 @@ static void on_switch_state_changed(shared_ptr c, uint8_t command, uint8 //////////////////////////////////////////////////////////////////////////////// -template +template void on_movement(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { const auto& cmd = check_size_t(data, size); if (cmd.header.client_id != c->lobby_client_id) { @@ -732,10 +786,10 @@ void on_movement(shared_ptr 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 +template void on_movement_with_floor(shared_ptr c, uint8_t command, uint8_t flag, const void* data, size_t size) { const auto& cmd = check_size_t(data, size); if (cmd.header.client_id != c->lobby_client_id) { @@ -746,7 +800,12 @@ void on_movement_with_floor(shared_ptr 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 c, uint8_t command, uint8_t flag, const void* data, size_t size) { + check_size_t(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 c, uint8_t, uint8_t //////////////////////////////////////////////////////////////////////////////// +static void handle_subcommand_dc_nte(shared_ptr 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(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(data), size); + non_nte_data[0] = non_nte_subcommand; + } + send_command(lc, command, flag, non_nte_data); + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// + typedef void (*subcommand_handler_t)(shared_ptr 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, - /* 6x20 */ on_movement_with_floor, - /* 6x21 */ on_change_floor, - /* 6x22 */ on_forward_check_size_client, - /* 6x23 */ on_set_player_visibility, + /* 6x1F */ on_change_floor_6x1F, + /* 6x20 */ on_movement_with_floor, + /* 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, - /* 6x3F */ on_movement_with_floor, - /* 6x40 */ on_movement, + /* 6x3E */ on_movement_with_floor, + /* 6x3F */ on_movement_with_floor, + /* 6x40 */ on_movement, /* 6x41 */ nullptr, - /* 6x42 */ on_movement, + /* 6x42 */ on_movement, /* 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 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(false); - if (header.size != 0) { - size = header.size << 2; - } else { - const auto& ext_header = r.get>(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(false); + if (header.size != 0) { + size = header.size << 2; + } else { + const auto& ext_header = r.get>(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); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index ab996744..141b8e11 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1473,9 +1473,9 @@ static void send_join_spectator_team(shared_ptr c, shared_ptr 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 c, shared_ptr 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 c, shared_ptr 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 c, shared_ptr 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(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 c, shared_ptr l, shared_ptrdisp.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(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 c, shared_ptr l, shared_ptrlobby_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(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 c, shared_ptr 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(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));