diff --git a/README.md b/README.md index cfff4b18..11a154a5 100644 --- a/README.md +++ b/README.md @@ -677,19 +677,21 @@ To enable the HTTP server, add a port number in the HTTPListen list in config.js All returned data is JSON-encoded, and all request data (for POST requests) must also be JSON-encoded with the `Content-Type: application/json` header. The HTTP server has the following endpoints: +* `GET /`: Returns the server's build date and revision. * `GET /y/data/ep3-cards`: Returns the Episode 3 card definitions. * `GET /y/data/ep3-cards-trial`: Returns the Episode 3 Trial Edition card definitions. * `GET /y/data/common-tables`: Returns the parameters for generating common items (ItemPT files). This endpoint returns a lot of data and can be slow! * `GET /y/data/rare-tables`: Returns a list of rare table names. * `GET /y/data/rare-tables/` (for example, `/y/data/rare-tables/rare-table-v4`): Returns the contents of a rare item table. +* `GET /y/data/quests`: Returns metadata about all available quests and quest categories. * `GET /y/data/config`: Returns the server's configuration file. +* `GET /y/accounts`: Returns information about all registered accounts. * `GET /y/clients`: Returns information about all connected clients on the game server. * `GET /y/proxy-clients`: Returns information about all connected clients on the proxy server. * `GET /y/lobbies`: Returns information about all lobbies and games. * `GET /y/server`: Returns information about the server. -* `GET /y/all`: Returns the same information as the above four endpoints, but in a single call. This endpoint can be slow! * `GET /y/summary`: Returns a summary of the server's state, connected clients, active games, and proxy sessions. -* `GET /y/rare-drops/stream`: WebSocket endpoint that sends messages whenever an announceable rare item is dropped in any game. See below. +* `WS /y/rare-drops/stream`: WebSocket endpoint that sends messages whenever an announceable rare item is dropped in any game. See below. * `POST /y/shell-exec`: Runs a server shell command. Input should be a JSON dict of e.g. `{"command": "announce hello"}`; response will be a JSON dict of `{"result": ""}` or an HTTP error. ### Rare drop stream endpoint diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index 942ccecb..291acca4 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -13,6 +13,7 @@ #include "EventUtils.hh" #include "Loggers.hh" #include "ProxyServer.hh" +#include "Revision.hh" #include "Server.hh" #include "ShellCommands.hh" @@ -441,22 +442,12 @@ void HTTPServer::dispatch_handle_request(struct evhttp_request* req, void* ctx) reinterpret_cast(ctx)->handle_request(req); } -phosg::JSON HTTPServer::generate_quest_json_st(shared_ptr q) { - if (!q) { - return nullptr; - } - auto battle_rules_json = q->battle_rules ? q->battle_rules->json() : nullptr; - auto challenge_template_index_json = (q->challenge_template_index >= 0) - ? q->challenge_template_index - : phosg::JSON(nullptr); +phosg::JSON HTTPServer::generate_server_version_st() { return phosg::JSON::dict({ - {"Number", q->quest_number}, - {"Episode", name_for_episode(q->episode)}, - {"Joinable", q->joinable}, - {"LockStatusRegister", (q->lock_status_register >= 0) ? q->lock_status_register : phosg::JSON(nullptr)}, - {"Name", q->name}, - {"BattleRules", std::move(battle_rules_json)}, - {"ChallengeTemplateIndex", std::move(challenge_template_index_json)}, + {"ServerType", "newserv"}, + {"BuildTime", BUILD_TIMESTAMP}, + {"BuildTimeStr", phosg::format_time(BUILD_TIMESTAMP)}, + {"Revision", GIT_REVISION_HASH}, }); } @@ -575,7 +566,7 @@ phosg::JSON HTTPServer::generate_game_client_json_st(shared_ptr c, if (p) { if (!is_ep3(c->version())) { if (c->version() != Version::DC_NTE) { - ret.emplace("InventoryLanguage", p->inventory.language); + ret.emplace("InventoryLanguage", name_for_language_code(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())) { @@ -861,7 +852,7 @@ phosg::JSON HTTPServer::generate_lobby_json_st(shared_ptr l, shared } } ret.emplace("FloorItems", std::move(floor_items_json)); - ret.emplace("Quest", HTTPServer::generate_quest_json_st(l->quest)); + ret.emplace("Quest", l->quest ? l->quest->json() : phosg::JSON(nullptr)); } else { ret.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); @@ -966,6 +957,16 @@ phosg::JSON HTTPServer::generate_lobby_json_st(shared_ptr l, shared return ret; } +phosg::JSON HTTPServer::generate_accounts_json() const { + return call_on_event_thread(this->state->base, [&]() { + auto res = phosg::JSON::list(); + for (const auto& it : this->state->account_index->all()) { + res.emplace_back(it->json()); + } + return res; + }); +} + phosg::JSON HTTPServer::generate_game_server_clients_json() const { return call_on_event_thread(this->state->base, [&]() { auto res = phosg::JSON::list(); @@ -1083,7 +1084,7 @@ phosg::JSON HTTPServer::generate_summary_json() const { game_json.emplace("SectionID", name_for_section_id(l->effective_section_id())); game_json.emplace("Mode", name_for_mode(l->mode)); game_json.emplace("Difficulty", name_for_difficulty(l->difficulty)); - game_json.emplace("Quest", this->generate_quest_json_st(l->quest)); + game_json.emplace("Quest", l->quest ? l->quest->json() : phosg::JSON(nullptr)); } games_json.emplace_back(std::move(game_json)); } @@ -1155,6 +1156,12 @@ phosg::JSON HTTPServer::generate_rare_table_json(const std::string& table_name) } } +phosg::JSON HTTPServer::generate_quest_list_json(std::shared_ptr quest_index) { + return call_on_event_thread(this->state->base, [&]() { + return quest_index->json(); + }); +} + void HTTPServer::require_GET(struct evhttp_request* req) { if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) { throw HTTPServer::http_error(405, "GET method required for this endpoint"); @@ -1198,23 +1205,7 @@ void HTTPServer::handle_request(struct evhttp_request* req) { if (uri == "/") { this->require_GET(req); - auto endpoints_json = phosg::JSON::list({ - "/y/data/ep3-cards", - "/y/data/ep3-cards-trial", - "/y/data/common-tables", - "/y/data/rare-tables", - "/y/data/rare-tables/", - "/y/data/config", - "/y/clients", - "/y/proxy-clients", - "/y/lobbies", - "/y/server", - "/y/rare-drops/stream", - "/y/summary", - "/y/all", - "/y/shell-exec", - }); - ret = make_shared(phosg::JSON::dict({{"endpoints", std::move(endpoints_json)}})); + ret = make_shared(this->generate_server_version_st()); } else if (uri == "/y/shell-exec") { auto json = this->require_POST(req); @@ -1233,7 +1224,7 @@ void HTTPServer::handle_request(struct evhttp_request* req) { throw http_error(400, "this path requires a websocket connection"); } else { this->rare_drop_subscribers.emplace(c); - auto version_message = phosg::JSON::dict({{"ServerType", "newserv"}}); + auto version_message = this->generate_server_version_st(); this->send_websocket_message(c, version_message.serialize()); return; } @@ -1253,9 +1244,15 @@ void HTTPServer::handle_request(struct evhttp_request* req) { } else if (!strncmp(uri.c_str(), "/y/data/rare-tables/", 20)) { this->require_GET(req); ret = make_shared(this->generate_rare_table_json(uri.substr(20))); + } else if (uri == "/y/data/quests") { + this->require_GET(req); + ret = make_shared(this->generate_quest_list_json(this->state->quest_index(Version::GC_V3))); } else if (uri == "/y/data/config") { this->require_GET(req); ret = call_on_event_thread>(this->state->base, [this]() { return this->state->config_json; }); + } else if (uri == "/y/accounts") { + this->require_GET(req); + ret = make_shared(this->generate_accounts_json()); } else if (uri == "/y/clients") { this->require_GET(req); ret = make_shared(this->generate_game_server_clients_json()); @@ -1271,9 +1268,6 @@ void HTTPServer::handle_request(struct evhttp_request* req) { } else if (uri == "/y/summary") { this->require_GET(req); ret = make_shared(this->generate_summary_json()); - } else if (uri == "/y/all") { - this->require_GET(req); - ret = make_shared(this->generate_all_json()); } else { throw http_error(404, "unknown action"); diff --git a/src/HTTPServer.hh b/src/HTTPServer.hh index 1cfe8cae..9bff1c86 100644 --- a/src/HTTPServer.hh +++ b/src/HTTPServer.hh @@ -100,12 +100,13 @@ protected: const std::string& key, const std::string* _default = nullptr); - static phosg::JSON generate_quest_json_st(std::shared_ptr q); + static phosg::JSON generate_server_version_st(); static phosg::JSON generate_client_config_json_st(const Client::Config& config); static phosg::JSON generate_account_json_st(std::shared_ptr a); static phosg::JSON generate_game_client_json_st(std::shared_ptr c, std::shared_ptr item_name_index); static phosg::JSON generate_proxy_client_json_st(std::shared_ptr ses); static phosg::JSON generate_lobby_json_st(std::shared_ptr l, std::shared_ptr item_name_index); + phosg::JSON generate_accounts_json() const; phosg::JSON generate_game_server_clients_json() const; phosg::JSON generate_proxy_server_clients_json() const; phosg::JSON generate_server_info_json() const; @@ -117,4 +118,5 @@ protected: phosg::JSON generate_common_tables_json() const; phosg::JSON generate_rare_tables_json() const; phosg::JSON generate_rare_table_json(const std::string& table_name) const; + phosg::JSON generate_quest_list_json(std::shared_ptr q); }; diff --git a/src/Quest.cc b/src/Quest.cc index 7322bc64..82f9df7e 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -397,6 +397,41 @@ Quest::Quest(shared_ptr initial_version) this->add_version(initial_version); } +phosg::JSON Quest::json() const { + auto versions_json = phosg::JSON::list(); + for (const auto& [_, vq] : this->versions) { + versions_json.emplace_back(phosg::JSON::dict({ + {"Version", phosg::name_for_enum(vq->version)}, + {"Language", name_for_language_code(vq->language)}, + {"ShortDescription", vq->short_description}, + {"LongDescription", vq->long_description}, + {"BINFileSize", vq->bin_contents ? vq->bin_contents->size() : phosg::JSON(nullptr)}, + {"DATFileSize", vq->dat_contents ? vq->dat_contents->size() : phosg::JSON(nullptr)}, + {"PVRFileSize", vq->pvr_contents ? vq->pvr_contents->size() : phosg::JSON(nullptr)}, + })); + } + + auto battle_rules_json = this->battle_rules ? this->battle_rules->json() : nullptr; + auto challenge_template_index_json = (this->challenge_template_index >= 0) + ? this->challenge_template_index + : phosg::JSON(nullptr); + return phosg::JSON::dict({ + {"Number", this->quest_number}, + {"CategoryID", this->category_id}, + {"Episode", name_for_episode(this->episode)}, + {"AllowStartFromChatCommand", this->allow_start_from_chat_command}, + {"Joinable", this->joinable}, + {"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)}, + {"Name", this->name}, + {"BattleRules", std::move(battle_rules_json)}, + {"ChallengeTemplateIndex", std::move(challenge_template_index_json)}, + {"DescriptionFlag", this->description_flag}, + {"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)}, + {"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)}, + {"Versions", std::move(versions_json)}, + }); +} + uint32_t Quest::versions_key(Version v, uint8_t language) { return (static_cast(v) << 8) | language; } @@ -863,6 +898,34 @@ QuestIndex::QuestIndex( } } +phosg::JSON QuestIndex::json() const { + auto categories_json = phosg::JSON::dict(); + for (const auto& cat : this->category_index->categories) { + auto dict = phosg::JSON::dict({ + {"CategoryID", cat->category_id}, + {"Flags", cat->enabled_flags}, + {"DirectoryName", cat->directory_name}, + {"Name", cat->name}, + {"Description", cat->description}, + }); + categories_json.emplace(cat->name, std::move(dict)); + } + + auto quests_json = phosg::JSON::list(); + for (const auto& [_, q] : this->quests_by_number) { + quests_json.emplace_back(q->json()); + } + + return phosg::JSON::dict({ + {"Directory", this->directory}, + {"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 { try { return this->quests_by_number.at(quest_number); diff --git a/src/Quest.hh b/src/Quest.hh index 3b9cdead..bc8fcdfb 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -113,24 +113,7 @@ struct VersionedQuest { std::string encode_qst() const; }; -class Quest { -public: - Quest() = delete; - explicit Quest(std::shared_ptr initial_version); - Quest(const Quest&) = default; - Quest(Quest&&) = default; - Quest& operator=(const Quest&) = default; - Quest& operator=(Quest&&) = default; - - std::shared_ptr get_supermap(int64_t random_seed) const; - - void add_version(std::shared_ptr vq); - bool has_version(Version v, uint8_t language) const; - bool has_version_any_language(Version v) const; - std::shared_ptr version(Version v, uint8_t language) const; - - static uint32_t versions_key(Version v, uint8_t language); - +struct Quest { uint32_t quest_number; uint32_t category_id; Episode episode; @@ -145,6 +128,24 @@ public: std::shared_ptr available_expression; std::shared_ptr enabled_expression; std::map> versions; + + Quest() = delete; + explicit Quest(std::shared_ptr initial_version); + Quest(const Quest&) = default; + Quest(Quest&&) = default; + Quest& operator=(const Quest&) = default; + Quest& operator=(Quest&&) = default; + + phosg::JSON json() const; + + std::shared_ptr get_supermap(int64_t random_seed) const; + + void add_version(std::shared_ptr vq); + bool has_version(Version v, uint8_t language) const; + bool has_version_any_language(Version v) const; + std::shared_ptr version(Version v, uint8_t language) const; + + static uint32_t versions_key(Version v, uint8_t language); }; struct QuestIndex { @@ -163,6 +164,7 @@ struct QuestIndex { std::map>> quests_by_category_id_and_number; QuestIndex(const std::string& directory, std::shared_ptr category_index, bool is_ep3); + phosg::JSON json() const; std::shared_ptr get(uint32_t quest_number) const; std::shared_ptr get(const std::string& name) const;