gsgbackup/sgbackup/game.py

1398 lines
47 KiB
Python

###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 Christian Moser #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from gi.repository.GObject import Property,GObject,Signal,SignalFlags
from gi.repository import GLib
import os
import json
import re
import fnmatch
from enum import StrEnum
import sys
import logging
import pathlib
import datetime
logger = logging.getLogger(__name__)
from .settings import settings
if sys.platform.lower() == "win32":
PLATFORM_WIN32 = True
import winreg
else:
PLATFORM_WIN32 = False
if sys.platform.lower() in ['linux','freebsd','netbsd','openbsd','dragonfly']:
PLATFORM_LINUX = True
else:
PLATFORM_LINUX = False
class SavegameType(StrEnum):
"""
SavegameType The savegame type for `Game` instance.
The SavegameType selects the `GameData` provider for the
`Game` instance.
"""
#: UNSET The SavegameType is unset
UNSET = "unset"
#: OTHER Not listed game-provider.
#:
#: **Currently not supported!**
OTHER = "other"
#: WINDOWS Native Windows game
WINDOWS = "windows"
#: LINUX Native Linux game
LINUX = "linux"
#: MACOS Native MacOS game
MACOS = "macos"
#: STEAM_WINDOWS *Steam* for Windows
STEAM_WINDOWS = "steam_windows"
#: STEAM_LINUX *Steam* for Linux
STEAM_LINUX = "steam_linux"
#: STEAM_MACOS *Steam* for MacOS
STEAM_MACOS = "steam_macos"
#: GOG WINDOWS *Good old Games* for Windows
#:
#: **Currently not supported!**
GOG_WINDOWS = "gog_windows"
#: GOG_LINUX *Good old Games* for Linux
#:
#: **Currently not supported!**
GOG_LINUX = "gog_linux"
#: EPIC_WINDOWS *Epic Games* for Windows
#:
#: **Currently not supported!**
EPIC_WINDOWS = "epic_windows"
#: EPIC_LINUX *Epic Games* for Linux
#:
#: **Currently not supported!**
EPIC_LINUX = "epic_linux"
@staticmethod
def from_string(typestring:str):
"""
from_string Get SavegameType from string.
:param typestring: The string to parse
:type typestring: str
:return: The SavegameType if any. If no matching SavegameType is found,
SavegameType.UNSET is returned.
:rtype: SavegameType
"""
st=SavegameType
s=typestring.lower()
if (s == 'other'):
return st.OTHER
elif (s == 'windows'):
return st.WINDOWS
elif (s == 'linux'):
return st.LINUX
elif (s == 'macos'):
return st.MACOS
elif (s == 'steam_windows' or s == 'steam-windows' or s == 'steamwindows' or s == 'steam.windows'):
return st.STEAM_WINDOWS
elif (s == 'steam_linux' or s == 'steam-linux' or s == 'steamlinux' or s == 'steam.linux'):
return st.STEAM_LINUX
elif (s == 'steam_macos' or s == 'steam-macos' or s == 'steammacos' or s == 'steam.macos'):
return st.STEAM_MACOS
elif (s == 'gog_winows' or s == 'gog-windows' or s == 'gogwindows' or s == 'gog.windows'):
return st.GOG_WINDOWS
elif (s == 'gog_linux' or s == 'gog-linux' or s == 'goglinux' or s == 'gog.linux'):
return st.GOG_LINUX
elif (s == 'epic_windows' or s == 'epic-windows' or s == 'epicwindows' or s == 'epic.windows'):
return st.EPIC_WINDOWS
elif (s == 'epic_linux' or s == 'epic-linux' or s == 'epiclinux' or s == 'epic.linux'):
return st.EPIC_LINUX
return st.UNSET
class GameFileType(StrEnum):
"""
GameFileType The file matcher type for `GameFileMatcher`.
The path to be matched is originating from *${SAVEGAME_ROOT}/${SAVEGAME_DIR}*.
"""
#: GLOB Glob matching.
GLOB = "glob"
#: REGEX Regex file matching
REGEX = "regex"
#: FILENAME Filename matching.
FILENAME = "filename"
@staticmethod
def from_string(typestring:str):
"""
from_string Get the `GameFileType` from a string.
If an illegal string-value is given this method raises a `ValueError`.
:param typestring: The string to be used.
:type typestring: str
:raises ValueError: If an illegal string is given.
:return: The corresponding Enum-value
:rtype: GameFileType
"""
s = typestring.lower()
if (s == 'glob'):
return GameFileType.GLOB
elif (s == 'regex'):
return GameFileType.REGEX
elif (s == 'filename'):
return GameFileType.FILENAME
raise ValueError("Unknown GameFileType \"{}\"!".format(typestring))
class GameFileMatcher(GObject):
"""
GameFileMatcher Match savegame files if they are to be included in the backup.
"""
__gtype_name__ = "GameFileMatcher"
def __init__(self,match_type:GameFileType,match_file:str):
GObject.__init__(self)
self.match_type = type
self.match_file = match_file
@Property
def match_type(self)->GameFileType:
"""
match_type The type of the matcher.
:type: GameFileType
"""
return self.__match_type
@match_type.setter
def match_type(self,type:GameFileType):
if not isinstance(type,GameFileType):
raise TypeError("match_type is not a GameFileType instance!")
self.__match_type = type
@Property(type=str)
def match_file(self)->str:
"""
match_file The matcher value.
:type: str
"""
return self.__match_file
@match_file.setter
def match_file(self,file:str):
self.__match_file = file
def match(self,rel_filename:str)->bool:
"""
match Match the file.
:param rel_filename: The relative filename originating from
*${SAVEGAME_ROOT}/${SAVEGAME_DIR}*.
:type rel_filename: str
:rtype: bool
:returns: True if file matches
"""
def match_glob(filename)->bool:
return fnmatch.fnmatch(filename,self.match_file)
# match_glob()
def match_filename(filename):
if (PLATFORM_WIN32):
fn = filename.replace("/","\\")
if (self.match_file.endswith("\\")):
if fn == self.match_file[:-1] or fn.startswith(self.match_file):
return True
elif fn == self.match_file:
return True
else:
if (self.match_file.endswith('/')):
if fn == self.match_file[:-1] or fn.startswith(self.match_file):
return True
elif fn == self.match_file:
return True
return False
# match_filename()
def match_regex(filename):
return (re.search(self.match_file,filename) is not None)
# match_filename()
if (self.match_type == GameFileType.FILENAME):
return match_filename(rel_filename)
elif (self.match_type == GameFileType.GLOB):
return match_glob(rel_filename)
elif (self.match_type == GameFileType.REGEX):
return match_regex(rel_filename)
return False
class GameData(GObject):
__gtype_name__ = 'GameData'
"""
:class: GameData Base class for savegame specific data data.
"""
def __init__(self,
savegame_type:SavegameType,
savegame_root:str,
savegame_dir:str,
variables:dict|None=None,
file_match:list|None=None,
ignore_match:list|None=None):
GObject.__init__(self)
self.__savegame_type = savegame_type
self.__savegame_root = savegame_root
self.__savegame_dir = savegame_dir
self.__variables = {}
self.__filematchers = []
self.__ignorematchers = []
if variables is not None:
variables.update(variables)
if file_match is not None:
for fm in file_match:
self.add_file_match(fm)
if ignore_match is not None:
for fm in ignore_match:
self.add_ignore_match(fm)
@Property
def savegame_type(self)->SavegameType:
"""
:type: SavegameType
"""
return self.__savegame_type
@Property(type=str)
def savegame_root(self)->str:
"""
:type: str
"""
return self.__savegame_root
@savegame_root.setter
def savegame_root(self,sgroot:str):
self.__savegame_root = sgroot
@Property
def savegame_dir(self)->str:
"""
:type: str
"""
return self.__savegame_dir
@savegame_dir.setter
def savegame_dir(self,sgdir:str):
self.__savegame_dir = sgdir
@Property
def variables(self)->dict[str:str]:
"""
:type: dict[str:str]
"""
return self.__variables
@variables.setter
def variables(self,vars:dict|None):
if not vars:
self.__variables = {}
else:
self.__variables = dict(vars)
@Property
def file_matchers(self)->list[GameFileMatcher]:
"""
:type: list[GameFileMatcher]
"""
return self.__filematchers
@file_matchers.setter
def file_matchers(self,fm:list[GameFileMatcher]|None):
if not fm:
self.__filematchers = []
else:
for matcher in fm:
if not isinstance(matcher,GameFileMatcher):
raise TypeError("\"file_match\" needs to be \"None\" or a list of \"GameFileMatcher\" instances!")
self.__filematchers = list(fm)
@Property
def ignore_matchers(self)->list[GameFileMatcher]:
"""
:type: list[GameFileMatcher]
"""
return self.__ignorematchers
@ignore_matchers.setter
def ignore_matchers(self,im:list[GameFileMatcher]|None):
if not im:
self.__ignorematchers = []
else:
for matcher in im:
if not isinstance(matcher,GameFileMatcher):
raise TypeError("\"ignore_match\" needs to be \"None\" or a list of \"GameFileMatcher\" instances!")
self.__ignorematchers = list(im)
def has_variable(self,name:str)->bool:
"""
has_variable Check if variable exists.
:param name: The variable name.
:type name: str
:return: `True` if the variable exists.
:rtype: bool
"""
return (name in self.__variables)
def get_variable(self,name:str)->str:
"""
get_variable Get a variable value byy variable name.
:param name: The variable name
:type name: str
:return: The vairable value if the variable exists or an empty string.
:rtype: str
"""
if name not in self.__variables:
return ""
return self.__variables[name]
def set_variable(self,name:str,value:str):
"""
set_variable Set a variable.
If the variable exists, it is replaced by the new variable.
:param name: The variable name.
:type name: str
:param value: The variable value.
:type value: str
"""
self.__variables[name] = value
def delete_variable(self,name:str):
"""
delete_variable Deletes as variable if the variable exists
:param name: The vairable name to delete.
:type name: str
"""
if name in self.__variables:
del self.__variables[name]
def get_variables(self)->dict[str:str]:
"""
get_variables Get the variables set by this instance.
:return: The variables as a dict.
:rtype: dict[str:str]
"""
return self.variables
def match_file(self,rel_filename:str)->bool:
"""
match_file Matches a file with the `GameFileMatcher`s for this class.
This method returns `True` if there is no `GameFileMatcher` set for
`GameData.file_match`.
:param rel_filename: The relative filename originating from *$SAVEGAME_DIR*
:type rel_filename: str
:return: `True` if the file matches.
:rtype: bool
"""
if not self.file_matchers:
return True
for fm in self.file_matchers:
if fm.match(rel_filename):
return True
return False
def match_ignore(self,rel_filename:str)->bool:
"""
match_ignore Matches file agains the ignore_match `GameFileMatcher`s.
This method returns `False` if there is no `GameFileMatcher` set in
`GameData.ignore_match`.
:param rel_filename: The relative filename originating from *$SAVEGAME_DIR*
:type rel_filename: str
:return: `True` if the file matches.
:rtype: bool
"""
if not self.ignore_matchers:
return False
for fm in self.ignore_matchers:
if fm.match(rel_filename):
return True
return False
def match(self,rel_filename:str)->bool:
"""
match Match files against `file_match` and `ignore_match`.
If this method returns `True` the file should be included in the
savegame backup.
:param rel_filename: The relative filename originating from *$SAVEGAME_DIR*
:type rel_filename: str
:return: True if the file should be included in the savegame backup.
:rtype: bool
"""
if self.match_file(rel_filename) and not self.match_ignore(rel_filename):
return True
return False
def add_file_match(self,matcher:GameFileMatcher):
"""
add_file_match Add a `GameFileMatcher` to `file_match`.
:param matcher: The `GameFileMatcher` to add.
:type matcher: GameFileMatcher
:raises TypeError: If the `matcher` is not a `GameFileMatcher` instance.
"""
if not isinstance(matcher,GameFileMatcher):
raise TypeError("matcher is not a \"GameFileMatcher\" instance!")
self.__filematchers.append(matcher)
def remove_file_match(self,matcher:GameFileMatcher):
"""
remove_file_match Remove a file_matcher.
:param matcher: The `GameFileMatcher` to remove.
:type matcher: GameFileMatcher
"""
for i in reversed(range(len(self.__filematchers))):
if (matcher == self.__filematchers[i]):
del self.__filematchers[i]
def add_ignore_match(self,matcher:GameFileMatcher):
"""
add_file_match Add a `GameFileMatcher` to `ignore_match`.
:param matcher: The `GameFileMatcher` to add.
:type matcher: GameFileMatcher
:raises TypeError: If the `matcher` is not a `GameFileMatcher` instance.
"""
if not isinstance(matcher,GameFileMatcher):
raise TypeError("matcher is not a \"GameFileMatcher\" instance!")
self.__ignorematchers.append(matcher)
def remove_ignore_match(self,matcher:GameFileMatcher):
"""
remove_file_match Remove a ignore_match.
:param matcher: The `GameFileMatcher` to remove.
:type matcher: GameFileMatcher
"""
for i in reversed(range(len(self.__ignorematchers))):
if (matcher == self.__ignorematchers[i]):
del self.__ignorematchers[i]
def serialize(self)->dict:
"""
serialize Serialize the instance to a dict.
This method should be overloaded by child-classes, so that their data
is exported too.
:return: The dict holding the data for recreating an instance of this class.
:rtype: dict
"""
ret = {
'savegame_root': self.savegame_root,
'savegame_dir': self.savegame_dir,
}
if (self.__variables):
ret['variables'] = self.variables
if (self.file_matchers):
fm = []
for matcher in self.file_matchers:
fm.append({'type':matcher.match_type.value,'match':matcher.match_file})
ret['file_match'] = fm
if (self.ignore_matchers):
im = []
for matcher in self.ignore_matchers:
im.append({'type':matcher.match_type.value,'match':matcher.match_file})
ret['ignore_match'] = im
return ret
class WindowsGame(GameData):
def __init__(self,
savegame_root:str,
savegame_dir:str,
variables:dict|None=None,
installdir:str|None=None,
game_registry_keys:str|list|None=None,
installdir_registry_keys:str|list|None=None,
file_match:list|None=None,
ignore_match:list|None=None):
GameData.__init__(self,
SavegameType.WINDOWS,
savegame_root,
savegame_dir,
variables,
file_match,
ignore_match)
if not installdir:
self.__installdir = None
else:
self.__installdir = installdir
if not game_registry_keys:
self.__game_registry_keys = []
elif isinstance(game_registry_keys,str):
self.__game_registry_keys = [game_registry_keys]
else:
self.__game_registry_keys = list(game_registry_keys)
if not installdir_registry_keys:
self.__installdir_registry_keys = []
elif isinstance(installdir_registry_keys,str):
self.__installdir_registry_keys = [installdir_registry_keys]
else:
self.__installdir_registry_keys = list(installdir_registry_keys)
def __get_hkey(self,regkey:str):
regvec = regkey.split("\\")
hkstr = regvec[0]
if (hkstr == 'HKLM' or hkstr == 'HKEY_LOCAL_MACHINE'):
return winreg.HKEY_LOCAL_MACHINE
elif (hkstr == 'HKCU' or hkstr == 'HKEY_CURRENT_USER'):
return winreg.HKEY_CURRENT_USER
elif (hkstr == 'HKCC' or hkstr == 'HKEY_CURRENT_CONFIG'):
return winreg.HKEY_CURRENT_CONFIG
elif (hkstr == 'HKCR' or hkstr == 'HKEY_CLASSES_ROOT'):
return winreg.HKEY_CLASSES_ROOT
elif (hkstr == 'HKU' or hkstr == 'HKEY_USERS'):
return winreg.HKEY_USERS
return None
@Property
def installdir(self)->str|None:
return self.__installdir
@installdir.setter
def installdir(self,installdir:str|None):
self.__installdir = installdir
@Property
def game_registry_keys(self)->list:
return self.__game_registry_keys
@Property
def installdir_registry_keys(self)->list:
return self.__installdir_registry_keys
@Property
def is_installed(self)->bool|None:
if not PLATFORM_WIN32 or not self.game_registry_keys:
return None
for regkey in self.__game_registry_keys:
hkey = self.__get_hkey(regkey)
regvec = regkey.split('\\')
if (regvec > 1):
key = '\\'.join(regvec[1:])
try:
rkey = winreg.OpenKeyEx(hkey,key)
winreg.CloseKey(rkey)
return True
except OSError as ex:
continue
return False
@Property
def registry_installdir(self)->str|None:
if not PLATFORM_WIN32 or not (self.installdir_registry_keys):
return None
for regkey in self.__game_registry_keys:
hkey = self.__get_hkey(regkey)
regvec = regkey.split('\\')
if (regvec > 2):
key = '\\'.join(regvec[1:-1])
try:
rkey = None
rkey = winreg.OpenKeyEx(hkey,key)
retval = winreg.QueryValue(rkey,regvec[-1])
winreg.CloseKey(rkey)
if retval:
return str(retval[0])
except OSError as ex:
if (rkey):
winreg.CloseKey(rkey)
continue
return None
def get_variables(self):
variables = super().get_variables()
variables["INSTALLDIR"] = self.installdir if self.installdir else ""
def serialize(self):
ret = super().serialize()
if (self.installdir):
ret['installdir'] = self.installdir
if (self.game_registry_keys):
ret['game_registry_keys'] = self.game_registry_keys
if (self.installdir_registry_keys):
ret['installdir_registry_keys'] = self.installdir_registry_keys
return ret
class LinuxGame(GameData):
def __init__(self,
savegame_root:str,
savegame_dir:str,
variables:dict|None=None,
binary:str|None=None,
file_match:list|None=None,
ignore_match:list|None=None):
GameData.__init__(self,
SavegameType.LINUX,
savegame_root,
savegame_dir,
variables,
file_match,
ignore_match)
self.__binary = binary
@Property
def binary(self)->str|None:
return self.__binary
@binary.setter
def binary(self,bin:str):
self.__binary = bin
@Property
def is_installed(self)->bool|None:
if PLATFORM_LINUX and self.binary:
return bool(GLib.find_program_in_path(self.binary))
else:
return None
def serialize(self):
ret = super().serialize()
if self.binary:
ret['binary'] = self.binary
return ret
class MacOSGame(GameData):
def __init__(self,
savegame_root:str,
savegame_dir:str,
variables:dict|None=None,
binary:str|None=None,
file_match:list|None=None,
ignore_match:list|None=None):
GameData.__init__(self,
SavegameType.MACOS,
savegame_root,
savegame_dir,
variables,
file_match,
ignore_match)
self.__binary = binary
@Property
def binary(self)->str|None:
return self.__binary
@binary.setter
def binary(self,bin:str):
self.__binary = bin
@Property
def is_installed(self)->bool|None:
if PLATFORM_LINUX and self.binary:
return bool(GLib.find_program_in_path(self.binary))
else:
return None
def serialize(self):
ret = super().serialize()
if self.binary:
ret['binary'] = self.binary
return ret
class SteamGame(GameData):
def __init__(self,
sgtype:SavegameType,
appid:int,
savegame_root:str,
savegame_dir:str,
variables:dict|None=None,
installdir:str|None=None,
file_match:list|None=None,
ignore_match:list|None=None):
if sgtype not in (SavegameType.STEAM_WINDOWS,
SavegameType.STEAM_LINUX,
SavegameType.STEAM_MACOS):
raise TypeError("SaveGameType")
GameData.__init__(self,
sgtype,
savegame_root,
savegame_dir,
variables,
file_match,
ignore_match)
self.appid = int(appid)
self.installdir = installdir
def get_variables(self):
vars = super().get_variables()
vars["INSTALLDIR"] = self.installdir if self.installdir else ""
@Property(type=int)
def appid(self):
return self.__appid
@appid.setter
def appid(self,appid):
self.__appid = appid
@Property
def installdir(self):
return self.__installdir
@installdir.setter
def installdir(self,installdir:str|None):
self.__installdir = installdir
def serialize(self):
ret = super().serialize()
ret['appid'] = self.appid
if self.installdir:
ret['installdir'] = self.installdir
return ret
class SteamWindowsGame(SteamGame):
def __init__(self,
appid:int,
savegame_root:str,
savegame_dir:str,
variables:dict|None=None,
installdir:str|None=None,
file_match:list|None=None,
ignore_match:list|None=None):
SteamGame.__init__(self,
SavegameType.STEAM_WINDOWS,
appid,
savegame_root,
savegame_dir,
variables,
installdir,
file_match,
ignore_match)
class SteamLinuxGame(SteamGame):
def __init__(self,
appid:int,
savegame_root:str,
savegame_dir:str,
variables:dict|None=None,
installdir:str|None=None,
file_match:list|None=None,
ignore_match:list|None=None):
SteamGame.__init__(self,
SavegameType.STEAM_LINUX,
appid,
savegame_root,
savegame_dir,
variables,
installdir,
file_match,
ignore_match)
class SteamMacOSGame(SteamGame):
def __init__(self,
appid:int,
savegame_root:str,
savegame_dir:str,
variables:dict|None=None,
installdir:str|None=None,
file_match:list|None=None,
ignore_match:list|None=None):
SteamGame.__init__(self,
SavegameType.STEAM_MACOS,
appid,
savegame_root,
savegame_dir,
variables,
installdir,
file_match,
ignore_match)
class Game(GObject):
__gtype_name__ = "Game"
@staticmethod
def new_from_dict(config:str):
logger = logger.getChild("Game.new_from_dict()")
def get_file_match(conf:dict):
conf_fm = conf['file_match'] if 'file_match' in conf else None
conf_im = conf['ignore_match'] if 'ignore_match' in conf else None
if (conf_fm):
file_match = []
for cfm in conf_fm:
if ('type' in cfm and 'match' in cfm):
try:
file_match.append(GameFileMatcher(GameFileType.from_string(cfm['type'],cfm['match'])))
except Exception as ex:
logger.error("Adding GameFileMatcher to file_match failed! ({})!".format(ex))
else:
logger.error("Illegal file_match settings! (\"type\" or \"match\" missing!)")
else:
file_match = None
if (conf_im):
ignore_match = []
for cim in conf_im:
if ('type' in cim and 'match' in cim):
try:
file_match.append(GameFileMatcher(GameFileType.from_string(cim['type'],cim['match'])))
except Exception as ex:
logger.error("Adding GameFileMatcher to ignore_match failed! ({})!".format(ex))
else:
logger.error("Illegal ignore_match settings! (\"type\" or \"match\" missing!)")
else:
ignore_match = None
return (file_match,ignore_match)
def new_steam_game(conf,cls:SteamGame):
appid = conf['appid'] if 'appid' in conf else None
sgroot = conf['savegame_root'] if 'savegame_root' in conf else None
sgdir = conf['savegame_dir'] if 'savegame_dir' in conf else None
vars = conf['variables'] if 'variables' in conf else None
installdir = conf['installdir'] if 'installdir' in conf else None
file_match,ignore_match = get_file_match(conf)
if appid is not None and sgroot and sgdir:
cls(appid,sgroot,sgdir,vars,installdir,file_match,ignore_match)
return None
# new_steam_game()
if not 'id' in config or not 'name' in config:
return None
id = config['id']
name = config['name']
sgname = config['savegame_name'] if 'savegame_name' in config else id
sgtype = config['savegame_type'] if 'savegame_type' in config else SavegameType.UNSET
game = Game(id,name,sgname)
game.savegame_type = sgtype
game.is_active = config['is_active'] if 'is_active' in config else False
game.is_live = config['is_live'] if 'is_live' in config else True
if 'windows' in config:
winconf = config['windows']
sgroot = winconf['savegame_root'] if 'savegame_root' in winconf else None
sgdir = winconf['savegame_dir'] if 'savegame_dir' in winconf else None
vars = winconf['variables'] if 'variables' in winconf else None
installdir = winconf['installdir'] if 'installdir' in winconf else None
game_regkeys = winconf['game_registry_keys'] if 'game_registry_keys' in winconf else None
installdir_regkeys = winconf['installdir_registry_keys'] if 'installdir_registry_keys' in winconf else None
file_match,ignore_match = get_file_match(winconf)
if (sgroot and sgdir):
game.windows = WindowsGame(sgroot,
sgdir,
vars,
installdir,
game_regkeys,
installdir_regkeys,
file_match,
ignore_match)
if 'linux' in config:
linconf = config['linux']
sgroot = linconf['savegame_root'] if 'savegame_root' in linconf else None
sgdir = linconf['savegame_dir'] if 'savegame_dir' in linconf else None
vars = linconf['variables'] if 'variables' in linconf else None
binary = linconf['binary'] if 'binary' in linconf else None
file_match,ignore_match = get_file_match(linconf)
if (sgroot and sgdir):
game.linux = LinuxGame(sgroot,sgdir,vars,binary,file_match,ignore_match)
if 'macos' in config:
macconf = config['macos']
sgroot = macconf['savegame_root'] if 'savegame_root' in macconf else None
sgdir = macconf['savegame_dir'] if 'savegame_dir' in macconf else None
vars = macconf['variables'] if 'variables' in macconf else None
binary = macconf['binary'] if 'binary' in macconf else None
file_match,ignore_match = get_file_match(macconf)
if (sgroot and sgdir):
game.macos = MacOSGame(sgroot,sgdir,vars,binary,file_match,ignore_match)
if 'steam_windows' in config:
game.steam_windows = new_steam_game(config['steam_windows'],SteamWindowsGame)
if 'steam_linux' in config:
game.steam_linux = new_steam_game(config['steam_linux'],SteamLinuxGame)
if 'steam_macos' in config:
game.steam_macos = new_steam_game(config['steam_macos'],SteamMacOSGame)
return game
@staticmethod
def new_from_json_file(filename:str):
if not os.path.isfile(filename):
raise FileNotFoundError("Filename \"{filename}\" not found!".format(filename=filename))
with open(filename,'rt',encoding="UTF-8") as ifile:
return Game.new_from_dict(json.loads(ifile.read()))
def __init__(self,key:str,name:str,savegame_name:str):
GObject.__init__(self)
self.__dbid = None
self.__key = key
self.__name = name
self.__filename = None
self.__savegame_name = savegame_name
self.__savegame_type = SavegameType.UNSET
self.__active = False
self.__live = True
self.__variables = dict()
self.__windows = None
self.__linux = None
self.__macos = None
self.__steam_windows = None
self.__steam_linux = None
self.__steam_macos = None
self.__gog_windows = None
self.__gog_linux = None
self.__epic_windows = None
self.__epic_linux = None
@Property(type=str)
def dbid(self)->str:
return self.__id
@dbid.setter
def id(self,id:str):
self.__id = id
@Property(type=str)
def key(self)->str:
return self.__key
@key.setter
def key(self,key:str):
set_game = False
if self.__key in GAMES:
del GAMES[self.__key]
set_game = True
self.__key = key
if set_game:
GAMES[self.__key] = self
@Property(type=str)
def name(self)->str:
return self.__name
@name.setter
def name(self,name:str):
self.__name = name
@Property(type=str)
def savegame_name(self)->str:
return self.__savegame_name
@savegame_name.setter
def savegame_name(self,sgname:str):
self.__savegame_name = sgname
@Property
def savegame_type(self)->SavegameType:
return self.__savegame_type
@savegame_type.setter
def savegame_type(self,sgtype:SavegameType):
self.__savegame_type = sgtype
@Property(type=bool,default=False)
def is_active(self)->bool:
return self.__active
@is_active.setter
def is_active(self,active:bool):
self.__active = bool(active)
@Property(type=bool,default=True)
def is_live(self)->bool:
return self.__live
@is_live.setter
def is_live(self,live:bool):
self.__live = bool(live)
@Property
def filename(self)->str|None:
if not self.__id:
return None
if not self.__filename:
GLib.build_filename(settings.gameconf_dir,'.'.join((self.id,'gameconf')))
return self.__filename
@filename.setter
def filename(self,fn:str):
if not os.path.isabs(fn):
self.__filename = GLib.build_filename(settings.gameconf_dir,fn)
else:
self.__filename = fn
@Property
def variables(self):
return self.__variables
@variables.setter
def variables(self,vars:dict|None):
if not vars:
self.__variables = {}
else:
self.__variables = dict(vars)
@Property
def game_data(self):
sgtype = self.savegame_type
if (sgtype == SavegameType.WINDOWS):
return self.windows
elif (sgtype == SavegameType.LINUX):
return self.linux
elif (sgtype == SavegameType.MACOS):
return self.macos
elif (sgtype == SavegameType.STEAM_WINDOWS):
return self.steam_windows
elif (sgtype == SavegameType.STEAM_LINUX):
return self.steam_linux
elif (sgtype == SavegameType.STEAM_MACOS):
return self.steam_macos
elif (sgtype == SavegameType.GOG_WINDOWS):
return self.__gog_windows
elif (sgtype == SavegameType.GOG_LINUX):
return self.__gog_linux
elif (sgtype == SavegameType.EPIC_WINDOWS):
return self.__epic_windows
elif (sgtype == SavegameType.EPIC_LINUX):
return self.__epic_linux
return None
@Property
def windows(self)->WindowsGame|None:
return self.__windows
@windows.setter
def windows(self,data:WindowsGame|None):
if not data:
self.__windows = None
else:
if not isinstance(data,WindowsGame):
raise TypeError("WindowsGame")
self.__windows = data
@Property
def linux(self)->LinuxGame|None:
return self.__linux
@linux.setter
def linux(self,data:LinuxGame):
if not data:
self.__linux = None
else:
if not isinstance(data,LinuxGame):
raise TypeError("LinuxGame")
self.__linux = data
@Property
def macos(self)->MacOSGame|None:
return self.__macos
@macos.setter
def macos(self,data:MacOSGame|None):
if not data:
self.__macos = None
else:
if not isinstance(data,MacOSGame):
raise TypeError("MacOSGame")
self.__macos = data
@Property
def steam_windows(self)->SteamWindowsGame|None:
return self.__steam_windows
@steam_windows.setter
def steam_windows(self,data:SteamWindowsGame|None):
if not data:
self.__steam_windows = None
else:
if not isinstance(data,SteamWindowsGame):
raise TypeError("SteamWindowsGame")
self.__steam_windows = data
@Property
def steam_linux(self)->SteamLinuxGame|None:
return self.__steam_linux
@steam_linux.setter
def steam_windows(self,data:SteamLinuxGame|None):
if not data:
self.__steam_linux = None
else:
if not isinstance(data,SteamLinuxGame):
raise TypeError("SteamWindowsGame")
self.__steam_linux = data
@Property
def steam_macos(self)->SteamMacOSGame|None:
return self.__steam_macos
@steam_macos.setter
def steam_macos(self,data:SteamMacOSGame|None):
if not data:
self.__steam_macos = None
else:
if not isinstance(data,SteamMacOSGame):
raise TypeError("SteamWindowsGame")
self.__steam_macos = data
@Property
def savegame_root(self)->str|None:
if not self.game_data:
return None
return self.game_data.savegame_root
@Property
def savegame_dir(self)->str|None:
if not self.game_data:
return None
return self.game_data.savegame_dir
def add_variable(self,name:str,value:str):
self.__variables[str(name)] = str(value)
def delete_variable(self,name):
if name in self.__variables:
del self.__variables[name]
def get_variable(self,name):
vars = dict(os.environ)
#vars.update(settings.variables)
vars.update(self.__variables)
game_data = self.game_data
if (game_data is not None):
vars.update(game_data.variables)
def serialize(self)->dict:
ret = {
'id': self.id,
'name': self.name,
'savegame_name': self.savegame_name,
'savegame_type': self.savegame_type.value,
'is_active': self.is_active,
'is_live': self.is_live,
}
if (self.windows):
ret['windows'] = self.windows.serialize()
if (self.linux):
ret['linux'] = self.linux.serialize()
if (self.macos):
ret['macos'] = self.macos.serialize()
if (self.steam_windows):
ret['steam_windows'] = self.steam_windows.serialize()
if (self.steam_linux):
ret['steam_linux'] = self.steam_linux.serialize()
if (self.steam_macos):
ret['steam_macos'] = self.steam_macos.serialize()
#if self.gog_windows:
# ret['gog_windows'] = self.gog_windows.serialize()
#if self.gog_linux:
# ret['gog_linux'] = self.gog_linux.serialize()
#if self.epic_windows:
# ret['epic_windows'] = self.epic_windows.serialize()
#if self.epic_linux:
# ret['epic_linux'] = self.epic_linux.serialize()
return ret
def save(self):
old_path = pathlib.Path(self.filename).resolve()
new_path = pathlib.Path(settings.gameconf_dir / '.'.join(self.id,'gameconf')).resolve()
if (str(old_path) != str(new_path)) and old_path.is_file():
os.unlink(old_path)
if not new_path.parent.is_dir():
os.makedirs(new_path.parent)
with open(new_path,'wt',encoding='utf-8') as ofile:
ofile.write(json.dumps(self.serialize(),ensure_ascii=False,indent=4))
def __bool__(self):
return (bool(self.game_data) and bool(self.savegame_root) and bool(self.savegame_dir))
def is_backup_file(self,filename:str):
pass
def get_backup_files(self)->dict[str:str]|None:
def get_backup_files_recursive(sgroot:pathlib.Path,sgdir:str,subdir:str|None=None):
ret = {}
if subdir:
path = sgroot / sgdir / subdir
else:
path = sgroot / sgdir
ret = {}
for dirent in os.listdir(path):
file_path = path / dirent
if file_path.is_file():
if subdir:
fname = (os.path.join(subdir,dirent))
else:
fname = dirent
if self.game_data.match(fname):
ret[str(path)] = os.path.join(sgdir,fname)
elif file_path.is_dir():
ret.update(get_backup_files_recursive(sgroot,sgdir,os.path.join(subdir,dirent)))
return ret
if not bool(self):
return None
sgroot = pathlib.Path(self.savegame_root).resolve()
sgdir = self.savegame_dir
sgpath = sgroot / sgdir
if not os.path.exists(sgpath):
return None
backup_files = get_backup_files_recursive(sgroot,sgdir)
@Property(type=str)
def savegame_subdir(self)->str:
"""
savegame_subdir The subdir for the savegame backup.
If `is_live` results to `True`, *"live"* is returned. Else *"finished"* is returned.
:type: str
"""
if (self.is_live):
return "live"
return "finished"
class GameManager(GObject):
__global_gamemanager = None
@staticmethod
def get_global():
if GameManager.__global_gamemanager is None:
GameManager.__global_gamemanager = GameManager()
return GameManager.__global_gamemanager
def __init__(self):
GObject.__init__(self)
self.__games = {}
self.__steam_games = {}
self.__steam_linux_games = {}
self.__steam_windows_games = {}
self.__steam_macos_games = {}
self.load()
@Property(type=object)
def games(self)->dict[str:Game]:
return self.__games
@Property(type=object)
def stam_games(self)->dict[int:Game]:
return self.__steam_games
@Property(type=object)
def steam_windows_games(self)->dict[int:Game]:
return self.__steam_windows_games
@Property(type=object)
def steam_linux_games(self)->dict[int:Game]:
return self.__steam_linux_games
@Property(type=object)
def steam_macos_games(self)->dict[int:Game]:
return self.__steam_macos_games
def load(self):
if self.__games:
self.__games = {}
gameconf_dir = settings.gameconf_dir
if not os.path.isdir(gameconf_dir):
return
for gcf in (os.path.join(gameconf_dir,i) for i in os.listdir(gameconf_dir)):
if not os.path.isfile(gcf) or not gcf.endswith('.gameconf'):
continue
try:
game = Game.new_from_json_file(gcf)
if not game:
continue
except Exception as ex:
logger.error("Unable to load gameconf {gameconf}! ({what})".format(
gameconf = os.path.basename(gcf),
what = str(ex)))
continue
self.add_game(game)
def add_game(self,game:Game):
self.__[game.key] = game
if (game.steam_macos):
self.__steam_games[game.steam_macos.appid] = game
self.__steam_macos_games[game.steam_macos.appid] = game
if (game.steam_linux):
self.__steam_games[game.steam_linux.appid] = game
self.__steam_linux_games[game.steam_linux.appid] = game
if (game.steam_windows):
self.__steam_games[game.steam_windows.appid] = game
self.__steam_windows_games[game.steam_windows.appid] = game