2010-10-12 10:55:23 +02:00
|
|
|
/*
|
2019-08-03 19:43:48 +02:00
|
|
|
* This software is licensed under the terms of the MIT License.
|
2017-02-10 23:05:22 +01:00
|
|
|
* See COPYING for further information.
|
2011-03-05 13:44:21 +01:00
|
|
|
* ---
|
2024-05-16 23:30:41 +02:00
|
|
|
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
|
|
|
|
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
|
2010-10-12 10:55:23 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "stage.h"
|
2024-05-17 04:41:28 +02:00
|
|
|
|
|
|
|
#include "audio/audio.h"
|
|
|
|
#include "common_tasks.h" // IWYU pragma: keep
|
|
|
|
#include "config.h"
|
|
|
|
#include "dynstage.h"
|
|
|
|
#include "eventloop/eventloop.h"
|
|
|
|
#include "events.h"
|
2010-10-12 10:55:23 +02:00
|
|
|
#include "global.h"
|
2024-05-17 04:41:28 +02:00
|
|
|
#include "lasers/draw.h"
|
|
|
|
#include "log.h"
|
|
|
|
#include "menu/gameovermenu.h"
|
|
|
|
#include "menu/ingamemenu.h"
|
|
|
|
#include "player.h"
|
2023-04-07 07:57:49 +02:00
|
|
|
#include "replay/demoplayer.h"
|
2021-05-30 03:26:21 +02:00
|
|
|
#include "replay/stage.h"
|
2023-04-07 07:57:49 +02:00
|
|
|
#include "replay/state.h"
|
2021-05-30 03:26:21 +02:00
|
|
|
#include "replay/struct.h"
|
2024-05-17 04:41:28 +02:00
|
|
|
#include "resource/bgm.h"
|
2017-04-07 14:20:45 +02:00
|
|
|
#include "stagedraw.h"
|
2020-05-16 22:41:54 +02:00
|
|
|
#include "stageinfo.h"
|
2024-05-17 04:41:28 +02:00
|
|
|
#include "stageobjects.h"
|
|
|
|
#include "stagetext.h"
|
|
|
|
#include "util/env.h"
|
2024-05-04 12:47:00 +02:00
|
|
|
#include "watchdog.h"
|
2017-02-11 12:38:50 +01:00
|
|
|
|
2020-06-24 19:51:00 +02:00
|
|
|
typedef struct StageFrameState {
|
|
|
|
StageInfo *stage;
|
2023-03-22 23:41:32 +01:00
|
|
|
ResourceGroup *rg;
|
2020-06-24 19:51:00 +02:00
|
|
|
CallChain cc;
|
|
|
|
CoSched sched;
|
2022-01-09 13:11:26 +01:00
|
|
|
Replay *quicksave;
|
2022-01-28 17:03:22 +01:00
|
|
|
bool quicksave_is_automatic;
|
2022-01-09 13:11:26 +01:00
|
|
|
bool quickload_requested;
|
2023-06-13 04:08:06 +02:00
|
|
|
bool was_skipping;
|
stage: fix pause menu related crashes
There were two distinct things going on here:
1. If we receive multiple buffered TE_GAME_PAUSE events in the same
frame, we'd process all of them and create a pause menu for each.
This could theoretically overflow the evloop stack and crash the
game.
2. The `stage_comain` task starts before scheduling the stage main
loop via `eventloop_enter`. It initializes systems that depend on
tasks, and then immediatelly enters its per-frame async loop,
finishing its first iteration before yielding back to `_stage_enter`
and thus allowing `eventloop_enter` to be finally called. If there is
a TE_GAME_PAUSE event in the queue at this point, it would be handled
right there, and a pause menu would be created before the stage main
loop is scheduled. This messes things up quite a bit, leaking a
"zombie" pause menu into the evloop stack. After the stage is
destroyed, the evloop would try to switch to the frame created for
this menu. The menu's draw function would then attempt to reference
free'd resources of the destroyed stage, crashing the game. This
crash has actually been observed and reported (thanks @0kalekale)
To fix #1, the stage now tracks its paused state and refuses to open a
pause menu if one already exists.
To fix #2, `stage_comain` now yields before starting its async loop, to
let the stage set up its main loop early.
Note that because the stage main loop runs all coroutine tasks before
incrementing the frame counter, `stage_comain`'s per-frame logic would
execute twice on frame 0. This is obviously wrong, but this behavior
must be preserved to maintain compatibility with v1.4 replays. For that
reason, the `stage_comain` loop now skips its first YIELD. This hack can
be removed once v1.4 compat is no longer a concern.
2024-04-20 21:27:31 +02:00
|
|
|
bool paused;
|
2022-01-28 17:03:22 +01:00
|
|
|
uint32_t dynstage_generation;
|
2020-06-24 19:51:00 +02:00
|
|
|
int transition_delay;
|
2021-05-30 03:26:21 +02:00
|
|
|
int desync_check_freq;
|
2020-06-24 19:51:00 +02:00
|
|
|
uint16_t last_replay_fps;
|
|
|
|
float view_shake;
|
2022-01-09 13:11:26 +01:00
|
|
|
int bgm_start_time;
|
|
|
|
double bgm_start_pos;
|
2020-06-24 19:51:00 +02:00
|
|
|
} StageFrameState;
|
|
|
|
|
|
|
|
static StageFrameState *_current_stage_state; // TODO remove this shitty hack
|
|
|
|
|
|
|
|
#define BGM_FADE_LONG (2.0 * FADE_TIME / (double)FPS)
|
|
|
|
#define BGM_FADE_SHORT (FADE_TIME / (double)FPS)
|
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
static inline bool is_quickloading(StageFrameState *fstate) {
|
|
|
|
return fstate->quicksave && fstate->quicksave == global.replay.input.replay;
|
|
|
|
}
|
|
|
|
|
2023-06-13 04:08:06 +02:00
|
|
|
static inline bool should_skip_frame(StageFrameState *fstate) {
|
|
|
|
return
|
|
|
|
is_quickloading(fstate) || (
|
|
|
|
global.replay.input.replay &&
|
|
|
|
global.replay.input.play.skip_frames > 0
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-04-07 07:57:49 +02:00
|
|
|
bool stage_is_demo_mode(void) {
|
|
|
|
return global.replay.input.replay && global.replay.input.play.demo_mode;
|
|
|
|
}
|
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
static void sync_bgm(StageFrameState *fstate) {
|
2023-06-13 04:08:06 +02:00
|
|
|
if(stage_is_demo_mode()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
double t = fstate->bgm_start_pos + (global.frames - fstate->bgm_start_time) / (double)FPS;
|
|
|
|
audio_bgm_seek_realtime(t);
|
|
|
|
}
|
|
|
|
|
2023-06-13 04:08:06 +02:00
|
|
|
static void recover_after_skip(StageFrameState *fstate) {
|
|
|
|
if(fstate->was_skipping) {
|
|
|
|
fstate->was_skipping = false;
|
|
|
|
sync_bgm(fstate);
|
|
|
|
audio_sfx_set_enabled(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-22 16:41:03 +02:00
|
|
|
#ifdef HAVE_SKIP_MODE
|
|
|
|
|
2023-06-13 04:08:06 +02:00
|
|
|
// TODO refactor this unholy mess
|
|
|
|
// somehow reconcile with what's implemented for quickloads and demos.
|
|
|
|
|
2020-06-22 16:41:03 +02:00
|
|
|
static struct {
|
|
|
|
const char *skip_to_bookmark;
|
|
|
|
bool skip_to_dialog;
|
|
|
|
bool was_skip_mode;
|
|
|
|
} skip_state;
|
|
|
|
|
|
|
|
void _stage_bookmark(const char *name) {
|
|
|
|
log_debug("Bookmark [%s] reached at %i", name, global.frames);
|
|
|
|
|
|
|
|
if(skip_state.skip_to_bookmark && !strcmp(skip_state.skip_to_bookmark, name)) {
|
|
|
|
skip_state.skip_to_bookmark = NULL;
|
|
|
|
global.plr.iddqd = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
DEFINE_EXTERN_TASK(stage_bookmark) {
|
|
|
|
_stage_bookmark(ARGS.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool stage_is_skip_mode(void) {
|
|
|
|
return skip_state.skip_to_bookmark || skip_state.skip_to_dialog;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void skipstate_init(void) {
|
|
|
|
skip_state.skip_to_dialog = env_get_int("TAISEI_SKIP_TO_DIALOG", 0);
|
|
|
|
skip_state.skip_to_bookmark = env_get_string_nonempty("TAISEI_SKIP_TO_BOOKMARK", NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
static LogicFrameAction skipstate_handle_frame(void) {
|
|
|
|
if(skip_state.skip_to_dialog && dialog_is_active(global.dialog)) {
|
|
|
|
skip_state.skip_to_dialog = false;
|
|
|
|
global.plr.iddqd = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool skip_mode = stage_is_skip_mode();
|
|
|
|
|
|
|
|
if(!skip_mode && skip_state.was_skip_mode) {
|
2022-01-09 13:11:26 +01:00
|
|
|
sync_bgm(_current_stage_state);
|
2020-06-22 16:41:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
skip_state.was_skip_mode = skip_mode;
|
|
|
|
|
|
|
|
if(skip_mode) {
|
|
|
|
return LFRAME_SKIP_ALWAYS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(gamekeypressed(KEY_SKIP)) {
|
|
|
|
return LFRAME_SKIP;
|
|
|
|
}
|
|
|
|
|
|
|
|
return LFRAME_WAIT;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void skipstate_shutdown(void) {
|
|
|
|
memset(&skip_state, 0, sizeof(skip_state));
|
|
|
|
}
|
|
|
|
|
|
|
|
#else
|
|
|
|
|
|
|
|
INLINE LogicFrameAction skipstate_handle_frame(void) { return LFRAME_WAIT; }
|
|
|
|
INLINE void skipstate_init(void) { }
|
|
|
|
INLINE void skipstate_shutdown(void) { }
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
2017-02-26 13:17:48 +01:00
|
|
|
static void stage_start(StageInfo *stage) {
|
2011-06-13 18:48:36 +02:00
|
|
|
global.frames = 0;
|
2019-02-22 00:56:03 +01:00
|
|
|
global.gameover = 0;
|
|
|
|
global.voltage_threshold = 0;
|
2017-02-10 23:05:22 +01:00
|
|
|
|
2017-10-08 13:30:51 +02:00
|
|
|
player_stage_pre_init(&global.plr);
|
2017-02-26 13:17:48 +01:00
|
|
|
|
2020-04-26 21:27:13 +02:00
|
|
|
stats_stage_reset(&global.plr.stats);
|
|
|
|
|
2017-02-26 13:17:48 +01:00
|
|
|
if(stage->type == STAGE_SPELL) {
|
2018-01-21 10:52:54 +01:00
|
|
|
global.is_practice_mode = true;
|
2017-03-21 11:09:32 +01:00
|
|
|
global.plr.lives = 0;
|
2017-02-26 13:17:48 +01:00
|
|
|
global.plr.bombs = 0;
|
2017-09-11 21:09:30 +02:00
|
|
|
} else if(global.is_practice_mode) {
|
|
|
|
global.plr.lives = PLR_STGPRACTICE_LIVES;
|
|
|
|
global.plr.bombs = PLR_STGPRACTICE_BOMBS;
|
2018-01-20 16:54:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if(global.is_practice_mode) {
|
2022-10-03 20:36:21 +02:00
|
|
|
global.plr.power_stored = config_get_int(CONFIG_PRACTICE_POWER);
|
2018-01-20 16:54:45 +01:00
|
|
|
}
|
|
|
|
|
2023-09-28 15:27:33 +02:00
|
|
|
global.plr.power_stored = clamp(global.plr.power_stored, 0, PLR_MAX_POWER_STORED);
|
2017-03-02 11:23:30 +01:00
|
|
|
|
2020-06-22 16:41:03 +02:00
|
|
|
reset_all_sfx();
|
2010-10-12 10:55:23 +02:00
|
|
|
}
|
|
|
|
|
2017-10-10 16:06:46 +02:00
|
|
|
static bool ingame_menu_interrupts_bgm(void) {
|
|
|
|
return global.stage->type != STAGE_SPELL;
|
|
|
|
}
|
|
|
|
|
2024-05-03 04:18:27 +02:00
|
|
|
static inline bool is_quicksave_allowed(void) {
|
|
|
|
#ifndef DEBUG
|
|
|
|
if(!global.is_practice_mode) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if(global.gameover != GAMEOVER_NONE) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
typedef struct IngameMenuCallContext {
|
2020-06-24 19:51:00 +02:00
|
|
|
CallChain next;
|
|
|
|
BGM *saved_bgm;
|
|
|
|
double saved_bgm_pos;
|
|
|
|
bool bgm_interrupted;
|
2024-05-03 04:18:27 +02:00
|
|
|
} IngameMenuCallContext;
|
2020-06-24 19:51:00 +02:00
|
|
|
|
2024-05-03 04:18:27 +02:00
|
|
|
static void setup_ingame_menu_bgm(IngameMenuCallContext *ctx, BGM *bgm) {
|
2020-06-24 19:51:00 +02:00
|
|
|
if(ingame_menu_interrupts_bgm()) {
|
|
|
|
ctx->bgm_interrupted = true;
|
|
|
|
|
|
|
|
if(bgm) {
|
|
|
|
ctx->saved_bgm = audio_bgm_current();
|
|
|
|
ctx->saved_bgm_pos = audio_bgm_tell();
|
|
|
|
audio_bgm_play(bgm, true, 0, 1);
|
|
|
|
} else {
|
|
|
|
audio_bgm_pause();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-03 04:18:27 +02:00
|
|
|
static void resume_bgm(IngameMenuCallContext *ctx) {
|
2020-06-24 19:51:00 +02:00
|
|
|
if(ctx->bgm_interrupted) {
|
|
|
|
if(ctx->saved_bgm) {
|
|
|
|
audio_bgm_play(ctx->saved_bgm, true, ctx->saved_bgm_pos, 0.5);
|
|
|
|
ctx->saved_bgm = NULL;
|
|
|
|
} else {
|
|
|
|
audio_bgm_resume();
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx->bgm_interrupted = false;
|
|
|
|
}
|
2017-12-17 01:00:06 +01:00
|
|
|
}
|
|
|
|
|
Emscripten compatibility (#161)
* Major refactoring of the main loop(s) and control flow (WIP)
run_at_fps() is gone 🦀
Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.
A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html
Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.
Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.
* improve build system for emscripten + various fixes
* squish menu bugs
* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS
Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:
https://github.com/taisei-project/freetype2/tree/emscripten
* Enable -Wcast-function-type
Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.
* webgl: workaround a crash on some browsers
* emscripten improvements:
* Persist state (config, progress, replays, ...) in local IndexDB
* Simpler HTML shell (temporary)
* Enable more optimizations
* fix build if validate_glsl=false
* emscripten: improve asset packaging, with local cache
Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.
* emscripten: customize the html shell
* emscripten: force "show log" checkbox unchecked initially
* emscripten: remove quit shortcut from main menu (since there's no quit)
* emscripten: log area fixes
* emscripten/webgl: workaround for fullscreen viewport issue
* emscripten: implement frameskip
* emscripter: improve framerate limiter
* align List to at least 8 bytes (shut up warnings)
* fix non-emscripten builds
* improve fullscreen handling, mainly for emscripten
* Workaround to make audio work in chromium
emscripten-core/emscripten#6511
* emscripten: better vsync handling; enable vsync & disable fxaa by default
2019-03-09 20:32:32 +01:00
|
|
|
static void stage_leave_ingame_menu(CallChainResult ccr) {
|
2024-05-03 04:18:27 +02:00
|
|
|
IngameMenuCallContext *ctx = ccr.ctx;
|
Emscripten compatibility (#161)
* Major refactoring of the main loop(s) and control flow (WIP)
run_at_fps() is gone 🦀
Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.
A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html
Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.
Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.
* improve build system for emscripten + various fixes
* squish menu bugs
* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS
Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:
https://github.com/taisei-project/freetype2/tree/emscripten
* Enable -Wcast-function-type
Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.
* webgl: workaround a crash on some browsers
* emscripten improvements:
* Persist state (config, progress, replays, ...) in local IndexDB
* Simpler HTML shell (temporary)
* Enable more optimizations
* fix build if validate_glsl=false
* emscripten: improve asset packaging, with local cache
Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.
* emscripten: customize the html shell
* emscripten: force "show log" checkbox unchecked initially
* emscripten: remove quit shortcut from main menu (since there's no quit)
* emscripten: log area fixes
* emscripten/webgl: workaround for fullscreen viewport issue
* emscripten: implement frameskip
* emscripter: improve framerate limiter
* align List to at least 8 bytes (shut up warnings)
* fix non-emscripten builds
* improve fullscreen handling, mainly for emscripten
* Workaround to make audio work in chromium
emscripten-core/emscripten#6511
* emscripten: better vsync handling; enable vsync & disable fxaa by default
2019-03-09 20:32:32 +01:00
|
|
|
MenuData *m = ccr.result;
|
2017-10-10 16:06:46 +02:00
|
|
|
|
Emscripten compatibility (#161)
* Major refactoring of the main loop(s) and control flow (WIP)
run_at_fps() is gone 🦀
Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.
A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html
Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.
Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.
* improve build system for emscripten + various fixes
* squish menu bugs
* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS
Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:
https://github.com/taisei-project/freetype2/tree/emscripten
* Enable -Wcast-function-type
Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.
* webgl: workaround a crash on some browsers
* emscripten improvements:
* Persist state (config, progress, replays, ...) in local IndexDB
* Simpler HTML shell (temporary)
* Enable more optimizations
* fix build if validate_glsl=false
* emscripten: improve asset packaging, with local cache
Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.
* emscripten: customize the html shell
* emscripten: force "show log" checkbox unchecked initially
* emscripten: remove quit shortcut from main menu (since there's no quit)
* emscripten: log area fixes
* emscripten/webgl: workaround for fullscreen viewport issue
* emscripten: implement frameskip
* emscripter: improve framerate limiter
* align List to at least 8 bytes (shut up warnings)
* fix non-emscripten builds
* improve fullscreen handling, mainly for emscripten
* Workaround to make audio work in chromium
emscripten-core/emscripten#6511
* emscripten: better vsync handling; enable vsync & disable fxaa by default
2019-03-09 20:32:32 +01:00
|
|
|
if(m->state != MS_Dead) {
|
|
|
|
return;
|
|
|
|
}
|
2017-10-10 16:06:46 +02:00
|
|
|
|
2019-02-22 00:56:03 +01:00
|
|
|
if(global.gameover > 0) {
|
2020-06-22 16:41:03 +02:00
|
|
|
stop_all_sfx();
|
2017-10-10 16:06:46 +02:00
|
|
|
|
2020-06-24 19:51:00 +02:00
|
|
|
if(ctx->bgm_interrupted) {
|
|
|
|
audio_bgm_stop(global.gameover == GAMEOVER_RESTART ? BGM_FADE_SHORT : BGM_FADE_LONG);
|
2017-10-10 16:06:46 +02:00
|
|
|
}
|
|
|
|
} else {
|
2020-06-24 19:51:00 +02:00
|
|
|
resume_bgm(ctx);
|
2021-11-20 13:40:37 +01:00
|
|
|
events_emit(TE_GAME_PAUSE_STATE_CHANGED, false, NULL, NULL);
|
2017-10-10 16:06:46 +02:00
|
|
|
}
|
Emscripten compatibility (#161)
* Major refactoring of the main loop(s) and control flow (WIP)
run_at_fps() is gone 🦀
Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.
A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html
Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.
Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.
* improve build system for emscripten + various fixes
* squish menu bugs
* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS
Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:
https://github.com/taisei-project/freetype2/tree/emscripten
* Enable -Wcast-function-type
Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.
* webgl: workaround a crash on some browsers
* emscripten improvements:
* Persist state (config, progress, replays, ...) in local IndexDB
* Simpler HTML shell (temporary)
* Enable more optimizations
* fix build if validate_glsl=false
* emscripten: improve asset packaging, with local cache
Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.
* emscripten: customize the html shell
* emscripten: force "show log" checkbox unchecked initially
* emscripten: remove quit shortcut from main menu (since there's no quit)
* emscripten: log area fixes
* emscripten/webgl: workaround for fullscreen viewport issue
* emscripten: implement frameskip
* emscripter: improve framerate limiter
* align List to at least 8 bytes (shut up warnings)
* fix non-emscripten builds
* improve fullscreen handling, mainly for emscripten
* Workaround to make audio work in chromium
emscripten-core/emscripten#6511
* emscripten: better vsync handling; enable vsync & disable fxaa by default
2019-03-09 20:32:32 +01:00
|
|
|
|
2020-06-22 16:41:03 +02:00
|
|
|
resume_all_sfx();
|
|
|
|
|
2020-06-24 19:51:00 +02:00
|
|
|
run_call_chain(&ctx->next, NULL);
|
2023-01-09 04:19:31 +01:00
|
|
|
mem_free(ctx);
|
2012-07-14 16:37:52 +02:00
|
|
|
}
|
|
|
|
|
2020-06-24 19:51:00 +02:00
|
|
|
static void stage_enter_ingame_menu(MenuData *m, BGM *bgm, CallChain next) {
|
2021-11-20 13:40:37 +01:00
|
|
|
events_emit(TE_GAME_PAUSE_STATE_CHANGED, true, NULL, NULL);
|
2024-05-03 04:18:27 +02:00
|
|
|
auto ctx = ALLOC(IngameMenuCallContext, { .next = next });
|
2020-06-24 19:51:00 +02:00
|
|
|
setup_ingame_menu_bgm(ctx, bgm);
|
2020-06-22 16:41:03 +02:00
|
|
|
pause_all_sfx();
|
2020-06-24 19:51:00 +02:00
|
|
|
enter_menu(m, CALLCHAIN(stage_leave_ingame_menu, ctx));
|
Emscripten compatibility (#161)
* Major refactoring of the main loop(s) and control flow (WIP)
run_at_fps() is gone 🦀
Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.
A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html
Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.
Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.
* improve build system for emscripten + various fixes
* squish menu bugs
* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS
Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:
https://github.com/taisei-project/freetype2/tree/emscripten
* Enable -Wcast-function-type
Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.
* webgl: workaround a crash on some browsers
* emscripten improvements:
* Persist state (config, progress, replays, ...) in local IndexDB
* Simpler HTML shell (temporary)
* Enable more optimizations
* fix build if validate_glsl=false
* emscripten: improve asset packaging, with local cache
Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.
* emscripten: customize the html shell
* emscripten: force "show log" checkbox unchecked initially
* emscripten: remove quit shortcut from main menu (since there's no quit)
* emscripten: log area fixes
* emscripten/webgl: workaround for fullscreen viewport issue
* emscripten: implement frameskip
* emscripter: improve framerate limiter
* align List to at least 8 bytes (shut up warnings)
* fix non-emscripten builds
* improve fullscreen handling, mainly for emscripten
* Workaround to make audio work in chromium
emscripten-core/emscripten#6511
* emscripten: better vsync handling; enable vsync & disable fxaa by default
2019-03-09 20:32:32 +01:00
|
|
|
}
|
2018-01-10 20:50:01 +01:00
|
|
|
|
stage: fix pause menu related crashes
There were two distinct things going on here:
1. If we receive multiple buffered TE_GAME_PAUSE events in the same
frame, we'd process all of them and create a pause menu for each.
This could theoretically overflow the evloop stack and crash the
game.
2. The `stage_comain` task starts before scheduling the stage main
loop via `eventloop_enter`. It initializes systems that depend on
tasks, and then immediatelly enters its per-frame async loop,
finishing its first iteration before yielding back to `_stage_enter`
and thus allowing `eventloop_enter` to be finally called. If there is
a TE_GAME_PAUSE event in the queue at this point, it would be handled
right there, and a pause menu would be created before the stage main
loop is scheduled. This messes things up quite a bit, leaking a
"zombie" pause menu into the evloop stack. After the stage is
destroyed, the evloop would try to switch to the frame created for
this menu. The menu's draw function would then attempt to reference
free'd resources of the destroyed stage, crashing the game. This
crash has actually been observed and reported (thanks @0kalekale)
To fix #1, the stage now tracks its paused state and refuses to open a
pause menu if one already exists.
To fix #2, `stage_comain` now yields before starting its async loop, to
let the stage set up its main loop early.
Note that because the stage main loop runs all coroutine tasks before
incrementing the frame counter, `stage_comain`'s per-frame logic would
execute twice on frame 0. This is obviously wrong, but this behavior
must be preserved to maintain compatibility with v1.4 replays. For that
reason, the `stage_comain` loop now skips its first YIELD. This hack can
be removed once v1.4 compat is no longer a concern.
2024-04-20 21:27:31 +02:00
|
|
|
static void stage_unpause(CallChainResult ccr) {
|
|
|
|
StageFrameState *fstate = ccr.ctx;
|
|
|
|
fstate->paused = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void stage_pause(StageFrameState *fstate) {
|
|
|
|
if(fstate->paused) {
|
|
|
|
log_debug("Pause request ignored, already paused");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-06-22 16:41:03 +02:00
|
|
|
if(global.gameover == GAMEOVER_TRANSITIONING || stage_is_skip_mode()) {
|
Emscripten compatibility (#161)
* Major refactoring of the main loop(s) and control flow (WIP)
run_at_fps() is gone 🦀
Instead of nested blocking event loops, there is now an eventloop API
that manages an explicit stack of scenes. This makes Taisei a lot more
portable to async environments where spinning a loop forever without
yielding control simply is not an option, and that is the entire point
of this change.
A prime example of such an environment is the Web (via emscripten).
Taisei was able to run there through a terrible hack: inserting
emscripten_sleep calls into the loop, which would yield to the browser.
This has several major drawbacks: first of all, every function that
could possibly call emscripten_sleep must be compiled into a special
kind of bytecode, which then has to be interpreted at runtime, *much*
slower than JITed WebAssembly. And that includes *everything* down the
call stack, too! For more information, see
https://emscripten.org/docs/porting/emterpreter.html
Even though that method worked well enough for experimenting, despite
suboptimal performance, there is another obvious drawback:
emscripten_sleep is implemented via setTimeout(), which can be very
imprecise and is generally not reliable for fluid animation. Browsers
actually have an API specifically for that use case:
window.requestAnimationFrame(), but Taisei's original blocking control
flow style is simply not compatible with it. Emscripten exposes this API
with its emscripten_set_main_loop(), which the eventloop backend now
uses on that platform.
Unfortunately, C is still C, with no fancy closures or coroutines.
With blocking calls into menu/scene loops gone, the control flow is
reimplemented via so-called (pun intended) "call chains". That is
basically an euphemism for callback hell. With manual memory management
and zero type-safety. Not that the menu system wasn't shitty enough
already. I'll just keep telling myself that this is all temporary and
will be replaced with scripts in v1.4.
* improve build system for emscripten + various fixes
* squish menu bugs
* improve emscripten event loop; disable EMULATE_FUNCTION_POINTER_CASTS
Note that stock freetype does not work without
EMULATE_FUNCTION_POINTER_CASTS; use a patched version from the
"emscripten" branch here:
https://github.com/taisei-project/freetype2/tree/emscripten
* Enable -Wcast-function-type
Calling functions through incompatible pointers is nasal demons and
doesn't work in WASM.
* webgl: workaround a crash on some browsers
* emscripten improvements:
* Persist state (config, progress, replays, ...) in local IndexDB
* Simpler HTML shell (temporary)
* Enable more optimizations
* fix build if validate_glsl=false
* emscripten: improve asset packaging, with local cache
Note that even though there are rules to build audio bundles, audio
does *not* work yet. It looks like SDL2_mixer can not work without
threads, which is a problem. Yet another reason to write an OpenAL
backend - emscripten supports that natively.
* emscripten: customize the html shell
* emscripten: force "show log" checkbox unchecked initially
* emscripten: remove quit shortcut from main menu (since there's no quit)
* emscripten: log area fixes
* emscripten/webgl: workaround for fullscreen viewport issue
* emscripten: implement frameskip
* emscripter: improve framerate limiter
* align List to at least 8 bytes (shut up warnings)
* fix non-emscripten builds
* improve fullscreen handling, mainly for emscripten
* Workaround to make audio work in chromium
emscripten-core/emscripten#6511
* emscripten: better vsync handling; enable vsync & disable fxaa by default
2019-03-09 20:32:32 +01:00
|
|
|
return;
|
2018-01-10 20:50:01 +01:00
|
|
|
}
|
|
|
|
|
2021-05-30 03:26:21 +02:00
|
|
|
MenuData *m;
|
|
|
|
|
|
|
|
if(global.replay.input.replay) {
|
|
|
|
m = create_ingame_menu_replay();
|
|
|
|
} else {
|
|
|
|
m = create_ingame_menu();
|
|
|
|
}
|
2020-06-24 19:51:00 +02:00
|
|
|
|
stage: fix pause menu related crashes
There were two distinct things going on here:
1. If we receive multiple buffered TE_GAME_PAUSE events in the same
frame, we'd process all of them and create a pause menu for each.
This could theoretically overflow the evloop stack and crash the
game.
2. The `stage_comain` task starts before scheduling the stage main
loop via `eventloop_enter`. It initializes systems that depend on
tasks, and then immediatelly enters its per-frame async loop,
finishing its first iteration before yielding back to `_stage_enter`
and thus allowing `eventloop_enter` to be finally called. If there is
a TE_GAME_PAUSE event in the queue at this point, it would be handled
right there, and a pause menu would be created before the stage main
loop is scheduled. This messes things up quite a bit, leaking a
"zombie" pause menu into the evloop stack. After the stage is
destroyed, the evloop would try to switch to the frame created for
this menu. The menu's draw function would then attempt to reference
free'd resources of the destroyed stage, crashing the game. This
crash has actually been observed and reported (thanks @0kalekale)
To fix #1, the stage now tracks its paused state and refuses to open a
pause menu if one already exists.
To fix #2, `stage_comain` now yields before starting its async loop, to
let the stage set up its main loop early.
Note that because the stage main loop runs all coroutine tasks before
incrementing the frame counter, `stage_comain`'s per-frame logic would
execute twice on frame 0. This is obviously wrong, but this behavior
must be preserved to maintain compatibility with v1.4 replays. For that
reason, the `stage_comain` loop now skips its first YIELD. This hack can
be removed once v1.4 compat is no longer a concern.
2024-04-20 21:27:31 +02:00
|
|
|
fstate->paused = true;
|
|
|
|
stage_enter_ingame_menu(m, NULL, CALLCHAIN(stage_unpause, fstate));
|
2018-01-10 20:50:01 +01:00
|
|
|
}
|
|
|
|
|
2024-05-03 03:20:24 +02:00
|
|
|
static void stage_do_quickload(StageFrameState *fstate);
|
|
|
|
|
2024-05-03 04:18:27 +02:00
|
|
|
static void stage_continue_game(void) {
|
|
|
|
log_info("The game is being continued...");
|
|
|
|
player_event(&global.plr, &global.replay.input, &global.replay.output, EV_CONTINUE, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void gameover_menu_result(CallChainResult ccr) {
|
|
|
|
GameoverMenuAction *_action = ccr.ctx;
|
|
|
|
auto action = *_action;
|
|
|
|
mem_free(_action);
|
|
|
|
|
|
|
|
switch(action) {
|
|
|
|
case GAMEOVERMENU_ACTION_QUIT:
|
|
|
|
if(global.plr.stats.total.continues_used >= MAX_CONTINUES) {
|
|
|
|
global.gameover = GAMEOVER_DEFEAT;
|
|
|
|
} else {
|
|
|
|
global.gameover = GAMEOVER_ABORT;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GAMEOVERMENU_ACTION_CONTINUE:
|
|
|
|
stage_continue_game();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GAMEOVERMENU_ACTION_RESTART:
|
|
|
|
global.gameover = GAMEOVER_RESTART;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GAMEOVERMENU_ACTION_QUICKLOAD:
|
|
|
|
stage_do_quickload(_current_stage_state);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default: UNREACHABLE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-17 20:58:23 +02:00
|
|
|
void stage_gameover(void) {
|
2017-02-28 00:07:03 +01:00
|
|
|
if(global.stage->type == STAGE_SPELL && config_get_int(CONFIG_SPELLSTAGE_AUTORESTART)) {
|
2024-05-03 03:20:24 +02:00
|
|
|
auto fstate = _current_stage_state;
|
|
|
|
if(fstate->quicksave) {
|
|
|
|
stage_do_quickload(fstate);
|
|
|
|
} else {
|
|
|
|
global.gameover = GAMEOVER_RESTART;
|
|
|
|
}
|
2017-02-28 00:07:03 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-06-24 19:51:00 +02:00
|
|
|
BGM *bgm = NULL;
|
|
|
|
|
|
|
|
if(ingame_menu_interrupts_bgm()) {
|
|
|
|
bgm = res_bgm("gameover");
|
|
|
|
progress_unlock_bgm("gameover");
|
|
|
|
}
|
|
|
|
|
2024-05-03 04:18:27 +02:00
|
|
|
auto action = ALLOC(GameoverMenuAction);
|
|
|
|
auto menu = create_gameover_menu(&(GameoverMenuParams) {
|
|
|
|
.output = action,
|
|
|
|
.quickload_shown = is_quicksave_allowed(),
|
|
|
|
.quickload_enabled = _current_stage_state->quicksave != NULL,
|
|
|
|
});
|
|
|
|
|
|
|
|
stage_enter_ingame_menu(menu, bgm, CALLCHAIN(gameover_menu_result, action));
|
2012-08-17 20:58:23 +02:00
|
|
|
}
|
|
|
|
|
2017-12-28 02:35:53 +01:00
|
|
|
static bool stage_input_common(SDL_Event *event, void *arg) {
|
stage: fix pause menu related crashes
There were two distinct things going on here:
1. If we receive multiple buffered TE_GAME_PAUSE events in the same
frame, we'd process all of them and create a pause menu for each.
This could theoretically overflow the evloop stack and crash the
game.
2. The `stage_comain` task starts before scheduling the stage main
loop via `eventloop_enter`. It initializes systems that depend on
tasks, and then immediatelly enters its per-frame async loop,
finishing its first iteration before yielding back to `_stage_enter`
and thus allowing `eventloop_enter` to be finally called. If there is
a TE_GAME_PAUSE event in the queue at this point, it would be handled
right there, and a pause menu would be created before the stage main
loop is scheduled. This messes things up quite a bit, leaking a
"zombie" pause menu into the evloop stack. After the stage is
destroyed, the evloop would try to switch to the frame created for
this menu. The menu's draw function would then attempt to reference
free'd resources of the destroyed stage, crashing the game. This
crash has actually been observed and reported (thanks @0kalekale)
To fix #1, the stage now tracks its paused state and refuses to open a
pause menu if one already exists.
To fix #2, `stage_comain` now yields before starting its async loop, to
let the stage set up its main loop early.
Note that because the stage main loop runs all coroutine tasks before
incrementing the frame counter, `stage_comain`'s per-frame logic would
execute twice on frame 0. This is obviously wrong, but this behavior
must be preserved to maintain compatibility with v1.4 replays. For that
reason, the `stage_comain` loop now skips its first YIELD. This hack can
be removed once v1.4 compat is no longer a concern.
2024-04-20 21:27:31 +02:00
|
|
|
StageFrameState *fstate = NOT_NULL(arg);
|
2017-12-28 02:35:53 +01:00
|
|
|
TaiseiEvent type = TAISEI_EVENT(event->type);
|
|
|
|
int32_t code = event->user.code;
|
|
|
|
|
|
|
|
switch(type) {
|
|
|
|
case TE_GAME_KEY_DOWN:
|
|
|
|
switch(code) {
|
|
|
|
case KEY_STOP:
|
|
|
|
stage_finish(GAMEOVER_DEFEAT);
|
|
|
|
return true;
|
|
|
|
|
|
|
|
case KEY_RESTART:
|
|
|
|
stage_finish(GAMEOVER_RESTART);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TE_GAME_PAUSE:
|
stage: fix pause menu related crashes
There were two distinct things going on here:
1. If we receive multiple buffered TE_GAME_PAUSE events in the same
frame, we'd process all of them and create a pause menu for each.
This could theoretically overflow the evloop stack and crash the
game.
2. The `stage_comain` task starts before scheduling the stage main
loop via `eventloop_enter`. It initializes systems that depend on
tasks, and then immediatelly enters its per-frame async loop,
finishing its first iteration before yielding back to `_stage_enter`
and thus allowing `eventloop_enter` to be finally called. If there is
a TE_GAME_PAUSE event in the queue at this point, it would be handled
right there, and a pause menu would be created before the stage main
loop is scheduled. This messes things up quite a bit, leaking a
"zombie" pause menu into the evloop stack. After the stage is
destroyed, the evloop would try to switch to the frame created for
this menu. The menu's draw function would then attempt to reference
free'd resources of the destroyed stage, crashing the game. This
crash has actually been observed and reported (thanks @0kalekale)
To fix #1, the stage now tracks its paused state and refuses to open a
pause menu if one already exists.
To fix #2, `stage_comain` now yields before starting its async loop, to
let the stage set up its main loop early.
Note that because the stage main loop runs all coroutine tasks before
incrementing the frame counter, `stage_comain`'s per-frame logic would
execute twice on frame 0. This is obviously wrong, but this behavior
must be preserved to maintain compatibility with v1.4 replays. For that
reason, the `stage_comain` loop now skips its first YIELD. This hack can
be removed once v1.4 compat is no longer a concern.
2024-04-20 21:27:31 +02:00
|
|
|
stage_pause(fstate);
|
2017-12-28 02:35:53 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-02-22 00:56:03 +01:00
|
|
|
static bool stage_input_key_filter(KeyIndex key, bool is_release) {
|
|
|
|
if(key == KEY_HAHAIWIN) {
|
|
|
|
IF_DEBUG(
|
|
|
|
if(!is_release) {
|
|
|
|
stage_finish(GAMEOVER_WIN);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
IF_NOT_DEBUG(
|
|
|
|
if(
|
|
|
|
key == KEY_IDDQD ||
|
|
|
|
key == KEY_POWERUP ||
|
|
|
|
key == KEY_POWERDOWN
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
if(stage_is_cleared()) {
|
|
|
|
if(key == KEY_SHOT) {
|
|
|
|
if(
|
|
|
|
global.gameover == GAMEOVER_SCORESCREEN &&
|
|
|
|
global.frames - global.gameover_time > GAMEOVER_SCORE_DELAY * 2
|
|
|
|
) {
|
|
|
|
if(!is_release) {
|
|
|
|
stage_finish(GAMEOVER_WIN);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(key == KEY_BOMB || key == KEY_SPECIAL) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-02-16 10:45:49 +01:00
|
|
|
attr_nonnull_all
|
2022-01-09 13:11:26 +01:00
|
|
|
static Replay *create_quicksave_replay(ReplayStage *rstg_src) {
|
|
|
|
ReplayStage *rstg = memdup(rstg_src, sizeof(*rstg));
|
|
|
|
rstg->num_events = 0;
|
|
|
|
memset(&rstg->events, 0, sizeof(rstg->events));
|
|
|
|
|
|
|
|
dynarray_ensure_capacity(&rstg->events, rstg_src->events.num_elements + 1);
|
|
|
|
dynarray_set_elements(&rstg->events, rstg_src->events.num_elements, rstg_src->events.data);
|
2023-06-13 04:06:30 +02:00
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
replay_stage_event(rstg, global.frames, EV_RESUME, 0);
|
2023-06-13 04:06:30 +02:00
|
|
|
replay_stage_update_final_stats(rstg, &global.plr.stats);
|
2022-01-09 13:11:26 +01:00
|
|
|
|
2023-01-09 04:19:31 +01:00
|
|
|
auto rpy = ALLOC(Replay);
|
2022-01-09 13:11:26 +01:00
|
|
|
rpy->stages.num_elements = rpy->stages.capacity = 1;
|
|
|
|
rpy->stages.data = rstg;
|
|
|
|
|
|
|
|
log_info("Created quicksave replay on frame %i", global.frames);
|
|
|
|
return rpy;
|
|
|
|
}
|
|
|
|
|
2022-01-28 17:03:22 +01:00
|
|
|
static void stage_do_quicksave(StageFrameState *fstate, bool isauto) {
|
|
|
|
if(isauto && fstate->quicksave && !fstate->quicksave_is_automatic) {
|
|
|
|
// Do not overwrite a manual quicksave with an auto quicksave
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(fstate->quicksave) {
|
|
|
|
replay_reset(fstate->quicksave);
|
2023-01-09 04:19:31 +01:00
|
|
|
mem_free(fstate->quicksave);
|
2022-01-28 17:03:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
fstate->quicksave = create_quicksave_replay(global.replay.output.stage);
|
|
|
|
fstate->quicksave_is_automatic = isauto;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void stage_do_quickload(StageFrameState *fstate) {
|
|
|
|
if(fstate->quicksave) {
|
|
|
|
fstate->quickload_requested = true;
|
|
|
|
} else {
|
|
|
|
log_info("No active quicksave");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-24 21:21:08 +01:00
|
|
|
static bool stage_input_handler_gameplay(SDL_Event *event, void *arg) {
|
2022-01-09 13:11:26 +01:00
|
|
|
StageFrameState *fstate = NOT_NULL(arg);
|
|
|
|
|
|
|
|
if(event->type == SDL_KEYDOWN && !event->key.repeat && is_quicksave_allowed()) {
|
|
|
|
if(event->key.keysym.scancode == config_get_int(CONFIG_KEY_QUICKSAVE)) {
|
2022-01-28 17:03:22 +01:00
|
|
|
stage_do_quicksave(fstate, false);
|
2022-01-09 13:11:26 +01:00
|
|
|
} else if(event->key.keysym.scancode == config_get_int(CONFIG_KEY_QUICKLOAD)) {
|
2022-01-28 17:03:22 +01:00
|
|
|
stage_do_quickload(fstate);
|
2022-01-09 13:11:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-09-29 21:03:49 +02:00
|
|
|
TaiseiEvent type = TAISEI_EVENT(event->type);
|
|
|
|
int32_t code = event->user.code;
|
|
|
|
|
2017-12-28 02:35:53 +01:00
|
|
|
if(stage_input_common(event, arg)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-05-30 03:26:21 +02:00
|
|
|
if(
|
|
|
|
(type == TE_GAME_KEY_DOWN || type == TE_GAME_KEY_UP) &&
|
|
|
|
code == KEY_SHOT &&
|
|
|
|
config_get_int(CONFIG_SHOT_INVERTED)
|
|
|
|
) {
|
|
|
|
type = type == TE_GAME_KEY_DOWN ? TE_GAME_KEY_UP : TE_GAME_KEY_DOWN;
|
|
|
|
}
|
|
|
|
|
|
|
|
ReplayState *rpy = &global.replay.output;
|
|
|
|
|
2012-08-13 17:50:28 +02:00
|
|
|
switch(type) {
|
2017-09-29 21:03:49 +02:00
|
|
|
case TE_GAME_KEY_DOWN:
|
2019-02-22 00:56:03 +01:00
|
|
|
if(stage_input_key_filter(code, false)) {
|
2021-05-30 03:26:21 +02:00
|
|
|
player_event(&global.plr, NULL, rpy, EV_PRESS, code);
|
2017-02-15 18:10:56 +01:00
|
|
|
}
|
2012-08-13 17:50:28 +02:00
|
|
|
break;
|
2017-02-10 23:05:22 +01:00
|
|
|
|
2017-09-29 21:03:49 +02:00
|
|
|
case TE_GAME_KEY_UP:
|
2019-02-22 00:56:03 +01:00
|
|
|
if(stage_input_key_filter(code, true)) {
|
2021-05-30 03:26:21 +02:00
|
|
|
player_event(&global.plr, NULL, rpy, EV_RELEASE, code);
|
2019-02-22 00:56:03 +01:00
|
|
|
}
|
2012-08-07 05:28:41 +02:00
|
|
|
break;
|
2017-02-10 23:05:22 +01:00
|
|
|
|
2012-08-13 17:50:28 +02:00
|
|
|
default: break;
|
|
|
|
}
|
2017-09-29 21:03:49 +02:00
|
|
|
|
|
|
|
return false;
|
2012-08-13 17:50:28 +02:00
|
|
|
}
|
|
|
|
|
2019-01-24 21:21:08 +01:00
|
|
|
static bool stage_input_handler_replay(SDL_Event *event, void *arg) {
|
2017-12-28 02:35:53 +01:00
|
|
|
stage_input_common(event, arg);
|
2017-09-29 21:03:49 +02:00
|
|
|
return false;
|
2012-08-13 17:50:28 +02:00
|
|
|
}
|
|
|
|
|
2023-04-07 07:57:49 +02:00
|
|
|
static bool stage_input_handler_demo(SDL_Event *event, void *arg) {
|
|
|
|
if(event->type == SDL_KEYDOWN && !event->key.repeat) {
|
|
|
|
goto exit;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch(TAISEI_EVENT(event->type)) {
|
|
|
|
case TE_GAME_KEY_DOWN:
|
|
|
|
case TE_GAMEPAD_BUTTON_DOWN:
|
2023-07-11 23:51:44 +02:00
|
|
|
case TE_GAMEPAD_AXIS_DIGITAL:
|
2023-04-07 07:57:49 +02:00
|
|
|
exit:
|
|
|
|
stage_finish(GAMEOVER_ABORT);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
struct replay_event_arg {
|
|
|
|
ReplayState *st;
|
|
|
|
ReplayEvent *resume_event;
|
|
|
|
};
|
|
|
|
|
2021-05-30 03:26:21 +02:00
|
|
|
static void handle_replay_event(ReplayEvent *e, void *arg) {
|
2022-01-09 13:11:26 +01:00
|
|
|
struct replay_event_arg *a = NOT_NULL(arg);
|
2021-05-30 03:26:21 +02:00
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
if(UNLIKELY(a->resume_event != NULL)) {
|
|
|
|
log_warn(
|
|
|
|
"Got replay event [%i:%02x:%04x] after resume event in the same frame, ignoring",
|
|
|
|
e->frame, e->type, e->value
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch(e->type) {
|
|
|
|
case EV_OVER:
|
|
|
|
global.gameover = GAMEOVER_DEFEAT;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case EV_RESUME:
|
|
|
|
a->resume_event = e;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
player_event(&global.plr, a->st, &global.replay.output, e->type, e->value);
|
|
|
|
break;
|
2021-05-30 03:26:21 +02:00
|
|
|
}
|
|
|
|
}
|
2017-02-10 23:05:22 +01:00
|
|
|
|
2022-01-28 17:03:22 +01:00
|
|
|
static void leave_replay_mode(StageFrameState *fstate, ReplayState *rp_in) {
|
|
|
|
replay_state_deinit(rp_in);
|
|
|
|
}
|
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
static void replay_input(StageFrameState *fstate) {
|
2023-06-13 04:08:06 +02:00
|
|
|
if(!should_skip_frame(fstate)) {
|
2022-01-09 13:11:26 +01:00
|
|
|
events_poll((EventHandler[]){
|
stage: fix pause menu related crashes
There were two distinct things going on here:
1. If we receive multiple buffered TE_GAME_PAUSE events in the same
frame, we'd process all of them and create a pause menu for each.
This could theoretically overflow the evloop stack and crash the
game.
2. The `stage_comain` task starts before scheduling the stage main
loop via `eventloop_enter`. It initializes systems that depend on
tasks, and then immediatelly enters its per-frame async loop,
finishing its first iteration before yielding back to `_stage_enter`
and thus allowing `eventloop_enter` to be finally called. If there is
a TE_GAME_PAUSE event in the queue at this point, it would be handled
right there, and a pause menu would be created before the stage main
loop is scheduled. This messes things up quite a bit, leaking a
"zombie" pause menu into the evloop stack. After the stage is
destroyed, the evloop would try to switch to the frame created for
this menu. The menu's draw function would then attempt to reference
free'd resources of the destroyed stage, crashing the game. This
crash has actually been observed and reported (thanks @0kalekale)
To fix #1, the stage now tracks its paused state and refuses to open a
pause menu if one already exists.
To fix #2, `stage_comain` now yields before starting its async loop, to
let the stage set up its main loop early.
Note that because the stage main loop runs all coroutine tasks before
incrementing the frame counter, `stage_comain`'s per-frame logic would
execute twice on frame 0. This is obviously wrong, but this behavior
must be preserved to maintain compatibility with v1.4 replays. For that
reason, the `stage_comain` loop now skips its first YIELD. This hack can
be removed once v1.4 compat is no longer a concern.
2024-04-20 21:27:31 +02:00
|
|
|
{
|
|
|
|
.proc =
|
|
|
|
stage_is_demo_mode()
|
|
|
|
? stage_input_handler_demo
|
|
|
|
: stage_input_handler_replay,
|
|
|
|
.arg = fstate,
|
2023-04-07 07:57:49 +02:00
|
|
|
},
|
2022-01-09 13:11:26 +01:00
|
|
|
{ NULL }
|
|
|
|
}, EFLAG_GAME);
|
|
|
|
}
|
|
|
|
|
|
|
|
ReplayState *rp_in = &global.replay.input;
|
2017-02-10 23:05:22 +01:00
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
if(UNLIKELY(rp_in->mode == REPLAY_NONE)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct replay_event_arg a = { .st = rp_in };
|
|
|
|
replay_state_play_advance(rp_in, global.frames, handle_replay_event, &a);
|
2012-07-14 19:46:03 +02:00
|
|
|
player_applymovement(&global.plr);
|
2022-01-09 13:11:26 +01:00
|
|
|
|
|
|
|
if(a.resume_event) {
|
2022-01-28 17:03:22 +01:00
|
|
|
leave_replay_mode(fstate, rp_in);
|
2022-01-09 13:11:26 +01:00
|
|
|
}
|
2012-07-14 19:46:03 +02:00
|
|
|
}
|
|
|
|
|
2020-06-22 16:41:03 +02:00
|
|
|
static void display_bgm_title(void) {
|
|
|
|
BGM *bgm = audio_bgm_current();
|
|
|
|
const char *title = bgm ? bgm_get_title(bgm) : NULL;
|
|
|
|
|
|
|
|
if(title) {
|
|
|
|
char txt[strlen(title) + 6];
|
|
|
|
snprintf(txt, sizeof(txt), "BGM: %s", title);
|
|
|
|
stagetext_add(txt, VIEWPORT_W-15 + I * (VIEWPORT_H-20), ALIGN_RIGHT, res_font("standard"), RGB(1, 1, 1), 30, 180, 35, 35);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool stage_handle_bgm_change(SDL_Event *evt, void *a) {
|
2022-01-09 13:11:26 +01:00
|
|
|
StageFrameState *fstate = NOT_NULL(a);
|
|
|
|
fstate->bgm_start_time = global.frames;
|
|
|
|
fstate->bgm_start_pos = audio_bgm_tell();
|
|
|
|
|
2020-06-22 16:41:03 +02:00
|
|
|
if(dialog_is_active(global.dialog)) {
|
|
|
|
INVOKE_TASK_WHEN(&global.dialog->events.fadeout_began, common_call_func, display_bgm_title);
|
|
|
|
} else {
|
|
|
|
display_bgm_title();
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-01-09 13:11:26 +01:00
|
|
|
static void stage_input(StageFrameState *fstate) {
|
2021-05-05 19:32:56 +02:00
|
|
|
if(stage_is_skip_mode()) {
|
|
|
|
events_poll((EventHandler[]){
|
2022-01-09 13:11:26 +01:00
|
|
|
{
|
|
|
|
.proc = stage_handle_bgm_change,
|
|
|
|
.event_type = MAKE_TAISEI_EVENT(TE_AUDIO_BGM_STARTED),
|
|
|
|
.arg = fstate,
|
|
|
|
},
|
2021-05-05 19:32:56 +02:00
|
|
|
{NULL}
|
|
|
|
}, EFLAG_NOPUMP);
|
|
|
|
} else {
|
|
|
|
events_poll((EventHandler[]){
|
2022-01-09 13:11:26 +01:00
|
|
|
{ .proc = stage_input_handler_gameplay, .arg = fstate },
|
|
|
|
{
|
|
|
|
.proc = stage_handle_bgm_change,
|
|
|
|
.event_type = MAKE_TAISEI_EVENT(TE_AUDIO_BGM_STARTED),
|
|
|
|
.arg = fstate,
|
|
|
|
},
|
2021-05-05 19:32:56 +02:00
|
|
|
{NULL}
|
|
|
|
}, EFLAG_GAME);
|
|
|
|
}
|
|
|
|
|
2021-05-30 03:26:21 +02:00
|
|
|
player_fix_input(&global.plr, &global.replay.output);
|
2017-03-07 00:57:14 +01:00
|
|
|
player_applymovement(&global.plr);
|
2010-10-12 10:55:23 +02:00
|
|
|
}
|
|
|
|
|
2018-08-05 19:58:50 +02:00
|
|
|
void stage_clear_hazards_predicate(bool (*predicate)(EntityInterface *ent, void *arg), void *arg, ClearHazardsFlags flags) {
|
2019-03-26 16:58:38 +01:00
|
|
|
bool force = flags & CLEAR_HAZARDS_FORCE;
|
|
|
|
|
2018-01-06 10:24:46 +01:00
|
|
|
if(flags & CLEAR_HAZARDS_BULLETS) {
|
2019-01-26 02:54:57 +01:00
|
|
|
for(Projectile *p = global.projs.first, *next; p; p = next) {
|
|
|
|
next = p->next;
|
2018-08-05 19:58:50 +02:00
|
|
|
|
2019-03-26 16:58:38 +01:00
|
|
|
if(!force && !projectile_is_clearable(p)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-08-05 19:58:50 +02:00
|
|
|
if(!predicate || predicate(&p->ent, arg)) {
|
2019-02-22 00:56:03 +01:00
|
|
|
clear_projectile(p, flags);
|
2018-08-05 19:58:50 +02:00
|
|
|
}
|
2018-01-06 09:54:13 +01:00
|
|
|
}
|
2017-04-06 00:46:00 +02:00
|
|
|
}
|
|
|
|
|
2018-01-06 10:24:46 +01:00
|
|
|
if(flags & CLEAR_HAZARDS_LASERS) {
|
2018-06-01 20:40:18 +02:00
|
|
|
for(Laser *l = global.lasers.first, *next; l; l = next) {
|
2018-01-06 19:23:38 +01:00
|
|
|
next = l->next;
|
2018-08-05 19:58:50 +02:00
|
|
|
|
2019-03-26 16:58:38 +01:00
|
|
|
if(!force && !laser_is_clearable(l)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-08-05 19:58:50 +02:00
|
|
|
if(!predicate || predicate(&l->ent, arg)) {
|
2019-02-22 00:56:03 +01:00
|
|