5f5e471169
This property reports the default platform to use for a game based on legendary's configuration and if they platform is available in the game's assets. Using that property we can make better choices on what platform to operate on without user intervention. Currently we use it to infer the platform in when installing, importing, and calculating game versions.
320 lines
10 KiB
Python
320 lines
10 KiB
Python
from abc import abstractmethod
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from enum import IntEnum
|
|
from logging import getLogger
|
|
from typing import Optional, List, Tuple
|
|
|
|
from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, QThreadPool, QSettings
|
|
from legendary.lfs import eos
|
|
from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame
|
|
|
|
from rare.lgndr.core import LegendaryCore
|
|
from rare.models.install import UninstallOptionsModel, InstallOptionsModel
|
|
|
|
logger = getLogger("RareGameBase")
|
|
|
|
|
|
@dataclass
|
|
class RareSaveGame:
|
|
file: SaveGameFile
|
|
status: SaveGameStatus = SaveGameStatus.NO_SAVE
|
|
dt_local: Optional[datetime] = None
|
|
dt_remote: Optional[datetime] = None
|
|
description: Optional[str] = ""
|
|
|
|
|
|
class RareGameBase(QObject):
|
|
|
|
class State(IntEnum):
|
|
IDLE = 0
|
|
RUNNING = 1
|
|
DOWNLOADING = 2
|
|
VERIFYING = 3
|
|
MOVING = 4
|
|
UNINSTALLING = 5
|
|
SYNCING = 6
|
|
|
|
class Signals:
|
|
class Progress(QObject):
|
|
start = pyqtSignal()
|
|
update = pyqtSignal(int)
|
|
finish = pyqtSignal(bool)
|
|
|
|
class Widget(QObject):
|
|
update = pyqtSignal()
|
|
|
|
class Download(QObject):
|
|
enqueue = pyqtSignal(str)
|
|
dequeue = pyqtSignal(str)
|
|
|
|
class Game(QObject):
|
|
install = pyqtSignal(InstallOptionsModel)
|
|
installed = pyqtSignal(str)
|
|
uninstall = pyqtSignal(UninstallOptionsModel)
|
|
uninstalled = pyqtSignal(str)
|
|
launched = pyqtSignal(str)
|
|
finished = pyqtSignal(str)
|
|
|
|
def __init__(self):
|
|
super(RareGameBase.Signals, self).__init__()
|
|
self.progress = RareGameBase.Signals.Progress()
|
|
self.widget = RareGameBase.Signals.Widget()
|
|
self.download = RareGameBase.Signals.Download()
|
|
self.game = RareGameBase.Signals.Game()
|
|
|
|
def __del__(self):
|
|
self.progress.deleteLater()
|
|
self.widget.deleteLater()
|
|
self.download.deleteLater()
|
|
self.game.deleteLater()
|
|
|
|
__slots__ = "igame"
|
|
|
|
def __init__(self, legendary_core: LegendaryCore, game: Game):
|
|
super(RareGameBase, self).__init__()
|
|
self.signals = RareGameBase.Signals()
|
|
self.core = legendary_core
|
|
self.game: Game = game
|
|
self._state = RareGameBase.State.IDLE
|
|
|
|
def __del__(self):
|
|
del self.signals
|
|
|
|
@property
|
|
def state(self) -> 'RareGameBase.State':
|
|
return self._state
|
|
|
|
@state.setter
|
|
def state(self, state: 'RareGameBase.State'):
|
|
if state != self._state:
|
|
self._state = state
|
|
self.signals.widget.update.emit()
|
|
|
|
@property
|
|
def is_idle(self):
|
|
return self.state == RareGameBase.State.IDLE
|
|
|
|
@property
|
|
def app_name(self) -> str:
|
|
return self.game.app_name
|
|
|
|
@property
|
|
def app_title(self) -> str:
|
|
return self.game.app_title
|
|
|
|
@property
|
|
@abstractmethod
|
|
def is_installed(self) -> bool:
|
|
pass
|
|
|
|
@abstractmethod
|
|
def set_installed(self, installed: bool) -> None:
|
|
pass
|
|
|
|
@property
|
|
def platforms(self) -> Tuple:
|
|
"""!
|
|
@brief Property that holds the platforms a game is available for
|
|
|
|
@return Tuple
|
|
"""
|
|
return tuple(self.game.asset_infos.keys())
|
|
|
|
@property
|
|
def default_platform(self) -> str:
|
|
return self.core.default_platform if self.core.default_platform in self.platforms else "Windows"
|
|
|
|
@property
|
|
def is_mac(self) -> bool:
|
|
"""!
|
|
@brief Property to report if Game has a mac version
|
|
|
|
@return bool
|
|
"""
|
|
return "Mac" in self.game.asset_infos.keys()
|
|
|
|
@property
|
|
def is_win32(self) -> bool:
|
|
"""!
|
|
@brief Property to report if Game is 32bit game
|
|
|
|
@return bool
|
|
"""
|
|
return "Win32" in self.game.asset_infos.keys()
|
|
|
|
@property
|
|
def is_origin(self) -> bool:
|
|
"""!
|
|
@brief Property to report if a Game is an Origin game
|
|
|
|
Legendary and by extenstion Rare can't launch Origin games directly,
|
|
it just launches the Origin client and thus requires a bit of a special
|
|
handling to let the user know.
|
|
|
|
@return bool If the game is an Origin game
|
|
"""
|
|
return (
|
|
self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin"
|
|
)
|
|
|
|
@property
|
|
def is_overlay(self):
|
|
return self.app_name == eos.EOSOverlayApp.app_name
|
|
|
|
@property
|
|
def version(self) -> str:
|
|
"""!
|
|
@brief Reports the currently installed version of the Game
|
|
|
|
If InstalledGame reports the currently installed version, which might be
|
|
different from the remote version available from EGS. For not installed Games
|
|
it reports the already known version.
|
|
|
|
@return str The current version of the game
|
|
"""
|
|
return self.igame.version if self.igame is not None else self.game.app_version(self.default_platform)
|
|
|
|
@property
|
|
def install_path(self) -> Optional[str]:
|
|
if self.igame:
|
|
return self.igame.install_path
|
|
return None
|
|
|
|
|
|
class RareGameSlim(RareGameBase):
|
|
|
|
def __init__(self, legendary_core: LegendaryCore, game: Game):
|
|
super(RareGameSlim, self).__init__(legendary_core, game)
|
|
# None if origin or not installed
|
|
self.igame: Optional[InstalledGame] = self.core.get_installed_game(game.app_name)
|
|
self.saves: List[RareSaveGame] = []
|
|
|
|
@property
|
|
def is_installed(self) -> bool:
|
|
return True
|
|
|
|
def set_installed(self, installed: bool) -> None:
|
|
pass
|
|
|
|
@property
|
|
def auto_sync_saves(self):
|
|
return self.supports_cloud_saves and QSettings().value(
|
|
f"{self.app_name}/auto_sync_cloud",
|
|
QSettings().value("auto_sync_cloud", False, bool),
|
|
bool
|
|
)
|
|
|
|
@property
|
|
def save_path(self) -> Optional[str]:
|
|
if self.igame is not None:
|
|
return self.igame.save_path
|
|
return None
|
|
|
|
@property
|
|
def latest_save(self) -> Optional[RareSaveGame]:
|
|
if self.saves:
|
|
saves = sorted(self.saves, key=lambda s: s.file.datetime, reverse=True)
|
|
return saves[0]
|
|
return None
|
|
|
|
@property
|
|
def save_game_state(self) -> Tuple[SaveGameStatus, Tuple[Optional[datetime], Optional[datetime]]]:
|
|
if self.saves and self.save_path:
|
|
latest = self.latest_save
|
|
# lk: if the save path wasn't known at startup, dt_local will be None
|
|
# In that case resolve the save again before returning
|
|
latest.status, (latest.dt_local, latest.dt_remote) = self.core.check_savegame_state(
|
|
self.save_path, latest.file
|
|
)
|
|
return latest.status, (latest.dt_local, latest.dt_remote)
|
|
return SaveGameStatus.NO_SAVE, (None, None)
|
|
|
|
def upload_saves(self, thread=True):
|
|
status, (dt_local, dt_remote) = self.save_game_state
|
|
|
|
def _upload():
|
|
logger.info(f"Uploading save for {self.app_title}")
|
|
self.state = RareGameSlim.State.SYNCING
|
|
self.core.upload_save(self.app_name, self.igame.save_path, dt_local)
|
|
self.state = RareGameSlim.State.IDLE
|
|
self.update_saves()
|
|
|
|
if not self.supports_cloud_saves:
|
|
return
|
|
if status == SaveGameStatus.NO_SAVE or not dt_local:
|
|
logger.warning("Can't upload non existing save")
|
|
return
|
|
if self.state == RareGameSlim.State.SYNCING:
|
|
logger.error(f"{self.app_title} is already syncing")
|
|
return
|
|
|
|
if thread:
|
|
worker = QRunnable.create(lambda: _upload())
|
|
QThreadPool.globalInstance().start(worker)
|
|
else:
|
|
_upload()
|
|
|
|
def download_saves(self, thread=True):
|
|
status, (dt_local, dt_remote) = self.save_game_state
|
|
|
|
def _download():
|
|
logger.info(f"Downloading save for {self.app_title}")
|
|
self.state = RareGameSlim.State.SYNCING
|
|
self.core.download_saves(self.app_name, self.latest_save.file.manifest_name, self.save_path)
|
|
self.state = RareGameSlim.State.IDLE
|
|
self.update_saves()
|
|
|
|
if not self.supports_cloud_saves:
|
|
return
|
|
if status == SaveGameStatus.NO_SAVE or not dt_remote:
|
|
logger.error("Can't download non existing save")
|
|
return
|
|
if self.state == RareGameSlim.State.SYNCING:
|
|
logger.error(f"{self.app_title} is already syncing")
|
|
return
|
|
|
|
if thread:
|
|
worker = QRunnable.create(lambda: _download())
|
|
QThreadPool.globalInstance().start(worker)
|
|
else:
|
|
_download()
|
|
|
|
def load_saves(self, saves: List[SaveGameFile]):
|
|
""" Use only in a thread """
|
|
self.saves.clear()
|
|
for save in saves:
|
|
if self.save_path:
|
|
status, (dt_local, dt_remote) = self.core.check_savegame_state(self.save_path, save)
|
|
rsave = RareSaveGame(save, status, dt_local, dt_remote)
|
|
else:
|
|
rsave = RareSaveGame(save, SaveGameStatus.SAME_AGE, dt_local=None, dt_remote=save.datetime)
|
|
self.saves.append(rsave)
|
|
self.signals.widget.update.emit()
|
|
|
|
def update_saves(self):
|
|
""" Use only in a thread """
|
|
saves = self.core.get_save_games(self.app_name)
|
|
self.load_saves(saves)
|
|
|
|
@property
|
|
def is_save_up_to_date(self):
|
|
status, (_, _) = self.save_game_state
|
|
return (status == SaveGameStatus.SAME_AGE) \
|
|
or (status == SaveGameStatus.NO_SAVE)
|
|
|
|
@property
|
|
def raw_save_path(self) -> str:
|
|
if self.game.supports_cloud_saves:
|
|
return self.game.metadata.get("customAttributes", {}).get("CloudSaveFolder", {}).get("value")
|
|
return ""
|
|
|
|
@property
|
|
def raw_save_path_mac(self) -> str:
|
|
if self.game.supports_mac_cloud_saves:
|
|
return self.game.metadata.get("customAttributes", {}).get('CloudSaveFolder_MAC', {}).get('value')
|
|
return ""
|
|
|
|
@property
|
|
def supports_cloud_saves(self):
|
|
return self.game.supports_cloud_saves or self.game.supports_mac_cloud_saves
|