File: display.cpp

package info (click to toggle)
scummvm 2.9.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 450,580 kB
  • sloc: cpp: 4,299,825; asm: 28,322; python: 12,901; sh: 11,302; java: 9,289; xml: 7,895; perl: 2,639; ansic: 2,465; yacc: 1,670; javascript: 1,020; makefile: 933; lex: 578; awk: 275; objc: 82; sed: 11; php: 1
file content (799 lines) | stat: -rw-r--r-- 30,138 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/config-manager.h"
#include "common/std/algorithm.h"
#include "ags/engine/ac/display.h"
#include "ags/shared/ac/common.h"
#include "ags/shared/font/ags_font_renderer.h"
#include "ags/shared/font/fonts.h"
#include "ags/engine/ac/character.h"
#include "ags/engine/ac/draw.h"
#include "ags/engine/ac/game.h"
#include "ags/shared/ac/game_setup_struct.h"
#include "ags/engine/ac/game_state.h"
#include "ags/engine/ac/global_audio.h"
#include "ags/engine/ac/global_game.h"
#include "ags/engine/ac/gui.h"
#include "ags/engine/ac/mouse.h"
#include "ags/engine/ac/overlay.h"
#include "ags/engine/ac/sys_events.h"
#include "ags/engine/ac/screen_overlay.h"
#include "ags/engine/ac/speech.h"
#include "ags/engine/ac/string.h"
#include "ags/engine/ac/system.h"
#include "ags/engine/ac/top_bar_settings.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/engine/gfx/blender.h"
#include "ags/shared/gui/gui_button.h"
#include "ags/shared/gui/gui_main.h"
#include "ags/engine/main/game_run.h"
#include "ags/engine/platform/base/ags_platform_driver.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/engine/gfx/gfx_util.h"
#include "ags/shared/util/string_utils.h"
#include "ags/engine/ac/mouse.h"
#include "ags/engine/media/audio/audio_system.h"
#include "ags/engine/ac/timer.h"
#include "ags/ags.h"
#include "ags/globals.h"

namespace AGS3 {

using namespace AGS::Shared;
using namespace AGS::Shared::BitmapHelper;

struct DisplayVars {
	int linespacing = 0;   // font's line spacing
	int fulltxtheight = 0; // total height of all the text
} disp;

// Generates a textual image and returns a disposable bitmap
Bitmap *create_textual_image(const char *text, int asspch, int isThought,
							 int &xx, int &yy, int &adjustedXX, int &adjustedYY, int wii, int usingfont, int allowShrink,
							 bool &alphaChannel) {
	//
	// Configure the textual image
	//
	const bool use_speech_textwindow = (asspch < 0) && (_GP(game).options[OPT_SPEECHTYPE] >= 2);
	const bool use_thought_gui = (isThought) && (_GP(game).options[OPT_THOUGHTGUI] > 0);

	alphaChannel = false;
	int usingGui = -1;
	if (use_speech_textwindow)
		usingGui = _GP(play).speech_textwindow_gui;
	else if (use_thought_gui)
		usingGui = _GP(game).options[OPT_THOUGHTGUI];

	const int padding = get_textwindow_padding(usingGui);
	const int paddingScaled = get_fixed_pixel_size(padding);
 	// Just in case screen size is not neatly divisible by 320x200
	const int paddingDoubledScaled = get_fixed_pixel_size(padding * 2);

	// WORKAROUND: Guard Duty specifies a wii of 100,000, which is larger
	// than can be supported by ScummVM's surface classes
	wii = MIN(wii, 8000);

	// Make message copy, because ensure_text_valid_for_font() may modify it
	char todis[STD_BUFFER_SIZE];
	snprintf(todis, STD_BUFFER_SIZE - 1, "%s", text);
	ensure_text_valid_for_font(todis, usingfont);
	break_up_text_into_lines(todis, _GP(Lines), wii - 2 * padding, usingfont);
	disp.linespacing = get_font_linespacing(usingfont);
	disp.fulltxtheight = get_text_lines_surf_height(usingfont, _GP(Lines).Count());

	if (_GP(topBar).wantIt) {
		// ensure that the window is wide enough to display any top bar text
		int topBarWid = get_text_width_outlined(_GP(topBar).text, _GP(topBar).font);
		topBarWid += data_to_game_coord(_GP(play).top_bar_borderwidth + 2) * 2;
		if (_G(longestline) < topBarWid)
			_G(longestline) = topBarWid;
	}

	const Rect &ui_view = _GP(play).GetUIViewport();
	if (xx == OVR_AUTOPLACE);
	// centre text in middle of screen
	else if (yy < 0) yy = ui_view.GetHeight() / 2 - disp.fulltxtheight / 2 - padding;
	// speech, so it wants to be above the character's head
	else if (asspch > 0) {
		yy -= disp.fulltxtheight;
		if (yy < 5) yy = 5;
		yy = adjust_y_for_guis(yy);
	}

	if (_G(longestline) < wii - paddingDoubledScaled) {
		// shrink the width of the dialog box to fit the text
		int oldWid = wii;
		//if ((asspch >= 0) || (allowShrink > 0))
		// If it's not speech, or a shrink is allowed, then shrink it
		if ((asspch == 0) || (allowShrink > 0))
			wii = _G(longestline) + paddingDoubledScaled;

		// shift the dialog box right to align it, if necessary
		if ((allowShrink == 2) && (xx >= 0))
			xx += (oldWid - wii);
	}

	if (xx < -1) {
		xx = (-xx) - wii / 2;
		if (xx < 0)
			xx = 0;

		xx = adjust_x_for_guis(xx, yy);

		if (xx + wii >= ui_view.GetWidth())
			xx = (ui_view.GetWidth() - wii) - 5;
	} else if (xx < 0) xx = ui_view.GetWidth() / 2 - wii / 2;

	const int extraHeight = paddingDoubledScaled;
	color_t text_color = MakeColor(15);
	const int bmp_width = MAX(2, wii);
	const int bmp_height = MAX(2, disp.fulltxtheight + extraHeight);
	Bitmap *text_window_ds = BitmapHelper::CreateTransparentBitmap(
		bmp_width, bmp_height, _GP(game).GetColorDepth());

	// inform draw_text_window to free the old bitmap
	const bool wantFreeScreenop = true;

	//
	// Create the textual image (may also adjust some params in the process)
	//
	// may later change if usingGUI, needed to avoid changing original coordinates
	adjustedXX = xx;
	adjustedYY = yy;

	if ((strlen(todis) < 1) || (strcmp(todis, "  ") == 0) || (wii == 0));
	// if it's an empty speech line, don't draw anything
	else if (asspch) { //text_color = ds->GetCompatibleColor(12);
		int ttxleft = 0, ttxtop = paddingScaled, oriwid = wii - padding * 2;
		int drawBackground = 0;

		if (use_speech_textwindow) {
			drawBackground = 1;
		} else if (use_thought_gui) {
			// make it treat it as drawing inside a window now
			if (asspch > 0)
				asspch = -asspch;
			drawBackground = 1;
		}

		if (drawBackground) {
			draw_text_window_and_bar(&text_window_ds, wantFreeScreenop, &ttxleft, &ttxtop, &adjustedXX, &adjustedYY, &wii, &text_color, 0, usingGui);
			if (usingGui > 0) {
				alphaChannel = _GP(guis)[usingGui].HasAlphaChannel();
			}
		} else if ((ShouldAntiAliasText()) && (_GP(game).GetColorDepth() >= 24))
			alphaChannel = true;

		for (size_t ee = 0; ee < _GP(Lines).Count(); ee++) {
			//int ttxp=wii/2 - get_text_width_outlined(lines[ee], usingfont)/2;
			int ttyp = ttxtop + ee * disp.linespacing;
			// asspch < 0 means that it's inside a text box so don't
			// centre the text
			if (asspch < 0) {
				if ((usingGui >= 0) &&
				        ((_GP(game).options[OPT_SPEECHTYPE] >= 2) || (isThought)))
					text_color = text_window_ds->GetCompatibleColor(_GP(guis)[usingGui].FgColor);
				else
					text_color = text_window_ds->GetCompatibleColor(-asspch);

				wouttext_aligned(text_window_ds, ttxleft, ttyp, oriwid, usingfont, text_color, _GP(Lines)[ee].GetCStr(), _GP(play).text_align);
			} else {
				text_color = text_window_ds->GetCompatibleColor(asspch);
				wouttext_aligned(text_window_ds, ttxleft, ttyp, wii, usingfont, text_color, _GP(Lines)[ee].GetCStr(), _GP(play).speech_text_align);
			}
		}
	} else {
		int xoffs, yoffs, oriwid = wii - padding * 2;
		draw_text_window_and_bar(&text_window_ds, wantFreeScreenop, &xoffs, &yoffs, &adjustedXX, &adjustedYY, &wii, &text_color);

		if (_GP(game).options[OPT_TWCUSTOM] > 0) {
			alphaChannel = _GP(guis)[_GP(game).options[OPT_TWCUSTOM]].HasAlphaChannel();
		}

		adjust_y_coordinate_for_text(&yoffs, usingfont);

		for (size_t ee = 0; ee < _GP(Lines).Count(); ee++)
			wouttext_aligned(text_window_ds, xoffs, yoffs + ee * disp.linespacing, oriwid, usingfont, text_color, _GP(Lines)[ee].GetCStr(), _GP(play).text_align);
	}

	return text_window_ds;
}

// Pass yy = -1 to find Y co-ord automatically
// allowShrink = 0 for none, 1 for leftwards, 2 for rightwards
// pass blocking=2 to create permanent overlay
ScreenOverlay *display_main(int xx, int yy, int wii, const char *text, int disp_type, int usingfont,
							 int asspch, int isThought, int allowShrink, bool overlayPositionFixed, bool roomlayer) {
	//
	// Prepare for the message display
	//

	// AGS 2.x: If the screen is faded out, fade in again when displaying a message box.
	if (!asspch && (_G(loaded_game_file_version) <= kGameVersion_272))
		_GP(play).screen_is_faded_out = 0;

	// if it's a normal message box and the game was being skipped,
	// ensure that the screen is up to date before the message box
	// is drawn on top of it
	// TODO: is this really necessary anymore?
	if ((_GP(play).skip_until_char_stops >= 0) && (disp_type == DISPLAYTEXT_MESSAGEBOX))
		render_graphics();

	// TODO: should this really be called regardless of message type?
	// _display_main may be called even for custom textual overlays
	EndSkippingUntilCharStops();

	if (_GP(topBar).wantIt) {
		// the top bar should behave like DisplaySpeech wrt blocking
		disp_type = DISPLAYTEXT_SPEECH;
	}

	if ((asspch > 0) && (disp_type < DISPLAYTEXT_NORMALOVERLAY)) {
		// update the all_buttons_disabled variable in advance
		// of the adjust_x/y_for_guis calls
		_GP(play).disabled_user_interface++;
		update_gui_disabled_status();
		_GP(play).disabled_user_interface--;
	}

	// remove any previous blocking texts if necessary
	if (disp_type < DISPLAYTEXT_NORMALOVERLAY)
		remove_screen_overlay(_GP(play).text_overlay_on);

	// If fast-forwarding, then skip any blocking message immediately
	if (_GP(play).fast_forward && (disp_type < DISPLAYTEXT_NORMALOVERLAY)) {
		_GP(play).SetWaitSkipResult(SKIP_AUTOTIMER);
		post_display_cleanup();
		return nullptr;
	}

	//
	// Configure and create an overlay object
	//

	int ovrtype;
	switch (disp_type) {
	case DISPLAYTEXT_SPEECH: ovrtype = OVER_TEXTSPEECH; break;
	case DISPLAYTEXT_MESSAGEBOX: ovrtype = OVER_TEXTMSG; break;
	case DISPLAYTEXT_NORMALOVERLAY: ovrtype = OVER_CUSTOM; break;
	default: ovrtype = disp_type; break; // must be precreated overlay id
	}

	int adjustedXX, adjustedYY;
	bool alphaChannel;
	Bitmap *text_window_ds = create_textual_image(text, asspch, isThought, xx, yy, adjustedXX, adjustedYY, wii, usingfont, allowShrink, alphaChannel);

	size_t nse = add_screen_overlay(roomlayer, xx, yy, ovrtype, text_window_ds, adjustedXX - xx, adjustedYY - yy, alphaChannel);
	auto *over = get_overlay(nse); // FIXME: optimize return value
	// we should not delete text_window_ds here, because it is now owned by Overlay

	// If it's a non-blocking overlay type, then we're done here
	if (disp_type >= DISPLAYTEXT_NORMALOVERLAY) {
		return over;
	}

	//
	// Wait for the blocking text to timeout or until skipped by another command
	//

	if (disp_type == DISPLAYTEXT_MESSAGEBOX) {

		int countdown = GetTextDisplayTime(text);
		int skip_setting = user_to_internal_skip_speech((SkipSpeechStyle)_GP(play).skip_display);
		// Loop until skipped
		while (true) {
			if (SHOULD_QUIT)
				return 0;

			sys_evt_process_pending();

			update_audio_system_on_game_loop();
			UpdateCursorAndDrawables();
			render_graphics();
			eAGSMouseButton mbut;
			int mwheelz;
			if (run_service_mb_controls(mbut, mwheelz) && mbut > kMouseNone) {
				check_skip_cutscene_mclick(mbut);
				if (_GP(play).fast_forward)
					break;
				if (skip_setting & SKIP_MOUSECLICK && !_GP(play).IsIgnoringInput()) {
					_GP(play).SetWaitSkipResult(SKIP_MOUSECLICK, mbut);
					break;
				}
			}
			bool do_break = false;
			while (!_GP(play).fast_forward && !do_break && ags_keyevent_ready()) {
				KeyInput ki;
				if (run_service_key_controls(ki)) {
					check_skip_cutscene_keypress(ki.Key);
					if ((skip_setting & SKIP_KEYPRESS) && !_GP(play).IsIgnoringInput() && !IsAGSServiceKey(ki.Key)) {
						_GP(play).SetWaitKeySkip(ki);
						do_break = true;
					}
				}
			}
			if (do_break)
				break;

			update_polled_stuff();

			if (_GP(play).fast_forward == 0) {
				WaitForNextFrame();
			}

			countdown--;

			// Special behavior when coupled with a voice-over
			if (_GP(play).speech_has_voice) {
				// extend life of text if the voice hasn't finished yet
				if (AudioChans::ChannelIsPlaying(SCHAN_SPEECH) && (_GP(play).fast_forward == 0)) {
					if (countdown <= 1)
						countdown = 1;
				} else  // if the voice has finished, remove the speech
					countdown = 0;
			}
			// Test for the timed auto-skip
			if ((countdown < 1) && (skip_setting & SKIP_AUTOTIMER)) {
				_GP(play).SetWaitSkipResult(SKIP_AUTOTIMER);
				_GP(play).SetIgnoreInput(_GP(play).ignore_user_input_after_text_timeout_ms);
				break;
			}
			// if skipping cutscene, don't get stuck on No Auto Remove text boxes
			if ((countdown < 1) && (_GP(play).fast_forward))
				break;
		}

		remove_screen_overlay(OVER_TEXTMSG);
		invalidate_screen();
	} else {
		/* DISPLAYTEXT_SPEECH */
		if (!overlayPositionFixed) {
			over->SetRoomRelative(true);
			VpPoint vpt = _GP(play).GetRoomViewport(0)->ScreenToRoom(over->x, over->y, false);
			over->x = vpt.first.X;
			over->y = vpt.first.Y;
		}

		GameLoopUntilNoOverlay();
	}

	//
	// Post-message cleanup
	//
	post_display_cleanup();
	return nullptr;
}

void display_at(int xx, int yy, int wii, const char *text) {
	EndSkippingUntilCharStops();
	// Start voice-over, if requested by the tokens in speech text
	try_auto_play_speech(text, text, _GP(play).narrator_speech);
	display_main(xx, yy, wii, text, DISPLAYTEXT_MESSAGEBOX, FONT_NORMAL, 0, 0, 0, false);

	// Stop any blocking voice-over, if was started by this function
	if (_GP(play).IsBlockingVoiceSpeech())
		stop_voice_speech();
}

void post_display_cleanup() {
	ags_clear_input_buffer();
	_GP(play).messagetime = -1;
	_GP(play).speech_in_post_state = false;
}

bool try_auto_play_speech(const char *text, const char *&replace_text, int charid) {
	int voice_num;
	const char *src = parse_voiceover_token(text, &voice_num);
	if (src == text)
		return false; // no token

	if (voice_num <= 0)
		quit("DisplaySpeech: auto-voice symbol '&' not followed by valid integer");

	replace_text = src; // skip voice tag
	if (play_voice_speech(charid, voice_num)) {
		// if Voice Only, then blank out the text
		if (_GP(play).speech_mode == kSpeech_VoiceOnly)
			replace_text = "  ";
		return true;
	}
	return false;
}

int GetTextDisplayLength(const char *text) {
	// Skip voice-over token from the length calculation if required
	if (_GP(play).unfactor_speech_from_textlength != 0)
		text = parse_voiceover_token(text, nullptr);
	return static_cast<int>(strlen(text));
}

// Calculates lipsync frame duration (or duration per character) in game loops.
// NOTE: historical formula was this:
//   loops_per_character = (((text_len / play.lipsync_speed) + 1) * fps) / text_len;
// But because of a precision loss due integer division this resulted in "jumping" values.
// The new formula uses float division, and coefficent found experimentally to make
// results match the old formula in certain key text lengths, for backwards compatibility.
int CalcLipsyncFrameDuration(int text_len, int fps) {
	return static_cast<int>((((static_cast<float>(text_len) / _GP(play).lipsync_speed) + 0.75f) * fps) / text_len);
}

int GetTextDisplayTime(const char *text, int canberel) {
	int uselen = 0;
	auto fpstimer = ::lround(get_game_fps());

	// if it's background speech, make it stay relative to game speed
	if ((canberel == 1) && (_GP(play).bgspeech_game_speed == 1))
		fpstimer = 40; // NOTE: should be a fixed constant here, not game speed value

	if (_G(source_text_length) >= 0) {
		// sync to length of original text, to make sure any animations
		// and music sync up correctly
		uselen = _G(source_text_length);
		_G(source_text_length) = -1;
	} else {
		uselen = GetTextDisplayLength(text);
	}

	if (uselen <= 0)
		return 0;

	if (_GP(play).text_speed + _GP(play).text_speed_modifier <= 0)
		quit("!Text speed is zero; unable to display text. Check your _GP(game).text_speed settings.");

	// Store how many game loops per character of text
	_G(loops_per_character) = CalcLipsyncFrameDuration(uselen, fpstimer);

	int textDisplayTimeInMS = ((uselen / (_GP(play).text_speed + _GP(play).text_speed_modifier)) + 1) * 1000;
	if (textDisplayTimeInMS < _GP(play).text_min_display_time_ms)
		textDisplayTimeInMS = _GP(play).text_min_display_time_ms;

	return (textDisplayTimeInMS * fpstimer) / 1000;
}

bool ShouldAntiAliasText() {
	return (_GP(game).GetColorDepth() >= 24) && (_GP(game).options[OPT_ANTIALIASFONTS] != 0 || ::AGS::g_vm->_forceTextAA);
}

void wouttextxy_AutoOutline(Bitmap *ds, size_t font, int32_t color, const char *texx, int &xxp, int &yyp) {
	const FontInfo &finfo = get_fontinfo(font);
	int const thickness = finfo.AutoOutlineThickness;
	auto const style = finfo.AutoOutlineStyle;
	if (thickness <= 0)
		return;

	// 16-bit games should use 32-bit stencils to keep anti-aliasing working
	// because 16-bit blending works correctly if there's an actual color
	// on the destination bitmap (and our intermediate bitmaps are transparent).
	int const  ds_cd = ds->GetColorDepth();
	bool const antialias = ds_cd >= 16 && _GP(game).options[OPT_ANTIALIASFONTS] != 0 && !is_bitmap_font(font);
	int const  stencil_cd = antialias ? 32 : ds_cd;
	if (antialias) // This is to make sure TTFs render proper alpha channel in 16-bit games too
		color |= makeacol32(0, 0, 0, 0xff);

	// WORKAROUND: Clifftop's Spritefont plugin returns a wrong font height for font 2 in Kathy Rain, which causes a partial outline
	// for some letters. Unfortunately fixing the value on the plugin side breaks the line spacing, so let's just correct it here.
	const int t_width = get_text_width(texx, font);
	const auto t_extent = get_font_surface_extent(font);
	const int t_height = t_extent.second - t_extent.first + ((strcmp(_GP(game).guid, "{d6795d1c-3cfe-49ec-90a1-85c313bfccaf}") == 0) && (font == 2) ? 1 : 0);

	if (t_width == 0 || t_height == 0)
		return;

	// Prepare stencils
	const int t_yoff = t_extent.first;
	Bitmap *texx_stencil, *outline_stencil;
	alloc_font_outline_buffers(font, &texx_stencil, &outline_stencil,
		t_width, t_height, stencil_cd);
	texx_stencil->ClearTransparent();
	outline_stencil->ClearTransparent();
	// Ready text stencil
	// Note we are drawing with y off, in case some font's glyphs exceed font's ascender
	wouttextxy(texx_stencil, 0, -t_yoff, font, color, texx);

	// Anti-aliased TTFs require to be alpha-blended, not blit,
	// or the alpha values will be plain copied and final image will be broken.
	void(Bitmap:: * pfn_drawstencil)(Bitmap * src, int dst_x, int dst_y);
	if (antialias) { // NOTE: we must set out blender AFTER wouttextxy, or it will be overidden
		set_argb2any_blender();
		pfn_drawstencil = &Bitmap::TransBlendBlt;
	} else {
		pfn_drawstencil = &Bitmap::MaskedBlit;
	}

	// move start of text so that the outline doesn't drop off the bitmap
	xxp += thickness;
	int const outline_y = yyp + t_yoff;
	yyp += thickness;

	// What we do here: first we paint text onto outline_stencil offsetting vertically;
	// then we paint resulting outline_stencil onto final dest offsetting horizontally.
	int largest_y_diff_reached_so_far = -1;
	for (int x_diff = thickness; x_diff >= 0; x_diff--) {
		// Integer arithmetics: In the following, we use terms k*(k + 1) to account for rounding.
		//     (k + 0.5)^2 == k*k + 2*k*0.5 + 0.5^2 == k*k + k + 0.25 ==approx. k*(k + 1)
		int y_term_limit = thickness * (thickness + 1);
		if (FontInfo::kRounded == style)
			y_term_limit -= x_diff * x_diff;

		// Extend the outline stencil to the top and bottom
		for (int y_diff = largest_y_diff_reached_so_far + 1;
			y_diff <= thickness && y_diff * y_diff <= y_term_limit;
			y_diff++) {
			(outline_stencil->*pfn_drawstencil)(texx_stencil, 0, thickness - y_diff);
			if (y_diff > 0)
				(outline_stencil->*pfn_drawstencil)(texx_stencil, 0, thickness + y_diff);
			largest_y_diff_reached_so_far = y_diff;
		}

		// Stamp the outline stencil to the left and right of the text
		(ds->*pfn_drawstencil)(outline_stencil, xxp - x_diff, outline_y);
		if (x_diff > 0)
			(ds->*pfn_drawstencil)(outline_stencil, xxp + x_diff, outline_y);
	}
}

// Draw an outline if requested, then draw the text on top
void wouttext_outline(Shared::Bitmap *ds, int xxp, int yyp, int font, color_t text_color, const char *texx) {
	size_t const text_font = static_cast<size_t>(font);
	// Draw outline (a backdrop) if requested
	color_t const outline_color = ds->GetCompatibleColor(_GP(play).speech_text_shadow);
	int const outline_font = get_font_outline(font);
	if (outline_font >= 0)
		wouttextxy(ds, xxp, yyp, static_cast<size_t>(outline_font), outline_color, texx);
	else if (outline_font == FONT_OUTLINE_AUTO)
		wouttextxy_AutoOutline(ds, text_font, outline_color, texx, xxp, yyp);
	else
		; // no outline

	// Draw text on top
	wouttextxy(ds, xxp, yyp, text_font, text_color, texx);
}

void wouttext_aligned(Bitmap *ds, int usexp, int yy, int oriwid, int usingfont, color_t text_color, const char *text, HorAlignment align) {

	if (align & kMAlignHCenter)
		usexp = usexp + (oriwid / 2) - (get_text_width_outlined(text, usingfont) / 2);
	else if (align & kMAlignRight)
		usexp = usexp + (oriwid - get_text_width_outlined(text, usingfont));

	wouttext_outline(ds, usexp, yy, usingfont, text_color, text);
}

int get_font_outline_padding(int font) {
	if (get_font_outline(font) == FONT_OUTLINE_AUTO) {
		// scaled up bitmap font, push outline further out
		if (is_bitmap_font(font) && get_font_scaling_mul(font) > 1)
			return get_fixed_pixel_size(2); // FIXME: should be 2 + get_fixed_pixel_size(2)?
		// otherwise, just push outline by 1 pixel
		else
			return 2;
	}
	return 0;
}

void do_corner(Bitmap *ds, int sprn, int x, int y, int offx, int offy) {
	if (sprn < 0) return;
	if (!_GP(spriteset).DoesSpriteExist(sprn)) {
		sprn = 0;
	}

	x = x + offx * _GP(game).SpriteInfos[sprn].Width;
	y = y + offy * _GP(game).SpriteInfos[sprn].Height;
	draw_gui_sprite_v330(ds, sprn, x, y);
}

int get_but_pic(GUIMain *guo, int indx) {
	int butid = guo->GetControlID(indx);
	return butid >= 0 ? _GP(guibuts)[butid].GetNormalImage() : 0;
}

void draw_button_background(Bitmap *ds, int xx1, int yy1, int xx2, int yy2, GUIMain *iep) {
	color_t draw_color;
	if (iep == nullptr) {  // standard window
		draw_color = ds->GetCompatibleColor(15);
		ds->FillRect(Rect(xx1, yy1, xx2, yy2), draw_color);
		draw_color = ds->GetCompatibleColor(16);
		ds->DrawRect(Rect(xx1, yy1, xx2, yy2), draw_color);
	} else {
		if (_G(loaded_game_file_version) < kGameVersion_262) {
			// In pre-2.62 color 0 should be treated as "black" instead of "transparent";
			// this was an unintended effect in older versions (see 2.62 changelog fixes).
			if (iep->BgColor == 0)
				iep->BgColor = 16;
		}

		if (iep->BgColor >= 0) draw_color = ds->GetCompatibleColor(iep->BgColor);
		else draw_color = ds->GetCompatibleColor(0); // black backrgnd behind picture

		if (iep->BgColor > 0)
			ds->FillRect(Rect(xx1, yy1, xx2, yy2), draw_color);

		const int leftRightWidth = _GP(game).SpriteInfos[get_but_pic(iep, 4)].Width;
		const int topBottomHeight = _GP(game).SpriteInfos[get_but_pic(iep, 6)].Height;
		// GUI middle space
		if (iep->BgImage > 0) {
			{
				// offset the background image and clip it so that it is drawn
				// such that the border graphics can have a transparent outside
				// edge
				int bgoffsx = xx1 - leftRightWidth / 2;
				int bgoffsy = yy1 - topBottomHeight / 2;
				ds->SetClip(Rect(bgoffsx, bgoffsy, xx2 + leftRightWidth / 2, yy2 + topBottomHeight / 2));
				int bgfinishx = xx2;
				int bgfinishy = yy2;
				int bgoffsyStart = bgoffsy;
				while (bgoffsx <= bgfinishx) {
					bgoffsy = bgoffsyStart;
					while (bgoffsy <= bgfinishy) {
						draw_gui_sprite_v330(ds, iep->BgImage, bgoffsx, bgoffsy);
						bgoffsy += _GP(game).SpriteInfos[iep->BgImage].Height;
					}
					bgoffsx += _GP(game).SpriteInfos[iep->BgImage].Width;
				}
				// return to normal clipping rectangle
				ds->ResetClip();
			}
		}
		// Vertical borders
		ds->SetClip(Rect(xx1 - leftRightWidth, yy1, xx2 + 1 + leftRightWidth, yy2));
		for (int uu = yy1; uu <= yy2; uu += _GP(game).SpriteInfos[get_but_pic(iep, 4)].Height) {
			do_corner(ds, get_but_pic(iep, 4), xx1, uu, -1, 0);    // left side
			do_corner(ds, get_but_pic(iep, 5), xx2 + 1, uu, 0, 0); // right side
		}
		// Horizontal borders
		ds->SetClip(Rect(xx1, yy1 - topBottomHeight, xx2, yy2 + 1 + topBottomHeight));
		for (int uu = xx1; uu <= xx2; uu += _GP(game).SpriteInfos[get_but_pic(iep, 6)].Width) {
			do_corner(ds, get_but_pic(iep, 6), uu, yy1, 0, -1);    // top side
			do_corner(ds, get_but_pic(iep, 7), uu, yy2 + 1, 0, 0); // bottom side
		}
		ds->ResetClip();
		// Four corners
		do_corner(ds, get_but_pic(iep, 0), xx1, yy1, -1, -1);       // top left
		do_corner(ds, get_but_pic(iep, 1), xx1, yy2 + 1, -1, 0);    // bottom left
		do_corner(ds, get_but_pic(iep, 2), xx2 + 1, yy1, 0, -1);    // top right
		do_corner(ds, get_but_pic(iep, 3), xx2 + 1, yy2 + 1, 0, 0); // bottom right
	}
}

// Calculate the width that the left and right border of the textwindow
// GUI take up
int get_textwindow_border_width(int twgui) {
	if (twgui < 0)
		return 0;

	if (!_GP(guis)[twgui].IsTextWindow())
		quit("!GUI set as text window but is not actually a text window GUI");

	int borwid = _GP(game).SpriteInfos[get_but_pic(&_GP(guis)[twgui], 4)].Width +
	             _GP(game).SpriteInfos[get_but_pic(&_GP(guis)[twgui], 5)].Width;

	return borwid;
}

// get the hegiht of the text window's top border
int get_textwindow_top_border_height(int twgui) {
	if (twgui < 0)
		return 0;

	if (!_GP(guis)[twgui].IsTextWindow())
		quit("!GUI set as text window but is not actually a text window GUI");

	return _GP(game).SpriteInfos[get_but_pic(&_GP(guis)[twgui], 6)].Height;
}

// Get the padding for a text window
// -1 for the game's custom text window
int get_textwindow_padding(int ifnum) {
	int result;

	if (ifnum < 0)
		ifnum = _GP(game).options[OPT_TWCUSTOM];
	if (ifnum > 0 && ifnum < _GP(game).numgui)
		result = _GP(guis)[ifnum].Padding;
	else
		result = TEXTWINDOW_PADDING_DEFAULT;

	return result;
}

void draw_text_window(Bitmap **text_window_ds, bool should_free_ds,
                      int *xins, int *yins, int *xx, int *yy, int *wii, color_t *set_text_color, int ovrheight, int ifnum) {
	assert(text_window_ds);
	Bitmap *ds = *text_window_ds;
	if (ifnum < 0)
		ifnum = _GP(game).options[OPT_TWCUSTOM];

	if (ifnum <= 0) {
		if (ovrheight)
			quit("!Cannot use QFG4 style options without custom text window");
		draw_button_background(ds, 0, 0, ds->GetWidth() - 1, ds->GetHeight() - 1, nullptr);
		if (set_text_color)
			*set_text_color = ds->GetCompatibleColor(16);
		xins[0] = 3;
		yins[0] = 3;
	} else {
		if (ifnum >= _GP(game).numgui)
			quitprintf("!Invalid GUI %d specified as text window (total GUIs: %d)", ifnum, _GP(game).numgui);
		if (!_GP(guis)[ifnum].IsTextWindow())
			quit("!GUI set as text window but is not actually a text window GUI");

		int tbnum = get_but_pic(&_GP(guis)[ifnum], 0);

		wii[0] += get_textwindow_border_width(ifnum);
		xx[0] -= _GP(game).SpriteInfos[tbnum].Width;
		yy[0] -= _GP(game).SpriteInfos[tbnum].Height;
		if (ovrheight == 0)
			ovrheight = disp.fulltxtheight;

		if (should_free_ds)
			delete *text_window_ds;
		int padding = get_textwindow_padding(ifnum);
		*text_window_ds = BitmapHelper::CreateTransparentBitmap(wii[0], ovrheight + (padding * 2) + _GP(game).SpriteInfos[tbnum].Height * 2, _GP(game).GetColorDepth());
		ds = *text_window_ds;
		int xoffs = _GP(game).SpriteInfos[tbnum].Width, yoffs = _GP(game).SpriteInfos[tbnum].Height;
		draw_button_background(ds, xoffs, yoffs, (ds->GetWidth() - xoffs) - 1, (ds->GetHeight() - yoffs) - 1, &_GP(guis)[ifnum]);
		if (set_text_color)
			*set_text_color = ds->GetCompatibleColor(_GP(guis)[ifnum].FgColor);
		xins[0] = xoffs + padding;
		yins[0] = yoffs + padding;
	}
}

void draw_text_window_and_bar(Bitmap **text_window_ds, bool should_free_ds,
                              int *xins, int *yins, int *xx, int *yy, int *wii, color_t *set_text_color, int ovrheight, int ifnum) {
	assert(text_window_ds);
	draw_text_window(text_window_ds, should_free_ds, xins, yins, xx, yy, wii, set_text_color, ovrheight, ifnum);

	if ((_GP(topBar).wantIt) && (text_window_ds && *text_window_ds)) {
		// top bar on the dialog window with character's name
		// create an enlarged window, then free the old one
		Bitmap *ds = *text_window_ds;
		Bitmap *newScreenop = BitmapHelper::CreateBitmap(ds->GetWidth(), ds->GetHeight() + _GP(topBar).height, _GP(game).GetColorDepth());
		newScreenop->Blit(ds, 0, 0, 0, _GP(topBar).height, ds->GetWidth(), ds->GetHeight());
		delete *text_window_ds;
		*text_window_ds = newScreenop;
		ds = *text_window_ds;

		// draw the top bar
		color_t draw_color = ds->GetCompatibleColor(_GP(play).top_bar_backcolor);
		ds->FillRect(Rect(0, 0, ds->GetWidth() - 1, _GP(topBar).height - 1), draw_color);
		if (_GP(play).top_bar_backcolor != _GP(play).top_bar_bordercolor) {
			// draw the border
			draw_color = ds->GetCompatibleColor(_GP(play).top_bar_bordercolor);
			for (int j = 0; j < data_to_game_coord(_GP(play).top_bar_borderwidth); j++)
				ds->DrawRect(Rect(j, j, ds->GetWidth() - (j + 1), _GP(topBar).height - (j + 1)), draw_color);
		}

		// draw the text
		int textx = (ds->GetWidth() / 2) - get_text_width_outlined(_GP(topBar).text, _GP(topBar).font) / 2;
		color_t text_color = ds->GetCompatibleColor(_GP(play).top_bar_textcolor);
		wouttext_outline(ds, textx, _GP(play).top_bar_borderwidth + get_fixed_pixel_size(1), _GP(topBar).font, text_color, _GP(topBar).text);

		// don't draw it next time
		_GP(topBar).wantIt = 0;
		// adjust the text Y position
		yins[0] += _GP(topBar).height;
	} else if (_GP(topBar).wantIt)
		_GP(topBar).wantIt = 0;
}

} // namespace AGS3