[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 25282 additions and 127 deletions

View File

@ -70,6 +70,7 @@ SET(common_sources
perl_eqdb.cpp
perl_eqdb_res.cpp
process/process.cpp
process.cpp
proc_launcher.cpp
profanity_manager.cpp
ptimer.cpp
@ -90,6 +91,7 @@ SET(common_sources
timer.cpp
unix.cpp
platform.cpp
json/json.hpp
json/jsoncpp.cpp
zone_store.cpp
net/console_server.cpp
@ -583,6 +585,7 @@ SET(common_headers
path_manager.cpp
platform.h
process/process.h
process.h
proc_launcher.h
profanity_manager.h
profiler.h

View File

@ -39,15 +39,15 @@ namespace EQEmuCommand {
{
if (cmd[{"-d", "--debug"}]) {
std::cout << "Positional args:\n";
for (auto &pos_arg : cmd.pos_args())
for (auto &pos_arg: cmd.pos_args())
std::cout << '\t' << pos_arg << std::endl;
std::cout << "\nFlags:\n";
for (auto &flag : cmd.flags())
for (auto &flag: cmd.flags())
std::cout << '\t' << flag << std::endl;
std::cout << "\nParameters:\n";
for (auto &param : cmd.params())
for (auto &param: cmd.params())
std::cout << '\t' << param.first << " : " << param.second << std::endl;
}
}
@ -69,8 +69,8 @@ namespace EQEmuCommand {
{
bool arguments_filled = true;
int index = 2;
for (auto &arg : arguments) {
int index = 2;
for (auto &arg: arguments) {
if (cmd(arg).str().empty() && cmd(index).str().empty()) {
arguments_filled = false;
}
@ -79,12 +79,12 @@ namespace EQEmuCommand {
if (!arguments_filled || (argc == 2 && !cmd[{"-h", "--help"}]) || (argc == 3 && cmd[{"-h", "--help"}])) {
std::string arguments_string;
for (auto &arg : arguments) {
for (auto &arg: arguments) {
arguments_string += " " + arg;
}
std::string options_string;
for (auto &opt : options) {
for (auto &opt: options) {
options_string += " " + opt + "\n";
}
@ -124,14 +124,6 @@ namespace EQEmuCommand {
)
{
std::string description;
bool ran_command = false;
for (auto &it: in_function_map) {
if (it.first == argv[1]) {
(it.second)(argc, argv, cmd, description);
ran_command = true;
}
}
if (cmd[{"-h", "--help"}]) {
std::cout << std::endl;
std::cout <<
@ -142,9 +134,7 @@ namespace EQEmuCommand {
<< std::endl
<< std::endl;
/**
* Get max command length for padding length
*/
// Get max command length for padding length
int max_command_length = 0;
for (auto &it: in_function_map) {
@ -155,18 +145,14 @@ namespace EQEmuCommand {
}
}
/**
* Display command menu
*/
// Display command menu
std::string command_section;
for (auto &it: in_function_map) {
description.clear();
(it.second)(argc, argv, cmd, description);
/**
* Print section header
*/
// Print section header
std::string command_prefix = it.first.substr(0, it.first.find(":"));
if (command_prefix.find("test") != std::string::npos) {
@ -178,9 +164,7 @@ namespace EQEmuCommand {
std::cout << termcolor::reset << command_prefix << std::endl;
}
/**
* Print commands
*/
// Print commands
std::stringstream command;
command << termcolor::colorize << termcolor::yellow << it.first << termcolor::reset;
printf(" %-*s %s\n", max_command_length, command.str().c_str(), description.c_str());
@ -191,6 +175,15 @@ namespace EQEmuCommand {
std::exit(0);
}
bool ran_command = false;
for (auto &it: in_function_map) {
if (it.first == argv[1]) {
(it.second)(argc, argv, cmd, description);
ran_command = true;
}
}
if (ran_command) {
std::exit(0);
}

24640
common/json/json.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -72,6 +72,8 @@ std::string GetPlatformName()
return "HC";
case EQEmuExePlatform::ExePlatformTests:
return "Tests";
case EQEmuExePlatform::ExePlatformZoneSidecar:
return "ZoneSidecar";
default:
return "";
}

View File

@ -37,7 +37,8 @@ enum EQEmuExePlatform
ExePlatformClientImport,
ExePlatformClientExport,
ExePlatformHC,
ExePlatformTests
ExePlatformTests,
ExePlatformZoneSidecar
};
void RegisterExecutablePlatform(EQEmuExePlatform p);

44
common/process.cpp Normal file
View File

@ -0,0 +1,44 @@
#include <string>
#include <fstream>
#include <algorithm>
#include "process.h"
inline std::string random_string(size_t length)
{
auto randchar = []() -> char {
const char charset[] = "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz";
const size_t max_index = (sizeof(charset) - 1);
return charset[static_cast<size_t>(std::rand()) % max_index];
};
std::string str(length, 0);
std::generate_n(str.begin(), length, randchar);
return str;
}
std::string Process::execute(const std::string &cmd, bool return_result)
{
std::string random = "/tmp/" + random_string(25);
const char *file_name = random.c_str();
if (return_result) {
#ifdef _WINDOWS
std::system((cmd + " > " + file_name + " 2>&1").c_str());
#else
std::system((cmd + " > " + file_name + " 2>&1").c_str());
#endif
}
else {
std::system((cmd).c_str());
}
std::string result;
if (return_result) {
std::ifstream file(file_name);
result = {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
std::remove(file_name);
}
return result;
}

10
common/process.h Normal file
View File

@ -0,0 +1,10 @@
#ifndef EQEMU_PROCESS_H
#define EQEMU_PROCESS_H
class Process {
public:
static std::string execute(const std::string &cmd, bool return_result = true);
};
#endif //EQEMU_PROCESS_H

View File

@ -42,10 +42,32 @@
#include <stdio.h>
#include <iostream>
#include <random>
#include <string>
//Const char based
#include "strings_legacy.cpp" // legacy c functions
#include "strings_misc.cpp" // anything non "Strings" scoped
std::string Strings::Random(size_t length)
{
static auto &chrs = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
thread_local static std::mt19937 rg{std::random_device{}()};
thread_local static std::uniform_int_distribution<std::string::size_type> pick(0, sizeof(chrs) - 2);
std::string s;
s.reserve(length);
while (length--) {
s += chrs[pick(rg)];
}
return s;
}
std::vector<std::string> Strings::Split(const std::string &str, const char delim)
{
std::vector<std::string> ret;
@ -64,7 +86,7 @@ std::vector<std::string> Strings::Split(const std::string &str, const char delim
}
// this one takes delimiter length into consideration
std::vector<std::string> Strings::Split(const std::string& s, const std::string& delimiter)
std::vector<std::string> Strings::Split(const std::string &s, const std::string &delimiter)
{
size_t pos_start = 0, pos_end, delim_len = delimiter.length();
std::string token;
@ -783,21 +805,6 @@ bool Strings::ToBool(const std::string& bool_string)
return false;
}
// returns a random string of specified length
std::string Strings::Random(size_t length)
{
auto randchar = []() -> char {
const char charset[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
const size_t max_index = (sizeof(charset) - 1);
return charset[static_cast<size_t>(std::rand()) % max_index];
};
std::string str(length, 0);
std::generate_n(str.begin(), length, randchar);
return str;
}
// a wrapper for stoi which will return a fallback if the string
// fails to cast to a number
int Strings::ToInt(const std::string &s, int fallback)

View File

@ -185,7 +185,6 @@ public:
value = strtod(tmp_str.data(), nullptr);
return res;
}
};
const std::string StringFormat(const char *format, ...);

View File

@ -133,6 +133,8 @@ SET(zone_sources
quest_parser_collection.cpp
raids.cpp
raycast_mesh.cpp
sidecar_api/sidecar_api.cpp
sidecar_api/loot_simulator_controller.cpp
shared_task_zone_messaging.cpp
spawn2.cpp
spawn2.h
@ -253,6 +255,7 @@ SET(zone_headers
quest_parser_collection.h
raids.h
raycast_mesh.h
sidecar_api/sidecar_api.h
shared_task_zone_messaging.h
spawn2.cpp
spawn2.h
@ -273,8 +276,9 @@ SET(zone_headers
zone_config.h
zonedb.h
zonedump.h
zone_cli.h
zone_reload.h
)
zone_cli.cpp)
ADD_EXECUTABLE(zone ${zone_sources} ${zone_headers})

View File

@ -0,0 +1,24 @@
#include "../../common/http/httplib.h"
#include "../../common/eqemu_logsys.h"
#include "../sidecar_api/sidecar_api.h"
#include "../../common/platform.h"
void ZoneCLI::SidecarServeHttp(int argc, char **argv, argh::parser &cmd, std::string &description)
{
if (cmd[{"-h", "--help"}]) {
return;
}
RegisterExecutablePlatform(EQEmuExePlatform::ExePlatformZoneSidecar);
int port = 0;
std::string key;
if (!cmd("--port").str().empty()) {
port = strtoll(cmd("--port").str().c_str(), nullptr, 10);
}
if (!cmd("--key").str().empty()) {
key = cmd("--key").str();
}
SidecarApi::BootWebserver(port, key);
}

View File

@ -68,6 +68,7 @@
#ifdef _WINDOWS
#else
#include <pthread.h>
#include "../common/unix.h"
@ -85,6 +86,8 @@ extern volatile bool is_zone_loaded;
#include "../common/events/player_event_logs.h"
#include "../common/path_manager.h"
#include "../common/database/database_update.h"
#include "zone_event_scheduler.h"
#include "zone_cli.h"
EntityList entity_list;
WorldServer worldserver;
@ -112,17 +115,23 @@ const ZoneConfig *Config;
double frame_time = 0.0;
void Shutdown();
void UpdateWindowTitle(char* iNewTitle);
void UpdateWindowTitle(char *iNewTitle);
void CatchSignal(int sig_num);
extern void MapOpcodes();
int main(int argc, char** argv) {
int main(int argc, char **argv)
{
RegisterExecutablePlatform(ExePlatformZone);
LogSys.LoadLogSettingsDefaults();
set_exception_handler();
// silence logging if we ran a command
if (ZoneCLI::RanConsoleCommand(argc, argv)) {
LogSys.SilenceConsoleLogging();
}
path.LoadPaths();
#ifdef USE_MAP_MMFS
@ -154,77 +163,80 @@ int main(int argc, char** argv) {
}
Config = ZoneConfig::get();
const char *zone_name;
uint32 instance_id = 0;
// static zone booting
const char *zone_name;
uint32 instance_id = 0;
std::string z_name;
if (argc == 4) {
instance_id = Strings::ToInt(argv[3]);
worldserver.SetLauncherName(argv[2]);
auto zone_port = Strings::Split(argv[1], ':');
if (!ZoneCLI::RanSidecarCommand(argc, argv)) {
if (argc == 4) {
instance_id = Strings::ToInt(argv[3]);
worldserver.SetLauncherName(argv[2]);
auto zone_port = Strings::Split(argv[1], ':');
if (!zone_port.empty()) {
z_name = zone_port[0];
if (!zone_port.empty()) {
z_name = zone_port[0];
}
if (zone_port.size() > 1) {
std::string p_name = zone_port[1];
Config->SetZonePort(Strings::ToInt(p_name));
}
worldserver.SetLaunchedName(z_name.c_str());
if (strncmp(z_name.c_str(), "dynamic_", 8) == 0) {
zone_name = ".";
}
else {
zone_name = z_name.c_str();
}
}
else if (argc == 3) {
worldserver.SetLauncherName(argv[2]);
auto zone_port = Strings::Split(argv[1], ':');
if (zone_port.size() > 1) {
std::string p_name = zone_port[1];
Config->SetZonePort(Strings::ToInt(p_name));
if (!zone_port.empty()) {
z_name = zone_port[0];
}
if (zone_port.size() > 1) {
std::string p_name = zone_port[1];
Config->SetZonePort(Strings::ToInt(p_name));
}
worldserver.SetLaunchedName(z_name.c_str());
if (strncmp(z_name.c_str(), "dynamic_", 8) == 0) {
zone_name = ".";
}
else {
zone_name = z_name.c_str();
}
}
else if (argc == 2) {
worldserver.SetLauncherName("NONE");
auto zone_port = Strings::Split(argv[1], ':');
worldserver.SetLaunchedName(z_name.c_str());
if (strncmp(z_name.c_str(), "dynamic_", 8) == 0) {
zone_name = ".";
if (!zone_port.empty()) {
z_name = zone_port[0];
}
if (zone_port.size() > 1) {
std::string p_name = zone_port[1];
Config->SetZonePort(Strings::ToInt(p_name));
}
worldserver.SetLaunchedName(z_name.c_str());
if (strncmp(z_name.c_str(), "dynamic_", 8) == 0) {
zone_name = ".";
}
else {
zone_name = z_name.c_str();
}
}
else {
zone_name = z_name.c_str();
}
}
else if (argc == 3) {
worldserver.SetLauncherName(argv[2]);
auto zone_port = Strings::Split(argv[1], ':');
if (!zone_port.empty()) {
z_name = zone_port[0];
}
if (zone_port.size() > 1) {
std::string p_name = zone_port[1];
Config->SetZonePort(Strings::ToInt(p_name));
}
worldserver.SetLaunchedName(z_name.c_str());
if (strncmp(z_name.c_str(), "dynamic_", 8) == 0) {
zone_name = ".";
worldserver.SetLaunchedName(".");
worldserver.SetLauncherName("NONE");
}
else {
zone_name = z_name.c_str();
}
}
else if (argc == 2) {
worldserver.SetLauncherName("NONE");
auto zone_port = Strings::Split(argv[1], ':');
if (!zone_port.empty()) {
z_name = zone_port[0];
}
if (zone_port.size() > 1) {
std::string p_name = zone_port[1];
Config->SetZonePort(Strings::ToInt(p_name));
}
worldserver.SetLaunchedName(z_name.c_str());
if (strncmp(z_name.c_str(), "dynamic_", 8) == 0) {
zone_name = ".";
}
else {
zone_name = z_name.c_str();
}
}
else {
zone_name = ".";
worldserver.SetLaunchedName(".");
worldserver.SetLauncherName("NONE");
}
auto mutex = new Mutex;
@ -235,7 +247,8 @@ int main(int argc, char** argv) {
Config->DatabaseUsername.c_str(),
Config->DatabasePassword.c_str(),
Config->DatabaseDB.c_str(),
Config->DatabasePort)) {
Config->DatabasePort
)) {
LogError("Cannot continue without a database connection");
return 1;
}
@ -243,7 +256,7 @@ int main(int argc, char** argv) {
// Multi-tenancy: Content Database
if (!Config->ContentDbHost.empty()) {
if (!content_db.Connect(
Config->ContentDbHost.c_str() ,
Config->ContentDbHost.c_str(),
Config->ContentDbUsername.c_str(),
Config->ContentDbPassword.c_str(),
Config->ContentDbName.c_str(),
@ -280,7 +293,12 @@ int main(int argc, char** argv) {
EQ::InitializeDynamicLookups();
}
/* Register Log System and Settings */
// command handler
if (ZoneCLI::RanConsoleCommand(argc, argv) && !ZoneCLI::RanSidecarCommand(argc, argv)) {
LogSys.EnableConsoleLogging();
ZoneCLI::CommandHandler(argc, argv);
}
LogSys.SetDatabase(&database)
->SetLogPath(path.GetLogPath())
->LoadLogDatabaseSettings()
@ -455,6 +473,11 @@ int main(int argc, char** argv) {
worldserver.Connect();
worldserver.SetScheduler(&event_scheduler);
// sidecar command handler
if (ZoneCLI::RanConsoleCommand(argc, argv) && ZoneCLI::RanSidecarCommand(argc, argv)) {
ZoneCLI::CommandHandler(argc, argv);
}
Timer InterserverTimer(INTERSERVER_TIMER); // does MySQL pings and auto-reconnect
#ifdef EQPROFILE
#ifdef PROFILE_DUMP_TIME
@ -486,13 +509,13 @@ int main(int argc, char** argv) {
Timer quest_timers(100);
UpdateWindowTitle(nullptr);
std::shared_ptr<EQStreamInterface> eqss;
EQStreamInterface *eqsi;
std::unique_ptr<EQ::Net::EQStreamManager> eqsm;
std::shared_ptr<EQStreamInterface> eqss;
EQStreamInterface *eqsi;
std::unique_ptr<EQ::Net::EQStreamManager> eqsm;
std::chrono::time_point<std::chrono::system_clock> frame_prev = std::chrono::system_clock::now();
std::unique_ptr<EQ::Net::WebsocketServer> ws_server;
std::unique_ptr<EQ::Net::WebsocketServer> ws_server;
auto loop_fn = [&](EQ::Timer* t) {
auto loop_fn = [&](EQ::Timer *t) {
//Advance the timer to our current point in time
Timer::SetCurrentTime();
@ -520,12 +543,12 @@ int main(int argc, char** argv) {
LogInfo("Starting EQ Network server on port [{}]", Config->ZonePort);
EQStreamManagerInterfaceOptions opts(Config->ZonePort, false, RuleB(Network, CompressZoneStream));
opts.daybreak_options.resend_delay_ms = RuleI(Network, ResendDelayBaseMS);
opts.daybreak_options.resend_delay_ms = RuleI(Network, ResendDelayBaseMS);
opts.daybreak_options.resend_delay_factor = RuleR(Network, ResendDelayFactor);
opts.daybreak_options.resend_delay_min = RuleI(Network, ResendDelayMinMS);
opts.daybreak_options.resend_delay_max = RuleI(Network, ResendDelayMaxMS);
opts.daybreak_options.outgoing_data_rate = RuleR(Network, ClientDataRate);
eqsm = std::make_unique<EQ::Net::EQStreamManager>(opts);
opts.daybreak_options.resend_delay_min = RuleI(Network, ResendDelayMinMS);
opts.daybreak_options.resend_delay_max = RuleI(Network, ResendDelayMaxMS);
opts.daybreak_options.outgoing_data_rate = RuleR(Network, ClientDataRate);
eqsm = std::make_unique<EQ::Net::EQStreamManager>(opts);
eqsf_open = true;
eqsm->OnNewConnection(
@ -546,7 +569,7 @@ int main(int argc, char** argv) {
//check the stream identifier for any now-identified streams
while ((eqsi = stream_identifier.PopIdentified())) {
//now that we know what patch they are running, start up their client object
struct in_addr in;
struct in_addr in;
in.s_addr = eqsi->GetRemoteIP();
LogInfo("New client from [{}]:[{}]", inet_ntoa(in), ntohs(eqsi->GetRemotePort()));
auto client = new Client(eqsi);
@ -558,7 +581,15 @@ int main(int argc, char** argv) {
}
else {
if (worldwasconnected && is_zone_loaded) {
entity_list.ChannelMessageFromWorld(0, 0, ChatChannel_Broadcast, 0, 0, 100, "WARNING: World server connection lost");
entity_list.ChannelMessageFromWorld(
0,
0,
ChatChannel_Broadcast,
0,
0,
100,
"WARNING: World server connection lost"
);
worldwasconnected = false;
}
}
@ -614,8 +645,9 @@ int main(int argc, char** argv) {
safe_delete(Config);
if (zone != 0)
if (zone != 0) {
Zone::Shutdown(true);
}
//Fix for Linux world server problem.
safe_delete(task_manager);
safe_delete(npc_scale_manager);
@ -638,7 +670,8 @@ void Shutdown()
EQ::EventLoop::Get().Shutdown();
}
void CatchSignal(int sig_num) {
void CatchSignal(int sig_num)
{
#ifdef _WINDOWS
LogInfo("Recieved signal: [{}]", sig_num);
#endif
@ -646,7 +679,8 @@ void CatchSignal(int sig_num) {
}
/* Update Window Title with relevant information */
void UpdateWindowTitle(char* iNewTitle) {
void UpdateWindowTitle(char *iNewTitle)
{
#ifdef _WINDOWS
char tmp[500];
if (iNewTitle) {

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
);
}
}

View File

@ -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");
}
}

View File

@ -0,0 +1,4 @@
void SidecarApi::MapBestZController(const httplib::Request &req, httplib::Response &res)
{
}

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);
}

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

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");
}

View File

@ -1821,6 +1821,11 @@ void Zone::ResetShutdownTimer() {
autoshutdown_timer.Start(autoshutdown_timer.GetDuration(), true);
}
void Zone::StopShutdownTimer() {
LogInfo("Stopping zone shutdown timer");
autoshutdown_timer.Disable();
}
bool Zone::Depop(bool StartSpawnTimer) {
std::map<uint32,NPCType *>::iterator itr;
entity_list.Depop(StartSpawnTimer);

View File

@ -302,6 +302,7 @@ public:
void SpawnConditionChanged(const SpawnCondition &c, int16 old_value);
void StartShutdownTimer(uint32 set_time = (RuleI(Zone, AutoShutdownDelay)));
void ResetShutdownTimer();
void StopShutdownTimer();
void UpdateQGlobal(uint32 qid, QGlobal newGlobal);
void weatherSend(Client *client = nullptr);
void ClearSpawnTimers();

32
zone/zone_cli.cpp Normal file
View File

@ -0,0 +1,32 @@
#include "zone_cli.h"
#include "../common/cli/eqemu_command_handler.h"
#include <string.h>
bool ZoneCLI::RanConsoleCommand(int argc, char **argv)
{
return argc > 1 && (strstr(argv[1], ":") != nullptr || strstr(argv[1], "--") != nullptr);
}
bool ZoneCLI::RanSidecarCommand(int argc, char **argv)
{
return argc > 1 && (strstr(argv[1], "sidecar:") != nullptr);
}
void ZoneCLI::CommandHandler(int argc, char **argv)
{
if (argc == 1) { return; }
argh::parser cmd;
cmd.parse(argc, argv, argh::parser::PREFER_PARAM_FOR_UNREG_OPTION);
EQEmuCommand::DisplayDebug(cmd);
// Declare command mapping
auto function_map = EQEmuCommand::function_map;
// Register commands
function_map["sidecar:serve-http"] = &ZoneCLI::SidecarServeHttp;
EQEmuCommand::HandleMenu(function_map, cmd, argc, argv);
}
#include "cli/sidecar_serve_http.cpp"

15
zone/zone_cli.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef EQEMU_ZONE_CLI_H
#define EQEMU_ZONE_CLI_H
#include "../common/cli/argh.h"
class ZoneCLI {
public:
static void CommandHandler(int argc, char **argv);
static void SidecarServeHttp(int argc, char **argv, argh::parser &cmd, std::string &description);
static bool RanConsoleCommand(int argc, char **argv);
static bool RanSidecarCommand(int argc, char **argv);
};
#endif //EQEMU_ZONE_CLI_H