Organize scripts and add TWS plugins with HTTP timeouts

Vendor pinned T5 and T6 plugin sources under the new plugins layout. Add native HTTP deadlines, status and timing diagnostics, telemetry integration, and build/test tooling.
This commit is contained in:
2026-10-09 11:02:20 -04:00
parent b172086301
commit 1121b44857
253 changed files with 28028 additions and 59 deletions
+94
View File
@@ -0,0 +1,94 @@
int main()
{
using namespace scripting;
http::http_request_params_t params{};
array options;
http::parse_request_options(params, options);
assert(params.limits.connect_timeout_ms == 5000 && params.limits.timeout_ms == 15000);
options["connect_timeout_ms"] = 120;
options["timeout_ms"] = 450;
options["method"] = "POST";
options["body"] = "snapshot";
array headers;
headers["Authorization"] = "Bearer test-only";
options["headers"] = headers;
http::parse_request_options(params, options);
assert(params.limits.connect_timeout_ms == 120 && params.limits.timeout_ms == 450);
assert(params.method == "POST" && params.fields == "snapshot" && params.headers.at("Authorization") == "Bearer test-only");
for (const auto* key : {"connect_timeout_ms", "timeout_ms"})
{
for (script_value value : {script_value(0), script_value(-1), script_value(1.5), script_value("150")})
{
array bad;
bad[key] = value;
bool rejected = false;
try { http::parse_request_options(params, bad); }
catch (const std::runtime_error&) { rejected = true; }
assert(rejected);
}
}
http::parse_request_options(params, options);
auto send = [&]()
{
notifications.clear();
http::create_request(params);
http::check_requests();
assert(notifications.empty()); // Incomplete work never notifies early.
scheduler::thread_pool.run();
http::check_requests();
assert(notifications.size() == 1 && notifications.front().size() == 4);
assert(utils::http::observed_limits.connect_timeout_ms == 120 && utils::http::observed_limits.timeout_ms == 450);
auto diagnostics = notifications.front()[3].as<array>();
assert(diagnostics["worker_queue_ms"].as<double>() >= 0 && diagnostics["notify_ms"].as<double>() >= 0);
assert(diagnostics["connect_timeout_ms"].as<int>() == 120 && diagnostics["timeout_ms"].as<int>() == 450);
assert(diagnostics.data->size() == 11);
http::check_requests();
assert(notifications.size() == 1); // Exactly one completion.
return diagnostics;
};
utils::http::result successful;
successful.code = CURLE_OK;
successful.status = 200;
successful.buffer = "accepted";
successful.dns_ms = 1;
successful.connect_ms = 2;
successful.tls_ms = 3;
successful.first_byte_ms = 4;
successful.total_ms = 5;
utils::http::next_result = successful;
auto diagnostics = send();
assert(notifications.front()[0].as<std::string>() == "accepted" && notifications.front()[1].as<bool>());
assert(notifications.front()[2].get_raw().type == game::SCRIPT_NONE);
assert(diagnostics["http_status"].as<int>() == 200 && diagnostics["curl_code"].as<int>() == 0);
assert(diagnostics["dns_ms"].as<double>() == 1 && diagnostics["connect_ms"].as<double>() == 2);
assert(diagnostics["tls_ms"].as<double>() == 3 && diagnostics["first_byte_ms"].as<double>() == 4 && diagnostics["total_ms"].as<double>() == 5);
utils::http::next_result->status = 500;
assert(send()["http_status"].as<int>() == 500 && notifications.front()[1].as<bool>());
utils::http::next_result->code = CURLE_OPERATION_TIMEDOUT;
assert(send()["curl_code"].as<int>() == 28 && !notifications.front()[1].as<bool>());
assert(notifications.front()[0].get_raw().type == game::SCRIPT_NONE && notifications.front()[2].is<std::string>());
utils::http::next_result = std::nullopt;
send();
assert(!notifications.front()[1].as<bool>());
utils::http::throw_from_transfer = true;
send();
assert(!notifications.front()[1].as<bool>());
utils::http::throw_from_transfer = false;
utils::http::next_result = successful;
utils::http::next_result->buffer = std::string(0x5001, 'x');
send();
assert(notifications.front()[0].as<std::string>().size() == 0x5000);
// A resumed waiter can enqueue enough work to reallocate the pending vector.
utils::http::next_result = successful;
notifications.clear();
http::create_request(params);
scheduler::thread_pool.run();
after_notify = [&]() { for (int i = 0; i < 100; ++i) http::create_request(params); };
http::check_requests();
after_notify = {};
assert(http::requests.size() == 100 && notifications.size() == 1);
scheduler::thread_pool.run();
http::check_requests();
assert(http::requests.empty() && notifications.size() == 101);
puts("\nPASS component: option validation, metadata, compatible argument order, single completion on failures/exceptions, reentrant queue");
}
+121
View File
@@ -0,0 +1,121 @@
// Minimal host interfaces for compiling the real component without the game VM.
#pragma once
#include "http.hpp"
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <functional>
#include <map>
#include <memory>
#include <stdexcept>
#include <thread>
#include <variant>
#include <vector>
using namespace std::chrono_literals;
namespace game { constexpr int SCRIPT_NONE = 0; }
namespace plugin { class plugin {}; }
class component_interface
{
public:
virtual ~component_interface() = default;
virtual void on_startup(plugin::plugin*) {}
virtual void on_shutdown(plugin::plugin*) {}
};
#define REGISTER_COMPONENT(...)
namespace scripting
{
class array;
class script_value;
using fields = std::map<std::string, script_value>;
class script_value
{
public:
std::variant<std::monostate, int, bool, double, std::string, std::shared_ptr<fields>> data;
script_value() = default;
script_value(int v) : data(v) {}
script_value(bool v) : data(v) {}
script_value(double v) : data(v) {}
script_value(const char* v) : data(std::string(v)) {}
script_value(std::string v) : data(std::move(v)) {}
script_value(const array& v);
template <typename T> bool is() const { return std::holds_alternative<T>(data); }
template <typename T> T as() const { return std::get<T>(data); }
struct raw { int type; };
raw get_raw() const { return {static_cast<int>(data.index())}; }
std::string to_string() const { return as<std::string>(); }
};
class array
{
public:
std::shared_ptr<fields> data = std::make_shared<fields>();
script_value& operator[](const script_value& key) const { return (*data)[key.as<std::string>()]; }
std::vector<script_value> get_keys() const
{
std::vector<script_value> keys;
for (const auto& [key, value] : *data) keys.emplace_back(key);
return keys;
}
};
inline script_value::script_value(const array& v) : data(v.data) {}
template <> inline bool script_value::is<array>() const { return std::holds_alternative<std::shared_ptr<fields>>(data); }
template <> inline array script_value::as<array>() const
{
array result;
result.data = std::get<std::shared_ptr<fields>>(data);
return result;
}
using variadic_args = std::vector<script_value>;
class object
{
inline static unsigned next_id = 1;
unsigned id = next_id++;
public:
unsigned get_entity_id() const { return id; }
};
inline std::vector<std::vector<script_value>> notifications;
inline std::function<void()> after_notify;
inline void notify(unsigned, const std::string& event, const std::vector<script_value>& args)
{
assert(event == "done");
notifications.push_back(args);
if (after_notify) after_notify();
}
inline void on_shutdown(const std::function<void()>&) {}
}
namespace scheduler
{
struct pool
{
std::vector<std::function<void()>> tasks;
void push(std::function<void()> f) { tasks.push_back(std::move(f)); }
void run()
{
auto work = std::move(tasks);
tasks.clear();
for (auto& task : work) task();
}
};
inline pool thread_pool;
constexpr int server = 0;
inline void loop(const std::function<void()>&, int) {}
}
namespace gsc::function
{
template <typename F> void add(const char*, F) {}
template <typename F, typename... Names> void add_multiple(F, Names...) {}
}
namespace utils::http
{
inline bool throw_from_transfer = false;
inline std::optional<result> next_result = result{};
inline timeouts observed_limits;
inline std::optional<result> get_data(const std::string&, const std::string&, const headers&, const std::string&, const timeouts& limits)
{
observed_limits = limits;
if (throw_from_transfer) throw std::runtime_error("simulated worker error");
return next_result;
}
}
+26
View File
@@ -0,0 +1,26 @@
#include "http.hpp"
#include <chrono>
#include <cstdlib>
#include <iostream>
int main(int argc, char** argv)
{
if (argc != 4 || curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK)
return 2;
utils::http::timeouts limits;
// A dash exercises the defaults compiled into the actual plugin source.
if (std::string(argv[2]) != "-") limits.connect_timeout_ms = std::stol(argv[2]);
if (std::string(argv[3]) != "-") limits.timeout_ms = std::stol(argv[3]);
const auto started = std::chrono::steady_clock::now();
const auto result = utils::http::get_data(argv[1], "{\"event\":\"heartbeat\"}",
{{"Content-Type", "application/json"}, {"Authorization", "Bearer test-only"}}, "POST", limits);
const auto wall_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
if (!result) return 3;
std::cout << "{\"curl_code\":" << result->code << ",\"http_status\":" << result->status
<< ",\"dns_ms\":" << result->dns_ms << ",\"connect_ms\":" << result->connect_ms
<< ",\"tls_ms\":" << result->tls_ms << ",\"first_byte_ms\":" << result->first_byte_ms
<< ",\"total_ms\":" << result->total_ms << ",\"wall_ms\":" << wall_ms
<< ",\"body_size\":" << result->buffer.size() << "}\n";
curl_global_cleanup();
}