add /y/accounts and /y/data/quests in API

This commit is contained in:
Martin Michelsen
2025-01-15 20:34:56 -08:00
parent 6564db437a
commit 269d2178fb
5 changed files with 123 additions and 60 deletions
+4 -2
View File
@@ -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. 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: 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`: Returns the Episode 3 card definitions.
* `GET /y/data/ep3-cards-trial`: Returns the Episode 3 Trial Edition 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/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`: Returns a list of rare table names.
* `GET /y/data/rare-tables/<TABLE-NAME>` (for example, `/y/data/rare-tables/rare-table-v4`): Returns the contents of a rare item table. * `GET /y/data/rare-tables/<TABLE-NAME>` (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/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/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/proxy-clients`: Returns information about all connected clients on the proxy server.
* `GET /y/lobbies`: Returns information about all lobbies and games. * `GET /y/lobbies`: Returns information about all lobbies and games.
* `GET /y/server`: Returns information about the server. * `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/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": "<result text>"}` or an HTTP error. * `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": "<result text>"}` or an HTTP error.
### Rare drop stream endpoint ### Rare drop stream endpoint
+33 -39
View File
@@ -13,6 +13,7 @@
#include "EventUtils.hh" #include "EventUtils.hh"
#include "Loggers.hh" #include "Loggers.hh"
#include "ProxyServer.hh" #include "ProxyServer.hh"
#include "Revision.hh"
#include "Server.hh" #include "Server.hh"
#include "ShellCommands.hh" #include "ShellCommands.hh"
@@ -441,22 +442,12 @@ void HTTPServer::dispatch_handle_request(struct evhttp_request* req, void* ctx)
reinterpret_cast<HTTPServer*>(ctx)->handle_request(req); reinterpret_cast<HTTPServer*>(ctx)->handle_request(req);
} }
phosg::JSON HTTPServer::generate_quest_json_st(shared_ptr<const Quest> q) { phosg::JSON HTTPServer::generate_server_version_st() {
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);
return phosg::JSON::dict({ return phosg::JSON::dict({
{"Number", q->quest_number}, {"ServerType", "newserv"},
{"Episode", name_for_episode(q->episode)}, {"BuildTime", BUILD_TIMESTAMP},
{"Joinable", q->joinable}, {"BuildTimeStr", phosg::format_time(BUILD_TIMESTAMP)},
{"LockStatusRegister", (q->lock_status_register >= 0) ? q->lock_status_register : phosg::JSON(nullptr)}, {"Revision", GIT_REVISION_HASH},
{"Name", q->name},
{"BattleRules", std::move(battle_rules_json)},
{"ChallengeTemplateIndex", std::move(challenge_template_index_json)},
}); });
} }
@@ -575,7 +566,7 @@ phosg::JSON HTTPServer::generate_game_client_json_st(shared_ptr<const Client> c,
if (p) { if (p) {
if (!is_ep3(c->version())) { if (!is_ep3(c->version())) {
if (c->version() != Version::DC_NTE) { 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("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP));
ret.emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); ret.emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP));
if (!is_v1_or_v2(c->version())) { if (!is_v1_or_v2(c->version())) {
@@ -861,7 +852,7 @@ phosg::JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared
} }
} }
ret.emplace("FloorItems", std::move(floor_items_json)); 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 { } else {
ret.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); ret.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS));
@@ -966,6 +957,16 @@ phosg::JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared
return ret; return ret;
} }
phosg::JSON HTTPServer::generate_accounts_json() const {
return call_on_event_thread<phosg::JSON>(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 { phosg::JSON HTTPServer::generate_game_server_clients_json() const {
return call_on_event_thread<phosg::JSON>(this->state->base, [&]() { return call_on_event_thread<phosg::JSON>(this->state->base, [&]() {
auto res = phosg::JSON::list(); 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("SectionID", name_for_section_id(l->effective_section_id()));
game_json.emplace("Mode", name_for_mode(l->mode)); game_json.emplace("Mode", name_for_mode(l->mode));
game_json.emplace("Difficulty", name_for_difficulty(l->difficulty)); 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)); 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<const QuestIndex> quest_index) {
return call_on_event_thread<phosg::JSON>(this->state->base, [&]() {
return quest_index->json();
});
}
void HTTPServer::require_GET(struct evhttp_request* req) { void HTTPServer::require_GET(struct evhttp_request* req) {
if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) { if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) {
throw HTTPServer::http_error(405, "GET method required for this endpoint"); 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 == "/") { if (uri == "/") {
this->require_GET(req); this->require_GET(req);
auto endpoints_json = phosg::JSON::list({ ret = make_shared<phosg::JSON>(this->generate_server_version_st());
"/y/data/ep3-cards",
"/y/data/ep3-cards-trial",
"/y/data/common-tables",
"/y/data/rare-tables",
"/y/data/rare-tables/<TABLE-NAME>",
"/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>(phosg::JSON::dict({{"endpoints", std::move(endpoints_json)}}));
} else if (uri == "/y/shell-exec") { } else if (uri == "/y/shell-exec") {
auto json = this->require_POST(req); 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"); throw http_error(400, "this path requires a websocket connection");
} else { } else {
this->rare_drop_subscribers.emplace(c); 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()); this->send_websocket_message(c, version_message.serialize());
return; return;
} }
@@ -1253,9 +1244,15 @@ void HTTPServer::handle_request(struct evhttp_request* req) {
} else if (!strncmp(uri.c_str(), "/y/data/rare-tables/", 20)) { } else if (!strncmp(uri.c_str(), "/y/data/rare-tables/", 20)) {
this->require_GET(req); this->require_GET(req);
ret = make_shared<phosg::JSON>(this->generate_rare_table_json(uri.substr(20))); ret = make_shared<phosg::JSON>(this->generate_rare_table_json(uri.substr(20)));
} else if (uri == "/y/data/quests") {
this->require_GET(req);
ret = make_shared<phosg::JSON>(this->generate_quest_list_json(this->state->quest_index(Version::GC_V3)));
} else if (uri == "/y/data/config") { } else if (uri == "/y/data/config") {
this->require_GET(req); this->require_GET(req);
ret = call_on_event_thread<shared_ptr<const phosg::JSON>>(this->state->base, [this]() { return this->state->config_json; }); ret = call_on_event_thread<shared_ptr<const phosg::JSON>>(this->state->base, [this]() { return this->state->config_json; });
} else if (uri == "/y/accounts") {
this->require_GET(req);
ret = make_shared<phosg::JSON>(this->generate_accounts_json());
} else if (uri == "/y/clients") { } else if (uri == "/y/clients") {
this->require_GET(req); this->require_GET(req);
ret = make_shared<phosg::JSON>(this->generate_game_server_clients_json()); ret = make_shared<phosg::JSON>(this->generate_game_server_clients_json());
@@ -1271,9 +1268,6 @@ void HTTPServer::handle_request(struct evhttp_request* req) {
} else if (uri == "/y/summary") { } else if (uri == "/y/summary") {
this->require_GET(req); this->require_GET(req);
ret = make_shared<phosg::JSON>(this->generate_summary_json()); ret = make_shared<phosg::JSON>(this->generate_summary_json());
} else if (uri == "/y/all") {
this->require_GET(req);
ret = make_shared<phosg::JSON>(this->generate_all_json());
} else { } else {
throw http_error(404, "unknown action"); throw http_error(404, "unknown action");
+3 -1
View File
@@ -100,12 +100,13 @@ protected:
const std::string& key, const std::string& key,
const std::string* _default = nullptr); const std::string* _default = nullptr);
static phosg::JSON generate_quest_json_st(std::shared_ptr<const Quest> q); static phosg::JSON generate_server_version_st();
static phosg::JSON generate_client_config_json_st(const Client::Config& config); static phosg::JSON generate_client_config_json_st(const Client::Config& config);
static phosg::JSON generate_account_json_st(std::shared_ptr<const Account> a); static phosg::JSON generate_account_json_st(std::shared_ptr<const Account> a);
static phosg::JSON generate_game_client_json_st(std::shared_ptr<const Client> c, std::shared_ptr<const ItemNameIndex> item_name_index); static phosg::JSON generate_game_client_json_st(std::shared_ptr<const Client> c, std::shared_ptr<const ItemNameIndex> item_name_index);
static phosg::JSON generate_proxy_client_json_st(std::shared_ptr<const ProxyServer::LinkedSession> ses); static phosg::JSON generate_proxy_client_json_st(std::shared_ptr<const ProxyServer::LinkedSession> ses);
static phosg::JSON generate_lobby_json_st(std::shared_ptr<const Lobby> l, std::shared_ptr<const ItemNameIndex> item_name_index); static phosg::JSON generate_lobby_json_st(std::shared_ptr<const Lobby> l, std::shared_ptr<const ItemNameIndex> item_name_index);
phosg::JSON generate_accounts_json() const;
phosg::JSON generate_game_server_clients_json() const; phosg::JSON generate_game_server_clients_json() const;
phosg::JSON generate_proxy_server_clients_json() const; phosg::JSON generate_proxy_server_clients_json() const;
phosg::JSON generate_server_info_json() const; phosg::JSON generate_server_info_json() const;
@@ -117,4 +118,5 @@ protected:
phosg::JSON generate_common_tables_json() const; phosg::JSON generate_common_tables_json() const;
phosg::JSON generate_rare_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_rare_table_json(const std::string& table_name) const;
phosg::JSON generate_quest_list_json(std::shared_ptr<const QuestIndex> q);
}; };
+63
View File
@@ -397,6 +397,41 @@ Quest::Quest(shared_ptr<const VersionedQuest> initial_version)
this->add_version(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) { uint32_t Quest::versions_key(Version v, uint8_t language) {
return (static_cast<uint32_t>(v) << 8) | language; return (static_cast<uint32_t>(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<uint32_t, std::shared_ptr<Quest>> quests_by_number;
// std::map<std::string, std::shared_ptr<Quest>> quests_by_name;
// std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
}
shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const { shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
try { try {
return this->quests_by_number.at(quest_number); return this->quests_by_number.at(quest_number);
+20 -18
View File
@@ -113,24 +113,7 @@ struct VersionedQuest {
std::string encode_qst() const; std::string encode_qst() const;
}; };
class Quest { struct Quest {
public:
Quest() = delete;
explicit Quest(std::shared_ptr<const VersionedQuest> initial_version);
Quest(const Quest&) = default;
Quest(Quest&&) = default;
Quest& operator=(const Quest&) = default;
Quest& operator=(Quest&&) = default;
std::shared_ptr<const SuperMap> get_supermap(int64_t random_seed) const;
void add_version(std::shared_ptr<const VersionedQuest> vq);
bool has_version(Version v, uint8_t language) const;
bool has_version_any_language(Version v) const;
std::shared_ptr<const VersionedQuest> version(Version v, uint8_t language) const;
static uint32_t versions_key(Version v, uint8_t language);
uint32_t quest_number; uint32_t quest_number;
uint32_t category_id; uint32_t category_id;
Episode episode; Episode episode;
@@ -145,6 +128,24 @@ public:
std::shared_ptr<const IntegralExpression> available_expression; std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression; std::shared_ptr<const IntegralExpression> enabled_expression;
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions; std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
Quest() = delete;
explicit Quest(std::shared_ptr<const VersionedQuest> 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<const SuperMap> get_supermap(int64_t random_seed) const;
void add_version(std::shared_ptr<const VersionedQuest> vq);
bool has_version(Version v, uint8_t language) const;
bool has_version_any_language(Version v) const;
std::shared_ptr<const VersionedQuest> version(Version v, uint8_t language) const;
static uint32_t versions_key(Version v, uint8_t language);
}; };
struct QuestIndex { struct QuestIndex {
@@ -163,6 +164,7 @@ struct QuestIndex {
std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number; std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index, bool is_ep3); QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index, bool is_ep3);
phosg::JSON json() const;
std::shared_ptr<const Quest> get(uint32_t quest_number) const; std::shared_ptr<const Quest> get(uint32_t quest_number) const;
std::shared_ptr<const Quest> get(const std::string& name) const; std::shared_ptr<const Quest> get(const std::string& name) const;