@@ -90,6 +90,24 @@ def listInvalidUIFiles(folder):
9090
9191 return invalidUIFileList
9292
93+ def copyFileIfSourceExistsAndDestDoesNot (sourcePath , destinationPath ):
94+ #type: (str, str) -> None
95+ """
96+ Try to copy + move a file in a transactional way so you can't get a half-copied file.
97+ The copy will only be performed if the source path exists, and the destination path doesn't already exist.
98+ If the copy is not performed a warning message will be printed.
99+ """
100+ if not path .exists (sourcePath ):
101+ print ("WARNING: Not copying {} -> {} as source does not exist" .format (sourcePath , destinationPath ))
102+ return
103+
104+ if path .exists (destinationPath ):
105+ print ("WARNING: Not copying {} -> {} as destination already exists" .format (sourcePath , destinationPath ))
106+ return
107+
108+ shutil .copy (sourcePath , destinationPath + '.temp' )
109+ os .rename (destinationPath + '.temp' , destinationPath )
110+
93111class Installer :
94112 def getDataDirectory (self , installPath ):
95113 if common .Globals .IS_MAC :
@@ -170,31 +188,41 @@ def __init__(self, fullInstallConfiguration, extractDirectlyToGameDirectory, mod
170188
171189 self .downloaderAndExtractor .printPreview ()
172190
173- def backupUI (self ):
174- """
175- Backs up the `sharedassets0.assets` file
176- Try to do this in a transactional way so you can't get a half-copied .backup file.
177- This is important since the .backup file is needed to determine which ui file to use on future updates
178-
179- The file is not moved directly in case the installer is halted before the new UI file can be placed, resulting
180- in an install completely missing a sharedassets0.assets UI file.
181- """
182- try :
183- uiPath = path .join (self .dataDirectory , "sharedassets0.assets" )
184-
191+ def getBackupPath (self , relativePath ):
185192 # partialManualInstall is not really supported on MacOS, so just assume output folder is HigurashiEpX_Data
186193 if self .forcedExtractDirectory is not None :
187- backupPath = path .join (self .forcedExtractDirectory , self .info .subModConfig .dataName , "sharedassets0.assets. backup" )
194+ return path .join (self .forcedExtractDirectory , self .info .subModConfig .dataName , relativePath + '. backup' )
188195 else :
189- backupPath = path .join (self .dataDirectory , "sharedassets0.assets. backup" )
196+ return path .join (self .dataDirectory , relativePath + '. backup' )
190197
191- if path .exists (uiPath ) and not path .exists (backupPath ):
192- shutil .copy (uiPath , backupPath + '.temp' )
193- os .rename (backupPath + '.temp' , backupPath )
198+ def tryBackupFile (self , relativePath ):
199+ """
200+ Tries to backup a file relative to the dataDirectory of the game, unless a backup already exists.
201+ """
202+ try :
203+ sourcePath = path .join (self .dataDirectory , relativePath )
204+ backupPath = self .getBackupPath (relativePath )
205+ copyFileIfSourceExistsAndDestDoesNot (sourcePath , backupPath )
194206 except Exception as e :
195- print ('Error: Failed to backup sharedassets0.assets file: {} (need backup for future installs!) ' .format (e ))
207+ print ('Error: Failed to backup {} file: {}' .format (relativePath , e ))
196208 raise e
197209
210+
211+ def backupFiles (self ):
212+ """
213+ Backs up various files necessary for the installer to operate
214+ Usually this is to prevent the installer having issues if it fails or is stopped half-way
215+ """
216+ # Backs up the `sharedassets0.assets` file
217+ # Try to do this in a transactional way so you can't get a half-copied .backup file.
218+ # This is important since the .backup file is needed to determine which ui file to use on future updates
219+ # The file is not moved directly in case the installer is halted before the new UI file can be placed, resulting
220+ # in an install completely missing a sharedassets0.assets UI file.
221+ self .tryBackupFile ('sharedassets0.assets' )
222+ # Backs up the `resources.assets` file
223+ # The backup (resources.assets.backup) will be deleted on a successful install
224+ self .tryBackupFile ('resources.assets' )
225+
198226 def clearCompiledScripts (self ):
199227 compiledScriptsPattern = path .join (self .assetsDir , "CompiledUpdateScripts/*.mg" )
200228
@@ -439,6 +467,16 @@ def cleanup(self, cleanExtractionDirectory, cleanDownloadDirectory=True):
439467 # Removes the quarantine attribute from the game (which could cause it to get launched read-only, breaking the script compiler)
440468 subprocess .call (["xattr" , "-d" , "com.apple.quarantine" , self .directory ])
441469
470+ def removeResourcesAssetsBackup (self ):
471+ # Remove the resources.assets.backup file if install succeeds
472+ # This must be done immediately after extracting all files successfully (and before any languageSpecificAssets are applied)
473+ resourcesBackupPath = self .getBackupPath ('resources.assets' )
474+ try :
475+ if os .path .exists (resourcesBackupPath ):
476+ forceRemove (resourcesBackupPath )
477+ except Exception as e :
478+ print ("Warning: Failed to remove `{}`. Updating the mod may not work correctly unless this file is deleted." .format (resourcesBackupPath ))
479+
442480 def saveFileVersionInfoStarted (self ):
443481 self .fileVersionManager .saveVersionInstallStarted ()
444482
@@ -453,7 +491,7 @@ def main(fullInstallConfiguration):
453491
454492 isVoiceOnly = fullInstallConfiguration .subModConfig .subModName == 'voice-only'
455493 if isVoiceOnly :
456- print ("Performing Voice-Only Install - backupUI () and cleanOld() will NOT be performed." )
494+ print ("Performing Voice-Only Install - backupFiles () and cleanOld() will NOT be performed." )
457495
458496 modOptionParser = installConfiguration .ModOptionParser (fullInstallConfiguration )
459497 skipDownload = modOptionParser .downloadManually
@@ -465,6 +503,7 @@ def main(fullInstallConfiguration):
465503 installer = Installer (fullInstallConfiguration , extractDirectlyToGameDirectory = False , modOptionParser = modOptionParser , forcedExtractDirectory = extractDir )
466504 installer .download ()
467505 installer .extractFiles ()
506+ installer .removeResourcesAssetsBackup ()
468507 if installer .optionParser .installSteamGrid :
469508 steamGridExtractor .extractSteamGrid (installer .downloadDir )
470509 installer .applyLanguagePatchFixesIfNecessary ()
@@ -478,11 +517,12 @@ def main(fullInstallConfiguration):
478517 installer .download ()
479518 installer .saveFileVersionInfoStarted ()
480519 if not isVoiceOnly :
481- installer .backupUI ()
520+ installer .backupFiles ()
482521 installer .cleanOld ()
483522 print ("Extracting..." )
484523 installer .extractFiles ()
485524 commandLineParser .printSeventhModStatusUpdate (97 , "Cleaning up..." )
525+ installer .removeResourcesAssetsBackup ()
486526 if installer .optionParser .installSteamGrid :
487527 steamGridExtractor .extractSteamGrid (installer .downloadDir )
488528 installer .applyLanguagePatchFixesIfNecessary ()
@@ -497,10 +537,11 @@ def main(fullInstallConfiguration):
497537 installer .extractFiles ()
498538 commandLineParser .printSeventhModStatusUpdate (85 , "Moving files into place..." )
499539 if not isVoiceOnly :
500- installer .backupUI ()
540+ installer .backupFiles ()
501541 installer .cleanOld ()
502542 installer .moveFilesIntoPlace ()
503543 commandLineParser .printSeventhModStatusUpdate (97 , "Cleaning up..." )
544+ installer .removeResourcesAssetsBackup ()
504545 if installer .optionParser .installSteamGrid :
505546 steamGridExtractor .extractSteamGrid (installer .downloadDir )
506547 installer .applyLanguagePatchFixesIfNecessary ()
0 commit comments