#include "ReceiveCommands.hh" #include #include #include #include #include #include #include #include #include #include "Loggers.hh" #include "ChatCommands.hh" #include "FileContentsCache.hh" #include "ProxyServer.hh" #include "PSOProtocol.hh" #include "ReceiveSubcommands.hh" #include "SendCommands.hh" #include "StaticGameData.hh" #include "Text.hh" using namespace std; 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), MenuItem(static_cast(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0), MenuItem(static_cast(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0), MenuItem(static_cast(QuestCategory::VR), u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC), MenuItem(static_cast(QuestCategory::TOWER), u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC), }); vector quest_battle_menu({ MenuItem(static_cast(QuestCategory::BATTLE), u"Battle", u"$E$C6Battle mode rule\nsets", 0), }); vector quest_challenge_menu({ MenuItem(static_cast(QuestCategory::CHALLENGE), u"Challenge", u"$E$C6Challenge mode\nquests", 0), }); vector quest_solo_menu({ MenuItem(static_cast(QuestCategory::SOLO), u"Solo Quests", u"$E$C6Quests that require\na single player", 0), }); vector quest_government_menu({ MenuItem(static_cast(QuestCategory::GOVERNMENT_EPISODE_1), u"Hero in Red",u"$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline", 0), MenuItem(static_cast(QuestCategory::GOVERNMENT_EPISODE_2), u"The Military's Hero",u"$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline", 0), MenuItem(static_cast(QuestCategory::GOVERNMENT_EPISODE_4), u"The Meteor Impact Incident", u"$E$C6Quests that follow\nthe Episode 4\nstoryline", 0), }); vector quest_download_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), MenuItem(static_cast(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0), MenuItem(static_cast(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0), MenuItem(static_cast(QuestCategory::VR), u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC), MenuItem(static_cast(QuestCategory::TOWER), u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC), MenuItem(static_cast(QuestCategory::DOWNLOAD), u"Download", u"$E$C6Quests to download\nto your Memory Card", 0), }); //////////////////////////////////////////////////////////////////////////////// void process_connect(std::shared_ptr s, std::shared_ptr c) { switch (c->server_behavior) { case ServerBehavior::SPLIT_RECONNECT: { uint16_t pc_port = s->name_to_port_config.at("pc-login")->port; uint16_t gc_port = s->name_to_port_config.at("gc-jp10")->port; send_pc_gc_split_reconnect(c, s->connect_address_for_client(c), pc_port, gc_port); c->should_disconnect = true; break; } case ServerBehavior::LOGIN_SERVER: send_server_init(s, c, true, false); if (s->pre_lobby_event) { send_change_event(c, s->pre_lobby_event); } break; case ServerBehavior::PATCH_SERVER_BB: c->flags |= Client::Flag::BB_PATCH; [[fallthrough]]; case ServerBehavior::PATCH_SERVER_PC: case ServerBehavior::DATA_SERVER_BB: case ServerBehavior::LOBBY_SERVER: send_server_init(s, c, false, false); break; default: c->log.error("Unimplemented behavior: %" PRId64, static_cast(c->server_behavior)); } } void process_login_complete(shared_ptr s, shared_ptr c) { // 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(s, c); send_ep3_rank_update(c); } // On BB, send the pre-lobby event, if set. This normally happens on the // login server immediately after the encryption init command, but on BB we // don't know the client's state until after we receive the login command, // so we do it here instead. if ((c->version == GameVersion::BB) && s->pre_lobby_event) { send_change_event(c, s->pre_lobby_event); } if (s->welcome_message.empty() || (c->flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION) || !(c->flags & Client::Flag::AT_WELCOME_MESSAGE)) { c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE; send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { send_message_box(c, s->welcome_message.c_str()); } } else if (c->server_behavior == ServerBehavior::LOBBY_SERVER) { if (c->version == GameVersion::BB) { // This implicitly loads the client's account and player data send_complete_player_bb(c); } send_lobby_list(c, s); send_get_player_info(c); } } void process_disconnect(shared_ptr s, shared_ptr c) { // if the client was in a lobby, remove them and notify the other clients if (c->lobby_id) { s->remove_client_from_lobby(c); } // TODO: Make a timer event for each connected player that saves their data // periodically, not only when they disconnect // TODO: Track play time somewhere for BB players // c->game_data.player()->disp.play_time += ((now() - c->play_time_begin) / 1000000); // Note: The client's GameData destructor should save their player data // shortly after this point } //////////////////////////////////////////////////////////////////////////////// void process_verify_license_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // DB const auto& cmd = check_size_t(data); c->flags |= flags_for_version(c->version, cmd.sub_version); uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16); try { auto l = s->license_manager->verify_gc(serial_number, cmd.access_key, cmd.password); c->set_license(l); send_command(c, 0x9A, 0x02); } catch (const incorrect_access_key& e) { send_command(c, 0x9A, 0x03); c->should_disconnect = true; return; } catch (const incorrect_password& e) { send_command(c, 0x9A, 0x07); c->should_disconnect = true; return; } catch (const missing_license& e) { if (!s->allow_unregistered_users) { send_command(c, 0x9A, 0x04); c->should_disconnect = true; return; } else { auto l = LicenseManager::create_license_gc(serial_number, cmd.access_key, cmd.password, true); s->license_manager->add(l); c->set_license(l); send_command(c, 0x9A, 0x02); } } } void process_login_a_dc_pc_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 9A const auto& cmd = check_size_t(data); c->flags |= flags_for_version(c->version, cmd.sub_version); uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16); try { shared_ptr l; switch (c->version) { case GameVersion::PC: l = s->license_manager->verify_pc(serial_number, cmd.access_key); break; case GameVersion::GC: l = s->license_manager->verify_gc(serial_number, cmd.access_key); break; case GameVersion::XB: throw runtime_error("xbox licenses are not implemented"); break; default: throw logic_error("unsupported versioned command"); } c->set_license(l); send_command(c, 0x9A, 0x02); } catch (const incorrect_access_key& e) { send_command(c, 0x9A, 0x03); c->should_disconnect = true; return; } catch (const incorrect_password& e) { send_command(c, 0x9A, 0x07); c->should_disconnect = true; return; } catch (const missing_license& e) { // On V3, the client should have sent a different command containing the // password already, which should have created and added a temporary // license. So, if no license exists at this point, disconnect the client // even if unregistered clients are allowed. shared_ptr l; if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) { send_command(c, 0x9A, 0x04); c->should_disconnect = true; return; } else if (c->version == GameVersion::PC) { l = LicenseManager::create_license_pc(serial_number, cmd.access_key, true); s->license_manager->add(l); c->set_license(l); send_command(c, 0x9A, 0x02); } else { throw runtime_error("unsupported game version"); } } } void process_login_c_dc_pc_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 9C const auto& cmd = check_size_t(data); c->flags |= flags_for_version(c->version, cmd.sub_version); uint32_t serial_number = stoul(cmd.serial_number, nullptr, 16); try { shared_ptr l; switch (c->version) { case GameVersion::PC: l = s->license_manager->verify_pc(serial_number, cmd.access_key); break; case GameVersion::GC: l = s->license_manager->verify_gc(serial_number, cmd.access_key, cmd.password); break; case GameVersion::XB: throw runtime_error("xbox licenses are not implemented"); break; default: throw logic_error("unsupported versioned command"); } c->set_license(l); send_command(c, 0x9C, 0x01); } catch (const incorrect_password& e) { send_command(c, 0x9C, 0x00); c->should_disconnect = true; return; } catch (const missing_license& e) { if (!s->allow_unregistered_users) { send_command(c, 0x9C, 0x00); c->should_disconnect = true; return; } else { shared_ptr l; switch (c->version) { case GameVersion::PC: l = LicenseManager::create_license_pc(serial_number, cmd.access_key, true); break; case GameVersion::GC: l = LicenseManager::create_license_gc(serial_number, cmd.access_key, cmd.password, true); break; case GameVersion::XB: throw runtime_error("xbox licenses are not implemented"); break; default: throw logic_error("unsupported versioned command"); } s->license_manager->add(l); c->set_license(l); send_command(c, 0x9C, 0x01); } } } void process_login_d_e_pc_v3(shared_ptr s, shared_ptr c, uint16_t command, uint32_t, const string& data) { // 9D 9E // The client sends extra unused data the first time it sends these commands, // hence the odd check_size calls here const C_Login_PC_9D* base_cmd; if (command == 0x9D) { base_cmd = &check_size_t(data, sizeof(C_Login_PC_9D), sizeof(C_LoginExtended_PC_9D)); if (base_cmd->is_extended) { const auto& cmd = check_size_t(data); if (cmd.extension.menu_id == MenuID::LOBBY) { c->preferred_lobby_id = cmd.extension.preferred_lobby_id; } } } else if (command == 0x9E) { const auto& cmd = check_size_t(data, sizeof(C_Login_GC_9E), sizeof(C_LoginExtended_GC_9E)); base_cmd = &cmd; if (cmd.is_extended) { const auto& cmd = check_size_t(data); if (cmd.extension.menu_id == MenuID::LOBBY) { c->preferred_lobby_id = cmd.extension.preferred_lobby_id; } } try { c->import_config(cmd.client_config.cfg); } catch (const invalid_argument&) { // 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 = ClientStateBB::INITIAL_LOGIN; c->game_data.bb_player_index = 0; } } else { throw logic_error("9D/9E handler called for incorrect command"); } c->flags |= flags_for_version(c->version, base_cmd->sub_version); uint32_t serial_number = stoul(base_cmd->serial_number, nullptr, 16); try { shared_ptr l; switch (c->version) { case GameVersion::PC: l = s->license_manager->verify_pc(serial_number, base_cmd->access_key); break; case GameVersion::GC: l = s->license_manager->verify_gc(serial_number, base_cmd->access_key); break; case GameVersion::XB: throw runtime_error("xbox licenses are not implemented"); break; default: throw logic_error("unsupported versioned command"); } c->set_license(l); } catch (const incorrect_access_key& e) { send_command(c, 0x04, 0x03); c->should_disconnect = true; return; } catch (const incorrect_password& e) { send_command(c, 0x04, 0x06); c->should_disconnect = true; return; } catch (const missing_license& e) { // On V3, the client should have sent a different command containing the // password already, which should have created and added a temporary // license. So, if no license exists at this point, disconnect the client // even if unregistered clients are allowed. shared_ptr l; if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) { send_command(c, 0x04, 0x04); c->should_disconnect = true; return; } else if (c->version == GameVersion::PC) { l = LicenseManager::create_license_pc(serial_number, base_cmd->access_key, true); s->license_manager->add(l); c->set_license(l); } else { throw runtime_error("unsupported game version"); } } if ((c->flags & Client::Flag::EPISODE_3) && (s->ep3_menu_song >= 0)) { send_ep3_change_music(c, s->ep3_menu_song); } send_update_client_config(c); process_login_complete(s, c); } void process_login_bb(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 93 const auto& cmd = check_size_t(data, sizeof(C_Login_BB_93) - 8, sizeof(C_Login_BB_93)); bool is_old_format; if (data.size() == sizeof(C_Login_BB_93) - 8) { is_old_format = true; } else if (data.size() == sizeof(C_Login_BB_93)) { is_old_format = false; } else { throw runtime_error("invalid size for 93 command"); } c->flags |= flags_for_version(c->version, -1); try { auto l = s->license_manager->verify_bb(cmd.username, cmd.password); c->set_license(l); } catch (const incorrect_password& e) { u16string message = u"Login failed: " + decode_sjis(e.what()); send_message_box(c, message.c_str()); c->should_disconnect = true; return; } catch (const missing_license& e) { if (!s->allow_unregistered_users) { u16string message = u"Login failed: " + decode_sjis(e.what()); send_message_box(c, message.c_str()); c->should_disconnect = true; return; } else { shared_ptr l = LicenseManager::create_license_bb( fnv1a32(cmd.username) & 0x7FFFFFFF, cmd.username, cmd.password, true); s->license_manager->add(l); c->set_license(l); } } try { if (is_old_format) { c->import_config(cmd.var.old_clients_cfg.cfg); } else { c->import_config(cmd.var.new_clients.cfg.cfg); } if (c->bb_game_state < ClientStateBB::IN_GAME) { c->bb_game_state++; } } catch (const invalid_argument&) { c->bb_game_state = ClientStateBB::INITIAL_LOGIN; c->game_data.bb_player_index = 0; } if (cmd.menu_id == MenuID::LOBBY) { c->preferred_lobby_id = cmd.preferred_lobby_id; } send_client_init_bb(c, 0); switch (c->bb_game_state) { case ClientStateBB::INITIAL_LOGIN: // On first login, send the client to the data server port send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at("bb-data1")->port); break; case ClientStateBB::DOWNLOAD_DATA: case ClientStateBB::CHOOSE_PLAYER: case ClientStateBB::SAVE_PLAYER: // Just wait in these cases; the client will request something from us and // the command handlers will take care of it break; case ClientStateBB::SHIP_SELECT: process_login_complete(s, c); break; case ClientStateBB::IN_GAME: break; default: throw runtime_error("invalid bb game state"); } } void process_return_client_config(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // 9F if (c->version == GameVersion::BB) { const auto& cfg = check_size_t(data); c->import_config(cfg); } else { const auto& cfg = check_size_t(data); c->import_config(cfg); } } void process_client_checksum(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // 96 check_size_t(data); send_server_time(c); } void process_server_time_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // B1 check_size_v(data.size(), 0); send_server_time(c); // The B1 command is sent in response to a 97 command, which is normally part // of the pre-ship-select login sequence. However, newserv delays this until // after the ship select menu so that loading a GameCube program doesn't cause // the player's items to be deleted when they next play PSO. It's also not a // good idea to send a 97 and 19 at the same time, because the memory card and // BBA are on the same EXI bus on the GameCube and this seems to cause the SYN // packet after a 19 to get dropped pretty often, which causes a delay in // joining the lobby. This is why we delay the 19 command until the client // responds after saving. if (c->should_send_to_lobby_server) { static const vector version_to_port_name({ "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "xb-lobby", "bb-lobby"}); const auto& port_name = version_to_port_name.at(static_cast(c->version)); send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at(port_name)->port); } } //////////////////////////////////////////////////////////////////////////////// // Ep3 commands. Note that these commands are not at all functional. The command // handlers that partially worked were lost in a dead hard drive, unfortunately. void process_ep3_meseta_transaction(shared_ptr, shared_ptr c, uint16_t command, uint32_t, const string& data) { const auto& in_cmd = check_size_t(data); S_Meseta_GC_Ep3_BA out_cmd = {1000000, 1000000, in_cmd.request_token}; send_command(c, command, 0x03, &out_cmd, sizeof(out_cmd)); } void process_ep3_menu_challenge(shared_ptr, shared_ptr c, uint16_t, uint32_t flag, const string& data) { // DC check_size_v(data.size(), 0); if (flag != 0) { send_command(c, 0xDC, 0x00); } } void process_ep3_server_data_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // CA check_size_v(data.size(), 8, 0xFFFF); const PSOSubcommand* cmds = reinterpret_cast(data.data()); auto l = s->find_lobby(c->lobby_id); if (!l || !(l->flags & Lobby::Flag::EPISODE_3_ONLY) || !l->is_game()) { c->should_disconnect = true; return; } if (cmds[0].byte[0] != 0xB3) { c->should_disconnect = true; return; } switch (cmds[1].byte[0]) { // phase 1: map select case 0x40: send_ep3_map_list(s, l); break; case 0x41: send_ep3_map_data(s, l, cmds[4].dword); break; /*// phase 2: deck/name entry case 0x13: ti = FindTeam(s, c->teamID); memcpy(&ti->ep3game, ((DWORD)c->bufferin + 0x14), 0x2AC); CommandEp3InitChangeState(s, c, 1); break; case 0x1B: ti = FindTeam(s, c->teamID); memcpy(&ti->ep3names[*(BYTE*)((DWORD)c->bufferin + 0x24)], ((DWORD)c->bufferin + 0x14), 0x14); // NOTICE: may be 0x26 instead of 0x24 CommandEp3InitSendNames(s, c); break; case 0x14: ti = FindTeam(s, c->teamID); memcpy(&ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], ((DWORD)c->bufferin + 0x18), 0x58); // NOTICE: may be 0x16 instead of 0x14 Ep3FillHand(&ti->ep3game, &ti->ep3decks[*(BYTE*)((DWORD)c->bufferin + 0x14)], &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]); //Ep3RollDice(&ti->ep3game, &ti->ep3pcs[*(BYTE*)((DWORD)c->bufferin + 0x14)]); CommandEp3InitSendDecks(s, c); CommandEp3InitSendMapLayout(s, c); for (x = 0, param = 0; x < 4; x++) if ((ti->ep3decks[x].clientID != 0xFFFFFFFF) && (ti->ep3names[x].clientID != 0xFF)) param++; if (param >= ti->ep3game.numPlayers) CommandEp3InitChangeState(s, c, 3); break; // phase 3: hands & game states case 0x1D: ti = FindTeam(s, c->teamID); Ep3ReprocessMap(&ti->ep3game); CommandEp3SendMapData(s, c, ti->ep3game.mapID); for (y = 0, x = 0; x < 4; x++) { if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; Ep3EquipCard(&ti->ep3game, &ti->ep3decks[x], &ti->ep3pcs[x], 0); // equip SC card CommandEp3InitHandUpdate(s, c, x); CommandEp3InitStatUpdate(s, c, x); y++; } CommandEp3Init_B4_06(s, c, (y == 4) ? true : false); CommandEp3InitSendMapLayout(s, c); for (x = 0; x < 4; x++) { if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; CommandEp3Init_B4_4E(s, c, x); CommandEp3Init_B4_4C(s, c, x); CommandEp3Init_B4_4D(s, c, x); CommandEp3Init_B4_4F(s, c, x); } CommandEp3InitSendDecks(s, c); CommandEp3InitSendMapLayout(s, c); for (x = 0; x < 4; x++) { if ((ti->ep3decks[x].clientID == 0xFFFFFFFF) || (ti->ep3names[x].clientID == 0xFF)) continue; CommandEp3InitHandUpdate(s, c, x); } CommandEp3InitSendNames(s, c); CommandEp3InitChangeState(s, c, 4); CommandEp3Init_B4_50(s, c); CommandEp3InitSendMapLayout(s, c); CommandEp3Init_B4_39(s, c); // MISSING: 60 00 AC 00 B4 2A 00 00 39 56 00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 CommandEp3InitBegin(s, c); break; */ default: c->log.error("Unknown Episode III server data request: %02X", cmds[1].byte[0]); } } void process_ep3_tournament_control(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string&) { // E2 // The client will get stuck here unless we send something. An 01 (lobby // message box) seems to get them unstuck. send_lobby_message_box(c, u"$C6Tournaments are\nnot supported."); // In case we ever implement this (doubtful), the flag values are: // 00 - list tournaments // 01 - check tournament entry status // 02 - cancel tournament entry // 03 - create tournament spectator team (presumably get battle list, like get team list) // 04 - join tournament spectator team (presumably also get battle list) } //////////////////////////////////////////////////////////////////////////////// // menu commands void process_message_box_closed(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // D6 check_size_v(data.size(), 0); if (c->flags & Client::Flag::IN_INFORMATION_MENU) { send_menu(c, u"Information", MenuID::INFORMATION, *s->information_menu_for_version(c->version)); } else if (c->flags & Client::Flag::AT_WELCOME_MESSAGE) { send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE; send_update_client_config(c); } } void process_menu_item_info_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 09 const auto& cmd = check_size_t(data); switch (cmd.menu_id) { case MenuID::MAIN: for (const auto& item : s->main_menu) { if (item.item_id == cmd.item_id) { send_ship_info(c, item.description); } } break; case MenuID::INFORMATION: if (cmd.item_id == InformationMenuItemID::GO_BACK) { send_ship_info(c, u"Return to the\nmain menu."); } else { try { // we use item_id + 1 here because "go back" is the first item send_ship_info(c, s->information_menu_for_version(c->version)->at(cmd.item_id + 1).description.c_str()); } catch (const out_of_range&) { send_ship_info(c, u"$C4Missing information\nmenu item"); } } break; case MenuID::PROXY_DESTINATIONS: if (cmd.item_id == ProxyDestinationsMenuItemID::GO_BACK) { send_ship_info(c, u"Return to the\nmain menu."); } else { try { const auto& menu = s->proxy_destinations_menu_for_version(c->version); // we use item_id + 1 here because "go back" is the first item send_ship_info(c, menu.at(cmd.item_id + 1).description.c_str()); } catch (const out_of_range&) { send_ship_info(c, u"$C4Missing proxy\ndestination"); } } break; case MenuID::QUEST_FILTER: // Don't send anything here. The quest filter menu already has short // descriptions included with the entries, which the client shows in the // usual location on the screen. break; case MenuID::QUEST: { if (!s->quest_index) { send_quest_info(c, u"$C6Quests are not available.", !c->lobby_id); break; } auto q = s->quest_index->get(c->version, cmd.item_id); if (!q) { send_quest_info(c, u"$C4Quest does not\nexist.", !c->lobby_id); break; } send_quest_info(c, q->long_description.c_str(), !c->lobby_id); break; } case MenuID::GAME: { shared_ptr game; try { game = s->find_lobby(cmd.item_id); } catch (const out_of_range& e) { send_ship_info(c, u"$C4Game no longer\nexists."); break; } if (!game->is_game()) { send_ship_info(c, u"$C4Incorrect game ID"); } else { string info; for (size_t x = 0; x < game->max_clients; x++) { const auto& game_c = game->clients[x]; if (game_c.get()) { auto player = game_c->game_data.player(); auto name = encode_sjis(player->disp.name); if (game->flags & Lobby::Flag::EPISODE_3_ONLY) { info += string_printf("%zu: $C6%s$C7 L%" PRIu32 "\n", x + 1, name.c_str(), player->disp.level + 1); } else { info += string_printf("%zu: $C6%s$C7 %s L%" PRIu32 "\n", x + 1, name.c_str(), abbreviation_for_char_class(player->disp.char_class), player->disp.level + 1); } } } int episode = game->episode; if (episode == 3) { episode = 4; } else if (episode == 0xFF) { episode = 3; } string secid_str = name_for_section_id(game->section_id); info += string_printf("Ep%d %c %s %s\n", episode, abbreviation_for_difficulty(game->difficulty), abbreviation_for_game_mode(game->mode), secid_str.c_str()); bool cheats_enabled = game->flags & Lobby::Flag::CHEATS_ENABLED; bool locked = !game->password.empty(); if (cheats_enabled && locked) { info += "$C4Locked$C7, $C6cheats enabled$C7\n"; } else if (cheats_enabled) { info += "$C6Cheats enabled$C7\n"; } else if (locked) { info += "$C4Locked$C7\n"; } if (game->loading_quest) { if (game->flags & Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) { info += "$C6Quest: " + encode_sjis(game->loading_quest->name); } else { info += "$C4Quest: " + encode_sjis(game->loading_quest->name); } } else if (game->flags & Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) { info += "$C6Quest in progress"; } else if (game->flags & Lobby::Flag::QUEST_IN_PROGRESS) { info += "$C4Quest in progress"; } send_ship_info(c, decode_sjis(info)); } break; } case MenuID::PATCHES: // TODO: Find a way to provide desccriptions for patches. break; case MenuID::PROGRAMS: { if (cmd.item_id == ProgramsMenuItemID::GO_BACK) { send_ship_info(c, u"Return to the\nmain menu."); } else { try { auto dol = s->dol_file_index->item_id_to_file.at(cmd.item_id); string size_str = format_size(dol->data.size()); string info = string_printf("$C6%s$C7\n%s", dol->name.c_str(), size_str.c_str()); send_ship_info(c, decode_sjis(info)); } catch (const out_of_range&) { send_ship_info(c, u"Incorrect program ID."); } } break; } default: send_ship_info(c, u"Incorrect menu ID."); break; } } void process_menu_selection(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 10 bool uses_unicode = ((c->version == GameVersion::PC) || (c->version == GameVersion::BB)); uint32_t menu_id; uint32_t item_id; u16string password; if (data.size() > sizeof(C_MenuSelection_10_Flag00)) { if (uses_unicode) { const auto& cmd = check_size_t(data); password = cmd.password; menu_id = cmd.menu_id; item_id = cmd.item_id; } else { const auto& cmd = check_size_t(data); password = decode_sjis(cmd.password); menu_id = cmd.menu_id; item_id = cmd.item_id; } } else { const auto& cmd = check_size_t(data); menu_id = cmd.menu_id; item_id = cmd.item_id; } switch (menu_id) { case MenuID::MAIN: { switch (item_id) { case MainMenuItemID::GO_TO_LOBBY: { 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); } else { static const vector version_to_port_name({ "dc-lobby", "pc-lobby", "bb-lobby", "gc-lobby", "xb-lobby", "bb-lobby"}); const auto& port_name = version_to_port_name.at(static_cast(c->version)); send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at(port_name)->port); } break; } case MainMenuItemID::INFORMATION: send_menu(c, u"Information", MenuID::INFORMATION, *s->information_menu_for_version(c->version)); c->flags |= Client::Flag::IN_INFORMATION_MENU; break; case MainMenuItemID::PROXY_DESTINATIONS: send_menu(c, u"Proxy server", MenuID::PROXY_DESTINATIONS, s->proxy_destinations_menu_for_version(c->version)); break; case MainMenuItemID::DOWNLOAD_QUESTS: if (c->flags & Client::Flag::EPISODE_3) { shared_ptr l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr; auto quests = s->quest_index->filter( c->version, false, QuestCategory::EPISODE_3); if (quests.empty()) { send_lobby_message_box(c, u"$C6There are no quests\navailable."); } else { // Episode 3 has only download quests, not online quests, so this // is always the download quest menu. (Episode 3 does actually // have online quests, but they don't use the file download // paradigm that all other versions use.) send_quest_menu(c, MenuID::QUEST, quests, true); } } else { send_quest_menu(c, MenuID::QUEST_FILTER, quest_download_menu, true); } break; case MainMenuItemID::PATCHES: send_menu(c, u"Patches", MenuID::PATCHES, s->function_code_index->patch_menu()); break; case MainMenuItemID::PROGRAMS: send_menu(c, u"Programs", MenuID::PROGRAMS, s->dol_file_index->menu()); break; case MainMenuItemID::DISCONNECT: c->should_disconnect = true; break; case MainMenuItemID::CLEAR_LICENSE: send_command(c, 0x9A, 0x04); c->should_disconnect = true; break; default: send_message_box(c, u"Incorrect menu item ID."); break; } break; } case MenuID::INFORMATION: { if (item_id == InformationMenuItemID::GO_BACK) { c->flags &= ~Client::Flag::IN_INFORMATION_MENU; send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { try { send_message_box(c, s->information_contents->at(item_id).c_str()); } catch (const out_of_range&) { send_message_box(c, u"$C6No such information exists."); } } break; } case MenuID::PROXY_DESTINATIONS: { if (item_id == ProxyDestinationsMenuItemID::GO_BACK) { send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { const pair* dest = nullptr; try { dest = &s->proxy_destinations_for_version(c->version).at(item_id); } catch (const out_of_range&) { } if (!dest) { send_message_box(c, u"$C6No such destination exists."); c->should_disconnect = true; } else { // TODO: We can probably avoid using client config and reconnecting the // client here; it's likely we could build a way to just directly link // the client to the proxy server instead (would have to provide // license/char name/etc. for remote auth) static const vector version_to_port_name({ "dc-proxy", "pc-proxy", "", "gc-proxy", "xb-proxy", "bb-proxy"}); const auto& port_name = version_to_port_name.at(static_cast(c->version)); uint16_t local_port = s->name_to_port_config.at(port_name)->port; c->proxy_destination_address = resolve_ipv4(dest->first); c->proxy_destination_port = dest->second; send_update_client_config(c); s->proxy_server->delete_session(c->license->serial_number); s->proxy_server->create_licensed_session( c->license, local_port, c->version, c->export_config_bb()); send_reconnect(c, s->connect_address_for_client(c), local_port); } } break; } case MenuID::GAME: { auto game = s->find_lobby(item_id); if (!game) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because it no\nlonger exists."); break; } if (!game->is_game()) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because it is\nnot a game."); break; } if (game->count_clients() >= game->max_clients) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because it is\nfull."); break; } if ((game->version != c->version) || (!(game->flags & Lobby::Flag::EPISODE_3_ONLY) != !(c->flags & Client::Flag::EPISODE_3))) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because it is\nfor a different\nversion of PSO."); break; } if (game->flags & Lobby::Flag::QUEST_IN_PROGRESS) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because a\nquest is already\nin progress."); break; } if (game->any_client_loading()) { send_lobby_message_box(c, u"$C6You cannot join this\ngame because\nanother player is\ncurrently loading.\nTry again soon."); break; } if (game->mode == 3) { send_lobby_message_box(c, u"$C6You cannot join this\n game because it is\na Solo Mode game."); break; } if (!(c->license->privileges & Privilege::FREE_JOIN_GAMES)) { if (!game->password.empty() && (password != game->password)) { send_message_box(c, u"$C6Incorrect password."); break; } if (c->game_data.player()->disp.level < game->min_level) { send_message_box(c, u"$C6Your level is too\nlow to join this\ngame."); break; } if (c->game_data.player()->disp.level > game->max_level) { send_message_box(c, u"$C6Your level is too\nhigh to join this\ngame."); break; } } if (!s->change_client_lobby(c, game)) { throw logic_error("client cannot join game after all preconditions satisfied"); } c->flags |= Client::Flag::LOADING; break; } case MenuID::QUEST_FILTER: { if (!s->quest_index) { send_lobby_message_box(c, u"$C6Quests are not available."); break; } shared_ptr l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr; auto quests = s->quest_index->filter(c->version, c->flags & Client::Flag::DCV1, static_cast(item_id & 0xFF)); if (quests.empty()) { send_lobby_message_box(c, u"$C6There are no quests\navailable in that\ncategory."); break; } // Hack: assume the menu to be sent is the download quest menu if the // client is not in any lobby send_quest_menu(c, MenuID::QUEST, quests, !c->lobby_id); break; } case MenuID::QUEST: { if (!s->quest_index) { send_lobby_message_box(c, u"$C6Quests are not available."); break; } auto q = s->quest_index->get(c->version, item_id); if (!q) { send_lobby_message_box(c, u"$C6Quest does not exist."); break; } // If the client is not in a lobby, send the quest as a download quest. // Otherwise, they must be in a game to load a quest. shared_ptr l; if (c->lobby_id) { l = s->find_lobby(c->lobby_id); if (!l->is_game()) { send_lobby_message_box(c, u"$C6Quests cannot be loaded\nin lobbies."); break; } } bool is_ep3 = (q->episode == 0xFF); string bin_basename = q->bin_filename(); shared_ptr bin_contents = q->bin_contents(); string dat_basename; shared_ptr dat_contents; if (!is_ep3) { dat_basename = q->dat_filename(); dat_contents = q->dat_contents(); } if (l) { if (is_ep3 || !dat_contents) { throw runtime_error("episode 3 quests cannot be loaded during games"); } if (q->joinable) { l->flags |= Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS; } else { l->flags |= Lobby::Flag::QUEST_IN_PROGRESS; } l->loading_quest = q; for (size_t x = 0; x < l->max_clients; x++) { if (!l->clients[x]) { continue; } // TODO: It looks like blasting all the chunks to the client at once // can cause GC clients to crash in rare cases. Find a way to slow // this down (perhaps by only sending each new chunk when they // acknowledge the previous chunk with a 44 [first chunk] or 13 [later // chunks] command). send_quest_file(l->clients[x], bin_basename + ".bin", bin_basename, *bin_contents, QuestFileType::ONLINE); send_quest_file(l->clients[x], dat_basename + ".dat", dat_basename, *dat_contents, QuestFileType::ONLINE); // There is no such thing as command AC on PSO PC - quests just start // immediately when they're done downloading. There are also no chunk // acknowledgements (C->S 13 commands) like there are on GC. So, for // PC clients, we can just not set the loading flag, since we never // need to check/clear it later. if (l->clients[x]->version != GameVersion::PC) { l->clients[x]->flags |= Client::Flag::LOADING_QUEST; } } } else { string quest_name = encode_sjis(q->name); // Episode 3 uses the download quest commands (A6/A7) but does not // expect the server to have already encrypted the quest files, unlike // other versions. if (!is_ep3) { q = q->create_download_quest(); } send_quest_file(c, quest_name, bin_basename, *q->bin_contents(), is_ep3 ? QuestFileType::EPISODE_3 : QuestFileType::DOWNLOAD); if (dat_contents) { send_quest_file(c, quest_name, dat_basename, *q->dat_contents(), is_ep3 ? QuestFileType::EPISODE_3 : QuestFileType::DOWNLOAD); } } break; } case MenuID::PATCHES: if (item_id == PatchesMenuItemID::GO_BACK) { send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { if (c->flags & Client::Flag::DOES_NOT_SUPPORT_SEND_FUNCTION_CALL) { throw runtime_error("client does not support send_function_call"); } send_function_call( c, s->function_code_index->menu_item_id_to_patch_function.at(item_id)); send_menu(c, u"Patches", MenuID::PATCHES, s->function_code_index->patch_menu()); } break; case MenuID::PROGRAMS: if (item_id == ProgramsMenuItemID::GO_BACK) { send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { if (c->flags & Client::Flag::DOES_NOT_SUPPORT_SEND_FUNCTION_CALL) { throw runtime_error("client does not support send_function_call"); } c->loading_dol_file = s->dol_file_index->item_id_to_file.at(item_id); // Send the first function call, which triggers the process of loading a // DOL file. This function call determines the necessary base address // for loading the file. send_function_call( c, s->function_code_index->name_to_function.at("ReadMemoryWord"), {{"address", 0x80000034}}); // ArenaHigh from GC globals } break; default: send_message_box(c, u"Incorrect menu ID."); break; } } void process_change_lobby(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 84 const auto& cmd = check_size_t(data); if (cmd.menu_id != MenuID::LOBBY) { send_message_box(c, u"Incorrect menu ID."); return; } // If the client isn't in any lobby, then they just left a game. Ignore their // selection and add them to any lobby with room. If they're already in a // lobby, then they used the lobby teleporter - add them to a specific lobby. if (c->lobby_id == 0) { c->preferred_lobby_id = cmd.item_id; s->add_client_to_available_lobby(c); // If the client already is in a lobby, then they're using the lobby // teleporter; add them to the lobby they requested or send a failure message. } else { shared_ptr new_lobby; try { new_lobby = s->find_lobby(cmd.item_id); } catch (const out_of_range&) { send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby does not\nexist."); return; } if ((new_lobby->flags & Lobby::Flag::EPISODE_3_ONLY) && !(c->flags & Client::Flag::EPISODE_3)) { send_lobby_message_box(c, u"$C6Can't change lobby\n\n$C7The lobby is for\nEpisode 3 only."); return; } if (!s->change_client_lobby(c, new_lobby)) { send_lobby_message_box(c, u"$C6Can\'t change lobby\n\n$C7The lobby is full."); } } } void process_game_list_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 08 check_size_v(data.size(), 0); send_game_menu(c, s); } void process_information_menu_request_pc(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 1F check_size_v(data.size(), 0); send_menu(c, u"Information", MenuID::INFORMATION, *s->information_menu_for_version(c->version), true); } void process_change_ship(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string&) { // A0 // The client actually sends data in this command... looks like nothing // important (player_tag and guild_card_number are the only discernable // things, which we already know). We intentionally don't call check_size // here, but instead just ignore the data. // Delete the player from the lobby they're in (but only visible to themself). // This makes it safe to allow the player to choose download quests from the // main menu again - if we didn't do this, they could move in the lobby after // canceling the download quests menu, which looks really bad. send_self_leave_notification(c); // Sending a blank message box here works around the bug where the log window // contents appear prepended to the next large message box. send_message_box(c, u""); static const vector version_to_port_name({ "dc-login", "pc-login", "bb-patch", "gc-us3", "xb-login", "bb-init"}); const auto& port_name = version_to_port_name.at(static_cast(c->version)); send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at(port_name)->port); } void process_change_block(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, const string& data) { // A1 // newserv doesn't have blocks; treat block change the same as ship change process_change_ship(s, c, command, flag, data); } //////////////////////////////////////////////////////////////////////////////// // DOL loading commands static void send_dol_file_chunk(shared_ptr s, shared_ptr c, uint32_t start_addr) { size_t offset = start_addr - c->dol_base_addr; if (offset >= c->loading_dol_file->data.size()) { throw logic_error("DOL file offset beyond end of data"); } size_t bytes_to_send = min(0x7800, c->loading_dol_file->data.size() - offset); string data_to_send = c->loading_dol_file->data.substr(offset, bytes_to_send); auto fn = s->function_code_index->name_to_function.at("WriteMemory"); unordered_map label_writes( {{"dest_addr", start_addr}, {"size", bytes_to_send}}); send_function_call(c, fn, label_writes, data_to_send); size_t progress_percent = ((offset + bytes_to_send) * 100) / c->loading_dol_file->data.size(); string info = string_printf("Loading $C6%s$C7\n%zu%%%% complete", c->loading_dol_file->name.c_str(), progress_percent); send_ship_info(c, decode_sjis(info)); } void process_function_call_result(shared_ptr s, shared_ptr c, uint16_t, uint32_t flag, const string& data) { // B3 const auto& cmd = check_size_t(data); if (flag == 0) { return; } auto called_fn = s->function_code_index->index_to_function.at(flag); if (c->loading_dol_file.get()) { if (called_fn->name == "ReadMemoryWord") { c->dol_base_addr = (cmd.return_value - c->loading_dol_file->data.size()) & (~3); send_dol_file_chunk(s, c, c->dol_base_addr); } else if (called_fn->name == "WriteMemory") { if (cmd.return_value >= c->dol_base_addr + c->loading_dol_file->data.size()) { auto fn = s->function_code_index->name_to_function.at("RunDOL"); unordered_map label_writes( {{"dol_base_ptr", c->dol_base_addr}}); send_function_call(c, fn, label_writes); // The client will stop running PSO after this, so disconnect them c->should_disconnect = true; } else { send_dol_file_chunk(s, c, cmd.return_value); } } } } //////////////////////////////////////////////////////////////////////////////// // Quest commands void process_quest_list_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t flag, const string& data) { // A2 check_size_v(data.size(), 0); if (!s->quest_index) { send_lobby_message_box(c, u"$C6Quests are not available."); return; } auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game()) { send_lobby_message_box(c, u"$C6Quests are not available\nin lobbies."); return; } // In Episode 3, there are no quest categories, so skip directly to the quest // filter menu. if (c->flags & Client::Flag::EPISODE_3) { send_lobby_message_box(c, u"$C6Episode 3 does not\nprovide online quests\nvia this interface."); } else { vector* menu = nullptr; if ((c->version == GameVersion::BB) && flag) { menu = &quest_government_menu; } else { if (l->mode == 0) { menu = &quest_categories_menu; } else if (l->mode == 1) { menu = &quest_battle_menu; } else if (l->mode == 2) { menu = &quest_challenge_menu; } else if (l->mode == 3) { menu = &quest_solo_menu; } else { throw logic_error("no quest menu available for mode"); } } send_quest_menu(c, MenuID::QUEST_FILTER, *menu, false); } } void process_quest_barrier(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // AC check_size_v(data.size(), 0); auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game()) { return; } // If this client is NOT loading, they should not send an AC. Sending an AC to // a client that isn't waiting to start a quest will crash the client, so we // have to be careful not to do so. if (!(c->flags & Client::Flag::LOADING_QUEST)) { return; } c->flags &= ~Client::Flag::LOADING_QUEST; // check if any client is still loading // TODO: we need to handle clients disconnecting while loading. probably // process_client_disconnect needs to check for this case or something size_t x; for (x = 0; x < l->max_clients; x++) { if (!l->clients[x]) { continue; } if (l->clients[x]->flags & Client::Flag::LOADING_QUEST) { break; } } // if they're all done, start the quest if (x == l->max_clients) { send_command(l, 0xAC, 0x00); } } void process_update_quest_statistics(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // AA const auto& cmd = check_size_t(data); auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game() || !l->loading_quest.get() || (l->loading_quest->internal_id != cmd.quest_internal_id)) { return; } S_ConfirmUpdateQuestStatistics_AB response; response.unknown_a1 = 0x0000; response.unknown_a2 = 0x0000; response.request_token = cmd.request_token; response.unknown_a3 = 0xBFFF; send_command_t(c, 0xAB, 0x00, response); } void process_gba_file_request(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // D7 string filename(data); strip_trailing_zeroes(filename); static FileContentsCache gba_file_cache(300 * 1000 * 1000); auto f = gba_file_cache.get_or_load("system/gba/" + filename).file; send_quest_file(c, "", filename, f->data, QuestFileType::GBA_DEMO); } //////////////////////////////////////////////////////////////////////////////// // player data commands void process_player_data(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, const string& data) { // 61 98 // Note: we add extra buffer on the end when checking sizes because the // autoreply text is a variable length switch (c->version) { case GameVersion::PC: { const auto& pd = check_size_t(data, sizeof(PSOPlayerDataPC), 0xFFFF); c->game_data.import_player(pd); break; } case GameVersion::GC: case GameVersion::XB: { const PSOPlayerDataV3* pd; if (flag == 4) { // Episode 3 if (!(c->flags & Client::Flag::EPISODE_3)) { throw runtime_error("non-Episode 3 client sent Episode 3 player data"); } const auto* pd3 = &check_size_t(data); c->game_data.ep3_config.reset(new Ep3Config(pd3->ep3_config)); pd = reinterpret_cast(pd3); } else { pd = &check_size_t(data, sizeof(PSOPlayerDataV3), sizeof(PSOPlayerDataV3) + c->game_data.player()->auto_reply.bytes()); } c->game_data.import_player(*pd); break; } case GameVersion::BB: { const auto& pd = check_size_t(data, sizeof(PSOPlayerDataBB), sizeof(PSOPlayerDataBB) + c->game_data.player()->auto_reply.bytes()); c->game_data.import_player(pd); break; } default: throw logic_error("player data command not implemented for version"); } auto player = c->game_data.player(false); if (player) { string name_str = remove_language_marker(encode_sjis(player->disp.name)); c->channel.name = string_printf("C-%" PRIX64 " (%s)", c->id, name_str.c_str()); } // 98 should only be sent when leaving a game, and we should leave the client // in no lobby (they will send an 84 soon afterward to choose a lobby). if (command == 0x98) { s->remove_client_from_lobby(c); } else if (command == 0x61) { if (!c->pending_bb_save_username.empty()) { string prev_bb_username = c->game_data.bb_username; size_t prev_bb_player_index = c->game_data.bb_player_index; c->game_data.bb_username = c->pending_bb_save_username; c->game_data.bb_player_index = c->pending_bb_save_player_index; bool failure = false; try { c->game_data.save_player_data(); } catch (const exception& e) { u16string buffer = u"$C6PSOBB player data could\nnot be saved:\n" + decode_sjis(e.what()); send_text_message(c, buffer.c_str()); failure = true; } if (!failure) { send_text_message_printf(c, "$C6BB player data saved\nas player %hhu for user\n%s", static_cast(c->pending_bb_save_player_index + 1), c->pending_bb_save_username.c_str()); } c->game_data.bb_username = prev_bb_username; c->game_data.bb_player_index = prev_bb_player_index; c->pending_bb_save_username.clear(); } // We use 61 during the lobby server init sequence to trigger joining an // available lobby if (!c->lobby_id && (c->server_behavior == ServerBehavior::LOBBY_SERVER)) { s->add_client_to_available_lobby(c); } } } //////////////////////////////////////////////////////////////////////////////// // subcommands void process_game_command(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, const string& data) { // 60 62 6C 6D C9 CB (C9 CB are ep3 only) check_size_v(data.size(), 4, 0xFFFF); auto l = s->find_lobby(c->lobby_id); if (!l) { return; } process_subcommand(s, l, c, command, flag, data); } //////////////////////////////////////////////////////////////////////////////// // chat commands void process_chat_generic(shared_ptr s, shared_ptr c, const u16string& text) { // 06 u16string processed_text = remove_language_marker(text); if (processed_text.empty()) { return; } auto l = s->find_lobby(c->lobby_id); if (!l) { return; } if (processed_text[0] == L'$') { if (processed_text[1] == L'$') { processed_text = processed_text.substr(1); } else { process_chat_command(s, l, c, processed_text); return; } } if (!c->can_chat) { return; } for (size_t x = 0; x < l->max_clients; x++) { if (!l->clients[x]) { continue; } send_chat_message(l->clients[x], c->license->serial_number, c->game_data.player()->disp.name.data(), processed_text.c_str()); } } void process_chat_pc_bb(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 06 const auto& cmd = check_size_t(data, sizeof(C_Chat_06), 0xFFFF); u16string text(cmd.text.pcbb, (data.size() - sizeof(C_Chat_06)) / sizeof(char16_t)); strip_trailing_zeroes(text); process_chat_generic(s, c, text); } void process_chat_dc_v3(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { const auto& cmd = check_size_t(data, sizeof(C_Chat_06), 0xFFFF); u16string decoded_s = decode_sjis(cmd.text.dcv3, data.size() - sizeof(C_Chat_06)); process_chat_generic(s, c, decoded_s); } //////////////////////////////////////////////////////////////////////////////// // BB commands void process_key_config_request_bb(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { check_size_v(data.size(), 0); send_team_and_key_config_bb(c); } void process_player_preview_request_bb(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { const auto& cmd = check_size_t(data); if (c->bb_game_state == ClientStateBB::CHOOSE_PLAYER) { c->game_data.bb_player_index = cmd.player_index; c->bb_game_state++; send_client_init_bb(c, 0); send_approve_player_choice_bb(c); } else { if (!c->license) { c->should_disconnect = true; return; } ClientGameData temp_gd; temp_gd.serial_number = c->license->serial_number; temp_gd.bb_username = c->license->username; 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& e) { // Player doesn't exist send_player_preview_bb(c, cmd.player_index, nullptr); } } } void process_client_checksum_bb(shared_ptr, shared_ptr c, uint16_t command, uint32_t, const string& data) { constexpr size_t max_count = sizeof(GuildCardFileBB::entries) / sizeof(GuildCardEntryBB); constexpr size_t max_blocked = sizeof(GuildCardFileBB::blocked) / sizeof(GuildCardBB); switch (command) { case 0x01E8: { // Check guild card file checksum const auto& cmd = check_size_t(data); uint32_t checksum = c->game_data.account()->guild_cards.checksum(); c->log.info("(Guild card file) Server checksum = %08" PRIX32 ", client checksum = %08" PRIX32, checksum, cmd.checksum.load()); S_GuildCardChecksumResponse_BB_02E8 response = { (cmd.checksum != checksum), 0}; send_command_t(c, 0x02E8, 0x00000000, response); break; } case 0x03E8: // Download guild card file check_size_v(data.size(), 0); send_guild_card_header_bb(c); break; case 0x04E8: { // Add guild card auto& new_gc = check_size_t(data); auto& gcf = c->game_data.account()->guild_cards; for (size_t z = 0; z < max_count; z++) { if (!gcf.entries[z].data.present) { gcf.entries[z].data = new_gc; gcf.entries[z].unknown_a1.clear(0); c->log.info("Added guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z); break; } } break; } case 0x05E8: { // Delete guild card auto& cmd = check_size_t(data); auto& gcf = c->game_data.account()->guild_cards; for (size_t z = 0; z < max_count; z++) { if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) { c->log.info("Deleted guild card %" PRIu32 " at position %zu", cmd.guild_card_number.load(), z); for (z = 0; z < max_count - 1; z++) { gcf.entries[z] = gcf.entries[z + 1]; } gcf.entries[max_count - 1].clear(); break; } } break; } case 0x06E8: { // Update guild card auto& new_gc = check_size_t(data); auto& gcf = c->game_data.account()->guild_cards; for (size_t z = 0; z < max_count; z++) { if (gcf.entries[z].data.guild_card_number == new_gc.guild_card_number) { gcf.entries[z].data = new_gc; c->log.info("Updated guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z); } } break; } case 0x07E8: { // Add blocked user auto& new_gc = check_size_t(data); auto& gcf = c->game_data.account()->guild_cards; for (size_t z = 0; z < max_blocked; z++) { if (!gcf.blocked[z].present) { gcf.blocked[z] = new_gc; c->log.info("Added blocked guild card %" PRIu32 " at position %zu", new_gc.guild_card_number.load(), z); // Note: The client also sends a C6 command, so we don't have to // manually sync the actual blocked senders list here break; } } break; } case 0x08E8: { // Delete blocked user auto& cmd = check_size_t(data); auto& gcf = c->game_data.account()->guild_cards; for (size_t z = 0; z < max_blocked; z++) { if (gcf.blocked[z].guild_card_number == cmd.guild_card_number) { c->log.info("Deleted blocked guild card %" PRIu32 " at position %zu", cmd.guild_card_number.load(), z); for (z = 0; z < max_blocked - 1; z++) { gcf.blocked[z] = gcf.blocked[z + 1]; } gcf.blocked[max_blocked - 1].clear(); // Note: The client also sends a C6 command, so we don't have to // manually sync the actual blocked senders list here break; } } break; } case 0x09E8: { // Write comment auto& cmd = check_size_t(data); auto& gcf = c->game_data.account()->guild_cards; for (size_t z = 0; z < max_count; z++) { if (gcf.entries[z].data.guild_card_number == cmd.guild_card_number) { gcf.entries[z].comment = cmd.comment; c->log.info("Updated comment on guild card %" PRIu32 " at position %zu", cmd.guild_card_number.load(), z); break; } } break; } case 0x0AE8: { // Move guild card in list auto& cmd = check_size_t(data); auto& gcf = c->game_data.account()->guild_cards; if (cmd.position >= max_count) { throw invalid_argument("invalid new position"); } size_t index; for (index = 0; index < max_count; index++) { if (gcf.entries[index].data.guild_card_number == cmd.guild_card_number) { break; } } if (index >= max_count) { throw invalid_argument("player does not have requested guild card"); } auto moved_gc = gcf.entries[index]; for (; index < cmd.position; index++) { gcf.entries[index] = gcf.entries[index + 1]; } for (; index > cmd.position; index--) { gcf.entries[index] = gcf.entries[index - 1]; } gcf.entries[index] = moved_gc; c->log.info("Moved guild card %" PRIu32 " to position %zu", cmd.guild_card_number.load(), index); break; } default: throw invalid_argument("invalid command"); } } 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); 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 flag, const string& data) { check_size_v(data.size(), 0); if (command == 0x04EB) { send_stream_file_index_bb(c); } else if (command == 0x03EB) { send_stream_file_chunk_bb(c, flag); } else { throw invalid_argument("unimplemented command"); } } 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); if (!c->license) { send_message_box(c, u"$C6You are not logged in."); return; } // 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; } c->game_data.bb_player_index = cmd.player_index; try { c->game_data.create_player(cmd.preview, s->level_table); } catch (const exception& e) { string message = string_printf("$C6New character could not be created:\n%s", e.what()); send_message_box(c, decode_sjis(message)); return; } send_client_init_bb(c, 0); send_approve_player_choice_bb(c); } void process_change_account_data_bb(shared_ptr, shared_ptr c, uint16_t command, uint32_t, const string& data) { const auto* cmd = reinterpret_cast(data.data()); switch (command) { case 0x01ED: check_size_v(data.size(), sizeof(cmd->option)); c->game_data.account()->option_flags = cmd->option; break; case 0x02ED: check_size_v(data.size(), sizeof(cmd->symbol_chats)); c->game_data.account()->symbol_chats = cmd->symbol_chats; break; case 0x03ED: check_size_v(data.size(), sizeof(cmd->chat_shortcuts)); c->game_data.account()->shortcuts = cmd->chat_shortcuts; break; case 0x04ED: check_size_v(data.size(), sizeof(cmd->key_config)); c->game_data.account()->key_config.key_config = cmd->key_config; break; case 0x05ED: check_size_v(data.size(), sizeof(cmd->pad_config)); c->game_data.account()->key_config.joystick_config = cmd->pad_config; break; case 0x06ED: check_size_v(data.size(), sizeof(cmd->tech_menu)); c->game_data.player()->tech_menu_config = cmd->tech_menu; break; case 0x07ED: check_size_v(data.size(), sizeof(cmd->customize)); c->game_data.player()->disp.config = cmd->customize; break; default: throw invalid_argument("unknown account command"); } } void process_return_player_data_bb(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // 00E7 const auto& cmd = check_size_t(data); // We only trust the player's quest data and challenge data. c->game_data.player()->challenge_data = cmd.challenge_data; c->game_data.player()->quest_data1 = cmd.quest_data1; c->game_data.player()->quest_data2 = cmd.quest_data2; } void process_update_key_config_bb(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // Some clients have only a uint32_t at the end for team rewards auto& cmd = check_size_t(data, sizeof(KeyAndTeamConfigBB) - 4, sizeof(KeyAndTeamConfigBB)); c->game_data.account()->key_config = cmd; // TODO: We should send a response here, but I don't know which one! } //////////////////////////////////////////////////////////////////////////////// // Lobby commands void process_change_arrow_color(shared_ptr s, shared_ptr c, uint16_t, uint32_t flag, const string& data) { // 89 check_size_v(data.size(), 0); c->lobby_arrow_color = flag; auto l = s->find_lobby(c->lobby_id); if (l) { send_arrow_update(l); } } void process_card_search(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 40 const auto& cmd = check_size_t(data); try { auto result = s->find_client(nullptr, cmd.target_guild_card_number); auto result_lobby = s->find_lobby(result->lobby_id); send_card_search_result(s, c, result, result_lobby); } catch (const out_of_range&) { } } void process_choice_search(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string&) { // C0 // TODO: Implement choice search. send_text_message(c, u"$C6Choice Search is\nnot supported"); } void process_simple_mail(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 81 u16string message; uint32_t to_guild_card_number; if ((c->version == GameVersion::GC) || (c->version == GameVersion::XB)) { const auto& cmd = check_size_t(data); to_guild_card_number = cmd.to_guild_card_number; message = decode_sjis(cmd.text); } else if (c->version == GameVersion::PC) { const auto& cmd = check_size_t(data); to_guild_card_number = cmd.to_guild_card_number; message = cmd.text; } else if (c->version == GameVersion::BB) { const auto& cmd = check_size_t(data); to_guild_card_number = cmd.to_guild_card_number; message = cmd.text; } else { // TODO send_text_message(c, u"$C6Simple Mail is not\nsupported yet on\nthis platform."); return; } auto target = s->find_client(nullptr, to_guild_card_number); // If the sender is blocked, don't forward the mail for (size_t y = 0; y < 30; y++) { if (target->game_data.account()->blocked_senders.data()[y] == c->license->serial_number) { return; } } // If the target has auto-reply enabled, send the autoreply if (!target->game_data.player()->auto_reply.empty()) { send_simple_mail(c, target->license->serial_number, target->game_data.player()->disp.name, target->game_data.player()->auto_reply); } // Forward the message send_simple_mail( target, c->license->serial_number, c->game_data.player()->disp.name, message); } //////////////////////////////////////////////////////////////////////////////// // Info board commands void process_info_board_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // D8 check_size_v(data.size(), 0); send_info_board(c, s->find_lobby(c->lobby_id)); } template void process_write_info_board_t(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // D9 check_size_v(data.size(), 0, c->game_data.player()->info_board.size() * sizeof(CharT)); c->game_data.player()->info_board.assign( reinterpret_cast(data.data()), data.size() / sizeof(CharT)); } template void process_set_auto_reply_t(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // C7 check_size_v(data.size(), 0, c->game_data.player()->auto_reply.size() * sizeof(CharT)); c->game_data.player()->auto_reply.assign( reinterpret_cast(data.data()), data.size() / sizeof(CharT)); } void process_disable_auto_reply(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // C8 check_size_v(data.size(), 0); c->game_data.player()->auto_reply.clear(0); } void process_set_blocked_senders_list(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // C6 if (c->version == GameVersion::BB) { const auto& cmd = check_size_t(data); c->game_data.account()->blocked_senders = cmd.blocked_senders; } else { const auto& cmd = check_size_t(data); c->game_data.account()->blocked_senders = cmd.blocked_senders; } } //////////////////////////////////////////////////////////////////////////////// // Game commands shared_ptr create_game_generic(shared_ptr s, shared_ptr c, const std::u16string& name, const std::u16string& password, uint8_t episode, uint8_t difficulty, uint8_t battle, uint8_t challenge, uint8_t solo) { // 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 {0, 39, 79, 109}}; // episode 4 if (episode == 0) { episode = 0xFF; } if (((episode != 0xFF) && (episode > 3)) || (episode == 0)) { throw invalid_argument("incorrect episode number"); } bool is_ep3 = (episode == 0xFF); if (difficulty > 3) { throw invalid_argument("incorrect difficulty level"); } auto current_lobby = s->find_lobby(c->lobby_id); if (!current_lobby) { throw invalid_argument("cannot make a game from outside any lobby"); } uint8_t min_level = ((episode == 0xFF) ? 0 : default_minimum_levels[episode - 1][difficulty]); if (!(c->license->privileges & Privilege::FREE_JOIN_GAMES) && (min_level > c->game_data.player()->disp.level)) { throw invalid_argument("level too low for difficulty"); } bool item_tracking_enabled = (c->version == GameVersion::BB) | s->item_tracking_enabled; shared_ptr game = s->create_lobby(); game->name = name; game->password = password; game->version = c->version; game->section_id = c->override_section_id >= 0 ? c->override_section_id : c->game_data.player()->disp.section_id; game->episode = episode; game->difficulty = difficulty; if (battle) { game->mode = 1; } if (challenge) { game->mode = 2; } if (solo) { game->mode = 3; } if (c->override_random_seed >= 0) { game->random_seed = c->override_random_seed; game->random->seed(game->random_seed); } game->common_item_creator.reset(new CommonItemCreator( s->common_item_data, game->random)); game->event = Lobby::game_event_for_lobby_event(current_lobby->event); game->block = 0xFF; game->max_clients = 4; game->flags = (is_ep3 ? Lobby::Flag::EPISODE_3_ONLY : 0) | (item_tracking_enabled ? Lobby::Flag::ITEM_TRACKING_ENABLED : 0) | Lobby::Flag::GAME; game->min_level = min_level; game->max_level = 0xFFFFFFFF; if (game->version == GameVersion::BB) { // TODO: cache these somewhere so we don't read the file every time, lolz game->rare_item_set.reset(new RareItemSet("system/blueburst/ItemRT.rel", game->episode - 1, game->difficulty, game->section_id)); for (size_t x = 0; x < 4; x++) { game->next_item_id[x] = (0x00200000 * x) + 0x00010000; } game->next_game_item_id = 0x00810000; auto bp_subtable = s->battle_params->get_subtable(game->mode == 3, game->episode - 1, game->difficulty); generate_variations( game->variations, game->random, game->episode, game->mode == 3); for (size_t x = 0; x < 0x10; x++) { try { auto file = map_data_for_variation( game->episode, game->mode == 3, x, game->variations[x * 2 + 0], game->variations[x * 2 + 1]); auto area_enemies = parse_map( game->episode, game->difficulty, bp_subtable, file->data.data(), file->data.size(), false); game->enemies.insert( game->enemies.end(), area_enemies.begin(), area_enemies.end()); c->log.info("Loaded map for area %zu (%zu entries)", x, area_enemies.size()); for (size_t z = 0; z < area_enemies.size(); z++) { string e_str = area_enemies[z].str(); static_game_data_log.info("(Entry %zu) %s", z, e_str.c_str()); } } catch (const exception& e) { c->log.warning("Failed to load map for area %zu: %s", x, e.what()); } } if (game->enemies.empty()) { throw runtime_error("failed to load any map data"); } c->log.info("Loaded maps contain %zu entries overall", game->enemies.size()); } else if (is_ep3) { game->variations.clear(0); } else { // In non-BB non-Ep3 games, just set the variations (we don't track enemies) generate_variations(game->variations, game->random, game->episode, false); } s->change_client_lobby(c, game); c->flags |= Client::Flag::LOADING; return game; } void process_create_game_pc(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // C1 const auto& cmd = check_size_t(data); create_game_generic(s, c, cmd.name, cmd.password, 1, cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, 0); } void process_create_game_dc_v3(shared_ptr s, shared_ptr c, uint16_t command, uint32_t, const string& data) { // C1 EC (EC Ep3 only) const auto& cmd = check_size_t(data); // only allow EC from Ep3 clients bool client_is_ep3 = c->flags & Client::Flag::EPISODE_3; if ((command == 0xEC) && !client_is_ep3) { return; } uint8_t episode = cmd.episode; if ((c->version == GameVersion::DC) || (c->version == GameVersion::PC)) { episode = 1; } if (client_is_ep3) { episode = 0xFF; } u16string name = decode_sjis(cmd.name); u16string password = decode_sjis(cmd.password); create_game_generic(s, c, name.c_str(), password.c_str(), episode, cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, 0); } void process_create_game_bb(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // C1 const auto& cmd = check_size_t(data); create_game_generic(s, c, cmd.name, cmd.password, cmd.episode, cmd.difficulty, cmd.battle_mode, cmd.challenge_mode, cmd.solo_mode); } void process_lobby_name_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 8A check_size_v(data.size(), 0); auto l = s->find_lobby(c->lobby_id); if (!l) { throw invalid_argument("client not in any lobby"); } send_lobby_name(c, l->name.c_str()); } void process_client_ready(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // 6F check_size_v(data.size(), 0); auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game()) { // go home client; you're drunk throw invalid_argument("ready command cannot be sent outside game"); } c->flags &= (~Client::Flag::LOADING); send_resume_game(l, c); send_server_time(c); // Only get player info again on BB, since on other versions the returned info // only includes items that would be saved if the client disconnects // unexpectedly (that is, only equipped items are included). if (c->version == GameVersion::BB) { send_get_player_info(c); } } //////////////////////////////////////////////////////////////////////////////// // Trade window commands void process_trade_start(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // D0 auto& cmd = check_size_t(data); if (c->game_data.pending_item_trade) { throw runtime_error("player started a trade when one is already pending"); } if (cmd.item_count > 0x20) { throw runtime_error("invalid item count in trade items command"); } auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game()) { throw runtime_error("trade command received in non-game lobby"); } auto target_c = l->clients.at(cmd.target_client_id); if (!target_c) { throw runtime_error("trade command sent to missing player"); } c->game_data.pending_item_trade.reset(new PendingItemTrade()); c->game_data.pending_item_trade->other_client_id = cmd.target_client_id; for (size_t x = 0; x < cmd.item_count; x++) { c->game_data.pending_item_trade->items.emplace_back(cmd.items[x]); } // If the other player has a pending trade as well, assume this is the second // half of the trade sequence, and send a D1 to both clients (which should // cause them to delete the appropriate inventory items and send D2s). If the // other player does not have a pending trade, assume this is the first half // of the trade sequence, and send a D1 only to the target player (to request // its D0 command). send_command(target_c, 0xD1, 0x00); if (target_c->game_data.pending_item_trade) { send_command(c, 0xD1, 0x00); } } void process_trade_execute(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // D2 check_size_v(data.size(), 0); if (!c->game_data.pending_item_trade) { throw runtime_error("player executed a trade with none pending"); } auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game()) { throw runtime_error("trade command received in non-game lobby"); } auto target_c = l->clients.at(c->game_data.pending_item_trade->other_client_id); if (!target_c) { throw runtime_error("target player is missing"); } if (!target_c->game_data.pending_item_trade) { throw runtime_error("player executed a trade with no other side pending"); } c->game_data.pending_item_trade->confirmed = true; if (target_c->game_data.pending_item_trade->confirmed) { send_execute_item_trade(c, target_c->game_data.pending_item_trade->items); send_execute_item_trade(target_c, c->game_data.pending_item_trade->items); send_command(c, 0xD4, 0x01); send_command(target_c, 0xD4, 0x01); c->game_data.pending_item_trade.reset(); target_c->game_data.pending_item_trade.reset(); } } void process_trade_error(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // D4 check_size_v(data.size(), 0); // Annoyingly, if the other client disconnects at a certain point during the // trade sequence, the client can get into a state where it sends this command // many times in a row. To deal with this, we just do nothing if the client // has no trade pending. if (!c->game_data.pending_item_trade) { return; } uint8_t other_client_id = c->game_data.pending_item_trade->other_client_id; c->game_data.pending_item_trade.reset(); send_command(c, 0xD4, 0x00); // Cancel the other side of the trade too, if it's open auto l = s->find_lobby(c->lobby_id); if (!l || !l->is_game()) { throw runtime_error("trade command received in non-game lobby"); } auto target_c = l->clients.at(other_client_id); if (!target_c) { return; } if (!target_c->game_data.pending_item_trade) { return; } target_c->game_data.pending_item_trade.reset(); send_command(target_c, 0xD4, 0x00); } //////////////////////////////////////////////////////////////////////////////// // Team commands void process_team_command_bb(shared_ptr, shared_ptr c, uint16_t command, uint32_t, const string&) { // EA 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"); } } //////////////////////////////////////////////////////////////////////////////// // Patch server commands void process_encryption_ok_patch(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { check_size_v(data.size(), 0); send_command(c, 0x04, 0x00); // This requests the user's login information } static void change_to_directory_patch( shared_ptr c, vector& client_path_directories, const vector& file_path_directories) { // First, exit all leaf directories that don't match the desired path while (!client_path_directories.empty() && ((client_path_directories.size() > file_path_directories.size()) || (client_path_directories.back() != file_path_directories[client_path_directories.size() - 1]))) { send_command(c, 0x0A, 0x00); client_path_directories.pop_back(); } // At this point, client_path_directories should be a prefix of // file_path_directories (or should match exactly) if (client_path_directories.size() > file_path_directories.size()) { throw logic_error("did not exit all necessary directories"); } for (size_t x = 0; x < client_path_directories.size(); x++) { if (client_path_directories[x] != file_path_directories[x]) { throw logic_error("intermediate path is not a prefix of final path"); } } // Second, enter all necessary leaf directories while (client_path_directories.size() < file_path_directories.size()) { const string& dir = file_path_directories[client_path_directories.size()]; send_enter_directory_patch(c, dir); client_path_directories.emplace_back(dir); } } void process_login_patch(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { check_size_v(data.size(), sizeof(C_Login_Patch_04)); // On BB we can use colors and newlines should be \n; on PC we can't use // colors, the text is auto-word-wrapped, and newlines should be \r\n. const u16string& message = (c->flags & Client::Flag::BB_PATCH) ? s->bb_patch_server_message : s->pc_patch_server_message; if (!message.empty()) { send_message_box(c, message.c_str()); } auto index = (c->flags & Client::Flag::BB_PATCH) ? s->bb_patch_file_index : s->pc_patch_file_index; if (index.get()) { send_command(c, 0x0B, 0x00); // Start patch session; go to root directory vector path_directories; for (const auto& file : index->files) { change_to_directory_patch(c, path_directories, file->path_directories); S_FileChecksumRequest_Patch_0C req = { c->patch_file_checksum_requests.size(), file->name}; send_command_t(c, 0x0C, 0x00, req); c->patch_file_checksum_requests.emplace_back(file); } change_to_directory_patch(c, path_directories, {}); send_command(c, 0x0D, 0x00); // End of checksum requests } else { // No patch index present: just do something that will satisfy the client // without actually checking or downloading any files send_enter_directory_patch(c, "."); send_enter_directory_patch(c, "data"); send_enter_directory_patch(c, "scene"); send_command(c, 0x0A, 0x00); send_command(c, 0x0A, 0x00); send_command(c, 0x0A, 0x00); send_command(c, 0x12, 0x00); } } void process_file_checksum_result_patch(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string& data) { // 0F auto& cmd = check_size_t(data); auto& req = c->patch_file_checksum_requests.at(cmd.request_id); req.crc32 = cmd.checksum; req.size = cmd.size; req.response_received = true; } void process_file_checksum_results_done_patch(shared_ptr, shared_ptr c, uint16_t, uint32_t, const string&) { // 10 S_StartFileDownloads_Patch_11 start_cmd = {0, 0}; for (const auto& req : c->patch_file_checksum_requests) { if (!req.response_received) { throw runtime_error("client did not respond to checksum request"); } if (req.needs_update()) { start_cmd.total_bytes += req.file->size; start_cmd.num_files++; } } if (start_cmd.num_files) { send_command_t(c, 0x11, 0x00, start_cmd); vector path_directories; for (const auto& req : c->patch_file_checksum_requests) { if (req.needs_update()) { change_to_directory_patch(c, path_directories, req.file->path_directories); send_patch_file(c, req.file); } } change_to_directory_patch(c, path_directories, {}); } send_command(c, 0x12, 0x00); } //////////////////////////////////////////////////////////////////////////////// // Command pointer arrays void process_ignored_command(shared_ptr, shared_ptr, uint16_t, uint32_t, const string&) { } void process_unimplemented_command(shared_ptr, shared_ptr c, uint16_t command, uint32_t flag, const string& data) { c->log.warning("Unknown command: size=%04zX command=%04hX flag=%08" PRIX32 "\n", data.size(), command, flag); throw invalid_argument("unimplemented command"); } typedef void (*process_command_t)(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, const string& data); // The entries in these arrays correspond to the ID of the command received. For // instance, if a command 6C is received, the function at position 0x6C in the // array corresponding to the client's version is called. static process_command_t dc_handlers[0x100] = { // 00 nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, process_chat_dc_v3, nullptr, process_game_list_request, process_menu_item_info_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 10 process_menu_selection, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, // 20 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 30 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 40 process_card_search, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 50 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 60 process_game_command, nullptr, process_game_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_game_command, process_game_command, nullptr, process_client_ready, // 70 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 80 nullptr, process_simple_mail, nullptr, nullptr, process_change_lobby, nullptr, nullptr, nullptr, nullptr, process_change_arrow_color, process_lobby_name_request, nullptr, nullptr, nullptr, nullptr, nullptr, // 90 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_client_checksum, nullptr, process_player_data, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // A0 process_change_ship, process_change_block, process_quest_list_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, process_update_quest_statistics, nullptr, nullptr, nullptr, nullptr, nullptr, // B0 nullptr, process_server_time_request, nullptr, process_function_call_result, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // C0 nullptr, process_create_game_dc_v3, nullptr, nullptr, nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t, process_disable_auto_reply, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // D0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_info_board_request, process_write_info_board_t, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // E0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // F0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }; static process_command_t pc_handlers[0x100] = { // 00 nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, process_chat_pc_bb, nullptr, process_game_list_request, process_menu_item_info_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 10 process_menu_selection, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, nullptr, process_information_menu_request_pc, // 20 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 30 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 40 process_card_search, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 50 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 60 process_game_command, process_player_data, process_game_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_game_command, process_game_command, nullptr, process_client_ready, // 70 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 80 nullptr, process_simple_mail, nullptr, nullptr, process_change_lobby, nullptr, nullptr, nullptr, nullptr, process_change_arrow_color, process_lobby_name_request, nullptr, nullptr, nullptr, nullptr, nullptr, // 90 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_client_checksum, nullptr, process_player_data, process_ignored_command, process_login_a_dc_pc_v3, nullptr, process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, nullptr, // A0 process_change_ship, process_change_block, process_quest_list_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, process_update_quest_statistics, nullptr, nullptr, nullptr, nullptr, nullptr, // B0 nullptr, process_server_time_request, nullptr, process_function_call_result, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // C0 nullptr, process_create_game_pc, nullptr, nullptr, nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t, process_disable_auto_reply, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // D0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_info_board_request, process_write_info_board_t, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // E0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // F0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }; static process_command_t gc_handlers[0x100] = { // 00 nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, process_chat_dc_v3, nullptr, process_game_list_request, process_menu_item_info_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 10 process_menu_selection, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, // 20 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 30 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 40 process_card_search, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 50 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 60 process_game_command, process_player_data, process_game_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_game_command, process_game_command, nullptr, process_client_ready, // 70 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 80 nullptr, process_simple_mail, nullptr, nullptr, process_change_lobby, nullptr, nullptr, nullptr, nullptr, process_change_arrow_color, process_lobby_name_request, nullptr, nullptr, nullptr, nullptr, nullptr, // 90 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_client_checksum, nullptr, process_player_data, process_ignored_command, nullptr, nullptr, process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, process_return_client_config, // A0 process_change_ship, process_change_block, process_quest_list_request, nullptr, nullptr, nullptr, process_ignored_command, process_ignored_command, nullptr, process_ignored_command, process_update_quest_statistics, nullptr, process_quest_barrier, nullptr, nullptr, nullptr, // B0 nullptr, process_server_time_request, nullptr, process_function_call_result, nullptr, nullptr, nullptr, process_ignored_command, process_ignored_command, nullptr, process_ep3_meseta_transaction, nullptr, nullptr, nullptr, nullptr, nullptr, // C0 process_choice_search, process_create_game_dc_v3, nullptr, nullptr, nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t, process_disable_auto_reply, process_game_command, process_ep3_server_data_request, process_game_command, nullptr, nullptr, nullptr, nullptr, // D0 process_trade_start, nullptr, process_trade_execute, nullptr, process_trade_error, nullptr, process_message_box_closed, process_gba_file_request, process_info_board_request, process_write_info_board_t, nullptr, process_verify_license_v3, process_ep3_menu_challenge, nullptr, nullptr, nullptr, // E0 nullptr, nullptr, process_ep3_tournament_control, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_create_game_dc_v3, nullptr, nullptr, nullptr, // F0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }; static process_command_t xb_handlers[0x100] = { // 00 nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, process_chat_dc_v3, nullptr, process_game_list_request, process_menu_item_info_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 10 process_menu_selection, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, // 20 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 30 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 40 process_card_search, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 50 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 60 process_game_command, process_player_data, process_game_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_game_command, process_game_command, nullptr, process_client_ready, // 70 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 80 nullptr, process_simple_mail, nullptr, nullptr, process_change_lobby, nullptr, nullptr, nullptr, nullptr, process_change_arrow_color, process_lobby_name_request, nullptr, nullptr, nullptr, nullptr, nullptr, // 90 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_client_checksum, nullptr, process_player_data, process_ignored_command, nullptr, nullptr, process_login_c_dc_pc_v3, process_login_d_e_pc_v3, process_login_d_e_pc_v3, process_return_client_config, // A0 process_change_ship, process_change_block, process_quest_list_request, nullptr, nullptr, nullptr, process_ignored_command, process_ignored_command, nullptr, process_ignored_command, process_update_quest_statistics, nullptr, process_quest_barrier, nullptr, nullptr, nullptr, // B0 nullptr, process_server_time_request, nullptr, process_function_call_result, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // C0 process_choice_search, process_create_game_dc_v3, nullptr, nullptr, nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t, process_disable_auto_reply, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // D0 process_trade_start, nullptr, process_trade_execute, nullptr, process_trade_error, nullptr, process_message_box_closed, process_gba_file_request, process_info_board_request, process_write_info_board_t, nullptr, process_verify_license_v3, nullptr, nullptr, nullptr, nullptr, // E0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // F0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }; static process_command_t bb_handlers[0x100] = { // 00 nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, process_chat_pc_bb, nullptr, process_game_list_request, process_menu_item_info_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 10 process_menu_selection, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, // 20 nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 30 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 40 process_card_search, nullptr, nullptr, nullptr, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 50 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 60 process_game_command, process_player_data, process_game_command, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_game_command, process_game_command, nullptr, process_client_ready, // 70 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 80 nullptr, process_simple_mail, nullptr, nullptr, process_change_lobby, nullptr, nullptr, nullptr, nullptr, process_change_arrow_color, process_lobby_name_request, nullptr, nullptr, nullptr, nullptr, nullptr, // 90 nullptr, nullptr, nullptr, process_login_bb, nullptr, nullptr, nullptr, nullptr, process_player_data, process_ignored_command, nullptr, nullptr, nullptr, nullptr, nullptr, process_return_client_config, // A0 process_change_ship, process_change_block, process_quest_list_request, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_ignored_command, process_update_quest_statistics, nullptr, process_quest_barrier, nullptr, nullptr, nullptr, // B0 nullptr, nullptr, nullptr, process_function_call_result, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // C0 nullptr, process_create_game_bb, nullptr, nullptr, nullptr, nullptr, process_set_blocked_senders_list, process_set_auto_reply_t, process_disable_auto_reply, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // D0 process_trade_start, nullptr, process_trade_execute, nullptr, process_trade_error, nullptr, nullptr, nullptr, process_info_board_request, process_write_info_board_t, nullptr, nullptr, process_guild_card_data_request_bb, nullptr, nullptr, nullptr, // E0 process_key_config_request_bb, nullptr, process_update_key_config_bb, process_player_preview_request_bb, nullptr, process_create_character_bb, nullptr, process_return_player_data_bb, process_client_checksum_bb, nullptr, process_team_command_bb, process_stream_file_request_bb, process_ignored_command, process_change_account_data_bb, nullptr, nullptr, // F0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }; static process_command_t patch_handlers[0x100] = { // 00 nullptr, nullptr, process_encryption_ok_patch, nullptr, process_login_patch, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, process_file_checksum_result_patch, // 10 process_file_checksum_results_done_patch, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 20 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 30 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 40 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 50 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 60 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 70 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 80 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // 90 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // A0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // B0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // C0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // D0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // E0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, // F0 nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, }; static process_command_t* handlers[6] = { dc_handlers, pc_handlers, patch_handlers, gc_handlers, xb_handlers, bb_handlers}; void process_command(shared_ptr s, shared_ptr c, uint16_t command, uint32_t flag, const string& data) { string encoded_name; auto player = c->game_data.player(false); if (player) { encoded_name = remove_language_marker(encode_sjis(player->disp.name)); } auto fn = handlers[static_cast(c->version)][command & 0xFF]; if (fn) { fn(s, c, command, flag, data); } else { process_unimplemented_command(s, c, command, flag, data); } }