diff --git a/README.md b/README.md index 8208286e..37af523e 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ newserv supports several versions of PSO. Specifically: | GC Ep1&2 Plus | Yes | Yes | Yes | Yes | | GC Ep3 Trial | Yes | Yes | Partial (4) | Yes | | GC Ep3 | Yes | Yes | Yes | Yes | -| Xbox Ep1&2 | Yes | Yes | Yes | Partial (5) | +| Xbox Ep1&2 | Yes | Yes | Yes | Yes | | BB (vanilla) | Yes | Yes | Yes (2) | Yes | | BB (Tethealla) | Yes | Yes | Yes (2) | Yes | @@ -76,7 +76,6 @@ newserv supports several versions of PSO. Specifically: 2. *BB games are mostly playable, but there are still some unimplemented features (for example, some quests that use rare commands may not work). Please submit a GitHub issue if you find something that doesn't work.* 3. *Support for PSO Dreamcast Trial Edition and the December 2000 prototype is somewhat incomplete and probably never will be complete. These versions are rather unstable and seem to crash often, but it's not obvious whether it's because they're prototypes or because newserv sends data they can't handle.* 4. *Creating a game works and battle setup behaves mostly normally, but starting a battle doesn't work.* -5. *PSO Xbox sessions can be proxied to Sylverant. On Schtserv, Xbox proxy sessions can connect, but Schtserv does not recognize them as Xbox sessions when they join the lobby, so the session fails at that point.* ## Setup diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 7bcc6c22..20ed691b 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1888,6 +1888,9 @@ struct C_LoginExtended_PC_9D : C_Login_DC_PC_GC_9D { // Not used on GC Episodes 1&2 Trial Edition. // The extended version of this command is used in the same circumstances as // when PSO PC uses the extended version of the 9D command. +// PSO XB does not send the client config (security data) in the 9E command, +// even though there is a space for it. The server must use 9F instead to +// retrieve the client config. // header.flag is 1 if the client has UDP disabled. struct C_Login_GC_9E : C_Login_DC_PC_GC_9D { @@ -1897,7 +1900,8 @@ struct C_LoginExtended_GC_9E : C_Login_GC_9E { SC_MeetUserExtension extension; } __packed__; -struct C_Login_XB_9E : C_Login_GC_9E { +struct C_Login_XB_9E : C_Login_DC_PC_GC_9D { + parray unused; XBNetworkLocation netloc; parray unknown_a1a; le_uint32_t xb_user_id_high = 0; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index dec1c642..37107474 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -356,6 +356,7 @@ static HandlerResult S_V123P_02_17( } } throw logic_error("DC/PC init command not handled"); + case GameVersion::GC: if (command == 0x17) { C_VerifyLicense_V3_DB cmd; @@ -411,6 +412,7 @@ static HandlerResult S_V123P_02_17( return S_G_9A(ses, command, flag, data); } throw logic_error("GC init command not handled"); + case GameVersion::XB: { C_LoginExtended_XB_9E cmd; if (ses->remote_guild_card_number < 0) { @@ -434,20 +436,14 @@ static HandlerResult S_V123P_02_17( } else { cmd.name.encode(ses->character_name, ses->language()); } - cmd.client_config = ses->remote_client_config_data; - if (ses->wrapped_client && ses->wrapped_client->xb_netloc) { - cmd.netloc = *ses->wrapped_client->xb_netloc; - cmd.unknown_a1a = ses->wrapped_client->xb_9E_unknown_a1a; - } else { - cmd.netloc.account_id = ses->license->xb_account_id; - } + cmd.netloc = ses->xb_netloc; + cmd.unknown_a1a = ses->xb_9E_unknown_a1a; cmd.xb_user_id_high = (ses->license->xb_user_id >> 32) & 0xFFFFFFFF; cmd.xb_user_id_low = ses->license->xb_user_id & 0xFFFFFFFF; - ses->server_channel.send( - 0x9E, 0x01, &cmd, - cmd.is_extended ? sizeof(C_LoginExtended_XB_9E) : sizeof(C_Login_XB_9E)); + ses->server_channel.send(0x9E, 0x01, &cmd, sizeof(C_LoginExtended_XB_9E)); return HandlerResult::Type::SUPPRESS; } + default: throw logic_error("invalid game version in server init handler"); } @@ -1538,7 +1534,9 @@ static HandlerResult S_AC(shared_ptr ses, uint16_t, } static HandlerResult S_66_69_E9(shared_ptr ses, uint16_t, uint32_t, string& data) { - const auto& cmd = check_size_t(data); + // Schtserv sends a large command here for unknown reasons. The client ignores + // the extra data, so we allow the large command here. + const auto& cmd = check_size_t(data, 0xFFFF); size_t index = cmd.client_id; if (index >= ses->lobby_players.size()) { ses->log.warning("Lobby leave command references missing position"); diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index e014c7cb..e7143136 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -259,12 +259,6 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 auto s = server->state; bool should_close_unlinked_session = false; - shared_ptr license; - uint32_t sub_version = 0; - string character_name; - Client::Config config; - string login_command_bb; - string hardware_id; try { if (ses->version == GameVersion::DC) { @@ -272,18 +266,18 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 // anything else, disconnect if (command == 0x93) { const auto& cmd = check_size_t(data); - license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode()); - sub_version = cmd.sub_version; + ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode()); + ses->sub_version = cmd.sub_version; ses->channel.language = cmd.language; - character_name = cmd.name.decode(ses->channel.language); - hardware_id = cmd.hardware_id.decode(); - config.set_flag(Client::Flag::IS_DC_V1); + ses->character_name = cmd.name.decode(ses->channel.language); + ses->hardware_id = cmd.hardware_id.decode(); + ses->config.set_flag(Client::Flag::IS_DC_V1); } else if (command == 0x9D) { const auto& cmd = check_size_t(data, sizeof(C_LoginExtended_DC_GC_9D)); - license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode()); - sub_version = cmd.sub_version; + ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode()); + ses->sub_version = cmd.sub_version; ses->channel.language = cmd.language; - character_name = cmd.name.decode(ses->channel.language); + ses->character_name = cmd.name.decode(ses->channel.language); } else { throw runtime_error("command is not 93 or 9D"); } @@ -295,10 +289,10 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 throw runtime_error("command is not 9D"); } const auto& cmd = check_size_t(data, sizeof(C_LoginExtended_PC_9D)); - license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode()); - sub_version = cmd.sub_version; + ses->license = s->license_index->verify_v1_v2(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode()); + ses->sub_version = cmd.sub_version; ses->channel.language = cmd.language; - character_name = cmd.name.decode(ses->channel.language); + ses->character_name = cmd.name.decode(ses->channel.language); } else if (ses->version == GameVersion::GC) { // We should only get a 9E while the session is unlinked; if we get @@ -308,11 +302,34 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 throw runtime_error("command is not 9E"); } const auto& cmd = check_size_t(data, sizeof(C_LoginExtended_GC_9E)); - license = s->license_index->verify_gc(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode()); - sub_version = cmd.sub_version; + ses->license = s->license_index->verify_gc(stoul(cmd.serial_number.decode(), nullptr, 16), cmd.access_key.decode()); + ses->sub_version = cmd.sub_version; ses->channel.language = cmd.language; - character_name = cmd.name.decode(ses->channel.language); - config.parse_from(cmd.client_config); + ses->character_name = cmd.name.decode(ses->channel.language); + ses->config.parse_from(cmd.client_config); + + } else if (ses->version == GameVersion::XB) { + // We should only get a 9E or 9F while the session is unlinked; if we get + // anything else, disconnect + if (command == 0x9E) { + const auto& cmd = check_size_t(data, sizeof(C_LoginExtended_XB_9E)); + string xb_gamertag = cmd.serial_number.decode(); + uint64_t xb_user_id = stoull(cmd.access_key.decode(), nullptr, 16); + uint64_t xb_account_id = cmd.netloc.account_id; + ses->license = s->license_index->verify_xb(xb_gamertag, xb_user_id, xb_account_id); + ses->sub_version = cmd.sub_version; + ses->channel.language = cmd.language; + ses->character_name = cmd.name.decode(ses->channel.language); + ses->xb_netloc = cmd.netloc; + ses->xb_9E_unknown_a1a = cmd.unknown_a1a; + ses->channel.send(0x9F, 0x00); + return; + } else if (command == 0x9F) { + const auto& cmd = check_size_t(data); + ses->config.parse_from(cmd.data); + } else { + throw runtime_error("command is not 9E or 9F"); + } } else if (ses->version == GameVersion::XB) { throw runtime_error("xbox licenses are not implemented"); @@ -325,7 +342,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 } const auto& cmd = check_size_t(data); try { - license = s->license_index->verify_bb(cmd.username.decode(), cmd.password.decode()); + ses->license = s->license_index->verify_bb(cmd.username.decode(), cmd.password.decode()); } catch (const LicenseIndex::missing_license&) { if (!s->allow_unregistered_users) { throw; @@ -336,11 +353,11 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 l->bb_password = cmd.password.decode(); s->license_index->add(l); l->save(); - license = l; + ses->license = l; string l_str = l->str(); ses->log.info("Created license %s", l_str.c_str()); } - login_command_bb = std::move(data); + ses->login_command_bb = std::move(data); } else { throw logic_error("unsupported unlinked session version"); @@ -358,7 +375,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 // If license is non-null, then the client has a password and can be connected // to the remote lobby server. - if (license.get()) { + if (ses->license.get()) { // At this point, we will always close the unlinked session, even if it // doesn't get converted/merged to a linked session should_close_unlinked_session = true; @@ -366,18 +383,18 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 // Look up the linked session for this license (if any) shared_ptr linked_ses; try { - linked_ses = server->id_to_session.at(license->serial_number); + linked_ses = server->id_to_session.at(ses->license->serial_number); linked_ses->log.info("Resuming linked session from unlinked session"); } catch (const out_of_range&) { // If there's no open session for this license, then there must be a valid // destination somewhere - either in the client config or in the unlinked // session - if (config.proxy_destination_address != 0) { - linked_ses.reset(new LinkedSession(server, ses->local_port, ses->version, license, config)); + if (ses->config.proxy_destination_address != 0) { + linked_ses.reset(new LinkedSession(server, ses->local_port, ses->version, ses->license, ses->config)); linked_ses->log.info("Opened licensed session for unlinked session based on client config"); } else if (ses->next_destination.ss_family == AF_INET) { - linked_ses.reset(new LinkedSession(server, ses->local_port, ses->version, license, ses->next_destination)); + linked_ses.reset(new LinkedSession(server, ses->local_port, ses->version, ses->license, ses->next_destination)); linked_ses->log.info("Opened licensed session for unlinked session based on unlinked default destination"); } else { ses->log.error("Cannot open linked session: no valid destination in client config or unlinked session"); @@ -385,7 +402,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 } if (linked_ses.get()) { - server->id_to_session.emplace(license->serial_number, linked_ses); + server->id_to_session.emplace(ses->license->serial_number, linked_ses); if (linked_ses->version() != ses->version) { linked_ses->log.error("Linked session has different game version"); } else { @@ -395,14 +412,16 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 linked_ses->resume( std::move(ses->channel), ses->detector_crypt, - std::move(login_command_bb)); + std::move(ses->login_command_bb)); } else { linked_ses->resume( std::move(ses->channel), ses->detector_crypt, - sub_version, - character_name, - hardware_id); + ses->sub_version, + ses->character_name, + ses->hardware_id, + ses->xb_netloc, + ses->xb_9E_unknown_a1a); } } catch (const exception& e) { linked_ses->log.error("Failed to resume linked session: %s", e.what()); @@ -526,24 +545,19 @@ std::shared_ptr ProxyServer::LinkedSession::require_server_state() return this->require_server()->state; } -void ProxyServer::LinkedSession::resume_xb(shared_ptr c) { - this->sub_version = c->sub_version; - this->character_name = c->game_data.player()->disp.name.decode(); - this->config = c->config; - this->wrapped_client = c; - this->resume_inner(std::move(c->channel), detector_crypt); - c->suspend_timeouts(); -} - void ProxyServer::LinkedSession::resume( Channel&& client_channel, shared_ptr detector_crypt, uint32_t sub_version, const string& character_name, - const string& hardware_id) { + const string& hardware_id, + const XBNetworkLocation& xb_netloc, + const parray& xb_9E_unknown_a1a) { this->sub_version = sub_version; this->character_name = character_name; this->hardware_id = hardware_id; + this->xb_netloc = xb_netloc; + this->xb_9E_unknown_a1a = xb_9E_unknown_a1a; this->resume_inner(std::move(client_channel), detector_crypt); } @@ -720,43 +734,31 @@ void ProxyServer::LinkedSession::send_to_game_server(const char* error_message) this->client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd)); } - if (this->version() == GameVersion::XB) { - if (!this->wrapped_client) { - throw logic_error("wrapped client is missing from XB proxy session"); - } - this->wrapped_client->should_disconnect = false; - s->game_server->connect_client(this->wrapped_client, std::move(this->client_channel)); - on_login_complete(this->wrapped_client); - this->disconnect_action = DisconnectAction::CLOSE_IMMEDIATELY; - this->disconnect(); + const auto& port_name = version_to_login_port_name.at(static_cast(this->version())); + S_Reconnect_19 reconnect_cmd = {{0, s->name_to_port_config.at(port_name)->port, 0}}; + + // If the client is on a virtual connection, we can use any address + // here and they should be able to connect back to the game server. If + // the client is on a real connection, we'll use the sockname of the + // existing connection (like we do in the server 19 command handler). + if (this->client_channel.is_virtual_connection) { + struct sockaddr_in* dest_sin = reinterpret_cast(&this->next_destination); + if (dest_sin->sin_family != AF_INET) { + throw logic_error("ss not AF_INET"); + } + reconnect_cmd.address.store_raw(dest_sin->sin_addr.s_addr); } else { - const auto& port_name = version_to_login_port_name.at(static_cast(this->version())); - - S_Reconnect_19 reconnect_cmd = {{0, s->name_to_port_config.at(port_name)->port, 0}}; - - // If the client is on a virtual connection, we can use any address - // here and they should be able to connect back to the game server. If - // the client is on a real connection, we'll use the sockname of the - // existing connection (like we do in the server 19 command handler). - if (this->client_channel.is_virtual_connection) { - struct sockaddr_in* dest_sin = reinterpret_cast(&this->next_destination); - if (dest_sin->sin_family != AF_INET) { - throw logic_error("ss not AF_INET"); - } - reconnect_cmd.address.store_raw(dest_sin->sin_addr.s_addr); - } else { - const struct sockaddr_in* sin = reinterpret_cast( - &this->client_channel.local_addr); - if (sin->sin_family != AF_INET) { - throw logic_error("existing connection is not ipv4"); - } - reconnect_cmd.address.store_raw(sin->sin_addr.s_addr); + const struct sockaddr_in* sin = reinterpret_cast( + &this->client_channel.local_addr); + if (sin->sin_family != AF_INET) { + throw logic_error("existing connection is not ipv4"); } - - this->client_channel.send(0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd)); - this->disconnect_action = DisconnectAction::CLOSE_IMMEDIATELY; + reconnect_cmd.address.store_raw(sin->sin_addr.s_addr); } + + this->client_channel.send(0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd)); + this->disconnect_action = DisconnectAction::CLOSE_IMMEDIATELY; } } @@ -836,7 +838,9 @@ shared_ptr ProxyServer::get_session_by_name( } shared_ptr ProxyServer::create_licensed_session( - shared_ptr l, uint16_t local_port, GameVersion version, + shared_ptr l, + uint16_t local_port, + GameVersion version, const Client::Config& config) { shared_ptr session(new LinkedSession(this->shared_from_this(), local_port, version, l, config)); auto emplace_ret = this->id_to_session.emplace(session->id, session); diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index 1dbc9bc7..5dc5ae02 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -61,6 +61,8 @@ public: std::string character_name; std::string hardware_id; // Only used for DC sessions std::string login_command_bb; + XBNetworkLocation xb_netloc; + parray xb_9E_unknown_a1a; uint32_t challenge_rank_color_override; std::string challenge_rank_title_override; @@ -91,7 +93,6 @@ public: bool is_in_quest; std::shared_ptr detector_crypt; - std::shared_ptr wrapped_client; struct SavingFile { std::string basename; @@ -145,13 +146,14 @@ public: return this->client_channel.language; } - void resume_xb(std::shared_ptr wrapped_client); void resume( Channel&& client_channel, std::shared_ptr detector_crypt, uint32_t sub_version, const std::string& character_name, - const std::string& hardware_id); + const std::string& hardware_id, + const XBNetworkLocation& xb_netloc, + const parray& xb_9E_unknown_a1a); void resume( Channel&& client_channel, std::shared_ptr detector_crypt, @@ -222,6 +224,19 @@ private: std::shared_ptr detector_crypt; + // Temporary state used just before resuming a LinkedSession. These aren't + // just local variables inside on_input because XB requires two commands to + // get started (9E and 9F), so we need to store this state somewhere between + // those commands. + std::shared_ptr license; + uint32_t sub_version = 0; + std::string character_name; + Client::Config config; + std::string login_command_bb; + std::string hardware_id; + XBNetworkLocation xb_netloc; + parray xb_9E_unknown_a1a; + UnlinkedSession(std::shared_ptr server, struct bufferevent* bev, uint16_t port, GameVersion version); std::shared_ptr require_server() const; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index d9b34c06..e78f4c3c 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -94,28 +94,16 @@ static shared_ptr proxy_options_menu_for_client(shared_ptr c) { - if (c->version() == GameVersion::XB) { - c->server_behavior = ServerBehavior::LOGIN_SERVER; - on_login_complete(c); - - } else { - const auto& port_name = version_to_login_port_name.at(static_cast(c->version())); - auto s = c->require_server_state(); - send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at(port_name)->port); - } + const auto& port_name = version_to_login_port_name.at(static_cast(c->version())); + auto s = c->require_server_state(); + send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at(port_name)->port); } void send_client_to_lobby_server(shared_ptr c) { - if (c->version() == GameVersion::XB) { - c->server_behavior = ServerBehavior::LOBBY_SERVER; - on_login_complete(c); - - } else { - auto s = c->require_server_state(); - const auto& port_name = version_to_lobby_port_name.at(static_cast(c->version())); - send_reconnect(c, s->connect_address_for_client(c), - s->name_to_port_config.at(port_name)->port); - } + auto s = c->require_server_state(); + const auto& port_name = version_to_lobby_port_name.at(static_cast(c->version())); + send_reconnect(c, s->connect_address_for_client(c), + s->name_to_port_config.at(port_name)->port); } void send_client_to_proxy_server(shared_ptr c) { @@ -137,12 +125,7 @@ void send_client_to_proxy_server(shared_ptr c) { ses->remote_guild_card_number = 0; } - if (c->version() == GameVersion::XB) { - ses->resume_xb(c); - c->should_disconnect = true; - } else { - send_reconnect(c, s->connect_address_for_client(c), local_port); - } + send_reconnect(c, s->connect_address_for_client(c), local_port); } static void send_proxy_destinations_menu(shared_ptr c) { @@ -887,13 +870,6 @@ static void on_9E_XB(shared_ptr c, uint16_t, uint32_t, string& data) { } } - try { - c->config.parse_from(cmd.client_config); - } catch (const invalid_argument&) { - // If we can't import the config, assume that the client was not connected - // to newserv before, so we should show the welcome message. - c->config.set_flag(Client::Flag::AT_WELCOME_MESSAGE); - } c->xb_netloc.reset(new XBNetworkLocation(cmd.netloc)); c->xb_9E_unknown_a1a = cmd.unknown_a1a; @@ -947,8 +923,9 @@ static void on_9E_XB(shared_ptr c, uint16_t, uint32_t, string& data) { c->log.info("Created license %s", l_str.c_str()); } - send_update_client_config(c); - on_login_complete(c); + // The 9E command doesn't include the client config, so we need to request it + // separately with a 9F command. The 9F handler will call on_login_complete. + send_command(c, 0x9F, 0x00); } static void on_93_BB(shared_ptr c, uint16_t, uint32_t, string& data) { @@ -1045,13 +1022,35 @@ static void on_93_BB(shared_ptr c, uint16_t, uint32_t, string& data) { } } -static void on_9F_V3(shared_ptr c, uint16_t, uint32_t, string& data) { - if (c->version() == GameVersion::BB) { - const auto& cmd = check_size_t(data); - c->config.parse_from(cmd.data); - } else { - const auto& cmd = check_size_t(data); - c->config.parse_from(cmd.data); +static void on_9F(shared_ptr c, uint16_t, uint32_t, string& data) { + switch (c->version()) { + case GameVersion::GC: { + const auto& cmd = check_size_t(data); + c->config.parse_from(cmd.data); + break; + } + case GameVersion::XB: { + const auto& cmd = check_size_t(data); + // On XB, this command is part of the login sequence, so we may not be + // able to import the config the first time the client connects. If we + // can't import the config, assume that the client was not connected to + // newserv before, so we should show the welcome message. + try { + c->config.parse_from(cmd.data); + } catch (const invalid_argument&) { + c->config.set_flag(Client::Flag::AT_WELCOME_MESSAGE); + } + send_update_client_config(c); + on_login_complete(c); + break; + } + case GameVersion::BB: { + const auto& cmd = check_size_t(data); + c->config.parse_from(cmd.data); + break; + } + default: + throw logic_error("incorrect client version for 9F command"); } } @@ -1063,20 +1062,6 @@ static void on_96(shared_ptr c, uint16_t, uint32_t, string& data) { static void on_B1(shared_ptr c, uint16_t, uint32_t, string& data) { check_size_v(data.size(), 0); send_server_time(c); - // The B1 command is sent in response to a 97 command, which is normally part - // of the pre-ship-select login sequence. However, newserv delays this until - // after the ship select menu so that loading a GameCube program doesn't cause - // the player's items to be deleted when they next play PSO. It's also not a - // good idea to send a 97 and 19 at the same time, because the memory card and - // BBA are on the same EXI bus on the GameCube and this seems to cause the SYN - // packet after a 19 to get dropped pretty often, which causes a delay in - // joining the lobby. This is why we delay the 19 command until the client - // responds after saving. - if (c->should_send_to_lobby_server) { - send_client_to_lobby_server(c); - } else if (c->should_send_to_proxy_server) { - send_client_to_proxy_server(c); - } } static void on_BA_Ep3(shared_ptr c, uint16_t command, uint32_t, string& data) { @@ -3811,7 +3796,7 @@ static void on_6F(shared_ptr c, uint16_t command, uint32_t, string& data add_next_game_client(l); } -static void on_99_GC(shared_ptr c, uint16_t, uint32_t, string& data) { +static void on_99(shared_ptr c, uint16_t, uint32_t, string& data) { check_size_v(data.size(), 0); // This is an odd place to send 6xB4x52, but there's a reason for it. If the @@ -3829,6 +3814,22 @@ static void on_99_GC(shared_ptr c, uint16_t, uint32_t, string& data) { send_ep3_update_game_metadata(watched_l); } } + + // The 99 command is sent in response to a B1 command, which is normally part + // of the pre-ship-select login sequence. However, newserv delays the 97 + // command (and therefore the following B1 command) until after the ship + // select menu so that loading a GameCube program doesn't cause the player's + // items to be deleted when they next play PSO. It's also not a good idea to + // send a 97 and 19 at the same time, because the memory card and BBA are on + // the same EXI bus on the GameCube and this seems to cause the SYN packet + // after a 19 to get dropped pretty often, which causes a delay in joining the + // lobby. This is why we delay the 19 command until the client responds after + // saving. + if (c->should_send_to_lobby_server) { + send_client_to_lobby_server(c); + } else if (c->should_send_to_proxy_server) { + send_client_to_proxy_server(c); + } } static void on_D0_V3_BB(shared_ptr c, uint16_t, uint32_t, string& data) { @@ -4381,13 +4382,13 @@ static on_command_t handlers[0x100][6] = { /* 96 */ {nullptr, on_96, on_96, on_96, on_96, nullptr}, /* 97 */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* 98 */ {nullptr, on_61_98, on_61_98, on_61_98, on_61_98, on_61_98}, - /* 99 */ {nullptr, on_ignored, on_ignored, on_99_GC, on_ignored, on_ignored}, + /* 99 */ {nullptr, on_99, on_99, on_99, on_99, on_99}, /* 9A */ {nullptr, on_9A, on_9A, on_9A, nullptr, nullptr}, /* 9B */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}, /* 9C */ {nullptr, on_9C, on_9C, on_9C, on_9C, nullptr}, /* 9D */ {nullptr, on_9D_9E, on_9D_9E, on_9D_9E, on_9D_9E, nullptr}, /* 9E */ {nullptr, nullptr, on_9D_9E, on_9D_9E, on_9E_XB, nullptr}, - /* 9F */ {nullptr, nullptr, nullptr, on_9F_V3, on_9F_V3, nullptr}, + /* 9F */ {nullptr, nullptr, nullptr, on_9F, on_9F, on_9F}, // PATCH DC PC GC XB BB /* A0 */ {nullptr, on_A0, on_A0, on_A0, on_A0, on_A0}, /* A1 */ {nullptr, on_A1, on_A1, on_A1, on_A1, on_A1}, @@ -4504,7 +4505,6 @@ static void check_unlicensed_command(GameVersion version, uint8_t command) { } break; case GameVersion::GC: - case GameVersion::XB: // See comment in the DC case above for why DC commands are included here. if (command != 0x88 && // DC NTE command != 0x8B && // DC NTE @@ -4518,6 +4518,11 @@ static void check_unlicensed_command(GameVersion version, uint8_t command) { throw runtime_error("only commands 88, 8B, 90, 93, 9A, 9C, 9D, 9E, and DB may be sent before login"); } break; + case GameVersion::XB: + if (command != 0x9E && command != 0x9F) { + throw runtime_error("only commands 9E and 9F may be sent before login"); + } + break; case GameVersion::BB: if (command != 0x93) { throw runtime_error("only command 93 may be sent before login"); diff --git a/src/Version.cc b/src/Version.cc index 7c775aec..950ae97d 100644 --- a/src/Version.cc +++ b/src/Version.cc @@ -9,11 +9,11 @@ using namespace std; const vector version_to_login_port_name = { - "bb-patch", "console-login", "pc-login", "console-login", "console-login", "bb-init"}; + "bb-patch", "console-login", "pc-login", "console-login", "xb-login", "bb-init"}; const vector version_to_lobby_port_name = { - "bb-patch", "console-lobby", "pc-lobby", "console-lobby", "console-lobby", "bb-lobby"}; + "bb-patch", "console-lobby", "pc-lobby", "console-lobby", "xb-lobby", "bb-lobby"}; const vector version_to_proxy_port_name = { - "", "dc-proxy", "pc-proxy", "gc-proxy", "xb", "bb-proxy"}; + "", "dc-proxy", "pc-proxy", "gc-proxy", "xb-proxy", "bb-proxy"}; const char* name_for_version(GameVersion version) { switch (version) { diff --git a/tests/DCv1-GameSmokeTest.test.txt b/tests/DCv1-GameSmokeTest.test.txt index 446bf6cc..624e2843 100644 --- a/tests/DCv1-GameSmokeTest.test.txt +++ b/tests/DCv1-GameSmokeTest.test.txt @@ -85,10 +85,10 @@ I 40469 2023-05-26 10:40:58 - [Commands] Received from C-1 (version=DC command=B I 40469 2023-05-26 10:40:58 - [Commands] Sending to C-1 (version=DC command=B1 flag=00) 0000 | B1 00 1C 00 32 30 32 33 3A 30 35 3A 32 36 3A 20 | 2023:05:26: 0010 | 31 37 3A 34 30 3A 35 38 2E 30 30 30 | 17:40:58.000 -I 40469 2023-05-26 10:40:58 - [Commands] Sending to C-1 (version=DC command=19 flag=00) -0000 | 19 00 0C 00 0A 00 00 04 F6 13 00 00 | I 40469 2023-05-26 10:40:58 - [Commands] Received from C-1 (version=DC command=99 flag=00) 0000 | 99 00 04 00 | +I 40469 2023-05-26 10:40:58 - [Commands] Sending to C-1 (version=DC command=19 flag=00) +0000 | 19 00 0C 00 0A 00 00 04 F6 13 00 00 | I 40469 2023-05-26 10:40:58 - [Server] Client disconnected: C-1 on fd 38 I 40469 2023-05-26 10:40:58 - [C-1] Deleted I 40469 2023-05-26 10:40:58 - [C-2] Created diff --git a/tests/DCv2-GameSmokeTest.test.txt b/tests/DCv2-GameSmokeTest.test.txt index ba5da646..c20c368a 100644 --- a/tests/DCv2-GameSmokeTest.test.txt +++ b/tests/DCv2-GameSmokeTest.test.txt @@ -84,10 +84,10 @@ I 40992 2023-05-26 10:52:57 - [Commands] Received from C-1 (version=DC command=B I 40992 2023-05-26 10:52:57 - [Commands] Sending to C-1 (version=DC command=B1 flag=00) 0000 | B1 00 1C 00 32 30 32 33 3A 30 35 3A 32 36 3A 20 | 2023:05:26: 0010 | 31 37 3A 35 32 3A 35 37 2E 30 30 30 | 17:52:57.000 -I 40992 2023-05-26 10:52:57 - [Commands] Sending to C-1 (version=DC command=19 flag=00) -0000 | 19 00 0C 00 0A 00 00 04 F6 13 00 00 | I 40992 2023-05-26 10:52:57 - [Commands] Received from C-1 (version=DC command=99 flag=00) 0000 | 99 00 04 00 | +I 40992 2023-05-26 10:52:57 - [Commands] Sending to C-1 (version=DC command=19 flag=00) +0000 | 19 00 0C 00 0A 00 00 04 F6 13 00 00 | I 40992 2023-05-26 10:52:57 - [Server] Client disconnected: C-1 on fd 38 I 40992 2023-05-26 10:52:57 - [C-1] Deleted I 40992 2023-05-26 10:52:57 - [C-2] Created diff --git a/tests/GC-Episode3Battle.test.txt b/tests/GC-Episode3Battle.test.txt index 21d0eef9..e4ff742e 100644 --- a/tests/GC-Episode3Battle.test.txt +++ b/tests/GC-Episode3Battle.test.txt @@ -2658,10 +2658,10 @@ I 16332 2023-09-17 10:14:35 - [Commands] Received from C-1 (Tali) (version=GC co I 16332 2023-09-17 10:14:35 - [Commands] Sending to C-1 (Tali) (version=GC command=B1 flag=00) 0000 | B1 00 1C 00 32 30 32 33 3A 30 39 3A 31 37 3A 20 | 2023:09:17: 0010 | 31 37 3A 31 34 3A 33 35 2E 30 30 30 | 17:14:35.000 -I 16332 2023-09-17 10:14:35 - [Commands] Sending to C-1 (Tali) (version=GC command=19 flag=00) -0000 | 19 00 0C 00 23 23 23 23 F6 13 00 00 | #### I 16332 2023-09-17 10:14:37 - [Commands] Received from C-1 (Tali) (version=GC command=99 flag=00) 0000 | 99 00 04 00 | +I 16332 2023-09-17 10:14:35 - [Commands] Sending to C-1 (Tali) (version=GC command=19 flag=00) +0000 | 19 00 0C 00 23 23 23 23 F6 13 00 00 | #### I 16332 2023-09-17 10:14:37 - [Server] Client disconnected: C-1 on virtual connection 0x1048044d0 I 16332 2023-09-17 10:14:37 - [C-1] Deleted W 16332 2023-09-17 10:14:37 - [IPStackSimulator] Failed to process frame: non-SYN frame does not correspond to any open TCP connection diff --git a/tests/GC-Episode3BattleWithSpectator.test.txt b/tests/GC-Episode3BattleWithSpectator.test.txt index edf58a32..49ea7773 100644 --- a/tests/GC-Episode3BattleWithSpectator.test.txt +++ b/tests/GC-Episode3BattleWithSpectator.test.txt @@ -2658,10 +2658,10 @@ I 17097 2023-09-19 21:52:49 - [Commands] Received from C-1 (Tali) (version=GC co I 17097 2023-09-19 21:52:49 - [Commands] Sending to C-1 (Tali) (version=GC command=B1 flag=00) 0000 | B1 00 1C 00 32 30 32 33 3A 30 39 3A 32 30 3A 20 | 2023:09:20: 0010 | 30 34 3A 35 32 3A 34 39 2E 30 30 30 | 04:52:49.000 -I 17097 2023-09-19 21:52:49 - [Commands] Sending to C-1 (Tali) (version=GC command=19 flag=00) -0000 | 19 00 0C 00 23 23 23 23 F6 13 00 00 | #### I 17097 2023-09-19 21:52:50 - [Commands] Received from C-1 (Tali) (version=GC command=99 flag=00) 0000 | 99 00 04 00 | +I 17097 2023-09-19 21:52:49 - [Commands] Sending to C-1 (Tali) (version=GC command=19 flag=00) +0000 | 19 00 0C 00 23 23 23 23 F6 13 00 00 | #### I 17097 2023-09-19 21:52:50 - [Server] Client disconnected: C-1 on virtual connection 0x1027044d0 I 17097 2023-09-19 21:52:50 - [C-1] Deleted W 17097 2023-09-19 21:52:50 - [IPStackSimulator] Failed to process frame: non-SYN frame does not correspond to any open TCP connection @@ -8465,10 +8465,10 @@ I 17097 2023-09-19 21:53:53 - [Commands] Received from C-3 (Tali) (version=GC co I 17097 2023-09-19 21:53:53 - [Commands] Sending to C-3 (Tali) (version=GC command=B1 flag=00) 0000 | B1 00 1C 00 32 30 32 33 3A 30 39 3A 32 30 3A 20 | 2023:09:20: 0010 | 30 34 3A 35 33 3A 35 33 2E 30 30 30 | 04:53:53.000 +I 17097 2023-09-19 21:53:53 - [Commands] Received from C-3 (Tali) (version=GC command=99 flag=00) +0000 | 99 00 04 00 | I 17097 2023-09-19 21:53:53 - [Commands] Sending to C-3 (Tali) (version=GC command=19 flag=00) 0000 | 19 00 0C 00 23 23 23 23 F6 13 00 00 | #### -I 17097 2023-09-19 21:53:54 - [Commands] Received from C-3 (Tali) (version=GC command=99 flag=00) -0000 | 99 00 04 00 | I 17097 2023-09-19 21:53:54 - [Server] Client disconnected: C-3 on virtual connection 0x11ae090a0 I 17097 2023-09-19 21:53:54 - [C-3] Deleted W 17097 2023-09-19 21:53:54 - [IPStackSimulator] Failed to process frame: non-SYN frame does not correspond to any open TCP connection diff --git a/tests/GC-ForestGame.test.txt b/tests/GC-ForestGame.test.txt index 2b4cc93a..52e04585 100644 --- a/tests/GC-ForestGame.test.txt +++ b/tests/GC-ForestGame.test.txt @@ -104,10 +104,10 @@ I 49108 2023-05-26 16:18:06 - [Commands] Received from C-1 (version=GC command=B I 49108 2023-05-26 16:18:06 - [Commands] Sending to C-1 (version=GC command=B1 flag=00) 0000 | B1 00 1C 00 32 30 32 33 3A 30 35 3A 32 36 3A 20 | 2023:05:26: 0010 | 32 33 3A 31 38 3A 30 36 2E 30 30 30 | 23:18:06.000 -I 49108 2023-05-26 16:18:06 - [Commands] Sending to C-1 (version=GC command=19 flag=00) -0000 | 19 00 0C 00 23 23 23 23 F6 13 00 00 | #### I 49108 2023-05-26 16:18:07 - [Commands] Received from C-1 (version=GC command=99 flag=00) 0000 | 99 00 04 00 | +I 49108 2023-05-26 16:18:06 - [Commands] Sending to C-1 (version=GC command=19 flag=00) +0000 | 19 00 0C 00 23 23 23 23 F6 13 00 00 | #### I 49108 2023-05-26 16:18:07 - [Server] Client disconnected: C-1 on virtual connection 0x101c044d0 I 49108 2023-05-26 16:18:07 - [C-1] Deleted W 49108 2023-05-26 16:18:07 - [IPStackSimulator] Failed to process frame: non-SYN frame does not correspond to any open TCP connection diff --git a/tests/PC-BasicGame.test.txt b/tests/PC-BasicGame.test.txt index 77ffbe41..840aa957 100644 --- a/tests/PC-BasicGame.test.txt +++ b/tests/PC-BasicGame.test.txt @@ -92,10 +92,10 @@ I 49484 2023-05-26 16:35:14 - [Commands] Received from C-2 (version=PC command=B I 49484 2023-05-26 16:35:14 - [Commands] Sending to C-2 (version=PC command=B1 flag=00) 0000 | 1C 00 B1 00 32 30 32 33 3A 30 35 3A 32 36 3A 20 | 2023:05:26: 0010 | 32 33 3A 33 35 3A 31 34 2E 30 30 30 | 23:35:14.000 -I 49484 2023-05-26 16:35:14 - [Commands] Sending to C-2 (version=PC command=19 flag=00) -0000 | 0C 00 19 00 0A 00 00 04 F7 13 00 00 | I 49484 2023-05-26 16:35:14 - [Commands] Received from C-2 (version=PC command=99 flag=00) 0000 | 04 00 99 00 | +I 49484 2023-05-26 16:35:14 - [Commands] Sending to C-2 (version=PC command=19 flag=00) +0000 | 0C 00 19 00 0A 00 00 04 F7 13 00 00 | I 49484 2023-05-26 16:35:14 - [Server] Client disconnected: C-2 on fd 38 I 49484 2023-05-26 16:35:14 - [C-2] Deleted I 49484 2023-05-26 16:35:14 - [C-3] Created diff --git a/tests/XB-ForestGame.test.txt b/tests/XB-ForestGame.test.txt index d6113dc6..6b2a0db5 100644 --- a/tests/XB-ForestGame.test.txt +++ b/tests/XB-ForestGame.test.txt @@ -45,6 +45,12 @@ I 7323 2023-11-07 02:01:35 - [Commands] Received from C-1 (version=XB command=9E 0180 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | 0190 | 00 00 00 00 00 00 00 00 | I 7323 2023-11-07 02:01:35 - [Commands] Sending to C-1 (version=XB command=04 flag=00) +0000 | 9F 00 04 00 | +I 7323 2023-11-07 02:01:36 - [Commands] Received from C-1 (version=XB command=96 flag=00) +0000 | 9F 00 24 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0010 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0020 | 00 00 00 00 | +I 7323 2023-11-07 02:01:35 - [Commands] Sending to C-1 (version=XB command=04 flag=00) 0000 | 04 00 2C 00 00 00 01 00 E2 D4 45 39 32 AC 99 83 | , E92 0010 | 00 00 00 34 00 81 00 42 60 00 00 00 00 00 00 00 | 4 B` 0020 | 00 00 00 00 00 00 FF FF 80 FF FF FF | @@ -88,6 +94,8 @@ I 7323 2023-11-07 02:01:42 - [Commands] Received from C-1 (version=XB command=B1 I 7323 2023-11-07 02:01:42 - [Commands] Sending to C-1 (version=XB command=B1 flag=00) 0000 | B1 00 1C 00 32 30 32 33 3A 31 31 3A 30 37 3A 20 | 2023:11:07: 0010 | 30 32 3A 30 31 3A 34 32 2E 30 30 30 | 02:01:42.000 +I 7323 2023-11-07 02:01:42 - [Commands] Received from C-1 (version=XB command=99 flag=00) +0000 | 99 00 04 00 | I 7323 2023-11-07 02:01:42 - [Commands] Sending to C-1 (version=XB command=83 flag=0F) 0000 | 83 0F B8 00 33 00 00 33 01 00 00 00 00 00 00 00 | 3 3 0010 | 33 00 00 33 02 00 00 00 00 00 00 00 33 00 00 33 | 3 3 3 3 @@ -103,8 +111,6 @@ I 7323 2023-11-07 02:01:42 - [Commands] Sending to C-1 (version=XB command=83 fl 00B0 | 0F 00 00 00 00 00 00 00 | I 7323 2023-11-07 02:01:42 - [Commands] Sending to C-1 (version=XB command=95 flag=00) 0000 | 95 00 04 00 | -I 7323 2023-11-07 02:01:42 - [Commands] Received from C-1 (version=XB command=99 flag=00) -0000 | 99 00 04 00 | I 7323 2023-11-07 02:01:43 - [Commands] Received from C-1 (version=XB command=61 flag=03) 0000 | 61 03 80 06 03 00 00 01 02 00 00 00 4C 00 00 00 | a L 0010 | 02 00 05 00 F4 01 00 00 00 00 00 00 00 00 01 00 |