1 /*
2  * Copyright (C) Volition, Inc. 1999.  All rights reserved.
3  *
4  * All source code herein is the property of Volition, Inc. You may not sell
5  * or otherwise commercially exploit the source or things you created based on the
6  * source.
7  *
8 */
9 
10 
11 #include "debugconsole/console.h"
12 #include "globalincs/alphacolors.h"
13 #include "globalincs/version.h"
14 #include "graphics/2d.h"
15 #include "graphics/font.h"
16 #include "io/key.h"
17 #include "io/timer.h"
18 #include "osapi/osapi.h"
19 
20 #include <algorithm>
21 #include <cmath>
22 
23 // ========================= GLOBALS =========================
24 bool Dc_debug_on;		//!< Flag used to print console and command debugging strings
25 
26 // Commands and History
27 SCP_string dc_command_str;		//!< The entered command line, arguments and all.
28 								//!< Is progressively culled from the left as commands, arguments are parsed in DCF's
29 
30 // Misc
31 bool debug_inited = FALSE;
32 uint lastline = 0;  // Number of lines written to the console since the last command was processed
33 
34 
35 // ========================= LOCALS ==========================
36 // Text Buffer
37 uint DBROWS = 80;   // # of buffer rows
38 uint DBCOLS = 80;   // # of buffer columns
39 size_t lastwhite = 0; // Last whitespace character encountered, used by putc for 'true' word wrapping
40 ubyte DTABS = 4;    //!< Tab size in spaces
41 
42 /**
43  * Human readable versions of the dc_token's. Primarily used in error diagnosis
44  */
45 static
46 const char *token_str[DCT_MAX_ITEMS] =
47 {
48 	"nothing",
49 	"string",
50 	"float",
51 	"integer",
52 	"unsigned integer",
53 	"byte",
54 	"unsigned byte",
55 	"boolean"
56 };
57 
58 SCP_deque<SCP_string> dc_buffer;
59 
60 // Display Window
61 uint DROWS = 25;
62 uint DCOLS = 80;
63 const uint DROWS_MIN = 25;
64 const uint DCOLS_MIN = 80;
65 uint dc_scroll_x;   // X scroll position (Leftmost character)
66 uint dc_scroll_y;   // Y scroll position (Topmost character)
67 int row_height;     // Row/Line height, in pixels
68 int col_width;      // Col/Character width, in pixels
69 int dc_font = font::FONT1;
70 
71 SCP_string dc_title;
72 
73 #define SCROLL_Y_MAX (DBROWS - DROWS)
74 
75 // Commands and History
76 uint DCMDS = 40;			// Max number of commands to remember
77 
78 SCP_deque<SCP_string> dc_history;
79 SCP_deque<SCP_string>::iterator last_oldcommand;		// Iterator to the last old command. Is reset to the start every new command push.
80 
81 const char dc_prompt[]= "> ";	// The prompt c_str
82 SCP_string dc_command_buf;		// The command line as shown in the console. Essentially an input buffer for dc_command_str
83 
84 // Local functions
85 /**
86  * @brief Initializes the debug console.
87  */
88 void dc_init(void);
89 
90 /**
91  * @brief Process the entered command string
92  */
93 void dc_do_command(SCP_string *cmd_str);
94 
95 /**
96  * @brief Draws the in-game console.
97  */
98 void dc_draw(bool show_prompt);
99 
100 /**
101  * Draws the cursor
102  * @param [in] cmd_string	The formatted command string displayed by dc_draw_window
103  * @param [in] x		The x screen position of the command string
104  * @param [in] y		The y screen position of the command string
105  */
106 void dc_draw_cursor( SCP_string &cmd_string, int x, int y );
107 
108 /**
109  * Draws the window text
110  */
111 void dc_draw_window(bool show_prompt);
112 
113 /**
114  * @brief   Stuffs the given character into the output buffer.
115  * @details Also handles tab alignment, newlines, and maintains the target.
116  */
117 void dc_putc(char c);
118 
119 // ============================== IMPLEMENTATIONS =============================
dc_do_command(SCP_string * cmd_str)120 void dc_do_command(SCP_string *cmd_str)
121 {
122 	/**
123 	 * Grab the first word from the cmd_str
124 	 *  If it is not a literal, ignore it "Invalid keyword: %s"
125 	 *  Search for the command...
126 	 *      Compare the word against valid commands
127 	 *      If command not found, ignore it "Invalid or unknown command: %s"\
128 	 *  Process the command...
129 	 *      Call the function to process the command (the rest of the command line is in the parser)
130 	 *          Function takes care of long_help and status depending on the mode.
131 	 */
132 	int i;
133 	SCP_string command;
134 	extern debug_command* dc_commands[];	// z64: I don't like this extern here, at all. Nope nope nope.
135 
136 	if (cmd_str->empty()) {
137 		return;
138 	}
139 
140 	dc_parse_init(*cmd_str);
141 
142 	dc_stuff_string_white(command);		// Grab the first token, presumably this is a command
143 
144 	for (i = 0; i < dc_commands_size; ++i) {
145 
146 		if (stricmp(dc_commands[i]->name, command.c_str()) == 0) {
147 			break;
148 		} // Else, continue
149 	}
150 
151 	if (i == dc_commands_size) {
152 		dc_printf("Command not found: '%s'\n", command.c_str());
153 		return;
154 	} // Else, we found our command
155 
156 	try {
157 		dc_commands[i]->func();	// Run the command!
158 
159 	} catch (const errParseString& err) {
160 		dc_printf("Require string(s) not found: \n");
161 		for (uint j = 0; j < err.expected_tokens.size(); ++j) {
162 			dc_printf("%i: %s\n", j, err.expected_tokens[j].c_str());
163 		}
164 
165 		dc_printf("Found '%s' instead\n", err.found_token.c_str());
166 
167 	} catch (const errParse& err) {
168 		dc_printf("Invalid argument. Expected %s, found '%s'\n", token_str[err.expected_type], err.found_token.c_str());
169 
170 	}
171 
172 	// dc_maybe_stuff_string is vulnerable to overflow. Once the errParseOverflow throw class (or w/e) gets
173 	// implemented, this last command should be put into its own try{} catch{} block.
174 	if (dc_maybe_stuff_string(command)) {
175 		dc_printf( "Ignoring the unused command line tail '%s'\n", command.c_str() );
176 	}
177 }
178 
dc_draw(bool show_prompt=FALSE)179 void dc_draw(bool show_prompt = FALSE)
180 {
181 	gr_clear();
182 	font::set_font(dc_font);
183 	gr_set_color_fast( &Color_bright );
184 	int w;
185 	gr_get_string_size(&w, nullptr, dc_title.c_str());
186 
187 	gr_string((gr_screen.clip_width - w) / 2, 3, dc_title.c_str(), GR_RESIZE_NONE );
188 
189 	gr_set_color_fast( &Color_normal );
190 
191 	dc_draw_window(show_prompt);
192 
193 	gr_flip();
194 }
195 
dc_draw_cursor(SCP_string & cmd_string,int x,int y)196 void dc_draw_cursor( SCP_string &cmd_string, int x, int y )
197 {
198 	int t;
199 	int w, h;	// gr_string width and height
200 
201 	t = timer_get_fixed_seconds() / (F1_0/3);
202 	if ( t & 1 ) {
203 		gr_get_string_size( &w, &h, cmd_string.c_str() );
204 
205 		w %= (DCOLS * col_width);
206 		//gr_string( w, debug_y*16, "_" );
207 		gr_rect(gr_screen.center_offset_x + (x + (w + 1)), gr_screen.center_offset_y + (y + (h + 1)), 2,
208 			fl2i(font::get_current_font()->getHeight()), GR_RESIZE_NONE);
209 	}
210 }
211 
dc_draw_window(bool show_prompt)212 void dc_draw_window(bool show_prompt)
213 {
214 	size_t cmd_lines;               // Number of lines for the command string
215 	size_t buffer_lines;            // Number of lines from the buffer to draw
216 	size_t i;                       // The current row we're drawing
217 	size_t j;                       // The current row of the command string we're drawing
218 	SCP_string out_str;             // The command string + prompt character
219 	SCP_string::iterator str_it;    // Iterator to out_str
220 
221 	out_str = dc_prompt + dc_command_buf;
222 	cmd_lines = (out_str.size() / DCOLS) + 1;
223 	if (show_prompt) {
224 		buffer_lines = DROWS - cmd_lines;
225 	} else {
226 		buffer_lines = DROWS;
227 	}
228 
229 	// Ensure the window is not bigger than the buffer
230 	CLAMP(DROWS, DROWS_MIN, DBROWS);
231 	CLAMP(DCOLS, DCOLS_MIN, DBCOLS);
232 
233 	// Ensure we don't scroll too far
234 	dc_scroll_x = MIN(dc_scroll_x, (DBCOLS - DCOLS));
235 	if (dc_buffer.size() >= buffer_lines) {
236 		dc_scroll_y = MIN(dc_scroll_y, (uint32_t)(dc_buffer.size() - buffer_lines));
237 	} else {
238 		dc_scroll_y = 0;	// Disallow vscroll until the buffer is larger than the window
239 	}
240 
241 	// Draw the buffer strings
242 	for (i = 0; i < buffer_lines; ++i) {
243 		if ((i + dc_scroll_y) < dc_buffer.size()) {
244 			gr_string(gr_screen.center_offset_x, gr_screen.center_offset_y + (((int)i * row_height) + row_height), dc_buffer[i + dc_scroll_y].substr(dc_scroll_x).c_str(), GR_RESIZE_NONE);
245 		}
246 	}
247 
248 	// Draw the command string w/ padding only if the prompt is active.
249 	if (show_prompt) {
250 		i += 1;		// 1 line between the output and the input text
251 		j = 0;
252 		gr_set_color_fast(&Color_bright);
253 		for (str_it = out_str.begin(); str_it < out_str.end(); ++str_it) {
254 			if (j == (DCOLS - 1)) {
255 				// Insert a newline char at every place the string needs to return the 'carriage'
256 				str_it = out_str.insert(str_it, '\n');
257 				j = 0;
258 			} else {
259 				++j;
260 			}
261 		}
262 		gr_string(gr_screen.center_offset_x, gr_screen.center_offset_y + (((int)i * row_height) + row_height), out_str.c_str(), GR_RESIZE_NONE);
263 
264 		dc_draw_cursor(out_str, 0, (((int)i * row_height)));
265 		gr_set_color_fast(&Color_normal);
266 	}
267 }
268 
dc_init(void)269 void dc_init(void)
270 {
271 	if (debug_inited) {
272 		return;
273 	}
274 
275 	debug_inited = TRUE;
276 
277 	// Init window settings
278 	dc_font = font::FONT1;
279 	row_height = ((fl2i(font::get_current_font()->getHeight())) * 3) / 2;	// Row/Line height, in pixels
280 
281 	// This assumes that FONT1 is monospaced!
282 	gr_get_string_size(&col_width, nullptr, " "); // Col/Character width, in pixels
283 
284 	dc_scroll_x = 0;
285 	dc_scroll_y = 0;
286 	DCOLS = (gr_screen.center_w / col_width) - 1;	// Subtract as needed. Windowed mode has some quirks with the resolution
287 	DROWS = (gr_screen.center_h / row_height) - 2;
288 	DBCOLS = DCOLS;
289 	DBROWS = 2 * DROWS;
290 
291 	// Init History
292 	dc_history.clear();
293 	dc_history.push_back("");
294 	last_oldcommand = dc_history.begin();
295 
296 	// Init buffers
297 	dc_buffer.clear();
298 	dc_buffer.push_back("");
299 
300 	dc_command_buf.reserve(MAX_CLI_LEN);
301 	dc_command_buf.clear();
302 
303 	sprintf(dc_title, "FreeSpace Open v%s", FS_VERSION_FULL);
304 	dc_printf("Debug console started.\n" );
305 }
306 
dc_pause_output(void)307 bool dc_pause_output(void)
308 {
309 	dc_printf("More to follow. Press any key to continue. ESC halts output...");
310 
311 	int key;
312 	bool loop;
313 	do {
314 		loop = true;
315 
316 		os_poll();
317 
318 		dc_draw(FALSE);
319 
320 		key = key_inkey();
321 		switch (key) {
322 		case KEY_ESC:
323 			return true;
324 			break;
325 
326 		case KEY_PAGEUP:
327 			if (dc_scroll_y > 1) {
328 				dc_scroll_y--;
329 			}
330 			break;
331 
332 		case KEY_PAGEDOWN:
333 			if (dc_scroll_y < DBROWS) {
334 				dc_scroll_y++;
335 			} else {
336 				dc_scroll_y = DBROWS;
337 			}
338 			break;
339 
340 		case KEY_LEFT:
341 			// TODO: Scroll Left
342 			break;
343 		case KEY_RIGHT:
344 			// TODO: Scroll Right
345 			break;
346 		case 0:
347 			// No key pressed
348 			break;
349 		default:
350 			// Non-control key pressed, break.
351 			loop = false;
352 		}
353 	} while (loop);
354 
355 	dc_printf("\n");
356 	return false;
357 };
358 
dc_printf(const char * format,...)359 void dc_printf(const char *format, ...)
360 {
361 	SCP_string tmp;
362 	va_list args;
363 	SCP_string::iterator tmp_it;
364 
365 	va_start(args, format);
366 	vsprintf(tmp, format, args);
367 	va_end(args);
368 
369 	for (tmp_it = tmp.begin(); tmp_it != tmp.end(); ++tmp_it) {
370 		dc_putc(*tmp_it);
371 	}
372 }
373 
dc_putc(char c)374 void dc_putc(char c)
375 {
376 	SCP_string* line_str = &(dc_buffer.back());
377 	SCP_string temp_str;
378 	int i;
379 	int w;
380 
381 	if (c == ' ') {
382 		/**
383 		 * Push c onto the temp_str and get its gr_string width
384 		 *
385 		 * If we run out of room on the line, or
386 		 * If we run out of room on the screen, change c to a '\n' and let subsequent block handle it,
387 		 * Else, push the space onto the line and bail
388 		 */
389 		temp_str = *line_str;
390 		temp_str.push_back(c);
391 		gr_get_string_size(&w, NULL, temp_str.c_str());
392 
393 		if ((temp_str.size() >= DBCOLS) || (w > gr_screen.center_w)) {
394 			c = '\n';
395 
396 		} else {
397 			lastwhite = temp_str.size();
398 			*line_str = temp_str;
399 			return;
400 		}
401 	}
402 
403 	if (c == '\t') {
404 		/**
405 		 * Calculate how many spaces to put in to align tabs,
406 		 * Append temp_str with the spaces and get its gr_string width
407 		 *
408 		 * If we run out of room on the line, or
409 		 * If we run out of room on the screen, change c to a '\n' and let subsequent block handle it,
410 		 * Else, copy temp_str onto the line, update the lastwhite index, and bail
411 		 */
412 		i = DTABS - (line_str->size() % DTABS);
413 		temp_str = *line_str;
414 		temp_str.append(i, ' ');
415 		gr_get_string_size(&w, NULL, temp_str.c_str());
416 
417 		if ((temp_str.size() >= DBCOLS) || (w > gr_screen.center_w)) {
418 			c = '\n';
419 
420 		} else {
421 			lastwhite = temp_str.size();
422 			*line_str = temp_str;
423 			return;
424 		}
425 	}
426 
427 	if (c == '\n') {
428 		/**
429 		 * Trash whatever char happens to be past (DBCOLS - 1),
430 		 * Push a blank line onto the dc_buffer from the bottom,
431 		 * Increment the scroller, if needed,
432 		 * Trash the topmost line(s) in the buffer,
433 		 * Reset the lastwhite index,
434 		 * Increment the lastline counter, and finally
435 		 * bail
436 		 */
437 		if (line_str->size() > DBCOLS) {
438 			line_str->resize(DBCOLS);
439 		}
440 		dc_buffer.push_back("");
441 
442 		if ((dc_buffer.size() > DROWS) && (dc_scroll_y < SCROLL_Y_MAX)) {
443 			dc_scroll_y++;
444 		}
445 
446 		while (dc_buffer.size() > DBROWS) {
447 			dc_buffer.pop_front();
448 		}
449 
450 		lastwhite = 0;
451 		lastline++;
452 		return;
453 	}
454 
455 	// By this point, c is probably a writable character
456 	temp_str = *line_str;
457 	temp_str.push_back(c);
458 	gr_get_string_size(&w, NULL, temp_str.c_str());
459 
460 	if ((temp_str.size() >= DBCOLS) || (w > gr_screen.center_w)) {
461 		/**
462 		 * Word wrapping
463 		 * Save the word, clear the line of the word, push new line with the word on it
464 		 * Update scroll_y, if needed,
465 		 * Pop off old lines, and finally
466 		 * Push new character onto the new line
467 		 */
468 		temp_str = line_str->substr(lastwhite);
469 		line_str->resize(lastwhite);
470 		dc_buffer.push_back(temp_str);
471 		line_str = &dc_buffer.back();
472 
473 		if ((dc_buffer.size() > DROWS) && (dc_scroll_y < SCROLL_Y_MAX)) {
474 			dc_scroll_y++;
475 		}
476 
477 		while (dc_buffer.size() > DBROWS) {
478 			dc_buffer.pop_front();
479 		}
480 
481 		lastwhite = 0;
482 		lastline++;
483 		line_str->push_back(c);
484 		return;
485 	}
486 
487 	// Else, just push the char onto the line
488 	line_str->push_back(c);
489 }
490 
debug_console(void (* _func)(void))491 void debug_console(void (*_func)(void))
492 {
493 	int done = 0;
494 
495 	while( key_inkey() ) {
496 		os_poll();
497 	}
498 
499 	if ( !debug_inited ) {
500 		dc_init();
501 	}
502 
503 	dc_draw(TRUE);
504 
505 	while (!done) {
506 		// poll the os
507 		os_poll();
508 
509 		int k = key_inkey();
510 		switch( k ) {
511 
512 		case KEY_SHIFTED+KEY_ENTER:
513 		case KEY_ESC:
514 			done = TRUE;
515 			break;
516 
517 		case KEY_BACKSP:
518 			if (!dc_command_buf.empty()) {
519 				dc_command_buf.erase(dc_command_buf.size() - 1);
520 			}
521 			break;
522 
523 		case KEY_F3:
524 		case KEY_UP:
525 			if (last_oldcommand < (dc_history.end() - 1)) {
526 				++last_oldcommand;
527 			}
528 
529 			dc_command_buf = *last_oldcommand;
530 			break;
531 
532 		case KEY_DOWN:
533 			if (last_oldcommand > dc_history.begin()) {
534 				--last_oldcommand;
535 			}
536 
537 			dc_command_buf = *last_oldcommand;
538 			break;
539 
540 		case KEY_PAGEUP:
541 			if (dc_scroll_y > 1) {
542 				dc_scroll_y--;
543 			}
544 			break;
545 
546 		case KEY_PAGEDOWN:
547 			if (dc_scroll_y < (DBROWS - DROWS)) {
548 				dc_scroll_y++;
549 			} else {
550 				dc_scroll_y = (DBROWS - DROWS);
551 			}
552 			break;
553 
554 		case KEY_ENTER:
555 			dc_scroll_y = (DBROWS - DROWS);			// Set the scroll to look at the bottom
556 			last_oldcommand = dc_history.begin();	// Reset the last oldcommand
557 			lastline = 0;	// Reset the line counter
558 
559 			// Clear the command line on the window, but don't print the prompt until the command has processed
560 			// Stuff a copy of the command line onto the history
561 			// Search for the command
562 				// If not found:
563 				//   abort,
564 				//   dc_printf("Error: Invalid or Missing command %s", cmd.c_str()), and
565 				//   dc_printf(dc_prompt) when ready for input
566 			// Call the function for that command, and strip the cmd token from the command line string
567 			if (dc_command_buf.empty()) {
568 				dc_printf("No command given.\n");
569 				break;
570 			} // Else, continue to process the cmd_line
571 
572 			// z64: Thread Note: Maybe lock a mutex here to allow a previous DCF to finish/abort before starting a new one
573 			// z64: We'll just assume we won't be here unless a command has finished...
574 			dc_history.push_front(dc_command_buf);	// Push the command onto the history queue
575 			last_oldcommand = dc_history.begin();	// Reset oldcommand
576 
577 			while (dc_history.size() > DCMDS) {
578 				dc_history.pop_back();			// Keep the commands less than or equal to DCMDS
579 			}
580 
581 			dc_command_str = dc_command_buf;	// Xfer to the command string for processing
582 			dc_command_buf.resize(0);			// Nullify the buffer
583 			dc_printf("%s%s\n", dc_prompt, dc_command_str.c_str());	// Print the command w/ prompt.
584 			dc_draw(FALSE);					// Redraw the console without the command line.
585 			dc_do_command(&dc_command_str);	// Try to do the command
586 			break;
587 
588 		default:
589 			// Not any of the control key codes, so it's probably a letter or number.
590 			ubyte c = (ubyte)key_to_ascii(k);
591 			if ((c != 255) && (dc_command_buf.size() < MAX_CLI_LEN)) {
592 				dc_command_buf.push_back(c);
593 			}
594 		}
595 
596 		// Do the passed function
597 		if ( _func ) {
598 			_func();
599 		}
600 
601 		// All done, and ready for new entry
602 		dc_draw(TRUE);
603 	}
604 
605 	while( key_inkey() ) {
606 		os_poll();
607 	}
608 }
609