diff --git a/src/client/component/loadscreen.cpp b/src/client/component/loadscreen.cpp new file mode 100644 index 00000000..9a7471fc --- /dev/null +++ b/src/client/component/loadscreen.cpp @@ -0,0 +1,246 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include "scheduler.hpp" +#include "loadscreen.hpp" + +#include +#include + +namespace loadscreen +{ + namespace + { + game::dvar_t* cl_disable_map_movies = nullptr; + game::dvar_t* cl_loadscreen_image = nullptr; + game::dvar_t* cl_loadscreen_title = nullptr; + game::dvar_t* cl_loadscreen_desc = nullptr; + game::dvar_t* cl_loadscreen_obj = nullptr; + game::dvar_t* cl_loadscreen_obj_icon = nullptr; + + utils::hook::detour ui_draw_loadbar_hook; + + float white_color[4] = {0.8f, 0.8f, 0.8f, 1.f}; + float text_color[4] = {0.65f, 0.65f, 0.65f, 1.f}; + float gray_color[4] = {0.2f, 0.2f, 0.2f, 1.f}; + float icon_yellow_color[4] = {0.86f, 0.81f, 0.34f, 1.f}; + float icon_grey_color[4] = {0.6f, 0.6f, 0.6f, 1.f}; + + void draw_loadscreen_image() + { + const auto material = game::Material_RegisterHandle(cl_loadscreen_image->current.string); + const auto placement = game::ScrPlace_GetViewPlacement(); + + game::rectangle rect{}; + rect.p0.x = 0; + rect.p0.y = 0; + rect.p0.f2 = 0.f; + rect.p0.f3 = 1.f; + + rect.p1.x = 0 + placement->realViewportSize[0]; + rect.p1.y = 0; + rect.p1.f2 = 0.f; + rect.p1.f3 = 1.f; + + rect.p2.x = 0 + placement->realViewportSize[0]; + rect.p2.y = 0 + placement->realViewportSize[1]; + rect.p2.f2 = 0.f; + rect.p2.f3 = 1.f; + + rect.p3.x = 0; + rect.p3.y = 0 + placement->realViewportSize[1]; + rect.p3.f2 = 0.f; + rect.p3.f3 = 1.f; + + game::R_DrawRectangle(&rect, 0.f, 0.f, 1.f, 1.f, + white_color, material); + } + + void draw_loadscreen_progress_bar() + { + const auto fraction = utils::hook::invoke(0x140287E30); + + const auto w = 290.f; + const auto w_progress = w * fraction; + + const auto material = game::Material_RegisterHandle("white"); + const auto placement = game::ScrPlace_GetViewPlacement(); + + game::CL_DrawStretchPic(placement, -20, 290, w, 3, 0, 0, 0.0f, 0.0f, 1.0f, 1.0f, gray_color, material); + game::CL_DrawStretchPic(placement, -20, 290, w_progress, 3, 0, 0, 0.0f, 0.0f, 1.0f, 1.0f, white_color, material); + } + + void draw_loadscreen_title() + { + auto x = -20.f; + auto y = 290.f; + auto h = 24.f; + auto w = 0.f; + + const auto placement = game::ScrPlace_GetViewPlacement(); + game::ScrPlace_ApplyRect(placement, &x, &y, &w, &h, 0, 0); + + const auto font = game::R_RegisterFont("fonts/default.otf", static_cast(h)); + game::R_AddCmdDrawText(cl_loadscreen_title->current.string, 0x7FFFFFFF, font, x, y, 1.f, 1.f, 0.f, text_color, 0); + } + + void draw_loadscreen_desc() + { + const auto font = game::R_RegisterFont("fonts/default.otf", 20); + const auto placement = game::ScrPlace_GetViewPlacement(); + const auto text = cl_loadscreen_desc->current.string; + + game::rectDef_s rect{}; + rect.x = 0; + rect.y = 0; + rect.w = 290; + rect.horzAlign = 0; + rect.vertAlign = 0; + + game::rectDef_s text_rect{}; + + game::UI_DrawWrappedText(placement, text, &rect, font, -20, 310, 0.25f, text_color, 0, 0, &text_rect, 0); + } + + void draw_loadscreen_objective_icons() + { + if (*cl_loadscreen_obj_icon->current.string == 0) + { + return; + } + + const auto material = game::Material_RegisterHandle(cl_loadscreen_obj_icon->current.string); + const auto placement = game::ScrPlace_GetViewPlacement(); + + const auto w = 15.f; + const auto base_y = 365.f; + const auto base_x = -20.f; + + for (auto row = 0; row < 3; row++) + { + for (auto column = 0; column < 3; column++) + { + auto x = base_x + column * w; + auto y = base_y + row * w + 2; + + const auto color = column <= row ? icon_yellow_color : icon_grey_color; + game::CL_DrawStretchPic(placement, x, y, w, w, 0, 0, 0.f, 0.f, 1.f, 1.f, color, material); + } + } + } + + void draw_loadscreen_objective() + { + if (*cl_loadscreen_obj->current.string == 0) + { + return; + } + + draw_loadscreen_objective_icons(); + + const auto font = game::R_RegisterFont("fonts/default.otf", 20); + const auto placement = game::ScrPlace_GetViewPlacement(); + const auto text = cl_loadscreen_obj->current.string; + + game::rectDef_s rect{}; + rect.x = 0; + rect.y = 0; + rect.w = 290.f; + rect.horzAlign = 0; + rect.vertAlign = 0; + + game::rectDef_s text_rect{}; + + game::UI_DrawWrappedText(placement, text, &rect, font, 30.f, 365.f + 17.5f, 0.25f, text_color, 0, 0, &text_rect, 0); + } + + void draw_loadscreen() + { + if (!cl_disable_map_movies->current.enabled) + { + return; + } + + if (cl_loadscreen_image == nullptr || cl_loadscreen_title == nullptr || + cl_loadscreen_desc == nullptr || *cl_loadscreen_image->current.string == 0) + { + return; + } + + draw_loadscreen_image(); + draw_loadscreen_progress_bar(); + draw_loadscreen_title(); + draw_loadscreen_desc(); + draw_loadscreen_objective(); + } + + bool in_loadscreen() + { + return *reinterpret_cast(0x14203F3C4) == 4; + } + + void ui_set_active_menu_stub(utils::hook::assembler& a) + { + const auto player_start = a.newLabel(); + + a.mov(rax, qword_ptr(reinterpret_cast(&cl_disable_map_movies))); + a.mov(al, byte_ptr(rax, 0x10)); + a.cmp(al, 1); + a.jz(player_start); + + a.mov(rax, qword_ptr(static_cast(0x14BE6EA10))); + a.mov(al, byte_ptr(rax, 0x10)); + a.cmp(al, 1); + a.jz(player_start); + + a.jmp(0x1405F4701); + + a.bind(player_start); + a.call(0x1405F1B00); + a.jmp(0x1405F44F2); + } + } + + void clear() + { + game::Dvar_Reset(cl_disable_map_movies, game::DVAR_SOURCE_INTERNAL); + game::Dvar_Reset(cl_loadscreen_image, game::DVAR_SOURCE_INTERNAL); + game::Dvar_Reset(cl_loadscreen_title, game::DVAR_SOURCE_INTERNAL); + game::Dvar_Reset(cl_loadscreen_desc, game::DVAR_SOURCE_INTERNAL); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + // not registered, used in CL_StartLoading + cl_disable_map_movies = dvars::register_bool("cl_disableMapMovies", false, 0, "Disable map loading videos"); + + // auto start the game if cl_disableMapMovies is enabled + utils::hook::jump(0x1405F46EA, utils::hook::assemble(ui_set_active_menu_stub), true); + + scheduler::once([]() + { + cl_loadscreen_image = dvars::register_string("cl_loadscreenImage", "", 0, "Loadscreen background image"); + cl_loadscreen_title = dvars::register_string("cl_loadscreenTitle", "", 0, "Loadscreen mission title"); + cl_loadscreen_desc = dvars::register_string("cl_loadscreenDesc", "", 0, "Loadscreen mission description"); + cl_loadscreen_obj = dvars::register_string("cl_loadscreenObj", "", 0, "Loadscreen mission objective"); + cl_loadscreen_obj_icon = dvars::register_string("cl_loadscreenObjIcon", "", 0, "Loadscreen mission objective icon"); + }, scheduler::pipeline::main); + + scheduler::loop([]() + { + if (in_loadscreen()) + { + draw_loadscreen(); + } + }, scheduler::pipeline::renderer); + } + }; +} + +REGISTER_COMPONENT(loadscreen::component) diff --git a/src/client/component/loadscreen.hpp b/src/client/component/loadscreen.hpp new file mode 100644 index 00000000..02b1aed1 --- /dev/null +++ b/src/client/component/loadscreen.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace loadscreen +{ + void clear(); +} diff --git a/src/client/component/logger.cpp b/src/client/component/logger.cpp index fb9d82ab..5f5fe60b 100644 --- a/src/client/component/logger.cpp +++ b/src/client/component/logger.cpp @@ -1,8 +1,10 @@ #include #include "loader/component_loader.hpp" -#include "game/game.hpp" #include "console.hpp" +#include "loadscreen.hpp" + +#include "game/game.hpp" #include "game/dvars.hpp" #include @@ -60,6 +62,8 @@ namespace logger console::error("Error: %s\n", buffer); } + loadscreen::clear(); + com_error_hook.invoke(error, "%s", buffer); } diff --git a/src/client/component/materials.cpp b/src/client/component/materials.cpp index c9417280..d71d114f 100644 --- a/src/client/component/materials.cpp +++ b/src/client/component/materials.cpp @@ -4,6 +4,7 @@ #include "materials.hpp" #include "console.hpp" #include "filesystem.hpp" +#include "command.hpp" #include "game/game.hpp" #include "game/dvars.hpp" @@ -49,7 +50,7 @@ namespace materials return image; } - game::Material* create_material(const std::string& name, const std::string& data) + game::Material* create_material(const std::string& name, const utils::image& raw_image) { const auto white = *reinterpret_cast(0x141B09208); @@ -66,7 +67,7 @@ namespace materials image->name = material->name; material->textureTable = texture_table; - material->textureTable->u.image = setup_image(image, data); + material->textureTable->u.image = setup_image(image, raw_image); return material; } @@ -96,16 +97,37 @@ namespace materials data = i->second; } - if (data.empty() && !filesystem::read_file(utils::string::va("materials/%s.png", name.data()), &data)) + if (!data.empty()) { - data_.materials[name] = nullptr; - return nullptr; + const auto material = create_material(name, data); + data_.materials[name] = material; + return material; } - const auto material = create_material(name, data); - data_.materials[name] = material; + if (filesystem::read_file(utils::string::va("materials/%s.stbi_img", name.data()), &data)) + { + const auto buffer = data.data(); + const auto width = *reinterpret_cast(buffer); + const auto height = *reinterpret_cast(buffer + 4); + const auto image_data = std::string(reinterpret_cast(buffer + 8), data.size() - 8); - return material; + const auto image = utils::image(image_data, width, height); + + const auto material = create_material(name, image); + data_.materials[name] = material; + + return material; + } + + if (filesystem::read_file(utils::string::va("materials/%s.png", name.data()), &data)) + { + const auto material = create_material(name, data); + data_.materials[name] = material; + return material; + } + + data_.materials[name] = nullptr; + return nullptr; }); } @@ -193,6 +215,54 @@ namespace materials 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); + + command::add("preloadImage", [](const command::params& params) + { + if (params.size() < 2) + { + return; + } + + const auto image_name = params.join(1); + if (!utils::io::file_exists(image_name)) + { + console::error("Image file not found\n"); + return; + } + + const auto data = utils::io::read_file(image_name); + + try + { + const auto image = utils::image{ data }; + const auto last_of = image_name.find_last_of('.'); + const auto new_name = image_name.substr(0, last_of) + ".stbi_img"; + + auto width = image.get_width(); + auto height = image.get_height(); + auto size = image.get_size(); + + /* + int width; + int height; + char* data; + */ + + std::string buffer{}; + buffer.append(reinterpret_cast(&width), 4); + buffer.append(reinterpret_cast(&height), 4); + buffer.append(image.get_data()); + + utils::io::write_file(new_name, buffer, false); + + console::info("Image saved to %s\n", new_name.data()); + } + catch (const std::exception& e) + { + console::error("Error processing image: %s\n", e.what()); + } + + }); } }; } diff --git a/src/client/component/mods.cpp b/src/client/component/mods.cpp index 62a158f5..9a2e2e37 100644 --- a/src/client/component/mods.cpp +++ b/src/client/component/mods.cpp @@ -12,6 +12,7 @@ #include "mods.hpp" #include "mapents.hpp" #include "localized_strings.hpp" +#include "loadscreen.hpp" #include #include @@ -32,6 +33,7 @@ namespace mods materials::clear(); fonts::clear(); mapents::clear_dvars(); + loadscreen::clear(); } mapents::clear(); diff --git a/src/client/game/structs.hpp b/src/client/game/structs.hpp index 77f11014..2a0907b7 100644 --- a/src/client/game/structs.hpp +++ b/src/client/game/structs.hpp @@ -1211,6 +1211,16 @@ namespace game char __pad0[0x8]; }; + struct rectDef_s + { + float x; + float y; + float w; + float h; + int horzAlign; + int vertAlign; + }; + namespace hks { struct lua_State; diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 8b3cd8b5..8d03e5d0 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -23,6 +23,9 @@ namespace game WEAK symbol CG_GetWeaponDisplayName{0x1403B9210}; + WEAK symbol CL_DrawStretchPic{0x1403C9570}; + WEAK symbol Cmd_AddCommandInternal{0x14059A5F0}; WEAK symbol Cmd_ExecuteSingleCommand{0x14059ABA0}; @@ -58,6 +61,7 @@ namespace game WEAK symbol Dvar_SetCommand{0x14061A5C0}; WEAK symbol Dvar_SetFromStringFromSource{0x14061A910}; WEAK symbol Dvar_SetString{0x14061ABF0}; + WEAK symbol Dvar_Reset{0x140619FE0}; WEAK symbol generateHashValue{0x140343D20}; @@ -143,6 +147,8 @@ namespace game WEAK symbol ScrPlace_GetViewPlacement{0x1403E16A0}; WEAK symbol ScrPlace_GetView{0x1403E1660}; + WEAK symbol ScrPlace_ApplyRect{0x1403E0BF0}; WEAK symbol SL_ConvertToString{0x1405BFBB0}; WEAK symbol SL_GetString{0x1405C0170}; @@ -162,6 +168,8 @@ namespace game WEAK symbol UI_SafeTranslateString{0x1405A2930}; WEAK symbol UI_PlayLocalSoundAlias{0x140606080}; + WEAK symbol UI_DrawWrappedText{0x1406055E0}; WEAK symbol PM_playerTrace{0x14068F0A0}; diff --git a/src/common/utils/image.cpp b/src/common/utils/image.cpp index 33f865e2..10465927 100644 --- a/src/common/utils/image.cpp +++ b/src/common/utils/image.cpp @@ -26,6 +26,13 @@ namespace utils std::memmove(this->data.data(), rgb_image, size); } + image::image(const std::string& data_, int width_, int height_) + : data(data_) + , width(width_) + , height(height_) + { + } + int image::get_width() const { return this->width; diff --git a/src/common/utils/image.hpp b/src/common/utils/image.hpp index a617df76..071c52a9 100644 --- a/src/common/utils/image.hpp +++ b/src/common/utils/image.hpp @@ -8,6 +8,7 @@ namespace utils { public: image(const std::string& data); + image(const std::string& data_, int width_, int height_); int get_width() const; int get_height() const;