1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "common/config-manager.h"
24 #include "ags/lib/std/algorithm.h"
25 #include "ags/lib/std/math.h"
26 #include "ags/engine/ac/display.h"
27 #include "ags/shared/ac/common.h"
28 #include "ags/shared/font/ags_font_renderer.h"
29 #include "ags/shared/font/fonts.h"
30 #include "ags/engine/ac/character.h"
31 #include "ags/engine/ac/draw.h"
32 #include "ags/engine/ac/game.h"
33 #include "ags/shared/ac/game_setup_struct.h"
34 #include "ags/engine/ac/game_state.h"
35 #include "ags/engine/ac/global_audio.h"
36 #include "ags/engine/ac/global_game.h"
37 #include "ags/engine/ac/gui.h"
38 #include "ags/engine/ac/mouse.h"
39 #include "ags/engine/ac/overlay.h"
40 #include "ags/engine/ac/sys_events.h"
41 #include "ags/engine/ac/screen_overlay.h"
42 #include "ags/engine/ac/speech.h"
43 #include "ags/engine/ac/string.h"
44 #include "ags/engine/ac/system.h"
45 #include "ags/engine/ac/top_bar_settings.h"
46 #include "ags/engine/debugging/debug_log.h"
47 #include "ags/shared/gui/gui_button.h"
48 #include "ags/shared/gui/gui_main.h"
49 #include "ags/engine/main/game_run.h"
50 #include "ags/engine/platform/base/ags_platform_driver.h"
51 #include "ags/shared/ac/sprite_cache.h"
52 #include "ags/engine/gfx/gfx_util.h"
53 #include "ags/shared/util/string_utils.h"
54 #include "ags/engine/ac/mouse.h"
55 #include "ags/engine/media/audio/audio_system.h"
56 #include "ags/engine/ac/timer.h"
57 #include "ags/ags.h"
58 #include "ags/globals.h"
59 
60 namespace AGS3 {
61 
62 using namespace AGS::Shared;
63 
64 struct DisplayVars {
65 	int lineheight;    // font's height of single line
66 	int linespacing;   // font's line spacing
67 	int fulltxtheight; // total height of all the text
68 } disp;
69 
70 // Pass yy = -1 to find Y co-ord automatically
71 // allowShrink = 0 for none, 1 for leftwards, 2 for rightwards
72 // pass blocking=2 to create permanent overlay
_display_main(int xx,int yy,int wii,const char * text,int disp_type,int usingfont,int asspch,int isThought,int allowShrink,bool overlayPositionFixed)73 int _display_main(int xx, int yy, int wii, const char *text, int disp_type, int usingfont, int asspch, int isThought, int allowShrink, bool overlayPositionFixed) {
74 	const bool use_speech_textwindow = (asspch < 0) && (_GP(game).options[OPT_SPEECHTYPE] >= 2);
75 	const bool use_thought_gui = (isThought) && (_GP(game).options[OPT_THOUGHTGUI] > 0);
76 
77 	bool alphaChannel = false;
78 	char todis[STD_BUFFER_SIZE];
79 	snprintf(todis, STD_BUFFER_SIZE - 1, "%s", text);
80 	int usingGui = -1;
81 	if (use_speech_textwindow)
82 		usingGui = _GP(play).speech_textwindow_gui;
83 	else if (use_thought_gui)
84 		usingGui = _GP(game).options[OPT_THOUGHTGUI];
85 
86 	int padding = get_textwindow_padding(usingGui);
87 	int paddingScaled = get_fixed_pixel_size(padding);
88 	int paddingDoubledScaled = get_fixed_pixel_size(padding * 2); // Just in case screen size does is not neatly divisible by 320x200
89 
90 	// FIXME: Fixes the display of the F1 help dialog in La Croix Pan,
91 	// since it was previously incorrectly wrapping on the 's' at the end
92 	// of the 'Cursors' word. May be due to minor differences in width calcs
93 	if (padding == 3 && ConfMan.get("gameid") == "lacroixpan")
94 		padding = 0;
95 
96 	// WORKAROUND: Guard Duty specifies a wii of 100,000, which is larger
97 	// than can be supported by ScummVM's surface classes
98 	wii = MIN(wii, 8000);
99 
100 	ensure_text_valid_for_font(todis, usingfont);
101 	break_up_text_into_lines(todis, _GP(Lines), wii - 2 * padding, usingfont);
102 	disp.lineheight = getfontheight_outlined(usingfont);
103 	disp.linespacing = getfontspacing_outlined(usingfont);
104 	disp.fulltxtheight = getheightoflines(usingfont, _GP(Lines).Count());
105 
106 	// AGS 2.x: If the screen is faded out, fade in again when displaying a message box.
107 	if (!asspch && (_G(loaded_game_file_version) <= kGameVersion_272))
108 		_GP(play).screen_is_faded_out = 0;
109 
110 	// if it's a normal message box and the game was being skipped,
111 	// ensure that the screen is up to date before the message box
112 	// is drawn on top of it
113 	// TODO: is this really necessary anymore?
114 	if ((_GP(play).skip_until_char_stops >= 0) && (disp_type == DISPLAYTEXT_MESSAGEBOX))
115 		render_graphics();
116 
117 	EndSkippingUntilCharStops();
118 
119 	if (_GP(topBar).wantIt) {
120 		// ensure that the window is wide enough to display
121 		// any top bar text
122 		int topBarWid = wgettextwidth_compensate(_GP(topBar).text, _GP(topBar).font);
123 		topBarWid += data_to_game_coord(_GP(play).top_bar_borderwidth + 2) * 2;
124 		if (_G(longestline) < topBarWid)
125 			_G(longestline) = topBarWid;
126 		// the top bar should behave like DisplaySpeech wrt blocking
127 		disp_type = DISPLAYTEXT_SPEECH;
128 	}
129 
130 	if (asspch > 0) {
131 		// update the all_buttons_disabled variable in advance
132 		// of the adjust_x/y_for_guis calls
133 		_GP(play).disabled_user_interface++;
134 		update_gui_disabled_status();
135 		_GP(play).disabled_user_interface--;
136 	}
137 
138 	const Rect &ui_view = _GP(play).GetUIViewport();
139 	if (xx == OVR_AUTOPLACE);
140 	// centre text in middle of screen
141 	else if (yy < 0) yy = ui_view.GetHeight() / 2 - disp.fulltxtheight / 2 - padding;
142 	// speech, so it wants to be above the character's head
143 	else if (asspch > 0) {
144 		yy -= disp.fulltxtheight;
145 		if (yy < 5) yy = 5;
146 		yy = adjust_y_for_guis(yy);
147 	}
148 
149 	if (_G(longestline) < wii - paddingDoubledScaled) {
150 		// shrink the width of the dialog box to fit the text
151 		int oldWid = wii;
152 		//if ((asspch >= 0) || (allowShrink > 0))
153 		// If it's not speech, or a shrink is allowed, then shrink it
154 		if ((asspch == 0) || (allowShrink > 0))
155 			wii = _G(longestline) + paddingDoubledScaled;
156 
157 		// shift the dialog box right to align it, if necessary
158 		if ((allowShrink == 2) && (xx >= 0))
159 			xx += (oldWid - wii);
160 	}
161 
162 	if (xx < -1) {
163 		xx = (-xx) - wii / 2;
164 		if (xx < 0)
165 			xx = 0;
166 
167 		xx = adjust_x_for_guis(xx, yy);
168 
169 		if (xx + wii >= ui_view.GetWidth())
170 			xx = (ui_view.GetWidth() - wii) - 5;
171 	} else if (xx < 0) xx = ui_view.GetWidth() / 2 - wii / 2;
172 
173 	int extraHeight = paddingDoubledScaled;
174 	color_t text_color = MakeColor(15);
175 	if (disp_type < DISPLAYTEXT_NORMALOVERLAY)
176 		remove_screen_overlay(_GP(play).text_overlay_on); // remove any previous blocking texts
177 
178 	const int bmp_width = std::max(2, wii);
179 	const int bmp_height = std::max(2, disp.fulltxtheight + extraHeight);
180 	Bitmap *text_window_ds = BitmapHelper::CreateTransparentBitmap(
181 		bmp_width, bmp_height, _GP(game).GetColorDepth());
182 
183 	// inform draw_text_window to free the old bitmap
184 	const bool wantFreeScreenop = true;
185 
186 	//
187 	// Creating displayed graphic
188 	//
189 	// may later change if usingGUI, needed to avoid changing original coordinates
190 	int adjustedXX = xx;
191 	int adjustedYY = yy;
192 
193 	if ((strlen(todis) < 1) || (strcmp(todis, "  ") == 0) || (wii == 0));
194 	// if it's an empty speech line, don't draw anything
195 	else if (asspch) { //text_color = ds->GetCompatibleColor(12);
196 		int ttxleft = 0, ttxtop = paddingScaled, oriwid = wii - padding * 2;
197 		int drawBackground = 0;
198 
199 		if (use_speech_textwindow) {
200 			drawBackground = 1;
201 		} else if (use_thought_gui) {
202 			// make it treat it as drawing inside a window now
203 			if (asspch > 0)
204 				asspch = -asspch;
205 			drawBackground = 1;
206 		}
207 
208 		if (drawBackground) {
209 			draw_text_window_and_bar(&text_window_ds, wantFreeScreenop, &ttxleft, &ttxtop, &adjustedXX, &adjustedYY, &wii, &text_color, 0, usingGui);
210 			if (usingGui > 0) {
211 				alphaChannel = _GP(guis)[usingGui].HasAlphaChannel();
212 			}
213 		} else if ((ShouldAntiAliasText()) && (_GP(game).GetColorDepth() >= 24))
214 			alphaChannel = true;
215 
216 		for (size_t ee = 0; ee < _GP(Lines).Count(); ee++) {
217 			//int ttxp=wii/2 - wgettextwidth_compensate(lines[ee], usingfont)/2;
218 			int ttyp = ttxtop + ee * disp.linespacing;
219 			// asspch < 0 means that it's inside a text box so don't
220 			// centre the text
221 			if (asspch < 0) {
222 				if ((usingGui >= 0) &&
223 				        ((_GP(game).options[OPT_SPEECHTYPE] >= 2) || (isThought)))
224 					text_color = text_window_ds->GetCompatibleColor(_GP(guis)[usingGui].FgColor);
225 				else
226 					text_color = text_window_ds->GetCompatibleColor(-asspch);
227 
228 				wouttext_aligned(text_window_ds, ttxleft, ttyp, oriwid, usingfont, text_color, _GP(Lines)[ee].GetCStr(), _GP(play).text_align);
229 			} else {
230 				text_color = text_window_ds->GetCompatibleColor(asspch);
231 				//wouttext_outline(ttxp,ttyp,usingfont,lines[ee]);
232 				wouttext_aligned(text_window_ds, ttxleft, ttyp, wii, usingfont, text_color, _GP(Lines)[ee].GetCStr(), _GP(play).speech_text_align);
233 			}
234 		}
235 	} else {
236 		int xoffs, yoffs, oriwid = wii - padding * 2;
237 		draw_text_window_and_bar(&text_window_ds, wantFreeScreenop, &xoffs, &yoffs, &adjustedXX, &adjustedYY, &wii, &text_color);
238 
239 		if (_GP(game).options[OPT_TWCUSTOM] > 0) {
240 			alphaChannel = _GP(guis)[_GP(game).options[OPT_TWCUSTOM]].HasAlphaChannel();
241 		}
242 
243 		adjust_y_coordinate_for_text(&yoffs, usingfont);
244 
245 		for (size_t ee = 0; ee < _GP(Lines).Count(); ee++)
246 			wouttext_aligned(text_window_ds, xoffs, yoffs + ee * disp.linespacing, oriwid, usingfont, text_color, _GP(Lines)[ee].GetCStr(), _GP(play).text_align);
247 	}
248 
249 	int ovrtype;
250 	switch (disp_type) {
251 	case DISPLAYTEXT_SPEECH: ovrtype = OVER_TEXTSPEECH; break;
252 	case DISPLAYTEXT_MESSAGEBOX: ovrtype = OVER_TEXTMSG; break;
253 	case DISPLAYTEXT_NORMALOVERLAY: ovrtype = OVER_CUSTOM; break;
254 	default: ovrtype = disp_type; break; // must be precreated overlay id
255 	}
256 
257 	int nse = add_screen_overlay(xx, yy, ovrtype, text_window_ds, adjustedXX - xx, adjustedYY - yy, alphaChannel);
258 	// we should not delete text_window_ds here, because it is now owned by Overlay
259 
260 	if (disp_type >= DISPLAYTEXT_NORMALOVERLAY) {
261 		return _G(screenover)[nse].type;
262 	}
263 
264 	//
265 	// Wait for the blocking text to timeout or until skipped by another command
266 	//
267 	if (disp_type == DISPLAYTEXT_MESSAGEBOX) {
268 		// If fast-forwarding, then skip immediately
269 		if (_GP(play).fast_forward) {
270 			remove_screen_overlay(OVER_TEXTMSG);
271 			_GP(play).SetWaitSkipResult(SKIP_AUTOTIMER);
272 			_GP(play).messagetime = -1;
273 			return 0;
274 		}
275 
276 		if (!_GP(play).mouse_cursor_hidden)
277 			ags_domouse(DOMOUSE_ENABLE);
278 		int countdown = GetTextDisplayTime(todis);
279 		int skip_setting = user_to_internal_skip_speech((SkipSpeechStyle)_GP(play).skip_display);
280 		// Loop until skipped
281 		while (true) {
282 			if (SHOULD_QUIT)
283 				return 0;
284 
285 			sys_evt_process_pending();
286 
287 			update_audio_system_on_game_loop();
288 			render_graphics();
289 			int mbut, mwheelz;
290 			if (run_service_mb_controls(mbut, mwheelz) && mbut >= 0) {
291 				check_skip_cutscene_mclick(mbut);
292 				if (_GP(play).fast_forward)
293 					break;
294 				if (skip_setting & SKIP_MOUSECLICK && !_GP(play).IsIgnoringInput()) {
295 					_GP(play).SetWaitSkipResult(SKIP_MOUSECLICK, mbut);
296 					break;
297 				}
298 			}
299 			KeyInput kp;
300 			if (run_service_key_controls(kp)) {
301 				check_skip_cutscene_keypress(kp.Key);
302 				if (_GP(play).fast_forward)
303 					break;
304 				if ((skip_setting & SKIP_KEYPRESS) && !_GP(play).IsIgnoringInput()) {
305 					_GP(play).SetWaitSkipResult(SKIP_KEYPRESS, kp.Key);
306 					break;
307 				}
308 			}
309 
310 			update_polled_stuff_if_runtime();
311 
312 			if (_GP(play).fast_forward == 0) {
313 				WaitForNextFrame();
314 			}
315 
316 			countdown--;
317 
318 			// Special behavior when coupled with a voice-over
319 			if (_GP(play).speech_has_voice) {
320 				// extend life of text if the voice hasn't finished yet
321 				if (channel_is_playing(SCHAN_SPEECH) && (_GP(play).fast_forward == 0)) {
322 					if (countdown <= 1)
323 						countdown = 1;
324 				} else  // if the voice has finished, remove the speech
325 					countdown = 0;
326 			}
327 			// Test for the timed auto-skip
328 			if ((countdown < 1) && (skip_setting & SKIP_AUTOTIMER)) {
329 				_GP(play).SetWaitSkipResult(SKIP_AUTOTIMER);
330 				_GP(play).SetIgnoreInput(_GP(play).ignore_user_input_after_text_timeout_ms);
331 				break;
332 			}
333 			// if skipping cutscene, don't get stuck on No Auto Remove text boxes
334 			if ((countdown < 1) && (_GP(play).fast_forward))
335 				break;
336 		}
337 		if (!_GP(play).mouse_cursor_hidden)
338 			ags_domouse(DOMOUSE_DISABLE);
339 		remove_screen_overlay(OVER_TEXTMSG);
340 		invalidate_screen();
341 	} else {
342 		/* DISPLAYTEXT_SPEECH */
343 		// if the speech does not time out, but we are skipping a cutscene,
344 		// allow it to time out
345 		if ((_GP(play).messagetime < 0) && (_GP(play).fast_forward))
346 			_GP(play).messagetime = 2;
347 
348 		if (!overlayPositionFixed) {
349 			_G(screenover)[nse].positionRelativeToScreen = false;
350 			VpPoint vpt = _GP(play).GetRoomViewport(0)->ScreenToRoom(_G(screenover)[nse].x, _G(screenover)[nse].y, false);
351 			_G(screenover)[nse].x = vpt.first.X;
352 			_G(screenover)[nse].y = vpt.first.Y;
353 		}
354 
355 		GameLoopUntilNoOverlay();
356 	}
357 
358 	_GP(play).messagetime = -1;
359 	return 0;
360 }
361 
_display_at(int xx,int yy,int wii,const char * text,int disp_type,int asspch,int isThought,int allowShrink,bool overlayPositionFixed)362 void _display_at(int xx, int yy, int wii, const char *text, int disp_type, int asspch, int isThought, int allowShrink, bool overlayPositionFixed) {
363 	int usingfont = FONT_NORMAL;
364 	if (asspch) usingfont = FONT_SPEECH;
365 	// TODO: _display_at may be called from _displayspeech, which can start
366 	// and finalize voice speech on its own. Find out if we really need to
367 	// keep track of this and not just stop voice regardless.
368 	bool need_stop_speech = false;
369 
370 	EndSkippingUntilCharStops();
371 
372 	if (try_auto_play_speech(text, text, _GP(play).narrator_speech, true)) {// TODO: is there any need for this flag?
373 		need_stop_speech = true;
374 	}
375 	_display_main(xx, yy, wii, text, disp_type, usingfont, asspch, isThought, allowShrink, overlayPositionFixed);
376 
377 	if (need_stop_speech)
378 		stop_voice_speech();
379 }
380 
try_auto_play_speech(const char * text,const char * & replace_text,int charid,bool blocking)381 bool try_auto_play_speech(const char *text, const char *&replace_text, int charid, bool blocking) {
382 	const char *src = text;
383 	if (src[0] != '&')
384 		return false;
385 
386 	int sndid = atoi(&src[1]);
387 	while ((src[0] != ' ') & (src[0] != 0)) src++;
388 	if (src[0] == ' ') src++;
389 	if (sndid <= 0)
390 		quit("DisplaySpeech: auto-voice symbol '&' not followed by valid integer");
391 
392 	replace_text = src; // skip voice tag
393 	if (play_voice_speech(charid, sndid)) {
394 		// if Voice Only, then blank out the text
395 		if (_GP(play).want_speech == 2)
396 			replace_text = "  ";
397 		return true;
398 	}
399 	return false;
400 }
401 
GetTextDisplayLength(const char * text)402 int GetTextDisplayLength(const char *text) {
403 	int len = (int)strlen(text);
404 	if ((text[0] == '&') && (_GP(play).unfactor_speech_from_textlength != 0)) {
405 		// if there's an "&12 text" type line, remove "&12 " from the source length
406 		size_t j = 0;
407 		while ((text[j] != ' ') && (text[j] != 0))
408 			j++;
409 		j++;
410 		len -= j;
411 	}
412 	return len;
413 }
414 
GetTextDisplayTime(const char * text,int canberel)415 int GetTextDisplayTime(const char *text, int canberel) {
416 	int uselen = 0;
417 	auto fpstimer = ::lround(get_current_fps());
418 
419 	// if it's background speech, make it stay relative to game speed
420 	if ((canberel == 1) && (_GP(play).bgspeech_game_speed == 1))
421 		fpstimer = 40;
422 
423 	if (_G(source_text_length) >= 0) {
424 		// sync to length of original text, to make sure any animations
425 		// and music sync up correctly
426 		uselen = _G(source_text_length);
427 		_G(source_text_length) = -1;
428 	} else {
429 		uselen = GetTextDisplayLength(text);
430 	}
431 
432 	if (uselen <= 0)
433 		return 0;
434 
435 	if (_GP(play).text_speed + _GP(play).text_speed_modifier <= 0)
436 		quit("!Text speed is zero; unable to display text. Check your _GP(game).text_speed settings.");
437 
438 	// Store how many game loops per character of text
439 	// This is calculated using a hard-coded 15 for the text speed,
440 	// so that it's always the same no matter how fast the user
441 	// can read.
442 	_G(loops_per_character) = (((uselen / _GP(play).lipsync_speed) + 1) * fpstimer) / uselen;
443 
444 	int textDisplayTimeInMS = ((uselen / (_GP(play).text_speed + _GP(play).text_speed_modifier)) + 1) * 1000;
445 	if (textDisplayTimeInMS < _GP(play).text_min_display_time_ms)
446 		textDisplayTimeInMS = _GP(play).text_min_display_time_ms;
447 
448 	return (textDisplayTimeInMS * fpstimer) / 1000;
449 }
450 
ShouldAntiAliasText()451 bool ShouldAntiAliasText() {
452 	return (_GP(game).options[OPT_ANTIALIASFONTS] != 0 || ::AGS::g_vm->_forceTextAA);
453 }
454 
wouttext_outline(Shared::Bitmap * ds,int xxp,int yyp,int usingfont,color_t text_color,const char * texx)455 void wouttext_outline(Shared::Bitmap *ds, int xxp, int yyp, int usingfont, color_t text_color, const char *texx) {
456 	color_t outline_color = ds->GetCompatibleColor(_GP(play).speech_text_shadow);
457 	if (get_font_outline(usingfont) >= 0) {
458 		// MACPORT FIX 9/6/5: cast
459 		wouttextxy(ds, xxp, yyp, (int)get_font_outline(usingfont), outline_color, texx);
460 	} else if (get_font_outline(usingfont) == FONT_OUTLINE_AUTO) {
461 		int outlineDist = 1;
462 
463 		if (is_bitmap_font(usingfont) && get_font_scaling_mul(usingfont) > 1) {
464 			// if it's a scaled up bitmap font, move the outline out more
465 			outlineDist = get_fixed_pixel_size(1);
466 		}
467 
468 		// move the text over so that it's still within the bounding rect
469 		xxp += outlineDist;
470 		yyp += outlineDist;
471 
472 		wouttextxy(ds, xxp - outlineDist, yyp, usingfont, outline_color, texx);
473 		wouttextxy(ds, xxp + outlineDist, yyp, usingfont, outline_color, texx);
474 		wouttextxy(ds, xxp, yyp + outlineDist, usingfont, outline_color, texx);
475 		wouttextxy(ds, xxp, yyp - outlineDist, usingfont, outline_color, texx);
476 		wouttextxy(ds, xxp - outlineDist, yyp - outlineDist, usingfont, outline_color, texx);
477 		wouttextxy(ds, xxp - outlineDist, yyp + outlineDist, usingfont, outline_color, texx);
478 		wouttextxy(ds, xxp + outlineDist, yyp + outlineDist, usingfont, outline_color, texx);
479 		wouttextxy(ds, xxp + outlineDist, yyp - outlineDist, usingfont, outline_color, texx);
480 	}
481 
482 	wouttextxy(ds, xxp, yyp, usingfont, text_color, texx);
483 }
484 
wouttext_aligned(Bitmap * ds,int usexp,int yy,int oriwid,int usingfont,color_t text_color,const char * text,HorAlignment align)485 void wouttext_aligned(Bitmap *ds, int usexp, int yy, int oriwid, int usingfont, color_t text_color, const char *text, HorAlignment align) {
486 
487 	if (align & kMAlignHCenter)
488 		usexp = usexp + (oriwid / 2) - (wgettextwidth_compensate(text, usingfont) / 2);
489 	else if (align & kMAlignRight)
490 		usexp = usexp + (oriwid - wgettextwidth_compensate(text, usingfont));
491 
492 	wouttext_outline(ds, usexp, yy, usingfont, text_color, text);
493 }
494 
get_outline_adjustment(int font)495 int get_outline_adjustment(int font) {
496 	// automatic outline fonts are 2 pixels taller
497 	if (get_font_outline(font) == FONT_OUTLINE_AUTO) {
498 		// scaled up bitmap font, push outline further out
499 		if (is_bitmap_font(font) && get_font_scaling_mul(font) > 1)
500 			return get_fixed_pixel_size(2);
501 		// otherwise, just push outline by 1 pixel
502 		else
503 			return 2;
504 	}
505 	return 0;
506 }
507 
getfontheight_outlined(int font)508 int getfontheight_outlined(int font) {
509 	return getfontheight(font) + get_outline_adjustment(font);
510 }
511 
getfontspacing_outlined(int font)512 int getfontspacing_outlined(int font) {
513 	return use_default_linespacing(font) ?
514 	       getfontheight_outlined(font) :
515 	       getfontlinespacing(font);
516 }
517 
getfontlinegap(int font)518 int getfontlinegap(int font) {
519 	return getfontspacing_outlined(font) - getfontheight_outlined(font);
520 }
521 
getheightoflines(int font,int numlines)522 int getheightoflines(int font, int numlines) {
523 	return getfontspacing_outlined(font) * (numlines - 1) + getfontheight_outlined(font);
524 }
525 
wgettextwidth_compensate(const char * tex,int font)526 int wgettextwidth_compensate(const char *tex, int font) {
527 	int wdof = wgettextwidth(tex, font);
528 
529 	if (get_font_outline(font) == FONT_OUTLINE_AUTO) {
530 		// scaled up SCI font, push outline further out
531 		if (is_bitmap_font(font) && get_font_scaling_mul(font) > 1)
532 			wdof += get_fixed_pixel_size(2);
533 		// otherwise, just push outline by 1 pixel
534 		else
535 			wdof += get_fixed_pixel_size(1);
536 	}
537 
538 	return wdof;
539 }
540 
do_corner(Bitmap * ds,int sprn,int x,int y,int offx,int offy)541 void do_corner(Bitmap *ds, int sprn, int x, int y, int offx, int offy) {
542 	if (sprn < 0) return;
543 	if (_GP(spriteset)[sprn] == nullptr) {
544 		sprn = 0;
545 	}
546 
547 	x = x + offx * _GP(game).SpriteInfos[sprn].Width;
548 	y = y + offy * _GP(game).SpriteInfos[sprn].Height;
549 	draw_gui_sprite_v330(ds, sprn, x, y);
550 }
551 
get_but_pic(GUIMain * guo,int indx)552 int get_but_pic(GUIMain *guo, int indx) {
553 	int butid = guo->GetControlID(indx);
554 	return butid >= 0 ? _GP(guibuts)[butid].Image : 0;
555 }
556 
draw_button_background(Bitmap * ds,int xx1,int yy1,int xx2,int yy2,GUIMain * iep)557 void draw_button_background(Bitmap *ds, int xx1, int yy1, int xx2, int yy2, GUIMain *iep) {
558 	color_t draw_color;
559 	if (iep == nullptr) {  // standard window
560 		draw_color = ds->GetCompatibleColor(15);
561 		ds->FillRect(Rect(xx1, yy1, xx2, yy2), draw_color);
562 		draw_color = ds->GetCompatibleColor(16);
563 		ds->DrawRect(Rect(xx1, yy1, xx2, yy2), draw_color);
564 		/*    draw_color = ds->GetCompatibleColor(opts.tws.backcol); ds->FillRect(Rect(xx1,yy1,xx2,yy2);
565 		draw_color = ds->GetCompatibleColor(opts.tws.ds->GetTextColor()); ds->DrawRect(Rect(xx1+1,yy1+1,xx2-1,yy2-1);*/
566 	} else {
567 		if (_G(loaded_game_file_version) < kGameVersion_262) { // < 2.62
568 			// Color 0 wrongly shows as transparent instead of black
569 			// From the changelog of 2.62:
570 			//  - Fixed text windows getting a black background if colour 0 was
571 			//    specified, rather than being transparent.
572 			if (iep->BgColor == 0)
573 				iep->BgColor = 16;
574 		}
575 
576 		if (iep->BgColor >= 0) draw_color = ds->GetCompatibleColor(iep->BgColor);
577 		else draw_color = ds->GetCompatibleColor(0); // black backrgnd behind picture
578 
579 		if (iep->BgColor > 0)
580 			ds->FillRect(Rect(xx1, yy1, xx2, yy2), draw_color);
581 
582 		int leftRightWidth = _GP(game).SpriteInfos[get_but_pic(iep, 4)].Width;
583 		int topBottomHeight = _GP(game).SpriteInfos[get_but_pic(iep, 6)].Height;
584 		if (iep->BgImage > 0) {
585 			if ((_G(loaded_game_file_version) <= kGameVersion_272) // 2.xx
586 			        && (_GP(spriteset)[iep->BgImage]->GetWidth() == 1)
587 			        && (_GP(spriteset)[iep->BgImage]->GetHeight() == 1)
588 			        && (*((const unsigned int *)_GP(spriteset)[iep->BgImage]->GetData()) == 0x00FF00FF)) {
589 				// Don't draw fully transparent dummy GUI backgrounds
590 			} else {
591 				// offset the background image and clip it so that it is drawn
592 				// such that the border graphics can have a transparent outside
593 				// edge
594 				int bgoffsx = xx1 - leftRightWidth / 2;
595 				int bgoffsy = yy1 - topBottomHeight / 2;
596 				ds->SetClip(Rect(bgoffsx, bgoffsy, xx2 + leftRightWidth / 2, yy2 + topBottomHeight / 2));
597 				int bgfinishx = xx2;
598 				int bgfinishy = yy2;
599 				int bgoffsyStart = bgoffsy;
600 				while (bgoffsx <= bgfinishx) {
601 					bgoffsy = bgoffsyStart;
602 					while (bgoffsy <= bgfinishy) {
603 						draw_gui_sprite_v330(ds, iep->BgImage, bgoffsx, bgoffsy);
604 						bgoffsy += _GP(game).SpriteInfos[iep->BgImage].Height;
605 					}
606 					bgoffsx += _GP(game).SpriteInfos[iep->BgImage].Width;
607 				}
608 				// return to normal clipping rectangle
609 				ds->ResetClip();
610 			}
611 		}
612 		int uu;
613 		for (uu = yy1; uu <= yy2; uu += _GP(game).SpriteInfos[get_but_pic(iep, 4)].Height) {
614 			do_corner(ds, get_but_pic(iep, 4), xx1, uu, -1, 0);   // left side
615 			do_corner(ds, get_but_pic(iep, 5), xx2 + 1, uu, 0, 0);  // right side
616 		}
617 		for (uu = xx1; uu <= xx2; uu += _GP(game).SpriteInfos[get_but_pic(iep, 6)].Width) {
618 			do_corner(ds, get_but_pic(iep, 6), uu, yy1, 0, -1);  // top side
619 			do_corner(ds, get_but_pic(iep, 7), uu, yy2 + 1, 0, 0); // bottom side
620 		}
621 		do_corner(ds, get_but_pic(iep, 0), xx1, yy1, -1, -1);  // top left
622 		do_corner(ds, get_but_pic(iep, 1), xx1, yy2 + 1, -1, 0);  // bottom left
623 		do_corner(ds, get_but_pic(iep, 2), xx2 + 1, yy1, 0, -1);  //  top right
624 		do_corner(ds, get_but_pic(iep, 3), xx2 + 1, yy2 + 1, 0, 0);  // bottom right
625 	}
626 }
627 
628 // Calculate the width that the left and right border of the textwindow
629 // GUI take up
get_textwindow_border_width(int twgui)630 int get_textwindow_border_width(int twgui) {
631 	if (twgui < 0)
632 		return 0;
633 
634 	if (!_GP(guis)[twgui].IsTextWindow())
635 		quit("!GUI set as text window but is not actually a text window GUI");
636 
637 	int borwid = _GP(game).SpriteInfos[get_but_pic(&_GP(guis)[twgui], 4)].Width +
638 	             _GP(game).SpriteInfos[get_but_pic(&_GP(guis)[twgui], 5)].Width;
639 
640 	return borwid;
641 }
642 
643 // get the hegiht of the text window's top border
get_textwindow_top_border_height(int twgui)644 int get_textwindow_top_border_height(int twgui) {
645 	if (twgui < 0)
646 		return 0;
647 
648 	if (!_GP(guis)[twgui].IsTextWindow())
649 		quit("!GUI set as text window but is not actually a text window GUI");
650 
651 	return _GP(game).SpriteInfos[get_but_pic(&_GP(guis)[twgui], 6)].Height;
652 }
653 
654 // Get the padding for a text window
655 // -1 for the game's custom text window
get_textwindow_padding(int ifnum)656 int get_textwindow_padding(int ifnum) {
657 	int result;
658 
659 	if (ifnum < 0)
660 		ifnum = _GP(game).options[OPT_TWCUSTOM];
661 	if (ifnum > 0 && ifnum < _GP(game).numgui)
662 		result = _GP(guis)[ifnum].Padding;
663 	else
664 		result = TEXTWINDOW_PADDING_DEFAULT;
665 
666 	return result;
667 }
668 
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)669 void draw_text_window(Bitmap **text_window_ds, bool should_free_ds,
670                       int *xins, int *yins, int *xx, int *yy, int *wii, color_t *set_text_color, int ovrheight, int ifnum) {
671 
672 	Bitmap *ds = *text_window_ds;
673 	if (ifnum < 0)
674 		ifnum = _GP(game).options[OPT_TWCUSTOM];
675 
676 	if (ifnum <= 0) {
677 		if (ovrheight)
678 			quit("!Cannot use QFG4 style options without custom text window");
679 		draw_button_background(ds, 0, 0, ds->GetWidth() - 1, ds->GetHeight() - 1, nullptr);
680 		if (set_text_color)
681 			*set_text_color = ds->GetCompatibleColor(16);
682 		xins[0] = 3;
683 		yins[0] = 3;
684 	} else {
685 		if (ifnum >= _GP(game).numgui)
686 			quitprintf("!Invalid GUI %d specified as text window (total GUIs: %d)", ifnum, _GP(game).numgui);
687 		if (!_GP(guis)[ifnum].IsTextWindow())
688 			quit("!GUI set as text window but is not actually a text window GUI");
689 
690 		int tbnum = get_but_pic(&_GP(guis)[ifnum], 0);
691 
692 		wii[0] += get_textwindow_border_width(ifnum);
693 		xx[0] -= _GP(game).SpriteInfos[tbnum].Width;
694 		yy[0] -= _GP(game).SpriteInfos[tbnum].Height;
695 		if (ovrheight == 0)
696 			ovrheight = disp.fulltxtheight;
697 
698 		if (should_free_ds)
699 			delete *text_window_ds;
700 		int padding = get_textwindow_padding(ifnum);
701 		*text_window_ds = BitmapHelper::CreateTransparentBitmap(wii[0], ovrheight + (padding * 2) + _GP(game).SpriteInfos[tbnum].Height * 2, _GP(game).GetColorDepth());
702 		ds = *text_window_ds;
703 		int xoffs = _GP(game).SpriteInfos[tbnum].Width, yoffs = _GP(game).SpriteInfos[tbnum].Height;
704 		draw_button_background(ds, xoffs, yoffs, (ds->GetWidth() - xoffs) - 1, (ds->GetHeight() - yoffs) - 1, &_GP(guis)[ifnum]);
705 		if (set_text_color)
706 			*set_text_color = ds->GetCompatibleColor(_GP(guis)[ifnum].FgColor);
707 		xins[0] = xoffs + padding;
708 		yins[0] = yoffs + padding;
709 	}
710 }
711 
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)712 void draw_text_window_and_bar(Bitmap **text_window_ds, bool should_free_ds,
713                               int *xins, int *yins, int *xx, int *yy, int *wii, color_t *set_text_color, int ovrheight, int ifnum) {
714 
715 	draw_text_window(text_window_ds, should_free_ds, xins, yins, xx, yy, wii, set_text_color, ovrheight, ifnum);
716 
717 	if ((_GP(topBar).wantIt) && (text_window_ds && *text_window_ds)) {
718 		// top bar on the dialog window with character's name
719 		// create an enlarged window, then free the old one
720 		Bitmap *ds = *text_window_ds;
721 		Bitmap *newScreenop = BitmapHelper::CreateBitmap(ds->GetWidth(), ds->GetHeight() + _GP(topBar).height, _GP(game).GetColorDepth());
722 		newScreenop->Blit(ds, 0, 0, 0, _GP(topBar).height, ds->GetWidth(), ds->GetHeight());
723 		delete *text_window_ds;
724 		*text_window_ds = newScreenop;
725 		ds = *text_window_ds;
726 
727 		// draw the top bar
728 		color_t draw_color = ds->GetCompatibleColor(_GP(play).top_bar_backcolor);
729 		ds->FillRect(Rect(0, 0, ds->GetWidth() - 1, _GP(topBar).height - 1), draw_color);
730 		if (_GP(play).top_bar_backcolor != _GP(play).top_bar_bordercolor) {
731 			// draw the border
732 			draw_color = ds->GetCompatibleColor(_GP(play).top_bar_bordercolor);
733 			for (int j = 0; j < data_to_game_coord(_GP(play).top_bar_borderwidth); j++)
734 				ds->DrawRect(Rect(j, j, ds->GetWidth() - (j + 1), _GP(topBar).height - (j + 1)), draw_color);
735 		}
736 
737 		// draw the text
738 		int textx = (ds->GetWidth() / 2) - wgettextwidth_compensate(_GP(topBar).text, _GP(topBar).font) / 2;
739 		color_t text_color = ds->GetCompatibleColor(_GP(play).top_bar_textcolor);
740 		wouttext_outline(ds, textx, _GP(play).top_bar_borderwidth + get_fixed_pixel_size(1), _GP(topBar).font, text_color, _GP(topBar).text);
741 
742 		// don't draw it next time
743 		_GP(topBar).wantIt = 0;
744 		// adjust the text Y position
745 		yins[0] += _GP(topBar).height;
746 	} else if (_GP(topBar).wantIt)
747 		_GP(topBar).wantIt = 0;
748 }
749 
750 } // namespace AGS3
751