/* * Copyright 2020 Davis Cook * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System; using System.Collections.Generic; using System.IO; using System.Reflection; using HarmonyLib; using Klei; using PeterHan.PLib.Core; using CodexDictionary = System.Collections.Generic.Dictionary>; using WidgetMappingList = System.Collections.Generic.List>; namespace PeterHan.PLib.Database { /// /// Handles codex entries for mods by automatically loading YAML entries and subentries for /// critters and plants from the codex folder in their mod directories. /// /// The layerable files loader in the stock game is broken, so this class is required to /// correctly load new codex entries. /// public sealed class PCodexManager : PForwardedComponent { /// /// The subfolder from which critter codex entries are loaded. /// public const string CREATURES_DIR = "codex/Creatures"; /// /// The subfolder from which plant codex entries are loaded. /// public const string PLANTS_DIR = "codex/Plants"; /// /// The file extension used for codex entry/subentries. /// public const string CODEX_FILES = "*.yaml"; /// /// The codex category under which critter entries should go. /// public const string CREATURES_CATEGORY = "CREATURES"; /// /// The codex category under which plant entries should go. /// public const string PLANTS_CATEGORY = "PLANTS"; /// /// The version of this component. Uses the running PLib version. /// internal static readonly Version VERSION = new Version(PVersion.VERSION); /// /// Allow access to the private widget tag mappings field. /// Detouring sadly is not possible because CodexCache is a static class and cannot be /// a type parameter. /// private static readonly FieldInfo WIDGET_TAG_MAPPINGS = typeof(CodexCache). GetFieldSafe("widgetTagMappings", true); /// /// The instantiated copy of this class. /// internal static PCodexManager Instance { get; private set; } public override Version Version => VERSION; /// /// Applied to CodexCache to collect dynamic codex entries from the file system. /// private static void CollectEntries_Postfix(string folder, List __result, string ___baseEntryPath) { // Check to see if we are loading from either the "Creatures" directory or // "Plants" directory if (Instance != null) { string path = string.IsNullOrEmpty(folder) ? ___baseEntryPath : Path.Combine( ___baseEntryPath, folder); bool modified = false; if (path.EndsWith("Creatures")) { __result.AddRange(Instance.LoadEntries(CREATURES_CATEGORY)); modified = true; } if (path.EndsWith("Plants")) { __result.AddRange(Instance.LoadEntries(PLANTS_CATEGORY)); modified = true; } if (modified) { foreach (var codexEntry in __result) // Fill in a default sort string if necessary if (string.IsNullOrEmpty(codexEntry.sortString)) codexEntry.sortString = Strings.Get(codexEntry.title); __result.Sort((x, y) => x.sortString.CompareTo(y.sortString)); } } } /// /// Applied to CodexCache to collect dynamic codex sub entries from the file system. /// private static void CollectSubEntries_Postfix(List __result) { if (Instance != null) { int startSize = __result.Count; __result.AddRange(Instance.LoadSubEntries()); if (__result.Count != startSize) __result.Sort((x, y) => x.title.CompareTo(y.title)); } } /// /// Loads codex entries from the specified directory. /// /// The location where the data will be placed. /// The directory to load. /// The category to assign to each entry thus loaded. private static void LoadFromDirectory(ICollection entries, string dir, string category) { string[] codexFiles = new string[0]; try { // List codex data files in the codex directory codexFiles = Directory.GetFiles(dir, CODEX_FILES); } catch (UnauthorizedAccessException ex) { PUtil.LogExcWarn(ex); } catch (IOException ex) { PUtil.LogExcWarn(ex); } var widgetTagMappings = WIDGET_TAG_MAPPINGS?.GetValue(null) as WidgetMappingList; if (widgetTagMappings == null) PDatabaseUtils.LogDatabaseWarning("Unable to load codex files: no tag mappings found"); foreach (string str in codexFiles) try { string filename = str; var codexEntry = YamlIO.LoadFile(filename, YamlParseErrorCB, widgetTagMappings); if (codexEntry != null) { codexEntry.category = category; entries.Add(codexEntry); } } catch (IOException ex) { PDatabaseUtils.LogDatabaseWarning("Unable to load codex files from {0}:". F(dir)); PUtil.LogExcWarn(ex); } catch (InvalidDataException ex) { PUtil.LogException(ex); } #if DEBUG PDatabaseUtils.LogDatabaseDebug("Loaded codex entries from directory: {0}".F(dir)); #endif } /// /// Loads codex subentries from the specified directory. /// /// The location where the data will be placed. /// The directory to load. private static void LoadFromDirectory(ICollection entries, string dir) { string[] codexFiles = new string[0]; try { // List codex data files in the codex directory codexFiles = Directory.GetFiles(dir, CODEX_FILES, SearchOption. AllDirectories); } catch (UnauthorizedAccessException ex) { PUtil.LogExcWarn(ex); } catch (IOException ex) { PUtil.LogExcWarn(ex); } var widgetTagMappings = WIDGET_TAG_MAPPINGS?.GetValue(null) as WidgetMappingList; if (widgetTagMappings == null) PDatabaseUtils.LogDatabaseWarning("Unable to load codex files: no tag mappings found"); foreach (string filename in codexFiles) try { var subEntry = YamlIO.LoadFile(filename, YamlParseErrorCB, widgetTagMappings); if (entries != null) entries.Add(subEntry); } catch (IOException ex) { PDatabaseUtils.LogDatabaseWarning("Unable to load codex files from {0}:". F(dir)); PUtil.LogExcWarn(ex); } catch (InvalidDataException ex) { PUtil.LogException(ex); } #if DEBUG PDatabaseUtils.LogDatabaseDebug("Loaded codex sub entries from directory: {0}". F(dir)); #endif } /// /// A callback function for the YAML parser to process errors that it throws. /// /// The YAML parsing error internal static void YamlParseErrorCB(YamlIO.Error error, bool _) { throw new InvalidDataException(string.Format("{0} parse error in {1}\n{2}", error. severity, error.file.full_path, error.message), error.inner_exception); } /// /// The paths for creature codex entries. /// private readonly ISet creaturePaths; /// /// The paths for plant codex entries. /// private readonly ISet plantPaths; public PCodexManager() { creaturePaths = new HashSet(); plantPaths = new HashSet(); // Data is a hacky but usable 2 item dictionary InstanceData = new CodexDictionary(4) { { CREATURES_CATEGORY, creaturePaths }, { PLANTS_CATEGORY, plantPaths } }; PUtil.InitLibrary(false); PRegistry.Instance.AddCandidateVersion(this); } public override void Initialize(Harmony plibInstance) { Instance = this; plibInstance.Patch(typeof(CodexCache), nameof(CodexCache.CollectEntries), postfix: PatchMethod(nameof(CollectEntries_Postfix))); plibInstance.Patch(typeof(CodexCache), nameof(CodexCache.CollectSubEntries), postfix: PatchMethod(nameof(CollectSubEntries_Postfix))); } /// /// Loads all codex entries for all mods registered. /// /// The codex category under which these data entries should be loaded. /// The list of entries that were loaded. private IList LoadEntries(string category) { var entries = new List(32); var allMods = PRegistry.Instance.GetAllComponents(ID); if (allMods != null) foreach (var mod in allMods) { var codex = mod?.GetInstanceData(); if (codex != null && codex.TryGetValue(category, out ISet dirs)) foreach (var dir in dirs) LoadFromDirectory(entries, dir, category); } return entries; } /// /// Loads all codex subentries for all mods registered. /// /// The list of subentries that were loaded. private IList LoadSubEntries() { var entries = new List(32); var allMods = PRegistry.Instance.GetAllComponents(ID); if (allMods != null) foreach (var mod in allMods) { var codex = mod?.GetInstanceData(); if (codex != null) // Lots of nested for, but required! (entryType should only have // 2 values, and usually only one dir per mod) foreach (var entryType in codex) foreach (var dir in entryType.Value) LoadFromDirectory(entries, dir); } return entries; } /// /// Registers the calling mod as having custom creature codex entries. The entries will /// be read from the mod directory in the "codex/Creatures" subfolder. If the argument /// is omitted, the calling assembly is registered. /// /// The assembly to register as having creatures. public void RegisterCreatures(Assembly assembly = null) { if (assembly == null) assembly = Assembly.GetCallingAssembly(); string dir = Path.Combine(PUtil.GetModPath(assembly), CREATURES_DIR); creaturePaths.Add(dir); #if DEBUG PDatabaseUtils.LogDatabaseDebug("Registered codex creatures directory: {0}". F(dir)); #endif } /// /// Registers the calling mod as having custom plant codex entries. The entries will /// be read from the mod directory in the "codex/Plants" subfolder. If the argument /// is omitted, the calling assembly is registered. /// /// The assembly to register as having creatures. public void RegisterPlants(Assembly assembly = null) { if (assembly == null) assembly = Assembly.GetCallingAssembly(); string dir = Path.Combine(PUtil.GetModPath(assembly), PLANTS_DIR); plantPaths.Add(dir); #if DEBUG PDatabaseUtils.LogDatabaseDebug("Registered codex plants directory: {0}".F(dir)); #endif } } }