Merge master in next (Warning: it's broken)

This commit is contained in:
Mathieu Comandon 2018-11-06 22:19:45 -08:00
commit acbd4681e2
126 changed files with 5184 additions and 1885 deletions

82
AUTHORS
View file

@ -1,15 +1,79 @@
Copyright (C) 2010-2016 Mathieu Comandon <strider@strycore.com>
Copyright (C) 2010-2018 Mathieu Comandon <strider@strycore.com>
Contributors:
Mathieu Comandon <strider@strycore.com>
Ludovic Soulié <contact@ludal.net>
Pascal Reinhard (Xodetaetl) <dev@xod.me>
Rob Loach <robloach@gmail.com>
Rémi Verschelde <rverschelde@gmail.com>
Ivan <malkavi@users.noreply.github.com>
mikeyd <mdeguzis@gmail.com>
Travis Nickles <nickles.travis@gmail.com>
Patrick Griffis <tingping@tingping.se>
Julien Machiels <julien.a.machiels@gmail.com>
Daniel J (@djazz)
Tom Todd
Rob Loach <robloach@gmail.com>
cxf (@AccountOneOff)
Alexandr Oleynikov
Patrick Griffis <tingping@tingping.se>
Rebecca Wallander
Frederik “Freso” S. Olesen
telanus
Leandro Stanger
Travis Nickles <nickles.travis@gmail.com>
Medath
Manuel Vögele
Xenega
sigmaSd
Arne Sellmann
LeandroStanger
duhow
MrTimscampi
Nbiba Bedis
soredake
Alexander Ravenheart
Rémi Verschelde <rverschelde@gmail.com>
tcarrio
Tammas Loughran
Max le Fou
mandruis
999gary
Christoffer Anselm
bebop350
Ivan <malkavi@users.noreply.github.com>
Julien Machiels <julien.a.machiels@gmail.com>
Julio Campagnolo
Kukuh Syafaat
TotalCaesar659
mikeyd <mdeguzis@gmail.com>
nastys
v-vansteen
Christian Dannie Storgaard
Clonewayx
LEARAX
Roxie Gibson
Taeyeon Mori
BunnyApocalypse
glitchbunny
luthub
malt1
matthewkovacs
boombatower
Alan Pearce
Alexander Bessman
AsciiWolf
Benjamin Weis
FlyingWombat
Francesco Turco
Jan Havran
Jeff Corcoran
Joshua Strobl
Kevin Turner
Lucki
Marcin Mikołajczak
Mehdi Lahlou
Nathaniel Case
Nico Linder
Steven Pledger
Tom Willemse
Tomas Tomecek
Wybe Westra
Édouard Lopez
Ludovic Soulié
Yunusemre Şentürk
Yurii Kolesnykov
Patryk Obara (@dreamer)

118
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,118 @@
Contributing to Lutris
======================
Finding issues to work on
-------------------------
If you are looking for issues to work on, have a look at the
[milestones](https://github.com/lutris/lutris/milestones) and see which one is
the closest to release then look at the tickets targeted at this release.
If you are less experienced with code, you can also have a look at the issues
that are [not part of a release](https://github.com/lutris/lutris/milestone/29)
which usually include problems with specific games or runners.
Don't forget that lutris is not only a desktop client, there are also a lot of
issues to work on [on the website](https://github.com/lutris/website/issues)
and also in the [build scripts repository](https://github.com/lutris/buildbot)
where you can submit bash scripts for various open source games and engines we
do not already have.
Other areas can benefit non technical help. The Lutris UI is far from being
perfect and could use the input of people experienced with UX and design.
Also, while not fully ready, we do appreciate receiving translations for other
languages. Support for i18n will come in a future update.
Another area where users can help is [confirming some
issues](https://github.com/lutris/lutris/issues?q=is%3Aissue+is%3Aopen+label%3A%22need+help%22)
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.
Running Lutris from Git
-----------------------
Running Lutris from a local git repository is easy, it only requires cloning
the repository and executing Lutris from there.
git clone https://github.com/lutris/lutris
cd lutris
./bin/lutris -d
Make sure you have all necessary dependencies installed. It is recommended that
you keep a stable copy of the client installed with your package manager to
ensure that all dependencies are available.
If you are working on a branch implementing new features, such as the `next`
branch, it might introduce new dependencies. Check in the package configuration
files for new dependencies, for example Debian based distros will have their
dependencies listed in `debian/control` and in `lutris.spec` for RPM based
ones.
Under NO circumstances should you use a virtualenv or install dependencies with
pip. The PyGOject introspection libraries are not regular python packages and
it is not possible to pip install them or use them from a virtualenv. Make
sure to always use PyGOject from your distribution's package manager.
Formatting your code
--------------------
To ensure getting your contributions getting merged faster and to avoid other
developers from going back and fixing your code, please make your code pass the
pylint checks. We highly recommend that you install a pylint plugin for your
code editor. Once you have pylint set up to check the code, you can configure
it to use 120 characters max per line instead of 80.
You can help fixing formatting issues or other code smells by having a look at
the CodeFactor page: https://www.codefactor.io/repository/github/lutris/lutris
Writing tests
-------------
If your patch does not require interactions with a GUI or external processes,
please consider adding unit tests for your code. Have a look at the existing
test suite in the `tests` folder to see what kind of features are tested.
Submitting your changes
-----------------------
Make a new git branch based of `master` in most cases, or `next` if you want to
target a future release. Send a pull request through Github describing what
issue the patch solves. If the PR is related to and existing bug report, you
can add `(Closes #nnn)` or `(Fixes #nnn)` to your PR title or message, where
`nnn` is the ticket number you're fixing. If you have been fixing your PR with
several commits, please consider squashing those commits into one with `git
rebase -i`.
Developer resources
-------------------
Lutris uses Python 3 and GObject / Gtk+ 3 as its core stack, here are some
links to some resources that can help you familiarize yourself with the
project's code base.
* [Python 3 documentation](https://docs.python.org/3/)
* [PyGObject documentation](https://pygobject.readthedocs.io/en/latest/)
* [Python Gtk 3 tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/objects.html)
* [Fakegir GObject code completion](https://github.com/strycore/fakegir)
Project structure
-----------------
[root]-+ Config files and READMEs
|
+-[bin] Main lutris executable script
+-[debian] Debian / Ubuntu packaging configuration
+-[docs] User documentation
+-[lutris]-+ Source folder
| |
| +-[gui] Gtk UI code
| +-[installer] Install script interpreter
| +-[migrations] Migration scripts for user side changes
| +-[runners] Runner code, detailing launch options and settings
| +-[services] External services (Steam, GOG, ...)
| +-[util] Generic utilities
|
+-[po] Translation files
+-[share] Lutris resources like icons, ui files, scripts
+-[tests] Unit tests

View file

@ -2,157 +2,108 @@
Lutris
******
Lutris is an open source gaming platform for GNU/Linux.
It makes gaming on Linux easier by managing, installing and providing optimal
settings for games.
Lutris is an open source gaming platform that makes gaming on Linux easier by
managing, installing and providing optimal settings for games.
Lutris does not sell games; you have to provide your own copy of the games
unless they are open source or freeware.
Games can be installed anywhere on your system; Lutris does not impose
anything.
Lutris relies on various programs referenced as 'runners' to provide a vast
library of games.
These runners (with the exception of Steam and web browsers) are provided by
Lutris, you don't need to install them with your package manager.
We currently support the following runners:
* Linux (Native games)
* Steam
* Web
* Wine
* Wine + Steam
* Libretro
* DOSBox
* MAME
* MESS
* ScummVM
* ResidualVM
* Adventure Game Studio
* Mednafen
* FS-UAE
* Vice
* Stella
* Atari800
* Hatari
* Virtual Jaguar
* Snes9x
* Mupen64Plus
* Dolphin
* PCSX2
* PPSSPP
* Osmose
* Reicast
* Frotz
* jzIntv
* O2EM
* ZDoom
* Citra
* DeSmuME
* DGen
Lutris does not sell games. For commercial games, you must own a copy to install
the game on Lutris.
The platform uses programs referered to as 'runners' to launch games,
Those runners (with the exception of Steam and web browsers) are provided and
managed by Lutris, so you don't need to install them with your package manager.
Scripts written by the community allow access to a library of games.
Using scripts, games can be played without manual setup.
Installer scripts
=================
Lutris automates installation of games using configuration scripts written in
JSON or YAML, which list the various files needed to install a game and can
perform a series of actions on them.
The syntax of installers is described in ``docs/installers.rst``, and is also
available on `lutris.net <https://lutris.net>`_ when writing installers.
Lutris installations are fully automated through scripts, which can be written
in either JSON or YAML.
The scripting syntax is described in ``docs/installers.rst``, and is also
available online at `lutris.net <https://lutris.net>`_.
A web UI is planned to ease the creation of these scripts.
Game Library
Game library
============
You can optionally create an account on `lutris.net <https://lutris.net>`_ and
connect this account to the client.
This will allow you to sync your game library from the website to the client
(not the other way around).
If you wish, you can also sync your Steam library with your Lutris library on
the website.
Optional accounts can be created at `lutris.net
<https://lutris.net>`_ and linked with Lutris clients.
This enables your client to automatically sync fetch library from the website.
**It is currently not possible to sync from the client to the cloud.**
Via the website, it is also possible to sync your Steam library to your Lutris
library.
The client does not store your `lutris.net <https://lutris.net>`_ credentials
on your computer.
Instead, when you authenticate, the website will send a token which will
be used to sync your library.
The Lutris client only stores a token when connected with the website, and your
login credentials are never saved.
This token is stored in ``~/.cache/lutris/auth-token``.
Configuration files
===================
The client, runner, and game configuration files are stored in
``~/.config/lutris``.
There is no need to manually edit these files as everything should be done from
the client.
* ``~/.config/lutris``: The client, runners, and game configuration files
``lutris.conf``: preferences for the client's UI
* There is be no need to manually edit these files as everything should
be done from the client.
``system.yml``: default configuration for every game
* ``lutris.conf``: Preferences for the client's UI
``runners/*.yml``: runner-specific default configurations
* ``system.yml``: Default game configuration, which applies to every game
``games/*.yml``: game-specific configurations
* ``runners/*.yml``: Runner-specific configurations
The game configuration can override previously defined runner and system
configuration and runner configuration can override system configuration.
* ``games/*.yml``: Game-specific configurations
Game-specific configurations supersede runner-specific configurations, which in
turn supersede the system configuration.
Runners and the game database
=========================
=============================
The data necessary to manage your library and run the game is stored in
``~/.local/share/lutris``.
``~/.local/share/lutris``: All data necessary to manage Lutris' library and games, including:
``pga.db``: your game library, game installation status, locations on the
filesystem, and some additional metadata, all stored in an SQLite
database
* ``pga.db``: An SQLite database tracking the game library, game installation
status, various file locations, and some additional metadata
``runners/*``: runners downloaded from `lutris.net <https://lutris.net>`_
``runners/*``: Runners downloaded from `lutris.net <https://lutris.net>`_
``icons/*.png`` and ``banners/*.jpg``: game images
``icons/*.png`` and ``banners/*.jpg``: Game banners and icons
Command line options
====================
The following command line arguments are available::
--version show program's version number and exit
-h, --help show this help message and exit
-v, --verbose Verbose output
-d, --debug Show debug messages
-i INSTALLER_FILE, --install=INSTALLER_FILE
Install a game from a yml file
-l, --list-games List all games in database
-o, --installed Only list installed games
-j, --json Display the list of games in JSON format
--list-steam-games List available Steam games
--list-steam-folders List all known Steam library folders
--reinstall Reinstall game
-v, --version Print the version of Lutris and exit
-d, --debug Show debug messages
-i, --install Install a game from a yml file
-e, --exec Execute a program with the lutris runtime
-l, --list-games List all games in database
-o, --installed Only list installed games
-s, --list-steam-games List available Steam games
--list-steam-folders List all known Steam library folders
-j, --json Display the list of games in JSON format
--reinstall Reinstall game
--display=DISPLAY X display to use
Additionally, you can pass a ``lutris:`` protocol link followed by a game
identifier on the command line such as::
lutris lutris:quake
This will install the game if not already installed or launch the game
otherwise (unless the ``--reinstall`` flag is passed).
This will install the game if it is not already installed, otherwise it will
launch the game. The game will always be installed if the ``--reinstall`` flag is passed.
Planned features
================
Lutris is far from complete and some of the more interesting features have yet
Lutris is far from complete, and some features have yet
to be implemented.
Here's what to expect from the future versions of Lutris:
Here's what to expect from future versions of Lutris:
* Integration with GOG and Humble Bundle
* Integration with the TOSEC database
* Management of Personnal Game Archives (let you store your games files on
private storage, allowing you to reinstall them on all your devices)
* Game saves sync
* Community features (friends list, chat, multiplayer game scheduling)
* GOG and Humble Bundle integration
* TOSEC database integration
* Management of personal game data (i.e. syncing games across devices using private cloud storage)
* Community features (friends list, chat, multiplayer game scheduling, etc.)
* Controller configuration GUI (with xboxdrv support)
Come with us!

79
debian/changelog vendored
View file

@ -1,8 +1,81 @@
lutris (0.5.0-alpha) bionic; urgency=medium
lutris (0.4.23) bionic; urgency=medium
* Experimental packages
* Prevent monitor from quitting games that open a 2nd process
* Run on-demand scripts from game directory
* Tell the user what executable is expected after a failed install
* Fix a circular import causing issues on some distributions
* Add missing dependency for openSUSE Tumbleweed
-- Mathieu Comandon <strycore@gmail.com> Tue, 31 Jul 2018 19:23:31 -0700
-- Mathieu Comandon <strycore@gmail.com> Tue, 06 Nov 2018 19:10:19 -0800
lutris (0.4.22) bionic; urgency=medium
* Use lspci instead of xrandr to detect video cards
* Detect if Vulkan is supported by the system for DXVK games
* Add experimental playtime support
* Detect Proton and add it to Wine versions
* Fix runtime being downloaded when not needed
* Add experimental tray icon with last games played
* Add support for Feral Gamemode
* Prevent process monitor to quit games prematurely
* Code cleanup
-- Mathieu Comandon <strycore@gmail.com> Sat, 03 Nov 2018 00:01:19 -0700
lutris (0.4.21.1) bionic; urgency=medium
* Fix detection of libvulkan
-- Mathieu Comandon <strycore@gmail.com> Tue, 23 Oct 2018 19:31:14 -0700
lutris (0.4.21) bionic; urgency=medium
* Added an Esync toggle for wine builds with esync patches and a check for limits if the toggle was activated.
* Added a warning for wine games if wine is not installed on the system (to avoid issues with dependencies).
* Added a check for Vulkan loaders when using DXVK (forbids from launching the game if it can't detect them)
* Added check for the presence of executable after the installation finished.
* Added an option to sort installed games first
* Added a discouraging warning if Lutris was launched as root.
* Added a "--version" command line option.
* Added an error message if requested DXVK version does not exist.
* Improved behavior of Lutris' background process.
* Improved UI when changing game's identifier.
* Wine's own Virtual Desktop configuration is now respected.
* Merge command now has a 'copy' alias.
* Executable selection how has a text field.
* Blacklisted Proton and SteamWorks from showing up as games.
* Sidebar now shows number of installed games per runner and platform.
* Visual improvements to wine download dialog
* Fixed an issue when DXVK versions didn't get updated if dxvk directory wasn't present.
* Fixed an issue when the watcher would sync Steam games even if the feature was disabled.
* Fixed missing warning for existing prefix during installation process if the path contained "~".
* Prevent Steam games from being synced from the AppManifest watcher if Steam sync if off
* Games load properly when launching Lutris for the first time
* Minor improvements to wording in some menus.
-- Mathieu Comandon <strycore@gmail.com> Sat, 20 Oct 2018 17:39:31 -0700
lutris (0.4.20) bionic; urgency=medium
* Fix detection of winetricks path
* Improve visual feedback on wine download dialog
* Add skill and command-line arguments for Zdoom
* Add option to disable joypad auto-configuration
* Restore refresh rate on monitor reset
-- Mathieu Comandon <strycore@gmail.com> Mon, 24 Sep 2018 20:46:46 -0700
lutris (0.4.19) bionic; urgency=medium
* Prioritize winetricks from the runtime
* Populate DXVK versions with github releases
* Add support for DirectX 10 with DXVK
* Fix detection of xgamma
* Add 24BPP option for Xephyr
* Restore Alsa option for Wine
* Prepend additional system paths to runtime
-- Mathieu Comandon <strycore@gmail.com> Tue, 04 Sep 2018 18:48:52 -0700
lutris (0.4.18) bionic; urgency=medium

6
debian/control vendored
View file

@ -24,7 +24,11 @@ Depends: ${misc:Depends},
gir1.2-gtk-3.0,
gir1.2-gnomedesktop-3.0,
psmisc,
cabextract
cabextract,
unrar,
unzip,
p7zip,
curl
Recommends: python3-evdev,
libc6-i386 [amd64],
lib32gcc1 [amd64]

View file

@ -26,10 +26,10 @@ Examples:
::
files:
- file1: http://site.com/gamesetup.exe
- file1: https://example.com/gamesetup.exe
- file2: "N/A:Select the game's setup file"
- file3:
url: http://site.com/url-that-doesnt-resolve-to-a-proper-filename
url: https://example.com/url-that-doesnt-resolve-to-a-proper-filename
filename: actual_local_filename.zip
referer: www.mywebsite.com
@ -206,13 +206,14 @@ Example:
::
- move:
src: $game-file-id
src: game_file_id
dst: $GAMEDIR/location
Copying and merging directories
-------------------------------
Both merging and copying actions are done with the ``merge`` directive.
Both merging and copying actions are done with the ``merge`` or the ``copy`` directive.
It is not important which of these directives is used because ``copy`` is just an alias for ``merge``.
Whether the action does a merge or copy depends on the existence of the
destination directory. When merging into an existing directory, original files
with the same name as the ones present in the merged directory will be
@ -227,7 +228,7 @@ Example:
::
- merge:
src: $game-file-id
src: game_file_id
dst: $GAMEDIR/location
Extracting archives
@ -247,7 +248,7 @@ Example:
::
- extract:
file: $game-archive
file: game_archive
dst: $GAMEDIR/datadir/
Making a file executable
@ -276,7 +277,7 @@ Example:
- execute:
args: --argh
file: $great-id
file: great_id
terminal: true
env:
key: value
@ -569,7 +570,7 @@ Currently, the following tasks are implemented:
- task:
name: dosexec
executable: $file_id
executable: file_id
config: $GAMEDIR/game_install.conf
args: -scaler normal3x -conf more_conf.conf
@ -636,7 +637,7 @@ Example Linux game:
working_dir: $GAMEDIR
files:
- myfile: http://example.com/mygame.zip
- myfile: https://example.com/mygame.zip
installer:
- chmodx: $GAMEDIR/mygame
@ -666,10 +667,10 @@ Example wine game:
- installer: "N/A:Select the game's setup file"
installer:
- task:
executable: installer
name: wineexec
prefix: $GAMEDIR/prefix
arch: win64
executable: installer
name: wineexec
prefix: $GAMEDIR/prefix
arch: win64
wine:
Desktop: true
WineDesktop: 1024x768
@ -681,7 +682,11 @@ Example wine game:
WINEDLLOVERRIDES: d3d11=
SOMEENV: true
Example gog wine game, some installer crash with with /SILENT or /VERYSILENT option (Cuphead and Star Wars: Battlefront II for example), (most options can be found here http://www.jrsoftware.org/ishelp/index.php?topic=setupcmdline, there is undocumented gog option ``/nogui``, you need to use it when you use ``/silent`` and ``/suppressmsgboxes`` parameters):
Example gog wine game, some installer crash with with /SILENT or /VERYSILENT
option (Cuphead and Star Wars: Battlefront II for example), (most options can
be found here http://www.jrsoftware.org/ishelp/index.php?topic=setupcmdline,
there is undocumented gog option ``/NOGUI``, you need to use it when you use
``/SILENT`` and ``/SUPPRESSMSGBOXES`` parameters):
::
@ -702,11 +707,11 @@ Example gog wine game, some installer crash with with /SILENT or /VERYSILENT opt
- installer: "N/A:Select the game's setup file"
installer:
- task:
args: /SILENT /LANG=en /SP- /NOCANCEL /SUPPRESSMSGBOXES /NOGUI /DIR="C:/game"
executable: installer
name: wineexec
prefix: $GAMEDIR/prefix
arch: win64
args: /SILENT /LANG=en /SP- /NOCANCEL /SUPPRESSMSGBOXES /NOGUI /DIR="C:/game"
executable: installer
name: wineexec
prefix: $GAMEDIR/prefix
arch: win64
wine:
Desktop: true
WineDesktop: 1024x768
@ -740,7 +745,7 @@ Example gog wine game, alternative (requires innoextract):
- installer: "N/A:Select the game's setup file"
installer:
- execute:
args: --gog -d "$CACHE" "$setup"
args: --gog -d "$CACHE" setup
description: Extracting game data
file: innoextract
- move:
@ -779,9 +784,9 @@ Example gog linux game (mojosetup options found here https://www.reddit.com/r/li
installer:
- chmodx: installer
- execute:
executable: installer
description: Installing game, it will take a while...
args: -- --i-agree-to-all-licenses --noreadme --nooptions --noprompt --destination=$GAMEDIR
file: installer
description: Installing game, it will take a while...
args: -- --i-agree-to-all-licenses --noreadme --nooptions --noprompt --destination=$GAMEDIR
system:
terminal: true
@ -804,12 +809,12 @@ Example gog linux game, alternative (requires unzip):
- installer: "N/A:Select the game's setup file"
installer:
- execute:
args: $installer -d "$GAMEDIR" "data/noarch/*"
description: Extracting game data, it will take a while...
file: unzip
args: installer -d "$GAMEDIR" "data/noarch/*"
description: Extracting game data, it will take a while...
file: unzip
- rename:
dst: $GAMEDIR/Game
src: $GAMEDIR/data/noarch
dst: $GAMEDIR/Game
src: $GAMEDIR/data/noarch
system:
terminal: true
@ -832,10 +837,10 @@ Example winesteam game:
arch: win64
installer:
- task:
description: Setting up wine prefix
name: create_prefix
prefix: $GAMEDIR/prefix
arch: win64
description: Setting up wine prefix
name: create_prefix
prefix: $GAMEDIR/prefix
arch: win64
winesteam:
Desktop: true
WineDesktop: 1024x768
@ -877,7 +882,7 @@ When submitting the installer script to lutris.net, only copy the script part. R
args: --some-arg
files:
- myfile: http://example.com
- myfile: https://example.com
installer:
- chmodx: $GAMEDIR/mygame

View file

@ -6,7 +6,7 @@
%global appid net.lutris.Lutris
Name: lutris
Version: 0.5.0-alpha
Version: 0.4.23
Release: 2%{?dist}
Summary: Install and play any video game easily
@ -25,7 +25,7 @@ BuildRequires: python3-devel
BuildRequires: python3-gobject, python3-wheel, python3-setuptools, python3-gobject
Requires: python3-gobject, python3-PyYAML, cabextract, gnome-deskop3
Requires: gtk3, psmisc, xorg-x11-server-Xephyr, xorg-x11-server-utils
Recommends: wine
Recommends: wine-core
%endif
%if 0%{?rhel} || 0%{?centos}
BuildRequires: python3-gobject
@ -38,12 +38,8 @@ BuildRequires: update-desktop-files
BuildRequires: hicolor-icon-theme
BuildRequires: polkit
BuildRequires: python3-setuptools
Requires: python3-gobject, python3-PyYAML, cabextract
%endif
# Add Gdk dependency for Tumbleweed (package unavailable for other releases)
%if 0%{?suse_version} > 1500
Requires: python3-gobject-Gdk
Requires: (python3-gobject-Gdk or python3-gobject)
Requires: python3-PyYAML, cabextract, typelib-1_0-Gtk-3_0
%endif
%if 0%{?fedora} || 0%{?suse_version}

View file

@ -1,3 +1,4 @@
"""Functions to interact with the Lutris REST API"""
import os
import re
import json
@ -7,7 +8,7 @@ import urllib.error
import socket
from lutris import settings
from lutris.util import http
from lutris.util import http, system
from lutris.util.log import logger
@ -15,8 +16,8 @@ API_KEY_FILE_PATH = os.path.join(settings.CACHE_DIR, 'auth-token')
def read_api_key():
if not os.path.exists(API_KEY_FILE_PATH):
return
if not system.path_exists(API_KEY_FILE_PATH):
return None
with open(API_KEY_FILE_PATH, 'r') as token_file:
api_string = token_file.read()
username, token = api_string.split(":")
@ -45,7 +46,7 @@ def connect(username, password):
def disconnect():
if not os.path.exists(API_KEY_FILE_PATH):
if not system.path_exists(API_KEY_FILE_PATH):
return
os.remove(API_KEY_FILE_PATH)
@ -65,8 +66,7 @@ def get_library():
response_data = response.json
if response_data:
return response_data['games']
else:
return []
return []
def get_runners(runner_name):
@ -75,7 +75,8 @@ def get_runners(runner_name):
return response.json
def get_games(game_slugs=None, page=1):
def get_game_api_page(game_slugs, page):
"""Read a single page of games from the API and return the response"""
url = settings.SITE_URL + "/api/games"
if int(page) > 1:
@ -88,24 +89,30 @@ def get_games(game_slugs=None, page=1):
payload = None
response.get(data=payload)
response_data = response.json
logger.info("Loaded %s games from page %s",
len(response_data.get('results')), page)
if not response_data:
logger.warning('Unable to get games from API')
return None
return response_data
def get_games(game_slugs=None, page='1'):
response_data = get_game_api_page(game_slugs, page)
results = response_data.get('results', [])
while response_data.get('next'):
page_match = re.search(r'page=(\d+)', response_data['next'])
if page_match:
page = page_match.group(1)
next_page = page_match.group(1)
else:
logger.error("No page found in %s", response_data['next'])
break
page_result = get_games(game_slugs=game_slugs, page=page)
if not page_result:
logger.warning("Unable to get response for page %s", page)
logger.debug("Current page is %s, next page is %s", page, next_page)
response_data = get_game_api_page(game_slugs=game_slugs, page=next_page)
if not response_data.get('results'):
logger.warning("Unable to get response for page %s", next_page)
break
else:
results += page_result
results += response_data.get('results')
return results

View file

@ -1,10 +1,11 @@
"""Handle the game, runner and global system configurations."""
import os
from os.path import join
import sys
import time
import yaml
from os.path import join
from gi.repository import Gio
@ -68,12 +69,14 @@ def read_yaml_from_file(filename):
"""Read filename and return parsed yaml"""
if not path_exists(filename):
return {}
try:
content = open(filename, 'r').read()
yaml_content = yaml.load(content) or {}
except (yaml.scanner.ScannerError, yaml.parser.ParserError):
logger.error("error parsing file %s", filename)
yaml_content = {}
with open(filename, 'r') as yaml_file:
try:
yaml_content = yaml.safe_load(yaml_file) or {}
except (yaml.scanner.ScannerError, yaml.parser.ParserError):
logger.error("error parsing file %s", filename)
yaml_content = {}
return yaml_content
@ -85,7 +88,7 @@ def write_yaml_to_file(filepath, config):
filehandler.write(yaml_config)
class LutrisConfig(object):
class LutrisConfig:
"""Class where all the configuration handling happens.
Description
@ -177,14 +180,14 @@ class LutrisConfig(object):
@property
def runner_config_path(self):
if not self.runner_slug:
return
return None
return os.path.join(settings.CONFIG_DIR, "runners/%s.yml" %
self.runner_slug)
@property
def game_config_path(self):
if not self.game_config_id or self.game_config_id == TEMP_CONFIG:
return
return None
return os.path.join(settings.CONFIG_DIR, "games/%s.yml" %
self.game_config_id)
@ -236,7 +239,7 @@ class LutrisConfig(object):
self.raw_config = raw_config
def remove(self, game=None):
def remove(self):
"""Delete the configuration file from disk."""
if path_exists(self.game_config_path):
os.remove(self.game_config_path)
@ -272,14 +275,13 @@ class LutrisConfig(object):
def options_as_dict(self, options_type):
"""Convert the option list to a dict with option name as keys"""
options = {}
if options_type == 'system':
options = (sysoptions.with_runner_overrides(self.runner_slug)
if self.runner_slug
else sysoptions.system_options)
else:
if not self.runner_slug:
return
return None
attribute_name = options_type + '_options'
try:

View file

@ -19,6 +19,7 @@ from lutris.util.log import logger
from lutris.config import LutrisConfig
from lutris.thread import LutrisThread, HEARTBEAT_DELAY
from lutris.gui import dialogs
from lutris.util.timer import Timer
def watch_lutris_errors(function):
@ -49,18 +50,19 @@ class Game(GObject.Object):
"game-error": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
}
def __init__(self, id=None):
def __init__(self, game_id=None):
super().__init__()
self.id = id
self.id = game_id
self.runner = None
self.game_thread = None
self.prelaunch_thread = None
self.heartbeat = None
self.config = None
self.killswitch = None
self.state = self.STATE_IDLE
self.exit_main_loop = False
game_data = pga.get_game_by_field(id, 'id')
game_data = pga.get_game_by_field(game_id, 'id')
self.slug = game_data.get('slug') or ''
self.runner_name = game_data.get('runner') or ''
self.directory = game_data.get('directory') or ''
@ -83,6 +85,9 @@ class Game(GObject.Object):
self.log_buffer = Gtk.TextBuffer()
self.log_buffer.create_tag("warning", foreground="red")
self.timer = Timer()
self.playtime = game_data.get('playtime') or ''
def __repr__(self):
return self.__unicode__()
@ -92,16 +97,17 @@ class Game(GObject.Object):
value += " (%s)" % self.runner_name
return value
def show_error_message(self, message):
@staticmethod
def show_error_message(message):
"""Display an error message based on the runner's output."""
if "CUSTOM" == message['error']:
if message['error'] == "CUSTOM":
message_text = message['text'].replace('&', '&amp;')
dialogs.ErrorDialog(message_text)
elif "RUNNER_NOT_INSTALLED" == message['error']:
elif message['error'] == "RUNNER_NOT_INSTALLED":
dialogs.ErrorDialog('Error the runner is not installed')
elif "NO_BIOS" == message['error']:
elif message['error'] == "NO_BIOS":
dialogs.ErrorDialog("A bios file is required to run this game")
elif "FILE_NOT_FOUND" == message['error']:
elif message['error'] == "FILE_NOT_FOUND":
filename = message['file']
if filename:
message_text = "The file {} could not be found".format(
@ -110,8 +116,7 @@ class Game(GObject.Object):
else:
message_text = "No file provided"
dialogs.ErrorDialog(message_text)
elif "NOT_EXECUTABLE" == message['error']:
elif message['error'] == "NOT_EXECUTABLE":
message_text = message['file'].replace('&', '&amp;')
dialogs.ErrorDialog("The file %s is not executable" % message_text)
@ -136,10 +141,11 @@ class Game(GObject.Object):
else:
self.runner = runner_class(self.config)
def desktop_effects(self, enable):
def set_desktop_compositing(self, enable):
if enable:
system.execute(self.start_compositor, shell=True)
else:
<<<<<<< HEAD
session = os.environ.get('DESKTOP_SESSION')
if session == "plasma":
self.stop_compositor = "qdbus org.kde.KWin /Compositor org.kde.kwin.Compositing.suspend"
@ -152,6 +158,10 @@ class Game(GObject.Object):
self.start_compositor = "xfconf-query --channel=xfwm4 --property=/general/use_compositing --set=true"
if not (self.compositor_disabled or self.stop_compositor == ""):
=======
self.start_compositor, self.stop_compositor = display.get_compositor_commands()
if not (self.compositor_disabled or not self.stop_compositor):
>>>>>>> master
system.execute(self.stop_compositor, shell=True)
self.compositor_disabled = True
@ -201,7 +211,8 @@ class Game(GObject.Object):
installed=self.is_installed,
configpath=self.config.game_config_id,
steamid=self.steamid,
id=self.id
id=self.id,
playtime=self.playtime,
)
def prelaunch(self):
@ -218,6 +229,7 @@ class Game(GObject.Object):
dialogs.ErrorDialog("Runtime currently updating",
"Game might not work as expected")
if "wine" in self.runner_name and not wine.get_system_wine_version():
<<<<<<< HEAD
dialogs.DontShowAgainDialog(
'hide-wine-systemwide-install-warning',
"Wine is not installed on your system.",
@ -227,6 +239,12 @@ class Game(GObject.Object):
"href='https://github.com/lutris/lutris/wiki/Wine'>Lutris Wiki</a> to "
"install Wine"
)
=======
# TODO find a reference to the root window or better yet a way not
# to have Gtk dependent code in this class.
root_window = None
dialogs.WineNotInstalledWarning(parent=root_window)
>>>>>>> master
return True
def play(self):
@ -252,8 +270,16 @@ class Game(GObject.Object):
self.do_play(True)
@watch_lutris_errors
def do_play(self, prelaunched, _error=None):
def do_play(self, prelaunched, error=None):
self.timer.start_t()
if error:
logger.error(error)
dialogs.ErrorDialog(str(error))
if not prelaunched:
logger.error("Game prelaunch unsuccessful")
dialogs.ErrorDialog("An error prevented the game from running")
self.state = self.STATE_STOPPED
return
system_config = self.runner.system_config
@ -263,7 +289,7 @@ class Game(GObject.Object):
)
gameplay_info = self.runner.play()
logger.info("Launching %s", self.name)
logger.debug("Launching %s: %s", self.name, gameplay_info)
if 'error' in gameplay_info:
self.show_error_message(gameplay_info)
self.state = self.STATE_STOPPED
@ -274,7 +300,7 @@ class Game(GObject.Object):
sdl_gamecontrollerconfig = system_config.get('sdl_gamecontrollerconfig')
if sdl_gamecontrollerconfig:
path = os.path.expanduser(sdl_gamecontrollerconfig)
if os.path.exists(path):
if system.path_exists(path):
with open(path, "r") as f:
sdl_gamecontrollerconfig = f.read()
env['SDL_GAMECONTROLLERCONFIG'] = sdl_gamecontrollerconfig
@ -316,7 +342,7 @@ class Game(GObject.Object):
audio.reset_pulse()
self.killswitch = system_config.get('killswitch')
if self.killswitch and not os.path.exists(self.killswitch):
if self.killswitch and not system.path_exists(self.killswitch):
# Prevent setting a killswitch to a file that doesn't exists
self.killswitch = None
@ -415,11 +441,26 @@ class Game(GObject.Object):
monitoring_disabled = system_config.get('disable_monitoring')
if monitoring_disabled:
show_obnoxious_process_monitor_message()
dialogs.ErrorDialog("<b>The process monitor is disabled, Lutris "
"won't be able to keep track of the game status. "
"If this game requires the process monitor to be "
"disabled in order to run, please submit an "
"issue.</b>\n"
"To disable this message, re-enable the process monitor")
process_watch = not monitoring_disabled
if self.runner.system_config.get('disable_compositor'):
self.desktop_effects(False)
self.set_desktop_compositing(False)
prelaunch_command = self.runner.system_config.get("prelaunch_command")
if system.path_exists(prelaunch_command):
self.prelaunch_thread = LutrisThread(
[prelaunch_command],
include_processes=[os.path.basename(prelaunch_command)],
cwd=self.directory
)
self.prelaunch_thread.start()
logger.info("Running %s in the background", prelaunch_command)
self.game_thread = LutrisThread(launch_arguments,
runner=self.runner,
@ -449,15 +490,16 @@ class Game(GObject.Object):
command = [
"pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver",
"--dbus", "session", "--silent"
] + config.split()
] + shlex.split(config)
logger.debug("[xboxdrv] %s", ' '.join(command))
self.xboxdrv_thread = LutrisThread(command, include_processes=['xboxdrv'])
self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop)
self.xboxdrv_thread.start()
def xboxdrv_stop(self):
@staticmethod
def xboxdrv_stop():
os.system("pkexec xboxdrvctl --shutdown")
if os.path.exists("/usr/share/lutris/bin/resetxpad"):
if system.path_exists("/usr/share/lutris/bin/resetxpad"):
os.system("pkexec /usr/share/lutris/bin/resetxpad")
def beat(self):
@ -471,7 +513,7 @@ class Game(GObject.Object):
# The killswitch file should be set to a device (ie. /dev/input/js0)
# When that device is unplugged, the game is forced to quit.
killswitch_engage = (
self.killswitch and not os.path.exists(self.killswitch)
self.killswitch and not system.path_exists(self.killswitch)
)
if not self.game_thread.is_running or killswitch_engage:
logger.debug("Game thread stopped")
@ -489,10 +531,30 @@ class Game(GObject.Object):
def on_game_quit(self):
"""Restore some settings and cleanup after game quit."""
self.timer.end_t()
self.playtime = self.timer.increment(self.playtime)
if self.prelaunch_thread:
logger.info("Stopping prelaunch script")
self.prelaunch_thread.stop()
self.heartbeat = None
if self.state != self.STATE_STOPPED:
logger.debug("Game thread still running, stopping it (state: %s)", self.state)
self.stop()
# Check for post game script
postexit_command = self.runner.system_config.get("postexit_command")
if system.path_exists(postexit_command):
logger.info("Running post-exit command: %s", postexit_command)
postexit_thread = LutrisThread(
[postexit_command],
include_processes=[os.path.basename(postexit_command)],
cwd=self.directory
)
postexit_thread.start()
quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
logger.debug("%s stopped at %s", self.name, quit_time)
self.lastplayed = int(time.time())
@ -505,7 +567,7 @@ class Game(GObject.Object):
display.change_resolution(self.original_outputs)
if self.compositor_disabled:
self.desktop_effects(True)
self.set_desktop_compositing(True)
if self.runner.system_config.get('use_us_layout'):
subprocess.Popen(['setxkbmap'], env=os.environ).communicate()
@ -538,6 +600,8 @@ class Game(GObject.Object):
def notify_steam_game_changed(self, appmanifest):
"""Receive updates from Steam games and set the thread's ready state accordingly"""
if not self.game_thread:
return
if 'Fully Installed' in appmanifest.states and not self.game_thread.ready_state:
logger.info("Steam game %s is fully installed", appmanifest.steamid)
self.game_thread.ready_state = True
@ -545,21 +609,3 @@ class Game(GObject.Object):
logger.info("Steam game %s updating, setting game thread as not ready",
appmanifest.steamid)
self.game_thread.ready_state = False
def show_obnoxious_process_monitor_message():
"""Display an annoying message for people who disable the process monitor"""
for _ in range(5):
logger.critical("")
logger.critical(" ****************************************************")
logger.critical(" ****************************************************")
logger.critical(" *** YOU HAVE THE PROCESS MONITOR DISABLED!!!!! ***")
logger.critical(" ****************************************************")
logger.critical(" ****************************************************")
logger.critical("THIS OPTION WAS IMPLEMENTED AS A WORKAROUND FOR A BUG THAT HAS BEEN FIXED!!11!!1")
logger.critical("RUNNING GAMES WITH THE PROCESS MONITOR DISABLED IS NOT SUPPORTED!!!")
logger.critical("YOU ARE DISCOURAGED FROM REPORTING ISSUES WITH THE PROCESS MONITOR DISABLED!!!")
logger.critical("THIS OPTION WILL BE REMOVED IN A FUTURE RELEASE!!!!!!!")
logger.critical("IF YOU THINK THIS OPTION CAN BE USEFUL FOR ANY MEANS PLEASE LET US KNOW!!!!")
for _ in range(5):
logger.critical("")

View file

@ -1,4 +1,4 @@
# application.py
# pylint: disable=no-member
#
# Copyright (C) 2016 Patrick Griffis <tingping@tingping.se>
#
@ -19,28 +19,30 @@ import json
import logging
import os
import signal
import sys
import gettext
from gettext import gettext as _
import gi
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, GLib, Gtk
import gi # isort:skip
gi.require_version('Gdk', '3.0') # NOQA # isort:skip
gi.require_version('Gtk', '3.0') # NOQA # isort:skip
from gi.repository import Gio, GLib, Gtk
from lutris import pga
from lutris import settings
from lutris.config import check_config
from lutris.platforms import update_platforms
from lutris.gui.dialogs import ErrorDialog, InstallOrPlayDialog
from lutris.migrations import migrate
from lutris.platforms import update_platforms
from lutris.services.steam import AppManifest, get_appmanifests, get_steamapps_paths
from lutris.settings import read_setting, VERSION
from lutris.thread import exec_in_thread
from lutris.util import datapath
from lutris.util.log import logger, console_handler, DEBUG_FORMATTER
from lutris.util.resources import parse_installer_url
from lutris.services.steam import (AppManifest, get_appmanifests,
get_steamapps_paths)
from .lutriswindow import LutrisWindow
from lutris.gui.lutristray import LutrisTray
class Application(Gtk.Application):
@ -58,8 +60,12 @@ class Application(Gtk.Application):
GLib.set_application_name(_('Lutris'))
self.window = None
self.help_overlay = None
self.tray = None
self.css_provider = Gtk.CssProvider.new()
if os.geteuid() == 0:
ErrorDialog("Running Lutris as root is not recommended and may cause unexpected issues")
try:
self.css_provider.load_from_path(os.path.join(datapath.get(), 'ui', 'lutris.css'))
except GLib.Error as e:
@ -74,12 +80,19 @@ class Application(Gtk.Application):
if hasattr(self, 'set_option_context_summary'):
self.set_option_context_summary(
'Run a game directly by adding the parameter lutris:rungame/game-identifier.\n'
'If several games share the same identifier you can use the '
'numerical ID (displayed when running lutris --list-games) and add lutris:rungameid/numerical-id.\n'
'If several games share the same identifier you can use the numerical ID '
'(displayed when running lutris --list-games) and add '
'lutris:rungameid/numerical-id.\n'
'To install a game, add lutris:install/game-identifier.'
)
else:
logger.warning("This version of Gtk doesn't support set_option_context_summary")
self.add_main_option('version',
ord('v'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Print the version of Lutris and exit'),
None)
self.add_main_option('debug',
ord('d'),
GLib.OptionFlags.NONE,
@ -168,6 +181,18 @@ class Application(Gtk.Application):
shortcuts_item = Gio.MenuItem.new(_('Keyboard Shortcuts'), 'win.show-help-overlay')
last_section.prepend_item(shortcuts_item)
menubar = builder.get_object('menubar')
self.set_menubar(menubar)
self.set_tray_icon(read_setting('show_tray_icon', default='false') == 'true')
def set_tray_icon(self, active=False):
"""Creates or destroys a tray icon for the application"""
if self.tray:
self.tray.set_visible(active)
else:
self.tray = LutrisTray(application=self)
self.tray.set_visible(active)
def do_activate(self):
if not self.window:
self.window = LutrisWindow(application=self)
@ -179,7 +204,7 @@ class Application(Gtk.Application):
self.css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
self.window.present()
GLib.timeout_add(300, self.refresh_status)
@staticmethod
def _print(command_line, string):
@ -196,6 +221,13 @@ class Application(Gtk.Application):
# Text only commands
# Print Lutris version and exit
if options.contains('version'):
executable_name = os.path.basename(sys.argv[0])
print(executable_name + "-" + VERSION)
logger.setLevel(logging.NOTSET)
return 0
# List game
if options.contains('list-games'):
game_list = pga.get_games()
@ -278,37 +310,40 @@ class Application(Gtk.Application):
action = 'install'
if action == 'install':
self.window.present()
self.window.on_install_clicked(game_slug=game_slug,
installer_file=installer_file,
revision=revision)
elif action in ('rungame', 'rungameid'):
if not db_game or not db_game['id']:
logger.info("No game found in library, shutting down")
self.do_shutdown()
if self.window.is_visible():
logger.info("No game found in library")
else:
logger.info("No game found in library, shutting down")
self.do_shutdown()
return 0
logger.info("Launching %s" % db_game['name'])
logger.info("Launching %s", db_game['name'])
# If game is installed, run it without showing the GUI
# Also set a timer to shut down lutris when game ends
if db_game['installed']:
self.window.hide()
self.window.on_game_run(game_id=db_game['id'])
GLib.timeout_add(300, self.refresh_status)
# If game is not installed, show the GUI
else:
self.window.on_game_run(game_id=db_game['id'])
# If game is not installed, show the GUI before running. Otherwise leave the GUI closed.
if not db_game['installed']:
self.window.present()
self.window.on_game_run(game_id=db_game['id'])
else:
self.window.present()
return 0
def refresh_status(self):
if self.window.running_game.state == self.window.running_game.STATE_STOPPED:
self.do_shutdown()
return False
if self.window.running_game is None or self.window.running_game.state == self.window.running_game.STATE_STOPPED:
if not self.window.is_visible():
self.do_shutdown()
return False
return True
def get_lutris_action(self, url):
@staticmethod
def get_lutris_action(url):
installer_info = {
'game_slug': None,
'revision': None,
@ -368,12 +403,13 @@ class Application(Gtk.Application):
)
)
def execute_command(self, command):
@staticmethod
def execute_command(command):
"""
Execute an arbitrary command in a Lutris context
with the runtime enabled and monitored by LutrisThread
"""
logger.info("Running command '{}'".format(command))
logger.info("Running command '%s'", command)
thread = exec_in_thread(command)
try:
GLib.MainLoop().run()

View file

@ -1,4 +1,5 @@
"""Widget generators and their signal handlers"""
# pylint: disable=no-member
import os
from gi.repository import Gtk, Gdk
@ -18,6 +19,8 @@ class ConfigBox(VBox):
self.game = game
self.config = None
self.raw_config = None
self.option_widget = None
self.wrapper = None
def generate_top_info_box(self, text):
"""Add a top section with general help text for the current tab"""
@ -86,7 +89,7 @@ class ConfigBox(VBox):
# Set tooltip's "Default" part
default = option.get('default')
self.tooltip_default = default if type(default) is str else None
self.tooltip_default = default if isinstance(default, str) else None
# Generate option widget
self.option_widget = None
@ -108,7 +111,7 @@ class ConfigBox(VBox):
# Tooltip
helptext = option.get("help")
if type(self.tooltip_default) is str:
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:
@ -161,6 +164,9 @@ 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"],
@ -206,10 +212,34 @@ class ConfigBox(VBox):
self.wrapper.pack_start(checkbox, True, True, 5)
self.option_widget = checkbox
# Checkbox with callback
def generate_checkbox_with_callback(self, option, value=None):
"""Generate a checkbox. With callback"""
checkbox = Gtk.CheckButton(label=option["label"])
if option['active'] is True:
checkbox.set_sensitive(True)
else:
checkbox.set_sensitive(False)
if value is True:
checkbox.set_active(value)
checkbox.connect("toggled", self.checkbox_toggle_with_callback, option['option'], option['callback'], option['callback_on'])
self.wrapper.pack_start(checkbox, True, True, 5)
self.option_widget = checkbox
def checkbox_toggle(self, widget, option_name):
"""Action for the checkbox's toggled signal."""
self.option_changed(widget, option_name, widget.get_active())
def checkbox_toggle_with_callback(self, widget, option_name, callback, callback_on=None):
"""Action for the checkbox's toggled signal. With callback method"""
if widget.get_active() == callback_on or callback_on is None:
if callback(self.config):
self.option_changed(widget, option_name, widget.get_active())
else:
widget.set_active(False)
else:
self.option_changed(widget, option_name, widget.get_active())
# Entry
def generate_entry(self, option_name, label, value=None):
"""Generate an entry box."""
@ -233,7 +263,7 @@ class ConfigBox(VBox):
"""Generate a combobox (drop-down menu)."""
liststore = Gtk.ListStore(str, str)
for choice in choices:
if type(choice) is str:
if isinstance(choice, str):
choice = [choice, choice]
if choice[1] == default and not has_entry:
# Do not add default label to editable dropdowns since this gets
@ -263,6 +293,7 @@ class ConfigBox(VBox):
combobox.set_active_id(default)
combobox.connect('changed', self.on_combobox_change, option_name)
combobox.connect('scroll-event', self.on_combobox_scroll)
label = Label(label)
label.set_alignment(0.5, 0.5)
combobox.set_valign(Gtk.Align.CENTER)
@ -270,6 +301,13 @@ class ConfigBox(VBox):
self.wrapper.pack_start(combobox, True, True, 0)
self.option_widget = combobox
@staticmethod
def on_combobox_scroll(combobox, event):
"""Do not change options when scrolling
with cursor inside a ComboBox."""
combobox.stop_emission_by_name('scroll-event')
return False
def on_combobox_change(self, combobox, option):
"""Action triggered on combobox 'changed' signal."""
if combobox.get_has_entry():
@ -309,34 +347,39 @@ class ConfigBox(VBox):
"""Generate a file chooser button to select a file."""
option_name = option['option']
label = Label(option['label'])
file_chooser = Gtk.FileChooserButton("Choose a file for %s" % label)
file_chooser = FileChooserEntry(
title='Select file',
action=Gtk.FileChooserAction.OPEN,
default_path=path # reverse_expanduser(path)
)
file_chooser.set_size_request(200, 30)
if 'default_path' in option:
config_key = option['default_path']
default_path = self.lutris_config.system_config.get(config_key)
if default_path and os.path.exists(default_path):
file_chooser.set_current_folder(default_path)
file_chooser.entry.set_text(default_path)
file_chooser.set_action(Gtk.FileChooserAction.OPEN)
file_chooser.connect("file-set", self.on_chooser_file_set,
option_name)
if path:
# If path is relative, complete with game dir
if not os.path.isabs(path):
path = os.path.join(self.game.directory, path)
file_chooser.unselect_all()
file_chooser.select_filename(path)
label.set_alignment(0.5, 0.5)
path = os.path.expanduser(path)
if not os.path.isabs(path):
path = os.path.join(self.game.directory, path)
file_chooser.entry.set_text(path)
file_chooser.set_valign(Gtk.Align.CENTER)
label.set_alignment(0.5, 0.5)
self.wrapper.pack_start(label, False, False, 0)
self.wrapper.pack_start(file_chooser, True, True, 0)
self.option_widget = file_chooser
file_chooser.entry.connect('changed', self._on_chooser_file_set, option_name)
def on_chooser_file_set(self, widget, option):
def _on_chooser_file_set(self, entry, option):
"""Action triggered on file select dialog 'file-set' signal."""
filename = widget.get_filename()
self.option_changed(widget, option, filename)
if not os.path.isabs(entry.get_text()):
entry.set_text(os.path.expanduser(entry.get_text()))
self.option_changed(entry.get_parent(), option, entry.get_text())
# Directory chooser
def generate_directory_chooser(self, option_name, label_text, value=None):
@ -347,7 +390,7 @@ class ConfigBox(VBox):
action=Gtk.FileChooserAction.SELECT_FOLDER,
default_path=reverse_expanduser(value)
)
directory_chooser.entry.connect('changed', self.on_chooser_dir_set,
directory_chooser.entry.connect('changed', self._on_chooser_dir_set,
option_name)
directory_chooser.set_valign(Gtk.Align.CENTER)
label.set_alignment(0.5, 0.5)
@ -355,10 +398,9 @@ class ConfigBox(VBox):
self.wrapper.pack_start(directory_chooser, True, True, 0)
self.option_widget = directory_chooser
def on_chooser_dir_set(self, entry, option):
def _on_chooser_dir_set(self, entry, option):
"""Action triggered on file select dialog 'file-set' signal."""
filename = entry.get_text()
self.option_changed(entry.get_parent(), option, filename)
self.option_changed(entry.get_parent(), option, entry.get_text())
# Editable grid
def generate_editable_grid(self, option_name, label, value=None):
@ -391,7 +433,7 @@ class ConfigBox(VBox):
vbox.pack_end(button, False, False, 0)
if value:
if type(value) == str:
if isinstance(value, str):
self.files = [value]
else:
self.files = value
@ -461,7 +503,8 @@ class ConfigBox(VBox):
model.remove(treeiter)
self.raw_config[option].pop(row_index)
def on_query_tooltip(self, widget, x, y, keybmode, tooltip, text):
@staticmethod
def on_query_tooltip(widget, x, y, keybmode, tooltip, text):
"""Prepare a custom tooltip with a fixed width"""
label = Label(text)
label.set_use_markup(True)
@ -509,7 +552,8 @@ class ConfigBox(VBox):
option.get('default'))
self.wrapper.show_all()
def set_style_property(self, property_, value, wrapper):
@staticmethod
def set_style_property(property_, value, wrapper):
"""Add custom style."""
style_provider = Gtk.CssProvider()
style_provider.load_from_data(
@ -534,11 +578,8 @@ class GameBox(ConfigBox):
runner = game.runner
if runner:
self.options = runner.game_options
else:
self.options = []
else:
logger.warning("No runner in game supplied to GameBox")
self.options = []
self.generate_widgets('game')
@ -552,8 +593,6 @@ class RunnerBox(ConfigBox):
runner = None
if runner:
self.options = runner.get_runner_options()
else:
self.options = []
if lutris_config.level == 'game':
self.generate_top_info_box(

View file

@ -18,7 +18,7 @@ DIALOG_WIDTH = 780
DIALOG_HEIGHT = 560
class GameDialogCommon(object):
class GameDialogCommon:
no_runner_label = "Select a runner in the Game Info tab"
@staticmethod
@ -82,9 +82,9 @@ class GameDialogCommon(object):
self.slug_entry.connect('activate', self.on_slug_entry_activate)
box.pack_start(self.slug_entry, True, True, 0)
slug_change_button = Gtk.Button("Change")
slug_change_button.connect('clicked', self.on_slug_change_clicked)
box.pack_start(slug_change_button, False, False, 20)
self.slug_change_button = Gtk.Button("Change")
self.slug_change_button.connect('clicked', self.on_slug_change_clicked)
box.pack_start(self.slug_change_button, False, False, 20)
return box
@ -196,6 +196,7 @@ class GameDialogCommon(object):
def on_slug_change_clicked(self, widget):
if self.slug_entry.get_sensitive() is False:
widget.set_label("Apply")
self.slug_entry.set_sensitive(True)
else:
self.change_game_slug()
@ -206,6 +207,7 @@ class GameDialogCommon(object):
def change_game_slug(self):
self.slug = self.slug_entry.get_text()
self.slug_entry.set_sensitive(False)
self.slug_change_button.set_label("Change")
def on_install_runners_clicked(self, _button):
runners_dialog = gui.runnersdialog.RunnersDialog()
@ -336,6 +338,9 @@ class GameDialogCommon(object):
if not name:
ErrorDialog("Please fill in the name")
return False
if self.runner_name in ('steam', 'winesteam') and self.lutris_config.game_config.get('appid') is None:
ErrorDialog("Steam AppId not provided")
return False
return True
def on_save(self, _button, callback=None):

View file

@ -169,7 +169,7 @@ class InstallOrPlayDialog(Gtk.Dialog):
Gtk.Dialog.__init__(self, "%s is already installed" % game_name)
self.connect("delete-event", lambda *x: self.destroy())
self.action = None
self.action = "play"
self.action_confirmed = False
self.set_size_request(320, 120)
@ -193,9 +193,11 @@ class InstallOrPlayDialog(Gtk.Dialog):
self.run()
def on_button_toggled(self, button, action):
logger.debug("Action set to %s", action)
self.action = action
def on_confirm(self, button):
logger.debug("Action %s confirmed", self.action)
self.action_confirmed = True
self.destroy()
@ -230,8 +232,8 @@ class PgaSourceDialog(GtkBuilderDialog):
glade_file = 'dialog-pga-sources.ui'
dialog_object = 'pga_dialog'
def __init__(self):
super().__init__()
def __init__(self, parent=None):
super(PgaSourceDialog, self).__init__(parent=parent)
# GtkBuilder Objects
self.sources_selection = self.builder.get_object("sources_selection")
@ -315,7 +317,7 @@ class ClientLoginDialog(GtkBuilderDialog):
def get_credentials(self):
username = self.username_entry.get_text()
password = self.password_entry.get_text()
return (username, password)
return username, password
def on_username_entry_activate(self, widget):
if all(self.get_credentials()):
@ -343,7 +345,8 @@ class ClientUpdateDialog(GtkBuilderDialog):
glade_file = 'dialog-client-update.ui'
dialog_object = "client_update_dialog"
def on_open_downloads_clicked(self, _widget):
@staticmethod
def on_open_downloads_clicked(_widget):
open_uri("http://lutris.net")
@ -426,7 +429,7 @@ class InstallerSourceDialog(Gtk.Dialog):
class DontShowAgainDialog(Gtk.MessageDialog):
"""Display a message to the user and offer an option not to display this dialog again."""
def __init__(self, setting, message, secondary_message=None, parent=None):
def __init__(self, setting, message, secondary_message=None, parent=None, checkbox_message=None):
super().__init__(type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, parent=parent)
if settings.read_setting(setting) == 'True':
@ -440,7 +443,10 @@ class DontShowAgainDialog(Gtk.MessageDialog):
self.props.secondary_use_markup = True
self.props.secondary_text = secondary_message
dont_show_checkbutton = Gtk.CheckButton("Do not display this message again.")
if not checkbox_message:
checkbox_message = "Do not display this message again."
dont_show_checkbutton = Gtk.CheckButton(checkbox_message)
dont_show_checkbutton.props.halign = Gtk.Align.CENTER
dont_show_checkbutton.show()
@ -451,3 +457,18 @@ class DontShowAgainDialog(Gtk.MessageDialog):
if dont_show_checkbutton.get_active():
settings.write_setting(setting, True)
self.destroy()
class WineNotInstalledWarning(DontShowAgainDialog):
"""Display a warning if Wine is not detected on the system"""
def __init__(self, parent=None):
super().__init__(
'hide-wine-systemwide-install-warning',
"Wine is not installed on your system.",
secondary_message="Having Wine installed on your system guarantees that "
"Wine builds from Lutris will have all required dependencies.\n\nPlease "
"follow the instructions given in the <a "
"href='https://github.com/lutris/lutris/wiki/Wine'>Lutris Wiki</a> to "
"install Wine.",
parent=parent
)

257
lutris/gui/flowbox.py Normal file
View file

@ -0,0 +1,257 @@
from lutris import pga
from lutris.util.log import logger
from gi.repository import Gtk, Gdk, GObject, GLib
from lutris.gui.widgets.utils import get_pixbuf_for_game
from lutris.game import Game
try:
FlowBox = Gtk.FlowBox
FLOWBOX_SUPPORTED = True
except AttributeError:
FlowBox = object
FLOWBOX_SUPPORTED = False
class GameItem(Gtk.VBox):
def __init__(self, game, parent, icon_type='banner'):
super(GameItem, self).__init__()
self.icon_type = icon_type
self.parent = parent
self.game = Game(game['id'])
self.id = game['id']
self.name = game['name']
self.slug = game['slug']
self.runner = game['runner']
self.platform = game['platform']
self.installed = game['installed']
image = self.get_image()
self.pack_start(image, False, False, 0)
label = self.get_label()
self.pack_start(label, False, False, 0)
self.connect('button-press-event', self.popup_contextual_menu)
self.show_all()
def get_image(self):
# For some reason, button-press-events are not registered by the image
# so it needs to be wrapped in an EventBox
eventbox = Gtk.EventBox()
self.image = Gtk.Image()
self.set_image_pixbuf()
eventbox.add(self.image)
return eventbox
def set_image_pixbuf(self):
pixbuf = get_pixbuf_for_game(self.slug,
self.icon_type,
self.installed)
self.image.set_from_pixbuf(pixbuf)
def get_label(self):
self.label = Gtk.Label(self.name)
self.label.set_size_request(184, 40)
if self.icon_type == 'banner':
self.label.set_max_width_chars(20)
else:
self.label.set_max_width_chars(15)
self.label.set_property('wrap', True)
self.label.set_justify(Gtk.Justification.CENTER)
self.label.set_halign(Gtk.Align.CENTER)
eventbox = Gtk.EventBox()
eventbox.add(self.label)
return eventbox
def set_label_text(self, text):
self.label.set_text(text)
def popup_contextual_menu(self, widget, event):
if event.button != 3:
return
self.parent.popup_contextual_menu(event, self)
class GameFlowBox(FlowBox):
__gsignals__ = {
"game-selected": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-activated": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-installed": (GObject.SIGNAL_RUN_FIRST, None, (int,)),
"remove-game": (GObject.SIGNAL_RUN_FIRST, None, ())
}
def __init__(self, game_list, icon_type='banner', filter_installed=False):
super(GameFlowBox, self).__init__()
self.set_valign(Gtk.Align.START)
self.connect('child-activated', self.on_child_activated)
self.connect('selected-children-changed', self.on_selection_changed)
self.connect('key-press-event', self.handle_key_press)
self.set_filter_func(self.filter_func)
self.set_sort_func(self.sort_func)
self.set_activate_on_single_click(False)
self.set_max_children_per_line(1)
self.set_max_children_per_line(20)
self.contextual_menu = None
self.icon_type = icon_type
self.filter_text = ''
self.filter_runner = None
self.filter_platform = None
self.filter_installed = filter_installed
self.game_list = game_list
self.populate_games(self.game_list)
@property
def selected_game(self):
"""Because of shitty naming conventions in previous Game views, this
returns an id and not a game.
"""
children = self.get_selected_children()
if not children:
return None
game_item = children[0].get_children()[0]
return game_item.game.id
def populate_games(self, games):
loader = self._fill_store_generator(games)
GLib.idle_add(loader.__next__)
def _fill_store_generator(self, games, step=50):
"""Generator to fill the model in steps."""
n = 0
for game in games:
item = GameItem(game, self, icon_type=self.icon_type)
game['item'] = item # keep a reference of created widgets
self.add(item)
n += 1
if (n % step) == 0:
yield True
yield False
def filter_func(self, child):
game = child.get_children()[0]
if self.filter_installed:
if not game.installed:
return False
if self.filter_text:
if self.filter_text.lower() not in game.name.lower():
return False
if self.filter_runner:
if self.filter_runner != game.runner:
return False
if self.filter_platform:
if not game.game.runner:
return False
if self.filter_platform != game.game.platform:
return False
return True
@staticmethod
def sort_func(child1, child2):
game1 = child1.get_children()[0]
game2 = child2.get_children()[0]
if game1.name.lower() > game2.name.lower():
return 1
elif game1.name.lower() < game2.name.lower():
return -1
else:
return 0
def on_child_activated(self, widget, child):
self.emit('game-activated')
def on_selection_changed(self, widget):
self.emit('game-selected')
def get_child(self, game_item):
for child in self.get_children():
widget = child.get_children()[0]
if widget == game_item:
return child
def has_game_id(self, game_id):
for game in self.game_list:
if game['id'] == game_id:
return True
return False
def add_game_by_id(self, game_id):
if not game_id:
return
game = pga.get_game_by_field(game_id, 'id')
if not game or 'slug' not in game:
raise ValueError('Can\'t find game {} ({})'.format(
game_id, game
))
self.add_game(game)
def add_game(self, game):
item = GameItem(game, self)
game['item'] = item
self.add(item)
self.game_list.append(game)
def remove_game(self, game_id):
for index, game in enumerate(self.game_list):
if game['id'] == game_id:
child = self.get_child(game['item'])
self.remove(child)
child.destroy()
self.game_list.pop(index)
return
def set_installed(self, game):
for index, _game in enumerate(self.game_list):
if game.id == _game['id']:
_game['runner'] = game.runner_name
_game['item'].game.is_installed = True
_game['installed'] = True
self.update_image(_game['id'], True)
def set_uninstalled(self, game):
for index, _game in enumerate(self.game_list):
if game.id == _game['id']:
_game['runner'] = ''
_game['installed'] = False
_game['item'].game.is_installed = False
self.update_image(_game['id'], False)
def update_row(self, game):
for index, _game in enumerate(self.game_list):
if game['id'] == _game['id']:
self.update_image(game['id'], _game['installed'])
def update_image(self, game_id, is_installed):
for index, game in enumerate(self.game_list):
if game['id'] == game_id:
item = game.get('item')
if not item:
logger.error("Couldn't get item for game %s", game)
return
item.installed = is_installed
item.set_image_pixbuf()
def set_selected_game(self, game_id):
for game in self.game_list:
if game['id'] == game_id:
self.select_child(self.get_child(game['item']))
def popup_contextual_menu(self, event, widget):
self.select_child(self.get_child(widget))
self.contextual_menu.popup(event, game=widget.game)
def handle_key_press(self, widget, event):
if not self.selected_game:
return
key = event.keyval
if key == Gdk.KEY_Delete:
self.emit("remove-game")

View file

@ -30,8 +30,10 @@ from lutris.util.strings import gtk_safe
COL_LASTPLAYED_TEXT,
COL_INSTALLED,
COL_INSTALLED_AT,
COL_INSTALLED_AT_TEXT
) = list(range(13))
COL_INSTALLED_AT_TEXT,
COL_PLAYTIME,
COL_PLAYTIME_TEXT
) = list(range(15))
COLUMN_NAMES = {
COL_NAME: 'name',
@ -39,7 +41,8 @@ COLUMN_NAMES = {
COL_RUNNER_HUMAN_NAME: 'runner',
COL_PLATFORM: 'platform',
COL_LASTPLAYED_TEXT: 'lastplayed',
COL_INSTALLED_AT_TEXT: 'installed_at'
COL_INSTALLED_AT_TEXT: 'installed_at',
COL_PLAYTIME_TEXT: 'playtime'
}
sortings = {
'name': COL_NAME,
@ -57,20 +60,23 @@ class GameStore(GObject.Object):
"sorting-changed": (GObject.SIGNAL_RUN_FIRST, None, (str, bool,))
}
def __init__(self, games, icon_type, filter_installed, sort_key, sort_ascending):
super().__init__()
def __init__(self, games, icon_type, filter_installed, sort_key, sort_ascending, show_installed_first=False):
super(GameStore, self).__init__()
self.games = games
self.icon_type = icon_type
self.filter_installed = filter_installed
self.show_installed_first = show_installed_first
self.filter_text = None
self.filter_runner = None
self.filter_platform = None
self.runner_names = self.populate_runner_names()
self.store = Gtk.ListStore(int, str, str, Pixbuf, str, str, str, str, int, str, bool, int, str)
self.store.set_sort_column_id(COL_NAME, Gtk.SortType.ASCENDING)
self.prevent_sort_update = False # prevent recursion with signals
self.modelfilter = None
self.runner_names = {}
self.store = Gtk.ListStore(int, str, str, Pixbuf, str, str, str, str, int, str, bool, int, str, str, str)
if show_installed_first:
self.store.set_sort_column_id(COL_INSTALLED, Gtk.SortType.DESCENDING)
else:
self.store.set_sort_column_id(COL_NAME, Gtk.SortType.ASCENDING)
self.modelfilter = self.store.filter_new()
self.modelfilter.set_visible_func(self.filter_view)
self.modelsort = Gtk.TreeModelSort.sort_new_with_model(self.modelfilter)
@ -79,6 +85,12 @@ class GameStore(GObject.Object):
if games:
self.fill_store(games)
def __str__(self):
return (
"GameStore: <filter_installed: {filter_installed}, "
"filter_text: {filter_text}>".format(**self.__dict__)
)
def get_ids(self):
return [row[COL_ID] for row in self.store]
@ -89,10 +101,18 @@ class GameStore(GObject.Object):
names[runner] = runner_inst.human_name
return names
def fill_store(self, games):
"""Fill the model"""
def _fill_store_generator(self, games, batch=100):
"""Generator to fill the model in batches."""
loop = 0
for game in games:
self.add_game(game)
# Yield to GTK main loop once in a while
loop += 1
if (loop % batch) == 0:
# Returning True to GLib.idle_add makes it run the callback
# again. (Yeah, the GTK doc isn't clear about this feature :)
yield True
yield False
def filter_view(self, model, _iter, filter_data=None):
"""Filter the game list."""
@ -124,6 +144,14 @@ class GameStore(GObject.Object):
Gtk.SortType.ASCENDING if ascending else Gtk.SortType.DESCENDING
)
# def sort_view(self, show_installed_first=False):
# self.show_installed_first = show_installed_first
# self.store.set_sort_column_id(COL_NAME, Gtk.SortType.ASCENDING)
# self.modelfilter.get_model().set_sort_column_id(COL_NAME, Gtk.SortType.ASCENDING)
# if show_installed_first:
# self.store.set_sort_column_id(COL_INSTALLED, Gtk.SortType.DESCENDING)
# self.modelfilter.get_model().set_sort_column_id(COL_INSTALLED, Gtk.SortType.DESCENDING)
def on_sort_column_changed(self, model):
if self.prevent_sort_update:
return
@ -178,7 +206,11 @@ class GameStore(GObject.Object):
if game['installed_at']:
installed_at_text = time.strftime("%c", time.localtime(game['installed_at']))
pixbuf = get_pixbuf_for_game(game['slug'], self.icon_type, game['installed'])
pixbuf = get_pixbuf_for_game(game['slug'], self.icon_type,
game['installed'])
playtime_text = ''
if game['playtime']:
playtime_text = game['playtime']
self.store.append((
game['id'],
gtk_safe(game['slug']),
@ -192,8 +224,11 @@ class GameStore(GObject.Object):
gtk_safe(lastplayed_text),
game['installed'],
game['installed_at'],
gtk_safe(installed_at_text)
gtk_safe(installed_at_text),
game['playtime'],
playtime_text
))
self.sort_view(self.show_installed_first)
def set_icon_type(self, icon_type):
if icon_type != self.icon_type:
@ -205,7 +240,7 @@ class GameStore(GObject.Object):
self.emit('icons-changed', icon_type) # Obsolete, only for GridView
class GameView(object):
class GameView:
__gsignals__ = {
"game-selected": (GObject.SIGNAL_RUN_FIRST, None, ()),
"game-activated": (GObject.SIGNAL_RUN_FIRST, None, ()),
@ -284,6 +319,7 @@ class GameView(object):
row = self.get_row_by_id(game['id'])
if row:
row[COL_YEAR] = str(game['year'])
row[COL_PLAYTIME_TEXT] = game['playtime']
self.update_image(game['id'], row[COL_INSTALLED])
def update_image(self, game_id, is_installed=False):
@ -297,7 +333,7 @@ class GameView(object):
is_installed)
row[COL_ICON] = game_pixbuf
row[COL_INSTALLED] = is_installed
if type(self) is GameGridView:
if isinstance(self, GameGridView):
GLib.idle_add(self.queue_draw)
def popup_contextual_menu(self, view, event):
@ -307,9 +343,9 @@ class GameView(object):
try:
view.current_path = view.get_path_at_pos(event.x, event.y)
if view.current_path:
if type(view) is GameGridView:
if isinstance(view, GameGridView):
view.select_path(view.current_path)
elif type(view) is GameListView:
elif isinstance(view, GameListView):
view.set_cursor(view.current_path[0])
except ValueError:
(_, path) = view.get_selection().get_selected()
@ -353,8 +389,11 @@ class GameListView(Gtk.TreeView, GameView):
self.set_column(default_text_cell, "Year", COL_YEAR, 60)
self.set_column(default_text_cell, "Runner", COL_RUNNER_HUMAN_NAME, 120)
self.set_column(default_text_cell, "Platform", COL_PLATFORM, 120)
self.set_column(default_text_cell, "Last played", COL_LASTPLAYED_TEXT, 120, sort_id=COL_LASTPLAYED)
self.set_column(default_text_cell, "Installed at", COL_INSTALLED_AT_TEXT, 120, sort_id=COL_INSTALLED_AT)
self.set_column(default_text_cell, "Last played", COL_LASTPLAYED_TEXT, 120)
self.set_sort_with_column(COL_LASTPLAYED_TEXT, COL_LASTPLAYED)
self.set_column(default_text_cell, "Installed at", COL_INSTALLED_AT_TEXT, 120)
self.set_sort_with_column(COL_INSTALLED_AT_TEXT, COL_INSTALLED_AT)
self.set_column(default_text_cell, "Play Time", COL_PLAYTIME_TEXT, 100)
self.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
@ -362,7 +401,8 @@ class GameListView(Gtk.TreeView, GameView):
self.connect('row-activated', self.on_row_activated)
self.get_selection().connect('changed', self.on_cursor_changed)
def set_text_cell(self):
@staticmethod
def set_text_cell():
text_cell = Gtk.CellRendererText()
text_cell.set_padding(10, 0)
text_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
@ -404,10 +444,10 @@ class GameListView(Gtk.TreeView, GameView):
"""Return the currently selected game's id."""
selection = self.get_selection()
if not selection:
return
return None
model, select_iter = selection.get_selected()
if not select_iter:
return
return None
return model.get_value(select_iter, COL_ID)
def set_selected_game(self, game_id):
@ -423,7 +463,8 @@ class GameListView(Gtk.TreeView, GameView):
self.selected_game = self.get_selected_game()
self.emit("game-activated")
def on_column_width_changed(self, col, *args):
@staticmethod
def on_column_width_changed(col, *args):
col_name = col.get_title()
if col_name:
settings.write_setting(col_name.replace(' ', '') + '_column_width',
@ -556,7 +597,7 @@ class ContextualMenu(Gtk.Menu):
'browse': not is_installed or runner_slug == 'browser',
}
for menuitem in self.get_children():
if type(menuitem) is not Gtk.ImageMenuItem:
if not isinstance(menuitem, Gtk.ImageMenuItem):
continue
action = menuitem.action_id
visible = not hiding_condition.get(action)

View file

@ -37,7 +37,7 @@ class GtkTemplateWarning(UserWarning):
def _connect_func(builder, obj, signal_name, handler_name,
connect_object, flags, cls):
'''Handles GtkBuilder signal connect events'''
"""Handles GtkBuilder signal connect events"""
if connect_object is None:
extra = ()
@ -66,7 +66,7 @@ def _connect_func(builder, obj, signal_name, handler_name,
def _register_template(cls, template_bytes):
'''Registers the template for the widget and hooks init_template'''
"""Registers the template for the widget and hooks init_template"""
# This implementation won't work if there are nested templates, but
# we can't do that anyways due to PyGObject limitations so it's ok
@ -106,7 +106,7 @@ def _register_template(cls, template_bytes):
def _init_template(self, cls, base_init_template):
'''This would be better as an override for Gtk.Widget'''
"""This would be better as an override for Gtk.Widget"""
# TODO: could disallow using a metaclass.. but this is good enough
# .. if you disagree, feel free to fix it and issue a PR :)
@ -141,29 +141,29 @@ def _init_template(self, cls, base_init_template):
# TODO: Make it easier for IDE to introspect this
class _Child(object):
'''
class _Child:
"""
Assign this to an attribute in your class definition and it will
be replaced with a widget defined in the UI file when init_template
is called
'''
"""
__slots__ = []
@staticmethod
def widgets(count):
'''
"""
Allows declaring multiple widgets with less typing::
button \
label1 \
label2 = GtkTemplate.Child.widgets(3)
'''
"""
return [_Child() for _ in range(count)]
class _GtkTemplate(object):
'''
class _GtkTemplate:
"""
Use this class decorator to signify that a class is a composite
widget which will receive widgets and connect to signals as
defined in a UI template. You must call init_template to
@ -205,16 +205,16 @@ class _GtkTemplate(object):
.. note:: Due to limitations in PyGObject, you may not inherit from
python objects that use the GtkTemplate decorator.
'''
"""
__ui_path__ = None
@staticmethod
def Callback(f):
'''
"""
Decorator that designates a method to be attached to a signal from
the template
'''
"""
f._gtk_callback = True
return f
@ -222,7 +222,7 @@ class _GtkTemplate(object):
@staticmethod
def set_ui_path(*path):
'''
"""
If using file paths instead of resources, call this *before*
loading anything that uses GtkTemplate, or it will fail to load
your template file
@ -232,7 +232,7 @@ class _GtkTemplate(object):
TODO: Alternatively, could wait until first class instantiation
before registering templates? Would need a metaclass...
'''
"""
_GtkTemplate.__ui_path__ = abspath(join(*path))
def __init__(self, ui):

View file

@ -226,12 +226,12 @@ class InstallerWindow(Gtk.ApplicationWindow):
return label
self.description_label = _create_label(
"<i><b>{}</b></i>".format(self.scripts[0]['description'])
"<b>{}</b>".format(self.scripts[0]['description'])
)
self.installer_choice_box.pack_start(self.description_label, True, True, 10)
self.notes_label = _create_label(
"<i>{}</i>".format(self.scripts[0]['notes'])
"{}".format(self.scripts[0]['notes'])
)
notes_scrolled_area = Gtk.ScrolledWindow()
try:
@ -255,10 +255,10 @@ class InstallerWindow(Gtk.ApplicationWindow):
def on_installer_toggled(self, btn, script_index):
description = self.scripts[script_index]['description']
self.description_label.set_markup(
"<i><b>{}</b></i>".format(self._escape_text(description))
"<b>{}</b>".format(self._escape_text(description))
)
self.notes_label.set_markup(
"<i>{}</i>".format(self._escape_text(self.scripts[script_index]['notes']))
"{}".format(self._escape_text(self.scripts[script_index]['notes']))
)
if btn.get_active():
self.installer_choice = script_index
@ -302,7 +302,7 @@ class InstallerWindow(Gtk.ApplicationWindow):
self.install_button.grab_focus()
self.install_button.show()
def on_target_changed(self, text_entry):
def on_target_changed(self, text_entry, _):
"""Set the installation target for the game."""
path = text_entry.get_text()
self.interpreter.target_path = os.path.expanduser(path)
@ -313,6 +313,10 @@ class InstallerWindow(Gtk.ApplicationWindow):
if not self.location_entry:
return
path = self.location_entry.get_text()
# replace ~ with full path so os.path.exists and os.listdir work correctly
path = os.path.expanduser(path)
if os.path.exists(path) and os.listdir(path):
self.non_empty_label.show()
else:
@ -335,11 +339,30 @@ class InstallerWindow(Gtk.ApplicationWindow):
path = self.selected_directory
else:
path = os.path.expanduser('~')
self.set_path_chooser(None, 'file', default_path=path)
self.set_path_chooser(self.continue_guard, 'file', default_path=path)
def continue_guard(self, _, action):
loc = self.location_entry.get_text()
loc = os.path.expanduser(loc)
if ((action == Gtk.FileChooserAction.OPEN and os.path.isfile(loc))
or (action == Gtk.FileChooserAction.SELECT_FOLDER and os.path.isdir(loc))):
self.continue_button.set_sensitive(True)
self.continue_button.connect('clicked', self.on_file_selected)
self.continue_button.grab_focus()
else:
self.continue_button.set_sensitive(False)
def set_path_chooser(self, callback_on_changed, action=None,
default_path=None):
"""Display a file/folder chooser."""
self.install_button.set_visible(False)
self.continue_button.show()
self.continue_button.set_sensitive(False)
if action == 'file':
title = 'Select file'
action = Gtk.FileChooserAction.OPEN
@ -352,12 +375,9 @@ class InstallerWindow(Gtk.ApplicationWindow):
self.location_entry = FileChooserEntry(title, action, default_path)
self.location_entry.show_all()
if callback_on_changed:
self.location_entry.entry.connect('changed', callback_on_changed)
else:
self.install_button.set_visible(False)
self.continue_button.connect('clicked', self.on_file_selected)
self.continue_button.grab_focus()
self.continue_button.show()
self.location_entry.entry.connect(
'changed', callback_on_changed, action)
self.widget_box.pack_start(self.location_entry, False, False, 0)
def on_file_selected(self, widget):

View file

@ -10,6 +10,7 @@ class LogTextView(Gtk.TextView):
self.set_editable(False)
self.set_monospace(True)
self.set_left_margin(10)
self.scroll_max = 0
self.set_wrap_mode(Gtk.WrapMode.CHAR)
self.get_style_context().add_class('lutris-logview')
if autoscroll:
@ -17,7 +18,11 @@ class LogTextView(Gtk.TextView):
def autoscroll(self, *args):
adj = self.get_vadjustment()
adj.set_value(adj.get_upper() - adj.get_page_size())
if adj.get_value() == self.scroll_max or self.scroll_max == 0:
adj.set_value(adj.get_upper() - adj.get_page_size())
self.scroll_max = adj.get_value()
else:
self.scroll_max = adj.get_upper() - adj.get_page_size()
class LogWindow(Dialog):

68
lutris/gui/lutristray.py Normal file
View file

@ -0,0 +1,68 @@
"""Module for the tray icon"""
from gi.repository import Gtk
from lutris import runners
from lutris import pga
from lutris.gui.widgets.utils import get_runner_icon, get_pixbuf_for_game
class LutrisTray(Gtk.StatusIcon):
"""Lutris tray icon"""
def __init__(self, application, **_kwargs):
super().__init__()
self.set_tooltip_text('Lutris')
self.set_visible(True)
self.application = application
self.set_from_icon_name('lutris')
self.menu = None
self.load_menu()
self.connect('activate', self.on_activate)
self.connect('popup-menu', self.on_menu_popup)
def load_menu(self):
"""Instanciates the menu attached to the tray icon"""
self.menu = Gtk.Menu()
self.add_games()
self.menu.append(Gtk.SeparatorMenuItem())
quit_menu = Gtk.MenuItem()
quit_menu.set_label("Quit")
quit_menu.connect("activate", self.on_quit_application)
self.menu.append(quit_menu)
self.menu.show_all()
def on_activate(self, _status_icon, _event=None):
"""Callback to show or hide the window"""
self.application.window.present()
def on_menu_popup(self, _status_icon, button, time):
"""Callback to show the contextual menu"""
self.menu.popup(None, None, None, None, button, time)
def on_quit_application(self, _widget):
"""Callback to quit the program"""
self.application.do_shutdown()
def _make_menu_item_for_game(self, game):
menu_item = Gtk.ImageMenuItem()
menu_item.set_label(game["name"])
game_icon = get_pixbuf_for_game(game["slug"], "icon_small")
menu_item.set_image(Gtk.Image.new_from_pixbuf(game_icon))
menu_item.connect("activate", self.on_game_selected, game["id"])
return menu_item
def add_games(self):
"""Adds installed games in order of last use"""
number_of_games_in_menu = 10
installed_games = pga.get_games(filter_installed=True)
installed_games.sort(
key=lambda game: max(game["lastplayed"] or 0, game["installed_at"] or 0),
reverse=True)
for game in installed_games[:number_of_games_in_menu]:
self.menu.append(self._make_menu_item_for_game(game))
def on_game_selected(self, _widget, *data):
self.application.window.on_game_run(game_id=data[0])

View file

@ -1,5 +1,5 @@
"""Main window for the Lutris interface."""
# pylint: disable=E0611
# pylint: disable=no-member
import os
import math
import time
@ -16,11 +16,14 @@ from lutris.runtime import RuntimeUpdater
from lutris.util import resources
from lutris.util.log import logger
from lutris.util.jobs import AsyncCall
from lutris.util.system import open_uri
from lutris.util.system import open_uri, path_exists
from lutris.util import http
from lutris.util import datapath
from lutris.util.steam import SteamWatcher
from lutris.util.dxvk import init_dxvk_versions
from lutris.thread import LutrisThread
from lutris.services import get_services_synced_at_startup, steam, xdg
@ -83,6 +86,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.last_selected_game = None
self.selected_runner = None
self.selected_platform = None
self.icon_type = None
# Load settings
width = int(settings.read_setting('width') or 800)
@ -92,23 +96,32 @@ class LutrisWindow(Gtk.ApplicationWindow):
view_type = self.get_view_type()
self.load_icon_type_from_settings(view_type)
self.filter_installed = \
settings.read_setting('filter_installed') == 'true'
self.filter_installed = settings.read_setting('filter_installed') == 'true'
self.show_installed_first = \
settings.read_setting('show_installed_first') == 'true'
self.sidebar_visible = \
settings.read_setting('sidebar_visible') in ['true', None]
self.view_sorting = \
settings.read_setting('view_sorting') or 'name'
self.view_sorting_ascending = \
settings.read_setting('view_sorting_ascending') != 'false'
self.use_dark_theme = settings.read_setting('dark_theme') == 'true'
self.use_dark_theme = settings.read_setting('dark_theme', default='false').lower() == 'true'
self.show_tray_icon = settings.read_setting('show_tray_icon', default='false').lower() == 'true'
# Sync local lutris library with current Steam games and desktop games
for service in get_services_synced_at_startup():
service.sync_with_lutris()
# Window initialization
self.game_list = pga.get_games()
self.game_store = GameStore([], self.icon_type, self.filter_installed, self.view_sorting, self.view_sorting_ascending)
self.game_list = pga.get_games(show_installed_first=self.show_installed_first)
self.game_store = GameStore(
[],
self.icon_type,
self.filter_installed,
self.view_sorting,
self.view_sorting_ascending
self.show_installed_first
)
self.view = self.get_view(view_type)
self.game_store.connect('sorting-changed', self.on_game_store_sorting_changed)
super().__init__(default_width=width,
@ -141,6 +154,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
('install', "Install", self.on_install_clicked),
('add', "Add manually", self.on_add_manually),
('configure', "Configure", self.on_edit_game_configuration),
('execute-script', "Execute script", self.on_execute_script_clicked),
('browse', "Browse files", self.on_browse_files),
('desktop-shortcut', "Create desktop shortcut",
self.create_desktop_shortcut),
@ -177,6 +191,10 @@ class LutrisWindow(Gtk.ApplicationWindow):
steamapps_paths = steam.get_steamapps_paths(flat=True)
self.steam_watcher = SteamWatcher(steamapps_paths, self.on_steam_game_changed)
self.gui_needs_update = True
self.config_menu_first_access = True
def _init_actions(self):
Action = namedtuple('Action', ('callback', 'type', 'enabled', 'default', 'accel'))
Action.__new__.__defaults__ = (None, None, True, None, None)
@ -208,7 +226,10 @@ class LutrisWindow(Gtk.ApplicationWindow):
'show-installed-only': Action(self.on_show_installed_state_change, type='b',
default=self.filter_installed,
accel='<Primary>h'),
'toggle-viewtype': Action(self.on_toggle_viewtype),
'show-installed-first': Action(self.on_show_installed_first_state_change, type='b',
default=self.show_installed_first),
'view-type': Action(self.on_viewtype_state_change, type='s',
default=self.current_view_type),
'icon-type': Action(self.on_icontype_state_change, type='s',
default=self.icon_type),
'view-sorting': Action(self.on_view_sorting_state_change, type='s',
@ -217,6 +238,8 @@ class LutrisWindow(Gtk.ApplicationWindow):
default=self.view_sorting_ascending),
'use-dark-theme': Action(self.on_dark_theme_state_change, type='b',
default=self.use_dark_theme),
'show-tray-icon': Action(self.on_tray_icon_toggle, type='b',
default=self.show_tray_icon),
'show-side-bar': Action(self.on_sidebar_state_change, type='b',
default=self.sidebar_visible, accel='F9'),
}
@ -245,9 +268,11 @@ class LutrisWindow(Gtk.ApplicationWindow):
@property
def current_view_type(self):
"""Returns which kind of view is currently presented (grid or list)"""
return 'grid' if isinstance(self.view, GameGridView) else 'list'
def on_steam_game_changed(self, operation, path):
"""Action taken when a Steam AppManifest file is updated"""
appmanifest = steam.AppManifest(path)
if self.running_game and 'steam' in self.running_game.runner_name:
self.running_game.notify_steam_game_changed(appmanifest)
@ -277,31 +302,34 @@ class LutrisWindow(Gtk.ApplicationWindow):
'name': appmanifest.name,
'slug': appmanifest.slug,
}
game_id = steam.mark_as_installed(appmanifest.steamid,
runner_name,
game_info)
game_ids = [game['id'] for game in self.game_list]
if game_id not in game_ids:
self.add_game_to_view(game_id)
else:
self.view.set_installed(Game(game_id))
if steam in get_services_synced_at_startup():
game_id = steam.mark_as_installed(appmanifest.steamid,
runner_name,
game_info)
game_ids = [game['id'] for game in self.game_list]
if game_id not in game_ids:
self.add_game_to_view(game_id)
else:
self.view.set_installed(Game(game_id))
@staticmethod
def set_dark_theme(is_dark):
"""Enables or disbales dark theme"""
gtksettings = Gtk.Settings.get_default()
gtksettings.set_property("gtk-application-prefer-dark-theme", is_dark)
def get_view(self, view_type):
"""Return the appropriate widget for the current view"""
if view_type == 'grid':
return GameGridView(self.game_store)
else:
return GameListView(self.game_store)
return GameListView(self.game_store)
def connect_signals(self):
"""Connect signals from the view with the main window.
This must be called each time the view is rebuilt.
"""
self.connect('delete-event', lambda *x: self.hide_on_delete())
self.view.connect('game-installed', self.on_game_installed)
self.view.connect("game-activated", self.on_game_run)
self.view.connect("game-selected", self.game_selection_changed)
@ -329,6 +357,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
@staticmethod
def get_view_type():
"""Return the type of view saved by the user"""
view_type = settings.read_setting('view_type')
if view_type in ['grid', 'list']:
return view_type
@ -365,13 +394,19 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.icon_type = default
return self.icon_type
def switch_splash_screen(self):
if len(self.game_list) == 0:
self.main_box.hide()
self.splash_box.show()
else:
def switch_splash_screen(self, force=None):
"""Toggle the state of the splash screen based on the library contents"""
if not self.splash_box.get_visible() and self.game_list:
return
if self.game_list or force is True:
self.splash_box.hide()
self.main_box.show()
self.sidebar_paned.show()
self.games_scrollwindow.show()
else:
logger.debug('Showing splash screen')
self.splash_box.show()
self.sidebar_paned.hide()
self.games_scrollwindow.hide()
def switch_view(self, view_type):
"""Switch between grid view and list view."""
@ -383,7 +418,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.view.contextual_menu = self.menu
self.connect_signals()
scrollwindow_children = self.games_scrollwindow.get_children()
if len(scrollwindow_children):
if scrollwindow_children:
child = scrollwindow_children[0]
child.destroy()
self.games_scrollwindow.add(self.view)
@ -404,21 +439,24 @@ class LutrisWindow(Gtk.ApplicationWindow):
def sync_library(self):
"""Synchronize games with local stuff and server."""
def update_gui(result, error):
if error:
logger.error("Failed to synchrone library: %s", error)
return
if result:
added_ids, updated_ids = result
# sqlite limits the number of query parameters to 999, to
# bypass that limitation, divide the query in chunks
page_size = 999
size = 999
added_games = chain.from_iterable([
pga.get_games_where(
id__in=list(added_ids)[p * page_size:p * page_size + page_size]
id__in=list(added_ids)[page * size:page * size + size]
)
for p in range(math.ceil(len(added_ids) / page_size))
for page in range(math.ceil(len(added_ids) / size))
])
self.game_list += added_games
self.view.populate_games(added_games)
self.switch_splash_screen()
self.view.populate_games(added_games)
GLib.idle_add(self.update_existing_games, added_ids, updated_ids, True)
else:
logger.error("No results returned when syncing the library")
@ -432,49 +470,79 @@ class LutrisWindow(Gtk.ApplicationWindow):
AsyncCall(sync_from_remote, update_gui)
def open_sync_dialog(self):
"""Opens the service sync dialog"""
sync_dialog = SyncServiceDialog(parent=self)
sync_dialog.run()
def update_existing_games(self, added, updated, first_run=False):
"""???"""
for game_id in updated.difference(added):
# XXX this migth not work if the game has no 'item' set
# XXX this might not work if the game has no 'item' set
logger.debug("Updating row for ID %s", game_id)
self.view.update_row(pga.get_game_by_field(game_id, 'id'))
if first_run:
logger.info("Setting up view for first run")
for game_id in added:
logger.debug("Adding %s", game_id)
self.add_game_to_view(game_id)
icons_sync = AsyncCall(self.sync_icons, callback=None)
self.threads_stoppers.append(icons_sync.stop_request.set)
def update_runtime(self):
# self.runtime_updater.update(self.set_status) # TODO: Show this info?
"""Check that the runtime is up to date"""
self.runtime_updater.update(self.set_status)
self.threads_stoppers += self.runtime_updater.cancellables
def sync_icons(self):
"""Download missing icons"""
game_slugs = [game['slug'] for game in self.game_list]
if not game_slugs:
return
logger.debug("Syncing %d icons", len(game_slugs))
try:
resources.fetch_icons([game['slug'] for game in self.game_list],
callback=self.on_image_downloaded)
GLib.idle_add(
resources.fetch_icons, game_slugs,
self.on_image_downloaded
)
except TypeError as ex:
logger.exception("Invalid game list:\n%s\nException: %s", self.game_list, ex)
def set_status(self, text):
"""Sets the statusbar text"""
# update row at game exit
# XXX This is NOT a proper way to do it!!!!!!
# FIXME This is ugly and will cause issues!@@!
if text == "Game has quit" and self.gui_needs_update:
self.view.update_row(pga.get_game_by_field(self.running_game.id, 'id'))
for child_widget in self.status_box.get_children():
child_widget.destroy()
label = Gtk.Label(text)
label.show()
self.status_box.add(label)
def refresh_status(self):
"""Refresh status bar."""
if self.running_game:
name = self.running_game.name
if self.running_game.state == self.running_game.STATE_IDLE:
pass
self.set_status("Preparing to launch %s" % name)
self.gui_needs_update = True
elif self.running_game.state == self.running_game.STATE_STOPPED:
self.set_status("Game has quit")
self.gui_needs_update = False
self.actions['stop-game'].props.enabled = False
self.infobar_revealer.set_reveal_child(False)
elif self.running_game.state == self.running_game.STATE_RUNNING:
self.actions['stop-game'].props.enabled = True
self.infobar_label.props.label = '{} running'.format(name)
self.infobar_revealer.set_reveal_child(True)
self.gui_needs_update = True
return True
# ---------
# Callbacks
# ---------
def on_dark_theme_state_change(self, action, value):
"""Callback for theme switching action"""
action.set_state(value)
self.use_dark_theme = value.get_boolean()
setting_value = 'true' if self.use_dark_theme else 'false'
@ -482,30 +550,35 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.set_dark_theme(self.use_dark_theme)
@GtkTemplate.Callback
def on_connect(self, *args):
def on_connect(self, *_args):
"""Callback when a user connects to his account."""
login_dialog = dialogs.ClientLoginDialog(self)
login_dialog.connect('connected', self.on_connect_success)
return True
def on_connect_success(self, dialog, credentials):
def on_connect_success(self, _dialog, credentials):
"""Callback for user connect success"""
if isinstance(credentials, str):
username = credentials
else:
username = credentials["username"]
self.toggle_connection(True, username)
self.sync_library()
self.connect_link.hide()
self.connect_link.set_sensitive(False)
self.actions['synchronize'].props.enabled = True
self.actions['register-account'].props.enabled = False
@GtkTemplate.Callback
def on_disconnect(self, *args):
def on_disconnect(self, *_args):
"""Callback from user disconnect"""
api.disconnect()
self.toggle_connection(False)
self.connect_link.show()
self.actions['synchronize'].props.enabled = False
def toggle_connection(self, is_connected, username=None):
"""Sets or unset connected state for the current user"""
self.props.application.set_connect_state(is_connected)
self.connect_button.props.visible = not is_connected
self.register_button.props.visible = not is_connected
self.disconnect_button.props.visible = is_connected
@ -515,7 +588,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
logger.info('Connected to lutris.net as %s', username)
@GtkTemplate.Callback
def on_resize(self, widget, *args):
def on_resize(self, widget, *_args):
"""Size-allocate signal.
Updates stored window size and maximized state.
@ -527,7 +600,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.window_size = widget.get_size()
@GtkTemplate.Callback
def on_destroy(self, *args):
def on_destroy(self, *_args):
"""Signal for window close."""
# Stop cancellable running threads
for stopper in self.threads_stoppers:
@ -546,7 +619,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
settings.write_setting('maximized', self.maximized)
@GtkTemplate.Callback
def on_preferences_activate(self, *args):
def on_preferences_activate(self, *_args):
"""Callback when preferences is activated."""
SystemConfigDialog(parent=self)
@ -561,12 +634,30 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.game_store.sort_view(self.view_sorting, self.view_sorting_ascending)
self.no_results_overlay.props.visible = len(self.game_store.modelfilter) == 0
def on_show_installed_first_state_change(self, action, value):
"""Callback to handle installed games first toggle"""
action.set_state(value)
show_installed_first = value.get_boolean()
self.set_show_installed_first_state(show_installed_first)
def set_show_installed_first_state(self, show_installed_first):
"""Shows the installed games first in the view"""
self.show_installed_first = show_installed_first
setting_value = 'true' if show_installed_first else 'false'
settings.write_setting(
'show_installed_first', setting_value
)
self.game_store.sort_view(show_installed_first)
self.game_store.modelfilter.refilter()
def on_show_installed_state_change(self, action, value):
"""Callback to handle uninstalled game filter switch"""
action.set_state(value)
filter_installed = value.get_boolean()
self.set_show_installed_state(filter_installed)
def set_show_installed_state(self, filter_installed):
"""Shows or hide uninstalled games"""
self.filter_installed = filter_installed
setting_value = 'true' if filter_installed else 'false'
settings.write_setting(
@ -576,7 +667,8 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.invalidate_game_filter()
@GtkTemplate.Callback
def on_pga_menuitem_activate(self, *args):
def on_pga_menuitem_activate(self, *_args):
"""Callback for opening the PGA dialog"""
dialogs.PgaSourceDialog(parent=self)
@GtkTemplate.Callback
@ -608,7 +700,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.search_entry.grab_focus()
@GtkTemplate.Callback
def on_about_clicked(self, *args):
def on_about_clicked(self, *_args):
"""Open the about dialog."""
dialogs.AboutDialog(parent=self)
@ -618,16 +710,16 @@ class LutrisWindow(Gtk.ApplicationWindow):
"""
# Wait two seconds to avoid running a game twice
if time.time() - self.game_launch_time < 2:
return
return None
self.game_launch_time = time.time()
return self.view.selected_game
def on_game_run(self, *args, game_id=None):
def on_game_run(self, *_args, game_id=None):
"""Launch a game, or install it if it is not"""
if not game_id:
game_id = self._get_current_game_id()
if not game_id:
return
return None
self.running_game = Game(game_id)
self.running_game.connect('game-error', self.on_game_error)
if self.running_game.is_installed:
@ -645,13 +737,13 @@ class LutrisWindow(Gtk.ApplicationWindow):
dialogs.ErrorDialog(error, parent=self)
@GtkTemplate.Callback
def on_game_stop(self, *args):
"""Stop running game."""
def on_game_stop(self, *_args):
"""Callback to stop a running game."""
if self.running_game:
self.running_game.stop()
self.actions['stop-game'].props.enabled = False
def on_install_clicked(self, *args, game_slug=None, installer_file=None, revision=None):
def on_install_clicked(self, *_args, game_slug=None, installer_file=None, revision=None):
"""Install a game"""
logger.info("Installing %s%s",
game_slug if game_slug else installer_file,
@ -671,6 +763,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
application=self.application)
def game_selection_changed(self, _widget):
"""Callback to handle the selection of a game in the view"""
# Emulate double click to workaround GTK bug #484640
# https://bugzilla.gnome.org/show_bug.cgi?id=484640
if isinstance(self.view, GameGridView):
@ -686,10 +779,11 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.actions['remove-game'].props.enabled = sensitive
def on_game_installed(self, view, game_id):
if type(game_id) != int:
"""Callback to handle newly installed games"""
if not isinstance(game_id, int):
raise ValueError("game_id must be an int")
if not self.view.has_game_id(game_id):
logger.debug("Adding new installed game to view (%d)" % game_id)
logger.debug("Adding new installed game to view (%d)", game_id)
self.add_game_to_view(game_id, is_async=False)
game = Game(game_id)
@ -699,7 +793,8 @@ class LutrisWindow(Gtk.ApplicationWindow):
[game.slug], self.on_image_downloaded)
def on_image_downloaded(self, game_slugs):
logger.debug("Updated images for %d games" % len(game_slugs))
"""Callback for handling successful image downloads"""
logger.debug("Updated images for %d games", len(game_slugs))
for game_slug in game_slugs:
games = pga.get_games_where(slug=game_slug)
@ -708,7 +803,8 @@ class LutrisWindow(Gtk.ApplicationWindow):
is_installed = game.is_installed
self.view.update_image(game.id, is_installed)
def on_add_manually(self, widget, *args):
def on_add_manually(self, _widget, *_args):
"""Callback that presents the Add game dialog"""
def on_game_added(game):
self.view.set_installed(game)
self.sidebar_listbox.update()
@ -720,7 +816,8 @@ class LutrisWindow(Gtk.ApplicationWindow):
callback=lambda: on_game_added(game))
@GtkTemplate.Callback
def on_view_game_log_activate(self, *args):
def on_view_game_log_activate(self, *_args):
"""Callback for opening the log window"""
if not self.running_game:
dialogs.ErrorDialog('No game log available', parent=self)
return
@ -730,7 +827,7 @@ class LutrisWindow(Gtk.ApplicationWindow):
log_window.destroy()
@GtkTemplate.Callback
def on_add_game_button_clicked(self, *args):
def on_add_game_button_clicked(self, *_args):
"""Add a new game manually with the AddGameDialog."""
dialog = AddGameDialog(
self,
@ -751,8 +848,9 @@ class LutrisWindow(Gtk.ApplicationWindow):
def do_add_game():
self.view.add_game_by_id(game_id)
self.switch_splash_screen()
self.sidebar_listbox.update()
self.switch_splash_screen(force=True)
self.sidebar_treeview.update()
self.sidebar_listbox.update() # XXX
return False
if is_async:
@ -761,13 +859,15 @@ class LutrisWindow(Gtk.ApplicationWindow):
do_add_game()
@GtkTemplate.Callback
def on_remove_game(self, *args):
def on_remove_game(self, *_args):
"""Callback that present the uninstall dialog to the user"""
selected_game = self.view.selected_game
UninstallGameDialog(game_id=selected_game,
callback=self.remove_game_from_view,
parent=self)
def remove_game_from_view(self, game_id, from_library=False):
"""Remove a game from the view"""
def do_remove_game():
self.view.remove_game(game_id)
self.switch_splash_screen()
@ -778,22 +878,27 @@ class LutrisWindow(Gtk.ApplicationWindow):
self.view.update_image(game_id, is_installed=False)
self.sidebar_listbox.update()
def on_browse_files(self, widget):
def on_browse_files(self, _widget):
"""Callback to open a game folder in the file browser"""
game = Game(self.view.selected_game)
path = game.get_browse_dir()
if path and os.path.exists(path):
open_uri('file://' + path)
open_uri('file://%s' % path)
else:
dialogs.NoticeDialog(
"Can't open %s \nThe folder doesn't exist." % path
)
def on_view_game(self, widget):
def on_view_game(self, _widget):
"""Callback to open a game on lutris.net"""
game = Game(self.view.selected_game)
open_uri('https://lutris.net/games/' + game.slug)
open_uri('https://lutris.net/games/%s' % game.slug)
def on_edit_game_configuration(self, widget):
"""Edit game preferences."""
def on_edit_game_configuration(self, _widget):
"""Edit game preferences"""
if self.config_menu_first_access:
self.config_menu_first_access = False
init_dxvk_versions()
game = Game(self.view.selected_game)
def on_dialog_saved():
@ -812,8 +917,30 @@ class LutrisWindow(Gtk.ApplicationWindow):
else:
self.switch_view('grid')
def _set_icon_type(self, icon_type):
self.icon_type = icon_type
def on_execute_script_clicked(self, _widget):
"""Execute the game's associated script"""
game = Game(self.view.selected_game)
ondemand_command = game.runner.system_config.get(
"ondemand_command")
if path_exists(ondemand_command):
LutrisThread([ondemand_command],
include_processes=[os.path.basename(ondemand_command)],
cwd=game.directory
).start()
logger.info("Running %s in the background", ondemand_command)
def on_viewtype_state_change(self, action, val):
"""Callback to handle view type switch"""
action.set_state(val)
view_type = val.get_string()
if view_type != self.current_view_type:
self.switch_view(view_type)
def on_icontype_state_change(self, action, value):
"""Callback to handle icon size change"""
action.set_state(value)
self.icon_type = value.get_string()
if self.icon_type == self.game_store.icon_type:
return
if self.current_view_type == 'grid':
@ -845,27 +972,32 @@ class LutrisWindow(Gtk.ApplicationWindow):
'true' if self.view_sorting_ascending else 'false'
)
def create_menu_shortcut(self, *args):
def create_menu_shortcut(self, *_args):
"""Add the selected game to the system's Games menu."""
game = Game(self.view.selected_game)
xdg.create_launcher(game.slug, game.id, game.name, menu=True)
def create_desktop_shortcut(self, *args):
def create_desktop_shortcut(self, *_args):
"""Create a desktop launcher for the selected game."""
game = Game(self.view.selected_game)
xdg.create_launcher(game.slug, game.id, game.name, desktop=True)
def remove_menu_shortcut(self, *args):
def remove_menu_shortcut(self, *_args):
"""Remove an XDG menu shortcut"""
game = Game(self.view.selected_game)
xdg.remove_launcher(game.slug, game.id, menu=True)
def remove_desktop_shortcut(self, *args):
def remove_desktop_shortcut(self, *_args):
"""Remove a .desktop shortcut"""
game = Game(self.view.selected_game)
xdg.remove_launcher(game.slug, game.id, desktop=True)
def on_sidebar_state_change(self, action, value):
"""Callback to handle siderbar toggle"""
action.set_state(value)
self.sidebar_visible = value.get_boolean()
setting = 'true' if self.sidebar_visible else 'false'
settings.write_setting('sidebar_visible', setting)
if self.sidebar_visible:
settings.write_setting('sidebar_visible', 'true')
else:
@ -881,7 +1013,32 @@ class LutrisWindow(Gtk.ApplicationWindow):
else:
self.set_selected_filter(None, row.id)
def on_tray_icon_toggle(self, action, value):
"""Callback for handling tray icon toggle"""
action.set_state(value)
settings.write_setting('show_tray_icon', value)
self.application.set_tray_icon(value)
def show_sidebar(self):
"""Displays the sidebar"""
width = 180 if self.sidebar_visible else 0
self.sidebar_paned.set_position(width)
# def on_sidebar_changed(self, widget):
# """Callback to handle selected runner/platforms updates in sidebar"""
# filer_type, slug = widget.get_selected_filter()
# selected_runner = None
# selected_platform = None
# if not slug:
# pass
# elif filer_type == 'platforms':
# selected_platform = slug
# elif filer_type == 'runners':
# selected_runner = slug
# self.set_selected_filter(selected_runner, selected_platform)
def set_selected_filter(self, runner, platform):
"""Filter the view to a given runner and platform"""
self.selected_runner = runner
self.selected_platform = platform
self.game_store.filter_runner = self.selected_runner

View file

@ -1,8 +1,10 @@
# pylint: disable=missing-docstring
import os
import random
from gi.repository import GLib, GObject, Gtk
from lutris import api, settings
from lutris.gui.dialogs import ErrorDialog
from lutris.gui.dialogs import ErrorDialog, QuestionDialog
from lutris.gui.widgets.dialogs import Dialog
from lutris.util import jobs, system
from lutris.util.downloader import Downloader
@ -21,7 +23,7 @@ class RunnerInstallDialog(Dialog):
super().__init__(
title, parent, 0, ('_OK', Gtk.ResponseType.OK)
)
width, height = (340, 380)
width, height = (460, 380)
self.dialog_size = (width, height)
self.set_default_size(width, height)
@ -53,7 +55,7 @@ class RunnerInstallDialog(Dialog):
renderer_toggle = Gtk.CellRendererToggle()
renderer_text = Gtk.CellRendererText()
renderer_progress = Gtk.CellRendererProgress()
self.renderer_progress = Gtk.CellRendererProgress()
installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=3)
renderer_toggle.connect("toggled", self.on_installed_toggled)
@ -69,7 +71,7 @@ class RunnerInstallDialog(Dialog):
arch_column.set_property('min-width', 50)
treeview.append_column(arch_column)
progress_column = Gtk.TreeViewColumn(None, renderer_progress,
progress_column = Gtk.TreeViewColumn(None, self.renderer_progress,
value=self.COL_PROGRESS,
visible=self.COL_PROGRESS)
progress_column.set_property('fixed-width', 60)
@ -104,7 +106,8 @@ class RunnerInstallDialog(Dialog):
return os.path.join(settings.RUNNER_DIR, self.runner,
"{}-{}".format(version, arch))
def get_dest_path(self, row):
@staticmethod
def get_dest_path(row):
url = row[2]
filename = os.path.basename(url)
return os.path.join(settings.CACHE_DIR, filename)
@ -112,7 +115,12 @@ class RunnerInstallDialog(Dialog):
def on_installed_toggled(self, widget, path):
row = self.runner_store[path]
if row[self.COL_VER] in self.installing:
self.cancel_install(row)
confirm_dlg = QuestionDialog({
"question": "Do you want to cancel the download?",
"title": "Download starting"
})
if confirm_dlg.result == confirm_dlg.YES:
self.cancel_install(row)
elif row[self.COL_INSTALLED]:
self.uninstall_runner(row)
else:
@ -146,9 +154,18 @@ class RunnerInstallDialog(Dialog):
self.cancel_install(row)
return False
downloader.check_progress()
row[4] = downloader.progress_percentage
percent_downloaded = downloader.progress_percentage
if percent_downloaded >= 1:
row[4] = percent_downloaded
self.renderer_progress.props.pulse = -1
self.renderer_progress.props.text = "%d %%" % int(percent_downloaded)
else:
row[4] = 1
self.renderer_progress.props.pulse = random.randint(1, 100)
self.renderer_progress.props.text = "Downloading…"
if downloader.state == downloader.COMPLETED:
row[4] = 99
self.renderer_progress.props.text = "Extracting…"
self.on_runner_downloaded(row)
return False
return True
@ -160,7 +177,8 @@ class RunnerInstallDialog(Dialog):
dst = self.get_runner_path(version, architecture)
jobs.AsyncCall(self.extract, self.on_extracted, src, dst, row)
def extract(self, src, dst, row):
@staticmethod
def extract(src, dst, row):
extract_archive(src, dst)
return src, row
@ -169,6 +187,7 @@ class RunnerInstallDialog(Dialog):
os.remove(src)
row[self.COL_PROGRESS] = 0
row[self.COL_INSTALLED] = True
self.renderer_progress.props.text = ""
self.installing.pop(row[self.COL_VER])
def on_response(self, dialog, response):

View file

@ -1,6 +1,10 @@
# -*- coding:Utf-8 -*-
from gi.repository import GObject, Gtk
from lutris import runners, settings
from gi.repository import Gtk, GObject, Gdk
from lutris import runners
from lutris import settings
from lutris.util.system import open_uri
from lutris.gui.widgets.utils import get_runner_icon
from lutris.gui.dialogs import ErrorDialog
from lutris.gui.config_dialogs import RunnerConfigDialog
from lutris.gui.dialogs import ErrorDialog
from lutris.gui.runnerinstalldialog import RunnerInstallDialog
@ -49,10 +53,11 @@ class RunnersDialog(Gtk.Dialog):
# Header buttons
buttons_box = Gtk.Box(spacing=6)
refresh_button = Gtk.Button.new_from_icon_name('view-refresh-symbolic', Gtk.IconSize.BUTTON)
refresh_button.props.tooltip_text = 'Refresh runners'
refresh_button.connect('clicked', self.on_refresh_clicked)
buttons_box.add(refresh_button)
self.refresh_button = Gtk.Button.new_from_icon_name('view-refresh-symbolic', Gtk.IconSize.BUTTON)
self.refresh_button.props.tooltip_text = 'Refresh runners'
self.refresh_button.show()
self.refresh_button.connect('clicked', self.on_refresh_clicked)
buttons_box.add(self.refresh_button)
open_runner_button = Gtk.Button.new_from_icon_name('folder-symbolic', Gtk.IconSize.BUTTON)
open_runner_button.props.tooltip_text = 'Open Runners Folder'
@ -118,6 +123,12 @@ class RunnersDialog(Gtk.Dialog):
runner_label)
hbox.pack_start(self.install_button, False, False, 5)
self.remove_button = Gtk.Button("Remove")
self.remove_button.set_size_request(90, 30)
self.remove_button.set_valign(Gtk.Align.CENTER)
self.remove_button.connect("clicked", self.on_remove_clicked, runner, runner_label)
hbox.pack_start(self.remove_button, False, False, 5)
self.configure_button = Gtk.Button("Configure")
self.configure_button.set_size_request(90, 30)
self.configure_button.set_valign(Gtk.Align.CENTER)
@ -138,12 +149,21 @@ class RunnersDialog(Gtk.Dialog):
if runner.multiple_versions:
self.versions_button.show()
self.install_button.hide()
if runner.can_uninstall():
self.remove_button.show()
else:
self.remove_button.hide()
else:
self.versions_button.hide()
self.remove_button.hide()
self.install_button.show()
if runner.is_installed():
self.install_button.hide()
if runner.can_uninstall():
self.remove_button.show()
else:
self.remove_button.hide()
self.configure_button.show()
@ -165,15 +185,20 @@ class RunnersDialog(Gtk.Dialog):
ErrorDialog(ex.message, parent=self)
if runner.is_installed():
self.emit('runner-installed')
widget.hide()
runner_label.set_sensitive(True)
self.refresh_button.emit('clicked')
def on_configure_clicked(self, widget, runner, runner_label):
config_dialog = RunnerConfigDialog(runner, parent=self)
config_dialog.connect('destroy', self.set_install_state,
runner, runner_label)
def on_runner_open_clicked(self, widget):
def on_remove_clicked(self, widget, runner, runner_label):
if runner.is_installed():
runner.uninstall()
self.refresh_button.emit('clicked')
@staticmethod
def on_runner_open_clicked(widget):
open_uri('file://' + settings.RUNNER_DIR)
def on_refresh_clicked(self, widget):

View file

@ -1,5 +1,5 @@
import os
from gi.repository import Gtk, Pango, GObject
from gi.repository import Gtk, Pango, GObject, GdkPixbuf
from lutris import runners
from lutris import platforms
@ -14,6 +14,7 @@ TYPE = 0
SLUG = 1
ICON = 2
LABEL = 3
GAMECOUNT = 4
class SidebarRow(Gtk.ListBoxRow):
@ -38,7 +39,147 @@ class SidebarRow(Gtk.ListBoxRow):
ellipsize=Pango.EllipsizeMode.END)
self.box.add(label)
self.add(self.box)
class SidebarTreeView(Gtk.TreeView):
def __init__(self):
super(SidebarTreeView, self).__init__()
self.installed_runners = []
self.active_platforms = []
self.model = Gtk.TreeStore(str, str, GdkPixbuf.Pixbuf, str, str)
self.model_filter = self.model.filter_new()
self.model_filter.set_visible_func(self.filter_rule)
self.set_model(self.model_filter)
column = Gtk.TreeViewColumn("Runners")
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
# Type
type_renderer = Gtk.CellRendererText()
type_renderer.set_visible(False)
column.pack_start(type_renderer, True)
column.add_attribute(type_renderer, "text", TYPE)
# Runner slug
text_renderer = Gtk.CellRendererText()
text_renderer.set_visible(False)
column.pack_start(text_renderer, True)
column.add_attribute(text_renderer, "text", SLUG)
# Icon
icon_renderer = Gtk.CellRendererPixbuf()
icon_renderer.set_property('width', 20)
column.pack_start(icon_renderer, False)
column.add_attribute(icon_renderer, "pixbuf", ICON)
# Label
text_renderer2 = Gtk.CellRendererText()
column.pack_start(text_renderer2, True)
column.add_attribute(text_renderer2, "text", LABEL)
# Gamecount
text_renderer3 = Gtk.CellRendererText()
text_renderer3.set_alignment(1.0, 0.5)
column.pack_start(text_renderer3, True)
column.add_attribute(text_renderer3, "text", GAMECOUNT)
self.append_column(column)
self.set_headers_visible(False)
self.set_fixed_height_mode(True)
self.get_selection().set_mode(Gtk.SelectionMode.BROWSE)
# self.connect('button-press-event', self.popup_contextual_menu)
GObject.add_emission_hook(RunnersDialog, "runner-installed", self.update)
self.runners = sorted(runners.__all__)
self.platforms = sorted(platforms.__all__)
self.platform_node = None
self.load_runners()
self.load_platforms()
self.update()
self.expand_all()
def load_runners(self):
"""Append runners to the model."""
self.runner_node = self.model.append(None, ['runners', '', None, "All runners", None])
for slug in self.runners:
self.add_runner(slug)
# def add_runner(self, slug):
# name = runners.import_runner(slug).human_name
# icon = get_runner_icon(slug, format='pixbuf', size=(16, 16))
# self.model.append(self.runner_node, ['runners', slug, icon, name, None])
def load_platforms(self):
"""Update platforms in the model."""
self.platform_node = self.model.append(None, ['platforms', '', None, "All platforms", None])
for platform in self.platforms:
self.add_platform(platform)
def add_platform(self, name):
self.model.append(self.platform_node, ['platforms', name, None, name, None])
def get_selected_filter(self):
"""Return the selected runner's name."""
selection = self.get_selection()
if not selection:
return None
model, iter = selection.get_selected()
if not iter:
return None
type = model.get_value(iter, TYPE)
slug = model.get_value(iter, SLUG)
return type, slug
def filter_rule(self, model, iter, data):
if not model[iter][0]:
return False
if (model[iter][0] == 'runners' or model[iter][0] == 'platforms') and model[iter][1] == '':
return True
return (model[iter][0] == 'runners' and model[iter][1] in self.installed_runners) or \
(model[iter][0] == 'platforms' and model[iter][1] in self.active_platforms)
def update(self, *args):
self.installed_runners = [runner.name for runner in runners.get_installed()]
self.update_runners_game_count(pga.get_used_runners_game_count())
self.active_platforms = pga.get_used_platforms()
self.update_platforms_game_count(pga.get_used_platforms_game_count())
self.model_filter.refilter()
self.expand_all()
# Return False here because this method is called with GLib.idle_add
return False
def update_runners_game_count(self, counts):
runner_iter = self.model.iter_children(self.runner_node)
self.update_iter_game_counts(runner_iter, counts)
def update_platforms_game_count(self, counts):
platform_iter = self.model.iter_children(self.platform_node)
self.update_iter_game_counts(platform_iter, counts)
def update_iter_game_counts(self, model_iter, counts):
while model_iter is not None:
slug = self.model.get_value(model_iter, SLUG)
count = counts.get(slug, 0)
count_display = "({0})".format(count)
self.model.set_value(model_iter, GAMECOUNT, count_display)
model_iter = self.model.iter_next(model_iter)
# def popup_contextual_menu(self, view, event):
# if event.button != 3:
# return
# view.current_path = view.get_path_at_pos(event.x, event.y)
# if view.current_path:
# view.set_cursor(view.current_path[0])
# type, slug = self.get_selected_filter()
# if type != 'runners' or not slug or slug not in self.runners:
# return
# menu = ContextualMenu()
# menu.popup(event, slug, self.get_toplevel())
# self.add(self.box)
def _create_button_box(self):
self.btn_box = Gtk.Box(spacing=3, no_show_all=True, valign=Gtk.Align.CENTER,

View file

@ -98,8 +98,8 @@ class SyncServiceDialog(Gtk.Dialog):
self.get_content_area().add(box_outer)
description_label = Gtk.Label()
description_label.set_markup("You can import games from local game sources, \n"
"you can also choose to sync everytime Lutris starts")
description_label.set_markup("You can choose which local game sources will get synced each\n"
"time Lutris starts, or launch an immediate import of games.")
box_outer.pack_start(description_label, False, False, 5)
separator = Gtk.Separator()

View file

@ -10,7 +10,8 @@ class UninstallGameDialog(GtkBuilderDialog):
glade_file = 'dialog-uninstall-game.ui'
dialog_object = 'uninstall-game-dialog'
def substitute_label(self, widget, name, replacement):
@staticmethod
def substitute_label(widget, name, replacement):
if hasattr(widget, 'get_text'):
get_text = widget.get_text
set_text = widget.set_text

View file

@ -1,15 +1,46 @@
from gi.repository import Gtk, Pango
from gi.repository import Gtk, Pango, GObject
class GridViewCellRendererText(Gtk.CellRendererText):
"""CellRendererText adjusted for grid view display, removes extra padding"""
def __init__(self, width, **kwargs):
super().__init__(
alignment=Pango.Alignment.CENTER,
wrap_mode=Pango.WrapMode.WORD,
xalign=0.5,
yalign=0,
width=width,
wrap_width=width,
**kwargs
)
def __init__(self, width, *args, **kwargs):
super(GridViewCellRendererText, self).__init__(*args, **kwargs)
self.props.alignment = Pango.Alignment.CENTER
self.props.wrap_mode = Pango.WrapMode.WORD
self.props.xalign = 0.5
self.props.yalign = 0
self.props.width = width
self.props.wrap_width = width
class CellRendererButton(Gtk.CellRenderer):
value = GObject.Property(
type=str,
nick='value',
blurb='what data to render',
flags=(GObject.PARAM_READWRITE | GObject.PARAM_CONSTRUCT))
def __init__(self, layout):
Gtk.CellRenderer.__init__(self)
self.layout = layout
@staticmethod
def do_get_size(widget, cell_area=None):
height = 20
max_width = 100
if cell_area:
return (cell_area.x, cell_area.y,
max(cell_area.width, max_width), cell_area.height)
return 0, 0, max_width, height
def do_render(self, cr, widget, bg_area, cell_area, flags):
context = widget.get_style_context()
context.save()
context.add_class(Gtk.STYLE_CLASS_BUTTON)
self.layout.set_markup("Install")
(x, y, w, h) = self.do_get_size(widget, cell_area)
h -= 4
# Gtk.render_background(context, cr, x, y, w, h)
Gtk.render_frame(context, cr, x, y, w - 2, h + 4)
Gtk.render_layout(context, cr, x + 10, y, self.layout)
context.restore()

View file

@ -64,8 +64,12 @@ class FileChooserEntry(Gtk.Box):
self.file_chooser_dlg.set_create_folders(True)
if default_path:
if not os.path.isdir(default_path):
default_folder = os.path.dirname(default_path)
else:
default_folder = default_path
self.file_chooser_dlg.set_current_folder(
os.path.expanduser(default_path)
os.path.expanduser(default_folder)
)
button = Gtk.Button()
@ -76,6 +80,9 @@ class FileChooserEntry(Gtk.Box):
def get_text(self):
return self.entry.get_text()
def get_filename(self):
return self.entry.get_text()
def _open_filechooser(self, widget, default_path):
if default_path:
self.file_chooser_dlg.set_current_folder(

View file

@ -65,7 +65,7 @@ class DownloadProgressBox(Gtk.Box):
from lutris.gui.dialogs import ErrorDialog
ErrorDialog(ex.args[0])
self.emit('cancel', {})
return
return None
timer_id = GLib.timeout_add(100, self._progress)
self.cancel_button.set_sensitive(True)

View file

@ -51,8 +51,8 @@ def get_icon(icon_name, format='image', size=None, icon_type='runner'):
filename = icon_name.lower().replace(' ', '') + '.png'
icon_path = os.path.join(datapath.get(), 'media/' + icon_type + '_icons', filename)
if not os.path.exists(icon_path):
# The icon doesn't exist, so return nothing.
return
logger.error("Unable to find icon '%s'", icon_path)
return None
if format == 'image':
icon = Gtk.Image()
icon.set_from_file(icon_path)
@ -83,7 +83,7 @@ def get_pixbuf_for_game(game_slug, icon_type, is_installed=True):
icon_path = datapath.get_icon_path(game_slug)
else:
logger.error("Invalid icon type '%s'", icon_type)
return
return None
size = IMAGE_SIZES[icon_type]
@ -94,5 +94,3 @@ def get_pixbuf_for_game(game_slug, icon_type, is_installed=True):
0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 100)
return transparent_pixbuf
return pixbuf

View file

@ -14,6 +14,7 @@ from lutris import runtime
from lutris.util import extract, disks, system
from lutris.util.fileio import EvilConfigParser, MultiOrderedDict
from lutris.util.log import logger
from lutris.util.wine import get_wine_version_exe
from lutris.util import selective_merge
from lutris.runners import wine, import_task
@ -263,6 +264,10 @@ class CommandsMixin:
return
self._killable_process(system.merge_folders, src, dst)
def copy(self, params):
"""Alias for merge"""
self.merge(params)
def move(self, params):
"""Move a file or directory into a destination folder."""
self._check_required_params(['src', 'dst'], params, 'move')
@ -330,7 +335,7 @@ class CommandsMixin:
dst = self._substitute(dst_ref)
if not dst:
raise ScriptingError("Wrong value for 'dst' param", dst_ref)
return (src.rstrip('/'), dst.rstrip('/'))
return src.rstrip('/'), dst.rstrip('/')
def substitute_vars(self, data):
"""Subsitute variable names found in given file."""
@ -372,7 +377,7 @@ class CommandsMixin:
if runner_name.startswith('wine'):
wine_version = self._get_runner_version()
if wine_version:
data['wine_path'] = wine.get_wine_version_exe(wine_version)
data['wine_path'] = get_wine_version_exe(wine_version)
for key in data:
value = data[key]

View file

@ -3,7 +3,6 @@
import os
import time
import yaml
import shutil
from gi.repository import GLib
@ -12,12 +11,14 @@ from json import dumps
from lutris import pga
from lutris import settings
from lutris.game import Game
from lutris.gui.dialogs import WineNotInstalledWarning
from lutris.util import system
from lutris.util.strings import unpack_dependencies
from lutris.util.jobs import AsyncCall
from lutris.util.log import logger
from lutris.util.steam import get_app_state_log
from lutris.util.http import Request
from lutris.util.wine import get_wine_version_exe, get_system_wine_version
from lutris.config import LutrisConfig, make_game_config_id
@ -49,8 +50,7 @@ def fetch_script(game_slug, revision=None):
if key:
return response[key]
else:
return response
return response
def read_script(filename):
@ -64,6 +64,28 @@ def read_script(filename):
return scripts
def _get_game_launcher(script):
"""Return the key and value of the launcher"""
launcher_value = None
# exe64 can be provided to specify an executable for 64bit systems
exe = 'exe64' if 'exe64' in script and system.IS_64BIT else 'exe'
for launcher in (exe, 'iso', 'rom', 'disk', 'main_file'):
if launcher not in script:
continue
launcher_value = script[launcher]
if launcher == "exe64":
launcher = "exe" # If exe64 is used, rename it to exe
break
if not launcher_value:
launcher = None
return launcher, launcher_value
class ScriptInterpreter(CommandsMixin):
"""Convert raw installer script data into actions."""
def __init__(self, installer, parent):
@ -178,7 +200,8 @@ class ScriptInterpreter(CommandsMixin):
self.errors.append('Scripts can\'t have both extends and requires')
return not bool(self.errors)
def _get_installed_dependency(self, dependency):
@staticmethod
def _get_installed_dependency(dependency):
"""Return whether a dependency is installed"""
game = pga.get_game_by_field(dependency, field='installer_slug')
@ -207,7 +230,7 @@ class ScriptInterpreter(CommandsMixin):
else:
if not system.find_executable(dependency):
raise ScriptingError(
"This installer requires %s on your system" % (dependency)
"This installer requires %s on your system" % dependency
)
def _check_dependency(self):
@ -279,7 +302,10 @@ class ScriptInterpreter(CommandsMixin):
os.mkdir(self.cache_path)
if self.target_path and self.should_create_target:
os.makedirs(self.target_path)
try:
os.makedirs(self.target_path)
except PermissionError:
raise ScriptingError("Lutris does not have necessary permissions to install to choosen game dir:", self.target_path)
self.reversion_data['created_main_dir'] = True
if len(self.game_files) < len(self.files):
@ -399,6 +425,9 @@ class ScriptInterpreter(CommandsMixin):
params['fallback'] = False
if not runner.is_installed(**params):
self.runners_to_install.append(runner)
if self.runner.startswith('wine') and not get_system_wine_version():
WineNotInstalledWarning(parent=self.parent)
self.install_runners()
def install_runners(self):
@ -409,7 +438,7 @@ class ScriptInterpreter(CommandsMixin):
self.install_runner(runner)
def install_runner(self, runner):
logger.debug('Installing {}'.format(runner.name))
logger.debug('Installing %s', runner.name)
try:
runner.install(
version=self._get_runner_version(),
@ -525,7 +554,8 @@ class ScriptInterpreter(CommandsMixin):
else:
self._finish_install()
def _get_command_name_and_params(self, command_data):
@staticmethod
def _get_command_name_and_params(command_data):
if isinstance(command_data, dict):
command_name = list(command_data.keys())[0]
command_params = command_data[command_name]
@ -550,31 +580,24 @@ class ScriptInterpreter(CommandsMixin):
# ----------------
def _finish_install(self):
self.parent.set_status("Writing configuration")
self._write_config()
self.parent.set_status("Installation finished !")
self.parent.on_install_finished()
def _get_game_launcher(self):
"""Return the key and value of the launcher"""
game = self.script.get('game')
launcher_value = None
if game:
launcher, launcher_value = _get_game_launcher(game)
path = None
if launcher_value:
path = self._substitute(launcher_value)
if not os.path.isabs(path):
path = os.path.join(self.target_path, launcher_value)
# exe64 can be provided to specify an executable for 64bit systems
exe = 'exe64' if 'exe64' in self.script and system.IS_64BIT else 'exe'
for launcher in [exe, 'iso', 'rom', 'disk', 'main_file']:
if launcher not in self.script:
continue
launcher_value = self.script[launcher]
if launcher == "exe64":
launcher = "exe" # If exe64 is used, rename it to exe
break
if not launcher_value:
launcher = None
return (launcher, launcher_value)
if path and not os.path.isfile(path):
self.parent.set_status("Installation didn't complete successfully")
self.parent.on_install_error("Game executable not found in %s" % path)
else:
self.parent.set_status("Writing configuration")
self._write_config()
self.parent.set_status("Installation finished !")
self.parent.on_install_finished()
def _write_config(self):
"""Write the game configuration in the DB and config file."""
@ -629,8 +652,8 @@ class ScriptInterpreter(CommandsMixin):
# Game options such as exe or main_file can be added at the root of the
# script as a shortcut, this integrates them into the game config
# properly
launcher, launcher_value = self._get_game_launcher()
if type(launcher_value) == list:
launcher, launcher_value = _get_game_launcher(self.script)
if isinstance(launcher_value, list):
game_files = []
for game_file in launcher_value:
if game_file in self.game_files:
@ -643,9 +666,7 @@ class ScriptInterpreter(CommandsMixin):
launcher_value = (
self.game_files[launcher_value]
)
elif self.target_path and os.path.exists(
os.path.join(self.target_path, launcher_value)
):
elif self.target_path and os.path.exists(os.path.join(self.target_path, launcher_value)):
launcher_value = os.path.join(self.target_path, launcher_value)
config['game'][launcher] = launcher_value
@ -671,9 +692,7 @@ class ScriptInterpreter(CommandsMixin):
if isinstance(value, list):
config[key] = [self._substitute(i) for i in value]
elif isinstance(value, dict):
config[key] = dict(
[(k, self._substitute(v)) for (k, v) in value.items()]
)
config[key] = {k: self._substitute(v) for (k, v) in value.items()}
elif isinstance(value, bool):
config[key] = value
else:
@ -686,8 +705,7 @@ class ScriptInterpreter(CommandsMixin):
def cleanup(self):
os.chdir(os.path.expanduser('~'))
if os.path.exists(self.cache_path):
shutil.rmtree(self.cache_path)
system.remove_folder(self.cache_path)
# --------------
# Revert install
@ -701,8 +719,7 @@ class ScriptInterpreter(CommandsMixin):
self.abort_current_task()
if self.reversion_data.get('created_main_dir'):
if os.path.exists(self.target_path):
shutil.rmtree(self.target_path)
system.remove_folder(self.target_path)
# -------------
# Utility stuff
@ -714,6 +731,7 @@ class ScriptInterpreter(CommandsMixin):
"GAMEDIR": self.target_path,
"CACHE": self.cache_path,
"HOME": os.path.expanduser("~"),
"STEAM_DATA_DIR": steam.steam().steam_data_dir,
"DISC": self.game_disc,
"USER": os.getenv('USER'),
"INPUT": self._get_last_user_input(),
@ -803,8 +821,7 @@ class ScriptInterpreter(CommandsMixin):
logger.debug('Steam game has finished installing')
self._on_steam_game_installed()
return False
else:
return True
return True
def _on_steam_game_installed(self, *args):
"""Fired whenever a Steam game has finished installing."""
@ -839,6 +856,7 @@ class ScriptInterpreter(CommandsMixin):
)
self.prepare_game_files()
<<<<<<< HEAD
def _download_steam_data(self, file_uri, file_id):
"""Download the game files from Steam to use them outside of Steam.
@ -918,3 +936,9 @@ class ScriptInterpreter(CommandsMixin):
if installer['id'] == self.gog_data['installerid']:
return (True, installer)
return (False, "")
=======
def eject_wine_disc(self):
prefix = self.target_path
wine_path = get_wine_version_exe(self._get_runner_version())
wine.eject_disc(wine_path, prefix)
>>>>>>> master

View file

@ -1,6 +1,7 @@
import os
import shutil
from lutris.settings import RUNNER_DIR
from lutris.util import system
def migrate():
@ -13,4 +14,4 @@ def migrate():
'mupen64plus', 'nulldc', 'o2em', 'osmose',
'reicast', 'ResidualVM', 'residualvm', 'scummvm',
'snes9x', 'stella', 'vice', 'virtualjaguar', 'zdoom']:
shutil.rmtree(path)
system.remove_folder(path)

View file

@ -7,7 +7,7 @@ from itertools import chain
from lutris.util.strings import slugify
from lutris.util.log import logger
from lutris.util import sql
from lutris.util import sql, system
from lutris import settings
PGA_DB = settings.PGA_DB
@ -38,9 +38,8 @@ def get_schema(tablename):
return tables
def field_to_string(
name="", type="", not_null=False, default=None, indexed=False
):
def field_to_string(name="", type="", indexed=False): # pylint: disable=redefined-builtin
"""Converts a python based table definition to it's SQL statement"""
field_query = "%s %s" % (name, type)
if indexed:
field_query += " PRIMARY KEY"
@ -90,6 +89,7 @@ def migrate_games():
{'name': 'configpath', 'type': 'TEXT'},
{'name': 'has_custom_banner', 'type': 'INTEGER'},
{'name': 'has_custom_icon', 'type': 'INTEGER'},
{'name': 'playtime', 'type': 'TEXT'},
]
return migrate('games', schema)
@ -117,7 +117,7 @@ def set_config_paths():
continue
game_config_path = os.path.join(settings.CONFIG_DIR,
"games/%s.yml" % game['slug'])
if os.path.exists(game_config_path):
if system.path_exists(game_config_path):
logger.debug('Setting configpath to %s', game['slug'])
sql.db_update(
PGA_DB,
@ -127,7 +127,7 @@ def set_config_paths():
)
def get_games(name_filter=None, filter_installed=False, filter_runner=None, select='*'):
def get_games(name_filter=None, filter_installed=False, filter_runner=None, select='*', show_installed_first=False):
"""Get the list of every game in database."""
query = "select " + select + " from games"
params = []
@ -142,7 +142,10 @@ def get_games(name_filter=None, filter_installed=False, filter_runner=None, sele
filters.append("runner = ?")
if filters:
query += " WHERE " + " AND ".join([f for f in filters])
query += " ORDER BY slug"
if show_installed_first:
query += " ORDER BY installed DESC, slug"
else:
query += " ORDER BY slug"
return sql.db_query(PGA_DB, query, tuple(params))
@ -238,29 +241,34 @@ def add_games_bulk(games):
def add_or_update(**params):
slug = params.get('slug')
name = params.get('name')
id = params.get('id')
assert any([slug, name, id])
game_id = params.get('id')
assert any([slug, name, game_id])
if 'id' in params:
game = get_game_by_field(params['id'], 'id')
else:
if not slug:
slug = slugify(name)
game = get_game_by_field(slug, 'slug')
if game:
if (
game and
(
game['runner'] == params.get('runner') or
not all([params.get('runner'), game['runner']])
)
):
game_id = game['id']
sql.db_update(PGA_DB, "games", params, ('id', game_id))
return game_id
else:
return add_game(**params)
return add_game(**params)
def delete_game(id):
def delete_game(game_id):
"""Delete a game from the PGA."""
sql.db_delete(PGA_DB, "games", 'id', id)
sql.db_delete(PGA_DB, "games", 'id', game_id)
def set_uninstalled(id):
sql.db_update(PGA_DB, 'games', {'installed': 0, 'runner': ''}, ('id', id))
def set_uninstalled(game_id):
sql.db_update(PGA_DB, 'games', {'installed': 0, 'runner': ''}, ('id', game_id))
def add_source(uri):
@ -294,15 +302,13 @@ def check_for_file(game, file_id):
source = source[7:]
else:
protocol = source[:7]
logger.warn(
"PGA source protocol {} not implemented".format(protocol)
)
logger.warning("PGA source protocol %s not implemented", protocol)
continue
if not os.path.exists(source):
logger.info("PGA source {} unavailable".format(source))
if not system.path_exists(source):
logger.info("PGA source %s unavailable", source)
continue
game_dir = os.path.join(source, game)
if not os.path.exists(game_dir):
if not system.path_exists(game_dir):
continue
game_files = os.listdir(game_dir)
for game_file in game_files:
@ -322,6 +328,18 @@ def get_used_runners():
return [result[0] for result in results if result[0]]
def get_used_runners_game_count():
"""Return a dictionary listing for each runner in use, how many games are using it."""
with sql.db_cursor(PGA_DB) as cursor:
query = ("select runner, count(*) from games "
"where runner is not null "
"group by runner "
"order by runner")
rows = cursor.execute(query)
results = rows.fetchall()
return {result[0]: result[1] for result in results if result[0]}
def get_used_platforms():
"""Return a list of platforms currently in use"""
with sql.db_cursor(PGA_DB) as cursor:
@ -330,3 +348,18 @@ def get_used_platforms():
rows = cursor.execute(query)
results = rows.fetchall()
return [result[0] for result in results if result[0]]
def get_used_platforms_game_count():
"""Return a dictionary listing for each platform in use, how many games are using it."""
with sql.db_cursor(PGA_DB) as cursor:
# The extra check for 'installed is 1' is needed because
# the platform lists don't show uninstalled games, but the platform of a game
# is remembered even after the game is uninstalled.
query = ("select platform, count(*) from games "
"where platform is not null and platform is not '' and installed is 1 "
"group by platform "
"order by platform")
rows = cursor.execute(query)
results = rows.fetchall()
return {result[0]: result[1] for result in results if result[0]}

View file

@ -21,7 +21,7 @@ def update_platforms():
for pga_game in pga_games:
if pga_game.get('platform') or not pga_game['runner']:
continue
game = Game(id=pga_game['id'])
game = Game(game_id=pga_game['id'])
game.set_platform_from_runner()
game.save(metadata_only=True)

View file

@ -13,7 +13,7 @@ __all__ = (
# Atari
"stella", "atari800", "hatari", "virtualjaguar",
# Nintendo
"snes9x", "mupen64plus", "dolphin", "desmume", "citra",
"snes9x", "mupen64plus", "dolphin", "desmume", "citra", "melonds",
# Sony
"ppsspp", "pcsx2", "rpcs3",
# Sega
@ -49,7 +49,7 @@ def import_runner(runner_name):
"""Dynamically import a runner class."""
runner_module = get_runner_module(runner_name)
if not runner_module:
return
return None
return getattr(runner_module, runner_name)
@ -57,7 +57,7 @@ def import_task(runner, task):
"""Return a runner task."""
runner_module = get_runner_module(runner)
if not runner_module:
return
return None
return getattr(runner_module, task)

View file

@ -1,6 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class ags(Runner):
@ -37,7 +36,7 @@ class ags(Runner):
"""Run the game."""
main_file = self.game_config.get('main_file') or ''
if not os.path.exists(main_file):
if not system.path_exists(main_file):
return {'error': 'FILE_NOT_FOUND', 'file': main_file}
arguments = [self.get_executable()]

View file

@ -10,7 +10,6 @@ from lutris.util import display, extract, system
# pylint: disable=C0103
class atari800(Runner):
description = "Runs Atari 8bit games"
human_name = "Atari800"
platforms = ['Atari 8bit computers'] # FIXME try to determine the actual computer used
runner_executable = 'atari800/bin/atari800'
@ -37,6 +36,7 @@ class atari800(Runner):
}
]
@staticmethod
def get_resolutions():
try:
screen_resolutions = [(resolution, resolution)
@ -105,8 +105,8 @@ class atari800(Runner):
good_bios = {}
for filename in os.listdir(bios_path):
real_hash = system.get_md5_hash(os.path.join(bios_path, filename))
for bios_file in self.bios_checksums.keys():
if real_hash == self.bios_checksums[bios_file]:
for bios_file, checksum in self.bios_checksums.items():
if real_hash == checksum:
logging.debug("%s Checksum : OK", filename)
good_bios[bios_file] = filename
return good_bios
@ -133,9 +133,9 @@ class atari800(Runner):
if not system.path_exists(bios_path):
return {'error': 'NO_BIOS'}
good_bios = self.find_good_bioses(bios_path)
for bios in good_bios.keys():
for bios, filename in good_bios.items():
arguments.append("-%s" % bios)
arguments.append(os.path.join(bios_path, good_bios[bios]))
arguments.append(os.path.join(bios_path, filename))
rom = self.game_config.get('main_file') or ''
if not system.path_exists(rom):

View file

@ -4,7 +4,6 @@ from lutris.runners.runner import Runner
class browser(Runner):
human_name = "Browser"
description = "Runs browser games"
platforms = ["Web"]
description = "Runs games in the browser"
game_options = [
@ -12,7 +11,7 @@ class browser(Runner):
"option": "main_file",
"type": "string",
"label": "Full address (URL)",
'help': ("The full address of the game's web page.")
'help': "The full address of the game's web page."
}
]
runner_options = [

View file

@ -1,6 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class citra(Runner):
@ -12,14 +11,14 @@ class citra(Runner):
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
'help': ("The game data, commonly called a ROM image.")
'help': "The game data, commonly called a ROM image."
}]
def play(self):
"""Run the game."""
arguments = [self.get_executable()]
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
arguments.append(rom)
return {"command": arguments}

View file

View file

@ -0,0 +1,47 @@
"""DOSBox installer commands"""
import os
from lutris.runners import import_runner
from lutris.util import system
from lutris.util.log import logger
def dosexec(config_file=None, executable=None, args=None,
exit=True, working_dir=None):
"""Execute Dosbox with given config_file."""
if config_file:
run_with = "config {}".format(config_file)
if not working_dir:
working_dir = os.path.dirname(config_file)
elif executable:
run_with = "executable {}".format(executable)
if not working_dir:
working_dir = os.path.dirname(executable)
else:
raise ValueError("Neither a config file or an executable were provided")
logger.debug("Running dosbox with %s", run_with)
working_dir = system.create_folder(working_dir)
dosbox = import_runner('dosbox')
dosbox_runner = dosbox()
command = [dosbox_runner.get_executable()]
if config_file:
command += ['-conf', config_file]
if executable:
if not system.path_exists(executable):
raise OSError("Can't find file {}".format(executable))
command += [executable]
if args:
command += args.split()
if exit:
command.append('-exit')
system.execute(command, cwd=working_dir)
def makeconfig(path, drives, commands):
system.create_folder(os.path.dirname(path))
with open(path, 'w') as config_file:
config_file.write('[autoexec]\n')
for drive in drives:
config_file.write("mount {} \"{}\"\n".format(drive, drives[drive]))
for command in commands:
config_file.write("{}\n".format(command))

View file

@ -0,0 +1,297 @@
"""Wine commands for installers"""
# pylint: disable=too-many-arguments
import os
import shlex
import time
from lutris import runtime, settings
from lutris.config import LutrisConfig
from lutris.runners import import_runner
from lutris.thread import LutrisThread
from lutris.util import datapath, system
from lutris.util.log import logger
from lutris.util.wine import (
WINE_DIR,
detect_arch,
detect_prefix_arch,
get_overrides_env,
get_real_executable,
use_lutris_runtime
)
from lutris.util.wineprefix import WinePrefixManager
def set_regedit(path, key, value='', type='REG_SZ', # pylint: disable=redefined-builtin
wine_path=None, prefix=None, arch='win32'):
"""Add keys to the windows registry.
Path is something like HKEY_CURRENT_USER/Software/Wine/Direct3D
"""
formatted_value = {
'REG_SZ': '"%s"' % value,
'REG_DWORD': 'dword:' + value,
'REG_BINARY': 'hex:' + value.replace(' ', ','),
'REG_MULTI_SZ': 'hex(2):' + value,
'REG_EXPAND_SZ': 'hex(7):' + value,
}
# Make temporary reg file
reg_path = os.path.join(settings.CACHE_DIR, 'winekeys.reg')
with open(reg_path, "w") as reg_file:
reg_file.write(
'REGEDIT4\n\n[%s]\n"%s"=%s\n' % (path, key, formatted_value[type])
)
logger.debug("Setting [%s]:%s=%s", path, key, formatted_value[type])
set_regedit_file(reg_path, wine_path=wine_path, prefix=prefix, arch=arch)
os.remove(reg_path)
def set_regedit_file(filename, wine_path=None, prefix=None, arch='win32'):
"""Apply a regedit file to the Windows registry."""
if arch == 'win64' and wine_path and system.path_exists(wine_path + '64'):
# Use wine64 by default if set to a 64bit prefix. Using regular wine
# will prevent some registry keys from being created. Most likely to be
# a bug in Wine. see: https://github.com/lutris/lutris/issues/804
wine_path = wine_path + '64'
wineexec('regedit',
args="/S '%s'" % filename,
wine_path=wine_path,
prefix=prefix,
arch=arch,
blocking=True)
def delete_registry_key(key, wine_path=None, prefix=None, arch='win32'):
"""Deletes a registry key from a Wine prefix"""
wineexec('regedit', args='/S /D "%s"' % key, wine_path=wine_path,
prefix=prefix, arch=arch, blocking=True)
def create_prefix(prefix, wine_path=None, arch='win32', overrides={},
install_gecko=None, install_mono=None):
"""Create a new Wine prefix."""
logger.debug("Creating a %s prefix in %s", arch, prefix)
# Avoid issue of 64bit Wine refusing to create win32 prefix
# over an existing empty folder.
if os.path.isdir(prefix) and not os.listdir(prefix):
os.rmdir(prefix)
if not wine_path:
wine = import_runner('wine')
wine_path = wine().get_executable()
if not wine_path:
logger.error("Wine not found, can't create prefix")
return
wineboot_path = os.path.join(os.path.dirname(wine_path), 'wineboot')
if not system.path_exists(wineboot_path):
logger.error("No wineboot executable found in %s, "
"your wine installation is most likely broken", wine_path)
return
if install_gecko == 'False':
overrides['mshtml'] = 'disabled'
if install_mono == 'False':
overrides['mscoree'] = 'disabled'
wineenv = {
'WINEARCH': arch,
'WINEPREFIX': prefix,
'WINEDLLOVERRIDES': get_overrides_env(overrides)
}
system.execute([wineboot_path], env=wineenv)
for loop_index in range(50):
time.sleep(.25)
if system.path_exists(os.path.join(prefix, 'user.reg')):
break
if loop_index == 20:
logger.warning("Wine prefix creation is taking longer than expected...")
if not os.path.exists(os.path.join(prefix, 'user.reg')):
logger.error('No user.reg found after prefix creation. '
'Prefix might not be valid')
return
logger.info('%s Prefix created in %s', arch, prefix)
prefix_manager = WinePrefixManager(prefix)
prefix_manager.setup_defaults()
def winekill(prefix, arch='win32', wine_path=None, env=None, initial_pids=None):
"""Kill processes in Wine prefix."""
initial_pids = initial_pids or []
if not wine_path:
wine = import_runner('wine')
wine_path = wine().get_executable()
wine_root = os.path.dirname(wine_path)
if not env:
env = {
'WINEARCH': arch,
'WINEPREFIX': prefix
}
command = [os.path.join(wine_root, "wineserver"), "-k"]
logger.debug("Killing all wine processes: %s", command)
logger.debug("\tWine prefix: %s", prefix)
logger.debug("\tWine arch: %s", arch)
if initial_pids:
logger.debug("\tInitial pids: %s", initial_pids)
system.execute(command, env=env, quiet=True)
logger.debug("Waiting for wine processes to terminate")
# Wineserver needs time to terminate processes
num_cycles = 0
while True:
num_cycles += 1
running_processes = [
pid for pid in initial_pids
if system.path_exists("/proc/%s" % pid)
]
if not running_processes:
break
if num_cycles > 20:
logger.warning("Some wine processes are still running: %s",
', '.join(running_processes))
break
time.sleep(0.1)
def wineexec(executable, args="", wine_path=None, prefix=None, arch=None, # pylint: disable=too-many-locals
working_dir=None, winetricks_wine='', blocking=False,
config=None, include_processes=[], exclude_processes=[],
disable_runtime=False, env={}, overrides=None):
"""
Execute a Wine command.
Args:
executable (str): wine program to run, pass None to run wine itself
args (str): program arguments
wine_path (str): path to the wine version to use
prefix (str): path to the wine prefix to use
arch (str): wine architecture of the prefix
working_dir (str): path to the working dir for the process
winetricks_wine (str): path to the wine version used by winetricks
blocking (bool): if true, do not run the process in a thread
config (LutrisConfig): LutrisConfig object for the process context
watch (list): list of process names to monitor (even when in a ignore list)
Returns:
Process results if the process is running in blocking mode or
LutrisThread instance otherwise.
"""
executable = str(executable) if executable else ''
if not wine_path:
wine = import_runner('wine')
wine_path = wine().get_executable()
if not wine_path:
raise RuntimeError("Wine is not installed")
if not working_dir:
if os.path.isfile(executable):
working_dir = os.path.dirname(executable)
executable, _args, working_dir = get_real_executable(executable, working_dir)
if _args:
args = '{} "{}"'.format(_args[0], _args[1])
# Create prefix if necessary
if arch not in ('win32', 'win64'):
arch = detect_arch(prefix, wine_path)
if not detect_prefix_arch(prefix):
wine_bin = winetricks_wine if winetricks_wine else wine_path
create_prefix(prefix, wine_path=wine_bin, arch=arch)
wineenv = {
'WINEARCH': arch
}
if winetricks_wine:
wineenv['WINE'] = winetricks_wine
else:
wineenv['WINE'] = wine_path
if prefix:
wineenv['WINEPREFIX'] = prefix
wine_config = config or LutrisConfig(runner_slug='wine')
disable_runtime = disable_runtime or wine_config.system_config['disable_runtime']
if use_lutris_runtime(wine_path=wineenv['WINE'], force_disable=disable_runtime):
if WINE_DIR in wine_path:
wine_root_path = os.path.dirname(os.path.dirname(wine_path))
elif WINE_DIR in winetricks_wine:
wine_root_path = os.path.dirname(os.path.dirname(winetricks_wine))
else:
wine_root_path = None
wineenv['LD_LIBRARY_PATH'] = ':'.join(runtime.get_paths(
prefer_system_libs=wine_config.system_config['prefer_system_libs'],
wine_path=wine_root_path
))
if overrides:
wineenv['WINEDLLOVERRIDES'] = get_overrides_env(overrides)
wineenv.update(env)
command = [wine_path]
if executable:
command.append(executable)
command += shlex.split(args)
if blocking:
return system.execute(command, env=wineenv, cwd=working_dir)
wine = import_runner('wine')
thread = LutrisThread(command, runner=wine(), env=wineenv, cwd=working_dir,
include_processes=include_processes,
exclude_processes=exclude_processes)
thread.start()
return thread
def winetricks(app, prefix=None, arch=None, silent=True,
wine_path=None, config=None, disable_runtime=False):
"""Execute winetricks."""
winetricks_path = os.path.join(settings.RUNTIME_DIR, 'winetricks/winetricks')
if not system.path_exists(winetricks_path):
logger.warning("Could not find local winetricks install, falling back to bundled version")
winetricks_path = os.path.join(datapath.get(), 'bin/winetricks')
if wine_path:
winetricks_wine = wine_path
else:
wine = import_runner('wine')
winetricks_wine = wine().get_executable()
if arch not in ('win32', 'win64'):
arch = detect_arch(prefix, winetricks_wine)
args = app
if str(silent).lower() in ('yes', 'on', 'true'):
args = "--unattended " + args
return wineexec(None, prefix=prefix, winetricks_wine=winetricks_wine,
wine_path=winetricks_path, arch=arch, args=args,
config=config, disable_runtime=disable_runtime)
def winecfg(wine_path=None, prefix=None, arch='win32', config=None):
"""Execute winecfg."""
if not wine_path:
logger.debug("winecfg: Reverting to default wine")
wine = import_runner('wine')
wine_path = wine().get_executable()
winecfg_path = os.path.join(os.path.dirname(wine_path), "winecfg")
logger.debug("winecfg: %s", winecfg_path)
return wineexec(None, prefix=prefix, winetricks_wine=winecfg_path,
wine_path=winecfg_path, arch=arch, config=config,
include_processes=['winecfg.exe'])
def joycpl(wine_path=None, prefix=None, config=None):
"""Execute Joystick control panel."""
arch = detect_arch(prefix, wine_path)
wineexec('control', prefix=prefix,
wine_path=wine_path, arch=arch, args='joy.cpl')
def eject_disc(wine_path, prefix):
"""Use Wine to eject a drive"""
wineexec('eject', prefix=prefix, wine_path=wine_path, args='-a')

View file

@ -1,6 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class desmume(Runner):
@ -12,14 +11,14 @@ class desmume(Runner):
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
'help': ("The game data, commonly called a ROM image.")
'help': "The game data, commonly called a ROM image."
}]
def play(self):
"""Run the game."""
arguments = [self.get_executable()]
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
arguments.append(rom)
return {"command": arguments}

View file

@ -1,6 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class dgen(Runner):
@ -12,7 +11,7 @@ class dgen(Runner):
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
'help': ("The game data, commonly called a ROM image.")
'help': "The game data, commonly called a ROM image."
}]
runner_options = [
{
@ -29,7 +28,7 @@ class dgen(Runner):
if self.runner_config.get('fullscreen', True):
arguments.append('-f')
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
arguments.append(rom)
return {"command": arguments}

View file

@ -1,5 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class dolphin(Runner):
@ -31,10 +31,13 @@ class dolphin(Runner):
runner_options = []
def get_platform(self):
return self.platforms[int(self.game_config.get('platform') or 1)]
selected_platform = self.game_config.get('platform')
if selected_platform:
return self.platforms[int(selected_platform)]
return ''
def play(self):
iso = self.game_config.get('main_file') or ''
if not os.path.exists(iso):
if not system.path_exists(iso):
return {'error': 'FILE_NOT_FOUND', 'file': iso}
return {'command': [self.get_executable(), '-e', iso]}

View file

@ -1,55 +1,16 @@
# -*- coding: utf-8 -*-
import os
from lutris.util.log import logger
from lutris.util import system
from lutris.runners.runner import Runner
def dosexec(config_file=None, executable=None, args=None, exit=True,
working_dir=None):
"""Execute Dosbox with given config_file."""
if config_file:
run_with = "config {}".format(config_file)
if not working_dir:
working_dir = os.path.dirname(config_file)
elif executable:
run_with = "executable {}".format(executable)
if not working_dir:
working_dir = os.path.dirname(executable)
else:
raise ValueError("Neither a config file or an executable were provided")
logger.debug("Running dosbox with {}".format(run_with))
working_dir = system.create_folder(working_dir)
dosbox_runner = dosbox()
command = [dosbox_runner.get_executable()]
if config_file:
command += ['-conf', config_file]
if executable:
if not os.path.exists(executable):
raise OSError("Can't find file {}".format(executable))
command += [executable]
if args:
command += args.split()
if exit:
command.append('-exit')
system.execute(command, cwd=working_dir)
def makeconfig(path, drives, commands):
system.create_folder(os.path.dirname(path))
with open(path, 'w') as config_file:
config_file.write('[autoexec]\n')
for drive in drives:
config_file.write("mount {} \"{}\"\n".format(drive, drives[drive]))
for command in commands:
config_file.write("{}\n".format(command))
from lutris.runners.commands.dosbox import (
dosexec,
makeconfig
)
from lutris.util import system
class dosbox(Runner):
human_name = "DOSBox"
description = "MS-Dos emulator"
platforms = ["MS-DOS"]
description = "DOS Emulator"
runnable_alone = True
runner_executable = "dosbox/bin/dosbox"
game_options = [
@ -122,14 +83,14 @@ class dosbox(Runner):
"label": "Exit Dosbox with the game",
"type": "bool",
"default": True,
'help': ("Shut down Dosbox when the game is quit.")
'help': "Shut down Dosbox when the game is quit."
},
{
"option": "fullscreen",
"label": "Open game in fullscreen",
"type": "bool",
"default": False,
'help': ("Tells Dosbox to launch the game in fullscreen.")
'help': "Tells Dosbox to launch the game in fullscreen."
}
]
@ -156,7 +117,7 @@ class dosbox(Runner):
def play(self):
main_file = self.main_file
if not os.path.exists(main_file):
if not system.path_exists(main_file):
return {'error': "FILE_NOT_FOUND", 'file': main_file}
args = self.game_config.get('args') or ''

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# It is pitch black. You are likely to be eaten by a grue.
import os
from lutris.runners.runner import Runner
from lutris.util import system
class frotz(Runner):
@ -33,7 +33,7 @@ class frotz(Runner):
story = self.game_config.get('story') or ''
if not self.is_installed():
return {'error': 'RUNNER_NOT_INSTALLED'}
if not os.path.exists(story):
if not system.path_exists(story):
return {'error': 'FILE_NOT_FOUND', 'file': story}
command = [self.get_executable(), story]
return {'command': command}

View file

@ -46,7 +46,7 @@ class fsuae(Runner):
"type": "multiple",
"label": "Additionnal floppies",
'default_path': 'game_path',
'help': ("The additional floppy disk image(s).")
'help': "The additional floppy disk image(s)."
}
]
@ -57,7 +57,7 @@ class fsuae(Runner):
"type": "choice",
"choices": model_choices,
'default': 'A500',
'help': ("Specify the Amiga model you want to emulate.")
'help': "Specify the Amiga model you want to emulate."
},
{
"option": "kickstart_file",
@ -131,8 +131,7 @@ class fsuae(Runner):
return 'cdrom_drive'
elif disk_path.lower().endswith('.hdf'):
return 'hard_drive'
else:
return 'floppy_drive'
return 'floppy_drive'
def get_params(self):
params = []

View file

@ -60,7 +60,7 @@ class hatari(Runner):
"type": "bool",
"label": "Scale up display by 2 (Atari ST/STE)",
'default': True,
'help': ("Double the screen size in windowed mode.")
'help': "Double the screen size in windowed mode."
},
{
"option": "borders",

View file

@ -1,5 +1,5 @@
from lutris.runners.runner import Runner
from os.path import expanduser
from lutris.runners.runner import Runner
class higan(Runner):

View file

@ -1,5 +1,6 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class jzintv(Runner):
@ -56,13 +57,13 @@ class jzintv(Runner):
if self.runner_config.get("fullscreen"):
arguments = arguments + ["-f"]
bios_path = self.runner_config.get("bios_path", '')
if os.path.exists(bios_path):
if system.path_exists(bios_path):
arguments.append("--execimg=%s/exec.bin" % bios_path)
arguments.append("--gromimg=%s/grom.bin" % bios_path)
else:
return {'error': 'NO_BIOS'}
rom_path = self.game_config.get('main_file') or ''
if not os.path.exists(rom_path):
if not system.path_exists(rom_path):
return {'error': 'FILE_NOT_FOUND', 'file': rom_path}
romdir = os.path.dirname(rom_path)
romfile = os.path.basename(rom_path)

View file

@ -12,24 +12,30 @@ from lutris import settings
LIBRETRO_CORES = [
('4do (3DO)', '4do', '3DO'),
('atari800 (Atari 800/5200)', 'atari800', 'Atari 800/5200'),
('blueMSX (MSX/MSX2/MSX+)', 'bluemsx', 'MSX/MSX2/MSX+'),
('blueMSX (MSX/MSX2/MSX2+)', 'bluemsx', 'MSX/MSX2/MSX2+'),
('Caprice32 (Amstrad CPC)', 'cap32', 'Amstrad CPC'),
('ChaiLove', 'chailove', 'ChaiLove'),
('Citra (Nintendo 3DS)', 'citra', 'Nintendo 3DS'),
('Citra Canary (Nintendo 3DS)', 'citra_canary', 'Nintendo 3DS'),
('CrocoDS (Amstrad CPC)', 'crocods', 'Amstrad CPC'),
('Daphne (Arcade)', 'daphne', 'Arcade'),
('DesmuME (Nintendo DS)', 'desmume', 'Nintendo DS'),
('Dolphin (Nintendo Wii/Gamecube)', 'dolphin', 'Nintendo Wii/Gamecube'),
('EightyOne (Sinclair ZX81)', '81', 'Sinclair ZX81'),
('FB Alpha (Arcade)', 'fbalpha', 'Arcade'),
('FCEUmm (Nintendo Entertainment System)', 'fceumm', 'Nintendo NES'),
('fMSX (MSX/MSX2/MSX2+)', 'fmsx', 'MSX/MSX2/MSX2+'),
('FreeJ2ME (J2ME)', 'freej2me', 'J2ME'),
('Fuse (ZX Spectrum)', 'fuse', 'Sinclair ZX Spectrum'),
('Gambatte (Game Boy Color)', 'gambatte', 'Nintendo Game Boy Color'),
('Gearboy (Game Boy Color)', 'gearboy', 'Nintendo Game Boy Color'),
('Gearsystem (Sega Maste System/Gamegear)', 'gearsystem', 'Sega Maste System/Gamegear'),
('Genesis Plus GX (Sega Genesis)', 'genesis_plus_gx', 'Sega Genesis'),
('Handy (Atari Lynx)', 'handy', 'Atari Lynx'),
('Hatari (Atari ST/STE/TT/Falcon)', 'hatari', 'Atari ST/STE/TT/Falcon'),
('higan accuracy(Super Nintendo)', 'higan_sfc', 'Nintendo SNES'),
('higan balanced(Super Nintendo)', 'higan_sfc_balanced', 'Nintendo SNES'),
('Kronos (Sega Saturn)', 'kronos', 'Sega Saturn'),
('MAME (Arcade)', 'mame', 'Arcade'),
('Mednafen GBA (Game Boy Advance)', 'mednafen_gba', 'Nintendo Game Boy Advance'),
('Mednafen NGP (SNK Neo Geo Pocket)', 'mednafen_ngp', 'SNK Neo Geo Pocket'),
@ -47,6 +53,7 @@ LIBRETRO_CORES = [
('Neko Project 2 (NEC PC-98)', 'nekop2', 'NEC PC-98'),
('Neko Project II kai (NEC PC-98)', 'np2kai', 'NEC PC-98'),
('O2EM (Magnavox Odyssey²)', 'o2em', 'Magnavox Odyssey²'),
('ParaLLEl N64 (Nintendo 64)', 'parallel_n64', 'Nintendo N64'),
('PCSX Rearmed (Sony Playstation)', 'pcsx_rearmed', 'Sony PlayStation'),
('PicoDrive (Sega Genesis)', 'picodrive', 'Sega Genesis'),
('Portable SHARP X68000 Emulator (SHARP X68000)', 'px68k', 'Sharp X68000'),
@ -55,7 +62,9 @@ LIBRETRO_CORES = [
('Redream (Sega Dreamcast)', 'redream', 'Sega Dreamcast'),
('Reicast (Sega Dreamcast)', 'reicast', 'Sega Dreamcast'),
('Snes9x (Super Nintendo)', 'snes9x', 'Nintendo SNES'),
('Snes9x2010 (Super Nintendo)', 'snes9x2010', 'Nintendo SNES'),
('Stella (Atari 2600)', 'stella', 'Atari 2600'),
('Uzem (Uzebox)', 'uzem', 'Uzebox'),
('VecX (Vectrex)', 'vecx', 'Vectrex'),
('Yabause (Sega Saturn)', 'yabause', 'Sega Saturn'),
('VBA Next (Game Boy Advance)', 'vba_next', 'Nintendo Game Boy Advance'),
@ -71,9 +80,11 @@ LIBRETRO_CORES = [
def get_core_choices():
return [(core[0], core[1]) for core in LIBRETRO_CORES]
def get_default_config_path(path=''):
return os.path.join(settings.RUNNER_DIR, 'retroarch', path)
class libretro(Runner):
human_name = "Libretro"
description = "Multi system emulator"
@ -121,7 +132,8 @@ class libretro(Runner):
return core[2]
return ''
def get_core_path(self, core):
@staticmethod
def get_core_path(core):
return os.path.join(settings.RUNNER_DIR,
'retroarch/cores/{}_libretro.so'.format(core))
@ -129,14 +141,14 @@ class libretro(Runner):
return self.game_config['core']
def is_retroarch_installed(self):
return os.path.exists(self.get_executable())
return system.path_exists(self.get_executable())
def is_installed(self, core=None):
if self.game_config.get('core') and core is None:
core = self.game_config['core']
if not core or self.runner_config.get('runner_executable'):
return self.is_retroarch_installed()
is_core_installed = os.path.exists(self.get_core_path(core))
is_core_installed = system.path_exists(self.get_core_path(core))
return self.is_retroarch_installed() and is_core_installed
def install(self, version=None, downloader=None, callback=None):
@ -156,13 +168,15 @@ class libretro(Runner):
def get_run_data(self):
return {
'command': [self.get_executable()] + self.get_runner_parameters()
'command': [self.get_executable()] + self.get_runner_parameters(),
'env': self.get_env()
}
def get_config_file(self):
return self.runner_config.get('config_file') or get_default_config_path('retroarch.cfg')
def get_system_directory(self, retro_config):
@staticmethod
def get_system_directory(retro_config):
"""Return the system directory used for storing BIOS and firmwares."""
system_directory = retro_config['system_directory']
if not system_directory or system_directory == 'default':
@ -173,7 +187,7 @@ class libretro(Runner):
config_file = self.get_config_file()
# Create retroarch.cfg if it doesn't exist.
if not os.path.exists(config_file):
if not system.path_exists(config_file):
f = open(config_file, 'w')
f.write('# Lutris RetroArch Configuration')
f.close()
@ -202,7 +216,7 @@ class libretro(Runner):
core = self.game_config.get('core')
info_file = os.path.join(get_default_config_path('info'),
'{}_libretro.info'.format(core))
if os.path.exists(info_file):
if system.path_exists(info_file):
core_config = RetroConfig(info_file)
try:
firmware_count = int(core_config['firmware_count'])
@ -219,7 +233,7 @@ class libretro(Runner):
for index in range(firmware_count):
firmware_filename = core_config['firmware%d_path' % index]
firmware_path = os.path.join(system_path, firmware_filename)
if os.path.exists(firmware_path):
if system.path_exists(firmware_path):
if firmware_filename in checksums:
checksum = system.get_md5_hash(firmware_path)
if checksum == checksums[firmware_filename]:
@ -228,10 +242,9 @@ class libretro(Runner):
checksum_status = 'Checksum failed'
else:
checksum_status = 'No checksum info'
logger.info("Firmware '{}' found ({})".format(firmware_filename,
checksum_status))
logger.info("Firmware '%s' found (%s)", firmware_filename, checksum_status)
else:
logger.warning("Firmware '{}' not found!".format(firmware_filename))
logger.warning("Firmware '%s' not found!", firmware_filename)
# Before closing issue #431
# TODO check for firmware*_opt and display an error message if
@ -272,7 +285,7 @@ class libretro(Runner):
'error': 'CUSTOM',
'text': 'No game file specified'
}
if not os.path.exists(file):
if not system.path_exists(file):
return {
'error': 'FILE_NOT_FOUND',
'file': file

View file

@ -3,6 +3,7 @@ import os
import shlex
import stat
from lutris.runners.runner import Runner
from lutris.util import system
class linux(Runner):
@ -37,7 +38,7 @@ class linux(Runner):
"type": "file",
"label": "Preload library",
'advanced': True,
'help': ("A library to load before running the game's executable.")
'help': "A library to load before running the game's executable."
},
{
"option": "ld_library_path",
@ -86,8 +87,7 @@ class linux(Runner):
return os.path.expanduser(option)
if self.game_exe:
return os.path.dirname(self.game_exe)
else:
return super(linux, self).working_dir
return super(linux, self).working_dir
def is_installed(self):
"""Well of course Linux is installed, you're using Linux right ?"""
@ -97,7 +97,7 @@ class linux(Runner):
"""Run native game."""
launch_info = {}
if not self.game_exe or not os.path.exists(self.game_exe):
if not self.game_exe or not system.path_exists(self.game_exe):
return {'error': 'FILE_NOT_FOUND', 'file': self.game_exe}
# Quit if the file is not executable
@ -105,7 +105,7 @@ class linux(Runner):
if not mode & stat.S_IXUSR:
return {'error': 'NOT_EXECUTABLE', 'file': self.game_exe}
if not os.path.exists(self.game_exe):
if not system.path_exists(self.game_exe):
return {'error': 'FILE_NOT_FOUND', 'file': self.game_exe}
ld_preload = self.game_config.get('ld_preload')

View file

@ -1,6 +1,7 @@
import os
import subprocess
from lutris.runners.runner import Runner
from lutris.util import system
class mame(Runner):
@ -35,7 +36,7 @@ class mame(Runner):
return self.config_dir
def prelaunch(self):
if not os.path.exists(os.path.join(self.config_dir, "mame.ini")):
if not system.path_exists(os.path.join(self.config_dir, "mame.ini")):
try:
os.makedirs(self.config_dir)
except OSError:

View file

@ -1,9 +1,9 @@
import os
import subprocess
from lutris.runners.runner import Runner
from lutris.util.display import get_current_resolution
from lutris.util.log import logger
from lutris.util.joypad import get_controller_mappings
from lutris.util import system
class mednafen(Runner):
@ -58,7 +58,7 @@ class mednafen(Runner):
"type": "choice",
"label": "Machine type",
"choices": machine_choices,
'help': ("The emulated machine.")
'help': "The emulated machine."
}
]
runner_options = [
@ -144,11 +144,12 @@ class mednafen(Runner):
for joy in joy_list:
index = joy.find("Unique ID:")
joy_id = joy[index + 11:]
logger.debug('Joystick found id %s ' % joy_id)
logger.debug('Joystick found id %s ', joy_id)
joy_ids.append(joy_id)
return joy_ids
def set_joystick_controls(self, joy_ids, machine):
@staticmethod
def set_joystick_controls(joy_ids, machine):
""" Setup joystick mappings per machine """
# Get the controller mappings
@ -329,12 +330,12 @@ class mednafen(Runner):
"-" + machine + ".videoip", "1"]
joy_ids = self.find_joysticks()
dont_map_controllers = self.runner_config.get('dont_map_controllers')
if (len(joy_ids) > 0) and not dont_map_controllers:
if joy_ids and not dont_map_controllers:
controls = self.set_joystick_controls(joy_ids, machine)
for control in controls:
options.append(control)
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
command = [self.get_executable()]

23
lutris/runners/melonds.py Normal file
View file

@ -0,0 +1,23 @@
from lutris.runners.runner import Runner
from lutris.util import system
class melonds(Runner):
human_name = "melonDS"
description = "Nintendo DS Emulator"
platforms = ['Nintendo DS']
runner_executable = 'melonDS/melonDS'
game_options = [
{
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
'default_path': 'game_path'
}
]
def play(self):
rom = self.game_config.get('main_file') or ''
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
return {'command': [self.get_executable(), rom]}

View file

@ -2,6 +2,7 @@ import os
from lutris import settings
from lutris.util.log import logger
from lutris.runners.runner import Runner
from lutris.util import system
class mess(Runner):
@ -128,14 +129,14 @@ class mess(Runner):
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
'help': ("The game data, commonly called a ROM image.")
'help': "The game data, commonly called a ROM image."
},
{
'option': 'machine',
'type': 'choice_with_entry',
'label': "Machine",
'choices': machine_choices,
'help': ("The emulated machine.")
'help': "The emulated machine."
},
{
'option': 'device',
@ -217,17 +218,17 @@ class mess(Runner):
def play(self):
rompath = self.runner_config.get('rompath') or ''
if not os.path.exists(rompath):
if not system.path_exists(rompath):
logger.warning("BIOS path provided in %s doesn't exist", rompath)
rompath = os.path.join(settings.RUNNER_DIR, "mess/bios")
if not os.path.exists(rompath):
if not system.path_exists(rompath):
logger.error("Couldn't find %s", rompath)
return {'error': 'NO_BIOS'}
machine = self.game_config.get('machine')
if not machine:
return {'error': 'INCOMPLETE_CONFIG'}
rom = self.game_config.get('main_file') or ''
if rom and not os.path.exists(rom):
if rom and not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
device = self.game_config.get('device')
command = [self.get_executable()]

View file

@ -2,6 +2,7 @@
import os
from lutris import settings
from lutris.runners.runner import Runner
from lutris.util import system
class mupen64plus(Runner):
@ -13,7 +14,7 @@ class mupen64plus(Runner):
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
'help': ("The game data, commonly called a ROM image.")
'help': "The game data, commonly called a ROM image."
}]
runner_options = [
{
@ -45,7 +46,7 @@ class mupen64plus(Runner):
else:
arguments.append('--windowed')
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
arguments.append(rom)
return {'command': arguments}

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
from lutris.runners.runner import Runner
from lutris.util import system
class o2em(Runner):
@ -39,7 +40,7 @@ class o2em(Runner):
"type": "file",
"label": "ROM file",
"default_path": 'game_path',
'help': ("The game data, commonly called a ROM image.")
'help': "The game data, commonly called a ROM image."
}]
runner_options = [
{
@ -89,7 +90,7 @@ class o2em(Runner):
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
if not os.path.exists(self.bios_path):
if not system.path_exists(self.bios_path):
os.makedirs(self.bios_path)
if callback:
callback()
@ -109,7 +110,7 @@ class o2em(Runner):
if "controller2" in self.runner_config:
arguments.append("-s2=%s" % self.runner_config["controller2"])
rom_path = self.game_config.get('main_file') or ''
if not os.path.exists(rom_path):
if not system.path_exists(rom_path):
return {'error': 'FILE_NOT_FOUND', 'file': rom_path}
romdir = os.path.dirname(rom_path)
romfile = os.path.basename(rom_path)

View file

@ -1,5 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class openmsx(Runner):
@ -11,12 +11,12 @@ class openmsx(Runner):
"option": "main_file",
"type": "file",
"label": "ROM file",
'help': ("The game data, commonly called a ROM image.")
'help': "The game data, commonly called a ROM image."
}
]
def play(self):
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
return {'command': [self.get_executable(), rom]}

View file

@ -1,5 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class osmose(Runner):
@ -31,7 +31,7 @@ class osmose(Runner):
"""Run Sega Master System game"""
arguments = [self.get_executable()]
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
arguments.append(rom)
if self.runner_config.get('fullscreen'):

View file

@ -1,5 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class pcsx2(Runner):
@ -32,7 +32,7 @@ class pcsx2(Runner):
arguments.append('--fullscreen')
iso = self.game_config.get('main_file') or ''
if not os.path.exists(iso):
if not system.path_exists(iso):
return {'error': 'FILE_NOT_FOUND', 'file': iso}
arguments.append(iso)
return {'command': arguments}

View file

@ -1,5 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class ppsspp(Runner):
@ -32,7 +32,7 @@ class ppsspp(Runner):
arguments.append('--fullscreen')
iso = self.game_config.get('main_file') or ''
if not os.path.exists(iso):
if not system.path_exists(iso):
return {'error': 'FILE_NOT_FOUND', 'file': iso}
arguments.append(iso)
return {'command': arguments}

View file

@ -5,7 +5,6 @@ from lutris.gui.dialogs import QuestionDialog, FileDialog
from lutris import settings
class redream(Runner):
human_name = "Redream"
description = "Sega Dreamcast emulator"

View file

@ -102,11 +102,12 @@ class reicast(Runner):
self.joypads = joypad_list
return joypad_list
def write_config(self, config):
@staticmethod
def write_config(config):
parser = ConfigParser()
config_path = os.path.expanduser('~/.reicast/emu.cfg')
if os.path.exists(config_path):
if system.path_exists(config_path):
with open(config_path, 'r') as config_file:
parser.read(config_file)

View file

@ -39,10 +39,15 @@ class residualvm(Runner):
'default': False,
},
{
"option": "soft-renderer",
"label": "Software renderer",
"type": "bool",
'default': False,
"option": "renderer",
"label": "Renderer",
"type": "choice",
'choices': (
('OpenGL', '0'),
('OpenGL shaders', '1'),
('Software', '2'),
),
'default': 'OpenGL',
},
{
"option": "show-fps",
@ -77,10 +82,9 @@ class residualvm(Runner):
else:
command.append("--no-fullscreen")
if self.runner_config.get("soft-renderer"):
command.append("--soft-renderer")
else:
command.append("--no-soft-renderer")
renderer = self.runner_config.get("renderer")
if renderer:
command.append("--renderer=%s" % renderer)
if self.runner_config.get("show-fps"):
command.append("--show-fps")

View file

@ -12,7 +12,7 @@ class rpcs3(Runner):
"option": "main_file",
"type": "file",
"default_path": "game_path",
"label": "Game folder"
"label": "Path to EBOOT.BIN"
}
]

View file

@ -1,8 +1,6 @@
# -*- coding:Utf-8 -*-
"""Generic runner."""
"""Base module for runners"""
import os
import platform
import shutil
from gi.repository import Gtk
@ -24,9 +22,9 @@ def get_arch():
machine = platform.machine()
if '64' in machine:
return 'x86_64'
elif '86' in machine:
if '86' in machine:
return 'i386'
elif 'armv7' in machine:
if 'armv7' in machine:
return 'armv7'
@ -123,7 +121,8 @@ class Runner:
"""Return the working directory to use when running the game."""
return os.path.expanduser("~/")
def killall_on_exit(self):
@staticmethod
def killall_on_exit():
return True
def get_platform(self):
@ -249,14 +248,13 @@ class Runner:
version = None
if version:
return self.install(version=version)
else:
return self.install()
return self.install()
return False
def is_installed(self):
"""Return True if runner is installed else False."""
executable = self.get_executable()
if executable and os.path.exists(executable):
if executable and system.path_exists(executable):
return True
def get_runner_info(self, version=None):
@ -304,14 +302,7 @@ class Runner:
self.name, version, downloader, callback)
runner_info = self.get_runner_info(version)
if not runner_info:
raise RunnerInstallationError(
'{} is not available for the {} architecture'.format(
self.name, self.arch
)
)
dialogs.ErrorDialog(
)
return False
raise RunnerInstallationError('{} is not available for the {} architecture'.format(self.name, self.arch))
opts = {}
if downloader:
opts['downloader'] = downloader
@ -355,19 +346,25 @@ class Runner:
"""GObject callback received by downloader"""
self.extract(**user_data)
def extract(self, archive=None, dest=None, merge_single=None,
@staticmethod
def extract(archive=None, dest=None, merge_single=None,
callback=None):
if not os.path.exists(archive):
raise RunnerInstallationError("Failed to extract {}", archive)
if not system.path_exists(archive):
raise RunnerInstallationError("Failed to extract {}".format(archive))
extract_archive(archive, dest, merge_single=merge_single)
os.remove(archive)
if callback:
callback()
def remove_game_data(self, game_path=None):
@staticmethod
def remove_game_data(game_path=None):
system.remove_folder(game_path)
def can_uninstall(self):
runner_path = os.path.join(settings.RUNNER_DIR, self.name)
return os.path.isdir(runner_path)
def uninstall(self):
runner_path = os.path.join(settings.RUNNER_DIR, self.name)
if os.path.isdir(runner_path):
shutil.rmtree(runner_path)
system.remove_folder(runner_path)

View file

@ -4,6 +4,7 @@ import subprocess
from lutris import settings
from lutris.runners.runner import Runner
from lutris.util import system
class scummvm(Runner):
@ -76,7 +77,7 @@ class scummvm(Runner):
@property
def libs_dir(self):
path = os.path.join(settings.RUNNER_DIR, 'scummvm/lib')
return path if os.path.exists(path) else ''
return path if system.path_exists(path) else ''
def get_command(self):
return [
@ -116,8 +117,7 @@ class scummvm(Runner):
command.append("--path=%s" % self.game_path)
command.append(self.game_config.get('game_id'))
launch_info = {'command': command}
launch_info['ld_library_path'] = self.libs_dir
launch_info = {'command': command, 'ld_library_path': self.libs_dir}
return launch_info

View file

@ -5,6 +5,7 @@ import xml.etree.ElementTree as etree
from lutris.util.log import logger
from lutris.runners.runner import Runner
from lutris import settings
from lutris.util import system
SNES9X_DIR = os.path.join(settings.DATA_DIR, "runners/snes9x")
@ -21,7 +22,7 @@ class snes9x(Runner):
"type": "file",
"default_path": "game_path",
"label": "ROM file",
'help': ("The game data, commonly called a ROM image.")
'help': "The game data, commonly called a ROM image."
}
]
@ -55,9 +56,9 @@ class snes9x(Runner):
def set_option(self, option, value):
config_file = os.path.expanduser("~/.snes9x/snes9x.xml")
if not os.path.exists(config_file):
if not system.path_exists(config_file):
subprocess.Popen([self.get_executable(), '-help'])
if not os.path.exists(config_file):
if not system.path_exists(config_file):
logger.error("Snes9x config file creation failed")
return
tree = etree.parse(config_file)
@ -72,6 +73,6 @@ class snes9x(Runner):
self.set_option(option_name, self.runner_config.get(option_name))
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
return {'command': [self.get_executable(), rom]}

View file

@ -62,7 +62,7 @@ class steam(Runner):
'type': 'file',
'label': 'Steamless binary',
'advanced': True,
'help': ("Steamless binary for running the game directly")
'help': "Steamless binary for running the game directly"
},
]
runner_options = [
@ -150,7 +150,7 @@ class steam(Runner):
"""Return the "Steam" part of Steam's config.vdf as a dict."""
steam_data_dir = self.steam_data_dir
if not steam_data_dir:
return
return None
return read_config(steam_data_dir)
@property
@ -182,11 +182,10 @@ class steam(Runner):
def get_executable(self):
if self.runner_config.get('lsi_steam') and system.find_executable('lsi-steam'):
return system.find_executable('lsi-steam')
else:
runner_executable = self.runner_config.get('runner_executable')
if runner_executable and os.path.isfile(runner_executable):
return runner_executable
return system.find_executable(self.runner_executable)
runner_executable = self.runner_config.get('runner_executable')
if runner_executable and os.path.isfile(runner_executable):
return runner_executable
return system.find_executable(self.runner_executable)
@property
def working_dir(self):
@ -243,7 +242,7 @@ class steam(Runner):
steam_config = self.get_steam_config()
if steam_config:
i = 1
while ('BaseInstallFolder_%s' % i) in steam_config:
while 'BaseInstallFolder_%s' % i in steam_config:
path = steam_config['BaseInstallFolder_%s' % i] + '/SteamApps'
path = system.fix_path_case(path)
if path and os.path.isdir(path):
@ -279,12 +278,12 @@ class steam(Runner):
if is_running():
shutdown()
time.sleep(5)
command = ["steam", "steam://install/%s" % (appid)]
command = [self.get_executable(), "steam://install/%s" % appid]
subprocess.Popen(command)
def prelaunch(self):
def check_shutdown(is_running, times=10):
for i in range(1, times):
for _ in range(1, times):
time.sleep(1)
if not is_running():
return True
@ -312,7 +311,7 @@ class steam(Runner):
steamless_binary = self.game_config.get('steamless_binary')
if self.runner_config['run_without_steam'] and steamless_binary:
# Start without steam
if not os.path.exists(steamless_binary):
if not system.path_exists(steamless_binary):
return {'error': 'FILE_NOT_FOUND', 'file': steamless_binary}
self.original_steampid = None
command = [steamless_binary]
@ -346,7 +345,7 @@ class steam(Runner):
def watch_game_process(self):
if not self.appid or not hasattr(self, 'game_launch_time'):
return
return None
state_log = get_app_state_log(self.steam_data_dir, self.appid,
self.game_launch_time)
if not state_log:
@ -372,7 +371,7 @@ class steam(Runner):
if appid is None:
raise RuntimeError('No appid given for uninstallation '
'(game config=%s)' % self.game_config)
logger.debug("Launching Steam uninstall of game %s" % appid)
logger.debug("Launching Steam uninstall of game %s", appid)
command = [self.get_executable(), 'steam://uninstall/%s' % appid]
thread = LutrisThread(command, runner=self, env=self.get_env(), watch=False)
thread.start()

View file

@ -1,5 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class stella(Runner):
@ -22,6 +22,6 @@ class stella(Runner):
def play(self):
cart = self.game_config.get('main_file') or ''
if not os.path.exists(cart):
if not system.path_exists(cart):
return {'error': 'FILE_NOT_FOUND', 'file': cart}
return {'command': [self.get_executable(), cart]}

View file

@ -1,5 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class tic80(Runner):
@ -8,7 +8,7 @@ class tic80(Runner):
platforms = ['TIC-80']
runner_executable = 'tic80/tic80'
game_options = [
{
{
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
@ -47,7 +47,7 @@ class tic80(Runner):
if self.runner_config.get('nosound'):
arguments.append('-nosound')
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
arguments.append(rom)
return {"command": arguments}

View file

@ -112,9 +112,9 @@ class vice(Runner):
def on_runner_installed(*args):
config_path = system.create_folder('~/.vice')
lib_dir = os.path.join(settings.RUNNER_DIR, 'vice/lib/vice')
if not os.path.exists(lib_dir):
if not system.path_exists(lib_dir):
lib_dir = os.path.join(settings.RUNNER_DIR, 'vice/lib64/vice')
if not os.path.exists(lib_dir):
if not system.path_exists(lib_dir):
logger.error('Missing lib folder in the Vice runner')
else:
system.merge_folders(lib_dir, config_path)
@ -137,7 +137,8 @@ class vice(Runner):
root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
return os.path.join(root_dir, 'lib64/vice', paths[machine])
def get_option_prefix(self, machine):
@staticmethod
def get_option_prefix(machine):
prefixes = {
'c64': 'VICII',
'c128': 'VICII',
@ -148,7 +149,8 @@ class vice(Runner):
}
return prefixes[machine]
def get_joydevs(self, machine):
@staticmethod
def get_joydevs(machine):
joydevs = {
'c64': 2,
'c128': 2,
@ -159,7 +161,8 @@ class vice(Runner):
}
return joydevs[machine]
def get_rom_args(self, machine, rom):
@staticmethod
def get_rom_args(machine, rom):
args = []
if rom.endswith('.crt'):
@ -171,7 +174,7 @@ class vice(Runner):
'plus4': "-cart",
'cmbii': None,
}
if (crt_option[machine]):
if crt_option[machine]:
args.append(crt_option[machine])
args.append(rom)
@ -183,7 +186,7 @@ class vice(Runner):
rom = self.game_config.get('main_file')
if not rom:
return {'error': 'CUSTOM', 'text': 'No rom provided'}
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
params = [self.get_executable(machine)]

View file

@ -1,5 +1,5 @@
import os
from lutris.runners.runner import Runner
from lutris.util import system
class virtualjaguar(Runner):
@ -30,6 +30,6 @@ class virtualjaguar(Runner):
def play(self):
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
if not system.path_exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
return {'command': [self.get_executable(), rom]}

View file

@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
import os
import string
import shlex
from urllib.parse import urlparse
from lutris.runners.runner import Runner
from lutris.util import datapath
from lutris.util import datapath, system
from lutris import pga, settings
DEFAULT_ICON = os.path.join(datapath.get(), 'media/default_icon.png')
@ -21,7 +19,7 @@ class web(Runner):
"option": "main_file",
"type": "string",
"label": "Full URL or HTML file path",
'help': ("The full address of the game's web page or path to a HTML file.")
'help': "The full address of the game's web page or path to a HTML file."
}
]
runner_options = [
@ -30,14 +28,14 @@ class web(Runner):
"label": "Open in fullscreen",
"type": "bool",
"default": False,
'help': ("Launch the game in fullscreen.")
'help': "Launch the game in fullscreen."
},
{
"option": "maximize_window",
"label": "Open window maximized",
"type": "bool",
"default": False,
'help': ("Maximizes the window when game starts.")
'help': "Maximizes the window when game starts."
},
{
'option': 'window_size',
@ -46,21 +44,21 @@ class web(Runner):
'choices': ["640x480", "800x600", "1024x768",
"1280x720", "1280x1024", "1920x1080"],
'default': '800x600',
'help': ("The initial size of the game window when not opened.")
'help': "The initial size of the game window when not opened."
},
{
"option": "disable_resizing",
"label": "Disable window resizing (disables fullscreen and maximize)",
"type": "bool",
"default": False,
'help': ("You can't resize this window.")
'help': "You can't resize this window."
},
{
"option": "frameless",
"label": "Borderless window",
"type": "bool",
"default": False,
'help': ("The window has no borders/frame.")
'help': "The window has no borders/frame."
},
{
"option": "disable_menu_bar",
@ -75,7 +73,7 @@ class web(Runner):
"label": "Disable page scrolling and hide scrollbars",
"type": "bool",
"default": False,
'help': ("Disables scrolling on the page.")
'help': "Disables scrolling on the page."
},
{
"option": "hide_cursor",
@ -108,14 +106,14 @@ class web(Runner):
"label": "Enable Adobe Flash Player",
"type": "bool",
"default": False,
'help': ("Enable Adobe Flash Player.")
'help': "Enable Adobe Flash Player."
},
{
"option": "devtools",
"label": "Debug with Developer Tools",
"type": "bool",
"default": False,
'help': ("Let's you debug the page."),
'help': "Let's you debug the page.",
'advanced': True
},
{
@ -123,7 +121,7 @@ class web(Runner):
'label': 'Open in web browser (old behavior)',
'type': 'bool',
'default': False,
'help': ("Launch the game in a web browser.")
'help': "Launch the game in a web browser."
},
{
'option': 'custom_browser_executable',
@ -165,10 +163,10 @@ class web(Runner):
"verify the game's configuration."), }
# check if it's an url or a file
isUrl = urlparse(url).scheme is not ''
is_url = urlparse(url).scheme != ''
if not isUrl:
if not os.path.exists(url):
if not is_url:
if not system.path_exists(url):
return {'error': 'CUSTOM',
'text': ("The file " + url + " does not exist, \n"
"verify the game's configuration."), }
@ -197,21 +195,12 @@ class web(Runner):
return {'command': command}
icon = datapath.get_icon_path(game_data.get('slug'))
if not os.path.exists(icon):
if not system.path_exists(icon):
icon = DEFAULT_ICON
command = [self.get_executable()]
command.append(os.path.join(settings.RUNNER_DIR,
'web/electron/resources/app.asar'))
command.append(url)
command.append("--name")
command.append(game_data.get('name'))
command.append("--icon")
command.append(icon)
command = [self.get_executable(), os.path.join(settings.RUNNER_DIR,
'web/electron/resources/app.asar'), url, "--name",
game_data.get('name'), "--icon", icon]
if self.runner_config.get("fullscreen"):
command.append("--fullscreen")

View file

@ -1,497 +1,51 @@
"""Wine runner module"""
"""Wine runner"""
# pylint: disable=too-many-arguments
import os
import time
import shlex
import shutil
from functools import lru_cache
from collections import OrderedDict
from lutris import runtime
from lutris import settings
from lutris.config import LutrisConfig
from lutris.util import datapath, display, system
from lutris.gui.dialogs import FileDialog
from lutris.runners.runner import Runner
from lutris.util import datapath, display, dxvk, system
from lutris.util.log import logger
from lutris.util.strings import version_sort, parse_version
from lutris.util.strings import parse_version
from lutris.util.vkquery import is_vulkan_supported
from lutris.util.wineprefix import WinePrefixManager
from lutris.util.x360ce import X360ce
from lutris.util import dxvk
from lutris.runners.runner import Runner
from lutris.thread import LutrisThread
from lutris.gui.dialogs import FileDialog
from lutris.util.wine import (
PROTON_PATH,
WINE_DIR,
WINE_PATHS,
detect_arch,
display_vulkan_error,
esync_display_limit_warning,
esync_display_version_warning,
get_default_version,
get_overrides_env,
get_real_executable,
get_system_wine_version,
get_wine_versions,
is_esync_limit_set,
is_version_esync,
support_legacy_version
)
from lutris.runners.commands.wine import ( # pylint: disable=unused-import
create_prefix,
delete_registry_key,
eject_disc,
joycpl,
set_regedit,
set_regedit_file,
winecfg,
wineexec,
winekill,
winetricks
)
WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
MIN_SAFE_VERSION = '3.0' # Wine installers must run with at least this version
WINE_PATHS = {
'winehq-devel': '/opt/wine-devel/bin/wine',
'winehq-staging': '/opt/wine-staging/bin/wine',
'wine-development': '/usr/lib/wine-development/wine',
'system': 'wine',
}
def set_regedit(path, key, value='', type='REG_SZ', wine_path=None,
prefix=None, arch='win32'):
"""Add keys to the windows registry.
Path is something like HKEY_CURRENT_USER\Software\Wine\Direct3D
"""
formatted_value = {
'REG_SZ': '"%s"' % value,
'REG_DWORD': 'dword:' + value,
'REG_BINARY': 'hex:' + value.replace(' ', ','),
'REG_MULTI_SZ': 'hex(2):' + value,
'REG_EXPAND_SZ': 'hex(7):' + value,
}
# Make temporary reg file
reg_path = os.path.join(settings.CACHE_DIR, 'winekeys.reg')
with open(reg_path, "w") as reg_file:
reg_file.write(
'REGEDIT4\n\n[%s]\n"%s"=%s\n' % (path, key, formatted_value[type])
)
logger.debug("Setting [%s]:%s=%s", path, key, formatted_value[type])
set_regedit_file(reg_path, wine_path=wine_path, prefix=prefix, arch=arch)
os.remove(reg_path)
def get_overrides_env(overrides):
"""
Output a string of dll overrides usable with WINEDLLOVERRIDES
See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides
"""
if not overrides:
return ''
override_buckets = OrderedDict([
('n,b', []),
('b,n', []),
('b', []),
('n', []),
('', [])
])
for dll, value in overrides.items():
if not value:
value = ''
value = value.replace(' ', '')
value = value.replace('builtin', 'b')
value = value.replace('native', 'n')
value = value.replace('disabled', '')
try:
override_buckets[value].append(dll)
except KeyError:
logger.error('Invalid override value %s', value)
continue
override_strings = []
for value, dlls in override_buckets.items():
if not dlls:
continue
override_strings.append("{}={}".format(','.join(sorted(dlls)), value))
return ';'.join(override_strings)
def set_regedit_file(filename, wine_path=None, prefix=None, arch='win32'):
"""Apply a regedit file to the Windows registry."""
if arch == 'win64' and wine_path and os.path.exists(wine_path + '64'):
# Use wine64 by default if set to a 64bit prefix. Using regular wine
# will prevent some registry keys from being created. Most likely to be
# a bug in Wine. see: https://github.com/lutris/lutris/issues/804
wine_path = wine_path + '64'
wineexec('regedit',
args="/S '%s'" % filename,
wine_path=wine_path,
prefix=prefix,
arch=arch,
blocking=True)
def delete_registry_key(key, wine_path=None, prefix=None, arch='win32'):
wineexec('regedit', args='/S /D "%s"' % key, wine_path=wine_path,
prefix=prefix, arch=arch, blocking=True)
def create_prefix(prefix, wine_path=None, arch='win32', overrides={},
install_gecko=None, install_mono=None):
"""Create a new Wine prefix."""
if not prefix:
raise ValueError("No prefix path specified")
logger.debug("Creating a %s prefix in %s", arch, prefix)
# Avoid issue of 64bit Wine refusing to create win32 prefix
# over an existing empty folder.
if os.path.isdir(prefix) and not os.listdir(prefix):
os.rmdir(prefix)
if not wine_path:
wine_path = wine().get_executable()
if not wine_path:
logger.error("Wine not found, can't create prefix")
return
wineboot_path = os.path.join(os.path.dirname(wine_path), 'wineboot')
if not system.path_exists(wineboot_path):
logger.error("No wineboot executable found in %s, "
"your wine installation is most likely broken", wine_path)
return
if install_gecko is 'False':
overrides['mshtml'] = 'disabled'
if install_mono is 'False':
overrides['mscoree'] = 'disabled'
wineenv = {
'WINEARCH': arch,
'WINEPREFIX': prefix,
'WINEDLLOVERRIDES': get_overrides_env(overrides)
}
system.execute([wineboot_path], env=wineenv)
for i in range(20):
time.sleep(.25)
if os.path.exists(os.path.join(prefix, 'user.reg')):
break
if not os.path.exists(os.path.join(prefix, 'user.reg')):
logger.error('No user.reg found after prefix creation. '
'Prefix might not be valid')
return
logger.info('%s Prefix created in %s', arch, prefix)
prefix_manager = WinePrefixManager(prefix)
prefix_manager.setup_defaults()
def winekill(prefix, arch='win32', wine_path=None, env=None, initial_pids=None):
"""Kill processes in Wine prefix."""
initial_pids = initial_pids or []
if not wine_path:
wine_path = wine().get_executable()
wine_root = os.path.dirname(wine_path)
if not env:
env = {
'WINEARCH': arch,
'WINEPREFIX': prefix
}
command = [os.path.join(wine_root, "wineserver"), "-k"]
logger.debug("Killing all wine processes: %s", command)
logger.debug("\tWine prefix: %s", prefix)
logger.debug("\tWine arch: %s", arch)
if initial_pids:
logger.debug("\tInitial pids: %s", initial_pids)
system.execute(command, env=env, quiet=True)
logger.debug("Waiting for wine processes to terminate")
# Wineserver needs time to terminate processes
num_cycles = 0
while True:
num_cycles += 1
running_processes = [
pid for pid in initial_pids
if os.path.exists("/proc/%s" % pid)
]
if not running_processes:
break
if num_cycles > 20:
logger.warning("Some wine processes are still running: %s",
', '.join(running_processes))
break
time.sleep(0.1)
def wineexec(executable, args="", wine_path=None, prefix=None, arch=None,
working_dir=None, winetricks_wine='', blocking=False,
config=None, include_processes=[], exclude_processes=[],
disable_runtime=False, env={}, overrides=None):
"""
Execute a Wine command.
Args:
executable (str): wine program to run, pass None to run wine itself
args (str): program arguments
wine_path (str): path to the wine version to use
prefix (str): path to the wine prefix to use
arch (str): wine architecture of the prefix
working_dir (str): path to the working dir for the process
winetricks_wine (str): path to the wine version used by winetricks
blocking (bool): if true, do not run the process in a thread
config (LutrisConfig): LutrisConfig object for the process context
watch (list): list of process names to monitor (even when in a ignore list)
Returns:
Process results if the process is running in blocking mode or
LutrisThread instance otherwise.
"""
executable = str(executable) if executable else ''
if not wine_path:
wine_path = wine().get_executable()
if not wine_path:
raise RuntimeError("Wine is not installed")
if not working_dir:
if os.path.isfile(executable):
working_dir = os.path.dirname(executable)
executable, _args, working_dir = get_real_executable(executable, working_dir)
if _args:
args = '{} "{}"'.format(_args[0], _args[1])
# Create prefix if necessary
if arch not in ('win32', 'win64'):
arch = detect_arch(prefix, wine_path)
if not detect_prefix_arch(prefix):
wine_bin = winetricks_wine if winetricks_wine else wine_path
create_prefix(prefix, wine_path=wine_bin, arch=arch)
wineenv = {
'WINEARCH': arch
}
if winetricks_wine:
wineenv['WINE'] = winetricks_wine
else:
wineenv['WINE'] = wine_path
if prefix:
wineenv['WINEPREFIX'] = prefix
wine_config = config or LutrisConfig(runner_slug='wine')
disable_runtime = disable_runtime or wine_config.system_config['disable_runtime']
if use_lutris_runtime(wine_path=wineenv['WINE'], force_disable=disable_runtime):
if WINE_DIR in wine_path:
wine_root_path = os.path.dirname(os.path.dirname(wine_path))
else:
wine_root_path = None
wineenv['LD_LIBRARY_PATH'] = ':'.join(runtime.get_paths(
prefer_system_libs=wine_config.system_config['prefer_system_libs'],
wine_path=wine_root_path
))
if overrides:
wineenv['WINEDLLOVERRIDES'] = get_overrides_env(overrides)
wineenv.update(env)
command = [wine_path]
if executable:
command.append(executable)
command += shlex.split(args)
if blocking:
return system.execute(command, env=wineenv, cwd=working_dir)
thread = LutrisThread(command, runner=wine(), env=wineenv, cwd=working_dir,
include_processes=include_processes,
exclude_processes=exclude_processes)
thread.start()
return thread
def winetricks(app, prefix=None, arch=None, silent=True,
wine_path=None, config=None, disable_runtime=False):
"""Execute winetricks."""
winetricks_path = os.path.join(datapath.get(), 'bin/winetricks')
if wine_path:
winetricks_wine = wine_path
else:
winetricks_wine = wine().get_executable()
if arch not in ('win32', 'win64'):
arch = detect_arch(prefix, winetricks_wine)
args = app
if str(silent).lower() in ('yes', 'on', 'true'):
args = "--unattended " + args
return wineexec(None, prefix=prefix, winetricks_wine=winetricks_wine,
wine_path=winetricks_path, arch=arch, args=args,
config=config, disable_runtime=disable_runtime)
def winecfg(wine_path=None, prefix=None, arch='win32', config=None):
"""Execute winecfg."""
if not wine_path:
logger.debug("winecfg: Reverting to default wine")
wine_path = wine().get_executable()
winecfg_path = os.path.join(os.path.dirname(wine_path), "winecfg")
logger.debug("winecfg: %s", winecfg_path)
return wineexec(None, prefix=prefix, winetricks_wine=winecfg_path,
wine_path=winecfg_path, arch=arch, config=config,
include_processes=['winecfg.exe'])
def joycpl(wine_path=None, prefix=None, config=None):
"""Execute Joystick control panel."""
arch = detect_arch(prefix, wine_path)
wineexec('control', prefix=prefix,
wine_path=wine_path, arch=arch, args='joy.cpl')
def eject_disc(wine_path, prefix):
wineexec('eject', prefix=prefix, wine_path=wine_path, args='-a')
def detect_arch(prefix_path=None, wine_path=None):
arch = detect_prefix_arch(prefix_path)
if arch:
return arch
if wine_path and os.path.exists(wine_path + '64'):
return 'win64'
else:
return 'win32'
def detect_prefix_arch(prefix_path=None):
"""Return the architecture of the prefix found in `prefix_path`.
If no `prefix_path` given, return the arch of the system's default prefix.
If no prefix found, return None."""
if not prefix_path:
prefix_path = "~/.wine"
prefix_path = os.path.expanduser(prefix_path)
registry_path = os.path.join(prefix_path, 'system.reg')
if not os.path.isdir(prefix_path) or not os.path.isfile(registry_path):
# No prefix_path exists or invalid prefix
logger.debug("Prefix not found: %s", prefix_path)
return None
with open(registry_path, 'r') as registry:
for _line_no in range(5):
line = registry.readline()
if 'win64' in line:
return 'win64'
elif 'win32' in line:
return 'win32'
logger.debug("Failed to detect Wine prefix architecture in %s", prefix_path)
return None
def set_drive_path(prefix, letter, path):
dosdevices_path = os.path.join(prefix, "dosdevices")
if not os.path.exists(dosdevices_path):
raise OSError("Invalid prefix path %s" % prefix)
drive_path = os.path.join(dosdevices_path, letter + ":")
if os.path.exists(drive_path):
os.remove(drive_path)
logger.debug("Linking %s to %s", drive_path, path)
os.symlink(path, drive_path)
def use_lutris_runtime(wine_path, force_disable=False):
"""Returns whether to use the Lutris runtime.
The runtime can be forced to be disabled, otherwise it's disabled
automatically if Wine is installed system wide.
"""
if force_disable or runtime.RUNTIME_DISABLED:
return False
if WINE_DIR in wine_path:
logger.debug("%s is provided by Lutris, using runtime", wine_path)
return True
return not is_installed_systemwide()
def is_installed_systemwide():
"""Return whether Wine is installed outside of Lutris"""
for build in WINE_PATHS.values():
if system.find_executable(build):
if (
build == 'wine' and
os.path.exists('/usr/lib/wine/wine64') and
not os.path.exists('/usr/lib/wine/wine')
):
logger.warning("wine32 is missing from system")
return False
return True
return False
def get_wine_versions():
"""Return the list of Wine versions installed"""
versions = []
for build in sorted(WINE_PATHS.keys()):
version = get_system_wine_version(WINE_PATHS[build])
if version:
versions.append(build)
if os.path.exists(WINE_DIR):
dirs = version_sort(os.listdir(WINE_DIR), reverse=True)
for dirname in dirs:
if is_version_installed(dirname):
versions.append(dirname)
return versions
def get_wine_version_exe(version):
if not version:
version = get_default_version()
if not version:
raise RuntimeError("Wine is not installed")
return os.path.join(WINE_DIR, '{}/bin/wine'.format(version))
def is_version_installed(version):
return os.path.isfile(get_wine_version_exe(version))
def get_default_version():
"""Return the default version of wine. Prioritize 64bit builds"""
installed_versions = get_wine_versions()
wine64_versions = [version for version in installed_versions if '64' in version]
if wine64_versions:
return wine64_versions[0]
if installed_versions:
return installed_versions[0]
@lru_cache(maxsize=10)
def get_system_wine_version(wine_path="wine"):
"""Return the version of Wine installed on the system."""
if not system.path_exists(wine_path) and not shutil.which(wine_path):
return
if os.path.isabs(wine_path):
wine_stats = os.stat(wine_path)
if wine_stats.st_size < 2000:
# This version is a script, ignore it
return
version = system.execute([wine_path, "--version"])
if not version:
return
if version.startswith('wine-'):
version = version[5:]
return version
def support_legacy_version(version):
"""Since Lutris 0.3.7, wine version contains architecture and optional
info. Call this to keep existing games compatible with previous
configurations."""
if not version:
return
if version not in ('custom', 'system') and '-' not in version:
version += '-i386'
return version
def get_real_executable(windows_executable, working_dir=None):
"""Given a Windows executable, return the real program
capable of launching it along with necessary arguments."""
exec_name = windows_executable.lower()
if exec_name.endswith(".msi"):
return ('msiexec', ['/i', windows_executable], working_dir)
if exec_name.endswith(".bat") or exec_name.endswith(".cmd"):
if not working_dir or os.path.dirname(windows_executable) == working_dir:
working_dir = os.path.dirname(windows_executable) or None
windows_executable = os.path.basename(windows_executable)
return ('cmd', ['/C', windows_executable], working_dir)
if exec_name.endswith(".lnk"):
return ('start', ['/unix', windows_executable], working_dir)
return (windows_executable, [], working_dir)
# pylint: disable=C0103
class wine(Runner):
description = "Runs Windows games"
human_name = "Wine"
@ -541,13 +95,6 @@ class wine(Runner):
}
]
system_options_override = [
{
'option': 'disable_runtime',
'default': is_installed_systemwide(),
}
]
reg_prefix = "HKEY_CURRENT_USER/Software/Wine"
reg_keys = {
"RenderTargetLockMode": r"%s/Direct3D" % reg_prefix,
@ -555,10 +102,10 @@ class wine(Runner):
"MouseWarpOverride": r"%s/DirectInput" % reg_prefix,
"OffscreenRenderingMode": r"%s/Direct3D" % reg_prefix,
"StrictDrawOrdering": r"%s/Direct3D" % reg_prefix,
"Desktop": r"%s/Explorer" % reg_prefix,
"WineDesktop": r"%s/Explorer/Desktops" % reg_prefix,
"ShowCrashDialog": r"%s/WineDbg" % reg_prefix,
"UseXVidMode": r"%s/X11 Driver" % reg_prefix
"Desktop": "MANAGED",
"WineDesktop": "MANAGED",
"ShowCrashDialog": "MANAGED",
"UseXVidMode": "MANAGED"
}
core_processes = (
@ -611,6 +158,32 @@ class wine(Runner):
version_choices.append((version, version))
return version_choices
def esync_limit_callback(config):
limits_set = is_esync_limit_set()
wine_path = self.get_path_for_version(config['version'])
wine_ver = is_version_esync(wine_path)
if not limits_set and not wine_ver:
esync_display_version_warning(False)
esync_display_limit_warning()
return False
if not limits_set:
esync_display_limit_warning()
return False
if not wine_ver:
if not esync_display_version_warning(False):
return False
return True
def dxvk_vulkan_callback(config):
if not is_vulkan_supported():
if not display_vulkan_error(False):
return False
return True
self.runner_options = [
{
'option': 'version',
@ -632,8 +205,11 @@ class wine(Runner):
{
'option': 'dxvk',
'label': 'Enable DXVK',
'type': 'bool',
'help': 'Use DXVK to translate DirectX 11 calls to Vulkan'
'type': 'extended_bool',
'help': 'Use DXVK to translate DirectX 11 calls to Vulkan',
'callback': dxvk_vulkan_callback,
'callback_on': True,
'active': True
},
{
'option': 'dxvk_version',
@ -642,6 +218,15 @@ class wine(Runner):
'choices': get_dxvk_choices,
'default': dxvk.DXVK_LATEST
},
{
'option': 'esync',
'label': 'Enable Esync',
'type': 'extended_bool',
'help': 'Enable eventfd-based synchronization (esync)',
'callback': esync_limit_callback,
'callback_on': True,
'active': True
},
{
'option': 'x360ce-path',
'label': "Path to the game's executable, for x360ce support",
@ -691,8 +276,8 @@ class wine(Runner):
'option': 'WineDesktop',
'label': 'Virtual desktop resolution',
'type': 'choice_with_entry',
'choices': display.get_resolution_choices,
'help': ("The size of the virtual desktop in pixels.")
'choices': display.get_unique_resolutions,
'help': "The size of the virtual desktop in pixels."
},
{
'option': 'MouseWarpOverride',
@ -845,7 +430,7 @@ class wine(Runner):
'option': 'sandbox_dir',
'type': 'directory_chooser',
'label': 'Sandbox directory',
'help': ("Custom directory for desktop integration folders.")
'help': "Custom directory for desktop integration folders."
}
]
@ -872,8 +457,7 @@ class wine(Runner):
return option
if self.game_exe:
return os.path.dirname(self.game_exe)
else:
return super(wine, self).working_dir
return super(wine, self).working_dir
@property
def wine_arch(self):
@ -898,10 +482,11 @@ class wine(Runner):
def get_path_for_version(self, version):
if version in WINE_PATHS.keys():
return system.find_executable(WINE_PATHS[version])
elif version == 'custom':
if 'Proton' in version:
return os.path.join(PROTON_PATH, version, 'dist/bin/wine')
if version == 'custom':
return self.runner_config.get('custom_wine_path', '')
else:
return os.path.join(WINE_DIR, version, 'bin/wine')
return os.path.join(WINE_DIR, version, 'bin/wine')
def get_executable(self, version=None, fallback=True):
"""Return the path to the Wine executable.
@ -913,7 +498,7 @@ class wine(Runner):
return
wine_path = self.get_path_for_version(version)
if os.path.exists(wine_path):
if system.path_exists(wine_path):
return wine_path
if fallback:
@ -981,26 +566,17 @@ class wine(Runner):
self.prelaunch()
joycpl(prefix=self.prefix_path, wine_path=self.get_executable(), config=self)
def set_wine_desktop(self, enable_desktop=False):
prefix = self.prefix_path
prefix_manager = WinePrefixManager(prefix)
path = self.reg_keys['Desktop']
if enable_desktop:
prefix_manager.set_registry_key(path, 'Desktop', 'WineDesktop')
else:
prefix_manager.clear_registry_key(path)
def set_regedit_keys(self):
"""Reset regedit keys according to config."""
prefix = self.prefix_path
enable_wine_desktop = False
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,
'UseXVidMode': prefix_manager.use_xvid_mode
'UseXVidMode': prefix_manager.use_xvid_mode,
'Desktop': prefix_manager.set_virtual_desktop,
'WineDesktop': prefix_manager.set_desktop_size
}
for key, path in self.reg_keys.items():
@ -1008,10 +584,7 @@ class wine(Runner):
if not value or value == 'auto' and key not in managed_keys.keys():
prefix_manager.clear_registry_key(path)
elif key in self.runner_config:
if key == 'Desktop' and value is True:
enable_wine_desktop = True
continue
elif key in managed_keys.keys():
if key in managed_keys.keys():
# Do not pass fallback 'auto' value to managed keys
if value == 'auto':
value = None
@ -1019,13 +592,11 @@ class wine(Runner):
continue
prefix_manager.set_registry_key(path, key, value)
self.set_wine_desktop(enable_wine_desktop)
def toggle_dxvk(self, enable, version=None):
dxvk_manager = dxvk.DXVKManager(self.prefix_path, arch=self.wine_arch, version=version)
# manual version only sets the dlls to native
if version != 'manual':
if version.lower() != 'manual':
if enable:
if not dxvk_manager.is_available():
dxvk_manager.download()
@ -1038,7 +609,7 @@ class wine(Runner):
self.dll_overrides[dll] = 'n'
def prelaunch(self):
if not os.path.exists(os.path.join(self.prefix_path, 'user.reg')):
if not system.path_exists(os.path.join(self.prefix_path, 'user.reg')):
create_prefix(self.prefix_path, arch=self.wine_arch)
prefix_manager = WinePrefixManager(self.prefix_path)
if self.runner_config.get('autoconf_joypad', True):
@ -1073,6 +644,9 @@ class wine(Runner):
if self.prefix_path:
env['WINEPREFIX'] = self.prefix_path
if not ("WINEESYNC" in env and env["WINEESYNC"] == "1"):
env["WINEESYNC"] = "1" if self.runner_config.get('esync') else "0"
overrides = self.get_dll_overrides()
if overrides:
env['WINEDLLOVERRIDES'] = get_overrides_env(overrides)
@ -1081,7 +655,7 @@ class wine(Runner):
def get_runtime_env(self):
"""Return runtime environment variables with path to wine for Lutris builds"""
wine_path = self.get_executable()
if WINE_DIR in wine_path:
if WINE_DIR or PROTON_PATH in wine_path:
wine_root = os.path.dirname(os.path.dirname(wine_path))
else:
wine_root = None
@ -1124,7 +698,7 @@ class wine(Runner):
xinput_dest_path = os.path.join(x360ce_path, dll_file)
xinput_arch = self.runner_config.get('xinput-arch') or self.wine_arch
dll_path = os.path.join(datapath.get(), 'controllers/{}-{}'.format(mode, xinput_arch))
if not os.path.exists(xinput_dest_path):
if not system.path_exists(xinput_dest_path):
source_file = dll_file if mode == 'dumbxinputemu' else 'xinput1_3.dll'
shutil.copyfile(os.path.join(dll_path, source_file), xinput_dest_path)
@ -1154,19 +728,41 @@ class wine(Runner):
def play(self):
game_exe = self.game_exe
arguments = self.game_config.get('args', '')
using_dxvk = self.runner_config.get('dxvk')
if not os.path.exists(game_exe):
if using_dxvk:
if not is_vulkan_supported():
if not display_vulkan_error(True):
return {'error': 'VULKAN_NOT_FOUND'}
if not system.path_exists(game_exe):
return {'error': 'FILE_NOT_FOUND', 'file': game_exe}
launch_info = {}
launch_info['env'] = self.get_env(os_env=False)
launch_info = {
'env': self.get_env(os_env=False)
}
if 'WINEESYNC' in launch_info['env'].get('WINEESYNC') == "1":
limit_set = is_esync_limit_set()
wine_ver = is_version_esync(self.get_executable())
if not limit_set and not wine_ver:
esync_display_version_warning(True)
esync_display_limit_warning()
return {'error': 'ESYNC_LIMIT_NOT_SET'}
if not is_esync_limit_set():
esync_display_limit_warning()
return {'error': 'ESYNC_LIMIT_NOT_SET'}
if not wine_ver:
if not esync_display_version_warning(True):
return {'error': 'NON_ESYNC_WINE_VERSION'}
command = [self.get_executable()]
game_exe, _args, working_dir = get_real_executable(game_exe, self.working_dir)
game_exe, args, _working_dir = get_real_executable(game_exe, self.working_dir)
command.append(game_exe)
if _args:
command = command + _args
if args:
command = command + args
if arguments:
for arg in shlex.split(arguments):
@ -1197,8 +793,8 @@ class wine(Runner):
drive = os.readlink(drive)
return os.path.join(drive, path[3:])
elif path[0] == '/': # drive-relative path. C is as good a guess as any..
if path[0] == '/': # drive-relative path. C is as good a guess as any..
return os.path.join(prefix_path, 'drive_c', path[1:])
else: # Relative path
return path
# Relative path
return path

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Runner for the Steam platform"""
import os
import time
@ -15,16 +14,16 @@ from lutris.util.log import logger
from lutris.util.steam import get_app_state_log, read_config
from lutris.services.steam import get_path_from_appmanifest
from lutris.util.wineregistry import WineRegistry
# Redefine wine installer tasks
set_regedit = wine.set_regedit
set_regedit_file = wine.set_regedit_file
delete_registry_key = wine.delete_registry_key
create_prefix = wine.create_prefix
wineexec = wine.wineexec
winetricks = wine.winetricks
winecfg = wine.winecfg
winekill = wine.winekill
from lutris.runners.commands.wine import ( # pylint: disable=unused-import
set_regedit,
set_regedit_file,
delete_registry_key,
create_prefix,
wineexec,
winetricks,
winecfg,
winekill
)
STEAM_INSTALLER_URL = "http://lutris.net/files/runners/SteamInstall.msi"
@ -39,8 +38,7 @@ def is_running():
# If process is defunct, don't consider it as running
process = Process(pid)
return process.state != 'Z'
else:
return False
return False
def kill():
@ -70,7 +68,7 @@ class winesteam(wine.wine):
'option': 'args',
'type': 'string',
'label': 'Arguments',
'help': ("Command line arguments used when launching the game")
'help': "Command line arguments used when launching the game"
},
{
'option': 'prefix',
@ -105,7 +103,7 @@ class winesteam(wine.wine):
'type': 'file',
'label': 'Steamless binary',
'advanced': True,
'help': ("Steamless binary for running the game directly")
'help': "Steamless binary for running the game directly"
},
]
@ -128,7 +126,7 @@ class winesteam(wine.wine):
'label': "Stop Steam after game exits",
'type': 'bool',
'default': True,
'help': ("Shut down Steam after the game has quit.")
'help': "Shut down Steam after the game has quit."
},
{
'option': 'run_without_steam',
@ -192,7 +190,7 @@ class winesteam(wine.wine):
@property
def game_path(self):
if not self.appid:
return
return None
return self.get_game_path_from_appid(self.appid)
@property
@ -200,16 +198,14 @@ class winesteam(wine.wine):
"""Return the working directory to use when running the game."""
if self.runner_config['run_without_steam']:
steamless_binary = self.game_config.get('steamless_binary')
if (os.path.isfile(steamless_binary)):
if steamless_binary and os.path.isfile(steamless_binary):
return os.path.dirname(steamless_binary)
return os.path.expanduser("~/")
@property
def launch_args(self):
args = [self.get_executable(), self.get_steam_path()]
# Try to fix Steam's browser. Never worked but it's supposed to...
args.append('-no-cef-sandbox')
args = [self.get_executable(), self.get_steam_path(), '-no-cef-sandbox', '-console']
steam_args = self.runner_config.get('args') or ''
if steam_args:
@ -218,13 +214,14 @@ class winesteam(wine.wine):
return args
def get_open_command(self, registry):
@staticmethod
def get_open_command(registry):
"""Return Steam's Open command, useful for locating steam when it has
been installed but not yet launched"""
value = registry.query("Software/Classes/steam/Shell/Open/Command",
"default")
if not value:
return
return None
parts = value.split("\"")
return parts[1].strip('\\')
@ -232,7 +229,7 @@ class winesteam(wine.wine):
"""Return the "Steam" part of Steam's config.vfd as a dict"""
steam_data_dir = self.steam_data_dir
if not steam_data_dir:
return
return None
return read_config(steam_data_dir)
@property
@ -249,7 +246,7 @@ class winesteam(wine.wine):
custom_path = self.runner_config.get('steam_path') or ''
if custom_path:
custom_path = os.path.abspath(os.path.expanduser(os.path.join(custom_path, 'Steam.exe')))
if os.path.exists(custom_path):
if system.path_exists(custom_path):
return custom_path
candidates = [
@ -260,16 +257,16 @@ class winesteam(wine.wine):
for prefix in candidates:
# Try the default install path
for default_path in [
"drive_c/Program Files (x86)/Steam/Steam.exe",
"drive_c/Program Files/Steam/Steam.exe",
"drive_c/Program Files (x86)/Steam/Steam.exe",
"drive_c/Program Files/Steam/Steam.exe",
]:
steam_path = os.path.join(prefix, default_path)
if os.path.exists(steam_path):
if system.path_exists(steam_path):
return steam_path
# Try from the registry key
user_reg = os.path.join(prefix, "user.reg")
if not os.path.exists(user_reg):
if not system.path_exists(user_reg):
continue
registry = WineRegistry(user_reg)
steam_path = registry.query("Software/Valve/Steam", "SteamExe")
@ -314,7 +311,7 @@ class winesteam(wine.wine):
logger.warning('wine is not installed')
return False
steam_path = self.get_steam_path()
if not os.path.exists(self.get_default_prefix()):
if not system.path_exists(self.get_default_prefix()):
return False
return system.path_exists(steam_path)
@ -347,7 +344,7 @@ class winesteam(wine.wine):
steam_config = self.get_steam_config()
if steam_config:
i = 1
while ('BaseInstallFolder_%s' % i) in steam_config:
while 'BaseInstallFolder_%s' % i in steam_config:
path = steam_config['BaseInstallFolder_%s' % i] + '/steamapps'
linux_path = self.parse_wine_path(path, self.prefix_path)
linux_path = system.fix_path_case(linux_path)
@ -367,7 +364,7 @@ class winesteam(wine.wine):
arch = self.default_arch
wine_path = self.get_executable()
if not os.path.exists(os.path.dirname(prefix_dir)):
if not system.path_exists(os.path.dirname(prefix_dir)):
os.makedirs(os.path.dirname(prefix_dir))
create_prefix(prefix_dir, arch=arch, wine_path=wine_path)
@ -380,7 +377,7 @@ class winesteam(wine.wine):
if not arch or arch == 'auto':
arch = self.default_arch
prefix = self.get_default_prefix(arch=arch)
if not os.path.exists(prefix):
if not system.path_exists(prefix):
self.create_prefix(prefix, arch=arch)
return prefix
@ -423,8 +420,7 @@ class winesteam(wine.wine):
self.game_launch_time = time.localtime()
game_args = self.game_config.get('args') or ''
launch_info = {}
launch_info['env'] = self.get_env(os_env=False)
launch_info = {'env': self.get_env(os_env=False)}
if self.runner_config.get('x360ce-path'):
self.setup_x360ce(self.runner_config['x360ce-path'])
@ -432,7 +428,7 @@ class winesteam(wine.wine):
steamless_binary = self.game_config.get('steamless_binary')
if self.runner_config['run_without_steam'] and steamless_binary:
# Start without steam
if not os.path.exists(steamless_binary):
if not system.path_exists(steamless_binary):
return {'error': 'FILE_NOT_FOUND', 'file': steamless_binary}
command = [self.get_executable()]
runner_args = self.runner_config.get('args') or ''

View file

@ -1,5 +1,6 @@
import os
from lutris.util import display
import shlex
from lutris.util import display, system
from lutris.runners.runner import Runner
@ -14,7 +15,13 @@ class zdoom(Runner):
'option': 'main_file',
'type': 'file',
'label': 'WAD file',
'help': ("The game data, commonly called a WAD file.")
'help': "The game data, commonly called a WAD file."
},
{
'option': 'args',
'type': 'string',
'label': 'Arguments',
'help': "Command line arguments used when launching the game."
},
{
'option': 'files',
@ -27,7 +34,13 @@ class zdoom(Runner):
'option': 'warp',
'type': 'string',
'label': 'Warp to map',
'help': ("Starts the game on the given map.")
'help': "Starts the game on the given map."
},
{
'option': 'savedir',
'type': 'directory_chooser',
'label': 'Save path',
'help': ("User-specified path where save files should be located.")
}
]
runner_options = [
@ -56,12 +69,19 @@ class zdoom(Runner):
"default": '',
"choices": {
("None", ''),
("I'm Too Young To Die (0)", '0'),
("Hey, Not Too Rough (1)", '1'),
("Hurt Me Plenty (2)", '2'),
("Ultra-Violence (3)", '3'),
("Nightmare! (4)", '4'),
("I'm Too Young To Die (1)", '1'),
("Hey, Not Too Rough (2)", '2'),
("Hurt Me Plenty (3)", '3'),
("Ultra-Violence (4)", '4'),
("Nightmare! (5)", '5'),
}
},
{
"option": "config",
"label": "Config file",
"type": "file",
'help': ("Used to load a user-created configuration file. If specified, "
"the file must contain the wad directory list or launch will fail.")
}
]
@ -73,11 +93,11 @@ class zdoom(Runner):
def get_executable(self):
executable = super(zdoom, self).get_executable()
executable_dir = os.path.dirname(executable)
if not os.path.exists(executable_dir):
if not system.path_exists(executable_dir):
return executable
if not os.path.exists(executable):
if not system.path_exists(executable):
gzdoom_executable = os.path.join(executable_dir, 'gzdoom')
if os.path.exists(gzdoom_executable):
if system.path_exists(gzdoom_executable):
return gzdoom_executable
return executable
@ -106,6 +126,12 @@ class zdoom(Runner):
command.append("-skill")
command.append(skill)
# Append directory for configuration file, if provided.
config = self.runner_config.get('config')
if config:
command.append("-config")
command.append(config)
# Append the warp arguments.
warp = self.game_config.get('warp')
if warp:
@ -113,6 +139,12 @@ class zdoom(Runner):
for warparg in warp.split(' '):
command.append(warparg)
# Append directory for save games, if provided.
savedir = self.game_config.get('savedir')
if savedir:
command.append("-savedir")
command.append(savedir)
# Append the wad file to load, if provided.
wad = self.game_config.get('main_file')
if wad:
@ -135,4 +167,9 @@ class zdoom(Runner):
for pwad in pwads:
command.append(pwad)
# Append additional arguments, if provided.
args = self.game_config.get('args') or ''
for arg in shlex.split(args):
command.append(arg)
return {'command': command}

View file

@ -1,3 +1,4 @@
"""Runtime handling module"""
import os
import time
@ -11,21 +12,127 @@ from lutris.util.log import logger
RUNTIME_DISABLED = os.environ.get('LUTRIS_RUNTIME', '').lower() in ('0', 'off')
class Runtime:
"""Class for manipulating runtime folders"""
def __init__(self, name, updater):
self.name = name
self.updater = updater
@property
def local_runtime_path(self):
"""Return the local path for the runtime folder"""
if not self.name:
return None
return os.path.join(RUNTIME_DIR, self.name)
def get_updated_at(self):
"""Return the modification date of the runtime folder"""
if not system.path_exists(self.local_runtime_path):
return None
return time.gmtime(os.path.getmtime(self.local_runtime_path))
def set_updated_at(self):
"""Set the creation and modification time to now"""
if not system.path_exists(self.local_runtime_path):
logger.error("No local runtime path in %s", self.local_runtime_path)
return None
os.utime(self.local_runtime_path)
def should_update(self, remote_updated_at):
"""Determine if the current runtime should be updated"""
local_updated_at = self.get_updated_at()
if not local_updated_at:
logger.warning("Runtime %s is not available locally", self.name)
return True
if local_updated_at and local_updated_at >= remote_updated_at:
logger.debug(
"Runtime %s is already up to date (locally updated on %s, remote created on %s)",
self.name,
time.strftime("%c", local_updated_at),
time.strftime("%c", remote_updated_at)
)
return False
logger.debug(
"Runtime %s locally updated on %s, remote created on %s)",
self.name,
time.strftime("%c", local_updated_at),
time.strftime("%c", remote_updated_at)
)
return True
def download(self, remote_runtime_info):
"""Downloads a runtime locally"""
remote_updated_at = remote_runtime_info['created_at']
remote_updated_at = time.strptime(
remote_updated_at[:remote_updated_at.find('.')],
"%Y-%m-%dT%H:%M:%S"
)
if not self.should_update(remote_updated_at):
return None
url = remote_runtime_info['url']
archive_path = os.path.join(RUNTIME_DIR, os.path.basename(url))
downloader = Downloader(url, archive_path, overwrite=True)
downloader.start()
GLib.timeout_add(100, self.check_download_progress, downloader)
return downloader
def check_download_progress(self, downloader):
"""Call download.check_progress(), return True if download finished."""
if (not downloader or downloader.state in [downloader.CANCELLED,
downloader.ERROR]):
logger.debug("Runtime update interrupted")
return False
downloader.check_progress()
if downloader.state == downloader.COMPLETED:
self.on_downloaded(downloader.dest)
return False
return True
def on_downloaded(self, path):
"""Actions taken once a runtime is downloaded
Arguments:
path (str): local path to the runtime archive
"""
directory, _filename = os.path.split(path)
# Delete the existing runtime path
initial_path = os.path.join(directory, self.name)
system.remove_folder(initial_path)
# Extract the runtime archive
jobs.AsyncCall(extract_archive, self.on_extracted, path, RUNTIME_DIR,
merge_single=False)
def on_extracted(self, result, error):
"""Callback method when a runtime has extracted"""
if error:
logger.error("Runtime update failed")
logger.error(error)
return
archive_path, destination_path = result
logger.debug("Finished extracting %s to %s", archive_path, destination_path)
os.unlink(archive_path)
self.set_updated_at()
self.updater.notify_finish(self)
class RuntimeUpdater:
"""Class handling the runtime updates"""
current_updates = 0
status_updater = None
cancellables = []
def is_updating(self):
"""Return True if the update process is running"""
return self.current_updates > 0
def get_created_at(self, name):
path = os.path.join(RUNTIME_DIR, name)
if not os.path.exists(path):
return time.gmtime(0)
return time.gmtime(os.path.getctime(path))
def update(self, status_updater=None):
"""Launch the update process"""
if RUNTIME_DISABLED:
logger.debug("Runtime disabled, not updating it.")
return []
@ -37,10 +144,18 @@ class RuntimeUpdater:
if status_updater:
self.status_updater = status_updater
for runtime in self._iter_runtimes():
self.download_runtime(runtime)
if self.status_updater:
self.status_updater("Updating Runtime")
for remote_runtime in self._iter_remote_runtimes():
runtime = Runtime(remote_runtime['name'], self)
downloader = runtime.download(remote_runtime)
if downloader:
self.current_updates += 1
self.cancellables.append(downloader.cancel)
return None
def _iter_runtimes(self):
@staticmethod
def _iter_remote_runtimes():
request = http.Request(RUNTIME_URL)
response = request.get()
runtimes = response.json or []
@ -63,55 +178,12 @@ class RuntimeUpdater:
yield runtime
def download_runtime(self, runtime):
name = runtime['name']
created_at = runtime['created_at']
created_at = time.strptime(created_at[:created_at.find('.')],
"%Y-%m-%dT%H:%M:%S")
if self.get_created_at(name) >= created_at:
return
if self.status_updater:
self.status_updater("Updating Runtime")
logger.debug('Updating runtime %s', name)
url = runtime['url']
archive_path = os.path.join(RUNTIME_DIR, os.path.basename(url))
self.current_updates += 1
downloader = Downloader(url, archive_path, overwrite=True)
self.cancellables.append(downloader.cancel)
downloader.start()
GLib.timeout_add(100, self.check_download_progress, downloader)
def check_download_progress(self, downloader):
"""Call download.check_progress(), return True if download finished."""
if (not downloader or downloader.state in [downloader.CANCELLED,
downloader.ERROR]):
logger.debug("Runtime update interrupted")
return False
downloader.check_progress()
if downloader.state == downloader.COMPLETED:
self.on_downloaded(downloader.dest)
return False
return True
def on_downloaded(self, path):
dir, filename = os.path.split(path)
folder = os.path.join(dir, filename[:filename.find('.')])
system.remove_folder(folder)
jobs.AsyncCall(extract_archive, self.on_extracted, path, RUNTIME_DIR,
merge_single=False)
def on_extracted(self, result, error):
def notify_finish(self, runtime):
"""A runtime has finished downloading"""
logger.debug("Runtime %s is now updated and available", runtime.name)
self.current_updates -= 1
if error:
logger.debug("Runtime update failed")
return
archive_path = result[0]
os.unlink(archive_path)
if self.status_updater and self.current_updates == 0:
self.status_updater("Runtime updated")
logger.debug("Runtime updated")
def get_env(prefer_system_libs=True, wine_path=None):
@ -162,15 +234,17 @@ def get_paths(prefer_system_libs=True, wine_path=None):
# This prioritizes system libraries over
# the Lutris and Steam runtimes.
paths.append("/usr/lib")
if os.path.exists("/usr/lib32"):
if system.path_exists("/usr/lib32"):
paths.append("/usr/lib32")
if os.path.exists("/lib/x86_64-linux-gnu"):
if system.path_exists("/usr/lib64"):
paths.append("/usr/lib64")
if system.path_exists("/lib/x86_64-linux-gnu"):
paths.append("/lib/x86_64-linux-gnu")
if os.path.exists("/lib/i386-linux-gnu"):
if system.path_exists("/lib/i386-linux-gnu"):
paths.append("/lib/i386-linux-gnu")
if os.path.exists("/usr/lib/x86_64-linux-gnu"):
if system.path_exists("/usr/lib/x86_64-linux-gnu"):
paths.append("/usr/lib/x86_64-linux-gnu")
if os.path.exists("/usr/lib/i386-linux-gnu"):
if system.path_exists("/usr/lib/i386-linux-gnu"):
paths.append("/usr/lib/i386-linux-gnu")
# Then resolve absolute paths for the runtime

View file

@ -5,6 +5,7 @@ from lutris import pga
from lutris.util.log import logger
from lutris.util.strings import slugify
from lutris.config import make_game_config_id, LutrisConfig
from lutris.util import system
NAME = "ScummVM"
INSTALLER_SLUG = 'system-scummvm'
@ -13,7 +14,7 @@ SCUMMVM_CONFIG_FILE = os.path.join(os.path.expanduser("~/.config/scummvm"), "scu
def mark_as_installed(scummvm_id, name, path):
"""Add scummvm from the auto-import"""
logger.info("Setting %s as installed" % name)
logger.info("Setting %s as installed", name)
slug = slugify(name)
config_id = make_game_config_id(slug)
game_id = pga.add_or_update(
@ -46,7 +47,7 @@ def mark_as_uninstalled(game_info):
def get_scummvm_games():
if not os.path.exists(SCUMMVM_CONFIG_FILE):
if not system.path_exists(SCUMMVM_CONFIG_FILE):
logger.info("No ScummVM config found")
return []
config = ConfigParser()

View file

@ -5,7 +5,7 @@ from collections import defaultdict
from lutris import pga
from lutris.util.log import logger
from lutris.util.steam import vdf_parse
from lutris.util.system import fix_path_case
from lutris.util.system import fix_path_case, path_exists
from lutris.util.strings import slugify
from lutris.config import make_game_config_id, LutrisConfig
@ -42,9 +42,13 @@ class AppManifest:
self.appmanifest_path = appmanifest_path
self.steamapps_path, filename = os.path.split(appmanifest_path)
self.steamid = re.findall(r'(\d+)', filename)[-1]
if os.path.exists(appmanifest_path):
self.appmanifest_data = {}
if path_exists(appmanifest_path):
with open(appmanifest_path, "r") as appmanifest_file:
self.appmanifest_data = vdf_parse(appmanifest_file, {})
else:
logger.error("Path to AppManifest file %s doesn't exist", appmanifest_path)
def __repr__(self):
return "<AppManifest: %s>" % self.appmanifest_path
@ -88,12 +92,14 @@ class AppManifest:
def get_install_path(self):
if not self.installdir:
return
return None
install_path = fix_path_case(os.path.join(self.steamapps_path, "common",
self.installdir))
if install_path:
return install_path
return None
def get_platform(self):
steamapps_paths = get_steamapps_paths()
if self.steamapps_path in steamapps_paths['linux']:
@ -156,13 +162,13 @@ def get_appmanifest_from_appid(steamapps_path, appid):
"""Given the steam apps path and appid, return the corresponding appmanifest"""
if not steamapps_path:
raise ValueError("steamapps_path is mandatory")
if not os.path.exists(steamapps_path):
if not path_exists(steamapps_path):
raise IOError("steamapps_path must be a valid directory")
if not appid:
raise ValueError("Missing mandatory appid")
appmanifest_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid)
if not os.path.exists(appmanifest_path):
return
if not path_exists(appmanifest_path):
return None
return AppManifest(appmanifest_path)
@ -170,7 +176,7 @@ def get_path_from_appmanifest(steamapps_path, appid):
"""Return the path where a Steam game is installed."""
appmanifest = get_appmanifest_from_appid(steamapps_path, appid)
if not appmanifest:
return
return None
return appmanifest.get_install_path()
@ -179,7 +185,6 @@ def mark_as_installed(steamid, runner_name, game_info):
for key in ['name', 'slug']:
if key not in game_info:
raise ValueError("Missing %s field in %s" % (key, game_info))
logger.info("Setting %s as installed", game_info['name'])
config_id = (game_info.get('config_path') or make_game_config_id(game_info['slug']))
game_id = pga.add_or_update(
@ -205,7 +210,7 @@ def mark_as_uninstalled(game_info):
for key in ('id', 'name'):
if key not in game_info:
raise ValueError("Missing %s field in %s" % (key, game_info))
logger.info('Setting %s as uninstalled' % game_info['name'])
logger.info('Setting %s as uninstalled', game_info['name'])
game_id = pga.add_or_update(
id=game_info['id'],
runner='',
@ -243,7 +248,8 @@ def sync_with_lutris(platform='linux'):
logger.debug("Syncing Steam for %s games to Lutris", platform.capitalize())
steamapps_paths = get_steamapps_paths()
steam_games_in_lutris = pga.get_games_where(steamid__isnull=False, steamid__not='')
steamids_in_lutris = set([str(game['steamid']) for game in steam_games_in_lutris])
proton_ids = ["858280", "930400", "961940", "228980"]
steamids_in_lutris = {str(game['steamid']) for game in steam_games_in_lutris}
seen_ids = set() # Set of Steam appids seen while browsing AppManifests
for steamapps_path in steamapps_paths[platform]:
@ -252,7 +258,7 @@ def sync_with_lutris(platform='linux'):
steamid = re.findall(r'(\d+)', appmanifest_file)[0]
seen_ids.add(steamid)
appmanifest_path = os.path.join(steamapps_path, appmanifest_file)
if steamid not in steamids_in_lutris:
if steamid not in steamids_in_lutris and steamid not in proton_ids:
# New Steam game, not seen before in Lutris,
if platform != 'linux':
# Windows games might require additional steps.

View file

@ -167,7 +167,7 @@ class TOSEC:
'AND md5 = ? AND sha1 = ?',
rom_info
)
for row in rom_rows:
for _ in rom_rows:
rom_exists = True
if not rom_exists:
rom_info.append(game_id)
@ -189,8 +189,8 @@ class TOSEC:
return True
def get_rom_id(self, rom):
input = open(rom, "rb")
data = input.read()
opened_rom = open(rom, "rb")
data = opened_rom.read()
md5 = hashlib.md5(data).hexdigest()
sha1 = hashlib.sha1(data).hexdigest()
@ -260,23 +260,23 @@ def get_games_from_words(words):
else:
if word == "(":
# Add a new depth in the dictionaries tree
dict = game
dict_game = game
for element in path.split(" "):
if element != "":
dict = dict[element]
dict[tag] = {}
dict_game = dict_game[element]
dict_game[tag] = {}
if path == "":
path = tag
else:
path = path + " " + tag
else:
dict = game
dict_game = game
for element in path.split(" "):
dict = dict[element]
dict[tag] = word
dict_game = dict_game[element]
dict_game[tag] = word
tag = None
return (clrmamepro, games)
return clrmamepro, games
def split_game_title(game):
@ -294,7 +294,7 @@ def split_game_title(game):
title = result.group(1)
game_flags = result.group(2)
rom_flags = result.group(3)
return (title, game_flags, rom_flags)
return title, game_flags, rom_flags
def datefromiso(isoformat):

View file

@ -41,7 +41,7 @@ IGNORED_CATEGORIES = (
def mark_as_installed(appid, runner_name, game_info):
for key in ['name', 'slug']:
assert game_info[key]
logger.info("Setting %s as installed" % game_info['name'])
logger.info("Setting %s as installed", game_info['name'])
config_id = (game_info.get('config_path') or make_game_config_id(game_info['slug']))
game_id = pga.add_or_update(
name=game_info['name'],
@ -69,11 +69,8 @@ def mark_as_installed(appid, runner_name, game_info):
def mark_as_uninstalled(game_info):
logger.info('Uninstalling %s' % game_info['name'])
return pga.add_or_update(
id=game_info['id'],
installed=0
)
logger.info('Uninstalling %s', game_info['name'])
return pga.add_or_update(id=game_info['id'], installed=0)
def sync_with_lutris():
@ -88,10 +85,10 @@ def sync_with_lutris():
for name, appid, exe, args in get_games():
slug = slugify(name) or slugify(appid)
if not all([name, slug, appid]):
logger.error("Failed to load desktop game \"{}\" (app: {}, slug: {})".format(name, appid, slug))
logger.error("Failed to load desktop game \"%s\" (app: %s, slug: %s)", name, appid, slug)
continue
else:
logger.info("Found desktop game \"{}\" (app: {}, slug: {})".format(name, appid, slug))
logger.info("Found desktop game \"%s\" (app: %s, slug: %s)", name, appid, slug)
seen.add(slug)
if slug not in desktop_games.keys():
@ -131,8 +128,6 @@ def get_games():
except UnicodeDecodeError:
logger.error("Failed to read ID for app %s (non UTF-8 encoding). Reverting to executable name.", app)
appid = app.get_executable()
exe = None
args = []
# must be in Game category
categories = app.get_categories()
@ -143,11 +138,12 @@ def get_games():
continue
# contains a blacklisted category
ok = True
has_blacklisted = False
for category in categories:
if category in map(str.lower, IGNORED_CATEGORIES):
ok = False
if not ok:
has_blacklisted = True
break
if has_blacklisted:
continue
# game is blacklisted

View file

@ -58,7 +58,7 @@ def sync_game_details(remote_library):
if not sync_required:
continue
logger.debug("Syncing details for %s" % slug)
logger.debug("Syncing details for %s", slug)
game_id = pga.add_or_update(
name=local_game['name'],
runner=local_game['runner'],
@ -88,17 +88,17 @@ def sync_from_remote():
:rtype: tuple of sets, added games and updated games
"""
local_library = pga.get_games()
local_slugs = set([game['slug'] for game in local_library])
local_slugs = {game['slug'] for game in local_library}
try:
remote_library = api.get_library()
except Exception as ex:
logger.error("Error while downloading the remote library: %s" % ex)
logger.error("Error while downloading the remote library: %s", ex)
remote_library = {}
remote_slugs = set([game['slug'] for game in remote_library])
remote_slugs = {game['slug'] for game in remote_library}
missing_slugs = remote_slugs.difference(local_slugs)
added = sync_missing_games(missing_slugs, remote_library)
updated = sync_game_details(remote_library)
return (added, updated)
return added, updated

View file

@ -6,6 +6,44 @@ from lutris import runners
# from lutris.util.log import logger
from lutris.util import display, system
DISPLAYS = None
def get_displays():
global DISPLAYS
if not DISPLAYS:
DISPLAYS = display.get_output_names()
return DISPLAYS
def get_resolution_choices():
resolutions = display.get_resolutions()
resolution_choices = list(zip(resolutions, resolutions))
resolution_choices.insert(0, ("Keep current", 'off'))
return resolution_choices
def get_output_choices():
displays = get_displays()
output_choices = list(zip(displays, displays))
output_choices.insert(0, ("Off", 'off'))
return output_choices
def get_output_list():
choices = [
('Off', 'off'),
]
displays = get_displays()
for index, _ in enumerate(displays):
# Display name can't be used because they might not be in the right order
# Using DISPLAYS to get the number of connected monitors
choices.append(("Monitor {}".format(index + 1), str(index)))
return choices
def get_dri_prime():
return len(display.get_providers()) > 1
def get_optirun_choices():
choices = [('Off', 'off')]
@ -16,7 +54,7 @@ def get_optirun_choices():
return choices
system_options = [
system_options = [ # pylint: disable=invalid-name
{
'option': 'game_path',
'type': 'directory_chooser',
@ -64,6 +102,14 @@ system_options = [
"performance. primusrun normally has better performance, but"
"optirun/virtualgl works better for more games.")
},
{
'option': 'gamemode',
'type': 'bool',
'default': bool(system.GAMEMODE_PATH),
'condition': bool(system.GAMEMODE_PATH),
'label': 'Enable Feral gamemode',
'help': 'Request a set of optimisations be temporarily applied to the host OS'
},
{
'option': 'dri_prime',
'type': 'bool',
@ -139,6 +185,27 @@ system_options = [
'help': ("Command line instructions to add in front of the game's "
"execution command.")
},
{
'option': 'ondemand_command',
'type': 'file',
'label': 'On-demand command',
'advanced': True,
'help': ("Script to execute from the game's contextual menu")
},
{
'option': 'prelaunch_command',
'type': 'file',
'label': 'Pre-launch command',
'advanced': True,
'help': "Script to execute before the game starts"
},
{
'option': 'postexit_command',
'type': 'file',
'label': 'Post-exit command',
'advanced': True,
'help': "Script to execute when the game exits"
},
{
'option': 'include_processes',
'type': 'string',
@ -265,7 +332,8 @@ system_options = [
'choices': (
('Off', 'off'),
('8BPP (256 colors)', '8bpp'),
('16BPP (65536 colors)', '16bpp')
('16BPP (65536 colors)', '16bpp'),
('24BPP (16M colors)', '24bpp'),
),
'default': 'off',
'advanced': True,

View file

@ -12,8 +12,10 @@ import contextlib
from collections import defaultdict
from itertools import chain
from gi.repository import GLib
from textwrap import dedent
from gi.repository import GLib
from lutris import settings
from lutris import runtime
@ -23,7 +25,7 @@ from lutris.util import system
HEARTBEAT_DELAY = 2000 # Number of milliseconds between each heartbeat
WARMUP_TIME = 5 * 60
MAX_CYCLES_WITHOUT_CHILDREN = 20
MAX_CYCLES_WITHOUT_CHILDREN = 5
# List of process names that are ignored by the process monitoring
EXCLUDED_PROCESSES = [
'lutris', 'python', 'python3',
@ -38,15 +40,19 @@ class LutrisThread(threading.Thread):
"""Run the game in a separate thread."""
debug_output = True
def __init__(self, command, runner=None, env={}, rootpid=None, term=None,
watch=True, cwd=None, include_processes=[], exclude_processes=[], log_buffer=None):
def __init__(self, command, runner=None, env=None, rootpid=None, term=None,
watch=True, cwd=None, include_processes=None, exclude_processes=None, log_buffer=None):
"""Thread init"""
threading.Thread.__init__(self)
self.ready_state = True
self.env = env
if env is None:
self.env = {}
else:
self.env = env
self.original_env = {}
self.command = command
self.runner = runner
self.stop_func = lambda: True
self.game_process = None
self.return_code = None
self.rootpid = rootpid or os.getpid()
@ -60,13 +66,17 @@ class LutrisThread(threading.Thread):
self.monitoring_started = False
self.daemon = True
self.error = None
if isinstance(include_processes, str):
if include_processes is None:
include_processes = []
elif isinstance(include_processes, str):
include_processes = shlex.split(include_processes)
if isinstance(exclude_processes, str):
if exclude_processes is None:
exclude_processes = []
elif isinstance(exclude_processes, str):
exclude_processes = shlex.split(exclude_processes)
# process names from /proc only contain 15 characters
self.include_processes = [x[0:15] for x in include_processes]
self.exclude_processes = [x[0:15] for x in (EXCLUDED_PROCESSES + exclude_processes)]
self.exclude_processes = [x[0:15] for x in EXCLUDED_PROCESSES + exclude_processes]
self.log_buffer = log_buffer
self.stdout_monitor = None
@ -78,8 +88,8 @@ class LutrisThread(threading.Thread):
self.cwd = self.set_cwd(cwd)
self.env_string = ''
for (k, v) in self.env.items():
self.env_string += '%s="%s" ' % (k, v)
for key, value in self.env.items():
self.env_string += '%s="%s" ' % (key, value)
self.command_string = ' '.join(
['"%s"' % token for token in self.command]
@ -115,8 +125,8 @@ class LutrisThread(threading.Thread):
def run(self):
"""Run the thread."""
logger.debug("Command env: " + self.env_string)
logger.debug("Running command: " + self.command_string)
logger.debug("Command env: %s", self.env_string)
logger.debug("Running command: %s", self.command_string)
if self.terminal and system.find_executable(self.terminal):
self.game_process = self.run_in_terminal()
@ -184,9 +194,9 @@ class LutrisThread(threading.Thread):
os.makedirs(self.cwd)
if self.watch:
pipe=subprocess.PIPE
pipe = subprocess.PIPE
else:
pipe=None
pipe = None
return subprocess.Popen(command, bufsize=1,
stdout=pipe, stderr=subprocess.STDOUT,
@ -232,6 +242,13 @@ class LutrisThread(threading.Thread):
self.original_env = {}
def stop(self, killall=False):
"""Stops the current game process and cleans up the thread"""
# Remove logger early to avoid issues with zombie processes
# (unconfirmed)
if self.stdout_monitor:
logger.debug("Detaching logger")
GLib.source_remove(self.stdout_monitor)
for thread in self.attached_threads:
logger.debug("Stopping thread %s", thread)
thread.stop()
@ -290,8 +307,7 @@ class LutrisThread(threading.Thread):
processes['external'].append(str(child))
continue
if (child.name and child.name in self.exclude_processes and
child.name not in self.include_processes):
if child.name and child.name in self.exclude_processes and child.name not in self.include_processes:
processes['excluded'].append(str(child))
continue
num_watched_children += 1
@ -305,10 +321,17 @@ class LutrisThread(threading.Thread):
return processes, num_children, num_watched_children, terminated_children
def watch_children(self):
"""Poke at the running process(es)."""
if not self.game_process or not self.is_running:
"""Poke at the running process(es).
Return:
bool: True to keep monitoring, False to stop (Used by GLib.timeout_add)
"""
if not self.game_process:
logger.error('No game process available')
return False
if not self.is_running:
logger.error('Game is not running')
return False
if not self.ready_state:
# Don't monitor processes until the thread is in a ready state
@ -323,7 +346,7 @@ class LutrisThread(threading.Thread):
for key in processes:
if processes[key] != self.monitored_processes[key]:
self.monitored_processes[key] = processes[key]
logger.debug("Processes {}: {}".format(key, ', '.join(processes[key]) or 'none'))
logger.debug("Processes %s: %s", key, ', '.join(processes[key]) or 'none')
if self.runner and hasattr(self.runner, 'watch_game_process'):
if not self.runner.watch_game_process():
@ -342,18 +365,10 @@ class LutrisThread(threading.Thread):
logger.warning("Thread aborting now")
else:
self.cycles_without_children = 0
max_cycles_reached = (self.cycles_without_children >=
MAX_CYCLES_WITHOUT_CHILDREN)
if num_children == 0 or max_cycles_reached:
if self.cycles_without_children >= MAX_CYCLES_WITHOUT_CHILDREN:
self.is_running = False
# Remove logger early to avoid issues with zombie processes
# (unconfirmed)
if self.stdout_monitor:
logger.debug("Detaching logger")
GLib.source_remove(self.stdout_monitor)
resume_stop = self.stop()
if not resume_stop:
logger.info("Full shutdown prevented")
@ -371,22 +386,13 @@ class LutrisThread(threading.Thread):
return False
if terminated_children and terminated_children == num_watched_children:
logger.debug("Waiting for processes to exit")
try:
# Really not sure if that's necessary...
self.game_process.wait(2)
except subprocess.TimeoutExpired:
logger.warning("Processes are still running")
return True
if self.stdout_monitor:
logger.debug("Removing stdout monitor")
GLib.source_remove(self.stdout_monitor)
logger.debug("Thread is no longer running")
self.is_running = False
self.restore_environment()
return False
return True
def exec_in_thread(command):
"""Execute arbitrary command in a Lutris thread

View file

@ -1,9 +1,12 @@
"""Whatever it is we want to do with audio module"""
import subprocess
import time
from lutris.util.log import logger
def reset_pulse():
"""Reset pulseaudio."""
pulse_reset = "pulseaudio --kill && sleep 1 && pulseaudio --start"
subprocess.Popen(pulse_reset, shell=True)
subprocess.Popen(["pulseaudio", "--kill"])
time.sleep(1)
subprocess.Popen(["pulseaudio", "--start"])
logger.debug("PulseAudio restarted")

View file

@ -1,6 +1,7 @@
import os
import sys
from lutris import settings
from lutris.util import system
def get():
@ -10,7 +11,7 @@ def get():
data_path = '/usr/local/share/lutris'
elif launch_path.startswith("/usr"):
data_path = '/usr/share/lutris'
elif os.path.exists(os.path.normpath(os.path.join(sys.path[0], 'share'))):
elif system.path_exists(os.path.normpath(os.path.join(sys.path[0], 'share'))):
data_path = os.path.normpath(os.path.join(sys.path[0], 'share/lutris'))
else:
import lutris
@ -18,7 +19,7 @@ def get():
data_path = os.path.join(
os.path.dirname(os.path.dirname(lutris_module)), 'share/lutris'
)
if not os.path.exists(data_path):
if not system.path_exists(data_path):
raise IOError("data_path can't be found at : %s" % data_path)
return data_path

View file

@ -1,3 +1,5 @@
"""Module to deal with various aspects of displays"""
import os
import re
import subprocess
import time
@ -16,14 +18,17 @@ XRANDR_CACHE = None
XRANDR_CACHE_SET_AT = None
XGAMMA_FOUND = None
def cached(function):
def cached(func):
"""Something that does not belong here"""
def wrapper():
global XRANDR_CACHE
global XRANDR_CACHE_SET_AT
"""What does it feel being WRONG"""
global XRANDR_CACHE # Fucked up shit
global XRANDR_CACHE_SET_AT # Moar fucked up globals
if XRANDR_CACHE and time.time() - XRANDR_CACHE_SET_AT < 60:
return XRANDR_CACHE
XRANDR_CACHE = function()
XRANDR_CACHE = func()
XRANDR_CACHE_SET_AT = time.time()
return XRANDR_CACHE
return wrapper
@ -43,10 +48,9 @@ def get_outputs():
"""Return list of namedtuples containing output 'name', 'geometry', 'rotation' and wether it is the 'primary' display."""
outputs = []
vid_modes = get_vidmodes()
position=None
rotate=None
primary=None
name=None
display = None
position = None
rotate = None
if not vid_modes:
logger.error("xrandr didn't return anything")
return []
@ -68,16 +72,16 @@ def get_outputs():
if geom.startswith('('): # Screen turned off, no geometry
continue
if rotate.startswith('('): # Screen not rotated, no need to include
rotate = 'normal'
geom_parts = geom.split('+')
position=geom_parts[1] + "x" + geom_parts[2]
name=parts[0]
rotate = "normal"
geo_split = geom.split('+')
position = geo_split[1] + "x" + geo_split[2]
display = parts[0]
elif '*' in line:
mode=parts[0]
mode = parts[0]
for number in parts:
if '*' in number:
hertz=number[:5]
outputs.append(Output(name=name, mode=mode, position=position, rotation=rotate, primary=primary, rate=hertz))
refresh_rate = number[:5]
outputs.append((display, mode, position, rotate, refresh_rate))
return outputs
def get_output_names():
@ -144,23 +148,23 @@ def change_resolution(resolution):
subprocess.Popen(["xrandr", "-s", resolution])
else:
for display in resolution:
logger.debug("Switching to %s on %s", display.mode, display.name)
display_name = display[0]
display_mode = display[1]
logger.debug("Switching to %s on %s", display_mode, display_name)
position = display[2]
refresh_rate = display[4]
if (
display.rotation is not None and
display.rotation in ('normal', 'left', 'right', 'inverted')
):
rotation = display.rotation
if len(display) > 2 and display[3] in ('normal', 'left', 'right', 'inverted'):
rotation = display[3]
else:
rotation = "normal"
logger.info("Switching resolution of %s to %s", display.name, display.mode)
subprocess.Popen([
"xrandr",
"--output", display.name,
"--mode", display.mode,
"--pos", display.position,
"--output", display_name,
"--mode", display_mode,
"--pos", position,
"--rotate", rotation,
"--rate", display.rate
"--rate", refresh_rate
]).communicate()
@ -209,25 +213,6 @@ def get_graphics_adapaters():
]
def get_providers():
"""Return the list of available graphic cards"""
pattern = "name:"
providers = []
version = get_xrandr_version()
if version["major"] == 1 and version["minor"] >= 4:
logger.debug("Retrieving providers from XrandR")
xrandr_output = subprocess.Popen(["xrandr", "--listproviders"],
stdout=subprocess.PIPE).communicate()[0].decode()
for line in xrandr_output.split("\n"):
if line.find("Provider ") != 0:
continue
position = line.find(pattern) + len(pattern)
providers.append(line[position:].strip())
return providers
class LegacyDisplayManager:
@staticmethod
def get_resolutions():
@ -308,3 +293,33 @@ def get_output_list():
# Using DISPLAYS to get the number of connected monitors
choices.append((output, str(index)))
return choices
def get_providers():
"""Return the list of available graphic cards"""
providers = []
lspci_cmd = system.find_executable('lspci')
if not lspci_cmd:
logger.warning("lspci is not installed, unable to list graphics providers")
return providers
providers_cmd = subprocess.Popen([lspci_cmd], stdout=subprocess.PIPE).communicate()[0].decode()
for provider in providers_cmd.strip().split("\n"):
if "VGA" in provider:
providers.append(provider)
return providers
def get_compositor_commands():
"""Nominated for the worst function in lutris"""
start_compositor = None
stop_compositor = None
desktop_session = os.environ.get('DESKTOP_SESSION')
if desktop_session == "plasma":
stop_compositor = "qdbus org.kde.KWin /Compositor org.kde.kwin.Compositing.suspend"
start_compositor = "qdbus org.kde.KWin /Compositor org.kde.kwin.Compositing.resume"
elif desktop_session == "mate" and system.execute("gsettings get org.mate.Marco.general compositing-manager", shell=True) == 'true':
stop_compositor = "gsettings set org.mate.Marco.general compositing-manager false"
start_compositor = "gsettings set org.mate.Marco.general compositing-manager true"
elif desktop_session == "xfce" and system.execute("xfconf-query --channel=xfwm4 --property=/general/use_compositing", shell=True) == 'true':
stop_compositor = "xfconf-query --channel=xfwm4 --property=/general/use_compositing --set=false"
start_compositor = "xfconf-query --channel=xfwm4 --property=/general/use_compositing --set=true"
return start_compositor, stop_compositor

View file

@ -6,7 +6,7 @@ from lutris.util import http, jobs
from lutris.util.log import logger
class Downloader():
class Downloader:
"""Non-blocking downloader.
Do start() then check_progress() at regular intervals.
@ -47,14 +47,13 @@ class Downloader():
def start(self):
"""Start download job."""
logger.debug("Starting download of:\n " + self.url)
logger.debug("Starting download of:\n %s", self.url)
self.state = self.DOWNLOADING
self.last_check_time = time.time()
if self.overwrite and os.path.isfile(self.dest):
os.remove(self.dest)
self.file_pointer = open(self.dest, 'wb')
self.thread = jobs.AsyncCall(self.async_download, self.on_done,
self.url, self.queue)
self.thread = jobs.AsyncCall(self.async_download, self.on_done, self.url, self.queue)
self.stop_request = self.thread.stop_request
def check_progress(self):
@ -90,11 +89,11 @@ class Downloader():
self.file_pointer.close()
return
logger.debug("Download finished")
logger.debug("Finished downloading %s", self.url)
while self.queue.qsize():
self.check_progress()
if not self.downloaded_size:
logger.debug("Downloaded file is empty")
logger.warning("Downloaded file is empty")
if not self.full_size:
self.progress_fraction = 1.0
@ -180,7 +179,7 @@ class Downloader():
average_time_left = (
(self.full_size - self.downloaded_size) / self.average_speed
)
m, s = divmod(average_time_left, 60)
h, m = divmod(m, 60)
minutes, seconds = divmod(average_time_left, 60)
hours, minutes = divmod(minutes, 60)
self.time_left_check_time = time.time()
return '%d:%02d:%02d' % (h, m, s)
return '%d:%02d:%02d' % (hours, minutes, seconds)

View file

@ -1,22 +1,64 @@
"""DXVK helper module"""
import os
import json
import time
import shutil
import urllib.request
from lutris.settings import RUNTIME_DIR
from lutris.util.log import logger
from lutris.util.extract import extract_archive
from lutris.util.downloader import Downloader
from lutris.util import system
DXVK_LATEST = "0.60"
DXVK_PAST_RELEASES = ["0.54", "0.53", "0.52", "0.51", "0.50", "0.42", "0.31", "0.21"]
CACHE_MAX_AGE = 86400 # Re-download DXVK versions every day
DXVK_TAGS_URL = "https://api.github.com/repos/doitsujin/dxvk/tags"
DXVK_VERSIONS = [
"0.90", "0.81", "0.80",
"0.72", "0.71", "0.65",
"0.64", "0.63", "0.62",
"0.54", "0.53", "0.52",
"0.42", "0.31", "0.21"
]
DXVK_LATEST, DXVK_PAST_RELEASES = DXVK_VERSIONS[0], DXVK_VERSIONS[1:]
def get_dxvk_versions():
"""Get DXVK versions from GitHub"""
logger.info("Updating DXVK versions")
dxvk_path = os.path.join(RUNTIME_DIR, 'dxvk')
if not os.path.isdir(dxvk_path):
os.mkdir(dxvk_path)
versions_path = os.path.join(dxvk_path, 'dxvk_versions.json')
urllib.request.urlretrieve(DXVK_TAGS_URL, versions_path)
with open(versions_path, "r") as dxvk_tags:
dxvk_json = json.load(dxvk_tags)
dxvk_versions = [x['name'].replace('v', '') for x in dxvk_json]
return dxvk_versions
def init_dxvk_versions():
global DXVK_VERSIONS
global DXVK_LATEST
global DXVK_PAST_RELEASES
try:
DXVK_VERSIONS = get_dxvk_versions()
except Exception as ex: # pylint: disable= broad-except
logger.error(ex)
DXVK_LATEST, DXVK_PAST_RELEASES = DXVK_VERSIONS[0], DXVK_VERSIONS[1:]
class UnavailableDXVKVersion(RuntimeError):
"""Exception raised when a version of DXVK is not found"""
class DXVKManager:
"""Utility class to install DXVK dlls to a Wine prefix"""
base_url = "https://github.com/doitsujin/dxvk/releases/download/v{}/dxvk-{}.tar.gz"
base_dir = os.path.join(RUNTIME_DIR, 'dxvk')
dxvk_dlls = ('dxgi', 'd3d11')
dxvk_dlls = ('dxgi', 'd3d11', 'd3d10core', 'd3d10_1', 'd3d10')
latest_version = DXVK_LATEST
def __init__(self, prefix, arch='win64', version=None):
@ -42,25 +84,23 @@ class DXVKManager:
def is_dxvk_dll(dll_path):
"""Check if a given DLL path is provided by DXVK
Very basic check to see if a dll exists and is over 1MB. If this is the
Very basic check to see if a dll exists and is over 256K. If this is the
case, then consider the DLL to be from DXVK
"""
if os.path.exists(dll_path):
if system.path_exists(dll_path, check_symlinks=True):
dll_stats = os.stat(dll_path)
dll_size = dll_stats.st_size
else:
dll_size = 0
return dll_size > 1024 * 1024
return dll_size > 1024 * 256
def is_available(self):
"""Return whether DXVK is cached locally"""
return os.path.exists(self.dxvk_path)
return system.path_exists(self.dxvk_path)
def download(self):
"""Download DXVK to the local cache"""
# There's a glitch in some of the archive's names
fixed_version = 'v%s' % self.version if self.version in ['0.40'] else self.version
dxvk_url = self.base_url.format(self.version, fixed_version)
dxvk_url = self.base_url.format(self.version, self.version)
if self.is_available():
logger.warning("DXVK already available at %s", self.dxvk_path)
@ -68,15 +108,16 @@ class DXVKManager:
downloader = Downloader(dxvk_url, dxvk_archive_path)
downloader.start()
while downloader.check_progress() < 1:
time.sleep(1)
if not os.path.exists(dxvk_archive_path):
time.sleep(0.3)
if not system.path_exists(dxvk_archive_path):
logger.error("DXVK %s not downloaded")
return
if os.stat(dxvk_archive_path).st_size:
extract_archive(dxvk_archive_path, self.dxvk_path, merge_single=True)
os.remove(dxvk_archive_path)
else:
logger.error("%s is an empty file", self.dxvk_path)
os.remove(dxvk_archive_path)
os.remove(dxvk_archive_path)
raise UnavailableDXVKVersion("Failed to download DXVK %s" % self.version)
def enable_dxvk_dll(self, system_dir, dxvk_arch, dll):
"""Copies DXVK dlls to the appropriate destination"""
@ -84,14 +125,14 @@ class DXVKManager:
logger.info("Replacing %s/%s with DXVK version", system_dir, dll)
if not self.is_dxvk_dll(wine_dll_path):
# Backing up original version (may not be needed)
if os.path.exists(wine_dll_path):
if system.path_exists(wine_dll_path):
shutil.move(wine_dll_path, wine_dll_path + ".orig")
# Copying DXVK's version
dxvk_dll_path = os.path.join(self.dxvk_path, dxvk_arch, "%s.dll" % dll)
try:
shutil.copy(dxvk_dll_path, wine_dll_path)
except OSError:
logger.error("Failed to copy %s to %s", dxvk_dll_path, wine_dll_path)
if system.path_exists(dxvk_dll_path):
if system.path_exists(wine_dll_path):
os.remove(wine_dll_path)
os.symlink(dxvk_dll_path, wine_dll_path)
def disable_dxvk_dll(self, system_dir, dxvk_arch, dll):
"""Remove DXVK DLL from Wine prefix"""
@ -100,7 +141,7 @@ class DXVKManager:
logger.info("Removing DXVK dll %s/%s", system_dir, dll)
os.remove(wine_dll_path)
# Restoring original version (may not be needed)
if os.path.exists(wine_dll_path + '.orig'):
if system.path_exists(wine_dll_path + '.orig'):
shutil.move(wine_dll_path + '.orig', wine_dll_path)
def _iter_dxvk_dlls(self):
@ -121,7 +162,7 @@ class DXVKManager:
def enable(self):
"""Enable DXVK for the current prefix"""
if not os.path.exists(self.dxvk_path):
if not system.path_exists(self.dxvk_path):
logger.error("DXVK %s is not available locally", self.version)
return
for system_dir, dxvk_arch, dll in self._iter_dxvk_dlls():

View file

@ -19,11 +19,10 @@ def is_7zip_supported(path, extractor):
)
if extractor:
return extractor.lower() in supported_extractors
else:
_base, ext = os.path.splitext(path)
if ext:
ext = ext.lstrip('.').lower()
return ext in supported_extractors
_base, ext = os.path.splitext(path)
if ext:
ext = ext.lstrip('.').lower()
return ext in supported_extractors
def extract_archive(path, to_directory='.', merge_single=True, extractor=None):
@ -44,7 +43,7 @@ def extract_archive(path, to_directory='.', merge_single=True, extractor=None):
path.endswith('.tbz') or
extractor == 'bz2'):
opener, mode = tarfile.open, 'r:bz2'
elif(is_7zip_supported(path, extractor)):
elif is_7zip_supported(path, extractor):
opener = '7zip'
else:
raise RuntimeError(
@ -67,11 +66,12 @@ def extract_archive(path, to_directory='.', merge_single=True, extractor=None):
shutil.move(temp_path, to_directory)
os.removedirs(temp_dir)
else:
for f in os.listdir(temp_path):
logger.debug("Moving element %s of archive", f)
source_path = os.path.join(temp_path, f)
destination_path = os.path.join(to_directory, f)
if os.path.exists(destination_path):
for archive_file in os.listdir(temp_path):
source_path = os.path.join(temp_path, archive_file)
destination_path = os.path.join(to_directory, archive_file)
logger.debug("Moving extracted files from %s to %s", source_path, destination_path)
if system.path_exists(destination_path):
logger.warning("Overwrite existing path %s", destination_path)
if os.path.isfile(destination_path):
os.remove(destination_path)
@ -80,9 +80,9 @@ def extract_archive(path, to_directory='.', merge_single=True, extractor=None):
system.merge_folders(source_path, destination_path)
else:
shutil.move(source_path, destination_path)
shutil.rmtree(temp_dir)
system.remove_folder(temp_dir)
logger.debug("Finished extracting %s", path)
return (path, to_directory)
return path, to_directory
def _do_extract(archive, dest, opener, mode=None, extractor=None):

View file

@ -1,9 +1,9 @@
import os
from lutris.util import datapath
from lutris.util import datapath, system
from lutris.util.log import logger
class ControllerMapping():
class ControllerMapping:
valid_keys = [
"platform", "leftx", "lefty", "rightx", "righty", "a", "b", "back", "dpdown",
"dpleft", "dpright", "dpup", "guide", "leftshoulder", "leftstick",
@ -32,11 +32,11 @@ class ControllerMapping():
self.keys[xinput_key] = sdl_key
class GameControllerDB():
class GameControllerDB:
db_path = os.path.join(datapath.get(), 'controllers/gamecontrollerdb.txt')
def __init__(self):
if not os.path.exists(self.db_path):
if not system.path_exists(self.db_path):
raise OSError("Path to gamecontrollerdb.txt not provided or invalid")
self.controllers = {}
self.parsedb()

View file

@ -10,9 +10,9 @@ from lutris.settings import SITE_URL, VERSION, PROJECT
from lutris.util.log import logger
class Request(object):
class Request:
def __init__(self, url, timeout=30, stop_request=None,
thread_queue=None, headers={}, cookies=None):
thread_queue=None, headers=None, cookies=None):
if not url:
raise ValueError('An URL is required!')
@ -34,6 +34,8 @@ class Request(object):
'User-Agent': self.user_agent
}
self.response_headers = None
if headers is None:
headers = {}
if not isinstance(headers, dict):
raise TypeError('HTTP headers needs to be a dict ({})'.format(headers))
self.headers.update(headers)
@ -50,21 +52,26 @@ class Request(object):
platform.machine())
def get(self, data=None):
logger.debug("GET %s", self.url)
req = urllib.request.Request(url=self.url, data=data, headers=self.headers)
try:
if self.opener:
request = self.opener.open(req, timeout=self.timeout)
else:
request = urllib.request.urlopen(req, timeout=self.timeout)
except (urllib.error.HTTPError, CertificateError) as e:
logger.error("Unavailable url (%s): %s", self.url, e)
except (socket.timeout, urllib.error.URLError) as e:
logger.error("Unable to connect to server (%s): %s", self.url, e)
except (urllib.error.HTTPError, CertificateError) as error:
logger.error("Unavailable url (%s): %s", self.url, error)
except (socket.timeout, urllib.error.URLError) as error:
logger.error("Unable to connect to server (%s): %s", self.url, error)
else:
# Response code is available with getcode but should 200 if there
# is no exception
# logger.debug("Got response code: %s", request.getcode())
try:
total_size = request.info().get('Content-Length').strip()
total_size = int(total_size)
except AttributeError:
logger.warning("Failed to read response's content length")
total_size = 0
self.response_headers = request.getheaders()
@ -76,7 +83,7 @@ class Request(object):
return self
try:
chunk = request.read(self.buffer_size)
except socket.timeout as e:
except socket.timeout:
logger.error("Request timed out")
self.content = ''
return self
@ -112,8 +119,10 @@ class Request(object):
raise ValueError("Invalid response ({}:{}): {}".format(
self.url, self.status_code, self.text[:80]
))
return None
@property
def text(self):
if self.content:
return self.content.decode()
return None

View file

@ -1,4 +1,4 @@
import os
from lutris.util import system
def check_joysticks():
@ -7,7 +7,7 @@ def check_joysticks():
joysticks = []
for device_number in range(0, 8):
device_name = "/dev/input/js%d" % device_number
if os.path.exists(device_name):
if system.path_exists(device_name):
number_joysticks = number_joysticks + 1
joysticks.append(device_name)
return joysticks

View file

@ -9,7 +9,7 @@ from lutris.util.log import logger
class AsyncCall(threading.Thread):
debug_traceback = False
def __init__(self, function, callback=None, *args, **kwargs):
def __init__(self, func, callback=None, *args, **kwargs):
"""Execute `function` in a new thread then schedule `callback` for
execution in the main loop.
"""
@ -18,7 +18,7 @@ class AsyncCall(threading.Thread):
super(AsyncCall, self).__init__(target=self.target, args=args,
kwargs=kwargs)
self.function = function
self.function = func
self.callback = callback if callback else lambda r, e: None
self.daemon = kwargs.pop('daemon', True)

View file

@ -1,4 +1,4 @@
import os
from lutris.util import system
class RetroConfig:
@ -11,7 +11,7 @@ class RetroConfig:
def __init__(self, config_path):
if not config_path:
raise ValueError("Config path is mandatory")
if not os.path.exists(config_path):
if not system.path_exists(config_path):
raise OSError("Specified config file {} does not exist".format(config_path))
self.config_path = config_path
self.config = []
@ -42,12 +42,12 @@ class RetroConfig:
return value
def __getitem__(self, key):
for (k, value) in self.config:
for k, value in self.config:
if key == k:
return self.deserialize_value(value)
def __setitem__(self, key, value):
for index, (k, v) in enumerate(self.config):
for index, k, _ in enumerate(self.config):
if key == k:
self.config[index] = (key, self.serialize_value(value))
return

View file

@ -1,13 +1,13 @@
import os
from lutris.util.log import logger
from lutris.util.system import kill_pid
from lutris.util.system import kill_pid, path_exists
class InvalidPid(Exception):
pass
class Process(object):
class Process:
def __init__(self, pid, parent=None):
try:
self.pid = int(pid)
@ -25,14 +25,14 @@ class Process(object):
def get_stat(self, parsed=True):
stat_filename = "/proc/{}/stat".format(self.pid)
if not os.path.exists(stat_filename):
return
if not path_exists(stat_filename):
return None
with open(stat_filename) as stat_file:
try:
_stat = stat_file.readline()
except (ProcessLookupError, FileNotFoundError):
logger.warning('Unable to read stat for process %s', self.pid)
return
return None
if parsed:
return _stat[_stat.rfind(")") + 1:].split()
return _stat
@ -70,6 +70,7 @@ class Process(object):
_stat = self.get_stat(parsed=False)
if _stat:
return _stat[_stat.find("(") + 1:_stat.rfind(")")]
return None
@property
def state(self):
@ -81,6 +82,7 @@ class Process(object):
_stat = self.get_stat()
if _stat:
return _stat[0]
return None
@property
def ppid(self):
@ -88,6 +90,7 @@ class Process(object):
_stat = self.get_stat()
if _stat:
return _stat[1]
return None
@property
def pgrp(self):
@ -95,6 +98,7 @@ class Process(object):
_stat = self.get_stat()
if _stat:
return _stat[2]
return None
@property
def cmdline(self):

Some files were not shown because too many files have changed in this diff Show more