private static CacheData GetCacheableDataInFMInstalledDir(FanMission fm) { AssertR(fm.Installed, "fm.Installed is false when it should be true"); string thisFMInstallsBasePath = Config.GetFMInstallPathUnsafe(fm.Game); string path = Path.Combine(thisFMInstallsBasePath, fm.InstalledDir); var files = FastIO.GetFilesTopOnly(path, "*"); string t3ReadmePath1 = Path.Combine(path, Paths.T3ReadmeDir1); string t3ReadmePath2 = Path.Combine(path, Paths.T3ReadmeDir2); if (Directory.Exists(t3ReadmePath1)) { files.AddRange(FastIO.GetFilesTopOnly(t3ReadmePath1, "*")); } if (Directory.Exists(t3ReadmePath2)) { files.AddRange(FastIO.GetFilesTopOnly(t3ReadmePath2, "*")); } RemoveEmptyFiles(files); var readmes = new List <string>(files.Count); foreach (string f in files) { if (f.IsValidReadme()) { readmes.Add(f.Substring(path.Length + 1)); } } return(new CacheData(readmes)); }
private static List <string> GetFMSupportedLanguagesFromInstDir(string fmInstPath, bool earlyOutOnEnglish) { // Get initial list of base FM dirs the normal way: we don't want to count these as lang dirs even if // they're named such (matching FMSel behavior) var searchList = FastIO.GetDirsTopOnly(fmInstPath, "*", ignoreReparsePoints: true); if (searchList.Count == 0) { return(new List <string>()); } #region Move key dirs to end of list (priority) // Searching folders is horrendously slow, so prioritize folders most likely to contain lang dirs so // if we find English, we end up earlying-out much faster for (int i = 0; i < 3; i++) { string keyDir = i switch { 0 => "/books", 1 => "/intrface", _ => "/strings" }; for (int j = 0; j < searchList.Count; j++) { if (j < searchList.Count - 1 && searchList[j].PathEndsWithI(keyDir)) { string item = searchList[j]; searchList.RemoveAt(j); searchList.Add(item); break; } } } #endregion var langsFoundList = new HashSetI(Supported.Length); while (searchList.Count > 0) { string bdPath = searchList[searchList.Count - 1]; searchList.RemoveAt(searchList.Count - 1); bool englishFound = FastIO.SearchDirForLanguages(SupportedHash, bdPath, searchList, langsFoundList, earlyOutOnEnglish); // Matching FMSel behavior: early-out on English if (earlyOutOnEnglish && englishFound) { return new List <string> { "English" } } ; } return(SortLangsToSpec(langsFoundList)); }
private static void ClearCacheDir(FanMission fm) { string fmCachePath = Path.Combine(Paths.FMsCache, fm.InstalledDir); if (!fmCachePath.TrimEnd(CA_BS_FS).PathEqualsI(Paths.FMsCache.TrimEnd(CA_BS_FS)) && Directory.Exists(fmCachePath)) { try { foreach (string f in FastIO.GetFilesTopOnly(fmCachePath, "*")) { File.Delete(f); } foreach (string d in FastIO.GetDirsTopOnly(fmCachePath, "*")) { Directory.Delete(d, recursive: true); } } catch (Exception ex) { Log("Exception clearing files in FM cache for " + fm.Archive + " / " + fm.InstalledDir, ex); } } }
internal static void CreateOrClearTempPath(string path) { #region Safety check // Make sure we never delete any paths that are not safely tucked in our temp folder string baseTemp = _baseTemp.TrimEnd(CA_BS_FS_Space); // @DIRSEP: getting rid of this concat is more trouble than it's worth // This method is called rarely and only once in a row bool pathIsInTempDir = path.PathStartsWithI(baseTemp + "\\"); Misc.AssertR(pathIsInTempDir, "Path '" + path + "' is not in temp dir '" + baseTemp + "'"); if (!pathIsInTempDir) { return; } #endregion if (Directory.Exists(path)) { try { foreach (string f in Directory.GetFiles(path, "*", SearchOption.AllDirectories)) { new FileInfo(f).IsReadOnly = false; } foreach (string d in Directory.GetDirectories(path, "*", SearchOption.AllDirectories)) { Misc.Dir_UnSetReadOnly(d); } } catch (Exception ex) { Log("Exception setting temp path subtree to all non-readonly.\r\n" + "path was: " + path, ex); } try { foreach (string f in FastIO.GetFilesTopOnly(path, "*")) { File.Delete(f); } foreach (string d in FastIO.GetDirsTopOnly(path, "*")) { Directory.Delete(d, recursive: true); } } catch (Exception ex) { Log("Exception clearing temp path " + path, ex); } } else { try { Directory.CreateDirectory(path); } catch (Exception ex) { Log("Exception creating temp path " + path, ex); } } }
internal static async Task BackupFM(FanMission fm, string fmInstalledPath, string fmArchivePath) { bool backupSavesAndScreensOnly = fmArchivePath.IsEmpty() || (Config.BackupFMData == BackupFMData.SavesAndScreensOnly && (fm.Game != Game.Thief3 || !Config.T3UseCentralSaves)); if (!GameIsKnownAndSupported(fm.Game)) { Log("Game type is unknown or unsupported (" + fm.Archive + ", " + fm.InstalledDir + ", " + fm.Game + ")", stackTrace: true); return; } await Task.Run(() => { if (backupSavesAndScreensOnly && fm.InstalledDir.IsEmpty()) { return; } string thisFMInstallsBasePath = Config.GetFMInstallPathUnsafe(fm.Game); string savesDir = fm.Game == Game.Thief3 ? _t3SavesDir : _darkSavesDir; string savesPath = Path.Combine(thisFMInstallsBasePath, fm.InstalledDir, savesDir); string netSavesPath = Path.Combine(thisFMInstallsBasePath, fm.InstalledDir, _darkNetSavesDir); // Screenshots directory name is the same for T1/T2/T3/SS2 string screensPath = Path.Combine(thisFMInstallsBasePath, fm.InstalledDir, _screensDir); string ss2CurrentPath = Path.Combine(thisFMInstallsBasePath, fm.InstalledDir, _ss2CurrentDir); string bakFile = Path.Combine(Config.FMsBackupPath, (!fm.Archive.IsEmpty() ? fm.Archive.RemoveExtension() : fm.InstalledDir) + Paths.FMBackupSuffix); if (backupSavesAndScreensOnly) { var savesAndScreensFiles = new List <string>(); if (Directory.Exists(savesPath)) { savesAndScreensFiles.AddRange(Directory.GetFiles(savesPath, "*", SearchOption.AllDirectories)); } if (Directory.Exists(netSavesPath)) { savesAndScreensFiles.AddRange(Directory.GetFiles(netSavesPath, "*", SearchOption.AllDirectories)); } if (Directory.Exists(screensPath)) { savesAndScreensFiles.AddRange(Directory.GetFiles(screensPath, "*", SearchOption.AllDirectories)); } if (fm.Game == Game.SS2) { savesAndScreensFiles.AddRange(Directory.GetFiles(ss2CurrentPath, "*", SearchOption.AllDirectories)); var ss2SaveDirs = FastIO.GetDirsTopOnly( Path.Combine(thisFMInstallsBasePath, fm.InstalledDir), "save_*"); foreach (string dir in ss2SaveDirs) { if (_ss2SaveDirsOnDiskRegex.IsMatch(dir)) { savesAndScreensFiles.AddRange(Directory.GetFiles(dir, "*", SearchOption.AllDirectories)); } } } if (savesAndScreensFiles.Count == 0) { return; } using var archive = new ZipArchive(new FileStream(bakFile, FileMode.Create, FileAccess.Write), ZipArchiveMode.Create, leaveOpen: false); foreach (string f in savesAndScreensFiles) { string fn = f.Substring(fmInstalledPath.Length).Trim(CA_BS_FS); AddEntry(archive, f, fn); } return; } string[] installedFMFiles = Directory.GetFiles(fmInstalledPath, "*", SearchOption.AllDirectories); var(changedList, addedList, fullList) = GetFMDiff(installedFMFiles, fmInstalledPath, fmArchivePath, fm.Game); // If >90% of files are different, re-run and use only size difference // They could have been extracted with NDL which uses SevenZipSharp and that one puts different // timestamps, when it puts the right ones at all if (changedList.Count > 0 && ((double)changedList.Count / fullList.Count) > 0.9) { (changedList, addedList, fullList) = GetFMDiff(installedFMFiles, fmInstalledPath, fmArchivePath, fm.Game, useOnlySize: true); } try { using var archive = new ZipArchive(new FileStream(bakFile, FileMode.Create, FileAccess.Write), ZipArchiveMode.Create, leaveOpen: false); foreach (string f in installedFMFiles) { string fn = f.Substring(fmInstalledPath.Length).Trim(CA_BS_FS); if (IsSaveOrScreenshot(fn, fm.Game) || (!fn.PathEqualsI(Paths.FMSelInf) && !fn.PathEqualsI(_startMisSav) && (changedList.PathContainsI(fn) || addedList.PathContainsI(fn)))) { AddEntry(archive, f, fn); } } string fmSelInfString = ""; for (int i = 0; i < fullList.Count; i++) { string f = fullList[i]; if (!installedFMFiles.PathContainsI(Path.Combine(fmInstalledPath, f))) { // @DIRSEP: Test if FMSel is dirsep-agnostic here. If so, remove the ToSystemDirSeps() fmSelInfString += _removeFileEq + f.ToSystemDirSeps() + "\r\n"; } } if (!fmSelInfString.IsEmpty()) { var entry = archive.CreateEntry(Paths.FMSelInf, CompressionLevel.Fastest); using var eo = entry.Open(); using var sw = new StreamWriter(eo, Encoding.UTF8); sw.Write(fmSelInfString); } } catch (Exception ex) { Log("Exception in zip archive create and/or write (" + fm.Archive + ", " + fm.InstalledDir + ", " + fm.Game + ")", ex); } }); }
internal static async Task RestoreFM(FanMission fm) { if (!GameIsKnownAndSupported(fm.Game)) { Log("Game type is unknown or unsupported (" + fm.Archive + ", " + fm.InstalledDir + ", " + fm.Game + ")", stackTrace: true); return; } bool restoreSavesAndScreensOnly = Config.BackupFMData == BackupFMData.SavesAndScreensOnly && (fm.Game != Game.Thief3 || !Config.T3UseCentralSaves); bool fmIsT3 = fm.Game == Game.Thief3; await Task.Run(() => { (string Name, bool DarkLoader)fileToUse = ("", false); #region DarkLoader string dlBakDir = Path.Combine(Config.FMsBackupPath, Paths.DarkLoaderSaveBakDir); if (Directory.Exists(dlBakDir)) { foreach (string f in FastIO.GetFilesTopOnly(dlBakDir, "*.zip")) { string fn = f.GetFileNameFast(); int index = fn.LastIndexOf("_saves.zip", StringComparison.OrdinalIgnoreCase); if (index == -1) { continue; } string an = fn.Substring(0, index).Trim(); // Account for the fact that DarkLoader trims archive names for save backup zips // Note: I guess it doesn't?! The code heavily implies it does. Still, it works either // way, so whatever. if (!an.IsEmpty() && an.PathEqualsI(fm.Archive.RemoveExtension().Trim())) { fileToUse = (f, true); break; } } } #endregion #region AngelLoader / FMSel / NewDarkLoader if (fileToUse.Name.IsEmpty()) { var bakFiles = new List <FileInfo>(); void AddBakFilesFrom(string path) { for (int i = 0; i < 2; i++) { string fNoExt = i == 0 ? fm.Archive.RemoveExtension() : fm.InstalledDir; string bakFile = Path.Combine(path, fNoExt + Paths.FMBackupSuffix); if (File.Exists(bakFile)) { bakFiles.Add(new FileInfo(bakFile)); } } } // Our backup path, separate to avoid creating any more ambiguity AddBakFilesFrom(Config.FMsBackupPath); // If ArchiveName.bak and InstalledName.bak files both exist, use the newest of the two fileToUse.Name = bakFiles.Count == 1 ? bakFiles[0].FullName : bakFiles.Count > 1 ? bakFiles.OrderByDescending(x => x.LastWriteTime).ToList()[0].FullName : ""; bakFiles.Clear(); // Use file from our bak dir if it exists, otherwise use the newest file from all archive dirs // (for automatic use of FMSel/NDL saves) if (fileToUse.Name.IsEmpty()) { foreach (string path in FMArchives.GetFMArchivePaths()) { AddBakFilesFrom(path); } if (bakFiles.Count == 0) { return; } // Use the newest of all files found in all archive dirs fileToUse.Name = bakFiles.OrderByDescending(x => x.LastWriteTime).ToList()[0].FullName; } } #endregion var fileExcludes = new List <string>(); //var dirExcludes = new List<string>(); string thisFMInstallsBasePath = Config.GetFMInstallPathUnsafe(fm.Game); string fmInstalledPath = Path.Combine(thisFMInstallsBasePath, fm.InstalledDir); using (var archive = GetZipArchiveCharEnc(fileToUse.Name)) { int filesCount = archive.Entries.Count; if (fileToUse.DarkLoader) { for (int i = 0; i < filesCount; i++) { var entry = archive.Entries[i]; string fn = entry.FullName; if (!fn.ContainsDirSep()) { Directory.CreateDirectory(Path.Combine(fmInstalledPath, _darkSavesDir)); entry.ExtractToFile(Path.Combine(fmInstalledPath, _darkSavesDir, fn), overwrite: true); } else if (fm.Game == Game.SS2 && (_ss2SaveDirsInZipRegex.IsMatch(fn) || fn.PathStartsWithI(_ss2CurrentDirS))) { Directory.CreateDirectory(Path.Combine(fmInstalledPath, fn.Substring(0, fn.LastIndexOfDirSep()))); entry.ExtractToFile(Path.Combine(fmInstalledPath, fn), overwrite: true); } } } else { string savesDirS = fmIsT3 ? _t3SavesDirS : _darkSavesDirS; if (restoreSavesAndScreensOnly) { for (int i = 0; i < filesCount; i++) { var entry = archive.Entries[i]; string fn = entry.FullName; if (fn.Length > 0 && !fn[fn.Length - 1].IsDirSep() && (fn.PathStartsWithI(savesDirS) || fn.PathStartsWithI(_darkNetSavesDirS) || fn.PathStartsWithI(_screensDirS) || (fm.Game == Game.SS2 && (_ss2SaveDirsInZipRegex.IsMatch(fn) || fn.PathStartsWithI(_ss2CurrentDirS))))) { Directory.CreateDirectory(Path.Combine(fmInstalledPath, fn.Substring(0, fn.LastIndexOfDirSep()))); entry.ExtractToFile(Path.Combine(fmInstalledPath, fn), overwrite: true); } } } else { var fmSelInf = archive.GetEntry(Paths.FMSelInf); // Cap the length, cause... well, nobody's going to put a 500MB binary file named // fmsel.inf, but hey... // Null check required because GetEntry() can return null if (fmSelInf?.Length < ByteSize.MB * 10) { using var eo = fmSelInf.Open(); using var sr = new StreamReader(eo); string?line; while ((line = sr.ReadLine()) != null) { bool startsWithRemoveFile = line.StartsWithFast_NoNullChecks(_removeFileEq); bool startsWithRemoveDir = false; if (!startsWithRemoveFile) { startsWithRemoveDir = line.StartsWithFast_NoNullChecks(_removeDirEq); } if (!startsWithRemoveFile && !startsWithRemoveDir) { continue; } string val = line.Substring(startsWithRemoveFile ? 11 : 10).Trim(); if (!val.PathStartsWithI(savesDirS) && !val.PathStartsWithI(_darkNetSavesDirS) && !val.PathStartsWithI(_screensDirS) && (fm.Game != Game.SS2 || (!_ss2SaveDirsInZipRegex.IsMatch(val) && !val.PathStartsWithI(_ss2CurrentDirS))) && !val.PathEqualsI(Paths.FMSelInf) && !val.PathEqualsI(_startMisSav) && // Reject malformed and/or maliciously formed paths - we're going to // delete these files, and we don't want to delete anything outside // the FM folder !val.StartsWithDirSep() && !val.Contains(':') && // @DIRSEP: Critical: Check both / and \ here because we have no dirsep-agnostic string.Contains() !val.Contains("./") && !val.Contains(".\\")) { if (startsWithRemoveFile) { fileExcludes.Add(val); } //else //{ // dirExcludes.Add(val); //} } } } for (int i = 0; i < filesCount; i++) { var f = archive.Entries[i]; string fn = f.FullName; if (fn.PathEqualsI(Paths.FMSelInf) || fn.PathEqualsI(_startMisSav) || (fn.Length > 0 && fn[fn.Length - 1].IsDirSep()) || fileExcludes.PathContainsI(fn)) { continue; } if (fn.ContainsDirSep()) { Directory.CreateDirectory(Path.Combine(fmInstalledPath, fn.Substring(0, fn.LastIndexOfDirSep()))); } f.ExtractToFile(Path.Combine(fmInstalledPath, fn), overwrite: true); } } } } if (!restoreSavesAndScreensOnly) { foreach (string f in Directory.GetFiles(fmInstalledPath, "*", SearchOption.AllDirectories)) { if (fileExcludes.PathContainsI(f.Substring(fmInstalledPath.Length).Trim(CA_BS_FS))) { // TODO: Deleted dirs are not detected, they're detected as "delete every file in this dir" // If we have crf files replacing dirs, the empty dir will override the crf. We want // to store whether dirs were actually removed so we can remove them again. File.Delete(f); } } // Disabled till this is working completely #if false // Crappy hack method var crfs = Directory.GetFiles(fmInstalledPath, "*.crf", SearchOption.TopDirectoryOnly); var dirRemoveList = new List <string>(); foreach (string d in Directory.GetDirectories(fmInstalledPath, "*", SearchOption.TopDirectoryOnly)) { string dt = d.GetDirNameFast(); if (Directory.GetFiles(d, "*", SearchOption.AllDirectories).Length == 0) { for (int i = 0; i < crfs.Length; i++) { string ft = crfs[i].GetFileNameFast().RemoveExtension(); if (ft.PathEqualsI(dt)) { dirRemoveList.Add(d); } } } } if (dirRemoveList.Count > 0) { for (int i = 0; i < dirRemoveList.Count; i++) { Directory.Delete(dirRemoveList[i], recursive: true); } } // Proper method foreach (string d in Directory.GetDirectories(fmInstalledPath, "*", SearchOption.AllDirectories)) { if (dirExcludes.PathContainsI(d.Substring(fmInstalledPath.Length).Trim(CA_BS_FS))) { Directory.Delete(d, recursive: true); } } #endif } if (fileToUse.DarkLoader) { string dlOrigBakDir = Path.Combine(Config.FMsBackupPath, Paths.DarkLoaderSaveOrigBakDir); Directory.CreateDirectory(dlOrigBakDir); File.Move(fileToUse.Name, Path.Combine(dlOrigBakDir, fileToUse.Name.GetFileNameFast())); } }); }
// MT: On startup only, this is run in parallel with MainForm.ctor and .InitThreadable() // So don't touch anything the other touches: anything affecting the view. // @CAN_RUN_BEFORE_VIEW_INIT internal static void Find(bool startup = false) { if (!startup) { // Make sure we don't lose anything when we re-find! // NOTE: This also writes out TagsStrings and then reads them back in and syncs them with Tags. // Critical that that gets done. Ini.WriteFullFMDataIni(); // Do this every time we modify FMsViewList in realtime, to prevent FMsDGV from redrawing from // the list when it's in an indeterminate state (which can cause a selection change (bad) and/or // a visible change of the list (not really bad but unprofessional looking)). // MT: Don't do this on startup because we're running in parallel with the form new/init in that case. Core.View.SetRowCount(0); } // Init or reinit - must be deep-copied or changes propagate back because reference types // MT: This is thread-safe, the view ctor and InitThreadable() doesn't touch it. PresetTags.DeepCopyTo(GlobalTags); #region Back up lists and read FM data file // Copy FMs to backup lists before clearing, in case we can't read the ini file. We don't want to end // up with a blank or incomplete list and then glibly save it out later. var backupList = new List <FanMission>(FMDataIniList.Count); foreach (FanMission fm in FMDataIniList) { backupList.Add(fm); } var viewBackupList = new List <FanMission>(FMsViewList.Count); foreach (FanMission fm in FMsViewList) { viewBackupList.Add(fm); } FMDataIniList.Clear(); FMsViewList.Clear(); // Mark this false because this flag is only there as a perf hack to make SetFilter() not have to // iterate the FMs list for deletion markers unless it's actually going to find something. Our FMs' // deletion markers will disappear here implicitly because the objects are destroyed and new ones // created, but we need to explicitly set our perf-hack bool to false too. Core.OneOrMoreFMsAreMarkedDeleted = false; bool fmDataIniExists = File.Exists(Paths.FMDataIni); if (fmDataIniExists) { try { Ini.ReadFMDataIni(Paths.FMDataIni, FMDataIniList); } catch (Exception ex) { Log("Exception reading FM data ini", ex); if (startup) { // Language will be loaded by this point MessageBox.Show(LText.AlertMessages.FindFMs_ExceptionReadingFMDataIni, LText.AlertMessages.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); Core.EnvironmentExitDoShutdownTasks(1); } else { FMDataIniList.ClearAndAdd(backupList); FMsViewList.ClearAndAdd(viewBackupList); return; } } } #endregion #region Get installed dirs from disk // Could check inside the folder for a .mis file to confirm it's really an FM folder, but that's // horrendously expensive. Talking like eight seconds vs. < 4ms for the 1098 set. Weird. var perGameInstFMDirsList = new List <List <string> >(SupportedGameCount); var perGameInstFMDirsDatesList = new List <List <DateTime> >(SupportedGameCount); for (int gi = 0; gi < SupportedGameCount; gi++) { // NOTE! Make sure this list ends up with SupportedGameCount items in it. Just in case I change // the loop or something. perGameInstFMDirsList.Add(new List <string>()); perGameInstFMDirsDatesList.Add(new List <DateTime>()); string instPath = Config.FMInstallPaths[gi]; if (Directory.Exists(instPath)) { try { var dirs = FastIO.GetDirsTopOnly_FMs(instPath, "*", out List <DateTime> dateTimes); for (int di = 0; di < dirs.Count; di++) { string d = dirs[di]; if (!d.EqualsI(".fmsel.cache")) { perGameInstFMDirsList[gi].Add(d); perGameInstFMDirsDatesList[gi].Add(dateTimes[di]); } } } catch (Exception ex) { Log("Exception getting directories in " + instPath, ex); } } } #endregion #region Get archives from disk var fmArchives = new List <string>(); var fmArchivesDates = new List <DateTime>(); var archivePaths = GetFMArchivePaths(); bool onlyOnePath = archivePaths.Count == 1; for (int ai = 0; ai < archivePaths.Count; ai++) { try { // Returns filenames only (not full paths) var files = FastIO.GetFilesTopOnly_FMs(archivePaths[ai], "*", out List <DateTime> dateTimes); for (int fi = 0; fi < files.Count; fi++) { string f = files[fi]; // Only do .ContainsI() if we're searching multiple directories. Otherwise we're guaranteed // no duplicates and can avoid the expensive lookup. // @DIRSEP: These are filename only, no need for PathContainsI() if ((onlyOnePath || !fmArchives.ContainsI(f)) && f.ExtIsArchive() && !f.ContainsI(Paths.FMSelBak)) { fmArchives.Add(f); fmArchivesDates.Add(dateTimes[fi]); } } } catch (Exception ex) { Log("Exception getting files in " + archivePaths[ai], ex); } } #endregion #region Build FanMission objects from installed dirs var perGameFMsList = new List <List <FanMission> >(SupportedGameCount); for (int gi = 0; gi < SupportedGameCount; gi++) { // NOTE! List must have SupportedGameCount items in it perGameFMsList.Add(new List <FanMission>()); for (int di = 0; di < perGameInstFMDirsList[gi].Count; di++) { perGameFMsList[gi].Add(new FanMission { InstalledDir = perGameInstFMDirsList[gi][di], Game = GameIndexToGame((GameIndex)gi), Installed = true }); } } #endregion MergeNewArchiveFMs(fmArchives, fmArchivesDates); int instInitCount = FMDataIniList.Count; for (int i = 0; i < SupportedGameCount; i++) { var curGameInstFMsList = perGameFMsList[i]; if (curGameInstFMsList.Count > 0) { MergeNewInstalledFMs(curGameInstFMsList, perGameInstFMDirsDatesList[i], instInitCount); } } SetArchiveNames(fmArchives); SetInstalledNames(); BuildViewList(fmArchives, perGameInstFMDirsList); /* * TODO: There's an extreme corner case where duplicate FMs can appear in the list * It's so unlikely it's almost not worth worrying about, but here's the scenario: * -The FM is installed by hand and not truncated * -The FM is not in the list * -A matching archive exists for the FM * In this scenario, the FM is added twice to the list, once with the full installed folder name and * NoArchive set to true, and once with a truncated installed dir name, the correct archive name, and * NoArchive not present (false). * The code in here is so crazy-go-nuts I can't even find where this is happening. But putting this * note down for the future. */ }
// MT: On startup only, this is run in parallel with MainForm.ctor and .InitThreadable() // So don't touch anything the other touches: anything affecting the view. // @CAN_RUN_BEFORE_VIEW_INIT private static List <int> FindInternal(bool startup) { if (!startup) { // Make sure we don't lose anything when we re-find! // NOTE: This also writes out TagsStrings and then reads them back in and syncs them with Tags. // Critical that that gets done. Ini.WriteFullFMDataIni(); // Do this every time we modify FMsViewList in realtime, to prevent FMsDGV from redrawing from // the list when it's in an indeterminate state (which can cause a selection change (bad) and/or // a visible change of the list (not really bad but unprofessional looking)). // MT: Don't do this on startup because we're running in parallel with the form new/init in that case. Core.View.SetRowCount(0); } // Init or reinit - must be deep-copied or changes propagate back because reference types // MT: This is thread-safe, the view ctor and InitThreadable() doesn't touch it. PresetTags.DeepCopyTo(GlobalTags); #region Back up lists and read FM data file // Copy FMs to backup lists before clearing, in case we can't read the ini file. We don't want to end // up with a blank or incomplete list and then glibly save it out later. FanMission[] backupList = new FanMission[FMDataIniList.Count]; FMDataIniList.CopyTo(backupList); FanMission[] viewBackupList = new FanMission[FMsViewList.Count]; FMsViewList.CopyTo(viewBackupList); FMDataIniList.Clear(); FMsViewList.Clear(); bool fmDataIniExists = File.Exists(Paths.FMDataIni); if (fmDataIniExists) { try { Ini.ReadFMDataIni(Paths.FMDataIni, FMDataIniList); } catch (Exception ex) { Log("Exception reading FM data ini", ex); if (startup) { // Language will be loaded by this point Dialogs.ShowError(LText.AlertMessages.FindFMs_ExceptionReadingFMDataIni); Core.EnvironmentExitDoShutdownTasks(1); } else { FMDataIniList.ClearAndAdd(backupList); FMsViewList.ClearAndAdd(viewBackupList); return(new List <int>()); } } } #endregion #region Get installed dirs from disk // Could check inside the folder for a .mis file to confirm it's really an FM folder, but that's // horrendously expensive. Talking like eight seconds vs. < 4ms for the 1098 set. Weird. var perGameInstFMDirsList = new List <List <string> >(SupportedGameCount); var perGameInstFMDirsDatesList = new List <List <DateTime> >(SupportedGameCount); for (int gi = 0; gi < SupportedGameCount; gi++) { // NOTE! Make sure this list ends up with SupportedGameCount items in it. Just in case I change // the loop or something. perGameInstFMDirsList.Add(new List <string>()); perGameInstFMDirsDatesList.Add(new List <DateTime>()); string instPath = Config.GetFMInstallPath((GameIndex)gi); if (Directory.Exists(instPath)) { try { var dirs = FastIO.GetDirsTopOnly_FMs(instPath, "*", out List <DateTime> dateTimes); for (int di = 0; di < dirs.Count; di++) { string d = dirs[di]; if (!d.EqualsI(Paths.FMSelCache)) { perGameInstFMDirsList[gi].Add(d); perGameInstFMDirsDatesList[gi].Add(dateTimes[di]); } } } catch (Exception ex) { Log("Exception getting directories in " + instPath, ex); } } } #endregion #region Get archives from disk var fmArchivesAndDatesDict = new DictionaryI <DateTime>(); var archivePaths = FMArchives.GetFMArchivePaths(); bool onlyOnePath = archivePaths.Count == 1; for (int ai = 0; ai < archivePaths.Count; ai++) { try { // Returns filenames only (not full paths) var files = FastIO.GetFilesTopOnly_FMs(archivePaths[ai], "*", out List <DateTime> dateTimes); for (int fi = 0; fi < files.Count; fi++) { string f = files[fi]; // Do this first because it should be faster than a dictionary lookup if (!f.ExtIsArchive()) { continue; } // NOTE: We do a ContainsKey check to keep behavior the same as previously. When we use // dict[key] = value, it _replaces_ the value with the new one every time. What we want // is for it to just not touch it at all if the key is already in there. This check does // technically slow it down some, but the actual perf degradation is negligible. And we // still avoid the n-squared 1.6-million-call nightmare we get with ~1600 FMs in the list. // Nevertheless, we can avoid even this small extra cost if we only have one FM archive // path, so no harm in keeping the only-one-path check. if ((onlyOnePath || !fmArchivesAndDatesDict.ContainsKey(f)) && // @DIRSEP: These are filename only, no need for PathContainsI() !f.ContainsI(Paths.FMSelBak)) { fmArchivesAndDatesDict[f] = dateTimes[fi]; } } } catch (Exception ex) { Log("Exception getting files in " + archivePaths[ai], ex); } } int fmArchivesAndDatesDictLen = fmArchivesAndDatesDict.Count; // PERF_TODO: May want to keep these as dicts later or change other vars to dicts string[] fmArchives = new string[fmArchivesAndDatesDictLen]; DateTime[] fmArchivesDates = new DateTime[fmArchivesAndDatesDictLen]; { int i = 0; foreach (var item in fmArchivesAndDatesDict) { fmArchives[i] = item.Key; fmArchivesDates[i] = item.Value; i++; } } #endregion #region Build FanMission objects from installed dirs var perGameFMsList = new List <List <FanMission> >(SupportedGameCount); for (int gi = 0; gi < SupportedGameCount; gi++) { // NOTE! List must have SupportedGameCount items in it perGameFMsList.Add(new List <FanMission>()); for (int di = 0; di < perGameInstFMDirsList[gi].Count; di++) { perGameFMsList[gi].Add(new FanMission { InstalledDir = perGameInstFMDirsList[gi][di], Game = GameIndexToGame((GameIndex)gi), Installed = true }); } } #endregion MergeNewArchiveFMs(fmArchives, fmArchivesDates); int instInitCount = FMDataIniList.Count; for (int i = 0; i < SupportedGameCount; i++) { var curGameInstFMsList = perGameFMsList[i]; if (curGameInstFMsList.Count > 0) { MergeNewInstalledFMs(curGameInstFMsList, perGameInstFMDirsDatesList[i], instInitCount); } } SetArchiveNames(fmArchives); SetInstalledNames(); // Super quick-n-cheap hack for perf: So we don't have to iterate the whole list looking for unscanned // FMs. This will contain indexes into FMDataIniList (not FMsViewList!) var fmsViewListUnscanned = new List <int>(FMDataIniList.Count); BuildViewList(fmArchives, perGameInstFMDirsList, fmsViewListUnscanned); return(fmsViewListUnscanned); /* * TODO: There's an extreme corner case where duplicate FMs can appear in the list * It's so unlikely it's almost not worth worrying about, but here's the scenario: * -The FM is installed by hand and not truncated * -The FM is not in the list * -A matching archive exists for the FM * In this scenario, the FM is added twice to the list, once with the full installed folder name and * NoArchive set to true, and once with a truncated installed dir name, the correct archive name, and * NoArchive not present (false). * The code in here is so crazy-go-nuts I can't even find where this is happening. But putting this * note down for the future. */ }
private static async Task <CacheData> GetCacheableDataInFMCacheDir(FanMission fm, bool refreshCache) { var readmes = new List <string>(); AssertR(!fm.InstalledDir.IsEmpty(), "fm.InstalledFolderName is null or empty"); string fmCachePath = Path.Combine(Paths.FMsCache, fm.InstalledDir); if (Directory.Exists(fmCachePath)) { foreach (string fn in FastIO.GetFilesTopOnly(fmCachePath, "*")) { if (fn.IsValidReadme() && new FileInfo(fn).Length > 0) { readmes.Add(fn.Substring(fmCachePath.Length + 1)); } } for (int i = 0; i < 2; i++) { string t3ReadmePath = Path.Combine(fmCachePath, i == 0 ? Paths.T3ReadmeDir1 : Paths.T3ReadmeDir2); if (Directory.Exists(t3ReadmePath)) { foreach (string fn in FastIO.GetFilesTopOnly(t3ReadmePath, "*")) { if (fn.IsValidReadme() && new FileInfo(fn).Length > 0) { readmes.Add(fn.Substring(fmCachePath.Length + 1)); } } } } bool checkArchive = refreshCache || (readmes.Count == 0 && !fm.NoReadmes); if (!checkArchive) { return(new CacheData(readmes)); } } // If cache dir DOESN'T exist, the above checkArchive decision won't be run, so run it here (prevents // FMs with no readmes from being reloaded from their archive all the time, which is the whole purpose // of NoReadmes in the first place). if (!refreshCache && fm.NoReadmes) { return(new CacheData()); } readmes.Clear(); ClearCacheDir(fm); string fmArchivePath = FindFMArchive(fm.Archive); // In weird situations this could be true, so just say none and at least don't crash if (fmArchivePath.IsEmpty()) { return(new CacheData()); } if (fm.Archive.ExtIsZip()) { ZipExtract(fmArchivePath, fmCachePath, readmes); // TODO: Support HTML ref extraction for .7z files too // Will require full extract for the same reason scan does - we need to scan files to know what // other files to scan, etc. and a full extract is with 99.9999% certainty going to be faster // than chugging through the whole thing over and over and over for each new file we find we need // Guard check so we don't do useless HTML work if we don't have any HTML readmes bool htmlReadmeExists = false; for (int i = 0; i < readmes.Count; i++) { if (readmes[i].ExtIsHtml()) { htmlReadmeExists = true; break; } } if (htmlReadmeExists && Directory.Exists(fmCachePath)) { try { ExtractHTMLRefFiles(fmArchivePath, fmCachePath); } catch (Exception ex) { Log("Exception in " + nameof(ExtractHTMLRefFiles), ex); } } } else { await SevenZipExtract(fmArchivePath, fmCachePath, readmes); } fm.NoReadmes = readmes.Count == 0; return(new CacheData(readmes)); }