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