#include #include #include "game/game.hpp" #include "script_loading.hpp" #include "module/console.hpp" #include "module/file_system.hpp" #include "module/scripting.hpp" #include #include #include #include namespace gsc { std::uint16_t scr_func_max_id = 0x1C7; namespace { utils::memory::allocator script_file_allocator; std::unordered_map included_scripts; std::unordered_map loaded_scripts; std::unordered_map main_handles; std::unordered_map init_handles; const game::native::dvar_t* developer_script; void clear() { script_file_allocator.clear(); included_scripts.clear(); loaded_scripts.clear(); main_handles.clear(); init_handles.clear(); } bool read_raw_script_file(const std::string& name, std::string* data) { assert(data->empty()); char* buffer{}; const auto file_len = game::native::FS_ReadFile(name.data(), &buffer); if (file_len > 0 && buffer) { data->append(buffer, file_len); game::native::Hunk_FreeTempMemory(buffer); return true; } return false; } std::pair read_compiled_script_file(const std::string& name, const std::string& real_name) { const auto* script_file = game::native::DB_FindXAssetHeader(game::native::ASSET_TYPE_SCRIPTFILE, name.data(), false).scriptfile; if (!script_file) { throw std::runtime_error(std::format("Could not load scriptfile '{}'", real_name)); } console::info("Decompiling scriptfile '%s'\n", real_name.data()); if (const auto itr = included_scripts.find(name); itr != included_scripts.end()) { return {{script_file->bytecode, static_cast(script_file->bytecodeLen)}, {reinterpret_cast(itr->second.data()), itr->second.size()}}; } const std::string stack{script_file->buffer, static_cast(script_file->len)}; const auto decompressed_stack = utils::compression::zlib::decompress(stack); const auto result = included_scripts.emplace(std::make_pair(name, decompressed_stack)); const auto& itr = result.first; return {{script_file->bytecode, static_cast(script_file->bytecodeLen)}, {reinterpret_cast(itr->second.data()), itr->second.size()}}; } game::native::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::cxt->compiler(); auto& assembler = gsc::cxt->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); // Pair of two buffers. First is the byte code and second is the stack const auto output_script = assembler.assemble(*assembly_ptr); const auto script_file_ptr = static_cast(script_file_allocator.allocate(sizeof(game::native::ScriptFile))); script_file_ptr->name = file_name; const auto compressed_stack = utils::compression::zlib::compress({reinterpret_cast(output_script.second.data), output_script.second.size}); const auto byte_code_size = output_script.first.size + 1; script_file_ptr->len = static_cast(output_script.second.size); script_file_ptr->bytecodeLen = static_cast(output_script.first.size); script_file_ptr->buffer = static_cast(script_file_allocator.allocate(compressed_stack.size())); std::memcpy(const_cast(script_file_ptr->buffer), compressed_stack.data(), compressed_stack.size()); script_file_ptr->bytecode = static_cast(game::native::PMem_AllocFromSource_NoDebug(byte_code_size, 4, 0, game::native::PMEM_SOURCE_SCRIPT)); std::memcpy(script_file_ptr->bytecode, output_script.first.data, output_script.first.size); script_file_ptr->compressedLen = static_cast(compressed_stack.size()); 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_script_file_name(const std::string& name) { const auto id = gsc::cxt->token_id(name); if (!id) { return name; } return std::to_string(id); } std::string get_raw_script_file_name(const std::string& name) { if (name.ends_with(".gsh")) { return name; } return name + ".gsc"; } int db_is_x_asset_default(game::native::XAssetType type, const char* name) { if (loaded_scripts.contains(name)) { return 0; } return game::native::DB_IsXAssetDefault(type, name); } void g_scr_load_scripts_stub() { char path[game::native::MAX_OSPATH]{}; auto num_files = 0; auto** files = file_system::list_files("scripts/", "gsc", game::native::FS_LIST_ALL, &num_files, 10); for (auto i = 0; i < num_files; ++i) { const auto* script_file = files[i]; console::info("Loading script %s...\n", script_file); sprintf_s(path, "%s/%s", "scripts", script_file); // Scr_LoadScriptInternal will add the '.gsc' suffix so we remove it path[std::strlen(path) - 4] = '\0'; if (!game::native::Scr_LoadScript(path)) { console::error("Script %s encountered an error while loading\n", path); continue; } console::info("Script %s.gsc loaded successfully\n", path); const auto main_handle = game::native::Scr_GetFunctionHandle(path, static_cast(gsc::cxt->token_id("main"))); if (main_handle) { console::info("Loaded '%s::main'\n", path); main_handles[path] = main_handle; } const auto init_handle = game::native::Scr_GetFunctionHandle(path, static_cast(gsc::cxt->token_id("init"))); if (init_handle) { console::info("Loaded '%s::init'\n", path); init_handles[path] = init_handle; } } utils::hook::invoke(0x523DA0); } void scr_load_level_stub() { for (const auto& handle : main_handles) { console::info("Executing '%s::main'\n", handle.first.data()); const auto id = game::native::Scr_ExecThread(handle.second, 0); game::native::Scr_FreeThread(static_cast(id)); } utils::hook::invoke(0x517410); // Scr_LoadLevel for (const auto& handle : init_handles) { console::info("Executing '%s::init'\n", handle.first.data()); const auto id = game::native::Scr_ExecThread(handle.second, 0); game::native::Scr_FreeThread(static_cast(id)); } } void scr_begin_load_scripts_stub() { const auto comp_mode = developer_script->current.enabled ? xsk::gsc::build::dev : xsk::gsc::build::prod; gsc::cxt->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::native::DB_XAssetExists(game::native::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)); } return {xsk::gsc::buffer(reinterpret_cast(file_buffer.data()), file_buffer.size()), {}}; }); utils::hook::invoke(SELECT_VALUE(0x4B4EE0, 0x561E80)); } void scr_end_load_scripts_stub() { // Cleanup the compiler gsc::cxt->cleanup(); utils::hook::invoke(SELECT_VALUE(0x5DF010, 0x561D00)); } } game::native::ScriptFile* find_script(game::native::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::cxt->token_name(id); } auto* script = load_custom_script(name, real_name); if (script) { return script; } return game::native::DB_FindXAssetHeader(type, name, allow_create_default).scriptfile; } class script_loading final : public module { public: void post_start() override { gsc::cxt = std::make_unique(); } void post_load() override { if (game::is_mp()) this->patch_mp(); // ProcessScript utils::hook(SELECT_VALUE(0x44685E, 0x56B13E), find_script, HOOK_CALL).install()->quick(); utils::hook(SELECT_VALUE(0x446868, 0x56B148), db_is_x_asset_default, HOOK_CALL).install()->quick(); utils::hook(SELECT_VALUE(0x4FDD65, 0x523E03), scr_begin_load_scripts_stub, HOOK_CALL).install()->quick(); utils::hook(SELECT_VALUE(0x4FDECF, 0x523F4D), scr_end_load_scripts_stub, HOOK_CALL).install()->quick(); // GScr_LoadScripts developer_script = game::native::Dvar_RegisterBool("developer_script", false, game::native::DVAR_NONE, "Enable developer script comments"); scripting::on_shutdown([](int free_scripts) -> void { if (free_scripts) { clear(); } }); } static void patch_mp() { utils::hook(0x523F3E, g_scr_load_scripts_stub, HOOK_CALL).install()->quick(); utils::hook(0x50D4ED, scr_load_level_stub, HOOK_CALL).install()->quick(); } }; } REGISTER_MODULE(gsc::script_loading)