From 2ce9e581777414a4d73b9bbc617064d3474afa39 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 29 Oct 2022 19:27:44 -0700 Subject: [PATCH] add a bit more of dc nte --- README.md | 3 +- src/ChatCommands.cc | 2 +- src/CommandFormats.hh | 9 +++++- src/Menu.hh | 20 +++++++------ src/ReceiveCommands.cc | 34 +++++++++++++++++----- src/SendCommands.cc | 65 ++++++++++++++++++++++++++++++++---------- src/ServerState.cc | 9 ++++-- 7 files changed, 106 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 4a78f0d7..4db893ad 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Current known issues / missing features / things to do: newserv supports several versions of PSO. Specifically: | Version | Basic commands | Lobbies | Games | Proxy | |----------------------|----------------|---------------|---------------|---------------| -| Dreamcast Trial | Not supported | Not supported | Not supported | Not supported | +| Dreamcast Trial | Partial (6) | Not supported | Not supported | Not supported | | Dreamcast V1 | Supported (1) | Supported | Supported | Supported | | Dreamcast V2 | Supported (1) | Supported | Supported | Supported | | PC | Supported | Supported | Supported | Supported | @@ -65,6 +65,7 @@ newserv supports several versions of PSO. Specifically: 3. *Episode 3 players can download quests, join lobbies, create and join games, and trade cards, but CARD battles are not implemented yet. Tournaments are also not supported.* 4. *newserv's implementation of PSOX is based on disassembly of the client executable; it has never been tested with a real client and most likely doesn't work.* 5. *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.* +6. *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.* ## Usage diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index d8af0d43..07044b6e 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -610,7 +610,7 @@ static void server_command_convert_char_to_bb(shared_ptr s, // Request the player data. The client will respond with a 61, and the handler // for that command will execute the conversion - send_command(c, 0x95, 0x00); + send_get_player_info(c); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 7d46738e..90c96d4e 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1268,11 +1268,18 @@ struct C_Login_DCNTE_8B { ptext password; ptext name; parray unused; +}; + +struct C_LoginExtended_DCNTE_8B : C_Login_DCNTE_8B { SC_MeetUserExtension extension; }; // 8C: Invalid command -// 8D: Invalid command + +// 8D (S->C): Request player data (DC NTE only) +// Behaves the same as 95 (S->C) on all other versions. DC NTE crashes if it +// receives 95, so this is used instead. + // 8E: Invalid command // 8F: Invalid command diff --git a/src/Menu.hh b/src/Menu.hh index 607370e5..daa4255d 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -68,19 +68,23 @@ namespace ProxyOptionsMenuItemID { struct MenuItem { enum Flag { - INVISIBLE_ON_DC = 0x01, - INVISIBLE_ON_PC = 0x02, - INVISIBLE_ON_GC = 0x04, - INVISIBLE_ON_XB = 0x08, - INVISIBLE_ON_BB = 0x10, + // For menu items to be visible on DCNTE, they must not have either of the + // following two flags. (The INVISIBLE_ON_GCNTE flag behaves similarly.) + INVISIBLE_ON_DCNTE = 0x001, + INVISIBLE_ON_DC = 0x002, + INVISIBLE_ON_PC = 0x004, + INVISIBLE_ON_GC_TRIAL_EDITION = 0x008, + INVISIBLE_ON_GC = 0x010, + INVISIBLE_ON_XB = 0x020, + INVISIBLE_ON_BB = 0x040, DC_ONLY = INVISIBLE_ON_PC | INVISIBLE_ON_GC | INVISIBLE_ON_XB | INVISIBLE_ON_BB, PC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_GC | INVISIBLE_ON_XB | INVISIBLE_ON_BB, GC_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_XB | INVISIBLE_ON_BB, XB_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_GC | INVISIBLE_ON_BB, BB_ONLY = INVISIBLE_ON_DC | INVISIBLE_ON_PC | INVISIBLE_ON_GC | INVISIBLE_ON_XB, - REQUIRES_MESSAGE_BOXES = 0x20, - REQUIRES_SEND_FUNCTION_CALL = 0x40, - REQUIRES_SAVE_DISABLED = 0x80, + REQUIRES_MESSAGE_BOXES = 0x080, + REQUIRES_SEND_FUNCTION_CALL = 0x100, + REQUIRES_SAVE_DISABLED = 0x200, }; uint32_t item_id; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index ddd4318b..5dbea844 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -336,7 +336,8 @@ static void on_login_8_dcnte(shared_ptr s, shared_ptr c, static void on_login_b_dcnte(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 8B - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data, + sizeof(C_Login_DCNTE_8B), sizeof(C_LoginExtended_DCNTE_8B)); c->channel.version = GameVersion::DC; c->flags |= flags_for_version(c->version(), -1); c->flags |= Client::Flag::IS_DC_V1 | Client::Flag::IS_TRIAL_EDITION; @@ -346,7 +347,7 @@ static void on_login_b_dcnte(shared_ptr s, shared_ptr c, shared_ptr l = s->license_manager->verify_pc( serial_number, cmd.access_key); c->set_license(l); - send_command(c, 0x8B, 0x01); + // send_command(c, 0x8B, 0x01); } catch (const incorrect_access_key& e) { send_message_box(c, u"Incorrect access key"); @@ -361,9 +362,21 @@ static void on_login_b_dcnte(shared_ptr s, shared_ptr c, serial_number, cmd.access_key, true); s->license_manager->add(l); c->set_license(l); - send_command(c, 0x8B, 0x01); + // send_command(c, 0x8B, 0x01); } } + + if (cmd.is_extended) { + const auto& ext_cmd = check_size_t(data); + if (ext_cmd.extension.menu_id == MenuID::LOBBY) { + c->preferred_lobby_id = ext_cmd.extension.lobby_id; + } + } + + if (!c->should_disconnect) { + send_update_client_config(c); + on_login_complete(s, c); + } } static void on_login_0_dc_pc_v3(shared_ptr s, shared_ptr c, @@ -1188,8 +1201,14 @@ static void on_menu_selection(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; - send_command(c, 0x97, 0x01); - send_update_client_config(c); + // 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)) { + send_client_to_lobby_server(s, c); + } else { + send_command(c, 0x97, 0x01); + send_update_client_config(c); + } } else { send_client_to_lobby_server(s, c); } @@ -3178,7 +3197,7 @@ static on_command_t handlers[0x100][6] = { /* 88 */ {nullptr, on_login_8_dcnte, nullptr, on_login_8_dcnte, nullptr, nullptr, }, /* 88 */ /* 89 */ {nullptr, on_change_arrow_color, on_change_arrow_color, on_change_arrow_color, on_change_arrow_color, on_change_arrow_color, }, /* 89 */ /* 8A */ {nullptr, on_lobby_name_request, on_lobby_name_request, on_lobby_name_request, on_lobby_name_request, on_lobby_name_request, }, /* 8A */ - /* 8B */ {nullptr, on_login_b_dcnte, nullptr, nullptr, nullptr, nullptr, }, /* 8B */ + /* 8B */ {nullptr, on_login_b_dcnte, nullptr, on_login_b_dcnte, nullptr, nullptr, }, /* 8B */ /* 8C */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 8C */ /* 8D */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 8D */ /* 8E */ {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }, /* 8E */ @@ -3317,13 +3336,14 @@ static void check_unlicensed_command(GameVersion version, uint8_t command) { case GameVersion::XB: // See comment in the DC case above for why DC commands are included here. if (command != 0x88 && // DC NTE + command != 0x8B && // DC NTE command != 0x90 && // DC v1 command != 0x93 && // DC v1 command != 0x9A && // DC v2 command != 0x9D && // DC v2, GC trial edition command != 0x9E && // GC non-trial command != 0xDB) { // GC non-trial - throw runtime_error("only commands 90, 93, 9A, 9D, 9E, and DB may be sent before login"); + throw runtime_error("only commands 88, 8B, 90, 93, 9A, 9D, 9E, and DB may be sent before login"); } break; case GameVersion::BB: diff --git a/src/SendCommands.cc b/src/SendCommands.cc index f6b9ea63..ac680f68 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -23,6 +23,8 @@ using namespace std; const unordered_set v2_crypt_initial_client_commands({ 0x00260088, // (17) DCNTE license check + 0x00B0008B, // (02) DCNTE login + 0x0114008B, // (02) DCNTE extended login 0x00280090, // (17) DCv1 license check 0x00B00093, // (02) DCv1 login 0x01140093, // (02) DCv1 extended login @@ -881,21 +883,49 @@ void send_menu_t( } for (const auto& item : items) { - if (((c->version() == GameVersion::DC) && (item.flags & MenuItem::Flag::INVISIBLE_ON_DC)) || - ((c->version() == GameVersion::PC) && (item.flags & MenuItem::Flag::INVISIBLE_ON_PC)) || - ((c->version() == GameVersion::GC) && (item.flags & MenuItem::Flag::INVISIBLE_ON_GC)) || - ((c->version() == GameVersion::XB) && (item.flags & MenuItem::Flag::INVISIBLE_ON_XB)) || - ((c->version() == GameVersion::BB) && (item.flags & MenuItem::Flag::INVISIBLE_ON_BB)) || - ((item.flags & MenuItem::Flag::REQUIRES_MESSAGE_BOXES) && (c->flags & Client::Flag::NO_D6)) || - ((item.flags & MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL) && (c->flags & Client::Flag::NO_SEND_FUNCTION_CALL)) || - ((item.flags & MenuItem::Flag::REQUIRES_SAVE_DISABLED) && (c->flags & Client::Flag::SAVE_ENABLED))) { - continue; + bool is_visible = true; + switch (c->version()) { + case GameVersion::DC: + is_visible &= !(item.flags & MenuItem::Flag::INVISIBLE_ON_DC); + if (c->flags & Client::Flag::IS_TRIAL_EDITION) { + is_visible &= !(item.flags & MenuItem::Flag::INVISIBLE_ON_DCNTE); + } + break; + case GameVersion::PC: + is_visible &= !(item.flags & MenuItem::Flag::INVISIBLE_ON_PC); + break; + case GameVersion::GC: + is_visible &= !(item.flags & MenuItem::Flag::INVISIBLE_ON_GC); + if (c->flags & Client::Flag::IS_TRIAL_EDITION) { + is_visible &= !(item.flags & MenuItem::Flag::INVISIBLE_ON_GC_TRIAL_EDITION); + } + break; + case GameVersion::XB: + is_visible &= !(item.flags & MenuItem::Flag::INVISIBLE_ON_XB); + break; + case GameVersion::BB: + is_visible &= !(item.flags & MenuItem::Flag::INVISIBLE_ON_BB); + break; + default: + throw runtime_error("menus not supported for this game version"); + } + if (item.flags & MenuItem::Flag::REQUIRES_MESSAGE_BOXES) { + is_visible &= !(c->flags & Client::Flag::NO_D6); + } + if (item.flags & MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL) { + is_visible &= !(c->flags & Client::Flag::NO_SEND_FUNCTION_CALL); + } + if (item.flags & MenuItem::Flag::REQUIRES_SAVE_DISABLED) { + is_visible &= !(c->flags & Client::Flag::SAVE_ENABLED); + } + + if (is_visible) { + auto& e = entries.emplace_back(); + e.menu_id = menu_id; + e.item_id = item.item_id; + e.flags = (c->version() == GameVersion::BB) ? 0x0004 : 0x0F04; + e.text = item.name; } - auto& e = entries.emplace_back(); - e.menu_id = menu_id; - e.item_id = item.item_id; - e.flags = (c->version() == GameVersion::BB) ? 0x0004 : 0x0F04; - e.text = item.name; } send_command_vt(c, is_info_menu ? 0x1F : 0x07, entries.size() - 1, entries); @@ -1294,7 +1324,12 @@ void send_self_leave_notification(shared_ptr c) { } void send_get_player_info(shared_ptr c) { - send_command(c, 0x95, 0x00); + if ((c->version() == GameVersion::DC) && + (c->flags & Client::Flag::IS_TRIAL_EDITION)) { + send_command(c, 0x8D, 0x00); + } else { + send_command(c, 0x95, 0x00); + } } diff --git a/src/ServerState.cc b/src/ServerState.cc index 0e76f239..61d9aa84 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -406,8 +406,11 @@ void ServerState::create_menus(shared_ptr config_json) { this->main_menu.emplace_back(MainMenuItemID::GO_TO_LOBBY, u"Go to lobby", u"Join the lobby", 0); this->main_menu.emplace_back(MainMenuItemID::INFORMATION, u"Information", - u"View server\ninformation", MenuItem::Flag::REQUIRES_MESSAGE_BOXES); + u"View server\ninformation", MenuItem::Flag::INVISIBLE_ON_DCNTE | MenuItem::Flag::REQUIRES_MESSAGE_BOXES); uint32_t proxy_destinations_menu_item_flags = + // DCNTE doesn't support multiple ship select menus without changing + // servers (via a 19 command) apparently :( + MenuItem::Flag::INVISIBLE_ON_DCNTE | (this->proxy_destinations_dc.empty() ? MenuItem::Flag::INVISIBLE_ON_DC : 0) | (this->proxy_destinations_pc.empty() ? MenuItem::Flag::INVISIBLE_ON_PC : 0) | (this->proxy_destinations_gc.empty() ? MenuItem::Flag::INVISIBLE_ON_GC : 0) | @@ -416,7 +419,7 @@ void ServerState::create_menus(shared_ptr config_json) { this->main_menu.emplace_back(MainMenuItemID::PROXY_DESTINATIONS, u"Proxy server", u"Connect to another\nserver", proxy_destinations_menu_item_flags); this->main_menu.emplace_back(MainMenuItemID::DOWNLOAD_QUESTS, u"Download quests", - u"Download quests", MenuItem::Flag::INVISIBLE_ON_BB); + u"Download quests", MenuItem::Flag::INVISIBLE_ON_DCNTE | MenuItem::Flag::INVISIBLE_ON_BB); if (!this->function_code_index->patch_menu_empty()) { this->main_menu.emplace_back(MainMenuItemID::PATCHES, u"Patches", u"Change game\nbehaviors", MenuItem::Flag::GC_ONLY | MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); @@ -429,7 +432,7 @@ void ServerState::create_menus(shared_ptr config_json) { u"Disconnect", 0); this->main_menu.emplace_back(MainMenuItemID::CLEAR_LICENSE, u"Clear license", u"Disconnect with an\ninvalid license error\nso you can enter a\ndifferent serial\nnumber, access key,\nor password", - MenuItem::Flag::INVISIBLE_ON_BB); + MenuItem::Flag::INVISIBLE_ON_DCNTE | MenuItem::Flag::INVISIBLE_ON_BB); try { this->welcome_message = decode_sjis(d.at("WelcomeMessage")->as_string());