diff --git a/data/ui/properties.ui b/data/ui/properties.ui index bff81ba6..e1422dde 100644 --- a/data/ui/properties.ui +++ b/data/ui/properties.ui @@ -7,6 +7,9 @@ 400 250 dialog + + + False @@ -53,7 +56,6 @@ 18 6 12 - True True @@ -97,26 +99,26 @@ True - False + False end Hide game: fill - 0 - 4 + 0 + 4 True - True + True start center - 1 - 4 + 1 + 4 @@ -219,6 +221,79 @@ 2 + + + True + False + end + Game platform: + fill + + + 0 + 7 + + + + + True + False + start + vertical + center + + + Linux (native) + True + True + False + True + True + + + True + True + 0 + + + + + Windows (wine) + True + True + False + True + True + radiobutton_linux_type + + + True + True + 1 + + + + + Linux (adapted) + True + True + False + True + True + radiobutton_linux_type + + + True + True + 2 + + + + + 1 + 7 + + diff --git a/minigalaxy/api.py b/minigalaxy/api.py index ae1beae1..6cc34230 100644 --- a/minigalaxy/api.py +++ b/minigalaxy/api.py @@ -4,7 +4,7 @@ import requests import xml.etree.ElementTree as ET from minigalaxy.game import Game -from minigalaxy.constants import IGNORE_GAME_IDS, SESSION +from minigalaxy.constants import IGNORE_GAME_IDS, ADAPTED_GAMES, SESSION from minigalaxy.config import Config @@ -84,21 +84,33 @@ def get_library(self): } response = self.__request(url, params=params) total_pages = response["totalPages"] - + adapted_games_ids = [] + for adapted_game in ADAPTED_GAMES: + adapted_games_ids.append(adapted_game["id"]) for product in response["products"]: if product["id"] not in IGNORE_GAME_IDS: # Only support Linux unless the show_windows_games setting is enabled if product["worksOn"]["Linux"]: platform = "linux" + supported_platforms = [platform, "windows"] + elif product["id"] in adapted_games_ids: + platform = "adapted" + supported_platforms = [platform, "windows"] elif Config.get("show_windows_games"): platform = "windows" + supported_platforms = [platform] else: continue if not product["url"]: print("{} ({}) has no store page url".format(product["title"], product['id'])) game = Game(name=product["title"], url=product["url"], game_id=product["id"], - image_url=product["image"], platform=platform) - games.append(game) + image_url=product["image"], platform=platform, + supported_platforms=supported_platforms) + game_cfg_platform = game.get_info("platform") + if game_cfg_platform: + game.platform = game_cfg_platform + if Config.get("show_windows_games") or game.platform not in ["windows"]: + games.append(game) if current_page == total_pages: all_pages_processed = True current_page += 1 diff --git a/minigalaxy/constants.py b/minigalaxy/constants.py index a1a7d570..3d5efe93 100644 --- a/minigalaxy/constants.py +++ b/minigalaxy/constants.py @@ -48,6 +48,11 @@ 1486144755, # Cyberpunk 2077 Goodies Collection ] +# GOG provides only Windows support for those games, but we can make them native on Linux +ADAPTED_GAMES = [ + {"name": "Theme Hospital", "id": 1207659026, "require": ["dosbox"]} +] + DOWNLOAD_CHUNK_SIZE = 1024 * 1024 # 1 MB # This is the file size needed for the download manager to consider resuming worthwhile diff --git a/minigalaxy/custom_installers/__init__.py b/minigalaxy/custom_installers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/minigalaxy/custom_installers/theme_hospital.py b/minigalaxy/custom_installers/theme_hospital.py new file mode 100644 index 00000000..cf339475 --- /dev/null +++ b/minigalaxy/custom_installers/theme_hospital.py @@ -0,0 +1,42 @@ +import os + + +def start(game): + err_msg = "" + create_save_dir(game) + change_language_to_en(game) + create_start_script(game) + return err_msg + + +def create_save_dir(game): + hospital_saves_dir = os.path.join(game.install_dir, "SAVE") + if not os.path.isdir(hospital_saves_dir): + os.makedirs(hospital_saves_dir) + + +def change_language_to_en(game): + hospital_cfg_file = os.path.join(game.install_dir, "HOSPITAL.CFG") + if os.path.isfile(hospital_cfg_file): + cfg_file = open(hospital_cfg_file, "r") + cfg_content = cfg_file.readlines() + cfg_file.close() + cfg_content_mod = [] + for cfg_line in cfg_content: + if "LANGUAGE=" in cfg_line: + cfg_content_mod.append("LANGUAGE=EN\n") + else: + cfg_content_mod.append(cfg_line) + cfg_file = open(hospital_cfg_file, "w") + for cfg_line in cfg_content_mod: + cfg_file.write(cfg_line) + cfg_file.close() + + +def create_start_script(game): + hospital_start_file = os.path.join(game.install_dir, "minigalaxy-start.sh") + start_script = '#!/bin/bash\ndosbox "{}/HOSPITAL.EXE" -exit -fullscreen'.format(game.install_dir) + start_file = open(hospital_start_file, "w") + start_file.write(start_script) + start_file.close() + os.chmod(hospital_start_file, 0o775) diff --git a/minigalaxy/game.py b/minigalaxy/game.py index 91ff91e9..04e80cd9 100644 --- a/minigalaxy/game.py +++ b/minigalaxy/game.py @@ -1,24 +1,28 @@ import os import re import json +import shutil from minigalaxy.config import Config from minigalaxy.paths import CONFIG_GAMES_DIR +from minigalaxy.constants import ADAPTED_GAMES class Game: def __init__(self, name: str, url: str = "", md5sum=None, game_id: int = 0, install_dir: str = "", - image_url="", platform="linux", dlcs=None): + image_url="", platform="linux", supported_platforms: list = None, dlcs=None): self.name = name self.url = url self.md5sum = {} if md5sum is None else md5sum self.id = game_id self.install_dir = install_dir self.image_url = image_url - self.platform = platform self.dlcs = [] if dlcs is None else dlcs self.status_file_name = "{}.json".format(self.get_install_directory_name()) self.status_file_path = os.path.join(CONFIG_GAMES_DIR, self.status_file_name) + self.platform = platform + self.supported_platforms = [platform] if supported_platforms is None else supported_platforms + self.check_adapted() def get_stripped_name(self): return self.__strip_string(self.name) @@ -81,6 +85,26 @@ def fallback_read_installed_version(self): version = "0" return version + def check_adapted(self): + adapted_names = [] + adapted_require = [] + adapted_game_nr = -1 + for adapted_game in ADAPTED_GAMES: + adapted_game_nr += 1 + adapted_names.append(adapted_game["name"]) + adapted_require.append(adapted_game["require"]) + if self.name in adapted_names and "adapted" not in self.supported_platforms: + self.supported_platforms.append("adapted") + if not self.get_info("platform"): + self.set_platform("adapted") + if self.platform in "adapted" or "adapted" in self.supported_platforms: + for require in adapted_require[adapted_game_nr]: + if not shutil.which(require): + if "adapted" in self.supported_platforms: + self.supported_platforms.remove("adapted") + self.set_platform("windows") + break + def set_info(self, key, value): json_dict = self.load_minigalaxy_info_json() json_dict[key] = value @@ -137,6 +161,10 @@ def set_install_dir(self): if not self.install_dir: self.install_dir = os.path.join(Config.get("install_dir"), self.get_install_directory_name()) + def set_platform(self, platform): + self.platform = platform + self.set_info("platform", platform) + def __str__(self): return self.name diff --git a/minigalaxy/installer.py b/minigalaxy/installer.py index 99bcc8df..d49916c1 100644 --- a/minigalaxy/installer.py +++ b/minigalaxy/installer.py @@ -2,9 +2,13 @@ import shutil import subprocess import hashlib +import tarfile + +from minigalaxy.constants import SESSION, DOWNLOAD_CHUNK_SIZE from minigalaxy.translation import _ from minigalaxy.paths import CACHE_DIR, THUMBNAIL_DIR from minigalaxy.config import Config +from minigalaxy.custom_installers import theme_hospital def get_available_disk_space(location): @@ -52,6 +56,8 @@ def install_game(game, installer): error_message = move_and_overwrite(game, tmp_dir, game.install_dir) if not error_message: error_message = copy_thumbnail(game) + if not error_message: + error_message = additional_configuration(game) if not error_message: error_message = remove_installer(installer) else: @@ -109,14 +115,10 @@ def extract_installer(game, installer, temp_dir): error_message = "" if game.platform == "linux": command = ["unzip", "-qq", installer, "-d", temp_dir] + elif game.platform in ["adapted"]: + command, error_message = extract_by_innoextract(installer, temp_dir) else: - # Set the prefix for Windows games - prefix_dir = os.path.join(game.install_dir, "prefix") - if not os.path.exists(prefix_dir): - os.makedirs(prefix_dir, mode=0o755) - - # It's possible to set install dir as argument before installation - command = ["env", "WINEPREFIX={}".format(prefix_dir), "wine", installer, "/dir={}".format(temp_dir), "/VERYSILENT"] + command, error_message = extract_by_wine(game, installer, temp_dir) process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) process.wait() stdout, stderr = process.communicate() @@ -136,7 +138,8 @@ def move_and_overwrite(game, temp_dir, target_dir): if game.platform == "linux": source_dir = os.path.join(temp_dir, "data/noarch") else: - source_dir = temp_dir + innoextract_dir = os.path.join(temp_dir, "minigalaxy_game_files") + source_dir = temp_dir if not os.path.isdir(innoextract_dir) else innoextract_dir for src_dir, dirs, files in os.walk(source_dir): destination_dir = src_dir.replace(source_dir, target_dir, 1) if not os.path.exists(destination_dir): @@ -183,3 +186,44 @@ def uninstall_game(game): shutil.rmtree(game.install_dir, ignore_errors=True) if os.path.isfile(game.status_file_path): os.remove(game.status_file_path) + + +def extract_by_innoextract(installer, temp_dir): + err_msg = "" + innoextract_ver = "1.9" + innoextract_tar = "innoextract-{}-linux.tar.xz".format(innoextract_ver) + innoextract_url = "https://constexpr.org/innoextract/files/{}".format(innoextract_tar) + download_request = SESSION.get(innoextract_url, stream=True, timeout=30) + with open(os.path.join(temp_dir, innoextract_tar), "wb") as save_file: + for chunk in download_request.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE): + save_file.write(chunk) + save_file.close() + tar = tarfile.open(os.path.join(temp_dir, innoextract_tar)) + tar.extractall(path=temp_dir) + tar.close() + innoextract_file = os.path.join(temp_dir, "innoextract-{}-linux".format(innoextract_ver), "bin", "amd64", + "innoextract") + os.chmod(innoextract_file, 0o775) + cmd = [innoextract_file, installer, "-d", os.path.join(temp_dir, "minigalaxy_game_files")] + return cmd, err_msg + + +def extract_by_wine(game, installer, temp_dir): + err_msg = "" + # Set the prefix for Windows games + prefix_dir = os.path.join(game.install_dir, "prefix") + if not os.path.exists(prefix_dir): + os.makedirs(prefix_dir, mode=0o755) + + # It's possible to set install dir as argument before installation + command = ["env", "WINEPREFIX={}".format(prefix_dir), "wine", installer, "/dir={}".format(temp_dir), "/VERYSILENT"] + return command, err_msg + + +def additional_configuration(game): + err_msg = "" + if game.platform in ["adapted"]: + if game.id in [1207659026]: + err_msg = theme_hospital.start(game) + game.set_info("adapted", True) + return err_msg diff --git a/minigalaxy/launcher.py b/minigalaxy/launcher.py index 1c6e38e4..bb5f306b 100644 --- a/minigalaxy/launcher.py +++ b/minigalaxy/launcher.py @@ -47,6 +47,8 @@ def get_execute_command(game) -> list: exe_cmd = get_scummvm_exe_cmd(game, files) elif launcher_type in ["start_script", "wine"]: exe_cmd = get_start_script_exe_cmd(game, files) + elif launcher_type in ["adapted"]: + exe_cmd = get_adapted_exe_cmd(game, files) elif launcher_type in ["final_resort"]: exe_cmd = get_final_resort_exe_cmd(game, files) else: @@ -58,7 +60,9 @@ def get_execute_command(game) -> list: def determine_launcher_type(files): launcher_type = "unknown" - if "unins000.exe" in files: + if "minigalaxy-start.sh" in files: + launcher_type = "adapted" + elif "unins000.exe" in files: launcher_type = "windows" elif "dosbox" in files and shutil.which("dosbox"): launcher_type = "dosbox" @@ -141,6 +145,12 @@ def get_start_script_exe_cmd(game, files): return exec_start +def get_adapted_exe_cmd(game, files): + start_sh = "minigalaxy-start.sh" + exec_start = [os.path.join(game.install_dir, start_sh)] if start_sh in files else [""] + return exec_start + + def get_final_resort_exe_cmd(game, files): # This is the final resort, applies to FTL exe_cmd = [""] diff --git a/minigalaxy/ui/library.py b/minigalaxy/ui/library.py index 2f8c7341..60978c13 100644 --- a/minigalaxy/ui/library.py +++ b/minigalaxy/ui/library.py @@ -130,7 +130,11 @@ def __get_installed_games(self) -> List[Game]: game_id = 0 else: game_id = int(game_id) - games.append(Game(name=name, game_id=game_id, install_dir=full_path)) + game = Game(name=name, game_id=game_id, install_dir=full_path) + game_cfg_platform = game.get_info("platform") + if game_cfg_platform: + game.platform = game_cfg_platform + games.append(game) else: game_files = os.listdir(full_path) for file in game_files: @@ -143,6 +147,9 @@ def __get_installed_games(self) -> List[Game]: install_dir=full_path, platform="windows" ) + game_cfg_platform = game.get_info("platform") + if game_cfg_platform: + game.platform = game_cfg_platform games.append(game) return games diff --git a/minigalaxy/ui/properties.py b/minigalaxy/ui/properties.py index 6a7a9c81..45081705 100644 --- a/minigalaxy/ui/properties.py +++ b/minigalaxy/ui/properties.py @@ -1,3 +1,4 @@ +import shutil import urllib import gi import os @@ -31,6 +32,9 @@ class Properties(Gtk.Dialog): entry_properties_variable = Gtk.Template.Child() entry_properties_command = Gtk.Template.Child() label_game_description = Gtk.Template.Child() + radiobutton_linux_type = Gtk.Template.Child() + radiobutton_windows_type = Gtk.Template.Child() + radiobutton_adapted_type = Gtk.Template.Child() def __init__(self, parent, game, api): Gtk.Dialog.__init__(self, title=_("Properties of {}").format(game.name), parent=parent.parent.parent, @@ -69,6 +73,12 @@ def ok_pressed(self, button): self.game.set_info("show_fps", self.switch_properties_show_fps.get_active()) self.game.set_info("hide_game", self.switch_properties_hide_game.get_active()) self.parent.parent.filter_library() + if self.radiobutton_linux_type.get_active(): + self.game.set_platform("linux") + elif self.radiobutton_windows_type.get_active(): + self.game.set_platform("windows") + elif self.radiobutton_adapted_type.get_active(): + self.game.set_platform("adapted") self.destroy() @Gtk.Template.Callback("on_button_properties_winecfg_clicked") @@ -147,7 +157,22 @@ def button_sensitive(self, game): self.entry_properties_variable.set_sensitive(False) self.button_properties_regedit.set_sensitive(False) self.switch_properties_show_fps.set_sensitive(False) + if "linux" not in self.game.supported_platforms: + self.radiobutton_linux_type.set_sensitive(False) + if "windows" not in self.game.supported_platforms or not shutil.which("wine"): + self.radiobutton_windows_type.set_sensitive(False) + if "adapted" not in self.game.supported_platforms: + self.radiobutton_adapted_type.set_sensitive(False) + else: + self.radiobutton_linux_type.set_sensitive(False) + self.radiobutton_windows_type.set_sensitive(False) + self.radiobutton_adapted_type.set_sensitive(False) - if game.platform == 'linux': + if game.platform in ["linux"]: self.button_properties_winecfg.hide() self.button_properties_regedit.hide() + self.radiobutton_linux_type.set_active(True) + elif game.platform in ["windows"]: + self.radiobutton_windows_type.set_active(True) + elif game.platform in ["adapted"]: + self.radiobutton_adapted_type.set_active(True)