diff --git a/src/AsyncHTTPServer.cc b/src/AsyncHTTPServer.cc index db63db7e..13c9bd28 100644 --- a/src/AsyncHTTPServer.cc +++ b/src/AsyncHTTPServer.cc @@ -163,7 +163,7 @@ asio::awaitable HTTPClient::recv_http_request(size_t max_line_size, } else if (method_token == "TRACE") { req.method = HTTPRequest::Method::TRACE; } else { - throw HTTPError(400, "unknown request method"); + throw HTTPError(400, "Unknown request method"); } req.http_version = std::move(line_tokens[2]); @@ -237,26 +237,26 @@ asio::awaitable HTTPClient::recv_http_request(size_t max_line_size, size_t parse_offset = 0; size_t chunk_size = stoull(line, &parse_offset, 16); if (parse_offset != line.size()) { - throw HTTPError(400, "invalid chunk header during chunked encoding"); + throw HTTPError(400, "Invalid chunk header during chunked encoding"); } if (chunk_size == 0) { break; } total_data_bytes += chunk_size; if (total_data_bytes > max_body_size) { - throw HTTPError(400, "request data size too large"); + throw HTTPError(400, "Request data size too large"); } chunks.emplace_back(co_await this->r.read_data(chunk_size)); auto after_chunk_data = co_await this->r.read_line("\r\n", 0x20); if (!after_chunk_data.empty()) { - throw HTTPError(400, "incorrect trailing sequence after chunk data"); + throw HTTPError(400, "Incorrect trailing sequence after chunk data"); } } } else { auto content_length_header = req.get_header("content-length"); size_t content_length = content_length_header ? stoull(*content_length_header) : 0; if (content_length > max_body_size) { - throw HTTPError(400, "request data size too large"); + throw HTTPError(400, "Request data size too large"); } else if (content_length > 0) { req.data = co_await this->r.read_data(content_length); } @@ -289,8 +289,7 @@ asio::awaitable HTTPClient::recv_websocket_message(size_t max_ while (this->r.get_socket().is_open()) { WebSocketMessage msg; - // We need at most 10 bytes to determine if there's a valid frame, or as - // little as 2 + // We need at most 10 bytes to determine if there's a valid frame, or as little as 2 co_await this->r.read_data_into(msg.header, 2); // Get the payload size diff --git a/src/AsyncHTTPServer.hh b/src/AsyncHTTPServer.hh index 173e4bc9..0ecc3cf3 100644 --- a/src/AsyncHTTPServer.hh +++ b/src/AsyncHTTPServer.hh @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +83,106 @@ struct HTTPClient { asio::awaitable send_websocket_message(const std::string& data, uint8_t opcode = 0x01); }; +template +class HTTPRouter { +public: + struct Args { + std::shared_ptr client; + const HTTPRequest& req; + std::unordered_map params; + phosg::JSON post_data; + + template + requires(std::is_integral_v) + T get_param(const char* name, bool hex = false) const { + const auto& value_str = this->params.at(name); + size_t conversion_end; + int64_t v = std::stoull(value_str, &conversion_end, hex ? 16 : 0); + if (conversion_end != value_str.size()) { + throw HTTPError(400, "Invalid integer value"); + } + + uint64_t uv = static_cast(v); + if constexpr (std::is_unsigned_v) { + if (uv & (~phosg::mask_for_type)) { + throw HTTPError(400, "Unsigned value out of range"); + } + return uv; + } else { + if (((uv & (~(phosg::mask_for_type >> 1))) != 0) && ((uv & (~(phosg::mask_for_type >> 1))) != (~(phosg::mask_for_type >> 1)))) { + throw HTTPError(400, "Signed value out of range"); + } + return v; + } + } + }; + + using Handler = std::function(Args&&)>; + + static std::vector split_and_normalize_path(const std::string& path) { + auto path_tokens = phosg::split(path, '/'); + while (!path_tokens.empty() && path_tokens.back().empty()) { + path_tokens.pop_back(); + } + return path_tokens; + } + + void add(HTTPRequest::Method method, const std::string& path_pattern, Handler handler) { + this->routes.emplace_back(Route{ + .method = method, .path_tokens = this->split_and_normalize_path(path_pattern), .handler = handler}); + } + + asio::awaitable call_handler(std::shared_ptr c, const HTTPRequest& req) { + Args args = {.client = c, .req = req, .params = {}, .post_data = phosg::JSON()}; + + auto tokens = this->split_and_normalize_path(req.path); + for (const auto& route : this->routes) { + if (route.path_tokens.size() != tokens.size()) { + continue; + } + + bool matched = true; + args.params.clear(); + for (size_t z = 0; z < tokens.size(); z++) { + if (route.path_tokens[z].starts_with(':')) { + args.params.emplace(route.path_tokens[z].substr(1), tokens[z]); + } else if (route.path_tokens[z] != tokens[z]) { + matched = false; + break; + } + } + + if (matched) { + if (req.method != route.method) { + throw HTTPError(405, "Incorrect HTTP method"); + } + if (req.method == HTTPRequest::Method::POST) { + auto* content_type = req.get_header("content-type"); + if (!content_type || (*content_type != "application/json")) { + throw HTTPError(400, "POST requests must use the application/json content type"); + } + try { + args.post_data = phosg::JSON::parse(req.data); + } catch (const std::exception& e) { + throw HTTPError(400, std::format("Invalid JSON: {}", e.what())); + } + } + co_return co_await route.handler(std::move(args)); + } + } + + throw HTTPError(404, "Request path did not match any route"); + } + +private: + struct Route { + HTTPRequest::Method method; + std::vector path_tokens; + Handler handler; + }; + std::vector routes; +}; + struct HTTPServerLimits { size_t max_http_request_line_size = 0x1000; // 4KB size_t max_http_data_size = 0x200000; // 2MB @@ -120,6 +221,29 @@ public: protected: HTTPServerLimits limits; + void require_GET(const HTTPRequest& req) { + if (req.method != HTTPRequest::Method::GET) { + throw HTTPError(405, "GET method required for this endpoint"); + } + } + + phosg::JSON require_JSON_POST(const HTTPRequest& req) { + if (req.method != HTTPRequest::Method::POST) { + throw HTTPError(405, "POST method required for this endpoint"); + } + + auto* content_type = req.get_header("content-type"); + if (!content_type || (*content_type != "application/json")) { + throw HTTPError(400, "POST requests must use the application/json content type"); + } + + try { + return phosg::JSON::parse(req.data); + } catch (const std::exception& e) { + throw HTTPError(400, std::format("Invalid JSON: {}", e.what())); + } + } + // Attempts to switch the client to WebSockets. Returns true if this is done // successfully (and the caller should then receive/send WebSocket messages), // or false if this failed (and the caller should send an HTTP response). diff --git a/src/AsyncUtils.hh b/src/AsyncUtils.hh index 60f621b1..fa1a3847 100644 --- a/src/AsyncUtils.hh +++ b/src/AsyncUtils.hh @@ -265,7 +265,11 @@ asio::awaitable> call_on_thread_pool(asio::t // call_on_thread_pool coroutine has been destroyed) auto promise = std::make_shared>(); asio::post(pool, [bound = std::move(bound), promise]() mutable { - promise->set_value(bound()); + try { + promise->set_value(bound()); + } catch (...) { + promise->set_exception(std::current_exception()); + } }); co_return co_await promise->get(); } diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index d8813339..282e8bc2 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -17,7 +17,714 @@ using namespace std; HTTPServer::HTTPServer(shared_ptr state) - : AsyncHTTPServer(state->io_context, "[HTTPServer] "), state(state) {} + : AsyncHTTPServer(state->io_context, "[HTTPServer] "), state(state) { + using RouterRetT = std::variant>; + using RetT = asio::awaitable; + using ArgsT = HTTPRouter::Args; + + auto generate_server_version_json = []() -> phosg::JSON { + return phosg::JSON::dict({ + {"ServerType", "newserv"}, + {"BuildTime", BUILD_TIMESTAMP}, + {"BuildTimeStr", phosg::format_time(BUILD_TIMESTAMP)}, + {"Revision", GIT_REVISION_HASH}, + }); + }; + + this->router.add(HTTPRequest::Method::GET, "/", [generate_server_version_json](ArgsT&&) -> RetT { + co_return make_shared(generate_server_version_json()); + }); + + this->router.add(HTTPRequest::Method::POST, "/y/shell-exec", [this](ArgsT&& args) -> RetT { + auto command = args.post_data.get_string("command"); + try { + auto dispatch_res = co_await ShellCommand::dispatch_str(this->state, command); + co_return make_shared(phosg::JSON::dict({{"result", phosg::join(dispatch_res, "\n")}})); + } catch (const exception& e) { + throw HTTPError(400, e.what()); + } + }); + + this->router.add(HTTPRequest::Method::GET, "/y/rare-drops/stream", [this, generate_server_version_json](ArgsT&& args) -> RetT { + if (!(co_await this->enable_websockets(args.client, args.req))) { + throw HTTPError(400, "this path requires a websocket connection"); + } + this->rare_drop_subscribers.emplace(args.client); + co_await args.client->send_websocket_message(generate_server_version_json().serialize()); + co_return nullptr; + }); + + this->router.add(HTTPRequest::Method::GET, "/y/clients", [this](ArgsT&&) -> RetT { + auto res = make_shared(phosg::JSON::list()); + for (const auto& c : this->state->game_server->all_clients()) { + auto item_name_index = this->state->item_name_index_opt(c->version()); + + const char* drop_notifications_mode = "unknown"; + switch (c->get_drop_notification_mode()) { + case Client::ItemDropNotificationMode::NOTHING: + drop_notifications_mode = "off"; + break; + case Client::ItemDropNotificationMode::RARES_ONLY: + drop_notifications_mode = "rare"; + break; + case Client::ItemDropNotificationMode::ALL_ITEMS: + drop_notifications_mode = "on"; + break; + case Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA: + drop_notifications_mode = "every"; + break; + } + auto client_json = phosg::JSON::dict({ + {"ID", c->id}, + {"RemoteAddress", c->channel->default_name()}, + {"Version", phosg::name_for_enum(c->version())}, + {"SubVersion", c->sub_version}, + {"Language", name_for_language(c->language())}, + {"LocationX", c->pos.x.load()}, + {"LocationZ", c->pos.z.load()}, + {"LocationFloor", c->floor}, + {"CanChat", c->can_chat}, + {"SpecificVersion", c->specific_version}, + {"SwitchAssistEnabled", (c->check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? true : false)}, + {"InfiniteHPEnabled", (c->check_flag(Client::Flag::INFINITE_HP_ENABLED) ? true : false)}, + {"InfiniteTPEnabled", (c->check_flag(Client::Flag::INFINITE_TP_ENABLED) ? true : false)}, + {"DropNotificationMode", drop_notifications_mode}, + {"DebugEnabled", (c->check_flag(Client::Flag::DEBUG_ENABLED) ? true : false)}, + {"ProxySaveFilesEnabled", (c->check_flag(Client::Flag::PROXY_SAVE_FILES) ? true : false)}, + {"ProxyChatCommandsEnabled", (c->check_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED) ? true : false)}, + {"ProxyPlayerNotificationsEnabled", (c->check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) ? true : false)}, + {"ProxyEp3InfiniteMesetaEnabled", (c->check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED) ? true : false)}, + {"ProxyEp3InfiniteTimeEnabled", (c->check_flag(Client::Flag::PROXY_EP3_INFINITE_TIME_ENABLED) ? true : false)}, + {"ProxyBlockFunctionCalls", (c->check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS) ? true : false)}, + {"ProxyEp3UnmaskWhispers", (c->check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) ? true : false)}, + {"OverrideRandomSeed", c->override_random_seed}, + {"OverrideSectionID", ((c->override_section_id != 0xFF) ? c->override_section_id : phosg::JSON(nullptr))}, + {"OverrideLobbyEvent", ((c->override_lobby_event != 0xFF) ? c->override_lobby_event : phosg::JSON(nullptr))}, + {"OverrideLobbyNumber", ((c->override_lobby_number != 0x80) ? c->override_lobby_number : phosg::JSON(nullptr))}, + }); + if (c->login) { + client_json.emplace("Account", c->login->account->json()); + } else { + client_json.emplace("Account", phosg::JSON()); + } + auto l = c->lobby.lock(); + if (l) { + client_json.emplace("LobbyID", l->lobby_id); + client_json.emplace("LobbyClientID", c->lobby_client_id); + } + if (c->version() == Version::BB_V4) { + client_json.emplace("BBCharacterIndex", c->bb_character_index); + } + auto p = c->character_file(false, false); + if (p) { + if (!is_ep3(c->version())) { + if (c->version() != Version::DC_NTE) { + client_json.emplace("InventoryLanguage", name_for_language(p->inventory.language)); + client_json.emplace("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP)); + client_json.emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); + if (!is_v1_or_v2(c->version())) { + client_json.emplace("NumPowerMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER)); + client_json.emplace("NumDefMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF)); + client_json.emplace("NumMindMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND)); + client_json.emplace("NumEvadeMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE)); + client_json.emplace("NumLuckMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); + } + } + phosg::JSON items_json = phosg::JSON::list(); + for (size_t z = 0; z < p->inventory.num_items; z++) { + const auto& item = p->inventory.items[z]; + auto item_dict = phosg::JSON::dict({ + {"Flags", item.flags.load()}, + {"Data", item.data.hex()}, + {"ItemID", item.data.id.load()}, + }); + if (item_name_index) { + item_dict.emplace("Description", item_name_index->describe_item(item.data)); + } + items_json.emplace_back(std::move(item_dict)); + } + client_json.emplace("InventoryItems", std::move(items_json)); + client_json.emplace("ATP", p->disp.stats.char_stats.atp.load()); + client_json.emplace("MST", p->disp.stats.char_stats.mst.load()); + client_json.emplace("EVP", p->disp.stats.char_stats.evp.load()); + client_json.emplace("HP", p->disp.stats.char_stats.hp.load()); + client_json.emplace("DFP", p->disp.stats.char_stats.dfp.load()); + client_json.emplace("ATA", p->disp.stats.char_stats.ata.load()); + client_json.emplace("LCK", p->disp.stats.char_stats.lck.load()); + client_json.emplace("EXP", p->disp.stats.experience.load()); + client_json.emplace("Meseta", p->disp.stats.meseta.load()); + auto tech_levels_json = phosg::JSON::dict(); + for (size_t z = 0; z < 0x13; z++) { + auto level = p->get_technique_level(z); + tech_levels_json.emplace(name_for_technique(z), (level != 0xFF) ? (level + 1) : phosg::JSON(nullptr)); + } + client_json.emplace("TechniqueLevels", std::move(tech_levels_json)); + } + client_json.emplace("Height", p->disp.stats.height.load()); + client_json.emplace("Level", p->disp.stats.level.load() + 1); + client_json.emplace("NameColor", p->disp.visual.name_color.load()); + client_json.emplace("ExtraModel", (p->disp.visual.validation_flags & 2) ? p->disp.visual.extra_model : phosg::JSON(nullptr)); + client_json.emplace("SectionID", name_for_section_id(p->disp.visual.section_id)); + client_json.emplace("CharClass", name_for_char_class(p->disp.visual.char_class)); + client_json.emplace("Costume", p->disp.visual.costume.load()); + client_json.emplace("Skin", p->disp.visual.skin.load()); + client_json.emplace("Face", p->disp.visual.face.load()); + client_json.emplace("Head", p->disp.visual.head.load()); + client_json.emplace("Hair", p->disp.visual.hair.load()); + client_json.emplace("HairR", p->disp.visual.hair_r.load()); + client_json.emplace("HairG", p->disp.visual.hair_g.load()); + client_json.emplace("HairB", p->disp.visual.hair_b.load()); + client_json.emplace("ProportionX", p->disp.visual.proportion_x.load()); + client_json.emplace("ProportionY", p->disp.visual.proportion_y.load()); + + client_json.emplace("Name", p->disp.name.decode(c->language())); + client_json.emplace("PlayTimeSeconds", p->play_time_seconds.load()); + + client_json.emplace("AutoReply", p->auto_reply.decode(c->language())); + client_json.emplace("InfoBoard", p->info_board.decode(c->language())); + auto battle_place_counts = phosg::JSON::list({ + p->battle_records.place_counts[0].load(), + p->battle_records.place_counts[1].load(), + p->battle_records.place_counts[2].load(), + p->battle_records.place_counts[3].load(), + }); + client_json.emplace("BattlePlaceCounts", std::move(battle_place_counts)); + client_json.emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load()); + + if (!is_ep3(c->version())) { + auto json_for_challenge_times = [](const parray& times) -> phosg::JSON { + auto times_json = phosg::JSON::list(); + for (size_t z = 0; z < times.size(); z++) { + times_json.emplace_back(times[z].decode()); + } + return times_json; + }; + client_json.emplace("ChallengeTitleColorXRGB1555", p->challenge_records.title_color.load()); + client_json.emplace("ChallengeTimesEp1Online", json_for_challenge_times(p->challenge_records.times_ep1_online)); + client_json.emplace("ChallengeTimesEp2Online", json_for_challenge_times(p->challenge_records.times_ep2_online)); + client_json.emplace("ChallengeTimesEp1Offline", json_for_challenge_times(p->challenge_records.times_ep1_offline)); + client_json.emplace("ChallengeGraveIsEp2", p->challenge_records.grave_is_ep2 ? true : false); + client_json.emplace("ChallengeGraveStageNum", p->challenge_records.grave_stage_num); + client_json.emplace("ChallengeGraveFloor", p->challenge_records.grave_floor); + client_json.emplace("ChallengeGraveDeaths", p->challenge_records.grave_deaths.load()); + { + uint16_t year = 2000 + ((p->challenge_records.grave_time >> 28) & 0x0F); + uint8_t month = (p->challenge_records.grave_time >> 24) & 0x0F; + uint8_t day = (p->challenge_records.grave_time >> 16) & 0xFF; + uint8_t hour = (p->challenge_records.grave_time >> 8) & 0xFF; + uint8_t minute = p->challenge_records.grave_time & 0xFF; + client_json.emplace("ChallengeGraveTime", std::format("{:04}-{:02}-{:02} {:02}:{:02}:00", year, month, day, hour, minute)); + } + string grave_enemy_types; + if (p->challenge_records.grave_defeated_by_enemy_rt_index) { + for (EnemyType type : enemy_types_for_rare_table_index(p->challenge_records.grave_is_ep2 ? Episode::EP2 : Episode::EP1, p->challenge_records.grave_defeated_by_enemy_rt_index)) { + if (!grave_enemy_types.empty()) { + grave_enemy_types += "/"; + } + grave_enemy_types += phosg::name_for_enum(type); + } + } + client_json.emplace("ChallengeGraveDefeatedByEnemy", std::move(grave_enemy_types)); + client_json.emplace("ChallengeGraveX", p->challenge_records.grave_x.load()); + client_json.emplace("ChallengeGraveY", p->challenge_records.grave_y.load()); + client_json.emplace("ChallengeGraveZ", p->challenge_records.grave_z.load()); + client_json.emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode()); + client_json.emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode()); + client_json.emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load()); + client_json.emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.decode()); + client_json.emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load()); + client_json.emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.decode()); + client_json.emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load()); + client_json.emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.decode()); + client_json.emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode()); + } + } + auto ses = c->proxy_session; + if (ses) { + auto lobby_players_json = phosg::JSON::list(); + for (size_t z = 0; z < ses->lobby_players.size(); z++) { + const auto& p = ses->lobby_players[z]; + if (p.guild_card_number) { + lobby_players_json.emplace_back(phosg::JSON::dict({ + {"GuildCardNumber", p.guild_card_number}, + {"Name", p.name}, + {"Language", name_for_language(p.language)}, + {"SectionID", name_for_section_id(p.section_id)}, + {"CharClass", name_for_char_class(p.char_class)}, + })); + lobby_players_json.back().emplace("XBUserID", p.xb_user_id ? p.xb_user_id : phosg::JSON(nullptr)); + } else { + lobby_players_json.emplace_back(nullptr); + } + } + + auto ses_json = phosg::JSON::dict({ + {"RemoteServerAddress", ses->server_channel->default_name()}, + {"RemoteGuildCardNumber", ses->remote_guild_card_number}, + {"RemoteClientConfigData", phosg::format_data_string(&ses->remote_client_config_data[0], ses->remote_client_config_data.size())}, + {"IsInGame", ses->is_in_game}, + {"IsInQuest", ses->is_in_quest}, + {"LobbyLeaderClientID", ses->leader_client_id}, + {"LobbyEvent", ses->lobby_event}, + {"LobbyDifficulty", name_for_difficulty(ses->lobby_difficulty)}, + {"LobbySectionID", name_for_section_id(ses->lobby_section_id)}, + {"LobbyMode", name_for_mode(ses->lobby_mode)}, + {"LobbyEpisode", name_for_episode(ses->lobby_episode)}, + {"LobbyRandomSeed", ses->lobby_random_seed}, + {"LobbyPlayers", std::move(lobby_players_json)}, + }); + switch (ses->drop_mode) { + case ProxyDropMode::DISABLED: + ses_json.emplace("DropMode", "none"); + break; + case ProxyDropMode::PASSTHROUGH: + ses_json.emplace("DropMode", "default"); + break; + case ProxyDropMode::INTERCEPT: + ses_json.emplace("DropMode", "proxy"); + break; + } + client_json.emplace("ProxySession", std::move(ses_json)); + } else { + client_json.emplace("ProxySession", phosg::JSON()); + } + + res->emplace_back(std::move(client_json)); + } + co_return res; + }); + + this->router.add(HTTPRequest::Method::GET, "/y/lobbies", [this](ArgsT&&) -> RetT { + auto res = make_shared(phosg::JSON::list()); + for (const auto& [_, l] : this->state->id_to_lobby) { + auto leader = l->clients[l->leader_id]; + Version v = leader ? leader->version() : Version::BB_V4; + auto item_name_index = this->state->item_name_index_opt(v); + + auto client_ids_json = phosg::JSON::list(); + for (size_t z = 0; z < l->max_clients; z++) { + client_ids_json.emplace_back(l->clients[z] ? l->clients[z]->id : phosg::JSON(nullptr)); + } + + auto lobby_json = phosg::JSON::dict({ + {"ID", l->lobby_id}, + {"AllowedVersions", l->allowed_versions}, + {"Event", l->event}, + {"LeaderClientID", l->leader_id}, + {"MaxClients", l->max_clients}, + {"IdleTimeoutUsecs", l->idle_timeout_usecs}, + {"ClientIDs", std::move(client_ids_json)}, + {"IsGame", l->is_game()}, + {"IsPersistent", l->check_flag(Lobby::Flag::PERSISTENT)}, + }); + + if (l->is_game()) { + lobby_json.emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)); + lobby_json.emplace("MinLevel", l->min_level + 1); + lobby_json.emplace("MaxLevel", l->max_level + 1); + lobby_json.emplace("Episode", name_for_episode(l->episode)); + lobby_json.emplace("HasPassword", !l->password.empty()); + lobby_json.emplace("Name", l->name); + lobby_json.emplace("RandomSeed", l->random_seed); + if (l->episode != Episode::EP3) { + lobby_json.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); + lobby_json.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); + lobby_json.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); + lobby_json.emplace("Variations", l->variations.json()); + uint8_t effective_section_id = l->effective_section_id(); + if (effective_section_id < 10) { + lobby_json.emplace("SectionID", name_for_section_id(effective_section_id)); + } else { + lobby_json.emplace("SectionID", nullptr); + } + lobby_json.emplace("Mode", name_for_mode(l->mode)); + lobby_json.emplace("Difficulty", name_for_difficulty(l->difficulty)); + lobby_json.emplace("BaseEXPMultiplier", l->base_exp_multiplier); + lobby_json.emplace("EXPShareMultiplier", l->exp_share_multiplier); + lobby_json.emplace("AllowedDropModes", l->allowed_drop_modes); + switch (l->drop_mode) { + case ServerDropMode::DISABLED: + lobby_json.emplace("DropMode", "none"); + break; + case ServerDropMode::CLIENT: + lobby_json.emplace("DropMode", "client"); + break; + case ServerDropMode::SERVER_SHARED: + lobby_json.emplace("DropMode", "shared"); + break; + case ServerDropMode::SERVER_PRIVATE: + lobby_json.emplace("DropMode", "private"); + break; + case ServerDropMode::SERVER_DUPLICATE: + lobby_json.emplace("DropMode", "duplicate"); + break; + } + if (l->mode == GameMode::CHALLENGE) { + lobby_json.emplace("ChallengeEXPMultiplier", l->challenge_exp_multiplier); + if (l->challenge_params) { + lobby_json.emplace("ChallengeStageNumber", l->challenge_params->stage_number); + lobby_json.emplace("ChallengeRankColor", l->challenge_params->rank_color); + lobby_json.emplace("ChallengeRankText", l->challenge_params->rank_text); + lobby_json.emplace("ChallengeRank0ThresholdBitmask", l->challenge_params->rank_thresholds[0].bitmask); + lobby_json.emplace("ChallengeRank0ThresholdSeconds", l->challenge_params->rank_thresholds[0].seconds); + lobby_json.emplace("ChallengeRank1ThresholdBitmask", l->challenge_params->rank_thresholds[1].bitmask); + lobby_json.emplace("ChallengeRank1ThresholdSeconds", l->challenge_params->rank_thresholds[1].seconds); + lobby_json.emplace("ChallengeRank2ThresholdBitmask", l->challenge_params->rank_thresholds[2].bitmask); + lobby_json.emplace("ChallengeRank2ThresholdSeconds", l->challenge_params->rank_thresholds[2].seconds); + } + } + + auto floor_items_json = phosg::JSON::list(); + for (size_t floor = 0; floor < l->floor_item_managers.size(); floor++) { + for (const auto& it : l->floor_item_managers[floor].items) { + const auto& item = it.second; + auto item_dict = phosg::JSON::dict({ + {"LocationFloor", floor}, + {"LocationX", item->pos.x.load()}, + {"LocationZ", item->pos.z.load()}, + {"DropNumber", item->drop_number}, + {"Flags", item->flags}, + {"Data", item->data.hex()}, + {"ItemID", item->data.id.load()}, + }); + if (item_name_index) { + item_dict.emplace("Description", item_name_index->describe_item(item->data)); + } + floor_items_json.emplace_back(std::move(item_dict)); + } + } + lobby_json.emplace("FloorItems", std::move(floor_items_json)); + lobby_json.emplace("Quest", l->quest ? l->quest->json() : phosg::JSON(nullptr)); + + } else { + lobby_json.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); + lobby_json.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); + lobby_json.emplace("SpectatorsForbidden", l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)); + + auto ep3s = l->ep3_server; + if (ep3s) { + auto players_json = phosg::JSON::list(); + for (size_t z = 0; z < 4; z++) { + if (!ep3s->name_entries[z].present) { + players_json.emplace_back(nullptr); + } else { + auto lc = l->clients[z]; + + auto deck_entry = ep3s->deck_entries[z]; + phosg::JSON deck_json = nullptr; + if (deck_entry) { + auto cards_json = phosg::JSON::list(); + for (size_t w = 0; w < deck_entry->card_ids.size(); w++) { + try { + const auto& ce = ep3s->options.card_index->definition_for_id(deck_entry->card_ids[w]); + auto name = ce->def.en_name.decode(); + if (name.empty()) { + name = ce->def.en_short_name.decode(); + } + if (name.empty()) { + name = ce->def.jp_name.decode(); + } + if (name.empty()) { + name = ce->def.jp_short_name.decode(); + } + cards_json.emplace_back(name); + } catch (const out_of_range&) { + cards_json.emplace_back(deck_entry->card_ids[w].load()); + } + } + deck_json = phosg::JSON::dict({ + {"Name", deck_entry->name.decode(lc ? lc->language() : Language::ENGLISH)}, + {"TeamID", deck_entry->team_id.load()}, + {"Cards", std::move(cards_json)}, + {"GodWhimFlag", deck_entry->god_whim_flag}, + {"PlayerLevel", deck_entry->player_level.load() + 1}, + }); + } + + auto player_json = phosg::JSON::dict({ + {"PlayerName", ep3s->name_entries[z].name.decode(lc ? lc->language() : Language::ENGLISH)}, + {"ClientID", ep3s->name_entries[z].client_id}, + {"IsCOM", !!ep3s->name_entries[z].is_cpu_player}, + {"Deck", std::move(deck_json)}, + }); + players_json.emplace_back(std::move(player_json)); + } + } + auto battle_state_json = phosg::JSON::dict({ + {"BehaviorFlags", ep3s->options.behavior_flags}, + {"RandomSeed", ep3s->options.rand_crypt->seed()}, + {"Tournament", ep3s->options.tournament ? ep3s->options.tournament->json() : nullptr}, + {"MapNumber", ep3s->last_chosen_map ? ep3s->last_chosen_map->map_number : phosg::JSON(nullptr)}, + {"EnvironmentNumber", ep3s->map_and_rules ? ep3s->map_and_rules->environment_number : phosg::JSON(nullptr)}, + {"Rules", ep3s->map_and_rules ? ep3s->map_and_rules->rules.json() : nullptr}, + {"Players", std::move(players_json)}, + {"IsBattleFinished", ep3s->battle_finished}, + {"IsBattleInprogress", ep3s->battle_in_progress}, + {"RoundNumber", ep3s->round_num}, + {"FirstTeamTurn", ep3s->first_team_turn}, + {"CurrentTeamTurn", ep3s->current_team_turn1}, + {"BattlePhase", phosg::name_for_enum(ep3s->battle_phase)}, + {"SetupPhase", ep3s->setup_phase}, + {"RegistrationPhase", ep3s->registration_phase}, + {"ActionSubphase", ep3s->action_subphase}, + {"BattleStartTimeUsecs", ep3s->battle_start_usecs}, + {"TeamEXP", phosg::JSON::list({ep3s->team_exp[0], ep3s->team_exp[1]})}, + {"TeamDiceBonus", phosg::JSON::list({ep3s->team_dice_bonus[0], ep3s->team_dice_bonus[1]})}, + }); + // std::shared_ptr state_flags; + // std::array, 4> player_states; + lobby_json.emplace("Episode3BattleState", std::move(battle_state_json)); + } else { + lobby_json.emplace("Episode3BattleState", nullptr); + } + auto watched_lobby = l->watched_lobby.lock(); + if (watched_lobby) { + lobby_json.emplace("WatchedLobbyID", watched_lobby->lobby_id); + } + auto watcher_lobby_ids_json = phosg::JSON::list(); + for (const auto& watcher_lobby : l->watcher_lobbies) { + watcher_lobby_ids_json.emplace_back(watcher_lobby->lobby_id); + } + lobby_json.emplace("WatcherLobbyIDs", std::move(watcher_lobby_ids_json)); + lobby_json.emplace("IsReplayLobby", !!l->battle_player); + } + + } else { // Not game + lobby_json.emplace("IsPublic", l->check_flag(Lobby::Flag::PUBLIC)); + lobby_json.emplace("IsDefault", l->check_flag(Lobby::Flag::DEFAULT)); + lobby_json.emplace("IsOverflow", l->check_flag(Lobby::Flag::IS_OVERFLOW)); + lobby_json.emplace("Block", l->block); + } + + res->emplace_back(std::move(lobby_json)); + } + co_return res; + }); + + this->router.add(HTTPRequest::Method::GET, "/y/accounts", [this](ArgsT&&) -> RetT { + auto res = make_shared(phosg::JSON::list()); + for (const auto& it : this->state->account_index->all()) { + res->emplace_back(it->json()); + } + co_return res; + }); + + this->router.add(HTTPRequest::Method::GET, "/y/account/:account_id", [this](ArgsT&& args) -> RetT { + uint32_t account_id = args.get_param("account_id"); + try { + co_return make_shared(this->state->account_index->from_account_id(account_id)->json()); + } catch (const AccountIndex::missing_account&) { + throw HTTPError(404, "Account does not exist"); + } + }); + + this->router.add(HTTPRequest::Method::GET, "/y/teams", [this](ArgsT&& params) -> RetT { + auto res = make_shared(phosg::JSON::dict()); + for (const auto& it : this->state->team_index->all()) { + res->emplace(std::format("{}", it->team_id), it->json()); + } + co_return res; + }); + + this->router.add(HTTPRequest::Method::GET, "/y/team/:team_id", [this](ArgsT&& args) -> RetT { + uint32_t team_id = args.get_param("team_id"); + auto team = this->state->team_index->get_by_id(team_id); + if (!team) { + throw HTTPError(404, "Team does not exist"); + } + co_return make_shared(team->json()); + }); + + this->router.add(HTTPRequest::Method::GET, "/y/team/:team_id/flag", [this](ArgsT&& args) -> RetT { + uint32_t team_id = args.get_param("team_id"); + auto team = this->state->team_index->get_by_id(team_id); + if (!team) { + throw HTTPError(404, "Team does not exist"); + } + if (!team->flag_data) { + throw HTTPError(404, "Team does not have a flag"); + } + auto img = team->decode_flag_data(); + co_return RawResponse{.content_type = "image/png", .data = img.serialize(phosg::ImageFormat::PNG)}; + }); + + std::function generate_server_info_json = [this]() -> phosg::JSON { + size_t game_count = 0; + size_t lobby_count = 0; + for (const auto& it : this->state->id_to_lobby) { + if (it.second->is_game()) { + game_count++; + } else { + lobby_count++; + } + } + uint64_t uptime_usecs = phosg::now() - this->state->creation_time; + return phosg::JSON::dict({ + {"StartTimeUsecs", this->state->creation_time}, + {"StartTime", phosg::format_time(this->state->creation_time)}, + {"UptimeUsecs", uptime_usecs}, + {"Uptime", phosg::format_duration(uptime_usecs)}, + {"LobbyCount", lobby_count}, + {"GameCount", game_count}, + {"ClientCount", this->state->game_server->all_clients().size() - ProxySession::num_proxy_sessions}, + {"ProxySessionCount", ProxySession::num_proxy_sessions}, + {"ServerName", this->state->name}, + }); + }; + + this->router.add(HTTPRequest::Method::GET, "/y/server", [generate_server_info_json](ArgsT&&) -> RetT { + co_return make_shared(generate_server_info_json()); + }); + + this->router.add(HTTPRequest::Method::GET, "/y/config", [this](ArgsT&&) -> RetT { + co_return this->state->config_json; + }); + + this->router.add(HTTPRequest::Method::GET, "/y/summary", [this, generate_server_info_json](ArgsT&& args) -> RetT { + auto clients_json = phosg::JSON::list(); + for (const auto& c : this->state->game_server->all_clients()) { + auto p = c->character_file(false, false); + auto l = c->lobby.lock(); + clients_json.emplace_back(phosg::JSON::dict({ + {"ID", c->id}, + {"AccountID", c->login ? c->login->account->account_id : phosg::JSON(nullptr)}, + {"Name", p ? p->disp.name.decode(c->language()) : phosg::JSON(nullptr)}, + {"Version", phosg::name_for_enum(c->version())}, + {"Language", name_for_language(c->language())}, + {"Level", p ? (p->disp.stats.level + 1) : phosg::JSON(nullptr)}, + {"Class", p ? name_for_char_class(p->disp.visual.char_class) : phosg::JSON(nullptr)}, + {"SectionID", p ? name_for_section_id(p->disp.visual.section_id) : phosg::JSON(nullptr)}, + {"LobbyID", l ? l->lobby_id : phosg::JSON(nullptr)}, + {"IsOnProxy", c->proxy_session ? true : false}, + })); + } + + auto games_json = phosg::JSON::list(); + for (const auto& it : this->state->id_to_lobby) { + auto l = it.second; + if (l->is_game()) { + auto game_json = phosg::JSON::dict({ + {"ID", l->lobby_id}, + {"Name", l->name}, + {"Players", l->count_clients()}, + {"CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)}, + {"Episode", name_for_episode(l->episode)}, + {"HasPassword", !l->password.empty()}, + }); + if (l->episode == Episode::EP3) { + auto ep3s = l->ep3_server; + game_json.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); + game_json.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); + game_json.emplace("MapNumber", (ep3s && ep3s->last_chosen_map) ? ep3s->last_chosen_map->map_number : phosg::JSON(nullptr)); + game_json.emplace("Rules", (ep3s && ep3s->map_and_rules) ? ep3s->map_and_rules->rules.json() : nullptr); + } else { + game_json.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); + game_json.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); + game_json.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); + uint8_t effective_section_id = l->effective_section_id(); + if (effective_section_id < 10) { + game_json.emplace("SectionID", name_for_section_id(effective_section_id)); + } else { + game_json.emplace("SectionID", nullptr); + } + game_json.emplace("Mode", name_for_mode(l->mode)); + game_json.emplace("Difficulty", name_for_difficulty(l->difficulty)); + game_json.emplace("Quest", l->quest ? l->quest->json() : phosg::JSON(nullptr)); + } + games_json.emplace_back(std::move(game_json)); + } + } + + co_return make_shared(phosg::JSON::dict({ + {"Clients", std::move(clients_json)}, + {"Games", std::move(games_json)}, + {"Server", generate_server_info_json()}, + })); + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/cards", [this](ArgsT&& args) -> RetT { + auto& index = args.req.query_params.count("trial") ? this->state->ep3_card_index_trial : this->state->ep3_card_index; + co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { + return make_shared(index->definitions_json()); + }); + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/card/:card_id", [this](ArgsT&& args) -> RetT { + auto& index = args.req.query_params.count("trial") ? this->state->ep3_card_index_trial : this->state->ep3_card_index; + uint32_t card_id = args.get_param("card_id"); + try { + co_return make_shared(index->definition_for_id(card_id)->def.json()); + } catch (const std::out_of_range&) { + throw HTTPError(404, "Card definition does not exist"); + } + }); + + // TODO: Add /y/data/ep3/maps, /y/data/ep3/map/:map_number and /y/data/ep3/map/:map_number/raw + + this->router.add(HTTPRequest::Method::GET, "/y/data/common-tables", [this](ArgsT&&) -> RetT { + auto ret = make_shared(phosg::JSON::list()); + for (const auto& it : this->state->common_item_sets) { + ret->emplace_back(it.first); + } + co_return ret; + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/common-table/:table_name", [this](ArgsT&& args) -> RetT { + try { + const auto& table = this->state->common_item_sets.at(args.params.at("table_name")); + co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { + return make_shared(table->json()); + }); + } catch (const out_of_range&) { + throw HTTPError(404, "Table does not exist"); + } + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/rare-tables", [this](ArgsT&&) -> RetT { + auto ret = make_shared(phosg::JSON::list()); + for (const auto& it : this->state->rare_item_sets) { + ret->emplace_back(it.first); + } + co_return ret; + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/rare-table/:table_name", [this](ArgsT&& args) -> RetT { + try { + const auto& table_name = args.params.at("table_name"); + const auto& table = this->state->rare_item_sets.at(table_name); + shared_ptr name_index; + if (table_name.ends_with("-v1")) { + name_index = this->state->item_name_index_opt(Version::DC_V1); + } else if (table_name.ends_with("-v2")) { + name_index = this->state->item_name_index_opt(Version::PC_V2); + } else if (table_name.ends_with("-v3")) { + name_index = this->state->item_name_index_opt(Version::GC_V3); + } else if (table_name.ends_with("-v4")) { + name_index = this->state->item_name_index_opt(Version::BB_V4); + } + co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { + return make_shared(table->json(name_index)); + }); + } catch (const out_of_range&) { + throw HTTPError(404, "Table does not exist"); + } + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/quests", [this](ArgsT&& args) -> RetT { + co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { + return make_shared(this->state->quest_index->json()); + }); + }); + + this->router.add(HTTPRequest::Method::GET, "/y/data/quest/:quest_num", [this](ArgsT&& args) -> RetT { + uint32_t quest_num = args.get_param("quest_num"); + auto q = this->state->quest_index->get(quest_num); + if (!q) { + throw HTTPError(404, "Quest does not exist"); + } + co_return make_shared(q->json()); + }); +} asio::awaitable HTTPServer::send_rare_drop_notification(shared_ptr message) { if (!this->rare_drop_subscribers.empty()) { @@ -49,715 +756,8 @@ asio::awaitable HTTPServer::send_rare_drop_notification(shared_ptr HTTPServer::generate_server_version() const { - return make_shared(phosg::JSON::dict({ - {"ServerType", "newserv"}, - {"BuildTime", BUILD_TIMESTAMP}, - {"BuildTimeStr", phosg::format_time(BUILD_TIMESTAMP)}, - {"Revision", GIT_REVISION_HASH}, - })); -} - -std::shared_ptr HTTPServer::generate_account_json(shared_ptr a) const { - auto dc_nte_licenses_json = phosg::JSON::list(); - for (const auto& it : a->dc_nte_licenses) { - dc_nte_licenses_json.emplace_back(it.first); - } - auto dc_licenses_json = phosg::JSON::list(); - for (const auto& it : a->dc_licenses) { - dc_licenses_json.emplace_back(it.first); - } - auto pc_licenses_json = phosg::JSON::list(); - for (const auto& it : a->pc_licenses) { - pc_licenses_json.emplace_back(it.first); - } - auto gc_licenses_json = phosg::JSON::list(); - for (const auto& it : a->gc_licenses) { - gc_licenses_json.emplace_back(it.first); - } - auto xb_licenses_json = phosg::JSON::list(); - for (const auto& it : a->xb_licenses) { - xb_licenses_json.emplace_back(it.first); - } - auto bb_licenses_json = phosg::JSON::list(); - for (const auto& it : a->bb_licenses) { - bb_licenses_json.emplace_back(it.first); - } - auto auto_patches_json = phosg::JSON::list(); - for (const auto& it : a->auto_patches_enabled) { - auto_patches_json.emplace_back(it); - } - return make_shared(phosg::JSON::dict({ - {"AccountID", a->account_id}, - {"Flags", a->flags}, - {"BanEndTime", a->ban_end_time ? a->ban_end_time : phosg::JSON(nullptr)}, - {"Ep3CurrentMeseta", a->ep3_current_meseta}, - {"Ep3TotalMesetaEarned", a->ep3_total_meseta_earned}, - {"BBTeamID", a->bb_team_id}, - {"LastPlayerName", a->last_player_name}, - {"AutoReplyMessage", a->auto_reply_message}, - {"IsTemporary", a->is_temporary}, - {"DCNTELicenses", std::move(dc_nte_licenses_json)}, - {"DCLicenses", std::move(dc_licenses_json)}, - {"PCLicenses", std::move(pc_licenses_json)}, - {"GCLicenses", std::move(gc_licenses_json)}, - {"XBLicenses", std::move(xb_licenses_json)}, - {"BBLicenses", std::move(bb_licenses_json)}, - {"AutoPatchesEnabled", std::move(auto_patches_json)}, - })); -}; - -std::shared_ptr HTTPServer::generate_client_json( - shared_ptr c, shared_ptr item_name_index) const { - auto s = c->require_server_state(); - - const char* drop_notifications_mode = "unknown"; - switch (c->get_drop_notification_mode()) { - case Client::ItemDropNotificationMode::NOTHING: - drop_notifications_mode = "off"; - break; - case Client::ItemDropNotificationMode::RARES_ONLY: - drop_notifications_mode = "rare"; - break; - case Client::ItemDropNotificationMode::ALL_ITEMS: - drop_notifications_mode = "on"; - break; - case Client::ItemDropNotificationMode::ALL_ITEMS_INCLUDING_MESETA: - drop_notifications_mode = "every"; - break; - } - - auto ret = make_shared(phosg::JSON::dict({ - {"ID", c->id}, - {"RemoteAddress", c->channel->default_name()}, - {"Version", phosg::name_for_enum(c->version())}, - {"SubVersion", c->sub_version}, - {"Language", name_for_language(c->language())}, - {"LocationX", c->pos.x.load()}, - {"LocationZ", c->pos.z.load()}, - {"LocationFloor", c->floor}, - {"CanChat", c->can_chat}, - {"SpecificVersion", c->specific_version}, - {"SwitchAssistEnabled", (c->check_flag(Client::Flag::SWITCH_ASSIST_ENABLED) ? true : false)}, - {"InfiniteHPEnabled", (c->check_flag(Client::Flag::INFINITE_HP_ENABLED) ? true : false)}, - {"InfiniteTPEnabled", (c->check_flag(Client::Flag::INFINITE_TP_ENABLED) ? true : false)}, - {"DropNotificationMode", drop_notifications_mode}, - {"DebugEnabled", (c->check_flag(Client::Flag::DEBUG_ENABLED) ? true : false)}, - {"ProxySaveFilesEnabled", (c->check_flag(Client::Flag::PROXY_SAVE_FILES) ? true : false)}, - {"ProxyChatCommandsEnabled", (c->check_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED) ? true : false)}, - {"ProxyPlayerNotificationsEnabled", (c->check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) ? true : false)}, - {"ProxyEp3InfiniteMesetaEnabled", (c->check_flag(Client::Flag::PROXY_EP3_INFINITE_MESETA_ENABLED) ? true : false)}, - {"ProxyEp3InfiniteTimeEnabled", (c->check_flag(Client::Flag::PROXY_EP3_INFINITE_TIME_ENABLED) ? true : false)}, - {"ProxyBlockFunctionCalls", (c->check_flag(Client::Flag::PROXY_BLOCK_FUNCTION_CALLS) ? true : false)}, - {"ProxyEp3UnmaskWhispers", (c->check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) ? true : false)}, - {"OverrideRandomSeed", c->override_random_seed}, - {"OverrideSectionID", ((c->override_section_id != 0xFF) ? c->override_section_id : phosg::JSON(nullptr))}, - {"OverrideLobbyEvent", ((c->override_lobby_event != 0xFF) ? c->override_lobby_event : phosg::JSON(nullptr))}, - {"OverrideLobbyNumber", ((c->override_lobby_number != 0x80) ? c->override_lobby_number : phosg::JSON(nullptr))}, - })); - if (c->login) { - auto acc_json = HTTPServer::generate_account_json(c->login->account); - ret->emplace("Account", std::move(*acc_json)); - } else { - ret->emplace("Account", phosg::JSON()); - } - auto l = c->lobby.lock(); - if (l) { - ret->emplace("LobbyID", l->lobby_id); - ret->emplace("LobbyClientID", c->lobby_client_id); - } - if (c->version() == Version::BB_V4) { - ret->emplace("BBCharacterIndex", c->bb_character_index); - } - auto p = c->character_file(false, false); - if (p) { - if (!is_ep3(c->version())) { - if (c->version() != Version::DC_NTE) { - ret->emplace("InventoryLanguage", name_for_language(p->inventory.language)); - ret->emplace("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP)); - ret->emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); - if (!is_v1_or_v2(c->version())) { - ret->emplace("NumPowerMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER)); - ret->emplace("NumDefMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF)); - ret->emplace("NumMindMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND)); - ret->emplace("NumEvadeMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE)); - ret->emplace("NumLuckMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); - } - } - phosg::JSON items_json = phosg::JSON::list(); - for (size_t z = 0; z < p->inventory.num_items; z++) { - const auto& item = p->inventory.items[z]; - auto item_dict = phosg::JSON::dict({ - {"Flags", item.flags.load()}, - {"Data", item.data.hex()}, - {"ItemID", item.data.id.load()}, - }); - if (item_name_index) { - item_dict.emplace("Description", item_name_index->describe_item(item.data)); - } - items_json.emplace_back(std::move(item_dict)); - } - ret->emplace("InventoryItems", std::move(items_json)); - ret->emplace("ATP", p->disp.stats.char_stats.atp.load()); - ret->emplace("MST", p->disp.stats.char_stats.mst.load()); - ret->emplace("EVP", p->disp.stats.char_stats.evp.load()); - ret->emplace("HP", p->disp.stats.char_stats.hp.load()); - ret->emplace("DFP", p->disp.stats.char_stats.dfp.load()); - ret->emplace("ATA", p->disp.stats.char_stats.ata.load()); - ret->emplace("LCK", p->disp.stats.char_stats.lck.load()); - ret->emplace("EXP", p->disp.stats.experience.load()); - ret->emplace("Meseta", p->disp.stats.meseta.load()); - auto tech_levels_json = phosg::JSON::dict(); - for (size_t z = 0; z < 0x13; z++) { - auto level = p->get_technique_level(z); - tech_levels_json.emplace(name_for_technique(z), (level != 0xFF) ? (level + 1) : phosg::JSON(nullptr)); - } - ret->emplace("TechniqueLevels", std::move(tech_levels_json)); - } - ret->emplace("Height", p->disp.stats.height.load()); - ret->emplace("Level", p->disp.stats.level.load() + 1); - ret->emplace("NameColor", p->disp.visual.name_color.load()); - ret->emplace("ExtraModel", (p->disp.visual.validation_flags & 2) ? p->disp.visual.extra_model : phosg::JSON(nullptr)); - ret->emplace("SectionID", name_for_section_id(p->disp.visual.section_id)); - ret->emplace("CharClass", name_for_char_class(p->disp.visual.char_class)); - ret->emplace("Costume", p->disp.visual.costume.load()); - ret->emplace("Skin", p->disp.visual.skin.load()); - ret->emplace("Face", p->disp.visual.face.load()); - ret->emplace("Head", p->disp.visual.head.load()); - ret->emplace("Hair", p->disp.visual.hair.load()); - ret->emplace("HairR", p->disp.visual.hair_r.load()); - ret->emplace("HairG", p->disp.visual.hair_g.load()); - ret->emplace("HairB", p->disp.visual.hair_b.load()); - ret->emplace("ProportionX", p->disp.visual.proportion_x.load()); - ret->emplace("ProportionY", p->disp.visual.proportion_y.load()); - - ret->emplace("Name", p->disp.name.decode(c->language())); - ret->emplace("PlayTimeSeconds", p->play_time_seconds.load()); - - ret->emplace("AutoReply", p->auto_reply.decode(c->language())); - ret->emplace("InfoBoard", p->info_board.decode(c->language())); - auto battle_place_counts = phosg::JSON::list({ - p->battle_records.place_counts[0].load(), - p->battle_records.place_counts[1].load(), - p->battle_records.place_counts[2].load(), - p->battle_records.place_counts[3].load(), - }); - ret->emplace("BattlePlaceCounts", std::move(battle_place_counts)); - ret->emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load()); - - if (!is_ep3(c->version())) { - auto json_for_challenge_times = [](const parray& times) -> phosg::JSON { - auto times_json = phosg::JSON::list(); - for (size_t z = 0; z < times.size(); z++) { - times_json.emplace_back(times[z].decode()); - } - return times_json; - }; - ret->emplace("ChallengeTitleColorXRGB1555", p->challenge_records.title_color.load()); - ret->emplace("ChallengeTimesEp1Online", json_for_challenge_times(p->challenge_records.times_ep1_online)); - ret->emplace("ChallengeTimesEp2Online", json_for_challenge_times(p->challenge_records.times_ep2_online)); - ret->emplace("ChallengeTimesEp1Offline", json_for_challenge_times(p->challenge_records.times_ep1_offline)); - ret->emplace("ChallengeGraveIsEp2", p->challenge_records.grave_is_ep2 ? true : false); - ret->emplace("ChallengeGraveStageNum", p->challenge_records.grave_stage_num); - ret->emplace("ChallengeGraveFloor", p->challenge_records.grave_floor); - ret->emplace("ChallengeGraveDeaths", p->challenge_records.grave_deaths.load()); - { - uint16_t year = 2000 + ((p->challenge_records.grave_time >> 28) & 0x0F); - uint8_t month = (p->challenge_records.grave_time >> 24) & 0x0F; - uint8_t day = (p->challenge_records.grave_time >> 16) & 0xFF; - uint8_t hour = (p->challenge_records.grave_time >> 8) & 0xFF; - uint8_t minute = p->challenge_records.grave_time & 0xFF; - ret->emplace("ChallengeGraveTime", std::format("{:04}-{:02}-{:02} {:02}:{:02}:00", year, month, day, hour, minute)); - } - string grave_enemy_types; - if (p->challenge_records.grave_defeated_by_enemy_rt_index) { - for (EnemyType type : enemy_types_for_rare_table_index(p->challenge_records.grave_is_ep2 ? Episode::EP2 : Episode::EP1, p->challenge_records.grave_defeated_by_enemy_rt_index)) { - if (!grave_enemy_types.empty()) { - grave_enemy_types += "/"; - } - grave_enemy_types += phosg::name_for_enum(type); - } - } - ret->emplace("ChallengeGraveDefeatedByEnemy", std::move(grave_enemy_types)); - ret->emplace("ChallengeGraveX", p->challenge_records.grave_x.load()); - ret->emplace("ChallengeGraveY", p->challenge_records.grave_y.load()); - ret->emplace("ChallengeGraveZ", p->challenge_records.grave_z.load()); - ret->emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode()); - ret->emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode()); - ret->emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load()); - ret->emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.decode()); - ret->emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load()); - ret->emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.decode()); - ret->emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load()); - ret->emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.decode()); - ret->emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode()); - } - } - auto ses = c->proxy_session; - if (ses) { - auto lobby_players_json = phosg::JSON::list(); - for (size_t z = 0; z < ses->lobby_players.size(); z++) { - const auto& p = ses->lobby_players[z]; - if (p.guild_card_number) { - lobby_players_json.emplace_back(phosg::JSON::dict({ - {"GuildCardNumber", p.guild_card_number}, - {"Name", p.name}, - {"Language", name_for_language(p.language)}, - {"SectionID", name_for_section_id(p.section_id)}, - {"CharClass", name_for_char_class(p.char_class)}, - })); - lobby_players_json.back().emplace("XBUserID", p.xb_user_id ? p.xb_user_id : phosg::JSON(nullptr)); - } else { - lobby_players_json.emplace_back(nullptr); - } - } - - auto ses_json = phosg::JSON::dict({ - {"RemoteServerAddress", ses->server_channel->default_name()}, - {"RemoteGuildCardNumber", ses->remote_guild_card_number}, - {"RemoteClientConfigData", phosg::format_data_string(&ses->remote_client_config_data[0], ses->remote_client_config_data.size())}, - {"IsInGame", ses->is_in_game}, - {"IsInQuest", ses->is_in_quest}, - {"LobbyLeaderClientID", ses->leader_client_id}, - {"LobbyEvent", ses->lobby_event}, - {"LobbyDifficulty", name_for_difficulty(ses->lobby_difficulty)}, - {"LobbySectionID", name_for_section_id(ses->lobby_section_id)}, - {"LobbyMode", name_for_mode(ses->lobby_mode)}, - {"LobbyEpisode", name_for_episode(ses->lobby_episode)}, - {"LobbyRandomSeed", ses->lobby_random_seed}, - {"LobbyPlayers", std::move(lobby_players_json)}, - }); - switch (ses->drop_mode) { - case ProxyDropMode::DISABLED: - ses_json.emplace("DropMode", "none"); - break; - case ProxyDropMode::PASSTHROUGH: - ses_json.emplace("DropMode", "default"); - break; - case ProxyDropMode::INTERCEPT: - ses_json.emplace("DropMode", "proxy"); - break; - } - ret->emplace("ProxySession", std::move(ses_json)); - } else { - ret->emplace("ProxySession", phosg::JSON()); - } - return ret; -} - -std::shared_ptr HTTPServer::generate_lobby_json( - shared_ptr l, shared_ptr item_name_index) const { - std::array, 12> clients; - - auto client_ids_json = phosg::JSON::list(); - for (size_t z = 0; z < l->max_clients; z++) { - client_ids_json.emplace_back(l->clients[z] ? l->clients[z]->id : phosg::JSON(nullptr)); - } - - auto ret = make_shared(phosg::JSON::dict({ - {"ID", l->lobby_id}, - {"AllowedVersions", l->allowed_versions}, - {"Event", l->event}, - {"LeaderClientID", l->leader_id}, - {"MaxClients", l->max_clients}, - {"IdleTimeoutUsecs", l->idle_timeout_usecs}, - {"ClientIDs", std::move(client_ids_json)}, - {"IsGame", l->is_game()}, - {"IsPersistent", l->check_flag(Lobby::Flag::PERSISTENT)}, - })); - - if (l->is_game()) { - ret->emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)); - ret->emplace("MinLevel", l->min_level + 1); - ret->emplace("MaxLevel", l->max_level + 1); - ret->emplace("Episode", name_for_episode(l->episode)); - ret->emplace("HasPassword", !l->password.empty()); - ret->emplace("Name", l->name); - ret->emplace("RandomSeed", l->random_seed); - if (l->episode != Episode::EP3) { - ret->emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); - ret->emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); - ret->emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); - ret->emplace("Variations", l->variations.json()); - uint8_t effective_section_id = l->effective_section_id(); - if (effective_section_id < 10) { - ret->emplace("SectionID", name_for_section_id(effective_section_id)); - } else { - ret->emplace("SectionID", nullptr); - } - ret->emplace("Mode", name_for_mode(l->mode)); - ret->emplace("Difficulty", name_for_difficulty(l->difficulty)); - ret->emplace("BaseEXPMultiplier", l->base_exp_multiplier); - ret->emplace("EXPShareMultiplier", l->exp_share_multiplier); - ret->emplace("AllowedDropModes", l->allowed_drop_modes); - switch (l->drop_mode) { - case ServerDropMode::DISABLED: - ret->emplace("DropMode", "none"); - break; - case ServerDropMode::CLIENT: - ret->emplace("DropMode", "client"); - break; - case ServerDropMode::SERVER_SHARED: - ret->emplace("DropMode", "shared"); - break; - case ServerDropMode::SERVER_PRIVATE: - ret->emplace("DropMode", "private"); - break; - case ServerDropMode::SERVER_DUPLICATE: - ret->emplace("DropMode", "duplicate"); - break; - } - if (l->mode == GameMode::CHALLENGE) { - ret->emplace("ChallengeEXPMultiplier", l->challenge_exp_multiplier); - if (l->challenge_params) { - ret->emplace("ChallengeStageNumber", l->challenge_params->stage_number); - ret->emplace("ChallengeRankColor", l->challenge_params->rank_color); - ret->emplace("ChallengeRankText", l->challenge_params->rank_text); - ret->emplace("ChallengeRank0ThresholdBitmask", l->challenge_params->rank_thresholds[0].bitmask); - ret->emplace("ChallengeRank0ThresholdSeconds", l->challenge_params->rank_thresholds[0].seconds); - ret->emplace("ChallengeRank1ThresholdBitmask", l->challenge_params->rank_thresholds[1].bitmask); - ret->emplace("ChallengeRank1ThresholdSeconds", l->challenge_params->rank_thresholds[1].seconds); - ret->emplace("ChallengeRank2ThresholdBitmask", l->challenge_params->rank_thresholds[2].bitmask); - ret->emplace("ChallengeRank2ThresholdSeconds", l->challenge_params->rank_thresholds[2].seconds); - } - } - - auto floor_items_json = phosg::JSON::list(); - for (size_t floor = 0; floor < l->floor_item_managers.size(); floor++) { - for (const auto& it : l->floor_item_managers[floor].items) { - const auto& item = it.second; - auto item_dict = phosg::JSON::dict({ - {"LocationFloor", floor}, - {"LocationX", item->pos.x.load()}, - {"LocationZ", item->pos.z.load()}, - {"DropNumber", item->drop_number}, - {"Flags", item->flags}, - {"Data", item->data.hex()}, - {"ItemID", item->data.id.load()}, - }); - if (item_name_index) { - item_dict.emplace("Description", item_name_index->describe_item(item->data)); - } - floor_items_json.emplace_back(std::move(item_dict)); - } - } - ret->emplace("FloorItems", std::move(floor_items_json)); - ret->emplace("Quest", l->quest ? l->quest->json() : phosg::JSON(nullptr)); - - } else { - ret->emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); - ret->emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); - ret->emplace("SpectatorsForbidden", l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)); - - auto ep3s = l->ep3_server; - if (ep3s) { - auto players_json = phosg::JSON::list(); - for (size_t z = 0; z < 4; z++) { - if (!ep3s->name_entries[z].present) { - players_json.emplace_back(nullptr); - } else { - auto lc = l->clients[z]; - - auto deck_entry = ep3s->deck_entries[z]; - phosg::JSON deck_json = nullptr; - if (deck_entry) { - auto cards_json = phosg::JSON::list(); - for (size_t w = 0; w < deck_entry->card_ids.size(); w++) { - try { - const auto& ce = ep3s->options.card_index->definition_for_id(deck_entry->card_ids[w]); - auto name = ce->def.en_name.decode(); - if (name.empty()) { - name = ce->def.en_short_name.decode(); - } - if (name.empty()) { - name = ce->def.jp_name.decode(); - } - if (name.empty()) { - name = ce->def.jp_short_name.decode(); - } - cards_json.emplace_back(name); - } catch (const out_of_range&) { - cards_json.emplace_back(deck_entry->card_ids[w].load()); - } - } - deck_json = phosg::JSON::dict({ - {"Name", deck_entry->name.decode(lc ? lc->language() : Language::ENGLISH)}, - {"TeamID", deck_entry->team_id.load()}, - {"Cards", std::move(cards_json)}, - {"GodWhimFlag", deck_entry->god_whim_flag}, - {"PlayerLevel", deck_entry->player_level.load() + 1}, - }); - } - - auto player_json = phosg::JSON::dict({ - {"PlayerName", ep3s->name_entries[z].name.decode(lc ? lc->language() : Language::ENGLISH)}, - {"ClientID", ep3s->name_entries[z].client_id}, - {"IsCOM", !!ep3s->name_entries[z].is_cpu_player}, - {"Deck", std::move(deck_json)}, - }); - players_json.emplace_back(std::move(player_json)); - } - } - auto battle_state_json = phosg::JSON::dict({ - {"BehaviorFlags", ep3s->options.behavior_flags}, - {"RandomSeed", ep3s->options.rand_crypt->seed()}, - {"Tournament", ep3s->options.tournament ? ep3s->options.tournament->json() : nullptr}, - {"MapNumber", ep3s->last_chosen_map ? ep3s->last_chosen_map->map_number : phosg::JSON(nullptr)}, - {"EnvironmentNumber", ep3s->map_and_rules ? ep3s->map_and_rules->environment_number : phosg::JSON(nullptr)}, - {"Rules", ep3s->map_and_rules ? ep3s->map_and_rules->rules.json() : nullptr}, - {"Players", std::move(players_json)}, - {"IsBattleFinished", ep3s->battle_finished}, - {"IsBattleInprogress", ep3s->battle_in_progress}, - {"RoundNumber", ep3s->round_num}, - {"FirstTeamTurn", ep3s->first_team_turn}, - {"CurrentTeamTurn", ep3s->current_team_turn1}, - {"BattlePhase", phosg::name_for_enum(ep3s->battle_phase)}, - {"SetupPhase", ep3s->setup_phase}, - {"RegistrationPhase", ep3s->registration_phase}, - {"ActionSubphase", ep3s->action_subphase}, - {"BattleStartTimeUsecs", ep3s->battle_start_usecs}, - {"TeamEXP", phosg::JSON::list({ep3s->team_exp[0], ep3s->team_exp[1]})}, - {"TeamDiceBonus", phosg::JSON::list({ep3s->team_dice_bonus[0], ep3s->team_dice_bonus[1]})}, - }); - // std::shared_ptr state_flags; - // std::array, 4> player_states; - ret->emplace("Episode3BattleState", std::move(battle_state_json)); - } else { - ret->emplace("Episode3BattleState", nullptr); - } - auto watched_lobby = l->watched_lobby.lock(); - if (watched_lobby) { - ret->emplace("WatchedLobbyID", watched_lobby->lobby_id); - } - auto watcher_lobby_ids_json = phosg::JSON::list(); - for (const auto& watcher_lobby : l->watcher_lobbies) { - watcher_lobby_ids_json.emplace_back(watcher_lobby->lobby_id); - } - ret->emplace("WatcherLobbyIDs", std::move(watcher_lobby_ids_json)); - ret->emplace("IsReplayLobby", !!l->battle_player); - } - - } else { // Not game - ret->emplace("IsPublic", l->check_flag(Lobby::Flag::PUBLIC)); - ret->emplace("IsDefault", l->check_flag(Lobby::Flag::DEFAULT)); - ret->emplace("IsOverflow", l->check_flag(Lobby::Flag::IS_OVERFLOW)); - ret->emplace("Block", l->block); - } - return ret; -} - -std::shared_ptr HTTPServer::generate_accounts_json() const { - auto res = make_shared(phosg::JSON::list()); - for (const auto& it : this->state->account_index->all()) { - res->emplace_back(it->json()); - } - return res; -} - -std::shared_ptr HTTPServer::generate_clients_json() const { - auto res = make_shared(phosg::JSON::list()); - for (const auto& it : this->state->game_server->all_clients()) { - auto client_json = this->generate_client_json(it, this->state->item_name_index_opt(it->version())); - res->emplace_back(std::move(*client_json)); - } - return res; -} - -std::shared_ptr HTTPServer::generate_server_info_json() const { - size_t game_count = 0; - size_t lobby_count = 0; - for (const auto& it : this->state->id_to_lobby) { - if (it.second->is_game()) { - game_count++; - } else { - lobby_count++; - } - } - uint64_t uptime_usecs = phosg::now() - this->state->creation_time; - return make_shared(phosg::JSON::dict({ - {"StartTimeUsecs", this->state->creation_time}, - {"StartTime", phosg::format_time(this->state->creation_time)}, - {"UptimeUsecs", uptime_usecs}, - {"Uptime", phosg::format_duration(uptime_usecs)}, - {"LobbyCount", lobby_count}, - {"GameCount", game_count}, - {"ClientCount", this->state->game_server->all_clients().size() - ProxySession::num_proxy_sessions}, - {"ProxySessionCount", ProxySession::num_proxy_sessions}, - {"ServerName", this->state->name}, - })); -} - -std::shared_ptr HTTPServer::generate_lobbies_json() const { - auto res = make_shared(phosg::JSON::list()); - for (const auto& it : this->state->id_to_lobby) { - auto leader = it.second->clients[it.second->leader_id]; - Version v = leader ? leader->version() : Version::BB_V4; - auto lobby_json = this->generate_lobby_json(it.second, this->state->item_name_index_opt(v)); - res->emplace_back(std::move(*lobby_json)); - } - return res; -} - -std::shared_ptr HTTPServer::generate_summary_json() const { - auto clients_json = phosg::JSON::list(); - for (const auto& c : this->state->game_server->all_clients()) { - auto p = c->character_file(false, false); - auto l = c->lobby.lock(); - clients_json.emplace_back(phosg::JSON::dict({ - {"ID", c->id}, - {"AccountID", c->login ? c->login->account->account_id : phosg::JSON(nullptr)}, - {"Name", p ? p->disp.name.decode(c->language()) : phosg::JSON(nullptr)}, - {"Version", phosg::name_for_enum(c->version())}, - {"Language", name_for_language(c->language())}, - {"Level", p ? (p->disp.stats.level + 1) : phosg::JSON(nullptr)}, - {"Class", p ? name_for_char_class(p->disp.visual.char_class) : phosg::JSON(nullptr)}, - {"SectionID", p ? name_for_section_id(p->disp.visual.section_id) : phosg::JSON(nullptr)}, - {"LobbyID", l ? l->lobby_id : phosg::JSON(nullptr)}, - {"IsOnProxy", c->proxy_session ? true : false}, - })); - } - - auto games_json = phosg::JSON::list(); - for (const auto& it : this->state->id_to_lobby) { - auto l = it.second; - if (l->is_game()) { - auto game_json = phosg::JSON::dict({ - {"ID", l->lobby_id}, - {"Name", l->name}, - {"Players", l->count_clients()}, - {"CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)}, - {"Episode", name_for_episode(l->episode)}, - {"HasPassword", !l->password.empty()}, - }); - if (l->episode == Episode::EP3) { - auto ep3s = l->ep3_server; - game_json.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); - game_json.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); - game_json.emplace("MapNumber", (ep3s && ep3s->last_chosen_map) ? ep3s->last_chosen_map->map_number : phosg::JSON(nullptr)); - game_json.emplace("Rules", (ep3s && ep3s->map_and_rules) ? ep3s->map_and_rules->rules.json() : nullptr); - } else { - game_json.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); - game_json.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); - game_json.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); - uint8_t effective_section_id = l->effective_section_id(); - if (effective_section_id < 10) { - game_json.emplace("SectionID", name_for_section_id(effective_section_id)); - } else { - game_json.emplace("SectionID", nullptr); - } - game_json.emplace("Mode", name_for_mode(l->mode)); - game_json.emplace("Difficulty", name_for_difficulty(l->difficulty)); - game_json.emplace("Quest", l->quest ? l->quest->json() : phosg::JSON(nullptr)); - } - games_json.emplace_back(std::move(game_json)); - } - } - - auto server_json = this->generate_server_info_json(); - return make_shared(phosg::JSON::dict({ - {"Clients", std::move(clients_json)}, - {"Games", std::move(games_json)}, - {"Server", std::move(*server_json)}, - })); -} - -std::shared_ptr HTTPServer::generate_all_json() const { - auto clients_json = this->generate_clients_json(); - auto lobbies_json = this->generate_lobbies_json(); - auto server_json = this->generate_server_info_json(); - return make_shared(phosg::JSON::dict({ - {"Clients", std::move(*clients_json)}, - {"Lobbies", std::move(*lobbies_json)}, - {"Server", std::move(*server_json)}, - })); -} - -asio::awaitable> HTTPServer::generate_ep3_cards_json(bool trial) const { - auto& index = trial ? this->state->ep3_card_index_trial : this->state->ep3_card_index; - co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { - return make_shared(index->definitions_json()); - }); -} - -std::shared_ptr HTTPServer::generate_common_table_list_json() const { - auto ret = make_shared(phosg::JSON::list()); - for (const auto& it : this->state->common_item_sets) { - ret->emplace_back(it.first); - } - return ret; -} - -asio::awaitable> HTTPServer::generate_common_table_json(const std::string& table_name) const { - try { - const auto& table = this->state->common_item_sets.at(table_name); - co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { - return make_shared(table->json()); - }); - } catch (const out_of_range&) { - throw HTTPError(404, "Table does not exist"); - } -} - -std::shared_ptr HTTPServer::generate_rare_table_list_json() const { - auto ret = make_shared(phosg::JSON::list()); - for (const auto& it : this->state->rare_item_sets) { - ret->emplace_back(it.first); - } - return ret; -} - -asio::awaitable> HTTPServer::generate_rare_table_json(const std::string& table_name) const { - try { - const auto& table = this->state->rare_item_sets.at(table_name); - shared_ptr name_index; - if (table_name.ends_with("-v1")) { - name_index = this->state->item_name_index_opt(Version::DC_V1); - } else if (table_name.ends_with("-v2")) { - name_index = this->state->item_name_index_opt(Version::PC_V2); - } else if (table_name.ends_with("-v3")) { - name_index = this->state->item_name_index_opt(Version::GC_V3); - } else if (table_name.ends_with("-v4")) { - name_index = this->state->item_name_index_opt(Version::BB_V4); - } - co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { - return make_shared(table->json(name_index)); - }); - } catch (const out_of_range&) { - throw HTTPError(404, "Table does not exist"); - } -} - -asio::awaitable> HTTPServer::generate_quest_list_json() { - co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { - return make_shared(this->state->quest_index->json()); - }); -} - -void HTTPServer::require_GET(const HTTPRequest& req) { - if (req.method != HTTPRequest::Method::GET) { - throw HTTPError(405, "GET method required for this endpoint"); - } -} - -phosg::JSON HTTPServer::require_POST(const HTTPRequest& req) { - if (req.method != HTTPRequest::Method::POST) { - throw HTTPError(405, "POST method required for this endpoint"); - } - - auto* content_type = req.get_header("content-type"); - if (!content_type || (*content_type != "application/json")) { - throw HTTPError(400, "POST requests must use the application/json content type"); - } - - try { - return phosg::JSON::parse(req.data); - } catch (const exception& e) { - throw HTTPError(400, string("Invalid JSON: ") + e.what()); - } -} - asio::awaitable> HTTPServer::handle_request(shared_ptr c, HTTPRequest&& req) { - shared_ptr ret; + variant> ret; uint32_t serialize_options = phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY; uint64_t start_time = phosg::now(); @@ -766,7 +766,6 @@ asio::awaitable> HTTPServer::handle_request(shared auto resp = make_unique(); resp->http_version = req.http_version; resp->response_code = 200; - resp->headers.emplace("Content-Type", "application/json"); resp->headers.emplace("Server", "newserv"); resp->headers.emplace("X-Newserv-Revision", GIT_REVISION_HASH); resp->headers.emplace("X-Newserv-Build-Timestamp", phosg::format_time(BUILD_TIMESTAMP)); @@ -781,74 +780,7 @@ asio::awaitable> HTTPServer::handle_request(shared serialize_options |= phosg::JSON::SerializeOption::HEX_INTEGERS; } - if (req.path == "/") { - this->require_GET(req); - ret = this->generate_server_version(); - - } else if (req.path == "/y/shell-exec") { - auto json = this->require_POST(req); - auto command = json.get_string("command"); - try { - auto dispatch_res = co_await ShellCommand::dispatch_str(this->state, command); - ret = make_shared(phosg::JSON::dict({{"result", phosg::join(dispatch_res, "\n")}})); - } catch (const exception& e) { - throw HTTPError(400, e.what()); - } - - } else if (req.path == "/y/rare-drops/stream") { - this->require_GET(req); - if (!(co_await this->enable_websockets(c, req))) { - throw HTTPError(400, "this path requires a websocket connection"); - } else { - this->rare_drop_subscribers.emplace(c); - auto version_message = this->generate_server_version(); - co_await c->send_websocket_message(version_message->serialize()); - co_return nullptr; - } - - } else if (req.path == "/y/data/ep3-cards") { - this->require_GET(req); - ret = co_await this->generate_ep3_cards_json(false); - } else if (req.path == "/y/data/ep3-cards-trial") { - this->require_GET(req); - ret = co_await this->generate_ep3_cards_json(true); - } else if (req.path == "/y/data/common-tables") { - this->require_GET(req); - ret = this->generate_common_table_list_json(); - } else if (req.path.starts_with("/y/data/common-tables/")) { - this->require_GET(req); - ret = co_await this->generate_common_table_json(req.path.substr(22)); - } else if (req.path == "/y/data/rare-tables") { - this->require_GET(req); - ret = this->generate_rare_table_list_json(); - } else if (req.path.starts_with("/y/data/rare-tables/")) { - this->require_GET(req); - ret = co_await this->generate_rare_table_json(req.path.substr(20)); - } else if (req.path == "/y/data/quests") { - this->require_GET(req); - ret = co_await this->generate_quest_list_json(); - } else if (req.path == "/y/data/config") { - this->require_GET(req); - ret = this->state->config_json; - } else if (req.path == "/y/accounts") { - this->require_GET(req); - ret = this->generate_accounts_json(); - } else if (req.path == "/y/clients") { - this->require_GET(req); - ret = this->generate_clients_json(); - } else if (req.path == "/y/lobbies") { - this->require_GET(req); - ret = this->generate_lobbies_json(); - } else if (req.path == "/y/server") { - this->require_GET(req); - ret = this->generate_server_info_json(); - } else if (req.path == "/y/summary") { - this->require_GET(req); - ret = this->generate_summary_json(); - - } else { - throw HTTPError(404, "unknown action"); - } + ret = co_await this->router.call_handler(c, req); } catch (const HTTPError& e) { ret = make_shared(phosg::JSON::dict({{"Error", true}, {"Message", e.what()}})); @@ -857,22 +789,34 @@ asio::awaitable> HTTPServer::handle_request(shared ret = make_shared(phosg::JSON::dict({{"Error", true}, {"Message", e.what()}})); resp->response_code = 500; } - - if (!ret) { - throw logic_error("ret was not set after HTTP handler completed"); - } - uint64_t handler_end = phosg::now(); - resp->data = co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> string { - return ret->serialize(serialize_options, 0); - }); - uint64_t serialize_end = phosg::now(); - string handler_time = phosg::format_duration(handler_end - start_time); - string serialize_time = phosg::format_duration(serialize_end - handler_end); - string size_str = phosg::format_size(resp->data.size()); - this->log.info_f("{} in [handler: {}, serialize: {}, size: {}]", - req.path, handler_time, serialize_time, size_str); + if (holds_alternative>(ret)) { + // If the handler returns nullptr (not JSON null), assume it called enable_websockets and send no response + auto& json = get>(ret); + if (!json) { + co_return nullptr; + } + resp->headers.emplace("Content-Type", "application/json"); + resp->data = co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> string { + return json->serialize(serialize_options, 0); + }); + uint64_t serialize_end = phosg::now(); + + string handler_time = phosg::format_duration(handler_end - start_time); + string serialize_time = phosg::format_duration(serialize_end - handler_end); + string size_str = phosg::format_size(resp->data.size()); + this->log.info_f("{} in [handler: {}, serialize: {}, size: {}]", req.path, handler_time, serialize_time, size_str); + + } else { + auto& raw_resp = get(ret); + resp->headers.emplace("Content-Type", std::move(raw_resp.content_type)); + resp->data = std::move(raw_resp.data); + + string handler_time = phosg::format_duration(handler_end - start_time); + string size_str = phosg::format_size(resp->data.size()); + this->log.info_f("{} in [handler: {}, size: {}]", req.path, handler_time, size_str); + } co_return resp; } diff --git a/src/HTTPServer.hh b/src/HTTPServer.hh index e31a19e1..9ba8480e 100644 --- a/src/HTTPServer.hh +++ b/src/HTTPServer.hh @@ -4,6 +4,7 @@ #include #include +#include #include "AsyncHTTPServer.hh" #include "ServerState.hh" @@ -20,28 +21,13 @@ public: asio::awaitable send_rare_drop_notification(std::shared_ptr message); protected: + struct RawResponse { + std::string content_type; + std::string data; + }; std::shared_ptr state; std::unordered_set> rare_drop_subscribers; - - std::shared_ptr generate_server_version() const; - std::shared_ptr generate_account_json(std::shared_ptr a) const; - std::shared_ptr generate_client_json( - std::shared_ptr c, std::shared_ptr item_name_index) const; - std::shared_ptr generate_lobby_json( - std::shared_ptr l, std::shared_ptr item_name_index) const; - std::shared_ptr generate_accounts_json() const; - std::shared_ptr generate_clients_json() const; - std::shared_ptr generate_server_info_json() const; - std::shared_ptr generate_lobbies_json() const; - std::shared_ptr generate_summary_json() const; - std::shared_ptr generate_all_json() const; - - asio::awaitable> generate_ep3_cards_json(bool trial) const; - std::shared_ptr generate_common_table_list_json() const; - std::shared_ptr generate_rare_table_list_json() const; - asio::awaitable> generate_common_table_json(const std::string& table_name) const; - asio::awaitable> generate_rare_table_json(const std::string& table_name) const; - asio::awaitable> generate_quest_list_json(); + HTTPRouter>> router; void require_GET(const HTTPRequest& req); phosg::JSON require_POST(const HTTPRequest& req); diff --git a/src/Quest.cc b/src/Quest.cc index dba53de2..b583cfff 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -845,9 +845,6 @@ phosg::JSON QuestIndex::json() const { {"Categories", std::move(categories_json)}, {"Quests", std::move(quests_json)}, }); - // std::map> quests_by_number; - // std::map> quests_by_name; - // std::map>> quests_by_category_id_and_number; } shared_ptr QuestIndex::get(uint32_t quest_number) const { diff --git a/src/QuestMetadata.cc b/src/QuestMetadata.cc index beded9ce..17d4865a 100644 --- a/src/QuestMetadata.cc +++ b/src/QuestMetadata.cc @@ -171,7 +171,7 @@ phosg::JSON QuestMetadata::json() const { enemy_exp_overrides_json.emplace(key_str, exp_override); } - auto create_item_mask_entries_json = phosg::JSON::dict(); + auto create_item_mask_entries_json = phosg::JSON::list(); for (const auto& item : this->create_item_mask_entries) { create_item_mask_entries_json.emplace_back(item.str()); } diff --git a/src/TeamIndex.cc b/src/TeamIndex.cc index 8d42461a..2d18ca49 100644 --- a/src/TeamIndex.cc +++ b/src/TeamIndex.cc @@ -20,6 +20,7 @@ TeamIndex::Team::Member::Member(const phosg::JSON& json) try { this->account_id = json.get_int("AccountID"); } catch (const out_of_range&) { + // Old format this->account_id = json.get_int("SerialNumber"); } } @@ -78,7 +79,7 @@ void TeamIndex::Team::load_config() { this->reward_flags = json.get_int("RewardFlags"); } -void TeamIndex::Team::save_config() const { +phosg::JSON TeamIndex::Team::json() const { phosg::JSON members_json = phosg::JSON::list(); for (const auto& it : this->members) { members_json.emplace_back(it.second.json()); @@ -87,14 +88,17 @@ void TeamIndex::Team::save_config() const { for (const auto& it : this->reward_keys) { reward_keys_json.emplace_back(it); } - phosg::JSON root = phosg::JSON::dict({ + return phosg::JSON::dict({ {"Name", this->name}, {"SpentPoints", this->spent_points}, {"Members", std::move(members_json)}, {"RewardKeys", std::move(reward_keys_json)}, {"RewardFlags", this->reward_flags}, }); - phosg::save_file(this->json_filename(), root.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY)); +} + +void TeamIndex::Team::save_config() const { + phosg::save_file(this->json_filename(), this->json().serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY)); } void TeamIndex::Team::load_flag() { @@ -110,16 +114,21 @@ void TeamIndex::Team::load_flag() { } } -void TeamIndex::Team::save_flag() const { - if (!this->flag_data) { - return; - } +phosg::ImageRGBA8888N TeamIndex::Team::decode_flag_data() const { phosg::ImageRGBA8888N img(32, 32); for (size_t y = 0; y < 32; y++) { for (size_t x = 0; x < 32; x++) { img.write(x, y, phosg::rgba8888_for_argb1555(this->flag_data->at(y * 0x20 + x))); } } + return img; +} + +void TeamIndex::Team::save_flag() const { + if (!this->flag_data) { + return; + } + auto img = this->decode_flag_data(); phosg::save_file(this->flag_filename(), img.serialize(phosg::ImageFormat::WINDOWS_BITMAP)); } diff --git a/src/TeamIndex.hh b/src/TeamIndex.hh index 64b54d05..93de4a66 100644 --- a/src/TeamIndex.hh +++ b/src/TeamIndex.hh @@ -76,6 +76,7 @@ public: void load_config(); void save_config() const; void load_flag(); + phosg::ImageRGBA8888N decode_flag_data() const; void save_flag() const; void delete_files() const;