diff --git a/codxe.vcxproj b/codxe.vcxproj index 902aba34..fba6c513 100644 --- a/codxe.vcxproj +++ b/codxe.vcxproj @@ -174,6 +174,7 @@ + diff --git a/src/game/iw5/mp/console.cpp b/src/game/iw5/mp/console.cpp new file mode 100644 index 00000000..f2bfb56e --- /dev/null +++ b/src/game/iw5/mp/console.cpp @@ -0,0 +1,253 @@ +#include "pch.h" +#include "console.h" + +namespace iw5 +{ +namespace mp +{ +namespace +{ +const DWORD keyboard_user_index = 0; +const DWORD keyboard_flags = XINPUT_FLAG_KEYBOARD; +const int max_keystrokes_per_frame = 16; + +const int max_lines = 128; +const int max_line_length = 160; +const int visible_lines = 16; +const int max_input_length = 200; + +// Layout in 640x480 virtual space (HORIZONTAL_ALIGN_LEFT / VERTICAL_ALIGN_TOP) +const int horz_align_left = 1; +const int vert_align_top = 1; +const float text_x = 12.0f; +const float text_y = 24.0f; +const float line_height = 11.0f; +const float text_scale = 0.22f; + +const float color_output[4] = {1.0f, 1.0f, 1.0f, 0.9f}; +const float color_input[4] = {1.0f, 1.0f, 0.4f, 1.0f}; +const float color_hint[4] = {0.7f, 0.7f, 0.7f, 0.8f}; + +CRITICAL_SECTION lines_lock; +char lines[max_lines][max_line_length]; +int line_next = 0; +int line_count = 0; +int scroll_offset = 0; + +bool console_open = false; +char input_line[max_input_length + 1]; +int input_length = 0; + +void AddLine(const char *text, int length) +{ + if (length >= max_line_length) + length = max_line_length - 1; + + EnterCriticalSection(&lines_lock); + memcpy(lines[line_next], text, length); + lines[line_next][length] = '\0'; + line_next = (line_next + 1) % max_lines; + if (line_count < max_lines) + line_count++; + LeaveCriticalSection(&lines_lock); +} + +// CL_ConsolePrint text can hold several lines and usually ends with '\n' +void AddText(const char *txt) +{ + if (!txt) + return; + + const char *start = txt; + for (const char *p = txt;; ++p) + { + if (*p == '\n' || *p == '\0') + { + if (p > start) + AddLine(start, static_cast(p - start)); + + if (*p == '\0') + break; + + start = p + 1; + } + } +} + +void ExecuteInput() +{ + if (input_length == 0) + return; + + char echo[max_input_length + 3]; + _snprintf(echo, sizeof(echo), "]%s", input_line); + echo[sizeof(echo) - 1] = '\0'; + AddText(echo); + + char command[max_input_length + 2]; + _snprintf(command, sizeof(command), "%s\n", input_line); + command[sizeof(command) - 1] = '\0'; + Cbuf_AddText(0, command); + + input_length = 0; + input_line[0] = '\0'; + scroll_offset = 0; +} + +bool IsConsoleKey(const XINPUT_KEYSTROKE &keystroke) +{ + return keystroke.VirtualKey == VK_OEM_3 || keystroke.Unicode == '`' || keystroke.Unicode == '~'; +} + +bool IsRepeat(const XINPUT_KEYSTROKE &keystroke) +{ +#ifdef XINPUT_KEYSTROKE_REPEAT + return (keystroke.Flags & XINPUT_KEYSTROKE_REPEAT) != 0; +#else + return (keystroke.Flags & 4) != 0; +#endif +} + +void HandleKeystroke(const XINPUT_KEYSTROKE &keystroke) +{ + if ((keystroke.Flags & XINPUT_KEYSTROKE_KEYDOWN) == 0 && !IsRepeat(keystroke)) + return; + + if (IsConsoleKey(keystroke)) + { + if (keystroke.Flags & XINPUT_KEYSTROKE_KEYDOWN) + console_open = !console_open; + return; + } + + if (!console_open) + return; + + switch (keystroke.VirtualKey) + { + case VK_RETURN: + ExecuteInput(); + return; + case VK_BACK: + if (input_length > 0) + input_line[--input_length] = '\0'; + return; + case VK_ESCAPE: + console_open = false; + return; + case VK_PRIOR: + scroll_offset += visible_lines / 2; + if (scroll_offset > line_count - visible_lines) + scroll_offset = line_count > visible_lines ? line_count - visible_lines : 0; + return; + case VK_NEXT: + scroll_offset -= visible_lines / 2; + if (scroll_offset < 0) + scroll_offset = 0; + return; + default: + break; + } + + const WCHAR ch = keystroke.Unicode; + if (ch >= 32 && ch <= 126 && input_length < max_input_length) + { + input_line[input_length++] = static_cast(ch); + input_line[input_length] = '\0'; + } +} + +void PollKeyboard() +{ + for (int i = 0; i < max_keystrokes_per_frame; ++i) + { + XINPUT_KEYSTROKE keystroke = {}; + const DWORD result = XInputGetKeystroke(keyboard_user_index, keyboard_flags, &keystroke); + + if (result != ERROR_SUCCESS || keystroke.Flags == 0) + return; + + HandleKeystroke(keystroke); + } +} + +void Draw(LocalClientNum_t localClientNum) +{ + const ScreenPlacement *placement = ScrPlace_GetActivePlacement(localClientNum); + Font_s *font = sharedUiInfo->assets.consoleFont; + if (!placement || !font) + return; + + EnterCriticalSection(&lines_lock); + const int shown = line_count < visible_lines ? line_count : visible_lines; + for (int i = 0; i < shown; ++i) + { + // oldest visible line first + const int age = scroll_offset + shown - 1 - i; + const int index = (line_next - 1 - age + max_lines * 2) % max_lines; + UI_DrawText(placement, lines[index], max_line_length, font, text_x, text_y + line_height * i, horz_align_left, + vert_align_top, text_scale, color_output, 0); + } + LeaveCriticalSection(&lines_lock); + + char prompt[max_input_length + 4]; + const bool cursor_on = (GetTickCount() / 500) % 2 == 0; + _snprintf(prompt, sizeof(prompt), "]%s%s", input_line, cursor_on ? "_" : ""); + prompt[sizeof(prompt) - 1] = '\0'; + UI_DrawText(placement, prompt, max_input_length + 2, font, text_x, text_y + line_height * (visible_lines + 1), + horz_align_left, vert_align_top, text_scale, color_input, 0); + + if (scroll_offset > 0) + UI_DrawText(placement, "^3-- scrolled (PgDn) --", 32, font, text_x, + text_y + line_height * (visible_lines + 2), horz_align_left, vert_align_top, text_scale, + color_hint, 0); +} +} // namespace + +Detour Console::UI_Refresh_Detour; +Detour Console::CL_ConsolePrint_Detour; + +void Console::UI_Refresh_Hook(LocalClientNum_t localClientNum) +{ + UI_Refresh_Detour.GetOriginal()(localClientNum); + + // UI refresh runs once per local client; the keyboard belongs to the first one + if (localClientNum != 0) + return; + + PollKeyboard(); + + if (console_open) + Draw(localClientNum); +} + +void Console::CL_ConsolePrint_Hook(LocalClientNum_t localClientNum, int channel, const char *txt, + unsigned int duration, unsigned int pixelWidth, int flags) +{ +#ifndef NDEBUG + DbgPrint("[codxe][IW5][CL_ConsolePrint] %s\n", txt); +#endif + AddText(txt); + CL_ConsolePrint_Detour.GetOriginal()(localClientNum, channel, txt, duration, pixelWidth, flags); +} + +Console::Console() +{ + InitializeCriticalSection(&lines_lock); + input_line[0] = '\0'; + + CL_ConsolePrint_Detour = Detour(CL_ConsolePrint, CL_ConsolePrint_Hook); + CL_ConsolePrint_Detour.Install(); + + UI_Refresh_Detour = Detour(UI_Refresh, UI_Refresh_Hook); + UI_Refresh_Detour.Install(); +} + +Console::~Console() +{ + UI_Refresh_Detour.Remove(); + CL_ConsolePrint_Detour.Remove(); + DeleteCriticalSection(&lines_lock); +} +} // namespace mp +} // namespace iw5 diff --git a/src/game/iw5/mp/console.h b/src/game/iw5/mp/console.h new file mode 100644 index 00000000..3bb8c58b --- /dev/null +++ b/src/game/iw5/mp/console.h @@ -0,0 +1,27 @@ +#pragma once + +#include "pch.h" + +namespace iw5 +{ +namespace mp +{ +// Retail MW3 has the interactive console compiled out (no toggleconsole command, no console drawing), +// so this is a small overlay: keyboard input via XInputGetKeystroke, commands via Cbuf_AddText, +// output captured from CL_ConsolePrint and drawn with UI_DrawText. +class Console : public Module +{ + public: + Console(); + ~Console(); + + private: + static Detour UI_Refresh_Detour; + static void UI_Refresh_Hook(LocalClientNum_t localClientNum); + + static Detour CL_ConsolePrint_Detour; + static void CL_ConsolePrint_Hook(LocalClientNum_t localClientNum, int channel, const char *txt, + unsigned int duration, unsigned int pixelWidth, int flags); +}; +} // namespace mp +} // namespace iw5 diff --git a/src/game/iw5/mp/main.cpp b/src/game/iw5/mp/main.cpp index ccf809da..98f45e82 100644 --- a/src/game/iw5/mp/main.cpp +++ b/src/game/iw5/mp/main.cpp @@ -6,6 +6,7 @@ #include "pm.h" #include "gsc.h" #include "bots.h" +#include "console.h" #include "events.h" namespace iw5 @@ -226,6 +227,7 @@ IW5_MP_Plugin::IW5_MP_Plugin() RegisterModule(new PlayerMovement()); RegisterModule(new GSC()); RegisterModule(new Bots()); + RegisterModule(new Console()); DB_FindXAssetHeader_Detour = Detour(DB_FindXAssetHeader, DB_FindXAssetHeader_Hook); DB_FindXAssetHeader_Detour.Install(); diff --git a/src/game/iw5/mp/patches.cpp b/src/game/iw5/mp/patches.cpp index a4f1368b..121c686f 100644 --- a/src/game/iw5/mp/patches.cpp +++ b/src/game/iw5/mp/patches.cpp @@ -47,17 +47,6 @@ void DisableDvarWriteChecks() Detour Jump_Start_Detour; -#ifndef NDEBUG -Detour CL_ConsolePrint_Detour; - -void CL_ConsolePrint_Hook(LocalClientNum_t localClientNum, int channel, const char *txt, unsigned int duration, - unsigned int pixelWidth, int flags) -{ - DbgPrint("[codxe][IW5][CL_ConsolePrint] %s\n", txt); - CL_ConsolePrint_Detour.GetOriginal()(localClientNum, channel, txt, duration, pixelWidth, flags); -} -#endif - void Jump_Start_Hook(pmove_t *pm, pml_t *pml, double height) { static const dvar_t *jump_height = Dvar_FindMalleableVar("jump_height"); @@ -73,17 +62,11 @@ patches::patches() Jump_Start_Detour = Detour(Jump_Start, Jump_Start_Hook); // Jump_Start_Detour.Install(); -#ifndef NDEBUG - CL_ConsolePrint_Detour = Detour(CL_ConsolePrint, CL_ConsolePrint_Hook); - CL_ConsolePrint_Detour.Install(); -#endif + // CL_ConsolePrint is detoured by the Console module (it also DbgPrints in debug builds) } patches::~patches() { -#ifndef NDEBUG - CL_ConsolePrint_Detour.Remove(); -#endif // Jump_Start_Detour.Remove(); } } // namespace mp diff --git a/src/game/iw5/mp/structs.h b/src/game/iw5/mp/structs.h index 7d1c5e8c..4d8014eb 100644 --- a/src/game/iw5/mp/structs.h +++ b/src/game/iw5/mp/structs.h @@ -1988,6 +1988,7 @@ typedef void (*PlayerCmd_GetViewmodel_t)(scr_entref_t entref); typedef void (*PlayerCmd_UFO_t)(scr_entref_t *entref); typedef void (*UI_DrawBuildNumber_t)(LocalClientNum_t localClientNum); +typedef void (*UI_Refresh_t)(LocalClientNum_t localClientNum); typedef void (*UI_DrawText_t)(const ScreenPlacement *scrPlace, const char *text, int maxChars, Font_s *font, double x, double y, int horzAlign, int vertAlign, double scale, const float *color, int style); diff --git a/src/game/iw5/mp/symbols.h b/src/game/iw5/mp/symbols.h index a533cd31..a8bfb2e5 100644 --- a/src/game/iw5/mp/symbols.h +++ b/src/game/iw5/mp/symbols.h @@ -73,6 +73,11 @@ static auto PlayerCmd_UFO = reinterpret_cast(0x82236588); static auto va = reinterpret_cast(0x823365F0); static auto UI_DrawBuildNumber = reinterpret_cast(0x822ECA88); +// Per-frame UI refresh, called from both the menu and the in-game frame draw (0x8217CF90 / 0x8217CB60). +// Calls UI_DrawBuildNumber when disconnected with a menu open. +static auto UI_Refresh = reinterpret_cast(0x822F82E8); + +static auto Cbuf_AddText = reinterpret_cast(0x82287F68); static auto UI_DrawText = reinterpret_cast(0x822EC490); static auto Weapon_RocketLauncher_Fire = reinterpret_cast(0x82271710);