Lua injection, custom BGCache and other things (#30)

This commit is contained in:
Antoine Willerval 2024-01-09 15:26:31 +01:00 committed by GitHub
parent b4d1c8955b
commit 60597903d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2364 additions and 45 deletions

View File

@ -1,8 +1,10 @@
#include <std_include.hpp>
#include "definitions/game.hpp"
#include "definitions/game_runtime_errors.hpp"
#include "component/scheduler.hpp"
#include "loader/component_loader.hpp"
#include <utilities/hook.hpp>
#include <utilities/string.hpp>
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<void>(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);
}

View File

@ -1,12 +1,16 @@
#include <std_include.hpp>
#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 <utilities/hook.hpp>
#include <utilities/io.hpp>
#include <utilities/json_config.hpp>
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<rapidjson::StringBuffer> 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<game::GSC_EXPORT_ITEM*>(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);
}

View File

@ -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);
}

View File

@ -0,0 +1,256 @@
#include <std_include.hpp>
#include "hashes.hpp"
#include "gsc_funcs.hpp"
#include "definitions/variables.hpp"
#include "loader/component_loader.hpp"
#include <utilities/json_config.hpp>
namespace hashes
{
namespace
{
bool enabled = true;
std::unordered_map<uint64_t, std::string>& hash_storage()
{
static std::unordered_map<uint64_t, std::string> 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 "<invalid>";
}
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: <hash>,<string>
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<hash>, <string>
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)

View File

@ -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);
}

View File

@ -0,0 +1,211 @@
#include <std_include.hpp>
#include "definitions/game.hpp"
#include "loader/component_loader.hpp"
#include <utilities/json_config.hpp>
#include <utilities/hook.hpp>
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<void>(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<void>(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)

View File

@ -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 <utilities/io.hpp>
#include <utilities/hook.hpp>
@ -148,6 +150,9 @@ namespace mods {
{
xassets::lua_file_header header{};
std::unordered_set<uint64_t> hooks{};
uint64_t noext_name{};
std::unordered_set<uint64_t> 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<game::eModes> hooks_modes{};
std::unordered_set<uint64_t> hooks_map{};
std::unordered_set<uint64_t> hooks_gametype{};
};
class mod_storage
@ -183,6 +210,18 @@ namespace mods {
std::vector<raw_file> raw_files{};
std::vector<lua_file> lua_files{};
std::vector<string_table_file> csv_files{};
std::vector<localize> localizes{};
std::vector<cache_entry> cache_entries{};
std::unordered_map<int64_t, int64_t> assets_redirects[xassets::ASSET_TYPE_COUNT] = {};
std::vector<xassets::bg_cache_info_def> 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<void*>(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<bool>(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<int>(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<int>(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<void>();
}
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();
}
};
}

View File

@ -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()

View File

@ -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<void(const char* file, int line, int code, const char* fmt, ...)> Com_Error_{ 0x14288B410_g };
// mutex
WEAK symbol<void(scoped_critical_section* sec, int32_t s, scoped_critical_section_type type)> ScopedCriticalSectionConstructor{ 0x14289E3C0_g };
WEAK symbol<void(scoped_critical_section* sec)> ScopedCriticalSectionDestructor{ 0x14289E440_g };
// CMD
WEAK symbol<void(int localClientNum, const char* text)> Cbuf_AddText{ 0x143CDE880_g };
@ -716,6 +930,8 @@ namespace game
WEAK symbol<bool()> Com_IsInGame{ 0x14288FDB0_g };
WEAK symbol<bool()> Com_IsRunningUILevel{ 0x14288FDF0_g };
WEAK symbol<eModes()> Com_SessionMode_GetMode{ 0x14289EFF0_g };
WEAK symbol<eModes(const char* str)> Com_SessionMode_GetModeForAbbreviation{ 0x14289F000_g };
WEAK symbol<const char*(eModes mode)> Com_SessionMode_GetAbbreviationForMode{0x14289EC70_g};
WEAK symbol<int> keyCatchers{ 0x148A53F84_g };
WEAK symbol<PlayerKeyState> playerKeys{ 0x148A3EF80_g };
@ -737,11 +953,20 @@ namespace game
WEAK symbol<int64_t(scriptInstance_t inst, unsigned int index)> ScrVm_GetInt{ 0x142773B50_g };
WEAK symbol<const char*(scriptInstance_t inst, unsigned int index)> ScrVm_GetString{ 0x142774840_g };
WEAK symbol<void(scriptInstance_t inst, unsigned int index, vec3_t* vector)> ScrVm_GetVector{ 0x142774E40_g };
WEAK symbol<int32_t(scriptInstance_t inst, unsigned int index)> ScrVm_GetConstString{ 0x142772E10_g };
WEAK symbol<ScrVarIndex_t(scriptInstance_t inst, unsigned int index)> ScrVm_GetConstString{ 0x142772E10_g };
WEAK symbol<uint32_t(scriptInstance_t inst)> ScrVm_GetNumParam{ 0x142774440_g };
WEAK symbol<ScrVarType_t(scriptInstance_t inst, unsigned int index)> ScrVm_GetPointerType{ 0x1427746E0_g };
WEAK symbol<ScrVarType_t(scriptInstance_t inst, unsigned int index)> ScrVm_GetType{ 0x142774A20_g };
WEAK symbol<uint32_t(scriptInstance_t inst)> ScrVm_AddStruct{ 0x14276EF00_g };
WEAK symbol<void(scriptInstance_t inst, uint32_t structId, uint32_t name)> ScrVm_SetStructField{ 0x142778450_g };
WEAK symbol<void(scriptInstance_t inst)> ScrVm_AddToArray{ 0x14276F1C0_g };
WEAK symbol<void(scriptInstance_t inst, BO4_AssetRef_t* name)> ScrVm_AddToArrayStringIndexed{ 0x14276F230_g };
WEAK symbol<void(scriptInstance_t inst, vec3_t* vec)> ScrVm_AddVector{ 0x14276F490_g };
WEAK symbol<void(scriptInstance_t inst)> ScrVar_PushArray{ 0x142775CF0_g };
WEAK symbol<const char* (ScrVarIndex_t index)> ScrStr_ConvertToString{ 0x142759030_g };
WEAK symbol<ScrVarIndex_t(scriptInstance_t inst, ScrVarIndex_t parentId, ScrVarNameIndex_t index)> ScrVar_NewVariableByIndex{ 0x142760440_g };
WEAK symbol<void(scriptInstance_t inst, ScrVarIndex_t id, ScrVarValue_t* value)> ScrVar_SetValue{ 0x1427616B0_g };
WEAK symbol<BuiltinFunction(uint32_t canonId, int* type, int* min_args, int* max_args)> CScr_GetFunction{ 0x141F13140_g };
WEAK symbol<BuiltinFunction(uint32_t canonId, int* type, int* min_args, int* max_args)> Scr_GetFunction{ 0x1433AF840_g };
WEAK symbol<void*(uint32_t canonId, int* type, int* min_args, int* max_args)> CScr_GetMethod{ 0x141F13650_g };
@ -749,13 +974,26 @@ namespace game
WEAK symbol<void(uint64_t code, scriptInstance_t inst, char* unused, bool terminal)> ScrVm_Error{ 0x142770330_g };
WEAK symbol<BO4_scrVarPub> scrVarPub{ 0x148307880_g };
WEAK symbol<BO4_scrVarGlob> scrVarGlob{ 0x148307830_g };
WEAK symbol<BO4_scrVmPub> scrVmPub{ 0x148307AA0_g };
WEAK symbol<VM_OP_FUNC> gVmOpJumpTable{ 0x144EED340_g };
WEAK symbol<uint32_t> gObjFileInfoCount{ 0x1482F76B0_g };
WEAK symbol<objFileInfo_t[SCRIPTINSTANCE_MAX][650]> gObjFileInfo{ 0x1482EFCD0_g };
// lua functions
WEAK symbol<bool(lua_state* luaVM, const char* file)> Lua_CoD_LoadLuaFile{ 0x143962DF0_g };
WEAK symbol<void(int code, const char* error, lua_state* s)> Lua_CoD_LuaStateManager_Error{ 0x14398A860_g };
WEAK symbol<const char*(lua_state* luaVM, hks_object* obj, size_t* len)> hks_obj_tolstring{ 0x143755730_g };
WEAK symbol<float(lua_state* luaVM, const hks_object* obj)> hks_obj_tonumber{ 0x143755A90_g };
// console labels
WEAK symbol<const char*> builtinLabels{ 0x144F11530_g };
// gsc types
WEAK symbol<const char*> var_typename{ 0x144EED240_g };
WEAK symbol<void(BO4_AssetRef_t* cmdName, xcommand_t function, cmd_function_t* allocedCmd)> 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
{
};
}

View File

@ -0,0 +1,185 @@
#include <std_include.hpp>
#include "definitions/game.hpp"
#include "definitions/xassets.hpp"
#include "game_runtime_errors.hpp"
namespace game::runtime_errors
{
namespace
{
std::unordered_map<uint64_t, const char*> 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>): 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>, <perk>): 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;
}
}

View File

@ -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);
}

View File

@ -1,19 +1,35 @@
#include <std_include.hpp>
#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);
}

View File

@ -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);
}

View File

@ -0,0 +1,91 @@
#include <std_include.hpp>
#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;
}
}

View File

@ -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<xasset_type_info> s_XAssetTypeInfo{ 0x14498BB00_g };
WEAK game::symbol<void(bg_cache_info* cache, int32_t flags)> Demo_AddBGCacheAndRegister{ 0x1405CF5A0_g };
WEAK game::symbol<int(BGCacheTypes type, game::BO4_AssetRef_t* name)> BG_Cache_RegisterAndGet{ 0x1405CEC20_g };
WEAK game::symbol<BGCacheTypes(const char* name)> 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<xasset_header(XAssetType type, game::BO4_AssetRef_t* name, bool errorIfMissing, int waittime)> DB_FindXAssetHeader{ 0x142EB75B0_g };
}