Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 135 additions & 33 deletions src/Features/AutoSubmit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@
#include "Event.hpp"
#include "Features/Hud/Toasts.hpp"
#include "Features/NetMessage.hpp"
#include "Features/Session.hpp"
#include "Features/Speedrun/SpeedrunTimer.hpp"
#include "Modules/Client.hpp"
#include "Modules/Engine.hpp"
#include "Modules/FileSystem.hpp"
#include "Modules/Server.hpp"
#include "Utils/json11.hpp"
#include "Version.hpp"
#define STB_IMAGE_IMPLEMENTATION
#include "Utils/stb_image.h"

#include <cctype>
#include <curl/curl.h>
#include <filesystem>
#include <fstream>
#include <map>
#include <optional>
#include <sstream>
#include <string>
Expand All @@ -40,9 +42,7 @@ ON_EVENT(SESSION_START) {
}

ON_INIT {
NetMessage::RegisterHandler(COOP_NAME_MESSAGE_TYPE, +[](const void *data, size_t size) {
AutoSubmit::g_partner_name = std::string((char *)data, size);
});
NetMessage::RegisterHandler(COOP_NAME_MESSAGE_TYPE, +[](const void *data, size_t size) { AutoSubmit::g_partner_name = std::string((char *)data, size); });
}

ON_EVENT(SESSION_START) {
Expand All @@ -62,7 +62,9 @@ static std::thread g_worker;
static std::thread g_worker_search;
static std::map<std::string, std::string> g_map_ids;
static bool g_is_querying;
static std::vector<json11::Json> g_times;
static std::vector<PortalLeaderboardItem_t> g_times;
static std::vector<PortalLeaderboardItem_t> g_portals;
static bool g_lp;

static bool ensureCurlReady(CURL **curl) {
if (!*curl) {
Expand Down Expand Up @@ -143,6 +145,7 @@ static void testApiKey() {
g_map_ids.insert({map.fileName, map.chamberId});
}
}
client->EnableCustomLeaderboards();
return;
}

Expand All @@ -165,6 +168,8 @@ static void testApiKey() {
}

THREAD_PRINT("Downloaded %i maps!\n", g_map_ids.size());

client->EnableCustomLeaderboards();
}

std::optional<std::string> AutoSubmit::GetMapId(std::string map_name) {
Expand Down Expand Up @@ -213,7 +218,46 @@ static std::optional<int> getCurrentPbScore(std::string map_id) {
return atoi(str.c_str());
}

static std::shared_ptr<uint8_t> getAvatar(std::string url) {
if (!ensureCurlReady(&g_curl_search)) return {};

auto response = request(g_curl_search, url);

if (!response) return {};

auto avatar = *response;
uint8_t *bytes = (uint8_t *)avatar.c_str();
size_t len = avatar.length();

/* decode avatar jpg */
int w, h;
int channels;
auto img = stbi_load_from_memory(bytes, len, &w, &h, &channels, 0);

if (!img) return {};

/* avatar doesnt have alpha channel, but IImage needs it */
size_t size = w * h * channels;
size_t new_size = w * h * 4;
auto new_img = std::shared_ptr<uint8_t>(new uint8_t[new_size], std::default_delete<uint8_t[]>());
auto new_p = new_img.get();

size_t i = 0;
for (uint8_t *p = img; p != img + size; p += channels, i += 4) {
new_p[i + 0] = p[0];
new_p[i + 1] = p[1];
new_p[i + 2] = p[2];
new_p[i + 3] = 0xFF;
}

/* free old decoded jpg */
stbi_image_free(img);

return new_img;
}

static void startSearching(std::string mapName) {
/* get scores */
auto map_id = AutoSubmit::GetMapId(mapName);
if (!map_id.has_value()) {
g_is_querying = false;
Expand All @@ -222,9 +266,64 @@ static void startSearching(std::string mapName) {

auto json = AutoSubmit::GetTopScores(*map_id);

/* move map into vector so we can sort it */
std::vector<std::pair<std::string, json11::Json>> times;
for (const auto &it : json) {
times.push_back(it);
}

/* sort by rank */
std::sort(times.begin(), times.end(), [](const std::pair<std::string, json11::Json> &lhs, const std::pair<std::string, json11::Json> &rhs) {
return lhs.second["scoreData"]["playerRank"].int_value() < rhs.second["scoreData"]["playerRank"].int_value();
});

g_times.clear();
for (auto score : json) {
g_times.push_back(score);
size_t i = 0;
for (const auto &time : times) {
if (i == 40)
break;

PortalLeaderboardItem_t data;
strncpy(data.name, time.second["userData"]["boardname"].string_value().c_str(), sizeof(data.name));
strncpy(data.autorender, time.second["scoreData"]["autorender_id"].string_value().c_str(), sizeof(data.autorender));
data.avatarTex = getAvatar(time.second["userData"]["avatar"].string_value());
data.rank = time.second["scoreData"]["playerRank"].int_value();
data.score = time.second["scoreData"]["score"].int_value();

g_times.push_back(data);

++i;
}

/* get lp scores */
auto lp_map_id = std::to_string(distance(Game::mapNames.begin(), find(Game::mapNames.begin(), Game::mapNames.end(), mapName)) + 1);

auto lp_json = AutoSubmit::GetLeastPortals(lp_map_id);

g_portals.clear();
g_lp = false;
for (const auto &score : lp_json) {
g_lp = true;

bool is_coop = stoi(lp_map_id) > 60;

PortalLeaderboardItem_t data;

if (!is_coop) {
strncpy(data.name, score["user"]["user_name"].string_value().c_str(), sizeof(data.name));
strncpy(data.autorender, "null", sizeof(data.autorender));
data.avatarTex = getAvatar(score["user"]["avatar_link"].string_value());
data.rank = score["placement"].int_value();
data.score = score["score_count"].int_value();
} else {
strncpy(data.name, (score["host"]["user_name"].string_value() + " & " + score["partner"]["user_name"].string_value()).c_str(), sizeof(data.name));
strncpy(data.autorender, "null", sizeof(data.autorender));
data.avatarTex = getAvatar(score["host"]["avatar_link"].string_value());
data.rank = score["placement"].int_value();
data.score = score["score_count"].int_value();
}

g_portals.push_back(data);
}

g_is_querying = false;
Expand All @@ -237,33 +336,27 @@ void AutoSubmit::Search(std::string map) {
g_worker_search = std::thread(startSearching, map);
}

json11::Json::array AutoSubmit::GetTopScores(std::string &map_id) {
json11::Json::object AutoSubmit::GetTopScores(std::string &map_id) {
if (!ensureCurlReady(&g_curl_search)) return {};

curl_mime *form = curl_mime_init(g_curl_search);
curl_mimepart *field;
auto response = request(g_curl_search, g_api_base.substr(0, g_api_base.length() - 6) + "chamber/" + map_id + "/json");

field = curl_mime_addpart(form);
curl_mime_name(field, "auth_hash");
curl_mime_data(field, g_api_key.c_str(), CURL_ZERO_TERMINATED);

field = curl_mime_addpart(form);
curl_mime_name(field, "mapId");
curl_mime_data(field, map_id.c_str(), CURL_ZERO_TERMINATED);
if (!response) return {};

field = curl_mime_addpart(form);
curl_mime_name(field, "before");
curl_mime_data(field, "3", CURL_ZERO_TERMINATED);
std::string err;
auto json = json11::Json::parse(*response, err);

field = curl_mime_addpart(form);
curl_mime_name(field, "after");
curl_mime_data(field, "2", CURL_ZERO_TERMINATED);
if (err != "") {
return {};
}

curl_easy_setopt(g_curl_search, CURLOPT_MIMEPOST, form);
return json.object_items();
}

auto response = request(g_curl_search, g_api_base + "/top-scores");
json11::Json::array AutoSubmit::GetLeastPortals(std::string &map_id) {
if (!ensureCurlReady(&g_curl_search)) return {};

curl_mime_free(form);
auto response = request(g_curl_search, "https://lp.portal2.sr/api/v1/maps/" + map_id + "/leaderboards?page=1&pageSize=40");

if (!response) return {};

Expand All @@ -274,17 +367,26 @@ json11::Json::array AutoSubmit::GetTopScores(std::string &map_id) {
return {};
}

return json.array_items();
return json["data"]["records"].array_items();
}


bool AutoSubmit::IsQuerying() {
return g_is_querying;
}

const std::vector<json11::Json>& AutoSubmit::GetTimes() {
const std::vector<PortalLeaderboardItem_t> &AutoSubmit::GetTimes() {
return g_times;
}

const std::vector<PortalLeaderboardItem_t> &AutoSubmit::GetPortals() {
return g_portals;
}

bool AutoSubmit::IsLpAvailable() {
return g_lp;
}

static void submitTime(int score, std::string demopath, bool coop, std::string map_id, std::optional<std::string> rename_if_pb, std::optional<std::string> replay_append_if_pb) {
auto score_str = std::to_string(score);

Expand Down Expand Up @@ -362,7 +464,7 @@ static void submitTime(int score, std::string demopath, bool coop, std::string m

curl_easy_setopt(g_curl, CURLOPT_MIMEPOST, form);

auto resp = request(g_curl, g_api_base + "/auto-submit");
auto resp = request(g_curl, g_api_base + "/auto-submit");

curl_mime_free(form);

Expand Down Expand Up @@ -456,7 +558,7 @@ void AutoSubmit::LoadApiKey(bool output_nonexist) {
return;
}

g_api_base = "https://" + base + "/api-v2";
g_api_base = "https://" + base + "/api-v2";
g_api_key = key;
g_key_valid = false;
console->Print("Set API key! Testing...\n");
Expand Down Expand Up @@ -527,7 +629,7 @@ void retrieveMtriggers(int rank, std::string map_name) {
auto ticks = split["ticks"].int_value();
auto segmentTime = ticks * engine->GetIPT();
time += ticks * engine->GetIPT();
auto timeS = SpeedrunTimer::Format(time);
auto timeS = SpeedrunTimer::Format(time);
auto segmentTimeS = SpeedrunTimer::Format(segmentTime);
THREAD_PRINT("[%s] - %s (%s) (%i)\n", split["name"].string_value().c_str(), timeS.c_str(), segmentTimeS.c_str(), ticks);
}
Expand Down Expand Up @@ -605,6 +707,6 @@ void AutoSubmit::FinishRun(float final_time, const char *demopath, std::optional
}

ON_EVENT(SAR_UNLOAD) {
if (g_worker.joinable()) g_worker.detach();
if (g_worker.joinable()) g_worker.join();
if (g_worker_search.joinable()) g_worker_search.join();
}
18 changes: 16 additions & 2 deletions src/Features/AutoSubmit.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
#pragma once
#include "Utils/json11.hpp"

#include <map>
#include <memory>
#include <optional>
#include <string>

// NOTE: this actually has nothing to do with the game's PortalLeaderboardItem_t
struct PortalLeaderboardItem_t {
char name[32];
char autorender[16];
std::shared_ptr<uint8_t> avatarTex;
int32_t rank;
int32_t score;
};

namespace AutoSubmit {
extern bool g_cheated;
extern std::string g_partner_name;
Expand All @@ -12,7 +23,10 @@ namespace AutoSubmit {
void FinishRun(float final_time, const char *demopath, std::optional<std::string> rename_if_pb, std::optional<std::string> replay_append_if_pb);
std::optional<std::string> GetMapId(std::string map_name);
void Search(std::string map);
json11::Json::array GetTopScores(std::string &map_id);
json11::Json::object GetTopScores(std::string &map_id);
json11::Json::array GetLeastPortals(std::string &map_id);
bool IsQuerying();
const std::vector<json11::Json>& GetTimes();
const std::vector<PortalLeaderboardItem_t> &GetTimes();
const std::vector<PortalLeaderboardItem_t> &GetPortals();
bool IsLpAvailable();
};
Loading