1357 lines
32 KiB
C
1357 lines
32 KiB
C
/*
|
|
* This software is licensed under the terms of the MIT License.
|
|
* See COPYING for further information.
|
|
* ---
|
|
* 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"
|
|
|
|
typedef struct StageFrameState {
|
|
StageInfo *stage;
|
|
ResourceGroup *rg;
|
|
CallChain cc;
|
|
CoSched sched;
|
|
Replay *quicksave;
|
|
bool quicksave_is_automatic;
|
|
bool quickload_requested;
|
|
bool was_skipping;
|
|
bool paused;
|
|
uint32_t dynstage_generation;
|
|
int transition_delay;
|
|
int desync_check_freq;
|
|
uint16_t last_replay_fps;
|
|
float view_shake;
|
|
int bgm_start_time;
|
|
double bgm_start_pos;
|
|
} 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
|
|
|
|
static void stage_start(StageInfo *stage) {
|
|
global.frames = 0;
|
|
global.gameover = 0;
|
|
global.voltage_threshold = 0;
|
|
|
|
player_stage_pre_init(&global.plr);
|
|
|
|
stats_stage_reset(&global.plr.stats);
|
|
|
|
if(stage->type == STAGE_SPELL) {
|
|
global.is_practice_mode = true;
|
|
global.plr.lives = 0;
|
|
global.plr.bombs = 0;
|
|
} 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);
|
|
}
|
|
|
|
global.plr.power_stored = clamp(global.plr.power_stored, 0, PLR_MAX_POWER_STORED);
|
|
|
|
reset_all_sfx();
|
|
}
|
|
|
|
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 {
|
|
CallChain next;
|
|
BGM *saved_bgm;
|
|
double saved_bgm_pos;
|
|
bool bgm_interrupted;
|
|
} IngameMenuCallContext;
|
|
|
|
static void setup_ingame_menu_bgm(IngameMenuCallContext *ctx, BGM *bgm) {
|
|
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) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
static void stage_leave_ingame_menu(CallChainResult ccr) {
|
|
IngameMenuCallContext *ctx = ccr.ctx;
|
|
MenuData *m = ccr.result;
|
|
|
|
if(m->state != MS_Dead) {
|
|
return;
|
|
}
|
|
|
|
if(global.gameover > 0) {
|
|
stop_all_sfx();
|
|
|
|
if(ctx->bgm_interrupted) {
|
|
audio_bgm_stop(global.gameover == GAMEOVER_RESTART ? BGM_FADE_SHORT : BGM_FADE_LONG);
|
|
}
|
|
} else {
|
|
resume_bgm(ctx);
|
|
events_emit(TE_GAME_PAUSE_STATE_CHANGED, false, NULL, NULL);
|
|
}
|
|
|
|
resume_all_sfx();
|
|
|
|
run_call_chain(&ctx->next, NULL);
|
|
mem_free(ctx);
|
|
}
|
|
|
|
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 });
|
|
setup_ingame_menu_bgm(ctx, bgm);
|
|
pause_all_sfx();
|
|
enter_menu(m, CALLCHAIN(stage_leave_ingame_menu, ctx));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
MenuData *m;
|
|
|
|
if(global.replay.input.replay) {
|
|
m = create_ingame_menu_replay();
|
|
} else {
|
|
m = create_ingame_menu();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
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;
|
|
|
|
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_pause(fstate);
|
|
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;
|
|
|
|
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;
|
|
|
|
switch(type) {
|
|
case TE_GAME_KEY_DOWN:
|
|
if(stage_input_key_filter(code, false)) {
|
|
player_event(&global.plr, NULL, rpy, EV_PRESS, code);
|
|
}
|
|
break;
|
|
|
|
case TE_GAME_KEY_UP:
|
|
if(stage_input_key_filter(code, true)) {
|
|
player_event(&global.plr, NULL, rpy, EV_RELEASE, code);
|
|
}
|
|
break;
|
|
|
|
default: break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool stage_input_handler_replay(SDL_Event *event, void *arg) {
|
|
stage_input_common(event, arg);
|
|
return false;
|
|
}
|
|
|
|
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[]){
|
|
{
|
|
.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) {
|
|
bool force = flags & CLEAR_HAZARDS_FORCE;
|
|
|
|
if(flags & CLEAR_HAZARDS_BULLETS) {
|
|
for(Projectile *p = global.projs.first, *next; p; p = next) {
|
|
next = p->next;
|
|
|
|
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) {
|
|
next = l->next;
|
|
|
|
if(!force && !laser_is_clearable(l)) {
|
|
continue;
|
|
}
|
|
|
|
if(!predicate || predicate(&l->ent, arg)) {
|
|
clear_laser(l, flags);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void stage_clear_hazards(ClearHazardsFlags flags) {
|
|
stage_clear_hazards_predicate(NULL, NULL, flags);
|
|
}
|
|
|
|
static bool proximity_predicate(EntityInterface *ent, void *varg) {
|
|
Circle *area = varg;
|
|
|
|
switch(ent->type) {
|
|
case ENT_TYPE_ID(Projectile): {
|
|
Projectile *p = ENT_CAST(ent, Projectile);
|
|
return cabs(p->pos - area->origin) < area->radius;
|
|
}
|
|
|
|
case ENT_TYPE_ID(Laser): {
|
|
Laser *l = ENT_CAST(ent, Laser);
|
|
return laser_intersects_circle(l, *area);
|
|
}
|
|
|
|
default: UNREACHABLE;
|
|
}
|
|
}
|
|
|
|
static bool ellipse_predicate(EntityInterface *ent, void *varg) {
|
|
Ellipse *e = varg;
|
|
|
|
switch(ent->type) {
|
|
case ENT_TYPE_ID(Projectile): {
|
|
Projectile *p = ENT_CAST(ent, Projectile);
|
|
return point_in_ellipse(p->pos, *e);
|
|
}
|
|
|
|
case ENT_TYPE_ID(Laser): {
|
|
Laser *l = ENT_CAST(ent, Laser);
|
|
return laser_intersects_ellipse(l, *e);
|
|
}
|
|
|
|
default: UNREACHABLE;
|
|
}
|
|
}
|
|
|
|
void stage_clear_hazards_at(cmplx origin, double radius, ClearHazardsFlags flags) {
|
|
Circle area = { origin, radius };
|
|
|
|
if(UNLIKELY(radius <= 0)) {
|
|
return;
|
|
}
|
|
|
|
stage_clear_hazards_predicate(proximity_predicate, &area, flags);
|
|
}
|
|
|
|
void stage_clear_hazards_in_ellipse(Ellipse e, ClearHazardsFlags flags) {
|
|
stage_clear_hazards_predicate(ellipse_predicate, &e, flags);
|
|
}
|
|
|
|
TASK(clear_dialog) {
|
|
assert(global.dialog != NULL);
|
|
// dialog_deinit() should've been called by dialog_end() at this point
|
|
global.dialog = NULL;
|
|
}
|
|
|
|
void stage_begin_dialog(Dialog *d) {
|
|
assert(global.dialog == NULL);
|
|
global.dialog = d;
|
|
dialog_init(d);
|
|
INVOKE_TASK_WHEN(&d->events.fadeout_ended, clear_dialog);
|
|
}
|
|
|
|
static void stage_free(void) {
|
|
delete_enemies(&global.enemies);
|
|
delete_items();
|
|
delete_lasers();
|
|
|
|
delete_projectiles(&global.projs);
|
|
delete_projectiles(&global.particles);
|
|
|
|
if(global.dialog) {
|
|
dialog_deinit(global.dialog);
|
|
global.dialog = NULL;
|
|
}
|
|
|
|
if(global.boss) {
|
|
free_boss(global.boss);
|
|
global.boss = NULL;
|
|
}
|
|
|
|
lasers_shutdown();
|
|
projectiles_free();
|
|
stagetext_free();
|
|
}
|
|
|
|
static void stage_finalize(CallChainResult ccr) {
|
|
global.gameover = (intptr_t)ccr.ctx;
|
|
}
|
|
|
|
void stage_finish(int gameover) {
|
|
if(global.gameover == GAMEOVER_TRANSITIONING) {
|
|
return;
|
|
}
|
|
|
|
int prev_gameover = global.gameover;
|
|
global.gameover_time = global.frames;
|
|
|
|
if(gameover == GAMEOVER_SCORESCREEN) {
|
|
global.gameover = GAMEOVER_SCORESCREEN;
|
|
} else {
|
|
global.gameover = GAMEOVER_TRANSITIONING;
|
|
CallChain cc = CALLCHAIN(stage_finalize, (void*)(intptr_t)gameover);
|
|
set_transition(TransFadeBlack, FADE_TIME, FADE_TIME*2, cc);
|
|
|
|
if(!stage_is_demo_mode()) {
|
|
audio_bgm_stop(BGM_FADE_LONG);
|
|
}
|
|
}
|
|
|
|
if(
|
|
global.replay.input.replay == NULL &&
|
|
prev_gameover != GAMEOVER_SCORESCREEN &&
|
|
(gameover == GAMEOVER_SCORESCREEN || gameover == GAMEOVER_WIN)
|
|
) {
|
|
StageProgress *p = NOT_NULL(stageinfo_get_progress(global.stage, global.diff, true));
|
|
progress_register_stage_cleared(p, global.plr.mode);
|
|
}
|
|
|
|
if(gameover == GAMEOVER_SCORESCREEN && stage_is_skip_mode()) {
|
|
// don't get stuck in an infinite loop
|
|
taisei_quit();
|
|
}
|
|
}
|
|
|
|
static void stage_preload(StageInfo *si, ResourceGroup *rg) {
|
|
difficulty_preload(rg);
|
|
projectiles_preload(rg);
|
|
player_preload(rg);
|
|
items_preload(rg);
|
|
boss_preload(rg);
|
|
laserdraw_preload(rg);
|
|
enemies_preload(rg);
|
|
|
|
if(si->type != STAGE_SPELL) {
|
|
res_group_preload(rg, RES_BGM, RESF_OPTIONAL, "gameover", NULL);
|
|
dialog_preload(rg);
|
|
}
|
|
|
|
si->procs->preload(rg);
|
|
}
|
|
|
|
static void display_stage_title(StageInfo *info) {
|
|
if(stage_is_demo_mode()) {
|
|
return;
|
|
}
|
|
|
|
stagetext_add(info->title, VIEWPORT_W/2 + I * (VIEWPORT_H/2-40), ALIGN_CENTER, res_font("big"), RGB(1, 1, 1), 50, 85, 35, 35);
|
|
stagetext_add(info->subtitle, VIEWPORT_W/2 + I * (VIEWPORT_H/2), ALIGN_CENTER, res_font("standard"), RGB(1, 1, 1), 60, 85, 35, 35);
|
|
}
|
|
|
|
TASK(start_bgm, { BGM *bgm; }) {
|
|
_current_stage_state->bgm_start_time = global.frames + 1;
|
|
audio_bgm_play(ARGS.bgm, true, 0, 0);
|
|
}
|
|
|
|
void stage_start_bgm(const char *bgm) {
|
|
if(stage_is_demo_mode()) {
|
|
return;
|
|
}
|
|
|
|
INVOKE_TASK_DELAYED(1, start_bgm, res_bgm(bgm));
|
|
}
|
|
|
|
void stage_set_voltage_thresholds(uint easy, uint normal, uint hard, uint lunatic) {
|
|
switch(global.diff) {
|
|
case D_Easy: global.voltage_threshold = easy; return;
|
|
case D_Normal: global.voltage_threshold = normal; return;
|
|
case D_Hard: global.voltage_threshold = hard; return;
|
|
case D_Lunatic: global.voltage_threshold = lunatic; return;
|
|
default: UNREACHABLE;
|
|
}
|
|
}
|
|
|
|
bool stage_is_cleared(void) {
|
|
return global.gameover == GAMEOVER_SCORESCREEN || global.gameover == GAMEOVER_TRANSITIONING;
|
|
}
|
|
|
|
static void stage_update_fps(StageFrameState *fstate) {
|
|
if(global.replay.output.replay) {
|
|
uint16_t replay_fps = (uint16_t)rint(global.fps.logic.fps);
|
|
|
|
if(replay_fps != fstate->last_replay_fps) {
|
|
replay_stage_event(global.replay.output.stage, global.frames, EV_FPS, replay_fps);
|
|
fstate->last_replay_fps = replay_fps;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void stage_give_clear_bonus(const StageInfo *stage, StageClearBonus *bonus) {
|
|
memset(bonus, 0, sizeof(*bonus));
|
|
|
|
// FIXME: this is clunky...
|
|
if(!global.is_practice_mode && stage->type == STAGE_STORY) {
|
|
StageInfo *next = stageinfo_get_by_id(stage->id + 1);
|
|
|
|
if(next == NULL || next->type != STAGE_STORY) {
|
|
bonus->all_clear.base = global.plr.point_item_value * 100 + global.plr.points / 10;
|
|
bonus->all_clear.diff_multiplier = difficulty_value(1.0, 1.1, 1.3, 1.6);
|
|
bonus->all_clear.diff_bonus = bonus->all_clear.base * (bonus->all_clear.diff_multiplier - 1.0);
|
|
}
|
|
}
|
|
|
|
if(stage->type == STAGE_STORY) {
|
|
bonus->base = stage->id * 1000000;
|
|
}
|
|
|
|
bonus->voltage = max(0, (int)global.plr.voltage - (int)global.voltage_threshold) * (global.plr.point_item_value / 25);
|
|
bonus->lives = global.plr.lives * global.plr.point_item_value * 5;
|
|
|
|
// TODO: maybe a difficulty multiplier?
|
|
|
|
bonus->total = (
|
|
bonus->base +
|
|
bonus->voltage +
|
|
bonus->lives +
|
|
bonus->graze +
|
|
bonus->all_clear.base +
|
|
bonus->all_clear.diff_bonus +
|
|
0);
|
|
player_add_points(&global.plr, bonus->total, global.plr.pos);
|
|
}
|
|
|
|
static void stage_replay_sync(StageFrameState *fstate) {
|
|
uint16_t desync_check = (rng_u64() ^ global.plr.points) & 0xFFFF;
|
|
ReplaySyncStatus rpsync = replay_state_check_desync(&global.replay.input, global.frames, desync_check);
|
|
|
|
if(
|
|
global.replay.output.stage &&
|
|
fstate->desync_check_freq > 0 &&
|
|
!(global.frames % fstate->desync_check_freq)
|
|
) {
|
|
replay_stage_event(global.replay.output.stage, global.frames, EV_CHECK_DESYNC, desync_check);
|
|
}
|
|
|
|
if(rpsync == REPLAY_SYNC_FAIL) {
|
|
if(
|
|
global.is_replay_verification &&
|
|
!global.replay.output.stage
|
|
) {
|
|
exit(1);
|
|
}
|
|
|
|
if(fstate->quicksave && fstate->quicksave == global.replay.input.replay) {
|
|
log_warn("Quicksave replay desynced; resuming prematurely!");
|
|
leave_replay_mode(fstate, &global.replay.input);
|
|
}
|
|
}
|
|
}
|
|
|
|
static LogicFrameAction stage_logic_frame(void *arg) {
|
|
StageFrameState *fstate = arg;
|
|
StageInfo *stage = fstate->stage;
|
|
|
|
stage_update_fps(fstate);
|
|
|
|
if(watchdog_signaled() && !stage_is_demo_mode()) {
|
|
global.gameover = GAMEOVER_ABORT;
|
|
}
|
|
|
|
if(stage_is_skip_mode()) {
|
|
global.plr.iddqd = true;
|
|
}
|
|
|
|
fapproach_p(&fstate->view_shake, 0, 1);
|
|
fapproach_asymptotic_p(&fstate->view_shake, 0, 0.05, 1e-2);
|
|
|
|
if(
|
|
global.replay.input.replay == NULL &&
|
|
fstate->dynstage_generation != dynstage_get_generation()
|
|
) {
|
|
log_info("Stages library updated, attempting to hot-reload");
|
|
stage_do_quicksave(fstate, true); // no-op if user has a manual save
|
|
stage_do_quickload(fstate);
|
|
}
|
|
|
|
if(global.gameover == GAMEOVER_TRANSITIONING) {
|
|
// Usually stage_comain will do this
|
|
events_poll(NULL, 0);
|
|
} else {
|
|
cosched_run_tasks(&fstate->sched);
|
|
update_all_sfx();
|
|
stage_replay_sync(fstate);
|
|
|
|
global.frames++;
|
|
|
|
/*
|
|
* TODO: Investigate why/if any of this needs to happen after global.frames++,
|
|
* and possibly fix that.
|
|
*/
|
|
|
|
stagetext_update();
|
|
|
|
if(global.replay.input.replay && global.gameover != GAMEOVER_TRANSITIONING) {
|
|
ReplayStage *rstg = global.replay.input.stage;
|
|
ReplayEvent *last_event = dynarray_get_ptr(
|
|
&rstg->events, rstg->events.num_elements - 1);
|
|
|
|
if(
|
|
global.frames == last_event->frame - FADE_TIME &&
|
|
last_event->type != EV_RESUME
|
|
) {
|
|
stage_finish(GAMEOVER_DEFEAT);
|
|
}
|
|
}
|
|
|
|
if(
|
|
global.gameover == GAMEOVER_SCORESCREEN &&
|
|
global.frames - global.gameover_time == GAMEOVER_SCORE_DELAY
|
|
) {
|
|
StageClearBonus b;
|
|
stage_give_clear_bonus(stage, &b);
|
|
stage_display_clear_screen(&b);
|
|
}
|
|
|
|
if(stage->type == STAGE_SPELL && !global.boss && !fstate->transition_delay) {
|
|
fstate->transition_delay = 120;
|
|
}
|
|
}
|
|
|
|
if(fstate->transition_delay) {
|
|
if(!--fstate->transition_delay) {
|
|
stage_finish(GAMEOVER_WIN);
|
|
}
|
|
} else if(!(should_skip_frame(fstate) && stage_is_demo_mode())) {
|
|
update_transition();
|
|
}
|
|
|
|
if(global.replay.input.replay == NULL) {
|
|
StageProgress *p = NOT_NULL(stageinfo_get_progress(fstate->stage, global.diff, true));
|
|
progress_register_hiscore(p, global.plr.mode, global.plr.points);
|
|
}
|
|
|
|
if(fstate->quickload_requested) {
|
|
log_info("Quickload initiated");
|
|
return LFRAME_STOP;
|
|
}
|
|
|
|
if(global.gameover > 0) {
|
|
return LFRAME_STOP;
|
|
}
|
|
|
|
if(should_skip_frame(fstate)) {
|
|
fstate->was_skipping = true;
|
|
return LFRAME_SKIP_ALWAYS;
|
|
}
|
|
|
|
recover_after_skip(fstate);
|
|
|
|
|
|
LogicFrameAction skipmode = skipstate_handle_frame();
|
|
if(skipmode != LFRAME_WAIT) {
|
|
return skipmode;
|
|
}
|
|
|
|
if(global.replay.input.replay && gamekeypressed(KEY_SKIP)) {
|
|
return LFRAME_SKIP;
|
|
}
|
|
|
|
return LFRAME_WAIT;
|
|
}
|
|
|
|
static RenderFrameAction stage_render_frame(void *arg) {
|
|
StageFrameState *fstate = arg;
|
|
StageInfo *stage = fstate->stage;
|
|
|
|
if(stage_is_skip_mode()) {
|
|
return RFRAME_DROP;
|
|
}
|
|
|
|
rng_lock(&global.rand_game);
|
|
rng_make_active(&global.rand_visual);
|
|
BEGIN_DRAW_CODE();
|
|
stage_draw_scene(stage);
|
|
END_DRAW_CODE();
|
|
rng_unlock(&global.rand_game);
|
|
rng_make_active(&global.rand_game);
|
|
draw_transition();
|
|
|
|
return RFRAME_SWAP;
|
|
}
|
|
|
|
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;
|
|
|
|
stage->procs->begin();
|
|
player_stage_post_init(&global.plr);
|
|
|
|
if(global.stage->type != STAGE_SPELL) {
|
|
display_stage_title(stage);
|
|
}
|
|
|
|
YIELD;
|
|
|
|
for(;;YIELD) {
|
|
process_input(fstate);
|
|
process_boss(&global.boss);
|
|
process_enemies(&global.enemies);
|
|
process_projectiles(&global.projs, true);
|
|
process_items();
|
|
process_lasers();
|
|
process_projectiles(&global.particles, false);
|
|
|
|
if(global.dialog) {
|
|
dialog_update(global.dialog);
|
|
|
|
if((global.plr.inputflags & INFLAG_SKIP) && dialog_is_active(global.dialog)) {
|
|
dialog_page(global.dialog);
|
|
}
|
|
}
|
|
|
|
if(stage_is_skip_mode()) {
|
|
if(dialog_is_active(global.dialog)) {
|
|
dialog_page(global.dialog);
|
|
}
|
|
|
|
if(global.boss) {
|
|
ent_damage(&global.boss->ent, &(DamageInfo) { 400, DMG_PLAYER_SHOT } );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void _stage_enter(
|
|
StageInfo *stage, ResourceGroup *rg, CallChain next, Replay *quickload, bool quicksave_is_automatic
|
|
) {
|
|
assert(stage);
|
|
assert(stage->procs);
|
|
|
|
if(global.gameover == GAMEOVER_WIN) {
|
|
global.gameover = 0;
|
|
} else if(global.gameover) {
|
|
run_call_chain(&next, NULL);
|
|
return;
|
|
}
|
|
|
|
uint32_t dynstage_generation = dynstage_get_generation();
|
|
stageinfo_reload();
|
|
|
|
#define STUB_PROC(proc, stub) do {\
|
|
if(!stage->procs->proc) { \
|
|
stage->procs->proc = stub; \
|
|
log_debug(#proc " proc is missing"); \
|
|
} \
|
|
} while(0)
|
|
|
|
static const ShaderRule shader_rules_stub[1] = { NULL };
|
|
|
|
STUB_PROC(preload, stage_preload_stub_proc);
|
|
STUB_PROC(begin, stage_stub_proc);
|
|
STUB_PROC(end, stage_stub_proc);
|
|
STUB_PROC(draw, stage_stub_proc);
|
|
STUB_PROC(shader_rules, (ShaderRule*)shader_rules_stub);
|
|
|
|
if(quickload) {
|
|
assert(global.replay.input.stage == NULL);
|
|
ReplayStage *qload_stage = dynarray_get_ptr(&quickload->stages, 0);
|
|
assert(qload_stage->stage == stage->id);
|
|
replay_state_init_play(&global.replay.input, quickload, qload_stage);
|
|
}
|
|
|
|
// I really want to separate all of the game state from the global struct sometime
|
|
global.stage = stage;
|
|
|
|
ent_init();
|
|
stage_objpools_init();
|
|
stage_draw_preload(rg);
|
|
stage_preload(stage, rg);
|
|
stage_draw_init();
|
|
lasers_init();
|
|
|
|
rng_make_active(&global.rand_game);
|
|
stage_start(stage);
|
|
|
|
uint64_t start_time, seed;
|
|
|
|
if(global.replay.input.replay) {
|
|
ReplayStage *rstg = NOT_NULL(global.replay.input.stage);
|
|
assert(stageinfo_get_by_id(rstg->stage) == stage);
|
|
assert(!rstg->skip_frames || !quickload);
|
|
|
|
start_time = rstg->start_time;
|
|
seed = rstg->rng_seed;
|
|
global.diff = rstg->diff;
|
|
|
|
log_debug("REPLAY_PLAY mode: %d events, stage: \"%s\"", rstg->events.num_elements, stage->title);
|
|
} else {
|
|
start_time = (uint64_t)time(0);
|
|
seed = makeseed();
|
|
|
|
StageProgress *p = NOT_NULL(stageinfo_get_progress(stage, global.diff, true));
|
|
progress_register_stage_played(p, global.plr.mode);
|
|
}
|
|
|
|
rng_seed(&global.rand_game, seed);
|
|
|
|
if(global.replay.input.replay) {
|
|
player_init(&global.plr);
|
|
replay_stage_sync_player_state(global.replay.input.stage, &global.plr);
|
|
}
|
|
|
|
if(global.replay.output.replay) {
|
|
global.replay.output.stage = replay_stage_new(
|
|
global.replay.output.replay,
|
|
stage,
|
|
start_time,
|
|
seed,
|
|
global.diff,
|
|
&global.plr
|
|
);
|
|
player_init(&global.plr);
|
|
replay_stage_sync_player_state(global.replay.output.stage, &global.plr);
|
|
}
|
|
|
|
plrmode_preload(global.plr.mode, rg);
|
|
|
|
res_purge();
|
|
|
|
auto fstate = ALLOC(StageFrameState, {
|
|
.stage = stage,
|
|
.cc = next,
|
|
.quicksave = quickload,
|
|
.quicksave_is_automatic = quicksave_is_automatic,
|
|
.desync_check_freq = env_get("TAISEI_REPLAY_DESYNC_CHECK_FREQUENCY", FPS * 5),
|
|
.dynstage_generation = dynstage_generation,
|
|
.rg = rg,
|
|
});
|
|
|
|
cosched_init(&fstate->sched);
|
|
_current_stage_state = fstate;
|
|
|
|
skipstate_init();
|
|
|
|
if(should_skip_frame(fstate)) {
|
|
audio_sfx_set_enabled(false);
|
|
}
|
|
|
|
if(!is_quickloading(fstate)) {
|
|
demoplayer_suspend();
|
|
}
|
|
|
|
SCHED_INVOKE_TASK(&fstate->sched, stage_comain, fstate);
|
|
eventloop_enter(fstate, stage_logic_frame, stage_render_frame, stage_end_loop, FPS);
|
|
}
|
|
|
|
void stage_enter(StageInfo *stage, ResourceGroup *rg, CallChain next) {
|
|
_stage_enter(stage, rg, next, NULL, false);
|
|
}
|
|
|
|
void stage_end_loop(void *ctx) {
|
|
StageFrameState *s = ctx;
|
|
assert(s == _current_stage_state);
|
|
|
|
recover_after_skip(s);
|
|
|
|
Replay *quicksave = s->quicksave;
|
|
bool quicksave_is_automatic = s->quicksave_is_automatic;
|
|
bool is_quickload = s->quickload_requested;
|
|
|
|
if(is_quickload) {
|
|
assume(quicksave != NULL);
|
|
}
|
|
|
|
if(global.replay.output.replay) {
|
|
if(is_quickload) {
|
|
// rollback this stage, as we're about to replay it
|
|
global.replay.output.replay->stages.num_elements--;
|
|
replay_stage_destroy_events(global.replay.output.stage);
|
|
} else {
|
|
replay_stage_event(global.replay.output.stage, global.frames, EV_OVER, 0);
|
|
global.replay.output.stage->plr_points_final = global.plr.points;
|
|
|
|
if(global.gameover == GAMEOVER_WIN) {
|
|
global.replay.output.stage->flags |= REPLAY_SFLAG_CLEAR;
|
|
}
|
|
|
|
replay_stage_update_final_stats(global.replay.output.stage, &global.plr.stats);
|
|
}
|
|
}
|
|
|
|
if(quicksave && !is_quickload) {
|
|
replay_reset(quicksave);
|
|
mem_free(quicksave);
|
|
}
|
|
|
|
s->stage->procs->end();
|
|
stage_draw_shutdown();
|
|
cosched_finish(&s->sched);
|
|
stage_free();
|
|
player_free(&global.plr);
|
|
ent_shutdown();
|
|
rng_make_active(&global.rand_visual);
|
|
stop_all_sfx();
|
|
|
|
taisei_commit_persistent_data();
|
|
skipstate_shutdown();
|
|
|
|
if(taisei_quit_requested()) {
|
|
global.gameover = GAMEOVER_ABORT;
|
|
}
|
|
|
|
_current_stage_state = NULL;
|
|
|
|
StageInfo *stginfo = s->stage;
|
|
CallChain cc = s->cc;
|
|
ResourceGroup *rg = s->rg;
|
|
|
|
mem_free(s);
|
|
|
|
if(is_quickload) {
|
|
_stage_enter(stginfo, rg, cc, quicksave, quicksave_is_automatic);
|
|
} else {
|
|
demoplayer_resume();
|
|
run_call_chain(&cc, NULL);
|
|
}
|
|
}
|
|
|
|
void stage_unlock_bgm(const char *bgm) {
|
|
if(global.replay.input.replay == NULL && !global.plr.stats.total.continues_used) {
|
|
progress_unlock_bgm(bgm);
|
|
}
|
|
}
|
|
|
|
void stage_shake_view(float strength) {
|
|
assume(strength >= 0);
|
|
_current_stage_state->view_shake += strength;
|
|
}
|
|
|
|
float stage_get_view_shake_strength(void) {
|
|
if(_current_stage_state) {
|
|
return _current_stage_state->view_shake;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void stage_load_quicksave(void) {
|
|
stage_do_quickload(NOT_NULL(_current_stage_state));
|
|
}
|
|
|
|
CoSched *stage_get_sched(void) {
|
|
return &NOT_NULL(_current_stage_state)->sched;
|
|
}
|