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:
Andrei Alexeyev 2021-05-30 04:26:21 +03:00
parent 239bec95b0
commit 173c8c3cc6
No known key found for this signature in database
GPG key ID: 72D26128040B9690
30 changed files with 1449 additions and 1164 deletions

View file

@ -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);

View file

@ -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) {

View file

@ -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;

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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) {

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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)) {