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 #include <cstdio>
11 #include <cstdarg>
12 #include <string>
13 #include <sstream>
14 #include <climits>
15 
16 #include "graphics/software/font_internal.h"
17 #include "graphics/software/FontManager.h"
18 #include "graphics/software/FSFont.h"
19 #include "graphics/software/VFNTFont.h"
20 #include "graphics/software/NVGFont.h"
21 
22 #include "graphics/2d.h"
23 
24 #include "mod_table/mod_table.h"
25 
26 #include "def_files/def_files.h"
27 #include "bmpman/bmpman.h"
28 #include "cfile/cfile.h"
29 #include "localization/localize.h"
30 #include "parse/parselo.h"
31 #include "tracing/Monitor.h"
32 
33 namespace
34 {
35 	namespace fo = font;
36 	using namespace font;
37 
38 	bool font_initialized = false;
39 
40 	constexpr ubyte DEFAULT_SPECIAL_CHAR_INDEX = 0;
41 
42 	// max allowed special char index; i.e. 7 special chars in retail fonts 1 & 3
43 	const int MAX_SPECIAL_CHAR_IDX = UCHAR_MAX - 6;
44 
45 
parse_type(FontType & type,SCP_string & fileName)46 	bool parse_type(FontType &type, SCP_string &fileName)
47 	{
48 		int num = optional_string_either("$TrueType:", "$Font:");
49 		if (num == 0)
50 		{
51 			type = NVG_FONT;
52 		}
53 		else if (num == 1)
54 		{
55 			type = VFNT_FONT;
56 		}
57 		else
58 		{
59 			type = UNKNOWN_FONT;
60 		}
61 
62 
63 		if (type != UNKNOWN_FONT)
64 		{
65 			stuff_string(fileName, F_NAME);
66 			return true;
67 		}
68 		else
69 		{
70 			return false;
71 		}
72 	}
73 
parse_nvg_font(const SCP_string & fontFilename)74 	void parse_nvg_font(const SCP_string& fontFilename)
75 	{
76 		float size = 8.0f;
77 		SCP_string fontStr;
78 		bool hasName = false;
79 
80 		if (optional_string("+Name:"))
81 		{
82 			stuff_string(fontStr, F_NAME);
83 
84 			hasName = true;
85 		}
86 
87 		if (optional_string("+Size:"))
88 		{
89 			stuff_float(&size);
90 
91 			if (size <= 0.0f)
92 			{
93 				error_display(0, "+Size has to be bigger than 0 for font \"%s\".", fontFilename.c_str());
94 				size = 8;
95 			}
96 		}
97 
98 		// Build name from existing values if no name is specified
99 		if (!hasName)
100 		{
101 			SCP_stringstream ss;
102 
103 			ss << fontFilename << "-";
104 
105 			ss << size;
106 
107 			fontStr = ss.str();
108 		}
109 
110 		if (FontManager::getFont(fontStr) != NULL)
111 		{
112 			if (hasName)
113 			{
114 				error_display(0, "Font with name \"%s\" is already present! Font names have to be unique!", fontStr.c_str());
115 				return;
116 			}
117 			else
118 			{
119 				error_display(0, "Found font with same default name (\"%s\"). This is most likely a duplicate.", fontStr.c_str());
120 				return;
121 			}
122 		}
123 
124 		NVGFont *nvgFont = FontManager::loadNVGFont(fontFilename, size);
125 
126 		if (nvgFont == NULL)
127 		{
128 			error_display(0, "Couldn't load font \"%s\".", fontFilename.c_str());
129 			return;
130 		}
131 
132 		if (optional_string("+Top offset:"))
133 		{
134 			float temp;
135 
136 			stuff_float(&temp);
137 
138 			nvgFont->setTopOffset(temp);
139 		}
140 
141 		if (optional_string("+Bottom offset:"))
142 		{
143 			float temp;
144 
145 			stuff_float(&temp);
146 
147 			nvgFont->setBottomOffset(temp);
148 		}
149 
150 		if (optional_string("+Tab width:"))
151 		{
152 			float temp;
153 			stuff_float(&temp);
154 
155 			if (temp < 0.0f)
156 			{
157 				error_display(0, "Invalid tab spacing %f. Has to be greater or equal to zero.", temp);
158 			}
159 			else
160 			{
161 				nvgFont->setTabWidth(temp);
162 			}
163 		}
164 
165 		if (optional_string("+Letter spacing:"))
166 		{
167 			float temp;
168 			stuff_float(&temp);
169 
170 			if (temp < 0.0f)
171 			{
172 				error_display(0, "Invalid letter spacing %f! Has to be greater or equal to zero.", temp);
173 			}
174 			else
175 			{
176 				nvgFont->setLetterSpacing(temp);
177 			}
178 		}
179 
180 		if (!Unicode_text_mode) {
181 			int special_char_index = DEFAULT_SPECIAL_CHAR_INDEX;
182 			// Special characters only exist in non-Unicode mode
183 			if (optional_string("+Special Character Font:"))
184 			{
185 				SCP_string fontName;
186 				stuff_string(fontName, F_NAME);
187 
188 				fo::font* fontData = FontManager::loadFontOld(fontName.c_str());
189 
190 				if (fontData == NULL)
191 				{
192 					error_display(0, "Failed to load font \"%s\" for special characters of font \"%s\"!", fontName.c_str(), fontFilename.c_str());
193 				}
194 				else
195 				{
196 					nvgFont->setSpecialCharacterFont(fontData);
197 				}
198 
199 				if (optional_string("+Special Character Index:")) {
200 					stuff_int(&special_char_index);
201 					if (special_char_index < 0 || special_char_index >= MAX_SPECIAL_CHAR_IDX) {
202 						Warning(LOCATION, "Special character index (%d) for font (%s) is invalid, must be 0 - %u. Defaulting to 0.",
203 							special_char_index, fontName.c_str(), MAX_SPECIAL_CHAR_IDX);
204 						special_char_index = DEFAULT_SPECIAL_CHAR_INDEX;
205 					}
206 				}
207 				else {
208 					auto old_entry = FontManager::getFontByFilename(fontName);
209 
210 					if (old_entry != nullptr) {
211 						int old_index = FontManager::getFontIndex(old_entry);
212 						special_char_index = Lcl_languages[0].special_char_indexes[old_index];
213 					} else {
214 
215 						Warning(LOCATION,
216 							"A \"+Special Character Index:\" was not specified after the \"+Special Character Font:\" entry for the %s font. And a previous entry with a \"+Special Character Index:\" was not found."
217 							"\n\n This entry must be specified directly or indirectly to avoid this warning.  Defaulting to 0.",
218 							fontStr.c_str());
219 					}
220 				}
221 
222 			}
223 			else
224 			{
225 				fo::font* fontData = FontManager::loadFontOld("font01.vf");
226 
227 				if (fontData == nullptr) {
228 					error_display(0,
229 								  "Failed to load default font \"%s\" for special characters of font \"%s\"! "
230 									  "This font is required for rendering special characters, if another font is not defined, and will cause an error later.",
231 								  "font01.vf",
232 								  fontFilename.c_str());
233 				} else {
234 					nvgFont->setSpecialCharacterFont(fontData);
235 				}
236 			}
237 
238 			auto font_id = FontManager::getFontIndex(nvgFont);
239 
240 			// add the index specified to all languages
241 			for (auto & Lcl_language : Lcl_languages) {
242 
243 				// if there wasn't already something specified for this font.
244 				if (font_id >= (int)Lcl_language.special_char_indexes.size()) {
245 
246 					// if there were absolutely no special characters defined, put the default value as the language default.
247 					if (Lcl_language.special_char_indexes.empty() == 0) {
248 						Lcl_language.special_char_indexes.push_back(DEFAULT_SPECIAL_CHAR_INDEX);
249 					}
250 
251 					// if somehow a lot of indexes were missing, add the language default to fill in the gaps until ...
252 					while (font_id > (int)Lcl_language.special_char_indexes.size()) {
253 							Lcl_language.special_char_indexes.push_back(Lcl_language.special_char_indexes[0]);
254 					}
255 
256 					// we're at just the right place in the vector to add the index we just read from the table.
257 					Lcl_language.special_char_indexes.push_back((ubyte)special_char_index);
258 
259 				} // replace if there was something already specified, except if nothing was specified in the table
260 				else if (special_char_index != DEFAULT_SPECIAL_CHAR_INDEX) {
261 					Lcl_language.special_char_indexes[font_id] = (ubyte)special_char_index;
262 				}
263 			}
264 		}
265 
266 		nvgFont->setName(fontStr);
267 		nvgFont->setFilename(fontFilename);
268 
269 		// Make sure that the height is not invalid
270 		nvgFont->computeFontMetrics();
271 	}
272 
parse_vfnt_font(const SCP_string & fontFilename)273 	void parse_vfnt_font(const SCP_string& fontFilename)
274 	{
275 		VFNTFont *font = FontManager::loadVFNTFont(fontFilename);
276 
277 		if (font == NULL)
278 		{
279 			error_display(0, "Couldn't load font\"%s\".", fontFilename.c_str());
280 			return;
281 		}
282 
283 		SCP_string fontName;
284 
285 		if (optional_string("+Name:"))
286 		{
287 			stuff_string(fontName, F_NAME);
288 		}
289 		else
290 		{
291 			fontName.assign(fontFilename);
292 		}
293 
294 		font->setName(fontName);
295 		font->setFilename(fontFilename);
296 
297 		auto font_id = FontManager::getFontIndex(font);
298 
299 		int user_defined_default_special_char_index = (int)DEFAULT_SPECIAL_CHAR_INDEX;
300 		// 'default' special char index for all languages using this font
301 		if (optional_string("+Default Special Character Index:")) {
302 			stuff_int(&user_defined_default_special_char_index);
303 
304 			if (user_defined_default_special_char_index < 0 || user_defined_default_special_char_index >= MAX_SPECIAL_CHAR_IDX) {
305 				Warning(LOCATION, "Default special character index (%d) for font (%s), must be 0 - %u.  Defaulting to 0.", user_defined_default_special_char_index,
306 					  fontName.c_str(), MAX_SPECIAL_CHAR_IDX);
307 			}
308 
309 		}
310 
311 		// add the index specified to all the languages
312 		for (auto & Lcl_language : Lcl_languages) {
313 
314 			// if there wasn't already something specified for this font.
315 			if (font_id >= (int)Lcl_language.special_char_indexes.size()) {
316 
317 				// if the default for the language was not already specified, set that default to the general default
318 				if (Lcl_language.special_char_indexes.empty()) {
319 					Lcl_language.special_char_indexes.push_back(DEFAULT_SPECIAL_CHAR_INDEX);
320 				}
321 
322 				// if a lot of indexes were missing, add some defaults to fill in the gaps until ...
323 				while (font_id > (int)Lcl_language.special_char_indexes.size()) {
324 					// take into account retail special character settings for font index 2 (size is 1 indexed)
325 					if (Lcl_language.special_char_indexes.size() == FONT3 - 1) {
326 						Lcl_language.special_char_indexes.push_back(176);
327 					}
328 					else {
329 						Lcl_language.special_char_indexes.push_back(Lcl_language.special_char_indexes[0]);
330 					}
331 				}
332 				// we're at just the right place in the vector to add the index we just read from the table.
333 				Lcl_language.special_char_indexes.push_back((ubyte)DEFAULT_SPECIAL_CHAR_INDEX);
334 			} // if there was something already specified that's being replaced.
335 			else if (user_defined_default_special_char_index != DEFAULT_SPECIAL_CHAR_INDEX) {
336 				Lcl_language.special_char_indexes[font_id] = (ubyte)user_defined_default_special_char_index;
337 			}
338 		}
339 
340 		while (optional_string("+Language:")) {
341 			char lang_name[LCL_LANG_NAME_LEN + 1];
342 			int special_char_index, lang_idx = -1;
343 
344 			stuff_string(lang_name, F_NAME, LCL_LANG_NAME_LEN + 1);
345 
346 			// find language and set the index, or if not found move to the next one
347 			for (auto i = 0; i < (int)Lcl_languages.size(); ++i) {
348 				if (!strcmp(Lcl_languages[i].lang_name, lang_name)) {
349 					lang_idx = i;
350 					break;
351 				}
352 			}
353 
354 			if (lang_idx == -1) {
355 				Warning(LOCATION, "Ignoring invalid language (%s) specified by font (%s); not built-in or in strings.tbl",
356 						lang_name, fontName.c_str());
357 				skip_to_start_of_string_either("+Language:", "$Font:", "#End");
358 				continue;
359 			}
360 
361 			if (optional_string("+Special Character Index:")) {
362 				stuff_int(&special_char_index);
363 
364 				if (special_char_index < 0 || special_char_index >= MAX_SPECIAL_CHAR_IDX) {
365 					Error(LOCATION, "Special character index (%d) for font (%s), language (%s) is invalid, must be 0 - %u",
366 						  special_char_index, fontName.c_str(), lang_name, MAX_SPECIAL_CHAR_IDX);
367 				}
368 
369 				Lcl_languages[lang_idx].special_char_indexes[font_id] = (ubyte)special_char_index;
370 			}
371 		}
372 
373 		if (optional_string("+Top offset:"))
374 		{
375 			float temp;
376 
377 			stuff_float(&temp);
378 
379 			font->setTopOffset(temp);
380 		}
381 
382 		if (optional_string("+Bottom offset:"))
383 		{
384 			float temp;
385 
386 			stuff_float(&temp);
387 
388 			font->setBottomOffset(temp);
389 		}
390 		// Make sure that the height is not invalid
391 		font->computeFontMetrics();
392 	}
393 
font_parse_setup(const char * fileName)394 	void font_parse_setup(const char *fileName)
395 	{
396 		bool noTable = false;
397 		if (!strcmp(fileName, "fonts.tbl"))
398 		{
399 			if (!cf_exists_full(fileName, CF_TYPE_TABLES))
400 			{
401 				noTable = true;
402 			}
403 		}
404 
405 		if (noTable)
406 		{
407 			read_file_text_from_default(defaults_get_file("fonts.tbl"));
408 		}
409 		else
410 		{
411 			read_file_text(fileName, CF_TYPE_TABLES);
412 		}
413 
414 		reset_parse();
415 
416 		// start parsing
417 		required_string("#Fonts");
418 	}
419 
parse_font_tbl(const char * fileName)420 	void parse_font_tbl(const char *fileName)
421 	{
422 		try
423 		{
424 			font_parse_setup(fileName);
425 
426 			FontType type;
427 			SCP_string fontName;
428 
429 			while (parse_type(type, fontName))
430 			{
431 				switch (type)
432 				{
433 				case VFNT_FONT:
434 					if (Unicode_text_mode) {
435 						error_display(1, "Bitmap fonts are not supported in Unicode text mode!");
436 					}
437 					parse_vfnt_font(fontName);
438 					break;
439 				case NVG_FONT:
440 					parse_nvg_font(fontName);
441 					break;
442 				default:
443 					error_display(0, "Unknown font type %d! Get a coder!", (int)type);
444 					break;
445 				}
446 			}
447 
448 			// done parsing
449 			required_string("#End");
450 		}
451 		catch (const parse::ParseException& e)
452 		{
453 			mprintf(("TABLES: Unable to parse '%s'!  Error message = %s.\n", fileName, e.what()));
454 		}
455 	}
456 
parse_fonts_tbl()457 	void parse_fonts_tbl()
458 	{
459 		//Parse main TBL first
460 		parse_font_tbl("fonts.tbl");
461 
462 		//Then other ones
463 		parse_modular_table("*-fnt.tbm", parse_font_tbl);
464 
465 		// double check
466 		if (FontManager::numberOfFonts() < 3) {
467 			Error(LOCATION, "At least three fonts have to be loaded but only %d valid entries were found!", FontManager::numberOfFonts());
468 		}
469 	}
470 }
471 
472 namespace font
473 {
init()474 	void init()
475 	{
476 		if (font_initialized) {
477 			// Already initialized
478 			return;
479 		}
480 
481 		FontManager::init();
482 
483 		parse_fonts_tbl();
484 
485 		set_font(0);
486 
487 		font_initialized = true;
488 	}
489 
close()490 	void close()
491 	{
492 		if (!font_initialized) {
493 			return;
494 		}
495 
496 		FontManager::close();
497 
498 		font_initialized = false;
499 	}
500 
force_fit_string(char * str,int max_str,int max_width)501 	int force_fit_string(char *str, int max_str, int max_width)
502 	{
503 		int w;
504 
505 		gr_get_string_size(&w, NULL, str);
506 		if (w > max_width) {
507 			if ((int)strlen(str) > max_str - 3) {
508 				Assert(max_str >= 3);
509 				str[max_str - 3] = 0;
510 			}
511 
512 			strcpy(str + strlen(str) - 1, "...");
513 			gr_get_string_size(&w, NULL, str);
514 			while (w > max_width) {
515 				Assert(strlen(str) >= 4);  // if this is hit, a bad max_width was passed in and the calling function needs fixing.
516 				strcpy(str + strlen(str) - 4, "...");
517 				gr_get_string_size(&w, NULL, str);
518 			}
519 		}
520 
521 		return w;
522 	}
523 
stuff_first(SCP_string & firstFont)524 	void stuff_first(SCP_string &firstFont)
525 	{
526 		try
527 		{
528 			font_parse_setup("fonts.tbl");
529 
530 			FontType type;
531 			parse_type(type, firstFont);
532 		}
533 		catch (const parse::ParseException& e)
534 		{
535 			Error(LOCATION, "Failed to setup font parsing. This may be caused by an empty fonts.tbl file.\nError message: %s", e.what());
536 			firstFont = "";
537 		}
538 	}
539 
parse_font()540 	int parse_font()
541 	{
542 		int font_idx;
543 
544 		SCP_string input;
545 		stuff_string(input, F_NAME);
546 		SCP_stringstream ss(input);
547 
548 		int fontNum;
549 		ss >> fontNum;
550 
551 		if (ss.fail())
552 		{
553 			fontNum = FontManager::getFontIndex(input);
554 
555 			if (fontNum < 0)
556 			{
557 				error_display(0, "Invalid font name \"%s\"!", input.c_str());
558 				font_idx = -1;
559 			}
560 			else
561 			{
562 				font_idx = fontNum;
563 			}
564 		}
565 		else
566 		{
567 			if (fontNum < 0 || fontNum >= FontManager::numberOfFonts())
568 			{
569 				error_display(0, "Invalid font number %d! must be greater or equal to zero and smaller than %d.", fontNum, FontManager::numberOfFonts());
570 				font_idx = -1;
571 			}
572 			else
573 			{
574 				font_idx = fontNum;
575 			}
576 		}
577 
578 		return font_idx;
579 	}
580 
get_current_font()581 	FSFont *get_current_font()
582 	{
583 		return FontManager::getCurrentFont();
584 	}
585 
get_font(const SCP_string & name)586 	FSFont *get_font(const SCP_string& name)
587 	{
588 		return FontManager::getFont(name);
589 	}
590 
get_font_by_filename(const SCP_string & name)591 	FSFont *get_font_by_filename(const SCP_string& name)
592 	{
593 		return FontManager::getFontByFilename(name);
594 	}
595 
596 
597 
598 	/**
599 	* @brief	Gets the width of an character.
600 	*
601 	* Returns the width of the specified charachter also taking account of kerning.
602 	*
603 	* @param fnt				The font data
604 	* @param c1				The character that should be checked.
605 	* @param c2				The character which follows this character. Used to compute the kerning
606 	* @param [out]	width   	If non-null, the width.
607 	* @param [out]	spaceing	If non-null, the spaceing.
608 	*
609 	* @return	The character width.
610 	*/
get_char_width_old(fo::font * fnt,ubyte c1,ubyte c2,int * width,int * spacing)611 	int get_char_width_old(fo::font* fnt, ubyte c1, ubyte c2, int *width, int* spacing)
612 	{
613 		int i, letter;
614 
615 		letter = c1 - fnt->first_ascii;
616 
617 		//not in font, draw as space
618 		if (letter < 0 || letter >= fnt->num_chars)
619 		{
620 			*width = 0;
621 			*spacing = fnt->w;
622 			return -1;
623 		}
624 
625 		*width = fnt->char_data[letter].byte_width;
626 		*spacing = fnt->char_data[letter].spacing;
627 
628 		i = fnt->char_data[letter].kerning_entry;
629 		if (i > -1)
630 		{
631 			if (!(c2 == 0 || c2 == '\n'))
632 			{
633 				int letter2;
634 
635 				letter2 = c2 - fnt->first_ascii;
636 
637 				if ((letter2 >= 0) && (letter2 < fnt->num_chars) && (i < fnt->num_kern_pairs))
638 				{
639 					font_kernpair *k = &fnt->kern_data[i];
640 					while ((i < fnt->num_kern_pairs) && (k->c1 == (char)letter) && (k->c2 < (char)letter2))
641 					{
642 						i++;
643 						k++;
644 					}
645 
646 					if ((i < fnt->num_kern_pairs) && (k->c2 == (char)letter2))
647 					{
648 						*spacing += k->offset;
649 					}
650 				}
651 			}
652 		}
653 
654 		return letter;
655 	}
656 }
657 
gr_get_font_height()658 int gr_get_font_height()
659 {
660 	if (FontManager::isReady())
661 	{
662 		return fl2i(FontManager::getCurrentFont()->getHeight());
663 	}
664 	else
665 	{
666 		return 16;
667 	}
668 }
669 
gr_get_string_size(int * w1,int * h1,const char * text,int len)670 void gr_get_string_size(int *w1, int *h1, const char *text, int len)
671 {
672 	if (!FontManager::isReady())
673 	{
674 		if (w1)
675 			*w1 = 16;
676 
677 		if (h1)
678 			*h1 = 16;
679 
680 		return;
681 	}
682 
683 	float w = 0.0f;
684 	float h = 0.0f;
685 
686 	FontManager::getCurrentFont()->getStringSize(text, static_cast<size_t>(len), -1, &w, &h);
687 
688 	if (w1)
689 	{
690 		*w1 = fl2i(ceil(w));
691 	}
692 	if (h1)
693 	{
694 		*h1 = fl2i(ceil(h));
695 	}
696 }
697 
MONITOR(FontChars)698 MONITOR(FontChars)
699 
700 #ifdef _WIN32
701 
702 void gr_string_win(int x, int y, char *s)
703 {
704 	using namespace font;
705 
706 	int old_bitmap = gr_screen.current_bitmap;
707 	set_font(FONT1);
708 	gr_string(x, y, s);
709 	gr_screen.current_bitmap = old_bitmap;
710 }
711 
712 #endif   // ifdef _WIN32
713 
714 char grx_printf_text[2048];
715 
gr_printf(int x,int y,const char * format,...)716 void gr_printf(int x, int y, const char * format, ...)
717 {
718 	va_list args;
719 
720 	if (!FontManager::isReady()) return;
721 
722 	va_start(args, format);
723 	vsnprintf(grx_printf_text, sizeof(grx_printf_text) - 1, format, args);
724 	va_end(args);
725 	grx_printf_text[sizeof(grx_printf_text) - 1] = '\0';
726 
727 	gr_string(x, y, grx_printf_text);
728 }
729 
gr_printf_menu(int x,int y,const char * format,...)730 void gr_printf_menu(int x, int y, const char * format, ...)
731 {
732 	va_list args;
733 
734 	if (!FontManager::isReady()) return;
735 
736 	va_start(args, format);
737 	vsnprintf(grx_printf_text, sizeof(grx_printf_text) - 1, format, args);
738 	va_end(args);
739 	grx_printf_text[sizeof(grx_printf_text) - 1] = '\0';
740 
741 	gr_string(x, y, grx_printf_text, GR_RESIZE_MENU);
742 }
743 
gr_printf_menu_zoomed(int x,int y,const char * format,...)744 void gr_printf_menu_zoomed(int x, int y, const char * format, ...)
745 {
746 	va_list args;
747 
748 	if (!FontManager::isReady()) return;
749 
750 	va_start(args, format);
751 	vsnprintf(grx_printf_text, sizeof(grx_printf_text) - 1, format, args);
752 	va_end(args);
753 	grx_printf_text[sizeof(grx_printf_text) - 1] = '\0';
754 
755 	gr_string(x, y, grx_printf_text, GR_RESIZE_MENU_ZOOMED);
756 }
757 
gr_printf_no_resize(int x,int y,const char * format,...)758 void gr_printf_no_resize(int x, int y, const char * format, ...)
759 {
760 	va_list args;
761 
762 	if (!FontManager::isReady()) return;
763 
764 	va_start(args, format);
765 	vsnprintf(grx_printf_text, sizeof(grx_printf_text) - 1, format, args);
766 	va_end(args);
767 	grx_printf_text[sizeof(grx_printf_text) - 1] = '\0';
768 
769 	gr_string(x, y, grx_printf_text, GR_RESIZE_NONE);
770 }
771