// @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; } } } }
// @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; } } }
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); }
// MT: On startup only, this is run in parallel with MainForm.ctor and .InitThreadable() // So don't touch anything the other touches: anything affecting the view. // @CAN_RUN_BEFORE_VIEW_INIT internal static void Find(bool startup = false) { if (!startup) { // Make sure we don't lose anything when we re-find! // NOTE: This also writes out TagsStrings and then reads them back in and syncs them with Tags. // Critical that that gets done. Ini.WriteFullFMDataIni(); // Do this every time we modify FMsViewList in realtime, to prevent FMsDGV from redrawing from // the list when it's in an indeterminate state (which can cause a selection change (bad) and/or // a visible change of the list (not really bad but unprofessional looking)). // MT: Don't do this on startup because we're running in parallel with the form new/init in that case. Core.View.SetRowCount(0); } // Init or reinit - must be deep-copied or changes propagate back because reference types // MT: This is thread-safe, the view ctor and InitThreadable() doesn't touch it. PresetTags.DeepCopyTo(GlobalTags); #region Back up lists and read FM data file // Copy FMs to backup lists before clearing, in case we can't read the ini file. We don't want to end // up with a blank or incomplete list and then glibly save it out later. var backupList = new List <FanMission>(FMDataIniList.Count); foreach (FanMission fm in FMDataIniList) { backupList.Add(fm); } var viewBackupList = new List <FanMission>(FMsViewList.Count); foreach (FanMission fm in FMsViewList) { viewBackupList.Add(fm); } FMDataIniList.Clear(); FMsViewList.Clear(); // Mark this false because this flag is only there as a perf hack to make SetFilter() not have to // iterate the FMs list for deletion markers unless it's actually going to find something. Our FMs' // deletion markers will disappear here implicitly because the objects are destroyed and new ones // created, but we need to explicitly set our perf-hack bool to false too. Core.OneOrMoreFMsAreMarkedDeleted = false; bool fmDataIniExists = File.Exists(Paths.FMDataIni); if (fmDataIniExists) { try { Ini.ReadFMDataIni(Paths.FMDataIni, FMDataIniList); } catch (Exception ex) { Log("Exception reading FM data ini", ex); if (startup) { // Language will be loaded by this point MessageBox.Show(LText.AlertMessages.FindFMs_ExceptionReadingFMDataIni, LText.AlertMessages.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); Core.EnvironmentExitDoShutdownTasks(1); } else { FMDataIniList.ClearAndAdd(backupList); FMsViewList.ClearAndAdd(viewBackupList); return; } } } #endregion #region Get installed dirs from disk // Could check inside the folder for a .mis file to confirm it's really an FM folder, but that's // horrendously expensive. Talking like eight seconds vs. < 4ms for the 1098 set. Weird. var perGameInstFMDirsList = new List <List <string> >(SupportedGameCount); var perGameInstFMDirsDatesList = new List <List <DateTime> >(SupportedGameCount); for (int gi = 0; gi < SupportedGameCount; gi++) { // NOTE! Make sure this list ends up with SupportedGameCount items in it. Just in case I change // the loop or something. perGameInstFMDirsList.Add(new List <string>()); perGameInstFMDirsDatesList.Add(new List <DateTime>()); string instPath = Config.FMInstallPaths[gi]; if (Directory.Exists(instPath)) { try { var dirs = FastIO.GetDirsTopOnly_FMs(instPath, "*", out List <DateTime> dateTimes); for (int di = 0; di < dirs.Count; di++) { string d = dirs[di]; if (!d.EqualsI(".fmsel.cache")) { perGameInstFMDirsList[gi].Add(d); perGameInstFMDirsDatesList[gi].Add(dateTimes[di]); } } } catch (Exception ex) { Log("Exception getting directories in " + instPath, ex); } } } #endregion #region Get archives from disk var fmArchives = new List <string>(); var fmArchivesDates = new List <DateTime>(); var archivePaths = GetFMArchivePaths(); bool onlyOnePath = archivePaths.Count == 1; for (int ai = 0; ai < archivePaths.Count; ai++) { try { // Returns filenames only (not full paths) var files = FastIO.GetFilesTopOnly_FMs(archivePaths[ai], "*", out List <DateTime> dateTimes); for (int fi = 0; fi < files.Count; fi++) { string f = files[fi]; // Only do .ContainsI() if we're searching multiple directories. Otherwise we're guaranteed // no duplicates and can avoid the expensive lookup. // @DIRSEP: These are filename only, no need for PathContainsI() if ((onlyOnePath || !fmArchives.ContainsI(f)) && f.ExtIsArchive() && !f.ContainsI(Paths.FMSelBak)) { fmArchives.Add(f); fmArchivesDates.Add(dateTimes[fi]); } } } catch (Exception ex) { Log("Exception getting files in " + archivePaths[ai], ex); } } #endregion #region Build FanMission objects from installed dirs var perGameFMsList = new List <List <FanMission> >(SupportedGameCount); for (int gi = 0; gi < SupportedGameCount; gi++) { // NOTE! List must have SupportedGameCount items in it perGameFMsList.Add(new List <FanMission>()); for (int di = 0; di < perGameInstFMDirsList[gi].Count; di++) { perGameFMsList[gi].Add(new FanMission { InstalledDir = perGameInstFMDirsList[gi][di], Game = GameIndexToGame((GameIndex)gi), Installed = true }); } } #endregion MergeNewArchiveFMs(fmArchives, fmArchivesDates); int instInitCount = FMDataIniList.Count; for (int i = 0; i < SupportedGameCount; i++) { var curGameInstFMsList = perGameFMsList[i]; if (curGameInstFMsList.Count > 0) { MergeNewInstalledFMs(curGameInstFMsList, perGameInstFMDirsDatesList[i], instInitCount); } } SetArchiveNames(fmArchives); SetInstalledNames(); BuildViewList(fmArchives, perGameInstFMDirsList); /* * TODO: There's an extreme corner case where duplicate FMs can appear in the list * It's so unlikely it's almost not worth worrying about, but here's the scenario: * -The FM is installed by hand and not truncated * -The FM is not in the list * -A matching archive exists for the FM * In this scenario, the FM is added twice to the list, once with the full installed folder name and * NoArchive set to true, and once with a truncated installed dir name, the correct archive name, and * NoArchive not present (false). * The code in here is so crazy-go-nuts I can't even find where this is happening. But putting this * note down for the future. */ }
// MT: On startup only, this is run in parallel with MainForm.ctor and .InitThreadable() // So don't touch anything the other touches: anything affecting the view. // @CAN_RUN_BEFORE_VIEW_INIT private static List <int> FindInternal(bool startup) { if (!startup) { // Make sure we don't lose anything when we re-find! // NOTE: This also writes out TagsStrings and then reads them back in and syncs them with Tags. // Critical that that gets done. Ini.WriteFullFMDataIni(); // Do this every time we modify FMsViewList in realtime, to prevent FMsDGV from redrawing from // the list when it's in an indeterminate state (which can cause a selection change (bad) and/or // a visible change of the list (not really bad but unprofessional looking)). // MT: Don't do this on startup because we're running in parallel with the form new/init in that case. Core.View.SetRowCount(0); } // Init or reinit - must be deep-copied or changes propagate back because reference types // MT: This is thread-safe, the view ctor and InitThreadable() doesn't touch it. PresetTags.DeepCopyTo(GlobalTags); #region Back up lists and read FM data file // Copy FMs to backup lists before clearing, in case we can't read the ini file. We don't want to end // up with a blank or incomplete list and then glibly save it out later. FanMission[] backupList = new FanMission[FMDataIniList.Count]; FMDataIniList.CopyTo(backupList); FanMission[] viewBackupList = new FanMission[FMsViewList.Count]; FMsViewList.CopyTo(viewBackupList); FMDataIniList.Clear(); FMsViewList.Clear(); bool fmDataIniExists = File.Exists(Paths.FMDataIni); if (fmDataIniExists) { try { Ini.ReadFMDataIni(Paths.FMDataIni, FMDataIniList); } catch (Exception ex) { Log("Exception reading FM data ini", ex); if (startup) { // Language will be loaded by this point Dialogs.ShowError(LText.AlertMessages.FindFMs_ExceptionReadingFMDataIni); Core.EnvironmentExitDoShutdownTasks(1); } else { FMDataIniList.ClearAndAdd(backupList); FMsViewList.ClearAndAdd(viewBackupList); return(new List <int>()); } } } #endregion #region Get installed dirs from disk // Could check inside the folder for a .mis file to confirm it's really an FM folder, but that's // horrendously expensive. Talking like eight seconds vs. < 4ms for the 1098 set. Weird. var perGameInstFMDirsList = new List <List <string> >(SupportedGameCount); var perGameInstFMDirsDatesList = new List <List <DateTime> >(SupportedGameCount); for (int gi = 0; gi < SupportedGameCount; gi++) { // NOTE! Make sure this list ends up with SupportedGameCount items in it. Just in case I change // the loop or something. perGameInstFMDirsList.Add(new List <string>()); perGameInstFMDirsDatesList.Add(new List <DateTime>()); string instPath = Config.GetFMInstallPath((GameIndex)gi); if (Directory.Exists(instPath)) { try { var dirs = FastIO.GetDirsTopOnly_FMs(instPath, "*", out List <DateTime> dateTimes); for (int di = 0; di < dirs.Count; di++) { string d = dirs[di]; if (!d.EqualsI(Paths.FMSelCache)) { perGameInstFMDirsList[gi].Add(d); perGameInstFMDirsDatesList[gi].Add(dateTimes[di]); } } } catch (Exception ex) { Log("Exception getting directories in " + instPath, ex); } } } #endregion #region Get archives from disk var fmArchivesAndDatesDict = new DictionaryI <DateTime>(); var archivePaths = FMArchives.GetFMArchivePaths(); bool onlyOnePath = archivePaths.Count == 1; for (int ai = 0; ai < archivePaths.Count; ai++) { try { // Returns filenames only (not full paths) var files = FastIO.GetFilesTopOnly_FMs(archivePaths[ai], "*", out List <DateTime> dateTimes); for (int fi = 0; fi < files.Count; fi++) { string f = files[fi]; // Do this first because it should be faster than a dictionary lookup if (!f.ExtIsArchive()) { continue; } // NOTE: We do a ContainsKey check to keep behavior the same as previously. When we use // dict[key] = value, it _replaces_ the value with the new one every time. What we want // is for it to just not touch it at all if the key is already in there. This check does // technically slow it down some, but the actual perf degradation is negligible. And we // still avoid the n-squared 1.6-million-call nightmare we get with ~1600 FMs in the list. // Nevertheless, we can avoid even this small extra cost if we only have one FM archive // path, so no harm in keeping the only-one-path check. if ((onlyOnePath || !fmArchivesAndDatesDict.ContainsKey(f)) && // @DIRSEP: These are filename only, no need for PathContainsI() !f.ContainsI(Paths.FMSelBak)) { fmArchivesAndDatesDict[f] = dateTimes[fi]; } } } catch (Exception ex) { Log("Exception getting files in " + archivePaths[ai], ex); } } int fmArchivesAndDatesDictLen = fmArchivesAndDatesDict.Count; // PERF_TODO: May want to keep these as dicts later or change other vars to dicts string[] fmArchives = new string[fmArchivesAndDatesDictLen]; DateTime[] fmArchivesDates = new DateTime[fmArchivesAndDatesDictLen]; { int i = 0; foreach (var item in fmArchivesAndDatesDict) { fmArchives[i] = item.Key; fmArchivesDates[i] = item.Value; i++; } } #endregion #region Build FanMission objects from installed dirs var perGameFMsList = new List <List <FanMission> >(SupportedGameCount); for (int gi = 0; gi < SupportedGameCount; gi++) { // NOTE! List must have SupportedGameCount items in it perGameFMsList.Add(new List <FanMission>()); for (int di = 0; di < perGameInstFMDirsList[gi].Count; di++) { perGameFMsList[gi].Add(new FanMission { InstalledDir = perGameInstFMDirsList[gi][di], Game = GameIndexToGame((GameIndex)gi), Installed = true }); } } #endregion MergeNewArchiveFMs(fmArchives, fmArchivesDates); int instInitCount = FMDataIniList.Count; for (int i = 0; i < SupportedGameCount; i++) { var curGameInstFMsList = perGameFMsList[i]; if (curGameInstFMsList.Count > 0) { MergeNewInstalledFMs(curGameInstFMsList, perGameInstFMDirsDatesList[i], instInitCount); } } SetArchiveNames(fmArchives); SetInstalledNames(); // Super quick-n-cheap hack for perf: So we don't have to iterate the whole list looking for unscanned // FMs. This will contain indexes into FMDataIniList (not FMsViewList!) var fmsViewListUnscanned = new List <int>(FMDataIniList.Count); BuildViewList(fmArchives, perGameInstFMDirsList, fmsViewListUnscanned); return(fmsViewListUnscanned); /* * TODO: There's an extreme corner case where duplicate FMs can appear in the list * It's so unlikely it's almost not worth worrying about, but here's the scenario: * -The FM is installed by hand and not truncated * -The FM is not in the list * -A matching archive exists for the FM * In this scenario, the FM is added twice to the list, once with the full installed folder name and * NoArchive set to true, and once with a truncated installed dir name, the correct archive name, and * NoArchive not present (false). * The code in here is so crazy-go-nuts I can't even find where this is happening. But putting this * note down for the future. */ }