Sync Python 3 branch
2
AUTHORS
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
4
Makefile
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
39
bin/lutris
|
@ -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
|
@ -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
|
@ -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
|
||||
|
|
2
debian/source/format
vendored
|
@ -1 +1 @@
|
|||
3.0 (quilt)
|
||||
3.0 (native)
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
10
lutris.spec
|
@ -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
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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}
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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
|
@ -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}
|
|
@ -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"]
|
||||
|
|
100
lutris/sync.py
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Version=1.0
|
||||
Name=Lutris
|
||||
Comment=Lutris application
|
||||
Categories=Game;Network;
|
||||
Categories=Game;
|
||||
Exec=lutris %U
|
||||
Icon=lutris
|
||||
Terminal=false
|
||||
|
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 40 KiB |
BIN
share/lutris/media/runner_icons/citra.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
share/lutris/media/runner_icons/desmume.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
share/lutris/media/runner_icons/zdoom.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"><b>Login to lutris.net</b></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>
|
||||
|
|
|
@ -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"><b>Personnal Game Archives sources</b></property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
|
|
|
@ -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.")
|
||||
|
|