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
|