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); }
GetFMSupportedLanguagesFromArchive(string archiveName, bool earlyOutOnEnglish) { // @DIRSEP: '/' conversion in here due to string.IndexOf() (bool, List <string>)failed = (false, new List <string>()); string archivePath = FMArchives.FindFirstMatch(archiveName); if (archivePath.IsEmpty()) { return(failed); } var ret = new List <string>(Supported.Length); bool[] FoundLangInArchive = new bool[Supported.Length]; // Pre-concat each string only once for perf string[] SLangsFSPrefixed = new string[Supported.Length]; for (int i = 0; i < Supported.Length; i++) { SLangsFSPrefixed[i] = "/" + Supported[i]; } FMScanner.FastZipReader.ZipArchiveFast?zipArchive = null; SevenZipExtractor?sevenZipArchive = null; try { bool fmIsZip = archivePath.ExtIsZip(); if (fmIsZip) { zipArchive = new FMScanner.FastZipReader.ZipArchiveFast(File.OpenRead(archivePath)); } else { sevenZipArchive = new SevenZipExtractor(archivePath); } int filesCount = fmIsZip ? zipArchive !.Entries.Count : (int)sevenZipArchive !.FilesCount; for (int i = 0; i < filesCount; i++) { string fn = fmIsZip // ZipArchiveFast guarantees full names to never contain backslashes ? zipArchive !.Entries[i].FullName // For some reason ArchiveFileData[i].FileName is like 20x faster than ArchiveFileNames[i] : sevenZipArchive !.ArchiveFileData[i].FileName.ToForwardSlashes(); for (int j = 0; j < Supported.Length; j++) { string sl = Supported[j]; if (!FoundLangInArchive[j]) { // Do as few string operations as humanly possible int index = fn.IndexOf(SLangsFSPrefixed[j], StringComparison.OrdinalIgnoreCase); if (index < 1) { continue; } if ((fn.Length > index + sl.Length + 1 && fn[index + sl.Length + 1] == '/') || (index == (fn.Length - sl.Length) - 1)) { if (earlyOutOnEnglish && j == 0) { return(true, new List <string> { "English" }); } ret.Add(sl); FoundLangInArchive[j] = true; } } } } } catch (Exception ex) { Log("Scanning archive '" + archivePath + "' for languages failed.", ex); return(false, new List <string>()); } finally { zipArchive?.Dispose(); sevenZipArchive?.Dispose(); } return(true, ret); }