/* =========================================================================== IceTech GPL Source Code Copyright (C) 2026 Justin Marshall This file is part of the IceTech GPL Source Code (?IceTech Source Code?). =========================================================================== */ #include "precompiled.h" #pragma hdrstop #include "qe3.h" #include "Radiant.h" #include "ConsoleDlg.h" // CConsoleDlg dialog IMPLEMENT_DYNCREATE(CConsoleDlg, CDialog) CConsoleDlg::CConsoleDlg(CWnd* pParent /*=NULL*/) : CDialog(CConsoleDlg::IDD, pParent) { currentHistoryPosition = -1; currentCommand = ""; saveCurrentCommand = true; } CConsoleDlg::~CConsoleDlg() { if (blackBrush.GetSafeHandle() != NULL) { blackBrush.DeleteObject(); } } void CConsoleDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); // Do not DDX IDC_EDIT_CONSOLE here. // The resource control can stay as a normal EDIT placeholder. // OnInitDialog destroys it and creates the RichEdit in its place. DDX_Control(pDX, IDC_EDIT_INPUT, editInput); } BEGIN_MESSAGE_MAP(CConsoleDlg, CDialog) ON_WM_SIZE() ON_WM_SETFOCUS() ON_WM_ACTIVATE() ON_WM_CTLCOLOR() END_MESSAGE_MAP() BOOL CConsoleDlg::OnInitDialog() { CDialog::OnInitDialog(); // Needed before creating CRichEditCtrl. // Safe if already initialized elsewhere. AfxInitRichEdit2(); if (blackBrush.GetSafeHandle() == NULL) { blackBrush.CreateSolidBrush(RGB(0, 0, 0)); } CreateRichConsoleFromPlaceholder(); SetupRichConsoleDefaults(); if (editInput.GetSafeHwnd() != NULL) { editInput.SetLimitText(1024); editInput.SetWindowText(""); } return TRUE; } void CConsoleDlg::CreateRichConsoleFromPlaceholder() { CWnd* oldConsole = GetDlgItem(IDC_EDIT_CONSOLE); if (oldConsole == NULL || oldConsole->GetSafeHwnd() == NULL) { return; } CRect rc; oldConsole->GetWindowRect(&rc); ScreenToClient(&rc); oldConsole->DestroyWindow(); DWORD style = WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | ES_NOHIDESEL | ES_SAVESEL; // Older MFC CRichEditCtrl::Create only takes 4 args. // Do not use CreateEx here. editConsole.Create( style, rc, this, IDC_EDIT_CONSOLE ); } void CConsoleDlg::SetupRichConsoleDefaults() { if (editConsole.GetSafeHwnd() == NULL) { return; } editConsole.SetEventMask(0); editConsole.SetOptions(ECOOP_OR, ECO_AUTOVSCROLL); editConsole.SetReadOnly(TRUE); // CRichEditCtrl uses LimitText(), not SetLimitText(). editConsole.LimitText(1024 * 1024); editConsole.SetBackgroundColor(FALSE, RGB(0, 0, 0)); CHARFORMAT cf; memset(&cf, 0, sizeof(cf)); cf.cbSize = sizeof(cf); cf.dwMask = CFM_COLOR | CFM_FACE | CFM_SIZE; cf.crTextColor = ICE_CONSOLE_DEFAULT_COLOR; cf.yHeight = 180; // 9pt lstrcpyn(cf.szFaceName, "Consolas", LF_FACESIZE); editConsole.SetDefaultCharFormat(cf); editConsole.SetSel(0, -1); editConsole.SetSelectionCharFormat(cf); editConsole.SetSel(-1, -1); } CString CConsoleDlg::NormalizeNewlines(const CString& in) const { CString out; out.Preallocate(in.GetLength() + 16); for (int i = 0; i < in.GetLength(); i++) { TCHAR c = in[i]; if (c == '\r') { if (i + 1 < in.GetLength() && in[i + 1] == '\n') { out += "\r\n"; i++; } else { out += "\r\n"; } } else if (c == '\n') { out += "\r\n"; } else { out += c; } } return out; } COLORREF CConsoleDlg::DoomColorForIndex(char c) const { switch (c) { case '0': return RGB(0, 0, 0); // black case '1': return RGB(255, 64, 64); // red case '2': return RGB(64, 255, 64); // green case '3': return RGB(255, 255, 64); // yellow case '4': return RGB(80, 140, 255); // blue case '5': return RGB(64, 255, 255); // cyan case '6': return RGB(255, 64, 255); // magenta case '7': return RGB(230, 230, 230); // white case '8': return RGB(160, 160, 160); // gray case '9': return RGB(255, 150, 32); // orange case 'r': case 'R': return RGB(255, 64, 64); case 'g': case 'G': return RGB(64, 255, 64); case 'y': case 'Y': return RGB(255, 255, 64); case 'b': case 'B': return RGB(80, 140, 255); case 'c': case 'C': return RGB(64, 255, 255); case 'm': case 'M': return RGB(255, 64, 255); case 'w': case 'W': return RGB(230, 230, 230); default: return ICE_CONSOLE_DEFAULT_COLOR; } } static bool ConsoleStartsWithNoCase(const char* s, const char* prefix) { if (s == NULL || prefix == NULL) { return false; } while (*prefix != '\0') { char a = *s; char b = *prefix; if (a >= 'A' && a <= 'Z') { a = a - 'A' + 'a'; } if (b >= 'A' && b <= 'Z') { b = b - 'A' + 'a'; } if (a != b) { return false; } s++; prefix++; } return true; } COLORREF CConsoleDlg::DoomColorForName(const char* name, int& consumed) const { consumed = 0; struct colorName_t { const char* name; COLORREF color; }; static const colorName_t names[] = { { "S_COLOR_BLACK", RGB(0, 0, 0) }, { "S_COLOR_RED", RGB(255, 64, 64) }, { "S_COLOR_GREEN", RGB(64, 255, 64) }, { "S_COLOR_YELLOW", RGB(255, 255, 64) }, { "S_COLOR_BLUE", RGB(80, 140, 255) }, { "S_COLOR_CYAN", RGB(64, 255, 255) }, { "S_COLOR_MAGENTA", RGB(255, 64, 255) }, { "S_COLOR_WHITE", RGB(230, 230, 230) }, { "S_COLOR_GRAY", RGB(160, 160, 160) }, { "S_COLOR_GREY", RGB(160, 160, 160) }, { "S_COLOR_ORANGE", RGB(255, 150, 32) }, { "COLOR_BLACK", RGB(0, 0, 0) }, { "COLOR_RED", RGB(255, 64, 64) }, { "COLOR_GREEN", RGB(64, 255, 64) }, { "COLOR_YELLOW", RGB(255, 255, 64) }, { "COLOR_BLUE", RGB(80, 140, 255) }, { "COLOR_CYAN", RGB(64, 255, 255) }, { "COLOR_MAGENTA", RGB(255, 64, 255) }, { "COLOR_WHITE", RGB(230, 230, 230) }, { "COLOR_GRAY", RGB(160, 160, 160) }, { "COLOR_GREY", RGB(160, 160, 160) }, { "COLOR_ORANGE", RGB(255, 150, 32) } }; for (int i = 0; i < sizeof(names) / sizeof(names[0]); i++) { if (ConsoleStartsWithNoCase(name, names[i].name)) { consumed = (int)strlen(names[i].name); return names[i].color; } } return ICE_CONSOLE_DEFAULT_COLOR; } bool CConsoleDlg::TryConsumeColorToken(const char* s, int& consumed, COLORREF& color) const { consumed = 0; color = ICE_CONSOLE_DEFAULT_COLOR; if (s == NULL || s[0] == '\0') { return false; } // Doom/idTech style color escape: ^1, ^2, ^7, etc. if (s[0] == '^' && s[1] != '\0') { // ^^ means literal caret, not a color. if (s[1] == '^') { return false; } color = DoomColorForIndex(s[1]); consumed = 2; return true; } // Optional support if some code emits literal token names. int namedConsumed = 0; COLORREF namedColor = DoomColorForName(s, namedConsumed); if (namedConsumed > 0) { color = namedColor; consumed = namedConsumed; return true; } return false; } void CConsoleDlg::AppendConsoleRun(const CString& text, COLORREF color) { if (text.IsEmpty() || editConsole.GetSafeHwnd() == NULL) { return; } CString normalized = NormalizeNewlines(text); editConsole.SetReadOnly(FALSE); int len = editConsole.GetTextLength(); editConsole.SetSel(len, len); CHARFORMAT cf; memset(&cf, 0, sizeof(cf)); cf.cbSize = sizeof(cf); cf.dwMask = CFM_COLOR | CFM_FACE | CFM_SIZE; cf.crTextColor = color; cf.yHeight = 180; lstrcpyn(cf.szFaceName, "Consolas", LF_FACESIZE); editConsole.SetSelectionCharFormat(cf); editConsole.ReplaceSel(normalized, FALSE); editConsole.SetReadOnly(TRUE); } void CConsoleDlg::TrimConsoleIfNeeded() { if (editConsole.GetSafeHwnd() == NULL) { return; } const int maxChars = 512 * 1024; const int trimToChars = 384 * 1024; int len = editConsole.GetTextLength(); if (len <= maxChars) { return; } int removeCount = len - trimToChars; editConsole.SetReadOnly(FALSE); editConsole.SetSel(0, removeCount); editConsole.ReplaceSel("", FALSE); editConsole.SetReadOnly(TRUE); } void CConsoleDlg::AppendConsoleTextColored(const char* text) { if (text == NULL || editConsole.GetSafeHwnd() == NULL) { return; } COLORREF currentColor = ICE_CONSOLE_DEFAULT_COLOR; CString run; const char* p = text; while (*p != '\0') { int consumed = 0; COLORREF newColor = ICE_CONSOLE_DEFAULT_COLOR; if (TryConsumeColorToken(p, consumed, newColor)) { if (!run.IsEmpty()) { AppendConsoleRun(run, currentColor); run.Empty(); } currentColor = newColor; p += consumed; continue; } if (p[0] == '^' && p[1] == '^') { run += '^'; p += 2; continue; } run += *p; p++; } if (!run.IsEmpty()) { AppendConsoleRun(run, currentColor); } TrimConsoleIfNeeded(); int len = editConsole.GetTextLength(); editConsole.SetSel(len, len); editConsole.SendMessage(EM_SCROLLCARET, 0, 0); } void CConsoleDlg::AddText(const char* msg) { if (msg == NULL || editConsole.GetSafeHwnd() == NULL) { return; } idStr work; work = msg; // DO NOT call RemoveColors() here. // The RichEdit append path consumes Doom color codes and applies colors. work = CEntityDlg::TranslateString(work.c_str()); AppendConsoleTextColored(work.c_str()); } void CConsoleDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); if (editInput.GetSafeHwnd() == NULL) { return; } CRect clientRect; GetClientRect(&clientRect); CRect inputRect; editInput.GetWindowRect(&inputRect); const int margin = 4; const int inputHeight = inputRect.Height(); editInput.SetWindowPos( NULL, margin, clientRect.Height() - margin - inputHeight, clientRect.Width() - margin * 2, inputHeight, SWP_SHOWWINDOW ); if (editConsole.GetSafeHwnd() != NULL) { editConsole.SetWindowPos( NULL, margin, margin, clientRect.Width() - margin * 2, clientRect.Height() - inputHeight - margin * 3, SWP_SHOWWINDOW ); } } BOOL CConsoleDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg->hwnd == editInput.GetSafeHwnd()) { if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_ESCAPE) { Select_Deselect(); g_pParentWnd->SetFocus(); return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { ExecuteCommand(); return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_UP) { // Save off the current in-progress command so we can get back to it. if (saveCurrentCommand == true) { CString str; editInput.GetWindowText(str); currentCommand = str.GetBuffer(0); saveCurrentCommand = false; } if (consoleHistory.Num() > 0) { if (currentHistoryPosition < 0) { currentHistoryPosition = consoleHistory.Num() - 1; } if (currentHistoryPosition >= consoleHistory.Num()) { currentHistoryPosition = consoleHistory.Num() - 1; } editInput.SetWindowText(consoleHistory[currentHistoryPosition]); int selLocation = consoleHistory[currentHistoryPosition].Length(); editInput.SetSel(selLocation, selLocation); if (currentHistoryPosition > 0) { --currentHistoryPosition; } } return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_DOWN) { int selLocation = 0; if (currentHistoryPosition < consoleHistory.Num() - 1) { ++currentHistoryPosition; editInput.SetWindowText(consoleHistory[currentHistoryPosition]); selLocation = consoleHistory[currentHistoryPosition].Length(); } else { editInput.SetWindowText(currentCommand); selLocation = currentCommand.Length(); currentCommand.Clear(); saveCurrentCommand = true; } editInput.SetSel(selLocation, selLocation); return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_TAB) { common->Printf("Command History\n----------------\n"); for (int i = 0; i < consoleHistory.Num(); i++) { common->Printf("[cmd %d]: %s\n", i, consoleHistory[i].c_str()); } common->Printf("----------------\n"); return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_NEXT) { if (editConsole.GetSafeHwnd() != NULL) { editConsole.LineScroll(10); } return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_PRIOR) { if (editConsole.GetSafeHwnd() != NULL) { editConsole.LineScroll(-10); } return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_HOME) { if (editConsole.GetSafeHwnd() != NULL) { editConsole.LineScroll(-editConsole.GetLineCount()); } return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_END) { if (editConsole.GetSafeHwnd() != NULL) { editConsole.LineScroll(editConsole.GetLineCount()); } return TRUE; } } return CDialog::PreTranslateMessage(pMsg); } void CConsoleDlg::OnSetFocus(CWnd* pOldWnd) { CDialog::OnSetFocus(pOldWnd); if (editInput.GetSafeHwnd() != NULL) { editInput.SetFocus(); } } void CConsoleDlg::SetConsoleText(const idStr& text) { if (editInput.GetSafeHwnd() != NULL) { editInput.Clear(); editInput.SetWindowText(text.c_str()); } } void CConsoleDlg::ExecuteCommand(const idStr& cmd) { CString str; if (cmd.Length() > 0) { str = cmd; } else { editInput.GetWindowText(str); } if (str != "") { editInput.SetWindowText(""); common->Printf("%s\n", str.GetBuffer(0)); // Avoid adding multiple identical commands in a row. int index = consoleHistory.Num(); if (index == 0 || str.GetBuffer(0) != consoleHistory[index - 1]) { // Keep the history to 16 commands, removing the oldest command. if (consoleHistory.Num() > 16) { consoleHistory.RemoveIndex(0); } currentHistoryPosition = consoleHistory.Append(str.GetBuffer(0)); } else { currentHistoryPosition = consoleHistory.Num() - 1; } currentCommand.Clear(); saveCurrentCommand = true; bool propogateCommand = true; // Process some of our own special commands. if (str.CompareNoCase("clear") == 0) { if (editConsole.GetSafeHwnd() != NULL) { editConsole.SetReadOnly(FALSE); editConsole.SetSel(0, -1); editConsole.ReplaceSel("", FALSE); editConsole.SetReadOnly(TRUE); } propogateCommand = false; } else if (str.CompareNoCase("edit") == 0) { propogateCommand = false; } if (propogateCommand) { cmdSystem->BufferCommandText(CMD_EXEC_NOW, str); } Sys_UpdateWindows(W_ALL); } } void CConsoleDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) { CDialog::OnActivate(nState, pWndOther, bMinimized); if (nState == WA_ACTIVE || nState == WA_CLICKACTIVE) { if (editInput.GetSafeHwnd() != NULL) { editInput.SetFocus(); } } } HBRUSH CConsoleDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor); if (pWnd != NULL && pWnd->GetSafeHwnd() == editInput.GetSafeHwnd()) { pDC->SetBkColor(RGB(0, 0, 0)); pDC->SetTextColor(ICE_CONSOLE_DEFAULT_COLOR); return (HBRUSH)blackBrush.GetSafeHandle(); } return hbr; }