Ejemplo n.º 1
0
        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));
        }
Ejemplo n.º 2
0
        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));
        }
Ejemplo n.º 3
0
        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);
                }
            }
        }
Ejemplo n.º 4
0
        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);
                }
            }
        }
Ejemplo n.º 5
0
        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);
                }
            });
        }
Ejemplo n.º 6
0
        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()));
                }
            });
        }
Ejemplo n.º 7
0
        // 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.
             */
        }
Ejemplo n.º 8
0
        // 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.
             */
        }
Ejemplo n.º 9
0
        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));
        }