From ddffc3806334bb1aaf7dda7441981639de73f263 Mon Sep 17 00:00:00 2001 From: fed <58637860+fedddddd@users.noreply.github.com> Date: Tue, 7 Mar 2023 19:34:33 +0100 Subject: [PATCH] Port new gsc interface from s1x --- .gitmodules | 6 +- deps/gsc-tool | 1 + deps/gsc-tool-h2 | 1 - deps/premake/gsc-tool.lua | 88 +- src/client/component/fastfiles.cpp | 2 - src/client/component/gsc.cpp | 789 ------------------ src/client/component/gsc.hpp | 8 - src/client/component/gsc/script_error.cpp | 326 ++++++++ src/client/component/gsc/script_error.hpp | 6 + src/client/component/gsc/script_extension.cpp | 328 ++++++++ src/client/component/gsc/script_extension.hpp | 11 + src/client/component/gsc/script_loading.cpp | 431 ++++++++++ src/client/component/gsc/script_loading.hpp | 12 + src/client/component/mapents.cpp | 5 +- src/client/component/notifies.hpp | 2 + src/client/component/scripting.cpp | 20 +- src/client/component/scripting.hpp | 1 + src/client/game/scripting/functions.cpp | 32 +- src/client/game/scripting/lua/context.cpp | 11 +- src/client/game/structs.hpp | 38 + src/client/game/symbols.hpp | 4 +- src/client/std_include.hpp | 3 + 22 files changed, 1239 insertions(+), 886 deletions(-) create mode 160000 deps/gsc-tool delete mode 160000 deps/gsc-tool-h2 delete mode 100644 src/client/component/gsc.cpp delete mode 100644 src/client/component/gsc.hpp create mode 100644 src/client/component/gsc/script_error.cpp create mode 100644 src/client/component/gsc/script_error.hpp create mode 100644 src/client/component/gsc/script_extension.cpp create mode 100644 src/client/component/gsc/script_extension.hpp create mode 100644 src/client/component/gsc/script_loading.cpp create mode 100644 src/client/component/gsc/script_loading.hpp diff --git a/.gitmodules b/.gitmodules index 1c83538a..0a1dba8b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,9 +43,9 @@ [submodule "deps/curl"] path = deps/curl url = https://github.com/curl/curl.git -[submodule "deps/gsc-tool-h2"] - path = deps/gsc-tool-h2 - url = https://github.com/fedddddd/gsc-tool-h2.git [submodule "deps/json"] path = deps/json url = https://github.com/nlohmann/json.git +[submodule "deps/gsc-tool"] + path = deps/gsc-tool + url = https://github.com/xensik/gsc-tool.git diff --git a/deps/gsc-tool b/deps/gsc-tool new file mode 160000 index 00000000..e05d853b --- /dev/null +++ b/deps/gsc-tool @@ -0,0 +1 @@ +Subproject commit e05d853ba6448212166b18847b430768d99d39e3 diff --git a/deps/gsc-tool-h2 b/deps/gsc-tool-h2 deleted file mode 160000 index c86b0b53..00000000 --- a/deps/gsc-tool-h2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c86b0b53921e91a300359f0f4da38e103641ef5a diff --git a/deps/premake/gsc-tool.lua b/deps/premake/gsc-tool.lua index 0c5367f4..eedc274a 100644 --- a/deps/premake/gsc-tool.lua +++ b/deps/premake/gsc-tool.lua @@ -1,68 +1,62 @@ gsc_tool = { - source = path.join(dependencies.basePath, "gsc-tool-h2/src") + source = path.join(dependencies.basePath, "gsc-tool"), } function gsc_tool.import() - links {"xsk-gsc-h2", "xsk-gsc-utils"} - gsc_tool.includes() + links { "xsk-gsc-h2", "xsk-gsc-utils" } + gsc_tool.includes() end function gsc_tool.includes() - includedirs { - path.join(gsc_tool.source, "utils"), - path.join(gsc_tool.source, "h2"), - path.join(dependencies.basePath, "extra/gsc-tool") -- https://github.com/GEEKiDoS/open-teknomw3/blob/master/deps/extra/gsc-tool - } + includedirs { + path.join(gsc_tool.source, "include"), + } end --- https://github.com/xensik/gsc-tool/blob/dev/premake5.lua#L95 function gsc_tool.project() - project "xsk-gsc-utils" - kind "StaticLib" - language "C++" + project "xsk-gsc-utils" + kind "StaticLib" + language "C++" - pchheader "stdafx.hpp" - pchsource(path.join(gsc_tool.source, "utils/stdafx.cpp")) + files { + path.join(gsc_tool.source, "include/xsk/utils/*.hpp"), + path.join(gsc_tool.source, "src/utils/*.cpp"), + } - files { - path.join(gsc_tool.source, "utils/**.h"), - path.join(gsc_tool.source, "utils/**.hpp"), - path.join(gsc_tool.source, "utils/**.cpp") - } + includedirs { + path.join(gsc_tool.source, "include"), + } - includedirs { - path.join(gsc_tool.source, "utils"), - gsc_tool.source - } + zlib.includes() - zlib.includes() + project "xsk-gsc-h2" + kind "StaticLib" + language "C++" - project "xsk-gsc-h2" - kind "StaticLib" - language "C++" + filter "action:vs*" + buildoptions "/Zc:__cplusplus" + filter {} - pchheader "stdafx.hpp" - pchsource(path.join(gsc_tool.source, "h2/stdafx.cpp")) + files { + path.join(gsc_tool.source, "include/xsk/stdinc.hpp"), - files { - path.join(gsc_tool.source, "h2/**.h"), - path.join(gsc_tool.source, "h2/**.hpp"), - path.join(gsc_tool.source, "h2/**.cpp"), - path.join(dependencies.basePath, "extra/gsc-tool/interface.cpp") - } + path.join(gsc_tool.source, "include/xsk/gsc/engine/h2.hpp"), + path.join(gsc_tool.source, "src/gsc/engine/h2.cpp"), - includedirs { - path.join(gsc_tool.source, "h2"), - gsc_tool.source, - path.join(dependencies.basePath, "extra/gsc-tool") - } + path.join(gsc_tool.source, "src/gsc/engine/h2_code.cpp"), + path.join(gsc_tool.source, "src/gsc/engine/h2_func.cpp"), + path.join(gsc_tool.source, "src/gsc/engine/h2_meth.cpp"), + path.join(gsc_tool.source, "src/gsc/engine/h2_token.cpp"), - -- https://github.com/xensik/gsc-tool/blob/dev/premake5.lua#L25 - -- adding these build options fixes a bunch of parser stuff - filter "action:vs*" - buildoptions "/bigobj" - buildoptions "/Zc:__cplusplus" - filter {} + path.join(gsc_tool.source, "src/gsc/*.cpp"), + + path.join(gsc_tool.source, "src/gsc/common/*.cpp"), + path.join(gsc_tool.source, "include/xsk/gsc/common/*.hpp"), + } + + includedirs { + path.join(gsc_tool.source, "include"), + } end -table.insert(dependencies, gsc_tool) +table.insert(dependencies, gsc_tool) \ No newline at end of file diff --git a/src/client/component/fastfiles.cpp b/src/client/component/fastfiles.cpp index 0bb8c80c..915a07d5 100644 --- a/src/client/component/fastfiles.cpp +++ b/src/client/component/fastfiles.cpp @@ -249,8 +249,6 @@ namespace fastfiles return reallocate_asset_pool(); } -#define RVA(ptr) static_cast(reinterpret_cast(ptr) - 0x140000000) - void reallocate_xmodel_pool() { // array used for DB_GetAllXAssetOfType, not big enough if many assets are added diff --git a/src/client/component/gsc.cpp b/src/client/component/gsc.cpp deleted file mode 100644 index 3f1234f3..00000000 --- a/src/client/component/gsc.cpp +++ /dev/null @@ -1,789 +0,0 @@ -#include -#include "loader/component_loader.hpp" - -#include "game/game.hpp" -#include "game/dvars.hpp" - -#include "console.hpp" -#include "filesystem.hpp" -#include "scripting.hpp" -#include "gsc.hpp" -#include "scheduler.hpp" -#include "fastfiles.hpp" -#include "command.hpp" - -#include "game/scripting/execution.hpp" -#include "game/scripting/functions.hpp" -#include "game/scripting/lua/error.hpp" - -#include "notifies.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace gsc -{ - void* func_table[0x1000]{}; - - namespace - { - game::dvar_t* developer_script = nullptr; - - auto compiler = ::gsc::compiler(); - auto decompiler = ::gsc::decompiler(); - auto assembler = ::gsc::assembler(); - auto disassembler = ::gsc::disassembler(); - - std::unordered_map main_handles; - std::unordered_map init_handles; - - std::unordered_map functions; - std::optional gsc_error; - - utils::memory::allocator scriptfile_allocator; - std::unordered_map loaded_scripts; - - struct - { - char* buf = nullptr; - char* pos = nullptr; - unsigned int size = 0x1000000; - } script_memory; - - char* allocate_buffer(size_t size) - { - if (script_memory.buf == nullptr) - { - script_memory.buf = game::PMem_AllocFromSource_NoDebug(script_memory.size, 4, 1, game::PMEM_SOURCE_SCRIPT); - script_memory.pos = script_memory.buf; - } - - if (script_memory.pos + size > script_memory.buf + script_memory.size) - { - game::Com_Error(game::ERR_FATAL, "Out of custom script memory"); - } - - const auto pos = script_memory.pos; - script_memory.pos += size; - return pos; - } - - void free_script_memory() - { - game::PMem_PopFromSource_NoDebug(script_memory.buf, script_memory.size, 4, 1, game::PMEM_SOURCE_SCRIPT); - script_memory.buf = nullptr; - script_memory.pos = nullptr; - } - - void clear() - { - main_handles.clear(); - init_handles.clear(); - loaded_scripts.clear(); - scriptfile_allocator.clear(); - free_script_memory(); - } - - bool read_scriptfile(const std::string& name, std::string* data) - { - if (filesystem::read_file(name, data)) - { - return true; - } - - const auto name_str = name.data(); - if (game::DB_XAssetExists(game::ASSET_TYPE_RAWFILE, name_str) && - !game::DB_IsXAssetDefault(game::ASSET_TYPE_RAWFILE, name_str)) - { - const auto asset = game::DB_FindXAssetHeader(game::ASSET_TYPE_RAWFILE, name.data(), false); - const auto len = game::DB_GetRawFileLen(asset.rawfile); - data->resize(len); - game::DB_GetRawBuffer(asset.rawfile, data->data(), len); - if (len > 0) - { - data->pop_back(); - } - - return true; - } - - return false; - } - - game::ScriptFile* load_custom_script(const char* file_name, const std::string& real_name) - { - if (loaded_scripts.find(real_name) != loaded_scripts.end()) - { - return loaded_scripts[real_name]; - } - - std::string source_buffer{}; - if (!read_scriptfile(real_name + ".gsc", &source_buffer)) - { - return nullptr; - } - - auto data = std::vector{source_buffer.begin(), source_buffer.end()}; - - try - { - compiler->compile(real_name, data); - } - catch (const std::exception& e) - { - - console::error("*********** script compile error *************\n"); - console::error("failed to compile '%s':\n%s", real_name.data(), e.what()); - console::error("**********************************************\n"); - return nullptr; - } - - auto assembly = compiler->output(); - - try - { - assembler->assemble(real_name, assembly); - } - catch (const std::exception& e) - { - console::error("*********** script compile error *************\n"); - console::error("failed to assemble '%s':\n%s", real_name.data(), e.what()); - console::error("**********************************************\n"); - return nullptr; - } - - const auto script_file_ptr = scriptfile_allocator.allocate(); - script_file_ptr->name = file_name; - - const auto stack = assembler->output_stack(); - script_file_ptr->len = static_cast(stack.size()); - - const auto script = assembler->output_script(); - script_file_ptr->bytecodeLen = static_cast(script.size()); - - script_file_ptr->buffer = scriptfile_allocator.allocate_array(stack.size() + 1); - std::memcpy(script_file_ptr->buffer, stack.data(), stack.size()); - - script_file_ptr->bytecode = allocate_buffer(script.size() + 1); - std::memcpy(script_file_ptr->bytecode, script.data(), script.size()); - - script_file_ptr->compressedLen = 0; - - loaded_scripts[real_name] = script_file_ptr; - - return script_file_ptr; - } - - void load_script(const std::string& name) - { - if (!game::Scr_LoadScript(name.data())) - { - return; - } - - const auto main_handle = game::Scr_GetFunctionHandle(name.data(), - xsk::gsc::h2::resolver::token_id("main")); - const auto init_handle = game::Scr_GetFunctionHandle(name.data(), - xsk::gsc::h2::resolver::token_id("init")); - - if (main_handle) - { - console::info("Loaded '%s::main'\n", name.data()); - main_handles[name] = main_handle; - } - - if (init_handle) - { - console::info("Loaded '%s::init'\n", name.data()); - init_handles[name] = init_handle; - } - } - - void load_scripts(const std::filesystem::path& root_dir, const std::string& subfolder) - { - std::filesystem::path script_dir = root_dir / subfolder; - if (!utils::io::directory_exists(script_dir.generic_string())) - { - return; - } - - const auto scripts = utils::io::list_files(script_dir.generic_string()); - for (const auto& script : scripts) - { - if (!script.ends_with(".gsc")) - { - continue; - } - - std::filesystem::path path(script); - const auto relative = path.lexically_relative(root_dir).generic_string(); - const auto base_name = relative.substr(0, relative.size() - 4); - - load_script(base_name); - } - } - - void load_gametype_script_stub(void* a1, void* a2) - { - utils::hook::invoke(0x1404E1400, a1, a2); - - fastfiles::enum_assets(game::ASSET_TYPE_RAWFILE, [](game::XAssetHeader header) - { - std::string name = header.rawfile->name; - - if (name.ends_with(".gsc") && name.starts_with("scripts/")) - { - const auto base_name = name.substr(0, name.size() - 4); - load_script(base_name); - } - }, true); - - const auto mapname = game::Dvar_FindVar("mapname"); - for (const auto& path : filesystem::get_search_paths()) - { - load_scripts(path, "scripts"); - load_scripts(path, "scripts/"s + mapname->current.string); - } - } - - void g_load_structs_stub() - { - for (auto& function_handle : main_handles) - { - console::info("Executing '%s::main'\n", function_handle.first.data()); - const auto thread = game::Scr_ExecThread(function_handle.second, 0); - game::RemoveRefToObject(thread); - } - - utils::hook::invoke(0x140510B40); - } - - void scr_load_level_stub() - { - utils::hook::invoke(0x1404FD130); - - for (auto& function_handle : init_handles) - { - console::info("Executing '%s::init'\n", function_handle.first.data()); - const auto thread = game::Scr_ExecThread(function_handle.second, 0); - game::RemoveRefToObject(thread); - } - } - - int db_is_xasset_default(int type, const char* name) - { - if (loaded_scripts.find(name) != loaded_scripts.end()) - { - return 0; - } - - return utils::hook::invoke(0x1404143C0, type, name); - } - - void db_get_raw_buffer_stub(const game::RawFile* rawfile, char* buf, const int size) - { - if (rawfile->len > 0 && rawfile->compressedLen == 0) - { - std::memset(buf, 0, size); - std::memcpy(buf, rawfile->buffer, std::min(rawfile->len, size)); - return; - } - - utils::hook::invoke(0x140413C40, rawfile, buf, size); - } - - std::optional> find_function(const char* pos) - { - for (const auto& file : scripting::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 {std::make_pair(i->first, file.first)}; - } - } - } - - 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\"", - function.value().first.data(), function.value().second.data()); - } - else - { - console::warn("\tat unknown location %p", pos); - } - } - } - - std::optional get_opcode_name(const std::uint8_t opcode) - { - try - { - return {xsk::gsc::h2::resolver::opcode_name(opcode)}; - } - catch (...) - { - return {}; - } - } - - 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", - xsk::gsc::h2::resolver::method_name(function_id).data(), error.data()); - } - else - { - console::warn("in call to builtin function \"%s\"%s", - xsk::gsc::h2::resolver::function_name(function_id).data(), error.data()); - } - } - - bool force_error_print = false; - void* vm_error_stub(void* a1) - { - if (!developer_script->current.enabled && !force_error_print) - { - return utils::hook::invoke(0x140614670, a1); - } - - console::warn("*********** script runtime error *************\n"); - - const auto opcode_id = *reinterpret_cast(0x14BAA93E8); - const std::string error = gsc_error.has_value() - ? utils::string::va(": %s", gsc_error.value().data()) - : ""; - - if ((opcode_id >= 0x1A && opcode_id <= 0x20) || (opcode_id >= 0xA9 && opcode_id <= 0xAF)) - { - 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 = {}; - - print_callstack(); - console::warn("**********************************************\n"); - return utils::hook::invoke(0x140614670, a1); - } - - std::string unknown_function_error{}; - void get_unknown_function_error(const char* code_pos) - { - const auto function = find_function(code_pos); - if (function.has_value()) - { - const auto& pos = function.value(); - unknown_function_error = utils::string::va( - "while processing function '%s' in script '%s':\nunknown script '%s'", - pos.first.data(), pos.second.data(), scripting::current_file.data() - ); - } - else - { - unknown_function_error = utils::string::va( - "unknown script '%s'", - scripting::current_file.data() - ); - } - } - - unsigned int current_filename{}; - std::string get_filename_name() - { - const auto filename_str = game::SL_ConvertToString( - static_cast(current_filename)); - const auto id = std::atoi(filename_str); - if (id == 0) - { - return filename_str; - } - - return scripting::get_token_single(id); - } - - - void get_unknown_function_error(unsigned int thread_name) - { - const auto filename = get_filename_name(); - const auto name = scripting::get_token_single(thread_name); - - unknown_function_error = utils::string::va( - "while processing script '%s':\nunknown function '%s::%s'", - scripting::current_file.data(), filename.data(), name.data() - ); - } - - void unknown_function_stub(const char* code_pos) - { - get_unknown_function_error(code_pos); - game::Com_Error(game::ERR_DROP, "script link error\n%s", - unknown_function_error.data()); - } - - unsigned int find_variable_stub(unsigned int parent_id, unsigned int thread_name) - { - const auto res = game::FindVariable(parent_id, thread_name); - if (!res) - { - get_unknown_function_error(thread_name); - game::Com_Error(game::ERR_DROP, "script link error\n%s", - unknown_function_error.data()); - } - return res; - } - - void register_gsc_functions_stub(void* a1, void* a2) - { - utils::hook::invoke(0x140509F20, a1, a2); - for (const auto& func : functions) - { - game::Scr_RegisterFunction(func.first, 0, func.second); - } - } - - scripting::script_value get_argument(int index) - { - if (index >= static_cast(game::scr_VmPub->outparamcount)) - { - return {}; - } - - return game::scr_VmPub->top[-index]; - } - - auto function_id_start = 0x320; - void add_function(const std::string& name, scripting::script_function function) - { - if (xsk::gsc::h2::resolver::find_function(name)) - { - const auto id = xsk::gsc::h2::resolver::function_id(name); - functions[function] = id; - } - else - { - const auto id = ++function_id_start; - xsk::gsc::h2::resolver::add_function(name, static_cast(id)); - functions[function] = id; - } - } - - void execute_custom_function(scripting::script_function function) - { - auto error = false; - - try - { - function({}); - } - catch (const std::exception& e) - { - error = true; - force_error_print = true; - gsc_error = e.what(); - } - - if (error) - { - game::Scr_ErrorInternal(); - } - } - - void vm_call_builtin_stub(scripting::script_function function) - { - auto custom = false; - { - custom = functions.find(function) != functions.end(); - } - - if (!custom) - { - function({}); - } - else - { - execute_custom_function(function); - } - } - - utils::hook::detour scr_emit_function_hook; - void scr_emit_function_stub(unsigned int filename, unsigned int thread_name, char* code_pos) - { - current_filename = filename; - scr_emit_function_hook.invoke(filename, thread_name, code_pos); - } - - std::string get_script_file_name(const std::string& name) - { - const auto id = xsk::gsc::h2::resolver::token_id(name); - if (id == 0) - { - return name; - } - - return std::to_string(id); - } - - std::vector decompile_scriptfile(const std::string& name, const std::string& real_name) - { - const auto* scriptfile = game::DB_FindXAssetHeader(game::ASSET_TYPE_SCRIPTFILE, name.data(), false).scriptfile; - if (scriptfile == nullptr) - { - throw std::runtime_error(std::format("couldn't load scriptfile '{}'", real_name)); - } - - console::info("Decompiling scriptfile '%s'\n", real_name.data()); - - std::vector stack{scriptfile->buffer, scriptfile->buffer + scriptfile->len}; - std::vector bytecode{scriptfile->bytecode, scriptfile->bytecode + scriptfile->bytecodeLen}; - - auto decompressed_stack = xsk::utils::zlib::decompress(stack, static_cast(stack.size())); - - disassembler->disassemble(name, bytecode, decompressed_stack); - auto output = disassembler->output(); - - decompiler->decompile(name, output); - - return decompiler->output(); - } - - void pmem_init_stub() - { - utils::hook::invoke(0x14061EC80); - - const auto type_0 = &game::g_scriptmem[0]; - const auto type_1 = &game::g_scriptmem[1]; - - const auto size_0 = 0x200000; // default size - const auto size_1 = 0x200000 + script_memory.size; - - const auto block = reinterpret_cast(VirtualAlloc(NULL, size_0 + size_1, MEM_RESERVE, PAGE_READWRITE)); - - type_0->buf = block; - type_0->size = size_0; - - type_1->buf = block + size_0; - type_1->size = size_1; - - utils::hook::set(0x14061EC72, size_0 + size_1); - } - } - - game::ScriptFile* find_script(game::XAssetType /*type*/, const char* name, int /*allow_create_default*/) - { - std::string real_name = name; - const auto id = static_cast(std::atoi(name)); - if (id) - { - real_name = xsk::gsc::h2::resolver::token_name(id); - } - - const auto script = load_custom_script(name, real_name); - if (script) - { - return script; - } - - return game::DB_FindXAssetHeader(game::ASSET_TYPE_SCRIPTFILE, name, 1).scriptfile; - } - - class component final : public component_interface - { - public: - void post_unpack() override - { - developer_script = dvars::register_bool("developer_script", false, 0, "Print GSC errors"); - - // Allow custom scripts to include other custom scripts - xsk::gsc::h2::resolver::init([](const auto& include_name) - { - const auto real_name = include_name + ".gsc"; - - std::string file_buffer; - if (!read_scriptfile(real_name, &file_buffer) || file_buffer.empty()) - { - const auto name = get_script_file_name(include_name); - if (game::DB_XAssetExists(game::ASSET_TYPE_SCRIPTFILE, name.data())) - { - return decompile_scriptfile(name, real_name); - } - else - { - throw std::runtime_error(std::format("couldn't load gsc file '{}'", real_name)); - } - } - - std::vector result; - result.assign(file_buffer.begin(), file_buffer.end()); - - return result; - }); - - utils::hook::call(0x1405C6177, find_script); - utils::hook::call(0x1405C6187, db_is_xasset_default); - - // Loads scripts with an uncompressed stack - utils::hook::call(0x1405C61E0, db_get_raw_buffer_stub); - - // load handles - utils::hook::call(0x1404E17B2, load_gametype_script_stub); - - // execute handles - utils::hook::call(0x1404C8F71, g_load_structs_stub); - utils::hook::call(0x1404C8F80, scr_load_level_stub); - - utils::hook::call(0x1405CB94F, vm_error_stub); - - utils::hook::call(0x1405BC583, unknown_function_stub); - utils::hook::call(0x1405BC5CF, unknown_function_stub); - utils::hook::call(0x1405BC6BA, find_variable_stub); - scr_emit_function_hook.create(0x1405BC5E0, scr_emit_function_stub); - - utils::hook::call(0x1405BCBAB, register_gsc_functions_stub); - utils::hook::set(0x1405BC7BC, 0x1000); // change builtin func count - -#define RVA(ptr) static_cast(reinterpret_cast(ptr) - 0x140000000) - utils::hook::set(0x1405BC7C2 + 4, RVA(&func_table)); - utils::hook::inject(0x1405BCB78 + 3, &func_table); - utils::hook::set(0x1405CA678 + 4, RVA(&func_table)); - - utils::hook::nop(0x1405CA683, 8); - utils::hook::call(0x1405CA683, vm_call_builtin_stub); - - // Increase script memory - utils::hook::call(0x1405A4798, pmem_init_stub); - - add_function("print", [](const game::scr_entref_t ref) - { - const auto num = game::Scr_GetNumParam(); - std::string buffer{}; - - for (auto i = 0; i < num; i++) - { - const auto str = game::Scr_GetString(i); - buffer.append(str); - buffer.append("\t"); - } - - printf("%s\n", buffer.data()); - }); - - add_function("assert", [](const game::scr_entref_t ref) - { - const auto expr = get_argument(0).as(); - if (!expr) - { - throw std::runtime_error("assert fail"); - } - }); - - add_function("assertex", [](const game::scr_entref_t ref) - { - const auto expr = get_argument(0).as(); - if (!expr) - { - const auto error = get_argument(1).as(); - throw std::runtime_error(error); - } - }); - - add_function("replacefunc", [](const game::scr_entref_t ref) - { - const auto what = get_argument(0).get_raw(); - const auto with = get_argument(1).get_raw(); - - if (what.type != game::SCRIPT_FUNCTION) - { - throw std::runtime_error("parameter 1 must be a function"); - } - - if (with.type != game::SCRIPT_FUNCTION) - { - throw std::runtime_error("parameter 2 must be a function"); - } - - notifies::set_gsc_hook(what.u.codePosValue, with.u.codePosValue); - }); - - add_function("getsoundlength", [](const game::scr_entref_t ref) - { - const auto name = get_argument(0); - if (!name.is()) - { - throw std::runtime_error("parameter 1 must be a string"); - } - - const auto name_str = name.as(); - const auto sound = game::DB_FindXAssetHeader(game::ASSET_TYPE_SOUND, name_str.data(), false).sound; - if (!sound || !sound->count || !sound->head->soundFile || sound->head->soundFile->type != game::SAT_STREAMED) - { - return game::Scr_AddInt(-1); - } - - return game::Scr_AddInt(sound->head->soundFile->u.streamSnd.totalMsec); - }); - - add_function("executecommand", [](const game::scr_entref_t ref) - { - const auto cmd = get_argument(0).as(); - command::execute(cmd); - }); - - scripting::on_shutdown([](bool free_scripts, bool post_shutdown) - { - if (free_scripts && post_shutdown) - { - xsk::gsc::h2::resolver::cleanup(); - clear(); - } - }); - } - }; -} - -REGISTER_COMPONENT(gsc::component) diff --git a/src/client/component/gsc.hpp b/src/client/component/gsc.hpp deleted file mode 100644 index 12277297..00000000 --- a/src/client/component/gsc.hpp +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -namespace gsc -{ - extern void* func_table[0x1000]; - - game::ScriptFile* find_script(game::XAssetType /*type*/, const char* name, int /*allow_create_default*/); -} diff --git a/src/client/component/gsc/script_error.cpp b/src/client/component/gsc/script_error.cpp new file mode 100644 index 00000000..e237ec6a --- /dev/null +++ b/src/client/component/gsc/script_error.cpp @@ -0,0 +1,326 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include "script_extension.hpp" +#include "script_error.hpp" + +#include "component/scripting.hpp" + +#include +#include + +namespace gsc +{ + namespace + { + utils::hook::detour scr_emit_function_hook; + + unsigned int current_filename = 0; + + std::string unknown_function_error; + + std::array var_typename = + { + "undefined", + "object", + "string", + "localized string", + "vector", + "float", + "int", + "codepos", + "precodepos", + "function", + "builtin function", + "builtin method", + "stack", + "animation", + "pre animation", + "thread", + "thread", + "thread", + "thread", + "struct", + "removed entity", + "entity", + "array", + "removed thread", + "", + "thread list", + "endon list", + }; + + void scr_emit_function_stub(unsigned int filename, unsigned int thread_name, char* code_pos) + { + current_filename = filename; + scr_emit_function_hook.invoke(filename, thread_name, code_pos); + } + + std::string get_filename_name() + { + const auto filename_str = game::SL_ConvertToString(static_cast(current_filename)); + const auto id = std::atoi(filename_str); + if (!id) + { + return filename_str; + } + + return scripting::get_token(id); + } + + void get_unknown_function_error(const char* code_pos) + { + const auto function = find_function(code_pos); + if (function.has_value()) + { + const auto& pos = function.value(); + unknown_function_error = std::format( + "while processing function '{}' in script '{}':\nunknown script '{}'", pos.first, pos.second, scripting::current_file + ); + } + else + { + unknown_function_error = std::format("unknown script '{}'", scripting::current_file); + } + } + + void get_unknown_function_error(unsigned int thread_name) + { + const auto filename = get_filename_name(); + const auto name = scripting::get_token(thread_name); + + unknown_function_error = std::format( + "while processing script '{}':\nunknown function '{}::{}'", scripting::current_file, filename, name + ); + } + + void compile_error_stub(const char* code_pos, [[maybe_unused]] const char* msg) + { + get_unknown_function_error(code_pos); + game::Com_Error(game::ERR_DROP, "script link error\n%s", unknown_function_error.data()); + } + + unsigned int find_variable_stub(unsigned int parent_id, unsigned int thread_name) + { + const auto res = game::FindVariable(parent_id, thread_name); + if (!res) + { + get_unknown_function_error(thread_name); + game::Com_Error(game::ERR_DROP, "script link error\n%s", unknown_function_error.data()); + } + + return res; + } + + unsigned int scr_get_object(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_POINTER) + { + return value->u.pointerValue; + } + + scr_error(false, "Type %s is not an object", var_typename[value->type]); + } + + scr_error(false, "parameter %u does not exist", index + 1); + return 0; + } + + unsigned int scr_get_const_string(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (game::Scr_CastString(value)) + { + assert(value->type == game::VAR_STRING); + return value->u.stringValue; + } + + game::Scr_ErrorInternal(); + } + + scr_error(false, "parameter %u does not exist", index + 1); + return 0; + } + + unsigned int scr_get_const_istring(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_ISTRING) + { + return value->u.stringValue; + } + + scr_error(false, "type %s is not a localized string", var_typename[value->type]); + } + + scr_error(false, "parameter %u does not exist", index + 1); + return 0; + } + + void scr_validate_localized_string_ref(int parm_index, const char* token, int token_len) + { + assert(token); + assert(token_len >= 0); + + if (token_len < 2) + { + return; + } + + for (auto char_iter = 0; char_iter < token_len; ++char_iter) + { + if (!std::isalnum(static_cast(token[char_iter])) && token[char_iter] != '_') + { + scr_error(false, "illegal localized string reference: %s must contain only alpha-numeric characters and underscores", token); + } + } + } + + void scr_get_vector(unsigned int index, float* vector_value) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_VECTOR) + { + std::memcpy(vector_value, value->u.vectorValue, sizeof(std::float_t[3])); + return; + } + + scr_error(false, "type %s is not a vector", var_typename[value->type]); + } + + scr_error(false, "parameter %u does not exist", index + 1); + } + + int scr_get_int(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_INTEGER) + { + return value->u.intValue; + } + + scr_error(false, "type %s is not an int", var_typename[value->type]); + } + + scr_error(false, "parameter %u does not exist", index + 1); + return 0; + } + + float scr_get_float(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_FLOAT) + { + return value->u.floatValue; + } + + if (value->type == game::VAR_INTEGER) + { + return static_cast(value->u.intValue); + } + + scr_error(false, "type %s is not a float", var_typename[value->type]); + } + + scr_error(false, "parameter %u does not exist", index + 1); + return 0.0f; + } + + int scr_get_pointer_type(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + if ((game::scr_VmPub->top - index)->type == game::VAR_POINTER) + { + return static_cast(game::GetObjectType((game::scr_VmPub->top - index)->u.uintValue)); + } + + scr_error(false, "type %s is not an object", var_typename[(game::scr_VmPub->top - index)->type]); + } + + scr_error(false, "parameter %u does not exist", index + 1); + return 0; + } + + int scr_get_type(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + return (game::scr_VmPub->top - index)->type; + } + + scr_error(false, "parameter %u does not exist", index + 1); + return 0; + } + + const char* scr_get_type_name(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + return var_typename[(game::scr_VmPub->top - index)->type]; + } + + scr_error(false, "parameter %u does not exist", index + 1); + return nullptr; + } + } + + std::optional> find_function(const char* pos) + { + for (const auto& file : scripting::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 {std::make_pair(i->first, file.first)}; + } + } + } + + return {}; + } + + class error final : public component_interface + { + public: + void post_unpack() override + { + scr_emit_function_hook.create(0x1405BC5E0, scr_emit_function_stub); + + utils::hook::call(0x1405BC583, compile_error_stub); // LinkFile + utils::hook::call(0x1405BC5CF, compile_error_stub); // LinkFile + utils::hook::call(0x1405BC6BA, find_variable_stub); // Scr_EmitFunction + + // Restore basic error messages for commonly used scr functions + utils::hook::jump(0x1405C7950, scr_get_object); + utils::hook::jump(0x1405C7420, scr_get_const_string); + utils::hook::jump(0x1405C71E0, scr_get_const_istring); + utils::hook::jump(0x1404FEED0, scr_validate_localized_string_ref); + utils::hook::jump(0x1405C7E90, scr_get_vector); + utils::hook::jump(0x1405C7890, scr_get_int); + utils::hook::jump(0x1405C7730, scr_get_float); + + utils::hook::jump(0x1405C7B70, scr_get_pointer_type); + utils::hook::jump(0x1405C7D40, scr_get_type); + utils::hook::jump(0x1405C7DB0, scr_get_type_name); + } + }; +} + +REGISTER_COMPONENT(gsc::error) diff --git a/src/client/component/gsc/script_error.hpp b/src/client/component/gsc/script_error.hpp new file mode 100644 index 00000000..284af439 --- /dev/null +++ b/src/client/component/gsc/script_error.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace gsc +{ + std::optional> find_function(const char* pos); +} diff --git a/src/client/component/gsc/script_extension.cpp b/src/client/component/gsc/script_extension.cpp new file mode 100644 index 00000000..b6710c49 --- /dev/null +++ b/src/client/component/gsc/script_extension.cpp @@ -0,0 +1,328 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "script_error.hpp" +#include "script_extension.hpp" +#include "script_loading.hpp" + +#include "component/console.hpp" +#include "component/command.hpp" +#include "component/notifies.hpp" + +#include "game/scripting/script_value.hpp" + +#include +#include + +namespace gsc +{ + std::uint16_t function_id_start = 0x320; + void* func_table[FUNC_TABLE_SIZE]; + + namespace + { + 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; + + utils::hook::detour scr_register_function_hook; + + unsigned int scr_get_function_stub(const char** p_name, int* type) + { + const auto result = utils::hook::invoke(0x140509F20, p_name, type); + + for (const auto& [func, id] : functions) + { + game::Scr_RegisterFunction(func, 0, id); + } + + return result; + } + + void execute_custom_function(game::BuiltinFunction function) + { + auto error = false; + + try + { + function(); + } + catch (const std::exception& e) + { + error = true; + force_error_print = true; + gsc_error_msg = e.what(); + } + + if (error) + { + game::Scr_ErrorInternal(); + } + } + + bool is_in_game_memory_range(uintptr_t ptr) + { + return ptr <= BASE_ADDRESS + BINARY_PAYLOAD_SIZE; + } + + bool is_in_game_memory_range(void* ptr) + { + return is_in_game_memory_range(reinterpret_cast(ptr)); + } + + void vm_call_builtin_function(const game::BuiltinFunction function) + { + const auto custom = !is_in_game_memory_range(function); + if (!custom) + { + function(); + } + else + { + execute_custom_function(function); + } + } + + 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 > FUNC_TABLE_SIZE) + { + 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); + + 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); + } + } + } + + bool is_call_opcode(const std::uint8_t opcode_id) + { + return (opcode_id >= 0x1A && opcode_id <= 0x20) || (opcode_id >= 0xA9 && opcode_id <= 0xAF); + } + + void vm_error_stub(int mark_pos) + { + if (!developer_script->current.enabled && !force_error_print) + { + utils::hook::invoke(0x140614670, mark_pos); + return; + } + + console::warn("******* script runtime error ********\n"); + const auto opcode_id = *reinterpret_cast(0x14BAA93E8); + + const std::string error = gsc_error_msg.has_value() ? std::format(": {}", gsc_error_msg.value()) : std::string(); + + if (is_call_opcode(opcode_id)) + { + 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"); + utils::hook::invoke(0x140614670, mark_pos); + } + + void scr_print() + { + const auto num = game::Scr_GetNumParam(); + std::string buffer{}; + + for (auto i = 0u; i < num; i++) + { + const auto str = game::Scr_GetString(i); + buffer.append(str); + buffer.append("\t"); + } + + console::info("%s\n", buffer.data()); + } + + void assert_cmd() + { + if (!game::Scr_GetInt(0)) + { + scr_error(true, "assert fail"); + } + } + + void assert_ex_cmd() + { + if (!game::Scr_GetInt(0)) + { + scr_error(true, "assert fail: %s", game::Scr_GetString(1)); + } + } + + void assert_msg_cmd() + { + scr_error(true, "assert fail: %s", game::Scr_GetString(0)); + } + + scripting::script_value get_argument(int index) + { + if (index >= static_cast(game::scr_VmPub->outparamcount)) + { + return {}; + } + + return game::scr_VmPub->top[-index]; + } + } + + void scr_error(bool force_print, const char* fmt, ...) + { + { + char buffer[2048]{}; + + va_list ap; + va_start(ap, fmt); + + vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, fmt, ap); + + va_end(ap); + + force_error_print = force_print; + gsc_error_msg = buffer; + } + + game::Scr_ErrorInternal(); + } + + void add_function(const std::string& name, game::BuiltinFunction function) + { + if (gsc_ctx->func_exists(name)) + { + const auto id = gsc_ctx->func_id(name); + functions[function] = id; + } + else + { + const auto id = ++function_id_start; + functions[function] = id; + gsc_ctx->func_add(name, id); + } + } + + class extension final : public component_interface + { + public: + void post_unpack() override + { + utils::hook::set(0x1405BC7BC, FUNC_TABLE_SIZE); // change builtin func count + + utils::hook::set(0x1405BC7C2 + 4, RVA(&func_table)); + utils::hook::inject(0x1405BCB78 + 3, &func_table); + utils::hook::set(0x1405CA678 + 4, RVA(&func_table)); + + utils::hook::nop(0x1405CA683, 8); + utils::hook::call(0x1405CA683, vm_call_builtin_function); + + utils::hook::call(0x1405CB94F, vm_error_stub); // LargeLocalResetToMark + + utils::hook::call(0x1405BCBAB, scr_get_function_stub); + + add_function("print", scr_print); + add_function("println", scr_print); + + add_function("assert", assert_cmd); + add_function("assertex", assert_ex_cmd); + add_function("assertmsg", assert_msg_cmd); + + add_function("replacefunc", []() + { + const auto what = get_argument(0).get_raw(); + const auto with = get_argument(1).get_raw(); + + if (what.type != game::SCRIPT_FUNCTION) + { + throw std::runtime_error("parameter 1 must be a function"); + } + + if (with.type != game::SCRIPT_FUNCTION) + { + throw std::runtime_error("parameter 2 must be a function"); + } + + notifies::set_gsc_hook(what.u.codePosValue, with.u.codePosValue); + }); + + add_function("getsoundlength", []() + { + const auto name = game::Scr_GetString(0); + const auto sound = game::DB_FindXAssetHeader(game::ASSET_TYPE_SOUND, name, false).sound; + if (!sound || !sound->count || !sound->head->soundFile || sound->head->soundFile->type != game::SAT_STREAMED) + { + return game::Scr_AddInt(-1); + } + + return game::Scr_AddInt(sound->head->soundFile->u.streamSnd.totalMsec); + }); + + add_function("executecommand", []() + { + const auto cmd = game::Scr_GetString(0); + command::execute(cmd); + }); + } + }; +} + +REGISTER_COMPONENT(gsc::extension) diff --git a/src/client/component/gsc/script_extension.hpp b/src/client/component/gsc/script_extension.hpp new file mode 100644 index 00000000..1c9f7dd5 --- /dev/null +++ b/src/client/component/gsc/script_extension.hpp @@ -0,0 +1,11 @@ +#pragma once + +#define FUNC_TABLE_SIZE 0x1000 + +namespace gsc +{ + extern void* func_table[FUNC_TABLE_SIZE]; + + void scr_error(bool force_print, const char* fmt, ...); + void add_function(const std::string& name, game::BuiltinFunction function); +} diff --git a/src/client/component/gsc/script_loading.cpp b/src/client/component/gsc/script_loading.cpp new file mode 100644 index 00000000..a72e8091 --- /dev/null +++ b/src/client/component/gsc/script_loading.cpp @@ -0,0 +1,431 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "component/filesystem.hpp" +#include "component/console.hpp" +#include "component/scripting.hpp" +#include "component/fastfiles.hpp" + +#include "script_loading.hpp" + +#include +#include +#include +#include + +namespace gsc +{ + std::unique_ptr gsc_ctx; + game::dvar_t* developer_script = nullptr; + + namespace + { + std::unordered_map main_handles; + std::unordered_map init_handles; + + std::unordered_map loaded_scripts; + utils::memory::allocator script_allocator; + + struct + { + char* buf = nullptr; + char* pos = nullptr; + unsigned int size = 0x1000000; + } script_memory; + + char* allocate_buffer(size_t size) + { + if (script_memory.buf == nullptr) + { + script_memory.buf = game::PMem_AllocFromSource_NoDebug(script_memory.size, 4, 1, game::PMEM_SOURCE_SCRIPT); + script_memory.pos = script_memory.buf; + } + + if (script_memory.pos + size > script_memory.buf + script_memory.size) + { + game::Com_Error(game::ERR_FATAL, "Out of custom script memory"); + } + + const auto pos = script_memory.pos; + script_memory.pos += size; + return pos; + } + + void free_script_memory() + { + game::PMem_PopFromSource_NoDebug(script_memory.buf, script_memory.size, 4, 1, game::PMEM_SOURCE_SCRIPT); + script_memory.buf = nullptr; + script_memory.pos = nullptr; + } + + void clear() + { + main_handles.clear(); + init_handles.clear(); + loaded_scripts.clear(); + script_allocator.clear(); + free_script_memory(); + } + + bool read_raw_script_file(const std::string& name, std::string* data) + { + if (filesystem::read_file(name, data)) + { + return true; + } + + const auto name_str = name.data(); + if (game::DB_XAssetExists(game::ASSET_TYPE_RAWFILE, name_str) && + !game::DB_IsXAssetDefault(game::ASSET_TYPE_RAWFILE, name_str)) + { + const auto asset = game::DB_FindXAssetHeader(game::ASSET_TYPE_RAWFILE, name.data(), false); + const auto len = game::DB_GetRawFileLen(asset.rawfile); + data->resize(len); + game::DB_GetRawBuffer(asset.rawfile, data->data(), len); + if (len > 0) + { + data->pop_back(); + } + + return true; + } + + return false; + } + + game::ScriptFile* load_custom_script(const char* file_name, const std::string& real_name) + { + if (const auto itr = loaded_scripts.find(real_name); itr != loaded_scripts.end()) + { + return itr->second; + } + + try + { + auto& compiler = gsc_ctx->compiler(); + auto& assembler = gsc_ctx->assembler(); + + std::string source_buffer{}; + if (!read_raw_script_file(real_name + ".gsc", &source_buffer)) + { + return nullptr; + } + + std::vector data; + data.assign(source_buffer.begin(), source_buffer.end()); + + const auto assembly_ptr = compiler.compile(real_name, data); + const auto output_script = assembler.assemble(*assembly_ptr); + + const auto script_file_ptr = static_cast(script_allocator.allocate(sizeof(game::ScriptFile))); + script_file_ptr->name = file_name; + + script_file_ptr->len = static_cast(output_script.second.size); + script_file_ptr->bytecodeLen = static_cast(output_script.first.size); + + const auto stack_size = static_cast(output_script.second.size + 1); + const auto byte_code_size = static_cast(output_script.first.size + 1); + + script_file_ptr->buffer = static_cast(script_allocator.allocate(stack_size)); + std::memcpy(const_cast(script_file_ptr->buffer), output_script.second.data, output_script.second.size); + + script_file_ptr->bytecode = allocate_buffer(byte_code_size); + std::memcpy(script_file_ptr->bytecode, output_script.first.data, output_script.first.size); + + script_file_ptr->compressedLen = 0; + + loaded_scripts[real_name] = script_file_ptr; + + return script_file_ptr; + } + catch (const std::exception& ex) + { + console::error("*********** script compile error *************\n"); + console::error("failed to compile '%s':\n%s", real_name.data(), ex.what()); + console::error("**********************************************\n"); + return nullptr; + } + } + + std::string get_raw_script_file_name(const std::string& name) + { + if (name.ends_with(".gsh")) + { + return name; + } + + return name + ".gsc"; + } + + std::string get_script_file_name(const std::string& name) + { + const auto id = gsc_ctx->token_id(name); + if (!id) + { + return name; + } + + return std::to_string(id); + } + + auto read_compiled_script_file(const std::string& name, const std::string& real_name) + { + const auto* script_file = game::DB_FindXAssetHeader(game::ASSET_TYPE_SCRIPTFILE, name.data(), false).scriptfile; + if (!script_file) + { + throw std::runtime_error(std::format("Could not load scriptfile '{}'", real_name)); + } + + const auto len = script_file->compressedLen; + const std::string stack{script_file->buffer, static_cast(len)}; + + const auto decompressed_stack = utils::compression::zlib::decompress(stack); + + std::vector stack_data; + stack_data.assign(decompressed_stack.begin(), decompressed_stack.end()); + + const xsk::gsc::buffer buffer{reinterpret_cast(script_file->bytecode), static_cast(script_file->bytecodeLen)}; + + return std::make_pair(buffer, stack_data); + } + + void load_script(const std::string& name) + { + if (!game::Scr_LoadScript(name.data())) + { + return; + } + + const auto main_handle = game::Scr_GetFunctionHandle(name.data(), gsc_ctx->token_id("main")); + const auto init_handle = game::Scr_GetFunctionHandle(name.data(), gsc_ctx->token_id("init")); + + if (main_handle) + { + console::info("Loaded '%s::main'\n", name.data()); + main_handles[name] = main_handle; + } + + if (init_handle) + { + console::info("Loaded '%s::init'\n", name.data()); + init_handles[name] = init_handle; + } + } + + void load_scripts(const std::filesystem::path& root_dir, const std::string& subfolder) + { + std::filesystem::path script_dir = root_dir / subfolder; + if (!utils::io::directory_exists(script_dir.generic_string())) + { + return; + } + + const auto scripts = utils::io::list_files(script_dir.generic_string()); + for (const auto& script : scripts) + { + if (!script.ends_with(".gsc")) + { + continue; + } + + std::filesystem::path path(script); + const auto relative = path.lexically_relative(root_dir).generic_string(); + const auto base_name = relative.substr(0, relative.size() - 4); + + load_script(base_name); + } + } + + int db_is_x_asset_default(game::XAssetType type, const char* name) + { + if (loaded_scripts.contains(name)) + { + return 0; + } + + return game::DB_IsXAssetDefault(type, name); + } + + void load_gametype_script_stub(void* a1, void* a2) + { + utils::hook::invoke(0x1404E1400, a1, a2); + + fastfiles::enum_assets(game::ASSET_TYPE_RAWFILE, [](game::XAssetHeader header) + { + std::string name = header.rawfile->name; + + if (name.ends_with(".gsc") && name.starts_with("scripts/")) + { + const auto base_name = name.substr(0, name.size() - 4); + load_script(base_name); + } + }, true); + + const auto mapname = game::Dvar_FindVar("mapname"); + for (const auto& path : filesystem::get_search_paths()) + { + load_scripts(path, "scripts"); + load_scripts(path, "scripts/"s + mapname->current.string); + } + } + + void db_get_raw_buffer_stub(const game::RawFile* rawfile, char* buf, const int size) + { + if (rawfile->len > 0 && rawfile->compressedLen == 0) + { + std::memset(buf, 0, size); + std::memcpy(buf, rawfile->buffer, std::min(rawfile->len, size)); + return; + } + + game::DB_GetRawBuffer(rawfile, buf, size); + } + + void g_load_structs_stub() + { + for (auto& function_handle : main_handles) + { + console::info("Executing '%s::main'\n", function_handle.first.data()); + const auto thread = game::Scr_ExecThread(function_handle.second, 0); + game::RemoveRefToObject(thread); + } + + utils::hook::invoke(0x140510B40); + } + + void scr_load_level_stub() + { + utils::hook::invoke(0x1404FD130); + + for (auto& function_handle : init_handles) + { + console::info("Executing '%s::init'\n", function_handle.first.data()); + const auto thread = game::Scr_ExecThread(function_handle.second, 0); + game::RemoveRefToObject(thread); + } + } + + void scr_begin_load_scripts_stub() + { + const auto comp_mode = developer_script->current.enabled + ? xsk::gsc::build::dev + : xsk::gsc::build::prod; + + gsc_ctx->init(comp_mode, [](const std::string& include_name) + -> std::pair> + { + const auto real_name = get_raw_script_file_name(include_name); + + std::string file_buffer; + if (!read_raw_script_file(real_name, &file_buffer) || file_buffer.empty()) + { + const auto name = get_script_file_name(include_name); + if (game::DB_XAssetExists(game::ASSET_TYPE_SCRIPTFILE, name.data())) + { + return read_compiled_script_file(name, real_name); + } + + throw std::runtime_error(std::format("Could not load gsc file '{}'", real_name)); + } + + std::vector script_data; + script_data.assign(file_buffer.begin(), file_buffer.end()); + + return {{}, script_data}; + }); + + utils::hook::invoke(0x1405BCAE0); + } + + void sl_end_load_scripts_stub() + { + gsc_ctx->cleanup(); + utils::hook::invoke(0x1405BFBF0); + } + + void pmem_init_stub() + { + utils::hook::invoke(0x14061EC80); + + const auto type_0 = &game::g_scriptmem[0]; + const auto type_1 = &game::g_scriptmem[1]; + + const auto size_0 = 0x200000; // default size + const auto size_1 = 0x200000 + script_memory.size; + + const auto block = reinterpret_cast(VirtualAlloc(NULL, size_0 + size_1, MEM_RESERVE, PAGE_READWRITE)); + + type_0->buf = block; + type_0->size = size_0; + + type_1->buf = block + size_0; + type_1->size = size_1; + + utils::hook::set(0x14061EC72, size_0 + size_1); + } + } + + game::ScriptFile* find_script(game::XAssetType type, const char* name, int allow_create_default) + { + std::string real_name = name; + const auto id = static_cast(std::strtol(name, nullptr, 10)); + if (id) + { + real_name = gsc_ctx->token_name(id); + } + + auto* script = load_custom_script(name, real_name); + if (script) + { + return script; + } + + return game::DB_FindXAssetHeader(type, name, allow_create_default).scriptfile; + } + + class loading final : public component_interface + { + public: + void post_load() override + { + gsc_ctx = std::make_unique(); + } + + void post_unpack() override + { + utils::hook::call(0x1404E1627, scr_begin_load_scripts_stub); + utils::hook::call(0x1405BCC14, sl_end_load_scripts_stub); + + developer_script = dvars::register_bool("developer_script", false, game::DVAR_FLAG_NONE, "Enable developer script comments"); + + utils::hook::call(0x1405C6177, find_script); + utils::hook::call(0x1405C6187, db_is_x_asset_default); + + // Loads scripts with an uncompressed stack + utils::hook::call(0x1405C61E0, db_get_raw_buffer_stub); + + // load handles + utils::hook::call(0x1404E17B2, load_gametype_script_stub); + + // execute handles + utils::hook::call(0x1404C8F71, g_load_structs_stub); + utils::hook::call(0x1404C8F80, scr_load_level_stub); + + // increase script memory + utils::hook::call(0x1405A4798, pmem_init_stub); + + scripting::on_shutdown([](bool free_scripts, bool post_shutdown) + { + if (free_scripts && post_shutdown) + { + clear(); + } + }); + } + }; +} + +REGISTER_COMPONENT(gsc::loading) diff --git a/src/client/component/gsc/script_loading.hpp b/src/client/component/gsc/script_loading.hpp new file mode 100644 index 00000000..9bda4f47 --- /dev/null +++ b/src/client/component/gsc/script_loading.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "game/scripting/functions.hpp" +#include + +namespace gsc +{ + extern std::unique_ptr gsc_ctx; + extern game::dvar_t* developer_script; + + game::ScriptFile* find_script(game::XAssetType type, const char* name, int allow_create_default); +} diff --git a/src/client/component/mapents.cpp b/src/client/component/mapents.cpp index bf9988ef..d5e7c2c3 100644 --- a/src/client/component/mapents.cpp +++ b/src/client/component/mapents.cpp @@ -8,9 +8,6 @@ #include "command.hpp" #include "game/scripting/functions.hpp" -#include -#include - #include #include #include @@ -50,7 +47,7 @@ namespace mapents continue; } - const auto token = xsk::gsc::h2::resolver::token_name(static_cast(id)); + const auto token = scripting::find_token_single(static_cast(id)); const auto key = "\"" + token + "\""; const auto new_line = key + line.substr(first_space); diff --git a/src/client/component/notifies.hpp b/src/client/component/notifies.hpp index 9ea66a9c..410e4590 100644 --- a/src/client/component/notifies.hpp +++ b/src/client/component/notifies.hpp @@ -1,5 +1,7 @@ #pragma once +#include "game/scripting/lua/error.hpp" + namespace notifies { extern bool hook_enabled; diff --git a/src/client/component/scripting.cpp b/src/client/component/scripting.cpp index 03a1a214..1aacf55a 100644 --- a/src/client/component/scripting.cpp +++ b/src/client/component/scripting.cpp @@ -162,9 +162,9 @@ namespace scripting { auto result = scripting::find_token(id); - if (canonical_string_table.find(id) != canonical_string_table.end()) + if (const auto itr = canonical_string_table.find(id); itr != canonical_string_table.end()) { - result.push_back(canonical_string_table[id]); + result.push_back(itr->second); } return result; @@ -323,12 +323,22 @@ namespace scripting std::optional get_canonical_string(const unsigned int id) { - if (canonical_string_table.find(id) == canonical_string_table.end()) + if (const auto itr = canonical_string_table.find(id); itr != canonical_string_table.end()) { - return {}; + return itr->second; } - return {canonical_string_table[id]}; + return {}; + } + + std::string get_token(unsigned int id) + { + if (const auto itr = canonical_string_table.find(id); itr != canonical_string_table.end()) + { + return itr->second; + } + + return find_token_single(id); } class component final : public component_interface diff --git a/src/client/component/scripting.hpp b/src/client/component/scripting.hpp index 3f0d5b64..2f4dca3f 100644 --- a/src/client/component/scripting.hpp +++ b/src/client/component/scripting.hpp @@ -17,4 +17,5 @@ namespace scripting void on_shutdown(const std::function& callback); std::optional get_canonical_string(const unsigned int id); std::string get_token_single(unsigned int id); + std::string get_token(unsigned int id); } \ No newline at end of file diff --git a/src/client/game/scripting/functions.cpp b/src/client/game/scripting/functions.cpp index 8f11e151..a88f8181 100644 --- a/src/client/game/scripting/functions.cpp +++ b/src/client/game/scripting/functions.cpp @@ -3,8 +3,8 @@ #include "../../component/gsc.hpp" -#include -#include +#include "component/gsc/script_extension.hpp" +#include "component/gsc/script_loading.hpp" #include @@ -15,23 +15,17 @@ namespace scripting int find_function_index(const std::string& name, const bool prefer_global) { const auto target = utils::string::to_lower(name); - auto first = xsk::gsc::h2::resolver::function_id; - auto second = xsk::gsc::h2::resolver::method_id; - if (!prefer_global) + auto const& first = gsc::gsc_ctx->func_map(); + auto const& second = gsc::gsc_ctx->meth_map(); + + if (const auto itr = first.find(name); itr != first.end()) { - std::swap(first, second); + return static_cast(itr->second); } - const auto first_res = first(target); - if (first_res) + if (const auto itr = second.find(name); itr != second.end()) { - return first_res; - } - - const auto second_res = second(target); - if (second_res) - { - return second_res; + return static_cast(itr->second); } return -1; @@ -67,20 +61,20 @@ namespace scripting results.push_back(utils::string::va("_ID%i", id)); results.push_back(utils::string::va("_id_%04X", id)); - results.push_back(xsk::gsc::h2::resolver::token_name(static_cast(id))); + results.push_back(gsc::gsc_ctx->token_name(id)); return results; } std::string find_token_single(unsigned int id) { - return xsk::gsc::h2::resolver::token_name(static_cast(id)); + return gsc::gsc_ctx->token_name(id); } unsigned int find_token_id(const std::string& name) { - const auto id = xsk::gsc::h2::resolver::token_id(name); - if (id != 0) + const auto id = gsc::gsc_ctx->token_id(name); + if (id) { return id; } diff --git a/src/client/game/scripting/lua/context.cpp b/src/client/game/scripting/lua/context.cpp index a9c05780..44de5fca 100644 --- a/src/client/game/scripting/lua/context.cpp +++ b/src/client/game/scripting/lua/context.cpp @@ -13,13 +13,10 @@ #include "component/scheduler.hpp" #include "component/filesystem.hpp" +#include "component/gsc/script_loading.hpp" + #include "game/ui_scripting/execution.hpp" -#include "lualib.h" - -#include -#include - #include #include #include @@ -359,7 +356,7 @@ namespace scripting::lua auto entity_type = state.new_usertype("entity"); - for (const auto& func : xsk::gsc::h2::resolver::get_methods()) + for (const auto& func : gsc::gsc_ctx->meth_map()) { const auto name = std::string(func.first); entity_type[name.data()] = [name](const entity& entity, const sol::this_state s, sol::variadic_args va) @@ -490,7 +487,7 @@ namespace scripting::lua auto game_type = state.new_usertype("game_"); state["game"] = game(); - for (const auto& func : xsk::gsc::h2::resolver::get_functions()) + for (const auto& func : gsc::gsc_ctx->func_map()) { const auto name = std::string(func.first); game_type[name] = [name](const game&, const sol::this_state s, sol::variadic_args va) diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index fdd9c081..75473d12 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1179,6 +1179,44 @@ namespace game unsigned short classnum; }; + typedef void(*BuiltinMethod)(scr_entref_t); + typedef void(*BuiltinFunction)(); + + enum + { + VAR_UNDEFINED = 0x0, + VAR_BEGIN_REF = 0x1, + VAR_POINTER = 0x1, + VAR_STRING = 0x2, + VAR_ISTRING = 0x3, + VAR_VECTOR = 0x4, + VAR_END_REF = 0x5, + VAR_FLOAT = 0x5, + VAR_INTEGER = 0x6, + VAR_CODEPOS = 0x7, + VAR_PRECODEPOS = 0x8, + VAR_FUNCTION = 0x9, + VAR_BUILTIN_FUNCTION = 0xA, + VAR_BUILTIN_METHOD = 0xB, + VAR_STACK = 0xC, + VAR_ANIMATION = 0xD, + VAR_PRE_ANIMATION = 0xE, + VAR_THREAD = 0xF, + VAR_NOTIFY_THREAD = 0x10, + VAR_TIME_THREAD = 0x11, + VAR_CHILD_THREAD = 0x12, + VAR_OBJECT = 0x13, + VAR_DEAD_ENTITY = 0x14, + VAR_ENTITY = 0x15, + VAR_ARRAY = 0x16, + VAR_DEAD_THREAD = 0x17, + VAR_COUNT = 0x18, + VAR_FREE = 0x18, + VAR_THREAD_LIST = 0x19, + VAR_ENDON_LIST = 0x1A, + VAR_TOTAL_COUNT = 0x1B, + }; + enum scriptType_e { SCRIPT_NONE = 0, diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 4849c285..50bcefb7 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -86,6 +86,7 @@ namespace game WEAK symbol GetNewArrayVariable{0x1405C2130}; WEAK symbol SetNewVariableValue{0x1405C5EA0}; WEAK symbol RemoveVariableValue{0x1405C2A50}; + WEAK symbol GetObjectType{0x1405C25D0}; WEAK symbol G_GetWeaponForName{0x14051B260}; WEAK symbol @@ -136,10 +137,11 @@ namespace game WEAK symbol Scr_AddInt{0x1405C69A0}; WEAK symbol Scr_AddString{0x1405C6A80}; WEAK symbol Scr_LoadScript{0x1405BCEC0}; - WEAK symbol Scr_GetNumParam{0x1405C7940}; + WEAK symbol Scr_GetNumParam{0x1405C7940}; WEAK symbol Scr_GetFunctionHandle{0x1405BCD50}; WEAK symbol Scr_ExecThread{0x1405C6F40}; WEAK symbol Scr_RegisterFunction{0x1405BC7B0}; + WEAK symbol Scr_CastString{0x1405C33A0}; WEAK symbol PMem_AllocFromSource_NoDebug{0x14061E680}; diff --git a/src/client/std_include.hpp b/src/client/std_include.hpp index 6fe72a49..f6fbf804 100644 --- a/src/client/std_include.hpp +++ b/src/client/std_include.hpp @@ -1,8 +1,11 @@ #pragma once #define BINARY_PAYLOAD_SIZE 0x12000000 +#define BASE_ADDRESS 0x140000000 #define INJECT_HOST_AS_LIB +#define RVA(ptr) static_cast(reinterpret_cast(ptr) - BASE_ADDRESS) + #pragma warning(push) #pragma warning(disable: 4100) #pragma warning(disable: 4127)