#include #include "loader/component_loader.hpp" #include "game/dvars.hpp" #include "game/game.hpp" #include "game/scripting/execution.hpp" #include "game/scripting/function.hpp" #include "game/scripting/functions.hpp" #include "game/scripting/lua/error.hpp" #include #include "component/command.hpp" #include "component/console.hpp" #include "component/scripting.hpp" #include "component/logfile.hpp" #include #include #include "script_extension.hpp" #include "script_error.hpp" namespace gsc { std::uint16_t function_id_start = 0x30A; std::uint16_t method_id_start = 0x8586; builtin_function func_table[0x1000]; builtin_method meth_table[0x1000]; const game::dvar_t* developer_script = nullptr; namespace { std::unordered_map functions; std::unordered_map methods; bool force_error_print = false; std::optional gsc_error_msg; game::scr_entref_t saved_ent_ref; function_args get_arguments() { std::vector args; for (auto i = 0; static_cast(i) < game::scr_VmPub->outparamcount; ++i) { const auto value = game::scr_VmPub->top[-i]; args.push_back(value); } return args; } void return_value(const scripting::script_value& value) { if (game::scr_VmPub->outparamcount) { game::Scr_ClearOutParams(); } scripting::push_value(value); } std::uint16_t get_function_id() { const auto pos = game::scr_function_stack->pos; return *reinterpret_cast( reinterpret_cast(pos - 2)); } void execute_custom_function(const std::uint16_t id) { auto error = false; try { const auto& function = functions[id]; const auto result = function(get_arguments()); const auto type = result.get_raw().type; if (type) { return_value(result); } } catch (const std::exception& e) { error = true; force_error_print = true; gsc_error_msg = e.what(); } if (error) { game::Scr_ErrorInternal(); } } void execute_custom_method(const std::uint16_t id) { auto error = false; try { const auto& method = methods[id]; const auto result = method(saved_ent_ref, get_arguments()); const auto type = result.get_raw().type; if (type) { return_value(result); } } catch (const std::exception& e) { error = true; force_error_print = true; gsc_error_msg = e.what(); } if (error) { game::Scr_ErrorInternal(); } } void vm_call_builtin_function_stub(builtin_function function) { const auto function_id = get_function_id(); if (!functions.contains(function_id)) { function(); } else { execute_custom_function(function_id); } } game::scr_entref_t get_entity_id_stub(std::uint32_t ent_id) { const auto ref = game::Scr_GetEntityIdRef(ent_id); saved_ent_ref = ref; return ref; } void vm_call_builtin_method_stub(builtin_method method) { const auto function_id = get_function_id(); if (!methods.contains(function_id)) { method(saved_ent_ref); } else { execute_custom_method(function_id); } } void builtin_call_error(const std::string& error) { const auto function_id = get_function_id(); if (function_id > 0x1000) { console::warn("in call to builtin method \"%s\"%s", xsk::gsc::h1::resolver::method_name(function_id).data(), error.data()); } else { console::warn("in call to builtin function \"%s\"%s", xsk::gsc::h1::resolver::function_name(function_id).data(), error.data()); } } std::optional get_opcode_name(const std::uint8_t opcode) { try { return {xsk::gsc::h1::resolver::opcode_name(opcode)}; } catch (...) { return {}; } } void print_callstack() { for (auto frame = game::scr_VmPub->function_frame; frame != game::scr_VmPub->function_frame_start; --frame) { const auto pos = frame == game::scr_VmPub->function_frame ? game::scr_function_stack->pos : frame->fs.pos; const auto function = find_function(frame->fs.pos); if (function.has_value()) { console::warn("\tat function \"%s\" in file \"%s.gsc\"\n", function.value().first.data(), function.value().second.data()); } else { console::warn("\tat unknown location %p\n", pos); } } } void vm_error_stub(int mark_pos) { if (!developer_script->current.enabled && !force_error_print) { utils::hook::invoke(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), mark_pos); return; } console::warn("*********** script runtime error *************\n"); const auto opcode_id = *reinterpret_cast(SELECT_VALUE(0xC4015E8_b, 0xB7B8968_b)); const std::string error_str = gsc_error_msg.has_value() ? utils::string::va(": %s", gsc_error_msg.value().data()) : ""; if ((opcode_id >= 0x1A && opcode_id <= 0x20) || (opcode_id >= 0xA9 && opcode_id <= 0xAF)) { builtin_call_error(error_str); } else { const auto opcode = get_opcode_name(opcode_id); if (opcode.has_value()) { console::warn("while processing instruction %s%s\n", opcode.value().data(), error_str.data()); } else { console::warn("while processing instruction 0x%X%s\n", opcode_id, error_str.data()); } } force_error_print = false; gsc_error_msg = {}; print_callstack(); console::warn("**********************************************\n"); utils::hook::invoke(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), mark_pos); } void print(const function_args& args) { std::string buffer{}; for (auto i = 0u; i < args.size(); ++i) { const auto str = args[i].to_string(); buffer.append(str); buffer.append("\t"); } console::info("%s\n", buffer.data()); } scripting::script_value typeof(const function_args& args) { return args[0].type_name(); } } namespace function { void add(const std::string& name, script_function function) { if (xsk::gsc::h1::resolver::find_function(name)) { const auto id = xsk::gsc::h1::resolver::function_id(name); functions[id] = function; } else { const auto id = ++function_id_start; xsk::gsc::h1::resolver::add_function(name, static_cast(id)); functions[id] = function; } } } namespace method { void add(const std::string& name, script_method method) { if (xsk::gsc::h1::resolver::find_method(name)) { const auto id = xsk::gsc::h1::resolver::method_id(name); methods[id] = method; } else { const auto id = ++method_id_start; xsk::gsc::h1::resolver::add_method(name, static_cast(id)); methods[id] = method; } } } function_args::function_args(std::vector values) : values_(values) { } std::uint32_t function_args::size() const { return static_cast(this->values_.size()); } std::vector function_args::get_raw() const { return this->values_; } scripting::value_wrap function_args::get(const int index) const { if (index >= this->values_.size()) { throw std::runtime_error(utils::string::va("parameter %d does not exist", index)); } return {this->values_[index], index}; } class extension final : public component_interface { public: void post_unpack() override { utils::hook::set(SELECT_VALUE(0x3BD86C_b, 0x50484C_b), 0x1000); // change builtin func count utils::hook::set(SELECT_VALUE(0x3BD872_b, 0x504852_b) + 4, static_cast(reverse_b((&func_table)))); utils::hook::set(SELECT_VALUE(0x3CB718_b, 0x512778_b) + 4, static_cast(reverse_b((&func_table)))); utils::hook::inject(SELECT_VALUE(0x3BDC28_b, 0x504C58_b) + 3, &func_table); utils::hook::set(SELECT_VALUE(0x3BDC1E_b, 0x504C4E_b), sizeof(func_table)); utils::hook::set(SELECT_VALUE(0x3BD882_b, 0x504862_b) + 4, static_cast(reverse_b((&meth_table)))); utils::hook::set(SELECT_VALUE(0x3CBA3B_b, 0x512A9B_b) + 4, static_cast(reverse_b(&meth_table))); utils::hook::inject(SELECT_VALUE(0x3BDC36_b, 0x504C66_b) + 3, &meth_table); utils::hook::set(SELECT_VALUE(0x3BDC3F_b, 0x504C6F_b), sizeof(meth_table)); if (game::environment::is_sp()) { return; } developer_script = dvars::register_bool("developer_script", false, 0, "Enable developer script comments"); utils::hook::nop(SELECT_VALUE(0x3CB723_b, 0x512783_b), 8); utils::hook::call(SELECT_VALUE(0x3CB723_b, 0x512783_b), vm_call_builtin_function_stub); utils::hook::call(SELECT_VALUE(0x3CBA12_b, 0x512A72_b), get_entity_id_stub); utils::hook::nop(SELECT_VALUE(0x3CBA46_b, 0x512AA6_b), 6); utils::hook::nop(SELECT_VALUE(0x3CBA4E_b, 0x512AAE_b), 2); utils::hook::call(SELECT_VALUE(0x3CBA46_b, 0x512AA6_b), vm_call_builtin_method_stub); utils::hook::call(SELECT_VALUE(0x3CC9F3_b, 0x513A53_b), vm_error_stub); if (game::environment::is_dedi()) { function::add("isusingmatchrulesdata", [](const function_args& args) { // return 0 so the game doesn't override the cfg return 0; }); } function::add("print", [](const function_args& args) { print(args); return scripting::script_value{}; }); function::add("println", [](const function_args& args) { print(args); return scripting::script_value{}; }); function::add("assert", [](const function_args& args) { const auto expr = args[0].as(); if (!expr) { throw std::runtime_error("assert fail"); } return scripting::script_value{}; }); function::add("assertex", [](const function_args& args) { const auto expr = args[0].as(); if (!expr) { const auto error = args[1].as(); throw std::runtime_error(error); } return scripting::script_value{}; }); function::add("getfunction", [](const function_args& args) { const auto filename = args[0].as(); const auto function = args[1].as(); if (!scripting::script_function_table[filename].contains(function)) { throw std::runtime_error("function not found"); } return scripting::function{scripting::script_function_table[filename][function]}; }); function::add("replacefunc", [](const function_args& args) { const auto what = args[0].get_raw(); const auto with = args[1].get_raw(); if (what.type != game::SCRIPT_FUNCTION || with.type != game::SCRIPT_FUNCTION) { throw std::runtime_error("replaceFunc: parameter 1 must be a function"); } logfile::set_gsc_hook(what.u.codePosValue, with.u.codePosValue); return scripting::script_value{}; }); function::add("toupper", [](const function_args& args) { const auto string = args[0].as(); return utils::string::to_upper(string); }); function::add("logprint", [](const function_args& args) { std::string buffer{}; for (auto i = 0u; i < args.size(); ++i) { const auto string = args[i].as(); buffer.append(string); } game::G_LogPrintf("%s", buffer.data()); return scripting::script_value{}; }); function::add("executecommand", [](const function_args& args) { const auto cmd = args[0].as(); command::execute(cmd, true); return scripting::script_value{}; }); function::add("say", [](const function_args& args) { const auto message = args[0].as(); game::SV_GameSendServerCommand(-1, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s\"", 84, message.data())); return scripting::script_value{}; }); function::add("typeof", typeof); function::add("type", typeof); method::add("tell", [](const game::scr_entref_t ent, const function_args& args) { if (ent.classnum != 0) { throw std::runtime_error("Invalid entity"); } const auto client = ent.entnum; if (game::mp::g_entities[client].client == nullptr) { throw std::runtime_error("Not a player entity"); } const auto message = args[0].as(); game::SV_GameSendServerCommand(client, game::SV_CMD_CAN_IGNORE, utils::string::va("%c \"%s\"", 84, message.data())); return scripting::script_value{}; }); } }; } REGISTER_COMPONENT(gsc::extension)