include game version info in replays

the game can still read old v1.1 replays, and currently writes in that
version by default, too. it should be defaulted to the new format when
the first gameplay change from v1.1 is made.
This commit is contained in:
Andrei "Akari" Alexeyev 2017-09-26 22:50:22 +03:00
parent 57e70931ad
commit 76205422b3
No known key found for this signature in database
GPG key ID: 048C3D2A5648B785
6 changed files with 187 additions and 25 deletions

View file

@ -28,8 +28,8 @@ if(NOT NO_AUDIO)
endif()
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/version.c.in"
"${CMAKE_CURRENT_BINARY_DIR}/version.c"
"${CMAKE_CURRENT_SOURCE_DIR}/version_auto.c.in"
"${CMAKE_CURRENT_BINARY_DIR}/version_auto.c"
)
set(SRCs
@ -115,7 +115,8 @@ set(SRCs
vfs/vdir.c
vfs/zipfile.c
vfs/zippath.c
"${CMAKE_CURRENT_BINARY_DIR}/version.c"
version.c
"${CMAKE_CURRENT_BINARY_DIR}/version_auto.c"
)
if(WIN32)

View file

@ -184,18 +184,24 @@ static bool replay_write_stage(ReplayStage *stg, SDL_RWops *file) {
return true;
}
bool replay_write(Replay *rpy, SDL_RWops *file, bool compression) {
uint16_t version = REPLAY_STRUCT_VERSION;
bool replay_write(Replay *rpy, SDL_RWops *file, uint16_t version) {
uint16_t base_version = (version & ~REPLAY_VERSION_COMPRESSION_BIT);
bool compression = (version & REPLAY_VERSION_COMPRESSION_BIT);
int i, j;
SDL_RWwrite(file, replay_magic_header, sizeof(replay_magic_header), 1);
if(compression) {
version |= REPLAY_VERSION_COMPRESSION_BIT;
}
SDL_WriteLE16(file, version);
if(base_version >= REPLAY_STRUCT_VERSION_TS102000_REV0) {
TaiseiVersion v;
TAISEI_VERSION_GET_CURRENT(&v);
if(taisei_version_write(file, &v) != TAISEI_VERSION_SIZE) {
log_warn("Failed to write game version: %s", SDL_GetError());
return false;
}
}
void *buf;
SDL_RWops *abuf = NULL;
SDL_RWops *vfile = file;
@ -281,16 +287,44 @@ static bool replay_read_header(Replay *rpy, SDL_RWops *file, int64_t filesize, s
CHECKPROP(rpy->version = SDL_ReadLE16(file), u);
(*ofs) += 2;
if((rpy->version & ~REPLAY_VERSION_COMPRESSION_BIT) != REPLAY_STRUCT_VERSION) {
log_warn("%s: Incorrect version", source);
return false;
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: {
if(taisei_version_read(file, &rpy->game_version) != TAISEI_VERSION_SIZE) {
log_warn("%s: Failed to read game version", source);
return false;
}
(*ofs) += TAISEI_VERSION_SIZE;
break;
}
default: {
log_warn("%s: Unknown struct version %u", source, base_version);
return false;
}
}
if(rpy->version & REPLAY_VERSION_COMPRESSION_BIT) {
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;
}
(*ofs) += 4;
return true;
}
@ -494,13 +528,13 @@ bool replay_save(Replay *rpy, const char *name) {
return false;
}
bool result = replay_write(rpy, file, REPLAY_WRITE_COMPRESSED);
bool result = replay_write(rpy, file, REPLAY_STRUCT_VERSION_WRITE);
SDL_RWclose(file);
return result;
}
static const char* replay_mode_string(ReplayReadMode mode) {
if(mode & REPLAY_READ_ALL) {
if((mode & REPLAY_READ_ALL) == REPLAY_READ_ALL) {
return "full";
}
@ -630,7 +664,7 @@ int replay_test(void) {
SDL_RWwrite(handle, replay_magic_header, sizeof(replay_magic_header), 1);
SDL_WriteLE16(handle, REPLAY_STRUCT_VERSION);
SDL_WriteLE16(handle, REPLAY_STRUCT_VERSION_TS101000);
SDL_WriteLE16(handle, 4);
SDL_WriteU8(handle, 't');
SDL_WriteU8(handle, 'e');

View file

@ -11,6 +11,7 @@
#include "stage.h"
#include "player.h"
#include "version.h"
/*
@ -20,16 +21,25 @@
* Please maintain this convention, it makes it easier to grasp the replay file structure just by looking at this header.
*/
// -{ ALWAYS UPDATE THESE WHEN YOU MAKE CHANGES TO THE FILE/STRUCT LAYOUT!
// Lets us fail early on incompatible versions and garbage data
#define REPLAY_STRUCT_VERSION 5
// The struct version is a numeric designation given to the replay file format.
// Always bump it when making incompatible changes to the layout.
// If dropping support for a version, comment out its #define and remove all related code.
// -}
/* BEGIN supported struct versions */
// Taisei v1.1 legacy format
#define REPLAY_STRUCT_VERSION_TS101000 5
// Taisei v1.2 revision 0: like v1.1, but with game version information
#define REPLAY_STRUCT_VERSION_TS102000_REV0 6
/* END supported struct versions */
#define REPLAY_VERSION_COMPRESSION_BIT 0x8000
#define REPLAY_COMPRESSION_CHUNK_SIZE 4096
#define REPLAY_WRITE_COMPRESSED true
// What struct version to use when saving recorded replays
// XXX: the v1.1 format is used currently; change it and remove this line with the first gameplay change.
#define REPLAY_STRUCT_VERSION_WRITE (REPLAY_STRUCT_VERSION_TS101000 | REPLAY_VERSION_COMPRESSION_BIT)
#define REPLAY_ALLOC_INITIAL 256
@ -104,6 +114,13 @@ typedef struct Replay {
// must be equal to REPLAY_STRUCT_VERSION
uint16_t version;
/* BEGIN REPLAY_STRUCT_VERSION_TS102000_REV0 and above */
// Game version this replay was recorded on
TaiseiVersion game_version;
/* END REPLAY_STRUCT_VERSION_TS102000_REV0 and above */
// Where the events begin
// NOTE: this is not present in uncompressed replays!
uint32_t fileoffset;
@ -153,7 +170,7 @@ void replay_stage_event(ReplayStage *stg, uint32_t frame, uint8_t type, uint16_t
void replay_stage_check_desync(ReplayStage *stg, int time, uint16_t check, ReplayMode mode);
void replay_stage_sync_player_state(ReplayStage *stg, Player *plr);
bool replay_write(Replay *rpy, SDL_RWops *file, bool compression);
bool replay_write(Replay *rpy, SDL_RWops *file, uint16_t version);
bool replay_read(Replay *rpy, SDL_RWops *file, ReplayReadMode mode, const char *source);
bool replay_save(Replay *rpy, const char *name);

74
src/version.c Normal file
View file

@ -0,0 +1,74 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2017, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2017, Andrei Alexeyev <akari@alienslab.net>.
*/
#include <version.h>
int taisei_version_compare(TaiseiVersion *v1, TaiseiVersion *v2, TaiseiVersionCmpLevel level) {
int result = 0;
if((result = v1->major - v2->major) && level >= VCMP_MAJOR) return result;
if((result = v1->minor - v2->minor) && level >= VCMP_MINOR) return result;
if((result = v1->patch - v2->patch) && level >= VCMP_PATCH) return result;
if((result = v1->tweak - v2->tweak) && level >= VCMP_TWEAK) return result;
return result;
}
size_t taisei_version_read(SDL_RWops *rwops, TaiseiVersion *version) {
// XXX: detect errors somehow?
version->major = SDL_ReadU8(rwops);
version->minor = SDL_ReadU8(rwops);
version->patch = SDL_ReadU8(rwops);
version->tweak = SDL_ReadLE16(rwops);
return TAISEI_VERSION_SIZE;
}
size_t taisei_version_write(SDL_RWops *rwops, TaiseiVersion *version) {
size_t wrote_now = 0, wrote_total = 0;
if(!(wrote_now = SDL_WriteU8(rwops, version->major))) {
return wrote_total;
} else {
wrote_total += wrote_now;
}
if(!(wrote_now = SDL_WriteU8(rwops, version->minor))) {
return wrote_total;
} else {
wrote_total += wrote_now;
}
if(!(wrote_now = SDL_WriteU8(rwops, version->patch))) {
return wrote_total;
} else {
wrote_total += wrote_now;
}
if(!(wrote_now = 2 * SDL_WriteLE16(rwops, version->tweak))) {
return wrote_total;
} else {
wrote_total += wrote_now;
}
assert(wrote_total == TAISEI_VERSION_SIZE);
return wrote_total;
}
char* taisei_version_tostring(TaiseiVersion *version) {
if(!version->tweak) {
if(!version->patch) {
return strfmt("%u.%u", version->major, version->minor);
}
return strfmt("%u.%u.%u", version->major, version->minor, version->patch);
}
return strfmt("%u.%u.%u.%u", version->major, version->minor, version->patch, version->tweak);
}

View file

@ -9,7 +9,7 @@
#ifndef TSVERSION_H
#define TSVERSION_H
#include <stdint.h>
#include "util.h"
extern const char *const TAISEI_VERSION;
extern const char *const TAISEI_VERSION_FULL;
@ -20,4 +20,40 @@ extern const uint8_t TAISEI_VERSION_MINOR;
extern const uint8_t TAISEI_VERSION_PATCH;
extern const uint16_t TAISEI_VERSION_TWEAK;
typedef struct TaiseiVersion {
uint8_t major;
uint8_t minor;
uint8_t patch;
uint16_t tweak;
} TaiseiVersion;
#define TAISEI_VERSION_SET(v,ma,mi,pa,tw) { \
(v)->major = (ma); \
(v)->minor = (mi); \
(v)->patch = (pa); \
(v)->tweak = (tw); \
}
#define TAISEI_VERSION_GET_CURRENT(v) TAISEI_VERSION_SET(v, \
TAISEI_VERSION_MAJOR, \
TAISEI_VERSION_MINOR, \
TAISEI_VERSION_PATCH, \
TAISEI_VERSION_TWEAK \
)
typedef enum {
VCMP_MAJOR,
VCMP_MINOR,
VCMP_PATCH,
VCMP_TWEAK,
} TaiseiVersionCmpLevel;
// this is for IO purposes. sizeof(TaiseiVersion) may not match.
#define TAISEI_VERSION_SIZE (sizeof(uint8_t) * 3 + sizeof(uint16_t))
int taisei_version_compare(TaiseiVersion *v1, TaiseiVersion *v2, TaiseiVersionCmpLevel level);
char* taisei_version_tostring(TaiseiVersion *version);
size_t taisei_version_read(SDL_RWops *rwops, TaiseiVersion *version);
size_t taisei_version_write(SDL_RWops *rwops, TaiseiVersion *version);
#endif