1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 //
24 // Game loop
25 //
26 
27 #include "ags/lib/std/limits.h"
28 #include "ags/engine/ac/button.h"
29 #include "ags/shared/ac/common.h"
30 #include "ags/engine/ac/character_extras.h"
31 #include "ags/shared/ac/character_info.h"
32 #include "ags/engine/ac/draw.h"
33 #include "ags/engine/ac/event.h"
34 #include "ags/engine/ac/game.h"
35 #include "ags/engine/ac/game_setup.h"
36 #include "ags/shared/ac/game_setup_struct.h"
37 #include "ags/engine/ac/game_state.h"
38 #include "ags/engine/ac/global_debug.h"
39 #include "ags/engine/ac/global_display.h"
40 #include "ags/engine/ac/global_game.h"
41 #include "ags/engine/ac/global_gui.h"
42 #include "ags/engine/ac/global_region.h"
43 #include "ags/engine/ac/gui.h"
44 #include "ags/engine/ac/hotspot.h"
45 #include "ags/shared/ac/keycode.h"
46 #include "ags/engine/ac/mouse.h"
47 #include "ags/engine/ac/overlay.h"
48 #include "ags/shared/ac/sprite_cache.h"
49 #include "ags/engine/ac/sys_events.h"
50 #include "ags/engine/ac/room.h"
51 #include "ags/engine/ac/room_object.h"
52 #include "ags/engine/ac/room_status.h"
53 #include "ags/engine/debugging/debugger.h"
54 #include "ags/engine/debugging/debug_log.h"
55 #include "ags/engine/device/mouse_w32.h"
56 #include "ags/engine/gui/animating_gui_button.h"
57 #include "ags/shared/gui/gui_inv.h"
58 #include "ags/shared/gui/gui_main.h"
59 #include "ags/shared/gui/gui_textbox.h"
60 #include "ags/engine/main/engine.h"
61 #include "ags/engine/main/game_run.h"
62 #include "ags/engine/main/update.h"
63 #include "ags/engine/media/audio/audio_system.h"
64 #include "ags/engine/platform/base/ags_platform_driver.h"
65 #include "ags/plugins/ags_plugin.h"
66 #include "ags/plugins/plugin_engine.h"
67 #include "ags/engine/script/script.h"
68 #include "ags/engine/script/script_runtime.h"
69 #include "ags/events.h"
70 #include "ags/globals.h"
71 
72 namespace AGS3 {
73 
74 using namespace AGS::Shared;
75 
76 static int ShouldStayInWaitMode();
77 
78 #define UNTIL_ANIMEND   1
79 #define UNTIL_MOVEEND   2
80 #define UNTIL_CHARIS0   3
81 #define UNTIL_NOOVERLAY 4
82 #define UNTIL_NEGATIVE  5
83 #define UNTIL_INTIS0    6
84 #define UNTIL_SHORTIS0  7
85 #define UNTIL_INTISNEG  8
86 
ProperExit()87 static void ProperExit() {
88 	_G(want_exit) = 0;
89 	_G(proper_exit) = 1;
90 	quit("||exit!");
91 }
92 
game_loop_check_problems_at_start()93 static void game_loop_check_problems_at_start() {
94 	if ((_G(in_enters_screen) != 0) & (_G(displayed_room) == _G(starting_room)))
95 		quit("!A text script run in the Player Enters Screen event caused the\n"
96 		     "screen to be updated. If you need to use Wait(), do so in After Fadein");
97 	if ((_G(in_enters_screen) != 0) && (_G(done_es_error) == 0)) {
98 		debug_script_warn("Wait() was used in Player Enters Screen - use Enters Screen After Fadein instead");
99 		_G(done_es_error) = 1;
100 	}
101 	if (_G(no_blocking_functions))
102 		quit("!A blocking function was called from within a non-blocking event such as " REP_EXEC_ALWAYS_NAME);
103 }
104 
game_loop_check_new_room()105 static void game_loop_check_new_room() {
106 	if (_G(in_new_room) == 0) {
107 		// Run the room and game script repeatedly_execute
108 		run_function_on_non_blocking_thread(&_GP(repExecAlways));
109 		setevent(EV_TEXTSCRIPT, TS_REPEAT);
110 		setevent(EV_RUNEVBLOCK, EVB_ROOM, 0, 6);
111 	}
112 	// run this immediately to make sure it gets done before fade-in
113 	// (player enters screen)
114 	check_new_room();
115 }
116 
game_loop_do_late_update()117 static void game_loop_do_late_update() {
118 	if (_G(in_new_room) == 0) {
119 		// Run the room and game script late_repeatedly_execute
120 		run_function_on_non_blocking_thread(&_GP(lateRepExecAlways));
121 	}
122 }
123 
game_loop_check_ground_level_interactions()124 static int game_loop_check_ground_level_interactions() {
125 	if ((_GP(play).ground_level_areas_disabled & GLED_INTERACTION) == 0) {
126 		// check if he's standing on a hotspot
127 		int hotspotThere = get_hotspot_at(_G(playerchar)->x, _G(playerchar)->y);
128 		// run Stands on Hotspot event
129 		setevent(EV_RUNEVBLOCK, EVB_HOTSPOT, hotspotThere, 0);
130 
131 		// check current region
132 		int onRegion = GetRegionIDAtRoom(_G(playerchar)->x, _G(playerchar)->y);
133 		int inRoom = _G(displayed_room);
134 
135 		if (onRegion != _GP(play).player_on_region) {
136 			// we need to save this and set _GP(play).player_on_region
137 			// now, so it's correct going into RunRegionInteraction
138 			int oldRegion = _GP(play).player_on_region;
139 
140 			_GP(play).player_on_region = onRegion;
141 			// Walks Off last region
142 			if (oldRegion > 0)
143 				RunRegionInteraction(oldRegion, 2);
144 			// Walks Onto new region
145 			if (onRegion > 0)
146 				RunRegionInteraction(onRegion, 1);
147 		}
148 		if (_GP(play).player_on_region > 0)   // player stands on region
149 			RunRegionInteraction(_GP(play).player_on_region, 0);
150 
151 		// one of the region interactions sent us to another room
152 		if (inRoom != _G(displayed_room)) {
153 			check_new_room();
154 		}
155 
156 		// if in a Wait loop which is no longer valid (probably
157 		// because the Region interaction did a NewRoom), abort
158 		// the rest of the loop
159 		if ((_G(restrict_until)) && (!ShouldStayInWaitMode())) {
160 			// cancel the Rep Exec and Stands on Hotspot events that
161 			// we just added -- otherwise the event queue gets huge
162 			_G(numevents) = _G(numEventsAtStartOfFunction);
163 			return 0;
164 		}
165 	} // end if checking ground level interactions
166 
167 	return RETURN_CONTINUE;
168 }
169 
lock_mouse_on_click()170 static void lock_mouse_on_click() {
171 	if (_GP(usetup).mouse_auto_lock && _GP(scsystem).windowed)
172 		_GP(mouse).TryLockToWindow();
173 }
174 
toggle_mouse_lock()175 static void toggle_mouse_lock() {
176 	if (_GP(scsystem).windowed) {
177 		if (_GP(mouse).IsLockedToWindow())
178 			_GP(mouse).UnlockFromWindow();
179 		else
180 			_GP(mouse).TryLockToWindow();
181 	}
182 }
183 
184 // Runs default mouse button handling
check_mouse_controls()185 static void check_mouse_controls() {
186 	int mongu = -1;
187 
188 	mongu = gui_on_mouse_move();
189 
190 	_G(mouse_on_iface) = mongu;
191 	if ((_G(ifacepopped) >= 0) && (_G(mousey) >= _GP(guis)[_G(ifacepopped)].Y + _GP(guis)[_G(ifacepopped)].Height))
192 		remove_popup_interface(_G(ifacepopped));
193 
194 	// check mouse clicks on GUIs
195 	if ((_G(wasbutdown) > 0) && (ags_misbuttondown(_G(wasbutdown) - 1))) {
196 		gui_on_mouse_hold(_G(wasongui), _G(wasbutdown));
197 	} else if ((_G(wasbutdown) > 0) && (!ags_misbuttondown(_G(wasbutdown) - 1))) {
198 		gui_on_mouse_up(_G(wasongui), _G(wasbutdown));
199 		_G(wasbutdown) = 0;
200 	}
201 
202 	int mbut = MouseNone;
203 	int mwheelz = 0;
204 	if (run_service_mb_controls(mbut, mwheelz) && mbut >= 0) {
205 
206 		check_skip_cutscene_mclick(mbut);
207 
208 		if (_GP(play).fast_forward || _GP(play).IsIgnoringInput()) { /* do nothing if skipping cutscene or input disabled */
209 		} else if ((_GP(play).wait_counter != 0) && (_GP(play).key_skip_wait & SKIP_MOUSECLICK) != 0) {
210 			_GP(play).SetWaitSkipResult(SKIP_MOUSECLICK, mbut);
211 		} else if (_GP(play).text_overlay_on > 0) {
212 			if (_GP(play).cant_skip_speech & SKIP_MOUSECLICK) {
213 				remove_screen_overlay(_GP(play).text_overlay_on);
214 				_GP(play).SetWaitSkipResult(SKIP_MOUSECLICK, mbut);
215 			}
216 		} else if (!IsInterfaceEnabled());  // blocking cutscene, ignore mouse
217 		else if (pl_run_plugin_hooks(AGSE_MOUSECLICK, mbut + 1)) {
218 			// plugin took the click
219 			debug_script_log("Plugin handled mouse button %d", mbut + 1);
220 		} else if (mongu >= 0) {
221 			if (_G(wasbutdown) == 0) {
222 				gui_on_mouse_down(mongu, mbut + 1);
223 			}
224 			_G(wasongui) = mongu;
225 			_G(wasbutdown) = mbut + 1;
226 		} else setevent(EV_TEXTSCRIPT, TS_MCLICK, mbut + 1);
227 		//    else RunTextScriptIParam(_G(gameinst),"on_mouse_click",aa+1);
228 	}
229 
230 	if (mwheelz < 0)
231 		setevent(EV_TEXTSCRIPT, TS_MCLICK, 9);
232 	else if (mwheelz > 0)
233 		setevent(EV_TEXTSCRIPT, TS_MCLICK, 8);
234 }
235 
236 
237 
238 // Special flags to OR saved SDL_Keymod flags with:
239 // Mod key combination already fired (wait until full mod release)
240 #define KEY_MODS_FIRED      0x80000000
241 
242 int cur_key_mods = 0;
243 int old_key_mod = 0; // for saving previous key mods
244 
245 // Runs service key controls, returns false if service key combinations were handled
246 // and no more processing required, otherwise returns true and provides current keycode and key shifts.
run_service_key_controls(KeyInput & out_key)247 bool run_service_key_controls(KeyInput &out_key) {
248 	bool handled = false;
249 	const bool key_valid = ags_keyevent_ready();
250 	const Common::Event key_evt = key_valid ? ags_get_next_keyevent() : Common::Event();
251 	const bool is_only_mod_key = key_evt.type == Common::EVENT_KEYDOWN ?
252 		is_mod_key(key_evt.kbd.keycode) : false;
253 
254 	// Following section is for testing for pushed and released mod-keys.
255 	// A bit of explanation: some service actions may require combination of
256 	// mod-keys, for example [Ctrl + Alt] toggles mouse lock in window.
257 	// Here comes a problem: other actions may also use [Ctrl + Alt] mods in
258 	// combination with a third key: e.g. [Ctrl + Alt + V] displays engine info.
259 	// For this reason we cannot simply test for pressed Ctrl and Alt here,
260 	// but we must wait until player *releases at least one mod key* of this combo,
261 	// while no third key was pressed.
262 	// In other words, such action should only trigger if:
263 	// * if combination of held down mod-keys was gathered,
264 	// * if no other key was pressed meanwhile,
265 	// * if at least one of those gathered mod-keys was released.
266 	//
267 	// TODO: maybe split this mod handling into sep procedure and make it easier to use (not that it's used alot)?
268 
269 	// First, check mods
270 	const int cur_mod = make_merged_mod(key_evt.kbd.flags);
271 
272 	// If shifts combination have already triggered an action, then do nothing
273 	// until new shifts are empty, in which case reset saved shifts
274 	if (old_key_mod & KEY_MODS_FIRED) {
275 		if (cur_mod == 0)
276 			old_key_mod = 0;
277 	} else {
278 		// If any non-mod key is pressed, add fired flag to indicate that
279 		// this is no longer a pure mod keys combination
280 		if (key_valid && !is_only_mod_key) {
281 			old_key_mod = cur_mod | KEY_MODS_FIRED;
282 		}
283 		// If all the previously registered mods are still pressed,
284 		// then simply resave new mods state.
285 		else if ((old_key_mod & cur_mod) == old_key_mod) {
286 			old_key_mod = cur_mod;
287 		}
288 		// Otherwise some of the mods were released, then run key combo action
289 		// and set KEY_MODS_FIRED flag to prevent multiple execution
290 		else if (old_key_mod) {
291 			// Toggle mouse lock on Ctrl + Alt
292 			if (old_key_mod == (Common::KBD_CTRL | Common::KBD_ALT)) {
293 				toggle_mouse_lock();
294 				handled = true;
295 			}
296 			old_key_mod |= KEY_MODS_FIRED;
297 		}
298 	}
299 	cur_key_mods = cur_mod;
300 
301 	if (!key_valid)
302 		return false; // if there was no key press, finish after handling current mod state
303 	if (is_only_mod_key || handled)
304 		return false; // rest of engine currently does not use pressed mod keys
305 	// change this when it's no longer true (but be mindful about key-skipping!)
306 
307 	KeyInput ki = ags_keycode_from_scummvm(key_evt);
308 	eAGSKeyCode agskey = ki.Key;
309 	if (agskey == eAGSKeyCodeNone)
310 		return false; // should skip this key event
311 
312 	// LAlt or RAlt + Enter/Return
313 	if ((cur_mod == Common::KBD_ALT) && agskey == eAGSKeyCodeReturn) {
314 		engine_try_switch_windowed_gfxmode();
315 		return false;
316 	}
317 
318 	// Alt+X, abort (but only once game is loaded)
319 	if ((_G(displayed_room) >= 0) && (_GP(play).abort_key > 0 && agskey == _GP(play).abort_key)) {
320 		_G(check_dynamic_sprites_at_exit) = 0;
321 		quit("!|");
322 	}
323 
324 	// debug console
325 	if ((agskey == '`') && (_GP(play).debug_mode > 0)) {
326 		_G(display_console) = !_G(display_console);
327 		return false;
328 	}
329 
330 	if ((agskey == eAGSKeyCodeCtrlE) && (_G(display_fps) == kFPS_Forced)) {
331 		// if --fps paramter is used, Ctrl+E will max out frame rate
332 		setTimerFps(isTimerFpsMaxed() ? _G(frames_per_second) : 1000);
333 		return false;
334 	}
335 
336 	if ((agskey == eAGSKeyCodeCtrlD) && (_GP(play).debug_mode > 0)) {
337 		// ctrl+D - show info
338 		char infobuf[900];
339 		int ff;
340 		// MACPORT FIX 9/6/5: added last %s
341 		sprintf(infobuf, "In room %d %s[Player at %d, %d (view %d, loop %d, frame %d)%s%s%s",
342 		        _G(displayed_room), (_G(noWalkBehindsAtAll) ? "(has no walk-behinds)" : ""), _G(playerchar)->x, _G(playerchar)->y,
343 		        _G(playerchar)->view + 1, _G(playerchar)->loop, _G(playerchar)->frame,
344 		        (IsGamePaused() == 0) ? "" : "[Game paused.",
345 		        (_GP(play).ground_level_areas_disabled == 0) ? "" : "[Ground areas disabled.",
346 		        (IsInterfaceEnabled() == 0) ? "[Game in Wait state" : "");
347 		for (ff = 0; ff < _G(croom)->numobj; ff++) {
348 			if (ff >= 8) break; // buffer not big enough for more than 7
349 			sprintf(&infobuf[strlen(infobuf)],
350 			        "[Object %d: (%d,%d) size (%d x %d) on:%d moving:%s animating:%d slot:%d trnsp:%d clkble:%d",
351 			        ff, _G(objs)[ff].x, _G(objs)[ff].y,
352 			        (_GP(spriteset)[_G(objs)[ff].num] != nullptr) ? _GP(game).SpriteInfos[_G(objs)[ff].num].Width : 0,
353 			        (_GP(spriteset)[_G(objs)[ff].num] != nullptr) ? _GP(game).SpriteInfos[_G(objs)[ff].num].Height : 0,
354 			        _G(objs)[ff].on,
355 			        (_G(objs)[ff].moving > 0) ? "yes" : "no", _G(objs)[ff].cycling,
356 			        _G(objs)[ff].num, _G(objs)[ff].transparent,
357 			        ((_G(objs)[ff].flags & OBJF_NOINTERACT) != 0) ? 0 : 1);
358 		}
359 		Display(infobuf);
360 		int chd = _GP(game).playercharacter;
361 		char bigbuffer[STD_BUFFER_SIZE] = "CHARACTERS IN THIS ROOM:[";
362 		for (ff = 0; ff < _GP(game).numcharacters; ff++) {
363 			if (_GP(game).chars[ff].room != _G(displayed_room)) continue;
364 			if (strlen(bigbuffer) > 430) {
365 				strcat(bigbuffer, "and more...");
366 				Display(bigbuffer);
367 				strcpy(bigbuffer, "CHARACTERS IN THIS ROOM (cont'd):[");
368 			}
369 			chd = ff;
370 			sprintf(&bigbuffer[strlen(bigbuffer)],
371 			        "%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)[",
372 			        _GP(game).chars[chd].scrname, _GP(game).chars[chd].view + 1, _GP(game).chars[chd].loop, _GP(game).chars[chd].frame,
373 			        _GP(game).chars[chd].x, _GP(game).chars[chd].y, _GP(game).chars[chd].z,
374 			        _GP(game).chars[chd].idleview, _GP(game).chars[chd].idletime, _GP(game).chars[chd].idleleft,
375 			        _GP(game).chars[chd].walking, _GP(game).chars[chd].animating, _GP(game).chars[chd].following,
376 			        _GP(game).chars[chd].flags, _GP(game).chars[chd].wait, _G(charextra)[chd].zoom);
377 		}
378 		Display(bigbuffer);
379 		return false;
380 	}
381 
382 	if (((agskey == eAGSKeyCodeCtrlV) && (cur_key_mods & Common::KBD_ALT) != 0)
383 	        && (_GP(play).wait_counter < 1) && (_GP(play).text_overlay_on == 0) && (_G(restrict_until) == 0)) {
384 		// make sure we can't interrupt a Wait()
385 		// and desync the music to cutscene
386 		_GP(play).debug_mode++;
387 		script_debug(1, 0);
388 		_GP(play).debug_mode--;
389 		return false;
390 	}
391 
392 	// No service operation triggered? return active keypress and mods to caller
393 	out_key = ki;
394 	return true;
395 }
396 
run_service_mb_controls(int & mbut,int & mwheelz)397 bool run_service_mb_controls(int &mbut, int &mwheelz) {
398 	int mb = ags_mgetbutton();
399 	int mz = ags_check_mouse_wheel();
400 	if (mb == MouseNone && mz == 0)
401 		return false;
402 	lock_mouse_on_click(); // do not claim
403 	mbut = mb;
404 	mwheelz = mz;
405 	return true;
406 }
407 
408 // Runs default keyboard handling
check_keyboard_controls()409 static void check_keyboard_controls() {
410 	// First check for service engine's combinations (mouse lock, display mode switch, and so forth)
411 	KeyInput ki;
412 	if (!run_service_key_controls(ki)) {
413 		return;
414 	}
415 	eAGSKeyCode kgn = ki.Key;
416 	// Then, check cutscene skip
417 	check_skip_cutscene_keypress(kgn);
418 	if (_GP(play).fast_forward) {
419 		return;
420 	}
421 	if (_GP(play).IsIgnoringInput()) {
422 		return;
423 	}
424 	// Now check for in-game controls
425 	if (pl_run_plugin_hooks(AGSE_KEYPRESS, kgn)) {
426 		// plugin took the keypress
427 		debug_script_log("Keypress code %d taken by plugin", kgn);
428 		return;
429 	}
430 
431 	// skip speech if desired by Speech.SkipStyle
432 	if ((_GP(play).text_overlay_on > 0) && (_GP(play).cant_skip_speech & SKIP_KEYPRESS)) {
433 		// only allow a key to remove the overlay if the icon bar isn't up
434 		if (IsGamePaused() == 0) {
435 			// check if it requires a specific keypress
436 			if ((_GP(play).skip_speech_specific_key > 0) &&
437 				(kgn != _GP(play).skip_speech_specific_key)) {
438 			} else {
439 				remove_screen_overlay(_GP(play).text_overlay_on);
440 				_GP(play).SetWaitSkipResult(SKIP_KEYPRESS, kgn);
441 			}
442 		}
443 
444 		return;
445 	}
446 
447 	if ((_GP(play).wait_counter != 0) && (_GP(play).key_skip_wait & SKIP_KEYPRESS) != 0) {
448 		_GP(play).SetWaitSkipResult(SKIP_KEYPRESS, kgn);
449 		return;
450 	}
451 
452 	if (_G(inside_script)) {
453 		// Don't queue up another keypress if it can't be run instantly
454 		debug_script_log("Keypress %d ignored (game blocked)", kgn);
455 		return;
456 	}
457 
458 	int keywasprocessed = 0;
459 
460 	// determine if a GUI Text Box should steal the click
461 	// it should do if a displayable character (32-255) is
462 	// pressed, but exclude control characters (<32) and
463 	// extended keys (eg. up/down arrow; 256+)
464 	if ((((kgn >= 32) && (kgn <= 255) && (kgn != '[')) || (kgn == eAGSKeyCodeReturn) || (kgn == eAGSKeyCodeBackspace))
465 	        && !_G(all_buttons_disabled)) {
466 		for (int guiIndex = 0; guiIndex < _GP(game).numgui; guiIndex++) {
467 			auto &gui = _GP(guis)[guiIndex];
468 
469 			if (!gui.IsDisplayed()) continue;
470 
471 			for (int controlIndex = 0; controlIndex < gui.GetControlCount(); controlIndex++) {
472 				// not a text box, ignore it
473 				if (gui.GetControlType(controlIndex) != kGUITextBox) {
474 					continue;
475 				}
476 
477 				auto *guitex = static_cast<GUITextBox *>(gui.GetControl(controlIndex));
478 				if (guitex == nullptr) {
479 					continue;
480 				}
481 
482 				// if the text box is disabled, it cannot accept keypresses
483 				if (!guitex->IsEnabled()) {
484 					continue;
485 				}
486 				if (!guitex->IsVisible()) {
487 					continue;
488 				}
489 
490 				keywasprocessed = 1;
491 
492 				guitex->OnKeyPress(ki);
493 
494 				if (guitex->IsActivated) {
495 					guitex->IsActivated = false;
496 					setevent(EV_IFACECLICK, guiIndex, controlIndex, 1);
497 				}
498 			}
499 		}
500 	}
501 
502 	if (!keywasprocessed) {
503 		int sckey = AGSKeyToScriptKey(kgn);
504 		debug_script_log("Running on_key_press keycode %d", sckey);
505 		setevent(EV_TEXTSCRIPT, TS_KEYPRESS, sckey);
506 	}
507 
508 	// RunTextScriptIParam(_G(gameinst),"on_key_press",kgn);
509 }
510 
511 // check_controls: checks mouse & keyboard interface
check_controls()512 static void check_controls() {
513 	_G(our_eip) = 1007;
514 
515 	sys_evt_process_pending();
516 
517 	check_mouse_controls();
518 	check_keyboard_controls();
519 }
520 
check_room_edges(int numevents_was)521 static void check_room_edges(int numevents_was) {
522 	if ((IsInterfaceEnabled()) && (IsGamePaused() == 0) &&
523 	        (_G(in_new_room) == 0) && (_G(new_room_was) == 0)) {
524 		// Only allow walking off edges if not in wait mode, and
525 		// if not in Player Enters Screen (allow walking in from off-screen)
526 		int edgesActivated[4] = { 0, 0, 0, 0 };
527 		// Only do it if nothing else has happened (eg. mouseclick)
528 		if ((_G(numevents) == numevents_was) &&
529 		        ((_GP(play).ground_level_areas_disabled & GLED_INTERACTION) == 0)) {
530 
531 			if (_G(playerchar)->x <= _GP(thisroom).Edges.Left)
532 				edgesActivated[0] = 1;
533 			else if (_G(playerchar)->x >= _GP(thisroom).Edges.Right)
534 				edgesActivated[1] = 1;
535 			if (_G(playerchar)->y >= _GP(thisroom).Edges.Bottom)
536 				edgesActivated[2] = 1;
537 			else if (_G(playerchar)->y <= _GP(thisroom).Edges.Top)
538 				edgesActivated[3] = 1;
539 
540 			if ((_GP(play).entered_edge >= 0) && (_GP(play).entered_edge <= 3)) {
541 				// once the player is no longer outside the edge, forget the stored edge
542 				if (edgesActivated[_GP(play).entered_edge] == 0)
543 					_GP(play).entered_edge = -10;
544 				// if we are walking in from off-screen, don't activate edges
545 				else
546 					edgesActivated[_GP(play).entered_edge] = 0;
547 			}
548 
549 			for (int ii = 0; ii < 4; ii++) {
550 				if (edgesActivated[ii])
551 					setevent(EV_RUNEVBLOCK, EVB_ROOM, 0, ii);
552 			}
553 		}
554 	}
555 	_G(our_eip) = 1008;
556 
557 }
558 
game_loop_check_controls(bool checkControls)559 static void game_loop_check_controls(bool checkControls) {
560 	// don't let the player do anything before the screen fades in
561 	if ((_G(in_new_room) == 0) && (checkControls)) {
562 		int inRoom = _G(displayed_room);
563 		int numevents_was = _G(numevents);
564 		check_controls();
565 		check_room_edges(numevents_was);
566 
567 		if (_G(abort_engine))
568 			return;
569 
570 		// If an inventory interaction changed the room
571 		if (inRoom != _G(displayed_room))
572 			check_new_room();
573 	}
574 }
575 
game_loop_do_update()576 static void game_loop_do_update() {
577 	if (_G(debug_flags) & DBG_NOUPDATE);
578 	else if (_G(game_paused) == 0) update_stuff();
579 }
580 
game_loop_update_animated_buttons()581 static void game_loop_update_animated_buttons() {
582 	// update animating GUI buttons
583 	// this bit isn't in update_stuff because it always needs to
584 	// happen, even when the game is paused
585 	for (int aa = 0; aa < _G(numAnimButs); aa++) {
586 		if (UpdateAnimatingButton(aa)) {
587 			StopButtonAnimation(aa);
588 			aa--;
589 		}
590 	}
591 }
592 
game_loop_do_render_and_check_mouse(IDriverDependantBitmap * extraBitmap,int extraX,int extraY)593 static void game_loop_do_render_and_check_mouse(IDriverDependantBitmap *extraBitmap, int extraX, int extraY) {
594 	if (!_GP(play).fast_forward) {
595 		int mwasatx = _G(mousex), mwasaty = _G(mousey);
596 
597 		// Only do this if we are not skipping a cutscene
598 		render_graphics(extraBitmap, extraX, extraY);
599 
600 		// Check Mouse Moves Over Hotspot event
601 		// TODO: move this out of render related function? find out why we remember mwasatx and mwasaty before render
602 		// TODO: do not use static variables!
603 		// TODO: if we support rotation then we also need to compare full transform!
604 		if (_G(displayed_room) < 0)
605 			return;
606 		auto view = _GP(play).GetRoomViewportAt(_G(mousex), _G(mousey));
607 		auto cam = view ? view->GetCamera() : nullptr;
608 		if (cam) {
609 			// NOTE: all cameras are in same room right now, so their positions are in same coordinate system;
610 			// therefore we may use this as an indication that mouse is over different camera too.
611 			static int offsetxWas = -1000, offsetyWas = -1000;
612 			int offsetx = cam->GetRect().Left;
613 			int offsety = cam->GetRect().Top;
614 
615 			if (((mwasatx != _G(mousex)) || (mwasaty != _G(mousey)) ||
616 			        (offsetxWas != offsetx) || (offsetyWas != offsety))) {
617 				// mouse moves over hotspot
618 				if (__GetLocationType(game_to_data_coord(_G(mousex)), game_to_data_coord(_G(mousey)), 1) == LOCTYPE_HOTSPOT) {
619 					int onhs = _G(getloctype_index);
620 
621 					setevent(EV_RUNEVBLOCK, EVB_HOTSPOT, onhs, 6);
622 				}
623 			}
624 
625 			offsetxWas = offsetx;
626 			offsetyWas = offsety;
627 		} // camera found under mouse
628 	}
629 }
630 
game_loop_update_events()631 static void game_loop_update_events() {
632 	_G(new_room_was) = _G(in_new_room);
633 	if (_G(in_new_room) > 0)
634 		setevent(EV_FADEIN, 0, 0, 0);
635 	_G(in_new_room) = 0;
636 	update_events();
637 	if (!_G(abort_engine) && (_G(new_room_was) > 0) && (_G(in_new_room) == 0)) {
638 		// if in a new room, and the room wasn't just changed again in update_events,
639 		// then queue the Enters Screen scripts
640 		// run these next time round, when it's faded in
641 		if (_G(new_room_was) == 2)  // first time enters screen
642 			setevent(EV_RUNEVBLOCK, EVB_ROOM, 0, 4);
643 		if (_G(new_room_was) != 3)   // enters screen after fadein
644 			setevent(EV_RUNEVBLOCK, EVB_ROOM, 0, 7);
645 	}
646 }
647 
game_loop_update_background_animation()648 static void game_loop_update_background_animation() {
649 	if (_GP(play).bg_anim_delay > 0) _GP(play).bg_anim_delay--;
650 	else if (_GP(play).bg_frame_locked);
651 	else {
652 		_GP(play).bg_anim_delay = _GP(play).anim_background_speed;
653 		_GP(play).bg_frame++;
654 		if ((size_t)_GP(play).bg_frame >= _GP(thisroom).BgFrameCount)
655 			_GP(play).bg_frame = 0;
656 		if (_GP(thisroom).BgFrameCount >= 2) {
657 			// get the new frame's palette
658 			on_background_frame_change();
659 		}
660 	}
661 }
662 
game_loop_update_loop_counter()663 static void game_loop_update_loop_counter() {
664 	_G(loopcounter)++;
665 
666 	if (_GP(play).wait_counter > 0) _GP(play).wait_counter--;
667 	if (_GP(play).shakesc_length > 0) _GP(play).shakesc_length--;
668 
669 	if (_G(loopcounter) % 5 == 0) {
670 		update_ambient_sound_vol();
671 		update_directional_sound_vol();
672 	}
673 }
674 
game_loop_update_fps()675 static void game_loop_update_fps() {
676 	auto t2 = AGS_Clock::now();
677 	auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - _G(t1));
678 	auto frames = _G(loopcounter) - _G(lastcounter);
679 
680 	if (duration >= std::chrono::milliseconds(1000) && frames > 0) {
681 		_G(fps) = 1000.0f * frames / duration.count();
682 		_G(t1) = t2;
683 		_G(lastcounter) = _G(loopcounter);
684 	}
685 }
686 
get_current_fps()687 float get_current_fps() {
688 	// if we have maxed out framerate then return the frame rate we're seeing instead
689 	// fps must be greater that 0 or some timings will take forever.
690 	if (isTimerFpsMaxed() && _G(fps) > 0.0f) {
691 		return _G(fps);
692 	}
693 	return _G(frames_per_second);
694 }
695 
set_loop_counter(unsigned int new_counter)696 void set_loop_counter(unsigned int new_counter) {
697 	_G(loopcounter) = new_counter;
698 	_G(t1) = AGS_Clock::now();
699 	_G(lastcounter) = _G(loopcounter);
700 	_G(fps) = std::numeric_limits<float>::quiet_undefined();
701 }
702 
UpdateGameOnce(bool checkControls,IDriverDependantBitmap * extraBitmap,int extraX,int extraY)703 void UpdateGameOnce(bool checkControls, IDriverDependantBitmap *extraBitmap, int extraX, int extraY) {
704 
705 	int res;
706 
707 	sys_evt_process_pending();
708 
709 	_G(numEventsAtStartOfFunction) = _G(numevents);
710 
711 	if (_G(want_exit)) {
712 		ProperExit();
713 	}
714 
715 	ccNotifyScriptStillAlive();
716 	_G(our_eip) = 1;
717 
718 	game_loop_check_problems_at_start();
719 
720 	// if we're not fading in, don't count the fadeouts
721 	if ((_GP(play).no_hicolor_fadein) && (_GP(game).options[OPT_FADETYPE] == FADE_NORMAL))
722 		_GP(play).screen_is_faded_out = 0;
723 
724 	_G(our_eip) = 1014;
725 
726 	update_gui_disabled_status();
727 
728 	_G(our_eip) = 1004;
729 
730 	game_loop_check_new_room();
731 	if (_G(abort_engine))
732 		return;
733 
734 	_G(our_eip) = 1005;
735 
736 	res = game_loop_check_ground_level_interactions();
737 	if (res != RETURN_CONTINUE) {
738 		return;
739 	}
740 
741 	_G(mouse_on_iface) = -1;
742 
743 	check_debug_keys();
744 
745 	game_loop_check_controls(checkControls);
746 
747 	if (_G(abort_engine))
748 		return;
749 
750 	_G(our_eip) = 2;
751 
752 	game_loop_do_update();
753 
754 	game_loop_update_animated_buttons();
755 
756 	game_loop_do_late_update();
757 
758 	update_audio_system_on_game_loop();
759 
760 	game_loop_do_render_and_check_mouse(extraBitmap, extraX, extraY);
761 
762 	_G(our_eip) = 6;
763 
764 	game_loop_update_events();
765 
766 	if (_G(abort_engine))
767 		return;
768 
769 	_G(our_eip) = 7;
770 
771 	update_polled_stuff_if_runtime();
772 	if (_G(abort_engine))
773 		return;
774 
775 	game_loop_update_background_animation();
776 
777 	game_loop_update_loop_counter();
778 
779 	// Immediately start the next frame if we are skipping a cutscene
780 	if (_GP(play).fast_forward)
781 		return;
782 
783 	_G(our_eip) = 72;
784 
785 	game_loop_update_fps();
786 
787 	update_polled_stuff_if_runtime();
788 	if (_G(abort_engine))
789 		return;
790 
791 	WaitForNextFrame();
792 }
793 
UpdateMouseOverLocation()794 static void UpdateMouseOverLocation() {
795 	// Call GetLocationName - it will internally force a GUI refresh
796 	// if the result it returns has changed from last time
797 	char tempo[STD_BUFFER_SIZE];
798 	GetLocationName(game_to_data_coord(_G(mousex)), game_to_data_coord(_G(mousey)), tempo);
799 
800 	if ((_GP(play).get_loc_name_save_cursor >= 0) &&
801 	        (_GP(play).get_loc_name_save_cursor != _GP(play).get_loc_name_last_time) &&
802 	        (_G(mouse_on_iface) < 0) && (_G(ifacepopped) < 0)) {
803 		// we have saved the cursor, but the mouse location has changed
804 		// and it's time to restore it
805 		_GP(play).get_loc_name_save_cursor = -1;
806 		set_cursor_mode(_GP(play).restore_cursor_mode_to);
807 
808 		if (_G(cur_mode) == _GP(play).restore_cursor_mode_to) {
809 			// make sure it changed -- the new mode might have been disabled
810 			// in which case don't change the image
811 			set_mouse_cursor(_GP(play).restore_cursor_image_to);
812 		}
813 		debug_script_log("Restore mouse to mode %d cursor %d", _GP(play).restore_cursor_mode_to, _GP(play).restore_cursor_image_to);
814 	}
815 }
816 
817 // Checks if user interface should remain disabled for now
ShouldStayInWaitMode()818 static int ShouldStayInWaitMode() {
819 	if (_G(restrict_until) == 0)
820 		quit("end_wait_loop called but game not in loop_until state");
821 	int retval = _G(restrict_until);
822 
823 	if (_G(restrict_until) == UNTIL_MOVEEND) {
824 		const int16 *wkptr = (const int16 *)_G(user_disabled_data);
825 		if (wkptr[0] < 1) retval = 0;
826 	} else if (_G(restrict_until) == UNTIL_CHARIS0) {
827 		const char *chptr = (const char *)_G(user_disabled_data);
828 		if (chptr[0] == 0) retval = 0;
829 	} else if (_G(restrict_until) == UNTIL_NEGATIVE) {
830 		const int16 *wkptr = (const int16 *)_G(user_disabled_data);
831 		if (wkptr[0] < 0) retval = 0;
832 	} else if (_G(restrict_until) == UNTIL_INTISNEG) {
833 		const int *wkptr = (const int *)_G(user_disabled_data);
834 		if (wkptr[0] < 0) retval = 0;
835 	} else if (_G(restrict_until) == UNTIL_NOOVERLAY) {
836 		if (_GP(play).text_overlay_on == 0) retval = 0;
837 	} else if (_G(restrict_until) == UNTIL_INTIS0) {
838 		const int *wkptr = (const int *)_G(user_disabled_data);
839 		if (wkptr[0] == 0) retval = 0;
840 	} else if (_G(restrict_until) == UNTIL_SHORTIS0) {
841 		const int16 *wkptr = (const int16 *)_G(user_disabled_data);
842 		if (wkptr[0] == 0) retval = 0;
843 	} else quit("loop_until: unknown until event");
844 
845 	return retval;
846 }
847 
UpdateWaitMode()848 static int UpdateWaitMode() {
849 	if (_G(restrict_until) == 0) {
850 		return RETURN_CONTINUE;
851 	}
852 
853 	_G(restrict_until) = ShouldStayInWaitMode();
854 	_G(our_eip) = 77;
855 
856 	if (_G(restrict_until) != 0) {
857 		return RETURN_CONTINUE;
858 	}
859 
860 	auto was_disabled_for = _G(user_disabled_for);
861 
862 	set_default_cursor();
863 	if (_G(gui_disabled_style) != GUIDIS_UNCHANGED) { // If GUI looks change when disabled, then update them all
864 		GUI::MarkAllGUIForUpdate();
865 	}
866 	_GP(play).disabled_user_interface--;
867 	_G(user_disabled_for) = 0;
868 
869 	switch (was_disabled_for) {
870 	// case FOR_ANIMATION:
871 	//     run_animation((FullAnimation*)user_disabled_data2,user_disabled_data3);
872 	//     break;
873 	case FOR_EXITLOOP:
874 		return -1;
875 	case FOR_SCRIPT:
876 		quit("err: for_script obsolete (v2.1 and earlier only)");
877 		break;
878 	default:
879 		quit("Unknown _G(user_disabled_for) in end _G(restrict_until)");
880 	}
881 
882 	// we shouldn't get here.
883 	return RETURN_CONTINUE;
884 }
885 
886 // Run single game iteration; calls UpdateGameOnce() internally
GameTick()887 static int GameTick() {
888 	if (_G(displayed_room) < 0)
889 		quit("!A blocking function was called before the first room has been loaded");
890 
891 	UpdateGameOnce(true);
892 
893 	if (_G(abort_engine))
894 		return -1;
895 
896 	UpdateMouseOverLocation();
897 
898 	_G(our_eip) = 76;
899 
900 	int res = UpdateWaitMode();
901 	if (res == RETURN_CONTINUE) {
902 		return 0;
903 	} // continue looping
904 	return res;
905 }
906 
SetupLoopParameters(int untilwhat,const void * udata)907 static void SetupLoopParameters(int untilwhat, const void *udata) {
908 	_GP(play).disabled_user_interface++;
909 	if (_G(gui_disabled_style) != GUIDIS_UNCHANGED) { // If GUI looks change when disabled, then update them all
910 		GUI::MarkAllGUIForUpdate();
911 	}
912 	// Only change the mouse cursor if it hasn't been specifically changed first
913 	// (or if it's speech, always change it)
914 	if (((_G(cur_cursor) == _G(cur_mode)) || (untilwhat == UNTIL_NOOVERLAY)) &&
915 	        (_G(cur_mode) != CURS_WAIT))
916 		set_mouse_cursor(CURS_WAIT);
917 
918 	_G(restrict_until) = untilwhat;
919 	_G(user_disabled_data) = udata;
920 	_G(user_disabled_for) = FOR_EXITLOOP;
921 }
922 
923 // This function is called from lot of various functions
924 // in the game core, character, room object etc
GameLoopUntilEvent(int untilwhat,const void * daaa)925 static void GameLoopUntilEvent(int untilwhat, const void *daaa) {
926 	// blocking cutscene - end skipping
927 	EndSkippingUntilCharStops();
928 
929 	// this function can get called in a nested context, so
930 	// remember the state of these vars in case a higher level
931 	// call needs them
932 	auto cached_restrict_until = _G(restrict_until);
933 	auto cached_user_disabled_data = _G(user_disabled_data);
934 	auto cached_user_disabled_for = _G(user_disabled_for);
935 
936 	SetupLoopParameters(untilwhat, daaa);
937 	while (GameTick() == 0 && !_G(abort_engine)) {}
938 
939 	_G(our_eip) = 78;
940 
941 	_G(restrict_until) = cached_restrict_until;
942 	_G(user_disabled_data) = cached_user_disabled_data;
943 	_G(user_disabled_for) = cached_user_disabled_for;
944 }
945 
GameLoopUntilValueIsZero(const int8 * value)946 void GameLoopUntilValueIsZero(const int8 *value) {
947 	GameLoopUntilEvent(UNTIL_CHARIS0, value);
948 }
949 
GameLoopUntilValueIsZero(const short * value)950 void GameLoopUntilValueIsZero(const short *value) {
951 	GameLoopUntilEvent(UNTIL_SHORTIS0, value);
952 }
953 
GameLoopUntilValueIsZero(const int * value)954 void GameLoopUntilValueIsZero(const int *value) {
955 	GameLoopUntilEvent(UNTIL_INTIS0, value);
956 }
957 
GameLoopUntilValueIsZeroOrLess(const short * value)958 void GameLoopUntilValueIsZeroOrLess(const short *value) {
959 	GameLoopUntilEvent(UNTIL_MOVEEND, value);
960 }
961 
GameLoopUntilValueIsNegative(const short * value)962 void GameLoopUntilValueIsNegative(const short *value) {
963 	GameLoopUntilEvent(UNTIL_NEGATIVE, value);
964 }
965 
GameLoopUntilValueIsNegative(const int * value)966 void GameLoopUntilValueIsNegative(const int *value) {
967 	GameLoopUntilEvent(UNTIL_INTISNEG, value);
968 }
969 
GameLoopUntilNotMoving(const short * move)970 void GameLoopUntilNotMoving(const short *move) {
971 	GameLoopUntilEvent(UNTIL_MOVEEND, move);
972 }
973 
GameLoopUntilNoOverlay()974 void GameLoopUntilNoOverlay() {
975 	GameLoopUntilEvent(UNTIL_NOOVERLAY, 0);
976 }
977 
978 
979 
RunGameUntilAborted()980 void RunGameUntilAborted() {
981 	// skip ticks to account for time spent starting _GP(game).
982 	skipMissedTicks();
983 
984 	while (!_G(abort_engine)) {
985 		GameTick();
986 
987 		if (_G(load_new_game)) {
988 			RunAGSGame(nullptr, _G(load_new_game), 0);
989 			_G(load_new_game) = 0;
990 		}
991 	}
992 }
993 
update_polled_stuff_if_runtime()994 void update_polled_stuff_if_runtime() {
995 	::AGS::g_events->pollEvents();
996 
997 	if (_G(want_exit)) {
998 		_G(want_exit) = 0;
999 		quit("||exit!");
1000 
1001 	} else if (_G(editor_debugging_initialized))
1002 		check_for_messages_from_editor();
1003 }
1004 
1005 } // namespace AGS3
1006