From 47f97f357fcf22c4721e9ab5d13141fee27797da Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 26 Jun 2022 11:41:25 -0700 Subject: [PATCH] add some undocumented client commands from PC, GC and BB --- src/CommandFormats.hh | 461 ++++++++++++++++++++++++++++++++++------- src/ProxyCommands.cc | 12 +- src/ProxyServer.cc | 6 +- src/ReceiveCommands.cc | 103 ++++++--- src/SendCommands.cc | 26 ++- src/SendCommands.hh | 5 +- 6 files changed, 481 insertions(+), 132 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 0c77db91..533fd2e4 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -277,6 +277,20 @@ struct S_ServerInit_DC_PC_GC_02_17_92_9B { ptext after_message; }; +// 03 (C->S): Legacy login (non-BB) + +struct C_LegacyLogin_PC_GC_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 + // 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 + // field on the client's connection context object.) + ptext serial_number2; + ptext access_key2; +}; + // 03 (S->C): Legacy password check result (non-BB) // 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 @@ -303,14 +317,20 @@ struct S_ServerInit_BB_03 { // 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_GC_04 { - uint32_t unknown_a1[2]; +struct C_LegacyLogin_PC_GC_04 { + le_uint64_t unused1; // Same as unused field in 9D/9E le_uint32_t sub_version; - le_uint32_t player_tag; + le_uint32_t unused2; ptext serial_number; ptext access_key; }; +struct C_LegacyLogin_BB_04 { + parray unknown_a1; + ptext username; + ptext password; +}; + // 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 @@ -427,17 +447,44 @@ struct S_Unknown_GC_0E { // 0F: Invalid command // 10 (C->S): Menu selection +// header.flag contains two flags: 02 specifies if a password is present, and 01 +// specifies... something else. These two bits directly correspond to the two +// lowest bits in the flags field of the game menu: 02 specifies that the game +// is locked, but the function of 01 is unknown. -struct C_MenuSelection { +struct C_MenuSelection_10_Flag00 { le_uint32_t menu_id; le_uint32_t item_id; - // Password is only present when client attempts to join a locked game. - union { - char dcgc[0]; - char16_t pcbb[0]; - } password; }; +template +struct C_MenuSelection_10_Flag01 { + le_uint32_t menu_id; + le_uint32_t item_id; + ptext unknown_a1; +}; +struct C_MenuSelection_DC_GC_10_Flag01 : C_MenuSelection_10_Flag01 { }; +struct C_MenuSelection_PC_BB_10_Flag01 : C_MenuSelection_10_Flag01 { }; + +template +struct C_MenuSelection_10_Flag02 { + le_uint32_t menu_id; + le_uint32_t item_id; + ptext password; +}; +struct C_MenuSelection_DC_GC_10_Flag02 : C_MenuSelection_10_Flag02 { }; +struct C_MenuSelection_PC_BB_10_Flag02 : C_MenuSelection_10_Flag02 { }; + +template +struct C_MenuSelection_10_Flag03 { + le_uint32_t menu_id; + le_uint32_t item_id; + ptext unknown_a1; + ptext password; +}; +struct C_MenuSelection_DC_GC_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. @@ -527,6 +574,11 @@ struct S_ReconnectSplit_19 { // 1E: Invalid command +// 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 +// unused. + // 1F (S->C): Information menu // Same format and usage as 07 command, except: // - The menu title will say "Information" instead of "Ship Select". @@ -542,13 +594,16 @@ struct S_ReconnectSplit_19 { // Format unknown // 22: GameGuard check (BB) -// An older version of BB used a 4-byte challenge in the flag field (and the -// command had no payload). The latest version uses this 16-byte challenge. -struct SC_GameCardCheck_BB_22 { - parray data; +// Command 0022 is a 16-byte challenge (sent in the data field) using the +// following structure. + +struct SC_GameCardCheck_BB_0022 { + parray data; }; +// Command 0122 uses a 4-byte challenge sent in the header.flag field instead. + // 23 (S->C): Unknown (BB) // header.flag is used, but command has no other arguments. @@ -701,7 +756,8 @@ struct C_OpenFileConfirmation_44_A6 { // 61 (C->S): Player data // See PSOPlayerDataPC, PSOPlayerDataGC, and PSOPlayerDataBB in Player.hh for -// this command's format. +// 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. // 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 @@ -966,7 +1022,8 @@ struct S_ArrowUpdateEntry_88 { struct C_LegacyLogin_GC_90 { ptext serial_number; - ptext access_key; + ptext access_key; + parray unused; }; // 90 (S->C): License verification result (GC) @@ -1024,23 +1081,32 @@ struct C_Login_BB_93 { // servers may have just duplicated Sega's behavior verbatim. // Client will respond with a 61 command. -// 96 (C->S): Client checksum +// 96 (C->S): Character save information -struct C_ClientChecksum_GC_96 { - // It seems only at most 48 bits of this are actually used - the highest-order - // (last) two bytes seem to always be zero. - le_uint64_t checksum; +struct C_CharSaveInfo_GC_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; + // This field counts certain events on a per-character basis. One of the + // relevant events is the act of sending a 96 command; another is the act of + // receiving a 97 command (to which the client responds with a B1 command). + // Presumably Sega's original implementation could keep track of this value + // for each character and could therefore tell if a character had connected to + // an unofficial server between connections to Sega's servers. + le_uint32_t event_counter; }; // 97 (S->C): Save to memory card // No arguments -// According to Sylverant's documentation, sending this command with header.flag -// == 0 will cause the client to delete a lot of the character's data. This was -// presumably intended to be used by Sega if they detected cheating. newserv -// always sends header.flag = 1 here, which saves without deleting anything. +// Sending this command with header.flag == 0 will show a message saying that +// "character data was improperly saved", and will delete the character's items +// and challenge mode records. newserv (and all other unofficial servers) always +// send this command with flag == 1, which causes the client to save normally. +// Client will respond with a B1 command if header.flag is nonzero. // 98 (C->S): Leave game -// Same format as 61 command. +// 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. // Client will send an 84 when it's ready to join a lobby. // 99 (C->S): Server time accepted @@ -1049,12 +1115,13 @@ struct C_ClientChecksum_GC_96 { // 9A (C->S): Initial login (no password or client config) struct C_Login_DC_PC_GC_9A { - ptext unused; + ptext unused1; + ptext unused2; ptext serial_number; ptext access_key; - uint32_t player_tag; - uint32_t guild_card_number; - uint32_t sub_version; + le_uint32_t player_tag; + le_uint32_t guild_card_number; + le_uint32_t sub_version; ptext serial_number2; ptext access_key2; ptext email_address; @@ -1093,9 +1160,10 @@ struct C_Login_DC_PC_GC_9A { // - The client will respond with a command DB instead of a command 93. // 9C (C->S): Register +// It appears PSO GC sends uninitialized data in the header.flag field here. struct C_Register_DC_PC_GC_9C { - ptext unused; + le_uint64_t unused; le_uint32_t sub_version; le_uint32_t unused2; ptext serial_number; @@ -1103,6 +1171,14 @@ struct C_Register_DC_PC_GC_9C { ptext password; }; +struct C_Register_BB_9C { + le_uint32_t sub_version; + le_uint32_t unknown_a1; // Only the second byte (0x0000??00) is used, but is 0 + ptext username; + ptext password; + ptext game_tag; // "psopc2" on BB +}; + // 9C (S->C): Register result // The only possible error here seems to be wrong password (127) which is // displayed if the header.flag field is zero. If header.flag is nonzero, the @@ -1119,15 +1195,19 @@ struct C_Login_PC_9D { le_uint32_t guild_card_number; // 0xFFFFFFFF if not set le_uint64_t unused; le_uint32_t sub_version; - parray unused2; // 00 01 00 00 ... (rest is 00) + uint8_t is_extended; // If 1, structure has extended format + uint8_t unknown_a1; // Always 1? + 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 name; }; -struct C_LoginWithUnusedSpace_PC_9D : C_Login_PC_9D { - parray unused_space; +struct C_LoginExtended_PC_9D : C_Login_PC_9D { + parray unknown_a2; }; // 9E (C->S): Log in with client config @@ -1143,8 +1223,25 @@ struct C_Login_GC_9E : C_Login_PC_9D { ClientConfigFields() : data() { } } client_config; }; -struct C_LoginWithUnusedSpace_GC_9E : C_Login_GC_9E { - parray unused_space; +struct C_LoginExtended_GC_9E : C_Login_GC_9E { + parray unknown_a2; +}; + +struct C_LoginExtended_BB_9E { + le_uint32_t player_tag; + le_uint32_t guild_card_number; // == serial_number when on newserv + le_uint32_t sub_version; + le_uint32_t unknown_a1; + le_uint32_t unknown_a2; + ptext unknown_a3; // Always blank? + ptext unknown_a4; // == "?" + ptext unknown_a5; // Always blank? + ptext unknown_a6; // Always blank? + ptext username; + ptext password; + ptext guild_card_number_str; + parray unknown_a7; + parray unused; // Always zero }; // 9F (S->C): Request client config / security data @@ -1159,7 +1256,7 @@ struct C_LoginWithUnusedSpace_GC_9E : C_Login_GC_9E { struct C_ChangeShipOrBlock_A0_A1 { le_uint32_t player_tag; le_uint32_t guild_card_number; - parray unknown; // all zeroes + parray unused; // Verified as unused from client disassembly }; // A0 (S->C): Ship select menu @@ -1225,6 +1322,7 @@ struct S_QuestMenuEntry_BB_A2_A4 { // This command is sent when the in-game quest menu (A2) is closed. When the // download quest menu is closed, either by downloading a quest or canceling, // the client sends A0 instead. +// Curiously, PSO GC sends uninitialized data in the flag argument. // AA (C->S): Update quest statistics // This command is used in Maximum Attack 2, but its format is unlikely to be @@ -1233,13 +1331,14 @@ struct S_QuestMenuEntry_BB_A2_A4 { // The server will respond with an AB command. struct C_UpdateQuestStatistics_AA { - le_uint32_t quest_internal_id; + le_uint16_t quest_internal_id; + le_uint16_t unused; le_uint16_t request_token; le_uint16_t unknown_a1; le_uint32_t unknown_a2; le_uint32_t kill_count; le_uint32_t time_taken; // in seconds - parray unknown_a3; + parray unknown_a3; }; // AB (S->C): Confirm update quest statistics @@ -1356,9 +1455,9 @@ struct S_RankUpdate_GC_Ep3_B7 { le_uint32_t jukebox_songs_unlocked; }; -// B8 (C->S): Confirm rank update +// B7 (C->S): Confirm rank update // No arguments -// The client sends this after it receives a B8 from the server. +// The client sends this after it receives a B7 from the server. // B8 (S->C): Update card definitions (Episode 3) // Contents is a single little-endian le_uint32_t specifying the size of the @@ -1450,15 +1549,15 @@ struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry { }; template struct C_CreateGame { - le_uint64_t unused; + parray unused; ptext name; ptext password; - uint8_t difficulty; // 0-3 - uint8_t battle_mode; // 0 or 1 + uint8_t difficulty; // 0-3 (always 0 on Episode 3) + uint8_t battle_mode; // 0 or 1 (always 0 on Episode 3) // Note: Episode 3 uses the challenge mode flag for view battle permissions. // 0 = view battle allowed; 1 = not allowed uint8_t challenge_mode; // 0 or 1 - uint8_t episode; // 1-4 on V3+; unused on DC/PC + 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_PC_C1 : C_CreateGame { }; @@ -1537,7 +1636,38 @@ struct C_SetBlockedSenders_GC_BB_C6 { // Same as 60, but only send to Episode 3 clients. // CA (C->S): Server data request (Episode 3) -// TODO: Document this. It does many different things. +// The format is generally the same as the subcommand-based commands (60, 62, +// etc.), but the server is expected to respond to the command instead of +// forwarding it. (The client has no handler for CA commands at all.) +// Generally a CA command looks like this: +// CA 00 SS SS B3 TT 00 00 WW 00 00 00 ... +// S = command size +// T = subcommand size in uint32_ts (== (S / 4) - 1) +// W = subcommand number +// We refer to Episode 3 server data commands as CAxWW, where W comes from the +// format above. The server data commands are: +// CAx0B (T=05) - Unknown +// CAx0C (T=05) - Unknown +// CAx0D (T=07) - Unknown +// CAx0E (T=05) - Unknown +// CAx0F (T=07) - Unknown +// CAx10 (T=06) - Unknown +// CAx11 (T=1E) - Unknown +// CAx12 (T=05) - Unknown +// CAx13 (T=AF) - Update game state (?) +// CAx14 (T=1B) - Update playfield state (?) +// CAx1B (T=09) - Update names (?) +// CAx1D (T=04) - Unknown +// CAx21 (T=05) - Unknown +// CAx28 (T=05) - Unknown +// CAx2B (T=05) - Unknown +// CAx34 (T=05) - Unknown +// CAx3A (T=04) - Unknown +// CAx40 (T=04) - Map list request. See send_ep3_map_list for server response. +// CAx41 (T=05) - Map data request. See send_ep3_map_data for server response. +// CAx48 (T=05) - Unknown +// CAx49 (T=C1) - Unknown +// TODO: Document the above commands that are currently unknown. // CB: Broadcast command (Episode 3) // Same as 60, but only send to Episode 3 clients. @@ -1562,12 +1692,15 @@ struct S_Unknown_GC_Ep3_CC { // D0 (C->S): Execute trade via trade window (GC/BB) // General sequence: client sends D0, server sends D1 to that client, server // sends D3 to other client, server sends D4 to both (?) clients. -// Format unknown +// Format unknown. On PSO GC it appears to be always 0x288 bytes in size; on BB +// it is 0x28C bytes in size, implying that the format is the same between the +// two versions (since BB headers are 4 bytes longer). // D1 (S->C): Confirm trade to initiator (GC/BB) // No arguments -// D2: Invalid command +// D2 (C->S): Unknown (used in trade sequence) +// No arguments // D3 (S->C): Execute trade with accepter (GC/BB) // Format unknown; appears to be same as D0. @@ -1640,14 +1773,18 @@ struct C_VerifyLicense_GC_DB { ptext password; }; +// Note: This login pathway generally isn't used on BB (and isn't supported at +// all during the data server phase). All current servers use 03/93 instead. struct C_VerifyLicense_BB_DB { - ptext unknown_a1; - ptext unknown_a2; - ptext unknown_a3; - ptext unknown_a4; - ptext unknown_a5; - ptext unknown_a6; - ptext unknown_a7; + // Note: These four fields are likely the same as those used in BB's 9E + ptext unknown_a3; // Always blank? + ptext unknown_a4; // == "?" + ptext unknown_a5; // Always blank? + ptext unknown_a6; // Always blank? + le_uint32_t sub_version; + ptext username; + ptext password; + ptext game_tag; // "psopc2" }; // DC: Player menu state (Episode 3) @@ -1691,7 +1828,39 @@ struct S_RareMonsterConfig_BB_DE { le_uint16_t data[16]; }; -// DF: Invalid command +// DF (C->S): Unknown (BB) +// This command has many subcommands. It's not clear what any of them do. + +struct C_Unknown_BB_01DF { + le_uint32_t unknown_a1; +}; + +struct C_Unknown_BB_02DF { + le_uint32_t unknown_a1; +}; + +struct C_Unknown_BB_03DF { + le_uint32_t unknown_a1; +}; + +struct C_Unknown_BB_04DF { + le_uint32_t unknown_a1; +}; + +struct C_Unknown_BB_05DF { + le_uint32_t unknown_a1; + ptext unknown_a2; +}; + +struct C_Unknown_BB_06DF { + parray unknown_a1; +}; + +struct C_Unknown_BB_07DF { + le_uint32_t unused1; // Always 0xFFFFFFFF + le_uint32_t unused2; // Always 0 + parray unknown_a1; +}; // E0 (S->C): Tournament list (Episode 3) @@ -1718,11 +1887,13 @@ struct S_Unknown_GC_Ep3_E1 { }; // E2 (C->S): Tournament control (Episode 3) -// Flag = 0 => request tournament list (server responds with E0) -// Flag = 1 => check tournament -// Flag = 2 => cancel tournament entry -// Flag = 3 => create tournament spectator team -// Flag = 4 => join tournament spectator team +// No arguments (in any of its forms) +// Command meaning differs based on the value of header.flag. Specifically: +// header.flag = 00 => request tournament list (server responds with E0) +// header.flag = 01 => check tournament +// header.flag = 02 => cancel tournament entry +// header.flag = 03 => create tournament spectator team +// header.flag = 04 => join tournament spectator team // E2 (S->C): Tournament entry control (Episode 3) @@ -1795,6 +1966,10 @@ struct S_PlayerPreview_NoPlayer_BB_00E4 { le_uint32_t error; // 2 = no player present }; +// E5 (C->S): CARD lobby game? (Episode 3) +// Appears to be the same as E4 (C->S), but also appears never to be sent by the +// client. + // E5 (S->C): Player preview (BB) // E5 (C->S): Create character (BB) @@ -1803,8 +1978,25 @@ struct SC_PlayerPreview_CreateCharacter_BB_00E5 { PlayerDispDataBBPreview preview; }; -// E6: Spectator team list (Episode 3) +// E6 (C->S): Spectator team control (Episode 3) + +// With header.flag == 0, this command has no arguments and is used for +// requesting the spectator team list. The server responds with an E6 command. + +// With header.flag == 1, this command is presumably used for joining a +// spectator team (TODO: verify this). The following arguments are given in this +// form: + +struct C_JoinSpectatorTeam_GC_Ep3_E6_Flag01 { + le_uint32_t menu_id; + le_uint32_t item_id; +}; + +// E6 (S->C): Spectator team list (Episode 3) // Same format as 08 command. +// TODO: There are two separate functions on the client for this, one that sends +// with header.flag == 0, and one that sends with header.flag == 1. Figure out +// the difference between these two and document it here. // E6 (S->C): Set guild card number and update client config (BB) @@ -1817,11 +2009,22 @@ struct S_ClientInit_BB_00E6 { le_uint32_t caps; // should be 0x00000102 }; +// E7 (C->S): Create spectator team (Episode 3) + +struct C_CreateSpectatorTeam_GC_Ep3_E7 { + le_uint32_t menu_id; + le_uint32_t item_id; + ptext name; + ptext password; + le_uint32_t unused; +}; + // E7 (S->C): Unknown (Episode 3) // Same format as E2 command. // E7: Save or load full player data (BB) // See export_bb_player_data() in Player.cc for format. +// TODO: Verify full breakdown from send_E7 in BB disassembly. // E8 (S->C): Join spectator team (probably) (Episode 3) @@ -1873,14 +2076,43 @@ struct S_AcceptClientChecksum_BB_02E8 { // No arguments // Server should send the guild card file data using DC commands. -// 04E8 (C->S): Accept sent guild card +// 04E8 (C->S): Add guild card + +struct C_AddOrUpdateGuildCard_BB_04E8_06E8_07E8 { + // TODO: Document this format + parray unknown_a1; +}; + // 05E8 (C->S): Delete guild card + +struct C_DeleteGuildCard_BB_05E8_08E8 { + le_uint32_t guild_card_number; +}; + // 06E8 (C->S): Set guild card text -// 07E8 (C->S): Block user -// 08E8 (C->S): Unblock user +// Same format as 04E8. + +// 07E8 (C->S): Add blocked user +// Same format as 04E8. + +// 08E8 (C->S): Delete blocked user +// Same format as 05E8. + // 09E8 (C->S): Write comment + +struct C_WriteGuildCardComment_BB_09E8 { + ptext comment; +}; + // 0AE8 (C->S): Set guild card position in list +struct C_MoveGuildCard_BB_0AE8 { + // TODO: One of these is the GC number, the other is the position. Figure out + // which is which. + le_uint32_t unknown_a1; + le_uint32_t unknown_a2; +}; + // E9 (S->C): Other player left spectator team (probably) (Episode 3) // Same format as 66/69 commands. @@ -1896,24 +2128,83 @@ struct S_Unknown_GC_Ep3_EA { // EA: Team control (BB) // 01EA (C->S): Create team + +struct C_CreateTeam_BB_01EA { + ptext name; +}; + // 03EA (C->S): Add team member + +struct C_AddOrRemoveTeamMember_BB_03EA_05EA { + le_uint32_t guild_card_number; +}; + // 05EA (C->S): Remove team member +// Same format as 03EA. + // 07EA (C->S): Team chat + // 08EA (C->S): Team admin +// No arguments + // 0DEA (C->S): Unknown +// No arguments + // 0EEA (S->C): Unknown + // 0FEA (C->S): Set team flag + +struct C_SetTeamFlag_BB_0FEA { + parray data; +}; + // 10EA: Delete team +// No arguments + // 11EA (C->S): Promote team member +// TODO: header.flag is used for this command. Figure out what it's for. + +struct C_PromoteTeamMember_BB_11EA { + le_uint32_t unknown_a1; +}; + // 12EA (S->C): Unknown + // 13EA: Unknown +// No arguments + // 14EA (C->S): Unknown +// No arguments. Client always sends 1 in the header.flag field. + // 15EA (S->C): Unknown + // 18EA: Membership information +// No arguments (C->S) +// TODO: Document S->C format + // 19EA: Privilege list +// No arguments (C->S) +// TODO: Document S->C format + // 1AEA: Unknown // 1CEA (C->S): Ranking information +// 1BEA (C->S): Unknown +// header.flag is used, but no other arguments + +// 1CEA (C->S): Unknown +// No arguments + +// 1EEA (C->S): Unknown +// header.flag is used, but it's unknown what the value means. + +struct C_Unknown_BB_1EEA { + ptext unknown_a1; +}; + +// 20EA (C->S): Unknown +// header.flag is used, but no other arguments + // EB (S->C): Unknown (Episode 3) // Looks like another lobby-joining type of command. @@ -1963,6 +2254,7 @@ union C_UpdateAccountData_BB_ED { parray pad_config; // 05ED parray tech_menu; // 06ED parray customize; // 07ED + parray challenge_battle_config; // 08ED }; // EE (S->C): Unknown (Episode 3) @@ -1970,28 +2262,39 @@ union C_UpdateAccountData_BB_ED { // the relevant flag values match the Episodes 1 & 2 trade window commands, this // may be used for trading cards. -union S_Unknown_GC_Ep3_EE { - struct { - le_uint16_t unknown_a1; - le_uint16_t unknown_a2; - struct Entry { - le_uint32_t unknown_a1; - le_uint32_t unknown_a2; - }; - Entry entries[4]; - } flag_D3; - struct { +struct C_Unknown_GC_Ep3_EE_FlagD0 { + parray unknown_a1; +}; + +struct S_Unknown_GC_Ep3_EE_FlagD1 { + le_uint32_t unknown_a1; +}; + +// EE D2 04 00 (C->S) - no arguments + +struct S_Unknown_GC_Ep3_EE_FlagD3 { + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + struct Entry { le_uint32_t unknown_a1; - } flag_D1; - struct { - le_uint32_t unknown_a1; - } flag_D4; + le_uint32_t unknown_a2; + }; + Entry entries[4]; +}; + +// EE D4 04 00 (C->S) - no arguments + +struct S_Unknown_GC_Ep3_EE_FlagD4 { + le_uint32_t unknown_a1; }; // EE (S->C): Scrolling message (BB) // Same format as 01. The message appears at the top of the screen and slowly // scrolls to the left. +// EF (C->S): Unknown (Episode 3) +// No arguments + // EF (S->C): Unknown (Episode 3) struct S_Unknown_GC_Ep3_EF { diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index e935ae15..c93e8c05 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -117,7 +117,7 @@ static HandlerResult process_server_gc_9A(shared_ptr, return HandlerResult::FORWARD; } - C_LoginWithUnusedSpace_GC_9E cmd; + C_LoginExtended_GC_9E cmd; if (session.remote_guild_card_number == 0) { cmd.player_tag = 0xFFFF0000; cmd.guild_card_number = 0xFFFFFFFF; @@ -127,7 +127,8 @@ static HandlerResult process_server_gc_9A(shared_ptr, } cmd.unused = 0; cmd.sub_version = session.sub_version; - cmd.unused2.data()[1] = 1; + cmd.is_extended = session.remote_guild_card_number ? 0 : 1; + cmd.unknown_a1 = 1; cmd.serial_number = string_printf("%08" PRIX32 "", session.license->serial_number); cmd.access_key = session.license->access_key; cmd.serial_number2 = cmd.serial_number; @@ -139,8 +140,8 @@ static HandlerResult process_server_gc_9A(shared_ptr, // right after the client config data session.server_channel.send( - 0x9E, 0x01, &cmd, - sizeof(C_LoginWithUnusedSpace_GC_9E) - (session.remote_guild_card_number ? sizeof(cmd.unused_space) : 0)); + 0x9E, 0x01, &cmd, + cmd.is_extended ? sizeof(C_LoginExtended_GC_9E) : sizeof(C_Login_GC_9E)); return HandlerResult::SUPPRESS; } @@ -209,7 +210,8 @@ static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr s, } cmd.unused = 0xFFFFFFFFFFFF0000; cmd.sub_version = session.sub_version; - cmd.unused2.data()[1] = 1; + cmd.is_extended = 0; + cmd.unknown_a1 = 1; cmd.serial_number = string_printf("%08" PRIX32 "", session.license->serial_number); cmd.access_key = session.license->access_key; diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index d745e4a6..a13162f1 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -209,7 +209,7 @@ void ProxyServer::on_client_connect( parray client_key; random_data(server_key.data(), server_key.bytes()); random_data(client_key.data(), client_key.bytes()); - auto cmd = prepare_server_init_contents_bb(server_key, client_key); + auto cmd = prepare_server_init_contents_bb(server_key, client_key, false); session->channel.send(0x03, 0x00, &cmd, sizeof(cmd)); // TODO: Is this actually needed? // bufferevent_flush(session->bev.get(), EV_READ | EV_WRITE, BEV_FLUSH); @@ -265,7 +265,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 throw runtime_error("command is not 9D"); } const auto& cmd = check_size_t( - data, sizeof(C_Login_PC_9D), sizeof(C_LoginWithUnusedSpace_PC_9D)); + data, sizeof(C_Login_PC_9D), sizeof(C_LoginExtended_PC_9D)); license = session->server->state->license_manager->verify_pc( stoul(cmd.serial_number, nullptr, 16), cmd.access_key); sub_version = cmd.sub_version; @@ -278,7 +278,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 throw runtime_error("command is not 9E"); } const auto& cmd = check_size_t( - data, sizeof(C_Login_GC_9E), sizeof(C_LoginWithUnusedSpace_GC_9E)); + data, sizeof(C_Login_GC_9E), sizeof(C_LoginExtended_GC_9E)); license = session->server->state->license_manager->verify_gc( stoul(cmd.serial_number, nullptr, 16), cmd.access_key); sub_version = cmd.sub_version; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index f4b57da9..1bbf928e 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -79,7 +79,7 @@ void process_connect(std::shared_ptr s, std::shared_ptr c) } case ServerBehavior::LOGIN_SERVER: - send_server_init(s, c, true); + send_server_init(s, c, true, false); if (s->pre_lobby_event) { send_change_event(c, s->pre_lobby_event); } @@ -88,7 +88,7 @@ void process_connect(std::shared_ptr s, std::shared_ptr c) case ServerBehavior::DATA_SERVER_BB: case ServerBehavior::PATCH_SERVER: case ServerBehavior::LOBBY_SERVER: - send_server_init(s, c, false); + send_server_init(s, c, false, false); break; default: @@ -257,11 +257,11 @@ void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, const C_Login_PC_9D* base_cmd; if (command == 0x9D) { base_cmd = &check_size_t(data, - sizeof(C_Login_PC_9D), sizeof(C_Login_PC_9D) + 0x84); + sizeof(C_Login_PC_9D), sizeof(C_LoginExtended_PC_9D)); } else if (command == 0x9E) { const auto& cmd = check_size_t(data, - sizeof(C_Login_GC_9E), sizeof(C_Login_GC_9E) + 0x64); + sizeof(C_Login_GC_9E), sizeof(C_LoginExtended_GC_9E)); base_cmd = &cmd; try { @@ -391,7 +391,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); } @@ -722,20 +722,68 @@ void process_menu_item_info_request(shared_ptr s, shared_ptr s, shared_ptr c, - uint16_t, uint32_t, const string& data) { // 10 + uint16_t, uint32_t flags, const string& data) { // 10 bool uses_unicode = ((c->version == GameVersion::PC) || (c->version == GameVersion::BB)); - const auto& cmd = check_size_t(data, - sizeof(C_MenuSelection), sizeof(C_MenuSelection) + 0x10 * (1 + uses_unicode)); + uint32_t menu_id; + uint32_t item_id; + u16string password; + u16string unknown_a1; - switch (cmd.menu_id) { + if (flags == 0) { + const auto& cmd = check_size_t(data); + menu_id = cmd.menu_id; + item_id = cmd.item_id; + } else if (flags == 1) { + if (uses_unicode) { + const auto& cmd = check_size_t(data); + menu_id = cmd.menu_id; + item_id = cmd.item_id; + unknown_a1 = cmd.unknown_a1; + } else { + const auto& cmd = check_size_t(data); + menu_id = cmd.menu_id; + item_id = cmd.item_id; + unknown_a1 = decode_sjis(cmd.unknown_a1); + } + } else if (flags == 2) { + if (uses_unicode) { + const auto& cmd = check_size_t(data); + menu_id = cmd.menu_id; + item_id = cmd.item_id; + password = cmd.password; + } else { + const auto& cmd = check_size_t(data); + menu_id = cmd.menu_id; + item_id = cmd.item_id; + password = decode_sjis(cmd.password); + } + } else if (flags == 3) { + if (uses_unicode) { + const auto& cmd = check_size_t(data); + menu_id = cmd.menu_id; + item_id = cmd.item_id; + unknown_a1 = cmd.unknown_a1; + password = cmd.password; + } else { + const auto& cmd = check_size_t(data); + menu_id = cmd.menu_id; + item_id = cmd.item_id; + unknown_a1 = decode_sjis(cmd.unknown_a1); + password = decode_sjis(cmd.password); + } + } else { + throw runtime_error("invalid flag"); + } + + switch (menu_id) { case MenuID::MAIN: { - switch (cmd.item_id) { + switch (item_id) { case MainMenuItemID::GO_TO_LOBBY: { c->should_send_to_lobby_server = true; if (!(c->flags & Client::Flag::SAVE_ENABLED)) { - send_command(c, 0x97, 0x01); c->flags |= Client::Flag::SAVE_ENABLED; + send_command(c, 0x97, 0x01); send_update_client_config(c); } else { static const vector version_to_port_name({ @@ -802,13 +850,13 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } case MenuID::INFORMATION: { - if (cmd.item_id == InformationMenuItemID::GO_BACK) { + if (item_id == InformationMenuItemID::GO_BACK) { c->flags &= ~Client::Flag::IN_INFORMATION_MENU; send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { try { - send_message_box(c, s->information_contents->at(cmd.item_id).c_str()); + send_message_box(c, s->information_contents->at(item_id).c_str()); } catch (const out_of_range&) { send_message_box(c, u"$C6No such information exists."); } @@ -817,13 +865,13 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } case MenuID::PROXY_DESTINATIONS: { - if (cmd.item_id == ProxyDestinationsMenuItemID::GO_BACK) { + if (item_id == ProxyDestinationsMenuItemID::GO_BACK) { send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { const pair* dest = nullptr; try { - dest = &s->proxy_destinations_for_version(c->version).at(cmd.item_id); + dest = &s->proxy_destinations_for_version(c->version).at(item_id); } catch (const out_of_range&) { } if (!dest) { @@ -855,7 +903,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } case MenuID::GAME: { - auto game = s->find_lobby(cmd.item_id); + auto game = s->find_lobby(item_id); if (!game) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because it no\nlonger exists."); break; @@ -887,17 +935,6 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } if (!(c->license->privileges & Privilege::FREE_JOIN_GAMES)) { - ptext password; - if (data.size() > sizeof(C_MenuSelection)) { - if (uses_unicode) { - size_t max_chars = (data.size() - sizeof(C_MenuSelection)) / sizeof(char16_t); - password.assign(cmd.password.pcbb, max_chars); - } else { - size_t max_chars = (data.size() - sizeof(C_MenuSelection)) / sizeof(char); - password = decode_sjis(cmd.password.dcgc, max_chars); - } - } - if (!game->password.empty() && (password != game->password)) { send_message_box(c, u"$C6Incorrect password."); break; @@ -925,7 +962,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, shared_ptr l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr; auto quests = s->quest_index->filter(c->version, c->flags & Client::Flag::DCV1, - static_cast(cmd.item_id & 0xFF)); + static_cast(item_id & 0xFF)); if (quests.empty()) { send_lobby_message_box(c, u"$C6There are no quests\navailable in that\ncategory."); break; @@ -942,7 +979,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, send_lobby_message_box(c, u"$C6Quests are not available."); break; } - auto q = s->quest_index->get(c->version, cmd.item_id); + auto q = s->quest_index->get(c->version, item_id); if (!q) { send_lobby_message_box(c, u"$C6Quest does not exist."); break; @@ -1022,7 +1059,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } case MenuID::PATCHES: - if (cmd.item_id == PatchesMenuItemID::GO_BACK) { + if (item_id == PatchesMenuItemID::GO_BACK) { send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { @@ -1031,13 +1068,13 @@ void process_menu_selection(shared_ptr s, shared_ptr c, } send_function_call( - c, s->function_code_index->menu_item_id_to_patch_function.at(cmd.item_id)); + c, s->function_code_index->menu_item_id_to_patch_function.at(item_id)); send_menu(c, u"Patches", MenuID::PATCHES, s->function_code_index->patch_menu()); } break; case MenuID::PROGRAMS: - if (cmd.item_id == ProgramsMenuItemID::GO_BACK) { + if (item_id == ProgramsMenuItemID::GO_BACK) { send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { @@ -1045,7 +1082,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, throw runtime_error("client does not support send_function_call"); } - c->loading_dol_file = s->dol_file_index->item_id_to_file.at(cmd.item_id); + c->loading_dol_file = s->dol_file_index->item_id_to_file.at(item_id); // Send the first function call, which triggers the process of loading a // DOL file. This function call determines the necessary base address diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 115c37a8..35f43fc6 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -97,7 +97,7 @@ static const char* anti_copyright = "This server is in no way affiliated, sponso static const char* dc_port_map_copyright = "DreamCast Port Map. Copyright SEGA Enterprises. 1999"; static const char* dc_lobby_server_copyright = "DreamCast Lobby Server. Copyright SEGA Enterprises. 1999"; static const char* bb_game_server_copyright = "Phantasy Star Online Blue Burst Game Server. Copyright 1999-2004 SONICTEAM."; -// static const char* bb_pm_server_copyright = "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM."; +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_92_9B prepare_server_init_contents_dc_pc_gc( @@ -140,26 +140,32 @@ void send_server_init_dc_pc_gc(shared_ptr c, S_ServerInit_BB_03 prepare_server_init_contents_bb( const parray& server_key, - const parray& client_key) { + const parray& client_key, + bool use_secondary_message) { S_ServerInit_BB_03 cmd; - cmd.copyright = bb_game_server_copyright; + cmd.copyright = use_secondary_message ? bb_pm_server_copyright : bb_game_server_copyright; cmd.server_key = server_key; cmd.client_key = client_key; cmd.after_message = anti_copyright; return cmd; } -void send_server_init_bb(shared_ptr s, shared_ptr c) { +void send_server_init_bb(shared_ptr s, shared_ptr c, + bool use_secondary_message) { parray server_key; parray client_key; random_data(server_key.data(), server_key.bytes()); random_data(client_key.data(), client_key.bytes()); - auto cmd = prepare_server_init_contents_bb(server_key, client_key); - send_command_t(c, 0x03, 0x00, cmd); + auto cmd = prepare_server_init_contents_bb(server_key, client_key, use_secondary_message); + send_command_t(c, use_secondary_message ? 0x9B : 0x03, 0x00, cmd); - static const string expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8); + static const string primary_expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8); + static const string secondary_expected_first_data("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8); shared_ptr detector_crypt(new PSOBBMultiKeyDetectorEncryption( - s->bb_private_keys, expected_first_data, cmd.client_key.data(), sizeof(cmd.client_key))); + s->bb_private_keys, + use_secondary_message ? secondary_expected_first_data : primary_expected_first_data, + cmd.client_key.data(), + sizeof(cmd.client_key))); c->channel.crypt_in = detector_crypt; c->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption( detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true)); @@ -180,7 +186,7 @@ void send_server_init_patch(shared_ptr c) { } void send_server_init(shared_ptr s, shared_ptr c, - bool initial_connection) { + bool initial_connection, bool use_secondary_message) { switch (c->version) { case GameVersion::DC: case GameVersion::PC: @@ -191,7 +197,7 @@ void send_server_init(shared_ptr s, shared_ptr c, send_server_init_patch(c); break; case GameVersion::BB: - send_server_init_bb(s, c); + send_server_init_bb(s, c, use_secondary_message); break; default: throw logic_error("unimplemented versioned command"); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 41f26dc2..02ecea4e 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -102,9 +102,10 @@ S_ServerInit_DC_PC_GC_02_17_92_9B prepare_server_init_contents_dc_pc_gc( uint32_t client_key); S_ServerInit_BB_03 prepare_server_init_contents_bb( const parray& server_key, - const parray& client_key); + const parray& client_key, + bool use_secondary_message); void send_server_init(std::shared_ptr s, std::shared_ptr c, - bool initial_connection); + bool initial_connection, bool use_secondary_message); void send_update_client_config(std::shared_ptr c); void send_function_call(