/* 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 . * */ #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(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((((static_cast(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(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(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