diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 8500080e..8311fb61 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -40,6 +40,7 @@ add_subdirectory(qr) add_subdirectory(natneg) add_subdirectory(GP) add_subdirectory(gamestats) +add_subdirectory(gamestats_http) add_subdirectory(search) add_subdirectory(FESL) add_subdirectory(peerchat) diff --git a/code/gamestats/server/commands/handle_auth.cpp b/code/gamestats/server/commands/handle_auth.cpp index dd4f909a..70c72eb0 100644 --- a/code/gamestats/server/commands/handle_auth.cpp +++ b/code/gamestats/server/commands/handle_auth.cpp @@ -54,6 +54,17 @@ namespace GS { } m_response = data_parser.GetValue("response"); + if (gamename.compare("sniperelpc") == 0) { + m_game = OS::GameData(); + m_game.gamename = gamename; + m_game.secretkey = "hP58dm"; + if (IsResponseValid(m_response.c_str())) { + std::ostringstream ss; + ss << "\\lc\\2\\sesskey\\" << m_session_key << "\\proof\\0\\id\\" << local_id; + SendPacket(ss.str()); + return; + } + } GPPersistRequestData *persist_request_data = (GPPersistRequestData *)malloc(sizeof(GPPersistRequestData)); persist_request_data->profileid = 0; diff --git a/code/gamestats/server/commands/handle_updgame.cpp b/code/gamestats/server/commands/handle_updgame.cpp index 71cf10d2..919cf23c 100644 --- a/code/gamestats/server/commands/handle_updgame.cpp +++ b/code/gamestats/server/commands/handle_updgame.cpp @@ -57,6 +57,80 @@ namespace GS { } game_data = OS::KeyStringToMap(gamedata); + + // Sniper Elite leaderboard writes (Redis) + if (m_game.gamename == "sniperelpc") { + auto it_mission = game_data.find("mission"); + auto it_pid = game_data.find("pid_0"); + auto it_total = game_data.find("total_0"); + + if (it_mission != game_data.end() && it_pid != game_data.end() && it_total != game_data.end()) { + int mission = atoi(it_mission->second.c_str()); + int pid = atoi(it_pid->second.c_str()); + long long total = _strtoi64(it_total->second.c_str(), nullptr, 10); + + redisContext* ctx = TaskShared::getThreadLocalRedisContext(); + if (ctx) { + { + std::ostringstream hk; + hk << "gstats:sniperelpc:missionstats:" << mission << ":" << pid; + std::string hkey = hk.str(); + + auto getv = [&](const char *k) -> const char * { + auto it = game_data.find(k); + if (it == game_data.end()) return "0"; + if (it->second.empty()) return "0"; + return it->second.c_str(); + }; + + redisReply* r = (redisReply*)redisCommand( + ctx, + "HMSET %s " + "twoforone %s " + "threeforone %s " + "fourforone %s " + "silentkill %s " + "covertkill %s " + "2covertkill %s " + "3coverkill %s " + "headshot %s " + "moving %s " + "healthlost %s " + "accuracy %s " + "longestshot %s " + "pinpull %s " + "difficulty %s", + hkey.c_str(), + getv("twoforone_0"), + getv("threeforone_0"), + getv("fourforone_0"), + getv("silentkill_0"), + getv("covertkill_0"), + getv("2covertkill_0"), + getv("3coverkill_0"), + getv("headshot_0"), + getv("moving_0"), + getv("healthlost_0"), + getv("accuracy_0"), + getv("longestshot_0"), + getv("pinpull_0"), + getv("difficulty_0")); + if (r) freeReplyObject(r); + } + { + redisReply* r = (redisReply*)redisCommand(ctx, "ZADD %s %lld %d", "gstats:sniperelpc:total", total, pid); + if (r) freeReplyObject(r); + } + { + std::ostringstream k; + k << "gstats:sniperelpc:mission:" << mission; + redisReply* r = (redisReply*)redisCommand(ctx, "ZADD %s %lld %d", k.str().c_str(), total, pid); + if (r) freeReplyObject(r); + } + } + } + } + PersistBackendRequest req; req.profileid = m_profile.id; req.mp_peer = this; diff --git a/code/gamestats_http/CMakeLists.txt b/code/gamestats_http/CMakeLists.txt new file mode 100644 index 00000000..26172d7c --- /dev/null +++ b/code/gamestats_http/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required (VERSION 3.22) + +project(gstats_http) + +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + +file (GLOB MAIN_SRCS "*.cpp") +file (GLOB MAIN_HDRS "*.h") +file (GLOB SERVER_SRCS "server/*.cpp") +file (GLOB SERVER_HDRS "server/*.h") + +set (ALL_SRCS ${MAIN_SRCS} ${MAIN_HDRS} ${SERVER_SRCS} ${SERVER_HDRS}) + +include_directories (${CMAKE_CURRENT_SOURCE_DIR}) + +source_group("Sources" FILES ${MAIN_SRCS}) +source_group("Sources\\Server" FILES ${SERVER_SRCS}) + +source_group("Headers" FILES ${MAIN_HDRS}) +source_group("Headers\\Server" FILES ${SERVER_HDRS}) + +add_executable (gstats_http ${ALL_SRCS}) + +target_link_libraries(gstats_http openspy SharedTasks ZLIB::ZLIB) diff --git a/code/gamestats_http/main.cpp b/code/gamestats_http/main.cpp new file mode 100644 index 00000000..3cb1ad69 --- /dev/null +++ b/code/gamestats_http/main.cpp @@ -0,0 +1,52 @@ +#include +#include +#include +#include + +#include "server/GHTTPServer.h" +#include "server/GHTTPDriver.h" + +INetServer *g_httpserver = NULL; + +void tick_handler(uv_timer_t *handle) { + g_httpserver->tick(); +} + +int main() { + uv_loop_t *loop = uv_default_loop(); + uv_timer_t tick_timer; + + uv_timer_init(uv_default_loop(), &tick_timer); + + OS::Init("gstats_http"); + g_httpserver = new GHTTP::Server(); + + char address_buff[256]; + char port_buff[16]; + size_t temp_env_sz = sizeof(address_buff); + + if (uv_os_getenv("OPENSPY_GSTATS_HTTP_BIND_ADDR", (char *)&address_buff, &temp_env_sz) != UV_ENOENT) { + temp_env_sz = sizeof(port_buff); + + uint16_t port = 80; + if (uv_os_getenv("OPENSPY_GSTATS_HTTP_BIND_PORT", (char *)&port_buff, &temp_env_sz) != UV_ENOENT) { + port = atoi(port_buff); + } + + GHTTP::Driver *driver = new GHTTP::Driver(g_httpserver, address_buff, port); + + OS::LogText(OS::ELogLevel_Info, "Adding gstats_http Driver: %s:%d\n", address_buff, port); + g_httpserver->addNetworkDriver(driver); + } else { + OS::LogText(OS::ELogLevel_Warning, "Missing gstats_http bind address environment variable"); + } + + uv_timer_start(&tick_timer, tick_handler, 0, 250); + uv_run(loop, UV_RUN_DEFAULT); + uv_loop_close(loop); + + delete g_httpserver; + + OS::Shutdown(); + return 0; +} diff --git a/code/gamestats_http/main.h b/code/gamestats_http/main.h new file mode 100644 index 00000000..15d739e6 --- /dev/null +++ b/code/gamestats_http/main.h @@ -0,0 +1,4 @@ +#ifndef _GSTATS_HTTP_MAIN_H +#define _GSTATS_HTTP_MAIN_H + +#endif diff --git a/code/gamestats_http/server/GHTTPDriver.cpp b/code/gamestats_http/server/GHTTPDriver.cpp new file mode 100644 index 00000000..2dfb0c04 --- /dev/null +++ b/code/gamestats_http/server/GHTTPDriver.cpp @@ -0,0 +1,9 @@ +#include "GHTTPDriver.h" + +namespace GHTTP { + Driver::Driver(INetServer *server, const char *host, uint16_t port) : TCPDriver(server, host, port) { + } + INetPeer *Driver::CreatePeer(uv_tcp_t *sd) { + return new Peer(this, sd); + } +} diff --git a/code/gamestats_http/server/GHTTPDriver.h b/code/gamestats_http/server/GHTTPDriver.h new file mode 100644 index 00000000..cdefdbde --- /dev/null +++ b/code/gamestats_http/server/GHTTPDriver.h @@ -0,0 +1,18 @@ +#ifndef _GHTTP_DRIVER_H +#define _GHTTP_DRIVER_H + +#include +#include + +#include "GHTTPPeer.h" + +namespace GHTTP { + class Driver : public OS::TCPDriver { + public: + Driver(INetServer *server, const char *host, uint16_t port); + protected: + virtual INetPeer *CreatePeer(uv_tcp_t *sd); + }; +} + +#endif diff --git a/code/gamestats_http/server/GHTTPPeer.cpp b/code/gamestats_http/server/GHTTPPeer.cpp new file mode 100644 index 00000000..1e473234 --- /dev/null +++ b/code/gamestats_http/server/GHTTPPeer.cpp @@ -0,0 +1,659 @@ +#include "GHTTPPeer.h" +#include "GHTTPDriver.h" + +#include +#include + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + struct WarRecordState { + std::chrono::steady_clock::time_point war_record_until; + std::vector> recent_missions; + }; + + struct NickCacheEntry { + std::string nick; + std::chrono::steady_clock::time_point expires_at; + }; + + static std::mutex s_state_mutex; + static std::unordered_map s_war_state; + static std::mutex s_nick_mutex; + static std::unordered_map s_nick_cache; + + static std::string make_state_key(const OS::Address &addr, int pid) { + std::ostringstream ss; + ss << addr.ToString(true) << ":" << pid; + return ss.str(); + } + + static bool should_treat_as_war_record(const std::string &key, int mission) { + using namespace std::chrono; + auto now = steady_clock::now(); + std::lock_guard lk(s_state_mutex); + + WarRecordState &st = s_war_state[key]; + if (st.war_record_until > now) { + return true; + } + + st.recent_missions.push_back({mission, now}); + auto window = seconds(2); + st.recent_missions.erase( + std::remove_if(st.recent_missions.begin(), st.recent_missions.end(), [&](const auto &p) { + return (now - p.second) > window; + }), + st.recent_missions.end()); + + std::unordered_set uniq; + for (auto &p : st.recent_missions) { + uniq.insert(p.first); + } + + if (uniq.size() >= 3) { + st.war_record_until = now + seconds(10); + return true; + } + + return false; + } + + static std::string get_query_value(const std::map &q, const char *k) { + auto it = q.find(k); + if (it == q.end()) return ""; + return it->second; + } + + static int from_hex(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); + return -1; + } + + static std::string url_decode(const std::string &in) { + std::string out; + out.reserve(in.size()); + for (size_t i = 0; i < in.size(); i++) { + char c = in[i]; + if (c == '+') { + out.push_back(' '); + continue; + } + if (c == '%' && i + 2 < in.size()) { + int hi = from_hex(in[i + 1]); + int lo = from_hex(in[i + 2]); + if (hi >= 0 && lo >= 0) { + out.push_back((char)((hi << 4) | lo)); + i += 2; + continue; + } + } + out.push_back(c); + } + return out; + } + + static int to_int(const std::string &s, int def) { + if (s.empty()) return def; + return atoi(s.c_str()); + } + + static std::string build_http_status(int code) { + switch (code) { + case 200: return "200 OK"; + case 404: return "404 Not Found"; + case 400: return "400 Bad Request"; + default: return "500 Internal Server Error"; + } + } + + static bool synthetic_fallback_enabled() { + static int s_cached = -1; + if (s_cached != -1) return s_cached == 1; + char buf[16]; + size_t sz = sizeof(buf); + if (uv_os_getenv("OPENSPY_GSTATS_HTTP_SYNTHETIC", buf, &sz) == 0) { + s_cached = atoi(buf) ? 1 : 0; + } else { + s_cached = 0; + } + return s_cached == 1; + } + + static std::string zset_key_total() { + return "gstats:sniperelpc:total"; + } + static std::string zset_key_mission(int mission) { + std::ostringstream ss; + ss << "gstats:sniperelpc:mission:" << mission; + return ss.str(); + } + + static bool webservices_ready() { + return OS::g_webServicesURL && OS::g_webServicesURL[0] && OS::g_webServicesAPIKey && OS::g_webServicesAPIKey[0]; + } + + static std::unordered_map fetch_nicks_webservices(const std::vector &pids) { + std::unordered_map out; + if (pids.empty() || !webservices_ready()) return out; + + json_t *root = json_object(); + json_t *arr = json_array(); + for (int pid : pids) { + json_array_append_new(arr, json_integer(pid)); + } + json_object_set_new(root, "target_profileids", arr); + + char *payload = json_dumps(root, 0); + json_decref(root); + if (!payload) return out; + + std::string url = std::string(OS::g_webServicesURL) + "/v1/Profile/lookup"; + OS::HTTPClient client(url); + OS::HTTPResponse resp = client.Post(payload, nullptr); + free(payload); + + if (resp.status_code != 200 || resp.buffer.empty()) return out; + + json_error_t jerr; + json_t *resp_json = json_loads(resp.buffer.c_str(), 0, &jerr); + if (!resp_json) return out; + + auto try_add_profile = [&](json_t *profile_obj) { + if (!profile_obj || !json_is_object(profile_obj)) return; + json_t *id_obj = json_object_get(profile_obj, "id"); + if (!id_obj || !json_is_integer(id_obj)) return; + int pid = (int)json_integer_value(id_obj); + + const char *name = nullptr; + json_t *un = json_object_get(profile_obj, "uniquenick"); + if (un && json_is_string(un)) name = json_string_value(un); + if (!name || !name[0]) { + json_t *n = json_object_get(profile_obj, "nick"); + if (n && json_is_string(n)) name = json_string_value(n); + } + if (name && name[0]) { + out[pid] = name; + } + }; + + if (json_is_array(resp_json)) { + size_t n = json_array_size(resp_json); + for (size_t i = 0; i < n; i++) { + try_add_profile(json_array_get(resp_json, i)); + } + } else { + try_add_profile(resp_json); + } + + json_decref(resp_json); + return out; + } + + static std::unordered_map resolve_nicks(const std::vector &pids) { + using namespace std::chrono; + std::unordered_map result; + if (pids.empty()) return result; + + std::vector missing; + missing.reserve(pids.size()); + auto now = steady_clock::now(); + + { + std::lock_guard lk(s_nick_mutex); + for (int pid : pids) { + auto it = s_nick_cache.find(pid); + if (it != s_nick_cache.end() && it->second.expires_at > now && !it->second.nick.empty()) { + result[pid] = it->second.nick; + } else { + missing.push_back(pid); + } + } + } + + if (!missing.empty()) { + auto fetched = fetch_nicks_webservices(missing); + std::lock_guard lk(s_nick_mutex); + for (int pid : missing) { + NickCacheEntry ent; + auto fit = fetched.find(pid); + if (fit != fetched.end()) { + ent.nick = fit->second; + result[pid] = ent.nick; + } + ent.expires_at = now + minutes(5); + s_nick_cache[pid] = ent; + } + } + + return result; + } +} + +namespace GHTTP { + Peer::Peer(Driver *driver, uv_tcp_t *sd) : INetPeer(driver, sd) { + m_delete_flag = false; + m_timeout_flag = false; + uv_clock_gettime(UV_CLOCK_MONOTONIC, &m_last_recv); + uv_clock_gettime(UV_CLOCK_MONOTONIC, &m_last_ping); + OnConnectionReady(); + } + + Peer::~Peer(){} + + void Peer::OnConnectionReady() { + OS::LogText(OS::ELogLevel_Info, "[%s] New HTTP connection", getAddress().ToString().c_str()); + } + + void Peer::think() { + if (m_delete_flag) return; + } + + void Peer::Delete(bool timeout) { + m_timeout_flag = timeout; + m_delete_flag = true; + } + + void Peer::on_stream_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { + m_recv_accumulator.append((const char *)buf->base, nread); + if (m_recv_accumulator.size() > MAX_UNPROCESSED_DATA) { + Delete(); + return; + } + + size_t header_end = m_recv_accumulator.find("\r\n\r\n"); + if (header_end == std::string::npos) { + return; + } + std::string req = m_recv_accumulator.substr(0, header_end + 4); + m_recv_accumulator.erase(0, header_end + 4); + + handle_http_request(req); + } + + bool Peer::parse_request_line(const std::string &request, std::string &method, std::string &target) { + size_t line_end = request.find("\r\n"); + if (line_end == std::string::npos) return false; + std::string line = request.substr(0, line_end); + size_t sp1 = line.find(' '); + if (sp1 == std::string::npos) return false; + size_t sp2 = line.find(' ', sp1 + 1); + if (sp2 == std::string::npos) return false; + method = line.substr(0, sp1); + target = line.substr(sp1 + 1, sp2 - (sp1 + 1)); + return true; + } + + void Peer::parse_query_string(const std::string &query, std::map &out) { + size_t start = 0; + while (start < query.size()) { + size_t amp = query.find('&', start); + std::string part = (amp == std::string::npos) ? query.substr(start) : query.substr(start, amp - start); + size_t eq = part.find('='); + if (eq != std::string::npos) { + out[url_decode(part.substr(0, eq))] = url_decode(part.substr(eq + 1)); + } else if (!part.empty()) { + out[url_decode(part)] = ""; + } + if (amp == std::string::npos) break; + start = amp + 1; + } + } + + void Peer::handle_http_request(const std::string &request) { + std::string method, target; + if (!parse_request_line(request, method, target) || (method != "GET" && method != "HEAD")) { + send_http_response(400, ""); + return; + } + + OS::LogText(OS::ELogLevel_Debug, "[%s] HTTP %s %s", getAddress().ToString().c_str(), method.c_str(), target.c_str()); + + std::string path = target; + std::string query_str; + size_t qpos = target.find('?'); + if (qpos != std::string::npos) { + path = target.substr(0, qpos); + query_str = target.substr(qpos + 1); + } + + std::map query; + parse_query_string(query_str, query); + + std::string body; + if (path == "/sniperelpc/score.asp") { + body = handle_score(query); + if (method == "HEAD") body.clear(); + send_http_response(200, body); + return; + } + if (path == "/sniperelpc/mission.asp") { + body = handle_mission(query); + if (method == "HEAD") body.clear(); + send_http_response(200, body); + return; + } + + send_http_response(404, ""); + } + + void Peer::send_http_response(int status_code, const std::string &body) { + std::ostringstream ss; + ss << "HTTP/1.1 " << build_http_status(status_code) << "\r\n"; + ss << "Content-Type: text/plain\r\n"; + ss << "Content-Length: " << body.size() << "\r\n"; + ss << "Connection: close\r\n"; + ss << "\r\n"; + ss << body; + + OS::Buffer buffer; + std::string out = ss.str(); + buffer.WriteBuffer(out.c_str(), out.size()); + append_send_buffer(buffer, true); + Delete(); + } + + std::string Peer::handle_score(const std::map &query) { + redisContext *ctx = TaskShared::getThreadLocalRedisContext(); + std::ostringstream out; + out << "SnipeScore|"; + + if (!ctx) return out.str(); + + int rows = to_int(get_query_value(query, "rows"), 10); + int pid = to_int(get_query_value(query, "pid"), -1); + int anchor = to_int(get_query_value(query, "score"), -1); + + OS::LogText(OS::ELogLevel_Debug, "[%s] score.asp rows=%d pid=%d score=%d", getAddress().ToString().c_str(), rows, pid, anchor); + + std::string key = zset_key_total(); + + std::unordered_map nick_map; + auto emit_row = [&](int rank1, int row_pid, long long score) { + out << rank1 << "|"; + auto it = nick_map.find(row_pid); + out << (it != nick_map.end() ? it->second : "") << "|"; + out << row_pid << "|"; + out << score << "|"; + }; + + if (pid != -1 && anchor < 0) { + redisReply *rreply = (redisReply *)redisCommand(ctx, "ZREVRANK %s %d", key.c_str(), pid); + if (!rreply || rreply->type != REDIS_REPLY_INTEGER) { + if (rreply) freeReplyObject(rreply); + return out.str(); + } + int rank0 = (int)rreply->integer; + freeReplyObject(rreply); + + if (rows <= 1) { + redisReply *sreply = (redisReply *)redisCommand(ctx, "ZSCORE %s %d", key.c_str(), pid); + if (!sreply || sreply->type != REDIS_REPLY_STRING) { + if (sreply) freeReplyObject(sreply); + return out.str(); + } + long long score = atoll(sreply->str); + freeReplyObject(sreply); + + nick_map = resolve_nicks({pid}); + emit_row(rank0 + 1, pid, score); + return out.str(); + } + + int half = rows / 2; + int start = rank0 - half; + if (start < 0) start = 0; + int end = start + (rows - 1); + + redisReply *reply = (redisReply *)redisCommand(ctx, "ZREVRANGE %s %d %d WITHSCORES", key.c_str(), start, end); + if (!reply || reply->type != REDIS_REPLY_ARRAY) { + if (reply) freeReplyObject(reply); + return out.str(); + } + if (reply->elements < 2) { + freeReplyObject(reply); + return out.str(); + } + + { + std::vector pids; + pids.reserve(reply->elements / 2); + for (size_t i = 0; i + 1 < reply->elements; i += 2) { + pids.push_back(atoi(reply->element[i]->str)); + } + nick_map = resolve_nicks(pids); + } + + for (size_t i = 0; i + 1 < reply->elements; i += 2) { + int row_pid = atoi(reply->element[i]->str); + long long score = atoll(reply->element[i + 1]->str); + emit_row(start + (int)(i / 2) + 1, row_pid, score); + } + + freeReplyObject(reply); + return out.str(); + } + + redisReply *reply = NULL; + if (anchor >= 0) { + reply = (redisReply *)redisCommand(ctx, "ZREVRANGEBYSCORE %s %d -inf LIMIT 0 %d WITHSCORES", key.c_str(), anchor, rows); + } else { + reply = (redisReply *)redisCommand(ctx, "ZREVRANGE %s 0 %d WITHSCORES", key.c_str(), rows - 1); + } + + if (!reply || reply->type != REDIS_REPLY_ARRAY) { + if (reply) freeReplyObject(reply); + return out.str(); + } + if (reply->elements < 2) { + freeReplyObject(reply); + return out.str(); + } + + { + std::vector pids; + pids.reserve(reply->elements / 2); + for (size_t i = 0; i + 1 < reply->elements; i += 2) { + pids.push_back(atoi(reply->element[i]->str)); + } + nick_map = resolve_nicks(pids); + } + + for (size_t i = 0; i + 1 < reply->elements; i += 2) { + int row_pid = atoi(reply->element[i]->str); + long long score = atoll(reply->element[i + 1]->str); + + redisReply *rreply = (redisReply *)redisCommand(ctx, "ZREVRANK %s %d", key.c_str(), row_pid); + if (!rreply || rreply->type != REDIS_REPLY_INTEGER) { + if (rreply) freeReplyObject(rreply); + continue; + } + int rank1 = (int)rreply->integer + 1; + freeReplyObject(rreply); + + emit_row(rank1, row_pid, score); + } + + freeReplyObject(reply); + return out.str(); + } + + std::string Peer::handle_mission(const std::map &query) { + redisContext *ctx = TaskShared::getThreadLocalRedisContext(); + std::ostringstream out; + out << "SnipeMission|"; + + if (!ctx) return out.str(); + + int rows = to_int(get_query_value(query, "rows"), 10); + int pid = to_int(get_query_value(query, "pid"), -1); + int anchor = to_int(get_query_value(query, "score"), -1); + int mission = to_int(get_query_value(query, "mission"), -1); + + OS::LogText(OS::ELogLevel_Debug, "[%s] mission.asp rows=%d pid=%d score=%d mission=%d", getAddress().ToString().c_str(), rows, pid, anchor, mission); + + if (mission < 0) { + return out.str(); + } + + std::string key = zset_key_mission(mission); + + std::unordered_map nick_map; + auto fetch_stats = [&](int row_pid) { + std::array vals; + vals.fill(0); + std::ostringstream hk; + hk << "gstats:sniperelpc:missionstats:" << mission << ":" << row_pid; + std::string hkey = hk.str(); + + redisReply *hreply = (redisReply *)redisCommand( + ctx, + "HMGET %s twoforone threeforone fourforone silentkill covertkill 2covertkill 3coverkill headshot moving healthlost accuracy longestshot pinpull difficulty", + hkey.c_str()); + if (!hreply || hreply->type != REDIS_REPLY_ARRAY) { + if (hreply) freeReplyObject(hreply); + return vals; + } + for (size_t i = 0; i < hreply->elements && i < vals.size(); i++) { + redisReply *e = hreply->element[i]; + if (!e) continue; + if (e->type == REDIS_REPLY_STRING && e->str) { + vals[i] = _strtoi64(e->str, nullptr, 10); + } else if (e->type == REDIS_REPLY_INTEGER) { + vals[i] = (long long)e->integer; + } + } + freeReplyObject(hreply); + return vals; + }; + auto emit_row = [&](int rank1, int row_pid, long long total_score) { + out << rank1 << "|"; + auto it = nick_map.find(row_pid); + out << (it != nick_map.end() ? it->second : "player" + std::to_string(row_pid)) << "|"; + out << row_pid << "|"; + auto stats = fetch_stats(row_pid); + for (size_t i = 0; i < stats.size(); i++) { + out << stats[i] << "|"; + } + out << total_score << "|"; + }; + + // Personal row lookup (used by Own Score in the game). + if (pid != -1 && anchor < 0) { + redisReply *sreply = (redisReply *)redisCommand(ctx, "ZSCORE %s %d", key.c_str(), pid); + if (!sreply || sreply->type != REDIS_REPLY_STRING) { + if (sreply) freeReplyObject(sreply); + return out.str(); + } + long long score = atoll(sreply->str); + freeReplyObject(sreply); + + redisReply *rreply = (redisReply *)redisCommand(ctx, "ZREVRANK %s %d", key.c_str(), pid); + if (!rreply || rreply->type != REDIS_REPLY_INTEGER) { + if (rreply) freeReplyObject(rreply); + return out.str(); + } + int rank0 = (int)rreply->integer; + freeReplyObject(rreply); + + if (rows <= 1) { + nick_map = resolve_nicks({pid}); + emit_row(rank0 + 1, pid, score); + return out.str(); + } + + int half = rows / 2; + int start = rank0 - half; + if (start < 0) start = 0; + int end = start + (rows - 1); + + redisReply *reply = (redisReply *)redisCommand(ctx, "ZREVRANGE %s %d %d WITHSCORES", key.c_str(), start, end); + if (!reply || reply->type != REDIS_REPLY_ARRAY) { + if (reply) freeReplyObject(reply); + return out.str(); + } + if (reply->elements < 2) { + freeReplyObject(reply); + return out.str(); + } + + { + std::vector pids; + pids.reserve(reply->elements / 2); + for (size_t i = 0; i + 1 < reply->elements; i += 2) { + pids.push_back(atoi(reply->element[i]->str)); + } + nick_map = resolve_nicks(pids); + } + + for (size_t i = 0; i + 1 < reply->elements; i += 2) { + int row_pid = atoi(reply->element[i]->str); + long long row_score = atoll(reply->element[i + 1]->str); + emit_row(start + (int)(i / 2) + 1, row_pid, row_score); + } + + freeReplyObject(reply); + return out.str(); + } + + // Leaderboard paging/list request. + redisReply *reply = NULL; + if (anchor >= 0) { + reply = (redisReply *)redisCommand(ctx, "ZREVRANGEBYSCORE %s %d -inf LIMIT 0 %d WITHSCORES", key.c_str(), anchor, rows); + } else { + reply = (redisReply *)redisCommand(ctx, "ZREVRANGE %s 0 %d WITHSCORES", key.c_str(), rows - 1); + } + + if (!reply || reply->type != REDIS_REPLY_ARRAY) { + if (reply) freeReplyObject(reply); + return out.str(); + } + if (reply->elements < 2) { + freeReplyObject(reply); + return out.str(); + } + + { + std::vector pids; + pids.reserve(reply->elements / 2); + for (size_t i = 0; i + 1 < reply->elements; i += 2) { + pids.push_back(atoi(reply->element[i]->str)); + } + nick_map = resolve_nicks(pids); + } + + for (size_t i = 0; i + 1 < reply->elements; i += 2) { + int row_pid = atoi(reply->element[i]->str); + long long score = atoll(reply->element[i + 1]->str); + + redisReply *rreply = (redisReply *)redisCommand(ctx, "ZREVRANK %s %d", key.c_str(), row_pid); + if (!rreply || rreply->type != REDIS_REPLY_INTEGER) { + if (rreply) freeReplyObject(rreply); + continue; + } + int rank1 = (int)rreply->integer + 1; + freeReplyObject(rreply); + + emit_row(rank1, row_pid, score); + } + + freeReplyObject(reply); + return out.str(); + } +} diff --git a/code/gamestats_http/server/GHTTPPeer.h b/code/gamestats_http/server/GHTTPPeer.h new file mode 100644 index 00000000..321e5ccd --- /dev/null +++ b/code/gamestats_http/server/GHTTPPeer.h @@ -0,0 +1,38 @@ +#ifndef _GHTTP_PEER_H +#define _GHTTP_PEER_H + +#include +#include +#include + +#define MAX_UNPROCESSED_DATA 32768 + +namespace GHTTP { + class Driver; + + class Peer : public INetPeer { + public: + Peer(Driver *driver, uv_tcp_t *sd); + virtual ~Peer(); + + void OnConnectionReady(); + void think(); + void Delete(bool timeout = false); + + private: + void on_stream_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf); + + void handle_http_request(const std::string &request); + void send_http_response(int status_code, const std::string &body); + + static bool parse_request_line(const std::string &request, std::string &method, std::string &target); + static void parse_query_string(const std::string &query, std::map &out); + + std::string handle_score(const std::map &query); + std::string handle_mission(const std::map &query); + + std::string m_recv_accumulator; + }; +} + +#endif diff --git a/code/gamestats_http/server/GHTTPServer.cpp b/code/gamestats_http/server/GHTTPServer.cpp new file mode 100644 index 00000000..0a0aadce --- /dev/null +++ b/code/gamestats_http/server/GHTTPServer.cpp @@ -0,0 +1,18 @@ +#include "GHTTPServer.h" + +namespace GHTTP { + Server::Server() : INetServer() { + uv_loop_set_data(uv_default_loop(), this); + } + Server::~Server() { + } + void Server::tick() { + std::vector::iterator it = m_net_drivers.begin(); + while (it != m_net_drivers.end()) { + INetDriver *driver = *it; + driver->think(); + it++; + } + NetworkTick(); + } +} diff --git a/code/gamestats_http/server/GHTTPServer.h b/code/gamestats_http/server/GHTTPServer.h new file mode 100644 index 00000000..8dffce64 --- /dev/null +++ b/code/gamestats_http/server/GHTTPServer.h @@ -0,0 +1,15 @@ +#ifndef _GHTTP_SERVER_H +#define _GHTTP_SERVER_H + +#include + +namespace GHTTP { + class Server : public INetServer { + public: + Server(); + virtual ~Server(); + void tick(); + }; +} + +#endif