Пример #1
0
        internal static bool RemoveTagFromFM(FanMission fm, string catText, string tagText)
        {
            if (tagText.IsEmpty())
            {
                return(false);
            }

            // Parent node (category)
            if (catText.IsEmpty())
            {
                // TODO: These messageboxes are annoying, but they prevent accidental deletion.
                // Figure out something better.
                bool cont = Core.View.AskToContinue(LText.TagsTab.AskRemoveCategory, LText.TagsTab.TabText, true);
                if (!cont)
                {
                    return(false);
                }

                CatAndTags?cat = fm.Tags.FirstOrDefault(x => x.Category == tagText);
                if (cat != null)
                {
                    fm.Tags.Remove(cat);
                    UpdateFMTagsString(fm);

                    // TODO: Profile the FirstOrDefaults and see if I should make them for loops
                    GlobalCatAndTags?globalCat = GlobalTags.FirstOrDefault(x => x.Category.Name == cat.Category);
                    if (globalCat != null && !globalCat.Category.IsPreset)
                    {
                        if (globalCat.Category.UsedCount > 0)
                        {
                            globalCat.Category.UsedCount--;
                        }
                        if (globalCat.Category.UsedCount == 0)
                        {
                            GlobalTags.Remove(globalCat);
                        }
                    }
                }
            }
            // Child node (tag)
            else
            {
                bool cont = Core.View.AskToContinue(LText.TagsTab.AskRemoveTag, LText.TagsTab.TabText, true);
                if (!cont)
                {
                    return(false);
                }

                CatAndTags?cat = fm.Tags.FirstOrDefault(x => x.Category == catText);
                string?    tag = cat?.Tags.FirstOrDefault(x => x == tagText);
                if (tag != null)
                {
                    cat !.Tags.Remove(tag);
                    if (cat.Tags.Count == 0)
                    {
                        fm.Tags.Remove(cat);
                    }
                    UpdateFMTagsString(fm);

                    GlobalCatAndTags?globalCat = GlobalTags.FirstOrDefault(x => x.Category.Name == cat.Category);
                    GlobalCatOrTag?  globalTag = globalCat?.Tags.FirstOrDefault(x => x.Name == tagText);
                    if (globalTag != null && !globalTag.IsPreset)
                    {
                        if (globalTag.UsedCount > 0)
                        {
                            globalTag.UsedCount--;
                        }
                        if (globalTag.UsedCount == 0)
                        {
                            globalCat !.Tags.Remove(globalTag);
                        }
                        if (globalCat !.Tags.Count == 0)
                        {
                            GlobalTags.Remove(globalCat);
                        }
                    }
                }
            }

            Ini.WriteFullFMDataIni();

            return(true);
        }
Пример #2
0
 internal static void AddTagToFM(FanMission fm, string catAndTag)
 {
     AddTagsToFMAndGlobalList(catAndTag, fm.Tags);
     UpdateFMTagsString(fm);
     Ini.WriteFullFMDataIni();
 }
Пример #3
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.
             */
        }
Пример #4
0
        internal static async Task <bool> InstallFM(FanMission fm)
        {
            #region Checks

            AssertR(!fm.Installed, "fm.Installed == false");

            if (!GameIsKnownAndSupported(fm.Game))
            {
                Log("FM game type is unknown or unsupported.\r\n" +
                    "FM: " + (!fm.Archive.IsEmpty() ? fm.Archive : fm.InstalledDir) + "\r\n" +
                    "FM game was: " + fm.Game);
                Dialogs.ShowError(ErrorText.FMGameTypeUnknownOrUnsupported);
                return(false);
            }

            GameIndex gameIndex = GameToGameIndex(fm.Game);

            string fmArchivePath = FMArchives.FindFirstMatch(fm.Archive);

            if (fmArchivePath.IsEmpty())
            {
                Log("FM archive field was empty; this means an archive was not found for it on the last search.\r\n" +
                    "FM: " + (!fm.Archive.IsEmpty() ? fm.Archive : fm.InstalledDir) + "\r\n" +
                    "FM game was: " + fm.Game);
                Dialogs.ShowError(LText.AlertMessages.Install_ArchiveNotFound);
                return(false);
            }

            string gameExe  = Config.GetGameExe(gameIndex);
            string gameName = GetLocalizedGameName(gameIndex);
            if (!File.Exists(gameExe))
            {
                Log("Game executable not found.\r\n" +
                    "Game executable: " + gameExe);
                Dialogs.ShowError(gameName + ":\r\n" +
                                  LText.AlertMessages.Install_ExecutableNotFound);
                return(false);
            }

            string instBasePath = Config.GetFMInstallPath(gameIndex);

            if (!Directory.Exists(instBasePath))
            {
                Log("FM install path not found.\r\n" +
                    "FM: " + (!fm.Archive.IsEmpty() ? fm.Archive : fm.InstalledDir) + "\r\n" +
                    "FM game was: " + fm.Game + "\r\n" +
                    "FM install path: " + instBasePath
                    );
                Dialogs.ShowError(gameName + ":\r\n" +
                                  LText.AlertMessages.Install_FMInstallPathNotFound);
                return(false);
            }

            if (GameIsRunning(gameExe))
            {
                Dialogs.ShowAlert(gameName + ":\r\n" +
                                  LText.AlertMessages.Install_GameIsRunning, LText.AlertMessages.Alert);
                return(false);
            }

            #endregion

            string fmInstalledPath = Path.Combine(instBasePath, fm.InstalledDir);

            _extractCts = new CancellationTokenSource();

            Core.View.ShowProgressBox(ProgressTask.InstallFM);

            // Framework zip extracting is much faster, so use it if possible
            bool canceled = !await(fmArchivePath.ExtIsZip()
                ? Task.Run(() => InstallFMZip(fmArchivePath, fmInstalledPath))
                : Task.Run(() => InstallFMSevenZip(fmArchivePath, fmInstalledPath)));

            if (canceled)
            {
                Core.View.SetCancelingFMInstall();
                await Task.Run(() =>
                {
                    try
                    {
                        Directory.Delete(fmInstalledPath, recursive: true);
                    }
                    catch (Exception ex)
                    {
                        // @BetterErrors(InstallFM() - install cancellation (folder deletion) failed)
                        Log("Unable to delete FM installed directory " + fmInstalledPath, ex);
                    }
                });

                Core.View.HideProgressBox();
                return(false);
            }

            fm.Installed = true;

            Ini.WriteFullFMDataIni();

            try
            {
                using var sw = new StreamWriter(Path.Combine(fmInstalledPath, Paths.FMSelInf), append: false);
                await sw.WriteLineAsync("Name=" + fm.InstalledDir);

                await sw.WriteLineAsync("Archive=" + fm.Archive);
            }
            catch (Exception ex)
            {
                Log("Couldn't create " + Paths.FMSelInf + " in " + fmInstalledPath, ex);
            }

            // Only Dark engine games need audio conversion
            if (GameIsDark(gameIndex))
            {
                try
                {
                    Core.View.ShowProgressBox(ProgressTask.ConvertFiles);

                    // Dark engine games can't play MP3s, so they must be converted in all cases.
                    // This one won't be called anywhere except during install, because it always runs during
                    // install so there's no need to make it optional elsewhere. So we don't need to have a
                    // check bool or anything.
                    await FMAudio.ConvertToWAVs(fm, AudioConvert.MP3ToWAV, false);

                    if (Config.ConvertOGGsToWAVsOnInstall)
                    {
                        await FMAudio.ConvertToWAVs(fm, AudioConvert.OGGToWAV, false);
                    }
                    if (Config.ConvertWAVsTo16BitOnInstall)
                    {
                        await FMAudio.ConvertToWAVs(fm, AudioConvert.WAVToWAV16, false);
                    }
                }
                catch (Exception ex)
                {
                    Log("Exception in audio conversion", ex);
                }
            }

            // Don't be lazy about this; there can be no harm and only benefits by doing it right away
            GenerateMissFlagFileIfRequired(fm);

            // TODO: Put up a "Restoring saves and screenshots" box here to avoid the "converting files" one lasting beyond its time?
            try
            {
                await RestoreFM(fm);
            }
            catch (Exception ex)
            {
                Log(ex: ex);
            }
            finally
            {
                Core.View.HideProgressBox();
            }

            // Not doing RefreshFM(rowOnly: true) because that wouldn't update the install/uninstall buttons
            Core.View.RefreshFM(fm);

            return(true);
        }
Пример #5
0
        internal static async Task InstallIfNeededAndPlay(FanMission fm, bool askConfIfRequired = false, bool playMP = false)
        {
            if (!GameIsKnownAndSupported(fm.Game))
            {
                Log("Game is unknown or unsupported for FM " + (!fm.Archive.IsEmpty() ? fm.Archive : fm.InstalledDir) + "\r\n" +
                    "fm.Game was: " + fm.Game, stackTrace: true);
                Dialogs.ShowError(ErrorText.FMGameTypeUnknownOrUnsupported);
                return;
            }

            GameIndex gameIndex = GameToGameIndex(fm.Game);

            if (playMP && gameIndex != GameIndex.Thief2)
            {
                Log("playMP was true, but fm.Game was not Thief 2.\r\n" +
                    "fm: " + (!fm.Archive.IsEmpty() ? fm.Archive : fm.InstalledDir) + "\r\n" +
                    "fm.Game was: " + fm.Game, stackTrace: true);
                Dialogs.ShowError(ErrorText.MultiplayerForNonThief2);
                return;
            }

            if (askConfIfRequired && Config.ConfirmPlayOnDCOrEnter)
            {
                string message = fm.Installed
                    ? LText.AlertMessages.Play_ConfirmMessage
                    : LText.AlertMessages.Play_InstallAndPlayConfirmMessage;

                if (Core.View.GetSelectedFMOrNull() != fm)
                {
                    message += "\r\n\r\n" +
                               fm.Archive + "\r\n" +
                               fm.Title + "\r\n" +
                               fm.Author + "\r\n";
                }

                (bool cancel, bool dontAskAgain) = Dialogs.AskToContinueYesNoCustomStrings(
                    message: message,
                    title: LText.AlertMessages.Confirm,
                    icon: MessageBoxIcon.None,
                    showDontAskAgain: true,
                    yes: LText.Global.Yes,
                    no: LText.Global.No);

                if (cancel)
                {
                    return;
                }

                Config.ConfirmPlayOnDCOrEnter = !dontAskAgain;
            }

            if (!fm.Installed && !await InstallFM(fm))
            {
                return;
            }

            if (playMP && gameIndex == GameIndex.Thief2 && Config.GetT2MultiplayerExe_FromDisk().IsEmpty())
            {
                Log("Thief2MP.exe not found in Thief 2 game directory.\r\n" +
                    "Thief 2 game directory: " + Config.GetGamePath(GameIndex.Thief2));
                Dialogs.ShowError(LText.AlertMessages.Thief2_Multiplayer_ExecutableNotFound);
                return;
            }

            if (PlayFM(fm, playMP))
            {
                fm.LastPlayed.DateTime = DateTime.Now;
                Core.View.RefreshFM(fm);
                Ini.WriteFullFMDataIni();
            }
        }
Пример #6
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.
             */
        }