#include #include "console.hpp" #include "loader/component_loader.hpp" #include "resource.hpp" #include "game/game.hpp" #include "scheduler.hpp" #include #include #include #include #define CONSOLE_BUFFER_SIZE 16384 #define WINDOW_WIDTH 608 namespace console { void set_title(const std::string& title) { SetWindowTextA(*game::s_wcd::hWnd, title.data()); } namespace { utils::image::object logo; std::atomic_bool started{false}; std::atomic_bool terminate_runner{false}; utils::concurrency::container> message_queue{}; void print_message(const char* message) { #ifndef NDEBUG OutputDebugStringA(message); #endif if (started && !terminate_runner) { game::Com_Printf(0, 0, "%s", message); } } void queue_message(const char* message) { message_queue.access([message](std::queue& queue) { queue.push(message); }); } void print_message_to_console(const char* message) { static auto print_func = utils::hook::assemble([](utils::hook::assembler& a) { a.push(rbx); a.mov(eax, 0x8030); a.jmp(game::select(0x142332AA7, 0x140597527)); }); static_cast(print_func)(message); } std::queue empty_message_queue() { std::queue current_queue{}; message_queue.access([&](std::queue& queue) { current_queue = std::move(queue); queue = {}; }); return current_queue; } void print_stub(const char* fmt, ...) { va_list ap; va_start(ap, fmt); char buffer[1024]{0}; const int res = vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, fmt, ap); (void)res; print_message(buffer); va_end(ap); } INT_PTR get_gray_brush() { static utils::image::object b(CreateSolidBrush(RGB(50, 50, 50))); return reinterpret_cast(b.get()); } LRESULT con_wnd_proc(const HWND hwnd, const UINT msg, const WPARAM wparam, const LPARAM lparam) { switch (msg) { case WM_CTLCOLOREDIT: case WM_CTLCOLORSTATIC: SetBkColor(reinterpret_cast(wparam), RGB(50, 50, 50)); SetTextColor(reinterpret_cast(wparam), RGB(232, 230, 227)); return get_gray_brush(); case WM_CLOSE: game::Cbuf_AddText(0, "quit\n"); [[fallthrough]]; default: return utils::hook::invoke(game::select(0x142332960, 0x1405973E0), hwnd, msg, wparam, lparam); } } LRESULT input_line_wnd_proc(const HWND hwnd, const UINT msg, const WPARAM wparam, const LPARAM lparam) { return utils::hook::invoke(game::select(0x142332C60, 0x1405976E0), hwnd, msg, wparam, lparam); } void sys_create_console_stub(const HINSTANCE h_instance) { char text[CONSOLE_BUFFER_SIZE]{0}; const auto* class_name = "BOIII WinConsole"; const auto* window_name = game::is_server() ? "BOIII Server" : "BOIII Console"; WNDCLASSA wnd_class{}; wnd_class.style = 0; wnd_class.lpfnWndProc = con_wnd_proc; wnd_class.cbClsExtra = 0; wnd_class.cbWndExtra = 0; wnd_class.hInstance = h_instance; wnd_class.hIcon = LoadIconA(h_instance, reinterpret_cast(1)); wnd_class.hCursor = LoadCursorA(nullptr, reinterpret_cast(0x7F00)); wnd_class.hbrBackground = CreateSolidBrush(RGB(50, 50, 50)); wnd_class.lpszMenuName = nullptr; wnd_class.lpszClassName = class_name; if (!RegisterClassA(&wnd_class)) { return; } RECT rect{}; rect.left = 0; rect.right = 620; rect.top = 0; rect.bottom = 450; AdjustWindowRect(&rect, 0x80CA0000, 0); auto dc = GetDC(GetDesktopWindow()); const auto swidth = GetDeviceCaps(dc, 8); const auto sheight = GetDeviceCaps(dc, 10); ReleaseDC(GetDesktopWindow(), dc); utils::hook::set(game::s_wcd::windowWidth, (rect.right - rect.left + 1)); utils::hook::set(game::s_wcd::windowHeight, (rect.bottom - rect.top + 1)); utils::hook::set(game::s_wcd::hWnd, CreateWindowExA( 0, class_name, window_name, 0x80CA0000, (swidth - 600) / 2, (sheight - 450) / 2, rect.right - rect.left + 1, rect.bottom - rect.top + 1, nullptr, nullptr, h_instance, nullptr)); if (!*game::s_wcd::hWnd) { return; } // create fonts dc = GetDC(*game::s_wcd::hWnd); const auto n_height = MulDiv(8, GetDeviceCaps(dc, 90), 72); utils::hook::set(game::s_wcd::hfBufferFont, CreateFontA( -n_height, 0, 0, 0, 300, 0, 0, 0, 1u, 0, 0, 0, 0x31u, "Courier New")); ReleaseDC(*game::s_wcd::hWnd, dc); if (logo) { utils::hook::set(game::s_wcd::codLogo, CreateWindowExA( 0, "Static", nullptr, 0x5000000Eu, 5, 5, 0, 0, *game::s_wcd::hWnd, reinterpret_cast(1), h_instance, nullptr)); SendMessageA(*game::s_wcd::codLogo, STM_SETIMAGE, IMAGE_BITMAP, logo); } // create the input line utils::hook::set(game::s_wcd::hwndInputLine, CreateWindowExA( 0, "edit", nullptr, 0x50800080u, 6, 400, WINDOW_WIDTH, 20, *game::s_wcd::hWnd, reinterpret_cast(0x65), h_instance, nullptr)); utils::hook::set(game::s_wcd::hwndBuffer, CreateWindowExA( 0, "edit", nullptr, 0x50A00844u, 6, 70, WINDOW_WIDTH, 324, *game::s_wcd::hWnd, reinterpret_cast(0x64), h_instance, nullptr)); SendMessageA(*game::s_wcd::hwndBuffer, WM_SETFONT, reinterpret_cast(*game::s_wcd::hfBufferFont), 0); utils::hook::set(game::s_wcd::SysInputLineWndProc, reinterpret_cast(SetWindowLongPtrA( *game::s_wcd::hwndInputLine, -4, reinterpret_cast(input_line_wnd_proc)))); SendMessageA(*game::s_wcd::hwndInputLine, WM_SETFONT, reinterpret_cast(*game::s_wcd::hfBufferFont), 0); SetFocus(*game::s_wcd::hwndInputLine); game::Con_GetTextCopy(text, std::min(0x4000, static_cast(sizeof(text)))); SetWindowTextA(*game::s_wcd::hwndBuffer, text); } } struct component final : generic_component { void post_unpack() override { utils::hook::jump(printf, print_stub); if (!game::is_server()) { utils::hook::set(0x14133D2FE_g, 0xEB); // Always enable ingame console } utils::hook::jump(game::select(0x142332C30, 0x1405976B0), queue_message); utils::hook::nop(game::select(0x142332C4A, 0x1405976CA), 2); // Print from every thread //const auto self = utils::nt::library::get_by_address(sys_create_console_stub); //logo = LoadImageA(self.get_handle(), MAKEINTRESOURCEA(IMAGE_LOGO), 0, 0, 0, LR_COPYFROMRESOURCE); const auto res = utils::nt::load_resource(IMAGE_LOGO); const auto img = utils::image::load_image(res); logo = utils::image::create_bitmap(img); terminate_runner = false; this->message_runner_ = utils::thread::create_named_thread("Console IO", [] { while (!terminate_runner) { std::string message_buffer{}; auto current_queue = empty_message_queue(); while (!current_queue.empty()) { const auto& msg = current_queue.front(); message_buffer.append(msg); current_queue.pop(); } if (!message_buffer.empty()) { print_message_to_console(message_buffer.data()); } std::this_thread::sleep_for(5ms); } }); this->console_runner_ = utils::thread::create_named_thread("Console Window", [this] { { static utils::hook::detour sys_create_console_hook; sys_create_console_hook.create(game::select(0x142332E00, 0x140597880), sys_create_console_stub); game::Sys_ShowConsole(); started = true; } MSG msg{}; while (!terminate_runner) { if (PeekMessageW(&msg, nullptr, NULL, NULL, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } else { std::this_thread::sleep_for(5ms); } } }); while (!started) { std::this_thread::sleep_for(10ms); } } void pre_destroy() override { terminate_runner = true; if (this->message_runner_.joinable()) { this->message_runner_.join(); } if (this->console_runner_.joinable()) { this->console_runner_.join(); } } private: std::thread console_runner_{}; std::thread message_runner_{}; }; } REGISTER_COMPONENT_WORKING(console::component)