Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codxe.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
<ClCompile Include="src\game\iw5\mp\patches.cpp" />
<ClCompile Include="src\game\iw5\mp\pm.cpp" />
<ClCompile Include="src\game\iw5\mp\gsc.cpp" />
<ClCompile Include="src\game\iw5\mp\console.cpp" />

<ClCompile Include="src\game\t4\mp\main.cpp" />
<ClCompile Include="src\game\t4\mp\symbols.cpp" />
Expand Down
253 changes: 253 additions & 0 deletions src/game/iw5/mp/console.cpp
Original file line number Diff line number Diff line change
@@ -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<int>(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<char>(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<UI_Refresh_t>()(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<CL_ConsolePrint_t>()(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
27 changes: 27 additions & 0 deletions src/game/iw5/mp/console.h
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/game/iw5/mp/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "pm.h"
#include "gsc.h"
#include "bots.h"
#include "console.h"
#include "events.h"

namespace iw5
Expand Down Expand Up @@ -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();
Expand Down
19 changes: 1 addition & 18 deletions src/game/iw5/mp/patches.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<CL_ConsolePrint_t>()(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");
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/game/iw5/mp/structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/game/iw5/mp/symbols.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ static auto PlayerCmd_UFO = reinterpret_cast<PlayerCmd_UFO_t>(0x82236588);
static auto va = reinterpret_cast<char *(*)(const char *format, ...)>(0x823365F0);

static auto UI_DrawBuildNumber = reinterpret_cast<UI_DrawBuildNumber_t>(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<UI_Refresh_t>(0x822F82E8);

static auto Cbuf_AddText = reinterpret_cast<void (*)(int localClientNum, const char *text)>(0x82287F68);
static auto UI_DrawText = reinterpret_cast<UI_DrawText_t>(0x822EC490);

static auto Weapon_RocketLauncher_Fire = reinterpret_cast<Weapon_RocketLauncher_Fire_t>(0x82271710);
Expand Down
Loading