replay: general refactor
* Split replay.c into multiple files under replay/; improve logical separation of replay-related code. * Separate replay playback state from data. * Get rid of global static replay struct and avoid unnecessary replay copying. * Replay playback and recording are now independent and may occur simultaneously, although this functionality is not yet exposed. This enables replay "re-recording" while synthesizing new desync check events, possibly at a different rate from the original replay. * Rate of recorded desync check events can now be controlled with the TAISEI_REPLAY_DESYNC_CHECK_FREQUENCY environment variable. The default value is 300 as before. * Probably other stuff I forgot about.
This commit is contained in:
parent
239bec95b0
commit
173c8c3cc6
30 changed files with 1449 additions and 1164 deletions
|
@ -130,7 +130,7 @@ static const Color *boss_healthbar_color(AttackType atype) {
|
|||
}
|
||||
|
||||
static StageProgress *get_spellstage_progress(Attack *a, StageInfo **out_stginfo, bool write) {
|
||||
if(!write || (global.replaymode == REPLAY_RECORD && global.stage->type == STAGE_STORY)) {
|
||||
if(!write || (global.replay.input.replay == NULL && global.stage->type == STAGE_STORY)) {
|
||||
StageInfo *i = stageinfo_get_by_spellcard(a->info, global.diff);
|
||||
if(i) {
|
||||
StageProgress *p = stageinfo_get_progress(i, global.diff, write);
|
||||
|
|
|
@ -19,9 +19,6 @@ void init_global(CLIAction *cli) {
|
|||
rng_init(&global.rand_visual, time(0));
|
||||
rng_make_active(&global.rand_visual);
|
||||
|
||||
memset(&global.replay, 0, sizeof(Replay));
|
||||
|
||||
global.replaymode = REPLAY_RECORD;
|
||||
global.frameskip = cli->frameskip;
|
||||
|
||||
if(cli->type == CLI_VerifyReplay) {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
#include "refs.h"
|
||||
#include "config.h"
|
||||
#include "resource/resource.h"
|
||||
#include "replay.h"
|
||||
#include "replay/state.h"
|
||||
#include "random.h"
|
||||
#include "events.h"
|
||||
#include "difficulty.h"
|
||||
|
@ -122,9 +122,9 @@ typedef struct {
|
|||
FPSCounter busy;
|
||||
} fps;
|
||||
|
||||
Replay replay;
|
||||
ReplayMode replaymode;
|
||||
ReplayStage *replay_stage;
|
||||
struct {
|
||||
ReplayState input, output;
|
||||
} replay;
|
||||
|
||||
uint voltage_threshold;
|
||||
|
||||
|
|
18
src/main.c
18
src/main.c
|
@ -29,6 +29,7 @@
|
|||
#include "coroutine.h"
|
||||
#include "util/gamemode.h"
|
||||
#include "cutscenes/cutscene.h"
|
||||
#include "replay/struct.h"
|
||||
|
||||
attr_unused
|
||||
static void taisei_shutdown(void) {
|
||||
|
@ -195,7 +196,7 @@ static noreturn void main_vfstree(CallChainResult ccr);
|
|||
|
||||
static noreturn void main_quit(MainContext *ctx, int status) {
|
||||
free_cli_action(&ctx->cli);
|
||||
replay_destroy(&ctx->replay);
|
||||
replay_reset(&ctx->replay);
|
||||
free(ctx);
|
||||
exit(status);
|
||||
}
|
||||
|
@ -362,9 +363,11 @@ static void main_singlestg_cleanup(CallChainResult ccr);
|
|||
|
||||
static void main_singlestg_begin_game(CallChainResult ccr) {
|
||||
SingleStageContext *ctx = ccr.ctx;
|
||||
MainContext *mctx = ctx->mctx;
|
||||
|
||||
global.replay_stage = NULL;
|
||||
replay_init(&global.replay);
|
||||
replay_reset(&mctx->replay);
|
||||
replay_state_init_record(&global.replay.output, &mctx->replay);
|
||||
replay_state_deinit(&global.replay.input);
|
||||
global.gameover = 0;
|
||||
player_init(&global.plr);
|
||||
stats_init(&global.plr.stats);
|
||||
|
@ -377,18 +380,20 @@ static void main_singlestg_begin_game(CallChainResult ccr) {
|
|||
}
|
||||
|
||||
static void main_singlestg_end_game(CallChainResult ccr) {
|
||||
SingleStageContext *ctx = ccr.ctx;
|
||||
MainContext *mctx = ctx->mctx;
|
||||
|
||||
if(global.gameover == GAMEOVER_RESTART) {
|
||||
replay_destroy(&global.replay);
|
||||
replay_reset(&mctx->replay);
|
||||
main_singlestg_begin_game(ccr);
|
||||
} else {
|
||||
ask_save_replay(CALLCHAIN(main_singlestg_cleanup, ccr.ctx));
|
||||
ask_save_replay(&mctx->replay, CALLCHAIN(main_singlestg_cleanup, ccr.ctx));
|
||||
}
|
||||
}
|
||||
|
||||
static void main_singlestg_cleanup(CallChainResult ccr) {
|
||||
SingleStageContext *ctx = ccr.ctx;
|
||||
MainContext *mctx = ctx->mctx;
|
||||
replay_destroy(&global.replay);
|
||||
free(ccr.ctx);
|
||||
main_quit(mctx, 0);
|
||||
}
|
||||
|
@ -423,7 +428,6 @@ static void main_singlestg(MainContext *mctx) {
|
|||
|
||||
static void main_replay(MainContext *mctx) {
|
||||
replay_play(&mctx->replay, mctx->replay_idx, CALLCHAIN(main_cleanup, mctx));
|
||||
replay_destroy(&mctx->replay); // replay_play makes a copy
|
||||
eventloop_run();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,15 @@
|
|||
#include "mainmenu.h"
|
||||
#include "progress.h"
|
||||
#include "video.h"
|
||||
#include "replay/struct.h"
|
||||
#include "replay/state.h"
|
||||
|
||||
typedef struct StartGameContext {
|
||||
StageInfo *restart_stage;
|
||||
StageInfo *current_stage;
|
||||
MenuData *diff_menu;
|
||||
MenuData *char_menu;
|
||||
Replay replay;
|
||||
Difficulty difficulty;
|
||||
} StartGameContext;
|
||||
|
||||
|
@ -88,9 +91,8 @@ static void reset_game(StartGameContext *ctx) {
|
|||
ctx->current_stage = ctx->restart_stage;
|
||||
|
||||
global.gameover = GAMEOVER_NONE;
|
||||
global.replay_stage = NULL;
|
||||
replay_destroy(&global.replay);
|
||||
replay_init(&global.replay);
|
||||
replay_reset(&ctx->replay);
|
||||
replay_state_init_record(&global.replay.output, &ctx->replay);
|
||||
player_init(&global.plr);
|
||||
stats_init(&global.plr.stats);
|
||||
global.plr.mode = plrmode_find(
|
||||
|
@ -152,10 +154,10 @@ static void start_game_do_leave_stage(CallChainResult ccr) {
|
|||
cc = CALLCHAIN(start_game_do_cleanup, ctx);
|
||||
}
|
||||
|
||||
ask_save_replay(cc);
|
||||
ask_save_replay(&ctx->replay, cc);
|
||||
}
|
||||
} else {
|
||||
ask_save_replay(CALLCHAIN(start_game_do_cleanup, ctx));
|
||||
ask_save_replay(&ctx->replay, CALLCHAIN(start_game_do_cleanup, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,12 +182,12 @@ static void start_game_do_show_credits(CallChainResult ccr) {
|
|||
|
||||
static void start_game_do_cleanup(CallChainResult ccr) {
|
||||
StartGameContext *ctx = ccr.ctx;
|
||||
replay_reset(&ctx->replay);
|
||||
kill_aux_menus(ctx);
|
||||
free(ctx);
|
||||
free_resources(false);
|
||||
global.replay_stage = NULL;
|
||||
global.gameover = GAMEOVER_NONE;
|
||||
replay_destroy(&global.replay);
|
||||
replay_state_deinit(&global.replay.output);
|
||||
main_menu_update_practice_menus();
|
||||
audio_bgm_play(res_bgm("menu"), true, 0, 0);
|
||||
}
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
|
||||
static void continue_game(MenuData *m, void *arg) {
|
||||
log_info("The game is being continued...");
|
||||
assert(global.replaymode == REPLAY_RECORD);
|
||||
player_event_with_replay(&global.plr, EV_CONTINUE, 0);
|
||||
player_event(&global.plr, &global.replay.input, &global.replay.output, EV_CONTINUE, 0);
|
||||
}
|
||||
|
||||
static void give_up(MenuData *m, void *arg) {
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
#include "plrmodes.h"
|
||||
#include "video.h"
|
||||
#include "common.h"
|
||||
#include "replay/state.h"
|
||||
#include "replay/struct.h"
|
||||
|
||||
// Type of MenuData.context
|
||||
typedef struct ReplayviewContext {
|
||||
|
@ -148,7 +150,7 @@ static void replayview_run(MenuData *menu, void *arg) {
|
|||
|
||||
static void replayview_freearg(void *a) {
|
||||
ReplayviewItemContext *ctx = a;
|
||||
replay_destroy(ctx->replay);
|
||||
replay_reset(ctx->replay);
|
||||
free(ctx->replay);
|
||||
free(ctx->replayname);
|
||||
free(ctx);
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
#include "savereplay.h"
|
||||
#include "mainmenu.h"
|
||||
#include "global.h"
|
||||
#include "replay.h"
|
||||
#include "replay/struct.h"
|
||||
#include "plrmodes.h"
|
||||
#include "common.h"
|
||||
#include "video.h"
|
||||
|
||||
attr_nonnull_all
|
||||
static void do_save_replay(Replay *rpy) {
|
||||
char strtime[FILENAME_TIMESTAMP_MIN_BUF_SIZE], *name;
|
||||
char prepr[16], drepr[16];
|
||||
|
@ -34,12 +35,19 @@ static void do_save_replay(Replay *rpy) {
|
|||
name = strfmt("taisei_%s_stg%X_%s_%s", strtime, stg->stage, prepr, drepr);
|
||||
}
|
||||
|
||||
replay_save(rpy, name);
|
||||
if(rpy->playername) {
|
||||
replay_save(rpy, name);
|
||||
} else {
|
||||
rpy->playername = config_get_str(CONFIG_PLAYERNAME);
|
||||
replay_save(rpy, name);
|
||||
rpy->playername = NULL;
|
||||
}
|
||||
|
||||
free(name);
|
||||
}
|
||||
|
||||
static void save_rpy(MenuData *menu, void *a) {
|
||||
do_save_replay(&global.replay);
|
||||
do_save_replay(a);
|
||||
}
|
||||
|
||||
static void draw_saverpy_menu(MenuData *m) {
|
||||
|
@ -110,7 +118,7 @@ static void update_saverpy_menu(MenuData *m) {
|
|||
});
|
||||
}
|
||||
|
||||
static MenuData* create_saverpy_menu(void) {
|
||||
static MenuData* create_saverpy_menu(Replay *rpy) {
|
||||
MenuData *m = alloc_menu();
|
||||
|
||||
m->input = saverpy_menu_input;
|
||||
|
@ -118,25 +126,23 @@ static MenuData* create_saverpy_menu(void) {
|
|||
m->logic = update_saverpy_menu;
|
||||
m->flags = MF_Transient;
|
||||
|
||||
add_menu_entry(m, "Yes", save_rpy, NULL);
|
||||
add_menu_entry(m, "Yes", save_rpy, rpy);
|
||||
add_menu_entry(m, "No", menu_action_close, NULL);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
void ask_save_replay(CallChain next) {
|
||||
assert(global.replay_stage != NULL);
|
||||
|
||||
void ask_save_replay(Replay *rpy, CallChain next) {
|
||||
switch(config_get_int(CONFIG_SAVE_RPY)) {
|
||||
case 1:
|
||||
do_save_replay(&global.replay);
|
||||
do_save_replay(rpy);
|
||||
// fallthrough
|
||||
case 0:
|
||||
run_call_chain(&next, NULL);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
enter_menu(create_saverpy_menu(), next);
|
||||
enter_menu(create_saverpy_menu(rpy), next);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
|
||||
#include "menu.h"
|
||||
#include "eventloop/eventloop.h"
|
||||
#include "replay/replay.h"
|
||||
|
||||
void ask_save_replay(CallChain next);
|
||||
void ask_save_replay(Replay *rpy, CallChain next)
|
||||
attr_nonnull(1);
|
||||
|
||||
#endif // IGUARD_menu_savereplay_h
|
||||
|
|
|
@ -88,7 +88,6 @@ taisei_src = files(
|
|||
'projectile_prototypes.c',
|
||||
'random.c',
|
||||
'refs.c',
|
||||
'replay.c',
|
||||
'stage.c',
|
||||
'stagedraw.c',
|
||||
'stageinfo.c',
|
||||
|
@ -127,6 +126,7 @@ subdir('menu')
|
|||
subdir('pixmap')
|
||||
subdir('plrmodes')
|
||||
subdir('renderer')
|
||||
subdir('replay')
|
||||
subdir('resource')
|
||||
subdir('rwops')
|
||||
subdir('stages')
|
||||
|
@ -144,6 +144,7 @@ taisei_src += [
|
|||
pixmap_src,
|
||||
plrmodes_src,
|
||||
renderer_src,
|
||||
replay_src,
|
||||
resource_src,
|
||||
rwops_src,
|
||||
stages_src,
|
||||
|
|
99
src/player.c
99
src/player.c
|
@ -19,6 +19,8 @@
|
|||
#include "stats.h"
|
||||
#include "entity.h"
|
||||
#include "util/glm.h"
|
||||
#include "replay/stage.h"
|
||||
#include "replay/struct.h"
|
||||
|
||||
DEFINE_ENTITY_TYPE(PlayerIndicators, {
|
||||
Player *plr;
|
||||
|
@ -562,7 +564,10 @@ DEFINE_TASK(player_logic) {
|
|||
|
||||
fapproach_p(&plr->bomb_cutin_alpha, 0, 1/200.0);
|
||||
|
||||
if(plr->respawntime - PLR_RESPAWN_TIME/2 == global.frames && plr->lives < 0 && global.replaymode != REPLAY_PLAY) {
|
||||
if(
|
||||
plr->respawntime - PLR_RESPAWN_TIME/2 == global.frames &&
|
||||
plr->lives < 0 && global.replay.input.replay == NULL
|
||||
) {
|
||||
stage_gameover();
|
||||
}
|
||||
|
||||
|
@ -1055,10 +1060,15 @@ static bool player_set_axis(int *aptr, uint16_t value) {
|
|||
return true;
|
||||
}
|
||||
|
||||
void player_event(Player *plr, uint8_t type, uint16_t value, bool *out_useful, bool *out_cheat) {
|
||||
PlayerEventResult player_event(
|
||||
Player *plr,
|
||||
ReplayState *rpy_in,
|
||||
ReplayState *rpy_out,
|
||||
ReplayEventCode type,
|
||||
uint16_t value
|
||||
) {
|
||||
bool useful = true;
|
||||
bool cheat = false;
|
||||
bool is_replay = global.replaymode == REPLAY_PLAY;
|
||||
|
||||
switch(type) {
|
||||
case EV_PRESS:
|
||||
|
@ -1137,65 +1147,60 @@ void player_event(Player *plr, uint8_t type, uint16_t value, bool *out_useful, b
|
|||
break;
|
||||
}
|
||||
|
||||
if(is_replay) {
|
||||
if(rpy_in && rpy_in->stage) {
|
||||
assert(rpy_in->mode == REPLAY_PLAY);
|
||||
|
||||
if(!useful) {
|
||||
log_warn("Useless event in replay: [%i:%02x:%04x]", global.frames, type, value);
|
||||
log_warn("Replay input: useless event: [%i:%02x:%04x]", global.frames, type, value);
|
||||
}
|
||||
|
||||
if(cheat) {
|
||||
log_warn("Cheat event in replay: [%i:%02x:%04x]", global.frames, type, value);
|
||||
log_warn("Replay input: Cheat event: [%i:%02x:%04x]", global.frames, type, value);
|
||||
|
||||
if( !(global.replay.flags & REPLAY_GFLAG_CHEATS) ||
|
||||
!(global.replay_stage->flags & REPLAY_SFLAG_CHEATS)) {
|
||||
if( !(rpy_in->replay->flags & REPLAY_GFLAG_CHEATS) ||
|
||||
!(rpy_in->stage->flags & REPLAY_SFLAG_CHEATS)) {
|
||||
log_warn("...but this replay was NOT properly cheat-flagged! Not cool, not cool at all");
|
||||
}
|
||||
}
|
||||
|
||||
if(type == EV_CONTINUE && (
|
||||
!(global.replay.flags & REPLAY_GFLAG_CONTINUES) ||
|
||||
!(global.replay_stage->flags & REPLAY_SFLAG_CONTINUES))) {
|
||||
log_warn("Continue event in replay: [%i:%02x:%04x], but this replay was not properly continue-flagged", global.frames, type, value);
|
||||
!(rpy_in->replay->flags & REPLAY_GFLAG_CONTINUES) ||
|
||||
!(rpy_in->stage->flags & REPLAY_SFLAG_CONTINUES))) {
|
||||
log_warn("Replay input: Continue event [%i:%02x:%04x], but this replay was not properly continue-flagged", global.frames, type, value);
|
||||
}
|
||||
}
|
||||
|
||||
if(out_useful) {
|
||||
*out_useful = useful;
|
||||
if(rpy_out && rpy_out->stage) {
|
||||
assert(rpy_out->mode == REPLAY_RECORD);
|
||||
|
||||
if(useful) {
|
||||
replay_stage_event(rpy_out->stage, global.frames, type, value);
|
||||
|
||||
if(type == EV_CONTINUE) {
|
||||
rpy_out->replay->flags |= REPLAY_GFLAG_CONTINUES;
|
||||
rpy_out->stage->flags |= REPLAY_SFLAG_CONTINUES;
|
||||
}
|
||||
|
||||
if(cheat) {
|
||||
rpy_out->replay->flags |= REPLAY_GFLAG_CHEATS;
|
||||
rpy_out->stage->flags |= REPLAY_SFLAG_CHEATS;
|
||||
}
|
||||
} else {
|
||||
log_debug("Replay output: Useless event discarded: [%i:%02x:%04x]", global.frames, type, value);
|
||||
}
|
||||
}
|
||||
|
||||
if(out_cheat) {
|
||||
*out_cheat = cheat;
|
||||
}
|
||||
}
|
||||
|
||||
bool player_event_with_replay(Player *plr, uint8_t type, uint16_t value) {
|
||||
bool useful, cheat;
|
||||
assert(global.replaymode == REPLAY_RECORD);
|
||||
|
||||
if(config_get_int(CONFIG_SHOT_INVERTED) && value == KEY_SHOT && (type == EV_PRESS || type == EV_RELEASE)) {
|
||||
type = type == EV_PRESS ? EV_RELEASE : EV_PRESS;
|
||||
}
|
||||
|
||||
player_event(plr, type, value, &useful, &cheat);
|
||||
PlayerEventResult res = 0;
|
||||
|
||||
if(useful) {
|
||||
replay_stage_event(global.replay_stage, global.frames, type, value);
|
||||
|
||||
if(type == EV_CONTINUE) {
|
||||
global.replay.flags |= REPLAY_GFLAG_CONTINUES;
|
||||
global.replay_stage->flags |= REPLAY_SFLAG_CONTINUES;
|
||||
}
|
||||
|
||||
if(cheat) {
|
||||
global.replay.flags |= REPLAY_GFLAG_CHEATS;
|
||||
global.replay_stage->flags |= REPLAY_SFLAG_CHEATS;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
log_debug("Useless event discarded: [%i:%02x:%04x]", global.frames, type, value);
|
||||
res |= PLREVT_USEFUL;
|
||||
}
|
||||
|
||||
return false;
|
||||
if(cheat) {
|
||||
res |= PLREVT_CHEAT;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// free-axis movement
|
||||
|
@ -1297,7 +1302,7 @@ void player_applymovement(Player *plr) {
|
|||
player_move(&global.plr, direction);
|
||||
}
|
||||
|
||||
void player_fix_input(Player *plr) {
|
||||
void player_fix_input(Player *plr, ReplayState *rpy_out) {
|
||||
// correct input state to account for any events we might have missed,
|
||||
// usually because the pause menu ate them up
|
||||
|
||||
|
@ -1324,18 +1329,18 @@ void player_fix_input(Player *plr) {
|
|||
}
|
||||
|
||||
if(newflags != plr->inputflags) {
|
||||
player_event_with_replay(plr, EV_INFLAGS, newflags);
|
||||
player_event(plr, NULL, rpy_out, EV_INFLAGS, newflags);
|
||||
}
|
||||
|
||||
int axis_lr = gamepad_player_axis_value(PLRAXIS_LR);
|
||||
int axis_ud = gamepad_player_axis_value(PLRAXIS_UD);
|
||||
|
||||
if(plr->axis_lr != axis_lr) {
|
||||
player_event_with_replay(plr, EV_AXIS_LR, axis_lr);
|
||||
player_event(plr, NULL, rpy_out, EV_AXIS_LR, axis_lr);
|
||||
}
|
||||
|
||||
if(plr->axis_ud != axis_ud) {
|
||||
player_event_with_replay(plr, EV_AXIS_UD, axis_ud);
|
||||
player_event(plr, NULL, rpy_out, EV_AXIS_UD, axis_ud);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
29
src/player.h
29
src/player.h
|
@ -28,6 +28,8 @@
|
|||
#include "stats.h"
|
||||
#include "resource/animation.h"
|
||||
#include "entity.h"
|
||||
#include "replay/state.h"
|
||||
#include "replay/eventcodes.h"
|
||||
|
||||
enum {
|
||||
PLR_MAX_POWER = 400,
|
||||
|
@ -165,18 +167,10 @@ DEFINE_ENTITY_TYPE(Player, {
|
|||
)
|
||||
});
|
||||
|
||||
// this is used by both player and replay code
|
||||
enum {
|
||||
EV_PRESS,
|
||||
EV_RELEASE,
|
||||
EV_OVER, // replay-only
|
||||
EV_AXIS_LR,
|
||||
EV_AXIS_UD,
|
||||
EV_CHECK_DESYNC, // replay-only
|
||||
EV_FPS, // replay-only
|
||||
EV_INFLAGS,
|
||||
EV_CONTINUE,
|
||||
};
|
||||
typedef enum PlayerEventResult {
|
||||
PLREVT_USEFUL = (1 << 0),
|
||||
PLREVT_CHEAT = (1 << 1),
|
||||
} PlayerEventResult;
|
||||
|
||||
// This is called first before we even enter stage_loop.
|
||||
// It's also called right before syncing player state from a replay stage struct, if a replay is being watched or recorded, before every stage.
|
||||
|
@ -206,10 +200,15 @@ void player_realdeath(Player*);
|
|||
void player_death(Player*);
|
||||
void player_graze(Player *plr, cmplx pos, int pts, int effect_intensity, const Color *color);
|
||||
|
||||
void player_event(Player *plr, uint8_t type, uint16_t value, bool *out_useful, bool *out_cheat);
|
||||
bool player_event_with_replay(Player *plr, uint8_t type, uint16_t value);
|
||||
PlayerEventResult player_event(
|
||||
Player *plr,
|
||||
ReplayState *rpy_in,
|
||||
ReplayState *rpy_out,
|
||||
ReplayEventCode type,
|
||||
uint16_t value
|
||||
) attr_nonnull(1);
|
||||
void player_fix_input(Player *plr, ReplayState *rpy_out);
|
||||
void player_applymovement(Player* plr);
|
||||
void player_fix_input(Player *plr);
|
||||
|
||||
void player_add_life_fragments(Player *plr, int frags);
|
||||
void player_add_bomb_fragments(Player *plr, int frags);
|
||||
|
|
927
src/replay.c
927
src/replay.c
|
@ -1,927 +0,0 @@
|
|||
/*
|
||||
* 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 "replay.h"
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#include "global.h"
|
||||
|
||||
static uint8_t replay_magic_header[] = REPLAY_MAGIC_HEADER;
|
||||
|
||||
void replay_init(Replay *rpy) {
|
||||
memset(rpy, 0, sizeof(Replay));
|
||||
log_debug("Replay at %p initialized for writing", (void*)rpy);
|
||||
}
|
||||
|
||||
ReplayStage* replay_create_stage(Replay *rpy, StageInfo *stage, uint64_t start_time, uint64_t seed, Difficulty diff, Player *plr) {
|
||||
ReplayStage *s;
|
||||
|
||||
rpy->stages = (ReplayStage*)realloc(rpy->stages, sizeof(ReplayStage) * (++rpy->numstages));
|
||||
s = rpy->stages + rpy->numstages - 1;
|
||||
memset(s, 0, sizeof(ReplayStage));
|
||||
|
||||
get_system_time(&s->init_time);
|
||||
|
||||
dynarray_ensure_capacity(&s->events, REPLAY_ALLOC_INITIAL);
|
||||
|
||||
s->stage = stage->id;
|
||||
s->start_time = start_time;
|
||||
s->rng_seed = seed;
|
||||
s->diff = diff;
|
||||
|
||||
s->plr_pos_x = floor(creal(plr->pos));
|
||||
s->plr_pos_y = floor(cimag(plr->pos));
|
||||
|
||||
s->plr_points = plr->points;
|
||||
s->plr_continues_used = plr->stats.total.continues_used;
|
||||
// s->plr_focus = plr->focus; FIXME remove and bump version
|
||||
s->plr_char = plr->mode->character->id;
|
||||
s->plr_shot = plr->mode->shot_mode;
|
||||
s->plr_lives = plr->lives;
|
||||
s->plr_life_fragments = plr->life_fragments;
|
||||
s->plr_bombs = plr->bombs;
|
||||
s->plr_bomb_fragments = plr->bomb_fragments;
|
||||
s->plr_power = plr->power + plr->power_overflow;
|
||||
s->plr_graze = plr->graze;
|
||||
s->plr_point_item_value = plr->point_item_value;
|
||||
s->plr_inputflags = plr->inputflags;
|
||||
|
||||
log_debug("Created a new stage %p in replay %p", (void*)s, (void*)rpy);
|
||||
return s;
|
||||
}
|
||||
|
||||
void replay_stage_sync_player_state(ReplayStage *stg, Player *plr) {
|
||||
plr->points = stg->plr_points;
|
||||
plr->stats.total.continues_used = stg->plr_continues_used;
|
||||
plr->mode = plrmode_find(stg->plr_char, stg->plr_shot);
|
||||
plr->pos = stg->plr_pos_x + I * stg->plr_pos_y;
|
||||
// plr->focus = stg->plr_focus; FIXME remove and bump version
|
||||
plr->lives = stg->plr_lives;
|
||||
plr->life_fragments = stg->plr_life_fragments;
|
||||
plr->bombs = stg->plr_bombs;
|
||||
plr->bomb_fragments = stg->plr_bomb_fragments;
|
||||
plr->power = (stg->plr_power > PLR_MAX_POWER ? PLR_MAX_POWER : stg->plr_power);
|
||||
plr->power_overflow = (stg->plr_power > PLR_MAX_POWER ? stg->plr_power - PLR_MAX_POWER : 0);
|
||||
plr->graze = stg->plr_graze;
|
||||
plr->point_item_value = stg->plr_point_item_value;
|
||||
plr->inputflags = stg->plr_inputflags;
|
||||
|
||||
plr->stats.total.lives_used = stg->plr_stats_total_lives_used;
|
||||
plr->stats.stage.lives_used = stg->plr_stats_stage_lives_used;
|
||||
plr->stats.total.bombs_used = stg->plr_stats_total_bombs_used;
|
||||
plr->stats.stage.bombs_used = stg->plr_stats_stage_bombs_used;
|
||||
plr->stats.stage.continues_used = stg->plr_stats_stage_continues_used;
|
||||
}
|
||||
|
||||
static void replay_destroy_stage(ReplayStage *stage) {
|
||||
dynarray_free_data(&stage->events);
|
||||
memset(stage, 0, sizeof(ReplayStage));
|
||||
}
|
||||
|
||||
void replay_destroy_events(Replay *rpy) {
|
||||
if(!rpy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(rpy->stages) {
|
||||
for(int i = 0; i < rpy->numstages; ++i) {
|
||||
ReplayStage *stg = rpy->stages + i;
|
||||
dynarray_free_data(&stg->events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void replay_destroy(Replay *rpy) {
|
||||
if(!rpy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(rpy->stages) {
|
||||
for(int i = 0; i < rpy->numstages; ++i) {
|
||||
replay_destroy_stage(rpy->stages + i);
|
||||
}
|
||||
|
||||
free(rpy->stages);
|
||||
}
|
||||
|
||||
free(rpy->playername);
|
||||
|
||||
memset(rpy, 0, sizeof(Replay));
|
||||
}
|
||||
|
||||
void replay_stage_event(ReplayStage *stg, uint32_t frame, uint8_t type, uint16_t value) {
|
||||
assert(stg != NULL);
|
||||
|
||||
dynarray_size_t old_capacity = stg->events.capacity;
|
||||
|
||||
ReplayEvent *e = dynarray_append(&stg->events);
|
||||
e->frame = frame;
|
||||
e->type = type;
|
||||
e->value = value;
|
||||
|
||||
if(stg->events.capacity > old_capacity && stg->events.capacity > UINT16_MAX) {
|
||||
log_error("Too many events in replay; saving WILL FAIL!");
|
||||
}
|
||||
|
||||
if(type == EV_OVER) {
|
||||
log_debug("The replay is OVER");
|
||||
}
|
||||
}
|
||||
|
||||
static void replay_write_string(SDL_RWops *file, char *str, uint16_t version) {
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
|
||||
SDL_WriteU8(file, strlen(str));
|
||||
} else {
|
||||
SDL_WriteLE16(file, strlen(str));
|
||||
}
|
||||
|
||||
SDL_RWwrite(file, str, 1, strlen(str));
|
||||
}
|
||||
|
||||
static bool replay_write_events(Replay *rpy, SDL_RWops *file) {
|
||||
for(int stgidx = 0; stgidx < rpy->numstages; ++stgidx) {
|
||||
dynarray_foreach_elem(&rpy->stages[stgidx].events, ReplayEvent *evt, {
|
||||
SDL_WriteLE32(file, evt->frame);
|
||||
SDL_WriteU8(file, evt->type);
|
||||
SDL_WriteLE16(file, evt->value);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t replay_calc_stageinfo_checksum(ReplayStage *stg, uint16_t version) {
|
||||
uint32_t cs = 0;
|
||||
|
||||
cs += stg->stage;
|
||||
cs += stg->rng_seed;
|
||||
cs += stg->diff;
|
||||
cs += stg->plr_points;
|
||||
cs += stg->plr_char;
|
||||
cs += stg->plr_shot;
|
||||
cs += stg->plr_pos_x;
|
||||
cs += stg->plr_pos_y;
|
||||
cs += stg->plr_focus; // FIXME remove and bump version
|
||||
cs += stg->plr_power;
|
||||
cs += stg->plr_lives;
|
||||
cs += stg->plr_life_fragments;
|
||||
cs += stg->plr_bombs;
|
||||
cs += stg->plr_bomb_fragments;
|
||||
cs += stg->plr_inputflags;
|
||||
|
||||
if(!stg->num_events && stg->events.num_elements) {
|
||||
cs += (uint16_t)stg->events.num_elements;
|
||||
} else {
|
||||
cs += stg->num_events;
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
|
||||
cs += stg->plr_continues_used;
|
||||
cs += stg->flags;
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV2) {
|
||||
cs += stg->plr_graze;
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV0) {
|
||||
cs += stg->plr_point_item_value;
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV3) {
|
||||
cs += stg->plr_points_final;
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS104000_REV0) {
|
||||
cs += stg->plr_stats_total_lives_used;
|
||||
cs += stg->plr_stats_stage_lives_used;
|
||||
cs += stg->plr_stats_total_bombs_used;
|
||||
cs += stg->plr_stats_stage_bombs_used;
|
||||
cs += stg->plr_stats_stage_continues_used;
|
||||
}
|
||||
|
||||
log_debug("%08x", cs);
|
||||
return cs;
|
||||
}
|
||||
|
||||
static bool replay_write_stage(ReplayStage *stg, SDL_RWops *file, uint16_t version) {
|
||||
assert(version >= REPLAY_STRUCT_VERSION_TS103000_REV2);
|
||||
|
||||
SDL_WriteLE32(file, stg->flags);
|
||||
SDL_WriteLE16(file, stg->stage);
|
||||
SDL_WriteLE64(file, stg->start_time);
|
||||
SDL_WriteLE64(file, stg->rng_seed);
|
||||
SDL_WriteU8(file, stg->diff);
|
||||
SDL_WriteLE64(file, stg->plr_points);
|
||||
SDL_WriteU8(file, stg->plr_continues_used);
|
||||
SDL_WriteU8(file, stg->plr_char);
|
||||
SDL_WriteU8(file, stg->plr_shot);
|
||||
SDL_WriteLE16(file, stg->plr_pos_x);
|
||||
SDL_WriteLE16(file, stg->plr_pos_y);
|
||||
SDL_WriteU8(file, stg->plr_focus); // FIXME remove and bump version
|
||||
SDL_WriteLE16(file, stg->plr_power);
|
||||
SDL_WriteU8(file, stg->plr_lives);
|
||||
SDL_WriteLE16(file, stg->plr_life_fragments);
|
||||
SDL_WriteU8(file, stg->plr_bombs);
|
||||
SDL_WriteLE16(file, stg->plr_bomb_fragments);
|
||||
SDL_WriteU8(file, stg->plr_inputflags);
|
||||
SDL_WriteLE32(file, stg->plr_graze);
|
||||
SDL_WriteLE32(file, stg->plr_point_item_value);
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV3) {
|
||||
SDL_WriteLE64(file, stg->plr_points_final);
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS104000_REV0) {
|
||||
SDL_WriteU8(file, stg->plr_stats_total_lives_used);
|
||||
SDL_WriteU8(file, stg->plr_stats_stage_lives_used);
|
||||
SDL_WriteU8(file, stg->plr_stats_total_bombs_used);
|
||||
SDL_WriteU8(file, stg->plr_stats_stage_bombs_used);
|
||||
SDL_WriteU8(file, stg->plr_stats_stage_continues_used);
|
||||
}
|
||||
|
||||
if(stg->events.num_elements > UINT16_MAX) {
|
||||
log_error("Too many events in replay, cannot write this");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_WriteLE16(file, stg->events.num_elements);
|
||||
SDL_WriteLE32(file, 1 + ~replay_calc_stageinfo_checksum(stg, version));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void fix_flags(Replay *rpy) {
|
||||
for(int i = 0; i < rpy->numstages; ++i) {
|
||||
if(!(rpy->stages[i].flags & REPLAY_SFLAG_CLEAR)) {
|
||||
rpy->flags &= ~REPLAY_GFLAG_CLEAR;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
rpy->flags |= REPLAY_GFLAG_CLEAR;
|
||||
}
|
||||
|
||||
bool replay_write(Replay *rpy, SDL_RWops *file, uint16_t version) {
|
||||
assert(version >= REPLAY_STRUCT_VERSION_TS103000_REV2);
|
||||
|
||||
uint16_t base_version = (version & ~REPLAY_VERSION_COMPRESSION_BIT);
|
||||
bool compression = (version & REPLAY_VERSION_COMPRESSION_BIT);
|
||||
|
||||
SDL_RWwrite(file, replay_magic_header, sizeof(replay_magic_header), 1);
|
||||
SDL_WriteLE16(file, version);
|
||||
|
||||
TaiseiVersion v;
|
||||
TAISEI_VERSION_GET_CURRENT(&v);
|
||||
|
||||
if(taisei_version_write(file, &v) != TAISEI_VERSION_SIZE) {
|
||||
log_error("Failed to write game version: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
void *buf;
|
||||
SDL_RWops *abuf = NULL;
|
||||
SDL_RWops *vfile = file;
|
||||
|
||||
if(compression) {
|
||||
abuf = SDL_RWAutoBuffer(&buf, 64);
|
||||
vfile = SDL_RWWrapZlibWriter(
|
||||
abuf, RW_DEFLATE_LEVEL_DEFAULT, REPLAY_COMPRESSION_CHUNK_SIZE, false
|
||||
);
|
||||
}
|
||||
|
||||
replay_write_string(vfile, config_get_str(CONFIG_PLAYERNAME), base_version);
|
||||
fix_flags(rpy);
|
||||
|
||||
SDL_WriteLE32(vfile, rpy->flags);
|
||||
SDL_WriteLE16(vfile, rpy->numstages);
|
||||
|
||||
for(int i = 0; i < rpy->numstages; ++i) {
|
||||
if(!replay_write_stage(rpy->stages + i, vfile, base_version)) {
|
||||
if(compression) {
|
||||
SDL_RWclose(vfile);
|
||||
SDL_RWclose(abuf);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(compression) {
|
||||
SDL_RWclose(vfile);
|
||||
SDL_WriteLE32(file, SDL_RWtell(file) + SDL_RWtell(abuf) + 4);
|
||||
SDL_RWwrite(file, buf, SDL_RWtell(abuf), 1);
|
||||
SDL_RWclose(abuf);
|
||||
vfile = SDL_RWWrapZlibWriter(file, RW_DEFLATE_LEVEL_DEFAULT, REPLAY_COMPRESSION_CHUNK_SIZE, false);
|
||||
}
|
||||
|
||||
bool events_ok = replay_write_events(rpy, vfile);
|
||||
|
||||
if(compression) {
|
||||
SDL_RWclose(vfile);
|
||||
}
|
||||
|
||||
if(!events_ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// useless byte to simplify the premature EOF check, can be anything
|
||||
SDL_WriteU8(file, REPLAY_USELESS_BYTE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef REPLAY_LOAD_DEBUG
|
||||
#define PRINTPROP(prop,fmt) log_debug(#prop " = %" # fmt " [%"PRIi64" / %"PRIi64"]", prop, SDL_RWtell(file), filesize)
|
||||
#else
|
||||
#define PRINTPROP(prop,fmt) (void)(prop)
|
||||
#endif
|
||||
|
||||
#define CHECKPROP(prop,fmt) PRINTPROP(prop,fmt); if(filesize > 0 && SDL_RWtell(file) == filesize) { log_error("%s: Premature EOF", source); return false; }
|
||||
|
||||
static void replay_read_string(SDL_RWops *file, char **ptr, uint16_t version) {
|
||||
size_t len;
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
|
||||
len = SDL_ReadU8(file);
|
||||
} else {
|
||||
len = SDL_ReadLE16(file);
|
||||
}
|
||||
|
||||
*ptr = malloc(len + 1);
|
||||
memset(*ptr, 0, len + 1);
|
||||
|
||||
SDL_RWread(file, *ptr, 1, len);
|
||||
}
|
||||
|
||||
static bool replay_read_header(Replay *rpy, SDL_RWops *file, int64_t filesize, size_t *ofs, const char *source) {
|
||||
uint8_t header[sizeof(replay_magic_header)];
|
||||
(*ofs) += sizeof(header);
|
||||
|
||||
SDL_RWread(file, header, sizeof(header), 1);
|
||||
|
||||
if(memcmp(header, replay_magic_header, sizeof(header))) {
|
||||
log_error("%s: Incorrect header", source);
|
||||
return false;
|
||||
}
|
||||
|
||||
CHECKPROP(rpy->version = SDL_ReadLE16(file), u);
|
||||
(*ofs) += 2;
|
||||
|
||||
uint16_t base_version = (rpy->version & ~REPLAY_VERSION_COMPRESSION_BIT);
|
||||
bool compression = (rpy->version & REPLAY_VERSION_COMPRESSION_BIT);
|
||||
bool gamev_assumed = false;
|
||||
|
||||
switch(base_version) {
|
||||
case REPLAY_STRUCT_VERSION_TS101000: {
|
||||
// legacy format with no versioning, assume v1.1
|
||||
TAISEI_VERSION_SET(&rpy->game_version, 1, 1, 0, 0);
|
||||
gamev_assumed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case REPLAY_STRUCT_VERSION_TS102000_REV0:
|
||||
case REPLAY_STRUCT_VERSION_TS102000_REV1:
|
||||
case REPLAY_STRUCT_VERSION_TS102000_REV2:
|
||||
case REPLAY_STRUCT_VERSION_TS103000_REV0:
|
||||
case REPLAY_STRUCT_VERSION_TS103000_REV1:
|
||||
case REPLAY_STRUCT_VERSION_TS103000_REV2:
|
||||
case REPLAY_STRUCT_VERSION_TS103000_REV3:
|
||||
case REPLAY_STRUCT_VERSION_TS104000_REV0:
|
||||
{
|
||||
if(taisei_version_read(file, &rpy->game_version) != TAISEI_VERSION_SIZE) {
|
||||
log_error("%s: Failed to read game version", source);
|
||||
return false;
|
||||
}
|
||||
|
||||
(*ofs) += TAISEI_VERSION_SIZE;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
log_error("%s: Unknown struct version %u", source, base_version);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
char *gamev = taisei_version_tostring(&rpy->game_version);
|
||||
log_info("Struct version %u (%scompressed), game version %s%s",
|
||||
base_version, compression ? "" : "un", gamev, gamev_assumed ? " (assumed)" : "");
|
||||
free(gamev);
|
||||
|
||||
if(compression) {
|
||||
CHECKPROP(rpy->fileoffset = SDL_ReadLE32(file), u);
|
||||
(*ofs) += 4;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _replay_read_meta(Replay *rpy, SDL_RWops *file, int64_t filesize, const char *source) {
|
||||
uint16_t version = rpy->version & ~REPLAY_VERSION_COMPRESSION_BIT;
|
||||
|
||||
replay_read_string(file, &rpy->playername, version);
|
||||
PRINTPROP(rpy->playername, s);
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
|
||||
CHECKPROP(rpy->flags = SDL_ReadLE32(file), u);
|
||||
}
|
||||
|
||||
CHECKPROP(rpy->numstages = SDL_ReadLE16(file), u);
|
||||
|
||||
if(!rpy->numstages) {
|
||||
log_error("%s: No stages in replay", source);
|
||||
return false;
|
||||
}
|
||||
|
||||
rpy->stages = malloc(sizeof(ReplayStage) * rpy->numstages);
|
||||
memset(rpy->stages, 0, sizeof(ReplayStage) * rpy->numstages);
|
||||
|
||||
for(int i = 0; i < rpy->numstages; ++i) {
|
||||
ReplayStage *stg = rpy->stages + i;
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
|
||||
CHECKPROP(stg->flags = SDL_ReadLE32(file), u);
|
||||
}
|
||||
|
||||
CHECKPROP(stg->stage = SDL_ReadLE16(file), u);
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV2) {
|
||||
CHECKPROP(stg->start_time = SDL_ReadLE64(file), u);
|
||||
CHECKPROP(stg->rng_seed = SDL_ReadLE64(file), u);
|
||||
} else {
|
||||
stg->rng_seed = SDL_ReadLE32(file);
|
||||
stg->start_time = stg->rng_seed;
|
||||
}
|
||||
|
||||
CHECKPROP(stg->diff = SDL_ReadU8(file), u);
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV0) {
|
||||
CHECKPROP(stg->plr_points = SDL_ReadLE64(file), zu);
|
||||
} else {
|
||||
CHECKPROP(stg->plr_points = SDL_ReadLE32(file), zu);
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
|
||||
CHECKPROP(stg->plr_continues_used = SDL_ReadU8(file), u);
|
||||
}
|
||||
|
||||
CHECKPROP(stg->plr_char = SDL_ReadU8(file), u);
|
||||
CHECKPROP(stg->plr_shot = SDL_ReadU8(file), u);
|
||||
CHECKPROP(stg->plr_pos_x = SDL_ReadLE16(file), u);
|
||||
CHECKPROP(stg->plr_pos_y = SDL_ReadLE16(file), u);
|
||||
CHECKPROP(stg->plr_focus = SDL_ReadU8(file), u); // FIXME remove and bump version
|
||||
CHECKPROP(stg->plr_power = SDL_ReadLE16(file), u);
|
||||
CHECKPROP(stg->plr_lives = SDL_ReadU8(file), u);
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV1) {
|
||||
CHECKPROP(stg->plr_life_fragments = SDL_ReadLE16(file), u);
|
||||
} else {
|
||||
CHECKPROP(stg->plr_life_fragments = SDL_ReadU8(file), u);
|
||||
}
|
||||
|
||||
CHECKPROP(stg->plr_bombs = SDL_ReadU8(file), u);
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV1) {
|
||||
CHECKPROP(stg->plr_bomb_fragments = SDL_ReadLE16(file), u);
|
||||
} else {
|
||||
CHECKPROP(stg->plr_bomb_fragments = SDL_ReadU8(file), u);
|
||||
}
|
||||
|
||||
CHECKPROP(stg->plr_inputflags = SDL_ReadU8(file), u);
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV0) {
|
||||
CHECKPROP(stg->plr_graze = SDL_ReadLE32(file), u);
|
||||
CHECKPROP(stg->plr_point_item_value = SDL_ReadLE32(file), u);
|
||||
} else if(version >= REPLAY_STRUCT_VERSION_TS102000_REV2) {
|
||||
CHECKPROP(stg->plr_graze = SDL_ReadLE16(file), u);
|
||||
stg->plr_point_item_value = PLR_START_PIV;
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS103000_REV3) {
|
||||
CHECKPROP(stg->plr_points_final = SDL_ReadLE64(file), zu);
|
||||
}
|
||||
|
||||
if(version >= REPLAY_STRUCT_VERSION_TS104000_REV0) {
|
||||
CHECKPROP(stg->plr_stats_total_lives_used = SDL_ReadU8(file), u);
|
||||
CHECKPROP(stg->plr_stats_stage_lives_used = SDL_ReadU8(file), u);
|
||||
CHECKPROP(stg->plr_stats_total_bombs_used = SDL_ReadU8(file), u);
|
||||
CHECKPROP(stg->plr_stats_stage_bombs_used = SDL_ReadU8(file), u);
|
||||
CHECKPROP(stg->plr_stats_stage_continues_used = SDL_ReadU8(file), u);
|
||||
}
|
||||
|
||||
CHECKPROP(stg->num_events = SDL_ReadLE16(file), u);
|
||||
|
||||
if(replay_calc_stageinfo_checksum(stg, version) + SDL_ReadLE32(file)) {
|
||||
log_error("%s: Stageinfo is corrupt", source);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool replay_read_meta(Replay *rpy, SDL_RWops *file, int64_t filesize, const char *source) {
|
||||
rpy->playername = NULL;
|
||||
rpy->stages = NULL;
|
||||
|
||||
if(!_replay_read_meta(rpy, file, filesize, source)) {
|
||||
free(rpy->playername);
|
||||
free(rpy->stages);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _replay_read_events(Replay *rpy, SDL_RWops *file, int64_t filesize, const char *source) {
|
||||
for(int i = 0; i < rpy->numstages; ++i) {
|
||||
ReplayStage *stg = rpy->stages + i;
|
||||
|
||||
if(!stg->num_events) {
|
||||
log_error("%s: No events in stage", source);
|
||||
return false;
|
||||
}
|
||||
|
||||
dynarray_ensure_capacity(&stg->events, stg->num_events);
|
||||
|
||||
for(int j = 0; j < stg->num_events; ++j) {
|
||||
ReplayEvent *evt = dynarray_append(&stg->events);
|
||||
|
||||
CHECKPROP(evt->frame = SDL_ReadLE32(file), u);
|
||||
CHECKPROP(evt->type = SDL_ReadU8(file), u);
|
||||
CHECKPROP(evt->value = SDL_ReadLE16(file), u);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool replay_read_events(Replay *rpy, SDL_RWops *file, int64_t filesize, const char *source) {
|
||||
if(!_replay_read_events(rpy, file, filesize, source)) {
|
||||
replay_destroy_events(rpy);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool replay_read(Replay *rpy, SDL_RWops *file, ReplayReadMode mode, const char *source) {
|
||||
int64_t filesize; // must be signed
|
||||
SDL_RWops *vfile = file;
|
||||
|
||||
if(!source) {
|
||||
source = "<unknown>";
|
||||
}
|
||||
|
||||
if(!(mode & REPLAY_READ_ALL) ) {
|
||||
log_fatal("%s: Called with invalid read mode %x", source, mode);
|
||||
}
|
||||
|
||||
mode &= REPLAY_READ_ALL;
|
||||
filesize = SDL_RWsize(file);
|
||||
|
||||
if(filesize < 0) {
|
||||
log_warn("%s: SDL_RWsize() failed: %s", source, SDL_GetError());
|
||||
}
|
||||
|
||||
if(mode & REPLAY_READ_META) {
|
||||
memset(rpy, 0, sizeof(Replay));
|
||||
|
||||
if(filesize > 0 && filesize <= sizeof(replay_magic_header) + 2) {
|
||||
log_error("%s: Replay file is too short (%"PRIi64")", source, filesize);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ofs = 0;
|
||||
|
||||
if(!replay_read_header(rpy, file, filesize, &ofs, source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool compression = false;
|
||||
|
||||
if(rpy->version & REPLAY_VERSION_COMPRESSION_BIT) {
|
||||
if(rpy->fileoffset < SDL_RWtell(file)) {
|
||||
log_error("%s: Invalid offset %"PRIi32"", source, rpy->fileoffset);
|
||||
return false;
|
||||
}
|
||||
|
||||
vfile = SDL_RWWrapZlibReader(
|
||||
SDL_RWWrapSegment(file, ofs, rpy->fileoffset, false),
|
||||
REPLAY_COMPRESSION_CHUNK_SIZE,
|
||||
true
|
||||
);
|
||||
|
||||
filesize = -1;
|
||||
compression = true;
|
||||
}
|
||||
|
||||
if(!replay_read_meta(rpy, vfile, filesize, source)) {
|
||||
if(compression) {
|
||||
SDL_RWclose(vfile);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if(compression) {
|
||||
SDL_RWclose(vfile);
|
||||
vfile = file;
|
||||
} else {
|
||||
rpy->fileoffset = SDL_RWtell(file);
|
||||
}
|
||||
}
|
||||
|
||||
if(mode & REPLAY_READ_EVENTS) {
|
||||
if(!(mode & REPLAY_READ_META)) {
|
||||
if(!rpy->fileoffset) {
|
||||
log_fatal("%s: Tried to read events before reading metadata", source);
|
||||
}
|
||||
|
||||
for(int i = 0; i < rpy->numstages; ++i) {
|
||||
if(rpy->stages->events.data) {
|
||||
log_warn("%s: BUG: Reading events into a replay that already had events, call replay_destroy_events() if this is intended", source);
|
||||
replay_destroy_events(rpy);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(SDL_RWseek(file, rpy->fileoffset, RW_SEEK_SET) < 0) {
|
||||
log_error("%s: SDL_RWseek() failed: %s", source, SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool compression = false;
|
||||
|
||||
if(rpy->version & REPLAY_VERSION_COMPRESSION_BIT) {
|
||||
vfile = SDL_RWWrapZlibReader(file, REPLAY_COMPRESSION_CHUNK_SIZE, false);
|
||||
filesize = -1;
|
||||
compression = true;
|
||||
}
|
||||
|
||||
if(!replay_read_events(rpy, vfile, filesize, source)) {
|
||||
if(compression) {
|
||||
SDL_RWclose(vfile);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if(compression) {
|
||||
SDL_RWclose(vfile);
|
||||
}
|
||||
|
||||
// useless byte to simplify the premature EOF check, can be anything
|
||||
SDL_ReadU8(file);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#undef CHECKPROP
|
||||
#undef PRINTPROP
|
||||
|
||||
static char* replay_getpath(const char *name, bool ext) {
|
||||
return ext ? strfmt("storage/replays/%s.%s", name, REPLAY_EXTENSION) :
|
||||
strfmt("storage/replays/%s", name);
|
||||
}
|
||||
|
||||
bool replay_save(Replay *rpy, const char *name) {
|
||||
char *p = replay_getpath(name, !strendswith(name, REPLAY_EXTENSION));
|
||||
char *sp = vfs_repr(p, true);
|
||||
log_info("Saving %s", sp);
|
||||
free(sp);
|
||||
|
||||
SDL_RWops *file = vfs_open(p, VFS_MODE_WRITE);
|
||||
free(p);
|
||||
|
||||
if(!file) {
|
||||
log_error("VFS error: %s", vfs_get_error());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = replay_write(rpy, file, REPLAY_STRUCT_VERSION_WRITE);
|
||||
SDL_RWclose(file);
|
||||
vfs_sync(VFS_SYNC_STORE, NO_CALLCHAIN);
|
||||
return result;
|
||||
}
|
||||
|
||||
static const char* replay_mode_string(ReplayReadMode mode) {
|
||||
if((mode & REPLAY_READ_ALL) == REPLAY_READ_ALL) {
|
||||
return "full";
|
||||
}
|
||||
|
||||
if(mode & REPLAY_READ_META) {
|
||||
return "meta";
|
||||
}
|
||||
|
||||
if(mode & REPLAY_READ_EVENTS) {
|
||||
return "events";
|
||||
}
|
||||
|
||||
log_fatal("Bad mode %i", mode);
|
||||
}
|
||||
|
||||
bool replay_load(Replay *rpy, const char *name, ReplayReadMode mode) {
|
||||
char *p = replay_getpath(name, !strendswith(name, REPLAY_EXTENSION));
|
||||
char *sp = vfs_repr(p, true);
|
||||
log_info("Loading %s (%s)", sp, replay_mode_string(mode));
|
||||
|
||||
SDL_RWops *file = vfs_open(p, VFS_MODE_READ);
|
||||
free(p);
|
||||
|
||||
if(!file) {
|
||||
log_error("VFS error: %s", vfs_get_error());
|
||||
free(sp);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = replay_read(rpy, file, mode, sp);
|
||||
|
||||
free(sp);
|
||||
SDL_RWclose(file);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool replay_load_syspath(Replay *rpy, const char *path, ReplayReadMode mode) {
|
||||
log_info("Loading %s (%s)", path, replay_mode_string(mode));
|
||||
SDL_RWops *file;
|
||||
|
||||
#ifndef __WINDOWS__
|
||||
if(!strcmp(path, "-"))
|
||||
file = SDL_RWFromFP(stdin,false);
|
||||
else
|
||||
file = SDL_RWFromFile(path, "rb");
|
||||
#else
|
||||
file = SDL_RWFromFile(path, "rb");
|
||||
#endif
|
||||
|
||||
if(!file) {
|
||||
log_error("SDL_RWFromFile() failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = replay_read(rpy, file, mode, path);
|
||||
|
||||
SDL_RWclose(file);
|
||||
return result;
|
||||
}
|
||||
|
||||
void replay_copy(Replay *dst, Replay *src, bool steal_events) {
|
||||
int i;
|
||||
|
||||
replay_destroy(dst);
|
||||
memcpy(dst, src, sizeof(Replay));
|
||||
|
||||
dst->playername = strdup(src->playername);
|
||||
dst->stages = memdup(src->stages, sizeof(*src->stages) * src->numstages);
|
||||
|
||||
for(i = 0; i < src->numstages; ++i) {
|
||||
ReplayStage *s, *d;
|
||||
s = src->stages + i;
|
||||
d = dst->stages + i;
|
||||
|
||||
if(steal_events) {
|
||||
memset(&s->events, 0, sizeof(s->events));
|
||||
} else {
|
||||
dynarray_set_elements(&d->events, s->events.num_elements, s->events.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void replay_stage_check_desync(ReplayStage *stg, int time, uint16_t check, ReplayMode mode) {
|
||||
if(!stg || time % (FPS * 5)) {
|
||||