diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1291447c..e88b1ed6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/replay.c b/src/replay.c index 8e3af387..34be6226 100644 --- a/src/replay.c +++ b/src/replay.c @@ -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'); diff --git a/src/replay.h b/src/replay.h index 3bc59d0f..dc4dd6f9 100644 --- a/src/replay.h +++ b/src/replay.h @@ -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); diff --git a/src/version.c b/src/version.c new file mode 100644 index 00000000..492da250 --- /dev/null +++ b/src/version.c @@ -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 . + * Copyright (c) 2012-2017, Andrei Alexeyev . + */ + +#include + +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); +} diff --git a/src/version.h b/src/version.h index af901c1d..b6ae0fe7 100644 --- a/src/version.h +++ b/src/version.h @@ -9,7 +9,7 @@ #ifndef TSVERSION_H #define TSVERSION_H -#include +#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 diff --git a/src/version.c.in b/src/version_auto.c.in similarity index 100% rename from src/version.c.in rename to src/version_auto.c.in