mirror of
https://github.com/lutris/lutris
synced 2024-10-14 11:42:36 +00:00
Implementation of Discord Rich Presence, along with some typo/flake8 fixes
This commit is contained in:
parent
2c41d9a86e
commit
383db48ccc
|
@ -20,12 +20,12 @@ class LutrisConfig:
|
|||
|
||||
Description
|
||||
===========
|
||||
Lutris' configuration uses a cascading mecanism where
|
||||
Lutris' configuration uses a cascading mechanism where
|
||||
each higher, more specific level overrides the lower ones
|
||||
|
||||
The levels are (highest to lowest): `game`, `runner` and `system`.
|
||||
Each level has its own set of options (config section), available to and
|
||||
overriden by upper levels:
|
||||
overridden by upper levels:
|
||||
```
|
||||
level | Config sections
|
||||
-------|----------------------
|
||||
|
|
113
lutris/game.py
113
lutris/game.py
|
@ -18,7 +18,11 @@ from lutris.config import LutrisConfig
|
|||
from lutris.command import MonitoredCommand
|
||||
from lutris.gui import dialogs
|
||||
from lutris.util.timer import Timer
|
||||
from lutris.settings import DEFAULT_DISCORD_CLIENT_ID
|
||||
|
||||
from pypresence import Presence
|
||||
from pypresence.exceptions import PyPresenceException
|
||||
import asyncio
|
||||
|
||||
HEARTBEAT_DELAY = 2000
|
||||
|
||||
|
@ -63,6 +67,11 @@ class Game(GObject.Object):
|
|||
self.steamid = game_data.get("steamid") or ""
|
||||
self.has_custom_banner = bool(game_data.get("has_custom_banner"))
|
||||
self.has_custom_icon = bool(game_data.get("has_custom_icon"))
|
||||
self.discord_last_rpc = 0
|
||||
self.discord_rpc_interval = 60
|
||||
self.discord_presence_connected = False
|
||||
self.discord_rpc_client = None
|
||||
self.discord_client_id = None
|
||||
try:
|
||||
self.playtime = float(game_data.get("playtime") or 0.0)
|
||||
except ValueError:
|
||||
|
@ -85,6 +94,7 @@ class Game(GObject.Object):
|
|||
self.original_outputs = None
|
||||
self._log_buffer = None
|
||||
self.timer = Timer()
|
||||
self.discord_rpc_client = Presence(self.discord_client_id)
|
||||
|
||||
@property
|
||||
def log_buffer(self):
|
||||
|
@ -159,6 +169,11 @@ class Game(GObject.Object):
|
|||
runner_slug=self.runner_name, game_config_id=self.game_config_id
|
||||
)
|
||||
self.runner = self._get_runner()
|
||||
self.discord_client_id = self.config.game_config.get("discord_client_id") or DEFAULT_DISCORD_CLIENT_ID
|
||||
self.discord_custom_game_name = self.config.game_config.get("discord_custom_game_name") or ""
|
||||
self.discord_show_runner = self.config.game_config.get("discord_show_runner", True)
|
||||
self.discord_custom_runner_name = self.config.game_config.get("discord_custom_runner_name") or ""
|
||||
self.discord_rpc_enabled = self.config.game_config.get("discord_rpc_enabled", True)
|
||||
|
||||
def set_desktop_compositing(self, enable):
|
||||
"""Enables or disables compositing"""
|
||||
|
@ -585,6 +600,9 @@ class Game(GObject.Object):
|
|||
logger.debug("Game thread stopped")
|
||||
self.on_game_quit()
|
||||
return False
|
||||
if int(time.time()) - self.discord_rpc_interval > self.discord_last_rpc:
|
||||
self.discord_last_rpc = int(time.time())
|
||||
self.update_discord_rich_presence()
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
|
@ -623,6 +641,7 @@ class Game(GObject.Object):
|
|||
cwd=self.directory,
|
||||
)
|
||||
postexit_thread.start()
|
||||
self.clear_discord_rich_presence()
|
||||
|
||||
quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
|
||||
logger.debug("%s stopped at %s", self.name, quit_time)
|
||||
|
@ -680,3 +699,97 @@ class Game(GObject.Object):
|
|||
appmanifest.steamid,
|
||||
)
|
||||
self.game_thread.ready_state = False
|
||||
|
||||
def ensure_discord_connected(self):
|
||||
"""Make sure we are actually connected before trying to send requests"""
|
||||
logger.debug("Ensuring connected.")
|
||||
if self.discord_presence_connected:
|
||||
logger.debug("Already connected!")
|
||||
else:
|
||||
logger.debug("Creating Presence object.")
|
||||
self.discord_rpc_client = Presence(self.discord_client_id)
|
||||
try:
|
||||
logger.debug("Attempting to connect.")
|
||||
self.discord_rpc_client.connect()
|
||||
self.discord_presence_connected = True
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to reach Discord. Skipping update: {e}")
|
||||
self.ensure_discord_disconnected()
|
||||
return self.discord_presence_connected
|
||||
|
||||
def ensure_discord_disconnected(self):
|
||||
"""Ensure we are definitely disconnected and fix broken event loop from pypresence"""
|
||||
logger.debug("Ensuring disconnected.")
|
||||
if self.discord_rpc_client is not None:
|
||||
try:
|
||||
self.discord_rpc_client.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to close Discord RPC connection: {e}")
|
||||
if self.discord_rpc_client.sock_writer is not None:
|
||||
try:
|
||||
logger.debug("Forcefully closing sock writer.")
|
||||
self.discord_rpc_client.sock_writer.close()
|
||||
except Exception:
|
||||
logger.debug("Sock writer could not be closed.")
|
||||
try:
|
||||
logger.debug("Forcefully closing event loop.")
|
||||
self.discord_rpc_client.loop.close()
|
||||
except Exception:
|
||||
logger.debug("Could not close event loop.")
|
||||
try:
|
||||
logger.debug("Forcefully replacing event loop.")
|
||||
self.discord_rpc_client.loop = None
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not replace event loop: {e}")
|
||||
try:
|
||||
logger.debug("Forcefully deleting RPC client.")
|
||||
del self.discord_rpc_client
|
||||
except Exception:
|
||||
pass
|
||||
self.discord_rpc_client = None
|
||||
self.discord_presence_connected = False
|
||||
|
||||
def update_discord_rich_presence(self):
|
||||
"""Dispatch a request to Discord to update presence"""
|
||||
if self.discord_rpc_enabled:
|
||||
logger.debug("RPC is enabled")
|
||||
connected = self.ensure_discord_connected()
|
||||
if not connected:
|
||||
return
|
||||
try:
|
||||
if self.discord_custom_game_name != "":
|
||||
logger.debug(f"Got custom game name: {self.discord_custom_game_name}")
|
||||
game_name = self.discord_custom_game_name
|
||||
else:
|
||||
logger.debug("Using default name")
|
||||
game_name = self.name
|
||||
if self.discord_show_runner:
|
||||
if self.discord_custom_runner_name != "":
|
||||
logger.debug(f"Got custom runner name: {self.discord_custom_runner_name}")
|
||||
runner_name = self.discord_custom_runner_name
|
||||
else:
|
||||
logger.debug("Using default runner name")
|
||||
runner_name = self.runner_name
|
||||
if runner_name != "":
|
||||
state_text = f"via {runner_name}"
|
||||
else:
|
||||
state_text = " "
|
||||
logger.info(f"Attempting to update Discord status: {game_name}, {state_text}")
|
||||
self.discord_rpc_client.update(details=f"Playing {game_name}", state=state_text)
|
||||
except PyPresenceException as e:
|
||||
logger.error(f"Unable to update Discord: {e}")
|
||||
else:
|
||||
logger.debug("RPC disabled")
|
||||
|
||||
def clear_discord_rich_presence(self):
|
||||
"""Dispatch a request to Discord to clear presence"""
|
||||
if self.discord_rpc_enabled:
|
||||
connected = self.ensure_discord_connected()
|
||||
if connected:
|
||||
try:
|
||||
logger.info('Attempting to clear Discord status.')
|
||||
self.discord_rpc_client.clear()
|
||||
except PyPresenceException as e:
|
||||
logger.error(f"Unable to clear Discord: {e}")
|
||||
self.ensure_discord_disconnected()
|
||||
|
|
|
@ -25,10 +25,6 @@ from gettext import gettext as _
|
|||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GnomeDesktop", "3.0")
|
||||
|
||||
from gi.repository import Gio, GLib, Gtk
|
||||
from lutris import pga
|
||||
from lutris.game import Game
|
||||
|
@ -51,6 +47,10 @@ from lutris.util.wine.dxvk import init_dxvk_versions
|
|||
from .lutriswindow import LutrisWindow
|
||||
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GnomeDesktop", "3.0")
|
||||
|
||||
class Application(Gtk.Application):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
|
|
|
@ -41,7 +41,7 @@ LIBRETRO_CORES = [
|
|||
("MAME (Arcade)", "mame", "Arcade"),
|
||||
("Mednafen GBA (Game Boy Advance)", "mednafen_gba", "Nintendo Game Boy Advance"),
|
||||
("Mednafen NGP (SNK Neo Geo Pocket)", "mednafen_ngp", "SNK Neo Geo Pocket"),
|
||||
("Mednafen PCE (TurboGrafx-16)", "mednafen_pce", "NEC PC Engine (TurboGrafx-16)"),
|
||||
("Mednafen PCE (TurboGrafx-16)", "mednafen_pce", "NEC PC Engine (TurboGrafx-16)"),
|
||||
("Mednafen PCE FAST (TurboGrafx-16)", "mednafen_pce_fast", "NEC PC Engine (TurboGrafx-16)"),
|
||||
("Mednafen PCFX (NEC PC-FX)", "mednafen_pcfx", "NEC PC-FX"),
|
||||
("Mednafen Saturn (Sega Saturn)", "mednafen_saturn", "Sega Saturn"),
|
||||
|
|
|
@ -26,6 +26,40 @@ class Runner:
|
|||
context_menu_entries = []
|
||||
depends_on = None
|
||||
runner_executable = None
|
||||
common_game_options = [
|
||||
{
|
||||
"option": "discord_rpc_enabled",
|
||||
"type": "bool",
|
||||
"label": "Discord Rich Presence",
|
||||
"default": True,
|
||||
"help": "Enable notification to Discord of this game being played",
|
||||
},
|
||||
{
|
||||
"option": "discord_show_runner",
|
||||
"type": "bool",
|
||||
"label": "Discord Show Runner",
|
||||
"default": True,
|
||||
"help": "Embed the runner name in the Discord notification",
|
||||
},
|
||||
{
|
||||
"option": "discord_custom_game_name",
|
||||
"type": "string",
|
||||
"label": "Discord Custom Game Name",
|
||||
"help": "Custom name to override with and send to Discord",
|
||||
},
|
||||
{
|
||||
"option": "discord_custom_runner_name",
|
||||
"type": "string",
|
||||
"label": "Discord Custom Runner Name",
|
||||
"help": "Custom runner name to override with and send to Discord",
|
||||
},
|
||||
{
|
||||
"option": "discord_client_id",
|
||||
"type": "string",
|
||||
"label": "Discord Client ID",
|
||||
"help": "Custom Discord Client ID for sending notifications",
|
||||
},
|
||||
]
|
||||
|
||||
def __init__(self, config=None):
|
||||
"""Initialize runner."""
|
||||
|
@ -36,6 +70,7 @@ class Runner:
|
|||
self.game_data = pga.get_game_by_field(
|
||||
self.config.game_config_id, "configpath"
|
||||
)
|
||||
self.inject_common_game_options()
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.name < other.name
|
||||
|
@ -106,6 +141,31 @@ class Runner:
|
|||
"""Return the working directory to use when running the game."""
|
||||
return self.game_path or os.path.expanduser("~/")
|
||||
|
||||
@property
|
||||
def discord_rpc_enabled(self):
|
||||
if self.game_data.get("discord_rpc_enabled"):
|
||||
return self.game_data.get("discord_rpc_enabled")
|
||||
|
||||
@property
|
||||
def discord_show_runner(self):
|
||||
if self.game_data.get("discord_show_runner"):
|
||||
return self.game_data.get("discord_show_runner")
|
||||
|
||||
@property
|
||||
def discord_custom_game_name(self):
|
||||
if self.game_data.get("discord_custom_game_name"):
|
||||
return self.game_data.get("discord_custom_game_name")
|
||||
|
||||
@property
|
||||
def discord_custom_runner_name(self):
|
||||
if self.game_data.get("discord_custom_runner_name"):
|
||||
return self.game_data.get("discord_custom_runner_name")
|
||||
|
||||
@property
|
||||
def discord_client_id(self):
|
||||
if self.game_data.get("discord_client_id"):
|
||||
return self.game_data.get("discord_client_id")
|
||||
|
||||
def get_platform(self):
|
||||
return self.platforms[0]
|
||||
|
||||
|
@ -365,3 +425,9 @@ class Runner:
|
|||
runner_path = os.path.join(settings.RUNNER_DIR, self.name)
|
||||
if os.path.isdir(runner_path):
|
||||
system.remove_folder(runner_path)
|
||||
|
||||
def inject_common_game_options(self):
|
||||
"""Dynamically add all the common game options to all runner classes"""
|
||||
for item in self.common_game_options:
|
||||
if item not in self.game_options:
|
||||
self.game_options.append(item)
|
||||
|
|
|
@ -325,7 +325,7 @@ class steam(Runner):
|
|||
else:
|
||||
command.append("-applaunch")
|
||||
command.append(self.appid)
|
||||
|
||||
|
||||
if game_args:
|
||||
for arg in shlex.split(game_args):
|
||||
command.append(arg)
|
||||
|
|
|
@ -37,5 +37,7 @@ ICON_URL = SITE_URL + "/games/icon/%s.png"
|
|||
BANNER_URL = SITE_URL + "/games/banner/%s.jpg"
|
||||
RUNTIME_URL = "https://lutris.net/api/runtime"
|
||||
|
||||
DEFAULT_DISCORD_CLIENT_ID = "576189919748161556"
|
||||
|
||||
read_setting = sio.read_setting
|
||||
write_setting = sio.write_setting
|
||||
|
|
Loading…
Reference in a new issue