taisei/src/replay/read.c

427 lines
11 KiB
C

/*
* 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 "rw_common.h"
#include "player.h"
#include "rwops/rwops_segment.h"
typedef struct ReplayReadContext {
Replay *replay;
SDL_RWops *stream;
const char *filename;
int64_t filesize;
uint16_t version;
ReplayReadMode mode;
} ReplayReadContext;
#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 RETURN_ERROR if(!(ctx->mode & REPLAY_READ_IGNORE_ERRORS)) return false
#define CHECKPROP(prop, fmt) \
do { \
PRINTPROP(prop,fmt); \
if(ctx->filesize > 0 && SDL_RWtell(ctx->stream) == ctx->filesize) { \
log_error("%s: Premature EOF", ctx->filename); \
RETURN_ERROR; \
} \
} while(0)
static void replay_read_string(ReplayReadContext *ctx, char **ptr) {
size_t len;
if(ctx->version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
len = SDL_ReadU8(ctx->stream);
} else {
len = SDL_ReadLE16(ctx->stream);
}
*ptr = mem_alloc(len + 1);
SDL_RWread(ctx->stream, *ptr, 1, len);
}
static bool replay_read_header(
ReplayReadContext *ctx,
size_t *ofs
) {
auto rpy = ctx->replay;
auto file = ctx->stream;
auto source = ctx->filename;
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_ERROR;
}
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:
case REPLAY_STRUCT_VERSION_TS104000_REV1:
{
if(taisei_version_read(file, &rpy->game_version) != TAISEI_VERSION_SIZE) {
log_error("%s: Failed to read game version", source);
RETURN_ERROR;
}
(*ofs) += TAISEI_VERSION_SIZE;
break;
}
default: {
log_error("%s: Unknown struct version %u", source, base_version);
RETURN_ERROR;
}
}
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)" : "");
mem_free(gamev);
if(compression) {
CHECKPROP(rpy->fileoffset = SDL_ReadLE32(file), u);
(*ofs) += 4;
}
return true;
}
static bool replay_read_stage(ReplayReadContext *ctx, ReplayStage *stg) {
auto version = ctx->version;
auto file = ctx->stream;
*stg = (ReplayStage) { };
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_TS104000_REV1) {
CHECKPROP(stg->skip_frames = SDL_ReadLE16(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_TS104000_REV1) {
CHECKPROP(stg->plr_total_lives_used = SDL_ReadU8(file), u);
CHECKPROP(stg->plr_total_bombs_used = SDL_ReadU8(file), u);
}
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
CHECKPROP(stg->plr_total_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);
if(version <= REPLAY_STRUCT_VERSION_TS104000_REV0) {
// NOTE: old plr_focus field, ignored
SDL_ReadU8(file);
}
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) {
// NOTE: These fields were always bugged, so we ignore them
uint8_t junk[5];
SDL_RWread(file, junk, sizeof(junk), 1);
}
if(version >= REPLAY_STRUCT_VERSION_TS104000_REV1) {
CHECKPROP(stg->plr_stage_lives_used_final = SDL_ReadU8(file), u);
CHECKPROP(stg->plr_stage_bombs_used_final = SDL_ReadU8(file), u);
CHECKPROP(stg->plr_stage_continues_used_final = SDL_ReadU8(file), u);
}
CHECKPROP(stg->num_events = SDL_ReadLE16(file), u);
if(replay_struct_stage_metadata_checksum(stg, version) + SDL_ReadLE32(file)) {
log_error("%s: Stageinfo is corrupt", ctx->filename);
RETURN_ERROR;
}
return true;
}
static bool _replay_read_meta(ReplayReadContext *ctx) {
auto rpy = ctx->replay;
auto file = ctx->stream;
uint16_t version = rpy->version & ~REPLAY_VERSION_COMPRESSION_BIT;
ctx->version = version;
rpy->playername = NULL;
replay_read_string(ctx, &rpy->playername);
PRINTPROP(rpy->playername, s);
if(version >= REPLAY_STRUCT_VERSION_TS102000_REV1) {
CHECKPROP(rpy->flags = SDL_ReadLE32(file), u);
}
uint64_t numstages;
CHECKPROP(numstages = SDL_ReadLE16(file), u);
if(!numstages) {
log_error("%s: No stages in replay", ctx->filename);
RETURN_ERROR;
}
dynarray_ensure_capacity(&rpy->stages, numstages);
for(int i = 0; i < numstages; ++i) {
if(!replay_read_stage(ctx, dynarray_append(&rpy->stages))) {
RETURN_ERROR;
}
}
return true;
}
static bool replay_read_meta(ReplayReadContext *ctx) {
if(!_replay_read_meta(ctx)) {
mem_free(ctx->replay->playername);
ctx->replay->playername = NULL;
dynarray_free_data(&ctx->replay->stages);
return false;
}
return true;
}
static bool _replay_read_events(ReplayReadContext *ctx) {
dynarray_foreach_elem(&ctx->replay->stages, ReplayStage *stg, {
if(!stg->num_events) {
log_error("%s: No events in stage", ctx->filename);
RETURN_ERROR;
}
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(ctx->stream), u);
CHECKPROP(evt->type = SDL_ReadU8(ctx->stream), u);
CHECKPROP(evt->value = SDL_ReadLE16(ctx->stream), u);
}
});
return true;
}
static bool replay_read_events(ReplayReadContext *ctx) {
if(!_replay_read_events(ctx)) {
replay_destroy_events(ctx->replay);
return false;
}
return true;
}
bool replay_read(Replay *rpy, SDL_RWops *file, ReplayReadMode mode, const char *source) {
int64_t filesize; // must be signed
if(!source) {
source = "<unknown>";
}
assert((mode & REPLAY_READ_ALL) != 0);
filesize = SDL_RWsize(file);
if(filesize < 0) {
log_warn("%s: SDL_RWsize() failed: %s", source, SDL_GetError());
}
ReplayReadContext _ctx = {
.replay = rpy,
.stream = file,
.filename = source,
.filesize = filesize,
.version = rpy->version & ~REPLAY_VERSION_COMPRESSION_BIT,
.mode = mode,
};
auto ctx = &_ctx;
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);
if(!(mode & REPLAY_READ_IGNORE_ERRORS)) {
return false;
}
}
size_t ofs = 0;
if(!replay_read_header(ctx, &ofs)) {
RETURN_ERROR;
}
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_ERROR;
}
ctx->stream = replay_wrap_stream_decompress(
rpy->version,
SDL_RWWrapSegment(file, ofs, rpy->fileoffset, false),
true
);
ctx->filesize = -1;
compression = true;
}
if(!replay_read_meta(ctx)) {
if(compression) {
SDL_RWclose(ctx->stream);
}
return false;
}
if(compression) {
SDL_RWclose(ctx->stream);
ctx->stream = 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);
}
dynarray_foreach_elem(&rpy->stages, ReplayStage *stg, {
if(stg->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) {
ctx->stream = replay_wrap_stream_decompress(rpy->version, file, false);
ctx->filesize = -1;
compression = true;
}
if(!replay_read_events(ctx)) {
if(compression) {
SDL_RWclose(ctx->stream);
}
return false;
}
if(compression) {
SDL_RWclose(ctx->stream);
}
// useless byte to simplify the premature EOF check, can be anything
SDL_ReadU8(file);
}
return true;
}