taisei/src/stage.c

1358 lines
32 KiB
C
Raw Permalink Normal View History

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
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>.
*/
#include "stage.h"
#include "audio/audio.h"
#include "common_tasks.h" // IWYU pragma: keep
#include "config.h"
#include "dynstage.h"
#include "eventloop/eventloop.h"
#include "events.h"
#include "global.h"
#include "lasers/draw.h"
#include "log.h"
#include "menu/gameovermenu.h"
#include "menu/ingamemenu.h"
#include "player.h"
#include "replay/demoplayer.h"
#include "replay/stage.h"
#include "replay/state.h"
#include "replay/struct.h"
#include "resource/bgm.h"
#include "stagedraw.h"
#include "stageinfo.h"
#include "stageobjects.h"
#include "stagetext.h"
#include "util/env.h"
#include "watchdog.h"
2017-02-11 12:38:50 +01:00
2020-06-24 19:51:00 +02:00
typedef struct StageFrameState {
StageInfo *stage;
ResourceGroup *rg;
2020-06-24 19:51:00 +02:00
CallChain cc;
CoSched sched;
Replay *quicksave;
bool quicksave_is_automatic;
bool quickload_requested;
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;
uint32_t dynstage_generation;
2020-06-24 19:51:00 +02:00
int transition_delay;
int desync_check_freq;
2020-06-24 19:51:00 +02:00
uint16_t last_replay_fps;
float view_shake;
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)
static inline bool is_quickloading(StageFrameState *fstate) {
return fstate->quicksave && fstate->quicksave == global.replay.input.replay;
}
static inline bool should_skip_frame(StageFrameState *fstate) {
return
is_quickloading(fstate) || (
global.replay.input.replay &&
global.replay.input.play.skip_frames > 0
);
}
bool stage_is_demo_mode(void) {
return global.replay.input.replay && global.replay.input.play.demo_mode;
}
static void sync_bgm(StageFrameState *fstate) {
if(stage_is_demo_mode()) {
return;
}
double t = fstate->bgm_start_pos + (global.frames - fstate->bgm_start_time) / (double)FPS;
audio_bgm_seek_realtime(t);
}
static void recover_after_skip(StageFrameState *fstate) {
if(fstate->was_skipping) {
fstate->was_skipping = false;
sync_bgm(fstate);
audio_sfx_set_enabled(true);
}
}
#ifdef HAVE_SKIP_MODE
// TODO refactor this unholy mess
// somehow reconcile with what's implemented for quickloads and demos.
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) {
sync_bgm(_current_stage_state);
}
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) {
global.frames = 0;
global.gameover = 0;
global.voltage_threshold = 0;
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) {
global.is_practice_mode = true;
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;
}
if(global.is_practice_mode) {
global.plr.power_stored = config_get_int(CONFIG_PRACTICE_POWER);
}
2023-09-28 15:27:33 +02:00
global.plr.power_stored = clamp(global.plr.power_stored, 0, PLR_MAX_POWER_STORED);
reset_all_sfx();
}
2017-10-10 16:06:46 +02:00
static bool ingame_menu_interrupts_bgm(void) {
return global.stage->type != STAGE_SPELL;
}
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;
} IngameMenuCallContext;
2020-06-24 19:51:00 +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();
}
}
}
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) {
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
if(global.gameover > 0) {
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);
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
resume_all_sfx();
2020-06-24 19:51:00 +02:00
run_call_chain(&ctx->next, NULL);
mem_free(ctx);
}
2020-06-24 19:51:00 +02:00
static void stage_enter_ingame_menu(MenuData *m, BGM *bgm, CallChain next) {
events_emit(TE_GAME_PAUSE_STATE_CHANGED, true, NULL, NULL);
auto ctx = ALLOC(IngameMenuCallContext, { .next = next });
2020-06-24 19:51:00 +02:00
setup_ingame_menu_bgm(ctx, bgm);
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
}
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;
}
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;
}
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));
}
static void stage_do_quickload(StageFrameState *fstate);
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;
}
}
void stage_gameover(void) {
if(global.stage->type == STAGE_SPELL && config_get_int(CONFIG_SPELLSTAGE_AUTORESTART)) {
auto fstate = _current_stage_state;
if(fstate->quicksave) {
stage_do_quickload(fstate);
} else {
global.gameover = GAMEOVER_RESTART;
}
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");
}
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));
}
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;
}
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;
}
attr_nonnull_all
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);
replay_stage_event(rstg, global.frames, EV_RESUME, 0);
replay_stage_update_final_stats(rstg, &global.plr.stats);
auto rpy = ALLOC(Replay);
rpy->stages.num_elements = rpy->stages.capacity = 1;
rpy->stages.data = rstg;
log_info("Created quicksave replay on frame %i", global.frames);
return rpy;
}
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);
mem_free(fstate->quicksave);
}
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");
}
}
static bool stage_input_handler_gameplay(SDL_Event *event, void *arg) {
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)) {
stage_do_quicksave(fstate, false);
} else if(event->key.keysym.scancode == config_get_int(CONFIG_KEY_QUICKLOAD)) {
stage_do_quickload(fstate);
}
return false;
}
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;
}
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) {
case TE_GAME_KEY_DOWN:
if(stage_input_key_filter(code, false)) {
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;
case TE_GAME_KEY_UP:
if(stage_input_key_filter(code, true)) {
player_event(&global.plr, NULL, rpy, EV_RELEASE, code);
}
2012-08-07 05:28:41 +02:00
break;
2012-08-13 17:50:28 +02:00
default: break;
}
return false;
2012-08-13 17:50:28 +02: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);
return false;
2012-08-13 17:50:28 +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:
case TE_GAMEPAD_AXIS_DIGITAL:
exit:
stage_finish(GAMEOVER_ABORT);
}
return false;
}
struct replay_event_arg {
ReplayState *st;
ReplayEvent *resume_event;
};
static void handle_replay_event(ReplayEvent *e, void *arg) {
struct replay_event_arg *a = NOT_NULL(arg);
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;
}
}
static void leave_replay_mode(StageFrameState *fstate, ReplayState *rp_in) {
replay_state_deinit(rp_in);
}
static void replay_input(StageFrameState *fstate) {
if(!should_skip_frame(fstate)) {
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,
},
{ NULL }
}, EFLAG_GAME);
}
ReplayState *rp_in = &global.replay.input;
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);
player_applymovement(&global.plr);
if(a.resume_event) {
leave_replay_mode(fstate, rp_in);
}
}
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) {
StageFrameState *fstate = NOT_NULL(a);
fstate->bgm_start_time = global.frames;
fstate->bgm_start_pos = audio_bgm_tell();
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;
}
static void stage_input(StageFrameState *fstate) {
if(stage_is_skip_mode()) {
events_poll((EventHandler[]){
{
.proc = stage_handle_bgm_change,
.event_type = MAKE_TAISEI_EVENT(TE_AUDIO_BGM_STARTED),
.arg = fstate,
},
{NULL}
}, EFLAG_NOPUMP);
} else {
events_poll((EventHandler[]){
{ .proc = stage_input_handler_gameplay, .arg = fstate },
{
.proc = stage_handle_bgm_change,
.event_type = MAKE_TAISEI_EVENT(TE_AUDIO_BGM_STARTED),
.arg = fstate,
},
{NULL}
}, EFLAG_GAME);
}
player_fix_input(&global.plr, &global.replay.output);
player_applymovement(&global.plr);
}
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;
if(flags & CLEAR_HAZARDS_BULLETS) {
for(Projectile *p = global.projs.first, *next; p; p = next) {
next = p->next;
2019-03-26 16:58:38 +01:00
if(!force && !projectile_is_clearable(p)) {
continue;
}
if(!predicate || predicate(&p->ent, arg)) {
clear_projectile(p, flags);
}
}
}
if(flags & CLEAR_HAZARDS_LASERS) {
for(Laser *l = global.lasers.first, *next; l; l = next) {
2018-01-06 19:23:38 +01:00
next = l->next;
2019-03-26 16:58:38 +01:00
if(!force && !laser_is_clearable(l)) {
continue;
}
if(!predicate || predicate(&l->ent, arg)) {