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
@@ -0,0 +1,348 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "gsc.hpp"
#include "scripting.hpp"
#include "scheduler.hpp"
#include <utils/hook.hpp>
#include <utils/info_string.hpp>
#include <utils/string.hpp>
namespace chat
{
namespace
{
struct chat_command_t
{
scripting::function callback;
bool hide_message;
bool enabled;
bool sync;
};
struct chat_callback_t
{
scripting::function callback;
bool sync;
};
using userinfo_map = std::unordered_map<std::string, std::string>;
utils::hook::detour sv_get_user_info_hook;
utils::hook::detour client_connect_hook;
utils::hook::detour g_say_hook;
std::array<userinfo_map, 18> userinfo_overrides;
std::unordered_map<std::string, chat_command_t> chat_commands;
std::vector<chat_callback_t> chat_callbacks;
void sv_get_user_info_stub(int index, char* buffer, int buffer_size)
{
sv_get_user_info_hook.invoke<void>(index, buffer, buffer_size);
utils::info_string map(buffer);
for (const auto& [key, val] : userinfo_overrides[index])
{
if (val.empty())
{
map.remove(key);
}
else
{
map.set(key, val);
}
}
const auto userinfo = map.build();
strncpy_s(buffer, 1024, userinfo.data(), _TRUNCATE);
}
void clear_client_overrides(unsigned int client)
{
userinfo_overrides[client].clear();
}
bool handle_chat_command(game::gentity_s* ent, const std::string& text)
{
const auto trimmed = utils::string::trim(text);
const auto args = utils::string::split(trimmed, ' ');
if (args.size() <= 0)
{
return false;
}
const auto name = utils::string::to_lower(args[0]);
const auto iter = chat_commands.find(name);
if (iter == chat_commands.end())
{
return false;
}
if (!iter->second.enabled)
{
return false;
}
if (iter->second.sync)
{
const auto entity_id = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, ent->number, 0, 0);
const auto result = iter->second.callback(entity_id, {args});
if (result.is<bool>())
{
return result.as<bool>();
}
}
else
{
scheduler::once([=]
{
const auto entity_id = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, ent->number, 0, 0);
iter->second.callback(entity_id, {args});
}, scheduler::server);
}
return iter->second.hide_message;
}
bool handle_chat_callback(game::gentity_s* ent, const std::string& text, const int mode)
{
auto hidden = false;
for (const auto& chat_callback : chat_callbacks)
{
if (chat_callback.sync)
{
const auto entity_id = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, ent->number, 0, 0);
const auto result = chat_callback.callback(entity_id, {text, mode});
if (result.is<bool>())
{
hidden = hidden || result.as<bool>();
}
}
else
{
scheduler::once([=]
{
const auto entity_id = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, ent->number, 0, 0);
chat_callback.callback(entity_id, {text, mode});
}, scheduler::server);
}
}
return hidden;
}
void g_say_stub(game::gentity_s* ent, game::gentity_s* target, int mode, const char* text)
{
auto hidden = false;
hidden = hidden || handle_chat_command(ent, text);
hidden = hidden || handle_chat_callback(ent, text, mode);
if (!hidden)
{
g_say_hook.invoke<void>(ent, target, mode, text);
}
}
void client_clean_name(const char* in, char* out, int out_size)
{
utils::hook::invoke<void>(SELECT(0x424F00, 0x655010), out, in, out_size); // I_strncpyz
}
__declspec(naked) void client_clean_name_stub()
{
__asm
{
pushad
push [esp + 0x20 + 0x4] // outSize
push edx // out
push ecx // in
call client_clean_name
add esp, 0xC
popad
ret
}
}
game::scr_entref_t check_entity(const scripting::entity& entity)
{
const auto ent = entity.get_entity_reference();
if (ent.classnum != 0)
{
throw std::runtime_error("invalid entity");
}
if (game::g_entities[ent.entnum].client == nullptr)
{
throw std::runtime_error("not a player entity");
}
return ent;
}
template <bool value>
void enable_chat_command(const std::string& name)
{
const auto lower = utils::string::to_lower(name);
const auto iter = chat_commands.find(lower);
if (iter == chat_commands.end())
{
return;
}
iter->second.enabled = value;
}
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
const auto plutonium_g_say_stub = utils::hook::extract<std::size_t>(SELECT(0x6A7A40, 0x493DF0) + 1);
g_say_hook.create(plutonium_g_say_stub, g_say_stub);
sv_get_user_info_hook.create(SELECT(0x68BB90, 0x4C10F0), sv_get_user_info_stub);
plugin->get_interface()->callbacks()->on_player_connect(clear_client_overrides);
plugin->get_interface()->callbacks()->on_player_disconnect(clear_client_overrides);
utils::hook::call(SELECT(0x4ED764, 0x427E84), client_clean_name_stub);
utils::hook::call(SELECT(0x4ED79F, 0x427EBF), client_clean_name_stub);
scripting::on_shutdown([]()
{
for (auto& entry : userinfo_overrides)
{
entry.clear();
}
chat_callbacks.clear();
chat_commands.clear();
});
gsc::method::add("rename", [](const scripting::entity& entity, const std::string& name)
{
const auto ent = check_entity(entity);
userinfo_overrides[ent.entnum]["name"] = name;
game::ClientUserInfoChanged(ent.entnum);
});
gsc::method::add("setname", [](const scripting::entity& entity, const std::string& name)
{
const auto ent = check_entity(entity);
userinfo_overrides[ent.entnum]["name"] = name;
game::ClientUserInfoChanged(ent.entnum);
});
gsc::method::add("resetname", [](const scripting::entity& entity)
{
const auto ent = check_entity(entity);
userinfo_overrides[ent.entnum].erase("name");
game::ClientUserInfoChanged(ent.entnum);
});
gsc::method::add("resetclantag", [](const scripting::entity& entity)
{
const auto ent = check_entity(entity);
userinfo_overrides[ent.entnum].erase("clantag");
userinfo_overrides[ent.entnum].erase("clanAbbrev");
userinfo_overrides[ent.entnum].erase("clanAbbrevEV");
game::ClientUserInfoChanged(ent.entnum);
});
gsc::method::add("setclantag", [](const scripting::entity& entity, const std::string& name)
{
const auto ent = check_entity(entity);
userinfo_overrides[ent.entnum]["clantag"] = name;
userinfo_overrides[ent.entnum]["clanAbbrev"] = name;
userinfo_overrides[ent.entnum]["clanAbbrevEV"] = "1";
game::ClientUserInfoChanged(ent.entnum);
});
gsc::method::add("tell", [](const scripting::entity& entity, const std::string& msg)
{
const auto ent = check_entity(entity);
game::SV_GameSendServerCommand(ent.entnum, 0, utils::string::va("j \"%s\"", msg.data()));
});
gsc::function::add("say", [](const std::string& msg)
{
game::SV_GameSendServerCommand(-1, 0, utils::string::va("j \"%s\"", msg.data()));
});
gsc::function::add("sendservercommand", [](const int client, const std::string& cmd)
{
game::SV_GameSendServerCommand(client, 0, cmd.data());
});
gsc::method::add("sendservercommand", [](const scripting::entity& entity, const std::string& cmd)
{
const auto ent = check_entity(entity);
game::SV_GameSendServerCommand(ent.entnum, 0, cmd.data());
});
gsc::function::add_multiple([](const scripting::function& callback, const scripting::variadic_args& va)
{
chat_callback_t chat_callback{};
chat_callback.sync = false;
chat_callback.callback = callback;
if (va.size() >= 1)
{
chat_callback.sync = va[0].as<bool>();
}
chat_callbacks.emplace_back(chat_callback);
}, "onplayersay", "chat::register_callback");
gsc::function::add("chat::register_command", [](const scripting::function_argument& names_or_name,
const scripting::function& callback, const scripting::variadic_args& va)
{
chat_command_t command{};
command.hide_message = true;
command.sync = false;
command.callback = callback;
command.enabled = true;
if (va.size() >= 1)
{
command.hide_message = va[0].as<bool>();
}
if (va.size() >= 2)
{
command.sync = va[1].as<bool>();
}
if (names_or_name.is<scripting::array>())
{
const std::vector<std::string> names = names_or_name;
for (const auto& name : names)
{
const auto lower = utils::string::to_lower(name);
chat_commands.insert(std::make_pair(lower, command));
}
}
else
{
const auto name = names_or_name.as<std::string>();
const auto lower = utils::string::to_lower(name);
chat_commands.insert(std::make_pair(lower, command));
}
});
gsc::function::add("chat::disable_command", enable_chat_command<false>);
gsc::function::add("chat::enable_command", enable_chat_command<true>);
}
};
}
REGISTER_COMPONENT(chat::component)
@@ -0,0 +1,120 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "gsc.hpp"
namespace clients
{
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
gsc::function::add("dropallbots", []
{
const auto* dvar = game::Dvar_FindVar("com_maxclients");
const auto client_count = game::Dvar_GetInt(dvar);
for (auto i = 0; i < client_count; ++i)
{
if (game::SV_IsTestClient(i))
{
game::SV_GameDropClient(i, "GAME_DROPPEDFORINACTIVITY");
}
}
});
gsc::method::add("noclip", [](const scripting::entity& entity)
{
const auto entref = entity.get_entity_reference();
const auto* ent = game::GetPlayerEntity(entref);
if (ent->client->flags & game::gclientFlag::NOCLIP)
{
ent->client->flags &= ~game::gclientFlag::NOCLIP;
}
else
{
ent->client->flags |= game::gclientFlag::NOCLIP;
}
game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F,
(ent->client->flags & game::gclientFlag::NOCLIP) ? "GAME_NOCLIPON" : "GAME_NOCLIPOFF"));
});
gsc::method::add("ufo", [](const scripting::entity& entity)
{
const auto entref = entity.get_entity_reference();
const auto* ent = game::GetPlayerEntity(entref);
if (ent->client->flags & game::gclientFlag::UFO)
{
ent->client->flags &= ~game::gclientFlag::UFO;
}
else
{
ent->client->flags |= game::gclientFlag::UFO;
}
game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F,
(ent->client->flags & game::gclientFlag::UFO) ? "GAME_UFOON" : "GAME_UFOOFF"));
});
gsc::method::add("god", [](const scripting::entity& entity)
{
const auto entref = entity.get_entity_reference();
auto* ent = game::GetEntity(entref);
if (ent->flags & game::entityFlag::FL_GODMODE)
{
ent->flags &= ~game::entityFlag::FL_GODMODE;
}
else
{
ent->flags |= game::entityFlag::FL_GODMODE;
}
game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F,
(ent->flags & game::entityFlag::FL_GODMODE) ? "GAME_GODMODE_ON" : "GAME_GODMODE_OFF"));
});
gsc::method::add("demigod", [](const scripting::entity& entity)
{
const auto entref = entity.get_entity_reference();
auto* ent = game::GetEntity(entref);
if (ent->flags & game::entityFlag::FL_DEMI_GODMODE)
{
ent->flags &= ~game::entityFlag::FL_DEMI_GODMODE;
}
else
{
ent->flags |= game::entityFlag::FL_DEMI_GODMODE;
}
game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F,
(ent->flags & game::entityFlag::FL_DEMI_GODMODE) ? "GAME_DEMI_GODMODE_ON" : "GAME_DEMI_GODMODE_OFF"));
});
gsc::method::add("notarget", [](const scripting::entity& entity)
{
const auto entref = entity.get_entity_reference();
auto* ent = game::GetEntity(entref);
if (ent->flags & game::entityFlag::FL_NOTARGET)
{
ent->flags &= ~game::entityFlag::FL_NOTARGET;
}
else
{
ent->flags |= game::entityFlag::FL_NOTARGET;
}
game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F,
(ent->flags & game::entityFlag::FL_NOTARGET) ? "GAME_NOTARGETON" : "GAME_NOTARGETOFF"));
});
}
};
}
REGISTER_COMPONENT(clients::component)
@@ -0,0 +1,334 @@
#include "stdinc.hpp"
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "command.hpp"
#include "gsc.hpp"
#include "scheduler.hpp"
#include "scripting.hpp"
#include <utils/hook.hpp>
#include <utils/string.hpp>
#include <utils/memory.hpp>
namespace command
{
std::unordered_map<std::string, std::function<void(params&)>> handlers;
std::unordered_map<std::string, std::function<void(int, params_sv&)>> handlers_sv;
std::vector<std::string> script_commands;
std::vector<std::string> script_sv_commands;
utils::memory::allocator allocator;
utils::hook::detour client_command_hook;
game::CmdArgs* get_cmd_args()
{
return static_cast<game::CmdArgs*>(game::Sys_GetValue(4));
}
void main_handler()
{
params params = {};
const auto command = utils::string::to_lower(params[0]);
if (handlers.find(command) != handlers.end())
{
handlers[command](params);
}
}
params::params()
: nesting_(get_cmd_args()->nesting)
{
}
int params::size() const
{
const auto cmd_args = get_cmd_args();
return cmd_args->argc[this->nesting_];
}
const char* params::get(int index) const
{
if (index >= this->size())
{
return "";
}
const auto cmd_args = get_cmd_args();
return cmd_args->argv[this->nesting_][index];
}
std::string params::join(int index) const
{
std::string result = {};
for (int i = index; i < this->size(); i++)
{
if (i > index)
{
result.append(" ");
}
result.append(this->get(i));
}
return result;
}
params_sv::params_sv()
: nesting_(game::sv_cmd_args->nesting)
{
}
int params_sv::size() const
{
return game::sv_cmd_args->argc[this->nesting_];
}
const char* params_sv::get(const int index) const
{
if (index >= this->size())
{
return "";
}
return game::sv_cmd_args->argv[this->nesting_][index];
}
std::string params_sv::join(const int index) const
{
std::string result = {};
for (auto i = index; i < this->size(); i++)
{
if (i > index)
{
result.append(" ");
}
result.append(this->get(i));
}
return result;
}
std::vector<std::string> params_sv::get_all() const
{
std::vector<std::string> params;
for (auto i = 0; i < this->size(); i++)
{
params.emplace_back(this->get(i));
}
return params;
}
void add_raw(const char* name, void (*callback)())
{
game::Cmd_AddCommandInternal(name, callback, utils::memory::get_allocator()->allocate<game::cmd_function_t>());
}
void add(const char* name, const std::function<void(params&)>& callback)
{
const auto command = utils::string::to_lower(name);
if (handlers.find(command) == handlers.end())
{
add_raw(name, main_handler);
}
handlers[command] = callback;
}
void add_sv(const std::string& name, const std::function<void(int, const params_sv&)>& callback)
{
const auto command = utils::string::to_lower(name);
if (handlers_sv.find(command) == handlers_sv.end())
{
handlers_sv[command] = callback;
}
}
void add_script_command(const std::string& name, const std::function<void(const params&)>& callback)
{
script_commands.push_back(name);
add(allocator.duplicate_string(name), callback);
}
void add_script_sv_command(const std::string& name, const std::function<void(int, const params_sv&)>& callback)
{
script_sv_commands.push_back(name);
add_sv(name, callback);
}
void clear_script_commands()
{
for (const auto& name : script_commands)
{
handlers.erase(name);
game::Cmd_RemoveCommand(name.data());
}
for (const auto& name : script_sv_commands)
{
handlers_sv.erase(name);
}
allocator.clear();
script_commands.clear();
script_sv_commands.clear();
}
void execute(std::string command, const bool sync)
{
command += "\n";
if (sync)
{
game::Cmd_ExecuteSingleCommand(0, 0, command.data());
}
else
{
game::Cbuf_AddText(0, command.data());
}
}
void client_command_stub(const int client_num)
{
params_sv params = {};
const auto command = utils::string::to_lower(params[0]);
if (handlers_sv.find(command) != handlers_sv.end())
{
handlers_sv[command](client_num, params);
return;
}
client_command_hook.invoke<void>(client_num);
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
scripting::on_shutdown(clear_script_commands);
client_command_hook.create(SELECT(0x47E590, 0x4490C0), client_command_stub);
add("notifylevel", [](const params& params)
{
if (params.size() < 2)
{
printf("Usage: notifylevel <name> [...]\n");
return;
}
if (!*game::levelEntityId)
{
printf("Level not loaded\n");
return;
}
const auto argc = params.size();
for (auto i = argc - 1; i > 1; i--)
{
const auto arg = params[i];
scripting::push_value(arg);
}
const auto name = game::SL_GetString(params[1], 0);
game::Scr_NotifyId(game::SCRIPTINSTANCE_SERVER, 0, *game::levelEntityId, name, argc - 2);
});
add("notifynum", [](const params& params)
{
if (params.size() < 3)
{
printf("Usage: notifynum <num> <name> [...]\n");
return;
}
if (!*game::levelEntityId)
{
printf("Level not loaded\n");
return;
}
const auto client = atoi(params[1]);
const auto sv_maxclients = game::Dvar_FindVar("sv_maxclients")->current.integer;
if (client < 0 || client >= sv_maxclients)
{
return;
}
if (game::g_entities[client].client == nullptr)
{
return;
}
const auto argc = params.size();
for (auto i = argc - 1; i > 2; i--)
{
const auto arg = params[i];
scripting::push_value(arg);
}
const auto name = game::SL_GetString(params[2], 0);
game::Scr_NotifyNum(client, 0, name, argc - 3);
});
gsc::function::add_multiple([](const std::string& cmd)
{
execute(cmd);
}, "executecommand", "command::execute");
gsc::function::add_multiple([](const std::string& name, const scripting::function& function)
{
add_script_command(name, [function](const params& params)
{
scripting::array array;
for (auto i = 0; i < params.size(); i++)
{
array.emplace_back(params[i]);
}
function({array.get_raw()});
});
}, "addcommand", "command::add");
gsc::function::add_multiple([](const std::string& name, const scripting::function& function)
{
add_script_sv_command(name, [function](const int client_num, const params_sv& params)
{
const auto params_ = params.get_all();
scheduler::once([=]()
{
const scripting::entity player = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, client_num, 0, 0);
scripting::array array;
for (auto i = 0; i < params.size(); i++)
{
array.emplace_back(params[i]);
}
function(player, {array});
}, scheduler::pipeline::server);
});
}, "addclientcommand", "command::add_sv");
}
};
}
REGISTER_COMPONENT(command::component)
@@ -0,0 +1,51 @@
#pragma once
namespace command
{
class params
{
public:
params();
[[nodiscard]] int size() const;
[[nodiscard]] const char* get(int index) const;
[[nodiscard]] std::string join(int index) const;
[[nodiscard]] const char* operator[](const int index) const
{
return this->get(index);
}
private:
int nesting_;
};
class params_sv
{
public:
params_sv();
[[nodiscard]] int size() const;
[[nodiscard]] const char* get(int index) const;
[[nodiscard]] std::string join(int index) const;
[[nodiscard]] std::vector<std::string> get_all() const;
[[nodiscard]] const char* operator[](const int index) const
{
return this->get(index);
}
private:
int nesting_;
};
void add_raw(const char* name, void (*callback)());
void add(const char* name, const std::function<void(params&)>& callback);
void add_sv(const std::string& name, const std::function<void(int, const params_sv&)>& callback);
void add_script_command(const std::string& name, const std::function<void(const params&)>& callback);
void clear_script_commands();
void execute(std::string command, bool sync = false);
}
@@ -0,0 +1,151 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "gsc.hpp"
#include "scripting.hpp"
#include <utils/hook.hpp>
namespace config_strings
{
namespace
{
std::unordered_set<std::uint32_t> reserved_config_strings;
std::unordered_map<game::game_hudelem_s*, std::uint32_t> hudelem_config_strings;
bool is_string_reserved(const std::uint32_t offset)
{
return reserved_config_strings.contains(offset);
}
std::uint32_t get_config_string()
{
auto index = -1;
const auto scr_const_ = *reinterpret_cast<short*>(SELECT(0x24E5588, 0x24B7188));
for (auto i = 1; i < 512; i++)
{
const auto string_value = game::sv_configstrings[i + 488];
if (string_value == scr_const_ && !is_string_reserved(i))
{
index = i;
break;
}
}
if (index == -1)
{
throw std::runtime_error("out of config strings");
}
reserved_config_strings.insert(index);
return index;
}
utils::hook::detour scr_free_hud_elem_hook;
void scr_free_hud_elem_stub(game::game_hudelem_s* hud)
{
const auto _0 = gsl::finally([&]
{
scr_free_hud_elem_hook.invoke<void>(hud);
});
const auto iter = hudelem_config_strings.find(hud);
if (iter == hudelem_config_strings.end())
{
return;
}
game::SV_SetConfigString(iter->second + 488, 0);
reserved_config_strings.erase(iter->second);
hudelem_config_strings.erase(iter);
}
void g_localized_string_index_stub_1(utils::hook::assembler& a)
{
const auto not_reserved = a.newLabel();
a.add(esp, 4);
a.pushad();
a.push(esi);
a.call(is_string_reserved);
a.cmp(al, 0);
a.jz(not_reserved);
a.pop(esi);
a.popad();
a.jmp(SELECT(0x6D657E, 0x467D6E));
a.bind(not_reserved);
a.pop(esi);
a.popad();
a.cmp(eax, ecx);
a.jmp(SELECT(0x6D6578, 0x467D68));
}
void g_localized_string_index_stub_2(utils::hook::assembler& a)
{
const auto not_reserved = a.newLabel();
a.add(esp, 4);
a.pushad();
a.push(esi);
a.call(is_string_reserved);
a.cmp(al, 0);
a.jz(not_reserved);
a.pop(esi);
a.popad();
a.jmp(SELECT(0x6D6638, 0x467E2E));
a.bind(not_reserved);
a.pop(esi);
a.popad();
a.cmp(eax, ecx);
a.jmp(SELECT(0x6D663E, 0x467E28));
}
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
utils::hook::jump(SELECT(0x6D6573, 0x467D63), utils::hook::assemble(g_localized_string_index_stub_1));
utils::hook::jump(SELECT(0x6D6633, 0x467E23), utils::hook::assemble(g_localized_string_index_stub_2));
scr_free_hud_elem_hook.create(SELECT(0x56E800, 0x6186A0), scr_free_hud_elem_stub);
scripting::on_shutdown([&]
{
reserved_config_strings.clear();
hudelem_config_strings.clear();
});
gsc::method::add("hudelem::set_text", [](const scripting::entity& entity, const std::string& text)
{
const auto ent = entity.get_entity_reference();
const auto hudelem = &game::g_hudelems[ent.entnum];
const auto iter = hudelem_config_strings.find(hudelem);
if (iter == hudelem_config_strings.end())
{
const auto index = get_config_string();
hudelem->elem.text = static_cast<short>(index);
hudelem_config_strings.insert(std::make_pair(hudelem, index));
}
else
{
hudelem->elem.text = static_cast<short>(iter->second);
}
game::SV_SetConfigString(hudelem->elem.text + 488, text.data());
});
}
};
}
REGISTER_COMPONENT(config_strings::component)
@@ -0,0 +1,135 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "gsc.hpp"
#include "io.hpp"
#include "scheduler.hpp"
#include "scripting.hpp"
#include <utils/hook.hpp>
#include <utils/io.hpp>
#include <utils/cryptography.hpp>
#include <utils/flags.hpp>
namespace cryptography
{
namespace
{
std::vector<utils::cryptography::ecc::key> keys;
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
scripting::on_shutdown([]
{
keys.clear();
});
gsc::function::add("ecc::generate_key", [](int bits)
{
const auto key = utils::cryptography::ecc::generate_key(bits);
keys.emplace_back(key);
return keys.size() - 1;
});
gsc::function::add("ecc::create_key_from_public_key", [](const std::string& data)
{
utils::cryptography::ecc::key key;
key.set(utils::string::hex_to_bin(data));
keys.emplace_back(key);
return keys.size() - 1;
});
gsc::function::add("ecc::encrypt", [](const std::uint32_t key_index, const std::string& data)
{
if (key_index < 0 || key_index >= keys.size())
{
throw std::runtime_error("invalid key");
}
const auto& key = keys.at(key_index);
std::string encrypted = data;
if (!utils::cryptography::ecc::encrypt(key, encrypted))
{
throw std::runtime_error("failed to encrypt");
}
return utils::string::bin_to_hex(encrypted);
});
gsc::function::add("ecc::decrypt", [](const std::uint32_t key_index, const std::string& data)
{
if (key_index < 0 || key_index >= keys.size())
{
throw std::runtime_error("invalid key");
}
const auto& key = keys.at(key_index);
std::string decrypted = utils::string::hex_to_bin(data);
if (!utils::cryptography::ecc::decrypt(key, decrypted))
{
throw std::runtime_error("failed to decrypt");
}
return decrypted;
});
gsc::function::add("ecc::import_key", [](const std::string& data)
{
utils::cryptography::ecc::key key;
key.deserialize(utils::string::hex_to_bin(data));
keys.emplace_back(key);
return keys.size() - 1;
});
gsc::function::add("ecc::export_public_key", [](const std::uint32_t key_index)
{
if (key_index < 0 || key_index >= keys.size())
{
throw std::runtime_error("invalid key");
}
const auto& key = keys.at(key_index);
return utils::string::bin_to_hex(key.serialize(PK_PUBLIC));
});
gsc::function::add("ecc::export_private_key", [](const std::uint32_t key_index)
{
if (key_index < 0 || key_index >= keys.size())
{
throw std::runtime_error("invalid key");
}
const auto& key = keys.at(key_index);
return utils::string::bin_to_hex(key.serialize(PK_PRIVATE));
});
gsc::function::add("aes::encrypt", [](const std::string& data, const std::string& iv, const std::string& key)
{
return utils::string::bin_to_hex(utils::cryptography::aes::encrypt(data,
utils::string::hex_to_bin(iv),
utils::string::hex_to_bin(key))
);
});
gsc::function::add("aes::decrypt", [](const std::string& data, const std::string& iv, const std::string& key)
{
return utils::cryptography::aes::decrypt(utils::string::hex_to_bin(data),
utils::string::hex_to_bin(iv),
utils::string::hex_to_bin(key));
});
gsc::function::add("cryptography::random_bytes", [](const int len)
{
return utils::string::bin_to_hex(utils::cryptography::random::get_data(len));
});
}
};
}
REGISTER_COMPONENT(cryptography::component)
@@ -0,0 +1,145 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "scheduler.hpp"
#include "gsc.hpp"
#include "json.hpp"
#include "scripting.hpp"
#include "game/scripting/execution.hpp"
#include <utils/hook.hpp>
#include <utils/io.hpp>
#include <utils/string.hpp>
#include <utils/thread.hpp>
#include <utils/compression.hpp>
#include <exception/minidump.hpp>
namespace exception
{
namespace
{
thread_local struct
{
DWORD code = 0;
PVOID address = nullptr;
} exception_data;
void show_mouse_cursor()
{
while (ShowCursor(TRUE) < 0);
}
void display_error_dialog()
{
std::string error_str = utils::string::va("Fatal error (0x%08X) at 0x%p.\n"
"A minidump has been written.\n\n",
exception_data.code, exception_data.address);
error_str += "Make sure to update your graphics card drivers and install operating system updates!";
utils::thread::suspend_other_threads();
show_mouse_cursor();
MessageBoxA(nullptr, error_str.data(), "Plutonium T6 ERROR", MB_ICONERROR);
TerminateProcess(GetCurrentProcess(), exception_data.code);
}
void reset_state()
{
display_error_dialog();
}
size_t get_reset_state_stub()
{
static auto* stub = utils::hook::assemble([](utils::hook::assembler& a)
{
a.sub(esp, 0x10);
a.or_(esp, 0x8);
a.jmp(reset_state);
});
return reinterpret_cast<size_t>(stub);
}
std::string generate_crash_info(const LPEXCEPTION_POINTERS exceptioninfo)
{
std::string info{};
const auto line = [&info](const std::string& text)
{
info.append(text);
info.append("\r\n");
};
line("Plutonium T6 Crash Dump");
line("");
line("Timestamp: "s + utils::string::get_timestamp());
line(utils::string::va("Exception: 0x%08X", exceptioninfo->ExceptionRecord->ExceptionCode));
line(utils::string::va("Address: 0x%lX", exceptioninfo->ExceptionRecord->ExceptionAddress));
#pragma warning(push)
#pragma warning(disable: 4996)
OSVERSIONINFOEXA version_info;
ZeroMemory(&version_info, sizeof(version_info));
version_info.dwOSVersionInfoSize = sizeof(version_info);
GetVersionExA(reinterpret_cast<LPOSVERSIONINFOA>(&version_info));
#pragma warning(pop)
line(utils::string::va("OS Version: %u.%u", version_info.dwMajorVersion, version_info.dwMinorVersion));
return info;
}
void write_minidump(const LPEXCEPTION_POINTERS exceptioninfo)
{
const std::string crash_name = utils::string::va("minidumps/plutonium-t6-crash-%s.zip",
utils::string::get_timestamp().data());
utils::compression::zip::archive zip_file{};
zip_file.add("crash.dmp", create_minidump(exceptioninfo));
zip_file.add("info.txt", generate_crash_info(exceptioninfo));
zip_file.write(crash_name, "Plutonium T6 Crash Dump");
}
bool is_harmless_error(const LPEXCEPTION_POINTERS exceptioninfo)
{
const auto code = exceptioninfo->ExceptionRecord->ExceptionCode;
return code == STATUS_INTEGER_OVERFLOW || code == STATUS_FLOAT_OVERFLOW || code == STATUS_SINGLE_STEP;
}
LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS exceptioninfo)
{
if (is_harmless_error(exceptioninfo))
{
return EXCEPTION_CONTINUE_EXECUTION;
}
write_minidump(exceptioninfo);
exception_data.code = exceptioninfo->ExceptionRecord->ExceptionCode;
exception_data.address = exceptioninfo->ExceptionRecord->ExceptionAddress;
exceptioninfo->ContextRecord->Eip = get_reset_state_stub();
return EXCEPTION_CONTINUE_EXECUTION;
}
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI set_unhandled_exception_filter_stub(LPTOP_LEVEL_EXCEPTION_FILTER)
{
// Don't register anything here...
return &exception_filter;
}
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
SetUnhandledExceptionFilter(exception_filter);
utils::hook::jump(reinterpret_cast<uintptr_t>(&SetUnhandledExceptionFilter), set_unhandled_exception_filter_stub);
}
};
}
REGISTER_COMPONENT(exception::component)
@@ -0,0 +1,585 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/structs.hpp"
#include "game/game.hpp"
#include "gsc.hpp"
#include "json.hpp"
#include "scheduler.hpp"
#include "scripting.hpp"
#include "command.hpp"
#include <utils/hook.hpp>
#include <utils/string.hpp>
namespace gsc
{
std::unordered_map<std::string, function_t> functions;
std::unordered_map<std::string, function_t> methods;
std::unordered_map<std::string, void*> function_wraps;
std::unordered_map<std::string, void*> method_wraps;
namespace
{
utils::hook::detour player_get_method_hook;
utils::hook::detour scr_get_common_function_hook;
utils::hook::detour scr_error_internal_hook;
utils::hook::detour scr_post_load_scripts_hook;
auto field_offset_start = 0xA000;
std::unordered_map<unsigned int, entity_field_t> custom_fields[class_count];
std::string current_namespace;
std::unordered_map<std::string, scripting::script_value> world;
std::string get_full_name(const std::string& name)
{
if (current_namespace.empty())
{
return name;
}
return std::format("{}::{}", current_namespace, name);
}
void* find_function(const std::string& name)
{
const auto full_name = get_full_name(name);
const auto iter = function_wraps.find(full_name);
if (iter == function_wraps.end())
{
return nullptr;
}
return iter->second;
}
void* find_method(const std::string& name)
{
const auto full_name = get_full_name(name);
const auto iter = method_wraps.find(full_name);
if (iter == method_wraps.end())
{
return nullptr;
}
return iter->second;
}
builtin_function scr_get_common_function(const char** name, int* type, int* min_args, int* max_args)
{
const auto func = reinterpret_cast<builtin_function>(find_function(*name));
if (func == nullptr)
{
return scr_get_common_function_hook.invoke<builtin_function>(name, type, min_args, max_args);
}
*type = 1;
*min_args = 0;
*max_args = 512;
return func;
}
builtin_method player_get_method(const char** name, int* min_args, int* max_args)
{
const auto method = reinterpret_cast<builtin_method>(find_method(*name));
if (method == nullptr)
{
return player_get_method_hook.invoke<builtin_method>(name, min_args, max_args);
}
*min_args = 0;
*max_args = 512;
return method;
}
const gsc::entity_field_t* find_field(unsigned int classnum, unsigned int offset)
{
const auto& class_map = custom_fields[classnum];
const auto field_iter = class_map.find(offset);
if (field_iter == class_map.end())
{
return nullptr;
}
return &field_iter->second;
}
utils::hook::detour scr_get_object_field_hook;
void scr_get_object_field_stub(unsigned int classnum, int entnum, unsigned int offset)
{
const auto field = find_field(classnum, offset);
if (field == nullptr)
{
return scr_get_object_field_hook.invoke<void>(classnum, entnum, offset);
}
try
{
const auto result = field->getter(entnum);
return_value(result);
}
catch (const std::exception& e)
{
printf("******* script runtime error *******\n");
printf("while getting field \"%s\": %s\n", field->name.data(), e.what());
printf("************************************\n");
}
}
utils::hook::detour scr_set_object_field_hook;
int scr_set_object_field_stub(unsigned int classnum, int entnum, unsigned int offset)
{
const auto field = find_field(classnum, offset);
if (field == nullptr)
{
return scr_set_object_field_hook.invoke<int>(classnum, entnum, offset);
}
const auto args = get_arguments();
try
{
field->setter(entnum, args[0]);
}
catch (const std::exception& e)
{
printf("******* script runtime error *******\n");
printf("while setting field \"%s\": %s\n", field->name.data(), e.what());
printf("************************************\n");
}
return 1;
}
void scr_post_load_scripts_stub()
{
for (auto i = 0; i < class_id_t::class_count; i++)
{
const auto& class_map = custom_fields[i];
for (const auto& [offset, field] : class_map)
{
game::Scr_AddClassField(game::SCRIPTINSTANCE_SERVER, i, field.name.data(), offset);
}
}
return scr_post_load_scripts_hook.invoke<void>();
}
__declspec(naked) void scr_error_internal_stub_1_mp()
{
__asm
{
mov eax, 0
mov edx, eax
imul edx, 0x54
push 0x8F3F65
retn
}
}
__declspec(naked) void scr_error_internal_stub_1_zm()
{
__asm
{
mov eax, 0
mov edx, eax
imul edx, 0x54
push 0x8F2CC5
retn
}
}
void gsc_obj_resolve_stub_1(game::GSC_OBJ* obj, game::GSC_IMPORT_ITEM* item)
{
const auto name = &obj->magic[item->name];
current_namespace = &obj->magic[item->name_space];
if (current_namespace[0] == 0)
{
return;
}
const auto is_function = find_function(name);
const auto is_method = !is_function && find_method(name);
if (is_function || is_method)
{
item->name_space += static_cast<unsigned short>(current_namespace.size());
}
}
__declspec(naked) void gsc_obj_resolve_stub_mp()
{
__asm
{
pushad
push edi
push esi
call gsc_obj_resolve_stub_1
pop esi
pop edi
popad
movzx eax, byte ptr[edi + 7]
and eax, 0xF
dec eax
push 0x6CD7F1
ret
}
}
__declspec(naked) void gsc_obj_resolve_stub_zm()
{
__asm
{
pushad
push edi
push esi
call gsc_obj_resolve_stub_1
pop esi
pop edi
popad
movzx eax, byte ptr[edi + 7]
and eax, 0xF
dec eax
push 0x4F0441
ret
}
}
}
void call_function(const function_t& function, const std::string& name)
{
const auto args = get_arguments();
try
{
const auto value = function(args);
return_value(value);
}
catch (const std::exception& e)
{
printf("******* script runtime error *******\n");
printf("in call to builtin function \"%s\": %s\n", name.data(), e.what());
printf("************************************\n");
}
}
void call_method(const function_t& method, const std::string& name, const game::scr_entref_t entref)
{
const auto args = get_arguments();
try
{
const scripting::entity entity = game::Scr_GetEntityId(
game::SCRIPTINSTANCE_SERVER, entref.entnum, entref.classnum, 0);
std::vector<scripting::script_value> next_args{};
next_args.push_back(entity);
for (const auto& arg : args)
{
next_args.push_back(arg);
}
const auto value = method(next_args);
return_value(value);
}
catch (const std::exception& e)
{
printf("******* script runtime error *******\n");
printf("in call to builtin method \"%s\": %s\n", name.data(), e.what());
printf("************************************\n");
}
}
namespace field
{
void add(const class_id_t classnum, const std::string& name, const field_getter_t getter, const field_setter_t& setter)
{
const auto offset = field_offset_start++;
auto& class_map = custom_fields[classnum];
entity_field_t field{};
field.name = name;
field.getter = getter;
field.setter = setter;
class_map.insert(std::make_pair(offset, field));
}
}
void return_value(const scripting::script_value& value)
{
if (game::scr_VmPub->outparamcount)
{
game::Scr_ClearOutParams(game::SCRIPTINSTANCE_SERVER);
}
scripting::push_value(value);
}
std::vector<scripting::script_value> get_arguments()
{
std::vector<scripting::script_value> args;
for (auto i = 0; static_cast<unsigned int>(i) < game::scr_VmPub->outparamcount; i++)
{
const auto value = game::scr_VmPub->top[-i];
args.push_back(value);
}
return args;
}
std::string find_builtin_name(void* function)
{
for (auto i = scripting::function_map.begin(); i != scripting::function_map.end(); ++i)
{
if (i->second.actionFunc == function)
{
return i->second.actionString;
}
}
for (auto i = function_wraps.begin(); i != function_wraps.end(); ++i)
{
if (i->second == function)
{
return i->first;
}
}
return {};
}
std::string find_builtin_method_name(void* function)
{
for (auto i = scripting::method_map.begin(); i != scripting::method_map.end(); ++i)
{
if (i->second.actionFunc == function)
{
return i->second.actionString;
}
}
for (auto i = method_wraps.begin(); i != method_wraps.end(); ++i)
{
if (i->second == function)
{
return i->first;
}
}
return {};
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
utils::hook::jump(SELECT(0x6CD7E9, 0x4F0439), SELECT(gsc_obj_resolve_stub_mp, gsc_obj_resolve_stub_zm));
scr_get_common_function_hook.create(SELECT(0x4B57B0, 0x4AD040), scr_get_common_function);
player_get_method_hook.create(SELECT(0x432480, 0x6F2DB0), player_get_method);
scr_get_object_field_hook.create(SELECT(0x573160, 0x4224E0), scr_get_object_field_stub);
scr_set_object_field_hook.create(SELECT(0x5B9820, 0x43F2A0), scr_set_object_field_stub);
scr_post_load_scripts_hook.create(SELECT(0x6B75B0, 0x492440), scr_post_load_scripts_stub);
utils::hook::jump(SELECT(0x8F3F60, 0x8F2CC0), SELECT(scr_error_internal_stub_1_mp, scr_error_internal_stub_1_zm));
field::add(class_id_t::class_entity, "eflags",
[](unsigned int entnum) -> scripting::script_value
{
const auto entity = &game::g_entities[entnum];
return entity->flags;
},
[](unsigned int entnum, const scripting::script_value& value)
{
const auto entity = &game::g_entities[entnum];
entity->flags = value.as<int>();
}
);
field::add(class_id_t::class_entity, "eflags2",
[](unsigned int entnum) -> scripting::script_value
{
const auto entity = &game::g_entities[entnum];
return entity->eFlags2;
},
[](unsigned int entnum, const scripting::script_value& value)
{
const auto entity = &game::g_entities[entnum];
entity->eFlags2 = value.as<int>();
}
);
field::add(class_id_t::class_entity, "clientflags",
[](unsigned int entnum) -> scripting::script_value
{
const auto entity = &game::g_entities[entnum];
return entity->client->flags;
},
[](unsigned int entnum, const scripting::script_value& value)
{
const auto entity = &game::g_entities[entnum];
entity->client->flags = value.as<int>();
}
);
field::add(class_id_t::class_entity, "address",
[](unsigned int entnum) -> scripting::script_value
{
if (entnum >= 18)
{
throw std::runtime_error("not a player entity");
}
const auto clients = *game::svs_clients;
const auto client = &clients[entnum];
const auto address = client->header.netchan.remoteAddress.ip;
const std::string address_str = utils::string::va("%i.%i.%i.%i",
address[0],
address[1],
address[2],
address[3]
);
return address_str;
},
[](unsigned int, const scripting::script_value&)
{
throw std::runtime_error("cannot set address");
}
);
function::add("getfunctionargcount", [](const scripting::function& function)
{
const auto pos = function.get_pos();
if (*pos != 0x17) // OP_SafeCreateLocalVariables
{
return 0;
}
return static_cast<int>(pos[1]);
});
function::add("arrayremovekey", [](const scripting::array& array, const std::string& key)
{
array.erase(key);
});
function::add("xor", [](const int a, const int b)
{
return a ^ b;
});
function::add("not", [](const int a)
{
return ~a;
});
function::add("and", [](const int a, const int b)
{
return a & b;
});
function::add("or", [](const int a, const int b)
{
return a | b;
});
function::add("structget", [](const scripting::object& obj, const std::string& key)
{
return obj[key];
});
function::add("structset", [](const scripting::object& obj, const std::string& key,
const scripting::script_value& value)
{
obj[key] = value;
});
function::add("struct::get", [](const scripting::object& obj, const std::string& key)
{
return obj[key];
});
function::add("struct::set", [](const scripting::object& obj, const std::string& key,
const scripting::script_value& value)
{
obj[key] = value;
});
function::add("structremove", [](const scripting::object& obj, const std::string& key)
{
obj.erase(key);
});
function::add("getstructkeys", [](const scripting::object& obj)
{
return obj.get_keys();
});
function::add("isfunctionptr", [](const scripting::script_value& value)
{
return value.is<scripting::function>();
});
function::add("isentity", [](const scripting::script_value& value)
{
const auto& raw = value.get_raw();
const auto type = game::scr_VarGlob->objectVariableValue[raw.u.uintValue].w.type & 0x7F;
return raw.type == game::SCRIPT_OBJECT && type == game::SCRIPT_ENTITY;
});
function::add("isstruct", [](const scripting::script_value& value)
{
return value.is<scripting::object>();
});
function::add("typeof", [](const scripting::script_value& value)
{
return value.type_name();
});
method::add("get", [](const scripting::entity& entity, const std::string& field)
{
return entity.get(field);
});
method::add("set", [](const scripting::entity& entity, const std::string& field,
const scripting::script_value& value)
{
entity.set(field, value);
});
function::add("worldget", [](const std::string& key)
{
return world[key];
});
function::add("worldset", [](const std::string& key, const scripting::script_value& value)
{
world[key] = value;
});
}
};
}
REGISTER_COMPONENT(gsc::component)
@@ -0,0 +1,171 @@
#pragma once
#include "game/scripting/array.hpp"
#include "game/scripting/execution.hpp"
#include "../plugin.hpp"
#if _HAS_CXX20
namespace gsc
{
using function_t = std::function<scripting::script_value(const scripting::function_arguments& args)>;
using script_function = void(*)(game::scr_entref_t);
extern std::unordered_map<std::string, function_t> functions;
extern std::unordered_map<std::string, function_t> methods;
extern std::unordered_map<std::string, void*> function_wraps;
extern std::unordered_map<std::string, void*> method_wraps;
// auto = []{} forces template reevaluation
template <class... Args, std::size_t... I, auto = [] {} >
auto wrap_function(const std::function<void(Args...)>& f, std::index_sequence<I...>)
{
return [f]([[maybe_unused]] const scripting::function_arguments& args)
{
f(args[I]...);
return scripting::script_value{};
};
}
template <class... Args, std::size_t... I, auto = [] {} >
auto wrap_function(const std::function<scripting::script_value(Args...)>& f, std::index_sequence<I...>)
{
return [f]([[maybe_unused]] const scripting::function_arguments& args)
{
return f(args[I]...);
};
}
template <typename R, class... Args, std::size_t... I, auto = [] {} >
auto wrap_function(const std::function<R(Args...)>& f, std::index_sequence<I...>)
{
return [f]([[maybe_unused]] const scripting::function_arguments& args)
{
return scripting::script_value{f(args[I]...)};
};
}
template <typename R, class... Args, auto = [] {} >
auto wrap_function(const std::function<R(Args...)>& f)
{
return wrap_function(f, std::index_sequence_for<Args...>{});
}
template <class F, auto = [] {} >
auto wrap_function(F f)
{
return wrap_function(std::function(f));
}
enum class_id_t
{
class_entity,
class_hudelem,
class_pathnode,
class_node,
class_count
};
using field_getter_t = std::function<scripting::script_value(unsigned int entnum)>;
using field_setter_t = std::function<void(unsigned int entnum, const scripting::script_value&)>;
struct entity_field_t
{
std::string name;
field_getter_t getter;
field_setter_t setter;
};
void return_value(const scripting::script_value& value);
std::vector<scripting::script_value> get_arguments();
using builtin_function = void(*)();
using builtin_method = void(*)(game::scr_entref_t);
std::string find_builtin_name(void* function);
std::string find_builtin_method_name(void* function);
void call_function(const function_t& function, const std::string& name);
void call_method(const function_t& method, const std::string& name, const game::scr_entref_t entref);
namespace function
{
template <typename F, auto = [] {} >
void add_internal(const std::string& name, F function)
{
static auto called = false;
assert(!called);
called = true;
#ifdef DEBUG
printf("[function] %s\n", name.data());
#endif
static const auto lower = utils::string::to_lower(name);
static const auto [iterator, was_inserted] = functions.insert(std::make_pair(lower, function));
static const auto& function_ptr = iterator->second;
function_wraps[lower] = []()
{
call_function(function_ptr, lower);
};
}
template <typename F, auto = [] {} >
void add(const std::string& name, F f)
{
add_internal(name, wrap_function(f));
}
template <typename ...Args, typename F, auto = [] {} >
void add_multiple(F f, Args&& ...names)
{
(add(names, f), ...);
}
}
namespace method
{
template <typename F, auto = [] {} >
void add_internal(const std::string& name, F function)
{
static auto called = false;
assert(!called);
called = true;
#ifdef DEBUG
printf("[method] %s\n", name.data());
#endif
static const auto lower = utils::string::to_lower(name);
static const auto [iterator, was_inserted] = functions.insert(std::make_pair(lower, function));
static const auto& function_ptr = iterator->second;
method_wraps[lower] = [](game::scr_entref_t entref)
{
call_method(function_ptr, lower, entref);
};
}
template <typename F, auto = [] {} >
void add(const std::string& name, F f)
{
add_internal(name, wrap_function(f));
}
template <typename ...Args, typename F, auto = [] {} >
void add_multiple(F f, Args&& ...names)
{
(add(names, f), ...);
}
}
namespace field
{
void add(const class_id_t classnum, const std::string& name, const field_getter_t getter, const field_setter_t& setter);
}
}
#else
#error pre c++20 is not supported
#endif
@@ -0,0 +1,287 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/structs.hpp"
#include "game/game.hpp"
#include "gsc.hpp"
#include "scheduler.hpp"
#include "scripting.hpp"
#include <utils/http.hpp>
#include <utils/concurrency.hpp>
#include <curl/curl.h>
namespace http
{
namespace
{
constexpr const auto max_result_size = 0x5000u;
struct http_request_params_t
{
std::string method;
std::string url;
std::string fields;
utils::http::headers headers;
utils::http::timeouts limits;
};
struct http_request_t
{
http_request_params_t params;
scripting::object handle;
std::optional<utils::http::result> result;
std::chrono::steady_clock::time_point queued_at = std::chrono::steady_clock::now();
std::chrono::steady_clock::time_point started_at;
std::chrono::steady_clock::time_point finished_at;
std::atomic_bool completed{false};
};
std::vector<std::shared_ptr<http_request_t>> requests;
double elapsed_ms(const std::chrono::steady_clock::time_point start,
const std::chrono::steady_clock::time_point end)
{
return std::chrono::duration<double, std::milli>(end - start).count();
}
void notify_request_result(std::shared_ptr<http_request_t>& request)
{
const auto handle_id = request->handle.get_entity_id();
const utils::http::result missing;
const auto& result = request->result ? *request->result : missing;
scripting::array diagnostics;
diagnostics["http_status"] = static_cast<int>(result.status);
diagnostics["curl_code"] = static_cast<int>(result.code);
diagnostics["dns_ms"] = result.dns_ms;
diagnostics["connect_ms"] = result.connect_ms;
diagnostics["tls_ms"] = result.tls_ms;
diagnostics["first_byte_ms"] = result.first_byte_ms;
diagnostics["total_ms"] = result.total_ms;
diagnostics["worker_queue_ms"] = elapsed_ms(request->queued_at, request->started_at);
diagnostics["notify_ms"] = elapsed_ms(request->finished_at, std::chrono::steady_clock::now());
diagnostics["connect_timeout_ms"] = static_cast<int>(request->params.limits.connect_timeout_ms);
diagnostics["timeout_ms"] = static_cast<int>(request->params.limits.timeout_ms);
if (!request->result.has_value())
{
scripting::notify(handle_id, "done", {{}, false, "unknown error", diagnostics});
return;
}
const auto error = curl_easy_strerror(result.code);
if (result.code != CURLE_OK)
{
scripting::notify(handle_id, "done", {{}, false, error, diagnostics});
return;
}
auto& buffer = request->result->buffer;
if (buffer.size() >= max_result_size)
{
printf("^3WARNING: http result size bigger than %i bytes (%i), truncating!", max_result_size,
static_cast<int>(buffer.size()));
buffer.resize(max_result_size);
}
// Keep the original three values; older waiters ignore the fourth.
// HTTP 4xx/5xx are completed transfers, not transport failures.
scripting::notify(handle_id, "done", {buffer, true, {}, diagnostics});
}
void check_requests()
{
std::vector<std::shared_ptr<http_request_t>> ready;
for (auto i = requests.begin(); i != requests.end(); )
{
auto& request = *i;
if (!request->completed)
{
++i;
continue;
}
else
{
ready.emplace_back(request);
i = requests.erase(i);
}
}
// A resumed script may enqueue another request. Do not hold an iterator
// into requests while notifying the VM.
for (auto& request : ready)
notify_request_result(request);
}
scripting::object create_request(const http_request_params_t& params)
{
const auto request = std::make_shared<http_request_t>();
requests.emplace_back(request);
request->params = params;
scheduler::thread_pool.push([request]
{
request->started_at = std::chrono::steady_clock::now();
try
{
request->result = utils::http::get_data(
request->params.url,
request->params.fields,
request->params.headers,
request->params.method,
request->params.limits);
}
catch (...)
{
// Exceptions must also release GSC waiters through the done event.
request->result = std::nullopt;
}
request->finished_at = std::chrono::steady_clock::now();
request->completed = true;
});
return request->handle;
}
template <typename T>
void push_request(T&& r)
{
const auto request = std::make_shared<http_request_t>(std::forward<T>(r));
requests.emplace_back(request);
}
void wait_and_clear_requests()
{
for (auto& task : requests)
{
while (!task->completed)
{
std::this_thread::sleep_for(10ms);
}
}
requests.clear();
}
long timeout_option(const scripting::array& options, const char* name, const long fallback)
{
const auto value = options[name];
if (value.get_raw().type == game::SCRIPT_NONE)
return fallback;
if (!value.is<int>() || value.as<int>() <= 0)
throw std::runtime_error(std::string(name) + " must be a positive integer in milliseconds");
return value.as<int>();
}
void parse_request_options(http_request_params_t& params, const scripting::array& options)
{
params.limits.connect_timeout_ms = timeout_option(options, "connect_timeout_ms", params.limits.connect_timeout_ms);
params.limits.timeout_ms = timeout_option(options, "timeout_ms", params.limits.timeout_ms);
const auto fields = options["parameters"];
const auto body = options["body"];
const auto headers = options["headers"];
const auto method = options["method"];
if (method.is<std::string>())
{
params.method = method.as<std::string>();
}
if (fields.is<scripting::array>())
{
const auto fields_ = fields.as<scripting::array>();
const auto keys = fields_.get_keys();
for (const auto& key : keys)
{
if (!key.is<std::string>())
{
continue;
}
const auto key_ = key.as<std::string>();
const auto value = fields_[key].to_string();
params.fields += key_ + "=" + value + "&";
}
}
else if (body.is<std::string>())
{
params.fields = body.as<std::string>();
}
if (headers.is<scripting::array>())
{
const auto headers_arr = headers.as<scripting::array>();
const auto keys = headers_arr.get_keys();
for (const auto& key : keys)
{
if (!key.is<std::string>())
{
continue;
}
const auto key_str = key.as<std::string>();
const auto value = headers_arr[key].to_string();
params.headers[key_str] = value;
}
}
}
}
class component final : public component_interface
{
public:
void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override
{
scripting::on_shutdown(wait_and_clear_requests);
}
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
scheduler::loop(check_requests, scheduler::server);
scripting::on_shutdown(wait_and_clear_requests);
gsc::function::add_multiple([](const std::string& url)
{
http_request_params_t params{};
params.url = url;
return create_request(params);
}, "http::get", "httpget", "curl");
gsc::function::add("http::request", [](const std::string& url, const scripting::variadic_args& va)
{
http_request_params_t params{};
params.url = url;
if (va.size() > 0)
{
const auto options = va[0].as<scripting::array>();
parse_request_options(params, options);
}
return create_request(params);
});
gsc::function::add_multiple([](const std::string& url, const scripting::variadic_args& va)
{
http_request_params_t params{};
params.url = url;
params.method = "POST";
if (va.size() > 0)
{
const auto options = va[0].as<scripting::array>();
parse_request_options(params, options);
}
return create_request(params);
}, "httppost", "http::post");
}
};
}
REGISTER_COMPONENT(http::component)
@@ -0,0 +1,119 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "gsc.hpp"
#include <utils/io.hpp>
#include <utils/hook.hpp>
#define INT64_OPERATION(expr) \
[](const std::int64_t a, [[maybe_unused]] const std::int64_t b) \
{ \
return expr; \
} \
namespace int64
{
namespace
{
std::int64_t get_int64_arg(const scripting::variadic_args& args, const size_t index, bool optional)
{
if (optional && index >= static_cast<int>(args.size()))
{
return 0;
}
if (args[index].is<int>())
{
return static_cast<std::int64_t>(args[index].as<int>());
}
if (args[index].is<const char*>())
{
return std::strtoll(args[index].as<const char*>(), nullptr, 0);
}
throw std::runtime_error(std::format("parameter {} does not have type 'string' or 'int'", index));
}
std::unordered_map<std::string, std::function<std::int64_t(std::int64_t, std::int64_t)>> operations =
{
{"+", INT64_OPERATION(a + b)},
{"-", INT64_OPERATION(a - b)},
{"*", INT64_OPERATION(a * b)},
{"/", INT64_OPERATION(a / b)},
{"&", INT64_OPERATION(a & b)},
{"^", INT64_OPERATION(a ^ b)},
{"|", INT64_OPERATION(a | b)},
{"~", INT64_OPERATION(~a)},
{"%", INT64_OPERATION(a % b)},
{">>", INT64_OPERATION(a >> b)},
{"<<", INT64_OPERATION(a << b)},
{"++", INT64_OPERATION(a + 1)},
{"--", INT64_OPERATION(a - 1)},
};
std::unordered_map<std::string, std::function<bool(std::int64_t, std::int64_t)>> comparisons =
{
{">", INT64_OPERATION(a > b)},
{">=", INT64_OPERATION(a >= b)},
{"==", INT64_OPERATION(a == b)},
{"!=", INT64_OPERATION(a != b)},
{"<=", INT64_OPERATION(a <= b)},
{"<", INT64_OPERATION(a < b)},
};
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
gsc::function::add_multiple([](const scripting::variadic_args& args)
{
auto value = get_int64_arg(args, 0, false);
return value <= std::numeric_limits<std::int32_t>::max() && value >= std::numeric_limits<std::int32_t>::min();
}, "int64_is_int", "int64::is_int");
gsc::function::add_multiple([](const scripting::variadic_args& args)
{
return static_cast<std::int32_t>(get_int64_arg(args, 0, false));
}, "int64_to_int", "int64::to_int");
gsc::function::add_multiple([](const scripting::variadic_args& args) -> scripting::script_value
{
auto a = get_int64_arg(args, 0, false);
const auto op = args[1].as<std::string>();
auto b = get_int64_arg(args, 2, true);
if (const auto iter = operations.find(op); iter != operations.end())
{
return std::to_string(iter->second(a, b));
}
if (const auto iter = comparisons.find(op); iter != comparisons.end())
{
return iter->second(a, b);
}
throw std::runtime_error("invalid int64 operation");
}, "int64_op", "int64::op");
gsc::function::add_multiple([](const scripting::variadic_args& args)
{
const auto a = get_int64_arg(args, 0, false);
const auto b = get_int64_arg(args, 1, false);
return std::to_string(std::min(a, b));
}, "int64_min", "int64::min");
gsc::function::add_multiple([](const scripting::variadic_args& args)
{
const auto a = get_int64_arg(args, 0, false);
const auto b = get_int64_arg(args, 1, false);
return std::to_string(std::max(a, b));
}, "int64_max", "int64::max");
}
};
}
REGISTER_COMPONENT(int64::component)
@@ -0,0 +1,133 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "gsc.hpp"
#include "io.hpp"
#include "scheduler.hpp"
#include "scripting.hpp"
#include <utils/hook.hpp>
#include <utils/http.hpp>
#include <utils/io.hpp>
#include <utils/file_watcher.hpp>
#include <utils/flags.hpp>
namespace io
{
namespace
{
struct watched_file
{
std::unique_ptr<utils::io::file_watcher> watcher;
scripting::object handle;
};
std::vector<watched_file> watched_files;
void poll_files()
{
for (auto& file : watched_files)
{
std::string data;
if (file.watcher->run(1ms, &data, true))
{
scripting::notify(file.handle.get_entity_id(), "data", {data});
}
}
}
}
class component final : public component_interface
{
public:
void on_after_dvar_init([[maybe_unused]] plugin::plugin* plugin) override
{
const auto fs_homepath = game::Dvar_FindVar("fs_homepath");
if (fs_homepath == nullptr)
{
return;
}
std::filesystem::current_path(fs_homepath->current.string);
printf("working directory: %s", fs_homepath->current.string);
}
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
gsc::function::add("hashstring", [](const char* str)
{
return game::BG_StringHashValue(str);
});
gsc::function::add_multiple([](const std::string& file, const std::string& data,
const scripting::variadic_args& va)
{
auto append = false;
if (va.size() > 0)
{
append = va[0];
}
return utils::io::write_file(file, data, append);
}, "writefile", "io::write_file");
gsc::function::add_multiple([](const std::string& file, const std::string& data)
{
return utils::io::write_file(file, data, true);
}, "appendfile", "io::append_file");
gsc::function::add_multiple(utils::io::file_exists, "fileexists", "io::file_exists");
gsc::function::add_multiple(utils::io::move_file, "movefile", "io::move_file");
gsc::function::add_multiple(utils::io::file_size, "filesize", "io::file_size");
gsc::function::add_multiple(utils::io::create_directory, "createdirectory", "io::create_directory");
gsc::function::add_multiple(utils::io::directory_exists, "directoryexists", "io::directory_exists");
gsc::function::add_multiple(utils::io::directory_is_empty, "directoryisempty", "io::directory_is_empty");
gsc::function::add_multiple(utils::io::list_files, "listfiles", "io::list_files");
gsc::function::add_multiple(utils::io::remove_file, "removefile", "io::remove_file");
gsc::function::add_multiple([](const std::string& directory, const scripting::variadic_args& va)
{
auto recursive = false;
if (va.size() > 0)
{
recursive = va[0];
}
utils::io::remove_directory(directory, recursive);
}, "removedirectory", "io::remove_directory");
gsc::function::add_multiple(utils::io::copy_folder, "copyfolder", "io::copy_folder");
gsc::function::add_multiple(utils::io::copy_folder, "copydirectory", "io::copy_directory");
gsc::function::add_multiple(static_cast<std::string(*)(const std::string&)>(utils::io::read_file), "readfile", "io::read_file");
if (utils::flags::has_flag("experimental-utils"))
{
scheduler::loop(poll_files, scheduler::server_packet_loop);
scripting::on_shutdown([]()
{
watched_files.clear();
});
gsc::function::add("io::watch_file", [](const std::string& file)
{
scripting::object handle;
auto watcher = std::make_unique<utils::io::file_watcher>(file);
watcher->init();
watched_file watched_file{};
watched_file.watcher = std::move(watcher);
watched_file.handle = handle;
watched_files.emplace_back(std::move(watched_file));
return handle;
});
}
}
};
}
REGISTER_COMPONENT(io::component)
@@ -0,0 +1,6 @@
#include <stdinc.hpp>
namespace io
{
}
@@ -0,0 +1,319 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/scripting/event.hpp"
#include "game/scripting/execution.hpp"
#include "game/scripting/array.hpp"
#include "gsc.hpp"
#include "json.hpp"
#include "scripting.hpp"
#include <utils/io.hpp>
namespace json
{
namespace
{
constexpr const auto max_str_len = 0x5000u;
struct dump_context_t
{
std::unordered_set<std::uint32_t> visited_objects;
};
nlohmann::json gsc_to_json(const scripting::script_value& value, bool print_id, dump_context_t& ctx);
nlohmann::json array_to_json(const scripting::array& array, bool print_id, dump_context_t& ctx)
{
if (array.size() == 0)
{
return nlohmann::json::array();
}
nlohmann::json obj;
auto string_indexed = -1;
const auto keys = array.get_keys();
for (const auto& key : keys)
{
const auto is_int = key.is<int>();
const auto is_string = key.is<std::string>();
if (string_indexed == -1)
{
string_indexed = is_string;
}
if (!string_indexed && is_int)
{
const auto index = key.as<int>();
obj[index] = gsc_to_json(array[index], print_id, ctx);
}
else if (string_indexed && is_string)
{
const auto key_str = key.as<std::string>();
obj.emplace(key_str, gsc_to_json(array[key_str], print_id, ctx));
}
}
return obj;
}
nlohmann::json object_to_json(const scripting::object& object, bool print_id, dump_context_t& ctx)
{
const auto id = object.get_entity_id();
if (ctx.visited_objects.find(id) != ctx.visited_objects.end())
{
return utils::string::va("[struct reference %i]", id);
}
ctx.visited_objects.insert(id);
auto obj = nlohmann::json::object();
if (print_id)
{
obj["__id"] = id;
}
const auto keys = object.get_keys();
for (const auto& key : keys)
{
if (key == "__id")
{
continue;
}
obj.emplace(key, gsc_to_json(object[key], print_id, ctx));
}
return obj;
}
nlohmann::json vector_to_array(const float* value)
{
nlohmann::json obj;
obj.push_back(value[0]);
obj.push_back(value[1]);
obj.push_back(value[2]);
return obj;
}
nlohmann::json gsc_to_json(const scripting::script_value& value, bool print_id, dump_context_t& ctx)
{
const auto& variable = value.get_raw();
if (value.is<int>())
{
return value.as<int>();
}
if (value.is<float>())
{
return value.as<float>();
}
if (value.is<std::string>())
{
return value.as<std::string>();
}
if (value.is<scripting::vector>())
{
return vector_to_array(variable.u.vectorValue);
}
if (value.is<scripting::object>())
{
return object_to_json(variable.u.uintValue, print_id, ctx);
}
if (value.is<scripting::array>())
{
return array_to_json(variable.u.uintValue, print_id, ctx);
}
if (value.is<scripting::entity>())
{
return object_to_json(variable.u.uintValue, print_id, ctx);
}
if (value.is<scripting::function>())
{
const auto function = value.as<scripting::function>();
return utils::string::va("[[ %s ]]", function.get_name().data());
}
if (variable.type == game::SCRIPT_NONE)
{
return {};
}
if (variable.type == game::SCRIPT_CODEPOS)
{
const auto function = scripting::find_function(variable.u.codePosValue);
return utils::string::va("[codepos: %s]", function.data());
}
if (variable.type == game::SCRIPT_END)
{
const auto function = scripting::find_function(variable.u.codePosValue);
return utils::string::va("[precodepos: %s]", function.data());
}
return utils::string::va("[%s]", value.type_name().data());
}
scripting::script_value json_to_gsc(nlohmann::json obj)
{
const auto type = obj.type();
switch (type)
{
case (nlohmann::detail::value_t::boolean):
return obj.get<bool>();
case (nlohmann::detail::value_t::number_integer):
case (nlohmann::detail::value_t::number_unsigned):
return obj.get<int>();
case (nlohmann::detail::value_t::number_float):
return obj.get<float>();
case (nlohmann::detail::value_t::string):
return obj.get<std::string>();
case (nlohmann::detail::value_t::array):
{
scripting::array array;
for (const auto& [key, value] : obj.items())
{
array.emplace_back(json_to_gsc(value));
}
return array.get_raw();
}
case (nlohmann::detail::value_t::object):
{
scripting::array array;
for (const auto& [key, value] : obj.items())
{
array[key] = json_to_gsc(value);
}
return array.get_raw();
}
}
return {};
}
}
std::string gsc_to_string(const scripting::script_value& value)
{
dump_context_t ctx;
return gsc_to_json(value, false, ctx).dump();
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
gsc::function::add_multiple([](const scripting::variadic_args& args)
{
scripting::array array;
for (auto i = 0u; i < args.size(); i += 2)
{
if (i >= args.size() - 1)
{
continue;
}
const auto key = args[i].as<std::string>();
array[key] = args[i + 1];
}
return array;
}, "createmap", "json::create_map");
gsc::function::add_multiple([](const std::string& json)
{
const auto obj = nlohmann::json::parse(json);
return json_to_gsc(obj);
}, "jsonparse", "json::parse");
gsc::function::add_multiple([](const scripting::script_value& value, const scripting::variadic_args& args)
{
auto indent = -1;
auto print_id = false;
if (args.size() > 0)
{
indent = args[0].as<int>();
}
if (args.size() > 1)
{
print_id = args[1].as<bool>();
}
dump_context_t ctx;
const auto json = gsc_to_json(value, print_id, ctx);
auto dump = json.dump(indent);
if (dump.size() > max_str_len)
{
printf("^3WARNING: json dump longer than %i bytes, truncating!", max_str_len);
dump.resize(max_str_len);
}
return dump;
}, "jsonserialize", "json::serialize");
gsc::function::add_multiple([](const std::string& file, const scripting::script_value& value, const scripting::variadic_args& va)
{
std::string file_name = file;
auto indent = -1;
auto print_id = false;
if (!file_name.ends_with(".json"))
{
file_name.append(".json");
}
if (va.size() > 0)
{
indent = va[0].as<int>();
}
if (va.size() > 1)
{
print_id = va[1].as<bool>();
}
dump_context_t ctx;
const auto json = gsc_to_json(value, print_id, ctx);
const auto dump = json.dump(indent);
return utils::io::write_file(file_name, dump);
}, "jsondump", "json::dump");
gsc::function::add_multiple([](const scripting::variadic_args& args)
{
std::string buffer;
for (const auto& arg : args)
{
buffer.append(gsc_to_string(arg));
buffer.append("\t");
}
printf("%s\n", buffer.data());
}, "jsonprint", "json::print");
}
};
}
REGISTER_COMPONENT(json::component)
@@ -0,0 +1,6 @@
#pragma once
namespace json
{
std::string gsc_to_string(const scripting::script_value& value);
}
@@ -0,0 +1,49 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include <utils/hook.hpp>
#include <utils/string.hpp>
#include <utils/concurrency.hpp>
#include <utils/io.hpp>
#include <utils/flags.hpp>
namespace logfile
{
namespace
{
std::string filename;
std::mutex mutex;
bool is_logging_enabled()
{
static const auto is_enabled = utils::flags::has_flag("log");
return is_enabled;
}
}
void log_hook(const std::string& buffer)
{
if (!is_logging_enabled())
{
return;
}
std::lock_guard _(mutex);
utils::io::write_file(filename, buffer, true);
}
class component final : public component_interface
{
public:
void on_after_dvar_init([[maybe_unused]] plugin::plugin* plugin) override
{
utils::io::create_directory("logs");
filename = utils::string::va("logs/console-%s.log",
utils::string::get_timestamp().data());
}
};
}
REGISTER_COMPONENT(logfile::component)
@@ -0,0 +1,6 @@
#pragma once
namespace logfile
{
void log_hook(const std::string& buffer);
}
@@ -0,0 +1,545 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "mysql.hpp"
#include "component/gsc.hpp"
#include "component/scheduler.hpp"
#include "component/scripting.hpp"
#include <utils/string.hpp>
namespace mysql
{
namespace
{
struct mysql_result_t
{
MYSQL_RES* result;
MYSQL_STMT* stmt;
uint64_t affected_rows;
std::string error;
};
struct task_t
{
scripting::object handle;
mysql_result_t result{};
std::atomic_bool completed;
};
std::vector<std::shared_ptr<task_t>> tasks;
std::array<connection, max_connections> connection_pool;
scripting::script_value field_to_value(const MYSQL_FIELD* field, const std::string& row)
{
switch (field->type)
{
case enum_field_types::MYSQL_TYPE_INT24:
case enum_field_types::MYSQL_TYPE_LONG:
case enum_field_types::MYSQL_TYPE_SHORT:
return std::atoi(row.data());
case enum_field_types::MYSQL_TYPE_LONGLONG:
return row;
case enum_field_types::MYSQL_TYPE_FLOAT:
case enum_field_types::MYSQL_TYPE_DOUBLE:
return static_cast<float>(std::atof(row.data()));
}
return row;
}
scripting::script_value bind_to_value(const MYSQL_BIND* bind)
{
switch (bind->buffer_type)
{
case enum_field_types::MYSQL_TYPE_INT24:
case enum_field_types::MYSQL_TYPE_LONG:
case enum_field_types::MYSQL_TYPE_SHORT:
return *reinterpret_cast<int*>(bind->buffer);
case enum_field_types::MYSQL_TYPE_LONGLONG:
return std::to_string(*reinterpret_cast<std::int64_t*>(bind->buffer));
case enum_field_types::MYSQL_TYPE_FLOAT:
return *reinterpret_cast<float*>(bind->buffer);
case enum_field_types::MYSQL_TYPE_DOUBLE:
return static_cast<float>(*reinterpret_cast<double*>(bind->buffer));
case enum_field_types::MYSQL_TYPE_STRING:
return std::string{reinterpret_cast<char*>(bind->buffer), bind->buffer_length};
}
return {};
}
scripting::array generate_result(MYSQL_STMT* stmt)
{
if (stmt == nullptr)
{
return {};
}
utils::memory::allocator allocator;
scripting::array result_arr;
const auto meta = mysql_stmt_result_metadata(stmt);
if (meta == nullptr)
{
return {};
}
const auto column_count = mysql_num_fields(meta);
const auto fields = mysql_fetch_fields(meta);
const auto is_null = allocator.allocate_array<my_bool>(column_count);
const auto errors = allocator.allocate_array<my_bool>(column_count);
const auto real_lengths = allocator.allocate_array<unsigned long>(column_count);
const auto binds = allocator.allocate_array<MYSQL_BIND>(column_count);
for (auto i = 0u; i < column_count; i++)
{
binds[i].length = &real_lengths[i];
binds[i].is_null = &is_null[i];
binds[i].error = &errors[i];
}
if (mysql_stmt_bind_result(stmt, binds) != 0 ||
mysql_stmt_store_result(stmt) != 0)
{
return {};
}
while (mysql_stmt_fetch(stmt) != MYSQL_NO_DATA)
{
for (auto i = 0u; i < column_count; i++)
{
switch (fields[i].type)
{
case enum_field_types::MYSQL_TYPE_INT24:
case enum_field_types::MYSQL_TYPE_LONG:
case enum_field_types::MYSQL_TYPE_SHORT:
{
binds[i].buffer_type = MYSQL_TYPE_LONG;
binds[i].buffer = allocator.allocate<int>();
binds[i].buffer_length = sizeof(int);
break;
};
case enum_field_types::MYSQL_TYPE_LONGLONG:
{
binds[i].buffer_type = MYSQL_TYPE_LONGLONG;
binds[i].buffer = allocator.allocate<std::int64_t>();
binds[i].buffer_length = sizeof(std::int64_t);
break;
};
case enum_field_types::MYSQL_TYPE_FLOAT:
binds[i].buffer_type = MYSQL_TYPE_FLOAT;
binds[i].buffer = allocator.allocate<float>();
binds[i].buffer_length = sizeof(float);
break;
case enum_field_types::MYSQL_TYPE_DOUBLE:
{
binds[i].buffer_type = MYSQL_TYPE_DOUBLE;
binds[i].buffer = allocator.allocate<double>();
binds[i].buffer_length = sizeof(double);
break;
}
default:
binds[i].buffer_type = MYSQL_TYPE_STRING;
binds[i].buffer = allocator.allocate_array<char>(real_lengths[i]);
binds[i].buffer_length = real_lengths[i];
break;
}
mysql_stmt_fetch_column(stmt, &binds[i], i, 0);
}
scripting::array row_arr;
for (auto i = 0u; i < column_count; i++)
{
const auto field = &fields[i];
const std::string field_name = {field->name, field->name_length};
row_arr[field_name] = bind_to_value(&binds[i]);
}
result_arr.emplace_back(row_arr);
}
mysql_free_result(meta);
return result_arr;
}
scripting::array generate_result(MYSQL_RES* result)
{
if (result == nullptr)
{
return {};
}
scripting::array result_arr;
const auto num_rows = mysql_num_rows(result);
const auto num_fields = mysql_num_fields(result);
const auto fields = mysql_fetch_fields(result);
for (auto i = 0u; i < num_rows; i++)
{
scripting::array row_arr;
const auto row = mysql_fetch_row(result);
const auto lengths = mysql_fetch_lengths(result);
for (auto f = 0u; f < num_fields; f++)
{
const auto field = &fields[f];
const std::string field_str = {field->name, field->name_length};
const std::string row_str = {row[f], lengths[f]};
const auto value = field_to_value(field, row_str);
row_arr[field_str] = value;
}
result_arr.emplace_back(row_arr);
}
return result_arr;
}
template <typename F>
scripting::object create_mysql_query(F&& cb)
{
auto task = std::make_shared<task_t>();
tasks.emplace_back(task);
scheduler::thread_pool.push([=]
{
try
{
mysql::access([&](database_t& db)
{
task->result = cb(db);
task->completed = true;
});
}
catch (const std::exception& e)
{
printf("^1MySQL ERROR: %s\n", e.what());
task->completed = true;
}
});
return task->handle;
}
void wait_and_clear_tasks()
{
for (auto& task : tasks)
{
while (!task->completed)
{
std::this_thread::sleep_for(10ms);
}
}
tasks.clear();
}
void notify_task_result(std::shared_ptr<task_t>& task)
{
const auto result = task->result.result
? generate_result(task->result.result)
: generate_result(task->result.stmt);
const auto rows = static_cast<std::size_t>(task->result.affected_rows);
scripting::notify(task->handle.get_entity_id(), "done", {result, rows, task->result.error});
if (task->result.result != nullptr)
{
mysql_free_result(task->result.result);
task->result.result = nullptr;
}
if (task->result.stmt != nullptr)
{
mysql_stmt_close(task->result.stmt);
task->result.stmt = nullptr;
}
}
void check_tasks()
{
for (auto i = tasks.begin(); i != tasks.end(); )
{
auto& task = *i;
if (!task->completed)
{
++i;
continue;
}
else
{
notify_task_result(task);
i = tasks.erase(i);
}
}
}
template <typename T>
MYSQL_BIND* bind_statement_args(const T& args, std::size_t& bind_count)
{
bind_count = args.size();
const auto binds = utils::memory::allocate_array<MYSQL_BIND>(bind_count);
for (auto i = 0u; i < args.size(); i++)
{
const auto& arg = args[i];
const auto& raw_value = arg.get_raw();
switch (raw_value.type)
{
case game::SCRIPT_FLOAT:
{
binds[i].buffer = utils::memory::allocate<float>();
binds[i].buffer_type = MYSQL_TYPE_FLOAT;
*reinterpret_cast<float*>(binds[i].buffer) = raw_value.u.floatValue;
break;
}
case game::SCRIPT_INTEGER:
{
binds[i].buffer = utils::memory::allocate<int>();
binds[i].buffer_type = MYSQL_TYPE_LONG;
*reinterpret_cast<int*>(binds[i].buffer) = raw_value.u.intValue;
break;
}
case game::SCRIPT_STRING:
{
const auto str = arg.as<std::string>();
const auto str_copy = utils::memory::duplicate_string(str);
binds[i].buffer = str_copy;
binds[i].buffer_length = str.size();
binds[i].buffer_type = MYSQL_TYPE_STRING;
break;
}
default:
{
binds[i].buffer_type = MYSQL_TYPE_NULL;
break;
}
}
}
return binds;
}
void cleanup_connections()
{
for (auto& connection : connection_pool)
{
connection.cleanup();
}
}
void free_statement_binds(MYSQL_BIND* binds, const std::size_t bind_count)
{
if (binds == nullptr)
{
return;
}
for (auto i = 0u; i < bind_count; i++)
{
utils::memory::free(binds[i].buffer);
}
utils::memory::free(binds);
}
}
utils::concurrency::container<sql::connection_config>& get_config()
{
static utils::concurrency::container<sql::connection_config> config;
static auto initialized = false;
if (!initialized)
{
config.access([&](sql::connection_config& cfg)
{
cfg.user = "root";
cfg.password = "root";
cfg.host = "localhost";
cfg.port = 3306;
cfg.database = "default";
});
initialized = true;
}
return config;
}
void connection::check()
{
const auto now = std::chrono::high_resolution_clock::now();
const auto diff = now - this->start_;
if (this->db.get() == nullptr || !this->db->ping_server() || diff >= connection_timeout)
{
get_config().access([&](sql::connection_config& cfg)
{
this->db = std::make_unique<sql::connection>(cfg);
this->start_ = now;
});
}
this->last_access_ = now;
}
void connection::cleanup()
{
std::unique_lock<database_mutex_t> lock(this->mutex, std::try_to_lock);
if (!lock.owns_lock())
{
return;
}
const auto now = std::chrono::high_resolution_clock::now();
const auto diff = now - this->last_access_;
if (diff >= connection_timeout)
{
this->db.reset();
}
}
connection* get_connection(std::unique_lock<database_mutex_t>& lock)
{
static thread_local connection* last_connection{};
if (last_connection != nullptr)
{
lock = std::unique_lock(last_connection->mutex, std::try_to_lock);
if (lock.owns_lock())
{
return last_connection;
}
}
for (auto i = 0u; i < connection_pool.size(); i++)
{
auto connection = &connection_pool[i];
if (connection == last_connection)
{
continue;
}
lock = std::unique_lock(connection->mutex, std::try_to_lock);
if (!lock.owns_lock())
{
continue;
}
last_connection = connection;
return connection;
}
return nullptr;
}
class component final : public component_interface
{
public:
void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override
{
wait_and_clear_tasks();
}
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
scripting::on_shutdown(wait_and_clear_tasks);
scheduler::loop(cleanup_connections, scheduler::async, 60s);
scheduler::loop(check_tasks, scheduler::server);
gsc::function::add("mysql::set_config", [](const scripting::object& config)
{
get_config().access([&](sql::connection_config& cfg)
{
cfg.host = config["host"].as<std::string>();
cfg.user = config["user"].as<std::string>();
cfg.password = config["password"].as<std::string>();
cfg.port = config["port"].as<unsigned short>();
cfg.database = config["database"].as<std::string>();
});
});
gsc::function::add_multiple([](const std::string& query)
{
return create_mysql_query([=](database_t& db)
{
const auto handle = db->get_handle();
mysql_result_t result{};
if (mysql_query(handle, query.data()) != 0)
{
result.error = mysql_error(handle);
return result;
}
result.result = mysql_store_result(handle);
result.affected_rows = mysql_affected_rows(handle);
return result;
});
}, "mysql::query", "mysql::execute");
gsc::function::add("mysql::prepared_statement", [](const std::string& query, const scripting::variadic_args& values)
{
MYSQL_BIND* binds = nullptr;
auto bind_count = 0u;
try
{
if (values.size() > 0 && values[0].is<scripting::array>())
{
binds = bind_statement_args(values[0].as<scripting::array>(), bind_count);
}
else
{
binds = bind_statement_args(values, bind_count);
}
const auto handle = create_mysql_query([binds, bind_count, query](database_t& db)
{
const auto _0 = gsl::finally([=]
{
free_statement_binds(binds, bind_count);
});
mysql_result_t result{};
const auto handle = db->get_handle();
const auto stmt = mysql_stmt_init(handle);
if (mysql_stmt_prepare(stmt, query.data(), query.size()) != 0 ||
(binds != nullptr && mysql_stmt_bind_param(stmt, binds) != 0) ||
mysql_stmt_execute(stmt) != 0)
{
result.error = mysql_stmt_error(stmt);
return result;
}
result.stmt = stmt;
result.affected_rows = mysql_stmt_affected_rows(stmt);
return result;
});
return handle;
}
catch (const std::exception& e)
{
free_statement_binds(binds, bind_count);
throw e;
}
});
}
};
}
REGISTER_COMPONENT(mysql::component)
@@ -0,0 +1,58 @@
#pragma once
#include <utils/concurrency.hpp>
#pragma warning(push)
#pragma warning(disable: 4127)
#pragma warning(disable: 4267)
#pragma warning(disable: 4018)
#pragma warning(disable: 4996)
#pragma warning(disable: 4244)
#include <sqlpp11/sqlpp11.h>
#include <sqlpp11/mysql/mysql.h>
#pragma warning(pop)
namespace sql = sqlpp::mysql;
namespace mysql
{
constexpr auto max_connections = 100;
constexpr auto connection_timeout = 200s;
using database_mutex_t = std::recursive_mutex;
using database_t = std::unique_ptr<sql::connection>;
utils::concurrency::container<sql::connection_config>& get_config();
class connection
{
public:
connection() = default;
void check();
void cleanup();
database_t db;
database_mutex_t mutex;
private:
std::chrono::high_resolution_clock::time_point start_;
std::chrono::high_resolution_clock::time_point last_access_;
};
connection* get_connection(std::unique_lock<database_mutex_t>& lock);
template <typename T = void, typename F>
T access(F&& accessor)
{
std::unique_lock<database_mutex_t> lock;
auto conn = get_connection(lock);
if (conn == nullptr)
{
throw std::runtime_error("out of connections");
}
conn->check();
return accessor(conn->db);
}
}
@@ -0,0 +1,210 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "gsc.hpp"
#include "io.hpp"
#include "scheduler.hpp"
#include "scripting.hpp"
#include <utils/hook.hpp>
#include <utils/io.hpp>
#include <utils/flags.hpp>
namespace network
{
namespace
{
struct socket_t
{
SOCKET socket;
scripting::object handle;
};
std::vector<socket_t> connected_sockets;
std::unordered_map<std::uint32_t, SOCKET> handle_to_socket;
void poll_sockets()
{
for (const auto& inst : connected_sockets)
{
std::string data;
data.reserve(0x1000);
auto result = 0;
do
{
char buffer[0x1000]{};
result = recv(inst.socket, buffer, sizeof(buffer), 0);
if (result != -1 && result != 0)
{
data.append(buffer);
}
} while (result != -1 && result != 0);
if (data.size() == 0)
{
continue;
}
auto start_index = 0;
scripting::array chunks;
for (auto i = 0u; i < data.size(); i++)
{
const auto is_eof = data[i] == EOF;
const auto is_end = i == data.size() - 1;
if (is_eof || is_end)
{
const auto chunk = data.substr(start_index, i - start_index);
chunks.emplace_back(chunk);
start_index = i + 1;
}
}
scripting::notify(inst.handle.get_entity_id(), "data", {chunks.get_raw()});
}
}
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
if (!utils::flags::has_flag("experimental-utils"))
{
return;
}
scripting::on_shutdown([]
{
for (const auto& inst : connected_sockets)
{
closesocket(inst.socket);
}
connected_sockets.clear();
handle_to_socket.clear();
});
scheduler::loop(poll_sockets, scheduler::server_packet_loop);
gsc::function::add("network::connect", [](const std::string& type, const std::string& address, const int port)
-> scripting::script_value
{
static std::unordered_map<std::string, int> socket_types =
{
{"tcp", SOCK_STREAM},
{"udp", SOCK_DGRAM},
};
const auto type_lower = utils::string::to_lower(type);
const auto type_id = socket_types.find(type_lower);
if (type_id == socket_types.end())
{
throw std::runtime_error("invalid socket type");
}
const auto sock = socket(AF_INET, type_id->second, 0);
u_long mode = 1;
if (ioctlsocket(sock, FIONBIO, &mode) == SOCKET_ERROR)
{
closesocket(sock);
return {};
}
int flag = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&flag, sizeof(int));
SOCKADDR_IN sin{};
sin.sin_family = AF_INET;
sin.sin_port = htons(static_cast<std::uint16_t>(port));
inet_pton(AF_INET, address.data(), &sin.sin_addr);
if (connect(sock, reinterpret_cast<SOCKADDR*>(&sin), sizeof(sin)) == SOCKET_ERROR)
{
const auto error = WSAGetLastError();
if (error != WSAEWOULDBLOCK && error != WSAEINPROGRESS)
{
closesocket(sock);
return {};
}
}
scripting::object handle;
socket_t socket_ref{};
socket_ref.socket = sock;
socket_ref.handle = handle;
const auto handle_id = handle.get_entity_id();
handle_to_socket[handle_id] = sock;
connected_sockets.emplace_back(std::move(socket_ref));
return handle;
});
gsc::function::add("network::send", [](const scripting::object& handle, std::string data)
{
const auto handle_id = handle.get_entity_id();
const auto iter = handle_to_socket.find(handle_id);
if (iter == handle_to_socket.end())
{
return -1;
}
const auto sock = iter->second;
data += EOF;
return send(sock, data.data(), data.size(), 0);
});
gsc::function::add("network::close", [](const scripting::object& handle)
{
const auto handle_id = handle.get_entity_id();
const auto iter = handle_to_socket.find(handle_id);
if (iter == handle_to_socket.end())
{
return;
}
closesocket(iter->second);
handle_to_socket.erase(iter);
for (auto i = connected_sockets.begin(), end = connected_sockets.end(); i != end; ++i)
{
if (i->handle.get_entity_id() == handle_id)
{
connected_sockets.erase(i);
break;
}
}
});
gsc::function::add("network::is_connected", [](const scripting::object& handle)
{
const auto handle_id = handle.get_entity_id();
const auto iter = handle_to_socket.find(handle_id);
if (iter == handle_to_socket.end())
{
return (int)0;
}
char code{};
int size = sizeof(code);
getsockopt(iter->second, SOL_SOCKET, SO_ERROR, &code, &size);
return static_cast<int>(code);
});
gsc::function::add("network::get_last_error", []()
{
return WSAGetLastError();
});
}
};
}
REGISTER_COMPONENT(network::component)
@@ -0,0 +1,211 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/scripting/stack_isolation.hpp"
#include "gsc.hpp"
#include "scheduler.hpp"
#include "scripting.hpp"
#include <utils/concurrency.hpp>
#include <utils/hook.hpp>
#include <utils/io.hpp>
namespace notifies
{
namespace
{
struct notify_group
{
unsigned int owner_id;
unsigned int notify_target;
unsigned int main_notify;
std::unordered_set<unsigned int> sub_notifies;
};
using notify_groups_t = std::vector<notify_group>;
bool in_notify_queue;
notify_groups_t notify_groups_queue;
utils::hook::detour vm_notify_hook;
void push_sl_string(unsigned int value)
{
game::VariableValue var{};
var.type = game::SCRIPT_STRING;
var.u.stringValue = value;
scripting::push_value(var);
}
void vm_notify_stub(game::scriptInstance_t inst, unsigned int notify_list_owner_id,
unsigned int string_value, game::VariableValue* top)
{
vm_notify_hook.invoke<void>(inst, notify_list_owner_id, string_value, top);
if (in_notify_queue)
{
return;
}
const auto disconnect_str = *reinterpret_cast<unsigned short*>(0x24B71CE);
in_notify_queue = true;
const auto _0 = gsl::finally([]
{
in_notify_queue = false;
});
std::unordered_map<unsigned int, std::unordered_set<unsigned int>> notified;
for (auto i = notify_groups_queue.begin(); i != notify_groups_queue.end(); )
{
if (i->owner_id == notify_list_owner_id)
{
const auto should_notify = i->sub_notifies.find(string_value) != i->sub_notifies.end();
const auto should_delete = string_value == disconnect_str || notified[i->notify_target].contains(i->main_notify);
const auto object_type = game::scr_VarGlob->objectVariableValue[i->notify_target].w.type;
if (object_type != game::SCRIPT_FREE && should_notify)
{
scripting::stack_isolation _1;
push_sl_string(string_value);
game::Scr_NotifyId(inst, 0, i->notify_target, i->main_notify, 1);
notified[i->notify_target].insert(i->main_notify);
}
if (should_notify || should_delete)
{
i = notify_groups_queue.erase(i);
continue;
}
}
++i;
}
}
void clear_notify_group(unsigned int id)
{
for (auto i = notify_groups_queue.begin(); i != notify_groups_queue.end(); )
{
if (i->owner_id == id)
{
i = notify_groups_queue.erase(i);
}
else
{
++i;
}
}
}
void free_variable_stub_1(utils::hook::assembler& a)
{
a.pushad();
a.push(ebx);
a.call(clear_notify_group);
a.pop(ebx);
a.popad();
a.movzx(ecx, word_ptr(edi, 2));
a.xor_(edx, edx);
a.jmp(0x550EA5);
}
void free_variable_stub_2(utils::hook::assembler& a)
{
a.pushad();
a.push(edi);
a.call(clear_notify_group);
a.pop(edi);
a.popad();
a.mov(word_ptr(esi, 2), di);
a.pop(edi);
a.jmp(0x415EA2);
}
void* client_connect_stub(int client_num, int script_pers_id)
{
const scripting::entity player = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, client_num, 0, 0);
scripting::notify(*game::levelEntityId, "direct_connect", {player});
return utils::hook::invoke<void*>(0x41BE10, client_num, script_pers_id);
}
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
if (!game::environment::t6zm())
{
return;
}
vm_notify_hook.create(0x8F3620, vm_notify_stub);
utils::hook::jump(0x550E9F, utils::hook::assemble(free_variable_stub_1));
utils::hook::jump(0x415E9D, utils::hook::assemble(free_variable_stub_2));
utils::hook::call(0x501F0F, client_connect_stub);
scripting::on_shutdown([]
{
notify_groups_queue.clear();
});
gsc::function::add("createnotifygroup", [](const scripting::variadic_args& args)
-> scripting::script_value
{
std::unordered_set<unsigned int> notifies;
const auto& entity = args[0].get_raw();
if (entity.type != game::SCRIPT_OBJECT)
{
throw std::runtime_error("argument 1 must be a script object");
}
const auto& entity_target = args[1].get_raw();
if (entity_target.type != game::SCRIPT_OBJECT)
{
throw std::runtime_error("argument 1 must be a script object");
}
const auto main_notify_str = args[2].as<std::string>();
const auto main_notify = game::SL_GetString(main_notify_str.data(), 0);
for (auto i = 3u; i < args.size(); i++)
{
const auto& arg = args[i];
if (arg.is<scripting::array>())
{
const auto arr = arg.as<scripting::array>();
for (const auto& [key, value] : arr)
{
const auto str = value.as<std::string>();
notifies.insert(game::SL_GetString(str.data(), 0));
}
}
else if (arg.is<std::string>())
{
const auto str = arg.as<std::string>();
notifies.insert(game::SL_GetString(str.data(), 0));
}
}
if (notifies.size() <= 0)
{
throw std::runtime_error("must pass atleast 1 notify");
}
notify_groups_queue.emplace_back(entity.u.uintValue, entity_target.u.uintValue,
main_notify, notifies);
return {};
});
}
};
}
REGISTER_COMPONENT(notifies::component)
@@ -0,0 +1,72 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "game/scripting/safe_execution.hpp"
#include "component/gsc.hpp"
#include <utils/hook.hpp>
#include <utils/string.hpp>
namespace patches
{
namespace
{
const game::dvar_t* sv_display_clan_tag;
char* cs_display_name_stub(const void* client_state, int type)
{
if (!sv_display_clan_tag->current.enabled)
{
type = 1;
}
return utils::hook::invoke<char*>(SELECT(0x59AE90, 0x563AD0), client_state, type);
}
void* bg_get_client_field_stub(int a1, const char* a2)
{
const auto result = utils::hook::invoke<void*>(0x5B4220, a1, a2);
if (!result)
{
const auto err = utils::string::va("no client field named %s found", a2);
game::Scr_Error(game::SCRIPTINSTANCE_SERVER, err, false);
}
return result;
}
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
patch();
if (game::environment::t6zm())
{
patch_zm();
}
}
void patch()
{
sv_display_clan_tag = game::Dvar_RegisterBool("sv_display_clan_tag", false, 0, "Display the clan tag in the game log");
utils::hook::call(SELECT(0x820F1C, 0x81F91C), cs_display_name_stub);
}
void patch_zm()
{
// Fix some common crashes, not necessary because of `patch()` but just to be sure
// sets an always null pointer :/
utils::hook::nop(0x6FF36E, 0x2);
// related to custom perks and tombstone/electric chair on motd and possibly other contexts
utils::hook::call(0x85842A, bg_get_client_field_stub);
}
};
}
REGISTER_COMPONENT(patches::component)
@@ -0,0 +1,185 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "scheduler.hpp"
#include <utils/concurrency.hpp>
#include <utils/hook.hpp>
namespace scheduler
{
utils::thread_pool thread_pool;
namespace
{
struct task
{
std::function<bool()> handler{};
std::chrono::milliseconds interval{};
std::chrono::high_resolution_clock::time_point last_call{};
};
using task_list = std::vector<task>;
utils::hook::detour com_server_packet_event_hook;
class task_pipeline
{
public:
void add(task&& task)
{
new_callbacks_.access([&task](task_list& tasks)
{
tasks.emplace_back(std::move(task));
});
}
void execute()
{
callbacks_.access([&](task_list& tasks)
{
this->merge_callbacks();
for (auto i = tasks.begin(); i != tasks.end();)
{
const auto now = std::chrono::high_resolution_clock::now();
const auto diff = now - i->last_call;
if (diff < i->interval)
{
++i;
continue;
}
i->last_call = now;
const auto res = i->handler();
if (res == cond_end)
{
i = tasks.erase(i);
}
else
{
++i;
}
}
});
}
private:
utils::concurrency::container<task_list> new_callbacks_;
utils::concurrency::container<task_list, std::recursive_mutex> callbacks_;
void merge_callbacks()
{
callbacks_.access([&](task_list& tasks)
{
new_callbacks_.access([&](task_list& new_tasks)
{
tasks.insert(tasks.end(), std::move_iterator<task_list::iterator>(new_tasks.begin()), std::move_iterator<task_list::iterator>(new_tasks.end()));
new_tasks = {};
});
});
}
};
std::thread thread;
std::atomic_bool killed = false;
task_pipeline pipelines[pipeline::count];
void execute(const pipeline type)
{
assert(type >= 0 && type < pipeline::count);
pipelines[type].execute();
}
void execute_server()
{
execute(pipeline::server);
}
void server_frame_stub(utils::hook::assembler& a)
{
a.pushad();
a.call(execute_server);
a.popad();
a.jmp(SELECT(0x49E910, 0x5001A0));
}
void com_server_packet_event_stub()
{
execute(pipeline::server_packet_loop);
com_server_packet_event_hook.invoke<void>();
}
}
void schedule(const std::function<bool()>& callback, const pipeline type,
const std::chrono::milliseconds delay)
{
assert(type >= 0 && type < pipeline::count);
task task;
task.handler = callback;
task.interval = delay;
task.last_call = std::chrono::high_resolution_clock::now();
pipelines[type].add(std::move(task));
}
void loop(const std::function<void()>& callback, const pipeline type,
const std::chrono::milliseconds delay)
{
schedule([callback]()
{
callback();
return cond_continue;
}, type, delay);
}
void once(const std::function<void()>& callback, const pipeline type,
const std::chrono::milliseconds delay)
{
schedule([callback]()
{
callback();
return cond_end;
}, type, delay);
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
thread_pool.initialize(8);
thread_pool.start();
thread = std::thread([]()
{
while (!killed)
{
execute(pipeline::async);
std::this_thread::sleep_for(10ms);
}
});
utils::hook::jump(SELECT(0x4A59F7, 0x6AA2F7), utils::hook::assemble(server_frame_stub));
com_server_packet_event_hook.create(SELECT(0x42F850, 0x549300), com_server_packet_event_stub);
}
void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override
{
thread_pool.stop();
killed = true;
if (thread.joinable())
{
thread.join();
}
}
};
}
REGISTER_COMPONENT(scheduler::component)
@@ -0,0 +1,26 @@
#pragma once
#include <utils/thread_pool.hpp>
namespace scheduler
{
enum pipeline
{
server,
server_packet_loop,
async,
count,
};
static const bool cond_continue = false;
static const bool cond_end = true;
void schedule(const std::function<bool()>& callback, pipeline type = pipeline::server,
std::chrono::milliseconds delay = 0ms);
void loop(const std::function<void()>& callback, pipeline type = pipeline::server,
std::chrono::milliseconds delay = 0ms);
void once(const std::function<void()>& callback, pipeline type = pipeline::server,
std::chrono::milliseconds delay = 0ms);
extern utils::thread_pool thread_pool;
}
@@ -0,0 +1,283 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "game/scripting/event.hpp"
#include "game/scripting/execution.hpp"
#include "scripting.hpp"
#include "scheduler.hpp"
#include <utils/hook.hpp>
#include <utils/string.hpp>
namespace scripting
{
std::unordered_map<int, std::unordered_map<std::string, int>> fields_table;
std::unordered_map<std::string, game::BuiltinMethodDef> method_map;
std::unordered_map<std::string, game::BuiltinFunctionDef> function_map;
std::unordered_map<std::string, std::unordered_map<std::string, const char*>> script_function_table;
std::unordered_map<std::string, std::vector<std::pair<std::string, const char*>>> script_function_table_sort;
namespace
{
utils::hook::detour scr_add_class_field_hook;
utils::hook::detour g_shutdown_game_hook;
void scr_add_class_field_stub(game::scriptInstance_t inst, unsigned int classnum, char const* name, unsigned int offset)
{
if (fields_table[classnum].find(name) == fields_table[classnum].end())
{
fields_table[classnum][name] = offset;
}
scr_add_class_field_hook.invoke<void>(inst, classnum, name, offset);
}
game::BuiltinMethodDef get_method(const std::string& name)
{
game::BuiltinMethodDef method{};
auto pName = name.data();
int arg = 0;
const auto func = game::Scr_GetMethod(&pName, &arg, &arg, &arg);
if (func)
{
method.actionFunc = reinterpret_cast<script_function>(func);
method.actionString = pName;
}
return method;
}
void load_functions()
{
for (auto i = 0; i < 278; i++)
{
const auto method = game::player_methods[i];
method_map[method.actionString] = method;
}
for (auto i = 0; i < 22; i++)
{
const auto method = game::scriptEnt_methods[i];
method_map[method.actionString] = method;
}
for (auto i = 0; i < 114; i++)
{
const auto method = game::scriptVehicle_methods[i];
method_map[method.actionString] = method;
}
for (auto i = 0; i < 29; i++)
{
const auto method = game::hudElem_methods[i];
method_map[method.actionString] = method;
}
for (auto i = 0; i < 29; i++)
{
const auto method = game::helicopter_methods[i];
method_map[method.actionString] = method;
}
for (auto i = 0; i < 109; i++)
{
const auto method = game::actor_methods[i];
method_map[method.actionString] = method;
}
for (auto i = 0; i < 95; i++)
{
const auto method = game::builtInCommon_methods[i];
method_map[method.actionString] = method;
}
for (auto i = 0; i < 286; i++)
{
const auto method = game::builtIn_methods[i];
method_map[method.actionString] = method;
}
for (auto i = 0; i < 334; i++)
{
const auto function = game::functions[i];
function_map[function.actionString] = function;
}
for (auto i = 0; i < 220; i++)
{
const auto function = game::common_functions[i];
function_map[function.actionString] = function;
}
method_map["notifyonplayercommand"] = get_method("notifyonplayercommand");
}
utils::hook::detour scr_load_script_hook;
void extract_obj_functions(game::GSC_OBJ* obj)
{
const auto exported = reinterpret_cast<game::GSC_EXPORT_ITEM*>(&obj->magic[obj->exports_offset]);
const auto count = obj->exports_count;
auto iter = &obj->magic[obj->exports_offset + 8];
std::string filename = &obj->magic[obj->name];
if (filename.ends_with(".gsc"))
{
filename = filename.substr(0, filename.size() - 4);
}
std::string original = filename;
auto filename_count = 0;
while (script_function_table.find(filename) != script_function_table.end())
{
filename = utils::string::va("%s_%i", original.data(), filename_count++);
}
for (auto i = 0; i < count; i++)
{
const auto index = *reinterpret_cast<unsigned __int16*>(iter);
const auto function = &obj->magic[index];
const auto address = reinterpret_cast<char*>(reinterpret_cast<unsigned int>(obj) + exported[i].address);
script_function_table[filename][function] = address;
script_function_table_sort[filename].push_back({function, address});
if (i == count - 1)
{
script_function_table[filename]["$end"] = &obj->magic[obj->fixup_offset];
script_function_table_sort[filename].push_back({"$end", &obj->magic[obj->fixup_offset]});
}
iter += 12;
}
}
utils::hook::detour scr_post_load_scripts_hook;
unsigned int post_load_scripts_stub(unsigned int inst)
{
script_function_table.clear();
script_function_table_sort.clear();
const auto script_count = *reinterpret_cast<unsigned int*>(SELECT(0x2DB9F18, 0x2D8A218));
const auto scripts = reinterpret_cast<game::objFileInfo_t*>(SELECT(0x2DA2FE8, 0x2D732E8));
for (auto i = 0u; i < script_count; i++)
{
const auto obj = scripts[i].activeVersion;
extract_obj_functions(obj);
}
return scr_post_load_scripts_hook.invoke<unsigned int>(inst);
}
std::vector<std::function<void()>> shutdown_callbacks;
void g_shutdown_game_stub(const int free_scripts)
{
for (const auto& callback : shutdown_callbacks)
{
callback();
}
return g_shutdown_game_hook.invoke<void>(free_scripts);
}
}
script_function find_function_ptr(const std::string& _name)
{
const auto name = utils::string::to_lower(_name);
if (function_map.find(name) != function_map.end())
{
const auto func = function_map[name].actionFunc;
return reinterpret_cast<script_function>(func);
}
if (method_map.find(name) != method_map.end())
{
const auto func = method_map[name].actionFunc;
return reinterpret_cast<script_function>(func);
}
return nullptr;
}
std::string find_function(const char* pos)
{
for (const auto& file : script_function_table_sort)
{
for (auto i = file.second.begin(); i != file.second.end() && std::next(i) != file.second.end(); ++i)
{
const auto next = std::next(i);
if (pos >= i->second && pos < next->second)
{
return utils::string::va("%s::%s", file.first.data(), i->first.data());
}
}
}
return "unknown function";
}
std::optional<std::pair<std::string, std::string>> find_function_pair(const char* pos)
{
for (const auto& file : script_function_table_sort)
{
for (auto i = file.second.begin(); i != file.second.end() && std::next(i) != file.second.end(); ++i)
{
const auto next = std::next(i);
if (pos >= i->second && pos < next->second)
{
return {{file.first, i->first}};
}
}
}
return {};
}
const char* find_function_start(const char* pos)
{
for (const auto& file : script_function_table_sort)
{
for (auto i = file.second.begin(); i != file.second.end() && std::next(i) != file.second.end(); ++i)
{
const auto next = std::next(i);
if (pos >= i->second && pos < next->second)
{
return i->second;
}
}
}
return nullptr;
}
void on_shutdown(const std::function<void()>& callback)
{
shutdown_callbacks.push_back(callback);
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
g_shutdown_game_hook.create(SELECT(0x60DCF0, 0x688A40), g_shutdown_game_stub);
scr_add_class_field_hook.create(SELECT(0x6B7620, 0x438AD0), scr_add_class_field_stub);
scr_post_load_scripts_hook.create(SELECT(0x642EB0, 0x425F80), post_load_scripts_stub);
load_functions();
}
};
}
REGISTER_COMPONENT(scripting::component)
@@ -0,0 +1,22 @@
#pragma once
#include "game/game.hpp"
namespace scripting
{
extern std::unordered_map<int, std::unordered_map<std::string, int>> fields_table;
extern std::unordered_map<std::string, std::unordered_map<std::string, const char*>> script_function_table;
extern std::unordered_map<std::string, std::vector<std::pair<std::string, const char*>>> script_function_table_sort;
extern std::unordered_map<std::string, game::BuiltinMethodDef> method_map;
extern std::unordered_map<std::string, game::BuiltinFunctionDef> function_map;
using script_function = void(*)(game::scr_entref_t);
script_function find_function_ptr(const std::string& name);
std::string find_function(const char* pos);
const char* find_function_start(const char* pos);
std::optional<std::pair<std::string, std::string>> find_function_pair(const char* pos);
void on_shutdown(const std::function<void()>& callback);
}
@@ -0,0 +1,201 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "gsc.hpp"
// lua/lstrlib.c
#define MAX_FORMAT 32
#define L_FMTFLAGSF "-+#0 "
#define L_FMTFLAGSX "-#0"
#define L_FMTFLAGSI "-+0 "
#define L_FMTFLAGSU "-0"
#define L_FMTFLAGSC "-"
namespace string
{
namespace
{
// lua/lstrlib.c
const char* getformat(const char* strfrmt, char* form)
{
const auto len = std::strspn(strfrmt, L_FMTFLAGSF "123456789.") + 1;
if (len >= MAX_FORMAT - 10)
{
throw std::runtime_error("invalid format (too long)");
}
*(form++) = '%';
std::memcpy(form, strfrmt, len * sizeof(char));
*(form + len) = '\0';
return strfrmt + len - 1;
}
// lua/lstrlib.c
const char* get_2_digits(const char* s)
{
if (isdigit(static_cast<unsigned char>(*s)))
{
s++;
if (isdigit(static_cast<unsigned char>(*s)))
{
s++;
}
}
return s;
}
// lua/lstrlib.c
void check_format(const char* form, const char* flags, int precision)
{
const char* spec = form + 1;
spec += std::strspn(spec, flags);
if (*spec != '0')
{
spec = get_2_digits(spec);
if (*spec == '.' && precision)
{
spec++;
spec = get_2_digits(spec);
}
}
if (!std::isalpha(static_cast<unsigned char>(*spec)))
{
throw std::runtime_error(utils::string::va("invalid conversion specification: '%s'", form));
}
}
// partially lua/lstrlib.c
std::string format_string(const std::string& fmt, const scripting::variadic_args& va)
{
std::string buffer{};
size_t va_index{};
const char* strfrmt = fmt.data();
const char* strfrmt_end = strfrmt + fmt.size();
while (strfrmt < strfrmt_end)
{
if (*strfrmt != '%')
{
buffer.push_back(*strfrmt++);
}
else if (*++strfrmt == '%')
{
buffer.push_back(*strfrmt++);
}
else
{
char form[MAX_FORMAT]{};
const char* flags = "";
strfrmt = getformat(strfrmt, form);
switch (*strfrmt++)
{
case 'd':
case 'i':
flags = L_FMTFLAGSI;
goto intcase;
case 'u':
case 'p':
flags = L_FMTFLAGSU;
goto intcase;
case 'o':
case 'x':
case 'X':
flags = L_FMTFLAGSX;
intcase:
{
check_format(form, flags, 1);
const auto value = va[va_index].as<int>();
buffer.append(utils::string::va(form, value));
va_index++;
break;
}
case 'f':
case 'F':
case 'e':
case 'E':
case 'g':
case 'G':
{
check_format(form, L_FMTFLAGSF, 1);
const auto value = va[va_index].as<float>();
buffer.append(utils::string::va(form, value));
va_index++;
break;
}
case 'c':
{
const auto value = va[va_index].as<int>();
check_format(form, L_FMTFLAGSC, 0);
buffer.append(utils::string::va(form, static_cast<char>(value)));
va_index++;
break;
}
case 's':
{
const auto str = va[va_index].as<std::string>();
buffer.append(str);
va_index++;
break;
}
default:
{
throw std::runtime_error(utils::string::va("invalid conversion '%s' to 'format'", form));
}
}
}
}
return buffer;
}
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
gsc::function::add_multiple(format_string, "va", "string::va",
"formatstring", "string::format", "sprintf");
gsc::function::add_multiple(utils::string::to_upper, "toupper", "string::to_upper");
gsc::function::add_multiple(utils::string::to_lower, "tolower", "string::to_lower");
gsc::function::add("string::is_numeric", utils::string::is_numeric);
gsc::function::add("string::starts_with", utils::string::starts_with);
gsc::function::add("string::ends_with", utils::string::ends_with);
gsc::function::add("string::replace", utils::string::replace);
gsc::function::add("string::regex_replace", [](const std::string& str, const std::regex& expr,
const std::string& with)
{
return std::regex_replace(str, expr, with);
});
gsc::function::add("string::regex_match", [](const std::string& str, const std::regex& expr)
{
scripting::array array_match{};
std::smatch match{};
if (std::regex_match(str, match, std::regex(expr)))
{
for (const auto& s : match)
{
array_match.emplace_back((s.str()));
}
}
return array_match;
});
gsc::function::add("string::regex_search", [](const std::string& str, const std::regex& expr)
{
return std::regex_search(str, expr);
});
}
};
}
REGISTER_COMPONENT(string::component)
@@ -0,0 +1,418 @@
#include <stdinc.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "gsc.hpp"
#include "game/structs.hpp"
#include "game/game.hpp"
#include <utils/io.hpp>
#include <utils/hook.hpp>
#define TURRET_DUMP_FIELD(__field__) \
data[#__field__] = turret->__field__; \
#define TURRET_ADD_FIELD_INTERNAL(__name__, __getter__, __setter__) \
{utils::string::to_lower(#__name__), turret_field{offsetof(game::TurretInfo, __name__), __getter__, __setter__}} \
#define TURRET_ADD_FIELD_GETTER(__name__, __getter__) \
TURRET_ADD_FIELD_INTERNAL(__name__, __getter__, get_default_setter(dummy_info.__name__, #__name__, offsetof(game::TurretInfo, __name__))) \
#define TURRET_ADD_FIELD_SETTER(__name__, __setter__) \
TURRET_ADD_FIELD_INTERNAL(__name__, get_default_getter(dummy_info.__name__, offsetof(game::TurretInfo, __name__)), __setter__) \
#define TURRET_ADD_FIELD(__name__) \
TURRET_ADD_FIELD_INTERNAL(__name__, get_default_getter(dummy_info.__name__, offsetof(game::TurretInfo, __name__)), get_default_setter(dummy_info.__name__, #__name__, offsetof(game::TurretInfo, __name__))) \
#define NEW_MAX_TURRETS 255
namespace turret
{
namespace
{
game::TurretInfo turret_info_store_relocated[NEW_MAX_TURRETS];
game::TurretInfo dummy_info{};
using getter_t = std::function<scripting::script_value(game::TurretInfo* info)>;
using setter_t = std::function<void(game::TurretInfo* info, const scripting::function_argument& value)>;
struct turret_field
{
std::size_t offset;
getter_t getter;
setter_t setter;
};
template <typename T>
getter_t get_default_getter([[maybe_unused]] T, const size_t offset)
{
return [=](game::TurretInfo* info)
{
return *reinterpret_cast<T*>(reinterpret_cast<size_t>(info) + offset);
};
}
template <typename T>
typename std::enable_if<std::is_same<T, float*>::value, setter_t>::type
get_default_setter([[maybe_unused]] T, const std::string& name, const size_t offset)
{
return [=](game::TurretInfo* info, const scripting::function_argument& value)
{
if (!value.is<T>())
{
const auto type_name = value.type_name();
throw std::runtime_error(utils::string::va("type '%s' is not valid for field '%s'", type_name.data(), name.data()));
}
const auto vec = value.as<scripting::vector>();
const auto val = reinterpret_cast<float*>(reinterpret_cast<size_t>(info) + offset);
val[0] = vec.get_x();
val[1] = vec.get_y();
val[2] = vec.get_z();
};
}
template <typename T>
typename std::enable_if<!std::is_same<T, float*>::value, setter_t>::type
get_default_setter([[maybe_unused]] T, const std::string& name, const size_t offset)
{
return [=](game::TurretInfo* info, const scripting::function_argument& value)
{
if (!value.is<T>())
{
const auto type_name = value.type_name();
throw std::runtime_error(utils::string::va("type '%s' is not valid for field '%s'", type_name.data(), name.data()));
}
*reinterpret_cast<T*>(reinterpret_cast<size_t>(info) + offset) = value.as<T>();
};
}
scripting::script_value turret_get_team(game::TurretInfo* info)
{
return utils::hook::invoke<const char*>(0x4D7A30, info->eTeam);
}
void turret_set_team(game::TurretInfo* info, const scripting::function_argument& value)
{
const auto str = value.as<std::string>();
const auto team_names = reinterpret_cast<const char**>(0xD39970);
for (auto i = 0; i < game::TEAM_NUM_TEAMS; i++)
{
if (team_names[i] == str)
{
info->eTeam = static_cast<game::team_t>(i);
return;
}
}
throw std::runtime_error(utils::string::va("invalid team '%s'", str.data()));
}
std::unordered_map<std::string, turret_field> turret_fields =
{
TURRET_ADD_FIELD(inuse),
TURRET_ADD_FIELD(state),
TURRET_ADD_FIELD(flags),
TURRET_ADD_FIELD(fireTime),
TURRET_ADD_FIELD(manualTarget.infoIndex),
TURRET_ADD_FIELD(manualTarget.number),
TURRET_ADD_FIELD(target.infoIndex),
TURRET_ADD_FIELD(target.number),
TURRET_ADD_FIELD(targetPos),
TURRET_ADD_FIELD(targetTime),
TURRET_ADD_FIELD(missOffsetNormalized),
TURRET_ADD_FIELD(arcmin[0]),
TURRET_ADD_FIELD(arcmin[1]),
TURRET_ADD_FIELD(arcmax[0]),
TURRET_ADD_FIELD(arcmax[1]),
TURRET_ADD_FIELD(initialYawmin),
TURRET_ADD_FIELD(initialYawmax),
TURRET_ADD_FIELD(forwardAngleDot),
TURRET_ADD_FIELD(dropPitch),
TURRET_ADD_FIELD(scanningPitch),
TURRET_ADD_FIELD(convergenceTime[0]),
TURRET_ADD_FIELD(convergenceTime[1]),
TURRET_ADD_FIELD(suppressTime),
TURRET_ADD_FIELD(maxRangeSquared),
TURRET_ADD_FIELD(detachSentient.infoIndex),
TURRET_ADD_FIELD(detachSentient.number),
TURRET_ADD_FIELD(stance),
TURRET_ADD_FIELD(prevStance),
TURRET_ADD_FIELD(fireSndDelay),
TURRET_ADD_FIELD(accuracy),
TURRET_ADD_FIELD(userOrigin),
TURRET_ADD_FIELD(prevSentTarget),
TURRET_ADD_FIELD(aiSpread),
TURRET_ADD_FIELD(playerSpread),
TURRET_ADD_FIELD_INTERNAL(eTeam, turret_get_team, turret_set_team),
TURRET_ADD_FIELD(heatVal),
TURRET_ADD_FIELD(overheating),
TURRET_ADD_FIELD(fireBarrel),
TURRET_ADD_FIELD(scanSpeed),
TURRET_ADD_FIELD(scanDecelYaw),
TURRET_ADD_FIELD(scanPauseTime),
TURRET_ADD_FIELD(originError),
TURRET_ADD_FIELD(anglesError),
TURRET_ADD_FIELD(pitchCap),
TURRET_ADD_FIELD(triggerDown),
TURRET_ADD_FIELD(fireSnd),
TURRET_ADD_FIELD(fireSndPlayer),
TURRET_ADD_FIELD(startFireSnd),
TURRET_ADD_FIELD(startFireSndPlayer),
TURRET_ADD_FIELD(loopFireEnd),
TURRET_ADD_FIELD(loopFireEndPlayer),
TURRET_ADD_FIELD(rotateLoopSnd),
TURRET_ADD_FIELD(rotateLoopSndPlayer),
TURRET_ADD_FIELD(rotateStopSnd),
TURRET_ADD_FIELD(rotateStopSndPlayer),
TURRET_ADD_FIELD(sndIsFiring),
TURRET_ADD_FIELD(targetOffset),
TURRET_ADD_FIELD(onTargetAngle),
TURRET_ADD_FIELD(previousAngles),
TURRET_ADD_FIELD(previousAngles),
};
utils::hook::detour bg_get_weapon_def_hook;
game::WeaponDef* bg_get_weapon_def_stub(const unsigned int weapon)
{
auto asset = bg_get_weapon_def_hook.invoke<game::WeaponDef*>(weapon);
if (asset->maxTurnSpeed[0] == 0.f && asset->maxTurnSpeed[1] == 0.f)
{
asset->maxTurnSpeed[0] = 360.f;
asset->maxTurnSpeed[1] = 360.f;
asset->minTurnSpeed[0] = 1.f;
asset->minTurnSpeed[1] = 1.f;
}
return asset;
}
void g_init_turrets_stub()
{
for (int i = 0; i < NEW_MAX_TURRETS; ++i)
{
turret_info_store_relocated[i].inuse = 0;
}
// level.turrets = turret_info_store_relocated;
utils::hook::set<game::TurretInfo*>(0x233CAB8, turret_info_store_relocated);
}
auto turret_index = 0;
const auto turret_index_ptr = &turret_index;
game::TurretInfo* g_spawn_turret_relocate()
{
turret_index = 0;
game::TurretInfo* turret_info{};
for (auto i = 0; i < NEW_MAX_TURRETS; ++i)
{
turret_info = &turret_info_store_relocated[i];
if (!turret_info->inuse)
{
break;
}
turret_index++;
}
return turret_info;
}
void __declspec(naked) g_spawn_turret_stub()
{
__asm
{
push esi;
pushad;
call g_spawn_turret_relocate;
mov[esp + 0x20], eax;
popad;
pop esi;
mov ecx, [turret_index_ptr];
push 0x60AE90;
retn;
}
}
game::gentity_s* g_spawn_turret_static_entity_stub(int /*unk1*/)
{
return utils::hook::invoke<game::gentity_s*>(0x54FCC0);
}
void reallocate_turrets()
{
// Modify G_InitTurrets to initialize more tuurets
utils::hook::jump(0x61E850, g_init_turrets_stub);
// Modify G_SpawnTurret to check for a larger amount of turretInfoStore
utils::hook::jump(0x60AE3B, g_spawn_turret_stub);
// Modify G_SpawnturetStaticEntity to call G_Spawn instead
utils::hook::jump(0x6E1B00, g_spawn_turret_static_entity_stub);
// Patch G_SpawnTurret com_error to happen at a higher limit (255)
utils::hook::set<std::uint8_t>(0x60AE92, NEW_MAX_TURRETS);
// Patch level.num_gentities to min of 78 to not preallocate turret slots
utils::hook::set<int>(0x67EEA2 + 6, 78);
// Patch SV_LocateGameData arg so sv.num_entities is the same as the above
utils::hook::set<std::uint8_t>(0x67EE9F + 1, 78);
// G_FreeEntity remove turrets from slot calculations
utils::hook::set<std::uint8_t>(0x6658E7 + 2, 78);
}
}
class component final : public component_interface
{
public:
void on_startup([[maybe_unused]] plugin::plugin* plugin) override
{
if (!game::environment::t6zm())
{
return;
}
gsc::method::add("setturretfield", [](const scripting::entity& entity, const std::string& field, const scripting::variadic_args& args)
{
const auto ent = entity.get_entity_reference();
if (ent.classnum != 0)
{
throw std::runtime_error("Invalid entity");
}
const auto turret = game::g_entities[ent.entnum].pTurretInfo;
if (turret == nullptr)
{
throw std::runtime_error("Not a valid turret entity");
}
const auto field_name = utils::string::to_lower(field);
const auto itr = turret_fields.find(field_name);
if (itr == turret_fields.end())
{
throw std::runtime_error(utils::string::va("Invalid field %s", field_name.data()));
}
itr->second.setter(turret, args[0]);
});
gsc::method::add("getturretfield", [](const scripting::entity& entity, const std::string& field)
{
const auto ent = entity.get_entity_reference();
if (ent.classnum != 0)
{
throw std::runtime_error("Invalid entity");
}
const auto turret = game::g_entities[ent.entnum].pTurretInfo;
if (turret == nullptr)
{
throw std::runtime_error("Not a valid turret entity");
}
const auto field_name = utils::string::to_lower(field);
const auto itr = turret_fields.find(field_name);
if (itr == turret_fields.end())
{
throw std::runtime_error(utils::string::va("Invalid field %s", field_name.data()));
}
return itr->second.getter(turret);
});
gsc::function::add("dumpturret", [](const scripting::entity& entity, const std::string& name)
{
const auto ent = entity.get_entity_reference();
if (ent.classnum != 0)
{
throw std::runtime_error("Invalid entity");
}
const auto turret = game::g_entities[ent.entnum].pTurretInfo;
if (turret == nullptr)
{
throw std::runtime_error("Not a valid turret entity");
}
nlohmann::ordered_json data;
TURRET_DUMP_FIELD(inuse);
TURRET_DUMP_FIELD(state);
TURRET_DUMP_FIELD(flags);
TURRET_DUMP_FIELD(fireTime);
data["manualTarget"]["infoIndex"] = turret->manualTarget.infoIndex;
data["manualTarget"]["number"] = turret->manualTarget.number;
data["target"]["infoIndex"] = turret->target.infoIndex;
data["target"]["number"] = turret->target.number;
TURRET_DUMP_FIELD(targetPos);
TURRET_DUMP_FIELD(targetTime);
TURRET_DUMP_FIELD(missOffsetNormalized);
TURRET_DUMP_FIELD(arcmin);
TURRET_DUMP_FIELD(arcmax);
TURRET_DUMP_FIELD(initialYawmin);
TURRET_DUMP_FIELD(initialYawmax);
TURRET_DUMP_FIELD(forwardAngleDot);
TURRET_DUMP_FIELD(dropPitch);
TURRET_DUMP_FIELD(scanningPitch);
TURRET_DUMP_FIELD(convergenceTime);
TURRET_DUMP_FIELD(suppressTime);
TURRET_DUMP_FIELD(maxRangeSquared);
data["detachSentient"]["infoIndex"] = turret->detachSentient.infoIndex;
data["detachSentient"]["number"] = turret->detachSentient.number;
TURRET_DUMP_FIELD(stance);
TURRET_DUMP_FIELD(prevStance);
TURRET_DUMP_FIELD(fireSndDelay);
TURRET_DUMP_FIELD(accuracy);
TURRET_DUMP_FIELD(userOrigin);
TURRET_DUMP_FIELD(prevSentTarget);
TURRET_DUMP_FIELD(aiSpread);
TURRET_DUMP_FIELD(playerSpread);
TURRET_DUMP_FIELD(eTeam);
TURRET_DUMP_FIELD(heatVal);
TURRET_DUMP_FIELD(overheating);
TURRET_DUMP_FIELD(fireBarrel);
TURRET_DUMP_FIELD(scanSpeed);
TURRET_DUMP_FIELD(scanDecelYaw);
TURRET_DUMP_FIELD(scanPauseTime);
TURRET_DUMP_FIELD(originError);
TURRET_DUMP_FIELD(anglesError);
TURRET_DUMP_FIELD(pitchCap);
TURRET_DUMP_FIELD(triggerDown);
TURRET_DUMP_FIELD(fireSnd);
TURRET_DUMP_FIELD(fireSndPlayer);
TURRET_DUMP_FIELD(startFireSnd);
TURRET_DUMP_FIELD(startFireSndPlayer);
TURRET_DUMP_FIELD(loopFireEnd);
TURRET_DUMP_FIELD(loopFireEndPlayer);
TURRET_DUMP_FIELD(rotateLoopSnd);
TURRET_DUMP_FIELD(rotateLoopSndPlayer);
TURRET_DUMP_FIELD(rotateStopSnd);
TURRET_DUMP_FIELD(rotateStopSndPlayer);
TURRET_DUMP_FIELD(sndIsFiring);
TURRET_DUMP_FIELD(targetOffset);
TURRET_DUMP_FIELD(onTargetAngle);
TURRET_DUMP_FIELD(turretRotateState);
TURRET_DUMP_FIELD(previousAngles);
const std::string buffer = data.dump(4);
const std::string turret_entity_path = utils::string::va("entity_dumps/turrets/%s.json", name.data());
return utils::io::write_file(turret_entity_path, buffer, false);
});
// Check for whether a weapon can be spawned as a turret
utils::hook::nop(0x60AF4E, 5);
// Don't use weapon def value for turrets turnspeed
bg_get_weapon_def_hook.create(0x5906B0, bg_get_weapon_def_stub);
// Reallocate turrets to NEW_MAX_TURRETS
reallocate_turrets();
}
};
}
REGISTER_COMPONENT(turret::component)