Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/actions/setup-python/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ inputs:
runs:
using: 'composite'
steps:
- name: GH Actions silliness (please add lower)
id: silly
shell: bash
run: |
echo "pyarch=$(echo ${{ inputs.architecture }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- uses: actions/setup-python@v7
with:
cache: pip
python-version: ${{ inputs.version }}
check-latest: true
architecture: ${{ inputs.architecture }}
architecture: ${{ steps.silly.outputs.pyarch }}
4 changes: 1 addition & 3 deletions packaging/requirements-dev.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
pylint
mypy
black[d]
PySide6-stubs
qstylizer
ruff
ty
19 changes: 10 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies = [
"pywin32 ; platform_system == 'Windows'",
]

[project.optional-dependencies]
[dependency-groups]
webview = [
"pywebview[gtk]; platform_system == 'Linux'",
"pywebview[gtk]; platform_system == 'FreeBSD'",
Expand All @@ -42,18 +42,18 @@ webview = [
discordrpc = [
"pypresence",
]
packaging = [
"cx-freeze",
"nuitka",
]
dev = [
"pylint",
"mypy",
"black[d]",
"PySide6-stubs",
"qstylizer",
"ruff",
]
packaging = [
"cx-freeze",
"nuitka",
"pyinstaller",
"ty",
{include-group = "webview"},
{include-group = "discordrpc"},
{include-group = "packaging"},
]

[project.urls]
Expand Down Expand Up @@ -133,6 +133,7 @@ force-exclude = '''
'''

[tool.ruff]
target-version = "py310"
include = ["pyproject.toml", "rare/*.py"]
exclude = [
"rare/ui",
Expand Down
2 changes: 1 addition & 1 deletion rare/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@

sys.exit(main())

__all__ = ['__version__', '__version_tuple__', '__codename__']
__all__ = ['__codename__', '__version__', '__version_tuple__']
63 changes: 39 additions & 24 deletions rare/commands/launcher/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
from rare.lgndr.core import LegendaryCore
from rare.models.game_slim import RareGameSlim
from rare.models.launcher import Actions, BaseModel, ErrorModel, FinishedModel, StateChangedModel
from rare.shared.workers.cloud_sync import CloudSyncWorker
from rare.shared.workers.cloud import CloudSyncWorker
from rare.utils.paths import get_rare_executable
from rare.widgets.rare_app import RareApp, RareAppException

from .cloud_sync_dialog import CloudSyncDialog, CloudSyncDialogResult
from .cloud_dialog import CloudSyncDialog, CloudSyncDialogResult
from .console_dialog import ConsoleDialog
from .lgd_helper import InitParams, LaunchParams, dict_to_qprocenv, get_configured_qprocess, get_launch_params

Expand Down Expand Up @@ -83,7 +83,7 @@ def run(self) -> None:
def prepare_launch(self, args: InitParams) -> LaunchParams | None:
try:
launch = get_launch_params(self.rgame, args)
except Exception as e:
except Exception as e: # noqa: BLE001
self.signals.error_occurred.emit(str(e))
return None
if not launch:
Expand Down Expand Up @@ -122,7 +122,7 @@ def __init__(self, core: LegendaryCore, rgame: RareGameSlim):
def run(self) -> None:
try:
self.rgame.update_saves()
except Exception as e:
except Exception as e: # noqa: BLE001
self.signals.error_occurred.emit(str(e))
return
self.signals.sync_state_ready.emit()
Expand Down Expand Up @@ -203,16 +203,28 @@ def __init__(self, args: InitParams):

@Slot(QProcess.ProcessState)
def _on_game_process_changed(self, state: QProcess.ProcessState):
if not self.console:
return
self.console.kill_button.setEnabled(state == QProcess.ProcessState.Running)
self.console.terminate_button.setEnabled(state == QProcess.ProcessState.Running)

@Slot()
def _proc_log_stdout(self):
self.console.log_stdout(self.game_process.readAllStandardOutput().data().decode('utf-8', 'ignore'))
if not self.console:
return
data = self.game_process.readAllStandardOutput().data()
if isinstance(data, memoryview):
data = data.tobytes()
self.console.log_stdout(data.decode('utf-8', 'ignore'))

@Slot()
def _proc_log_stderr(self):
self.console.log_stderr(self.game_process.readAllStandardError().data().decode('utf-8', 'ignore'))
if not self.console:
return
data = self.game_process.readAllStandardError().data()
if isinstance(data, memoryview):
data = data.tobytes()
self.console.log_stderr(data.decode('utf-8', 'ignore'))

@Slot()
def _proc_term(self):
Expand All @@ -233,37 +245,39 @@ def new_server_connection(self):
with contextlib.suppress(RuntimeError):
self.socket.disconnectFromServer()
self.logger.info('New connection')
self.socket = self.server.nextPendingConnection()
self.socket.disconnected.connect(self.socket_disconnected)
self.socket.flush()
socket = self.server.nextPendingConnection()
socket.disconnected.connect(self.socket_disconnected)
socket.flush()
self.socket = socket

def socket_disconnected(self):
self.logger.info('Server disconnected')
self.socket = None

def send_message(self, message: BaseModel):
if self.socket:
self.socket.write(json.dumps(vars(message)).encode('utf-8'))
self.socket.flush()
else:
if not self.socket:
self.logger.error("Can't send message")
return
self.socket.write(json.dumps(vars(message)).encode('utf-8'))
self.socket.flush()

def check_saves(self, exit_code: int):
# self.rgame.signals.widget.refresh.connect(lambda: self.on_exit(exit_code))
self.rgame.signals.widget.refresh.connect((lambda obj: obj.on_exit(exit_code)).__get__(self))

state, (dt_local, dt_remote) = self.rgame.save_game_state
status, (dt_local, dt_remote) = self.rgame.save_game_state

if state == SaveGameStatus.LOCAL_NEWER and not self.no_sync_on_exit:
if status == SaveGameStatus.LOCAL_NEWER and not self.no_sync_on_exit:
action = CloudSyncDialogResult.UPLOAD
self.check_saves_finished(exit_code, action)
else:
self.sync_dialog = CloudSyncDialog(self.rgame.igame, dt_local, dt_remote)
sync_dialog = CloudSyncDialog(self.rgame.igame, status, dt_local, dt_remote)
# self.sync_dialog.result_ready.connect(
# lambda a: self.__check_saves_finished(exit_code, a)
# )
self.sync_dialog.result_ready.connect((lambda obj, a: obj.check_saves_finished(exit_code, a)).__get__(self))
self.sync_dialog.open()
sync_dialog.result_ready.connect((lambda obj, a: obj.check_saves_finished(exit_code, a)).__get__(self))
sync_dialog.open()
self.sync_dialog = sync_dialog

@Slot(int, int)
@Slot(int, CloudSyncDialogResult)
Expand Down Expand Up @@ -365,7 +379,7 @@ def launch_game(self, params: LaunchParams):
cmd_line = get_rare_executable()
executable, arguments = cmd_line[0], cmd_line[1:]

if appid := os.environ.get('SteamGameId', False): # noqa: SIM112
if appid := os.environ.get('SteamGameId', None): # noqa: SIM112
params.environment['SteamGameId'] = appid
elif params.environment.get('SteamGameId', False):
appid = params.environment['SteamGameId']
Expand Down Expand Up @@ -426,10 +440,11 @@ def sync_ready(self):
self.start_prepare()
return

_, (dt_local, dt_remote) = self.rgame.save_game_state
self.sync_dialog = CloudSyncDialog(self.rgame.igame, dt_local, dt_remote)
self.sync_dialog.result_ready.connect(self.__sync_ready)
self.sync_dialog.open()
status, (dt_local, dt_remote) = self.rgame.save_game_state
sync_dialog = CloudSyncDialog(self.rgame.igame, status, dt_local, dt_remote)
sync_dialog.result_ready.connect(self.__sync_ready)
sync_dialog.open()
self.sync_dialog = sync_dialog

@Slot(int)
@Slot(CloudSyncDialogResult)
Expand Down Expand Up @@ -534,7 +549,7 @@ def signal_handler(sig, frame):

try:
exit_code = app.exec()
except Exception as e:
except Exception as e: # noqa: BLE001
app.logger.error('Unhandled error %s', e)
exit_code = 1
finally:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import sys
from datetime import datetime
from datetime import datetime, timezone
from enum import IntEnum
from logging import getLogger

from legendary.models.game import InstalledGame
from legendary.models.game import InstalledGame, SaveGameStatus
from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import QApplication, QDialog, QLabel, QVBoxLayout, QWidget
from PySide6.QtWidgets import QApplication, QDialog, QLabel, QVBoxLayout

from rare.lgndr.core import LegendaryCore
from rare.ui.components.tabs.library.details.cloud_sync_widget import Ui_CloudSyncWidget
from rare.utils.misc import qta_icon
from rare.widgets.cloudsync_widget import CloudSyncWidget
from rare.widgets.dialogs import ButtonDialog, game_title

logger = getLogger('CloudSyncDialog')
Expand All @@ -25,16 +25,21 @@ class CloudSyncDialogResult(IntEnum):
class CloudSyncDialog(ButtonDialog):
result_ready: Signal = Signal(CloudSyncDialogResult)

def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime, parent=None):
def __init__(self, igame: InstalledGame, status: SaveGameStatus, dt_local: datetime | None, dt_remote: datetime | None, parent=None):
super(CloudSyncDialog, self).__init__(parent=parent)
header = self.tr('Cloud saves for')
self.setWindowTitle(game_title(header, igame.title))

title_label = QLabel(f'<h4>{game_title(header, igame.title)}</h4>', self)

sync_widget = QWidget(self)
self.sync_ui = Ui_CloudSyncWidget()
self.sync_ui.setupUi(sync_widget)
sync_widget = CloudSyncWidget(self)
sync_widget.update_widget(status, dt_local, dt_remote)
sync_widget.upload_clicked.connect(self._on_upload)
sync_widget.download_clicked.connect(self._on_download)

min_width = max(sync_widget.local.minimumSizeHint().width(), sync_widget.remote.minimumSizeHint().width())
sync_widget.local.setMinimumWidth(min_width)
sync_widget.remote.setMinimumWidth(min_width)

layout = QVBoxLayout()
layout.addWidget(title_label)
Expand All @@ -47,10 +52,8 @@ def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime

self.status = CloudSyncDialogResult.CANCEL

newer = self.tr('Newer')
if dt_remote and dt_local:
self.sync_ui.age_label_local.setText(f'<b>{newer}</b>' if dt_remote < dt_local else ' ')
self.sync_ui.age_label_remote.setText(f'<b>{newer}</b>' if dt_remote > dt_local else ' ')
pass
# Set status, if one of them is None
elif dt_remote and not dt_local:
self.status = CloudSyncDialogResult.DOWNLOAD
Expand All @@ -59,24 +62,14 @@ def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime
else:
self.status = CloudSyncDialogResult.SKIP

local_tz = datetime.now().astimezone().tzinfo
self.sync_ui.date_info_local.setText(dt_local.astimezone(local_tz).strftime('%A, %d %B %Y %X') if dt_local else 'None')
self.sync_ui.date_info_remote.setText(dt_remote.astimezone(local_tz).strftime('%A, %d %B %Y %X') if dt_remote else 'None')

self.sync_ui.icon_local.setPixmap(qta_icon('mdi.harddisk', 'fa5s.desktop').pixmap(128, 128))
self.sync_ui.icon_remote.setPixmap(qta_icon('mdi.cloud-outline', 'fa5s.cloud').pixmap(128, 128))

self.sync_ui.upload_button.clicked.connect(self.__on_upload)
self.sync_ui.download_button.clicked.connect(self.__on_download)

if self.status == CloudSyncDialogResult.SKIP:
self.accept()

def __on_upload(self):
def _on_upload(self):
self.status = CloudSyncDialogResult.UPLOAD
self.done(QDialog.DialogCode.Accepted)

def __on_download(self):
def _on_download(self):
self.status = CloudSyncDialogResult.DOWNLOAD
self.done(QDialog.DialogCode.Accepted)

Expand All @@ -98,7 +91,12 @@ def reject_handler(self):
def __callback(status: int):
print(repr(CloudSyncDialogResult(status)))

dlg = CloudSyncDialog(core.get_installed_list()[0], datetime.now(), datetime.strptime('2021,1', '%Y,%M'))
dlg = CloudSyncDialog(
core.get_installed_list()[0],
SaveGameStatus.LOCAL_NEWER,
datetime.now(tz=timezone.utc),
datetime.strptime('2021,1', '%Y,%M').replace(tzinfo=timezone.utc)
)
dlg.result_ready.connect(__callback)
dlg.open()
app.exec()
10 changes: 5 additions & 5 deletions rare/commands/subreaper/subreaper_bsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,17 @@ def get_pstree_from_pid(root_pid: int) -> set[int]:
for line in lines:
ppid, pid = map(int, line.split())
pid_to_ppid[pid] = ppid
except Exception:
except Exception: # noqa: BLE001
return descendants

current_pid: list[int] = [root_pid]
while current_pid:
current = current_pid.pop()
# Ignore. mypy flags [arg-type] due to the reuse of pid variable
for pid, ppid in pid_to_ppid.items(): # type: ignore
for pid, ppid in pid_to_ppid.items():
if ppid == current and pid not in descendants:
descendants.add(pid) # type: ignore
current_pid.append(pid) # type: ignore
descendants.add(pid)
current_pid.append(pid)

return descendants

Expand Down Expand Up @@ -123,7 +123,7 @@ def signal_handler(sig, frame):

if pid == 0:
os.chdir(workdir)
os.execvp(command[0], command) # noqa: S606
os.execvp(command[0], command)
else:
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
Expand Down
8 changes: 4 additions & 4 deletions rare/commands/subreaper/subreaper_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ def get_pstree_from_pid(root_pid: int) -> set[int]:
while current_pid:
current = current_pid.pop()
# Ignore. mypy flags [arg-type] due to the reuse of pid variable
for pid, ppid in pid_to_ppid.items(): # type: ignore
for pid, ppid in pid_to_ppid.items():
if ppid == current and pid not in descendants:
descendants.add(pid) # type: ignore
current_pid.append(pid) # type: ignore
descendants.add(pid)
current_pid.append(pid)

return descendants

Expand Down Expand Up @@ -116,7 +116,7 @@ def signal_handler(sig, frame):
sys.stdout.flush()
sys.stderr.flush()
os.chdir(workdir)
os.execvp(command[0], command) # noqa: S606
os.execvp(command[0], command)
else:
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
Expand Down
4 changes: 2 additions & 2 deletions rare/commands/subreaper/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ def find_mangohud_shim() -> str:
return '/'.join(ret)


def find_mangohud_bin() -> str:
def find_mangohud_bin() -> str| None:
return shutil.which('mangohud')


if __name__ == '__main__':
print(find_mangohud_shim())


__all__ = ['find_mangohud_shim', 'find_mangohud_bin']
__all__ = ['find_mangohud_bin', 'find_mangohud_shim']
Loading
Loading