diff --git a/Directory.Build.props b/Directory.Build.props index a2be5c89..e2aa9f15 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.5.4 + 1.6.0 netstandard2.1 SpaceWarp 11 diff --git a/SpaceWarp.Core/API/Configuration/BepInExConfigEntry.cs b/SpaceWarp.Core/API/Configuration/BepInExConfigEntry.cs index b84977f6..1901d908 100644 --- a/SpaceWarp.Core/API/Configuration/BepInExConfigEntry.cs +++ b/SpaceWarp.Core/API/Configuration/BepInExConfigEntry.cs @@ -8,17 +8,23 @@ namespace SpaceWarp.API.Configuration; public class BepInExConfigEntry : IConfigEntry { public readonly ConfigEntryBase EntryBase; - - public BepInExConfigEntry(ConfigEntryBase entryBase) + public event Action Callbacks; + public BepInExConfigEntry(ConfigEntryBase entryBase, IValueConstraint constraint = null) { EntryBase = entryBase; + Constraint = constraint; } public object Value { get => EntryBase.BoxedValue; - set => EntryBase.BoxedValue = value; + set + { + Callbacks?.Invoke(EntryBase.BoxedValue, value); + EntryBase.BoxedValue = value; + } } + public Type ValueType => EntryBase.SettingType; public T Get() where T : class @@ -38,8 +44,18 @@ public void Set(T value) throw new InvalidCastException($"Cannot cast {ValueType} to {typeof(T)}"); } + if (Constraint != null) + { + if (!Constraint.IsConstrained(value)) return; + } + Callbacks?.Invoke(EntryBase.BoxedValue, value); EntryBase.BoxedValue = Convert.ChangeType(value, ValueType); } public string Description => EntryBase.Description.Description; + public IValueConstraint Constraint { get; } + public void RegisterCallback(Action valueChangedCallback) + { + Callbacks += valueChangedCallback; + } } \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/BepInExConfigFile.cs b/SpaceWarp.Core/API/Configuration/BepInExConfigFile.cs index 720a2ff3..a1a5c60f 100644 --- a/SpaceWarp.Core/API/Configuration/BepInExConfigFile.cs +++ b/SpaceWarp.Core/API/Configuration/BepInExConfigFile.cs @@ -27,6 +27,12 @@ public IConfigEntry Bind(string section, string key, T defaultValue = default return new BepInExConfigEntry(AdaptedConfigFile.Bind(section, key, defaultValue, description)); } + public IConfigEntry Bind(string section, string key, T defaultValue, string description, IValueConstraint valueConstraint) + { + return new BepInExConfigEntry(AdaptedConfigFile.Bind(new ConfigDefinition(section, key), defaultValue, + new ConfigDescription(description, valueConstraint.ToAcceptableValueBase()))); + } + public IReadOnlyList Sections => AdaptedConfigFile.Keys.Select(x => x.Section).Distinct().ToList(); public IReadOnlyList this[string section] => AdaptedConfigFile.Keys.Where(x => x.Section == section) diff --git a/SpaceWarp.Core/API/Configuration/ConfigValue.cs b/SpaceWarp.Core/API/Configuration/ConfigValue.cs index 0eb6b192..b4a54278 100644 --- a/SpaceWarp.Core/API/Configuration/ConfigValue.cs +++ b/SpaceWarp.Core/API/Configuration/ConfigValue.cs @@ -20,6 +20,21 @@ public ConfigValue(IConfigEntry entry) public T Value { get => (T)Entry.Value; - set => Entry.Value = value; + set + { + Entry.Value = value; + } + } + + public void RegisterCallback(Action callback) + { + // Callbacks += callback; + Entry.RegisterCallback(NewCallback); + return; + + void NewCallback(object from, object to) + { + callback.Invoke((T)from, (T)to); + } } } \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/EmptyConfigFile.cs b/SpaceWarp.Core/API/Configuration/EmptyConfigFile.cs index 7be78914..503d8914 100644 --- a/SpaceWarp.Core/API/Configuration/EmptyConfigFile.cs +++ b/SpaceWarp.Core/API/Configuration/EmptyConfigFile.cs @@ -17,6 +17,11 @@ public IConfigEntry Bind(string section, string key, T defaultValue = default throw new System.NotImplementedException(); } + public IConfigEntry Bind(string section, string key, T defaultValue, string description, IValueConstraint valueConstraint) + { + throw new System.NotImplementedException(); + } + public IReadOnlyList Sections => new List(); public IReadOnlyList this[string section] => throw new KeyNotFoundException($"{section}"); diff --git a/SpaceWarp.Core/API/Configuration/IConfigEntry.cs b/SpaceWarp.Core/API/Configuration/IConfigEntry.cs index 36dba68d..f5f062a9 100644 --- a/SpaceWarp.Core/API/Configuration/IConfigEntry.cs +++ b/SpaceWarp.Core/API/Configuration/IConfigEntry.cs @@ -12,4 +12,12 @@ public interface IConfigEntry public void Set(T value); public string Description { get; } + + public IValueConstraint Constraint { get; } + + /// + /// Called when setting the value on a config file + /// + /// An action that takes te old value and the new value and calls a callback + public void RegisterCallback(Action valueChangedCallback); } \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/IConfigFile.cs b/SpaceWarp.Core/API/Configuration/IConfigFile.cs index 7247c455..87aedf23 100644 --- a/SpaceWarp.Core/API/Configuration/IConfigFile.cs +++ b/SpaceWarp.Core/API/Configuration/IConfigFile.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using JetBrains.Annotations; namespace SpaceWarp.API.Configuration; @@ -11,7 +12,7 @@ public interface IConfigFile public IConfigEntry this[string section, string key] { get; } public IConfigEntry Bind(string section, string key, T defaultValue = default, string description = ""); - + public IReadOnlyList Sections { get; } public IReadOnlyList this[string section] { get; } } \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/IValueConstraint.cs b/SpaceWarp.Core/API/Configuration/IValueConstraint.cs new file mode 100644 index 00000000..4256ceba --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/IValueConstraint.cs @@ -0,0 +1,9 @@ +using BepInEx.Configuration; + +namespace SpaceWarp.API.Configuration; + +public interface IValueConstraint +{ + public bool IsConstrained(object o); + public AcceptableValueBase ToAcceptableValueBase(); +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/JsonConfigEntry.cs b/SpaceWarp.Core/API/Configuration/JsonConfigEntry.cs index 07453942..977f910c 100644 --- a/SpaceWarp.Core/API/Configuration/JsonConfigEntry.cs +++ b/SpaceWarp.Core/API/Configuration/JsonConfigEntry.cs @@ -8,11 +8,14 @@ public class JsonConfigEntry : IConfigEntry { private readonly JsonConfigFile _configFile; private object _value; + public event Action Callbacks; + - public JsonConfigEntry(JsonConfigFile configFile, Type type, string description, object value) + public JsonConfigEntry(JsonConfigFile configFile, Type type, string description, object value, IValueConstraint constraint = null) { _configFile = configFile; _value = value; + Constraint = constraint; Description = description; ValueType = type; } @@ -23,6 +26,7 @@ public object Value get => _value; set { + Callbacks?.Invoke(_value, value); _value = value; _configFile.Save(); } @@ -45,9 +49,17 @@ public void Set(T value) { throw new InvalidCastException($"Cannot cast {ValueType} to {typeof(T)}"); } - + if (Constraint != null) + { + if (!Constraint.IsConstrained(value)) return; + } Value = Convert.ChangeType(value, ValueType); } public string Description { get; } + public IValueConstraint Constraint { get; } + public void RegisterCallback(Action valueChangedCallback) + { + Callbacks += valueChangedCallback; + } } \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/JsonConfigFile.cs b/SpaceWarp.Core/API/Configuration/JsonConfigFile.cs index 8dd50620..aafbfc8b 100644 --- a/SpaceWarp.Core/API/Configuration/JsonConfigFile.cs +++ b/SpaceWarp.Core/API/Configuration/JsonConfigFile.cs @@ -16,7 +16,7 @@ public class JsonConfigFile : IConfigFile { [CanBeNull] private JObject _previousConfigObject; - private Dictionary> _currentEntries = new(); + internal Dictionary> CurrentEntries = new(); private readonly string _file; public JsonConfigFile(string file) @@ -40,11 +40,11 @@ public JsonConfigFile(string file) public void Save() { - if (!_currentEntries.Any(value => value.Value.Count > 0)) return; + if (!CurrentEntries.Any(value => value.Value.Count > 0)) return; var result = new StringBuilder(); result.AppendLine("{"); var hadPreviousSection = false; - foreach (var section in _currentEntries.Where(section => section.Value.Count > 0)) + foreach (var section in CurrentEntries.Where(section => section.Value.Count > 0)) { hadPreviousSection = DumpSection(hadPreviousSection, result, section); } @@ -128,15 +128,15 @@ private static bool DumpEntry(StringBuilder result, bool hadPreviousKey, KeyValu return true; } - public IConfigEntry this[string section, string key] => _currentEntries[section][key]; + public IConfigEntry this[string section, string key] => CurrentEntries[section][key]; public IConfigEntry Bind(string section, string key, T defaultValue = default, string description = "") { // So now we have to check if its already bound, and/or if the previous config object has it - if (!_currentEntries.TryGetValue(section, out var previousSection)) + if (!CurrentEntries.TryGetValue(section, out var previousSection)) { previousSection = new Dictionary(); - _currentEntries.Add(section,previousSection); + CurrentEntries.Add(section,previousSection); } if (previousSection.TryGetValue(key, out var result)) @@ -173,7 +173,7 @@ public IConfigEntry Bind(string section, string key, T defaultValue = default return previousSection[key]; } - public IReadOnlyList Sections => _currentEntries.Keys.ToList(); + public IReadOnlyList Sections => CurrentEntries.Keys.ToList(); - public IReadOnlyList this[string section] => _currentEntries[section].Keys.ToList(); + public IReadOnlyList this[string section] => CurrentEntries[section].Keys.ToList(); } \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/ListConstraint.cs b/SpaceWarp.Core/API/Configuration/ListConstraint.cs new file mode 100644 index 00000000..b9610ee4 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/ListConstraint.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using BepInEx.Configuration; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +public class ListConstraint : ValueConstraint where T : IEquatable +{ + [PublicAPI] + public List AcceptableValues; + + public ListConstraint(IEnumerable acceptableValues) + { + AcceptableValues = acceptableValues.ToList(); + } + + public ListConstraint(params T[] acceptableValues) + { + AcceptableValues = acceptableValues.ToList(); + } + + public override bool IsValid(T o) => AcceptableValues.Any(x => x.Equals(o)); + public override AcceptableValueBase ToAcceptableValueBase() + { + return new AcceptableValueList(AcceptableValues.ToArray()); + } + + public override string ToString() + { + StringBuilder sb = new(); + sb.Append("["); + for (int i = 0; i < AcceptableValues.Count; i++) + { + if (i != 0) + { + sb.Append(", "); + } + + sb.Append(AcceptableValues[i]); + } + + sb.Append("]"); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/RangeConstraint.cs b/SpaceWarp.Core/API/Configuration/RangeConstraint.cs new file mode 100644 index 00000000..00fbc08d --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/RangeConstraint.cs @@ -0,0 +1,30 @@ +using System; +using BepInEx.Configuration; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +public class RangeConstraint : ValueConstraint where T : IComparable, IComparable +{ + [PublicAPI] + public T Minimum; + [PublicAPI] + public T Maximum; + + public RangeConstraint(T minimum, T maximum) + { + Minimum = minimum; + Maximum = maximum; + } + + public override bool IsValid(T o) => Minimum.CompareTo(o) <= 0 && Maximum.CompareTo(o) >= 0; + public override AcceptableValueBase ToAcceptableValueBase() + { + return new AcceptableValueRange(Minimum, Maximum); + } + + public override string ToString() + { + return $"{Minimum} - {Maximum}"; + } +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/ValueConstraint.cs b/SpaceWarp.Core/API/Configuration/ValueConstraint.cs new file mode 100644 index 00000000..ddabf763 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/ValueConstraint.cs @@ -0,0 +1,19 @@ +using BepInEx.Configuration; + +namespace SpaceWarp.API.Configuration; + +public abstract class ValueConstraint : IValueConstraint +{ + public abstract bool IsValid(T o); + public bool IsValid(object o) + { + return IsValid((T)o); + } + + public bool IsConstrained(object o) + { + return IsValid((T)o); + } + + public abstract AcceptableValueBase ToAcceptableValueBase(); +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs b/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs new file mode 100644 index 00000000..792d0ee1 --- /dev/null +++ b/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs @@ -0,0 +1,83 @@ +using JetBrains.Annotations; +using SpaceWarp.Backend.SaveGameManager; +using System; +using System.Collections.Generic; +using SpaceWarp.API.Logging; + +namespace SpaceWarp.API.SaveGameManager; + +[PublicAPI] +public static class ModSaves +{ + private static readonly ILogger Logger = new UnityLogSource("SpaceWarp.ModSaves"); + + internal static List InternalPluginSaveData = new(); + + /// + /// Registers your mod data for saving and loading events. + /// + /// Any object + /// Your mod GUID. Or, technically, any kind of string can be passed here, but what is mandatory is that it's unique compared to what other mods will use. + /// Function that will execute when a SAVE event is triggered. Defaults to null or no callback. + /// Function that will execute when a LOAD event is triggered. Defaults to null or no callback. + /// Your object that will be saved to a save file during a save event and that will be updated when a load event pulls new data. Ensure that a new instance of this object is NOT created after registration. + /// T saveData object you passed as a parameter, or a default instance of object T if you didn't pass anything + public static T RegisterSaveLoadGameData(string modGuid, Action onSave = null, Action onLoad = null, T saveData = default) + { + // Check if this GUID is already registered + if (InternalPluginSaveData.Find(p => p.ModGuid == modGuid) != null) + { + throw new ArgumentException($"Mod GUID '{modGuid}' is already registered. Skipping.", nameof(modGuid)); + } + + saveData ??= Activator.CreateInstance(); + + InternalPluginSaveData.Add(new PluginSaveData { ModGuid = modGuid, SaveEventCallback = SaveCallbackAdapter, LoadEventCallback = LoadCallbackAdapter, SaveData = saveData }); + Logger.LogInfo($"Registered '{modGuid}' for save/load events."); + return saveData; + + void LoadCallbackAdapter(object dataToBeLoaded) + { + if (onLoad != null && dataToBeLoaded is T data) + { + onLoad(data); + } + } + + // Create adapter functions to convert Action to CallbackFunctionDelegate + void SaveCallbackAdapter(object dataToBeSaved) + { + if (onSave != null && dataToBeSaved is T data) + { + onSave(data); + } + } + } + + /// + /// Unregister your previously registered mod data for saving and loading. Use this if you no longer need your data to be saved and loaded. + /// + /// Your mod GUID you used when registering. + public static void UnRegisterSaveLoadGameData(string modGuid) + { + var toRemove = InternalPluginSaveData.Find(p => p.ModGuid == modGuid); + if (toRemove == null) return; + InternalPluginSaveData.Remove(toRemove); + Logger.LogInfo($"Unregistered '{modGuid}' for save/load events."); + } + + /// + /// Unregisters then again registers your mod data for saving and loading events + /// + /// Any object + /// Your mod GUID. Or, technically, any kind of string can be passed here, but what is mandatory is that it's unique compared to what other mods will use. + /// Function that will execute when a SAVE event is triggered. Defaults to null or no callback. + /// Function that will execute when a LOAD event is triggered. Defaults to null or no callback. + /// Your object that will be saved to a save file during a save event and that will be updated when a load event pulls new data. Ensure that a new instance of this object is NOT created after registration. + /// T saveData object you passed as a parameter, or a default instance of object T if you didn't pass anything + public static T ReregisterSaveLoadGameData(string modGuid, Action onSave = null, Action onLoad = null, T saveData = default(T)) + { + UnRegisterSaveLoadGameData(modGuid); + return RegisterSaveLoadGameData(modGuid, onSave, onLoad, saveData); + } +} diff --git a/SpaceWarp.Core/Backend/SaveGameManager/PluginSaveData.cs b/SpaceWarp.Core/Backend/SaveGameManager/PluginSaveData.cs new file mode 100644 index 00000000..768e5bf3 --- /dev/null +++ b/SpaceWarp.Core/Backend/SaveGameManager/PluginSaveData.cs @@ -0,0 +1,17 @@ +using System; + +namespace SpaceWarp.Backend.SaveGameManager; + +internal delegate void SaveGameCallbackFunctionDelegate(object data); + +[Serializable] +public class PluginSaveData +{ + public string ModGuid { get; set; } + public object SaveData { get; set; } + + [NonSerialized] + internal SaveGameCallbackFunctionDelegate SaveEventCallback; + [NonSerialized] + internal SaveGameCallbackFunctionDelegate LoadEventCallback; +} diff --git a/SpaceWarp.Core/Backend/SaveGameManager/SpaceWarpSerializedSavedGame.cs b/SpaceWarp.Core/Backend/SaveGameManager/SpaceWarpSerializedSavedGame.cs new file mode 100644 index 00000000..a777997c --- /dev/null +++ b/SpaceWarp.Core/Backend/SaveGameManager/SpaceWarpSerializedSavedGame.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using UnityEngine.Serialization; + +namespace SpaceWarp.Backend.SaveGameManager; + +/// +/// Extension of game's save/load data class +/// +[Serializable] +public class SpaceWarpSerializedSavedGame : KSP.Sim.SerializedSavedGame +{ + public List serializedPluginSaveData = new(); +} diff --git a/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs b/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs index 46db62f8..55accc73 100644 --- a/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs +++ b/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs @@ -1,4 +1,7 @@ +using System.Reflection; +using System; using UnityEngine; +using System.Collections; namespace SpaceWarp.InternalUtilities; @@ -9,4 +12,39 @@ internal static void Persist(this UnityObject obj) UnityObject.DontDestroyOnLoad(obj); obj.hideFlags |= HideFlags.HideAndDontSave; } + + internal static void CopyFieldAndPropertyDataFromSourceToTargetObject(object source, object target) + { + // check if it's a dictionary + if (source is IDictionary sourceDictionary && target is IDictionary targetDictionary) + { + // copy dictionary items + foreach (DictionaryEntry entry in sourceDictionary) + { + targetDictionary[entry.Key] = entry.Value; + } + } + else + { + // copy fields + foreach (FieldInfo field in source.GetType().GetFields()) + { + object value = field.GetValue(source); + + try + { + field.SetValue(target, value); + } + catch (FieldAccessException) + { /* some fields are constants */ } + } + + // copy properties + foreach (PropertyInfo property in source.GetType().GetProperties()) + { + object value = property.GetValue(source); + property.SetValue(target, value); + } + } + } } \ No newline at end of file diff --git a/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs b/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs index 47dac15c..cfb2dbce 100644 --- a/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs @@ -21,8 +21,21 @@ public AddressableAction(string name, string label, Action action) : base(nam Action = action; } + private bool DoesLabelExist(object label) + { + return GameManager.Instance.Assets._registeredResourceLocators.Any(locator => locator.Keys.Contains(label)) + || Addressables.ResourceLocators.Any(locator => locator.Keys.Contains(label)); + } + public override void DoAction(Action resolve, Action reject) { + if (!DoesLabelExist(Label)) + { + Debug.Log($"[Space Warp] Skipping loading addressables for {Label} which does not exist."); + resolve(); + return; + } + try { GameManager.Instance.Assets.LoadByLabel(Label,Action,delegate(IList assetLocations) diff --git a/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs b/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs new file mode 100644 index 00000000..5dad1816 --- /dev/null +++ b/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs @@ -0,0 +1,89 @@ +using HarmonyLib; +using KSP.Game.Load; +using KSP.IO; +using SpaceWarp.API.Logging; +using SpaceWarp.API.SaveGameManager; +using SpaceWarp.Backend.SaveGameManager; +using SpaceWarp.InternalUtilities; +using System; + +namespace SpaceWarp.Patching.SaveGameManager; + +[HarmonyPatch] +internal class SaveLoadPatches +{ + private static readonly ILogger _logger = new UnityLogSource("SpaceWarp.SaveLoadPatches"); + + /// SAVING /// + + [HarmonyPatch(typeof(SerializeGameDataFlowAction), MethodType.Constructor), HarmonyPostfix] + [HarmonyPatch(new Type[] { typeof(string), typeof(LoadGameData) })] + private static void InjectPluginSaveGameData(string filename, LoadGameData data, SerializeGameDataFlowAction __instance) + { + // Skip plugin data injection if there are no mods that have registered for save/load actions + if (ModSaves.InternalPluginSaveData.Count == 0) + return; + + // Take the game's LoadGameData, extend it with our own class and copy plugin save data to it + SpaceWarpSerializedSavedGame modSaveData = new(); + InternalExtensions.CopyFieldAndPropertyDataFromSourceToTargetObject(data.SavedGame, modSaveData); + modSaveData.serializedPluginSaveData = ModSaves.InternalPluginSaveData; + data.SavedGame = modSaveData; + + // Initiate save callback for plugins that specified a callback function + foreach (var plugin in ModSaves.InternalPluginSaveData) + { + plugin.SaveEventCallback(plugin.SaveData); + } + } + + /// LOADING /// + + [HarmonyPatch(typeof(DeserializeContentsFlowAction), "DoAction"), HarmonyPrefix] + private static bool DeserializeLoadedPluginData(Action resolve, Action reject, DeserializeContentsFlowAction __instance) + { + // Skip plugin deserialization if there are no mods that have registered for save/load actions + if (ModSaves.InternalPluginSaveData.Count == 0) + return true; + + __instance._game.UI.SetLoadingBarText(__instance.Description); + try + { + // Deserialize save data to our own class that extends game's SerializedSavedGame + SpaceWarpSerializedSavedGame serializedSavedGame = new(); + IOProvider.FromJsonFile(__instance._filename, out serializedSavedGame); + __instance._data.SavedGame = serializedSavedGame; + __instance._data.DataLength = IOProvider.GetFileSize(__instance._filename); + + // Perform plugin load data if plugin data is found in the save file + if (serializedSavedGame.serializedPluginSaveData.Count > 0) + { + // Iterate through each plugin + foreach (var loadedData in serializedSavedGame.serializedPluginSaveData) + { + // Match registered plugin GUID with the GUID found in the save file + var existingData = ModSaves.InternalPluginSaveData.Find(p => p.ModGuid == loadedData.ModGuid); + if (existingData == null) + { + _logger.LogWarning($"Saved data for plugin '{loadedData.ModGuid}' found during a load event, however that plugin isn't registered for save/load events. Skipping load for this plugin."); + continue; + } + + // Perform a callback if plugin specified a callback function. This is done before plugin data is actually updated. + existingData.LoadEventCallback(loadedData.SaveData); + + // Copy loaded data to the SaveData object plugin registered + InternalExtensions.CopyFieldAndPropertyDataFromSourceToTargetObject(loadedData.SaveData, existingData.SaveData); + } + } + } + catch (Exception ex) + { + UnityEngine.Debug.LogException(ex); + reject(ex.Message); + } + resolve(); + + return false; + } +} diff --git a/SpaceWarp.UI/API/UI/Settings/ModsPropertyDrawers.cs b/SpaceWarp.UI/API/UI/Settings/ModsPropertyDrawers.cs index 74414e4c..75d3fb6f 100644 --- a/SpaceWarp.UI/API/UI/Settings/ModsPropertyDrawers.cs +++ b/SpaceWarp.UI/API/UI/Settings/ModsPropertyDrawers.cs @@ -335,15 +335,30 @@ private static Func GenerateGenericDrawerFor(Type e private static Func GenerateAbstractGenericDrawerFor(Type entrySettingType) { - var valueListMethod = typeof(ModsPropertyDrawers).GetMethod(nameof(CreateFromAcceptableValueList), + var valueListMethod = typeof(ModsPropertyDrawers).GetMethod(nameof(CreateFromListConstraint), BindingFlags.Static | BindingFlags.NonPublic) ?.MakeGenericMethod(entrySettingType); - var valueRangeMethod = typeof(ModsPropertyDrawers).GetMethod(nameof(CreateFromAcceptableValueRange), + var valueRangeMethod = typeof(ModsPropertyDrawers).GetMethod(nameof(CreateFromRangeConstraint), BindingFlags.Static | BindingFlags.NonPublic) ?.MakeGenericMethod(entrySettingType); return (name, entry) => { + var t = entry.Constraint?.GetType(); + if (t?.GetGenericTypeDefinition() == typeof(ListConstraint<>) && + t.GenericTypeArguments[0] == entrySettingType) + { + if (valueListMethod != null) + return (GameObject)valueListMethod.Invoke(null, new object[] { name, entry, entry.Constraint }); + } + if (t?.GetGenericTypeDefinition() == typeof(RangeConstraint<>) && + t.GenericTypeArguments[0] == entrySettingType) + { + if (valueRangeMethod != null) + { + return (GameObject)valueRangeMethod.Invoke(null, new object[] { name, entry, entry.Constraint }); + } + } var inputFieldCopy = UnityObject.Instantiate(InputFieldPrefab); var lab = inputFieldCopy.GetChild("Label"); lab.GetComponent().SetTerm(name); @@ -574,6 +589,93 @@ private static GameObject CreateFromAcceptableValueRange(ConfigEntry entry return slCopy; } + private static GameObject CreateFromListConstraint(string key, IConfigEntry entry, ListConstraint constraint) where T : IEquatable + { + var value = new ConfigValue(entry); + var ddCopy = UnityObject.Instantiate(DropdownPrefab); + var lab = ddCopy.GetChild("Label"); + lab.GetComponent().SetTerm(key); + lab.GetComponent().text = key; + var dropdown = ddCopy.GetChild("Setting").GetChild("BTN-Dropdown"); + var extended = dropdown.GetComponent(); + // Start by clearing the options data + extended.options.Clear(); + foreach (var option in constraint.AcceptableValues) + { + extended.options.Add(new TMP_Dropdown.OptionData(option as string ?? (option is Color color + ? (color.a < 1 ? ColorUtility.ToHtmlStringRGBA(color) : ColorUtility.ToHtmlStringRGB(color)) + : option.ToString()))); + } + + extended.value = constraint.AcceptableValues.IndexOf(value.Value); + extended.onValueChanged.AddListener(idx => { value.Value = constraint.AcceptableValues[idx]; }); + var sec = ddCopy.AddComponent(); + sec.description = entry.Description; + sec.isInputSettingElement = false; + ddCopy.SetActive(true); + ddCopy.name = key; + return ddCopy; + } + + private static GameObject CreateFromRangeConstraint(string key, IConfigEntry entry, + RangeConstraint constraint) where T : IComparable, IComparable + { + var value = new ConfigValue(entry); + // Now we have to have a "slider" prefab + var slCopy = UnityObject.Instantiate(SliderPrefab); + var lab = slCopy.GetChild("Label"); + lab.GetComponent().SetTerm(key); + lab.GetComponent().text = key; + var setting = slCopy.GetChild("Setting"); + var slider = setting.GetChild("KSP2SliderLinear").GetComponent(); + var amount = setting.GetChild("Amount display"); + var text = amount.GetComponentInChildren(); + text.text = entry.Value.ToString(); + Func toFloat = x => Convert.ToSingle(x); + // if (!typeof(T).IsIntegral()) + // { + // var convT = TypeDescriptor.GetConverter(typeof(T)) ?? + // throw new ArgumentNullException("TypeDescriptor.GetConverter(typeof(T))"); + // toT = x => (T)convT.ConvertFrom(x); + // } + Func toT = Type.GetTypeCode(typeof(T)) switch + { + TypeCode.Byte => x => (T)(object)Convert.ToByte(x), + TypeCode.SByte => x => (T)(object)Convert.ToSByte(x), + TypeCode.UInt16 => x => (T)(object)Convert.ToUInt16(x), + TypeCode.UInt32 => x => (T)(object)Convert.ToUInt32(x), + TypeCode.UInt64 => x => (T)(object)Convert.ToUInt64(x), + TypeCode.Int16 => x => (T)(object)Convert.ToInt16(x), + TypeCode.Int32 => x => (T)(object)Convert.ToInt32(x), + TypeCode.Int64 => x => (T)(object)Convert.ToInt64(x), + TypeCode.Decimal => x => (T)(object)Convert.ToDecimal(x), + TypeCode.Double => x => (T)(object)Convert.ToDouble(x), + TypeCode.Single => x => (T)(object)x, + _ => x => throw new NotImplementedException(typeof(T).ToString()) + }; + slider.minValue = toFloat(constraint.Minimum); + slider.maxValue = toFloat(constraint.Maximum); + slider.SetValueWithoutNotify(toFloat(value.Value)); + slider.onValueChanged.AddListener(val => + { + // var trueValue = (acceptableValues.MaxValue-acceptableValues.MinValue) * (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value) + // var trueValue = (toFloat(acceptableValues.MaxValue) - toFloat(acceptableValues.MinValue)) * value + + // toFloat(acceptableValues.MinValue); + var trueValue = val; + + value.Value = toT(trueValue) ?? value.Value; + if (entry.Value != null) text.text = entry.Value.ToString(); + slider.SetWithoutCallback(toFloat(value.Value)); + }); + + var sec = slCopy.AddComponent(); + sec.description = entry.Description; + sec.isInputSettingElement = false; + slCopy.SetActive(true); + slCopy.name = key; + return slCopy; + } + private static GameObject CreateStringConfig(ConfigEntryBase entryBase) { var entry = (ConfigEntry)entryBase; diff --git a/SpaceWarp.nuspec b/SpaceWarp.nuspec index 8b7451fd..4907f98b 100644 --- a/SpaceWarp.nuspec +++ b/SpaceWarp.nuspec @@ -2,7 +2,7 @@ SpaceWarp - 1.5.4 + 1.6.0 SpaceWarp contributors false https://raw.githubusercontent.com/SpaceWarp/SpaceWarp/main/LICENSE diff --git a/SpaceWarpBuildTemplate/swinfo.json b/SpaceWarpBuildTemplate/swinfo.json index 5b45cfd9..683d2a97 100644 --- a/SpaceWarpBuildTemplate/swinfo.json +++ b/SpaceWarpBuildTemplate/swinfo.json @@ -6,7 +6,7 @@ "description": "Space-Warp is an API for KSP2 mod developers.", "source": "https://github.com/SpaceWarpDev/SpaceWarp", "version_check": "https://raw.githubusercontent.com/SpaceWarpDev/SpaceWarp/main/SpaceWarpBuildTemplate/swinfo.json", - "version": "1.5.4", + "version": "1.6.0", "dependencies": [ { "id": "UitkForKsp2",