[API] Implement Zone Sidecar (#3635)

* Zone sidecar work

* Process management work

* Merge

* Sidecar work

* API config option

* Request proxy work

* Proxy headers and params

* Change port

* Remove code

* Sim work

* Sidecar work

* Update loot_simulator_controller.cpp

* Update loot_simulator_controller.cpp

* Formatting

* Post merge change

* Windows compile fix

* Update sidecar_api.cpp

* Update strings.cpp
This commit is contained in:
Chris Miles
2023-10-23 22:39:37 -05:00
committed by GitHub
parent 0bbfcf7adc
commit b027edd21e
22 changed files with 25282 additions and 127 deletions
+18
View File
@@ -0,0 +1,18 @@
void SidecarApi::RequestLogHandler(const httplib::Request &req, const httplib::Response &res)
{
if (!req.path.empty()) {
std::vector<std::string> params;
for (auto &p: req.params) {
params.emplace_back(fmt::format("{}={}", p.first, p.second));
}
LogInfo(
"[API] Request [{}] [{}{}] via [{}:{}]",
res.status,
req.path,
(!params.empty() ? "?" + Strings::Join(params, "&") : ""),
req.remote_addr,
req.remote_port
);
}
}
@@ -0,0 +1,173 @@
#include "sidecar_api.h"
#include "../../common/json/json.hpp"
#include "../zone.h"
extern Zone* zone;
void SidecarApi::LootSimulatorController(const httplib::Request &req, httplib::Response &res)
{
int loottable_id = req.has_param("loottable_id") ? std::stoi(req.get_param_value("loottable_id")) : 4027;
int npc_id = req.has_param("npc_id") ? std::stoi(req.get_param_value("npc_id")) : 32040; // lord nagafen
auto iterations = 100;
auto log_enabled = false;
LogSys.log_settings[Logs::Loot].log_to_console = 0;
nlohmann::json j;
auto npc_type = content_db.LoadNPCTypesData(npc_id);
if (npc_type) {
auto npc = new NPC(
npc_type,
nullptr,
glm::vec4(0, 0, 0, 0),
GravityBehavior::Water
);
BenchTimer benchmark;
// depop the previous one
for (auto &n: entity_list.GetNPCList()) {
if (n.second->GetNPCTypeID() == npc_id) {
LogInfo("found npc id [{}]", npc_id);
n.second->Depop(false);
}
}
entity_list.Process();
entity_list.MobProcess();
npc->SetRecordLootStats(true);
for (int i = 0; i < iterations; i++) {
npc->AddLootTable(loottable_id);
for (auto &id: zone->GetGlobalLootTables(npc)) {
npc->AddLootTable(id);
}
}
entity_list.AddNPC(npc);
j["data"]["loottable_id"] = loottable_id;
j["data"]["npc_id"] = npc_id;
j["data"]["npc_name"] = npc->GetCleanName();
j["data"]["rolled_items_count"] = npc->GetRolledItems().size();
j["data"]["iterations"] = iterations;
// npc level loot table
auto loot_table = database.GetLootTable(loottable_id);
if (!loot_table) {
res.status = 400;
j["error"] = fmt::format("Loot table not found [{}]", loottable_id);
res.set_content(j.dump(), "application/json");
return;
}
for (uint32 i = 0; i < loot_table->NumEntries; i++) {
auto le = loot_table->Entries[i];
nlohmann::json jle;
jle["lootdrop_id"] = le.lootdrop_id;
jle["droplimit"] = le.droplimit;
jle["mindrop"] = le.mindrop;
jle["multiplier"] = le.multiplier;
jle["probability"] = le.probability;
auto loot_drop = database.GetLootDrop(le.lootdrop_id);
if (!loot_drop) {
continue;
}
for (uint32 ei = 0; ei < loot_drop->NumEntries; ei++) {
auto e = loot_drop->Entries[ei];
int rolled_count = npc->GetRolledItemCount(e.item_id);
const EQ::ItemData *item = database.GetItem(e.item_id);
auto rolled_percentage = (float) ((float) ((float) rolled_count / (float) iterations) * 100);
nlohmann::json drop;
drop["slot"] = ei;
drop["item_id"] = e.item_id;
drop["item_name"] = item->Name;
drop["chance"] = fmt::format("{:.2f}", e.chance);
drop["simulate_rolled_count"] = rolled_count;
drop["simulate_rolled_percentage"] = fmt::format("{:.2f}", rolled_percentage);
jle["drops"].push_back(drop);
}
j["lootdrops"].push_back(jle);
}
// global loot
for (auto &id: zone->GetGlobalLootTables(npc)) {
loot_table = database.GetLootTable(id);
if (!loot_table) {
LogInfo("Global Loot table not found [{}]", id);
continue;
}
for (uint32 i = 0; i < loot_table->NumEntries; i++) {
auto le = loot_table->Entries[i];
LogInfo(
"# Lootdrop ID [{}] drop_limit [{}] min_drop [{}] mult [{}] probability [{}]",
le.lootdrop_id,
le.droplimit,
le.mindrop,
le.multiplier,
le.probability
);
nlohmann::json jle;
jle["lootdrop_id"] = le.lootdrop_id;
jle["droplimit"] = le.droplimit;
jle["mindrop"] = le.mindrop;
jle["multiplier"] = le.multiplier;
jle["probability"] = le.probability;
auto loot_drop = database.GetLootDrop(le.lootdrop_id);
if (!loot_drop) {
continue;
}
for (uint32 ei = 0; ei < loot_drop->NumEntries; ei++) {
auto e = loot_drop->Entries[ei];
int rolled_count = npc->GetRolledItemCount(e.item_id);
const EQ::ItemData *item = database.GetItem(e.item_id);
auto rolled_percentage = (float) ((float) ((float) rolled_count / (float) iterations) *
100);
LogInfo(
"-- [{}] item_id [{}] chance [{}] rolled_count [{}] ({:.2f}%) name [{}]",
ei,
e.item_id,
e.chance,
rolled_count,
rolled_percentage,
item->Name
);
nlohmann::json drop;
drop["slot"] = ei;
drop["item_id"] = e.item_id;
drop["item_name"] = item->Name;
drop["chance"] = fmt::format("{:.2f}", e.chance);
drop["simulate_rolled_count"] = rolled_count;
drop["simulate_rolled_percentage"] = fmt::format("{:.2f}", rolled_percentage);
jle["drops"].push_back(drop);
j["global"]["lootdrops"].push_back(jle);
}
}
}
j["data"]["time"] = benchmark.elapsed();
res.status = 200;
res.set_content(j.dump(), "application/json");
}
else {
res.status = 400;
j["error"] = fmt::format("Failed to spawn NPC ID [{}]", npc_id);
res.set_content(j.dump(), "application/json");
}
}
@@ -0,0 +1,4 @@
void SidecarApi::MapBestZController(const httplib::Request &req, httplib::Response &res)
{
}
+119
View File
@@ -0,0 +1,119 @@
#include "sidecar_api.h"
#include "../../common/http/httplib.h"
#include "../../common/eqemu_logsys.h"
#include "../zonedb.h"
#include "../../shared_memory/loot.h"
#include "../../common/process.h"
#include "../common.h"
#include "../zone.h"
#include "../client.h"
#include "../../common/json/json.hpp"
#include <csignal>
void CatchSidecarSignal(int sig_num)
{
LogInfo("[SidecarAPI] Caught signal [{}]", sig_num);
LogInfo("Forcefully exiting for now");
std::exit(0);
}
#include "log_handler.cpp"
#include "test_controller.cpp"
#include "map_best_z_controller.cpp"
#include "../../common/file.h"
constexpr static int HTTP_RESPONSE_OK = 200;
constexpr static int HTTP_RESPONSE_BAD_REQUEST = 400;
constexpr static int HTTP_RESPONSE_UNAUTHORIZED = 401;
std::string authorization_key;
void SidecarApi::BootWebserver(int port, const std::string &key)
{
LogInfo("Booting zone sidecar API");
std::signal(SIGINT, CatchSidecarSignal);
std::signal(SIGTERM, CatchSidecarSignal);
std::signal(SIGKILL, CatchSidecarSignal);
if (!key.empty()) {
authorization_key = key;
LogInfo("Booting with authorization key [{}]", authorization_key);
}
int web_api_port = port > 0 ? port : 9099;
std::string hotfix_name = "zonesidecar_api_";
// bake shared memory if it doesn't exist
// TODO: Windows
if (!File::Exists("shared/zonesidecar_api_loot_drop")) {
LogInfo("Creating shared memory for prefix [{}]", hotfix_name);
std::string output = Process::execute(
fmt::format(
"./bin/shared_memory -hotfix={} loot items",
hotfix_name
)
);
std::cout << output << "\n";
}
LogInfo("Loading loot tables");
if (!database.LoadLoot(hotfix_name)) {
LogError("Loading loot failed!");
}
// bootup a fake zone
Zone::Bootup(ZoneID("qrg"), 0, false);
zone->StopShutdownTimer();
httplib::Server api;
api.set_logger(SidecarApi::RequestLogHandler);
api.set_pre_routing_handler(
[](const auto &req, auto &res) {
for (const auto &header: req.headers) {
auto header_key = header.first;
auto header_value = header.second;
LogHTTPDetail("[API] header_key [{}] header_value [{}]", header_key, header_value);
if (header_key == "Authorization") {
std::string auth_key = header_value;
Strings::FindReplace(auth_key, "Bearer", "");
Strings::Trim(auth_key);
LogHTTPDetail(
"Request Authorization key is [{}] set key is [{}] match [{}]",
auth_key,
authorization_key,
auth_key == authorization_key ? "true" : "false"
);
// authorization key matches, pass the request on to the route handler
if (!authorization_key.empty() && auth_key != authorization_key) {
LogHTTPDetail("[Sidecar] Returning as unhandled, authorization passed");
return httplib::Server::HandlerResponse::Unhandled;
}
}
}
if (!authorization_key.empty()) {
nlohmann::json j;
j["error"] = "Authorization key not valid!";
res.set_content(j.dump(), "application/json");
res.status = HTTP_RESPONSE_UNAUTHORIZED;
return httplib::Server::HandlerResponse::Handled;
}
return httplib::Server::HandlerResponse::Unhandled;
}
);
api.Get("/api/v1/test-controller", SidecarApi::TestController);
api.Get("/api/v1/loot-simulate", SidecarApi::LootSimulatorController);
LogInfo("Webserver API now listening on port [{0}]", web_api_port);
// this is not supposed to bind to the outside world
api.listen("localhost", web_api_port);
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef EQEMU_SIDECAR_API_H
#define EQEMU_SIDECAR_API_H
#include "../../common/http/httplib.h"
class SidecarApi {
public:
static void BootWebserver(int req = 0, const std::string& key = "");
static void AuthMiddleware(const httplib::Request &req, const httplib::Response &res);
static void RequestLogHandler(const httplib::Request &req, const httplib::Response &res);
static void TestController(const httplib::Request &req, httplib::Response &res);
static void LootSimulatorController(const httplib::Request &req, httplib::Response &res);
static void MapBestZController(const httplib::Request &req, httplib::Response &res);
};
#endif //EQEMU_SIDECAR_API_H
+10
View File
@@ -0,0 +1,10 @@
#include "sidecar_api.h"
void SidecarApi::TestController(const httplib::Request &req, httplib::Response &res)
{
nlohmann::json j;
j["data"]["test"] = "test";
res.set_content(j.dump(), "application/json");
}