replay/demoplayer: automatic "demo" playback when idling in menus

Currently the game comes with no demos; place your own replays into
$userdir/resources/demos if you want to test this.
This commit is contained in:
Andrei Alexeyev 2023-04-07 07:57:49 +02:00
parent 71109fd253
commit 57a08d4c7a
No known key found for this signature in database
GPG key ID: 72D26128040B9690
15 changed files with 368 additions and 12 deletions

View file

@ -90,6 +90,8 @@ glsl_files = files(
'text_cutscene.frag.glsl',
'text_default.frag.glsl',
'text_default.vert.glsl',
'text_demo.frag.glsl',
'text_demo.vert.glsl',
'text_dialog.frag.glsl',
'text_dialog.vert.glsl',
'text_example.frag.glsl',

View file

@ -0,0 +1,36 @@
#version 330 core
#include "lib/render_context.glslh"
#include "lib/sprite_main.frag.glslh"
#include "lib/util.glslh"
vec3 colormap(float p) {
vec3 c;
c.r = smoothstep(0.30, 0.7, p) * smoothstep(0.20, 0.30, 1.0 - p);
c.g = smoothstep(0.35, 0.7, p) * smoothstep(0.23, 0.32, 1.0 - p);
c.b = smoothstep(0.35, 0.7, p) * smoothstep(0.24, 0.31, 1.0 - p);
return c;
}
void spriteMain(out vec4 fragColor) {
float t = customParams.r;
vec2 tco = flip_native_to_bottomleft(texCoordOverlay);
float base_gradient = smoothstep(-0.5, 0.8, tco.y);
vec4 clr = vec4(
pow(vec3(base_gradient, base_gradient, base_gradient),
vec3(1.3, 1.2, 1.1) - 0.5
), 1);
float go = tco.y;
go += tco.x * mix(-1, 1, 1 - tco.y);
vec4 g = vec4(colormap(fract(-0.5 * t + go)), 0);
clr = alphaCompose(clr, g);
vec3 outlines = texture(tex, texCoord).rgb;
vec4 border = vec4(vec3(g.rgb), 0.5) * outlines.g;
vec4 fill = clr * outlines.r;
fragColor = alphaCompose(border, fill);
}

View file

@ -0,0 +1,2 @@
objects = text_demo.vert text_demo.frag

View file

@ -0,0 +1,8 @@
#version 330 core
#define SPRITE_OUT_COLOR
#define SPRITE_OUT_TEXCOORD
#define SPRITE_OUT_TEXCOORD_OVERLAY
#define SPRITE_OUT_CUSTOM
#include "lib/sprite_default.vert.glslh"

View file

@ -33,6 +33,7 @@
#include "filewatch/filewatch.h"
#include "dynstage.h"
#include "eventloop/eventloop.h"
#include "replay/demoplayer.h"
attr_unused
static void taisei_shutdown(void) {
@ -43,8 +44,8 @@ static void taisei_shutdown(void) {
progress_save();
}
demoplayer_shutdown();
progress_unload();
stage_objpools_shutdown();
gamemode_shutdown();
shutdown_resources();
@ -423,12 +424,13 @@ static void main_post_vfsinit(CallChainResult ccr) {
return;
}
enter_menu(create_main_menu(), CALLCHAIN(main_cleanup, ctx));
run_call_chain(&CALLCHAIN(main_mainmenu, ctx), NULL);
eventloop_run();
}
static void main_mainmenu(CallChainResult ccr) {
MainContext *ctx = ccr.ctx;
demoplayer_init();
enter_menu(create_main_menu(), CALLCHAIN(main_cleanup, ctx));
}
@ -514,7 +516,7 @@ static void main_replay(MainContext *mctx) {
stralloc(&mctx->replay_out->playername, mctx->replay_in->playername);
}
replay_play(mctx->replay_in, mctx->replay_idx, CALLCHAIN(main_cleanup, mctx));
replay_play(mctx->replay_in, mctx->replay_idx, false, CALLCHAIN(main_cleanup, mctx));
eventloop_run();
}

View file

@ -217,7 +217,10 @@ static void action_play_bgm(MenuData *m, void *arg) {
static void add_bgm(MenuData *m, const char *bgm_name, bool preload) {
if(preload) {
preload_resource(RES_BGM, bgm_name, RESF_OPTIONAL);
// FIXME HACK: make this just RESF_OPTIONAL once we have proper refcounting for resources!
// Currently without RESF_PERMANENT we segfault after returning from demo playback,
// because transient resources get unloaded.
preload_resource(RES_BGM, bgm_name, RESF_PERMANENT | RESF_OPTIONAL);
return;
}

193
src/replay/demoplayer.c Normal file
View file

@ -0,0 +1,193 @@
/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "taisei.h"
#include "demoplayer.h"
#include "replay.h"
#include "replay/struct.h"
#include "global.h"
#include "vfs/public.h"
#include "events.h"
#define DEMOPLAYER_DIR_PATH "res/demos"
#define DEMOPLAYER_WAIT_TIME (60 * FPS)
struct {
uint time;
uint wait_time;
uint next_demo_index;
char **demo_files;
size_t num_demo_files;
int suspend_level;
} dplr;
static bool demo_path_filter(const char *path) {
return strendswith(path, "." REPLAY_EXTENSION);
}
static bool demoplayer_check_demos(void) {
if(!dplr.demo_files || dplr.num_demo_files == 0) {
log_warn("No demos found");
return false;
}
return true;
}
void demoplayer_init(void) {
dplr.demo_files = vfs_dir_list_sorted(
DEMOPLAYER_DIR_PATH, &dplr.num_demo_files, vfs_dir_list_order_ascending, demo_path_filter
);
if(demoplayer_check_demos()) {
log_info("Found %zu demo files", dplr.num_demo_files);
}
dplr.wait_time = env_get("TAISEI_DEMO_TIME", DEMOPLAYER_WAIT_TIME);
if(!dplr.wait_time) {
dplr.wait_time = UINT32_MAX;
}
dplr.suspend_level = 1;
demoplayer_resume();
}
void demoplayer_shutdown(void) {
if(dplr.suspend_level == 0) {
demoplayer_suspend();
}
vfs_dir_list_free(dplr.demo_files, dplr.num_demo_files);
dplr.demo_files = NULL;
dplr.num_demo_files = 0;
}
typedef struct DemoPlayerContext {
Replay rpy;
} DemoPlayerContext;
static void demoplayer_start_demo_posttransition(CallChainResult ccr);
static void demoplayer_end_demo(CallChainResult ccr);
static void demoplayer_start_demo(void) {
auto ctx = ALLOC(DemoPlayerContext);
CallChain cc = CALLCHAIN(demoplayer_end_demo, ctx);
demoplayer_suspend();
if(!demoplayer_check_demos()) {
run_call_chain(&cc, NULL);
return;
}
uint demoidx = dplr.next_demo_index;
dplr.next_demo_index = (demoidx + 1) % dplr.num_demo_files;
char *demo_filename = dplr.demo_files[demoidx];
char demo_path[sizeof(DEMOPLAYER_DIR_PATH) + 1 + strlen(demo_filename)];
snprintf(demo_path, sizeof(demo_path), DEMOPLAYER_DIR_PATH "/%s", demo_filename);
log_debug("Staring demo %s", demo_path);
if(!replay_load_vfspath(&ctx->rpy, demo_path, REPLAY_READ_ALL)) {
log_error("Failed to load replay %s", demo_path);
run_call_chain(&cc, NULL);
return;
}
set_transition(TransFadeWhite, FADE_TIME * 3, FADE_TIME * 2,
CALLCHAIN(demoplayer_start_demo_posttransition, ctx));
}
static void demoplayer_start_demo_posttransition(CallChainResult ccr) {
DemoPlayerContext *ctx = ccr.ctx;
CallChain end = CALLCHAIN(demoplayer_end_demo, ctx);
if(TRANSITION_RESULT_CANCELED(ccr)) {
run_call_chain(&end, NULL);
} else {
replay_play(&ctx->rpy, 0, true, end);
}
}
static void demoplayer_end_demo(CallChainResult ccr) {
DemoPlayerContext *ctx = ccr.ctx;
replay_reset(&ctx->rpy);
mem_free(ctx);
demoplayer_resume();
}
static bool demoplayer_frame_event(SDL_Event *evt, void *arg) {
if(dplr.time == dplr.wait_time) {
demoplayer_start_demo();
dplr.time = 0;
return false;
}
assert(dplr.time < dplr.wait_time);
++dplr.time;
// log_debug("%u", dplr.time);
return false;
}
static bool demoplayer_activity_event(SDL_Event *evt, void *arg) {
switch(evt->type) {
case SDL_KEYDOWN:
goto reset;
default: switch(TAISEI_EVENT(evt->type)) {
case TE_GAMEPAD_BUTTON_DOWN:
case TE_GAMEPAD_AXIS:
goto reset;
}
return false;
}
reset:
// log_debug("Reset timer (was %u)", dplr.time);
dplr.time = 0;
return false;
}
void demoplayer_suspend(void) {
dplr.suspend_level++;
assert(dplr.suspend_level > 0);
log_debug("Suspend level is now %i", dplr.suspend_level);
if(dplr.suspend_level > 1) {
return;
}
log_debug("Removing event handlers");
events_unregister_handler(demoplayer_frame_event);
events_unregister_handler(demoplayer_activity_event);
}
void demoplayer_resume(void) {
dplr.suspend_level--;
assert(dplr.suspend_level >= 0);
log_debug("Suspend level is now %i", dplr.suspend_level);
if(dplr.suspend_level > 0) {
return;
}
log_debug("Installing event handlers");
events_register_handler(&(EventHandler) {
demoplayer_frame_event, NULL, EPRIO_LAST, MAKE_TAISEI_EVENT(TE_FRAME),
});
events_register_handler(&(EventHandler) {
demoplayer_activity_event, NULL, EPRIO_SYSTEM,
});
}

15
src/replay/demoplayer.h Normal file
View file

@ -0,0 +1,15 @@
/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#pragma once
#include "taisei.h"
void demoplayer_init(void);
void demoplayer_shutdown(void);
void demoplayer_suspend(void);
void demoplayer_resume(void);

View file

@ -1,5 +1,6 @@
replay_src = files(
'demoplayer.c',
'play.c',
'read.c',
'replay.c',

View file

@ -20,13 +20,14 @@ typedef struct ReplayContext {
CallChain cc;
Replay *rpy;
int stage_idx;
bool demo_mode;
} ReplayContext;
static void replay_do_cleanup(CallChainResult ccr);
static void replay_do_play(CallChainResult ccr);
static void replay_do_post_play(CallChainResult ccr);
void replay_play(Replay *rpy, int firstidx, CallChain next) {
void replay_play(Replay *rpy, int firstidx, bool demo_mode, CallChain next) {
if(firstidx >= rpy->stages.num_elements || firstidx < 0) {
log_error("No stage #%i in the replay", firstidx);
run_call_chain(&next, NULL);
@ -37,6 +38,7 @@ void replay_play(Replay *rpy, int firstidx, CallChain next) {
.cc = next,
.rpy = rpy,
.stage_idx = firstidx,
.demo_mode = demo_mode,
}), NULL));
}
@ -63,6 +65,7 @@ static void replay_do_play(CallChainResult ccr) {
} else {
assume(rstg != NULL);
replay_state_init_play(&global.replay.input, rpy, rstg);
global.replay.input.play.demo_mode = ctx->demo_mode;
replay_state_deinit(&global.replay.output);
global.plr.mode = plrmode_find(rstg->plr_char, rstg->plr_shot);
stage_enter(stginfo, CALLCHAIN(replay_do_post_play, ctx));

View file

@ -44,4 +44,4 @@ bool replay_load_vfspath(Replay *rpy, const char *path, ReplayReadMode mode) att
int replay_find_stage_idx(Replay *rpy, uint8_t stageid) attr_nonnull_all;
void replay_play(Replay *rpy, int firstidx, CallChain next) attr_nonnull_all;
void replay_play(Replay *rpy, int firstidx, bool demo_mode, CallChain next) attr_nonnull_all;

View file

@ -29,6 +29,7 @@ typedef struct ReplayState {
uint16_t desync_check;
int desync_check_frame;
int desync_frame;
bool demo_mode;
} play;
struct {

View file

@ -13,8 +13,9 @@
#include "global.h"
#include "video.h"
#include "resource/bgm.h"
#include "replay/state.h"
#include "replay/demoplayer.h"
#include "replay/stage.h"
#include "replay/state.h"
#include "replay/struct.h"
#include "config.h"
#include "player.h"
@ -55,6 +56,10 @@ static inline bool is_quickloading(StageFrameState *fstate) {
return fstate->quicksave && fstate->quicksave == global.replay.input.replay;
}
bool stage_is_demo_mode(void) {
return global.replay.input.replay && global.replay.input.play.demo_mode;
}
static void sync_bgm(StageFrameState *fstate) {
double t = fstate->bgm_start_pos + (global.frames - fstate->bgm_start_time) / (double)FPS;
audio_bgm_seek_realtime(t);
@ -445,6 +450,21 @@ static bool stage_input_handler_replay(SDL_Event *event, void *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:
exit:
stage_finish(GAMEOVER_ABORT);
}
return false;
}
struct replay_event_arg {
ReplayState *st;
ReplayEvent *resume_event;
@ -488,7 +508,11 @@ static void leave_replay_mode(StageFrameState *fstate, ReplayState *rp_in) {
static void replay_input(StageFrameState *fstate) {
if(!is_quickloading(fstate)) {
events_poll((EventHandler[]){
{ .proc = stage_input_handler_replay },
{ .proc =
stage_is_demo_mode()
? stage_input_handler_demo
: stage_input_handler_replay
},
{ NULL }
}, EFLAG_GAME);
}
@ -746,7 +770,10 @@ void stage_finish(int gameover) {
global.gameover = GAMEOVER_TRANSITIONING;
CallChain cc = CALLCHAIN(stage_finalize, (void*)(intptr_t)gameover);
set_transition(TransFadeBlack, FADE_TIME, FADE_TIME*2, cc);
audio_bgm_stop(BGM_FADE_LONG);
if(!stage_is_demo_mode()) {
audio_bgm_stop(BGM_FADE_LONG);
}
}
if(
@ -786,6 +813,10 @@ static void stage_preload(void) {
}
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);
}
@ -795,6 +826,10 @@ TASK(start_bgm, { BGM *bgm; }) {
}
void stage_start_bgm(const char *bgm) {
if(stage_is_demo_mode()) {
return;
}
INVOKE_TASK_DELAYED(1, start_bgm, res_bgm(bgm));
}
@ -1127,6 +1162,8 @@ static void _stage_enter(
if(is_quickloading(fstate)) {
audio_sfx_set_enabled(false);
} else {
demoplayer_suspend();
}
SCHED_INVOKE_TASK(&fstate->sched, stage_comain, fstate);
@ -1195,6 +1232,7 @@ void stage_end_loop(void *ctx) {
if(is_quickload) {
_stage_enter(stginfo, cc, quicksave, quicksave_is_automatic);
} else {
demoplayer_resume();
run_call_chain(&cc, NULL);
}
}

View file

@ -66,6 +66,8 @@ void stage_load_quicksave(void);
CoSched *stage_get_sched(void);
bool stage_is_demo_mode(void);
#ifdef DEBUG
#define HAVE_SKIP_MODE
#endif

View file

@ -314,6 +314,12 @@ void stage_draw_pre_init(void) {
"monosmall",
NULL);
if(stage_is_demo_mode()) {
preload_resources(RES_SHADER_PROGRAM, RESF_DEFAULT,
"text_demo",
NULL);
}
stagedraw.framerate_graphs = env_get("TAISEI_FRAMERATE_GRAPHS", GRAPHS_DEFAULT);
stagedraw.objpool_stats = env_get("TAISEI_OBJPOOL_STATS", OBJPOOLSTATS_DEFAULT);
@ -1501,9 +1507,8 @@ void stage_draw_bottom_text(void) {
.font_ptr = font,
});
if(global.replay.input.replay != NULL) {
ReplayState *rst = &global.replay.input;
ReplayState *rst = &global.replay.input;
if(rst->replay != NULL && (!stage_is_demo_mode() || rst->play.desync_frame >= 0)) {
r_shader_ptr(stagedraw.hud_text.shader);
// XXX: does it make sense to use the monospace font here?
@ -1832,6 +1837,51 @@ void stage_draw_hud(void) {
.color = RGBA(1 - red, 1 - red, 1 - red, 1 - red),
});
}
// Demo indicator
if(stage_is_demo_mode()) {
cmplxf pos = CMPLXF(
VIEWPORT_X + VIEWPORT_W * 0.5f,
VIEWPORT_Y + VIEWPORT_H * 0.5f - 75
);
float bg_width = 250;
float bg_height = 60;
Sprite *bg = res_sprite("part/smoke");
SpriteParams sp = {
.sprite_ptr = bg,
.shader_ptr = res_shader("sprite_default"),
.color = RGBA(0.1, 0.1, 0.2, 0.07),
};
r_mat_mv_push();
r_mat_mv_translate(crealf(pos), cimagf(pos), 0);
r_mat_mv_scale(bg_width / bg->w, bg_height / bg->h, 1);
r_mat_mv_push();
r_mat_mv_rotate(global.frames * 0.5 * DEG2RAD, 0, 0, 1);
r_draw_sprite(&sp);
r_mat_mv_pop();
sp.color = RGBA(0.2, 0.1, 0.1, 0.07);
r_mat_mv_push();
r_mat_mv_rotate(global.frames * -0.93 * DEG2RAD, 0, 0, 1);
r_draw_sprite(&sp);
r_mat_mv_pop();
r_mat_mv_pop();
text_draw("Demo", &(TextParams) {
.align = ALIGN_CENTER,
.font = "big",
.shader = "text_demo",
.shader_params = &(ShaderCustomParams) { global.frames / 60.0f },
.pos.as_cmplx = pos,
});
}
}
void stage_display_clear_screen(const StageClearBonus *bonus) {