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.
This commit is contained in:
parent
e54b144973
commit
4a50c85574
2 changed files with 42 additions and 15 deletions
55
src/stage.c
55
src/stage.c
|
@ -40,6 +40,7 @@ typedef struct StageFrameState {
|
|||
bool quicksave_is_automatic;
|
||||
bool quickload_requested;
|
||||
bool was_skipping;
|
||||
bool paused;
|
||||
uint32_t dynstage_generation;
|
||||
int transition_delay;
|
||||
int desync_check_freq;
|
||||
|
@ -255,7 +256,17 @@ static void stage_enter_ingame_menu(MenuData *m, BGM *bgm, CallChain next) {
|
|||
enter_menu(m, CALLCHAIN(stage_leave_ingame_menu, ctx));
|
||||
}
|
||||
|
||||
void stage_pause(void) {
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
|
@ -268,7 +279,8 @@ void stage_pause(void) {
|
|||
m = create_ingame_menu();
|
||||
}
|
||||
|
||||
stage_enter_ingame_menu(m, NULL, NO_CALLCHAIN);
|
||||
fstate->paused = true;
|
||||
stage_enter_ingame_menu(m, NULL, CALLCHAIN(stage_unpause, fstate));
|
||||
}
|
||||
|
||||
void stage_gameover(void) {
|
||||
|
@ -288,6 +300,7 @@ void stage_gameover(void) {
|
|||
}
|
||||
|
||||
static bool stage_input_common(SDL_Event *event, void *arg) {
|
||||
StageFrameState *fstate = NOT_NULL(arg);
|
||||
TaiseiEvent type = TAISEI_EVENT(event->type);
|
||||
int32_t code = event->user.code;
|
||||
|
||||
|
@ -306,7 +319,7 @@ static bool stage_input_common(SDL_Event *event, void *arg) {
|
|||
break;
|
||||
|
||||
case TE_GAME_PAUSE:
|
||||
stage_pause();
|
||||
stage_pause(fstate);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -522,10 +535,12 @@ static void leave_replay_mode(StageFrameState *fstate, ReplayState *rp_in) {
|
|||
static void replay_input(StageFrameState *fstate) {
|
||||
if(!should_skip_frame(fstate)) {
|
||||
events_poll((EventHandler[]){
|
||||
{ .proc =
|
||||
stage_is_demo_mode()
|
||||
? stage_input_handler_demo
|
||||
: stage_input_handler_replay
|
||||
{
|
||||
.proc =
|
||||
stage_is_demo_mode()
|
||||
? stage_input_handler_demo
|
||||
: stage_input_handler_replay,
|
||||
.arg = fstate,
|
||||
},
|
||||
{ NULL }
|
||||
}, EFLAG_GAME);
|
||||
|
@ -1011,6 +1026,14 @@ static void stage_end_loop(void *ctx);
|
|||
static void stage_stub_proc(void) { }
|
||||
static void stage_preload_stub_proc(ResourceGroup *rg) { }
|
||||
|
||||
static void process_input(StageFrameState *fstate) {
|
||||
if(global.replay.input.replay != NULL) {
|
||||
replay_input(fstate);
|
||||
} else {
|
||||
stage_input(fstate);
|
||||
}
|
||||
}
|
||||
|
||||
TASK(stage_comain, { StageFrameState *fstate; }) {
|
||||
StageFrameState *fstate = ARGS.fstate;
|
||||
StageInfo *stage = fstate->stage;
|
||||
|
@ -1022,13 +1045,12 @@ TASK(stage_comain, { StageFrameState *fstate; }) {
|
|||
display_stage_title(stage);
|
||||
}
|
||||
|
||||
for(;;YIELD) {
|
||||
if(global.replay.input.replay != NULL) {
|
||||
replay_input(fstate);
|
||||
} else {
|
||||
stage_input(fstate);
|
||||
}
|
||||
YIELD;
|
||||
|
||||
bool first_frame = true;
|
||||
|
||||
for(;;) {
|
||||
process_input(fstate);
|
||||
process_boss(&global.boss);
|
||||
process_enemies(&global.enemies);
|
||||
process_projectiles(&global.projs, true);
|
||||
|
@ -1053,6 +1075,13 @@ TASK(stage_comain, { StageFrameState *fstate; }) {
|
|||
ent_damage(&global.boss->ent, &(DamageInfo) { 400, DMG_PLAYER_SHOT } );
|
||||
}
|
||||
}
|
||||
|
||||
if(first_frame) {
|
||||
// HACK: Run frame 0 logic twice for compatibility with a v1.4 bug
|
||||
first_frame = false;
|
||||
} else {
|
||||
YIELD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,8 +35,6 @@ typedef struct StageClearBonus {
|
|||
|
||||
void stage_enter(StageInfo *stage, ResourceGroup *rg, CallChain next);
|
||||
void stage_finish(int gameover);
|
||||
|
||||
void stage_pause(void);
|
||||
void stage_gameover(void);
|
||||
|
||||
void stage_start_bgm(const char *bgm);
|
||||
|
|
Loading…
Reference in a new issue