#include #include "loader/component_loader.hpp" #include "game/game.hpp" #include "game/dvars.hpp" #include "game_console.hpp" #include "command.hpp" #include "scheduler.hpp" #include #include #define console_font game::R_RegisterFont("fonts/fira_mono_regular.ttf", 18) #define material_white game::Material_RegisterHandle("white") namespace game_console { namespace { struct console_globals { float x; float y; float left_x; float font_height; bool may_auto_complete; char auto_complete_choice[64]; int info_line_count; }; struct ingame_console { char buffer[256]; int cursor; int font_height; int visible_line_count; int visible_pixel_width; float screen_min[2]; //left & top float screen_max[2]; //right & bottom console_globals globals; bool output_visible; int display_line_offset; int line_count; std::deque output; }; ingame_console con; std::int32_t history_index = -1; std::deque history; std::string fixed_input; std::vector matches; float color_white[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; float color_title[4] = { 0.9f, 0.9f, 0.5f, 1.0f }; void clear() { strncpy_s(con.buffer, "", 256); con.cursor = 0; fixed_input = ""; matches.clear(); } void print(const std::string& data) { if (con.visible_line_count > 0 && con.display_line_offset == (con.output.size() - con.visible_line_count)) { con.display_line_offset++; } con.output.push_back(data); printf("%s\n", data.data()); if (con.output.size() > 1024) { con.output.pop_front(); } } void toggle_console() { clear(); con.output_visible = false; *game::keyCatchers ^= 1; } void toggle_console_output() { con.output_visible = con.output_visible == 0; } void check_resize() { con.screen_min[0] = 6.0f; con.screen_min[1] = 6.0f; con.screen_max[0] = game::ScrPlace_GetViewPlacement()->realViewportSize[0] - 6.0f; con.screen_max[1] = game::ScrPlace_GetViewPlacement()->realViewportSize[1] - 6.0f; if (console_font) { con.font_height = console_font->pixelHeight; con.visible_line_count = static_cast((con.screen_max[1] - con.screen_min[1] - (con.font_height * 2) ) - 24.0f) / con.font_height; con.visible_pixel_width = static_cast(((con.screen_max[0] - con.screen_min[0]) - 10.0f) - 18.0f); } else { con.font_height = 0; con.visible_line_count = 0; con.visible_pixel_width = 0; } } void draw_box(const float x, const float y, const float w, const float h, float* color) { game::vec4_t dark_color; dark_color[0] = color[0] * 0.5f; dark_color[1] = color[1] * 0.5f; dark_color[2] = color[2] * 0.5f; dark_color[3] = color[3]; game::R_AddCmdDrawStretchPic(x, y, w, h, 0.0f, 0.0f, 0.0f, 0.0f, color, material_white); game::R_AddCmdDrawStretchPic(x, y, 2.0f, h, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, material_white); game::R_AddCmdDrawStretchPic((x + w) - 2.0f, y, 2.0f, h, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, material_white); game::R_AddCmdDrawStretchPic(x, y, w, 2.0f, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, material_white); game::R_AddCmdDrawStretchPic(x, (y + h) - 2.0f, w, 2.0f, 0.0f, 0.0f, 0.0f, 0.0f, dark_color, material_white); } void draw_input_box(const int lines, float* color) { draw_box( con.globals.x - 6.0f, con.globals.y - 6.0f, (con.screen_max[0] - con.screen_min[0]) - ((con.globals.x - 6.0f) - con.screen_min[0]), (lines * con.globals.font_height) + 12.0f, color); } void draw_input_text_and_over(const char* str, float* color) { game::R_AddCmdDrawText(str, 0x7FFFFFFF, console_font, con.globals.x, con.globals.y + con.globals.font_height, 1.0f, 1.0f, 0.0f, color, 0); con.globals.x = game::R_TextWidth(str, 0, console_font) + con.globals.x + 6.0f; } void draw_hint_box(const int lines, float* color, [[maybe_unused]] float offset_x = 0.0f, [[maybe_unused]] float offset_y = 0.0f) { const auto _h = lines * con.globals.font_height + 12.0f; const auto _y = con.globals.y - 3.0f + con.globals.font_height + 12.0f; const auto _w = (con.screen_max[0] - con.screen_min[0]) - ((con.globals.x - 6.0f) - con.screen_min[0]); draw_box(con.globals.x - 6.0f, _y, _w, _h, color); } void draw_hint_text(const int line, const char* text, float* color, const float offset = 0.0f) { const auto _y = con.globals.font_height + con.globals.y + (con.globals.font_height * (line + 1)) + 15.0f; game::R_AddCmdDrawText(text, 0x7FFFFFFF, console_font, con.globals.x + offset, _y, 1.0f, 1.0f, 0.0f, color, 0); } bool match_compare(const std::string& input, const std::string& text, const bool exact) { if (exact && text == input) return true; if (!exact && text.find(input) != std::string::npos) return true; return false; } void find_matches(std::string input, std::vector& suggestions, const bool exact) { input = utils::string::to_lower(input); for (const auto& dvar : dvars::dvar_list) { auto name = utils::string::to_lower(dvar); if (match_compare(input, name, exact)) { suggestions.push_back(dvar); } if (exact && suggestions.size() > 1) { return; } } if (suggestions.size() == 0 && game::Dvar_FindVar(input.data())) { suggestions.push_back(input.data()); } game::cmd_function_s* cmd = (*game::cmd_functions); while (cmd) { if (cmd->name) { std::string name = utils::string::to_lower(cmd->name); if (match_compare(input, name, exact)) { suggestions.push_back(cmd->name); } if (exact && suggestions.size() > 1) { return; } } cmd = cmd->next; } } void draw_input() { con.globals.font_height = static_cast(console_font->pixelHeight); con.globals.x = con.screen_min[0] + 6.0f; con.globals.y = con.screen_min[1] + 6.0f; con.globals.left_x = con.screen_min[0] + 6.0f; draw_input_box(1, dvars::con_inputBoxColor->current.vector); draw_input_text_and_over("H1-Mod >", color_title); con.globals.left_x = con.globals.x; con.globals.auto_complete_choice[0] = 0; game::R_AddCmdDrawTextWithCursor(con.buffer, 0x7FFFFFFF, console_font, 18, con.globals.x, con.globals.y + con.globals.font_height, 1.0f, 1.0f, 0, color_white, 0, con.cursor, '|'); //game::R_AddCmdDrawText(con.buffer, 0x7FFF, console_font, con.globals.x, // con.globals.y + con.globals.font_height, 1.0f, 1.0f, 0.0f, color_white, 0); // check if using a prefixed '/' or not const auto input = con.buffer[1] && (con.buffer[0] == '/' || con.buffer[0] == '\\') ? std::string(con.buffer).substr(1) : std::string(con.buffer); if (!input.length()) { return; } if (input != fixed_input) { matches.clear(); if (input.find(" ") != std::string::npos) { find_matches(input.substr(0, input.find(" ")), matches, true); } else { find_matches(input, matches, false); } fixed_input = input; } con.globals.may_auto_complete = false; if (matches.size() > 24) { draw_hint_box(1, dvars::con_inputHintBoxColor->current.vector); draw_hint_text(0, utils::string::va("%i matches (too many to show here)", matches.size()), dvars::con_inputDvarMatchColor->current.vector); } else if (matches.size() == 1) { auto* const dvar = game::Dvar_FindVar(matches[0].data()); const auto line_count = dvar ? 2 : 1; draw_hint_box(line_count, dvars::con_inputHintBoxColor->current.vector); draw_hint_text(0, matches[0].data(), dvar ? dvars::con_inputDvarMatchColor->current.vector : dvars::con_inputCmdMatchColor->current.vector); if (dvar) { const auto offset = (con.screen_max[0] - con.globals.x) / 2.5f; draw_hint_text(0, game::Dvar_ValueToString(dvar, dvar->current, 0), dvars::con_inputDvarValueColor->current.vector, offset); draw_hint_text(1, " default", dvars::con_inputDvarInactiveValueColor->current.vector); draw_hint_text(1, game::Dvar_ValueToString(dvar, dvar->reset, 0), dvars::con_inputDvarInactiveValueColor->current.vector, offset); } strncpy_s(con.globals.auto_complete_choice, matches[0].data(), 64); con.globals.may_auto_complete = true; } else if (matches.size() > 1) { draw_hint_box(static_cast(matches.size()), dvars::con_inputHintBoxColor->current.vector); const auto offset = (con.screen_max[0] - con.globals.x) / 2.5f; for (size_t i = 0; i < matches.size(); i++) { auto* const dvar = game::Dvar_FindVar(matches[i].data()); draw_hint_text(static_cast(i), matches[i].data(), dvar ? dvars::con_inputDvarMatchColor->current.vector : dvars::con_inputCmdMatchColor->current.vector); if (dvar) { draw_hint_text(static_cast(i), game::Dvar_ValueToString(dvar, dvar->current, 0), dvars::con_inputDvarValueColor->current.vector, offset); } } strncpy_s(con.globals.auto_complete_choice, matches[0].data(), 64); con.globals.may_auto_complete = true; } } void draw_output_scrollbar(const float x, float y, const float width, const float height) { const auto _x = (x + width) - 10.0f; draw_box(_x, y, 10.0f, height, dvars::con_outputBarColor->current.vector); auto _height = height; if (con.output.size() > con.visible_line_count) { const auto percentage = static_cast(con.visible_line_count) / con.output.size(); _height *= percentage; const auto remainingSpace = height - _height; const auto percentageAbove = static_cast(con.display_line_offset) / (con.output.size() - con. visible_line_count); y = y + (remainingSpace * percentageAbove); } draw_box(_x, y, 10.0f, _height, dvars::con_outputSliderColor->current.vector); } void draw_output_text(const float x, float y) { const auto offset = con.output.size() >= con.visible_line_count ? 0.0f : (con.font_height * (con.visible_line_count - con.output.size())); for (auto i = 0; i < con.visible_line_count; i++) { y = console_font->pixelHeight + y; const auto index = i + con.display_line_offset; if (index >= con.output.size()) { break; } game::R_AddCmdDrawText(con.output.at(index).data(), 0x7FFF, console_font, x, y + offset, 1.0f, 1.0f, 0.0f, color_white, 0); } } void draw_output_window() { draw_box(con.screen_min[0], con.screen_min[1] + 32.0f, con.screen_max[0] - con.screen_min[0], (con.screen_max[1] - con.screen_min[1]) - 32.0f, dvars::con_outputWindowColor->current.vector); const auto x = con.screen_min[0] + 6.0f; const auto y = (con.screen_min[1] + 32.0f) + 6.0f; const auto width = (con.screen_max[0] - con.screen_min[0]) - 12.0f; const auto height = ((con.screen_max[1] - con.screen_min[1]) - 32.0f) - 12.0f; game::R_AddCmdDrawText("H1-Mod 1.4", 0x7FFFFFFF, console_font, x, ((height - 16.0f) + y) + console_font->pixelHeight, 1.0f, 1.0f, 0.0f, color_title, 0); draw_output_scrollbar(x, y, width, height); draw_output_text(x, y); } void draw_console() { check_resize(); if (*game::keyCatchers & 1) { if (!(*game::keyCatchers & 1)) { con.output_visible = false; } if (con.output_visible) { draw_output_window(); } draw_input(); } } } void print(const int type, const char* fmt, ...) { char va_buffer[0x200] = { 0 }; va_list ap; va_start(ap, fmt); vsprintf_s(va_buffer, fmt, ap); va_end(ap); const auto formatted = std::string(va_buffer); const auto lines = utils::string::split(formatted, '\n'); for (auto& line : lines) { print(type == con_type_info ? line : "^"s.append(std::to_string(type)).append(line)); } } bool console_char_event(const int localClientNum, const int key) { if (key == '`' || key == '~' || key == '|' || key == '\\') { return false; } if (key > 127) { return true; } if (*game::keyCatchers & 1) { if (key == game::keyNum_t::K_TAB) // tab (auto complete) { if (con.globals.may_auto_complete) { const auto firstChar = con.buffer[0]; clear(); if (firstChar == '\\' || firstChar == '/') { con.buffer[0] = firstChar; con.buffer[1] = '\0'; } strncat_s(con.buffer, con.globals.auto_complete_choice, 64); con.cursor = static_cast(std::string(con.buffer).length()); if (con.cursor != 254) { con.buffer[con.cursor++] = ' '; con.buffer[con.cursor] = '\0'; } } } if (key == 'v' - 'a' + 1) // paste { const auto clipboard = utils::string::get_clipboard_data(); if (clipboard.empty()) { return false; } for (auto i = 0; i < clipboard.length(); i++) { console_char_event(localClientNum, clipboard[i]); } return false; } if (key == 'c' - 'a' + 1) // clear { clear(); con.line_count = 0; con.output.clear(); history_index = -1; history.clear(); return false; } if (key == 'h' - 'a' + 1) // backspace { if (con.cursor > 0) { memmove(con.buffer + con.cursor - 1, con.buffer + con.cursor, strlen(con.buffer) + 1 - con.cursor); con.cursor--; } return false; } if (key < 32) { return false; } if (con.cursor == 256 - 1) { return false; } memmove(con.buffer + con.cursor + 1, con.buffer + con.cursor, strlen(con.buffer) + 1 - con.cursor); con.buffer[con.cursor] = static_cast(key); con.cursor++; if (con.cursor == strlen(con.buffer) + 1) { con.buffer[con.cursor] = 0; } } return true; } void execute(const char* cmd) { game::Cbuf_AddText(0, utils::string::va("%s \n", cmd)); } bool console_key_event(const int localClientNum, const int key, const int down) { if (key == game::keyNum_t::K_GRAVE || key == game::keyNum_t::K_TILDE) { if (!down) { return false; } if (game::playerKeys[localClientNum].keys[game::keyNum_t::K_SHIFT].down) { if (!(*game::keyCatchers & 1)) toggle_console(); toggle_console_output(); return false; } toggle_console(); return false; } if (*game::keyCatchers & 1) { if (down) { if (key == game::keyNum_t::K_UPARROW) { if (++history_index >= history.size()) { history_index = static_cast(history.size()) - 1; } clear(); if (history_index != -1) { strncpy_s(con.buffer, history.at(history_index).c_str(), 0x100); con.cursor = static_cast(strlen(con.buffer)); } } else if (key == game::keyNum_t::K_DOWNARROW) { if (--history_index < -1) { history_index = -1; } clear(); if (history_index != -1) { strncpy_s(con.buffer, history.at(history_index).c_str(), 0x100); con.cursor = static_cast(strlen(con.buffer)); } } if (key == game::keyNum_t::K_RIGHTARROW) { if (con.cursor < strlen(con.buffer)) { con.cursor++; } return false; } if (key == game::keyNum_t::K_LEFTARROW) { if (con.cursor > 0) { con.cursor--; } return false; } //scroll through output if (key == game::keyNum_t::K_MWHEELUP || key == game::keyNum_t::K_PGUP) { if (con.output.size() > con.visible_line_count && con.display_line_offset > 0) { con.display_line_offset--; } } else if (key == game::keyNum_t::K_MWHEELDOWN || key == game::keyNum_t::K_PGDN) { if (con.output.size() > con.visible_line_count && con.display_line_offset < (con.output.size() - con. visible_line_count)) { con.display_line_offset++; } } if (key == game::keyNum_t::K_ENTER) { execute(fixed_input.data()); if (history_index != -1) { const auto itr = history.begin() + history_index; if (*itr == con.buffer) { history.erase(history.begin() + history_index); } } history.push_front(con.buffer); print(""s.append(con.buffer)); if (history.size() > 10) { history.erase(history.begin() + 10); } history_index = -1; clear(); } } } return true; } class component final : public component_interface { public: void post_unpack() override { scheduler::loop(draw_console, scheduler::pipeline::renderer); con.cursor = 0; con.visible_line_count = 0; con.output_visible = false; con.display_line_offset = 0; con.line_count = 0; strncpy_s(con.buffer, "", 256); con.globals.x = 0.0f; con.globals.y = 0.0f; con.globals.left_x = 0.0f; con.globals.font_height = 0.0f; con.globals.may_auto_complete = false; con.globals.info_line_count = 0; strncpy_s(con.globals.auto_complete_choice, "", 64); // //add clear command //command::add("clear", [&]() //{ //clear(); //con.line_count = 0; //con.output.clear(); //history_index = -1; //history.clear(); //}); // add our dvars dvars::con_inputBoxColor = dvars::register_vec4( "con_inputBoxColor", 0.2f, 0.2f, 0.2f, 0.9f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); dvars::con_inputHintBoxColor = dvars::register_vec4( "con_inputHintBoxColor", 0.3f, 0.3f, 0.3f, 1.0f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); dvars::con_outputBarColor = dvars::register_vec4( "con_outputBarColor", 0.5f, 0.5f, 0.5f, 0.6f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); dvars::con_outputSliderColor = dvars::register_vec4( "con_outputSliderColor", 0.9f, 0.9f, 0.5f, 1.00f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); dvars::con_outputWindowColor = dvars::register_vec4( "con_outputWindowColor", 0.25f, 0.25f, 0.25f, 0.85f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); dvars::con_inputDvarMatchColor = dvars::register_vec4( "con_inputDvarMatchColor", 1.0f, 1.0f, 0.8f, 1.0f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); dvars::con_inputDvarValueColor = dvars::register_vec4( "con_inputDvarValueColor", 1.0f, 1.0f, 0.8f, 1.0f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); dvars::con_inputDvarInactiveValueColor = dvars::register_vec4( "con_inputDvarInactiveValueColor", 0.8f, 0.8f, 0.8f, 1.0f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); dvars::con_inputCmdMatchColor = dvars::register_vec4( "con_inputCmdMatchColor", 0.80f, 0.80f, 1.0f, 1.0f, 0.0f, 1.0f, game::DVAR_FLAG_SAVED); } }; } REGISTER_COMPONENT(game_console::component)