#include #include "loader/component_loader.hpp" #include "game/game.hpp" #include "game/dvars.hpp" #include "game/scripting/functions.hpp" #include #include #include "component/console.hpp" #include "component/notifies.hpp" #include "component/command.hpp" #include "script_extension.hpp" #include "script_error.hpp" #include "script_loading.hpp" namespace gsc { std::uint16_t function_id_start = 0x25D; std::uint16_t method_id_start = 0x8429; void* func_table[0x1000]; namespace { #define RVA(ptr) static_cast(reinterpret_cast(ptr) - 0x140000000) struct gsc_error : public std::runtime_error { using std::runtime_error::runtime_error; }; std::unordered_map functions; bool force_error_print = false; std::optional gsc_error_msg; std::vector devmap_entries{}; std::optional get_devmap_entry(const std::uint8_t* codepos) { const auto itr = std::ranges::find_if(devmap_entries, [codepos](const devmap_entry& entry) -> bool { return codepos >= entry.bytecode && codepos < entry.bytecode + entry.size; }); if (itr != devmap_entries.end()) { return *itr; } return {}; } std::optional> get_line_and_col_for_codepos(const std::uint8_t* codepos) { const auto entry = get_devmap_entry(codepos); if (!entry.has_value()) { return {}; } std::optional> best_line_info{}; std::uint32_t best_codepos = 0; assert(codepos >= entry->bytecode); const std::uint32_t codepos_offset = static_cast(codepos - entry->bytecode); for (const auto& instruction : entry->devmap) { if (instruction.codepos > codepos_offset) { continue; } if (best_line_info.has_value() && codepos_offset - instruction.codepos > codepos_offset - best_codepos) { continue; } best_line_info = { { instruction.line, instruction.col } }; best_codepos = instruction.codepos; } return best_line_info; } unsigned int scr_get_function_stub(const char** p_name, int* type) { const auto result = game::Scr_GetFunction(p_name, type); for (const auto& [name, func] : functions) { game::Scr_RegisterFunction(func, 0, name); } return result; } void execute_custom_function(game::BuiltinFunction function) { auto error = false; try { function(); } catch (const std::exception& ex) { error = true; force_error_print = true; gsc_error_msg = ex.what(); } if (error) { game::Scr_ErrorInternal(); } } void vm_call_builtin_function(const std::uint32_t index) { const auto func = reinterpret_cast(scripting::get_function_by_index(index)); const auto custom = functions.contains(static_cast(index)); if (!custom) { func(); } else { execute_custom_function(func); } } void builtin_call_error(const std::string& error) { const auto pos = game::scr_function_stack->pos; const auto function_id = *reinterpret_cast(reinterpret_cast(pos - 2)); if (function_id > 0x1000) { console::warn("in call to builtin method \"%s\"%s", gsc_ctx->meth_name(function_id).data(), error.data()); } else { console::warn("in call to builtin function \"%s\"%s", gsc_ctx->func_name(function_id).data(), error.data()); } } std::optional get_opcode_name(const std::uint8_t opcode) { try { const auto index = gsc_ctx->opcode_enum(opcode); return {gsc_ctx->opcode_name(index)}; } 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); const char* location; if (function.has_value()) { location = utils::string::va("function \"%s\" in file \"%s\"", function.value().first.data(), function.value().second.data()); } else { location = utils::string::va("unknown location %p", pos); } const auto line_info = get_line_and_col_for_codepos(reinterpret_cast(pos)); if (line_info.has_value()) { location = utils::string::va("%s line \"%d\" column \"%d\"", location, line_info->first, line_info->second); } console::warn("\tat %s\n", location); } } void vm_error_stub(const unsigned __int64 mark_pos) { if (!dvars::com_developer_script->current.enabled && !force_error_print) { game::LargeLocalResetToMark(mark_pos); return; } console::warn("******* script runtime error ********\n"); const auto opcode_id = *reinterpret_cast(SELECT_VALUE(0x1455BE740, 0x144D57840)); const std::string error = gsc_error_msg.has_value() ? std::format(": {}", gsc_error_msg.value()) : std::string(); if ((opcode_id >= 0x1A && opcode_id <= 0x20) || (opcode_id >= 0xA8 && opcode_id <= 0xAE)) { builtin_call_error(error); } 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.data()); } else { console::warn("while processing instruction 0x%X%s\n", opcode_id, error.data()); } } force_error_print = false; gsc_error_msg = {}; print_callstack(); console::warn("************************************\n"); game::LargeLocalResetToMark(mark_pos); } void inc_in_param() { game::Scr_ClearOutParams(); if (game::scr_VmPub->top == game::scr_VmPub->maxstack) { game::Sys_Error("Internal script stack overflow"); } ++game::scr_VmPub->top; ++game::scr_VmPub->inparamcount; } void add_code_pos(const char* pos) { inc_in_param(); game::scr_VmPub->top->type = game::VAR_FUNCTION; game::scr_VmPub->top->u.codePosValue = pos; } void scr_print() { for (auto i = 0u; i < game::Scr_GetNumParam(); ++i) { console::info("%s", game::Scr_GetString(i)); } } void scr_print_ln() { for (auto i = 0u; i < game::Scr_GetNumParam(); ++i) { console::info("%s", game::Scr_GetString(i)); } console::info("\n"); } void assert_cmd() { if (!game::Scr_GetInt(0)) { scr_error("Assert fail"); } } void assert_ex_cmd() { if (!game::Scr_GetInt(0)) { scr_error(utils::string::va("Assert fail: %s", game::Scr_GetString(1))); } } void assert_msg_cmd() { scr_error(utils::string::va("Assert fail: %s", game::Scr_GetString(0))); } void scr_cmd_is_dedicated_server() { game::Scr_AddInt(game::environment::is_dedi()); } const char* get_code_pos(const int index) { if (static_cast(index) >= game::scr_VmPub->outparamcount) { scr_error("Scr_GetCodePos: index is out of range"); return ""; } const auto* value = &game::scr_VmPub->top[-index]; if (value->type != game::VAR_FUNCTION) { scr_error("Scr_GetCodePos requires a function as parameter"); return ""; } return value->u.codePosValue; } } void add_devmap_entry(std::uint8_t* codepos, std::size_t size, const std::string& name, xsk::gsc::buffer devmap_buf) { std::vector devmap{}; const auto* devmap_ptr = reinterpret_cast(devmap_buf.data); devmap.resize(devmap_ptr->num_instructions); std::memcpy(devmap.data(), devmap_ptr->instructions, sizeof(dev_map_instruction) * devmap_ptr->num_instructions); devmap_entries.emplace_back(codepos, size, name, std::move(devmap)); } void clear_devmap() { devmap_entries.clear(); } void add_function(const std::string& name, game::BuiltinFunction function) { ++function_id_start; functions[function_id_start] = function; gsc_ctx->func_add(name, function_id_start); } void scr_error(const char* error) { force_error_print = true; gsc_error_msg = error; game::Scr_ErrorInternal(); } class extension final : public component_interface { public: void post_unpack() override { utils::hook::set(SELECT_VALUE(0x14086F468, 0x1409E6CE8), scr_print); utils::hook::set(SELECT_VALUE(0x14086F480, 0x1409E6D00), scr_print_ln); utils::hook::set(SELECT_VALUE(0x1403D353B + 1, 0x14042E33B + 1), 0x1000); // Scr_RegisterFunction utils::hook::set(SELECT_VALUE(0x1403D3542 + 4, 0x14042E342 + 4), RVA(&func_table)); // Scr_RegisterFunction // utils::hook::set(SELECT_VALUE(0x1403E0BDD + 3, 0x14043BBBE + 3), RVA(&func_table)); // VM_Execute_0 utils::hook::inject(SELECT_VALUE(0x1403D38E4 + 3, 0x14042E734 + 3), &func_table); // Scr_BeginLoadScripts utils::hook::nop(SELECT_VALUE(0x1403E0BDD + 5, 0x14043BBBE + 5), 2); utils::hook::call(SELECT_VALUE(0x1403E0BDD, 0x14043BBBE), vm_call_builtin_function); utils::hook::call(SELECT_VALUE(0x1403D391F, 0x14042E76F), scr_get_function_stub); utils::hook::call(SELECT_VALUE(0x1403E1ED0, 0x14043CEB1), vm_error_stub); add_function("replacefunc", [] { if (scr_get_type(0) != game::VAR_FUNCTION || scr_get_type(1) != game::VAR_FUNCTION) { throw gsc_error("Parameter must be a function"); } notifies::set_gsc_hook(get_code_pos(0), get_code_pos(1)); }); add_function("executecommand", [] { const auto* cmd = game::Scr_GetString(0); command::execute(cmd); }); if (game::environment::is_sp()) { return; } utils::hook::set(0x1409E6E38, assert_ex_cmd); utils::hook::set(0x1409E6E50, assert_msg_cmd); utils::hook::set(0x1409E6E20, assert_cmd); utils::hook::set(0x1409E94D0, scr_cmd_is_dedicated_server); } }; } REGISTER_COMPONENT(gsc::extension)