def findPossibleGamePathsWindows(): """ Blindly retrieve all game folders in the `Steam\steamappps\common` folder (no filtering is performed) TODO: scan other locations than just the steamapps folder :return: a list of absolute paths, which are the folders in the `Steam\steamappps\common` folder :rtype: list[str] """ try: import winreg except ImportError: import _winreg as winreg allSteamPaths = [] try: try: registryKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\Valve\Steam') except WindowsError: # I installed Steam on a Win 10 64-bit machine and it used this alternate registry key location. Not sure why. registryKey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'Software\Wow6432Node\Valve\Steam') defaultSteamPath, _regType = winreg.QueryValueEx(registryKey, 'SteamPath') allSteamPaths.append(defaultSteamPath) winreg.CloseKey(registryKey) except WindowsError: print("findPossibleGamePaths: Couldn't read Steam registry key - Steam not installed?") return [] # now that we know the steam path, search the "Steam\config\config.vdf" file for extra install paths # this is a purely optional step, so it's OK if it fails try: import io baseInstallFolderRegex = re.compile(r'^\s*"BaseInstallFolder_\d+"\s*"([^"]+)"', re.MULTILINE) steamConfigVDFLocation = os.path.join(defaultSteamPath, r'config\config.vdf') with io.open(steamConfigVDFLocation, 'r', encoding='UTF-8') as configVDFFile: allSteamPaths += baseInstallFolderRegex.findall(configVDFFile.read()) except Exception as e: traceback.print_exc() logger.printNoTerminal("Will scan the following steam install locations: {}".format(allSteamPaths)) # normpath added so returned paths have consistent slash directions (registry key has forward slashes on Win...) try: allPossibleGamePaths = [] for steamCommonPath in (os.path.join(steamPath, r'steamapps\common') for steamPath in allSteamPaths): for gameFolderName in os.listdir(steamCommonPath): gameFolderPath = os.path.join(steamCommonPath, gameFolderName) if os.path.isdir(gameFolderPath): allPossibleGamePaths.append( os.path.normpath( gameFolderPath ) ) return allPossibleGamePaths except: print("findPossibleGamePaths: Couldn't open registry key folder - Steam folder deleted?") return [] return []
def getSubModConfigList(modList): subModconfigList = [] for mod in modList: for submod in mod['submods']: conf = installConfiguration.SubModConfig(mod, submod) logger.printNoTerminal(conf) subModconfigList.append(conf) return subModconfigList
def startInstallHandler(requestData): # this is not a 'proper' submod - just a handle returned form getSubModHandlesRequestHandler() webSubModHandle = requestData['subMod'] webModOptionGroups = webSubModHandle['modOptionGroups'] id = webSubModHandle['id'] validateOnly = requestData.get('validateOnly', False) deleteVersionInformation = requestData.get( 'deleteVersionInformation', False) installSteamGrid = requestData.get('installSteamGrid', False) subMod = self.idToSubMod[id] updateModOptionsFromWebFormat(subMod.modOptions, webModOptionGroups) logger.printNoTerminal("\nUser selected options for install:") for modOption in subMod.modOptions: logger.printNoTerminal(modOption) installPath = requestData.get('installPath', None) installValid, fullInstallConfiguration = self.try_start_install( subMod, installPath, validateOnly, installSteamGrid) retval = {'installStarted': installValid} if installValid: if deleteVersionInformation: fileVersionManagement.VersionManager.tryDeleteLocalVersionFile( fullInstallConfiguration.installPath) downloadItemsPreview, totalDownloadSize, numUpdatesRequired, fullUpdateRequired, partialReinstallDetected = getDownloadPreview( fullInstallConfiguration) haveEnoughFreeSpace, freeSpaceAdvisoryString = common.checkFreeSpace( installPath=fullInstallConfiguration.installPath, recommendedFreeSpaceBytes=totalDownloadSize * common.Globals.DOWNLOAD_TO_EXTRACTION_SCALING) CWDHaveEnoughFreeSpaceInstallerPath, CWDFreeSpaceAdvisoryStringInstallerPath = common.checkFreeSpace( installPath=os.getcwd(), recommendedFreeSpaceBytes=totalDownloadSize * common.Globals.DOWNLOAD_TO_EXTRACTION_SCALING) retval[ 'validatedInstallPath'] = fullInstallConfiguration.installPath retval['haveEnoughFreeSpace'] = haveEnoughFreeSpace retval['freeSpaceAdvisoryString'] = freeSpaceAdvisoryString retval[ 'CWDHaveEnoughFreeSpace'] = CWDHaveEnoughFreeSpaceInstallerPath retval[ 'CWDFreeSpaceAdvisoryString'] = CWDFreeSpaceAdvisoryStringInstallerPath retval['downloadItemsPreview'] = downloadItemsPreview retval['numUpdatesRequired'] = numUpdatesRequired retval['fullUpdateRequired'] = fullUpdateRequired retval[ 'partialReinstallDetected'] = partialReinstallDetected return retval
def __init__(self, subMod, modFileList, localVersionFolder, _testRemoteSubModVersion=None): #type: (installConfiguration.SubModConfig, List[installConfiguration.ModFile], str, Optional[SubModVersionInfo]) -> None self.targetID = subMod.modName + '/' + subMod.subModName self.unfilteredModFileList = modFileList self.localVersionFilePath = os.path.join(localVersionFolder, VersionManager.localVersionFileName) # Get remote and local versions try: self.localVersionInfo = getLocalVersion(self.localVersionFilePath) except Exception as error: self.localVersionInfo = None print("VersionManager: Error while retrieving version information: {}".format(error)) # allow overriding the remote sub mod version for testing purposes if _testRemoteSubModVersion is not None: self.remoteVersionInfo = _testRemoteSubModVersion else: try: self.remoteVersionInfo = getRemoteVersion(self.targetID) except Exception as error: self.remoteVersionInfo = None print("VersionManager: Error while retrieving remote version information {}".format(error)) logger.printNoTerminal("\nLocal Version: {}".format(self.localVersionInfo)) logger.printNoTerminal("Remote Version: {}".format(self.remoteVersionInfo)) # If can't retrieve version info, mark everything as needing update if self.localVersionInfo is None: self.updatesRequiredDict = {} for file in self.unfilteredModFileList: self.updatesRequiredDict[file.id] = (True, "No local version information - Assuming update is required") elif self.remoteVersionInfo is None: self.updatesRequiredDict = {} for file in self.unfilteredModFileList: self.updatesRequiredDict[file.id] = (True, "Failed to retrieve remote version information") else: # Mark files which need update self.updatesRequiredDict = getFilesNeedingUpdate(self.unfilteredModFileList, self.localVersionInfo, self.remoteVersionInfo) print("\nInstaller Update Information:") for fileID, (needsUpdate, updateReason) in self.updatesRequiredDict.items(): print("[{}]: status: [{}] because [{}]".format(fileID, needsUpdate, updateReason)) # Check how many updates are required updatesRequiredList = self.updatesRequiredDict.values() self.totalNumUpdates = len(updatesRequiredList) self.numUpdatesRequired = sum([needsUpdate for (needsUpdate, _) in updatesRequiredList]) print("Full Update: {} ({}/{}) excluding mod options".format(self.fullUpdateRequired(), self.numUpdatesRequired, self.totalNumUpdates))
def handleInstallerData(body_string): # type: (str) -> str requestType, requestData = _decodeJSONRequest(body_string) if requestType != 'statusUpdate': logger.printNoTerminal('Got Request [{}] Data [{}]'.format( requestType, requestData)) # requestData: set which game the user selected by specifying the mods->name field from the json, eg "Onikakushi Ch.1" # responseData: a dictionary indicating if it's a valid selection (true, false) def setModName(requestData): userSelectedModToInstall = requestData['modName'] modNames = [config.modName for config in self.allSubModConfigs] modNameValid = userSelectedModToInstall in modNames if modNameValid: self.selectedModName = userSelectedModToInstall return {'valid': modNameValid, 'modNames': modNames} # requestData: leave as null. will be ignored. # responseData: A dictionary containing basic information about each subModConfig, along with it's index. # Most important is the index, which must be submitted in the 'getGamePaths' request. def getSubModHandlesRequestHandler(requestData): # a list of 'handles' to each submod. # This contains just enough information about each submod so that the python script knows # which config was chosen, and which subModHandles = [] for subModConfig in self.allSubModConfigs: subModHandles.append({ 'id': subModConfig.id, 'modName': subModConfig.modName, 'subModName': subModConfig.subModName, 'descriptionID': subModConfig.descriptionID, 'modOptionGroups': modOptionsToWebFormat(subModConfig.modOptions), 'family': subModConfig.family, 'identifiers': subModConfig.identifiers, }) return { 'selectedMod': self.selectedModName, 'subModHandles': subModHandles, 'logFilePath': os.path.abspath(common.Globals.LOG_FILE_PATH), 'os': common.Globals.OS_STRING, } # requestData: A dictionary, which contains a field 'id' containing the ID of the subMod to install, or None to get ALL possible games # responseData: A dictionary containing basic information about each fullConfig. Most important is the path # which must be submitted in the final install step. # NOTE: the idOfSubMod is not unique in the returned list. You must supply both a submod ID # and a path to the next stage def getGamePathsHandler(requestData): id = requestData['id'] selectedSubMods = [ self.idToSubMod[id] ] if id is not None else self.allSubModConfigs fullInstallConfigs = gameScanner.scanForFullInstallConfigs( selectedSubMods) fullInstallConfigHandles = [] for fullConfig in fullInstallConfigs: fullInstallConfigHandles.append({ 'id': fullConfig.subModConfig.id, 'modName': fullConfig.subModConfig.modName, 'subModName': fullConfig.subModConfig.subModName, 'path': fullConfig.installPath, 'isSteam': fullConfig.isSteam, }) return fullInstallConfigHandles #TODO: for security reasons, can't get full path from browser. Either need to copy paste, or open a # tk window . Adding a tk window would then require tk dependencies (no problem except requring tk on linux) # requestData: The submod ID and install path. If the install path is not specified, then the tkinter # window chooser will be used # responseData: If the path is valid: # If the path is invalid: null is returned def startInstallHandler(requestData): # this is not a 'proper' submod - just a handle returned form getSubModHandlesRequestHandler() webSubModHandle = requestData['subMod'] webModOptionGroups = webSubModHandle['modOptionGroups'] id = webSubModHandle['id'] validateOnly = requestData.get('validateOnly', False) deleteVersionInformation = requestData.get( 'deleteVersionInformation', False) subMod = self.idToSubMod[id] updateModOptionsFromWebFormat(subMod.modOptions, webModOptionGroups) logger.printNoTerminal("\nUser selected options for install:") for modOption in subMod.modOptions: logger.printNoTerminal(modOption) installPath = requestData.get('installPath', None) installValid, fullInstallConfiguration = self.try_start_install( subMod, installPath, validateOnly) retval = {'installStarted': installValid} if installValid: if deleteVersionInformation: fileVersionManagement.VersionManager.tryDeleteLocalVersionFile( fullInstallConfiguration.installPath) downloadItemsPreview, totalDownloadSize, numUpdatesRequired, fullUpdateRequired = getDownloadPreview( fullInstallConfiguration) haveEnoughFreeSpace, freeSpaceAdvisoryString = common.checkFreeSpace( installPath=fullInstallConfiguration.installPath, recommendedFreeSpaceBytes=totalDownloadSize * common.Globals.DOWNLOAD_TO_EXTRACTION_SCALING) CWDHaveEnoughFreeSpaceInstallerPath, CWDFreeSpaceAdvisoryStringInstallerPath = common.checkFreeSpace( installPath=os.getcwd(), recommendedFreeSpaceBytes=totalDownloadSize * common.Globals.DOWNLOAD_TO_EXTRACTION_SCALING) retval[ 'validatedInstallPath'] = fullInstallConfiguration.installPath retval['haveEnoughFreeSpace'] = haveEnoughFreeSpace retval['freeSpaceAdvisoryString'] = freeSpaceAdvisoryString retval[ 'CWDHaveEnoughFreeSpace'] = CWDHaveEnoughFreeSpaceInstallerPath retval[ 'CWDFreeSpaceAdvisoryString'] = CWDFreeSpaceAdvisoryStringInstallerPath retval['downloadItemsPreview'] = downloadItemsPreview retval['numUpdatesRequired'] = numUpdatesRequired retval['fullUpdateRequired'] = fullUpdateRequired return retval # requestData: Not necessary - will be ignored # responseData: Returns a list of dictionaries. Each dictionary may have different fields depending on the # type of status returned. # Please check the _loggerMessageToStatusDict() function for a full list of fields. def statusUpdate(requestData): return [ _loggerMessageToStatusDict(x) for x in logger.getGlobalLogger().threadSafeReadAll() ] def getNews(requestData): return common.tryGetRemoteNews(requestData) def getDonationStatus(requestData): monthsRemaining, progressPercent = common.getDonationStatus() return { 'monthsRemaining': monthsRemaining, 'progressPercent': progressPercent, } def getInstallerMetaInfo(requestData): return { 'buildInfo': common.Globals.BUILD_INFO, 'lockFileExists': common.lockFileExists( ), # Indicate if it looks like install already in progress 'operatingSystem': common.Globals.OS_STRING, 'installAlreadyInProgress': self.installAlreadyInProgress(), } # This causes a TKInter window to open allowing the user to choose a game path. # The request data should be the submod ID. # This is required so that the correct file filter can be applied to the tkinter file chooser. # The function returns None (Javascript null) if the user failed to select a path by pressing 'cancel'. def showFileChooser(requestDataSubModID): subMod = self.idToSubMod[requestDataSubModID] selectedPath = _TKAskPath(subMod) return {'path': selectedPath if selectedPath else None} def unknownRequestHandler(requestData): return 'Invalid request type [{}]. Should be one of [{}]'.format( requestType, requestTypeToRequestHandlers.items()) # This function takes identical arguments to 'startInstallHandler(...)' # TODO: Add correct paths for Linux and Mac def troubleshoot(requestData): action = requestData['action'] id = requestData['subMod']['id'] subMod = self.idToSubMod[id] # If the requestData included the install path, use that. Otherwise, open a dialog to choose the path # returns the empty string if user cancels selecting a path def _getInstallPath(): _installPath = requestData.get('installPath', None) if _installPath is None: userSelectedPath = os.path.dirname(_TKAskPath(subMod)) fullInstallConfigs, errorMessage = gameScanner.scanUserSelectedPath( [subMod], userSelectedPath) _installPath = '' if not fullInstallConfigs else fullInstallConfigs[ 0].installPath return _installPath if action == 'getLogsZip': installPath = _getInstallPath() higurashi_log_file_name = 'output_log.txt' gameLogPath = os.path.join(installPath, subMod.dataName, higurashi_log_file_name) gameLogExists = os.path.exists(gameLogPath) with zipfile.ZipFile( os.path.join(workingDirectory, common.Globals.LOGS_ZIP_FILE_PATH), 'w') as myzip: for filename in os.listdir(common.Globals.LOG_FOLDER): path = os.path.join(common.Globals.LOG_FOLDER, filename) myzip.write(path, os.path.basename(path)) if gameLogExists: myzip.write(gameLogPath, higurashi_log_file_name) print('Game Log [{}] {}'.format( gameLogPath, "was found" if gameLogExists else "WAS NOT FOUND")) return { 'filePath': common.Globals.LOGS_ZIP_FILE_PATH, 'gameLogFound': gameLogExists } elif action == 'showLogs': installPath = _getInstallPath() logsPath = installPath if subMod.family == 'higurashi': if common.Globals.IS_MAC: logsPath = os.path.join(installPath, "Contents/Resources/Data") else: logsPath = os.path.join(installPath, subMod.dataName) if os.path.exists(logsPath): print('Trying to open [{}]'.format(logsPath)) common.trySystemOpen(logsPath, normalizePath=True) else: return { 'error': 'Cant open Logs Folder [{}] as it doesnt exist!'. format(logsPath) } return {} elif action == 'openSaveFolder': if subMod.family == 'higurashi': result = re.findall(r'\d\d', subMod.dataName) if result: saveFolderName = os.path.expandvars( '%appdata%\Mangagamer\higurashi' + result[0]) else: return { 'error': 'Sorry, cant figure out higurashi episode number :(' } elif subMod.family == 'umineko': saveFolderName = os.path.join(_getInstallPath(), 'mysav') elif subMod.family == 'umineko_nscripter': # For now just open the all users profile folder # The actual save folder will be set according to the ';gameid' defined at the top of the script file saveFolderName = os.path.expandvars( '%AllUsersProfile%') else: return { 'error': 'Cant open save folder: Unknown game family {}'. format(subMod.family) } if os.path.exists(saveFolderName): print('Trying to open [{}]'.format(saveFolderName)) common.trySystemOpen(saveFolderName, normalizePath=True) else: return { 'error': 'Save Folder [{}] doesnt exist! Have you made any saves yet?' .format(saveFolderName) } return {} requestTypeToRequestHandlers = { 'setModName': setModName, 'subModHandles': getSubModHandlesRequestHandler, 'gamePaths': getGamePathsHandler, 'startInstall': startInstallHandler, 'statusUpdate': statusUpdate, 'getNews': getNews, 'getDonationStatus': getDonationStatus, 'troubleshoot': troubleshoot, 'showFileChooser': showFileChooser, 'getInstallerMetaInfo': getInstallerMetaInfo, } requestHandler = requestTypeToRequestHandlers.get( requestType, None) # Check for unknown request if not requestHandler: return _makeJSONResponse('unknownRequest', unknownRequestHandler(requestData)) # Try and execute the request. If an exception is thrown, display the reason to the user on the web GUI try: responseDataJson = requestHandler(requestData) except Exception as exception: print('Exception Thrown handling request {}: {}'.format( requestType, exception)) traceback.print_exc() return _makeJSONResponse( 'error', { 'errorReason': 'Exception handling [{}] request: {}'.format( requestType, traceback.format_exc()) }) return _makeJSONResponse(responseType=requestType, responseDataJson=responseDataJson)
def send_head(self): """ Copy and pasted from SimpleHTTPRequestHandler class because it's difficult to alter the headers without modifying the function directly Common code for GET and HEAD commands. This sends the response code and MIME headers. Return value is either a file object (which has to be copied to the outputfile by the caller unless the command was HEAD, and must be closed by the caller under all circumstances), or None, in which case the caller has nothing further to do. """ originalPath = self.translate_path(self.path) # --------- THE FOLLOWING WAS ADDED --------- # Python 3 has the ability to change web directory built-in, but Python 2 does not. relativePath = os.path.relpath(originalPath, os.getcwd()) path = os.path.join( working_directory, relativePath ) # working_directory is captured from outer scope! logger.printNoTerminal( 'Browser requested [{}], Trying to deliver [{}]'.format( self.path, path)) # --------- END ADDED SECTION --------- f = None if os.path.isdir(path): parts = urlparse.urlsplit(self.path) if not parts.path.endswith('/'): # redirect browser - doing basically what apache does self.send_response(301) new_parts = (parts[0], parts[1], parts[2] + '/', parts[3], parts[4]) new_url = urlparse.urlunsplit(new_parts) self.send_header("Location", new_url) self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) ctype = self.guess_type(path) try: # Always read in binary mode. Opening files in text mode may cause # newline translations, making the actual size of the content # transmitted *less* than the content-length! f = open(path, 'rb') except IOError: self.send_error(404, "File not found") logger.printNoTerminal( '404 Error: Cant deliver [{}] - file not found!\n'.format( path)) return None try: self.send_response(200) self.send_header("Content-type", ctype) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) # --------- THE FOLLOWING WAS ADDED --------- self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate') self.send_header('Pragma', 'no-cache') self.send_header('Expires', '0') # --------- END ADDED SECTION --------- self.end_headers() return f except: f.close() raise
def scanForFullInstallConfigs(subModConfigList, possiblePaths=None, scanExtraPaths=True): # type: (List[installConfiguration.SubModConfig], [str], bool) -> [installConfiguration.FullInstallConfiguration, List[str]] """ This function has two purposes: - When given a specific game path ('possiblePaths' argument), it checks if any of the given SubModConfig can be installed into that path. Each SubModConfig which can be installed into that path will be returned as a FullInstallConfiguration object. - When not given a specific game path, it searches the computer for valid installations where the given SubModConfig could be installed to. Each valid (installation + SubModConfig) combination will be returned as a FullInstallConfiguration object. :param subModConfigList: A **list** of SubModConfig which are to be searched for on disk :param possiblePaths: (Optional) Specify folders to check if the given SubModConfig can be installed into that path. :return: 1. A list of FullInstallConfig, each representing a valid install path that the given SubModConfig(s) couldbe installed into. 2. A list of games which were "partially uninstalled" by Steam - steam deletes game files, but not the mod files. The user should be notified to delete these files manually. """ returnedFullConfigs = [] returnedPartiallyUninstalledPaths = [] pathsToBeScanned = possiblePaths if not pathsToBeScanned: pathsToBeScanned = getMaybeGamePaths() # Build a mapping from subModIdentifier -> List[subMod] # This tells us, for each identifier, which subMods are compatible with that identifier (can be installed) # In all our games, the identifiers are the same for each subMod (but different for each Mod), # but it is easier to work with in the installer if we work with subMods from collections import defaultdict subModConfigDictionary = defaultdict( list) #type: defaultdict[List[installConfiguration.SubModConfig]] for subMod in subModConfigList: # If autodetection is disabled, and autodetection requested, do not scan for this submod if not subMod.autodetect and possiblePaths is None: continue for identifier in subMod.identifiers: subModConfigDictionary[identifier].append(subMod) # If there are no identifiers to be matched, give up immediately as we'll never find a match if not subModConfigDictionary: return [], [] if scanExtraPaths: extraPaths = [] for gamePath in pathsToBeScanned: # MacOS: Any subpath with '.app' is also checked in case the containing path was manually entered extraPaths.extend(glob.glob(os.path.join(gamePath, "*.app"))) # GOG Linux: Higurashi might be inside a 'game' subfolder extraPaths.extend(glob.glob(os.path.join(gamePath, "game"))) pathsToBeScanned += extraPaths logger.printNoTerminal("Scanning:\n\t- " + "\n\t- ".join(pathsToBeScanned)) for gamePath in pathsToBeScanned: possibleIdentifiers = getPossibleIdentifiersFromFolder(gamePath) subModConfigsInThisGamePath = set() possibleSteamPaths = [ os.path.join(gamePath, "steam_api.dll"), os.path.join(gamePath, "Contents/Plugins/CSteamworks.bundle"), os.path.join(gamePath, "libsteam_api.so") ] isSteam = False for possibleSteamPath in possibleSteamPaths: if os.path.exists(possibleSteamPath): isSteam = True if gamePathIsPartiallyUninstalled(gamePath): returnedPartiallyUninstalledPaths.append(gamePath) for possibleIdentifier in possibleIdentifiers: try: # Add each submod which is compatible with the found identifier, unless it has already been detected at this path. for subModConfig in subModConfigDictionary[possibleIdentifier]: if subModConfig not in subModConfigsInThisGamePath: subModConfigsInThisGamePath.add(subModConfig) returnedFullConfigs.append( installConfiguration.FullInstallConfiguration( subModConfig, gamePath, isSteam)) print("Found Game [{}] at [{}] id [{}]".format( subModConfig.modName, gamePath, possibleIdentifier)) except KeyError: pass return returnedFullConfigs, returnedPartiallyUninstalledPaths
def __init__(self, subMod, modFileList, localVersionFolder, _testRemoteSubModVersion=None, verbosePrinting=True): #type: (installConfiguration.SubModConfig, List[installConfiguration.ModFile], str, Optional[SubModVersionInfo], bool) -> None self.verbosePrinting = verbosePrinting self.targetID = subMod.modName + '/' + subMod.subModName self.unfilteredModFileList = modFileList self.localVersionFilePath = os.path.join( localVersionFolder, VersionManager.localVersionFileName) # Get remote and local versions try: self.localVersionInfo = getLocalVersion(self.localVersionFilePath) except Exception as error: self.localVersionInfo = None print( "VersionManager: Error while retrieving version information: {}" .format(error)) # allow overriding the remote sub mod version for testing purposes if _testRemoteSubModVersion is not None: self.remoteVersionInfo = _testRemoteSubModVersion else: try: self.remoteVersionInfo = getRemoteVersion(self.targetID) except Exception as error: self.remoteVersionInfo = None print( "VersionManager: Error while retrieving remote version information {}" .format(error)) if verbosePrinting: logger.printNoTerminal("\nLocal Version: {}".format( self.localVersionInfo)) logger.printNoTerminal("Remote Version: {}".format( self.remoteVersionInfo)) # If can't retrieve version info, mark everything as needing update if self.localVersionInfo is None: self.updatesRequiredDict = {} for file in self.unfilteredModFileList: self.updatesRequiredDict[file.id] = ( True, "No local version information - Assuming update is required" ) elif self.remoteVersionInfo is None: self.updatesRequiredDict = {} for file in self.unfilteredModFileList: self.updatesRequiredDict[file.id] = ( True, "Failed to retrieve remote version information") else: # Mark files which need update self.updatesRequiredDict = getFilesNeedingUpdate( self.unfilteredModFileList, self.localVersionInfo, self.remoteVersionInfo) if verbosePrinting: print("\nInstaller Update Information:") for fileID, (needsUpdate, updateReason) in self.updatesRequiredDict.items(): print("[{}]: status: [{}] because [{}]".format( fileID, needsUpdate, updateReason)) # If file has 'skipIfModNewerThan' property, disable it if the mod install is older than the given date for file in self.unfilteredModFileList: if file.skipIfModNewerThan is not None and self.updatesRequiredDict[ file.id][0]: installIsNewer, reason = installNewerThanDate( self.localVersionFilePath, file.skipIfModNewerThan) if installIsNewer: msg = "Not installing {} because: ({})".format( file.id, reason) self.updatesRequiredDict[file.id] = ( False, "Not installing because you already have these files") print(msg) else: msg = "{} - Will install because: ({})".format( self.updatesRequiredDict[file.id][1], reason) self.updatesRequiredDict[file.id] = ( self.updatesRequiredDict[file.id][0], "You are missing these files (judging from your last mod install date)" ) print(msg) # Check how many updates are required updatesRequiredList = self.updatesRequiredDict.values() self.totalNumUpdates = len(updatesRequiredList) self.numUpdatesRequired = sum( [needsUpdate for (needsUpdate, _) in updatesRequiredList]) if verbosePrinting: print("Full Update: {} ({}/{}) excluding mod options".format( self.fullUpdateRequired(), self.numUpdatesRequired, self.totalNumUpdates))