scummvm/engines/ags/engine/ac/dialog.cpp
Walter Agazzi c366ce5eef AGS: Removed update_polled_stuff_if_runtime() from data loading functions
Leave it with a new name as `update_polled_stuff()` only in functions
related to the game update.
From upstream 95dc139f51e704da19792e5480e69bbfe7ec27aa
2023-03-12 20:58:28 +00:00

1245 lines
40 KiB
C++

/* 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/stack.h"
#include "ags/engine/ac/dialog.h"
#include "ags/shared/ac/common.h"
#include "ags/engine/ac/character.h"
#include "ags/shared/ac/character_info.h"
#include "ags/shared/ac/dialog_topic.h"
#include "ags/engine/ac/display.h"
#include "ags/engine/ac/draw.h"
#include "ags/engine/ac/game_state.h"
#include "ags/shared/ac/game_setup_struct.h"
#include "ags/engine/ac/global_character.h"
#include "ags/engine/ac/global_dialog.h"
#include "ags/engine/ac/global_display.h"
#include "ags/engine/ac/global_game.h"
#include "ags/engine/ac/global_gui.h"
#include "ags/engine/ac/global_room.h"
#include "ags/engine/ac/global_translation.h"
#include "ags/shared/ac/keycode.h"
#include "ags/engine/ac/overlay.h"
#include "ags/engine/ac/mouse.h"
#include "ags/engine/ac/parser.h"
#include "ags/engine/ac/sys_events.h"
#include "ags/engine/ac/string.h"
#include "ags/engine/ac/dynobj/script_dialog_options_rendering.h"
#include "ags/engine/ac/dynobj/script_drawing_surface.h"
#include "ags/engine/ac/system.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/shared/font/fonts.h"
#include "ags/engine/script/cc_instance.h"
#include "ags/shared/gui/gui_main.h"
#include "ags/shared/gui/gui_textbox.h"
#include "ags/engine/main/game_run.h"
#include "ags/engine/platform/base/ags_platform_driver.h"
#include "ags/engine/script/script.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/engine/gfx/ddb.h"
#include "ags/engine/gfx/gfx_util.h"
#include "ags/engine/gfx/graphics_driver.h"
#include "ags/engine/ac/mouse.h"
#include "ags/engine/media/audio/audio_system.h"
#include "ags/shared/debugging/out.h"
#include "ags/engine/script/script_api.h"
#include "ags/engine/script/script_runtime.h"
#include "ags/engine/ac/dynobj/script_string.h"
#include "ags/ags.h"
#include "ags/globals.h"
namespace AGS3 {
using namespace AGS::Shared;
void Dialog_Start(ScriptDialog *sd) {
RunDialog(sd->id);
}
#define CHOSE_TEXTPARSER -3053
#define SAYCHOSEN_USEFLAG 1
#define SAYCHOSEN_YES 2
#define SAYCHOSEN_NO 3
int Dialog_DisplayOptions(ScriptDialog *sd, int sayChosenOption) {
if ((sayChosenOption < 1) || (sayChosenOption > 3))
quit("!Dialog.DisplayOptions: invalid parameter passed");
int chose = show_dialog_options(sd->id, sayChosenOption, (_GP(game).options[OPT_RUNGAMEDLGOPTS] != 0));
if (SHOULD_QUIT)
return -1;
if (chose != CHOSE_TEXTPARSER) {
chose++;
}
return chose;
}
void Dialog_SetOptionState(ScriptDialog *sd, int option, int newState) {
SetDialogOption(sd->id, option, newState);
}
int Dialog_GetOptionState(ScriptDialog *sd, int option) {
return GetDialogOption(sd->id, option);
}
int Dialog_HasOptionBeenChosen(ScriptDialog *sd, int option) {
if ((option < 1) || (option > _G(dialog)[sd->id].numoptions))
quit("!Dialog.HasOptionBeenChosen: Invalid option number specified");
option--;
if (_G(dialog)[sd->id].optionflags[option] & DFLG_HASBEENCHOSEN)
return 1;
return 0;
}
void Dialog_SetHasOptionBeenChosen(ScriptDialog *sd, int option, bool chosen) {
if (option < 1 || option > _G(dialog)[sd->id].numoptions) {
quit("!Dialog.HasOptionBeenChosen: Invalid option number specified");
}
option--;
if (chosen) {
_G(dialog)[sd->id].optionflags[option] |= DFLG_HASBEENCHOSEN;
} else {
_G(dialog)[sd->id].optionflags[option] &= ~DFLG_HASBEENCHOSEN;
}
}
int Dialog_GetOptionCount(ScriptDialog *sd) {
return _G(dialog)[sd->id].numoptions;
}
int Dialog_GetShowTextParser(ScriptDialog *sd) {
return (_G(dialog)[sd->id].topicFlags & DTFLG_SHOWPARSER) ? 1 : 0;
}
const char *Dialog_GetOptionText(ScriptDialog *sd, int option) {
if ((option < 1) || (option > _G(dialog)[sd->id].numoptions))
quit("!Dialog.GetOptionText: Invalid option number specified");
option--;
return CreateNewScriptString(get_translation(_G(dialog)[sd->id].optionnames[option]));
}
int Dialog_GetID(ScriptDialog *sd) {
return sd->id;
}
//=============================================================================
#define RUN_DIALOG_STAY -1
#define RUN_DIALOG_STOP_DIALOG -2
#define RUN_DIALOG_GOTO_PREVIOUS -4
// dialog manager stuff
void get_dialog_script_parameters(unsigned char *&script, unsigned short *param1, unsigned short *param2) {
script++;
*param1 = *script;
script++;
*param1 += *script * 256;
script++;
if (param2) {
*param2 = *script;
script++;
*param2 += *script * 256;
script++;
}
}
int run_dialog_script(int dialogID, int offse, int optionIndex) {
_G(said_speech_line) = 0;
int result = RUN_DIALOG_STAY;
if (_G(dialogScriptsInst)) {
char func_name[100];
snprintf(func_name, sizeof(func_name), "_run_dialog%d", dialogID);
RuntimeScriptValue params[]{ optionIndex };
RunScriptFunction(_G(dialogScriptsInst), func_name, 1, params);
result = _G(dialogScriptsInst)->returnValue;
} else {
// old dialog format
if (offse == -1)
return result;
unsigned char *script = _G(old_dialog_scripts)[dialogID].get() + offse;
unsigned short param1 = 0;
unsigned short param2 = 0;
bool script_running = true;
while (script_running) {
switch (*script) {
case DCMD_SAY:
get_dialog_script_parameters(script, &param1, &param2);
if (param1 == DCHAR_PLAYER)
param1 = _GP(game).playercharacter;
if (param1 == DCHAR_NARRATOR)
Display(get_translation(_G(old_speech_lines)[param2].GetCStr()));
else
DisplaySpeech(get_translation(_G(old_speech_lines)[param2].GetCStr()), param1);
_G(said_speech_line) = 1;
break;
case DCMD_OPTOFF:
get_dialog_script_parameters(script, &param1, nullptr);
SetDialogOption(dialogID, param1 + 1, 0, true);
break;
case DCMD_OPTON:
get_dialog_script_parameters(script, &param1, nullptr);
SetDialogOption(dialogID, param1 + 1, DFLG_ON, true);
break;
case DCMD_RETURN:
script_running = false;
break;
case DCMD_STOPDIALOG:
result = RUN_DIALOG_STOP_DIALOG;
script_running = false;
break;
case DCMD_OPTOFFFOREVER:
get_dialog_script_parameters(script, &param1, nullptr);
SetDialogOption(dialogID, param1 + 1, DFLG_OFFPERM, true);
break;
case DCMD_RUNTEXTSCRIPT:
get_dialog_script_parameters(script, &param1, nullptr);
result = run_dialog_request(param1);
script_running = (result == RUN_DIALOG_STAY);
break;
case DCMD_GOTODIALOG:
get_dialog_script_parameters(script, &param1, nullptr);
result = param1;
script_running = false;
break;
case DCMD_PLAYSOUND:
get_dialog_script_parameters(script, &param1, nullptr);
play_sound(param1);
break;
case DCMD_ADDINV:
get_dialog_script_parameters(script, &param1, nullptr);
add_inventory(param1);
break;
case DCMD_SETSPCHVIEW:
get_dialog_script_parameters(script, &param1, &param2);
SetCharacterSpeechView(param1, param2);
break;
case DCMD_NEWROOM:
get_dialog_script_parameters(script, &param1, nullptr);
NewRoom(param1);
_G(in_new_room) = 1;
result = RUN_DIALOG_STOP_DIALOG;
script_running = false;
break;
case DCMD_SETGLOBALINT:
get_dialog_script_parameters(script, &param1, &param2);
SetGlobalInt(param1, param2);
break;
case DCMD_GIVESCORE:
get_dialog_script_parameters(script, &param1, nullptr);
GiveScore(param1);
break;
case DCMD_GOTOPREVIOUS:
result = RUN_DIALOG_GOTO_PREVIOUS;
script_running = false;
break;
case DCMD_LOSEINV:
get_dialog_script_parameters(script, &param1, nullptr);
lose_inventory(param1);
break;
case DCMD_ENDSCRIPT:
result = RUN_DIALOG_STOP_DIALOG;
script_running = false;
break;
}
}
}
if (_G(in_new_room) > 0 || _G(abort_engine))
return RUN_DIALOG_STOP_DIALOG;
if (_G(said_speech_line) > 0) {
// the line below fixes the problem with the close-up face remaining on the
// screen after they finish talking; however, it makes the dialog options
// area flicker when going between topics.
DisableInterface();
UpdateGameOnce(); // redraw the screen to make sure it looks right
EnableInterface();
// if we're not about to abort the dialog, switch back to arrow
if (result != RUN_DIALOG_STOP_DIALOG)
set_mouse_cursor(CURS_ARROW);
}
return result;
}
int write_dialog_options(Bitmap *ds, bool ds_has_alpha, int dlgxp, int curyp, int numdisp, int mouseison, int areawid,
int bullet_wid, int usingfont, DialogTopic *dtop, int *disporder, short *dispyp,
int linespacing, int utextcol, int padding) {
int ww;
color_t text_color;
for (ww = 0; ww < numdisp; ww++) {
if ((dtop->optionflags[(int)disporder[ww]] & DFLG_HASBEENCHOSEN) &&
(_GP(play).read_dialog_option_colour >= 0)) {
// 'read' colour
text_color = ds->GetCompatibleColor(_GP(play).read_dialog_option_colour);
} else {
// 'unread' colour
text_color = ds->GetCompatibleColor(_G(playerchar)->talkcolor);
}
if (mouseison == ww) {
if (text_color == ds->GetCompatibleColor(utextcol))
text_color = ds->GetCompatibleColor(13); // the normal colour is the same as highlight col
else text_color = ds->GetCompatibleColor(utextcol);
}
break_up_text_into_lines(get_translation(dtop->optionnames[(int)disporder[ww]]), _GP(Lines), areawid - (2 * padding + 2 + bullet_wid), usingfont);
dispyp[ww] = curyp;
if (_GP(game).dialog_bullet > 0) {
draw_gui_sprite_v330(ds, _GP(game).dialog_bullet, dlgxp, curyp, ds_has_alpha);
}
if (_GP(game).options[OPT_DIALOGNUMBERED] == kDlgOptNumbering) {
char tempbfr[20];
int actualpicwid = 0;
if (_GP(game).dialog_bullet > 0)
actualpicwid = _GP(game).SpriteInfos[_GP(game).dialog_bullet].Width + 3;
snprintf(tempbfr, sizeof(tempbfr), "%d.", ww + 1);
wouttext_outline(ds, dlgxp + actualpicwid, curyp, usingfont, text_color, tempbfr);
}
for (size_t cc = 0; cc < _GP(Lines).Count(); cc++) {
wouttext_outline(ds, dlgxp + ((cc == 0) ? 0 : 9) + bullet_wid, curyp, usingfont, text_color, _GP(Lines)[cc].GetCStr());
curyp += linespacing;
}
if (ww < numdisp - 1)
curyp += data_to_game_coord(_GP(game).options[OPT_DIALOGGAP]);
}
return curyp;
}
#define GET_OPTIONS_HEIGHT {\
needheight = 0;\
for (int i = 0; i < numdisp; ++i) {\
break_up_text_into_lines(get_translation(dtop->optionnames[(int)disporder[i]]), _GP(Lines), areawid-(2*padding+2+bullet_wid), usingfont);\
needheight += get_text_lines_surf_height(usingfont, _GP(Lines).Count()) + data_to_game_coord(_GP(game).options[OPT_DIALOGGAP]);\
}\
if (parserInput) needheight += parserInput->Height + data_to_game_coord(_GP(game).options[OPT_DIALOGGAP]);\
}
void draw_gui_for_dialog_options(Bitmap *ds, GUIMain *guib, int dlgxp, int dlgyp) {
if (guib->BgColor != 0) {
color_t draw_color = ds->GetCompatibleColor(guib->BgColor);
ds->FillRect(Rect(dlgxp, dlgyp, dlgxp + guib->Width, dlgyp + guib->Height), draw_color);
}
if (guib->BgImage > 0)
GfxUtil::DrawSpriteWithTransparency(ds, _GP(spriteset)[guib->BgImage], dlgxp, dlgyp);
}
bool get_custom_dialog_options_dimensions(int dlgnum) {
_GP(ccDialogOptionsRendering).Reset();
_GP(ccDialogOptionsRendering).dialogID = dlgnum;
_GP(getDialogOptionsDimensionsFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
run_function_on_non_blocking_thread(&_GP(getDialogOptionsDimensionsFunc));
if ((_GP(ccDialogOptionsRendering).width > 0) &&
(_GP(ccDialogOptionsRendering).height > 0)) {
return true;
}
return false;
}
#define DLG_OPTION_PARSER 99
struct DialogOptions {
int dlgnum;
bool runGameLoopsInBackground;
int dlgxp;
int dlgyp;
int dialog_abs_x; // absolute dialog position on screen
int padding;
int usingfont;
int lineheight;
int linespacing;
int curswas;
int bullet_wid;
int needheight;
IDriverDependantBitmap *ddb;
Bitmap *subBitmap;
GUITextBox *parserInput;
DialogTopic *dtop;
// display order of options
int disporder[MAXTOPICOPTIONS];
// display Y coordinate of options
short dispyp[MAXTOPICOPTIONS];
// number of displayed options
int numdisp;
// currently chosen option
int chose;
Bitmap *tempScrn;
int parserActivated;
int curyp;
bool needRedraw;
bool wantRefresh;
bool usingCustomRendering;
int orixp;
int oriyp;
int areawid;
int is_textwindow;
int dirtyx;
int dirtyy;
int dirtywidth;
int dirtyheight;
int mouseison;
int mousewason;
int forecol;
void Prepare(int _dlgnum, bool _runGameLoopsInBackground);
void Show();
void Redraw();
// Runs the dialog options update;
// returns whether should continue to run options loop, or stop
bool Run();
// Process all the buffered key events;
// returns whether should continue to run options loop, or stop
bool RunKeyControls();
// Process single key event
// returns whether should continue to run options loop, or stop
bool RunKey(const KeyInput &ki);
void Close();
};
void DialogOptions::Prepare(int _dlgnum, bool _runGameLoopsInBackground) {
dlgnum = _dlgnum;
runGameLoopsInBackground = _runGameLoopsInBackground;
dlgyp = get_fixed_pixel_size(160);
usingfont = FONT_NORMAL;
lineheight = get_font_height_outlined(usingfont);
linespacing = get_font_linespacing(usingfont);
curswas = _G(cur_cursor);
bullet_wid = 0;
ddb = nullptr;
subBitmap = nullptr;
parserInput = nullptr;
dtop = nullptr;
if ((dlgnum < 0) || (dlgnum >= _GP(game).numdialog))
quit("!RunDialog: invalid dialog number specified");
can_run_delayed_command();
_GP(play).in_conversation ++;
if (_GP(game).dialog_bullet > 0)
bullet_wid = _GP(game).SpriteInfos[_GP(game).dialog_bullet].Width + 3;
// numbered options, leave space for the numbers
if (_GP(game).options[OPT_DIALOGNUMBERED] == kDlgOptNumbering)
bullet_wid += get_text_width_outlined("9. ", usingfont);
_G(said_text) = 0;
const Rect &ui_view = _GP(play).GetUIViewport();
tempScrn = BitmapHelper::CreateBitmap(ui_view.GetWidth(), ui_view.GetHeight(), _GP(game).GetColorDepth());
set_mouse_cursor(CURS_ARROW);
dtop = &_G(dialog)[dlgnum];
chose = -1;
numdisp = 0;
parserActivated = 0;
if ((dtop->topicFlags & DTFLG_SHOWPARSER) && (_GP(play).disable_dialog_parser == 0)) {
parserInput = new GUITextBox();
parserInput->Height = lineheight + get_fixed_pixel_size(4);
parserInput->SetShowBorder(true);
parserInput->Font = usingfont;
}
numdisp = 0;
for (int i = 0; i < dtop->numoptions; ++i) {
if ((dtop->optionflags[i] & DFLG_ON) == 0) continue;
ensure_text_valid_for_font(dtop->optionnames[i], usingfont);
disporder[numdisp] = i;
numdisp++;
}
}
void DialogOptions::Show() {
if (numdisp < 1) {
debug_script_warn("Dialog: all options have been turned off, stopping dialog.");
return;
}
// Don't display the options if there is only one and the parser
// is not enabled.
if (!((numdisp > 1) || (parserInput != nullptr) || (_GP(play).show_single_dialog_option))) {
chose = disporder[0]; // only one choice, so select it
return;
}
is_textwindow = 0;
forecol = _GP(play).dialog_options_highlight_color;
mouseison = -1;
mousewason = -10;
const Rect &ui_view = _GP(play).GetUIViewport();
dirtyx = 0;
dirtyy = 0;
dirtywidth = ui_view.GetWidth();
dirtyheight = ui_view.GetHeight();
usingCustomRendering = false;
dlgxp = 1;
if (get_custom_dialog_options_dimensions(dlgnum)) {
usingCustomRendering = true;
dirtyx = data_to_game_coord(_GP(ccDialogOptionsRendering).x);
dirtyy = data_to_game_coord(_GP(ccDialogOptionsRendering).y);
dirtywidth = data_to_game_coord(_GP(ccDialogOptionsRendering).width);
dirtyheight = data_to_game_coord(_GP(ccDialogOptionsRendering).height);
dialog_abs_x = dirtyx;
} else if (_GP(game).options[OPT_DIALOGIFACE] > 0) {
GUIMain *guib = &_GP(guis)[_GP(game).options[OPT_DIALOGIFACE]];
if (guib->IsTextWindow()) {
// text-window, so do the QFG4-style speech options
is_textwindow = 1;
forecol = guib->FgColor;
} else {
dlgxp = guib->X;
dlgyp = guib->Y;
dirtyx = dlgxp;
dirtyy = dlgyp;
dirtywidth = guib->Width;
dirtyheight = guib->Height;
dialog_abs_x = guib->X;
areawid = guib->Width - 5;
padding = TEXTWINDOW_PADDING_DEFAULT;
GET_OPTIONS_HEIGHT
if (_GP(game).options[OPT_DIALOGUPWARDS]) {
// They want the options upwards from the bottom
dlgyp = (guib->Y + guib->Height) - needheight;
}
}
} else {
//dlgyp=(_GP(play).viewport.GetHeight()-numdisp*txthit)-1;
areawid = ui_view.GetWidth() - 5;
padding = TEXTWINDOW_PADDING_DEFAULT;
GET_OPTIONS_HEIGHT
dlgyp = ui_view.GetHeight() - needheight;
dirtyx = 0;
dirtyy = dlgyp - 1;
dirtywidth = ui_view.GetWidth();
dirtyheight = ui_view.GetHeight() - dirtyy;
dialog_abs_x = 0;
}
if (!is_textwindow)
areawid -= data_to_game_coord(_GP(play).dialog_options_x) * 2;
orixp = dlgxp;
oriyp = dlgyp;
needRedraw = false;
wantRefresh = false;
mouseison = -10;
Redraw();
while (Run() && !SHOULD_QUIT) {}
// Close custom dialog options
if (usingCustomRendering) {
_GP(runDialogOptionCloseFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
run_function_on_non_blocking_thread(&_GP(runDialogOptionCloseFunc));
}
}
void DialogOptions::Redraw() {
wantRefresh = true;
if (usingCustomRendering) {
tempScrn = recycle_bitmap(tempScrn, _GP(game).GetColorDepth(),
data_to_game_coord(_GP(ccDialogOptionsRendering).width),
data_to_game_coord(_GP(ccDialogOptionsRendering).height));
}
tempScrn->ClearTransparent();
Bitmap *ds = tempScrn;
dlgxp = orixp;
dlgyp = oriyp;
const Rect &ui_view = _GP(play).GetUIViewport();
bool options_surface_has_alpha = false;
if (usingCustomRendering) {
_GP(ccDialogOptionsRendering).surfaceToRenderTo = _G(dialogOptionsRenderingSurface);
_GP(ccDialogOptionsRendering).surfaceAccessed = false;
_G(dialogOptionsRenderingSurface)->linkedBitmapOnly = tempScrn;
_G(dialogOptionsRenderingSurface)->hasAlphaChannel = _GP(ccDialogOptionsRendering).hasAlphaChannel;
options_surface_has_alpha = _G(dialogOptionsRenderingSurface)->hasAlphaChannel != 0;
_GP(renderDialogOptionsFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
run_function_on_non_blocking_thread(&_GP(renderDialogOptionsFunc));
if (!_GP(ccDialogOptionsRendering).surfaceAccessed)
debug_script_warn("dialog_options_get_dimensions was implemented, but no dialog_options_render function drew anything to the surface");
if (parserInput) {
parserInput->X = data_to_game_coord(_GP(ccDialogOptionsRendering).parserTextboxX);
curyp = data_to_game_coord(_GP(ccDialogOptionsRendering).parserTextboxY);
areawid = data_to_game_coord(_GP(ccDialogOptionsRendering).parserTextboxWidth);
if (areawid == 0)
areawid = tempScrn->GetWidth();
}
_GP(ccDialogOptionsRendering).needRepaint = false;
} else if (is_textwindow) {
// text window behind the options
areawid = data_to_game_coord(_GP(play).max_dialogoption_width);
int biggest = 0;
padding = _GP(guis)[_GP(game).options[OPT_DIALOGIFACE]].Padding;
for (int i = 0; i < numdisp; ++i) {
break_up_text_into_lines(get_translation(dtop->optionnames[(int)disporder[i]]), _GP(Lines), areawid - ((2 * padding + 2) + bullet_wid), usingfont);
if (_G(longestline) > biggest)
biggest = _G(longestline);
}
if (biggest < areawid - ((2 * padding + 6) + bullet_wid))
areawid = biggest + ((2 * padding + 6) + bullet_wid);
if (areawid < data_to_game_coord(_GP(play).min_dialogoption_width)) {
areawid = data_to_game_coord(_GP(play).min_dialogoption_width);
if (_GP(play).min_dialogoption_width > _GP(play).max_dialogoption_width)
quit("!_GP(game).min_dialogoption_width is larger than _GP(game).max_dialogoption_width");
}
GET_OPTIONS_HEIGHT
int savedwid = areawid;
int txoffs = 0, tyoffs = 0, yspos = ui_view.GetHeight() / 2 - (2 * padding + needheight) / 2;
int xspos = ui_view.GetWidth() / 2 - areawid / 2;
// shift window to the right if QG4-style full-screen pic
if ((_GP(game).options[OPT_SPEECHTYPE] == 3) && (_G(said_text) > 0))
xspos = (ui_view.GetWidth() - areawid) - get_fixed_pixel_size(10);
// needs to draw the right text window, not the default
Bitmap *text_window_ds = nullptr;
draw_text_window(&text_window_ds, false, &txoffs, &tyoffs, &xspos, &yspos, &areawid, nullptr, needheight, _GP(game).options[OPT_DIALOGIFACE]);
options_surface_has_alpha = _GP(guis)[_GP(game).options[OPT_DIALOGIFACE]].HasAlphaChannel();
// since draw_text_window incrases the width, restore it
areawid = savedwid;
dirtyx = xspos;
dirtyy = yspos;
dirtywidth = text_window_ds->GetWidth();
dirtyheight = text_window_ds->GetHeight();
dialog_abs_x = txoffs + xspos;
GfxUtil::DrawSpriteWithTransparency(ds, text_window_ds, xspos, yspos);
// TODO: here we rely on draw_text_window always assigning new bitmap to text_window_ds;
// should make this more explicit
delete text_window_ds;
// Ignore the dialog_options_x/y offsets when using a text window
txoffs += xspos;
tyoffs += yspos;
dlgyp = tyoffs;
curyp = write_dialog_options(ds, options_surface_has_alpha, txoffs, tyoffs, numdisp, mouseison, areawid, bullet_wid, usingfont, dtop, disporder, dispyp, linespacing, forecol, padding);
if (parserInput)
parserInput->X = txoffs;
} else {
if (wantRefresh) {
// redraw the black background so that anti-alias
// fonts don't re-alias themselves
if (_GP(game).options[OPT_DIALOGIFACE] == 0) {
color_t draw_color = ds->GetCompatibleColor(16);
ds->FillRect(Rect(0, dlgyp - 1, ui_view.GetWidth() - 1, ui_view.GetHeight() - 1), draw_color);
} else {
GUIMain *guib = &_GP(guis)[_GP(game).options[OPT_DIALOGIFACE]];
if (!guib->IsTextWindow())
draw_gui_for_dialog_options(ds, guib, dlgxp, dlgyp);
}
}
dirtyx = 0;
dirtywidth = ui_view.GetWidth();
if (_GP(game).options[OPT_DIALOGIFACE] > 0) {
// the whole GUI area should be marked dirty in order
// to ensure it gets drawn
GUIMain *guib = &_GP(guis)[_GP(game).options[OPT_DIALOGIFACE]];
dirtyheight = guib->Height;
dirtyy = dlgyp;
options_surface_has_alpha = guib->HasAlphaChannel();
} else {
dirtyy = dlgyp - 1;
dirtyheight = needheight + 1;
options_surface_has_alpha = false;
}
dlgxp += data_to_game_coord(_GP(play).dialog_options_x);
dlgyp += data_to_game_coord(_GP(play).dialog_options_y);
// if they use a negative dialog_options_y, make sure the
// area gets marked as dirty
if (dlgyp < dirtyy)
dirtyy = dlgyp;
curyp = dlgyp;
curyp = write_dialog_options(ds, options_surface_has_alpha, dlgxp, curyp, numdisp, mouseison, areawid, bullet_wid, usingfont, dtop, disporder, dispyp, linespacing, forecol, padding);
if (parserInput)
parserInput->X = dlgxp;
}
if (parserInput) {
// Set up the text box, if present
parserInput->Y = curyp + data_to_game_coord(_GP(game).options[OPT_DIALOGGAP]);
parserInput->Width = areawid - get_fixed_pixel_size(10);
parserInput->TextColor = _G(playerchar)->talkcolor;
if (mouseison == DLG_OPTION_PARSER)
parserInput->TextColor = forecol;
if (_GP(game).dialog_bullet) { // the parser X will get moved in a second
draw_gui_sprite_v330(ds, _GP(game).dialog_bullet, parserInput->X, parserInput->Y, options_surface_has_alpha);
}
parserInput->Width -= bullet_wid;
parserInput->X += bullet_wid;
parserInput->Draw(ds, parserInput->X, parserInput->Y);
parserInput->IsActivated = false;
}
wantRefresh = false;
subBitmap = recycle_bitmap(subBitmap,
_G(gfxDriver)->GetCompatibleBitmapFormat(tempScrn->GetColorDepth()), dirtywidth, dirtyheight);
if (usingCustomRendering) {
subBitmap->Blit(tempScrn, 0, 0, 0, 0, tempScrn->GetWidth(), tempScrn->GetHeight());
invalidate_rect(dirtyx, dirtyy, dirtyx + subBitmap->GetWidth(), dirtyy + subBitmap->GetHeight(), false);
} else {
subBitmap->Blit(tempScrn, dirtyx, dirtyy, 0, 0, dirtywidth, dirtyheight);
}
if ((ddb != nullptr) &&
((ddb->GetWidth() != dirtywidth) ||
(ddb->GetHeight() != dirtyheight))) {
_G(gfxDriver)->DestroyDDB(ddb);
ddb = nullptr;
}
if (ddb == nullptr)
ddb = _G(gfxDriver)->CreateDDBFromBitmap(subBitmap, options_surface_has_alpha, false);
else
_G(gfxDriver)->UpdateDDBFromBitmap(ddb, subBitmap, options_surface_has_alpha);
if (runGameLoopsInBackground) {
render_graphics(ddb, dirtyx, dirtyy);
}
}
bool DialogOptions::Run() {
// Run() can be called in a loop, so keep events going.
sys_evt_process_pending();
const bool new_custom_render = usingCustomRendering && _GP(game).options[OPT_DIALOGOPTIONSAPI] >= 0;
if (runGameLoopsInBackground) {
_GP(play).disabled_user_interface++;
UpdateGameOnce(false, ddb, dirtyx, dirtyy);
_GP(play).disabled_user_interface--;
} else {
update_audio_system_on_game_loop();
render_graphics(ddb, dirtyx, dirtyy);
}
if (new_custom_render) {
_GP(runDialogOptionRepExecFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
run_function_on_non_blocking_thread(&_GP(runDialogOptionRepExecFunc));
}
needRedraw = false;
// Handle keyboard
if (!RunKeyControls())
return false; // end loop
if (needRedraw)
Redraw();
// Handle mouse
mousewason = mouseison;
mouseison = -1;
if (new_custom_render); // do not automatically detect option under mouse
else if (usingCustomRendering) {
if ((_G(mousex) >= dirtyx) && (_G(mousey) >= dirtyy) &&
(_G(mousex) < dirtyx + tempScrn->GetWidth()) &&
(_G(mousey) < dirtyy + tempScrn->GetHeight())) {
_GP(getDialogOptionUnderCursorFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
run_function_on_non_blocking_thread(&_GP(getDialogOptionUnderCursorFunc));
if (!_GP(getDialogOptionUnderCursorFunc).atLeastOneImplementationExists)
quit("!The script function dialog_options_get_active is not implemented. It must be present to use a custom dialogue system.");
mouseison = _GP(ccDialogOptionsRendering).activeOptionID;
} else {
_GP(ccDialogOptionsRendering).activeOptionID = -1;
}
} else if (_G(mousex) >= dialog_abs_x && _G(mousex) < (dialog_abs_x + areawid) &&
_G(mousey) >= dlgyp && _G(mousey) < curyp) {
mouseison = numdisp - 1;
for (int i = 0; i < numdisp; ++i) {
if (_G(mousey) < dispyp[i]) {
mouseison = i - 1; break;
}
}
if ((mouseison < 0) | (mouseison >= numdisp)) mouseison = -1;
}
if (parserInput != nullptr) {
int relative_mousey = _G(mousey);
if (usingCustomRendering)
relative_mousey -= dirtyy;
if ((relative_mousey > parserInput->Y) &&
(relative_mousey < parserInput->Y + parserInput->Height))
mouseison = DLG_OPTION_PARSER;
if (parserInput->IsActivated)
parserActivated = 1;
}
eAGSMouseButton mbut;
int mwheelz;
if (run_service_mb_controls(mbut, mwheelz) && mbut > kMouseNone &&
!_GP(play).IsIgnoringInput()) {
if (mouseison < 0 && !new_custom_render) {
if (usingCustomRendering) {
_GP(runDialogOptionMouseClickHandlerFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
_GP(runDialogOptionMouseClickHandlerFunc).params[1].SetInt32(mbut);
run_function_on_non_blocking_thread(&_GP(runDialogOptionMouseClickHandlerFunc));
if (_GP(runDialogOptionMouseClickHandlerFunc).atLeastOneImplementationExists) {
Redraw();
return true; // continue running loop
}
}
return true; // continue running loop
}
if (mouseison == DLG_OPTION_PARSER) {
// they clicked the text box
parserActivated = 1;
} else if (new_custom_render) {
_GP(runDialogOptionMouseClickHandlerFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
_GP(runDialogOptionMouseClickHandlerFunc).params[1].SetInt32(mbut);
run_function_on_non_blocking_thread(&_GP(runDialogOptionMouseClickHandlerFunc));
} else if (usingCustomRendering) {
chose = mouseison;
return false; // end dialog options running loop
} else {
chose = disporder[mouseison];
return false; // end dialog options running loop
}
}
if (usingCustomRendering) {
if (mwheelz != 0) {
_GP(runDialogOptionMouseClickHandlerFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
_GP(runDialogOptionMouseClickHandlerFunc).params[1].SetInt32((mwheelz < 0) ? 9 : 8);
run_function_on_non_blocking_thread(&_GP(runDialogOptionMouseClickHandlerFunc));
if (!new_custom_render) {
if (_GP(runDialogOptionMouseClickHandlerFunc).atLeastOneImplementationExists)
Redraw();
return true; // continue running loop
}
}
}
if (parserActivated) {
// They have selected a custom parser-based option
if (!parserInput->Text.IsEmpty() != 0) {
chose = DLG_OPTION_PARSER;
return false; // end dialog options running loop
} else {
parserActivated = 0;
parserInput->IsActivated = 0;
}
}
if (mousewason != mouseison) {
Redraw();
return true; // continue running loop
}
if (new_custom_render) {
if (_GP(ccDialogOptionsRendering).chosenOptionID >= 0) {
chose = _GP(ccDialogOptionsRendering).chosenOptionID;
_GP(ccDialogOptionsRendering).chosenOptionID = -1;
return false; // end dialog options running loop
}
if (_GP(ccDialogOptionsRendering).needRepaint) {
Redraw();
return true; // continue running loop
}
}
update_polled_stuff();
if (!runGameLoopsInBackground && (_GP(play).fast_forward == 0)) { // note if runGameLoopsInBackground then it's called inside UpdateGameOnce
WaitForNextFrame();
}
return true; // continue running loop
}
bool DialogOptions::RunKeyControls() {
// Handle all the buffered key events
bool do_break = false; // continue the loop or end dialog options
while (ags_keyevent_ready()) {
KeyInput ki;
if (run_service_key_controls(ki) && !_GP(play).IsIgnoringInput()) {
if (!do_break && !RunKey(ki)) {
ags_clear_input_buffer();
do_break = true; // end dialog options
}
}
}
return !do_break;
}
bool DialogOptions::RunKey(const KeyInput &ki) {
const bool new_custom_render = usingCustomRendering && _GP(game).options[OPT_DIALOGOPTIONSAPI] >= 0;
const bool old_keyhandle = _GP(game).options[OPT_KEYHANDLEAPI] == 0;
const eAGSKeyCode agskey = ki.Key;
if (parserInput) {
wantRefresh = true;
// type into the parser
// TODO: find out what are these key commands, and are these documented?
if ((agskey == eAGSKeyCodeF3) || ((agskey == eAGSKeyCodeSpace) && (parserInput->Text.GetLength() == 0))) {
// write previous contents into textbox (F3 or Space when box is empty)
size_t last_len = ustrlen(_GP(play).lastParserEntry);
size_t cur_len = ustrlen(parserInput->Text.GetCStr());
// [ikm] CHECKME: tbh I don't quite get the logic here (it was like this in original code);
// but what we do is copying only the last part of the previous string
if (cur_len < last_len) {
const char *entry = _GP(play).lastParserEntry;
// TODO: utility function for advancing N utf-8 chars
for (size_t i = 0; i < cur_len; ++i) ugetxc(&entry);
parserInput->Text.Append(entry);
}
needRedraw = true;
return true; // continue running loop
} else if ((agskey >= eAGSKeyCodeSpace) || (agskey == eAGSKeyCodeReturn) || (agskey == eAGSKeyCodeBackspace)) {
parserInput->OnKeyPress(ki);
if (!parserInput->IsActivated) {
needRedraw = true;
return true; // continue running loop
}
}
} else if (new_custom_render) {
if (old_keyhandle || (ki.UChar == 0)) {
// "dialog_options_key_press"
_GP(runDialogOptionKeyPressHandlerFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
_GP(runDialogOptionKeyPressHandlerFunc).params[1].SetInt32(AGSKeyToScriptKey(ki.Key));
_GP(runDialogOptionKeyPressHandlerFunc).params[2].SetInt32(ki.Mod);
run_function_on_non_blocking_thread(&_GP(runDialogOptionKeyPressHandlerFunc));
}
if (!old_keyhandle && (ki.UChar > 0)) {
// "dialog_options_text_input"
_GP(runDialogOptionTextInputHandlerFunc).params[0].SetDynamicObject(&_GP(ccDialogOptionsRendering), &_GP(ccDialogOptionsRendering));
_GP(runDialogOptionTextInputHandlerFunc).params[1].SetInt32(ki.UChar);
run_function_on_non_blocking_thread(&_GP(runDialogOptionKeyPressHandlerFunc));
}
}
// Allow selection of options by keyboard shortcuts
else if (_GP(game).options[OPT_DIALOGNUMBERED] >= kDlgOptKeysOnly &&
agskey >= '1' && agskey <= '9') {
int numkey = agskey - '1';
if (numkey < numdisp) {
chose = disporder[numkey];
return false; // end dialog options running loop
}
}
return true; // continue running loop
}
void DialogOptions::Close() {
ags_clear_input_buffer();
invalidate_screen();
if (parserActivated) {
snprintf(_GP(play).lastParserEntry, MAX_MAXSTRLEN, "%s", parserInput->Text.GetCStr());
ParseText(parserInput->Text.GetCStr());
chose = CHOSE_TEXTPARSER;
}
if (parserInput) {
delete parserInput;
parserInput = nullptr;
}
if (ddb != nullptr)
_G(gfxDriver)->DestroyDDB(ddb);
delete subBitmap;
set_mouse_cursor(curswas);
// In case it's the QFG4 style dialog, remove the black screen
_GP(play).in_conversation--;
remove_screen_overlay(OVER_COMPLETE);
delete tempScrn;
}
int show_dialog_options(int _dlgnum, int sayChosenOption, bool _runGameLoopsInBackground) {
DialogOptions dlgopt;
dlgopt.Prepare(_dlgnum, _runGameLoopsInBackground);
dlgopt.Show();
dlgopt.Close();
int dialog_choice = dlgopt.chose;
if (dialog_choice >= 0) { // NOTE: this condition also excludes CHOSE_TEXTPARSER
assert(dialog_choice >= 0 && dialog_choice < MAXTOPICOPTIONS);
DialogTopic *dialog_topic = dlgopt.dtop;
int32_t &option_flags = dialog_topic->optionflags[dialog_choice];
const char *option_name = dlgopt.dtop->optionnames[dialog_choice];
option_flags |= DFLG_HASBEENCHOSEN;
bool sayTheOption = false;
if (sayChosenOption == SAYCHOSEN_YES) {
sayTheOption = true;
} else if (sayChosenOption == SAYCHOSEN_USEFLAG) {
sayTheOption = ((option_flags & DFLG_NOREPEAT) == 0);
}
if (sayTheOption)
DisplaySpeech(get_translation(option_name), _GP(game).playercharacter);
}
return dialog_choice;
}
// Dialog execution state
struct DialogExec {
int DlgNum = -1;
int DlgWas = -1;
// CHECKME: this may be unnecessary, investigate later
bool IsFirstEntry = true;
// nested dialogs "stack"
Common::Stack<int> TopicHist;
DialogExec(int start_dlgnum) : DlgNum(start_dlgnum) {}
int HandleDialogResult(int res);
void Run();
};
int DialogExec::HandleDialogResult(int res) {
// Handle goto-previous, see if there's any previous dialog in history
if (res == RUN_DIALOG_GOTO_PREVIOUS) {
if (TopicHist.size() == 0)
return RUN_DIALOG_STOP_DIALOG;
res = TopicHist.top();
TopicHist.pop();
}
// Continue to the next dialog
if (res >= 0) {
// save the old topic number in the history, and switch to the new one
TopicHist.push(DlgNum);
DlgNum = res;
return DlgNum;
}
return res;
}
void DialogExec::Run() {
while (DlgNum >= 0) {
if (DlgNum < 0 || DlgNum >= _GP(game).numdialog)
quitprintf("!RunDialog: invalid dialog number specified: %d", DlgNum);
// current dialog object
DialogTopic *dtop = &_G(dialog)[DlgNum];
int res = 0; // dialog execution result
// If a new dialog topic: run dialog entry point
if (DlgNum != DlgWas) {
res = run_dialog_script(DlgNum, dtop->startupentrypoint, 0);
DlgWas = DlgNum;
// Handle the dialog entry's result
res = HandleDialogResult(res);
if (res == RUN_DIALOG_STOP_DIALOG)
return; // stop the dialog
IsFirstEntry = false;
if (res != RUN_DIALOG_STAY)
continue; // skip to the next dialog
}
// Show current dialog's options
int chose = show_dialog_options(DlgNum, SAYCHOSEN_USEFLAG, (_GP(game).options[OPT_RUNGAMEDLGOPTS] != 0));
if (chose == CHOSE_TEXTPARSER) {
_G(said_speech_line) = 0;
res = run_dialog_request(DlgNum);
if (_G(said_speech_line) > 0) {
// fix the problem with the close-up face remaining on screen
DisableInterface();
UpdateGameOnce(); // redraw the screen to make sure it looks right
EnableInterface();
set_mouse_cursor(CURS_ARROW);
}
} else if (chose >= 0) {
// chose some option - run its script
res = run_dialog_script(DlgNum, dtop->entrypoints[chose], chose + 1);
} else {
return; // no option chosen? - stop the dialog
}
// Handle the dialog option's result
res = HandleDialogResult(res);
if (res == RUN_DIALOG_STOP_DIALOG)
return; // stop the dialog
// continue to the next dialog or show same dialog's options again
}
}
void do_conversation(int dlgnum) {
EndSkippingUntilCharStops();
// AGS 2.x always makes the mouse cursor visible when displaying a dialog.
if (_G(loaded_game_file_version) <= kGameVersion_272)
_GP(play).mouse_cursor_hidden = 0;
DialogExec dlgexec(dlgnum);
dlgexec.Run();
// CHECKME: find out if this is safe to do always, regardless of number of iterations
if (dlgexec.IsFirstEntry) {
// bail out from first startup script
remove_screen_overlay(OVER_COMPLETE);
_GP(play).in_conversation--;
}
}
// end dialog manager
//=============================================================================
//
// Script API Functions
//
//=============================================================================
// int (ScriptDialog *sd)
RuntimeScriptValue Sc_Dialog_GetID(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_INT(ScriptDialog, Dialog_GetID);
}
// int (ScriptDialog *sd)
RuntimeScriptValue Sc_Dialog_GetOptionCount(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_INT(ScriptDialog, Dialog_GetOptionCount);
}
// int (ScriptDialog *sd)
RuntimeScriptValue Sc_Dialog_GetShowTextParser(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_INT(ScriptDialog, Dialog_GetShowTextParser);
}
// int (ScriptDialog *sd, int sayChosenOption)
RuntimeScriptValue Sc_Dialog_DisplayOptions(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_INT_PINT(ScriptDialog, Dialog_DisplayOptions);
}
// int (ScriptDialog *sd, int option)
RuntimeScriptValue Sc_Dialog_GetOptionState(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_INT_PINT(ScriptDialog, Dialog_GetOptionState);
}
// const char* (ScriptDialog *sd, int option)
RuntimeScriptValue Sc_Dialog_GetOptionText(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_CONST_OBJCALL_OBJ_PINT(ScriptDialog, const char, _GP(myScriptStringImpl), Dialog_GetOptionText);
}
// int (ScriptDialog *sd, int option)
RuntimeScriptValue Sc_Dialog_HasOptionBeenChosen(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_INT_PINT(ScriptDialog, Dialog_HasOptionBeenChosen);
}
RuntimeScriptValue Sc_Dialog_SetHasOptionBeenChosen(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_VOID_PINT_PBOOL(ScriptDialog, Dialog_SetHasOptionBeenChosen);
}
// void (ScriptDialog *sd, int option, int newState)
RuntimeScriptValue Sc_Dialog_SetOptionState(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_VOID_PINT2(ScriptDialog, Dialog_SetOptionState);
}
// void (ScriptDialog *sd)
RuntimeScriptValue Sc_Dialog_Start(void *self, const RuntimeScriptValue *params, int32_t param_count) {
API_OBJCALL_VOID(ScriptDialog, Dialog_Start);
}
void RegisterDialogAPI() {
ccAddExternalObjectFunction("Dialog::get_ID", Sc_Dialog_GetID);
ccAddExternalObjectFunction("Dialog::get_OptionCount", Sc_Dialog_GetOptionCount);
ccAddExternalObjectFunction("Dialog::get_ShowTextParser", Sc_Dialog_GetShowTextParser);
ccAddExternalObjectFunction("Dialog::DisplayOptions^1", Sc_Dialog_DisplayOptions);
ccAddExternalObjectFunction("Dialog::GetOptionState^1", Sc_Dialog_GetOptionState);
ccAddExternalObjectFunction("Dialog::GetOptionText^1", Sc_Dialog_GetOptionText);
ccAddExternalObjectFunction("Dialog::HasOptionBeenChosen^1", Sc_Dialog_HasOptionBeenChosen);
ccAddExternalObjectFunction("Dialog::SetHasOptionBeenChosen^2", Sc_Dialog_SetHasOptionBeenChosen);
ccAddExternalObjectFunction("Dialog::SetOptionState^2", Sc_Dialog_SetOptionState);
ccAddExternalObjectFunction("Dialog::Start^0", Sc_Dialog_Start);
}
} // namespace AGS3