/*
* 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 Newtonsoft.Json;
using PeterHan.PLib.Core;
using PeterHan.PLib.UI;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using UnityEngine;
namespace PeterHan.PLib.Options {
///
/// Adds an "Options" screen to a mod in the Mods menu.
///
public sealed class POptions : PForwardedComponent {
///
/// The configuration file name used by default for classes that do not specify
/// otherwise. This file name is case sensitive.
///
public const string CONFIG_FILE_NAME = "config.json";
///
/// The maximum nested class depth which will be serialized in mod options to avoid
/// infinite loops.
///
public const int MAX_SERIALIZATION_DEPTH = 8;
///
/// The margins around the Options button.
///
internal static readonly RectOffset OPTION_BUTTON_MARGIN = new RectOffset(11, 11, 5, 5);
///
/// The shared mod configuration folder, which works between archived versions and
/// local/dev/Steam.
///
public const string SHARED_CONFIG_FOLDER = "config";
///
/// The version of this component. Uses the running PLib version.
///
internal static readonly Version VERSION = new Version(PVersion.VERSION);
///
/// The instantiated copy of this class.
///
internal static POptions Instance { get; private set; }
///
/// Applied to ModsScreen if mod options are registered, after BuildDisplay runs.
///
private static void BuildDisplay_Postfix(GameObject ___entryPrefab,
System.Collections.IEnumerable ___displayedMods) {
if (Instance != null) {
int index = 0;
// Harmony does not check the type at all on accessing private fields with ___
foreach (var displayedMod in ___displayedMods)
Instance.AddModOptions(displayedMod, index++, ___entryPrefab);
}
}
///
/// Retrieves the configuration file path used by PLib Options for a specified type.
///
/// The options type stored in the config file.
/// The path to the configuration file that will be used by PLib for that
/// mod's config.
public static string GetConfigFilePath(Type optionsType) {
return GetConfigPath(optionsType.GetCustomAttribute(),
optionsType.Assembly);
}
///
/// Attempts to find the mod which owns the specified type.
///
/// The type to look up.
/// The Mod that owns it, or null if no owning mod could be found, such as for
/// types in System or Assembly-CSharp.
internal static KMod.Mod GetModFromType(Type optionsType) {
if (optionsType == null)
throw new ArgumentNullException(nameof(optionsType));
var sd = PRegistry.Instance.GetSharedData(typeof(POptions).FullName);
// Look up mod in the shared data
if (!(sd is IDictionary lookup) || !lookup.TryGetValue(
optionsType.Assembly, out KMod.Mod result))
result = null;
return result;
}
///
/// Retrieves the configuration file path used by PLib Options for a specified type.
///
/// The config file attribute for that type.
/// The assembly to use for determining the path.
/// The path to the configuration file that will be used by PLib for that
/// mod's config.
private static string GetConfigPath(ConfigFileAttribute attr, Assembly modAssembly) {
string path, name = modAssembly.GetNameSafe();
path = (name != null && (attr?.UseSharedConfigLocation == true)) ?
Path.Combine(KMod.Manager.GetDirectory(), SHARED_CONFIG_FOLDER, name) :
PUtil.GetModPath(modAssembly);
return Path.Combine(path, attr?.ConfigFileName ?? CONFIG_FILE_NAME);
}
///
/// Reads a mod's settings from its configuration file. The assembly defining T is used
/// to resolve the proper settings folder.
///
/// The type of the settings object.
/// The settings read, or null if they could not be read (e.g. newly installed).
public static T ReadSettings() where T : class {
var type = typeof(T);
return ReadSettings(GetConfigPath(type.GetCustomAttribute(),
type.Assembly), type) as T;
}
///
/// Reads a mod's settings from its configuration file.
///
/// The path to the settings file.
/// The options type.
/// The settings read, or null if they could not be read (e.g. newly installed)
internal static object ReadSettings(string path, Type optionsType) {
object options = null;
try {
using (var jr = new JsonTextReader(File.OpenText(path))) {
var serializer = new JsonSerializer { MaxDepth = MAX_SERIALIZATION_DEPTH };
// Deserialize from stream avoids reading file text into memory
options = serializer.Deserialize(jr, optionsType);
}
} catch (FileNotFoundException) {
#if DEBUG
PUtil.LogDebug("{0} was not found; using default settings".F(Path.GetFileName(
path)));
#endif
} catch (UnauthorizedAccessException e) {
// Options will be set to defaults
PUtil.LogExcWarn(e);
} catch (IOException e) {
// Again set defaults
PUtil.LogExcWarn(e);
} catch (JsonException e) {
// Again set defaults
PUtil.LogExcWarn(e);
}
return options;
}
///
/// Shows a mod options dialog now, as if Options was used inside the Mods menu.
///
/// The type of the options to show. The mod to configure,
/// configuration directory, and so forth will be retrieved from the provided type.
/// This type must be the same type configured in RegisterOptions for the mod.
/// The method to call when the dialog is closed.
public static void ShowDialog(Type optionsType, Action