Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Lutris: Add Basic Games List #192

Merged
merged 5 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pupgui2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,6 @@ def PALETTE_DARK():
STEAM_STL_CACHE_PATH = os.path.join(os.path.expanduser('~'), '.cache', 'steamtinkerlaunch')
STEAM_STL_DATA_PATH = os.path.join(os.path.expanduser('~'), '.local', 'share', 'steamtinkerlaunch')
STEAM_STL_SHELL_FILES = [ '.bashrc', '.zshrc', '.kshrc' ]
STEAM_STL_FISH_VARIABLES = os.path.join(os.path.expanduser('~'), '.config/fish/fish_variables')
STEAM_STL_FISH_VARIABLES = os.path.join(os.path.expanduser('~'), '.config/fish/fish_variables')

LUTRIS_WEB_URL='https://lutris.net/games/'
3 changes: 2 additions & 1 deletion pupgui2/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ class LutrisGame:
runner = ''
installer_slug = ''
installed_at = 0
install_dir = ''

install_loc = None

Expand All @@ -156,7 +157,7 @@ def get_game_config(self):
break

lutris_game_cfg = os.path.join(os.path.expanduser(lutris_config_dir), 'games', fn)
if not os.path.exists(lutris_game_cfg):
if not os.path.isfile(lutris_game_cfg):
return {}
with open(lutris_game_cfg, 'r') as f:
return yaml.safe_load(f)
16 changes: 14 additions & 2 deletions pupgui2/lutrisutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pupgui2.datastructures import LutrisGame


LUTRIS_PGA_GAMELIST_QUERY = 'SELECT slug, name, runner, installer_slug, installed_at FROM games'
LUTRIS_PGA_GAMELIST_QUERY = 'SELECT slug, name, runner, installer_slug, installed_at, directory FROM games'


def get_lutris_game_list(install_loc) -> List[LutrisGame]:
Expand All @@ -15,7 +15,6 @@ def get_lutris_game_list(install_loc) -> List[LutrisGame]:
"""
install_dir = os.path.expanduser(install_loc.get('install_dir'))
lutris_data_dir = os.path.join(install_dir, os.pardir, os.pardir)

pga_db_file = os.path.join(lutris_data_dir, 'pga.db')
lgs = []
try:
Expand All @@ -31,6 +30,19 @@ def get_lutris_game_list(install_loc) -> List[LutrisGame]:
lg.runner = g[2]
lg.installer_slug = g[3]
lg.installed_at = g[4]

# Lutris database file will only store some fields for games installed via an installer and not if it was manually added
# If a game doesn't have an install dir (e.g. it was manually added to Lutris), try to use the following for the install dir:
# - Working directory (may not be specified)
# - Executable: may not be accurate as the exe could be heavily nested, but a good fallback)
lutris_install_dir = g[5]
if not lutris_install_dir:
lg_config = lg.get_game_config()
working_dir = lg_config.get('game', {}).get('working_dir')
exe_dir = lg_config.get('game', {}).get('exe')
lutris_install_dir = working_dir or (os.path.dirname(str(exe_dir)) if exe_dir else None)

lg.install_dir = os.path.abspath(lutris_install_dir) if lutris_install_dir else ''
lgs.append(lg)
except Exception as e:
print('Error: Could not get lutris game list:', e)
Expand Down
4 changes: 2 additions & 2 deletions pupgui2/pupgui2.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ def update_ui(self):

if install_loc.get('launcher') == 'steam' and 'vdf_dir' in install_loc:
self.ui.btnShowGameList.setVisible(True)
#elif install_loc.get('launcher') == 'lutris':
# self.ui.btnShowGameList.setVisible(True)
elif install_loc.get('launcher') == 'lutris':
self.ui.btnShowGameList.setVisible(True)
else:
self.ui.btnShowGameList.setVisible(False)

Expand Down
127 changes: 99 additions & 28 deletions pupgui2/pupgui2gamelistdialog.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import os
import pkgutil

from typing import List, Callable
from datetime import datetime

from PySide6.QtCore import QObject, Signal, Slot, QDataStream, QByteArray, Qt
from PySide6.QtGui import QPixmap, QBrush, QColor
from PySide6.QtWidgets import QLabel, QComboBox, QPushButton, QTableWidgetItem
from PySide6.QtUiTools import QUiLoader

from pupgui2.constants import PROTONDB_COLORS, STEAM_APP_PAGE_URL, AWACY_WEB_URL, PROTONDB_APP_PAGE_URL
from pupgui2.constants import PROTONDB_COLORS, STEAM_APP_PAGE_URL, AWACY_WEB_URL, PROTONDB_APP_PAGE_URL, LUTRIS_WEB_URL
from pupgui2.datastructures import AWACYStatus, SteamApp, SteamDeckCompatEnum
from pupgui2.lutrisutil import get_lutris_game_list
from pupgui2.lutrisutil import get_lutris_game_list, LutrisGame
from pupgui2.steamutil import steam_update_ctools, get_steam_game_list
from pupgui2.steamutil import is_steam_running, get_steam_ctool_list
from pupgui2.steamutil import get_protondb_status
Expand All @@ -25,10 +28,10 @@ def __init__(self, install_dir, parent=None):
super(PupguiGameListDialog, self).__init__(parent)
self.install_dir = install_dir
self.parent = parent

self.queued_changes = {}

self.install_loc = get_install_location_from_directory_name(install_dir)
self.launcher = self.install_loc.get('launcher')

self.load_ui()
self.setup_ui()
Expand All @@ -42,27 +45,44 @@ def load_ui(self):
self.ui = loader.load(ui_file.device())

def setup_ui(self):
if self.install_loc.get('launcher') == 'steam':
self.ui.tableGames.setHorizontalHeaderLabels([self.tr('Game'), self.tr('Compatibility Tool'), self.tr('Deck compatibility'), self.tr('Anticheat'), 'ProtonDB'])
self.ui.tableGames.horizontalHeaderItem(3).setToolTip('https://areweanticheatyet.com')
self.update_game_list_steam()

if os.path.exists('/.flatpak-info'):
self.ui.lblSteamRunningWarning.setVisible(True)
self.ui.lblSteamRunningWarning.setStyleSheet('QLabel { color: grey; }')
elif is_steam_running():
self.ui.lblSteamRunningWarning.setVisible(True)
else:
self.ui.lblSteamRunningWarning.setVisible(False)
if self.launcher == 'steam':
self.setup_steam_list_ui()
elif self.launcher == 'lutris':
self.setup_lutris_list_ui()

self.ui.tableGames.itemDoubleClicked.connect(self.item_doubleclick_action)
self.ui.btnApply.clicked.connect(self.btn_apply_clicked)

elif self.install_loc.get('launcher') == 'lutris':
self.update_game_list_lutris()
def setup_steam_list_ui(self):
self.ui.tableGames.setHorizontalHeaderLabels([self.tr('Game'), self.tr('Compatibility Tool'), self.tr('Deck compatibility'), self.tr('Anticheat'), 'ProtonDB'])
self.ui.tableGames.horizontalHeaderItem(3).setToolTip('https://areweanticheatyet.com')
self.update_game_list_steam()

if os.path.exists('/.flatpak-info'):
self.ui.lblSteamRunningWarning.setVisible(True)
self.ui.lblSteamRunningWarning.setStyleSheet('QLabel { color: grey; }')
elif is_steam_running():
self.ui.lblSteamRunningWarning.setVisible(True)
else:
self.ui.lblSteamRunningWarning.setVisible(False)

self.ui.tableGames.setColumnWidth(0, 300)
self.ui.tableGames.setColumnWidth(3, 70)
self.ui.tableGames.setColumnWidth(4, 70)
self.ui.btnApply.clicked.connect(self.btn_apply_clicked)

def setup_lutris_list_ui(self):
self.ui.tableGames.setHorizontalHeaderLabels([self.tr('Game'), self.tr('Runner'), self.tr('Install Location'), self.tr('Installed Date'), ''])
self.update_game_list_lutris()

self.ui.lblSteamRunningWarning.setVisible(False)

self.ui.tableGames.setColumnWidth(0, 300)
self.ui.tableGames.setColumnWidth(1, 70)
self.ui.tableGames.setColumnWidth(2, 280)
self.ui.tableGames.setColumnWidth(3, 30)
self.ui.tableGames.setColumnHidden(4, True)

self.ui.btnApply.setText(self.tr('Close'))

def update_game_list_steam(self):
""" update the game list for the Steam launcher """
Expand All @@ -71,12 +91,10 @@ def update_game_list_steam(self):
ctools.extend(t.ctool_name for t in get_steam_ctool_list(steam_config_folder=self.install_loc.get('vdf_dir')))

self.ui.tableGames.setRowCount(len(self.games))
self.ui.tableGames.itemDoubleClicked.connect(self.item_doubleclick_action)

game_id_table_lables = []
for i, game in enumerate(self.games):
game_item = QTableWidgetItem()
game_item.setText(game.game_name)
game_item = QTableWidgetItem(game.game_name)
game_item.setData(Qt.UserRole, f'{STEAM_APP_PAGE_URL}{game.app_id}') # e.g. https://store.steampowered.com/app/620
game_item.setToolTip(f'{game.game_name} ({game.app_id})')

Expand Down Expand Up @@ -167,16 +185,68 @@ def update_game_list_steam(self):
self.ui.tableGames.setCellWidget(i, 3, lblicon)

game_id_table_lables.append(game.app_id)
self.ui.tableGames.setVerticalHeaderLabels(game_id_table_lables)

def update_game_list_lutris(self):
""" update the game list for the Lutris launcher """
games = get_lutris_game_list(self.install_loc)
# Filter blank runners and Steam games, because we can't change any compat tool options for Steam games via Lutris
# Steam games can be seen from the Steam games list, so no need to duplicate it here
games: List[LutrisGame] = list(filter(lambda lutris_game: (lutris_game.runner is not None and lutris_game.runner != 'steam' and len(lutris_game.runner) > 0), get_lutris_game_list(self.install_loc)))

self.ui.tableGames.setRowCount(len(games))

for i, game in enumerate(games):
self.ui.tableGames.setItem(i, 0, QTableWidgetItem(game.name))
# Not sure if we can allow compat tool updating from here, as Lutris allows configuring more than just Wine version
# It lets you set Wine/DXVK/vkd3d/etc independently, so for now the dialog just displays game information
for i, game in enumerate(games):
name_item = QTableWidgetItem(game.name)
name_item.setToolTip(f'{game.name} ({game.slug})')
if game.installer_slug:
# Only games with an installer_slug will have a Lutris web URL - Could be an edge case that runners get removed/updated from lutris.net?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine I guess. That shouldn't occur often.

name_item.setData(Qt.UserRole, f'{LUTRIS_WEB_URL}{game.slug}')

runner_item = QTableWidgetItem(game.runner.capitalize())
runner_item.setTextAlignment(Qt.AlignCenter)
# Display wine runner information in tooltip
if game.runner == 'wine':
game_cfg = game.get_game_config()
runnerinfo = game_cfg.get('wine', {})

wine_ver = runnerinfo.get('version')
dxvk_ver = runnerinfo.get('dxvk_version')
vkd3d_ver = runnerinfo.get('vkd3d_version')

tooltip = ''
tooltip += f'Wine version: {wine_ver}' if wine_ver else ''
tooltip += f'\nDXVK version: {dxvk_ver}' if dxvk_ver else ''
tooltip += f'\nvkd3d version: {vkd3d_ver}' if vkd3d_ver else ''

runner_item.setToolTip(tooltip)

# Some games may be in Lutris but not have a valid install path, though the yml should *usually* have some path
install_dir_text = game.install_dir or self.tr('Unknown')
install_dir_item = QTableWidgetItem(install_dir_text)
if not game.install_dir:
install_dir_item.setForeground(QBrush(QColor(PROTONDB_COLORS.get('gold'))))
else:
if os.path.isdir(install_dir_text):
# Set double click action to open valid install dir with xdg-open
install_dir_item.setToolTip(self.tr('Double-click to browse...'))
install_dir_item.setData(Qt.UserRole, lambda url: os.system(f'xdg-open "{url}"'))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that will work with the Flatpak version of ProtonUp-Qt. If inside Flatpak, it requires the prefix flatpak-spawn --host:
flatpak-spawn --host xdg-open /path/here/

Maybe we should add a function like host_which that runs commands on outside the sandbox.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not sure if it would work, I lifted this logic from Line 384-386 in pupgui2.py.

If this won't work in Flatpak, we could see about implementing something like host_xdgopen or something along these lines specifically for this action :-)

There is also the possibility that this won't work outside of Flatpak, if xdg-utils is not installed. But I think most systems, especially those for gaming (including Steam Deck) will come with xdg-utils.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I did some testing yesterday and it didn't work. Tried again after a reboot, now it works.
It makes sense that xdg works with Flatpak, probably something wasn't running properly.

else:
install_dir_item.setToolTip(self.tr('Install location does not exist!'))

install_date = datetime.fromtimestamp(int(game.installed_at)).isoformat().split('T')
install_date_short = f'{install_date[0]}'
install_date_tooltip = self.tr('Installed at {DATE} ({TIME})').format(DATE=install_date[0], TIME=install_date[1])

install_date_item = QTableWidgetItem(install_date_short)
install_date_item.setData(Qt.UserRole, int(game.installed_at))
install_date_item.setToolTip(install_date_tooltip)
install_date_item.setTextAlignment(Qt.AlignCenter)

self.ui.tableGames.setItem(i, 0, name_item)
self.ui.tableGames.setItem(i, 1, runner_item)
self.ui.tableGames.setItem(i, 2, install_dir_item)
self.ui.tableGames.setItem(i, 3, install_date_item)

def btn_apply_clicked(self):
self.update_queued_ctools_steam()
Expand Down Expand Up @@ -223,8 +293,9 @@ def update_queued_ctools_steam(self):

def item_doubleclick_action(self, item):
""" open link attached for QTableWidgetItem in browser """

item_url = item.data(Qt.UserRole)
if isinstance(item_url, str):
# UserRole should always hold URL
open_webbrowser_thread(item_url)
open_webbrowser_thread(item_url) # Str UserRole should always hold URL
elif isinstance(item_url, Callable):
item_url(item.text())

6 changes: 6 additions & 0 deletions pupgui2/resources/ui/pupgui2_gamelistdialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@
<property name="columnCount">
<number>5</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>160</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column/>
<column/>
<column/>
Expand Down