From 202427e33126ddd0ce8a5c889a6da7deb2ae85a9 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 13 Aug 2022 00:35:10 -0700 Subject: [PATCH] implement GC Ep1&2 trial edition --- src/Client.hh | 3 +++ src/CommandFormats.hh | 55 +++++++++++++++++++++++--------------- src/ProxyCommands.cc | 2 +- src/ProxyServer.cc | 4 +-- src/ReceiveCommands.cc | 35 +++++++++++++++++------- src/ReplaySession.cc | 4 +-- src/SendCommands.cc | 23 +++++++++------- src/Version.cc | 12 +++++++++ src/Version.hh | 2 ++ system/config.example.json | 9 ++++++- tests/config.json | 7 ++++- 11 files changed, 109 insertions(+), 47 deletions(-) diff --git a/src/Client.hh b/src/Client.hh index 26cfd465..daa22014 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -51,6 +51,9 @@ struct Client { ENCRYPTED_SEND_FUNCTION_CALL = 0x0800, // Client supports send_function_call but does not actually run the code SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x1000, + // Client is GC Trial Edition, and therefore uses V2 encryption instead of + // V3, and doesn't support some commands + GC_TRIAL_EDITION = 0x2000, }; uint64_t id; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 391cca3a..5e903202 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -376,8 +376,10 @@ struct C_LegacyLogin_BB_04 { // 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. +// The client config field in this command is ignored by pre-V3 clients as well +// as Episodes 1&2 Trial Edition. All other V3 clients save it as opaque data to +// be returned in a 9E or 9F command later. newserv sends the client config +// anyway to clients that 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, @@ -433,8 +435,8 @@ struct S_MenuEntry { le_uint16_t flags; // should be 0x0F04 ptext text; }; -struct S_MenuEntry_PC_BB_07_1F : S_MenuEntry { }; -struct S_MenuEntry_DC_V3_07_1F : S_MenuEntry { }; +struct S_MenuEntry_PC_BB_07_1F : S_MenuEntry { }; +struct S_MenuEntry_DC_V3_07_1F : S_MenuEntry { }; // 08 (C->S): Request game list // No arguments @@ -578,8 +580,9 @@ struct C_WriteFileConfirmation_V3_BB_13_A7 { // All commands after this command will be encrypted with PSO V2 encryption on // PC, or PSO V3 encryption 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. +// command in any online session, with the exception of Episodes 1&2 trial +// edition (which responds with a 9A). After the first time, V3 clients 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" @@ -1307,8 +1310,9 @@ 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 without client config (PC) -// Not used on V3 - the client sends 9E instead. +// 9D (C->S): Log in without client config (PC/GC) +// Not used on most versions of V3 - the client sends 9E instead. The one +// type of PSO V3 that uses 9D is the Trial Edition of Episodes 1&2. // 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 @@ -1322,7 +1326,7 @@ struct C_Login_MeetUserExtension { ptext target_player_name; }; -struct C_Login_PC_9D { +struct C_Login_PC_GC_9D { le_uint32_t player_tag; // 0x00010000 if guild card is set (via 04) le_uint32_t guild_card_number; // 0xFFFFFFFF if not set le_uint64_t unused; @@ -1338,7 +1342,7 @@ struct C_Login_PC_9D { ptext access_key2; // On XB, this is the XBL user ID ptext name; }; -struct C_LoginExtended_PC_9D : C_Login_PC_9D { +struct C_LoginExtended_PC_GC_9D : C_Login_PC_GC_9D { C_Login_MeetUserExtension extension; }; @@ -1346,7 +1350,7 @@ struct C_LoginExtended_PC_9D : C_Login_PC_9D { // 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 { +struct C_Login_GC_9E : C_Login_PC_GC_9D { union ClientConfigFields { ClientConfig cfg; parray data; @@ -1383,6 +1387,8 @@ struct C_LoginExtended_BB_9E { }; // 9F (S->C): Request client config / security data (V3/BB) +// This command is not valid on PSO GC Episodes 1&2 Trial Edition, nor any +// pre-V3 PSO versions. // No arguments // 9F (C->S): Client config / security data response (V3/BB) @@ -1476,6 +1482,8 @@ struct S_QuestMenuEntry_BB_A2_A4 : S_QuestMenuEntry { }; // specific to that quest. The structure here represents the only instance I've // seen so far. // The server will respond with an AB command. +// This command is likely never sent by PSO GC Episodes 1&2 Trial Edition, +// because the following command (AB) is definitely not valid on that version. struct C_UpdateQuestStatistics_AA { le_uint16_t quest_internal_id; @@ -1489,6 +1497,7 @@ struct C_UpdateQuestStatistics_AA { }; // AB (S->C): Confirm update quest statistics +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. // TODO: Does this command have a different meaning in Episode 3? Is it used at // all there, or is the handler an undeleted vestige from Episodes 1&2? @@ -1508,6 +1517,7 @@ struct S_ConfirmUpdateQuestStatistics_AB { // which starts the quest for all players at (approximately) the same time. // Sending this command to a GC client when it is not waiting to start a quest // will cause it to crash. +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. // AD: Invalid command // AE: Invalid command @@ -1939,6 +1949,7 @@ struct C_GBAGameRequest_V3_D7 { // D7 (S->C): Unknown (V3/BB) // No arguments +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. // 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 @@ -1950,6 +1961,7 @@ struct C_GBAGameRequest_V3_D7 { // The server should respond with a D8 command (described below). // D8 (S->C): Info board contents (V3/BB) +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. // 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. @@ -1967,6 +1979,7 @@ struct S_InfoBoardEntry_V3_D8 : S_InfoBoardEntry_D8 { }; // DA (S->C): Change lobby event (V3/BB) // header.flag = new event number; no other arguments. +// This command is not valid on PSO GC Episodes 1&2 Trial Edition. // DB (C->S): Verify license (V3/BB) // Server should respond with a 9A command. @@ -3007,10 +3020,10 @@ struct G_EnemyDropItemRequest_6x60 { // 68: Telepipe/Ryuker // 69: Unknown (supported; game only) // 6A: Unknown (supported; game only; not valid on Episode 3) -// 6B: Unknown (used while loading into game) -// 6C: Unknown (used while loading into game) -// 6D: Unknown (used while loading into game) -// 6E: Unknown (used while loading into game) +// 6B: Sync enemy state (used while loading into game) +// 6C: Sync object state (used while loading into game) +// 6D: Sync item state (used while loading into game) +// 6E: Sync flag state (used while loading into game) // 6F: Unknown (used while loading into game) // 70: Unknown (used while loading into game) // 71: Unknown (used while loading into game) @@ -3087,12 +3100,12 @@ struct G_BoxItemDropRequest_6xA2 { // AA: Episode 2 boss actions (not valid on PC or Episode 3) // AB: Create lobby chair (not valid on PC) // AC: Unknown (not valid on PC) -// AD: Unknown (not valid on PC or Episode 3) -// AE: Set chair state? (sent by existing clients at join time; not valid on PC) -// AF: Turn in lobby chair (not valid on PC) -// B0: Move in lobby chair (not valid on PC) -// B1: Unknown (not valid on PC) -// B2: Unknown (not valid on PC) +// AD: Unknown (not valid on PC, Episode 3, or GC Trial Edition) +// AE: Set chair state? (sent by existing clients at join time; not valid on PC or GC Trial Edition) +// AF: Turn in lobby chair (not valid on PC or GC Trial Edition) +// B0: Move in lobby chair (not valid on PC or GC Trial Edition) +// B1: Unknown (not valid on PC or GC Trial Edition) +// B2: Unknown (not valid on PC or GC Trial Edition) // B3: Unknown (Episode 3 only) // B4: Unknown (Episode 3 only) // B5: Episode 3 game setup menu state sync diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index e3f14380..95d54780 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -217,7 +217,7 @@ static HandlerResult process_server_pc_v3_patch_02_17(shared_ptr s, return HandlerResult::Type::SUPPRESS; } else if (session.version == GameVersion::PC) { - C_Login_PC_9D cmd; + C_Login_PC_GC_9D cmd; if (session.remote_guild_card_number == 0) { cmd.player_tag = 0xFFFF0000; cmd.guild_card_number = 0xFFFFFFFF; diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index dac375aa..db721310 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -269,8 +269,8 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 if (command != 0x9D) { throw runtime_error("command is not 9D"); } - const auto& cmd = check_size_t( - data, sizeof(C_Login_PC_9D), sizeof(C_LoginExtended_PC_9D)); + const auto& cmd = check_size_t( + data, sizeof(C_Login_PC_GC_9D), sizeof(C_LoginExtended_PC_GC_9D)); license = session->server->state->license_manager->verify_pc( 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 390ddbeb..2cd709b7 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -76,6 +76,9 @@ void process_connect(std::shared_ptr s, std::shared_ptr c) break; } + case ServerBehavior::LOGIN_SERVER_GC_TRIAL_EDITION: + c->flags |= Client::Flag::GC_TRIAL_EDITION; + [[fallthrough]]; case ServerBehavior::LOGIN_SERVER: send_server_init(s, c, true, false); if (s->pre_lobby_event) { @@ -85,6 +88,11 @@ void process_connect(std::shared_ptr s, std::shared_ptr c) case ServerBehavior::PATCH_SERVER_BB: c->flags |= Client::Flag::BB_PATCH; + send_server_init(s, c, false, false); + break; + + case ServerBehavior::LOBBY_SERVER_GC_TRIAL_EDITION: + c->flags |= Client::Flag::GC_TRIAL_EDITION; [[fallthrough]]; case ServerBehavior::PATCH_SERVER_PC: case ServerBehavior::DATA_SERVER_BB: @@ -319,12 +327,12 @@ void process_login_d_e_pc_v3(shared_ptr s, shared_ptr c, // The client sends extra unused data the first time it sends these commands, // hence the odd check_size calls here - const C_Login_PC_9D* base_cmd; + const C_Login_PC_GC_9D* base_cmd; if (command == 0x9D) { - base_cmd = &check_size_t(data, - sizeof(C_Login_PC_9D), sizeof(C_LoginExtended_PC_9D)); + base_cmd = &check_size_t(data, + sizeof(C_Login_PC_GC_9D), sizeof(C_LoginExtended_PC_GC_9D)); if (base_cmd->is_extended) { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); if (cmd.extension.menu_id == MenuID::LOBBY) { c->preferred_lobby_id = cmd.extension.preferred_lobby_id; } @@ -1119,12 +1127,15 @@ void process_menu_selection(shared_ptr s, shared_ptr c, send_quest_file(l->clients[x], dat_basename + ".dat", dat_basename, *dat_contents, QuestFileType::ONLINE); - // There is no such thing as command AC on PSO PC - quests just start - // immediately when they're done downloading. There are also no chunk - // acknowledgements (C->S 13 commands) like there are on GC. So, for - // PC clients, we can just not set the loading flag, since we never - // need to check/clear it later. - if (l->clients[x]->version != GameVersion::PC) { + // There is no such thing as command AC on PSO V2 - quests just start + // immediately when they're done downloading. (This is also the case + // on V3 Trial Edition.) There are also no chunk acknowledgements + // (C->S 13 commands) like there are on GC. So, for PC/Trial clients, + // we can just not set the loading flag, since we never need to + // check/clear it later. + if ((l->clients[x]->version != GameVersion::DC) && + (l->clients[x]->version != GameVersion::PC) && + !(l->clients[x]->flags & Client::Flag::GC_TRIAL_EDITION)) { l->clients[x]->flags |= Client::Flag::LOADING_QUEST; } } @@ -1407,6 +1418,10 @@ void process_update_quest_statistics(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // AA const auto& cmd = check_size_t(data); + if (c->flags & Client::Flag::GC_TRIAL_EDITION) { + throw runtime_error("trial edition client sent update quest stats command"); + } + auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game() || !l->loading_quest.get() || (l->loading_quest->internal_id != cmd.quest_internal_id)) { diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index 081df002..11ccade9 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -105,8 +105,8 @@ void ReplaySession::check_for_password(shared_ptr ev) const { check_ak(cmd.access_key); check_pw(cmd.password); } else if (header.command == 0x9D) { - const auto& cmd = check_size_t(cmd_data, cmd_size, - sizeof(C_Login_PC_9D), sizeof(C_LoginExtended_PC_9D)); + const auto& cmd = check_size_t(cmd_data, cmd_size, + sizeof(C_Login_PC_GC_9D), sizeof(C_LoginExtended_PC_GC_9D)); check_ak(cmd.access_key); check_ak(cmd.access_key2); } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 679c9f7e..b7381e9d 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -104,8 +104,7 @@ S_ServerInit_DC_PC_V3_02_17_91_9B prepare_server_init_contents_dc_pc_v3( return cmd; } -void send_server_init_dc_pc_v3(shared_ptr c, - bool initial_connection) { +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(); @@ -122,8 +121,13 @@ void send_server_init_dc_pc_v3(shared_ptr c, break; case GameVersion::GC: case GameVersion::XB: - c->channel.crypt_out.reset(new PSOV3Encryption(server_key)); - c->channel.crypt_in.reset(new PSOV3Encryption(client_key)); + if (c->flags & Client::Flag::GC_TRIAL_EDITION) { + c->channel.crypt_out.reset(new PSOV2Encryption(server_key)); + c->channel.crypt_in.reset(new PSOV2Encryption(client_key)); + } else { + 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"); @@ -1584,12 +1588,13 @@ 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 V3 - if ((c->version == GameVersion::GC) || - (c->version == GameVersion::XB) || - (c->version == GameVersion::BB)) { - send_command(c, 0xDA, new_event); + // This command isn't supported on versions before V3, nor on Trial Edition. + if ((c->version == GameVersion::DC) || + (c->version == GameVersion::PC) || + (c->flags & Client::Flag::GC_TRIAL_EDITION)) { + return; } + send_command(c, 0xDA, new_event); } void send_change_event(shared_ptr l, uint8_t new_event) { diff --git a/src/Version.cc b/src/Version.cc index 483a279f..0790398e 100644 --- a/src/Version.cc +++ b/src/Version.cc @@ -39,6 +39,10 @@ uint16_t flags_for_version(GameVersion version, int64_t sub_version) { return Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION | Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY; + // TODO: GC Ep1&2 trial edition uses sub_version 0x30, but also uses PSO V2 + // encryption! Find a way to tell that version apart from the others before + // starting encryption (hopefully it connects on a port not shared by other + // versions). 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 0x34: // GC Ep1&2 JP v1.03 @@ -114,8 +118,12 @@ const char* name_for_server_behavior(ServerBehavior behavior) { return "split_reconnect"; case ServerBehavior::LOGIN_SERVER: return "login_server"; + case ServerBehavior::LOGIN_SERVER_GC_TRIAL_EDITION: + return "login_server_gcte"; case ServerBehavior::LOBBY_SERVER: return "lobby_server"; + case ServerBehavior::LOBBY_SERVER_GC_TRIAL_EDITION: + return "lobby_server_gcte"; case ServerBehavior::DATA_SERVER_BB: return "data_server_bb"; case ServerBehavior::PATCH_SERVER_PC: @@ -134,8 +142,12 @@ ServerBehavior server_behavior_for_name(const char* name) { return ServerBehavior::SPLIT_RECONNECT; } else if (!strcasecmp(name, "login_server") || !strcasecmp(name, "login")) { return ServerBehavior::LOGIN_SERVER; + } else if (!strcasecmp(name, "login_server_gcte") || !strcasecmp(name, "login_gcte")) { + return ServerBehavior::LOGIN_SERVER_GC_TRIAL_EDITION; } else if (!strcasecmp(name, "lobby_server") || !strcasecmp(name, "lobby")) { return ServerBehavior::LOBBY_SERVER; + } else if (!strcasecmp(name, "lobby_server_gcte") || !strcasecmp(name, "lobby_gcte")) { + return ServerBehavior::LOBBY_SERVER_GC_TRIAL_EDITION; } else if (!strcasecmp(name, "data_server_bb") || !strcasecmp(name, "data_server") || !strcasecmp(name, "data")) { return ServerBehavior::DATA_SERVER_BB; } else if (!strcasecmp(name, "patch_server_pc") || !strcasecmp(name, "patch_pc")) { diff --git a/src/Version.hh b/src/Version.hh index 157afd78..da615a00 100644 --- a/src/Version.hh +++ b/src/Version.hh @@ -16,7 +16,9 @@ enum class GameVersion { enum class ServerBehavior { SPLIT_RECONNECT = 0, LOGIN_SERVER, + LOGIN_SERVER_GC_TRIAL_EDITION, LOBBY_SERVER, + LOBBY_SERVER_GC_TRIAL_EDITION, DATA_SERVER_BB, PATCH_SERVER_PC, PATCH_SERVER_BB, diff --git a/system/config.example.json b/system/config.example.json index 56b2e103..4e285d9a 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -23,14 +23,19 @@ // Various versions of PSO hardcode these ports in the clients. Don't change // these unless you don't want to support certain versions of PSO. + // TODO: GC Episodes 1&2 Trial Edition also uses port 9000, but a real + // version of PSO uses that port too. Figure out a way to differentiate + // between the two versions. "gc-jp10": [9000, "gc", "login_server"], "gc-jp11": [9001, "gc", "login_server"], - "gc-jpte": [9002, "gc", "login_server"], + "gc-jp3te": [9002, "gc", "login_server"], "gc-jp3": [9003, "gc", "login_server"], + "gc-us12t1": [9064, "gc", "login_server_gcte"], "gc-us10": [9100, "pc", "split_reconnect"], "gc-us3": [9103, "gc", "login_server"], "gc-eu10": [9200, "gc", "login_server"], "gc-eu11": [9201, "gc", "login_server"], + "gc-us12t2": [9202, "gc", "login_server_gcte"], "gc-eu3": [9203, "gc", "login_server"], "pc-login": [9300, "pc", "login_server"], "pc-patch": [10000, "patch", "patch_server_pc"], @@ -60,10 +65,12 @@ "gc-lobby": [9421, "gc", "lobby_server"], "bb-lobby": [9422, "bb", "lobby_server"], "xb-lobby": [9423, "xb", "lobby_server"], + "gct-lobby": [9424, "gc", "lobby_server_gcte"], "pc-proxy": [9520, "pc", "proxy_server"], "gc-proxy": [9521, "gc", "proxy_server"], "bb-proxy": [9522, "bb", "proxy_server"], "xb-proxy": [9523, "xb", "proxy_server"], + // TODO: Implement proxy server for GC Trial Edition "bb-data1": [12004, "bb", "data_server_bb"], "bb-data2": [12005, "bb", "data_server_bb"], }, diff --git a/tests/config.json b/tests/config.json index 5346585e..71431c6a 100644 --- a/tests/config.json +++ b/tests/config.json @@ -19,12 +19,14 @@ "PortConfiguration": { "gc-jp10": [9000, "gc", "login_server"], "gc-jp11": [9001, "gc", "login_server"], - "gc-jpte": [9002, "gc", "login_server"], + "gc-jp3te": [9002, "gc", "login_server"], "gc-jp3": [9003, "gc", "login_server"], + "gc-us12t1": [9064, "gc", "login_server_gcte"], "gc-us10": [9100, "pc", "split_reconnect"], "gc-us3": [9103, "gc", "login_server"], "gc-eu10": [9200, "gc", "login_server"], "gc-eu11": [9201, "gc", "login_server"], + "gc-us12t2": [9202, "gc", "login_server_gcte"], "gc-eu3": [9203, "gc", "login_server"], "pc-login": [9300, "pc", "login_server"], "pc-patch": [10000, "patch", "patch_server_pc"], @@ -38,9 +40,12 @@ "pc-lobby": [9420, "pc", "lobby_server"], "gc-lobby": [9421, "gc", "lobby_server"], "bb-lobby": [9422, "bb", "lobby_server"], + "xb-lobby": [9423, "xb", "lobby_server"], + "gct-lobby": [9424, "gc", "lobby_server_gcte"], "pc-proxy": [9520, "pc", "proxy_server"], "gc-proxy": [9521, "gc", "proxy_server"], "bb-proxy": [9522, "bb", "proxy_server"], + "xb-proxy": [9523, "xb", "proxy_server"], }, "ProxyDestinations-GC": {