mirror of
https://github.com/lutris/lutris
synced 2024-10-04 14:59:37 +00:00
Merge branch 'master' into next
This commit is contained in:
commit
bc299cb934
|
@ -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
|
||||
|
|
|
@ -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
28
debian/changelog
vendored
|
@ -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
|
||||
|
|
|
@ -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,19 +65,50 @@ 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):
|
||||
|
||||
::
|
||||
|
||||
game:
|
||||
exe: drive_c/Game/game.exe
|
||||
prefix: $GAMEDIR
|
||||
args: -arg
|
||||
exe: drive_c/Game/game.exe
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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('&', "&")
|
||||
runner = None
|
||||
platform = ''
|
||||
|
@ -139,15 +136,21 @@ class GameStore(GObject.Object):
|
|||
if runner_name in self.runner_names:
|
||||
runner_human_name = self.runner_names[runner_name]
|
||||
else:
|
||||
runner = runners.import_runner(runner_name)
|
||||
runner_human_name = runner.human_name
|
||||
self.runner_names[runner_name] = runner_human_name
|
||||
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
|
||||
|
||||
lastplayed = ''
|
||||
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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
# Determine whether 'file' value is a file id or a path
|
||||
exec_path = self._get_file(file_ref) or self._substitute(file_ref)
|
||||
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
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
# 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:
|
||||
controls = []
|
||||
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}
|
||||
|
|
|
@ -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'),
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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,33 +917,35 @@ 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))
|
||||
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':
|
||||
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)
|
||||
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')
|
||||
|
||||
x360ce_config = X360ce()
|
||||
x360ce_config.populate_controllers()
|
||||
x360ce_config.write(os.path.join(x360ce_path, 'x360ce.ini'))
|
||||
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):
|
||||
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)
|
||||
|
||||
x360ce_config = X360ce()
|
||||
x360ce_config.populate_controllers()
|
||||
x360ce_config.write(os.path.join(x360ce_path, 'x360ce.ini'))
|
||||
|
||||
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."""
|
||||
if not prefix_path:
|
||||
prefix_path = os.path.expanduser("~/.wine")
|
||||
|
||||
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:]
|
||||
return path
|
||||
|
||||
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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
]
|
||||
|
|
130
lutris/thread.py
130
lutris/thread.py
|
@ -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
|
||||
self.killall()
|
||||
|
||||
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
|
||||
|
||||
logger.debug("Processes: " + " | ".join([
|
||||
"{}: {}".format(key, ', '.join(processes[key]))
|
||||
for key in processes if processes[key]
|
||||
]))
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
182
po/cs.po
Normal 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
BIN
share/lutris/controllers/dumbxinputemu-win32/xinput1_3.dll
Executable file
BIN
share/lutris/controllers/dumbxinputemu-win32/xinput1_3.dll
Executable file
Binary file not shown.
BIN
share/lutris/controllers/dumbxinputemu-win32/xinput9_1_0.dll
Executable file
BIN
share/lutris/controllers/dumbxinputemu-win32/xinput9_1_0.dll
Executable file
Binary file not shown.
BIN
share/lutris/controllers/dumbxinputemu-win64/xinput1_3.dll
Executable file
BIN
share/lutris/controllers/dumbxinputemu-win64/xinput1_3.dll
Executable file
Binary file not shown.
BIN
share/lutris/controllers/dumbxinputemu-win64/xinput9_1_0.dll
Executable file
BIN
share/lutris/controllers/dumbxinputemu-win64/xinput9_1_0.dll
Executable file
Binary file not shown.
BIN
share/lutris/controllers/x360ce-win64/dinput8.dll
Normal file
BIN
share/lutris/controllers/x360ce-win64/dinput8.dll
Normal file
Binary file not shown.
Loading…
Reference in a new issue