Skip to content

Commit

Permalink
add /y/accounts and /y/data/quests in API
Browse files Browse the repository at this point in the history
  • Loading branch information
fuzziqersoftware committed Jan 16, 2025
1 parent 6564db4 commit 269d217
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 59 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<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/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": "<result text>"}` or an HTTP error.

### Rare drop stream endpoint
Expand Down
72 changes: 33 additions & 39 deletions src/HTTPServer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "EventUtils.hh"
#include "Loggers.hh"
#include "ProxyServer.hh"
#include "Revision.hh"
#include "Server.hh"
#include "ShellCommands.hh"

Expand Down Expand Up @@ -441,22 +442,12 @@ void HTTPServer::dispatch_handle_request(struct evhttp_request* req, void* ctx)
reinterpret_cast<HTTPServer*>(ctx)->handle_request(req);
}

phosg::JSON HTTPServer::generate_quest_json_st(shared_ptr<const Quest> 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},
});
}

Expand Down Expand Up @@ -575,7 +566,7 @@ phosg::JSON HTTPServer::generate_game_client_json_st(shared_ptr<const Client> 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())) {
Expand Down Expand Up @@ -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("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));
Expand Down Expand Up @@ -966,6 +957,16 @@ phosg::JSON HTTPServer::generate_lobby_json_st(shared_ptr<const Lobby> l, shared
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 {
return call_on_event_thread<phosg::JSON>(this->state->base, [&]() {
auto res = phosg::JSON::list();
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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) {
if (evhttp_request_get_command(req) != EVHTTP_REQ_GET) {
throw HTTPServer::http_error(405, "GET method required for this endpoint");
Expand Down Expand Up @@ -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/<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)}}));
ret = make_shared<phosg::JSON>(this->generate_server_version_st());

} else if (uri == "/y/shell-exec") {
auto json = this->require_POST(req);
Expand All @@ -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;
}
Expand All @@ -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<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") {
this->require_GET(req);
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") {
this->require_GET(req);
ret = make_shared<phosg::JSON>(this->generate_game_server_clients_json());
Expand All @@ -1271,9 +1268,6 @@ void HTTPServer::handle_request(struct evhttp_request* req) {
} else if (uri == "/y/summary") {
this->require_GET(req);
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 {
throw http_error(404, "unknown action");
Expand Down
4 changes: 3 additions & 1 deletion src/HTTPServer.hh
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,13 @@ protected:
const std::string& key,
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_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_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);
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;
Expand All @@ -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<const QuestIndex> q);
};
63 changes: 63 additions & 0 deletions src/Quest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,41 @@ Quest::Quest(shared_ptr<const VersionedQuest> 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<uint32_t>(v) << 8) | language;
}
Expand Down Expand Up @@ -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 {
try {
return this->quests_by_number.at(quest_number);
Expand Down
36 changes: 19 additions & 17 deletions src/Quest.hh
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,31 @@ struct VersionedQuest {
std::string encode_qst() const;
};

class Quest {
public:
struct Quest {
uint32_t quest_number;
uint32_t category_id;
Episode episode;
bool allow_start_from_chat_command;
bool joinable;
int16_t lock_status_register;
std::string name;
mutable std::shared_ptr<const SuperMap> supermap;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
uint8_t description_flag;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
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);
Expand All @@ -130,21 +146,6 @@ public:
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 category_id;
Episode episode;
bool allow_start_from_chat_command;
bool joinable;
int16_t lock_status_register;
std::string name;
mutable std::shared_ptr<const SuperMap> supermap;
std::shared_ptr<const BattleRules> battle_rules;
ssize_t challenge_template_index;
uint8_t description_flag;
std::shared_ptr<const IntegralExpression> available_expression;
std::shared_ptr<const IntegralExpression> enabled_expression;
std::map<uint32_t, std::shared_ptr<const VersionedQuest>> versions;
};

struct QuestIndex {
Expand All @@ -163,6 +164,7 @@ struct QuestIndex {
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);
phosg::JSON json() const;

std::shared_ptr<const Quest> get(uint32_t quest_number) const;
std::shared_ptr<const Quest> get(const std::string& name) const;
Expand Down

0 comments on commit 269d217

Please sign in to comment.