internal static void TryAddToVersionManifest(VersionManifest manifest) { if (!hasLoadedMods) { LoadMods(); } var breakMyGame = File.Exists(Path.Combine(ModDirectory, "break.my.game")); LogWithDate("Adding in mod manifests!"); if (breakMyGame) { var mddPath = Path.Combine(Path.Combine(StreamingAssetsDirectory, "MDD"), "MetadataDatabase.db"); var mddBackupPath = mddPath + ".orig"; Log($"\tBreak my game mode enabled! All new modded content (doesn't currently support merges) will be added to the DB."); if (!File.Exists(mddBackupPath)) { Log($"\t\tBacking up metadata database to {Path.GetFileName(mddBackupPath)}"); File.Copy(mddPath, mddBackupPath); } } foreach (var modName in modLoadOrder) { if (!ModManifest.ContainsKey(modName)) { continue; } Log($"\t{modName}:"); foreach (var modEntry in ModManifest[modName]) { var existingEntry = manifest.Find(x => x.Id == modEntry.Id); VersionManifestAddendum addendum = null; if (!string.IsNullOrEmpty(modEntry.AddToAddendum)) { addendum = manifest.GetAddendumByName(modEntry.AddToAddendum); // create the addendum if it doesn't exist if (addendum == null) { Log($"\t\tCreated addendum {modEntry.AddToAddendum}:"); addendum = new VersionManifestAddendum(modEntry.AddToAddendum); manifest.ApplyAddendum(addendum); } } if (modEntry.Type == null) { // null type means that we have to find existing entry with the same rel path to fill in the entry // 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.Combine(StreamingAssetsDirectory, relPath); existingEntry = manifest.Find(x => Path.GetFullPath(x.FilePath) == Path.GetFullPath(fakeStreamingAssetsPath)); if (existingEntry == null) { continue; } modEntry.Id = existingEntry.Id; modEntry.Type = existingEntry.Type; } if (Path.GetExtension(modEntry.Path).ToLower() == ".json" && modEntry.ShouldMergeJSON && existingEntry != null) { // read the manifest pointed entry and hash the contents JsonHashToId[File.ReadAllText(existingEntry.FilePath).GetHashCode()] = modEntry.Id; // The manifest already contains this information, so we need to queue it to be merged var partialJson = File.ReadAllText(modEntry.Path); if (!JsonMerges.ContainsKey(modEntry.Id)) { JsonMerges.Add(modEntry.Id, new List <string>()); } if (JsonMerges[modEntry.Id].Contains(partialJson)) { Log($"\t\tAlready added {modEntry.Id} to JsonMerges"); continue; } Log($"\t\tAdding {modEntry.Id} to JsonMerges"); JsonMerges[modEntry.Id].Add(partialJson); continue; } if (breakMyGame && Path.GetExtension(modEntry.Path).ToLower() == ".json") { var type = (BattleTechResourceType)Enum.Parse(typeof(BattleTechResourceType), modEntry.Type); using (var metadataDatabase = new MetadataDatabase()) { VersionManifestHotReload.InstantiateResourceAndUpdateMDDB(type, modEntry.Path, metadataDatabase); Log($"\t\tAdding to MDDB! {type} {modEntry.Path}"); } } if (!string.IsNullOrEmpty(modEntry.AddToAddendum)) { Log($"\t\tAddOrUpdate {modEntry.Type} {modEntry.Id} to addendum {addendum.Name}"); addendum.AddOrUpdate(modEntry.Id, modEntry.Path, modEntry.Type, DateTime.Now, modEntry.AssetBundleName, modEntry.AssetBundlePersistent); continue; } // This is a new definition or a replacement that doesn't get merged, so add or update the manifest Log($"\t\tAddOrUpdate {modEntry.Type} {modEntry.Id}"); manifest.AddOrUpdate(modEntry.Id, modEntry.Path, modEntry.Type, DateTime.Now, modEntry.AssetBundleName, modEntry.AssetBundlePersistent); } } Log(""); }
internal static void AddModEntries(VersionManifest manifest) { if (!hasLoadedMods) { LoadMods(); } stopwatch.Start(); // there are no mods loaded, just return if (modLoadOrder == null || modLoadOrder.Count == 0) { return; } if (modEntries != null) { LogWithDate("Loading another manifest with already setup mod manifests."); foreach (var modEntry in modEntries) { AddModEntry(manifest, modEntry); } stopwatch.Stop(); Log(""); LogWithDate($"Done. Elapsed running time: {stopwatch.Elapsed.TotalSeconds} seconds\n"); return; } LogWithDate("Setting up mod manifests..."); var jsonMerges = new Dictionary <string, List <string> >(); modEntries = new List <ModDef.ManifestEntry>(); foreach (var modName in modLoadOrder) { if (!modManifest.ContainsKey(modName)) { continue; } Log($"\t{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)); List <string> types; if (typeCache.ContainsKey(fakeStreamingAssetsPath)) { types = typeCache[fakeStreamingAssetsPath]; } else { // get the type from the manifest var matchingEntries = manifest.FindAll(x => Path.GetFullPath(x.FilePath) == fakeStreamingAssetsPath); if (matchingEntries == null || matchingEntries.Count == 0) { 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; } types = new List <string>(); foreach (var existingEntry in matchingEntries) { types.Add(existingEntry.Type); } typeCache[fakeStreamingAssetsPath] = types; } if (Path.GetExtension(modEntry.Path).ToLower() == ".json" && modEntry.ShouldMergeJSON) { if (!typeCache.ContainsKey(fakeStreamingAssetsPath)) { Log($"\t\tUnable to determine type of {modEntry.Id}. Is there someone screwy with your this mod.json?"); continue; } 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 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 if (modEntry.Type == "Video") { var fileName = Path.GetFileName(modEntry.Path); if (fileName != null && File.Exists(modEntry.Path)) { Log($"\t\tVideo => {fileName}"); ModVideos.Add(fileName, 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); } } } // write type cache to disk WriteJsonFile(TypeCachePath, typeCache); // perform merges into cache LogWithDate("Doing merges..."); foreach (var jsonMerge in jsonMerges) { 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]; cacheEntry.Id = InferIDFromFile(cachePath); if (AddModEntry(manifest, cacheEntry)) { modEntries.Add(cacheEntry); } } // 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>(); 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 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(ModDBPath)) { File.Delete(ModDBPath); } File.Copy(Path.Combine(Path.Combine(StreamingAssetsDirectory, "MDD"), MDD_FILE_NAME), ModDBPath); dbCache = new Dictionary <string, DateTime>(); } // add needed files to db using (var metadataDatabase = new MetadataDatabase()) { foreach (var modEntry in modEntries) { if (modEntry.AddToDB && AddModEntryToDB(metadataDatabase, modEntry.Path, modEntry.Type)) { Log($"\tAdded/Updated {modEntry.Id} ({modEntry.Type})"); } } } // write db/type cache to disk WriteJsonFile(DBCachePath, dbCache); stopwatch.Stop(); Log(""); LogWithDate($"Done. Elapsed running time: {stopwatch.Elapsed.TotalSeconds} seconds\n"); }
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; }
private static VersionManifestEntry GetEntryFromCachedOrBTRLEntries(string id) { return(BTRLEntries.FindLast(x => x.Id == id)?.GetVersionManifestEntry() ?? CachedVersionManifest.Find(x => x.Id == id)); }