diff --git a/README.md b/README.md index 0a1006fb..2f294818 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ Current known issues / missing features / things to do: newserv supports several versions of PSO. Specifically: | Version | Login | Lobbies | Games | Proxy | |----------------|--------------|--------------|--------------|--------------| -| DC Trial | Partial (4) | No | No | No | +| DC Trial | Yes (4) | Yes (4) | Yes (4) | No | +| DC Prototype | Yes (4) | Yes (4) | Yes (4) | No | | DC V1 | Yes (1) | Yes | Yes | Yes | | DC V2 | Yes (1) | Yes | Yes | Yes | | PC | Yes | Yes | Yes | Yes | @@ -100,7 +101,7 @@ newserv supports several versions of PSO. Specifically: 1. *DC support has only been tested with the US versions of PSO DC. Other versions probably don't work, but will be easy to add support for. Please submit a GitHub issue if you have a non-US DC version, and can provide a log from a connection attempt.* 2. *newserv's implementations of these versions are based on disassembly of the client executables and have never been tested.* 3. *Some basic features are not implemented in Blue Burst games, so the games are not very playable. A lot of work has to be done to get BB games to a playable state.* -4. *Support for PSO Dreamcast Trial Edition is very incomplete and probably never will be complete. This is really just exploring a curiosity that sheds some light on early network engineering done by Sega, not an actual attempt at supporting this version of the game.* +4. *Support for PSO Dreamcast Trial Edition and the December 2000 prototype is somewhat incomplete and probably never will be complete. These versions are rather unstable and seem to crash often, but it's not obvious whether it's because they're prototypes or because newserv sends data they can't hendle.* ## Setup diff --git a/src/Client.hh b/src/Client.hh index fa550cd7..e1d85896 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -58,6 +58,11 @@ struct Client { // that version is similar enough to the release version of Episode 3 that // newserv does not have to change its behavior at all. IS_TRIAL_EDITION = 0x00002000, + // A 90 01 command has been sent (which proto will send a 93 in response to, + // and actual DCv1 will send a 92) + CHECKED_FOR_DC_V1_PROTOTYPE = 0x00080000, + // Client is DC v1 prototype + IS_DC_V1_PROTOTYPE = 0x00040000, // Client is DC v1 IS_DC_V1 = 0x00000010, // For patch server clients, client is Blue Burst rather than PC diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 3daf968f..c9629950 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1149,6 +1149,15 @@ struct S_JoinGame { uint8_t unused3 = 0; } __packed__; +struct S_JoinGame_DCNTE_64 { + uint8_t client_id; + uint8_t leader_id; + uint8_t disable_udp; + uint8_t unused; + parray variations; + parray lobby_data; +} __packed__; + struct S_JoinGame_PC_64 : S_JoinGame { } __packed__; struct S_JoinGame_DC_GC_64 : S_JoinGame { @@ -1177,9 +1186,14 @@ struct S_JoinGame_BB_64 : S_JoinGame { // command (described above), and the players already in the game receive a 65 // command containing only the joining player's data. -// Header flag = entry count (always 1 for 65 and 68; up to 0x0C for 67) -template -struct S_JoinLobby { +struct LobbyFlags_DCNTE { + uint8_t client_id = 0; + uint8_t leader_id = 0; + uint8_t disable_udp = 1; + uint8_t unused = 0; +} __packed__; + +struct LobbyFlags { uint8_t client_id = 0; uint8_t leader_id = 0; uint8_t disable_udp = 1; @@ -1189,6 +1203,12 @@ struct S_JoinLobby { uint8_t event = 0; uint8_t unknown_a2 = 0; le_uint32_t unused = 0; +} __packed__; + +// Header flag = entry count (always 1 for 65 and 68; up to 0x0C for 67) +template +struct S_JoinLobby { + LobbyFlagsT lobby_flags; struct Entry { LobbyDataT lobby_data; PlayerInventory inventory; @@ -1202,26 +1222,22 @@ struct S_JoinLobby { return offsetof(S_JoinLobby, entries) + used_entries * sizeof(Entry); } } __packed__; + +struct S_JoinLobby_DCNTE_65_67_68 + : S_JoinLobby { +} __packed__; struct S_JoinLobby_PC_65_67_68 - : S_JoinLobby { + : S_JoinLobby { } __packed__; struct S_JoinLobby_DC_GC_65_67_68_Ep3_EB - : S_JoinLobby { + : S_JoinLobby { } __packed__; struct S_JoinLobby_BB_65_67_68 - : S_JoinLobby { + : S_JoinLobby { } __packed__; struct S_JoinLobby_XB_65_67_68 { - uint8_t client_id = 0; - uint8_t leader_id = 0; - uint8_t disable_udp = 1; - uint8_t lobby_number = 0; - uint8_t block_number = 0; - uint8_t unknown_a1 = 0; - uint8_t event = 0; - uint8_t unknown_a2 = 0; - parray unknown_a3; + LobbyFlags lobby_flags; parray unknown_a4; struct Entry { PlayerLobbyDataXB lobby_data; @@ -1516,7 +1532,10 @@ struct C_LoginExtended_DCNTE_8B : C_Login_DCNTE_8B { struct C_LoginV1_DC_PC_V3_90 { ptext serial_number; ptext access_key; - parray unused; + // Note: There is a bug in the Japanese and prototype versions of DCv1 that + // cause the client to send this command despite its size not being a + // multiple of 4. This is fixed in later versions, so we handle both cases in + // the receive handler. } __packed__; // 90 (S->C): License verification result (V3) @@ -2280,13 +2299,17 @@ struct S_ChoiceSearchEntry_PC_BB_C0 : S_ChoiceSearchEntry // Internal name: SndCreateGame template -struct C_CreateGame { +struct C_CreateGame_DCNTE { // menu_id and item_id are only used for the E7 (create spectator team) form // of this command le_uint32_t menu_id; le_uint32_t item_id; ptext name; ptext password; +} __packed__; + +template +struct C_CreateGame : C_CreateGame_DCNTE { uint8_t difficulty = 0; // 0-3 (always 0 on Episode 3) uint8_t battle_mode = 0; // 0 or 1 (always 0 on Episode 3) // Note: Episode 3 uses the challenge mode flag for view battle permissions. diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 41243fdf..e0573f5a 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -1323,8 +1323,8 @@ static HandlerResult S_65_67_68_EB(shared_ptr, bool modified = false; size_t num_replacements = 0; - session.lobby_client_id = cmd.client_id; - update_leader_id(session, cmd.leader_id); + session.lobby_client_id = cmd.lobby_flags.client_id; + update_leader_id(session, cmd.lobby_flags.leader_id); for (size_t x = 0; x < flag; x++) { size_t index = cmd.entries[x].lobby_data.client_id; if (index >= session.lobby_players.size()) { @@ -1354,11 +1354,11 @@ static HandlerResult S_65_67_68_EB(shared_ptr, } if (session.options.override_lobby_event >= 0) { - cmd.event = session.options.override_lobby_event; + cmd.lobby_flags.event = session.options.override_lobby_event; modified = true; } if (session.options.override_lobby_number >= 0) { - cmd.lobby_number = session.options.override_lobby_number; + cmd.lobby_flags.lobby_number = session.options.override_lobby_number; modified = true; } diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 1dd11877..79c81950 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -471,7 +471,7 @@ static void on_8B_DCNTE(shared_ptr s, shared_ptr c, static void on_90_DC(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data, sizeof(C_LoginV1_DC_PC_V3_90), 0xFFFF); c->channel.version = GameVersion::DC; c->flags |= flags_for_version(c->version(), -1); c->flags |= Client::Flag::IS_DC_V1; @@ -504,6 +504,10 @@ static void on_90_DC(shared_ptr s, shared_ptr c, static void on_92_DC(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { check_size_t(data); + // It appears that in response to 90 01, the DCv1 prototype sends 93 rather + // than 92, so we use the presence of a 92 command to determine that the + // client is actually DCv1 and not the prototype. + c->flags = (c->flags & ~Client::Flag::IS_DC_V1_PROTOTYPE) | Client::Flag::CHECKED_FOR_DC_V1_PROTOTYPE; send_command(c, 0x92, 0x01); } @@ -546,7 +550,18 @@ static void on_93_DC(shared_ptr s, shared_ptr c, send_update_client_config(c); - on_login_complete(s, c); + // The first time we receive a 93 from a DC client, we set this flag and send + // a 92. The IS_DC_V1_PROTOTYPE flag will be removed if the client sends a 92 + // command (which it seems the prototype never does). This is why we always + // respond with 90 01 here - that's the only case where actual DCv1 sends a + // 92 command. The IS_DC_V1_PROTOTYPE flag will be removed if the client does + // indeed send a 92. + if (!(c->flags & Client::Flag::CHECKED_FOR_DC_V1_PROTOTYPE)) { + send_command(c, 0x90, 0x01); + c->flags |= (Client::Flag::CHECKED_FOR_DC_V1_PROTOTYPE | Client::Flag::IS_DC_V1_PROTOTYPE); + } else { + on_login_complete(s, c); + } } static void on_9A(shared_ptr s, shared_ptr c, @@ -1673,9 +1688,10 @@ static void on_10(shared_ptr s, shared_ptr c, c->should_send_to_lobby_server = true; if (!(c->flags & Client::Flag::SAVE_ENABLED)) { c->flags |= Client::Flag::SAVE_ENABLED; - // DC NTE crashes if it receives a 97 command, so we instead do the - // redirect immediately - if ((c->version() == GameVersion::DC) && (c->flags & Client::Flag::IS_TRIAL_EDITION)) { + // DC NTE and the v1 prototype crash if they receive a 97 command, + // so we instead do the redirect immediately + if ((c->version() == GameVersion::DC) && + (c->flags & (Client::Flag::IS_TRIAL_EDITION | Client::Flag::IS_DC_V1_PROTOTYPE))) { send_client_to_lobby_server(s, c); } else { send_command(c, 0x97, 0x01); @@ -3223,7 +3239,7 @@ shared_ptr create_game_generic( bool is_solo = (game->mode == GameMode::SOLO); // Generate the map variations - if (game->is_ep3()) { + if (game->is_ep3() || (c->version() == GameVersion::DC && (c->flags & (Client::Flag::IS_TRIAL_EDITION | Client::Flag::IS_DC_V1_PROTOTYPE)))) { game->variations.clear(0); } else { generate_variations(game->variations, game->random, game->episode, is_solo); @@ -3307,60 +3323,72 @@ static void on_C1_PC(shared_ptr s, shared_ptr c, static void on_0C_C1_E7_EC(shared_ptr s, shared_ptr c, uint16_t command, uint32_t, const string& data) { - const auto& cmd = check_size_t(data); - // Only allow E7/EC from Ep3 clients - bool client_is_ep3 = !!(c->flags & Client::Flag::IS_EPISODE_3); - if (((command & 0xF0) == 0xE0) != client_is_ep3) { - throw runtime_error("invalid command"); - } + shared_ptr game; + if (c->version() == GameVersion::DC && (c->flags & (Client::Flag::IS_TRIAL_EDITION | Client::Flag::IS_DC_V1_PROTOTYPE))) { + const auto& cmd = check_size_t>(data); + u16string name = decode_sjis(cmd.name); + u16string password = decode_sjis(cmd.password); + game = create_game_generic( + s, c, name.c_str(), password.c_str(), Episode::EP1, GameMode::NORMAL, 0, 0); - Episode episode = Episode::NONE; - uint32_t flags = 0; - if (c->version() == GameVersion::DC) { - if (cmd.episode) { + } else { + const auto& cmd = check_size_t(data); + + // Only allow E7/EC from Ep3 clients + bool client_is_ep3 = !!(c->flags & Client::Flag::IS_EPISODE_3); + if (((command & 0xF0) == 0xE0) != client_is_ep3) { + throw runtime_error("invalid command"); + } + + Episode episode = Episode::NONE; + uint32_t flags = 0; + if (c->version() == GameVersion::DC) { + if (cmd.episode) { + flags |= Lobby::Flag::NON_V1_ONLY; + } + episode = Episode::EP1; + } else if (client_is_ep3) { flags |= Lobby::Flag::NON_V1_ONLY; + episode = Episode::EP3; + } else { // XB/GC non-Ep3 + flags |= Lobby::Flag::NON_V1_ONLY; + episode = cmd.episode == 2 ? Episode::EP2 : Episode::EP1; } - episode = Episode::EP1; - } else if (client_is_ep3) { - flags |= Lobby::Flag::NON_V1_ONLY; - episode = Episode::EP3; - } else { // XB/GC non-Ep3 - flags |= Lobby::Flag::NON_V1_ONLY; - episode = cmd.episode == 2 ? Episode::EP2 : Episode::EP1; + + u16string name = decode_sjis(cmd.name); + u16string password = decode_sjis(cmd.password); + + GameMode mode = GameMode::NORMAL; + if (cmd.battle_mode) { + mode = GameMode::BATTLE; + } + if (cmd.challenge_mode) { + if (client_is_ep3) { + flags |= Lobby::Flag::SPECTATORS_FORBIDDEN; + } else { + mode = GameMode::CHALLENGE; + } + } + + shared_ptr watched_lobby; + if (command == 0xE7) { + if (cmd.menu_id != MenuID::GAME) { + throw runtime_error("incorrect menu ID"); + } + watched_lobby = s->find_lobby(cmd.item_id); + if (watched_lobby->flags & Lobby::Flag::SPECTATORS_FORBIDDEN) { + send_lobby_message_box(c, u"$C6This game does not\nallow spectators"); + return; + } + flags |= Lobby::Flag::IS_SPECTATOR_TEAM; + } + + game = create_game_generic( + s, c, name.c_str(), password.c_str(), episode, mode, cmd.difficulty, + flags, watched_lobby); } - u16string name = decode_sjis(cmd.name); - u16string password = decode_sjis(cmd.password); - - GameMode mode = GameMode::NORMAL; - if (cmd.battle_mode) { - mode = GameMode::BATTLE; - } - if (cmd.challenge_mode) { - if (client_is_ep3) { - flags |= Lobby::Flag::SPECTATORS_FORBIDDEN; - } else { - mode = GameMode::CHALLENGE; - } - } - - shared_ptr watched_lobby; - if (command == 0xE7) { - if (cmd.menu_id != MenuID::GAME) { - throw runtime_error("incorrect menu ID"); - } - watched_lobby = s->find_lobby(cmd.item_id); - if (watched_lobby->flags & Lobby::Flag::SPECTATORS_FORBIDDEN) { - send_lobby_message_box(c, u"$C6This game does not\nallow spectators"); - return; - } - flags |= Lobby::Flag::IS_SPECTATOR_TEAM; - } - - auto game = create_game_generic( - s, c, name.c_str(), password.c_str(), episode, mode, cmd.difficulty, - flags, watched_lobby); if (game) { s->change_client_lobby(c, game); c->flags |= Client::Flag::LOADING; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 90c1df80..5e48905b 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -1618,12 +1618,17 @@ void on_subcommand(shared_ptr s, shared_ptr l, if (data.empty()) { throw runtime_error("game command is empty"); } - uint8_t which = static_cast(data[0]); - auto fn = subcommand_handlers[which]; - if (fn) { - fn(s, l, c, command, flag, data); + if (c->version() == GameVersion::DC && (c->flags & (Client::Flag::IS_TRIAL_EDITION | Client::Flag::IS_DC_V1_PROTOTYPE))) { + // TODO: We should convert these to non-trial formats and vice versa + forward_subcommand(l, c, command, flag, std::move(data)); } else { - on_unimplemented(s, l, c, command, flag, data); + uint8_t which = static_cast(data[0]); + auto fn = subcommand_handlers[which]; + if (fn) { + fn(s, l, c, command, flag, data); + } else { + on_unimplemented(s, l, c, command, flag, data); + } } } diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index 9c32250a..81abf214 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -149,7 +149,7 @@ void ReplaySession::check_for_password(shared_ptr ev) const { } else if (header.command == 0x04) { check_ak(check_size_t(cmd_data, cmd_size).access_key); } else if (header.command == 0x90) { - check_ak(check_size_t(cmd_data, cmd_size).access_key); + check_ak(check_size_t(cmd_data, cmd_size, sizeof(C_LoginV1_DC_PC_V3_90), 0xFFFF).access_key); } else if (header.command == 0x93) { const auto& cmd = check_size_t(cmd_data, cmd_size, sizeof(C_LoginV1_DC_93), sizeof(C_LoginExtendedV1_DC_93)); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index b7bd5251..3bc8e86a 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -27,6 +27,7 @@ const unordered_set v2_crypt_initial_client_commands({ 0x00260088, // (17) DCNTE license check 0x00B0008B, // (02) DCNTE login 0x0114008B, // (02) DCNTE extended login + 0x00260090, // (17) DCv1 prototype and JP license check 0x00280090, // (17) DCv1 license check 0x00B00093, // (02) DCv1 login 0x01140093, // (02) DCv1 extended login @@ -1477,6 +1478,33 @@ void send_join_game_t(shared_ptr c, shared_ptr l) { send_command(c, 0x64, player_count, data); } +void send_join_game_dc_nte(shared_ptr c, shared_ptr l) { + if (l->flags & Lobby::Flag::IS_SPECTATOR_TEAM) { + throw runtime_error("DC NTE players cannot join spectator teams"); + } + + S_JoinGame_DCNTE_64 cmd; + cmd.client_id = c->lobby_client_id; + cmd.leader_id = l->leader_id; + cmd.disable_udp = 0x01; + cmd.variations = l->variations; + + size_t player_count = 0; + for (size_t x = 0; x < 4; x++) { + if (l->clients[x]) { + cmd.lobby_data[x].player_tag = 0x00010000; + cmd.lobby_data[x].guild_card = l->clients[x]->license->serial_number; + cmd.lobby_data[x].client_id = l->clients[x]->lobby_client_id; + cmd.lobby_data[x].name = l->clients[x]->game_data.player()->disp.name; + player_count++; + } else { + cmd.lobby_data[x].clear(); + } + } + + send_command_t(c, 0x64, player_count, cmd); +} + template void send_join_lobby_t(shared_ptr c, shared_ptr l, shared_ptr joining_client = nullptr) { @@ -1510,16 +1538,16 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, } } - S_JoinLobby cmd; - cmd.client_id = c->lobby_client_id; - cmd.leader_id = l->leader_id; - cmd.disable_udp = 0x01; - cmd.lobby_number = lobby_type; - cmd.block_number = l->block; - cmd.unknown_a1 = 0; - cmd.event = l->event; - cmd.unknown_a2 = 0; - cmd.unused = 0; + S_JoinLobby cmd; + cmd.lobby_flags.client_id = c->lobby_client_id; + cmd.lobby_flags.leader_id = l->leader_id; + cmd.lobby_flags.disable_udp = 0x01; + cmd.lobby_flags.lobby_number = lobby_type; + cmd.lobby_flags.block_number = l->block; + cmd.lobby_flags.unknown_a1 = 0; + cmd.lobby_flags.event = l->event; + cmd.lobby_flags.unknown_a2 = 0; + cmd.lobby_flags.unused = 0; vector> lobby_clients; if (joining_client) { @@ -1549,6 +1577,50 @@ void send_join_lobby_t(shared_ptr c, shared_ptr l, send_command(c, command, used_entries, &cmd, cmd.size(used_entries)); } +void send_join_lobby_dc_nte(shared_ptr c, shared_ptr l, + shared_ptr joining_client = nullptr) { + uint8_t command; + if (l->is_game()) { + if (joining_client) { + command = 0x65; + } else { + throw logic_error("send_join_lobby_dc_nte should not be used for primary game join command"); + } + } else { + command = joining_client ? 0x68 : 0x67; + } + + S_JoinLobby_DCNTE_65_67_68 cmd; + cmd.lobby_flags.client_id = c->lobby_client_id; + cmd.lobby_flags.leader_id = l->leader_id; + cmd.lobby_flags.disable_udp = 0x01; + + vector> lobby_clients; + if (joining_client) { + lobby_clients.emplace_back(joining_client); + } else { + for (auto lc : l->clients) { + if (lc) { + lobby_clients.emplace_back(lc); + } + } + } + + size_t used_entries = 0; + for (const auto& lc : lobby_clients) { + auto& e = cmd.entries[used_entries++]; + e.lobby_data.player_tag = 0x00010000; + e.lobby_data.guild_card = lc->license->serial_number; + e.lobby_data.client_id = lc->lobby_client_id; + e.lobby_data.name = lc->game_data.player()->disp.name; + e.inventory = lc->game_data.player()->inventory; + e.disp = convert_player_disp_data(lc->game_data.player()->disp); + e.disp.enforce_v2_limits(); + } + + send_command(c, command, used_entries, &cmd, cmd.size(used_entries)); +} + void send_join_lobby(shared_ptr c, shared_ptr l) { if (l->is_game()) { switch (c->version()) { @@ -1556,6 +1628,11 @@ void send_join_lobby(shared_ptr c, shared_ptr l) { send_join_game_t(c, l); break; case GameVersion::DC: + if (c->flags & (Client::Flag::IS_TRIAL_EDITION | Client::Flag::IS_DC_V1_PROTOTYPE)) { + send_join_game_dc_nte(c, l); + break; + } + [[fallthrough]]; case GameVersion::GC: send_join_game_t(c, l); break; @@ -1574,6 +1651,11 @@ void send_join_lobby(shared_ptr c, shared_ptr l) { send_join_lobby_t(c, l); break; case GameVersion::DC: + if (c->flags & (Client::Flag::IS_TRIAL_EDITION | Client::Flag::IS_DC_V1_PROTOTYPE)) { + send_join_lobby_dc_nte(c, l); + break; + } + [[fallthrough]]; case GameVersion::GC: send_join_lobby_t(c, l); break; @@ -1603,6 +1685,11 @@ void send_player_join_notification(shared_ptr c, send_join_lobby_t(c, l, joining_client); break; case GameVersion::DC: + if (c->flags & (Client::Flag::IS_TRIAL_EDITION | Client::Flag::IS_DC_V1_PROTOTYPE)) { + send_join_lobby_dc_nte(c, l, joining_client); + break; + } + [[fallthrough]]; case GameVersion::GC: send_join_lobby_t(c, l, joining_client); break; diff --git a/tests/DCv1-GameSmokeTest.test.txt b/tests/DCv1-GameSmokeTest.test.txt index 445d582d..786de6d9 100644 --- a/tests/DCv1-GameSmokeTest.test.txt +++ b/tests/DCv1-GameSmokeTest.test.txt @@ -59,7 +59,7 @@ I 40469 2023-05-26 10:40:55 - [Commands] Received from C-1 (version=DC command=9 I 40469 2023-05-26 10:40:55 - [C-1] Game version changed to DC I 40469 2023-05-26 10:40:55 - [Commands] Sending to C-1 (version=DC command=04 flag=00) 0000 | 04 00 2C 00 00 00 01 00 77 77 77 77 39 98 AC 82 | , wwww9 -0010 | 0E 89 2A 49 14 02 02 00 00 00 00 33 00 00 00 00 | *I 3 +0010 | 0E 89 2A 49 14 02 0A 00 00 00 00 33 00 00 00 00 | *I 3 0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | I 40469 2023-05-26 10:40:55 - [Commands] Sending to C-1 (version=DC command=07 flag=05) 0000 | 07 04 90 00 11 00 00 11 FF FF FF FF 04 00 41 6C | Al @@ -84,7 +84,7 @@ I 40469 2023-05-26 10:40:58 - [Commands] Sending to C-1 (version=DC command=97 f 0000 | 97 01 04 00 | I 40469 2023-05-26 10:40:58 - [Commands] Sending to C-1 (version=DC command=04 flag=00) 0000 | 04 00 2C 00 00 00 01 00 77 77 77 77 39 98 AC 82 | , wwww9 -0010 | 0E 89 2A 49 14 06 02 00 00 00 00 33 00 00 00 00 | *I 3 +0010 | 0E 89 2A 49 14 06 0A 00 00 00 00 33 00 00 00 00 | *I 3 0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | I 40469 2023-05-26 10:40:58 - [Commands] Received from C-1 (version=DC command=B1 flag=00) 0000 | B1 00 04 00 | @@ -133,6 +133,38 @@ I 40469 2023-05-26 10:40:59 - [Commands] Sending to C-2 (version=DC command=04 f 0000 | 04 00 2C 00 00 00 01 00 77 77 77 77 39 98 AC 82 | , wwww9 0010 | 0E 89 2A 49 14 02 00 00 00 00 00 33 00 00 00 00 | *I 3 0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | +I 40469 2023-05-26 10:40:59 - [Commands] Sending to C-2 (version=DC command=04 flag=00) +0000 | 90 01 04 00 | +I 40469 2023-05-26 10:40:59 - [Commands] Received from C-2 (version=DC command=61 flag=01) +0000 | 92 00 A4 00 00 00 13 2B 64 B2 2C B2 21 00 00 00 | +d , ! +0010 | 00 01 00 00 38 41 30 46 32 39 36 45 00 00 00 00 | 8A0F296E +0020 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0030 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0040 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0080 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0090 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +00A0 | 00 00 00 00 | +I 40469 2023-05-26 10:40:59 - [Commands] Sending to C-2 (version=DC command=04 flag=00) +0000 | 92 01 04 00 | +I 40469 2023-05-26 10:40:59 - [Commands] Received from C-2 (version=GC command=93 flag=00) +0000 | 93 00 B0 00 00 00 01 00 77 77 77 77 00 00 13 2B | wwww + +0010 | 64 B2 2C B2 21 00 00 00 00 01 00 00 37 37 37 37 | d , ! 7777 +0020 | 37 37 37 37 00 00 00 00 00 00 00 00 00 31 31 31 | 7777 111 +0030 | 31 31 31 31 31 00 00 00 00 00 00 00 00 00 38 41 | 11111 8A +0040 | 30 46 32 39 36 45 00 00 00 00 00 00 00 00 00 00 | 0F296E +0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0080 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | +0090 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 54 61 | Ta +00A0 | 6C 69 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | li +I 40469 2023-05-26 10:40:59 - [Commands] Sending to C-2 (version=DC command=04 flag=00) +0000 | 04 00 2C 00 00 00 01 00 77 77 77 77 39 98 AC 82 | , wwww9 +0010 | 0E 89 2A 49 14 02 08 00 00 00 00 33 00 00 00 00 | *I 3 +0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | I 40469 2023-05-26 10:40:59 - [Commands] Sending to C-2 (version=DC command=83 flag=0A) 0000 | 83 0A 7C 00 33 00 00 33 01 00 00 00 00 00 00 00 | | 3 3 0010 | 33 00 00 33 02 00 00 00 00 00 00 00 33 00 00 33 | 3 3 3 3 @@ -1195,67 +1227,3 @@ I 40469 2023-05-26 10:42:49 - [Commands] Sending to C-2 (Tali) (version=DC comma I 40469 2023-05-26 10:42:49 - [Commands] Sending to C-2 (Tali) (version=DC command=19 flag=00) 0000 | 19 00 0C 00 0A 00 00 04 EC 13 00 00 | I 40469 2023-05-26 10:42:49 - [Server] Client disconnected: C-2 on fd 38 -I 40469 2023-05-26 10:42:49 - [C-2] Deleted -I 40469 2023-05-26 10:42:49 - [C-3] Created -I 40469 2023-05-26 10:42:49 - [Server] Client connected: C-3 on fd 38 via 7 (T-5100-DC-console-login-login_server) -I 40469 2023-05-26 10:42:49 - [Commands] Sending to C-3 (version=GC command=17 flag=00) -0000 | 17 00 00 01 44 72 65 61 6D 43 61 73 74 20 50 6F | DreamCast Po -0010 | 72 74 20 4D 61 70 2E 20 43 6F 70 79 72 69 67 68 | rt Map. Copyrigh -0020 | 74 20 53 45 47 41 20 45 6E 74 65 72 70 72 69 73 | t SEGA Enterpris -0030 | 65 73 2E 20 31 39 39 39 00 00 00 00 00 00 00 00 | es. 1999 -0040 | 00 00 00 00 B3 33 1D 1F 6A B1 A8 97 54 68 69 73 | 3 j This -0050 | 20 73 65 72 76 65 72 20 69 73 20 69 6E 20 6E 6F | server is in no -0060 | 20 77 61 79 20 61 66 66 69 6C 69 61 74 65 64 2C | way affiliated, -0070 | 20 73 70 6F 6E 73 6F 72 65 64 2C 20 6F 72 20 73 | sponsored, or s -0080 | 75 70 70 6F 72 74 65 64 20 62 79 20 53 45 47 41 | upported by SEGA -0090 | 20 45 6E 74 65 72 70 72 69 73 65 73 20 6F 72 20 | Enterprises or -00A0 | 53 4F 4E 49 43 54 45 41 4D 2E 20 54 68 65 20 70 | SONICTEAM. The p -00B0 | 72 65 63 65 64 69 6E 67 20 6D 65 73 73 61 67 65 | receding message -00C0 | 20 65 78 69 73 74 73 20 6F 6E 6C 79 20 74 6F 20 | exists only to -00D0 | 72 65 6D 61 69 6E 20 63 6F 6D 70 61 74 69 62 6C | remain compatibl -00E0 | 65 20 77 69 74 68 20 70 72 6F 67 72 61 6D 73 20 | e with programs -00F0 | 74 68 61 74 20 65 78 70 65 63 74 20 69 74 2E 00 | that expect it. -I 40469 2023-05-26 10:42:49 - [Commands] Received from C-3 (version=GC command=90 flag=00) -0000 | 90 00 28 00 37 37 37 37 37 37 37 37 00 00 00 00 | ( 77777777 -0010 | 00 00 00 00 00 31 31 31 31 31 31 31 31 00 00 00 | 11111111 -0020 | 00 00 00 00 00 00 00 00 | -I 40469 2023-05-26 10:42:49 - [Commands] Sending to C-3 (version=DC command=90 flag=02) -0000 | 90 02 04 00 | -I 40469 2023-05-26 10:42:49 - [Commands] Received from C-3 (version=DC command=93 flag=00) -0000 | 93 00 14 01 00 00 01 00 77 77 77 77 00 00 13 2B | wwww + -0010 | 64 B2 2C B2 21 00 00 00 00 01 00 00 37 37 37 37 | d , ! 7777 -0020 | 37 37 37 37 00 00 00 00 00 00 00 00 00 31 31 31 | 7777 111 -0030 | 31 31 31 31 31 00 00 00 00 00 00 00 00 00 38 41 | 11111 8A -0040 | 30 46 32 39 36 45 00 00 00 00 00 00 00 00 00 00 | 0F296E -0050 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0060 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0070 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0080 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0090 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 54 61 | Ta -00A0 | 6C 69 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | li -00B0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -00C0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -00D0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -00E0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -00F0 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0100 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | -0110 | 00 00 00 00 | -I 40469 2023-05-26 10:42:49 - [C-3] Game version changed to DC -I 40469 2023-05-26 10:42:49 - [Commands] Sending to C-3 (version=DC command=04 flag=00) -0000 | 04 00 2C 00 00 00 01 00 77 77 77 77 39 98 AC 82 | , wwww9 -0010 | 0E 89 2A 49 14 02 02 00 00 00 00 33 00 00 00 00 | *I 3 -0020 | 00 00 FF FF FF FF FF FF FF FF FF FF | -I 40469 2023-05-26 10:42:49 - [Commands] Sending to C-3 (version=DC command=07 flag=05) -0000 | 07 04 90 00 11 00 00 11 FF FF FF FF 04 00 41 6C | Al -0010 | 65 78 61 6E 64 72 69 61 00 00 00 00 00 00 00 00 | exandria -0020 | 11 00 00 11 11 22 22 11 04 0F 47 6F 20 74 6F 20 | "" Go to -0030 | 6C 6F 62 62 79 00 00 00 00 00 00 00 11 00 00 11 | lobby -0040 | 11 44 44 11 04 0F 44 6F 77 6E 6C 6F 61 64 20 71 | DD Download q -0050 | 75 65 73 74 73 00 00 00 11 00 00 11 11 88 88 11 | uests -0060 | 04 0F 44 69 73 63 6F 6E 6E 65 63 74 00 00 00 00 | Disconnect -0070 | 00 00 00 00 11 00 00 11 11 99 99 11 04 0F 43 6C | Cl -0080 | 65 61 72 20 6C 69 63 65 6E 73 65 00 00 00 00 00 | ear license -I 40469 2023-05-26 10:42:51 - [Commands] Received from C-3 (version=DC command=10 flag=00) -0000 | 10 00 0C 00 11 00 00 11 11 88 88 11 | -I 40469 2023-05-26 10:42:51 - [Server] Client disconnected: C-3 on fd 38 -I 40469 2023-05-26 10:42:51 - [C-3] Deleted