Initial commit
This commit is contained in:
316
mod/PLibDatabase/PCodexManager.cs
Normal file
316
mod/PLibDatabase/PCodexManager.cs
Normal file
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* 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<string, System.Collections.
|
||||
Generic.ISet<string>>;
|
||||
using WidgetMappingList = System.Collections.Generic.List<Tuple<string, System.Type>>;
|
||||
|
||||
namespace PeterHan.PLib.Database {
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class PCodexManager : PForwardedComponent {
|
||||
/// <summary>
|
||||
/// The subfolder from which critter codex entries are loaded.
|
||||
/// </summary>
|
||||
public const string CREATURES_DIR = "codex/Creatures";
|
||||
|
||||
/// <summary>
|
||||
/// The subfolder from which plant codex entries are loaded.
|
||||
/// </summary>
|
||||
public const string PLANTS_DIR = "codex/Plants";
|
||||
|
||||
/// <summary>
|
||||
/// The file extension used for codex entry/subentries.
|
||||
/// </summary>
|
||||
public const string CODEX_FILES = "*.yaml";
|
||||
|
||||
/// <summary>
|
||||
/// The codex category under which critter entries should go.
|
||||
/// </summary>
|
||||
public const string CREATURES_CATEGORY = "CREATURES";
|
||||
|
||||
/// <summary>
|
||||
/// The codex category under which plant entries should go.
|
||||
/// </summary>
|
||||
public const string PLANTS_CATEGORY = "PLANTS";
|
||||
|
||||
/// <summary>
|
||||
/// The version of this component. Uses the running PLib version.
|
||||
/// </summary>
|
||||
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static readonly FieldInfo WIDGET_TAG_MAPPINGS = typeof(CodexCache).
|
||||
GetFieldSafe("widgetTagMappings", true);
|
||||
|
||||
/// <summary>
|
||||
/// The instantiated copy of this class.
|
||||
/// </summary>
|
||||
internal static PCodexManager Instance { get; private set; }
|
||||
|
||||
public override Version Version => VERSION;
|
||||
|
||||
/// <summary>
|
||||
/// Applied to CodexCache to collect dynamic codex entries from the file system.
|
||||
/// </summary>
|
||||
private static void CollectEntries_Postfix(string folder, List<CodexEntry> __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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applied to CodexCache to collect dynamic codex sub entries from the file system.
|
||||
/// </summary>
|
||||
private static void CollectSubEntries_Postfix(List<SubEntry> __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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads codex entries from the specified directory.
|
||||
/// </summary>
|
||||
/// <param name="entries">The location where the data will be placed.</param>
|
||||
/// <param name="dir">The directory to load.</param>
|
||||
/// <param name="category">The category to assign to each entry thus loaded.</param>
|
||||
private static void LoadFromDirectory(ICollection<CodexEntry> 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<CodexEntry>(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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads codex subentries from the specified directory.
|
||||
/// </summary>
|
||||
/// <param name="entries">The location where the data will be placed.</param>
|
||||
/// <param name="dir">The directory to load.</param>
|
||||
private static void LoadFromDirectory(ICollection<SubEntry> 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<SubEntry>(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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A callback function for the YAML parser to process errors that it throws.
|
||||
/// </summary>
|
||||
/// <param name="error">The YAML parsing error</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The paths for creature codex entries.
|
||||
/// </summary>
|
||||
private readonly ISet<string> creaturePaths;
|
||||
|
||||
/// <summary>
|
||||
/// The paths for plant codex entries.
|
||||
/// </summary>
|
||||
private readonly ISet<string> plantPaths;
|
||||
|
||||
public PCodexManager() {
|
||||
creaturePaths = new HashSet<string>();
|
||||
plantPaths = new HashSet<string>();
|
||||
// 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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all codex entries for all mods registered.
|
||||
/// </summary>
|
||||
/// <param name="category">The codex category under which these data entries should be loaded.</param>
|
||||
/// <returns>The list of entries that were loaded.</returns>
|
||||
private IList<CodexEntry> LoadEntries(string category) {
|
||||
var entries = new List<CodexEntry>(32);
|
||||
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||
if (allMods != null)
|
||||
foreach (var mod in allMods) {
|
||||
var codex = mod?.GetInstanceData<CodexDictionary>();
|
||||
if (codex != null && codex.TryGetValue(category, out ISet<string> dirs))
|
||||
foreach (var dir in dirs)
|
||||
LoadFromDirectory(entries, dir, category);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads all codex subentries for all mods registered.
|
||||
/// </summary>
|
||||
/// <returns>The list of subentries that were loaded.</returns>
|
||||
private IList<SubEntry> LoadSubEntries() {
|
||||
var entries = new List<SubEntry>(32);
|
||||
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||
if (allMods != null)
|
||||
foreach (var mod in allMods) {
|
||||
var codex = mod?.GetInstanceData<CodexDictionary>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to register as having creatures.</param>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to register as having creatures.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
172
mod/PLibDatabase/PColonyAchievement.cs
Normal file
172
mod/PLibDatabase/PColonyAchievement.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* 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 Database;
|
||||
using FMODUnity;
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.Detours;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PeterHan.PLib.Database {
|
||||
/// <summary>
|
||||
/// A wrapper class used to create ColonyAchievement instances.
|
||||
/// </summary>
|
||||
public sealed class PColonyAchievement {
|
||||
/// <summary>
|
||||
/// Prototypes the new ColonyAchievment constructor. This one is a monster with a
|
||||
/// zillion parallel parameters used only for victory animations (Klei please!) and
|
||||
/// gets changed often enough to warrant a detour.
|
||||
/// </summary>
|
||||
private delegate ColonyAchievement NewColonyAchievement(string Id,
|
||||
string platformAchievementId, string Name, string description,
|
||||
bool isVictoryCondition, List<ColonyAchievementRequirement> requirementChecklist,
|
||||
string messageTitle, string messageBody, string videoDataName,
|
||||
string victoryLoopVideo, Action<KMonoBehaviour> VictorySequence);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new colony achievement.
|
||||
/// </summary>
|
||||
private static readonly NewColonyAchievement NEW_COLONY_ACHIEVEMENT =
|
||||
typeof(ColonyAchievement).DetourConstructor<NewColonyAchievement>();
|
||||
|
||||
/// <summary>
|
||||
/// The achievement description (string, not a string key!)
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The icon to use for the achievement.
|
||||
/// </summary>
|
||||
public string Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The achievement ID.
|
||||
/// </summary>
|
||||
public string ID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this colony achievement is considered a victory achievement.
|
||||
///
|
||||
/// Victory achievements are displayed at the top, and can play a movie when they
|
||||
/// are satisfied.
|
||||
/// </summary>
|
||||
public bool IsVictory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The achievement display name (string, not a string key!)
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The callback triggered if this achievement is a victory achievement when it is
|
||||
/// completed.
|
||||
/// </summary>
|
||||
public Action<KMonoBehaviour> OnVictory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The requirements for this achievement.
|
||||
/// </summary>
|
||||
public List<ColonyAchievementRequirement> Requirements { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member is obsolete since the Sweet Dreams update. Use VictoryAudioSnapshoRef
|
||||
/// instead.
|
||||
/// </summary>
|
||||
[Obsolete("Set victory audio snapshot directly due to Klei changes in the Sweet Dreams update")]
|
||||
public string VictoryAudioSnapshot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message body to display when this achievement triggers.
|
||||
///
|
||||
/// The game does not use this field by default, but it is available for victory
|
||||
/// callbacks.
|
||||
/// </summary>
|
||||
public string VictoryMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message title to display when this achievement triggers.
|
||||
///
|
||||
/// The game does not use this field by default, but it is available for victory
|
||||
/// callbacks.
|
||||
/// </summary>
|
||||
public string VictoryTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The video data file to play when this achievement triggers.
|
||||
///
|
||||
/// The game does not use this field by default, but it is available for victory
|
||||
/// callbacks.
|
||||
/// </summary>
|
||||
public string VictoryVideoData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The video data file to loop behind the message when this achievement triggers.
|
||||
///
|
||||
/// The game does not use this field by default, but it is available for victory
|
||||
/// callbacks.
|
||||
/// </summary>
|
||||
public string VictoryVideoLoop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new colony achievement wrapper.
|
||||
/// </summary>
|
||||
/// <param name="id">The achievement ID.</param>
|
||||
public PColonyAchievement(string id) {
|
||||
if (string.IsNullOrEmpty(id))
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
Description = "";
|
||||
Icon = "";
|
||||
ID = id;
|
||||
IsVictory = false;
|
||||
Name = "";
|
||||
OnVictory = null;
|
||||
Requirements = null;
|
||||
VictoryMessage = "";
|
||||
VictoryTitle = "";
|
||||
VictoryVideoData = "";
|
||||
VictoryVideoLoop = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and adds the achievement to the database. As platform achievements cannot
|
||||
/// be added using mods, the platform achievement ID will always be empty.
|
||||
/// </summary>
|
||||
public void AddAchievement() {
|
||||
if (Requirements == null)
|
||||
throw new ArgumentNullException("No colony achievement requirements specified");
|
||||
var achieve = NEW_COLONY_ACHIEVEMENT.Invoke(ID, "", Name, Description, IsVictory,
|
||||
Requirements, VictoryTitle, VictoryMessage, VictoryVideoData, VictoryVideoLoop,
|
||||
OnVictory);
|
||||
achieve.icon = Icon;
|
||||
PDatabaseUtils.AddColonyAchievement(achieve);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj) {
|
||||
return obj is PColonyAchievement other && ID == other.ID;
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
return ID.GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "PColonyAchievement[ID={0},Name={1}]".F(ID, Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
mod/PLibDatabase/PDatabaseUtils.cs
Normal file
73
mod/PLibDatabase/PDatabaseUtils.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* 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 Database;
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.Database {
|
||||
/// <summary>
|
||||
/// Functions which deal with entries in the game database and strings.
|
||||
/// </summary>
|
||||
public static class PDatabaseUtils {
|
||||
/// <summary>
|
||||
/// Adds a colony achievement to the colony summary screen. Must be invoked after the
|
||||
/// database is initialized (Db.Initialize() postfix recommended).
|
||||
///
|
||||
/// Note that achievement structures significantly changed from Vanilla to the DLC.
|
||||
/// </summary>
|
||||
/// <param name="achievement">The achievement to add.</param>
|
||||
public static void AddColonyAchievement(ColonyAchievement achievement) {
|
||||
if (achievement == null)
|
||||
throw new ArgumentNullException(nameof(achievement));
|
||||
Db.Get()?.ColonyAchievements?.resources?.Add(achievement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the name and description for a status item.
|
||||
///
|
||||
/// Must be used before the StatusItem is first instantiated.
|
||||
/// </summary>
|
||||
/// <param name="id">The status item ID.</param>
|
||||
/// <param name="category">The status item category.</param>
|
||||
/// <param name="name">The name to display in the UI.</param>
|
||||
/// <param name="desc">The description to display in the UI.</param>
|
||||
public static void AddStatusItemStrings(string id, string category, string name,
|
||||
string desc) {
|
||||
string uid = id.ToUpperInvariant();
|
||||
string ucategory = category.ToUpperInvariant();
|
||||
Strings.Add("STRINGS." + ucategory + ".STATUSITEMS." + uid + ".NAME", name);
|
||||
Strings.Add("STRINGS." + ucategory + ".STATUSITEMS." + uid + ".TOOLTIP", desc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a message encountered by the PLib database system.
|
||||
/// </summary>
|
||||
/// <param name="message">The debug message.</param>
|
||||
internal static void LogDatabaseDebug(string message) {
|
||||
Debug.LogFormat("[PLibDatabase] {0}", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a warning encountered by the PLib database system.
|
||||
/// </summary>
|
||||
/// <param name="message">The warning message.</param>
|
||||
internal static void LogDatabaseWarning(string message) {
|
||||
Debug.LogWarningFormat("[PLibDatabase] {0}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
mod/PLibDatabase/PLibDatabase.csproj
Normal file
20
mod/PLibDatabase/PLibDatabase.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Title>PLib Database</Title>
|
||||
<AssemblyTitle>PLib.Database</AssemblyTitle>
|
||||
<Version>4.11.0.0</Version>
|
||||
<UsesPLib>false</UsesPLib>
|
||||
<RootNamespace>PeterHan.PLib.Database</RootNamespace>
|
||||
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||
<DistributeMod>false</DistributeMod>
|
||||
<PLibCore>true</PLibCore>
|
||||
<Platforms>Vanilla;Mergedown</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\$(Platform)\Release\PLibDatabase.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
155
mod/PLibDatabase/PLocalization.cs
Normal file
155
mod/PLibDatabase/PLocalization.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* 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 HarmonyLib;
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace PeterHan.PLib.Database {
|
||||
/// <summary>
|
||||
/// Handles localization for mods by automatically loading po files from the translations
|
||||
/// folder in their mod directories.
|
||||
/// </summary>
|
||||
public sealed class PLocalization : PForwardedComponent {
|
||||
/// <summary>
|
||||
/// The subfolder from which translations will be loaded.
|
||||
/// </summary>
|
||||
public const string TRANSLATIONS_DIR = "translations";
|
||||
|
||||
/// <summary>
|
||||
/// The version of this component. Uses the running PLib version.
|
||||
/// </summary>
|
||||
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||
|
||||
/// <summary>
|
||||
/// Localizes the specified mod assembly.
|
||||
/// </summary>
|
||||
/// <param name="modAssembly">The assembly to localize.</param>
|
||||
/// <param name="locale">The locale file name to be used.</param>
|
||||
private static void Localize(Assembly modAssembly, Localization.Locale locale) {
|
||||
string path = PUtil.GetModPath(modAssembly);
|
||||
string locCode = locale.Code;
|
||||
if (string.IsNullOrEmpty(locCode))
|
||||
locCode = Localization.GetCurrentLanguageCode();
|
||||
var poFile = Path.Combine(Path.Combine(path, TRANSLATIONS_DIR), locCode +
|
||||
PLibLocalization.TRANSLATIONS_EXT);
|
||||
try {
|
||||
Localization.OverloadStrings(Localization.LoadStringsFile(poFile, false));
|
||||
RewriteStrings(modAssembly);
|
||||
} catch (FileNotFoundException) {
|
||||
// No localization available for this locale
|
||||
#if DEBUG
|
||||
PDatabaseUtils.LogDatabaseDebug("No {0} localization available for mod {1}".F(
|
||||
locCode, modAssembly.GetNameSafe() ?? "?"));
|
||||
#endif
|
||||
} catch (DirectoryNotFoundException) {
|
||||
} catch (IOException e) {
|
||||
PDatabaseUtils.LogDatabaseWarning("Failed to load {0} localization for mod {1}:".
|
||||
F(locCode, modAssembly.GetNameSafe() ?? "?"));
|
||||
PUtil.LogExcWarn(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches types in the assembly (no worries, Localization did this anyways, so they
|
||||
/// all either loaded or failed to load) for fields that already had loc string keys
|
||||
/// created, and fixes them if so.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to check for strings.</param>
|
||||
internal static void RewriteStrings(Assembly assembly) {
|
||||
foreach (var type in assembly.GetTypes())
|
||||
foreach (var field in type.GetFields(PPatchTools.BASE_FLAGS | BindingFlags.
|
||||
FlattenHierarchy | BindingFlags.Static)) {
|
||||
// Only use fields of type LocString
|
||||
if (field.FieldType == typeof(LocString) && field.GetValue(null) is
|
||||
LocString ls) {
|
||||
#if DEBUG
|
||||
PDatabaseUtils.LogDatabaseDebug("Rewrote string {0}: {1} to {2}".F(ls.
|
||||
key.String, Strings.Get(ls.key.String), ls.text));
|
||||
#endif
|
||||
Strings.Add(ls.key.String, ls.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override Version Version => VERSION;
|
||||
|
||||
/// <summary>
|
||||
/// The assemblies to be localized.
|
||||
/// </summary>
|
||||
private readonly ICollection<Assembly> toLocalize;
|
||||
|
||||
public PLocalization() {
|
||||
toLocalize = new List<Assembly>(4);
|
||||
InstanceData = toLocalize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debug dumps the translation templates for ALL registered PLib localized mods.
|
||||
/// </summary>
|
||||
internal void DumpAll() {
|
||||
var allMods = PRegistry.Instance.GetAllComponents(ID);
|
||||
if (allMods != null)
|
||||
foreach (var toDump in allMods) {
|
||||
// Reach for those assemblies
|
||||
var assemblies = toDump.GetInstanceData<ICollection<Assembly>>();
|
||||
if (assemblies != null)
|
||||
foreach (var modAssembly in assemblies)
|
||||
ModUtil.RegisterForTranslation(modAssembly.GetTypes()[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize(Harmony plibInstance) {
|
||||
// PLibLocalization will invoke Process here
|
||||
}
|
||||
|
||||
public override void Process(uint operation, object _) {
|
||||
var locale = Localization.GetLocale();
|
||||
if (locale != null && operation == 0)
|
||||
foreach (var modAssembly in toLocalize)
|
||||
Localize(modAssembly, locale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified assembly for automatic PLib localization. If the argument
|
||||
/// is omitted, the calling assembly is registered.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to register for PLib localization.</param>
|
||||
public void Register(Assembly assembly = null) {
|
||||
if (assembly == null)
|
||||
assembly = Assembly.GetCallingAssembly();
|
||||
var types = assembly.GetTypes();
|
||||
if (types == null || types.Length == 0)
|
||||
PDatabaseUtils.LogDatabaseWarning("Registered assembly " + assembly.
|
||||
GetNameSafe() + " that had no types for localization!");
|
||||
else {
|
||||
RegisterForForwarding();
|
||||
toLocalize.Add(assembly);
|
||||
// This call searches all types in the assembly implicitly
|
||||
Localization.RegisterForTranslation(types[0]);
|
||||
#if DEBUG
|
||||
PDatabaseUtils.LogDatabaseDebug("Localizing assembly {0} using base namespace {1}".
|
||||
F(assembly.GetNameSafe(), types[0].Namespace));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user