1 /*
2  * This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>.
3  * It is copyright by its individual contributors, as recorded in the
4  * project's Git history.  See COPYING.txt at the top level for license
5  * terms and a link to the Git history.
6  */
7 /*
8  *
9  * Game console
10  *
11  */
12 
13 #include <algorithm>
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <stdarg.h>
17 #include <string.h>
18 #include <sys/time.h>
19 #include <SDL.h>
20 #include "window.h"
21 #include "event.h"
22 #include "console.h"
23 #include "args.h"
24 #include "gr.h"
25 #include "physfsx.h"
26 #include "gamefont.h"
27 #include "game.h"
28 #include "key.h"
29 #include "vers_id.h"
30 #include "timer.h"
31 #include "cli.h"
32 #include "cmd.h"
33 #include "cvar.h"
34 
35 #include "dxxsconf.h"
36 #include <array>
37 
38 #ifdef _WIN32
39 #include <windows.h>
40 #endif
41 
42 namespace dcx {
43 
44 namespace {
45 
46 #ifndef DXX_CONSOLE_TIME_SHOW_YMD
47 #define DXX_CONSOLE_TIME_SHOW_YMD	0
48 #endif
49 
50 #ifndef DXX_CONSOLE_TIME_SHOW_MSEC
51 #define DXX_CONSOLE_TIME_SHOW_MSEC	1
52 #endif
53 
54 #ifndef DXX_CONSOLE_SHOW_TIME_STDOUT
55 #define DXX_CONSOLE_SHOW_TIME_STDOUT	0
56 #endif
57 
58 constexpr unsigned CON_LINES_ONSCREEN = 18;
59 constexpr auto CON_SCROLL_OFFSET = CON_LINES_ONSCREEN - 3;
60 constexpr unsigned CON_LINES_MAX = 128;
61 
62 enum con_state {
63 	CON_STATE_CLOSING = -1,
64 	CON_STATE_CLOSED = 0,
65 	CON_STATE_OPENING = 1,
66 	CON_STATE_OPEN = 2
67 };
68 
69 struct console_window : window
70 {
71 	using window::window;
72 	virtual window_event_result event_handler(const d_event &) override;
73 };
74 
75 static RAIIPHYSFS_File gamelog_fp;
76 static std::array<console_buffer, CON_LINES_MAX> con_buffer;
77 static con_state con_state;
78 static int con_scroll_offset, con_size;
79 static void con_force_puts(con_priority priority, char *buffer, size_t len);
80 
con_add_buffer_line(const con_priority priority,const char * const buffer,const size_t len)81 static void con_add_buffer_line(const con_priority priority, const char *const buffer, const size_t len)
82 {
83 	/* shift con_buffer for one line */
84 	std::move(std::next(con_buffer.begin()), con_buffer.end(), con_buffer.begin());
85 	console_buffer &c = con_buffer.back();
86 	c.priority=priority;
87 
88 	size_t copy = std::min(len, CON_LINE_LENGTH - 1);
89 	c.line[copy] = 0;
90 	memcpy(&c.line,buffer, copy);
91 }
92 
93 }
94 
95 void (con_printf)(const con_priority_wrapper priority, const char *const fmt, ...)
96 {
97 	va_list arglist;
98 	char buffer[CON_LINE_LENGTH];
99 
100 	if (priority <= CGameArg.DbgVerbose)
101 	{
102 		va_start (arglist, fmt);
103 		auto &&leader = priority.insert_location_leader(buffer);
104 		size_t len = vsnprintf (leader.first, leader.second, fmt, arglist);
105 		va_end (arglist);
106 		con_force_puts(priority, buffer, len);
107 	}
108 }
109 
110 namespace {
111 
con_scrub_markup(char * buffer)112 static void con_scrub_markup(char *buffer)
113 {
114 	char *p1 = buffer, *p2 = p1;
115 	do
116 		switch (*p1)
117 		{
118 			case CC_COLOR:
119 			case CC_LSPACING:
120 				if (!*++p1)
121 					break;
122 				DXX_BOOST_FALLTHROUGH;
123 			case CC_UNDERLINE:
124 				p1++;
125 				break;
126 			default:
127 				*p2++ = *p1++;
128 		}
129 	while (*p1);
130 	*p2 = 0;
131 }
132 
con_print_file(const char * const buffer)133 static void con_print_file(const char *const buffer)
134 {
135 	char buf[1024];
136 #if !DXX_CONSOLE_SHOW_TIME_STDOUT
137 #ifndef _WIN32
138 	/* Print output to stdout */
139 	puts(buffer);
140 #endif
141 
142 	/* Print output to gamelog.txt */
143 	if (gamelog_fp)
144 #endif
145 	{
146 #if DXX_CONSOLE_TIME_SHOW_YMD
147 #define DXX_CONSOLE_TIME_FORMAT_YMD	"%04i-%02i-%02i "
148 #define DXX_CONSOLE_TIME_ARG_YMD	tm_year, tm_month, tm_day,
149 #else
150 #define DXX_CONSOLE_TIME_FORMAT_YMD	""
151 #define DXX_CONSOLE_TIME_ARG_YMD
152 #endif
153 #if DXX_CONSOLE_TIME_SHOW_MSEC
154 #ifdef _WIN32
155 #define DXX_CONSOLE_TIME_FORMAT_MSEC	".%03i"
156 #else
157 #define DXX_CONSOLE_TIME_FORMAT_MSEC	".%06i"
158 #endif
159 #define DXX_CONSOLE_TIME_ARG_MSEC	tm_msec,
160 #else
161 #define DXX_CONSOLE_TIME_FORMAT_MSEC	""
162 #define DXX_CONSOLE_TIME_ARG_MSEC
163 #endif
164 		int
165 			DXX_CONSOLE_TIME_ARG_YMD
166 			DXX_CONSOLE_TIME_ARG_MSEC
167 			tm_hour, tm_min, tm_sec;
168 #ifdef _WIN32
169 #define DXX_LF	"\r\n"
170 		SYSTEMTIME st = {};
171 		GetLocalTime(&st);
172 #if DXX_CONSOLE_TIME_SHOW_YMD
173 		tm_year = st.wYear;
174 		tm_month = st.wMonth;
175 		tm_day = st.wDay;
176 #endif
177 		tm_hour = st.wHour;
178 		tm_min = st.wMinute;
179 		tm_sec = st.wSecond;
180 #if DXX_CONSOLE_TIME_SHOW_MSEC
181 		tm_msec = st.wMilliseconds;
182 #endif
183 #else
184 #define DXX_LF	"\n"
185 		struct timeval tv;
186 		if (gettimeofday(&tv, nullptr))
187 			tv = {};
188 		if (const auto lt = localtime(&tv.tv_sec))
189 		{
190 #if DXX_CONSOLE_TIME_SHOW_YMD
191 			tm_year = lt->tm_year;
192 			tm_month = lt->tm_mon;
193 			tm_day = lt->tm_mday;
194 #endif
195 			tm_hour = lt->tm_hour;
196 			tm_min = lt->tm_min;
197 			tm_sec = lt->tm_sec;
198 #if DXX_CONSOLE_TIME_SHOW_MSEC
199 			tm_msec = tv.tv_usec;
200 #endif
201 		}
202 		else
203 		{
204 #if DXX_CONSOLE_TIME_SHOW_YMD
205 			tm_year = tm_month = tm_day =
206 #endif
207 #if DXX_CONSOLE_TIME_SHOW_MSEC
208 			tm_msec =
209 #endif
210 			tm_hour = tm_min = tm_sec = -1;
211 		}
212 #endif
213 		const size_t len = snprintf(buf, sizeof(buf), DXX_CONSOLE_TIME_FORMAT_YMD "%02i:%02i:%02i" DXX_CONSOLE_TIME_FORMAT_MSEC " %s" DXX_LF, DXX_CONSOLE_TIME_ARG_YMD tm_hour, tm_min, tm_sec, DXX_CONSOLE_TIME_ARG_MSEC buffer);
214 #if DXX_CONSOLE_SHOW_TIME_STDOUT
215 #ifndef _WIN32
216 		fputs(buf, stdout);
217 #endif
218 		if (gamelog_fp)
219 #endif
220 		{
221 			PHYSFS_write(gamelog_fp, buf, 1, len);
222 		}
223 #undef DXX_LF
224 #undef DXX_CONSOLE_TIME_ARG_MSEC
225 #undef DXX_CONSOLE_TIME_FORMAT_MSEC
226 #undef DXX_CONSOLE_TIME_ARG_YMD
227 #undef DXX_CONSOLE_TIME_FORMAT_YMD
228 	}
229 }
230 
231 /*
232  * The caller is assumed to have checked that the priority allows this
233  * entry to be logged.
234  */
con_force_puts(const con_priority priority,char * const buffer,const size_t len)235 static void con_force_puts(const con_priority priority, char *const buffer, const size_t len)
236 {
237 	con_add_buffer_line(priority, buffer, len);
238 	con_scrub_markup(buffer);
239 	/* Produce a sanitised version and send it to the console */
240 	con_print_file(buffer);
241 }
242 
243 }
244 
con_puts(const con_priority_wrapper priority,char * const buffer,const size_t len)245 void con_puts(const con_priority_wrapper priority, char *const buffer, const size_t len)
246 {
247 	if (priority <= CGameArg.DbgVerbose)
248 	{
249 		typename con_priority_wrapper::scratch_buffer<CON_LINE_LENGTH> scratch_buffer;
250 		auto &&b = priority.prepare_buffer(scratch_buffer, buffer, len);
251 		con_force_puts(priority, b.first, b.second);
252 	}
253 }
254 
con_puts(const con_priority_wrapper priority,const char * const buffer,const size_t len)255 void con_puts(const con_priority_wrapper priority, const char *const buffer, const size_t len)
256 {
257 	if (priority <= CGameArg.DbgVerbose)
258 	{
259 		typename con_priority_wrapper::scratch_buffer<CON_LINE_LENGTH> scratch_buffer;
260 		auto &&b = priority.prepare_buffer(scratch_buffer, buffer, len);
261 		/* add given string to con_buffer */
262 		con_add_buffer_line(priority, b.first, b.second);
263 		con_print_file(b.first);
264 	}
265 }
266 
267 namespace {
268 
get_console_color_by_priority(const int priority)269 static color_palette_index get_console_color_by_priority(const int priority)
270 {
271 	int r, g, b;
272 	switch (priority)
273 	{
274 		case CON_CRITICAL:
275 			r = 28 * 2, g = 0 * 2, b = 0 * 2;
276 			break;
277 		case CON_URGENT:
278 			r = 54 * 2, g = 54 * 2, b = 0 * 2;
279 			break;
280 		case CON_DEBUG:
281 		case CON_VERBOSE:
282 			r = 14 * 2, g = 14 * 2, b = 14 * 2;
283 			break;
284 		case CON_HUD:
285 			r = 0 * 2, g = 28 * 2, b = 0 * 2;
286 			break;
287 		default:
288 			r = 255 * 2, g = 255 * 2, b = 255 * 2;
289 			break;
290 	}
291 	return gr_find_closest_color(r, g, b);
292 }
293 
con_draw(void)294 static void con_draw(void)
295 {
296 	int i = 0, y = 0;
297 
298 	if (con_size <= 0)
299 		return;
300 
301 	gr_set_default_canvas();
302 	auto &canvas = *grd_curcanv;
303 	auto &game_font = *GAME_FONT;
304 	gr_set_curfont(canvas, game_font);
305 	const uint8_t color = BM_XRGB(0, 0, 0);
306 	gr_settransblend(canvas, gr_fade_level{7}, gr_blend::normal);
307 	const auto &&fspacy1 = FSPACY(1);
308 	const auto &&line_spacing = LINE_SPACING(*canvas.cv_font, *GAME_FONT);
309 	y = fspacy1 + (line_spacing * con_size);
310 	gr_rect(canvas, 0, 0, SWIDTH, y, color);
311 	gr_settransblend(canvas, GR_FADE_OFF, gr_blend::normal);
312 	i+=con_scroll_offset;
313 
314 	gr_set_fontcolor(canvas, BM_XRGB(255, 255, 255), -1);
315 	y = cli_draw(y, line_spacing);
316 
317 	const auto &&fspacx = FSPACX();
318 	const auto &&fspacx1 = fspacx(1);
319 	for (;;)
320 	{
321 		auto &b = con_buffer[CON_LINES_MAX - 1 - i];
322 		gr_set_fontcolor(canvas, get_console_color_by_priority(b.priority), -1);
323 		const auto &&[w, h] = gr_get_string_size(game_font, b.line);
324 		y -= h + fspacy1;
325 		gr_string(canvas, game_font, fspacx1, y, b.line, w, h);
326 		i++;
327 
328 		if (y<=0 || CON_LINES_MAX-1-i <= 0 || i < 0)
329 			break;
330 	}
331 	gr_rect(canvas, 0, 0, SWIDTH, line_spacing, color);
332 	gr_set_fontcolor(canvas, BM_XRGB(255, 255, 255),-1);
333 	gr_printf(canvas, game_font, fspacx1, fspacy1, "%s LOG", DESCENT_VERSION);
334 	gr_string(canvas, game_font, SWIDTH - fspacx(110), fspacy1, "PAGE-UP/DOWN TO SCROLL");
335 }
336 
event_handler(const d_event & event)337 window_event_result console_window::event_handler(const d_event &event)
338 {
339 	int key;
340 	static fix64 last_scroll_time = 0;
341 
342 	switch (event.type)
343 	{
344 		case EVENT_WINDOW_ACTIVATED:
345 			key_toggle_repeat(1);
346 			break;
347 
348 		case EVENT_WINDOW_DEACTIVATED:
349 			key_toggle_repeat(0);
350 			con_size = 0;
351 			con_state = CON_STATE_CLOSED;
352 			break;
353 
354 		case EVENT_KEY_COMMAND:
355 			key = event_key_get(event);
356 			switch (key)
357 			{
358 				case KEY_SHIFTED + KEY_ESC:
359 					switch (con_state)
360 					{
361 						case CON_STATE_OPEN:
362 						case CON_STATE_OPENING:
363 							con_state = CON_STATE_CLOSING;
364 							break;
365 						case CON_STATE_CLOSED:
366 						case CON_STATE_CLOSING:
367 							con_state = CON_STATE_OPENING;
368 						default:
369 							break;
370 					}
371 					break;
372 				case KEY_PAGEUP:
373 					con_scroll_offset+=CON_SCROLL_OFFSET;
374 					if (con_scroll_offset >= CON_LINES_MAX-1)
375 						con_scroll_offset = CON_LINES_MAX-1;
376 					while (con_buffer[CON_LINES_MAX-1-con_scroll_offset].line[0]=='\0')
377 						con_scroll_offset--;
378 					break;
379 				case KEY_PAGEDOWN:
380 					con_scroll_offset-=CON_SCROLL_OFFSET;
381 					if (con_scroll_offset<0)
382 						con_scroll_offset=0;
383 					break;
384 				case KEY_CTRLED + KEY_A:
385 				case KEY_HOME:              cli_cursor_home();      break;
386 				case KEY_END:
387 				case KEY_CTRLED + KEY_E:    cli_cursor_end();       break;
388 				case KEY_CTRLED + KEY_C:    cli_clear();            break;
389 				case KEY_LEFT:              cli_cursor_left();      break;
390 				case KEY_RIGHT:             cli_cursor_right();     break;
391 				case KEY_BACKSP:            cli_cursor_backspace(); break;
392 				case KEY_CTRLED + KEY_D:
393 				case KEY_DELETE:            cli_cursor_del();       break;
394 				case KEY_UP:                cli_history_prev();     break;
395 				case KEY_DOWN:              cli_history_next();     break;
396 				case KEY_TAB:               cli_autocomplete();     break;
397 				case KEY_ENTER:             cli_execute();          break;
398 				case KEY_INSERT:
399 					cli_toggle_overwrite_mode();
400 					break;
401 				default:
402 					int character = key_ascii();
403 					if (character == 255)
404 						break;
405 					cli_add_character(character);
406 					break;
407 			}
408 			return window_event_result::handled;
409 
410 		case EVENT_WINDOW_DRAW:
411 			timer_delay2(50);
412 			if (con_state == CON_STATE_OPENING)
413 			{
414 				if (con_size < CON_LINES_ONSCREEN && timer_query() >= last_scroll_time+(F1_0/30))
415 				{
416 					last_scroll_time = timer_query();
417 					if (++ con_size >= CON_LINES_ONSCREEN)
418 						con_state = CON_STATE_OPEN;
419 				}
420 			}
421 			else if (con_state == CON_STATE_CLOSING)
422 			{
423 				if (con_size > 0 && timer_query() >= last_scroll_time+(F1_0/30))
424 				{
425 					last_scroll_time = timer_query();
426 					if (! -- con_size)
427 						con_state = CON_STATE_CLOSED;
428 				}
429 			}
430 			con_draw();
431 			if (con_state == CON_STATE_CLOSED)
432 			{
433 				return window_event_result::close;
434 			}
435 			break;
436 		case EVENT_WINDOW_CLOSE:
437 			break;
438 		default:
439 			break;
440 	}
441 
442 	return window_event_result::ignored;
443 }
444 
445 }
446 
con_init(void)447 void con_init(void)
448 {
449 	con_buffer = {};
450 	if (CGameArg.DbgSafelog)
451 		gamelog_fp.reset(PHYSFS_openWrite("gamelog.txt"));
452 	else
453 		gamelog_fp = PHYSFSX_openWriteBuffered("gamelog.txt").first;
454 
455 	cli_init();
456 	cmd_init();
457 	cvar_init();
458 }
459 
460 }
461 
462 namespace dsx {
463 
con_showup(control_info & Controls)464 void con_showup(control_info &Controls)
465 {
466 	game_flush_inputs(Controls);
467 	con_state = CON_STATE_OPENING;
468 	auto wind = window_create<console_window>(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT);
469 	(void)wind;
470 }
471 
472 }
473