Sync Python 3 branch

This commit is contained in:
Mathieu Comandon 2016-08-04 01:50:54 -07:00
commit cfef4b5024
57 changed files with 3191 additions and 3476 deletions

View file

@ -10,3 +10,5 @@ Contributors:
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>

View file

@ -21,9 +21,10 @@ the following command as root::
$ python setup.py install
Warning: there is no way to cleanly uninstall programs installed with setup.py
other than manuall deleting the created files. Prefer installing Lutris
through distribution packages or run it directly from the source directory::
**Warning:** there is no way to cleanly uninstall programs installed with
setup.py other than manuall deleting the created files. Prefer installing
Lutris through distribution packages or run it directly from the source
directory::
cd /path/to/lutris/source
./bin/lutris

View file

@ -1,4 +1,4 @@
VERSION="0.3.7.3"
VERSION="0.3.8"
cover:
rm tests/fixtures/pga.db -f
@ -35,4 +35,4 @@ clean:
build-all: deb
upload:
scp build/lutris_${VERSION}.tar.gz lutris.net:/srv/releases/
scp build/lutris_${VERSION}.tar.xz lutris.net:/srv/releases/

View file

@ -39,17 +39,16 @@ We currently support the following runners:
* Mupen64 Plus
* Dolphin
* PCSXR
* PPSSPP
* PCSX2
* Osmose
* GenS
* Reicast
* Frotz
* Jzintv
* O2em
* ZDoom
Runners that will be added in future versions of Lutris:
* PPSSPP
* PCSX2
Installer scripts
=================
@ -162,4 +161,3 @@ You can always reach us on:
* Twitter: https://twitter.com/LutrisGaming
* Google+: https://plus.google.com/+LutrisNet
* Email: contact@lutris.net

View file

@ -40,7 +40,6 @@ if LAUNCH_PATH != "/usr/bin":
SOURCE_PATH = normpath(os.path.join(LAUNCH_PATH, '..'))
sys.path.insert(0, SOURCE_PATH)
from lutris.gui import dialogs
from lutris.migrations import migrate
from lutris import pga, runtime
@ -50,6 +49,7 @@ from lutris.game import Game
from lutris.gui.installgamedialog import InstallerDialog
from lutris.settings import VERSION
from lutris.util import service
from lutris.util.steam import get_steamapps_paths
# Support for command line options.
parser = optparse.OptionParser(version="%prog " + VERSION)
@ -63,8 +63,10 @@ parser.add_option("-l", "--list-games", action="store_true", dest="list_games",
help="List games in database")
parser.add_option("-o", "--installed", action="store_true", dest="list_installed",
help="Only list installed games")
parser.add_option("-s", "--list-steam", action="store_true",
parser.add_option("--list-steam", action="store_true",
help="List Steam (Windows) games")
parser.add_option("--list-steam-folders", action="store_true",
help="List all known Steam library folders")
parser.add_option("-j", "--json", action="store_true", dest="json",
help="Display the list of games in JSON format")
parser.add_option("--reinstall", action="store_true", help="Reinstall game")
@ -102,9 +104,19 @@ if options.list_games:
).encode('utf-8'))
exit()
if options.list_steam:
# FIXME: this returns a list of appids
# FIXME: this only works for Wine Steam games
from lutris.runners import winesteam
steam_runner = winesteam.winesteam()
print(steam_runner.get_appid_list())
for appid in steam_runner.get_appid_list():
print appid
exit()
if options.list_steam_folders:
steamapps_paths = get_steamapps_paths()
for path in steamapps_paths['linux']:
print(path)
for path in steamapps_paths['windows']:
print(path)
exit()
@ -125,15 +137,12 @@ if type(lutris) is dbus.Interface:
try:
lutris.is_running()
except dbus.exceptions.DBusException as e:
logger.debug(e)
q = dialogs.QuestionDialog(
{'title': "Error",
'question': ("Lutris is already running \n"
"but seems unresponsive,\n"
"do you want to restart it?")}
)
if q.result == Gtk.ResponseType.NO:
exit()
logger.error(e)
# FIXME This whole thing below is utterly broken and I've never seen
# the expected behavior happen. Now, if only I knew how to reproduce
# this state, maybe I might be able to write a fix but I still have no
# idea what's causing this non-responsive DBus thing.
try:
# Get existing process' PID
dbus_proxy = bus.get_object('org.freedesktop.DBus',
@ -142,8 +151,10 @@ if type(lutris) is dbus.Interface:
pid = dbus_interface.GetConnectionUnixProcessID(lutris.bus_name)
os.kill(pid, signal.SIGKILL)
except (OSError, dbus.exceptions.DBusException):
logger.debug(e)
except (OSError, dbus.exceptions.DBusException) as ex:
logger.error("Lutris was non responsive, we tried, we failed and failed "
"some more. Please now try to restart Lutris")
logger.error(ex)
exit() # Give up :(
else:
time.sleep(1) # Wait for bus name to be available again

31
debian/changelog vendored
View file

@ -1,3 +1,34 @@
lutris (0.3.8) xenial; urgency=medium
* Add option to use the dark GTK theme variant
* Add Desmume runner
* Add option to limit games to a single CPU core
* Fix button mappings on mednafen
* Improve Reicast installation
* Add controller support to Reicast
* Disable Wine crash dialogs by default
* Sync steam games without depending on the remote library
* Use inotify to detect changes in Steam folders
* Allow to browse for mounted CD images during installation
-- Mathieu Comandon <strycore@gmail.com> Thu, 04 Aug 2016 00:13:38 -0700
lutris (0.3.7.5) xenial; urgency=medium
* Fix a bug where booleans in scripts would be converted to strings
* Update Debian package source format
-- Mathieu Comandon <strycore@gmail.com> Mon, 07 Mar 2016 09:57:29 -0800
lutris (0.3.7.4) xenial; urgency=medium
* Add support for Xephyr
* Detect Wine versions installed from WineHQ
* Update koku-xinput-wine to work with the build provided in the runtime
* Always install the required runner when a game is installed
-- Mathieu Comandon <strycore@gmail.com> Sun, 06 Mar 2016 14:37:09 -0800
lutris (0.3.7.3) xenial; urgency=medium
* Add PCSX2 runner

3
debian/control vendored
View file

@ -20,9 +20,12 @@ Depends: ${misc:Depends},
${python:Depends},
python-yaml,
python-dbus,
python-gi,
python-pyinotify,
gir1.2-gtk-3.0,
xdg-user-dirs,
python-xdg,
psmisc,
libc6-i386 [amd64],
lib32gcc1 [amd64]
Description: Install and play any video game easily

View file

@ -1 +1 @@
3.0 (quilt)
3.0 (native)

View file

@ -193,7 +193,7 @@ Example:
Making a file executable
------------------------
Marking the file as executable is done with the ``chmodx`` command. It is often
Marking the file as executable is done with the ``chmodx`` directive. It is often
needed for games that ship in a zip file, which does not retain file
permissions.
@ -204,9 +204,11 @@ Executing a file
Execute files with the ``execute`` directive. Use the ``file`` parameter to
reference a ``file id`` or a path, ``args`` to add command arguments,
``terminal`` (set to "true") to execute in a new terminal window.
``terminal`` (set to "true") to execute in a new terminal window, ``working_dir``
to set the directory to execute the command in (defaults to the install path).
The command is executed within the Lutris Runtime (resolving most shared
library dependencies).
library dependencies). The file is made executable if necessary, no need to run
chmodx before.
Example:

View file

@ -1,14 +1,14 @@
%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")}
Name: lutris
Version: 0.3.7.3
Version: 0.3.8
Release: 2%{?dist}
Summary: Install and play any video game easily
License: GPL-3.0+
Group: Amusements/Games/Other
URL: http://lutris.net
Source0: http://lutris.net/releases/lutris_%{version}.tar.gz
Source0: http://lutris.net/releases/lutris_%{version}.tar.xz
BuildArch: noarch
@ -18,11 +18,11 @@ BuildRequires: python-devel
%if 0%{?fedora_version}
BuildRequires: pygobject3, python3-gobject
Requires: pygobject3, PyYAML, pyxdg
Requires: pygobject3, PyYAML, pyxdg, dbus-python
%endif
%if 0%{?rhel_version} || 0%{?centos_version}
BuildRequires: pygobject3
Requires: pygobject3, PyYAML
Requires: pygobject3, PyYAML, pyxdg, dbus-python
%endif
%if 0%{?suse_version}
BuildRequires: python-gobject
@ -30,7 +30,7 @@ BuildRequires: update-desktop-files
# Needed to workaround "directories not owned by a package" issue
BuildRequires: hicolor-icon-theme
BuildRequires: polkit
Requires: python-gobject, python-gtk, python-PyYAML, python-xdg
Requires: python-gobject, python-gtk, python-PyYAML, python-xdg, dbus-1-python
%endif
%if 0%{?fedora_version} || 0%{?suse_version}
BuildRequires: fdupes

View file

@ -127,7 +127,7 @@ class LutrisConfig(object):
"""
def __init__(self, runner_slug=None, game_config_id=None, level=None):
self.game_config_id = game_config_id
self.runner_slug = runner_slug
self.runner_slug = str(runner_slug)
# Cascaded config sections (for reading)
self.game_config = {}

View file

@ -166,6 +166,9 @@ class Game(object):
system_config = self.runner.system_config
self.original_outputs = display.get_outputs()
gameplay_info = self.runner.play()
env = {}
logger.debug("Launching %s: %s" % (self.name, gameplay_info))
if 'error' in gameplay_info:
show_error_message(gameplay_info)
@ -199,10 +202,36 @@ class Game(object):
if primusrun and system.find_executable('primusrun'):
launch_arguments.insert(0, 'primusrun')
xephyr = system_config.get('xephyr') or 'off'
if xephyr != 'off':
if xephyr == '8bpp':
xephyr_depth = '8'
else:
xephyr_depth = '16'
xephyr_resolution = system_config.get('xephyr_resolution') or '640x480'
xephyr_command = ['Xephyr', ':2', '-ac', '-screen',
xephyr_resolution + 'x' + xephyr_depth, '-glamor',
'-reset', '-terminate', '-fullscreen']
xephyr_thread = LutrisThread(xephyr_command)
xephyr_thread.start()
time.sleep(3)
env['DISPLAY'] = ':2'
pulse_latency = system_config.get('pulse_latency')
if pulse_latency:
env['PULSE_LATENCY_MSEC'] = '60'
prefix_command = system_config.get("prefix_command") or ''
if prefix_command.strip():
launch_arguments.insert(0, prefix_command)
single_cpu = system_config.get('single_cpu') or False
if single_cpu:
logger.info('The game will run on a single CPU core')
launch_arguments.insert(0, '0')
launch_arguments.insert(0, '-c')
launch_arguments.insert(0, 'taskset')
terminal = system_config.get('terminal')
if terminal:
terminal = system_config.get("terminal_app",
@ -214,7 +243,6 @@ class Game(object):
self.state = self.STATE_STOPPED
return
# Env vars
env = {}
game_env = gameplay_info.get('env') or {}
env.update(game_env)
system_env = system_config.get('env') or {}
@ -290,6 +318,11 @@ class Game(object):
def beat(self):
"""Watch the game's process(es)."""
if self.game_thread.error:
dialogs.ErrorDialog("<b>Error lauching the game:</b>\n"
+ self.game_thread.error)
self.on_game_quit()
return False
self.game_log = self.game_thread.stdout
killswitch_engage = self.killswitch and \
not os.path.exists(self.killswitch)

View file

@ -77,12 +77,13 @@ class QuestionDialog(Gtk.MessageDialog):
class DirectoryDialog(Gtk.FileChooserDialog):
"""Ask the user to select a directory."""
def __init__(self, message):
def __init__(self, message, parent=None):
super(DirectoryDialog, self).__init__(
title=message,
action=Gtk.FileChooserAction.SELECT_FOLDER,
buttons=('_Cancel', Gtk.ResponseType.CLOSE,
'_OK', Gtk.ResponseType.OK)
'_OK', Gtk.ResponseType.OK),
parent=parent
)
self.result = self.run()
self.folder = self.get_current_folder()

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import os
import time
from gi.repository import Gtk, Pango
@ -8,7 +9,7 @@ from lutris import pga, settings, shortcuts
from lutris.installer import interpreter
from lutris.game import Game
from lutris.gui.config_dialogs import AddGameDialog
from lutris.gui.dialogs import NoInstallerDialog
from lutris.gui.dialogs import NoInstallerDialog, DirectoryDialog
from lutris.gui.widgets import DownloadProgressBox, FileChooserEntry
from lutris.util import display, jobs
from lutris.util.log import logger
@ -82,6 +83,7 @@ class InstallerDialog(Gtk.Window):
self.continue_handler = None
self.get_scripts()
self.present()
def add_button(self, label, handler=None):
button = Gtk.Button.new_with_mnemonic(label)
@ -265,7 +267,7 @@ class InstallerDialog(Gtk.Window):
def on_install_clicked(self, button):
"""Let the interpreter take charge of the next stages."""
button.hide()
self.interpreter.iter_game_files()
self.interpreter.check_runner_install()
def ask_user_for_file(self, message):
self.clean_widgets()
@ -315,8 +317,8 @@ class InstallerDialog(Gtk.Window):
{'url': file_uri, 'dest': dest_file}, cancelable=True
)
self.download_progress.cancel_button.hide()
callback_function = callback or self.on_download_complete
self.download_progress.connect('complete', callback_function, data)
callback = callback or self.on_download_complete
self.download_progress.connect('complete', callback, data)
self.widget_box.pack_start(self.download_progress, False, False, 10)
self.download_progress.show()
self.download_progress.start()
@ -331,19 +333,43 @@ class InstallerDialog(Gtk.Window):
# "Commands" stage
# ----------------
def wait_for_user_action(self, message, callback, data=None):
"""Ask the user to do something."""
def ask_for_disc(self, message, callback, requires):
"""Ask the user to do insert a CD-ROM."""
time.sleep(0.3)
self.clean_widgets()
label = Gtk.Label(label=message)
label.set_use_markup(True)
self.widget_box.add(label)
label.show()
button = Gtk.Button(label='Ok')
button.connect('clicked', callback, data)
self.widget_box.add(button)
button.grab_focus()
button.show()
buttons_box = Gtk.Box()
buttons_box.show()
buttons_box.set_margin_top(40)
buttons_box.set_margin_bottom(40)
self.widget_box.add(buttons_box)
autodetect_button = Gtk.Button(label='Autodetect')
autodetect_button.connect('clicked', callback, requires)
autodetect_button.grab_focus()
autodetect_button.show()
buttons_box.pack_start(autodetect_button, True, True, 40)
browse_button = Gtk.Button(label='Browse…')
callback_data = {
'callback': callback,
'requires': requires
}
browse_button.connect('clicked', self.on_browse_clicked, callback_data)
browse_button.show()
buttons_box.pack_start(browse_button, True, True, 40)
def on_browse_clicked(self, widget, callback_data):
dialog = DirectoryDialog("Select the folder where the disc is mounted",
parent=self)
folder = dialog.folder
callback = callback_data['callback']
requires = callback_data['requires']
callback(widget, requires, folder)
def on_eject_clicked(self, widget, data=None):
self.interpreter.eject_wine_disc()

View file

@ -2,8 +2,9 @@
# pylint: disable=E0611
import os
import time
import subprocess
from gi.repository import Gtk, Gdk, GLib
from gi.repository import Gtk, Gdk, GLib, Gio
from lutris import api, pga, runtime, settings, shortcuts
from lutris.game import Game, get_game_list
@ -13,6 +14,7 @@ from lutris.util import display, resources
from lutris.util.log import logger
from lutris.util.jobs import AsyncCall
from lutris.util import datapath
from lutris.util import steam
from lutris.gui import dialogs
from lutris.gui.sidebar import SidebarTreeView
@ -36,10 +38,14 @@ def load_view(view, store):
return view
class LutrisWindow(object):
class LutrisWindow(Gtk.Application):
"""Handler class for main window signals."""
def __init__(self, service=None):
Gtk.Application.__init__(
self, application_id="net.lutris.main",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
)
ui_filename = os.path.join(
datapath.get(), 'ui', 'LutrisWindow.ui'
)
@ -63,6 +69,8 @@ class LutrisWindow(object):
width = int(settings.read_setting('width') or 800)
height = int(settings.read_setting('height') or 600)
self.window_size = (width, height)
window = self.builder.get_object('window')
window.resize(width, height)
view_type = self.get_view_type()
self.icon_type = self.get_icon_type(view_type)
filter_installed = \
@ -70,6 +78,12 @@ class LutrisWindow(object):
self.sidebar_visible = \
settings.read_setting('sidebar_visible') in ['true', None]
# Set theme to dark if set in the settings
dark_theme_menuitem = self.builder.get_object('dark_theme_menuitem')
use_dark_theme = settings.read_setting('dark_theme') == 'true'
dark_theme_menuitem.set_active(use_dark_theme)
self.set_dark_theme(use_dark_theme)
# Load view
logger.debug("Loading view")
self.game_store = GameStore([], self.icon_type, filter_installed)
@ -78,6 +92,7 @@ class LutrisWindow(object):
logger.debug("Connecting signals")
self.main_box = self.builder.get_object('main_box')
self.splash_box = self.builder.get_object('splash_box')
self.connect_link = self.builder.get_object('connect_link')
# View menu
installed_games_only_menuitem =\
self.builder.get_object('filter_installed')
@ -138,8 +153,8 @@ class LutrisWindow(object):
self.view.contextual_menu = self.menu
# Sidebar
sidebar_paned = self.builder.get_object('sidebar_paned')
sidebar_paned.set_position(150)
self.sidebar_paned = self.builder.get_object('sidebar_paned')
self.sidebar_paned.set_position(150)
self.sidebar_treeview = SidebarTreeView()
self.sidebar_treeview.connect('cursor-changed', self.on_sidebar_changed)
self.sidebar_viewport = self.builder.get_object('sidebar_viewport')
@ -154,10 +169,16 @@ class LutrisWindow(object):
self.builder.connect_signals(self)
self.connect_signals()
self.statusbar = self.builder.get_object("statusbar")
# XXX Hide PGA config menu item until it actually gets implemented
pga_menuitem = self.builder.get_object('pga_menuitem')
pga_menuitem.hide()
# Sync local lutris library with current Steam games before setting up
# view
steam.sync_with_lutris()
self.init_game_store()
self.update_runtime()
@ -171,8 +192,43 @@ class LutrisWindow(object):
self.sync_library()
# Timers
self.timer_ids = [GLib.timeout_add(300, self.refresh_status),
GLib.timeout_add(10000, self.on_sync_timer)]
self.timer_ids = [GLib.timeout_add(300, self.refresh_status)]
steamapps_paths = steam.get_steamapps_paths(flat=True)
self.steam_watcher = steam.SteamWatcher(steamapps_paths,
self.on_steam_game_changed)
def on_steam_game_changed(self, operation, path):
appmanifest = steam.AppManifest(path)
runner_name = appmanifest.get_runner_name()
games = pga.get_game_by_field(appmanifest.steamid, field='steamid', all=True)
if operation == 'DELETE':
for game in games:
if game['runner'] == runner_name:
steam.mark_as_uninstalled(game)
self.remove_game_from_view(game['id'])
break
elif operation in ('MODIFY', 'CREATE'):
if not appmanifest.is_installed():
return
if runner_name == 'windows':
return
game_info = None
for game in games:
if game['installed'] == 0:
game_info = game
if not game_info:
game_info = {
'name': appmanifest.name,
'slug': appmanifest.slug,
}
game_id = steam.mark_as_installed(appmanifest.steamid,
runner_name,
game_info)
self.add_game_to_view(game_id)
def set_dark_theme(self, is_dark):
gtksettings = Gtk.Settings.get_default()
gtksettings.set_property("gtk-application-prefer-dark-theme", is_dark)
def init_game_store(self):
logger.debug("Getting game list")
@ -198,7 +254,6 @@ class LutrisWindow(object):
def check_update(self):
"""Verify availability of client update."""
pass
def on_version_received(version, error):
if not version:
@ -272,33 +327,22 @@ class LutrisWindow(object):
"""Synchronize games with local stuff and server."""
def update_gui(result, error):
if result:
added, updated, installed, uninstalled = result
added, updated = result # , installed, uninstalled = result
self.switch_splash_screen()
self.game_store.fill_store(added)
GLib.idle_add(self.update_existing_games,
added, updated, installed, uninstalled, True)
GLib.idle_add(self.update_existing_games, added, updated, True)
else:
logger.error("No results returned when syncing the library")
self.set_status("Syncing library")
AsyncCall(Sync().sync_all, update_gui)
def update_existing_games(self, added, updated, installed, uninstalled,
first_run=False):
def update_existing_games(self, added, updated, first_run=False):
# , installed, uninstalled, first_run=False):
for game_id in updated.difference(added):
self.view.update_row(pga.get_game_by_field(game_id, 'id'))
for game_id in installed.difference(added):
if not self.view.get_row_by_id(game_id):
self.view.add_game(game_id)
self.view.set_installed(Game(game_id))
for game_id in uninstalled.difference(added):
self.view.set_uninstalled(game_id)
self.sidebar_treeview.update()
if first_run:
icons_sync = AsyncCall(self.sync_icons, None, stoppable=True)
self.threads_stoppers.append(icons_sync.stop_request.set)
@ -348,6 +392,13 @@ class LutrisWindow(object):
# Callbacks
# ---------
def on_dark_theme_toggled(self, widget):
use_dark_theme = widget.get_active()
setting_value = 'true' if use_dark_theme else 'false'
logger.debug("Dark theme now %s", setting_value)
settings.write_setting('dark_theme', setting_value)
self.set_dark_theme(use_dark_theme)
def on_clear_search(self, widget, icon_pos, event):
if icon_pos == Gtk.EntryIconPosition.SECONDARY:
widget.set_text('')
@ -356,6 +407,7 @@ class LutrisWindow(object):
"""Callback when a user connects to his account."""
login_dialog = dialogs.ClientLoginDialog(self.window)
login_dialog.connect('connected', self.on_connect_success)
self.connect_link.hide()
def on_connect_success(self, dialog, credentials):
if isinstance(credentials, str):
@ -368,6 +420,7 @@ class LutrisWindow(object):
def on_disconnect(self, *args):
api.disconnect()
self.toggle_connection(False)
self.connect_link.show()
def toggle_connection(self, is_connected, username=None):
disconnect_menuitem = self.builder.get_object('disconnect_menuitem')
@ -386,37 +439,34 @@ class LutrisWindow(object):
connection_label.set_text(connection_status)
def on_register_account(self, *args):
Gtk.show_uri(None, "http://lutris.net/user/register", Gdk.CURRENT_TIME)
register_url = "https://lutris.net/user/register"
try:
subprocess.check_call(["xdg-open", register_url])
except subprocess.CalledProcessError:
Gtk.show_uri(None, register_url, Gdk.CURRENT_TIME)
def on_synchronize_manually(self, *args):
"""Callback when Synchronize Library is activated."""
self.sync_library()
def on_sync_timer(self):
if (not self.running_game
or self.running_game.state == Game.STATE_STOPPED):
def update_gui(result, error):
if result:
self.update_existing_games(set(), set(), *result)
else:
logger.error('No results while syncing local Steam database')
AsyncCall(Sync().sync_local, update_gui)
return True
def on_resize(self, widget, *args):
"""WTF is this doing?"""
self.window_size = widget.get_size()
def on_destroy(self, *args):
"""Signal for window close."""
# Stop cancellable running threads
for stopper in self.threads_stoppers:
logger.debug("Stopping %s", stopper)
stopper()
self.steam_watcher.stop()
if self.running_game:
logger.info("%s is still running, stopping it", self.running_game.name)
self.running_game.stop()
if self.service:
logger.debug('Stopping service')
self.service.stop()
# Save settings
@ -650,10 +700,18 @@ class LutrisWindow(object):
def toggle_sidebar(self, _widget=None):
if self.sidebar_visible:
self.sidebar_viewport.hide()
self.sidebar_paned.remove(self.games_scrollwindow)
self.main_box.remove(self.sidebar_paned)
self.main_box.remove(self.statusbar)
self.main_box.pack_start(self.games_scrollwindow, True, True, 0)
self.main_box.pack_start(self.statusbar, False, False, 0)
settings.write_setting('sidebar_visible', 'false')
else:
self.sidebar_viewport.show()
self.main_box.remove(self.games_scrollwindow)
self.sidebar_paned.add2(self.games_scrollwindow)
self.main_box.remove(self.statusbar)
self.main_box.pack_start(self.sidebar_paned, True, True, 0)
self.main_box.pack_start(self.statusbar, False, False, 0)
settings.write_setting('sidebar_visible', 'true')
self.sidebar_visible = not self.sidebar_visible

View file

@ -1,9 +1,9 @@
from gi.repository import Gtk, GdkPixbuf
from gi.repository import Gtk, GdkPixbuf, GObject
import lutris.runners
from lutris import pga
from lutris import runners
from lutris.gui.runnerinstalldialog import RunnerInstallDialog
from lutris.gui.config_dialogs import RunnerConfigDialog
from lutris.gui.runnersdialog import RunnersDialog
from lutris.gui.widgets import get_runner_icon
SLUG = 0
@ -45,9 +45,10 @@ class SidebarTreeView(Gtk.TreeView):
self.set_fixed_height_mode(True)
self.connect('button-press-event', self.popup_contextual_menu)
GObject.add_emission_hook(RunnersDialog, "runner-installed", self.update)
self.runners = sorted(lutris.runners.__all__)
self.used_runners = pga.get_used_runners()
self.runners = sorted(runners.__all__)
self.installed_runners = [runner.name for runner in runners.get_installed()]
self.load_all_runners()
self.update()
self.expand_all()
@ -56,7 +57,7 @@ class SidebarTreeView(Gtk.TreeView):
"""Append runners to the model."""
runner_node = self.model.append(None, ['runners', None, "Runners"])
for slug in self.runners:
name = lutris.runners.import_runner(slug).human_name
name = runners.import_runner(slug).human_name
icon = get_runner_icon(slug, format='pixbuf', size=(16, 16))
self.model.append(runner_node, [slug, icon, name])
@ -73,12 +74,13 @@ class SidebarTreeView(Gtk.TreeView):
def filter_rule(self, model, iter, data):
if model[iter][0] == 'runners':
return True
return model[iter][0] in self.used_runners
return model[iter][0] in self.installed_runners
def update(self):
self.used_runners = pga.get_used_runners()
def update(self, *args):
self.used_runners = runners.get_installed()
self.model_filter.refilter()
self.expand_all()
return True
def popup_contextual_menu(self, view, event):
if event.button != 3:
@ -107,7 +109,7 @@ class ContextualMenu(Gtk.Menu):
self.append(menuitem)
def popup(self, event, runner_slug, parent_window):
self.runner = lutris.runners.import_runner(runner_slug)()
self.runner = runners.import_runner(runner_slug)()
self.parent_window = parent_window
# Clear existing menu

View file

@ -3,16 +3,16 @@ import os
import shutil
import shlex
from gi.repository import Gdk, GLib
from gi.repository import GLib
from .errors import ScriptingError
from lutris import runtime
from lutris.util import extract, devices, system
from lutris.util import extract, disks, system
from lutris.util.fileio import EvilConfigParser, MultiOrderedDict
from lutris.util.log import logger
from lutris.runners import wine, import_task, import_runner, InvalidRunner
from lutris.runners import wine, import_task
from lutris.thread import LutrisThread
@ -22,7 +22,7 @@ class CommandsMixin(object):
def __init__(self):
raise RuntimeError("Don't instanciate this class, it's a mixin!!!!!!!!!!!!!!!!")
def _get_wine_version(self):
def _get_runner_version(self):
if self.script.get('wine'):
return wine.support_legacy_version(self.script['wine'].get('version'))
@ -52,14 +52,19 @@ class CommandsMixin(object):
def execute(self, data):
"""Run an executable file."""
args = []
terminal = None
working_dir = None
if isinstance(data, dict):
self._check_required_params('file', data, 'execute')
file_ref = data['file']
args_string = data.get('args', '')
for arg in shlex.split(args_string):
args.append(self._substitute(arg))
terminal = data.get('terminal')
working_dir = data.get('working_dir')
else:
file_ref = data
# Determine whether 'file' value is a file id or a path
exec_path = self._get_file(file_ref) or self._substitute(file_ref)
if not exec_path:
@ -68,12 +73,15 @@ class CommandsMixin(object):
if not os.path.exists(exec_path):
raise ScriptingError("Unable to find required executable",
exec_path)
self.chmodx(exec_path)
if not os.access(exec_path, os.X_OK):
self.chmodx(exec_path)
terminal = data.get('terminal')
if terminal:
terminal = system.get_default_terminal()
if not working_dir or not os.path.exists(working_dir):
working_dir = self.target_path
command = [exec_path] + args
logger.debug("Executing %s" % command)
thread = LutrisThread(command, env=runtime.get_env(), term=terminal,
@ -132,7 +140,8 @@ class CommandsMixin(object):
requires = data.get('requires')
message = data.get(
'message',
"Insert game disc or mount disk image and click OK."
"Insert or mount game disc and click Autodetect or\n"
"use Browse if the disc is mounted on a non standard location."
)
message += (
"\n\nLutris is looking for a mounted disk drive or image \n"
@ -141,19 +150,21 @@ class CommandsMixin(object):
)
if self.runner == 'wine':
GLib.idle_add(self.parent.eject_button.show)
GLib.idle_add(self.parent.wait_for_user_action, message,
GLib.idle_add(self.parent.ask_for_disc, message,
self._find_matching_disc, requires)
return 'STOP'
def _find_matching_disc(self, widget, requires):
drives = devices.get_mounted_discs()
def _find_matching_disc(self, widget, requires, extra_path=None):
if extra_path:
drives = [extra_path]
else:
drives = disks.get_mounted_discs()
for drive in drives:
mount_point = drive.get_root().get_path()
required_abspath = os.path.join(mount_point, requires)
required_abspath = os.path.join(drive, requires)
required_abspath = system.fix_path_case(required_abspath)
if required_abspath:
logger.debug("Found %s on cdrom %s" % (requires, mount_point))
self.game_disc = mount_point
logger.debug("Found %s on cdrom %s" % (requires, drive))
self.game_disc = drive
self._iter_commands()
break
@ -265,6 +276,15 @@ class CommandsMixin(object):
dest_file.write(line)
os.rename(tmp_filename, filename)
def _get_task_runner_and_name(self, task_name):
if '.' in task_name:
# Run a task from a different runner
# than the one for this installer
runner_name, task_name = task_name.split('.')
else:
runner_name = self.script["runner"]
return runner_name, task_name
def task(self, data):
"""Directive triggering another function specific to a runner.
@ -274,40 +294,17 @@ class CommandsMixin(object):
self._check_required_params('name', data, 'task')
if self.parent:
GLib.idle_add(self.parent.cancel_button.set_sensitive, False)
task_name = data.pop('name')
if '.' in task_name:
# Run a task from a different runner
# than the one for this installer
runner_name, task_name = task_name.split('.')
else:
runner_name = self.script["runner"]
try:
runner_class = import_runner(runner_name)
except InvalidRunner:
GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
raise ScriptingError('Invalid runner provided %s', runner_name)
runner = runner_class()
runner_name, task_name = self._get_task_runner_and_name(data.pop('name'))
# Check/install Wine runner at version specified in the script
# TODO : move this, the runner should be installed before the install
# starts
wine_version = None
if runner_name == 'wine':
wine_version = self._get_wine_version()
wine_version = self._get_runner_version()
if wine_version and task_name == 'wineexec':
if not wine.is_version_installed(wine_version):
Gdk.threads_init()
Gdk.threads_enter()
runner.install(wine_version)
Gdk.threads_leave()
data['wine_path'] = wine.get_wine_version_exe(wine_version)
# Check/install other runner
elif not runner.is_installed():
Gdk.threads_init()
Gdk.threads_enter()
runner.install()
Gdk.threads_leave()
for key in data:
data[key] = self._substitute(data[key])
@ -333,9 +330,9 @@ class CommandsMixin(object):
return True
def write_config(self, params):
"""Write a key-value pair into an INI type config file."""
self._check_required_params(['file', 'section', 'key', 'value'],
params, 'write_config')
"""Write a key-value pair into an INI type config file."""
# Get file
config_file = self._get_file(params['file'])
if not config_file:
@ -351,9 +348,11 @@ class CommandsMixin(object):
parser.optionxform = str # Preserve text case
parser.read(config_file)
value = self._substitute(params['value'])
if not parser.has_section(params['section']):
parser.add_section(params['section'])
parser.set(params['section'], params['key'], params['value'])
parser.set(params['section'], params['key'], value)
with open(config_file, 'wb') as f:
parser.write(f)

View file

@ -19,7 +19,11 @@ from lutris.util.log import logger
from lutris.util.steam import get_app_state_log
from lutris.config import LutrisConfig, make_game_config_id
from lutris.runners import wine, winesteam, steam
from lutris.runners import (
wine, winesteam, steam, import_runner,
InvalidRunner, NonInstallableRunnerError, RunnerInstallationError
)
def fetch_script(game_ref):
@ -54,6 +58,8 @@ class ScriptInterpreter(CommandsMixin):
self.user_inputs = []
self.steam_data = {}
self.script = script
self.runners_to_install = []
self.prev_states = [] # Previous states for the Steam installer
if not self.script:
return
if not self.is_valid():
@ -85,7 +91,7 @@ class ScriptInterpreter(CommandsMixin):
return os.path.expanduser(os.path.join(games_dir, self.game_slug))
@property
def download_cache_path(self):
def cache_path(self):
return os.path.join(settings.CACHE_DIR,
"installer/%s" % self.game_slug)
@ -144,8 +150,8 @@ class ScriptInterpreter(CommandsMixin):
def iter_game_files(self):
if self.files:
# Create cache dir if needed
if not os.path.exists(self.download_cache_path):
os.mkdir(self.download_cache_path)
if not os.path.exists(self.cache_path):
os.mkdir(self.cache_path)
if self.target_path and self.should_create_target:
os.makedirs(self.target_path)
@ -199,7 +205,7 @@ class ScriptInterpreter(CommandsMixin):
file_uri = pga_uri
# Setup destination path
dest_file = os.path.join(self.download_cache_path, filename)
dest_file = os.path.join(self.cache_path, filename)
if file_uri.startswith("N/A"):
# Ask the user where the file is located
@ -269,37 +275,58 @@ class ScriptInterpreter(CommandsMixin):
self.steam_data['platform'] = "linux"
self.install_steam_game(steam.steam, is_game_files=True)
def check_steam_install(self):
"""Checks that the required version of Steam is installed.
Return a boolean indicating whether is it or not.
def check_runner_install(self):
"""Check if the runner is installed before starting the installation
Install the required runner(s) if necessary. This should handle runner
dependencies (wine for winesteam) or runners used for installer tasks.
"""
if self.steam_data['platform'] == 'windows':
# Check that wine is installed
wine_runner = wine.wine()
if not wine_runner.is_installed():
logger.debug('Wine is not installed')
wine_runner.install(
downloader=self.parent.start_download,
callback=self.check_steam_install
required_runners = []
runner = self.get_runner_class(self.runner)
if runner.depends_on is not None:
required_runners.append(runner.depends_on())
required_runners.append(runner())
for command in self.script.get('installer', []):
command_name, command_params = self._get_command_name_and_params(command)
if command_name == 'task':
runner_name, _task_name = self._get_task_runner_and_name(
command_params['name']
)
return False
# Getting data from Wine Steam
steam_runner = winesteam.winesteam()
if not steam_runner.is_installed():
logger.debug('Winesteam not installed')
winesteam.download_steam(
downloader=self.parent.start_download,
callback=self.on_steam_downloaded
)
return False
return True
runner_names = [r.name for r in required_runners]
if runner_name not in runner_names:
required_runners.append(self.get_runner_class(runner_name)())
for runner in required_runners:
if not runner.is_installed():
self.runners_to_install.append(runner)
self.install_runners()
def install_runners(self):
if not self.runners_to_install:
self.iter_game_files()
else:
steam_runner = steam.steam()
if not steam_runner.is_installed():
raise ScriptingError(
"Install Steam for Linux and start installer again"
)
return True
runner = self.runners_to_install.pop(0)
self.install_runner(runner)
def install_runner(self, runner):
logger.debug('Installing {}'.format(runner.name))
try:
runner.install(
version=self._get_runner_version(),
downloader=self.parent.start_download,
callback=self.install_runners
)
except (NonInstallableRunnerError, RunnerInstallationError) as ex:
logger.error(ex.message)
raise ScriptingError(ex.message)
def get_runner_class(self, runner_name):
try:
runner = import_runner(runner_name)
except InvalidRunner:
GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
raise ScriptingError('Invalid runner provided %s', runner_name)
return runner
def file_selected(self, file_path):
file_id = self.current_file_id
@ -319,6 +346,9 @@ class ScriptInterpreter(CommandsMixin):
if self.target_path and os.path.exists(self.target_path):
os.chdir(self.target_path)
if not os.path.exists(self.cache_path):
os.mkdir(self.cache_path)
# Add steam installation to commands if it's a Steam game
if self.runner in ('steam', 'winesteam'):
try:
@ -359,9 +389,7 @@ class ScriptInterpreter(CommandsMixin):
else:
self._finish_install()
def _map_command(self, command_data):
"""Map a directive from the `installer` section to an internal
method."""
def _get_command_name_and_params(self, command_data):
if isinstance(command_data, dict):
command_name = list(command_data.keys())[0]
command_params = command_data[command_name]
@ -370,8 +398,14 @@ class ScriptInterpreter(CommandsMixin):
command_params = {}
command_name = command_name.replace("-", "_")
command_name = command_name.strip("_")
return command_name, command_params
def _map_command(self, command_data):
"""Map a directive from the `installer` section to an internal
method."""
command_name, command_params = self._get_command_name_and_params(command_data)
if not hasattr(self, command_name):
raise ScriptingError("The command %s does not exists"
raise ScriptingError('The command "%s" does not exist.'
% command_name)
return getattr(self, command_name), command_params
@ -491,6 +525,8 @@ class ScriptInterpreter(CommandsMixin):
config[key] = dict(
[(k, self._substitute(v)) for (k, v) in value.iteritems()]
)
elif isinstance(value, bool):
config[key] = value
else:
config[key] = self._substitute(value)
return config
@ -501,8 +537,8 @@ class ScriptInterpreter(CommandsMixin):
def cleanup(self):
os.chdir(os.path.expanduser('~'))
if os.path.exists(self.download_cache_path):
shutil.rmtree(self.download_cache_path)
if os.path.exists(self.cache_path):
shutil.rmtree(self.cache_path)
# --------------
# Revert install
@ -527,7 +563,7 @@ class ScriptInterpreter(CommandsMixin):
"""Replace path aliases with real paths."""
replacements = {
"GAMEDIR": self.target_path,
"CACHE": settings.CACHE_DIR,
"CACHE": self.cache_path,
"HOME": os.path.expanduser("~"),
"DISC": self.game_disc,
"USER": os.getenv('USER'),
@ -559,9 +595,6 @@ class ScriptInterpreter(CommandsMixin):
# Check if Steam is installed, save the method's arguments so it can
# be called again once Steam is installed.
self.steam_data['callback_args'] = (runner_class, is_game_files)
is_installed = self.check_steam_install()
if not is_installed:
return 'STOP'
steam_runner = self._get_steam_runner(runner_class)
self.steam_data['is_game_files'] = is_game_files
@ -603,13 +636,16 @@ class ScriptInterpreter(CommandsMixin):
steam_runner = self._get_steam_runner()
states = get_app_state_log(steam_runner.steam_data_dir, appid,
self.install_start_time)
logger.debug(states)
if states and states.pop().startswith('Fully Installed'):
self._on_steam_game_installed()
if states != self.prev_states:
logger.debug("Steam installation status:")
logger.debug(states)
self.prev_states = states
if states and states[-1].startswith('Fully Installed'):
logger.debug('Steam game has finished installing')
self._on_steam_game_installed()
return False
else:
logger.debug('Steam game still installing')
return True
def _on_steam_game_installed(self, *args):
@ -643,19 +679,7 @@ class ScriptInterpreter(CommandsMixin):
os.path.join(data_path, self.steam_data['steam_rel_path'])
self.iter_game_files()
def on_steam_downloaded(self, *args):
logger.debug("Steam downloaded")
dest = winesteam.get_steam_installer_dest()
winesteam_runner = winesteam.winesteam()
AsyncCall(winesteam_runner.install, self.on_winesteam_installed, dest)
def on_winesteam_installed(self, *args):
logger.debug("Winesteam installed")
callback_args = self.steam_data['callback_args']
self.parent.add_spinner()
self.install_steam_game(*callback_args)
def eject_wine_disc(self):
prefix = self.target_path
wine_path = wine.get_wine_version_exe(self._get_wine_version())
wine_path = wine.get_wine_version_exe(self._get_runner_version())
wine.eject_disc(wine_path, prefix)

View file

@ -12,5 +12,5 @@ def migrate():
'gens', 'hatari', 'jzintv', 'mame', 'mednafen', 'mess',
'mupen64plus', 'nulldc', 'o2em', 'osmose', 'pcsxr',
'reicast', 'ResidualVM', 'residualvm', 'scummvm',
'snes9x', 'stella', 'vice', 'virtualjaguar']:
'snes9x', 'stella', 'vice', 'virtualjaguar', 'zdoom']:
shutil.rmtree(path)

View file

@ -145,33 +145,29 @@ def get_table_length(table='games'):
def get_games(name_filter=None, filter_installed=False):
"""Get the list of every game in database."""
with sql.db_cursor(PGA_DB) as cursor:
query = "select * from games"
params = ()
filters = []
if name_filter:
params = (name_filter, )
filters.append("name LIKE ?")
if filter_installed:
filters.append("installed = 1")
if filters:
query += " WHERE " + " AND ".join([f for f in filters])
query += " ORDER BY slug"
rows = cursor.execute(query, params)
results = rows.fetchall()
column_names = [column[0] for column in cursor.description]
game_list = []
for row in results:
game_info = {}
for index, column in enumerate(column_names):
game_info[column] = row[index]
game_list.append(game_info)
return game_list
query = "select * from games"
params = ()
filters = []
if name_filter:
params = (name_filter, )
filters.append("name LIKE ?")
if filter_installed:
filters.append("installed = 1")
if filters:
query += " WHERE " + " AND ".join([f for f in filters])
query += " ORDER BY slug"
return sql.db_query(PGA_DB, query, params)
def get_steam_games():
"""Return the games with a SteamID"""
query = "select * from games where steamid is not null and steamid != ''"
return sql.db_query(PGA_DB, query)
def get_game_by_field(value, field='slug', all=False):
"""Query a game based on a database field"""
if field not in ('slug', 'installer_slug', 'id', 'configpath'):
if field not in ('slug', 'installer_slug', 'id', 'configpath', 'steamid'):
raise ValueError("Can't query by field '%s'" % field)
game_result = sql.db_select(PGA_DB, "games", condition=(field, value))
if game_result:
@ -205,26 +201,27 @@ def add_games_bulk(games):
return inserted_ids
def add_or_update(name, runner, slug=None, **kwargs):
def add_or_update(**params):
"""
FIXME probably not the desired behavior since it disallows multiple games
with the same slug
"""
if not slug:
slug = slugify(name)
if 'id' in kwargs:
game = get_game_by_field(kwargs['id'], 'id')
slug = params.get('slug')
name = params.get('name')
id = params.get('id')
assert any([slug, name, 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')
kwargs['name'] = name
kwargs['runner'] = runner
kwargs['slug'] = slug
if game:
game_id = game['id']
sql.db_update(PGA_DB, "games", kwargs, ('id', game_id))
sql.db_update(PGA_DB, "games", params, ('id', game_id))
return game_id
else:
return add_game(**kwargs)
return add_game(**params)
def delete_game(id):

View file

@ -13,13 +13,13 @@ __all__ = (
# Atari
"stella", "atari800", "hatari", "virtualjaguar",
# Nintendo
"snes9x", "mupen64plus", "dolphin",
"snes9x", "mupen64plus", "dolphin", "desmume", "citra",
# Sony
"pcsxr", "ppsspp", "pcsx2",
# Sega
"osmose", "dgen", "reicast",
# Misc legacy systems
"frotz", "jzintv", "o2em",
"frotz", "jzintv", "o2em", "zdoom"
)
@ -27,6 +27,14 @@ class InvalidRunner(Exception):
pass
class RunnerInstallationError(Exception):
pass
class NonInstallableRunnerError(Exception):
pass
def get_runner_module(runner_name):
if runner_name not in __all__:
raise InvalidRunner("Invalid runner name '%s'", runner_name)

View file

@ -82,24 +82,24 @@ class atari800(Runner):
}
]
def install(self):
success = super(atari800, self).install()
if not success:
return False
config_path = os.path.expanduser("~/.atari800")
if not os.path.exists(config_path):
os.makedirs(config_path)
bios_archive = os.path.join(config_path, 'atari800-bioses.zip')
dlg = DownloadDialog(self.bios_url, bios_archive)
dlg.run()
if not os.path.exists(bios_archive):
ErrorDialog("Could not download Atari800 BIOS archive")
return
extract.extract_archive(bios_archive, config_path)
os.remove(bios_archive)
config = LutrisConfig(runner_slug='atari800')
config.raw_runner_config.update({'bios_path': config_path})
config.save()
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
config_path = system.create_folder("~/.atari800")
bios_archive = os.path.join(config_path, 'atari800-bioses.zip')
dlg = DownloadDialog(self.bios_url, bios_archive)
dlg.run()
if not system.path_exists(bios_archive):
ErrorDialog("Could not download Atari800 BIOS archive")
return
extract.extract_archive(bios_archive, config_path)
os.remove(bios_archive)
config = LutrisConfig(runner_slug='atari800')
config.raw_runner_config.update({'bios_path': config_path})
config.save()
if callback:
callback()
super(atari800, self).install(version, downloader, on_runner_installed)
def get_executable(self):
return os.path.join(settings.RUNNER_DIR, 'atari800/bin/atari800')
@ -134,7 +134,7 @@ class atari800(Runner):
arguments.append("-%s" % self.runner_config["machine"])
bios_path = self.runner_config.get("bios_path")
if not os.path.exists(bios_path):
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():
@ -142,7 +142,7 @@ class atari800(Runner):
arguments.append(os.path.join(bios_path, good_bios[bios]))
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)

28
lutris/runners/citra.py Normal file
View file

@ -0,0 +1,28 @@
import os
from lutris import settings
from lutris.runners.runner import Runner
class citra(Runner):
human_name = "Citra"
platform = 'Nintendo 3DS'
description = 'Nintendo 3DS emulator'
game_options = [{
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
'help': ("The game data, commonly called a ROM image.")
}]
def get_executable(self):
return os.path.join(settings.RUNNER_DIR, 'citra/citra-qt')
def play(self):
"""Run the game."""
arguments = [self.get_executable()]
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
arguments.append(rom)
return {"command": arguments}

28
lutris/runners/desmume.py Normal file
View file

@ -0,0 +1,28 @@
import os
from lutris import settings
from lutris.runners.runner import Runner
class desmume(Runner):
human_name = "DeSmuME"
platform = 'Nintendo DS'
description = 'Nintendo DS emulator'
game_options = [{
'option': 'main_file',
'type': 'file',
'label': 'ROM file',
'help': ("The game data, commonly called a ROM image.")
}]
def get_executable(self):
return os.path.join(settings.RUNNER_DIR, 'desmume/bin/desmume')
def play(self):
"""Run the game."""
arguments = [self.get_executable()]
rom = self.game_config.get('main_file') or ''
if not os.path.exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}
arguments.append(rom)
return {"command": arguments}

View file

@ -4,6 +4,7 @@ from lutris import settings
from lutris.config import LutrisConfig
from lutris.gui.dialogs import QuestionDialog, FileDialog
from lutris.runners.runner import Runner
from lutris.util import system
class hatari(Runner):
@ -99,31 +100,28 @@ class hatari(Runner):
}
]
def install(self):
success = super(hatari, self).install()
if not success:
return False
config_path = os.path.expanduser('~/.hatari')
if not os.path.exists(config_path):
os.makedirs(config_path)
bios_path = os.path.expanduser('~/.hatari/bios')
if not os.path.exists(bios_path):
os.makedirs(bios_path)
dlg = QuestionDialog({
'question': "Do you want to select an Atari ST BIOS file?",
'title': "Use BIOS file?",
})
if dlg.result == dlg.YES:
bios_dlg = FileDialog("Select a BIOS file")
bios_filename = bios_dlg.filename
if not bios_filename:
return
shutil.copy(bios_filename, bios_path)
bios_path = os.path.join(bios_path, os.path.basename(bios_filename))
config = LutrisConfig(runner_slug='hatari')
config.raw_runner_config.update({'bios_file': bios_path})
config.save()
return True
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
bios_path = system.create_folder('~/.hatari/bios')
dlg = QuestionDialog({
'question': "Do you want to select an Atari ST BIOS file?",
'title': "Use BIOS file?",
})
if dlg.result == dlg.YES:
bios_dlg = FileDialog("Select a BIOS file")
bios_filename = bios_dlg.filename
if not bios_filename:
return
shutil.copy(bios_filename, bios_path)
bios_path = os.path.join(bios_path, os.path.basename(bios_filename))
config = LutrisConfig(runner_slug='hatari')
config.raw_runner_config.update({'bios_file': bios_path})
config.save()
if callback:
callback()
super(hatari, self).install(version=version,
downloader=downloader,
callback=on_runner_installed)
def get_executable(self):
return os.path.join(settings.RUNNER_DIR, 'hatari/bin/hatari')

View file

@ -14,10 +14,13 @@ class mednafen(Runner):
"GameBoy Advance, NES, PC Engine (TurboGrafx 16), PC-FX, "
"SuperGrafx, NeoGeo Pocket, NeoGeo Pocket Color, WonderSwan")
package = "mednafen"
machine_choices = (("NES", "nes"),
("PC Engine", "pce"),
('Game Boy', 'gb'),
('Game Boy Advance', 'gba'))
machine_choices = (
("NES", "nes"),
("PC Engine", "pce"),
('Game Boy', 'gb'),
('Game Boy Advance', 'gba'),
('Playstation', 'psx')
)
game_options = [
{
"option": "main_file",
@ -40,6 +43,43 @@ class mednafen(Runner):
"type": "bool",
"label": "Fullscreen",
"default": False,
},
{
"option": "stretch",
"type": "choice",
"label": "Aspect ratio",
"choices": (
("Disabled", "0"),
("Stretched", "full"),
("Preserve aspect ratio", "aspect"),
("Integer scale", "aspect_int"),
("Multiple of 2 scale", "aspect_mult2"),
),
"default": "0"
},
{
"option": "scaler",
"type": "choice",
"label": "Video scaler",
"choices": (
("none", "none"),
("hq2x", "hq2x"),
("hq3x", "hq3x"),
("hq4x", "hq4x"),
("scale2x", "scale2x"),
("scale3x", "scale3x"),
("scale4x", "scale4x"),
("2xsai", "2xsai"),
("super2xsai", "super2xsai"),
("supereagle", "supereagle"),
("nn2x", "nn2x"),
("nn3x", "nn3x"),
("nn4x", "nn4x"),
("nny2x", "nny2x"),
("nny3x", "nny3x"),
("nny4x", "nny4x"),
),
"default": "hq4x",
}
]
@ -93,9 +133,9 @@ class mednafen(Runner):
nes_controls = [
"-nes.input.port1.gamepad.a",
"joystick {} {}".format(joy_ids[0], BTN_A),
"-nes.input.port1.gamepad.b",
"joystick {} {}".format(joy_ids[0], BTN_B),
"-nes.input.port1.gamepad.b",
"joystick {} {}".format(joy_ids[0], BTN_A),
"-nes.input.port1.gamepad.start",
"joystick {} {}".format(joy_ids[0], BTN_START),
"-nes.input.port1.gamepad.select",
@ -112,9 +152,9 @@ class mednafen(Runner):
gba_controls = [
"-gba.input.builtin.gamepad.a",
"joystick {} {}".format(joy_ids[0], BTN_A),
"-gba.input.builtin.gamepad.b",
"joystick {} {}".format(joy_ids[0], BTN_B),
"-gba.input.builtin.gamepad.b",
"joystick {} {}".format(joy_ids[0], BTN_A),
"-gba.input.builtin.gamepad.shoulder_r",
"joystick {} {}".format(joy_ids[0], BTN_R),
"-gba.input.builtin.gamepad.shoulder_l",
@ -154,9 +194,9 @@ class mednafen(Runner):
pce_controls = [
"-pce.input.port1.gamepad.i",
"joystick {} {}".format(joy_ids[0], BTN_A),
"-pce.input.port1.gamepad.ii",
"joystick {} {}".format(joy_ids[0], BTN_B),
"-pce.input.port1.gamepad.ii",
"joystick {} {}".format(joy_ids[0], BTN_A),
"-pce.input.port1.gamepad.run",
"joystick {} {}".format(joy_ids[0], BTN_START),
"-pce.input.port1.gamepad.select",
@ -188,10 +228,15 @@ class mednafen(Runner):
rom = self.game_config.get('main_file') or ''
machine = self.game_config.get('machine') or ''
if self.runner_config.get("fs"):
fullscreen = self.runner_config.get("fs") or "0"
if fullscreen is True:
fullscreen = "1"
else:
elif fullscreen is False:
fullscreen = "0"
stretch = self.runner_config.get('stretch') or "0"
scaler = self.runner_config.get('scaler') or "hq4x"
resolution = get_current_resolution()
(resolutionx, resolutiony) = resolution.split("x")
xres = str(resolutionx)
@ -199,8 +244,8 @@ class mednafen(Runner):
options = ["-fs", fullscreen,
"-" + machine + ".xres", xres,
"-" + machine + ".yres", yres,
"-" + machine + ".stretch", "1",
"-" + machine + ".special", "hq4x",
"-" + machine + ".stretch", stretch,
"-" + machine + ".special", scaler,
"-" + machine + ".videoip", "1"]
joy_ids = self.find_joysticks()
if len(joy_ids) > 0:

View file

@ -22,7 +22,8 @@ class mess(Runner):
("Amstrad CPC 464", 'cpc464'),
("Amstrad CPC 6128", 'cpc6128'),
("Amstrad GX4000", 'gx4000'),
("Apple II", 'apple2'),
("Apple II", 'apple2ee'),
("Apple IIGS", 'apple2gs'),
("Commodore 64", 'c64'),
("ZX Spectrum", 'spectrum'),
("ZX Spectrum 128", 'spec128'),
@ -35,6 +36,8 @@ class mess(Runner):
'label': "Storage type",
'choices': [
("Floppy disk", 'flop'),
("Floppy drive 1", 'flop1'),
("Floppy drive 2", 'flop2'),
("Cassette (tape)", 'cass'),
("Cartridge", 'cart'),
("Snapshot", 'snapshot'),

View file

@ -8,6 +8,7 @@ class o2em(Runner):
human_name = "O2EM"
description = "Magnavox Oyssey² Emulator"
platform = "Magnavox Odyssey 2, Phillips Videopac+"
bios_path = os.path.expanduser("~/.o2em/bios")
checksums = {
'o2rom': "562d5ebf9e030a40d6fabfc2f33139fd",
@ -73,18 +74,19 @@ class o2em(Runner):
}
]
def install(self):
super(o2em, self).install()
bios_path = os.path.expanduser("~/.o2em/bios")
if not os.path.exists(bios_path):
os.makedirs(bios_path)
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
if not os.path.exists(self.bios_path):
os.makedirs(self.bios_path)
if callback:
callback()
super(o2em, self).install(version, downloader, on_runner_installed)
def get_executable(self):
return os.path.join(settings.RUNNER_DIR, 'o2em/o2em')
def play(self):
bios_path = os.path.join(os.path.expanduser("~"), ".o2em/bios/")
arguments = ["-biosdir=%s" % bios_path]
arguments = ["-biosdir=%s" % self.bios_path]
if self.runner_config.get("fullscreen"):
arguments.append("-fullscreen")

View file

@ -4,7 +4,7 @@ from lutris import settings
from lutris.config import LutrisConfig
from lutris.gui.dialogs import QuestionDialog, FileDialog
from lutris.runners.runner import Runner
from lutris.util.system import find_executable
from lutris.util import system
class pcsxr(Runner):
@ -51,42 +51,35 @@ class pcsxr(Runner):
# System wide available emulator
candidates = ('pcsx', 'pcsxr')
for candidate in candidates:
executable = find_executable(candidate)
executable = system.find_executable(candidate)
if executable:
return executable
def install(self):
success = super(pcsxr, self).install()
if not success:
return False
config_path = os.path.expanduser('~/.pcsxr')
if not os.path.exists(config_path):
os.makedirs(config_path)
# Bios
bios_path = os.path.expanduser('~/.pcsxr/bios')
if not os.path.exists(bios_path):
os.makedirs(bios_path)
dlg = QuestionDialog({
'question': ("Do you want to select a Playstation BIOS file?\n\n"
"The BIOS is the core code running the machine.\n"
"PCSX-Reloaded includes an emulated BIOS, but it is "
"still incomplete. \n"
"Using an original BIOS avoids some bugs and reduced "
"compatibility \n"
"with some games."),
'title': "Use BIOS file?",
})
if dlg.result == dlg.YES:
bios_dlg = FileDialog("Select a BIOS file")
bios_src = bios_dlg.filename
shutil.copy(bios_src, bios_path)
# Save bios in config
bios_path = os.path.join(bios_path, os.path.basename(bios_src))
config = LutrisConfig(runner_slug='pcsxr')
config.raw_runner_config.update({'bios': bios_path})
config.save()
return True
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
bios_path = system.create_folder('~/.pcsxr/bios')
dlg = QuestionDialog({
'question': ("Do you want to select a Playstation BIOS file?\n\n"
"The BIOS is the core code running the machine.\n"
"PCSX-Reloaded includes an emulated BIOS, but it is "
"still incomplete. \n"
"Using an original BIOS avoids some bugs and reduced "
"compatibility \n"
"with some games."),
'title': "Use BIOS file?",
})
if dlg.result == dlg.YES:
bios_dlg = FileDialog("Select a BIOS file")
bios_src = bios_dlg.filename
shutil.copy(bios_src, bios_path)
# Save bios in config
bios_path = os.path.join(bios_path, os.path.basename(bios_src))
config = LutrisConfig(runner_slug='pcsxr')
config.raw_runner_config.update({'bios': bios_path})
config.save()
if callback:
callback()
super(pcsxr, self).install(version, downloader, on_runner_installed)
def play(self):
"""Run Playstation game."""

View file

@ -1,7 +1,13 @@
# -*- coding: utf-8 -*-
import re
import os
import shutil
from ConfigParser import ConfigParser
from collections import Counter
from lutris import settings
from lutris.runners.runner import Runner
from lutris.util import joypad, system
from lutris.gui.dialogs import NoticeDialog
class reicast(Runner):
@ -17,24 +23,130 @@ class reicast(Runner):
"Supported formats: ISO, CDI")
}]
runner_options = [
{
"option": "fullscreen",
"type": "bool",
"label": "Fullscreen",
'default': False,
}
]
def __init__(self, config=None):
super(reicast, self).__init__(config)
self._joypads = None
self.runner_options = [
{
'option': 'fullscreen',
'type': 'bool',
'label': 'Fullscreen',
'default': False,
},
{
'option': 'device_id_1',
'type': 'choice',
'label': 'Joypad 1',
'choices': self.get_joypads(),
'default': '-1'
},
{
'option': 'device_id_2',
'type': 'choice',
'label': 'Joypad 2',
'choices': self.get_joypads(),
'default': '-1'
},
{
'option': 'device_id_3',
'type': 'choice',
'label': 'Joypad 3',
'choices': self.get_joypads(),
'default': '-1'
},
{
'option': 'device_id_4',
'type': 'choice',
'label': 'Joypad 4',
'choices': self.get_joypads(),
'default': '-1'
}
]
def install(self, version=None, downloader=None, callback=None):
def on_runner_installed(*args):
mapping_path = system.create_folder('~/.reicast/mappings')
mapping_source = os.path.join(settings.RUNNER_DIR, 'reicast/mappings')
for mapping_file in os.listdir(mapping_source):
shutil.copy(os.path.join(mapping_source, mapping_file), mapping_path)
system.create_folder('~/.reicast/data')
NoticeDialog("You have to copy valid BIOS files to ~/.reicast/data "
"before playing")
super(reicast, self).install(version, downloader, on_runner_installed)
def get_joypads(self):
"""Return list of joypad in a format usable in the options"""
if self._joypads:
return self._joypads
joypad_list = [('No joystick', '-1')]
joypad_devices = joypad.get_joypads()
name_counter = Counter([j[1] for j in joypad_devices])
name_indexes = {}
for (dev, joy_name) in joypad_devices:
dev_id = re.findall(r'(\d+)', dev)[0]
if name_counter[joy_name] > 1:
if joy_name not in name_indexes:
index = 1
else:
index = name_indexes[joy_name] + 1
name_indexes[joy_name] = index
else:
index = 0
if index:
joy_name += " (%d)" % index
joypad_list.append((joy_name, dev_id))
self._joypads = joypad_list
return joypad_list
def get_executable(self):
return os.path.join(settings.RUNNER_DIR, 'reicast/reicast.elf')
def write_config(self, config):
parser = ConfigParser()
config_path = os.path.expanduser('~/.reicast/emu.cfg')
if os.path.exists(config_path):
with open(config_path, 'r') as config_file:
parser.read(config_file)
for section in config:
if not parser.has_section(section):
parser.add_section(section)
for (key, value) in config[section].iteritems():
parser.set(section, key, value)
with open(config_path, 'w') as config_file:
parser.write(config_file)
def play(self):
iso = self.game_config.get('iso')
fullscreen = '1' if self.runner_config.get('fullscreen') else '0'
reicast_config = {
'x11': {
'fullscreen': fullscreen
},
'input': {},
'players': {
'nb': '1'
}
}
players = 1
reicast_config['input'] = {}
for index in range(1, 5):
config_string = 'device_id_%d' % index
joy_id = self.runner_config.get(config_string) or '-1'
reicast_config['input']['evdev_{}'.format(config_string)] = joy_id
if index > 1 and joy_id != '-1':
players += 1
reicast_config['players']['nb'] = players
self.write_config(reicast_config)
iso = self.game_config.get('iso')
command = [
self.get_executable(),
"-config", "config:image={}".format(iso),
"-config", "x11:fullscreen={}".format(fullscreen)
]
return {'command': command}

View file

@ -15,6 +15,7 @@ from lutris.util.extract import extract_archive
from lutris.util.log import logger
from lutris.util import system
from lutris.util.http import Request
from lutris.runners import RunnerInstallationError
def get_arch():
@ -37,6 +38,7 @@ class Runner(object):
runner_options = []
system_options_override = []
context_menu_entries = []
depends_on = None
def __init__(self, config=None):
"""Initialize runner."""
@ -205,7 +207,10 @@ class Runner(object):
response_content = response.json
if response_content:
versions = response_content.get('versions') or []
arch = get_arch()
if self.name == 'wine':
arch = 'i386'
else:
arch = self.arch
if version:
if version.endswith('-i386') or version.endswith('-x86_64'):
version, arch = version.rsplit('-', 1)
@ -237,10 +242,16 @@ class Runner(object):
def install(self, version=None, downloader=None, callback=None):
"""Install runner using package management systems."""
logger.debug("Installing %s (version=%s, downloader=%s, callback=%s)",
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(
'This runner is not available for your platform'
)
return False
opts = {}
@ -256,8 +267,7 @@ class Runner(object):
opts['dest'] = os.path.join(settings.RUNNER_DIR,
self.name, dirname)
url = runner_info['url']
is_extracted = self.download_and_extract(url, **opts)
return is_extracted
self.download_and_extract(url, **opts)
def download_and_extract(self, url, dest=None, **opts):
merge_single = opts.get('merge_single', False)
@ -278,8 +288,8 @@ class Runner(object):
else:
dialog = dialogs.DownloadDialog(url, runner_archive)
dialog.run()
return self.extract(archive=runner_archive, dest=dest,
merge_single=merge_single)
self.extract(archive=runner_archive, dest=dest, merge_single=merge_single,
callback=callback)
def on_downloaded(self, widget, data, user_data):
"""GObject callback received by downloader"""
@ -288,14 +298,12 @@ class Runner(object):
def extract(self, archive=None, dest=None, merge_single=None,
callback=None):
if not os.path.exists(archive):
logger.error("Can't find %s, aborting install", archive)
return False
# TODO Check install methods to catch RunnerInstallationError
raise RunnerInstallationError("Failed to extract {}", archive)
extract_archive(archive, dest, merge_single=merge_single)
os.remove(archive)
if callback:
callback()
else:
return True
def remove_game_data(self, game_path=None):
system.remove_folder(game_path)

View file

@ -1,8 +1,8 @@
import os
import time
import subprocess
from lutris.runners import NonInstallableRunnerError
from lutris.runners.runner import Runner
from lutris.gui.dialogs import NoticeDialog
from lutris.thread import LutrisThread
from lutris.util.log import logger
from lutris.util import system
@ -84,8 +84,7 @@ class steam(Runner):
return False
return self.game_path
@property
def steam_config(self):
def get_steam_config(self):
"""Return the "Steam" part of Steam's config.vdf as a dict."""
steam_data_dir = self.steam_data_dir
if not steam_data_dir:
@ -104,15 +103,15 @@ class steam(Runner):
def steam_data_dir(self):
"""Return dir where Steam files lie."""
candidates = (
"~/.local/share/Steam/",
"~/.local/share/steam/",
"~/.steam/",
"~/.Steam/",
"~/.local/share/steam/SteamApps",
"~/.steam/steam/SteamApps",
"~/.steam/SteamApps",
)
for candidate in candidates:
path = os.path.expanduser(candidate)
if os.path.isdir(path):
return path
path = system.fix_path_case(path)
if path:
return path.rstrip('sSteamAp')
def get_executable(self):
return system.find_executable('steam')
@ -135,7 +134,7 @@ class steam(Runner):
if main_dir and os.path.isdir(main_dir):
dirs.append(main_dir)
# Custom dirs
steam_config = self.steam_config
steam_config = self.get_steam_config()
if steam_config:
i = 1
while ('BaseInstallFolder_%s' % i) in steam_config:
@ -152,11 +151,12 @@ class steam(Runner):
return steamapps_paths[0]
def install(self):
message = "Steam for Linux installation is not handled by Lutris.\n" \
"Please go to " \
"<a href='http://steampowered.com'>http://steampowered.com</a>" \
raise NonInstallableRunnerError(
"Steam for Linux installation is not handled by Lutris.\n"
"Please go to "
"<a href='http://steampowered.com'>http://steampowered.com</a>"
" or install Steam with the package provided by your distribution."
NoticeDialog(message)
)
def install_game(self, appid, generate_acf=False):
logger.debug("Installing steam game %s", appid)

View file

@ -1,6 +1,6 @@
import os
import time
import shlex
import shutil
import subprocess
from textwrap import dedent
@ -13,6 +13,11 @@ from lutris.runners.runner import Runner
from lutris.thread import LutrisThread
WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
WINE_PATHS = {
'winehq-devel': '/opt/wine-devel/bin/wine',
'winehq-staging': '/opt/wine-staging/bin/wine',
'system': 'wine',
}
def set_regedit(path, key, value='', type='REG_SZ', wine_path=None,
@ -46,7 +51,7 @@ def set_regedit(path, key, value='', type='REG_SZ', wine_path=None,
def set_regedit_file(filename, wine_path=None, prefix=None, arch='win32'):
"""Apply a regedit file to the Windows registry."""
wineexec('regedit', args="/S " + filename, wine_path=wine_path, prefix=prefix,
wineexec('regedit', args="/S '%s'" % (filename), wine_path=wine_path, prefix=prefix,
arch=arch, blocking=True)
@ -58,6 +63,12 @@ def delete_registry_key(key, wine_path=None, prefix=None, arch='win32'):
def create_prefix(prefix, wine_dir=None, arch='win32'):
"""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_dir:
wine_dir = os.path.dirname(wine().get_executable())
wineboot_path = os.path.join(wine_dir, 'wineboot')
@ -68,7 +79,8 @@ def create_prefix(prefix, wine_dir=None, arch='win32'):
}
system.execute([wineboot_path], env=env)
if not os.path.exists(os.path.join(prefix, 'system.reg')):
logger.error('No system.reg found after prefix creation. Prefix might not be valid')
logger.error('No system.reg found after prefix creation. '
'Prefix might not be valid')
logger.info('%s Prefix created in %s', arch, prefix)
if prefix:
@ -112,7 +124,7 @@ def wineexec(executable, args="", wine_path=None, prefix=None, arch=None,
command = [wine_path]
if executable:
command.append(executable)
command += args.split()
command += shlex.split(args)
if blocking:
return system.execute(command, env=env, cwd=working_dir)
else:
@ -123,8 +135,7 @@ def wineexec(executable, args="", wine_path=None, prefix=None, arch=None,
def winetricks(app, prefix=None, winetricks_env=None, silent=True):
"""Execute winetricks."""
path = (system.find_executable('winetricks')
or os.path.join(datapath.get(), 'bin/winetricks'))
path = os.path.join(datapath.get(), 'bin/winetricks')
arch = detect_prefix_arch(prefix) or 'win32'
if not winetricks_env:
winetricks_env = wine().get_executable()
@ -249,6 +260,18 @@ def get_default_version():
return installed_versions[0]
def get_system_wine_version(wine_path="wine"):
"""Return the version of Wine installed on the system."""
try:
version = subprocess.check_output([wine_path, "--version"]).strip()
except OSError:
return
else:
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
@ -319,6 +342,7 @@ class wine(Runner):
"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
}
core_processes = (
@ -341,11 +365,22 @@ class wine(Runner):
]
def get_wine_version_choices():
return (
[('System (%s)' % self.system_wine_version, 'system')] +
[('Custom (select executable below)', 'custom')] +
[(version, version) for version in get_wine_versions()]
versions = []
labels = {
'winehq-devel': 'WineHQ devel (%s)',
'winehq-staging': 'WineHQ staging (%s)',
'system': 'System (%s)',
}
for build in sorted(WINE_PATHS.keys()):
version = get_system_wine_version(WINE_PATHS[build])
if version:
versions.append((labels[build] % version, build))
versions.append(
('Custom (select executable below)', 'custom')
)
versions += [(v, v) for v in get_wine_versions()]
return versions
self.runner_options = [
{
@ -465,6 +500,12 @@ class wine(Runner):
"for your system. Alsa is the default for modern"
"Linux distributions.")
},
{
'option': 'ShowCrashDialog',
'label': 'Show crash dialogs',
'type': 'bool',
'default': False
},
{
'option': 'show_debug',
'label': 'Output debugging info',
@ -501,16 +542,6 @@ class wine(Runner):
else:
return super(wine, self).working_dir
@property
def system_wine_version(self):
"""Return the version of Wine installed on the system."""
try:
version = subprocess.check_output(["wine", "--version"])
except OSError:
return "not installed"
else:
return version.strip('wine-\n')
@property
def wine_arch(self):
"""Return the wine architecture.
@ -539,9 +570,10 @@ class wine(Runner):
if not version:
return
if version == 'system':
if system.find_executable('wine'):
return 'wine'
if version in WINE_PATHS.keys():
abs_path = system.find_executable(WINE_PATHS[version])
if abs_path:
return abs_path
# Fall back on bundled Wine
version = get_default_version()
elif version == 'custom':
@ -552,18 +584,6 @@ class wine(Runner):
return os.path.join(path, version, 'bin/wine')
def is_installed(self):
version = self.get_version()
if version == 'system':
if system.find_executable('wine'):
return True
else:
return False
elif version == 'custom':
custom_path = self.runner_config.get('custom_wine_path', '')
if os.path.exists(custom_path):
return True
else:
return False
executable = self.get_executable()
if executable:
return os.path.exists(executable)
@ -571,16 +591,16 @@ class wine(Runner):
return False
@classmethod
def msi_exec(cls, msi_file, quiet=False, prefix=None, wine_path=None):
def msi_exec(cls, msi_file, quiet=False, prefix=None, wine_path=None, working_dir=None, blocking=False):
msi_args = "/i %s" % msi_file
if quiet:
msi_args += " /q"
return wineexec("msiexec", args=msi_args, prefix=prefix,
wine_path=wine_path)
wine_path=wine_path, working_dir=working_dir, blocking=blocking)
def run_winecfg(self, *args):
winecfg(wine_path=self.get_executable(), prefix=self.prefix_path,
arch=self.wine_arch)
arch=self.wine_arch, blocking=False)
def run_regedit(self, *args):
wineexec("regedit", wine_path=self.get_executable(), prefix=self.prefix_path)
@ -591,20 +611,45 @@ class wine(Runner):
def run_joycpl(self, *args):
joycpl(prefix=self.prefix_path, wine_path=self.get_executable())
def set_wine_desktop(self, enable_desktop=False):
path = self.reg_keys['Desktop']
if enable_desktop:
set_regedit(path, 'Desktop', 'WineDesktop',
wine_path=self.get_executable(),
prefix=self.prefix_path,
arch=self.wine_arch)
else:
delete_registry_key(path,
wine_path=self.get_executable(),
prefix=self.prefix_path,
arch=self.wine_arch)
def set_regedit_keys(self):
"""Reset regedit keys according to config."""
prefix = self.prefix_path
enable_wine_desktop = False
for key, path in self.reg_keys.iteritems():
value = self.runner_config.get(key) or 'auto'
if not value or value == 'auto':
if not value or value == 'auto' and key != 'ShowCrashDialog':
delete_registry_key(path, wine_path=self.get_executable(),
prefix=prefix, arch=self.wine_arch)
elif key in self.runner_config:
if key == 'Desktop' and value is True:
value = 'WineDesktop'
set_regedit(path, key, value,
wine_path=self.get_executable(), prefix=prefix,
arch=self.wine_arch)
enable_wine_desktop = True
else:
if key == 'ShowCrashDialog':
if value is True:
value = '00000001'
else:
value = '00000000'
type = 'REG_DWORD'
else:
type = 'REG_SZ'
set_regedit(path, key, value, type=type,
wine_path=self.get_executable(), prefix=prefix,
arch=self.wine_arch)
self.set_wine_desktop(enable_wine_desktop)
overrides = self.runner_config.get('overrides') or {}
overrides_path = "%s\DllOverrides" % self.reg_prefix
for dll, value in overrides.iteritems():
@ -639,11 +684,10 @@ class wine(Runner):
return system.get_pids_using_file(exe)
def get_xinput_path(self):
xinput_path = os.path.abspath(
os.path.join(datapath.get(), 'lib/koku-xinput-wine.so')
)
logger.debug('Preloading %s', xinput_path)
return xinput_path
xinput_path = os.path.join(settings.RUNTIME_DIR,
'lib32/koku-xinput-wine/koku-xinput-wine.so')
if os.path.exists(xinput_path):
return xinput_path
def play(self):
game_exe = self.game_exe
@ -656,7 +700,12 @@ class wine(Runner):
launch_info['env'] = self.get_env(full=False)
if self.runner_config.get('xinput'):
launch_info['ld_preload'] = self.get_xinput_path()
xinput_path = self.get_xinput_path()
if xinput_path:
logger.debug('Preloading %s', xinput_path)
launch_info['ld_preload'] = self.get_xinput_path()
else:
logger.error('Missing koku-xinput-wine.so, Xinput won\'t be enabled')
command = [self.get_executable()]
if game_exe.endswith(".msi"):

View file

@ -31,18 +31,6 @@ def get_steam_installer_dest():
return os.path.join(settings.TMP_PATH, "SteamInstall.msi")
def download_steam(downloader=None, callback=None, callback_data=None):
"""Downloads steam with `downloader` then calls `callback`"""
steam_installer_path = get_steam_installer_dest()
if not downloader:
dialog = DownloadDialog(STEAM_INSTALLER_URL, steam_installer_path)
dialog.run()
else:
downloader(STEAM_INSTALLER_URL,
steam_installer_path, callback, callback_data)
return steam_installer_path
def is_running():
pid = system.get_pid('Steam.exe$')
if pid:
@ -64,6 +52,7 @@ class winesteam(wine.wine):
human_name = "Wine Steam"
platform = "Steam for Windows"
runnable_alone = True
depends_on = wine.wine
game_options = [
{
'option': 'appid',
@ -164,7 +153,15 @@ class winesteam(wine.wine):
@property
def launch_args(self):
return [self.get_executable(), self.get_steam_path(), '-no-dwrite']
args = [self.get_executable(), self.get_steam_path()]
# Fix invisible text in Steam
args.append('-no-dwrite')
# Try to fix Steam's browser. Never worked but it's supposed to...
args.append('-no-cef-sandox')
return args
def get_open_command(self, registry):
"""Return Steam's Open command, useful for locating steam when it has
@ -176,8 +173,7 @@ class winesteam(wine.wine):
parts = value.split("\"")
return parts[1].strip('\\')
@property
def steam_config(self):
def get_steam_config(self):
"""Return the "Steam" part of Steam's config.vfd as a dict"""
steam_data_dir = self.steam_data_dir
if not steam_data_dir:
@ -224,17 +220,30 @@ class winesteam(wine.wine):
if path:
return path
def install(self, installer_path=None, version=None):
def install(self, version=None, downloader=None, callback=None):
installer_path = get_steam_installer_dest()
def on_steam_downloaded(*args):
prefix = self.get_or_create_default_prefix()
self.msi_exec(installer_path,
quiet=True,
prefix=prefix,
wine_path=self.get_executable(),
working_dir="/tmp",
blocking=True)
if callback:
callback()
if not self.is_wine_installed():
wine.wine().install(version=version)
prefix = self.get_or_create_default_prefix()
if not self.get_steam_path():
if not installer_path:
installer_path = get_steam_installer_dest()
download_steam()
self.msi_exec(installer_path, quiet=True, prefix=prefix,
wine_path=self.get_executable())
return True
# FIXME find another way to do that (already fixed from the install game
# dialog)
wine.wine().install(version=version, downloader=downloader)
if downloader:
downloader(STEAM_INSTALLER_URL, installer_path, on_steam_downloaded)
else:
dialog = DownloadDialog(STEAM_INSTALLER_URL, installer_path)
dialog.run()
on_steam_downloaded()
def is_wine_installed(self):
return super(winesteam, self).is_installed()
@ -252,9 +261,9 @@ class winesteam(wine.wine):
def get_appid_list(self):
"""Return the list of appids of all user's games"""
config = self.steam_config
if config:
apps = config['apps']
steam_config = self.get_steam_config()
if steam_config:
apps = steam_config['apps']
return apps.keys()
def get_game_path_from_appid(self, appid):
@ -276,7 +285,7 @@ class winesteam(wine.wine):
if main_dir and os.path.isdir(main_dir):
dirs.append(main_dir)
# Custom dirs
steam_config = self.steam_config
steam_config = self.get_steam_config()
if steam_config:
i = 1
while ('BaseInstallFolder_%s' % i) in steam_config:
@ -319,10 +328,14 @@ class winesteam(wine.wine):
return default_prefix
def install_game(self, appid, generate_acf=False):
if not appid:
raise ValueError("Missing appid in winesteam.install_game")
command = self.launch_args + ["steam://install/%s" % appid]
subprocess.Popen(command, env=self.get_env())
def validate_game(self, appid):
if not appid:
raise ValueError("Missing appid in winesteam.validate_game")
command = self.launch_args + ["steam://validate/%s" % appid]
subprocess.Popen(command, env=self.get_env())
@ -381,12 +394,8 @@ class winesteam(wine.wine):
def shutdown(self):
"""Shutdown Steam in a clean way."""
pid = system.get_pid('Steam.exe$')
if not pid:
return
p = subprocess.Popen(self.launch_args + ['-shutdown'],
env=self.get_env())
p.wait()
wineserver = self.get_executable() + 'server'
subprocess.Popen([wineserver, '-k'], env=self.get_env())
def stop(self):
if self.runner_config.get('quit_steam_on_exit'):

134
lutris/runners/zdoom.py Normal file
View file

@ -0,0 +1,134 @@
import os
from lutris import settings
from lutris.runners.runner import Runner
# ZDoom Runner
# http://zdoom.org/wiki/Command_line_parameters
class zdoom(Runner):
description = "ZDoom DOOM Game Engine"
human_name = "ZDoom"
platform = "PC"
game_options = [
{
'option': 'main_file',
'type': 'file',
'label': 'WAD file',
'help': ("The game data, commonly called a WAD file.")
},
{
'option': 'file',
'type': 'string',
'label': 'PWAD file',
'help': ("Used to load one or more PWAD files which generally contain user-created levels.")
},
{
'option': 'warp',
'type': 'string',
'label': 'Warp to map',
'help': ("Starts the game on the given map.")
}
]
runner_options = [
{
"option": "2",
"label": "Pixel Doubling",
"type": "bool",
'default': False
},
{
"option": "4",
"label": "Pixel Quadrupling",
"type": "bool",
'default': False
},
{
"option": "nostartup",
"label": "Disable Startup Screens",
"type": "bool",
'default': False
},
{
"option": "nosound",
"label": "Disable Both Music and Sound Effects",
"type": "bool",
'default': False,
},
{
"option": "nosfx",
"label": "Disable Sound Effects",
"type": "bool",
'default': False
},
{
"option": "nomusic",
"label": "Disable Music",
"type": "bool",
'default': False
},
{
"option": "skill",
"label": "Skill",
"type": "choice",
"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'),
}
}
]
def get_executable(self):
return os.path.join(settings.RUNNER_DIR, 'zdoom')
@property
def working_dir(self):
# Run in the installed game's directory.
return self.game_path
def play(self):
command = [
self.get_executable()
]
resolution = self.runner_config.get("resolution")
if resolution:
if resolution == 'desktop':
resolution = display.get_current_resolution()
width, height = resolution.split('x')
command.append("-width %s" % width)
command.append("-height %s" % height)
# Append any boolean options.
boolOptions = ['nomusic', 'nosfx', 'nosound', '2', '4', 'nostartup']
for option in boolOptions:
if self.runner_config.get(option):
command.append("-%s" % option)
# Append the skill level.
skill = self.runner_config.get('skill')
if skill:
command.append("-skill %s" % skill)
# Append the warp map.
warp = self.game_config.get('warp')
if warp:
command.append("-warp %s" % warp)
# Append the wad file to load, if provided.
wad = self.game_config.get('main_file')
if wad:
command.append("-iwad %s" % wad)
# Append the pwad files to load, if provided.
pwad = self.game_config.get('file')
if pwad:
command.append("-file %s" % pwad)
# TODO: Find out why the paths are not found correctly. Something wrong with working_dir?
print(command)
return {'command': command}

View file

@ -5,7 +5,7 @@ from gi.repository import GLib
from lutris.util.settings import SettingsIO
PROJECT = "Lutris"
VERSION = "0.3.7.3"
VERSION = "0.3.8"
COPYRIGHT = "(c) 2010-2016 Lutris Gaming Platform"
AUTHORS = ["Mathieu Comandon <strycore@gmail.com>",
"Pascal Reinhard (Xodetaetl) <dev@xod.me"]

View file

@ -1,14 +1,7 @@
# -*- coding:Utf-8 -*-
"""Synchronization of the game library with server and local data."""
import os
import re
from lutris import api, config, pga
from lutris.runners.steam import steam
from lutris.runners.winesteam import winesteam
from lutris import api, pga
from lutris.util import resources
from lutris.util.log import logger
from lutris.util.steam import vdf_parse
class Sync(object):
@ -17,13 +10,7 @@ class Sync(object):
def sync_all(self):
added, updated = self.sync_from_remote()
installed, uninstalled = self.sync_local()
return added, updated, installed, uninstalled
def sync_local(self):
"""Synchronize games state with local third parties."""
installed, uninstalled = self.sync_steam_local()
return installed, uninstalled
return added, updated
def sync_from_remote(self):
"""Synchronize from remote to local library.
@ -135,86 +122,3 @@ class Sync(object):
logger.debug("%d games updated", len(updated))
return updated
def sync_steam_local(self):
"""Sync Steam games in library with Steam and Wine Steam"""
steamrunner = steam()
winesteamrunner = winesteam()
installed = set()
uninstalled = set()
# Get installed steamapps
installed_steamapps = self.get_installed_steamapps(steamrunner)
installed_winesteamapps = self.get_installed_steamapps(winesteamrunner)
for game_info in self.library:
slug = game_info['slug']
runner = game_info['runner']
steamid = game_info['steamid']
installed_in_steam = steamid in installed_steamapps
installed_in_winesteam = steamid in installed_winesteamapps
# Set installed
if not game_info['installed']:
if not installed_in_steam: # (Linux Steam only)
continue
logger.debug("Setting %s as installed" % game_info['name'])
config_id = (game_info['configpath']
or config.make_game_config_id(slug))
game_id = pga.add_or_update(
name=game_info['name'],
runner='steam',
slug=slug,
installed=1,
configpath=config_id,
)
game_config = config.LutrisConfig(
runner_slug='steam',
game_config_id=config_id,
)
game_config.raw_game_config.update({'appid': str(steamid)})
game_config.save()
installed.add(game_id)
# Set uninstalled
elif not (installed_in_steam or installed_in_winesteam):
if runner not in ['steam', 'winesteam']:
continue
if runner == 'steam' and not steamrunner.is_installed():
continue
if runner == 'winesteam' and not winesteamrunner.is_installed():
continue
logger.debug("Setting %(name)s (%(steamid)s) as uninstalled", game_info)
game_id = pga.add_or_update(
name=game_info['name'],
runner='',
slug=game_info['slug'],
installed=0
)
uninstalled.add(game_id)
return (installed, uninstalled)
@staticmethod
def get_installed_steamapps(runner):
"""Return a list of appIDs of the installed Steam games."""
if not runner.is_installed():
return []
installed = []
dirs = runner.get_steamapps_dirs()
for dirname in dirs:
appmanifests = [f for f in os.listdir(dirname)
if re.match(r'^appmanifest_\d+.acf$', f)]
for filename in appmanifests:
basename, ext = os.path.splitext(filename)
steamid = int(basename[12:])
appmanifest_path = os.path.join(
dirname, "appmanifest_%s.acf" % str(steamid)
)
with open(appmanifest_path, "r") as appmanifest_file:
appmanifest = vdf_parse(appmanifest_file, {})
appstate = appmanifest.get('AppState') or {}
is_installed = appstate.get('LastOwner') or '0'
if not is_installed == '0':
installed.append(steamid)
return installed

View file

@ -114,6 +114,14 @@ system_options = [
'help': ("Command line instructions to add in front of the game's "
"execution command.")
},
{
'option': 'single_cpu',
'type': 'bool',
'label': 'Restrict to single core',
'advanced': True,
'default': False,
'help': "Restrict the game to a single CPU core."
},
{
'option': 'disable_runtime',
'type': 'bool',
@ -133,6 +141,17 @@ system_options = [
'condition': system.find_executable('pulseaudio'),
'help': "Restart PulseAudio before launching the game."
},
{
'option': 'pulse_latency',
'type': 'bool',
'label': 'Reduce PulseAudio latency',
'default': False,
'advanced': True,
'condition': system.find_executable('pulseaudio'),
'help': ('Set the environment variable PULSE_LATENCY_MSEC=60 to improve '
'audio quality on some games')
},
{
'option': 'killswitch',
'type': 'string',
@ -150,6 +169,25 @@ system_options = [
'condition': system.find_executable('xboxdrv'),
'help': ("Command line options for xboxdrv, a driver for XBOX 360"
"controllers. Requires the xboxdrv package installed.")
},
{
'option': 'xephyr',
'type': 'choice',
'label': "Use Xephyr",
'type': 'choice',
'choices': (
('Off', 'off'),
('8BPP (256 colors)', '8bpp'),
('16BPP (65536 colors)', '16bpp')
),
'default': 'off',
'advanced': True,
'help': "Run program in Xephyr to support 8BPP and 16BPP color modes",
},
{
'option': 'xephyr_resolution',
'type': 'string',
'label': 'Xephyr resolution'
}
]

View file

@ -42,6 +42,8 @@ class LutrisThread(threading.Thread):
self.max_cycles_without_children = 15
self.startup_time = time.time()
self.monitoring_started = False
self.daemon = True
self.error = None
if cwd:
self.cwd = cwd
@ -77,6 +79,8 @@ class LutrisThread(threading.Thread):
env = os.environ.copy()
env.update(self.env)
self.game_process = self.execute_process(self.command, env)
if not self.game_process:
return
for line in iter(self.game_process.stdout.readline, ''):
self.stdout += line
if self.debug_output:
@ -105,9 +109,12 @@ class LutrisThread(threading.Thread):
self.game_process = self.execute_process([self.terminal, '-e', file_path])
def execute_process(self, command, env=None):
return subprocess.Popen(command, bufsize=1,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
cwd=self.cwd, env=env)
try:
return subprocess.Popen(command, bufsize=1,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
cwd=self.cwd, env=env)
except OSError as ex:
self.error = ex.strerror
def iter_children(self, process, topdown=True, first=True):
if self.runner and self.runner.name.startswith('wine') and first:
@ -147,6 +154,9 @@ class LutrisThread(threading.Thread):
def watch_children(self):
"""Poke at the running process(es)."""
if not self.game_process:
logger.error('No game process available')
return False
process = Process(self.rootpid)
num_children = 0
num_watched_children = 0
@ -166,7 +176,7 @@ class LutrisThread(threading.Thread):
'bash', 'control', 'lutris', 'PnkBstrA.exe', 'python', 'regedit',
'sh', 'steam', 'Steam.exe', 'steamer', 'steamerrorrepor',
'SteamService.ex', 'steamwebhelper', 'steamwebhelper.', 'tee',
'tr', 'winecfg.exe', 'zenity',
'tr', 'winecfg.exe', 'zenity', 'wdfmgr.exe'
)
if child.name in excluded:
continue

View file

@ -10,14 +10,14 @@ def get_mounted_discs():
vm = Gio.VolumeMonitor.get()
drives = []
for m in vm.get_mounts():
if m.get_volume():
device = m.get_volume().get_identifier('unix-device')
for mount in vm.get_mounts():
if mount.get_volume():
device = mount.get_volume().get_identifier('unix-device')
if not device:
logger.debug("No device for mount %s", m.get_name())
logger.debug("No device for mount %s", mount.get_name())
continue
# Device is a disk drive or ISO image
if '/dev/sr' in device or '/dev/loop' in device:
drives.append(m)
drives.append(mount.get_root().get_path())
return drives

View file

@ -17,7 +17,7 @@ class AsyncCall(threading.Thread):
kwargs=kwargs)
self.function = function
self.on_done = on_done if on_done else lambda r, e: None
self.daemon = kwargs.pop('daemon', False)
self.daemon = kwargs.pop('daemon', True)
self.start()

29
lutris/util/joypad.py Normal file
View file

@ -0,0 +1,29 @@
try:
import evdev
except ImportError:
evdev = None
def get_joypads():
"""Return a list of tuples with the device and the joypad name"""
if not evdev:
return []
device_names = evdev.list_devices()
return [(dev, evdev.InputDevice(dev).name) for dev in device_names]
def read_button(device):
"""Reference function for reading controller buttons and axis values.
Not to be used as is.
"""
for event in device.read_loop():
if event.type == evdev.ecodes.EV_KEY and event.value == 0:
print "button %s (%s): %s" % (event.code, hex(event.code), event.value)
if event.type == evdev.ecodes.EV_ABS:
sticks = (0, 1, 3, 4)
if event.code not in sticks or abs(event.value) > 5000:
print "axis %s (%s): %s" % (event.code, hex(event.code), event.value)
# Unreacheable return statement, to return the even, place a 'break' in the
# for loop
return event

View file

@ -81,6 +81,20 @@ def db_select(db_path, table, fields=None, condition=None):
return results
def db_query(db_path, query, params=()):
with db_cursor(db_path) as cursor:
cursor.execute(query, params)
rows = cursor.fetchall()
column_names = [column[0] for column in cursor.description]
results = []
for row in rows:
row_data = {}
for index, column in enumerate(column_names):
row_data[column] = row[index]
results.append(row_data)
return results
def _decode_utf8_values(values_list):
"""Return a tuple of values with UTF-8 string values being decoded.
XXX Might be obsolete in Python3 (Removed the decoding part)

View file

@ -1,8 +1,19 @@
import os
import re
import time
from lutris.util.log import logger
import threading
try:
import pyinotify
from pyinotify import ProcessEvent
except ImportError:
pyinotify = None
ProcessEvent = object
from collections import OrderedDict
from lutris import pga
from lutris.util.log import logger
from lutris.util.system import fix_path_case
from lutris.util.strings import slugify
from lutris.config import make_game_config_id, LutrisConfig
APP_STATE_FLAGS = [
@ -98,48 +109,26 @@ def read_config(steam_data_dir):
return config
def get_manifest_info(steamapps_path, appid):
"""Given the steam apps path and appid, return the corresponding
appmanifest info."""
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):
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)
appmanifest_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid)
if not os.path.exists(appmanifest_path):
return {}
with open(appmanifest_path, "r") as appmanifest_file:
config = vdf_parse(appmanifest_file, {})
return config
return
return AppManifest(appmanifest_path)
def get_path_from_appmanifest(steamapps_path, appid):
"""Return the path where a Steam game is installed."""
config = get_manifest_info(steamapps_path, appid)
if not config:
appmanifest = get_appmanifest_from_appid(steamapps_path, appid)
if not appmanifest:
return
installdir = config.get('AppState', {}).get('installdir')
install_path = fix_path_case(os.path.join(steamapps_path, "common",
installdir))
if install_path and os.path.exists(install_path):
return install_path
def get_app_states(steamapps_path, appid):
"""Return the states of a Steam game."""
states = []
if not steamapps_path:
return states
manifest_info = get_manifest_info(steamapps_path, appid)
state_flags = manifest_info.get('AppState', {}).get('StateFlags', 0)
state_flags = bin(int(state_flags))[:1:-1]
for index, flag in enumerate(state_flags):
if flag == '1':
states.append(APP_STATE_FLAGS[index + 1])
return states
return appmanifest.get_install_path()
def _get_last_content_log(steam_data_dir):
@ -197,3 +186,221 @@ def get_app_state_log(steam_data_dir, appid, start_time=None):
if line[0].endswith("state changed"):
state_log.append(line[1][:-2])
return state_log
def get_appmanifests(steamapps_path):
"""Return the list for all appmanifest files in a Steam library folder"""
return [f for f in os.listdir(steamapps_path)
if re.match(r'^appmanifest_\d+.acf$', f)]
def get_steamapps_paths(flat=False):
from lutris.runners import winesteam, steam
if flat:
steamapps_paths = []
else:
steamapps_paths = {
'linux': [],
'windows': []
}
winesteam_runner = winesteam.winesteam()
steam_runner = steam.steam()
for folder in steam_runner.get_steamapps_dirs():
if flat:
steamapps_paths.append(folder)
else:
steamapps_paths['linux'].append(folder)
for folder in winesteam_runner.get_steamapps_dirs():
if flat:
steamapps_paths.append(folder)
else:
steamapps_paths['windows'].append(folder)
return steamapps_paths
def mark_as_installed(steamid, runner_name, game_info):
for key in ['name', 'slug']:
assert game_info[key]
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(
steamid=int(steamid),
name=game_info['name'],
runner=runner_name,
slug=game_info['slug'],
installed=1,
configpath=config_id,
)
game_config = LutrisConfig(
runner_slug=runner_name,
game_config_id=config_id,
)
game_config.raw_game_config.update({'appid': steamid})
game_config.save()
return game_id
def mark_as_uninstalled(game_info):
assert 'id' in game_info
assert 'name' in game_info
logger.info('Setting %s as uninstalled' % game_info['name'])
game_id = pga.add_or_update(
id=game_info['id'],
runner='',
installed=0
)
return game_id
def sync_with_lutris():
steamapps_paths = get_steamapps_paths()
steam_games_in_lutris = pga.get_steam_games()
steamids_in_lutris = set([str(game['steamid']) for game in steam_games_in_lutris])
seen_ids = set()
for platform in steamapps_paths:
for steamapps_path in steamapps_paths[platform]:
appmanifests = get_appmanifests(steamapps_path)
for appmanifest_file in appmanifests:
steamid = re.findall(r'(\d+)', appmanifest_file)[0]
seen_ids.add(steamid)
game_info = None
if steamid not in steamids_in_lutris and platform == 'linux':
appmanifest_path = os.path.join(steamapps_path, appmanifest_file)
appmanifest = AppManifest(appmanifest_path)
if appmanifest.is_installed():
game_info = {
'name': appmanifest.name,
'slug': appmanifest.slug,
}
mark_as_installed(steamid, 'steam', game_info)
else:
for game in steam_games_in_lutris:
if str(game['steamid']) == steamid and not game['installed']:
game_info = game
break
if game_info:
appmanifest_path = os.path.join(steamapps_path, appmanifest_file)
appmanifest = AppManifest(appmanifest_path)
if appmanifest.is_installed():
runner_name = appmanifest.get_runner_name()
mark_as_installed(steamid, runner_name, game_info)
unavailable_ids = steamids_in_lutris.difference(seen_ids)
for steamid in unavailable_ids:
for game in steam_games_in_lutris:
if str(game['steamid']) == steamid \
and game['installed'] \
and game['runner'] in ('steam', 'winesteam'):
mark_as_uninstalled(game)
class SteamWatchHandler(ProcessEvent):
def __init__(self, callback):
self.callback = callback
def process_IN_MODIFY(self, event):
self.process_event('MODIFY', event.pathname)
def process_IN_CREATE(self, event):
self.process_event('CREATE', event.pathname)
def process_IN_DELETE(self, event):
self.process_event('DELETE', event.pathname)
def process_event(self, event_type, path):
if not path.endswith('.acf'):
return
self.callback(event_type, path)
class SteamWatcher(threading.Thread):
def __init__(self, steamapps_paths, callback=None):
self.notifier = None
if not pyinotify:
logger.error("pyinotify is not installed, "
"Lutris won't keep track of steam games")
else:
self.steamapps_paths = steamapps_paths
self.callback = callback
super(SteamWatcher, self).__init__()
self.daemon = True
self.start()
def run(self):
watch_manager = pyinotify.WatchManager()
event_handler = SteamWatchHandler(self.callback)
mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY
self.notifier = pyinotify.Notifier(watch_manager, event_handler)
for steamapp_path in self.steamapps_paths:
logger.info('Watching Steam folder %s', steamapp_path)
watch_manager.add_watch(steamapp_path, mask, rec=False)
self.notifier.loop()
def stop(self):
if self.notifier:
self.notifier.stop()
print self.notifier
class AppManifest:
def __init__(self, appmanifest_path):
self.steamapps_path, filename = os.path.split(appmanifest_path)
self.steamid = re.findall(r'(\d+)', filename)[0]
if os.path.exists(appmanifest_path):
with open(appmanifest_path, "r") as appmanifest_file:
self.appmanifest_data = vdf_parse(appmanifest_file, {})
@property
def app_state(self):
return self.appmanifest_data.get('AppState') or {}
@property
def name(self):
return self.app_state.get('name')
@property
def slug(self):
return slugify(self.name)
@property
def installdir(self):
return self.app_state.get('installdir')
@property
def states(self):
"""Return the states of a Steam game."""
states = []
state_flags = self.app_state.get('StateFlags', 0)
state_flags = bin(int(state_flags))[:1:-1]
for index, flag in enumerate(state_flags):
if flag == '1':
states.append(APP_STATE_FLAGS[index + 1])
return states
def is_installed(self):
return 'Fully Installed' in self.states
def get_install_path(self):
if not self.installdir:
return
install_path = fix_path_case(os.path.join(self.steamapps_path, "common",
self.installdir))
if install_path:
return install_path
def get_platform(self):
steamapps_paths = get_steamapps_paths()
if self.steamapps_path in steamapps_paths['linux']:
return 'linux'
elif self.steamapps_path in steamapps_paths['windows']:
return 'windows'
else:
raise ValueError("Can't find %s in %s"
% (self.steamapps_path, steamapps_paths))
def get_runner_name(self):
platform = self.get_platform()
if platform == 'linux':
return 'steam'
else:
return 'winesteam'

View file

@ -24,18 +24,23 @@ def execute(command, env=None, cwd=None, log_errors=False):
# Piping stderr can cause slowness in the programs, use carefully
# (especially when using regedit with wine)
if log_errors:
stderr_config = subprocess.PIPE
stderr_handler = subprocess.PIPE
stderr_needs_closing = False
else:
stderr_config = None
stderr_handler = open(os.devnull, 'w')
stderr_needs_closing = True
try:
stdout, stderr = subprocess.Popen(command,
shell=False,
stdout=subprocess.PIPE,
stderr=stderr_config,
stderr=stderr_handler,
env=existing_env, cwd=cwd).communicate()
except OSError as ex:
logger.error('Could not run command %s: %s', command, ex)
return
finally:
if stderr_needs_closing:
stderr_handler.close()
if stderr and log_errors:
logger.error(stderr)
return stdout.strip()
@ -146,6 +151,13 @@ def remove_folder(path):
shutil.rmtree(path)
def create_folder(path):
path = os.path.expanduser(path)
if not os.path.exists(path):
os.makedirs(path)
return path
def is_removeable(path, excludes=None):
"""Check if a folder is safe to remove (not system or home, ...)"""
if not path:

View file

@ -2,7 +2,7 @@
Version=1.0
Name=Lutris
Comment=Lutris application
Categories=Game;Network;
Categories=Game;
Exec=lutris %U
Icon=lutris
Terminal=false

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -3,62 +3,6 @@
<interface>
<requires lib="gtk+" version="3.10"/>
<!-- interface-local-resource-path ../media -->
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">preferences-desktop</property>
<property name="use_fallback">True</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">text-x-generic</property>
<property name="use_fallback">True</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">application-exit</property>
<property name="use_fallback">True</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">media-playback-start</property>
<property name="use_fallback">True</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="image5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">media-playback-stop</property>
<property name="use_fallback">True</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="image6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-add</property>
<property name="use_fallback">True</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="image7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-remove</property>
<property name="use_fallback">True</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="image8">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">help-about</property>
<property name="use_fallback">True</property>
<property name="icon_size">1</property>
</object>
<object class="GtkRadioAction" id="radioaction1">
<property name="hide_if_empty">False</property>
<property name="draw_as_radio">True</property>
@ -67,22 +11,19 @@
<object class="GtkImage" id="view_grid_symbolic">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xpad">4</property>
<property name="pixel_size">16</property>
<property name="icon_name">view-grid-symbolic</property>
</object>
<object class="GtkImage" id="view_list_symbolic">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xpad">4</property>
<property name="pixel_size">16</property>
<property name="icon_name">view-list-symbolic</property>
</object>
<object class="GtkWindow" id="window">
<property name="width_request">640</property>
<property name="height_request">300</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Lutris</property>
<property name="window_position">center</property>
<property name="icon">../media/lutris.svg</property>
<property name="icon_name">lutris</property>
<signal name="destroy" handler="on_destroy" swapped="no"/>
@ -170,27 +111,23 @@
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="preferences_menuitem">
<property name="label" translatable="yes">_Preferences</property>
<object class="GtkMenuItem" id="preferences_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="has_tooltip">True</property>
<property name="tooltip_markup" translatable="yes">configure the default options</property>
<property name="tooltip_text" translatable="yes">configure the default options</property>
<property name="label" translatable="yes">_Preferences</property>
<property name="use_underline">True</property>
<property name="image">image1</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_preferences_activate" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="quit_menuitem">
<property name="label">_Quit</property>
<object class="GtkMenuItem" id="quit_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">_Quit</property>
<property name="use_underline">True</property>
<property name="image">image3</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_destroy" swapped="no"/>
</object>
</child>
@ -301,6 +238,15 @@
</child>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="dark_theme_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Use _dark theme</property>
<property name="use_underline">True</property>
<signal name="toggled" handler="on_dark_theme_toggled" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="separatormenuitem2">
<property name="visible">True</property>
@ -333,24 +279,20 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="playgame_menuitem">
<property name="label" translatable="yes">_Play</property>
<object class="GtkMenuItem" id="playgame_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Play</property>
<property name="use_underline">True</property>
<property name="image">image4</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_game_run" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="stopgame_menuitem">
<property name="label">_Stop</property>
<object class="GtkMenuItem" id="stopgame_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">_Stop</property>
<property name="use_underline">True</property>
<property name="image">image5</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_game_stop" swapped="no"/>
</object>
</child>
@ -361,24 +303,20 @@
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="addgame_menuitem">
<property name="label">_Add</property>
<object class="GtkMenuItem" id="addgame_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">_Add</property>
<property name="use_underline">True</property>
<property name="image">image6</property>
<property name="use_stock">False</property>
<signal name="activate" handler="add_game" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="removegame_menuitem">
<property name="label">_Remove</property>
<object class="GtkMenuItem" id="removegame_menuitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">_Remove</property>
<property name="use_underline">True</property>
<property name="image">image7</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_remove_game" swapped="no"/>
</object>
</child>
@ -389,13 +327,11 @@
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="view_game_log">
<property name="label" translatable="yes">View last game's _log</property>
<object class="GtkMenuItem" id="view_game_log">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">View last game's _log</property>
<property name="use_underline">True</property>
<property name="image">image2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_view_game_log_activate" swapped="no"/>
</object>
</child>
@ -414,13 +350,11 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="imagemenuitem10">
<property name="label">_About</property>
<object class="GtkMenuItem" id="imagemenuitem10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">_About</property>
<property name="use_underline">True</property>
<property name="image">image8</property>
<property name="use_stock">False</property>
<signal name="activate" handler="about" swapped="no"/>
</object>
</child>
@ -512,7 +446,6 @@
<object class="GtkToolItem" id="search_area">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">10</property>
<child>
<object class="GtkEntry" id="search_entry">
<property name="visible">True</property>
@ -549,7 +482,7 @@
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Grid view</property>
<property name="image">view_grid_symbolic</property>
<property name="xalign">0</property>
<property name="xalign">0.5</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_viewbtn_toggled" swapped="no"/>
@ -567,7 +500,7 @@
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">List view</property>
<property name="image">view_list_symbolic</property>
<property name="xalign">0</property>
<property name="xalign">0.5</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="group">switch_grid_view_btn</property>
@ -597,100 +530,90 @@
</packing>
</child>
<child>
<object class="GtkAlignment" id="splash_alignment">
<object class="GtkBox" id="splash_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">50</property>
<property name="bottom_padding">50</property>
<property name="left_padding">150</property>
<property name="right_padding">150</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<property name="baseline_position">top</property>
<child>
<object class="GtkBox" id="splash_box">
<object class="GtkLabel" id="title_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<property name="baseline_position">top</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Welcome to Lutris</property>
<attributes>
<attribute name="scale" value="2"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">There are no games currently installed, you can start adding some by:</property>
<property name="lines">4</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLinkButton" id="add_game_link">
<property name="label" translatable="yes">Manually adding a game installed on your hard drive</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="resize_mode">queue</property>
<property name="relief">none</property>
<signal name="activate-link" handler="add_game" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLinkButton" id="connect_link">
<property name="label" translatable="yes">Connecting to your Lutris.net account to sync your library</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<signal name="activate-link" handler="on_connect" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkLinkButton" id="website_link">
<property name="label" translatable="yes">Browsing Lutris.net for games to install</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<property name="uri">http://lutris.net/games/</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">4</property>
</packing>
</child>
<property name="label" translatable="yes">Welcome to Lutris</property>
<attributes>
<attribute name="scale" value="2"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">There are no games currently installed, you can start adding some by:</property>
<property name="lines">4</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLinkButton" id="add_game_link">
<property name="label" translatable="yes">Manually adding a game installed on your hard drive</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<signal name="activate-link" handler="add_game" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLinkButton" id="connect_link">
<property name="label" translatable="yes">Connecting to your Lutris.net account to sync your library</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<signal name="activate-link" handler="on_connect" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkLinkButton" id="website_link">
<property name="label" translatable="yes">Browsing Lutris.net for games to install</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<property name="uri">http://lutris.net/games/</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="padding">40</property>
<property name="position">2</property>
</packing>
</child>
@ -751,7 +674,6 @@
<child>
<object class="GtkImage" id="js1image">
<property name="can_focus">False</property>
<property name="xpad">2</property>
<property name="pixbuf">../media/gamepad.png</property>
</object>
<packing>
@ -763,7 +685,6 @@
<child>
<object class="GtkImage" id="js0image">
<property name="can_focus">False</property>
<property name="xpad">2</property>
<property name="pixbuf">../media/gamepad.png</property>
</object>
<packing>
@ -775,7 +696,6 @@
<child>
<object class="GtkImage" id="js3image">
<property name="can_focus">False</property>
<property name="xpad">2</property>
<property name="pixbuf">../media/gamepad.png</property>
</object>
<packing>
@ -787,7 +707,6 @@
<child>
<object class="GtkImage" id="js2image">
<property name="can_focus">False</property>
<property name="xpad">2</property>
<property name="pixbuf">../media/gamepad.png</property>
</object>
<packing>
@ -800,11 +719,8 @@
<object class="GtkLabel" id="status_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xpad">5</property>
<property name="ypad">5</property>
<property name="label" translatable="yes">Lutris</property>
<property name="lines">1</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
@ -817,7 +733,6 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">label</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="expand">True</property>

View file

@ -40,7 +40,6 @@
<property name="has_default">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
<property name="yalign">0.56000000238418579</property>
<signal name="clicked" handler="on_open_downloads_clicked" swapped="no"/>
</object>
<packing>
@ -61,8 +60,6 @@
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xpad">40</property>
<property name="ypad">30</property>
<property name="label" translatable="yes">A new version of Lutris is available!</property>
</object>
<packing>

View file

@ -1,32 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.16.1 -->
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<object class="GtkImage" id="image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-wired</property>
<property name="use_fallback">True</property>
</object>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">dialog-cancel</property>
<property name="use_fallback">True</property>
</object>
<object class="GtkDialog" id="lutris-login">
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="title" translatable="yes">Connect to lutris.net</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox1">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="spacing">15</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area1">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<property name="homogeneous">True</property>
<property name="layout_style">center</property>
<child>
<object class="GtkLinkButton" id="linkbutton1">
<property name="label" translatable="yes">Forgot password?</property>
@ -34,6 +28,7 @@
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="has_tooltip">True</property>
<property name="valign">center</property>
<property name="relief">none</property>
<property name="uri">http://lutris.net/user/password/reset/</property>
</object>
@ -49,11 +44,10 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">image2</property>
<property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
@ -66,11 +60,13 @@
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">True</property>
<property name="image">image1</property>
<property name="use_underline">True</property>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
@ -84,106 +80,80 @@
</packing>
</child>
<child>
<object class="GtkFrame" id="frame1">
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<property name="valign">start</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkAlignment" id="alignment1">
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">15</property>
<property name="bottom_padding">15</property>
<property name="left_padding">15</property>
<property name="right_padding">15</property>
<child>
<object class="GtkGrid" id="grid1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_spacing">14</property>
<property name="row_homogeneous">True</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Username</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Password</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="username_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="hexpand">True</property>
<property name="max_length">30</property>
<property name="invisible_char">•</property>
<property name="width_chars">32</property>
<property name="shadow_type">none</property>
<signal name="activate" handler="on_username_entry_activate" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="max_length">1024</property>
<property name="visibility">False</property>
<property name="invisible_char">•</property>
<signal name="activate" handler="on_password_entry_activate" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
</object>
</child>
<property name="halign">end</property>
<property name="margin_left">83</property>
<property name="label" translatable="yes">Password</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child type="label">
<object class="GtkLabel" id="label1">
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Login to lutris.net&lt;/b&gt;</property>
<property name="use_markup">True</property>
<property name="halign">end</property>
<property name="margin_left">83</property>
<property name="label" translatable="yes">Username</property>
<property name="justify">right</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="max_length">1024</property>
<property name="visibility">False</property>
<property name="invisible_char">•</property>
<property name="width_chars">36</property>
<property name="input_purpose">password</property>
<signal name="activate" handler="on_password_entry_activate" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="username_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="max_length">30</property>
<property name="invisible_char">•</property>
<property name="width_chars">36</property>
<property name="shadow_type">none</property>
<signal name="activate" handler="on_username_entry_activate" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">0</property>
</packing>
</child>
</object>

View file

@ -76,7 +76,6 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="ypad">5</property>
<property name="label" translatable="yes">&lt;b&gt;Personnal Game Archives sources&lt;/b&gt;</property>
<property name="use_markup">True</property>
</object>

View file

@ -48,4 +48,4 @@ class TestScriptInterpreter(TestCase):
{'_substitute': 'foo'}
)
self.assertEqual(ex.exception.message,
"The command substitute does not exists")
"The command \"substitute\" does not exist.")