From ec205062ad38eedce0318a5922ec2cd8f18a4974 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Mon, 25 Jul 2022 22:06:53 -0700 Subject: [PATCH] add findings from psox disassembly --- src/CatSession.cc | 19 +- src/Channel.cc | 7 +- src/ChatCommands.cc | 4 +- src/Client.cc | 2 +- src/CommandFormats.hh | 507 +++++++++++++++++++++++-------------- src/Main.cc | 7 +- src/Menu.hh | 18 +- src/PSOEncryption.cc | 26 +- src/PSOEncryption.hh | 16 +- src/PSOProtocol.cc | 20 +- src/PSOProtocol.hh | 7 +- src/Player.cc | 165 ++++++++---- src/Player.hh | 84 ++++-- src/ProxyCommands.cc | 198 ++++++++++----- src/ProxyServer.cc | 21 +- src/Quest.cc | 20 +- src/ReceiveCommands.cc | 256 ++++++++++++++----- src/ReceiveSubcommands.cc | 4 +- src/ReplaySession.cc | 45 ++-- src/SendCommands.cc | 225 ++++++++-------- src/SendCommands.hh | 2 +- src/ServerState.cc | 61 +++-- src/ServerState.hh | 4 +- src/Version.cc | 37 +-- src/Version.hh | 3 +- system/config.example.json | 8 +- 26 files changed, 1124 insertions(+), 642 deletions(-) diff --git a/src/CatSession.cc b/src/CatSession.cc index eef5181d..6e8b5639 100644 --- a/src/CatSession.cc +++ b/src/CatSession.cc @@ -75,17 +75,18 @@ void CatSession::on_channel_input( uint16_t command, uint32_t flag, std::string& data) { if (this->channel.version != GameVersion::BB) { if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) { - const auto& cmd = check_size_t(data, - offsetof(S_ServerInit_DC_PC_GC_02_17_91_9B, after_message), 0xFFFF); - if (this->channel.version == GameVersion::GC) { - this->channel.crypt_in.reset(new PSOGCEncryption(cmd.server_key)); - this->channel.crypt_out.reset(new PSOGCEncryption(cmd.client_key)); - this->log.info("Enabled GC encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", + const auto& cmd = check_size_t(data, + offsetof(S_ServerInit_DC_PC_V3_02_17_91_9B, after_message), 0xFFFF); + if ((this->channel.version == GameVersion::GC) || + (this->channel.version == GameVersion::XB)) { + this->channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key)); + this->channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key)); + this->log.info("Enabled V3 encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", cmd.server_key.load(), cmd.client_key.load()); } else { // PC, DC, or patch server - this->channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key)); - this->channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key)); - this->log.info("Enabled PC encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", + this->channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key)); + this->channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key)); + this->log.info("Enabled V2 encryption (server key %08" PRIX32 ", client key %08" PRIX32 ")", cmd.server_key.load(), cmd.client_key.load()); } } diff --git a/src/Channel.cc b/src/Channel.cc index 85cff151..b26701f9 100644 --- a/src/Channel.cc +++ b/src/Channel.cc @@ -253,9 +253,10 @@ void Channel::send(uint16_t cmd, uint32_t flag, const void* data, size_t size, size_t logical_size; size_t send_data_size = 0; switch (this->version) { + case GameVersion::DC: case GameVersion::GC: - case GameVersion::DC: { - PSOCommandHeaderDCGC header; + case GameVersion::XB: { + PSOCommandHeaderDCV3 header; if (this->crypt_out.get()) { send_data_size = (sizeof(header) + size + 3) & ~3; } else { @@ -311,7 +312,7 @@ void Channel::send(uint16_t cmd, uint32_t flag, const void* data, size_t size, throw logic_error("unimplemented game version in send_command"); } - // All versions of PSO I've seen (PC, GC, BB) have a receive buffer 0x7C00 + // All versions of PSO I've seen (so far) have a receive buffer 0x7C00 // bytes in size if (send_data_size > 0x7C00) { throw runtime_error("outbound command too large"); diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 9032ef8e..1d2eb270 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -246,7 +246,9 @@ static void proxy_command_lobby_event(shared_ptr, send_text_message(session.client_channel, u"$C6No such lobby event."); } else { session.override_lobby_event = new_event; - if (session.version == GameVersion::GC || session.version == GameVersion::BB) { + if ((session.version == GameVersion::GC) || + (session.version == GameVersion::XB) || + (session.version == GameVersion::BB)) { session.client_channel.send(0xDA, session.override_lobby_event); } } diff --git a/src/Client.cc b/src/Client.cc index d37792e3..382df51b 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -31,7 +31,7 @@ Client::Client( log("", client_log.min_level), version(version), bb_game_state(0), - flags(flags_for_version(this->version, 0)), + flags(flags_for_version(this->version, -1)), channel(bev, this->version, nullptr, nullptr, this, string_printf("C-%" PRIX64, this->id), TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN), server_behavior(server_behavior), should_disconnect(false), diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index f0db8e2c..5dcc7633 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -28,6 +28,15 @@ // S/C denotes who sends the command (S = server, C = client, SC = both) // If versions are not specified, the format is the same for all versions. +// The version tokens are as follows: +// D1 = Dreamcast v1 +// DC = Dreamcast v2 +// PC = PSO PC (v2) +// GC = PSO GC Episodes 1&2 and/or Episode 3 +// XB = PSO XBOX Episodes 1&2 +// BB = PSO Blue Burst +// V3 = PSO GC and PSO XBOX (these versions are similar and share many formats) + // For variable-length commands, generally a zero-length array is included on // the end of the struct if the command is received by newserv, and is omitted // if it's sent by newserv. In the latter case, we often use StringWriter to @@ -84,7 +93,7 @@ struct ClientConfigBB { // A patch server session generally goes like this: // Server: 02 (unencrypted) -// (all the following commands encrypted with PSOPC encryption, even on BB) +// (all the following commands encrypted with PSO V2 encryption, even on BB) // Client: 02 // Server: 04 // Client: 04 @@ -123,6 +132,7 @@ struct ClientConfigBB { // 02 (S->C): Start encryption // Client will respond with an 02 command. +// All commands after this command will be encrypted with PSO V2 encryption. // If this command is sent during an encrypted session, the client will not // reject it; it will simply re-initialize its encryption state and respond with // an 02 as normal. @@ -131,10 +141,10 @@ struct ClientConfigBB { struct S_ServerInit_Patch_02 { ptext copyright; - le_uint32_t server_key; - le_uint32_t client_key; - // BB rejects the command if it's larger than this size, so we can't add the - // after_message like we do in the other server init commands + le_uint32_t server_key; // Key for commands sent by server + le_uint32_t client_key; // Key for commands sent by client + // The client rejects the command if it's larger than this size, so we can't + // add the after_message like we do in the other server init commands. }; // 02 (C->S): Encryption started @@ -171,7 +181,10 @@ struct S_OpenFile_Patch_06 { // 07 (S->C): Write file // The client's handler table says this command's maximum size is 0x6010 // including the header, but the only servers I've seen use this command limit -// chunks to 0x4010 (including the header). +// chunks to 0x4010 (including the header). Unlike the game server's 13 and A7 +// commands, the chunks do not need to be the same size - the game opens the +// file with the "a+b" mode each time it is written, so the new data is always +// appended to the end. struct S_WriteFileHeader_Patch_07 { le_uint32_t chunk_index; @@ -181,10 +194,12 @@ struct S_WriteFileHeader_Patch_07 { }; // 08 (S->C): Close current file -// Maximum size 8 (including header) +// The unused field is optional. It's not clear whether this field was ever +// used; it could be a remnant from pre-release testing, or someone could have +// simply set the maximum size of this command incorrectly. struct S_CloseCurrentFile_Patch_08 { - le_uint32_t unknown; // Seems to always be zero + le_uint32_t unused; }; // 09 (S->C): Enter directory @@ -196,7 +211,7 @@ struct S_EnterDirectory_Patch_09 { // 0A (S->C): Exit directory // No arguments -// 0B (S->C): Unknown; maybe start patch session, or go to game's root directory +// 0B (S->C): Start patch session and go to patch root directory // No arguments // 0C (S->C): File checksum request @@ -206,7 +221,7 @@ struct S_FileChecksumRequest_Patch_0C { ptext filename; }; -// 0D (S->C): End of file check requests +// 0D (S->C): End of file checksum requests // No arguments // 0E: Invalid command @@ -214,7 +229,7 @@ struct S_FileChecksumRequest_Patch_0C { // 0F (C->S): File information struct C_FileInformation_Patch_0F { - le_uint32_t request_id; + le_uint32_t request_id; // Matches a request ID from an earlier 0C command le_uint32_t checksum; // CRC32 of the file's data le_uint32_t size; }; @@ -259,37 +274,44 @@ struct S_StartFileDownloads_Patch_11 { // 01 (S->C): Lobby message box // A small message box appears in lower-right corner, and the player must press -// a key to continue. +// a key to continue. The maximum length of the message is 0x200 bytes. +// This format is shared by multiple commands; for all of them except 06 (S->C), +// the guild_card_number field is unused and should be 0. struct SC_TextHeader_01_06_11_B0_EE { le_uint32_t unused; le_uint32_t guild_card_number; - // Text immediately follows this header (char[] on DC/GC, char16_t[] on PC/BB) + // Text immediately follows here (char[] on DC/V3, char16_t[] on PC/BB) }; // 02 (S->C): Start encryption (except on BB) // This command should be used for non-initial sessions (after the client has // already selected a ship, for example). Command 17 should be used instead for // the first connection. -// The client will respond with an (encrypted) 9A or 9E command on PSO GC; on -// PSO PC, the client will respond with an (encrypted) 9A or 9D command. +// All commands after this command will be encrypted with PSO V2 encryption on +// PC, or PSO V3 on V3. +// The client will respond with an (encrypted) 9A or 9E command on V3; on PSO +// PC, the client will respond with an (encrypted) 9A or 9D command. // The copyright field in the below structure must contain the following text: // "DreamCast Lobby Server. Copyright SEGA Enterprises. 1999" -struct S_ServerInit_DC_PC_GC_02_17_91_9B { +struct S_ServerInit_DC_PC_V3_02_17_91_9B { ptext copyright; le_uint32_t server_key; // Key for data sent by server le_uint32_t client_key; // Key for data sent by client - // This field is not part of SEGA's implementation. The client ignores it. + // This field is not part of SEGA's implementation; the client ignores it. + // newserv sends a message here disavowing the preceding copyright notice. ptext after_message; }; // 03 (C->S): Legacy login (non-BB) -struct C_LegacyLogin_PC_GC_03 { +struct C_LegacyLogin_PC_V3_03 { le_uint64_t unused; // Same as unused field in 9D/9E le_uint32_t sub_version; - parray unused2; // Same as the first 4 bytes of 9D/9E's unused2 + uint8_t unknown_a1; + uint8_t language; // Same as 9D/9E + le_uint16_t unknown_a2; // Note: These are suffixed with 2 since they come from the same source data // as the corresponding fields in 9D/9E. (Even though serial_number and // serial_number2 have the same contents in 9E, they do not come from the same @@ -302,13 +324,14 @@ struct C_LegacyLogin_PC_GC_03 { // header.flag specifies if the password was correct. If header.flag is 0, the // password saved to the memory card (if any) is deleted and the client is // disconnected. If header.flag is nonzero, the client responds with an 04 -// command. On GC at least, it's likely this command is a relic from earlier -// versions of the login sequence; most servers use the DB/9A/9C/9E commands for -// logging in GC clients. +// command. On PC/V3, it's likely this command is a relic from earlier versions +// of the login sequence; most servers use the DB/9A/9C/9E commands for logging +// in PC/V3 clients. // No other arguments // 03 (S->C): Start encryption (BB) // Client will respond with an (encrypted) 93 command. +// All commands after this command will be encrypted with PSO BB encryption. // The copyright field in the below structure must contain the following text: // "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM." @@ -316,7 +339,7 @@ struct S_ServerInit_BB_03_9B { ptext copyright; parray server_key; parray client_key; - // This field is not part of SEGA's implementation. The client ignores it. + // As in 02, this field is not part of SEGA's implementation. ptext after_message; }; @@ -324,7 +347,7 @@ struct S_ServerInit_BB_03_9B { // See comments on non-BB 03 (S->C). This is likely a relic of an older, // now-unused sequence. // header.flag is nonzero, but it's not clear what it's used for. -struct C_LegacyLogin_PC_GC_04 { +struct C_LegacyLogin_PC_V3_04 { le_uint64_t unused1; // Same as unused field in 9D/9E le_uint32_t sub_version; uint8_t unknown_a1; @@ -341,9 +364,18 @@ struct C_LegacyLogin_BB_04 { }; // 04 (S->C): Set guild card number and update client config ("security data") -// The client config field in this command is only used by V3 clients (PSO GC). -// We send it anyway to clients on earlier versions (e.g. PSO PC), but they -// simply ignore it. +// header.flag specifies an error code; the format described below is only used +// if this code is 0 (no error). Otherwise, the command has no arguments. +// Error codes: +// 01 = Line is busy (103) +// 02 = Already logged in (104) +// 03 = Incorrect password (106) +// 04 = Account suspended (107) +// 05 = Server down for maintenance (108) +// 06 = Incorrect password (127) +// Any other nonzero value = Generic failure (101) +// The client config field in this command is only used by V3 clients. newserv +// sends it anyway to clients on earlier versions, but they ignore it. // Client will respond with a 96 command, but only the first time it receives // this command - for later 04 commands, the client will still update its client // config but will not respond. Changing the security data at any time seems ok, @@ -351,6 +383,10 @@ struct C_LegacyLogin_BB_04 { // confuse the client, and (on pre-V3 clients) possibly corrupt the character // data. For this reason, newserv tries pretty hard to hide the remote guild // card number when clients connect to the proxy server. +// BB clients have multiple client configs. This command sets the client config +// that is returned by the 9E and 9F commands, but does not affect the client +// config set by the E6 command (and returned in the 93 command). In most cases, +// E6 should be used for BB clients instead of 04. template struct S_UpdateClientConfig { @@ -363,21 +399,22 @@ struct S_UpdateClientConfig { ClientConfigT cfg; }; -struct S_UpdateClientConfig_DC_PC_GC_04 : S_UpdateClientConfig { }; +struct S_UpdateClientConfig_DC_PC_V3_04 : S_UpdateClientConfig { }; struct S_UpdateClientConfig_BB_04 : S_UpdateClientConfig { }; // 05: Disconnect // No arguments // 06: Chat -// Server->client format is same as 01 command. +// Server->client format is same as 01 command. The maximum size of the message +// is 0x200 bytes. // Client->server format is very similar; we include a zero-length array in this // struct to make parsing easier. struct C_Chat_06 { parray unused; union { - char dcgc[0]; + char dcv3[0]; char16_t pcbb[0]; } text; }; @@ -395,7 +432,7 @@ struct S_MenuEntry { ptext text; }; struct S_MenuEntry_PC_BB_07_1F : S_MenuEntry { }; -struct S_MenuEntry_DC_GC_07_1F : S_MenuEntry { }; +struct S_MenuEntry_DC_V3_07_1F : S_MenuEntry { }; // 08 (C->S): Request game list // No arguments @@ -416,7 +453,7 @@ struct S_GameMenuEntry { uint8_t flags; // 02 = locked, 04 = disabled (BB), 10 = battle, 20 = challenge }; struct S_GameMenuEntry_PC_BB_08 : S_GameMenuEntry { }; -struct S_GameMenuEntry_GC_08_Ep3_E6 : S_GameMenuEntry { }; +struct S_GameMenuEntry_V3_08_Ep3_E6 : S_GameMenuEntry { }; // 09 (C->S): Menu item info request // Server will respond with an 11 command, or an A3 if it's the quest menu. @@ -433,13 +470,17 @@ struct C_MenuItemInfoRequest_09 { // 0D: Invalid command -// 0E (S->C): Unknown; possibly legacy join game (PC/GC) +// 0E (S->C): Unknown; possibly legacy join game (PC/V3) +// There is a failure mode in the command handlers on PC and V3 that causes the +// thread receiving the command to loop infinitely doing nothing, effectively +// softlocking the game. struct S_Unknown_PC_0E { PlayerLobbyDataPC lobby_data[4]; // This type is a guess parray unknown_a1; }; +// TODO: Document XB format for this struct S_Unknown_GC_0E { PlayerLobbyDataGC lobby_data[4]; // This type is a guess struct UnknownA0 { @@ -470,14 +511,14 @@ template struct C_MenuSelection_10_Flag01 : C_MenuSelection_10_Flag00 { ptext unknown_a1; }; -struct C_MenuSelection_DC_GC_10_Flag01 : C_MenuSelection_10_Flag01 { }; +struct C_MenuSelection_DC_V3_10_Flag01 : C_MenuSelection_10_Flag01 { }; struct C_MenuSelection_PC_BB_10_Flag01 : C_MenuSelection_10_Flag01 { }; template struct C_MenuSelection_10_Flag02 : C_MenuSelection_10_Flag00 { ptext password; }; -struct C_MenuSelection_DC_GC_10_Flag02 : C_MenuSelection_10_Flag02 { }; +struct C_MenuSelection_DC_V3_10_Flag02 : C_MenuSelection_10_Flag02 { }; struct C_MenuSelection_PC_BB_10_Flag02 : C_MenuSelection_10_Flag02 { }; template @@ -485,13 +526,13 @@ struct C_MenuSelection_10_Flag03 : C_MenuSelection_10_Flag00 { ptext unknown_a1; ptext password; }; -struct C_MenuSelection_DC_GC_10_Flag03 : C_MenuSelection_10_Flag03 { }; +struct C_MenuSelection_DC_V3_10_Flag03 : C_MenuSelection_10_Flag03 { }; struct C_MenuSelection_PC_BB_10_Flag03 : C_MenuSelection_10_Flag03 { }; // 11 (S->C): Ship info // Same format as 01 command. -// 12 (S->C): Valid but ignored (PC/GC/BB) +// 12 (S->C): Valid but ignored (PC/V3/BB) // 13 (S->C): Write online quest file // Used for downloading online quests. For download quests (to be saved to the @@ -513,27 +554,31 @@ struct S_WriteFile_13_A7 { // This structure is for documentation only; newserv ignores these. // header.flag = file chunk index (same as in the 13/A7 sent by the server) -struct C_WriteFileConfirmation_GC_BB_13_A7 { +struct C_WriteFileConfirmation_V3_BB_13_A7 { ptext filename; }; -// 14 (S->C): Valid but ignored (PC/GC/BB) +// 14 (S->C): Valid but ignored (PC/V3/BB) // 15: Invalid command -// 16 (S->C): Valid but ignored (PC/GC/BB) +// 16 (S->C): Valid but ignored (PC/V3/BB) // 17 (S->C): Start encryption at login server (except on BB) // Same format as 02 command, but a different copyright string. -// V3 (PSO GC) clients will respond with a DB command the first time they -// receive a 17 command in any online session; after the first time, they will -// respond with a 9E. Non-V3 clients will respond with a 9D. +// All commands after this command will be encrypted with PSO V2 encryption on +// PC, or PSO V3 on V3. +// V3 clients will respond with a DB command the first time they receive a 17 +// command in any online session; after the first time, they will respond with a +// 9E. Non-V3 clients will respond with a 9D. // The copyright field in the structure must contain the following text: // "DreamCast Port Map. Copyright SEGA Enterprises. 1999" -// 18 (S->C): License verification result (PC/GC) +// 18 (S->C): License verification result (PC/V3) // Behaves exactly the same as 9A (S->C). No arguments except header.flag. // 19 (S->C): Reconnect to different address // Client will disconnect, and reconnect to the given address/port. +// Note: PSO XB seems to ignore the address field, which makes sense given its +// networking architecture. struct S_Reconnect_19 { be_uint32_t address; @@ -559,9 +604,9 @@ struct S_ReconnectSplit_19 { }; // 1A (S->C): Large message box -// On V3 (PSO GC), client will usually respond with a D6 command (see D6 for -// more information). -// Contents are plain text (char on DC/GC, char16_t on PC/BB). There must be at +// On V3, client will usually respond with a D6 command (see D6 for more +// information). +// Contents are plain text (char on DC/V3, char16_t on PC/BB). There must be at // least one null character ('\0') before the end of the command data. // There is a bug in V3 (and possibly all versions) where if this command is // sent after the client has joined a lobby, the chat log window contents will @@ -569,8 +614,8 @@ struct S_ReconnectSplit_19 { // The maximum length of the message is 0x400 bytes. This is the only difference // between this command and the D5 command. -// 1B (S->C): Valid but ignored (PC/GC) -// 1C (S->C): Valid but ignored (PC/GC) +// 1B (S->C): Valid but ignored (PC/V3) +// 1C (S->C): Valid but ignored (PC/V3) // 1D: Ping // No arguments @@ -581,7 +626,7 @@ struct S_ReconnectSplit_19 { // 1F (C->S): Request information menu // No arguments -// This command is used in PSO PC. It exists in PSO GC as well but is apparently +// This command is used in PSO PC. It exists in V3 as well but is apparently // unused. // 1F (S->C): Information menu @@ -677,17 +722,22 @@ struct S_GuildCardSearchResult { le_uint32_t result_guild_card_number; HeaderT reconnect_command_header; S_Reconnect_19 reconnect_command; - // The format of this string is "GAME-NAME,Block ##,SERVER-NAME". If the - // result player is not in a game, GAME-NAME may be a lobby name (e.g. - // "LOBBY01") or simply blank (so the string begins with a comma). + // The format of this string is "ROOM-NAME,BLOCK##,SERVER-NAME". If the result + // player is not in a game, GAME-NAME should be the lobby name - for standard + // lobbies this is "BLOCK-"; for CARD lobbies this is + // "BLOCK-C". ptext location_string; + // If the player chooses to meet the user, this menu ID and lobby ID is sent + // in the login command (9D/9E) after connecting to the server designated in + // reconnect_command. In fact, the remaining fields in this structure directly + // match the fields in the extended 9D/9E commands. le_uint32_t menu_id; le_uint32_t lobby_id; - ptext unused; + ptext unused; ptext name; }; struct S_GuildCardSearchResult_PC_41 : S_GuildCardSearchResult { }; -struct S_GuildCardSearchResult_DC_GC_41 : S_GuildCardSearchResult { }; +struct S_GuildCardSearchResult_DC_V3_41 : S_GuildCardSearchResult { }; struct S_GuildCardSearchResult_BB_41 : S_GuildCardSearchResult { }; // 42: Invalid command @@ -699,7 +749,7 @@ struct S_GuildCardSearchResult_BB_41 : S_GuildCardSearchResult name; parray unused; le_uint16_t flags; // 0 = download quest, 2 = online quest, 3 = Episode 3 @@ -707,6 +757,13 @@ struct S_OpenFile_PC_GC_44_A6 { le_uint32_t file_size; }; +// Curiously, PSO XB expects an extra 0x18 bytes at the end of this command, but +// those extra bytes are unused, and the client does not fail if they're +// omitted. +struct S_OpenFile_XB_44_A6 : S_OpenFile_PC_V3_44_A6 { + parray unused2; +}; + struct S_OpenFile_BB_44_A6 { parray unused; le_uint16_t flags; @@ -762,9 +819,10 @@ struct C_OpenFileConfirmation_44_A6 { // the client will exhibit undefined behavior. // 61 (C->S): Player data -// See PSOPlayerDataPC, PSOPlayerDataGC, and PSOPlayerDataBB in Player.hh for -// this command's format. header.flag appears to be the game's major version: -// it's 02 on PSO PC, 03 on PSO GC and BB, and 04 on Episode 3. +// See PSOPlayerDataPC, PSOPlayerDataV3, and PSOPlayerDataBB in Player.hh for +// this command's format. header.flag specifies the format version, which is +// related to (but not identical to) the game's major version. For example, the +// format version is 02 on PSO PC, 03 on PSO GC, XB, and BB, and 04 on Ep3. // Upon joining a game, the client assigns inventory item IDs sequentially as // (0x00010000 + (0x00200000 * lobby_client_id) + x). So, for example, player // 3's 8th item's ID would become 0x00610007. The item IDs from the last game @@ -811,17 +869,25 @@ struct S_JoinGame { uint8_t unused2; // Should be 1 for PSO PC? uint8_t solo_mode; uint8_t unused3; - struct PlayerEntry { - PlayerInventory inventory; - DispDataT disp; - }; - // This field is only present if the game (and client) is Episode 3. Similarly - // to lobby_data above, all four of these are always present and they are - // filled in in slot positions. - PlayerEntry players_ep3[4]; }; -struct S_JoinGame_PC_64 : S_JoinGame { }; -struct S_JoinGame_GC_64 : S_JoinGame { }; + +struct S_JoinGame_PC_64 : S_JoinGame { }; +struct S_JoinGame_GC_64 : S_JoinGame { }; + +struct S_JoinGame_GC_Ep3_64 : S_JoinGame_GC_64 { + // This field is only present if the game (and client) is Episode 3. Similarly + // to lobby_data in the base struct, all four of these are always present and + // they are filled in in slot positions. + struct { + PlayerInventory inventory; + PlayerDispDataPCV3 disp; + } players_ep3[4]; +}; + +struct S_JoinGame_XB_64 : S_JoinGame { + parray unknown_a1; +}; + struct S_JoinGame_BB_64 : S_JoinGame { }; // 65 (S->C): Other player joined game @@ -833,9 +899,11 @@ struct S_JoinLobby { uint8_t leader_id; uint8_t disable_udp; uint8_t lobby_number; - le_uint16_t block_number; - le_uint16_t event; - le_uint32_t unused; + uint8_t block_number; + uint8_t unknown_a1; + uint8_t event; + uint8_t unknown_a2; + parray unknown_a3; struct Entry { LobbyDataT lobby_data; PlayerInventory inventory; @@ -849,10 +917,35 @@ struct S_JoinLobby { return offsetof(S_JoinLobby, entries) + used_entries * sizeof(Entry); } }; -struct S_JoinLobby_PC_65_67_68 : S_JoinLobby { }; -struct S_JoinLobby_GC_65_67_68 : S_JoinLobby { }; +struct S_JoinLobby_PC_65_67_68 : S_JoinLobby { }; +struct S_JoinLobby_GC_65_67_68 : S_JoinLobby { }; struct S_JoinLobby_BB_65_67_68 : S_JoinLobby { }; +struct S_JoinLobby_XB_65_67_68 { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t lobby_number; + uint8_t block_number; + uint8_t unknown_a1; + uint8_t event; + uint8_t unknown_a2; + parray unknown_a3; + parray unknown_a4; + struct Entry { + PlayerLobbyDataXB lobby_data; + PlayerInventory inventory; + PlayerDispDataPCV3 disp; + }; + // Note: not all of these will be filled in and sent if the lobby isn't full + // (the command size will be shorter than this struct's size) + Entry entries[0x0C]; + + static inline size_t size(size_t used_entries) { + return offsetof(S_JoinLobby_XB_65_67_68, entries) + used_entries * sizeof(Entry); + } +}; + // 66 (S->C): Other player left game // This is sent to all players in a game except the leaving player. @@ -907,9 +1000,9 @@ struct S_LeaveLobby_66_69_Ep3_E9 { // 7E: Invalid command // 7F: Invalid command -// 80 (S->C): Ignored (PC/GC) +// 80 (S->C): Ignored (PC/V3) -struct S_Unknown_PC_GC_80 { +struct S_Unknown_PC_V3_80 { le_uint32_t which; // Expected to be in the range 00-0B... maybe client ID? le_uint32_t unknown_a1; // Could be player_tag le_uint32_t unknown_a2; // Could be guild_card_number @@ -926,7 +1019,7 @@ struct S_Unknown_PC_GC_80 { // uninitialized data for security reasons before forwarding and things seem to // work fine. -struct SC_SimpleMail_GC_81 { +struct SC_SimpleMail_V3_81 { le_uint32_t player_tag; le_uint32_t from_guild_card_number; ptext from_name; @@ -999,7 +1092,7 @@ struct S_ArrowUpdateEntry_88 { // No arguments // 8A (S->C): Lobby/game name -// Contents is a string (char16_t on PC/BB, char on DC/GC) containing the lobby +// Contents is a string (char16_t on PC/BB, char on DC/V3) containing the lobby // or game name. The client generally only sends this immediately after joining // a game, but Sega's servers also replied to it if it was sent in a lobby. They // would return a string like "LOBBY01" even though this would never be used @@ -1011,18 +1104,18 @@ struct S_ArrowUpdateEntry_88 { // 8E: Invalid command // 8F: Invalid command -// 90 (C->S): Legacy login (PC/GC) +// 90 (C->S): Legacy login (PC/V3) // From looking at Sylverant source, it appears this command is used during the -// DC login sequence. If a GC client receives a 91 command, however, it will +// DC login sequence. If a V3 client receives a 91 command, however, it will // also send a 90 in response, though the contents will be blank (all zeroes). -struct C_LegacyLogin_GC_90 { +struct C_LegacyLogin_V3_90 { ptext serial_number; ptext access_key; parray unused; }; -// 90 (S->C): License verification result (GC) +// 90 (S->C): License verification result (V3) // Behaves exactly the same as 9A (S->C). No arguments except header.flag. // 91 (S->C): Start encryption at login server (legacy; non-BB only) @@ -1079,7 +1172,7 @@ struct C_Login_BB_93 { // 96 (C->S): Character save information -struct C_CharSaveInfo_GC_BB_96 { +struct C_CharSaveInfo_V3_BB_96 { // This field appears to be a checksum or random stamp of some sort; it seems // to be unique and constant per character. le_uint32_t unknown_a1; @@ -1101,8 +1194,7 @@ struct C_CharSaveInfo_GC_BB_96 { // Client will respond with a B1 command if header.flag is nonzero. // 98 (C->S): Leave game -// Same format as 61 command. header.flag appears to be the major game version; -// it's 03 on PSO GC Episodes 1 & 2 (and BB) and 04 on Episode 3. +// Same format as 61 command. // Client will send an 84 when it's ready to join a lobby. // 99 (C->S): Server time accepted @@ -1110,7 +1202,7 @@ struct C_CharSaveInfo_GC_BB_96 { // 9A (C->S): Initial login (no password or client config) -struct C_Login_DC_PC_GC_9A { +struct C_Login_DC_PC_V3_9A { ptext unused1; ptext unused2; ptext serial_number; @@ -1158,13 +1250,13 @@ struct C_Login_DC_PC_GC_9A { // 9C (C->S): Register // It appears PSO GC sends uninitialized data in the header.flag field here. -struct C_Register_DC_PC_GC_9C { +struct C_Register_DC_PC_V3_9C { le_uint64_t unused; le_uint32_t sub_version; le_uint32_t unused2; - ptext serial_number; - ptext access_key; - ptext password; + ptext serial_number; // On XB, this is the XBL gamertag + ptext access_key; // On XB, this is the XBL user ID + ptext password; // On XB, this is unused }; struct C_Register_BB_9C { @@ -1180,11 +1272,20 @@ struct C_Register_BB_9C { // displayed if the header.flag field is zero. If header.flag is nonzero, the // client proceeds with the login procedure by sending a 9D/9E. -// 9D (C->S): Log in -// Not used on V3 (PSO GC) - the client sends 9E instead. -// In some cases the client sends extra unused data at the end of this command. -// This seems to only occur if the client has not yet received an 04 (guild card -// number / client config) command. +// 9D (C->S): Log in without client config (PC) +// Not used on V3 - the client sends 9E instead. +// The extended version of this command is sent if the client has not yet +// received an 04 (in which case the extended fields are blank) or if the client +// selected the Meet User option, in which case it specifies the requested lobby +// by its menu ID and item ID. + +template +struct C_Login_MeetUserExtension { + le_uint32_t menu_id; + le_uint32_t preferred_lobby_id; + parray unknown_a1; + ptext target_player_name; +}; struct C_Login_PC_9D { le_uint32_t player_tag; // 0x00010000 if guild card is set (via 04) @@ -1196,23 +1297,19 @@ struct C_Login_PC_9D { parray unused3; // Always zeroes? ptext unused1; // Same as unused1/unused2 in 9A ptext unused2; - ptext serial_number; - ptext access_key; - ptext serial_number2; - ptext access_key2; + ptext serial_number; // On XB, this is the XBL gamertag + ptext access_key; // On XB, this is the XBL user ID + ptext serial_number2; // On XB, this is the XBL gamertag + ptext access_key2; // On XB, this is the XBL user ID ptext name; }; struct C_LoginExtended_PC_9D : C_Login_PC_9D { - le_uint32_t menu_id; - le_uint32_t preferred_lobby_id; - parray unknown_a1; // TODO: target_player_name is somewhere in here + C_Login_MeetUserExtension extension; }; -// 9E (C->S): Log in with client config -// Not used on versions before V3 (PSO GC). -// In some cases the client sends extra unused data at the end of this command. -// This seems to only occur if the client has not yet received an 04 (guild card -// number / client config) command. +// 9E (C->S): Log in with client config (V3/BB) +// The extended version of this command is used in the same circumstances as +// when PSO PC uses the extended version of the 9D command. struct C_Login_GC_9E : C_Login_PC_9D { union ClientConfigFields { @@ -1222,11 +1319,15 @@ struct C_Login_GC_9E : C_Login_PC_9D { } client_config; }; struct C_LoginExtended_GC_9E : C_Login_GC_9E { - le_uint32_t menu_id; - le_uint32_t preferred_lobby_id; - parray unknown_a1; - ptext target_player_name; - parray unknown_a2; + C_Login_MeetUserExtension extension; +}; + +struct C_Login_XB_9E : C_Login_GC_9E { + XBNetworkLocation netloc; + parray unknown_a1; +}; +struct C_LoginExtended_XB_9E : C_Login_XB_9E { + C_Login_MeetUserExtension extension; }; struct C_LoginExtended_BB_9E { @@ -1243,17 +1344,18 @@ struct C_LoginExtended_BB_9E { ptext password; ptext guild_card_number_str; parray unknown_a7; - parray unused; // Always zero + C_Login_MeetUserExtension extension; }; -// 9F (S->C): Request client config / security data +// 9F (S->C): Request client config / security data (V3/BB) // No arguments -// 9F (C->S): Client config / security data response +// 9F (C->S): Client config / security data response (V3/BB) // The data is opaque to the client, as described at the top of this file. // If newserv ever sent a 9F command (it currently does not), the response -// format here would be ClientConfig (0x20 bytes) on GC, or ClientConfigBB (0x28 -// bytes) on BB. +// format here would be ClientConfig (0x20 bytes) on V3, or ClientConfigBB (0x28 +// bytes) on BB. However, on BB, this returns the client config that was set by +// a preceding 04 command, not the config set by a preceding E6 command. // A0 (C->S): Change ship // This structure is for documentation only; newserv ignores the arguments here. @@ -1261,7 +1363,7 @@ struct C_LoginExtended_BB_9E { struct C_ChangeShipOrBlock_A0_A1 { le_uint32_t player_tag; le_uint32_t guild_card_number; - parray unused; // Verified as unused from client disassembly + parray unused; }; // A0 (S->C): Ship select menu @@ -1282,33 +1384,30 @@ struct C_ChangeShipOrBlock_A0_A1 { // should send another quest menu (if a category was chosen), or send the quest // data with 44/13 commands; for A9, the server does not need to respond. -template +template struct S_QuestMenuEntry { le_uint32_t menu_id; le_uint32_t item_id; ptext name; - ptext short_desc; -}; -struct S_QuestMenuEntry_PC_A2_A4 : S_QuestMenuEntry { }; -struct S_QuestMenuEntry_GC_A2_A4 : S_QuestMenuEntry { }; - -struct S_QuestMenuEntry_BB_A2_A4 { - le_uint32_t menu_id; - le_uint32_t item_id; - ptext name; - // Why is this 10 characters longer than on other versions...? - ptext short_desc; + ptext short_desc; }; +struct S_QuestMenuEntry_PC_A2_A4 : S_QuestMenuEntry { }; +struct S_QuestMenuEntry_GC_A2_A4 : S_QuestMenuEntry { }; +struct S_QuestMenuEntry_XB_A2_A4 : S_QuestMenuEntry { }; +struct S_QuestMenuEntry_BB_A2_A4 : S_QuestMenuEntry { }; // A3 (S->C): Quest information // Same format as 1A/D5 command (plain text) // A4 (S->C): Download quest menu // Same format as A2, but can be used when not in a game. The client responds -// similarly as for command A2; the primary difference is that if a quest is -// chosen, it should be sent with A6/A7 commands rather than 44/13, and it must -// be in a different encrypted format. The download quest format is documented -// in create_download_quest and create_download_quest_file in Quest.cc. +// similarly as for command A2 with the following differences: +// - Descriptions should be sent with the A5 command instead of A3. +// - If a quest is chosen, it should be sent with A6/A7 commands rather than +// 44/13, and it must be in a different encrypted format. The download quest +// format is documented in create_download_quest_file in Quest.cc. +// - After the download is done, or if the player cancels the menu, the client +// sends an A0 command instead of A9. // A5 (S->C): Download quest information // Same format as 1A/D5 command (plain text) @@ -1319,6 +1418,8 @@ struct S_QuestMenuEntry_BB_A2_A4 { // if the filename ends in .bin/.dat (download quests), .gba (GameBoy Advance // games), and, curiously, .pvr (textures). To my knowledge, the .pvr handler in // this command has never been used. +// It also appears that the .gba handler was not deleted in PSO XB, even though +// it doesn't make sense for an XB client to receive such a file. // For .bin files, the flags field should be zero. For .pvr files, the flags // field should be 1. For .dat and .gba files, it seems the value in the flags // field does not matter. @@ -1363,8 +1464,8 @@ struct S_ConfirmUpdateQuestStatistics_AB { le_uint16_t unknown_a3; // Schtserv always sends 0xBFFF here }; -// AC: Quest barrier -// No arguments; header.flag must be 0 +// AC: Quest barrier (V3/BB) +// No arguments; header.flag must be 0 (or else the client disconnects) // After a quest begins loading in a game (the server sends 44/13 commands to // each player with the quest's data), each player will send an AC to the server // when it has parsed the quest and is ready to start. When all players in a @@ -1400,13 +1501,13 @@ struct S_ConfirmUpdateQuestStatistics_AB { // relocated, but is not actually executed, so the return_value field in the // resulting B3 command is always 0. The checksum functionality does work on PSO // PC, just like the other versions. -// This command doesn't work on the later JP PSO Plus (v1.05), US PSO Plus -// (v1.02), or US/EU Episode 3. Sega presumably removed it after taking heat -// from Nintendo about enabling homebrew on the GameCube. On the earlier JP PSO -// Plus (v1.04) and JP Episode 3, this command is implemented as described here, -// with some additional compression and encryption steps added, similarly to how -// download quests are encoded. See send_function_call in SendCommands.cc for -// more details on how this works. +// This command doesn't work on some PSO GC versions, namely the later JP PSO +// Plus (v1.05), US PSO Plus (v1.02), or US/EU Episode 3. Sega presumably +// removed it after taking heat from Nintendo about enabling homebrew on the +// GameCube. On the earlier JP PSO Plus (v1.04) and JP Episode 3, this command +// is implemented as described here, with some additional compression and +// encryption steps added, similarly to how download quests are encoded. See +// send_function_call in SendCommands.cc for more details on how this works. struct S_ExecuteCode_B2 { // If code_size == 0, no code is executed, but checksumming may still occur. @@ -1420,12 +1521,18 @@ struct S_ExecuteCode_B2 { template struct S_ExecuteCode_Footer_B2 { - // Relocations is a list of words (uint16_t) containing the number of - // doublewords (uint32_t) to skip for each relocation. The relocation pointer - // starts immediately after the checksum_size field in the header, and - // advances by the value of one relocation word (times 4) before each - // relocation. At each relocated doubleword, the address of the first byte of - // the code (after checksum_size) is added to the existing value. + // Relocations is a list of words (le_uint16_t on PC/XB/BB, be_uint16_t on GC) + // containing the number of doublewords (uint32_t) to skip for each + // relocation. The relocation pointer starts immediately after the + // checksum_size field in the header, and advances by the value of one + // relocation word (times 4) before each relocation. At each relocated + // doubleword, the address of the first byte of the code (after checksum_size) + // is added to the existing value. + // For example, if the code segment contains the following data (where R + // specifies doublewords to relocate): + // RR RR RR RR ?? ?? ?? ?? ?? ?? ?? ?? RR RR RR RR + // RR RR RR RR ?? ?? ?? ?? RR RR RR RR + // then the relocation words should be 0000, 0003, 0001, and 0002. // If there is a small number of relocations, they may be placed in the unused // fields of this structure to save space and/or confuse reverse engineers. // The game never accesses the last 12 bytes of this structure unless @@ -1443,15 +1550,15 @@ struct S_ExecuteCode_Footer_B2 { }; struct S_ExecuteCode_Footer_GC_B2 : S_ExecuteCode_Footer_B2 { }; -struct S_ExecuteCode_Footer_PC_BB_B2 : S_ExecuteCode_Footer_B2 { }; +struct S_ExecuteCode_Footer_PC_XB_BB_B2 : S_ExecuteCode_Footer_B2 { }; // B3 (C->S): Execute code and/or checksum memory result // Not used on versions that don't support the B2 command (see above). struct C_ExecuteCodeResult_B3 { - // On PSO PC, return_value is always 0. - // On PSO GC, return_value holds the value in r3 when the function returned. - // On PSO BB, return_value holds the value in eax when the function returned. + // On PC, return_value is always 0. + // On GC, return_value has the value in r3 when the function returns. + // On XB and BB, return_value has the value in eax when the function returns. // If code_size was 0 in the B2 command, return_value is always 0. le_uint32_t return_value; le_uint32_t checksum; // 0 if no checksum was computed @@ -1545,7 +1652,7 @@ struct S_ChoiceSearchEntry { le_uint16_t category_id; ptext text; }; -struct S_ChoiceSearchEntry_DC_GC_C0 : S_ChoiceSearchEntry { }; +struct S_ChoiceSearchEntry_DC_V3_C0 : S_ChoiceSearchEntry { }; struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry { }; // Top-level categories are things like "Level", "Class", etc. @@ -1575,7 +1682,7 @@ struct C_CreateGame { uint8_t challenge_mode; // 0 or 1 uint8_t episode; // 1-4 on V3+ (3 on Episode 3); unused on DC/PC }; -struct C_CreateGame_DC_GC_C1_EC : C_CreateGame { }; +struct C_CreateGame_DC_V3_C1_Ep3_EC : C_CreateGame { }; struct C_CreateGame_PC_C1 : C_CreateGame { }; struct C_CreateGame_BB_C1 : C_CreateGame { @@ -1611,9 +1718,9 @@ struct C_ExecuteChoiceSearch_C3 { // C4 (S->C): Choice search results // Command is a list of these; header.flag is the entry count -struct S_ChoiceSearchResultEntry_GC_C4 { +struct S_ChoiceSearchResultEntry_V3_C4 { le_uint32_t guild_card_number; - ptext name; // No language marker, as usual on GC + ptext name; // No language marker, as usual on V3 ptext info_string; // Usually something like " Lvl " // Format is stricter here; this is "LOBBYNAME,BLOCKNUM,SHIPNAME" // If target is in game, for example, "Game Name,BLOCK01,Alexandria" @@ -1629,33 +1736,36 @@ struct S_ChoiceSearchResultEntry_GC_C4 { parray unused2; }; -// C5 (S->C): Challenge rank update (GC/BB) +// C5 (S->C): Challenge rank update (V3/BB) // header.flag = entry count // The server sends this command when a player joins a lobby to update the // challenge mode records of all the present players. -// Entry format is PlayerChallengeDataGC or PlayerChallengeDataBB. -// newserv currently doesn't send this command at all because the GC and +// Entry format is PlayerChallengeDataV3 or PlayerChallengeDataBB. +// newserv currently doesn't send this command at all because the V3 and // BB formats aren't fully documented. // TODO: Figure out where the text is in those formats, write appropriate // conversion functions, and implement the command. Don't forget to override the // client_id field in each entry before sending. -// C6 (C->S): Set blocked senders list (GC/BB) +// C6 (C->S): Set blocked senders list (V3/BB) -struct C_SetBlockedSenders_GC_BB_C6 { +struct C_SetBlockedSenders_V3_BB_C6 { // The command always contains 30 entries, even if the entries at the end are // blank (zero). parray blocked_senders; }; -// C7 (C->S): Enable simple mail auto-reply (GC/BB) +// C7 (C->S): Enable simple mail auto-reply (V3/BB) // Same format as 1A/D5 command (plain text). // Server does not respond -// C8 (C->S): Disable simple mail auto-reply (GC/BB) +// C8 (C->S): Disable simple mail auto-reply (V3/BB) // No arguments // Server does not respond +// C9 (C->S): Unknown (XBOX) +// No arguments except header.flag + // C9: Broadcast command (Episode 3) // Same as 60, but only send to Episode 3 clients. @@ -1713,7 +1823,7 @@ struct S_Unknown_GC_Ep3_CC { // CE: Invalid command // CF: Invalid command -// D0 (C->S): Start trade sequence (GC/BB) +// D0 (C->S): Start trade sequence (V3/BB) // The trade window sequence is a bit complicated. The normal flow is: // - Clients sync trade state with 60xA6 commands (technically 62xA6) // - When both have confirmed, one client (the initiator) sends a D0 @@ -1742,29 +1852,29 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server ItemData items[0x20]; }; -// D1 (S->C): Advance trade state (GC/BB) +// D1 (S->C): Advance trade state (V3/BB) // No arguments // See D0 description for usage information. -// D2 (C->S): Trade can proceed (GC/BB) +// D2 (C->S): Trade can proceed (V3/BB) // No arguments // See D0 description for usage information. -// D3 (S->C): Execute trade (GC/BB) +// D3 (S->C): Execute trade (V3/BB) // Same format as D0. See D0 description for usage information. -// D4 (C->S): Trade failed (GC/BB) +// D4 (C->S): Trade failed (V3/BB) // No arguments // See D0 description for usage information. -// D4 (S->C): Trade complete (GC/BB) +// D4 (S->C): Trade complete (V3/BB) // header.flag must be 0 (trade failed) or 1 (trade complete). // See D0 description for usage information. -// D5: Large message box (GC/BB) +// D5: Large message box (V3/BB) // Same as 1A command, except the maximum length of the message is 0x1000 bytes. -// D6 (C->S): Large message box closed (GC) +// D6 (C->S): Large message box closed (V3) // No arguments // DC, PC, and BB do not send this command at all. GC US v1.00 and v1.01 will // send this command when any large message box (1A/D5) is closed; GC Plus and @@ -1773,26 +1883,28 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server // still be displayed if sent by the server, but the client won't send a D6 when // they are closed.) -// D7 (C->S): Request GBA game file (GC) +// D7 (C->S): Request GBA game file (V3) // The server should send the requested file using A6/A7 commands. +// This command exists on XB as well, but it presumably is never sent by the +// client. -struct C_GBAGameRequest_GC_D7 { +struct C_GBAGameRequest_V3_D7 { ptext filename; }; -// D7 (S->C): Unknown (GC/BB) +// D7 (S->C): Unknown (V3/BB) // No arguments -// On PSO GC, this command does... something. The command isn't *completely* +// On PSO V3, this command does... something. The command isn't *completely* // ignored: it sets a global state variable, but it's not clear what that // variable does. That variable is also set when a D7 is sent by the client, so // it likely is related to GBA game loading in some way. // PSO BB completely ignores this command. -// D8 (C->S): Info board request (GC/BB) +// D8 (C->S): Info board request (V3/BB) // No arguments // The server should respond with a D8 command (described below). -// D8 (S->C): Info board (GC/BB) +// D8 (S->C): Info board contents (V3/BB) // Command is a list of these; header.flag is the entry count. There should be // one entry for each player in the current lobby/game. @@ -1802,27 +1914,27 @@ struct S_InfoBoardEntry_D8 { ptext message; }; struct S_InfoBoardEntry_BB_D8 : S_InfoBoardEntry_D8 { }; -struct S_InfoBoardEntry_GC_D8 : S_InfoBoardEntry_D8 { }; +struct S_InfoBoardEntry_V3_D8 : S_InfoBoardEntry_D8 { }; -// D9 (C->S): Write info board (GC/BB) +// D9 (C->S): Write info board (V3/BB) // Contents are plain text, like 1A/D5. // Server does not respond -// DA (S->C): Change lobby event (GC/BB) +// DA (S->C): Change lobby event (V3/BB) // header.flag = new event number; no other arguments. -// DB (C->S): Verify license (GC/BB) +// DB (C->S): Verify license (V3/BB) // Server should respond with a 9A command. -struct C_VerifyLicense_GC_DB { +struct C_VerifyLicense_V3_DB { ptext unused; - ptext serial_number; - ptext access_key; + ptext serial_number; // On XB, this is the XBL gamertag + ptext access_key; // On XB, this is the XBL user ID ptext unused2; le_uint32_t sub_version; - ptext serial_number2; - ptext access_key2; - ptext password; + ptext serial_number2; // On XB, this is the XBL gamertag + ptext access_key2; // On XB, this is the XBL user ID + ptext password; // On XB, this is unused }; // Note: This login pathway generally isn't used on BB (and isn't supported at @@ -1991,13 +2103,13 @@ struct C_PlayerPreviewRequest_BB_E3 { // commands have different formats). // Header flag = seated state (1 = present, 0 = leaving) -struct C_CardLobbyGame_GC_E4 { +struct C_CardLobbyGame_GC_Ep3_E4 { le_uint16_t table_number; le_uint16_t seat_number; }; // Header flag = table number -struct S_CardLobbyGame_GC_E4 { +struct S_CardLobbyGame_GC_Ep3_E4 { struct Entry { le_uint16_t present; // 1 = player present, 0 = no player le_uint16_t unknown_a1; @@ -2048,6 +2160,9 @@ struct C_JoinSpectatorTeam_GC_Ep3_E6_Flag01 { // Same format as 08 command. // E6 (S->C): Set guild card number and update client config (BB) +// BB clients have multiple client configs. This command sets the client config +// that is returned by the 93 commands, but does not affect the client config +// set by the 04 command (and returned in the 9E and 9F commands). struct S_ClientInit_BB_00E6 { le_uint32_t error; @@ -2082,7 +2197,7 @@ struct S_Unknown_GC_Ep3_E8 { struct PlayerEntry { PlayerLobbyDataGC lobby_data; PlayerInventory inventory; - PlayerDispDataPCGC disp; + PlayerDispDataPCV3 disp; }; PlayerEntry players[4]; parray unknown_a2; @@ -2267,7 +2382,7 @@ struct S_Unknown_GC_Ep3_EB { struct PlayerEntry { PlayerLobbyDataGC lobby_data; PlayerInventory inventory; - PlayerDispDataPCGC disp; + PlayerDispDataPCV3 disp; }; PlayerEntry players[12]; }; @@ -2320,7 +2435,7 @@ union C_UpdateAccountData_BB_ED { // EE (S->C): Unknown (Episode 3) // This command has different forms depending on the header.flag value. Since // the relevant flag values match the Episodes 1 & 2 trade window commands, this -// may be used for trading cards. +// is likely used for trading cards. struct C_Unknown_GC_Ep3_EE_FlagD0 { parray unknown_a1; @@ -2438,7 +2553,7 @@ struct G_SwitchStateChanged_6x05 { // 06: Send guild card template -struct G_SendGuildCard_PC_GC { +struct G_SendGuildCard_PC_V3 { uint8_t subcommand; uint8_t size; le_uint16_t unused; @@ -2452,8 +2567,8 @@ struct G_SendGuildCard_PC_GC { uint8_t section_id; uint8_t char_class; }; -struct G_SendGuildCard_PC_6x06 : G_SendGuildCard_PC_GC { }; -struct G_SendGuildCard_GC_6x06 : G_SendGuildCard_PC_GC { }; +struct G_SendGuildCard_PC_6x06 : G_SendGuildCard_PC_V3 { }; +struct G_SendGuildCard_V3_6x06 : G_SendGuildCard_PC_V3 { }; struct G_SendGuildCard_BB_6x06 { uint8_t subcommand; diff --git a/src/Main.cc b/src/Main.cc index 9912d536..ec9e06c8 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -322,6 +322,8 @@ int main(int argc, char** argv) { cli_version = GameVersion::PC; } else if (!strcmp(argv[x], "--gc")) { cli_version = GameVersion::GC; + } else if (!strcmp(argv[x], "--xb")) { + cli_version = GameVersion::XB; } else if (!strcmp(argv[x], "--bb")) { cli_version = GameVersion::BB; } else if (!strncmp(argv[x], "--seed=", 7)) { @@ -350,10 +352,11 @@ int main(int argc, char** argv) { case GameVersion::PATCH: case GameVersion::DC: case GameVersion::PC: - crypt.reset(new PSOPCEncryption(stoul(seed, nullptr, 16))); + crypt.reset(new PSOV2Encryption(stoul(seed, nullptr, 16))); break; case GameVersion::GC: - crypt.reset(new PSOGCEncryption(stoul(seed, nullptr, 16))); + case GameVersion::XB: + crypt.reset(new PSOV3Encryption(stoul(seed, nullptr, 16))); break; case GameVersion::BB: { seed = parse_data_string(seed); diff --git a/src/Menu.hh b/src/Menu.hh index 032ccbec..18054072 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -58,14 +58,16 @@ struct MenuItem { INVISIBLE_ON_DC = 0x01, INVISIBLE_ON_PC = 0x02, INVISIBLE_ON_GC = 0x04, - INVISIBLE_ON_BB = 0x08, - DC_ONLY = INVISIBLE_ON_PC | INVISIBLE_ON_GC | INVISIBLE_ON_BB, - PC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_GC | INVISIBLE_ON_BB, - GC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_BB, - BB_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_GC, - REQUIRES_MESSAGE_BOXES = 0x10, - REQUIRES_SEND_FUNCTION_CALL = 0x20, - REQUIRES_SAVE_DISABLED = 0x40, + INVISIBLE_ON_XB = 0x08, + INVISIBLE_ON_BB = 0x10, + DC_ONLY = INVISIBLE_ON_PC | INVISIBLE_ON_GC | INVISIBLE_ON_XB | INVISIBLE_ON_BB, + PC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_GC | INVISIBLE_ON_XB | INVISIBLE_ON_BB, + GC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_XB | INVISIBLE_ON_BB, + XB_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_GC | INVISIBLE_ON_BB, + BB_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_GC | INVISIBLE_ON_XB, + REQUIRES_MESSAGE_BOXES = 0x20, + REQUIRES_SEND_FUNCTION_CALL = 0x40, + REQUIRES_SAVE_DISABLED = 0x80, }; uint32_t item_id; diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index a2652d00..ca168069 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -23,7 +23,7 @@ void PSOEncryption::decrypt(void* data, size_t size, bool advance) { -void PSOPCEncryption::update_stream() { +void PSOV2Encryption::update_stream() { uint32_t esi, edi, eax, ebp, edx; edi = 1; edx = 0x18; @@ -47,7 +47,7 @@ void PSOPCEncryption::update_stream() { } } -PSOPCEncryption::PSOPCEncryption(uint32_t seed) : offset(1) { +PSOV2Encryption::PSOV2Encryption(uint32_t seed) : offset(1) { uint32_t esi, ebx, edi, eax, edx, var1; esi = 1; ebx = seed; @@ -69,8 +69,8 @@ PSOPCEncryption::PSOPCEncryption(uint32_t seed) : offset(1) { } } -uint32_t PSOPCEncryption::next(bool advance) { - if (this->offset == PC_STREAM_LENGTH) { +uint32_t PSOV2Encryption::next(bool advance) { + if (this->offset == V2_STREAM_LENGTH) { this->update_stream(); this->offset = 1; } @@ -81,7 +81,7 @@ uint32_t PSOPCEncryption::next(bool advance) { return ret; } -void PSOPCEncryption::encrypt(void* vdata, size_t size, bool advance) { +void PSOV2Encryption::encrypt(void* vdata, size_t size, bool advance) { if (size & 3) { throw invalid_argument("size must be a multiple of 4"); } @@ -98,25 +98,25 @@ void PSOPCEncryption::encrypt(void* vdata, size_t size, bool advance) { -void PSOGCEncryption::update_stream() { +void PSOV3Encryption::update_stream() { uint32_t r5, r6, r7; r5 = 0; r6 = 489; r7 = 0; - while (r6 != GC_STREAM_LENGTH) { + while (r6 != V3_STREAM_LENGTH) { this->stream[r5++] ^= this->stream[r6++]; } - while (r5 != GC_STREAM_LENGTH) { + while (r5 != V3_STREAM_LENGTH) { this->stream[r5++] ^= this->stream[r7++]; } this->offset = 0; } -uint32_t PSOGCEncryption::next(bool advance) { - if (this->offset == GC_STREAM_LENGTH) { +uint32_t PSOV3Encryption::next(bool advance) { + if (this->offset == V3_STREAM_LENGTH) { this->update_stream(); } uint32_t ret = this->stream[this->offset]; @@ -126,7 +126,7 @@ uint32_t PSOGCEncryption::next(bool advance) { return ret; } -PSOGCEncryption::PSOGCEncryption(uint32_t seed) : offset(0) { +PSOV3Encryption::PSOV3Encryption(uint32_t seed) : offset(0) { uint32_t x, y, basekey, source1, source2, source3; basekey = 0; @@ -149,7 +149,7 @@ PSOGCEncryption::PSOGCEncryption(uint32_t seed) : offset(0) { source1 = 0; source2 = 1; source3 = this->offset - 1; - while (this->offset != GC_STREAM_LENGTH) { + while (this->offset != V3_STREAM_LENGTH) { this->stream[this->offset++] = (this->stream[source3++] ^ (((this->stream[source1++] << 23) & 0xFF800000) ^ ((this->stream[source2++] >> 9) & 0x007FFFFF))); } @@ -158,7 +158,7 @@ PSOGCEncryption::PSOGCEncryption(uint32_t seed) : offset(0) { } } -void PSOGCEncryption::encrypt(void* vdata, size_t size, bool advance) { +void PSOV3Encryption::encrypt(void* vdata, size_t size, bool advance) { if (size & 3) { throw invalid_argument("size must be a multiple of 4"); } diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index 3280249a..725ebb64 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -12,8 +12,8 @@ -#define PC_STREAM_LENGTH 56 -#define GC_STREAM_LENGTH 521 +#define V2_STREAM_LENGTH 56 +#define V3_STREAM_LENGTH 521 #define BB_STREAM_LENGTH 1042 class PSOEncryption { @@ -34,9 +34,9 @@ protected: PSOEncryption() = default; }; -class PSOPCEncryption : public PSOEncryption { +class PSOV2Encryption : public PSOEncryption { public: - explicit PSOPCEncryption(uint32_t seed); + explicit PSOV2Encryption(uint32_t seed); virtual void encrypt(void* data, size_t size, bool advance = true); @@ -45,13 +45,13 @@ public: protected: void update_stream(); - uint32_t stream[PC_STREAM_LENGTH + 1]; + uint32_t stream[V2_STREAM_LENGTH + 1]; uint8_t offset; }; -class PSOGCEncryption : public PSOEncryption { +class PSOV3Encryption : public PSOEncryption { public: - explicit PSOGCEncryption(uint32_t key); + explicit PSOV3Encryption(uint32_t key); virtual void encrypt(void* data, size_t size, bool advance = true); @@ -60,7 +60,7 @@ public: protected: void update_stream(); - uint32_t stream[GC_STREAM_LENGTH]; + uint32_t stream[V3_STREAM_LENGTH]; uint16_t offset; }; diff --git a/src/PSOProtocol.cc b/src/PSOProtocol.cc index 890428be..d1660f0b 100644 --- a/src/PSOProtocol.cc +++ b/src/PSOProtocol.cc @@ -27,6 +27,8 @@ uint16_t PSOCommandHeader::command(GameVersion version) const { return this->dc.command; case GameVersion::GC: return this->gc.command; + case GameVersion::XB: + return this->xb.command; case GameVersion::PC: case GameVersion::PATCH: return this->pc.command; @@ -45,6 +47,9 @@ void PSOCommandHeader::set_command(GameVersion version, uint16_t command) { case GameVersion::GC: this->gc.command = command; break; + case GameVersion::XB: + this->xb.command = command; + break; case GameVersion::PC: case GameVersion::PATCH: this->pc.command = command; @@ -63,6 +68,8 @@ uint16_t PSOCommandHeader::size(GameVersion version) const { return this->dc.size; case GameVersion::GC: return this->gc.size; + case GameVersion::XB: + return this->xb.size; case GameVersion::PC: case GameVersion::PATCH: return this->pc.size; @@ -81,6 +88,9 @@ void PSOCommandHeader::set_size(GameVersion version, uint32_t size) { case GameVersion::GC: this->gc.size = size; break; + case GameVersion::XB: + this->xb.size = size; + break; case GameVersion::PC: case GameVersion::PATCH: this->pc.size = size; @@ -99,6 +109,8 @@ uint32_t PSOCommandHeader::flag(GameVersion version) const { return this->dc.flag; case GameVersion::GC: return this->gc.flag; + case GameVersion::XB: + return this->xb.flag; case GameVersion::PC: case GameVersion::PATCH: return this->pc.flag; @@ -117,6 +129,9 @@ void PSOCommandHeader::set_flag(GameVersion version, uint32_t flag) { case GameVersion::GC: this->gc.flag = flag; break; + case GameVersion::XB: + this->xb.flag = flag; + break; case GameVersion::PC: case GameVersion::PATCH: this->pc.flag = flag; @@ -157,9 +172,10 @@ std::string prepend_command_header( const std::string& data) { StringWriter ret; switch (version) { + case GameVersion::DC: case GameVersion::GC: - case GameVersion::DC: { - PSOCommandHeaderDCGC header; + case GameVersion::XB: { + PSOCommandHeaderDCV3 header; if (encryption_enabled) { header.size = (sizeof(header) + data.size() + 3) & ~3; } else { diff --git a/src/PSOProtocol.hh b/src/PSOProtocol.hh index ed7bbf7e..7be8064b 100644 --- a/src/PSOProtocol.hh +++ b/src/PSOProtocol.hh @@ -15,7 +15,7 @@ struct PSOCommandHeaderPC { uint8_t flag; } __attribute__((packed)); -struct PSOCommandHeaderDCGC { +struct PSOCommandHeaderDCV3 { uint8_t command; uint8_t flag; le_uint16_t size; @@ -28,9 +28,10 @@ struct PSOCommandHeaderBB { } __attribute__((packed)); union PSOCommandHeader { - PSOCommandHeaderDCGC dc; + PSOCommandHeaderDCV3 dc; PSOCommandHeaderPC pc; - PSOCommandHeaderDCGC gc; + PSOCommandHeaderDCV3 gc; + PSOCommandHeaderDCV3 xb; PSOCommandHeaderBB bb; uint16_t command(GameVersion version) const; diff --git a/src/Player.cc b/src/Player.cc index 9d4bce48..55e6df22 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -34,7 +34,7 @@ static FileContentsCache player_files_cache(300 * 1000 * 1000); PlayerStats::PlayerStats() noexcept : atp(0), mst(0), evp(0), hp(0), dfp(0), ata(0), lck(0) { } -PlayerDispDataPCGC::PlayerDispDataPCGC() noexcept +PlayerDispDataPCV3::PlayerDispDataPCV3() noexcept : level(0), experience(0), meseta(0), @@ -58,7 +58,7 @@ PlayerDispDataPCGC::PlayerDispDataPCGC() noexcept proportion_x(0), proportion_y(0) { } -void PlayerDispDataPCGC::enforce_pc_limits() { +void PlayerDispDataPCV3::enforce_pc_limits() { // PC has fewer classes, so we'll substitute some here if (this->char_class == 11) { this->char_class = 0; // FOmar -> HUmar @@ -77,7 +77,7 @@ void PlayerDispDataPCGC::enforce_pc_limits() { this->version = 2; } -PlayerDispDataBB PlayerDispDataPCGC::to_bb() const { +PlayerDispDataBB PlayerDispDataPCV3::to_bb() const { PlayerDispDataBB bb; bb.stats.atp = this->stats.atp; bb.stats.mst = this->stats.mst; @@ -143,43 +143,43 @@ PlayerDispDataBB::PlayerDispDataBB() noexcept proportion_x(0), proportion_y(0) { } -PlayerDispDataPCGC PlayerDispDataBB::to_pcgc() const { - PlayerDispDataPCGC pcgc; - pcgc.stats.atp = this->stats.atp; - pcgc.stats.mst = this->stats.mst; - pcgc.stats.evp = this->stats.evp; - pcgc.stats.hp = this->stats.hp; - pcgc.stats.dfp = this->stats.dfp; - pcgc.stats.ata = this->stats.ata; - pcgc.stats.lck = this->stats.lck; - pcgc.unknown_a1 = this->unknown_a1; - pcgc.level = this->level; - pcgc.experience = this->experience; - pcgc.meseta = this->meseta; - pcgc.unknown_a2 = this->unknown_a2; - pcgc.name_color = this->name_color; - pcgc.extra_model = this->extra_model; - pcgc.unused = this->unused; - pcgc.name_color_checksum = this->name_color_checksum; - pcgc.section_id = this->section_id; - pcgc.char_class = this->char_class; - pcgc.v2_flags = this->v2_flags; - pcgc.version = this->version; - pcgc.v1_flags = this->v1_flags; - pcgc.costume = this->costume; - pcgc.skin = this->skin; - pcgc.face = this->face; - pcgc.head = this->head; - pcgc.hair = this->hair; - pcgc.hair_r = this->hair_r; - pcgc.hair_g = this->hair_g; - pcgc.hair_b = this->hair_b; - pcgc.proportion_x = this->proportion_x; - pcgc.proportion_y = this->proportion_y; - pcgc.name = remove_language_marker(this->name); - pcgc.config = this->config; - pcgc.technique_levels = this->technique_levels; - return pcgc; +PlayerDispDataPCV3 PlayerDispDataBB::to_pcv3() const { + PlayerDispDataPCV3 ret; + ret.stats.atp = this->stats.atp; + ret.stats.mst = this->stats.mst; + ret.stats.evp = this->stats.evp; + ret.stats.hp = this->stats.hp; + ret.stats.dfp = this->stats.dfp; + ret.stats.ata = this->stats.ata; + ret.stats.lck = this->stats.lck; + ret.unknown_a1 = this->unknown_a1; + ret.level = this->level; + ret.experience = this->experience; + ret.meseta = this->meseta; + ret.unknown_a2 = this->unknown_a2; + ret.name_color = this->name_color; + ret.extra_model = this->extra_model; + ret.unused = this->unused; + ret.name_color_checksum = this->name_color_checksum; + ret.section_id = this->section_id; + ret.char_class = this->char_class; + ret.v2_flags = this->v2_flags; + ret.version = this->version; + ret.v1_flags = this->v1_flags; + ret.costume = this->costume; + ret.skin = this->skin; + ret.face = this->face; + ret.head = this->head; + ret.hair = this->hair; + ret.hair_r = this->hair_r; + ret.hair_g = this->hair_g; + ret.hair_b = this->hair_b; + ret.proportion_x = this->proportion_x; + ret.proportion_y = this->proportion_y; + ret.name = remove_language_marker(this->name); + ret.config = this->config; + ret.technique_levels = this->technique_levels; + return ret; } PlayerDispDataBBPreview PlayerDispDataBB::to_preview() const { @@ -267,11 +267,20 @@ PlayerDispDataBBPreview::PlayerDispDataBBPreview() noexcept -GuildCardGC::GuildCardGC() noexcept - : player_tag(0), serial_number(0), reserved1(1), reserved2(1), section_id(0), char_class(0) { } +GuildCardV3::GuildCardV3() noexcept + : player_tag(0), + serial_number(0), + reserved1(1), + reserved2(1), + section_id(0), + char_class(0) { } GuildCardBB::GuildCardBB() noexcept - : serial_number(0), reserved1(1), reserved2(1), section_id(0), char_class(0) { } + : serial_number(0), + reserved1(1), + reserved2(1), + section_id(0), + char_class(0) { } @@ -455,7 +464,7 @@ void ClientGameData::import_player(const PSOPlayerDataPC& pc) { // auto_reply = pc.auto_reply; } -void ClientGameData::import_player(const PSOPlayerDataGC& gc) { +void ClientGameData::import_player(const PSOPlayerDataV3& gc) { auto account = this->account(); auto player = this->player(); player->inventory = gc.inventory; @@ -518,15 +527,77 @@ PlayerBB ClientGameData::export_player_bb() { +XBNetworkLocation::XBNetworkLocation() noexcept + : internal_ipv4_address(0x0A0A0A0A), + external_ipv4_address(0x23232323), + port(9100), + account_id(0xFFFFFFFFFFFFFFFF) { + this->unknown_a1[0] = 0xCCCCCCCC; + this->unknown_a1[1] = 0xDDDDDDDD; + this->mac_address.clear(0x77); +} + +// There's a strange behavior (bug? "feature"?) in Episode 3 where the start +// button does nothing in the lobby (hence you can't "quit game") if the +// client's IP address is zero. So, we fill it in with a fake nonzero value to +// avoid this behavior, and to be consistent, we make IP addresses fake and +// nonzero on all other versions too. + PlayerLobbyDataPC::PlayerLobbyDataPC() noexcept - : player_tag(0), guild_card(0), ip_address(0), client_id(0) { } + : player_tag(0), guild_card(0), ip_address(0x7F000001), client_id(0) { } PlayerLobbyDataGC::PlayerLobbyDataGC() noexcept - : player_tag(0), guild_card(0), ip_address(0), client_id(0) { } + : player_tag(0), guild_card(0), ip_address(0x7F000001), client_id(0) { } + +PlayerLobbyDataXB::PlayerLobbyDataXB() noexcept + : player_tag(0), guild_card(0), client_id(0) { } PlayerLobbyDataBB::PlayerLobbyDataBB() noexcept - : player_tag(0), guild_card(0), ip_address(0), client_id(0), unknown2(0) { } + : player_tag(0), guild_card(0), ip_address(0x7F000001), client_id(0), unknown_a2(0) { } +void PlayerLobbyDataPC::clear() { + this->player_tag = 0; + this->guild_card = 0; + this->ip_address = 0; + this->client_id = 0; + ptext name; +} + +void PlayerLobbyDataGC::clear() { + this->player_tag = 0; + this->guild_card = 0; + this->ip_address = 0; + this->client_id = 0; + ptext name; +} + +void XBNetworkLocation::clear() { + this->internal_ipv4_address = 0; + this->external_ipv4_address = 0; + this->port = 0; + this->mac_address.clear(0); + this->unknown_a1.clear(0); + this->account_id = 0; + this->unknown_a2.clear(0); +} + +void PlayerLobbyDataXB::clear() { + this->player_tag = 0; + this->guild_card = 0; + this->netloc.clear(); + this->client_id = 0; + this->name.clear(0); +} + +void PlayerLobbyDataBB::clear() { + this->player_tag = 0; + this->guild_card = 0; + this->ip_address = 0; + this->unknown_a1.clear(0); + this->client_id = 0; + this->name.clear(0); + this->unknown_a2 = 0; +} //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Player.hh b/src/Player.hh index a9af7a06..eb878784 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -93,8 +93,8 @@ struct PendingItemTrade { struct PlayerDispDataBB; -// PC/GC player appearance and stats data -struct PlayerDispDataPCGC { // 0xD0 bytes +// PC/V3 player appearance and stats data +struct PlayerDispDataPCV3 { // 0xD0 bytes PlayerStats stats; parray unknown_a1; le_uint32_t level; @@ -128,7 +128,7 @@ struct PlayerDispDataPCGC { // 0xD0 bytes // that has a fixed-size array. If we didn't define this constructor, the // trivial fields in that array's members would be uninitialized, and we could // send uninitialized memory to the client. - PlayerDispDataPCGC() noexcept; + PlayerDispDataPCV3() noexcept; void enforce_pc_limits(); PlayerDispDataBB to_bb() const; @@ -200,14 +200,16 @@ struct PlayerDispDataBB { PlayerDispDataBB() noexcept; inline void enforce_pc_limits() { } - PlayerDispDataPCGC to_pcgc() const; + PlayerDispDataPCV3 to_pcv3() const; PlayerDispDataBBPreview to_preview() const; void apply_preview(const PlayerDispDataBBPreview&); } __attribute__((packed)); -struct GuildCardGC { +// TODO: Is this the same for XB as it is for GC? (This struct is based on the +// GC format) +struct GuildCardV3 { le_uint32_t player_tag; le_uint32_t serial_number; ptext name; @@ -217,7 +219,7 @@ struct GuildCardGC { uint8_t section_id; uint8_t char_class; - GuildCardGC() noexcept; + GuildCardV3() noexcept; } __attribute__((packed)); // BB guild card format @@ -271,6 +273,7 @@ struct PlayerLobbyDataPC { ptext name; PlayerLobbyDataPC() noexcept; + void clear(); } __attribute__((packed)); struct PlayerLobbyDataGC { @@ -281,6 +284,31 @@ struct PlayerLobbyDataGC { ptext name; PlayerLobbyDataGC() noexcept; + void clear(); +} __attribute__((packed)); + +struct XBNetworkLocation { + le_uint32_t internal_ipv4_address; + le_uint32_t external_ipv4_address; + le_uint16_t port; + parray mac_address; + parray unknown_a1; + le_uint64_t account_id; + parray unknown_a2; + + XBNetworkLocation() noexcept; + void clear(); +} __attribute__((packed)); + +struct PlayerLobbyDataXB { + le_uint32_t player_tag; + le_uint32_t guild_card; + XBNetworkLocation netloc; + le_uint32_t client_id; + ptext name; + + PlayerLobbyDataXB() noexcept; + void clear(); } __attribute__((packed)); struct PlayerLobbyDataBB { @@ -290,14 +318,15 @@ struct PlayerLobbyDataBB { parray unknown_a1; le_uint32_t client_id; ptext name; - le_uint32_t unknown2; + le_uint32_t unknown_a2; PlayerLobbyDataBB() noexcept; + void clear(); } __attribute__((packed)); -struct PlayerChallengeDataGC { +struct PlayerChallengeDataV3 { le_uint32_t client_id; struct { le_uint16_t unknown_a1; @@ -309,21 +338,21 @@ struct PlayerChallengeDataGC { parray unknown_a3; parray unknown_a4; parray unknown_a5; - } __attribute__((packed)) unknown_a4; + } __attribute__((packed)) unknown_a4; // 0x50 bytes struct { parray unknown_a1; parray unknown_a2; - } __attribute__((packed)) unknown_a5; + } __attribute__((packed)) unknown_a5; // 0x10 bytes struct UnknownPair { le_uint32_t unknown_a1; le_uint32_t unknown_a2; } __attribute__((packed)); - parray unknown_a6; + parray unknown_a6; // 0x18 bytes parray unknown_a7; - } __attribute__((packed)) unknown_a1; + } __attribute__((packed)) unknown_a1; // 0x100 bytes parray unknown_a2; parray unknown_a3; -} __attribute__((packed)); +} __attribute__((packed)); // 0x11C bytes struct PlayerChallengeDataBB { le_uint32_t client_id; @@ -334,24 +363,27 @@ struct PlayerChallengeDataBB { struct PSOPlayerDataPC { // For command 61 PlayerInventory inventory; - PlayerDispDataPCGC disp; + PlayerDispDataPCV3 disp; } __attribute__((packed)); -struct PSOPlayerDataGC { // For command 61 +struct PSOPlayerDataV3 { // For command 61 PlayerInventory inventory; - PlayerDispDataPCGC disp; - PlayerChallengeDataGC challenge_data; + PlayerDispDataPCV3 disp; + PlayerChallengeDataV3 challenge_data; parray unknown; ptext info_board; parray blocked_senders; le_uint32_t auto_reply_enabled; + // The auto-reply message can be up to 0x200 bytes. If it's shorter than that, + // the client truncates the command after the first zero byte (rounded up to + // the next 4-byte boundary). char auto_reply[0]; } __attribute__((packed)); struct PSOPlayerDataGCEp3 { // For command 61 PlayerInventory inventory; - PlayerDispDataPCGC disp; - PlayerChallengeDataGC challenge_data; + PlayerDispDataPCV3 disp; + PlayerChallengeDataV3 challenge_data; parray unknown; ptext info_board; parray blocked_senders; @@ -480,7 +512,7 @@ public: void save_player_data() const; void import_player(const PSOPlayerDataPC& pd); - void import_player(const PSOPlayerDataGC& pd); + void import_player(const PSOPlayerDataV3& pd); void import_player(const PSOPlayerDataBB& pd); // Note: this function is not const because it can cause player and account // data to be loaded @@ -500,20 +532,20 @@ DestT convert_player_disp_data(const SrcT&) { } template <> -inline PlayerDispDataPCGC convert_player_disp_data( - const PlayerDispDataPCGC& src) { +inline PlayerDispDataPCV3 convert_player_disp_data( + const PlayerDispDataPCV3& src) { return src; } template <> -inline PlayerDispDataPCGC convert_player_disp_data( +inline PlayerDispDataPCV3 convert_player_disp_data( const PlayerDispDataBB& src) { - return src.to_pcgc(); + return src.to_pcv3(); } template <> -inline PlayerDispDataBB convert_player_disp_data( - const PlayerDispDataPCGC& src) { +inline PlayerDispDataBB convert_player_disp_data( + const PlayerDispDataPCV3& src) { return src.to_bb(); } diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index e03b8328..a7f0abb6 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -68,7 +68,8 @@ static void send_text_message_to_client( const std::string& message) { StringWriter w; w.put({0, 0}); - if (session.version == GameVersion::PC) { + if ((session.version == GameVersion::PC) || + (session.version == GameVersion::BB)) { auto decoded = decode_sjis(message); w.write(decoded.data(), decoded.size() * sizeof(decoded[0])); w.put_u16l(0); @@ -158,7 +159,7 @@ static HandlerResult process_server_gc_9A(shared_ptr, return HandlerResult::Type::SUPPRESS; } -static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr s, +static HandlerResult process_server_pc_v3_patch_02_17(shared_ptr s, ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) { if (session.version == GameVersion::PATCH && command == 0x17) { throw invalid_argument("patch server sent 17 server init"); @@ -166,8 +167,8 @@ static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr s, // Most servers don't include after_message or have a shorter // after_message than newserv does, so don't require it - const auto& cmd = check_size_t(data, - offsetof(S_ServerInit_DC_PC_GC_02_17_91_9B, after_message), 0xFFFF); + const auto& cmd = check_size_t(data, + offsetof(S_ServerInit_DC_PC_V3_02_17_91_9B, after_message), 0xFFFF); if (!session.license) { session.log.info("No license in linked session"); @@ -176,16 +177,17 @@ static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr s, // client will be able to understand it. forward_command(session, false, command, flag, data); - if (session.version == GameVersion::GC) { - session.server_channel.crypt_in.reset(new PSOGCEncryption(cmd.server_key)); - session.server_channel.crypt_out.reset(new PSOGCEncryption(cmd.client_key)); - session.client_channel.crypt_in.reset(new PSOGCEncryption(cmd.client_key)); - session.client_channel.crypt_out.reset(new PSOGCEncryption(cmd.server_key)); + if ((session.version == GameVersion::GC) || + (session.version == GameVersion::XB)) { + session.server_channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key)); + session.server_channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key)); + session.client_channel.crypt_in.reset(new PSOV3Encryption(cmd.client_key)); + session.client_channel.crypt_out.reset(new PSOV3Encryption(cmd.server_key)); } else { // PC or patch server (they both use PC encryption) - session.server_channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key)); - session.server_channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key)); - session.client_channel.crypt_in.reset(new PSOPCEncryption(cmd.client_key)); - session.client_channel.crypt_out.reset(new PSOPCEncryption(cmd.server_key)); + session.server_channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key)); + session.server_channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key)); + session.client_channel.crypt_in.reset(new PSOV2Encryption(cmd.client_key)); + session.client_channel.crypt_out.reset(new PSOV2Encryption(cmd.server_key)); } return HandlerResult::Type::SUPPRESS; @@ -194,12 +196,14 @@ static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr s, session.log.info("Existing license in linked session"); // This isn't forwarded to the client, so don't recreate the client's crypts - if ((session.version == GameVersion::PATCH) || (session.version == GameVersion::PC)) { - session.server_channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key)); - session.server_channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key)); - } else if (session.version == GameVersion::GC) { - session.server_channel.crypt_in.reset(new PSOGCEncryption(cmd.server_key)); - session.server_channel.crypt_out.reset(new PSOGCEncryption(cmd.client_key)); + if ((session.version == GameVersion::PATCH) || + (session.version == GameVersion::PC)) { + session.server_channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key)); + session.server_channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key)); + } else if ((session.version == GameVersion::GC) || + (session.version == GameVersion::XB)) { + session.server_channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key)); + session.server_channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key)); } else { throw invalid_argument("unsupported version"); } @@ -236,7 +240,7 @@ static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr s, } else if (session.version == GameVersion::GC) { if (command == 0x17) { - C_VerifyLicense_GC_DB cmd; + C_VerifyLicense_V3_DB cmd; cmd.serial_number = string_printf("%08" PRIX32 "", session.license->serial_number); cmd.access_key = session.license->access_key; @@ -252,6 +256,9 @@ static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr s, return process_server_gc_9A(s, session, command, flag, data); } + } else if (session.version == GameVersion::XB) { + throw runtime_error("xbox licenses are not implemented"); + } else { throw logic_error("invalid game version in server init handler"); } @@ -314,13 +321,13 @@ static HandlerResult process_server_bb_03(shared_ptr s, } } -static HandlerResult process_server_dc_pc_gc_04(shared_ptr, +static HandlerResult process_server_dc_pc_v3_04(shared_ptr, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { // Some servers send a short 04 command if they don't use all of the 0x20 // bytes available. We should be prepared to handle that. - auto& cmd = check_size_t(data, - offsetof(S_UpdateClientConfig_DC_PC_GC_04, cfg), - sizeof(S_UpdateClientConfig_DC_PC_GC_04)); + auto& cmd = check_size_t(data, + offsetof(S_UpdateClientConfig_DC_PC_V3_04, cfg), + sizeof(S_UpdateClientConfig_DC_PC_V3_04)); // If this is a licensed session, hide the guild card number assigned by the // remote server so the client doesn't see it change. If this is an unlicensed @@ -351,7 +358,7 @@ static HandlerResult process_server_dc_pc_gc_04(shared_ptr, : "t Port Map. Copyright SEGA Enter", session.remote_client_config_data.bytes()); memcpy(session.remote_client_config_data.data(), &cmd.cfg, - min(data.size() - sizeof(S_UpdateClientConfig_DC_PC_GC_04), + min(data.size() - sizeof(S_UpdateClientConfig_DC_PC_V3_04), session.remote_client_config_data.bytes())); // If the guild card number was not set, pretend (to the server) that this is @@ -369,7 +376,7 @@ static HandlerResult process_server_dc_pc_gc_04(shared_ptr, return session.license ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD; } -static HandlerResult process_server_dc_pc_gc_06(shared_ptr, +static HandlerResult process_server_dc_pc_v3_06(shared_ptr, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { if (session.license) { auto& cmd = check_size_t(data, @@ -538,7 +545,7 @@ static HandlerResult process_server_C4(shared_ptr, static HandlerResult process_server_gc_E4(shared_ptr, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { - auto& cmd = check_size_t(data); + auto& cmd = check_size_t(data); bool modified = false; for (size_t x = 0; x < 4; x++) { if (cmd.entries[x].guild_card_number == session.remote_guild_card_number) { @@ -640,12 +647,12 @@ static HandlerResult process_server_game_19_patch_14(shared_ptr, } } -static HandlerResult process_server_gc_1A_D5(shared_ptr, +static HandlerResult process_server_v3_1A_D5(shared_ptr, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) { // If the client is a version that sends close confirmations and the client // has the no-close-confirmation flag set in its newserv client config, send a // fake confirmation to the remote server immediately. - if ((session.version == GameVersion::GC) && + if (((session.version == GameVersion::GC) || (session.version == GameVersion::XB)) && (session.newserv_client_config.cfg.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION)) { session.server_channel.send(0xD6); } @@ -676,7 +683,7 @@ template static HandlerResult process_server_44_A6(shared_ptr, ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) { if (session.save_files) { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); bool is_download_quest = (command == 0xA6); string filename = cmd.filename; @@ -821,21 +828,26 @@ static HandlerResult process_server_64(shared_ptr, session.lobby_players.resize(4); session.log.info("Cleared lobby players"); - const size_t expected_size = session.sub_version >= 0x40 - ? sizeof(CmdT) - : offsetof(CmdT, players_ep3); - auto& cmd = check_size_t(data, expected_size, expected_size); + CmdT* cmd; + S_JoinGame_GC_Ep3_64* cmd_ep3 = nullptr; + if (session.sub_version >= 0x40) { + cmd = &check_size_t(data, sizeof(S_JoinGame_GC_Ep3_64), sizeof(S_JoinGame_GC_Ep3_64)); + cmd_ep3 = &check_size_t(data); + } else { + cmd = &check_size_t(data); + } + bool modified = false; - session.lobby_client_id = cmd.client_id; + session.lobby_client_id = cmd->client_id; for (size_t x = 0; x < flag; x++) { - if (cmd.lobby_data[x].guild_card == session.remote_guild_card_number) { - cmd.lobby_data[x].guild_card = session.license->serial_number; + if (cmd->lobby_data[x].guild_card == session.remote_guild_card_number) { + cmd->lobby_data[x].guild_card = session.license->serial_number; modified = true; } - session.lobby_players[x].guild_card_number = cmd.lobby_data[x].guild_card; - if (data.size() == sizeof(CmdT)) { - ptext name = cmd.players_ep3[x].disp.name; + session.lobby_players[x].guild_card_number = cmd->lobby_data[x].guild_card; + if (cmd_ep3) { + ptext name = cmd_ep3->players_ep3[x].disp.name; session.lobby_players[x].name = name; } else { session.lobby_players[x].name.clear(); @@ -847,15 +859,15 @@ static HandlerResult process_server_64(shared_ptr, } if (session.override_section_id >= 0) { - cmd.section_id = session.override_section_id; + cmd->section_id = session.override_section_id; modified = true; } if (session.override_lobby_event >= 0) { - cmd.event = session.override_lobby_event; + cmd->event = session.override_lobby_event; modified = true; } if (session.override_random_seed >= 0) { - cmd.rare_seed = session.override_random_seed; + cmd->rare_seed = session.override_random_seed; modified = true; } @@ -885,7 +897,7 @@ static HandlerResult process_client_06(shared_ptr s, text = u16string(cmd.text.pcbb, (data.size() - sizeof(C_Chat_06)) / sizeof(char16_t)); } else { const auto& cmd = check_size_t(data, sizeof(C_Chat_06), 0xFFFF); - text = decode_sjis(cmd.text.dcgc, data.size() - sizeof(C_Chat_06)); + text = decode_sjis(cmd.text.dcv3, data.size() - sizeof(C_Chat_06)); } strip_trailing_zeroes(text); @@ -939,7 +951,7 @@ static HandlerResult process_client_40(shared_ptr, template static HandlerResult process_client_81(shared_ptr, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { - auto& cmd = check_size_t(data); + auto& cmd = check_size_t(data); if (session.license) { if (cmd.from_guild_card_number == session.license->serial_number) { cmd.from_guild_card_number = session.remote_guild_card_number; @@ -1016,7 +1028,7 @@ HandlerResult process_client_60_62_6C_6D_C9_CB(shared_ptr, return HandlerResult::Type::FORWARD; } -static HandlerResult process_client_dc_pc_gc_A0_A1(shared_ptr s, +static HandlerResult process_client_dc_pc_v3_A0_A1(shared_ptr s, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) { if (!session.license) { return HandlerResult::Type::FORWARD; @@ -1041,14 +1053,14 @@ static HandlerResult process_client_dc_pc_gc_A0_A1(shared_ptr s, "You\'ve returned to\n\tC6%s", encoded_name.c_str())); // Restore newserv_client_config, so the login server gets the client flags - S_UpdateClientConfig_DC_PC_GC_04 update_client_config_cmd; + S_UpdateClientConfig_DC_PC_V3_04 update_client_config_cmd; update_client_config_cmd.player_tag = 0x00010000; update_client_config_cmd.guild_card_number = session.license->serial_number; update_client_config_cmd.cfg = session.newserv_client_config.cfg; session.client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd)); static const vector version_to_port_name({ - "dc-login", "pc-login", "bb-patch", "gc-us3", "bb-login"}); + "dc-login", "pc-login", "bb-patch", "gc-us3", "xb-login", "bb-login"}); const auto& port_name = version_to_port_name.at(static_cast( session.version)); @@ -1094,11 +1106,11 @@ typedef HandlerResult (*process_command_t)( auto defh = process_default; static process_command_t dc_server_handlers[0x100] = { - /* 00 */ defh, defh, defh, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 00 */ defh, defh, defh, defh, process_server_dc_pc_v3_04, defh, process_server_dc_pc_v3_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 40 */ defh, process_server_41, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, process_server_41, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 60 */ process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, defh, defh, process_server_66_69, defh, defh, process_server_66_69, defh, defh, process_server_60_62_6C_6D_C9_CB, process_server_60_62_6C_6D_C9_CB, defh, defh, /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -1112,17 +1124,17 @@ static process_command_t dc_server_handlers[0x100] = { /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, }; static process_command_t pc_server_handlers[0x100] = { - /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, + /* 00 */ defh, defh, process_server_pc_v3_patch_02_17, defh, process_server_dc_pc_v3_04, defh, process_server_dc_pc_v3_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_v3_patch_02_17, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 60 */ process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_64, process_server_65_67_68, process_server_66_69, process_server_65_67_68, process_server_65_67_68, process_server_66_69, defh, defh, process_server_60_62_6C_6D_C9_CB, process_server_60_62_6C_6D_C9_CB, defh, defh, /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, process_server_88, defh, defh, defh, defh, defh, defh, defh, /* 90 */ defh, defh, defh, defh, defh, defh, defh, process_server_97, defh, defh, defh, defh, defh, defh, defh, defh, - /* A0 */ defh, defh, defh, defh, defh, defh, process_server_44_A6, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, process_server_44_A6, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -1130,23 +1142,41 @@ static process_command_t pc_server_handlers[0x100] = { /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, }; static process_command_t gc_server_handlers[0x100] = { - /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_game_19_patch_14, process_server_gc_1A_D5, defh, defh, defh, defh, defh, + /* 00 */ defh, defh, process_server_pc_v3_patch_02_17, defh, process_server_dc_pc_v3_04, defh, process_server_dc_pc_v3_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_v3_patch_02_17, defh, process_server_game_19_patch_14, process_server_v3_1A_D5, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 60 */ process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_64, process_server_65_67_68, process_server_66_69, process_server_65_67_68, process_server_65_67_68, process_server_66_69, defh, defh, process_server_60_62_6C_6D_C9_CB, process_server_60_62_6C_6D_C9_CB, defh, defh, /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 80 */ defh, process_server_81, defh, defh, defh, defh, defh, defh, process_server_88, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, process_server_81, defh, defh, defh, defh, defh, defh, process_server_88, defh, defh, defh, defh, defh, defh, defh, /* 90 */ defh, defh, defh, defh, defh, defh, defh, process_server_97, defh, defh, process_server_gc_9A, defh, defh, defh, defh, defh, - /* A0 */ defh, defh, defh, defh, defh, defh, process_server_44_A6, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, process_server_44_A6, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, /* B0 */ defh, defh, process_server_B2, defh, defh, defh, defh, defh, process_server_gc_B8, defh, defh, defh, defh, defh, defh, defh, - /* C0 */ defh, defh, defh, defh, process_server_C4, defh, defh, defh, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, defh, defh, defh, - /* D0 */ defh, defh, defh, defh, defh, process_server_gc_1A_D5, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, process_server_C4, defh, defh, defh, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, process_server_v3_1A_D5, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* E0 */ defh, defh, defh, defh, process_server_gc_E4, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, }; +static process_command_t xb_server_handlers[0x100] = { + /* 00 */ defh, defh, process_server_pc_v3_patch_02_17, defh, process_server_dc_pc_v3_04, defh, process_server_dc_pc_v3_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_v3_patch_02_17, defh, process_server_game_19_patch_14, process_server_v3_1A_D5, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_64, process_server_65_67_68, process_server_66_69, process_server_65_67_68, process_server_65_67_68, process_server_66_69, defh, defh, process_server_60_62_6C_6D_C9_CB, process_server_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, process_server_81, defh, defh, defh, defh, defh, defh, process_server_88, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, process_server_97, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ defh, defh, defh, defh, defh, defh, process_server_44_A6, process_server_13_A7, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, process_server_B2, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, process_server_C4, defh, defh, defh, defh, process_server_60_62_6C_6D_C9_CB, defh, process_server_60_62_6C_6D_C9_CB, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, process_server_v3_1A_D5, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; static process_command_t bb_server_handlers[0x100] = { /* 00 */ defh, defh, defh, process_server_bb_03, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, @@ -1166,7 +1196,7 @@ static process_command_t bb_server_handlers[0x100] = { /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, }; static process_command_t patch_server_handlers[0x100] = { - /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 00 */ defh, defh, process_server_pc_v3_patch_02_17, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 10 */ defh, defh, defh, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -1197,7 +1227,7 @@ static process_command_t dc_client_handlers[0x100] = { /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* A0 */ process_client_dc_pc_gc_A0_A1, process_client_dc_pc_gc_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ process_client_dc_pc_v3_A0_A1, process_client_dc_pc_v3_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -1215,7 +1245,7 @@ static process_command_t pc_client_handlers[0x100] = { /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 80 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* A0 */ process_client_dc_pc_gc_A0_A1, process_client_dc_pc_gc_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ process_client_dc_pc_v3_A0_A1, process_client_dc_pc_v3_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -1229,11 +1259,29 @@ static process_command_t gc_client_handlers[0x100] = { /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 40 */ process_client_40, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 60 */ process_client_60_62_6C_6D_C9_CB, defh, process_client_60_62_6C_6D_C9_CB, defh, defh, defh, defh, defh, defh, defh, defh, defh, process_client_60_62_6C_6D_C9_CB, process_client_60_62_6C_6D_C9_CB, defh, defh, + /* 60 */ process_client_60_62_6C_6D_C9_CB, defh, process_client_60_62_6C_6D_C9_CB, defh, defh, defh, defh, defh, defh, defh, defh, defh, process_client_60_62_6C_6D_C9_CB, process_client_60_62_6C_6D_C9_CB, defh, defh, /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 80 */ defh, process_client_81, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, process_client_81, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* A0 */ process_client_dc_pc_gc_A0_A1, process_client_dc_pc_gc_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ process_client_dc_pc_v3_A0_A1, process_client_dc_pc_v3_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* E0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* F0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, +}; +static process_command_t xb_client_handlers[0x100] = { + /* 00 */ defh, defh, defh, defh, defh, defh, process_client_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 40 */ process_client_40, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 50 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 60 */ process_client_60_62_6C_6D_C9_CB, defh, process_client_60_62_6C_6D_C9_CB, defh, defh, defh, defh, defh, defh, defh, defh, defh, process_client_60_62_6C_6D_C9_CB, process_client_60_62_6C_6D_C9_CB, defh, defh, + /* 70 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 80 */ defh, process_client_81, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 90 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* A0 */ process_client_dc_pc_v3_A0_A1, process_client_dc_pc_v3_A0_A1, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* B0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* C0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* D0 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -1280,13 +1328,23 @@ static process_command_t patch_client_handlers[0x100] = { static process_command_t* server_handlers[] = { - dc_server_handlers, pc_server_handlers, patch_server_handlers, gc_server_handlers, bb_server_handlers}; + dc_server_handlers, + pc_server_handlers, + patch_server_handlers, + gc_server_handlers, + xb_server_handlers, + bb_server_handlers}; static process_command_t* client_handlers[] = { - dc_client_handlers, pc_client_handlers, patch_client_handlers, gc_client_handlers, bb_client_handlers}; + dc_client_handlers, + pc_client_handlers, + patch_client_handlers, + gc_client_handlers, + xb_client_handlers, + bb_client_handlers}; static process_command_t get_handler(GameVersion version, bool from_server, uint8_t command) { size_t version_index = static_cast(version); - if (version_index >= 5) { + if (version_index >= 6) { throw logic_error("invalid game version on proxy server"); } return (from_server ? server_handlers : client_handlers)[version_index][command]; diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index 6c6e53cc..bdb4d910 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -187,20 +187,21 @@ void ProxyServer::on_client_connect( case GameVersion::PATCH: throw logic_error("cannot create unlinked patch session"); case GameVersion::PC: - case GameVersion::GC: { + case GameVersion::GC: + case GameVersion::XB: { uint32_t server_key = random_object(); uint32_t client_key = random_object(); - auto cmd = prepare_server_init_contents_dc_pc_gc( + auto cmd = prepare_server_init_contents_dc_pc_v3( false, server_key, client_key); session->channel.send(0x02, 0x00, &cmd, sizeof(cmd)); // TODO: Is this actually needed? // bufferevent_flush(session->channel.bev.get(), EV_READ | EV_WRITE, BEV_FLUSH); if (version == GameVersion::PC) { - session->channel.crypt_out.reset(new PSOPCEncryption(server_key)); - session->channel.crypt_in.reset(new PSOPCEncryption(client_key)); + session->channel.crypt_out.reset(new PSOV2Encryption(server_key)); + session->channel.crypt_in.reset(new PSOV2Encryption(client_key)); } else { - session->channel.crypt_out.reset(new PSOGCEncryption(server_key)); - session->channel.crypt_in.reset(new PSOGCEncryption(client_key)); + session->channel.crypt_out.reset(new PSOV3Encryption(server_key)); + session->channel.crypt_in.reset(new PSOV3Encryption(client_key)); } break; } @@ -230,7 +231,10 @@ void ProxyServer::on_client_connect( ProxyServer::UnlinkedSession::UnlinkedSession( - ProxyServer* server, struct bufferevent* bev, uint16_t local_port, GameVersion version) + ProxyServer* server, + struct bufferevent* bev, + uint16_t local_port, + GameVersion version) : server(server), log(string_printf("[ProxyServer:UnlinkedSession:%p] ", bev), proxy_server_log.min_level), channel( @@ -288,6 +292,9 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 character_name = cmd.name; client_config.cfg = cmd.client_config.cfg; + } else if (session->version == GameVersion::XB) { + throw runtime_error("xbox licenses are not implemented"); + } else if (session->version == GameVersion::BB) { // We should only get a 93 while the session is unlinked; if we get // anything else, disconnect diff --git a/src/Quest.cc b/src/Quest.cc index 84741933..80511382 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -112,6 +112,8 @@ struct PSOQuestHeaderPC { ptext long_description; } __attribute__((packed)); +// TODO: Is the XB quest header format the same as on GC? If not, make a +// separate struct; if so, rename this struct to V3. struct PSOQuestHeaderGC { uint32_t start_offset; uint32_t unknown_offset1; @@ -248,6 +250,7 @@ Quest::Quest(const string& bin_filename) {"pc", GameVersion::PC}, {"gc", GameVersion::GC}, {"gc3", GameVersion::GC}, + {"xb", GameVersion::XB}, {"bb", GameVersion::BB}, }); this->version = name_to_version.at(tokens[1]); @@ -290,6 +293,7 @@ Quest::Quest(const string& bin_filename) break; } + case GameVersion::XB: case GameVersion::GC: { if (this->category == QuestCategory::EPISODE_3) { // these all appear to be the same size @@ -467,12 +471,11 @@ string Quest::decode_dlq(const string& filename) { data = read_all(f.get()); } - PSOPCEncryption encr(key); - - // The compressed data size does not need to be a multiple of 4, but the PC + // The compressed data size does not need to be a multiple of 4, but the V2 // encryption (which is used for all download quests, even in V3) requires the // data size to be a multiple of 4. We'll just temporarily stick a few bytes // on the end, then throw them away later if needed. + PSOV2Encryption encr(key); size_t original_size = data.size(); data.resize((data.size() + 3) & (~3)); encr.decrypt(data); @@ -583,15 +586,15 @@ pair Quest::decode_qst(const string& filename) { // the first 4 bytes in the file: // - BB: 58 00 44 00 // - PC: 3C ?? 44 00 - // - DC/GC: 44 ?? 3C 00 + // - DC/V3: 44 ?? 3C 00 uint32_t signature = freadx(f.get()); fseek(f.get(), 0, SEEK_SET); if (signature == 0x58004400) { return decode_qst_t(f.get()); } else if ((signature & 0xFF00FFFF) == 0x3C004400) { - return decode_qst_t(f.get()); + return decode_qst_t(f.get()); } else if ((signature & 0xFF00FFFF) == 0x44003C00) { - return decode_qst_t(f.get()); + return decode_qst_t(f.get()); } else { throw runtime_error("invalid qst file format"); } @@ -681,11 +684,11 @@ static string create_download_quest_file(const string& compressed_data, data += compressed_data; // Add temporary extra bytes if necessary so encryption won't fail - the data - // size must be a multiple of 4 for PSO PC encryption. + // size must be a multiple of 4 for PSO V2 encryption. size_t original_size = data.size(); data.resize((data.size() + 3) & (~3)); - PSOPCEncryption encr(encryption_seed); + PSOV3Encryption encr(encryption_seed); encr.encrypt(data.data() + sizeof(PSODownloadQuestHeader), data.size() - sizeof(PSODownloadQuestHeader)); data.resize(original_size); @@ -715,6 +718,7 @@ shared_ptr Quest::create_download_quest() const { } reinterpret_cast(data_ptr)->is_download = 0x01; break; + case GameVersion::XB: case GameVersion::GC: if (decompressed_bin.size() < sizeof(PSOQuestHeaderGC)) { throw runtime_error("bin file is too small for header"); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 0ba0659d..d6550806 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -149,9 +149,9 @@ void process_disconnect(shared_ptr s, shared_ptr c) { //////////////////////////////////////////////////////////////////////////////// -void process_verify_license_gc(shared_ptr s, shared_ptr c, +void process_verify_license_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // DB - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16); try { @@ -176,29 +176,37 @@ void process_verify_license_gc(shared_ptr s, shared_ptr c, send_command(c, 0x9A, 0x02); } -void process_login_a_dc_pc_gc(shared_ptr s, shared_ptr c, +void process_login_a_dc_pc_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 9A - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); c->flags |= flags_for_version(c->version, cmd.sub_version); uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16); try { shared_ptr l; - if (c->version == GameVersion::GC) { - l = s->license_manager->verify_gc(serial_number, cmd.access_key); - } else { - l = s->license_manager->verify_pc(serial_number, cmd.access_key); + switch (c->version) { + case GameVersion::PC: + l = s->license_manager->verify_pc(serial_number, cmd.access_key); + break; + case GameVersion::GC: + l = s->license_manager->verify_gc(serial_number, cmd.access_key); + break; + case GameVersion::XB: + throw runtime_error("xbox licenses are not implemented"); + break; + default: + throw logic_error("unsupported versioned command"); } c->set_license(l); } catch (const exception& e) { - // On GC, the client should have sent a different command containing the + // On V3, the client should have sent a different command containing the // password already, which should have created and added a temporary // license. So, if no license exists at this point, disconnect the client // even if unregistered clients are allowed. shared_ptr l; - if (c->version == GameVersion::GC) { + if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) { u16string message = u"Login failed: " + decode_sjis(e.what()); send_message_box(c, message.c_str()); c->should_disconnect = true; @@ -215,20 +223,28 @@ void process_login_a_dc_pc_gc(shared_ptr s, shared_ptr c, send_command(c, 0x9C, 0x01); } -void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, +void process_login_c_dc_pc_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 9C - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); c->flags |= flags_for_version(c->version, cmd.sub_version); uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16); try { shared_ptr l; - if (c->version == GameVersion::GC) { - l = s->license_manager->verify_gc(serial_number, cmd.access_key, - cmd.password); - } else { - l = s->license_manager->verify_pc(serial_number, cmd.access_key); + switch (c->version) { + case GameVersion::PC: + l = s->license_manager->verify_pc(serial_number, cmd.access_key); + break; + case GameVersion::GC: + l = s->license_manager->verify_gc(serial_number, cmd.access_key, + cmd.password); + break; + case GameVersion::XB: + throw runtime_error("xbox licenses are not implemented"); + break; + default: + throw logic_error("unsupported versioned command"); } c->set_license(l); @@ -240,12 +256,20 @@ void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, return; } else { shared_ptr l; - if (c->version == GameVersion::GC) { - l = LicenseManager::create_license_gc(serial_number, cmd.access_key, - cmd.password, true); - } else { - l = LicenseManager::create_license_pc(serial_number, cmd.access_key, - true); + switch (c->version) { + case GameVersion::PC: + l = LicenseManager::create_license_pc(serial_number, cmd.access_key, + true); + break; + case GameVersion::GC: + l = LicenseManager::create_license_gc(serial_number, cmd.access_key, + cmd.password, true); + break; + case GameVersion::XB: + throw runtime_error("xbox licenses are not implemented"); + break; + default: + throw logic_error("unsupported versioned command"); } s->license_manager->add(l); c->set_license(l); @@ -255,7 +279,7 @@ void process_login_c_dc_pc_gc(shared_ptr s, shared_ptr c, send_command(c, 0x9C, 0x01); } -void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, +void process_login_d_e_pc_v3(shared_ptr s, shared_ptr c, uint16_t command, uint32_t, const string& data) { // 9D 9E // The client sends extra unused data the first time it sends these commands, @@ -272,8 +296,8 @@ void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, base_cmd = &cmd; if (cmd.is_extended) { const auto& cmd = check_size_t(data); - if (cmd.menu_id == MenuID::LOBBY) { - c->preferred_lobby_id = cmd.preferred_lobby_id; + if (cmd.extension.menu_id == MenuID::LOBBY) { + c->preferred_lobby_id = cmd.extension.preferred_lobby_id; } } @@ -296,12 +320,18 @@ void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, uint32_t serial_number = stoul(base_cmd->serial_number, nullptr, 16); try { shared_ptr l; - if (c->version == GameVersion::GC) { - l = s->license_manager->verify_gc(serial_number, - base_cmd->access_key); - } else { - l = s->license_manager->verify_pc(serial_number, - base_cmd->access_key); + switch (c->version) { + case GameVersion::PC: + l = s->license_manager->verify_pc(serial_number, base_cmd->access_key); + break; + case GameVersion::GC: + l = s->license_manager->verify_gc(serial_number, base_cmd->access_key); + break; + case GameVersion::XB: + throw runtime_error("xbox licenses are not implemented"); + break; + default: + throw logic_error("unsupported versioned command"); } c->set_license(l); @@ -337,7 +367,7 @@ void process_login_bb(shared_ptr s, shared_ptr c, throw runtime_error("invalid size for 93 command"); } - c->flags |= flags_for_version(c->version, 0); + c->flags |= flags_for_version(c->version, -1); try { auto l = s->license_manager->verify_bb(cmd.username, cmd.password); @@ -411,7 +441,7 @@ void process_return_client_config(shared_ptr, shared_ptr c, void process_client_checksum(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // 96 - check_size_t(data); + check_size_t(data); send_server_time(c); } @@ -430,7 +460,7 @@ void process_server_time_request(shared_ptr s, shared_ptr c // responds after saving. if (c->should_send_to_lobby_server) { static const vector version_to_port_name({ - "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "bb-lobby"}); + "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "xb-lobby", "bb-lobby"}); const auto& port_name = version_to_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); @@ -756,7 +786,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, item_id = cmd.item_id; unknown_a1 = cmd.unknown_a1; } else { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); menu_id = cmd.menu_id; item_id = cmd.item_id; unknown_a1 = decode_sjis(cmd.unknown_a1); @@ -768,7 +798,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, item_id = cmd.item_id; password = cmd.password; } else { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); menu_id = cmd.menu_id; item_id = cmd.item_id; password = decode_sjis(cmd.password); @@ -781,7 +811,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, unknown_a1 = cmd.unknown_a1; password = cmd.password; } else { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); menu_id = cmd.menu_id; item_id = cmd.item_id; unknown_a1 = decode_sjis(cmd.unknown_a1); @@ -802,7 +832,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, send_update_client_config(c); } else { static const vector version_to_port_name({ - "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "bb-lobby"}); + "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "xb-lobby", "bb-lobby"}); const auto& port_name = version_to_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); @@ -899,7 +929,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, // license/char name/etc. for remote auth) static const vector version_to_port_name({ - "dc-proxy", "pc-proxy", "", "gc-proxy", "bb-proxy"}); + "dc-proxy", "pc-proxy", "", "gc-proxy", "xb-proxy", "bb-proxy"}); const auto& port_name = version_to_port_name.at(static_cast(c->version)); uint16_t local_port = s->name_to_port_config.at(port_name)->port; @@ -1038,10 +1068,11 @@ void process_menu_selection(shared_ptr s, shared_ptr c, continue; } - // TODO: It looks like blasting all the chunks to the client at once can - // cause GC clients to crash in rare cases. Find a way to slow this down - // (perhaps by only sending each new chunk when they acknowledge the - // previous chunk with a 44 [first chunk] or 13 [later chunks] command). + // TODO: It looks like blasting all the chunks to the client at once + // can cause GC clients to crash in rare cases. Find a way to slow + // this down (perhaps by only sending each new chunk when they + // acknowledge the previous chunk with a 44 [first chunk] or 13 [later + // chunks] command). send_quest_file(l->clients[x], bin_basename + ".bin", bin_basename, *bin_contents, QuestFileType::ONLINE); send_quest_file(l->clients[x], dat_basename + ".dat", dat_basename, @@ -1186,7 +1217,7 @@ void process_change_ship(shared_ptr s, shared_ptr c, send_message_box(c, u""); static const vector version_to_port_name({ - "dc-login", "pc-login", "bb-patch", "gc-us3", "bb-init"}); + "dc-login", "pc-login", "bb-patch", "gc-us3", "xb-login", "bb-init"}); const auto& port_name = version_to_port_name.at(static_cast(c->version)); send_reconnect(c, s->connect_address_for_client(c), @@ -1377,18 +1408,19 @@ void process_player_data(shared_ptr s, shared_ptr c, c->game_data.import_player(pd); break; } - case GameVersion::GC: { - const PSOPlayerDataGC* pd; + case GameVersion::GC: + case GameVersion::XB: { + const PSOPlayerDataV3* pd; if (flag == 4) { // Episode 3 if (!(c->flags & Client::Flag::EPISODE_3)) { throw runtime_error("non-Episode 3 client sent Episode 3 player data"); } const auto* pd3 = &check_size_t(data); c->game_data.ep3_config.reset(new Ep3Config(pd3->ep3_config)); - pd = reinterpret_cast(pd3); + pd = reinterpret_cast(pd3); } else { - pd = &check_size_t(data, sizeof(PSOPlayerDataGC), - sizeof(PSOPlayerDataGC) + c->game_data.player()->auto_reply.bytes()); + pd = &check_size_t(data, sizeof(PSOPlayerDataV3), + sizeof(PSOPlayerDataV3) + c->game_data.player()->auto_reply.bytes()); } c->game_data.import_player(*pd); break; @@ -1514,10 +1546,10 @@ void process_chat_pc_bb(shared_ptr s, shared_ptr c, process_chat_generic(s, c, text); } -void process_chat_dc_gc(shared_ptr s, shared_ptr c, +void process_chat_dc_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { const auto& cmd = check_size_t(data, sizeof(C_Chat_06), 0xFFFF); - u16string decoded_s = decode_sjis(cmd.text.dcgc, data.size() - sizeof(C_Chat_06)); + u16string decoded_s = decode_sjis(cmd.text.dcv3, data.size() - sizeof(C_Chat_06)); process_chat_generic(s, c, decoded_s); } @@ -1722,13 +1754,13 @@ void process_choice_search(shared_ptr, shared_ptr c, void process_simple_mail(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 81 - if (c->version != GameVersion::GC) { - // TODO: implement this for DC, PC, BB + if ((c->version != GameVersion::GC) && (c->version != GameVersion::XB)) { + // TODO: implement this for non-V3 send_text_message(c, u"$C6Simple Mail is not\nsupported yet on\nthis platform."); return; } - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); auto target = s->find_client(nullptr, cmd.to_guild_card_number); @@ -1788,7 +1820,7 @@ void process_disable_auto_reply(shared_ptr, shared_ptr c, void process_set_blocked_senders_list(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // C6 - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); c->game_data.account()->blocked_senders = cmd.blocked_senders; } @@ -1957,9 +1989,9 @@ void process_create_game_pc(shared_ptr s, shared_ptr c, cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, 0); } -void process_create_game_dc_gc(shared_ptr s, shared_ptr c, +void process_create_game_dc_v3(shared_ptr s, shared_ptr c, uint16_t command, uint32_t, const string& data) { // C1 EC (EC Ep3 only) - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); // only allow EC from Ep3 clients bool client_is_ep3 = c->flags & Client::Flag::EPISODE_3; @@ -1968,7 +2000,7 @@ void process_create_game_dc_gc(shared_ptr s, shared_ptr c, } uint8_t episode = cmd.episode; - if (c->version == GameVersion::DC) { + if ((c->version == GameVersion::DC) || (c->version == GameVersion::PC)) { episode = 1; } if (client_is_ep3) { @@ -2212,7 +2244,7 @@ typedef void (*process_command_t)(shared_ptr s, shared_ptr static process_command_t dc_handlers[0x100] = { // 00 nullptr, nullptr, nullptr, nullptr, - nullptr, process_ignored_command, process_chat_dc_gc, nullptr, + nullptr, process_ignored_command, process_chat_dc_v3, nullptr, process_game_list_request, process_menu_item_info_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, @@ -2273,7 +2305,7 @@ static process_command_t dc_handlers[0x100] = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // C0 - nullptr, process_create_game_dc_gc, nullptr, nullptr, + nullptr, process_create_game_dc_v3, nullptr, nullptr, nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t, process_disable_auto_reply, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, @@ -2341,8 +2373,8 @@ static process_command_t pc_handlers[0x100] = { // 90 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_client_checksum, nullptr, - process_player_data, process_ignored_command, process_login_a_dc_pc_gc, nullptr, - process_login_c_dc_pc_gc, process_login_d_e_pc_gc, process_login_d_e_pc_gc, nullptr, + process_player_data, process_ignored_command, process_login_a_dc_pc_v3, nullptr, + process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, nullptr, // A0 process_change_ship, process_change_block, process_quest_list_request, nullptr, @@ -2378,7 +2410,7 @@ static process_command_t pc_handlers[0x100] = { static process_command_t gc_handlers[0x100] = { // 00 nullptr, nullptr, nullptr, nullptr, - nullptr, process_ignored_command, process_chat_dc_gc, nullptr, + nullptr, process_ignored_command, process_chat_dc_v3, nullptr, process_game_list_request, process_menu_item_info_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, @@ -2426,7 +2458,7 @@ static process_command_t gc_handlers[0x100] = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_client_checksum, nullptr, process_player_data, process_ignored_command, nullptr, nullptr, - process_login_c_dc_pc_gc, process_login_d_e_pc_gc, process_login_d_e_pc_gc, process_return_client_config, + process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, process_return_client_config, // A0 process_change_ship, process_change_block, process_quest_list_request, nullptr, @@ -2441,7 +2473,7 @@ static process_command_t gc_handlers[0x100] = { nullptr, nullptr, nullptr, nullptr, // C0 - process_choice_search, process_create_game_dc_gc, nullptr, nullptr, + process_choice_search, process_create_game_dc_v3, nullptr, nullptr, nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t, process_disable_auto_reply, process_game_command, process_ep3_server_data_request, process_game_command, nullptr, nullptr, nullptr, nullptr, @@ -2449,14 +2481,100 @@ static process_command_t gc_handlers[0x100] = { // D0 process_trade_start, nullptr, process_trade_execute, nullptr, process_trade_error, nullptr, process_message_box_closed, process_gba_file_request, - process_info_board_request, process_write_info_board_t, nullptr, process_verify_license_gc, + process_info_board_request, process_write_info_board_t, nullptr, process_verify_license_v3, process_ep3_menu_challenge, nullptr, nullptr, nullptr, // E0 nullptr, nullptr, process_ep3_tournament_control, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, - process_create_game_dc_gc, nullptr, nullptr, nullptr, + process_create_game_dc_v3, nullptr, nullptr, nullptr, + + // F0 + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, +}; + +static process_command_t xb_handlers[0x100] = { + // 00 + nullptr, nullptr, nullptr, nullptr, + nullptr, process_ignored_command, process_chat_dc_v3, nullptr, + process_game_list_request, process_menu_item_info_request, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + + // 10 + process_menu_selection, nullptr, nullptr, process_ignored_command, + nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + nullptr, process_ignored_command, nullptr, nullptr, + + // 20 + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + + // 30 + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + + // 40 + process_card_search, nullptr, nullptr, nullptr, + process_ignored_command, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + + // 50 + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + + // 60 + process_game_command, process_player_data, process_game_command, nullptr, + nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + process_game_command, process_game_command, nullptr, process_client_ready, + + // 70 + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + + // 80 + nullptr, process_simple_mail, nullptr, nullptr, + process_change_lobby, nullptr, nullptr, nullptr, + nullptr, process_change_arrow_color, process_lobby_name_request, nullptr, + nullptr, nullptr, nullptr, nullptr, + + // 90 + nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, process_client_checksum, nullptr, + process_player_data, process_ignored_command, nullptr, nullptr, + process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, process_return_client_config, + + // A0 + process_change_ship, process_change_block, process_quest_list_request, nullptr, + nullptr, nullptr, process_ignored_command, process_ignored_command, + nullptr, process_ignored_command, process_update_quest_statistics, nullptr, + process_quest_barrier, nullptr, nullptr, nullptr, + + // B0 + nullptr, process_server_time_request, nullptr, process_function_call_result, + nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + + // C0 + process_choice_search, process_create_game_dc_v3, nullptr, nullptr, + nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t, + process_disable_auto_reply, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + + // D0 + process_trade_start, nullptr, process_trade_execute, nullptr, + process_trade_error, nullptr, process_message_box_closed, process_gba_file_request, + process_info_board_request, process_write_info_board_t, nullptr, process_verify_license_v3, + nullptr, nullptr, nullptr, nullptr, + + // E0 + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // F0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, @@ -2606,7 +2724,7 @@ static process_command_t patch_handlers[0x100] = { }; static process_command_t* handlers[6] = { - dc_handlers, pc_handlers, patch_handlers, gc_handlers, bb_handlers}; + dc_handlers, pc_handlers, patch_handlers, gc_handlers, xb_handlers, bb_handlers}; void process_command(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, const string& data) { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 7afc3482..918faede 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -117,8 +117,8 @@ static void process_subcommand_send_guild_card(shared_ptr, if (c->version == GameVersion::PC) { const auto* cmd = check_size_sc(data); c->game_data.player()->guild_card_desc = cmd->desc; - } else if (c->version == GameVersion::GC) { - const auto* cmd = check_size_sc(data); + } else if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) { + const auto* cmd = check_size_sc(data); c->game_data.player()->guild_card_desc = cmd->desc; } else if (c->version == GameVersion::BB) { const auto* cmd = check_size_sc(data); diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index 489a554f..b0615f24 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -62,22 +62,23 @@ void ReplaySession::apply_default_mask(shared_ptr ev) { break; } case GameVersion::PC: - case GameVersion::GC: { + case GameVersion::GC: + case GameVersion::XB: { uint8_t command; if (version == GameVersion::PC) { command = check_size_t( ev->data, sizeof(PSOCommandHeaderPC), 0xFFFF).command; - } else { - command = check_size_t( - ev->data, sizeof(PSOCommandHeaderDCGC), 0xFFFF).command; + } else { // V3 + command = check_size_t( + ev->data, sizeof(PSOCommandHeaderDCV3), 0xFFFF).command; } switch (command) { case 0x02: case 0x17: case 0x91: case 0x9B: { - auto& cmd_mask = check_size_t( - cmd_data, cmd_size, sizeof(S_ServerInit_DC_PC_GC_02_17_91_9B), 0xFFFF); + auto& cmd_mask = check_size_t( + cmd_data, cmd_size, sizeof(S_ServerInit_DC_PC_V3_02_17_91_9B), 0xFFFF); cmd_mask.server_key = 0; cmd_mask.client_key = 0; break; @@ -90,13 +91,12 @@ void ReplaySession::apply_default_mask(shared_ptr ev) { } case 0x64: { if (version == GameVersion::PC) { - auto& cmd_mask = check_size_t(cmd_data, cmd_size, - offsetof(S_JoinGame_GC_64, players_ep3)); + auto& cmd_mask = check_size_t(cmd_data, cmd_size); cmd_mask.variations.clear(0); cmd_mask.rare_seed = 0; - } else { // GC + } else { // V3 auto& cmd_mask = check_size_t(cmd_data, cmd_size, - offsetof(S_JoinGame_GC_64, players_ep3)); + sizeof(S_JoinGame_GC_64), sizeof(S_JoinGame_GC_Ep3_64)); cmd_mask.variations.clear(0); cmd_mask.rare_seed = 0; } @@ -128,9 +128,7 @@ void ReplaySession::apply_default_mask(shared_ptr ev) { break; } case 0x0064: { - auto& cmd_mask = check_size_t(cmd_data, cmd_size, - offsetof(S_JoinGame_BB_64, players_ep3), - offsetof(S_JoinGame_BB_64, players_ep3)); + auto& cmd_mask = check_size_t(cmd_data, cmd_size); cmd_mask.variations.clear(0); cmd_mask.rare_seed = 0; break; @@ -422,21 +420,22 @@ void ReplaySession::on_command_received( case GameVersion::PATCH: if (command == 0x02) { auto& cmd = check_size_t(data); - c->channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key)); - c->channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key)); + c->channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key)); + c->channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key)); } break; case GameVersion::PC: case GameVersion::GC: + case GameVersion::XB: if (command == 0x02 || command == 0x17 || command == 0x91 || command == 0x9B) { - auto& cmd = check_size_t(data, - offsetof(S_ServerInit_DC_PC_GC_02_17_91_9B, after_message), 0xFFFF); - if (c->version == GameVersion::GC) { - c->channel.crypt_in.reset(new PSOGCEncryption(cmd.server_key)); - c->channel.crypt_out.reset(new PSOGCEncryption(cmd.client_key)); - } else { - c->channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key)); - c->channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key)); + auto& cmd = check_size_t(data, + offsetof(S_ServerInit_DC_PC_V3_02_17_91_9B, after_message), 0xFFFF); + if (c->version == GameVersion::PC) { + c->channel.crypt_in.reset(new PSOV2Encryption(cmd.server_key)); + c->channel.crypt_out.reset(new PSOV2Encryption(cmd.client_key)); + } else { // V3 + c->channel.crypt_in.reset(new PSOV3Encryption(cmd.server_key)); + c->channel.crypt_out.reset(new PSOV3Encryption(cmd.client_key)); } } break; diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 4917b007..f6bb634b 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -61,9 +61,10 @@ void send_command_with_header_t(Channel& ch, const void* data, size_t size) { void send_command_with_header(Channel& ch, const void* data, size_t size) { switch (ch.version) { - case GameVersion::GC: case GameVersion::DC: - send_command_with_header_t(ch, data, size); + case GameVersion::GC: + case GameVersion::XB: + send_command_with_header_t(ch, data, size); break; case GameVersion::PC: case GameVersion::PATCH: @@ -79,15 +80,6 @@ void send_command_with_header(Channel& ch, const void* data, size_t size) { -// specific command sending functions follow. in general, they're written in -// such a way that you don't need to think about anything, even the client's -// version, before calling them. for this reason, some of them are quite -// complex. many are split into several functions, one for each version of PSO, -// named with suffixes _GC, _BB, and the like. in these cases, the function -// without the suffix simply calls the appropriate function for the client's -// version. thus, if you change something in one of the version-specific -// functions, you may have to change it in all of them. - //////////////////////////////////////////////////////////////////////////////// // CommandServerInit: this function sends the command that initializes encryption @@ -99,11 +91,11 @@ static const char* bb_game_server_copyright = "Phantasy Star Online Blue Burst G static const char* bb_pm_server_copyright = "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM."; static const char* patch_server_copyright = "Patch Server. Copyright SonicTeam, LTD. 2001"; -S_ServerInit_DC_PC_GC_02_17_91_9B prepare_server_init_contents_dc_pc_gc( +S_ServerInit_DC_PC_V3_02_17_91_9B prepare_server_init_contents_dc_pc_v3( bool initial_connection, uint32_t server_key, uint32_t client_key) { - S_ServerInit_DC_PC_GC_02_17_91_9B cmd; + S_ServerInit_DC_PC_V3_02_17_91_9B cmd; cmd.copyright = initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright; cmd.server_key = server_key; @@ -112,25 +104,26 @@ S_ServerInit_DC_PC_GC_02_17_91_9B prepare_server_init_contents_dc_pc_gc( return cmd; } -void send_server_init_dc_pc_gc(shared_ptr c, +void send_server_init_dc_pc_v3(shared_ptr c, bool initial_connection) { uint8_t command = initial_connection ? 0x17 : 0x02; uint32_t server_key = random_object(); uint32_t client_key = random_object(); - auto cmd = prepare_server_init_contents_dc_pc_gc( + auto cmd = prepare_server_init_contents_dc_pc_v3( initial_connection, server_key, client_key); send_command_t(c, command, 0x00, cmd); switch (c->version) { case GameVersion::DC: case GameVersion::PC: - c->channel.crypt_out.reset(new PSOPCEncryption(server_key)); - c->channel.crypt_in.reset(new PSOPCEncryption(client_key)); + c->channel.crypt_out.reset(new PSOV2Encryption(server_key)); + c->channel.crypt_in.reset(new PSOV2Encryption(client_key)); break; case GameVersion::GC: - c->channel.crypt_out.reset(new PSOGCEncryption(server_key)); - c->channel.crypt_in.reset(new PSOGCEncryption(client_key)); + case GameVersion::XB: + c->channel.crypt_out.reset(new PSOV3Encryption(server_key)); + c->channel.crypt_in.reset(new PSOV3Encryption(client_key)); break; default: throw invalid_argument("incorrect client version"); @@ -180,8 +173,8 @@ void send_server_init_patch(shared_ptr c) { cmd.client_key = client_key; send_command_t(c, 0x02, 0x00, cmd); - c->channel.crypt_out.reset(new PSOPCEncryption(server_key)); - c->channel.crypt_in.reset(new PSOPCEncryption(client_key)); + c->channel.crypt_out.reset(new PSOV2Encryption(server_key)); + c->channel.crypt_in.reset(new PSOV2Encryption(client_key)); } void send_server_init(shared_ptr s, shared_ptr c, @@ -190,7 +183,8 @@ void send_server_init(shared_ptr s, shared_ptr c, case GameVersion::DC: case GameVersion::PC: case GameVersion::GC: - send_server_init_dc_pc_gc(c, initial_connection); + case GameVersion::XB: + send_server_init_dc_pc_v3(c, initial_connection); break; case GameVersion::PATCH: send_server_init_patch(c); @@ -207,7 +201,7 @@ void send_server_init(shared_ptr s, shared_ptr c, // for non-BB clients, updates the client's guild card and security data void send_update_client_config(shared_ptr c) { - S_UpdateClientConfig_DC_PC_GC_04 cmd; + S_UpdateClientConfig_DC_PC_V3_04 cmd; cmd.player_tag = 0x00010000; cmd.guild_card_number = c->license->serial_number; cmd.cfg = c->export_config(); @@ -236,31 +230,33 @@ void send_function_call( string data; uint32_t index = 0; if (code.get()) { + // TODO: If we end up supporting B2 on non-GC platforms, we have to rework + // generate_client_command to be able to generate little-endian footers. data = code->generate_client_command(label_writes, suffix); index = code->index; - } - if (c->flags & Client::Flag::ENCRYPTED_SEND_FUNCTION_CALL) { - uint32_t key = random_object(); + if (c->flags & Client::Flag::ENCRYPTED_SEND_FUNCTION_CALL) { + uint32_t key = random_object(); - StringWriter w; - w.put_u32b(data.size()); - w.put_u32b(key); + StringWriter w; + w.put_u32b(data.size()); + w.put_u32b(key); - // Round size up to a multiple of 4 for encryption - data.resize((data.size() + 3) & ~3); + // Round size up to a multiple of 4 for encryption + data.resize((data.size() + 3) & ~3); - // For this format, the code section is decrypted without byteswapping on - // the client (GameCube), which is big-endian, so we have to treat the data - // the same way here (hence we can't just use crypt.encrypt). - data = prs_compress(data); - StringReader compressed_r(data); - PSOPCEncryption crypt(key); - while (!compressed_r.eof()) { - w.put_u32b(compressed_r.get_u32b() ^ crypt.next()); + // For this format, the code section is decrypted without byteswapping on + // the client (GameCube), which is big-endian, so we have to treat the data + // the same way here (hence we can't just use crypt.encrypt). + data = prs_compress(data); + StringReader compressed_r(data); + PSOV2Encryption crypt(key); + while (!compressed_r.eof()) { + w.put_u32b(compressed_r.get_u32b() ^ crypt.next()); + } + + data = move(w.str()); } - - data = move(w.str()); } S_ExecuteCode_B2 header = {data.size(), checksum_addr, checksum_size}; @@ -462,7 +458,9 @@ void send_enter_directory_patch(shared_ptr c, const string& dir) { void send_text(Channel& ch, StringWriter& w, uint16_t command, const u16string& text, bool should_add_color) { - if ((ch.version == GameVersion::DC) || (ch.version == GameVersion::GC)) { + if ((ch.version == GameVersion::DC) || + (ch.version == GameVersion::GC) || + (ch.version == GameVersion::XB)) { string data = encode_sjis(text); if (should_add_color) { add_color(w, data.c_str(), data.size()); @@ -558,9 +556,9 @@ void send_chat_message(shared_ptr c, uint32_t from_guild_card_number, send_header_text(c->channel, 0x06, from_guild_card_number, data, false); } -void send_simple_mail_gc(shared_ptr c, uint32_t from_guild_card_number, +void send_simple_mail_v3(shared_ptr c, uint32_t from_guild_card_number, const u16string& from_name, const u16string& text) { - SC_SimpleMail_GC_81 cmd; + SC_SimpleMail_V3_81 cmd; cmd.player_tag = 0x00010000; cmd.from_guild_card_number = from_guild_card_number; cmd.from_name = from_name; @@ -571,8 +569,8 @@ void send_simple_mail_gc(shared_ptr c, uint32_t from_guild_card_number, void send_simple_mail(shared_ptr c, uint32_t from_guild_card_number, const u16string& from_name, const u16string& text) { - if (c->version == GameVersion::GC) { - send_simple_mail_gc(c, from_guild_card_number, from_name, text); + if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) { + send_simple_mail_v3(c, from_guild_card_number, from_name, text); } else { throw logic_error("unimplemented versioned command"); } @@ -619,7 +617,7 @@ void send_card_search_result_t( shared_ptr result, shared_ptr result_lobby) { static const vector version_to_port_name({ - "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "bb-lobby"}); + "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "xb-lobby", "bb-lobby"}); const auto& port_name = version_to_port_name.at(static_cast(c->version)); S_GuildCardSearchResult cmd; @@ -659,8 +657,10 @@ void send_card_search_result( shared_ptr c, shared_ptr result, shared_ptr result_lobby) { - if ((c->version == GameVersion::DC) || (c->version == GameVersion::GC)) { - send_card_search_result_t( + if ((c->version == GameVersion::DC) || + (c->version == GameVersion::GC) || + (c->version == GameVersion::XB)) { + send_card_search_result_t( s, c, result, result_lobby); } else if (c->version == GameVersion::PC) { send_card_search_result_t( @@ -676,7 +676,7 @@ void send_card_search_result( template -void send_guild_card_pc_gc(shared_ptr c, shared_ptr source) { +void send_guild_card_pc_v3(shared_ptr c, shared_ptr source) { CmdT cmd; cmd.subcommand = 0x06; cmd.size = sizeof(CmdT) / 4; @@ -711,9 +711,10 @@ void send_guild_card_bb(shared_ptr c, shared_ptr source) { void send_guild_card(shared_ptr c, shared_ptr source) { if (c->version == GameVersion::PC) { - send_guild_card_pc_gc(c, source); - } else if (c->version == GameVersion::GC) { - send_guild_card_pc_gc(c, source); + send_guild_card_pc_v3(c, source); + } else if ((c->version == GameVersion::GC) || + (c->version == GameVersion::XB)) { + send_guild_card_pc_v3(c, source); } else if (c->version == GameVersion::BB) { send_guild_card_bb(c, source); } else { @@ -747,6 +748,7 @@ void send_menu_t( if (((c->version == GameVersion::DC) && (item.flags & MenuItem::Flag::INVISIBLE_ON_DC)) || ((c->version == GameVersion::PC) && (item.flags & MenuItem::Flag::INVISIBLE_ON_PC)) || ((c->version == GameVersion::GC) && (item.flags & MenuItem::Flag::INVISIBLE_ON_GC)) || + ((c->version == GameVersion::XB) && (item.flags & MenuItem::Flag::INVISIBLE_ON_XB)) || ((c->version == GameVersion::BB) && (item.flags & MenuItem::Flag::INVISIBLE_ON_BB)) || ((item.flags & MenuItem::Flag::REQUIRES_MESSAGE_BOXES) && (c->flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION)) || ((item.flags & MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL) && (c->flags & Client::Flag::DOES_NOT_SUPPORT_SEND_FUNCTION_CALL)) || @@ -769,7 +771,7 @@ void send_menu(shared_ptr c, const u16string& menu_name, c->version == GameVersion::BB) { send_menu_t(c, menu_name, menu_id, items, is_info_menu); } else { - send_menu_t(c, menu_name, menu_id, items, is_info_menu); + send_menu_t(c, menu_name, menu_id, items, is_info_menu); } } @@ -817,7 +819,9 @@ void send_game_menu_t(shared_ptr c, shared_ptr s) { } void send_game_menu(shared_ptr c, shared_ptr s) { - if ((c->version == GameVersion::DC) || (c->version == GameVersion::GC)) { + if ((c->version == GameVersion::DC) || + (c->version == GameVersion::GC) || + (c->version == GameVersion::XB)) { send_game_menu_t(c, s); } else { send_game_menu_t(c, s); @@ -868,6 +872,8 @@ void send_quest_menu(shared_ptr c, uint32_t menu_id, send_quest_menu_t(c, menu_id, quests, is_download_menu); } else if (c->version == GameVersion::GC) { send_quest_menu_t(c, menu_id, quests, is_download_menu); + } else if (c->version == GameVersion::XB) { + send_quest_menu_t(c, menu_id, quests, is_download_menu); } else if (c->version == GameVersion::BB) { send_quest_menu_t(c, menu_id, quests, is_download_menu); } else { @@ -881,6 +887,8 @@ void send_quest_menu(shared_ptr c, uint32_t menu_id, send_quest_menu_t(c, menu_id, items, is_download_menu); } else if (c->version == GameVersion::GC) { send_quest_menu_t(c, menu_id, items, is_download_menu); + } else if (c->version == GameVersion::XB) { + send_quest_menu_t(c, menu_id, items, is_download_menu); } else if (c->version == GameVersion::BB) { send_quest_menu_t(c, menu_id, items, is_download_menu); } else { @@ -917,47 +925,55 @@ void send_lobby_list(shared_ptr c, shared_ptr s) { template void send_join_game_t(shared_ptr c, shared_ptr l) { - S_JoinGame cmd; + bool is_ep3 = (l->flags & Lobby::Flag::EPISODE_3_ONLY); + string data(is_ep3 ? sizeof(S_JoinGame_GC_Ep3_64) : sizeof(S_JoinGame), '\0'); - cmd.variations = l->variations; + // TODO: This is a terrible way to handle the different Ep3 format within the + // template. Find a way to make this cleaner. + auto* cmd = reinterpret_cast*>(data.data()); + S_JoinGame_GC_Ep3_64* cmd_ep3 = nullptr; + if (is_ep3) { + cmd_ep3 = reinterpret_cast(data.data()); + new (cmd_ep3) S_JoinGame_GC_Ep3_64(); + } else { + new (cmd) S_JoinGame(); + } + + cmd->variations = l->variations; size_t player_count = 0; for (size_t x = 0; x < 4; x++) { if (l->clients[x]) { - cmd.lobby_data[x].player_tag = 0x00010000; - cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; - // See comment in send_join_lobby_t about Episode III behavior here - cmd.lobby_data[x].ip_address = 0x7F000001; - cmd.lobby_data[x].client_id = c->lobby_client_id; - cmd.lobby_data[x].name = l->clients[x]->game_data.player()->disp.name; - if (l->flags & Lobby::Flag::EPISODE_3_ONLY) { - cmd.players_ep3[x].inventory = l->clients[x]->game_data.player()->inventory; - cmd.players_ep3[x].disp = convert_player_disp_data( + cmd->lobby_data[x].player_tag = 0x00010000; + cmd->lobby_data[x].guild_card = l->clients[x]->license->serial_number; + cmd->lobby_data[x].client_id = c->lobby_client_id; + 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; + cmd_ep3->players_ep3[x].disp = convert_player_disp_data( l->clients[x]->game_data.player()->disp); } player_count++; + } else { + cmd->lobby_data[x].clear(); } } - cmd.client_id = c->lobby_client_id; - cmd.leader_id = l->leader_id; - cmd.disable_udp = 0x01; // TODO: This is unused on PC/BB. Is it OK to use 1 here anyway? - cmd.difficulty = l->difficulty; - cmd.battle_mode = (l->mode == 1) ? 1 : 0; - cmd.event = l->event; - cmd.section_id = l->section_id; - cmd.challenge_mode = (l->mode == 2) ? 1 : 0; - cmd.rare_seed = l->random_seed; - cmd.episode = l->episode; - cmd.unused2 = 0x01; - cmd.solo_mode = (l->mode == 3); - cmd.unused3 = 0x00; + cmd->client_id = c->lobby_client_id; + cmd->leader_id = l->leader_id; + cmd->disable_udp = 0x01; // Unused on PC/XB/BB + cmd->difficulty = l->difficulty; + cmd->battle_mode = (l->mode == 1) ? 1 : 0; + cmd->event = l->event; + cmd->section_id = l->section_id; + cmd->challenge_mode = (l->mode == 2) ? 1 : 0; + cmd->rare_seed = l->random_seed; + cmd->episode = l->episode; + cmd->unused2 = 0x01; + cmd->solo_mode = (l->mode == 3); + cmd->unused3 = 0x00; - // Player data is only sent in Episode III games; in other versions, the - // players send each other their data using 62/6D commands during loading - size_t data_size = (l->flags & Lobby::Flag::EPISODE_3_ONLY) - ? sizeof(cmd) : (sizeof(cmd) - sizeof(cmd.players_ep3)); - send_command(c, 0x64, player_count, &cmd, data_size); + send_command(c, 0x64, player_count, data); } template @@ -975,7 +991,8 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, } uint8_t lobby_type = (l->type > 14) ? (l->block - 1) : l->type; - // Allow non-canonical lobby types on GC + // Allow non-canonical lobby types on GC. They may work on other versions too, + // but I haven't verified which values don't crash on each version. if (c->version == GameVersion::GC) { if (c->flags & Client::Flag::EPISODE_3) { if ((l->type > 0x14) && (l->type < 0xE9)) { @@ -998,8 +1015,9 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, cmd.disable_udp = 0x01; cmd.lobby_number = lobby_type; cmd.block_number = l->block; + cmd.unknown_a1 = 0; cmd.event = l->event; - cmd.unused = 0x00000000; + cmd.unknown_a2 = 0; vector> lobby_clients; if (joining_client) { @@ -1017,11 +1035,6 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, auto& e = cmd.entries[used_entries++]; e.lobby_data.player_tag = 0x00010000; e.lobby_data.guild_card = lc->license->serial_number; - // There's a strange behavior (bug? "feature"?) in Episode 3 where the start - // button does nothing in the lobby (hence you can't "quit game") if the - // client's IP address is zero. So, we fill it in with a fake nonzero value - // to avoid this behavior. - e.lobby_data.ip_address = 0x7F000001; 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; @@ -1037,9 +1050,11 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, void send_join_lobby(shared_ptr c, shared_ptr l) { if (l->is_game()) { if (c->version == GameVersion::PC) { - send_join_game_t(c, l); + send_join_game_t(c, l); } else if (c->version == GameVersion::GC) { - send_join_game_t(c, l); + send_join_game_t(c, l); + } else if (c->version == GameVersion::XB) { + send_join_game_t(c, l); } else if (c->version == GameVersion::BB) { send_join_game_t(c, l); } else { @@ -1047,9 +1062,11 @@ void send_join_lobby(shared_ptr c, shared_ptr l) { } } else { if (c->version == GameVersion::PC) { - send_join_lobby_t(c, l); + send_join_lobby_t(c, l); } else if (c->version == GameVersion::GC) { - send_join_lobby_t(c, l); + send_join_lobby_t(c, l); + } else if (c->version == GameVersion::XB) { + send_join_lobby_t(c, l); } else if (c->version == GameVersion::BB) { send_join_lobby_t(c, l); } else { @@ -1069,9 +1086,11 @@ void send_join_lobby(shared_ptr c, shared_ptr l) { void send_player_join_notification(shared_ptr c, shared_ptr l, shared_ptr joining_client) { if (c->version == GameVersion::PC) { - send_join_lobby_t(c, l, joining_client); + send_join_lobby_t(c, l, joining_client); } else if (c->version == GameVersion::GC) { - send_join_lobby_t(c, l, joining_client); + send_join_lobby_t(c, l, joining_client); + } else if (c->version == GameVersion::XB) { + send_join_lobby_t(c, l, joining_client); } else if (c->version == GameVersion::BB) { send_join_lobby_t(c, l, joining_client); } else { @@ -1470,8 +1489,10 @@ void send_quest_file_chunk( void send_quest_file(shared_ptr c, const string& quest_name, const string& basename, const string& contents, QuestFileType type) { - if (c->version == GameVersion::PC || c->version == GameVersion::GC) { - send_quest_open_file_t( + if ((c->version == GameVersion::PC) || + (c->version == GameVersion::GC) || + (c->version == GameVersion::XB)) { + send_quest_open_file_t( c, quest_name, basename, contents.size(), type); } else if (c->version == GameVersion::BB) { send_quest_open_file_t( @@ -1509,8 +1530,10 @@ void send_server_time(shared_ptr c) { } void send_change_event(shared_ptr c, uint8_t new_event) { - // THis command isn't supported on versions before GC apparently - if ((c->version == GameVersion::GC) || (c->version == GameVersion::BB)) { + // THis command isn't supported on versions before V3 + if ((c->version == GameVersion::GC) || + (c->version == GameVersion::XB) || + (c->version == GameVersion::BB)) { send_command(c, 0xDA, new_event); } } diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 8d2c311b..418b5c4c 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -92,7 +92,7 @@ void send_command_with_header(Channel& c, const void* data, size_t size); -S_ServerInit_DC_PC_GC_02_17_91_9B prepare_server_init_contents_dc_pc_gc( +S_ServerInit_DC_PC_V3_02_17_91_9B prepare_server_init_contents_dc_pc_v3( bool initial_connection, uint32_t server_key, uint32_t client_key); diff --git a/src/ServerState.cc b/src/ServerState.cc index 1311907d..53f56e57 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -218,8 +218,8 @@ uint32_t ServerState::connect_address_for_client(std::shared_ptr c) { shared_ptr> ServerState::information_menu_for_version(GameVersion version) { if (version == GameVersion::PC) { return this->information_menu_pc; - } else if (version == GameVersion::GC) { - return this->information_menu_gc; + } else if ((version == GameVersion::GC) || (version == GameVersion::XB)) { + return this->information_menu_v3; } throw out_of_range("no information menu exists for this version"); } @@ -229,6 +229,8 @@ const vector& ServerState::proxy_destinations_menu_for_version(GameVer return this->proxy_destinations_menu_pc; } else if (version == GameVersion::GC) { return this->proxy_destinations_menu_gc; + } else if (version == GameVersion::XB) { + return this->proxy_destinations_menu_xb; } throw out_of_range("no proxy destinations menu exists for this version"); } @@ -238,6 +240,8 @@ const vector>& ServerState::proxy_destinations_for_versio return this->proxy_destinations_pc; } else if (version == GameVersion::GC) { return this->proxy_destinations_gc; + } else if (version == GameVersion::XB) { + return this->proxy_destinations_xb; } throw out_of_range("no proxy destinations menu exists for this version"); } @@ -265,10 +269,10 @@ void ServerState::create_menus(shared_ptr config_json) { const auto& d = config_json->as_dict(); shared_ptr> information_menu_pc(new vector()); - shared_ptr> information_menu_gc(new vector()); + shared_ptr> information_menu_v3(new vector()); shared_ptr> information_contents(new vector()); - information_menu_gc->emplace_back(InformationMenuItemID::GO_BACK, u"Go back", + information_menu_v3->emplace_back(InformationMenuItemID::GO_BACK, u"Go back", u"Return to the\nmain menu", 0); { uint32_t item_id = 0; @@ -276,45 +280,52 @@ void ServerState::create_menus(shared_ptr config_json) { auto& v = item->as_list(); information_menu_pc->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), decode_sjis(v.at(1)->as_string()), 0); - information_menu_gc->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), + information_menu_v3->emplace_back(item_id, decode_sjis(v.at(0)->as_string()), decode_sjis(v.at(1)->as_string()), MenuItem::Flag::REQUIRES_MESSAGE_BOXES); information_contents->emplace_back(decode_sjis(v.at(2)->as_string())); item_id++; } } this->information_menu_pc = information_menu_pc; - this->information_menu_gc = information_menu_gc; + this->information_menu_v3 = information_menu_v3; this->information_contents = information_contents; - auto generate_proxy_destinations_menu = +[]( + auto generate_proxy_destinations_menu = [&]( vector& ret_menu, vector>& ret_pds, - const unordered_map>& d) { - ret_menu.clear(); - ret_pds.clear(); + const char* key) { + try { + const auto& items = d.at(key); + ret_menu.clear(); + ret_pds.clear(); - ret_menu.emplace_back(ProxyDestinationsMenuItemID::GO_BACK, u"Go back", - u"Return to the\nmain menu", 0); + ret_menu.emplace_back(ProxyDestinationsMenuItemID::GO_BACK, u"Go back", + u"Return to the\nmain menu", 0); - uint32_t item_id = 0; - for (const auto& item : d) { - const string& netloc_str = item.second->as_string(); - const string& description = "$C7Remote server:\n$C6" + netloc_str; - ret_menu.emplace_back(item_id, decode_sjis(item.first), - decode_sjis(description), 0); - ret_pds.emplace_back(parse_netloc(netloc_str)); - item_id++; - } + uint32_t item_id = 0; + for (const auto& item : items->as_dict()) { + const string& netloc_str = item.second->as_string(); + const string& description = "$C7Remote server:\n$C6" + netloc_str; + ret_menu.emplace_back(item_id, decode_sjis(item.first), + decode_sjis(description), 0); + ret_pds.emplace_back(parse_netloc(netloc_str)); + item_id++; + } + } catch (const out_of_range&) { } }; generate_proxy_destinations_menu( this->proxy_destinations_menu_pc, this->proxy_destinations_pc, - d.at("ProxyDestinations-PC")->as_dict()); + "ProxyDestinations-PC"); generate_proxy_destinations_menu( this->proxy_destinations_menu_gc, this->proxy_destinations_gc, - d.at("ProxyDestinations-GC")->as_dict()); + "ProxyDestinations-GC"); + generate_proxy_destinations_menu( + this->proxy_destinations_menu_xb, + this->proxy_destinations_xb, + "ProxyDestinations-XB"); try { const string& netloc_str = d.at("ProxyDestination-Patch")->as_string(); @@ -355,6 +366,10 @@ void ServerState::create_menus(shared_ptr config_json) { this->main_menu.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, u"Proxy server", u"Connect to another\nserver", MenuItem::Flag::GC_ONLY); } + if (!this->proxy_destinations_xb.empty()) { + this->main_menu.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, u"Proxy server", + u"Connect to another\nserver", MenuItem::Flag::XB_ONLY); + } this->main_menu.emplace_back(MainMenuItemID::DOWNLOAD_QUESTS, u"Download quests", u"Download quests", MenuItem::Flag::INVISIBLE_ON_BB); if (!this->function_code_index->patch_menu_empty()) { diff --git a/src/ServerState.hh b/src/ServerState.hh index 77b84f67..41db60f2 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -62,12 +62,14 @@ struct ServerState { std::vector main_menu; std::shared_ptr> information_menu_pc; - std::shared_ptr> information_menu_gc; + std::shared_ptr> information_menu_v3; std::shared_ptr> information_contents; std::vector proxy_destinations_menu_pc; std::vector proxy_destinations_menu_gc; + std::vector proxy_destinations_menu_xb; std::vector> proxy_destinations_pc; std::vector> proxy_destinations_gc; + std::vector> proxy_destinations_xb; std::pair proxy_destination_patch; std::pair proxy_destination_bb; std::u16string welcome_message; diff --git a/src/Version.cc b/src/Version.cc index 26a9579d..28ba8072 100644 --- a/src/Version.cc +++ b/src/Version.cc @@ -10,13 +10,14 @@ using namespace std; -uint16_t flags_for_version(GameVersion version, uint8_t sub_version) { +uint16_t flags_for_version(GameVersion version, int64_t sub_version) { switch (sub_version) { - case 0x00: // initial check (before 9E recognition) + case -1: // initial check (before 9E recognition) switch (version) { case GameVersion::DC: return Client::Flag::DEFAULT_V2_DC; case GameVersion::GC: + case GameVersion::XB: return Client::Flag::DEFAULT_V3_GC; case GameVersion::PC: return Client::Flag::DEFAULT_V2_PC; @@ -26,33 +27,35 @@ uint16_t flags_for_version(GameVersion version, uint8_t sub_version) { return Client::Flag::DEFAULT_V4_BB; } break; - case 0x29: // PSO PC + case 0x29: // PC return Client::Flag::DEFAULT_V2_PC; - case 0x30: // PSO Ep1&2 JP v1.02 - case 0x31: // PSO Ep1&2 US v1.00, US v1.01, EU v1.00, JP v1.00 - case 0x32: // PSO Ep1&2 EU 50Hz - case 0x33: // PSO Ep1&2 EU 60Hz - case 0x34: // PSO Ep1&2 JP v1.03 + case 0x30: // GC Ep1&2 JP v1.02, at least one version of PSO XB + case 0x31: // GC Ep1&2 US v1.00, GC US v1.01, GC EU v1.00, GC JP v1.00 + case 0x32: // GC Ep1&2 EU 50Hz + case 0x33: // GC Ep1&2 EU 60Hz + case 0x34: // GC Ep1&2 JP v1.03 return Client::Flag::DEFAULT_V3_GC; - case 0x35: // PSO Ep1&2 JP v1.04 (Plus) + case 0x35: // GC Ep1&2 JP v1.04 (Plus) return Client::Flag::DEFAULT_V3_GC_PLUS; - case 0x36: // PSO Ep1&2 US v1.02 (Plus) - case 0x39: // PSO Ep1&2 JP v1.05 (Plus) + case 0x36: // GC Ep1&2 US v1.02 (Plus) + case 0x39: // GC Ep1&2 JP v1.05 (Plus) return Client::Flag::DEFAULT_V3_GC_PLUS_NO_SFC; - case 0x42: // PSO Ep3 JP + case 0x42: // GC Ep3 JP return Client::Flag::DEFAULT_V3_GC_EP3; - case 0x40: // PSO Ep3 trial (TODO: Does this support send_function_call?) - case 0x41: // PSO Ep3 US - case 0x43: // PSO Ep3 EU + case 0x40: // GC Ep3 trial (TODO: Does this support send_function_call?) + case 0x41: // GC Ep3 US + case 0x43: // GC Ep3 EU return Client::Flag::DEFAULT_V3_GC_EP3_NO_SFC; } - return 0; + throw runtime_error("unknown sub_version"); } const char* name_for_version(GameVersion version) { switch (version) { case GameVersion::GC: return "GC"; + case GameVersion::XB: + return "XB"; case GameVersion::PC: return "PC"; case GameVersion::BB: @@ -73,6 +76,8 @@ GameVersion version_for_name(const char* name) { return GameVersion::PC; } else if (!strcasecmp(name, "GC") || !strcasecmp(name, "GameCube")) { return GameVersion::GC; + } else if (!strcasecmp(name, "XB") || !strcasecmp(name, "Xbox")) { + return GameVersion::XB; } else if (!strcasecmp(name, "BB") || !strcasecmp(name, "BlueBurst") || !strcasecmp(name, "Blue Burst")) { return GameVersion::BB; diff --git a/src/Version.hh b/src/Version.hh index c71ae18d..381d7a66 100644 --- a/src/Version.hh +++ b/src/Version.hh @@ -9,6 +9,7 @@ enum class GameVersion { PC, PATCH, GC, + XB, BB, }; @@ -21,7 +22,7 @@ enum class ServerBehavior { PROXY_SERVER, }; -uint16_t flags_for_version(GameVersion version, uint8_t sub_version); +uint16_t flags_for_version(GameVersion version, int64_t sub_version); const char* name_for_version(GameVersion version); GameVersion version_for_name(const char* name); diff --git a/system/config.example.json b/system/config.example.json index e80d01c2..3e72a495 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -36,6 +36,9 @@ "bb-patch": [11000, "patch", "patch_server"], "bb-init": [12000, "bb", "data_server_bb"], + // TODO: If Xbox support ever gets built, add this port to the above config. + // "xb-login": [????, "xb", "login_server"], + // Schthack PSOBB uses these ports. // "bb-patch2": [10500, "patch", "patch_server"], // "bb-init2": [13000, "bb", "data_server_bb"], @@ -55,9 +58,11 @@ "pc-lobby": [9420, "pc", "lobby_server"], "gc-lobby": [9421, "gc", "lobby_server"], "bb-lobby": [9422, "bb", "lobby_server"], + "xb-lobby": [9423, "xb", "lobby_server"], "pc-proxy": [9520, "pc", "proxy_server"], "gc-proxy": [9521, "gc", "proxy_server"], "bb-proxy": [9522, "bb", "proxy_server"], + "xb-proxy": [9523, "xb", "proxy_server"], "bb-data1": [12004, "bb", "data_server_bb"], "bb-data2": [12005, "bb", "data_server_bb"], }, @@ -75,8 +80,9 @@ // Other servers to support proxying to. If either of these is empty, the // proxy server is disabled for that game version. Entries are like // "name": "address:port"; the names are used in the proxy server menu. - "ProxyDestinations-GC": {}, "ProxyDestinations-PC": {}, + "ProxyDestinations-GC": {}, + "ProxyDestinations-XB": {}, // Proxy destination for patch server clients. If this is given, the internal // patch server (for PC and BB) is bypassed, and any client that connects to // the patch server is instead proxied to this destination.