diff --git a/data/ui_scripts/mods/__init__.lua b/data/ui_scripts/mods/__init__.lua new file mode 100644 index 00000000..1ca1f8d2 --- /dev/null +++ b/data/ui_scripts/mods/__init__.lua @@ -0,0 +1,3 @@ +if (game:issingleplayer()) then + require("loading") +end diff --git a/data/ui_scripts/mods/loading.lua b/data/ui_scripts/mods/loading.lua new file mode 100644 index 00000000..16cc3084 --- /dev/null +++ b/data/ui_scripts/mods/loading.lua @@ -0,0 +1,116 @@ +game:addlocalizedstring("MENU_MODS", "MODS") +game:addlocalizedstring("MENU_MODS_DESC", "Load installed mods.") +game:addlocalizedstring("LUA_MENU_MOD_DESC_DEFAULT", "Load &&1.") +game:addlocalizedstring("LUA_MENU_MOD_DESC", "&&1\nAuthor: &&2\nVersion: &&3") +game:addlocalizedstring("LUA_MENU_OPEN_STORE", "Open store") +game:addlocalizedstring("LUA_MENU_OPEN_STORE_DESC", "Download and install mods.") +game:addlocalizedstring("LUA_MENU_LOADED_MOD", "Loaded mod: ^2&&1") +game:addlocalizedstring("LUA_MENU_AVAILABLE_MODS", "Available mods") +game:addlocalizedstring("LUA_MENU_UNLOAD", "Unload") +game:addlocalizedstring("LUA_MENU_UNLOAD_DESC", "Unload the currently loaded mod.") + +function createdivider(menu, text) + local element = LUI.UIElement.new( { + leftAnchor = true, + rightAnchor = true, + left = 0, + right = 0, + topAnchor = true, + bottomAnchor = false, + top = 0, + bottom = 33.33 + }) + + element.scrollingToNext = true + element:addElement(LUI.MenuBuilder.BuildRegisteredType("h1_option_menu_titlebar", { + title_bar_text = Engine.ToUpperCase(text) + })) + + menu.list:addElement(element) +end + +function string:truncate(length) + if (#self <= length) then + return self + end + + return self:sub(1, length - 3) .. "..." +end + +LUI.addmenubutton("main_campaign", { + index = 6, + text = "@MENU_MODS", + description = Engine.Localize("@MENU_MODS_DESC"), + callback = function() + LUI.FlowManager.RequestAddMenu(nil, "mods_menu") + end +}) + +function getmodname(path) + local name = path + local desc = Engine.Localize("@LUA_MENU_MOD_DESC_DEFAULT", name) + local infofile = path .. "/info.json" + + if (io.fileexists(infofile)) then + pcall(function() + local data = json.decode(io.readfile(infofile)) + desc = Engine.Localize("@LUA_MENU_MOD_DESC", + data.description, data.author, data.version) + name = data.name + end) + end + + return name, desc +end + +LUI.MenuBuilder.m_types_build["mods_menu"] = function(a1) + local menu = LUI.MenuTemplate.new(a1, { + menu_title = "@MENU_MODS", + exclusiveController = 0, + menu_width = 400, + menu_top_indent = LUI.MenuTemplate.spMenuOffset, + showTopRightSmallBar = true + }) + + local modfolder = game:getloadedmod() + if (modfolder ~= "") then + createdivider(menu, Engine.Localize("@LUA_MENU_LOADED_MOD", getmodname(modfolder):truncate(24))) + + menu:AddButton("@LUA_MENU_UNLOAD", function() + Engine.Exec("unloadmod") + end, nil, true, nil, { + desc_text = Engine.Localize("@LUA_MENU_UNLOAD_DESC") + }) + end + + createdivider(menu, Engine.Localize("@LUA_MENU_AVAILABLE_MODS")) + + if (io.directoryexists("mods")) then + local mods = io.listfiles("mods/") + for i = 1, #mods do + if (io.directoryexists(mods[i]) and not io.directoryisempty(mods[i])) then + local name, desc = getmodname(mods[i]) + + if (mods[i] ~= modfolder) then + game:addlocalizedstring(name, name) + menu:AddButton(name, function() + Engine.Exec("loadmod " .. mods[i]) + end, nil, true, nil, { + desc_text = desc + }) + end + end + end + end + + menu:AddBackButton(function(a1) + Engine.PlaySound(CoD.SFX.MenuBack) + LUI.FlowManager.RequestLeaveMenu(a1) + end) + + LUI.Options.InitScrollingList(menu.list, nil) + menu:CreateBottomDivider() + menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) + + return menu +end diff --git a/deps/GSL b/deps/GSL index 4377f6e6..38372367 160000 --- a/deps/GSL +++ b/deps/GSL @@ -1 +1 @@ -Subproject commit 4377f6e603c64a86c934f1546aa9db482f2e1a4e +Subproject commit 383723676cd548d615159701ac3d050f8dd1e128 diff --git a/deps/asmjit b/deps/asmjit index f1a399c4..752eb38a 160000 --- a/deps/asmjit +++ b/deps/asmjit @@ -1 +1 @@ -Subproject commit f1a399c4fe74d1535a4190a2b8727c51045cc914 +Subproject commit 752eb38a4dbe590995cbadaff06baadd8378eeeb diff --git a/deps/libtomcrypt b/deps/libtomcrypt index 673f5ce2..06a81aeb 160000 --- a/deps/libtomcrypt +++ b/deps/libtomcrypt @@ -1 +1 @@ -Subproject commit 673f5ce29015a9bba3c96792920a10601b5b0718 +Subproject commit 06a81aeb227424182125363f7554fad5146d6d2a diff --git a/deps/libtommath b/deps/libtommath index 66de8642..5108f123 160000 --- a/deps/libtommath +++ b/deps/libtommath @@ -1 +1 @@ -Subproject commit 66de86426e9cdb88526974c765108f01554af2b0 +Subproject commit 5108f12350b6daa4aa5dbc846517ad1db2f8388a diff --git a/deps/zlib b/deps/zlib index 2014a993..ec3df002 160000 --- a/deps/zlib +++ b/deps/zlib @@ -1 +1 @@ -Subproject commit 2014a993addbc8f1b9785d97f55fd189792c2f78 +Subproject commit ec3df00224d4b396e2ac6586ab5d25f673caa4c2 diff --git a/premake5.lua b/premake5.lua index 16043516..fc9e6e5b 100644 --- a/premake5.lua +++ b/premake5.lua @@ -264,6 +264,7 @@ filter {} filter "configurations:Debug" optimize "Debug" + buildoptions {"/bigobj"} defines {"DEBUG", "_DEBUG"} filter {} @@ -320,6 +321,10 @@ if _OPTIONS["copy-to"] then postbuildcommands {"copy /y \"$(TargetPath)\" \"" .. _OPTIONS["copy-to"] .. "\""} end +if _OPTIONS["debug-dir"] then + debugdir ( _OPTIONS["debug-dir"] ) +end + dependencies.imports() project "tlsdll" diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index da11d2e4..d1827936 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace filesystem { @@ -70,6 +71,40 @@ namespace filesystem return this->name_; } + std::unordered_set& get_search_paths() + { + static std::unordered_set search_paths{}; + return search_paths; + } + + std::string read_file(const std::string& path) + { + for (const auto& search_path : get_search_paths()) + { + const auto path_ = search_path + "/" + path; + if (utils::io::file_exists(path_)) + { + return utils::io::read_file(path_); + } + } + + return {}; + } + + bool read_file(const std::string& path, std::string* data) + { + for (const auto& search_path : get_search_paths()) + { + const auto path_ = search_path + "/" + path; + if (utils::io::read_file(path_, data)) + { + return true; + } + } + + return false; + } + class component final : public component_interface { public: @@ -87,6 +122,10 @@ namespace filesystem utils::hook::call(SELECT_VALUE(0x1403B8D31, 0x1404EE3D0), register_custom_path_stub); utils::hook::call(SELECT_VALUE(0x1403B8D51, 0x1404EE3F0), register_custom_path_stub); utils::hook::call(SELECT_VALUE(0x1403B8D90, 0x1404EE42F), register_custom_path_stub); + + get_search_paths().insert("."); + get_search_paths().insert("h1-mod"); + get_search_paths().insert("data"); } }; } diff --git a/src/client/component/filesystem.hpp b/src/client/component/filesystem.hpp index 6cec8c87..c3d36dc6 100644 --- a/src/client/component/filesystem.hpp +++ b/src/client/component/filesystem.hpp @@ -16,4 +16,8 @@ namespace filesystem std::string name_; std::string buffer_; }; + + std::unordered_set& get_search_paths(); + std::string read_file(const std::string& path); + bool read_file(const std::string& path, std::string* data); } \ No newline at end of file diff --git a/src/client/component/fonts.cpp b/src/client/component/fonts.cpp new file mode 100644 index 00000000..32f55399 --- /dev/null +++ b/src/client/component/fonts.cpp @@ -0,0 +1,136 @@ +#include +#include "loader/component_loader.hpp" + +#include "fonts.hpp" +#include "console.hpp" +#include "filesystem.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include +#include +#include +#include + +namespace fonts +{ + namespace + { + struct font_data_t + { + std::unordered_map fonts; + std::unordered_map raw_fonts; + }; + + utils::concurrency::container font_data; + + game::TTF* create_font(const std::string& name, const std::string& data) + { + const auto font = utils::memory::get_allocator()->allocate(); + font->name = utils::memory::get_allocator()->duplicate_string(name); + font->buffer = utils::memory::get_allocator()->duplicate_string(data); + font->len = static_cast(data.size()); + font->fontFace = 0; + return font; + } + + void free_font(game::TTF* font) + { + utils::memory::get_allocator()->free(font->buffer); + utils::memory::get_allocator()->free(font->name); + utils::memory::get_allocator()->free(font); + } + + game::TTF* load_font(const std::string& name) + { + return font_data.access([&](font_data_t& data_) -> game::TTF* + { + if (const auto i = data_.fonts.find(name); i != data_.fonts.end()) + { + return i->second; + } + + std::string data{}; + if (const auto i = data_.raw_fonts.find(name); i != data_.raw_fonts.end()) + { + data = i->second; + } + + if (data.empty() && !filesystem::read_file(name, &data)) + { + return nullptr; + } + + const auto material = create_font(name, data); + data_.fonts[name] = material; + + return material; + }); + } + + game::TTF* try_load_font(const std::string& name) + { + try + { + return load_font(name); + } + catch (const std::exception& e) + { + console::error("Failed to load font %s: %s\n", name.data(), e.what()); + } + + return nullptr; + } + + game::TTF* db_find_xasset_header_stub(game::XAssetType type, const char* name, int create_default) + { + auto result = try_load_font(name); + if (result == nullptr) + { + result = game::DB_FindXAssetHeader(type, name, create_default).ttf; + } + return result; + } + } + + void add(const std::string& name, const std::string& data) + { + font_data.access([&](font_data_t& data_) + { + data_.raw_fonts[name] = data; + }); + } + + void clear() + { + font_data.access([&](font_data_t& data_) + { + for (auto& font : data_.fonts) + { + free_font(font.second); + } + + data_.fonts.clear(); + utils::hook::set(SELECT_VALUE(0x14F09DBB8, 0x14FD61EE8), 0); // reset registered font count + }); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (game::environment::is_dedi()) + { + return; + } + + utils::hook::call(SELECT_VALUE(0x1404D41B6, 0x1405D9296), db_find_xasset_header_stub); + } + }; +} + +REGISTER_COMPONENT(fonts::component) diff --git a/src/client/component/fonts.hpp b/src/client/component/fonts.hpp new file mode 100644 index 00000000..c2d22869 --- /dev/null +++ b/src/client/component/fonts.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace fonts +{ + void add(const std::string& name, const std::string& data); + void clear(); +} diff --git a/src/client/component/game_console.cpp b/src/client/component/game_console.cpp index 23b63212..26d492ce 100644 --- a/src/client/component/game_console.cpp +++ b/src/client/component/game_console.cpp @@ -571,7 +571,7 @@ namespace game_console { if (key == game::keyNum_t::K_F10) { - if (!game::Com_InFrontEnd()) + if (!game::Com_InFrontend()) { return false; } diff --git a/src/client/component/input.cpp b/src/client/component/input.cpp index a10ac12a..7801bcbd 100644 --- a/src/client/component/input.cpp +++ b/src/client/component/input.cpp @@ -4,6 +4,7 @@ #include "game/game.hpp" #include "game_console.hpp" +#include "game/ui_scripting/execution.hpp" #include @@ -14,8 +15,22 @@ namespace input utils::hook::detour cl_char_event_hook; utils::hook::detour cl_key_event_hook; + bool lui_running() + { + return *game::hks::lua_state != nullptr; + } + void cl_char_event_stub(const int local_client_num, const int key) { + if (lui_running()) + { + ui_scripting::notify("keypress", + { + {"keynum", key}, + {"key", game::Key_KeynumToString(key, 0, 1)}, + }); + } + if (!game_console::console_char_event(local_client_num, key)) { return; @@ -26,6 +41,15 @@ namespace input void cl_key_event_stub(const int local_client_num, const int key, const int down) { + if (lui_running()) + { + ui_scripting::notify(down ? "keydown" : "keyup", + { + {"keynum", key}, + {"key", game::Key_KeynumToString(key, 0, 1)}, + }); + } + if (!game_console::console_key_event(local_client_num, key, down)) { return; diff --git a/src/client/component/logfile.cpp b/src/client/component/logfile.cpp index fc949d92..08ce07dd 100644 --- a/src/client/component/logfile.cpp +++ b/src/client/component/logfile.cpp @@ -212,7 +212,7 @@ namespace logfile return false; } - const auto hook = vm_execute_hooks[pos]; + const auto& hook = vm_execute_hooks[pos]; const auto state = hook.lua_state(); const scripting::entity self = local_id_to_entity(game::scr_VmPub->function_frame->fs.localId); @@ -296,6 +296,8 @@ namespace logfile public: void post_unpack() override { + utils::hook::jump(SELECT_VALUE(0x140376655, 0x140444645), utils::hook::assemble(vm_execute_stub), true); + if (game::environment::is_sp()) { return; @@ -308,8 +310,6 @@ namespace logfile utils::hook::call(0x140484EC0, g_shutdown_game_stub); utils::hook::call(0x1404853C1, g_shutdown_game_stub); - - utils::hook::jump(SELECT_VALUE(0x140376655, 0x140444645), utils::hook::assemble(vm_execute_stub), true); } }; } diff --git a/src/client/component/materials.cpp b/src/client/component/materials.cpp index 287238c0..4fbb3ed8 100644 --- a/src/client/component/materials.cpp +++ b/src/client/component/materials.cpp @@ -3,6 +3,7 @@ #include "materials.hpp" #include "console.hpp" +#include "filesystem.hpp" #include "game/game.hpp" #include "game/dvars.hpp" @@ -20,6 +21,7 @@ namespace materials { utils::hook::detour db_material_streaming_fail_hook; utils::hook::detour material_register_handle_hook; + utils::hook::detour db_get_material_index_hook; struct material_data_t { @@ -27,6 +29,8 @@ namespace materials std::unordered_map images; }; + char constant_table[0x20] = {}; + utils::concurrency::container material_data; game::GfxImage* setup_image(game::GfxImage* image, const utils::image& raw_image) @@ -47,8 +51,7 @@ namespace materials game::Material* create_material(const std::string& name, const std::string& data) { - const auto white = *reinterpret_cast(SELECT_VALUE(0x141F3D860, 0x14282C330)); - + const auto white = material_register_handle_hook.invoke("white"); const auto material = utils::memory::get_allocator()->allocate(); const auto texture_table = utils::memory::get_allocator()->allocate(); const auto image = utils::memory::get_allocator()->allocate(); @@ -57,6 +60,7 @@ namespace materials std::memcpy(texture_table, white->textureTable, sizeof(game::MaterialTextureDef)); std::memcpy(image, white->textureTable->u.image, sizeof(game::GfxImage)); + material->constantTable = &constant_table; material->name = utils::memory::get_allocator()->duplicate_string(name); image->name = material->name; @@ -66,6 +70,16 @@ namespace materials return material; } + void free_material(game::Material* material) + { + material->textureTable->u.image->textures.___u0.map->Release(); + material->textureTable->u.image->textures.shaderView->Release(); + utils::memory::get_allocator()->free(material->textureTable->u.image); + utils::memory::get_allocator()->free(material->textureTable); + utils::memory::get_allocator()->free(material->name); + utils::memory::get_allocator()->free(material); + } + game::Material* load_material(const std::string& name) { return material_data.access([&](material_data_t& data_) -> game::Material* @@ -81,10 +95,9 @@ namespace materials data = i->second; } - if (data.empty() - && !utils::io::read_file(utils::string::va("h1-mod/materials/%s.png", name.data()), &data) - && !utils::io::read_file(utils::string::va("data/materials/%s.png", name.data()), &data)) + if (data.empty() && !filesystem::read_file(utils::string::va("materials/%s.png", name.data()), &data)) { + data_.materials[name] = nullptr; return nullptr; } @@ -124,24 +137,24 @@ namespace materials return result; } - bool db_material_streaming_fail_stub(game::Material* material) + int db_material_streaming_fail_stub(game::Material* material) { - const auto found = material_data.access([material](material_data_t& data_) + if (material->constantTable == &constant_table) { - if (data_.materials.find(material->name) != data_.materials.end()) - { - return true; - } - - return false; - }); - - if (found) - { - return false; + return 0; } - return db_material_streaming_fail_hook.invoke(material); + return db_material_streaming_fail_hook.invoke(material); + } + + unsigned int db_get_material_index_stub(game::Material* material) + { + if (material->constantTable == &constant_table) + { + return 0; + } + + return db_get_material_index_hook.invoke(material); } } @@ -153,6 +166,24 @@ namespace materials }); } + void clear() + { + material_data.access([&](material_data_t& data_) + { + for (auto& material : data_.materials) + { + if (material.second == nullptr) + { + continue; + } + + free_material(material.second); + } + + data_.materials.clear(); + }); + } + class component final : public component_interface { public: @@ -165,6 +196,7 @@ namespace materials material_register_handle_hook.create(game::Material_RegisterHandle, material_register_handle_stub); db_material_streaming_fail_hook.create(SELECT_VALUE(0x1401D3180, 0x1402C6260), db_material_streaming_fail_stub); + db_get_material_index_hook.create(SELECT_VALUE(0x1401CAD00, 0x1402BBB20), db_get_material_index_stub); } }; } diff --git a/src/client/component/materials.hpp b/src/client/component/materials.hpp index ac58b511..3a548548 100644 --- a/src/client/component/materials.hpp +++ b/src/client/component/materials.hpp @@ -3,4 +3,5 @@ namespace materials { void add(const std::string& name, const std::string& data); + void clear(); } diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp new file mode 100644 index 00000000..ea6a9027 --- /dev/null +++ b/src/client/component/mods.cpp @@ -0,0 +1,119 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "command.hpp" +#include "console.hpp" +#include "scheduler.hpp" +#include "filesystem.hpp" +#include "materials.hpp" +#include "fonts.hpp" +#include "mods.hpp" + +#include +#include + +namespace mods +{ + std::string mod_path{}; + + namespace + { + utils::hook::detour db_release_xassets_hook; + bool release_assets = false; + + void db_release_xassets_stub() + { + if (release_assets) + { + materials::clear(); + fonts::clear(); + } + + db_release_xassets_hook.invoke(); + } + + void restart() + { + scheduler::once([]() + { + release_assets = true; + game::Com_Shutdown(""); + release_assets = false; + }, scheduler::pipeline::main); + } + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (!game::environment::is_sp()) + { + return; + } + + if (!utils::io::directory_exists("mods")) + { + utils::io::create_directory("mods"); + } + + db_release_xassets_hook.create(SELECT_VALUE(0x1401CD560, 0x1402BF160), db_release_xassets_stub); + + command::add("loadmod", [](const command::params& params) + { + if (params.size() < 2) + { + console::info("Usage: loadmod mods/"); + return; + } + + if (!game::Com_InFrontend()) + { + console::info("Cannot load mod while in-game!\n"); + game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); + return; + } + + const auto path = params.get(1); + if (!utils::io::directory_exists(path)) + { + console::info("Mod %s not found!\n", path); + return; + } + + console::info("Loading mod %s\n", path); + filesystem::get_search_paths().erase(mod_path); + filesystem::get_search_paths().insert(path); + mod_path = path; + restart(); + }); + + command::add("unloadmod", [](const command::params& params) + { + if (mod_path.empty()) + { + console::info("No mod loaded\n"); + return; + } + + if (!game::Com_InFrontend()) + { + console::info("Cannot unload mod while in-game!\n"); + game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); + return; + } + + console::info("Unloading mod %s\n", mod_path.data()); + filesystem::get_search_paths().erase(mod_path); + mod_path.clear(); + restart(); + }); + } + }; +} + +REGISTER_COMPONENT(mods::component) diff --git a/src/client/component/mods.hpp b/src/client/component/mods.hpp new file mode 100644 index 00000000..364a2f11 --- /dev/null +++ b/src/client/component/mods.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace mods +{ + extern std::string mod_path; +} \ No newline at end of file diff --git a/src/client/component/patches.cpp b/src/client/component/patches.cpp index 7c323ded..594879f1 100644 --- a/src/client/component/patches.cpp +++ b/src/client/component/patches.cpp @@ -241,10 +241,10 @@ namespace patches // unlock safeArea_* utils::hook::jump(0x1402624F5, 0x140262503); utils::hook::jump(0x14026251C, 0x140262547); - dvars::override::register_int("safeArea_adjusted_horizontal", 1, 0, 1, game::DVAR_FLAG_SAVED); - dvars::override::register_int("safeArea_adjusted_vertical", 1, 0, 1, game::DVAR_FLAG_SAVED); - dvars::override::register_int("safeArea_horizontal", 1, 0, 1, game::DVAR_FLAG_SAVED); - dvars::override::register_int("safeArea_vertical", 1, 0, 1, game::DVAR_FLAG_SAVED); + dvars::override::register_float("safeArea_adjusted_horizontal", 1, 0, 1, game::DVAR_FLAG_SAVED); + dvars::override::register_float("safeArea_adjusted_vertical", 1, 0, 1, game::DVAR_FLAG_SAVED); + dvars::override::register_float("safeArea_horizontal", 1, 0, 1, game::DVAR_FLAG_SAVED); + dvars::override::register_float("safeArea_vertical", 1, 0, 1, game::DVAR_FLAG_SAVED); // allow servers to check for new packages more often dvars::override::register_int("sv_network_fps", 1000, 20, 1000, game::DVAR_FLAG_SAVED); @@ -257,7 +257,7 @@ namespace patches dvars::register_int("scr_game_spectatetype", 1, 0, 99, game::DVAR_FLAG_REPLICATED, ""); - dvars::override::register_bool("ui_drawcrosshair", true, game::DVAR_FLAG_WRITE); + dvars::override::register_bool("ui_drawCrosshair", true, game::DVAR_FLAG_WRITE); dvars::override::register_int("com_maxfps", 0, 0, 1000, game::DVAR_FLAG_SAVED); diff --git a/src/client/component/scripting.cpp b/src/client/component/scripting.cpp index e3116122..b31e2329 100644 --- a/src/client/component/scripting.cpp +++ b/src/client/component/scripting.cpp @@ -2,7 +2,7 @@ #include "loader/component_loader.hpp" #include "game/game.hpp" -#include +#include "game/dvars.hpp" #include "game/scripting/entity.hpp" #include "game/scripting/functions.hpp" @@ -13,14 +13,20 @@ #include "scheduler.hpp" #include "scripting.hpp" +#include +#include +#include + namespace scripting { std::unordered_map> fields_table; std::unordered_map> script_function_table; + utils::concurrency::container shared_table; namespace { utils::hook::detour vm_notify_hook; + utils::hook::detour vm_execute_hook; utils::hook::detour scr_load_level_hook; utils::hook::detour g_shutdown_game_hook; @@ -29,7 +35,14 @@ namespace scripting utils::hook::detour scr_set_thread_position_hook; utils::hook::detour process_script_hook; + utils::hook::detour sl_get_canonical_string_hook; + + utils::hook::detour db_find_xasset_header_hook; + std::string current_file; + unsigned int current_file_id{}; + + game::dvar_t* g_dump_scripts; void vm_notify_stub(const unsigned int notify_list_owner_id, const game::scr_string_t string_value, game::VariableValue* top) @@ -48,11 +61,6 @@ namespace scripting e.arguments.emplace_back(*value); } - if (e.name == "entitydeleted") - { - scripting::clear_entity_fields(e.entity); - } - lua::engine::notify(e); } } @@ -60,6 +68,16 @@ namespace scripting vm_notify_hook.invoke(notify_list_owner_id, string_value, top); } + unsigned int vm_execute_stub() + { + if (!lua::engine::is_running()) + { + lua::engine::start(); + } + + return vm_execute_hook.invoke(); + } + void scr_load_level_stub() { scr_load_level_hook.invoke(); @@ -71,20 +89,25 @@ namespace scripting void g_shutdown_game_stub(const int free_scripts) { + if (free_scripts) + { + script_function_table.clear(); + } + lua::engine::stop(); return g_shutdown_game_hook.invoke(free_scripts); } - void scr_add_class_field_stub(unsigned int classnum, game::scr_string_t _name, unsigned int canonicalString, unsigned int offset) + void scr_add_class_field_stub(unsigned int classnum, game::scr_string_t name, unsigned int canonical_string, unsigned int offset) { - const auto name = game::SL_ConvertToString(_name); + const auto name_str = game::SL_ConvertToString(name); - if (fields_table[classnum].find(name) == fields_table[classnum].end()) + if (fields_table[classnum].find(name_str) == fields_table[classnum].end()) { - fields_table[classnum][name] = offset; + fields_table[classnum][name_str] = offset; } - scr_add_class_field_hook.invoke(classnum, _name, canonicalString, offset); + scr_add_class_field_hook.invoke(classnum, name, canonical_string, offset); } void process_script_stub(const char* filename) @@ -92,21 +115,69 @@ namespace scripting const auto file_id = atoi(filename); if (file_id) { - current_file = scripting::find_token(file_id); + current_file_id = file_id; } else { + current_file_id = 0; current_file = filename; } process_script_hook.invoke(filename); } - void scr_set_thread_position_stub(unsigned int threadName, const char* codePos) + void add_function(const std::string& file, unsigned int id, const char* pos) { - const auto function_name = scripting::find_token(threadName); - script_function_table[current_file][function_name] = codePos; - scr_set_thread_position_hook.invoke(threadName, codePos); + const auto function_names = scripting::find_token(id); + for (const auto& name : function_names) + { + script_function_table[file][name] = pos; + } + } + + void scr_set_thread_position_stub(unsigned int thread_name, const char* code_pos) + { + if (current_file_id) + { + const auto names = scripting::find_token(current_file_id); + for (const auto& name : names) + { + add_function(name, thread_name, code_pos); + } + } + else + { + add_function(current_file, thread_name, code_pos); + } + + scr_set_thread_position_hook.invoke(thread_name, code_pos); + } + + unsigned int sl_get_canonical_string_stub(const char* str) + { + const auto result = sl_get_canonical_string_hook.invoke(str); + scripting::token_map[str] = result; + return result; + } + + game::XAssetHeader db_find_xasset_header_stub(game::XAssetType type, const char* name, int allow_create_default) + { + const auto result = db_find_xasset_header_hook.invoke(type, name, allow_create_default); + if (!g_dump_scripts->current.enabled || type != game::XAssetType::ASSET_TYPE_SCRIPTFILE) + { + return result; + } + + std::string buffer; + buffer.append(result.scriptfile->name, strlen(result.scriptfile->name) + 1); + buffer.append(reinterpret_cast(&result.scriptfile->compressedLen), 4); + buffer.append(reinterpret_cast(&result.scriptfile->len), 4); + buffer.append(reinterpret_cast(&result.scriptfile->bytecodeLen), 4); + buffer.append(result.scriptfile->buffer, result.scriptfile->compressedLen); + buffer.append(result.scriptfile->bytecode, result.scriptfile->bytecodeLen); + utils::io::write_file(utils::string::va("gsc_dump/%s.gscbin", name), buffer); + + return result; } } @@ -115,21 +186,28 @@ namespace scripting public: void post_unpack() override { - if (game::environment::is_sp()) - { - return; - } - vm_notify_hook.create(SELECT_VALUE(0x140379A00, 0x1404479F0), vm_notify_stub); scr_add_class_field_hook.create(SELECT_VALUE(0x140370370, 0x14043E2C0), scr_add_class_field_stub); scr_set_thread_position_hook.create(SELECT_VALUE(0x14036A180, 0x140437D10), scr_set_thread_position_stub); process_script_hook.create(SELECT_VALUE(0x1403737E0, 0x1404417E0), process_script_stub); + sl_get_canonical_string_hook.create(game::SL_GetCanonicalString, sl_get_canonical_string_stub); + + if (!game::environment::is_sp()) + { + scr_load_level_hook.create(SELECT_VALUE(0x1402A5BE0, 0x1403727C0), scr_load_level_stub); + } + else + { + vm_execute_hook.create(SELECT_VALUE(0x140376590, 0x140444580), vm_execute_stub); + } - scr_load_level_hook.create(SELECT_VALUE(0x1402A5BE0, 0x1403727C0), scr_load_level_stub); g_shutdown_game_hook.create(SELECT_VALUE(0x140277D40, 0x140345A60), g_shutdown_game_stub); + db_find_xasset_header_hook.create(game::DB_FindXAssetHeader, db_find_xasset_header_stub); + g_dump_scripts = dvars::register_bool("g_dumpScripts", false, game::DVAR_FLAG_NONE, "Dump GSC scripts"); + scheduler::loop([]() { lua::engine::run_frame(); @@ -138,4 +216,4 @@ namespace scripting }; } -REGISTER_COMPONENT(scripting::component) \ No newline at end of file +REGISTER_COMPONENT(scripting::component) diff --git a/src/client/component/scripting.hpp b/src/client/component/scripting.hpp index 865ae858..5794bff2 100644 --- a/src/client/component/scripting.hpp +++ b/src/client/component/scripting.hpp @@ -3,6 +3,9 @@ namespace scripting { + using shared_table_t = std::unordered_map; + extern std::unordered_map> fields_table; extern std::unordered_map> script_function_table; + extern utils::concurrency::container shared_table; } \ No newline at end of file diff --git a/src/client/component/stats.cpp b/src/client/component/stats.cpp index 81e7c3da..99ffdd5f 100644 --- a/src/client/component/stats.cpp +++ b/src/client/component/stats.cpp @@ -21,7 +21,7 @@ namespace stats utils::hook::detour is_item_unlocked_hook2; utils::hook::detour is_item_unlocked_hook3; - int is_item_unlocked_stub(void* a1, void* a2, void* a3) + int is_item_unlocked_stub(int a1, void* a2, int a3) { if (cg_unlock_all_items->current.enabled) { @@ -31,7 +31,7 @@ namespace stats return is_item_unlocked_hook.invoke(a1, a2, a3); } - int is_item_unlocked_stub2(void* a1, void* a2, void* a3, void* a4, void* a5, void* a6) + int is_item_unlocked_stub2(int a1, void* a2, void* a3, void* a4, int a5, void* a6) { if (cg_unlock_all_items->current.enabled) { @@ -41,7 +41,7 @@ namespace stats return is_item_unlocked_hook2.invoke(a1, a2, a3, a4, a5, a6); } - int is_item_unlocked_stub3(void* a1) + int is_item_unlocked_stub3(int a1) { if (cg_unlock_all_items->current.enabled) { @@ -50,6 +50,11 @@ namespace stats return is_item_unlocked_hook3.invoke(a1); } + + int is_item_unlocked() + { + return 0; + } } class component final : public component_interface @@ -57,19 +62,28 @@ namespace stats public: void post_unpack() override { - if (!game::environment::is_mp()) + if (game::environment::is_sp()) { return; } - cg_unlock_all_items = dvars::register_bool("cg_unlockall_items", false, game::DVAR_FLAG_SAVED, - "Whether items should be locked based on the player's stats or always unlocked."); - dvars::register_bool("cg_unlockall_classes", false, game::DVAR_FLAG_SAVED, - "Whether classes should be locked based on the player's stats or always unlocked."); + if (game::environment::is_dedi()) + { + utils::hook::jump(0x140413E60, is_item_unlocked); + utils::hook::jump(0x140413860, is_item_unlocked); + utils::hook::jump(0x140412B70, is_item_unlocked); + } + else + { + cg_unlock_all_items = dvars::register_bool("cg_unlockall_items", false, game::DVAR_FLAG_SAVED, + "Whether items should be locked based on the player's stats or always unlocked."); + dvars::register_bool("cg_unlockall_classes", false, game::DVAR_FLAG_SAVED, + "Whether classes should be locked based on the player's stats or always unlocked."); - is_item_unlocked_hook.create(0x140413E60, is_item_unlocked_stub); - is_item_unlocked_hook2.create(0x140413860, is_item_unlocked_stub2); - is_item_unlocked_hook3.create(0x140412B70, is_item_unlocked_stub3); + is_item_unlocked_hook.create(0x140413E60, is_item_unlocked_stub); + is_item_unlocked_hook2.create(0x140413860, is_item_unlocked_stub2); + is_item_unlocked_hook3.create(0x140412B70, is_item_unlocked_stub3); + } } }; } diff --git a/src/client/game/scripting/execution.cpp b/src/client/game/scripting/execution.cpp index 273b4f7a..1380ab1e 100644 --- a/src/client/game/scripting/execution.cpp +++ b/src/client/game/scripting/execution.cpp @@ -145,47 +145,6 @@ namespace scripting return exec_ent_thread(entity, pos, arguments); } - static std::unordered_map> custom_fields; - - script_value get_custom_field(const entity& entity, const std::string& field) - { - auto& fields = custom_fields[entity.get_entity_id()]; - const auto _field = fields.find(field); - if (_field != fields.end()) - { - return _field->second; - } - return {}; - } - - void set_custom_field(const entity& entity, const std::string& field, const script_value& value) - { - const auto id = entity.get_entity_id(); - - if (custom_fields[id].find(field) != custom_fields[id].end()) - { - custom_fields[id][field] = value; - return; - } - - custom_fields[id].insert(std::make_pair(field, value)); - } - - void clear_entity_fields(const entity& entity) - { - const auto id = entity.get_entity_id(); - - if (custom_fields.find(id) != custom_fields.end()) - { - custom_fields[id].clear(); - } - } - - void clear_custom_fields() - { - custom_fields.clear(); - } - void set_entity_field(const entity& entity, const std::string& field, const script_value& value) { const auto entref = entity.get_entity_reference(); @@ -206,8 +165,7 @@ namespace scripting } else { - // Read custom fields - set_custom_field(entity, field, value); + set_object_variable(entity.get_entity_id(), field, value); } } @@ -234,8 +192,7 @@ namespace scripting return value; } - // Add custom fields - return get_custom_field(entity, field); + return get_object_variable(entity.get_entity_id(), field); } unsigned int make_array() @@ -248,4 +205,47 @@ namespace scripting return index; } + + void set_object_variable(const unsigned int parent_id, const unsigned int id, const script_value& value) + { + const auto offset = 0xFA00 * (parent_id & 3); + const auto variable_id = game::GetVariable(parent_id, id); + const auto variable = &game::scr_VarGlob->childVariableValue[variable_id + offset]; + const auto& raw_value = value.get_raw(); + + game::AddRefToValue(raw_value.type, raw_value.u); + game::RemoveRefToValue(variable->type, variable->u.u); + + variable->type = static_cast(raw_value.type); + variable->u.u = raw_value.u; + } + + void set_object_variable(const unsigned int parent_id, const std::string& name, const script_value& value) + { + const auto id = scripting::find_token_id(name); + set_object_variable(parent_id, id, value); + } + + script_value get_object_variable(const unsigned int parent_id, const unsigned int id) + { + const auto offset = 0xFA00 * (parent_id & 3); + const auto variable_id = game::FindVariable(parent_id, id); + if (!variable_id) + { + return {}; + } + + const auto variable = &game::scr_VarGlob->childVariableValue[variable_id + offset]; + game::VariableValue value{}; + value.type = static_cast(variable->type); + value.u = variable->u.u; + + return value; + } + + script_value get_object_variable(const unsigned int parent_id, const std::string& name) + { + const auto id = scripting::find_token_id(name); + return get_object_variable(parent_id, id); + } } diff --git a/src/client/game/scripting/execution.hpp b/src/client/game/scripting/execution.hpp index 94a2678d..9ec6f62f 100644 --- a/src/client/game/scripting/execution.hpp +++ b/src/client/game/scripting/execution.hpp @@ -27,13 +27,16 @@ namespace scripting script_value call_script_function(const entity& entity, const std::string& filename, const std::string& function, const std::vector& arguments); - void clear_entity_fields(const entity& entity); - void clear_custom_fields(); - void set_entity_field(const entity& entity, const std::string& field, const script_value& value); script_value get_entity_field(const entity& entity, const std::string& field); void notify(const entity& entity, const std::string& event, const std::vector& arguments); unsigned int make_array(); + + script_value get_object_variable(const unsigned int parent_id, const unsigned int id); + script_value get_object_variable(const unsigned int parent_id, const std::string& name); + + void set_object_variable(const unsigned int parent_id, const std::string& name, const script_value& value); + void set_object_variable(const unsigned int parent_id, const unsigned int id, const script_value& value); } diff --git a/src/client/game/scripting/function_tables.cpp b/src/client/game/scripting/function_tables.cpp index b36aea0f..e0cba30c 100644 --- a/src/client/game/scripting/function_tables.cpp +++ b/src/client/game/scripting/function_tables.cpp @@ -931,12 +931,12 @@ namespace scripting {"setstablemissile", 0x8092}, // SP 0x1402AD800 MP 0x000000000 {"playersetgroundreferenceent", 0x8093}, // SP 0x1402A9070 MP 0x1403752A0 {"dontinterpolate", 0x8094}, // SP 0x1402A0070 MP 0x140358360 - {"_meth_8095", 0x8095}, // SP 0x1402AAC80 MP 0x000000000 - {"_meth_8096", 0x8096}, // SP 0x1402AAD20 MP 0x000000000 + {"dospawn", 0x8095}, // SP 0x1402AAC80 MP 0x000000000 + {"stalingradspawn", 0x8096}, // SP 0x1402AAD20 MP 0x000000000 {"getorigin", 0x8097}, // SP 0x1402AADC0 MP 0x140377CA0 - {"_meth_8098", 0x8098}, // SP 0x1402AAE70 MP 0x000000000 - {"_meth_8099", 0x8099}, // SP 0x1402AAF90 MP 0x000000000 - {"_meth_809a", 0x809A}, // SP 0x1402AC1D0 MP 0x000000000 + {"getcentroid", 0x8098}, // SP 0x1402AAE70 MP 0x000000000 + {"getshootatpos", 0x8099}, // SP 0x1402AAF90 MP 0x000000000 + {"getdebugeye", 0x809A}, // SP 0x1402AC1D0 MP 0x000000000 {"useby", 0x809B}, // SP 0x1402AC470 MP 0x140377D00 {"playsound", 0x809C}, // SP 0x1402AC9B0 MP 0x140377F40 {"_meth_809d", 0x809D}, @@ -1336,14 +1336,14 @@ namespace scripting {"vehicleturretcontroloff", 0x8227}, // SP 0x140464260 MP 0x140562970 {"isturretready", 0x8228}, // SP 0x140464340 MP 0x1405629E0 {"_meth_8229", 0x8229}, // SP 0x140464570 MP 0x140562C70 - {"dospawn", 0x822A}, // SP 0x1404646D0 MP 0x140562D90 - {"isphysveh", 0x822B}, // SP 0x1404647A0 MP 0x140562E80 - {"crash", 0x822C}, // SP 0x140464840 MP 0x140562F80 - {"launch", 0x822D}, // SP 0x140464980 MP 0x1405630A0 - {"disablecrashing", 0x822E}, // SP 0x140464B20 MP 0x140563240 - {"enablecrashing", 0x822F}, // SP 0x140464C10 MP 0x140563310 - {"setspeed", 0x8230}, // SP 0x140464C90 MP 0x1405633E0 - {"setconveyorbelt", 0x8231}, // SP 0x140464E50 MP 0x140563610 + {"vehicle_dospawn", 0x822A}, // SP 0x1404646D0 MP 0x140562D90 + {"vehicle_isphysveh", 0x822B}, // SP 0x1404647A0 MP 0x140562E80 + {"vehphys_crash", 0x822C}, // SP 0x140464840 MP 0x140562F80 + {"vehphys_launch", 0x822D}, // SP 0x140464980 MP 0x1405630A0 + {"vehphys_disablecrashing", 0x822E}, // SP 0x140464B20 MP 0x140563240 + {"vehphys_enablecrashing", 0x822F}, // SP 0x140464C10 MP 0x140563310 + {"vehphys_setspeed", 0x8230}, // SP 0x140464C90 MP 0x1405633E0 + {"vehphys_setconveyorbelt", 0x8231}, // SP 0x140464E50 MP 0x140563610 {"freevehicle", 0x8232}, // SP 0x000000000 MP 0x1405609B0 {"_meth_8233", 0x8233}, // SP 0x140290E70 MP 0x000000000 {"_meth_8234", 0x8234}, // SP 0x1402910E0 MP 0x000000000 @@ -1393,8 +1393,8 @@ namespace scripting {"canturrettargetpoint", 0x8260}, // SP 0x1404635A0 MP 0x140561DF0 {"setlookatent", 0x8261}, // SP 0x1404638A0 MP 0x1405620F0 {"clearlookatent", 0x8262}, // SP 0x140463950 MP 0x1405621A0 - {"setweapon", 0x8263}, // SP 0x140463AB0 MP 0x140562280 - {"_meth_8264", 0x8264}, // SP 0x140463B20 MP 0x1405622F0 + {"setvehweapon", 0x8263}, // SP 0x140463AB0 MP 0x140562280 + {"fireweapon", 0x8264}, // SP 0x140463B20 MP 0x1405622F0 {"vehicleturretcontrolon", 0x8265}, // SP 0x1404641D0 MP 0x1405628F0 {"finishplayerdamage", 0x8266}, // SP 0x000000000 MP 0x1403337A0 {"suicide", 0x8267}, // SP 0x000000000 MP 0x140333E20 @@ -1421,16 +1421,16 @@ namespace scripting {"setswitchnode", 0x827C}, // SP 0x140465740 MP 0x14055F4D0 {"setwaitspeed", 0x827D}, // SP 0x140465840 MP 0x14055F560 {"finishdamage", 0x827E}, // SP 0x000000000 MP 0x14055F5E0 - {"setspeed", 0x827F}, // SP 0x1404658C0 MP 0x14055F840 - {"setspeedimmediate", 0x8280}, // SP 0x140465930 MP 0x14055F8B0 - {"_meth_8281", 0x8281}, // SP 0x140465AC0 MP 0x14055FA60 - {"getspeed", 0x8282}, // SP 0x140465BE0 MP 0x14055FB80 - {"getvelocity", 0x8283}, // SP 0x140465CD0 MP 0x14055FC70 - {"getbodyvelocity", 0x8284}, // SP 0x140465D40 MP 0x14055FCE0 - {"getsteering", 0x8285}, // SP 0x140465DB0 MP 0x14055FD50 - {"getthrottle", 0x8286}, // SP 0x140465E30 MP 0x14055FDE0 - {"turnengineoff", 0x8287}, // SP 0x140465EA0 MP 0x14055FE50 - {"turnengineon", 0x8288}, // SP 0x140465F00 MP 0x14055FEC0 + {"vehicle_setspeed", 0x827F}, // SP 0x1404658C0 MP 0x14055F840 + {"vehicle_setspeedimmediate", 0x8280}, // SP 0x140465930 MP 0x14055F8B0 + {"vehicle_rotateyaw", 0x8281}, // SP 0x140465AC0 MP 0x14055FA60 + {"vehicle_getspeed", 0x8282}, // SP 0x140465BE0 MP 0x14055FB80 + {"vehicle_getvelocity", 0x8283}, // SP 0x140465CD0 MP 0x14055FC70 + {"vehicle_getbodyvelocity", 0x8284}, // SP 0x140465D40 MP 0x14055FCE0 + {"vehicle_getsteering", 0x8285}, // SP 0x140465DB0 MP 0x14055FD50 + {"vehicle_getthrottle", 0x8286}, // SP 0x140465E30 MP 0x14055FDE0 + {"vehicle_turnengineoff", 0x8287}, // SP 0x140465EA0 MP 0x14055FE50 + {"vehicle_turnengineon", 0x8288}, // SP 0x140465F00 MP 0x14055FEC0 {"_meth_8289", 0x8289}, // SP 0x140465F60 MP 0x000000000 {"getgoalspeedmph", 0x828A}, // SP 0x140466020 MP 0x14055FF30 {"_meth_828b", 0x828B}, // SP 0x140466090 MP 0x14055FFA0 @@ -1457,36 +1457,36 @@ namespace scripting {"visionsyncwithplayer", 0x82A0}, // SP 0x000000000 MP 0x14032ED90 {"showhudsplash", 0x82A1}, // SP 0x140263850 MP 0x14032FB10 {"setperk", 0x82A2}, // SP 0x140265490 MP 0x1403297E0 - {"_meth_82a3", 0x82A3}, // SP 0x1402659A0 MP 0x140329D00 - {"_meth_82a4", 0x82A4}, // SP 0x1402661B0 MP 0x14032A460 - {"_meth_82a5", 0x82A5}, // SP 0x140265D40 MP 0x14032A0A0 + {"hasperk", 0x82A3}, // SP 0x1402659A0 MP 0x140329D00 + {"clearperks", 0x82A4}, // SP 0x1402661B0 MP 0x14032A460 + {"unsetperk", 0x82A5}, // SP 0x140265D40 MP 0x14032A0A0 {"registerparty", 0x82A6}, // SP 0x000000000 MP 0x1403323C0 - {"_meth_82a7", 0x82A7}, // SP 0x000000000 MP 0x1403324F0 - {"_meth_82a8", 0x82A8}, // SP 0x1405D92F0 MP 0x14032A8F0 - {"_meth_82a9", 0x82A9}, // SP 0x1405D92F0 MP 0x14032A900 + {"getfireteammembers", 0x82A7}, // SP 0x000000000 MP 0x1403324F0 + {"noclip", 0x82A8}, // SP 0x1405D92F0 MP 0x14032A8F0 + {"ufo", 0x82A9}, // SP 0x1405D92F0 MP 0x14032A900 {"moveto", 0x82AA}, // SP 0x1402B2A10 MP 0x14037E950 - {"rotatepitch", 0x82AB}, // SP 0x1402B2F60 MP 0x14037EEB0 - {"rotateyaw", 0x82AC}, // SP 0x1402B2F70 MP 0x14037EEC0 - {"rotateroll", 0x82AD}, // SP 0x1402B2F90 MP 0x14037EEE0 + {"movex", 0x82AB}, // SP 0x1402B2F60 MP 0x14037EEB0 + {"movey", 0x82AC}, // SP 0x1402B2F70 MP 0x14037EEC0 + {"movez", 0x82AD}, // SP 0x1402B2F90 MP 0x14037EEE0 {"movegravity", 0x82AE}, // SP 0x1402B2C10 MP 0x14037EB00 - {"_meth_82af", 0x82AF}, // SP 0x1402B2D70 MP 0x14037EC90 - {"_meth_82b0", 0x82B0}, // SP 0x1402B2EE0 MP 0x14037EE20 + {"moveslide", 0x82AF}, // SP 0x1402B2D70 MP 0x14037EC90 + {"stopmoveslide", 0x82B0}, // SP 0x1402B2EE0 MP 0x14037EE20 {"rotateto", 0x82B1}, // SP 0x1402B3030 MP 0x14037EF10 - {"_meth_82b2", 0x82B2}, // SP 0x1402B3460 MP 0x14037F060 + {"rotatepitch", 0x82B2}, // SP 0x1402B3460 MP 0x14037F060 {"rotateyaw", 0x82B3}, // SP 0x1402B3470 MP 0x14037F070 - {"_meth_82b4", 0x82B4}, // SP 0x1402B3490 MP 0x14037F090 // looks similar to moveto/rotateto, wtf + {"rotateroll", 0x82B4}, // SP 0x1402B3490 MP 0x14037F090 // looks similar to moveto/rotateto, wtf {"addpitch", 0x82B5}, // SP 0x1402B3410 MP 0x14037F010 {"addyaw", 0x82B6}, // SP 0x1402B3430 MP 0x14037F030 - {"addoll", 0x82B7}, // SP 0x1402B3450 MP 0x14037F050 - {"_meth_82b8", 0x82B8}, // SP 0x1402B34B0 MP 0x14037F0B0 + {"addroll", 0x82B7}, // SP 0x1402B3450 MP 0x14037F050 + {"vibrate", 0x82B8}, // SP 0x1402B34B0 MP 0x14037F0B0 {"rotatevelocity", 0x82B9}, // SP 0x1402B3700 MP 0x14037F3C0 {"solid", 0x82BA}, // SP 0x1402B45E0 MP 0x1403808A0 {"notsolid", 0x82BB}, // SP 0x1402B4690 MP 0x140380950 {"setcandamage", 0x82BC}, // SP 0x1402B3880 MP 0x14037F590 {"setcanradiusdamage", 0x82BD}, // SP 0x1402B38E0 MP 0x14037F5F0 {"physicslaunchclient", 0x82BE}, // SP 0x1402B3960 MP 0x14037F670 - {"_meth_82bf", 0x82BF}, // SP 0x000000000 MP 0x1403351A0 - {"_meth_82c0", 0x82C0}, // SP 0x000000000 MP 0x1403351B0 + {"setcardicon", 0x82BF}, // SP 0x000000000 MP 0x1403351A0 + {"setcardnameplate", 0x82C0}, // SP 0x000000000 MP 0x1403351B0 {"setcarddisplayslot", 0x82C1}, // SP 0x000000000 MP 0x1403351C0 {"regweaponforfxremoval", 0x82C2}, // SP 0x000000000 MP 0x1403352B0 {"laststandrevive", 0x82C3}, // SP 0x000000000 MP 0x140331E00 @@ -1509,15 +1509,15 @@ namespace scripting {"visionsetthermalforplayer", 0x82D4}, // SP 0x140263710 MP 0x14032FD20 {"visionsetpainforplayer", 0x82D5}, // SP 0x140263730 MP 0x14032FD40 {"setblurforplayer", 0x82D6}, // SP 0x140264890 MP 0x140330B80 - {"_meth_82d7", 0x82D7}, // SP 0x140264C80 MP 0x140331310 - {"_meth_82d8", 0x82D8}, // SP 0x140264C80 MP 0x140331330 - {"_meth_82d9", 0x82D9}, // SP 0x1402ABE90 MP 0x000000000 - {"getbuildnumber", 0x82DA}, // SP 0x1402663A0 MP 0x14032A910 - {"_meth_82db", 0x82DB}, // SP 0x140266AF0 MP 0x14032AE90 - {"_meth_82dc", 0x82DC}, // SP 0x140266CD0 MP 0x14032B120 + {"getplayerweaponmodel", 0x82D7}, // SP 0x140264C80 MP 0x140331310 + {"getplayerknifemodel", 0x82D8}, // SP 0x140264C80 MP 0x140331330 + {"updateplayermodelwithweapons", 0x82D9}, // SP 0x1402ABE90 MP 0x000000000 + {"notifyonplayercommand", 0x82DA}, // SP 0x1402663A0 MP 0x14032A910 + {"canmantle", 0x82DB}, // SP 0x140266AF0 MP 0x14032AE90 + {"forcemantle", 0x82DC}, // SP 0x140266CD0 MP 0x14032B120 {"ismantling", 0x82DD}, // SP 0x140266FE0 MP 0x14032B500 {"playfx", 0x82DE}, // SP 0x140267330 MP 0x14032B9F0 - {"playerrecoilscaleon", 0x82DF}, // SP 0x140267530 MP 0x14032BD00 + {"player_recoilscaleon", 0x82DF}, // SP 0x140267530 MP 0x14032BD00 {"player_recoilscaleoff", 0x82E0}, // SP 0x140267600 MP 0x14032BDD0 {"weaponlockstart", 0x82E1}, // SP 0x1402676E0 MP 0x14032C000 {"weaponlockfinalize", 0x82E2}, // SP 0x140260240 MP 0x14032C240 diff --git a/src/client/game/scripting/functions.cpp b/src/client/game/scripting/functions.cpp index 0f233547..ab2c0b9f 100644 --- a/src/client/game/scripting/functions.cpp +++ b/src/client/game/scripting/functions.cpp @@ -69,19 +69,40 @@ namespace scripting return reinterpret_cast(method_table)[index - 0x8000]; } + + unsigned int parse_token_id(const std::string& name) + { + if (name.starts_with("_ID")) + { + return static_cast(std::strtol(name.substr(3).data(), nullptr, 10)); + } + + if (name.starts_with("_id_")) + { + return static_cast(std::strtol(name.substr(4).data(), nullptr, 16)); + } + + return 0; + } } - std::string find_token(unsigned int id) + std::vector find_token(unsigned int id) { + std::vector results; + + results.push_back(utils::string::va("_id_%X", id)); + results.push_back(utils::string::va("_ID%i", id)); + for (const auto& token : token_map) { if (token.second == id) { - return token.first; + results.push_back(token.first); + break; } } - return utils::string::va("_ID%i", id); + return results; } unsigned int find_token_id(const std::string& name) @@ -93,7 +114,13 @@ namespace scripting return result->second; } - return 0; + const auto parsed_id = parse_token_id(name); + if (parsed_id) + { + return parsed_id; + } + + return game::SL_GetCanonicalString(name.data()); } script_function find_function(const std::string& name, const bool prefer_global) diff --git a/src/client/game/scripting/functions.hpp b/src/client/game/scripting/functions.hpp index 0422bcf7..95c51763 100644 --- a/src/client/game/scripting/functions.hpp +++ b/src/client/game/scripting/functions.hpp @@ -9,7 +9,7 @@ namespace scripting using script_function = void(*)(game::scr_entref_t); - std::string find_token(unsigned int id); + std::vector find_token(unsigned int id); unsigned int find_token_id(const std::string& name); script_function find_function(const std::string& name, const bool prefer_global); diff --git a/src/client/game/scripting/lua/context.cpp b/src/client/game/scripting/lua/context.cpp index 7aaa9f89..396b3837 100644 --- a/src/client/game/scripting/lua/context.cpp +++ b/src/client/game/scripting/lua/context.cpp @@ -9,6 +9,7 @@ #include "../../../component/command.hpp" #include "../../../component/logfile.hpp" #include "../../../component/scripting.hpp" +#include "../../../component/fastfiles.hpp" #include #include @@ -17,7 +18,6 @@ namespace scripting::lua { namespace { - vector normalize_vector(const vector& vec) { const auto length = sqrt( @@ -155,12 +155,52 @@ namespace scripting::lua { return normalize_vector(a); }; + + vector_type["normalize"] = [](const vector& a) + { + return normalize_vector(a); + }; + + vector_type["toangles"] = [](const vector& a) + { + return call("vectortoangles", {a}).as(); + }; + + vector_type["toyaw"] = [](const vector& a) + { + return call("vectortoyaw", {a}).as(); + }; + + vector_type["tolerp"] = [](const vector& a) + { + return call("vectortolerp", {a}).as(); + }; + + vector_type["toup"] = [](const vector& a) + { + return call("anglestoup", {a}).as(); + }; + + vector_type["toright"] = [](const vector& a) + { + return call("anglestoright", {a}).as(); + }; + + vector_type["toforward"] = [](const vector& a) + { + return call("anglestoforward", {a}).as(); + }; } void setup_entity_type(sol::state& state, event_handler& handler, scheduler& scheduler) { state["level"] = entity{*game::levelEntityId}; + if (game::environment::is_sp()) + { + state["player"] = call("getentbynum", {0}).as(); + } + auto entity_type = state.new_usertype("entity"); for (const auto& func : method_map) @@ -462,6 +502,76 @@ namespace scripting::lua return detour; }; + + game_type["assetlist"] = [](const game&, const sol::this_state s, const std::string& type_string) + { + auto table = sol::table::create(s.lua_state()); + auto index = 1; + auto type_index = -1; + + for (auto i = 0; i < ::game::XAssetType::ASSET_TYPE_COUNT; i++) + { + if (type_string == ::game::g_assetNames[i]) + { + type_index = i; + } + } + + if (type_index == -1) + { + throw std::runtime_error("Asset type does not exist"); + } + + const auto type = static_cast<::game::XAssetType>(type_index); + fastfiles::enum_assets(type, [type, &table, &index](const ::game::XAssetHeader header) + { + const auto asset = ::game::XAsset{type, header}; + const std::string asset_name = ::game::DB_GetXAssetName(&asset); + table[index++] = asset_name; + }, true); + + return table; + }; + + game_type["sharedset"] = [](const game&, const std::string& key, const std::string& value) + { + scripting::shared_table.access([key, value](scripting::shared_table_t& table) + { + table[key] = value; + }); + }; + + game_type["sharedget"] = [](const game&, const std::string& key) + { + std::string result; + scripting::shared_table.access([key, &result](scripting::shared_table_t& table) + { + result = table[key]; + }); + return result; + }; + + game_type["sharedclear"] = [](const game&) + { + scripting::shared_table.access([](scripting::shared_table_t& table) + { + table.clear(); + }); + }; + + game_type["getentbyref"] = [](const game&, const sol::this_state s, + const unsigned int entnum, const unsigned int classnum) + { + const auto id = ::game::Scr_GetEntityId(entnum, classnum); + if (id) + { + return convert(s, scripting::entity{id}); + } + else + { + return sol::lua_value{s, sol::lua_nil}; + } + }; } } diff --git a/src/client/game/scripting/lua/engine.cpp b/src/client/game/scripting/lua/engine.cpp index d481dc76..6d025409 100644 --- a/src/client/game/scripting/lua/engine.cpp +++ b/src/client/game/scripting/lua/engine.cpp @@ -4,6 +4,7 @@ #include "../execution.hpp" #include "../../../component/logfile.hpp" +#include "../../../component/filesystem.hpp" #include @@ -11,6 +12,8 @@ namespace scripting::lua::engine { namespace { + bool running = false; + auto& get_scripts() { static std::vector> scripts{}; @@ -38,21 +41,27 @@ namespace scripting::lua::engine void stop() { + running = false; logfile::clear_callbacks(); get_scripts().clear(); } void start() { - // No SP until there is a concept - if (game::environment::is_sp()) - { - return; - } - stop(); - load_scripts("h1-mod/scripts/"); - load_scripts("data/scripts/"); + running = true; + for (const auto& path : filesystem::get_search_paths()) + { + load_scripts(path + "/scripts/"); + if (game::environment::is_sp()) + { + load_scripts(path + "/scripts/sp/"); + } + else + { + load_scripts(path + "/scripts/mp/"); + } + } } void notify(const event& e) @@ -70,4 +79,9 @@ namespace scripting::lua::engine script->run_frame(); } } + + bool is_running() + { + return running; + } } diff --git a/src/client/game/scripting/lua/engine.hpp b/src/client/game/scripting/lua/engine.hpp index 471316cd..2df0b0ef 100644 --- a/src/client/game/scripting/lua/engine.hpp +++ b/src/client/game/scripting/lua/engine.hpp @@ -8,4 +8,5 @@ namespace scripting::lua::engine void stop(); void notify(const event& e); void run_frame(); + bool is_running(); } diff --git a/src/client/game/scripting/lua/value_conversion.cpp b/src/client/game/scripting/lua/value_conversion.cpp index a838c787..a12abc8e 100644 --- a/src/client/game/scripting/lua/value_conversion.cpp +++ b/src/client/game/scripting/lua/value_conversion.cpp @@ -165,56 +165,33 @@ namespace scripting::lua auto table = sol::table::create(state); auto metatable = sol::table::create(state); - const auto offset = 64000 * (parent_id & 3); - - metatable[sol::meta_function::new_index] = [offset, parent_id](const sol::table t, const sol::this_state s, + metatable[sol::meta_function::new_index] = [parent_id](const sol::table t, const sol::this_state s, const sol::lua_value& field, const sol::lua_value& value) { - const auto id = field.is() - ? scripting::find_token_id(field.as()) - : field.as(); - - if (!id) + const auto new_variable = convert({s, value}); + if (field.is()) { - return; + scripting::set_object_variable(parent_id, field.as(), new_variable); + } + else if (field.is()) + { + scripting::set_object_variable(parent_id, field.as(), new_variable); } - - const auto variable_id = game::GetVariable(parent_id, id); - const auto variable = &game::scr_VarGlob->childVariableValue[variable_id + offset]; - const auto new_variable = convert({s, value}).get_raw(); - - game::AddRefToValue(new_variable.type, new_variable.u); - game::RemoveRefToValue(variable->type, variable->u.u); - - variable->type = (char)new_variable.type; - variable->u.u = new_variable.u; }; - metatable[sol::meta_function::index] = [offset, parent_id](const sol::table t, const sol::this_state s, + metatable[sol::meta_function::index] = [parent_id](const sol::table t, const sol::this_state s, const sol::lua_value& field) { - const auto id = field.is() - ? scripting::find_token_id(field.as()) - : field.as(); - - if (!id) + if (field.is()) { - return sol::lua_value{s, sol::lua_nil}; + return convert(s, scripting::get_object_variable(parent_id, field.as())); + } + else if (field.is()) + { + return convert(s, scripting::get_object_variable(parent_id, field.as())); } - const auto variable_id = game::FindVariable(parent_id, id); - if (!variable_id) - { - return sol::lua_value{s, sol::lua_nil}; - } - - const auto variable = game::scr_VarGlob->childVariableValue[variable_id + offset]; - - game::VariableValue result{}; - result.u = variable.u.u; - result.type = (game::scriptType_e)variable.type; - - return convert(s, result); + return sol::lua_value{s, sol::lua_nil}; }; table[sol::metatable_key] = metatable; diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index bb7c3aec..4d6b3405 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1175,7 +1175,12 @@ namespace game const char* name; char __pad0[0x118]; char textureCount; - char __pad1[7]; + char constantCount; + char stateBitsCount; + char stateFlags; + char cameraRegion; + char materialType; + char assetFlags; MaterialTechniqueSet* techniqueSet; MaterialTextureDef* textureTable; void* constantTable; @@ -1311,6 +1316,14 @@ namespace game const char* buffer; }; + struct TTF + { + const char* name; + int len; + const char* buffer; + int fontFace; + }; + struct GfxImageLoadDef { char levelCount; @@ -1382,6 +1395,7 @@ namespace game StringTable* stringTable; LuaFile* luaFile; GfxImage* image; + TTF* ttf; }; struct XAsset diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 9381150a..58058533 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -33,18 +33,23 @@ namespace game WEAK symbol Com_Frame_Try_Block_Function{0x1401CE8D0, 0x1400D8310}; WEAK symbol Com_GetCurrentCoDPlayMode{0, 0x1405039A0}; - WEAK symbol Com_InFrontEnd{0x1400E4B30, 0x140176A30}; + WEAK symbol Com_InFrontend{0x1400E4B30, 0x140176A30}; WEAK symbol Com_SetSlowMotion{0, 0x1400DB790}; WEAK symbol Com_Error{0x1403509C0, 0x1400D78A0}; WEAK symbol Com_Quit_f{0x140352BE0, 0x1400DA830}; + WEAK symbol Com_Shutdown{0x140353B70, 0x1400DB8A0}; + WEAK symbol Quit{0x140352D90, 0x1400DA830}; WEAK symbol CG_GameMessage{0x1401389A0, 0x140220CC0}; WEAK symbol CG_GameMessageBold{0x140138750, 0x140220620}; WEAK symbol CG_SetClientDvarFromServer{0, 0x140236120}; + WEAK symbol CG_GetWeaponDisplayName{0x14016EC30, 0x1400B5840}; WEAK symbol CL_IsCgameInitialized{0x14017EE30, 0x140245650}; + WEAK symbol CL_VirtualLobbyShutdown{0, 0x140256D40}; WEAK symbol Dvar_SetCommand{0x1403C72B0, 0x1404FD0A0}; WEAK symbol Dvar_FindVar{0x1403C5D50, 0x1404FBB00}; @@ -70,7 +75,7 @@ namespace game WEAK symbol FS_Startup{0x1403B85D0, 0x1404EDD30}; WEAK symbol FS_AddLocalizedGameDirectory{0x1403B6030, 0x1404EBE20}; - WEAK symbol GetVariable{0x14036FDD0, 0x1403F3730}; + WEAK symbol GetVariable{0x14036FDD0, 0x14043DD70}; WEAK symbol GetNewVariable{0x14036FA00, 0x14043D990}; WEAK symbol GetNewArrayVariable{0x14036F880, 0x14043D810}; WEAK symbol GScr_LoadConsts{0x1402D13E0, 0x140393810}; @@ -136,6 +141,7 @@ namespace game WEAK symbol Scr_ClearOutParams{0x140374460, 0x140442510}; WEAK symbol Scr_GetEntityIdRef{0x140372D50, 0x140440D80}; WEAK symbol Scr_GetEntityId{0x140372CA0, 0x140440CD0}; + WEAK symbol Scr_SetObjectField{0x1402B9F60, 0x140385330}; WEAK symbol ScrPlace_GetViewPlacement{0x1401981F0, 0x140288550}; @@ -143,16 +149,22 @@ namespace game DB_EnumXAssets_Internal{0x1401C9C10, 0x1402BA830}; WEAK symbol DB_GetXAssetName{0x14019A390, 0x14028BE50}; WEAK symbol DB_GetXAssetTypeSize{0x14019A3B0, 0x14028BE70}; + WEAK symbol DB_FindXAssetHeader{0x1401CA150, 0x1402BAC70}; WEAK symbol LUI_OpenMenu{0x14039D5F0, 0x1404CD210}; + WEAK symbol LUI_BeginEvent{0x1400D27F0, 0x140161A00}; + WEAK symbol LUI_EndEvent{0x1400D3A80, 0x140162CD0}; + WEAK symbol LUI_EnterCriticalSection{0x1400D3B70, 0x140162DC0}; + WEAK symbol LUI_LeaveCriticalSection{0x1400D8DB0, 0x140168150}; WEAK symbol Menu_IsMenuOpenAndVisible{0x1404709C0, 0x1404C7320}; WEAK symbol SL_FindString{0x14036D700, 0x14043B470}; WEAK symbol SL_GetString{0x14036D9A0, 0x14043B840}; - WEAK symbol SL_ConvertToString{0x14036D420, 0x14043B170}; - WEAK symbol Scr_SetObjectField{0x1402B9F60, 0x140385330}; + WEAK symbol SL_ConvertToString{0x14036D420, 0x14043B170}; + WEAK symbol SL_GetCanonicalString{0x14036A310, 0x140437EA0}; WEAK symbol SV_DirectConnect{0, 0x140480860}; WEAK symbol SV_Cmd_ArgvBuffer{0x1403446C0, 0x140404CA0}; @@ -266,5 +278,6 @@ namespace game int internal_, int profilerTreatClosureAsFunc)> cclosure_Create{0x14008B5D0, 0x14011B540}; WEAK symbol hksi_luaL_ref{0x1400A64D0, 0x140136D30}; WEAK symbol hksi_luaL_unref{0x14009EF10, 0x14012F610}; + WEAK symbol closePendingUpvalues{0x14008EA00, 0x14011E970}; } } diff --git a/src/client/game/ui_scripting/execution.cpp b/src/client/game/ui_scripting/execution.cpp index fff6b88e..716fe1ca 100644 --- a/src/client/game/ui_scripting/execution.cpp +++ b/src/client/game/ui_scripting/execution.cpp @@ -37,17 +37,56 @@ namespace ui_scripting return values; } + bool notify(const std::string& name, const event_arguments& arguments) + { + const auto state = *game::hks::lua_state; + if (!state) + { + return false; + } + + const auto _1 = gsl::finally(game::LUI_LeaveCriticalSection); + game::LUI_EnterCriticalSection(); + + try + { + const auto globals = table((*::game::hks::lua_state)->globals.v.table); + const auto engine = globals.get("Engine").as(); + const auto root = engine.get("GetLuiRoot").as().call({})[0].as(); + const auto process_event = root.get("processEvent").as(); + + table event{}; + event.set("name", name); + + for (const auto& arg : arguments) + { + event.set(arg.first, arg.second); + } + + process_event.call({root, event}); + return true; + } + catch (const std::exception& e) + { + printf("Error processing event '%s' %s\n", name.data(), e.what()); + return false; + } + } + arguments call_script_function(const function& function, const arguments& arguments) { const auto state = *game::hks::lua_state; - state->m_apistack.top = state->m_apistack.base; + stack stack; push_value(function); for (auto i = arguments.begin(); i != arguments.end(); ++i) { push_value(*i); } + const auto num_args = static_cast(arguments.size()); + stack.save(num_args + 1); + const auto _1 = gsl::finally(&disable_error_hook); enable_error_hook(); @@ -59,6 +98,7 @@ namespace ui_scripting } catch (const std::exception& e) { + stack.fix(); throw std::runtime_error(std::string("Error executing script function: ") + e.what()); } } @@ -66,9 +106,10 @@ namespace ui_scripting script_value get_field(const userdata& self, const script_value& key) { const auto state = *game::hks::lua_state; - state->m_apistack.top = state->m_apistack.base; + stack stack; push_value(key); + stack.save(1); const auto _1 = gsl::finally(&disable_error_hook); enable_error_hook(); @@ -85,16 +126,18 @@ namespace ui_scripting } catch (const std::exception& e) { - throw std::runtime_error(std::string("Error getting userdata field: ") + e.what()); + stack.fix(); + throw std::runtime_error("Error getting userdata field: "s + e.what()); } } script_value get_field(const table& self, const script_value& key) { const auto state = *game::hks::lua_state; - state->m_apistack.top = state->m_apistack.base; + stack stack; push_value(key); + stack.save(1); const auto _1 = gsl::finally(&disable_error_hook); enable_error_hook(); @@ -111,14 +154,17 @@ namespace ui_scripting } catch (const std::exception& e) { - throw std::runtime_error(std::string("Error getting table field: ") + e.what()); + stack.fix(); + throw std::runtime_error("Error getting table field: "s + e.what()); } } void set_field(const userdata& self, const script_value& key, const script_value& value) { const auto state = *game::hks::lua_state; - state->m_apistack.top = state->m_apistack.base; + + stack stack; + stack.save(0); const auto _1 = gsl::finally(&disable_error_hook); enable_error_hook(); @@ -133,14 +179,17 @@ namespace ui_scripting } catch (const std::exception& e) { - throw std::runtime_error(std::string("Error setting userdata field: ") + e.what()); + stack.fix(); + throw std::runtime_error("Error setting userdata field: "s + e.what()); } } void set_field(const table& self, const script_value& key, const script_value& value) { const auto state = *game::hks::lua_state; - state->m_apistack.top = state->m_apistack.base; + + stack stack; + stack.save(0); const auto _1 = gsl::finally(&disable_error_hook); enable_error_hook(); @@ -155,7 +204,8 @@ namespace ui_scripting } catch (const std::exception& e) { - throw std::runtime_error(std::string("Error setting table field: ") + e.what()); + stack.fix(); + throw std::runtime_error("Error setting table field: "s + e.what()); } } } diff --git a/src/client/game/ui_scripting/execution.hpp b/src/client/game/ui_scripting/execution.hpp index 24f4dd72..4a3d3562 100644 --- a/src/client/game/ui_scripting/execution.hpp +++ b/src/client/game/ui_scripting/execution.hpp @@ -9,6 +9,8 @@ namespace ui_scripting script_value get_return_value(int offset); arguments get_return_values(int count); + bool notify(const std::string& name, const event_arguments& arguments); + arguments call_script_function(const function& function, const arguments& arguments); script_value get_field(const userdata& self, const script_value& key); diff --git a/src/client/game/ui_scripting/lua/context.cpp b/src/client/game/ui_scripting/lua/context.cpp index 96286cdd..d9a930f9 100644 --- a/src/client/game/ui_scripting/lua/context.cpp +++ b/src/client/game/ui_scripting/lua/context.cpp @@ -2,6 +2,7 @@ #include "context.hpp" #include "error.hpp" #include "value_conversion.hpp" +#include "../../scripting/execution.hpp" #include "../script_value.hpp" #include "../execution.hpp" @@ -10,6 +11,9 @@ #include "../../../component/updater.hpp" #include "../../../component/fps.hpp" #include "../../../component/localized_strings.hpp" +#include "../../../component/fastfiles.hpp" +#include "../../../component/scripting.hpp" +#include "../../../component/mods.hpp" #include "component/game_console.hpp" #include "component/scheduler.hpp" @@ -22,6 +26,31 @@ namespace ui_scripting::lua { namespace { + const auto json_script = utils::nt::load_resource(LUA_JSON); + + void setup_json(sol::state& state) + { + const auto json = state.safe_script(json_script, &sol::script_pass_on_error); + handle_error(json); + state["json"] = json; + } + + void setup_io(sol::state& state) + { + state["io"]["fileexists"] = utils::io::file_exists; + state["io"]["writefile"] = utils::io::write_file; + state["io"]["movefile"] = utils::io::move_file; + state["io"]["filesize"] = utils::io::file_size; + state["io"]["createdirectory"] = utils::io::create_directory; + state["io"]["directoryexists"] = utils::io::directory_exists; + state["io"]["directoryisempty"] = utils::io::directory_is_empty; + state["io"]["listfiles"] = utils::io::list_files; + state["io"]["copyfolder"] = utils::io::copy_folder; + state["io"]["removefile"] = utils::io::remove_file; + state["io"]["removedirectory"] = utils::io::remove_directory; + state["io"]["readfile"] = static_cast(utils::io::read_file); + } + void setup_types(sol::state& state, scheduler& scheduler) { struct game @@ -68,6 +97,78 @@ namespace ui_scripting::lua localized_strings::override(string, value); }; + game_type["sharedset"] = [](const game&, const std::string& key, const std::string& value) + { + scripting::shared_table.access([key, value](scripting::shared_table_t& table) + { + table[key] = value; + }); + }; + + game_type["sharedget"] = [](const game&, const std::string& key) + { + std::string result; + scripting::shared_table.access([key, &result](scripting::shared_table_t& table) + { + result = table[key]; + }); + return result; + }; + + game_type["sharedclear"] = [](const game&) + { + scripting::shared_table.access([](scripting::shared_table_t& table) + { + table.clear(); + }); + }; + + game_type["assetlist"] = [](const game&, const sol::this_state s, const std::string& type_string) + { + auto table = sol::table::create(s.lua_state()); + auto index = 1; + auto type_index = -1; + + for (auto i = 0; i < ::game::XAssetType::ASSET_TYPE_COUNT; i++) + { + if (type_string == ::game::g_assetNames[i]) + { + type_index = i; + } + } + + if (type_index == -1) + { + throw std::runtime_error("Asset type does not exist"); + } + + const auto type = static_cast<::game::XAssetType>(type_index); + fastfiles::enum_assets(type, [type, &table, &index](const ::game::XAssetHeader header) + { + const auto asset = ::game::XAsset{type, header}; + const std::string asset_name = ::game::DB_GetXAssetName(&asset); + table[index++] = asset_name; + }, true); + + return table; + }; + + game_type["getweapondisplayname"] = [](const game&, const std::string& name) + { + const auto alternate = name.starts_with("alt_"); + const auto weapon = ::game::G_GetWeaponForName(name.data()); + + char buffer[0x400] = {0}; + ::game::CG_GetWeaponDisplayName(weapon, alternate, buffer, 0x400); + + return std::string(buffer); + }; + + game_type["getloadedmod"] = [](const game&) + { + return mods::mod_path; + }; + auto userdata_type = state.new_usertype("userdata_"); userdata_type["new"] = sol::property( @@ -193,6 +294,51 @@ namespace ui_scripting::lua updater_table["getcurrentfile"] = updater::get_current_file; state["updater"] = updater_table; + + if (::game::environment::is_sp()) + { + struct player + { + }; + auto player_type = state.new_usertype("player_"); + state["player"] = player(); + + player_type["notify"] = [](const player&, const sol::this_state s, const std::string& name, sol::variadic_args va) + { + if (!::game::CL_IsCgameInitialized() || !::game::sp::g_entities[0].client) + { + throw std::runtime_error("Not in game"); + } + + const sol::state_view view{s}; + const auto to_string = view["tostring"].get(); + + std::vector args{}; + for (auto arg : va) + { + args.push_back(to_string.call(arg).get()); + } + + ::scheduler::once([s, name, args]() + { + try + { + std::vector arguments{}; + + for (const auto& arg : args) + { + arguments.push_back(arg); + } + + const auto player = scripting::call("getentbynum", {0}).as(); + scripting::notify(player, name, arguments); + } + catch (...) + { + } + }, ::scheduler::pipeline::server); + }; + } } } @@ -207,6 +353,8 @@ namespace ui_scripting::lua sol::lib::math, sol::lib::table); + setup_json(this->state_); + setup_io(this->state_); setup_types(this->state_, this->scheduler_); if (type == script_type::file) diff --git a/src/client/game/ui_scripting/lua/engine.cpp b/src/client/game/ui_scripting/lua/engine.cpp index 8faf198c..a52b1b9e 100644 --- a/src/client/game/ui_scripting/lua/engine.cpp +++ b/src/client/game/ui_scripting/lua/engine.cpp @@ -3,6 +3,7 @@ #include "context.hpp" #include "../../../component/ui_scripting.hpp" +#include "../../../component/filesystem.hpp" #include #include @@ -52,8 +53,18 @@ namespace ui_scripting::lua::engine load_code(lui_common); load_code(lui_updater); - load_scripts("h1-mod/ui_scripts/"); - load_scripts("data/ui_scripts/"); + for (const auto& path : filesystem::get_search_paths()) + { + load_scripts(path + "/ui_scripts/"); + if (game::environment::is_sp()) + { + load_scripts(path + "/ui_scripts/sp/"); + } + else + { + load_scripts(path + "/ui_scripts/mp/"); + } + } } void stop() diff --git a/src/client/game/ui_scripting/script_value.hpp b/src/client/game/ui_scripting/script_value.hpp index 3de52ddf..a16d2622 100644 --- a/src/client/game/ui_scripting/script_value.hpp +++ b/src/client/game/ui_scripting/script_value.hpp @@ -53,4 +53,5 @@ namespace ui_scripting }; using arguments = std::vector; + using event_arguments = std::unordered_map; } diff --git a/src/client/game/ui_scripting/types.cpp b/src/client/game/ui_scripting/types.cpp index 37032b1b..66e8d497 100644 --- a/src/client/game/ui_scripting/types.cpp +++ b/src/client/game/ui_scripting/types.cpp @@ -273,4 +273,38 @@ namespace ui_scripting { return call_script_function(*this, arguments); } + + /*************************************************************** + * Stack + **************************************************************/ + + stack::stack() + { + this->state = *game::hks::lua_state; + this->state->m_apistack.top = this->state->m_apistack.base; + } + + void stack::save(int num_args) + { + this->num_args_ = num_args; + this->num_calls_ = state->m_numberOfCCalls; + this->base_bottom_ = state->m_apistack.base - state->m_apistack.bottom; + this->top_bottom_ = state->m_apistack.top - state->m_apistack.bottom; + this->callstack_ = state->m_callStack.m_current - state->m_callStack.m_records; + } + + void stack::fix() + { + this->state->m_numberOfCCalls = this->num_calls_; + + game::hks::closePendingUpvalues(this->state, &this->state->m_apistack.bottom[this->top_bottom_ - this->num_args_]); + this->state->m_callStack.m_current = &this->state->m_callStack.m_records[this->callstack_]; + + this->state->m_apistack.base = &this->state->m_apistack.bottom[this->base_bottom_]; + this->state->m_apistack.top = &this->state->m_apistack.bottom[this->top_bottom_ - static_cast(this->num_args_ + 1)]; + + this->state->m_apistack.bottom[this->top_bottom_].t = this->state->m_apistack.top[-1].t; + this->state->m_apistack.bottom[this->top_bottom_].v.ptr = this->state->m_apistack.top[-1].v.ptr; + this->state->m_apistack.top = &this->state->m_apistack.bottom[this->top_bottom_ + 1]; + } } diff --git a/src/client/game/ui_scripting/types.hpp b/src/client/game/ui_scripting/types.hpp index 1924407f..bc2f7216 100644 --- a/src/client/game/ui_scripting/types.hpp +++ b/src/client/game/ui_scripting/types.hpp @@ -86,4 +86,28 @@ namespace ui_scripting int ref{}; }; + + class stack final + { + public: + stack(); + + void save(int num_args); + void fix(); + + stack(stack&&) = delete; + stack(const stack&) = delete; + stack& operator=(stack&&) = delete; + stack& operator=(const stack&) = delete; + + private: + game::hks::lua_State* state; + + int num_args_; + int num_calls_; + + uint64_t base_bottom_; + uint64_t top_bottom_; + uint64_t callstack_; + }; } diff --git a/src/client/resource.hpp b/src/client/resource.hpp index adc92d9c..ceb43a33 100644 --- a/src/client/resource.hpp +++ b/src/client/resource.hpp @@ -18,3 +18,5 @@ #define LUI_COMMON 309 #define LUI_UPDATER 310 + +#define LUA_JSON 311 diff --git a/src/client/resource.rc b/src/client/resource.rc index c53c106a..2996abfa 100644 --- a/src/client/resource.rc +++ b/src/client/resource.rc @@ -121,6 +121,8 @@ ICON_IMAGE RCDATA "resources/icon.png" LUI_COMMON RCDATA "resources/ui_scripts/common.lua" LUI_UPDATER RCDATA "resources/ui_scripts/updater.lua" +LUA_JSON RCDATA "resources/json.lua" + #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// diff --git a/src/client/resources/json.lua b/src/client/resources/json.lua new file mode 100644 index 00000000..711ef786 --- /dev/null +++ b/src/client/resources/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/src/client/resources/main.html b/src/client/resources/main.html index 887b6863..52a2756f 100644 --- a/src/client/resources/main.html +++ b/src/client/resources/main.html @@ -18,8 +18,7 @@ body { -ms-overflow-style: none; margin: 0; - /* Also we can use image incoder https://www.base64-image.de/ */ - background: url(https://cdn.discordapp.com/attachments/895680402142941194/935150813901365268/360_F_176672598_cJ4yPCFhxvDXm9Cu7vDLIcXpvTMQJ9zm.jpg) no-repeat center center fixed; + background: url('data:image/jpeg;base64,/9j/4QAWRXhpZgAATU0AKgAAAAgAAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAA8cAVoAAxslRxwBAAACAAQA/+EMgWh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSfvu78nIGlkPSdXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQnPz4KPHg6eG1wbWV0YSB4bWxuczp4PSdhZG9iZTpuczptZXRhLycgeDp4bXB0az0nSW1hZ2U6OkV4aWZUb29sIDEwLjEwJz4KPHJkZjpSREYgeG1sbnM6cmRmPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjJz4KCiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0nJwogIHhtbG5zOnRpZmY9J2h0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvJz4KICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogIDx0aWZmOlhSZXNvbHV0aW9uPjMwMC8xPC90aWZmOlhSZXNvbHV0aW9uPgogIDx0aWZmOllSZXNvbHV0aW9uPjMwMC8xPC90aWZmOllSZXNvbHV0aW9uPgogPC9yZGY6RGVzY3JpcHRpb24+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczp4bXBNTT0naHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyc+CiAgPHhtcE1NOkRvY3VtZW50SUQ+YWRvYmU6ZG9jaWQ6c3RvY2s6Y2YxZTI3ZTYtN2Q3ZS00MTg4LTlhOGItNzAyNzdiOTRmZmExPC94bXBNTTpEb2N1bWVudElEPgogIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6ZWQwYmQ4OTItYWMzOS00MTdmLWFkNmQtNTQyM2EyY2IzNDI5PC94bXBNTTpJbnN0YW5jZUlEPgogPC9yZGY6RGVzY3JpcHRpb24+CjwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9J3cnPz7/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/wAALCAFoAhwBAREA/8QAGQABAQEBAQEAAAAAAAAAAAAAAQIAAwQH/8QAGRABAQEBAQEAAAAAAAAAAAAAAAERAhJh/9oACAEBAAA/APhXNdOavmunNdOa6c1fNdOa6c105rpzV810lXzVyrlXKuVcqpVyrlVKqVUqpVSnTp062tra2totGi0Wi1NqbU2ptTam1FqbUWotTai1FqLUWotRai1HVRa52o6qLUWo6qLUWvPzXTmr5rpzXTmr5rpzXTmunNXzXTmunNXKvmrlXKuVcqpVyrlVKqVUqpVTpUplMp062tp0a3oaLRaLRam0WptTai1NqbUWptRanqotRai1FqLUWotR1UWotR1UWotR1UWotcOavmr5rpzXTmr5rpzXTmr5rpzV810lXKvmrlXKuVUq5VyqlVKuVUqpVSmVUradOtra2traNGtqbRam0WptTam1NqLU2ptRai1NqLUWotRai1Nrnai1FqLU2udqbUWotcOavmunNXzXTmr5rpzV8105q+avmunNXKuVcq5VSrlXKqVUq5VSqlVKqUyqlOtp1tOto1tGj0NFotFqbU2ptTam1NqbUWptRam1FqLUWp6qLUWotRai1FqLUWotRam1w5q+a6c1fNXzXSVfNdOavmr5rpzV81cq5VyqlXKuVUq5VSqlVKqVUqpTKqdHTra2nRra2jW0WjU2i1NotTam1NqbUWptTai1NqLUWptRai1FqLU2otRai1HVRai1OuHNXzXTmr5q5XSVfNXzV8105q+auVcq5VSrlXKqVcqpVSqlXKZVSqlMqtOnW062to1tGto0am0WptFqbU2ptTam1NqLU2otTai1NqLUWptRai1FqLUWptRai1OuErpzVyr5q+auVcrpKvmr5q5VyrlXKqVcqpVyqlXKqVUqpVSmVUplVKZTradb03oa2totGi0WptFqbRam1NqbU2otTam1FqbUWptRam1FqLUWptRai1FqbUWptcZVyrlXKuVfNXzVyr5q5V81cq5VyqlXKqVcqpVSqlVKqVUqpTKZVadOnW1tbW0a2jRo0WptFotTam1NqbU2ptTam1FqbUWptRam1FqbUWotTai1FqbUWptcZVyrlXKvmrlXKuVcq5VyrlVKuVUq5VSqlXKqVUqpTKqVUplVplMp1vR1tbW0a2i0aLRaLRam1NotTam1NqbU2otTam1FqbU2otTai1FqbUWptRai1NqdcZV81cq5VSrlXKuVcq5VyrlVKuVUq5VSqlVKqVUqpVSqlMqpTKdOnTra2tra2jRotGjRaLU2i1NqbRam1NqLU2ptTam1FqbUWptRam1FqbUWptRam1OuMq5VyrlVKuVcq5VSr5q5VyqlXKqVUqpVyqlVKqUyqlVKZVTo6Z0fR9N6PpvTa3oa2jW9C9C9J0XoXpNotTam1NqbU2ptTam1NqLU2ptRam1FqbUWptRam1NqdcZVyqlXKuVUq5VyqlXKuVUq5VSqlXKqVUqpVSqlM6VOjOlTozoyq9N6Po+m9H03pvQ9N6HoXoeheheheham9C1NqbU2i1NqbUWptTam1NqLU2ptRam1NqLU2otTanXKVUq5VSrlXKqVcqpVyrlVKqVcqpVSqlVKqVUqpTOlSmdKnRnRnR0zpvR9H03pvTem0em9D0PQvQtFqbRaLU2ptTaLU2ptTam1NqbUWptTam1FqbU2otTam1NqdcpVSqlXKqVcqpVyqlXKqVcqpVSqlVKqVUqpVSmVUqp0Z0Z0dV6b0fR9H03pvR9N6HpvQ9N6F6FotFqbRaLU2ptFqbU2ptTam1NqbU2ptTam1FqbU2ptTai1No1xlXKqVUq5VSrlVKqVcqpVSrlVKqVUplVKqVUpnSp0dVOjKfR9HTp9Np1vTem9N6Gto9DW1NranRam0WptFqbU2i1NqbU2ptTam1NqbU2otTam1NqbU2ptcpVSrlVKqVUq5VSqlXKqVUqpVSqlVKqVUplVKqUymVU6OnTp9N6Po63pvR9N6b0PTeheh6GjRaLRaLU2i1NqbRam1NqbRam1NqbU2ptTam1NqLU2ptTaNcpVSqlVKuVUqpVSrlVKqVUqpVSqlVKZVSmVUqpTKdVKZ0fR063o+m9H03pvTem9NrWjR6Gi0XoaLRam0WptFqbRam1NqbU2i1NqbU2ptTam1NqbU2ptTa5yqlVKqVUqpVSqlXKqVUqpVSmVUqpVSmVUplVKZTKdVplOtp0+m0+m1vTem1vTaNGi0aLRaLRaLU2i1NotTam0WptTam0WptTam1NqbU2ptFqbU65yqlVKqVUqpVSqlVKqVUqpVSmVUqpTKqUyqlMp1Up0ynT6bT6Otrej6bW1tb0NbR6GtaNTotFo0WptFqbRam0WptTam0WptTam1NotTam1NqbRrnKZVSqlVKqVUqpVSqlVKqUyqlVKZVSmVUplMqtOmU6ZTradbTradbW1tbRraNGtotGi1No0aLU2i1NotTaLU2ptFqbU2i1NqbU2i1NqbRrnKZVSqlVKqVUqpTKqVUqpTKqVUplMqpTKqUymUymU6dOtp062tp1vTem9N6Gto1tGi0Wi0Wi0WptFotTaLU2i1NqbRam0WptTaLU2ptFqbRrnKZVSqlVKZVSqlVKqUyqlMqpTKqUyqlMplMplOnTp06dbTra2nW1tbW0a2to0aNFo0Wi0Wi0WptFotTaLU2i1NotTam0WptFqbU2i0a5ymVUqpTKqVUqpTKqVUplVKZTKqUymVUp0ymU6dOtp062nW062tra2traNbRraNGjRaLRotFqbRaLU2i1NotFqbU2i1NotTaLU2i1OolMqpTKqVUplVKqUyqlMqpTKZVSmU6ZVSmU6dbTp062nW062tra2tra2to1tGjW0aLRaNFqbRaLRam0Wi1Oi1NotFqbRam0WptFo1zlMqpTKqUyqlVKZVSmVUplMqpTKZTp06dOnTp062nW062tradbW0a2traNbRo0WjWtGptFotFotFqbRaLU2i0WptFqbRam0Wi0a5ymVUplVKZVSmVUplVKZTKqU6ZTplOmU6dOnTradbTradbW1tbW1tbW0a2jW0aNGjRaLRotFotGptFotFqbRaLRam0am0Wi0a5yqlMplVKdVKZVSmUyqlOmUynTplOmU6dOnW06dbTra2nW1tbW1tbRra2jW0aNGjRo1tTaLRaLRaLRam0Wi0am0Wi1OjRaNRKZTKqUymVUplMqpTplMp06qVtVradOnTradOtp1tOtra2tp0a2tra2jW0a2jRaNbRotGjRaLU6LRaNFotTaNFotFqbRaESmUyqlMplVKZTplVKdOmU6ZTKdMp1tOnTradbTra2nW1tbW1tbW1tbRraNbRraLRotGjRaNFotGi0WjU2i0WjRaLRqbQiKhlMplMqpTplMp0ymU6qVtOnTp062nTradbTra2nW1tbW1tbW1tbRrb9Gto1tGjRo1rRotGjRaNTotGjRaNGi0WjQgwynTKqUymUynTplOmU6dOnW06dOtp062nfra2nW1tbW362/W362tra2jW1tGto0a1o0WjRaNFo0aLRo0WjRaLRotFotCITqjDKdMp06ZTplOnTp1tOnTradOtrb9Otradbfrb9bTra2to362/W362jW0b9bW0aNFo1tGi0aLRo0WjRaNGijRoo1kSmGU6TplMp0ynTp06dbTp0ytp062nW062nW1tOtra2tra2traNbW0a2jW0a2jRo1rRotGi0aNGjWo0UaNA0JlJlbTDph06ZTplOmU62nTK2nTraZTra2nfra2nW1tbfra2trb9bW1tGtraNbW0aNbRo0a2jRo0aNa0aKNA0aG0IlJOk6Tp0tp06dOtp062nW06dbW062tp362tp1tbW1tbW0a2tv0a2traNbRo1tFo1rRotGitoGgaG0DQNSdJ1jKTp0nW06dOtpOtp1tOtp1tOtv1tOtra2tv1t+tv1t+tv1tbW1tGtraNbW0aNbRo1tGjW0DQNYaBoFrDUmVjpMpLadJ1jpbTp1tOtp1tOtradZtOtra2tra2tra2to1tbRra2jW0a2jQ2ga2gaG0DQ2gaG1OsZSdLaTp1jpbTp1tLadbTra2nW062tra2nfrb9bfra2tv1t+tv0a2traNbRra2jW0aw0No0NoGhtA1gNZLGUtpOltJbSdbS2nWOtpbTraxbW1tbW062tra2jW1tbW1g2to1hrawGsNGsNDaA2gaw0JMpbSTrHS2nWOnWOtpbTrMdbW1tLa2tp1tbW1tbW0a2trDW1tbWDaNYa2htGhtA1tA1gGGplJbSdLHWOljrFtOs2nW0tra2nWbTra2tra2tra2tobWbW0a2sNbRrNo1ho1hraBrBtA1gGOsdLHSx1jraWOtrHW06zNp1tbWLM2tra2trMwZtbRrM2jW0NraGGhhrBtDaA2hJ0tpY6WOsW06x1tLNp1tYtra2nW1mZmZmZmZtGtrawbW0azDW0No1ho1m0aGGsAx1i2ljpZtOlm06zHW0szadbW1mZizMwZmbW1tGszDW0M2jWYaGbQG0azDQG0sdLNpY62ljraWbW06zHW1tbW0szMzMzMzDW1tbW0M2jW1mGtoYawbRrMNDNr/2Q==') no-repeat center center fixed; background-size: cover; background-position: center; background-repeat: no-repeat; @@ -416,8 +415,7 @@
- - +
Singleplayer @@ -435,7 +433,7 @@
- +
Multiplayer @@ -462,7 +460,7 @@
-
+

Special thanks to all contributors.

This project is based on S1x and H2-mod. diff --git a/src/common/utils/io.cpp b/src/common/utils/io.cpp index 4968f449..9b161d39 100644 --- a/src/common/utils/io.cpp +++ b/src/common/utils/io.cpp @@ -104,6 +104,11 @@ namespace utils::io return std::filesystem::is_empty(directory); } + bool remove_directory(const std::string& directory) + { + return std::filesystem::remove_all(directory); + } + std::vector list_files(const std::string& directory) { std::vector files; diff --git a/src/common/utils/io.hpp b/src/common/utils/io.hpp index ab4ebaa4..38344987 100644 --- a/src/common/utils/io.hpp +++ b/src/common/utils/io.hpp @@ -16,6 +16,7 @@ namespace utils::io bool create_directory(const std::string& directory); bool directory_exists(const std::string& directory); bool directory_is_empty(const std::string& directory); + bool remove_directory(const std::string& directory); std::vector list_files(const std::string& directory); void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target); }