Пример #1
0
        // @BigO(FindFMs.SetArchiveNames())
        private static void SetArchiveNames(string[] fmArchives)
        {
            // Attempt to set archive names for newly found installed FMs (best effort search)
            for (int i = 0; i < FMDataIniList.Count; i++)
            {
                FanMission fm = FMDataIniList[i];

                if (fm.Archive.IsEmpty())
                {
                    if (fm.InstalledDir.IsEmpty())
                    {
                        FMDataIniList.RemoveAt(i);
                        i--;
                        continue;
                    }

                    // PERF_TODO: Should we keep null here because it's faster? Is it faster? (tight loop)
                    string?archiveName = null;
                    // Skip the expensive archive name search if we're marked as having no archive
                    if (!fm.NoArchive)
                    {
                        // @BigO(FindFMs.SetArchiveNames()/GetArchiveNamesFromInstalledDir():
                        // This one potentially does multiple searches through large lists for archive / inst /
                        // inst-truncated / etc. and we're in a loop here already.
                        archiveName = GetArchiveNameFromInstalledDir(fm, fmArchives);
                    }
                    if (archiveName.IsEmpty())
                    {
                        continue;
                    }

                    // Exponential (slow) stuff, but we only do it once to correct the list and then never again
                    // NOTE: I guess this removes duplicates, which is why it has to do the search?
                    FanMission?existingFM = FMDataIniList.Find(x => x.Archive.EqualsI(archiveName));
                    if (existingFM != null)
                    {
                        existingFM.InstalledDir = fm.InstalledDir;
                        existingFM.Installed    = true;
                        existingFM.Game         = fm.Game;
                        existingFM.DateAdded ??= fm.DateAdded;
                        FMDataIniList.RemoveAt(i);
                        i--;
                    }
                    else
                    {
                        fm.Archive = archiveName;
                        if (fm.NoArchive)
                        {
                            string?fmselInf = GetFMSelInfPath(fm);
                            if (!fmselInf.IsEmpty())
                            {
                                WriteFMSelInf(fm, fmselInf, archiveName);
                            }
                        }
                        fm.NoArchive = false;
                    }
                }
            }
        }
Пример #2
0
        // @BigO(FindFMs.SetInstalledNames())
        private static void SetInstalledNames()
        {
            // Fill in empty installed dir names, making sure to check for and handle truncated name collisions
            foreach (FanMission fm in FMDataIniList)
            {
                if (fm.InstalledDir.IsEmpty())
                {
                    bool   truncate = fm.Game != Game.Thief3;
                    string instDir  = fm.Archive.ToInstDirNameFMSel(truncate);
                    int    i        = 0;

                    // Again, an exponential search, but again, we only do it once to correct the list and then
                    // never again
                    while (FMDataIniList.Any(x => x.InstalledDir.EqualsI(instDir)))
                    {
                        // Yeah, this'll never happen, but hey
                        if (i > 999)
                        {
                            break;
                        }

                        // Conform to FMSel's numbering format
                        string append = "(" + (i + 2).ToString() + ")";

                        if (truncate && instDir.Length + append.Length > 30)
                        {
                            instDir = instDir.Substring(0, 30 - append.Length);
                        }
                        instDir += append;

                        i++;
                    }

                    // If it overflowed, oh well. You get what you deserve in that case.
                    fm.InstalledDir = instDir;
                }
            }
        }
Пример #3
0
        MergeImportedFMData(ImportType importType, List <FanMission> importedFMs, FieldsToImport?fields = null
                            /*, bool addMergedFMsToPriorityList = false*/)
        {
            fields ??= new FieldsToImport
            {
                Title          = true,
                ReleaseDate    = true,
                LastPlayed     = true,
                FinishedOn     = true,
                Comment        = true,
                Rating         = true,
                DisabledMods   = true,
                Tags           = true,
                SelectedReadme = true,
                Size           = true
            };

            // Perf
            int initCount = FMDataIniList.Count;

            bool[] checkedArray = new bool[initCount];

            // We can't just send back the list we got in, because we will have deep-copied them to the main list
            var importedFMsInMainList = new List <FanMission>();

            for (int impFMi = 0; impFMi < importedFMs.Count; impFMi++)
            {
                var importedFM = importedFMs[impFMi];

                bool existingFound = false;
                for (int mainFMi = 0; mainFMi < initCount; mainFMi++)
                {
                    var mainFM = FMDataIniList[mainFMi];

                    if (!checkedArray[mainFMi] &&
                        (importType == ImportType.DarkLoader &&
                         mainFM.Archive.EqualsI(importedFM.Archive)) ||
                        (importType == ImportType.FMSel &&
                         (!importedFM.Archive.IsEmpty() && mainFM.Archive.EqualsI(importedFM.Archive)) ||
                         importedFM.InstalledDir.EqualsI(mainFM.InstalledDir)) ||
                        (importType == ImportType.NewDarkLoader &&
                         mainFM.InstalledDir.EqualsI(importedFM.InstalledDir)))
                    {
                        var priorityFMData = new FanMission();

                        if (fields.Title && !importedFM.Title.IsEmpty())
                        {
                            mainFM.Title         = importedFM.Title;
                            priorityFMData.Title = importedFM.Title;
                        }
                        if (fields.ReleaseDate && importedFM.ReleaseDate.DateTime != null)
                        {
                            mainFM.ReleaseDate.DateTime         = importedFM.ReleaseDate.DateTime;
                            priorityFMData.ReleaseDate.DateTime = importedFM.ReleaseDate.DateTime;
                        }
                        if (fields.LastPlayed)
                        {
                            mainFM.LastPlayed.DateTime         = importedFM.LastPlayed.DateTime;
                            priorityFMData.LastPlayed.DateTime = importedFM.LastPlayed.DateTime;
                        }
                        if (fields.FinishedOn)
                        {
                            mainFM.FinishedOn         = importedFM.FinishedOn;
                            priorityFMData.FinishedOn = importedFM.FinishedOn;
                            if (importType != ImportType.FMSel)
                            {
                                mainFM.FinishedOnUnknown         = false;
                                priorityFMData.FinishedOnUnknown = false;
                            }
                        }
                        if (fields.Comment)
                        {
                            mainFM.Comment         = importedFM.Comment;
                            priorityFMData.Comment = importedFM.Comment;
                        }

                        if (importType == ImportType.NewDarkLoader ||
                            importType == ImportType.FMSel)
                        {
                            if (fields.Rating)
                            {
                                mainFM.Rating         = importedFM.Rating;
                                priorityFMData.Rating = importedFM.Rating;
                            }
                            if (fields.DisabledMods)
                            {
                                mainFM.DisabledMods           = importedFM.DisabledMods;
                                priorityFMData.DisabledMods   = importedFM.DisabledMods;
                                mainFM.DisableAllMods         = importedFM.DisableAllMods;
                                priorityFMData.DisableAllMods = importedFM.DisableAllMods;
                            }
                            if (fields.Tags)
                            {
                                mainFM.TagsString         = importedFM.TagsString;
                                priorityFMData.TagsString = importedFM.TagsString;
                            }
                            if (fields.SelectedReadme)
                            {
                                mainFM.SelectedReadme         = importedFM.SelectedReadme;
                                priorityFMData.SelectedReadme = importedFM.SelectedReadme;
                            }
                        }
                        if (importType == ImportType.NewDarkLoader || importType == ImportType.DarkLoader)
                        {
                            if (fields.Size && mainFM.SizeBytes == 0)
                            {
                                mainFM.SizeBytes         = importedFM.SizeBytes;
                                priorityFMData.SizeBytes = importedFM.SizeBytes;
                            }
                        }
                        else if (importType == ImportType.FMSel && mainFM.FinishedOn == 0 && !mainFM.FinishedOnUnknown)
                        {
                            if (fields.FinishedOn)
                            {
                                mainFM.FinishedOnUnknown         = importedFM.FinishedOnUnknown;
                                priorityFMData.FinishedOnUnknown = importedFM.FinishedOnUnknown;
                            }
                        }

                        mainFM.MarkedScanned = true;

                        checkedArray[mainFMi] = true;

                        importedFMsInMainList.Add(mainFM);

                        //PriorityAdd(mainFM, priorityFMData, importType, fields);

                        existingFound = true;
                        break;
                    }
                }
                if (!existingFound)
                {
                    var newFM = new FanMission
                    {
                        Archive      = importedFM.Archive,
                        InstalledDir = importedFM.InstalledDir
                    };

                    var priorityFMData = new FanMission();

                    if (fields.Title)
                    {
                        newFM.Title = !importedFM.Title.IsEmpty() ? importedFM.Title :
                                      !importedFM.Archive.IsEmpty() ? importedFM.Archive.RemoveExtension() :
                                      importedFM.InstalledDir;
                        priorityFMData.Title = !importedFM.Title.IsEmpty() ? importedFM.Title :
                                               !importedFM.Archive.IsEmpty() ? importedFM.Archive.RemoveExtension() :
                                               importedFM.InstalledDir;
                    }
                    if (fields.ReleaseDate)
                    {
                        newFM.ReleaseDate.DateTime          = importedFM.ReleaseDate.DateTime;
                        priorityFMData.ReleaseDate.DateTime = importedFM.ReleaseDate.DateTime;
                    }
                    if (fields.LastPlayed)
                    {
                        newFM.LastPlayed.DateTime          = importedFM.LastPlayed.DateTime;
                        priorityFMData.LastPlayed.DateTime = importedFM.LastPlayed.DateTime;
                    }
                    if (fields.Comment)
                    {
                        newFM.Comment          = importedFM.Comment;
                        priorityFMData.Comment = importedFM.Comment;
                    }

                    if (importType == ImportType.NewDarkLoader ||
                        importType == ImportType.FMSel)
                    {
                        if (fields.Rating)
                        {
                            newFM.Rating          = importedFM.Rating;
                            priorityFMData.Rating = importedFM.Rating;
                        }
                        if (fields.DisabledMods)
                        {
                            newFM.DisabledMods            = importedFM.DisabledMods;
                            priorityFMData.DisabledMods   = importedFM.DisabledMods;
                            newFM.DisableAllMods          = importedFM.DisableAllMods;
                            priorityFMData.DisableAllMods = importedFM.DisableAllMods;
                        }
                        if (fields.Tags)
                        {
                            newFM.TagsString          = importedFM.TagsString;
                            priorityFMData.TagsString = importedFM.TagsString;
                        }
                        if (fields.SelectedReadme)
                        {
                            newFM.SelectedReadme          = importedFM.SelectedReadme;
                            priorityFMData.SelectedReadme = importedFM.SelectedReadme;
                        }
                    }
                    if (importType == ImportType.NewDarkLoader || importType == ImportType.DarkLoader)
                    {
                        if (fields.Size)
                        {
                            newFM.SizeBytes          = importedFM.SizeBytes;
                            priorityFMData.SizeBytes = importedFM.SizeBytes;
                        }
                        if (fields.FinishedOn)
                        {
                            newFM.FinishedOn          = importedFM.FinishedOn;
                            priorityFMData.FinishedOn = importedFM.FinishedOn;
                        }
                    }
                    else if (importType == ImportType.FMSel)
                    {
                        if (fields.FinishedOn)
                        {
                            newFM.FinishedOnUnknown          = importedFM.FinishedOnUnknown;
                            priorityFMData.FinishedOnUnknown = importedFM.FinishedOnUnknown;
                        }
                    }

                    newFM.MarkedScanned = true;

                    FMDataIniList.Add(newFM);
                    importedFMsInMainList.Add(newFM);

                    //PriorityAdd(newFM, priorityFMData, importType, fields);
                }
            }

            return(importedFMsInMainList);
        }
Пример #4
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.
             */
        }
Пример #5
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.
             */
        }