Files
scummvm/engines/ags/engine/main/game_run.cpp
T
Walter Agazzi 7abf9ab5fd AGS: Engine: fixed "mouse up" event could cause a recursive on_event
First "on_event" firing after mouse button is released may cause a sequence of blocking actions.
If there's any control checks during them, the global "wasbutdown" variable will be tested again,
potentially leading to a recursive "mouse up" event, except it's not run immediately, but scheduled
until after the first one has completed running.

For this reason "wasbutdown" must be reset BEFORE firing any events.

This was broken a VERY long time ago, it seems, probably by commit 2f54a16
From upstream ff1baccbffd8ee79fe3a5aff547e2ac7b8d8aaf5 (branch 3.6.1)
2024-05-03 12:49:51 +02:00

1129 lines
36 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/>.
*
*/
//
// Game loop
//
#include "ags/lib/std/limits.h"
#include "ags/engine/ac/button.h"
#include "ags/shared/ac/common.h"
#include "ags/engine/ac/character_extras.h"
#include "ags/shared/ac/character_info.h"
#include "ags/engine/ac/draw.h"
#include "ags/engine/ac/event.h"
#include "ags/engine/ac/game.h"
#include "ags/engine/ac/game_setup.h"
#include "ags/shared/ac/game_setup_struct.h"
#include "ags/engine/ac/game_state.h"
#include "ags/engine/ac/global_debug.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_region.h"
#include "ags/engine/ac/gui.h"
#include "ags/engine/ac/hotspot.h"
#include "ags/shared/ac/keycode.h"
#include "ags/engine/ac/mouse.h"
#include "ags/engine/ac/overlay.h"
#include "ags/shared/ac/sprite_cache.h"
#include "ags/engine/ac/sys_events.h"
#include "ags/engine/ac/room.h"
#include "ags/engine/ac/room_object.h"
#include "ags/engine/ac/room_status.h"
#include "ags/engine/ac/view_frame.h"
#include "ags/engine/ac/walk_behind.h"
#include "ags/engine/debugging/debugger.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/engine/device/mouse_w32.h"
#include "ags/engine/gui/animating_gui_button.h"
#include "ags/shared/gui/gui_inv.h"
#include "ags/shared/gui/gui_main.h"
#include "ags/shared/gui/gui_textbox.h"
#include "ags/engine/main/engine.h"
#include "ags/engine/main/game_run.h"
#include "ags/engine/main/update.h"
#include "ags/engine/media/audio/audio_system.h"
#include "ags/engine/platform/base/ags_platform_driver.h"
#include "ags/plugins/ags_plugin.h"
#include "ags/plugins/plugin_engine.h"
#include "ags/engine/script/script.h"
#include "ags/engine/script/script_runtime.h"
#include "ags/events.h"
#include "ags/globals.h"
namespace AGS3 {
using namespace AGS::Shared;
static bool ShouldStayInWaitMode();
#define UNTIL_ANIMEND 1
#define UNTIL_MOVEEND 2
#define UNTIL_CHARIS0 3
#define UNTIL_NOOVERLAY 4
#define UNTIL_NEGATIVE 5
#define UNTIL_INTIS0 6
#define UNTIL_SHORTIS0 7
#define UNTIL_INTISNEG 8
#define UNTIL_ANIMBTNEND 9
static void ProperExit() {
_G(want_exit) = false;
_G(proper_exit) = 1;
quit("||exit!");
}
static void game_loop_check_problems_at_start() {
if ((_G(in_enters_screen) != 0) & (_G(displayed_room) == _G(starting_room)))
quit("!A text script run in the Player Enters Screen event caused the screen to be updated. If you need to use Wait(), do so in After Fadein");
if ((_G(in_enters_screen) != 0) && (_G(done_es_error) == 0)) {
debug_script_warn("Wait() was used in Player Enters Screen - use Enters Screen After Fadein instead");
_G(done_es_error) = 1;
}
if (_G(no_blocking_functions))
quit("!A blocking function was called from within a non-blocking event such as " REP_EXEC_ALWAYS_NAME);
}
// Runs rep-exec
static void game_loop_do_early_script_update() {
if (_G(in_new_room) == 0) {
// Run the room and game script repeatedly_execute
run_function_on_non_blocking_thread(&_GP(repExecAlways));
setevent(EV_TEXTSCRIPT, TS_REPEAT);
setevent(EV_RUNEVBLOCK, EVB_ROOM, 0, EVROM_REPEXEC);
}
}
// Runs late-rep-exec
static void game_loop_do_late_script_update() {
if (_G(in_new_room) == 0) {
// Run the room and game script late_repeatedly_execute
run_function_on_non_blocking_thread(&_GP(lateRepExecAlways));
}
}
static int game_loop_check_ground_level_interactions() {
if ((_GP(play).ground_level_areas_disabled & GLED_INTERACTION) == 0) {
// check if he's standing on a hotspot
int hotspotThere = get_hotspot_at(_G(playerchar)->x, _G(playerchar)->y);
// run Stands on Hotspot event
setevent(EV_RUNEVBLOCK, EVB_HOTSPOT, hotspotThere, EVHOT_STANDSON);
// check current region
int onRegion = GetRegionIDAtRoom(_G(playerchar)->x, _G(playerchar)->y);
int inRoom = _G(displayed_room);
if (onRegion != _GP(play).player_on_region) {
// we need to save this and set _GP(play).player_on_region
// now, so it's correct going into RunRegionInteraction
int oldRegion = _GP(play).player_on_region;
_GP(play).player_on_region = onRegion;
// Walks Off last region
if (oldRegion > 0)
RunRegionInteraction(oldRegion, 2);
// Walks Onto new region
if (onRegion > 0)
RunRegionInteraction(onRegion, 1);
}
if (_GP(play).player_on_region > 0) // player stands on region
RunRegionInteraction(_GP(play).player_on_region, 0);
// one of the region interactions sent us to another room
if (inRoom != _G(displayed_room)) {
check_new_room();
}
// if in a Wait loop which is no longer valid (probably
// because the Region interaction did a NewRoom), abort
// the rest of the loop
if ((_G(restrict_until).type > 0) && (!ShouldStayInWaitMode())) {
// cancel the Rep Exec and Stands on Hotspot events that
// we just added -- otherwise the event queue gets huge
_GP(events).resize(_G(numEventsAtStartOfFunction));
return 0;
}
} // end if checking ground level interactions
return RETURN_CONTINUE;
}
static void lock_mouse_on_click() {
if (_GP(usetup).mouse_auto_lock && _GP(scsystem).windowed)
_GP(mouse).TryLockToWindow();
}
static void toggle_mouse_lock() {
if (_GP(scsystem).windowed) {
if (_GP(mouse).IsLockedToWindow())
_GP(mouse).UnlockFromWindow();
else
_GP(mouse).TryLockToWindow();
}
}
// Runs default mouse button handling
static void check_mouse_controls() {
int mongu = -1;
mongu = gui_on_mouse_move();
_G(mouse_on_iface) = mongu;
if ((_G(ifacepopped) >= 0) && (_G(mousey) >= _GP(guis)[_G(ifacepopped)].Y + _GP(guis)[_G(ifacepopped)].Height))
remove_popup_interface(_G(ifacepopped));
// check mouse clicks on GUIs
if ((_G(wasbutdown) > kMouseNone) && (ags_misbuttondown(_G(wasbutdown)))) {
gui_on_mouse_hold(_G(wasongui), _G(wasbutdown));
} else if ((_G(wasbutdown) > kMouseNone) && (!ags_misbuttondown(_G(wasbutdown)))) {
eAGSMouseButton mouse_btn_up = _G(wasbutdown);
_G(wasbutdown) = kMouseNone; // reset before event, avoid recursive call of "mouse up"
gui_on_mouse_up(_G(wasongui), mouse_btn_up);
}
eAGSMouseButton mbut;
int mwheelz;
if (run_service_mb_controls(mbut, mwheelz) && mbut > kMouseNone) {
check_skip_cutscene_mclick(mbut);
if (_GP(play).fast_forward || _GP(play).IsIgnoringInput()) { /* do nothing if skipping cutscene or input disabled */
} else if ((_GP(play).wait_counter != 0) && (_GP(play).key_skip_wait & SKIP_MOUSECLICK) != 0) {
_GP(play).SetWaitSkipResult(SKIP_MOUSECLICK, mbut);
} else if (_GP(play).text_overlay_on > 0) {
if (_GP(play).speech_skip_style & SKIP_MOUSECLICK) {
remove_screen_overlay(_GP(play).text_overlay_on);
_GP(play).SetWaitSkipResult(SKIP_MOUSECLICK, mbut);
}
} else if (!IsInterfaceEnabled()); // blocking cutscene, ignore mouse
else if (pl_run_plugin_hooks(AGSE_MOUSECLICK, mbut)) {
// plugin took the click
debug_script_log("Plugin handled mouse button %d", mbut);
} else if (mongu >= 0) {
if (_G(wasbutdown) == kMouseNone) {
gui_on_mouse_down(mongu, mbut);
}
_G(wasongui) = mongu;
_G(wasbutdown) = mbut;
} else
setevent(EV_TEXTSCRIPT, TS_MCLICK, mbut);
}
if (mwheelz < 0)
setevent(EV_TEXTSCRIPT, TS_MCLICK, 9);
else if (mwheelz > 0)
setevent(EV_TEXTSCRIPT, TS_MCLICK, 8);
}
// Special flags to OR saved SDL_Keymod flags with:
// Mod key combination already fired (wait until full mod release)
#define KEY_MODS_FIRED 0x80000000
int cur_key_mods = 0;
int old_key_mod = 0; // for saving previous key mods
// Runs service key controls, returns false if service key combinations were handled
// and no more processing required, otherwise returns true and provides current keycode and key shifts.
//
// * old_keyhandle mode is a backward compatible input handling mode, where
// - lone mod keys are not passed further into the engine;
// - key + mod combos are merged into one key code for the script callback.
bool run_service_key_controls(KeyInput &out_key) {
const bool old_keyhandle = (_GP(game).options[OPT_KEYHANDLEAPI] == 0);
bool handled = false;
const bool key_valid = ags_keyevent_ready();
const Common::Event key_evt = key_valid ? ags_get_next_keyevent() : Common::Event();
const bool is_only_mod_key = key_evt.type == Common::EVENT_KEYDOWN ?
is_mod_key(key_evt.kbd.keycode) : false;
out_key = KeyInput(); // reset to default
// Following section is for testing for pushed and released mod-keys.
// A bit of explanation: some service actions may require combination of
// mod-keys, for example [Ctrl + Alt] toggles mouse lock in window.
// Here comes a problem: other actions may also use [Ctrl + Alt] mods in
// combination with a third key: e.g. [Ctrl + Alt + V] displays engine info.
// For this reason we cannot simply test for pressed Ctrl and Alt here,
// but we must wait until player *releases at least one mod key* of this combo,
// while no third key was pressed.
// In other words, such action should only trigger if:
// * if combination of held down mod-keys was gathered,
// * if no other key was pressed meanwhile,
// * if at least one of those gathered mod-keys was released.
//
// TODO: maybe split this mod handling into sep procedure and make it easier to use (not that it's used a lot)?
// First, check mods
const int cur_mod = make_merged_mod(key_evt.kbd.flags);
// If shifts combination have already triggered an action, then do nothing
// until new shifts are empty, in which case reset saved shifts
if (old_key_mod & KEY_MODS_FIRED) {
if (cur_mod == 0)
old_key_mod = 0;
} else {
// If any non-mod key is pressed, add fired flag to indicate that
// this is no longer a pure mod keys combination
if (key_valid && !is_only_mod_key) {
old_key_mod = cur_mod | KEY_MODS_FIRED;
}
// If all the previously registered mods are still pressed,
// then simply resave new mods state.
else if ((old_key_mod & cur_mod) == old_key_mod) {
old_key_mod = cur_mod;
}
// Otherwise some of the mods were released, then run key combo action
// and set KEY_MODS_FIRED flag to prevent multiple execution
else if (old_key_mod) {
// Toggle mouse lock on Ctrl + Alt
if (old_key_mod == (Common::KBD_CTRL | Common::KBD_ALT)) {
toggle_mouse_lock();
handled = true;
}
old_key_mod |= KEY_MODS_FIRED;
}
}
cur_key_mods = cur_mod;
if (!key_valid)
return false; // if there was no key press, finish after handling current mod state
if (handled || (old_keyhandle && is_only_mod_key))
return false; // in backward mode the engine does not react to single mod keys
KeyInput ki = ags_keycode_from_scummvm(key_evt, old_keyhandle);
if (ki.Key == eAGSKeyCodeNone)
return false; // should skip this key event
// Use backward-compatible combined key for special controls
eAGSKeyCode agskey = ki.CompatKey;
// LAlt or RAlt + Enter/Return
if ((cur_mod == Common::KBD_ALT) && agskey == eAGSKeyCodeReturn) {
engine_try_switch_windowed_gfxmode();
return false;
}
// Alt+X, abort (but only once game is loaded)
if ((_G(displayed_room) >= 0) && (agskey == _GP(play).abort_key)) {
Debug::Printf("Abort key pressed");
_G(check_dynamic_sprites_at_exit) = 0;
quit("!|");
}
// debug console
if ((agskey == '`') && (_GP(play).debug_mode > 0)) {
_G(display_console) = !_G(display_console);
return false;
}
if ((agskey == eAGSKeyCodeCtrlE) && (_G(display_fps) == kFPS_Forced)) {
// if --fps paramter is used, Ctrl+E will max out frame rate
setTimerFps(isTimerFpsMaxed() ? _G(frames_per_second) : 1000);
return false;
}
if ((agskey == eAGSKeyCodeCtrlD) && (_GP(play).debug_mode > 0)) {
// ctrl+D - show info
char infobuf[900];
size_t ln = 0;
ln += Common::sprintf_s(infobuf, "In room %d %s[Player at %d, %d (view %d, loop %d, frame %d)%s%s%s",
_G(displayed_room), (_G(noWalkBehindsAtAll) ? "(has no walk-behinds)" : ""), _G(playerchar)->x, _G(playerchar)->y,
_G(playerchar)->view + 1, _G(playerchar)->loop, _G(playerchar)->frame,
(IsGamePaused() == 0) ? "" : "[Game paused.",
(_GP(play).ground_level_areas_disabled == 0) ? "" : "[Ground areas disabled.",
(IsInterfaceEnabled() == 0) ? "[Game in Wait state" : "");
for (uint32_t ff = 0; ff < _G(croom)->numobj; ff++) {
if (ff >= 8) break; // buffer not big enough for more than 7
ln += Common::sprintf_s(&infobuf[ln], sizeof(infobuf) - ln,
"[Object %d: (%d,%d) size (%d x %d) on:%d moving:%s animating:%d slot:%d trnsp:%d clkble:%d",
ff, _G(objs)[ff].x, _G(objs)[ff].y,
(_GP(spriteset)[_G(objs)[ff].num] != nullptr) ? _GP(game).SpriteInfos[_G(objs)[ff].num].Width : 0,
(_GP(spriteset)[_G(objs)[ff].num] != nullptr) ? _GP(game).SpriteInfos[_G(objs)[ff].num].Height : 0,
_G(objs)[ff].on,
(_G(objs)[ff].moving > 0) ? "yes" : "no", _G(objs)[ff].cycling,
_G(objs)[ff].num, _G(objs)[ff].transparent,
((_G(objs)[ff].flags & OBJF_NOINTERACT) != 0) ? 0 : 1);
}
Display(infobuf);
int chd = _GP(game).playercharacter;
char bigbuffer[STD_BUFFER_SIZE] = "CHARACTERS IN THIS ROOM:[";
for (int ff = 0; ff < _GP(game).numcharacters; ff++) {
if (_GP(game).chars[ff].room != _G(displayed_room)) continue;
if (strlen(bigbuffer) > 430) {
Common::strcat_s(bigbuffer, "and more...");
Display(bigbuffer);
Common::strcpy_s(bigbuffer, "CHARACTERS IN THIS ROOM (cont'd):[");
}
chd = ff;
ln = strlen(bigbuffer);
Common::sprintf_s(&bigbuffer[ln], sizeof(bigbuffer) - ln,
"%s (view/loop/frm:%d,%d,%d x/y/z:%d,%d,%d idleview:%d,time:%d,left:%d walk:%d anim:%d follow:%d flags:%X wait:%d zoom:%d)[",
_GP(game).chars[chd].scrname, _GP(game).chars[chd].view + 1, _GP(game).chars[chd].loop, _GP(game).chars[chd].frame,
_GP(game).chars[chd].x, _GP(game).chars[chd].y, _GP(game).chars[chd].z,
_GP(game).chars[chd].idleview, _GP(game).chars[chd].idletime, _GP(game).chars[chd].idleleft,
_GP(game).chars[chd].walking, _GP(game).chars[chd].animating, _GP(game).chars[chd].following,
_GP(game).chars[chd].flags, _GP(game).chars[chd].wait, _GP(charextra)[chd].zoom);
}
Display(bigbuffer);
return false;
}
if (((agskey == eAGSKeyCodeCtrlV) && (cur_key_mods & Common::KBD_ALT) != 0)
&& (_GP(play).wait_counter < 1) && (_GP(play).text_overlay_on == 0) && (_G(restrict_until).type == 0)) {
// make sure we can't interrupt a Wait()
// and desync the music to cutscene
_GP(play).debug_mode++;
script_debug(1, 0);
_GP(play).debug_mode--;
return false;
}
// No service operation triggered? return active keypress and mods to caller
out_key = ki;
return true;
}
bool run_service_mb_controls(eAGSMouseButton &mbut, int &mwheelz) {
mbut = ags_mgetbutton();
mwheelz = ags_check_mouse_wheel();
if (mbut == kMouseNone && mwheelz == 0)
return false;
lock_mouse_on_click();
return true;
}
// Runs default keyboard handling
static void check_keyboard_controls() {
const bool old_keyhandle = _GP(game).options[OPT_KEYHANDLEAPI] == 0;
// First check for service engine's combinations (mouse lock, display mode switch, and so forth)
KeyInput ki;
if (!run_service_key_controls(ki)) {
return;
}
// Use backward-compatible combined key for special controls
const eAGSKeyCode agskey = ki.CompatKey;
// Then, check cutscene skip
check_skip_cutscene_keypress(agskey);
if (_GP(play).fast_forward) {
return;
}
if (_GP(play).IsIgnoringInput()) {
return;
}
// Now check for in-game controls
if (pl_run_plugin_hooks(AGSE_KEYPRESS, agskey)) {
// plugin took the keypress
debug_script_log("Keypress code %d taken by plugin", agskey);
return;
}
// skip speech if desired by Speech.SkipStyle
if ((_GP(play).text_overlay_on > 0) && (_GP(play).speech_skip_style & SKIP_KEYPRESS) && !IsAGSServiceKey(ki.Key)) {
// only allow a key to remove the overlay if the icon bar isn't up
if (IsGamePaused() == 0) {
// check if it requires a specific keypress
if ((_GP(play).skip_speech_specific_key == 0) ||
(agskey == _GP(play).skip_speech_specific_key)) {
remove_screen_overlay(_GP(play).text_overlay_on);
_GP(play).SetWaitKeySkip(ki);
}
}
return;
}
if ((_GP(play).wait_counter != 0) && (_GP(play).key_skip_wait & SKIP_KEYPRESS) && !IsAGSServiceKey(ki.Key)) {
_GP(play).SetWaitKeySkip(ki);
return;
}
if (_G(inside_script)) {
// Don't queue up another keypress if it can't be run instantly
debug_script_log("Keypress %d ignored (game blocked)", agskey);
return;
}
int keywasprocessed = 0;
// determine if a GUI Text Box should steal the click
// it should do if a displayable character (32-255) is
// pressed, but exclude control characters (<32) and
// extended keys (eg. up/down arrow; 256+)
if ( (((agskey >= 32) && (agskey <= 255) && (agskey != '[')) ||
(agskey == eAGSKeyCodeReturn) || (agskey == eAGSKeyCodeBackspace))
&& (_G(all_buttons_disabled) < 0)) {
for (int guiIndex = 0; guiIndex < _GP(game).numgui; guiIndex++) {
auto &gui = _GP(guis)[guiIndex];
if (!gui.IsDisplayed()) continue;
for (int controlIndex = 0; controlIndex < gui.GetControlCount(); controlIndex++) {
// not a text box, ignore it
if (gui.GetControlType(controlIndex) != kGUITextBox) {
continue;
}
auto *guitex = static_cast<GUITextBox *>(gui.GetControl(controlIndex));
if (guitex == nullptr) {
continue;
}
// if the text box is disabled, it cannot accept keypresses
if (!guitex->IsEnabled()) {
continue;
}
if (!guitex->IsVisible()) {
continue;
}
keywasprocessed = 1;
guitex->OnKeyPress(ki);
if (guitex->IsActivated) {
guitex->IsActivated = false;
setevent(EV_IFACECLICK, guiIndex, controlIndex, 1);
}
}
}
}
// Built-in key-presses
if (agskey == _GP(usetup).key_save_game) {
do_save_game_dialog();
return;
} else if (agskey == _GP(usetup).key_restore_game) {
do_restore_game_dialog();
return;
}
if (!keywasprocessed) {
const int sckey = AGSKeyToScriptKey(ki.Key);
const int sckeymod = ki.Mod;
if (old_keyhandle || (ki.UChar == 0)) {
debug_script_log("Running on_key_press keycode %d, mod %d", sckey, sckeymod);
setevent(EV_TEXTSCRIPT, TS_KEYPRESS, sckey, sckeymod);
}
if (!old_keyhandle && (ki.UChar > 0)) {
debug_script_log("Running on_text_input char %s (%d)", ki.Text, ki.UChar);
setevent(EV_TEXTSCRIPT, TS_TEXTINPUT, ki.UChar);
}
}
}
// check_controls: checks mouse & keyboard interface
static void check_controls() {
_G(our_eip) = 1007;
sys_evt_process_pending();
check_mouse_controls();
// Handle all the buffered key events
while (ags_keyevent_ready())
check_keyboard_controls();
}
static void check_room_edges(size_t numevents_was) {
if ((IsInterfaceEnabled()) && (IsGamePaused() == 0) &&
(_G(in_new_room) == 0) && (_G(new_room_was) == 0)) {
// Only allow walking off edges if not in wait mode, and
// if not in Player Enters Screen (allow walking in from off-screen)
int edgesActivated[4] = { 0, 0, 0, 0 };
// Only do it if nothing else has happened (eg. mouseclick)
if ((_GP(events).size() == numevents_was) &&
((_GP(play).ground_level_areas_disabled & GLED_INTERACTION) == 0)) {
if (_G(playerchar)->x <= _GP(thisroom).Edges.Left)
edgesActivated[0] = 1;
else if (_G(playerchar)->x >= _GP(thisroom).Edges.Right)
edgesActivated[1] = 1;
if (_G(playerchar)->y >= _GP(thisroom).Edges.Bottom)
edgesActivated[2] = 1;
else if (_G(playerchar)->y <= _GP(thisroom).Edges.Top)
edgesActivated[3] = 1;
if ((_GP(play).entered_edge >= 0) && (_GP(play).entered_edge <= 3)) {
// once the player is no longer outside the edge, forget the stored edge
if (edgesActivated[_GP(play).entered_edge] == 0)
_GP(play).entered_edge = -10;
// if we are walking in from off-screen, don't activate edges
else
edgesActivated[_GP(play).entered_edge] = 0;
}
for (int ii = 0; ii < 4; ii++) {
if (edgesActivated[ii])
setevent(EV_RUNEVBLOCK, EVB_ROOM, 0, ii);
}
}
}
_G(our_eip) = 1008;
}
static void game_loop_check_controls(bool checkControls) {
// don't let the player do anything before the screen fades in
if ((_G(in_new_room) == 0) && (checkControls)) {
int inRoom = _G(displayed_room);
size_t numevents_was = _GP(events).size();
check_controls();
check_room_edges(numevents_was);
if (_G(abort_engine))
return;
// If an inventory interaction changed the room
if (inRoom != _G(displayed_room))
check_new_room();
}
}
static void game_loop_do_update() {
if (_G(debug_flags) & DBG_NOUPDATE);
else if (_G(game_paused) == 0) update_stuff();
}
static void game_loop_update_animated_buttons() {
// update animating GUI buttons
// this bit isn't in update_stuff because it always needs to
// happen, even when the game is paused
for (size_t i = 0; i < GetAnimatingButtonCount(); ++i) {
if (!UpdateAnimatingButton(i)) {
StopButtonAnimation(i);
i--;
}
}
}
// Updates GUI reaction to the cursor position change
// TODO: possibly may be merged with gui_on_mouse_move()
static void update_cursor_over_gui() {
if (((_G(debug_flags) & DBG_NOIFACE) != 0) || (_G(displayed_room) < 0))
return; // GUI is disabled (debug flag) or room is not loaded
if (!IsInterfaceEnabled())
return; // interface is disabled (by script or blocking action)
// Poll guis
for (auto &gui : _GP(guis)) {
if (!gui.IsDisplayed())
continue; // not on screen
// Don't touch GUI if "GUIs Turn Off When Disabled"
if ((_GP(game).options[OPT_DISABLEOFF] == kGuiDis_Off) &&
(_G(all_buttons_disabled) >= 0) &&
(gui.PopupStyle != kGUIPopupNoAutoRemove))
continue;
gui.Poll(_G(mousex), _G(mousey));
}
}
static void update_cursor_view() {
// update animating mouse cursor
if (_GP(game).mcurs[_G(cur_cursor)].view >= 0) {
// only on mousemove, and it's not moving
if (((_GP(game).mcurs[_G(cur_cursor)].flags & MCF_ANIMMOVE) != 0) &&
(_G(mousex) == _G(lastmx)) && (_G(mousey) == _G(lastmy)))
;
// only on hotspot, and it's not on one
else if (((_GP(game).mcurs[_G(cur_cursor)].flags & MCF_HOTSPOT) != 0) &&
(GetLocationType(game_to_data_coord(_G(mousex)), game_to_data_coord(_G(mousey))) == 0))
set_new_cursor_graphic(_GP(game).mcurs[_G(cur_cursor)].pic);
else if (_G(mouse_delay) > 0)
_G(mouse_delay)--;
else {
int viewnum = _GP(game).mcurs[_G(cur_cursor)].view;
int loopnum = 0;
if (loopnum >= _GP(views)[viewnum].numLoops)
quitprintf("An animating mouse cursor is using view %d which has no loops", viewnum + 1);
if (_GP(views)[viewnum].loops[loopnum].numFrames < 1)
quitprintf("An animating mouse cursor is using view %d which has no frames in loop %d", viewnum + 1, loopnum);
_G(mouse_frame)++;
if (_G(mouse_frame) >= _GP(views)[viewnum].loops[loopnum].numFrames)
_G(mouse_frame) = 0;
set_new_cursor_graphic(_GP(views)[viewnum].loops[loopnum].frames[_G(mouse_frame)].pic);
_G(mouse_delay) = _GP(views)[viewnum].loops[loopnum].frames[_G(mouse_frame)].speed + _GP(game).mcurs[_G(cur_cursor)].animdelay;
CheckViewFrame(viewnum, loopnum, _G(mouse_frame));
}
_G(lastmx) = _G(mousex);
_G(lastmy) = _G(mousey);
}
}
static void update_cursor_over_location(int mwasatx, int mwasaty) {
if (_GP(play).fast_forward)
return;
if (_G(displayed_room) < 0)
return;
// Check Mouse Moves Over Hotspot event
auto view = _GP(play).GetRoomViewportAt(_G(mousex), _G(mousey));
auto cam = view ? view->GetCamera() : nullptr;
if (!cam)
return;
// NOTE: all cameras are in same room right now, so their positions are in same coordinate system;
// therefore we may use this as an indication that mouse is over different camera too.
// TODO: do not use static variables!
// TODO: if we support rotation then we also need to compare full transform!
static int offsetxWas = -1000, offsetyWas = -1000;
int offsetx = cam->GetRect().Left;
int offsety = cam->GetRect().Top;
if (((mwasatx != _G(mousex)) || (mwasaty != _G(mousey)) ||
(offsetxWas != offsetx) || (offsetyWas != offsety))) {
// mouse moves over hotspot
if (__GetLocationType(game_to_data_coord(_G(mousex)), game_to_data_coord(_G(mousey)), 1) == LOCTYPE_HOTSPOT) {
int onhs = _G(getloctype_index);
setevent(EV_RUNEVBLOCK, EVB_HOTSPOT, onhs, EVHOT_MOUSEOVER);
}
}
offsetxWas = offsetx;
offsetyWas = offsety;
}
static void game_loop_update_events() {
_G(new_room_was) = _G(in_new_room);
if (_G(in_new_room) > 0)
setevent(EV_FADEIN, 0, 0, 0);
_G(in_new_room) = 0;
processallevents();
if (!_G(abort_engine) && (_G(new_room_was) > 0) && (_G(in_new_room) == 0)) {
// if in a new room, and the room wasn't just changed again in update_events,
// then queue the Enters Screen scripts
// run these next time round, when it's faded in
if (_G(new_room_was) == 2) // first time enters screen
setevent(EV_RUNEVBLOCK, EVB_ROOM, 0, EVROM_FIRSTENTER);
if (_G(new_room_was) != 3) // enters screen after fadein
setevent(EV_RUNEVBLOCK, EVB_ROOM, 0, EVROM_AFTERFADEIN);
}
}
static void game_loop_update_background_animation() {
if (_GP(play).bg_anim_delay > 0) _GP(play).bg_anim_delay--;
else if (_GP(play).bg_frame_locked);
else {
_GP(play).bg_anim_delay = _GP(play).anim_background_speed;
_GP(play).bg_frame++;
if ((size_t)_GP(play).bg_frame >= _GP(thisroom).BgFrameCount)
_GP(play).bg_frame = 0;
if (_GP(thisroom).BgFrameCount >= 2) {
// get the new frame's palette
on_background_frame_change();
}
}
}
static void game_loop_update_loop_counter() {
_G(loopcounter)++;
if (_GP(play).wait_counter > 0) _GP(play).wait_counter--;
if (_GP(play).shakesc_length > 0) _GP(play).shakesc_length--;
if (_G(loopcounter) % 5 == 0) {
update_ambient_sound_vol();
update_directional_sound_vol();
}
}
static void game_loop_update_fps() {
auto t2 = AGS_Clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - _G(t1));
auto frames = _G(loopcounter) - _G(lastcounter);
if (duration >= std::chrono::milliseconds(1000) && frames > 0) {
_G(fps) = 1000.0f * frames / duration.count();
_G(t1) = t2;
_G(lastcounter) = _G(loopcounter);
}
}
float get_current_fps() {
// if we have maxed out framerate then return the frame rate we're seeing instead
// fps must be greater that 0 or some timings will take forever.
if (isTimerFpsMaxed() && _G(fps) > 0.0f) {
return _G(fps);
}
return _G(frames_per_second);
}
void set_loop_counter(unsigned int new_counter) {
_G(loopcounter) = new_counter;
_G(t1) = AGS_Clock::now();
_G(lastcounter) = _G(loopcounter);
_G(fps) = std::numeric_limits<float>::quiet_NaN();
}
void UpdateGameOnce(bool checkControls, IDriverDependantBitmap *extraBitmap, int extraX, int extraY) {
int res;
sys_evt_process_pending();
_G(numEventsAtStartOfFunction) = _GP(events).size();
if (_G(want_exit)) {
ProperExit();
}
ccNotifyScriptStillAlive();
_G(our_eip) = 1;
game_loop_check_problems_at_start();
// if we're not fading in, don't count the fadeouts
if ((_GP(play).no_hicolor_fadein) && (_GP(game).options[OPT_FADETYPE] == FADE_NORMAL))
_GP(play).screen_is_faded_out = 0;
_G(our_eip) = 1014;
update_gui_disabled_status();
_G(our_eip) = 1004;
game_loop_do_early_script_update();
// run this immediately to make sure it gets done before fade-in
// (player enters screen)
check_new_room();
if (_G(abort_engine))
return;
_G(our_eip) = 1005;
res = game_loop_check_ground_level_interactions();
if (res != RETURN_CONTINUE) {
return;
}
_G(mouse_on_iface) = -1;
check_debug_keys();
// Handle player's input
// remember old mouse pos, needed for update_cursor_over_location() later
const int mwasatx = _G(mousex), mwasaty = _G(mousey);
// update mouse position (mousex, mousey)
ags_domouse();
// update gui under mouse; this also updates gui control focus;
// atm we must call this before "check_controls", because GUI interaction
// relies on remembering which control was focused by the cursor prior
update_cursor_over_gui();
// handle actual input (keys, mouse, and so forth)
game_loop_check_controls(checkControls);
if (_G(abort_engine))
return;
_G(our_eip) = 2;
// do the overall game state update
game_loop_do_update();
game_loop_update_animated_buttons();
game_loop_do_late_script_update();
update_audio_system_on_game_loop();
update_cursor_over_location(mwasatx, mwasaty);
update_cursor_view();
// Only render if we are not skipping a cutscene
if (!_GP(play).fast_forward)
render_graphics(extraBitmap, extraX, extraY);
_G(our_eip) = 6;
game_loop_update_events();
if (_G(abort_engine))
return;
_G(our_eip) = 7;
update_polled_stuff();
if (_G(abort_engine))
return;
game_loop_update_background_animation();
game_loop_update_loop_counter();
// Immediately start the next frame if we are skipping a cutscene
if (_GP(play).fast_forward)
return;
_G(our_eip) = 72;
game_loop_update_fps();
update_polled_stuff();
if (_G(abort_engine))
return;
WaitForNextFrame();
}
void UpdateGameAudioOnly() {
update_audio_system_on_game_loop();
game_loop_update_loop_counter();
game_loop_update_fps();
WaitForNextFrame();
}
static void UpdateMouseOverLocation() {
// Call GetLocationName - it will internally force a GUI refresh
// if the result it returns has changed from last time
char tempo[STD_BUFFER_SIZE];
GetLocationName(game_to_data_coord(_G(mousex)), game_to_data_coord(_G(mousey)), tempo);
if ((_GP(play).get_loc_name_save_cursor >= 0) &&
(_GP(play).get_loc_name_save_cursor != _GP(play).get_loc_name_last_time) &&
(_G(mouse_on_iface) < 0) && (_G(ifacepopped) < 0)) {
// we have saved the cursor, but the mouse location has changed
// and it's time to restore it
_GP(play).get_loc_name_save_cursor = -1;
set_cursor_mode(_GP(play).restore_cursor_mode_to);
if (_G(cur_mode) == _GP(play).restore_cursor_mode_to) {
// make sure it changed -- the new mode might have been disabled
// in which case don't change the image
set_mouse_cursor(_GP(play).restore_cursor_image_to);
}
debug_script_log("Restore mouse to mode %d cursor %d", _GP(play).restore_cursor_mode_to, _GP(play).restore_cursor_image_to);
}
}
// Checks if user interface should remain disabled for now
// Checks if user interface should remain disabled for now
static bool ShouldStayInWaitMode() {
if (_G(restrict_until).type == 0)
quit("end_wait_loop called but game not in loop_until state");
switch (_G(restrict_until).type) {
case UNTIL_MOVEEND: {
const short *wkptr = (const short *)_G(restrict_until).data_ptr;
return !(wkptr[0] < 1);
}
case UNTIL_CHARIS0: {
const char *chptr = (const char *)_G(restrict_until).data_ptr;
return !(chptr[0] == 0);
}
case UNTIL_NEGATIVE: {
const short *wkptr = (const short *)_G(restrict_until).data_ptr;
return !(wkptr[0] < 0);
}
case UNTIL_INTISNEG: {
const int *wkptr = (const int *)_G(restrict_until).data_ptr;
return !(wkptr[0] < 0);
}
case UNTIL_NOOVERLAY: {
return !(_GP(play).text_overlay_on == 0);
}
case UNTIL_INTIS0: {
const int *wkptr = (const int *)_G(restrict_until).data_ptr;
return !(wkptr[0] == 0);
}
case UNTIL_SHORTIS0: {
const short *wkptr = (const short *)_G(restrict_until).data_ptr;
return !(wkptr[0] == 0);
}
case UNTIL_ANIMBTNEND: {
// still animating?
return FindButtonAnimation(_G(restrict_until).data1, _G(restrict_until).data2) >= 0;
}
default:
quit("loop_until: unknown until event");
}
return true; // should stay in wait
}
static int UpdateWaitMode() {
if (_G(restrict_until).type == 0) {
return RETURN_CONTINUE;
}
if (!ShouldStayInWaitMode())
_G(restrict_until).type = 0;
_G(our_eip) = 77;
if (_G(restrict_until).type > 0) {
return RETURN_CONTINUE;
}
auto was_disabled_for = _G(restrict_until).disabled_for;
set_default_cursor();
// If GUI looks change when disabled, then mark all of them for redraw
GUI::MarkAllGUIForUpdate(GUI::Options.DisabledStyle != kGuiDis_Unchanged, true);
_GP(play).disabled_user_interface--;
_G(restrict_until).disabled_for = 0;
switch (was_disabled_for) {
// case FOR_ANIMATION:
// run_animation((FullAnimation*)user_disabled_data2,user_disabled_data3);
// break;
case FOR_EXITLOOP:
return -1;
case FOR_SCRIPT:
quit("err: for_script obsolete (v2.1 and earlier only)");
break;
default:
quit("Unknown user_disabled_for in end _G(restrict_until)");
}
// we shouldn't get here.
return RETURN_CONTINUE;
}
// Run single game iteration; calls UpdateGameOnce() internally
static int GameTick() {
if (_G(displayed_room) < 0)
quit("!A blocking function was called before the first room has been loaded");
UpdateGameOnce(true);
if (_G(abort_engine))
return -1;
UpdateMouseOverLocation();
_G(our_eip) = 76;
int res = UpdateWaitMode();
if (res == RETURN_CONTINUE) {
return 0;
} // continue looping
return res;
}
static void SetupLoopParameters(int untilwhat, const void *data_ptr = nullptr, int data1 = 0, int data2 = 0) {
_GP(play).disabled_user_interface++;
// If GUI looks change when disabled, then mark all of them for redraw
GUI::MarkAllGUIForUpdate(GUI::Options.DisabledStyle != kGuiDis_Unchanged, true);
// Only change the mouse cursor if it hasn't been specifically changed first
// (or if it's speech, always change it)
if (((_G(cur_cursor) == _G(cur_mode)) || (untilwhat == UNTIL_NOOVERLAY)) &&
(_G(cur_mode) != CURS_WAIT))
set_mouse_cursor(CURS_WAIT);
_G(restrict_until).type = untilwhat;
_G(restrict_until).data_ptr = data_ptr;
_G(restrict_until).data1 = data1;
_G(restrict_until).data2 = data2;
_G(restrict_until).disabled_for = FOR_EXITLOOP;
}
// This function is called from lot of various functions
// in the game core, character, room object etc
static void GameLoopUntilEvent(int untilwhat, const void *data_ptr = nullptr, int data1 = 0, int data2 = 0) {
// blocking cutscene - end skipping
EndSkippingUntilCharStops();
// this function can get called in a nested context, so
// remember the state of these vars in case a higher level
// call needs them
auto cached_restrict_until = _G(restrict_until);
SetupLoopParameters(untilwhat, data_ptr, data1, data2);
while (GameTick() == 0);
_G(our_eip) = 78;
_G(restrict_until) = cached_restrict_until;
}
void GameLoopUntilValueIsZero(const int8 *value) {
GameLoopUntilEvent(UNTIL_CHARIS0, value);
}
void GameLoopUntilValueIsZero(const short *value) {
GameLoopUntilEvent(UNTIL_SHORTIS0, value);
}
void GameLoopUntilValueIsZero(const int *value) {
GameLoopUntilEvent(UNTIL_INTIS0, value);
}
void GameLoopUntilValueIsZeroOrLess(const short *value) {
GameLoopUntilEvent(UNTIL_MOVEEND, value);
}
void GameLoopUntilValueIsNegative(const short *value) {
GameLoopUntilEvent(UNTIL_NEGATIVE, value);
}
void GameLoopUntilValueIsNegative(const int *value) {
GameLoopUntilEvent(UNTIL_INTISNEG, value);
}
void GameLoopUntilNotMoving(const short *move) {
GameLoopUntilEvent(UNTIL_MOVEEND, move);
}
void GameLoopUntilNoOverlay() {
GameLoopUntilEvent(UNTIL_NOOVERLAY);
}
void GameLoopUntilButAnimEnd(int guin, int objn) {
GameLoopUntilEvent(UNTIL_ANIMBTNEND, nullptr, guin, objn);
}
void RunGameUntilAborted() {
// skip ticks to account for time spent starting game.
skipMissedTicks();
while (!_G(abort_engine)) {
GameTick();
if (_G(load_new_game)) {
RunAGSGame(nullptr, _G(load_new_game), 0);
_G(load_new_game) = 0;
}
}
}
void update_cursor_and_dependent() {
const int mwasatx = _G(mousex), mwasaty = _G(mousey);
ags_domouse();
update_cursor_over_gui();
update_cursor_over_location(mwasatx, mwasaty);
update_cursor_view();
}
void update_polled_stuff() {
::AGS::g_events->pollEvents();
if (_G(want_exit)) {
_G(want_exit) = false;
quit("||exit!");
} else if (_G(editor_debugging_initialized))
check_for_messages_from_debugger();
}
} // namespace AGS3