diff --git a/data/gtk-style.css b/data/gtk-style.css
index c4cfd62cc66a86176e12cd326f2472ebcc300b11..410a5f596559a5d13c9507c1f5e57fa7f15ff734 100644
--- a/data/gtk-style.css
+++ b/data/gtk-style.css
@@ -8,6 +8,20 @@
background-color: @theme_base_color;
}
+.savestate-row {
+ padding: 0px;
+}
+
+.savestate-thumbnail {
+ min-width: 64px;
+ min-height: 64px;
+ color: rgba(255, 255, 255, 0.5);
+ background: rgba (0, 0, 0, .5);
+ border: 1px solid rgba (0, 0, 0, .5);
+ margin: 6px;
+ border-radius: 5px;
+}
+
gamesgamethumbnail {
background-color: mix (@theme_base_color, @theme_bg_color, 0.5);
border-width: 1px;
diff --git a/data/org.gnome.Games.gresource.xml b/data/org.gnome.Games.gresource.xml
index 2a73870ad32f0ed236895ed2a9c51f3c28f5dda5..d157b0a0f2d20bca3fb405851a78552cde01953f 100644
--- a/data/org.gnome.Games.gresource.xml
+++ b/data/org.gnome.Games.gresource.xml
@@ -48,6 +48,8 @@
ui/reset-controller-mapping-dialog.ui
ui/resume-dialog.ui
ui/resume-failed-dialog.ui
+ ui/savestate-listbox-row.ui
+ ui/savestates-list.ui
ui/search-bar.ui
ui/shortcuts-window.ui
diff --git a/data/ui/display-box.ui b/data/ui/display-box.ui
index db028f546cdb967cbf2815e6e540db4360e9e934..7cef4bdf699e6b4e98765f11d11063b386e1bd41 100644
--- a/data/ui/display-box.ui
+++ b/data/ui/display-box.ui
@@ -25,8 +25,19 @@
-
-
+
+
+ end
+
+
+
+
False
False
center
True
-
+
-
- Restore
+
+ Fullscreen
-
+
True
- view-restore-symbolic
+ view-fullscreen-symbolic
1
@@ -104,5 +130,72 @@
+
+
+
+
+
+ GTK_SIZE_GROUP_HORIZONTAL
+
+
+
+
+
+
diff --git a/data/ui/savestate-listbox-row.ui b/data/ui/savestate-listbox-row.ui
new file mode 100644
index 0000000000000000000000000000000000000000..5fc58d0b88654b5a4386122922eefac35ff2e775
--- /dev/null
+++ b/data/ui/savestate-listbox-row.ui
@@ -0,0 +1,56 @@
+
+
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+
+
+ true
+ vertical
+ 12
+ 12
+ center
+
+
+ true
+ true
+ 0
+
+
+
+
+
+
+
+
+ true
+ true
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/ui/savestates-list.ui b/data/ui/savestates-list.ui
new file mode 100644
index 0000000000000000000000000000000000000000..4fca186f83eb19c142a4767b011a1398ce673b87
--- /dev/null
+++ b/data/ui/savestates-list.ui
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+ True
+ False
+ slide-left
+
+
+ True
+
+
+ True
+
+
+
+
+
+ True
+ True
+ 350
+
+
+ True
+
+
+
+
+ True
+
+
+
+ True
+
+
+ True
+ list-add-symbolic
+ 32
+
+
+
+
+
+ True
+ 12
+ Create new savestate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/desktop/src/desktop-plugin.vala b/plugins/desktop/src/desktop-plugin.vala
index e7967f5db3b14b66548abbf3e996db0be01e31d0..1b03dcd4455cefe4e001579245787690a28a3651 100644
--- a/plugins/desktop/src/desktop-plugin.vala
+++ b/plugins/desktop/src/desktop-plugin.vala
@@ -4,11 +4,12 @@ private class Games.DesktopPlugin : Object, Plugin {
private const string MIME_TYPE = "application/x-desktop";
private const string PLATFORM_ID = "Desktop";
private const string PLATFORM_NAME = _("Desktop");
+ private const string PLATFORM_UID_PREFIX = "desktop";
private static Platform platform;
static construct {
- platform = new GenericPlatform (PLATFORM_ID, PLATFORM_NAME);
+ platform = new GenericPlatform (PLATFORM_ID, PLATFORM_NAME, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
diff --git a/plugins/dreamcast/src/dreamcast-plugin.vala b/plugins/dreamcast/src/dreamcast-plugin.vala
index bcf4c2fe6c26b0bddab2dde62cb97d848f884fe0..93a8994897c4a3a51676146c0a7713854c3d5aca 100644
--- a/plugins/dreamcast/src/dreamcast-plugin.vala
+++ b/plugins/dreamcast/src/dreamcast-plugin.vala
@@ -4,11 +4,12 @@ private class Games.DreamcastPlugin : Object, Plugin {
private const string MIME_TYPE = "application/x-dc-rom";
private const string PLATFORM_ID = "Dreamcast";
private const string PLATFORM_NAME = _("Dreamcast");
+ private const string PLATFORM_UID_PREFIX = "dreamcast";
private static RetroPlatform platform;
static construct {
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE });
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE }, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
diff --git a/plugins/game-cube/src/game-cube-plugin.vala b/plugins/game-cube/src/game-cube-plugin.vala
index eea79684ef2148805a1d945be48b1129d01f09cf..8bc4411e3077a05e4d57dff463744dcb7b1a6f56 100644
--- a/plugins/game-cube/src/game-cube-plugin.vala
+++ b/plugins/game-cube/src/game-cube-plugin.vala
@@ -4,11 +4,12 @@ private class Games.GameCubePlugin : Object, Plugin {
private const string MIME_TYPE = "application/x-gamecube-rom";
private const string PLATFORM_ID = "GameCube";
private const string PLATFORM_NAME = _("Nintendo GameCube");
+ private const string PLATFORM_UID_PREFIX = "game-cube";
private static RetroPlatform platform;
static construct {
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE });
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE }, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
diff --git a/plugins/libretro/src/libretro-plugin.vala b/plugins/libretro/src/libretro-plugin.vala
index 75ec64b97a46d84776360594d43bc4479ab54450..84008eb4964a683d69b8419ce89fde94df4c8978 100644
--- a/plugins/libretro/src/libretro-plugin.vala
+++ b/plugins/libretro/src/libretro-plugin.vala
@@ -4,11 +4,12 @@ private class Games.LibretroPlugin : Object, Plugin {
private const string LIBRETRO_FILE_SCHEME = "libretro+file";
private const string PLATFORM_ID = "Libretro";
private const string PLATFORM_NAME = _("Libretro");
+ private const string PLATFORM_UID_PREFIX = "libretro";
private static Platform platform;
static construct {
- platform = new GenericPlatform (PLATFORM_ID, PLATFORM_NAME);
+ platform = new GenericPlatform (PLATFORM_ID, PLATFORM_NAME, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
diff --git a/plugins/love/src/love-plugin.vala b/plugins/love/src/love-plugin.vala
index add7c2377aed8dfd48f49c4f949f1f0a1fb60673..34e51038e43a153f5a92e71525b027ad9ec1595a 100644
--- a/plugins/love/src/love-plugin.vala
+++ b/plugins/love/src/love-plugin.vala
@@ -1,15 +1,15 @@
// This file is part of GNOME Games. License: GPL-3.0+.
private class Games.LovePlugin : Object, Plugin {
- private const string FINGERPRINT_PREFIX = "love";
private const string MIME_TYPE = "application/x-love-game";
private const string PLATFORM_ID = "LOVE";
private const string PLATFORM_NAME = _("LÖVE");
+ private const string PLATFORM_UID_PREFIX = "love";
private static Platform platform;
static construct {
- platform = new GenericPlatform (PLATFORM_ID, PLATFORM_NAME);
+ platform = new GenericPlatform (PLATFORM_ID, PLATFORM_NAME, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
@@ -29,7 +29,7 @@ private class Games.LovePlugin : Object, Plugin {
}
private static Game game_for_uri (Uri uri) throws Error {
- var uid = new FingerprintUid (uri, FINGERPRINT_PREFIX);
+ var uid = new FingerprintUid (uri, PLATFORM_UID_PREFIX);
var package = new LovePackage (uri);
var title = new LoveTitle (package);
var icon = new LoveIcon (package);
diff --git a/plugins/ms-dos/src/ms-dos-plugin.vala b/plugins/ms-dos/src/ms-dos-plugin.vala
index 10fdd1a3e6040d965221f5b9f83403f806f5bbe6..42ed8935d92e5284b6d47041788219c17a2a485d 100644
--- a/plugins/ms-dos/src/ms-dos-plugin.vala
+++ b/plugins/ms-dos/src/ms-dos-plugin.vala
@@ -1,15 +1,15 @@
// This file is part of GNOME Games. License: GPL-3.0+.
private class Games.MsDosPlugin : Object, Plugin {
- private const string FINGERPRINT_PREFIX = "ms-dos";
private const string MIME_TYPE = "application/x-ms-dos-executable";
private const string PLATFORM_ID = "MSDOS";
private const string PLATFORM_NAME = _("MS-DOS");
+ private const string PLATFORM_UID_PREFIX = "ms-dos";
private static RetroPlatform platform;
static construct {
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE });
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE }, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
@@ -25,7 +25,7 @@ private class Games.MsDosPlugin : Object, Plugin {
}
private static Game game_for_uri (Uri uri) throws Error {
- var uid = new FingerprintUid (uri, FINGERPRINT_PREFIX);
+ var uid = new FingerprintUid (uri, PLATFORM_UID_PREFIX);
var title = new FilenameTitle (uri);
var media = new GriloMedia (title, MIME_TYPE);
var release_date = new GriloReleaseDate (media);
diff --git a/plugins/nintendo-ds/src/nintendo-ds-plugin.vala b/plugins/nintendo-ds/src/nintendo-ds-plugin.vala
index c397fda2dcca4974239a807e5e3dc231596c165a..0ad5cd68661afc59826e697c33bda22a1b16254c 100644
--- a/plugins/nintendo-ds/src/nintendo-ds-plugin.vala
+++ b/plugins/nintendo-ds/src/nintendo-ds-plugin.vala
@@ -1,15 +1,15 @@
// This file is part of GNOME Games. License: GPL-3.0+.
private class Games.NintendoDsPlugin : Object, Plugin {
- private const string FINGERPRINT_PREFIX = "nintendo-ds";
private const string MIME_TYPE = "application/x-nintendo-ds-rom";
private const string PLATFORM_ID = "NintendoDS";
private const string PLATFORM_NAME = _("Nintendo DS");
+ private const string PLATFORM_UID_PREFIX = "nintendo-ds";
private static RetroPlatform platform;
static construct {
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE });
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE }, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
@@ -29,7 +29,7 @@ private class Games.NintendoDsPlugin : Object, Plugin {
}
private static Game game_for_uri (Uri uri) throws Error {
- var uid = new FingerprintUid (uri, FINGERPRINT_PREFIX);
+ var uid = new FingerprintUid (uri, PLATFORM_UID_PREFIX);
var title = new FilenameTitle (uri);
var icon = new NintendoDsIcon (uri);
var media = new GriloMedia (title, MIME_TYPE);
diff --git a/plugins/playstation/src/playstation-plugin.vala b/plugins/playstation/src/playstation-plugin.vala
index 967b4bef00b5da5f50aa8470b537177cf0f61da7..7695de96a52f6d431ae176908d8960a458016ee0 100644
--- a/plugins/playstation/src/playstation-plugin.vala
+++ b/plugins/playstation/src/playstation-plugin.vala
@@ -5,12 +5,13 @@ private class Games.PlayStation : Object, Plugin {
private const string PHONY_MIME_TYPE = "application/x-playstation-rom";
private const string PLATFORM_ID = "PlayStation";
private const string PLATFORM_NAME = _("PlayStation");
+ private const string PLATFORM_UID_PREFIX = "playstation";
private static RetroPlatform platform;
static construct {
string[] mime_types = { CUE_MIME_TYPE, PHONY_MIME_TYPE };
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, mime_types);
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, mime_types, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
diff --git a/plugins/sega-cd/src/sega-cd-plugin.vala b/plugins/sega-cd/src/sega-cd-plugin.vala
index ae376ff8b60db1dcdc9afb51e9a8f4cedd8c01dd..56869813a7cc9d6071a4c99863ef2d5d3c47dce8 100644
--- a/plugins/sega-cd/src/sega-cd-plugin.vala
+++ b/plugins/sega-cd/src/sega-cd-plugin.vala
@@ -3,7 +3,8 @@
private class Games.SegaCDPlugin : Object, Plugin {
private const string 32X_MIME_TYPE = "application/x-genesis-32x-rom";
- private const string SEGA_CD_PREFIX = "mega-cd";
+ private const string SEGA_CD_UID_PREFIX = "mega-cd";
+ private const string SEGA_CD_32X_UID_PREFIX = "mega-cd";
private const string CUE_MIME_TYPE = "application/x-cue";
private const string SEGA_CD_MIME_TYPE = "application/x-sega-cd-rom";
private const string SEGA_CD_PLATFORM_ID = "SegaCD";
@@ -19,8 +20,8 @@ private class Games.SegaCDPlugin : Object, Plugin {
static construct {
string[] mime_types = { CUE_MIME_TYPE, SEGA_CD_MIME_TYPE };
string[] mime_types_32x = { CUE_MIME_TYPE, SEGA_CD_MIME_TYPE, 32X_MIME_TYPE };
- platform_sega_cd = new RetroPlatform (SEGA_CD_PLATFORM_ID, SEGA_CD_PLATFORM_NAME, mime_types);
- platform_sega_cd_32x = new RetroPlatform (SEGA_CD_32X_PLATFORM_ID, SEGA_CD_32X_PLATFORM_NAME, mime_types_32x);
+ platform_sega_cd = new RetroPlatform (SEGA_CD_PLATFORM_ID, SEGA_CD_PLATFORM_NAME, mime_types, SEGA_CD_UID_PREFIX);
+ platform_sega_cd_32x = new RetroPlatform (SEGA_CD_32X_PLATFORM_ID, SEGA_CD_32X_PLATFORM_NAME, mime_types_32x, SEGA_CD_32X_UID_PREFIX);
}
public Platform[] get_platforms () {
@@ -73,7 +74,7 @@ private class Games.SegaCDPlugin : Object, Plugin {
var bin_uri = new Uri (bin_file.get_uri ());
var header_offset = header.get_offset ();
- var uid = new FingerprintUid.for_chunk (bin_uri, SEGA_CD_PREFIX, header_offset, SegaCDHeader.HEADER_LENGTH);
+ var uid = new FingerprintUid.for_chunk (bin_uri, SEGA_CD_UID_PREFIX, header_offset, SegaCDHeader.HEADER_LENGTH);
var title = new FilenameTitle (uri);
var media = new GriloMedia (title, SEGA_CD_MIME_TYPE);
var cover = new CompositeCover ({
diff --git a/plugins/sega-saturn/src/sega-saturn-plugin.vala b/plugins/sega-saturn/src/sega-saturn-plugin.vala
index 4637d2d28e844352d473d10120aaabf60ed01d58..1e0aac0412f7ed4c1c8b3cc7e98d0c3809a68e8d 100644
--- a/plugins/sega-saturn/src/sega-saturn-plugin.vala
+++ b/plugins/sega-saturn/src/sega-saturn-plugin.vala
@@ -5,12 +5,13 @@ private class Games.SegaSaturnPlugin : Object, Plugin {
private const string SEGA_SATURN_MIME_TYPE = "application/x-saturn-rom";
private const string PLATFORM_ID = "SegaSaturn";
private const string PLATFORM_NAME = _("Sega Saturn");
+ private const string PLATFORM_UID_PREFIX = "sega-saturn";
private static RetroPlatform platform;
static construct {
string[] mime_types = { CUE_MIME_TYPE, SEGA_SATURN_MIME_TYPE };
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, mime_types);
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, mime_types, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
diff --git a/plugins/steam/src/steam-plugin.vala b/plugins/steam/src/steam-plugin.vala
index 0e60dd90b3bb89c93907c4efb23fd3d0aae474cd..5ea51ecca4da4f693cae834bcd8841164de5d388 100644
--- a/plugins/steam/src/steam-plugin.vala
+++ b/plugins/steam/src/steam-plugin.vala
@@ -8,11 +8,12 @@ private class Games.SteamPlugin : Object, Plugin {
private const string FLATPAK_STEAM_FILE_SCHEME = "flatpak+steam+file";
private const string PLATFORM_ID = "Steam";
private const string PLATFORM_NAME = _("Steam");
+ private const string PLATFORM_UID_PREFIX = "steam";
private static Platform platform;
static construct {
- platform = new GenericPlatform (PLATFORM_ID, PLATFORM_NAME);
+ platform = new GenericPlatform (PLATFORM_ID, PLATFORM_NAME, PLATFORM_UID_PREFIX);
// Add directories where Steam installs icons
var home = Environment.get_home_dir ();
diff --git a/plugins/turbografx-cd/src/turbografx-cd-plugin.vala b/plugins/turbografx-cd/src/turbografx-cd-plugin.vala
index 1abe0cc067628b97e562e7668a3a56645b7d15e4..bde08da1de645eec0ed2396cdde020d8e6db7838 100644
--- a/plugins/turbografx-cd/src/turbografx-cd-plugin.vala
+++ b/plugins/turbografx-cd/src/turbografx-cd-plugin.vala
@@ -1,19 +1,19 @@
// This file is part of GNOME Games. License: GPL-3.0+.
private class Games.TurboGrafxCDPlugin : Object, Plugin {
- private const string FINGERPRINT_PREFIX = "pc-engine";
private const string PHONY_MIME_TYPE = "application/x-pc-engine-cd-rom";
private const string CUE_MIME_TYPE = "application/x-cue";
private const string CD_MAGIC_VALUE = "PC Engine CD-ROM SYSTEM";
private const string PLATFORM_ID = "TurboGrafxCD";
/* translators: known as "CD-ROM²" in eastern Asia and France */
private const string PLATFORM_NAME = _("TurboGrafx-CD");
+ private const string PLATFORM_UID_PREFIX = "pc-engine";
private static RetroPlatform platform;
static construct {
string[] mime_types = { CUE_MIME_TYPE, PHONY_MIME_TYPE };
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, mime_types);
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, mime_types, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
@@ -36,7 +36,7 @@ private class Games.TurboGrafxCDPlugin : Object, Plugin {
if (!is_valid_disc (uri))
throw new TurboGrafxCDError.INVALID_DISC ("“%s” isn’t a valid TurboGrafx-CD disc.", uri.to_string ());
- var uid = new FingerprintUid (uri, FINGERPRINT_PREFIX);
+ var uid = new FingerprintUid (uri, PLATFORM_UID_PREFIX);
var title = new FilenameTitle (uri);
var media = new GriloMedia (title, PHONY_MIME_TYPE);
var cover = new CompositeCover ({
diff --git a/plugins/virtual-boy/src/virtual-boy-plugin.vala b/plugins/virtual-boy/src/virtual-boy-plugin.vala
index c6ce11c2095a7f8f92a6d4fbc1997c7f92a996df..88463241fb583c9388b3b954aa1a390f4b967a18 100644
--- a/plugins/virtual-boy/src/virtual-boy-plugin.vala
+++ b/plugins/virtual-boy/src/virtual-boy-plugin.vala
@@ -1,15 +1,15 @@
// This file is part of GNOME Games. License: GPL-3.0+.
private class Games.VirtualBoyPlugin : Object, Plugin {
- private const string FINGERPRINT_PREFIX = "virtual-boy";
private const string MIME_TYPE = "application/x-virtual-boy-rom";
private const string PLATFORM_ID = "VirtualBoy";
private const string PLATFORM_NAME = _("Virtual Boy");
+ private const string PLATFORM_UID_PREFIX = "virtual-boy";
private static RetroPlatform platform;
static construct {
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE });
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE }, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
@@ -34,7 +34,7 @@ private class Games.VirtualBoyPlugin : Object, Plugin {
var header = new VirtualBoyHeader (file);
header.check_validity ();
- var uid = new FingerprintUid (uri, FINGERPRINT_PREFIX);
+ var uid = new FingerprintUid (uri, PLATFORM_UID_PREFIX);
var title = new FilenameTitle (uri);
var media = new GriloMedia (title, MIME_TYPE);
var cover = new CompositeCover ({
diff --git a/plugins/wii/src/wii-plugin.vala b/plugins/wii/src/wii-plugin.vala
index 3b3b9e6a8b0f81917091218d44a3c015e195b4cd..b9e3aadd4ccd4e61b46080366bd4454ce9fe2125 100644
--- a/plugins/wii/src/wii-plugin.vala
+++ b/plugins/wii/src/wii-plugin.vala
@@ -4,11 +4,12 @@ private class Games.WiiPlugin : Object, Plugin {
private const string MIME_TYPE = "application/x-wii-rom";
private const string PLATFORM_ID = "Wii";
private const string PLATFORM_NAME = _("Wii");
+ private const string PLATFORM_UID_PREFIX = "wii";
private static RetroPlatform platform;
static construct {
- platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE });
+ platform = new RetroPlatform (PLATFORM_ID, PLATFORM_NAME, { MIME_TYPE }, PLATFORM_UID_PREFIX);
}
public Platform[] get_platforms () {
diff --git a/src/command/command-runner.vala b/src/command/command-runner.vala
index 8db2a5e045e1145992b4e6ca9ad38afa9a0cfbf2..1f6a57a3edc253b23766ddb5ad9e8f1dbab21d40 100644
--- a/src/command/command-runner.vala
+++ b/src/command/command-runner.vala
@@ -5,11 +5,15 @@ public class Games.CommandRunner : Object, Runner {
get { return false; }
}
- public bool can_quit_safely {
- get { return true; }
+ public bool can_resume {
+ get { return false; }
}
- public bool can_resume {
+ public bool supports_savestates {
+ get { return false; }
+ }
+
+ public bool can_support_savestates {
get { return false; }
}
@@ -28,7 +32,7 @@ public class Games.CommandRunner : Object, Runner {
this.args = args;
}
- public bool check_is_valid (out string error_message) throws Error {
+ public bool try_init_phase_one (out string error_message) {
if (args.length > 0) {
error_message = "";
@@ -49,6 +53,22 @@ public class Games.CommandRunner : Object, Runner {
return null;
}
+ public void capture_current_state_pixbuf () {
+ }
+
+ public void preview_current_state () {
+ }
+
+ public void preview_savestate (Savestate savestate) {
+ }
+
+ public void load_previewed_savestate () {
+ }
+
+ public Savestate[] get_savestates () {
+ return {};
+ }
+
public void start () throws Error {
string? working_directory = null;
string[]? envp = null;
@@ -73,7 +93,7 @@ public class Games.CommandRunner : Object, Runner {
}
}
- public void resume () throws Error {
+ public void resume () {
}
public void pause () {
@@ -82,6 +102,13 @@ public class Games.CommandRunner : Object, Runner {
public void stop () {
}
+ public Savestate? try_create_savestate (bool is_automatic) {
+ return null;
+ }
+
+ public void delete_savestate (Savestate savestate) {
+ }
+
public InputMode[] get_available_input_modes () {
return { };
}
diff --git a/src/core/migrator.vala b/src/core/migrator.vala
new file mode 100644
index 0000000000000000000000000000000000000000..e3f4353b098c45445bfd3b31fce26ad88975416e
--- /dev/null
+++ b/src/core/migrator.vala
@@ -0,0 +1,270 @@
+// This file is part of GNOME Games. License: GPL-3.0+.
+
+public class Games.Migrator : Object {
+ // Returns true if the migration wasn't necessary or
+ // if it was performed succesfully
+ public static bool apply_migration_if_necessary () {
+ var data_dir_path = Application.get_data_dir ();
+ var data_dir = File.new_for_path (data_dir_path);
+
+ var backup_archive_path = Path.build_filename (data_dir_path, "exported_data.zip");
+ var backup_archive = File.new_for_path (backup_archive_path);
+ var database_path = Application.get_database_path ();
+ string[] backup_excluded_files = { database_path, backup_archive_path };
+
+ var version_file = data_dir.get_child (".version");
+
+ // If the version file exists, there's no need
+ // to apply the migration
+ if (version_file.query_exists ())
+ return true;
+
+ info ("[Migrator]: Migration is necessary");
+
+ // Attempt to create a backup of the previous data
+ try {
+ backup_archive.create (FileCreateFlags.NONE);
+ FileOperations.compress_dir (backup_archive_path, data_dir, backup_excluded_files);
+ }
+ catch (Error e) {
+ critical ("Unable to backup data, aborting migration: %s", e.message);
+ return false;
+ }
+
+ try {
+ // The migration executes file I/O which may result in errors being
+ // thrown
+ apply_migration (version_file);
+ }
+ catch (Error e) {
+ critical ("Migration failed: %s", e.message);
+
+ // Delete all directories from the data dir
+ var savestates_dir_path = Path.build_filename (data_dir_path, "/savestates");
+ var savestates_dir = File.new_for_path (savestates_dir_path);
+
+ delete_files_no_errors (savestates_dir);
+ delete_old_directories ();
+
+ // Attempt to restore data from backup
+ if (try_restore_data (backup_archive_path, data_dir_path, backup_excluded_files)) {
+ // Successfully restored data from backup, deleting backup
+ delete_files_no_errors (backup_archive);
+ }
+ else {
+ // Something went seriously wrong here
+ // Migration failed and restoring backup data also failed
+ assert_not_reached ();
+ }
+
+ return false;
+ }
+
+ // Migration applied succesfully, deleting backup
+ delete_files_no_errors (backup_archive);
+
+ return true;
+ }
+
+ private static void apply_migration (File version_file) throws Error {
+ // Create the version file
+ version_file.create (FileCreateFlags.NONE);
+
+ // Create the savestates dir
+ var savestates_dir = File.new_for_path (get_savestates_dir_path ());
+ savestates_dir.make_directory ();
+
+ // Currently any game has only one snapshot file
+ // So for every snapshot file create a savestate
+ var snapshots_dir_path = get_old_snapshots_dir_path ();
+ var snapshots_dir = Dir.open (snapshots_dir_path, 0);
+ var file_name = "";
+ while ((file_name = snapshots_dir.read_name ()) != null) {
+ var file_name_tokens = file_name.split (".snapshot");
+
+ if (file_name_tokens.length != 2)
+ continue; // Not a snapshot file
+
+ // The snapshot files are curently named "[game_uid].snapshot"
+ var game_uid = file_name_tokens[0];
+ create_first_game_savestate (game_uid);
+ }
+
+ delete_old_directories ();
+ }
+
+ private static void create_first_game_savestate (string game_uid) throws Error {
+ // Inside the savestates dir there will be a sub-dir for each game
+ // which will contain all of the savestates for that game
+ // These sub-dirs will be named "[game_uid]-[core]"
+
+ // Getting the core_id
+ var platform = platform_from_game_uid (game_uid);
+ var core_manager = RetroCoreManager.get_instance ();
+ var preferred_core = core_manager.get_preferred_core (platform);
+ var core_id = preferred_core.get_id ();
+
+ // Create the directory for the game's savestates
+ var core_id_prefix = core_id.replace (".libretro", ""); // Remove the ".libretro" from the core_id
+ var game_savestates_dir_name = game_uid + "-" + core_id_prefix;
+ var game_savestates_dir_path = Path.build_filename (get_savestates_dir_path (), game_savestates_dir_name);
+ var game_savestates_dir = File.new_for_path (game_savestates_dir_path);
+
+ game_savestates_dir.make_directory ();
+
+ // Create the directory for the first savestate
+ var now_time = new DateTime.now ();
+ var now_time_str = now_time.to_string ();
+ var savestate_dir_path = Path.build_filename (game_savestates_dir_path, now_time_str);
+ var savestate_dir = File.new_for_path (savestate_dir_path);
+
+ savestate_dir.make_directory ();
+
+ // Use the currently existing game data (snapshot, screenshot,
+ // save file, save dir) to populate the savestate
+ var snapshots_dir_path = get_old_snapshots_dir_path ();
+ var snapshot_path = Path.build_filename (snapshots_dir_path, game_uid + ".snapshot");
+ var screenshot_path = Path.build_filename (snapshots_dir_path, game_uid + ".png");
+ var saves_dir = get_old_saves_dir_path ();
+ var save_dir_path = Path.build_filename (saves_dir, game_uid);
+ var save_file_path = save_dir_path + ".save";
+ var medias_dir = get_old_medias_dir_path ();
+ var media_file_path = Path.build_filename (medias_dir, game_uid + ".media");
+
+ var snapshot_file = File.new_for_path (snapshot_path);
+ var screenshot_file = File.new_for_path (screenshot_path);
+ var save_dir = File.new_for_path (save_dir_path);
+ var save_file = File.new_for_path (save_file_path);
+ var media_file = File.new_for_path (media_file_path);
+
+ var savestate_snapshot_file_path = Path.build_filename (savestate_dir_path, "snapshot");
+ var savestate_snapshot_file = File.new_for_path (savestate_snapshot_file_path);
+ FileOperations.copy_contents (snapshot_file, savestate_snapshot_file);
+
+ var savestate_screenshot_file_path = Path.build_filename (savestate_dir_path, "screenshot");
+ var savestate_screenshot_file = File.new_for_path (savestate_screenshot_file_path);
+ FileOperations.copy_contents (screenshot_file, savestate_screenshot_file);
+
+ if (!save_dir.query_exists ())
+ save_dir.make_directory ();
+
+ var savestate_save_dir_path = Path.build_filename (savestate_dir_path, "save-dir");
+ var savestate_save_dir = File.new_for_path (savestate_save_dir_path);
+ FileOperations.copy_dir (save_dir, savestate_save_dir);
+
+ if (save_file.query_exists ()) {
+ var savestate_save_file_path = Path.build_filename (savestate_dir_path, "save");
+ var savestate_save_file = File.new_for_path (savestate_save_file_path);
+ FileOperations.copy_contents (save_file, savestate_save_file);
+ }
+
+ if (media_file.query_exists ()) {
+ var savestate_media_file_path = Path.build_filename (savestate_dir_path, "media");
+ var savestate_media_file = File.new_for_path (savestate_media_file_path);
+ FileOperations.copy_contents (media_file, savestate_media_file);
+ }
+
+ // Create a KeyFile with additional data
+ var metadata = new KeyFile ();
+ var metadata_file_path = Path.build_filename (savestate_dir_path, "metadata");
+
+ // Automatic means whether the savestate was created automatically when
+ // quitting/loading the game or manually by the user using the Save button
+ metadata.set_boolean ("Metadata", "Automatic", true);
+ metadata.set_string ("Metadata", "Creation Date", now_time_str);
+ metadata.set_string ("Metadata", "Platform", platform.get_uid_prefix ());
+ metadata.set_string ("Metadata", "Core", core_id);
+ metadata.save_to_file (metadata_file_path);
+ }
+
+
+ private static RetroPlatform platform_from_game_uid (string game_uid) {
+ // [game_uid] is currently formed as "[platform]-[hash]"
+ // So we can use the game_uid to get the platform
+ var platforms_register = PlatformRegister.get_register ();
+ var platforms = platforms_register.get_all_platforms ();
+
+ string best_match = null;
+ RetroPlatform result_platform = null;
+
+ foreach (var platform in platforms) {
+ var retro_platform = platform as RetroPlatform;
+
+ if (retro_platform == null)
+ continue; // not a RetroPlatform
+
+ var platform_uid_prefix = platform.get_uid_prefix ();
+
+ if (game_uid.contains (platform_uid_prefix)) {
+ if (best_match == null || platform_uid_prefix.length > best_match.length) {
+ best_match = platform_uid_prefix;
+ result_platform = retro_platform;
+ }
+ }
+ }
+
+ return result_platform;
+ }
+
+ // Delete the old snapshots, saves and medias directories
+ private static void delete_old_directories () {
+ var snapshots_dir_path = get_old_snapshots_dir_path ();
+ var saves_dir_path = get_old_saves_dir_path ();
+ var medias_dir_path = get_old_medias_dir_path ();
+
+ var snapshots_dir = File.new_for_path (snapshots_dir_path);
+ var saves_dir = File.new_for_path (saves_dir_path);
+ var medias_dir = File.new_for_path (medias_dir_path);
+
+ delete_files_no_errors (snapshots_dir);
+ delete_files_no_errors (saves_dir);
+ delete_files_no_errors (medias_dir);
+ }
+
+ private static string get_old_snapshots_dir_path () {
+ var data_dir = Application.get_data_dir ();
+
+ return @"$data_dir/snapshots";
+ }
+
+ private static string get_old_saves_dir_path () {
+ var data_dir = Application.get_data_dir ();
+
+ return @"$data_dir/saves";
+ }
+
+ private static string get_old_medias_dir_path () {
+ var data_dir = Application.get_data_dir ();
+
+ return @"$data_dir/medias";
+ }
+
+ private static string get_savestates_dir_path () {
+ var data_dir = Application.get_data_dir ();
+
+ return @"$data_dir/savestates";
+ }
+
+ // Method for deleting files that treats errors locally because there isn't
+ // much that can go wrong with deleting files
+ private static void delete_files_no_errors (File file) {
+ try {
+ FileOperations.delete_files (file, {});
+ }
+ catch (Error e) {
+ warning ("Cannot delete file %s: %s", file.get_path (), e.message);
+ }
+ }
+
+ private static bool try_restore_data (string backup_archive_path, string data_dir_path, string[] backup_excluded_files) {
+ try {
+ FileOperations.extract_archive (backup_archive_path, data_dir_path, backup_excluded_files);
+ }
+ catch (Error e) {
+ critical ("Failed to restore data from backup archive %s: %s", backup_archive_path, e.message);
+ return false;
+ }
+
+ return true; // Succesfully restored data from backup
+ }
+}
diff --git a/src/core/platform.vala b/src/core/platform.vala
index a5a57f77ba0d797098ade0d1911a8f7011517069..6f07cec65b1c8676a428dba735506c8934655031 100644
--- a/src/core/platform.vala
+++ b/src/core/platform.vala
@@ -5,6 +5,8 @@ public interface Games.Platform : Object {
public abstract string get_name ();
+ public abstract string get_uid_prefix ();
+
public abstract PreferencesPagePlatformsRow get_row ();
public static uint hash (Platform platform) {
diff --git a/src/core/runner.vala b/src/core/runner.vala
index e53748661dca5e596f522ae8df193ada0e861f1e..dbcf52f987684702c31548024b0953b2b0d53dd1 100644
--- a/src/core/runner.vala
+++ b/src/core/runner.vala
@@ -4,20 +4,31 @@ public interface Games.Runner : Object {
public signal void stopped ();
public abstract bool can_fullscreen { get; }
- public abstract bool can_quit_safely { get; }
public abstract bool can_resume { get; }
+ public abstract bool supports_savestates { get; }
+ public abstract bool can_support_savestates { get; } // Now or in the future
public abstract MediaSet? media_set { get; }
public abstract InputMode input_mode { get; set; }
- public abstract bool check_is_valid (out string error_message) throws Error;
public abstract Gtk.Widget get_display ();
public abstract Gtk.Widget? get_extra_widget ();
+
+ public abstract bool try_init_phase_one (out string error_message);
public abstract void start () throws Error;
- public abstract void resume () throws Error;
+ public abstract void resume ();
public abstract void pause ();
public abstract void stop ();
- public abstract InputMode[] get_available_input_modes ();
+ public abstract void capture_current_state_pixbuf ();
+ public abstract void preview_current_state ();
+
+ public abstract Savestate? try_create_savestate (bool is_automatic);
+ public abstract void delete_savestate (Savestate savestate);
+ public abstract void preview_savestate (Savestate savestate);
+ public abstract void load_previewed_savestate () throws Error;
+ public abstract Savestate[] get_savestates ();
+
+ 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 0000000000000000000000000000000000000000..77deb3f1bd89e54f3e31d4c573e771a938747800
--- /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-platform.vala b/src/dummy/dummy-platform.vala
index 6d83eee3584ad86a076810422f15acb4bc4bebba..0b41086709e40dd26da3faa6ebf83338f8f125c8 100644
--- a/src/dummy/dummy-platform.vala
+++ b/src/dummy/dummy-platform.vala
@@ -9,6 +9,10 @@ public class Games.DummyPlatform : Object, Platform {
return _("Unknown");
}
+ public string get_uid_prefix () {
+ return "unknown";
+ }
+
public PreferencesPagePlatformsRow get_row () {
return new PreferencesPagePlatformsGenericRow (_("Unknown"));
}
diff --git a/src/dummy/dummy-runner.vala b/src/dummy/dummy-runner.vala
index d82572e00f68eecf4695bdf6efa9fe6f5bd24f07..6b494162eb0908f2c02e0107c9f056ce1aeab10a 100644
--- a/src/dummy/dummy-runner.vala
+++ b/src/dummy/dummy-runner.vala
@@ -5,11 +5,15 @@ private class Games.DummyRunner : Object, Runner {
get { return false; }
}
- public bool can_quit_safely {
- get { return true; }
+ public bool can_resume {
+ get { return false; }
}
- public bool can_resume {
+ public bool supports_savestates {
+ get { return false; }
+ }
+
+ public bool can_support_savestates {
get { return false; }
}
@@ -22,7 +26,7 @@ private class Games.DummyRunner : Object, Runner {
set { }
}
- public bool check_is_valid (out string error_message) throws Error {
+ public bool try_init_phase_one (out string error_message) {
error_message = "";
return true;
@@ -36,10 +40,26 @@ private class Games.DummyRunner : Object, Runner {
return null;
}
+ public void capture_current_state_pixbuf () {
+ }
+
+ public void preview_current_state () {
+ }
+
+ public void preview_savestate (Savestate savestate) {
+ }
+
+ public void load_previewed_savestate () {
+ }
+
+ public Savestate[] get_savestates () {
+ return {};
+ }
+
public void start () throws Error {
}
- public void resume () throws Error {
+ public void resume () {
}
public void pause () {
@@ -48,6 +68,13 @@ private class Games.DummyRunner : Object, Runner {
public void stop () {
}
+ public Savestate? try_create_savestate (bool is_automatic) {
+ return null;
+ }
+
+ public void delete_savestate (Savestate savestate) {
+ }
+
public InputMode[] get_available_input_modes () {
return { };
}
diff --git a/src/generic/generic-platform.vala b/src/generic/generic-platform.vala
index 09abf1c41958075baee1da9fe25864f90110f43f..96b0412b9596636753a3e41ceab237a3a0414a9f 100644
--- a/src/generic/generic-platform.vala
+++ b/src/generic/generic-platform.vala
@@ -3,10 +3,12 @@
public class Games.GenericPlatform : Object, Platform {
private string name;
private string id;
+ private string uid_prefix;
- public GenericPlatform (string id, string name) {
+ public GenericPlatform (string id, string name, string uid_prefix) {
this.id = id;
this.name = name;
+ this.uid_prefix = uid_prefix;
}
public string get_id () {
@@ -17,6 +19,10 @@ public class Games.GenericPlatform : Object, Platform {
return name;
}
+ public string get_uid_prefix () {
+ return uid_prefix;
+ }
+
public PreferencesPagePlatformsRow get_row () {
return new PreferencesPagePlatformsGenericRow (name);
}
diff --git a/src/meson.build b/src/meson.build
index 86c0a3a12efc48c18b04818367c1b0239b27a6ed..467fb9e4f9b3976e3aee88f005e4a460ad2d2461 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -30,6 +30,7 @@ vala_sources = [
'core/media-info.vala',
'core/media-set/media-set.vala',
'core/media-set/media-set-error.vala',
+ 'core/migrator.vala',
'core/platform.vala',
'core/platform-register.vala',
'core/players.vala',
@@ -41,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',
@@ -178,6 +180,9 @@ vala_sources = [
'ui/reset-controller-mapping-dialog.vala',
'ui/resume-dialog.vala',
'ui/resume-failed-dialog.vala',
+ 'ui/savestate-listbox-row.vala',
+ 'ui/savestates-list.vala',
+ 'ui/savestates-list-state.vala',
'ui/search-bar.vala',
'ui/shortcuts-window.vala',
'ui/ui-view.vala',
diff --git a/src/retro/retro-core-source.vala b/src/retro/retro-core-source.vala
index 7b75ecaa5415f618c1e842369af19c7272a01606..cc0ad4918fab7c3294c389e1e163112b73bc392c 100644
--- a/src/retro/retro-core-source.vala
+++ b/src/retro/retro-core-source.vala
@@ -13,6 +13,12 @@ public class Games.RetroCoreSource : Object {
return platform;
}
+ public string get_core_id () throws Error {
+ ensure_module_is_found ();
+
+ return core_descriptor.get_id ();
+ }
+
public string get_module_path () throws Error {
ensure_module_is_found ();
diff --git a/src/retro/retro-platform.vala b/src/retro/retro-platform.vala
index b5faf0f9a4bf79049d4d49d7e96a122a559afc47..a84838e4c882219e7a3b7946531f2199e173d40b 100644
--- a/src/retro/retro-platform.vala
+++ b/src/retro/retro-platform.vala
@@ -4,11 +4,13 @@ public class Games.RetroPlatform : Object, Platform {
private string name;
private string id;
private string[] mime_types;
+ private string prefix;
- public RetroPlatform (string id, string name, string[] mime_types) {
+ public RetroPlatform (string id, string name, string[] mime_types, string prefix) {
this.id = id;
this.name = name;
this.mime_types = mime_types;
+ this.prefix = prefix;
}
public string get_id () {
@@ -19,6 +21,10 @@ public class Games.RetroPlatform : Object, Platform {
return name;
}
+ public string get_uid_prefix () {
+ return prefix;
+ }
+
public string[] get_mime_types () {
return mime_types;
}
diff --git a/src/retro/retro-runner.vala b/src/retro/retro-runner.vala
index 8da75815dec79a17b94140432953c10b16ce2b97..d2803edd0d6f63eb047beba245b9ae3d4a446fe0 100644
--- a/src/retro/retro-runner.vala
+++ b/src/retro/retro-runner.vala
@@ -8,28 +8,16 @@ public class Games.RetroRunner : Object, Runner {
get { return true; }
}
- public bool can_quit_safely {
- get { return !should_save; }
- }
-
public bool can_resume {
- get {
- try {
- init ();
- if (!core.get_can_access_state ())
- return false;
-
- var snapshot_path = get_snapshot_path ();
- var file = File.new_for_path (snapshot_path);
+ get { return game_savestates.length != 0; }
+ }
- return file.query_exists ();
- }
- catch (Error e) {
- warning (e.message);
- }
+ public bool supports_savestates {
+ get { return core.get_can_access_state (); }
+ }
- return false;
- }
+ public bool can_support_savestates {
+ get { return true; }
}
private MediaSet _media_set;
@@ -50,11 +38,6 @@ public class Games.RetroRunner : Object, Runner {
}
}
- private string save_directory_path;
- private string save_path;
- private string snapshot_path;
- private string screenshot_path;
-
private Retro.CoreDescriptor core_descriptor;
private RetroCoreSource core_source;
private Platform platform;
@@ -63,22 +46,24 @@ public class Games.RetroRunner : Object, Runner {
private Settings settings;
private Title game_title;
+ private Savestate[] game_savestates;
+ private Savestate latest_savestate;
+ private Savestate tmp_live_savestate;
+ private Savestate previewed_savestate;
+
+ private Gdk.Pixbuf current_state_pixbuf;
+
private bool _running;
private bool running {
get { return _running; }
set {
_running = value;
-
- if (running)
- should_save = true;
-
view.sensitive = running;
}
}
private bool is_initialized;
private bool is_ready;
- private bool should_save;
public RetroRunnerBuilder builder {
construct {
@@ -98,7 +83,6 @@ public class Games.RetroRunner : Object, Runner {
construct {
is_initialized = false;
is_ready = false;
- should_save = false;
settings = new Settings ("org.gnome.Games");
}
@@ -108,10 +92,13 @@ public class Games.RetroRunner : Object, Runner {
deinit ();
}
- public bool check_is_valid (out string error_message) throws Error {
+ // init_phase_one attempts to init everything that can be init-ed right away
+ // It is called by the DisplayView to check if a runner can be used
+ // This method must be called before other methods/properties
+ public bool try_init_phase_one (out string error_message) {
try {
- load_media_data ();
- init ();
+ init_phase_one ();
+ // TODO: Check for the two RetroErrors using RetroCoreManager
}
catch (RetroError.MODULE_NOT_FOUND e) {
debug (e.message);
@@ -125,12 +112,48 @@ public class Games.RetroRunner : Object, Runner {
return false;
}
+ catch (Error e) {
+ debug (e.message);
+ error_message = e.message;
- error_message = "";
+ return false;
+ }
+ // Nothing went wrong
+ error_message = "";
return true;
}
+ private string get_core_id () throws Error {
+ if (core_descriptor != null)
+ return core_descriptor.get_id ();
+ else
+ return core_source.get_core_id ();
+ }
+
+ private void init_phase_one () throws Error {
+ // Step 1) Load the game's savestates ----------------------------------
+ game_savestates = Savestate.get_game_savestates (uid, get_core_id ());
+ if (game_savestates.length != 0)
+ latest_savestate = game_savestates[0];
+
+ // Step 2) Init the CoreView -------------------------------------------
+ // This is done here such that get_display() won't return null
+ view = new Retro.CoreView ();
+ settings.changed["video-filter"].connect (on_video_filter_changed);
+ on_video_filter_changed ();
+
+ // Step 3) Instantiate the core
+ // This is needed to check if the core supports savestates
+ tmp_live_savestate = Savestate.create_empty_in_tmp ();
+ instantiate_core (tmp_live_savestate.get_save_directory_path ());
+
+ // Step 4) Preview the latest savestate --------------------------------
+ if (latest_savestate != null)
+ preview_savestate (latest_savestate);
+
+ }
+
public Gtk.Widget get_display () {
return view;
}
@@ -139,50 +162,97 @@ public class Games.RetroRunner : Object, Runner {
return null;
}
- public void start () throws Error {
- load_media_data ();
+ public void capture_current_state_pixbuf () {
+ current_state_pixbuf = view.get_pixbuf ();
+ }
- if (!is_initialized)
- init ();
+ public void preview_current_state () {
+ view.set_pixbuf (current_state_pixbuf);
+ }
- loop.stop ();
+ public void preview_savestate (Savestate savestate) {
+ previewed_savestate = savestate;
- if (!is_ready) {
- load_ram ();
- is_ready = true;
+ var screenshot_path = savestate.get_screenshot_path ();
+ Gdk.Pixbuf pixbuf = null;
+
+ // Treat errors locally because loading the savestate screenshot is not
+ // a critical operation
+ try {
+ pixbuf = new Gdk.Pixbuf.from_file (screenshot_path);
+ }
+ catch (Error e) {
+ warning ("Couldn't load %s: %s", screenshot_path, e.message);
}
- core.reset ();
+
+ view.set_pixbuf (pixbuf);
+ }
+
+ public void load_previewed_savestate () throws Error {
+ loop.stop ();
+
+ tmp_live_savestate = previewed_savestate.clone_in_tmp ();
+ core.save_directory = tmp_live_savestate.get_save_directory_path ();
+ load_save_ram (previewed_savestate.get_save_ram_path ());
+ core.set_state (previewed_savestate.get_snapshot_data ());
+
+ if (previewed_savestate.has_media_data ())
+ media_set.selected_media_number = previewed_savestate.get_media_data ();
loop.start ();
+
+ is_ready = true;
running = true;
}
- public void resume () throws Error {
- if (!is_initialized)
- init ();
+ public Savestate[] get_savestates () {
+ if (game_savestates == null) {
+ critical ("RetroRunner hasn't loaded savestates. Call try_init_phase_one()");
+ }
- loop.stop ();
+ return game_savestates;
+ }
+
+ public void start () throws Error {
+ if (latest_savestate != null && latest_savestate.has_media_data ())
+ media_set.selected_media_number = latest_savestate.get_media_data ();
+
+ if (!is_initialized) {
+ if (latest_savestate != null)
+ tmp_live_savestate = latest_savestate.clone_in_tmp ();
+ else
+ tmp_live_savestate = Savestate.create_empty_in_tmp ();
+
+ instantiate_core (tmp_live_savestate.get_save_directory_path ());
+ }
if (!is_ready) {
- load_ram ();
- core.reset ();
- load_snapshot ();
+ if (latest_savestate != null)
+ load_save_ram (latest_savestate.get_save_ram_path ());
+
is_ready = true;
}
+ core.reset ();
loop.start ();
running = true;
}
- private void init () throws Error {
- if (is_initialized)
+ public void resume () {
+ if (!is_ready) {
+ critical ("RetroRunner.resume() cannot be called if the game isn't playing");
return;
+ }
- view = new Retro.CoreView ();
- settings.changed["video-filter"].connect (on_video_filter_changed);
- on_video_filter_changed ();
+ // Unpause an already running game
+ loop.start ();
+ running = true;
+ }
- prepare_core ();
+ // instantiate_core is used to setup the core, which needs to have a savestate
+ // in /tmp created and ready
+ private void instantiate_core (string core_save_directory_path) throws Error {
+ prepare_core (core_save_directory_path);
var present_analog_sticks = input_capabilities == null || input_capabilities.get_allow_analog_gamepads ();
input_manager = new RetroInputManager (core, view, present_analog_sticks);
@@ -196,8 +266,6 @@ public class Games.RetroRunner : Object, Runner {
loop = new Retro.MainLoop (core);
running = false;
- load_screenshot ();
-
is_initialized = true;
}
@@ -210,15 +278,19 @@ public class Games.RetroRunner : Object, Runner {
game_deinit ();
core = null;
- view.set_core (null);
- view = null;
+ //view.set_core (null);
+
+ // FIXME: Not sure if requires fixing but:
+ // This is commented out because otherwise the screen appears freezed
+ // when loading a savestate
+ //view = null;
+
input_manager = null;
loop = null;
_running = false;
is_initialized = false;
is_ready = false;
- should_save = false;
}
private void on_video_filter_changed () {
@@ -227,7 +299,7 @@ public class Games.RetroRunner : Object, Runner {
view.set_filter (filter);
}
- private void prepare_core () throws Error {
+ private void prepare_core (string save_directory_path) throws Error {
string module_path;
if (core_descriptor != null) {
var module_file = core_descriptor.get_module_file ();
@@ -257,9 +329,7 @@ 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;
+ core.save_directory = save_directory_path;
core.log.connect (Retro.g_log);
view.set_core (core);
@@ -280,15 +350,12 @@ public class Games.RetroRunner : Object, Runner {
return;
loop.stop ();
- running = false;
+ //FIXME:
+ // In the future here there will be code which updates the currently
+ // used temporary savestate
- try {
- save ();
- }
- catch (Error e) {
- warning (e.message);
- }
+ running = false;
}
public void stop () {
@@ -297,7 +364,6 @@ public class Games.RetroRunner : Object, Runner {
pause ();
deinit ();
-
stopped ();
}
@@ -356,31 +422,102 @@ public class Games.RetroRunner : Object, Runner {
return;
}
+ }
+
+ private string get_game_savestates_dir_path () throws Error {
+ // 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 ();
+ var core_id = get_core_id ();
+ var core_id_prefix = core_id.replace (".libretro", "");
+
+ return Path.build_filename (savestates_dir_path, uid + "-" + core_id_prefix);
+ }
+
+ // Returns the created Savestate or null if the Savestate couldn't be created
+ // Currently the callers are the DisplayView and the SavestatesList
+ // In the future we might want to throw Errors from here in case there is
+ // something that can be done, but right now there's nothing we can do if
+ // savestate creation fails except warn the user of unsaved progress via the
+ // QuitDialog in the DisplayView
+ public Savestate? try_create_savestate (bool is_automatic) {
+ if (!core.get_can_access_state ()) // Check if the core can support savestates
+ return null;
try {
- save_media_data ();
+ return create_savestate (is_automatic);
}
catch (Error e) {
- warning (e.message);
+ critical ("RetroRunner failed to create savestate: %s", e.message);
+
+ return null;
}
}
- private void save () throws Error {
- if (!should_save)
- return;
+ private Savestate create_savestate (bool is_automatic) throws Error {
+ // Decide if there are too many automatic savestates and delete the
+ // first one if so
+ var nr_automatic_savestates = count_automatic_savestates ();
+ if (is_automatic) {
+ var max_nr_automatic_savestates = 5;
+
+ if (nr_automatic_savestates >= max_nr_automatic_savestates)
+ delete_last_automatic_savestate ();
+ }
- save_ram ();
+ // Populate the savestate in tmp with data from the current state of the game
+ store_save_ram_in_tmp ();
if (media_set.get_size () > 1)
- save_media_data ();
+ tmp_live_savestate.set_media_data (media_set);
- if (!core.get_can_access_state ())
- return;
+ tmp_live_savestate.set_snapshot_data (core.get_state ());
+ save_screenshot_in_tmp ();
+
+ // Populate the metadata file
+ var now_time = new DateTime.now ();
+ var platform_prefix = platform.get_uid_prefix ();
+ if (is_automatic)
+ tmp_live_savestate.set_metadata_automatic (now_time, platform_prefix, get_core_id ());
+ else {
+ var nr_manual_savestates = game_savestates.length - nr_automatic_savestates;
+ var savestate_name = _("New savestate %d").printf (nr_manual_savestates + 1);
+
+ tmp_live_savestate.set_metadata_manual (savestate_name, now_time, platform_prefix, get_core_id ());
+ }
+
+ // Save the tmp_live_savestate into the game savestates directory
+ var game_savestates_dir_path = get_game_savestates_dir_path ();
+ tmp_live_savestate.save_in (game_savestates_dir_path);
+
+ // Instantiate the Savestate object
+ var savestate_path = Path.build_filename (game_savestates_dir_path, now_time.to_string ());
+ Savestate savestate = new Savestate (savestate_path);
+
+ // Update the game_savestates array
+ // Insert the new savestate at the beginning of the array since it's the latest savestate
+ Savestate[] new_game_savestates = {};
+
+ new_game_savestates += savestate;
+ foreach (var existing_savestate in game_savestates)
+ new_game_savestates += existing_savestate;
- save_snapshot ();
- save_screenshot ();
+ game_savestates = new_game_savestates;
- should_save = false;
+ return savestate;
+ }
+
+ public void delete_savestate (Savestate savestate) {
+ Savestate[] new_game_savestates = {};
+
+ foreach (var existing_savestate in game_savestates) {
+ if (savestate != existing_savestate)
+ new_game_savestates += existing_savestate;
+ }
+
+ game_savestates = new_game_savestates;
+ savestate.delete_from_disk ();
}
private string get_options_path () throws Error {
@@ -395,50 +532,21 @@ 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;
- }
-
- 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 () throws Error{
+ private void store_save_ram_in_tmp () throws Error {
var bytes = core.get_memory (Retro.MemoryType.SAVE_RAM);
var save = bytes.get_data ();
if (save.length == 0)
return;
- var dir = Application.get_saves_dir ();
- Application.try_make_dir (dir);
-
- var save_path = get_save_path ();
-
- FileUtils.set_data (save_path, save);
+ tmp_live_savestate.set_save_ram_data (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)
@@ -448,96 +556,12 @@ public class Games.RetroRunner : Object, Runner {
core.set_memory (Retro.MemoryType.SAVE_RAM, bytes);
}
- private string get_snapshot_path () throws Error {
- if (snapshot_path != null)
- return snapshot_path;
-
- var dir = Application.get_snapshots_dir ();
- var uid = uid.get_uid ();
- snapshot_path = @"$dir/$uid.snapshot";
-
- return snapshot_path;
- }
-
- private void save_snapshot () throws Error {
- var bytes = core.get_state ();
- var buffer = bytes.get_data ();
-
- var dir = Application.get_snapshots_dir ();
- Application.try_make_dir (dir);
-
- var snapshot_path = get_snapshot_path ();
-
- FileUtils.set_data (snapshot_path, buffer);
- }
-
- private void load_snapshot () throws Error {
- if (!core.get_can_access_state ())
- return;
-
- var snapshot_path = get_snapshot_path ();
-
- 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 () throws Error {
- var dir = Application.get_medias_dir ();
- Application.try_make_dir (dir);
-
- var medias_path = get_medias_path ();
-
- string contents = media_set.selected_media_number.to_string ();
-
- FileUtils.set_contents (medias_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;
- }
-
- private string get_medias_path () throws Error {
- var dir = Application.get_medias_dir ();
- var uid = uid.get_uid ();
-
- return @"$dir/$uid.media";
- }
-
- 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 ();
- screenshot_path = @"$dir/$uid.png";
-
- return screenshot_path;
- }
-
- private void save_screenshot () throws Error {
- if (!core.get_can_access_state ())
- return;
-
- var pixbuf = view.get_pixbuf ();
+ private void save_screenshot_in_tmp () throws Error {
+ var pixbuf = current_state_pixbuf;
if (pixbuf == null)
return;
- var screenshot_path = get_screenshot_path ();
+ var screenshot_path = tmp_live_savestate.get_screenshot_path ();
var now = new GLib.DateTime.now_local ();
var creation_time = now.to_string ();
@@ -566,19 +590,6 @@ public class Games.RetroRunner : Object, Runner {
null);
}
- private void load_screenshot () throws Error {
- if (!core.get_can_access_state ())
- return;
-
- var screenshot_path = get_screenshot_path ();
-
- if (!FileUtils.test (screenshot_path, FileTest.EXISTS))
- return;
-
- var pixbuf = new Gdk.Pixbuf.from_file (screenshot_path);
- view.set_pixbuf (pixbuf);
- }
-
private bool on_shutdown () {
stop ();
@@ -596,4 +607,26 @@ public class Games.RetroRunner : Object, Runner {
public Retro.Core get_core () {
return core;
}
+
+ private int count_automatic_savestates () {
+ int counter = 0;
+
+ foreach (var savestate in game_savestates) {
+ if (savestate.is_automatic ())
+ counter++;
+ }
+
+ return counter;
+ }
+
+ private void delete_last_automatic_savestate () {
+ // Delete the last automatic savestate (assume they are sorted
+ // by creation date for now)
+ Savestate last_automatic_savestate = null;
+
+ foreach (var savestate in game_savestates)
+ if (savestate.is_automatic ())
+ last_automatic_savestate = savestate;
+ }
}
+
diff --git a/src/ui/application.vala b/src/ui/application.vala
index 747eb119d1b145a116b426fb15b9589c9eee4905..3795364de95fb39867de6cce610ceb67e702e5ce 100644
--- a/src/ui/application.vala
+++ b/src/ui/application.vala
@@ -86,18 +86,6 @@ public class Games.Application : Gtk.Application {
return @"$data_dir/gnome-games";
}
- public static string get_saves_dir () {
- var data_dir = get_data_dir ();
-
- return @"$data_dir/saves";
- }
-
- public static string get_snapshots_dir () {
- var data_dir = get_data_dir ();
-
- return @"$data_dir/snapshots";
- }
-
public static string get_database_path () {
var data_dir = get_data_dir ();
@@ -145,12 +133,6 @@ public class Games.Application : Gtk.Application {
}
}
- public static string get_medias_dir () {
- var data_dir = get_data_dir ();
-
- return @"$data_dir/medias";
- }
-
public static bool is_running_in_flatpak () {
if (is_flatpak != null)
return is_flatpak;
@@ -290,7 +272,7 @@ public class Games.Application : Gtk.Application {
}
var platform_name = simple_type.get_platform_name ();
- var platform = new RetroPlatform (simple_type.platform, platform_name, { simple_type.mime_type });
+ var platform = new RetroPlatform (simple_type.platform, platform_name, { simple_type.mime_type }, simple_type.prefix);
platform_register.add_platform (platform);
var game_uri_adapter = new RetroSimpleGameUriAdapter (simple_type, platform);
@@ -329,6 +311,11 @@ public class Games.Application : Gtk.Application {
debug ("Error: %s", e.message);
}
}
+
+ // Re-organize data_dir layout if necessary
+ // This operation has to be executed _after_ the PlatformsRegister has
+ // been populated and therefore this call is placed here
+ Migrator.apply_migration_if_necessary ();
}
private async Game? game_for_uris (Uri[] uris) {
@@ -458,6 +445,6 @@ public class Games.Application : Gtk.Application {
var data_dir = File.new_for_path (Application.get_data_dir ());
string[] database = { Application.get_database_path () };
- FileOperations.compress_dir (file_path, data_dir, data_dir, database);
+ FileOperations.compress_dir (file_path, data_dir, database);
}
}
diff --git a/src/ui/display-box.vala b/src/ui/display-box.vala
index 1162066c0510cbfdc5144cfd4f4f7e724432dcc8..f268d8ea5a8d760faf4fbb780c2ac646ce3857fa 100644
--- a/src/ui/display-box.vala
+++ b/src/ui/display-box.vala
@@ -10,11 +10,21 @@ private class Games.DisplayBox : Gtk.Bin {
get { return fullscreen_header_bar; }
}
+ public SavestatesListState savestates_list_state {
+ get { return savestates_list.state; }
+ set {
+ value.notify["is-revealed"].connect (on_savestates_list_state_changed);
+
+ savestates_list.state = value;
+ fullscreen_header_bar.savestates_list_state = value;
+ }
+ }
+
private Runner _runner;
public Runner runner {
get { return _runner; }
set {
- stack.visible_child = display_bin;
+ stack.visible_child = display_box;
_runner = value;
remove_display ();
@@ -25,6 +35,8 @@ private class Games.DisplayBox : Gtk.Bin {
var display = runner.get_display ();
set_display (display);
+
+ savestates_list.runner = value;
}
}
@@ -35,11 +47,15 @@ private class Games.DisplayBox : Gtk.Bin {
[GtkChild]
private ErrorDisplay error_display;
[GtkChild]
+ private Gtk.Box display_box;
+ [GtkChild]
private Gtk.EventBox display_bin;
[GtkChild]
private DisplayHeaderBar fullscreen_header_bar;
- private Binding fullscreen_binding;
+ [GtkChild]
+ private SavestatesList savestates_list;
+ private Binding fullscreen_binding;
private long timeout_id;
construct {
@@ -49,6 +65,10 @@ private class Games.DisplayBox : Gtk.Bin {
timeout_id = -1;
}
+ public DisplayBox (SavestatesListState savestates_list_state) {
+ Object (savestates_list_state: savestates_list_state);
+ }
+
public void display_running_game_failed (Game game, string error_message) {
stack.visible_child = error_display;
error_display.running_game_failed (game, error_message);
@@ -94,4 +114,8 @@ private class Games.DisplayBox : Gtk.Bin {
return runner.gamepad_button_press_event (button);
}
+
+ public void on_savestates_list_state_changed () {
+ fullscreen_box.autohide = !savestates_list.state.is_revealed;
+ }
}
diff --git a/src/ui/display-header-bar.vala b/src/ui/display-header-bar.vala
index 7e572fc436483264c0f3c409eb850d45f42c7c2b..9d1e9939b0bec9bbb303308d5bb35e541a6b4230 100644
--- a/src/ui/display-header-bar.vala
+++ b/src/ui/display-header-bar.vala
@@ -1,18 +1,32 @@
// This file is part of GNOME Games. License: GPL-3.0+.
[GtkTemplate (ui = "/org/gnome/Games/ui/display-header-bar.ui")]
-private class Games.DisplayHeaderBar : Gtk.Bin {
+private class Games.DisplayHeaderBar : Gtk.Stack {
public signal void back ();
[GtkChild]
private MediaMenuButton media_button;
+ private SavestatesListState _savestates_list_state;
+ public SavestatesListState savestates_list_state {
+ get { return _savestates_list_state; }
+ set {
+ _savestates_list_state = value;
+
+ if (value != null)
+ value.notify["is-revealed"].connect (on_savestates_list_state_changed);
+ }
+ }
+
public string game_title {
- set { header_bar.title = value; }
+ set {
+ ingame_header_bar.title = value;
+ savestates_header_bar.title = value;
+ }
}
public bool show_title_buttons {
- set { header_bar.show_close_button = value; }
+ set { ingame_header_bar.show_close_button = value; }
}
public bool can_fullscreen { get; set; }
@@ -31,8 +45,12 @@ private class Games.DisplayHeaderBar : Gtk.Bin {
_runner = value;
input_mode_switcher.runner = value;
- if (runner != null)
+ if (runner != null) {
extra_widget = runner.get_extra_widget ();
+
+ secondary_menu_button.sensitive = runner.supports_savestates;
+ secondary_menu_button.visible = runner.can_support_savestates;
+ }
else
extra_widget = null;
}
@@ -46,28 +64,40 @@ private class Games.DisplayHeaderBar : Gtk.Bin {
return;
if (extra_widget != null)
- header_bar.remove (extra_widget);
+ ingame_header_bar.remove (extra_widget);
_extra_widget = value;
if (extra_widget != null)
- header_bar.pack_end (extra_widget);
+ ingame_header_bar.pack_end (extra_widget);
}
}
[GtkChild]
- private Gtk.HeaderBar header_bar;
+ private Gtk.HeaderBar ingame_header_bar;
[GtkChild]
private Gtk.Button fullscreen;
[GtkChild]
private Gtk.Button restore;
+ [GtkChild]
+ private Gtk.MenuButton secondary_menu_button;
+ [GtkChild]
+ private Gtk.HeaderBar savestates_header_bar;
private Settings settings;
+ public DisplayHeaderBar (SavestatesListState savestates_list_state) {
+ Object (savestates_list_state: savestates_list_state);
+ }
+
construct {
settings = new Settings ("org.gnome.Games");
}
+ public void hide_secondary_menu_button () {
+ secondary_menu_button.visible = false;
+ }
+
[GtkCallback]
private void on_fullscreen_changed () {
fullscreen.visible = can_fullscreen && !is_fullscreen;
@@ -90,4 +120,31 @@ private class Games.DisplayHeaderBar : Gtk.Bin {
is_fullscreen = false;
settings.set_boolean ("fullscreen", false);
}
+
+ [GtkCallback]
+ private void on_secondary_menu_savestates_clicked () {
+ savestates_list_state.is_revealed = true;
+ }
+
+ [GtkCallback]
+ private void on_savestates_load_clicked () {
+ savestates_list_state.load_clicked ();
+ }
+
+ [GtkCallback]
+ private void on_savestates_delete_clicked () {
+ savestates_list_state.delete_clicked ();
+ }
+
+ [GtkCallback]
+ private void on_savestates_cancel_clicked () {
+ savestates_list_state.is_revealed = false;
+ }
+
+ private void on_savestates_list_state_changed () {
+ if (savestates_list_state.is_revealed)
+ set_visible_child (savestates_header_bar);
+ else
+ set_visible_child (ingame_header_bar);
+ }
}
diff --git a/src/ui/display-view.vala b/src/ui/display-view.vala
index d4859feba4aa95ac82f46ba2721233991a7a54d1..8b542cfa38da5839df3e9e3083d44cfd07eb2ceb 100644
--- a/src/ui/display-view.vala
+++ b/src/ui/display-view.vala
@@ -52,6 +52,8 @@ private class Games.DisplayView : Object, UiView {
private ResumeFailedDialog resume_failed_dialog;
private QuitDialog quit_dialog;
+ private SavestatesListState savestates_list_state;
+
private long focus_out_timeout_id;
public DisplayView (Gtk.Window window) {
@@ -59,14 +61,16 @@ private class Games.DisplayView : Object, UiView {
}
construct {
- box = new DisplayBox ();
- header_bar = new DisplayHeaderBar ();
+ savestates_list_state = new SavestatesListState ();
+ box = new DisplayBox (savestates_list_state);
+ header_bar = new DisplayHeaderBar (savestates_list_state);
box.back.connect (on_display_back);
header_bar.back.connect (on_display_back);
settings = new Settings ("org.gnome.Games");
+ // Bind the is_fullscreen property between the header_bar and the box
box_fullscreen_binding = bind_property ("is-fullscreen", box, "is-fullscreen",
BindingFlags.BIDIRECTIONAL);
header_bar_fullscreen_binding = bind_property ("is-fullscreen", header_bar,
@@ -94,14 +98,15 @@ private class Games.DisplayView : Object, UiView {
if ((event.keyval == Gdk.Key.f || event.keyval == Gdk.Key.F) &&
(event.state & default_modifiers) == Gdk.ModifierType.CONTROL_MASK &&
- header_bar.can_fullscreen) {
+ header_bar.can_fullscreen && !savestates_list_state.is_revealed) {
is_fullscreen = !is_fullscreen;
settings.set_boolean ("fullscreen", is_fullscreen);
return true;
}
- if (event.keyval == Gdk.Key.F11 && header_bar.can_fullscreen) {
+ if (event.keyval == Gdk.Key.F11 && header_bar.can_fullscreen &&
+ !savestates_list_state.is_revealed) {
is_fullscreen = !is_fullscreen;
settings.set_boolean ("fullscreen", is_fullscreen);
@@ -148,7 +153,7 @@ private class Games.DisplayView : Object, UiView {
switch (button) {
case EventCode.BTN_MODE:
- back ();
+ on_display_back ();
return true;
default:
@@ -165,6 +170,9 @@ private class Games.DisplayView : Object, UiView {
}
private void on_display_back () {
+ if (savestates_list_state.is_revealed)
+ return;
+
back ();
}
@@ -228,7 +236,7 @@ private class Games.DisplayView : Object, UiView {
try {
var runner = game.get_runner ();
string error_message;
- if (runner.check_is_valid (out error_message))
+ if (runner.try_init_phase_one (out error_message))
return runner;
reset_display_page ();
@@ -269,10 +277,15 @@ private class Games.DisplayView : Object, UiView {
return (Gtk.ResponseType) response;
}
- private bool try_run_with_cancellable (Runner runner, bool resume, Cancellable cancellable) {
+ private bool try_run_with_cancellable (Runner runner, bool use_latest_savestate, Cancellable cancellable) {
try {
- if (resume)
- box.runner.resume ();
+ if (use_latest_savestate) {
+ var savestates = box.runner.get_savestates ();
+ var latest_savestate = savestates[0];
+
+ box.runner.preview_savestate (latest_savestate);
+ box.runner.load_previewed_savestate ();
+ }
else
runner.start ();
@@ -344,12 +357,23 @@ private class Games.DisplayView : Object, UiView {
box.runner.pause ();
- if (box.runner.can_quit_safely) {
+ if (!box.runner.can_support_savestates) {
+ // Game does not and will not support savestates (e.g. Steam games)
+ // => Progress cannot be saved so game can be quit safely
box.runner.stop ();
+ return true;
+ }
+ box.runner.capture_current_state_pixbuf ();
+
+ if (box.runner.try_create_savestate (true) != null) {
+ // Progress saved => can quit game safely
+ box.runner.stop ();
return true;
}
+ // Failed to save progress => warn the user of unsaved progress
+ // via the QuitDialog
if (quit_dialog != null)
return false;
@@ -381,17 +405,14 @@ private class Games.DisplayView : Object, UiView {
private bool cancel_quitting_game () {
if (box.runner != null)
- try {
- box.runner.resume ();
- }
- catch (Error e) {
- warning (e.message);
- }
+ box.runner.resume ();
return false;
}
private void reset_display_page () {
+ header_bar.hide_secondary_menu_button ();
+
header_bar.can_fullscreen = false;
box.header_bar.can_fullscreen = false;
header_bar.runner = null;
@@ -409,13 +430,10 @@ private class Games.DisplayView : Object, UiView {
if (!can_update_pause ())
return;
- if (window.is_active)
- try {
+ if (window.is_active) {
+ if (!savestates_list_state.is_revealed)
box.runner.resume ();
- }
- catch (Error e) {
- warning (e.message);
- }
+ }
else if (with_delay)
focus_out_timeout_id = Timeout.add (FOCUS_OUT_DELAY_MILLISECONDS, on_focus_out_delay_elapsed);
else
diff --git a/src/ui/fullscreen-box.vala b/src/ui/fullscreen-box.vala
index 17553d82642b6101b8b32f9aa107ccc2513d6030..dcb767df5338203deecc874fc40e72bb8460d6fe 100644
--- a/src/ui/fullscreen-box.vala
+++ b/src/ui/fullscreen-box.vala
@@ -7,6 +7,33 @@ private class Games.FullscreenBox : Gtk.EventBox, Gtk.Buildable {
public bool is_fullscreen { get; set; }
+ private bool _autohide;
+ public bool autohide {
+ get { return _autohide; }
+ set {
+ _autohide = value;
+
+ if (value) {
+ show_ui ();
+ on_cursor_moved ();
+ }
+ else {
+ // Disable timers
+ if (ui_timeout_id != -1) {
+ Source.remove (ui_timeout_id);
+ ui_timeout_id = -1;
+ }
+
+ if (cursor_timeout_id != -1) {
+ Source.remove (cursor_timeout_id);
+ cursor_timeout_id = -1;
+ }
+
+ show_cursor (true);
+ }
+ }
+ }
+
private Gtk.Widget _header_bar;
public Gtk.Widget header_bar {
get { return _header_bar; }
@@ -73,6 +100,9 @@ private class Games.FullscreenBox : Gtk.EventBox, Gtk.Buildable {
[GtkCallback]
private bool on_motion_event (Gdk.EventMotion event) {
+ if (!autohide)
+ return false;
+
if (event.y_root <= SHOW_HEADERBAR_DISTANCE)
show_ui ();
diff --git a/src/ui/savestate-listbox-row.vala b/src/ui/savestate-listbox-row.vala
new file mode 100644
index 0000000000000000000000000000000000000000..51b1cdb3ba7ea886845d4270555ce80c0966d5dd
--- /dev/null
+++ b/src/ui/savestate-listbox-row.vala
@@ -0,0 +1,73 @@
+// This file is part of GNOME Games. License: GPL-3.0+.
+
+[GtkTemplate (ui = "/org/gnome/Games/ui/savestate-listbox-row.ui")]
+private class Games.SavestateListBoxRow : Gtk.ListBoxRow {
+ [GtkChild]
+ private Gtk.Image image;
+ [GtkChild]
+ private Gtk.Label name_label;
+ [GtkChild]
+ private Gtk.Label date_label;
+
+ private Savestate _savestate;
+ public Savestate savestate {
+ get { return _savestate; }
+ set {
+ _savestate = value;
+
+ if (savestate.is_automatic ())
+ name_label.label = _("Autosave");
+ else {
+ name_label.label = savestate.get_name ();
+ }
+
+ var creation_date = savestate.get_creation_date ();
+
+ /* Translators: this is the day number followed
+ * by the abbreviated month name followed by the year followed
+ * by a time in 24h format i.e. "3 Feb 2015 23:04:00" */
+ /* xgettext:no-c-format */
+ var creation_date_str = creation_date.format (_("%-e %b %Y %X"));
+ date_label.label = creation_date_str;
+
+ // Load the savestate thumbnail
+ var screenshot_path = savestate.get_screenshot_path ();
+ var screenshot_width = 0;
+ var screenshot_height = 0;
+
+ Gdk.Pixbuf.get_file_info (screenshot_path, out screenshot_width, out screenshot_height);
+
+ var aspect_ratio = ((double) screenshot_width) / screenshot_height;
+
+ // Calculate the thumbnail width and height
+ const int thumbnail_max_width_height = 64;
+ var thumbnail_width = screenshot_width;
+ var thumbnail_height = screenshot_height;
+
+ if (screenshot_width > screenshot_height && screenshot_width != thumbnail_max_width_height) {
+ thumbnail_width = thumbnail_max_width_height;
+ thumbnail_height = (int) (thumbnail_max_width_height / aspect_ratio);
+ }
+
+ if (screenshot_height > screenshot_width && screenshot_height != thumbnail_max_width_height) {
+ thumbnail_height = thumbnail_max_width_height;
+ thumbnail_width = (int) (thumbnail_max_width_height * aspect_ratio);
+ }
+
+ Gdk.Pixbuf thumbnail = null;
+
+ try {
+ thumbnail = new Gdk.Pixbuf.from_file_at_scale (screenshot_path, thumbnail_width, thumbnail_height, false);
+ image.set_from_pixbuf (thumbnail);
+ }
+ catch (Error e) {
+ warning ("Failed to load savestate thumbnail: %s", e.message);
+ }
+ }
+ }
+
+ public SavestateListBoxRow (Savestate savestate) {
+ Object (savestate: savestate);
+ }
+}
+
diff --git a/src/ui/savestates-list-state.vala b/src/ui/savestates-list-state.vala
new file mode 100644
index 0000000000000000000000000000000000000000..0915d300f83c3b4b8e2b83a9a4c6a40606fde69d
--- /dev/null
+++ b/src/ui/savestates-list-state.vala
@@ -0,0 +1,6 @@
+private class Games.SavestatesListState : Object {
+ public signal void load_clicked ();
+ public signal void delete_clicked ();
+
+ public bool is_revealed { get; set; }
+}
diff --git a/src/ui/savestates-list.vala b/src/ui/savestates-list.vala
new file mode 100644
index 0000000000000000000000000000000000000000..22429fedcdf94f06c523f8b06c121dd1e30bddff
--- /dev/null
+++ b/src/ui/savestates-list.vala
@@ -0,0 +1,171 @@
+// This file is part of GNOME Games. License: GPL-3.0+.
+
+[GtkTemplate (ui = "/org/gnome/Games/ui/savestates-list.ui")]
+private class Games.SavestatesList : Gtk.Box {
+ [GtkChild]
+ private Gtk.Revealer revealer;
+ [GtkChild]
+ private Gtk.ListBox list_box;
+ [GtkChild]
+ private Gtk.ListBoxRow new_savestate_row;
+
+ public bool is_revealed {
+ get { return revealer.reveal_child; }
+ set { revealer.reveal_child = value; }
+ }
+
+ private SavestatesListState _state;
+ public SavestatesListState state {
+ get { return _state; }
+ set {
+ if (_state != null)
+ _state.notify["is-revealed"].disconnect (on_state_changed);
+
+ _state = value;
+
+ if (value != null) {
+ value.notify["is-revealed"].connect (on_state_changed);
+ value.load_clicked.connect (on_load_clicked);
+ value.delete_clicked.connect (on_delete_clicked);
+ }
+ }
+ }
+
+ private Runner _runner;
+ public Runner runner {
+ get { return _runner; }
+ set {
+ _runner = value;
+
+ // Remove current savestate rows
+ var list_rows = list_box.get_children ();
+ foreach (var row in list_rows) {
+ if (row != new_savestate_row)
+ list_box.remove (row);
+ }
+
+ if (value == null)
+ return;
+
+ // value != null
+ var savestates = _runner.get_savestates ();
+ foreach (var savestate in savestates) {
+ var list_row = new SavestateListBoxRow (savestate);
+
+ list_box.add (list_row);
+ }
+ }
+ }
+
+ construct {
+ list_box.set_header_func (update_header);
+ }
+
+ [GtkCallback]
+ private void on_row_activated (Gtk.ListBoxRow activated_row) {
+ if (activated_row == new_savestate_row) {
+ var savestate = runner.try_create_savestate (false);
+
+ if (savestate != null) {
+ var savestate_row = new SavestateListBoxRow (savestate);
+
+ list_box.insert (savestate_row, 1);
+ select_and_preview_row (savestate_row);
+ }
+ else {
+ // Savestate creation failed
+ list_box.select_row (list_box.get_row_at_index (1));
+
+ // TODO: Perhaps we should warn the user that the creation of
+ // the savestate failed via an in-app notification ?
+ }
+ } else {
+ var savestate_row = activated_row as SavestateListBoxRow;
+ var savestate = savestate_row.savestate;
+
+ runner.preview_savestate (savestate);
+ }
+ }
+
+ private void on_load_clicked () {
+ if (!try_runner_load_previewed_savestate ()) {
+ // TODO: Here we could show a dialog with one button like
+ // "Failed to load savestate [Ok]"
+ }
+
+ state.is_revealed = false;
+ }
+
+ private bool try_runner_load_previewed_savestate () {
+ try {
+ _runner.load_previewed_savestate ();
+ }
+ catch (Error e) {
+ critical ("Failed to load savestate: %s", e.message);
+
+ return false;
+ }
+
+ // Nothing went wrong
+ return true;
+ }
+
+ private void on_state_changed () {
+ revealer.reveal_child = state.is_revealed;
+
+ if (state.is_revealed) {
+ list_box.select_row (null);
+ runner.capture_current_state_pixbuf ();
+ runner.pause ();
+ }
+ else
+ runner.resume ();
+ }
+
+ private void on_delete_clicked () {
+ var selected_row = list_box.get_selected_row ();
+ var selected_row_index = selected_row.get_index ();
+ var savestate_row = selected_row as SavestateListBoxRow;
+ var savestate = savestate_row.savestate;
+
+ runner.delete_savestate (savestate);
+ list_box.remove (selected_row);
+
+ // Select and preview a new row
+ var next_row = list_box.get_row_at_index (selected_row_index);
+
+ if (next_row == null) { // The last row in the list has been deleted
+ var nr_rows = list_box.get_children ().length ();
+
+ if (nr_rows == 1) {
+ // The only remaining row in the list is the create savestate one
+ runner.preview_current_state ();
+ }
+ else {
+ // The last row of the list has been deleted but there are still
+ // rows remaining in the list
+ var last_row = list_box.get_row_at_index (selected_row_index - 1);
+ select_and_preview_row (last_row);
+ }
+
+ return;
+ }
+
+ select_and_preview_row (next_row);
+ }
+
+ private void update_header (Gtk.ListBoxRow row, Gtk.ListBoxRow? before) {
+ if (before != null && row.get_header () == null) {
+ var separator = new Gtk.Separator (Gtk.Orientation.HORIZONTAL);
+ row.set_header (separator);
+ }
+ }
+
+ private void select_and_preview_row (Gtk.ListBoxRow row) {
+ var savestate_row = row as SavestateListBoxRow;
+ var savestate = savestate_row.savestate;
+
+ list_box.select_row (row);
+ runner.preview_savestate (savestate);
+ }
+}
diff --git a/src/utils/file-operations.vala b/src/utils/file-operations.vala
index 6ee835805a8a27b7b769d41ed3eccc35dbafd3f6..41d92c84efcb0d780693e46d38516b4b47da61df 100644
--- a/src/utils/file-operations.vala
+++ b/src/utils/file-operations.vala
@@ -16,13 +16,13 @@ public errordomain Games.ExtractionError {
}
public class Games.FileOperations {
- public static void compress_dir (string name, File parent_dir, File export_data, string[]? exclude_files = null) throws CompressionError {
+ public static void compress_dir (string archive_path, File exported_data, string[]? exclude_files = null) throws CompressionError {
var archive = new Archive.Write ();
archive.add_filter_gzip ();
archive.set_format_pax_restricted ();
- archive.open_filename (name);
+ archive.open_filename (archive_path);
- backup_data (parent_dir, export_data, archive, exclude_files);
+ backup_data (exported_data, exported_data, archive, exclude_files);
if (archive.close () != Archive.Result.OK) {
var error_message = _("Error: %s (%d)").printf (archive.error_string (), archive.errno ());
throw new CompressionError.CLOSING_FAILED (error_message);
@@ -179,4 +179,43 @@ public class Games.FileOperations {
if (last_result != Archive.Result.EOF)
throw new ExtractionError.DIDNT_REACH_EOF ("%s (%d)", restore_archive.error_string (), restore_archive.errno ());
}
+
+ public static void copy_dir (File src, File dest) throws Error {
+ copy_recursively (src, dest, false);
+ }
+
+ public static void copy_contents (File src, File dest) throws Error {
+ copy_recursively (src, dest, true);
+ }
+
+ // If the merge_flag is set to true then the copy operation will behave
+ // similarly to how the file system does merging when copy & pasting
+ private static void copy_recursively (File src, File dest, bool merge_flag) throws Error {
+ var src_type = src.query_file_type (FileQueryInfoFlags.NONE);
+
+ if (src_type == FileType.DIRECTORY) {
+ if (!dest.query_exists () || !merge_flag) {
+ dest.make_directory ();
+ src.copy_attributes (dest, FileCopyFlags.NONE);
+ }
+
+ var src_path = src.get_path ();
+ var dest_path = dest.get_path ();
+ var enumerator = src.enumerate_children (FileAttribute.STANDARD_NAME, FileQueryInfoFlags.NONE);
+
+ for (var info = enumerator.next_file (); info != null; info = enumerator.next_file ()) {
+ // src_object is any file found in the src directory (could be
+ // a file or another directory)
+ var info_name = info.get_name ();
+ var src_object_path = Path.build_filename (src_path, info_name);
+ var src_object = File.new_for_path (src_object_path);
+ var dest_object_path = Path.build_filename (dest_path, info_name);
+ var dest_object = File.new_for_path (dest_object_path);
+
+ copy_recursively (src_object, dest_object, merge_flag);
+ }
+ }
+ else if (src_type == FileType.REGULAR)
+ src.copy (dest, FileCopyFlags.NONE);
+ }
}