diff --git a/resources/00-taisei.pkgdir/shader/meson.build b/resources/00-taisei.pkgdir/shader/meson.build index bd34b530..668dfea1 100644 --- a/resources/00-taisei.pkgdir/shader/meson.build +++ b/resources/00-taisei.pkgdir/shader/meson.build @@ -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', diff --git a/resources/00-taisei.pkgdir/shader/text_demo.frag.glsl b/resources/00-taisei.pkgdir/shader/text_demo.frag.glsl new file mode 100644 index 00000000..fb7d40d9 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/text_demo.frag.glsl @@ -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); +} diff --git a/resources/00-taisei.pkgdir/shader/text_demo.prog b/resources/00-taisei.pkgdir/shader/text_demo.prog new file mode 100644 index 00000000..c6e11369 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/text_demo.prog @@ -0,0 +1,2 @@ + +objects = text_demo.vert text_demo.frag diff --git a/resources/00-taisei.pkgdir/shader/text_demo.vert.glsl b/resources/00-taisei.pkgdir/shader/text_demo.vert.glsl new file mode 100644 index 00000000..b6fa9724 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/text_demo.vert.glsl @@ -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" diff --git a/src/main.c b/src/main.c index 9123f7a1..0f5213e7 100644 --- a/src/main.c +++ b/src/main.c @@ -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(); } diff --git a/src/menu/musicroom.c b/src/menu/musicroom.c index e20a88de..4f98b96a 100644 --- a/src/menu/musicroom.c +++ b/src/menu/musicroom.c @@ -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; } diff --git a/src/replay/demoplayer.c b/src/replay/demoplayer.c new file mode 100644 index 00000000..379ac965 --- /dev/null +++ b/src/replay/demoplayer.c @@ -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 . + * Copyright (c) 2012-2019, Andrei Alexeyev . +*/ + +#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, + }); +} diff --git a/src/replay/demoplayer.h b/src/replay/demoplayer.h new file mode 100644 index 00000000..1610b1c8 --- /dev/null +++ b/src/replay/demoplayer.h @@ -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 . + * Copyright (c) 2012-2019, Andrei Alexeyev . +*/ + +#pragma once +#include "taisei.h" + +void demoplayer_init(void); +void demoplayer_shutdown(void); +void demoplayer_suspend(void); +void demoplayer_resume(void); diff --git a/src/replay/meson.build b/src/replay/meson.build index 84367bc0..fd0bc241 100644 --- a/src/replay/meson.build +++ b/src/replay/meson.build @@ -1,5 +1,6 @@ replay_src = files( + 'demoplayer.c', 'play.c', 'read.c', 'replay.c', diff --git a/src/replay/play.c b/src/replay/play.c index 2782c183..8de3bb46 100644 --- a/src/replay/play.c +++ b/src/replay/play.c @@ -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)); diff --git a/src/replay/replay.h b/src/replay/replay.h index ef6646dd..e71fe1c1 100644 --- a/src/replay/replay.h +++ b/src/replay/replay.h @@ -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; diff --git a/src/replay/state.h b/src/replay/state.h index 0e57d90d..f0b83e63 100644 --- a/src/replay/state.h +++ b/src/replay/state.h @@ -29,6 +29,7 @@ typedef struct ReplayState { uint16_t desync_check; int desync_check_frame; int desync_frame; + bool demo_mode; } play; struct { diff --git a/src/stage.c b/src/stage.c index 16347403..b2b10c4f 100644 --- a/src/stage.c +++ b/src/stage.c @@ -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); } } diff --git a/src/stage.h b/src/stage.h index c06f8643..21475a19 100644 --- a/src/stage.h +++ b/src/stage.h @@ -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 diff --git a/src/stagedraw.c b/src/stagedraw.c index f79c17d1..c0a16cf8 100644 --- a/src/stagedraw.c +++ b/src/stagedraw.c @@ -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) {