og-odamex/client/src/c_console.cpp
2026-01-05 01:52:47 +00:00

2440 lines
53 KiB
C++

// Emacs style mode select -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id$
//
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// DESCRIPTION:
// C_CONSOLE
//
//-----------------------------------------------------------------------------
#include "odamex.h"
#include <stdarg.h>
#include <cmath>
#include "m_alloc.h"
#include "m_memio.h"
#include "g_game.h"
#include "c_console.h"
#include "c_dispatch.h"
#include "c_bind.h"
#include "i_system.h"
#include "i_video.h"
#include "v_palette.h"
#include "v_video.h"
#include "v_text.h"
#include "w_wad.h"
#include "z_zone.h"
#include "r_main.h"
#include "r_interp.h"
#include "st_stuff.h"
#include "s_sound.h"
#include "cl_responderkeys.h"
#include "cl_download.h"
#include "g_gametype.h"
#include "m_fileio.h"
#include <list>
#include <algorithm>
#ifdef __SWITCH__
#include "nx_io.h"
#endif
#include "cl_demo.h"
extern NetDemo netdemo;
static constexpr int MAX_LINE_LENGTH = 8192;
static bool ShouldTabCycle = false;
static size_t NextCycleIndex = 0;
static size_t PrevCycleIndex = 0;
static IWindowSurface* background_surface;
extern int gametic;
static unsigned int ConRows, ConCols, ConCharSize, ConScale;
static bool cursoron = false;
static int ConBottom = 0;
static int RowAdjust = 0;
int ConBottomStep; // Console fall/raise bottom pixels at the end of the tic, for interp purposes
int CursorTicker, ScrollState = 0;
constate_e ConsoleState = c_up;
extern byte *ConChars;
bool KeysShifted;
bool KeysCtrl;
bool KeysAlt;
bool NumLockEnabled;
static bool midprinting;
#define SCROLLUP 1
#define SCROLLDN 2
#define SCROLLNO 0
EXTERN_CVAR(con_scaletext)
EXTERN_CVAR(con_coloredmessages)
EXTERN_CVAR(con_buffersize)
EXTERN_CVAR(show_messages)
EXTERN_CVAR(print_stdout)
EXTERN_CVAR(con_notifytime)
EXTERN_CVAR(message_showpickups)
EXTERN_CVAR(message_showobituaries)
static unsigned int TickerAt, TickerMax;
static const char *TickerLabel;
#define NUMNOTIFIES 4
int V_TextScaleXAmount();
int V_TextScaleYAmount();
static struct NotifyText
{
int timeout;
int printlevel;
byte text[256];
} NotifyStrings[NUMNOTIFIES];
// Default Printlevel
#define PRINTLEVELS 9 //(5 + 3)
int PrintColors[PRINTLEVELS] =
{ CR_RED, // Pickup
CR_GOLD, // Obituaries
CR_GRAY, // Messages
CR_GREEN, // Chat
CR_GREEN, // Team chat
CR_GOLD, // Server chat
CR_YELLOW, // Warning messages
CR_RED, // Critical messages
CR_GOLD, // Centered Messages
};
// ============================================================================
//
// ConsoleLine class interface
//
// Stores the console's output text.
//
// ============================================================================
class ConsoleLine
{
public:
ConsoleLine();
ConsoleLine(const std::string& _text, const std::string& _color_code = "\034-", // TEXTCOLOR_ESCAPE
int _print_level = PRINT_HIGH);
void join(const ConsoleLine& other);
ConsoleLine split(size_t max_width);
[[nodiscard]] bool expired() const;
std::string text;
std::string color_code;
bool wrapped;
int print_level;
int timeout;
};
// ============================================================================
//
// ConsoleLine class implementation
//
// ============================================================================
ConsoleLine::ConsoleLine()
: color_code("\034-"), wrapped(false), print_level(PRINT_HIGH),
timeout(gametic + int(con_notifytime * TICRATE))
{
}
ConsoleLine::ConsoleLine(const std::string& _text, const std::string& _color_code,
int _print_level)
: text(_text), color_code(_color_code), wrapped(false), print_level(_print_level),
timeout(gametic + int(con_notifytime * TICRATE))
{
}
//
// ConsoleLine::join
//
// Appends the other console line to this line.
//
void ConsoleLine::join(const ConsoleLine& other)
{
text.append(other.text);
wrapped = other.wrapped;
}
//
// ConsoleLine::split
//
// Splits this line into a second line with this line having a maximum
// pixel width of max_width.
//
ConsoleLine ConsoleLine::split(size_t max_width)
{
char wrapped_color_code[4] = { 0 };
size_t width = 0;
size_t break_pos = 0;
const char* s = text.c_str();
while (s)
{
if (s[0] == TEXTCOLOR_ESCAPE && s[1] != '\0')
{
strncpy(wrapped_color_code, s, 2);
s += 2;
continue;
}
if (*s == ' ' || *s == '\n' || *s == '\t')
break_pos = s - text.c_str();
else if (*s == '-')
break_pos = s + 1 - text.c_str();
if (width + ConCharSize > max_width)
{
// Is this word is too long to fit on the line?
// Breaking it here is the only option.
if (break_pos == 0)
break_pos = s - text.c_str();
ConsoleLine new_line(text.substr(break_pos, std::string::npos));
TrimStringStart(new_line.text);
// continue the color markup onto the next line
if (*wrapped_color_code)
new_line.text = wrapped_color_code + new_line.text;
new_line.wrapped = wrapped;
new_line.print_level = print_level;
new_line.timeout = timeout;
new_line.color_code = color_code;
text = text.substr(0, break_pos);
wrapped = true;
return new_line;
}
s++;
width += ConCharSize;
}
// didn't have to wrap
return ConsoleLine();
}
bool ConsoleLine::expired() const
{
return gametic > timeout;
}
// ============================================================================
//
// ConsoleCommandLine class interface
//
// Stores the text and cursor position for the console's command line. Also
// provides basic functions to manipulate the cursor or insert and remove
// characters.
//
// ============================================================================
class ConsoleCommandLine
{
public:
ConsoleCommandLine() : cursor_position(0), scrolled_columns(0) { }
void clear();
void moveCursorLeft();
void moveCursorRight();
void moveCursorLeftWord();
void moveCursorRightWord();
void moveCursorHome();
void moveCursorEnd();
void insertCharacter(char c);
void insertString(const std::string& str);
void replaceString(const std::string& str);
void deleteCharacter();
void deleteLeftWord();
void deleteRightWord();
void backspace();
std::string text;
size_t cursor_position;
int scrolled_columns;
private:
void doScrolling();
};
// ============================================================================
//
// ConsoleCommandLine class implementation
//
// ============================================================================
//
// ConsoleCommandLine::doScrolling
//
// Helper function that recalculates the scrolled_columns variable after
// the cursor has been moved or text has been inserted or deleted.
//
void ConsoleCommandLine::doScrolling()
{
int n = scrolled_columns;
// Start of visible line is beyond end of line
if (scrolled_columns >= text.length())
n = cursor_position - ConCols + 2;
// The cursor_position is beyond the visible part of the line
if (((int)cursor_position - (int)scrolled_columns) >= (int)(ConCols - 2))
n = cursor_position - ConCols + 2;
// The cursor_positionor is in front of the visible part of the line
if (scrolled_columns > cursor_position)
n = cursor_position;
if (n < 0)
n = 0;
scrolled_columns = n;
}
void ConsoleCommandLine::clear()
{
text.clear();
cursor_position = 0;
scrolled_columns = 0;
}
void ConsoleCommandLine::moveCursorLeft()
{
if (cursor_position > 0)
cursor_position--;
doScrolling();
}
void ConsoleCommandLine::moveCursorRight()
{
if (cursor_position < text.length())
cursor_position++;
doScrolling();
}
void ConsoleCommandLine::moveCursorLeftWord()
{
const char* str = text.c_str();
bool firstSpacesCleared = false;
bool spaceWord = false;
if (cursor_position > 0 && isspace(str[cursor_position]))
firstSpacesCleared = true;
if (cursor_position > 0)
cursor_position--;
if (cursor_position > 0 && firstSpacesCleared)
spaceWord = true;
while (cursor_position > 0)
{
if (spaceWord)
{
if (!isspace(str[cursor_position - 1]))
{
break;
}
}
else
{
if (firstSpacesCleared && isspace(str[cursor_position - 1]))
{
break;
}
else if (!isspace(str[cursor_position - 1]) && !firstSpacesCleared)
{
firstSpacesCleared = true;
}
}
cursor_position--;
}
doScrolling();
}
void ConsoleCommandLine::moveCursorRightWord()
{
// find first non-space character after the next space(s)
cursor_position = text.find_first_not_of(' ', text.find_first_of(' ', cursor_position));
if (cursor_position == std::string::npos)
cursor_position = text.length();
doScrolling();
}
void ConsoleCommandLine::moveCursorHome()
{
cursor_position = 0;
doScrolling();
}
void ConsoleCommandLine::moveCursorEnd()
{
cursor_position = text.length();
doScrolling();
}
void ConsoleCommandLine::insertCharacter(char c)
{
text.insert(cursor_position, &c, 1);
cursor_position++;
doScrolling();
}
void ConsoleCommandLine::insertString(const std::string& str)
{
text.insert(cursor_position, str);
cursor_position += str.length();
doScrolling();
}
void ConsoleCommandLine::replaceString(const std::string& str)
{
text = str;
cursor_position = str.length();
doScrolling();
}
void ConsoleCommandLine::deleteCharacter()
{
if (cursor_position < text.length())
{
text.erase(cursor_position, 1);
doScrolling();
}
}
void ConsoleCommandLine::deleteLeftWord()
{
size_t word = 0;
bool firstSpacesCleared = false;
bool spaceWord = false;
const char* str = text.c_str();
// Delete trailing spaces instead of the word
if (cursor_position > 0 && isspace(str[cursor_position - 1]))
{
spaceWord = true;
}
while (cursor_position > 0)
{
if (spaceWord)
{
if (!isspace(str[cursor_position - 1]))
{
break;
}
}
else
{
if (firstSpacesCleared && isspace(str[cursor_position]))
{
break;
}
else if (!isspace(str[cursor_position]) && !firstSpacesCleared)
{
firstSpacesCleared = true;
}
}
cursor_position--;
word++;
}
text.erase(cursor_position, word);
doScrolling();
}
void ConsoleCommandLine::deleteRightWord()
{
bool spaceWord = false;
const char* str = text.c_str();
if (cursor_position > 0 &&
cursor_position < text.length() &&
isspace(str[cursor_position]))
{
spaceWord = true;
}
size_t word = 0;
size_t wordLength = 0;
if (spaceWord)
{
word = text.find_first_not_of(' ', cursor_position);
}
else
{
word = text.find_first_not_of(' ', text.find_first_of(' ', cursor_position));
}
if (word == std::string::npos)
wordLength = text.length();
else
wordLength = word - cursor_position;
text.erase(cursor_position, wordLength);
doScrolling();
}
void ConsoleCommandLine::backspace()
{
if (cursor_position > 0)
{
text.erase(cursor_position - 1, 1);
moveCursorLeft();
}
}
// ============================================================================
//
// ConsoleHistory class interface
//
// Stores a copy of each line of text entered on the command line and provides
// iteration functions to recall previous command lines entered.
//
// ============================================================================
class ConsoleHistory
{
public:
ConsoleHistory();
void clear();
void resetPosition();
void addString(const std::string& str);
[[nodiscard]] const std::string& getString() const;
void movePositionUp();
void movePositionDown();
void dump();
private:
static constexpr size_t MAX_HISTORY_ITEMS = 50;
typedef std::list<std::string> ConsoleHistoryList;
ConsoleHistoryList history;
ConsoleHistoryList::const_iterator history_it;
};
// ============================================================================
//
// ConsoleHistory class implementation
//
// ============================================================================
ConsoleHistory::ConsoleHistory()
{
resetPosition();
}
void ConsoleHistory::clear()
{
history.clear();
resetPosition();
}
void ConsoleHistory::resetPosition()
{
history_it = history.end();
}
void ConsoleHistory::addString(const std::string& str)
{
// only add the string if it's different from the most recent in history
if (!str.empty() && (history.empty() || str.compare(history.back()) != 0))
{
while (history.size() >= MAX_HISTORY_ITEMS)
history.pop_front();
history.push_back(str);
}
}
const std::string& ConsoleHistory::getString() const
{
if (history_it == history.end())
{
static std::string blank_string;
return blank_string;
}
return *history_it;
}
void ConsoleHistory::movePositionUp()
{
if (history_it != history.begin())
--history_it;
}
void ConsoleHistory::movePositionDown()
{
if (history_it != history.end())
++history_it;
}
void ConsoleHistory::dump()
{
for (const auto& it : history)
PrintFmt(PRINT_HIGH, " {}\n", it);
}
class ConsoleCompletions
{
std::vector<std::string> _completions;
size_t _maxlen;
public:
void add(const std::string& completion)
{
_completions.push_back(completion);
if (_maxlen < completion.length())
_maxlen = completion.length();
}
[[nodiscard]] const std::string& at(size_t index) const
{
return _completions.at(index);
}
void clear()
{
_completions.clear();
_maxlen = 0;
}
[[nodiscard]] bool empty() const
{
return _completions.empty();
}
//
// Get longest common substring of completions.
//
[[nodiscard]] std::string getCommon() const
{
bool diff = false;
std::string common;
for (size_t index = 0;; index++)
{
char compare = '\xFF';
for (const auto& comp : _completions)
{
if (index >= comp.length())
{
// End of string, this is an implicit failed match.
diff = true;
break;
}
if (compare == '\xFF')
{
// Set character to compare against.
compare = comp.at(index);
}
else if (compare != comp.at(index))
{
// Found a different character.
diff = true;
break;
}
}
// If we found a different character, break, otherwise add it
// to the string we're going to return and try the next index.
if (diff == true)
break;
else
common += compare;
}
return common;
}
[[nodiscard]] size_t getMaxLen() const
{
return _maxlen;
}
[[nodiscard]] size_t size() const
{
return _completions.size();
}
};
// ============================================================================
//
// Console object definitions
//
// ============================================================================
typedef std::list<ConsoleLine> ConsoleLineList;
static ConsoleLineList Lines;
static ConsoleCommandLine CmdLine;
static ConsoleHistory History;
static ConsoleCompletions CmdCompletions;
/****** Tab completion code ******/
typedef std::map<std::string, size_t> tabcommand_map_t; // name, use count
tabcommand_map_t& TabCommands()
{
static tabcommand_map_t _TabCommands;
return _TabCommands;
}
void C_AddTabCommand(const char* name)
{
const tabcommand_map_t::iterator it = TabCommands().find(StdStringToLower(name));
if (it != TabCommands().end())
TabCommands()[name]++;
else
TabCommands()[name] = 1;
}
void C_RemoveTabCommand(const char* name)
{
const tabcommand_map_t::iterator it = TabCommands().find(StdStringToLower(name));
if (it != TabCommands().end())
if (!--it->second)
TabCommands().erase(it);
}
//
// Start tab cycling.
//
// Note that this initial state points to the front and back of the completions
// index, which is a unique state that is not possible to get into after you
// start hitting tab.
//
static void TabCycleStart()
{
::ShouldTabCycle = true;
::NextCycleIndex = 0;
::PrevCycleIndex = CmdCompletions.size() - 1;
}
//
// Given an specific completion index, determine the next and previous index.
//
static void TabCycleSet(size_t index)
{
// Find the next index.
if (index >= CmdCompletions.size() - 1)
::NextCycleIndex = 0;
else
::NextCycleIndex = index + 1;
// Find the previous index.
if (index == 0)
::PrevCycleIndex = CmdCompletions.size() - 1;
else
::PrevCycleIndex = index - 1;
}
//
// Get out of the tab cycle state.
//
static void TabCycleClear()
{
::ShouldTabCycle = false;
::NextCycleIndex = 0;
::PrevCycleIndex = 0;
}
enum TabCompleteDirection
{
TAB_COMPLETE_FORWARD,
TAB_COMPLETE_BACKWARD
};
//
// Handle tab-completion and cycling.
//
static void TabComplete(TabCompleteDirection dir)
{
// Pressing tab twice in a row starts cycling the completion.
if (::ShouldTabCycle && ::CmdCompletions.size() > 0)
{
if (dir == TAB_COMPLETE_FORWARD)
{
const size_t index = ::NextCycleIndex;
::CmdLine.replaceString(::CmdCompletions.at(index));
TabCycleSet(index);
}
else if (dir == TAB_COMPLETE_BACKWARD)
{
const size_t index = ::PrevCycleIndex;
::CmdLine.replaceString(::CmdCompletions.at(index));
TabCycleSet(index);
}
return;
}
// Clear the completions.
::CmdCompletions.clear();
// Figure out what we need to autocomplete.
size_t tabStart = ::CmdLine.text.find_first_not_of(' ', 0);
if (tabStart == std::string::npos)
tabStart = 0;
size_t tabEnd = ::CmdLine.text.find(' ', 0);
if (tabEnd == std::string::npos)
tabEnd = ::CmdLine.text.length();
const size_t tabLen = tabEnd - tabStart;
// Don't complete if the cursor is past the command.
if (::CmdLine.cursor_position >= tabEnd + 1)
return;
// Find all substrings.
const std::string sTabPos = StdStringToLower(::CmdLine.text.substr(tabStart, tabLen));
const char* cTabPos = sTabPos.c_str();
tabcommand_map_t::iterator it = TabCommands().lower_bound(sTabPos);
for (; it != TabCommands().end(); ++it)
{
if (strncmp(cTabPos, it->first.c_str(), sTabPos.length()) == 0)
::CmdCompletions.add(it->first.c_str());
}
if (::CmdCompletions.size() > 1)
{
// Get common substring of all completions.
const std::string common = ::CmdCompletions.getCommon();
::CmdLine.replaceString(common);
}
else if (::CmdCompletions.size() == 1)
{
// We found a single completion, use it.
::CmdLine.replaceString(::CmdCompletions.at(0));
::CmdCompletions.clear();
}
// If we press tab twice, cycle the command.
TabCycleStart();
}
static void setmsgcolor(int index, const char *color);
cvar_t msglevel("msg", "0", "", CVARTYPE_INT, CVAR_ARCHIVE | CVAR_NOENABLEDISABLE);
CVAR_FUNC_IMPL(msg0color)
{
setmsgcolor(0, var.cstring());
}
CVAR_FUNC_IMPL(msg1color)
{
setmsgcolor(1, var.cstring());
}
CVAR_FUNC_IMPL(msg2color)
{
setmsgcolor(2, var.cstring());
}
CVAR_FUNC_IMPL(msg3color)
{
setmsgcolor(3, var.cstring());
}
CVAR_FUNC_IMPL(msg4color)
{
setmsgcolor(4, var.cstring());
}
CVAR_FUNC_IMPL(msgmidcolor)
{
setmsgcolor(PRINTLEVELS-1, var.cstring());
}
CVAR_FUNC_IMPL(con_scaletext)
{
C_NewModeAdjust();
}
// NES - Activating this locks the scroll position in place when
// scrolling up. Otherwise, any new messages will
// automatically pull the console back to the bottom.
//
// con_scrlock 0 = All new lines bring scroll to the bottom.
// con_scrlock 1 = Only input commands bring scroll to the bottom.
// con_scrlock 2 = Nothing brings scroll to the bottom.
EXTERN_CVAR(con_scrlock)
//
// C_InitConCharsFont
//
// Loads the CONCHARS lump from disk and converts it to the format used by
// the console for printing text.
//
void C_InitConCharsFont()
{
static palindex_t transcolor = 0xF7;
// Load the CONCHARS lump and convert it from patch_t format
// to a raw linear byte buffer with a background color of 'transcolor'
IWindowSurface* temp_surface = I_AllocateSurface(128, 128, 8);
temp_surface->lock();
// fill with color 'transcolor'
for (int y = 0; y < 128; y++)
memset(temp_surface->getBuffer() + y * temp_surface->getPitchInPixels(), transcolor, 128);
// paste the patch into the linear byte bufer
const DCanvas* canvas = temp_surface->getDefaultCanvas();
canvas->DrawPatch(W_CachePatch("CONCHARS"), 0, 0);
ConChars = new byte[256*8*8*2];
byte* dest = ConChars;
for (int y = 0; y < 16; y++)
{
for (int x = 0; x < 16; x++)
{
const byte* source = temp_surface->getBuffer() + x * 8 + (y * 8 * temp_surface->getPitch());
for (int z = 0; z < 8; z++)
{
for (int a = 0; a < 8; a++)
{
const byte val = source[a];
if (val == transcolor)
{
dest[a] = 0x00;
dest[a + 8] = 0xff;
}
else
{
dest[a] = val;
dest[a + 8] = 0x00;
}
}
dest += 16;
source += temp_surface->getPitch();
}
}
}
temp_surface->unlock();
I_FreeSurface(temp_surface);
}
//
// C_ShutdownConCharsFont
//
void STACK_ARGS C_ShutdownConCharsFont()
{
delete [] ConChars;
ConChars = NULL;
}
//
// C_StringWidth
//
// Determines the width of a string in the CONCHARS font
//
static int C_StringWidth(const char* str)
{
int width = 0;
while (*str)
{
// skip over color markup escape codes
if (str[0] == TEXTCOLOR_ESCAPE && str[1] != '\0')
{
str += 2;
continue;
}
width += ConCharSize;
str++;
}
return width;
}
void C_ClearCommand()
{
::CmdLine.clear();
::CmdCompletions.clear();
}
//
// C_InitConsoleBackground
//
// Allocates background surface and draws the appropriate background patch
//
void C_InitConsoleBackground()
{
const patch_t* bg_patch = W_CachePatch(W_GetNumForName("CONBACK"));
background_surface = I_AllocateSurface(bg_patch->width(), bg_patch->height(), 8);
background_surface->lock();
background_surface->getDefaultCanvas()->DrawPatch(bg_patch, 0, 0);
background_surface->unlock();
}
//
// C_ShutdownConsoleBackground
//
// Frees the background_surface
//
void STACK_ARGS C_ShutdownConsoleBackground()
{
I_FreeSurface(background_surface);
}
//
// C_ShutdownConsole
//
void STACK_ARGS C_ShutdownConsole()
{
Lines.clear();
History.clear();
CmdLine.clear();
CmdCompletions.clear();
}
//
// C_InitConsole
//
void C_InitConsole()
{
C_NewModeAdjust();
C_FullConsole();
}
//
// C_SetConsoleDimensions
//
static void C_SetConsoleDimensions(int width, int height)
{
static int old_width = -1, old_height = -1, old_scale = -1;
if (width != old_width || height != old_height || ConScale != old_scale)
{
ConCols = width / ConCharSize - 2;
// ConCols has changed so any lines of text that are currently wrapped
// need to be adjusted.
for (ConsoleLineList::iterator current_line_it = Lines.begin();
current_line_it != Lines.end(); ++current_line_it)
{
if (current_line_it->wrapped)
{
// The current line has wrapped around to the next line. Append the
// next line to the current line for now.
ConsoleLineList::iterator next_line_it = current_line_it;
++next_line_it;
if (next_line_it != Lines.end())
{
current_line_it->join(*next_line_it);
Lines.erase(next_line_it);
}
}
if ((unsigned)C_StringWidth(current_line_it->text.c_str()) > ConCols*ConCharSize)
{
ConsoleLineList::iterator next_line_it = current_line_it;
++next_line_it;
ConsoleLine new_line = current_line_it->split(ConCols*ConCharSize);
Lines.insert(next_line_it, new_line);
}
}
old_width = width;
old_height = height;
old_scale = ConScale;
}
}
static void setmsgcolor(int index, const char *color)
{
int i = atoi(color);
if (i < 0 || i >= NUM_TEXT_COLORS)
i = 0;
PrintColors[index] = i;
}
//
// C_AddNotifyString
//
// Prioritise messages on top of screen
// Break up the lines so that they wrap around the screen boundary
//
void C_AddNotifyString(int printlevel, const char* color_code, const char* source)
{
static enum
{
NEWLINE,
APPENDLINE,
REPLACELINE
} addtype = NEWLINE;
char work[MAX_LINE_LENGTH];
brokenlines_t *lines;
const size_t len = strlen(source);
if ((printlevel != 128 && !show_messages) || len == 0 ||
(gamestate != GS_LEVEL && gamestate != GS_INTERMISSION) )
return;
// Do not display filtered chat messages
if (printlevel == PRINT_FILTERCHAT)
return;
// Do not display filtered normal messages
if (printlevel == PRINT_FILTERHIGH)
return;
const int width = I_GetSurfaceWidth() / V_TextScaleXAmount();
if (addtype == APPENDLINE && NotifyStrings[NUMNOTIFIES-1].printlevel == printlevel)
{
snprintf(work, MAX_LINE_LENGTH, "%s%s", NotifyStrings[NUMNOTIFIES - 1].text, source);
lines = V_BreakLines(width, work);
}
else
{
lines = V_BreakLines(width, source);
addtype = (addtype == APPENDLINE) ? NEWLINE : addtype;
}
if (!lines)
return;
for (int i = 0; lines[i].width != -1; i++)
{
if (addtype == NEWLINE)
memmove(&NotifyStrings[0], &NotifyStrings[1], sizeof(struct NotifyText) * (NUMNOTIFIES-1));
M_StringCopy((char *)NotifyStrings[NUMNOTIFIES-1].text, lines[i].string, 256);
NotifyStrings[NUMNOTIFIES-1].timeout = gametic + (con_notifytime.asInt() * TICRATE);
NotifyStrings[NUMNOTIFIES-1].printlevel = printlevel;
addtype = NEWLINE;
}
V_FreeBrokenLines(lines);
switch (source[len-1])
{
case '\r':
addtype = REPLACELINE;
break;
case '\n':
addtype = NEWLINE;
break;
default:
addtype = APPENDLINE;
break;
}
}
//
// C_PrintStringStdOut
//
// Prints the given string to stdout, stripping away any color markup
// escape codes.
//
static size_t C_PrintStringStdOut(std::string str)
{
StripColorCodes(str);
fmt::print("{}", str);
fflush(stdout);
return str.length();
}
//
// C_PrintString
//
// Provide our own Printf() that is sensitive of the
// console status (in or out of game).
//
static size_t C_PrintString(int printlevel, const char* color_code, const char* outline)
{
if (I_VideoInitialized() && !midprinting)
{
const bool noPickups = printlevel == PRINT_PICKUP && !::message_showpickups;
const bool noObits = printlevel == PRINT_OBITUARY && !::message_showobituaries;
if (!noPickups && !noObits)
C_AddNotifyString(printlevel, color_code, outline);
}
// Revert filtered chat to a normal chat to display to the console
if (printlevel == PRINT_FILTERCHAT)
printlevel = PRINT_CHAT;
if (printlevel == PRINT_FILTERHIGH)
printlevel = PRINT_HIGH;
const char* line_start = outline;
const char* line_end = line_start;
// [SL] the user's message color preference overrides the given color_code
// ...unless it's supposed to be formatted bold.
static char printlevel_color_code[3];
if (color_code && color_code[1] != '+' && printlevel >= 0 && printlevel < PRINTLEVELS)
{
snprintf(printlevel_color_code, sizeof(printlevel_color_code), "\034%c", 'a' + PrintColors[printlevel]);
color_code = printlevel_color_code;
}
while (*line_start)
{
// Find the next line-breaking character (\n or \0) and set
// line_end to point to it.
line_end = line_start;
while (*line_end != '\n' && *line_end != '\0')
line_end++;
const size_t len = line_end - line_start;
char str[MAX_LINE_LENGTH + 1];
strncpy(str, line_start, len);
str[len] = '\0';
const bool wrap_new_line = *line_end != '\n';
ConsoleLine new_line(str, color_code, wrap_new_line);
// Add a new line to ConsoleLineList if the last line in ConsoleLineList
// ends in \n, or add onto the last line if does not.
if (!Lines.empty() && Lines.back().wrapped)
Lines.back().join(new_line);
else
Lines.push_back(new_line);
// Wrap the current line if it's too long.
const unsigned int line_width = C_StringWidth(Lines.back().text.c_str());
if (line_width > ConCols*ConCharSize)
{
new_line = Lines.back().split(ConCols*ConCharSize);
Lines.push_back(new_line);
}
if (con_scrlock > 0 && RowAdjust != 0)
RowAdjust++;
else
RowAdjust = 0;
line_start = line_end;
if (*line_end == '\n')
line_start++;
}
return strlen(outline);
}
size_t C_BasePrint(const int printlevel, const char* color_code, const std::string& str)
{
extern bool gameisdead;
if (gameisdead)
return 0;
std::string newStr = str;
// denis - 0x07 is a system beep, which can DoS the console (lol)
// ToDo: there may be more characters not allowed on a consoleprint,
// maybe restrict a few ASCII stuff later on ?
for (auto& c : newStr)
{
if (c == 0x07)
c = '.';
}
// Prevents writing a whole lot of new lines to the log file
if (gamestate != GS_FORCEWIPE)
{
std::string logStr = newStr;
// [Nes] - Horizontal line won't show up as-is in the logfile.
for (auto& c : logStr)
{
if (c == '\35' || c == '\36' || c == '\37')
c = '=';
}
// Up the row buffer for the console.
// This is incremented here so that any time we
// print something we know about it. This feels pretty hacky!
// We need to know if there were any new lines being printed
// in our string.
const int newLineCount = std::count(logStr.begin(), logStr.end(), '\n');
if (ConRows < (unsigned int)con_buffersize.asInt())
ConRows += (newLineCount > 1) ? newLineCount + 1 : 1;
}
if (print_stdout && gamestate != GS_FORCEWIPE)
C_PrintStringStdOut(newStr);
if (!con_coloredmessages)
StripColorCodes(newStr);
C_PrintString(printlevel, color_code, newStr.c_str());
// Once done, log
if (LOG.is_open())
{
// Strip if not already done
if (con_coloredmessages)
StripColorCodes(newStr);
LOG << newStr;
LOG.flush();
}
#if defined (_WIN32) && defined(_DEBUG)
// [AM] Since we don't have stdout/stderr in a non-console Win32 app,
// this outputs the string to the "Output" window.
OutputDebugStringA(newStr.c_str());
#endif
return newStr.length();
}
void C_FlushDisplay()
{
for (auto& notify : NotifyStrings)
notify.timeout = 0;
}
void C_Ticker()
{
const int surface_height = I_GetSurfaceHeight();
if (ConsoleState == c_falling)
{
ConBottomStep += (surface_height * 2 / 25);
}
else if (ConsoleState == c_fallfull)
{
ConBottomStep += (surface_height * 2 / 15);
}
else if (ConsoleState == c_rising)
{
ConBottomStep -= (surface_height * 2 / 25);
}
else if (ConsoleState == c_risefull)
{
ConBottomStep -= (surface_height * 2 / 15);
}
else if (ConsoleState == c_up)
{
// Console suddenly snapped up? Don't interp it anymore.
ConBottomStep = 0;
}
if (ConsoleState != c_up)
{
if (ScrollState == SCROLLUP)
{
if (KeysCtrl)
{
RowAdjust += 16;
ScrollState = SCROLLNO;
}
else
RowAdjust++;
if (RowAdjust > ConRows - ConBottom / ConCharSize)
RowAdjust = ConRows - ConBottom / ConCharSize;
}
else if (ScrollState == SCROLLDN)
{
if (KeysCtrl)
{
RowAdjust-=16;
ScrollState = SCROLLNO;
} else
RowAdjust--;
if (RowAdjust < 0)
RowAdjust = 0;
}
if (RowAdjust + (ConBottom/ConCharSize) + 1 > (unsigned int)con_buffersize.asInt())
RowAdjust = con_buffersize.asInt() - (ConBottom/ConCharSize);
}
if (--CursorTicker <= 0)
{
cursoron ^= 1;
CursorTicker = C_BLINKRATE;
}
OInterpolation::getInstance().beginConsoleInterpolation(render_lerp_amount);
}
//
// C_DrawNotifyText
//
// Prints the HUD notification messages to the top of the HUD. Each of these
// messages will be displayed for a fixed amount of time before being removed.
//
static void C_DrawNotifyText()
{
if ((gamestate != GS_LEVEL && gamestate != GS_INTERMISSION) || menuactive)
return;
int ypos = 0;
for (const auto& notify : NotifyStrings)
{
if (notify.timeout > gametic)
{
if (!show_messages && notify.printlevel != 128)
continue;
int color;
if (notify.printlevel >= PRINTLEVELS)
color = CR_RED;
else
color = PrintColors[notify.printlevel];
screen->DrawTextStretched(color, 0, ypos, notify.text,
V_TextScaleXAmount(), V_TextScaleYAmount());
ypos += 8 * V_TextScaleYAmount();
}
}
}
void C_InitTicker(const char *label, unsigned int max)
{
TickerMax = max;
TickerLabel = label;
TickerAt = 0;
}
void C_SetTicker(unsigned int at)
{
TickerAt = at > TickerMax ? TickerMax : at;
}
//
// C_UseFullConsole
//
// Returns true if the console should be drawn fullscreen. This should be
// any time the client is not in a level, intermission or playing the demo loop.
static bool C_UseFullConsole()
{
return (gamestate != GS_LEVEL && gamestate != GS_DEMOSCREEN && gamestate != GS_INTERMISSION);
}
//
// C_AdjustBottom
//
// Changes ConBottom based on the console's state. In a fullscreen console,
// ConBottom should be the bottom of the screen. Otherwise, it should be
// the middle of the screen.
//
void C_AdjustBottom()
{
const unsigned int surface_height = I_GetSurfaceHeight();
if (ConsoleState == c_up)
ConBottom = 0;
else if (C_UseFullConsole())
ConBottom = surface_height;
else if (ConsoleState == c_down || ConBottom > surface_height / 2)
ConBottom = surface_height / 2;
// don't adjust if the console is raising or lowering because C_Ticker
// handles that already
}
//
// C_NewModeAdjust
//
// Reinitializes the console after a video mode change.
//
void C_NewModeAdjust()
{
const int surface_width = I_GetSurfaceWidth(), surface_height = I_GetSurfaceHeight();
ConScale = con_scaletext ? con_scaletext : MAX(1, static_cast<int>(std::round(surface_height / 450.0f)));
ConCharSize = 8 * ConScale;
if (I_VideoInitialized())
C_SetConsoleDimensions(surface_width, surface_height);
else
C_SetConsoleDimensions(80 * ConCharSize, 25 * ConCharSize);
// clear HUD notify text
C_FlushDisplay();
C_AdjustBottom();
}
//
// C_FullConsole
//
void C_FullConsole()
{
// SoM: disconnect effect.
if ((gamestate == GS_LEVEL || gamestate == GS_INTERMISSION) && ConsoleState == c_up && !menuactive)
screen->Dim(0, 0, I_GetSurfaceWidth(), I_GetSurfaceHeight());
ConsoleState = c_down;
TabCycleClear();
C_AdjustBottom();
}
//
// C_HideConsole
//
// Sets the console's state to hidden.
//
void C_HideConsole()
{
ConsoleState = c_up;
ConBottom = 0;
CmdLine.clear();
CmdCompletions.clear();
History.resetPosition();
}
static void PauseResumeSound(bool pause_sound)
{
if (gamestate != GS_LEVEL || multiplayer || demoplayback || netdemo.isPlaying())
{
return;
}
if (pause_sound)
{
S_PauseSound();
}
else
{
S_ResumeSound();
}
}
//
// C_ToggleConsole
//
// Toggles the console's state (if possible). This has no effect in fullscreen
// console mode.
//
void C_ToggleConsole()
{
if (C_UseFullConsole())
return;
const bool bring_console_down =
(ConsoleState == c_up || ConsoleState == c_rising || ConsoleState == c_risefull);
if (bring_console_down)
{
if (C_UseFullConsole())
ConsoleState = c_fallfull;
else
ConsoleState = c_falling;
TabCycleClear();
}
else
{
if (ConBottom == I_GetSurfaceHeight())
ConsoleState = c_risefull;
else
ConsoleState = c_rising;
C_FlushDisplay();
}
CmdLine.clear();
CmdCompletions.clear();
History.resetPosition();
PauseResumeSound(bring_console_down);
}
//
// C_DisplayTicker
//
// This allows us to tic things separate from the gametics
// So we can enable smoother display of console falling/rising
//
void C_DisplayTicker()
{
unsigned int surface_height = I_GetSurfaceHeight();
// Attaching ConBottom to gametic will still cause falling/rising to
// be pinned to the gametic i.e. 35fps.
// Interp where the console bottom should be based on current and previous
fixed_t bottom =
OInterpolation::getInstance().getInterpolatedConsoleBottom(render_lerp_amount);
if (ConsoleState == c_falling)
{
ConBottom = bottom;
if (ConBottom >= surface_height / 2)
{
ConBottom = surface_height / 2;
ConBottomStep = surface_height / 2;
ConsoleState = c_down;
}
}
else if (ConsoleState == c_fallfull)
{
ConBottom = bottom;
if (ConBottom >= surface_height)
{
ConBottom = surface_height;
ConBottomStep = surface_height;
ConsoleState = c_down;
}
}
else if (ConsoleState == c_rising)
{
ConBottom = bottom;
if (ConBottom <= 0)
{
ConsoleState = c_up;
ConBottom = 0;
ConBottomStep = 0;
}
}
else if (ConsoleState == c_risefull)
{
ConBottom = bottom;
if (ConBottom <= 0)
{
ConsoleState = c_up;
ConBottom = 0;
ConBottomStep = 0;
}
}
if (RowAdjust + (ConBottom / ConCharSize) + 1 > (unsigned int)con_buffersize.asInt())
RowAdjust = con_buffersize.asInt() - (ConBottom / ConCharSize);
}
//
// C_DrawConsole
//
void C_DrawConsole()
{
#define CONPX(A) ((A) * ConScale)
IWindowSurface* primary_surface = I_GetPrimarySurface();
int primary_surface_width = primary_surface->getWidth();
int primary_surface_height = primary_surface->getHeight();
int left = CONPX(8);
int lines = (ConBottom - CONPX(12)) / CONPX(8);
int offset;
if (lines * CONPX(8) > ConBottom - CONPX(16))
offset = CONPX(-16);
else
offset = CONPX(-12);
if (ConsoleState == c_up || ConBottom == 0)
{
C_DrawNotifyText();
return;
}
if (!C_UseFullConsole())
{
// Non-fullscreen console. Overlay a translucent background.
screen->Dim(0, 0, primary_surface_width, ConBottom);
}
else if (::background_surface != NULL)
{
// Fullscreen console. Blit the image in the center of a black background.
screen->Clear(0, 0, primary_surface_width, primary_surface_height, argb_t(0, 0, 0));
int x = (primary_surface_width - background_surface->getWidth()) / 2;
int y = (primary_surface_height - background_surface->getHeight()) / 2;
background_surface->lock();
primary_surface->blit(background_surface, 0, 0,
background_surface->getWidth(), background_surface->getHeight(),
x, y, background_surface->getWidth(), background_surface->getHeight());
background_surface->unlock();
}
if (ConBottom >= CONPX(12))
{
if (CL_IsDownloading())
{
// Use the remaining space for a download bar.
size_t chars = (primary_surface_width - CONPX(8)) / C_StringWidth(" ");
std::string download;
// Stamp out the text bits.
std::string filename = M_ExtractFileName(CL_DownloadFilename());
if (filename.empty())
filename = "...";
OTransferProgress progress = CL_DownloadProgress();
std::string dlnow;
StrFormatBytes(dlnow, progress.dlnow);
std::string dltotal;
StrFormatBytes(dltotal, progress.dltotal);
download = fmt::sprintf("%s: %s/%s", filename.c_str(), dlnow.c_str(),
dltotal.c_str());
// Avoid divide by zero.
if (progress.dltotal == 0)
progress.dltotal = 1;
// Stamp out the bar...if we have enough room - if we at tiny
// resolutions we may not.
size_t dltxtlen = download.length();
size_t barchars = chars - dltxtlen;
if (barchars >= 2 && barchars <= chars)
{
download.resize(chars);
for (size_t i = 0; i < barchars; i++)
{
char ch = '\30'; // empty middle
if (i == 0)
ch = '\27'; // empty left
else if (i == barchars - 1)
ch = '\31'; // empty right
double barpct = i / (double)barchars;
double dlpct = progress.dlnow / (double)progress.dltotal;
if (dlpct > barpct)
ch += 3; // full bar
download.at(i + dltxtlen) = ch;
}
}
// Draw the thing.
screen->PrintStr(left + CONPX(2), ConBottom - CONPX(12), download.c_str(), CR_GREEN, true, ConScale);
}
else
{
const char* version = NiceVersion();
// print the Odamex version in gold in the bottom right corner of console
screen->PrintStr(primary_surface_width - CONPX(8) - C_StringWidth(version),
ConBottom - CONPX(12), version, CR_ORANGE, true, ConScale);
}
if (TickerMax)
{
char tickstr[256];
size_t i, tickend = ConCols - primary_surface_width / CONPX(90) - 6;
size_t tickbegin = 0;
if (TickerLabel)
{
tickbegin = strlen(TickerLabel) + 2;
tickend -= tickbegin;
snprintf(tickstr, 256, "%s: ", TickerLabel);
}
if (tickend > 256 - 8)
tickend = 256 - 8;
tickstr[tickbegin] = -128;
memset(tickstr + tickbegin + 1, 0x81, tickend - tickbegin);
tickstr[tickend + 1] = -126;
tickstr[tickend + 2] = ' ';
i = tickbegin + 1 + (TickerAt * (tickend - tickbegin - 1)) / TickerMax;
if (i > tickend)
i = tickend;
tickstr[i] = -125;
size_t buflen = 256 - tickend - 3;
snprintf(tickstr + tickend + 3, buflen, "%u%%", (TickerAt * 100) / TickerMax);
screen->PrintStr(CONPX(8), ConBottom - CONPX(12), tickstr, -1, true, ConScale);
}
}
if (menuactive)
return;
if (lines > 0)
{
// First draw any completions, if we have any.
if (!::CmdCompletions.empty())
{
// True if we have too many completions to render all of them.
bool cOverflow = false;
// We want at least 8-space tabs.
size_t cTabLen = (::CmdCompletions.getMaxLen() + 1);
if (cTabLen < 8)
cTabLen = 8;
// How many columns can we fit on the screen at one time?
size_t cColumns = ::ConCols / cTabLen;
if (cColumns == 0)
cColumns += 1;
// Given the number of columns, how many lines do we need?
size_t cLines = ::CmdCompletions.size() / cColumns;
if (::CmdCompletions.size() % cColumns != 0)
cLines += 1;
// Currently we cap the number of completion lines to 5
if (cLines > 5)
{
cLines = 5;
cOverflow = true;
}
// Offset our standard console printing.
if (cOverflow)
lines -= cLines + 1;
else
lines -= cLines;
static char rowstring[MAX_LINE_LENGTH];
// Completions are rendered top to bottom in columns like a
// backwards "N".
for (size_t l = 0; l < cLines; l++)
{
// Prepare a row string to copy completions into.
memset(rowstring, ' ', ARRAY_LENGTH(rowstring));
size_t col = 0;
for (size_t c = 0; c < cColumns; c++)
{
// Turn our current line/column into an index.
size_t index = (c * cLines) + l;
if (index >= ::CmdCompletions.size())
{
rowstring[col] = '\0';
break;
}
// Copy our completion into the row.
const std::string& str = ::CmdCompletions.at(index);
memcpy(&rowstring[col], str.c_str(), str.length());
col += cTabLen;
if (c + 1 == cColumns)
rowstring[col - 1] = '\0';
else
rowstring[col - 1] = ' ';
}
screen->PrintStr(left, offset + (lines + l + 1) * CONPX(8), rowstring,
CR_YELLOW, true, ConScale);
}
// Render an overflow message if necessary.
if (cOverflow)
{
snprintf(rowstring, ARRAY_LENGTH(rowstring), "...and %zu more...",
::CmdCompletions.size() - (cLines * cColumns));
screen->PrintStr(left, offset + (lines + cLines + 1) * CONPX(8), rowstring,
CR_YELLOW, true, ConScale);
}
}
// find the ConsoleLine that will be printed to bottom of the console
ConsoleLineList::reverse_iterator current_line_it = Lines.rbegin();
for (unsigned i = 0; i < RowAdjust && current_line_it != Lines.rend(); i++)
++current_line_it;
// print as many ConsoleLines as will fit in the screen, starting at the bottom
for (; lines > 1 && current_line_it != Lines.rend(); lines--, ++current_line_it)
{
const char* str = current_line_it->text.c_str();
const char* color_code = current_line_it->color_code.c_str();
int color = color_code[0] != '\0' ? V_GetTextColor(color_code) : CR_GRAY;
screen->PrintStr(left, offset + lines * CONPX(8), str, color, true, ConScale);
}
if (ConBottom >= CONPX(20))
{
screen->PrintStr(left, ConBottom - CONPX(20), "]", CR_TAN, true, ConScale);
size_t cmdline_len = std::min<size_t>(CmdLine.text.length() - CmdLine.scrolled_columns, ConCols - 1);
if (cmdline_len)
{
char str[MAX_LINE_LENGTH];
strncpy(str, CmdLine.text.c_str() + CmdLine.scrolled_columns, cmdline_len);
str[cmdline_len] = '\0';
bool use_color_codes = false;
screen->PrintStr(left + CONPX(8), ConBottom - CONPX(20), str, CR_GRAY, use_color_codes, ConScale);
}
if (cursoron)
{
const char str[] = "_";
size_t cursor_offset = CmdLine.cursor_position - CmdLine.scrolled_columns;
screen->PrintStr(left + CONPX(8) + CONPX(8) * cursor_offset, ConBottom - CONPX(20), str, CR_TAN, true, ConScale);
}
if (RowAdjust && ConBottom >= CONPX(28))
{
// Indicate that the view has been scrolled up (10)
// and if we can scroll no further (12)
const char scrolled_up_str[] = "\012"; // 10 = \012 octal
const char no_scroll_str[] = "\014"; // 12 = \014 octal
const char* str = (RowAdjust + ConBottom/CONPX(8) < ConRows) ? scrolled_up_str : no_scroll_str;
screen->PrintStr(0, ConBottom - CONPX(28), str, -1, true, ConScale);
}
}
}
#undef CONPX
}
static bool C_HandleKey(const event_t* ev)
{
const int ch = ev->data1;
const char* cmd = Bindings.GetBind(ev->data1).c_str();
if (Key_IsMenuKey(ch) || (cmd && stricmp(cmd, "toggleconsole") == 0))
{
// don't eat the Esc key if we're in full console
// let it be processed elsewhere (to bring up the menu)
if (C_UseFullConsole())
return false;
C_ToggleConsole();
return true;
}
#ifdef __SWITCH__
if (ev->data1 == OKEY_JOY3)
{
char oldtext[64], text[64], fulltext[65];
M_StringCopy(text, CmdLine.text.c_str(), 64);
// Initiate the console
NX_SetKeyboard(text, 64);
// No action if no text or same as earlier
if (!strlen(text) || !strcmp(oldtext, text))
return true;
// add command line text to history
History.addString(text);
History.resetPosition();
PrintFmt(127, "]{}\n", text);
AddCommandString(text);
CmdLine.clear();
}
#endif
// Add modifiers for these keys
KeysCtrl = (ev->mod & OMOD_CTRL);
KeysAlt = (ev->mod & OMOD_ALT && !(ev->mod & OMOD_RALT && ev->mod & OMOD_LCTRL)); // Alt without AltGr
KeysShifted = (ev->mod & OMOD_SHIFT);
NumLockEnabled = (ev->mod & OMOD_NUM);
switch (ch)
{
case OKEY_BACKSPACE:
if (KeysCtrl)
{
CmdLine.deleteLeftWord();
TabCycleClear();
return true;
}
else
{
CmdLine.backspace();
TabCycleClear();
return true;
}
case OKEY_MOUSE3:
// Paste from clipboard - add each character to command line
CmdLine.insertString(I_GetClipboardText());
CmdCompletions.clear();
TabCycleClear();
return true;
}
// General keys used by all systems
{
if (Key_IsHomeKey(ch, NumLockEnabled))
{
CmdLine.moveCursorHome();
return true;
}
else if (Key_IsEndKey(ch, NumLockEnabled))
{
CmdLine.moveCursorEnd();
return true;
}
else if (Key_IsDelKey(ch, NumLockEnabled))
{
if (KeysAlt)
{
CmdLine.deleteRightWord();
TabCycleClear();
return true;
}
else
{
CmdLine.deleteCharacter();
TabCycleClear();
return true;
}
}
else if (Key_IsPageUpKey(ch, NumLockEnabled))
{
if ((int)(ConRows) > (int)(ConBottom / ConCharSize))
{
if (KeysShifted)
// Move to top of console buffer
RowAdjust = ConRows - ConBottom / ConCharSize;
else
// Start scrolling console buffer up
ScrollState = SCROLLUP;
}
return true;
}
else if (Key_IsPageDownKey(ch, NumLockEnabled))
{
if (KeysShifted)
// Move to bottom of console buffer
RowAdjust = 0;
else
// Start scrolling console buffer down
ScrollState = SCROLLDN;
return true;
}
else if (Key_IsLeftKey(ch, NumLockEnabled))
{
if (KeysCtrl)
CmdLine.moveCursorLeftWord();
else
CmdLine.moveCursorLeft();
return true;
}
else if (Key_IsRightKey(ch, NumLockEnabled))
{
if (KeysCtrl)
CmdLine.moveCursorRightWord();
else
CmdLine.moveCursorRight();
return true;
}
else if (Key_IsUpKey(ch, NumLockEnabled))
{
// Move to previous entry in the command history
History.movePositionUp();
CmdLine.clear();
CmdLine.insertString(History.getString());
CmdCompletions.clear();
TabCycleClear();
return true;
}
else if (Key_IsDownKey(ch, NumLockEnabled))
{
// Move to next entry in the command history
History.movePositionDown();
CmdLine.clear();
CmdLine.insertString(History.getString());
CmdCompletions.clear();
TabCycleClear();
return true;
}
else if (Key_IsAcceptKey(ch))
{
// Execute command line (ENTER)
if (con_scrlock == 1) // NES - If con_scrlock = 1, send console scroll to bottom.
RowAdjust = 0; // con_scrlock = 0 does it automatically.
// add command line text to history
History.addString(CmdLine.text);
History.resetPosition();
PrintFmt(127, "]{}\n", CmdLine.text.c_str());
AddCommandString(CmdLine.text.c_str());
CmdLine.clear();
CmdCompletions.clear();
TabCycleClear();
return true;
}
else if (Key_IsTabulationKey(ch))
{
if (::KeysShifted)
TabComplete(TAB_COMPLETE_BACKWARD);
else
TabComplete(TAB_COMPLETE_FORWARD);
return true;
}
}
const char keytext = ev->data3;
if (KeysCtrl)
{
// handle key combinations
// NOTE: we have to use ev->data1 here instead of the
// localization-aware ev->data3 since SDL2 does not send a SDL_TEXTINPUT
// event when Ctrl is held down.
// Go to beginning of line
if (tolower(ev->data1) == 'a')
CmdLine.moveCursorHome();
// Go to end of line
if (tolower(ev->data1) == 'e')
CmdLine.moveCursorEnd();
// Paste from clipboard - add each character to command line
if (tolower(ev->data1) == 'v')
{
CmdLine.insertString(I_GetClipboardText());
TabCycleClear();
}
return true;
}
if (keytext)
{
// Add keypress to command line
CmdLine.insertCharacter(keytext);
TabCycleClear();
}
return true;
}
bool C_Responder(event_t *ev)
{
if (ConsoleState == c_up || ConsoleState == c_rising || ConsoleState == c_risefull || menuactive)
return false;
if (ev->type == ev_keyup)
{
// General Keys used by all systems
if (Key_IsPageUpKey(ev->data1, NumLockEnabled) || Key_IsPageDownKey(ev->data1, NumLockEnabled))
{
ScrollState = SCROLLNO;
}
else
{
return false;
}
}
else if (ev->type == ev_keydown)
{
return C_HandleKey(ev);
}
if(ev->type == ev_mouse)
return true;
return false;
}
BEGIN_COMMAND(history)
{
History.dump();
}
END_COMMAND(history)
BEGIN_COMMAND(clear)
{
RowAdjust = 0;
C_FlushDisplay();
Lines.clear();
History.resetPosition();
CmdLine.clear();
CmdCompletions.clear();
}
END_COMMAND(clear)
BEGIN_COMMAND(echo)
{
if (argc > 1)
{
const std::string str = C_ArgCombine(argc - 1, (const char **)(argv + 1));
PrintFmt(PRINT_HIGH, "{}\n", str);
}
}
END_COMMAND(echo)
BEGIN_COMMAND(toggleconsole)
{
C_ToggleConsole();
}
END_COMMAND(toggleconsole)
/* Printing in the middle of the screen */
static brokenlines_t *MidMsg = NULL;
static int MidTicker = 0, MidLines;
EXTERN_CVAR(con_midtime)
void C_MidPrint(const char *msg, player_t *p, int msgtime)
{
unsigned int i;
const float fmsgtime = msgtime ? float(msgtime) : con_midtime;
if (MidMsg)
V_FreeBrokenLines(MidMsg);
if (msg)
{
midprinting = true;
// [Russell] - convert textual "\n" into the binary representation for
// line breaking
std::string str = msg;
for (size_t pos = str.find("\\n"); pos != std::string::npos; pos = str.find("\\n", pos))
{
str[pos] = '\n';
str.erase(pos+1, 1);
}
char *newmsg = strdup(str.c_str());
PrintFmt(PRINT_HIGH, "{}\n", newmsg);
midprinting = false;
if ( (MidMsg = V_BreakLines(I_GetSurfaceWidth() / V_TextScaleXAmount(), (byte *)newmsg)) )
{
MidTicker = (int)(fmsgtime * TICRATE) + gametic;
for (i = 0; MidMsg[i].width != -1; i++)
;
MidLines = i;
}
M_Free(newmsg);
}
else
MidMsg = NULL;
}
void C_DrawMid()
{
if (MidMsg)
{
const int surface_width = I_GetSurfaceWidth(), surface_height = I_GetSurfaceHeight();
const int xscale = V_TextScaleXAmount();
const int yscale = V_TextScaleYAmount();
const int line_height = 8 * yscale;
const int bottom = R_StatusBarVisible()
? ST_StatusBarY(surface_width, surface_height) : surface_height;
const int x = surface_width / 2;
int y = (bottom - line_height * MidLines) / 2;
for (int i = 0; i < MidLines; i++, y += line_height)
{
screen->DrawTextStretched(PrintColors[PRINTLEVELS-1],
x - xscale * (MidMsg[i].width / 2),
y, (byte *)MidMsg[i].string, xscale, yscale);
}
if (gametic >= MidTicker)
{
V_FreeBrokenLines(MidMsg);
MidMsg = NULL;
}
}
}
static brokenlines_t *GameMsg = NULL;
static int GameTicker = 0, GameColor = CR_GREY, GameLines;
// [AM] This is literally the laziest excuse of a copy-paste job I have ever
// done, but I really want CTF messages in time for 0.6.2. Please replace
// me eventually. The two statics above, the two functions below, and
// any direct calls to these two functions are all you need to remove.
void C_GMidPrint(const char* msg, int color, int msgtime)
{
unsigned int i;
const float fmsgtime = msgtime ? float(msgtime) : con_midtime;
if (GameMsg)
V_FreeBrokenLines(GameMsg);
if (msg)
{
// [Russell] - convert textual "\n" into the binary representation for
// line breaking
std::string str = msg;
for (size_t pos = str.find("\\n");pos != std::string::npos;pos = str.find("\\n", pos))
{
str[pos] = '\n';
str.erase(pos + 1, 1);
}
char *newmsg = strdup(str.c_str());
if ((GameMsg = V_BreakLines(I_GetSurfaceWidth() / V_TextScaleXAmount(), (byte *)newmsg)) )
{
GameTicker = (int)(fmsgtime * TICRATE) + gametic;
for (i = 0;GameMsg[i].width != -1;i++)
;
GameLines = i;
}
GameColor = color;
M_Free(newmsg);
}
else
{
GameMsg = NULL;
GameColor = CR_GREY;
}
}
void C_DrawGMid()
{
if (GameMsg)
{
const int surface_width = I_GetSurfaceWidth(), surface_height = I_GetSurfaceHeight();
const int xscale = V_TextScaleXAmount();
const int yscale = V_TextScaleYAmount();
const int line_height = 8 * yscale;
const int bottom = R_StatusBarVisible()
? ST_StatusBarY(surface_width, surface_height) : surface_height;
const int x = surface_width / 2;
int y = (bottom / 2 - line_height * GameLines) / 2;
for (int i = 0; i < GameLines; i++, y += line_height)
{
screen->DrawTextStretched(GameColor,
x - xscale * (GameMsg[i].width / 2),
y, (byte*)GameMsg[i].string, xscale, yscale);
}
if (gametic >= GameTicker)
{
V_FreeBrokenLines(GameMsg);
GameMsg = NULL;
}
}
}
// denis - moved secret discovery message to this function
EXTERN_CVAR(hud_revealsecrets)
void C_RevealSecret()
{
if(!hud_revealsecrets || !G_IsCoopGame() || !show_messages) // [ML] 09/4/06: Check for hud_revealsecrets
return; // NES - Also check for deathmatch
C_MidPrint("A secret is revealed!");
if (hud_revealsecrets == 1 || hud_revealsecrets == 3)
S_Sound(CHAN_INTERFACE, "misc/secret", 1, ATTN_NONE);
}
VERSION_CONTROL (c_console_cpp, "$Id$")