internal static void SetFMResource(FanMission fm, CustomResources resource, bool value) { if (value) { fm.Resources |= resource; } else { fm.Resources &= ~resource; } }
// Static for perf - this gets called from most comparer classes and we don't want to be instantiating // new title-sort classes in a loop! internal static int TitleCompare(FanMission x, FanMission y) { // Important: these get modified, so don't use the originals! string xTitle = x.Title; string yTitle = y.Title; // null for perf: don't create a new List<string> just to signify empty var articles = Config.EnableArticles ? Config.Articles : null; if (xTitle == yTitle) { return(TitleOrFallback(xTitle, yTitle, x, y, compareTitles: false)); } if (xTitle.IsEmpty()) { return(-1); } if (yTitle.IsEmpty()) { return(1); } int xTitleLen = xTitle.Length; int yTitleLen = yTitle.Length; if (articles == null || articles.Count == 0) { return(TitleOrFallback(xTitle, yTitle, x, y)); } bool xArticleSet = false; bool yArticleSet = false; foreach (string a in articles) { int aLen = a.Length; // Avoid concats for perf if (!xArticleSet && xTitle.StartsWithI(a) && xTitleLen > aLen && char.IsWhiteSpace(xTitle[aLen])) { xTitle = xTitle.Substring(aLen + 1); xArticleSet = true; } if (!yArticleSet && yTitle.StartsWithI(a) && yTitleLen > aLen && char.IsWhiteSpace(yTitle[aLen])) { yTitle = yTitle.Substring(aLen + 1); yArticleSet = true; } } return(TitleOrFallback(xTitle, yTitle, x, y)); }
public int Compare(FanMission x, FanMission y) { int ret = x.DisableAllMods && !y.DisableAllMods ? -1 : !x.DisableAllMods && y.DisableAllMods ? 1 : (x.DisableAllMods && y.DisableAllMods) || x.DisabledMods == y.DisabledMods ? TitleCompare(x, y) : // Sort this column content-first for better UX x.DisabledMods.IsEmpty() ? 1 : y.DisabledMods.IsEmpty() ? -1 : string.Compare(x.DisabledMods, y.DisabledMods, StringComparison.InvariantCultureIgnoreCase); return(SortOrder == SortOrder.Ascending ? ret : -ret); }
// PERF_TODO: ffmpeg can do multiple files in one run. Switch to that, and see if ffprobe can do it too. // OpenAL doesn't play nice with anything over 16 bits, blasting out white noise when it tries to play // such. Converting all >16bit wavs to 16 bit fixes this. internal static async Task ConvertWAVsTo16Bit(FanMission fm, bool doChecksAndProgressBox) { if (doChecksAndProgressBox) { var(success, refreshFM) = ChecksPassed(fm); if (!success) { if (refreshFM) { await Core.View.RefreshSelectedFM(refreshReadme : false); } return; } }
public async void InstallFM_Test() { var fm = new FanMission(); // TODO: // -Create test fm zip file // -Fill out fm fields with appropriate values // -Either modify InstallFM so that we pass it everything it uses, or else just fill out every global // it uses with correct values for our test //bool success = await FMInstallAndPlay.InstallFM(fm); // TODO: Assert audio files have been converted properly }
public void UpdateFMTagsString_Test() { var fm = new FanMission(); var cat1 = new CatAndTags { Category = "author" }; cat1.Tags.Add("Tannar"); cat1.Tags.Add("Random_Taffer"); fm.Tags.Add(cat1); var cat2 = new CatAndTags { Category = "contest" }; cat2.Tags.Add("10 rooms"); fm.Tags.Add(cat2); var cat3 = new CatAndTags { Category = "length" }; cat3.Tags.Add("short"); fm.Tags.Add(cat3); var cat4 = new CatAndTags { Category = "series" }; fm.Tags.Add(cat4); var cat5 = new CatAndTags { Category = "misc" }; cat5.Tags.Add("campaign"); cat5.Tags.Add("atmospheric"); cat5.Tags.Add("other protagonist"); cat5.Tags.Add("water"); cat5.Tags.Add("thing_shaped"); fm.Tags.Add(cat5); FMTags.UpdateFMTagsString(fm); Assert.Equal( "author:Tannar,author:Random_Taffer,contest:10 rooms,length:short,series,misc:campaign,misc:atmospheric,misc:other protagonist,misc:water,misc:thing_shaped", fm.TagsString); }
// @BetterErrors(FillFMSupportedLangs()) internal static void FillFMSupportedLangs(FanMission fm) { // We should already have checked before getting here, but just for safety... if (!GameIsDark(fm.Game)) { return; } string fmInstPath = Path.Combine(Config.GetFMInstallPath(GameToGameIndex(fm.Game)), fm.InstalledDir); List <string> langs; if (FMIsReallyInstalled(fm)) { try { langs = GetFMSupportedLanguagesFromInstDir(fmInstPath, earlyOutOnEnglish: false); } catch (Exception ex) { Log("Exception trying to detect language folders in installed dir for fm '" + fm.Archive + "' (inst dir '" + fm.InstalledDir + "')", ex); fm.LangsScanned = false; return; } } else { try { (_, langs) = GetFMSupportedLanguagesFromArchive(fm.Archive, earlyOutOnEnglish: false); } catch (Exception ex) { Log("Exception trying to detect language folders in archive for fm '" + fm.Archive + "' (inst dir '" + fm.InstalledDir + "')", ex); fm.LangsScanned = false; return; } } if (langs.Count > 0) { langs = SortLangsToSpec(langs.ToHashSetI()); fm.Langs = string.Join(",", langs); } fm.LangsScanned = true; }
static int TitleOrFallback(string title1, string title2, FanMission fm1, FanMission fm2, bool compareTitles = true) { int ret; if (compareTitles) { ret = string.Compare(title1, title2, StringComparison.InvariantCultureIgnoreCase); if (ret != 0) { return(ret); } } ret = string.Compare(fm1.Archive, fm2.Archive, StringComparison.InvariantCultureIgnoreCase); if (ret != 0) { return(ret); } return(string.Compare(fm1.InstalledDir, fm2.InstalledDir, StringComparison.InvariantCultureIgnoreCase)); }
private static void ClearCacheDir(FanMission fm) { string fmCachePath = Path.Combine(Paths.FMsCache, fm.InstalledDir); if (!fmCachePath.TrimEnd(CA_BS_FS).PathEqualsI(Paths.FMsCache.TrimEnd(CA_BS_FS)) && Directory.Exists(fmCachePath)) { try { foreach (string f in FastIO.GetFilesTopOnly(fmCachePath, "*")) { File.Delete(f); } foreach (string d in FastIO.GetDirsTopOnly(fmCachePath, "*")) { Directory.Delete(d, recursive: true); } } catch (Exception ex) { Log("Exception clearing files in FM cache for " + fm.Archive + " / " + fm.InstalledDir, ex); } } }
// If some files exist but not all that are in the zip, the user can just re-scan for this data by clicking // a button, so don't worry about it internal static async Task <CacheData> GetCacheableData(FanMission fm, bool refreshCache) { if (fm.Game == Game.Unsupported) { if (!fm.InstalledDir.IsEmpty()) { ClearCacheDir(fm); } return(new CacheData()); } try { return(FMIsReallyInstalled(fm) ? GetCacheableDataInFMInstalledDir(fm) : await GetCacheableDataInFMCacheDir(fm, refreshCache)); } catch (Exception ex) { Log("Exception in GetCacheableData", ex); return(new CacheData()); } }
private static void FMData_InstalledDir_Set(FanMission fm, string valTrimmed, string valRaw) { fm.InstalledDir = valTrimmed; }
// Static for perf - this gets called from most comparer classes and we don't want to be instantiating // new title-sort classes in a loop! private static int TitleCompare(FanMission x, FanMission y) {
ImportInternal(string iniFile, bool importFMData, bool importSaves, bool returnUnmergedFMsList = false, FieldsToImport?fields = null) { string[] lines = await Task.Run(() => File.ReadAllLines(iniFile)); var fms = new List <FanMission>(); var error = ImportError.None; if (importFMData) { bool missionDirsRead = false; var archiveDirs = new List <string>(); error = await Task.Run(() => { try { for (int i = 0; i < lines.Length; i++) { string line = lines[i]; string lineTS = line.TrimStart(); string lineTB = lineTS.TrimEnd(); #region Read archive directories // We need to know the archive dirs before doing anything, because we may need to recreate // some lossy names (if any bad chars have been removed by DarkLoader). if (!missionDirsRead && lineTB == "[mission directories]") { while (i < lines.Length - 1) { string lt = lines[i + 1].Trim(); if (!lt.IsEmpty() && lt[0] != '[' && lt.EndsWith("=1")) { archiveDirs.Add(lt.Substring(0, lt.Length - 2)); } else if (!lt.IsEmpty() && lt[0] == '[' && lt[lt.Length - 1] == ']') { break; } i++; } if (archiveDirs.Count == 0 || archiveDirs.All(x => x.IsWhiteSpace())) { return(ImportError.NoArchiveDirsFound); } // Restart from the beginning of the file, this time skipping anything that isn't an // FM entry i = -1; missionDirsRead = true; continue; } #endregion #region Read FM entries // MUST CHECK missionDirsRead OR IT ADDS EVERY FM TWICE! if (missionDirsRead && !NonFMHeaders.Contains(lineTB) && lineTB.Length > 0 && lineTB[0] == '[' && lineTB[lineTB.Length - 1] == ']' && lineTB.Contains('.') && DarkLoaderFMRegex.Match(lineTB).Success) { int lastIndexDot = lineTB.LastIndexOf('.'); string archive = lineTB.Substring(1, lastIndexDot - 1); string size = lineTB.Substring(lastIndexDot + 1, lineTB.Length - lastIndexDot - 2); foreach (string dir in archiveDirs) { if (!Directory.Exists(dir)) { continue; } try { // DarkLoader only does zip format foreach (string f in FastIO.GetFilesTopOnly(dir, "*.zip")) { string fn = Path.GetFileNameWithoutExtension(f); if (RemoveDLArchiveBadChars(fn).EqualsI(archive)) { archive = fn; goto breakout; } } } catch (Exception ex) { Log("Exception in DarkLoader archive dir file enumeration", ex); } } breakout: // Add .zip back on; required because everything expects it, and furthermore if there's // a dot anywhere in the name then everything after it will be treated as the extension // and is liable to be lopped off at any time archive += ".zip"; ulong.TryParse(size, out ulong sizeBytes); var fm = new FanMission { Archive = archive, InstalledDir = archive.ToInstDirNameFMSel(), SizeBytes = sizeBytes }; // We don't import game type, because DarkLoader by default gets it wrong for NewDark // FMs (the user could have changed it manually in the ini file, and in fact it's // somewhat likely they would have done so, but still, better to just scan for it // ourselves later) while (i < lines.Length - 1) { string lts = lines[i + 1].TrimStart(); string ltb = lts.TrimEnd(); if (lts.StartsWith("comment=\"")) { string comment = ltb.Substring(9); if (comment.Length >= 2 && comment[comment.Length - 1] == '\"') { comment = comment.Substring(0, comment.Length - 1); fm.Comment = DLUnescapeChars(comment); } } else if (lts.StartsWith("title=\"")) { string title = ltb.Substring(7); if (title.Length >= 2 && title[title.Length - 1] == '\"') { title = title.Substring(0, title.Length - 1); fm.Title = DLUnescapeChars(title); } } else if (lts.StartsWith("misdate=")) { ulong.TryParse(ltb.Substring(8), out ulong result); try { var date = new DateTime(1899, 12, 30).AddDays(result); fm.ReleaseDate.DateTime = date.Year > 1998 ? date : (DateTime?)null; } catch (ArgumentOutOfRangeException) { fm.ReleaseDate.DateTime = null; } } else if (lts.StartsWith("date=")) { ulong.TryParse(ltb.Substring(5), out ulong result); try { var date = new DateTime(1899, 12, 30).AddDays(result); fm.LastPlayed.DateTime = date.Year > 1998 ? date : (DateTime?)null; } catch (ArgumentOutOfRangeException) { fm.LastPlayed.DateTime = null; } } else if (lts.StartsWith("finished=")) { uint.TryParse(ltb.Substring(9), out uint result); // result will be 0 on fail, which is the empty value so it's fine fm.FinishedOn = result; } else if (!ltb.IsEmpty() && ltb[0] == '[' && ltb[ltb.Length - 1] == ']') { break; } i++; } fms.Add(fm); } #endregion } return(ImportError.None); } catch (Exception ex) { Log("Exception in " + nameof(ImportDarkLoader) + "." + nameof(ImportInternal), ex); return(ImportError.Unknown); } finally { Core.View.InvokeSync(new Action(Core.View.HideProgressBox)); } }); } if (error != ImportError.None) { return(error, fms); } if (importSaves) { bool success = await ImportSaves(lines); } var importedFMs = returnUnmergedFMsList ? fms : ImportCommon.MergeImportedFMData(ImportType.DarkLoader, fms, fields); return(ImportError.None, importedFMs); }
// Update fm.TagsString here. We keep TagsString around because when we're reading, writing, and merging // FMs, we don't want to spend time converting back and forth. So Tags is session-only, and only gets // filled out for FMs that will be displayed. TagsString is the one that gets saved and loaded, and must // be kept in sync with Tags. This should ONLY be called when a tag is added or removed. Keep it simple // so we can see and follow the logic. private static void UpdateFMTagsString(FanMission fm) { fm.TagsString = TagsToString(fm.Tags, writeEmptyCategories: false); }
private static void FMData_Installed_Set(FanMission fm, string valTrimmed, string valRaw) { fm.Installed = valTrimmed.EqualsTrue(); }
private static void FMData_Author_Set(FanMission fm, string valTrimmed, string valRaw) { fm.Author = valTrimmed; }
private static void FMData_HasResources_Set(FanMission fm, string valTrimmed, string valRaw) { fm.ResourcesScanned = !valTrimmed.EqualsI("NotScanned"); FillFMHasXFields(fm, valTrimmed); }
internal static bool FMHasResource(FanMission fm, CustomResources resource) => (fm.Resources & resource) == resource;
internal static void AddTagToFM(FanMission fm, string catAndTag) { AddTagsToFMAndGlobalList(catAndTag, fm.Tags); UpdateFMTagsString(fm); Ini.WriteFullFMDataIni(); }
private static void FMData_HasSubtitles_Set(FanMission fm, string valTrimmed, string valRaw) { SetFMResource(fm, CustomResources.Subtitles, valTrimmed.EqualsTrue()); fm.ResourcesScanned = true; }
// This nonsense is to allow for keys to be looked up in a dictionary rather than running ten thousand // if statements on every line. private static void FMData_NoArchive_Set(FanMission fm, string valTrimmed, string valRaw) { fm.NoArchive = valTrimmed.EqualsTrue(); }
private static void FMData_TagsString_Set(FanMission fm, string valTrimmed, string valRaw) { fm.TagsString = valTrimmed; }
private static void FMData_SelectedLang_Set(FanMission fm, string valTrimmed, string valRaw) { fm.SelectedLang = valTrimmed; }
private static void FMData_Langs_Set(FanMission fm, string valTrimmed, string valRaw) { fm.Langs = valTrimmed; }
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); }
private static void FMData_Title_Set(FanMission fm, string valTrimmed, string valRaw) { fm.Title = valTrimmed; }
private static void FMData_MarkedScanned_Set(FanMission fm, string valTrimmed, string valRaw) { fm.MarkedScanned = valTrimmed.EqualsTrue(); }
private static void FMData_DisableAllMods_Set(FanMission fm, string valTrimmed, string valRaw) { fm.DisableAllMods = valTrimmed.EqualsTrue(); }
internal static bool FMNeedsScan(FanMission fm) => fm.Game == Game.Null || (fm.Game != Game.Unsupported && !fm.MarkedScanned);
private static void FMData_Archive_Set(FanMission fm, string valTrimmed, string valRaw) { fm.Archive = valTrimmed; }