/*
SDLPoP, a port/conversion of the DOS game Prince of Persia.
Copyright (C) 2013-2018 Dávid Nagy
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
The authors of this program may be contacted at http://forum.princed.org
*/
#include "common.h"
#include <time.h>
#ifndef _MSC_VER // unistd.h does not exist in the Windows SDK.
#include <unistd.h>
#endif
#include <sys/stat.h>
// Directory listing using dirent.h is available using MinGW on Windows, but not using MSVC (need to use Win32 API).
// NOTE: If we are using MinGW, we'll opt to use the Win32 API as well: dirent.h would just wrap Win32 anyway!
#ifdef _WIN32
#define USE_WIN32_API_FOR_LISTING_REPLAY_FILES
#endif
#ifdef USE_WIN32_API_FOR_LISTING_REPLAY_FILES
#include <windows.h>
#include <wchar.h>
#else
#include <dirent.h>
#endif
#ifdef USE_REPLAY
const char replay_magic_number[3] = "P1R";
const word replay_format_class = 0; // unique number associated with this SDLPoP implementation / fork
const char* implementation_name = "SDLPoP v" SDLPOP_VERSION;
#define REPLAY_FORMAT_CURR_VERSION 101 // current version number of the replay format
#define REPLAY_FORMAT_MIN_VERSION 101 // SDLPoP will open replays with this version number and higher
#define REPLAY_FORMAT_DEPRECATION_NUMBER 1 // SDLPoP won't open replays with a higher deprecation number
#define MAX_REPLAY_DURATION 345600 // 8 hours: 720 * 60 * 8 ticks
byte moves[MAX_REPLAY_DURATION] = {0}; // static memory for now because it is easier (should this be dynamic?)
char replay_levelset_name[POP_MAX_PATH];
char stored_levelset_name[POP_MAX_PATH];
// 1-byte structure representing which controls were active at a particular game tick
typedef union replay_move_type {
struct {
sbyte x : 2;
sbyte y : 2;
byte shift : 1;
byte special : 3; // enum replay_special_moves, see types.h
};
byte bits;
} replay_move_type;
dword curr_tick = 0;
FILE* replay_fp = NULL;
byte replay_file_open = 0;
int current_replay_number = 0;
int next_replay_number = 0;
byte* savestate_buffer = NULL;
dword savestate_offset = 0;
dword savestate_size = 0;
#define MAX_SAVESTATE_SIZE 4096
// These are defined in seg000.c:
typedef int process_func_type(void* data, size_t data_size);
extern int quick_process(process_func_type process_func);
extern const char quick_version[9];
// header information read from the first part of a replay file
typedef struct replay_header_type {
byte uses_custom_levelset;
char levelset_name[POP_MAX_PATH];
char implementation_name[POP_MAX_PATH];
} replay_header_type;
// information needed to keep track of all listed replay files, and to sort them by their creation date
typedef struct replay_info_type {
char filename[POP_MAX_PATH];
time_t creation_time;
replay_header_type header;
} replay_info_type;
#define REPLAY_HEADER_ERROR_MESSAGE_MAX 512
int read_replay_header(replay_header_type* header, FILE* fp, char* error_message) {
// Explicitly go to the beginning, because the current filepos might be nonzero.
// read the magic number
char magic[3] = "";
if (strncmp(magic
, replay_magic_number
, 3) != 0) {
if (error_message != NULL) {
snprintf(error_message
, REPLAY_HEADER_ERROR_MESSAGE_MAX
, "not a valid replay file!");
}
return 0; // incompatible, magic number not correct!
}
// read the unique number associated with this SDLPoP implementation / fork (for normal SDLPoP: 0)
word class;
fread(&class
, sizeof(class
), 1, fp
);
// read the format version number
byte version_number
= (byte
) fgetc(fp
);
// read the format deprecation number
byte deprecation_number
= (byte
) fgetc(fp
);
// creation time (seconds since 1970) is embedded in the format, but not used in SDLPoP right now
fseek(fp
, sizeof(Sint64
), SEEK_CUR
);
// read the levelset_name
byte len_read
= (byte
) fgetc(fp
);
header->uses_custom_levelset = (len_read != 0);
fread(header
->levelset_name
, sizeof(char), len_read
, fp
);
header->levelset_name[len_read] = '\0';
// read the implementation_name
len_read
= (byte
) fgetc(fp
);
fread(header
->implementation_name
, sizeof(char), len_read
, fp
);
header->implementation_name[len_read] = '\0';
if (class != replay_format_class) {
// incompatible, replay format is associated with a different implementation of SDLPoP
if (error_message != NULL) {
snprintf(error_message
, REPLAY_HEADER_ERROR_MESSAGE_MAX
,
"replay created with \"%s\"...\nIncompatible replay class identifier! (expected %d, found %d)",
header->implementation_name, replay_format_class, class);
}
return 0;
}
if (version_number < REPLAY_FORMAT_MIN_VERSION) {
// incompatible, replay format is too old
if (error_message != NULL) {
snprintf(error_message
, REPLAY_HEADER_ERROR_MESSAGE_MAX
,
"replay created with \"%s\"...\nReplay format version too old! (minimum %d, found %d)",
header->implementation_name, REPLAY_FORMAT_MIN_VERSION, version_number);
}
return 0;
}
if (deprecation_number > REPLAY_FORMAT_DEPRECATION_NUMBER) {
// incompatible, replay format is too new
if (error_message != NULL) {
snprintf(error_message
, REPLAY_HEADER_ERROR_MESSAGE_MAX
,
"replay created with \"%s\"...\nReplay deprecation number too new! (max %d, found %d)",
header->implementation_name, REPLAY_FORMAT_DEPRECATION_NUMBER, deprecation_number);
}
return 0;
}
if (is_validate_mode) {
static byte is_replay_info_printed = 0;
if (!is_replay_info_printed) {
printf("\nReplay created with %s.\n", header
->implementation_name
);
printf("Format: class identifier %d, version number %d, deprecation number %d.\n",
class, version_number, deprecation_number);
if (header->levelset_name[0] == '\0') {
printf("Levelset: original Prince of Persia.\n");
} else {
printf("Levelset: %s.\n", header
->levelset_name
);
}
is_replay_info_printed = 1; // do this only once
}
}
return 1;
}
int num_replay_files = 0; // number of listed replays
size_t max_replay_files = 128; // initially, may grow if there are > 128 replay files found
replay_info_type* replay_list = NULL;
// Compare function -- for qsort() in list_replay_files() below
// Compares creation dates of replays, so they can be loaded in reverse creation order (newest first)
static int compare_replay_creation_time(const void* a, const void* b)
{
return (int) difftime( ((replay_info_type
*)b
)->creation_time
, ((replay_info_type
*)a
)->creation_time
);
}
// OS abstraction for listing directory contents (for list_replay_files() below)
// - Under GNU/Linux, etc (or if compiling with MinGW on Windows), we can use dirent.h
// - Under Windows, we'd like to directly call the Win32 API. (Note: MSVC does not include dirent.h)
#ifdef USE_WIN32_API_FOR_LISTING_REPLAY_FILES
// These macros are from the SDL2 source. (src/core/windows/SDL_windows.h)
// The pointers returned by these macros must be freed with SDL_free().
#define WIN_StringToUTF8(S) SDL_iconv_string("UTF-8", "UTF-16LE", (char *)(S), (SDL_wcslen(S)+1)*sizeof(WCHAR))
#define WIN_UTF8ToString(S) (WCHAR *)SDL_iconv_string("UTF-16LE", "UTF-8", (char *)(S), SDL_strlen(S)+1)
FILE* fopen_UTF8(const char* filename, const char* mode);
#define fopen fopen_UTF8
int chdir_UTF8(const char* path);
#define chdir chdir_UTF8
// This hack is needed because SDL uses UTF-8 everywhere (even in argv!), but fopen on Windows uses whatever code page is currently set.
FILE* fopen_UTF8(const char* filename_UTF8, const char* mode_UTF8) {
WCHAR* filename_UTF16 = WIN_UTF8ToString(filename_UTF8);
WCHAR* mode_UTF16 = WIN_UTF8ToString(mode_UTF8);
FILE* result = _wfopen(filename_UTF16, mode_UTF16);
SDL_free(mode_UTF16);
SDL_free(filename_UTF16);
return result;
}
int chdir_UTF8(const char* path_UTF8) {
WCHAR* path_UTF16 = WIN_UTF8ToString(path_UTF8);
int result = _wchdir(path_UTF16);
SDL_free(path_UTF16);
return result;
}
typedef struct directory_listing_data_type {
char search_pattern[POP_MAX_PATH];
WIN32_FIND_DATAW find_data;
HANDLE search_handle;
char* current_filename_UTF8;
} directory_listing_type;
static inline bool init_directory_listing_and_find_first_file(directory_listing_type *data) {
data->current_filename_UTF8 = NULL;
snprintf( data
->search_pattern
, POP_MAX_PATH
, "%s\\*.p1r", replays_folder
);
WCHAR* search_pattern_UTF16 = WIN_UTF8ToString(data->search_pattern);
data->search_handle = FindFirstFileW( search_pattern_UTF16, &data->find_data );
SDL_free(search_pattern_UTF16);
return (data->search_handle != INVALID_HANDLE_VALUE);
}
static inline char* get_current_filename_from_directory_listing(directory_listing_type* data) {
SDL_free(data->current_filename_UTF8);
data->current_filename_UTF8 = NULL;
data->current_filename_UTF8 = WIN_StringToUTF8(data->find_data.cFileName);
return data->current_filename_UTF8;
}
static inline bool find_next_file(directory_listing_type* data) {
return (bool) FindNextFileW( data->search_handle, &data->find_data );
}
static inline void directory_listing_close(directory_listing_type *data) {
FindClose(data->search_handle);
SDL_free(data->current_filename_UTF8);
data->current_filename_UTF8 = NULL;
}
#else // use dirent.h API for listing replay files
typedef struct directory_listing_data_type {
DIR* dp;
char* found_filename;
} directory_listing_type;
static inline bool init_directory_listing_and_find_first_file(directory_listing_type *data) {
bool ok = false;
data->dp = opendir(replays_folder);
if (data->dp != NULL) {
struct dirent* ep;
while ((ep = readdir(data->dp))) {
char *ext
= strrchr(ep
->d_name
, '.');
if (ext != NULL && strcasecmp(ext, ".p1r") == 0) {
data->found_filename = ep->d_name;
ok = true;
break;
}
}
}
return ok;
}
static inline char* get_current_filename_from_directory_listing(directory_listing_type* data) {
return data->found_filename;
}
static inline bool find_next_file(directory_listing_type* data) {
bool ok = false;
struct dirent* ep;
while ((ep = readdir(data->dp))) {
char *ext
= strrchr(ep
->d_name
, '.');
if (ext != NULL && strcasecmp(ext, ".p1r") == 0) {
data->found_filename = ep->d_name;
ok = true;
break;
}
}
return ok;
}
static inline void directory_listing_close(directory_listing_type *data) {
closedir(data->dp);
}
#endif
void list_replay_files() {
if (replay_list == NULL) {
// need to allocate enough memory to store info about all replay files in the directory
replay_list
= malloc( max_replay_files
* sizeof( replay_info_type
) ); // will realloc() later if > 256 files exist
}
num_replay_files = 0;
directory_listing_type directory_listing = {0};
if (!init_directory_listing_and_find_first_file(&directory_listing)) {
return;
}
do {
++num_replay_files;
if (num_replay_files > max_replay_files) {
// too many files, expand the memory available for replay_list
max_replay_files += 128;
replay_list
= realloc( replay_list
, max_replay_files
* sizeof( replay_info_type
) );
}
replay_info_type* replay_info = &replay_list[num_replay_files - 1]; // current replay file
memset( replay_info
, 0, sizeof( replay_info_type
) );
// store the filename of the replay
snprintf( replay_info
->filename
, POP_MAX_PATH
, "%s/%s", replays_folder
,
get_current_filename_from_directory_listing(&directory_listing) );
// get the creation time
struct stat st;
if (stat( replay_info->filename, &st ) == 0) {
replay_info->creation_time = st.st_ctime;
}
// read and store the levelset name associated with the replay
FILE
* fp
= fopen( replay_info
->filename
, "rb" );
int ok = 0;
if (fp != NULL) {
ok = read_replay_header( &replay_info->header, fp, NULL );
}
if (!ok) --num_replay_files; // scrap the file if it is not compatible
} while (find_next_file(&directory_listing));
directory_listing_close(&directory_listing);
if (num_replay_files > 1) {
// sort listed replays by their creation date
qsort( replay_list
, (size_t) num_replay_files
, sizeof( replay_info_type
), compare_replay_creation_time
);
}
};
byte open_replay_file(const char *filename) {
if (replay_file_open
) fclose(replay_fp
);
replay_fp
= fopen(filename
, "rb");
if (replay_fp != NULL) {
replay_file_open = 1;
return 1;
}
else {
replay_file_open = 0;
return 0;
}
}
void change_working_dir_to_sdlpop_root() {
char* exe_path = g_argv[0];
// strip away everything after the last slash or backslash in the path
int len;
for (len
= strlen(exe_path
); len
> 0; --len
) {
if (exe_path[len] == '\\' || exe_path[len] == '/') {
break;
}
}
if (len > 0) {
char exe_dir[POP_MAX_PATH];
exe_dir[len] = '\0';
int result = chdir(exe_dir);
if (result != 0) {
perror("Can't change into SDLPoP directory");
}
}
};
// Called in pop_main(); check whether a replay file is being opened directly (double-clicked, dragged onto .exe, etc.)
void start_with_replay_file(const char *filename) {
if (open_replay_file(filename)) {
change_working_dir_to_sdlpop_root();
current_replay_number = -1; // don't cycle when pressing Tab
// We should read the header in advance so we know the levelset name
// then the game can immediately load the correct resources
replay_header_type header = {0};
char header_error_message[REPLAY_HEADER_ERROR_MESSAGE_MAX];
int ok = read_replay_header(&header, replay_fp, header_error_message);
if (!ok) {
char error_message[REPLAY_HEADER_ERROR_MESSAGE_MAX];
snprintf(error_message
, REPLAY_HEADER_ERROR_MESSAGE_MAX
,
"Error opening replay file: %s\n",
header_error_message);
fprintf(stderr
, "%s", error_message
);
replay_fp = NULL;
replay_file_open = 0;
if (is_validate_mode) // Validating replays is cmd-line only, so, no sense continuing from here.
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "SDLPoP", error_message, NULL);
return;
}
if (header.uses_custom_levelset) {
strncpy(replay_levelset_name
, header.
levelset_name, sizeof(replay_levelset_name
)); // use the replays's levelset
}
rewind(replay_fp
); // replay file is still open and will be read in load_replay() later
need_start_replay = 1; // will later call start_replay(), from init_record_replay()
}
}
int process_rw_write(SDL_RWops* rw, void* data, size_t data_size) {
return SDL_RWwrite(rw, data, data_size, 1);
}
int process_rw_read(SDL_RWops* rw, void* data, size_t data_size) {
return SDL_RWread(rw, data, data_size, 1);
// if this returns 0, most likely the end of the stream has been reached
}
// The functions options_process_* below each process (read/write) a section of options variables (using SDL_RWops)
// This is I/O for the *binary* representation of the relevant options - this gets saved as part of a replay.
typedef int rw_process_func_type(SDL_RWops* rw, void* data, size_t data_size);
typedef void process_options_section_func_type(SDL_RWops* rw, rw_process_func_type process_func);
#define process(x) if (!process_func(rw, &(x), sizeof(x))) return
void options_process_features(SDL_RWops* rw, rw_process_func_type process_func) {
process(enable_copyprot);
process(enable_quicksave);
process(enable_quicksave_penalty);
}
void options_process_enhancements(SDL_RWops* rw, rw_process_func_type process_func) {
process(use_fixes_and_enhancements);
process(enable_crouch_after_climbing);
process(enable_freeze_time_during_end_music);
process(enable_remember_guard_hp);
}
void options_process_fixes(SDL_RWops* rw, rw_process_func_type process_func) {
process(fix_gate_sounds);
process(fix_two_coll_bug);
process(fix_infinite_down_bug);
process(fix_gate_drawing_bug);
process(fix_bigpillar_climb);
process(fix_jump_distance_at_edge);
process(fix_edge_distance_check_when_climbing);
process(fix_painless_fall_on_guard);
process(fix_wall_bump_triggers_tile_below);
process(fix_stand_on_thin_air);
process(fix_press_through_closed_gates);
process(fix_grab_falling_speed);
process(fix_skeleton_chomper_blood);
process(fix_move_after_drink);
process(fix_loose_left_of_potion);
process(fix_guard_following_through_closed_gates);
process(fix_safe_landing_on_spikes);
process(fix_glide_through_wall);
process(fix_drop_through_tapestry);
process(fix_land_against_gate_or_tapestry);
process(fix_unintended_sword_strike);
process(fix_retreat_without_leaving_room);
process(fix_running_jump_through_tapestry);
process(fix_push_guard_into_wall);
process(fix_jump_through_wall_above_gate);
process(fix_chompers_not_starting);
process(fix_feather_interrupted_by_leveldoor);
process(fix_offscreen_guards_disappearing);
process(fix_move_after_sheathe);
}
void options_process_custom_general(SDL_RWops* rw, rw_process_func_type process_func) {
process(start_minutes_left);
process(start_ticks_left);
process(start_hitp);
process(max_hitp_allowed);
process(saving_allowed_first_level);
process(saving_allowed_last_level);
process(start_upside_down);
process(start_in_blind_mode);
process(copyprot_level);
process(drawn_tile_top_level_edge);
process(drawn_tile_left_level_edge);
process(level_edge_hit_tile);
process(allow_triggering_any_tile);
process(enable_wda_in_palace);
process(vga_palette);
process(first_level);
process(skip_title);
process(shift_L_allowed_until_level);
process(shift_L_reduced_minutes);
process(shift_L_reduced_ticks);
}
void options_process_custom_per_level(SDL_RWops* rw, rw_process_func_type process_func) {
process(tbl_level_type);
process(tbl_level_color);
process(tbl_guard_type);
process(tbl_guard_hp);
process(tbl_cutscenes_by_index);
}
#undef process
// struct for keeping track of both the normal and the replay options (which we want to easily switch between)
// (separately for each 'section', so adding future options becomes easy without messing up the format!)
typedef struct replay_options_section_type {
dword data_size;
byte replay_data[POP_MAX_OPTIONS_SIZE]; // binary representation of the options that are active during the replay
byte stored_data[POP_MAX_OPTIONS_SIZE]; // normal options are restored from this, after the replay is finished
process_options_section_func_type* section_func;
} replay_options_section_type;
replay_options_section_type replay_options_sections[] = {
{.section_func = options_process_features},
{.section_func = options_process_enhancements},
{.section_func = options_process_fixes},
{.section_func = options_process_custom_general},
{.section_func = options_process_custom_per_level},
};
// output the current options to a memory buffer (e.g. to remember them before a replay is loaded)
size_t save_options_to_buffer(void* options_buffer, size_t max_size, process_options_section_func_type* process_section_func) {
SDL_RWops* rw = SDL_RWFromMem(options_buffer, max_size);
process_section_func(rw, process_rw_write);
Sint64 section_size = SDL_RWtell(rw);
if (section_size < 0) section_size = 0;
SDL_RWclose(rw);
return (size_t) section_size;
}
void apply_cutscene_pointers() {
int i;
for (i = 0; i < 16; ++i) {
tbl_cutscenes[i] = tbl_cutscenes_lookup[tbl_cutscenes_by_index[i]];
}
}
// restore the options from a memory buffer (e.g. reapply the original options after a replay is finished)
void load_options_from_buffer(void* options_buffer, size_t options_size, process_options_section_func_type* process_section_func) {
SDL_RWops* rw = SDL_RWFromMem(options_buffer, options_size);
process_section_func(rw, process_rw_read);
apply_cutscene_pointers();
SDL_RWclose(rw);
}
void init_record_replay() {
if (!enable_replay) return;
if (check_param("record")) {
start_recording();
}
else if (need_start_replay || check_param("replay")) {
start_replay();
}
}
void replay_restore_level() {
// Need to restore the savestate at the right time (just before the first room of the level is drawn).
// Otherwise, for "on-the-fly" recordings, the screen will visibly "jump" to the replay savestate.
// This only needs to happen at the very beginning of the replay (curr_tick == 0)
if (curr_tick == 0) restore_savestate_from_buffer();
}
int process_to_buffer(void* data, size_t data_size) {
if (savestate_offset + data_size > MAX_SAVESTATE_SIZE) {
printf("Saving savestate to memory failed: buffer is overflowing!\n");
return 0;
}
memcpy(savestate_buffer
+ savestate_offset
, data
, data_size
);
savestate_offset += data_size;
return 1;
}
int process_load_from_buffer(void* data, size_t data_size) {
memcpy(data
, savestate_buffer
+ savestate_offset
, data_size
);
savestate_offset += data_size;
return 1;
}
int savestate_to_buffer() {
int ok = 0;
if (savestate_buffer == NULL)
savestate_buffer
= malloc(MAX_SAVESTATE_SIZE
);
if (savestate_buffer != NULL) {
savestate_offset = 0;
savestate_size = 0;
ok = quick_process(process_to_buffer);
savestate_size = savestate_offset;
}
return ok;
}
void reload_resources() {
// the replay's levelset might use different sounds, so we need to free and reload sounds
// (except the music (OGG) files, which take too long to reload and cannot (yet) be easily replaced by a mod)
reload_non_music_sounds();
free_all_chtabs_from(id_chtab_0_sword);
// chtabs 3 and higher will be freed/reloaded in load_lev_spr() (called by restore_room_after_quick_load())
// However, chtabs 0-2 are usually not freed at all (they are loaded only once, in init_game_main())
// So we should reload them manually (PRINCE.DAT and KID.DAT may still have been modified after all!)
dat_type* dat = open_dat("PRINCE.DAT", 0);
// PRINCE.DAT: sword
chtab_addrs[id_chtab_0_sword] = load_sprites_from_file(700, 1<<2, 1);
// PRINCE.DAT: flame, sword on floor, potion
chtab_addrs[id_chtab_1_flameswordpotion] = load_sprites_from_file(150, 1<<3, 1);
close_dat(dat);
load_kid_sprite(); // reloads chtab 2
}
int restore_savestate_from_buffer() {
int ok = 0;
savestate_offset = 0;
while (savestate_offset < savestate_size) {
ok = quick_process(process_load_from_buffer);
}
reload_resources();
restore_room_after_quick_load();
return ok;
}
void start_recording() {
curr_tick = 0;
recording = 1; // further set-up is done in add_replay_move, on the first gameplay tick
}
void add_replay_move() {
if (curr_tick == 0) {
prandom(1); // make sure random_seed is initialized
saved_random_seed = random_seed;
seed_was_init = 1;
savestate_to_buffer(); // create a savestate in memory
display_text_bottom("RECORDING");
text_time_total = 24;
text_time_remaining = 24;
}
replay_move_type curr_move = {{0}};
curr_move.x = control_x;
curr_move.y = control_y;
if (control_shift) curr_move.shift = 1;
if (special_move) {
curr_move.special = special_move;
special_move = 0;
}
moves[curr_tick] = curr_move.bits;
++curr_tick;
if (curr_tick >= MAX_REPLAY_DURATION) { // max replay length exceeded
stop_recording();
}
}
void stop_recording() {
recording = 0;
if (save_recorded_replay()) {
display_text_bottom("REPLAY SAVED");
} else {
display_text_bottom("REPLAY CANCELED");
}
text_time_total = 24;
text_time_remaining = 24;
}
void apply_replay_options() {
// store the current options, so they can be restored later
for (int i = 0; i < COUNT(replay_options_sections); ++i) {
save_options_to_buffer(replay_options_sections[i].stored_data, POP_MAX_OPTIONS_SIZE, replay_options_sections[i].section_func);
}
// apply the options from the memory buffer (max. replay_options_size bytes will be read)
for (int i = 0; i < COUNT(replay_options_sections); ++i) {
load_options_from_buffer(replay_options_sections[i].replay_data, replay_options_sections[i].data_size, replay_options_sections[i].section_func);
}
if (!use_fixes_and_enhancements) disable_fixes_and_enhancements();
enable_replay = 1; // just to be safe...
memcpy(stored_levelset_name
, levelset_name
, sizeof(levelset_name
));
memcpy(levelset_name
, replay_levelset_name
, sizeof(levelset_name
));
use_custom_levelset = (levelset_name[0] == '\0') ? 0 : 1;
reload_resources();
}
void restore_normal_options() {
// apply the stored options
for (int i = 0; i < COUNT(replay_options_sections); ++i) {
load_options_from_buffer(replay_options_sections[i].stored_data, POP_MAX_OPTIONS_SIZE, replay_options_sections[i].section_func);
}
start_level = -1; // may have been set to a different value by the replay
memcpy(levelset_name
, stored_levelset_name
, sizeof(levelset_name
));
use_custom_levelset = (levelset_name[0] == '\0') ? 0 : 1;
}
static void print_remaining_time() {
if (rem_min > 0) {
printf("Remaining time: %d min, %d sec, %d ticks. ",
rem_min - 1, rem_tick / 12, rem_tick % 12);
} else {
printf("Elapsed time: %d min, %d sec, %d ticks. ",
-(rem_min + 1), (719 - rem_tick) / 12, (719 - rem_tick) % 12);
}
printf("(rem_min=%d, rem_tick=%d)\n", rem_min
, rem_tick
);
}
void start_replay() {
if (!enable_replay) return;
need_start_replay = 0;
if (!is_validate_mode) {
list_replay_files();
if (num_replay_files == 0) return;
}
if (!load_replay()) return;
apply_replay_options();
replaying = 1;
curr_tick = 0;
}
void end_replay() {
if (!is_validate_mode) {
replaying = 0;
skipping_replay = 0;
restore_normal_options();
start_game();
} else {
printf("\nReplay ended in level %d, room %d.\n", current_level
, drawn_room
);
if (Kid.alive < 0)
else {
if (text_time_total == 288 && text_time_remaining <= 1) {
printf("Kid is dead. (Did not press button to continue.)\n");
} else {
}
}
print_remaining_time();
int minute_ticks = curr_tick % 720;
printf("Play duration: %d min, %d sec, %d ticks. (curr_tick=%d)\n\n",
curr_tick / 720, minute_ticks / 12, minute_ticks % 12, curr_tick);
if (num_replay_ticks != curr_tick) {
printf("WARNING: Play duration does not match replay length. (%d ticks)\n", num_replay_ticks
);
} else {
printf("Play duration matches replay length. (%d ticks)\n", num_replay_ticks
);
}
}
}
void do_replay_move() {
if (curr_tick == 0) {
random_seed = saved_random_seed;
seed_was_init = 1;
if (is_validate_mode) {
printf("Replay started in level %d, room %d.\n", current_level
, drawn_room
);
print_remaining_time();
skipping_replay = 1;
replay_seek_target = replay_seek_2_end;
}
}
if (curr_tick == num_replay_ticks) { // replay is finished
end_replay();
return;
}
if (current_level == next_level) {
replay_move_type curr_move;
curr_move.bits = moves[curr_tick];
control_x = curr_move.x;
control_y = curr_move.y;
// Ignore shift if the kid is dead: restart moves are hard-coded as a 'special move'.
if (rem_min != 0 && Kid.alive > 6)
control_shift = 0;
else
control_shift = (curr_move.shift) ? -1 : 0;
if (curr_move.special == MOVE_RESTART_LEVEL) { // restart level
stop_sounds();
is_restart_level = 1;
} else if (curr_move.special == MOVE_EFFECT_END) {
stop_sounds();
need_level1_music = 0;
is_feather_fall = 0;
}
// if (curr_tick > 5 ) printf("rem_tick: %d\t curr_tick: %d\tlast 5 moves: %d, %d, %d, %d, %d\n", rem_tick, curr_tick,
// moves[curr_tick-4], moves[curr_tick-3], moves[curr_tick-2], moves[curr_tick-1], moves[curr_tick]);
++curr_tick;
}
}
int save_recorded_replay() {
// prompt for replay filename
rect_type rect;
short bgcolor = color_8_darkgray;
short color = color_15_brightwhite;
current_target_surface = onscreen_surface_;
screen_updates_suspended = 1;
method_1_blit_rect(offscreen_surface, onscreen_surface_, ©prot_dialog->peel_rect, ©prot_dialog->peel_rect, 0);
draw_dialog_frame(copyprot_dialog);
shrink2_rect(&rect, ©prot_dialog->text_rect, 2, 1);
show_text_with_color(&rect, 0, 0, "Save replay\nenter the filename...\n\n", color_15_brightwhite);
clear_kbd_buf();
rect_type text_rect;
rect_type input_rect = {104, 64, 118, 256};
offset4_rect_add(&text_rect, &input_rect, -2, 0, 2, 0);
//peel_type* peel = read_peel_from_screen(&input_rect);
draw_rect(&text_rect, bgcolor);
current_target_surface = onscreen_surface_;
screen_updates_suspended = 0;
need_full_redraw = 1; // lazy: instead of neatly restoring the dialog peel, just redraw the whole screen
char input_filename[POP_MAX_PATH] = "";
int input_length;
do {
input_length = input_str(&input_rect, input_filename, 64, "", 0, 0, color, bgcolor);
} while (input_length == 0); // filename must be at least 1 character
if (input_length < 0) {
return 0; // Escape was pressed -> discard the replay
}
char full_filename[POP_MAX_PATH] = "";
snprintf(full_filename
, sizeof(full_filename
), "%s/%s.p1r", replays_folder
, input_filename
);
// create the "replays" folder if it does not exist already
#if defined WIN32 || _WIN32 || WIN64 || _WIN64
mkdir (replays_folder);
#else
mkdir (replays_folder, 0700);
#endif
// NOTE: We currently overwrite the replay file if it exists already. Maybe warn / ask for confirmation??
replay_fp
= fopen(full_filename
, "wb");
if (replay_fp != NULL) {
fwrite(replay_magic_number
, COUNT
(replay_magic_number
), 1, replay_fp
); // magic number "P1R"
fwrite(&replay_format_class
, sizeof(replay_format_class
), 1, replay_fp
);
putc(REPLAY_FORMAT_CURR_VERSION
, replay_fp
);
putc(REPLAY_FORMAT_DEPRECATION_NUMBER
, replay_fp
);
Sint64 seconds
= time(NULL
);
fwrite(&seconds
, sizeof(seconds
), 1, replay_fp
);
// levelset_name
putc(strnlen
(levelset_name
, UINT8_MAX
), replay_fp
); // length of the levelset name (is zero for original levels)
fputs(levelset_name
, replay_fp
);
// implementation name
putc(strnlen
(implementation_name
, UINT8_MAX
), replay_fp
);
fputs(implementation_name
, replay_fp
);
// embed a savestate into the replay
fwrite(&savestate_size
, sizeof(savestate_size
), 1, replay_fp
);
fwrite(savestate_buffer
, savestate_size
, 1, replay_fp
);
// save the options, organized per section
byte temp_options[POP_MAX_OPTIONS_SIZE];
for (int i = 0; i < COUNT(replay_options_sections); ++i) {
dword section_size = save_options_to_buffer(temp_options, sizeof(temp_options), replay_options_sections[i].section_func);
fwrite(§ion_size
, sizeof(section_size
), 1, replay_fp
);
fwrite(temp_options
, section_size
, 1, replay_fp
);
}
// save the rest of the replay data
fwrite(&start_level
, sizeof(start_level
), 1, replay_fp
);
fwrite(&saved_random_seed
, sizeof(saved_random_seed
), 1, replay_fp
);
num_replay_ticks = curr_tick;
fwrite(&num_replay_ticks
, sizeof(num_replay_ticks
), 1, replay_fp
);
fwrite(moves
, num_replay_ticks
, 1, replay_fp
);
replay_fp = NULL;
}
return 1;
}
byte open_next_replay_file() {
if (next_replay_number > num_replay_files-1) {
return 0; // reached the last replay file, return to title screen
}
current_replay_number = next_replay_number;
++next_replay_number; // cycle
open_replay_file(replay_list[current_replay_number].filename);
if (replay_file_open) {
return 1;
}
return 0;
}
void replay_cycle() {
need_replay_cycle = 0;
skipping_replay = 0;
stop_sounds();
if (current_replay_number == -1 /* opened .P1R file directly, so cycling is disabled */ ||
!open_next_replay_file() ||
!load_replay()
) {
// there is no replay to be cycled to after the current one --> restart the game
replaying = 0;
restore_normal_options();
start_game();
return;
}
curr_tick = 0;
apply_replay_options();
restore_savestate_from_buffer();
show_level();
}
int load_replay() {
if (!replay_file_open) {
next_replay_number = 0;
if (!open_next_replay_file()) {
return 0;
}
}
if (savestate_buffer == NULL)
savestate_buffer
= malloc(MAX_SAVESTATE_SIZE
);
if (replay_fp != NULL && savestate_buffer != NULL) {
replay_header_type header = {0};
char error_message[REPLAY_HEADER_ERROR_MESSAGE_MAX];
int ok = read_replay_header(&header, replay_fp, error_message);
if (!ok) {
printf("Error loading replay: %s!\n", error_message
);
replay_fp = NULL;
replay_file_open = 0;
return 0;
}
memcpy(replay_levelset_name
, header.
levelset_name, sizeof(header.
levelset_name));
// load the savestate
fread(&savestate_size
, sizeof(savestate_size
), 1, replay_fp
);
fread(savestate_buffer
, savestate_size
, 1, replay_fp
);
// load the replay options, organized per section
for (int i = 0; i < COUNT(replay_options_sections); ++i) {
dword section_size = 0;
fread(§ion_size
, sizeof(section_size
), 1, replay_fp
);
fread(replay_options_sections
[i
].
replay_data, section_size
, 1, replay_fp
);
replay_options_sections[i].data_size = section_size;
}
// load the rest of the replay data
fread(&start_level
, sizeof(start_level
), 1, replay_fp
);
fread(&saved_random_seed
, sizeof(saved_random_seed
), 1, replay_fp
);
fread(&num_replay_ticks
, sizeof(num_replay_ticks
), 1, replay_fp
);
fread(moves
, num_replay_ticks
, 1, replay_fp
);
replay_fp = NULL;
replay_file_open = 0;
return 1; // success
}
return 0;
}
void key_press_while_recording(int* key_ptr) {
int key = *key_ptr;
switch(key) {
case SDL_SCANCODE_A | WITH_CTRL:
special_move = MOVE_RESTART_LEVEL;
break;
case SDL_SCANCODE_R | WITH_CTRL:
save_recorded_replay();
recording = 0;
default:
break;
}
}
void key_press_while_replaying(int* key_ptr) {
int key = *key_ptr;
switch(key) {
case 0: // 'no key pressed'
break;
default:
// cannot manually do most stuff during a replay, so cancel the pressed key...
*key_ptr = 1; // don't set to zero (we would be unable to unpause a replay because all keys are ignored)
// (1 is not in use as a scancode, see https://wiki.libsdl.org/SDLScancodeLookup)
break;
// ...but these are allowable actions:
case SDL_SCANCODE_ESCAPE: // pause
case SDL_SCANCODE_ESCAPE | WITH_SHIFT:
case SDL_SCANCODE_SPACE: // time
case SDL_SCANCODE_S | WITH_CTRL: // sound toggle
case SDL_SCANCODE_V | WITH_CTRL: // version
case SDL_SCANCODE_C: // room numbers
case SDL_SCANCODE_C | WITH_SHIFT:
case SDL_SCANCODE_I | WITH_SHIFT: // invert
case SDL_SCANCODE_B | WITH_SHIFT: // blind
case SDL_SCANCODE_T: // debug time
break;
case SDL_SCANCODE_R | WITH_CTRL: // restart game
replaying = 0;
restore_normal_options();
break;
case SDL_SCANCODE_TAB:
need_replay_cycle = 1;
restore_normal_options();
break;
case SDL_SCANCODE_F: // skip forward to next room
skipping_replay = 1;
replay_seek_target = replay_seek_0_next_room;
break;
case SDL_SCANCODE_F | WITH_SHIFT: // skip forward to start of next level
skipping_replay = 1;
replay_seek_target = replay_seek_1_next_level;
break;
}
}
#endif // USE_REPLAY