diff --git a/source/proxy-dll/component/debugging.cpp b/source/proxy-dll/component/debugging.cpp index fcb6b1d..340f224 100644 --- a/source/proxy-dll/component/debugging.cpp +++ b/source/proxy-dll/component/debugging.cpp @@ -1,8 +1,10 @@ #include #include "definitions/game.hpp" +#include "definitions/game_runtime_errors.hpp" #include "component/scheduler.hpp" #include "loader/component_loader.hpp" +#include #include namespace debugging @@ -17,7 +19,7 @@ namespace debugging { if ((1 << bitNumber) & infoBitmask) { - connectionInfoString[bitNumber * 2] = bitNumber + 0x41; + connectionInfoString[bitNumber * 2] = (char)(bitNumber + 0x41); } else { @@ -42,7 +44,7 @@ namespace debugging if (!connected) { - float color[4] = { 0.8f, 1.0f, 0.3, 0.8f }; + float color[4] = { 0.8f, 1.0f, 0.3f, 0.8f }; const char* sz = get_connectivity_info_string(infoBitmask); @@ -68,11 +70,30 @@ namespace debugging } } + utilities::hook::detour sys_error_hook; + + void sys_error_stub(uint32_t code, const char* message) + { + const char* error_message = game::runtime_errors::get_error_message(code); + + if (error_message) + { + logger::write(logger::LOG_TYPE_ERROR, "[sys_error] %s (%d): %s", error_message, code, message); + } + else + { + logger::write(logger::LOG_TYPE_ERROR, "[sys_error] %d: %s", code, message); + } + + sys_error_hook.invoke(code, message); + } + class component final : public component_interface { public: void post_unpack() override { + sys_error_hook.create(0x143D36CC0_g, sys_error_stub); scheduler::loop(draw_debug_info, scheduler::renderer); scheduler::loop(test_key_catcher, scheduler::main); } diff --git a/source/proxy-dll/component/gsc_funcs.cpp b/source/proxy-dll/component/gsc_funcs.cpp index b88a891..0fdd47a 100644 --- a/source/proxy-dll/component/gsc_funcs.cpp +++ b/source/proxy-dll/component/gsc_funcs.cpp @@ -1,12 +1,16 @@ #include #include "gsc_funcs.hpp" #include "gsc_custom.hpp" +#include "hashes.hpp" #include "definitions/game.hpp" +#include "definitions/game_runtime_errors.hpp" +#include "definitions/xassets.hpp" #include "loader/component_loader.hpp" #include "component/scheduler.hpp" #include +#include #include namespace gsc_funcs @@ -21,8 +25,25 @@ namespace gsc_funcs hash = ((c + hash) ^ ((c + hash) << 10)) + (((c + hash) ^ ((c + hash) << 10)) >> 6); } - return 0x8001 * ((9 * hash) ^ ((9 * hash) >> 11)); + uint32_t val = 0x8001 * ((9 * hash) ^ ((9 * hash) >> 11)); + + hashes::add_hash(val, str); + + return val; } + + uint32_t canon_hash_pattern(const char* str) + { + std::string_view v{ str }; + + // basic notations hash_123, var_123 + if (!v.rfind("hash_", 0)) return std::strtoul(&str[5], nullptr, 16) & 0xFFFFFFFF; + if (!v.rfind("var_", 0)) return std::strtoul(&str[4], nullptr, 16) & 0xFFFFFFFF; + + // unknown, use hashed value + return canon_hash(str); + } + void gsc_error(const char* message, game::scriptInstance_t inst, bool terminal, ...) { static char buffer[game::scriptInstance_t::SCRIPTINSTANCE_MAX][0x800]; @@ -32,11 +53,38 @@ namespace gsc_funcs vsprintf_s(buffer[inst], message, va); va_end(va); - game::ScrVm_Error(custom_error_id, inst, buffer[inst], terminal); + game::ScrVm_Error(game::runtime_errors::custom_error_id, inst, buffer[inst], terminal); } + const char* lookup_hash(game::scriptInstance_t inst, const char* type, uint64_t hash) + { + static char buffer[game::SCRIPTINSTANCE_MAX][0x50]; + const char* str = hashes::lookup(hash); + + if (str) + { + return str; + } + + sprintf_s(buffer[inst], "%s_%llx", type, hash); + + return buffer[inst]; + } + + void ScrVm_AddToArrayIntIndexed(game::scriptInstance_t inst, uint64_t index) + { + auto& vm = game::scrVmPub[inst]; + --vm.callNesting; + --vm.top; + game::ScrVarIndex_t varidx = game::ScrVar_NewVariableByIndex(inst, vm.top->u.pointerValue, index); + game::ScrVar_SetValue(inst, varidx, vm.top + 1); + } + + namespace { + constexpr auto gsc_json_data_name_max_length = 40; + constexpr const char* gsc_json_type = "$.type"; enum hud_elem_align_x { HUD_ALIGN_X_LEFT = 0, @@ -140,13 +188,16 @@ namespace gsc_funcs case game::TYPE_UNDEFINED: logger::write(logger::LOG_TYPE_INFO, "[ %s VM ] undefined", inst ? "CSC" : "GSC"); break; + case game::TYPE_POINTER: + logger::write(logger::LOG_TYPE_INFO, "[ %s VM ] Pointer[%s]", inst ? "CSC" : "GSC", game::var_typename[game::ScrVm_GetPointerType(inst, offset)]); + break; case game::TYPE_STRING: logger::write(logger::LOG_TYPE_INFO, "[ %s VM ] %s", inst ? "CSC" : "GSC", game::ScrVm_GetString(inst, offset)); break; case game::TYPE_HASH: { game::BO4_AssetRef_t hash{}; - logger::write(logger::LOG_TYPE_INFO, "[ %s VM ] %llx", inst ? "CSC" : "GSC", game::ScrVm_GetHash(&hash, inst, offset)->hash); + logger::write(logger::LOG_TYPE_INFO, "[ %s VM ] %llx", inst ? "CSC" : "GSC", lookup_hash(inst, "hash", game::ScrVm_GetHash(&hash, inst, offset)->hash)); } break; case game::TYPE_INTEGER: @@ -163,7 +214,7 @@ namespace gsc_funcs } break; default: - gsc_error("Call of ShieldLog with unknown type: %d", inst, false, type); + gsc_error("Call of ShieldLog with unknown type: %s", inst, false, game::var_typename[type]); break; } } @@ -476,9 +527,427 @@ namespace gsc_funcs } else { - gsc_error("compiler::function_%llx not implemented", inst, false, hash); + gsc_error("compiler::%s not implemented", inst, false, lookup_hash(inst, "function", hash)); } } + + void pre_cache_resource(game::scriptInstance_t inst) + { + game::BO4_AssetRef_t hashRef{}; + + byte type; + if (game::ScrVm_GetType(inst, 0) == game::TYPE_STRING) + { + type = xassets::BG_Cache_GetTypeIndex(game::ScrVm_GetString(inst, 0)); + } + else if (game::ScrVm_GetType(inst, 0) == game::TYPE_INTEGER) + { + type = (byte)(game::ScrVm_GetInt(inst, 0) & 0xFF); + } + else + { + gsc_error("bad param type for PreCache, excepted int or string, received %s", inst, false, game::var_typename[game::ScrVm_GetType(inst, 0)]); + return; + } + + if (!type || type >= xassets::BG_CACHE_TYPE_COUNT) + { + gsc_error("bad bgcache type for PreCache", inst, false); + return; + } + + uint64_t res = game::ScrVm_GetHash(&hashRef, inst, 1)->hash; + + logger::write(logger::LOG_TYPE_DEBUG, "precaching resource type=%d/name=hash_%llx", type, res); + + hashRef.hash = res; + hashRef.null = 0; + xassets::BG_Cache_RegisterAndGet((xassets::BGCacheTypes)type, &hashRef); + } + + void shield_to_json_val(game::scriptInstance_t inst, game::ScrVarValue_t* val, rapidjson::Value& member, rapidjson::Document& doc, int depth) + { + if (depth >= 10) + { + // avoid recursion + member.SetNull(); + return; + } + switch (val->type) + { + case game::TYPE_UNDEFINED: return; // ignore + case game::TYPE_FLOAT: + { + member.SetFloat(val->u.floatValue); + } + break; + case game::TYPE_INTEGER: + { + member.SetInt64(val->u.intValue); + } + break; + case game::TYPE_STRING: + { + member.SetString(game::ScrStr_ConvertToString(val->u.pointerValue), doc.GetAllocator()); + } + break; + case game::TYPE_VECTOR: + { + member.SetObject(); + auto obj = member.GetObj(); + obj.AddMember(rapidjson::StringRef(gsc_json_type), "vector", doc.GetAllocator()); + obj.AddMember(rapidjson::StringRef("x"), val->u.vectorValue[0], doc.GetAllocator()); + obj.AddMember(rapidjson::StringRef("y"), val->u.vectorValue[1], doc.GetAllocator()); + obj.AddMember(rapidjson::StringRef("z"), val->u.vectorValue[2], doc.GetAllocator()); + } + break; + case game::TYPE_HASH: + { + member.SetObject(); + auto obj = member.GetObj(); + obj.AddMember(rapidjson::StringRef(gsc_json_type), "hash", doc.GetAllocator()); + auto hash = val->u.intValue & 0x7FFFFFFFFFFFFFFF; + std::string name = std::format("hash_{:x}", hash); + obj.AddMember(rapidjson::StringRef("hash"), name, doc.GetAllocator()); + } + break; + case game::TYPE_POINTER: + { + game::ScrVarIndex_t ptr_id = val->u.pointerValue; + game::ScrVarValue_t& ptr_val = game::scrVarGlob[inst].scriptValues[ptr_id]; + + member.SetObject(); + auto obj = member.GetObj(); + + + if (ptr_val.type == game::TYPE_ARRAY) + { + obj.AddMember(rapidjson::StringRef(gsc_json_type), "array", doc.GetAllocator()); + + auto size = game::scrVarGlob[inst].scriptVariablesObjectInfo1[ptr_id].size; + if (size) + { + game::ScrVar_t* var = &game::scrVarGlob[inst].scriptVariables[ptr_val.u.pointerValue]; + game::ScrVarValue_t* value = &game::scrVarGlob[inst].scriptValues[ptr_val.u.pointerValue]; + + while (var) + { + rapidjson::Value subval{}; + + // read struct value + shield_to_json_val(inst, value, subval, doc, depth + 1); + + if (var->_anon_0.nameType == 1) // integer index + { + std::string keyval = std::format("{}", var->nameIndex); + rapidjson::Value keyjson{ rapidjson::kStringType }; + keyjson.SetString(keyval, doc.GetAllocator()); + obj.AddMember(keyjson, subval, doc.GetAllocator()); + } + else + { + std::string keyval = std::format("#var_{:x}", var->nameIndex); + rapidjson::Value keyjson{ rapidjson::kStringType }; + keyjson.SetString(keyval, doc.GetAllocator()); + obj.AddMember(keyjson, subval, doc.GetAllocator()); + } + + if (!var->nextSibling) + { + break; + } + + value = &game::scrVarGlob[inst].scriptValues[var->nextSibling]; + var = &game::scrVarGlob[inst].scriptVariables[var->nextSibling]; + } + } + return; + } + + if (ptr_val.type == game::TYPE_STRUCT) + { + auto size = game::scrVarGlob[inst].scriptVariablesObjectInfo1[ptr_id].size; + if (size) + { + game::ScrVar_t* var = &game::scrVarGlob[inst].scriptVariables[ptr_val.u.pointerValue]; + game::ScrVarValue_t* value = &game::scrVarGlob[inst].scriptValues[ptr_val.u.pointerValue]; + + while (var) + { + rapidjson::Value subval{}; + + // read struct value + shield_to_json_val(inst, value, subval, doc, depth + 1); + + std::string keyval = std::format("var_{:x}", var->nameIndex); + rapidjson::Value keyjson{ rapidjson::kStringType }; + keyjson.SetString(keyval, doc.GetAllocator()); + obj.AddMember(keyjson, subval, doc.GetAllocator()); + + if (!var->nextSibling) + { + break; + } + + value = &game::scrVarGlob[inst].scriptValues[var->nextSibling]; + var = &game::scrVarGlob[inst].scriptVariables[var->nextSibling]; + } + } + return; + } + // shared_struct and entity aren't using the same syntax + gsc_error("invalid tojson param pointer type: %s", inst, false, game::var_typename[ptr_val.type]); + return; + } + break; + default: + { + gsc_error("invalid tojson param type: %s", inst, false, game::var_typename[val->type]); + return; + } + } + } + + void shield_to_json(game::scriptInstance_t inst) + { + const char* fileid = game::ScrVm_GetString(inst, 0); + std::string_view v{ fileid }; + + if (v.rfind("/", 0) != std::string::npos || v.rfind("\\", 0) != std::string::npos) + { + gsc_error("can't save json containing '/' or '\\'", inst, false); + return; + } + + if (v.length() > gsc_json_data_name_max_length) + { + gsc_error("json name can't be longer than %d", inst, false, gsc_json_data_name_max_length); + return; + } + + std::string file{ std::format("project-bo4/saved/{}/{}.json", (inst ? "client" : "server"), fileid) }; + + if (inst) + { + std::filesystem::create_directories("project-bo4/saved/client"); + } + else + { + std::filesystem::create_directories("project-bo4/saved/server"); + } + + game::ScrVarValue_t* val = &game::scrVmPub[inst].top[-1]; + + if (game::ScrVm_GetNumParam(inst) == 1 || val->type == game::TYPE_UNDEFINED) + { + std::filesystem::remove(file); + return; + } + + rapidjson::Document doc{}; + + shield_to_json_val(inst, val, doc, doc, 0); + + rapidjson::StringBuffer buffer; + rapidjson::PrettyWriter writer(buffer); + doc.Accept(writer); + + std::string json_data(buffer.GetString(), buffer.GetLength()); + utilities::io::write_file(file, json_data); + } + + void shield_from_json_push_struct(game::scriptInstance_t inst, rapidjson::Value& member) + { + if (member.IsString()) + { + game::ScrVm_AddString(inst, member.GetString()); + return; + } + + if (member.IsFloat()) + { + game::ScrVm_AddFloat(inst, member.GetFloat()); + return; + } + + if (member.IsNumber()) + { + game::ScrVm_AddInt(inst, member.GetInt64()); + return; + } + + if (member.IsBool()) + { + game::ScrVm_AddBool(inst, member.GetBool()); + return; + } + + if (member.IsNull()) + { + logger::write(logger::LOG_TYPE_WARN, "ShieldFromJson: read null"); + game::ScrVm_AddUndefined(inst); + return; + } + + if (member.IsArray()) + { + auto arr = member.GetArray(); + + game::ScrVar_PushArray(inst); + + for (rapidjson::Value& elem : arr) + { + shield_from_json_push_struct(inst, elem); + game::ScrVm_AddToArray(inst); + } + return; + } + + if (member.IsObject()) + { + auto obj = member.GetObj(); + auto typefield = obj.FindMember(gsc_json_type); + + if (typefield != obj.MemberEnd() && typefield->value.IsString()) + { + const char* type = typefield->value.GetString(); + + if (!_strcmpi(type, "array")) + { + game::ScrVar_PushArray(inst); + + game::BO4_AssetRef_t name{}; + + for (auto& [key, elem] : obj) + { + if (!key.IsString()) + { + gsc_error("read bad key value for array", inst, false); + return; + } + if (!_strcmpi(key.GetString(), gsc_json_type)) + { + continue; + } + const char* keystr = key.GetString(); + if (!*keystr) + { + continue; + } + shield_from_json_push_struct(inst, elem); + + if (*keystr == '#') + { + name.hash = fnv1a::generate_hash_pattern(key.GetString() + 1); + game::ScrVm_AddToArrayStringIndexed(inst, &name); + } + else + { + uint64_t keyval{}; + try + { + keyval = std::strtoull(keystr, nullptr, 10); + } + catch (const std::invalid_argument& e) + { + gsc_error("invalid key for array member %s", inst, false, e.what()); + return; + } + ScrVm_AddToArrayIntIndexed(inst, keyval); + } + } + return; + } + + if (!_strcmpi(type, "vector")) + { + auto x = obj.FindMember("x"); + auto y = obj.FindMember("y"); + auto z = obj.FindMember("z"); + + if ( + x != obj.MemberEnd() && y != obj.MemberEnd() && z != obj.MemberEnd() + && x->value.IsNumber() && y->value.IsNumber() && z->value.IsNumber()) + { + game::vec3_t vec{}; + + vec[0] = x->value.GetFloat(); + vec[1] = y->value.GetFloat(); + vec[2] = z->value.GetFloat(); + game::ScrVm_AddVector(inst, &vec); + return; + } + } + if (!_strcmpi(type, "hash")) + { + auto value = obj.FindMember("hash"); + + if (value != obj.MemberEnd() && value->value.IsString()) { + game::BO4_AssetRef_t hash + { + .hash = (int64_t)fnv1a::generate_hash_pattern(value->value.GetString()) + }; + + game::ScrVm_AddHash(inst, &hash); + return; + } + } + } + + // object by default + uint32_t struct_id = game::ScrVm_AddStruct(inst); + + for (auto& [key, elem] : obj) + { + if (!key.IsString()) + { + gsc_error("read bad key value for struct", inst, false); + return; + } + if (!_strcmpi(key.GetString(), gsc_json_type)) + { + continue; + } + shield_from_json_push_struct(inst, elem); + game::ScrVm_SetStructField(inst, struct_id, canon_hash_pattern(key.GetString())); + } + return; + } + gsc_error("bad json element: %d", inst, false, (int)member.GetType()); + } + + void shield_from_json(game::scriptInstance_t inst) + { + const char* fileid = game::ScrVm_GetString(inst, 0); + std::string_view v{ fileid }; + + if (v.rfind("/", 0) != std::string::npos || v.rfind("\\", 0) != std::string::npos) + { + gsc_error("can't save json containing '/' or '\\'", inst, false); + return; + } + + if (v.length() > gsc_json_data_name_max_length) + { + gsc_error("json name can't be longer than %d", inst, false, gsc_json_data_name_max_length); + return; + } + + std::string file{ std::format("project-bo4/saved/{}/{}.json", (inst ? "client" : "server"), fileid) }; + + std::string file_content{}; + + if (!utilities::io::read_file(file, &file_content)) + { + logger::write(logger::LOG_TYPE_WARN, "trying to read unknown config file %s", file.c_str()); + return; + } + + rapidjson::Document doc{}; + doc.Parse(file_content); + + shield_from_json_push_struct(inst, doc); + } game::BO4_BuiltinFunctionDef custom_functions_gsc[] = { @@ -551,6 +1020,34 @@ namespace gsc_funcs .max_args = 255, .actionFunc = serious_custom_func, .type = 0, + }, + { // PreCache(type, name) + .canonId = canon_hash("PreCache"), + .min_args = 2, + .max_args = 2, + .actionFunc = pre_cache_resource, + .type = 0, + }, + { // ShieldFromJson(name)->object + .canonId = canon_hash("ShieldFromJson"), + .min_args = 1, + .max_args = 1, + .actionFunc = shield_from_json, + .type = 0 + }, + { // ShieldFromJson(name) + .canonId = canon_hash("ShieldRemoveJson"), + .min_args = 1, + .max_args = 1, + .actionFunc = shield_to_json, + .type = 0 + }, + {// ShieldFromJson(name, object = undefined) + .canonId = canon_hash("ShieldToJson"), + .min_args = 1, + .max_args = 2, + .actionFunc = shield_to_json, + .type = 0 } }; game::BO4_BuiltinFunctionDef custom_functions_csc[] = @@ -624,6 +1121,27 @@ namespace gsc_funcs .max_args = 255, .actionFunc = serious_custom_func, .type = 0, + }, + { // ShieldFromJson(name)->object + .canonId = canon_hash("ShieldFromJson"), + .min_args = 1, + .max_args = 1, + .actionFunc = shield_from_json, + .type = 0 + }, + { // ShieldFromJson(name) + .canonId = canon_hash("ShieldRemoveJson"), + .min_args = 1, + .max_args = 1, + .actionFunc = shield_to_json, + .type = 0 + }, + {// ShieldToJson(name, object = undefined) + .canonId = canon_hash("ShieldToJson"), + .min_args = 1, + .max_args = 2, + .actionFunc = shield_to_json, + .type = 0 } }; @@ -811,10 +1329,14 @@ namespace gsc_funcs break; default: // put custom message for our id - if (code == custom_error_id) + if (code == game::runtime_errors::custom_error_id) { game::scrVarPub[inst].error_message = unused; } + else + { + game::scrVarPub[inst].error_message = game::runtime_errors::get_error_message(code); + } break; } @@ -843,6 +1365,100 @@ namespace gsc_funcs logger::write(logger::LOG_TYPE_ERROR, str); } + void patch_scrvm_runtime_error() + { + const auto scrvm_runtimeerror = 0x1427775B0_g; + void* stub = utilities::hook::assemble([scrvm_runtimeerror](utilities::hook::assembler& a) + { + a.mov(ebx, 0x20); // set errorcode to 0x20 + a.mov(al, 0); // set ZF to avoid the ebx overwrite + + a.jmp(scrvm_runtimeerror + 0x3DC); + } + ); + + utilities::hook::jump(scrvm_runtimeerror + 0x3A0, stub); + } + + void get_gsc_export_info(game::scriptInstance_t inst, byte* codepos, const char** scriptname, int32_t* sloc, int32_t* crc, int32_t* vm) + { + static char scriptnamebuffer[game::scriptInstance_t::SCRIPTINSTANCE_MAX][0x200]; + game::GSC_OBJ* script_obj = nullptr; + { + game::scoped_critical_section scs{ 0x36, game::SCOPED_CRITSECT_NORMAL }; + + uint32_t count = game::gObjFileInfoCount[inst]; + + for (size_t i = 0; i < count; i++) + { + game::objFileInfo_t& info = (*game::gObjFileInfo)[inst][i]; + + game::GSC_OBJ* obj = info.activeVersion; + + if (codepos >= obj->magic + obj->start_data && codepos < obj->magic + obj->start_data + obj->data_length) + { + script_obj = obj; + break; + } + } + } + + if (script_obj) + { + game::GSC_EXPORT_ITEM* export_item = nullptr; + + uint32_t rloc = (uint32_t)(codepos - script_obj->magic); + + for (size_t i = 0; i < script_obj->exports_count; i++) + { + game::GSC_EXPORT_ITEM* exp = reinterpret_cast(script_obj->magic + script_obj->exports_offset) + i; + + if (rloc < exp->address) + { + continue; // our code is after + } + + if (export_item && export_item->address > exp->address) + { + continue; // we already have a better candidate + } + + export_item = exp; + } + + + if (scriptname) + { + if (export_item) + { + std::string script_name = lookup_hash(inst, "script", script_obj->name & 0x7FFFFFFFFFFFFFFF); + + sprintf_s(scriptnamebuffer[inst], "%s::%s@%x", script_name.c_str(), lookup_hash(inst, "function", export_item->name), rloc - export_item->address); + } else + { + sprintf_s(scriptnamebuffer[inst], "%s", lookup_hash(inst, "script", script_obj->name & 0x7FFFFFFFFFFFFFFF)); + } + + *scriptname = scriptnamebuffer[inst]; + } + + if (sloc) + { + *sloc = rloc; + } + + if (crc) + { + *crc = script_obj->crc; + } + + if (vm) + { + *vm = script_obj->magic[7]; + } + } + } + class component final : public component_interface { public: @@ -861,6 +1477,10 @@ namespace gsc_funcs // log gsc errors scrvm_error.create(0x142770330_g, scrvm_error_stub); utilities::hook::jump(0x142890470_g, scrvm_log_compiler_error); + + // better runtime error + utilities::hook::jump(0x142748550_g, get_gsc_export_info); + patch_scrvm_runtime_error(); scheduler::loop(draw_hud, scheduler::renderer); } diff --git a/source/proxy-dll/component/gsc_funcs.hpp b/source/proxy-dll/component/gsc_funcs.hpp index 0cb592b..91d4b97 100644 --- a/source/proxy-dll/component/gsc_funcs.hpp +++ b/source/proxy-dll/component/gsc_funcs.hpp @@ -9,8 +9,10 @@ namespace gsc_funcs extern bool enable_dev_func; uint32_t canon_hash(const char* str); - - constexpr uint64_t custom_error_id = 0x42693201; - + uint32_t canon_hash_pattern(const char* str); + void gsc_error(const char* message, game::scriptInstance_t inst, bool terminal, ...); + const char* lookup_hash(game::scriptInstance_t inst, const char* type, uint64_t hash); + + void ScrVm_AddToArrayIntIndexed(game::scriptInstance_t inst, uint64_t index); } \ No newline at end of file diff --git a/source/proxy-dll/component/hashes.cpp b/source/proxy-dll/component/hashes.cpp new file mode 100644 index 0000000..23b4319 --- /dev/null +++ b/source/proxy-dll/component/hashes.cpp @@ -0,0 +1,256 @@ +#include +#include "hashes.hpp" +#include "gsc_funcs.hpp" + +#include "definitions/variables.hpp" +#include "loader/component_loader.hpp" +#include + +namespace hashes +{ + namespace + { + bool enabled = true; + std::unordered_map& hash_storage() + { + static std::unordered_map storage{}; + return storage; + } + + + + const char* hff_names[] + { + "COMMON", + "STRING", + "SERIOUS_COMPILER" + }; + + bool is_valid_h32(const char* str) + { + const char* s = str; + + while (*s) + { + char c = *(s++); + + if (!( + (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '_')) + { + return false; + } + } + return true; // [A-Za-z0-9_]+ + } + } + + const char* lookup(uint64_t hash) + { + auto& hs = hash_storage(); + auto it = hs.find(hash); + if (it == hs.end()) + { + return nullptr; + } + return it->second.c_str(); + } + + const char* lookup_tmp(const char* type, uint64_t hash) + { + static char tmp_buffer[0x50]; + + const char* val = lookup(hash); + + if (val) + { + return val; + } + + sprintf_s(tmp_buffer, "%s_%llx", type, hash); + + return tmp_buffer; + } + + void add_hash(uint64_t hash, const char* value) + { + if (!enabled) + { + return; + } + hash_storage()[hash] = value; + } + + const char* get_format_name(hashes_file_format format) + { + if (format >= 0 && format < HFF_COUNT) + { + return hff_names[format]; + } + return ""; + } + + hashes_file_format get_format_idx(const char* name) + { + for (size_t i = 0; i < HFF_COUNT; i++) + { + if (!_strcmpi(name, hff_names[i])) + { + return (hashes_file_format)i; + } + } + return HFF_COUNT; + } + + bool load_file(std::filesystem::path& file, hashes_file_format format) + { + if (!enabled) + { + return true; + } + + logger::write(logger::LOG_TYPE_DEBUG, std::format("loading hash file {}", file.string())); + switch (format) + { + case HFF_STRING: + { + // basic format, each line is a string, allows fast updates + + std::ifstream s(file); + + if (!s) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("can't read hash file {}", file.string())); + return false; // nothing to read + } + + std::string line; + while (s.good() && std::getline(s, line)) + { + const char* str = line.c_str(); + + if (is_valid_h32(str)) + { + add_hash(gsc_funcs::canon_hash(str), str); + } + + add_hash(fnv1a::generate_hash(str), str); + } + + s.close(); + return true; + } + case HFF_COMMON: + { + // common precomputed format used by greyhound index and other tools + // each line: , + + std::ifstream s(file); + + if (!s) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("can't read hash file {}", file.string())); + return false; // nothing to read + } + + std::string line; + while (s.good() && std::getline(s, line)) + { + auto idx = line.rfind(", ", 18); + + if (idx == std::string::npos) + { + continue; // bad line + } + + char value[18] = {}; + const char* str = line.c_str(); + + memcpy(value, str, idx); + + add_hash(std::strtoull(value, nullptr, 16), str + idx + 1); + } + + s.close(); + return false; + } + case HFF_SERIOUS_COMPILER: + { + // precomputed format generated by the serious compiler + // each line: 0x, + + std::ifstream s(file); + + if (!s) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("can't read hash file {}", file.string())); + return false; // nothing to read + } + + std::string line; + while (s.good() && std::getline(s, line)) + { + if (!line.starts_with("0x")) + { + continue; // bad line + } + + auto idx = line.rfind(", ", 18); + + if (idx == std::string::npos) + { + continue; // bad line + } + + char value[18] = {}; + const char* str = line.c_str(); + + memcpy(value, str + 2, idx - 2); + + add_hash(std::strtoull(value, nullptr, 16), str + idx + 2); + } + + s.close(); + + return true; + } + default: + logger::write(logger::LOG_TYPE_ERROR, std::format("bad format type {} for file {}", (int)format, file.string())); + return false; + } + } + + + class component final : public component_interface + { + public: + void pre_start() override + { + enabled = utilities::json_config::ReadBoolean("gsc", "hashes", true); + + if (!enabled) + { + return; + } + + // load default files + std::filesystem::path default_file_name_str = "project-bo4/strings.txt"; + + if (std::filesystem::exists(default_file_name_str)) + { + load_file(default_file_name_str, HFF_STRING); + } + + std::filesystem::path default_file_name_common = "project-bo4/hashes.csv"; + + if (std::filesystem::exists(default_file_name_common)) + { + load_file(default_file_name_common, HFF_COMMON); + } + } + }; +} + +REGISTER_COMPONENT(hashes::component) \ No newline at end of file diff --git a/source/proxy-dll/component/hashes.hpp b/source/proxy-dll/component/hashes.hpp new file mode 100644 index 0000000..794acbc --- /dev/null +++ b/source/proxy-dll/component/hashes.hpp @@ -0,0 +1,18 @@ +#pragma once + +namespace hashes +{ + enum hashes_file_format + { + HFF_COMMON = 0, + HFF_STRING, + HFF_SERIOUS_COMPILER, + HFF_COUNT + }; + const char* lookup(uint64_t hash); + const char* lookup_tmp(const char* type, uint64_t hash); + void add_hash(uint64_t hash, const char* value); + const char* get_format_name(hashes_file_format format); + hashes_file_format get_format_idx(const char* name); + bool load_file(std::filesystem::path& file, hashes_file_format format); +} \ No newline at end of file diff --git a/source/proxy-dll/component/lua.cpp b/source/proxy-dll/component/lua.cpp new file mode 100644 index 0000000..39031b6 --- /dev/null +++ b/source/proxy-dll/component/lua.cpp @@ -0,0 +1,211 @@ +#include +#include "definitions/game.hpp" +#include "loader/component_loader.hpp" + +#include +#include + +namespace lua { + typedef void (*HksLogFunc)(game::lua_state* s, const char* fmt, ...); + + utilities::hook::detour lual_error_hook; + utilities::hook::detour hksi_lual_error_hook; + + void lual_error_stub(game::lua_state* state, const char* fmt, ...) + { + va_list va; + va_start(va, fmt); + char buffer[0x800]; + + vsprintf_s(buffer, fmt, va); + va_end(va); + + logger::write(logger::LOG_TYPE_ERROR, std::format("[lual_error] {}", buffer)); + lual_error_hook.invoke(state, "%s", buffer); + } + + void hksi_lual_error_stub(game::lua_state* state, const char* fmt, ...) + { + va_list va; + va_start(va, fmt); + char buffer[0x800]; + + vsprintf_s(buffer, fmt, va); + va_end(va); + + logger::write(logger::LOG_TYPE_ERROR, std::format("[hksi_lual_error] {}", buffer)); + hksi_lual_error_hook.invoke(state, "%s", buffer); + } + + void print_out(logger::type type, game::consoleLabel_e label, const char* info) + { + size_t len = std::strlen(info); + + while (len && info[len - 1] == '\n') + { + len--; + } + + std::string buff{ info, len }; + + if (label != game::CON_LABEL_TEMP) + { + const char* label_str; + + if (label > 0 && label < game::CON_LABEL_COUNT) + { + label_str = game::builtinLabels[label]; + } + else + { + label_str = "INVALID"; + } + + logger::write(type, std::format("[lua] {} - {}", label_str, buff)); + + } + else + { + // no label + logger::write(type, std::format("[lua] {}", buff)); + } + } + + void lua_engine_print(logger::type type, game::lua_state* s) + { + game::hks_object* base = s->m_apistack.base; + game::hks_object* top = s->m_apistack.top; + + game::consoleLabel_e label = game::CON_LABEL_TEMP; + const char* info = ""; + + if (base < top) + { + label = (game::consoleLabel_e)game::hks_obj_tonumber(s, base); + } + if (base + 1 < top) + { + info = game::hks_obj_tolstring(s, base + 1, nullptr); + } + + print_out(type, label, info); + } + + void lua_engine_print_info(game::lua_state* s) + { + lua_engine_print(logger::LOG_TYPE_INFO, s); + } + + void lua_engine_print_warning(game::lua_state* s) + { + lua_engine_print(logger::LOG_TYPE_WARN, s); + } + + void lua_engine_print_error(game::lua_state* s) + { + lua_engine_print(logger::LOG_TYPE_ERROR, s); + } + + int lua_unsafe_function_stub(game::lua_state* state) + { + static std::once_flag f{}; + + std::call_once(f, []() { logger::write(logger::LOG_TYPE_ERROR, "calling of a disabled lua unsafe method, these methods can enabled in the config file."); }); + return 0; + } + + void patch_unsafe_lua_functions() + { + if (utilities::json_config::ReadBoolean("lua", "allow_unsafe_function", false)) + { + return; + } + + // in boiii, need to be added, not in bo4? + // - 0x141FD3220_g engine_openurl + // - 0x141D34190_g debug + + utilities::hook::jump(0x1437358D0_g, lua_unsafe_function_stub); // base_loadfile + utilities::hook::jump(0x143736A50_g, lua_unsafe_function_stub); // base_load + utilities::hook::jump(0x14373B640_g, lua_unsafe_function_stub); // string_dump + + + // io helpers + utilities::hook::jump(0x14373F000_g, lua_unsafe_function_stub); // os_exit + utilities::hook::jump(0x14373F020_g, lua_unsafe_function_stub); // os_remove + utilities::hook::jump(0x14373F150_g, lua_unsafe_function_stub); // os_rename + utilities::hook::jump(0x14373EEC0_g, lua_unsafe_function_stub); // os_tmpname + utilities::hook::jump(0x14373EE90_g, lua_unsafe_function_stub); // os_sleep + utilities::hook::jump(0x14373ED30_g, lua_unsafe_function_stub); // os_execute + utilities::hook::jump(0x14373ED90_g, lua_unsafe_function_stub); // os_getenv + + utilities::hook::jump(0x14373D170_g, lua_unsafe_function_stub); // io_close + utilities::hook::jump(0x14373D060_g, lua_unsafe_function_stub); // io_close_file + utilities::hook::jump(0x14373D030_g, lua_unsafe_function_stub); // io_flush + utilities::hook::jump(0x14375C7B0_g, lua_unsafe_function_stub); // io_output/io_input + utilities::hook::jump(0x14373D7B0_g, lua_unsafe_function_stub); // io_lines + utilities::hook::jump(0x14373D670_g, lua_unsafe_function_stub); // io_lines + utilities::hook::jump(0x14373DD20_g, lua_unsafe_function_stub); // io_open + utilities::hook::jump(0x14373D5B0_g, lua_unsafe_function_stub); // io_read + utilities::hook::jump(0x14373D200_g, lua_unsafe_function_stub); // io_read_file + utilities::hook::jump(0x14373DA90_g, lua_unsafe_function_stub); // io_type + utilities::hook::jump(0x14373CFB0_g, lua_unsafe_function_stub); // io_write + utilities::hook::jump(0x14373CF70_g, lua_unsafe_function_stub); // io_write_file + utilities::hook::jump(0x14373DC60_g, lua_unsafe_function_stub); // io_tmpfile + utilities::hook::jump(0x14373E160_g, lua_unsafe_function_stub); // io_popen + utilities::hook::jump(0x14373DE40_g, lua_unsafe_function_stub); // io_seek_file + utilities::hook::jump(0x14373DF80_g, lua_unsafe_function_stub); // io_setvbuf + utilities::hook::jump(0x14373E0A0_g, lua_unsafe_function_stub); // io_tostring + utilities::hook::jump(0x143735360_g, lua_unsafe_function_stub); // serialize_persist + utilities::hook::jump(0x143735590_g, lua_unsafe_function_stub); // serialize_unpersist + + utilities::hook::jump(0x14373BC40_g, lua_unsafe_function_stub); // havokscript_compiler_settings + utilities::hook::jump(0x14373C670_g, lua_unsafe_function_stub); // havokscript_getgcweights + utilities::hook::jump(0x14373C100_g, lua_unsafe_function_stub); // havokscript_setgcweights + utilities::hook::jump(0x143734DC0_g, lua_unsafe_function_stub); // package_loadlib + + } + + int lui_panic_stub(game::lua_state* vm) + { + game::hks_object* arg0 = vm->m_apistack.top - 1; + + const char* error_message = ""; + if (arg0 >= vm->m_apistack.base) + { + error_message = game::hks_obj_tolstring(vm, arg0, nullptr); + + logger::write(logger::LOG_TYPE_ERROR, std::format("[lui_panic] {}", error_message)); + } + else { + logger::write(logger::LOG_TYPE_ERROR, std::format("[lui_panic] empty")); + } + + game::Lua_CoD_LuaStateManager_Error(100007, error_message, vm); + return 0; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + if (utilities::json_config::ReadBoolean("lua", "info", false)) + { + utilities::hook::jump(0x143956010_g, lua_engine_print_info); + + } + if (utilities::json_config::ReadBoolean("lua", "error", false)) + { + hksi_lual_error_hook.create(0x143757780_g, hksi_lual_error_stub); + lual_error_hook.create(0x14375D410_g, lual_error_stub); + utilities::hook::jump(0x14423A1D0_g, lui_panic_stub); + utilities::hook::jump(0x143955FA0_g, lua_engine_print_error); + utilities::hook::jump(0x143956090_g, lua_engine_print_warning); + } + + patch_unsafe_lua_functions(); + } + }; +} +REGISTER_COMPONENT(lua::component) \ No newline at end of file diff --git a/source/proxy-dll/component/mods.cpp b/source/proxy-dll/component/mods.cpp index 351d7b7..9959e89 100644 --- a/source/proxy-dll/component/mods.cpp +++ b/source/proxy-dll/component/mods.cpp @@ -6,8 +6,10 @@ #include "gsc_funcs.hpp" #include "gsc_custom.hpp" -#include "loader/component_loader.hpp" +#include "dvars.hpp" +#include "hashes.hpp" +#include "loader/component_loader.hpp" #include #include @@ -148,6 +150,9 @@ namespace mods { { xassets::lua_file_header header{}; + std::unordered_set hooks{}; + uint64_t noext_name{}; + std::unordered_set hooks_post{}; std::string data{}; auto* get_header() @@ -172,6 +177,28 @@ namespace mods { return &header; } }; + struct localize + { + xassets::localize_entry_header header{}; + + std::string str{}; + + auto* get_header() + { + header.string = str.data(); + + return &header; + } + + }; + struct cache_entry + { + game::BO4_AssetRef_t name{}; + xassets::BGCacheTypes type{}; + std::unordered_set hooks_modes{}; + std::unordered_set hooks_map{}; + std::unordered_set hooks_gametype{}; + }; class mod_storage @@ -183,6 +210,18 @@ namespace mods { std::vector raw_files{}; std::vector lua_files{}; std::vector csv_files{}; + std::vector localizes{}; + std::vector cache_entries{}; + std::unordered_map assets_redirects[xassets::ASSET_TYPE_COUNT] = {}; + std::vector custom_cache_entries{}; + + xassets::bg_cache_info custom_cache + { + .name + { + .hash = (int64_t)fnv1a::generate_hash("shield_cache") // 2c4f76fcf5cfbebd + } + }; ~mod_storage() { @@ -199,6 +238,13 @@ namespace mods { gsc_files.clear(); lua_files.clear(); csv_files.clear(); + localizes.clear(); + cache_entries.clear(); + + for (auto& redirect : assets_redirects) + { + redirect.clear(); + } for (char* str : allocated_strings) { @@ -217,6 +263,54 @@ namespace mods { return str; } + void sync_cache_entries() + { + std::lock_guard lg{ load_mutex }; + custom_cache.defCount = 0; + custom_cache.def = nullptr; + custom_cache_entries.clear(); + + if (!cache_entries.size()) + { + return; // nothing to sync + } + + game::dvar_t* sv_mapname = dvars::find_dvar("sv_mapname"); + game::dvar_t* g_gametype = dvars::find_dvar("g_gametype"); + + if (!sv_mapname || !g_gametype) + { + logger::write(logger::LOG_TYPE_ERROR, "Can't find bgcache dvars"); + return; + } + + std::string mapname = dvars::get_value_string(sv_mapname, &sv_mapname->value->current); + std::string gametype = dvars::get_value_string(g_gametype, &g_gametype->value->current); + game::eModes mode = game::Com_SessionMode_GetMode(); + + uint64_t mapname_hash = fnv1a::generate_hash(mapname.data()); + uint64_t gametype_hash = fnv1a::generate_hash(gametype.data()); + + int count = 0; + for (auto& entry : cache_entries) + { + if ( + entry.hooks_modes.find(mode) != entry.hooks_modes.end() + || entry.hooks_map.find(mapname_hash) != entry.hooks_map.end() + || entry.hooks_gametype.find(gametype_hash) != entry.hooks_gametype.end() + ) + { + auto& ref = custom_cache_entries.emplace_back(); + ref.type = entry.type; + ref.name.hash = entry.name.hash; + count++; + } + } + custom_cache.def = custom_cache_entries.data(); + custom_cache.defCount = (int)custom_cache_entries.size(); + logger::write(logger::LOG_TYPE_DEBUG, "sync %d custom bgcache entries", count); + } + void* get_xasset(xassets::XAssetType type, uint64_t name) { std::lock_guard lg{ load_mutex }; @@ -254,6 +348,14 @@ namespace mods { return it->get_header(); } + case xassets::ASSET_TYPE_LOCALIZE_ENTRY: + { + auto it = std::find_if(localizes.begin(), localizes.end(), [name](const localize& file) { return file.header.name == name; }); + + if (it == localizes.end()) return nullptr; + + return it->get_header(); + } default: return nullptr; // unknown resource type } @@ -391,6 +493,27 @@ namespace mods { logger::write(logger::LOG_TYPE_DEBUG, std::format("mod {}: loaded raw file {} -> {:x}", mod_name, raw_file_path.string(), tmp.header.name)); raw_files.emplace_back(tmp); } + else if (!_strcmpi("localizeentry", type_val)) + { + auto name_mb = member.FindMember("name"); + auto value_mb = member.FindMember("value"); + + if ( + name_mb == member.MemberEnd() || value_mb == member.MemberEnd() + || !name_mb->value.IsString() || !value_mb->value.IsString() + ) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a bad localized entry def, missing/bad name or value", mod_name)); + return false; + } + + localize tmp{}; + tmp.str = value_mb->value.GetString(); + tmp.header.name = fnv1a::generate_hash_pattern(name_mb->value.GetString()); + + logger::write(logger::LOG_TYPE_DEBUG, std::format("mod {}: loaded localized entry {:x}", mod_name, tmp.header.name)); + localizes.emplace_back(tmp); + } else if (!_strcmpi("luafile", type_val)) { auto name_mb = member.FindMember("name"); @@ -408,7 +531,55 @@ namespace mods { lua_file tmp{}; std::filesystem::path path_cfg = path_mb->value.GetString(); auto lua_file_path = path_cfg.is_absolute() ? path_cfg : (mod_path / path_cfg); - tmp.header.name = fnv1a::generate_hash_pattern(name_mb->value.GetString()); + // it injects the name without the .lua and load the name with the .lua, good luck to replace with an unknown hash! + tmp.noext_name = fnv1a::generate_hash_pattern(name_mb->value.GetString()); + tmp.header.name = fnv1a::generate_hash(".lua", tmp.noext_name); + + auto hooks = member.FindMember("hooks_pre"); + + if (hooks != member.MemberEnd()) + { + // no hooks might not be an error, to replace a script for example + + if (!hooks->value.IsArray()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a bad luafile pre hook def, not an array for {}", mod_name, lua_file_path.string())); + return false; + } + + for (auto& hook : hooks->value.GetArray()) + { + if (!hook.IsString()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a bad luafile pre hook def, not a string for {}", mod_name, lua_file_path.string())); + return false; + } + + tmp.hooks.insert(fnv1a::generate_hash_pattern(hook.GetString())); + } + } + + auto hooks_post = member.FindMember("hooks_post"); + + if (hooks_post != member.MemberEnd()) + { + if (!hooks_post->value.IsArray()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a bad luafile post hook def, not an array for {}", mod_name, lua_file_path.string())); + return false; + } + + for (auto& hook : hooks_post->value.GetArray()) + { + if (!hook.IsString()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a bad luafile post hook def, not a string for {}", mod_name, lua_file_path.string())); + return false; + } + + tmp.hooks_post.insert(fnv1a::generate_hash_pattern(hook.GetString())); + } + } if (!utilities::io::read_file(lua_file_path.string(), &tmp.data)) { @@ -416,7 +587,7 @@ namespace mods { return false; } - logger::write(logger::LOG_TYPE_DEBUG, std::format("mod {}: loaded lua file {} -> {:x}", mod_name, lua_file_path.string(), tmp.header.name)); + logger::write(logger::LOG_TYPE_DEBUG, std::format("mod {}: loaded lua file {} -> x64:{:x}.lua ({:x})", mod_name, lua_file_path.string(), tmp.noext_name, tmp.header.name)); lua_files.emplace_back(tmp); } else if (!_strcmpi("stringtable", type_val)) @@ -448,7 +619,29 @@ namespace mods { std::stringstream stream{ tmp.data }; - doc.Load(stream, rapidcsv::LabelParams(-1, -1)); + auto separator_mb = member.FindMember("separator"); + + char sep = ','; + + if (separator_mb != member.MemberEnd()) + { + if (!separator_mb->value.IsString()) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("bad separator type for stringtable file {} for mod {}", stringtable_file_path.string(), mod_name)); + return false; + } + const char* sepval = separator_mb->value.GetString(); + + if (!sepval[0] || sepval[1]) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("bad separator for stringtable file {} for mod {}, a separator should contain only one character", stringtable_file_path.string(), mod_name)); + return false; + } + + sep = *sepval; + } + + doc.Load(stream, rapidcsv::LabelParams(-1, -1), rapidcsv::SeparatorParams(sep)); size_t rows_count_tmp = doc.GetRowCount(); tmp.header.rows_count = rows_count_tmp != 0 ? (int32_t)(rows_count_tmp - 1) : 0; @@ -556,9 +749,33 @@ namespace mods { } } - logger::write(logger::LOG_TYPE_DEBUG, std::format("mod {}: loaded stringtable file {} -> {:x}", mod_name, stringtable_file_path.string(), tmp.header.name)); + logger::write(logger::LOG_TYPE_DEBUG, std::format("mod {}: loaded stringtable file {} -> {:x} ({}x{})", mod_name, stringtable_file_path.string(), tmp.header.name, tmp.header.columns_count, tmp.header.rows_count)); csv_files.emplace_back(tmp); } + else if (!_strcmpi("hashes", type_val)) + { + auto path_mb = member.FindMember("path"); + + if (path_mb == member.MemberEnd() || !path_mb->value.IsString()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a hashes storage without a path", mod_name)); + return false; + } + + auto format_mb = member.FindMember("format"); + hashes::hashes_file_format format; + + if (format_mb == member.MemberEnd() || !format_mb->value.IsString() || (format = hashes::get_format_idx(format_mb->value.GetString())) == hashes::HFF_COUNT) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a hashes storage without a valid format", mod_name)); + return false; + } + + std::filesystem::path path_cfg = path_mb->value.GetString(); + auto path = path_cfg.is_absolute() ? path_cfg : (mod_path / path_cfg); + + return hashes::load_file(path, format); + } else { logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is load data member with an unknown type '{}'", mod_name, type_val)); @@ -568,6 +785,184 @@ namespace mods { return true; } + bool read_cache_entry(rapidjson::Value& member, const char* mod_name, const std::filesystem::path& mod_path) + { + auto type = member.FindMember("type"); + + if (type == member.MemberEnd() || !type->value.IsString()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a cache member without a valid type", mod_name)); + return false; + } + + auto name = member.FindMember("name"); + + if (name == member.MemberEnd() || !name->value.IsString()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a cache member without a valid name", mod_name)); + return false; + } + + const char* name_val = name->value.GetString(); + const char* type_val = type->value.GetString(); + + xassets::BGCacheTypes bgtype = xassets::BG_Cache_GetTypeIndex(type_val); + + if (!bgtype) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a cache member with a bad type: {}", mod_name, type_val)); + return false; + } + + cache_entry tmp{}; + + tmp.name.hash = fnv1a::generate_hash_pattern(name_val); + tmp.type = bgtype; + + auto hook_map = member.FindMember("map"); + auto hook_mode = member.FindMember("mode"); + auto hook_gametype = member.FindMember("gametype"); + + if (hook_map != member.MemberEnd()) + { + if (hook_map->value.IsArray()) + { + auto data_array = hook_map->value.GetArray(); + + for (rapidjson::Value& hookmember : data_array) + { + if (!hookmember.IsString()) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is containing a cache member with a bad map hook", mod_name)); + continue; + } + tmp.hooks_map.insert(fnv1a::generate_hash(hookmember.GetString())); + } + } + else if (hook_map->value.IsString()) + { + tmp.hooks_map.insert(fnv1a::generate_hash(hook_map->value.GetString())); + } + else + { + logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is containing a cache member with a bad map hook", mod_name)); + return false; + } + } + + if (hook_mode != member.MemberEnd()) + { + if (hook_mode->value.IsArray()) + { + auto data_array = hook_mode->value.GetArray(); + + for (rapidjson::Value& hookmember : data_array) + { + if (!hookmember.IsString()) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is containing a cache member with a bad mode hook", mod_name)); + continue; + } + game::eModes loaded = game::Com_SessionMode_GetModeForAbbreviation(hookmember.GetString()); + if (loaded == game::eModes::MODE_COUNT) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is containing a cache member with a bad mode hook", mod_name)); + continue; + } + tmp.hooks_modes.insert(loaded); + } + } + else if (hook_mode->value.IsString()) + { + game::eModes loaded = game::Com_SessionMode_GetModeForAbbreviation(hook_mode->value.GetString()); + if (loaded == game::eModes::MODE_COUNT) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is containing a cache member with a bad mode hook", mod_name)); + return false; + } + tmp.hooks_modes.insert(loaded); + } + else + { + logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is containing a cache member with a bad mode hook", mod_name)); + return false; + } + } + + if (hook_gametype != member.MemberEnd()) + { + if (hook_gametype->value.IsArray()) + { + auto data_array = hook_gametype->value.GetArray(); + + for (rapidjson::Value& hookmember : data_array) + { + if (!hookmember.IsString()) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is containing a cache member with a bad gametype hook", mod_name)); + continue; + } + tmp.hooks_gametype.insert(fnv1a::generate_hash(hookmember.GetString())); + } + } + else if (hook_gametype->value.IsString()) + { + tmp.hooks_gametype.insert(fnv1a::generate_hash(hook_gametype->value.GetString())); + } + else + { + logger::write(logger::LOG_TYPE_ERROR, std::format("mod {} is containing a cache member with a bad gametype hook", mod_name)); + return false; + } + } + + cache_entries.push_back(tmp); + + return true; + } + bool read_redirect_entry(rapidjson::Value& member, const char* mod_name, const std::filesystem::path& mod_path) + { + auto type = member.FindMember("type"); + + if (type == member.MemberEnd() || !type->value.IsString()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a redirect member without a valid type", mod_name)); + return false; + } + + auto origin = member.FindMember("origin"); + + if (origin == member.MemberEnd() || !origin->value.IsString()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a redirect member without a valid origin", mod_name)); + return false; + } + + auto target = member.FindMember("target"); + + if (target == member.MemberEnd() || !target->value.IsString()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a redirect member without a valid target", mod_name)); + return false; + } + + xassets::XAssetType assettype = xassets::DB_GetXAssetTypeIndex(type->value.GetString()); + + if (assettype == xassets::ASSET_TYPE_COUNT) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a redirect member without a valid type: {}", mod_name, type->value.GetString())); + return false; + } + + int64_t from = fnv1a::generate_hash_pattern(origin->value.GetString()); + int64_t to = fnv1a::generate_hash_pattern(target->value.GetString()); + assets_redirects[assettype][from] = to; + + logger::write(logger::LOG_TYPE_DEBUG, std::format("mod {}: loaded redirect {:x} -> {:x} ({})", mod_name, from, to, xassets::DB_GetXAssetTypeName(assettype))); + + return true; + } + bool load_mods() { std::lock_guard lg{ load_mutex }; @@ -634,7 +1029,52 @@ namespace mods { continue; } - if (!read_data_entry(member, mod_name, mod_path)) + auto ignore_error_mb = member.FindMember("ignore_error"); + bool ignore_error = ignore_error_mb != member.MemberEnd() && ignore_error_mb->value.IsBool() && ignore_error_mb->value.GetBool(); + + if (!read_data_entry(member, mod_name, mod_path) && !ignore_error) + { + mod_errors++; + } + } + } + auto cache_member = info.FindMember("cache"); + + if (cache_member != info.MemberEnd() && cache_member->value.IsArray()) + { + auto data_array = cache_member->value.GetArray(); + + for (rapidjson::Value& member : data_array) + { + if (!member.IsObject()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a bad cache member", mod_name)); + mod_errors++; + continue; + } + + if (!read_cache_entry(member, mod_name, mod_path)) + { + mod_errors++; + } + } + } + auto redirect_member = info.FindMember("redirect"); + + if (redirect_member != info.MemberEnd() && redirect_member->value.IsArray()) + { + auto redirect_array = redirect_member->value.GetArray(); + + for (rapidjson::Value& member : redirect_array) + { + if (!member.IsObject()) + { + logger::write(logger::LOG_TYPE_WARN, std::format("mod {} is containing a bad redirect member", mod_name)); + mod_errors++; + continue; + } + + if (!read_redirect_entry(member, mod_name, mod_path)) { mod_errors++; } @@ -675,10 +1115,25 @@ namespace mods { } utilities::hook::detour db_find_xasset_header_hook; + utilities::hook::detour db_does_xasset_exist_hook; utilities::hook::detour scr_gsc_obj_link_hook; + utilities::hook::detour hksl_loadfile_hook; void* db_find_xasset_header_stub(xassets::XAssetType type, game::BO4_AssetRef_t* name, bool errorIfMissing, int waitTime) { + auto& redirect = storage.assets_redirects[type]; + + auto replaced = redirect.find(name->hash & 0x7FFFFFFFFFFFFFFF); + + game::BO4_AssetRef_t redirected_name; + if (replaced != redirect.end()) + { + // replace xasset + redirected_name.hash = replaced->second; + redirected_name.null = 0; + name = &redirected_name; + } + void* header = storage.get_xasset(type, name->hash); if (header) @@ -689,6 +1144,31 @@ namespace mods { return db_find_xasset_header_hook.invoke(type, name, errorIfMissing, waitTime); } + bool db_does_xasset_exist_stub(xassets::XAssetType type, game::BO4_AssetRef_t* name) + { + auto& redirect = storage.assets_redirects[type]; + + auto replaced = redirect.find(name->hash & 0x7FFFFFFFFFFFFFFF); + + game::BO4_AssetRef_t redirected_name; + if (replaced != redirect.end()) + { + // replace xasset + redirected_name.hash = replaced->second; + redirected_name.null = 0; + name = &redirected_name; + } + + void* header = storage.get_xasset(type, name->hash); + + if (header) + { + return true; + } + + return db_does_xasset_exist_hook.invoke(type, name); + } + int scr_gsc_obj_link_stub(game::scriptInstance_t inst, game::GSC_OBJ* prime_obj, bool runScript) { // link the injected scripts if we find a hook, sync the gsic fields at the same time @@ -719,24 +1199,77 @@ namespace mods { return scr_gsc_obj_link_hook.invoke(inst, prime_obj, runScript); } + int hksl_loadfile_stub(game::lua_state* state, const char* filename) + { + + uint64_t hash{}; + if (!storage.lua_files.empty()) + { + hash = fnv1a::generate_hash_pattern(filename); + } + + for (auto& lua : storage.lua_files) + { + // we need to use the hash because filename is x64:HASH or unhashed + + if (lua.hooks.find(hash) != lua.hooks.end()) + { + std::string name = std::format("x64:{:x}.lua", lua.noext_name); + if (!game::Lua_CoD_LoadLuaFile(state, name.c_str())) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("error when loading hook lua {} (pre)", name)); + } + } + } + + int load = hksl_loadfile_hook.invoke(state, filename); + + for (auto& lua : storage.lua_files) + { + // we need to use the hash because filename is x64:HASH or unhashed + + if (lua.hooks_post.find(hash) != lua.hooks_post.end()) + { + std::string name = std::format("x64:{:x}.lua", lua.noext_name); + if (!game::Lua_CoD_LoadLuaFile(state, name.c_str())) + { + logger::write(logger::LOG_TYPE_ERROR, std::format("error when loading hook lua {} (post)", name)); + } + } + } + + return load; + } + + utilities::hook::detour bg_cache_sync_hook; + + void bg_cache_sync_stub() + { + storage.sync_cache_entries(); + + xassets::Demo_AddBGCacheAndRegister(&storage.custom_cache, 0x16000); + + // sync default + bg_cache_sync_hook.invoke(); + } + class component final : public component_interface { public: void post_unpack() override { - // custom assets loading - db_find_xasset_header_hook.create(0x142EB75B0_g, db_find_xasset_header_stub); + storage.load_mods(); + // custom assets loading + db_find_xasset_header_hook.create(xassets::DB_FindXAssetHeader.get(), db_find_xasset_header_stub); + db_does_xasset_exist_hook.create(0x142EB6C90_g, db_does_xasset_exist_stub); scr_gsc_obj_link_hook.create(0x142748F10_g, scr_gsc_obj_link_stub); + hksl_loadfile_hook.create(0x14375D6A0_g, hksl_loadfile_stub); + bg_cache_sync_hook.create(0x1405CE0B0_g, bg_cache_sync_stub); // register load mods command Cmd_AddCommand("reload_mods", load_mods_cmd); } - - void pre_start() override - { - storage.load_mods(); - } }; } diff --git a/source/proxy-dll/definitions/game.cpp b/source/proxy-dll/definitions/game.cpp index d0970b7..8c80a6b 100644 --- a/source/proxy-dll/definitions/game.cpp +++ b/source/proxy-dll/definitions/game.cpp @@ -9,6 +9,17 @@ namespace game typedef const char* (__fastcall* Com_GetBuildVersion_t)(); Com_GetBuildVersion_t Com_GetBuildVersion = (Com_GetBuildVersion_t)0x142892F40_g; + + scoped_critical_section::scoped_critical_section(int32_t s, scoped_critical_section_type type) : _s(0), _hasOwnership(false), _isScopedRelease(false), _next(nullptr) + { + game::ScopedCriticalSectionConstructor(this, s, type); + } + + scoped_critical_section::~scoped_critical_section() + { + game::ScopedCriticalSectionDestructor(this); + } + namespace { void verify_game_version() diff --git a/source/proxy-dll/definitions/game.hpp b/source/proxy-dll/definitions/game.hpp index 90165ad..18cad94 100644 --- a/source/proxy-dll/definitions/game.hpp +++ b/source/proxy-dll/definitions/game.hpp @@ -81,7 +81,7 @@ namespace game int32_t include_offset; uint16_t string_count; uint16_t exports_count; - int32_t ukn20; + int32_t start_data; int32_t string_offset; int16_t imports_count; uint16_t fixup_count; @@ -93,12 +93,12 @@ namespace game int32_t fixup_offset; int32_t globalvar_offset; int32_t script_size; - int32_t ukn4c_offset; + int32_t requires_implements_offset; int32_t ukn50; - int32_t ukn54; + int32_t data_length; uint16_t include_count; byte ukn5a; - byte ukn4c_count; + byte requires_implements_count; inline GSC_EXPORT_ITEM* get_exports() { @@ -183,7 +183,8 @@ namespace game TYPE_REMOVED_THREAD = 0x19, TYPE_FREE = 0x1a, TYPE_THREAD_LIST = 0x1b, - TYPE_ENT_LIST = 0x1c + TYPE_ENT_LIST = 0x1c, + TYPE_COUNT }; @@ -222,6 +223,39 @@ namespace game ScrVarType_t type; }; + struct ScrVar_t_Info + { + uint32_t nameType : 3; + uint32_t flags : 5; + uint32_t refCount : 24; + }; + + struct ScrVar_t + { + ScrVarNameIndex_t nameIndex; + ScrVar_t_Info _anon_0; + ScrVarIndex_t nextSibling; + ScrVarIndex_t prevSibling; + ScrVarIndex_t parentId; + ScrVarIndex_t nameSearchHashList; + uint32_t pad0; + }; + + union ScrVarObjectInfo1_t + { + uint64_t object_o; + unsigned int size; + ScrVarIndex_t nextEntId; + ScrVarIndex_t self; + ScrVarIndex_t free; + }; + + union ScrVarObjectInfo2_t + { + uint32_t object_w; + ScrVarIndex_t stackId; + }; + struct function_stack_t { byte* pos; @@ -232,6 +266,11 @@ namespace game uint16_t profileInfoCount; }; + struct function_frame_t + { + function_stack_t fs; + }; + struct ScrVmContext_t { ScrVarIndex_t fieldValueId; @@ -242,6 +281,48 @@ namespace game typedef void (*VM_OP_FUNC)(scriptInstance_t, function_stack_t*, ScrVmContext_t*, bool*); + + struct BO4_scrVarGlob + { + ScrVarIndex_t* scriptNameSearchHashList; + ScrVar_t* scriptVariables; + ScrVarObjectInfo1_t* scriptVariablesObjectInfo1; + ScrVarObjectInfo2_t* scriptVariablesObjectInfo2; + ScrVarValue_t* scriptValues; + }; + + struct BO4_scrVmPub + { + void* unk0; + void* unk8; + void* executionQueueHeap; // HunkUser + void* timeByValueQueue; // VmExecutionQueueData_t + void* timeByThreadQueue[1024]; // VmExecutionQueue_t + void* frameByValueQueue; // VmExecutionQueueData_t + void* frameByThreadQueue[1024]; // VmExecutionQueue_t + void* timeoutByValueList; // VmExecutionQueueData_t + void* timeoutByThreadList[1024]; // VmExecutionQueue_t + void* notifyByObjectQueue[1024]; // VmExecutionNotifyQueue_t + void* notifyByThreadQueue[1024]; // VmExecutionNotifyQueue_t + void* endonByObjectList[1024]; // VmExecutionNotifyQueue_t + void* endonByThreadList[1024]; // VmExecutionNotifyQueue_t + ScrVarIndex_t* localVars; + ScrVarValue_t* maxstack; + function_frame_t* function_frame; + ScrVarValue_t* top; + function_frame_t function_frame_start[64]; + ScrVarValue_t stack[2048]; + uint32_t time; + uint32_t frame; + int function_count; + int callNesting; + unsigned int inparamcount; + bool showError; + bool systemInitialized; + bool vmInitialized; + bool isShutdown; + }; + struct objFileInfo_t { GSC_OBJ* activeVersion; @@ -655,6 +736,135 @@ namespace game MODE_FIRST = 0x0, }; + enum consoleLabel_e : int32_t + { + CON_LABEL_TEMP = 0x0, + CON_LABEL_GFX = 0x2, + CON_LABEL_TASKMGR2 = 0x3, + CON_LABEL_LIVE = 0x4, + CON_LABEL_LIVE_XBOX = 0x5, + CON_LABEL_LIVE_PS4 = 0x6, + CON_LABEL_MATCHMAKING = 0x7, + CON_LABEL_DEMONWARE = 0x8, + CON_LABEL_LEADERBOARDS = 0x9, + CON_LABEL_LOBBY = 0x0A, + CON_LABEL_LOBBYHOST = 0x0B, + CON_LABEL_LOBBYCLIENT = 0x0C, + CON_LABEL_LOBBYVM = 0x0D, + CON_LABEL_MIGRATION = 0x0E, + CON_LABEL_IG_MIGRATION_Host = 0x0F, + CON_LABEL_IG_MIGRATION_Client = 0x10, + CON_LABEL_SCRIPTER = 0x11, + CON_LABEL_VM = 0x12, + CON_LABEL_DVAR = 0x13, + CON_LABEL_TOOL = 0x14, + CON_LABEL_ANIM = 0x15, + CON_LABEL_NETCHAN = 0x16, + CON_LABEL_BGCACHE = 0x17, + CON_LABEL_PM = 0x18, + CON_LABEL_MAPSWITCH = 0x19, + CON_LABEL_AI = 0x1A, + CON_LABEL_GADGET = 0x1B, + CON_LABEL_SOUND = 0x1C, + CON_LABEL_SNAPSHOT = 0x1D, + CON_LABEL_PLAYGO = 0x1E, + CON_LABEL_LUI = 0x1F, + CON_LABEL_LUA = 0x20, + CON_LABEL_VOIP = 0x21, + CON_LABEL_DEMO = 0x22, + CON_LABEL_DB = 0x23, + CON_LABEL_HTTP = 0x24, + CON_LABEL_DCACHE = 0x25, + CON_LABEL_MEM = 0x26, + CON_LABEL_CINEMATIC = 0x27, + CON_LABEL_DDL = 0x28, + CON_LABEL_STORAGE = 0x29, + CON_LABEL_STEAM = 0x2A, + CON_LABEL_CHKPTSAVE = 0x2B, + CON_LABEL_DLOG = 0x2C, + CON_LABEL_FILESHARE = 0x2D, + CON_LABEL_LPC = 0x2E, + CON_LABEL_MARKETING = 0x2F, + CON_LABEL_STORE = 0x30, + CON_LABEL_TESTING = 0x31, + CON_LABEL_LOOT = 0x32, + CON_LABEL_MATCHRECORDER = 0x33, + CON_LABEL_EXCHANGE = 0x34, + CON_LABEL_SCRIPTERROR = 0x35, + CON_LABEL_PLAYOFTHEMATCH = 0x36, + CON_LABEL_FILESYS = 0x37, + CON_LABEL_JSON = 0x38, + CON_LABEL_CUSTOMGAMES = 0x39, + CON_LABEL_GAMEPLAY = 0x3A, + CON_LABEL_STREAM = 0x3B, + CON_LABEL_XPAK = 0x3C, + CON_LABEL_AE = 0x3D, + CON_LABEL_STRINGTABLE = 0x3E, + CON_LABEL_COUNT = 0x3F + }; + + enum scoped_critical_section_type : int32_t + { + SCOPED_CRITSECT_NORMAL = 0x0, + SCOPED_CRITSECT_DISABLED = 0x1, + SCOPED_CRITSECT_RELEASE = 0x2, + SCOPED_CRITSECT_TRY = 0x3, + }; + + class scoped_critical_section + { + int32_t _s; + bool _hasOwnership; + bool _isScopedRelease; + scoped_critical_section* _next; + public: + scoped_critical_section(int32_t s, scoped_critical_section_type type); + ~scoped_critical_section(); + }; + + struct hks_global {}; + struct hks_callstack + { + void* m_records; // hks::CallStack::ActivationRecord* + void* m_lastrecord; // hks::CallStack::ActivationRecord* + void* m_current; // hks::CallStack::ActivationRecord* + const void* m_current_lua_pc; // const hksInstruction* + const void* m_hook_return_addr; // const hksInstruction* + int32_t m_hook_level; + }; + struct lua_state; + struct hks_object + { + uint32_t t; + union { + void* ptr; + float number; + int32_t boolean; + uint32_t native; + lua_state* thread; + } v; + }; + struct hks_api_stack + { + hks_object* top; + hks_object* base; + hks_object* alloc_top; + hks_object* bottom; + }; + + struct lua_state + { + // hks::GenericChunkHeader + size_t m_flags; + // hks::ChunkHeader + void* m_next; + + hks_global* m_global; + hks_callstack m_callStack; + hks_api_stack m_apistack; + + // ... + }; ////////////////////////////////////////////////////////////////////////// // SYMBOLS // @@ -691,6 +901,10 @@ namespace game // Main Functions WEAK symbol Com_Error_{ 0x14288B410_g }; + // mutex + WEAK symbol ScopedCriticalSectionConstructor{ 0x14289E3C0_g }; + WEAK symbol ScopedCriticalSectionDestructor{ 0x14289E440_g }; + // CMD WEAK symbol Cbuf_AddText{ 0x143CDE880_g }; @@ -716,6 +930,8 @@ namespace game WEAK symbol Com_IsInGame{ 0x14288FDB0_g }; WEAK symbol Com_IsRunningUILevel{ 0x14288FDF0_g }; WEAK symbol Com_SessionMode_GetMode{ 0x14289EFF0_g }; + WEAK symbol Com_SessionMode_GetModeForAbbreviation{ 0x14289F000_g }; + WEAK symbol Com_SessionMode_GetAbbreviationForMode{0x14289EC70_g}; WEAK symbol keyCatchers{ 0x148A53F84_g }; WEAK symbol playerKeys{ 0x148A3EF80_g }; @@ -737,11 +953,20 @@ namespace game WEAK symbol ScrVm_GetInt{ 0x142773B50_g }; WEAK symbol ScrVm_GetString{ 0x142774840_g }; WEAK symbol ScrVm_GetVector{ 0x142774E40_g }; - WEAK symbol ScrVm_GetConstString{ 0x142772E10_g }; + WEAK symbol ScrVm_GetConstString{ 0x142772E10_g }; WEAK symbol ScrVm_GetNumParam{ 0x142774440_g }; WEAK symbol ScrVm_GetPointerType{ 0x1427746E0_g }; WEAK symbol ScrVm_GetType{ 0x142774A20_g }; - + WEAK symbol ScrVm_AddStruct{ 0x14276EF00_g }; + WEAK symbol ScrVm_SetStructField{ 0x142778450_g }; + WEAK symbol ScrVm_AddToArray{ 0x14276F1C0_g }; + WEAK symbol ScrVm_AddToArrayStringIndexed{ 0x14276F230_g }; + WEAK symbol ScrVm_AddVector{ 0x14276F490_g }; + WEAK symbol ScrVar_PushArray{ 0x142775CF0_g }; + WEAK symbol ScrStr_ConvertToString{ 0x142759030_g }; + WEAK symbol ScrVar_NewVariableByIndex{ 0x142760440_g }; + WEAK symbol ScrVar_SetValue{ 0x1427616B0_g }; + WEAK symbol CScr_GetFunction{ 0x141F13140_g }; WEAK symbol Scr_GetFunction{ 0x1433AF840_g }; WEAK symbol CScr_GetMethod{ 0x141F13650_g }; @@ -749,13 +974,26 @@ namespace game WEAK symbol ScrVm_Error{ 0x142770330_g }; WEAK symbol scrVarPub{ 0x148307880_g }; + WEAK symbol scrVarGlob{ 0x148307830_g }; + WEAK symbol scrVmPub{ 0x148307AA0_g }; WEAK symbol gVmOpJumpTable{ 0x144EED340_g }; WEAK symbol gObjFileInfoCount{ 0x1482F76B0_g }; WEAK symbol gObjFileInfo{ 0x1482EFCD0_g }; - + + // lua functions + WEAK symbol Lua_CoD_LoadLuaFile{ 0x143962DF0_g }; + WEAK symbol Lua_CoD_LuaStateManager_Error{ 0x14398A860_g }; + WEAK symbol hks_obj_tolstring{ 0x143755730_g }; + WEAK symbol hks_obj_tonumber{ 0x143755A90_g }; + + // console labels + WEAK symbol builtinLabels{ 0x144F11530_g }; + // gsc types + WEAK symbol var_typename{ 0x144EED240_g }; + WEAK symbol Cmd_AddCommandInternal{0x143CDEE80_g}; - + #define Cmd_AddCommand(name, function) \ static game::cmd_function_t __cmd_func_##function; \ game::BO4_AssetRef_t __cmd_func_name_##function { (int64_t)fnv1a::generate_hash(name), 0 }; \ @@ -769,4 +1007,12 @@ namespace game #define Com_Error(code, fmt, ...) \ Com_Error_(__FILE__, __LINE__, code, fmt, ##__VA_ARGS__) + + class scoped_critical_section_guard_lock + { + + + + + }; } \ No newline at end of file diff --git a/source/proxy-dll/definitions/game_runtime_errors.cpp b/source/proxy-dll/definitions/game_runtime_errors.cpp new file mode 100644 index 0000000..0c98474 --- /dev/null +++ b/source/proxy-dll/definitions/game_runtime_errors.cpp @@ -0,0 +1,185 @@ +#include +#include "definitions/game.hpp" +#include "definitions/xassets.hpp" +#include "game_runtime_errors.hpp" + + +namespace game::runtime_errors +{ + namespace + { + std::unordered_map errors = + { + { 1045192683, "Scr_RandomFloatRange's second parameter must be greater than the first." }, + { 1047729873, "exitlevel already called" }, + { 104978404, "cannot cast type to canon" }, + { 1072585573, "Raw file is not a file of the right type" }, + { 1088278929, "Raw file is not a file of the right type" }, + { 1099145600, "Can't find asset" }, + { 1132507782, "bad opcode" }, + { 1137123674, "GScr_LUINotifyEvent: Expected Istrings, hash or integers only" }, + { 1252503459, "caller is not an entity" }, + { 1273214009, "CScr_PlayFX: invalid effect" }, + { 1333649786, "IsMature can only be called on a player." }, + { 1364331101, "Object must be an array" }, + { 1377489376, "Gesture key can't have the higher bit set" }, + { 1385570291, "assert fail (with message)" }, + { 1402557361, "bad opcode" }, + { 1403832952, "Attempt to register ClientField failed. Client Field set either already contains a field, or a hash collision has occurred." }, + { 1412130119, "parameter can't be cast to a hash" }, + { 1427704235, "expected struct type to add value pair" }, + { 1480037573, "Invalid canonical name hash" }, + { 1480821566, "Error registering client field. Attempted field size is not acceptable bit number range 1->32" }, + { 1517473035, "Value out of range. Allowed values: 0 to 2" }, + { 1609894461, "bad entity" }, + { 1670707254, "linking error" }, + { 1850691545, "Debug Break" }, + { 1895566756, "dvar is not a 3d vector, but GetDvarVector3D has been called on it" }, + { 1909233687, "Optional argument must be a vector type" }, + { 1915758537, "RegisterClientField can only accept bit ranges between 1 and 32." }, + { 1957162421, "Can't find bgCache entry" }, + { 1999906612, "type is not a integer or float" }, + { 2078816051, "not a valid name for a clientfield set." }, + { 209668787, "RandomInt parm must be positive integer." }, + { 2116335949, "function called with too many parameters" }, + { 219569925, "hasperk() can only be called on local players" }, + { 219686544, "object is not an array index" }, + { 2253722136, "CamAnimScripted can only be called on a player." }, + { 2269096660, "vector scale expecting vector" }, + { 2279633554, "SV_SetConfigstring: bad index" }, + { 2331090760, "stack does not have the pool" }, + { 2344222932, "assert fail" }, + { 2355618801, "Gesture table key can't have the higher bit set" }, + { 2408700928, "not a valid name for a clientfield set." }, + { 2448966512, "string too long" }, + { 245612264, "foreach should be used with an array" }, + { 247375020, "Attempting to register client field in client field set using bits, but system is out of space." }, + { 2479879368, "RegisterClientField can only accept bit ranges between 1 and 32." }, + { 249068885, "Failed to allocate from state pool" }, + { 2517242050, "parameter does not exist" }, + { 2532286589, "error message" }, + { 2538360978, "not a function pointer" }, + { 2572009355, "vector scale expecting vector" }, + { 2606724305, "parameter does not exist" }, + { 2626909173, "exceeded maximum number of script strings" }, + { 2681972741, "bad clientfield for name" }, + { 2687742442, "Forced script exception." }, + { 269518924, "Ammo count must not be negative" }, + { 2706458388, "Trying to get a local client index for a client that is not a local client." }, + { 2734491973, "Error need at least one argument for LUINotifyEvent." }, + { 2737681163, "assert fail (with message)" }, + { 2751867714, "self isn't a field object" }, + { 2792722947, "RegisterClientField can only accept 5 parameters" }, + { 280703902, "parameter does not exist" }, + { 2838301872, "Gesture table key can't have the higher bit set" }, + { 2855209542, "Cannot call IncrementClientField on a non-'counter' type clientfield." }, + { 2940210599, "Invalid Version Handling. Grab Bat !!!" }, + { 3015158315, "getperks() can only be called on local players" }, + { 3016026156, "Can't find asset" }, + { 3030895505, "Whitelist failure for title" }, + { 3059411687, "Argument and parameter count mismatch for LUINotifyEvent" }, + { 3086288875, "playFx called with (0 0 0) forward direction" }, + { 3122940489, "caller is not an entity" }, + { 312545010, "not a vector" }, + { 3143575744, "parameter does not exist" }, + { 317100267, "unmatching types" }, + { 3189465155, "Invalid bgCache type" }, + { 3221522156, "Failed to alloc client field" }, + { 3222417139, "size cannot be applied to type" }, + { 3251676101, "Could not load raw file" }, + { 3255107847, "LUINotifyEvent: entity must be a player entity" }, + { 3288551912, "expected array type to add value pair" }, + { 3385008561, "cannot set field on type" }, + { 3459949409, "Failed to alloc client field - MAX_CLIENTFIELD_FIELDS_IN_SET=512 exceeded." }, + { 3523382186, "ScrEvent map is full, unable to register new event" }, + { 3524483616, "Ammo count must not be negative" }, + { 3592841213, "cannot directly set the origin on AI. Use the teleport command instead." }, + { 359760836, "G_Spawn: no free entities" }, + { 3654063291, "ScrEvent map is full, unable to register new event" }, + { 3679846953, "LUINotifyEvent: entity must be a player entity" }, + { 3699844858, "parameter does not exist" }, + { 3761634992, "not a pointer" }, + { 3894031202, "Can't find gamedata/playeranim/playeranimtypes.txt" }, + { 3967909977, "Trying to get version of a demo when the demo system is idle." }, + { 3990130335, "player getperks(): localClientNum out of range" }, + { 4039057166, "LUINotifyEvent: entity must be a player entity" }, + { 4047738848, "Invalid opcode (Recovery)" }, + { 409067247, "No clientfield named found in set." }, + { 4100218247, "Error sending LUI notify event: LUI event name is not precached." }, + { 4103906837, "Entity is not an item." }, + { 4104994143, "can't allocate more script variables" }, + { 4106063796, "key value provided for array is not valid" }, + { 4163774148, "Optional argument must be a float or integer type" }, + { 4178589916, "Model was not cached by the linker." }, + { 4196473479, "parameter does not exist" }, + { 4213634562, "precacheLeaderboards must be called before any wait statements in the gametype or level script" }, + { 4220857104, "Cannot call IncrementClientField on a 'counter' type clientfield on the frame it is spawned, since newEnts on the clientside will not process 'counter' type clientfields." }, + { 467754466, "Error registering client field. bCallbacksFor0WhenNew (CF_CALLBACK_ZERO_ON_NEW_ENT) is disallowed for CF_SCRIPTMOVERFIELDS." }, + { 512306404, "not a function pointer" }, + { 57350207, "Unknown perk" }, + { 580674660, "Unknown ent type in GScr_GetFieldSetForEntityType." }, + { 619241173, "Failed to allocate from element pool" }, + { 647662103, "parameter can't be cast to a string" }, + { 657813230, "Error registering client field. bCallbacksFor0WhenNew (CF_CALLBACK_ZERO_ON_NEW_ENT) is disallowed for counter type clientfields. Due to it's treatment of the old and new val as a ring buffer, the counter type is not valid on a new snapshot, new ent, or demojump" }, + { 665902298, "Parameter must be an array" }, + { 71894325, "CamAnimScripted or ExtraCamAnimScripted can only be called on a player." }, + { 732489269, "Non-player entity passed to UploadStats()" }, + { 744499668, "too many vehicles" }, + { 750896894, "Invalid bgCache type" }, + { 753495682, "RandomIntRange's second parameter must be greater than the first." }, + { 754846421, "invalid vehicle spawn origin" }, + { 829015102, "var isn't a field object" }, + { 876169112, "key value provided for struct is not valid" }, + { 887228744, "origin being set to NAN." }, + { 904544970, "Attempt to register Client Field post finalization of Client Field registration period failed." }, + { 941828720, "exitlevel already called" }, + { 946363963, "Invalid opcode" }, + { 949934674, "Invalid Version Handling. Grab Bat !!!" }, + { 952690413, "parameter can't be cast to a boolean" }, + { 962032109, "Entity does not exist." }, + { 968521323, "player hasperk(, ): localClientNum out of range" }, + { 4088624643, "Can't find asset" }, + { game::runtime_errors::custom_error_id, "Shield Error" } + }; + } + + const char* get_error_message(uint64_t code) + { + auto it = errors.find(code); + // known error + if (it != errors.end()) + { + return it->second; + } + + // read from the csv + static game::BO4_AssetRef_t custom_errors_file = game::AssetRef(fnv1a::generate_hash("gamedata/shield/custom_errors.csv")); + + xassets::stringtable_header* table = xassets::DB_FindXAssetHeader(xassets::ASSET_TYPE_STRINGTABLE, &custom_errors_file, false, -1).stringtable; + + if (!table || !table->columns_count || table->columns_count < 2) + { + return nullptr; + } + + for (size_t i = 0; i < table->rows_count; i++) + { + auto* rows = &table->values[i * table->columns_count]; + + if (rows[0].type != xassets::STC_TYPE_INT || rows[1].type != xassets::STC_TYPE_STRING) + { + continue; // bad types + } + + if (rows[0].value.hash_value == code) + { + return rows[1].value.string_value; + } + } + + // unknown + return nullptr; + } + +} + diff --git a/source/proxy-dll/definitions/game_runtime_errors.hpp b/source/proxy-dll/definitions/game_runtime_errors.hpp new file mode 100644 index 0000000..760d67c --- /dev/null +++ b/source/proxy-dll/definitions/game_runtime_errors.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace game::runtime_errors +{ + constexpr uint64_t custom_error_id = 0x42693201; + const char* get_error_message(uint64_t code); +} \ No newline at end of file diff --git a/source/proxy-dll/definitions/variables.cpp b/source/proxy-dll/definitions/variables.cpp index b0fc822..599f1b7 100644 --- a/source/proxy-dll/definitions/variables.cpp +++ b/source/proxy-dll/definitions/variables.cpp @@ -1,19 +1,35 @@ #include #include "variables.hpp" +#include "component/hashes.hpp" namespace fnv1a { - uint64_t generate_hash(const char* string) + uint64_t generate_hash(const char* string, uint64_t start) { - uint64_t Result = 0xCBF29CE484222325; + uint64_t res = start; - for (uint64_t i = 0; i < strlen(string); i++) + for (const char* c = string; *c; c++) { - Result ^= string[i]; - Result *= 0x100000001B3; + if (*c == '\\') + { + res ^= '/'; + } + else + { + res ^= tolower(*c); + } + + res *= 0x100000001B3; } - return (Result & 0x7FFFFFFFFFFFFFFF); + uint64_t val = res & 0x7FFFFFFFFFFFFFFF; + + if (start == 0xCBF29CE484222325) + { + hashes::add_hash(val, string); + } + + return val; } uint64_t generate_hash_pattern(const char* string) @@ -21,13 +37,29 @@ namespace fnv1a std::string_view v{ string }; // basic notations hash_123, file_123, script_123 - if (!v.rfind("hash_", 0)) return std::strtoull(&string[5], nullptr, 16); - if (!v.rfind("file_", 0)) return std::strtoull(&string[5], nullptr, 16); - if (!v.rfind("script_", 0)) return std::strtoull(&string[7], nullptr, 16); + if (!v.rfind("hash_", 0)) return std::strtoull(&string[5], nullptr, 16) & 0x7FFFFFFFFFFFFFFF; + if (!v.rfind("file_", 0)) return std::strtoull(&string[5], nullptr, 16) & 0x7FFFFFFFFFFFFFFF; + if (!v.rfind("script_", 0)) return std::strtoull(&string[7], nullptr, 16) & 0x7FFFFFFFFFFFFFFF; // lua notation x64:123 - if (!v.rfind("x64:", 0)) return std::strtoull(&string[4], nullptr, 16); - + if (!v.rfind("x64:", 0)) + { + if (v.length() <= 0x18 && v.ends_with(".lua")) + { + // x64:123456789abcdf.lua + // + // extract hash value + char tmpbuffer[0x17] = {}; + + memcpy(tmpbuffer, string + 4, v.length() - 8); + + // gen the hash and add .lua add the end + return generate_hash(".lua", std::strtoull(&string[4], nullptr, 16) & 0x7FFFFFFFFFFFFFFF); + } + + return std::strtoull(&string[4], nullptr, 16) & 0x7FFFFFFFFFFFFFFF; + } + // unknown, use hashed value return generate_hash(string); } diff --git a/source/proxy-dll/definitions/variables.hpp b/source/proxy-dll/definitions/variables.hpp index a0e2818..0bc5928 100644 --- a/source/proxy-dll/definitions/variables.hpp +++ b/source/proxy-dll/definitions/variables.hpp @@ -2,7 +2,7 @@ namespace fnv1a { - uint64_t generate_hash(const char* string); + uint64_t generate_hash(const char* string, uint64_t start = 0xCBF29CE484222325); uint64_t generate_hash_pattern(const char* string); } diff --git a/source/proxy-dll/definitions/xassets.cpp b/source/proxy-dll/definitions/xassets.cpp new file mode 100644 index 0000000..448d2b2 --- /dev/null +++ b/source/proxy-dll/definitions/xassets.cpp @@ -0,0 +1,91 @@ +#include +#include "xassets.hpp" + +namespace xassets +{ + namespace + { + const char* cache_names[] + { + "invalid", + "vehicle", + "model", + "aitype", + "character", + "xmodelalias", + "weapon", + "gesture", + "gesturetable", + "zbarrier", + "rumble", + "shellshock", + "statuseffect", + "xcam", + "destructible", + "streamerhint", + "flowgraph", + "xanim", + "sanim", + "scriptbundle", + "talent", + "statusicon", + "locationselector", + "menu", + "material", + "string", + "eventstring", + "moviefile", + "objective", + "fx", + "lui_menu_data", + "lui_elem", + "lui_elem_uid", + "radiant_exploder", + "soundalias", + "client_fx", + "client_tagfxset", + "client_lui_elem", + "client_lui_elem_uid", + "requires_implements" + }; + } + + BGCacheTypes BG_Cache_GetTypeIndex(const char* name) + { + //BG_Cache_GetTypeIndexInternal base isn't available after unpack + for (size_t i = 0; i < BGCacheTypes::BG_CACHE_TYPE_COUNT; i++) + { + if (!_strcmpi(name, cache_names[i])) + { + return (BGCacheTypes)i; + } + } + return BG_CACHE_TYPE_INVALID; + } + + const char* BG_Cache_GetTypeName(BGCacheTypes type) + { + return cache_names[type]; + } + + XAssetType DB_GetXAssetTypeIndex(const char* name) + { + for (size_t i = 0; i < XAssetType::ASSET_TYPE_COUNT; i++) + { + if (!_strcmpi(name, s_XAssetTypeInfo[i].name)) + { + return (XAssetType)i; + } + } + return XAssetType::ASSET_TYPE_COUNT; + } + + const char* DB_GetXAssetTypeName(XAssetType type) + { + if (type >= ASSET_TYPE_COUNT) + { + return "invalid"; + } + return s_XAssetTypeInfo[type].name; + } +} \ No newline at end of file diff --git a/source/proxy-dll/definitions/xassets.hpp b/source/proxy-dll/definitions/xassets.hpp index 6b08b69..58d968d 100644 --- a/source/proxy-dll/definitions/xassets.hpp +++ b/source/proxy-dll/definitions/xassets.hpp @@ -29,6 +29,11 @@ namespace xassets byte* buffer{}; }; + struct localize_entry_header { + const char* string = ""; + uint64_t name{}; + uint64_t namepad{}; + }; union stringtable_cell_value { @@ -80,6 +85,8 @@ namespace xassets lua_file_header* lua_file; scriptparsetree_header* scriptparsetree; stringtable_header* stringtable; + localize_entry_header* localize; + void* ptr; }; enum XAssetType : byte @@ -257,4 +264,83 @@ namespace xassets ASSET_TYPE_REPORT = 0xA9, ASSET_TYPE_FULL_COUNT = 0xAA, }; + + enum BGCacheTypes : byte + { + BG_CACHE_TYPE_INVALID = 0, + BG_CACHE_TYPE_VEHICLE = 1, + BG_CACHE_TYPE_MODEL = 2, + BG_CACHE_TYPE_AITYPE = 3, + BG_CACHE_TYPE_CHARACTER = 4, + BG_CACHE_TYPE_XMODELALIAS = 5, + BG_CACHE_TYPE_WEAPON = 6, + BG_CACHE_TYPE_GESTURE = 7, + BG_CACHE_TYPE_GESTURETABLE = 8, + BG_CACHE_TYPE_ZBARRIER = 9, + BG_CACHE_TYPE_RUMBLE = 10, + BG_CACHE_TYPE_SHELLSHOCK = 11, + BG_CACHE_TYPE_STATUSEFFECT = 12, + BG_CACHE_TYPE_XCAM = 13, + BG_CACHE_TYPE_DESTRUCTIBLE = 14, + BG_CACHE_TYPE_STREAMERHINT = 15, + BG_CACHE_TYPE_FLOWGRAPH = 16, + BG_CACHE_TYPE_XANIM = 17, + BG_CACHE_TYPE_SANIM = 18, + BG_CACHE_TYPE_SCRIPTBUNDLE = 19, + BG_CACHE_TYPE_TALENT = 20, + BG_CACHE_TYPE_STATUSICON = 21, + BG_CACHE_TYPE_LOCATIONSELECTOR = 22, + BG_CACHE_TYPE_MENU = 23, + BG_CACHE_TYPE_MATERIAL = 24, + BG_CACHE_TYPE_STRING = 25, + BG_CACHE_TYPE_EVENTSTRING = 26, + BG_CACHE_TYPE_MOVIEFILE = 27, + BG_CACHE_TYPE_OBJECTIVE = 28, + BG_CACHE_TYPE_FX = 29, + BG_CACHE_TYPE_LUI_MENU_DATA = 30, + BG_CACHE_TYPE_LUI_ELEM = 31, + BG_CACHE_TYPE_LUI_ELEM_UID = 32, + BG_CACHE_TYPE_RADIANT_EXPLODER = 33, + BG_CACHE_TYPE_SOUNDALIAS = 34, + BG_CACHE_TYPE_CLIENT_FX = 35, + BG_CACHE_TYPE_CLIENT_TAGFXSET = 36, + BG_CACHE_TYPE_CLIENT_LUI_ELEM = 37, + BG_CACHE_TYPE_CLIENT_LUI_ELEM_UID = 38, + BG_CACHE_TYPE_REQUIRES_IMPLEMENTS = 39, + BG_CACHE_TYPE_COUNT + }; + + struct bg_cache_info_def + { + BGCacheTypes type; + game::BO4_AssetRef_t name; + uint64_t asset; + }; + + struct bg_cache_info + { + game::BO4_AssetRef_t name{}; + bg_cache_info_def* def{}; + int defCount{}; + }; + + struct xasset_type_info { + const char* name; + uint64_t unk8; + uint64_t unk10; + game::BO4_AssetRef_t* (*get_name)(game::BO4_AssetRef_t* name, const void* header); + void(*set_name)(void* header, game::BO4_AssetRef_t* name); + }; + + WEAK game::symbol s_XAssetTypeInfo{ 0x14498BB00_g }; + + WEAK game::symbol Demo_AddBGCacheAndRegister{ 0x1405CF5A0_g }; + WEAK game::symbol BG_Cache_RegisterAndGet{ 0x1405CEC20_g }; + WEAK game::symbol BG_Cache_GetTypeIndexInternal{ 0x1405CDBD0_g }; + BGCacheTypes BG_Cache_GetTypeIndex(const char* name); + const char* BG_Cache_GetTypeName(BGCacheTypes type); + XAssetType DB_GetXAssetTypeIndex(const char* name); + const char* DB_GetXAssetTypeName(XAssetType type); + + WEAK game::symbol DB_FindXAssetHeader{ 0x142EB75B0_g }; } \ No newline at end of file