From 8ef256917ceb702c91a480a0188aa1e26947af3d Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 7 May 2022 22:38:58 -0700 Subject: [PATCH] fix bb login, char creation, and some lobby/game behaviors --- README.md | 4 +- src/Client.cc | 12 ++- src/Client.hh | 10 +- src/CommandFormats.hh | 43 +++++--- src/FileContentsCache.cc | 15 ++- src/FileContentsCache.hh | 5 + src/Main.cc | 74 +++++++------ src/Player.cc | 22 +--- src/Player.hh | 4 +- src/ProxyCommands.cc | 50 ++++++--- src/ReceiveCommands.cc | 131 +++++++++++----------- src/ReceiveSubcommands.cc | 6 +- src/SendCommands.cc | 186 +++++++++++++++++--------------- src/SendCommands.hh | 19 ++-- src/Server.cc | 44 +++++--- src/Server.hh | 15 ++- src/Version.cc | 51 +++++++-- src/Version.hh | 12 +++ system/blueburst/streamfile.ind | Bin 684 -> 0 bytes system/config.example.json | 43 ++++++++ 20 files changed, 447 insertions(+), 299 deletions(-) delete mode 100755 system/blueburst/streamfile.ind diff --git a/README.md b/README.md index 5e67a8f2..8d89a501 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ newserv is a game server and proxy for Phantasy Star Online (PSO). This project includes code that was reverse-engineered by the community in ages long past, and has been included in many projects since then. It also includes some game data from Phantasy Star Online itself; this data was originally created by Sega. -This project is a rewrite of a rewrite of a game server that I wrote many years ago. So far, it works well with PSO GC Episodes 1 & 2, and lobbies (but not games) are implemented on Episode 3. Some basic functionality works on PSO PC, but there are probably still some cases that lead to errors (which will disconnect the client). newserv is based on an older project of mine that supported BB as well, but I no longer have a way to test BB, so the implementation here probably doesn't work for it. +This project is a rewrite of a rewrite of a game server that I wrote many years ago. So far, it works well with PSO GC Episodes 1 & 2, and lobbies (but not games) are implemented on Episode 3. Some basic functionality works on PSO PC and PSO BB, but there are probably still some cases that lead to errors (which will disconnect the client). Feel free to submit GitHub issues if you find bugs or have feature requests. I'd like to make the server as stable and complete as possible, but I can't promise that I'll respond to issues in a timely manner. @@ -15,7 +15,7 @@ This project is primarily for my own nostalgia; I offer no guarantees on how or Current known issues / missing features: - Test all the communication features (info board, simple mail, card search, etc.) - The trade window isn't implemented yet. -- PSO PC and PSOBB are essentially entirely untested. Only GC is fairly well-tested. +- PSO PC and PSOBB are not well-tested and likely will disconnect when clients try to use unimplemented features. Only GC is known to be stable and mostly complete. - Add all the chat commands that khyller used to have. (Most, but not all, currently exist in newserv.) ## Usage diff --git a/src/Client.cc b/src/Client.cc index cb615479..08111f64 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -69,8 +69,6 @@ void Client::set_license(shared_ptr l) { ClientConfig Client::export_config() const { ClientConfig cc; cc.magic = CLIENT_CONFIG_MAGIC; - cc.bb_game_state = this->bb_game_state; - cc.bb_player_index = this->bb_player_index; cc.flags = this->flags; cc.proxy_destination_address = this->proxy_destination_address; cc.proxy_destination_port = this->proxy_destination_port; @@ -81,6 +79,8 @@ ClientConfig Client::export_config() const { ClientConfigBB Client::export_config_bb() const { ClientConfigBB cc; cc.cfg = this->export_config(); + cc.bb_game_state = this->bb_game_state; + cc.bb_player_index = this->bb_player_index; cc.unused.clear(0xFF); return cc; } @@ -89,9 +89,13 @@ void Client::import_config(const ClientConfig& cc) { if (cc.magic != CLIENT_CONFIG_MAGIC) { throw invalid_argument("invalid client config"); } - this->bb_game_state = cc.bb_game_state; - this->bb_player_index = cc.bb_player_index; this->flags = cc.flags; this->proxy_destination_address = cc.proxy_destination_address; this->proxy_destination_port = cc.proxy_destination_port; } + +void Client::import_config(const ClientConfigBB& cc) { + this->import_config(cc.cfg); + this->bb_game_state = cc.bb_game_state; + this->bb_player_index = cc.bb_player_index; +} diff --git a/src/Client.hh b/src/Client.hh index 54b19b08..ca94c2bf 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -18,15 +18,6 @@ extern const uint64_t CLIENT_CONFIG_MAGIC; -enum class ServerBehavior { - SPLIT_RECONNECT = 0, - LOGIN_SERVER, - LOBBY_SERVER, - DATA_SERVER_BB, - PATCH_SERVER, - PROXY_SERVER, -}; - struct Client { enum Flag { // For patch server clients, client is Blue Burst rather than PC @@ -121,4 +112,5 @@ struct Client { ClientConfig export_config() const; ClientConfigBB export_config_bb() const; void import_config(const ClientConfig& cc); + void import_config(const ClientConfigBB& cc); }; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 7629b40d..48acf202 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -43,19 +43,36 @@ // technically part of the PSO protocol. Because it is opaque to the client, we // can use the server's native-endian types instead of being explicit as we do // for all the other structs in this file. +enum ClientStateBB : uint8_t { + // Initial connection; server will redirect client to another port + INITIAL_LOGIN = 0x00, + // Second connection; server will send client game data and account data + DOWNLOAD_DATA = 0x01, + // Third connection; client will show the choose character menu + CHOOSE_PLAYER = 0x02, + // Fourth connection; used for saving characters only. If you do not create a + // character, the server sets this state during the third connection so this + // connection is effectively skipped. + SAVE_PLAYER = 0x03, + // Fifth connection; redirects client to login server + SHIP_SELECT = 0x04, + // All other connections + IN_GAME = 0x05, +}; + struct ClientConfig { uint64_t magic; - uint8_t bb_game_state; - uint8_t bb_player_index; uint16_t flags; uint32_t proxy_destination_address; uint16_t proxy_destination_port; - parray unused; + parray unused; }; struct ClientConfigBB { ClientConfig cfg; - parray unused; + uint8_t bb_game_state; + uint8_t bb_player_index; + parray unused; }; @@ -1163,7 +1180,7 @@ struct C_VerifyLicense_GC_DB { // DC: Guild card data (BB) -struct C_GuildCardDataRequest_BB_DC { +struct C_GuildCardDataRequest_BB_03DC { le_uint32_t unknown; le_uint32_t chunk_index; le_uint32_t cont; @@ -1171,7 +1188,7 @@ struct C_GuildCardDataRequest_BB_DC { struct S_GuildCardHeader_BB_01DC { le_uint32_t unknown; // should be 1 - le_uint32_t filesize; // 0x0000490 + le_uint32_t filesize; // 0x0000D590 le_uint32_t checksum; }; @@ -1210,18 +1227,13 @@ struct S_TournamentEntry_GC_Ep3_E0 { // E2 (S->C): Team and key config (BB) // See KeyAndTeamConfigBB in Player.hh for format -// E3: Player previews (BB) +// E3: Player preview request (BB) struct C_PlayerPreviewRequest_BB_E3 { le_uint32_t player_index; le_uint32_t unused; }; -struct S_PlayerPreview_BB_E3 { - le_uint32_t player_index; - PlayerDispDataBBPreview preview; -}; - // E4: CARD lobby game (Episode 3) // When client sends an E4, server should respond with another E4 (but these // commands have different formats). @@ -1253,9 +1265,10 @@ struct S_PlayerPreview_NoPlayer_BB_E4 { le_uint32_t error; }; +// E5 (S->C): Player preview (BB) // E5 (C->S): Create character (BB) -struct C_CreateCharacter_BB_E5 { +struct SC_PlayerPreview_CreateCharacter_BB_E5 { le_uint32_t player_index; PlayerDispDataBBPreview preview; }; @@ -1293,8 +1306,8 @@ struct S_AcceptClientChecksum_BB_02E8 { // Command is a list of these; header.flag is the entry count. struct S_StreamFileIndexEntry_BB_01EB { le_uint32_t size; - le_uint32_t checksum; - le_uint32_t offset; + le_uint32_t checksum; // crc32 of file data + le_uint32_t offset; // offset in stream (== sum of all previous files' sizes) ptext filename; }; diff --git a/src/FileContentsCache.cc b/src/FileContentsCache.cc index eba140ff..39180dfc 100644 --- a/src/FileContentsCache.cc +++ b/src/FileContentsCache.cc @@ -12,7 +12,15 @@ FileContentsCache::File::File(const string& name, shared_ptr conte uint64_t load_time) : name(name), contents(contents), load_time(load_time) { } shared_ptr FileContentsCache::get(const std::string& name) { + return this->get(name, [name]() -> string { return load_file(name); }); +} +shared_ptr FileContentsCache::get(const char* name) { + return this->get(string(name)); +} + +shared_ptr FileContentsCache::get(const std::string& name, + std::function generate) { uint64_t t = now(); try { auto& entry = this->name_to_file.at(name); @@ -21,7 +29,7 @@ shared_ptr FileContentsCache::get(const std::string& name) { } } catch (const out_of_range& e) { } - shared_ptr contents(new string(load_file(name))); + shared_ptr contents(new string(generate())); this->name_to_file.erase(name); this->name_to_file.emplace(piecewise_construct, forward_as_tuple(name), forward_as_tuple(name, contents, t)); @@ -29,6 +37,7 @@ shared_ptr FileContentsCache::get(const std::string& name) { return contents; } -shared_ptr FileContentsCache::get(const char* name) { - return this->get(string(name)); +shared_ptr FileContentsCache::get(const char* name, + std::function generate) { + return this->get(string(name), generate); } diff --git a/src/FileContentsCache.hh b/src/FileContentsCache.hh index f505b003..10605a98 100644 --- a/src/FileContentsCache.hh +++ b/src/FileContentsCache.hh @@ -35,6 +35,11 @@ public: std::shared_ptr get(const std::string& name); std::shared_ptr get(const char* name); + std::shared_ptr get( + const std::string& name, std::function generate); + std::shared_ptr get( + const char* name, std::function generate); + private: std::unordered_map name_to_file; }; diff --git a/src/Main.cc b/src/Main.cc index 40e6447a..310b4a1b 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -30,35 +30,19 @@ bool use_terminal_colors = false; -static const vector default_port_to_behavior({ - {"gc-jp10", 9000, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-jp11", 9001, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-jp3", 9003, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-us10", 9100, GameVersion::PC, ServerBehavior::SPLIT_RECONNECT}, - {"gc-us3", 9103, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-eu10", 9200, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-eu11", 9201, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"gc-eu3", 9203, GameVersion::GC, ServerBehavior::LOGIN_SERVER}, - {"pc-login", 9300, GameVersion::PC, ServerBehavior::LOGIN_SERVER}, - {"pc-patch", 10000, GameVersion::PATCH, ServerBehavior::PATCH_SERVER}, - {"bb-patch", 11000, GameVersion::PATCH, ServerBehavior::PATCH_SERVER}, - {"bb-init", 12000, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, - {"bb-patch2", 13000, GameVersion::PATCH, ServerBehavior::PATCH_SERVER}, - {"bb-init2", 14000, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, - - // These aren't hardcoded in clients; user should be allowed to override them - {"bb-data1", 12004, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, - {"bb-data2", 12005, GameVersion::BB, ServerBehavior::DATA_SERVER_BB}, - {"bb-login", 12008, GameVersion::BB, ServerBehavior::LOGIN_SERVER}, - {"pc-lobby", 9420, GameVersion::PC, ServerBehavior::LOBBY_SERVER}, - {"gc-lobby", 9421, GameVersion::GC, ServerBehavior::LOBBY_SERVER}, - {"bb-lobby", 9422, GameVersion::BB, ServerBehavior::LOBBY_SERVER}, - {"pc-proxy", 9520, GameVersion::PC, ServerBehavior::PROXY_SERVER}, - {"gc-proxy", 9521, GameVersion::GC, ServerBehavior::PROXY_SERVER}, - {"bb-proxy", 9522, GameVersion::BB, ServerBehavior::PROXY_SERVER}, -}); - - +static vector parse_port_configuration( + shared_ptr json) { + vector ret; + for (const auto& item_json_it : json->as_dict()) { + auto item_list = item_json_it.second->as_list(); + PortConfiguration& pc = ret.emplace_back(); + pc.name = item_json_it.first; + pc.port = item_list[0]->as_int(); + pc.version = version_for_name(item_list[1]->as_string().c_str()); + pc.behavior = server_behavior_for_name(item_list[2]->as_string().c_str()); + } + return ret; +} template vector parse_int_vector(shared_ptr o) { @@ -88,8 +72,7 @@ void populate_state_from_config(shared_ptr s, } } catch (const out_of_range&) { } - // TODO: make this configurable - s->set_port_configuration(default_port_to_behavior); + s->set_port_configuration(parse_port_configuration(d.at("PortConfiguration"))); auto enemy_categories = parse_int_vector(d.at("CommonItemDropRates-Enemy")); auto box_categories = parse_int_vector(d.at("CommonItemDropRates-Box")); @@ -297,6 +280,17 @@ void drop_privileges(const string& username) { int main(int, char**) { + if (true) { + static const string seed("\x33\xF7\x40\xA2\xB4\xFD\x5C\x07\xD8\x94\x09\x9F\x8B\x76\x35\xF9\x97\x76\x8B\x16\x5C\x73\x9F\x2E\xF1\x1F\x1A\xC0\xB9\x53\xFE\x59\xE4\xDD\xC5\xC8\x11\xA0\x78\xD5\x56\x5A\xF7\xC3\x47\xD5\xCA\x67", 0x30); + static const string encrypted_data("\x83\x9A\xE7\xE1\xDD\xB2\x41\x38", 0x08); + string decrypted_data = encrypted_data; + auto private_key = load_object_file("system/blueburst/keys/default.nsk"); + PSOBBEncryption crypt(private_key, seed.data(), seed.size()); + crypt.decrypt(decrypted_data.data(), decrypted_data.size()); + fprintf(stderr, "decrypted\n"); + print_data(stderr, decrypted_data); + } + signal(SIGPIPE, SIG_IGN); if (isatty(fileno(stderr))) { @@ -344,7 +338,8 @@ int main(int, char**) { log(INFO, "Opening sockets"); for (const auto& it : state->name_to_port_config) { - if (it.second->behavior == ServerBehavior::PROXY_SERVER) { + const auto& pc = it.second; + if (pc->behavior == ServerBehavior::PROXY_SERVER) { if (!state->proxy_server.get()) { log(INFO, "Starting proxy server"); state->proxy_server.reset(new ProxyServer(base, state)); @@ -355,18 +350,18 @@ int main(int, char**) { // no way to ask the client which destination they want, so only one // destination is supported, and we have to manually specify the // destination netloc here. - if (it.second->version == GameVersion::PATCH) { + if (pc->version == GameVersion::PATCH) { struct sockaddr_storage ss = make_sockaddr_storage( state->proxy_destination_patch.first, state->proxy_destination_patch.second).first; - state->proxy_server->listen(it.second->port, it.second->version, &ss); - } if (it.second->version == GameVersion::BB) { + state->proxy_server->listen(pc->port, pc->version, &ss); + } else if (pc->version == GameVersion::BB) { struct sockaddr_storage ss = make_sockaddr_storage( state->proxy_destination_bb.first, state->proxy_destination_bb.second).first; - state->proxy_server->listen(it.second->port, it.second->version, &ss); + state->proxy_server->listen(pc->port, pc->version, &ss); } else { - state->proxy_server->listen(it.second->port, it.second->version); + state->proxy_server->listen(pc->port, pc->version); } } } else { @@ -374,7 +369,10 @@ int main(int, char**) { log(INFO, "Starting game server"); game_server.reset(new Server(base, state)); } - game_server->listen("", it.second->port, it.second->version, it.second->behavior); + string name = string_printf("%s (%s, %s) on port %hu", + pc->name.c_str(), name_for_version(pc->version), + name_for_server_behavior(pc->behavior), pc->port); + game_server->listen(name, "", pc->port, pc->version, pc->behavior); } } diff --git a/src/Player.cc b/src/Player.cc index 7d8b66d8..b8ea338c 100644 --- a/src/Player.cc +++ b/src/Player.cc @@ -284,23 +284,6 @@ void PlayerBank::save(const string& filename) const { //////////////////////////////////////////////////////////////////////////////// -uint32_t compute_guild_card_checksum(const void* vdata, size_t size) { - const uint8_t* data = reinterpret_cast(vdata); - - uint32_t cs = 0xFFFFFFFF; - for (size_t offset = 0; offset < size; offset++) { - cs ^= data[offset]; - for (size_t y = 0; y < 8; y++) { - if (!(cs & 1)) { - cs = (cs >> 1) & 0x7FFFFFFF; - } else { - cs = ((cs >> 1) & 0x7FFFFFFF) ^ 0xEDB88320; - } - } - } - return (cs ^ 0xFFFFFFFF); -} - ClientGameData::~ClientGameData() { if (!this->bb_username.empty()) { if (this->account_data.get()) { @@ -360,9 +343,6 @@ string ClientGameData::player_data_filename() const { if (this->bb_username.empty()) { throw logic_error("non-BB players do not have account data"); } - if (this->bb_player_index == 0) { - throw logic_error("0 is not a valid player index"); - } return string_printf("system/players/player_%s_%zu.nsc", this->bb_username.c_str(), this->bb_player_index + 1); } @@ -477,7 +457,7 @@ void ClientGameData::import_player(const PSOPlayerDataBB& bb) { } } -PlayerBB ClientGameData::export_player_bb() const { +PlayerBB ClientGameData::export_player_bb() { auto account = this->account(); auto player = this->player(); diff --git a/src/Player.hh b/src/Player.hh index 01ef6e9c..9dbcffe7 100644 --- a/src/Player.hh +++ b/src/Player.hh @@ -425,7 +425,9 @@ public: void import_player(const PSOPlayerDataPC& pd); void import_player(const PSOPlayerDataGC& pd); void import_player(const PSOPlayerDataBB& pd); - PlayerBB export_player_bb() const; + // Note: this function is not const because it can cause player and account + // data to be loaded + PlayerBB export_player_bb(); }; diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 43570824..abbb3292 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -142,9 +142,7 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr s, session.log(INFO, "Existing license in linked session"); // This isn't forwarded to the client, so don't recreate the client's crypts - if (session.version == GameVersion::PATCH) { - throw logic_error("patch session is indirect"); - } else if (session.version == GameVersion::PC) { + if ((session.version == GameVersion::PATCH) || (session.version == GameVersion::PC)) { session.server_input_crypt.reset(new PSOPCEncryption(cmd.server_key)); session.server_output_crypt.reset(new PSOPCEncryption(cmd.client_key)); } else if (session.version == GameVersion::GC) { @@ -154,10 +152,15 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr s, throw invalid_argument("unsupported version"); } - // Respond with an appropriate login command. We don't let the - // client do this because it believes it already did (when it was - // in an unlinked session). - if (session.version == GameVersion::PC) { + // Respond with an appropriate login command. We don't let the client do this + // because it believes it already did (when it was in an unlinked session, or + // in the patch server case, during the current session due to a hidden + // redirect). + if (session.version == GameVersion::PATCH) { + session.send_to_end(true, 0x02, 0x00); + return false; + + } else if (session.version == GameVersion::PC) { C_Login_PC_9D cmd; if (session.remote_guild_card_number == 0) { cmd.player_tag = 0xFFFF0000; @@ -390,8 +393,8 @@ static bool process_server_gc_E4(shared_ptr, return true; } -static bool process_server_19(shared_ptr, - ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { +static bool process_server_game_19_patch_14(shared_ptr, + ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) { // This weird maximum size is here to properly handle the version-split // command that some servers (including newserv) use on port 9100 auto& cmd = check_size_t(data, sizeof(S_Reconnect_19), 0xB0); @@ -404,6 +407,23 @@ static bool process_server_19(shared_ptr, if (!session.client_bev.get()) { session.log(WARNING, "Received reconnect command with no destination present"); + return false; + + } else if (command == 0x14) { + // On the patch server, hide redirects from the client completely. The new + // destination server will presumably send a new 02 command to start + // encryption; it appears that PSOBB doesn't fail if this happens, and + // simply re-initializes its encryption appropriately. + session.server_input_crypt.reset(); + session.server_output_crypt.reset(); + + struct sockaddr_in* dest_sin = reinterpret_cast( + &session.next_destination); + dest_sin->sin_family = AF_INET; + dest_sin->sin_addr.s_addr = cmd.address.load_raw(); + dest_sin->sin_port = cmd.port; + session.connect(); + return false; } else { // If the client is on a virtual connection (fd < 0), only change @@ -427,8 +447,8 @@ static bool process_server_19(shared_ptr, } else { cmd.port = session.local_port; } + return true; } - return true; } static bool process_server_gc_1A_D5(shared_ptr, @@ -806,7 +826,7 @@ auto defh = process_default; static process_command_t dc_server_handlers[0x100] = { /* 00 */ defh, defh, defh, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, process_server_19, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 40 */ defh, process_server_41, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -824,7 +844,7 @@ static process_command_t dc_server_handlers[0x100] = { }; static process_command_t pc_server_handlers[0x100] = { /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_19, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -842,7 +862,7 @@ static process_command_t pc_server_handlers[0x100] = { }; static process_command_t gc_server_handlers[0x100] = { /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_dc_pc_gc_04, defh, process_server_dc_pc_gc_06, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_19, process_server_gc_1A_D5, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, process_server_pc_gc_patch_02_17, defh, process_server_game_19_patch_14, process_server_gc_1A_D5, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -860,7 +880,7 @@ static process_command_t gc_server_handlers[0x100] = { }; static process_command_t bb_server_handlers[0x100] = { /* 00 */ defh, defh, defh, process_server_bb_03, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, process_server_19, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, process_server_13_A7, defh, defh, defh, defh, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 40 */ defh, process_server_41, defh, defh, process_server_44_A6, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, @@ -878,7 +898,7 @@ static process_command_t bb_server_handlers[0x100] = { }; static process_command_t patch_server_handlers[0x100] = { /* 00 */ defh, defh, process_server_pc_gc_patch_02_17, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, - /* 10 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, + /* 10 */ defh, defh, defh, defh, process_server_game_19_patch_14, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 20 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 30 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, /* 40 */ defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, defh, diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index dd436124..9f871a07 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -28,23 +28,6 @@ extern FileContentsCache file_cache; -enum ClientStateBB { - // Initial connection; server will redirect client to another port - INITIAL_LOGIN = 0x00, - // Second connection; server will send client game data and account data - DOWNLOAD_DATA = 0x01, - // Third connection; client will show the choose character menu - CHOOSE_PLAYER = 0x02, - // Fourth connection; used for saving characters only. If you do not create a - // character, the server sets this state during the third connection so this - // connection is effectively skipped. - SAVE_PLAYER = 0x03, - // Last connection; redirects client to login server - SHIP_SELECT = 0x04, -}; - - - vector quest_categories_menu({ MenuItem(static_cast(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0), MenuItem(static_cast(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0), @@ -103,9 +86,9 @@ void process_connect(std::shared_ptr s, std::shared_ptr c) } break; - case ServerBehavior::LOBBY_SERVER: case ServerBehavior::DATA_SERVER_BB: case ServerBehavior::PATCH_SERVER: + case ServerBehavior::LOBBY_SERVER: send_server_init(s, c, false); break; @@ -116,8 +99,11 @@ void process_connect(std::shared_ptr s, std::shared_ptr c) } void process_login_complete(shared_ptr s, shared_ptr c) { - if (c->server_behavior == ServerBehavior::LOGIN_SERVER) { - // on the login server, send the ep3 updates and the main menu or welcome + // On the BB data server, this function is called only on the last connection + // (when we should send ths ship select menu). + if ((c->server_behavior == ServerBehavior::LOGIN_SERVER) || + (c->server_behavior == ServerBehavior::DATA_SERVER_BB)) { + // On the login server, send the ep3 updates and the main menu or welcome // message if (c->flags & Client::Flag::EPISODE_3) { send_ep3_card_list_update(c); @@ -285,7 +271,7 @@ void process_login_d_e_pc_gc(shared_ptr s, shared_ptr c, // If we can't import the config, assume that the client was not connected // to newserv before, so we should show the welcome message. c->flags |= Client::Flag::AT_WELCOME_MESSAGE; - c->bb_game_state = 0; + c->bb_game_state = ClientStateBB::INITIAL_LOGIN; c->bb_player_index = 0; } @@ -342,10 +328,12 @@ void process_login_bb(shared_ptr s, shared_ptr c, } try { - c->import_config(cmd.client_config.cfg.cfg); - c->bb_game_state++; + c->import_config(cmd.client_config.cfg); + if (c->bb_game_state < ClientStateBB::IN_GAME) { + c->bb_game_state++; + } } catch (const invalid_argument&) { - c->bb_game_state = 0; + c->bb_game_state = ClientStateBB::INITIAL_LOGIN; c->bb_player_index = 0; } @@ -369,9 +357,11 @@ void process_login_bb(shared_ptr s, shared_ptr c, process_login_complete(s, c); break; + case ClientStateBB::IN_GAME: + break; + default: - send_reconnect(c, s->connect_address_for_client(c), - s->name_to_port_config.at("bb-login")->port); + throw runtime_error("invalid bb game state"); } } @@ -1213,14 +1203,15 @@ void process_player_preview_request_bb(shared_ptr, shared_ptrlicense->serial_number; temp_gd.bb_username = c->license->username; - temp_gd.bb_player_index = c->bb_player_index; + temp_gd.bb_player_index = cmd.player_index; try { auto preview = temp_gd.player()->disp.to_preview(); send_player_preview_bb(c, cmd.player_index, &preview); - } catch (const exception&) { + } catch (const exception& e) { // Player doesn't exist + log(INFO, "[BB debug] No player in slot: %s", e.what()); send_player_preview_bb(c, cmd.player_index, nullptr); } } @@ -1228,11 +1219,11 @@ void process_player_preview_request_bb(shared_ptr, shared_ptr, shared_ptr c, uint16_t command, uint32_t, const string& data) { - check_size_v(data.size(), 0); - if (command == 0x01E8) { + check_size_v(data.size(), 8); send_accept_client_checksum_bb(c); } else if (command == 0x03E8) { + check_size_v(data.size(), 0); send_guild_card_header_bb(c); } else { throw invalid_argument("unimplemented command"); @@ -1241,18 +1232,20 @@ void process_client_checksum_bb(shared_ptr, shared_ptr c, void process_guild_card_data_request_bb(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data); if (cmd.cont) { send_guild_card_chunk_bb(c, cmd.chunk_index); } } void process_stream_file_request_bb(shared_ptr, shared_ptr c, - uint16_t command, uint32_t, const string& data) { + uint16_t command, uint32_t flag, const string& data) { check_size_v(data.size(), 0); if (command == 0x04EB) { - send_stream_file_bb(c); + send_stream_file_index_bb(c); + } else if (command == 0x03EB) { + send_stream_file_chunk_bb(c, flag); } else { throw invalid_argument("unimplemented command"); } @@ -1260,14 +1253,25 @@ void process_stream_file_request_bb(shared_ptr, shared_ptr void process_create_character_bb(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); if (!c->license) { send_message_box(c, u"$C6You are not logged in."); return; } - if (!c->game_data.player()->disp.name.empty()) { - send_message_box(c, u"$C6You have already loaded a character."); + + // Hack: We use the security data to indicate to the server which phase the + // client is in (download data, character select, lobby, etc.). This presents + // a problem: the client expects to get an E4 (approve player choice) command + // immediately after the E5 (create character) command, but the client also + // will disconnect immediately after it receives that command. If we send an + // E6 before the E5 to update the security data (setting the correct next + // state), then the client sends ANOTHER E5, but this time it's blank! So, to + // be able to both create characters correctly and set security data + // correctly, we need to process only the first E5, and ignore the second. We + // do this by only creating a player if the current connection has no loaded + // player data. + if (c->game_data.player(false).get()) { return; } @@ -1470,6 +1474,8 @@ shared_ptr create_game_generic(shared_ptr s, {1, 1, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}; + // A player's actual level is their displayed level - 1, so the minimums for + // Episode 1 (for example) are actually 1, 20, 40, 80. static const uint32_t default_minimum_levels[3][4] = { {0, 19, 39, 79}, // episode 1 {0, 29, 49, 89}, // episode 2 @@ -1537,42 +1543,33 @@ shared_ptr create_game_generic(shared_ptr s, const auto* bp_subtable = s->battle_params->get_subtable(game->mode == 3, game->episode - 1, game->difficulty); - if (game->mode == 3) { - if (episode > 0 && episode < 4) { - variation_maxes = variation_maxes_solo[episode - 1]; - } - for (size_t x = 0; x < 0x10; x++) { - for (const char* type_char = "sm"; *type_char; type_char++) { - try { - auto filename = string_printf( - "system/blueburst/map/%c%hhu%zu%" PRIu32 "%" PRIu32 ".dat", - *type_char, game->episode, x, - game->variations.data()[x * 2].load(), - game->variations.data()[(x * 2) + 1].load()); - game->enemies = load_map(filename.c_str(), game->episode, - game->difficulty, bp_subtable, false); - break; - } catch (const exception& e) { } - } - if (game->enemies.empty()) { - throw runtime_error("failed to load any map data"); - } - } - } else { - if (episode > 0 && episode < 4) { - variation_maxes = variation_maxes_online[episode - 1]; - } - for (size_t x = 0; x < 0x10; x++) { + const char* type_chars = (game->mode == 3) ? "sm" : "m"; + if (episode > 0 && episode < 4) { + variation_maxes = (game->mode == 3) ? variation_maxes_solo[episode - 1] : variation_maxes_online[episode - 1]; + } + + for (size_t x = 0; x < 0x10; x++) { + for (const char* type = type_chars; *type; type++) { auto filename = string_printf( - "system/blueburst/map/m%hhu%zu%" PRIu32 "%" PRIu32 ".dat", - game->episode, x, + "system/blueburst/map/%c%hhX%zX%" PRIX32 "%" PRIX32 ".dat", + *type, game->episode, x, game->variations.data()[x * 2].load(), game->variations.data()[(x * 2) + 1].load()); - game->enemies = load_map(filename.c_str(), game->episode, - game->difficulty, bp_subtable, false); + try { + game->enemies = load_map(filename.c_str(), game->episode, + game->difficulty, bp_subtable, false); + log(INFO, "Loaded map %s", filename.c_str()); + break; + } catch (const exception& e) { + log(WARNING, "Failed to load map %s: %s", filename.c_str(), e.what()); + } } } + if (game->enemies.empty()) { + throw runtime_error("failed to load any map data"); + } + } else { // In non-BB games, just set the variations (we don't track items/enemies/ // etc.) @@ -1686,6 +1683,8 @@ void process_team_command_bb(shared_ptr, shared_ptr c, if (command == 0x01EA) { send_lobby_message_box(c, u"$C6Teams are not supported."); + } else if (command == 0x14EA) { + // Do nothing (for now) } else { throw invalid_argument("unimplemented team command"); } diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 6ccf0a8b..64366f3f 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -220,7 +220,7 @@ static void process_subcommand_switch_state_changed(shared_ptr, log(INFO, "[Switch assist] Replaying previous enable command"); forward_subcommand(l, c, command, flag, &c->last_switch_enabled_command, sizeof(c->last_switch_enabled_command)); - send_command(c, command, flag, c->last_switch_enabled_command); + send_command_t(c, command, flag, c->last_switch_enabled_command); } c->last_switch_enabled_command = cmd; } @@ -756,7 +756,7 @@ static void process_subcommand_phase_setup(shared_ptr, 0.0f, 0xE0AEDC0100000002, }; - send_command(c, 0x62, l->leader_id, req); + send_command_t(c, 0x62, l->leader_id, req); } } } @@ -904,7 +904,7 @@ static void process_subcommand_identify_item_bb(shared_ptr, res.client_id = c->lobby_client_id; res.unused = 0; res.item = c->game_data.identify_result.data; - send_command(l, 0x60, 0x00, res); + send_command_t(l, 0x60, 0x00, res); } else { forward_subcommand(l, c, command, flag, data); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 507ac164..a340fbe1 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include "PSOProtocol.hh" #include "CommandFormats.hh" @@ -186,7 +187,7 @@ void send_server_init_dc_pc_gc(shared_ptr c, auto cmd = prepare_server_init_contents_dc_pc_gc( initial_connection, server_key, client_key); - send_command(c, command, 0x00, cmd); + send_command_t(c, command, 0x00, cmd); switch (c->version) { case GameVersion::DC: @@ -209,7 +210,7 @@ void send_server_init_bb(shared_ptr s, shared_ptr c) { random_data(cmd.server_key.data(), cmd.server_key.bytes()); random_data(cmd.client_key.data(), cmd.client_key.bytes()); cmd.after_message = anti_copyright; - send_command(c, 0x03, 0x00, cmd); + send_command_t(c, 0x03, 0x00, cmd); static const string expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8); shared_ptr client_encr(new PSOBBMultiKeyClientEncryption( @@ -226,7 +227,7 @@ void send_server_init_patch(shared_ptr c) { cmd.copyright = patch_server_copyright; cmd.server_key = server_key; cmd.client_key = client_key; - send_command(c, 0x02, 0x00, cmd); + send_command_t(c, 0x02, 0x00, cmd); c->crypt_out.reset(new PSOPCEncryption(server_key)); c->crypt_in.reset(new PSOPCEncryption(client_key)); @@ -259,17 +260,17 @@ void send_update_client_config(shared_ptr c) { cmd.player_tag = 0x00010000; cmd.guild_card_number = c->license->serial_number; cmd.cfg = c->export_config(); - send_command(c, 0x04, 0x00, cmd); + send_command_t(c, 0x04, 0x00, cmd); } void send_reconnect(shared_ptr c, uint32_t address, uint16_t port) { S_Reconnect_19 cmd = {address, port, 0}; - send_command(c, 0x19, 0x00, cmd); + send_command_t(c, 0x19, 0x00, cmd); } -// sends the command (first used by Schthack) that separates PC and GC users +// Sends the command (first used by Schthack) that separates PC and GC users // that connect on the same port void send_pc_gc_split_reconnect(shared_ptr c, uint32_t address, uint16_t pc_port, uint16_t gc_port) { @@ -281,7 +282,7 @@ void send_pc_gc_split_reconnect(shared_ptr c, uint32_t address, cmd.gc_size = 0x97; cmd.gc_address = address; cmd.gc_port = gc_port; - send_command(c, 0x19, 0x00, cmd); + send_command_t(c, 0x19, 0x00, cmd); } @@ -294,11 +295,11 @@ void send_client_init_bb(shared_ptr c, uint32_t error) { cmd.team_id = static_cast(random_object()); cmd.cfg = c->export_config_bb(); cmd.caps = 0x00000102; - send_command(c, 0x00E6, 0x00000000, cmd); + send_command_t(c, 0x00E6, 0x00000000, cmd); } void send_team_and_key_config_bb(shared_ptr c) { - send_command(c, 0x00E2, 0x00000000, c->game_data.account()->key_config); + send_command_t(c, 0x00E2, 0x00000000, c->game_data.account()->key_config); } void send_player_preview_bb(shared_ptr c, uint8_t player_index, @@ -307,24 +308,24 @@ void send_player_preview_bb(shared_ptr c, uint8_t player_index, if (!preview) { // no player exists S_PlayerPreview_NoPlayer_BB_E4 cmd = {player_index, 0x00000002}; - send_command(c, 0x00E4, 0x00000000, cmd); + send_command_t(c, 0x00E4, 0x00000000, cmd); } else { - S_PlayerPreview_BB_E3 cmd = {player_index, *preview}; - send_command(c, 0x00E3, 0x00000000, cmd); + SC_PlayerPreview_CreateCharacter_BB_E5 cmd = {player_index, *preview}; + send_command_t(c, 0x00E5, 0x00000000, cmd); } } void send_accept_client_checksum_bb(shared_ptr c) { S_AcceptClientChecksum_BB_02E8 cmd = {1, 0}; - send_command(c, 0x02E8, 0x00000000, cmd); + send_command_t(c, 0x02E8, 0x00000000, cmd); } void send_guild_card_header_bb(shared_ptr c) { - uint32_t checksum = compute_guild_card_checksum( + uint32_t checksum = crc32( &c->game_data.account()->guild_cards, sizeof(GuildCardFileBB)); - S_GuildCardHeader_BB_01DC cmd = {1, 0x00000490, checksum}; - send_command(c, 0x01DC, 0x00000000, cmd); + S_GuildCardHeader_BB_01DC cmd = {1, sizeof(GuildCardFileBB), checksum}; + send_command_t(c, 0x01DC, 0x00000000, cmd); } void send_guild_card_chunk_bb(shared_ptr c, size_t chunk_index) { @@ -346,63 +347,78 @@ void send_guild_card_chunk_bb(shared_ptr c, size_t chunk_index) { send_command(c, 0x02DC, 0x00000000, w.str()); } -void send_stream_file_bb(shared_ptr c) { - auto index_data = file_cache.get("system/blueburst/streamfile.ind"); - if (index_data->size() % sizeof(S_StreamFileIndexEntry_BB_01EB)) { - throw invalid_argument("stream file index not a multiple of entry size"); +static const vector stream_file_entries = { + "ItemMagEdit.prs", + "ItemPMT.prs", + "BattleParamEntry.dat", + "BattleParamEntry_on.dat", + "BattleParamEntry_lab.dat", + "BattleParamEntry_lab_on.dat", + "BattleParamEntry_ep4.dat", + "BattleParamEntry_ep4_on.dat", + "PlyLevelTbl.prs", +}; + +void send_stream_file_index_bb(shared_ptr c) { + + struct S_StreamFileIndexEntry_BB_01EB { + le_uint32_t size; + le_uint32_t checksum; // crc32 of file data + le_uint32_t offset; // offset in stream (== sum of all previous files' sizes) + ptext filename; + }; + + vector entries; + size_t offset = 0; + for (const string& filename : stream_file_entries) { + auto file_data = file_cache.get("system/blueburst/" + filename); + auto& e = entries.emplace_back(); + e.size = file_data->size(); + // TODO: memoize the checksum somewhere; computing it can be slow + e.checksum = crc32(file_data->data(), e.size); + e.offset = offset; + e.filename = filename; + offset += e.size; } + send_command_vt(c, 0x01EB, entries.size(), entries); +} - size_t entry_count = index_data->size() / sizeof(S_StreamFileIndexEntry_BB_01EB); - send_command(c, 0x01EB, entry_count, index_data); +void send_stream_file_chunk_bb(shared_ptr c, uint32_t chunk_index) { + auto contents = file_cache.get("", +[]() -> string { + size_t bytes = 0; + for (const auto& name : stream_file_entries) { + bytes += file_cache.get("system/blueburst/" + name)->size(); + } - auto* entries = reinterpret_cast(index_data->data()); + string ret; + ret.reserve(bytes); + for (const auto& name : stream_file_entries) { + ret += *file_cache.get("system/blueburst/" + name); + } + return ret; + }); S_StreamFileChunk_BB_02EB chunk_cmd; - chunk_cmd.chunk_index = 0; - - uint32_t buffer_offset = 0; - for (size_t x = 0; x < entry_count; x++) { - string base_filename = entries[x].filename; - auto filename = "system/blueburst/" + base_filename; - auto file_data = file_cache.get(filename); - - size_t file_data_remaining = file_data->size(); - if (file_data_remaining != entries[x].size) { - throw invalid_argument(filename + " does not match size in stream file index"); - } - while (file_data_remaining) { - size_t read_size = 0x6800 - buffer_offset; - if (read_size > file_data_remaining) { - read_size = file_data_remaining; - } - memcpy(&chunk_cmd.data[buffer_offset], - file_data->data() + file_data->size() - file_data_remaining, read_size); - // TODO: We probably should clear the rest of the buffer on the last chunk - buffer_offset += read_size; - file_data_remaining -= read_size; - - if (buffer_offset == 0x6800) { - // Note: the client sends 0x03EB in response to these, but we'll just - // ignore them because we don't need any of the contents - send_command(c, 0x02EB, 0x00000000, chunk_cmd); - buffer_offset = 0; - chunk_cmd.chunk_index++; - } - } - - if (buffer_offset > 0) { - send_command(c, 0x02EB, 0x00000000, &chunk_cmd, (buffer_offset + 15) & ~3); - } + chunk_cmd.chunk_index = chunk_index; + size_t offset = sizeof(chunk_cmd.data) * chunk_index; + if (offset > contents->size()) { + throw runtime_error("client requested chunk beyond end of stream file"); } + size_t bytes = min(contents->size() - offset, sizeof(chunk_cmd.data)); + memcpy(chunk_cmd.data, contents->data() + offset, bytes); + + size_t cmd_size = offsetof(S_StreamFileChunk_BB_02EB, data) + bytes; + cmd_size = (cmd_size + 3) & ~3; + send_command(c, 0x02EB, 0x00000000, &chunk_cmd, cmd_size); } void send_approve_player_choice_bb(shared_ptr c) { S_ApprovePlayerChoice_BB_00E4 cmd = {c->bb_player_index, 1}; - send_command(c, 0x00E4, 0x00000000, cmd); + send_command_t(c, 0x00E4, 0x00000000, cmd); } void send_complete_player_bb(shared_ptr c) { - send_command(c, 0x00E7, 0x00000000, c->game_data.export_player_bb()); + send_command_t(c, 0x00E7, 0x00000000, c->game_data.export_player_bb()); } @@ -412,7 +428,7 @@ void send_complete_player_bb(shared_ptr c) { void send_check_directory_patch(shared_ptr c, const string& dir) { S_CheckDirectory_Patch_09 cmd = {dir}; - send_command(c, 0x09, 0x00, cmd); + send_command_t(c, 0x09, 0x00, cmd); } @@ -508,7 +524,7 @@ void send_simple_mail_gc(std::shared_ptr c, uint32_t from_guild_card_num cmd.from_name = from_name; cmd.to_guild_card_number = c->license->serial_number; cmd.text = text; - send_command(c, 0x81, 0x00, cmd); + send_command_t(c, 0x81, 0x00, cmd); } void send_simple_mail(std::shared_ptr c, uint32_t from_guild_card_number, @@ -537,7 +553,7 @@ void send_info_board_t(shared_ptr c, shared_ptr l) { e.message = c->game_data.player()->info_board; add_color_inplace(e.message); } - send_command(c, 0xD8, entries.size(), entries); + send_command_vt(c, 0xD8, entries.size(), entries); } void send_info_board(shared_ptr c, shared_ptr l) { @@ -589,7 +605,7 @@ void send_card_search_result_t( cmd.lobby_id = result->lobby_id; cmd.name = result->game_data.player()->disp.name; - send_command(c, 0x41, 0x00, cmd); + send_command_t(c, 0x41, 0x00, cmd); } void send_card_search_result( @@ -628,7 +644,7 @@ void send_guild_card_pc_gc(shared_ptr c, shared_ptr source) { cmd.reserved2 = 1; cmd.section_id = source->game_data.player()->disp.section_id; cmd.char_class = source->game_data.player()->disp.char_class; - send_command(c, 0x62, c->lobby_client_id, cmd); + send_command_t(c, 0x62, c->lobby_client_id, cmd); } void send_guild_card_bb(shared_ptr c, shared_ptr source) { @@ -644,7 +660,7 @@ void send_guild_card_bb(shared_ptr c, shared_ptr source) { cmd.reserved2 = 1; cmd.section_id = source->game_data.player()->disp.section_id; cmd.char_class = source->game_data.player()->disp.char_class; - send_command(c, 0x62, c->lobby_client_id, cmd); + send_command_t(c, 0x62, c->lobby_client_id, cmd); } void send_guild_card(shared_ptr c, shared_ptr source) { @@ -696,7 +712,7 @@ void send_menu_t( e.text = item.name; } - send_command(c, is_info_menu ? 0x1F : 0x07, entries.size() - 1, entries); + send_command_vt(c, is_info_menu ? 0x1F : 0x07, entries.size() - 1, entries); } void send_menu(shared_ptr c, const u16string& menu_name, @@ -749,7 +765,7 @@ void send_game_menu_t(shared_ptr c, shared_ptr s) { e.name = l->name; } - send_command(c, 0x08, entries.size() - 1, entries); + send_command_vt(c, 0x08, entries.size() - 1, entries); } void send_game_menu(shared_ptr c, shared_ptr s) { @@ -777,7 +793,7 @@ void send_quest_menu_t( e.short_desc = quest->short_description; add_color_inplace(e.short_desc); } - send_command(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); + send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); } template @@ -795,7 +811,7 @@ void send_quest_menu_t( e.short_desc = item.description; add_color_inplace(e.short_desc); } - send_command(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); + send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); } void send_quest_menu(shared_ptr c, uint32_t menu_id, @@ -843,7 +859,7 @@ void send_lobby_list(shared_ptr c, shared_ptr s) { e.unused = 0; } - send_command(c, 0x83, entries.size(), entries); + send_command_vt(c, 0x83, entries.size(), entries); } @@ -1020,7 +1036,7 @@ void send_player_join_notification(shared_ptr c, void send_player_leave_notification(shared_ptr l, uint8_t leaving_client_id) { S_LeaveLobby_66_69 cmd = {leaving_client_id, l->leader_id, 0}; - send_command(l, l->is_game() ? 0x66 : 0x69, leaving_client_id, cmd); + send_command_t(l, l->is_game() ? 0x66 : 0x69, leaving_client_id, cmd); } void send_get_player_info(shared_ptr c) { @@ -1045,7 +1061,7 @@ void send_arrow_update(shared_ptr l) { e.arrow_color = l->clients[x]->lobby_arrow_color; } - send_command(l, 0x88, entries.size(), entries); + send_command_vt(l, 0x88, entries.size(), entries); } // tells the player that the joining player is done joining, and the game can resume @@ -1086,7 +1102,7 @@ void send_player_stats_change(shared_ptr l, shared_ptr c, } } - send_command(l, 0x60, 0x00, subs); + send_command_vt(l, 0x60, 0x00, subs); } void send_warp(shared_ptr c, uint32_t area) { @@ -1139,7 +1155,7 @@ void send_drop_item(shared_ptr l, const ItemData& item, bool from_enemy, uint8_t area, float x, float z, uint16_t request_id) { G_DropItem_6x5F cmd = { 0x5F, 0x0A, 0x0000, area, from_enemy, request_id, x, z, 0, item, 0}; - send_command(l, 0x60, 0x00, cmd); + send_command_t(l, 0x60, 0x00, cmd); } // notifies other players that a stack was split and part of it dropped (a new item was created) @@ -1149,7 +1165,7 @@ void send_drop_stacked_item(shared_ptr l, const ItemData& item, // GC sends {0, item} (the last two fields in the struct are switched). G_DropStackedItem_6x5D cmd = { 0x5D, 0x09, 0x00, 0x00, area, 0, x, z, item, 0}; - send_command(l, 0x60, 0x00, cmd); + send_command_t(l, 0x60, 0x00, cmd); } // notifies other players that an item was picked up @@ -1157,7 +1173,7 @@ void send_pick_up_item(shared_ptr l, shared_ptr c, uint32_t item_id, uint8_t area) { G_PickUpItem_6x59 cmd = { 0x59, 0x03, c->lobby_client_id, c->lobby_client_id, area, item_id}; - send_command(l, 0x60, 0x00, cmd); + send_command_t(l, 0x60, 0x00, cmd); } // creates an item in a player's inventory (used for withdrawing items from the bank) @@ -1165,7 +1181,7 @@ void send_create_inventory_item(shared_ptr l, shared_ptr c, const ItemData& item) { G_CreateInventoryItem_BB_6xBE cmd = { 0xBE, 0x07, c->lobby_client_id, item, 0}; - send_command(l, 0x60, 0x00, cmd); + send_command_t(l, 0x60, 0x00, cmd); } // destroys an item @@ -1173,7 +1189,7 @@ void send_destroy_item(shared_ptr l, shared_ptr c, uint32_t item_id, uint32_t amount) { G_ItemSubcommand cmd = { 0x29, 0x03, c->lobby_client_id, 0x00, item_id, amount}; - send_command(l, 0x60, 0x00, cmd); + send_command_t(l, 0x60, 0x00, cmd); } // sends the player their bank data @@ -1188,7 +1204,7 @@ void send_bank(shared_ptr c) { size_t size = 8 + sizeof(cmd) + items.size() * sizeof(PlayerBankItem); cmd.size = size; - send_command(c, 0x6C, 0x00, cmd, items); + send_command_t_vt(c, 0x6C, 0x00, cmd, items); } // sends the player a shop's contents @@ -1241,7 +1257,7 @@ void send_level_up(shared_ptr l, shared_ptr c) { stats.dfp, stats.ata, c->game_data.player()->disp.level}; - send_command(l, 0x60, 0x00, cmd); + send_command_t(l, 0x60, 0x00, cmd); } // gives a player EXP @@ -1249,7 +1265,7 @@ void send_give_experience(shared_ptr l, shared_ptr c, uint32_t amount) { G_GiveExperience_BB_6xBF cmd = { 0xBF, sizeof(G_GiveExperience_BB_6xBF) / 4, c->lobby_client_id, 0, amount}; - send_command(l, 0x60, 0x00, cmd); + send_command_t(l, 0x60, 0x00, cmd); } @@ -1272,7 +1288,7 @@ void send_ep3_card_list_update(shared_ptr c) { void send_ep3_rank_update(shared_ptr c) { S_RankUpdate_GC_Ep3_B7 cmd = { 0, "\0\0\0\0\0\0\0\0\0\0\0", 0x00FFFFFF, 0x00FFFFFF, 0xFFFFFFFF}; - send_command(c, 0xB7, 0x00, cmd); + send_command_t(c, 0xB7, 0x00, cmd); } // sends the map list (used for battle setup) to all players in a game @@ -1327,7 +1343,7 @@ void send_quest_open_file_t( cmd.file_size = file_size; cmd.name = "PSO/" + quest_name; cmd.filename = filename.c_str(); - send_command(c, is_download_quest ? 0xA6 : 0x44, 0x00, cmd); + send_command_t(c, is_download_quest ? 0xA6 : 0x44, 0x00, cmd); } void send_quest_file_chunk( @@ -1349,7 +1365,7 @@ void send_quest_file_chunk( } cmd.data_size = size; - send_command(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd); + send_command_t(c, is_download_quest ? 0xA7 : 0x13, chunk_index, cmd); } void send_quest_file(shared_ptr c, const string& quest_name, diff --git a/src/SendCommands.hh b/src/SendCommands.hh index e25a807c..85862fa3 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -62,26 +62,26 @@ inline void send_command(std::shared_ptr s, uint16_t command, } template -static void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, - const StructT& data) { +static void send_command_t(std::shared_ptr c, uint16_t command, + uint32_t flag, const StructT& data) { send_command(c, command, flag, &data, sizeof(data)); } template -static void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, - const std::string& data) { +static void send_command(std::shared_ptr c, uint16_t command, + uint32_t flag, const std::string& data) { send_command(c, command, flag, data.data(), data.size()); } template -void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, - const std::vector& data) { +void send_command_vt(std::shared_ptr c, uint16_t command, + uint32_t flag, const std::vector& data) { send_command(c, command, flag, data.data(), data.size() * sizeof(StructT)); } template -void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, - const StructT& data, const std::vector& array_data) { +void send_command_t_vt(std::shared_ptr c, uint16_t command, + uint32_t flag, const StructT& data, const std::vector& array_data) { std::string all_data(reinterpret_cast(&data), sizeof(StructT)); all_data.append(reinterpret_cast(array_data.data()), array_data.size() * sizeof(EntryT)); @@ -109,7 +109,8 @@ void send_player_preview_bb(std::shared_ptr c, uint8_t player_index, void send_accept_client_checksum_bb(std::shared_ptr c); void send_guild_card_header_bb(std::shared_ptr c); void send_guild_card_chunk_bb(std::shared_ptr c, size_t chunk_index); -void send_stream_file_bb(std::shared_ptr c); +void send_stream_file_index_bb(std::shared_ptr c); +void send_stream_file_chunk_bb(std::shared_ptr c, uint32_t chunk_index); void send_approve_player_choice_bb(std::shared_ptr c); void send_complete_player_bb(std::shared_ptr c); diff --git a/src/Server.cc b/src/Server.cc index c099c8d8..48372a69 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -106,7 +106,8 @@ void Server::on_listen_accept(struct evconnlistener* listener, return; } - this->log(INFO, "Client fd %d connected via fd %d", fd, listen_fd); + this->log(INFO, "Client fd %d connected via fd %d (%s)", + fd, listen_fd, listening_socket->name.c_str()); struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); @@ -227,37 +228,48 @@ Server::Server( base(base), state(state) { } -void Server::listen(const string& socket_path, GameVersion version, +void Server::listen( + const std::string& name, + const string& socket_path, + GameVersion version, ServerBehavior behavior) { int fd = ::listen(socket_path, 0, SOMAXCONN); - this->log(INFO, "Listening on Unix socket %s (%s) on fd %d", - socket_path.c_str(), name_for_version(version), fd); - this->add_socket(fd, version, behavior); + this->log(INFO, "Listening on Unix socket %s (%s) on fd %d (name: %s)", + socket_path.c_str(), name_for_version(version), fd, name.c_str()); + this->add_socket(name, fd, version, behavior); } -void Server::listen(const string& addr, int port, GameVersion version, +void Server::listen( + const std::string& name, + const string& addr, + int port, + GameVersion version, ServerBehavior behavior) { int fd = ::listen(addr, port, SOMAXCONN); string netloc_str = render_netloc(addr, port); - this->log(INFO, "Listening on TCP interface %s (%s) on fd %d", - netloc_str.c_str(), name_for_version(version), fd); - this->add_socket(fd, version, behavior); + this->log(INFO, "Listening on TCP interface %s (%s) on fd %d (name: %s)", + netloc_str.c_str(), name_for_version(version), fd, name.c_str()); + this->add_socket(name, fd, version, behavior); } -void Server::listen(int port, GameVersion version, ServerBehavior behavior) { - this->listen("", port, version, behavior); +void Server::listen(const std::string& name, int port, GameVersion version, ServerBehavior behavior) { + this->listen(name, "", port, version, behavior); } -Server::ListeningSocket::ListeningSocket(Server* s, int fd, - GameVersion version, ServerBehavior behavior) : - fd(fd), version(version), behavior(behavior), listener( +Server::ListeningSocket::ListeningSocket(Server* s, const std::string& name, + int fd, GameVersion version, ServerBehavior behavior) : + name(name), fd(fd), version(version), behavior(behavior), listener( evconnlistener_new(s->base.get(), Server::dispatch_on_listen_accept, s, LEV_OPT_REUSEABLE, 0, this->fd), evconnlistener_free) { evconnlistener_set_error_cb(this->listener.get(), Server::dispatch_on_listen_error); } -void Server::add_socket(int fd, GameVersion version, ServerBehavior behavior) { +void Server::add_socket( + const std::string& name, + int fd, + GameVersion version, + ServerBehavior behavior) { this->listening_sockets.emplace(piecewise_construct, forward_as_tuple(fd), - forward_as_tuple(this, fd, version, behavior)); + forward_as_tuple(this, name, fd, version, behavior)); } diff --git a/src/Server.hh b/src/Server.hh index 1dc699b8..1f7c7ee7 100644 --- a/src/Server.hh +++ b/src/Server.hh @@ -21,10 +21,10 @@ public: std::shared_ptr state); virtual ~Server() = default; - void listen(const std::string& socket_path, GameVersion version, ServerBehavior initial_state); - void listen(const std::string& addr, int port, GameVersion version, ServerBehavior initial_state); - void listen(int port, GameVersion version, ServerBehavior initial_state); - void add_socket(int fd, GameVersion version, ServerBehavior initial_state); + void listen(const std::string& name, const std::string& socket_path, GameVersion version, ServerBehavior initial_state); + void listen(const std::string& name, const std::string& addr, int port, GameVersion version, ServerBehavior initial_state); + void listen(const std::string& name, int port, GameVersion version, ServerBehavior initial_state); + void add_socket(const std::string& name, int fd, GameVersion version, ServerBehavior initial_state); void connect_client(struct bufferevent* bev, uint32_t address, uint16_t port, GameVersion version, ServerBehavior initial_state); @@ -34,12 +34,17 @@ private: std::shared_ptr base; struct ListeningSocket { + std::string name; int fd; GameVersion version; ServerBehavior behavior; std::unique_ptr listener; - ListeningSocket(Server* s, int fd, GameVersion version, + ListeningSocket( + Server* s, + const std::string& name, + int fd, + GameVersion version, ServerBehavior behavior); }; std::unordered_map listening_sockets; diff --git a/src/Version.cc b/src/Version.cc index 0f9f6940..9569aaf8 100644 --- a/src/Version.cc +++ b/src/Version.cc @@ -67,16 +67,53 @@ const char* name_for_version(GameVersion version) { GameVersion version_for_name(const char* name) { if (!strcasecmp(name, "DC") || !strcasecmp(name, "DreamCast")) { return GameVersion::DC; - } - if (!strcasecmp(name, "PC")) { + } else if (!strcasecmp(name, "PC")) { return GameVersion::PC; - } - if (!strcasecmp(name, "GC") || !strcasecmp(name, "GameCube")) { + } else if (!strcasecmp(name, "GC") || !strcasecmp(name, "GameCube")) { return GameVersion::GC; - } - if (!strcasecmp(name, "BB") || !strcasecmp(name, "BlueBurst") || + } else if (!strcasecmp(name, "BB") || !strcasecmp(name, "BlueBurst") || !strcasecmp(name, "Blue Burst")) { return GameVersion::BB; + } else if (!strcasecmp(name, "Patch")) { + return GameVersion::PATCH; + } else { + throw invalid_argument("incorrect version name"); } - throw invalid_argument("incorrect version name"); } + +const char* name_for_server_behavior(ServerBehavior behavior) { + switch (behavior) { + case ServerBehavior::SPLIT_RECONNECT: + return "split_reconnect"; + case ServerBehavior::LOGIN_SERVER: + return "login_server"; + case ServerBehavior::LOBBY_SERVER: + return "lobby_server"; + case ServerBehavior::DATA_SERVER_BB: + return "data_server_bb"; + case ServerBehavior::PATCH_SERVER: + return "patch_server"; + case ServerBehavior::PROXY_SERVER: + return "proxy_server"; + default: + throw logic_error("invalid server behavior"); + } +} + +ServerBehavior server_behavior_for_name(const char* name) { + if (!strcasecmp(name, "split_reconnect")) { + return ServerBehavior::SPLIT_RECONNECT; + } else if (!strcasecmp(name, "login_server") || !strcasecmp(name, "login")) { + return ServerBehavior::LOGIN_SERVER; + } else if (!strcasecmp(name, "lobby_server") || !strcasecmp(name, "lobby")) { + return ServerBehavior::LOBBY_SERVER; + } 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") || !strcasecmp(name, "patch")) { + return ServerBehavior::PATCH_SERVER; + } else if (!strcasecmp(name, "proxy_server") || !strcasecmp(name, "proxy")) { + return ServerBehavior::PROXY_SERVER; + } else { + throw invalid_argument("incorrect server behavior name"); + } +} \ No newline at end of file diff --git a/src/Version.hh b/src/Version.hh index 9be1931c..c71ae18d 100644 --- a/src/Version.hh +++ b/src/Version.hh @@ -12,6 +12,18 @@ enum class GameVersion { BB, }; +enum class ServerBehavior { + SPLIT_RECONNECT = 0, + LOGIN_SERVER, + LOBBY_SERVER, + DATA_SERVER_BB, + PATCH_SERVER, + PROXY_SERVER, +}; + uint16_t flags_for_version(GameVersion version, uint8_t sub_version); const char* name_for_version(GameVersion version); GameVersion version_for_name(const char* name); + +const char* name_for_server_behavior(ServerBehavior behavior); +ServerBehavior server_behavior_for_name(const char* name); diff --git a/system/blueburst/streamfile.ind b/system/blueburst/streamfile.ind deleted file mode 100755 index eeb144f87f09a7591a0fc0283ee0528b4d451631..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 684 zcmY#lW?*R0+P#PY2s}$tbA1!jT~jhk^a_fKK|&;fCm{?BtsLwtlz?V}%nI-gA`JEK1CE%_}LY)JsV$A<1Dtv;Nm_XlC