add some undocumented client commands from PC, GC and BB

This commit is contained in:
Martin Michelsen
2022-06-26 11:41:25 -07:00
parent cf8dd69edc
commit 47f97f357f
6 changed files with 481 additions and 132 deletions
+382 -79
View File
@@ -277,6 +277,20 @@ struct S_ServerInit_DC_PC_GC_02_17_92_9B {
ptext<char, 0xC0> 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<uint8_t, 4> 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<char, 0x10> serial_number2;
ptext<char, 0x10> 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<char, 0x10> serial_number;
ptext<char, 0x10> access_key;
};
struct C_LegacyLogin_BB_04 {
parray<le_uint32_t, 3> unknown_a1;
ptext<char, 0x10> username;
ptext<char, 0x10> 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 <typename CharT>
struct C_MenuSelection_10_Flag01 {
le_uint32_t menu_id;
le_uint32_t item_id;
ptext<CharT, 0x10> unknown_a1;
};
struct C_MenuSelection_DC_GC_10_Flag01 : C_MenuSelection_10_Flag01<char> { };
struct C_MenuSelection_PC_BB_10_Flag01 : C_MenuSelection_10_Flag01<char16_t> { };
template <typename CharT>
struct C_MenuSelection_10_Flag02 {
le_uint32_t menu_id;
le_uint32_t item_id;
ptext<CharT, 0x10> password;
};
struct C_MenuSelection_DC_GC_10_Flag02 : C_MenuSelection_10_Flag02<char> { };
struct C_MenuSelection_PC_BB_10_Flag02 : C_MenuSelection_10_Flag02<char16_t> { };
template <typename CharT>
struct C_MenuSelection_10_Flag03 {
le_uint32_t menu_id;
le_uint32_t item_id;
ptext<CharT, 0x10> unknown_a1;
ptext<CharT, 0x10> password;
};
struct C_MenuSelection_DC_GC_10_Flag03 : C_MenuSelection_10_Flag03<char> { };
struct C_MenuSelection_PC_BB_10_Flag03 : C_MenuSelection_10_Flag03<char16_t> { };
// 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<uint8_t, 0x10> data;
// Command 0022 is a 16-byte challenge (sent in the data field) using the
// following structure.
struct SC_GameCardCheck_BB_0022 {
parray<le_uint32_t, 4> 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<char, 0x11> serial_number;
ptext<char, 0x13> access_key;
ptext<char, 0x11> access_key;
parray<uint8_t, 2> 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<char, 0x20> unused;
ptext<char, 0x10> unused1;
ptext<char, 0x10> unused2;
ptext<char, 0x10> serial_number;
ptext<char, 0x10> 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<char, 0x30> serial_number2;
ptext<char, 0x30> access_key2;
ptext<char, 0x30> 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<char, 8> unused;
le_uint64_t unused;
le_uint32_t sub_version;
le_uint32_t unused2;
ptext<char, 0x30> serial_number;
@@ -1103,6 +1171,14 @@ struct C_Register_DC_PC_GC_9C {
ptext<char, 0x30> 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<char, 0x30> username;
ptext<char, 0x30> password;
ptext<char, 0x30> 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<uint8_t, 0x24> 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<uint8_t, 0x2> unused3; // Always zeroes?
ptext<char, 0x10> unused1; // Same as unused1/unused2 in 9A
ptext<char, 0x10> unused2;
ptext<char, 0x10> serial_number;
ptext<char, 0x10> access_key;
ptext<char, 0x30> serial_number2;
ptext<char, 0x30> access_key2;
ptext<char, 0x10> name;
};
struct C_LoginWithUnusedSpace_PC_9D : C_Login_PC_9D {
parray<uint8_t, 0x84> unused_space;
struct C_LoginExtended_PC_9D : C_Login_PC_9D {
parray<uint8_t, 0x84> 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<uint8_t, 0x64> unused_space;
struct C_LoginExtended_GC_9E : C_Login_GC_9E {
parray<uint8_t, 0x64> 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<char, 0x10> unknown_a3; // Always blank?
ptext<char, 0x10> unknown_a4; // == "?"
ptext<char, 0x10> unknown_a5; // Always blank?
ptext<char, 0x10> unknown_a6; // Always blank?
ptext<char, 0x30> username;
ptext<char, 0x30> password;
ptext<char, 0x10> guild_card_number_str;
parray<le_uint32_t, 10> unknown_a7;
parray<uint8_t, 0x84> 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<uint8_t, 0x10> unknown; // all zeroes
parray<uint8_t, 0x10> 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<uint8_t, 0x14> unknown_a3;
parray<le_uint32_t, 5> 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<char16_t> { };
template <typename CharT>
struct C_CreateGame {
le_uint64_t unused;
parray<le_uint32_t, 2> unused;
ptext<CharT, 0x10> name;
ptext<CharT, 0x10> 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<char> { };
struct C_CreateGame_PC_C1 : C_CreateGame<char16_t> { };
@@ -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<char, 0x30> 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<char, 0x10> unknown_a1;
ptext<char, 0x10> unknown_a2;
ptext<char, 0x10> unknown_a3;
ptext<char, 0x10> unknown_a4;
ptext<char, 0x30> unknown_a5;
ptext<char, 0x30> unknown_a6;
ptext<char, 0x30> unknown_a7;
// Note: These four fields are likely the same as those used in BB's 9E
ptext<char, 0x10> unknown_a3; // Always blank?
ptext<char, 0x10> unknown_a4; // == "?"
ptext<char, 0x10> unknown_a5; // Always blank?
ptext<char, 0x10> unknown_a6; // Always blank?
le_uint32_t sub_version;
ptext<char, 0x30> username;
ptext<char, 0x30> password;
ptext<char, 0x30> 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<char16_t, 0x0C> unknown_a2;
};
struct C_Unknown_BB_06DF {
parray<le_uint32_t, 3> unknown_a1;
};
struct C_Unknown_BB_07DF {
le_uint32_t unused1; // Always 0xFFFFFFFF
le_uint32_t unused2; // Always 0
parray<le_uint32_t, 5> 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<char, 0x10> name;
ptext<char, 0x10> 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<uint8_t, 0x0108> 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<char16_t, 0x5A> 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<char16_t, 0x10> 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<uint8_t, 0x800> 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<char16_t, 0x10> 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<uint8_t, 0x38> pad_config; // 05ED
parray<uint8_t, 0x28> tech_menu; // 06ED
parray<uint8_t, 0xE8> customize; // 07ED
parray<uint8_t, 0x140> 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<le_uint32_t, 9> 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 {
+7 -5
View File
@@ -117,7 +117,7 @@ static HandlerResult process_server_gc_9A(shared_ptr<ServerState>,
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<ServerState>,
}
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<ServerState>,
// 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<ServerState> 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;
+3 -3
View File
@@ -209,7 +209,7 @@ void ProxyServer::on_client_connect(
parray<uint8_t, 0x30> 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<C_Login_PC_9D>(
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<C_Login_GC_9E>(
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;
+70 -33
View File
@@ -79,7 +79,7 @@ void process_connect(std::shared_ptr<ServerState> s, std::shared_ptr<Client> 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<ServerState> s, std::shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> c,
const C_Login_PC_9D* base_cmd;
if (command == 0x9D) {
base_cmd = &check_size_t<C_Login_PC_9D>(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<C_Login_GC_9E>(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<ServerState>, shared_ptr<Client> c,
void process_client_checksum(shared_ptr<ServerState>, shared_ptr<Client> c,
uint16_t, uint32_t, const string& data) { // 96
check_size_t<C_ClientChecksum_GC_96>(data);
check_size_t<C_CharSaveInfo_GC_BB_96>(data);
send_server_time(c);
}
@@ -722,20 +722,68 @@ void process_menu_item_info_request(shared_ptr<ServerState> s, shared_ptr<Client
}
void process_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> 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<C_MenuSelection>(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<C_MenuSelection_10_Flag00>(data);
menu_id = cmd.menu_id;
item_id = cmd.item_id;
} else if (flags == 1) {
if (uses_unicode) {
const auto& cmd = check_size_t<C_MenuSelection_PC_BB_10_Flag01>(data);
menu_id = cmd.menu_id;
item_id = cmd.item_id;
unknown_a1 = cmd.unknown_a1;
} else {
const auto& cmd = check_size_t<C_MenuSelection_DC_GC_10_Flag01>(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<C_MenuSelection_PC_BB_10_Flag02>(data);
menu_id = cmd.menu_id;
item_id = cmd.item_id;
password = cmd.password;
} else {
const auto& cmd = check_size_t<C_MenuSelection_DC_GC_10_Flag02>(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<C_MenuSelection_PC_BB_10_Flag03>(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<C_MenuSelection_DC_GC_10_Flag03>(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<string> version_to_port_name({
@@ -802,13 +850,13 @@ void process_menu_selection(shared_ptr<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<string, uint16_t>* 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> c,
}
if (!(c->license->privileges & Privilege::FREE_JOIN_GAMES)) {
ptext<char16_t, 0x10> 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<ServerState> s, shared_ptr<Client> c,
shared_ptr<Lobby> 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<QuestCategory>(cmd.item_id & 0xFF));
static_cast<QuestCategory>(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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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
+16 -10
View File
@@ -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<Client> c,
S_ServerInit_BB_03 prepare_server_init_contents_bb(
const parray<uint8_t, 0x30>& server_key,
const parray<uint8_t, 0x30>& client_key) {
const parray<uint8_t, 0x30>& 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<ServerState> s, shared_ptr<Client> c) {
void send_server_init_bb(shared_ptr<ServerState> s, shared_ptr<Client> c,
bool use_secondary_message) {
parray<uint8_t, 0x30> server_key;
parray<uint8_t, 0x30> 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<PSOBBMultiKeyDetectorEncryption> 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<Client> c) {
}
void send_server_init(shared_ptr<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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");
+3 -2
View File
@@ -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<uint8_t, 0x30>& server_key,
const parray<uint8_t, 0x30>& client_key);
const parray<uint8_t, 0x30>& client_key,
bool use_secondary_message);
void send_server_init(std::shared_ptr<ServerState> s, std::shared_ptr<Client> c,
bool initial_connection);
bool initial_connection, bool use_secondary_message);
void send_update_client_config(std::shared_ptr<Client> c);
void send_function_call(