Binary movies support
This commit is contained in:
parent
a2fc2a19ec
commit
75b3e79ea7
9 changed files with 657 additions and 63 deletions
|
@ -1,6 +1,7 @@
|
|||
#ifndef _mainloop__hpp__included__
|
||||
#define _mainloop__hpp__included__
|
||||
|
||||
#include "settings.hpp"
|
||||
#include "rom.hpp"
|
||||
#include "moviefile.hpp"
|
||||
#include "movie.hpp"
|
||||
|
@ -27,4 +28,8 @@ void set_stop_at_frame(uint64_t frame = 0);
|
|||
void switch_projects(const std::string& newproj);
|
||||
void close_rom();
|
||||
|
||||
extern setting_var<setting_var_model_bool<setting_yes_no>> jukebox_dflt_binary;
|
||||
extern setting_var<setting_var_model_bool<setting_yes_no>> movie_dflt_binary;
|
||||
extern setting_var<setting_var_model_bool<setting_yes_no>> save_dflt_binary;
|
||||
|
||||
#endif
|
||||
|
|
|
@ -26,12 +26,12 @@ std::string resolve_relative_path(const std::string& path);
|
|||
std::pair<std::string, std::string> split_author(const std::string& author) throw(std::bad_alloc,
|
||||
std::runtime_error);
|
||||
|
||||
void do_save_state(const std::string& filename) throw(std::bad_alloc, std::runtime_error);
|
||||
void do_save_movie(const std::string& filename) throw(std::bad_alloc, std::runtime_error);
|
||||
void do_save_state(const std::string& filename, int binary) throw(std::bad_alloc, std::runtime_error);
|
||||
void do_save_movie(const std::string& filename, int binary) throw(std::bad_alloc, std::runtime_error);
|
||||
void do_load_beginning(bool reloading = false) throw(std::bad_alloc, std::runtime_error);
|
||||
void do_load_state(struct moviefile& _movie, int lmode);
|
||||
bool do_load_state(const std::string& filename, int lmode);
|
||||
std::string translate_name_mprefix(std::string original);
|
||||
std::string translate_name_mprefix(std::string original, int& binary, bool save);
|
||||
|
||||
extern std::string last_save;
|
||||
extern movie_logic movb;
|
||||
|
|
|
@ -37,10 +37,12 @@ struct moviefile
|
|||
*
|
||||
* parameter filename: The file to save to.
|
||||
* parameter compression: The compression level 0-9. 0 is uncompressed.
|
||||
* parameter binary: Save in binary form if true.
|
||||
* throws std::bad_alloc: Not enough memory.
|
||||
* throws std::runtime_error: Can't save the movie file.
|
||||
*/
|
||||
void save(const std::string& filename, unsigned compression) throw(std::bad_alloc, std::runtime_error);
|
||||
void save(const std::string& filename, unsigned compression, bool binary) throw(std::bad_alloc,
|
||||
std::runtime_error);
|
||||
|
||||
/**
|
||||
* Force loading as corrupt.
|
||||
|
@ -178,6 +180,9 @@ struct moviefile
|
|||
* returns: Length of the movie in nanoseconds.
|
||||
*/
|
||||
uint64_t get_movie_length() throw();
|
||||
private:
|
||||
void binary_io(std::ostream& stream) throw(std::bad_alloc, std::runtime_error);
|
||||
void binary_io(std::istream& stream, struct core_type& romtype) throw(std::bad_alloc, std::runtime_error);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -41,6 +41,12 @@
|
|||
void update_movie_state();
|
||||
time_t random_seed_value = 0;
|
||||
|
||||
setting_var<setting_var_model_bool<setting_yes_no>> jukebox_dflt_binary(lsnes_vset, "jukebox-default-binary",
|
||||
"Movie‣Saving‣Saveslots binary", true);
|
||||
setting_var<setting_var_model_bool<setting_yes_no>> movie_dflt_binary(lsnes_vset, "movie-default-binary",
|
||||
"Movie‣Saving‣Movies binary", false);
|
||||
setting_var<setting_var_model_bool<setting_yes_no>> save_dflt_binary(lsnes_vset, "savestate-default-binary",
|
||||
"Movie‣Saving‣Savestates binary", false);
|
||||
|
||||
namespace
|
||||
{
|
||||
|
@ -78,7 +84,7 @@ namespace
|
|||
std::string pending_load;
|
||||
std::string pending_new_project;
|
||||
//Queued saves (all savestates).
|
||||
std::set<std::string> queued_saves;
|
||||
std::set<std::pair<std::string, int>> queued_saves;
|
||||
//Save jukebox.
|
||||
size_t save_jukebox_pointer;
|
||||
//Special subframe location. One of SPECIAL_* constants.
|
||||
|
@ -268,21 +274,22 @@ namespace
|
|||
platform::set_paused(false);
|
||||
}
|
||||
|
||||
void mark_pending_save(const std::string& filename, int smode)
|
||||
void mark_pending_save(const std::string& filename, int smode, int binary)
|
||||
{
|
||||
int tmp = -1;
|
||||
if(smode == SAVE_MOVIE) {
|
||||
//Just do this immediately.
|
||||
do_save_movie(filename);
|
||||
flush_slotinfo(translate_name_mprefix(filename));
|
||||
do_save_movie(filename, binary);
|
||||
flush_slotinfo(translate_name_mprefix(filename, tmp, false));
|
||||
return;
|
||||
}
|
||||
if(location_special == SPECIAL_SAVEPOINT) {
|
||||
//We can save immediately here.
|
||||
do_save_state(filename);
|
||||
flush_slotinfo(translate_name_mprefix(filename));
|
||||
do_save_state(filename, binary);
|
||||
flush_slotinfo(translate_name_mprefix(filename, tmp, false));
|
||||
return;
|
||||
}
|
||||
queued_saves.insert(filename);
|
||||
queued_saves.insert(std::make_pair(filename, binary));
|
||||
messages << "Pending save on '" << filename << "'" << std::endl;
|
||||
}
|
||||
|
||||
|
@ -411,7 +418,8 @@ void update_movie_state()
|
|||
_status.set("!mode", "F");
|
||||
}
|
||||
if(jukebox_size > 0) {
|
||||
std::string sfilen = translate_name_mprefix(save_jukebox_name(save_jukebox_pointer));
|
||||
int tmp = -1;
|
||||
std::string sfilen = translate_name_mprefix(save_jukebox_name(save_jukebox_pointer), tmp, false);
|
||||
_status.set("!saveslot", (stringfmt() << (save_jukebox_pointer + 1)).str());
|
||||
_status.set("!saveslotinfo", get_slotinfo(sfilen));
|
||||
} else {
|
||||
|
@ -655,7 +663,7 @@ namespace
|
|||
[]() throw(std::bad_alloc, std::runtime_error) {
|
||||
if(jukebox_size == 0)
|
||||
throw std::runtime_error("No slot selected");
|
||||
mark_pending_save(save_jukebox_name(save_jukebox_pointer), SAVE_STATE);
|
||||
mark_pending_save(save_jukebox_name(save_jukebox_pointer), SAVE_STATE, -1);
|
||||
});
|
||||
|
||||
function_ptr_command<> padvance_frame(lsnes_cmd, "+advance-frame", "Advance one frame",
|
||||
|
@ -766,13 +774,37 @@ namespace
|
|||
function_ptr_command<arg_filename> save_state(lsnes_cmd, "save-state", "Save state",
|
||||
"Syntax: save-state <file>\nSaves SNES state to <file>\n",
|
||||
[](arg_filename args) throw(std::bad_alloc, std::runtime_error) {
|
||||
mark_pending_save(args, SAVE_STATE);
|
||||
mark_pending_save(args, SAVE_STATE, -1);
|
||||
});
|
||||
|
||||
function_ptr_command<arg_filename> save_state2(lsnes_cmd, "save-state-binary", "Save state (binary)",
|
||||
"Syntax: save-state-binary <file>\nSaves binary state to <file>\n",
|
||||
[](arg_filename args) throw(std::bad_alloc, std::runtime_error) {
|
||||
mark_pending_save(args, SAVE_STATE, 1);
|
||||
});
|
||||
|
||||
function_ptr_command<arg_filename> save_state3(lsnes_cmd, "save-state-zip", "Save state (zip)",
|
||||
"Syntax: save-state-zip <file>\nSaves zip state to <file>\n",
|
||||
[](arg_filename args) throw(std::bad_alloc, std::runtime_error) {
|
||||
mark_pending_save(args, SAVE_STATE, 0);
|
||||
});
|
||||
|
||||
function_ptr_command<arg_filename> save_movie(lsnes_cmd, "save-movie", "Save movie",
|
||||
"Syntax: save-movie <file>\nSaves SNES movie to <file>\n",
|
||||
[](arg_filename args) throw(std::bad_alloc, std::runtime_error) {
|
||||
mark_pending_save(args, SAVE_MOVIE);
|
||||
mark_pending_save(args, SAVE_MOVIE, -1);
|
||||
});
|
||||
|
||||
function_ptr_command<arg_filename> save_movie2(lsnes_cmd, "save-movie-binary", "Save movie (binary)",
|
||||
"Syntax: save-movie-binary <file>\nSaves binary movie to <file>\n",
|
||||
[](arg_filename args) throw(std::bad_alloc, std::runtime_error) {
|
||||
mark_pending_save(args, SAVE_MOVIE, 1);
|
||||
});
|
||||
|
||||
function_ptr_command<arg_filename> save_movie3(lsnes_cmd, "save-movie-zip", "Save movie (zip)",
|
||||
"Syntax: save-movie-zip <file>\nSaves zip movie to <file>\n",
|
||||
[](arg_filename args) throw(std::bad_alloc, std::runtime_error) {
|
||||
mark_pending_save(args, SAVE_MOVIE, 0);
|
||||
});
|
||||
|
||||
function_ptr_command<> set_rwmode(lsnes_cmd, "set-rwmode", "Switch to read/write mode",
|
||||
|
@ -1080,8 +1112,9 @@ nothing_to_do:
|
|||
if(!queued_saves.empty() || (do_unsafe_rewind && !unsafe_rewind_obj)) {
|
||||
our_rom->rtype->runtosave();
|
||||
for(auto i : queued_saves) {
|
||||
do_save_state(i);
|
||||
flush_slotinfo(translate_name_mprefix(i));
|
||||
do_save_state(i.first, i.second);
|
||||
int tmp = -1;
|
||||
flush_slotinfo(translate_name_mprefix(i.first, tmp, false));
|
||||
}
|
||||
if(do_unsafe_rewind && !unsafe_rewind_obj) {
|
||||
uint64_t t = get_utime();
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "core/framerate.hpp"
|
||||
#include "lua/lua.hpp"
|
||||
#include "core/misc.hpp"
|
||||
#include "core/mainloop.hpp"
|
||||
#include "core/moviedata.hpp"
|
||||
#include "core/project.hpp"
|
||||
#include "core/rrdata.hpp"
|
||||
|
@ -55,10 +56,10 @@ movie& get_movie()
|
|||
|
||||
namespace
|
||||
{
|
||||
setting_var<setting_var_model_int<0, 9>> savecompression(lsnes_vset, "savecompression", "Movie‣Compression",
|
||||
7);
|
||||
setting_var<setting_var_model_int<0, 9>> savecompression(lsnes_vset, "savecompression",
|
||||
"Movie‣Saving‣Compression", 7);
|
||||
setting_var<setting_var_model_bool<setting_yes_no>> readonly_load_preserves(lsnes_vset,
|
||||
"preserve_on_readonly_load", "Movie‣Preserve on readonly load", true);
|
||||
"preserve_on_readonly_load", "Movie‣Loading‣Preserve on readonly load", true);
|
||||
mutex_class mprefix_lock;
|
||||
std::string mprefix;
|
||||
bool mprefix_valid;
|
||||
|
@ -142,7 +143,7 @@ void set_mprefix_for_project(const std::string& pfx)
|
|||
set_mprefix(pfx);
|
||||
}
|
||||
|
||||
std::string translate_name_mprefix(std::string original)
|
||||
std::string translate_name_mprefix(std::string original, int& binary, bool save)
|
||||
{
|
||||
auto p = project_get();
|
||||
regex_results r;
|
||||
|
@ -151,14 +152,19 @@ std::string translate_name_mprefix(std::string original)
|
|||
}
|
||||
size_t prefixloc = original.find("${project}");
|
||||
if(prefixloc < original.length()) {
|
||||
if(binary < 0)
|
||||
binary = jukebox_dflt_binary ? 1 : 0;
|
||||
std::string pprf = lsnes_vset["slotpath"].str() + "/";
|
||||
if(prefixloc == 0)
|
||||
return pprf + get_mprefix() + original.substr(prefixloc + 10);
|
||||
else
|
||||
return original.substr(0, prefixloc) + get_mprefix() + original.substr(prefixloc + 10);
|
||||
} else
|
||||
} else {
|
||||
if(binary < 0)
|
||||
binary = (save ? save_dflt_binary : movie_dflt_binary) ? 1 : 0;
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> split_author(const std::string& author) throw(std::bad_alloc,
|
||||
std::runtime_error)
|
||||
|
@ -189,7 +195,7 @@ std::string resolve_relative_path(const std::string& path)
|
|||
}
|
||||
|
||||
//Save state.
|
||||
void do_save_state(const std::string& filename) throw(std::bad_alloc,
|
||||
void do_save_state(const std::string& filename, int binary) throw(std::bad_alloc,
|
||||
std::runtime_error)
|
||||
{
|
||||
if(!our_movie.gametype) {
|
||||
|
@ -197,7 +203,7 @@ void do_save_state(const std::string& filename) throw(std::bad_alloc,
|
|||
messages << "Can't save movie without a ROM" << std::endl;
|
||||
return;
|
||||
}
|
||||
std::string filename2 = translate_name_mprefix(filename);
|
||||
std::string filename2 = translate_name_mprefix(filename, binary, true);
|
||||
lua_callback_pre_save(filename2, true);
|
||||
try {
|
||||
uint64_t origtime = get_utime();
|
||||
|
@ -219,9 +225,11 @@ void do_save_state(const std::string& filename) throw(std::bad_alloc,
|
|||
our_movie.authors = prj->authors;
|
||||
}
|
||||
our_movie.active_macros = controls.get_macro_frames();
|
||||
our_movie.save(filename2, savecompression);
|
||||
our_movie.save(filename2, savecompression, binary > 0);
|
||||
uint64_t took = get_utime() - origtime;
|
||||
messages << "Saved state '" << filename2 << "' in " << took << " microseconds." << std::endl;
|
||||
std::string kind = (binary > 0) ? "(binary format)" : "(zip format)";
|
||||
messages << "Saved state " << kind << " '" << filename2 << "' in " << took << " microseconds."
|
||||
<< std::endl;
|
||||
lua_callback_post_save(filename2, true);
|
||||
} catch(std::bad_alloc& e) {
|
||||
throw;
|
||||
|
@ -239,14 +247,14 @@ void do_save_state(const std::string& filename) throw(std::bad_alloc,
|
|||
}
|
||||
|
||||
//Save movie.
|
||||
void do_save_movie(const std::string& filename) throw(std::bad_alloc, std::runtime_error)
|
||||
void do_save_movie(const std::string& filename, int binary) throw(std::bad_alloc, std::runtime_error)
|
||||
{
|
||||
if(!our_movie.gametype) {
|
||||
platform::error_message("Can't save movie without a ROM");
|
||||
messages << "Can't save movie without a ROM" << std::endl;
|
||||
return;
|
||||
}
|
||||
std::string filename2 = translate_name_mprefix(filename);
|
||||
std::string filename2 = translate_name_mprefix(filename, binary, false);
|
||||
lua_callback_pre_save(filename2, false);
|
||||
try {
|
||||
uint64_t origtime = get_utime();
|
||||
|
@ -258,9 +266,11 @@ void do_save_movie(const std::string& filename) throw(std::bad_alloc, std::runti
|
|||
our_movie.authors = prj->authors;
|
||||
}
|
||||
our_movie.active_macros.clear();
|
||||
our_movie.save(filename2, savecompression);
|
||||
our_movie.save(filename2, savecompression, binary > 0);
|
||||
uint64_t took = get_utime() - origtime;
|
||||
messages << "Saved movie '" << filename2 << "' in " << took << " microseconds." << std::endl;
|
||||
std::string kind = (binary > 0) ? "(binary format)" : "(zip format)";
|
||||
messages << "Saved movie " << kind << " '" << filename2 << "' in " << took << " microseconds."
|
||||
<< std::endl;
|
||||
lua_callback_post_save(filename2, false);
|
||||
} catch(std::bad_alloc& e) {
|
||||
OOM_panic();
|
||||
|
@ -399,6 +409,7 @@ void do_load_state(struct moviefile& _movie, int lmode)
|
|||
try {
|
||||
our_rom->region = _movie.gametype ? &(_movie.gametype->get_region()) : NULL;
|
||||
random_seed_value = _movie.movie_rtc_second;
|
||||
if(!will_load_state || our_movie.projectid != _movie.projectid)
|
||||
our_rom->load(_movie.settings, _movie.movie_rtc_second, _movie.movie_rtc_subsecond);
|
||||
|
||||
if(will_load_state) {
|
||||
|
@ -500,7 +511,8 @@ void do_load_state(struct moviefile& _movie, int lmode)
|
|||
//Load state
|
||||
bool do_load_state(const std::string& filename, int lmode)
|
||||
{
|
||||
std::string filename2 = translate_name_mprefix(filename);
|
||||
int tmp = -1;
|
||||
std::string filename2 = translate_name_mprefix(filename, tmp, false);
|
||||
uint64_t origtime = get_utime();
|
||||
lua_callback_pre_load(filename2);
|
||||
struct moviefile mfile;
|
||||
|
|
|
@ -4,8 +4,12 @@
|
|||
#include "core/rrdata.hpp"
|
||||
#include "library/zip.hpp"
|
||||
#include "library/string.hpp"
|
||||
#include "library/minmax.hpp"
|
||||
#include "library/serialization.hpp"
|
||||
#include "interface/romtype.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <boost/iostreams/copy.hpp>
|
||||
#include <boost/iostreams/device/back_inserter.hpp>
|
||||
|
@ -13,6 +17,27 @@
|
|||
#define DEFAULT_RTC_SECOND 1000000000ULL
|
||||
#define DEFAULT_RTC_SUBSECOND 0ULL
|
||||
|
||||
enum lsnes_movie_tags
|
||||
{
|
||||
TAG_ = 0xaddb2d86,
|
||||
TAG_ANCHOR_SAVE = 0xf5e0fad7,
|
||||
TAG_AUTHOR = 0xafff97b4,
|
||||
TAG_CORE_VERSION = 0xe4344c7e,
|
||||
TAG_GAMENAME = 0xe80d6970,
|
||||
TAG_HOSTMEMORY = 0x3bf9d187,
|
||||
TAG_MACRO = 0xd261338f,
|
||||
TAG_MOVIE = 0xf3dca44b,
|
||||
TAG_MOVIE_SRAM = 0xbbc824b7,
|
||||
TAG_MOVIE_TIME = 0x18c3a975,
|
||||
TAG_PROJECT_ID = 0x359bfbab,
|
||||
TAG_ROMHASH = 0x0428acfc,
|
||||
TAG_RRDATA = 0xa3a07f71,
|
||||
TAG_SAVE_SRAM = 0xae9bfb2f,
|
||||
TAG_SAVESTATE = 0x2e5bc2ac,
|
||||
TAG_SCREENSHOT = 0xc6760d0e,
|
||||
TAG_SUBTITLE = 0x6a7054d3,
|
||||
};
|
||||
|
||||
void read_linefile(zip_reader& r, const std::string& member, std::string& out, bool conditional = false)
|
||||
throw(std::bad_alloc, std::runtime_error)
|
||||
{
|
||||
|
@ -47,6 +72,233 @@ void write_linefile(zip_writer& w, const std::string& member, const std::string&
|
|||
|
||||
namespace
|
||||
{
|
||||
int lsnes_rename(const char* oldname, const char* newname)
|
||||
{
|
||||
#if defined(_WIN32) || defined(_WIN64) || defined(TEST_WIN32_CODE)
|
||||
return MoveFileEx(oldname, newname, MOVEFILE_REPLACE_EXISTING);
|
||||
#else
|
||||
return rename(oldname, newname);
|
||||
#endif
|
||||
}
|
||||
|
||||
void binary_write_byte(std::ostream& stream, uint8_t byte)
|
||||
{
|
||||
stream.write(reinterpret_cast<char*>(&byte), 1);
|
||||
}
|
||||
|
||||
void binary_write_number(std::ostream& stream, uint64_t number)
|
||||
{
|
||||
char data[10];
|
||||
size_t len = 0;
|
||||
do {
|
||||
bool cont = (number > 127);
|
||||
data[len++] = (cont ? 0x80 : 0x00) | (number & 0x7F);
|
||||
number >>= 7;
|
||||
} while(number);
|
||||
stream.write(data, len);
|
||||
}
|
||||
|
||||
void binary_write_number32(std::ostream& stream, uint32_t number)
|
||||
{
|
||||
char data[4];
|
||||
write32ube(data, number);
|
||||
stream.write(data, 4);
|
||||
}
|
||||
|
||||
void binary_write_string(std::ostream& stream, const std::string& string)
|
||||
{
|
||||
size_t slen = string.length();
|
||||
binary_write_number(stream, slen);
|
||||
std::copy(string.begin(), string.end(), std::ostream_iterator<char>(stream));
|
||||
}
|
||||
|
||||
void binary_write_string_implicit(std::ostream& stream, const std::string& string)
|
||||
{
|
||||
std::copy(string.begin(), string.end(), std::ostream_iterator<char>(stream));
|
||||
}
|
||||
|
||||
void binary_write_blob(std::ostream& stream, const std::vector<char>& blob)
|
||||
{
|
||||
stream.write(&blob[0], blob.size());
|
||||
}
|
||||
|
||||
void binary_write_extension(std::ostream& stream, uint32_t tag, std::function<void(std::ostream& s)> fn,
|
||||
bool even_empty = false);
|
||||
|
||||
void binary_write_extension(std::ostream& stream, uint32_t tag, std::function<void(std::ostream& s)> fn,
|
||||
bool even_empty)
|
||||
{
|
||||
std::ostringstream tmp;
|
||||
fn(tmp);
|
||||
std::string str = tmp.str();
|
||||
if(!even_empty && !str.length())
|
||||
return;
|
||||
binary_write_number32(stream, TAG_);
|
||||
binary_write_number32(stream, tag);
|
||||
binary_write_string(stream, str);
|
||||
}
|
||||
|
||||
uint8_t binary_read_byte(std::istream& stream)
|
||||
{
|
||||
char byte;
|
||||
stream.read(&byte, 1);
|
||||
if(!stream)
|
||||
throw std::runtime_error("Unexpected EOF");
|
||||
return byte;
|
||||
}
|
||||
|
||||
uint32_t binary_read_number32(std::istream& stream)
|
||||
{
|
||||
char c[4];
|
||||
stream.read(c, 4);
|
||||
if(!stream)
|
||||
throw std::runtime_error("Unexpected EOF");
|
||||
return read32ube(c);
|
||||
}
|
||||
|
||||
uint64_t binary_read_number(std::istream& stream)
|
||||
{
|
||||
uint64_t s = 0;
|
||||
int sh = 0;
|
||||
uint8_t c;
|
||||
do {
|
||||
c = binary_read_byte(stream);
|
||||
s |= (static_cast<uint64_t>(c & 0x7F) << sh);
|
||||
sh += 7;
|
||||
} while(c & 0x80);
|
||||
if(!stream)
|
||||
throw std::runtime_error("Unexpected EOF");
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string binary_read_string(std::istream& stream)
|
||||
{
|
||||
size_t sz = binary_read_number(stream);
|
||||
std::vector<char> _r;
|
||||
_r.resize(sz);
|
||||
stream.read(&_r[0], _r.size());
|
||||
if(!stream)
|
||||
throw std::runtime_error("Unexpected EOF");
|
||||
std::string r(_r.begin(), _r.end());
|
||||
return r;
|
||||
}
|
||||
|
||||
class extension_stream
|
||||
{
|
||||
public:
|
||||
extension_stream(std::istream& s, uint64_t _left) : under(s), left(_left) {}
|
||||
uint64_t binary_read_number()
|
||||
{
|
||||
uint64_t s = 0;
|
||||
int sh = 0;
|
||||
uint8_t c;
|
||||
do {
|
||||
c = binary_read_byte();
|
||||
s |= (static_cast<uint64_t>(c & 0x7F) << sh);
|
||||
sh += 7;
|
||||
} while(c & 0x80);
|
||||
return s;
|
||||
}
|
||||
void binary_read_movie(controller_frame_vector& v)
|
||||
{
|
||||
uint64_t stride = v.get_stride();
|
||||
uint64_t pageframes = v.get_frames_per_page();
|
||||
uint64_t vsize = 0;
|
||||
size_t pagenum = 0;
|
||||
uint64_t pagesize = stride * pageframes;
|
||||
while(left) {
|
||||
v.resize(vsize + pageframes);
|
||||
unsigned char* contents = v.get_page_buffer(pagenum++);
|
||||
uint64_t gcount = min(pagesize, left);
|
||||
read_stream(reinterpret_cast<char*>(contents), gcount);
|
||||
vsize += (gcount / stride);
|
||||
}
|
||||
v.resize(vsize);
|
||||
}
|
||||
uint32_t binary_read_number32()
|
||||
{
|
||||
char c[4];
|
||||
read_stream(c, 4);
|
||||
return read32ube(c);
|
||||
}
|
||||
std::string binary_read_string()
|
||||
{
|
||||
size_t l = binary_read_number();
|
||||
std::vector<char> _r;
|
||||
_r.resize(l);
|
||||
read_stream(&_r[0], l);
|
||||
std::string r(_r.begin(), _r.end());
|
||||
return r;
|
||||
}
|
||||
std::string binary_read_string_implicit()
|
||||
{
|
||||
std::vector<char> _r;
|
||||
_r.resize(left);
|
||||
read_stream(&_r[0], left);
|
||||
std::string r(_r.begin(), _r.end());
|
||||
return r;
|
||||
}
|
||||
uint8_t binary_read_byte()
|
||||
{
|
||||
char c;
|
||||
read_stream(&c, 1);
|
||||
return c;
|
||||
}
|
||||
void binary_read_blob(std::vector<char>& v)
|
||||
{
|
||||
v.resize(left);
|
||||
read_stream(&v[0], left);
|
||||
}
|
||||
private:
|
||||
void read_stream(char* dest, size_t size)
|
||||
{
|
||||
if(size > left)
|
||||
throw std::runtime_error("Substream unexpected EOF");
|
||||
under.read(dest, size);
|
||||
if(!under)
|
||||
throw std::runtime_error("Unexpected EOF");
|
||||
left -= size;
|
||||
}
|
||||
std::istream& under;
|
||||
uint64_t left;
|
||||
};
|
||||
|
||||
void for_each_extension(std::istream& stream, std::function<void(uint32_t tag, extension_stream& s)> fn)
|
||||
{
|
||||
while(stream) {
|
||||
char c[4];
|
||||
stream.read(c, 4);
|
||||
if(!stream)
|
||||
break;
|
||||
uint32_t tagid = read32ube(c);
|
||||
if(tagid != TAG_)
|
||||
throw std::runtime_error("Movie file packet structure desync");
|
||||
uint32_t tag = binary_read_number32(stream);
|
||||
uint64_t size = binary_read_number(stream);
|
||||
extension_stream strm(stream, size);
|
||||
fn(tag, strm);
|
||||
}
|
||||
}
|
||||
|
||||
void binary_write_movie(std::ostream& stream, controller_frame_vector& v)
|
||||
{
|
||||
uint64_t pages = v.get_page_count();
|
||||
uint64_t stride = v.get_stride();
|
||||
uint64_t pageframes = v.get_frames_per_page();
|
||||
uint64_t vsize = v.size();
|
||||
binary_write_number32(stream, TAG_);
|
||||
binary_write_number32(stream, TAG_MOVIE);
|
||||
binary_write_number(stream, vsize * stride);
|
||||
size_t pagenum = 0;
|
||||
while(vsize > 0) {
|
||||
uint64_t count = (vsize > pageframes) ? pageframes : vsize;
|
||||
size_t bytes = count * stride;
|
||||
unsigned char* content = v.get_page_buffer(pagenum++);
|
||||
stream.write(reinterpret_cast<char*>(content), bytes);
|
||||
vsize -= count;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> read_settings(zip_reader& r)
|
||||
{
|
||||
std::map<std::string, std::string> x;
|
||||
|
@ -63,18 +315,17 @@ namespace
|
|||
return x;
|
||||
}
|
||||
|
||||
void write_settings(zip_writer& w, const std::map<std::string, std::string>& settings,
|
||||
core_setting_group& sgroup)
|
||||
template<typename target>
|
||||
void write_settings(target& w, const std::map<std::string, std::string>& settings,
|
||||
core_setting_group& sgroup, std::function<void(target& w, const std::string& name,
|
||||
const std::string& value)> writefn)
|
||||
{
|
||||
for(auto i : settings) {
|
||||
if(!sgroup.settings.count(i.first))
|
||||
continue;
|
||||
if(sgroup.settings.find(i.first)->second.dflt == i.second)
|
||||
continue;
|
||||
if(regex_match("port[0-9]+", i.first))
|
||||
write_linefile(w, i.first, i.second);
|
||||
else
|
||||
write_linefile(w, "setting." + i.first, i.second);
|
||||
writefn(w, i.first, i.second);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -413,6 +664,17 @@ moviefile::moviefile(const std::string& movie, core_type& romtype) throw(std::ba
|
|||
is_savestate = false;
|
||||
lazy_project_create = false;
|
||||
std::string tmp;
|
||||
{
|
||||
std::istream& s = open_file_relative(movie, "");
|
||||
char buf[6] = {0};
|
||||
s.read(buf, 5);
|
||||
if(!strcmp(buf, "lsmv\x1A")) {
|
||||
binary_io(s, romtype);
|
||||
delete &s;
|
||||
return;
|
||||
}
|
||||
delete &s;
|
||||
}
|
||||
zip_reader r(movie);
|
||||
read_linefile(r, "systemid", tmp);
|
||||
if(tmp.substr(0, 8) != "lsnes-rr")
|
||||
|
@ -487,11 +749,36 @@ moviefile::moviefile(const std::string& movie, core_type& romtype) throw(std::ba
|
|||
read_input(r, input, 0);
|
||||
}
|
||||
|
||||
void moviefile::save(const std::string& movie, unsigned compression) throw(std::bad_alloc, std::runtime_error)
|
||||
void moviefile::save(const std::string& movie, unsigned compression, bool binary) throw(std::bad_alloc,
|
||||
std::runtime_error)
|
||||
{
|
||||
if(binary) {
|
||||
std::string tmp = movie + ".tmp";
|
||||
std::ofstream strm(tmp.c_str(), std::ios_base::binary);
|
||||
if(!strm)
|
||||
throw std::runtime_error("Can't open output file");
|
||||
char buf[5] = {'l', 's', 'm', 'v', 0x1A};
|
||||
strm.write(buf, 5);
|
||||
if(!strm)
|
||||
throw std::runtime_error("Failed to write to output file");
|
||||
binary_io(strm);
|
||||
if(!strm)
|
||||
throw std::runtime_error("Failed to write to output file");
|
||||
strm.close();
|
||||
std::string backup = movie + ".backup";
|
||||
lsnes_rename(movie.c_str(), backup.c_str());
|
||||
lsnes_rename(tmp.c_str(), movie.c_str());
|
||||
return;
|
||||
}
|
||||
zip_writer w(movie, compression);
|
||||
write_linefile(w, "gametype", gametype->get_name());
|
||||
write_settings(w, settings, gametype->get_type().get_settings());
|
||||
write_settings<zip_writer>(w, settings, gametype->get_type().get_settings(), [](zip_writer& w,
|
||||
const std::string& name, const std::string& value) -> void {
|
||||
if(regex_match("port[0-9]+", name))
|
||||
write_linefile(w, name, value);
|
||||
else
|
||||
write_linefile(w, "setting." + name, value);
|
||||
});
|
||||
write_linefile(w, "gamename", gamename, true);
|
||||
write_linefile(w, "systemid", "lsnes-rr1");
|
||||
write_linefile(w, "controlsversion", "0");
|
||||
|
@ -534,6 +821,249 @@ void moviefile::save(const std::string& movie, unsigned compression) throw(std::
|
|||
w.commit();
|
||||
}
|
||||
|
||||
/*
|
||||
Following need to be saved:
|
||||
- gametype (string)
|
||||
- settings (string name, value pairs)
|
||||
- gamename (optional string)
|
||||
- core version (string)
|
||||
- project id (string
|
||||
- rrdata (blob)
|
||||
- ROM hashes (2*27 table of optional strings)
|
||||
- Subtitles (list of number,number,string)
|
||||
- SRAMs (dictionary string->blob.)
|
||||
- Starttime (number,number)
|
||||
- Anchor savestate (optional blob)
|
||||
- Save frame (savestate-only, numeric).
|
||||
- Lag counter (savestate-only, numeric).
|
||||
- pollcounters (savestate-only, vector of numbers).
|
||||
- hostmemory (savestate-only, blob).
|
||||
- screenshot (savestate-only, blob).
|
||||
- Save SRAMs (savestate-only, dictionary string->blob.)
|
||||
- Save time (savestate-only, number,number)
|
||||
- Poll flag (savestate-only, boolean)
|
||||
- Macros (savestate-only, ???)
|
||||
- Authors (list of string,string).
|
||||
- Input (blob).
|
||||
- Extensions (???)
|
||||
*/
|
||||
void moviefile::binary_io(std::ostream& stream) throw(std::bad_alloc, std::runtime_error)
|
||||
{
|
||||
binary_write_string(stream, gametype->get_name());
|
||||
write_settings<std::ostream>(stream, settings, gametype->get_type().get_settings(), [](std::ostream& s,
|
||||
const std::string& name, const std::string& value) -> void {
|
||||
binary_write_byte(s, 0x01);
|
||||
binary_write_string(s, name);
|
||||
binary_write_string(s, value);
|
||||
});
|
||||
binary_write_byte(stream, 0x00);
|
||||
|
||||
binary_write_extension(stream, TAG_MOVIE_TIME, [this](std::ostream& s) {
|
||||
binary_write_number(s, movie_rtc_second);
|
||||
binary_write_number(s, movie_rtc_subsecond);
|
||||
});
|
||||
|
||||
binary_write_extension(stream, TAG_PROJECT_ID, [this](std::ostream& s) {
|
||||
binary_write_string_implicit(s, this->projectid);
|
||||
});
|
||||
|
||||
binary_write_extension(stream, TAG_CORE_VERSION, [this](std::ostream& s) {
|
||||
this->coreversion = this->gametype->get_type().get_core_identifier();
|
||||
binary_write_string_implicit(s, this->coreversion);
|
||||
});
|
||||
|
||||
for(unsigned i = 0; i < sizeof(romimg_sha256) / sizeof(romimg_sha256[0]); i++) {
|
||||
binary_write_extension(stream, TAG_ROMHASH, [this, i](std::ostream& s) {
|
||||
if(!this->romimg_sha256[i].length()) return;
|
||||
binary_write_byte(s, 2 * i);
|
||||
binary_write_string_implicit(s, romimg_sha256[i]);
|
||||
});
|
||||
binary_write_extension(stream, TAG_ROMHASH, [this, i](std::ostream& s) {
|
||||
if(!this->romxml_sha256[i].length()) return;
|
||||
binary_write_byte(s, 2 * i + 1);
|
||||
binary_write_string_implicit(s, romxml_sha256[i]);
|
||||
});
|
||||
}
|
||||
|
||||
binary_write_extension(stream, TAG_RRDATA, [this](std::ostream& s) {
|
||||
uint64_t count;
|
||||
std::vector<char> rrd;
|
||||
count = rrdata::write(rrd);
|
||||
binary_write_blob(s, rrd);
|
||||
});
|
||||
|
||||
for(auto i : movie_sram) {
|
||||
binary_write_extension(stream, TAG_MOVIE_SRAM, [&i](std::ostream& s) {
|
||||
binary_write_string(s, i.first);
|
||||
binary_write_blob(s, i.second);
|
||||
});
|
||||
}
|
||||
binary_write_extension(stream, TAG_ANCHOR_SAVE, [this](std::ostream& s) {
|
||||
binary_write_blob(s, this->anchor_savestate);
|
||||
});
|
||||
if(is_savestate) {
|
||||
binary_write_extension(stream, TAG_SAVESTATE, [this](std::ostream& s) {
|
||||
binary_write_number(s, this->save_frame);
|
||||
binary_write_number(s, this->lagged_frames);
|
||||
binary_write_number(s, this->rtc_second);
|
||||
binary_write_number(s, this->rtc_subsecond);
|
||||
binary_write_number(s, this->pollcounters.size());
|
||||
for(auto i : this->pollcounters)
|
||||
binary_write_number32(s, i);
|
||||
binary_write_byte(s, this->poll_flag ? 0x01 : 0x00);
|
||||
binary_write_blob(s, this->savestate);
|
||||
});
|
||||
|
||||
binary_write_extension(stream, TAG_HOSTMEMORY, [this](std::ostream& s) {
|
||||
binary_write_blob(s, this->host_memory);
|
||||
});
|
||||
|
||||
binary_write_extension(stream, TAG_SCREENSHOT, [this](std::ostream& s) {
|
||||
binary_write_blob(s, this->screenshot);
|
||||
});
|
||||
|
||||
for(auto i : sram) {
|
||||
binary_write_extension(stream, TAG_SAVE_SRAM, [&i](std::ostream& s) {
|
||||
binary_write_string(s, i.first);
|
||||
binary_write_blob(s, i.second);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
binary_write_extension(stream, TAG_GAMENAME, [this](std::ostream& s) {
|
||||
binary_write_string_implicit(s, this->gamename);
|
||||
});
|
||||
|
||||
for(auto i : subtitles)
|
||||
binary_write_extension(stream, TAG_SUBTITLE, [&i](std::ostream& s) {
|
||||
binary_write_number(s, i.first.get_frame());
|
||||
binary_write_number(s, i.first.get_length());
|
||||
binary_write_string_implicit(s, i.second);
|
||||
});
|
||||
|
||||
for(auto i : authors)
|
||||
binary_write_extension(stream, TAG_AUTHOR, [&i](std::ostream& s) {
|
||||
binary_write_string(s, i.first);
|
||||
binary_write_string_implicit(s, i.second);
|
||||
});
|
||||
|
||||
for(auto i : active_macros)
|
||||
binary_write_extension(stream, TAG_MACRO, [&i](std::ostream& s) {
|
||||
binary_write_number(s, i.second);
|
||||
binary_write_string_implicit(s, i.first);
|
||||
});
|
||||
binary_write_movie(stream, input);
|
||||
}
|
||||
|
||||
void moviefile::binary_io(std::istream& stream, core_type& romtype) throw(std::bad_alloc, std::runtime_error)
|
||||
{
|
||||
std::string tmp = binary_read_string(stream);
|
||||
try {
|
||||
gametype = &romtype.lookup_sysregion(tmp);
|
||||
} catch(std::bad_alloc& e) {
|
||||
throw;
|
||||
} catch(std::exception& e) {
|
||||
throw std::runtime_error("Illegal game type '" + tmp + "'");
|
||||
}
|
||||
while(binary_read_byte(stream)) {
|
||||
std::string name = binary_read_string(stream);
|
||||
settings[name] = binary_read_string(stream);
|
||||
}
|
||||
auto ctrldata = gametype->get_type().controllerconfig(settings);
|
||||
port_type_set& ports = port_type_set::make(ctrldata.ports, ctrldata.portindex());
|
||||
input.clear(ports);
|
||||
|
||||
for_each_extension(stream, [this](uint32_t tag, extension_stream& s) {
|
||||
switch(tag) {
|
||||
case TAG_ANCHOR_SAVE:
|
||||
s.binary_read_blob(this->anchor_savestate);
|
||||
break;
|
||||
case TAG_AUTHOR: {
|
||||
std::string a = s.binary_read_string();
|
||||
std::string b = s.binary_read_string_implicit();
|
||||
this->authors.push_back(std::make_pair(a, b));
|
||||
break;
|
||||
}
|
||||
case TAG_CORE_VERSION:
|
||||
this->coreversion = s.binary_read_string_implicit();
|
||||
break;
|
||||
case TAG_GAMENAME:
|
||||
this->gamename = s.binary_read_string_implicit();
|
||||
break;
|
||||
case TAG_HOSTMEMORY:
|
||||
s.binary_read_blob(this->host_memory);
|
||||
break;
|
||||
case TAG_MACRO: {
|
||||
uint64_t n = s.binary_read_number();
|
||||
this->active_macros[s.binary_read_string_implicit()] = n;
|
||||
break;
|
||||
}
|
||||
case TAG_MOVIE: {
|
||||
s.binary_read_movie(input);
|
||||
break;
|
||||
}
|
||||
case TAG_MOVIE_SRAM: {
|
||||
std::string a = s.binary_read_string();
|
||||
s.binary_read_blob(this->movie_sram[a]);
|
||||
break;
|
||||
}
|
||||
case TAG_MOVIE_TIME:
|
||||
this->movie_rtc_second = s.binary_read_number();
|
||||
this->movie_rtc_subsecond = s.binary_read_number();
|
||||
break;
|
||||
case TAG_PROJECT_ID:
|
||||
this->projectid = s.binary_read_string_implicit();
|
||||
break;
|
||||
case TAG_ROMHASH: {
|
||||
uint8_t n = s.binary_read_byte();
|
||||
std::string h = s.binary_read_string_implicit();
|
||||
if(n > 2 * (sizeof(this->romimg_sha256) / sizeof(romimg_sha256[0])))
|
||||
break;
|
||||
if(n & 1)
|
||||
romxml_sha256[n >> 1] = h;
|
||||
else
|
||||
romimg_sha256[n >> 1] = h;
|
||||
break;
|
||||
}
|
||||
case TAG_RRDATA: {
|
||||
s.binary_read_blob(c_rrdata);
|
||||
this->rerecords = (stringfmt() << rrdata::count(c_rrdata)).str();
|
||||
break;
|
||||
}
|
||||
case TAG_SAVE_SRAM: {
|
||||
std::string a = s.binary_read_string();
|
||||
s.binary_read_blob(this->sram[a]);
|
||||
break;
|
||||
}
|
||||
case TAG_SAVESTATE: {
|
||||
this->is_savestate = true;
|
||||
this->save_frame = s.binary_read_number();
|
||||
this->lagged_frames = s.binary_read_number();
|
||||
this->rtc_second = s.binary_read_number();
|
||||
this->rtc_subsecond = s.binary_read_number();
|
||||
this->pollcounters.resize(s.binary_read_number());
|
||||
for(auto& i : this->pollcounters)
|
||||
i = s.binary_read_number32();
|
||||
this->poll_flag = (s.binary_read_byte() != 0);
|
||||
s.binary_read_blob(this->savestate);
|
||||
break;
|
||||
}
|
||||
case TAG_SCREENSHOT:
|
||||
s.binary_read_blob(this->screenshot);
|
||||
break;
|
||||
case TAG_SUBTITLE: {
|
||||
uint64_t f = s.binary_read_number();
|
||||
uint64_t l = s.binary_read_number();
|
||||
std::string x = s.binary_read_string_implicit();
|
||||
this->subtitles[moviefile_subtiming(f, l)] = x;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
uint64_t moviefile::get_frame_count() throw()
|
||||
{
|
||||
return input.count_frames();
|
||||
|
|
|
@ -46,6 +46,9 @@ namespace
|
|||
const char* null_chars = "F";
|
||||
uint16_t null_cover_fbmem[512 * 448];
|
||||
|
||||
setting_var<setting_var_model_bool<setting_yes_no>> savestate_no_check(lsnes_vset, "dont-check-savestate",
|
||||
"Movie‣Loading‣Don't check savestates", false);
|
||||
|
||||
//Framebuffer.
|
||||
struct framebuffer_info null_fbinfo = {
|
||||
&_pixel_format_bgr16, //Format.
|
||||
|
@ -533,10 +536,12 @@ void loaded_rom::load_core_state(const std::vector<char>& buf, bool nochecksum)
|
|||
|
||||
if(buf.size() < 32)
|
||||
throw std::runtime_error("Savestate corrupt");
|
||||
if(!savestate_no_check) {
|
||||
unsigned char tmp[32];
|
||||
sha256::hash(tmp, reinterpret_cast<const uint8_t*>(&buf[0]), buf.size() - 32);
|
||||
if(memcmp(tmp, &buf[buf.size() - 32], 32))
|
||||
throw std::runtime_error("Savestate corrupt");
|
||||
}
|
||||
rtype->unserialize(&buf[0], buf.size() - 32);;
|
||||
}
|
||||
|
||||
|
|
|
@ -1080,12 +1080,13 @@ namespace
|
|||
{
|
||||
filedialog_input_params p;
|
||||
std::string ext = state ? project_savestate_ext() : "lsmv";
|
||||
if(save)
|
||||
p.types.push_back(filedialog_type_entry(state ? "Savestates" : "Movies", "*." + ext,
|
||||
std::string name = state ? "Savestates" : "Movies";
|
||||
if(save) {
|
||||
p.types.push_back(filedialog_type_entry(name, "*." + ext, ext));
|
||||
p.types.push_back(filedialog_type_entry(name + " (binary)", "*." + ext, ext));
|
||||
} else
|
||||
p.types.push_back(filedialog_type_entry(name, "*." + ext + ";*." + ext + ".backup",
|
||||
ext));
|
||||
else
|
||||
p.types.push_back(filedialog_type_entry(state ? "Savestates" : "Movies", "*." + ext +
|
||||
";*." + ext + ".backup", ext));
|
||||
if(!save && state) {
|
||||
p.types.push_back(filedialog_type_entry("Savestates [read only]", "*." + ext +
|
||||
";*." + ext + ".backup", ext));
|
||||
|
@ -1094,12 +1095,15 @@ namespace
|
|||
p.types.push_back(filedialog_type_entry("Savestates [preserve]", "*." + ext +
|
||||
";*." + ext + ".backup", ext));
|
||||
}
|
||||
p.default_type = 0;
|
||||
p.default_type = save ? (state ? save_dflt_binary : movie_dflt_binary) : 0;
|
||||
return p;
|
||||
}
|
||||
std::pair<std::string, std::string> output(const filedialog_output_params& p, bool save) const
|
||||
{
|
||||
std::string cmdmod;
|
||||
if(save)
|
||||
cmdmod = p.typechoice ? "-binary" : "-zip";
|
||||
else if(state)
|
||||
switch(p.typechoice) {
|
||||
case 0: cmdmod = ""; break;
|
||||
case 1: cmdmod = "-readonly"; break;
|
||||
|
@ -1161,19 +1165,19 @@ void wxwin_mainwindow::handle_menu_click_cancelable(wxCommandEvent& e)
|
|||
platform::queue("rewind-movie");
|
||||
return;
|
||||
case wxID_SAVE_MOVIE:
|
||||
filename = choose_file_save(this, "Save Movie", project_moviepath(), filetype_movie,
|
||||
project_prefixname("lsmv")).second;
|
||||
recent_movies->add(filename);
|
||||
platform::queue("save-movie " + filename);
|
||||
filename2 = choose_file_save(this, "Save Movie", project_moviepath(), filetype_movie,
|
||||
project_prefixname("lsmv"));
|
||||
recent_movies->add(filename2.second);
|
||||
platform::queue("save-movie" + filename2.first + " " + filename2.second);
|
||||
return;
|
||||
case wxID_SAVE_SUBTITLES:
|
||||
platform::queue("save-subtitle " + choose_file_save(this, "Save subtitles", project_moviepath(),
|
||||
filetype_sub, project_prefixname("sub")));
|
||||
return;
|
||||
case wxID_SAVE_STATE:
|
||||
filename = choose_file_save(this, "Save State", project_moviepath(), filetype_savestate).second;
|
||||
recent_movies->add(filename);
|
||||
platform::queue("save-state " + filename);
|
||||
filename2 = choose_file_save(this, "Save State", project_moviepath(), filetype_savestate);
|
||||
recent_movies->add(filename2.second);
|
||||
platform::queue("save-state" + filename2.first + " " + filename2.second);
|
||||
return;
|
||||
case wxID_SAVE_SCREENSHOT:
|
||||
platform::queue("take-screenshot " + choose_file_save(this, "Save Screenshot", project_moviepath(),
|
||||
|
|
|
@ -583,7 +583,7 @@ void wxwin_project::on_load(wxCommandEvent& e)
|
|||
try {
|
||||
moviefile mov = make_movie();
|
||||
mov.start_paused = false;
|
||||
mov.save(get_config_path() + "/movie.tmp", 0);
|
||||
mov.save(get_config_path() + "/movie.tmp", 0, true);
|
||||
platform::queue("load-state " + get_config_path() + "/movie.tmp");
|
||||
EndModal(0);
|
||||
} catch(std::exception& e) {
|
||||
|
|
Loading…
Add table
Reference in a new issue