[gnome-games] retro-runner: Add abstractions for savestates
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-games] retro-runner: Add abstractions for savestates
- Date: Fri, 9 Aug 2019 13:38:48 +0000 (UTC)
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]