mirror of
https://github.com/lutris/lutris
synced 2024-11-02 11:48:38 +00:00
commit
9b058eaaa2
49 changed files with 1944 additions and 1555 deletions
2
.github/scripts/build-ubuntu.sh
vendored
2
.github/scripts/build-ubuntu.sh
vendored
|
@ -74,7 +74,7 @@ if [[ -z $PPA_GPG_KEY_ID ]]; then
|
|||
PPA_GPG_KEY_ID=$(echo "${PPA_GPG_PRIVATE_KEY}" | gpg --import-options show-only --import | sed -n '2s/^\s*//p')
|
||||
export PPA_GPG_KEY_ID
|
||||
echo "${PPA_GPG_KEY_ID}"
|
||||
echo "${PPA_GPG_PRIVATE_KEY}" | gpg --batch --passphrase "${PPA_GPG_PASSPHRASE}" --import
|
||||
echo "${PPA_GPG_PRIVATE_KEY}" | gpg --batch --passphrase ${PPA_GPG_PASSPHRASE} --import
|
||||
echo "::endgroup::"
|
||||
|
||||
# May as well since we don't need after at this point.
|
||||
|
|
|
@ -18,6 +18,7 @@ addons:
|
|||
- libdbus-1-dev
|
||||
- python3-yaml
|
||||
- python3-gi
|
||||
- python3-gi-cairo
|
||||
- python3-pil
|
||||
- python3-setproctitle
|
||||
- python3-distro
|
||||
|
|
4
AUTHORS
4
AUTHORS
|
@ -1,8 +1,8 @@
|
|||
Copyright (C) 2010-2022 Mathieu Comandon <strider@strycore.com>
|
||||
Copyright (C) 2009 Mathieu Comandon <mathieucomandon@gmail.com>
|
||||
|
||||
Contributors:
|
||||
|
||||
Mathieu Comandon <strider@strycore.com>
|
||||
Mathieu Comandon <mathieucomandon@gmail.com>
|
||||
Pascal Reinhard (Xodetaetl) <dev@xod.me>
|
||||
Daniel J (@djazz)
|
||||
Tom Todd
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
Contributing to Lutris
|
||||
======================
|
||||
|
||||
IMPORTANT!
|
||||
|
||||
If you contribute to Lutris on a somewhat regular basis, be sure to add yourself to the AUTHORS file!
|
||||
|
||||
Finding features to work on
|
||||
---------------------------
|
||||
|
||||
|
@ -19,9 +23,7 @@ issues](https://github.com/lutris/lutris/issues?q=is%3Aissue+is%3Aopen+label%3A%
|
|||
that can't be reproduced on the developers setup. Other issues, tagged [need
|
||||
help](https://github.com/lutris/lutris/issues?q=is%3Aissue+is%3Aopen+label%3A%22need+help%22)
|
||||
might be a bit more technical to resolve but you can always have a look and see
|
||||
if they fit your area of expertise. Also, while not fully ready, we do
|
||||
appreciate receiving translations for other languages, support for i18n will
|
||||
come in a future update.
|
||||
if they fit your area of expertise.
|
||||
|
||||
Note that Lutris is not a playground or a toy project. One cannot submit new
|
||||
features that aren't on the roadmap and submit a pull request for them without
|
||||
|
|
|
@ -12,7 +12,7 @@ Lutris manually, it requires the following components:
|
|||
|
||||
* Python >= 3.7
|
||||
* PyGObject
|
||||
* PyGObject bindings for: Gtk, Gdk, GnomeDesktop, Webkit2, Notify
|
||||
* PyGObject bindings for: Gtk, Gdk, Cairo, GnomeDesktop, Webkit2, Notify
|
||||
* python3-requests
|
||||
* python3-pillow
|
||||
* python3-yaml
|
||||
|
@ -40,7 +40,7 @@ games themselves we recommend you install the following packages:
|
|||
To install all those dependencies (except for Wine and graphics drivers)
|
||||
on Ubuntu based systems, you can run::
|
||||
|
||||
sudo apt install python3-yaml python3-requests python3-pil python3-gi \
|
||||
sudo apt install python3-yaml python3-requests python3-pil python3-gi python3-gi-cairo \
|
||||
gir1.2-gtk-3.0 gir1.2-gnomedesktop-3.0 gir1.2-webkit2-4.0 \
|
||||
gir1.2-notify-0.7 psmisc cabextract unzip p7zip curl fluid-soundfont-gs \
|
||||
x11-xserver-utils python3-evdev libc6-i386 lib32gcc1 libgirepository1.0-dev \
|
||||
|
|
5
Makefile
5
Makefile
|
@ -36,9 +36,12 @@ github-ppa:
|
|||
# so that _must_ be the last parameter.
|
||||
echo "y" | debuild -S \
|
||||
-k"${PPA_GPG_KEY_ID}" \
|
||||
-p"gpg --batch --passphrase "${PPA_GPG_PASSPHRASE}" --pinentry-mode loopback" \
|
||||
-p"gpg --batch --passphrase ${PPA_GPG_PASSPHRASE} --pinentry-mode loopback" \
|
||||
--lintian-opts --suppress-tags malformed-debian-changelog-version
|
||||
|
||||
build-deps-ubuntu:
|
||||
sudo apt install devscripts debhelper dh-python meson
|
||||
|
||||
build:
|
||||
gbp buildpackage --git-debian-branch=${GITBRANCH}
|
||||
|
||||
|
|
4
debian/changelog
vendored
4
debian/changelog
vendored
|
@ -21,11 +21,11 @@ lutris (0.5.13) jammy; urgency=medium
|
|||
* Improve detection of DOSBox games on GOG
|
||||
* Added "Unspecified" Vulkan ICD option
|
||||
* Removed ResidualVM (now merged into ScummVM)
|
||||
* Detect obsolete Vulkan drivers, warn and default to DXVK 1.x for them
|
||||
* Detect obsolete Vulkan drivers and default to DXVK 1.x for them
|
||||
* Improved High-DPI support for custom media
|
||||
* Performance improvements
|
||||
|
||||
-- Mathieu Comandon <strider@strycore.com> Fri, 10 Feb 2023 13:15:40 -0800
|
||||
-- Mathieu Comandon <mathieucomandon@gmail.com> Fri, 16 May 2023 13:15:40 -0800
|
||||
|
||||
lutris (0.5.12) jammy; urgency=medium
|
||||
|
||||
|
|
3
debian/control
vendored
3
debian/control
vendored
|
@ -1,7 +1,7 @@
|
|||
Source: lutris
|
||||
Section: games
|
||||
Priority: optional
|
||||
Maintainer: Mathieu Comandon <strider@strycore.com>
|
||||
Maintainer: Mathieu Comandon <mathieucomandon@gmail.com>
|
||||
Build-Depends: debhelper-compat (= 12),
|
||||
appstream,
|
||||
dh-sequence-python3,
|
||||
|
@ -21,6 +21,7 @@ Depends: ${misc:Depends},
|
|||
python3-requests,
|
||||
python3-pil,
|
||||
python3-gi,
|
||||
python3-gi-cairo,
|
||||
python3-setproctitle,
|
||||
python3-magic,
|
||||
python3-distro,
|
||||
|
|
6
debian/copyright
vendored
6
debian/copyright
vendored
|
@ -1,16 +1,16 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: lutris
|
||||
Upstream-Contact: Mathieu Comandon <strider@strycore.com>
|
||||
Upstream-Contact: Mathieu Comandon <mathieucomandon@gmail.com>
|
||||
Upstream-Source: https://github.com/lutris
|
||||
|
||||
Files: *
|
||||
Copyright: 2009 Mathieu Comandon <strider@strycore.com>
|
||||
Copyright: 2009 Mathieu Comandon <mathieucomandon@gmail.com>
|
||||
License: GPL-3.0-or-later
|
||||
On Debian systems, the complete text of the General Public License version 3
|
||||
can be found in "/usr/share/common-licenses/GPL-3".
|
||||
|
||||
Files: share/metainfo/net.lutris.Lutris.metainfo.xml
|
||||
Copyright: Lutris Team <strider@lutris.net>
|
||||
Copyright: Lutris Team <mathieucomandon@gmail.com>
|
||||
License: CC0-1.0
|
||||
On Debian systems, the complete text of the CC0-1.0 license
|
||||
can be found in "/usr/share/common-licenses/CC0-1.0".
|
||||
|
|
|
@ -246,3 +246,9 @@ def get_unusued_game_name(game_name):
|
|||
assigned_name = f"{game_name} {assigned_index}"
|
||||
|
||||
return assigned_name
|
||||
|
||||
|
||||
def get_game_count(param, value):
|
||||
res = sql.db_select(settings.PGA_DB, "games", fields=("COUNT(id)",), condition=(param, value))
|
||||
if res:
|
||||
return res[0]["COUNT(id)"]
|
||||
|
|
|
@ -160,7 +160,7 @@ class AddGamesWindow(ModelessDialog): # pylint: disable=too-many-public-methods
|
|||
self.destroy()
|
||||
|
||||
def on_watched_error(self, error):
|
||||
ErrorDialog(str(error), parent=self)
|
||||
ErrorDialog(error, parent=self)
|
||||
|
||||
# Initial Page
|
||||
|
||||
|
@ -370,7 +370,7 @@ class AddGamesWindow(ModelessDialog): # pylint: disable=too-many-public-methods
|
|||
self.display_cancel_button(label=_("_Close"))
|
||||
|
||||
if error:
|
||||
ErrorDialog(str(error), parent=self)
|
||||
ErrorDialog(error, parent=self)
|
||||
self.stack.navigation_reset()
|
||||
return
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# pylint: disable=wrong-import-position
|
||||
#
|
||||
# Copyright (C) 2009 Mathieu Comandon <strider@strycore.com>
|
||||
# Copyright (C) 2009 Mathieu Comandon <mathieucomandon@gmail.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -767,7 +767,7 @@ class Application(Gtk.Application):
|
|||
|
||||
def on_watched_error(self, error):
|
||||
if self.window:
|
||||
ErrorDialog(str(error), parent=self.window)
|
||||
ErrorDialog(error, parent=self.window)
|
||||
|
||||
@staticmethod
|
||||
def get_lutris_action(url):
|
||||
|
|
|
@ -9,12 +9,11 @@ from gi.repository import Gdk, Gtk
|
|||
|
||||
# Lutris Modules
|
||||
from lutris import settings, sysoptions
|
||||
from lutris.gui.dialogs import ErrorDialog
|
||||
from lutris.gui.widgets.common import EditableGrid, FileChooserEntry, Label, VBox
|
||||
from lutris.gui.widgets.searchable_combobox import SearchableCombobox
|
||||
from lutris.runners import InvalidRunner, import_runner
|
||||
from lutris.util.jobs import AsyncCall
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.strings import gtk_safe
|
||||
|
||||
|
||||
class ConfigBox(VBox):
|
||||
|
@ -84,103 +83,106 @@ class ConfigBox(VBox):
|
|||
|
||||
# Go thru all options.
|
||||
for option in self.options:
|
||||
if "scope" in option:
|
||||
if self.config_section not in option["scope"]:
|
||||
continue
|
||||
option_key = option["option"]
|
||||
value = self.config.get(option_key)
|
||||
try:
|
||||
if "scope" in option:
|
||||
if self.config_section not in option["scope"]:
|
||||
continue
|
||||
option_key = option["option"]
|
||||
value = self.config.get(option_key)
|
||||
|
||||
if callable(option.get("choices")) and option["type"] != "choice_with_search":
|
||||
option["choices"] = option["choices"]()
|
||||
if callable(option.get("condition")):
|
||||
option["condition"] = option["condition"]()
|
||||
if callable(option.get("choices")) and option["type"] != "choice_with_search":
|
||||
option["choices"] = option["choices"]()
|
||||
if callable(option.get("condition")):
|
||||
option["condition"] = option["condition"]()
|
||||
|
||||
if option.get("section") != current_section:
|
||||
current_section = option.get("section")
|
||||
if current_section:
|
||||
frame = ConfigBox.SectionFrame(current_section)
|
||||
current_vbox = frame.vbox
|
||||
self.pack_start(frame, False, False, 0)
|
||||
else:
|
||||
current_vbox = self
|
||||
if option.get("section") != current_section:
|
||||
current_section = option.get("section")
|
||||
if current_section:
|
||||
frame = ConfigBox.SectionFrame(current_section)
|
||||
current_vbox = frame.vbox
|
||||
self.pack_start(frame, False, False, 0)
|
||||
else:
|
||||
current_vbox = self
|
||||
|
||||
self.wrapper = Gtk.Box()
|
||||
self.wrapper.set_spacing(12)
|
||||
self.wrapper.set_margin_bottom(6)
|
||||
self.wrappers[option_key] = self.wrapper
|
||||
self.wrapper = Gtk.Box()
|
||||
self.wrapper.set_spacing(12)
|
||||
self.wrapper.set_margin_bottom(6)
|
||||
self.wrappers[option_key] = self.wrapper
|
||||
|
||||
# Set tooltip's "Default" part
|
||||
default = option.get("default")
|
||||
self.tooltip_default = default if isinstance(default, str) else None
|
||||
# Set tooltip's "Default" part
|
||||
default = option.get("default")
|
||||
self.tooltip_default = default if isinstance(default, str) else None
|
||||
|
||||
# Generate option widget
|
||||
self.option_widget = None
|
||||
self.call_widget_generator(option, option_key, value, default)
|
||||
# Generate option widget
|
||||
self.option_widget = None
|
||||
self.call_widget_generator(option, option_key, value, default)
|
||||
|
||||
# Reset button
|
||||
reset_btn = Gtk.Button.new_from_icon_name("edit-undo-symbolic", Gtk.IconSize.MENU)
|
||||
reset_btn.set_valign(Gtk.Align.CENTER)
|
||||
reset_btn.set_margin_bottom(6)
|
||||
reset_btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
reset_btn.set_tooltip_text(_("Reset option to global or default config"))
|
||||
reset_btn.connect(
|
||||
"clicked",
|
||||
self.on_reset_button_clicked,
|
||||
option,
|
||||
self.option_widget,
|
||||
self.wrapper,
|
||||
)
|
||||
self.reset_buttons[option_key] = reset_btn
|
||||
|
||||
placeholder = Gtk.Box()
|
||||
placeholder.set_size_request(32, 32)
|
||||
|
||||
if option_key not in self.raw_config:
|
||||
reset_btn.set_visible(False)
|
||||
reset_btn.set_no_show_all(True)
|
||||
placeholder.pack_start(reset_btn, False, False, 0)
|
||||
|
||||
# Tooltip
|
||||
helptext = option.get("help")
|
||||
if isinstance(self.tooltip_default, str):
|
||||
helptext = helptext + "\n\n" if helptext else ""
|
||||
helptext += _("<b>Default</b>: ") + _(self.tooltip_default)
|
||||
if value != default and option_key not in self.raw_config:
|
||||
helptext = helptext + "\n\n" if helptext else ""
|
||||
helptext += _(
|
||||
"<i>(Italic indicates that this option is "
|
||||
"modified in a lower configuration level.)</i>"
|
||||
# Reset button
|
||||
reset_btn = Gtk.Button.new_from_icon_name("edit-undo-symbolic", Gtk.IconSize.MENU)
|
||||
reset_btn.set_valign(Gtk.Align.CENTER)
|
||||
reset_btn.set_margin_bottom(6)
|
||||
reset_btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
reset_btn.set_tooltip_text(_("Reset option to global or default config"))
|
||||
reset_btn.connect(
|
||||
"clicked",
|
||||
self.on_reset_button_clicked,
|
||||
option,
|
||||
self.option_widget,
|
||||
self.wrapper,
|
||||
)
|
||||
if helptext:
|
||||
self.wrapper.props.has_tooltip = True
|
||||
self.wrapper.connect("query-tooltip", self.on_query_tooltip, helptext)
|
||||
self.reset_buttons[option_key] = reset_btn
|
||||
|
||||
hbox = Gtk.Box(visible=True)
|
||||
option_container = hbox
|
||||
hbox.set_margin_left(18)
|
||||
hbox.pack_end(placeholder, False, False, 5)
|
||||
# Grey out option if condition unmet
|
||||
if "condition" in option and not option["condition"]:
|
||||
hbox.set_sensitive(False)
|
||||
placeholder = Gtk.Box()
|
||||
placeholder.set_size_request(32, 32)
|
||||
|
||||
hbox.pack_start(self.wrapper, True, True, 0)
|
||||
if option_key not in self.raw_config:
|
||||
reset_btn.set_visible(False)
|
||||
reset_btn.set_no_show_all(True)
|
||||
placeholder.pack_start(reset_btn, False, False, 0)
|
||||
|
||||
if "warning" in option:
|
||||
option_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True)
|
||||
option_container.pack_start(hbox, False, False, 0)
|
||||
warning = ConfigBox.WarningBox(option["warning"])
|
||||
warning.set_margin_left(18)
|
||||
warning.set_margin_right(18)
|
||||
warning.set_margin_bottom(6)
|
||||
warning.update_warning(self.config)
|
||||
self.warning_boxes[option_key] = warning
|
||||
option_container.pack_start(warning, False, False, 0)
|
||||
# Tooltip
|
||||
helptext = option.get("help")
|
||||
if isinstance(self.tooltip_default, str):
|
||||
helptext = helptext + "\n\n" if helptext else ""
|
||||
helptext += _("<b>Default</b>: ") + _(self.tooltip_default)
|
||||
if value != default and option_key not in self.raw_config:
|
||||
helptext = helptext + "\n\n" if helptext else ""
|
||||
helptext += _(
|
||||
"<i>(Italic indicates that this option is "
|
||||
"modified in a lower configuration level.)</i>"
|
||||
)
|
||||
if helptext:
|
||||
self.wrapper.props.has_tooltip = True
|
||||
self.wrapper.connect("query-tooltip", self.on_query_tooltip, helptext)
|
||||
|
||||
# Hide if advanced
|
||||
if option.get("advanced"):
|
||||
option_container.get_style_context().add_class("advanced")
|
||||
hbox = Gtk.Box(visible=True)
|
||||
option_container = hbox
|
||||
hbox.set_margin_left(18)
|
||||
hbox.pack_end(placeholder, False, False, 5)
|
||||
# Grey out option if condition unmet
|
||||
if "condition" in option and not option["condition"]:
|
||||
hbox.set_sensitive(False)
|
||||
|
||||
current_vbox.pack_start(option_container, False, False, 0)
|
||||
hbox.pack_start(self.wrapper, True, True, 0)
|
||||
|
||||
if "warning" in option:
|
||||
option_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True)
|
||||
option_container.pack_start(hbox, False, False, 0)
|
||||
warning = ConfigBox.WarningBox(option["warning"])
|
||||
warning.set_margin_left(18)
|
||||
warning.set_margin_right(18)
|
||||
warning.set_margin_bottom(6)
|
||||
warning.update_warning(self.config)
|
||||
self.warning_boxes[option_key] = warning
|
||||
option_container.pack_start(warning, False, False, 0)
|
||||
|
||||
# Hide if advanced
|
||||
if option.get("advanced"):
|
||||
option_container.get_style_context().add_class("advanced")
|
||||
|
||||
current_vbox.pack_start(option_container, False, False, 0)
|
||||
except Exception as ex:
|
||||
logger.exception("Failed to generate option widget for '%s': %s", option.get("option"), ex)
|
||||
self.show_all()
|
||||
|
||||
show_advanced = settings.read_setting("show_advanced_options") == "True"
|
||||
|
@ -202,10 +204,14 @@ class ConfigBox(VBox):
|
|||
self.pack_start(self.warning_label, False, False, 0)
|
||||
|
||||
def update_warning(self, config):
|
||||
if callable(self.warning):
|
||||
text = self.warning(config)
|
||||
else:
|
||||
text = self.warning
|
||||
try:
|
||||
if callable(self.warning):
|
||||
text = self.warning(config)
|
||||
else:
|
||||
text = self.warning
|
||||
except Exception as err:
|
||||
logger.exception("Unable to generate configuration warning: %s", err)
|
||||
text = gtk_safe(err)
|
||||
|
||||
if text:
|
||||
self.warning_label.set_markup(str(text))
|
||||
|
@ -271,9 +277,6 @@ class ConfigBox(VBox):
|
|||
elif option_type == "bool":
|
||||
self.generate_checkbox(option, value)
|
||||
self.tooltip_default = "Enabled" if default else "Disabled"
|
||||
elif option_type == "extended_bool":
|
||||
self.generate_checkbox_with_callback(option, value)
|
||||
self.tooltip_default = "Enabled" if default else "Disabled"
|
||||
elif option_type == "range":
|
||||
self.generate_range(option_key, option["min"], option["max"], option["label"], value)
|
||||
elif option_type == "string":
|
||||
|
@ -317,49 +320,10 @@ class ConfigBox(VBox):
|
|||
self.wrapper.pack_start(switch, False, False, 0)
|
||||
self.option_widget = switch
|
||||
|
||||
# Checkbox with callback
|
||||
def generate_checkbox_with_callback(self, option, value=None):
|
||||
"""Generate a checkbox. With callback"""
|
||||
|
||||
label = Label(option["label"])
|
||||
self.wrapper.pack_start(label, False, False, 0)
|
||||
|
||||
checkbox = Gtk.Switch()
|
||||
checkbox.set_sensitive(option["active"] is True)
|
||||
if value is True:
|
||||
checkbox.set_active(value)
|
||||
|
||||
checkbox.connect("notify::active", self._on_toggle_with_callback, option)
|
||||
checkbox.set_valign(Gtk.Align.CENTER)
|
||||
self.wrapper.pack_start(checkbox, False, False, 0)
|
||||
self.option_widget = checkbox
|
||||
|
||||
def checkbox_toggle(self, widget, _gparam, option_name):
|
||||
"""Action for the checkbox's toggled signal."""
|
||||
self.option_changed(widget, option_name, widget.get_active())
|
||||
|
||||
def _on_toggle_with_callback(self, widget, _gparam, option):
|
||||
"""Action for the checkbox's toggled signal. With callback method"""
|
||||
|
||||
option_name = option["option"]
|
||||
callback = option["callback"]
|
||||
callback_on = option.get("callback_on")
|
||||
if widget.get_active() == callback_on or callback_on is None:
|
||||
AsyncCall(callback, self._on_callback_finished, widget, option, self.config)
|
||||
else:
|
||||
self.option_changed(widget, option_name, widget.get_active())
|
||||
|
||||
def _on_callback_finished(self, result, error):
|
||||
if error:
|
||||
ErrorDialog(str(error), parent=self.get_toplevel())
|
||||
return
|
||||
|
||||
widget, option, response = result
|
||||
if response:
|
||||
self.option_changed(widget, option["option"], widget.get_active())
|
||||
else:
|
||||
widget.set_active(False)
|
||||
|
||||
# Entry
|
||||
def generate_entry(self, option_name, label, value=None, option_size=None):
|
||||
"""Generate an entry box."""
|
||||
|
|
|
@ -645,4 +645,4 @@ class GameDialogCommon(ModelessDialog, DialogInstallUIDelegate):
|
|||
self._set_image(image_type, self.image_buttons[image_type])
|
||||
|
||||
def on_watched_error(self, error):
|
||||
dialogs.ErrorDialog(str(error), parent=self)
|
||||
dialogs.ErrorDialog(error, parent=self)
|
||||
|
|
|
@ -144,4 +144,4 @@ class RunnerBox(Gtk.Box):
|
|||
self.action_alignment.add(self.get_action_button())
|
||||
|
||||
def on_watched_error(self, error):
|
||||
ErrorDialog(str(error), parent=self.get_toplevel())
|
||||
ErrorDialog(error, parent=self.get_toplevel())
|
||||
|
|
|
@ -15,7 +15,7 @@ from lutris.migrations import migrate
|
|||
from lutris.util import datapath
|
||||
from lutris.util.jobs import AsyncCall
|
||||
from lutris.util.log import logger
|
||||
|
||||
from lutris.util.strings import gtk_safe
|
||||
|
||||
class Dialog(Gtk.Dialog):
|
||||
|
||||
|
@ -168,11 +168,18 @@ class WarningDialog(Gtk.MessageDialog):
|
|||
class ErrorDialog(Gtk.MessageDialog):
|
||||
"""Display an error message."""
|
||||
|
||||
def __init__(self, message, secondary=None, parent=None):
|
||||
def __init__(self, error, secondary=None, parent=None):
|
||||
super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
|
||||
|
||||
# Some errors contain < and > and lok like markup, but aren't-
|
||||
# we'll need to protect the message box against this
|
||||
message = gtk_safe(error) if isinstance(error, BaseException) else str(error)
|
||||
|
||||
# Gtk doesn't wrap long labels containing no space correctly
|
||||
# the length of the message is limited to avoid display issues
|
||||
|
||||
self.set_markup(message[:256])
|
||||
|
||||
if secondary:
|
||||
self.format_secondary_text(secondary[:256])
|
||||
|
||||
|
@ -285,7 +292,7 @@ class LutrisInitDialog(Gtk.Dialog):
|
|||
|
||||
def init_cb(self, _result, error):
|
||||
if error:
|
||||
ErrorDialog(str(error), parent=self)
|
||||
ErrorDialog(error, parent=self)
|
||||
self.destroy()
|
||||
|
||||
def on_response(self, _widget, response):
|
||||
|
@ -551,7 +558,7 @@ class MoveDialog(ModelessDialog):
|
|||
|
||||
def on_game_moved(self, _result, error):
|
||||
if error:
|
||||
ErrorDialog(str(error), parent=self)
|
||||
ErrorDialog(error, parent=self)
|
||||
self.emit("game-moved")
|
||||
self.destroy()
|
||||
|
||||
|
|
|
@ -252,7 +252,7 @@ class InstallerWindow(ModelessDialog,
|
|||
)
|
||||
|
||||
def on_watched_error(self, error):
|
||||
ErrorDialog(str(error), parent=self)
|
||||
ErrorDialog(error, parent=self)
|
||||
self.stack.navigation_reset()
|
||||
|
||||
def set_status(self, text):
|
||||
|
|
|
@ -535,13 +535,15 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
def show_empty_label(self):
|
||||
"""Display a label when the view is empty"""
|
||||
filter_text = self.filters.get("text")
|
||||
has_uninstalled_games = games_db.get_game_count("installed", "0")
|
||||
has_hidden_games = games_db.get_game_count("hidden", "1")
|
||||
if filter_text:
|
||||
if self.filters.get("category") == "favorite":
|
||||
self.show_label(_("Add a game matching '%s' to your favorites to see it here.") % filter_text)
|
||||
elif self.filters.get("installed"):
|
||||
elif self.filters.get("installed") and has_uninstalled_games:
|
||||
self.show_label(
|
||||
_("No installed games matching '%s' found. Press Ctrl+I to show uninstalled games.") % filter_text)
|
||||
elif self.filters.get("hidden") is False: # but not if missing!
|
||||
elif self.filters.get("hidden") is False and has_hidden_games: # but not if missing!
|
||||
self.show_label(_("No visible games matching '%s' found. Press Ctrl+H to show hidden games.") %
|
||||
filter_text)
|
||||
else:
|
||||
|
@ -549,14 +551,15 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
else:
|
||||
if self.filters.get("category") == "favorite":
|
||||
self.show_label(_("Add games to your favorites to see them here."))
|
||||
elif self.filters.get("installed"):
|
||||
elif self.filters.get("installed") and has_uninstalled_games:
|
||||
self.show_label(_("No installed games found. Press Ctrl+I to show uninstalled games."))
|
||||
elif self.filters.get("hidden") is False: # but not if missing!
|
||||
elif self.filters.get("hidden") is False and has_hidden_games: # but not if missing!
|
||||
self.show_label(_("No visible games found. Press Ctrl+H to show hidden games."))
|
||||
elif (
|
||||
not self.filters.get("runner")
|
||||
and not self.filters.get("service")
|
||||
and not self.filters.get("platform")
|
||||
and not self.filters.get("dynamic_category")
|
||||
):
|
||||
self.show_splash()
|
||||
else:
|
||||
|
@ -791,7 +794,7 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
|
||||
def _service_reloaded_cb(self, error):
|
||||
if error:
|
||||
dialogs.ErrorDialog(str(error), parent=self)
|
||||
dialogs.ErrorDialog(error, parent=self)
|
||||
|
||||
def on_service_logout(self, service):
|
||||
if self.service and service.id == self.service.id:
|
||||
|
@ -874,7 +877,7 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
|
||||
def on_game_unhandled_error(self, game, error):
|
||||
"""Called when a game has sent the 'game-error' signal"""
|
||||
dialogs.ErrorDialog(str(error), parent=self)
|
||||
dialogs.ErrorDialog(error, parent=self)
|
||||
return True
|
||||
|
||||
@GtkTemplate.Callback
|
||||
|
@ -1054,4 +1057,4 @@ class LutrisWindow(Gtk.ApplicationWindow,
|
|||
game.emit("game-install")
|
||||
|
||||
def on_watched_error(self, error):
|
||||
dialogs.ErrorDialog(str(error), parent=self)
|
||||
dialogs.ErrorDialog(error, parent=self)
|
||||
|
|
|
@ -29,17 +29,18 @@ class GameGridView(Gtk.IconView, GameView):
|
|||
self.image_renderer = None
|
||||
self.set_item_padding(1)
|
||||
if hide_text:
|
||||
self.cell_renderer = None
|
||||
self.text_renderer = None
|
||||
else:
|
||||
self.cell_renderer = GridViewCellRendererText()
|
||||
self.pack_end(self.cell_renderer, False)
|
||||
self.add_attribute(self.cell_renderer, "markup", COL_NAME)
|
||||
self.text_renderer = GridViewCellRendererText()
|
||||
self.pack_end(self.text_renderer, False)
|
||||
self.add_attribute(self.text_renderer, "markup", COL_NAME)
|
||||
|
||||
self.set_game_store(store)
|
||||
|
||||
self.connect_signals()
|
||||
self.connect("item-activated", self.on_item_activated)
|
||||
self.connect("selection-changed", self.on_selection_changed)
|
||||
self.connect("style-updated", self.on_style_updated)
|
||||
|
||||
def set_game_store(self, game_store):
|
||||
self.game_store = game_store
|
||||
|
@ -53,9 +54,9 @@ class GameGridView(Gtk.IconView, GameView):
|
|||
self.image_renderer.media_width = size[0]
|
||||
self.image_renderer.media_height = size[1]
|
||||
|
||||
if self.cell_renderer:
|
||||
if self.text_renderer:
|
||||
cell_width = max(size[0], self.min_width)
|
||||
self.cell_renderer.set_width(cell_width)
|
||||
self.text_renderer.set_width(cell_width)
|
||||
|
||||
@property
|
||||
def show_badges(self):
|
||||
|
@ -104,3 +105,7 @@ class GameGridView(Gtk.IconView, GameView):
|
|||
selected_items = self.get_selected_item()
|
||||
if selected_items:
|
||||
self.emit("game-selected", selected_items)
|
||||
|
||||
def on_style_updated(self, widget):
|
||||
if self.text_renderer:
|
||||
self.text_renderer.clear_caches()
|
||||
|
|
|
@ -12,7 +12,8 @@ from lutris.gui.widgets.utils import (
|
|||
|
||||
|
||||
class GridViewCellRendererText(Gtk.CellRendererText):
|
||||
"""CellRendererText adjusted for grid view display, removes extra padding"""
|
||||
"""CellRendererText adjusted for grid view display, removes extra padding
|
||||
and caches cell metrics for improved resize performance."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -20,9 +21,66 @@ class GridViewCellRendererText(Gtk.CellRendererText):
|
|||
self.props.wrap_mode = Pango.WrapMode.WORD
|
||||
self.props.xalign = 0.5
|
||||
self.props.yalign = 0
|
||||
self.fixed_width = None
|
||||
self.cached_height = {}
|
||||
self.cached_width = {}
|
||||
|
||||
def set_width(self, width):
|
||||
self.fixed_width = width
|
||||
self.props.wrap_width = width
|
||||
self.clear_caches()
|
||||
|
||||
def clear_caches(self):
|
||||
self.cached_height.clear()
|
||||
self.cached_width.clear()
|
||||
|
||||
def do_get_preferred_width(self, widget):
|
||||
text = self.props.text # pylint:disable=no-member
|
||||
if self.fixed_width and text in self.cached_width:
|
||||
return self.cached_width[text]
|
||||
|
||||
width = Gtk.CellRendererText.do_get_preferred_width(self, widget)
|
||||
|
||||
if self.fixed_width:
|
||||
self.cached_width[text] = width
|
||||
|
||||
return width
|
||||
|
||||
def do_get_preferred_width_for_height(self, widget, width):
|
||||
text = self.props.text # pylint:disable=no-member
|
||||
if self.fixed_width and text in self.cached_width:
|
||||
return self.cached_width[text]
|
||||
|
||||
width = Gtk.CellRendererText.do_get_preferred_width_for_height(self, widget, width)
|
||||
|
||||
if self.fixed_width:
|
||||
self.cached_width[text] = width
|
||||
|
||||
return width
|
||||
|
||||
def do_get_preferred_height(self, widget):
|
||||
text = self.props.text # pylint:disable=no-member
|
||||
if self.fixed_width and text in self.cached_height:
|
||||
return self.cached_height[text]
|
||||
|
||||
height = Gtk.CellRendererText.do_get_preferred_height(self, widget)
|
||||
|
||||
if self.fixed_width:
|
||||
self.cached_height[text] = height
|
||||
|
||||
return height
|
||||
|
||||
def do_get_preferred_height_for_width(self, widget, width):
|
||||
text = self.props.text # pylint:disable=no-member
|
||||
if self.fixed_width and text in self.cached_height:
|
||||
return self.cached_height[text]
|
||||
|
||||
height = Gtk.CellRendererText.do_get_preferred_height_for_width(self, widget, width)
|
||||
|
||||
if self.fixed_width:
|
||||
self.cached_height[text] = height
|
||||
|
||||
return height
|
||||
|
||||
|
||||
class GridViewCellRendererImage(Gtk.CellRenderer):
|
||||
|
@ -39,6 +97,7 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
|
|||
self._is_installed = True
|
||||
self.cached_surfaces_new = {}
|
||||
self.cached_surfaces_old = {}
|
||||
self.cached_surfaces_loaded = 0
|
||||
self.cycle_cache_idle_id = None
|
||||
self.cached_surface_generation = 0
|
||||
|
||||
|
@ -115,15 +174,14 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
|
|||
|
||||
if surface:
|
||||
x, y = self.get_media_position(surface, cell_area)
|
||||
surface_width = get_surface_size(surface)[0]
|
||||
|
||||
if alpha >= 1:
|
||||
self.render_media(cr, widget, surface, x, y)
|
||||
self.render_platforms(cr, widget, x + surface_width, cell_area)
|
||||
self.render_platforms(cr, widget, surface, x, cell_area)
|
||||
else:
|
||||
cr.push_group()
|
||||
self.render_media(cr, widget, surface, x, y)
|
||||
self.render_platforms(cr, widget, x + surface_width, cell_area)
|
||||
self.render_platforms(cr, widget, surface, x, cell_area)
|
||||
cr.pop_group_to_source()
|
||||
cr.paint_with_alpha(alpha)
|
||||
|
||||
|
@ -132,6 +190,50 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
|
|||
if not self.cycle_cache_idle_id:
|
||||
self.cycle_cache_idle_id = GLib.idle_add(self.cycle_cache)
|
||||
|
||||
@staticmethod
|
||||
def is_bright_corner(surface, corner_size):
|
||||
"""Tests several pixels near the corner of the surface where the badges
|
||||
are drawn. If all are 'bright', we'll render the badges differently. This
|
||||
means all 4 components must be at least 128/255."""
|
||||
surface_format = surface.get_format()
|
||||
|
||||
# We only use the ARGB32 format, so we just give up
|
||||
# for anything else.
|
||||
if surface_format != cairo.FORMAT_ARGB32: # pylint:disable=no-member
|
||||
return False
|
||||
|
||||
# Scale the corner according to the surface's scale factor -
|
||||
# normally the same as our UI scale factor.
|
||||
device_scale_x, device_scale_y = surface.get_device_scale()
|
||||
corner_pixel_width = int(corner_size[0] * device_scale_x)
|
||||
corner_pixel_height = int(corner_size[1] * device_scale_y)
|
||||
pixel_width = surface.get_width()
|
||||
pixel_height = surface.get_height()
|
||||
|
||||
def is_bright_pixel(x, y):
|
||||
# Checks if a pixel is 'bright'; this does not care
|
||||
# if the pixel is big or little endian- it just checks
|
||||
# all four channels.
|
||||
if 0 <= x < pixel_width and 0 <= y < pixel_height:
|
||||
stride = surface.get_stride()
|
||||
data = surface.get_data()
|
||||
|
||||
offset = (y * stride) + x * 4
|
||||
pixel = data[offset: offset + 4]
|
||||
|
||||
for channel in pixel:
|
||||
if channel < 128:
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
return (
|
||||
is_bright_pixel(pixel_width - 1, pixel_height - 1)
|
||||
and is_bright_pixel(pixel_width - corner_pixel_width, pixel_height - 1)
|
||||
and is_bright_pixel(pixel_width - 1, pixel_height - corner_pixel_height)
|
||||
and is_bright_pixel(pixel_width - corner_pixel_width, pixel_height - corner_pixel_height)
|
||||
)
|
||||
|
||||
def get_media_position(self, surface, cell_area):
|
||||
"""Computes the position of the upper left corner where we will render
|
||||
a surface within the cell area."""
|
||||
|
@ -162,7 +264,7 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
|
|||
cr.rectangle(x, y, width, height)
|
||||
cr.fill()
|
||||
|
||||
def render_platforms(self, cr, widget, media_right, cell_area):
|
||||
def render_platforms(self, cr, widget, surface, surface_x, cell_area):
|
||||
"""Renders the stack of platform icons. They appear lined up vertically to the
|
||||
right of 'media_right', if that will fit in 'cell_area'."""
|
||||
platform = self.platform
|
||||
|
@ -176,23 +278,33 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
|
|||
icon_paths = [get_runtime_icon_path(p + "-symbolic") for p in platforms]
|
||||
icon_paths = [path for path in icon_paths if path]
|
||||
if icon_paths:
|
||||
self.render_badge_stack(cr, widget, icon_paths, icon_size, media_right, cell_area)
|
||||
self.render_badge_stack(cr, widget, surface, surface_x, icon_paths, icon_size, cell_area)
|
||||
|
||||
def render_badge_stack(self, cr, widget, icon_paths, icon_size, media_right, cell_area):
|
||||
def render_badge_stack(self, cr, widget, surface, surface_x, icon_paths, icon_size, cell_area):
|
||||
"""Renders a vertical stack of badges, placed at the edge of the media, off to the right
|
||||
of 'media_right' if this will fit in the 'cell_area'. The icons in icon_paths are drawn from
|
||||
top to bottom, and spaced to fit in 'cell_area', even if they overlap because of this."""
|
||||
def render_badge(badge_x, badge_y, path):
|
||||
cr.rectangle(badge_x, badge_y, icon_size[0], icon_size[0])
|
||||
cr.set_source_rgba(0.2, 0.2, 0.2, 0.6)
|
||||
cr.fill()
|
||||
|
||||
icon = self.get_cached_surface_by_path(widget, path, size=icon_size)
|
||||
cr.set_source_rgba(0.8, 0.8, 0.8, 0.6)
|
||||
cr.mask_surface(icon, badge_x, badge_y)
|
||||
|
||||
badge_width = icon_size[0]
|
||||
badge_height = icon_size[1]
|
||||
on_bright_surface = GridViewCellRendererImage.is_bright_corner(surface, (badge_width, badge_height))
|
||||
|
||||
alpha = 0.6
|
||||
bright_color = 0.8, 0.8, 0.8
|
||||
dark_color = 0.2, 0.2, 0.2
|
||||
back_color = bright_color if on_bright_surface else dark_color
|
||||
fore_color = dark_color if on_bright_surface else bright_color
|
||||
|
||||
def render_badge(badge_x, badge_y, path):
|
||||
cr.rectangle(badge_x, badge_y, icon_size[0], icon_size[0])
|
||||
cr.set_source_rgba(back_color[0], back_color[1], back_color[2], alpha)
|
||||
cr.fill()
|
||||
|
||||
icon = self.get_cached_surface_by_path(widget, path, size=icon_size)
|
||||
cr.set_source_rgba(fore_color[0], fore_color[1], fore_color[2], alpha)
|
||||
cr.mask_surface(icon, badge_x, badge_y)
|
||||
|
||||
media_right = surface_x + get_surface_size(surface)[0]
|
||||
|
||||
x = media_right - badge_width
|
||||
spacing = (cell_area.height - badge_height * len(icon_paths)) / max(1, len(icon_paths) - 1)
|
||||
|
@ -215,9 +327,15 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
|
|||
|
||||
We call this at idle time after rendering a cell; this should keep all the surfaces
|
||||
rendered at that time, so during scrolling the visible media are kept and scrolling is smooth.
|
||||
At other times we may discard almost all surfaces, saving memory."""
|
||||
self.cached_surfaces_old = self.cached_surfaces_new
|
||||
self.cached_surfaces_new = {}
|
||||
At other times we may discard almost all surfaces, saving memory.
|
||||
|
||||
We skip clearing anything if no surfaces have been loaded; this happens if drawing was
|
||||
serviced entirely from cache. GTK may have redrawn just one image or something, so
|
||||
let's not disturb the cache for that."""
|
||||
if self.cached_surfaces_loaded > 0:
|
||||
self.cached_surfaces_old = self.cached_surfaces_new
|
||||
self.cached_surfaces_new = {}
|
||||
self.cached_surfaces_loaded = 0
|
||||
self.cycle_cache_idle_id = None
|
||||
|
||||
def get_cached_surface_by_path(self, widget, path, size=None, preserve_aspect_ratio=True):
|
||||
|
@ -231,14 +349,17 @@ class GridViewCellRendererImage(Gtk.CellRenderer):
|
|||
|
||||
key = widget, path, size, preserve_aspect_ratio
|
||||
|
||||
surface = self.cached_surfaces_new.get(key)
|
||||
if surface:
|
||||
return surface
|
||||
if key in self.cached_surfaces_new:
|
||||
return self.cached_surfaces_new[key]
|
||||
|
||||
surface = self.cached_surfaces_old.get(key)
|
||||
|
||||
if not surface:
|
||||
if key in self.cached_surfaces_old:
|
||||
surface = self.cached_surfaces_old[key]
|
||||
else:
|
||||
surface = self.get_surface_by_path(widget, path, size, preserve_aspect_ratio)
|
||||
# We cache missing surfaces too, but only a successful load trigger
|
||||
# cache cycling
|
||||
if surface:
|
||||
self.cached_surfaces_loaded += 1
|
||||
|
||||
self.cached_surfaces_new[key] = surface
|
||||
return surface
|
||||
|
|
|
@ -76,7 +76,7 @@ class SearchableCombobox(Gtk.Bin):
|
|||
|
||||
def _populate_combobox_choices_cb(self, _result, error):
|
||||
if error:
|
||||
ErrorDialog(str(error), parent=self.get_toplevel())
|
||||
ErrorDialog(error, parent=self.get_toplevel())
|
||||
|
||||
@staticmethod
|
||||
def _on_combobox_scroll(combobox, _event):
|
||||
|
|
|
@ -15,6 +15,7 @@ from lutris.gui.dialogs import ErrorDialog
|
|||
from lutris.gui.dialogs.runner_install import RunnerInstallDialog
|
||||
from lutris.gui.widgets.utils import has_stock_icon
|
||||
from lutris.installer.interpreter import ScriptInterpreter
|
||||
from lutris.runners import InvalidRunner
|
||||
from lutris.services import SERVICES
|
||||
from lutris.services.base import AuthTokenExpired, BaseService
|
||||
|
||||
|
@ -166,7 +167,7 @@ class ServiceSidebarRow(SidebarRow):
|
|||
self.service.logout()
|
||||
self.service.login(parent=self.get_toplevel())
|
||||
else:
|
||||
ErrorDialog(str(error), parent=self.get_toplevel())
|
||||
ErrorDialog(error, parent=self.get_toplevel())
|
||||
GLib.timeout_add(2000, self.enable_refresh_button)
|
||||
|
||||
def enable_refresh_button(self):
|
||||
|
@ -212,7 +213,11 @@ class RunnerSidebarRow(SidebarRow):
|
|||
|
||||
# Creation is delayed because only installed runners can be imported
|
||||
# and all visible boxes should be installed.
|
||||
self.runner = runners.import_runner(self.id)()
|
||||
try:
|
||||
self.runner = runners.import_runner(self.id)()
|
||||
except InvalidRunner:
|
||||
return entries
|
||||
|
||||
if self.runner.multiple_versions:
|
||||
entries.append((
|
||||
"system-software-install-symbolic",
|
||||
|
@ -243,7 +248,7 @@ class RunnerSidebarRow(SidebarRow):
|
|||
runner=self.runner, parent=self.get_toplevel())
|
||||
|
||||
def on_watched_error(self, error):
|
||||
dialogs.ErrorDialog(str(error), parent=self.get_toplevel())
|
||||
dialogs.ErrorDialog(error, parent=self.get_toplevel())
|
||||
|
||||
|
||||
class SidebarHeader(Gtk.Box):
|
||||
|
|
|
@ -40,7 +40,7 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
|
|||
|
||||
def report_error(self, error):
|
||||
"""Called to report an error during installation. The installation will then stop."""
|
||||
logger.exception("Error during installation: %s", str(error))
|
||||
logger.exception("Error during installation: %s", error)
|
||||
|
||||
def report_status(self, status):
|
||||
"""Called to report the current activity of the installer."""
|
||||
|
@ -223,6 +223,11 @@ class ScriptInterpreter(GObject.Object, CommandsMixin):
|
|||
_("Lutris does not have the necessary permissions to install to path:"),
|
||||
self.target_path,
|
||||
) from err
|
||||
except FileNotFoundError as err:
|
||||
raise ScriptingError(
|
||||
_("Path %s not found, unable to create game folder. Is the disk mounted?"),
|
||||
self.target_path,
|
||||
) from err
|
||||
|
||||
def get_runners_to_install(self):
|
||||
"""Check if the runner is installed before starting the installation
|
||||
|
|
|
@ -120,7 +120,10 @@ def get_runner_human_name(runner_name):
|
|||
names."""
|
||||
if runner_name:
|
||||
if runner_name not in _cached_runner_human_names:
|
||||
_cached_runner_human_names[runner_name] = import_runner(runner_name)().human_name
|
||||
try:
|
||||
_cached_runner_human_names[runner_name] = import_runner(runner_name)().human_name
|
||||
except InvalidRunner:
|
||||
_cached_runner_human_names[runner_name] = runner_name # an obsolete runner
|
||||
return _cached_runner_human_names[runner_name]
|
||||
|
||||
return ""
|
||||
|
|
|
@ -247,7 +247,7 @@ def wineexec( # noqa: C901
|
|||
exclude_processes = shlex.split(exclude_processes)
|
||||
|
||||
if not runner:
|
||||
runner = import_runner("wine")()
|
||||
runner = import_runner("wine")(prefix=prefix, working_dir=working_dir, wine_arch=arch)
|
||||
|
||||
if not wine_path:
|
||||
wine_path = runner.get_executable()
|
||||
|
|
|
@ -3,6 +3,7 @@ import os
|
|||
import shlex
|
||||
from gettext import gettext as _
|
||||
|
||||
from lutris import settings
|
||||
# Lutris Modules
|
||||
from lutris.runners.commands.dosbox import dosexec, makeconfig # NOQA pylint: disable=unused-import
|
||||
from lutris.runners.runner import Runner
|
||||
|
@ -14,8 +15,8 @@ class dosbox(Runner):
|
|||
description = _("MS-DOS emulator")
|
||||
platforms = [_("MS-DOS")]
|
||||
runnable_alone = True
|
||||
runner_executable = "dosbox/bin/dosbox"
|
||||
require_libs = ["libopusfile.so.0", ]
|
||||
runner_executable = "dosbox/dosbox"
|
||||
require_libs = []
|
||||
game_options = [
|
||||
{
|
||||
"option": "main_file",
|
||||
|
@ -120,6 +121,23 @@ class dosbox(Runner):
|
|||
def main_file(self):
|
||||
return self.make_absolute(self.game_config.get("main_file"))
|
||||
|
||||
@property
|
||||
def libs_dir(self):
|
||||
path = os.path.join(settings.RUNNER_DIR, "dosbox/lib")
|
||||
return path if system.path_exists(path) else ""
|
||||
|
||||
def get_command(self):
|
||||
return [
|
||||
self.get_executable(),
|
||||
]
|
||||
|
||||
def get_run_data(self):
|
||||
env = self.get_env()
|
||||
env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
|
||||
self.libs_dir,
|
||||
env.get("LD_LIBRARY_PATH")]))
|
||||
return {"env": env, "command": self.get_command()}
|
||||
|
||||
@property
|
||||
def working_dir(self):
|
||||
"""Return the working directory to use when running the game."""
|
||||
|
@ -162,4 +180,4 @@ class dosbox(Runner):
|
|||
if args:
|
||||
command.extend(args)
|
||||
|
||||
return {"command": command}
|
||||
return {"command": command, "ld_library_path": self.libs_dir}
|
||||
|
|
|
@ -31,6 +31,7 @@ class JsonRunner(Runner):
|
|||
self.system_options_override = self._json_data.get("system_options_override", [])
|
||||
self.entry_point_option = self._json_data.get("entry_point_option", "main_file")
|
||||
self.download_url = self._json_data.get("download_url")
|
||||
self.runnable_alone = self._json_data.get("runnable_alone")
|
||||
|
||||
def play(self):
|
||||
"""Return a launchable command constructed from the options"""
|
||||
|
|
|
@ -37,19 +37,7 @@ class pcsx2(Runner):
|
|||
"type": "bool",
|
||||
"label": _("No GUI"),
|
||||
"default": False
|
||||
},
|
||||
{
|
||||
"option": "config_file",
|
||||
"type": "file",
|
||||
"label": _("Custom config file"),
|
||||
"advanced": True,
|
||||
},
|
||||
{
|
||||
"option": "config_path",
|
||||
"type": "directory_chooser",
|
||||
"label": _("Custom config path"),
|
||||
"advanced": True,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
# PCSX2 currently uses an AppImage, no need for the runtime.
|
||||
|
@ -59,15 +47,11 @@ class pcsx2(Runner):
|
|||
arguments = [self.get_executable()]
|
||||
|
||||
if self.runner_config.get("fullscreen"):
|
||||
arguments.append("--fullscreen")
|
||||
arguments.append("-fullscreen")
|
||||
if self.runner_config.get("full_boot"):
|
||||
arguments.append("--fullboot")
|
||||
arguments.append("-slowboot")
|
||||
if self.runner_config.get("nogui"):
|
||||
arguments.append("--nogui")
|
||||
if self.runner_config.get("config_file"):
|
||||
arguments.append("--cfg={}".format(self.runner_config["config_file"]))
|
||||
if self.runner_config.get("config_path"):
|
||||
arguments.append("--cfgpath={}".format(self.runner_config["config_path"]))
|
||||
arguments.append("-nogui")
|
||||
|
||||
iso = self.game_config.get("main_file") or ""
|
||||
if not system.path_exists(iso):
|
||||
|
|
|
@ -13,14 +13,13 @@ from lutris.runners.commands.wine import ( # noqa: F401 pylint: disable=unused-
|
|||
from lutris.runners.runner import Runner
|
||||
from lutris.util import system
|
||||
from lutris.util.display import DISPLAY_MANAGER, get_default_dpi
|
||||
from lutris.util.graphics.vkquery import is_vulkan_supported
|
||||
from lutris.util.jobs import thread_safe_call
|
||||
from lutris.util.graphics import vkquery
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.steam.config import get_steam_dir
|
||||
from lutris.util.strings import parse_version, split_arguments
|
||||
from lutris.util.wine.d3d_extras import D3DExtrasManager
|
||||
from lutris.util.wine.dgvoodoo2 import dgvoodoo2Manager
|
||||
from lutris.util.wine.dxvk import DXVKManager
|
||||
from lutris.util.wine.dxvk import REQUIRED_VULKAN_API_VERSION, DXVKManager
|
||||
from lutris.util.wine.dxvk_nvapi import DXVKNVAPIManager
|
||||
from lutris.util.wine.extract_icon import PEFILE_AVAILABLE, ExtractIcon
|
||||
from lutris.util.wine.prefix import DEFAULT_DLL_OVERRIDES, WinePrefixManager, find_prefix
|
||||
|
@ -36,6 +35,115 @@ DEFAULT_WINE_PREFIX = "~/.wine"
|
|||
MIN_SAFE_VERSION = "7.0" # Wine installers must run with at least this version
|
||||
|
||||
|
||||
def _get_prefix_warning(config):
|
||||
if config.get("prefix"):
|
||||
return None
|
||||
|
||||
exe = config.get("exe")
|
||||
if exe and find_prefix(exe):
|
||||
return None
|
||||
|
||||
return _("Some Wine configuration options cannot be applied, if no prefix can be found.")
|
||||
|
||||
|
||||
def _get_dxvk_warning(config):
|
||||
if config.get("dxvk") and not vkquery.is_vulkan_supported():
|
||||
return _("Vulkan is not installed or is not supported by your system")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_dxvk_version_warning(config):
|
||||
if config.get("dxvk") and vkquery.is_vulkan_supported():
|
||||
version = config.get("dxvk_version")
|
||||
if version and not version.startswith("v1."):
|
||||
required_api_version = REQUIRED_VULKAN_API_VERSION
|
||||
library_api_version = vkquery.get_vulkan_api_version()
|
||||
if library_api_version and library_api_version < required_api_version:
|
||||
return _("<b>Warning</b> Lutris has detected that Vulkan API version %s is installed, "
|
||||
"but to use the latest DXVK version, %s is required."
|
||||
) % (
|
||||
vkquery.format_version(library_api_version),
|
||||
vkquery.format_version(required_api_version)
|
||||
)
|
||||
|
||||
devices = vkquery.get_device_info()
|
||||
|
||||
if devices and devices[0].api_version < required_api_version:
|
||||
return _(
|
||||
"<b>Warning</b> Lutris has detected that the best device available ('%s') supports Vulkan API %s, "
|
||||
"but to use the latest DXVK version, %s is required."
|
||||
) % (
|
||||
devices[0].name,
|
||||
vkquery.format_version(devices[0].api_version),
|
||||
vkquery.format_version(required_api_version)
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_vkd3d_warning(config):
|
||||
if config.get("vkd3d"):
|
||||
if not vkquery.is_vulkan_supported():
|
||||
return _("<b>Warning</b> Vulkan is not installed or is not supported by your system")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_path_for_version(config, version=None):
|
||||
"""Return the absolute path of a wine executable for a given version,
|
||||
or the configured version if you don't ask for a version."""
|
||||
if not version:
|
||||
version = config["version"]
|
||||
|
||||
if version in WINE_PATHS:
|
||||
return system.find_executable(WINE_PATHS[version])
|
||||
if "Proton" in version:
|
||||
for proton_path in get_proton_paths():
|
||||
if os.path.isfile(os.path.join(proton_path, version, "dist/bin/wine")):
|
||||
return os.path.join(proton_path, version, "dist/bin/wine")
|
||||
if os.path.isfile(os.path.join(proton_path, version, "files/bin/wine")):
|
||||
return os.path.join(proton_path, version, "files/bin/wine")
|
||||
if version.startswith("PlayOnLinux"):
|
||||
version, arch = version.split()[1].rsplit("-", 1)
|
||||
return os.path.join(POL_PATH, "wine", "linux-" + arch, version, "bin/wine")
|
||||
if version == "custom":
|
||||
return config.get("custom_wine_path", "")
|
||||
return os.path.join(WINE_DIR, version, "bin/wine")
|
||||
|
||||
|
||||
def _get_esync_warning(config):
|
||||
if config.get("esync"):
|
||||
limits_set = is_esync_limit_set()
|
||||
wine_path = _get_path_for_version(config)
|
||||
wine_ver = is_version_esync(wine_path)
|
||||
|
||||
if not wine_ver:
|
||||
return _("<b>Warning</b> The Wine build you have selected does not support Esync")
|
||||
|
||||
if not limits_set:
|
||||
return _("<b>Warning</b> Your limits are not set correctly. Please increase them as described here:\n"
|
||||
"<a href='https://github.com/lutris/docs/blob/master/HowToEsync.md'>"
|
||||
"How-to-Esync (https://github.com/lutris/docs/blob/master/HowToEsync.md)</a>")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_fsync_warning(config):
|
||||
if config.get("fsync"):
|
||||
fsync_supported = is_fsync_supported()
|
||||
wine_path = _get_path_for_version(config)
|
||||
wine_ver = is_version_fsync(wine_path)
|
||||
|
||||
if not wine_ver:
|
||||
return _("<b>Warning</b> The Wine build you have selected does not support Fsync.")
|
||||
|
||||
if not fsync_supported:
|
||||
return _("<b>Warning</b> Your kernel is not patched for fsync.")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class wine(Runner):
|
||||
description = _("Runs Windows games")
|
||||
human_name = _("Wine")
|
||||
|
@ -71,6 +179,7 @@ class wine(Runner):
|
|||
"option": "prefix",
|
||||
"type": "directory_chooser",
|
||||
"label": _("Wine prefix"),
|
||||
"warning": _get_prefix_warning,
|
||||
"help": _(
|
||||
'The prefix used by Wine.\n'
|
||||
"It's a directory containing a set of files and "
|
||||
|
@ -106,8 +215,11 @@ class wine(Runner):
|
|||
"wineboot.exe",
|
||||
)
|
||||
|
||||
def __init__(self, config=None): # noqa: C901
|
||||
def __init__(self, config=None, prefix=None, working_dir=None, wine_arch=None): # noqa: C901
|
||||
super().__init__(config)
|
||||
self._prefix = prefix
|
||||
self._working_dir = working_dir
|
||||
self._wine_arch = wine_arch
|
||||
self.dll_overrides = DEFAULT_DLL_OVERRIDES.copy() # we'll modify this, so we better copy it
|
||||
|
||||
def get_wine_version_choices():
|
||||
|
@ -128,43 +240,6 @@ class wine(Runner):
|
|||
version_choices.append((label, version))
|
||||
return version_choices
|
||||
|
||||
def esync_limit_callback(widget, option, config):
|
||||
limits_set = is_esync_limit_set()
|
||||
wine_path = self.get_path_for_version(config["version"])
|
||||
wine_ver = is_version_esync(wine_path)
|
||||
response = True
|
||||
|
||||
if not wine_ver:
|
||||
response = thread_safe_call(esync_display_version_warning)
|
||||
|
||||
if not limits_set:
|
||||
thread_safe_call(esync_display_limit_warning)
|
||||
response = False
|
||||
|
||||
return widget, option, response
|
||||
|
||||
def fsync_support_callback(widget, option, config):
|
||||
fsync_supported = is_fsync_supported()
|
||||
wine_path = self.get_path_for_version(config["version"])
|
||||
wine_ver = is_version_fsync(wine_path)
|
||||
response = True
|
||||
|
||||
if not wine_ver:
|
||||
response = thread_safe_call(fsync_display_version_warning)
|
||||
|
||||
if not fsync_supported:
|
||||
thread_safe_call(fsync_display_support_warning)
|
||||
response = False
|
||||
|
||||
return widget, option, response
|
||||
|
||||
def dxvk_vulkan_callback(widget, option, config):
|
||||
response = True
|
||||
if not is_vulkan_supported():
|
||||
if not thread_safe_call(display_vulkan_error):
|
||||
response = False
|
||||
return widget, option, response
|
||||
|
||||
self.runner_options = [
|
||||
{
|
||||
"option": "version",
|
||||
|
@ -198,10 +273,9 @@ class wine(Runner):
|
|||
"option": "dxvk",
|
||||
"section": _("Graphics"),
|
||||
"label": _("Enable DXVK"),
|
||||
"type": "extended_bool",
|
||||
"callback": dxvk_vulkan_callback,
|
||||
"callback_on": True,
|
||||
"type": "bool",
|
||||
"default": True,
|
||||
"warning": _get_dxvk_warning,
|
||||
"active": True,
|
||||
"help": _(
|
||||
"Use DXVK to "
|
||||
|
@ -216,15 +290,15 @@ class wine(Runner):
|
|||
"type": "choice_with_entry",
|
||||
"choices": DXVKManager().version_choices,
|
||||
"default": DXVKManager().version,
|
||||
"warning": _get_dxvk_version_warning
|
||||
},
|
||||
|
||||
{
|
||||
"option": "vkd3d",
|
||||
"section": _("Graphics"),
|
||||
"label": _("Enable VKD3D"),
|
||||
"type": "extended_bool",
|
||||
"callback": dxvk_vulkan_callback,
|
||||
"callback_on": True,
|
||||
"type": "bool",
|
||||
"warning": _get_vkd3d_warning,
|
||||
"default": True,
|
||||
"active": True,
|
||||
"help": _(
|
||||
|
@ -306,9 +380,8 @@ class wine(Runner):
|
|||
{
|
||||
"option": "esync",
|
||||
"label": _("Enable Esync"),
|
||||
"type": "extended_bool",
|
||||
"callback": esync_limit_callback,
|
||||
"callback_on": True,
|
||||
"type": "bool",
|
||||
"warning": _get_esync_warning,
|
||||
"active": True,
|
||||
"default": True,
|
||||
"help": _(
|
||||
|
@ -320,10 +393,9 @@ class wine(Runner):
|
|||
{
|
||||
"option": "fsync",
|
||||
"label": _("Enable Fsync"),
|
||||
"type": "extended_bool",
|
||||
"type": "bool",
|
||||
"default": is_fsync_supported(),
|
||||
"callback": fsync_support_callback,
|
||||
"callback_on": True,
|
||||
"warning": _get_fsync_warning,
|
||||
"active": True,
|
||||
"help": _(
|
||||
"Enable futex-based synchronization (fsync). "
|
||||
|
@ -517,14 +589,26 @@ class wine(Runner):
|
|||
|
||||
@property
|
||||
def prefix_path(self):
|
||||
"""Return the absolute path of the Wine prefix"""
|
||||
_prefix_path = self.game_config.get("prefix") or os.environ.get("WINEPREFIX")
|
||||
"""Return the absolute path of the Wine prefix. Falls back to default WINE prefix."""
|
||||
_prefix_path = self._get_raw_prefix_path()
|
||||
if not _prefix_path:
|
||||
logger.warning("No WINE prefix provided, falling back to system default WINE prefix.")
|
||||
_prefix_path = DEFAULT_WINE_PREFIX
|
||||
return os.path.expanduser(_prefix_path)
|
||||
|
||||
@property
|
||||
def prefix_path_if_provided(self):
|
||||
"""Return the absolute path of the Wine prefix, if known. None if not."""
|
||||
_prefix_path = self._get_raw_prefix_path()
|
||||
if _prefix_path:
|
||||
return os.path.expanduser(_prefix_path)
|
||||
|
||||
def _get_raw_prefix_path(self):
|
||||
_prefix_path = self._prefix or self.game_config.get("prefix") or os.environ.get("WINEPREFIX")
|
||||
if not _prefix_path and self.game_config.get("exe"):
|
||||
# Find prefix from game if we have one
|
||||
_prefix_path = find_prefix(self.game_exe)
|
||||
if not _prefix_path:
|
||||
_prefix_path = DEFAULT_WINE_PREFIX
|
||||
return os.path.expanduser(_prefix_path)
|
||||
return _prefix_path
|
||||
|
||||
@property
|
||||
def game_exe(self):
|
||||
|
@ -544,9 +628,9 @@ class wine(Runner):
|
|||
@property
|
||||
def working_dir(self):
|
||||
"""Return the working directory to use when running the game."""
|
||||
option = self.game_config.get("working_dir")
|
||||
if option:
|
||||
return option
|
||||
_working_dir = self._working_dir or self.game_config.get("working_dir")
|
||||
if _working_dir:
|
||||
return _working_dir
|
||||
if self.game_exe:
|
||||
game_dir = os.path.dirname(self.game_exe)
|
||||
if os.path.isdir(game_dir):
|
||||
|
@ -563,7 +647,7 @@ class wine(Runner):
|
|||
"""Return the wine architecture.
|
||||
|
||||
Get it from the config or detect it from the prefix"""
|
||||
arch = self.game_config.get("arch") or "auto"
|
||||
arch = self._wine_arch or self.game_config.get("arch") or "auto"
|
||||
if arch not in ("win32", "win64"):
|
||||
arch = detect_arch(self.prefix_path, self.get_executable())
|
||||
return arch
|
||||
|
@ -588,20 +672,7 @@ class wine(Runner):
|
|||
|
||||
def get_path_for_version(self, version):
|
||||
"""Return the absolute path of a wine executable for a given version"""
|
||||
if version in WINE_PATHS:
|
||||
return system.find_executable(WINE_PATHS[version])
|
||||
if "Proton" in version:
|
||||
for proton_path in get_proton_paths():
|
||||
if os.path.isfile(os.path.join(proton_path, version, "dist/bin/wine")):
|
||||
return os.path.join(proton_path, version, "dist/bin/wine")
|
||||
if os.path.isfile(os.path.join(proton_path, version, "files/bin/wine")):
|
||||
return os.path.join(proton_path, version, "files/bin/wine")
|
||||
if version.startswith("PlayOnLinux"):
|
||||
version, arch = version.split()[1].rsplit("-", 1)
|
||||
return os.path.join(POL_PATH, "wine", "linux-" + arch, version, "bin/wine")
|
||||
if version == "custom":
|
||||
return self.runner_config.get("custom_wine_path", "")
|
||||
return os.path.join(WINE_DIR, version, "bin/wine")
|
||||
return _get_path_for_version(self.runner_config, version)
|
||||
|
||||
def resolve_config_path(self, path, relative_to=None):
|
||||
# Resolve paths with tolerance for Windows-isms;
|
||||
|
@ -780,35 +851,37 @@ class wine(Runner):
|
|||
|
||||
def set_regedit_keys(self):
|
||||
"""Reset regedit keys according to config."""
|
||||
prefix_manager = WinePrefixManager(self.prefix_path)
|
||||
# Those options are directly changed with the prefix manager and skip
|
||||
# any calls to regedit.
|
||||
managed_keys = {
|
||||
"ShowCrashDialog": prefix_manager.set_crash_dialogs,
|
||||
"Desktop": prefix_manager.set_virtual_desktop,
|
||||
"WineDesktop": prefix_manager.set_desktop_size,
|
||||
}
|
||||
prefix = self.prefix_path_if_provided
|
||||
if prefix:
|
||||
prefix_manager = WinePrefixManager(prefix)
|
||||
# Those options are directly changed with the prefix manager and skip
|
||||
# any calls to regedit.
|
||||
managed_keys = {
|
||||
"ShowCrashDialog": prefix_manager.set_crash_dialogs,
|
||||
"Desktop": prefix_manager.set_virtual_desktop,
|
||||
"WineDesktop": prefix_manager.set_desktop_size,
|
||||
}
|
||||
|
||||
for key, path in self.reg_keys.items():
|
||||
value = self.runner_config.get(key) or "auto"
|
||||
if not value or value == "auto" and key not in managed_keys:
|
||||
prefix_manager.clear_registry_subkeys(path, key)
|
||||
elif key in self.runner_config:
|
||||
if key in managed_keys:
|
||||
# Do not pass fallback 'auto' value to managed keys
|
||||
if value == "auto":
|
||||
value = None
|
||||
managed_keys[key](value)
|
||||
continue
|
||||
# Convert numeric strings to integers so they are saved as dword
|
||||
if value.isdigit():
|
||||
value = int(value)
|
||||
for key, path in self.reg_keys.items():
|
||||
value = self.runner_config.get(key) or "auto"
|
||||
if not value or value == "auto" and key not in managed_keys:
|
||||
prefix_manager.clear_registry_subkeys(path, key)
|
||||
elif key in self.runner_config:
|
||||
if key in managed_keys:
|
||||
# Do not pass fallback 'auto' value to managed keys
|
||||
if value == "auto":
|
||||
value = None
|
||||
managed_keys[key](value)
|
||||
continue
|
||||
# Convert numeric strings to integers so they are saved as dword
|
||||
if value.isdigit():
|
||||
value = int(value)
|
||||
|
||||
prefix_manager.set_registry_key(path, key, value)
|
||||
prefix_manager.set_registry_key(path, key, value)
|
||||
|
||||
# We always configure the DPI, because if the user turns off DPI scaling, but it
|
||||
# had been on the only way to implement that is to save 96 DPI into the registry.
|
||||
prefix_manager.set_dpi(self.get_dpi())
|
||||
# We always configure the DPI, because if the user turns off DPI scaling, but it
|
||||
# had been on the only way to implement that is to save 96 DPI into the registry.
|
||||
prefix_manager.set_dpi(self.get_dpi())
|
||||
|
||||
def get_dpi(self):
|
||||
"""Return the DPI to be used by Wine; returns None to allow Wine's own
|
||||
|
@ -817,10 +890,11 @@ class wine(Runner):
|
|||
explicit_dpi = self.runner_config.get("ExplicitDpi")
|
||||
if explicit_dpi == "auto":
|
||||
explicit_dpi = None
|
||||
try:
|
||||
explicit_dpi = int(explicit_dpi)
|
||||
except:
|
||||
explicit_dpi = None
|
||||
else:
|
||||
try:
|
||||
explicit_dpi = int(explicit_dpi)
|
||||
except:
|
||||
explicit_dpi = None
|
||||
return explicit_dpi or get_default_dpi()
|
||||
|
||||
return None
|
||||
|
@ -830,15 +904,17 @@ class wine(Runner):
|
|||
logger.warning("No valid prefix detected in %s, creating one...", self.prefix_path)
|
||||
create_prefix(self.prefix_path, wine_path=self.get_executable(), arch=self.wine_arch, runner=self)
|
||||
|
||||
prefix_manager = WinePrefixManager(self.prefix_path)
|
||||
if self.runner_config.get("autoconf_joypad", False):
|
||||
prefix_manager.configure_joypads()
|
||||
prefix_manager.create_user_symlinks()
|
||||
self.sandbox(prefix_manager)
|
||||
self.set_regedit_keys()
|
||||
prefix = self.prefix_path_if_provided
|
||||
if prefix:
|
||||
prefix_manager = WinePrefixManager(prefix)
|
||||
if self.runner_config.get("autoconf_joypad", False):
|
||||
prefix_manager.configure_joypads()
|
||||
prefix_manager.create_user_symlinks()
|
||||
self.sandbox(prefix_manager)
|
||||
self.set_regedit_keys()
|
||||
|
||||
for manager, enabled in self.get_dll_managers().items():
|
||||
manager.setup(enabled)
|
||||
for manager, enabled in self.get_dll_managers().items():
|
||||
manager.setup(enabled)
|
||||
|
||||
def get_dll_managers(self, enabled_only=False):
|
||||
"""Returns the DLL managers in a dict; the keys are the managers themselves,
|
||||
|
@ -853,17 +929,19 @@ class wine(Runner):
|
|||
]
|
||||
|
||||
managers = {}
|
||||
prefix = self.prefix_path_if_provided
|
||||
|
||||
for manager_class, enabled_option, version_option in manager_classes:
|
||||
enabled = bool(self.runner_config.get(enabled_option))
|
||||
version = self.runner_config.get(version_option)
|
||||
if enabled or not enabled_only:
|
||||
manager = manager_class(
|
||||
self.prefix_path,
|
||||
arch=self.wine_arch,
|
||||
version=version
|
||||
)
|
||||
managers[manager] = enabled
|
||||
if prefix:
|
||||
for manager_class, enabled_option, version_option in manager_classes:
|
||||
enabled = bool(self.runner_config.get(enabled_option))
|
||||
version = self.runner_config.get(version_option)
|
||||
if enabled or not enabled_only:
|
||||
manager = manager_class(
|
||||
prefix,
|
||||
arch=self.wine_arch,
|
||||
version=version
|
||||
)
|
||||
managers[manager] = enabled
|
||||
|
||||
return managers
|
||||
|
||||
|
@ -989,7 +1067,7 @@ class wine(Runner):
|
|||
if using_dxvk:
|
||||
# Set this to 1 to enable access to more RAM for 32bit applications
|
||||
launch_info["env"]["WINE_LARGE_ADDRESS_AWARE"] = "1"
|
||||
if not is_vulkan_supported():
|
||||
if not vkquery.is_vulkan_supported():
|
||||
if not display_vulkan_error(on_launch=True):
|
||||
return {"error": "VULKAN_NOT_FOUND"}
|
||||
|
||||
|
|
|
@ -108,12 +108,16 @@ def get_path_from_config(game):
|
|||
if not game.config:
|
||||
logger.warning("Game %s has no configuration", game)
|
||||
return ""
|
||||
game_config = game.config.game_config
|
||||
|
||||
# Skip MAME roms referenced by their ID
|
||||
if game.runner_name == "mame" and "." not in game.config.game_config["main_file"]:
|
||||
return
|
||||
if game.runner_name == "mame":
|
||||
if "main_file" in game_config and "." not in game_config["main_file"]:
|
||||
return ""
|
||||
|
||||
for key in ["exe", "main_file", "iso", "rom", "disk-a", "path", "files"]:
|
||||
if key in game.config.game_config:
|
||||
path = game.config.game_config[key]
|
||||
if key in game_config:
|
||||
path = game_config[key]
|
||||
if key == "files":
|
||||
path = path[0]
|
||||
if not path.startswith("/"):
|
||||
|
@ -176,11 +180,15 @@ def remove_from_path_cache(game):
|
|||
def get_path_cache():
|
||||
"""Return the contents of the path cache file"""
|
||||
with open(GAME_PATH_CACHE_PATH, encoding="utf-8") as cache_file:
|
||||
return json.load(cache_file)
|
||||
try:
|
||||
return json.load(cache_file)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
|
||||
def get_missing_game_ids():
|
||||
"""Return a list of IDs for games that can't be found"""
|
||||
logger.debug("Checking for missing games")
|
||||
missing_ids = []
|
||||
for game_id, path in get_path_cache().items():
|
||||
if not os.path.exists(path):
|
||||
|
|
|
@ -437,7 +437,7 @@ class AmazonService(OnlineService):
|
|||
if not response:
|
||||
logger.error("There was an error getting game manifest: %s", game_id)
|
||||
raise UnavailableGameError(_(
|
||||
"Unable to get game manifest info, please check your Amazon credentials and internet connectivity"))
|
||||
"Unable to get game manifest info"))
|
||||
|
||||
return response
|
||||
|
||||
|
@ -455,7 +455,7 @@ class AmazonService(OnlineService):
|
|||
except HTTPError as ex:
|
||||
logger.error("Failed http request %s", url)
|
||||
raise UnavailableGameError(_(
|
||||
"Unable to get game manifest, please check your Amazon credentials and internet connectivity")) from ex
|
||||
"Unable to get game manifest")) from ex
|
||||
|
||||
content = request.content
|
||||
|
||||
|
@ -471,8 +471,7 @@ class AmazonService(OnlineService):
|
|||
else:
|
||||
logger.error("Unknown compression algorithm found in manifest")
|
||||
raise UnavailableGameError(_(
|
||||
"Unknown compression algorithm found in manifest, "
|
||||
"please check your Amazon credentials and internet connectivity"))
|
||||
"Unknown compression algorithm found in manifest"))
|
||||
|
||||
manifest = Manifest()
|
||||
manifest.decode(raw_manifest)
|
||||
|
@ -532,11 +531,13 @@ class AmazonService(OnlineService):
|
|||
hashes.append(file_hash)
|
||||
files.append({"path": file.path.decode().replace("\\", "/"), "size": file.size, "url": None})
|
||||
|
||||
hashpairs.append(dict(
|
||||
sourceHash=None,
|
||||
targetHash=dict(value=file_hash,
|
||||
algorithm=HashAlgorithm.get_name(file.hash.algorithm)),
|
||||
))
|
||||
hashpairs.append({
|
||||
'sourceHash': None,
|
||||
'targetHash': {
|
||||
'value': file_hash,
|
||||
'algorithm': HashAlgorithm.get_name(file.hash.algorithm)
|
||||
}
|
||||
})
|
||||
for __, directory in enumerate(package.dirs):
|
||||
if directory.path is not None:
|
||||
directories.append(directory.path.decode().replace("\\", "/"))
|
||||
|
@ -572,7 +573,7 @@ class AmazonService(OnlineService):
|
|||
except HTTPError as ex:
|
||||
logger.error("Failed http request %s", fuel_url)
|
||||
raise UnavailableGameError(_(
|
||||
"Unable to get fuel.json file, please check your Amazon credentials and internet connectivity")) from ex
|
||||
"Unable to get fuel.json file, please check your Amazon credentials")) from ex
|
||||
|
||||
try:
|
||||
res_yaml_text = request.text
|
||||
|
|
|
@ -221,18 +221,23 @@ class EpicGamesStoreService(OnlineService):
|
|||
|
||||
def start_session(self, exchange_code=None, authorization_code=None):
|
||||
if exchange_code:
|
||||
params = dict(grant_type='exchange_code',
|
||||
exchange_code=exchange_code,
|
||||
token_type='eg1')
|
||||
params = {
|
||||
'grant_type': 'exchange_code',
|
||||
'exchange_code': exchange_code,
|
||||
'token_type': 'eg1'
|
||||
}
|
||||
elif authorization_code:
|
||||
params = dict(grant_type='authorization_code',
|
||||
code=authorization_code,
|
||||
token_type='eg1')
|
||||
params = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': authorization_code,
|
||||
'token_type': 'eg1'
|
||||
}
|
||||
else:
|
||||
params = dict(grant_type='refresh_token',
|
||||
refresh_token=self.session_data["refresh_token"],
|
||||
|
||||
token_type='eg1')
|
||||
params = {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': self.session_data["refresh_token"],
|
||||
'token_type': 'eg1'
|
||||
}
|
||||
|
||||
response = self.session.post(
|
||||
'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token',
|
||||
|
|
|
@ -226,7 +226,7 @@ class GOGService(OnlineService):
|
|||
request.get()
|
||||
except HTTPError:
|
||||
logger.error(
|
||||
"Failed to request %s, check your GOG credentials and internet connectivity",
|
||||
"Failed to request %s, check your GOG credentials",
|
||||
url,
|
||||
)
|
||||
return
|
||||
|
|
|
@ -133,7 +133,7 @@ class HumbleBundleService(OnlineService):
|
|||
request.get()
|
||||
except HTTPError:
|
||||
logger.error(
|
||||
"Failed to request %s, check your Humble Bundle credentials and internet connectivity",
|
||||
"Failed to request %s, check your Humble Bundle credentials",
|
||||
url,
|
||||
)
|
||||
return
|
||||
|
|
|
@ -20,11 +20,12 @@ from lutris.runtime import RuntimeUpdater
|
|||
from lutris.scanners.lutris import build_path_cache
|
||||
from lutris.services import DEFAULT_SERVICES
|
||||
from lutris.services.lutris import sync_media
|
||||
from lutris.util.display import USE_DRI_PRIME
|
||||
from lutris.util.graphics import drivers, vkquery
|
||||
from lutris.util.linux import LINUX_SYSTEM
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.steam.shortcut import update_all_artwork
|
||||
from lutris.util.system import create_folder
|
||||
from lutris.util.system import create_folder, preload_vulkan_gpu_names
|
||||
from lutris.util.wine.d3d_extras import D3DExtrasManager
|
||||
from lutris.util.wine.dgvoodoo2 import dgvoodoo2Manager
|
||||
from lutris.util.wine.dxvk import REQUIRED_VULKAN_API_VERSION, DXVKManager
|
||||
|
@ -143,51 +144,21 @@ def check_vulkan():
|
|||
logger.warning("Vulkan is not available or your system isn't Vulkan capable")
|
||||
else:
|
||||
required_api_version = REQUIRED_VULKAN_API_VERSION
|
||||
library_api_version = vkquery.get_vulkan_api_version_tuple()
|
||||
library_api_version = vkquery.get_vulkan_api_version()
|
||||
if library_api_version and library_api_version < required_api_version:
|
||||
logger.warning("Vulkan reports an API version of %s. "
|
||||
"%s is required for the latest DXVK.",
|
||||
vkquery.format_version_tuple(library_api_version),
|
||||
vkquery.format_version_tuple(library_api_version))
|
||||
setting = "dismiss-obsolete-vulkan-api-warning"
|
||||
if settings.read_setting(setting) != "True":
|
||||
DontShowAgainDialog(
|
||||
setting,
|
||||
_("Obsolete Vulkan libraries"),
|
||||
secondary_message=_(
|
||||
"Lutris has detected that Vulkan API version %s is installed, "
|
||||
"but to use the latest DXVK version, %s is required.\n\n"
|
||||
"DXVK 1.x will be used instead."
|
||||
) % (
|
||||
vkquery.format_version_tuple(library_api_version),
|
||||
vkquery.format_version_tuple(required_api_version)
|
||||
)
|
||||
)
|
||||
return
|
||||
vkquery.format_version(library_api_version),
|
||||
vkquery.format_version(required_api_version))
|
||||
|
||||
max_dev_name, max_dev_api_version = vkquery.get_best_device_info()
|
||||
devices = vkquery.get_device_info()
|
||||
|
||||
if max_dev_api_version and max_dev_api_version < required_api_version:
|
||||
if devices and devices[0].api_version < required_api_version:
|
||||
logger.warning("Vulkan reports that the '%s' device has API version of %s. "
|
||||
"%s is required for the latest DXVK.",
|
||||
max_dev_name,
|
||||
vkquery.format_version_tuple(max_dev_api_version),
|
||||
vkquery.format_version_tuple(required_api_version))
|
||||
setting = "dismiss-obsolete-vulkan-api-warning"
|
||||
if settings.read_setting(setting) != "True":
|
||||
DontShowAgainDialog(
|
||||
setting,
|
||||
_("Obsolete Vulkan driver support"),
|
||||
secondary_message=_(
|
||||
"Lutris has detected that the best device available ('%s') supports Vulkan API %s, "
|
||||
"but to use the latest DXVK version, %s is required.\n\n"
|
||||
"DXVK 1.x will be used instead."
|
||||
) % (
|
||||
max_dev_name,
|
||||
vkquery.format_version_tuple(max_dev_api_version),
|
||||
vkquery.format_version_tuple(required_api_version)
|
||||
)
|
||||
)
|
||||
devices[0].name,
|
||||
vkquery.format_version(devices[0].api_version),
|
||||
vkquery.format_version(required_api_version))
|
||||
|
||||
|
||||
def check_gnome():
|
||||
|
@ -219,6 +190,7 @@ def run_all_checks():
|
|||
check_libs()
|
||||
check_vulkan()
|
||||
check_gnome()
|
||||
preload_vulkan_gpu_names(USE_DRI_PRIME)
|
||||
fill_missing_platforms()
|
||||
build_path_cache()
|
||||
|
||||
|
|
|
@ -1,30 +1,12 @@
|
|||
"""Options list for system config."""
|
||||
import functools
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections import OrderedDict
|
||||
from gettext import gettext as _
|
||||
|
||||
from lutris import runners
|
||||
from lutris.util import linux, system
|
||||
from lutris.util.display import DISPLAY_MANAGER, SCREEN_SAVER_INHIBITOR, USE_DRI_PRIME
|
||||
from lutris.util.log import logger
|
||||
|
||||
# vulkan dirs used by distros or containers that aren't from:
|
||||
# https://github.com/KhronosGroup/Vulkan-Loader/blob/v1.3.235/docs/LoaderDriverInterface.md#driver-discovery-on-linux
|
||||
# don't include the /vulkan suffix
|
||||
FALLBACK_VULKAN_DATA_DIRS = [
|
||||
"/usr/local/etc", # standard site-local location
|
||||
"/usr/local/share", # standard site-local location
|
||||
"/etc", # standard location
|
||||
"/usr/share", # standard location
|
||||
"/usr/lib/x86_64-linux-gnu/GL", # Flatpak GL extension
|
||||
"/usr/lib/i386-linux-gnu/GL", # Flatpak GL32 extension
|
||||
"/opt/amdgpu-pro/etc" # AMD GPU Pro - TkG
|
||||
]
|
||||
from lutris.util.system import get_vk_icd_file_sets, get_vulkan_gpu_name
|
||||
|
||||
|
||||
def get_resolution_choices():
|
||||
|
@ -89,128 +71,17 @@ def get_optirun_choices():
|
|||
return choices
|
||||
|
||||
|
||||
# cache this to avoid calling vulkaninfo repeatedly, shouldn't change at runtime
|
||||
@functools.lru_cache
|
||||
def get_vulkan_gpus(icd_files):
|
||||
"""Runs vulkaninfo to determine the default and DRI_PRIME gpu if available"""
|
||||
|
||||
if not shutil.which("vulkaninfo"):
|
||||
logger.warning("vulkaninfo not available, unable to list GPUs")
|
||||
return "Unknown GPU"
|
||||
|
||||
gpu = get_vulkan_gpu(icd_files, False)
|
||||
if USE_DRI_PRIME:
|
||||
prime_gpu = get_vulkan_gpu(icd_files, True)
|
||||
if prime_gpu != gpu:
|
||||
gpu += f" (Discrete GPU: {prime_gpu})"
|
||||
return gpu
|
||||
|
||||
|
||||
def get_vulkan_gpu(icd_files, prime):
|
||||
"""Runs vulkaninfo to find the primary GPU"""
|
||||
|
||||
subprocess_env = dict(os.environ)
|
||||
if icd_files:
|
||||
subprocess_env["VK_DRIVER_FILES"] = icd_files
|
||||
subprocess_env["VK_ICD_FILENAMES"] = icd_files
|
||||
if prime:
|
||||
subprocess_env["DRI_PRIME"] = "1"
|
||||
|
||||
infocmd = "vulkaninfo --summary | grep deviceName | head -n 1 | tr -s '[:blank:]' | cut -d ' ' -f 3-"
|
||||
with subprocess.Popen(infocmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
env=subprocess_env) as infoget:
|
||||
result = infoget.communicate()[0].decode("utf-8").strip()
|
||||
if "Failed to detect any valid GPUs" in result or "ERROR: [Loader Message]" in result:
|
||||
return "No GPU"
|
||||
# Shorten result to just the friendly name of the GPU
|
||||
# vulkaninfo returns Vendor Friendly Name (Chip Developer Name)
|
||||
# AMD Radeon Pro W6800 (RADV NAVI21) -> AMD Radeon Pro W6800
|
||||
return re.sub(r"\s*\(.*?\)", "", result)
|
||||
|
||||
|
||||
def get_vk_icd_files():
|
||||
"""Returns available vulkan ICD files in the same search order as vulkan-loader"""
|
||||
all_icd_search_paths = []
|
||||
|
||||
def add_icd_search_path(paths):
|
||||
if paths:
|
||||
# unixy env vars with multiple paths are : delimited
|
||||
for path in paths.split(":"):
|
||||
path = os.path.join(path, "vulkan")
|
||||
if os.path.exists(path) and path not in all_icd_search_paths:
|
||||
all_icd_search_paths.append(path)
|
||||
|
||||
# Must match behavior of
|
||||
# https://github.com/KhronosGroup/Vulkan-Loader/blob/v1.3.235/docs/LoaderDriverInterface.md#driver-discovery-on-linux
|
||||
# (or a newer version of the same standard)
|
||||
|
||||
# 1.a XDG_CONFIG_HOME or ~/.config if unset
|
||||
add_icd_search_path(os.getenv("XDG_CONFIG_HOME") or (f"{os.getenv('HOME')}/.config"))
|
||||
# 1.b XDG_CONFIG_DIRS
|
||||
add_icd_search_path(os.getenv("XDG_CONFIG_DIRS") or "/etc/xdg")
|
||||
|
||||
# 2, 3 SYSCONFDIR and EXTRASYSCONFDIR
|
||||
# Compiled in default has both the same
|
||||
add_icd_search_path("/etc")
|
||||
|
||||
# 4 XDG_DATA_HOME
|
||||
add_icd_search_path(os.getenv("XDG_DATA_HOME") or (f"{os.getenv('HOME')}/.local/share"))
|
||||
|
||||
# 5 XDG_DATA_DIRS or fall back to /usr/local/share and /usr/share
|
||||
add_icd_search_path(os.getenv("XDG_DATA_DIRS") or "/usr/local/share:/usr/share")
|
||||
|
||||
# FALLBACK
|
||||
# dirs that aren't from the loader spec are searched last
|
||||
for fallback_dir in FALLBACK_VULKAN_DATA_DIRS:
|
||||
add_icd_search_path(fallback_dir)
|
||||
|
||||
all_icd_files = []
|
||||
|
||||
for data_dir in all_icd_search_paths:
|
||||
path = os.path.join(data_dir, "icd.d", "*.json")
|
||||
# sort here as directory enumeration order is not guaranteed in linux
|
||||
# so it's consistent every time
|
||||
icd_files = sorted(glob.glob(path))
|
||||
if icd_files:
|
||||
all_icd_files += icd_files
|
||||
|
||||
return all_icd_files
|
||||
|
||||
|
||||
def get_vk_icd_choices():
|
||||
"""Return available Vulkan ICD loaders"""
|
||||
intel = []
|
||||
amdradv = []
|
||||
nvidia = []
|
||||
amdvlk = []
|
||||
amdvlkpro = []
|
||||
# fallback in case any ICDs don't match a known type
|
||||
unknown = []
|
||||
icd_file_sets = get_vk_icd_file_sets()
|
||||
|
||||
all_icd_files = get_vk_icd_files()
|
||||
|
||||
# Add loaders for each vendor
|
||||
for loader in all_icd_files:
|
||||
if "intel" in loader:
|
||||
intel.append(loader)
|
||||
elif "radeon" in loader:
|
||||
amdradv.append(loader)
|
||||
elif "nvidia" in loader:
|
||||
nvidia.append(loader)
|
||||
elif "amd" in loader:
|
||||
if "pro" in loader:
|
||||
amdvlkpro.append(loader)
|
||||
else:
|
||||
amdvlk.append(loader)
|
||||
else:
|
||||
unknown.append(loader)
|
||||
|
||||
intel_files = ":".join(intel)
|
||||
amdradv_files = ":".join(amdradv)
|
||||
nvidia_files = ":".join(nvidia)
|
||||
amdvlk_files = ":".join(amdvlk)
|
||||
amdvlkpro_files = ":".join(amdvlkpro)
|
||||
unknown_files = ":".join(unknown)
|
||||
intel_files = ":".join(icd_file_sets["intel"])
|
||||
amdradv_files = ":".join(icd_file_sets["amdradv"])
|
||||
nvidia_files = ":".join(icd_file_sets["nvidia"])
|
||||
amdvlk_files = ":".join(icd_file_sets["amdvlk"])
|
||||
amdvlkpro_files = ":".join(icd_file_sets["amdvlkpro"])
|
||||
unknown_files = ":".join(icd_file_sets["unknown"])
|
||||
|
||||
# default choice should always be blank so the env var gets left as is
|
||||
# This ensures Lutris doesn't change the vulkan loader behavior unless you select
|
||||
|
@ -230,10 +101,10 @@ def get_vk_icd_choices():
|
|||
choices.append(("AMDVLK Open source", amdvlk_files))
|
||||
if amdvlkpro_files:
|
||||
choices.append(("AMDGPU-PRO Proprietary", amdvlkpro_files))
|
||||
if unknown:
|
||||
if unknown_files:
|
||||
choices.append(("Unknown Vendor", unknown_files))
|
||||
|
||||
choices = [(prefix + ": " + get_vulkan_gpus(files), files) for prefix, files in choices]
|
||||
choices = [(prefix + ": " + get_vulkan_gpu_name(files, USE_DRI_PRIME), files) for prefix, files in choices]
|
||||
|
||||
return choices
|
||||
|
||||
|
|
|
@ -38,10 +38,13 @@ def get_default_dpi():
|
|||
"""Computes the DPI to use for the primary monitor
|
||||
which we pass to WINE."""
|
||||
display = Gdk.Display.get_default()
|
||||
monitor = display.get_primary_monitor()
|
||||
scale = monitor.get_scale_factor()
|
||||
dpi = 96 * scale
|
||||
return int(dpi)
|
||||
if display:
|
||||
monitor = display.get_primary_monitor()
|
||||
if monitor:
|
||||
scale = monitor.get_scale_factor()
|
||||
dpi = 96 * scale
|
||||
return int(dpi)
|
||||
return 96
|
||||
|
||||
|
||||
def restore_gamma():
|
||||
|
@ -389,7 +392,7 @@ def _get_screen_saver_inhibitor():
|
|||
inhibitor.set_dbus_iface(name, path, interface)
|
||||
except GLib.Error as err:
|
||||
logger.warning("Failed to set up a DBus proxy for name %s, path %s, "
|
||||
"interface %s: %s", name, path, interface, str(err))
|
||||
"interface %s: %s", name, path, interface, err)
|
||||
|
||||
return inhibitor
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# Vulkan detection by Patryk Obara (@dreamer)
|
||||
|
||||
"""Query Vulkan capabilities"""
|
||||
from collections import namedtuple
|
||||
# Standard Library
|
||||
from ctypes import (
|
||||
CDLL, POINTER, Structure, byref, c_char, c_char_p, c_float, c_int32, c_size_t, c_uint8, c_uint32, c_uint64,
|
||||
|
@ -31,6 +32,8 @@ VkInstance = c_void_p # handle (struct ptr)
|
|||
VkPhysicalDevice = c_void_p # handle (struct ptr)
|
||||
VkDeviceSize = c_uint64
|
||||
|
||||
DeviceInfo = namedtuple('DeviceInfo', 'name api_version')
|
||||
|
||||
|
||||
def vk_make_version(major, minor, patch):
|
||||
"""
|
||||
|
@ -38,7 +41,7 @@ def vk_make_version(major, minor, patch):
|
|||
|
||||
https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#fundamentals-versionnum
|
||||
"""
|
||||
return c_uint32((major << 22) | (minor << 12) | patch)
|
||||
return (major << 22) | (minor << 12) | patch
|
||||
|
||||
|
||||
def vk_api_version_major(version):
|
||||
|
@ -75,8 +78,8 @@ class VkApplicationInfo(Structure):
|
|||
super().__init__()
|
||||
self.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO
|
||||
self.pApplicationName = name.encode()
|
||||
self.applicationVersion = vk_make_version(*version)
|
||||
self.apiVersion = vk_make_version(1, 0, 0)
|
||||
self.applicationVersion = c_uint32(vk_make_version(*version))
|
||||
self.apiVersion = c_uint32(vk_make_version(1, 0, 0))
|
||||
|
||||
|
||||
class VkInstanceCreateInfo(Structure):
|
||||
|
@ -262,10 +265,11 @@ def is_vulkan_supported():
|
|||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_vulkan_api_version_tuple():
|
||||
def get_vulkan_api_version():
|
||||
"""
|
||||
Queries libvulkan to get the API version; if this library is missing
|
||||
it returns None. Returns a tuple of (major, minor, patch) version numbers.
|
||||
it returns None. Returns an encoded Vulkan version integer; use
|
||||
vk_api_version_major() and like methods to parse it.
|
||||
"""
|
||||
try:
|
||||
vulkan = CDLL("libvulkan.so.1")
|
||||
|
@ -276,95 +280,79 @@ def get_vulkan_api_version_tuple():
|
|||
enumerate_instance_version = vulkan.vkEnumerateInstanceVersion
|
||||
except AttributeError:
|
||||
# Vulkan 1.0 did not have vkEnumerateInstanceVersion at all!
|
||||
return 1, 0
|
||||
return vk_make_version(1, 0, 0)
|
||||
|
||||
version = c_uint32(0)
|
||||
result = enumerate_instance_version(byref(version))
|
||||
if result == VK_SUCCESS:
|
||||
return make_version_tuple(version.value)
|
||||
|
||||
return None
|
||||
return version.value if result == VK_SUCCESS else None
|
||||
|
||||
|
||||
def get_device_info():
|
||||
"""
|
||||
Returns a dictionary of the physical devices known to Vulkan, omitting software
|
||||
rendered devices. The keys are the device names, and the values are their API
|
||||
version tuples.
|
||||
Returns a list of the physical devices known to Vulkan, represented as
|
||||
(name, api_version) named-tuples and the api_version numbers are encoded, so
|
||||
use vk_api_version_major() and friends to parse them. They are sorted so the
|
||||
highest version device is first, and software rendering devices are omitted.
|
||||
"""
|
||||
try:
|
||||
vulkan = CDLL("libvulkan.so.1")
|
||||
except OSError:
|
||||
return {}
|
||||
return []
|
||||
app_info = VkApplicationInfo("vkinfo", version=(0, 1, 0))
|
||||
create_info = VkInstanceCreateInfo(app_info)
|
||||
instance = VkInstance()
|
||||
result = vulkan.vkCreateInstance(byref(create_info), 0, byref(instance))
|
||||
if result != VK_SUCCESS:
|
||||
return {}
|
||||
return []
|
||||
dev_count = c_uint32(0)
|
||||
result = vulkan.vkEnumeratePhysicalDevices(instance, byref(dev_count), 0)
|
||||
if result != VK_SUCCESS or dev_count.value <= 0:
|
||||
return {}
|
||||
return []
|
||||
|
||||
devices = (VkPhysicalDevice * dev_count.value)()
|
||||
result = vulkan.vkEnumeratePhysicalDevices(instance, byref(dev_count), byref(devices))
|
||||
if result != VK_SUCCESS:
|
||||
return {}
|
||||
return []
|
||||
|
||||
getPhysicalDeviceProperties = vulkan.vkGetPhysicalDeviceProperties
|
||||
getPhysicalDeviceProperties.restype = None
|
||||
getPhysicalDeviceProperties.argtypes = [VkPhysicalDevice, c_void_p] # pointer(VkPhysicalDeviceProperties)]
|
||||
getPhysicalDeviceProperties.argtypes = [VkPhysicalDevice, c_void_p]
|
||||
|
||||
devices_dict = {}
|
||||
device_info = []
|
||||
for physical_device in devices:
|
||||
dev_props = VkPhysicalDeviceProperties()
|
||||
getPhysicalDeviceProperties(physical_device, byref(dev_props))
|
||||
|
||||
if dev_props.deviceType != VK_PHYSICAL_DEVICE_TYPE_CPU:
|
||||
name = dev_props.deviceName.decode("utf-8")
|
||||
api_version = make_version_tuple(dev_props.apiVersion)
|
||||
devices_dict[name] = api_version
|
||||
device_info.append(DeviceInfo(name, dev_props.apiVersion))
|
||||
|
||||
vulkan.vkDestroyInstance(instance, 0)
|
||||
return devices_dict
|
||||
|
||||
|
||||
def get_best_device_info():
|
||||
"""Returns the name and version tuple of the device with the highest
|
||||
version; this is the best tuple from the get_device_info() method, so
|
||||
the key element is the name, and the value element is a version tuple.
|
||||
Go nested tuples! If there are no devices at all, this returns
|
||||
(None, None), but still a tuple."""
|
||||
devices_dict = get_device_info()
|
||||
if not devices_dict:
|
||||
return None, None
|
||||
by_version = sorted(
|
||||
devices_dict.items(),
|
||||
key=lambda t: t[1],
|
||||
reverse=True
|
||||
)
|
||||
return by_version[0] if by_version else (None, None)
|
||||
return sorted(device_info, key=lambda t: t.api_version, reverse=True)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_expected_api_version_tuple():
|
||||
def get_expected_api_version():
|
||||
"""Returns the version tuple of the API version we expect
|
||||
to have; it is the least of the Vulkan library API version, and
|
||||
the best device's API version."""
|
||||
api_version = get_vulkan_api_version_tuple()
|
||||
_best_dev_name, best_dev_version = get_best_device_info()
|
||||
if best_dev_version:
|
||||
return min(api_version, best_dev_version)
|
||||
api_version = get_vulkan_api_version()
|
||||
|
||||
if not api_version:
|
||||
return None
|
||||
|
||||
devices = get_device_info()
|
||||
if devices:
|
||||
return min(api_version, devices[0].api_version)
|
||||
|
||||
return api_version
|
||||
|
||||
|
||||
def make_version_tuple(source_int):
|
||||
major = vk_api_version_major(source_int)
|
||||
minor = vk_api_version_minor(source_int)
|
||||
patch = vk_api_version_patch(source_int)
|
||||
return major, minor, patch
|
||||
def format_version(version):
|
||||
if version:
|
||||
major = vk_api_version_major(version)
|
||||
minor = vk_api_version_minor(version)
|
||||
patch = vk_api_version_patch(version)
|
||||
return "%s.%s.%s" % (major, minor, patch)
|
||||
|
||||
|
||||
def format_version_tuple(version_tuple):
|
||||
return "%s.%s.%s" % version_tuple
|
||||
return "(none)"
|
||||
|
|
|
@ -496,12 +496,12 @@ def gather_system_info_str():
|
|||
graphics_dict["Vendor"] = "Unable to obtain glxinfo"
|
||||
# check Vulkan support
|
||||
if vkquery.is_vulkan_supported():
|
||||
graphics_dict["Vulkan Version"] = vkquery.format_version_tuple(vkquery.get_vulkan_api_version_tuple())
|
||||
graphics_dict["Vulkan Version"] = vkquery.format_version(vkquery.get_vulkan_api_version())
|
||||
|
||||
graphics_dict["Vulkan Drivers"] = ", ".join({
|
||||
"%s (%s)" % (name, vkquery.format_version_tuple(version))
|
||||
"%s (%s)" % (name, vkquery.format_version(version))
|
||||
for name, version
|
||||
in vkquery.get_device_info().items()
|
||||
in vkquery.get_device_info()
|
||||
})
|
||||
else:
|
||||
graphics_dict["Vulkan"] = "Not Supported"
|
||||
|
|
|
@ -113,10 +113,7 @@ class Process:
|
|||
"""Return the process' environment variables"""
|
||||
environ_path = "/proc/{}/environ".format(self.pid)
|
||||
_environ_text = self._read_content(environ_path)
|
||||
if not _environ_text:
|
||||
return {}
|
||||
if "=" not in _environ_text:
|
||||
logger.debug("Invalid environment value '%s' (%s) in %s", _environ_text, len(_environ_text), environ_path)
|
||||
if not _environ_text or "=" not in _environ_text:
|
||||
return {}
|
||||
return dict([line.split("=", 1) for line in _environ_text.split("\x00") if line])
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""System utilities"""
|
||||
import glob
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
|
@ -8,12 +9,15 @@ import stat
|
|||
import string
|
||||
import subprocess
|
||||
import zipfile
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from gettext import gettext as _
|
||||
from pathlib import Path
|
||||
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from lutris import settings
|
||||
from lutris.util.jobs import AsyncCall
|
||||
from lutris.util.log import logger
|
||||
|
||||
# Home folders that should never get deleted.
|
||||
|
@ -28,6 +32,19 @@ PROTECTED_HOME_FOLDERS = (
|
|||
_("Games")
|
||||
)
|
||||
|
||||
# vulkan dirs used by distros or containers that aren't from:
|
||||
# https://github.com/KhronosGroup/Vulkan-Loader/blob/v1.3.235/docs/LoaderDriverInterface.md#driver-discovery-on-linux
|
||||
# don't include the /vulkan suffix
|
||||
FALLBACK_VULKAN_DATA_DIRS = [
|
||||
"/usr/local/etc", # standard site-local location
|
||||
"/usr/local/share", # standard site-local location
|
||||
"/etc", # standard location
|
||||
"/usr/share", # standard location
|
||||
"/usr/lib/x86_64-linux-gnu/GL", # Flatpak GL extension
|
||||
"/usr/lib/i386-linux-gnu/GL", # Flatpak GL32 extension
|
||||
"/opt/amdgpu-pro/etc" # AMD GPU Pro - TkG
|
||||
]
|
||||
|
||||
|
||||
def execute(command, env=None, cwd=None, log_errors=False, quiet=False, shell=False, timeout=None):
|
||||
"""
|
||||
|
@ -544,3 +561,136 @@ def set_keyboard_layout(layout):
|
|||
with subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin) as setxkbmap:
|
||||
setxkbmap.communicate()
|
||||
xkbcomp.communicate()
|
||||
|
||||
|
||||
def preload_vulkan_gpu_names(use_dri_prime):
|
||||
"""Runs threads to load the GPU data from vulkan info for each ICD file set,
|
||||
and one for the default 'unspecified' info. The results are cached by @lru_cache,
|
||||
so we can just ignore them here."""
|
||||
|
||||
try:
|
||||
all_files = [":".join(fs) for fs in get_vk_icd_file_sets().values()]
|
||||
all_files.append("")
|
||||
for files in all_files:
|
||||
# ignore any errors from get_vulkan_gpu_name
|
||||
AsyncCall(get_vulkan_gpu_name, None, files, use_dri_prime, daemon=True)
|
||||
except Exception as ex:
|
||||
logger.exception("Failed to preload Vulkan GPU Names: %s", ex)
|
||||
|
||||
|
||||
# cache this to avoid calling vulkaninfo repeatedly, shouldn't change at runtime
|
||||
@lru_cache
|
||||
def get_vulkan_gpu_name(icd_files, use_dri_prime):
|
||||
"""Runs vulkaninfo to determine the default and DRI_PRIME gpu if available,
|
||||
returns 'Not Found' if the GPU is not found or 'Unknown GPU' if vulkaninfo
|
||||
is not available."""
|
||||
|
||||
def fetch_vulkan_gpu_name(prime):
|
||||
"""Runs vulkaninfo to find the primary GPU"""
|
||||
subprocess_env = dict(os.environ)
|
||||
if icd_files:
|
||||
subprocess_env["VK_DRIVER_FILES"] = icd_files
|
||||
subprocess_env["VK_ICD_FILENAMES"] = icd_files
|
||||
if prime:
|
||||
subprocess_env["DRI_PRIME"] = "1"
|
||||
|
||||
infocmd = "vulkaninfo --summary | grep deviceName | head -n 1 | tr -s '[:blank:]' | cut -d ' ' -f 3-"
|
||||
with subprocess.Popen(infocmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
||||
env=subprocess_env) as infoget:
|
||||
result = infoget.communicate()[0].decode("utf-8").strip()
|
||||
|
||||
if "Failed to detect any valid GPUs" in result or "ERROR: [Loader Message]" in result:
|
||||
return "No GPU"
|
||||
|
||||
# Shorten result to just the friendly name of the GPU
|
||||
# vulkaninfo returns Vendor Friendly Name (Chip Developer Name)
|
||||
# AMD Radeon Pro W6800 (RADV NAVI21) -> AMD Radeon Pro W6800
|
||||
return re.sub(r"\s*\(.*?\)", "", result)
|
||||
|
||||
if not shutil.which("vulkaninfo"):
|
||||
logger.warning("vulkaninfo not available, unable to list GPUs")
|
||||
return "Unknown GPU"
|
||||
|
||||
gpu = fetch_vulkan_gpu_name(False)
|
||||
|
||||
if use_dri_prime:
|
||||
prime_gpu = fetch_vulkan_gpu_name(True)
|
||||
if prime_gpu != gpu:
|
||||
gpu += f" (Discrete GPU: {prime_gpu})"
|
||||
|
||||
return gpu or "Not Found"
|
||||
|
||||
|
||||
def get_vk_icd_file_sets():
|
||||
"""Returns the vulkan ICD files in a default-dict of lists; the keys are the separate
|
||||
drivers, 'intel', 'amdradv', 'amdvlkpro', 'amdvlk', 'nvidia', and 'unknown'."""
|
||||
|
||||
def get_vk_icd_files():
|
||||
"""Returns available vulkan ICD files in the same search order as vulkan-loader,
|
||||
but in a single list"""
|
||||
all_icd_search_paths = []
|
||||
|
||||
def add_icd_search_path(paths):
|
||||
if paths:
|
||||
# unixy env vars with multiple paths are : delimited
|
||||
for path in paths.split(":"):
|
||||
path = os.path.join(path, "vulkan")
|
||||
if os.path.exists(path) and path not in all_icd_search_paths:
|
||||
all_icd_search_paths.append(path)
|
||||
|
||||
# Must match behavior of
|
||||
# https://github.com/KhronosGroup/Vulkan-Loader/blob/v1.3.235/docs/LoaderDriverInterface.md#driver-discovery-on-linux
|
||||
# (or a newer version of the same standard)
|
||||
|
||||
# 1.a XDG_CONFIG_HOME or ~/.config if unset
|
||||
add_icd_search_path(os.getenv("XDG_CONFIG_HOME") or (f"{os.getenv('HOME')}/.config"))
|
||||
# 1.b XDG_CONFIG_DIRS
|
||||
add_icd_search_path(os.getenv("XDG_CONFIG_DIRS") or "/etc/xdg")
|
||||
|
||||
# 2, 3 SYSCONFDIR and EXTRASYSCONFDIR
|
||||
# Compiled in default has both the same
|
||||
add_icd_search_path("/etc")
|
||||
|
||||
# 4 XDG_DATA_HOME
|
||||
add_icd_search_path(os.getenv("XDG_DATA_HOME") or (f"{os.getenv('HOME')}/.local/share"))
|
||||
|
||||
# 5 XDG_DATA_DIRS or fall back to /usr/local/share and /usr/share
|
||||
add_icd_search_path(os.getenv("XDG_DATA_DIRS") or "/usr/local/share:/usr/share")
|
||||
|
||||
# FALLBACK
|
||||
# dirs that aren't from the loader spec are searched last
|
||||
for fallback_dir in FALLBACK_VULKAN_DATA_DIRS:
|
||||
add_icd_search_path(fallback_dir)
|
||||
|
||||
all_icd_files = []
|
||||
|
||||
for data_dir in all_icd_search_paths:
|
||||
path = os.path.join(data_dir, "icd.d", "*.json")
|
||||
# sort here as directory enumeration order is not guaranteed in linux
|
||||
# so it's consistent every time
|
||||
icd_files = sorted(glob.glob(path))
|
||||
if icd_files:
|
||||
all_icd_files += icd_files
|
||||
|
||||
return all_icd_files
|
||||
|
||||
sets = defaultdict(list)
|
||||
all_icd_files = get_vk_icd_files()
|
||||
|
||||
# Add loaders for each vendor
|
||||
for loader in all_icd_files:
|
||||
if "intel" in loader:
|
||||
sets["intel"].append(loader)
|
||||
elif "radeon" in loader:
|
||||
sets["amdradv"].append(loader)
|
||||
elif "nvidia" in loader:
|
||||
sets["nvidia"].append(loader)
|
||||
elif "amd" in loader:
|
||||
if "pro" in loader:
|
||||
sets["amdvlkpro"].append(loader)
|
||||
else:
|
||||
sets["amdvlk"].append(loader)
|
||||
else:
|
||||
sets["unknown"].append(loader)
|
||||
|
||||
return sets
|
||||
|
|
|
@ -11,7 +11,7 @@ from lutris.util.log import logger
|
|||
from lutris.util.system import create_folder, execute, remove_folder
|
||||
from lutris.util.wine.dll_manager import DLLManager
|
||||
|
||||
REQUIRED_VULKAN_API_VERSION = 1, 3, 0
|
||||
REQUIRED_VULKAN_API_VERSION = vkquery.vk_make_version(1, 3, 0)
|
||||
|
||||
|
||||
class DXVKManager(DLLManager):
|
||||
|
@ -20,7 +20,7 @@ class DXVKManager(DLLManager):
|
|||
versions_path = os.path.join(base_dir, "dxvk_versions.json")
|
||||
managed_dlls = ("dxgi", "d3d11", "d3d10core", "d3d9",)
|
||||
releases_url = "https://api.github.com/repos/lutris/dxvk/releases"
|
||||
vulkan_api_version = vkquery.get_expected_api_version_tuple()
|
||||
vulkan_api_version = vkquery.get_expected_api_version()
|
||||
|
||||
def is_recommended_version(self, version):
|
||||
# DXVK 2.x and later require Vulkan 1.3, so if that iss lacking
|
||||
|
|
44
po/ru.po
44
po/ru.po
|
@ -23,7 +23,7 @@ msgstr "Lutris"
|
|||
|
||||
#: share/applications/net.lutris.Lutris.desktop:4
|
||||
msgid "Video Game Preservation Platform"
|
||||
msgstr "Платформа для сохранения видеоигр"
|
||||
msgstr "Площадка для сохранения видеоигр"
|
||||
|
||||
#: share/applications/net.lutris.Lutris.desktop:6
|
||||
msgid "gaming;wine;emulator;"
|
||||
|
@ -32,7 +32,7 @@ msgstr "gaming;wine;emulator;"
|
|||
#: share/metainfo/net.lutris.Lutris.metainfo.xml:11
|
||||
#: share/lutris/ui/about-dialog.ui:18
|
||||
msgid "Video game preservation platform"
|
||||
msgstr "Платформа для сохранения видеоигр"
|
||||
msgstr "Площадка для сохранения видеоигр"
|
||||
|
||||
#: share/metainfo/net.lutris.Lutris.metainfo.xml:14
|
||||
msgid "Main window"
|
||||
|
@ -75,8 +75,8 @@ msgstr ""
|
|||
"или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Стандартной\n"
|
||||
"общественной лицензии GNU.\n"
|
||||
"\n"
|
||||
"Вы должны были получить копию Стандартной общественной лицензии GNU\n"
|
||||
"вместе с этой программой. Если это не так, см. <https://www.gnu.org/licenses/"
|
||||
"Вы должны были получить копию Основной общественной лицензии GNU\n"
|
||||
"вместе с этим приложением. Если это не так, см. <https://www.gnu.org/licenses/"
|
||||
">.\n"
|
||||
|
||||
#: share/lutris/ui/dialog-lutris-login.ui:8
|
||||
|
@ -126,7 +126,7 @@ msgstr "Масштаб:"
|
|||
|
||||
#: share/lutris/ui/lutris-window.ui:345
|
||||
msgid "Sort Ascending"
|
||||
msgstr "Сортировать по возрастанию"
|
||||
msgstr "Упорядочивать по возрастанию"
|
||||
|
||||
#: share/lutris/ui/lutris-window.ui:360 lutris/gui/config/common.py:127
|
||||
#: lutris/gui/views/list.py:48 lutris/gui/views/list.py:172
|
||||
|
@ -207,7 +207,7 @@ msgstr "Не удалось найти файл {}"
|
|||
|
||||
#: lutris/game.py:256
|
||||
msgid "This game has no executable set. The install process didn't finish properly."
|
||||
msgstr "В этой игре нет исполняемого набора. Процесс установки не завершился должным образом."
|
||||
msgstr "В этой игре нет исполняемого набора. Установка не завершилась должным образом."
|
||||
|
||||
#: lutris/game.py:259
|
||||
#, python-format
|
||||
|
@ -217,7 +217,7 @@ msgstr "Файл %s не является исполняемым"
|
|||
#: lutris/game.py:261
|
||||
#, python-format
|
||||
msgid "The path '%s' is not set. please set it in the options."
|
||||
msgstr "Путь '%s' не установлен, пожалуйста установите его в опциях."
|
||||
msgstr "Путь «%s» не установлен, пожалуйста установите его в опциях."
|
||||
|
||||
#: lutris/game.py:263
|
||||
#, python-format
|
||||
|
@ -395,7 +395,7 @@ msgid ""
|
|||
"The folder doesn't exist."
|
||||
msgstr ""
|
||||
"Не удается открыть %s \n"
|
||||
"Каталог не существует."
|
||||
"Папка не существует."
|
||||
|
||||
#. use primary configuration
|
||||
#: lutris/game_actions.py:335
|
||||
|
@ -405,15 +405,15 @@ msgstr "Удалить ярлык с рабочего стола"
|
|||
|
||||
#: lutris/gui/addgameswindow.py:28
|
||||
msgid "Search the Lutris website for installers"
|
||||
msgstr "Найдите установщик на веб-сайте Lutris"
|
||||
msgstr "Найдите установщик на сайте Lutris"
|
||||
|
||||
#: lutris/gui/addgameswindow.py:29
|
||||
msgid "Query our website for community installers"
|
||||
msgstr "Запросите установщик на веб-сайте нашего сообщества"
|
||||
msgstr "Запросите установщик на сайте нашего сообщества"
|
||||
|
||||
#: lutris/gui/addgameswindow.py:35
|
||||
msgid "Scan a folder for games"
|
||||
msgstr "Сканировать папку на наличие игр"
|
||||
msgstr "Проверить папку на наличие игр"
|
||||
|
||||
#: lutris/gui/addgameswindow.py:36
|
||||
msgid "Mass-import a folder of games"
|
||||
|
@ -480,7 +480,7 @@ msgstr "Отмена"
|
|||
|
||||
#: lutris/gui/addgameswindow.py:114 lutris/gui/config/boxes.py:439
|
||||
msgid "Select folder"
|
||||
msgstr "Выбрать каталог"
|
||||
msgstr "Выбрать папку"
|
||||
|
||||
#: lutris/gui/addgameswindow.py:123
|
||||
#, fuzzy
|
||||
|
@ -522,7 +522,7 @@ msgstr ""
|
|||
#: lutris/gui/addgameswindow.py:307
|
||||
#, fuzzy
|
||||
msgid "Folder to scan"
|
||||
msgstr "Выбрать каталог"
|
||||
msgstr "Выбрать папку"
|
||||
|
||||
#: lutris/gui/addgameswindow.py:313
|
||||
msgid ""
|
||||
|
@ -538,7 +538,7 @@ msgstr ""
|
|||
#: lutris/gui/addgameswindow.py:331
|
||||
#, fuzzy
|
||||
msgid "You must select a folder to scan for games."
|
||||
msgstr "Выбрать каталог"
|
||||
msgstr "Выбрать папку"
|
||||
|
||||
#: lutris/gui/addgameswindow.py:333
|
||||
#, python-format
|
||||
|
@ -562,7 +562,7 @@ msgstr "З_акрыть"
|
|||
#: lutris/gui/addgameswindow.py:409
|
||||
#, fuzzy
|
||||
msgid "Game name"
|
||||
msgstr "Информация об игре"
|
||||
msgstr "Сведения об игре"
|
||||
|
||||
#: lutris/gui/addgameswindow.py:425
|
||||
msgid ""
|
||||
|
@ -639,7 +639,7 @@ msgstr ""
|
|||
#: lutris/runners/snes9x.py:27 lutris/runners/vice.py:38
|
||||
#: lutris/runners/yuzu.py:23
|
||||
msgid "ROM file"
|
||||
msgstr "Файл образа ПЗУ (ROM)"
|
||||
msgstr "Файл образа ПЗУ"
|
||||
|
||||
#: lutris/gui/addgameswindow.py:557
|
||||
msgid ""
|
||||
|
@ -654,7 +654,7 @@ msgstr ""
|
|||
|
||||
#: lutris/gui/addgameswindow.py:576
|
||||
msgid "You must select a ROM file to install."
|
||||
msgstr "Вы должны выбрать файл ROM для установки."
|
||||
msgstr "Вы должны выбрать файл ПЗУ для установки."
|
||||
|
||||
#: lutris/gui/application.py:92
|
||||
msgid ""
|
||||
|
@ -1150,12 +1150,12 @@ msgstr "Скрывать текст за иконками"
|
|||
|
||||
#: lutris/gui/config/sysinfo_box.py:36
|
||||
msgid "Copy to clipboard"
|
||||
msgstr "Копировать в буфер обмена"
|
||||
msgstr "Скопировать в буфер обмена"
|
||||
|
||||
#: lutris/gui/config/sysinfo_box.py:39
|
||||
#, fuzzy
|
||||
msgid "<b>System information</b>"
|
||||
msgstr "Информация о системе"
|
||||
msgstr "Сведения о системе"
|
||||
|
||||
#: lutris/gui/dialogs/cache.py:13
|
||||
msgid "Cache configuration"
|
||||
|
@ -1167,7 +1167,7 @@ msgstr "Путь к кэшу"
|
|||
|
||||
#: lutris/gui/dialogs/cache.py:43
|
||||
msgid "Set the folder for the cache path"
|
||||
msgstr "Укажите каталог для кэша"
|
||||
msgstr "Укажите папку для кэша"
|
||||
|
||||
#: lutris/gui/dialogs/cache.py:55
|
||||
msgid ""
|
||||
|
@ -1183,7 +1183,7 @@ msgstr ""
|
|||
|
||||
#: lutris/gui/dialogs/delegates.py:70
|
||||
msgid "Runtime currently updating"
|
||||
msgstr "В данный момент среда выполнения обновляется"
|
||||
msgstr "На данное время среда исполнения обновляется"
|
||||
|
||||
#: lutris/gui/dialogs/delegates.py:71
|
||||
msgid "Game might not work as expected"
|
||||
|
@ -1414,7 +1414,7 @@ msgstr "Пожалуйста, выберите файл"
|
|||
|
||||
#: lutris/gui/dialogs/__init__.py:271
|
||||
msgid "Checking for runtime updates, please wait…"
|
||||
msgstr ""
|
||||
msgstr "Проверка обновлений сред исполения, пожалуйста, подождите"
|
||||
|
||||
#: lutris/gui/dialogs/__init__.py:309
|
||||
#, python-format
|
||||
|
|
2
setup.py
2
setup.py
|
@ -22,7 +22,7 @@ setup(
|
|||
version=VERSION,
|
||||
license='GPL-3',
|
||||
author='Mathieu Comandon',
|
||||
author_email='strider@strycore.com',
|
||||
author_email='mathieucomandon@gmail.com',
|
||||
packages=[
|
||||
'lutris',
|
||||
'lutris.database',
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"human_name": "Citra",
|
||||
"description": "Nintendo 3DS emulator",
|
||||
"platforms": ["Nintendo 3DS"],
|
||||
"require_libs": ["libQt5OpenGL.so.5", "libQt5Widgets.so.5", "libQt5Multimedia.so.5"],
|
||||
"require_libs": [],
|
||||
"runner_executable": "citra/citra-qt",
|
||||
"runnable_alone": true,
|
||||
"game_options": [
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"description": "Sony PSP emulator",
|
||||
"platforms": ["Sony PlayStation Portable"],
|
||||
"runner_executable": "ppsspp/PPSSPPSDL",
|
||||
"runnable_alone": true,
|
||||
"game_options": [
|
||||
{
|
||||
"option": "main_file",
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<translation type="gettext">lutris</translation>
|
||||
<developer_name translatable="no">Lutris Team</developer_name>
|
||||
<update_contact>strider@strycore.com</update_contact>
|
||||
<update_contact>mathieucomandon@gmail.com</update_contact>
|
||||
<name translatable="no">Lutris</name>
|
||||
<summary>Video game preservation platform</summary>
|
||||
<screenshots>
|
||||
|
@ -24,7 +24,7 @@
|
|||
<url type="bugtracker">https://github.com/lutris/lutris/issues</url>
|
||||
<launchable type="desktop-id">net.lutris.Lutris.desktop</launchable>
|
||||
<releases>
|
||||
<release date="2023-02-11" version="0.5.13" />
|
||||
<release date="2023-05-16" version="0.5.13" />
|
||||
</releases>
|
||||
<content_rating type="oars-1.1" />
|
||||
</component>
|
||||
|
|
|
@ -28,7 +28,7 @@ class TestPCSX2Runner(unittest.TestCase):
|
|||
mock_config.game_config = {'main_file': main_file}
|
||||
mock_config.runner_config = {'fullscreen': True}
|
||||
self.runner.config = mock_config
|
||||
expected = {'command': [self.runner.get_executable(), '--fullscreen', main_file]}
|
||||
expected = {'command': [self.runner.get_executable(), '-fullscreen', main_file]}
|
||||
self.assertEqual(self.runner.play(), expected)
|
||||
|
||||
@patch('lutris.util.system.path_exists')
|
||||
|
@ -39,7 +39,7 @@ class TestPCSX2Runner(unittest.TestCase):
|
|||
mock_config.game_config = {'main_file': main_file}
|
||||
mock_config.runner_config = {'full_boot': True}
|
||||
self.runner.config = mock_config
|
||||
expected = {'command': [self.runner.get_executable(), '--fullboot', main_file]}
|
||||
expected = {'command': [self.runner.get_executable(), '-slowboot', main_file]}
|
||||
self.assertEqual(self.runner.play(), expected)
|
||||
|
||||
@patch('lutris.util.system.path_exists')
|
||||
|
@ -50,47 +50,19 @@ class TestPCSX2Runner(unittest.TestCase):
|
|||
mock_config.game_config = {'main_file': main_file}
|
||||
mock_config.runner_config = {'nogui': True}
|
||||
self.runner.config = mock_config
|
||||
expected = {'command': [self.runner.get_executable(), '--nogui', main_file]}
|
||||
self.assertEqual(self.runner.play(), expected)
|
||||
|
||||
@patch('lutris.util.system.path_exists')
|
||||
def test_play_cfg_set(self, mock_path_exists):
|
||||
main_file = '/valid/path/to/iso'
|
||||
config_file = '/valid/path/to/cfg'
|
||||
cfg_arg = '--cfg=' + config_file
|
||||
mock_path_exists.return_value = True
|
||||
mock_config = MagicMock()
|
||||
mock_config.game_config = {'main_file': main_file}
|
||||
mock_config.runner_config = {'config_file': config_file}
|
||||
self.runner.config = mock_config
|
||||
expected = {'command': [self.runner.get_executable(), cfg_arg, main_file]}
|
||||
self.assertEqual(self.runner.play(), expected)
|
||||
|
||||
@patch('lutris.util.system.path_exists')
|
||||
def test_play_cfgpath_set(self, mock_path_exists):
|
||||
main_file = '/valid/path/to/iso'
|
||||
config_path = '/valid/path/to/cfgpath'
|
||||
cfgpath_arg = '--cfgpath=' + config_path
|
||||
mock_path_exists.return_value = True
|
||||
mock_config = MagicMock()
|
||||
mock_config.game_config = {'main_file': main_file}
|
||||
mock_config.runner_config = {'config_path': config_path}
|
||||
self.runner.config = mock_config
|
||||
expected = {'command': [self.runner.get_executable(), cfgpath_arg, main_file]}
|
||||
expected = {'command': [self.runner.get_executable(), '-nogui', main_file]}
|
||||
self.assertEqual(self.runner.play(), expected)
|
||||
|
||||
@patch('lutris.util.system.path_exists')
|
||||
def test_play(self, mock_path_exists):
|
||||
main_file = '/valid/path/to/iso'
|
||||
config_path = '/valid/path/to/cfgpath'
|
||||
cfgpath_arg = '--cfgpath=' + config_path
|
||||
mock_path_exists.return_value = True
|
||||
mock_config = MagicMock()
|
||||
mock_config.game_config = {'main_file': main_file}
|
||||
mock_config.runner_config = {
|
||||
'config_path': config_path, 'fullscreen': False, 'nogui': True,
|
||||
'full_boot': True, 'config_file': '',
|
||||
'fullscreen': False, 'nogui': True,
|
||||
'full_boot': True,
|
||||
}
|
||||
self.runner.config = mock_config
|
||||
expected = {'command': [self.runner.get_executable(), '--fullboot', '--nogui', cfgpath_arg, main_file]}
|
||||
expected = {'command': [self.runner.get_executable(), '-slowboot', '-nogui', main_file]}
|
||||
self.assertEqual(self.runner.play(), expected)
|
||||
|
|
Loading…
Reference in a new issue