Merge branch 'master' into next

This commit is contained in:
Mathieu Comandon 2017-12-12 01:12:40 -08:00
commit bc299cb934
35 changed files with 4103 additions and 3221 deletions

View file

@ -146,7 +146,7 @@ are yet to be implemented!
Here's what to expect from the future versions of lutris:
* Integration with GOG and Humble Bundle
* Integration with the TOSEC databse
* Integration with the TOSEC database
* Management of Personnal Game Archives (let you store your games files on
private storage, allowing you to reinstall them on all your devices)
* Game saves sync
@ -162,7 +162,7 @@ pre-releases or simply chat with the developers?
You can always reach us on:
* IRC: #lutris on the Freenode servers
* Github: http://github.com/lutris
* Github: https://github.com/lutris
* Twitter: https://twitter.com/LutrisGaming
* Google+: https://plus.google.com/+LutrisNet
* Email: contact@lutris.net

View file

@ -24,7 +24,11 @@ if LAUNCH_PATH != "/usr/bin":
SOURCE_PATH = normpath(os.path.join(LAUNCH_PATH, '..'))
sys.path.insert(0, SOURCE_PATH)
locale.setlocale(locale.LC_ALL, locale.getlocale())
try:
locale_name = locale.getlocale()
locale.setlocale(locale.LC_ALL, locale_name)
except locale.Error:
sys.stderr.write("Unsupported locale %s\n" % locale_name)
from lutris.gui.application import Application

28
debian/changelog vendored
View file

@ -1,3 +1,31 @@
lutris (0.4.14) artful; urgency=medium
* Add option to include and exclude processes from monitoring in installers
and during gameplay.
* Add winekill installer task
* Fix lutris eating 100% CPU after game quits
* Fix the way wine games quit
* Fix Wine Steam being killed on game exit even if the option is disabled
* Add support for 64bit dinput8.dll for x360ce
* Add support for dumbxinputemu as a x360ce alternative
* Add option to enable xinput9_1_0.dll in x360ce
* Deprecate koku-xinput option
* Add system option to enable DRI_PRIME
* Add more platforms to Mednafen
* Better controller support for Mednafen
-- Mathieu Comandon <strycore@gmail.com> Tue, 21 Nov 2017 20:48:38 -0800
lutris (0.4.13) zesty; urgency=medium
* Add new libretro cores
* Stop process monitoring as soon as process stops
* Default 'reset_desktop' option to False
* Make calling executables more robust
* Fix xboxdrv not being monitored properly
-- Mathieu Comandon <strycore@gmail.com> Wed, 26 Jul 2017 19:12:21 -0700
lutris (0.4.12) zesty; urgency=medium
* Increase process monitor delay

View file

@ -2,6 +2,8 @@
Writing installers
==================
See an example installer at the end of this document.
Fetching required files
=======================
@ -51,7 +53,7 @@ directly but make the installer extract it from an archive or something, you
can reference the rom with the ``main_file`` parameter.
Example: ``main_file: game.rom``
For browser games, specify the game's URL with ``main_file``.
For web games, specify the game's URL (or filename) with ``main_file``.
Example: ``main_file: http://www...``
Presetting game parameters
@ -63,12 +65,12 @@ parameters depend on the runner:
* linux: ``args`` (optional command arguments), ``working_dir``
(optional working directory, defaults to the exe's dir).
* wine: ``args``, ``prefix`` (optional Wine prefix), ``working_dir`` (optional
* wine: ``args``, ``arch`` (optional WINEARCH), ``prefix`` (optional Wine prefix), ``working_dir`` (optional
working directory, defaults to the exe's dir).
* winesteam: ``args``, ``prefix`` (optional Wine prefix).
Example:
Example (Windows game):
::
@ -77,6 +79,37 @@ Example:
prefix: $GAMEDIR
args: -arg
Runner configuration
--------------------
The runner can be preconfigured from the installer.
The name of the directive is the slug name of the runner,
for example ``wine``. Available parameters depend on the runner.
The best way to set this is to add the game to Lutris, tweak the
runner config and then copy it from ``.config/lutris/games/<game name and id>.yml``.
Example for Wine (set wine version for this installer):
::
wine:
version: overwatch-2.15-x86_64
System configuration
--------------------
The ``system`` directive lets you preset the system config for the game.
Example (setting some environment variables):
::
system:
env:
__GL_SHADER_DISK_CACHE: '1'
__GL_THREADED_OPTIMIZATIONS: '1'
mesa_glthread: 'true'
Mods and add-ons
----------------
@ -207,7 +240,7 @@ reference a ``file id`` or a path, ``args`` to add command arguments,
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). The file is made executable if necessary, no need to run
chmodx before.
chmodx before. You can also use ``env`` (environment variables), ``exclude_processes`` (space-separated list of processes to exclude from being watched), ``include_processes`` (the opposite of ``exclude_processes``, is used to override Lutris' built-in exclude list) and ``disable_runtime`` (run a process without the Lutris Runtime, useful for running system binaries).
Example:
@ -217,14 +250,42 @@ Example:
args: --argh
file: $great-id
terminal: true
env:
key: value
You can use the ``command`` parameter instead of ``file`` and ``args``. This
lets you run bash/shell commands easier. ``bash`` is used and is added to ``include_processes`` internally.
Example:
::
- execute:
command: 'echo Hello World! | cat'
Writing files
-------------
Writing text files
~~~~~~~~~~~~~~~~~~
Create or overwrite a file with the ``write_file`` directive. Use the ``file`` (an absolute path or a ``file id``) and ``content`` parameters.
Example:
::
- write_file:
file: $GAMEDIR/myfile.txt
content: 'This is the contents of the file.'
Writing into an INI type config file
------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Modify or create a config file with the ``write_config`` directive. A config file
is a text file composed of key=value (or key: value) lines grouped under
[sections]. Use the ``file`` (an absolute path or a ``file id``), ``section``,
``key`` and ``value`` parameters. Not that the file is entirely rewritten and
``key`` and ``value`` parameters. Note that the file is entirely rewritten and
comments are left out; Make sure to compare the initial and resulting file
to spot any potential parsing issues.
@ -233,11 +294,37 @@ Example:
::
- write_config:
file: $GAMEDIR/game.ini
file: $GAMEDIR/myfile.ini
section: Engine
key: Renderer
value: OpenGL
Writing into a JSON type file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Modify or create a JSON file with the ``write_json`` directive.
Use the ``file`` (an absolute path or a ``file id``) and ``data`` parameters.
Note that the file is entirely rewritten; Make sure to compare the initial
and resulting file to spot any potential parsing issues. You can set the optional parameter ``merge`` to ``false`` if you want to overwrite the JSON file instead of updating it.
Example:
::
- write_json:
file: $GAMEDIR/myfile.json
data:
Sound:
Enabled: 'false'
This writes (or updates) a file with the following content:
::
{
"Sound": {
"Enabled": "false"
}
}
Running a task provided by a runner
-----------------------------------
@ -255,7 +342,7 @@ Currently, the following tasks are implemented:
specified path. The other wine/winesteam directives below include the
creation of the prefix, so in most cases you won't need to use the
create_prefix command. Parameters are ``prefix`` (the path), ``arch``
(optional architecture of the prefix, default: win32).
(optional architecture of the prefix, default: win32), ``overrides`` (optional dll overrides)..
Example:
@ -269,7 +356,10 @@ Currently, the following tasks are implemented:
* wine / winesteam: ``wineexec`` Runs a windows executable. Parameters are
``executable`` (``file ID`` or path), ``args`` (optional arguments passed
to the executable), ``prefix`` (optional WINEPREFIX),
``working_dir`` (optional working directory).
``arch`` (optional WINEARCH, required when you created win64 prefix), ``blocking`` (if true, do not run the process in a thread), ``working_dir`` (optional working directory), ``include_processes`` (optional space-separated list of processes to include to
being watched)
``exclude_processes`` (optional space-separated list of processes to exclude from
being watched), ``env`` (optional environment variables), ``overrides`` (optional dll overrides).
Example:
@ -282,7 +372,7 @@ Currently, the following tasks are implemented:
args: --windowed
* wine / winesteam: ``winetricks`` Runs winetricks with the ``app`` argument.
``prefix`` is an optional WINEPREFIX path.
``prefix`` is an optional WINEPREFIX path. You can run many tricks at once by adding more to the ``app`` parameter (space-separated).
By default Winetricks will run in silent mode but that can cause issues
with some components such as XNA. In such cases, you can provide the
@ -297,10 +387,60 @@ Currently, the following tasks are implemented:
prefix: $GAMEDIR
app: nt40
* wine / winesteam: ``winecfg`` runs execute winecfg in your ``prefix`` argument. Parameters are
``prefix`` (optional wineprefix path), ``arch`` (optional WINEARCH, required when you created win64 prefix),
``config`` (dunno what is is).
example:
::
- task:
name: winecfg
prefix: $GAMEDIR
config: config-file
arch: win64
* wine / winesteam: ``joycpl`` runs joycpl in your ``prefix`` argument. Parameters are
``prefix`` (optional wineprefix path), ``arch`` (optional WINEARCH, required when you created win64 prefix).
example:
::
- task:
name: joypl
prefix: $GAMEDIR
arch: win64
* wine / winesteam: ``eject_disk`` runs eject_disk in your ``prefix`` argument. parameters are
``prefix`` (optional wineprefix path).
example:
::
- task:
name: eject_disc
prefix: $GAMEDIR
* wine / winesteam: ``disable_desktop_integration`` remove links to user directories in a ``prefix`` argument. parameters are
``prefix`` (wineprefix path).
example:
::
- task:
name: eject_disc
prefix: $GAMEDIR
* wine / winesteam: ``set_regedit`` Modifies the Windows registry. Parameters
are ``path`` (the registry path, use backslashes), ``key``, ``value``,
``type`` (optional value type, default is REG_SZ (string)), ``prefix``
(optional WINEPREFIX).
(optional WINEPREFIX), ``arch``
(optional architecture of the prefix, required when you created win64 prefix).
Example:
@ -313,9 +453,29 @@ Currently, the following tasks are implemented:
key: SuppressAutoRun
value: '00000000'
type: REG_DWORD
arch: win64
* wine / winesteam: ``delete_registry_key`` Deletes registry key in the Windows registry. Parameters
are ``key``, ``prefix``
(optional WINEPREFIX), ``arch`` (optional architecture of the prefix, required when you created win64 prefix).
Example:
::
- task:
name: set_regedit
prefix: $GAMEDIR
path: HKEY_CURRENT_USER\Software\Valve\Steam
key: SuppressAutoRun
value: '00000000'
type: REG_DWORD
arch: win64
* wine / winesteam: ``set_regedit_file`` Apply a regedit file to the
registry
registry, Parameters are ``filename`` (regfile name),
``arch`` (optional architecture of the prefix, required when you created win64 prefix).
Example::
@ -323,6 +483,20 @@ Currently, the following tasks are implemented:
name: set_regedit_file
prefix: $GAMEDIR
filename: myregfile
arch: win64
* wine / winesteam: ``winekill`` Stops processes running in Wine prefix. Parameters
are ``prefix`` (optional WINEPREFIX),
``arch`` (optional architecture of the prefix, required when you created win64 prefix).
Example
::
- task:
name: winekill
prefix: $GAMEDIR
arch: win64
* dosbox: ``dosexec`` Runs dosbox. Parameters are ``executable`` (optional
``file ID`` or path to executable), ``config_file``
@ -361,7 +535,7 @@ Example:
::
- input_menu:
description: Choose the game's language:
description: "Choose the game's language:"
id: LANG
options:
- en: English
@ -379,10 +553,49 @@ Trying the installer locally
============================
If needed (i.e. you didn't download the installer first from the website), add
the ``runner`` and ``name`` directives. The value for ``runner`` must be the
slug name for the runner. (E.g. winesteam for Steam Windows.)
Save your script in a file and use the following command in a terminal:
``lutris -i /path/to/file``
the ``name``, ``game_slug``, ``slug``, ``version`` and ``runner`` directives.
The value for ``runner`` must be the slug name for the runner.
(E.g. winesteam for Steam Windows.)
Under ``script``, add ``files``, ``installer``, ``game`` and other installer
directives. See below for an example.
Save your script in a .yaml file and use the following command in a terminal:
``lutris -i /path/to/file.yaml``
Example Linux game:
::
name: My Game
game_slug: my-game
version: Installer
slug: my-game-installer
runner: linux
script:
game:
exe: $GAMEDIR/mygame
args: --some-arg
files:
- myfile: http://example.com/mygame.zip
installer:
- chmodx: $GAMEDIR/mygame
When submitting the installer script to lutris.net, only copy the script part. Remove the two space indentation:
::
game:
exe: $GAMEDIR/mygame
args: --some-arg
files:
- myfile: http://example.com
installer:
- chmodx: $GAMEDIR/mygame
Calling the online installer

View file

@ -6,7 +6,7 @@
%global appid net.lutris.Lutris
Name: lutris
Version: 0.4.12
Version: 0.4.14
Release: 2%{?dist}
Summary: Install and play any video game easily
@ -30,7 +30,7 @@ BuildRequires: python3-gobject
Requires: python3-gobject, python3-PyYAML, cabextract
%endif
%if 0%{?suse_version}
BuildRequires: python3-gobject
BuildRequires: python3-gobject, python3-setuptools, typelib-1_0-Gtk-3_0
BuildRequires: update-desktop-files
# Needed to workaround "directories not owned by a package" issue
BuildRequires: hicolor-icon-theme

View file

@ -194,7 +194,7 @@ class Game(object):
self.state = self.STATE_STOPPED
return
system_config = self.runner.system_config
self.original_outputs = display.get_outputs()
self.original_outputs = sorted(display.get_outputs(), key=lambda e: e[0] == system_config.get('display'))
gameplay_info = self.runner.play()
env = {}
@ -295,12 +295,19 @@ class Game(object):
# Env vars
game_env = gameplay_info.get('env') or {}
env.update(game_env)
system_env = system_config.get('env') or {}
env.update(system_env)
ld_preload = gameplay_info.get('ld_preload') or ''
env["LD_PRELOAD"] = ld_preload
dri_prime = system_config.get('dri_prime')
if dri_prime:
env["DRI_PRIME"] = "1"
else:
env["DRI_PRIME"] = "0"
# Runtime management
ld_library_path = ""
if self.runner.use_runtime():
@ -315,8 +322,11 @@ class Game(object):
ld_library_path = '$LD_LIBRARY_PATH'
ld_library_path = ":".join([game_ld_libary_path, ld_library_path])
env["LD_LIBRARY_PATH"] = ld_library_path
# /Env vars
include_processes = shlex.split(system_config.get('include_processes', ''))
exclude_processes = shlex.split(system_config.get('exclude_processes', ''))
monitoring_disabled = system_config.get('disable_monitoring')
process_watch = not monitoring_disabled
@ -326,7 +336,9 @@ class Game(object):
rootpid=gameplay_info.get('rootpid'),
watch=process_watch,
term=terminal,
log_buffer=self.log_buffer)
log_buffer=self.log_buffer,
include_processes=include_processes,
exclude_processes=exclude_processes)
if hasattr(self.runner, 'stop'):
self.game_thread.set_stop_command(self.runner.stop)
self.game_thread.start()
@ -348,7 +360,7 @@ class Game(object):
"--dbus", "session", "--silent"
] + config.split()
logger.debug("[xboxdrv] %s", ' '.join(command))
self.xboxdrv_thread = LutrisThread(command)
self.xboxdrv_thread = LutrisThread(command, include_processes=['xboxdrv'])
self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop)
self.xboxdrv_thread.start()
@ -364,8 +376,12 @@ class Game(object):
self.game_thread.error)
self.on_game_quit()
return False
killswitch_engage = self.killswitch and \
not os.path.exists(self.killswitch)
# The killswitch file should be set to a device (ie. /dev/input/js0)
# When that device is unplugged, the game is forced to quit.
killswitch_engage = (
self.killswitch and not os.path.exists(self.killswitch)
)
if not self.game_thread.is_running or killswitch_engage:
logger.debug("Game thread stopped")
self.on_game_quit()
@ -373,16 +389,18 @@ class Game(object):
return True
def stop(self):
self.state = self.STATE_STOPPED
if self.runner.system_config.get('xboxdrv'):
log.debug("Stopping xboxdrv")
self.xboxdrv_thread.stop()
if self.game_thread:
jobs.AsyncCall(self.game_thread.stop, None, killall=True)
jobs.AsyncCall(self.game_thread.stop, None, killall=self.runner.killall_on_exit())
self.state = self.STATE_STOPPED
def on_game_quit(self):
"""Restore some settings and cleanup after game quit."""
self.heartbeat = None
if self.state != self.STATE_STOPPED:
logger.debug("Game thread still running, stopping it (state: %s)", self.state)
self.stop()
quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
logger.debug("%s stopped at %s", self.name, quit_time)
@ -423,3 +441,7 @@ class Game(object):
if strings.lookup_string_in_text(error, self.game_thread.stdout):
dialogs.ErrorDialog("<b>Error: A different Wine version is "
"already using the same Wine prefix.</b>")
def notify_steam_game_changed(self, appmanifest):
logger.debug(appmanifest)
logger.debug(appmanifest.states)

View file

@ -7,40 +7,14 @@ from lutris.game import Game
from lutris import gui
from lutris.gui.config_boxes import GameBox, RunnerBox, SystemBox
from lutris.gui.dialogs import ErrorDialog
from lutris.gui.widgets.common import VBox
from lutris.gui.widgets.common import VBox, SlugEntry, NumberEntry
from lutris.gui.widgets.dialogs import Dialog
from lutris.gui.widgets.utils import get_pixbuf_for_game, get_pixbuf, BANNER_SIZE, ICON_SIZE
from lutris.util.strings import slugify
from lutris.util import datapath
DIALOG_WIDTH = 550
DIALOG_HEIGHT = 550
class SlugEntry(Gtk.Entry, Gtk.Editable):
def __init__(self):
super().__init__()
def do_insert_text(self, new_text, length, position):
"""Filter inserted characters to only accept alphanumeric and dashes"""
new_text = ''.join([c for c in new_text if c.isalnum() or c == '-']).lower()
if new_text:
self.get_buffer().insert_text(position, new_text, length)
return position + length
return position
class NumberEntry(Gtk.Entry, Gtk.Editable):
def __init__(self):
super().__init__()
def do_insert_text(self, new_text, length, position):
"""Filter inserted characters to only accept numbers"""
new_text = ''.join([c for c in new_text if c.isnumeric()])
if new_text:
self.get_buffer().insert_text(position, new_text, length)
return position + length
return position
DIALOG_WIDTH = 780
DIALOG_HEIGHT = 560
class GameDialogCommon(object):

View file

@ -10,11 +10,10 @@ from lutris import runners
from lutris import settings
from lutris.game import Game
from lutris.gui.cellrenderers import GridViewCellRendererText
from lutris.gui.widgets.cellrenderers import GridViewCellRendererText
from lutris.gui.widgets.utils import get_pixbuf_for_game, BANNER_SIZE, BANNER_SMALL_SIZE
from lutris.services import xdg
from lutris.runners import import_runner, InvalidRunner
from lutris.util.log import logger
(
@ -125,8 +124,6 @@ class GameStore(GObject.Object):
self.add_game(game)
def add_game(self, game):
pixbuf = get_pixbuf_for_game(game['slug'], self.icon_type,
game['installed'])
name = game['name'].replace('&', "&amp;")
runner = None
platform = ''
@ -139,7 +136,11 @@ class GameStore(GObject.Object):
if runner_name in self.runner_names:
runner_human_name = self.runner_names[runner_name]
else:
try:
runner = runners.import_runner(runner_name)
except runners.InvalidRunner:
game['installed'] = False
else:
runner_human_name = runner.human_name
self.runner_names[runner_name] = runner_human_name
platform = game_inst.platform
@ -148,6 +149,8 @@ class GameStore(GObject.Object):
if game['lastplayed']:
lastplayed = time.strftime("%c", time.localtime(game['lastplayed']))
pixbuf = get_pixbuf_for_game(game['slug'], self.icon_type,
game['installed'])
self.store.append((
game['id'],
game['slug'],
@ -483,8 +486,8 @@ class ContextualMenu(Gtk.Menu):
if runner_slug:
game = game or Game(game_id)
try:
runner = import_runner(runner_slug)(game.config)
except InvalidRunner:
runner = runners.import_runner(runner_slug)(game.config)
except runners.InvalidRunner:
runner_entries = None
else:
runner_entries = runner.context_menu_entries

View file

@ -230,6 +230,9 @@ class LutrisWindow(Gtk.ApplicationWindow):
def on_steam_game_changed(self, operation, path):
appmanifest = steam.AppManifest(path)
if self.running_game and 'steam' in self.running_game.runner_name:
self.running_game.notify_steam_game_changed(appmanifest)
runner_name = appmanifest.get_runner_name()
games = pga.get_games_where(steamid=appmanifest.steamid)
if operation == Gio.FileMonitorEvent.DELETED:

View file

@ -7,6 +7,31 @@ from gi.repository import Gtk, GObject
from lutris.util.system import reverse_expanduser
class SlugEntry(Gtk.Entry, Gtk.Editable):
def __init__(self):
super(SlugEntry, self).__init__()
def do_insert_text(self, new_text, length, position):
"""Filter inserted characters to only accept alphanumeric and dashes"""
new_text = ''.join([c for c in new_text if c.isalnum() or c == '-']).lower()
length = len(new_text)
self.get_buffer().insert_text(position, new_text, length)
return position + length
class NumberEntry(Gtk.Entry, Gtk.Editable):
def __init__(self):
super(NumberEntry, self).__init__()
def do_insert_text(self, new_text, length, position):
"""Filter inserted characters to only accept numbers"""
new_text = ''.join([c for c in new_text if c.isnumeric()])
if new_text:
self.get_buffer().insert_text(position, new_text, length)
return position + length
return position
class FileChooserEntry(Gtk.Box):
def __init__(self, title='Select file', action=Gtk.FileChooserAction.OPEN,
default_path=None):

View file

@ -1,7 +1,9 @@
import multiprocessing
import os
import stat
import shutil
import shlex
import json
from gi.repository import GLib
@ -11,6 +13,7 @@ from lutris import runtime
from lutris.util import extract, disks, system
from lutris.util.fileio import EvilConfigParser, MultiOrderedDict
from lutris.util.log import logger
from lutris.util import selective_merge
from lutris.runners import wine, import_task
from lutris.thread import LutrisThread
@ -49,32 +52,59 @@ class CommandsMixin(object):
def chmodx(self, filename):
filename = self._substitute(filename)
os.popen('chmod +x "%s"' % filename)
st = os.stat(filename)
os.chmod(filename, st.st_mode | stat.S_IEXEC)
def execute(self, data):
"""Run an executable file."""
args = []
terminal = None
working_dir = None
env = {}
if isinstance(data, dict):
self._check_required_params('file', data, 'execute')
file_ref = data['file']
if 'command' not in data and 'file' not in data:
raise ScriptingError('Parameter file or command is mandatory '
'for the execute command', data)
elif 'command' in data and 'file' in data:
raise ScriptingError('Parameters file and command can\'t be '
'used at the same time for the execute '
'command', data)
file_ref = data.get('file', '')
command = data.get('command', '')
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')
if not data.get('disable_runtime', False):
env.update(runtime.get_env())
userenv = data.get('env', {})
for key in userenv:
v = userenv[key]
userenv[key] = self._get_file(v) or self._substitute(v)
env.update(userenv)
include_processes = shlex.split(data.get('include_processes', ''))
exclude_processes = shlex.split(data.get('exclude_processes', ''))
elif isinstance(data, str):
command = data
include_processes = []
exclude_processes = []
else:
file_ref = data
raise ScriptingError('No parameters supplied to execute command.', data)
if command:
command = command.strip()
command = self._get_file(command) or self._substitute(command)
file_ref = 'bash'
args = ['-c', command]
include_processes.append('bash')
else:
# Determine whether 'file' value is a file id or a path
exec_path = self._get_file(file_ref) or self._substitute(file_ref)
file_ref = self._get_file(file_ref) or self._substitute(file_ref)
exec_path = system.find_executable(file_ref)
if not exec_path:
raise ScriptingError("Unable to find file %s" % file_ref,
file_ref)
if not os.path.exists(exec_path):
raise ScriptingError("Unable to find required executable",
exec_path)
raise ScriptingError("Unable to find executable %s" % file_ref)
if not os.access(exec_path, os.X_OK):
self.chmodx(exec_path)
@ -86,8 +116,9 @@ class CommandsMixin(object):
command = [exec_path] + args
logger.debug("Executing %s" % command)
thread = LutrisThread(command, env=runtime.get_env(), term=terminal,
cwd=self.target_path)
thread = LutrisThread(command, env=env, term=terminal, cwd=working_dir,
include_processes=include_processes,
exclude_processes=exclude_processes)
thread.start()
GLib.idle_add(self.parent.attach_logger, thread)
self.heartbeat = GLib.timeout_add(1000, self._monitor_task, thread)
@ -340,16 +371,61 @@ class CommandsMixin(object):
return False
return True
def write_file(self, params):
"""Write text to a file."""
self._check_required_params(['file', 'content'], params, 'write_file')
# Get file
file = self._get_file(params['file']) or self._substitute(params['file'])
# Create dir if necessary
basedir = os.path.dirname(file)
if not os.path.exists(basedir):
os.makedirs(basedir)
mode = params.get('mode', 'w')
with open(file, mode) as f:
f.write(params['content'])
def write_json(self, params):
"""Write data into a json file."""
self._check_required_params(['file', 'data'], params, 'write_json')
# Get file
file = self._get_file(params['file']) or self._substitute(params['file'])
# Create dir if necessary
basedir = os.path.dirname(file)
if not os.path.exists(basedir):
os.makedirs(basedir)
merge = params.get('merge', True)
with open(file, 'a+') as f:
pass
with open(file, 'r+' if merge else 'w') as f:
data = {}
if merge:
try:
data = json.load(f)
except ValueError:
pass
data = selective_merge(data, params.get('data', {}))
f.seek(0)
f.write(json.dumps(data, indent=2))
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')
# Get file
config_file = self._get_file(params['file'])
if not config_file:
config_file = self._substitute(params['file'])
config_file = (self._get_file(params['file']) or
self._substitute(params['file']))
# Create it if necessary
# Create dir if necessary
basedir = os.path.dirname(config_file)
if not os.path.exists(basedir):
os.makedirs(basedir)

View file

@ -43,6 +43,9 @@ def fetch_script(game_slug, revision=None):
request = Request(installer_url)
request.get()
response = request.json
if response is None:
raise RuntimeError("Couldn't get installer at %s" % installer_url)
if key:
return response[key]
else:
@ -258,14 +261,14 @@ class ScriptInterpreter(CommandsMixin):
else:
file_uri = game_file[file_id]
filename = os.path.basename(file_uri)
if not filename:
raise ScriptingError("No filename provided, please provide 'url' and 'filename' parameters in the script")
if file_uri.startswith("/"):
file_uri = "file://" + file_uri
elif file_uri.startswith(("$WINESTEAM", "$STEAM")):
# Download Steam data
self._download_steam_data(file_uri, file_id)
return
if not filename:
raise ScriptingError("No filename provided, please provide 'url' and 'filename' parameters in the script")
# Check for file availability in PGA
pga_uri = pga.check_for_file(self.game_slug, file_id)

View file

@ -11,8 +11,15 @@ def get_core_choices():
# The order has to be the same!
return [
('4do (3DO)', '4do'),
('atari800 (Atari 800/5200)', 'atari800'),
('Caprice32 (Amstrad CPC)', 'cap32'),
('Citra (Nintendo 3DS)', 'citra'),
('CrocoDS (Amstrad CPC)', 'crocods'),
('DesmuME (Nintendo DS)', 'desmume'),
('Dolphin (Nintendo Wii/Gamecube)', 'dolphin'),
('EightyOne (Sinclair ZX81)', '81'),
('FCEUmm (Nintendo Entertainment System)', 'fceumm'),
('fMSX (MSX/MSX2/MSX2+)', 'fmsx'),
('Fuse (ZX Spectrum)', 'fuse'),
('Gambatte (Game Boy Color)', 'gambatte'),
('Genesis Plus GX (Sega Genesis)', 'genesis_plus_gx'),
@ -30,15 +37,24 @@ def get_core_choices():
('mGBA (Game Boy Advance)', 'mgba'),
('Mupen64Plus (Nintendo 64)', 'mupen64plus'),
('Nestopia (Nintendo Entertainment System)', 'nestopia'),
('Neko Project 2 (NEC PC-98)', 'nekop2'),
('O2EM (Magnavox Odyssey²)', 'o2em'),
('PCSX Rearmed (Sony Playstation)', 'pcsx_rearmed'),
('PicoDrive (Sega Genesis)', 'picodrive'),
('Portable SHARP X68000 Emulator (SHARP X68000)', 'px68k'),
('PPSSPP (PlayStation Portable)', 'ppsspp'),
('ProSystem (Atari 7800)', 'prosystem'),
('Redream (Sega Dreamcast)', 'redream'),
('Reicast (Sega Dreamcast)', 'reicast'),
('Snes9x (Super Nintendo)', 'snes9x'),
('VecX (Vectrex)', 'vecx'),
('Yabause (Sega Saturn)', 'yabause'),
('VBA Next (Game Boy Advance)', 'vba_next'),
('VBA-M (Game Boy Advance)', 'vbam'),
('VICE (Commodore 128)', 'vice_x128'),
('VICE (Commodore 16/Plus/4)', 'vice_xplus4'),
('VICE (Commodore 64)', 'vice_x64'),
('VICE (Commodore VIC-20)', 'vice_xvic'),
]
@ -63,8 +79,15 @@ class libretro(Runner):
description = "Multi system emulator"
platforms = (
'3DO',
'Atari 800/5200',
'Amstrad CPC',
'Nintendo 3DS',
'Amstrad CPC',
'Nintendo DS',
'Nintendo Wii/Gamecube',
'Sinclair ZX81',
'Nintendo NES',
'MSX/MSX2/MSX2+',
'Sinclair ZX Spectrum',
'Nintendo Game Boy Color',
'Sega Genesis',
@ -82,15 +105,24 @@ class libretro(Runner):
'Nintendo Game Boy Advance',
'Nintendo N64',
'Nintendo NES',
'NEC PC-98',
'Magnavox Odyssey²',
'Sony PlayStation',
'Sega Genesis',
'Sharp X68000',
'Sony PlayStation Portable',
'Atari 7800',
'Sega Dreamcast',
'Sega Dreamcast',
'Nintendo SNES',
'Vectrex',
'Sega Saturn',
'Nintendo Game Boy Advance',
'Nintendo Game Boy Advance',
'Commodore 128',
'Commodore 16/Plus/4',
'Commodore 64',
'Commodore VIC-20',
)
runnable_alone = True
runner_executable = 'retroarch/retroarch'

View file

@ -3,34 +3,45 @@ import subprocess
from lutris.runners.runner import Runner
from lutris.util.display import get_current_resolution
from lutris.util.log import logger
from lutris.util.joypad import get_controller_mappings
class mednafen(Runner):
human_name = "Mednafen"
description = ("Multi-system emulator including NES, GB(A), PC Engine "
"support.")
# TODO: Add more platforms/machines
platforms = [
'Nintendo Game Boy (Color)',
'Nintendo Game Boy Advance',
'Sega Game Gear',
'Sega Genesis/Mega Drive',
'Atari Lynx',
'Sega Master System',
'SNK Neo Geo Pocket (Color)',
'Nintendo NES',
'NEC PC Engine TurboGrafx-16',
'Nintendo Game Boy',
'Nintendo Game Boy Advance',
'NEC PC-FX',
'Sony PlayStation',
# 'Atari Lynx',
# 'Nintendo Game Boy Color',
# 'NEC PC Engine TurboGrafx-16',
# 'NEC PC-FX',
# 'NEC PC Engine SuperGrafx',
# 'SNK Neo Geo Pocket',
# 'SNK Neo Geo Pocket Color',
# 'Bandai WonderSwan',
'Sega Saturn',
'Nintendo SNES',
'Bandai WonderSwan',
'Nintendo Virtual Boy',
]
machine_choices = (
("NES", "nes"),
("PC Engine", "pce"),
('Game Boy', 'gb'),
('Game Boy (Color)', 'gb'),
('Game Boy Advance', 'gba'),
('Game Gear','gg'),
('Genesis/Mega Drive','md'),
('Lynx', 'lynx'),
('Master System','sms'),
('Neo Geo Pocket (Color)','gnp'),
('NES', 'nes'),
('PC Engine', 'pce'),
('PC-FX','pcfx'),
('PlayStation', 'psx'),
('Saturn','ss'),
('SNES','snes'),
('WonderSwan', 'wswan'),
('Virtual Boy','vb'),
)
runner_executable = 'mednafen/bin/mednafen'
game_options = [
@ -92,7 +103,14 @@ class mednafen(Runner):
("nny4x", "nny4x"),
),
"default": "hq4x",
},
{
"option": "dflt_cntrllr",
"type": "bool",
"label": "Use Mednafen controller configuration",
"default": False,
}
]
def get_platform(self):
@ -132,113 +150,143 @@ class mednafen(Runner):
def set_joystick_controls(self, joy_ids, machine):
""" Setup joystick mappings per machine """
# Button mappings (based on Xbox360 controller)
BTN_A = "00000000"
BTN_B = "00000001"
# BTN_X = "00000002"
# BTN_Y = "00000003"
BTN_R = "00000004"
BTN_L = "00000005"
BTN_SELECT = "00000006"
BTN_START = "00000007"
# BTN_HOME = "00000008"
# BTN_THUMB_L = "00000009"
# BTN_THUMB_R = "00000010"
AXIS_UP = "0000c001"
AXIS_DOWN = "00008001"
AXIS_LEFT = "0000c000"
AXIS_RIGHT = "00008000"
# Get the controller mappings
mapping = get_controller_mappings()[0][1]
nes_controls = [
"-nes.input.port1.gamepad.a",
"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",
"joystick {} {}".format(joy_ids[0], BTN_SELECT),
"-nes.input.port1.gamepad.up",
"joystick {} {}".format(joy_ids[0], AXIS_UP),
"-nes.input.port1.gamepad.down",
"joystick {} {}".format(joy_ids[0], AXIS_DOWN),
"-nes.input.port1.gamepad.left",
"joystick {} {}".format(joy_ids[0], AXIS_LEFT),
"-nes.input.port1.gamepad.right",
"joystick {} {}".format(joy_ids[0], AXIS_RIGHT),
]
# Consrtuct a dictionary of button codes to parse to mendafen
map_code = {'a':'','b':'','c':'','x':'','y':'','z':'','back':'',
'start':'','leftshoulder':'','rightshoulder':'',
'lefttrigger':'','righttrigger':'','leftstick':'',
'rightstick':'','select':'','shoulder_l':'','shoulder_r':'',
'i':'','ii':'','iii':'','iv':'','v':'','vi':'','run':'',
'ls':'','rs':'','fire1':'','fire2':'','option_1':'',
'option_2':'','cross':'','circle':'','square':'','triangle':'',
'r1':'','r2':'','l1':'','l2':'','option':'','l':'','r':'',
'right-x':'','right-y':'','left-x':'','left-y':'',
'up-x':'','up-y':'','down-x':'','down-y':'','up-l':'',
'up-r':'','down-l':'','down-r':'','left-l':'','left-r':'',
'right-l':'','right-r':'',
'lstick_up':'0000c001',
'lstick_down':'00008001',
'lstick_right':'00008000',
'lstick_left':'0000c000',
'rstick_up':'0000c003',
'rstick_down':'00008003',
'rstick_left':'0000c002',
'rstick_right':'00008002',
'dpup':'0000c005',
'dpdown':'00008005',
'dpleft':'0000c004',
'dpright':'00008004'}
gba_controls = [
"-gba.input.builtin.gamepad.a",
"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",
"joystick {} {}".format(joy_ids[0], BTN_L),
"-gba.input.builtin.gamepad.start",
"joystick {} {}".format(joy_ids[0], BTN_START),
"-gba.input.builtin.gamepad.select",
"joystick {} {}".format(joy_ids[0], BTN_SELECT),
"-gba.input.builtin.gamepad.up",
"joystick {} {}".format(joy_ids[0], AXIS_UP),
"-gba.input.builtin.gamepad.down",
"joystick {} {}".format(joy_ids[0], AXIS_DOWN),
"-gba.input.builtin.gamepad.left",
"joystick {} {}".format(joy_ids[0], AXIS_LEFT),
"-gba.input.builtin.gamepad.right",
"joystick {} {}".format(joy_ids[0], AXIS_RIGHT),
]
# Insert the button mapping number into the map_codes
for button in mapping.keys:
bttn_id = mapping.keys[button]
if bttn_id[0]=='b': # it's a button
map_code[button] = '000000'+bttn_id[1:].zfill(2)
gb_controls = [
"-gb.input.builtin.gamepad.a",
"joystick {} {}".format(joy_ids[0], BTN_B),
"-gb.input.builtin.gamepad.b",
"joystick {} {}".format(joy_ids[0], BTN_A),
"-gb.input.builtin.gamepad.start",
"joystick {} {}".format(joy_ids[0], BTN_START),
"-gb.input.builtin.gamepad.select",
"joystick {} {}".format(joy_ids[0], BTN_SELECT),
"-gb.input.builtin.gamepad.up",
"joystick {} {}".format(joy_ids[0], AXIS_UP),
"-gb.input.builtin.gamepad.down",
"joystick {} {}".format(joy_ids[0], AXIS_DOWN),
"-gb.input.builtin.gamepad.left",
"joystick {} {}".format(joy_ids[0], AXIS_LEFT),
"-gb.input.builtin.gamepad.right",
"joystick {} {}".format(joy_ids[0], AXIS_RIGHT),
]
# Duplicate button names that are emulated in mednanfen
map_code['up'] = map_code['dpup'] #
map_code['down'] = map_code['dpdown'] #
map_code['left'] = map_code['dpleft'] # Multiple systems
map_code['right'] = map_code['dpright'] #
map_code['select'] = map_code['back'] #
map_code['shoulder_r'] = map_code['rightshoulder'] #GBA
map_code['shoulder_l'] = map_code['leftshoulder'] #
map_code['i'] = map_code['b'] #
map_code['ii'] = map_code['a'] #
map_code['iii'] = map_code['leftshoulder'] #
map_code['iv'] = map_code['y'] # PCEngine and PCFX
map_code['v'] = map_code['x'] #
map_code['vi'] = map_code['rightshoulder'] #
map_code['run'] = map_code['start'] #
map_code['ls'] = map_code['leftshoulder'] #
map_code['rs'] = map_code['rightshoulder'] # Saturn
map_code['c'] = map_code['righttrigger'] #
map_code['z'] = map_code['lefttrigger'] #
map_code['fire1'] = map_code['a'] # Master System
map_code['fire2'] = map_code['b'] #
map_code['option_1'] = map_code['x'] # Lynx
map_code['option_2'] = map_code['y'] #
map_code['r1'] = map_code['rightshoulder'] #
map_code['r2'] = map_code['righttrigger'] #
map_code['l1'] = map_code['leftshoulder'] #
map_code['l2'] = map_code['lefttrigger'] # PlayStation
map_code['cross'] = map_code['a'] #
map_code['circle'] = map_code['b'] #
map_code['square'] = map_code['x'] #
map_code['triangle'] = map_code['y'] #
map_code['option'] = map_code['select'] # NeoGeo pocket
map_code['l'] = map_code['leftshoulder'] # SNES
map_code['r'] = map_code['rightshoulder'] #
map_code['right-x'] = map_code['dpright'] #
map_code['left-x'] = map_code['dpleft'] #
map_code['up-x'] = map_code['dpup'] #
map_code['down-x'] = map_code['dpdown'] # Wonder Swan
map_code['right-y'] = map_code['lstick_right'] #
map_code['left-y'] = map_code['lstick_left'] #
map_code['up-y'] = map_code['lstick_up'] #
map_code['down-y'] = map_code['lstick_down'] #
map_code['up-l'] = map_code['dpup'] #
map_code['down-l'] = map_code['dpdown'] #
map_code['left-l'] = map_code['dpleft'] #
map_code['right-l'] = map_code['dpright'] #
map_code['up-r'] = map_code['rstick_up'] #
map_code['down-r'] = map_code['rstick_down'] # Virtual boy
map_code['left-r'] = map_code['rstick_left'] #
map_code['right-r'] = map_code['rstick_right'] #
map_code['lt'] = map_code['leftshoulder'] #
map_code['rt'] = map_code['rightshoulder'] #
pce_controls = [
"-pce.input.port1.gamepad.i",
"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",
"joystick {} {}".format(joy_ids[0], BTN_SELECT),
"-pce.input.port1.gamepad.up",
"joystick {} {}".format(joy_ids[0], AXIS_UP),
"-pce.input.port1.gamepad.down",
"joystick {} {}".format(joy_ids[0], AXIS_DOWN),
"-pce.input.port1.gamepad.left",
"joystick {} {}".format(joy_ids[0], AXIS_LEFT),
"-pce.input.port1.gamepad.right",
"joystick {} {}".format(joy_ids[0], AXIS_RIGHT),
]
if machine == "pce":
controls = pce_controls
elif machine == "nes":
controls = nes_controls
elif machine == "gba":
controls = gba_controls
elif machine == "gb":
controls = gb_controls
else:
# Define which buttons to use for each machine
layout = {
'nes' : ['a','b','start','select','up','down','left','right'],
'gb' : ['a','b','start','select','up','down','left','right'],
'gba' : ['a','b','shoulder_r','shoulder_l','start','select',
'up','down','left', 'right'],
'pce' : ['i','ii','iii','iv','v','vi','run','select','up','down',
'left','right'],
'ss' : ['a','b','c','x','y','z','ls','rs','start','up','down',
'left','right'],
'gg' : ['button1','button2','start','up','down','left','right'],
'md' : ['a','b','c','x','y','z','start','up','down','left',
'right'],
'sms' : ['fire1','fire2','up','down','left','right'],
'lynx' : ['a','b','option_1','option_2','up','down','left',
'right'],
'psx' : ['cross','circle','square','triangle','l1','l2','r1','r2',
'start','select','lstick_up','lstick_down','lstick_right',
'lstick_left','rstick_up','rstick_down','rstick_left',
'rstick_right','up','down','left','right'],
'pcfx' : ['i','ii','iii','iv','v','vi','run','select','up','down',
'left','right'],
'ngp' : ['a','b','option','up','down','left','right'],
'snes' : ['a','b','x','y','l','r','start','select','up','down',
'left','right'],
'wswan' : ['a','b','right-x','right-y','left-x','left-y','up-x',
'up-y','down-x','down-y','start'],
'vb' : ['up-l','down-l','left-l','right-l','up-r','down-r',
'left-r','right-r','a','b','lt','rt']
}
# Select a the gamepad type
controls = []
if machine in ['gg','lynx','wswan','gb','gba','vb']:
gamepad = 'builtin.gamepad'
elif machine in ['md']:
gamepad = 'port1.gamepad6'
controls.append('-md.input.port1')
controls.append('gamepad6')
elif machine in ['psx']:
gamepad = 'port1.dualshock'
controls.append('-psx.input.port1')
controls.append('dualshock')
else:
gamepad = 'port1.gamepad'
# Construct the controlls options
for button in layout[machine]:
controls.append("-{}.input.{}.{}".format(machine,gamepad,button))
controls.append("joystick {} {}".format(joy_ids[0],map_code[button]))
return controls
def play(self):
@ -266,12 +314,13 @@ class mednafen(Runner):
"-" + machine + ".special", scaler,
"-" + machine + ".videoip", "1"]
joy_ids = self.find_joysticks()
if len(joy_ids) > 0:
use_dflt_cntrllr = self.runner_config.get('dflt_cntrllr')
if (len(joy_ids) > 0) and not use_dflt_cntrllr:
controls = self.set_joystick_controls(joy_ids, machine)
for control in controls:
options.append(control)
else:
logger.debug("No Joystick found")
logger.debug("No joystick specification")
if not os.path.exists(rom):
return {'error': 'FILE_NOT_FOUND', 'file': rom}

View file

@ -9,24 +9,118 @@ class mess(Runner):
description = "Multi-system (consoles and computers) emulator"
# TODO: A lot of platforms/machines are missing
platforms = (
'Acorn Atom',
'Adventure Vision',
'Amstrad CPC 464',
'Amstrad CPC 6128',
'Amstrad GX4000',
'Apple I',
'Apple II',
'Apple IIGS',
'Arcadia 2001',
'Bally Professional Arcade',
'BBC Micro',
'Casio PV-1000',
'Casio PV-2000',
'Chintendo Vii',
'Coleco Adam',
'Commodore 64',
'Creatronic Mega Duck',
'DEC PDP-1',
'Epoch Game Pocket Computer',
'Epoch Super Cassette Vision',
'Fairchild Channel F',
'Fujitsu FM 7',
'Fujitsu FM Towns',
'Funtech Super ACan',
'Game.com',
'Hartung Game Master',
'IBM PCjr',
'Intellivision',
'Interton VC 4000',
'Matra Alice',
'Mattel Aquarius',
'Memotech MTX',
'Milton Bradley MicroVision',
'NEC PC-8801',
'NEC PC-88VA',
'RCA Studio II',
'Sam Coupe',
'SEGA Computer 3000',
'Sega Pico',
'Sega SG-1000',
'Sharp MZ-2500',
'Sharp MZ-700',
'Sharp X1',
'Sinclair ZX Spectrum',
'Sinclair ZX Spectrum 128',
'Sony SMC777',
'Spectravision SVI-318',
'Tatung Einstein',
'Thomson MO5',
'Thomson MO6',
'Tomy Tutor',
'TRS-80 Color Computer',
'Videopac Plus G7400',
'VTech CreatiVision',
'Watara Supervision',
)
machine_choices = [
("Acorn Atom", 'atom'),
("Adventure Vision", 'advision'),
("Amstrad CPC 464", 'cpc464'),
("Amstrad CPC 6128", 'cpc6128'),
("Amstrad GX4000", 'gx4000'),
("Apple I", 'apple1'),
("Apple II", 'apple2ee'),
("Apple IIGS", 'apple2gs'),
("Arcadia 2001", 'arcadia'),
("Bally Professional Arcade", 'astrocde'),
("BBC Micro", 'bbcb'),
("Casio PV-1000", 'pv1000'),
("Casio PV-2000", 'pv2000'),
("Chintendo Vii", 'vii'),
("Coleco Adam", 'adam'),
("Commodore 64", 'c64'),
("Creatronic Mega Duck", 'megaduck'),
("DEC PDP-1", 'pdp1'),
("Epoch Game Pocket Computer", 'gamepock'),
("Epoch Super Cassette Vision", 'scv'),
("Fairchild Channel F", 'channelf'),
("Fujitsu FM 7", 'fm7'),
("Fujitsu FM Towns", 'fmtowns'),
("Funtech Super A'Can", 'supracan'),
("Game.com", 'gamecom'),
("Hartung Game Master", 'gmaster'),
("IBM PCjr", 'ibmpcjr'),
("Intellivision", 'intv'),
("Interton VC 4000", 'vc4000'),
("Matra Alice", 'alice90'),
("Mattel Aquarius", 'aquarius'),
("Memotech MTX", 'mtx'),
("Milton Bradley MicroVision", 'microvision'),
("NEC PC-8801", 'pc8801'),
("NEC PC-88VA", 'pc88va'),
("RCA Studio II", 'studio2'),
("Sam Coupe", 'samcoupe'),
("SEGA Computer 3000", 'sc3000'),
("Sega Pico", 'pico'),
("Sega SG-1000", 'sg1000'),
("Sharp MZ-2500", 'mz2500'),
("Sharp MZ-700", 'mz700'),
("Sharp X1", 'x1'),
("ZX Spectrum", 'spectrum'),
("ZX Spectrum 128", 'spec128'),
("Sony SMC777", 'smc777'),
("Spectravision SVI-318", 'svi318'),
("Tatung Einstein", 'einstein'),
("Thomson MO5", 'mo5'),
("Thomson MO6", 'mo6'),
("Tomy Tutor", 'tutor'),
("TRS-80 Color Computer", 'coco'),
("Videopac Plus G7400", 'g7400'),
("VTech CreatiVision", 'crvision'),
("Watara Supervision", 'svision'),
]
runner_executable = "mess/mess"
game_options = [
@ -51,11 +145,32 @@ class mess(Runner):
("Floppy disk", 'flop'),
("Floppy drive 1", 'flop1'),
("Floppy drive 2", 'flop2'),
("Floppy drive 3", 'flop3'),
("Floppy drive 4", 'flop4'),
("Cassette (tape)", 'cass'),
("Cassette 1 (tape)", 'cass1'),
("Cassette 2 (tape)", 'cass2'),
("Cartridge", 'cart'),
("Cartridge 1", 'cart1'),
("Cartridge 2", 'cart2'),
("Cartridge 3", 'cart3'),
("Cartridge 4", 'cart4'),
("Snapshot", 'snapshot'),
("Quickload", 'quickload'),
("Hard Disk", 'hard'),
("Hard Disk 1", 'hard1'),
("Hard Disk 2", 'hard2'),
("CDROM", 'cdrm'),
("CDROM 1", 'cdrm1'),
("CDROM 2", 'cdrm2'),
("Snapshot", 'dump'),
("Quickload", 'quickload'),
("Memory Card", 'memc'),
("Cylinder", 'cyln'),
("Punch Tape 1", 'ptap1'),
("Punch Tape 2", 'ptap2'),
("Print Out", 'prin'),
("Print Out", 'prin'),
]
}
]

View file

@ -103,7 +103,7 @@ class Runner:
"""Return the path to open with the Browse Files action."""
for key in self.game_config:
if key in ['exe', 'main_file', 'rom', 'disk', 'iso']:
path = os.path.dirname(self.game_config.get(key))
path = os.path.dirname(self.game_config.get(key) or '')
if not os.path.isabs(path):
path = os.path.join(self.game_path, path)
return path
@ -123,6 +123,9 @@ class Runner:
"""Return the working directory to use when running the game."""
return os.path.expanduser("~/")
def killall_on_exit(self):
return True
def get_platform(self):
return self.platforms[0]

View file

@ -55,7 +55,7 @@ def set_regedit(path, key, value='', type='REG_SZ', wine_path=None,
def get_overrides_env(overrides):
"""
Output a string of dll overrides usable with WINEDLLOVERRIDES
See: https://www.winehq.org/docs/wineusr-guide/x258#AEN309
See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides
"""
if not overrides:
return ''
@ -98,7 +98,8 @@ def delete_registry_key(key, wine_path=None, prefix=None, arch='win32'):
prefix=prefix, arch=arch, blocking=True)
def create_prefix(prefix, wine_path=None, arch='win32'):
def create_prefix(prefix, wine_path=None, arch='win32', overrides={},
install_gecko=None, install_mono=None):
"""Create a new Wine prefix."""
logger.debug("Creating a %s prefix in %s", arch, prefix)
@ -115,11 +116,18 @@ def create_prefix(prefix, wine_path=None, arch='win32'):
logger.error("No wineboot executable found in %s, your wine installation is most likely broken", wine_path)
return
env = {
if install_gecko is 'False':
overrides['mshtml'] = 'disabled'
if install_mono is 'False':
overrides['mscoree'] = 'disabled'
wineenv = {
'WINEARCH': arch,
'WINEPREFIX': prefix
'WINEPREFIX': prefix,
'WINEDLLOVERRIDES': get_overrides_env(overrides)
}
system.execute([wineboot_path], env=env)
system.execute([wineboot_path], env=wineenv)
for i in range(20):
time.sleep(.25)
if os.path.exists(os.path.join(prefix, 'user.reg')):
@ -133,9 +141,51 @@ def create_prefix(prefix, wine_path=None, arch='win32'):
prefix_manager.setup_defaults()
def winekill(prefix, arch='win32', wine_path=None, env=None, initial_pids=None):
"""Kill processes in Wine prefix."""
initial_pids = initial_pids or []
if not wine_path:
wine_path = wine().get_executable()
wine_root = os.path.dirname(wine_path)
if not env:
env = {
'WINEARCH': arch,
'WINEPREFIX': prefix
}
command = [os.path.join(wine_root, "wineserver"), "-k"]
logger.debug("Killing all wine processes: %s" % command)
logger.debug("\tWine prefix: %s", prefix)
logger.debug("\tWine arch: %s", arch)
if initial_pids:
logger.debug("\tInitial pids: %s", initial_pids)
system.execute(command, env=env, quiet=True)
logger.debug("Waiting for wine processes to terminate")
# Wineserver needs time to terminate processes
num_cycles = 0
while True:
num_cycles += 1
running_processes = [
pid for pid in initial_pids
if os.path.exists("/proc/%s" % pid)
]
if not running_processes:
break
if num_cycles > 20:
logger.warning("Some wine processes are still running: %s", ', '.join(running_processes))
break
time.sleep(0.1)
def wineexec(executable, args="", wine_path=None, prefix=None, arch=None,
working_dir=None, winetricks_wine='', blocking=False,
config=None, include_processes=[]):
config=None, include_processes=[], exclude_processes=[],
disable_runtime=False, env={}, overrides=None):
"""
Execute a Wine command.
@ -165,7 +215,7 @@ def wineexec(executable, args="", wine_path=None, prefix=None, arch=None,
if os.path.isfile(executable):
working_dir = os.path.dirname(executable)
executable, _args = get_real_executable(executable)
executable, _args, working_dir = get_real_executable(executable, working_dir)
if _args:
args = '{} "{}"'.format(_args[0], _args[1])
@ -174,34 +224,42 @@ def wineexec(executable, args="", wine_path=None, prefix=None, arch=None,
wine_bin = winetricks_wine if winetricks_wine else wine_path
create_prefix(prefix, wine_path=wine_bin, arch=arch)
env = {
wineenv = {
'WINEARCH': arch
}
if winetricks_wine:
env['WINE'] = winetricks_wine
wineenv['WINE'] = winetricks_wine
else:
env['WINE'] = wine_path
wineenv['WINE'] = wine_path
if prefix:
env['WINEPREFIX'] = prefix
wineenv['WINEPREFIX'] = prefix
wine_config = config or LutrisConfig(runner_slug='wine')
if not wine_config.system_config['disable_runtime'] and not runtime.is_disabled():
env['LD_LIBRARY_PATH'] = ':'.join(runtime.get_paths())
if (not wine_config.system_config['disable_runtime'] and
not runtime.is_disabled() and not disable_runtime):
wineenv['LD_LIBRARY_PATH'] = ':'.join(runtime.get_paths())
if overrides:
wineenv['WINEDLLOVERRIDES'] = get_overrides_env(overrides)
wineenv.update(env)
command = [wine_path]
if executable:
command.append(executable)
command += shlex.split(args)
if blocking:
return system.execute(command, env=env, cwd=working_dir)
return system.execute(command, env=wineenv, cwd=working_dir)
else:
thread = LutrisThread(command, runner=wine(), env=env, cwd=working_dir,
include_processes=include_processes)
thread = LutrisThread(command, runner=wine(), env=wineenv, cwd=working_dir,
include_processes=include_processes,
exclude_processes=exclude_processes)
thread.start()
return thread
def winetricks(app, prefix=None, arch=None, silent=True, wine_path=None, config=None):
def winetricks(app, prefix=None, arch=None, silent=True,
wine_path=None, config=None, disable_runtime=False):
"""Execute winetricks."""
winetricks_path = os.path.join(datapath.get(), 'bin/winetricks')
if arch not in ('win32', 'win64'):
@ -214,7 +272,8 @@ def winetricks(app, prefix=None, arch=None, silent=True, wine_path=None, config=
if str(silent).lower() in ('yes', 'on', 'true'):
args = "--unattended " + args
return wineexec(None, prefix=prefix, winetricks_wine=winetricks_wine,
wine_path=winetricks_path, arch=arch, args=args, config=config)
wine_path=winetricks_path, arch=arch, args=args,
config=config, disable_runtime=disable_runtime)
def winecfg(wine_path=None, prefix=None, arch='win32', config=None):
@ -358,21 +417,25 @@ def support_legacy_version(version):
return version
def get_real_executable(windows_executable):
def get_real_executable(windows_executable, working_dir=None):
"""Given a Windows executable, return the real program
capable of launching it along with necessary arguments."""
exec_name = windows_executable.lower()
if exec_name.endswith(".msi"):
return ('msiexec', ['/i', windows_executable])
return ('msiexec', ['/i', windows_executable], working_dir)
if exec_name.endswith(".bat"):
return ('cmd', ['/C', windows_executable])
if not working_dir or os.path.dirname(windows_executable) == working_dir:
working_dir = os.path.dirname(windows_executable) or None
windows_executable = os.path.basename(windows_executable)
return ('cmd', ['/C', windows_executable], working_dir)
if exec_name.endswith(".lnk"):
return ('start', ['/unix', windows_executable])
return ('start', ['/unix', windows_executable], working_dir)
return (windows_executable, [])
return (windows_executable, [], working_dir)
# pylint: disable=C0103
@ -495,14 +558,6 @@ class wine(Runner):
'help': ('The Wine executable to be used if you have '
'selected "Custom" as the Wine version.')
},
{
'option': 'xinput',
'label': 'Enable Koku-Xinput (experimental, try using the x360 option instead)',
'type': 'bool',
'default': False,
'help': ("Preloads a library that enables Joypads on games\n"
"using XInput.")
},
{
'option': 'x360ce-path',
'label': "Path to the game's executable, for x360ce support",
@ -511,11 +566,25 @@ class wine(Runner):
},
{
'option': 'x360ce-dinput',
'label': 'x360ce dinput mode',
'label': 'x360ce dinput 8 mode',
'type': 'bool',
'default': False,
'help': "Configure x360ce with dinput8.dll, required for some games such as Darksiders 1"
},
{
'option': 'x360ce-xinput9',
'label': 'x360ce xinput 9.1.0 mode',
'type': 'bool',
'default': False,
'help': "Configure x360ce with xinput9_1_0.dll, required for some newer games"
},
{
'option': 'dumbxinputemu',
'label': 'Use Dumb xinput Emulator (experimental)',
'type': 'bool',
'default': False,
'help': "Use the dlls from kozec/dumbxinputemu"
},
{
'option': 'Desktop',
'label': 'Windowed (virtual desktop)',
@ -631,7 +700,8 @@ class wine(Runner):
'type': 'choice',
'choices': [('Disabled', '-all'),
('Enabled', ''),
('Full', '+all')],
('Show FPS', '+fps'),
('Full (CAUTION: Will cause MASSIVE slowdown)', '+all')],
'default': '-all',
'advanced': True,
'help': ("Output debugging information in the game log "
@ -822,9 +892,11 @@ class wine(Runner):
overrides = self.runner_config.get('overrides') or {}
if self.runner_config.get('x360ce-path'):
overrides['xinput1_3'] = 'native,builtin'
overrides['xinput1_3'] = 'native'
if self.runner_config.get('x360ce-xinput9'):
overrides['xinput9_1_0'] = 'native'
if self.runner_config.get('x360ce-dinput'):
overrides['dinput8'] = 'native,builtin'
overrides['dinput8'] = 'native'
if overrides:
env['WINEDLLOVERRIDES'] = get_overrides_env(overrides)
@ -845,22 +917,24 @@ class wine(Runner):
pids = pids | pids_64
return pids
def get_xinput_path(self):
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 setup_x360ce(self, x360ce_path):
if not os.path.isdir(x360ce_path):
logger.error("%s is not a valid path for x360ce", x360ce_path)
return
xinput_dest_path = os.path.join(x360ce_path, 'xinput1_3.dll')
dll_path = os.path.join(datapath.get(), 'controllers/x360ce-{}'.format(self.wine_arch))
mode = 'dumbxinputemu' if self.runner_config.get('dumbxinputemu') else 'x360ce'
dll_files = ['xinput1_3.dll']
if self.runner_config.get('x360ce-xinput9'):
dll_files.append('xinput9_1_0.dll')
for dll_file in dll_files:
xinput_dest_path = os.path.join(x360ce_path, dll_file)
dll_path = os.path.join(datapath.get(), 'controllers/{}-{}'.format(mode, self.wine_arch))
if not os.path.exists(xinput_dest_path):
xinput1_3_path = os.path.join(dll_path, 'xinput1_3.dll')
shutil.copyfile(xinput1_3_path, xinput_dest_path)
if self.runner_config.get('x360ce-dinput') and self.wine_arch == 'win32':
source_file = dll_file if mode == 'dumbxinputemu' else 'xinput1_3.dll'
shutil.copyfile(os.path.join(dll_path, source_file), xinput_dest_path)
if mode == 'x360ce':
if self.runner_config.get('x360ce-dinput'):
dinput8_path = os.path.join(dll_path, 'dinput8.dll')
dinput8_dest_path = os.path.join(x360ce_path, 'dinput8.dll')
shutil.copyfile(dinput8_path, dinput8_dest_path)
@ -871,7 +945,7 @@ class wine(Runner):
def play(self):
game_exe = self.game_exe
arguments = self.game_config.get('args') or ''
arguments = self.game_config.get('args', '')
if not os.path.exists(game_exe):
return {'error': 'FILE_NOT_FOUND', 'file': game_exe}
@ -879,19 +953,12 @@ class wine(Runner):
launch_info = {}
launch_info['env'] = self.get_env(full=False)
if self.runner_config.get('xinput'):
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')
if self.runner_config.get('x360ce-path'):
self.setup_x360ce(self.runner_config['x360ce-path'])
command = [self.get_executable()]
game_exe, _args = get_real_executable(game_exe)
game_exe, _args, working_dir = get_real_executable(game_exe, self.working_dir)
command.append(game_exe)
if _args:
command = command + _args
@ -904,26 +971,28 @@ class wine(Runner):
def stop(self):
"""The kill command runs wineserver -k."""
wine_path = self.get_executable()
wine_root = os.path.dirname(wine_path)
env = self.get_env()
command = [os.path.join(wine_root, "wineserver"), "-k"]
logger.debug("Killing all wine processes: %s" % command)
try:
proc = subprocess.Popen(command, env=env)
proc.wait()
except OSError:
logger.exception('Could not terminate wineserver %s', command)
winekill(self.prefix_path,
arch=self.wine_arch,
wine_path=self.get_executable(),
env=self.get_env(),
initial_pids=self.get_pids())
@staticmethod
def parse_wine_path(path, prefix_path=None):
"""Take a Windows path, return the corresponding Linux path."""
path = path.replace("\\\\", "/").replace('\\', '/')
if path.startswith('C'):
if not prefix_path:
prefix_path = os.path.expanduser("~/.wine")
path = os.path.join(prefix_path, 'drive_c', path[3:])
elif path[1] == ':':
# Trim Windows path
path = path[2:]
path = path.replace("\\\\", "/").replace('\\', '/')
if path[1] == ':': # absolute path
drive = os.path.join(prefix_path, 'dosdevices', path[:2].lower())
if os.path.islink(drive): # Try to resolve the path
drive = os.readlink(drive)
return os.path.join(drive, path[3:])
elif path[0] == '/': # drive-relative path. C is as good a guess as any..
return os.path.join(prefix_path, 'drive_c', path[1:])
else: # Relative path
return path

View file

@ -397,9 +397,6 @@ class winesteam(wine.wine):
launch_info = {}
launch_info['env'] = self.get_env(full=False)
if self.runner_config.get('xinput'):
launch_info['ld_preload'] = self.get_xinput_path()
if self.runner_config.get('x360ce-path'):
self.setup_x360ce(self.runner_config['x360ce-path'])
@ -434,8 +431,11 @@ class winesteam(wine.wine):
logger.debug("Stopping all winesteam processes")
super(winesteam, self).stop()
def killall_on_exit(self):
return bool(self.runner_config.get('quit_steam_on_exit'))
def stop(self):
if self.runner_config.get('quit_steam_on_exit'):
if self.killall_on_exit():
logger.debug("Game configured to stop Steam on exit")
self.shutdown()

View file

@ -39,12 +39,16 @@ APP_STATE_FLAGS = [
class AppManifest:
def __init__(self, appmanifest_path):
self.appmanifest_path = appmanifest_path
self.steamapps_path, filename = os.path.split(appmanifest_path)
self.steamid = re.findall(r'(\d+)', filename)[-1]
if os.path.exists(appmanifest_path):
with open(appmanifest_path, "r") as appmanifest_file:
self.appmanifest_data = vdf_parse(appmanifest_file, {})
def __repr__(self):
return "<AppManifest: %s>" % self.appmanifest_path
@property
def app_state(self):
return self.appmanifest_data.get('AppState') or {}

View file

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

View file

@ -41,6 +41,10 @@ def get_output_list():
return choices
def get_dri_prime():
return len(display.get_providers()) > 1
system_options = [
{
'option': 'game_path',
@ -80,6 +84,17 @@ system_options = [
"activating your NVIDIA graphic chip for high 3D "
"performance.")
},
{
'option': 'dri_prime',
'type': 'bool',
'default': False,
'condition': get_dri_prime,
'label': 'Use PRIME (hybrid graphics on laptops)',
'help': ("If you have open source graphic drivers (Mesa), selecting this "
"option will run the game with the 'DRI_PRIME=1' environment variable, "
"activating your discrete graphic chip for high 3D "
"performance.")
},
{
'option': 'sdl_video_fullscreen',
'type': 'choice',
@ -143,6 +158,27 @@ system_options = [
'help': ("Command line instructions to add in front of the game's "
"execution command.")
},
{
'option': 'include_processes',
'type': 'string',
'label': 'Include processes',
'advanced': True,
'help': ('What processes to include in process monitoring. '
'This is to override the built-in exclude list.\n'
'Space-separated list, processes including spaces '
'can be wrapped in quotation marks.')
},
{
'option': 'exclude_processes',
'type': 'string',
'label': 'Exclude processes',
'advanced': True,
'help': ('What processes to exclude in process monitoring. '
'For example background processes that stick around '
'after the game has been closed.\n'
'Space-separated list, processes including spaces '
'can be wrapped in quotation marks.')
},
{
'option': 'single_cpu',
'type': 'bool',
@ -211,7 +247,7 @@ system_options = [
'label': 'xboxdrv config',
'advanced': True,
'condition': system.find_executable('xboxdrv'),
'help': ("Command line options for xboxdrv, a driver for XBOX 360"
'help': ("Command line options for xboxdrv, a driver for XBOX 360 "
"controllers. Requires the xboxdrv package installed.")
},
{
@ -219,7 +255,7 @@ system_options = [
'type': 'string',
'label': 'SDL2 gamepad mapping',
'advanced': True,
'help': ("SDL_GAMECONTROLLERCONFIG mapping string or path to a custom"
'help': ("SDL_GAMECONTROLLERCONFIG mapping string or path to a custom "
"gamecontrollerdb.txt file containing mappings.")
},
{
@ -240,6 +276,7 @@ system_options = [
'option': 'xephyr_resolution',
'type': 'string',
'label': 'Xephyr resolution',
'advanced': True,
'help': 'Screen resolution of the Xephyr server'
},
]

View file

@ -9,6 +9,7 @@ import threading
import subprocess
import contextlib
from collections import defaultdict
from itertools import chain
from gi.repository import GLib
from textwrap import dedent
@ -23,13 +24,13 @@ HEARTBEAT_DELAY = 2000 # Number of milliseconds between each heartbeat
WARMUP_TIME = 5 * 60
MAX_CYCLES_WITHOUT_CHILDREN = 20
# List of process names that are ignored by the process monitoring
EXCLUDED_PROCESSES = (
EXCLUDED_PROCESSES = [
'lutris', 'python', 'python3',
'bash', 'sh', 'tee', 'tr', 'zenity', 'xkbcomp', 'xboxdrv',
'steam', 'Steam.exe', 'steamer', 'steamerrorrepor', 'gameoverlayui',
'SteamService.ex', 'steamwebhelper', 'steamwebhelper.', 'PnkBstrA.exe',
'control', 'winecfg.exe', 'wdfmgr.exe', 'wineconsole', 'winedbg',
)
]
class LutrisThread(threading.Thread):
@ -37,7 +38,7 @@ class LutrisThread(threading.Thread):
debug_output = True
def __init__(self, command, runner=None, env={}, rootpid=None, term=None,
watch=True, cwd=None, include_processes=[], log_buffer=None):
watch=True, cwd=None, include_processes=[], exclude_processes=[], log_buffer=None):
"""Thread init"""
threading.Thread.__init__(self)
self.env = env
@ -57,21 +58,21 @@ class LutrisThread(threading.Thread):
self.monitoring_started = False
self.daemon = True
self.error = None
self.include_processes = include_processes
if isinstance(include_processes, str):
include_processes = shlex.split(include_processes)
if isinstance(exclude_processes, str):
exclude_processes = shlex.split(exclude_processes)
# process names from /proc only contain 15 characters
self.include_processes = [x[0:15] for x in include_processes]
self.exclude_processes = [x[0:15] for x in (EXCLUDED_PROCESSES + exclude_processes)]
self.log_buffer = log_buffer
self.stdout_monitor = None
self.monitored_processes = defaultdict(list) # Keep a copy of the monitored processes to allow comparisons
# Keep a copy of previously running processes
self.old_pids = system.get_all_pids()
if cwd:
self.cwd = cwd
elif self.runner:
self.cwd = runner.working_dir
else:
self.cwd = '/tmp'
self.cwd = os.path.expanduser(self.cwd)
self.cwd = self.set_cwd(cwd)
self.env_string = ''
for (k, v) in self.env.items():
self.env_string += '%s="%s" ' % (k, v)
@ -80,6 +81,11 @@ class LutrisThread(threading.Thread):
['"%s"' % token for token in self.command]
)
def set_cwd(self, cwd):
if not cwd:
cwd = self.runner.working_dir if self.runner else '/tmp'
return os.path.expanduser(cwd)
def attach_thread(self, thread):
"""Attach child process that need to be killed on game exit."""
self.attached_threads.append(thread)
@ -115,9 +121,12 @@ class LutrisThread(threading.Thread):
if self.watch:
GLib.timeout_add(HEARTBEAT_DELAY, self.watch_children)
self.stdout_monitor = GLib.io_add_watch(self.game_process.stdout, GLib.IO_IN, self.on_stdout_output)
self.stdout_monitor = GLib.io_add_watch(self.game_process.stdout, GLib.IO_IN | GLib.IO_HUP, self.on_stdout_output)
def on_stdout_output(self, fd, condition):
if condition == GLib.IO_HUP:
self.stdout_monitor = None
return False
if not self.is_running:
return False
try:
@ -165,7 +174,7 @@ class LutrisThread(threading.Thread):
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
cwd=self.cwd, env=env)
except OSError as ex:
logger.error(ex)
logger.exception("Failed to execute %s: %s", ' '.join(command), ex)
self.error = ex.strerror
def iter_children(self, process, topdown=True, first=True):
@ -192,12 +201,8 @@ class LutrisThread(threading.Thread):
def set_stop_command(self, func):
self.stop_func = func
def stop(self, killall=False):
for thread in self.attached_threads:
logger.debug("Stopping thread %s", thread)
thread.stop()
if hasattr(self, 'stop_func'):
self.stop_func()
def restore_environment(self):
logger.debug("Restoring environment")
for key in self.original_env:
if self.original_env[key] is None:
try:
@ -206,22 +211,42 @@ class LutrisThread(threading.Thread):
pass
else:
os.environ[key] = self.original_env[key]
self.original_env = {}
def stop(self, killall=False):
for thread in self.attached_threads:
logger.debug("Stopping thread %s", thread)
thread.stop()
if hasattr(self, 'stop_func'):
self.stop_func()
self.restore_environment()
self.is_running = False
if not killall:
return
if killall:
self.killall()
def killall(self):
"""Kill every remaining child process"""
logger.debug("Killing all remaining processes")
killed_processes = []
for process in self.iter_children(Process(self.rootpid),
topdown=False):
logger.debug("Killing process %s", process)
killed_processes.append(str(process))
process.kill()
if killed_processes:
logger.debug("Killed processes: %s", ', '.join(killed_processes))
def watch_children(self):
"""Poke at the running process(es)."""
if not self.game_process:
logger.error('No game process available')
return False
def is_zombie(self):
return all([
p.endswith('Z')
for p in chain(*[
self.monitored_processes[key]
for key in self.monitored_processes
if key != 'external'
])
])
def get_processes(self):
process = Process(self.rootpid)
num_children = 0
num_watched_children = 0
@ -240,20 +265,38 @@ class LutrisThread(threading.Thread):
if child.pid in self.old_pids:
processes['external'].append(str(child))
continue
if child.name in EXCLUDED_PROCESSES and child.name not in self.include_processes:
if (child.name and child.name in self.exclude_processes and
child.name not in self.include_processes):
processes['excluded'].append(str(child))
continue
num_watched_children += 1
processes['monitored'].append(str(child))
if child.state == 'Z':
terminated_children += 1
for child in self.monitored_processes['monitored']:
if child not in processes['monitored']:
num_children += 1
num_watched_children += 1
terminated_children += 1
return processes, num_children, num_watched_children, terminated_children
def watch_children(self):
"""Poke at the running process(es)."""
if not self.game_process or not self.is_running:
logger.error('No game process available')
return False
processes, num_children, num_watched_children, terminated_children = self.get_processes()
if processes != self.monitored_processes:
self.monitored_processes = processes
logger.debug("Processes: " + " | ".join([
"{}: {}".format(key, ', '.join(processes[key]))
for key in processes if processes[key]
]))
if num_watched_children > 0 and not self.monitoring_started:
logger.debug("Start process monitoring")
self.monitoring_started = True
if self.runner and hasattr(self.runner, 'watch_game_process'):
@ -264,8 +307,12 @@ class LutrisThread(threading.Thread):
time_since_start = time.time() - self.startup_time
if self.monitoring_started or time_since_start > WARMUP_TIME:
self.cycles_without_children += 1
logger.debug("Cycles without children: %s", self.cycles_without_children)
else:
self.cycles_without_children = 0
max_cycles_reached = (self.cycles_without_children >=
MAX_CYCLES_WITHOUT_CHILDREN)
if num_children == 0 or max_cycles_reached:
if max_cycles_reached:
logger.debug('Maximum number of cycles without children reached')
@ -276,16 +323,27 @@ class LutrisThread(threading.Thread):
self.game_process.communicate()
else:
logger.debug('Some processes are still active (%d)', num_children)
if self.is_zombie():
logger.debug('Killing game process')
self.game_process.kill()
self.return_code = self.game_process.returncode
if self.stdout_monitor:
GLib.source_remove(self.stdout_monitor)
return False
if terminated_children and terminated_children == num_watched_children:
logger.debug("All children terminated")
self.game_process.wait()
logger.debug("Waiting for processes to exit")
try:
self.game_process.wait(2)
except subprocess.TimeoutExpired:
logger.warning("Processes are still running")
return True
if self.stdout_monitor:
logger.debug("Removing stdout monitor")
GLib.source_remove(self.stdout_monitor)
logger.debug("Thread is no longer running")
self.is_running = False
self.restore_environment()
return False
return True

View file

@ -1 +1,14 @@
""" Misc common functions """
def selective_merge(base_obj, delta_obj):
""" used by write_json """
if not isinstance(base_obj, dict):
return delta_obj
common_keys = set(base_obj).intersection(delta_obj)
new_keys = set(delta_obj).difference(common_keys)
for k in common_keys:
base_obj[k] = selective_merge(base_obj[k], delta_obj[k])
for k in new_keys:
base_obj[k] = delta_obj[k]
return base_obj

View file

@ -23,10 +23,22 @@ def get_outputs():
if parts[1] == 'connected':
if len(parts) == 2:
continue
geom = parts[2] if parts[2] != 'primary' else parts[3]
if parts[2] != 'primary':
geom = parts[2]
rotate = parts[3]
else:
geom = parts[3]
rotate = parts[4]
if geom.startswith('('): # Screen turned off, no geometry
continue
outputs.append((parts[0], geom))
if rotate.startswith('('): # Screen not rotated, no need to include
outputs.append((parts[0], geom, "normal"))
else:
if rotate in ("left", "right"):
geom_parts = geom.split('+')
x_y = geom_parts[0].split('x')
geom = "{}x{}+{}+{}".format(x_y[1], x_y[0], geom_parts[1], geom_parts[2])
outputs.append((parts[0], geom, rotate))
return outputs
@ -85,14 +97,55 @@ def change_resolution(resolution):
display_resolution = display_geom[0]
position = (display_geom[1], display_geom[2])
if (
len(display) > 2 and
display[2] in ('normal', 'left', 'right', 'inverted')
):
rotation = display[2]
else:
rotation = "normal"
subprocess.Popen([
"xrandr",
"--output", display_name,
"--mode", display_resolution,
"--pos", "{}x{}".format(position[0], position[1])
"--pos", "{}x{}".format(position[0], position[1]),
"--rotate", rotation
]).communicate()
def restore_gamma():
"""Restores gamma to a normal level."""
subprocess.Popen(["xgamma", "-gamma", "1.0"])
def get_xrandr_version():
"""Return the major and minor version of XRandR utility"""
pattern = "version"
xrandr_output = subprocess.Popen(["xrandr", "--version"],
stdout=subprocess.PIPE).communicate()[0].decode()
position = xrandr_output.find(pattern) + len(pattern)
version_str = xrandr_output[position:].strip().split(".")
try:
return {"major": int(version_str[0]), "minor": int(version_str[1])}
except ValueError:
logger.error("Can't find version in: %s", xrandr_output)
return {"major": 0, "minor": 0}
def get_providers():
"""Return the list of available graphic cards"""
pattern = "name:"
providers = list()
version = get_xrandr_version()
if version["major"] == 1 and version["minor"] >= 4:
xrandr_output = subprocess.Popen(["xrandr", "--listproviders"],
stdout=subprocess.PIPE).communicate()[0].decode()
for line in xrandr_output.split("\n"):
if line.find("Provider ") != 0:
continue
position = line.find(pattern) + len(pattern)
providers.append(line[position:].strip())
return providers

View file

@ -1,6 +1,6 @@
import os
import signal
from lutris.util.log import logger
from lutris.util.system import kill_pid
class InvalidPid(Exception):
@ -50,12 +50,13 @@ class Process(object):
def get_children_pids_of_thread(self, tid):
"""Return pids of child processes opened by thread `tid` of process."""
children = []
children_path = '/proc/{}/task/{}/children'.format(self.pid, tid)
if os.path.exists(children_path):
try:
with open(children_path) as children_file:
children = children_file.read().strip().split()
return children
children_content = children_file.read()
except FileNotFoundError:
children_content = ''
return children_content.strip().split()
def get_children(self):
self.children = []
@ -108,8 +109,12 @@ class Process(object):
cwd_path = '/proc/%d/cwd' % int(self.pid)
return os.readlink(cwd_path)
def kill(self):
try:
os.kill(self.pid, signal.SIGKILL)
except OSError:
logger.error("Could not kill process %s", self.pid)
def kill(self, killed_processes=None):
if not killed_processes:
killed_processes = set()
for child_pid in reversed(sorted(self.get_thread_ids())):
child = Process(child_pid)
if child.pid not in killed_processes:
killed_processes.add(child.pid)
child.kill(killed_processes)
kill_pid(self.pid)

View file

@ -31,6 +31,12 @@ def vdf_parse(steam_config_file, config):
return config
if not line or line.strip() == "}":
return config
while not line.strip().endswith("\""):
nextline = steam_config_file.readline()
if not nextline:
break
line = line[:-1] + nextline
line_elements = line.strip().split("\"")
if len(line_elements) == 3:
key = line_elements[1]

View file

@ -1,4 +1,5 @@
import hashlib
import signal
import os
import re
import shutil
@ -142,11 +143,14 @@ def get_all_pids():
def kill_pid(pid):
try:
int(pid)
pid = int(pid)
except ValueError:
logger.error("Invalid pid %s")
return
execute(['kill', '-9', pid])
try:
os.kill(pid, signal.SIGKILL)
except OSError:
logger.error("Could not kill process %s", pid)
def get_command_line(pid):
@ -287,7 +291,7 @@ def get_pids_using_file(path):
logger.warning("fuser not available, please install psmisc")
return set([])
else:
fuser_output = execute([fuser_path, path])
fuser_output = execute([fuser_path, path], quiet=True)
return set(fuser_output.split())

182
po/cs.po Normal file
View file

@ -0,0 +1,182 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the lutris package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-11-09 21:49+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Czech <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../data/ui/AboutDialog.ui.h:1
msgid "About Lutris"
msgstr "O Lutris"
#: ../data/ui/AboutDialog.ui.h:2
msgid "© 2010, 2012 Mathieu Comandon"
msgstr "© 2010, 2012 Mathieu Comandon"
#: ../data/ui/AboutDialog.ui.h:3
msgid "Open Source Gaming Platform"
msgstr "Otevřená herní platforma"
#: ../data/ui/AboutDialog.ui.h:4
msgid "http://lutris.net"
msgstr "http://lutris.net"
#: ../data/ui/AboutDialog.ui.h:5
msgid ""
"This program is free software: you can redistribute it and/or modify\n"
"it under the terms of the GNU General Public License as published by\n"
"the Free Software Foundation, either version 3 of the License, or\n"
"(at your option) any later version.\n"
"\n"
"This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
"GNU General Public License for more details.\n"
"\n"
"You should have received a copy of the GNU General Public License\n"
"along with this program. If not, see <http://www.gnu.org/licenses/>.\n"
msgstr ""
#: ../data/ui/LutrisWindow3.ui.h:1 ../data/ui/LutrisWindow.ui.h:1
msgid "Lutris"
msgstr "Lutris"
#: ../data/ui/LutrisWindow3.ui.h:2
msgid "_File"
msgstr "_Soubor"
#: ../data/ui/LutrisWindow3.ui.h:3 ../data/ui/LutrisWindow.ui.h:9
msgid "_Edit"
msgstr "_Upravit"
#: ../data/ui/LutrisWindow3.ui.h:4 ../data/ui/LutrisWindow.ui.h:11
msgid "_View"
msgstr "_Zobrazit"
#: ../data/ui/LutrisWindow3.ui.h:5 ../data/ui/LutrisWindow.ui.h:16
msgid "_Help"
msgstr "_Nápověda"
#: ../data/ui/LutrisWindow3.ui.h:6 ../data/ui/LutrisWindow.ui.h:21
msgid "Add game"
msgstr "Přidat hru"
#: ../data/ui/LutrisWindow3.ui.h:7 ../data/ui/LutrisWindow.ui.h:18
msgid "Play"
msgstr "Hrát"
#: ../data/ui/LutrisWindow3.ui.h:8 ../data/ui/LutrisWindow.ui.h:20
msgid "Stop"
msgstr "Stop"
#: ../data/ui/LutrisWindow3.ui.h:9 ../data/ui/LutrisWindow.ui.h:24
msgid "Remove"
msgstr "Odstranit"
#: ../data/ui/LutrisWindow.ui.h:2
msgid "_Lutris"
msgstr "_Lutris"
#: ../data/ui/LutrisWindow.ui.h:3
msgid "Connect"
msgstr "Připojit"
#: ../data/ui/LutrisWindow.ui.h:4
msgid "Install and configure runners"
msgstr "Instalovat a nastavit spouštěče"
#: ../data/ui/LutrisWindow.ui.h:5
msgid "Manage runners"
msgstr "Spravovat spouštěče"
#: ../data/ui/LutrisWindow.ui.h:6
msgid "Import"
msgstr "Importovat"
#: ../data/ui/LutrisWindow.ui.h:7
msgid "Import games from ScummVM"
msgstr "Importovat hry ze ScummVM"
#: ../data/ui/LutrisWindow.ui.h:8
msgid "ScummVM"
msgstr "ScummVM"
#: ../data/ui/LutrisWindow.ui.h:10
msgid "configure the default options"
msgstr "nastavit výchozí hodnoty"
#: ../data/ui/LutrisWindow.ui.h:12
msgid "Icons"
msgstr "Ikony"
#: ../data/ui/LutrisWindow.ui.h:13
msgid "List"
msgstr "Seznam"
#: ../data/ui/LutrisWindow.ui.h:14
msgid "_Game"
msgstr "_Hra"
#: ../data/ui/LutrisWindow.ui.h:15
msgid "Start"
msgstr "Start"
#: ../data/ui/LutrisWindow.ui.h:17
msgid "Play game"
msgstr "Hrát hru"
#: ../data/ui/LutrisWindow.ui.h:19
msgid "Stop game"
msgstr "Zastavit hru"
#: ../data/ui/LutrisWindow.ui.h:22
msgid "Add Game"
msgstr "Přidat hru"
#: ../data/ui/LutrisWindow.ui.h:23
msgid "Remove game from library"
msgstr "Odstranit hru z knihovny"
#: ../data/ui/LutrisWindow.ui.h:25
msgid "Filter the list of games"
msgstr ""
#: ../data/ui/AboutLutrisDialog.ui.h:1
msgid ""
"# Copyright (C) 2010 Mathieu Comandon <strycore@gmail.com>\n"
"# This program is free software: you can redistribute it and/or modify it\n"
"# under the terms of the GNU General Public License version 3, as published\n"
"# by the Free Software Foundation.\n"
"#\n"
"# This program is distributed in the hope that it will be useful, but\n"
"# WITHOUT ANY WARRANTY; without even the implied warranties of\n"
"# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR\n"
"# PURPOSE. See the GNU General Public License for more details.\n"
"#\n"
"# You should have received a copy of the GNU General Public License along\n"
"# with this program. If not, see <http://www.gnu.org/licenses/>.\n"
msgstr ""
#: ../data/ui/AboutLutrisDialog.ui.h:14
msgid "Copyright (C) 2010 Mathieu Comandon <strycore@gmail.com>"
msgstr ""
#: ../data/ui/PreferencesLutrisDialog.ui.h:1
msgid "gtk-cancel"
msgstr ""
#: ../data/ui/PreferencesLutrisDialog.ui.h:2
msgid "gtk-ok"
msgstr ""

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.