diff --git a/src/app.vala b/src/app.vala index dd7a5ecc..1c3da098 100644 --- a/src/app.vala +++ b/src/app.vala @@ -145,15 +145,13 @@ namespace GameHub Providers.ImageProviders = { new Providers.Images.SteamGridDB(), new Providers.Images.JinxSGVI() }; Providers.DataProviders = { new Providers.Data.IGDB() }; - CompatTool[] tools = { new Compat.WineWrap(), new Compat.Innoextract(), new Compat.DOSBox(), new Compat.ScummVM() }; - foreach(var appid in Compat.Proton.APPIDS) - { - tools += new Compat.Proton(appid); - } + var proton_latest = new Compat.Proton(Compat.Proton.LATEST); - CompatTools = tools; + CompatTools = { new Compat.WineWrap(), new Compat.Innoextract(), new Compat.DOSBox(), new Compat.ScummVM(), proton_latest }; - tools += new Compat.Proton(Compat.Proton.LATEST); + Compat.Proton.find_proton_versions(); + + CompatTool[] tools = CompatTools; string[] wine_binaries = { "wine"/*, "wine64", "wine32"*/ }; string[] wine_arches = { "win64", "win32" }; @@ -173,6 +171,8 @@ namespace GameHub CompatTools = tools; + proton_latest.init(); + IconTheme.get_default().add_resource_path("/com/github/tkashkin/gamehub/icons"); screen = Screen.get_default(); diff --git a/src/data/compat/Proton.vala b/src/data/compat/Proton.vala index b6f2af10..bd77a56b 100644 --- a/src/data/compat/Proton.vala +++ b/src/data/compat/Proton.vala @@ -16,28 +16,31 @@ You should have received a copy of the GNU General Public License along with GameHub. If not, see . */ -using GameHub.Utils; +using Gee; using GameHub.Data.Sources.Steam; +using GameHub.Utils; namespace GameHub.Data.Compat { public class Proton: Wine { - public const string[] APPIDS = {"1054830", "996510", "961940", "930400", "858280"}; // 4.2, 3.16 Beta, 3.16, 3.7 Beta, 3.7 public const string LATEST = "latest"; - public string appid { get; construct; } + public string appid { get; construct set; } + public string? appname { get; construct set; } + + public bool is_latest { get; construct set; default = false; } - public Proton(string appid) + public Proton(string appid, string? appname=null) { - Object(appid: appid, binary: "", arch: ""); + Object(appid: appid, appname: appname, is_latest: appid == LATEST, binary: "", arch: ""); } construct { id = @"proton_$(appid)"; - name = "Proton"; + name = appname ?? "Proton"; icon = "source-steam-symbolic"; installed = false; @@ -64,8 +67,17 @@ namespace GameHub.Data.Compat new CompatTool.BoolOption("/NOGUI", _("No GUI"), false) }; - if(appid == Proton.LATEST) + if(!is_latest) + { + init(); + } + } + + public void init() + { + if(is_latest) { + name = "Proton (latest)"; foreach(var tool in CompatTools) { if(tool is Proton) @@ -74,7 +86,6 @@ namespace GameHub.Data.Compat if(proton.installed) { appid = proton.appid; - name = "Proton (latest)"; executable = proton.executable; installed = true; wine_binary = proton.wine_binary; @@ -90,12 +101,16 @@ namespace GameHub.Data.Compat { if(proton_dir != null) { - name = proton_dir.get_basename(); + name = appname ?? proton_dir.get_basename(); executable = proton_dir.get_child("proton"); installed = executable.query_exists(); wine_binary = proton_dir.get_child("dist/bin/wine"); } } + else + { + name = appname ?? "Proton"; + } } if(installed) @@ -228,5 +243,60 @@ namespace GameHub.Data.Compat yield Utils.run_thread({ executable.get_path(), "run", cmd.get_path() }, runnable.install_dir.get_path(), prepare_env(runnable), false, true); } } + + public void install_app() + { + if(!is_latest && !installed) + { + Steam.install_app(appid); + } + } + + public static void find_proton_versions() + { + if(Steam.instance == null) return; + + Steam.instance.load_appinfo(); + + if(Steam.instance.appinfo == null) return; + + ArrayList versions = new ArrayList(); + + foreach(var app_node in Steam.instance.appinfo.nodes.values) + { + if(app_node != null && app_node is BinaryVDF.ListNode) + { + var app = (BinaryVDF.ListNode) app_node; + var common = (BinaryVDF.ListNode) app.get_nested({"appinfo", "common"}); + + if(common != null) + { + var name = ((BinaryVDF.StringNode) common.get("name")).value; + var type = ((BinaryVDF.StringNode) common.get("type")).value; + + if(type.down() == "tool" && name.down().has_prefix("proton ")) + { + versions.add(new Proton(app.key, name)); + } + } + } + } + + if(versions.size > 0) + { + versions.sort((first, second) => { + return int.parse(second.appid) - int.parse(first.appid); + }); + + CompatTool[] tools = CompatTools; + + foreach(var proton in versions) + { + tools += proton; + } + + CompatTools = tools; + } + } } } diff --git a/src/data/sources/steam/Steam.vala b/src/data/sources/steam/Steam.vala index 2e806ae6..26a2013a 100644 --- a/src/data/sources/steam/Steam.vala +++ b/src/data/sources/steam/Steam.vala @@ -60,8 +60,8 @@ namespace GameHub.Data.Sources.Steam private bool? installed = null; - private BinaryVDF.ListNode? appinfo; - private BinaryVDF.ListNode? packageinfo; + public BinaryVDF.ListNode? appinfo; + public BinaryVDF.ListNode? packageinfo; public bool is_authenticated_in_steam_client { @@ -200,6 +200,18 @@ namespace GameHub.Data.Sources.Steam return Settings.Auth.Steam.instance.authenticated && is_authenticated_in_steam_client; } + public void load_appinfo() + { + if(appinfo == null) + { + appinfo = new AppInfoVDF(FSUtils.find_case_insensitive(FSUtils.file(FSUtils.Paths.Steam.Home), FSUtils.Paths.Steam.AppInfoVDF)).read(); + } + if(packageinfo == null) + { + packageinfo = new PackageInfoVDF(FSUtils.find_case_insensitive(FSUtils.file(FSUtils.Paths.Steam.Home), FSUtils.Paths.Steam.PackageInfoVDF)).read(); + } + } + private ArrayList _games = new ArrayList(Game.is_equal); public override ArrayList games { get { return _games; } } @@ -216,8 +228,7 @@ namespace GameHub.Data.Sources.Steam Utils.thread("SteamLoading", () => { _games.clear(); - appinfo = new AppInfoVDF(FSUtils.find_case_insensitive(FSUtils.file(FSUtils.Paths.Steam.Home), FSUtils.Paths.Steam.AppInfoVDF)).read(); - packageinfo = new PackageInfoVDF(FSUtils.find_case_insensitive(FSUtils.file(FSUtils.Paths.Steam.Home), FSUtils.Paths.Steam.PackageInfoVDF)).read(); + load_appinfo(); var cached = Tables.Games.get_all(this); games_count = 0; @@ -313,6 +324,11 @@ namespace GameHub.Data.Sources.Steam return pkgs; } + public static void install_app(string appid) + { + Utils.open_uri(@"steam://install/$(appid)"); + } + public static void install_multiple_apps(string[] appids) { if(instance.packageinfo == null) return; diff --git a/src/data/sources/steam/SteamGame.vala b/src/data/sources/steam/SteamGame.vala index 375f033f..485fd2ad 100644 --- a/src/data/sources/steam/SteamGame.vala +++ b/src/data/sources/steam/SteamGame.vala @@ -250,7 +250,7 @@ namespace GameHub.Data.Sources.Steam public override async void install(Runnable.Installer.InstallMode install_mode=Runnable.Installer.InstallMode.INTERACTIVE) { - Utils.open_uri(@"steam://install/$(id)"); + Steam.install_app(id); update_status(); } diff --git a/src/ui/dialogs/SettingsDialog/pages/sources/Steam.vala b/src/ui/dialogs/SettingsDialog/pages/sources/Steam.vala index f87c0ccc..03e51ba8 100644 --- a/src/ui/dialogs/SettingsDialog/pages/sources/Steam.vala +++ b/src/ui/dialogs/SettingsDialog/pages/sources/Steam.vala @@ -18,6 +18,9 @@ along with GameHub. If not, see . using Gtk; +using GameHub.Data; +using GameHub.Data.Compat; + using GameHub.Utils; namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources @@ -26,6 +29,8 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources { private Settings.Auth.Steam steam_auth; + private ListBox proton; + public Steam(SettingsDialog dlg) { Object( @@ -41,15 +46,34 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources construct { + root_grid.margin = 0; + header_grid.margin = 12; + header_grid.margin_bottom = 0; + content_area.margin = 0; + var paths = FSUtils.Paths.Settings.instance; steam_auth = Settings.Auth.Steam.instance; add_steam_apikey_entry(); - add_labeled_link(_("Steam API keys have limited number of uses per day"), _("Generate key"), "steam://openurl/https://steamcommunity.com/dev/apikey"); + adjust_margins(add_labeled_link(_("Steam API keys have limited number of uses per day"), _("Generate key"), "steam://openurl/https://steamcommunity.com/dev/apikey")); + + adjust_margins(add_separator()); + + adjust_margins(add_file_chooser(_("Installation directory"), FileChooserAction.SELECT_FOLDER, paths.steam_home, v => { paths.steam_home = v; request_restart(); }, false)); - add_separator(); - add_file_chooser(_("Installation directory"), FileChooserAction.SELECT_FOLDER, paths.steam_home, v => { paths.steam_home = v; request_restart(); }, false); + var proton_header = add_header("Proton"); + proton_header.margin_start = proton_header.margin_end = 12; + + proton = add_widget(new ListBox()); + proton.selection_mode = SelectionMode.NONE; + proton.get_style_context().add_class(Gtk.STYLE_CLASS_FRAME); + proton.expand = true; + + proton.margin_start = 7; + proton.margin_end = 3; + proton.margin_top = 0; + proton.margin_bottom = 6; status_switch.active = steam_auth.enabled; status_switch.notify["active"].connect(() => { @@ -83,9 +107,97 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources { status = description = steam.user_name != null ? _("Authenticated as %s").printf(steam.user_name) : _("Authenticated"); } + + proton.foreach(r => { + if(r != null) r.destroy(); + }); + + foreach(var tool in CompatTools) + { + if(tool is Proton) + { + var p = tool as Proton; + if(p != null && !p.is_latest) + { + proton.add(new ProtonRow(p, this)); + } + } + } + + proton.show_all(); } - protected void add_steam_apikey_entry() + private class ProtonRow: ListBoxRow + { + public Proton proton { get; construct; } + + public Steam page { private get; construct; } + + public ProtonRow(Proton proton, Steam page) + { + Object(proton: proton, page: page); + } + + construct + { + var grid = new Grid(); + grid.column_spacing = 12; + grid.margin_start = grid.margin_end = 8; + grid.margin_top = grid.margin_bottom = 4; + + var icon = new Image.from_icon_name("source-steam-symbolic", IconSize.LARGE_TOOLBAR); + icon.valign = Align.CENTER; + + var name = new Label(proton.name); + name.get_style_context().add_class("category-label"); + name.set_size_request(96, -1); + name.hexpand = false; + name.xalign = 0; + name.valign = Align.CENTER; + + var appid = new Label(proton.appid); + appid.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + appid.hexpand = true; + appid.xalign = 0; + appid.valign = Align.CENTER; + + var status = new Label(_("Not installed")); + status.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + status.hexpand = true; + status.ellipsize = Pango.EllipsizeMode.MIDDLE; + status.xalign = 0; + status.valign = Align.CENTER; + + var install = new Button.with_label(_("Install")); + install.valign = Align.CENTER; + install.sensitive = false; + + grid.attach(icon, 0, 0, 1, 2); + grid.attach(name, 1, 0); + grid.attach(appid, 2, 0); + grid.attach(status, 1, 1, 2, 1); + + if(proton.installed && proton.executable != null) + { + status.label = status.tooltip_text = proton.executable.get_path(); + } + else + { + install.sensitive = true; + grid.attach(install, 3, 0, 1, 2); + + install.clicked.connect(() => { + install.sensitive = false; + page.request_restart(); + proton.install_app(); + }); + } + + child = grid; + } + } + + protected Box add_steam_apikey_entry() { var steam_auth = Settings.Auth.Steam.instance; @@ -117,6 +229,16 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources hbox.add(label); hbox.add(entry); add_widget(hbox); + + adjust_margins(hbox); + + return hbox; + } + + private void adjust_margins(Widget w) + { + w.margin_start = 16; + w.margin_end = 12; } } } diff --git a/src/utils/BinaryVDF.vala b/src/utils/BinaryVDF.vala index b10366ee..4bc885e3 100644 --- a/src/utils/BinaryVDF.vala +++ b/src/utils/BinaryVDF.vala @@ -158,9 +158,10 @@ namespace GameHub.Utils public Node? get_nested(string[] keys, Node? def=null) { ListNode? list = this; + Node? node = null; foreach(var key in keys) { - var node = list.get(key); + node = list.get(key); if(node != null && node is ListNode) { list = (ListNode) node; @@ -170,7 +171,7 @@ namespace GameHub.Utils return node ?? def; } } - return def; + return node ?? def; } public override void show(int indent=0) diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala index 17c66208..e51362f6 100644 --- a/src/utils/FSUtils.vala +++ b/src/utils/FSUtils.vala @@ -265,7 +265,10 @@ namespace GameHub.Utils { foreach(var v in variables.entries) { - expanded_path = expanded_path.replace("${" + v.key + "}", v.value).replace("$" + v.key, v.value); + if(v.key != null && v.value != null) + { + expanded_path = expanded_path.replace("${" + v.key + "}", v.value).replace("$" + v.key, v.value); + } } } expanded_path = Utils.replace_prefix(expanded_path, "~/.cache", Environment.get_user_cache_dir());