[gnome-games] retro-runner: Add abstractions for savestates



commit 457bdc2111287023c2314f8c27693f6c22f6dcdd
Author: Yetizone <andreii lisita gmail com>
Date:   Thu May 30 15:32:23 2019 +0300

    retro-runner: Add abstractions for savestates

 src/command/command-runner.vala |   3 +
 src/core/runner.vala            |   3 +-
 src/core/savestate.vala         | 253 ++++++++++++++++++++++++++++++++++++++++
 src/dummy/dummy-runner.vala     |   3 +
 src/meson.build                 |   1 +
 src/retro/retro-runner.vala     | 173 +++++++++------------------
 src/ui/display-view.vala        |  16 +--
 7 files changed, 321 insertions(+), 131 deletions(-)
---
diff --git a/src/command/command-runner.vala b/src/command/command-runner.vala
index 8db2a5e0..03256016 100644
--- a/src/command/command-runner.vala
+++ b/src/command/command-runner.vala
@@ -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 { };
        }
diff --git a/src/core/runner.vala b/src/core/runner.vala
index e5374866..df28e351 100644
--- a/src/core/runner.vala
+++ b/src/core/runner.vala
@@ -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);
 }
diff --git a/src/core/savestate.vala b/src/core/savestate.vala
new file mode 100644
index 00000000..77deb3f1
--- /dev/null
+++ b/src/core/savestate.vala
@@ -0,0 +1,253 @@
+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;
+       }
+}
+
diff --git a/src/dummy/dummy-runner.vala b/src/dummy/dummy-runner.vala
index d82572e0..b8e697f1 100644
--- a/src/dummy/dummy-runner.vala
+++ b/src/dummy/dummy-runner.vala
@@ -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 { };
        }
diff --git a/src/meson.build b/src/meson.build
index bee993dc..a08f60cf 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -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',
diff --git a/src/retro/retro-runner.vala b/src/retro/retro-runner.vala
index cb3250dc..17b4900e 100644
--- a/src/retro/retro-runner.vala
+++ b/src/retro/retro-runner.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;
diff --git a/src/ui/display-view.vala b/src/ui/display-view.vala
index 325f2ada..d053ddbc 100644
--- a/src/ui/display-view.vala
+++ b/src/ui/display-view.vala
@@ -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) {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]