#include #include "loader/component_loader.hpp" #include "game/game.hpp" #include "game/dvars.hpp" #include #include #include #include #include "component/filesystem.hpp" #include "component/console.hpp" #include "component/scripting.hpp" #include "script_extension.hpp" #include "script_loading.hpp" namespace gsc { std::unique_ptr gsc_ctx; namespace { std::unordered_map main_handles; std::unordered_map init_handles; std::unordered_map loaded_scripts; utils::memory::allocator script_allocator; void clear() { main_handles.clear(); init_handles.clear(); loaded_scripts.clear(); script_allocator.clear(); clear_devmap(); } bool read_raw_script_file(const std::string& name, std::string* data) { if (filesystem::read_file(name, data)) { return true; } // This will prevent 'fake' GSC raw files from being compiled. // They are parsed by the game's own parser later as they are special files. if (name.starts_with("maps/createfx") || name.starts_with("maps/createart") || (name.starts_with("maps/mp") && name.ends_with("_fx.gsc"))) { #ifdef _DEBUG console::info("Refusing to compile rawfile '%s\n", name.data()); #endif return false; } 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); // 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_allocator.allocate(sizeof(game::ScriptFile))); script_file_ptr->name = file_name; script_file_ptr->bytecodeLen = static_cast(std::get<0>(output_script).size); script_file_ptr->len = static_cast(std::get<1>(output_script).size); const auto byte_code_size = static_cast(std::get<0>(output_script).size + 1); const auto stack_size = static_cast(std::get<1>(output_script).size + 1); script_file_ptr->buffer = static_cast(script_allocator.allocate(stack_size)); std::memcpy(const_cast(script_file_ptr->buffer), std::get<1>(output_script).data, std::get<1>(output_script).size); script_file_ptr->bytecode = static_cast(game::PMem_AllocFromSource_NoDebug(byte_code_size, 4, 1, 5)); std::memcpy(script_file_ptr->bytecode, std::get<0>(output_script).data, std::get<0>(output_script).size); script_file_ptr->compressedLen = 0; loaded_scripts[real_name] = script_file_ptr; const auto devmap = std::get<2>(output_script); if (devmap.size > 0 && (gsc_ctx->build() & xsk::gsc::build::dev_maps) != xsk::gsc::build::prod) { add_devmap_entry(script_file_ptr->bytecode, byte_code_size, real_name, devmap); } 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_ctx->token_id(name); if (!id) { return name; } return std::to_string(id); } std::pair> 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)); } console::info("Decompiling scriptfile '%s'\n", real_name.data()); 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()); return {{script_file->bytecode, static_cast(script_file->bytecodeLen)}, 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_from_folder(const std::filesystem::path& root_dir, const std::filesystem::path& script_dir) { console::info("Scanning directory '%s' for custom GSC scripts...\n", script_dir.generic_string().data()); 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_scripts(const std::filesystem::path& root_dir) { const auto load = [&root_dir](const std::filesystem::path& folder) -> void { const std::filesystem::path script_dir = root_dir / folder; if (utils::io::directory_exists(script_dir.generic_string())) { load_scripts_from_folder(root_dir, script_dir); } }; const std::filesystem::path base_dir = "scripts"; load(base_dir); const auto* map_name = game::Dvar_FindVar("mapname"); if (game::environment::is_sp()) { const std::filesystem::path game_folder = "sp"; load(base_dir / game_folder); load(base_dir / game_folder / map_name->current.string); } else { const std::filesystem::path game_folder = "mp"; load(base_dir / game_folder); load(base_dir / game_folder / map_name->current.string); const auto* game_type = game::Dvar_FindVar("g_gametype"); load(base_dir / game_folder / game_type->current.string); } } 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 gscr_post_load_scripts_stub() { utils::hook::invoke(0x140323F20); if (game::VirtualLobby_Loaded()) { return; } for (const auto& path : filesystem::get_search_paths()) { load_scripts(path); } } 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() { if (!game::VirtualLobby_Loaded()) { for (auto& function_handle : main_handles) { console::info("Executing '%s::main'\n", function_handle.first.data()); const auto thread = game::Scr_ExecThread(static_cast(function_handle.second), 0); game::RemoveRefToObject(thread); } } utils::hook::invoke(0x1403380D0); } int g_scr_set_level_script_stub(game::ScriptFunctions* functions) { const auto result = utils::hook::invoke(0x140262F60, functions); for (const auto& path : filesystem::get_search_paths()) { load_scripts(path); } return result; } void scr_load_level_singleplayer_stub() { for (auto& function_handle : main_handles) { console::info("Executing '%s::main'\n", function_handle.first.data()); const auto thread = game::Scr_ExecThread(static_cast(function_handle.second), 0); game::RemoveRefToObject(thread); } utils::hook::invoke(0x140257720); for (auto& function_handle : init_handles) { console::info("Executing '%s::init'\n", function_handle.first.data()); const auto thread = game::Scr_ExecThread(static_cast(function_handle.second), 0); game::RemoveRefToObject(thread); } } void scr_load_level_multiplayer_stub() { utils::hook::invoke(0x140325B90); if (game::VirtualLobby_Loaded()) { return; } for (auto& function_handle : init_handles) { console::info("Executing '%s::init'\n", function_handle.first.data()); const auto thread = game::Scr_ExecThread(static_cast(function_handle.second), 0); game::RemoveRefToObject(thread); } } void scr_begin_load_scripts_stub() { auto build = xsk::gsc::build::prod; if (dvars::com_developer && dvars::com_developer->current.integer > 0) { build = static_cast(static_cast(build) | static_cast(xsk::gsc::build::dev_maps)); } if (dvars::com_developer_script && dvars::com_developer_script->current.enabled) { build = static_cast(static_cast(build) | static_cast(xsk::gsc::build::dev_blocks)); } gsc_ctx->init(build, []([[maybe_unused]] auto const* ctx, const auto& included_path) -> std::pair> { const auto script_name = std::filesystem::path(included_path).replace_extension().string(); std::string file_buffer; if (!read_raw_script_file(included_path, &file_buffer) || file_buffer.empty()) { const auto name = get_script_file_name(script_name); if (game::DB_XAssetExists(game::ASSET_TYPE_SCRIPTFILE, name.data())) { return read_compiled_script_file(name, script_name); } throw std::runtime_error(std::format("Could not load gsc file '{}'", script_name)); } std::vector script_data; script_data.assign(file_buffer.begin(), file_buffer.end()); return {{}, script_data}; }); utils::hook::invoke(SELECT_VALUE(0x1403118E0, 0x1403EDE60)); } void scr_end_load_scripts_stub() { // Cleanup the compiler gsc_ctx->cleanup(); utils::hook::invoke(SELECT_VALUE(0x140243780, 0x1403EDF90)); } } 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 { // Load our scripts with an uncompressed stack utils::hook::call(SELECT_VALUE(0x14031ABB0, 0x1403F7380), db_get_raw_buffer_stub); utils::hook::call(SELECT_VALUE(0x1403309E9, 0x1403309E9), scr_begin_load_scripts_stub); // GScr_LoadScripts utils::hook::call(SELECT_VALUE(0x14023DA84, 0x140330B9C), scr_end_load_scripts_stub); // GScr_LoadScripts // ProcessScript utils::hook::call(SELECT_VALUE(0x14031AB47, 0x1403F7317), find_script); utils::hook::call(SELECT_VALUE(0x14031AB57, 0x1403F7327), db_is_x_asset_default); dvars::com_developer = game::Dvar_RegisterInt("developer", 0, 0, 2, game::DVAR_FLAG_NONE); dvars::com_developer_script = game::Dvar_RegisterBool("developer_script", false, game::DVAR_FLAG_NONE); if (game::environment::is_sp()) { utils::hook::call(0x1402632A5, g_scr_set_level_script_stub); utils::hook::call(0x140226931, scr_load_level_singleplayer_stub); } else { // GScr_LoadScripts utils::hook::call(0x140330B97, gscr_post_load_scripts_stub); // Exec script handles utils::hook::call(0x1402F71AE, g_load_structs_stub); utils::hook::call(0x1402F71C7, scr_load_level_multiplayer_stub); } scripting::on_shutdown([](const int clear_scripts) -> void { if (clear_scripts) { clear(); } }); } }; } REGISTER_COMPONENT(gsc::loading)