From 3c09055d2130a1e062d3fe6d6a9420c34bc1dc2a Mon Sep 17 00:00:00 2001 From: m Date: Thu, 28 Dec 2023 01:59:00 -0600 Subject: [PATCH] very early gsc not working TODO: - add script_extension.cpp for scripting.cpp --- .gitmodules | 3 + deps/gsc-tool | 1 + deps/premake/gsc-tool.lua | 62 +++ src/client/component/filesystem.cpp | 203 +++++++- src/client/component/filesystem.hpp | 24 +- src/client/component/gsc/script_error.cpp | 362 ++++++++++++++ src/client/component/gsc/script_error.hpp | 6 + src/client/component/gsc/script_extension.cpp | 471 ++++++++++++++++++ src/client/component/gsc/script_extension.hpp | 48 ++ src/client/component/gsc/script_loading.cpp | 428 ++++++++++++++++ src/client/component/gsc/script_loading.hpp | 9 + src/client/component/patches.cpp | 12 +- src/client/component/scripting.cpp | 221 ++++++++ src/client/component/scripting.hpp | 21 + src/client/game/scripting/array.cpp | 335 +++++++++++++ src/client/game/scripting/array.hpp | 89 ++++ src/client/game/scripting/entity.cpp | 120 +++++ src/client/game/scripting/entity.hpp | 50 ++ src/client/game/scripting/event.hpp | 13 + src/client/game/scripting/execution.cpp | 251 ++++++++++ src/client/game/scripting/execution.hpp | 47 ++ src/client/game/scripting/function.cpp | 44 ++ src/client/game/scripting/function.hpp | 35 ++ src/client/game/scripting/functions.cpp | 107 ++++ src/client/game/scripting/functions.hpp | 14 + src/client/game/scripting/safe_execution.cpp | 72 +++ src/client/game/scripting/safe_execution.hpp | 10 + src/client/game/scripting/script_value.cpp | 356 +++++++++++++ src/client/game/scripting/script_value.hpp | 223 +++++++++ src/client/game/scripting/stack_isolation.cpp | 27 + src/client/game/scripting/stack_isolation.hpp | 25 + src/client/game/scripting/variable_value.cpp | 68 +++ src/client/game/scripting/variable_value.hpp | 27 + src/client/game/scripting/vector.cpp | 85 ++++ src/client/game/scripting/vector.hpp | 31 ++ src/client/game/structs.hpp | 215 ++++++++ src/client/game/symbols.hpp | 62 +++ 37 files changed, 4141 insertions(+), 36 deletions(-) create mode 160000 deps/gsc-tool create mode 100644 deps/premake/gsc-tool.lua create mode 100644 src/client/component/gsc/script_error.cpp create mode 100644 src/client/component/gsc/script_error.hpp create mode 100644 src/client/component/gsc/script_extension.cpp create mode 100644 src/client/component/gsc/script_extension.hpp create mode 100644 src/client/component/gsc/script_loading.cpp create mode 100644 src/client/component/gsc/script_loading.hpp create mode 100644 src/client/component/scripting.cpp create mode 100644 src/client/component/scripting.hpp create mode 100644 src/client/game/scripting/array.cpp create mode 100644 src/client/game/scripting/array.hpp create mode 100644 src/client/game/scripting/entity.cpp create mode 100644 src/client/game/scripting/entity.hpp create mode 100644 src/client/game/scripting/event.hpp create mode 100644 src/client/game/scripting/execution.cpp create mode 100644 src/client/game/scripting/execution.hpp create mode 100644 src/client/game/scripting/function.cpp create mode 100644 src/client/game/scripting/function.hpp create mode 100644 src/client/game/scripting/functions.cpp create mode 100644 src/client/game/scripting/functions.hpp create mode 100644 src/client/game/scripting/safe_execution.cpp create mode 100644 src/client/game/scripting/safe_execution.hpp create mode 100644 src/client/game/scripting/script_value.cpp create mode 100644 src/client/game/scripting/script_value.hpp create mode 100644 src/client/game/scripting/stack_isolation.cpp create mode 100644 src/client/game/scripting/stack_isolation.hpp create mode 100644 src/client/game/scripting/variable_value.cpp create mode 100644 src/client/game/scripting/variable_value.hpp create mode 100644 src/client/game/scripting/vector.cpp create mode 100644 src/client/game/scripting/vector.hpp diff --git a/.gitmodules b/.gitmodules index fc70e9f2..dd690279 100644 --- a/.gitmodules +++ b/.gitmodules @@ -31,3 +31,6 @@ [submodule "deps/rapidjson"] path = deps/rapidjson url = https://github.com/Tencent/rapidjson.git +[submodule "deps/gsc-tool"] + path = deps/gsc-tool + url = https://github.com/xensik/gsc-tool.git diff --git a/deps/gsc-tool b/deps/gsc-tool new file mode 160000 index 00000000..cbfcce1d --- /dev/null +++ b/deps/gsc-tool @@ -0,0 +1 @@ +Subproject commit cbfcce1dd67534c2115331e41d4cb6893e96196c diff --git a/deps/premake/gsc-tool.lua b/deps/premake/gsc-tool.lua new file mode 100644 index 00000000..22849237 --- /dev/null +++ b/deps/premake/gsc-tool.lua @@ -0,0 +1,62 @@ +gsc_tool = { + source = path.join(dependencies.basePath, "gsc-tool") +} + +function gsc_tool.import() + links {"xsk-gsc-iw7", "xsk-gsc-utils"} + gsc_tool.includes() +end + +function gsc_tool.includes() + includedirs { + path.join(gsc_tool.source, "include") + } +end + +function gsc_tool.project() + project "xsk-gsc-utils" + kind "StaticLib" + language "C++" + warnings "Off" + + files { + path.join(gsc_tool.source, "include/xsk/utils/*.hpp"), + path.join(gsc_tool.source, "src/utils/*.cpp") + } + + includedirs { + path.join(gsc_tool.source, "include") + } + + zlib.includes() + + project "xsk-gsc-iw7" + kind "StaticLib" + language "C++" + warnings "Off" + + filter "action:vs*" + buildoptions "/Zc:__cplusplus" + filter {} + + files { + path.join(gsc_tool.source, "include/xsk/stdinc.hpp"), + + path.join(gsc_tool.source, "include/xsk/gsc/engine/iw7.hpp"), + path.join(gsc_tool.source, "src/gsc/engine/iw7.cpp"), + + path.join(gsc_tool.source, "src/gsc/engine/iw7_code.cpp"), + path.join(gsc_tool.source, "src/gsc/engine/iw7_func.cpp"), + path.join(gsc_tool.source, "src/gsc/engine/iw7_meth.cpp"), + path.join(gsc_tool.source, "src/gsc/engine/iw7_token.cpp"), path.join(gsc_tool.source, "src/gsc/*.cpp"), + + path.join(gsc_tool.source, "src/gsc/common/*.cpp"), + path.join(gsc_tool.source, "include/xsk/gsc/common/*.hpp") + } + + includedirs { + path.join(gsc_tool.source, "include") + } +end + +table.insert(dependencies, gsc_tool) diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp index ecdeb879..e01ed850 100644 --- a/src/client/component/filesystem.cpp +++ b/src/client/component/filesystem.cpp @@ -2,42 +2,202 @@ #include "loader/component_loader.hpp" #include "filesystem.hpp" -#include "game/game.hpp" +#include "component/dvars.hpp" +#include "component/console/console.hpp" -#include "dvars.hpp" +#include "game/game.hpp" #include #include +#include -namespace game::filesystem +namespace filesystem { - file::file(std::string name) - : name_(std::move(name)) + namespace { - char* buffer{}; - const auto size = game::FS_ReadFile(this->name_.data(), &buffer); + utils::hook::detour fs_startup_hook; - if (size >= 0 && buffer) + bool initialized = false; + + std::deque& get_search_paths_internal() { - this->valid_ = true; - this->buffer_.append(buffer, size); - game::FS_FreeFile(buffer); + static std::deque search_paths{}; + return search_paths; + } + + void fs_startup_stub(const char* name) + { + console::info("[FS] Startup\n"); + + initialized = true; + + filesystem::register_path(L"."); + filesystem::register_path(L"iw7-mod"); + filesystem::register_path(L"devraw"); + filesystem::register_path(L"devraw_shared"); + filesystem::register_path(L"raw_shared"); + filesystem::register_path(L"raw"); + filesystem::register_path(L"main"); + + fs_startup_hook.invoke(name); + } + + std::vector get_paths(const std::filesystem::path& path) + { + std::vector paths{}; + paths.push_back(path); + return paths; + } + + bool can_insert_path(const std::filesystem::path& path) + { + for (const auto& path_ : get_search_paths_internal()) + { + if (path_ == path) + { + return false; + } + } + + return true; + } + + const char* sys_default_install_path_stub() + { + static auto current_path = std::filesystem::current_path().string(); + return current_path.data(); } } - bool file::exists() const + std::string read_file(const std::string& path) { - return this->valid_; + for (const auto& search_path : get_search_paths_internal()) + { + const auto path_ = search_path / path; + if (utils::io::file_exists(path_.generic_string())) + { + return utils::io::read_file(path_.generic_string()); + } + } + + return {}; } - const std::string& file::get_buffer() const + bool read_file(const std::string& path, std::string* data, std::string* real_path) { - return this->buffer_; + for (const auto& search_path : get_search_paths_internal()) + { + const auto path_ = search_path / path; + if (utils::io::read_file(path_.generic_string(), data)) + { + if (real_path != nullptr) + { + *real_path = path_.generic_string(); + } + + return true; + } + } + + return false; } - const std::string& file::get_name() const + bool find_file(const std::string& path, std::string* real_path) { - return this->name_; + for (const auto& search_path : get_search_paths_internal()) + { + const auto path_ = search_path / path; + if (utils::io::file_exists(path_.generic_string())) + { + *real_path = path_.generic_string(); + return true; + } + } + + return false; + } + + bool exists(const std::string& path) + { + for (const auto& search_path : get_search_paths_internal()) + { + const auto path_ = search_path / path; + if (utils::io::file_exists(path_.generic_string())) + { + return true; + } + } + + return false; + } + + void register_path(const std::filesystem::path& path) + { + if (!initialized) + { + return; + } + + const auto paths = get_paths(path); + for (const auto& path_ : paths) + { + if (can_insert_path(path_)) + { + console::info("[FS] Registering path '%s'\n", path_.generic_string().data()); + get_search_paths_internal().push_front(path_); + } + } + } + + void unregister_path(const std::filesystem::path& path) + { + if (!initialized) + { + return; + } + + const auto paths = get_paths(path); + for (const auto& path_ : paths) + { + auto& search_paths = get_search_paths_internal(); + for (auto i = search_paths.begin(); i != search_paths.end();) + { + if (*i == path_) + { + console::debug("[FS] Unregistering path '%s'\n", path_.generic_string().data()); + i = search_paths.erase(i); + } + else + { + ++i; + } + } + } + } + + std::vector get_search_paths() + { + std::vector paths{}; + + for (const auto& path : get_search_paths_internal()) + { + paths.push_back(path.generic_string()); + } + + return paths; + } + + std::vector get_search_paths_rev() + { + std::vector paths{}; + const auto& search_paths = get_search_paths_internal(); + + for (auto i = search_paths.rbegin(); i != search_paths.rend(); ++i) + { + paths.push_back(i->generic_string()); + } + + return paths; } class component final : public component_interface @@ -45,9 +205,14 @@ namespace game::filesystem public: void post_unpack() override { - dvars::override::register_string("fs_basegame", "iw7-mod", 2048); + fs_startup_hook.create(0xCDD800_b, fs_startup_stub); + + utils::hook::jump(0xCFE5E0_b, sys_default_install_path_stub); + + // fs_game flags + utils::hook::set(0xCDD415_b, 0); } }; } -REGISTER_COMPONENT(game::filesystem::component) \ No newline at end of file +REGISTER_COMPONENT(filesystem::component) diff --git a/src/client/component/filesystem.hpp b/src/client/component/filesystem.hpp index c393796b..67d1d236 100644 --- a/src/client/component/filesystem.hpp +++ b/src/client/component/filesystem.hpp @@ -1,19 +1,15 @@ #pragma once -namespace game::filesystem +namespace filesystem { - class file - { - public: - file(std::string name); + std::string read_file(const std::string& path); + bool read_file(const std::string& path, std::string* data, std::string* real_path = nullptr); + bool find_file(const std::string& path, std::string* real_path); + bool exists(const std::string& path); - [[nodiscard]] bool exists() const; - [[nodiscard]] const std::string& get_buffer() const; - [[nodiscard]] const std::string& get_name() const; + void register_path(const std::filesystem::path& path); + void unregister_path(const std::filesystem::path& path); - private: - bool valid_ = false; - std::string name_; - std::string buffer_; - }; -} \ No newline at end of file + std::vector get_search_paths(); + std::vector get_search_paths_rev(); +} diff --git a/src/client/component/gsc/script_error.cpp b/src/client/component/gsc/script_error.cpp new file mode 100644 index 00000000..5a8b7d04 --- /dev/null +++ b/src/client/component/gsc/script_error.cpp @@ -0,0 +1,362 @@ +#include +/* +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include "script_extension.hpp" +#include "script_error.hpp" + +#include "component/scripting.hpp" + +#include +#include + +using namespace utils::string; + +namespace gsc +{ + namespace + { + utils::hook::detour scr_emit_function_hook; + + std::uint32_t current_filename = 0; + + std::string unknown_function_error; + + std::array var_typename = + { + "undefined", + "object", + "string", + "localized string", + "vector", + "float", + "int", + "codepos", + "precodepos", + "function", + "builtin function", + "builtin method", + "stack", + "animation", + "pre animation", + "thread", + "thread", + "thread", + "thread", + "struct", + "removed entity", + "entity", + "array", + "removed thread", + "", + "thread list", + "endon list", + }; + + void scr_emit_function_stub(std::uint32_t filename, std::uint32_t thread_name, char* code_pos) + { + current_filename = filename; + scr_emit_function_hook.invoke(filename, thread_name, code_pos); + } + + std::string get_filename_name() + { + const auto filename_str = game::SL_ConvertToString(static_cast(current_filename)); + const auto id = std::atoi(filename_str); + if (!id) + { + return filename_str; + } + + return scripting::get_token(id); + } + + void get_unknown_function_error(const char* code_pos) + { + const auto function = find_function(code_pos); + if (function.has_value()) + { + const auto& pos = function.value(); + unknown_function_error = std::format( + "while processing function '{}' in script '{}':\nunknown script '{}' ({})", + pos.first, pos.second, scripting::current_file, scripting::current_file_id + ); + } + else + { + unknown_function_error = std::format("unknown script '{}' ({})", + scripting::current_file, scripting::current_file_id); + } + } + + void get_unknown_function_error(std::uint32_t thread_name) + { + const auto filename = get_filename_name(); + const auto name = scripting::get_token(thread_name); + + unknown_function_error = std::format( + "while processing script '{}':\nunknown function '{}::{}'", + scripting::current_file, filename, name + ); + } + + void compile_error_stub(const char* code_pos, [[maybe_unused]] const char* msg) + { + get_unknown_function_error(code_pos); + game::Com_Error(game::ERR_SCRIPT_DROP, "script link error\n%s", unknown_function_error.data()); + } + + std::uint32_t find_variable_stub(std::uint32_t parent_id, std::uint32_t thread_name) + { + const auto res = game::FindVariable(parent_id, thread_name); + if (!res) + { + get_unknown_function_error(thread_name); + game::Com_Error(game::ERR_SCRIPT_DROP, "script link error\n%s", unknown_function_error.data()); + } + return res; + } + + unsigned int scr_get_object(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_POINTER) + { + return value->u.pointerValue; + } + + scr_error(va("Type %s is not an object", var_typename[value->type])); + } + + scr_error(va("Parameter %u does not exist", index + 1)); + return 0; + } + + unsigned int scr_get_const_string(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (game::Scr_CastString(value)) + { + assert(value->type == game::VAR_STRING); + return value->u.stringValue; + } + + game::Scr_ErrorInternal(); + } + + scr_error(va("Parameter %u does not exist", index + 1)); + return 0; + } + + unsigned int scr_get_const_istring(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_ISTRING) + { + return value->u.stringValue; + } + + scr_error(va("Type %s is not a localized string", var_typename[value->type])); + } + + scr_error(va("Parameter %u does not exist", index + 1)); + return 0; + } + + void scr_validate_localized_string_ref(int parm_index, const char* token, int token_len) + { + assert(token); + assert(token_len >= 0); + + if (token_len < 2) + { + return; + } + + for (auto char_iter = 0; char_iter < token_len; ++char_iter) + { + if (!std::isalnum(static_cast(token[char_iter])) && token[char_iter] != '_') + { + scr_error(va("Illegal localized string reference: %s must contain only alpha-numeric characters and underscores", token)); + } + } + } + + void scr_get_vector(unsigned int index, float* vector_value) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_VECTOR) + { + std::memcpy(vector_value, value->u.vectorValue, sizeof(std::float_t[3])); + return; + } + + scr_error(va("Type %s is not a vector", var_typename[value->type])); + } + + scr_error(va("Parameter %u does not exist", index + 1)); + } + + int scr_get_int(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_INTEGER) + { + return value->u.intValue; + } + + scr_error(va("Type %s is not an int", var_typename[value->type])); + } + + scr_error(va("Parameter %u does not exist", index + 1)); + return 0; + } + + float scr_get_float(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + auto* value = game::scr_VmPub->top - index; + if (value->type == game::VAR_FLOAT) + { + return value->u.floatValue; + } + + if (value->type == game::VAR_INTEGER) + { + return static_cast(value->u.intValue); + } + + scr_error(va("Type %s is not a float", var_typename[value->type])); + } + + scr_error(va("Parameter %u does not exist", index + 1)); + return 0.0f; + } + + int scr_get_pointer_type(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + if ((game::scr_VmPub->top - index)->type == game::VAR_POINTER) + { + return static_cast(game::GetObjectType((game::scr_VmPub->top - index)->u.uintValue)); + } + + scr_error(va("Type %s is not an object", var_typename[(game::scr_VmPub->top - index)->type])); + } + + scr_error(va("Parameter %u does not exist", index + 1)); + return 0; + } + + int scr_get_type(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + return (game::scr_VmPub->top - index)->type; + } + + scr_error(va("Parameter %u does not exist", index + 1)); + return 0; + } + + const char* scr_get_type_name(unsigned int index) + { + if (index < game::scr_VmPub->outparamcount) + { + return var_typename[(game::scr_VmPub->top - index)->type]; + } + + scr_error(va("Parameter %u does not exist", index + 1)); + return nullptr; + } + + template + void safe_func() + { + static utils::hook::detour hook; + static const auto stub = []() + { + __try + { + hook.invoke(); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + game::Scr_ErrorInternal(); + } + }; + + const auto ptr = rva + 0_b; + hook.create(reinterpret_cast(ptr), stub); + } + } + + std::optional> find_function(const char* pos) + { + for (const auto& file : scripting::script_function_table_sort) + { + for (auto i = file.second.begin(); i != file.second.end() && std::next(i) != file.second.end(); ++i) + { + const auto next = std::next(i); + if (pos >= i->second && pos < next->second) + { + return {std::make_pair(i->first, file.first)}; + } + } + } + + return {}; + } + + class error final : public component_interface + { + public: + void post_unpack() override + { + scr_emit_function_hook.create(SELECT_VALUE(0x3BD680_b, 0x504660_b), &scr_emit_function_stub); + + utils::hook::call(SELECT_VALUE(0x3BD626_b, 0x504606_b), compile_error_stub); // CompileError (LinkFile) + utils::hook::call(SELECT_VALUE(0x3BD672_b, 0x504652_b), compile_error_stub); // ^ + utils::hook::call(SELECT_VALUE(0x3BD75A_b, 0x50473A_b), find_variable_stub); // Scr_EmitFunction + + // Restore basic error messages for commonly used scr functions + utils::hook::jump(SELECT_VALUE(0x3C89F0_b, 0x50F9E0_b), scr_get_object); + utils::hook::jump(SELECT_VALUE(0x3C84C0_b, 0x50F560_b), scr_get_const_string); + utils::hook::jump(SELECT_VALUE(0x3C8280_b, 0x50F320_b), scr_get_const_istring); + utils::hook::jump(SELECT_VALUE(0x2D6950_b, 0x452EF0_b), scr_validate_localized_string_ref); + utils::hook::jump(SELECT_VALUE(0x3C8F30_b, 0x50FF20_b), scr_get_vector); + utils::hook::jump(SELECT_VALUE(0x3C8930_b, 0x50F920_b), scr_get_int); + utils::hook::jump(SELECT_VALUE(0x3C87D0_b, 0x50F870_b), scr_get_float); + + utils::hook::jump(SELECT_VALUE(0x3C8C10_b, 0x50FC00_b), scr_get_pointer_type); + utils::hook::jump(SELECT_VALUE(0x3C8DE0_b, 0x50FDD0_b), scr_get_type); + utils::hook::jump(SELECT_VALUE(0x3C8E50_b, 0x50FE40_b), scr_get_type_name); + + if (!game::environment::is_sp()) + { + safe_func<0xBA7A0>(); // fix vlobby cac crash + } + } + + void pre_destroy() override + { + scr_emit_function_hook.clear(); + } + }; +} + +REGISTER_COMPONENT(gsc::error) +*/ diff --git a/src/client/component/gsc/script_error.hpp b/src/client/component/gsc/script_error.hpp new file mode 100644 index 00000000..1cee00d2 --- /dev/null +++ b/src/client/component/gsc/script_error.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace gsc +{ + std::optional> find_function(const char* pos); +} \ No newline at end of file diff --git a/src/client/component/gsc/script_extension.cpp b/src/client/component/gsc/script_extension.cpp new file mode 100644 index 00000000..6e0aa207 --- /dev/null +++ b/src/client/component/gsc/script_extension.cpp @@ -0,0 +1,471 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "component/command.hpp" +#include "component/console/console.hpp" +#include "component/dvars.hpp" +#include "component/scripting.hpp" + +#include "script_extension.hpp" +#include "script_loading.hpp" + +#include + +namespace gsc +{ + std::uint16_t function_id_start = 0x30A; + std::uint16_t method_id_start = 0x8586; + + builtin_function func_table[0x1000]; + builtin_method meth_table[0x1000]; + + const game::dvar_t* developer_script = nullptr; + + namespace + { + std::unordered_map functions; + std::unordered_map methods; + + bool force_error_print = false; + std::optional gsc_error_msg; + game::scr_entref_t saved_ent_ref; + + function_args get_arguments() + { + std::vector args; + + for (auto i = 0; static_cast(i) < game::scr_VmPub->outparamcount; ++i) + { + const auto value = game::scr_VmPub->top[-i]; + args.push_back(value); + } + + return args; + } + + void return_value(const scripting::script_value& value) + { + if (game::scr_VmPub->outparamcount) + { + game::Scr_ClearOutParams(); + } + + scripting::push_value(value); + } + + std::uint16_t get_function_id() + { + const auto pos = game::scr_function_stack->pos; + return *reinterpret_cast( + reinterpret_cast(pos - 2)); + } + + game::scr_entref_t get_entity_id_stub(std::uint32_t ent_id) + { + const auto ref = game::Scr_GetEntityIdRef(ent_id); + saved_ent_ref = ref; + return ref; + } + + void execute_custom_function(const std::uint16_t id) + { + try + { + const auto& function = functions[id]; + const auto result = function(get_arguments()); + const auto type = result.get_raw().type; + + if (type) + { + return_value(result); + } + } + catch (const std::exception& ex) + { + scr_error(ex.what()); + } + } + + void vm_call_builtin_function_stub(builtin_function func) + { + const auto function_id = get_function_id(); + const auto custom = functions.contains(static_cast(function_id)); + if (custom) + { + execute_custom_function(function_id); + return; + } + + if (func == nullptr) + { + scr_error(utils::string::va("builtin function \"%s\" doesn't exist", gsc_ctx->func_name(function_id).data()), true); + return; + } + + func(); + } + + void execute_custom_method(const std::uint16_t id) + { + try + { + const auto& method = methods[id]; + const auto result = method(saved_ent_ref, get_arguments()); + const auto type = result.get_raw().type; + + if (type) + { + return_value(result); + } + } + catch (const std::exception& ex) + { + scr_error(ex.what()); + } + } + + void vm_call_builtin_method_stub(builtin_method meth) + { + const auto method_id = get_function_id(); + const auto custom = methods.contains(static_cast(method_id)); + if (custom) + { + execute_custom_method(method_id); + return; + } + + if (meth == nullptr) + { + scr_error(utils::string::va("builtin method \"%s\" doesn't exist", gsc_ctx->meth_name(method_id).data()), true); + return; + } + + meth(saved_ent_ref); + } + + void builtin_call_error(const std::string& error) + { + const auto function_id = get_function_id(); + + if (function_id > 0x1000) + { + console::warn("in call to builtin method \"%s\"%s", gsc_ctx->meth_name(function_id).data(), error.data()); + } + else + { + console::warn("in call to builtin function \"%s\"%s", gsc_ctx->func_name(function_id).data(), error.data()); + } + } + + std::optional get_opcode_name(const std::uint8_t opcode) + { + try + { + const auto index = gsc_ctx->opcode_enum(opcode); + return { gsc_ctx->opcode_name(index) }; + } + catch (...) + { + return {}; + } + } + + void print_callstack() + { + for (auto frame = game::scr_VmPub->function_frame; frame != game::scr_VmPub->function_frame_start; --frame) + { + const auto pos = frame == game::scr_VmPub->function_frame ? game::scr_function_stack->pos : frame->fs.pos; + const auto function = find_function(frame->fs.pos); + + if (function.has_value()) + { + console::warn("\tat function \"%s\" in file \"%s.gsc\"\n", function.value().first.data(), function.value().second.data()); + } + else + { + console::warn("\tat unknown location %p\n", pos); + } + } + } + + void vm_error_stub(int mark_pos) + { +#ifdef FALSE // TODO + const bool dev_script = developer_script ? developer_script->current.enabled : false; +#else + const bool dev_script = true; +#endif + if (!dev_script && !force_error_print) + { + utils::hook::invoke(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), mark_pos); + return; + } + + console::warn("*********** script runtime error *************\n"); + + const auto opcode_id = *reinterpret_cast(SELECT_VALUE(0xC4015E8_b, 0xB7B8968_b)); + const std::string error_str = gsc_error_msg.has_value() + ? utils::string::va(": %s", gsc_error_msg.value().data()) + : ""; + + if ((opcode_id >= 0x1A && opcode_id <= 0x20) || (opcode_id >= 0xA9 && opcode_id <= 0xAF)) + { + builtin_call_error(error_str); + } + else + { + const auto opcode = get_opcode_name(opcode_id); + if (opcode.has_value()) + { + console::warn("while processing instruction %s%s\n", opcode.value().data(), error_str.data()); + } + else + { + console::warn("while processing instruction 0x%X%s\n", opcode_id, error_str.data()); + } + } + + force_error_print = false; + gsc_error_msg = {}; + + print_callstack(); + console::warn("**********************************************\n"); + utils::hook::invoke(SELECT_VALUE(0x415C90_b, 0x59DDA0_b), mark_pos); + } + + void print(const function_args& args) + { + std::string buffer{}; + + for (auto i = 0u; i < args.size(); ++i) + { + const auto str = args[i].to_string(); + buffer.append(str); + buffer.append("\t"); + } + console::info("%s\n", buffer.data()); + } + + scripting::script_value typeof(const function_args& args) + { + return args[0].type_name(); + } + } + + void scr_error(const char* error, const bool force_print) + { + force_error_print = force_print; + gsc_error_msg = error; + + game::Scr_ErrorInternal(); + } + + namespace function + { + void add(const std::string& name, script_function function) + { + if (gsc_ctx->func_exists(name)) + { + const auto id = gsc_ctx->func_id(name); + functions[id] = function; + } + else + { + const auto id = ++function_id_start; + gsc_ctx->func_add(name, static_cast(id)); + functions[id] = function; + } + } + } + + namespace method + { + void add(const std::string& name, script_method method) + { + if (gsc_ctx->meth_exists(name)) + { + const auto id = gsc_ctx->meth_id(name); + methods[id] = method; + } + else + { + const auto id = ++method_id_start; + gsc_ctx->meth_add(name, static_cast(id)); + methods[id] = method; + } + } + } + + function_args::function_args(std::vector values) + : values_(values) + { + } + + std::uint32_t function_args::size() const + { + return static_cast(this->values_.size()); + } + + std::vector function_args::get_raw() const + { + return this->values_; + } + + scripting::value_wrap function_args::get(const int index) const + { + if (index >= this->values_.size()) + { + throw std::runtime_error(utils::string::va("parameter %d does not exist", index)); + } + + return {this->values_[index], index}; + } + + class extension final : public component_interface + { + public: + void post_unpack() override + { +#ifdef FALSE // TODO + developer_script = dvars::register_bool("developer_script", false, 0, "Enable developer script comments"); +#endif + + utils::hook::set(0xBFD16C_b, 0x1000); // change builtin func count + + utils::hook::set(0xBFD172_b + 4, + static_cast(reverse_b((&func_table)))); + + // TODO + utils::hook::set(SELECT_VALUE(0x3CB718_b, 0x512778_b) + 4, + static_cast(reverse_b((&func_table)))); + utils::hook::inject(SELECT_VALUE(0x3BDC28_b, 0x504C58_b) + 3, &func_table); + utils::hook::set(SELECT_VALUE(0x3BDC1E_b, 0x504C4E_b), sizeof(func_table)); + + utils::hook::set(SELECT_VALUE(0x3BD882_b, 0x504862_b) + 4, + static_cast(reverse_b((&meth_table)))); + utils::hook::set(SELECT_VALUE(0x3CBA3B_b, 0x512A9B_b) + 4, + static_cast(reverse_b(&meth_table))); + utils::hook::inject(SELECT_VALUE(0x3BDC36_b, 0x504C66_b) + 3, &meth_table); + utils::hook::set(SELECT_VALUE(0x3BDC3F_b, 0x504C6F_b), sizeof(meth_table)); + + utils::hook::nop(SELECT_VALUE(0x3CB723_b, 0x512783_b), 8); + utils::hook::call(SELECT_VALUE(0x3CB723_b, 0x512783_b), vm_call_builtin_function_stub); + + utils::hook::call(SELECT_VALUE(0x3CBA12_b, 0x512A72_b), get_entity_id_stub); + utils::hook::nop(SELECT_VALUE(0x3CBA46_b, 0x512AA6_b), 6); + utils::hook::nop(SELECT_VALUE(0x3CBA4E_b, 0x512AAE_b), 2); + utils::hook::call(SELECT_VALUE(0x3CBA46_b, 0x512AA6_b), vm_call_builtin_method_stub); + + utils::hook::call(SELECT_VALUE(0x3CC9F3_b, 0x513A53_b), vm_error_stub); // LargeLocalResetToMark + + if (game::environment::is_dedi()) + { + function::add("isusingmatchrulesdata", [](const function_args& args) + { + // return 0 so the game doesn't override the cfg + return 0; + }); + } + + /* + function::add("print", [](const function_args& args) + { + print(args); + return scripting::script_value{}; + }); + + function::add("println", [](const function_args& args) + { + print(args); + return scripting::script_value{}; + }); + + function::add("assert", [](const function_args& args) + { + const auto expr = args[0].as(); + if (!expr) + { + throw std::runtime_error("assert fail"); + } + + return scripting::script_value{}; + }); + + function::add("assertex", [](const function_args& args) + { + const auto expr = args[0].as(); + if (!expr) + { + const auto error = args[1].as(); + throw std::runtime_error(error); + } + + return scripting::script_value{}; + }); + + function::add("getfunction", [](const function_args& args) + { + const auto filename = args[0].as(); + const auto function = args[1].as(); + + if (!scripting::script_function_table[filename].contains(function)) + { + throw std::runtime_error("function not found"); + } + + return scripting::function{scripting::script_function_table[filename][function]}; + }); + + function::add("replacefunc", [](const function_args& args) + { + const auto what = args[0].get_raw(); + const auto with = args[1].get_raw(); + + if (what.type != game::VAR_FUNCTION || with.type != game::VAR_FUNCTION) + { + throw std::runtime_error("replacefunc: parameter 1 must be a function"); + } + + logfile::set_gsc_hook(what.u.codePosValue, with.u.codePosValue); + + return scripting::script_value{}; + }); + + function::add("toupper", [](const function_args& args) + { + const auto string = args[0].as(); + return utils::string::to_upper(string); + }); + + function::add("logprint", [](const function_args& args) + { + std::string buffer{}; + + for (auto i = 0u; i < args.size(); ++i) + { + const auto string = args[i].as(); + buffer.append(string); + } + + game::G_LogPrintf("%s", buffer.data()); + + return scripting::script_value{}; + }); + + function::add("executecommand", [](const function_args& args) + { + command::execute(args[0].as(), false); + + return scripting::script_value{}; + }); + + function::add("typeof", typeof); + function::add("type", typeof); + */ + } + }; +} + +REGISTER_COMPONENT(gsc::extension) diff --git a/src/client/component/gsc/script_extension.hpp b/src/client/component/gsc/script_extension.hpp new file mode 100644 index 00000000..ad57e2b5 --- /dev/null +++ b/src/client/component/gsc/script_extension.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "game/scripting/array.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/function.hpp" + +namespace gsc +{ + class function_args + { + public: + function_args(std::vector); + + unsigned int size() const; + std::vector get_raw() const; + scripting::value_wrap get(const int index) const; + + scripting::value_wrap operator[](const int index) const + { + return this->get(index); + } + private: + std::vector values_; + }; + + using builtin_function = void(*)(); + using builtin_method = void(*)(game::scr_entref_t); + + using script_function = std::function; + using script_method = std::function; + + extern builtin_function func_table[0x1000]; + extern builtin_method meth_table[0x1000]; + + extern const game::dvar_t* developer_script; + + void scr_error(const char* error, const bool force_print = false); + + namespace function + { + void add(const std::string& name, script_function function); + } + + namespace method + { + void add(const std::string& name, script_method function); + } +} \ No newline at end of file diff --git a/src/client/component/gsc/script_loading.cpp b/src/client/component/gsc/script_loading.cpp new file mode 100644 index 00000000..8609f237 --- /dev/null +++ b/src/client/component/gsc/script_loading.cpp @@ -0,0 +1,428 @@ +#include +#include "loader/component_loader.hpp" + +#include "component/console/console.hpp" +#include "component/fastfiles.hpp" +#include "component/filesystem.hpp" +#include "component/scripting.hpp" + +#include "game/game.hpp" + +#include "script_loading.hpp" + +#include +#include +#include +#include + +namespace gsc +{ + std::unique_ptr gsc_ctx = std::make_unique();; + + namespace + { + utils::hook::detour scr_begin_load_scripts_hook; + utils::hook::detour scr_end_load_scripts_hook; + + std::unordered_map main_handles; + std::unordered_map init_handles; + + utils::memory::allocator scriptfile_allocator; + std::unordered_map loaded_scripts; + + struct + { + char* buf = nullptr; + char* pos = nullptr; + const unsigned int size = 0x100000i64; + } script_memory; + + char* allocate_buffer(size_t size) + { + if (script_memory.buf == nullptr) + { + game::PMem_BeginAlloc("custom_gsc_script", game::PMEM_STACK_GAME); + script_memory.buf = game::PMem_AllocFromSource_NoDebug(script_memory.size, 4, game::PMEM_SOURCE_EXTERNAL); // PMEM_SOURCE_SCRIPT, defined as 0 on IW7?? + game::PMem_EndAlloc("custom_gsc_script", game::PMEM_STACK_GAME); + script_memory.pos = script_memory.buf; + } + + if (script_memory.pos + size > script_memory.buf + script_memory.size) + { + game::Com_Error(game::ERR_FATAL, "Out of custom script memory"); + } + + const auto pos = script_memory.pos; + script_memory.pos += size; + return pos; + } + + void free_script_memory() + { + game::PMem_Free("custom_gsc_script", game::PMEM_STACK_GAME); + script_memory.buf = nullptr; + script_memory.pos = nullptr; + } + + void clear() + { + main_handles.clear(); + init_handles.clear(); + loaded_scripts.clear(); + scriptfile_allocator.clear(); + free_script_memory(); + } + + bool read_raw_script_file(const std::string& name, std::string* data) + { + if (filesystem::read_file(name, data)) + { + return true; + } + + const auto* name_str = name.data(); + if (game::DB_XAssetExists(game::ASSET_TYPE_RAWFILE, name_str) && + !game::DB_IsXAssetDefault(game::ASSET_TYPE_RAWFILE, name_str)) + { + const auto asset = game::DB_FindXAssetHeader(game::ASSET_TYPE_RAWFILE, name_str, false); + const auto len = game::DB_GetRawFileLen(asset.rawfile); + data->resize(len); + game::DB_GetRawBuffer(asset.rawfile, data->data(), len); + if (len > 0) + { + data->pop_back(); + } + + return true; + } + + return false; + } + + game::ScriptFile* load_custom_script(const char* file_name, const std::string& real_name) + { + if (const auto itr = loaded_scripts.find(file_name); itr != loaded_scripts.end()) + { + return itr->second; + } + + if (game::Com_FrontEndScene_IsActive()) + { + return nullptr; + } + + std::string source_buffer{}; + if (!read_raw_script_file(real_name + ".gsc", &source_buffer) || source_buffer.empty()) + { + return nullptr; + } + + // filter out "GSC rawfiles" that were used for development usage and are not meant for us. + // each "GSC rawfile" has a ScriptFile counterpart to be used instead + if (game::DB_XAssetExists(game::ASSET_TYPE_SCRIPTFILE, file_name) && + !game::DB_IsXAssetDefault(game::ASSET_TYPE_SCRIPTFILE, file_name)) + { + if ((real_name.starts_with("maps/createfx") || real_name.starts_with("maps/createart") || real_name.starts_with("maps/mp")) + && (real_name.ends_with("_fx") || real_name.ends_with("_fog") || real_name.ends_with("_hdr"))) + { + console::debug("Refusing to compile rawfile '%s'\n", real_name.data()); + return game::DB_FindXAssetHeader(game::ASSET_TYPE_SCRIPTFILE, file_name, false).scriptfile; + } + } + + console::debug("Loading custom gsc '%s.gsc'", real_name.data()); + + try + { + auto& compiler = gsc_ctx->compiler(); + auto& assembler = gsc_ctx->assembler(); + + std::vector data; + data.assign(source_buffer.begin(), source_buffer.end()); + + const auto assembly_ptr = compiler.compile(real_name, data); + const auto output_script = assembler.assemble(*assembly_ptr); + + const auto bytecode = output_script.first; // formerly named "script" + const auto stack = output_script.second; + + const auto script_file_ptr = static_cast(scriptfile_allocator.allocate(sizeof(game::ScriptFile))); + script_file_ptr->name = file_name; + + script_file_ptr->len = static_cast(stack.size); + script_file_ptr->bytecodeLen = static_cast(bytecode.size); + + const auto stack_size = static_cast(stack.size + 1); + const auto byte_code_size = static_cast(bytecode.size + 1); + + script_file_ptr->buffer = static_cast(scriptfile_allocator.allocate(stack_size)); + std::memcpy(const_cast(script_file_ptr->buffer), stack.data, stack.size); + + script_file_ptr->bytecode = allocate_buffer(byte_code_size); + std::memcpy(script_file_ptr->bytecode, bytecode.data, bytecode.size); + + script_file_ptr->compressedLen = 0; + + loaded_scripts[file_name] = script_file_ptr; + + return script_file_ptr; + } + catch (const std::exception& e) + { + console::error("*********** script compile error *************\n"); + console::error("failed to compile '%s':\n%s", real_name.data(), e.what()); + console::error("**********************************************\n"); + return nullptr; + } + } + + std::string get_raw_script_file_name(const std::string& name) + { + if (name.ends_with(".gsh")) + { + return name; + } + + return name + ".gsc"; + } + + std::string get_script_file_name(const std::string& name) + { + const auto id = gsc_ctx->token_id(name); + if (!id) + { + return name; + } + + return std::to_string(id); + } + + std::pair> read_compiled_script_file(const std::string& name, const std::string& real_name) + { + const auto* script_file = game::DB_FindXAssetHeader(game::ASSET_TYPE_SCRIPTFILE, name.data(), false).scriptfile; + if (script_file == nullptr) + { + throw std::runtime_error(std::format("Could not load scriptfile '{}'", real_name)); + } + + console::debug("Decompiling scriptfile '%s'\n", real_name.data()); + + const auto len = script_file->compressedLen; + const std::string stack{script_file->buffer, static_cast(len)}; + + const auto decompressed_stack = utils::compression::zlib::decompress(stack); + + std::vector stack_data; + stack_data.assign(decompressed_stack.begin(), decompressed_stack.end()); + + return {{reinterpret_cast(script_file->bytecode), static_cast(script_file->bytecodeLen)}, stack_data}; + } + + void load_script(const std::string& name) + { + if (!game::Scr_LoadScript(name.data())) + { + return; + } + + const auto main_handle = game::Scr_GetFunctionHandle(name.data(), gsc_ctx->token_id("main")); + const auto init_handle = game::Scr_GetFunctionHandle(name.data(), gsc_ctx->token_id("init")); + + if (main_handle) + { + console::debug("Loaded '%s::main'\n", name.data()); + main_handles[name] = main_handle; + } + + if (init_handle) + { + console::debug("Loaded '%s::init'\n", name.data()); + init_handles[name] = init_handle; + } + } + + void load_scripts(const std::filesystem::path& root_dir, const std::filesystem::path& subfolder) + { + std::filesystem::path script_dir = root_dir / subfolder; + if (!utils::io::directory_exists(script_dir.generic_string())) + { + return; + } + + const auto scripts = utils::io::list_files(script_dir.generic_string()); + for (const auto& script : scripts) + { + if (!script.ends_with(".gsc")) + { + continue; + } + + std::filesystem::path path(script); + const auto relative = path.lexically_relative(root_dir).generic_string(); + const auto base_name = relative.substr(0, relative.size() - 4); + + load_script(base_name); + } + } + + int db_is_x_asset_default(game::XAssetType type, const char* name) + { + if (loaded_scripts.contains(name)) + { + return 0; + } + + return game::DB_IsXAssetDefault(type, name); + } + + void load_scripts_stub(void* a1, void* a2) + { + utils::hook::invoke(0xB50670_b, a1, a2); + + if (!game::Com_FrontEndScene_IsActive()) + { + for (const auto& path : filesystem::get_search_paths()) + { + load_scripts(path, "scripts/"); + } + } + } + + void db_get_raw_buffer_stub(const game::RawFile* rawfile, char* buf, const int size) + { + if (rawfile->len > 0 && rawfile->compressedLen == 0) + { + std::memset(buf, 0, size); + std::memcpy(buf, rawfile->buffer, std::min(rawfile->len, size)); + return; + } + + game::DB_GetRawBuffer(rawfile, buf, size); + } + + void scr_begin_load_scripts_stub(bool a1) + { + const bool dev_script = true; + const auto comp_mode = dev_script ? + xsk::gsc::build::dev: + xsk::gsc::build::prod; + + gsc_ctx->init(comp_mode, [](const std::string& include_name) + -> std::pair> + { + const auto real_name = get_raw_script_file_name(include_name); + + std::string file_buffer; + if (!read_raw_script_file(real_name, &file_buffer) || file_buffer.empty()) + { + const auto name = get_script_file_name(include_name); + if (game::DB_XAssetExists(game::ASSET_TYPE_SCRIPTFILE, name.data())) + { + return read_compiled_script_file(name, real_name); + } + + throw std::runtime_error(std::format("Could not load gsc file '{}'", real_name)); + } + + std::vector script_data; + script_data.assign(file_buffer.begin(), file_buffer.end()); + + return {{}, script_data}; + }); + + scr_begin_load_scripts_hook.invoke(a1); + } + + void scr_end_load_scripts_stub() + { + // cleanup the compiler + gsc_ctx->cleanup(); + + scr_end_load_scripts_hook.invoke(); + } + + utils::hook::detour g_load_structs_hook; + void g_load_structs_stub() + { + for (auto& function_handle : main_handles) + { + printf("Executing '%s::main'\n", function_handle.first.data()); + game::RemoveRefToObject(game::Scr_ExecThread(function_handle.second, 0)); + } + g_load_structs_hook.invoke(); + } + + utils::hook::detour scr_load_level_hook; + void scr_load_level_stub() + { + for (auto& function_handle : init_handles) + { + printf("Executing '%s::init'\n", function_handle.first.data()); + game::RemoveRefToObject(game::Scr_ExecThread(function_handle.second, 0)); + } + scr_load_level_hook.invoke(); + } + + utils::hook::detour g_shutdown_game_hook; + void g_shutdown_game_stub(bool full_clear) + { + clear(); + g_shutdown_game_hook.invoke(full_clear); + } + } + + game::ScriptFile* find_script(game::XAssetType type, const char* name, int allow_create_default) + { + std::string real_name = name; + const auto id = static_cast(std::atoi(name)); + if (id) + { + real_name = gsc_ctx->token_name(id); + } + + auto* script = load_custom_script(name, real_name); + if (script) + { + return script; + } + + return game::DB_FindXAssetHeader(type, name, allow_create_default).scriptfile; + } + + class loading final : public component_interface + { + public: + void post_unpack() override + { + // Load our scripts with an uncompressed stack + utils::hook::call(0xC09DA7_b, db_get_raw_buffer_stub); + + scr_begin_load_scripts_hook.create(0xBFD500_b, scr_begin_load_scripts_stub); + scr_end_load_scripts_hook.create(0xBFD630_b, scr_end_load_scripts_stub); + + // ProcessScript: hook xasset functions to return our own custom scripts + utils::hook::call(0xC09D37_b, find_script); + utils::hook::call(0xC09D47_b, db_is_x_asset_default); + + // GScr_LoadScripts: initial loading of scripts + utils::hook::call(0xB5CB70_b, load_scripts_stub); + + // execute main handle after G_LoadStructs (now called G_Spawn_LoadStructs) + g_load_structs_hook.create(0x409FB0_b, g_load_structs_stub); + + // execute init handle + scr_load_level_hook.create(0xB51B40_b, scr_load_level_stub); + + // clear memory (G_MainMP_ShutdownGameMemory) + g_shutdown_game_hook.create(0xC56C80_b, g_shutdown_game_stub); + } + + void pre_destroy() override + { + scr_begin_load_scripts_hook.clear(); + scr_end_load_scripts_hook.clear(); + } + }; +} + +REGISTER_COMPONENT(gsc::loading) \ No newline at end of file diff --git a/src/client/component/gsc/script_loading.hpp b/src/client/component/gsc/script_loading.hpp new file mode 100644 index 00000000..0ab9ae94 --- /dev/null +++ b/src/client/component/gsc/script_loading.hpp @@ -0,0 +1,9 @@ +#pragma once +#include + +namespace gsc +{ + extern std::unique_ptr gsc_ctx; + + game::ScriptFile* find_script(game::XAssetType type, const char* name, int allow_create_default); +} diff --git a/src/client/component/patches.cpp b/src/client/component/patches.cpp index 6a66ccea..0644cf96 100644 --- a/src/client/component/patches.cpp +++ b/src/client/component/patches.cpp @@ -198,10 +198,16 @@ namespace patches char* db_read_raw_file_stub(const char* filename, char* buf, const int size) { - const auto file = game::filesystem::file(filename); - if (file.exists()) + std::string file_name = filename; + if (file_name.find(".cfg") == std::string::npos) { - snprintf(buf, size, "%s\n", file.get_buffer().data()); + file_name.append(".cfg"); + } + + std::string buffer{}; + if (filesystem::read_file(file_name, &buffer)) + { + snprintf(buf, size, "%s\n", buffer.data()); return buf; } diff --git a/src/client/component/scripting.cpp b/src/client/component/scripting.cpp new file mode 100644 index 00000000..ec4f070f --- /dev/null +++ b/src/client/component/scripting.cpp @@ -0,0 +1,221 @@ +#include +#include "loader/component_loader.hpp" + +#include "component/gsc/script_loading.hpp" +#include "component/scheduler.hpp" +#include "component/scripting.hpp" + +#include "component/console/console.hpp" + +#include "game/game.hpp" + +#include "game/scripting/event.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/functions.hpp" + +#include + +namespace scripting +{ + std::unordered_map> fields_table; + + std::unordered_map> script_function_table; + std::unordered_map>> script_function_table_sort; + std::unordered_map> script_function_table_rev; + + utils::concurrency::container shared_table; + + std::string current_file; + unsigned int current_file_id{}; + + namespace + { + utils::hook::detour vm_notify_hook; + utils::hook::detour vm_execute_hook; + utils::hook::detour g_load_structs_hook; + utils::hook::detour scr_load_level_hook; + utils::hook::detour g_shutdown_game_hook; + + utils::hook::detour scr_add_class_field_hook; + + 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; + + const char* current_script_file_name; + + game::dvar_t* g_dump_scripts; + + std::vector> shutdown_callbacks; + + std::unordered_map canonical_string_table; + + void vm_notify_stub(const unsigned int notify_list_owner_id, const game::scr_string_t string_value, + game::VariableValue* top) + { + if (!game::Com_FrontEndScene_IsActive()) + { + const auto* string = game::SL_ConvertToString(string_value); + if (string) + { + event e{}; + e.name = string; + e.entity = notify_list_owner_id; + + for (auto* value = top; value->type != game::VAR_PRECODEPOS; --value) + { + e.arguments.emplace_back(*value); + } + } + } + + vm_notify_hook.invoke(notify_list_owner_id, string_value, top); + } + + void g_shutdown_game_stub(const int free_scripts) + { + if (free_scripts) + { + script_function_table_sort.clear(); + script_function_table.clear(); + script_function_table_rev.clear(); + canonical_string_table.clear(); + } + + for (const auto& callback : shutdown_callbacks) + { + callback(free_scripts, false); + } + + scripting::notify(*game::levelEntityId, "shutdownGame_called", {1}); + + g_shutdown_game_hook.invoke(); + + for (const auto& callback : shutdown_callbacks) + { + callback(free_scripts, true); + } + } + + void scr_add_class_field_stub(unsigned int classnum, game::scr_string_t name, unsigned int canonical_string, unsigned int offset) + { + const auto name_str = game::SL_ConvertToString(name); + + if (fields_table[classnum].find(name_str) == fields_table[classnum].end()) + { + fields_table[classnum][name_str] = offset; + } + + scr_add_class_field_hook.invoke(classnum, name, canonical_string, offset); + } + + void process_script_stub(const char* filename) + { + current_script_file_name = filename; + + const auto file_id = atoi(filename); + if (file_id) + { + current_file_id = static_cast(file_id); + current_file = scripting::get_token(current_file_id); + } + else + { + current_file_id = 0; + current_file = filename; + } + + process_script_hook.invoke(filename); + } + + void add_function_sort(unsigned int id, const char* pos) + { + std::string filename = current_file; + + if (!script_function_table_sort.contains(filename)) + { + const auto script = gsc::find_script(game::ASSET_TYPE_SCRIPTFILE, current_script_file_name, false); + if (script) + { + const auto end = &script->bytecode[script->bytecodeLen]; + script_function_table_sort[filename].emplace_back("__end__", end); + } + } + + const auto name = scripting::get_token(id); + auto& itr = script_function_table_sort[filename]; + itr.insert(itr.end() - 1, {name, pos}); + } + + void add_function(const std::string& file, unsigned int id, const char* pos) + { + const auto name = get_token(id); + script_function_table[file][name] = pos; + script_function_table_rev[pos] = {file, name}; + } + + void scr_set_thread_position_stub(unsigned int thread_name, const char* code_pos) + { + add_function_sort(thread_name, code_pos); + + 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); + canonical_string_table[result] = str; + return result; + } + } + + std::string get_token(unsigned int id) + { + if (canonical_string_table.find(id) != canonical_string_table.end()) + { + return canonical_string_table[id]; + } + + return scripting::find_token(id); + } + + void on_shutdown(const std::function& callback) + { + shutdown_callbacks.push_back(callback); + } + + std::optional get_canonical_string(const unsigned int id) + { + if (canonical_string_table.find(id) == canonical_string_table.end()) + { + return {}; + } + + return {canonical_string_table[id]}; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + vm_notify_hook.create(0xC10460_b, vm_notify_stub); + + scr_add_class_field_hook.create(0xC061F0_b, scr_add_class_field_stub); + + scr_set_thread_position_hook.create(0xBFD190_b, scr_set_thread_position_stub); + process_script_hook.create(0xC09D20_b, process_script_stub); + sl_get_canonical_string_hook.create(game::SL_GetCanonicalString, sl_get_canonical_string_stub); + + // clear memory (SV_GameMP_ShutdownGameVM) + g_shutdown_game_hook.create(0xB21CC0_b, g_shutdown_game_stub); + } + }; +} + +REGISTER_COMPONENT(scripting::component) diff --git a/src/client/component/scripting.hpp b/src/client/component/scripting.hpp new file mode 100644 index 00000000..14f2bd49 --- /dev/null +++ b/src/client/component/scripting.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +namespace scripting +{ + using shared_table_t = std::unordered_map; + + extern std::unordered_map> fields_table; + extern std::unordered_map> script_function_table; + extern std::unordered_map>> script_function_table_sort; + extern std::unordered_map> script_function_table_rev; + + extern utils::concurrency::container shared_table; + + extern std::string current_file; + extern unsigned int current_file_id; + + void on_shutdown(const std::function& callback); + std::optional get_canonical_string(const unsigned int id); + std::string get_token(unsigned int id); +} \ No newline at end of file diff --git a/src/client/game/scripting/array.cpp b/src/client/game/scripting/array.cpp new file mode 100644 index 00000000..c2255f08 --- /dev/null +++ b/src/client/game/scripting/array.cpp @@ -0,0 +1,335 @@ +#include +#include "array.hpp" +#include "script_value.hpp" +#include "execution.hpp" + +namespace scripting +{ + array_value::array_value(unsigned int parent_id, unsigned int id) + : id_(id) + , parent_id_(parent_id) + { + if (!this->id_) + { + return; + } + + const auto value = game::scr_VarGlob->childVariableValue[this->id_ + 0xFA00 * (this->parent_id_ & 3)]; + game::VariableValue variable; + variable.u = value.u.u; + variable.type = value.type; + + this->value_ = variable; + } + + void array_value::operator=(const script_value& _value) + { + if (!this->id_) + { + return; + } + + const auto value = _value.get_raw(); + + const auto variable = &game::scr_VarGlob->childVariableValue[this->id_ + 0xFA00 * (this->parent_id_ & 3)]; + game::AddRefToValue(value.type, value.u); + game::RemoveRefToValue(variable->type, variable->u.u); + + variable->type = (char)value.type; + variable->u.u = value.u; + + this->value_ = value; + } + + array::array(const unsigned int id) + : id_(id) + { + this->add(); + } + + array::array(const array& other) : array(other.id_) + { + } + + array::array(array&& other) noexcept + { + this->id_ = other.id_; + other.id_ = 0; + } + + array::array() + { + this->id_ = make_array(); + } + + array::array(std::vector values) + { + this->id_ = make_array(); + + for (const auto& value : values) + { + this->push(value); + } + } + + array::array(std::unordered_map values) + { + this->id_ = make_array(); + + for (const auto& value : values) + { + this->set(value.first, value.second); + } + } + + array::~array() + { + this->release(); + } + + array& array::operator=(const array& other) + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + this->add(); + } + + return *this; + } + + array& array::operator=(array&& other) noexcept + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + other.id_ = 0; + } + + return *this; + } + + void array::add() const + { + if (this->id_) + { + game::AddRefToValue(game::VAR_POINTER, {static_cast(this->id_)}); + } + } + + void array::release() const + { + if (this->id_) + { + game::RemoveRefToValue(game::VAR_POINTER, {static_cast(this->id_)}); + } + } + + std::vector array::get_keys() const + { + std::vector result; + + const auto offset = 0xFA00 * (this->id_ & 3); + auto current = game::scr_VarGlob->objectVariableChildren[this->id_].firstChild; + + for (auto i = offset + current; current; i = offset + current) + { + const auto var = game::scr_VarGlob->childVariableValue[i]; + + if (var.type == game::VAR_UNDEFINED) + { + current = var.nextSibling; + continue; + } + + const auto string_value = (game::scr_string_t)((unsigned __int8)var.name_lo + (var.k.keys.name_hi << 8)); + const auto* str = game::SL_ConvertToString(string_value); + + script_value key; + if (string_value < 0x40000 && str) + { + key = str; + } + else + { + key = (string_value - 0x800000) & 0xFFFFFF; + } + + result.push_back(key); + + current = var.nextSibling; + } + + return result; + } + + int array::size() const + { + return static_cast(game::scr_VarGlob->objectVariableValue[this->id_].u.f.next); + } + + unsigned int array::push(script_value value) const + { + this->set(this->size(), value); + return this->size(); + } + + void array::erase(const unsigned int index) const + { + const auto variable_id = game::FindVariable(this->id_, (index - 0x800000) & 0xFFFFFF); + if (variable_id) + { + game::RemoveVariableValue(this->id_, variable_id); + } + } + + void array::erase(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0); + const auto variable_id = game::FindVariable(this->id_, string_value); + if (variable_id) + { + game::RemoveVariableValue(this->id_, variable_id); + } + } + + script_value array::pop() const + { + const auto value = this->get(this->size() - 1); + this->erase(this->size() - 1); + return value; + } + + script_value array::get(const script_value& key) const + { + if (key.is()) + { + return this->get(key.as()); + } + else + { + return this->get(key.as()); + } + } + + script_value array::get(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0); + const auto variable_id = game::FindVariable(this->id_, string_value); + + if (!variable_id) + { + return {}; + } + + const auto value = game::scr_VarGlob->childVariableValue[variable_id + 0xFA00 * (this->id_ & 3)]; + game::VariableValue variable; + variable.u = value.u.u; + variable.type = value.type; + + return variable; + } + + script_value array::get(const unsigned int index) const + { + const auto variable_id = game::FindVariable(this->id_, (index - 0x800000) & 0xFFFFFF); + + if (!variable_id) + { + return {}; + } + + const auto value = game::scr_VarGlob->childVariableValue[variable_id + 0xFA00 * (this->id_ & 3)]; + game::VariableValue variable; + variable.u = value.u.u; + variable.type = value.type; + + return variable; + } + + void array::set(const script_value& key, const script_value& value) const + { + if (key.is()) + { + this->set(key.as(), value); + } + else + { + this->set(key.as(), value); + } + } + + void array::set(const std::string& key, const script_value& _value) const + { + const auto value = _value.get_raw(); + const auto variable_id = this->get_value_id(key); + + if (!variable_id) + { + return; + } + + const auto variable = &game::scr_VarGlob->childVariableValue[variable_id + 0xFA00 * (this->id_ & 3)]; + + game::AddRefToValue(value.type, value.u); + game::RemoveRefToValue(variable->type, variable->u.u); + + variable->type = (char)value.type; + variable->u.u = value.u; + } + + void array::set(const unsigned int index, const script_value& _value) const + { + const auto value = _value.get_raw(); + const auto variable_id = this->get_value_id(index); + + if (!variable_id) + { + return; + } + + const auto variable = &game::scr_VarGlob->childVariableValue[variable_id + 0xFA00 * (this->id_ & 3)]; + + game::AddRefToValue(value.type, value.u); + game::RemoveRefToValue(variable->type, variable->u.u); + + variable->type = (char)value.type; + variable->u.u = value.u; + } + + unsigned int array::get_entity_id() const + { + return this->id_; + } + + unsigned int array::get_value_id(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0); + const auto variable_id = game::FindVariable(this->id_, string_value); + + if (!variable_id) + { + return game::GetNewVariable(this->id_, string_value); + } + + return variable_id; + } + + unsigned int array::get_value_id(const unsigned int index) const + { + const auto variable_id = game::FindVariable(this->id_, (index - 0x800000) & 0xFFFFFF); + if (!variable_id) + { + return game::GetNewArrayVariable(this->id_, index); + } + + return variable_id; + } + + entity array::get_raw() const + { + return entity(this->id_); + } +} diff --git a/src/client/game/scripting/array.hpp b/src/client/game/scripting/array.hpp new file mode 100644 index 00000000..64a9269b --- /dev/null +++ b/src/client/game/scripting/array.hpp @@ -0,0 +1,89 @@ +#pragma once +#include "game/game.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class array_value : public script_value + { + public: + array_value(unsigned int, unsigned int); + void operator=(const script_value&); + private: + unsigned int id_; + unsigned int parent_id_; + }; + + class array final + { + public: + array(); + array(const unsigned int); + + array(std::vector); + array(std::unordered_map); + + array(const array& other); + array(array&& other) noexcept; + + ~array(); + + array& operator=(const array& other); + array& operator=(array&& other) noexcept; + + std::vector get_keys() const; + int size() const; + + unsigned int push(script_value) const; + void erase(const unsigned int) const; + void erase(const std::string&) const; + script_value pop() const; + + script_value get(const script_value&) const; + script_value get(const std::string&) const; + script_value get(const unsigned int) const; + + void set(const script_value&, const script_value&) const; + void set(const std::string&, const script_value&) const; + void set(const unsigned int, const script_value&) const; + + unsigned int get_entity_id() const; + + unsigned int get_value_id(const std::string&) const; + unsigned int get_value_id(const unsigned int) const; + + entity get_raw() const; + + array_value operator[](const int index) const + { + return {this->id_, this->get_value_id(index)}; + } + + array_value operator[](const std::string& key) const + { + return {this->id_, this->get_value_id(key)}; + } + + template + array_value operator[](const script_value& key) const + { + if (key.is()) + { + return { this->id_, this->get_value_id(key.as()) }; + } + + if (key.is()) + { + return { this->id_, this->get_value_id(key.as()) }; + } + + throw std::runtime_error("Invalid key type"); + } + + private: + void add() const; + void release() const; + + unsigned int id_; + }; +} diff --git a/src/client/game/scripting/entity.cpp b/src/client/game/scripting/entity.cpp new file mode 100644 index 00000000..1119220c --- /dev/null +++ b/src/client/game/scripting/entity.cpp @@ -0,0 +1,120 @@ +#include +#include "entity.hpp" +#include "script_value.hpp" +#include "execution.hpp" + +namespace scripting +{ + entity::entity() + : entity(0) + { + } + + entity::entity(const entity& other) : entity(other.entity_id_) + { + } + + entity::entity(entity&& other) noexcept + { + this->entity_id_ = other.entity_id_; + other.entity_id_ = 0; + } + + entity::entity(const unsigned int entity_id) + : entity_id_(entity_id) + { + this->add(); + } + + entity::entity(game::scr_entref_t entref) + : entity(game::FindEntityId(entref.entnum, entref.classnum)) + { + } + + entity::~entity() + { + this->release(); + } + + entity& entity::operator=(const entity& other) + { + if (&other != this) + { + this->release(); + this->entity_id_ = other.entity_id_; + this->add(); + } + + return *this; + } + + entity& entity::operator=(entity&& other) noexcept + { + if (&other != this) + { + this->release(); + this->entity_id_ = other.entity_id_; + other.entity_id_ = 0; + } + + return *this; + } + + unsigned int entity::get_entity_id() const + { + return this->entity_id_; + } + + game::scr_entref_t entity::get_entity_reference() const + { + if (!this->entity_id_) + { + const auto not_null = static_cast(~0ui16); + return game::scr_entref_t{not_null, not_null}; + } + + return game::Scr_GetEntityIdRef(this->get_entity_id()); + } + + bool entity::operator==(const entity& other) const noexcept + { + return this->get_entity_id() == other.get_entity_id(); + } + + bool entity::operator!=(const entity& other) const noexcept + { + return !this->operator==(other); + } + + void entity::add() const + { + if (this->entity_id_) + { + game::AddRefToValue(game::VAR_POINTER, {static_cast(this->entity_id_)}); + } + } + + void entity::release() const + { + if (this->entity_id_) + { + game::RemoveRefToValue(game::VAR_POINTER, {static_cast(this->entity_id_)}); + } + } + + void entity::set(const std::string& field, const script_value& value) const + { + set_entity_field(*this, field, value); + } + + template <> + script_value entity::get(const std::string& field) const + { + return get_entity_field(*this, field); + } + + script_value entity::call(const std::string& name, const std::vector& arguments) const + { + return call_function(name, *this, arguments); + } +} diff --git a/src/client/game/scripting/entity.hpp b/src/client/game/scripting/entity.hpp new file mode 100644 index 00000000..b1702379 --- /dev/null +++ b/src/client/game/scripting/entity.hpp @@ -0,0 +1,50 @@ +#pragma once +#include "game/game.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class entity final + { + public: + entity(); + entity(unsigned int entity_id); + entity(game::scr_entref_t entref); + + entity(const entity& other); + entity(entity&& other) noexcept; + + ~entity(); + + entity& operator=(const entity& other); + entity& operator=(entity&& other) noexcept; + + void set(const std::string& field, const script_value& value) const; + + template + T get(const std::string& field) const; + + script_value call(const std::string& name, const std::vector& arguments = {}) const; + + unsigned int get_entity_id() const; + game::scr_entref_t get_entity_reference() const; + + bool operator ==(const entity& other) const noexcept; + bool operator !=(const entity& other) const noexcept; + + private: + unsigned int entity_id_; + + void add() const; + void release() const; + }; + + template <> + script_value entity::get(const std::string& field) const; + + template + T entity::get(const std::string& field) const + { + return this->get(field).as(); + } +} diff --git a/src/client/game/scripting/event.hpp b/src/client/game/scripting/event.hpp new file mode 100644 index 00000000..bc1d53e0 --- /dev/null +++ b/src/client/game/scripting/event.hpp @@ -0,0 +1,13 @@ +#pragma once +#include "script_value.hpp" +#include "entity.hpp" + +namespace scripting +{ + struct event + { + std::string name; + entity entity{}; + std::vector arguments; + }; +} diff --git a/src/client/game/scripting/execution.cpp b/src/client/game/scripting/execution.cpp new file mode 100644 index 00000000..d72fc358 --- /dev/null +++ b/src/client/game/scripting/execution.cpp @@ -0,0 +1,251 @@ +#include +#include "execution.hpp" +#include "safe_execution.hpp" +#include "stack_isolation.hpp" + +#include "component/scripting.hpp" + +namespace scripting +{ + namespace + { + game::VariableValue* allocate_argument() + { + game::VariableValue* value_ptr = ++game::scr_VmPub->top; + ++game::scr_VmPub->inparamcount; + return value_ptr; + } + + int get_field_id(const int classnum, const std::string& field) + { + if (scripting::fields_table[classnum].find(field) != scripting::fields_table[classnum].end()) + { + return scripting::fields_table[classnum][field]; + } + + return -1; + } + + script_value get_return_value() + { + if (game::scr_VmPub->inparamcount == 0) + { + return {}; + } + + game::Scr_ClearOutParams(); + game::scr_VmPub->outparamcount = game::scr_VmPub->inparamcount; + game::scr_VmPub->inparamcount = 0; + + return script_value(game::scr_VmPub->top[1 - game::scr_VmPub->outparamcount]); + } + } + + void push_value(const script_value& value) + { + auto* value_ptr = allocate_argument(); + *value_ptr = value.get_raw(); + + game::AddRefToValue(value_ptr->type, value_ptr->u); + } + + void notify(const entity& entity, const std::string& event, const std::vector& arguments) + { + stack_isolation _; + for (auto i = arguments.rbegin(); i != arguments.rend(); ++i) + { + push_value(*i); + } + + const auto event_id = game::SL_GetString(event.data(), 0); + game::Scr_NotifyId(entity.get_entity_id(), event_id, game::scr_VmPub->inparamcount); + } + + script_value call_function(const std::string& name, const entity& entity, + const std::vector& arguments) + { + const auto entref = entity.get_entity_reference(); + + const auto is_method_call = *reinterpret_cast(&entref) != -1; + const auto function = find_function(name, !is_method_call); + if (function == nullptr) + { + throw std::runtime_error("Unknown "s + (is_method_call ? "method" : "function") + " '" + name + "'"); + } + + stack_isolation _; + + for (auto i = arguments.rbegin(); i != arguments.rend(); ++i) + { + push_value(*i); + } + + game::scr_VmPub->outparamcount = game::scr_VmPub->inparamcount; + game::scr_VmPub->inparamcount = 0; + + if (!safe_execution::call(function, entref)) + { + throw std::runtime_error( + "Error executing "s + (is_method_call ? "method" : "function") + " '" + name + "'"); + } + + return get_return_value(); + } + + script_value call_function(const std::string& name, const std::vector& arguments) + { + return call_function(name, entity(), arguments); + } + + template <> + script_value call(const std::string& name, const std::vector& arguments) + { + return call_function(name, arguments); + } + + script_value exec_ent_thread(const entity& entity, const char* pos, const std::vector& arguments) + { + const auto id = entity.get_entity_id(); + + stack_isolation _; + for (auto i = arguments.rbegin(); i != arguments.rend(); ++i) + { + push_value(*i); + } + + game::AddRefToObject(id); + + const auto local_id = game::AllocThread(id); + const auto result = game::VM_Execute(local_id, pos, static_cast(arguments.size())); + game::RemoveRefToObject(result); + + return get_return_value(); + } + + const char* get_function_pos(const std::string& filename, const std::string& function) + { + if (!script_function_table.contains(filename)) + { + throw std::runtime_error("File '" + filename + "' not found"); + } + + const auto& functions = script_function_table[filename]; + if (!functions.contains(function)) + { + throw std::runtime_error("Function '" + function + "' in file '" + filename + "' not found"); + } + + return functions.at(function); + } + + script_value call_script_function(const entity& entity, const std::string& filename, + const std::string& function, const std::vector& arguments) + { + const auto pos = get_function_pos(filename, function); + return exec_ent_thread(entity, pos, arguments); + } + + void set_entity_field(const entity& entity, const std::string& field, const script_value& value) + { + const auto entref = entity.get_entity_reference(); + const int id = get_field_id(entref.classnum, field); + + if (id != -1) + { + stack_isolation _; + push_value(value); + + game::scr_VmPub->outparamcount = game::scr_VmPub->inparamcount; + game::scr_VmPub->inparamcount = 0; + + if (!safe_execution::set_entity_field(entref, id)) + { + throw std::runtime_error("Failed to set value for field '" + field + "'"); + } + } + else + { + set_object_variable(entity.get_entity_id(), field, value); + } + } + + script_value get_entity_field(const entity& entity, const std::string& field) + { + const auto entref = entity.get_entity_reference(); + const auto id = get_field_id(entref.classnum, field); + + if (id != -1) + { + stack_isolation _; + + game::VariableValue value{}; + if (!safe_execution::get_entity_field(entref, id, &value)) + { + throw std::runtime_error("Failed to get value for field '" + field + "'"); + } + + const auto _0 = gsl::finally([value] + { + game::RemoveRefToValue(value.type, value.u); + }); + + return value; + } + + return get_object_variable(entity.get_entity_id(), field); + } + + unsigned int make_array() + { + unsigned int index = 0; + const auto variable = game::AllocVariable(&index); + variable->w.type = game::VAR_ARRAY; + variable->u.f.prev = 0; + variable->u.f.next = 0; + + 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 new file mode 100644 index 00000000..c1f8fe6f --- /dev/null +++ b/src/client/game/scripting/execution.hpp @@ -0,0 +1,47 @@ +#pragma once +#include "game/game.hpp" + +#include "entity.hpp" +#include "array.hpp" +#include "function.hpp" + +#include "script_value.hpp" + +namespace scripting +{ + void push_value(const script_value& value); + + script_value call_function(const std::string& name, const std::vector& arguments); + script_value call_function(const std::string& name, const entity& entity, + const std::vector& arguments); + + template + T call(const std::string& name, const std::vector& arguments = {}); + + template <> + script_value call(const std::string& name, const std::vector& arguments); + + template + T call(const std::string& name, const std::vector& arguments) + { + return call(name, arguments).as(); + } + + script_value exec_ent_thread(const entity& entity, const char* pos, const std::vector& arguments); + const char* get_function_pos(const std::string& filename, const std::string& function); + script_value call_script_function(const entity& entity, const std::string& filename, + const std::string& function, const std::vector& arguments); + + 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.cpp b/src/client/game/scripting/function.cpp new file mode 100644 index 00000000..a70cf95b --- /dev/null +++ b/src/client/game/scripting/function.cpp @@ -0,0 +1,44 @@ +#include + +#include "game/scripting/function.hpp" +#include "game/scripting/execution.hpp" + +#include "component/scripting.hpp" + +namespace scripting +{ + function::function(const char* pos) + : pos_(pos) + { + } + + script_value function::get_raw() const + { + game::VariableValue value; + value.type = game::VAR_FUNCTION; + value.u.codePosValue = this->pos_; + + return value; + } + + const char* function::get_pos() const + { + return this->pos_; + } + + std::string function::get_name() const + { + if (scripting::script_function_table_rev.contains(this->pos_)) + { + const auto& func = scripting::script_function_table_rev[this->pos_]; + return utils::string::va("%s::%s", func.first.data(), func.second.data()); + } + + return "unknown function"; + } + + script_value function::call(const entity& self, std::vector arguments) const + { + return exec_ent_thread(self, this->pos_, arguments); + } +} diff --git a/src/client/game/scripting/function.hpp b/src/client/game/scripting/function.hpp new file mode 100644 index 00000000..1af7b58d --- /dev/null +++ b/src/client/game/scripting/function.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "entity.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class function + { + public: + function(const char*); + + script_value get_raw() const; + const char* get_pos() const; + std::string get_name() const; + + script_value call(const entity& self, std::vector arguments) const; + + script_value operator()(const entity& self, std::vector arguments) const + { + return this->call(self, arguments); + } + + script_value operator()(std::vector arguments) const + { + return this->call(*game::levelEntityId, arguments); + } + + script_value operator()() const + { + return this->call(*game::levelEntityId, {}); + } + private: + const char* pos_; + }; +} diff --git a/src/client/game/scripting/functions.cpp b/src/client/game/scripting/functions.cpp new file mode 100644 index 00000000..bf278442 --- /dev/null +++ b/src/client/game/scripting/functions.cpp @@ -0,0 +1,107 @@ +#include +#include "functions.hpp" + +#include + +#include "component/gsc/script_loading.hpp" + +namespace scripting +{ + namespace + { + int find_function_index(const std::string& name, [[maybe_unused]] const bool prefer_global) + { + const auto target = utils::string::to_lower(name); + auto const& first = gsc::gsc_ctx->func_map(); + auto const& second = gsc::gsc_ctx->meth_map(); + + if (!prefer_global) + { + if (const auto itr = second.find(name); itr != second.end()) + { + return static_cast(itr->second); + } + + if (const auto itr = first.find(name); itr != first.end()) + { + return static_cast(itr->second); + } + } + + if (const auto itr = first.find(name); itr != first.end()) + { + return static_cast(itr->second); + } + + if (const auto itr = second.find(name); itr != second.end()) + { + return static_cast(itr->second); + } + + return -1; + } + } + + std::uint32_t 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(std::uint32_t id) + { + return gsc::gsc_ctx->token_name(id); + } + + std::string find_token_single(std::uint32_t id) + { + return gsc::gsc_ctx->token_name(id); + } + + unsigned int find_token_id(const std::string& name) + { + const auto id = gsc::gsc_ctx->token_id(name); + if (id) + { + return id; + } + + const auto parsed_id = parse_token_id(name); + if (parsed_id) + { + return parsed_id; + } + + return game::SL_GetCanonicalString(name.data()); + } + + script_function get_function_by_index(const std::uint32_t index) + { + static const auto function_table = &gsc::func_table; + static const auto method_table = &gsc::meth_table; + + if (index < 0x1000) + { + return reinterpret_cast(function_table)[index - 1]; + } + + return reinterpret_cast(method_table)[index - 0x8000]; + } + + script_function find_function(const std::string& name, const bool prefer_global) + { + const auto index = find_function_index(name, prefer_global); + if (index < 0) return nullptr; + + return get_function_by_index(index); + } +} \ No newline at end of file diff --git a/src/client/game/scripting/functions.hpp b/src/client/game/scripting/functions.hpp new file mode 100644 index 00000000..5bd1c764 --- /dev/null +++ b/src/client/game/scripting/functions.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + using script_function = void(*)(game::scr_entref_t); + + std::string find_token(std::uint32_t id); + std::string find_token_single(std::uint32_t id); + unsigned int find_token_id(const std::string& name); + + script_function get_function_by_index(std::uint32_t index); + script_function find_function(const std::string& name, const bool prefer_global); +} diff --git a/src/client/game/scripting/safe_execution.cpp b/src/client/game/scripting/safe_execution.cpp new file mode 100644 index 00000000..d100a928 --- /dev/null +++ b/src/client/game/scripting/safe_execution.cpp @@ -0,0 +1,72 @@ +#include +#include "safe_execution.hpp" + +#pragma warning(push) +#pragma warning(disable: 4611) + +namespace scripting::safe_execution +{ + namespace + { + bool execute_with_seh(const script_function function, const game::scr_entref_t& entref) + { + __try + { + function(entref); + return true; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + return false; + } + } + } + + bool call(const script_function function, const game::scr_entref_t& entref) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level])) + { + *game::g_script_error_level -= 1; + return false; + } + + const auto result = execute_with_seh(function, entref); + *game::g_script_error_level -= 1; + return result; + } + + bool set_entity_field(const game::scr_entref_t& entref, const int offset) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level])) + { + *game::g_script_error_level -= 1; + return false; + } + + game::Scr_SetObjectField(entref.classnum, entref.entnum, offset); + + *game::g_script_error_level -= 1; + return true; + } + + bool get_entity_field(const game::scr_entref_t& entref, const int offset, game::VariableValue* value) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level])) + { + value->type = game::VAR_UNDEFINED; + value->u.intValue = 0; + *game::g_script_error_level -= 1; + return false; + } + + game::GetEntityFieldValue(value, entref.classnum, entref.entnum, offset); + + *game::g_script_error_level -= 1; + return true; + } +} + +#pragma warning(pop) diff --git a/src/client/game/scripting/safe_execution.hpp b/src/client/game/scripting/safe_execution.hpp new file mode 100644 index 00000000..6eea59d2 --- /dev/null +++ b/src/client/game/scripting/safe_execution.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "functions.hpp" + +namespace scripting::safe_execution +{ + bool call(script_function function, const game::scr_entref_t& entref); + + bool set_entity_field(const game::scr_entref_t& entref, int offset); + bool get_entity_field(const game::scr_entref_t& entref, int offset, game::VariableValue* value); +} diff --git a/src/client/game/scripting/script_value.cpp b/src/client/game/scripting/script_value.cpp new file mode 100644 index 00000000..1cd7af4e --- /dev/null +++ b/src/client/game/scripting/script_value.cpp @@ -0,0 +1,356 @@ +#include +#include "script_value.hpp" +#include "entity.hpp" +#include "array.hpp" +#include "function.hpp" + +namespace scripting +{ + /*************************************************************** + * Constructors + **************************************************************/ + + script_value::script_value(const game::VariableValue& value) + : value_(value) + { + } + + script_value::script_value(const value_wrap& value) + : value_(value.get_raw()) + { + } + + script_value::script_value(const int value) + { + game::VariableValue variable{}; + variable.type = game::VAR_INTEGER; + variable.u.intValue = value; + + this->value_ = variable; + } + + script_value::script_value(const unsigned int value) + { + game::VariableValue variable{}; + variable.type = game::VAR_INTEGER; + variable.u.uintValue = value; + + this->value_ = variable; + } + + script_value::script_value(const bool value) + : script_value(static_cast(value)) + { + } + + script_value::script_value(const float value) + { + game::VariableValue variable{}; + variable.type = game::VAR_FLOAT; + variable.u.floatValue = value; + + this->value_ = variable; + } + + script_value::script_value(const double value) + : script_value(static_cast(value)) + { + } + + script_value::script_value(const char* value) + { + game::VariableValue variable{}; + variable.type = game::VAR_STRING; + variable.u.stringValue = game::SL_GetString(value, 0); + + const auto _ = gsl::finally([&variable]() + { + game::RemoveRefToValue(variable.type, variable.u); + }); + + this->value_ = variable; + } + + script_value::script_value(const std::string& value) + : script_value(value.data()) + { + } + + script_value::script_value(const entity& value) + { + game::VariableValue variable{}; + variable.type = game::VAR_POINTER; + variable.u.pointerValue = value.get_entity_id(); + + this->value_ = variable; + } + + script_value::script_value(const array& value) + { + game::VariableValue variable{}; + variable.type = game::VAR_POINTER; + variable.u.pointerValue = value.get_entity_id(); + + this->value_ = variable; + } + + script_value::script_value(const function& value) + { + game::VariableValue variable{}; + variable.type = game::VAR_FUNCTION; + variable.u.codePosValue = value.get_pos(); + + this->value_ = variable; + } + + script_value::script_value(const vector& value) + { + game::VariableValue variable{}; + variable.type = game::VAR_VECTOR; + variable.u.vectorValue = game::Scr_AllocVector(value); + + const auto _ = gsl::finally([&variable]() + { + game::RemoveRefToValue(variable.type, variable.u); + }); + + this->value_ = variable; + } + + /*************************************************************** + * Integer + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::VAR_INTEGER; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + int script_value::get() const + { + return this->get_raw().u.intValue; + } + + template <> + unsigned int script_value::get() const + { + return this->get_raw().u.uintValue; + } + + template <> + bool script_value::get() const + { + return this->get_raw().u.uintValue != 0; + } + + /*************************************************************** + * Float + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::VAR_FLOAT; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + float script_value::get() const + { + return this->get_raw().u.floatValue; + } + + template <> + double script_value::get() const + { + return static_cast(this->get_raw().u.floatValue); + } + + /*************************************************************** + * String + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::VAR_STRING; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + const char* script_value::get() const + { + return game::SL_ConvertToString(static_cast(this->get_raw().u.stringValue)); + } + + template <> + std::string script_value::get() const + { + return this->get(); + } + + /*************************************************************** + * Array + **************************************************************/ + + template <> + bool script_value::is() const + { + if (this->get_raw().type != game::VAR_POINTER) + { + return false; + } + + const auto id = this->get_raw().u.uintValue; + const auto type = game::scr_VarGlob->objectVariableValue[id].w.type; + + return type == game::VAR_ARRAY; + } + + template <> + array script_value::get() const + { + return array(this->get_raw().u.uintValue); + } + + /*************************************************************** + * Struct + **************************************************************/ + + template <> + bool script_value::is>() const + { + if (this->get_raw().type != game::VAR_POINTER) + { + return false; + } + + const auto id = this->get_raw().u.uintValue; + const auto type = game::scr_VarGlob->objectVariableValue[id].w.type; + + return type == game::VAR_OBJECT; + } + + /*************************************************************** + * Function + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::VAR_FUNCTION; + } + + template <> + function script_value::get() const + { + return function(this->get_raw().u.codePosValue); + } + + /*************************************************************** + * Entity + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::VAR_POINTER; + } + + template <> + entity script_value::get() const + { + return entity(this->get_raw().u.pointerValue); + } + + /*************************************************************** + * Vector + **************************************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::VAR_VECTOR; + } + + template <> + vector script_value::get() const + { + return this->get_raw().u.vectorValue; + } + + /*************************************************************** + * + **************************************************************/ + + const game::VariableValue& script_value::get_raw() const + { + return this->value_.get(); + } + + value_wrap::value_wrap(const scripting::script_value& value, int argument_index) + : value_(value) + , argument_index_(argument_index) + { + } + + std::string script_value::to_string() const + { + if (this->is()) + { + return utils::string::va("%i", this->as()); + } + + if (this->is()) + { + return utils::string::va("%f", this->as()); + } + + if (this->is()) + { + return this->as(); + } + + if (this->is()) + { + const auto vec = this->as(); + return utils::string::va("(%g, %g, %g)", + vec.get_x(), + vec.get_y(), + vec.get_z() + ); + } + + if (this->is()) + { + const auto func = this->as(); + return utils::string::va("[[ %s ]]", func.get_name().data()); + } + + return this->type_name(); + } +} diff --git a/src/client/game/scripting/script_value.hpp b/src/client/game/scripting/script_value.hpp new file mode 100644 index 00000000..8f8428a1 --- /dev/null +++ b/src/client/game/scripting/script_value.hpp @@ -0,0 +1,223 @@ +#pragma once +#include "game/game.hpp" +#include "variable_value.hpp" +#include "vector.hpp" + +#include + +namespace scripting +{ + class entity; + class array; + class function; + class value_wrap; + + namespace + { + std::array var_typename = + { + "undefined", + "object", + "string", + "localized string", + "vector", + "float", + "int", + "codepos", + "precodepos", + "function", + "builtin function", + "builtin method", + "stack", + "animation", + "pre animation", + "thread", + "thread", + "thread", + "thread", + "struct", + "removed entity", + "entity", + "array", + "removed thread", + "", + "thread list", + "endon list", + }; + + std::string get_typename(const game::VariableValue& value) + { + if (value.type == game::VAR_POINTER) + { + const auto type = game::scr_VarGlob->objectVariableValue[value.u.uintValue].w.type; + return var_typename[type]; + } + else + { + return var_typename[value.type]; + } + } + + template + std::string get_c_typename() + { + auto& info = typeid(T); + + if (info == typeid(std::string)) + { + return "string"; + } + + if (info == typeid(const char*)) + { + return "string"; + } + + if (info == typeid(entity)) + { + return "entity"; + } + + if (info == typeid(array)) + { + return "array"; + } + + if (info == typeid(function)) + { + return "function"; + } + + if (info == typeid(vector)) + { + return "vector"; + } + + return info.name(); + } + } + + class script_value + { + public: + script_value() = default; + script_value(const game::VariableValue& value); + script_value(const value_wrap& 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); + script_value(const std::string& value); + + script_value(const entity& value); + script_value(const array& value); + + script_value(const function& value); + + script_value(const vector& value); + + std::string type_name() const + { + return get_typename(this->get_raw()); + } + + std::string to_string() const; + + template + bool is() const; + + template + T as() const + { + if (!this->is()) + { + const auto type = get_typename(this->get_raw()); + const auto c_type = get_c_typename(); + throw std::runtime_error(std::format("has type '{}' but should be '{}'", type, c_type)); + } + + return get(); + } + + template + T* as_ptr() + { + const auto value = this->as(); + + if (!value) + { + throw std::runtime_error("is null"); + } + + return reinterpret_cast(value); + } + + const game::VariableValue& get_raw() const; + + variable_value value_{}; + private: + template + T get() const; + }; + + class value_wrap + { + public: + value_wrap(const scripting::script_value& value, int argument_index); + + std::string to_string() const + { + return this->value_.to_string(); + } + + std::string type_name() const + { + return this->value_.type_name(); + } + + template + T as() const + { + try + { + return this->value_.as(); + } + catch (const std::exception& e) + { + throw std::runtime_error(utils::string::va("parameter %d %s", this->argument_index_, e.what())); + } + } + + template + T* as_ptr() + { + try + { + return this->value_.as_ptr(); + } + catch (const std::exception& e) + { + throw std::runtime_error(utils::string::va("parameter %d %s", this->argument_index_, e.what())); + } + } + + template + T is() const + { + return this->value_.is(); + } + + const game::VariableValue& get_raw() const + { + return this->value_.get_raw(); + } + + int argument_index_{}; + scripting::script_value value_; + }; +} diff --git a/src/client/game/scripting/stack_isolation.cpp b/src/client/game/scripting/stack_isolation.cpp new file mode 100644 index 00000000..646e2584 --- /dev/null +++ b/src/client/game/scripting/stack_isolation.cpp @@ -0,0 +1,27 @@ +#include +#include "stack_isolation.hpp" + +namespace scripting +{ + stack_isolation::stack_isolation() + { + this->in_param_count_ = game::scr_VmPub->inparamcount; + this->out_param_count_ = game::scr_VmPub->outparamcount; + this->top_ = game::scr_VmPub->top; + this->max_stack_ = game::scr_VmPub->maxstack; + + game::scr_VmPub->top = this->stack_; + game::scr_VmPub->maxstack = &this->stack_[ARRAYSIZE(this->stack_) - 1]; + game::scr_VmPub->inparamcount = 0; + game::scr_VmPub->outparamcount = 0; + } + + stack_isolation::~stack_isolation() + { + game::Scr_ClearOutParams(); + game::scr_VmPub->inparamcount = this->in_param_count_; + game::scr_VmPub->outparamcount = this->out_param_count_; + game::scr_VmPub->top = this->top_; + game::scr_VmPub->maxstack = this->max_stack_; + } +} diff --git a/src/client/game/scripting/stack_isolation.hpp b/src/client/game/scripting/stack_isolation.hpp new file mode 100644 index 00000000..8dffd4bc --- /dev/null +++ b/src/client/game/scripting/stack_isolation.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class stack_isolation final + { + public: + stack_isolation(); + ~stack_isolation(); + + stack_isolation(stack_isolation&&) = delete; + stack_isolation(const stack_isolation&) = delete; + stack_isolation& operator=(stack_isolation&&) = delete; + stack_isolation& operator=(const stack_isolation&) = delete; + + private: + game::VariableValue stack_[512]{}; + + game::VariableValue* max_stack_; + game::VariableValue* top_; + unsigned int in_param_count_; + unsigned int out_param_count_; + }; +} diff --git a/src/client/game/scripting/variable_value.cpp b/src/client/game/scripting/variable_value.cpp new file mode 100644 index 00000000..74f5f510 --- /dev/null +++ b/src/client/game/scripting/variable_value.cpp @@ -0,0 +1,68 @@ +#include +#include "variable_value.hpp" + +namespace scripting +{ + variable_value::variable_value(const game::VariableValue& value) + { + this->assign(value); + } + + variable_value::variable_value(const variable_value& other) noexcept + { + this->operator=(other); + } + + variable_value::variable_value(variable_value&& other) noexcept + { + this->operator=(std::move(other)); + } + + variable_value& variable_value::operator=(const variable_value& other) noexcept + { + if (this != &other) + { + this->release(); + this->assign(other.value_); + } + + return *this; + } + + variable_value& variable_value::operator=(variable_value&& other) noexcept + { + if (this != &other) + { + this->release(); + this->value_ = other.value_; + other.value_.type = game::VAR_UNDEFINED; + } + + return *this; + } + + variable_value::~variable_value() + { + this->release(); + } + + const game::VariableValue& variable_value::get() const + { + return this->value_; + } + + void variable_value::assign(const game::VariableValue& value) + { + this->value_ = value; + game::AddRefToValue(this->value_.type, this->value_.u); + } + + void variable_value::release() + { + if (this->value_.type != game::VAR_UNDEFINED) + { + game::RemoveRefToValue(this->value_.type, this->value_.u); + this->value_.type = game::VAR_UNDEFINED; + } + } +} diff --git a/src/client/game/scripting/variable_value.hpp b/src/client/game/scripting/variable_value.hpp new file mode 100644 index 00000000..af533ee5 --- /dev/null +++ b/src/client/game/scripting/variable_value.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class variable_value + { + public: + variable_value() = default; + variable_value(const game::VariableValue& value); + variable_value(const variable_value& other) noexcept; + variable_value(variable_value&& other) noexcept; + + variable_value& operator=(const variable_value& other) noexcept; + variable_value& operator=(variable_value&& other) noexcept; + + ~variable_value(); + + const game::VariableValue& get() const; + + private: + void assign(const game::VariableValue& value); + void release(); + + game::VariableValue value_{{0}, game::VAR_UNDEFINED}; + }; +} diff --git a/src/client/game/scripting/vector.cpp b/src/client/game/scripting/vector.cpp new file mode 100644 index 00000000..e3b66dcd --- /dev/null +++ b/src/client/game/scripting/vector.cpp @@ -0,0 +1,85 @@ +#include +#include "vector.hpp" + +namespace scripting +{ + vector::vector(const float* value) + { + for (auto i = 0; i < 3; ++i) + { + this->value_[i] = value[i]; + } + } + + vector::vector(const game::vec3_t& value) + : vector(&value[0]) + { + } + + vector::vector(const float x, const float y, const float z) + { + this->value_[0] = x; + this->value_[1] = y; + this->value_[2] = z; + } + + vector::operator game::vec3_t&() + { + return this->value_; + } + + vector::operator const game::vec3_t&() const + { + return this->value_; + } + + game::vec_t& vector::operator[](const size_t i) + { + if (i >= 3) + { + throw std::runtime_error("Out of bounds."); + } + + return this->value_[i]; + } + + const game::vec_t& vector::operator[](const size_t i) const + { + if (i >= 3) + { + throw std::runtime_error("Out of bounds."); + } + + return this->value_[i]; + } + + float vector::get_x() const + { + return this->operator[](0); + } + + float vector::get_y() const + { + return this->operator[](1); + } + + float vector::get_z() const + { + return this->operator[](2); + } + + void vector::set_x(const float value) + { + this->operator[](0) = value; + } + + void vector::set_y(const float value) + { + this->operator[](1) = value; + } + + void vector::set_z(const float value) + { + this->operator[](2) = value; + } +} diff --git a/src/client/game/scripting/vector.hpp b/src/client/game/scripting/vector.hpp new file mode 100644 index 00000000..70b146d6 --- /dev/null +++ b/src/client/game/scripting/vector.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class vector final + { + public: + vector() = default; + vector(const float* value); + vector(const game::vec3_t& value); + vector(float x, float y, float z); + + operator game::vec3_t&(); + operator const game::vec3_t&() const; + + game::vec_t& operator[](size_t i); + const game::vec_t& operator[](size_t i) const; + + float get_x() const; + float get_y() const; + float get_z() const; + + void set_x(float value); + void set_y(float value); + void set_z(float value); + + private: + game::vec3_t value_{0}; + }; +} diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index f3e04e81..dc7cb990 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -3221,6 +3221,221 @@ namespace game BD_NAT_STRICT = 0x3, }; + struct scr_entref_t + { + unsigned short entnum; + unsigned short classnum; + }; + + enum PMem_Stack : __int32 + { + PMEM_STACK_GAME = 0x0, + PMEM_STACK_RENDER_TARGETS = 0x1, + PMEM_STACK_MEM_VIRTUAL = 0x2, + PMEM_STACK_MEMCARD_LARGE_BUFFER = 0x3, + PMEM_STACK_SOUND = 0x4, + PMEM_STACK_STASHED_MEMORY = 0x5, + PMEM_STACK_CINEMATIC = 0x6, + PMEM_STACK_COUNT = 0x7, + }; + + enum PMem_Source + { + PMEM_SOURCE_EXTERNAL = 0x0, + PMEM_SOURCE_DATABASE = 0x1, + PMEM_SOURCE_DEFAULT_LOW = 0x2, + PMEM_SOURCE_DEFAULT_HIGH = 0x3, + PMEM_SOURCE_MOVIE = 0x4, + PMEM_SOURCE_SCRIPT = 0x5, + PMEM_SOURCE_UNK5 = 0x5, + PMEM_SOURCE_UNK6 = 0x6, + PMEM_SOURCE_UNK7 = 0x7, + PMEM_SOURCE_UNK8 = 0x8, + PMEM_SOURCE_CUSTOMIZATION = 0x9, + }; + + enum VariableType + { + VAR_UNDEFINED = 0x0, + VAR_BEGIN_REF = 0x1, + VAR_POINTER = 0x1, + VAR_STRING = 0x2, + VAR_ISTRING = 0x3, + VAR_VECTOR = 0x4, + VAR_END_REF = 0x5, + VAR_FLOAT = 0x5, + VAR_INTEGER = 0x6, + VAR_CODEPOS = 0x7, + VAR_PRECODEPOS = 0x8, + VAR_FUNCTION = 0x9, + VAR_BUILTIN_FUNCTION = 0xA, + VAR_BUILTIN_METHOD = 0xB, + VAR_STACK = 0xC, + VAR_ANIMATION = 0xD, + VAR_PRE_ANIMATION = 0xE, + VAR_THREAD = 0xF, + VAR_NOTIFY_THREAD = 0x10, + VAR_TIME_THREAD = 0x11, + VAR_CHILD_THREAD = 0x12, + VAR_OBJECT = 0x13, + VAR_DEAD_ENTITY = 0x14, + VAR_ENTITY = 0x15, + VAR_ARRAY = 0x16, + VAR_DEAD_THREAD = 0x17, + VAR_COUNT = 0x18, + VAR_FREE = 0x18, + VAR_THREAD_LIST = 0x19, + VAR_ENDON_LIST = 0x1A, + VAR_TOTAL_COUNT = 0x1B, + }; + + struct VariableStackBuffer + { + const char* pos; + unsigned __int16 size; + unsigned __int16 bufLen; + unsigned __int16 localId; + char time; + char buf[1]; + }; + + union VariableUnion + { + int intValue; + unsigned int uintValue; + float floatValue; + unsigned int stringValue; + const float* vectorValue; + const char* codePosValue; + unsigned int pointerValue; + VariableStackBuffer* stackValue; + unsigned int entityOffset; + }; + + struct VariableValue + { + VariableUnion u; + int type; + }; + + struct function_stack_t + { + const char* pos; + unsigned int localId; + unsigned int localVarCount; + VariableValue* top; + VariableValue* startTop; + }; + + struct function_frame_t + { + function_stack_t fs; + int topType; + }; + + struct scrVmPub_t + { + unsigned int* localVars; + VariableValue* maxstack; + int function_count; + function_frame_t* function_frame; + VariableValue* top; + unsigned int inparamcount; + unsigned int outparamcount; + function_frame_t function_frame_start[32]; + VariableValue stack[2048]; + }; + + struct ObjectVariableChildren + { + unsigned __int16 firstChild; + unsigned __int16 lastChild; + }; + + struct ObjectVariableValue_u_f + { + unsigned __int16 prev; + unsigned __int16 next; + }; + + union ObjectVariableValue_u_o_u + { + unsigned __int16 size; + unsigned __int16 entnum; + unsigned __int16 nextEntId; + unsigned __int16 self; + }; + + struct ObjectVariableValue_u_o + { + unsigned __int16 refCount; + ObjectVariableValue_u_o_u u; + }; + + union ObjectVariableValue_w + { + unsigned int type; + unsigned int classnum; + unsigned int notifyName; + unsigned int waitTime; + unsigned int parentLocalId; + }; + + struct ChildVariableValue_u_f + { + unsigned __int16 prev; + unsigned __int16 next; + }; + + union ChildVariableValue_u + { + ChildVariableValue_u_f f; + VariableUnion u; + }; + + struct ChildBucketMatchKeys_keys + { + unsigned __int16 name_hi; + unsigned __int16 parentId; + }; + + union ChildBucketMatchKeys + { + ChildBucketMatchKeys_keys keys; + unsigned int match; + }; + + struct ChildVariableValue + { + ChildVariableValue_u u; + unsigned __int16 next; + char type; + char name_lo; + ChildBucketMatchKeys k; + unsigned __int16 nextSibling; + unsigned __int16 prevSibling; + }; + + union ObjectVariableValue_u + { + ObjectVariableValue_u_f f; + ObjectVariableValue_u_o o; + }; + + struct ObjectVariableValue + { + ObjectVariableValue_u u; + ObjectVariableValue_w w; + }; + + struct scrVarGlob_t + { + ObjectVariableValue objectVariableValue[40960]; + ObjectVariableChildren objectVariableChildren[40960]; + unsigned __int16 childVariableBucket[65536]; + ChildVariableValue childVariableValue[384000]; + }; + #pragma pack(push, 1) struct bdAuthTicket { diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 8b30df2c..80c4c763 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -8,10 +8,22 @@ namespace game * Functions **************************************************************/ + WEAK symbol AddRefToValue{ 0xC04360 }; + WEAK symbol RemoveRefToValue{ 0xC05DB0 }; + WEAK symbol AddRefToObject{ 0xC04350 }; + WEAK symbol RemoveRefToObject{ 0xC05CA0 }; + WEAK symbol AllocThread{ 0xC04580 }; + WEAK symbol AllocVariable{ 0xC04650 }; + WEAK symbol Scr_LoadScript{ 0xBFD900 }; + WEAK symbol Scr_GetFunctionHandle{ 0xBFD780 }; + WEAK symbol Scr_ExecThread{ 0xC0ACD0 }; + WEAK symbol BG_GetGravity{ 0x68DD0 }; WEAK symbol Com_Error{ 0xB8D830 }; + WEAK symbol Com_FrontEnd_IsInFrontEnd{ 0x5AE6C0 }; + WEAK symbol Com_Quit_f{ 0xBADC90 }; WEAK symbol Com_FrontEnd_IsInFrontEnd{ 0x5AE6C0 }; @@ -52,12 +64,16 @@ namespace game WEAK symbol Cmd_TokenizeString{ 0xB7D850 }; WEAK symbol Cmd_EndTokenizeString{ 0xB7CC90 }; + WEAK symbol DB_XAssetExists{ 0xA7C3A0 }; + WEAK symbol DB_GetRawBuffer{ 0xA77AB0 }; + WEAK symbol DB_GetXAssetHeaderName{ 0x9E5BA0 }; WEAK symbol DB_EnumXAssets_FastFile{ 0xA76CE0 }; WEAK symbol DB_IsXAssetDefault{ 0xA780D0 }; WEAK symbol DB_FindXAssetHeader{ 0xA76E00 }; WEAK symbol DB_IsLocalized{ 0x3BC500 }; WEAK symbol DB_ReadRawFile{ 0xA79E30 }; + WEAK symbol DB_GetRawFileLen{ 0xF20AF0 }; namespace DDL { @@ -104,6 +120,15 @@ namespace game WEAK symbol FS_FreeFile{ 0xCDE1F0 }; WEAK symbol FS_Printf{ 0xCDD1C0 }; + WEAK symbol GetVariable{ 0xC05A90 }; + WEAK symbol GetNewVariable{ 0xC05660 }; + WEAK symbol GetNewArrayVariable{ 0xC054E0 }; + WEAK symbol FindVariable{ 0xC05100 }; + WEAK symbol FindEntityId{ 0xC05000 }; + WEAK symbol RemoveVariableValue{ 0xC05E90 }; + WEAK symbol GetEntityFieldValue{ 0xC09CC0 }; + WEAK symbol G_MainMP_GetClientScore{ 0xB20550 }; WEAK symbol I_CleanStr{ 0xCFACC0 }; @@ -154,7 +179,21 @@ namespace game WEAK symbol Sys_SendPacket{ 0xD57DE0 }; WEAK symbol Sys_GetPacket{ 0xD57D50 }; + WEAK symbol PMem_BeginAlloc{ 0xCF0E10 }; + WEAK symbol PMem_EndAlloc{ 0xCF1070 }; + WEAK symbol PMem_AllocFromSource_NoDebug{ 0xCF0BB0 }; + WEAK symbol PMem_Free{ 0xCF10D0 }; + + WEAK symbol VM_Execute{ 0xC0CDB0 }; + + WEAK symbol Scr_NotifyId{ 0xC0C2B0 }; + WEAK symbol Scr_AllocVector{ 0xC06960 }; WEAK symbol Scr_AddInt{ 0xC0A580 }; + WEAK symbol Scr_ClearOutParams{ 0xC0ABC0 }; + WEAK symbol Scr_GetEntityIdRef{ 0xC09050 }; + WEAK symbol Scr_SetObjectField{ 0x40B6E0 }; WEAK symbol Scr_GetInt{ 0xC0B950 }; WEAK symbol ScrPlace_GetViewPlacement{ 0x9E4090 }; @@ -162,6 +201,10 @@ namespace game WEAK symbol StringTable_Lookup{ 0xCE7950 }; WEAK symbol StringTable_GetColumnValueForRow{ 0xCE78E0 }; + WEAK symbol SL_GetString{ 0xC037E0 }; + WEAK symbol SL_ConvertToString{ 0xC03300 }; + WEAK symbol SL_GetCanonicalString{ 0xBFD340 }; + WEAK symbol SV_Cmd_TokenizeString{ 0xB7DD00 }; WEAK symbol SV_Cmd_EndTokenizedString{ 0xB7DCC0 }; WEAK symbol SND_StopSounds{ 0xCA06E0 }; WEAK symbol SND_SetMusicState{ 0xC9E110 }; + WEAK symbol longjmp{ 0x12C0758 }; + WEAK symbol _setjmp{ 0x1423110 }; + /*************************************************************** * Variables **************************************************************/ + WEAK symbol g_script_error_level{ 0x6B16298 }; + WEAK symbol g_script_error{ 0x6B162A0 }; + + WEAK symbol levelEntityId{ 0x665A120 }; + WEAK symbol sv_cmd_args{ 0x5D65C20 }; WEAK symbol cmd_args{ 0x5D65B70 }; WEAK symbol cmd_functions{ 0x5D65CC8 }; @@ -216,4 +267,15 @@ namespace game WEAK symbol query_socket{ 0x779FDC8 }; WEAK symbol threadIds{ 0x602BAB0 }; + + WEAK symbol scr_VarGlob{ 0x6691180 }; + WEAK symbol scr_VmPub{ 0x6B183B0 }; + WEAK symbol scr_function_stack{ 0x6B22908 }; + + WEAK game::symbol pmem_size{ 0x7686A28 }; + WEAK game::symbol pmem_buffer{ 0x7686A20 }; + + WEAK game::symbol g_mem{ 0xD5F26E0, 0xC92E1E0 }; + WEAK game::symbol g_scriptmem{ 0xD5F3140, 0xC92EC40 }; + WEAK game::symbol g_physmem{ 0xD5F3BA0, 0xC92F6A0 }; }