Commit 457bdc21 authored by Andrei Lisita's avatar Andrei Lisita 🎮

retro-runner: Add abstractions for savestates

parent 19bffd9a
......@@ -82,6 +82,9 @@ public class Games.CommandRunner : Object, Runner {
public void stop () {
}
public void attempt_create_savestate () {
}
public InputMode[] get_available_input_modes () {
return { };
}
......
......@@ -16,8 +16,9 @@ public interface Games.Runner : Object {
public abstract void resume () throws Error;
public abstract void pause ();
public abstract void stop ();
public abstract InputMode[] get_available_input_modes ();
public abstract void attempt_create_savestate () throws Error;
public abstract InputMode[] get_available_input_modes ();
public abstract bool key_press_event (Gdk.EventKey event);
public abstract bool gamepad_button_press_event (uint16 button);
}
public class Games.Savestate : Object {
private string path; // Path to the savestate directory
public Savestate (string path) {
this.path = path;
}
public string? get_name () {
var metadata = new KeyFile ();
var metadata_file_path = Path.build_filename (path, "metadata");
try {
metadata.load_from_file (metadata_file_path, KeyFileFlags.NONE);
var is_automatic = metadata.get_boolean ("Metadata", "Automatic");
if (is_automatic)
return null;
else
return metadata.get_string ("Metadata", "Name");
}
catch (Error e) {
critical ("Failed to get name from metadata file for savestate at %s: %s", path, e.message);
return null;
}
}
public DateTime? get_creation_date () {
var metadata = new KeyFile ();
var metadata_file_path = Path.build_filename (path, "metadata");
try {
metadata.load_from_file (metadata_file_path, KeyFileFlags.NONE);
var creation_date_str = metadata.get_string ("Metadata", "Creation Date");
return new DateTime.from_iso8601 (creation_date_str, new TimeZone.local ());
}
catch (Error e) {
critical ("Failed to get creation date from metadata file for savestate at %s: %s", path, e.message);
return null;
}
}
public void set_snapshot_data (Bytes snapshot_data) throws Error {
var buffer = snapshot_data.get_data ();
var snapshot_path = Path.build_filename (path, "snapshot");
FileUtils.set_data (snapshot_path, buffer);
}
public Bytes get_snapshot_data () throws Error {
var snapshot_path = Path.build_filename (path, "snapshot");
uint8[] data = null;
FileUtils.get_data (snapshot_path, out data);
var bytes = new Bytes.take (data);
return bytes;
}
public string get_save_ram_path () {
return Path.build_filename (path, "save");
}
public void set_save_ram_data (uint8[] save_ram_data) throws Error {
var save_ram_path = Path.build_filename (path, "save");
FileUtils.set_data (save_ram_path, save_ram_data);
}
public string get_screenshot_path () {
return Path.build_filename (path, "screenshot");
}
public string get_save_directory_path () {
return Path.build_filename (path, "save-dir");
}
public bool has_media_data () {
var media_path = Path.build_filename (path, "media");
return FileUtils.test (media_path, FileTest.EXISTS);
}
// Currently all games only have a number as media_data, so this method
// returns an int, but in the future it might return an abstract MediaData
public int get_media_data () throws Error {
var media_path = Path.build_filename (path, "media");
if (!FileUtils.test (media_path, FileTest.EXISTS))
throw new FileError.ACCES ("Savestate at %s does not contain media file", path);
string contents;
FileUtils.get_contents (media_path, out contents);
int media_number = int.parse (contents);
return media_number;
}
public void set_media_data (MediaSet media_set) throws Error {
var media_path = Path.build_filename (path, "media");
var contents = media_set.selected_media_number.to_string ();
FileUtils.set_contents (media_path, contents, contents.length);
}
public Savestate clone_in_tmp () throws Error {
var tmp_savestate_path = prepare_empty_savestate_in_tmp ();
var tmp_savestate_dir = File.new_for_path (tmp_savestate_path);
var cloned_savestate_dir = File.new_for_path (path);
FileOperations.copy_contents (cloned_savestate_dir, tmp_savestate_dir);
return new Savestate (tmp_savestate_path);
}
// This method is used to save the savestate in /tmp as a regular savestate
// inside the savestates directory of a game
// It names the newly created savestate using the creation date in the
// metadata file
public void save_in (string game_savestates_dir_path) throws Error {
var metadata = new KeyFile ();
var metadata_file_path = Path.build_filename (path, "metadata");
metadata.load_from_file (metadata_file_path, KeyFileFlags.NONE);
var creation_date = metadata.get_string ("Metadata", "Creation Date");
var copied_dir = File.new_for_path (path);
var new_savestate_dir_path = Path.build_filename (game_savestates_dir_path, creation_date);
var new_savestate_dir = File.new_for_path (new_savestate_dir_path);
FileOperations.copy_dir (copied_dir, new_savestate_dir);
}
// Set the metadata for an automatic savestate
public void set_metadata_automatic (DateTime creation_date, string platform, string core) throws Error {
set_metadata (true, null, creation_date, platform, core);
}
// Set the metadata for a manual savestate
public void set_metadata_manual (string name, DateTime creation_date, string platform, string core) throws Error {
set_metadata (false, name, creation_date, platform, core);
}
private void set_metadata (bool is_automatic, string? name, DateTime creation_date,
string platform, string core) throws Error {
var metadata_file_path = Path.build_filename (path, "metadata");
var metadata_file = File.new_for_path (metadata_file_path);
var metadata = new KeyFile ();
if (metadata_file.query_exists ())
metadata_file.@delete ();
metadata.set_boolean ("Metadata", "Automatic", is_automatic);
if (name != null)
metadata.set_string ("Metadata", "Name", name);
metadata.set_string ("Metadata", "Creation Date", creation_date.to_string ());
metadata.set_string ("Metadata", "Platform", platform);
metadata.set_string ("Metadata", "Core", core);
metadata.save_to_file (metadata_file_path);
}
// Automatic means whether the savestate was created automatically when
// quitting/loading the game or manually by the user using the Save button
public bool is_automatic () {
var metadata = new KeyFile ();
var metadata_file_path = Path.build_filename (path, "metadata");
try {
metadata.load_from_file (metadata_file_path, KeyFileFlags.NONE);
return metadata.get_boolean ("Metadata", "Automatic");
}
catch (Error e) {
critical ("Failed to get Automatic field from metadata file for savestate at %s: %s", path, e.message);
return false;
}
}
public void delete_from_disk () {
var savestate_dir = File.new_for_path (path);
// Treat errors locally in this method because there isn't much that
// can go wrong with deleting files
try {
FileOperations.delete_files (savestate_dir, {});
}
catch (Error e) {
warning ("Failed to delete savestate at %s: %s", path, e.message);
}
}
public static Savestate[] get_game_savestates (Uid game_uid, string core_id) throws Error {
var data_dir_path = Application.get_data_dir ();
var savestates_dir_path = Path.build_filename (data_dir_path, "savestates");
var uid_str = game_uid.get_uid ();
var core_id_prefix = core_id.replace (".libretro", "");
var game_savestates_dir_path = Path.build_filename (savestates_dir_path, uid_str + "-" + core_id_prefix);
var game_savestates_dir_file = File.new_for_path (game_savestates_dir_path);
if (!game_savestates_dir_file.query_exists ()) {
// The game has no savestates directory so we create one
game_savestates_dir_file.make_directory_with_parents ();
return {}; // Obviously no savestates available either
}
var game_savestates_dir = Dir.open (game_savestates_dir_path);
Savestate[] game_savestates = {};
string savestate_name = null;
while ((savestate_name = game_savestates_dir.read_name ()) != null) {
var savestate_path = Path.build_filename (game_savestates_dir_path, savestate_name);
game_savestates += new Savestate (savestate_path);
}
// Sort the savestates array by creation dates
qsort_with_data (game_savestates, sizeof (Savestate), compare_savestates_creation_date);
return game_savestates;
}
private static int compare_savestates_creation_date (Savestate s1, Savestate s2) {
// We want the savestates with the latest creation dates to be the first in the array
var s1_creation_date_str = s1.get_creation_date ().to_string ();
var s2_creation_date_str = s2.get_creation_date ().to_string ();
if (s1_creation_date_str > s2_creation_date_str)
return -1;
if (s1_creation_date_str == s2_creation_date_str)
return 0;
// s1_creation_date_str < s2_creation_date_str
return 1;
}
public static Savestate create_empty_in_tmp () throws Error {
return new Savestate (prepare_empty_savestate_in_tmp ());
}
// Returns the path of the newly created dir in tmp
public static string prepare_empty_savestate_in_tmp () throws Error {
var tmp_savestate_path = DirUtils.make_tmp ("games_savestate_XXXXXX");
var save_dir_path = Path.build_filename (tmp_savestate_path, "save-dir");
var save_dir = File.new_for_path (save_dir_path);
save_dir.make_directory ();
return tmp_savestate_path;
}
}
......@@ -48,6 +48,9 @@ private class Games.DummyRunner : Object, Runner {
public void stop () {
}
public void attempt_create_savestate () {
}
public InputMode[] get_available_input_modes () {
return { };
}
......
......@@ -42,6 +42,7 @@ vala_sources = [
'core/rating.vala',
'core/release-date.vala',
'core/runner.vala',
'core/savestate.vala',
'core/title.vala',
'core/uid.vala',
'core/uri-game-factory.vala',
......
......@@ -15,15 +15,15 @@ public class Games.RetroRunner : Object, Runner {
public bool can_resume {
get {
try {
// Check if there are any existing savestates
init ();
// Check if the core can support savestates
if (!core.get_can_access_state ())
return false;
var game_savestates_dir_path = get_game_savestates_dir_path ();
var game_savestates_dir = Dir.open (game_savestates_dir_path);
return game_savestates_dir.read_name () != null;
// Check if there are any existing savestates
if (game_savestates.length != 0)
return true;
}
catch (Error e) {
warning (e.message);
......@@ -51,10 +51,6 @@ public class Games.RetroRunner : Object, Runner {
}
}
private string save_directory_path;
private string save_path;
private string screenshot_path;
private Retro.CoreDescriptor core_descriptor;
private RetroCoreSource core_source;
private Platform platform;
......@@ -62,6 +58,8 @@ public class Games.RetroRunner : Object, Runner {
private InputCapabilities input_capabilities;
private Settings settings;
private Title game_title;
private Savestate[] game_savestates;
private Savestate latest_savestate;
private bool _running;
private bool running {
......@@ -110,7 +108,7 @@ public class Games.RetroRunner : Object, Runner {
public bool check_is_valid (out string error_message) throws Error {
try {
load_media_data ();
media_set.selected_media_number = 0;
init ();
}
catch (RetroError.MODULE_NOT_FOUND e) {
......@@ -140,7 +138,8 @@ public class Games.RetroRunner : Object, Runner {
}
public void start () throws Error {
load_media_data ();
if (latest_savestate != null && latest_savestate.has_media_data ())
media_set.selected_media_number = latest_savestate.get_media_data ();
if (!is_initialized)
init ();
......@@ -148,7 +147,9 @@ public class Games.RetroRunner : Object, Runner {
loop.stop ();
if (!is_ready) {
load_ram ();
if (latest_savestate != null)
load_save_ram (latest_savestate.get_save_ram_path ());
is_ready = true;
}
core.reset ();
......@@ -172,22 +173,15 @@ public class Games.RetroRunner : Object, Runner {
}
private void load_latest_savestate () throws Error {
var game_savestates_dir_path = get_game_savestates_dir_path ();
var game_savestates_dir = Dir.open (game_savestates_dir_path);
string latest_savestate_name = null;
string dir_entry = null;
while ((dir_entry = game_savestates_dir.read_name ()) != null) {
latest_savestate_name = dir_entry;
}
// TODO: This method assumes that there exists at least a savestate
// [Yeti]: Perhaps we should bug-proof this using an Assert ?
load_save_ram (latest_savestate.get_save_ram_path ());
core.reset ();
core.set_state (latest_savestate.get_snapshot_data ());
var latest_savestate_dir_path = Path.build_filename (game_savestates_dir_path, latest_savestate_name);
var latest_savestate_dir = File.new_for_path (latest_savestate_dir_path);
if (latest_savestate.has_media_data ())
media_set.selected_media_number = latest_savestate.get_media_data ();
//load_ram ();
core.reset ();
load_snapshot (latest_savestate_dir);
is_ready = true;
}
......@@ -213,6 +207,20 @@ public class Games.RetroRunner : Object, Runner {
loop = new Retro.MainLoop (core);
running = false;
// Load the game's savestates if there are any
string core_id = null;
if (core_descriptor != null) {
core_id = core_descriptor.get_id ();
}
else {
core_id = core_source.get_core_id ();
}
game_savestates = Savestate.get_game_savestates (uid, core_id);
if (game_savestates.length != 0)
latest_savestate = game_savestates[0];
load_screenshot ();
is_initialized = true;
......@@ -274,9 +282,11 @@ public class Games.RetroRunner : Object, Runner {
var platform_id = platform.get_id ();
core.system_directory = @"$platforms_dir/$platform_id/system";
var save_directory = get_save_directory_path ();
Application.try_make_dir (save_directory);
core.save_directory = save_directory;
if (latest_savestate != null) {
var save_directory = latest_savestate.get_save_directory_path ();
Application.try_make_dir (save_directory);
core.save_directory = save_directory;
}
core.log.connect (Retro.g_log);
view.set_core (core);
......@@ -312,7 +322,7 @@ public class Games.RetroRunner : Object, Runner {
pause ();
try {
save ();
attempt_create_savestate ();
}
catch (Error e) {
warning (e.message);
......@@ -381,8 +391,7 @@ public class Games.RetroRunner : Object, Runner {
}
private string get_game_savestates_dir_path () throws Error {
// Get the savestates directory of the game currently being run
// Get the savestates directory of the game
var data_dir_path = Application.get_data_dir ();
var savestates_dir_path = Path.build_filename (data_dir_path, "savestates");
var uid = uid.get_uid ();
......@@ -401,9 +410,7 @@ public class Games.RetroRunner : Object, Runner {
return Path.build_filename (savestates_dir_path, uid + "-" + core_id_prefix);
}
// FIXME: This should be private, but it is public because of a temporary
// hack used in the DisplayView
public void save () throws Error {
public void attempt_create_savestate () throws Error {
if (!should_save)
return;
......@@ -415,7 +422,7 @@ public class Games.RetroRunner : Object, Runner {
new_savestate_dir.make_directory ();
save_ram (new_savestate_dir);
store_save_ram (new_savestate_dir);
if (media_set.get_size () > 1)
save_media_data (new_savestate_dir);
......@@ -441,49 +448,24 @@ public class Games.RetroRunner : Object, Runner {
return @"$(Config.OPTIONS_DIR)/$options_name.options";
}
private string get_save_directory_path () throws Error {
if (save_directory_path != null)
return save_directory_path;
var dir = Application.get_saves_dir ();
var uid = uid.get_uid ();
save_directory_path = @"$dir/$uid";
return save_directory_path;
}
// TODO: To be removed
private string get_save_path () throws Error {
if (save_path != null)
return save_path;
var dir = Application.get_saves_dir ();
var uid = uid.get_uid ();
save_path = @"$dir/$uid.save";
return save_path;
}
private void save_ram (File savestate_dir) throws Error{
private void store_save_ram (File savestate_dir) throws Error{
var bytes = core.get_memory (Retro.MemoryType.SAVE_RAM);
var save = bytes.get_data ();
if (save.length == 0)
return;
var savestate_dir_path = savestate_dir.get_path ();
var save_path = Path.build_filename (savestate_dir_path, "save");
var save_ram_path = Path.build_filename (savestate_dir_path, "save");
FileUtils.set_data (save_path, save);
FileUtils.set_data (save_ram_path, save);
}
private void load_ram () throws Error {
var save_path = get_save_path ();
if (!FileUtils.test (save_path, FileTest.EXISTS))
private void load_save_ram (string save_ram_path) throws Error {
if (!FileUtils.test (save_ram_path, FileTest.EXISTS))
return;
uint8[] data = null;
FileUtils.get_data (save_path, out data);
FileUtils.get_data (save_ram_path, out data);
var expected_size = core.get_memory_size (Retro.MemoryType.SAVE_RAM);
if (data.length != expected_size)
......@@ -503,23 +485,6 @@ public class Games.RetroRunner : Object, Runner {
FileUtils.set_data (snapshot_path, buffer);
}
private void load_snapshot (File savestate_dir) throws Error {
if (!core.get_can_access_state ())
return;
var savestate_dir_path = savestate_dir.get_path ();
var snapshot_path = Path.build_filename (savestate_dir_path, "snapshot");
if (!FileUtils.test (snapshot_path, FileTest.EXISTS))
return;
uint8[] data = null;
FileUtils.get_data (snapshot_path, out data);
var bytes = new Bytes.take (data);
core.set_state (bytes);
}
private void save_media_data (File savestate_dir) throws Error {
var savestate_dir_path = savestate_dir.get_path ();
var media_path = Path.build_filename (savestate_dir_path, "media");
......@@ -529,40 +494,6 @@ public class Games.RetroRunner : Object, Runner {
FileUtils.set_contents (media_path, contents, contents.length);
}
private void load_media_data () throws Error {
var medias_path = get_medias_path ();
if (!FileUtils.test (medias_path, FileTest.EXISTS))
return;
string contents;
FileUtils.get_contents (medias_path, out contents);
int disc_num = int.parse (contents);
media_set.selected_media_number = disc_num;
}
// TODO: To be removed
private string get_medias_path () throws Error {
var dir = Application.get_medias_dir ();
var uid = uid.get_uid ();
return @"$dir/$uid.media";
}
// TODO: To be removed
private string get_screenshot_path () throws Error {
if (screenshot_path != null)
return screenshot_path;
var dir = Application.get_snapshots_dir ();
var uid = uid.get_uid ();
var now_time_str = TimeVal ().to_iso8601 ();
screenshot_path = @"$dir/$uid/$now_time_str.png";
return screenshot_path;
}
private void save_screenshot (File savestate_dir) throws Error {
if (!core.get_can_access_state ())
return;
......@@ -605,7 +536,11 @@ public class Games.RetroRunner : Object, Runner {
if (!core.get_can_access_state ())
return;
var screenshot_path = get_screenshot_path ();
if (game_savestates.length == 0)
return;
// Load the screenshot of the latest savestate
var screenshot_path = latest_savestate.get_screenshot_path ();
if (!FileUtils.test (screenshot_path, FileTest.EXISTS))
return;
......
......@@ -344,17 +344,11 @@ private class Games.DisplayView : Object, UiView {
box.runner.pause ();
// FIXME: Temporary hack used to avoid displaying the Quit Dialog when
// not necessary
var retro_runner = box.runner as RetroRunner;
if (retro_runner != null) {
try {
retro_runner.save ();
}
catch (Error e) {
critical (e.message);
}
try {
box.runner.attempt_create_savestate ();
}
catch (Error e) {
warning (e.message);
}
if (box.runner.can_quit_safely) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment