From 964ceb5be9f730b9ef8aceafa293a02bfaf02067 Mon Sep 17 00:00:00 2001 From: Federico Cecchetto Date: Sat, 19 Mar 2022 23:06:00 +0100 Subject: [PATCH] Custom font & material support --- src/client/component/filesystem.cpp | 56 ++++++ src/client/component/filesystem.hpp | 8 + src/client/component/fonts.cpp | 131 +++++++++++++ src/client/component/fonts.hpp | 7 + src/client/component/images.cpp | 7 +- src/client/component/materials.cpp | 200 ++++++++++++++++++++ src/client/component/materials.hpp | 7 + src/client/component/mods.cpp | 43 +++-- src/client/game/scripting/lua/engine.cpp | 9 +- src/client/game/structs.hpp | 88 +++++++++ src/client/game/symbols.hpp | 3 + src/client/game/ui_scripting/lua/engine.cpp | 9 +- 12 files changed, 542 insertions(+), 26 deletions(-) create mode 100644 src/client/component/filesystem.cpp create mode 100644 src/client/component/filesystem.hpp create mode 100644 src/client/component/fonts.cpp create mode 100644 src/client/component/fonts.hpp create mode 100644 src/client/component/materials.cpp create mode 100644 src/client/component/materials.hpp diff --git a/src/client/component/filesystem.cpp b/src/client/component/filesystem.cpp new file mode 100644 index 00000000..cba191cc --- /dev/null +++ b/src/client/component/filesystem.cpp @@ -0,0 +1,56 @@ +#include +#include "loader/component_loader.hpp" + +#include "filesystem.hpp" + +#include + +namespace filesystem +{ + std::unordered_set& get_search_paths() + { + static std::unordered_set search_paths{}; + return search_paths; + } + + std::string read_file(const std::string& path) + { + for (const auto& search_path : get_search_paths()) + { + const auto path_ = search_path + "/" + path; + if (utils::io::file_exists(path_)) + { + return utils::io::read_file(path_); + } + } + + return {}; + } + + bool read_file(const std::string& path, std::string* data) + { + for (const auto& search_path : get_search_paths()) + { + const auto path_ = search_path + "/" + path; + if (utils::io::read_file(path_, data)) + { + return true; + } + } + + return false; + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + get_search_paths().insert("."); + get_search_paths().insert("h2-mod"); + get_search_paths().insert("data"); + } + }; +} + +REGISTER_COMPONENT(filesystem::component) diff --git a/src/client/component/filesystem.hpp b/src/client/component/filesystem.hpp new file mode 100644 index 00000000..718e5377 --- /dev/null +++ b/src/client/component/filesystem.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace filesystem +{ + std::unordered_set& get_search_paths(); + std::string read_file(const std::string& path); + bool read_file(const std::string& path, std::string* data); +} \ No newline at end of file diff --git a/src/client/component/fonts.cpp b/src/client/component/fonts.cpp new file mode 100644 index 00000000..bdd16e53 --- /dev/null +++ b/src/client/component/fonts.cpp @@ -0,0 +1,131 @@ +#include +#include "loader/component_loader.hpp" + +#include "fonts.hpp" +#include "game_console.hpp" +#include "filesystem.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include +#include +#include +#include + +namespace fonts +{ + namespace + { + struct font_data_t + { + std::unordered_map fonts; + std::unordered_map raw_fonts; + }; + + utils::concurrency::container font_data; + + game::TTF* create_font(const std::string& name, const std::string& data) + { + const auto font = utils::memory::get_allocator()->allocate(); + font->name = utils::memory::get_allocator()->duplicate_string(name); + font->buffer = utils::memory::get_allocator()->duplicate_string(data); + font->len = static_cast(data.size()); + font->fontFace = 0; + return font; + } + + void free_font(game::TTF* font) + { + utils::memory::get_allocator()->free(font->buffer); + utils::memory::get_allocator()->free(font->name); + utils::memory::get_allocator()->free(font); + } + + game::TTF* load_font(const std::string& name) + { + return font_data.access([&](font_data_t& data_) -> game::TTF* + { + if (const auto i = data_.fonts.find(name); i != data_.fonts.end()) + { + return i->second; + } + + std::string data{}; + if (const auto i = data_.raw_fonts.find(name); i != data_.raw_fonts.end()) + { + data = i->second; + } + + if (data.empty() && !filesystem::read_file(name, &data)) + { + return nullptr; + } + + const auto material = create_font(name, data); + data_.fonts[name] = material; + + return material; + }); + } + + game::TTF* try_load_font(const std::string& name) + { + try + { + return load_font(name); + } + catch (const std::exception& e) + { + game_console::print(game_console::con_type_error, "Failed to load font %s: %s\n", name.data(), e.what()); + } + + return nullptr; + } + + game::TTF* db_find_xasset_header_stub(game::XAssetType type, const char* name, int create_default) + { + auto result = try_load_font(name); + if (result == nullptr) + { + result = game::DB_FindXAssetHeader(type, name, create_default).ttf; + } + return result; + } + } + + void add(const std::string& name, const std::string& data) + { + font_data.access([&](font_data_t& data_) + { + data_.raw_fonts[name] = data; + }); + } + + void clear() + { + font_data.access([&](font_data_t& data_) + { + for (auto& font : data_.fonts) + { + free_font(font.second); + } + + data_.fonts.clear(); + utils::hook::set(0x14EE3ACB8, 0); // reset registered font count + }); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + utils::hook::call(0x140747096, db_find_xasset_header_stub); + } + }; +} + +REGISTER_COMPONENT(fonts::component) diff --git a/src/client/component/fonts.hpp b/src/client/component/fonts.hpp new file mode 100644 index 00000000..c2d22869 --- /dev/null +++ b/src/client/component/fonts.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace fonts +{ + void add(const std::string& name, const std::string& data); + void clear(); +} diff --git a/src/client/component/images.cpp b/src/client/component/images.cpp index e3bfbd61..ff7bcf8e 100644 --- a/src/client/component/images.cpp +++ b/src/client/component/images.cpp @@ -1,8 +1,11 @@ #include #include "loader/component_loader.hpp" -#include "game/game.hpp" + #include "images.hpp" #include "game_console.hpp" +#include "filesystem.hpp" + +#include "game/game.hpp" #include #include @@ -29,7 +32,7 @@ namespace images } }); - if (data.empty() && !utils::io::read_file(utils::string::va("images/%s.png", image->name), &data)) + if (data.empty() && !filesystem::read_file(utils::string::va("images/%s.png", image->name), &data)) { return {}; } diff --git a/src/client/component/materials.cpp b/src/client/component/materials.cpp new file mode 100644 index 00000000..d84a6b93 --- /dev/null +++ b/src/client/component/materials.cpp @@ -0,0 +1,200 @@ +#include +#include "loader/component_loader.hpp" + +#include "materials.hpp" +#include "game_console.hpp" +#include "filesystem.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include +#include +#include +#include + +namespace materials +{ + namespace + { + utils::hook::detour db_material_streaming_fail_hook; + utils::hook::detour material_register_handle_hook; + utils::hook::detour db_get_material_index_hook; + + struct material_data_t + { + std::unordered_map materials; + std::unordered_map images; + }; + + char constant_table[0x20] = {}; + + utils::concurrency::container material_data; + + game::GfxImage* setup_image(game::GfxImage* image, const utils::image& raw_image) + { + image->imageFormat = 0x1000003; + image->resourceSize = -1; + + D3D11_SUBRESOURCE_DATA data{}; + data.SysMemPitch = raw_image.get_width() * 4; + data.SysMemSlicePitch = data.SysMemPitch * raw_image.get_height(); + data.pSysMem = raw_image.get_buffer(); + + game::Image_Setup(image, raw_image.get_width(), raw_image.get_height(), image->depth, image->numElements, + image->imageFormat, DXGI_FORMAT_R8G8B8A8_UNORM, 0, image->name, &data); + + return image; + } + + game::Material* create_material(const std::string& name, const std::string& data) + { + const auto white = *reinterpret_cast(0x141B09208); + + const auto material = utils::memory::get_allocator()->allocate(); + const auto texture_table = utils::memory::get_allocator()->allocate(); + const auto image = utils::memory::get_allocator()->allocate(); + + std::memcpy(material, white, sizeof(game::Material)); + std::memcpy(texture_table, white->textureTable, sizeof(game::MaterialTextureDef)); + std::memcpy(image, white->textureTable->u.image, sizeof(game::GfxImage)); + + material->constantTable = &constant_table; + material->name = utils::memory::get_allocator()->duplicate_string(name); + image->name = material->name; + + material->textureTable = texture_table; + material->textureTable->u.image = setup_image(image, data); + + return material; + } + + void free_material(game::Material* material) + { + material->textureTable->u.image->textures.___u0.map->Release(); + material->textureTable->u.image->textures.shaderView->Release(); + utils::memory::get_allocator()->free(material->textureTable->u.image); + utils::memory::get_allocator()->free(material->textureTable); + utils::memory::get_allocator()->free(material->name); + utils::memory::get_allocator()->free(material); + } + + game::Material* load_material(const std::string& name) + { + return material_data.access([&](material_data_t& data_) -> game::Material* + { + if (const auto i = data_.materials.find(name); i != data_.materials.end()) + { + return i->second; + } + + std::string data{}; + if (const auto i = data_.images.find(name); i != data_.images.end()) + { + data = i->second; + } + + if (data.empty() && !filesystem::read_file(utils::string::va("materials/%s.png", name.data()), &data)) + { + data_.materials[name] = nullptr; + return nullptr; + } + + const auto material = create_material(name, data); + data_.materials[name] = material; + + return material; + }); + } + + game::Material* try_load_material(const std::string& name) + { + if (name == "white") + { + return nullptr; + } + + try + { + return load_material(name); + } + catch (const std::exception& e) + { + game_console::print(game_console::con_type_error, "Failed to load material %s: %s\n", name.data(), e.what()); + } + + return nullptr; + } + + game::Material* material_register_handle_stub(const char* name) + { + auto result = try_load_material(name); + if (result == nullptr) + { + result = material_register_handle_hook.invoke(name); + } + return result; + } + + int db_material_streaming_fail_stub(game::Material* material) + { + if (material->constantTable == &constant_table) + { + return 0; + } + + return db_material_streaming_fail_hook.invoke(material); + } + + unsigned int db_get_material_index_stub(game::Material* material) + { + if (material->constantTable == &constant_table) + { + return 0; + } + + return db_get_material_index_hook.invoke(material); + } + } + + void add(const std::string& name, const std::string& data) + { + material_data.access([&](material_data_t& data_) + { + data_.images[name] = data; + }); + } + + void clear() + { + material_data.access([&](material_data_t& data_) + { + for (auto& material : data_.materials) + { + if (material.second == nullptr) + { + continue; + } + + free_material(material.second); + } + + data_.materials.clear(); + }); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + material_register_handle_hook.create(game::Material_RegisterHandle.get(), material_register_handle_stub); + db_material_streaming_fail_hook.create(0x14041D140, db_material_streaming_fail_stub); + db_get_material_index_hook.create(0x140413BC0, db_get_material_index_stub); + } + }; +} + +REGISTER_COMPONENT(materials::component) diff --git a/src/client/component/materials.hpp b/src/client/component/materials.hpp new file mode 100644 index 00000000..3a548548 --- /dev/null +++ b/src/client/component/materials.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace materials +{ + void add(const std::string& name, const std::string& data); + void clear(); +} diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp index 32b58052..ae78b9e7 100644 --- a/src/client/component/mods.cpp +++ b/src/client/component/mods.cpp @@ -6,12 +6,35 @@ #include "command.hpp" #include "game_console.hpp" #include "scheduler.hpp" +#include "filesystem.hpp" +#include "materials.hpp" +#include "fonts.hpp" #include #include namespace mods { + namespace + { + utils::hook::detour db_release_xassets_hook; + + void db_release_xassets_stub() + { + materials::clear(); + fonts::clear(); + db_release_xassets_hook.invoke(); + } + + void restart() + { + scheduler::once([]() + { + game::Com_Shutdown(""); + }, scheduler::pipeline::main); + } + } + class component final : public component_interface { public: @@ -22,6 +45,8 @@ namespace mods utils::io::create_directory("mods"); } + db_release_xassets_hook.create(0x140416A80, db_release_xassets_stub); + command::add("loadmod", [](const command::params& params) { if (params.size() < 2) @@ -30,7 +55,7 @@ namespace mods return; } - if (::game::SV_Loaded()) + if (!game::Com_InFrontend()) { game_console::print(game_console::con_type_error, "Cannot load mod while in-game!\n"); game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); @@ -46,12 +71,9 @@ namespace mods return; } + filesystem::get_search_paths().insert(path); game::mod_folder = path; - - scheduler::once([]() - { - command::execute("lui_restart", true); - }, scheduler::pipeline::renderer); + restart(); }); command::add("unloadmod", [](const command::params& params) @@ -62,7 +84,7 @@ namespace mods return; } - if (::game::SV_Loaded()) + if (!game::Com_InFrontend()) { game_console::print(game_console::con_type_error, "Cannot unload mod while in-game!\n"); game::CG_GameMessage(0, "^1Cannot unload mod while in-game!"); @@ -70,12 +92,9 @@ namespace mods } game_console::print(game_console::con_type_info, "Unloading mod %s\n", game::mod_folder.data()); + filesystem::get_search_paths().erase(game::mod_folder); game::mod_folder.clear(); - - scheduler::once([]() - { - command::execute("lui_restart", true); - }, scheduler::pipeline::renderer); + restart(); }); } }; diff --git a/src/client/game/scripting/lua/engine.cpp b/src/client/game/scripting/lua/engine.cpp index 58008313..d9f0ae0e 100644 --- a/src/client/game/scripting/lua/engine.cpp +++ b/src/client/game/scripting/lua/engine.cpp @@ -3,6 +3,7 @@ #include "context.hpp" #include "../../../component/notifies.hpp" +#include "../../../component/filesystem.hpp" #include "../execution.hpp" #include @@ -49,13 +50,9 @@ namespace scripting::lua::engine load_generic_script(); - load_scripts("scripts/"); - load_scripts("h2-mod/scripts/"); - load_scripts("data/scripts/"); - - if (!game::mod_folder.empty()) + for (const auto& path : filesystem::get_search_paths()) { - load_scripts(utils::string::va("%s/scripts/", game::mod_folder.data())); + load_scripts(path + "/scripts/"); } } diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 291203a8..8c3017c2 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -126,11 +126,90 @@ namespace game // ... }; + struct GfxImage; + + union MaterialTextureDefInfo + { + GfxImage* image; + void* water; + }; + + struct MaterialTextureDef + { + unsigned int nameHash; + char nameStart; + char nameEnd; + char samplerState; + char semantic; + MaterialTextureDefInfo u; + }; + + struct MaterialPass + { + void* vertexShader; + void* vertexDecl; + void* hullShader; + void* domainShader; + void* pixelShader; + char pixelOutputMask; + char perPrimArgCount; + char perObjArgCount; + char stableArgCount; + unsigned __int16 perPrimArgSize; + unsigned __int16 perObjArgSize; + unsigned __int16 stableArgSize; + char zone; + char perPrimConstantBuffer; + char perObjConstantBuffer; + char stableConstantBuffer; + unsigned int customBufferFlags; + char customSamplerFlags; + char precompiledIndex; + char stageConfig; + void* args; + }; + + struct MaterialTechnique + { + const char* name; + unsigned __int16 flags; + unsigned __int16 passCount; + MaterialPass passArray[1]; + }; + + struct MaterialTechniqueSet + { + const char* name; + unsigned __int16 flags; + char worldVertFormat; + char preDisplacementOnlyCount; + MaterialTechnique* techniques[309]; + }; + + struct GfxStateBits + { + unsigned int loadBits[3]; + char zone; + char depthStencilState[11]; + char blendState; + char rasterizerState; + }; + struct Material { const char* name; + char __pad0[0x124]; + char textureCount; + char __pad1[0xB]; + MaterialTechniqueSet* techniqueSet; + MaterialTextureDef* textureTable; + void* constantTable; + GfxStateBits* stateBitsTable; + char __pad2[0x118]; }; + static_assert(sizeof(Material) == 0x270); + struct point { float x; @@ -678,6 +757,14 @@ namespace game const char* buffer; }; + struct TTF + { + const char* name; + int len; + const char* buffer; + int fontFace; + }; + union XAssetHeader { void* data; @@ -687,6 +774,7 @@ namespace game ScriptFile* scriptfile; StringTable* stringTable; LuaFile* luaFile; + TTF* ttf; }; struct XAsset diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 6abaeafa..920ab7d2 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -27,7 +27,9 @@ namespace game WEAK symbol Cmd_ExecuteSingleCommand{0x14059ABA0}; WEAK symbol Com_Error{0x1405A2D80}; + WEAK symbol Com_Shutdown{0x1405A62C0}; WEAK symbol Com_Quit_f{0x1405A50D0}; + WEAK symbol Com_InFrontend{0x140328BD0}; WEAK symbol Quit{0x1405A52A0}; WEAK symbol @@ -117,6 +119,7 @@ namespace game WEAK symbol R_RegisterFont{0x140746FE0}; WEAK symbol R_TextWidth{0x1407472A0}; WEAK symbol R_SyncRenderThread{0x14076E7D0}; + WEAK symbol R_WaitWorkerCmds{0x140794330}; WEAK symbol R_AddDObjToScene{0x140775C40}; diff --git a/src/client/game/ui_scripting/lua/engine.cpp b/src/client/game/ui_scripting/lua/engine.cpp index b204a78b..34fde3d1 100644 --- a/src/client/game/ui_scripting/lua/engine.cpp +++ b/src/client/game/ui_scripting/lua/engine.cpp @@ -4,6 +4,7 @@ #include "../../../component/scheduler.hpp" #include "../../../component/ui_scripting.hpp" +#include "../../../component/filesystem.hpp" #include #include @@ -73,13 +74,9 @@ namespace ui_scripting::lua::engine load_code(updater_script); - load_scripts("ui_scripts/"); - load_scripts("h2-mod/ui_scripts/"); - load_scripts("data/ui_scripts/"); - - if (!game::mod_folder.empty()) + for (const auto& path : filesystem::get_search_paths()) { - load_scripts(utils::string::va("%s/ui_scripts/", game::mod_folder.data())); + load_scripts(path + "/ui_scripts/"); } }