Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adapting Windows games to run native Linux #317

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 82 additions & 7 deletions data/ui/properties.ui
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<property name="default_width">400</property>
<property name="default_height">250</property>
<property name="type_hint">dialog</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
Expand Down Expand Up @@ -53,7 +56,6 @@
<property name="margin_top">18</property>
<property name="row_spacing">6</property>
<property name="column_spacing">12</property>
<property name="row_homogeneous">True</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkButton" id="button_properties_support">
Expand Down Expand Up @@ -97,26 +99,26 @@
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Hide game:</property>
<property name="justify">fill</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">4</property>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="switch_properties_hide_game">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can_focus">True</property>
<property name="halign">start</property>
<property name="valign">center</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">4</property>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
Expand Down Expand Up @@ -219,6 +221,79 @@
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="label" translatable="yes">Game platform:</property>
<property name="justify">fill</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="game_type">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="orientation">vertical</property>
<property name="layout_style">center</property>
<child>
<object class="GtkRadioButton" id="radiobutton_linux_type">
<property name="label" translatable="yes">Linux (native)</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="radiobutton_windows_type">
<property name="label" translatable="yes">Windows (wine)</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">radiobutton_linux_type</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="radiobutton_adapted_type">
<property name="label" translatable="yes">Linux (adapted)</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">radiobutton_linux_type</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<placeholder/>
</child>
Expand Down
20 changes: 16 additions & 4 deletions minigalaxy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions minigalaxy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Empty file.
42 changes: 42 additions & 0 deletions minigalaxy/custom_installers/theme_hospital.py
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 30 additions & 2 deletions minigalaxy/game.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
60 changes: 52 additions & 8 deletions minigalaxy/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -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):
Expand Down Expand Up @@ -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
Loading