internal static IEnumerator <ProgressReport> BuildCachedManifestLoop(VersionManifest manifest) { stopwatch.Start(); // there are no mods loaded, just return if (modLoadOrder == null || modLoadOrder.Count == 0) { yield break; } string loadingModText = "Loading Mod Manifests"; yield return(new ProgressReport(0.0f, loadingModText, "Setting up mod manifests...")); LogWithDate("Setting up mod manifests..."); var jsonMerges = new Dictionary <string, List <string> >(); modEntries = new List <ModDef.ManifestEntry>(); int modCount = 0; var manifestMods = modLoadOrder.Where(name => modManifest.ContainsKey(name)).ToList(); foreach (var modName in manifestMods) { Log($"\t{modName}:"); yield return(new ProgressReport((float)modCount++ / (float)manifestMods.Count, loadingModText, string.Format("Loading manifest for {0}", modName))); foreach (var modEntry in modManifest[modName]) { // type being null means we have to figure out the type from the path (StreamingAssets) if (modEntry.Type == null) { // TODO: + 16 is a little bizzare looking, it's the length of the substring + 1 because we want to get rid of it and the \ var relPath = modEntry.Path.Substring(modEntry.Path.LastIndexOf("StreamingAssets", StringComparison.Ordinal) + 16); var fakeStreamingAssetsPath = Path.GetFullPath(Path.Combine(StreamingAssetsDirectory, relPath)); var types = GetTypesFromCacheOrManifest(manifest, fakeStreamingAssetsPath); if (types == null) { Log($"\t\tCould not find an existing VersionManifest entry for {modEntry.Id}. Is this supposed to be a new entry? Don't put new entries in StreamingAssets!"); continue; } if (Path.GetExtension(modEntry.Path).ToLower() == ".json" && modEntry.ShouldMergeJSON) { if (!jsonMerges.ContainsKey(fakeStreamingAssetsPath)) { jsonMerges[fakeStreamingAssetsPath] = new List <string>(); } if (jsonMerges[fakeStreamingAssetsPath].Contains(modEntry.Path)) { continue; } // this assumes that .json can only have a single type // typeCache will always contain this path modEntry.Type = typeCache[fakeStreamingAssetsPath][0]; Log($"\t\tMerge => {modEntry.Id} ({modEntry.Type})"); jsonMerges[fakeStreamingAssetsPath].Add(modEntry.Path); continue; } foreach (var type in types) { var subModEntry = new ModDef.ManifestEntry(modEntry, modEntry.Path, modEntry.Id); subModEntry.Type = type; if (AddModEntry(manifest, subModEntry)) { modEntries.Add(subModEntry); } } continue; } // get "fake" entries that don't actually go into the game's VersionManifest // add videos to be loaded from an external path switch (modEntry.Type) { case "Video": var fileName = Path.GetFileName(modEntry.Path); if (fileName != null && File.Exists(modEntry.Path)) { Log($"\t\tVideo => {fileName}"); ModVideos.Add(fileName, modEntry.Path); } continue; case "AdvancedJSONMerge": var targetFileRelative = AdvancedJSONMerger.GetTargetFile(modEntry.Path); var targetFile = ResolvePath(targetFileRelative); // need to add the types of the file to the typeCache, so that they can be used later // this actually returns the type, but we don't actually care about that right now GetTypesFromCacheOrManifest(manifest, targetFile); if (!jsonMerges.ContainsKey(targetFile)) { jsonMerges[targetFile] = new List <string>(); } if (jsonMerges[targetFile].Contains(modEntry.Path)) { continue; } Log($"\t\tAdvancedJSONMerge => {modEntry.Id} ({modEntry.Type})"); jsonMerges[targetFile].Add(modEntry.Path); continue; } // non-streamingassets json merges if (Path.GetExtension(modEntry.Path)?.ToLower() == ".json" && modEntry.ShouldMergeJSON) { // have to find the original path for the manifest entry that we're merging onto var matchingEntry = manifest.Find(x => x.Id == modEntry.Id); if (matchingEntry == null) { Log($"\t\tCould not find an existing VersionManifest entry for {modEntry.Id}!"); continue; } if (!jsonMerges.ContainsKey(matchingEntry.FilePath)) { jsonMerges[matchingEntry.FilePath] = new List <string>(); } if (jsonMerges[matchingEntry.FilePath].Contains(modEntry.Path)) { continue; } // this assumes that .json can only have a single type modEntry.Type = matchingEntry.Type; if (!typeCache.ContainsKey(matchingEntry.FilePath)) { typeCache[matchingEntry.FilePath] = new List <string>(); typeCache[matchingEntry.FilePath].Add(modEntry.Type); } Log($"\t\tMerge => {modEntry.Id} ({modEntry.Type})"); jsonMerges[matchingEntry.FilePath].Add(modEntry.Path); continue; } if (AddModEntry(manifest, modEntry)) { modEntries.Add(modEntry); } } } yield return(new ProgressReport(100.0f, "JSON", "Writing JSON file to disk")); // write type cache to disk WriteJsonFile(TypeCachePath, typeCache); // perform merges into cache LogWithDate("Doing merges..."); yield return(new ProgressReport(0.0f, "Merges", "Doing Merges...")); int mergeCount = 0; foreach (var jsonMerge in jsonMerges) { yield return(new ProgressReport((float)mergeCount++ / jsonMerges.Count, "Merges", string.Format("Merging {0}", jsonMerge.Key))); var cachePath = jsonMergeCache.GetOrCreateCachedEntry(jsonMerge.Key, jsonMerge.Value); // something went wrong (the parent json prob had errors) if (cachePath == null) { continue; } var cacheEntry = new ModDef.ManifestEntry(cachePath); cacheEntry.ShouldMergeJSON = false; cacheEntry.Type = typeCache[jsonMerge.Key][0]; // this assumes only one type for each json file cacheEntry.Id = InferIDFromFile(cachePath); if (AddModEntry(manifest, cacheEntry)) { modEntries.Add(cacheEntry); } } yield return(new ProgressReport(100.0f, "Merge Cache", "Writing Merge Cache to disk")); // write merge cache to disk jsonMergeCache.WriteCacheToDisk(Path.Combine(CacheDirectory, MERGE_CACHE_FILE_NAME)); LogWithDate("Adding to DB..."); // check if files removed from DB cache var rebuildDB = false; var replacementEntries = new List <VersionManifestEntry>(); var removeEntries = new List <string>(); string dbText = "Syncing Database"; yield return(new ProgressReport(0.0f, dbText, "")); foreach (var kvp in dbCache) { var path = kvp.Key; if (File.Exists(path)) { continue; } Log($"\tNeed to remove DB entry from file in path: {path}"); // file is missing, check if another entry exists with same filename in manifest var fileName = Path.GetFileName(path); var existingEntry = manifest.Find(x => Path.GetFileName(x.FilePath) == fileName); if (existingEntry == null) { Log("\t\tHave to rebuild DB, no existing entry in VersionManifest matches removed entry"); rebuildDB = true; break; } replacementEntries.Add(existingEntry); removeEntries.Add(path); } // add removed entries replacements to db dbText = "Cleaning Database"; yield return(new ProgressReport(100.0f, dbText, "")); if (!rebuildDB) { // remove old entries foreach (var removeEntry in removeEntries) { dbCache.Remove(removeEntry); } using (var metadataDatabase = new MetadataDatabase()) { foreach (var replacementEntry in replacementEntries) { if (AddModEntryToDB(metadataDatabase, Path.GetFullPath(replacementEntry.FilePath), replacementEntry.Type)) { Log($"\t\tReplaced DB entry with an existing entry in path: {Path.GetFullPath(replacementEntry.FilePath)}"); } } } } // if an entry has been removed and we cannot find a replacement, have to rebuild the mod db if (rebuildDB) { if (File.Exists(ModMDDBPath)) { File.Delete(ModMDDBPath); } File.Copy(MDDBPath, ModMDDBPath); dbCache = new Dictionary <string, DateTime>(); } // add needed files to db dbText = "Populating Database"; int addCount = 0; yield return(new ProgressReport(0.0f, dbText, "")); using (var metadataDatabase = new MetadataDatabase()) { foreach (var modEntry in modEntries) { if (modEntry.AddToDB && AddModEntryToDB(metadataDatabase, modEntry.Path, modEntry.Type)) { yield return(new ProgressReport((float)addCount / (float)modEntries.Count, dbText, string.Format("Added {0}", modEntry.Path))); Log($"\tAdded/Updated {modEntry.Id} ({modEntry.Type})"); } addCount++; } } // write db/type cache to disk WriteJsonFile(DBCachePath, dbCache); stopwatch.Stop(); Log(""); LogWithDate($"Done. Elapsed running time: {stopwatch.Elapsed.TotalSeconds} seconds\n"); // Cache the completed manifest ModTek.cachedManifest = manifest; try { if (manifest != null && ModTek.modEntries != null) { ModTek.modtekOverrides = manifest.Entries.Where(e => ModTek.modEntries.Any(m => e.Id == m.Id)) // ToDictionary expects distinct keys, so take the last entry of each Id .GroupBy(ks => ks.Id) .Select(v => v.Last()) .ToDictionary(ks => ks.Id); } Logger.Log("Built {0} modtek overrides", ModTek.modtekOverrides.Count()); } catch (Exception e) { Logger.Log("Failed to build overrides {0}", e); } yield break; }
public CacheEntry(string cacheAbsolutePath, string originalAbsolutePath, List <string> mergePaths) { _cacheAbsolutePath = cacheAbsolutePath; CachePath = ModTek.GetRelativePath(cacheAbsolutePath, ModTek.GameDirectory); ContainingDirectory = Path.GetDirectoryName(cacheAbsolutePath); OriginalTime = File.GetLastWriteTimeUtc(originalAbsolutePath); if (string.IsNullOrEmpty(ContainingDirectory)) { HasErrors = true; return; } // get the parent JSON JObject parentJObj; try { parentJObj = ModTek.ParseGameJSONFile(originalAbsolutePath); } catch (Exception e) { Log($"\tParent JSON at path {originalAbsolutePath} has errors preventing any merges!"); Log($"\t\t{e.Message}"); HasErrors = true; return; } foreach (var mergePath in mergePaths) { Merges.Add(new PathTimeTuple(ModTek.GetRelativePath(mergePath, ModTek.GameDirectory), File.GetLastWriteTimeUtc(mergePath))); } Directory.CreateDirectory(ContainingDirectory); using (var writer = File.CreateText(cacheAbsolutePath)) { // merge all of the merges foreach (var mergePath in mergePaths) { JObject mergeJObj; try { mergeJObj = ModTek.ParseGameJSONFile(mergePath); } catch (Exception e) { Log($"\tMod merge JSON at path {originalAbsolutePath} has errors preventing any merges!"); Log($"\t\t{e.Message}"); continue; } if (AdvancedJSONMerger.IsAdvancedJSONMerge(mergeJObj)) { try { AdvancedJSONMerger.ProcessInstructionsJObject(parentJObj, mergeJObj); continue; } catch (Exception e) { Log($"\tMod advanced merge JSON at path {mergePath} has errors preventing advanced json merges!"); Log($"\t\t{e.Message}"); } } // assume standard merging parentJObj.Merge(mergeJObj, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); } // write the merged onto file to disk var jsonWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; parentJObj.WriteTo(jsonWriter); jsonWriter.Close(); } }
internal static IEnumerator <ProgressReport> BuildModManifestEntriesLoop() { stopwatch.Start(); // there are no mods loaded, just return if (modLoadOrder == null || modLoadOrder.Count == 0) { yield break; } Log(""); var jsonMerges = new Dictionary <string, List <string> >(); var manifestMods = modLoadOrder.Where(name => entriesByMod.ContainsKey(name)).ToList(); var entryCount = 0; var numEntries = 0; entriesByMod.Do(entries => numEntries += entries.Value.Count); foreach (var modName in manifestMods) { Log($"{modName}:"); foreach (var modEntry in entriesByMod[modName]) { yield return(new ProgressReport(entryCount++ / ((float)numEntries), $"Loading {modName}", modEntry.Id)); // type being null means we have to figure out the type from the path (StreamingAssets) if (modEntry.Type == null) { // TODO: + 16 is a little bizzare looking, it's the length of the substring + 1 because we want to get rid of it and the \ var relPath = modEntry.Path.Substring(modEntry.Path.LastIndexOf("StreamingAssets", StringComparison.Ordinal) + 16); var fakeStreamingAssetsPath = Path.GetFullPath(Path.Combine(StreamingAssetsDirectory, relPath)); if (!File.Exists(fakeStreamingAssetsPath)) { Log($"\tCould not find a file at {fakeStreamingAssetsPath} for {modName} {modEntry.Id}. NOT LOADING THIS FILE"); continue; } var types = GetTypesFromCacheOrManifest(CachedVersionManifest, modEntry.Id); if (types == null) { Log($"\tCould not find an existing VersionManifest entry for {modEntry.Id}. Is this supposed to be a new entry? Don't put new entries in StreamingAssets!"); continue; } // this is getting merged later and then added to the BTRL entries then if (Path.GetExtension(modEntry.Path).ToLower() == ".json" && modEntry.ShouldMergeJSON) { if (!jsonMerges.ContainsKey(modEntry.Id)) { jsonMerges[modEntry.Id] = new List <string>(); } if (jsonMerges[modEntry.Id].Contains(modEntry.Path)) // TODO: is this necessary? { continue; } // this assumes that .json can only have a single type // typeCache will always contain this path modEntry.Type = GetTypesFromCache(modEntry.Id)[0]; Log($"\tMerge: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\" ({modEntry.Type})"); jsonMerges[modEntry.Id].Add(modEntry.Path); continue; } foreach (var type in types) { var subModEntry = new ModDef.ManifestEntry(modEntry, modEntry.Path, modEntry.Id); subModEntry.Type = type; AddModEntry(CachedVersionManifest, subModEntry); // clear json merges for this entry, mod is overwriting the original file, previous mods merges are tossed if (jsonMerges.ContainsKey(modEntry.Id)) { jsonMerges.Remove(modEntry.Id); Log($"\t\tHad merges for {modEntry.Id} but had to toss, since original file is being replaced"); } } continue; } // get "fake" entries that don't actually go into the game's VersionManifest // add videos to be loaded from an external path switch (modEntry.Type) { case "Video": var fileName = Path.GetFileName(modEntry.Path); if (fileName != null && File.Exists(modEntry.Path)) { Log($"\tVideo: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\""); ModVideos.Add(fileName, modEntry.Path); } continue; case "AdvancedJSONMerge": var id = AdvancedJSONMerger.GetTargetID(modEntry.Path); // need to add the types of the file to the typeCache, so that they can be used later // if merging onto a file added by another mod, the type is already in the cache var types = GetTypesFromCacheOrManifest(CachedVersionManifest, id); if (!jsonMerges.ContainsKey(id)) { jsonMerges[id] = new List <string>(); } if (jsonMerges[id].Contains(modEntry.Path)) // TODO: is this necessary? { continue; } Log($"\tAdvancedJSONMerge: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\" ({types[0]})"); jsonMerges[id].Add(modEntry.Path); continue; } // non-streamingassets json merges if (Path.GetExtension(modEntry.Path)?.ToLower() == ".json" && modEntry.ShouldMergeJSON) { // have to find the original path for the manifest entry that we're merging onto var matchingEntry = GetEntryFromCachedOrBTRLEntries(modEntry.Id); if (matchingEntry == null) { Log($"\tCould not find an existing VersionManifest entry for {modEntry.Id}!"); continue; } var matchingPath = Path.GetFullPath(matchingEntry.FilePath); if (!jsonMerges.ContainsKey(modEntry.Id)) { jsonMerges[modEntry.Id] = new List <string>(); } if (jsonMerges[modEntry.Id].Contains(modEntry.Path)) // TODO: is this necessary? { continue; } Log($"\tMerge: \"{GetRelativePath(modEntry.Path, ModsDirectory)}\" ({modEntry.Type})"); // this assumes that .json can only have a single type modEntry.Type = matchingEntry.Type; TryAddTypeToCache(modEntry.Id, modEntry.Type); jsonMerges[modEntry.Id].Add(modEntry.Path); continue; } AddModEntry(CachedVersionManifest, modEntry); TryAddTypeToCache(modEntry.Id, modEntry.Type); // clear json merges for this entry, mod is overwriting the original file, previous mods merges are tossed if (jsonMerges.ContainsKey(modEntry.Id)) { jsonMerges.Remove(modEntry.Id); Log($"\t\tHad merges for {modEntry.Id} but had to toss, since original file is being replaced"); } } } WriteJsonFile(TypeCachePath, typeCache); // perform merges into cache Log(""); LogWithDate("Doing merges..."); yield return(new ProgressReport(1, "Merging", "")); var mergeCount = 0; foreach (var id in jsonMerges.Keys) { var existingEntry = GetEntryFromCachedOrBTRLEntries(id); if (existingEntry == null) { Log($"\tHave merges for {id} but cannot find an original file! Skipping."); continue; } var originalPath = Path.GetFullPath(existingEntry.FilePath); var mergePaths = jsonMerges[id]; if (!jsonMergeCache.HasCachedEntry(originalPath, mergePaths)) { yield return(new ProgressReport(mergeCount++ / ((float)jsonMerges.Count), "Merging", id)); } var cachePath = jsonMergeCache.GetOrCreateCachedEntry(originalPath, mergePaths); // something went wrong (the parent json prob had errors) if (cachePath == null) { continue; } var cacheEntry = new ModDef.ManifestEntry(cachePath) { ShouldMergeJSON = false, Type = GetTypesFromCache(id)[0], // this assumes only one type for each json file Id = id }; AddModEntry(CachedVersionManifest, cacheEntry); } jsonMergeCache.WriteCacheToDisk(Path.Combine(CacheDirectory, MERGE_CACHE_FILE_NAME)); Log(""); Log("Syncing Database"); yield return(new ProgressReport(1, "Syncing Database", "")); // check if files removed from DB cache var rebuildDB = false; var replacementEntries = new List <VersionManifestEntry>(); var removeEntries = new List <string>(); foreach (var path in dbCache.Keys) { var absolutePath = ResolvePath(path, GameDirectory); // check if the file in the db cache is still used if (BTRLEntries.Exists(x => x.Path == absolutePath)) { continue; } Log($"\tNeed to remove DB entry from file in path: {path}"); // file is missing, check if another entry exists with same filename in manifest or in BTRL entries var fileName = Path.GetFileName(path); var existingEntry = BTRLEntries.FindLast(x => Path.GetFileName(x.Path) == fileName)?.GetVersionManifestEntry() ?? CachedVersionManifest.Find(x => Path.GetFileName(x.FilePath) == fileName); if (existingEntry == null) { Log("\t\tHave to rebuild DB, no existing entry in VersionManifest matches removed entry"); rebuildDB = true; break; } replacementEntries.Add(existingEntry); removeEntries.Add(path); } // add removed entries replacements to db if (!rebuildDB) { // remove old entries foreach (var removeEntry in removeEntries) { dbCache.Remove(removeEntry); } using (var metadataDatabase = new MetadataDatabase()) { foreach (var replacementEntry in replacementEntries) { if (AddModEntryToDB(metadataDatabase, Path.GetFullPath(replacementEntry.FilePath), replacementEntry.Type)) { Log($"\t\tReplaced DB entry with an existing entry in path: {Path.GetFullPath(replacementEntry.FilePath)}"); } } } } // if an entry has been removed and we cannot find a replacement, have to rebuild the mod db if (rebuildDB) { if (File.Exists(ModMDDBPath)) { File.Delete(ModMDDBPath); } File.Copy(MDDBPath, ModMDDBPath); dbCache = new Dictionary <string, DateTime>(); } // add needed files to db var addCount = 0; using (var metadataDatabase = new MetadataDatabase()) { foreach (var modEntry in BTRLEntries) { if (modEntry.AddToDB && AddModEntryToDB(metadataDatabase, modEntry.Path, modEntry.Type)) { yield return(new ProgressReport(addCount / ((float)BTRLEntries.Count), "Populating Database", modEntry.Id)); Log($"\tAdded/Updated {modEntry.Id} ({modEntry.Type})"); } addCount++; } } // write db/type cache to disk WriteJsonFile(DBCachePath, dbCache); stopwatch.Stop(); Log(""); LogWithDate($"Done. Elapsed running time: {stopwatch.Elapsed.TotalSeconds} seconds\n"); CloseLogStream(); yield break; }