Describing the Quake Command System

Discuss programming topics for the various GPL'd game engine sources.
Spirit
Posts: 1065
Joined: Sat Nov 20, 2004 9:00 pm
Contact:

Re: Describing the Quake Command System

Post by Spirit »

Random link to reddit, some small C json parser was posted earlier and I thought of this thread: http://www.reddit.com/r/programming/com ... rser_in_c/
Improve Quaddicted, send me a pull request: https://github.com/SpiritQuaddicted/Quaddicted-reviews
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Describing the Quake Command System

Post by Baker »

I don't like certain things about the Quake console. I'm writing a console from nothingness and will be building "objects", so I can have multiple consoles if I want with a very simplistic interface.

Here are some things that the Quake console can't handle well:

1) Resizes affect the actual data in the console buffer. Now, to some extent this is unavoidable using a wrapping buffer of fixed column width. I'm addressing this by having 3 types of line breaking. A soft line break functions like a newline, except if you resize the console, you can convert all the soft line breaks into spaces and print it back into the console buffer. No loss of data. The third kind of line break is an implied line break.

2) Word wrapping essentially loses trailing spaces in the data. By introducing line breaks into the data, I can preserve those. Leading spaces simply aren't rendered.

The line breaking method itself is a bit of puzzle to write and keep in simplified form. I wanted the function concise, clear as it can be and to fit on my screen.

Code: Select all

// Note: Message_Print goes to the Console Log but not the Host_Log
void _Console_Print (fbool hostPrint, const char* in_text)
{
	const char *text = in_text;
	if (hostPrint)
		Host_Log (text);

	if (console.ready == False || console.console_text == NULL || text == NULL)
		return;

	while (*text)
	{
		int cursor, maxsize, stringlength = strlen(text);
		
		// Grab as much text as possible and throw it into the buffer
		wordlen = 0;
		maxsize = console.columns - console.cursor_column + 1;
		
		for (cursor = console.cursor_column; cursor < console.columns && text[cursor] && text[cursor] != '\n'; cursor ++)
		{
			if (text[cursor + 1] <= ' ')  // If the next character is a space, we assured to have breakable text that fits of X length
				wordlen = cursor - console.cursor_column + 1;
		}
		
		if (*text == '\n')										// HARD LINE BREAK: hit newline char
		{
			Console_Row_Advance (HARD_LINE_FEED);
			text ++;
		}
		else if (wordlen == maxsize)							// NO LINE BREAK:  simple row advance
		{
			memcpy (CURRENT_CELL, text, wordlen);  console.cursor_column += wordlen; text += wordlen;
			Console_Row_Advance (NO_LINE_FEED);
		}
		else if (wordlen > 0)									// SOFT line break:  word wrap
		{
			// Transfer wordlen to the console, put a soft carriage return after it and advance to next row
			memcpy (CURRENT_CELL, text, wordlen);  console.cursor_column += wordlen; text += wordlen;
			Console_Row_Advance (SOFT_LINE_FEED);
		}
		else if (String_WordLength(text) <= console.columns)	// Text too big for this line but fits on next
		{
			Console_Row_Advance (SOFT_LINE_FEED);
			// Copy nothing!  Let this run through again now that we advanced row.
		}
		else if (wordlen == 0)									// NO LINE BREAK:	Tough luck: Unbreakable text.
		{
			wordlen = maxsize;
			memcpy (CURRENT_CELL, text, wordlen);  console.cursor_column += wordlen; text += wordlen;
			Console_Row_Advance (NO_LINE_FEED);
		}
	}
}
More to do ...
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Describing the Quake Command System

Post by Baker »

Lossless resizeable console achieved. So more more work than I thought.

Had to think in 1980s terms of carriage returns and line feeds (i.e. move cursor to end of row and advance separately).

Ended up turning console into an object in an unintentional way, had to write a number of macros and some of them favored a pointer to the console and by the time I was done turned all functions into pointers to the console as convenience. For simplicity, resizing the console I had to copy the original console to feed text to the new one and kill the old one. So I wasn't specifically after turning the console into an object.

Dealing with white spaces issues and newlines in various situations was a pain.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Describing the Quake Command System

Post by Spike »

what about variable-width fonts?
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Describing the Quake Command System

Post by Baker »

Spike wrote:what about variable-width fonts?
I've had that in the back of my mind with this.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Describing the Quake Command System

Post by Baker »

Here's the code that I have that works:

Code: Select all

void Console_PrintText (console_t* printcon, const char* in_text)
{
	const char *text			= in_text;
	int stringlength			= strlen(in_text);
#ifdef _DEBUG
	const char* stuckcheck		= text;
	int stuckcheckcount			= 0;
#endif
	const char *output_row		= ROW_START_CELLTEXT(printcon, console.cursor_row);
	const char *output_cursor	= CURSOR_CELL(printcon);
	const char in_term			= in_text[stringlength - 1];
	
	if (console.ready == False || console.console_text == NULL || text == NULL)
		return;

	while (*text)
	{
		int cursor, maxsize, wordlen = 0, hitTerminator, leadingspaces = 0;
#ifdef _DEBUG
		if (text == stuckcheck)
		{
			if (++stuckcheckcount > 3)
				stuckcheckcount = stuckcheckcount;
		}
		else 
		{
			stuckcheck = text;
			stuckcheckcount = 0;
		}
#endif

		if (CURSOR_AT_END_OF_LINE (printcon))
		{
			// An end of line \n means we need to eat the line feed before advancing.			
			if (*text == '\n' && printcon->cursor_column == printcon->display_columns)	// HARD LINE BREAK: Print and advance to end of line
			{
				Text_Copy_Cursor_Advance (printcon, &text, 1);		// We can do this no matter what.  Even if past display_columns due
			}
			else Console_Row_Advance (printcon);
		}

		// Grab as much text as possible and throw it into the buffer
		maxsize = console.display_columns - console.cursor_column;
		
		for (cursor = 0; cursor < maxsize && text[cursor] && text[cursor] != '\n'; cursor ++)
		{
			if (text[cursor] > ' ' && text[cursor + 1] <= ' ')  // If the next character is a space, we assured to have breakable text that fits of X length
			{
				wordlen = cursor + 1;
			}
			else if (text[cursor] <= ' ' && wordlen == 0)
				leadingspaces ++;
		}

		if (wordlen == 0)
			wordlen = leadingspaces;

		hitTerminator = (cursor == maxsize) ? False : True;

		if (*text == '\n')										// HARD LINE BREAK: Print and advance to end of line
		{
			Text_Copy_Cursor_Advance (printcon, &text, 1);		// We can do this no matter what.  Even if past display_columns due
			Console_Row_End_of_Line (printcon);					// to 1 character of column padding.
		}
		else if (wordlen == maxsize)							// NO LINE BREAK:  simple row advance
		{
			Text_Copy_Cursor_Advance (printcon, &text, wordlen);
			// Do not advance line, next iteration will

		}
		else if (wordlen > 0 && hitTerminator)					// HARD line break on NEXT iteration so don't advance
		{
			Text_Copy_Cursor_Advance (printcon, &text, wordlen);
		}
		else if (wordlen > 0 && hitTerminator == False)			// SOFT line break:  word wrap
		{
			Text_Copy_Cursor_Advance (printcon, &text, wordlen);
		}
		else if (String_WordLength(text) <= printcon->display_columns && leadingspaces == 0)	// Text too big for this line but fits on next (We can always break spaces)
		{
			Console_Row_End_of_Line (printcon);
		}
		else if (wordlen == 0)									// NO LINE BREAK:	Tough luck: Unbreakable text.
		{
			wordlen = maxsize;
			Text_Copy_Cursor_Advance (printcon, &text, wordlen);
		}
		// End of text segment
	}
	// End of text

The "wordlen" variable ... in theory, in the look that determines the wordlen, a look up each character's width in a table for variable font could be used per character while building that. On resize, I take the old console buffer and print each line into the new one.

I also have macros like this:

Code: Select all

#define ROW_START_CELLNUM(z, rown)	((rown) * z->columns)
#define ROW_START_CELLTEXT(z, rown)	&z->console_text[ROW_START_CELLNUM(z, rown)]
#define CELL_TEXT(z, rown, coln)	&z->console_text[ROW_START_CELLNUM(z, rown) + coln]

#define ROWS_POPULATED(z)			(z->rows_filled + 1) // Include incompletely filled row
#define ROW_RANGE_SIZE(z)			(ROWS_POPULATED(z) < z->rows ? ROWS_POPULATED(z) : z->rows)
#define ROW_RANGE_END(z)			z->cursor_row
#define ROW_RANGE_BEGIN(z)			Row_Wrap (z, z->cursor_row - ROW_RANGE_SIZE (z) + 1)		// 0-79 ... 79 - 80 + 1 = 0 
#define BACKSCROLL_MAX(z)			(ROWS_POPULATED(z) - 2)	// -1 to keep one displayed, -1 more for backscroll bar

#define DISPLAY_RANGE_SIZE(z)		z->display_rows
#define DISPLAY_RANGE_END(z)		Row_Wrap (z, z->cursor_row - z->backscroll_rows)
#define DISPLAY_RANGE_BEGIN(z)		Row_Wrap (z, DISPLAY_RANGE_END (z) - DISPLAY_RANGE_SIZE (z) + 1)		// 0-79 ... 79 - 80 + 1 = 0 

#define DISPLAY_RANGE_EMPTIES(z)	max (z->display_rows - ROWS_POPULATED(z), 0)

#define COLUMN_MEM_LENGTH(z)		z->columns
#define ERASE_ROW(z, row)			memset(ROW_START_CELLTEXT(z, z->cursor_row), 0, COLUMN_MEM_LENGTH(z))

#define CURSOR_CELL(z)				CELL_TEXT(z, z->cursor_row, z->cursor_column)
#define CURSOR_AT_END_OF_LINE(z)	(z->cursor_column >= z->display_columns)
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Describing the Quake Command System

Post by Spike »

what about colours? :)

FTE's console printing is a bit different. Con_Print expands the string into colours+unicode chars first of all (using a generic reusable function to do so), then generates a list of buffers separated by the \ns. If it gets a \r then it arranges for the next char received to clear the entire line. Dynamically allocated, the screen width is not a concern. The console buffer is just a linked list of \n separated tokens, each char has its own flags for colouring.
The lines are wrapped at render time, where annother generic function splits the line into sub-lines that fit the width of the console.
Yay for centerprints reusing these generic functions too. :)
Advantages:
Console resizing just works. No weird single characters when resizing the window, etc.
Font code handles char width. No console resizing on font changes.
Doesn't waste memory simply because the user is running widescreen.
Can change the number of lines stored in the buffer with a cvar, without needing to memcpy everything or restart the client.
Colours are directly stored.
Disadvantages:
Moving up/down by one line has a tendancy to move up/down according to its \n chars rather than screen estate.
Doubly linked lists... And tracking pointers...
Can be a little slower...

Quake2 does Con_Printf("Loading %s...\r", model->name); etc in quite a few places. Its a nice trick. the \r does nothing until the next char is printed, which overwrites the first char of the line, so print the line, refresh the screen, load the model, print the next model and you don't spam the console needlessly.
Interestingly the same works for Quake. Beware of mods that spam lines with \r in them for some sort of radar!
\r is also commonly used in team messages in quakeworld.
It can also be used to put words into other people's mouths... say "\rIdiot: I'm a <INSERT INSULT HERE>"
You probably want to block that.
Fun stuff really. :)
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Describing the Quake Command System

Post by Baker »

Spike wrote:what about colours? :)
I'm not that much of a fan of the whole "colors" thing. I look at the console more like a tool, I guess.

But that just my opinion.

I'm more in the simpler is better camp. But I understand the appeal of the colors thing.
FTE's console printing is a bit different. Con_Print expands the string into colours+unicode chars first of all (using a generic reusable function to do so), then generates a list of buffers separated by the \ns. If it gets a \r then it arranges for the next char received to clear the entire line. Dynamically allocated, the screen width is not a concern. The console buffer is just a linked list of \n separated tokens
I almost went towards the a linked list of newlines method. But then I thought about wrapping and wanted to just keep it simple where I can easily calculate a character for each row and col coordinate.

But clearly there are advantages to the linked newlines method. I just didn't want to have to calculate space to kill after the buffer is full and each print.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Spike
Posts: 2914
Joined: Fri Nov 05, 2004 3:12 am
Location: UK
Contact:

Re: Describing the Quake Command System

Post by Spike »

colours can be nice for certain things, like using different colours for easier chat.
That said, quake is not an instant messenger.
ezquake's implementation of colours scares me. I really hope its more sane nowadays.

Tbh, mostly I'm paranoid about resizing. Though I assume that if you flag each line as having an explicit or implicit newline, you can always reform the line to resize.

Main issue with variable-width-fonts and fixed-width-consoles is that you need to allocate enough memory for the user to make a line that contains a whole lot of, eg, i chars, thereby resulting in weird wrapping if you didn't give it enough memory for the line.
Different problems, different solutions. :)

Strictly speaking, the console buffer is a ring buffer. You presumably already allocate memory inside it, just with line granularity instead of byte granularity. A little more complex, but would work fine for variable-width fonts.
mh
Posts: 2292
Joined: Sat Jan 12, 2008 1:38 am

Re: Describing the Quake Command System

Post by mh »

Colours would actually be neat for chat if they used the player skin colours (top half of text - shirt, bottom half = pants) - an extra visual cue as to who is doing to the talking, and being able to match a chat string to a player model on-screen at a glance. Might be some issues with ease of visibility though.

Personally though I don't like the idea of over-sexing the console. Basic display and input are good enough for me.
We had the power, we had the space, we had a sense of time and place
We knew the words, we knew the score, we knew what we were fighting for
Baker
Posts: 3666
Joined: Tue Mar 14, 2006 5:15 am

Re: Describing the Quake Command System

Post by Baker »

Code: Select all

{"@clip",					Cmd_AtClip,						"Sends command results to clipboard."},
Created a command "@clip" to take the results of the rest of the command args (I essentially reduce cmd_argc by 1, copy the args down a position and run remaining command). Setup a clipboard buffer of adequate X size, but if it needs more space does a realloc.

Largely killed any need for this personally when I made the entities inspector for FitzQuake Mark V in release #1 (June 29?) where you can visually see what's up with entities because they have a caption over them (that changes when you change weapon, i.e. pressing 1 shows edict #, pressing 2 might show origin x, y, z over the entity, but pressing 7 might show nextthink or pressing 3 might show model name + model index).

Historically, it sucked that there wasn't any easy way to check out entities since the edicts command can't be contained in even a unreasonably large console buffer.

Still redirecting output of a command to the clipboard can be convenient.
The night is young. How else can I annoy the world before sunsrise? 8) Inquisitive minds want to know ! And if they don't -- well like that ever has stopped me before ..
Post Reply