mirror of
https://github.com/lutris/lutris
synced 2024-10-04 14:59:37 +00:00
Merge master in next (Warning: it's broken)
This commit is contained in:
commit
acbd4681e2
82
AUTHORS
82
AUTHORS
|
@ -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
118
CONTRIBUTING.md
Normal 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
|
163
README.rst
163
README.rst
|
@ -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
79
debian/changelog
vendored
|
@ -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
6
debian/control
vendored
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
12
lutris.spec
12
lutris.spec
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
128
lutris/game.py
128
lutris/game.py
|
@ -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('&', '&')
|
||||
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('&', '&')
|
||||
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("")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
257
lutris/gui/flowbox.py
Normal 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")
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
68
lutris/gui/lutristray.py
Normal 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])
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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()]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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}
|
||||
|
|
0
lutris/runners/commands/__init__.py
Normal file
0
lutris/runners/commands/__init__.py
Normal file
47
lutris/runners/commands/dosbox.py
Normal file
47
lutris/runners/commands/dosbox.py
Normal 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))
|
297
lutris/runners/commands/wine.py
Normal file
297
lutris/runners/commands/wine.py
Normal 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')
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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 ''
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from lutris.runners.runner import Runner
|
||||
from os.path import expanduser
|
||||
from lutris.runners.runner import Runner
|
||||
|
||||
|
||||
class higan(Runner):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
23
lutris/runners/melonds.py
Normal 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]}
|
|
@ -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()]
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -12,7 +12,7 @@ class rpcs3(Runner):
|
|||
"option": "main_file",
|
||||
"type": "file",
|
||||
"default_path": "game_path",
|
||||
"label": "Game folder"
|
||||
"label": "Path to EBOOT.BIN"
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ''
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue