diff --git a/data/ui_scripts/luafixes/__init__.lua b/data/ui_scripts/luafixes/__init__.lua new file mode 100644 index 00000000..e04639b6 --- /dev/null +++ b/data/ui_scripts/luafixes/__init__.lua @@ -0,0 +1,4 @@ + +-- Fix LUI_NULL_FUNCTION messages +function Engine.PIXBeginEvent() end +function Engine.PIXEndEvent() end \ No newline at end of file diff --git a/data/ui_scripts/stats/__init__.lua b/data/ui_scripts/stats/__init__.lua new file mode 100644 index 00000000..3efddd04 --- /dev/null +++ b/data/ui_scripts/stats/__init__.lua @@ -0,0 +1,290 @@ +DataSources.MPStatsSettings = DataSourceHelpers.ListSetup( "MPStatsSettings", function ( controller ) + local optionsTable = {} + + table.insert( optionsTable, CoD.OptionsUtility.CreateDvarSettings( controller, "Unlock all loot", "Whether loot should be locked based on the player's stats or always unlocked.", "MPStatsSettings_unlock_loot", "cg_unlockall_loot", { + { + option = "MENU_DISABLED", + value = 0, + default = true + }, + { + option = "MENU_ENABLED", + value = 1 + }, + }, nil, function(f1_arg0, f1_arg1, f1_arg2, dvarName, f1_arg4) + local oldValue = Engine.DvarInt( nil, dvarName ) + local newValue = f1_arg1.value + UpdateInfoModels( f1_arg1 ) + if oldValue == newValue then + return + end + Engine.SetDvar( dvarName, f1_arg1.value ) + Engine.SetDvar( "ui_enableAllHeroes", f1_arg1.value ) + end) ) + + return optionsTable +end) + +if Dvar.cg_unlockall_loot:get() == true then + Engine.SetDvar( "ui_enableAllHeroes", 1 ) +end + +LUI.createMenu.MPStatsMenu = function ( controller ) + local self = CoD.Menu.NewForUIEditor( "MPStatsMenu" ) + if PreLoadFunc then + PreLoadFunc( self, controller ) + end + self.soundSet = "ChooseDecal" + self:setOwner( controller ) + self:setLeftRight( true, true, 0, 0 ) + self:setTopBottom( true, true, 0, 0 ) + self:playSound( "menu_open", controller ) + self.buttonModel = Engine.CreateModel( Engine.GetModelForController( controller ), "MPStatsMenu.buttonPrompts" ) + self.anyChildUsesUpdateState = true + + local GameSettingsBackground = CoD.GameSettings_Background.new( self, controller ) + GameSettingsBackground:setLeftRight( true, true, 0, 0 ) + GameSettingsBackground:setTopBottom( true, true, 0, 0 ) + GameSettingsBackground.MenuFrame.titleLabel:setText( Engine.Localize( "STATS SETTINGS" ) ) + GameSettingsBackground.MenuFrame.cac3dTitleIntermediary0.FE3dTitleContainer0.MenuTitle.TextBox1.Label0:setText( Engine.Localize( "STATS SETTINGS" ) ) + GameSettingsBackground.GameSettingsSelectedItemInfo.GameModeInfo:setAlpha( 0 ) + GameSettingsBackground.GameSettingsSelectedItemInfo.GameModeName:setAlpha( 0 ) + self:addElement( GameSettingsBackground ) + self.GameSettingsBackground = GameSettingsBackground + + local Options = CoD.Competitive_SettingsList.new( self, controller ) + Options:setLeftRight( true, false, 26, 741 ) + Options:setTopBottom( true, false, 135, 720 ) + Options.Title.DescTitle:setText( Engine.Localize( "Stats" ) ) + Options.ButtonList:setVerticalCount( 15 ) + Options.ButtonList:setDataSource( "MPStatsSettings" ) + self:addElement( Options ) + self.Options = Options + + self:AddButtonCallbackFunction( self, controller, Enum.LUIButton.LUI_KEY_XBB_PSCIRCLE, nil, function ( element, menu, controller, model ) + GoBack( self, controller ) + SetPerControllerTableProperty( controller, "disableGameSettingsOptions", nil ) + return true + end, function ( element, menu, controller ) + CoD.Menu.SetButtonLabel( menu, Enum.LUIButton.LUI_KEY_XBB_PSCIRCLE, "MENU_BACK" ) + return true + end, false ) + + GameSettingsBackground.MenuFrame:setModel( self.buttonModel, controller ) + Options.id = "Options" + + self:processEvent( { + name = "menu_loaded", + controller = controller + } ) + self:processEvent( { + name = "update_state", + menu = self + } ) + if not self:restoreState() then + self.Options:processEvent( { + name = "gain_focus", + controller = controller + } ) + end + + LUI.OverrideFunction_CallOriginalSecond( self, "close", function ( element ) + element.GameSettingsBackground:close() + element.Options:close() + Engine.UnsubscribeAndFreeModel( Engine.GetModel( Engine.GetModelForController( controller ), "MPStatsMenu.buttonPrompts" ) ) + end ) + + if PostLoadFunc then + PostLoadFunc( self, controller ) + end + + return self +end + +CoD.LobbyButtons.MP_STATS = { + stringRef = "STATS", + action = function ( self, element, controller, param, menu ) + SetPerControllerTableProperty( controller, "disableGameSettingsOptions", true ) + OpenPopup( menu, "MPStatsMenu", controller ) + end, + customId = "btnMPStats" +} + + +local IsGamescomDemo = function () + return Dvar.ui_execdemo_gamescom:get() +end + +local IsBetaDemo = function () + return Dvar.ui_execdemo_beta:get() +end + +local SetButtonState = function ( button, state ) + if state == nil then + return + elseif state == CoD.LobbyButtons.DISABLED then + button.disabled = true + elseif state == CoD.LobbyButtons.HIDDEN then + button.hidden = true + end +end + +local AddButton = function ( controller, options, button, isLargeButton ) + button.disabled = false + button.hidden = false + button.selected = false + button.warning = false + if button.defaultState ~= nil then + if button.defaultState == CoD.LobbyButtons.DISABLED then + button.disabled = true + elseif button.defaultState == CoD.LobbyButtons.HIDDEN then + button.hidden = true + end + end + if button.disabledFunc ~= nil then + button.disabled = button.disabledFunc( controller ) + end + if button.visibleFunc ~= nil then + button.hidden = not button.visibleFunc( controller ) + end + if IsBetaDemo() then + SetButtonState( button, button.demo_beta ) + elseif IsGamescomDemo() then + SetButtonState( button, button.demo_gamescom ) + end + if button.hidden then + return + end + local lobbyNav = LobbyData.GetLobbyNav() + if button.selectedFunc ~= nil then + button.selected = button.selectedFunc( button.selectedParam ) + elseif CoD.LobbyMenus.History[lobbyNav] ~= nil then + button.selected = CoD.LobbyMenus.History[lobbyNav] == button.customId + end + if button.newBreadcrumbFunc then + local f8_local1 = button.newBreadcrumbFunc + if type( f8_local1 ) == "string" then + f8_local1 = LUI.getTableFromPath( f8_local1 ) + end + if f8_local1 then + button.isBreadcrumbNew = f8_local1( controller ) + end + end + if button.warningFunc ~= nil then + button.warning = button.warningFunc( controller ) + end + if button.starterPack == CoD.LobbyButtons.STARTERPACK_UPGRADE then + button.starterPackUpgrade = true + if IsStarterPack() then + button.disabled = false + end + end + table.insert( options, { + optionDisplay = button.stringRef, + action = button.action, + param = button.param, + customId = button.customId, + isLargeButton = isLargeButton, + isLastButtonInGroup = false, + disabled = button.disabled, + selected = button.selected, + isBreadcrumbNew = button.isBreadcrumbNew, + warning = button.warning, + requiredChunk = button.selectedParam, + starterPackUpgrade = button.starterPackUpgrade, + unloadMod = button.unloadMod + } ) +end + +local AddLargeButton = function ( controller, options, button ) + AddButton( controller, options, button, true ) +end + +local AddSmallButton = function ( controller, options, button ) + AddButton( controller, options, button, false ) +end + +local AddSpacer = function ( options ) + if 0 < #options then + options[#options].isLastButtonInGroup = true + end +end + +CoD.LobbyMenus.MPButtonsOnline = function ( f26_arg0, f26_arg1, f26_arg2 ) + if f26_arg2 == 1 then + AddLargeButton( f26_arg0, f26_arg1, CoD.LobbyButtons.MP_FIND_MATCH ) + AddSpacer( f26_arg1 ) + end + AddLargeButton( f26_arg0, f26_arg1, CoD.LobbyButtons.MP_CAC_NO_WARNING ) + AddLargeButton( f26_arg0, f26_arg1, CoD.LobbyButtons.MP_SPECIALISTS_NO_WARNING ) + AddLargeButton( f26_arg0, f26_arg1, CoD.LobbyButtons.MP_SCORESTREAKS ) + if (Dvar.ui_execdemo_beta:get() or IsStarterPack()) and IsStoreAvailable() then + if CoD.isPC then + AddLargeButton( f26_arg0, f26_arg1, CoD.LobbyButtons.STEAM_STORE ) + else + AddLargeButton( f26_arg0, f26_arg1, CoD.LobbyButtons.STORE ) + end + end + if Engine.DvarBool( nil, "inventory_test_button_visible" ) then + AddLargeButton( f26_arg0, f26_arg1, CoD.LobbyButtons.MP_INVENTORY_TEST ) + end + AddSpacer( f26_arg1 ) + if not DisableBlackMarket() then + AddSmallButton( f26_arg0, f26_arg1, CoD.LobbyButtons.BLACK_MARKET ) + end + AddSpacer( f26_arg1 ) + AddSmallButton( f26_arg0, f26_arg1, CoD.LobbyButtons.MP_STATS ) +end + +local targetButtons = { + [LobbyData.UITargets.UI_MAIN.id] = CoD.LobbyMenus.ModeSelect, + [LobbyData.UITargets.UI_MODESELECT.id] = CoD.LobbyMenus.ModeSelect, + [LobbyData.UITargets.UI_CPLOBBYLANGAME.id] = CoD.LobbyMenus.CPButtonsLAN, + [LobbyData.UITargets.UI_CPLOBBYLANCUSTOMGAME.id] = CoD.LobbyMenus.CPButtonsLANCUSTOM, + [LobbyData.UITargets.UI_CPLOBBYONLINE.id] = CoD.LobbyMenus.CPButtonsOnline, + [LobbyData.UITargets.UI_CPLOBBYONLINEPUBLICGAME.id] = CoD.LobbyMenus.CPButtonsPublicGame, + [LobbyData.UITargets.UI_CPLOBBYONLINECUSTOMGAME.id] = CoD.LobbyMenus.CPButtonsCustomGame, + [LobbyData.UITargets.UI_CP2LOBBYLANGAME.id] = CoD.LobbyMenus.CPZMButtonsLAN, + [LobbyData.UITargets.UI_CP2LOBBYLANCUSTOMGAME.id] = CoD.LobbyMenus.CPButtonsLANCUSTOM, + [LobbyData.UITargets.UI_CP2LOBBYONLINE.id] = CoD.LobbyMenus.CPZMButtonsOnline, + [LobbyData.UITargets.UI_CP2LOBBYONLINEPUBLICGAME.id] = CoD.LobbyMenus.CPZMButtonsPublicGame, + [LobbyData.UITargets.UI_CP2LOBBYONLINECUSTOMGAME.id] = CoD.LobbyMenus.CPButtonsCustomGame, + [LobbyData.UITargets.UI_DOALOBBYLANGAME.id] = CoD.LobbyMenus.DOAButtonsLAN, + [LobbyData.UITargets.UI_DOALOBBYONLINE.id] = CoD.LobbyMenus.DOAButtonsOnline, + [LobbyData.UITargets.UI_DOALOBBYONLINEPUBLICGAME.id] = CoD.LobbyMenus.DOAButtonsPublicGame, + [LobbyData.UITargets.UI_MPLOBBYLANGAME.id] = CoD.LobbyMenus.MPButtonsLAN, + [LobbyData.UITargets.UI_MPLOBBYMAIN.id] = CoD.LobbyMenus.MPButtonsMain, + [LobbyData.UITargets.UI_MPLOBBYONLINE.id] = CoD.LobbyMenus.MPButtonsOnline, + [LobbyData.UITargets.UI_MPLOBBYONLINEPUBLICGAME.id] = CoD.LobbyMenus.MPButtonsOnlinePublic, + [LobbyData.UITargets.UI_MPLOBBYONLINEMODGAME.id] = CoD.LobbyMenus.MPButtonsModGame, + [LobbyData.UITargets.UI_MPLOBBYONLINECUSTOMGAME.id] = CoD.LobbyMenus.MPButtonsCustomGame, + [LobbyData.UITargets.UI_MPLOBBYONLINEARENA.id] = CoD.LobbyMenus.MPButtonsArena, + [LobbyData.UITargets.UI_MPLOBBYONLINEARENAGAME.id] = CoD.LobbyMenus.MPButtonsArenaGame, + [LobbyData.UITargets.UI_FRLOBBYONLINEGAME.id] = CoD.LobbyMenus.FRButtonsOnlineGame, + [LobbyData.UITargets.UI_FRLOBBYLANGAME.id] = CoD.LobbyMenus.FRButtonsLANGame, + [LobbyData.UITargets.UI_ZMLOBBYLANGAME.id] = CoD.LobbyMenus.ZMButtonsLAN, + [LobbyData.UITargets.UI_ZMLOBBYONLINE.id] = CoD.LobbyMenus.ZMButtonsOnline, + [LobbyData.UITargets.UI_ZMLOBBYONLINEPUBLICGAME.id] = CoD.LobbyMenus.ZMButtonsPublicGame, + [LobbyData.UITargets.UI_ZMLOBBYONLINECUSTOMGAME.id] = CoD.LobbyMenus.ZMButtonsCustomGame, + [LobbyData.UITargets.UI_MPLOBBYONLINETHEATER.id] = CoD.LobbyMenus.ButtonsTheaterGame, + [LobbyData.UITargets.UI_ZMLOBBYONLINETHEATER.id] = CoD.LobbyMenus.ButtonsTheaterGame +} +CoD.LobbyMenus.AddButtonsForTarget = function ( controller, id ) + local buttonFunc = targetButtons[id] + local model = nil + if Engine.IsLobbyActive( Enum.LobbyType.LOBBY_TYPE_GAME ) then + model = Engine.GetModel( DataSources.LobbyRoot.getModel( controller ), "gameClient.isHost" ) + else + model = Engine.GetModel( DataSources.LobbyRoot.getModel( controller ), "privateClient.isHost" ) + end + local isLeader = nil + if model ~= nil then + isLeader = Engine.GetModelValue( model ) + else + isLeader = 1 + end + local result = {} + buttonFunc( controller, result, isLeader ) + return result +end + diff --git a/src/client/component/dvars.cpp b/src/client/component/dvars.cpp new file mode 100644 index 00000000..40703391 --- /dev/null +++ b/src/client/component/dvars.cpp @@ -0,0 +1,91 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include +#include +#include + +#include "scheduler.hpp" + +namespace dvars +{ + namespace + { + bool initial_config_read = false; + utils::hook::detour dvar_register_new_hook; + utils::hook::detour dvar_set_variant_hook; + + utils::hook::detour set_config_dvar_hook; + utils::hook::detour for_each_name_match_hook; + utils::hook::detour get_debug_name_hook; + + + const std::string get_config_file_path() + { + return "players/user/config.cfg"; + } + + void write_archive_dvars() + { + std::string config_buffer; + + for (int i = 0; i < *game::g_dvarCount; ++i) + { + const auto* dvar = reinterpret_cast(&game::s_dvarPool[160 * i]); + + if (!dvar->debugName) + continue; + + auto name = dvar->debugName; + auto value = game::Dvar_DisplayableValue(dvar); + + config_buffer.append(utils::string::va("set %s %s\n", name, value)); + } + + if (config_buffer.length() == 0) + return; + + utils::io::write_file(get_config_file_path(), config_buffer); + } + + void dvar_set_variant_stub(game::dvar_t* dvar, game::DvarValue* value, unsigned int source) + { + dvar_set_variant_hook.invoke(dvar, value, source); + + if (initial_config_read && dvar->debugName) + { + write_archive_dvars(); + } + } + + void read_archive_dvars() + { + const std::string path = get_config_file_path(); + + if (!utils::io::file_exists(path)) + return; + + std::string filedata; + utils::io::read_file(path, &filedata); + + game::Cbuf_ExecuteBuffer(0, game::ControllerIndex_t::CONTROLLER_INDEX_0, filedata.c_str()); + initial_config_read = true; + scheduler::execute(scheduler::pipeline::dvars_loaded); + } + } + + class component final : public client_component + { + public: + void post_unpack() override + { + scheduler::once(read_archive_dvars, scheduler::pipeline::main); + + dvar_set_variant_hook.create(0x1422C9A90_g, dvar_set_variant_stub); + } + }; +} + +REGISTER_COMPONENT(dvars::component) diff --git a/src/client/component/loot.cpp b/src/client/component/loot.cpp index ee1fcbb3..928abbe6 100644 --- a/src/client/component/loot.cpp +++ b/src/client/component/loot.cpp @@ -10,13 +10,20 @@ namespace loot { namespace { + game::dvar_t* dvar_cg_unlockall_loot; + utils::hook::detour loot_getitemquantity_hook; utils::hook::detour liveinventory_getitemquantity_hook; utils::hook::detour liveinventory_areextraslotspurchased_hook; - int loot_getitemquantity_stub(const game::ControllerIndex_t /*controller_index*/, const game::eModes mode, - const int /*item_id*/) + int loot_getitemquantity_stub(const game::ControllerIndex_t controller_index, const game::eModes mode, + const int item_id) { + if (!dvar_cg_unlockall_loot->current.enabled) + { + return loot_getitemquantity_hook.invoke(controller_index, mode, item_id); + } + if (mode == game::eModes::MODE_ZOMBIES) { return 999; @@ -28,8 +35,8 @@ namespace loot int liveinventory_getitemquantity_stub(const game::ControllerIndex_t controller_index, const int item_id) { // Item id's for extra CaC slots, CWL camo's and paid specialist outfits - if (item_id == 99003 || item_id >= 99018 && item_id <= 99021 || item_id == 99025 || item_id >= 90047 && - item_id <= 90064) + if (dvar_cg_unlockall_loot->current.enabled && (item_id == 99003 || item_id >= 99018 && item_id <= 99021 || item_id == 99025|| + item_id >= 90047 && item_id <= 90064)) { return 1; } @@ -37,15 +44,14 @@ namespace loot return liveinventory_getitemquantity_hook.invoke(controller_index, item_id); } - bool liveinventory_areextraslotspurchased_stub(const game::ControllerIndex_t /*controller_index*/) + bool liveinventory_areextraslotspurchased_stub(const game::ControllerIndex_t controller_index) { - return true; - } + if (dvar_cg_unlockall_loot->current.enabled) + { + return true; + } - void set_dvars_on_startup() - { - game::Dvar_SetFromStringByName("ui_enableAllHeroes", "1", true); - game::Dvar_SetFromStringByName("ui_allLootUnlocked", "1", true); + return liveinventory_areextraslotspurchased_hook.invoke(controller_index); } }; @@ -53,10 +59,19 @@ namespace loot { void post_unpack() override { - scheduler::once(set_dvars_on_startup, scheduler::pipeline::main); + dvar_cg_unlockall_loot = game::Dvar_RegisterBool(game::Dvar_GenerateHash("cg_unlockall_loot"), "cg_unlockall_loot", false, (game::dvarFlags_e)0x0, "Unlocks blackmarket loot"); + dvar_cg_unlockall_loot->debugName = "cg_unlockall_loot"; + loot_getitemquantity_hook.create(0x141E82C90_g, loot_getitemquantity_stub); liveinventory_getitemquantity_hook.create(0x141E090C0_g, liveinventory_getitemquantity_stub); liveinventory_areextraslotspurchased_hook.create(0x141E089E0_g, liveinventory_areextraslotspurchased_stub); + + scheduler::once([]() { + if (dvar_cg_unlockall_loot->current.enabled) + { + game::Dvar_SetFromStringByName("ui_enableAllHeroes", "1", true); + } + }, scheduler::pipeline::dvars_loaded); } }; }; diff --git a/src/client/component/scheduler.cpp b/src/client/component/scheduler.cpp index ce8995dd..a964200c 100644 --- a/src/client/component/scheduler.cpp +++ b/src/client/component/scheduler.cpp @@ -91,11 +91,6 @@ namespace scheduler utils::hook::detour g_run_frame_hook; utils::hook::detour main_frame_hook; - void execute(const pipeline type) - { - assert(type >= 0 && type < pipeline::count); - pipelines[type].execute(); - } void r_end_frame_stub() { @@ -114,6 +109,12 @@ namespace scheduler main_frame_hook.invoke(); execute(pipeline::main); } + } + + void execute(const pipeline type) + { + assert(type >= 0 && type < pipeline::count); + pipelines[type].execute(); } void schedule(const std::function& callback, const pipeline type, diff --git a/src/client/component/scheduler.hpp b/src/client/component/scheduler.hpp index 1e0de7a8..ac5c3f06 100644 --- a/src/client/component/scheduler.hpp +++ b/src/client/component/scheduler.hpp @@ -14,13 +14,18 @@ namespace scheduler server, // The game's main thread - main, + main, + + // Dvars are done loading from the config file + dvars_loaded, count, }; static const bool cond_continue = false; - static const bool cond_end = true; + static const bool cond_end = true; + + void execute(const pipeline type); void schedule(const std::function& callback, pipeline type = pipeline::async, std::chrono::milliseconds delay = 0ms); diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp new file mode 100644 index 00000000..e23ddd28 --- /dev/null +++ b/src/client/component/ui_scripting.cpp @@ -0,0 +1,483 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include "game/ui_scripting/execution.hpp" + +#include "command.hpp" +#include "ui_scripting.hpp" +#include "scheduler.hpp" + +#include +#include +#include + +namespace ui_scripting +{ + namespace + { + std::unordered_map> converted_functions; + + utils::hook::detour ui_cod_init_hook; + utils::hook::detour ui_cod_lobbyui_init_hook; + utils::hook::detour ui_shutdown_hook; + utils::hook::detour hks_package_require_hook; + utils::hook::detour lua_cod_getrawfile_hook; + + game::dvar_t* dvar_cg_enable_unsafe_lua_functions; + static bool unsafe_function_called_message_shown = false; + + struct globals_t + { + std::string in_require_script; + std::unordered_map loaded_scripts; + std::unordered_map local_scripts; + bool load_raw_script{}; + std::string raw_script_name{}; + }; + + globals_t globals; + + bool is_loaded_script(const std::string& name) + { + return globals.loaded_scripts.contains(name); + } + + bool is_local_script(const std::string& name) + { + return globals.local_scripts.contains(name); + } + + std::string get_root_script(const std::string& name) + { + const auto itr = globals.loaded_scripts.find(name); + return (itr == globals.loaded_scripts.end()) ? std::string() : itr->second; + } + + table get_globals() + { + const auto state = *game::hks::lua_state; + return state->globals.v.table; + } + + void print_error(const std::string& error) + { + printf("************** LUI script execution error **************\n"); + printf("%s\n", error.data()); + printf("********************************************************\n"); + } + + void print_loading_script(const std::string& name) + { + printf("Loading LUI script '%s'\n", name.data()); + } + + std::string get_current_script(game::hks::lua_State* state) + { + game::hks::lua_Debug info{}; + game::hks::hksi_lua_getstack(state, 1, &info); + game::hks::hksi_lua_getinfo(state, "nSl", &info); + return info.short_src; + } + + int load_buffer(const std::string& name, const std::string& data) + { + const auto state = *game::hks::lua_state; + const auto sharing_mode = state->m_global->m_bytecodeSharingMode; + state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_ON; + + const auto _0 = utils::finally([&] + { + state->m_global->m_bytecodeSharingMode = sharing_mode; + }); + + game::hks::HksCompilerSettings compiler_settings{}; + return game::hks::hksi_hksL_loadbuffer(state, &compiler_settings, data.data(), data.size(), name.data()); + } + + void load_script(const std::string& name, const std::string& data) + { + globals.loaded_scripts[name] = name; + + const auto state = *game::hks::lua_state; + const auto lua = get_globals(); + state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_ON; + const auto load_results = lua["loadstring"](data, name); + state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_SECURE; + + if (load_results[0].is()) + { + const auto results = lua["pcall"](load_results); + if (!results[0].as()) + { + print_error(results[1].as()); + } + } + else if (load_results[1].is()) + { + print_error(load_results[1].as()); + } + } + + void load_local_script_files(const std::string& script_dir) + { + if (!utils::io::directory_exists(script_dir)) + { + return; + } + + const auto scripts = utils::io::list_files(script_dir); + + for (const auto& script : scripts) + { + if (std::filesystem::is_regular_file(script)) + { + const std::string file_path = script.substr(script.find("ui_scripts") + 11); + globals.local_scripts[file_path.c_str()] = script; + } + else if (std::filesystem::is_directory(script)) + { + load_local_script_files(script); + } + } + } + + void load_scripts(const std::string& script_dir) + { + if (!utils::io::directory_exists(script_dir)) + { + return; + } + + load_local_script_files(script_dir); + + const auto scripts = utils::io::list_files(script_dir); + + for (const auto& script : scripts) + { + std::string data; + + if (std::filesystem::is_directory(script) && utils::io::read_file(script + "/__init__.lua", &data)) + { + print_loading_script(script); + load_script(script + "/__init__.lua", data); + } + } + } + + void setup_functions() + { + const auto lua = get_globals(); + + using game = table; + auto game_type = game(); + lua["game"] = game_type; + } + + void enable_globals() + { + const auto lua = get_globals(); + const std::string code = + "local g = getmetatable(_G)\n" + "if not g then\n" + "g = {}\n" + "setmetatable(_G, g)\n" + "end\n" + "g.__newindex = nil\n"; + + const auto state = *game::hks::lua_state; + state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_ON; + lua["loadstring"](code)[0](); + state->m_global->m_bytecodeSharingMode = game::hks::HKS_BYTECODE_SHARING_SECURE; + } + + void start() + { + globals = {}; + + const auto lua = get_globals(); + enable_globals(); + + setup_functions(); + + lua["print"] = function(reinterpret_cast(0x141D30290_g)); // hks::base_print + lua["table"]["unpack"] = lua["unpack"]; + lua["luiglobals"] = lua; + + utils::nt::library host{}; + load_scripts(host.get_folder().append("/data/ui_scripts/").string()); + load_scripts("boiii/ui_scripts/"); + load_scripts("data/ui_scripts/"); + } + + void try_start() + { + try + { + start(); + } + catch (const std::exception& ex) + { + printf("Failed to load LUI scripts: %s\n", ex.what()); + } + } + + void ui_cod_init_stub(bool frontend) + { + ui_cod_init_hook.invoke(frontend); + + if (game::Com_IsRunningUILevel()) + { + // Fetch the names of the local files so file overrides are already handled + globals = {}; + utils::nt::library host{}; + load_local_script_files(host.get_folder().append("/data/ui_scripts/").string()); + load_local_script_files("boiii/ui_scripts/"); + load_local_script_files("data/ui_scripts/"); + return; + } + const auto _0 = utils::finally(&try_start); + } + + void ui_cod_lobbyui_init_stub() + { + ui_cod_lobbyui_init_hook.invoke(); + const auto _0 = utils::finally(&try_start); + } + + void ui_shutdown_stub() + { + converted_functions.clear(); + globals = {}; + return ui_shutdown_hook.invoke(); + } + + void* hks_package_require_stub(game::hks::lua_State* state) + { + const auto script = get_current_script(state); + const auto root = get_root_script(script); + globals.in_require_script = root; + return hks_package_require_hook.invoke(state); + } + + int hks_load_stub(game::hks::lua_State* state, void* compiler_options, void* reader, void* reader_data, void* debug_reader, void* debug_reader_data, const char* chunk_name) + { + if (globals.load_raw_script) + { + globals.load_raw_script = false; + globals.loaded_scripts[globals.raw_script_name] = globals.in_require_script; + return load_buffer(globals.raw_script_name, utils::io::read_file(globals.raw_script_name)); + } + + return utils::hook::invoke(0x141D3AFB0_g, state, compiler_options, reader, reader_data, debug_reader, debug_reader_data, chunk_name); + } + + game::XAssetHeader lua_cod_getrawfile_stub(char* filename) + { + game::XAssetHeader header{ .luaFile = nullptr }; + + if (!is_loaded_script(globals.in_require_script) && !is_local_script(filename)) + { + return lua_cod_getrawfile_hook.invoke(filename); + } + + const std::string name_ = filename; + std::string target_script; + if (is_loaded_script(globals.in_require_script)) + { + const auto folder = globals.in_require_script.substr(0, globals.in_require_script.find_last_of("/\\")); + target_script = folder + "/" + name_ + ".lua"; + } + else + { + target_script = globals.local_scripts[name_]; + } + + if (utils::io::file_exists(target_script)) + { + globals.load_raw_script = true; + globals.raw_script_name = target_script; + header.luaFile = reinterpret_cast(1); + + return header; + } + + return lua_cod_getrawfile_hook.invoke(filename); + } + + int luaopen_stub([[maybe_unused]] game::hks::lua_State* l) + { + return 0; + } + + int lua_unsafe_function_stub([[maybe_unused]] game::hks::lua_State* l) + { + if (!unsafe_function_called_message_shown) + { + auto state = get_globals(); + // TODO: Is it possible to do this with a confirm dialog? Doing this in LUI seems unsafe to me because mods will be able to change this aswell + state["LuaUtils"]["ShowMessageDialog"](0, 0, "The map/mod you are playing tried to run code that can be unsafe. This can include writing or reading files on your system, accessing environment variables, running system commands or loading a dll. These are usually used for storing data across games, integrating third party software like Discord or fetching data from a server to make the gameplay for dynamic.\nThis can also cause a lot of harm by the wrong people.\n\nIf you trust this map/mod and want to enable these features, open the following file in your Black Ops 3 installation: players/user/config.cfg.\nIn this file change 'set cg_enable_unsafe_lua_functions 0' to 'set cg_enable_unsafe_lua_functions 1' and restart Black Ops 3.", "Unsafe lua function called"); + unsafe_function_called_message_shown = true; + } + + return 0; + } + }; + + int main_handler(game::hks::lua_State* state) + { + const auto value = state->m_apistack.base[-1]; + if (value.t != game::hks::TCFUNCTION) + { + return 0; + } + + const auto closure = value.v.cClosure; + if (!converted_functions.contains(closure)) + { + return 0; + } + + const auto& function = converted_functions[closure]; + + try + { + const auto args = get_return_values(); + const auto results = function(args); + + for (const auto& result : results) + { + push_value(result); + } + + return static_cast(results.size()); + } + catch (const std::exception& ex) + { + game::hks::hksi_luaL_error(state, ex.what()); + } + + return 0; + } + + template + game::hks::cclosure* convert_function(F f) + { + const auto state = *game::hks::lua_state; + const auto closure = game::hks::cclosure_Create(state, main_handler, 0, 0, 0); + converted_functions[closure] = wrap_function(f); + return closure; + } + + class component final : public client_component + { + public: + void post_unpack() override + { + utils::hook::call(0x141D4979A_g, hks_load_stub); + + hks_package_require_hook.create(0x141D28EF0_g, hks_package_require_stub); + ui_cod_init_hook.create(0x141F298B0_g, ui_cod_init_stub); + ui_cod_lobbyui_init_hook.create(0x141F2C620_g, ui_cod_lobbyui_init_stub); + ui_shutdown_hook.create(0x14270E9C0_g, ui_shutdown_stub); + lua_cod_getrawfile_hook.create(0x141F0F880_g, lua_cod_getrawfile_stub); + + dvar_cg_enable_unsafe_lua_functions = game::Dvar_RegisterBool(game::Dvar_GenerateHash("cg_enable_unsafe_lua_functions"), "cg_enable_unsafe_lua_functions", false, (game::dvarFlags_e)0x1000, "Enables the use of unsafe lua functions"); + dvar_cg_enable_unsafe_lua_functions->debugName = "cg_enable_unsafe_lua_functions"; + + scheduler::once([]() { + game::dvar_t* dvar_callstack_ship = game::Dvar_FindVar("ui_error_callstack_ship"); + dvar_callstack_ship->flags = (game::dvarFlags_e)0; + game::dvar_t* dvar_report_delay= game::Dvar_FindVar("ui_error_report_delay"); + dvar_report_delay->flags = (game::dvarFlags_e)0; + + game::Dvar_SetFromStringByName("ui_error_callstack_ship", "1", true); + game::Dvar_SetFromStringByName("ui_error_report_delay", "0", true); + }, scheduler::pipeline::renderer); + + command::add("luiReload", [](auto& params) + { + auto frontend = game::Com_IsRunningUILevel(); + + if (frontend) + { + converted_functions.clear(); + + globals.loaded_scripts.clear(); + globals.local_scripts.clear(); + + game::UI_CoD_Shutdown(); + game::UI_CoD_Init(true); + + // Com_LoadFrontEnd stripped + game::Lua_CoD_LoadLuaFile(*game::hks::lua_state, "ui_mp.T6.main"); + game::UI_AddMenu(game::UI_CoD_GetRootNameForController(0), "main", -1, *game::hks::lua_state); + + game::UI_CoD_LobbyUI_Init(); + } + else + { + // TODO: Find a way to do a full shutdown & restart like in frontend, that opens up the loading screen that can't be easily closed + game::CG_LUIHUDRestart(0); + } + }); + + scheduler::once([]() { + if (!dvar_cg_enable_unsafe_lua_functions->current.enabled) + { + // Do not allow the HKS vm to open LUA's libraries + // Disable unsafe functions + utils::hook::jump(0x141D34190_g, luaopen_stub); // debug + + utils::hook::jump(0x141D300B0_g, lua_unsafe_function_stub); // base_loadfile + utils::hook::jump(0x141D31EE0_g, lua_unsafe_function_stub); // base_load + utils::hook::jump(0x141D2CF00_g, lua_unsafe_function_stub); // string_dump + utils::hook::jump(0x141FD3220_g, lua_unsafe_function_stub); // engine_openurl + + utils::hook::jump(0x141D2AFF0_g, lua_unsafe_function_stub); // os_getenv + utils::hook::jump(0x141D2B790_g, lua_unsafe_function_stub); // os_exit + utils::hook::jump(0x141D2B7C0_g, lua_unsafe_function_stub); // os_remove + utils::hook::jump(0x141D2BB70_g, lua_unsafe_function_stub); // os_rename + utils::hook::jump(0x141D2B360_g, lua_unsafe_function_stub); // os_tmpname + utils::hook::jump(0x141D2B0F0_g, lua_unsafe_function_stub); // os_sleep + utils::hook::jump(0x141D2AF90_g, lua_unsafe_function_stub); // os_execute + utils::hook::jump(0x141D2AFF0_g, lua_unsafe_function_stub); // os_getenv + + // io helpers + utils::hook::jump(0x141D32390_g, lua_unsafe_function_stub); // io_tostring + utils::hook::jump(0x141D2FDC0_g, lua_unsafe_function_stub); // io_close_file + utils::hook::jump(0x141D2FD50_g, lua_unsafe_function_stub); // io_flush + utils::hook::jump(0x141D31260_g, lua_unsafe_function_stub); // io_lines + utils::hook::jump(0x141D305C0_g, lua_unsafe_function_stub); // io_read_file + utils::hook::jump(0x141D305C0_g, lua_unsafe_function_stub); // io_read_file + utils::hook::jump(0x141D320A0_g, lua_unsafe_function_stub); // io_seek_file + utils::hook::jump(0x141D321E0_g, lua_unsafe_function_stub); // io_setvbuf + utils::hook::jump(0x141D2FCD0_g, lua_unsafe_function_stub); // io_write + // io functions + utils::hook::jump(0x141D2FD10_g, lua_unsafe_function_stub); // io_write + utils::hook::jump(0x141D30F40_g, lua_unsafe_function_stub); // io_read + utils::hook::jump(0x141D2FF00_g, lua_unsafe_function_stub); // io_close + utils::hook::jump(0x141D2FD90_g, lua_unsafe_function_stub); // io_flush + utils::hook::jump(0x141D313A0_g, lua_unsafe_function_stub); // io_lines + utils::hook::jump(0x141D31BA0_g, lua_unsafe_function_stub); // io_input + utils::hook::jump(0x141D31BC0_g, lua_unsafe_function_stub); // io_output + utils::hook::jump(0x141D31BE0_g, lua_unsafe_function_stub); // io_type + utils::hook::jump(0x141D31DD0_g, lua_unsafe_function_stub); // io_open + utils::hook::jump(0x141D31D70_g, lua_unsafe_function_stub); // io_tmpfile + utils::hook::jump(0x141D33C00_g, lua_unsafe_function_stub); // io_popen + + utils::hook::jump(0x141D2D0C0_g, lua_unsafe_function_stub); // serialize_persist + utils::hook::jump(0x141D2D480_g, lua_unsafe_function_stub); // serialize_unpersist + + utils::hook::jump(0x141D2F560_g, lua_unsafe_function_stub); // havokscript_compiler_settings + utils::hook::jump(0x141D2F660_g, lua_unsafe_function_stub); // havokscript_setgcweights + utils::hook::jump(0x141D2FB10_g, lua_unsafe_function_stub); // havokscript_getgcweights + + utils::hook::jump(0x141D299C0_g, lua_unsafe_function_stub); // package_loadlib + } + }, scheduler::pipeline::dvars_loaded); + } + }; +} + +REGISTER_COMPONENT(ui_scripting::component) diff --git a/src/client/component/ui_scripting.hpp b/src/client/component/ui_scripting.hpp new file mode 100644 index 00000000..68762c38 --- /dev/null +++ b/src/client/component/ui_scripting.hpp @@ -0,0 +1,47 @@ +#pragma once + +namespace ui_scripting +{ + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f](const function_arguments& args) + { + f(args[I]...); + return arguments{ {} }; + }; + } + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f](const function_arguments& args) + { + return f(args[I]...); + }; + } + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f](const function_arguments& args) + { + return arguments{ f(args[I]...) }; + }; + } + + template + auto wrap_function(const std::function& f) + { + return wrap_function(f, std::index_sequence_for{}); + } + + template + auto wrap_function(F f) + { + return wrap_function(std::function(f)); + } + + template + game::hks::cclosure* convert_function(F f); +} diff --git a/src/client/game/game.cpp b/src/client/game/game.cpp index 76aba355..18ca63bf 100644 --- a/src/client/game/game.cpp +++ b/src/client/game/game.cpp @@ -1,7 +1,6 @@ #include #include "game.hpp" -#include namespace game { diff --git a/src/client/game/game.hpp b/src/client/game/game.hpp index 34b480c1..02e6fa59 100644 --- a/src/client/game/game.hpp +++ b/src/client/game/game.hpp @@ -1,5 +1,8 @@ #pragma once +#include "structs.hpp" +#include + namespace game { size_t get_base(); diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 6ab7ee4a..1c249da8 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -6,11 +6,6 @@ namespace game { #endif - enum XAssetType - { - ASSET_TYPE_LEADERBOARD = 0x32, - ASSET_TYPE_DDL = 0x33, - }; enum ControllerIndex_t { @@ -421,6 +416,126 @@ namespace game ERROR_SOFTRESTART_KEEPDW = 0x800, }; + enum XAssetType + { + ASSET_TYPE_PHYSPRESET = 0x0, + ASSET_TYPE_PHYSCONSTRAINTS = 0x1, + ASSET_TYPE_DESTRUCTIBLEDEF = 0x2, + ASSET_TYPE_XANIMPARTS = 0x3, + ASSET_TYPE_XMODEL = 0x4, + ASSET_TYPE_XMODELMESH = 0x5, + ASSET_TYPE_MATERIAL = 0x6, + ASSET_TYPE_COMPUTE_SHADER_SET = 0x7, + ASSET_TYPE_TECHNIQUE_SET = 0x8, + ASSET_TYPE_IMAGE = 0x9, + ASSET_TYPE_SOUND = 0xA, + ASSET_TYPE_SOUND_PATCH = 0xB, + ASSET_TYPE_CLIPMAP = 0xC, + ASSET_TYPE_COMWORLD = 0xD, + ASSET_TYPE_GAMEWORLD = 0xE, + ASSET_TYPE_MAP_ENTS = 0xF, + ASSET_TYPE_GFXWORLD = 0x10, + ASSET_TYPE_LIGHT_DEF = 0x11, + ASSET_TYPE_LENSFLARE_DEF = 0x12, + ASSET_TYPE_UI_MAP = 0x13, + ASSET_TYPE_FONT = 0x14, + ASSET_TYPE_FONTICON = 0x15, + ASSET_TYPE_LOCALIZE_ENTRY = 0x16, + ASSET_TYPE_WEAPON = 0x17, + ASSET_TYPE_WEAPONDEF = 0x18, + ASSET_TYPE_WEAPON_VARIANT = 0x19, + ASSET_TYPE_WEAPON_FULL = 0x1A, + ASSET_TYPE_CGMEDIA = 0x1B, + ASSET_TYPE_PLAYERSOUNDS = 0x1C, + ASSET_TYPE_PLAYERFX = 0x1D, + ASSET_TYPE_SHAREDWEAPONSOUNDS = 0x1E, + ASSET_TYPE_ATTACHMENT = 0x1F, + ASSET_TYPE_ATTACHMENT_UNIQUE = 0x20, + ASSET_TYPE_WEAPON_CAMO = 0x21, + ASSET_TYPE_CUSTOMIZATION_TABLE = 0x22, + ASSET_TYPE_CUSTOMIZATION_TABLE_FE_IMAGES = 0x23, + ASSET_TYPE_CUSTOMIZATION_TABLE_COLOR = 0x24, + ASSET_TYPE_SNDDRIVER_GLOBALS = 0x25, + ASSET_TYPE_FX = 0x26, + ASSET_TYPE_TAGFX = 0x27, + ASSET_TYPE_NEW_LENSFLARE_DEF = 0x28, + ASSET_TYPE_IMPACT_FX = 0x29, + ASSET_TYPE_IMPACT_SOUND = 0x2A, + ASSET_TYPE_PLAYER_CHARACTER = 0x2B, + ASSET_TYPE_AITYPE = 0x2C, + ASSET_TYPE_CHARACTER = 0x2D, + ASSET_TYPE_XMODELALIAS = 0x2E, + ASSET_TYPE_RAWFILE = 0x2F, + ASSET_TYPE_STRINGTABLE = 0x30, + ASSET_TYPE_STRUCTURED_TABLE = 0x31, + ASSET_TYPE_LEADERBOARD = 0x32, + ASSET_TYPE_DDL = 0x33, + ASSET_TYPE_GLASSES = 0x34, + ASSET_TYPE_TEXTURELIST = 0x35, + ASSET_TYPE_SCRIPTPARSETREE = 0x36, + ASSET_TYPE_KEYVALUEPAIRS = 0x37, + ASSET_TYPE_VEHICLEDEF = 0x38, + ASSET_TYPE_ADDON_MAP_ENTS = 0x39, + ASSET_TYPE_TRACER = 0x3A, + ASSET_TYPE_SLUG = 0x3B, + ASSET_TYPE_SURFACEFX_TABLE = 0x3C, + ASSET_TYPE_SURFACESOUNDDEF = 0x3D, + ASSET_TYPE_FOOTSTEP_TABLE = 0x3E, + ASSET_TYPE_ENTITYFXIMPACTS = 0x3F, + ASSET_TYPE_ENTITYSOUNDIMPACTS = 0x40, + ASSET_TYPE_ZBARRIER = 0x41, + ASSET_TYPE_VEHICLEFXDEF = 0x42, + ASSET_TYPE_VEHICLESOUNDDEF = 0x43, + ASSET_TYPE_TYPEINFO = 0x44, + ASSET_TYPE_SCRIPTBUNDLE = 0x45, + ASSET_TYPE_SCRIPTBUNDLELIST = 0x46, + ASSET_TYPE_RUMBLE = 0x47, + ASSET_TYPE_BULLETPENETRATION = 0x48, + ASSET_TYPE_LOCDMGTABLE = 0x49, + ASSET_TYPE_AIMTABLE = 0x4A, + ASSET_TYPE_ANIMSELECTORTABLESET = 0x4B, + ASSET_TYPE_ANIMMAPPINGTABLE = 0x4C, + ASSET_TYPE_ANIMSTATEMACHINE = 0x4D, + ASSET_TYPE_BEHAVIORTREE = 0x4E, + ASSET_TYPE_BEHAVIORSTATEMACHINE = 0x4F, + ASSET_TYPE_TTF = 0x50, + ASSET_TYPE_SANIM = 0x51, + ASSET_TYPE_LIGHT_DESCRIPTION = 0x52, + ASSET_TYPE_SHELLSHOCK = 0x53, + ASSET_TYPE_XCAM = 0x54, + ASSET_TYPE_BG_CACHE = 0x55, + ASSET_TYPE_TEXTURE_COMBO = 0x56, + ASSET_TYPE_FLAMETABLE = 0x57, + ASSET_TYPE_BITFIELD = 0x58, + ASSET_TYPE_ATTACHMENT_COSMETIC_VARIANT = 0x59, + ASSET_TYPE_MAPTABLE = 0x5A, + ASSET_TYPE_MAPTABLE_LOADING_IMAGES = 0x5B, + ASSET_TYPE_MEDAL = 0x5C, + ASSET_TYPE_MEDALTABLE = 0x5D, + ASSET_TYPE_OBJECTIVE = 0x5E, + ASSET_TYPE_OBJECTIVE_LIST = 0x5F, + ASSET_TYPE_UMBRA_TOME = 0x60, + ASSET_TYPE_NAVMESH = 0x61, + ASSET_TYPE_NAVVOLUME = 0x62, + ASSET_TYPE_BINARYHTML = 0x63, + ASSET_TYPE_LASER = 0x64, + ASSET_TYPE_BEAM = 0x65, + ASSET_TYPE_STREAMER_HINT = 0x66, + ASSET_TYPE_COUNT = 0x67, + ASSET_TYPE_STRING = 0x68, + ASSET_TYPE_ASSETLIST = 0x69, + ASSET_TYPE_REPORT = 0x6A, + ASSET_TYPE_DEPEND = 0x68, + ASSET_TYPE_FULL_COUNT = 0x6C, + }; + + struct LuaFile + { + const char* name; + int len; + const char* buffer; + }; + struct XZoneBuffer { const void* data; @@ -502,6 +617,23 @@ namespace game DVAR_TYPE_COUNT = 0x10, }; + enum dvarFlags_e + { + DVAR_ARCHIVE = 1 << 0, + DVAR_USERINFO = 1 << 1, + DVAR_SYSTEMINFO = 1 << 2, + DVAR_CODINFO = 1 << 3, + DVAR_LATCH = 1 << 4, + DVAR_ROM = 1 << 5, + DVAR_SAVED = 1 << 6, + DVAR_INIT = 1 << 7, + DVAR_CHEAT = 1 << 8, + //DVAR_UNKNOWN = 1 << 9, + DVAR_EXTERNAL = 1 << 10, + //DVAR_UNKNOWN3x = 1 << 11-13, + DVAR_SESSIONMODE = 1 << 15 + }; + typedef float vec_t; union vec4_t @@ -525,7 +657,6 @@ namespace game vec4_t vector; const char* string; byte color[4]; - const dvar_t* indirect[3]; }; struct $7034703ED3857507327AE195CCA24A71 @@ -877,6 +1008,512 @@ namespace game JoinResult joinResult; }; + + namespace hks + { + struct lua_State; + struct HashTable; + struct StringTable; + struct cclosure; + typedef int hksBool; + typedef char hksChar; + typedef unsigned __int8 hksByte; + typedef __int16 hksShort16; + typedef unsigned __int16 hksUshort16; + typedef float HksNumber; + typedef int hksInt32; + typedef unsigned int hksUint32; + typedef __int64 hksInt64; + typedef unsigned __int64 hksUint64; + + typedef int HksGcCost; + + + typedef size_t hksSize; + typedef void* (*lua_Alloc)(void*, void*, size_t, size_t); + typedef hksInt32(*lua_CFunction)(lua_State*); + + struct GenericChunkHeader + { + hksSize m_flags; + }; + + struct ChunkHeader : GenericChunkHeader + { + ChunkHeader* m_next; + }; + + struct ChunkList + { + ChunkHeader m_head; + }; + + struct UserData : ChunkHeader + { + unsigned __int64 m_envAndSizeOffsetHighBits; + unsigned __int64 m_metaAndSizeOffsetLowBits; + char m_data[8]; + }; + + struct InternString + { + unsigned __int64 m_flags; + unsigned __int64 m_lengthbits; + unsigned int m_hash; + char m_data[30]; + }; + + union HksValue + { + cclosure* cClosure; + void* closure; + UserData* userData; + HashTable* table; + void* tstruct; + InternString* str; + void* thread; + void* ptr; + float number; + unsigned int native; + bool boolean; + }; + + enum HksObjectType + { + TANY = 0xFFFFFFFE, + TNONE = 0xFFFFFFFF, + TNIL = 0x0, + TBOOLEAN = 0x1, + TLIGHTUSERDATA = 0x2, + TNUMBER = 0x3, + TSTRING = 0x4, + TTABLE = 0x5, + TFUNCTION = 0x6, // idk + TUSERDATA = 0x7, + TTHREAD = 0x8, + TIFUNCTION = 0x9, // Lua function + TCFUNCTION = 0xA, // C function + TUI64 = 0xB, + TSTRUCT = 0xC, + NUM_TYPE_OBJECTS = 0xE, + }; + + struct HksObject + { + HksObjectType t; + HksValue v; + }; + + const struct hksInstruction + { + unsigned int code; + }; + + struct ActivationRecord + { + HksObject* m_base; + const hksInstruction* m_returnAddress; + __int16 m_tailCallDepth; + __int16 m_numVarargs; + int m_numExpectedReturns; + }; + + struct CallStack + { + ActivationRecord* m_records; + ActivationRecord* m_lastrecord; + ActivationRecord* m_current; + const hksInstruction* m_current_lua_pc; + const hksInstruction* m_hook_return_addr; + int m_hook_level; + }; + + struct ApiStack + { + HksObject* top; + HksObject* base; + HksObject* alloc_top; + HksObject* bottom; + }; + + struct UpValue : ChunkHeader + { + HksObject m_storage; + HksObject* loc; + UpValue* m_next; + }; + + struct CallSite + { + _SETJMP_FLOAT128 m_jumpBuffer[16]; + CallSite* m_prev; + }; + + enum Status + { + NEW = 0x1, + RUNNING = 0x2, + YIELDED = 0x3, + DEAD_ERROR = 0x4, + }; + + enum HksError + { + HKS_NO_ERROR = 0, + HKS_ERRSYNTAX = -4, + HKS_ERRFILE = -5, + HKS_ERRRUN = -100, + HKS_ERRMEM = -200, + HKS_ERRERR = -300, + HKS_THROWING_ERROR = -500, + HKS_GC_YIELD = 1, + }; + + struct lua_Debug + { + int event; + const char* name; + const char* namewhat; + const char* what; + const char* source; + int currentline; + int nups; + int nparams; + int ishksfunc; + int linedefined; + int lastlinedefined; + char short_src[512]; + int callstack_level; + int is_tail_call; + }; + + using lua_function = int(__fastcall*)(lua_State*); + + struct luaL_Reg + { + const char* name; + lua_function function; + }; + + struct Node + { + HksObject m_key; + HksObject m_value; + }; + + struct StringPinner + { + struct Node + { + InternString* m_strings[32]; + Node* m_prev; + }; + + lua_State* const m_state; + StringPinner* const m_prev; + InternString** m_nextStringsPlace; + Node m_firstNode; + Node* m_currentNode; + }; + + struct StringTable + { + InternString** m_data; + unsigned int m_count; + unsigned int m_mask; + StringPinner* m_pinnedStrings; + }; + + struct Metatable + { + }; + + struct HashTable : ChunkHeader + { + Metatable* m_meta; + unsigned int m_version; + unsigned int m_mask; + Node* m_hashPart; + HksObject* m_arrayPart; + unsigned int m_arraySize; + Node* m_freeNode; + }; + + struct cclosure : ChunkHeader + { + lua_function m_function; + HashTable* m_env; + __int16 m_numUpvalues; + __int16 m_flags; + InternString* m_name; + HksObject m_upvalues[1]; + }; + + enum HksCompilerSettings_BytecodeSharingFormat + { + BYTECODE_DEFAULT = 0x0, + BYTECODE_INPLACE = 0x1, + BYTECODE_REFERENCED = 0x2, + }; + + enum HksCompilerSettings_IntLiteralOptions + { + INT_LITERALS_NONE = 0x0, + INT_LITERALS_LUD = 0x1, + INT_LITERALS_32BIT = 0x1, + INT_LITERALS_UI64 = 0x2, + INT_LITERALS_64BIT = 0x2, + INT_LITERALS_ALL = 0x3, + }; + + struct HksCompilerSettings + { + int m_emitStructCode; + const char** m_stripNames; + int m_emitGlobalMemoization; + int _m_isHksGlobalMemoTestingMode; + HksCompilerSettings_BytecodeSharingFormat m_bytecodeSharingFormat; + HksCompilerSettings_IntLiteralOptions m_enableIntLiterals; + int(*m_debugMap)(const char*, int); + }; + + enum HksBytecodeSharingMode : __int64 + { + HKS_BYTECODE_SHARING_OFF = 0, + HKS_BYTECODE_SHARING_ON = 1, + HKS_BYTECODE_SHARING_SECURE = 2 + }; + + struct HksGcWeights + { + int m_removeString; + int m_finalizeUserdataNoMM; + int m_finalizeUserdataGcMM; + int m_cleanCoroutine; + int m_removeWeak; + int m_markObject; + int m_traverseString; + int m_traverseUserdata; + int m_traverseCoroutine; + int m_traverseWeakTable; + int m_freeChunk; + int m_sweepTraverse; + }; + + struct GarbageCollector_Stack + { + void* m_storage; + unsigned int m_numEntries; + unsigned int m_numAllocated; + }; + + struct ProtoList + { + void** m_protoList; + unsigned __int16 m_protoSize; + unsigned __int16 m_protoAllocSize; + }; + + struct MemoryManager; + + struct GarbageCollector + { + struct ResumeStack + { + void* m_storage; + hksInt32 m_numEntries; + hksUint32 m_numAllocated; + }; + + struct GreyStack + { + HksObject* m_storage; + hksSize m_numEntries; + hksSize m_numAllocated; + }; + + struct RemarkStack + { + HashTable** m_storage; + hksSize m_numAllocated; + hksSize m_numEntries; + }; + + struct WeakStack_Entry + { + hksInt32 m_weakness; + HashTable* m_table; + }; + + struct WeakStack + { + WeakStack_Entry* m_storage; + hksInt32 m_numEntries; + hksUint32 m_numAllocated; + }; + + HksGcCost m_target; + HksGcCost m_stepsLeft; + HksGcCost m_stepLimit; + HksGcWeights m_costs; + HksGcCost m_unit; + void* m_jumpPoint; + lua_State* m_mainState; + lua_State* m_finalizerState; + MemoryManager* m_memory; + void* m_emergencyGCMemory; + hksInt32 m_phase; + ResumeStack m_resumeStack; + GreyStack m_greyStack; + RemarkStack m_remarkStack; + WeakStack m_weakStack; + hksBool m_finalizing; + HksObject m_safeTableValue; + lua_State* m_startOfStateStackList; + lua_State* m_endOfStateStackList; + lua_State* m_currentState; + HksObject m_safeValue; + void* m_compiler; + void* m_bytecodeReader; + void* m_bytecodeWriter; + hksInt32 m_pauseMultiplier; + HksGcCost m_stepMultiplier; + hksSize m_emergencyMemorySize; + bool m_stopped; + lua_CFunction m_gcPolicy; + hksSize m_pauseTriggerMemoryUsage; + hksInt32 m_stepTriggerCountdown; + hksUint32 m_stringTableIndex; + hksUint32 m_stringTableSize; + UserData* m_lastBlackUD; + UserData* m_activeUD; + }; + + enum MemoryManager_ChunkColor + { + RED = 0x0, + BLACK = 0x1, + }; + + enum Hks_DeleteCheckingMode + { + HKS_DELETE_CHECKING_OFF = 0x0, + HKS_DELETE_CHECKING_ACCURATE = 0x1, + HKS_DELETE_CHECKING_SAFE = 0x2, + }; + + struct MemoryManager + { + enum ChunkColor : __int32 + { + WHITE = 0x0, + BLACK = 0x1, + }; + + lua_Alloc m_allocator; + void* m_allocatorUd; + ChunkColor m_chunkColor; + hksSize m_used; + hksSize m_highwatermark; + ChunkList m_allocationList; + ChunkList m_sweepList; + ChunkHeader* m_lastKeptChunk; + lua_State* m_state; + }; + + struct StaticStringCache + { + HksObject m_objects[41]; + }; + + enum HksBytecodeEndianness + { + HKS_BYTECODE_DEFAULT_ENDIAN = 0x0, + HKS_BYTECODE_BIG_ENDIAN = 0x1, + HKS_BYTECODE_LITTLE_ENDIAN = 0x2, + }; + + struct RuntimeProfileData_Stats + { + unsigned __int64 hksTime; + unsigned __int64 callbackTime; + unsigned __int64 gcTime; + unsigned __int64 cFinalizerTime; + unsigned __int64 compilerTime; + unsigned int hkssTimeSamples; + unsigned int callbackTimeSamples; + unsigned int gcTimeSamples; + unsigned int compilerTimeSamples; + unsigned int num_newuserdata; + unsigned int num_tablerehash; + unsigned int num_pushstring; + unsigned int num_pushcfunction; + unsigned int num_newtables; + }; + + struct RuntimeProfileData + { + __int64 stackDepth; + __int64 callbackDepth; + unsigned __int64 lastTimer; + RuntimeProfileData_Stats frameStats; + unsigned __int64 gcStartTime; + unsigned __int64 finalizerStartTime; + unsigned __int64 compilerStartTime; + unsigned __int64 compilerStartGCTime; + unsigned __int64 compilerStartGCFinalizerTime; + unsigned __int64 compilerCallbackStartTime; + __int64 compilerDepth; + void* outFile; + lua_State* rootState; + }; + + struct HksGlobal + { + MemoryManager m_memory; + GarbageCollector m_collector; + StringTable m_stringTable; + __int64 padding3; + HksBytecodeSharingMode m_bytecodeSharingMode; + int padding; + HksObject m_registry; + ChunkList m_userDataList; + lua_State* m_root; + StaticStringCache m_staticStringCache; + void* m_debugger; + void* m_profiler; + RuntimeProfileData m_runProfilerData; + HksCompilerSettings m_compilerSettings; + int(*m_panicFunction)(lua_State*); + void* m_luaplusObjectList; + int m_heapAssertionFrequency; + int m_heapAssertionCount; + void (*m_logFunction)(lua_State*, const char*, ...); + void (*m_emergencyGCFailFunction)(lua_State*, size_t); + HksBytecodeEndianness m_bytecodeDumpEndianness; + int padding2; + }; + + struct lua_State + { + ChunkHeader baseclass; + HksGlobal* m_global; + CallStack m_callStack; + ApiStack m_apistack; + UpValue* pending; + HksObject globals; + HksObject m_cEnv; + CallSite* m_callsites; + int m_numberOfCCalls; + void* m_context; + InternString* m_name; + lua_State* m_nextState; + lua_State* m_nextStateStack; + Status m_status; + HksError m_error; + }; + } + typedef uint32_t ScrVarCanonicalName_t; enum svscmd_type @@ -1019,6 +1656,7 @@ namespace game BeamDef* beamDef; StreamerHint* streamerHint;*/ void* data; + LuaFile* luaFile; }; struct XAsset diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 5a1d369e..2e01435f 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -21,13 +21,14 @@ namespace game WEAK symbol Com_Printf{0x1421499C0, 0x140505630}; WEAK symbol Com_Error_{0x1420F8BD0}; WEAK symbol Com_SessionMode_IsMode{0x1420F7DD0}; + WEAK symbol Com_IsRunningUILevel{0x142148DB0}; WEAK symbol Com_SwitchMode{ 0x14214AF30 }; WEAK symbol Cbuf_AddText{0x1420EC8B0, 0x1404F75B0}; WEAK symbol Cbuf_ExecuteBuffer{ - 0x0, 0x1404F78D0 + 0x14133BE10, 0x1404F78D0 }; WEAK symbol Cmd_AddCommandInternal{ 0x1420ED530, 0x1404F8210 @@ -74,15 +75,33 @@ namespace game WEAK symbol Sys_IsDatabaseReady{0x1421844C0}; // Dvar - WEAK symbol Dvar_FindVar{0x1422BD730, 0x140575540}; + WEAK symbol Dvar_FindVar{0x1422BD730, 0x140575540}; WEAK symbol Dvar_GenerateHash{0x14133DBF0}; WEAK symbol Dvar_FindMalleableVar{0x1422BD6A0}; WEAK symbol Dvar_GetDebugName{0x1422BDCB0}; WEAK symbol Dvar_GetString{0x1422BFFF0, 0x140575E30}; + WEAK symbol Dvar_DisplayableValue{0x1422BCAE0}; + WEAK symbol Dvar_GetBool{ 0x1422BD930 }; + WEAK symbol Dvar_RegisterBool{ + 0x1422D1360 + }; + WEAK symbol Dvar_ForEach{ 0x1422BD760 }; WEAK symbol Dvar_GetInt{0x1422BF2C0, 0x140575C20}; WEAK symbol Dvar_SetFromStringByName{ 0x1422C7F60 }; + WEAK symbol s_dvarPool{ 0x157AC8220 }; + WEAK symbol g_dvarCount{ 0x157AC81CC }; + + // UI + WEAK symbol UI_CoD_Init{ 0x141F298B0, 0x0 }; + WEAK symbol UI_CoD_LobbyUI_Init{ 0x141F2C620, 0x0 }; + WEAK symbol UI_CoD_Shutdown{ 0x141F336B0, 0x0 }; + WEAK symbol UI_AddMenu{ 0x1427024B0, 0x0 }; + WEAK symbol UI_CoD_GetRootNameForController{ 0x141F291E0, 0x0 }; + WEAK symbol Lua_CoD_LoadLuaFile{ 0x141F122C0, 0x0 }; + WEAK symbol CG_LUIHUDRestart{ 0x140F7E970 }; + WEAK symbol CL_CheckKeepDrawingConnectScreen{ 0x1413CCAE0 }; // Scr WEAK symbol Scr_AddInt{0x0, 0x14016F160}; @@ -143,4 +162,24 @@ namespace game bool I_isupper(int c); unsigned int Scr_CanonHash(const char* str); + + namespace hks + { + WEAK symbol lua_state { 0x159C78D88 }; + WEAK symbol hksi_lua_pushlstring{ 0x140A18430 }; + + WEAK symbol hks_obj_settable{ 0x141D4B660 }; + WEAK symbol hks_obj_gettable{ 0x141D4ABF0 }; + WEAK symbol vm_call_internal{ 0x141D71070 }; + WEAK symbol Hashtable_Create{ 0x141D3B5F0 }; + WEAK symbol cclosure_Create{ 0x141D3B7E0 }; + WEAK symbol hksi_luaL_ref{ 0x141D4D1A0 }; + WEAK symbol hksi_luaL_unref{ 0x141D4D320 }; + + WEAK symbol hksi_hksL_loadbuffer{ 0x141D4BD80 }; + WEAK symbol hksi_lua_getinfo{ 0x141D4D960 }; + WEAK symbol hksi_lua_getstack{ 0x141D4DC20 }; + WEAK symbol hksi_luaL_error{ 0x141D4D050 }; + WEAK symbol s_compilerTypeName{ 0x140A18430 }; + } } diff --git a/src/client/game/ui_scripting/execution.cpp b/src/client/game/ui_scripting/execution.cpp new file mode 100644 index 00000000..09572a6e --- /dev/null +++ b/src/client/game/ui_scripting/execution.cpp @@ -0,0 +1,169 @@ +#include +#include "execution.hpp" + +namespace ui_scripting +{ + namespace + { + script_value get_field(void* ptr, game::hks::HksObjectType type, const script_value& key) + { + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(key); + + game::hks::HksObject value{}; + game::hks::HksObject obj{}; + obj.t = type; + obj.v.ptr = ptr; + + game::hks::hks_obj_gettable(&value, state, &obj, &state->m_apistack.top[-1]); + state->m_apistack.top = top; + return value; + } + + void set_field(void* ptr, game::hks::HksObjectType type, const script_value& key, const script_value& value) + { + const auto state = *game::hks::lua_state; + + game::hks::HksObject obj{}; + obj.t = type; + obj.v.ptr = ptr; + + game::hks::hks_obj_settable(state, &obj, &key.get_raw(), &value.get_raw()); + } + } + + void push_value(const script_value& value) + { + const auto state = *game::hks::lua_state; + *state->m_apistack.top = value.get_raw(); + state->m_apistack.top++; + } + + void push_value(const game::hks::HksObject& value) + { + const auto state = *game::hks::lua_state; + *state->m_apistack.top = value; + state->m_apistack.top++; + } + + script_value get_return_value(std::int64_t offset) + { + const auto state = *game::hks::lua_state; + return state->m_apistack.top[-1 - offset]; + } + + arguments get_return_values() + { + const auto state = *game::hks::lua_state; + const auto count = state->m_apistack.top - state->m_apistack.base; + arguments values; + + for (auto i = count - 1; i >= 0; i--) + { + values.push_back(get_return_value(i)); + } + + if (values.empty()) + { + values.push_back({}); + } + + return values; + } + + arguments get_return_values(game::hks::HksObject* base) + { + const auto state = *game::hks::lua_state; + const auto count = state->m_apistack.top - base; + arguments values; + + for (auto i = count - 1; i >= 0; i--) + { + values.push_back(get_return_value(i)); + } + + if (values.empty()) + { + values.push_back({}); + } + + return values; + } + + bool notify(const std::string& name, const event_arguments& arguments) + { + const auto state = *game::hks::lua_state; + if (state == nullptr) + { + return false; + } + + //const auto _0 = 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")()[0].as(); + const auto process_event = root.get("processEvent"); + + table event{}; + event.set("name", name); + event.set("dispatchChildren", true); + + for (const auto& arg : arguments) + { + event.set(arg.first, arg.second); + } + + process_event(root, event); + return true; + } + catch (const std::exception& ex) + { + printf("Error processing event '%s' %s\n", name.data(), ex.what()); + } + + return false; + } + + arguments call_script_function(const function& function, const arguments& arguments) + { + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(function); + for (auto i = arguments.begin(); i != arguments.end(); ++i) + { + push_value(*i); + } + + game::hks::vm_call_internal(state, static_cast(arguments.size()), -1, nullptr); + const auto args = get_return_values(top); + state->m_apistack.top = top; + return args; + } + + script_value get_field(const userdata& self, const script_value& key) + { + return get_field(self.ptr, game::hks::TUSERDATA, key); + } + + script_value get_field(const table& self, const script_value& key) + { + return get_field(self.ptr, game::hks::TTABLE, key); + } + + void set_field(const userdata& self, const script_value& key, const script_value& value) + { + set_field(self.ptr, game::hks::TUSERDATA, key, value); + } + + void set_field(const table& self, const script_value& key, const script_value& value) + { + set_field(self.ptr, game::hks::TTABLE, key, value); + } +} diff --git a/src/client/game/ui_scripting/execution.hpp b/src/client/game/ui_scripting/execution.hpp new file mode 100644 index 00000000..d2e9cf0a --- /dev/null +++ b/src/client/game/ui_scripting/execution.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "game/game.hpp" +#include "types.hpp" +#include "script_value.hpp" + +namespace ui_scripting +{ + void push_value(const script_value& value); + void push_value(const game::hks::HksObject& value); + + script_value get_return_value(std::int64_t offset); + arguments get_return_values(); + arguments get_return_values(game::hks::HksObject* base); + + 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); + script_value get_field(const table& self, const script_value& key); + void set_field(const userdata& self, const script_value& key, const script_value& value); + void set_field(const table& self, const script_value& key, const script_value& value); +} diff --git a/src/client/game/ui_scripting/script_value.cpp b/src/client/game/ui_scripting/script_value.cpp new file mode 100644 index 00000000..454c7dfc --- /dev/null +++ b/src/client/game/ui_scripting/script_value.cpp @@ -0,0 +1,400 @@ +#include +#include "execution.hpp" +#include "types.hpp" +#include "script_value.hpp" + +namespace ui_scripting +{ + hks_object::hks_object(const game::hks::HksObject& value) + { + this->assign(value); + } + + hks_object::hks_object(const hks_object& other) noexcept + { + this->operator=(other); + } + + hks_object::hks_object(hks_object&& other) noexcept + { + this->operator=(std::move(other)); + } + + hks_object& hks_object::operator=(const hks_object& other) noexcept + { + if (this != &other) + { + this->release(); + this->assign(other.value_); + } + + return *this; + } + + hks_object& hks_object::operator=(hks_object&& other) noexcept + { + if (this != &other) + { + this->release(); + this->value_ = other.value_; + other.value_.t = game::hks::TNONE; + } + + return *this; + } + + hks_object::~hks_object() + { + this->release(); + } + + const game::hks::HksObject& hks_object::get() const + { + return this->value_; + } + + void hks_object::assign(const game::hks::HksObject& value) + { + this->value_ = value; + + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(this->value_); + this->ref_ = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + state->m_apistack.top = top; + } + + void hks_object::release() + { + if (this->ref_) + { + game::hks::hksi_luaL_unref(*game::hks::lua_state, -10000, this->ref_); + this->value_.t = game::hks::TNONE; + } + } + + /*************************************************************** + * Constructors + **************************************************************/ + + script_value::script_value(const game::hks::HksObject& value) + : value_(value) + { + } + + script_value::script_value(const int value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TNUMBER; + obj.v.number = static_cast(value); + + this->value_ = obj; + } + + script_value::script_value(const unsigned int value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TNUMBER; + obj.v.number = static_cast(value); + + this->value_ = obj; + } + + script_value::script_value(const bool value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TBOOLEAN; + obj.v.boolean = value; + + this->value_ = obj; + } + + script_value::script_value(const float value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TNUMBER; + obj.v.number = static_cast(value); + + this->value_ = obj; + } + + script_value::script_value(const double value) + : script_value(static_cast(value)) + { + } + + script_value::script_value(const char* value, const std::size_t len) + { + game::hks::HksObject obj{}; + + const auto state = *game::hks::lua_state; + if (state == nullptr) + { + return; + } + + const auto top = state->m_apistack.top; + game::hks::hksi_lua_pushlstring(state, value, static_cast(len)); + obj = state->m_apistack.top[-1]; + state->m_apistack.top = top; + + this->value_ = obj; + } + + script_value::script_value(const char* value) + : script_value(value, std::strlen(value)) + { + } + + script_value::script_value(const std::string& value) + : script_value(value.data(), value.size()) + { + } + + script_value::script_value(const lightuserdata& value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TLIGHTUSERDATA; + obj.v.ptr = value.ptr; + + this->value_ = obj; + } + + script_value::script_value(const userdata& value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TUSERDATA; + obj.v.ptr = value.ptr; + + this->value_ = obj; + } + + script_value::script_value(const table& value) + { + game::hks::HksObject obj{}; + obj.t = game::hks::TTABLE; + obj.v.ptr = value.ptr; + + this->value_ = obj; + } + + script_value::script_value(const function& value) + { + game::hks::HksObject obj{}; + obj.t = value.type; + obj.v.ptr = value.ptr; + + this->value_ = obj; + } + + /*************************************************************** + * Integer + **************************************************************/ + + template <> + bool script_value::is() const + { + const auto number = this->get_raw().v.number; + return this->get_raw().t == game::hks::TNUMBER && static_cast(number) == number; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + int script_value::get() const + { + return static_cast(this->get_raw().v.number); + } + + template <> + unsigned int script_value::get() const + { + return static_cast(this->get_raw().v.number); + } + + /*************************************************************** + * Boolean + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TBOOLEAN; + } + + template <> + bool script_value::get() const + { + return this->get_raw().v.boolean; + } + + /*************************************************************** + * Float + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TNUMBER; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + float script_value::get() const + { + return this->get_raw().v.number; + } + + template <> + double script_value::get() const + { + return this->get_raw().v.number; + } + + /*************************************************************** + * String + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TSTRING; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + const char* script_value::get() const + { + return this->get_raw().v.str->m_data; + } + + template <> + std::string script_value::get() const + { + return this->get(); + } + + /*************************************************************** + * Lightuserdata + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TLIGHTUSERDATA; + } + + template <> + lightuserdata script_value::get() const + { + return this->get_raw().v.ptr; + } + + /*************************************************************** + * Userdata + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TUSERDATA; + } + + template <> + userdata script_value::get() const + { + return this->get_raw().v.ptr; + } + + /*************************************************************** + * Table + **************************************************************/ + + template <> + bool script_value::is
() const + { + return this->get_raw().t == game::hks::TTABLE; + } + + template <> + table script_value::get() const + { + return this->get_raw().v.table; + } + + /*************************************************************** + * Function + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().t == game::hks::TIFUNCTION + || this->get_raw().t == game::hks::TCFUNCTION; + } + + template <> + function script_value::get() const + { + return { this->get_raw().v.cClosure, this->get_raw().t }; + } + + /*************************************************************** + * + **************************************************************/ + + const game::hks::HksObject& script_value::get_raw() const + { + return this->value_.get(); + } + + bool script_value::operator==(const script_value& other) const + { + if (this->get_raw().t != other.get_raw().t) + { + return false; + } + + if (this->get_raw().t == game::hks::TSTRING) + { + return this->get() == other.get(); + } + + return this->get_raw().v.native == other.get_raw().v.native; + } + + arguments script_value::operator()() const + { + return this->as()(); + } + + arguments script_value::operator()(const arguments& arguments) const + { + return this->as()(arguments); + } + + function_argument::function_argument(const arguments& args, const script_value& value, const int index) + : values_(args), value_(value), index_(index) + { + } + + function_arguments::function_arguments(const arguments& values) + : values_(values) + { + } +} diff --git a/src/client/game/ui_scripting/script_value.hpp b/src/client/game/ui_scripting/script_value.hpp new file mode 100644 index 00000000..479e8a11 --- /dev/null +++ b/src/client/game/ui_scripting/script_value.hpp @@ -0,0 +1,251 @@ +#pragma once +#include "game/game.hpp" + +#include + +namespace ui_scripting +{ + class lightuserdata; + class userdata_value; + class userdata; + class table_value; + class table; + class function; + class script_value; + + template + std::string get_typename() + { + auto& info = typeid(T); + + if (info == typeid(std::string) || + info == typeid(const char*)) + { + return "string"; + } + + if (info == typeid(lightuserdata)) + { + return "lightuserdata"; + } + + if (info == typeid(userdata)) + { + return "userdata"; + } + + if (info == typeid(table)) + { + return "table"; + } + + if (info == typeid(function)) + { + return "function"; + } + + if (info == typeid(int) || + info == typeid(float) || + info == typeid(unsigned int)) + { + return "number"; + } + + if (info == typeid(bool)) + { + return "boolean"; + } + + return info.name(); + } + + class hks_object + { + public: + hks_object() = default; + hks_object(const game::hks::HksObject& value); + hks_object(const hks_object& other) noexcept; + hks_object(hks_object&& other) noexcept; + + hks_object& operator=(const hks_object& other) noexcept; + hks_object& operator=(hks_object&& other) noexcept; + + ~hks_object(); + + const game::hks::HksObject& get() const; + + private: + void assign(const game::hks::HksObject& value); + void release(); + + game::hks::HksObject value_{ game::hks::TNONE, {} }; + int ref_{}; + }; + + using arguments = std::vector; + using event_arguments = std::unordered_map; + + class script_value + { + public: + script_value() = default; + script_value(const game::hks::HksObject& value); + + script_value(int value); + script_value(unsigned int value); + script_value(bool value); + + script_value(float value); + script_value(double value); + + script_value(const char* value, std::size_t len); + script_value(const char* value); + script_value(const std::string& value); + + script_value(const lightuserdata& value); + script_value(const userdata& value); + script_value(const table& value); + script_value(const function& value); + + template class C, class T, typename TableType = table> + script_value(const C>& container) + { + TableType table_{}; + int index = 1; + + for (const auto& value : container) + { + table_.set(index++, value); + } + + game::hks::HksObject obj{}; + obj.t = game::hks::TTABLE; + obj.v.ptr = table_.ptr; + + this->value_ = obj; + } + + template + script_value(F f) + : script_value(function(f)) + { + } + + bool operator==(const script_value& other) const; + + arguments operator()() const; + arguments operator()(const arguments& arguments) const; + + template + arguments operator()(T... arguments) const + { + return this->as().call({ arguments... }); + } + + template + table_value operator[](const char(&key)[Size]) const + { + return { this->as
(), key }; + } + + template + table_value operator[](const T& key) const + { + return { this->as
(), key }; + } + + template + [[nodiscard]] bool is() const; + + template + T as() const + { + if (!this->is()) + { + const auto hks_typename = game::hks::s_compilerTypeName[this->get_raw().t + 2]; + const auto typename_ = get_typename(); + + throw std::runtime_error(utils::string::va("%s expected, got %s", typename_.data(), hks_typename)); + } + + return get(); + } + + template + operator T() const + { + return this->as(); + } + + [[nodiscard]] const game::hks::HksObject& get_raw() const; + + hks_object value_{}; + + private: + template + T get() const; + }; + + class variadic_args : public arguments + { + }; + + class function_argument + { + public: + function_argument(const arguments& args, const script_value& value, const int index); + + template + T as() const + { + try + { + return this->value_.as(); + } + catch (const std::exception& e) + { + throw std::runtime_error(utils::string::va("bad argument #%d (%s)", this->index_ + 1, e.what())); + } + } + + template <> + variadic_args as() const + { + variadic_args args{}; + for (auto i = this->index_; i < this->values_.size(); i++) + { + args.push_back(this->values_[i]); + } + return args; + } + + template + operator T() const + { + return this->as(); + } + + private: + arguments values_{}; + script_value value_{}; + int index_{}; + }; + + class function_arguments + { + public: + function_arguments(const arguments& values); + + function_argument operator[](const int index) const + { + if (static_cast(index) >= values_.size()) + { + return { values_, {}, index }; + } + + return { values_, values_[index], index }; + } + private: + arguments values_{}; + }; +} diff --git a/src/client/game/ui_scripting/types.cpp b/src/client/game/ui_scripting/types.cpp new file mode 100644 index 00000000..d51b7973 --- /dev/null +++ b/src/client/game/ui_scripting/types.cpp @@ -0,0 +1,351 @@ +#include +#include "types.hpp" +#include "execution.hpp" + +namespace ui_scripting +{ + /*************************************************************** + * Lightuserdata + **************************************************************/ + + lightuserdata::lightuserdata(void* ptr_) + : ptr(ptr_) + { + } + + /*************************************************************** + * Userdata + **************************************************************/ + + userdata::userdata(void* ptr_) + : ptr(ptr_) + { + this->add(); + } + + userdata::userdata(const userdata& other) + { + this->operator=(other); + } + + userdata::userdata(userdata&& other) noexcept + { + this->ptr = other.ptr; + this->ref = other.ref; + other.ref = 0; + } + + userdata::~userdata() + { + this->release(); + } + + userdata& userdata::operator=(const userdata& other) + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->ref = other.ref; + this->add(); + } + + return *this; + } + + userdata& userdata::operator=(userdata&& other) noexcept + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->ref = other.ref; + other.ref = 0; + } + + return *this; + } + + void userdata::add() + { + game::hks::HksObject value{}; + value.v.ptr = this->ptr; + value.t = game::hks::TUSERDATA; + + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + state->m_apistack.top = top; + } + + void userdata::release() + { + if (this->ref) + { + game::hks::hksi_luaL_unref(*game::hks::lua_state, -10000, this->ref); + } + } + + void userdata::set(const script_value& key, const script_value& value) const + { + set_field(*this, key, value); + } + + script_value userdata::get(const script_value& key) const + { + return get_field(*this, key); + } + + userdata_value userdata::operator[](const script_value& key) const + { + return { *this, key }; + } + + userdata_value::userdata_value(const userdata& table, const script_value& key) + : userdata_(table), key_(key) + { + this->value_ = this->userdata_.get(key).get_raw(); + } + + void userdata_value::operator=(const script_value& value) + { + this->userdata_.set(this->key_, value); + this->value_ = value.get_raw(); + } + + bool userdata_value::operator==(const script_value& value) + { + return this->userdata_.get(this->key_) == value; + } + + /*************************************************************** + * Table + **************************************************************/ + + table::table() + { + const auto state = *game::hks::lua_state; + this->ptr = game::hks::Hashtable_Create(state, 0, 0); + this->add(); + } + + table::table(game::hks::HashTable* ptr_) + : ptr(ptr_) + { + this->add(); + } + + table::table(const table& other) + { + this->operator=(other); + } + + table::table(table&& other) noexcept + { + this->ptr = other.ptr; + this->ref = other.ref; + other.ref = 0; + } + + table::~table() + { + this->release(); + } + + table& table::operator=(const table& other) + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->ref = other.ref; + this->add(); + } + + return *this; + } + + table& table::operator=(table&& other) noexcept + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->ref = other.ref; + other.ref = 0; + } + + return *this; + } + + void table::add() + { + game::hks::HksObject value{}; + value.v.table = this->ptr; + value.t = game::hks::TTABLE; + + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + state->m_apistack.top = top; + } + + void table::release() + { + if (this->ref) + { + game::hks::hksi_luaL_unref(*game::hks::lua_state, -10000, this->ref); + } + } + + void table::set(const script_value& key, const script_value& value) const + { + set_field(*this, key, value); + } + + table_value table::operator[](const script_value& key) const + { + return { *this, key }; + } + + script_value table::get(const script_value& key) const + { + return get_field(*this, key); + } + + table_value::table_value(const table& table, const script_value& key) + : table_(table), key_(key) + { + this->value_ = this->table_.get(key).get_raw(); + } + + void table_value::operator=(const script_value& value) + { + this->table_.set(this->key_, value); + this->value_ = value.get_raw(); + } + + void table_value::operator=(const table_value& value) + { + this->table_.set(this->key_, value); + this->value_ = value.get_raw(); + } + + bool table_value::operator==(const script_value& value) + { + return this->table_.get(this->key_) == value; + } + + bool table_value::operator==(const table_value& value) + { + return this->table_.get(this->key_) == value; + } + + /*************************************************************** + * Function + **************************************************************/ + + function::function(game::hks::lua_function func) + { + const auto state = *game::hks::lua_state; + this->ptr = game::hks::cclosure_Create(state, func, 0, 0, 0); + this->type = game::hks::HksObjectType::TCFUNCTION; + this->add(); + } + + function::function(game::hks::cclosure* ptr_, game::hks::HksObjectType type_) + : ptr(ptr_), type(type_) + { + this->add(); + } + + function::function(const function& other) + { + this->operator=(other); + } + + function::function(function&& other) noexcept + { + this->ptr = other.ptr; + this->type = other.type; + this->ref = other.ref; + other.ref = 0; + } + + function::~function() + { + this->release(); + } + + function& function::operator=(const function& other) + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->type = other.type; + this->ref = other.ref; + this->add(); + } + + return *this; + } + + function& function::operator=(function&& other) noexcept + { + if (&other != this) + { + this->release(); + this->ptr = other.ptr; + this->type = other.type; + this->ref = other.ref; + other.ref = 0; + } + + return *this; + } + + void function::add() + { + game::hks::HksObject value{}; + value.v.cClosure = this->ptr; + value.t = this->type; + + const auto state = *game::hks::lua_state; + const auto top = state->m_apistack.top; + + push_value(value); + + this->ref = game::hks::hksi_luaL_ref(*game::hks::lua_state, -10000); + state->m_apistack.top = top; + } + + void function::release() + { + if (this->ref) + { + game::hks::hksi_luaL_unref(*game::hks::lua_state, -10000, this->ref); + } + } + + arguments function::call(const arguments& arguments) const + { + return call_script_function(*this, arguments); + } + + arguments function::operator()(const arguments& arguments) const + { + return this->call(arguments); + } + + arguments function::operator()() const + { + return this->call({}); + } +} diff --git a/src/client/game/ui_scripting/types.hpp b/src/client/game/ui_scripting/types.hpp new file mode 100644 index 00000000..3bf5226f --- /dev/null +++ b/src/client/game/ui_scripting/types.hpp @@ -0,0 +1,142 @@ +#pragma once +#include "game/game.hpp" +#include "script_value.hpp" + +#include "component/ui_scripting.hpp" + +namespace ui_scripting +{ + class lightuserdata + { + public: + lightuserdata(void*); + void* ptr; + }; + + class userdata_value; + + class userdata + { + public: + userdata(void*); + + userdata(const userdata& other); + userdata(userdata&& other) noexcept; + + ~userdata(); + + userdata& operator=(const userdata& other); + userdata& operator=(userdata&& other) noexcept; + + script_value get(const script_value& key) const; + void set(const script_value& key, const script_value& value) const; + + userdata_value operator[](const script_value& key) const; + + void* ptr; + + private: + void add(); + void release(); + + int ref{}; + }; + + class userdata_value : public script_value + { + public: + userdata_value(const userdata& table, const script_value& key); + void operator=(const script_value& value); + bool operator==(const script_value& value); + private: + userdata userdata_; + script_value key_; + }; + + class table_value; + + class table + { + public: + table(); + table(game::hks::HashTable* ptr_); + + table(const table& other); + table(table&& other) noexcept; + + ~table(); + + table& operator=(const table& other); + table& operator=(table&& other) noexcept; + + [[nodiscard]] script_value get(const script_value& key) const; + void set(const script_value& key, const script_value& value) const; + + table_value operator[](const script_value& key) const; + + game::hks::HashTable* ptr; + + private: + void add(); + void release(); + + int ref{}; + }; + + class table_value : public script_value + { + public: + table_value(const table& table, const script_value& key); + void operator=(const script_value& value); + void operator=(const table_value& value); + bool operator==(const script_value& value); + bool operator==(const table_value& value); + private: + table table_; + script_value key_; + }; + + + class function + { + public: + function(game::hks::lua_function); + function(game::hks::cclosure*, game::hks::HksObjectType); + + template + function(F f) + { + this->ptr = ui_scripting::convert_function(f); + this->type = game::hks::TCFUNCTION; + } + + function(const function& other); + function(function&& other) noexcept; + + ~function(); + + function& operator=(const function& other); + function& operator=(function&& other) noexcept; + + arguments call(const arguments& arguments) const; + + arguments operator()(const arguments& arguments) const; + + template + arguments operator()(T... arguments) const + { + return this->call({ arguments... }); + } + + arguments operator()() const; + + game::hks::cclosure* ptr; + game::hks::HksObjectType type; + + private: + void add(); + void release(); + + int ref{}; + }; +}