diff --git a/Directory.Build.props b/Directory.Build.props index abe159c5..582628b6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,8 @@ - 1.4.3 + 1.5.0 netstandard2.0 + SpaceWarp 11 true false @@ -28,4 +29,23 @@ System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; + + + + + + + + + + + + + + + + + + + diff --git a/SpaceWarp/API/Assets/AssetManager.cs b/SpaceWarp.Core/API/Assets/AssetManager.cs similarity index 99% rename from SpaceWarp/API/Assets/AssetManager.cs rename to SpaceWarp.Core/API/Assets/AssetManager.cs index efc50150..d1d956b2 100644 --- a/SpaceWarp/API/Assets/AssetManager.cs +++ b/SpaceWarp.Core/API/Assets/AssetManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using JetBrains.Annotations; using SpaceWarp.API.Lua; using UnityEngine; using Logger = BepInEx.Logging.Logger; @@ -8,6 +9,7 @@ namespace SpaceWarp.API.Assets; [SpaceWarpLuaAPI("Assets")] +[PublicAPI] public static class AssetManager { private static readonly Dictionary AllAssets = new(); @@ -135,7 +137,7 @@ public static bool TryGetAsset(string path, out T asset) where T : UnityObjec return true; } - + /// /// Gets an asset from the specified asset path /// diff --git a/SpaceWarp.Core/API/Configuration/BepInExConfigEntry.cs b/SpaceWarp.Core/API/Configuration/BepInExConfigEntry.cs new file mode 100644 index 00000000..b84977f6 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/BepInExConfigEntry.cs @@ -0,0 +1,45 @@ +using System; +using BepInEx.Configuration; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +[PublicAPI] +public class BepInExConfigEntry : IConfigEntry +{ + public readonly ConfigEntryBase EntryBase; + + public BepInExConfigEntry(ConfigEntryBase entryBase) + { + EntryBase = entryBase; + } + + public object Value + { + get => EntryBase.BoxedValue; + set => EntryBase.BoxedValue = value; + } + public Type ValueType => EntryBase.SettingType; + + public T Get() where T : class + { + if (!typeof(T).IsAssignableFrom(ValueType)) + { + throw new InvalidCastException($"Cannot cast {ValueType} to {typeof(T)}"); + } + + return Value as T; + } + + public void Set(T value) + { + if (!ValueType.IsAssignableFrom(typeof(T))) + { + throw new InvalidCastException($"Cannot cast {ValueType} to {typeof(T)}"); + } + + EntryBase.BoxedValue = Convert.ChangeType(value, ValueType); + } + + public string Description => EntryBase.Description.Description; +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/BepInExConfigFile.cs b/SpaceWarp.Core/API/Configuration/BepInExConfigFile.cs new file mode 100644 index 00000000..720a2ff3 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/BepInExConfigFile.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using BepInEx.Configuration; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +[PublicAPI] +public class BepInExConfigFile : IConfigFile +{ + + public readonly ConfigFile AdaptedConfigFile; + + public BepInExConfigFile(ConfigFile adaptedConfigFile) + { + AdaptedConfigFile = adaptedConfigFile; + } + + public void Save() + { + AdaptedConfigFile.Save(); + } + + public IConfigEntry this[string section, string key] => new BepInExConfigEntry(AdaptedConfigFile[section, key]); + + public IConfigEntry Bind(string section, string key, T defaultValue = default, string description = "") + { + return new BepInExConfigEntry(AdaptedConfigFile.Bind(section, key, defaultValue, description)); + } + + public IReadOnlyList Sections => AdaptedConfigFile.Keys.Select(x => x.Section).Distinct().ToList(); + + public IReadOnlyList this[string section] => AdaptedConfigFile.Keys.Where(x => x.Section == section) + .Select(x => x.Key).ToList(); +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/ConfigValue.cs b/SpaceWarp.Core/API/Configuration/ConfigValue.cs new file mode 100644 index 00000000..0eb6b192 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/ConfigValue.cs @@ -0,0 +1,25 @@ +using System; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +[PublicAPI] +public class ConfigValue +{ + public IConfigEntry Entry; + + public ConfigValue(IConfigEntry entry) + { + Entry = entry; + if (typeof(T) != entry.ValueType) + { + throw new ArgumentException(nameof(entry)); + } + } + + public T Value + { + get => (T)Entry.Value; + set => Entry.Value = value; + } +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/EmptyConfigFile.cs b/SpaceWarp.Core/API/Configuration/EmptyConfigFile.cs new file mode 100644 index 00000000..7be78914 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/EmptyConfigFile.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +[PublicAPI] +public class EmptyConfigFile : IConfigFile +{ + public void Save() + { + } + + public IConfigEntry this[string section, string key] => throw new KeyNotFoundException($"{section}/{key}"); + + public IConfigEntry Bind(string section, string key, T defaultValue = default, string description = "") + { + throw new System.NotImplementedException(); + } + + public IReadOnlyList Sections => new List(); + + public IReadOnlyList this[string section] => throw new KeyNotFoundException($"{section}"); +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/IConfigEntry.cs b/SpaceWarp.Core/API/Configuration/IConfigEntry.cs new file mode 100644 index 00000000..36dba68d --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/IConfigEntry.cs @@ -0,0 +1,15 @@ +using System; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +[PublicAPI] +public interface IConfigEntry +{ + public object Value { get; set; } + public Type ValueType { get; } + public T Get() where T : class; + public void Set(T value); + + public string Description { get; } +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/IConfigFile.cs b/SpaceWarp.Core/API/Configuration/IConfigFile.cs new file mode 100644 index 00000000..7247c455 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/IConfigFile.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +[PublicAPI] +public interface IConfigFile +{ + public void Save(); + + 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/JsonConfigEntry.cs b/SpaceWarp.Core/API/Configuration/JsonConfigEntry.cs new file mode 100644 index 00000000..07453942 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/JsonConfigEntry.cs @@ -0,0 +1,53 @@ +using System; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Configuration; + +[PublicAPI] +public class JsonConfigEntry : IConfigEntry +{ + private readonly JsonConfigFile _configFile; + private object _value; + + public JsonConfigEntry(JsonConfigFile configFile, Type type, string description, object value) + { + _configFile = configFile; + _value = value; + Description = description; + ValueType = type; + } + + + public object Value + { + get => _value; + set + { + _value = value; + _configFile.Save(); + } + } + + public Type ValueType { get; } + public T Get() where T : class + { + if (!typeof(T).IsAssignableFrom(ValueType)) + { + throw new InvalidCastException($"Cannot cast {ValueType} to {typeof(T)}"); + } + + return Value as T; + } + + public void Set(T value) + { + if (!ValueType.IsAssignableFrom(typeof(T))) + { + throw new InvalidCastException($"Cannot cast {ValueType} to {typeof(T)}"); + } + + Value = Convert.ChangeType(value, ValueType); + } + + public string Description { get; } +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Configuration/JsonConfigFile.cs b/SpaceWarp.Core/API/Configuration/JsonConfigFile.cs new file mode 100644 index 00000000..34f5c959 --- /dev/null +++ b/SpaceWarp.Core/API/Configuration/JsonConfigFile.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using JetBrains.Annotations; +using KSP.IO; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace SpaceWarp.API.Configuration; + +[PublicAPI] +public class JsonConfigFile : IConfigFile +{ + [CanBeNull] private JObject _previousConfigObject; + + private Dictionary> _currentEntries = new(); + private readonly string _file; + + public JsonConfigFile(string file) + { + // Use .cfg as this is going to have comments and that will be an issue + if (File.Exists(file)) + { + try + { + _previousConfigObject = JObject.Parse(File.ReadAllText(file)); + } + catch (Exception e) + { + Debug.LogError($"Error in attempting to load previous config file at '{file}': {e}"); + // ignored + } + } + + _file = file; + } + + public void Save() + { + 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)) + { + hadPreviousSection = DumpSection(hadPreviousSection, result, section); + } + result.AppendLine("\n}"); + File.WriteAllText(_file, result.ToString()); + } + + private static bool DumpSection(bool hadPreviousSection, StringBuilder result, KeyValuePair> section) + { + if (hadPreviousSection) + { + result.AppendLine(","); + } + + result.AppendLine($" \"{section.Key.Replace("\"", "\\\"").Replace("\n", "\\\n")}\": {{"); + var hadPreviousKey = false; + foreach (var entry in section.Value) + { + hadPreviousKey = DumpEntry(result, hadPreviousKey, entry); + } + + result.Append("\n }"); + return true; + } + + private static bool DumpEntry(StringBuilder result, bool hadPreviousKey, KeyValuePair entry) + { + if (hadPreviousKey) + { + result.AppendLine(","); + } + + // result.AppendLine($" // {entry.Value.Description}"); + if (entry.Value.Description != "") + { + var descriptionLines = entry.Value.Description.Split('\n').Select(x => x.TrimEnd()); + foreach (var line in descriptionLines) + { + result.AppendLine($" // {line}"); + } + } + + var serialized = IOProvider.ToJson(entry.Value.Value); + var serializedLines = serialized.Split('\n').Select(x => x.TrimEnd()).ToArray(); + if (serializedLines.Length > 1) + { + result.AppendLine($" \"{entry.Key.Replace("\"", "\\\"").Replace("\n", "\\\n")}\": "); + for (var i = 0; i < serializedLines.Length; i++) + { + if (i != serializedLines.Length - 1) + { + result.AppendLine($" {serializedLines[i]}"); + } + else + { + result.Append($" {serializedLines[i]}"); + } + } + } + else + { + result.Append($" \"{entry.Key.Replace("\"", "\\\"").Replace("\n", "\\\n")}\": {serializedLines[0]}"); + } + + return true; + } + + 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)) + { + previousSection = new Dictionary(); + _currentEntries.Add(section,previousSection); + } + + if (previousSection.TryGetValue(key, out var result)) + { + return result; + } + + if (_previousConfigObject != null && _previousConfigObject.TryGetValue(section, out var sect)) + { + try + { + if (sect is JObject obj && obj.TryGetValue(key, out var value)) + { + var previousValue = value.ToObject(typeof(T)); + previousSection[key] = new JsonConfigEntry(this, typeof(T), description, previousValue); + } + else + { + previousSection[key] = new JsonConfigEntry(this, typeof(T), description, defaultValue); + } + } + catch + { + previousSection[key] = new JsonConfigEntry(this, typeof(T), description, defaultValue); + // ignored + } + } + else + { + previousSection[key] = new JsonConfigEntry(this, typeof(T), description, defaultValue); + } + + Save(); + return previousSection[key]; + } + + public IReadOnlyList Sections => _currentEntries.Keys.ToList(); + + public IReadOnlyList this[string section] => _currentEntries[section].Keys.ToList(); +} \ No newline at end of file diff --git a/SpaceWarp/API/Loading/Loading.cs b/SpaceWarp.Core/API/Loading/Loading.cs similarity index 94% rename from SpaceWarp/API/Loading/Loading.cs rename to SpaceWarp.Core/API/Loading/Loading.cs index 538c6b39..e9a0a82d 100644 --- a/SpaceWarp/API/Loading/Loading.cs +++ b/SpaceWarp.Core/API/Loading/Loading.cs @@ -5,21 +5,17 @@ using KSP.Game.Flow; using SpaceWarp.API.Assets; using SpaceWarp.API.Mods; -using SpaceWarp.API.Mods.JSON; using SpaceWarp.InternalUtilities; using SpaceWarp.Patching.LoadingActions; namespace SpaceWarp.API.Loading; +[PublicAPI] public static class Loading { internal static List> DescriptorLoadingActionGenerators = new(); - internal static List> - FallbackDescriptorLoadingActionGenerators = - new(); - internal static List> LoadingActionGenerators = new(); internal static List> GeneralLoadingActions = new(); @@ -33,11 +29,7 @@ internal static List> public static void AddAssetLoadingAction(string subfolder, string name, Func> importFunction, params string[] extensions) { - AddModLoadingAction(name, - extensions.Length == 0 - ? CreateAssetLoadingActionWithoutExtension(subfolder, importFunction) - : CreateAssetLoadingActionWithExtensions(subfolder, importFunction, extensions)); - FallbackDescriptorLoadingActionGenerators.Add(d => new DescriptorLoadingAction(name, + DescriptorLoadingActionGenerators.Add(d => new DescriptorLoadingAction(name, extensions.Length == 0 ? CreateAssetLoadingActionWithoutExtensionDescriptor(subfolder, importFunction) : CreateAssetLoadingActionWithExtensionsDescriptor(subfolder, importFunction, extensions), d)); @@ -67,7 +59,7 @@ public static void AddDescriptorLoadingAction(string name, Action /// Registers a general loading action. Should be added either on Awake() or Start(). /// - /// The action generator + /// The action generator public static void AddGeneralLoadingAction(Func actionGenerator) { GeneralLoadingActions.Add(actionGenerator); diff --git a/SpaceWarp/API/Loading/SaveLoad.cs b/SpaceWarp.Core/API/Loading/SaveLoad.cs similarity index 96% rename from SpaceWarp/API/Loading/SaveLoad.cs rename to SpaceWarp.Core/API/Loading/SaveLoad.cs index aec8e10c..cd617ef5 100644 --- a/SpaceWarp/API/Loading/SaveLoad.cs +++ b/SpaceWarp.Core/API/Loading/SaveLoad.cs @@ -1,20 +1,22 @@ using System; +using JetBrains.Annotations; using KSP.Game.Flow; using SpaceWarp.Patching; namespace SpaceWarp.API.Loading; +[PublicAPI] public static class SaveLoad { /// /// Construct and add a FlowAction to the Game's load sequence. - /// + /// /// FlowActionType must have a public constructor that takes either no arguments, /// or a single GameManager. - /// + /// /// The action will be run after the first FlowAction with a name equal to referenceAction. /// If referenceAction is null, the action will be the first action run. - /// + /// /// /// The FlowActions that occur in this sequence by default are: (Updated for KSP 0.1.1.0) /// @@ -37,15 +39,15 @@ public static class SaveLoad /// Thrown if FlowActionType does not have a valid Constructor public static void AddFlowActionToGameLoadAfter(string referenceAction) where FlowActionType : FlowAction { - SequentialFlowLoadersPatcher.AddConstructor(referenceAction, typeof(FlowActionType), SequentialFlowLoadersPatcher.FLOW_METHOD_STARTGAME); + SequentialFlowLoadersPatcher.AddConstructor(referenceAction, typeof(FlowActionType), SequentialFlowLoadersPatcher.FlowMethodStartgame); } /// /// Add a FlowAction to the Game's load sequence. - /// + /// /// The action will be run after the first FlowAction with a name equal to referenceAction. /// If referenceAction is null, the action will be the first action run. - /// + /// /// /// The FlowActions that occur in this sequence by default are: (Updated for KSP 0.1.1.0) /// @@ -67,12 +69,12 @@ public static void AddFlowActionToGameLoadAfter(string reference /// The name of the action to insert a FlowActionType after. Use null to insert it at the start. public static void AddFlowActionToGameLoadAfter(FlowAction flowAction, string referenceAction) { - SequentialFlowLoadersPatcher.AddFlowAction(referenceAction, flowAction, SequentialFlowLoadersPatcher.FLOW_METHOD_STARTGAME); + SequentialFlowLoadersPatcher.AddFlowAction(referenceAction, flowAction, SequentialFlowLoadersPatcher.FlowMethodStartgame); } /// /// Add a FlowAction to the save file loading sequence. A new FlowActionType is constructed every load. - /// + /// /// /// FlowActionType must have a public constructor that at most one of each of the following types: /// @@ -81,10 +83,10 @@ public static void AddFlowActionToGameLoadAfter(FlowAction flowAction, string re /// LoadGameData /// /// - /// + /// /// The action will be run after the first FlowAction with a name equal to referenceAction. /// If referenceAction is null, the action will be the first action run. - /// + /// /// /// The FlowActions that occur in this sequence by default are: (Updated for KSP 0.1.1.0) /// @@ -148,15 +150,15 @@ public static void AddFlowActionToGameLoadAfter(FlowAction flowAction, string re /// Thrown if FlowActionType does not have a valid Constructor public static void AddFlowActionToCampaignLoadAfter(string referenceAction) where FlowActionType : FlowAction { - SequentialFlowLoadersPatcher.AddConstructor(referenceAction, typeof(FlowActionType), SequentialFlowLoadersPatcher.FLOW_METHOD_PRIVATELOADCOMMON); + SequentialFlowLoadersPatcher.AddConstructor(referenceAction, typeof(FlowActionType), SequentialFlowLoadersPatcher.FlowMethodPrivateloadcommon); } /// /// Add a FlowAction to the save file loading sequence. The same object is used for every load. - /// + /// /// The action will be run after the first FlowAction with a name equal to referenceAction. /// If referenceAction is null, the action will be the first action run. - /// + /// /// /// The FlowActions that occur in this sequence by default are: (Updated for KSP 0.1.1.0) /// @@ -219,12 +221,12 @@ public static void AddFlowActionToCampaignLoadAfter(string refer /// The name of the action to insert a FlowActionType after. Use null to insert it at the start. public static void AddFlowActionToCampaignLoadAfter(FlowAction flowAction, string referenceAction) { - SequentialFlowLoadersPatcher.AddFlowAction(referenceAction, flowAction, SequentialFlowLoadersPatcher.FLOW_METHOD_PRIVATELOADCOMMON); + SequentialFlowLoadersPatcher.AddFlowAction(referenceAction, flowAction, SequentialFlowLoadersPatcher.FlowMethodPrivateloadcommon); } /// /// Add a FlowAction to the save file writing sequence. A new FlowActionType is constructed every load. - /// + /// /// /// FlowActionType must have a public constructor that at most one of each of the following types: /// @@ -232,10 +234,10 @@ public static void AddFlowActionToCampaignLoadAfter(FlowAction flowAction, strin /// LoadOrSaveCampaignTicket /// /// - /// + /// /// The action will be run after the first FlowAction with a name equal to referenceAction. /// If referenceAction is null, the action will be the first action run. - /// + /// /// /// The FlowActions that occur in this sequence by default are: (Updated for KSP 0.1.1.0) /// @@ -261,15 +263,15 @@ public static void AddFlowActionToCampaignLoadAfter(FlowAction flowAction, strin /// Thrown if FlowActionType does not have a valid Constructor public static void AddFlowActionToCampaignSaveAfter(string referenceAction) where FlowActionType : FlowAction { - SequentialFlowLoadersPatcher.AddConstructor(referenceAction, typeof(FlowActionType), SequentialFlowLoadersPatcher.FLOW_METHOD_PRIVATESAVECOMMON); + SequentialFlowLoadersPatcher.AddConstructor(referenceAction, typeof(FlowActionType), SequentialFlowLoadersPatcher.FlowMethodPrivatesavecommon); } /// /// Add a FlowAction to the save file writing sequence. The same object is used for every load. - /// + /// /// The action will be run after the first FlowAction with a name equal to referenceAction. /// If referenceAction is null, the action will be the first action run. - /// + /// /// /// The FlowActions that occur in this sequence by default are: (Updated for KSP 0.1.1.0) /// @@ -294,6 +296,6 @@ public static void AddFlowActionToCampaignSaveAfter(string refer /// The name of the action to insert a FlowActionType after. Use null to insert it at the start. public static void AddFlowActionToCampaignSaveAfter(FlowAction flowAction, string referenceAction) { - SequentialFlowLoadersPatcher.AddFlowAction(referenceAction, flowAction, SequentialFlowLoadersPatcher.FLOW_METHOD_PRIVATESAVECOMMON); + SequentialFlowLoadersPatcher.AddFlowAction(referenceAction, flowAction, SequentialFlowLoadersPatcher.FlowMethodPrivatesavecommon); } } diff --git a/SpaceWarp/API/Logging/BaseLogger.cs b/SpaceWarp.Core/API/Logging/BaseLogger.cs similarity index 88% rename from SpaceWarp/API/Logging/BaseLogger.cs rename to SpaceWarp.Core/API/Logging/BaseLogger.cs index 1da7e7c1..7835d5b4 100644 --- a/SpaceWarp/API/Logging/BaseLogger.cs +++ b/SpaceWarp.Core/API/Logging/BaseLogger.cs @@ -1,5 +1,8 @@ -namespace SpaceWarp.API.Logging; +using JetBrains.Annotations; +namespace SpaceWarp.API.Logging; + +[PublicAPI] public abstract class BaseLogger : ILogger { public abstract void Log(LogLevel level, object x); diff --git a/SpaceWarp/API/Logging/BepInExLogger.cs b/SpaceWarp.Core/API/Logging/BepInExLogger.cs similarity index 72% rename from SpaceWarp/API/Logging/BepInExLogger.cs rename to SpaceWarp.Core/API/Logging/BepInExLogger.cs index 29e93966..662f73a3 100644 --- a/SpaceWarp/API/Logging/BepInExLogger.cs +++ b/SpaceWarp.Core/API/Logging/BepInExLogger.cs @@ -1,7 +1,9 @@ using BepInEx.Logging; +using JetBrains.Annotations; namespace SpaceWarp.API.Logging; +[PublicAPI] public class BepInExLogger : BaseLogger { private ManualLogSource _log; @@ -15,4 +17,6 @@ public override void Log(LogLevel level, object x) { _log.Log((BepInEx.Logging.LogLevel)level, x); } + + public static implicit operator BepInExLogger(ManualLogSource log) => new(log); } \ No newline at end of file diff --git a/SpaceWarp/API/Logging/ILogger.cs b/SpaceWarp.Core/API/Logging/ILogger.cs similarity index 82% rename from SpaceWarp/API/Logging/ILogger.cs rename to SpaceWarp.Core/API/Logging/ILogger.cs index 41837980..a77e76e3 100644 --- a/SpaceWarp/API/Logging/ILogger.cs +++ b/SpaceWarp.Core/API/Logging/ILogger.cs @@ -1,5 +1,8 @@ -namespace SpaceWarp.API.Logging; +using JetBrains.Annotations; +namespace SpaceWarp.API.Logging; + +[PublicAPI] public interface ILogger { public void Log(LogLevel level, object x); @@ -12,5 +15,4 @@ public interface ILogger public void LogInfo(object x); public void LogDebug(object x); public void LogAll(object x); - } \ No newline at end of file diff --git a/SpaceWarp/API/Logging/LogLevel.cs b/SpaceWarp.Core/API/Logging/LogLevel.cs similarity index 71% rename from SpaceWarp/API/Logging/LogLevel.cs rename to SpaceWarp.Core/API/Logging/LogLevel.cs index 3b4462e5..2ef7977f 100644 --- a/SpaceWarp/API/Logging/LogLevel.cs +++ b/SpaceWarp.Core/API/Logging/LogLevel.cs @@ -1,6 +1,8 @@ -namespace SpaceWarp.API.Logging; +using JetBrains.Annotations; +namespace SpaceWarp.API.Logging; +[PublicAPI] public enum LogLevel { None = 0, diff --git a/SpaceWarp/API/Logging/UnityLogSource.cs b/SpaceWarp.Core/API/Logging/UnityLogSource.cs similarity index 92% rename from SpaceWarp/API/Logging/UnityLogSource.cs rename to SpaceWarp.Core/API/Logging/UnityLogSource.cs index fc89eddc..654cb060 100644 --- a/SpaceWarp/API/Logging/UnityLogSource.cs +++ b/SpaceWarp.Core/API/Logging/UnityLogSource.cs @@ -1,7 +1,9 @@ -using UnityEngine; +using JetBrains.Annotations; +using UnityEngine; namespace SpaceWarp.API.Logging; +[PublicAPI] public class UnityLogSource : BaseLogger { public string Name; diff --git a/SpaceWarp/API/Lua/LuaMod.cs b/SpaceWarp.Core/API/Lua/LuaMod.cs similarity index 87% rename from SpaceWarp/API/Lua/LuaMod.cs rename to SpaceWarp.Core/API/Lua/LuaMod.cs index 583a46c8..14960196 100644 --- a/SpaceWarp/API/Lua/LuaMod.cs +++ b/SpaceWarp.Core/API/Lua/LuaMod.cs @@ -1,41 +1,38 @@ - -using System; +using System; using BepInEx.Logging; +using JetBrains.Annotations; using KSP.Game; using MoonSharp.Interpreter; -using UnityEngine; -using Logger = BepInEx.Logging.Logger; namespace SpaceWarp.API.Lua; - - [MoonSharpUserData] +[PublicAPI] public class LuaMod : KerbalMonoBehaviour { public Table ModTable; // TODO: Add more than just this to the behaviour but for now - + #region Message Definitions // Start - private Closure _start = null; + private Closure _start; // Update Functions - private Closure _update = null; - private Closure _fixedUpdate = null; - private Closure _lateUpdate = null; - + private Closure _update; + private Closure _fixedUpdate; + private Closure _lateUpdate; + // Enable/Disable - private Closure _onEnable = null; - private Closure _onDisable = null; - + private Closure _onEnable; + private Closure _onDisable; + // Destruction - private Closure _onDestroy = null; - + private Closure _onDestroy; + // Reset - private Closure _reset = null; + private Closure _reset; #endregion - + public ManualLogSource Logger; // First a pass through to the wrapped table @@ -67,7 +64,7 @@ private void TryRegister(string name, out Closure method) } method = null; } - + public void Awake() { if (ModTable.Get("Awake") != null && ModTable.Get("Awake").Type == DataType.Function) @@ -91,7 +88,7 @@ public void Awake() TryRegister(nameof(OnDestroy), out _onDestroy); TryRegister(nameof(Reset), out _reset); - + } // Start @@ -128,7 +125,7 @@ public void LateUpdate() TryCallMethod(_lateUpdate, this); } } - + // Enable/Disable public void OnEnable() @@ -146,7 +143,7 @@ public void OnDisable() TryCallMethod(_onDisable, this); } } - + // Destruction public void OnDestroy() diff --git a/SpaceWarp/API/Lua/SpaceWarpInterop.cs b/SpaceWarp.Core/API/Lua/SpaceWarpInterop.cs similarity index 65% rename from SpaceWarp/API/Lua/SpaceWarpInterop.cs rename to SpaceWarp.Core/API/Lua/SpaceWarpInterop.cs index 03856d33..96b762aa 100644 --- a/SpaceWarp/API/Lua/SpaceWarpInterop.cs +++ b/SpaceWarp.Core/API/Lua/SpaceWarpInterop.cs @@ -1,26 +1,19 @@ -using System; -using BepInEx.Bootstrap; +using BepInEx.Bootstrap; using JetBrains.Annotations; -using KSP.UI.Flight; using MoonSharp.Interpreter; -using SpaceWarp.API.Assets; -using SpaceWarp.API.UI.Appbar; -using SpaceWarp.Backend.UI.Appbar; using SpaceWarp.InternalUtilities; using UnityEngine; -using UnityEngine.UIElements; using Logger = BepInEx.Logging.Logger; -// ReSharper disable UnusedMember.Global namespace SpaceWarp.API.Lua; [SpaceWarpLuaAPI("SpaceWarp")] -// ReSharper disable once UnusedType.Global +[PublicAPI] public static class SpaceWarpInterop { public static LuaMod RegisterMod(string name, Table modTable) { - GameObject go = new GameObject(name); + var go = new GameObject(name); go.Persist(); go.transform.SetParent(Chainloader.ManagerObject.transform); go.SetActive(false); @@ -30,6 +23,4 @@ public static LuaMod RegisterMod(string name, Table modTable) go.SetActive(true); return mod; } - - } \ No newline at end of file diff --git a/SpaceWarp/API/Lua/SpaceWarpLuaAPIAttribute.cs b/SpaceWarp.Core/API/Lua/SpaceWarpLuaAPIAttribute.cs similarity index 84% rename from SpaceWarp/API/Lua/SpaceWarpLuaAPIAttribute.cs rename to SpaceWarp.Core/API/Lua/SpaceWarpLuaAPIAttribute.cs index 3d3f27b9..91fc288f 100644 --- a/SpaceWarp/API/Lua/SpaceWarpLuaAPIAttribute.cs +++ b/SpaceWarp.Core/API/Lua/SpaceWarpLuaAPIAttribute.cs @@ -1,8 +1,10 @@ using System; +using JetBrains.Annotations; namespace SpaceWarp.API.Lua; [AttributeUsage(AttributeTargets.Class)] +[MeansImplicitUse] public class SpaceWarpLuaAPIAttribute : Attribute { public string LuaName; diff --git a/SpaceWarp.Core/API/Lua/UI/LuaUITK.cs b/SpaceWarp.Core/API/Lua/UI/LuaUITK.cs new file mode 100644 index 00000000..34ae01f6 --- /dev/null +++ b/SpaceWarp.Core/API/Lua/UI/LuaUITK.cs @@ -0,0 +1,470 @@ +using JetBrains.Annotations; +using MoonSharp.Interpreter; +using SpaceWarp.API.Assets; +using UnityEngine.UIElements; + +namespace SpaceWarp.API.Lua.UI; + +[SpaceWarpLuaAPI("UI")] +[PublicAPI] +public static class LuaUITK +{ + #region Creation + + public static UIDocument Window(LuaMod mod, string id, string documentPath) + { + return Window(mod, id, AssetManager.GetAsset(documentPath)); + } + + public static UIDocument Window(LuaMod mod, string id, VisualTreeAsset uxml) + { + var parent = mod.transform; + return UitkForKsp2.API.Window.CreateFromUxml(uxml, id, parent, true); + } + + public static UIDocument Window(LuaMod mod, string id) + { + var parent = mod.transform; + return UitkForKsp2.API.Window.Create(out _, id, parent, true); + } + + #region Element Creation + + public static VisualElement VisualElement() + { + return new VisualElement(); + } + + public static ScrollView ScrollView() + { + return new ScrollView(); + } + + public static ListView ListView() + { + return new ListView(); + } + + public static Toggle Toggle(string text = "") + { + return new Toggle + { + text = text + }; + } + + public static Label Label(string text = "") + { + return new Label + { + text = text + }; + } + + public static Button Button(string text = "") + { + return new Button + { + text = text + }; + } + + public static Scroller Scroller() + { + return new Scroller(); + } + + public static TextField TextField(string text = "") + { + return new TextField + { + text = text + }; + } + + public static Foldout Foldout() + { + return new Foldout(); + } + + public static Slider Slider(float value = 0.0f, float minValue = 0.0f, float maxValue = 1.0f) + { + return new Slider + { + lowValue = minValue, + highValue = maxValue, + value = value + }; + } + + public static SliderInt SliderInt(int value = 0, int minValue = 0, int maxValue = 100) + { + return new SliderInt + { + lowValue = minValue, + highValue = maxValue, + value = value + }; + } + + public static MinMaxSlider MinMaxSlider(float minValue = 0.0f, float maxValue = 1.0f, float minLimit = 0.0f, + float maxLimit = 1.0f) + { + return new MinMaxSlider + { + minValue = minValue, + maxValue = maxValue, + lowLimit = minLimit, + highLimit = maxLimit + }; + } + + #endregion + + #endregion + + #region Callbacks + + public static void AddCallback(Button button, Closure callback, [CanBeNull] DynValue self = null) + { + if (self != null) + { + button.clicked += () => callback.Call(self); + } + else + { + button.clicked += () => callback.Call(); + } + } + + /// + /// Registers a value changed callback from lua + /// The lua functions parameters should be like function(self?,previous,new) + /// + /// + /// + /// + /// + public static void RegisterValueChangedCallback( + INotifyValueChanged element, + Closure callback, + [CanBeNull] DynValue self = null + ) + { + if (self != null) + { + element.RegisterValueChangedCallback(evt => callback.Call(self, evt.previousValue, evt.newValue)); + } + else + { + element.RegisterValueChangedCallback(evt => callback.Call(evt.previousValue, evt.newValue)); + } + } + + private static void RegisterGenericCallback( + VisualElement element, + Closure callback, + DynValue self, + bool trickleDown = false + ) where T : EventBase, new() + { + if (self != null) + { + element.RegisterCallback(evt => callback.Call(self, evt), + trickleDown ? TrickleDown.TrickleDown : TrickleDown.NoTrickleDown); + } + else + { + element.RegisterCallback(evt => callback.Call(evt), + trickleDown ? TrickleDown.TrickleDown : TrickleDown.NoTrickleDown); + } + } + + + #region Capture Events + + public static void RegisterMouseCaptureCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseCaptureOutCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterPointerCaptureCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterPointerCaptureOutCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #region Change Events + + public static void RegisterChangeBoolCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback>(element, callback, self, trickleDown); + + public static void RegisterChangeIntCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback>(element, callback, self, trickleDown); + + public static void RegisterChangeFloatCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback>(element, callback, self, trickleDown); + + public static void RegisterChangeStringCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback>(element, callback, self, trickleDown); + + #endregion + + #region Click Events + + public static void RegisterClickCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #region Focus Events + + public static void RegisterFocusOutCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterFocusInCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterBlurCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterFocusCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #region Input Events + + public static void RegisterInputCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #region Layout Events + + public static void RegisterGeometryChangedCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #region Mouse Events + + public static void RegisterMouseDownCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseUpCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseMoveCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterWheelCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseEnterWindowCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseLeaveWindowCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseEnterCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseLeaveCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseOverCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterMouseOutCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #region Pointer Events + + public static void RegisterPointerDownCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterPointerUpCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterPointerMoveCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterPointerEnterCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterPointerLeaveCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterPointerOverCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterPointerOutCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #region Panel Events + + public static void RegisterAttachToPanelCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + public static void RegisterDetachFromPanelCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #region Tooltip Events + + public static void RegisterTooltipCallback( + VisualElement element, + Closure callback, + [CanBeNull] DynValue self = null, + bool trickleDown = false + ) => RegisterGenericCallback(element, callback, self, trickleDown); + + #endregion + + #endregion +} \ No newline at end of file diff --git a/SpaceWarp/API/Mods/BaseKspLoaderSpaceWarpMod.cs b/SpaceWarp.Core/API/Mods/BaseKspLoaderSpaceWarpMod.cs similarity index 57% rename from SpaceWarp/API/Mods/BaseKspLoaderSpaceWarpMod.cs rename to SpaceWarp.Core/API/Mods/BaseKspLoaderSpaceWarpMod.cs index f62fdeef..0127632e 100644 --- a/SpaceWarp/API/Mods/BaseKspLoaderSpaceWarpMod.cs +++ b/SpaceWarp.Core/API/Mods/BaseKspLoaderSpaceWarpMod.cs @@ -1,12 +1,16 @@ -using KSP.Modding; +using JetBrains.Annotations; +using KSP.Modding; +using SpaceWarp.API.Configuration; using SpaceWarp.API.Logging; namespace SpaceWarp.API.Mods; +[PublicAPI] public abstract class BaseKspLoaderSpaceWarpMod : Mod, ISpaceWarpMod { public virtual void OnPreInitialized() { + } public virtual void OnInitialized() @@ -16,9 +20,16 @@ public virtual void OnInitialized() public virtual void OnPostInitialized() { } - + /// /// Gets set automatically, before awake is called /// - public ILogger SWLogger { get; internal set; } + public ILogger SWLogger { get; set; } + + public IConfigFile SWConfiguration { + get; + internal set; + } + + public SpaceWarpPluginDescriptor SWMetadata { get; set; } } \ No newline at end of file diff --git a/SpaceWarp/API/Mods/BaseSpaceWarpPlugin.cs b/SpaceWarp.Core/API/Mods/BaseSpaceWarpPlugin.cs similarity index 86% rename from SpaceWarp/API/Mods/BaseSpaceWarpPlugin.cs rename to SpaceWarp.Core/API/Mods/BaseSpaceWarpPlugin.cs index 69027956..2f7551f8 100644 --- a/SpaceWarp/API/Mods/BaseSpaceWarpPlugin.cs +++ b/SpaceWarp.Core/API/Mods/BaseSpaceWarpPlugin.cs @@ -1,17 +1,19 @@ using BepInEx; using BepInEx.Logging; +using JetBrains.Annotations; using KSP.Game; using KSP.Messages; using KSP.VFX; using SpaceWarp.API.Mods.JSON; -using JetBrains.Annotations; +using SpaceWarp.API.Configuration; using SpaceWarp.API.Logging; namespace SpaceWarp.API.Mods; /// -/// Represents a KSP2 Mod, you should inherit from this and do your manager processing. +/// Represents a KSP2 Mod, you should inherit from this and do your manager processing. /// +[PublicAPI] public abstract class BaseSpaceWarpPlugin : BaseUnityPlugin, ISpaceWarpMod { #region KspBehaviour things @@ -60,6 +62,10 @@ public virtual void OnPostInitialized() } private BepInExLogger _logger; public ILogger SWLogger => _logger ??= new BepInExLogger(Logger); + private BepInExConfigFile _configFile; + public IConfigFile SWConfiguration => _configFile ??= new BepInExConfigFile(Config); + public SpaceWarpPluginDescriptor SWMetadata { get; set; } + internal static string GetGuidBySpec(PluginInfo pluginInfo, ModInfo modInfo) { return modInfo.Spec >= SpecVersion.V1_2 diff --git a/SpaceWarp/API/Mods/GlobalModDefines.cs b/SpaceWarp.Core/API/Mods/GlobalModDefines.cs similarity index 86% rename from SpaceWarp/API/Mods/GlobalModDefines.cs rename to SpaceWarp.Core/API/Mods/GlobalModDefines.cs index d93043e8..4c36ad8e 100644 --- a/SpaceWarp/API/Mods/GlobalModDefines.cs +++ b/SpaceWarp.Core/API/Mods/GlobalModDefines.cs @@ -1,7 +1,9 @@ using System.IO; +using JetBrains.Annotations; namespace SpaceWarp.API.Mods; +[PublicAPI] public static class GlobalModDefines { public static readonly string AssetBundlesFolder = Path.Combine("assets", "bundles"); diff --git a/SpaceWarp.Core/API/Mods/ISpaceWarpMod.cs b/SpaceWarp.Core/API/Mods/ISpaceWarpMod.cs new file mode 100644 index 00000000..c2f9d446 --- /dev/null +++ b/SpaceWarp.Core/API/Mods/ISpaceWarpMod.cs @@ -0,0 +1,21 @@ +using JetBrains.Annotations; +using SpaceWarp.API.Configuration; +using SpaceWarp.API.Logging; + +namespace SpaceWarp.API.Mods; + +[PublicAPI] +public interface ISpaceWarpMod +{ + public void OnPreInitialized(); + + public void OnInitialized(); + + public void OnPostInitialized(); + + public ILogger SWLogger { get; } + + public IConfigFile SWConfiguration { get; } + + public SpaceWarpPluginDescriptor SWMetadata { get; set; } +} \ No newline at end of file diff --git a/SpaceWarp/API/Mods/JSON/Converters/SpecVersionConverter.cs b/SpaceWarp.Core/API/Mods/JSON/Converters/SpecVersionConverter.cs similarity index 100% rename from SpaceWarp/API/Mods/JSON/Converters/SpecVersionConverter.cs rename to SpaceWarp.Core/API/Mods/JSON/Converters/SpecVersionConverter.cs diff --git a/SpaceWarp/API/Mods/JSON/DependencyInfo.cs b/SpaceWarp.Core/API/Mods/JSON/DependencyInfo.cs similarity index 59% rename from SpaceWarp/API/Mods/JSON/DependencyInfo.cs rename to SpaceWarp.Core/API/Mods/JSON/DependencyInfo.cs index f40cbad9..f862fcd1 100644 --- a/SpaceWarp/API/Mods/JSON/DependencyInfo.cs +++ b/SpaceWarp.Core/API/Mods/JSON/DependencyInfo.cs @@ -1,12 +1,14 @@ -using Newtonsoft.Json; +using JetBrains.Annotations; +using Newtonsoft.Json; namespace SpaceWarp.API.Mods.JSON; /// -/// Represents the json property info. Properties have to use the same name as in the JSON file, that's why they break -/// convention. +/// Represents the json property info. Properties have to use the same name as in the JSON file, that's why they break +/// convention. /// [JsonObject(MemberSerialization.OptIn)] +[PublicAPI] public sealed class DependencyInfo { [JsonProperty("id")] public string ID { get; internal set; } diff --git a/SpaceWarp.Core/API/Mods/JSON/ModInfo.cs b/SpaceWarp.Core/API/Mods/JSON/ModInfo.cs new file mode 100644 index 00000000..9a971929 --- /dev/null +++ b/SpaceWarp.Core/API/Mods/JSON/ModInfo.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace SpaceWarp.API.Mods.JSON; + +/// +/// Representation of the mod info JSON file. +/// +[JsonObject(MemberSerialization.OptIn)] +[PublicAPI] +public sealed class ModInfo +{ + [JsonProperty("spec", Required = Required.DisallowNull)] public SpecVersion Spec { get; internal set; } = new(); + + [JsonProperty("mod_id", Required = Required.DisallowNull)] private string _modID = ""; + + public string ModID + { + get + { + if (Spec == SpecVersion.V1_2) + { + throw new DeprecatedSwinfoPropertyException(nameof(ModID), SpecVersion.V1_2); + } + return _modID; + } + internal set => _modID = value; + } + + + [JsonProperty("name")] + private string _name; + public string Name + { + get => _name ?? _modID; + internal set => _name = value; + } + + [JsonProperty("author", Required = Required.DisallowNull)] + public string Author { get; internal set; } = ""; + + [JsonProperty("description", Required = Required.DisallowNull)] public string Description { get; internal set; } = ""; + + [JsonProperty("source", Required = Required.DisallowNull)] public string Source { get; internal set; } = ""; + + [JsonProperty("version", Required = Required.Always)] public string Version { get; internal set; } + + [JsonProperty("dependencies", Required = Required.DisallowNull)] public List Dependencies { get; internal set; } = new(); + + [JsonProperty("ksp2_version", Required = Required.DisallowNull)] + public SupportedVersionsInfo SupportedKsp2Versions { get; internal set; } = new() + { + Min = "*", + Max = "*" + }; + + [JsonProperty("version_check")] + [CanBeNull] + public string VersionCheck { get; internal set; } + + [JsonProperty("version_check_type")] + [Obsolete("Only swinfo.json version checking will be allowed in 2.0.0.")] + public VersionCheckType VersionCheckType { get; internal set; } = VersionCheckType.SwInfo; + + [JsonProperty("conflicts", Required = Required.DisallowNull)] + public List Conflicts { get; internal set; } = new(); +} \ No newline at end of file diff --git a/SpaceWarp/API/Mods/JSON/SpecVersion.cs b/SpaceWarp.Core/API/Mods/JSON/SpecVersion.cs similarity index 72% rename from SpaceWarp/API/Mods/JSON/SpecVersion.cs rename to SpaceWarp.Core/API/Mods/JSON/SpecVersion.cs index 3d64594c..6d527c15 100644 --- a/SpaceWarp/API/Mods/JSON/SpecVersion.cs +++ b/SpaceWarp.Core/API/Mods/JSON/SpecVersion.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using Newtonsoft.Json; using SpaceWarp.API.Mods.JSON.Converters; @@ -8,6 +9,7 @@ namespace SpaceWarp.API.Mods.JSON; /// Represents the version of the API specification from the swinfo.json file. /// [JsonConverter(typeof(SpecVersionConverter))] +[PublicAPI] public sealed record SpecVersion { private const int DefaultMajor = 1; @@ -19,22 +21,29 @@ public sealed record SpecVersion // ReSharper disable InconsistentNaming /// - /// Specification version 1.0 (SpaceWarp < 1.2), used if "spec" is not specified in the swinfo.json file. + /// Specification version 1.0 (SpaceWarp < 1.2) - used if "spec" is not specified in the swinfo.json file. /// public static SpecVersion Default { get; } = new(); /// - /// Specification version 1.2 (SpaceWarp 1.2.x), replaces SpaceWarp's proprietary ModID with BepInEx plugin GUID. + /// Specification version 1.2 (SpaceWarp 1.2.x) - replaces SpaceWarp's proprietary ModID with BepInEx plugin GUID. /// public static SpecVersion V1_2 { get; } = new(1, 2); /// - /// Specification version 1.3 (SpaceWarp 1.3.x), adds back the ModID field, but enforces that it is the same as the BepInEx plugin GUID if there is a BepInEx plugin attached to the swinfo - /// Which w/ version 1.3 there does not have to be a plugin - /// Also enforces that all dependencies use BepInEx GUID, and that they are loaded + /// Specification version 1.3 (SpaceWarp 1.3.x) - adds back the ModID field, but enforces that it is the same + /// as the BepInEx plugin GUID if there is a BepInEx plugin attached to the swinfo, since w/ version 1.3 there + /// does not have to be a plugin. + /// Also enforces that all dependencies use BepInEx GUID, and that they are loaded. /// public static SpecVersion V1_3 { get; } = new(1, 3); + /// + /// Specification version 2.0 (SpaceWarp 1.5.x and 2.0.x) - removes support for version checking from .csproj files, + /// + /// + public static SpecVersion V2_0 { get; } = new(2, 0); + // ReSharper restore InconsistentNaming /// @@ -91,10 +100,11 @@ private static int Compare(SpecVersion a, SpecVersion b) /// /// Thrown if the specification version string is invalid. /// +[PublicAPI] public sealed class InvalidSpecVersionException : Exception { - public InvalidSpecVersionException(string version) : base( - $"Invalid spec version: {version}. The correct format is \"major.minor\".") + public InvalidSpecVersionException(string version) : + base($"Invalid spec version: {version}. The correct format is \"major.minor\".") { } } @@ -102,11 +112,11 @@ public InvalidSpecVersionException(string version) : base( /// /// Thrown if a property is deprecated in the current specification version. /// +[PublicAPI] public sealed class DeprecatedSwinfoPropertyException : Exception { - public DeprecatedSwinfoPropertyException(string property, SpecVersion deprecationVersion) : base( - $"The swinfo.json property \"{property}\" is deprecated in the spec version {deprecationVersion} and will be removed completely in the future." - ) + public DeprecatedSwinfoPropertyException(string property, SpecVersion deprecationVersion) : + base($"The swinfo.json property \"{property}\" is deprecated in the spec version {deprecationVersion} and will be removed completely in the future.") { } } \ No newline at end of file diff --git a/SpaceWarp/API/Mods/JSON/SupportedVersionsInfo.cs b/SpaceWarp.Core/API/Mods/JSON/SupportedVersionsInfo.cs similarity index 70% rename from SpaceWarp/API/Mods/JSON/SupportedVersionsInfo.cs rename to SpaceWarp.Core/API/Mods/JSON/SupportedVersionsInfo.cs index 4b700e15..fb4e3118 100644 --- a/SpaceWarp/API/Mods/JSON/SupportedVersionsInfo.cs +++ b/SpaceWarp.Core/API/Mods/JSON/SupportedVersionsInfo.cs @@ -1,16 +1,18 @@ -using Newtonsoft.Json; +using JetBrains.Annotations; +using Newtonsoft.Json; using SpaceWarp.API.Versions; namespace SpaceWarp.API.Mods.JSON; /// -/// Representation of the supported version info of a mod from a JSON file. +/// Representation of the supported version info of a mod from a JSON file. /// [JsonObject(MemberSerialization.OptIn)] +[PublicAPI] public sealed class SupportedVersionsInfo { - internal const string DefaultMin = "0.0.0"; - internal const string DefaultMax = "*"; + public const string DefaultMin = "0.0.0"; + public const string DefaultMax = "*"; [JsonProperty("min")] public string Min { get; internal set; } = DefaultMin; diff --git a/SpaceWarp/API/Mods/JSON/VersionCheckType.cs b/SpaceWarp.Core/API/Mods/JSON/VersionCheckType.cs similarity index 61% rename from SpaceWarp/API/Mods/JSON/VersionCheckType.cs rename to SpaceWarp.Core/API/Mods/JSON/VersionCheckType.cs index 46d2c003..bfa1b6db 100644 --- a/SpaceWarp/API/Mods/JSON/VersionCheckType.cs +++ b/SpaceWarp.Core/API/Mods/JSON/VersionCheckType.cs @@ -1,10 +1,14 @@ -using System.Runtime.Serialization; +using System; +using System.Runtime.Serialization; +using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Converters; namespace SpaceWarp.API.Mods.JSON; [JsonConverter(typeof(StringEnumConverter))] +[Obsolete("Only swinfo.json version checking will be allowed in 2.0.0.")] +[PublicAPI] public enum VersionCheckType { [EnumMember(Value = "swinfo")] diff --git a/SpaceWarp.Core/API/Mods/PluginList.cs b/SpaceWarp.Core/API/Mods/PluginList.cs new file mode 100644 index 00000000..97ea2079 --- /dev/null +++ b/SpaceWarp.Core/API/Mods/PluginList.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using BepInEx; +using BepInEx.Bootstrap; +using JetBrains.Annotations; +using SpaceWarp.API.Mods.JSON; +using SpaceWarp.API.Versions; +using SpaceWarpPatcher; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Switch.LowLevel; + +// Disable obsolete warning for Chainloader.Plugins +#pragma warning disable CS0618 + +namespace SpaceWarp.API.Mods; + +/// +/// API for accessing information about currently loaded and disabled plugins. +/// +[PublicAPI] +public static class PluginList +{ + #region Reading Plugins + + /// + /// Set if the plugin list is different in any way since last run (version differences, new mods, mods removed, + /// mods disabled, description differences, any different in any swinfo file and the disabled mod list). + /// + public static bool ModListChangedSinceLastRun => ChainloaderPatch.ModListChangedSinceLastRun; + + /// + /// Contains information about all currently loaded plugins. The key is the BepInEx GUID of the plugin. + /// + public static Dictionary LoadedPluginInfos { get; } = Chainloader.PluginInfos; + + /// + /// Contains information about all currently disabled plugins. The key is the BepInEx GUID of the plugin. + /// + public static Dictionary DisabledPluginInfos { get; } = ChainloaderPatch + .DisabledPluginGuids.Zip(ChainloaderPatch.DisabledPlugins, (guid, info) => new { guid, info }) + .ToDictionary(item => item.guid, item => item.info); + + /// + /// Returns whether the plugin with the specified GUID is currently loaded. + /// + /// GUID of the plugin + /// Returns true if the plugin is loaded, false otherwise + public static bool IsLoaded(string guid) + { + return LoadedPluginInfos.ContainsKey(guid); + } + + /// + /// Compares the version of the specified plugin with the given version. + /// + /// GUID of the plugin + /// Version to compare the plugin's version to + /// Returns -1 if the plugin is older than the given version, 0 if it's the same version and 1 if it's newer + /// Thrown if the plugin with the specified GUID is not loaded + public static int CompareVersion(string guid, Version version) + { + if (!IsLoaded(guid)) + { + throw new ArgumentException($"Plugin with GUID {guid} is not loaded"); + } + + return LoadedPluginInfos[guid].Metadata.Version.CompareTo(version); + } + + /// + /// Retrieves the of the specified plugin. Returns null if the specified plugin guid doesn't + /// have an associated . + /// + /// GUID of the plugin + /// of the plugin or null if not found + public static ModInfo TryGetSwinfo(string guid) + { + var swModInfo = AllEnabledAndActivePlugins + .FirstOrDefault(item => item.Guid == guid); + + if (swModInfo != null) + { + return swModInfo.SWInfo; + } + + var disabledModInfo = AllDisabledPlugins + .Where(item => item.Guid == guid) + .Select(item => item.SWInfo) + .FirstOrDefault(); + + return disabledModInfo; + } + + /// + /// Retrieves the of the specified plugin. Returns null if the specified plugin guid doesn't + /// have an associated . + /// + /// GUID of the plugin + /// of the plugin or null if not found + public static SpaceWarpPluginDescriptor TryGetDescriptor(string guid) + { + return AllEnabledAndActivePlugins + .FirstOrDefault(item => item.Guid == guid); + } + + /// + /// Retrieves the instance of the specified plugin class. + /// + /// The type of the plugin class + /// Plugin instance or null if not found + public static T TryGetPlugin() where T : BaseUnityPlugin => Chainloader.Plugins.OfType().FirstOrDefault(); + + /// + /// Retrieves the instance of a plugin class with the given BepInEx GUID. + /// + /// BepInEx GUID of the plugin + /// The type of the plugin class + /// Plugin instance or null if not found + public static T TryGetPlugin(string guid) where T : BaseUnityPlugin => + Chainloader.Plugins.Find(plugin => plugin.Info.Metadata.GUID == guid) as T; + + #endregion + + #region Registering Plugins + + private static List _allEnabledAndActivePlugins = new(); + + /// + /// All plugins that are enabled, and active (not errored) + /// + public static IReadOnlyList AllEnabledAndActivePlugins => _allEnabledAndActivePlugins; + + private static List _allDisabledPlugins = new(); + + /// + /// All disabled plugins + /// + public static IReadOnlyList AllDisabledPlugins => _allDisabledPlugins; + + private static List _allErroredPlugins = new(); + public static IReadOnlyList AllErroredPlugins => _allErroredPlugins; + + public static IEnumerable AllPlugins => _allEnabledAndActivePlugins + .Concat(_allDisabledPlugins).Concat(_allErroredPlugins.Select(x => x.Plugin)); + + public static void RegisterPlugin(SpaceWarpPluginDescriptor plugin) + { + if (AllPlugins.Any(x => x.Guid == plugin.Guid)) + { + SpaceWarpPlugin.Logger.LogError($"Attempting to register a mod with a duplicate GUID: {plugin.Guid}"); + } + + SpaceWarpPlugin.Logger.LogInfo($"Registered plugin: {plugin.Guid}"); + _allEnabledAndActivePlugins.Add(plugin); + } + + public static void Disable(string guid) + { + var descriptor = _allEnabledAndActivePlugins.FirstOrDefault(x => + string.Equals(x.Guid, guid, StringComparison.InvariantCultureIgnoreCase)); + if (descriptor != null) + { + _allEnabledAndActivePlugins.Remove(descriptor); + _allDisabledPlugins.Add(descriptor); + } + } + + public static SpaceWarpErrorDescription GetErrorDescriptor(SpaceWarpPluginDescriptor plugin) + { + if (_allErroredPlugins.Any(x => x.Plugin == plugin)) + { + return _allErroredPlugins.First(x => x.Plugin == plugin); + } + + if (_allEnabledAndActivePlugins.Any(x => x == plugin)) + { + _allEnabledAndActivePlugins.Remove(plugin); + } + + var newError = new SpaceWarpErrorDescription(plugin); + _allErroredPlugins.Add(newError); + return newError; + } + + public static void NoteMissingSwinfoError(SpaceWarpPluginDescriptor plugin) + { + var errorDescriptor = GetErrorDescriptor(plugin); + errorDescriptor.MissingSwinfo = true; + } + + public static void NoteBadDirectoryError(SpaceWarpPluginDescriptor plugin) + { + var errorDescriptor = GetErrorDescriptor(plugin); + errorDescriptor.BadDirectory = true; + } + + public static void NoteBadIDError(SpaceWarpPluginDescriptor plugin) + { + var errorDescriptor = GetErrorDescriptor(plugin); + errorDescriptor.BadID = true; + } + + public static void NoteMismatchedVersionError(SpaceWarpPluginDescriptor plugin) + { + var errorDescriptor = GetErrorDescriptor(plugin); + errorDescriptor.MismatchedVersion = true; + } + + public static void NoteUnspecifiedDependencyError(SpaceWarpPluginDescriptor plugin, string dependency) + { + var errorDescriptor = GetErrorDescriptor(plugin); + errorDescriptor.UnspecifiedDependencies.Add(dependency); + } + + private static SemanticVersion PadVersion(string version) + { + var length = version.Split('.').Length; + for (var i = 0; i < 3-length; i++) + { + version += ".0"; + } + + return new SemanticVersion(version); + } + + private static bool IsSupportedSemver(string version, string min, string max) + { + var basicVersion = PadVersion(version); + var minVersion = PadVersion(min.Replace("*", "0")); + var maxVersion = PadVersion(max.Replace("*", $"{int.MaxValue}")); + return basicVersion >= minVersion && basicVersion <= maxVersion; + } + + private static bool DependencyResolved( + SpaceWarpPluginDescriptor descriptor, + List resolvedPlugins + ) + { + if (descriptor.SWInfo.Spec < SpecVersion.V1_3) return true; + return !(from dependency in descriptor.SWInfo.Dependencies + let info = resolvedPlugins.FirstOrDefault(x => string.Equals( + x.Guid, + dependency.ID, + StringComparison.InvariantCultureIgnoreCase) + ) + where info == null || !IsSupportedSemver( + info.SWInfo.Version, + dependency.Version.Min, + dependency.Version.Max + ) + select dependency).Any(); + } + + private static void GetLoadOrder() + { + var changed = true; + List newOrder = new(); + while (changed) + { + changed = false; + for (var i = _allEnabledAndActivePlugins.Count - 1; i >= 0; i--) + { + if (!DependencyResolved(_allEnabledAndActivePlugins[i], newOrder)) continue; + newOrder.Add(_allEnabledAndActivePlugins[i]); + _allEnabledAndActivePlugins.RemoveAt(i); + changed = true; + } + } + + for (var i = _allEnabledAndActivePlugins.Count - 1; i >= 0; i--) + { + var info = _allEnabledAndActivePlugins[i]; + SpaceWarpPlugin.Logger.LogError($"Missing dependency for mod: {info.Name}, this mod will not be loaded"); + var error = GetErrorDescriptor(info); + error.MissingDependencies = info.SWInfo.Dependencies.Select(x => x.ID).Where(x => + !newOrder.Any(y => string.Equals(x, y.Guid, StringComparison.InvariantCultureIgnoreCase))).ToList(); + } + + _allEnabledAndActivePlugins = newOrder; + } + + private static void GetDependencyErrors() + { + foreach (var erroredPlugin in _allErroredPlugins.Where(erroredPlugin => + erroredPlugin.MissingDependencies.Count != 0)) + { + for (var i = erroredPlugin.MissingDependencies.Count - 1; i >= 0; i--) + { + var dep = erroredPlugin.MissingDependencies[i]; + if (AllEnabledAndActivePlugins.Any(x => + string.Equals(x.Guid, dep, StringComparison.InvariantCultureIgnoreCase))) + { + erroredPlugin.UnsupportedDependencies.Add(dep); + erroredPlugin.MissingDependencies.RemoveAt(i); + } + else if (AllErroredPlugins.Any(x => + string.Equals(x.Plugin.Guid, dep, StringComparison.InvariantCultureIgnoreCase))) + { + erroredPlugin.ErroredDependencies.Add(dep); + erroredPlugin.MissingDependencies.RemoveAt(i); + } + else if (AllDisabledPlugins.Any(x => + string.Equals(x.Guid, dep, StringComparison.InvariantCultureIgnoreCase))) + { + erroredPlugin.DisabledDependencies.Add(dep); + erroredPlugin.MissingDependencies.RemoveAt(i); + } + } + } + } + + private static void CheckCompatibility() + { + var incompatibilities = _allEnabledAndActivePlugins.Select(x => (Key: x.Guid, Value: new HashSet())) + .ToDictionary(x => x.Key, x => x.Value); + var versionLookup = _allEnabledAndActivePlugins.Select(x => (Key: x.Guid, Value: x.SWInfo.Version)) + .ToDictionary(x => x.Key, x => x.Value); + var pluginDictionary = _allEnabledAndActivePlugins.ToDictionary(x => x.Guid, x => x); + foreach (var mod in _allEnabledAndActivePlugins) + { + var swinfo = mod.SWInfo; + if (swinfo.Spec < SpecVersion.V2_0) continue; + foreach (var conflict in swinfo.Conflicts) + { + if (!versionLookup.TryGetValue(conflict.ID, out var conflictingVersion) || + !IsSupportedSemver(conflictingVersion, conflict.Version.Min, conflict.Version.Max)) continue; + incompatibilities[mod.Guid].Add(conflict.ID); + incompatibilities[conflict.ID].Add(mod.Guid); + } + } + + foreach (var incompatibility in incompatibilities) + { + if (incompatibility.Value.Count <= 0) continue; + var descriptor = GetErrorDescriptor(pluginDictionary[incompatibility.Key]); + descriptor.Incompatibilities.AddRange(incompatibility.Value); + } + } + + /// + /// This is done after Awake/LoadModule(), so that everything else can use it + /// + internal static void ResolveDependenciesAndLoadOrder() + { + GetLoadOrder(); + GetDependencyErrors(); + CheckCompatibility(); + } + + #endregion +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Mods/SpaceWarpErrorDescription.cs b/SpaceWarp.Core/API/Mods/SpaceWarpErrorDescription.cs new file mode 100644 index 00000000..9753a4e3 --- /dev/null +++ b/SpaceWarp.Core/API/Mods/SpaceWarpErrorDescription.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using BepInEx; +using JetBrains.Annotations; +using SpaceWarp.Patching; +using UnityEngine; + +namespace SpaceWarp.API.Mods; + +[PublicAPI] +public class SpaceWarpErrorDescription +{ + public SpaceWarpPluginDescriptor Plugin; + public bool MissingSwinfo; + public bool BadDirectory; + public bool BadID; + public bool MismatchedVersion; + + public List DisabledDependencies = new(); + public List ErroredDependencies = new(); + public List MissingDependencies = new(); + public List UnsupportedDependencies = new(); + public List UnspecifiedDependencies = new(); + public List Incompatibilities = new(); + + public SpaceWarpErrorDescription(SpaceWarpPluginDescriptor plugin) + { + Plugin = plugin; + // Essentially if we have an errored plugin, we delete the plugin code + if (plugin.Plugin != null) + { + switch (plugin.Plugin) + { + // If and only if this is space warp we don't destroy it + // Then we inform our loading patcher to do space warp specially + case SpaceWarpPlugin: + BootstrapPatch.ForceSpaceWarpLoadDueToError = true; + BootstrapPatch.ErroredSWPluginDescriptor = plugin; + return; + case BaseUnityPlugin unityPlugin: + Object.Destroy(unityPlugin); + break; + } + } + plugin.Plugin = null; + } + +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Mods/SpaceWarpPluginDescriptor.cs b/SpaceWarp.Core/API/Mods/SpaceWarpPluginDescriptor.cs new file mode 100644 index 00000000..fdf7edfb --- /dev/null +++ b/SpaceWarp.Core/API/Mods/SpaceWarpPluginDescriptor.cs @@ -0,0 +1,45 @@ +using System.IO; +using JetBrains.Annotations; +using SpaceWarp.API.Configuration; +using SpaceWarp.API.Mods.JSON; + +namespace SpaceWarp.API.Mods; + +[PublicAPI] +public class SpaceWarpPluginDescriptor +{ + public SpaceWarpPluginDescriptor( + [CanBeNull] ISpaceWarpMod plugin, + string guid, + string name, + ModInfo swInfo, + DirectoryInfo folder, + bool doLoadingActions = true, + [CanBeNull] IConfigFile configFile = null + ) + { + Plugin = plugin; + Guid = guid; + Name = name; + SWInfo = swInfo; + Folder = folder; + DoLoadingActions = doLoadingActions; + ConfigFile = configFile; + } + + [CanBeNull] public ISpaceWarpMod Plugin; + public readonly string Guid; + public readonly string Name; + public readonly ModInfo SWInfo; + public readonly DirectoryInfo Folder; + public bool DoLoadingActions; + [CanBeNull] public IConfigFile ConfigFile; + + // Set by the version checking system + public bool Outdated; + + public bool Unsupported = false; + + // Used to check for mods that have not been pre-initialized + public bool LatePreInitialize; +} \ No newline at end of file diff --git a/SpaceWarp/API/Parts/Colors.cs b/SpaceWarp.Core/API/Parts/Colors.cs similarity index 94% rename from SpaceWarp/API/Parts/Colors.cs rename to SpaceWarp.Core/API/Parts/Colors.cs index 2cf1842c..752bb847 100644 --- a/SpaceWarp/API/Parts/Colors.cs +++ b/SpaceWarp.Core/API/Parts/Colors.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; -using BepInEx.Logging; +using JetBrains.Annotations; using SpaceWarp.API.Lua; using SpaceWarp.Patching; +using UnityEngine; namespace SpaceWarp.API.Parts; [SpaceWarpLuaAPI("Colors")] +[PublicAPI] public static class Colors { /// @@ -54,5 +56,5 @@ public static void DeclareParts(string modGuid, IEnumerable partNameList /// Name of the part as described in the .json. /// [Obsolete("Use the shader \"KSP2/Parts/Paintable\" or \"Parts Replace\" instead")] - public static UnityEngine.Texture[] GetTextures(string partName) => new UnityEngine.Texture[0]; + public static Texture[] GetTextures(string partName) => Array.Empty(); } \ No newline at end of file diff --git a/SpaceWarp.Core/API/Versions/SemanticVersion.cs b/SpaceWarp.Core/API/Versions/SemanticVersion.cs new file mode 100644 index 00000000..ea45be74 --- /dev/null +++ b/SpaceWarp.Core/API/Versions/SemanticVersion.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; + +// Disable warnings for missing Equals and GetHashCode implementations +#pragma warning disable CS0660, CS0661 + +namespace SpaceWarp.API.Versions; + +/// +/// Extended version of semantic versioning (see https://semver.org/) that supports an unlimited amount of +/// version numbers. +/// +public class SemanticVersion : IComparable +{ + #region Properties + + /// + /// The major (first) version number. + /// + public int Major => VersionNumbers[0]; + + /// + /// The minor (second) version number. + /// + public int Minor => VersionNumbers[1]; + + /// + /// The patch (third) version number. + /// + public int Patch => VersionNumbers[2]; + + /// + /// List of all version numbers. + /// + public List VersionNumbers { get; } = new(); + + /// + /// Prerelease identifiers. + /// + public string Prerelease { get; } + + /// + /// Build metadata. + /// + public string Build { get; } + + #endregion + + #region Constructors + + /// + /// Creates a new SemanticVersion object. + /// + /// Major version number + /// Minor version number + /// Patch version number + /// Prerelease identifiers + /// Build metadata + public SemanticVersion(int major, int minor, int patch, string prerelease, string build) + { + VersionNumbers.Add(major); + VersionNumbers.Add(minor); + VersionNumbers.Add(patch); + Prerelease = prerelease ?? string.Empty; + Build = build ?? string.Empty; + } + + /// + /// Creates a new SemanticVersion object. + /// + /// Major version number + /// Minor version number + /// Patch version number + /// Prerelease identifiers + public SemanticVersion(int major, int minor, int patch, string prerelease) + : this(major, minor, patch, prerelease, string.Empty) + { + } + + /// + /// Creates a new SemanticVersion object. + /// + /// Major version number + /// Minor version number + /// Patch version number + public SemanticVersion(int major, int minor, int patch) + : this(major, minor, patch, string.Empty) + { + } + + /// + /// Creates a new SemanticVersion object from a version string. + /// + /// Version string in the format "major.minor.patch[-prerelease][+build]" + /// Thrown if the version string is invalid + public SemanticVersion(string version) + { + if (string.IsNullOrEmpty(version)) + { + throw new ArgumentException("Version string must not be null or empty."); + } + + var originalVersion = version; + + var buildIndex = version.IndexOf('+'); + if (buildIndex != -1) + { + Build = version[(buildIndex + 1)..]; + version = version[..buildIndex]; + } + + var prereleaseIndex = version.IndexOf('-'); + if (prereleaseIndex != -1) + { + Prerelease = version[(prereleaseIndex + 1)..]; + version = version[..prereleaseIndex]; + } + + var versionParts = version.Split('.'); + if (versionParts.Length < 3) + { + throw new ArgumentException( + $"Version string \"{originalVersion}\" must have at least 3 version numbers (major.minor.patch)." + ); + } + + foreach (var part in versionParts) + { + if (int.TryParse(part, out var versionNumber)) + { + VersionNumbers.Add(versionNumber); + } + else + { + throw new ArgumentException( + $"Invalid version number: \"{part}\" in version string \"{originalVersion}\"." + ); + } + } + } + + #endregion + + #region Comparisons + + /// + /// Compares this version to another version. + /// + /// The other version to compare to. + /// + /// Negative if this version is less than the other version, 0 if they are equal, positive if this version is + /// larger than the other version. + /// + public int CompareTo(SemanticVersion other) + { + /* + * Precedence MUST be calculated by separating the version into major, minor, patch and pre-release identifiers + * in that order (Build metadata does not figure into precedence). + * + * Precedence is determined by the first difference when comparing each of these identifiers from left to right + * as follows: Major, minor, and patch versions are always compared numerically. + * + * Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. + * + * When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version: + * + * Example: 1.0.0-alpha < 1.0.0. + * + * Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined by + * comparing each dot separated identifier from left to right until a difference is found as follows: + * + * Identifiers consisting of only digits are compared numerically. + * + * Identifiers with letters or hyphens are compared lexically in ASCII sort order. + * + * Numeric identifiers always have lower precedence than non-numeric identifiers. + * + * A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding + * identifiers are equal. + * + * Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 + * < 1.0.0-rc.1 < 1.0.0. + */ + + // Compare the individual version numbers + var maxLength = Math.Max(VersionNumbers.Count, other.VersionNumbers.Count); + for (var i = 0; i < maxLength; i++) + { + var version1Part = i < VersionNumbers.Count ? VersionNumbers[i] : 0; + var version2Part = i < other.VersionNumbers.Count ? other.VersionNumbers[i] : 0; + + if (version1Part > version2Part) + { + return 1; + } + + if (version1Part < version2Part) + { + return -1; + } + } + + // If the version numbers are equal, compare the prerelease identifiers if at least one of them is not null, + // otherwise the versions are considered equal + if (Prerelease == null && other.Prerelease == null) + { + return 0; + } + + if (Prerelease == null) + { + return 1; + } + + if (other.Prerelease == null) + { + return -1; + } + + // Both prerelease strings are not null, compare them + var prereleaseParts1 = Prerelease.Split('.'); + var prereleaseParts2 = other.Prerelease.Split('.'); + var maxPrereleaseLength = Math.Max(prereleaseParts1.Length, prereleaseParts2.Length); + for (var i = 0; i < maxPrereleaseLength; i++) + { + var prerelease1Part = i < prereleaseParts1.Length ? prereleaseParts1[i] : string.Empty; + var prerelease2Part = i < prereleaseParts2.Length ? prereleaseParts2[i] : string.Empty; + + if (prerelease1Part == prerelease2Part) + { + continue; + } + + var prerelease1PartIsNumeric = int.TryParse(prerelease1Part, out var prerelease1PartNumber); + var prerelease2PartIsNumeric = int.TryParse(prerelease2Part, out var prerelease2PartNumber); + + if (prerelease1PartIsNumeric && prerelease2PartIsNumeric) + { + return prerelease1PartNumber - prerelease2PartNumber; + } + + if (prerelease1PartIsNumeric) + { + return -1; + } + + if (prerelease2PartIsNumeric) + { + return 1; + } + + return string.Compare(prerelease1Part, prerelease2Part, StringComparison.Ordinal); + } + + return 0; + } + + /// + /// Compares whether one version is less than another version. + /// + /// First version to compare. + /// Second version to compare. + /// True if a is less than b, false otherwise. + public static bool operator <(SemanticVersion a, SemanticVersion b) => a.CompareTo(b) < 0; + + /// + /// Compares whether one version is greater than another version. + /// + /// First version to compare. + /// Second version to compare. + /// True if a is greater than b, false otherwise. + public static bool operator >(SemanticVersion a, SemanticVersion b) => a.CompareTo(b) > 0; + + /// + /// Compares whether one version is less than or equal to another version. + /// + /// First version to compare. + /// Second version to compare. + /// True if a is less than or equal to b, false otherwise. + public static bool operator <=(SemanticVersion a, SemanticVersion b) => a.CompareTo(b) <= 0; + + /// + /// Compares whether one version is greater than or equal to another version. + /// + /// First version to compare. + /// Second version to compare. + /// True if a is greater than or equal to b, false otherwise. + public static bool operator >=(SemanticVersion a, SemanticVersion b) => a.CompareTo(b) >= 0; + + /// + /// Compares whether one version is equal to another version. + /// + /// First version to compare. + /// Second version to compare. + /// True if a is equal to b, false otherwise. + public static bool operator ==(SemanticVersion a, SemanticVersion b) => + (a is null && b is null) || (a is not null && b is not null && a.CompareTo(b) == 0); + + /// + /// Compares whether one version is not equal to another version. + /// + /// First version to compare. + /// Second version to compare. + /// True if a is not equal to b, false otherwise. + public static bool operator !=(SemanticVersion a, SemanticVersion b) => !(a == b); + + #endregion + + #region String conversion + + /// + /// Returns the version as a string. + /// + /// Version string. + public override string ToString() + { + var versionString = $"{Major}.{Minor}.{Patch}"; + if (!string.IsNullOrEmpty(Prerelease)) + { + versionString += $"-{Prerelease}"; + } + + if (!string.IsNullOrEmpty(Build)) + { + versionString += $"+{Build}"; + } + + return versionString; + } + + /// + /// Implicitly converts a SemanticVersion object to a string. + /// + /// SemanticVersion object to convert. + /// Version string. + public static implicit operator string(SemanticVersion version) => version.ToString(); + + /// + /// Explicitly converts a string to a SemanticVersion object. + /// + /// Version string to convert. + /// SemanticVersion object. + public static explicit operator SemanticVersion(string version) => new(version); + + #endregion +} \ No newline at end of file diff --git a/SpaceWarp.Core/API/Versions/VersionUtility.cs b/SpaceWarp.Core/API/Versions/VersionUtility.cs new file mode 100644 index 00000000..b6f9b2a3 --- /dev/null +++ b/SpaceWarp.Core/API/Versions/VersionUtility.cs @@ -0,0 +1,150 @@ +using System; +using System.Text.RegularExpressions; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Versions; + +[PublicAPI] +public static class VersionUtility +{ + /// + /// Checks if one semantic version is newer than another + /// + /// The first version + /// The second version + /// version1 is newer than version2 + public static bool IsNewerThan(string version1, string version2) + { + return CompareSemanticVersionStrings(version1, version2) > 0; + } + + /// + /// Checks if one semantic version is older than another + /// + /// The first version + /// The second version + /// version1 is older than version2 + public static bool IsOlderThan(string version1, string version2) + { + return CompareSemanticVersionStrings(version1, version2) < 0; + } + + public static bool IsSupported(string version, string min, string max) + { + return !IsOlderThan(version, min) && !IsNewerThan(version, max); + } + + private static Regex _toClear = new("[^0-9.*]"); + private static string PreprocessSemanticVersion(string semver) => _toClear.Replace(semver, ""); + + + /// + /// Compares 2 semantic versions + /// + /// The first version + /// The second version + /// + /// 0 if version1 equals version2, -1 if version1 is less than version2, 1 if version1 is greater than version2 + /// + public static int CompareSemanticVersionStrings(string version1, string version2) + { + var semanticVersion1 = PreprocessSemanticVersion(version1); + var semanticVersion2 = PreprocessSemanticVersion(version2); + Console.WriteLine($"version 1: {version1} -> {semanticVersion1}"); + Console.WriteLine($"version 2: {version2} -> {semanticVersion2}"); + var version1Parts = semanticVersion1.Split('.'); + var version2Parts = semanticVersion2.Split('.'); + + var maxLength = Math.Max(version1Parts.Length, version2Parts.Length); + + for (var i = 0; i < maxLength; i++) + { + var version1PartString = i < version1Parts.Length ? version1Parts[i] : "0"; + var version2PartString = i < version2Parts.Length ? version2Parts[i] : "0"; + + if (version1PartString == "*" || version2PartString == "*") + { + break; + } + + var version1Part = int.Parse(version1PartString); + var version2Part = int.Parse(version2PartString); + + if (version1Part > version2Part) + { + return 1; + } + + if (version1Part < version2Part) + { + return -1; + } + } + + return ComparePrereleaseSemanticVersions(version1, version2); + } + + private static Regex _prereleaseVersion = new(@"(\D+)(\d+)"); + + private static int ComparePrereleaseSemanticVersions(string version1, string version2) + { + var dash1 = version1.IndexOf("-", StringComparison.Ordinal); + var dash2 = version2.IndexOf("-", StringComparison.Ordinal); + if (dash1 == -1 && dash2 == -1) + { + return 0; + } + + if (dash1 == -1) + { + return 1; + } + + if (dash2 == -1) + { + return -1; + } + + var alphaVersion1 = version1[(dash1 + 1)..]; + var alphaVersion2 = version2[(dash2 + 1)..]; + // So now we get the numerics from the end of here + int alphaVersionNumber1 = 0; + int alphaVersionNumber2 = 0; + var alphaVersionName1 = alphaVersion1; + var alphaVersionName2 = alphaVersion2; + if (_prereleaseVersion.IsMatch(alphaVersion1)) + { + var match = _prereleaseVersion.Match(alphaVersion1); + var name = match.Groups[1]; + var number = match.Groups[2]; + alphaVersionNumber1 = int.Parse(number.Value); + alphaVersionName1 = name.Value; + } + + if (_prereleaseVersion.IsMatch(alphaVersion2)) + { + var match = _prereleaseVersion.Match(alphaVersion2); + var name = match.Groups[1]; + var number = match.Groups[2]; + alphaVersionNumber2 = int.Parse(number.Value); + alphaVersionName2 = name.Value; + } + + var comparison = string.CompareOrdinal(alphaVersionName1, alphaVersionName2); + if (comparison == 0) + { + if (alphaVersionNumber1 > alphaVersionNumber2) + { + return 1; + } else if (alphaVersionNumber1 == alphaVersionNumber2) + { + return 0; + } + else + { + return -1; + } + } + return comparison; + } +} \ No newline at end of file diff --git a/SpaceWarp.Core/AssemblyInfo.cs b/SpaceWarp.Core/AssemblyInfo.cs new file mode 100644 index 00000000..7387ab6a --- /dev/null +++ b/SpaceWarp.Core/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SpaceWarp.UI")] +[assembly: InternalsVisibleTo("SpaceWarp.VersionChecking")] \ No newline at end of file diff --git a/SpaceWarp/Backend/Modding/AssetOnlyMod.cs b/SpaceWarp.Core/Backend/Modding/AssetOnlyMod.cs similarity index 73% rename from SpaceWarp/Backend/Modding/AssetOnlyMod.cs rename to SpaceWarp.Core/Backend/Modding/AssetOnlyMod.cs index 470a1f7d..cd34c581 100644 --- a/SpaceWarp/Backend/Modding/AssetOnlyMod.cs +++ b/SpaceWarp.Core/Backend/Modding/AssetOnlyMod.cs @@ -1,4 +1,5 @@ using BepInEx.Logging; +using SpaceWarp.API.Configuration; using SpaceWarp.API.Logging; using SpaceWarp.API.Mods; @@ -10,7 +11,7 @@ public AssetOnlyMod(string name) { SWLogger = new BepInExLogger(new ManualLogSource(name)); } - + public void OnPreInitialized() { } @@ -24,4 +25,6 @@ public void OnPostInitialized() } public ILogger SWLogger { get; } + public IConfigFile SWConfiguration => new EmptyConfigFile(); + public SpaceWarpPluginDescriptor SWMetadata { get; set; } } \ No newline at end of file diff --git a/SpaceWarp.Core/Backend/Modding/BepInExModAdapter.cs b/SpaceWarp.Core/Backend/Modding/BepInExModAdapter.cs new file mode 100644 index 00000000..84f61bda --- /dev/null +++ b/SpaceWarp.Core/Backend/Modding/BepInExModAdapter.cs @@ -0,0 +1,32 @@ +using BepInEx; +using SpaceWarp.API.Configuration; +using SpaceWarp.API.Logging; +using SpaceWarp.API.Mods; + +namespace SpaceWarp.Backend.Modding; + +public class BepInExModAdapter : ISpaceWarpMod +{ + public readonly BaseUnityPlugin Plugin; + + public void OnPreInitialized() + { + } + + public void OnInitialized() + { + } + + public void OnPostInitialized() + { + } + + public ILogger SWLogger => new BepInExLogger(Plugin.Logger); + public IConfigFile SWConfiguration => new BepInExConfigFile(Plugin.Config); + public SpaceWarpPluginDescriptor SWMetadata { get; set; } + + public BepInExModAdapter(BaseUnityPlugin plugin) + { + Plugin = plugin; + } +} \ No newline at end of file diff --git a/SpaceWarp.Core/Backend/Modding/Ksp2ModInfo.cs b/SpaceWarp.Core/Backend/Modding/Ksp2ModInfo.cs new file mode 100644 index 00000000..d8615dd9 --- /dev/null +++ b/SpaceWarp.Core/Backend/Modding/Ksp2ModInfo.cs @@ -0,0 +1,43 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace SpaceWarp.Backend.Modding; + +public class Ksp2ModInfo +{ + [JsonConverter(typeof(VersionConverter))] + [JsonProperty] + public Version APIVersion { get; private set; } + + // Token: 0x17001C93 RID: 7315 + // (get) Token: 0x06008070 RID: 32880 RVA: 0x001EE0B9 File Offset: 0x001EC2B9 + // (set) Token: 0x06008071 RID: 32881 RVA: 0x001EE0C1 File Offset: 0x001EC2C1 + [JsonConverter(typeof(VersionConverter))] + [JsonProperty] + public Version ModVersion { get; private set; } + + // Token: 0x17001C94 RID: 7316 + // (get) Token: 0x06008072 RID: 32882 RVA: 0x001EE0CA File Offset: 0x001EC2CA + // (set) Token: 0x06008073 RID: 32883 RVA: 0x001EE0D2 File Offset: 0x001EC2D2 + [JsonProperty] + public string ModName { get; private set; } + + // Token: 0x17001C95 RID: 7317 + // (get) Token: 0x06008074 RID: 32884 RVA: 0x001EE0DB File Offset: 0x001EC2DB + // (set) Token: 0x06008075 RID: 32885 RVA: 0x001EE0E3 File Offset: 0x001EC2E3 + [JsonProperty] + public string ModAuthor { get; private set; } + + // Token: 0x17001C96 RID: 7318 + // (get) Token: 0x06008076 RID: 32886 RVA: 0x001EE0EC File Offset: 0x001EC2EC + // (set) Token: 0x06008077 RID: 32887 RVA: 0x001EE0F4 File Offset: 0x001EC2F4 + [JsonProperty] + public string ModDescription { get; private set; } + + // Token: 0x17001C97 RID: 7319 + // (get) Token: 0x06008078 RID: 32888 RVA: 0x001EE0FD File Offset: 0x001EC2FD + // (set) Token: 0x06008079 RID: 32889 RVA: 0x001EE105 File Offset: 0x001EC305 + [JsonProperty] + public string Catalog { get; private set; } +} \ No newline at end of file diff --git a/SpaceWarp/Backend/Modding/KspModAdapter.cs b/SpaceWarp.Core/Backend/Modding/KspModAdapter.cs similarity index 63% rename from SpaceWarp/Backend/Modding/KspModAdapter.cs rename to SpaceWarp.Core/Backend/Modding/KspModAdapter.cs index 7600fc96..2e85d23c 100644 --- a/SpaceWarp/Backend/Modding/KspModAdapter.cs +++ b/SpaceWarp.Core/Backend/Modding/KspModAdapter.cs @@ -1,6 +1,5 @@ -using System; -using JetBrains.Annotations; -using KSP.Modding; +using KSP.Modding; +using SpaceWarp.API.Configuration; using SpaceWarp.API.Logging; using SpaceWarp.API.Mods; using UnityEngine; @@ -15,6 +14,7 @@ internal class KspModAdapter : MonoBehaviour, ISpaceWarpMod public void OnPreInitialized() { AdaptedMod.modCore?.ModStart(); + SWLogger = new UnityLogSource(AdaptedMod.ModName); } public void OnInitialized() @@ -30,5 +30,7 @@ public void Update() AdaptedMod.modCore?.ModUpdate(); } - public ILogger SWLogger { get; } + public ILogger SWLogger { get; private set; } + public IConfigFile SWConfiguration => new EmptyConfigFile(); + public SpaceWarpPluginDescriptor SWMetadata { get; set; } } \ No newline at end of file diff --git a/SpaceWarp.Core/Backend/Modding/PluginRegister.cs b/SpaceWarp.Core/Backend/Modding/PluginRegister.cs new file mode 100644 index 00000000..82cbe719 --- /dev/null +++ b/SpaceWarp.Core/Backend/Modding/PluginRegister.cs @@ -0,0 +1,530 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using BepInEx; +using BepInEx.Bootstrap; +using BepInEx.Configuration; +using Newtonsoft.Json; +using SpaceWarp.API.Configuration; +using SpaceWarp.API.Logging; +using SpaceWarp.API.Mods; +using SpaceWarp.API.Mods.JSON; +using SpaceWarp.API.Versions; +using SpaceWarpPatcher; + +namespace SpaceWarp.Backend.Modding; + +internal static class PluginRegister +{ + public static void RegisterAllMods() + { + RegisterAllBepInExMods(); + GetDisabledPlugins(); + RegisterAllCodelessMods(); + RegisterAllKspMods(); + RegisterAllErroredMods(); + DisableMods(); + } + + private static ILogger Logger = (BepInExLogger)SpaceWarpPlugin.Logger; + + private static ModInfo BepInExToSWInfo(PluginInfo plugin) + { + var newInfo = new ModInfo + { + Spec = SpecVersion.V2_0, + ModID = plugin.Metadata.GUID, + Name = plugin.Metadata.Name, + Author = "", + Description = "", + Source = "", + Version = plugin.Metadata.Version.ToString(), + Dependencies = plugin.Dependencies.Select(x => new DependencyInfo + { + ID = x.DependencyGUID, + Version = new SupportedVersionsInfo + { + Min = x.MinimumVersion.ToString(), + Max = "*" + } + }).ToList(), + SupportedKsp2Versions = new SupportedVersionsInfo + { + Min = "*", + Max = "*" + }, + VersionCheck = null, + VersionCheckType = VersionCheckType.SwInfo, + Conflicts = plugin.Incompatibilities.Select(x => new DependencyInfo + { + ID = x.IncompatibilityGUID, + Version = new SupportedVersionsInfo + { + Min = "*", + Max = "*" + } + }).ToList() + }; + return newInfo; + } + + private static bool AssertFolderPath(BaseSpaceWarpPlugin plugin, string folderPath) + { + if (Path.GetFileName(folderPath) != "plugins") return true; + + Logger.LogError( + $"Found Space Warp mod {plugin.Info.Metadata.Name} in the BepInEx/plugins directory. This mod will not be initialized."); + + var descriptor = new SpaceWarpPluginDescriptor( + plugin, + plugin.Info.Metadata.GUID, + plugin.Info.Metadata.Name, + BepInExToSWInfo(plugin.Info), + new DirectoryInfo(folderPath), + false, + new BepInExConfigFile(plugin.Config) + ); + PluginList.NoteBadDirectoryError(descriptor); + return false; + } + + private static bool AssertModInfoExistence(BaseSpaceWarpPlugin plugin, string modInfoPath, string folderPath) + { + if (File.Exists(modInfoPath)) return true; + + Logger.LogError( + $"Found Space Warp plugin {plugin.Info.Metadata.Name} without a swinfo.json next to it. This mod will not be initialized."); + PluginList.NoteMissingSwinfoError(new SpaceWarpPluginDescriptor(plugin, + plugin.Info.Metadata.GUID, + plugin.Info.Metadata.Name, + BepInExToSWInfo(plugin.Info), + new DirectoryInfo(folderPath))); + return false; + } + + private static bool TryReadModInfo(BaseUnityPlugin plugin, + string modInfoPath, string folderPath, out ModInfo metadata) + { + try + { + metadata = JsonConvert.DeserializeObject(File.ReadAllText(modInfoPath)); + } + catch + { + Logger.LogError( + $"Error reading metadata for spacewarp plugin {plugin.Info.Metadata.Name}. This mod will not be initialized"); + PluginList.NoteMissingSwinfoError(new SpaceWarpPluginDescriptor( + plugin as BaseSpaceWarpPlugin, + plugin.Info.Metadata.GUID, + plugin.Info.Metadata.Name, + BepInExToSWInfo(plugin.Info), + new DirectoryInfo(folderPath) + )); + metadata = null; + return false; + } + + return true; + } + + private static bool AssertSpecificationCompliance( + SpaceWarpPluginDescriptor descriptor, + BaseUnityPlugin plugin, + ModInfo metadata, + string folderPath + ) => metadata.Spec < SpecVersion.V1_3 || AssertSpecVersion13Compliance( + descriptor, + plugin, + metadata, + folderPath + ); + + private static bool AssertSpecVersion13Compliance( + SpaceWarpPluginDescriptor descriptor, + BaseUnityPlugin plugin, + ModInfo metadata, + string folderPath + ) => AssertMatchingModID(descriptor, plugin, metadata, folderPath) && + AssertMatchingVersions(descriptor, plugin, metadata, folderPath) && + AssertAllDependenciesAreSpecified(descriptor, plugin, metadata); + + private static bool AssertAllDependenciesAreSpecified( + SpaceWarpPluginDescriptor descriptor, + BaseUnityPlugin plugin, + ModInfo metadata + ) => plugin.Info.Dependencies.Aggregate( + true, + (current, dep) => current && AssertDependencyIsSpecified(plugin, descriptor, dep, metadata) + ); + + private static bool AssertDependencyIsSpecified( + BaseUnityPlugin plugin, + SpaceWarpPluginDescriptor descriptor, + BepInDependency dep, + ModInfo metadata + ) + { + if (metadata.Dependencies.Any( + x => string.Equals(x.ID, dep.DependencyGUID, StringComparison.InvariantCultureIgnoreCase)) + ) return true; + + Logger.LogError( + $"Found Space Warp Plugin {plugin.Info.Metadata.Name} that has an unspecified swinfo dependency found in its BepInDependencies: {dep.DependencyGUID}"); + PluginList.NoteUnspecifiedDependencyError(descriptor, dep.DependencyGUID); + metadata.Dependencies.Add(new DependencyInfo + { + ID = dep.DependencyGUID, + Version = new SupportedVersionsInfo + { + Min = dep.MinimumVersion.ToString(), + Max = "*" + } + }); + return false; + } + + private static string ClearPrerelease(string version) + { + var semver = new SemanticVersion(version); + return $"{semver.Major}.{semver.Minor}.{semver.Patch}{(semver.VersionNumbers.Count > 3 ? $".{semver.VersionNumbers[3]}" : "")}"; + } + + private static bool AssertMatchingVersions( + SpaceWarpPluginDescriptor descriptor, + BaseUnityPlugin plugin, + ModInfo metadata, + string folderPath + ) + { + if (new Version(ClearPrerelease(metadata.Version)) == plugin.Info.Metadata.Version) return true; + + Logger.LogError( + $"Found Space Warp plugin {plugin.Info.Metadata.Name} that's swinfo version ({metadata.Version}) does not match the plugin version ({plugin.Info.Metadata.Version}), this mod will not be initialized"); + PluginList.NoteMismatchedVersionError(descriptor); + return false; + } + + private static bool AssertMatchingModID( + SpaceWarpPluginDescriptor descriptor, + BaseUnityPlugin plugin, + ModInfo metadata, + string folderPath + ) + { + var modID = metadata.ModID; + if (modID == plugin.Info.Metadata.GUID) return true; + + Logger.LogError( + $"Found Space Warp plugin {plugin.Info.Metadata.Name} that has an swinfo.json w/ spec version >= 1.3 that's ModID is not the same as the plugins GUID, This mod will not be initialized."); + PluginList.NoteBadIDError(descriptor); + return false; + } + + private static void RegisterSingleSpaceWarpPlugin(BaseSpaceWarpPlugin plugin) + { + var folderPath = Path.GetDirectoryName(plugin.Info.Location); + plugin.PluginFolderPath = folderPath; + if (!AssertFolderPath(plugin, folderPath)) return; + + var modInfoPath = Path.Combine(folderPath!, "swinfo.json"); + + if (!AssertModInfoExistence(plugin, modInfoPath, folderPath)) return; + + if (!TryReadModInfo(plugin, modInfoPath, folderPath, out var metadata)) return; + + plugin.SpaceWarpMetadata = metadata; + var directoryInfo = new FileInfo(modInfoPath).Directory; + var descriptor = new SpaceWarpPluginDescriptor( + plugin, + metadata.Spec != SpecVersion.V1_2 ? metadata.ModID : plugin.Info.Metadata.GUID, + metadata.Name, + metadata, + directoryInfo, + true, + new BepInExConfigFile(plugin.Config) + ); + descriptor.Plugin!.SWMetadata = descriptor; + if (!AssertSpecificationCompliance(descriptor, plugin, metadata, folderPath)) return; + + PluginList.RegisterPlugin(descriptor); + } + + private static void RegisterSingleBepInExPlugin(BaseUnityPlugin plugin) + { + if (PluginList.AllPlugins.Any(x => x.Plugin is BaseUnityPlugin bup && bup == plugin)) + { + return; + } + + var folderPath = Path.GetDirectoryName(plugin.Info.Location); + var modInfoPath = Path.Combine(folderPath!, "swinfo.json"); + var directoryInfo = new DirectoryInfo(Path.GetDirectoryName(plugin.Info.Location)!); + if (File.Exists(modInfoPath)) + { + if (!TryReadModInfo(plugin, modInfoPath, folderPath, out var metadata)) return; + var descriptor = new SpaceWarpPluginDescriptor( + new BepInExModAdapter(plugin), + metadata.Spec != SpecVersion.V1_2 ? metadata.ModID : plugin.Info.Metadata.GUID, + metadata.Name, + metadata, + directoryInfo, + false, + new BepInExConfigFile(plugin.Config) + ); + descriptor.Plugin!.SWMetadata = descriptor; + if (!AssertSpecificationCompliance(descriptor, plugin, metadata, folderPath)) + return; + PluginList.RegisterPlugin(descriptor); + } + else + { + PluginList.RegisterPlugin(GetBepInExDescriptor(plugin)); + } + } + + private static SpaceWarpPluginDescriptor GetBepInExDescriptor(BaseUnityPlugin plugin) + { + var pluginAdapter = new BepInExModAdapter(plugin); + var descriptor = new SpaceWarpPluginDescriptor( + pluginAdapter, + plugin.Info.Metadata.GUID, + plugin.Info.Metadata.Name, + BepInExToSWInfo(plugin.Info), + new DirectoryInfo(Path.GetDirectoryName(plugin.Info.Location)!), + false, + new BepInExConfigFile(plugin.Config) + ); + pluginAdapter.SWMetadata = descriptor; + return descriptor; + } + + private static SpaceWarpPluginDescriptor GetBepInExDescriptor(PluginInfo info) => new( + null, + info.Metadata.GUID, + info.Metadata.Name, + BepInExToSWInfo(info), + new DirectoryInfo(Path.GetDirectoryName(info.Location)!) + ); + + private static void RegisterAllBepInExMods() + { +#pragma warning disable CS0618 + foreach (var plugin in Chainloader.Plugins) +#pragma warning restore CS0618 + { + if (plugin is BaseSpaceWarpPlugin spaceWarpMod) + { + Logger.LogInfo($"Registering SpaceWarp plugin: {plugin.Info.Metadata.Name}"); + RegisterSingleSpaceWarpPlugin(spaceWarpMod); + } + else + { + Logger.LogInfo($"Registering BIE plugin: {plugin.Info.Metadata.Name}"); + RegisterSingleBepInExPlugin(plugin); + } + } + } + + private static void RegisterAllCodelessMods() + { + var pluginPath = new DirectoryInfo(Paths.PluginPath); + foreach (var swinfo in pluginPath.GetFiles("swinfo.json", SearchOption.AllDirectories)) + { + ModInfo swinfoData; + try + { + swinfoData = JsonConvert.DeserializeObject(File.ReadAllText(swinfo.FullName)); + } + catch + { + Logger.LogError($"Error reading metadata file: {swinfo.FullName}, this mod will be ignored"); + continue; + } + + if (swinfoData.Spec < SpecVersion.V1_3) + { + Logger.LogWarning( + $"Found swinfo information for: {swinfoData.Name}, but its spec is less than v1.3, if this describes a \"codeless\" mod, it will be ignored"); + continue; + } + + var descriptor = new SpaceWarpPluginDescriptor( + new AssetOnlyMod(swinfoData.Name), + swinfoData.ModID, + swinfoData.Name, + swinfoData, swinfo.Directory, + true, + new BepInExConfigFile(FindOrCreateConfigFile(swinfoData.ModID)) + ); + descriptor.Plugin!.SWMetadata = descriptor; + + Logger.LogInfo($"Attempting to register codeless mod: {swinfoData.ModID}, {swinfoData.Name}"); + + if (PluginList.AllPlugins.Any( + x => string.Equals(x.Guid, swinfoData.ModID, StringComparison.InvariantCultureIgnoreCase) + )) continue; + + // Now we can just add it to our plugin list + PluginList.RegisterPlugin(descriptor); + } + } + + private static ConfigFile FindOrCreateConfigFile(string guid) + { + var path = Path.Combine(Paths.ConfigPath, $"{guid}.cfg"); + return new ConfigFile(path, true); + } + + private static ModInfo KspToSwinfo(Ksp2ModInfo mod) + { + var newInfo = new ModInfo + { + Spec = SpecVersion.V1_3, + ModID = mod.ModName, + Name = mod.ModName, + Author = mod.ModAuthor, + Description = mod.ModDescription, + Source = "", + Version = mod.ModVersion.ToString(), + Dependencies = new List(), + SupportedKsp2Versions = new SupportedVersionsInfo + { + Min = "*", + Max = "*" + }, + VersionCheck = null, + VersionCheckType = VersionCheckType.SwInfo + }; + return newInfo; + } + + private static void RegisterSingleKspMod(DirectoryInfo folder) + { + ModInfo metadata; + if (File.Exists(Path.Combine(folder.FullName, "swinfo.json"))) + { + metadata = JsonConvert.DeserializeObject( + File.ReadAllText(Path.Combine(folder.FullName, "swinfo.json")) + ); + } + else if (File.Exists(Path.Combine(folder.FullName, "modinfo.json"))) + { + var modinfo = JsonConvert.DeserializeObject( + File.ReadAllText(Path.Combine(folder.FullName, "modinfo.json")) + ); + metadata = KspToSwinfo(modinfo); + } + else return; + + // This descriptor *will* be modified later + var descriptor = new SpaceWarpPluginDescriptor( + null, + metadata.ModID, + metadata.Name, + metadata, + folder, + false + ) + { + LatePreInitialize = true + }; + PluginList.RegisterPlugin(descriptor); + } + + private static void RegisterAllKspMods() + { + var pluginPath = new DirectoryInfo(Path.Combine(Paths.GameRootPath, "GameData", "Mods")); + if (!pluginPath.Exists) return; + Logger.LogInfo($"KSP Loaded mods path: {pluginPath.FullName}"); + // Lets quickly register every KSP loader loaded mod into the load order before anything, with late pre-initialize + foreach (var plugin in pluginPath.EnumerateDirectories()) + { + Logger.LogInfo($"Attempting to register KSP loaded mod at {pluginPath.FullName}"); + RegisterSingleKspMod(plugin); + } + } + + private static void RegisterAllErroredMods() + { + // Lets do some magic here by copying some code to just get all the plugin types + var allPlugins = TypeLoader.FindPluginTypes( + Paths.PluginPath, + Chainloader.ToPluginInfo, + Chainloader.HasBepinPlugins, + "chainloader" + ); + foreach (var plugin in allPlugins) + { + foreach (var info in plugin.Value) + { + info.Location = plugin.Key; + if (PluginList.AllPlugins.Any( + x => string.Equals(x.Guid, info.Metadata.GUID, StringComparison.InvariantCultureIgnoreCase) + )) continue; + + var descriptor = new SpaceWarpPluginDescriptor( + null, + info.Metadata.GUID, + info.Metadata.Name, + BepInExToSWInfo(info), + new DirectoryInfo(Path.GetDirectoryName(info.Location)!) + ); + var errored = PluginList.GetErrorDescriptor(descriptor); + errored.MissingDependencies = info.Dependencies.Select(x => x.DependencyGUID) + .Where(guid => PluginList.AllEnabledAndActivePlugins.All(x => x.Guid != guid)) + .ToList(); + } + } + } + + private static void GetDisabledPlugins() + { + foreach (var plugin in ChainloaderPatch.DisabledPlugins) + { + GetSingleDisabledPlugin(plugin); + } + } + + private static void GetSingleDisabledPlugin(PluginInfo plugin) + { + var folderPath = Path.GetDirectoryName(plugin.Location); + var swInfoPath = Path.Combine(folderPath!, "swinfo.json"); + if (Path.GetFileName(folderPath) != "plugins" && File.Exists(swInfoPath)) + { + try + { + var swInfo = JsonConvert.DeserializeObject(File.ReadAllText(swInfoPath)); + var descriptor = new SpaceWarpPluginDescriptor( + null, + plugin.Metadata.GUID, + plugin.Metadata.Name, + swInfo, + new DirectoryInfo(folderPath) + ); + PluginList.RegisterPlugin(descriptor); + PluginList.Disable(descriptor.Guid); + } + catch + { + var descriptor = GetBepInExDescriptor(plugin); + PluginList.RegisterPlugin(descriptor); + PluginList.Disable(descriptor.Guid); + } + } + else + { + var descriptor = GetBepInExDescriptor(plugin); + PluginList.RegisterPlugin(descriptor); + PluginList.Disable(descriptor.Guid); + } + } + + private static void DisableMods() + { + foreach (var mod in ChainloaderPatch.DisabledPluginGuids) + { + PluginList.Disable(mod); + } + } +} \ No newline at end of file diff --git a/SpaceWarp/InternalUtilities/AssetHelpers.cs b/SpaceWarp.Core/InternalUtilities/AssetHelpers.cs similarity index 65% rename from SpaceWarp/InternalUtilities/AssetHelpers.cs rename to SpaceWarp.Core/InternalUtilities/AssetHelpers.cs index 764ea84f..5eeeaac3 100644 --- a/SpaceWarp/InternalUtilities/AssetHelpers.cs +++ b/SpaceWarp.Core/InternalUtilities/AssetHelpers.cs @@ -10,29 +10,29 @@ internal static class AssetHelpers { public static void LoadAddressable(string catalog) { - SpaceWarpManager.Logger.LogInfo($"Attempting to load {catalog}"); + SpaceWarpPlugin.Instance.SWLogger.LogInfo($"Attempting to load {catalog}"); var operation = Addressables.LoadContentCatalogAsync(catalog); operation.WaitForCompletion(); if (operation.Status == AsyncOperationStatus.Failed) { - SpaceWarpManager.Logger.LogError($"Failed to load addressables catalog {catalog}"); + SpaceWarpPlugin.Instance.SWLogger.LogError($"Failed to load addressables catalog {catalog}"); } else { - SpaceWarpManager.Logger.LogInfo($"Loaded addressables catalog {catalog}"); + SpaceWarpPlugin.Instance.SWLogger.LogInfo($"Loaded addressables catalog {catalog}"); var locator = operation.Result; - SpaceWarpManager.Logger.LogInfo($"{catalog} ----- {locator.LocatorId}"); + SpaceWarpPlugin.Instance.SWLogger.LogInfo($"{catalog} ----- {locator.LocatorId}"); GameManager.Instance.Assets.RegisterResourceLocator(locator); } } internal static void LoadLocalizationFromFolder(string folder) { - SpaceWarpManager.Logger.LogInfo($"Attempting to load localizations from {folder}"); + SpaceWarpPlugin.Instance.SWLogger.LogInfo($"Attempting to load localizations from {folder}"); LanguageSourceData languageSourceData = null; if (!Directory.Exists(folder)) { - SpaceWarpManager.Logger.LogInfo($"{folder} does not exist, not loading localizations."); + SpaceWarpPlugin.Instance.SWLogger.LogInfo($"{folder} does not exist, not loading localizations."); return; } @@ -40,27 +40,26 @@ internal static void LoadLocalizationFromFolder(string folder) foreach (var csvFile in info.GetFiles("*.csv")) { languageSourceData ??= new LanguageSourceData(); - var csvData = File.ReadAllText(csvFile.FullName); + var csvData = File.ReadAllText(csvFile.FullName).Replace("\r\n", "\n"); languageSourceData.Import_CSV("", csvData, eSpreadsheetUpdateMode.AddNewTerms); } foreach (var i2CsvFile in info.GetFiles("*.i2csv")) { languageSourceData ??= new LanguageSourceData(); - var i2CsvData = File.ReadAllText(i2CsvFile.FullName); + var i2CsvData = File.ReadAllText(i2CsvFile.FullName).Replace("\r\n", "\n"); languageSourceData.Import_I2CSV("", i2CsvData, eSpreadsheetUpdateMode.AddNewTerms); } - if (languageSourceData != null) { languageSourceData.OnMissingTranslation = LanguageSourceData.MissingTranslationAction.Fallback; - SpaceWarpManager.Logger.LogInfo($"Loaded localizations from {folder}"); + SpaceWarpPlugin.Instance.SWLogger.LogInfo($"Loaded localizations from {folder}"); LocalizationManager.AddSource(languageSourceData); } else { - SpaceWarpManager.Logger.LogInfo($"No localizations found in {folder}"); + SpaceWarpPlugin.Instance.SWLogger.LogInfo($"No localizations found in {folder}"); } } -} +} \ No newline at end of file diff --git a/SpaceWarp/InternalUtilities/InternalExtensions.cs b/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs similarity index 84% rename from SpaceWarp/InternalUtilities/InternalExtensions.cs rename to SpaceWarp.Core/InternalUtilities/InternalExtensions.cs index 5e0d1d35..46db62f8 100644 --- a/SpaceWarp/InternalUtilities/InternalExtensions.cs +++ b/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using UnityEngine; namespace SpaceWarp.InternalUtilities; diff --git a/SpaceWarp/InternalUtilities/PathHelpers.cs b/SpaceWarp.Core/InternalUtilities/PathHelpers.cs similarity index 96% rename from SpaceWarp/InternalUtilities/PathHelpers.cs rename to SpaceWarp.Core/InternalUtilities/PathHelpers.cs index 7e98f293..49eb5e80 100644 --- a/SpaceWarp/InternalUtilities/PathHelpers.cs +++ b/SpaceWarp.Core/InternalUtilities/PathHelpers.cs @@ -6,7 +6,7 @@ namespace SpaceWarp.InternalUtilities; internal static class PathHelpers { /// - /// Creates a relative path from one file or folder to another. + /// Creates a relative path from one file or folder to another. /// /// Contains the directory that defines the start of the relative path. /// Contains the path that defines the endpoint of the relative path. diff --git a/SpaceWarp.Core/Modules/ModuleManager.cs b/SpaceWarp.Core/Modules/ModuleManager.cs new file mode 100644 index 00000000..29876ccd --- /dev/null +++ b/SpaceWarp.Core/Modules/ModuleManager.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using HarmonyLib; +using SpaceWarp.API.Configuration; +using SpaceWarp.API.Logging; +using ILogger = SpaceWarp.API.Logging.ILogger; + +namespace SpaceWarp.Modules; + +public static class ModuleManager +{ + internal static List AllSpaceWarpModules = new(); + private static readonly ILogger ModuleManagerLogSource = new UnityLogSource("SpaceWarp.ModuleManager"); + + public static bool TryGetModule(string name, out SpaceWarpModule module) + { + module = AllSpaceWarpModules.FirstOrDefault(x => x.Name == name); + return module != null; + } + + internal static void LoadAllModules() + { + var location = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; + var modules = new DirectoryInfo(Path.Combine(location!.FullName, "modules")); + ModuleManagerLogSource.LogInfo($"Modules location: {modules}"); + var configDirectory = new DirectoryInfo(Path.Combine(location!.FullName, "config")); + if (!Directory.Exists(configDirectory.FullName)) configDirectory.Create(); + foreach (var module in modules.EnumerateFiles("*.dll", SearchOption.TopDirectoryOnly)) + { + try + { + var assembly = Assembly.LoadFile(module.FullName); + // AllSpaceWarpModules.AddRange(assembly.GetExportedTypes() + // .Where(type => typeof(SpaceWarpModule).IsAssignableFrom(type)).Select(Activator.CreateInstance) + // .Cast()); + foreach (var type in assembly.GetTypes().Where(type => typeof(SpaceWarpModule).IsAssignableFrom(type))) + { + ModuleManagerLogSource.LogInfo($"Loading module of type: {type}"); + var mod = (SpaceWarpModule)Activator.CreateInstance(type); + ModuleManagerLogSource.LogInfo($"Module name: {mod.Name}"); + AllSpaceWarpModules.Add(mod); + } + + Harmony.CreateAndPatchAll(assembly); + } + catch (Exception e) + { + ModuleManagerLogSource.LogError($"Could not load module(s) from path {module} due to error: {e}"); + } + } + + + TopologicallySortModules(); + List toRemove = new(); + + foreach (var module in AllSpaceWarpModules) + { + try + { + module.ModuleLogger = new UnityLogSource(module.Name); + module.ModuleConfiguration = new JsonConfigFile( + Path.Combine(configDirectory.FullName, module.Name + ".cfg") + ); + module.LoadModule(); + } + catch (Exception e) + { + ModuleManagerLogSource.LogError( + $"Error loading module {module.Name} due to error: {e}.\n Removing {module.Name} from further initialization"); + toRemove.Add(module); + } + } + + foreach (var module in toRemove) + { + AllSpaceWarpModules.Remove(module); + } + } + + private static void TopologicallySortModules() + { + var topologicalOrder = new List(); + var clone = AllSpaceWarpModules.ToList(); + + var changed = true; + while (changed) + { + changed = false; + for (var i = clone.Count - 1; i >= 0; i--) + { + var module = clone[i]; + var resolved = module.Prerequisites.All(prerequisite => + AllSpaceWarpModules.All(x => x.Name != prerequisite) || + topologicalOrder.Any(x => x.Name == prerequisite) + ); + changed = changed || resolved; + if (!resolved) continue; + clone.RemoveAt(i); + topologicalOrder.Add(module); + } + } + + AllSpaceWarpModules = topologicalOrder; + } + + internal static void PreInitializeAllModules() + { + List toRemove = new(); + foreach (var module in AllSpaceWarpModules) + { + try + { + ModuleManagerLogSource.LogInfo($"Pre-initializing: {module.Name}"); + module.PreInitializeModule(); + } + catch (Exception e) + { + ModuleManagerLogSource.LogError( + $"Error pre-initializing module {module.Name} due to error: {e}.\n Removing {module.Name} from further initialization"); + toRemove.Add(module); + } + } + + foreach (var module in toRemove) + { + AllSpaceWarpModules.Remove(module); + } + } + + internal static void InitializeAllModules() + { + List toRemove = new(); + foreach (var module in AllSpaceWarpModules) + { + try + { + ModuleManagerLogSource.LogInfo($"Initializing: {module.Name}"); + module.InitializeModule(); + } + catch (Exception e) + { + ModuleManagerLogSource.LogError( + $"Error initializing module {module.Name} due to error: {e}.\n Removing {module.Name} from further initialization"); + toRemove.Add(module); + } + } + + foreach (var module in toRemove) + { + AllSpaceWarpModules.Remove(module); + } + } + + internal static void PostInitializeAllModules() + { + foreach (var module in AllSpaceWarpModules) + { + try + { + ModuleManagerLogSource.LogInfo($"Post-Initializing: {module.Name}"); + module.PostInitializeModule(); + } + catch (Exception e) + { + ModuleManagerLogSource.LogError( + $"Error post-initializing module {module.Name} due to error: {e}."); + } + } + } +} \ No newline at end of file diff --git a/SpaceWarp.Core/Modules/SpaceWarpModule.cs b/SpaceWarp.Core/Modules/SpaceWarpModule.cs new file mode 100644 index 00000000..d2f89bd0 --- /dev/null +++ b/SpaceWarp.Core/Modules/SpaceWarpModule.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using SpaceWarp.API.Configuration; +using SpaceWarp.API.Logging; + +namespace SpaceWarp.Modules; + +public abstract class SpaceWarpModule +{ + public ILogger ModuleLogger; + public IConfigFile ModuleConfiguration; + + public abstract string Name { get; } + + public abstract void LoadModule(); + + public abstract void PreInitializeModule(); + + public abstract void InitializeModule(); + + public abstract void PostInitializeModule(); + + public virtual List Prerequisites => new(); +} \ No newline at end of file diff --git a/SpaceWarp.Core/Patching/BootstrapPatch.cs b/SpaceWarp.Core/Patching/BootstrapPatch.cs new file mode 100644 index 00000000..29a080fa --- /dev/null +++ b/SpaceWarp.Core/Patching/BootstrapPatch.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using HarmonyLib; +using KSP.Game; +using KSP.Game.Flow; +using MonoMod.Cil; +using SpaceWarp.API.Loading; +using SpaceWarp.API.Mods; +using SpaceWarp.Backend.Modding; +using SpaceWarp.Patching.LoadingActions; + +namespace SpaceWarp.Patching; + +[HarmonyPatch] +internal static class BootstrapPatch +{ + internal static bool ForceSpaceWarpLoadDueToError = false; + internal static SpaceWarpPluginDescriptor ErroredSWPluginDescriptor; + + [HarmonyPatch(typeof(GameManager), nameof(GameManager.Awake))] + [HarmonyPrefix] + private static void GetAllMods() + { + PluginRegister.RegisterAllMods(); + PluginList.ResolveDependenciesAndLoadOrder(); + } + + [HarmonyILManipulator] + [HarmonyPatch(typeof(GameManager), nameof(GameManager.StartBootstrap))] + private static void PatchInitializationsIL(ILContext ilContext, ILLabel endLabel) + { + ILCursor ilCursor = new(ilContext); + + var flowProp = AccessTools.DeclaredProperty(typeof(GameManager), nameof(GameManager.LoadingFlow)); + + ilCursor.GotoNext( + MoveType.After, + instruction => instruction.MatchCallOrCallvirt(flowProp.SetMethod) + ); + + ilCursor.EmitDelegate(InjectBeforeGameLoadMethods); + + ilCursor.GotoLabel(endLabel, MoveType.Before); + ilCursor.Index -= 1; + ilCursor.EmitDelegate(InjectAfterGameLoadMethods); + } + + private static void InjectAfterGameLoadMethods() + { + var flow = GameManager.Instance.LoadingFlow; + var allPlugins = GetAllPlugins(); + + LatePreinitialize(allPlugins); + DoLoadingActions(allPlugins, flow); + foreach (var actionGenerator in Loading.GeneralLoadingActions) + { + flow.AddAction(actionGenerator()); + } + + foreach (var plugin in allPlugins) + { + flow.AddAction(new InitializeModAction(plugin)); + } + + foreach (var plugin in allPlugins) + { + flow.AddAction(new LoadLuaAction(plugin)); + } + + foreach (var plugin in allPlugins) + { + flow.AddAction(new PostInitializeModAction(plugin)); + } + } + + private static void DoLoadingActions(IList allPlugins, SequentialFlow flow) + { + foreach (var plugin in allPlugins) + { + flow.AddAction(new LoadAddressablesAction(plugin)); + flow.AddAction(new LoadLocalizationAction(plugin)); + DoOldStyleLoadingActions(flow, plugin); + + foreach (var action in Loading.DescriptorLoadingActionGenerators) + { + flow.AddAction(action(plugin)); + } + } + } + + private static void DoOldStyleLoadingActions(SequentialFlow flow, SpaceWarpPluginDescriptor plugin) + { + if (plugin.Plugin is not BaseSpaceWarpPlugin baseSpaceWarpPlugin) return; + foreach (var action in Loading.LoadingActionGenerators) + { + flow.AddAction(action(baseSpaceWarpPlugin)); + } + } + + private static void LatePreinitialize(IList allPlugins) + { + foreach (var plugin in allPlugins.Where(plugin => plugin.LatePreInitialize)) + { + GameManager.Instance.LoadingFlow.AddAction(new PreInitializeModAction(plugin)); + } + } + + private static IList GetAllPlugins() + { + IList allPlugins; + if (ForceSpaceWarpLoadDueToError) + { + var l = new List { ErroredSWPluginDescriptor }; + l.AddRange(PluginList.AllEnabledAndActivePlugins); + allPlugins = l; + } + else + { + allPlugins = PluginList.AllPlugins.ToList(); + } + + return allPlugins; + } + + private static void InjectBeforeGameLoadMethods() + { + if (ForceSpaceWarpLoadDueToError) + { + GameManager.Instance.LoadingFlow.AddAction(new PreInitializeModAction(ErroredSWPluginDescriptor)); + } + + foreach (var plugin in PluginList.AllEnabledAndActivePlugins) + { + if (plugin.Plugin != null && !plugin.LatePreInitialize) + GameManager.Instance.LoadingFlow.AddAction(new PreInitializeModAction(plugin)); + } + } +} \ No newline at end of file diff --git a/SpaceWarp.Core/Patching/CodeGenerationUtilities.cs b/SpaceWarp.Core/Patching/CodeGenerationUtilities.cs new file mode 100644 index 00000000..60fcee96 --- /dev/null +++ b/SpaceWarp.Core/Patching/CodeGenerationUtilities.cs @@ -0,0 +1,29 @@ +using HarmonyLib; +using System.Reflection.Emit; + +namespace SpaceWarp.Patching; + +internal static class CodeGenerationUtilities +{ + /// + /// Creates a CodeInstruction that pushed an integer to the stack. + /// + /// The integer to push + /// An new CodeInstruction to push i + internal static CodeInstruction PushIntInstruction(int i) + { + return i switch + { + 0 => new CodeInstruction(OpCodes.Ldc_I4_0), + 1 => new CodeInstruction(OpCodes.Ldc_I4_1), + 2 => new CodeInstruction(OpCodes.Ldc_I4_2), + 3 => new CodeInstruction(OpCodes.Ldc_I4_3), + 4 => new CodeInstruction(OpCodes.Ldc_I4_4), + 5 => new CodeInstruction(OpCodes.Ldc_I4_5), + 6 => new CodeInstruction(OpCodes.Ldc_I4_6), + 7 => new CodeInstruction(OpCodes.Ldc_I4_7), + 8 => new CodeInstruction(OpCodes.Ldc_I4_8), + _ => new CodeInstruction(OpCodes.Ldc_I4, i) + }; + } +} \ No newline at end of file diff --git a/SpaceWarp/Patching/ColorsPatch.cs b/SpaceWarp.Core/Patching/ColorsPatch.cs similarity index 60% rename from SpaceWarp/Patching/ColorsPatch.cs rename to SpaceWarp.Core/Patching/ColorsPatch.cs index 4acbb2c2..5fac92af 100644 --- a/SpaceWarp/Patching/ColorsPatch.cs +++ b/SpaceWarp.Core/Patching/ColorsPatch.cs @@ -1,70 +1,58 @@ using System.Collections.Generic; using System.Reflection; using BepInEx.Logging; +using Castle.Core.Internal; using HarmonyLib; using KSP.Game; using KSP.Modules; using KSP.OAB; -using KSP.Sim; -using KSP.Sim.impl; using SpaceWarp.API.Assets; using UnityEngine; namespace SpaceWarp.Patching; /// -/// This patch is meant to give modders a way to use the new colors system on KSP2. -/// The patch will replace any renderer that has a "Parts Replace" or a "KSP2/Parts/Paintable" shader on it. -/// It will copy all its values onto the new material, including the material name. -/// Note: "Parts Replace" is obsolete and might be deleted on a later version. -/// Patch created by LuxStice. +/// This patch is meant to give modders a way to use the new colors system on KSP2. +/// The patch will replace any renderer that has a "Parts Replace" or a "KSP2/Parts/Paintable" shader on it. +/// It will copy all its values onto the new material, including the material name. +/// Note: "Parts Replace" is obsolete and might be deleted in a later version. +/// Patch created by LuxStice. /// [HarmonyPatch] internal class ColorsPatch { - private const string KSP2_OPAQUE_PATH = "KSP2/Scenery/Standard (Opaque)", - KSP2_TRANSPARENT_PATH = "KSP2/Scenery/Standard (Transparent)", - UNITY_STANDARD = "Standard"; + private const string Ksp2OpaquePath = "KSP2/Scenery/Standard (Opaque)"; + private const string Ksp2TransparentPath = "KSP2/Scenery/Standard (Transparent)"; + private const string UnityStandard = "Standard"; [HarmonyPatch(typeof(ObjectAssemblyPartTracker), nameof(ObjectAssemblyPartTracker.OnPartPrefabLoaded))] - internal static void Prefix(IObjectAssemblyAvailablePart obj, ref GameObject prefab) + public static void Prefix(IObjectAssemblyAvailablePart obj, ref GameObject prefab) { - ReplaceShader(ref prefab, Shader.Find(KSP2_OPAQUE_PATH), "Parts Replace", "KSP2/Parts/Paintable"); - } - - [HarmonyPostfix] - [HarmonyPatch(typeof(SimulationObjectView), nameof(SimulationObjectView.InitializeView))] - internal static void ApplyOnGameObjectFlight(GameObject instance, IUniverseView universe, SimulationObjectModel model) - { - ReplaceShader(ref instance, Shader.Find(KSP2_OPAQUE_PATH), "Parts Replace", "KSP2/Parts/Paintable"); - } - - internal static void ReplaceShader(ref GameObject target, Shader shaderToReplace,params string[] allowedShaders) - { - foreach (var renderer in target.GetComponentsInChildren(true)) + foreach (var renderer in prefab.GetComponentsInChildren()) { - string shaderName = renderer.material.shader.name; - if (allowedShaders.Contains(shaderName)) + var shaderName = renderer.material.shader.name; + if (shaderName is not ("Parts Replace" or "KSP2/Parts/Paintable")) continue; + Material material; + var mat = new Material(Shader.Find(Ksp2OpaquePath)) { - var mat = new Material(shaderToReplace); - mat.name = renderer.material.name; - mat.CopyPropertiesFromMaterial(renderer.material); - renderer.material = mat; - } + name = (material = renderer.material).name + }; + mat.CopyPropertiesFromMaterial(material); + renderer.material = mat; } } //Everything below this point will be removed in the next patch - private const int DIFFUSE = 0; - private const int METTALLIC = 1; - private const int BUMP = 2; - private const int OCCLUSION = 3; - private const int EMISSION = 4; - private const int PAINT_MAP = 5; - - - private const string displayName = "TTR"; //Taste the Rainbow - name by munix + private const int Diffuse = 0; + private const int Metallic = 1; + private const int Bump = 2; + private const int Occlusion = 3; + private const int Emission = 4; + private const int PaintMap = 5; + + private const string DisplayName = "TTR"; //Taste the Rainbow - name by munix private const bool LoadOnInit = true; + private static string[] _allParts; private static Dictionary _partHash; @@ -117,65 +105,63 @@ private static bool Init(MethodBase original) Shader.PropertyToID("_PaintMaskGlossMap") }; - _ksp2Opaque = Shader.Find(KSP2_OPAQUE_PATH); - _ksp2Transparent = Shader.Find(KSP2_TRANSPARENT_PATH); - _unityStandard = Shader.Find(UNITY_STANDARD); + _ksp2Opaque = Shader.Find(Ksp2OpaquePath); + _ksp2Transparent = Shader.Find(Ksp2TransparentPath); + _unityStandard = Shader.Find(UnityStandard); - Logger = BepInEx.Logging.Logger.CreateLogSource(displayName); + Logger = BepInEx.Logging.Logger.CreateLogSource(DisplayName); return true; // TODO: add config to enable/disable this patch, if disabled return false. } /// - /// Adds to internal parts list under + /// Adds to internal parts list under /// allowing them to have the patch applied. /// - /// guid of the mod that owns the parts. + /// guid of the mod that owns the parts. /// /// Collection of partNames. Names that end in XS, S, M, L or XL will be counted as the same /// part, /// - internal static void DeclareParts(string modGUID, params string[] partNameList) + internal static void DeclareParts(string modGuid, params string[] partNameList) { - DeclareParts(modGUID, partNameList.ToList()); + DeclareParts(modGuid, partNameList.ToList()); } /// - /// Adds to internal parts list under + /// Adds to internal parts list under /// allowing them to have the patch applied. /// - /// guid of the mod that owns the parts. + /// guid of the mod that owns the parts. /// /// Collection of partNames. Names that end in XS, S, M, L or XL will be counted as the same /// part. /// - internal static void DeclareParts(string modGUID, IEnumerable partNameList) + internal static void DeclareParts(string modGuid, IEnumerable partNameList) { - if (DeclaredParts.ContainsKey(modGUID)) + if (DeclaredParts.ContainsKey(modGuid)) { - LogWarning($"{modGUID} tried to declare their parts twice. Ignoring second call."); + LogWarning($"{modGuid} tried to declare their parts twice. Ignoring second call."); return; } var nameList = partNameList as string[] ?? partNameList.ToArray(); if (!nameList.Any()) { - LogWarning($"{modGUID} tried to declare no parts. Ignoring this call."); + LogWarning($"{modGuid} tried to declare no parts. Ignoring this call."); return; } - DeclaredParts.Add(modGUID, nameList.ToArray()); + DeclaredParts.Add(modGuid, nameList.ToArray()); } internal static Texture[] GetTextures(string partName) { if (_partHash.ContainsKey(partName)) return _partHash[partName]; - else - { - LogError($"Requested textures from {partName} but part doesn't exist on declared parts!"); - return null; - } + + LogError($"Requested textures from {partName} but part doesn't exist on declared parts!"); + return null; } private static void LoadDeclaredParts() @@ -190,11 +176,11 @@ private static void LoadDeclaredParts() if (LoadOnInit) { - foreach (var modGUID in DeclaredParts.Keys) + foreach (var modGuid in DeclaredParts.Keys) { - LoadTextures(modGUID); + LoadTextures(modGuid); - allPartsTemp.AddRange(DeclaredParts[modGUID].Select(partName => TrimPartName(partName))); + allPartsTemp.AddRange(DeclaredParts[modGuid].Select(partName => TrimPartName(partName))); } } @@ -233,11 +219,11 @@ private static void LoadTextures(string modGuid) var count = 0; //already has diffuse - if (AssetManager.TryGetAsset($"{pathWithoutSuffix}_{TextureSuffixes[DIFFUSE]}", out Texture2D dTex)) + if (AssetManager.TryGetAsset($"{pathWithoutSuffix}_{TextureSuffixes[Diffuse]}", out Texture2D dTex)) { - _partHash[trimmedPartName][DIFFUSE] = dTex; + _partHash[trimmedPartName][Diffuse] = dTex; count++; - LogMessage($"\t\t>({count}/6) Loaded {TextureNames[DIFFUSE]} texture"); + LogMessage($"\t\t>({count}/6) Loaded {TextureNames[Diffuse]} texture"); } else { @@ -247,19 +233,19 @@ private static void LoadTextures(string modGuid) for (int i = 1; i < _propertyIds.Length; i++) { - if (AssetManager.TryGetAsset($"{pathWithoutSuffix}_{TextureSuffixes[i]}", out Texture2D Tex)) + if (!AssetManager.TryGetAsset($"{pathWithoutSuffix}_{TextureSuffixes[i]}", out Texture2D tex)) continue; + + count++; + + if (i == Bump) //Converting texture to Bump texture { - count++; - - if (i == ColorsPatch.BUMP) //Converting texture to Bump texture - { - Texture2D normalTexture = new Texture2D(Tex.width, Tex.height, TextureFormat.RGBA32, false, true); - Graphics.CopyTexture(Tex, normalTexture); - Tex = normalTexture; - } - _partHash[trimmedPartName][i] = Tex; - LogMessage($"\t\t>({count}/6) Loaded {TextureNames[i]} texture"); + var normalTexture = new Texture2D(tex.width, tex.height, TextureFormat.RGBA32, false, true); + Graphics.CopyTexture(tex, normalTexture); + tex = normalTexture; } + + _partHash[trimmedPartName][i] = tex; + LogMessage($"\t\t>({count}/6) Loaded {TextureNames[i]} texture"); } if (count == 6) @@ -317,30 +303,33 @@ internal static void Prefix() internal static void Postfix(Module_Color __instance) { var partName = __instance.OABPart is not null ? __instance.OABPart.PartName : __instance.part.Name; + if (partName.IsNullOrEmpty()) return; var trimmedPartName = TrimPartName(partName); - if (DeclaredParts.Count > 0 && _allParts.Contains(trimmedPartName)) + if (DeclaredParts.Count <= 0 || !_allParts.Contains(trimmedPartName)) return; + + var mat = new Material(_ksp2Opaque) { - var mat = new Material(_ksp2Opaque); - mat.name = __instance.GetComponentInChildren().material.name; + name = __instance.GetComponentInChildren().material.name + }; - foreach (var renderer in __instance.GetComponentsInChildren(true)) + foreach (var renderer in __instance.GetComponentsInChildren(true)) + { + if (renderer.material.shader.name != _unityStandard.name) { - if (renderer.material.shader.name != _unityStandard.name) - { - continue; - } + continue; + } - SetTexturesToMaterial(trimmedPartName, ref mat); + SetTexturesToMaterial(trimmedPartName, ref mat); - renderer.material = mat; + renderer.material = mat; - if (renderer.material.shader.name != _ksp2Opaque.name) - { - renderer.SetMaterial(mat); //Sometimes the material Set doesn't work, this seems to be more reliable. - } + if (renderer.material.shader.name != _ksp2Opaque.name) + { + renderer.SetMaterial(mat); //Sometimes the material Set doesn't work, this seems to be more reliable. } - __instance.SomeColorUpdated(); } + + __instance.SomeColorUpdated(); } private static void LogMessage(object data) @@ -357,4 +346,4 @@ private static void LogError(object data) { Logger.LogError($"{data}"); } -} +} \ No newline at end of file diff --git a/SpaceWarp/Patching/FixGetTypes.cs b/SpaceWarp.Core/Patching/FixGetTypes.cs similarity index 80% rename from SpaceWarp/Patching/FixGetTypes.cs rename to SpaceWarp.Core/Patching/FixGetTypes.cs index b10319d8..19ddcf90 100644 --- a/SpaceWarp/Patching/FixGetTypes.cs +++ b/SpaceWarp.Core/Patching/FixGetTypes.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using BepInEx.Logging; using HarmonyLib; namespace SpaceWarp.Patching; @@ -17,13 +18,15 @@ private static Exception GetTypesFix(Exception __exception, Assembly __instance, return __exception; } - SpaceWarpManager.Logger.LogWarning( + var logger = new ManualLogSource("FixGetTypes"); + + logger.LogWarning( $"Types failed to load from assembly {__instance.FullName} due to the reasons below, continuing anyway."); - SpaceWarpManager.Logger.LogWarning($"Exception: {__exception}"); + logger.LogWarning($"Exception: {__exception}"); foreach (var exception in reflectionTypeLoadException.LoaderExceptions) { - SpaceWarpManager.Logger.LogWarning(exception.ToString()); + logger.LogWarning(exception.ToString()); } __result = reflectionTypeLoadException.Types.Where(type => type != null).ToArray(); diff --git a/SpaceWarp/Patching/LoadingActions/AddressableAction.cs b/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs similarity index 90% rename from SpaceWarp/Patching/LoadingActions/AddressableAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs index a9fdd2e3..47dac15c 100644 --- a/SpaceWarp/Patching/LoadingActions/AddressableAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/AddressableAction.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using KSP.Game; using KSP.Game.Flow; using UnityEngine; @@ -7,11 +8,13 @@ namespace SpaceWarp.Patching.LoadingActions; +[Obsolete("This will be moved to SpaceWarp.API.Loading in 2.0.0")] +[PublicAPI] public class AddressableAction : FlowAction where T : UnityObject { private string Label; private Action Action; - + public AddressableAction(string name, string label, Action action) : base(name) { Label = label; diff --git a/SpaceWarp/Patching/LoadingActions/DescriptorLoadingAction.cs b/SpaceWarp.Core/Patching/LoadingActions/DescriptorLoadingAction.cs similarity index 64% rename from SpaceWarp/Patching/LoadingActions/DescriptorLoadingAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/DescriptorLoadingAction.cs index f1d5f237..b1ab21d0 100644 --- a/SpaceWarp/Patching/LoadingActions/DescriptorLoadingAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/DescriptorLoadingAction.cs @@ -1,17 +1,22 @@ using System; +using JetBrains.Annotations; using KSP.Game.Flow; using SpaceWarp.API.Mods; namespace SpaceWarp.Patching.LoadingActions; +[Obsolete("This will be moved to SpaceWarp.API.Loading in 2.0.0")] +[PublicAPI] public class DescriptorLoadingAction : FlowAction { private readonly Action _action; private readonly SpaceWarpPluginDescriptor _plugin; - - - public DescriptorLoadingAction(string actionName, Action action, SpaceWarpPluginDescriptor plugin) : base( - $"{plugin.SWInfo.Name}: {actionName}") + + public DescriptorLoadingAction( + string actionName, + Action action, + SpaceWarpPluginDescriptor plugin + ) : base($"{plugin.SWInfo.Name}: {actionName}") { _action = action; _plugin = plugin; @@ -21,7 +26,9 @@ public override void DoAction(Action resolve, Action reject) { try { - _action(_plugin); + if (_plugin.DoLoadingActions) + _action(_plugin); + resolve(); } catch (Exception e) @@ -29,7 +36,8 @@ public override void DoAction(Action resolve, Action reject) if (_plugin.Plugin != null) _plugin.Plugin.SWLogger.LogError(e.ToString()); else - SpaceWarpPlugin.Logger.LogError(_plugin.SWInfo.Name + ": " + e); + SpaceWarpPlugin.Logger.LogError(_plugin.SWInfo.Name + ": " + e); + reject(null); } } diff --git a/SpaceWarp/Patching/LoadingActions/FunctionalLoadingActions.cs b/SpaceWarp.Core/Patching/LoadingActions/FunctionalLoadingActions.cs similarity index 63% rename from SpaceWarp/Patching/LoadingActions/FunctionalLoadingActions.cs rename to SpaceWarp.Core/Patching/LoadingActions/FunctionalLoadingActions.cs index 76283d90..ac4eec67 100644 --- a/SpaceWarp/Patching/LoadingActions/FunctionalLoadingActions.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/FunctionalLoadingActions.cs @@ -1,20 +1,16 @@ -using AK.Wwise; -using SpaceWarp.API.Sound; -using SpaceWarp.Backend.Sound; -using System; +using System; using System.Collections.Generic; -using System.Drawing; using System.IO; -using System.Runtime.InteropServices; -using Unity.Collections; using UnityEngine; -using UnityEngine.Assertions; namespace SpaceWarp.Patching.LoadingActions; internal static class FunctionalLoadingActions { - internal static List<(string name, UnityObject asset)> AssetBundleLoadingAction(string internalPath, string filename) + internal static List<(string name, UnityObject asset)> AssetBundleLoadingAction( + string internalPath, + string filename + ) { var assetBundle = AssetBundle.LoadFromFile(filename); if (assetBundle == null) @@ -49,21 +45,6 @@ internal static class FunctionalLoadingActions return assets; } - internal static List<(string name, UnityObject asset)> AssetSoundbankLoadingAction(string internalPath, string filename) - { - var fileData = File.ReadAllBytes(filename); - //Since theres no UnityObject that relates to soundbanks it passes null, saving only the internalpath - List<(string name, UnityObject asset)> assets = new() { ($"soundbanks/{internalPath}", null) }; - - //Banks are identified under Bank.soundbanks with their internal path - if (SoundAPI.LoadBank($"soundbanks/{internalPath}", fileData, out var bank)) - { - return assets; - } - else - throw new Exception( - $"Failed to load soundbank {internalPath}"); - } internal static List<(string name, UnityObject asset)> ImageLoadingAction(string internalPath, string filename) { diff --git a/SpaceWarp/Patching/LoadingActions/InitializeModAction.cs b/SpaceWarp.Core/Patching/LoadingActions/InitializeModAction.cs similarity index 76% rename from SpaceWarp/Patching/LoadingActions/InitializeModAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/InitializeModAction.cs index fba6995c..04ad68eb 100644 --- a/SpaceWarp/Patching/LoadingActions/InitializeModAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/InitializeModAction.cs @@ -18,12 +18,13 @@ public override void DoAction(Action resolve, Action reject) { try { - _plugin.Plugin.OnInitialized(); + if (_plugin.DoLoadingActions) + _plugin.Plugin.OnInitialized(); resolve(); } catch (Exception e) { - _plugin.Plugin.SWLogger.LogError(e.ToString()); + (_plugin.Plugin ?? SpaceWarpPlugin.Instance).SWLogger.LogError(e.ToString()); reject(null); } } diff --git a/SpaceWarp/Patching/LoadingActions/LoadAddressablesAction.cs b/SpaceWarp.Core/Patching/LoadingActions/LoadAddressablesAction.cs similarity index 100% rename from SpaceWarp/Patching/LoadingActions/LoadAddressablesAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/LoadAddressablesAction.cs diff --git a/SpaceWarp/Patching/LoadingActions/LoadLocalizationAction.cs b/SpaceWarp.Core/Patching/LoadingActions/LoadLocalizationAction.cs similarity index 90% rename from SpaceWarp/Patching/LoadingActions/LoadLocalizationAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/LoadLocalizationAction.cs index 60add6f6..69646912 100644 --- a/SpaceWarp/Patching/LoadingActions/LoadLocalizationAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/LoadLocalizationAction.cs @@ -10,8 +10,8 @@ internal sealed class LoadLocalizationAction : FlowAction { private readonly SpaceWarpPluginDescriptor _plugin; - public LoadLocalizationAction(SpaceWarpPluginDescriptor plugin) : base( - $"Loading localizations for plugin {plugin.SWInfo.Name}") + public LoadLocalizationAction(SpaceWarpPluginDescriptor plugin) + : base($"Loading localizations for plugin {plugin.SWInfo.Name}") { _plugin = plugin; } @@ -29,7 +29,7 @@ public override void DoAction(Action resolve, Action reject) if (_plugin.Plugin != null) _plugin.Plugin.SWLogger.LogError(e.ToString()); else - SpaceWarpPlugin.Logger.LogError(_plugin.SWInfo.Name + ": " + e); + SpaceWarpPlugin.Logger.LogError(_plugin.SWInfo.Name + ": " + e); reject(null); } } diff --git a/SpaceWarp/Patching/LoadingActions/LoadLuaAction.cs b/SpaceWarp.Core/Patching/LoadingActions/LoadLuaAction.cs similarity index 90% rename from SpaceWarp/Patching/LoadingActions/LoadLuaAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/LoadLuaAction.cs index 8f328b7e..0403bc07 100644 --- a/SpaceWarp/Patching/LoadingActions/LoadLuaAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/LoadLuaAction.cs @@ -9,8 +9,8 @@ internal sealed class LoadLuaAction : FlowAction { private readonly SpaceWarpPluginDescriptor _plugin; - public LoadLuaAction(SpaceWarpPluginDescriptor plugin) : base( - $"Running lua scripts for {plugin.SWInfo.Name}") + public LoadLuaAction(SpaceWarpPluginDescriptor plugin) + : base($"Running lua scripts for {plugin.SWInfo.Name}") { _plugin = plugin; } @@ -34,9 +34,10 @@ public override void DoAction(Action resolve, Action reject) if (_plugin.Plugin != null) _plugin.Plugin.SWLogger.LogError(e.ToString()); else - SpaceWarpPlugin.Logger.LogError(_plugin.SWInfo.Name + ": " + e); + SpaceWarpPlugin.Logger.LogError(_plugin.SWInfo.Name + ": " + e); } } + resolve(); } catch (Exception e) @@ -44,7 +45,7 @@ public override void DoAction(Action resolve, Action reject) if (_plugin.Plugin != null) _plugin.Plugin.SWLogger.LogError(e.ToString()); else - SpaceWarpPlugin.Logger.LogError(_plugin.SWInfo.Name + ": " + e); + SpaceWarpPlugin.Logger.LogError(_plugin.SWInfo.Name + ": " + e); reject(null); } } diff --git a/SpaceWarp/Patching/LoadingActions/LoadingAddressablesLocalizationsAction.cs b/SpaceWarp.Core/Patching/LoadingActions/LoadingAddressablesLocalizationsAction.cs similarity index 82% rename from SpaceWarp/Patching/LoadingActions/LoadingAddressablesLocalizationsAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/LoadingAddressablesLocalizationsAction.cs index b6c0ec9b..dc0e075b 100644 --- a/SpaceWarp/Patching/LoadingActions/LoadingAddressablesLocalizationsAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/LoadingAddressablesLocalizationsAction.cs @@ -18,8 +18,10 @@ public override void DoAction(Action resolve, Action reject) { try { - GameManager.Instance.Assets.LoadByLabel("language_source", - OnLanguageSourceAssetLoaded, delegate(IList languageAssetLocations) + GameManager.Instance.Assets.LoadByLabel( + "language_source", + OnLanguageSourceAssetLoaded, + delegate(IList languageAssetLocations) { if (languageAssetLocations != null) { @@ -27,7 +29,8 @@ public override void DoAction(Action resolve, Action reject) } resolve(); - }); + } + ); } catch (Exception e) { diff --git a/SpaceWarp/Patching/LoadingActions/ModLoadingAction.cs b/SpaceWarp.Core/Patching/LoadingActions/ModLoadingAction.cs similarity index 66% rename from SpaceWarp/Patching/LoadingActions/ModLoadingAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/ModLoadingAction.cs index a38ac489..e595db71 100644 --- a/SpaceWarp/Patching/LoadingActions/ModLoadingAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/ModLoadingAction.cs @@ -1,17 +1,23 @@ using System; +using JetBrains.Annotations; using KSP.Game.Flow; using SpaceWarp.API.Mods; namespace SpaceWarp.Patching.LoadingActions; +[Obsolete("This will be moved to SpaceWarp.API.Loading in 2.0.0")] +[PublicAPI] public class ModLoadingAction : FlowAction { private Action Action; private BaseSpaceWarpPlugin Plugin; - - - public ModLoadingAction(string actionName, Action action, BaseSpaceWarpPlugin plugin) : base( - $"{plugin.SpaceWarpMetadata.Name}:{actionName}") + + + public ModLoadingAction( + string actionName, + Action action, + BaseSpaceWarpPlugin plugin + ) : base($"{plugin.SpaceWarpMetadata.Name}:{actionName}") { Action = action; Plugin = plugin; diff --git a/SpaceWarp/Patching/LoadingActions/PostInitializeModAction.cs b/SpaceWarp.Core/Patching/LoadingActions/PostInitializeModAction.cs similarity index 68% rename from SpaceWarp/Patching/LoadingActions/PostInitializeModAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/PostInitializeModAction.cs index 17eb4ef9..e8c5894d 100644 --- a/SpaceWarp/Patching/LoadingActions/PostInitializeModAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/PostInitializeModAction.cs @@ -8,8 +8,8 @@ internal sealed class PostInitializeModAction : FlowAction { private readonly SpaceWarpPluginDescriptor _plugin; - public PostInitializeModAction(SpaceWarpPluginDescriptor plugin) : base( - $"Post-initialization for plugin {plugin.Name}") + public PostInitializeModAction(SpaceWarpPluginDescriptor plugin) + : base($"Post-initialization for plugin {plugin.Name}") { _plugin = plugin; } @@ -18,12 +18,13 @@ public override void DoAction(Action resolve, Action reject) { try { - _plugin.Plugin.OnPostInitialized(); + if (_plugin.DoLoadingActions) + _plugin.Plugin.OnPostInitialized(); resolve(); } catch (Exception e) { - _plugin.Plugin.SWLogger.LogError(e.ToString()); + (_plugin.Plugin ?? SpaceWarpPlugin.Instance).SWLogger.LogError(e.ToString()); reject(null); } } diff --git a/SpaceWarp/Patching/LoadingActions/PreInitializeModAction.cs b/SpaceWarp.Core/Patching/LoadingActions/PreInitializeModAction.cs similarity index 50% rename from SpaceWarp/Patching/LoadingActions/PreInitializeModAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/PreInitializeModAction.cs index ef25af9b..a8aac19c 100644 --- a/SpaceWarp/Patching/LoadingActions/PreInitializeModAction.cs +++ b/SpaceWarp.Core/Patching/LoadingActions/PreInitializeModAction.cs @@ -8,22 +8,31 @@ internal sealed class PreInitializeModAction : FlowAction { private readonly SpaceWarpPluginDescriptor _plugin; - public PreInitializeModAction(SpaceWarpPluginDescriptor plugin) : base( - $"Pre-initialization for plugin {plugin.Name}") + public PreInitializeModAction(SpaceWarpPluginDescriptor plugin) + : base($"Pre-initialization for plugin {plugin.Name}") { _plugin = plugin; } public override void DoAction(Action resolve, Action reject) { + SpaceWarpPlugin.Logger.LogInfo($"Pre-initializing: {_plugin.Name}?"); try { - _plugin.Plugin?.OnPreInitialized(); + if (_plugin.DoLoadingActions) + { + SpaceWarpPlugin.Logger.LogInfo($"YES! {_plugin.Plugin}"); + _plugin.Plugin.OnPreInitialized(); + } + else + { + SpaceWarpPlugin.Logger.LogInfo("NO!!"); + } resolve(); } catch (Exception e) { - _plugin.Plugin?.SWLogger.LogError(e.ToString()); + (_plugin.Plugin ?? SpaceWarpPlugin.Instance).SWLogger.LogError(e.ToString()); reject(null); } } diff --git a/SpaceWarp/Patching/LoadingActions/ResolvingPatchOrderAction.cs b/SpaceWarp.Core/Patching/LoadingActions/ResolvingPatchOrderAction.cs similarity index 100% rename from SpaceWarp/Patching/LoadingActions/ResolvingPatchOrderAction.cs rename to SpaceWarp.Core/Patching/LoadingActions/ResolvingPatchOrderAction.cs diff --git a/SpaceWarp.Core/Patching/ModLoaderPatch.cs b/SpaceWarp.Core/Patching/ModLoaderPatch.cs new file mode 100644 index 00000000..98d10a64 --- /dev/null +++ b/SpaceWarp.Core/Patching/ModLoaderPatch.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using HarmonyLib; +using KSP.Modding; +using Newtonsoft.Json; +using SpaceWarp.API.Configuration; +using SpaceWarp.API.Logging; +using SpaceWarp.API.Mods; +using SpaceWarp.API.Mods.JSON; +using SpaceWarp.Backend.Modding; +using SpaceWarp.InternalUtilities; +using SpaceWarpPatcher; +using UnityEngine; +using File = System.IO.File; + +namespace SpaceWarp.Patching; + +[HarmonyPatch(typeof(KSP2Mod))] +public static class ModLoaderPatch +{ + private static ModInfo KSPToSwinfo(KSP2Mod mod) + { + var newInfo = new ModInfo + { + Spec = SpecVersion.V1_3, + ModID = mod.ModName, + Name = mod.ModName, + Author = mod.ModAuthor, + Description = mod.ModDescription, + Source = "", + Version = mod.ModVersion.ToString(), + Dependencies = new List(), + SupportedKsp2Versions = new SupportedVersionsInfo + { + Min = "*", + Max = "*" + }, + VersionCheck = null, + VersionCheckType = VersionCheckType.SwInfo + }; + return newInfo; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(KSP2Mod.Load))] + private static bool LoadPre(KSP2Mod __instance, ref bool __result, out bool __state) + { + SpaceWarpPlugin.Logger.LogInfo($"KSP2Mod.Load (Pre): {__instance.ModName}"); + var path = __instance.ModRootPath; + var info = File.Exists(Path.Combine(path, "swinfo.json")) + ? JsonConvert.DeserializeObject(File.ReadAllText(path)) + : KSPToSwinfo(__instance); + var disabled = ChainloaderPatch.DisabledPluginGuids.Contains(info.ModID); + __state = disabled; + return !disabled; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(KSP2Mod.Load))] + private static void LoadPost(KSP2Mod __instance, ref bool __result, ref bool __state) + { + SpaceWarpPlugin.Logger.LogInfo($"KSP2Mod.Load (Post): {__instance.ModName}"); + if (__state) return; + var path = __instance.ModRootPath; + var info = File.Exists(Path.Combine(path, "swinfo.json")) + ? JsonConvert.DeserializeObject(File.ReadAllText(path)) + : KSPToSwinfo(__instance); + var descriptor = + PluginList.AllPlugins.FirstOrDefault(x => + string.Equals(x.Guid, info.ModID, StringComparison.InvariantCultureIgnoreCase)); + if (descriptor == null) return; + var go = new GameObject(__instance.ModName); + go.Persist(); + var addAdapter = true; + if (__instance.EntryPoint != null) + { + if (LoadCodeBasedMod(__instance, ref __result, go, ref descriptor.Plugin, ref addAdapter, + ref descriptor.ConfigFile, ref descriptor.DoLoadingActions)) return; + } + else + { + __instance.modType = KSP2ModType.ContentOnly; + } + + SpaceWarpPlugin.Logger.LogInfo($"KSP2Mod.Load (Loaded stuff): {__instance.ModName}"); + + if (addAdapter) + { + var adapter = go.AddComponent(); + adapter.AdaptedMod = __instance; + descriptor.Plugin = adapter; + } + + descriptor.Plugin!.SWMetadata = descriptor; + + go.SetActive(true); + // var descriptor = new SpaceWarpPluginDescriptor(swMod, info.ModID, info.Name, info, new DirectoryInfo(path), configFile); + } + + private static bool LoadCodeBasedMod( + KSP2Mod __instance, + ref bool __result, + GameObject go, + ref ISpaceWarpMod swMod, + ref bool addAdapter, + ref IConfigFile configFile, + ref bool isSWMod + ) + { + // Lets take a simple guess at what needs to be done. + if (File.Exists(Path.Combine(__instance.ModRootPath, __instance.EntryPoint))) + { + if (__instance.EntryPoint.EndsWith(".dll")) + { + if (LoadModWithDLLEntryPoint(__instance, ref __result, go, ref swMod, ref addAdapter, ref configFile, + ref isSWMod)) return true; + } + else if (__instance.EntryPoint.EndsWith(".lua")) + { + if (LoadModWithLuaEntryPoint(__instance, ref __result)) return true; + } + } + else + { + __instance.modType = KSP2ModType.Error; + __result = false; + return true; + } + + return false; + } + + private static bool LoadModWithLuaEntryPoint(KSP2Mod __instance, ref bool __result) + { + try + { + __instance.modCore = new KSP2LuaModCore( + __instance.APIVersion, + __instance.ModName, + __instance.EntryPoint, + __instance.ModRootPath + ); + __instance.modType = KSP2ModType.Lua; + __instance.currentState = KSP2ModState.Active; + } + catch (Exception e) + { + SpaceWarpPlugin.Logger.LogError(e); + __instance.modType = KSP2ModType.Error; + __result = false; + return true; + } + + return false; + } + + private static bool LoadModWithDLLEntryPoint( + KSP2Mod __instance, + ref bool __result, + GameObject go, + ref ISpaceWarpMod swMod, + ref bool addAdapter, + ref IConfigFile configFile, + ref bool isSWMod + ) + { + try + { + var asm = Assembly.LoadFile(Path.Combine(__instance.ModRootPath, __instance.EntryPoint)); + __instance.modType = KSP2ModType.CSharp; + foreach (var type in asm.GetTypes()) + { + if (!typeof(Mod).IsAssignableFrom(type) || type.IsAbstract) continue; + var comp = go.AddComponent(type); + if (comp is not BaseKspLoaderSpaceWarpMod baseKspLoaderSpaceWarpMod) continue; + + SpaceWarpPlugin.Logger.LogInfo($"Loading mod: {comp}"); + isSWMod = true; + baseKspLoaderSpaceWarpMod.SWLogger = new UnityLogSource(__instance.ModName); + // baseKspLoaderSpaceWarpMod.modFolder = __instance.ModRootPath; + configFile = baseKspLoaderSpaceWarpMod.SWConfiguration = new JsonConfigFile( + Path.Combine(__instance.ModRootPath, "config.cfg") + ); + swMod = baseKspLoaderSpaceWarpMod; + addAdapter = false; + } + + __instance.currentState = KSP2ModState.Active; + } + catch (Exception e) + { + SpaceWarpPlugin.Logger.LogError(e); + __instance.modType = KSP2ModType.Error; + __result = false; + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/SpaceWarp/Patching/SequentialFlowLoadersPatcher.cs b/SpaceWarp.Core/Patching/SequentialFlowLoadersPatcher.cs similarity index 50% rename from SpaceWarp/Patching/SequentialFlowLoadersPatcher.cs rename to SpaceWarp.Core/Patching/SequentialFlowLoadersPatcher.cs index 319eed0c..e84a7067 100644 --- a/SpaceWarp/Patching/SequentialFlowLoadersPatcher.cs +++ b/SpaceWarp.Core/Patching/SequentialFlowLoadersPatcher.cs @@ -2,83 +2,85 @@ using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; -using System.Text; -using System.Threading.Tasks; using BepInEx.Logging; using HarmonyLib; using KSP.Game; using KSP.Game.Flow; -using Newtonsoft.Json.Linq; -using static MoonSharp.Interpreter.Debugging.DebuggerAction; namespace SpaceWarp.Patching; [HarmonyPatch] internal static class SequentialFlowLoadersPatcher { - internal const int FLOW_METHOD_STARTGAME = 0; - internal const int FLOW_METHOD_PRIVATELOADCOMMON = 1; - internal const int FLOW_METHOD_PRIVATESAVECOMMON = 2; + internal const int FlowMethodStartgame = 0; + internal const int FlowMethodPrivateloadcommon = 1; + internal const int FlowMethodPrivatesavecommon = 2; - static SequentialFlowAdditions[] sequentialFlowAdditions = new SequentialFlowAdditions[] + private static SequentialFlowAdditions[] _sequentialFlowAdditions = { new(typeof(GameManager).GetMethod("StartGame")), // Must be index FLOW_METHOD_STARTGAME - new(AccessTools.Method("KSP.Game.SaveLoadManager:PrivateLoadCommon")), // Must be index FLOW_METHOD_PRIVATELOADCOMMON - new(AccessTools.Method("KSP.Game.SaveLoadManager:PrivateSaveCommon")), // Must be index FLOW_METHOD_PRIVATESAVECOMMON + new(AccessTools.Method( + "KSP.Game.SaveLoadManager:PrivateLoadCommon")), // Must be index FLOW_METHOD_PRIVATELOADCOMMON + new(AccessTools.Method( + "KSP.Game.SaveLoadManager:PrivateSaveCommon")), // Must be index FLOW_METHOD_PRIVATESAVECOMMON }; internal static void AddConstructor(string after, Type flowAction, int methodIndex) { - sequentialFlowAdditions[methodIndex].AddConstructor(after, flowAction); + _sequentialFlowAdditions[methodIndex].AddConstructor(after, flowAction); } internal static void AddFlowAction(string after, FlowAction flowAction, int methodIndex) { - sequentialFlowAdditions[methodIndex].AddAction(after, flowAction); + _sequentialFlowAdditions[methodIndex].AddAction(after, flowAction); } public static SequentialFlow Apply(SequentialFlow flow, object[] methodArguments, int methodIndex) { - sequentialFlowAdditions[methodIndex].ApplyTo(flow, methodArguments); + _sequentialFlowAdditions[methodIndex].ApplyTo(flow, methodArguments); return flow; } - static IEnumerable TranspileSequentialFlowBuilderMethod(IEnumerable instructions, int methodIndex) + private static IEnumerable TranspileSequentialFlowBuilderMethod( + IEnumerable instructions, + int methodIndex + ) { - var StartFlow = typeof(SequentialFlow).GetMethod("StartFlow"); + var startFlow = typeof(SequentialFlow).GetMethod("StartFlow"); - foreach (CodeInstruction instruction in instructions) + foreach (var instruction in instructions) { - if (instruction.opcode == OpCodes.Callvirt && StartFlow == (MethodInfo)instruction.operand) + if (instruction.opcode == OpCodes.Callvirt && startFlow == (MethodInfo)instruction.operand) { // Call to StartFlow found! // Before it occurs, insert any extra flow actions. // Get list of relevant arguments to pass to FlowAction constructors. // `parameterCount` has a `+ 1` because `GetParameters()` doesn't include the instance parameter (this). - var parameters = sequentialFlowAdditions[methodIndex].method.GetParameters().Where(parameter => !parameter.ParameterType.IsValueType).ToArray(); + var parameters = _sequentialFlowAdditions[methodIndex].method.GetParameters() + .Where(parameter => !parameter.ParameterType.IsValueType).ToArray(); var parameterCount = parameters.Length + 1; // Creation of argument `methodArguments` to `Apply()`: // Construct an array to hold the arguments. yield return CodeGenerationUtilities.PushIntInstruction(parameterCount); - yield return new(OpCodes.Newarr, typeof(object)); + yield return new CodeInstruction(OpCodes.Newarr, typeof(object)); // Assign `this` to element 0. - yield return new(OpCodes.Dup); - yield return new(OpCodes.Ldc_I4_0); - yield return new(OpCodes.Ldarg_0); - yield return new(OpCodes.Stelem_Ref); + yield return new CodeInstruction(OpCodes.Dup); + yield return new CodeInstruction(OpCodes.Ldc_I4_0); + yield return new CodeInstruction(OpCodes.Ldarg_0); + yield return new CodeInstruction(OpCodes.Stelem_Ref); // Assign the other arguments. for (int i = 1; i < parameterCount; i++) { var parameter = parameters[i - 1]; - yield return new(OpCodes.Dup); + yield return new CodeInstruction(OpCodes.Dup); yield return CodeGenerationUtilities.PushIntInstruction(i); - yield return new(OpCodes.Ldarg_S, (byte)(parameter.Position + 1)); - yield return new(OpCodes.Stelem_Ref); + yield return new CodeInstruction(OpCodes.Ldarg_S, (byte)(parameter.Position + 1)); + yield return new CodeInstruction(OpCodes.Stelem_Ref); } // Creation of argument `methodIndex` to `Apply()`: @@ -86,7 +88,10 @@ static IEnumerable TranspileSequentialFlowBuilderMethod(IEnumer // Call `Apply()`. // `flow` is already on the stack and does not need to be created. - yield return new(OpCodes.Call, typeof(SequentialFlowLoadersPatcher).GetMethod("Apply")); + yield return new CodeInstruction( + OpCodes.Call, + typeof(SequentialFlowLoadersPatcher).GetMethod("Apply") + ); } // Copy everything else. @@ -99,73 +104,80 @@ static IEnumerable TranspileSequentialFlowBuilderMethod(IEnumer [HarmonyPrefix] public static void PrefixGameManagerStartGame(GameManager __instance) { - sequentialFlowAdditions[FLOW_METHOD_STARTGAME].ApplyTo(__instance.LoadingFlow, new object[] { __instance }); + _sequentialFlowAdditions[FlowMethodStartgame].ApplyTo( + __instance.LoadingFlow, + new object[] { __instance } + ); } [HarmonyPatch(typeof(SaveLoadManager))] [HarmonyPatch("PrivateLoadCommon")] [HarmonyTranspiler] - public static IEnumerable TranspileSaveLoadManagerPrivateLoadCommon(IEnumerable instructions) + public static IEnumerable TranspileSaveLoadManagerPrivateLoadCommon( + IEnumerable instructions) { - return TranspileSequentialFlowBuilderMethod(instructions, FLOW_METHOD_PRIVATELOADCOMMON); + return TranspileSequentialFlowBuilderMethod(instructions, FlowMethodPrivateloadcommon); } [HarmonyPatch(typeof(SaveLoadManager))] [HarmonyPatch("PrivateSaveCommon")] [HarmonyTranspiler] - public static IEnumerable TranspileSaveLoadManagerPrivateSaveCommon(IEnumerable instructions) + public static IEnumerable TranspileSaveLoadManagerPrivateSaveCommon( + IEnumerable instructions) { - return TranspileSequentialFlowBuilderMethod(instructions, FLOW_METHOD_PRIVATESAVECOMMON); + return TranspileSequentialFlowBuilderMethod(instructions, FlowMethodPrivatesavecommon); } private class SequentialFlowAdditions { - readonly HashSet availableTypes; - readonly List> insertAfter = new(); - readonly internal MethodInfo method; + private readonly HashSet availableTypes; + private readonly List> insertAfter = new(); + internal readonly MethodInfo method; internal SequentialFlowAdditions(MethodInfo method) { - availableTypes = new(method.GetParameters().Select(parameter => parameter.ParameterType).Where(type => !type.IsValueType)) { method.DeclaringType }; + availableTypes = + new HashSet(method.GetParameters().Select(parameter => parameter.ParameterType) + .Where(type => !type.IsValueType)) { method.DeclaringType }; this.method = method; } internal void AddConstructor(string after, Type flowAction) { // Determine the correct constructor to use. - var constructor = flowAction.GetConstructors().OrderByDescending(constructor => constructor.GetParameters().Length).Where(constructor => - { - HashSet seen = new(); - - foreach (var parameter in constructor.GetParameters()) + var constructor = flowAction.GetConstructors() + .OrderByDescending(constructor => constructor.GetParameters().Length).Where(constructor => { - if (!availableTypes.Contains(parameter.ParameterType)) - return false; + HashSet seen = new(); - if (!seen.Add(parameter.GetType())) - return false; - } + foreach (var parameter in constructor.GetParameters()) + { + if (!availableTypes.Contains(parameter.ParameterType)) + return false; - return true; - }).FirstOrDefault() ?? throw new InvalidOperationException($"Flow action type {flowAction.Name} does not have a public constructor that has parameters compatible with {method.DeclaringType.Name}.{method.Name}"); + if (!seen.Add(parameter.GetType())) + return false; + } + + return true; + }).FirstOrDefault() ?? throw new InvalidOperationException( + $"Flow action type {flowAction.Name} does not have a public constructor that has parameters compatible with {method.DeclaringType.Name}.{method.Name}"); - insertAfter.Add(new(after, constructor)); + insertAfter.Add(new KeyValuePair(after, constructor)); } internal void AddAction(string after, FlowAction action) { - insertAfter.Add(new(after, action)); + insertAfter.Add(new KeyValuePair(after, action)); } - static FlowAction Construct(ConstructorInfo constructor, object[] methodArguments) + private static FlowAction Construct(ConstructorInfo constructor, object[] methodArguments) { // Figure out which type of object goes where in the arguments list. - Dictionary parameterIndices = new(); - - foreach (var parameter in constructor.GetParameters()) - { - parameterIndices.Add(parameter.ParameterType, parameter.Position); - } + var parameterIndices = constructor.GetParameters().ToDictionary( + parameter => parameter.ParameterType, + parameter => parameter.Position + ); // Create and populate the arguments list var arguments = new object[constructor.GetParameters().Length]; @@ -181,7 +193,7 @@ static FlowAction Construct(ConstructorInfo constructor, object[] methodArgument return (FlowAction)constructor.Invoke(arguments); } - void AddActionsAfter(string name, List actions, bool[] added, object[] methodArguments) + private void AddActionsAfter(string name, List actions, bool[] added, object[] methodArguments) { for (int i = 0; i < added.Length; i++) { @@ -189,28 +201,22 @@ void AddActionsAfter(string name, List actions, bool[] added, object if (added[i]) continue; - if (insertAfter[i].Key == name) - { - added[i] = true; + if (insertAfter[i].Key != name) continue; - FlowAction action = null; + added[i] = true; - if (insertAfter[i].Value is ConstructorInfo constructor) - { - action = Construct(constructor, methodArguments); - } - - else if (insertAfter[i].Value is FlowAction flowAction) - { - action = flowAction; - } + var action = insertAfter[i].Value switch + { + ConstructorInfo constructor => Construct(constructor, methodArguments), + FlowAction flowAction => flowAction, + _ => null + }; - actions.Add(action); + actions.Add(action); - // There could be actions set to run after the newly inserted action, - // so this needs to be called again. - AddActionsAfter(action.Name, actions, added, methodArguments); - } + // There could be actions set to run after the newly inserted action, + // so this needs to be called again. + AddActionsAfter(action.Name, actions, added, methodArguments); } } @@ -255,22 +261,22 @@ internal void ApplyTo(SequentialFlow flow, object[] methodArguments) // Warn about any actions that were not used. for (int i = 0; i < added.Length; i++) { - if (!added[i]) - { - if (insertAfter[i].Value is ConstructorInfo constructor) - { - var actionType = constructor.DeclaringType; - var logger = Logger.CreateLogSource($"{actionType.Assembly.GetName().Name}/{actionType.Name}"); - logger.LogWarning($"Flow action {actionType.Name} was set to be inserted after \"{insertAfter[i].Key}\" in {method.Name}, however that action does not exist in that flow"); - } + if (added[i]) continue; - else if (insertAfter[i].Value is FlowAction action) - { - var logger = Logger.CreateLogSource("SequentialFlow"); - logger.LogWarning($"Flow action \"{action.Name}\" was set to be inserted after \"{insertAfter[i].Key}\" in {method.Name}, however that action does not exist in that flow"); - } + if (insertAfter[i].Value is ConstructorInfo constructor) + { + var actionType = constructor.DeclaringType; + var logger = Logger.CreateLogSource($"{actionType.Assembly.GetName().Name}/{actionType.Name}"); + logger.LogWarning( + $"Flow action {actionType.Name} was set to be inserted after \"{insertAfter[i].Key}\" in {method.Name}, however that action does not exist in that flow"); + } + else if (insertAfter[i].Value is FlowAction action) + { + var logger = Logger.CreateLogSource("SequentialFlow"); + logger.LogWarning( + $"Flow action \"{action.Name}\" was set to be inserted after \"{insertAfter[i].Key}\" in {method.Name}, however that action does not exist in that flow"); } } } } -} +} \ No newline at end of file diff --git a/SpaceWarp/Patching/SettingsManagerPatcher.cs b/SpaceWarp.Core/Patching/SettingsManagerPatcher.cs similarity index 100% rename from SpaceWarp/Patching/SettingsManagerPatcher.cs rename to SpaceWarp.Core/Patching/SettingsManagerPatcher.cs diff --git a/SpaceWarp.Core/SpaceWarp.Core.csproj b/SpaceWarp.Core/SpaceWarp.Core.csproj new file mode 100644 index 00000000..591e9ff3 --- /dev/null +++ b/SpaceWarp.Core/SpaceWarp.Core.csproj @@ -0,0 +1,43 @@ + + + + + $(SpaceWarpVersion) + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + all + true + SpaceWarpPatcher.dll + + + + + + + + + + + + \ No newline at end of file diff --git a/SpaceWarp.Core/SpaceWarpPlugin.cs b/SpaceWarp.Core/SpaceWarpPlugin.cs new file mode 100644 index 00000000..92abfd40 --- /dev/null +++ b/SpaceWarp.Core/SpaceWarpPlugin.cs @@ -0,0 +1,115 @@ +global using UnityObject = UnityEngine.Object; +global using System.Linq; +using System; +using System.IO; +using System.Reflection; +using BepInEx; +using BepInEx.Logging; +using HarmonyLib; +using I2.Loc; +using JetBrains.Annotations; +using KSP.IO; +using KSP.ScriptInterop.impl.moonsharp; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop; +using SpaceWarp.API.Loading; +using SpaceWarp.API.Lua; +using SpaceWarp.API.Mods; +using SpaceWarp.InternalUtilities; +using UitkForKsp2; +using SpaceWarp.Modules; +using SpaceWarp.Patching.LoadingActions; + +namespace SpaceWarp; + +[BepInDependency("com.bepis.bepinex.configurationmanager", "17.1")] +[BepInDependency(UitkForKsp2Plugin.ModGuid, UitkForKsp2Plugin.ModVer)] +[BepInIncompatibility("com.shadow.quantum")] +[BepInPlugin(ModGuid, ModName, ModVer)] +public sealed class SpaceWarpPlugin : BaseSpaceWarpPlugin +{ + public static SpaceWarpPlugin Instance; + + [PublicAPI] public const string ModGuid = "com.github.x606.spacewarp"; + [PublicAPI] public const string ModName = "Space Warp"; + [PublicAPI] public const string ModVer = MyPluginInfo.PLUGIN_VERSION; // TODO: Don't hard code this, but I don't know much msbuild stuff so @munix wil have to do that, + // and @munix is really lazy to do it right now but he definitely will at some point :P + + internal ScriptEnvironment GlobalLuaState; + + internal new static ManualLogSource Logger; + + public SpaceWarpPlugin() + { + Assembly.LoadFile( + $"{new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName}\\SpaceWarp.dll"); + Logger = base.Logger; + Instance = this; + } + + private static void OnLanguageSourceAssetLoaded(LanguageSourceAsset asset) + { + if (!asset || LocalizationManager.Sources.Contains(asset.mSource)) + { + return; + } + + asset.mSource.owner = asset; + LocalizationManager.AddSource(asset.mSource); + } + public void Awake() + { + IOProvider.Init(); + + Harmony.CreateAndPatchAll(typeof(SpaceWarpPlugin).Assembly, ModGuid); + ModuleManager.LoadAllModules(); + + Loading.AddAssetLoadingAction("bundles", "loading asset bundles", FunctionalLoadingActions.AssetBundleLoadingAction, "bundle"); + Loading.AddAssetLoadingAction("images", "loading images", FunctionalLoadingActions.ImageLoadingAction); + Loading.AddAddressablesLoadingAction("localization","language_source",OnLanguageSourceAssetLoaded); + } + + private void SetupLuaState() + { + // I have been warned and I do not care + UserData.RegistrationPolicy = InteropRegistrationPolicy.Automatic; + + GlobalLuaState = (ScriptEnvironment)Game.ScriptEnvironment; + GlobalLuaState.script.Globals["SWLog"] = Logger; + + // Now we loop over every assembly and import static lua classes for methods + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + UserData.RegisterAssembly(assembly); + foreach (var type in assembly.GetTypes()) + { + // Now we can easily create API Objects from a [SpaceWarpLuaAPI] attribute + foreach (var attr in type.GetCustomAttributes()) + { + if (attr is not SpaceWarpLuaAPIAttribute luaAPIAttribute) continue; + // As this seems to be used here + GlobalLuaState.script.Globals[luaAPIAttribute.LuaName] = UserData.CreateStatic(type); + break; + } + } + } + } + + public override void OnPreInitialized() + { + // Persist all game objects so I don't need to stomp on config + BepInEx.Bootstrap.Chainloader.ManagerObject.Persist(); + ModuleManager.PreInitializeAllModules(); + } + + public override void OnInitialized() + { + ModuleManager.InitializeAllModules(); + SetupLuaState(); + } + + public override void OnPostInitialized() + { + ModuleManager.PostInitializeAllModules(); + } +} diff --git a/SpaceWarp/API/Game/Extensions/PartProviderExtensions.cs b/SpaceWarp.Game/API/Game/Extensions/PartProviderExtensions.cs similarity index 86% rename from SpaceWarp/API/Game/Extensions/PartProviderExtensions.cs rename to SpaceWarp.Game/API/Game/Extensions/PartProviderExtensions.cs index 619ddaf6..d77af419 100644 --- a/SpaceWarp/API/Game/Extensions/PartProviderExtensions.cs +++ b/SpaceWarp.Game/API/Game/Extensions/PartProviderExtensions.cs @@ -1,9 +1,12 @@ using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; using KSP.Game; using KSP.Sim.Definitions; namespace SpaceWarp.API.Game.Extensions; +[PublicAPI] public static class PartProviderExtensions { public static IEnumerable WithModule(this PartProvider provider) where T : ModuleData diff --git a/SpaceWarp/API/Game/Extensions/VesselVehicleExtensions.cs b/SpaceWarp.Game/API/Game/Extensions/VesselVehicleExtensions.cs similarity index 98% rename from SpaceWarp/API/Game/Extensions/VesselVehicleExtensions.cs rename to SpaceWarp.Game/API/Game/Extensions/VesselVehicleExtensions.cs index 12584da6..e8b4df65 100644 --- a/SpaceWarp/API/Game/Extensions/VesselVehicleExtensions.cs +++ b/SpaceWarp.Game/API/Game/Extensions/VesselVehicleExtensions.cs @@ -1,9 +1,11 @@ -using KSP.Sim.impl; +using JetBrains.Annotations; +using KSP.Sim.impl; using KSP.Sim.State; using UnityEngine; namespace SpaceWarp.API.Game.Extensions; +[PublicAPI] public static class VesselVehicleExtensions { public static void SetMainThrottle(this VesselVehicle vehicle, float throttle) diff --git a/SpaceWarp/API/Game/Messages/StateChanges.cs b/SpaceWarp.Game/API/Game/Messages/StateChanges.cs similarity index 97% rename from SpaceWarp/API/Game/Messages/StateChanges.cs rename to SpaceWarp.Game/API/Game/Messages/StateChanges.cs index ab29eb0e..c0f5768f 100644 --- a/SpaceWarp/API/Game/Messages/StateChanges.cs +++ b/SpaceWarp.Game/API/Game/Messages/StateChanges.cs @@ -1,19 +1,21 @@ using System; +using JetBrains.Annotations; using KSP.Game; using KSP.Messages; namespace SpaceWarp.API.Game.Messages; /// -/// A class that contains a list of events that are published either when a state is entered or left +/// A class that contains a list of events that are published either when a state is entered or left /// +[PublicAPI] public static class StateChanges { #region State changing /// - /// Invoked when the game state is changed - /// Action(Message,PreviousState,CurrentState) + /// Invoked when the game state is changed + /// Action(Message,PreviousState,CurrentState) /// public static event Action GameStateChanged; @@ -141,7 +143,6 @@ internal static void OnGameStateEntered(MessageCenterMessage message) } } - internal static void OnGameStateLeft(MessageCenterMessage message) { var msg = message as GameStateLeftMessage; @@ -217,5 +218,6 @@ internal static void OnGameStateChanged(MessageCenterMessage message) var msg = message as GameStateChangedMessage; GameStateChanged?.Invoke(msg!, msg!.PreviousState, msg!.CurrentState); } + #endregion } \ No newline at end of file diff --git a/SpaceWarp/API/Game/Messages/StateLoadings.cs b/SpaceWarp.Game/API/Game/Messages/StateLoadings.cs similarity index 94% rename from SpaceWarp/API/Game/Messages/StateLoadings.cs rename to SpaceWarp.Game/API/Game/Messages/StateLoadings.cs index 26e61904..94df110c 100644 --- a/SpaceWarp/API/Game/Messages/StateLoadings.cs +++ b/SpaceWarp.Game/API/Game/Messages/StateLoadings.cs @@ -1,8 +1,10 @@ using System; +using JetBrains.Annotations; using KSP.Messages; namespace SpaceWarp.API.Game.Messages; +[PublicAPI] public class StateLoadings { public static event Action TrainingCenterLoaded; @@ -11,7 +13,7 @@ public class StateLoadings public static void TrainingCenterLoadedHandler(MessageCenterMessage message) { TrainingCenterLoaded?.Invoke(message as TrainingCenterLoadedMessage); - } + } public static void TrackingStationLoadedHandler(MessageCenterMessage message) { TrackingStationLoaded?.Invoke(message as TrackingStationLoadedMessage); @@ -21,5 +23,5 @@ public static void TrackingStationUnloadedHandler(MessageCenterMessage message) { TrackingStationUnloaded?.Invoke(message as TrackingStationUnloadedMessage); } - + } \ No newline at end of file diff --git a/SpaceWarp/API/Game/Vehicle.cs b/SpaceWarp.Game/API/Game/Vehicle.cs similarity index 86% rename from SpaceWarp/API/Game/Vehicle.cs rename to SpaceWarp.Game/API/Game/Vehicle.cs index 78966006..68902970 100644 --- a/SpaceWarp/API/Game/Vehicle.cs +++ b/SpaceWarp.Game/API/Game/Vehicle.cs @@ -1,10 +1,12 @@ -using KSP.Game; +using JetBrains.Annotations; +using KSP.Game; using KSP.Sim.impl; using SpaceWarp.API.Lua; namespace SpaceWarp.API.Game; [SpaceWarpLuaAPI("Vehicle")] +[PublicAPI] public static class Vehicle { public static VesselVehicle ActiveVesselVehicle => GameManager.Instance.Game.ViewController._activeVesselVehicle; diff --git a/SpaceWarp.Game/Modules/Game.cs b/SpaceWarp.Game/Modules/Game.cs new file mode 100644 index 00000000..461156ff --- /dev/null +++ b/SpaceWarp.Game/Modules/Game.cs @@ -0,0 +1,35 @@ +using JetBrains.Annotations; +using KSP.Game; +using KSP.Messages; +using SpaceWarp.API.Game.Messages; + +namespace SpaceWarp.Modules; + +[UsedImplicitly] +public class Game : SpaceWarpModule +{ + public override string Name => "SpaceWarp.Game"; + + public override void LoadModule() + { + } + + public override void PreInitializeModule() + { + } + + public override void InitializeModule() + { + var game = GameManager.Instance.Game; + game.Messages.Subscribe(typeof(GameStateEnteredMessage), StateChanges.OnGameStateEntered, false, true); + game.Messages.Subscribe(typeof(GameStateLeftMessage), StateChanges.OnGameStateLeft, false, true); + game.Messages.Subscribe(typeof(GameStateChangedMessage), StateChanges.OnGameStateChanged, false, true); + game.Messages.Subscribe(typeof(TrackingStationLoadedMessage), StateLoadings.TrackingStationLoadedHandler, false, true); + game.Messages.Subscribe(typeof(TrackingStationUnloadedMessage), StateLoadings.TrackingStationUnloadedHandler, false, true); + game.Messages.Subscribe(typeof(TrainingCenterLoadedMessage), StateLoadings.TrainingCenterLoadedHandler, false, true); + } + + public override void PostInitializeModule() + { + } +} \ No newline at end of file diff --git a/SpaceWarp.Game/SpaceWarp.Game.csproj b/SpaceWarp.Game/SpaceWarp.Game.csproj new file mode 100644 index 00000000..9f663217 --- /dev/null +++ b/SpaceWarp.Game/SpaceWarp.Game.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/SpaceWarp.Messaging/API/Messaging/MessageBus.cs b/SpaceWarp.Messaging/API/Messaging/MessageBus.cs new file mode 100644 index 00000000..dc220ba3 --- /dev/null +++ b/SpaceWarp.Messaging/API/Messaging/MessageBus.cs @@ -0,0 +1,663 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEngine; + +namespace SpaceWarp.API.Messaging; + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish() + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (int i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7, T9 arg8) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7, T9 arg8, T10 arg9) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7, T9 arg8, T10 arg9, + T11 arg10) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7, T9 arg8, T10 arg9, + T11 arg10, T12 arg11) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7, T9 arg8, T10 arg9, + T11 arg10, T12 arg11, T13 arg12) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7, T9 arg8, T10 arg9, + T11 arg10, T12 arg11, T13 arg12, T14 arg13) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + arg13); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = new(); + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7, T9 arg8, T10 arg9, + T11 arg10, T12 arg11, T13 arg12, T14 arg13, T15 arg14) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + arg13, arg14); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} + +[PublicAPI] +public class MessageBus : MessageBusBase +{ + private readonly List> _handlers = + new(); + + internal override IReadOnlyList Handlers => _handlers; + internal override void RemoveHandlerAt(int index) => _handlers.RemoveAt(index); + + public void Subscribe(Action handler) + { + if (handler == null) + throw new ArgumentNullException(); + + _handlers.Add(handler); + } + + public void Unsubscribe(Action handler) + { + for (var i = _handlers.Count; i-- > 0;) + if (_handlers[i] == handler) + _handlers.RemoveAt(i); + } + + public void Publish(T1 arg0, T2 arg1, T3 arg2, T4 arg3, T5 arg4, T6 arg5, T7 arg6, T8 arg7, T9 arg8, T10 arg9, + T11 arg10, T12 arg11, T13 arg12, T14 arg13, T15 arg14, T16 arg15) + { + for (var i = _handlers.Count; i-- > 0;) + { + try + { + _handlers[i].Invoke(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, + arg13, arg14, arg15); + } + catch (Exception e) + { + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Error handling message '{Name}' : {e}"); + } + } + } +} \ No newline at end of file diff --git a/SpaceWarp.Messaging/API/Messaging/MessageBusBase.cs b/SpaceWarp.Messaging/API/Messaging/MessageBusBase.cs new file mode 100644 index 00000000..f8ae2782 --- /dev/null +++ b/SpaceWarp.Messaging/API/Messaging/MessageBusBase.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace SpaceWarp.API.Messaging; + +[PublicAPI] +public abstract class MessageBusBase +{ + public string Name { get; internal set; } + internal abstract IReadOnlyList Handlers { get; } + internal abstract void RemoveHandlerAt(int index); +} \ No newline at end of file diff --git a/SpaceWarp.Messaging/API/Messaging/MessageBusManager.cs b/SpaceWarp.Messaging/API/Messaging/MessageBusManager.cs new file mode 100644 index 00000000..8b256e92 --- /dev/null +++ b/SpaceWarp.Messaging/API/Messaging/MessageBusManager.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEngine; + +namespace SpaceWarp.API.Messaging; + +[PublicAPI] +public static class MessageBusManager +{ + private static Dictionary _messagesBusesByName = new Dictionary(); + + public static T Add(string name) where T : MessageBusBase, new() + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Null or empty MessageBus name"); + + if (_messagesBusesByName.ContainsKey(name)) + throw new Exception($"MessageBus '{name}' exists already"); + + var messageBus = new T + { + Name = name + }; + _messagesBusesByName.Add(name, messageBus); + + Modules.Messaging.Instance.ModuleLogger.LogDebug($"MessageBus '{name}' created"); + return messageBus; + } + + public static bool Exists(string messageBusName) => _messagesBusesByName.ContainsKey(messageBusName); + + public static bool TryGet(string messageBusName, out T messageBus) where T : MessageBusBase, new() + { + if (string.IsNullOrEmpty(messageBusName)) + throw new ArgumentException("Null or empty MessageBus name"); + + if (!_messagesBusesByName.TryGetValue(messageBusName, out MessageBusBase messageBusBase)) + { + messageBus = null; + return false; + } + + if (messageBusBase is not T @base) + throw new Exception( + $"Message bus '{messageBusBase.Name}' is of type '{messageBusBase.GetType()}' but the requested type was '{typeof(T)}'"); + messageBus = @base; + return true; + + } + + // Call this (potentially a bit heavy) method on some occasions, for example when a loading screen happens + internal static void CheckForMemoryLeaks() + { + var memoryLeaks = 0; + + foreach (var messageBus in _messagesBusesByName.Values) + { + var handlers = messageBus.Handlers; + + for (var i = handlers.Count; i-- > 0;) + { + var target = handlers[i].Target; + switch (target) + { + // bypass UnityEngine.Object null equality overload + case null: + continue; + case UnityEngine.Object uObj when uObj == null: + Modules.Messaging.Instance.ModuleLogger.LogDebug($"Memory leak detected : a destroyed instance of the '{target.GetType().Assembly.GetName().Name}:{target.GetType().Name}' class is holding a '{messageBus.Name}' MessageBus handler"); + messageBus.RemoveHandlerAt(i); + memoryLeaks++; + break; + } + } + } + + if (memoryLeaks > 0) + Modules.Messaging.Instance.ModuleLogger.LogDebug($"{memoryLeaks} detected!"); + } +} \ No newline at end of file diff --git a/SpaceWarp.Messaging/Modules/Messaging.cs b/SpaceWarp.Messaging/Modules/Messaging.cs new file mode 100644 index 00000000..861ca156 --- /dev/null +++ b/SpaceWarp.Messaging/Modules/Messaging.cs @@ -0,0 +1,26 @@ +using JetBrains.Annotations; + +namespace SpaceWarp.Modules; + +[PublicAPI] +public class Messaging : SpaceWarpModule +{ + public override string Name => "SpaceWarp.Messaging"; + internal static Messaging Instance; + public override void LoadModule() + { + Instance = this; + } + + public override void PreInitializeModule() + { + } + + public override void InitializeModule() + { + } + + public override void PostInitializeModule() + { + } +} \ No newline at end of file diff --git a/SpaceWarp.Messaging/SpaceWarp.Messaging.csproj b/SpaceWarp.Messaging/SpaceWarp.Messaging.csproj new file mode 100644 index 00000000..9eb0d595 --- /dev/null +++ b/SpaceWarp.Messaging/SpaceWarp.Messaging.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/SpaceWarp/Backend/Sound/Soundbank.cs b/SpaceWarp.Sound/API/Sound/Soundbank.cs similarity index 59% rename from SpaceWarp/Backend/Sound/Soundbank.cs rename to SpaceWarp.Sound/API/Sound/Soundbank.cs index 7fee0ccf..50a40657 100644 --- a/SpaceWarp/Backend/Sound/Soundbank.cs +++ b/SpaceWarp.Sound/API/Sound/Soundbank.cs @@ -1,13 +1,13 @@ -using AK.Wwise; -using System; -using System.Collections.Generic; +using System; using System.Runtime.InteropServices; -using UnityEngine; +using JetBrains.Annotations; -namespace SpaceWarp.Backend.Sound; -internal class Soundbank +namespace SpaceWarp.API.Sound; + +[PublicAPI] +public class Soundbank { - internal Soundbank(byte[] bankData) + public Soundbank(byte[] bankData) { BankData = bankData; Size = (uint)bankData.Length; @@ -16,40 +16,44 @@ internal Soundbank(byte[] bankData) /// /// Pointer to bank data /// - internal IntPtr? BankDataPtr; + public IntPtr? BankDataPtr; /// /// BankData supplied by the user /// - internal byte[] BankData; + public byte[] BankData; /// /// Handle for BankData array /// - internal GCHandle? Memory; + public GCHandle? Memory; /// /// Size of the bank in bytes /// - internal uint Size; + public uint Size; /// /// Spacewarp's asset ID. Also used to get the Soundbank. /// - internal string InternalPath; + public string InternalPath; /// /// Identifier for the engine /// - internal uint WwiseID; + public uint WwiseID; - internal AKRESULT Load() + public AKRESULT Load() { // Pins BankData array in memory BankDataPtr ??= (Memory = GCHandle.Alloc(BankData, GCHandleType.Pinned)).Value.AddrOfPinnedObject(); // Loads the entire array as a bank - var result = AkSoundEngine.LoadBankMemoryView(BankDataPtr!.Value, (uint)BankData.Length, out WwiseID); + var result = AkSoundEngine.LoadBankMemoryView( + BankDataPtr!.Value, + (uint)BankData.Length, + out WwiseID + ); if (result == AKRESULT.AK_Success) { @@ -59,9 +63,4 @@ internal AKRESULT Load() return result; } - - /// - /// Lookup table that saves all loaded soundbanks. Key is the asset internal path. - /// - internal static Dictionary soundbanks; } \ No newline at end of file diff --git a/SpaceWarp.Sound/API/Sound/SoundbankManager.cs b/SpaceWarp.Sound/API/Sound/SoundbankManager.cs new file mode 100644 index 00000000..7e1466f0 --- /dev/null +++ b/SpaceWarp.Sound/API/Sound/SoundbankManager.cs @@ -0,0 +1,41 @@ +using JetBrains.Annotations; +using System.Collections.Generic; + +namespace SpaceWarp.API.Sound; + +[PublicAPI] +public static class SoundbankManager +{ + public static bool LoadSoundbank(string internalPath, byte[] bankData, out Soundbank soundbank) + { + var bank = new Soundbank(bankData); + var result = bank.Load(); + + if (result != AKRESULT.AK_Success) + { + Modules.Sound.Instance.ModuleLogger.LogError($"Soundbank loading failed with result {result}"); + soundbank = null; + return false; + } + + bank.InternalPath = internalPath; + LoadedSoundbanks.Add(bank.InternalPath, bank); + soundbank = bank; + return true; + } + + public static Soundbank GetSoundbank(string internalPath) + { + return LoadedSoundbanks[internalPath]; + } + + public static bool TryGetSoundbank(string internalPath, out Soundbank soundbank) + { + return LoadedSoundbanks.TryGetValue(internalPath, out soundbank); + } + + /// + /// Lookup table that saves all loaded soundbanks. Key is the asset internal path. + /// + private static readonly Dictionary LoadedSoundbanks = new(); +} diff --git a/SpaceWarp.Sound/Modules/Sound.cs b/SpaceWarp.Sound/Modules/Sound.cs new file mode 100644 index 00000000..1497d6a2 --- /dev/null +++ b/SpaceWarp.Sound/Modules/Sound.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.IO; +using JetBrains.Annotations; +using SpaceWarp.API.Loading; +using SpaceWarp.API.Sound; +using UnityObject = UnityEngine.Object; + +namespace SpaceWarp.Modules; + +[UsedImplicitly] +public class Sound : SpaceWarpModule +{ + public override string Name => "SpaceWarp.Sound"; + internal static Sound Instance; + + private const string SoundbanksFolder = "soundbanks"; + + public override void LoadModule() + { + Instance = this; + Loading.AddAssetLoadingAction(SoundbanksFolder, "loading soundbanks", AssetSoundbankLoadingAction, "bnk"); + } + + public override void PreInitializeModule() + { + } + + public override void InitializeModule() + { + } + + public override void PostInitializeModule() + { + } + + private static List<(string name, UnityObject asset)> AssetSoundbankLoadingAction( + string internalPath, + string filename + ) + { + var fileData = File.ReadAllBytes(filename); + var fullPath = Path.Combine(SoundbanksFolder, internalPath); + + // Banks are identified by their internal path + if (SoundbankManager.LoadSoundbank(fullPath, fileData, out _)) + { + // Since there's no UnityObject related to soundbanks, we pass null, saving only the internalPath + return new List<(string name, UnityObject asset)> { (fullPath, null) }; + } + + throw new Exception($"Failed to load soundbank {internalPath}"); + } +} \ No newline at end of file diff --git a/SpaceWarp.Sound/SpaceWarp.Sound.csproj b/SpaceWarp.Sound/SpaceWarp.Sound.csproj new file mode 100644 index 00000000..10eff429 --- /dev/null +++ b/SpaceWarp.Sound/SpaceWarp.Sound.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/SpaceWarp/API/Lua/AppBarInterop.cs b/SpaceWarp.UI/API/Lua/AppBarInterop.cs similarity index 62% rename from SpaceWarp/API/Lua/AppBarInterop.cs rename to SpaceWarp.UI/API/Lua/AppBarInterop.cs index dd1344c2..6fe42aa6 100644 --- a/SpaceWarp/API/Lua/AppBarInterop.cs +++ b/SpaceWarp.UI/API/Lua/AppBarInterop.cs @@ -6,20 +6,17 @@ using UnityEngine; using UnityEngine.UIElements; -// ReSharper disable MemberCanBePrivate.Global - -// ReSharper disable UnusedMember.Global namespace SpaceWarp.API.Lua; [SpaceWarpLuaAPI("AppBar")] -// ReSharper disable once UnusedType.Global +[PublicAPI] public static class AppBarInterop { public static Sprite GetSprite(string texturePath, int width = 0, int height = 0) { return GetSprite(AssetManager.GetAsset(texturePath)); } - + public static Sprite GetSprite(Texture2D texture, int width = 0, int height = 0) { if (width == 0) @@ -34,9 +31,16 @@ public static Sprite GetSprite(Texture2D texture, int width = 0, int height = 0) return Sprite.Create(texture, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f)); } - - public static void RegisterAppButton(bool oab, bool inFlight, string name, string ID, Sprite icon, - Closure toggleCallback, [CanBeNull] DynValue self = null) + + public static void RegisterAppButton( + bool oab, + bool inFlight, + string name, + string ID, + Sprite icon, + Closure toggleCallback, + [CanBeNull] DynValue self = null + ) { Action callback; if (self != null) @@ -59,13 +63,27 @@ public static void RegisterAppButton(bool oab, bool inFlight, string name, strin } } - public static void RegisterAppButton(bool oab, bool inFlight, string name, string ID, string texturePath, - Closure toggleCallback, [CanBeNull] DynValue self = null) => - RegisterAppButton(oab, inFlight, name, ID, GetSprite(texturePath), toggleCallback, self); - - public static void RegisterAppWindow(bool oab, bool inFlight, string name, string ID, Sprite icon, VisualElement window, Closure toggleCallback, [CanBeNull] DynValue self = null) + public static void RegisterAppButton( + bool oab, + bool inFlight, + string name, + string ID, + string texturePath, + Closure toggleCallback, + [CanBeNull] DynValue self = null + ) => RegisterAppButton(oab, inFlight, name, ID, GetSprite(texturePath), toggleCallback, self); + + public static void RegisterAppWindow( + bool oab, + bool inFlight, + string name, + string ID, + Sprite icon, + VisualElement window, + Closure toggleCallback, + [CanBeNull] DynValue self = null + ) { - Action callback; if (self != null) { @@ -83,7 +101,7 @@ public static void RegisterAppWindow(bool oab, bool inFlight, string name, strin toggleCallback.Call(b); }; } - + if (oab) { Appbar.RegisterAppButton(name, ID, icon, callback); @@ -95,9 +113,16 @@ public static void RegisterAppWindow(bool oab, bool inFlight, string name, strin } } - public static void RegisterAppWindow(bool oab, bool inFlight, string name, string ID, string texturePath, VisualElement window, - Closure toggleCallback, [CanBeNull] DynValue self = null) => - RegisterAppWindow(oab, inFlight, name, ID, GetSprite(texturePath), window, toggleCallback, self); + public static void RegisterAppWindow( + bool oab, + bool inFlight, + string name, + string ID, + string texturePath, + VisualElement window, + Closure toggleCallback, + [CanBeNull] DynValue self = null + ) => RegisterAppWindow(oab, inFlight, name, ID, GetSprite(texturePath), window, toggleCallback, self); public static void SetAppButtonIndicator(string id, bool b) { diff --git a/SpaceWarp/API/UI/Appbar/Appbar.cs b/SpaceWarp.UI/API/UI/Appbar/Appbar.cs similarity index 74% rename from SpaceWarp/API/UI/Appbar/Appbar.cs rename to SpaceWarp.UI/API/UI/Appbar/Appbar.cs index 8a1e5819..af009f94 100644 --- a/SpaceWarp/API/UI/Appbar/Appbar.cs +++ b/SpaceWarp.UI/API/UI/Appbar/Appbar.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using BepInEx.Bootstrap; +using JetBrains.Annotations; using KSP.UI.Binding; using SpaceWarp.Backend.UI.Appbar; using SpaceWarp.InternalUtilities; @@ -8,15 +9,17 @@ namespace SpaceWarp.API.UI.Appbar; +[PublicAPI] public static class Appbar { private static readonly List<(string text, Sprite icon, string ID, Action action)> ButtonsToBeLoaded = new(); - private static readonly List<(string text, Sprite icon, string ID, Action action)> OABButtonsToBeLoaded = - new(); + private static readonly List<(string text, Sprite icon, string ID, Action action)> OABButtonsToBeLoaded = new(); + + private static readonly List<(string text, Sprite icon, string ID, Action action)> KSCButtonsToBeLoaded = new(); /// - /// Register a appbar menu for the game + /// Register an appbar menu for the game /// /// The text in the appbar menu /// The title of the menu @@ -38,7 +41,7 @@ public static T RegisterGameAppbarMenu(string text, string title, string id, } /// - /// Register a appbar menu for the game + /// Register a appbar menu for the game /// /// The text in the appbar menu /// The title of the menu @@ -52,7 +55,7 @@ public static T RegisterGameAppbarMenu(string text, string title, string id, } /// - /// Register a button on the games AppBar + /// Register a button on the games AppBar /// /// The text in the appbar menu /// A unique id for the appbar menu eg: "BTN-Example" @@ -64,7 +67,7 @@ public static void RegisterAppButton(string text, string id, Sprite icon, Action } /// - /// Register a button on the games AppBar + /// Register a button on the games AppBar /// /// The text in the appbar menu /// A unique id for the appbar menu eg: "BTN-Example" @@ -76,7 +79,7 @@ public static void RegisterAppButton(string text, string id, Texture2D icon, Act } /// - /// Register a button on the OAB AppBar + /// Register a button on the OAB AppBar /// /// The text in the appbar menu /// A unique id for the appbar menu eg: "BTN-ExampleOAB" @@ -88,7 +91,7 @@ public static void RegisterOABAppButton(string text, string id, Sprite icon, Act } /// - /// Register a button on the OAB AppBar + /// Register a button on the OAB AppBar /// /// The text in the appbar menu /// A unique id for the appbar menu eg: "BTN-ExampleOAB" @@ -100,7 +103,31 @@ public static void RegisterOABAppButton(string text, string id, Texture2D icon, } /// - /// Convert a Texture2D to a Sprite + /// Register a button on the KSC AppBar + /// + /// The text in the appbar menu + /// A unique id for the appbar menu eg: "BTN-ExampleKSC" + /// A Sprite for the icon in the appbar + /// The function to be called when this button is clicked + public static void RegisterKSCAppButton(string text, string id, Sprite icon, Action func) + { + KSCButtonsToBeLoaded.Add((text, icon, id, func)); + } + + /// + /// Register a button on the KSC AppBar + /// + /// The text in the appbar menu + /// A unique id for the appbar menu eg: "BTN-ExampleKSC" + /// A Texture2D for the icon in the appbar + /// The function to be called when this button is clicked + public static void RegisterKSCAppButton(string text, string id, Texture2D icon, Action func) + { + RegisterKSCAppButton(text, id, GetAppBarIconFromTexture(icon), func); + } + + /// + /// Convert a Texture2D to a Sprite /// /// The Texture2D /// The width of the sprite, 0 for inferring @@ -123,7 +150,7 @@ public static Sprite GetAppBarIconFromTexture(Texture2D texture, int width = 0, /// - /// Sets an app bar buttons indicator (the green sprite to the side of it) + /// Sets an app bar buttons indicator (the green sprite to the side of it) /// /// The id of the button, what you set when registering the app bar button /// The state of the indicator, true for on, false for off @@ -147,4 +174,12 @@ internal static void LoadOABButtons() AppbarBackend.AddOABButton(button.text, button.icon, button.ID, button.action); } } + + internal static void LoadKSCButtons() + { + foreach (var button in KSCButtonsToBeLoaded) + { + AppbarBackend.AddKSCButton(button.text, button.icon, button.ID, button.action); + } + } } \ No newline at end of file diff --git a/SpaceWarp/API/UI/Appbar/AppbarMenu.cs b/SpaceWarp.UI/API/UI/Appbar/AppbarMenu.cs similarity index 98% rename from SpaceWarp/API/UI/Appbar/AppbarMenu.cs rename to SpaceWarp.UI/API/UI/Appbar/AppbarMenu.cs index d271e617..2e130c1a 100644 --- a/SpaceWarp/API/UI/Appbar/AppbarMenu.cs +++ b/SpaceWarp.UI/API/UI/Appbar/AppbarMenu.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using KSP.Game; using KSP.Sim.impl; using UnityEngine; @@ -7,6 +8,7 @@ namespace SpaceWarp.API.UI.Appbar; [Obsolete("Spacewarps support for IMGUI will not be getting updates, please use UITK instead")] +[PublicAPI] public abstract class AppbarMenu : KerbalBehavior { [FormerlySerializedAs("Title")] public string title; diff --git a/SpaceWarp/API/UI/MainMenu.cs b/SpaceWarp.UI/API/UI/MainMenu.cs similarity index 86% rename from SpaceWarp/API/UI/MainMenu.cs rename to SpaceWarp.UI/API/UI/MainMenu.cs index eb9c4520..5667fd4b 100644 --- a/SpaceWarp/API/UI/MainMenu.cs +++ b/SpaceWarp.UI/API/UI/MainMenu.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; namespace SpaceWarp.API.UI; +[PublicAPI] public static class MainMenu { internal static readonly List<(string name, Action onClicked)> MenuButtonsToBeAdded = new(); internal static readonly List<(string term, Action onClicked)> LocalizedMenuButtonsToBeAdded = new(); /// - /// Registers a button to be added to the main menu. + /// Registers a button to be added to the main menu. /// /// The name of the button on the menu. /// The action that is invoked when the button is pressed @@ -17,9 +19,9 @@ public static void RegisterMenuButton(string name, Action onClicked) { MenuButtonsToBeAdded.Add((name, onClicked)); } - + /// - /// Registers a localized button to be added to the main menu. + /// Registers a localized button to be added to the main menu. /// /// The term of the translation for button on the menu. /// The action that is invoked when the button is pressed @@ -27,5 +29,5 @@ public static void RegisterLocalizedMenuButton(string term, Action onClicked) { LocalizedMenuButtonsToBeAdded.Add((term, onClicked)); } - + } \ No newline at end of file diff --git a/SpaceWarp.UI/API/UI/ModList.cs b/SpaceWarp.UI/API/UI/ModList.cs new file mode 100644 index 00000000..884652b1 --- /dev/null +++ b/SpaceWarp.UI/API/UI/ModList.cs @@ -0,0 +1,25 @@ +using System; +using JetBrains.Annotations; +using SpaceWarp.UI.ModList; +using UnityEngine.UIElements; + +namespace SpaceWarp.API.UI; + +[PublicAPI] +public static class ModList +{ + /// + /// Register a function to generate a foldout in the mods list for your mods details + /// Use only in PostInitialize() + /// + /// The guid of your mod + /// The action that generates your foldout + public static void RegisterDetailsFoldoutGenerator(string modGuid, Func generateFoldoutOnOpenAction) + { + var modListController = Modules.UI.Instance.ModListController; + if (modListController.BoundItems.TryGetValue(modGuid, out var controller)) + { + controller.GetDetails = generateFoldoutOnOpenAction; + } + } +} \ No newline at end of file diff --git a/SpaceWarp/API/UI/Settings/ModsPropertyDrawers.cs b/SpaceWarp.UI/API/UI/Settings/ModsPropertyDrawers.cs similarity index 58% rename from SpaceWarp/API/UI/Settings/ModsPropertyDrawers.cs rename to SpaceWarp.UI/API/UI/Settings/ModsPropertyDrawers.cs index a7f69cda..74414e4c 100644 --- a/SpaceWarp/API/UI/Settings/ModsPropertyDrawers.cs +++ b/SpaceWarp.UI/API/UI/Settings/ModsPropertyDrawers.cs @@ -1,28 +1,32 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Reflection; using BepInEx.Configuration; using I2.Loc; -using KSP; -using KSP.Api.CoreTypes; +using JetBrains.Annotations; using KSP.UI; -using KSP.UI.Binding; +using SpaceWarp.API.Configuration; using SpaceWarp.Backend.UI.Settings; -using SpaceWarp.InternalUtilities; using TMPro; using UnityEngine; using UnityEngine.UI; +using UnityObject = UnityEngine.Object; namespace SpaceWarp.API.UI.Settings; +[PublicAPI] public static class ModsPropertyDrawers { - private static Dictionary> AllPropertyDrawers = new(); + private static readonly Dictionary> AllPropertyDrawers = new(); + private static readonly Dictionary> AllAbstractedPropertyDrawers = new(); public static void AddDrawer(Func drawerGenerator) => AllPropertyDrawers.Add(typeof(T), drawerGenerator); + public static void AddAbstractedDrawer(Func drawerGenerator) => + AllAbstractedPropertyDrawers.Add(typeof(T), drawerGenerator); public static GameObject Drawer(ConfigEntryBase entry) { if (entry.SettingType.IsEnum && !AllPropertyDrawers.ContainsKey(entry.SettingType)) @@ -41,14 +45,29 @@ public static GameObject Drawer(ConfigEntryBase entry) return AllPropertyDrawers.TryGetValue(entry.SettingType, out var func) ? func(entry) : null; } - + public static GameObject Drawer(string name, IConfigEntry entry) + { + if (entry.ValueType.IsEnum && !AllAbstractedPropertyDrawers.ContainsKey(entry.ValueType)) + AllAbstractedPropertyDrawers.Add(entry.ValueType, GenerateAbstractEnumDrawerFor(entry.ValueType)); + if (!AllAbstractedPropertyDrawers.ContainsKey(entry.ValueType)) + { + try + { + AllAbstractedPropertyDrawers.Add(entry.ValueType,GenerateAbstractGenericDrawerFor(entry.ValueType)); + } + catch + { + //Ignored + } + } + return AllAbstractedPropertyDrawers.TryGetValue(entry.ValueType, out var func) ? func(name,entry) : null; + } internal static GameObject DropdownPrefab; internal static GameObject RadioPrefab; internal static GameObject RadioSettingPrefab; internal static GameObject InputFieldPrefab; - private static Func GenerateEnumDrawerFor(Type t) { var optionNames = t.GetEnumNames().ToList(); @@ -77,7 +96,7 @@ private static Func GenerateEnumDrawerFor(Type t) var lab = ddCopy.GetChild("Label"); lab.GetComponent().SetTerm(entry.Definition.Key); lab.GetComponent().text = entry.Definition.Key; - var dropdown = ddCopy.GetChild("Setting").GetChild("KSP2DropDown"); + var dropdown = ddCopy.GetChild("Setting").GetChild("BTN-Dropdown"); var extended = dropdown.GetComponent(); // Start by clearing the options data extended.options.Clear(); @@ -156,7 +175,112 @@ private static Func GenerateEnumDrawerFor(Type t) }; } - + private static Func GenerateAbstractEnumDrawerFor(Type t) + { + var optionNames = t.GetEnumNames().ToList(); + var optionValues = t.GetEnumValues().Cast().ToList(); + for (var i = 0; i < optionNames.Count; i++) + { + try + { + var memberInfos = t.GetMember(optionNames[i]); + var enumValueInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == t); + var valueAttributes = enumValueInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + optionNames[i] = ((DescriptionAttribute)valueAttributes[0]).Description; + } + catch + { + // ignored + } + } + + var shouldDropdown = optionNames.Count > 5 || optionNames.Select(x => x.Length).Sum() >= 50; + return (name, entry) => + { + if (shouldDropdown) + { + var ddCopy = UnityObject.Instantiate(DropdownPrefab); + var lab = ddCopy.GetChild("Label"); + lab.GetComponent().SetTerm(name); + lab.GetComponent().text = name; + 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 optionNames) + { + extended.options.Add(new TMP_Dropdown.OptionData(option)); + } + + extended.value = optionValues.IndexOf((int)entry.Value); + extended.onValueChanged.AddListener(idx => { entry.Value = optionValues[idx]; }); + var sec = ddCopy.AddComponent(); + sec.description = entry.Description; + sec.isInputSettingElement = false; + ddCopy.SetActive(true); + ddCopy.name = name; + return ddCopy; + } + else + { + var radioCopy = UnityObject.Instantiate(RadioPrefab); + var lab = radioCopy.GetChild("Label"); + lab.GetComponent().SetTerm(name); + lab.GetComponent().text = name; + var setting = radioCopy.GetChild("Setting"); + var idx = optionValues.IndexOf((int)entry.Value); + List allToggles = new(); + for (var i = 0; i < optionNames.Count; i++) + { + var settingCopy = UnityObject.Instantiate(RadioSettingPrefab, setting.transform, true); + var option = settingCopy.GetComponentInChildren(); + option.text = optionNames[i]; + var loc = settingCopy.GetComponentInChildren(); + loc.Term = optionNames[i]; + var toggle = settingCopy.GetComponent(); + toggle.Set(i == idx); + var i1 = i; + toggle.onValueChanged.AddListener(tgl => + { + // This should update automagically + var idx2 = optionValues.IndexOf((int)entry.Value); + if (i1 == idx2) + { + if (!tgl) + { + toggle.Set(true); + } + + return; + } + + if (!tgl) + { + return; + } + + entry.Value = optionValues[i1]; + for (var j = 0; j < allToggles.Count; j++) + { + if (j == i1) continue; + if (allToggles[j]) + allToggles[j].Set(false); + } + }); + allToggles.Add(toggle); + toggle.name = optionNames[i]; + settingCopy.SetActive(true); + } + + var sec = radioCopy.AddComponent(); + sec.description = entry.Description; + sec.isInputSettingElement = false; + radioCopy.SetActive(true); + radioCopy.name = name; + return radioCopy; + } + }; + } // Should satisfy most needs private static Func GenerateGenericDrawerFor(Type entrySettingType) { @@ -184,7 +308,7 @@ private static Func GenerateGenericDrawerFor(Type e return (GameObject)valueRangeMethod.Invoke(null, new object[] { entry, entry.Description.AcceptableValues }); } } - + var inputFieldCopy = UnityObject.Instantiate(InputFieldPrefab); var lab = inputFieldCopy.GetChild("Label"); lab.GetComponent().SetTerm(entry.Definition.Key); @@ -208,8 +332,42 @@ private static Func GenerateGenericDrawerFor(Type e return inputFieldCopy; }; } - - + + private static Func GenerateAbstractGenericDrawerFor(Type entrySettingType) + { + var valueListMethod = typeof(ModsPropertyDrawers).GetMethod(nameof(CreateFromAcceptableValueList), + BindingFlags.Static | BindingFlags.NonPublic) + ?.MakeGenericMethod(entrySettingType); + var valueRangeMethod = typeof(ModsPropertyDrawers).GetMethod(nameof(CreateFromAcceptableValueRange), + BindingFlags.Static | BindingFlags.NonPublic) + ?.MakeGenericMethod(entrySettingType); + return (name, entry) => + { + + var inputFieldCopy = UnityObject.Instantiate(InputFieldPrefab); + var lab = inputFieldCopy.GetChild("Label"); + lab.GetComponent().SetTerm(name); + lab.GetComponent().text = name; + var textField = inputFieldCopy.GetComponentInChildren(); + var textFieldObject = textField.gameObject; + textFieldObject.name = name + ": Text Field"; + textField.characterLimit = 256; + textField.readOnly = false; + textField.interactable = true; + textField.text = entry.Value.ToString(); + textField.onValueChanged.AddListener(str => { entry.Value = TypeDescriptor.GetConverter(entrySettingType).ConvertFromString(str); }); + var rectTransform = textFieldObject.transform.parent.gameObject.GetComponent(); + rectTransform.anchorMin = new Vector2(0, 2.7f); + rectTransform.anchorMax = new Vector2(0.19f, 2.25f); + var sec = inputFieldCopy.AddComponent(); + sec.description = entry.Description; + sec.isInputSettingElement = false; + inputFieldCopy.SetActive(true); + inputFieldCopy.name = name; + return inputFieldCopy; + }; + } + private static GameObject CreateBoolConfig(ConfigEntryBase baseEntry) { var entry = (ConfigEntry)baseEntry; @@ -269,6 +427,64 @@ private static GameObject CreateBoolConfig(ConfigEntryBase baseEntry) return radioCopy; } + private static GameObject CreateBoolConfigAbstracted(string name, IConfigEntry entry) + { + var radioCopy = UnityObject.Instantiate(RadioPrefab); + var lab = radioCopy.GetChild("Label"); + lab.GetComponent().SetTerm(name); + lab.GetComponent().text = name; + var setting = radioCopy.GetChild("Setting"); + var idx = (bool)entry.Value ? 0 : 1; + List allToggles = new(); + for (var i = 0; i < 2; i++) + { + var settingCopy = UnityObject.Instantiate(RadioSettingPrefab, setting.transform, true); + var option = settingCopy.GetComponentInChildren(); + option.text = i == 0 ? "Yes" : "No"; + var loc = settingCopy.GetComponentInChildren(); + loc.Term = option.text; + var toggle = settingCopy.GetComponent(); + toggle.Set(i == idx); + var i1 = i; + toggle.onValueChanged.AddListener(tgl => + { + // This should update automagically + var idx2 = (bool)entry.Value ? 0 : 1; + if (i1 == idx2) + { + if (!tgl) + { + toggle.Set(true); + } + + return; + } + + if (!tgl) + { + return; + } + + entry.Value = i1 == 0; + for (var j = 0; j < allToggles.Count; j++) + { + if (j == i1) continue; + if (allToggles[j]) + allToggles[j].Set(false); + } + }); + allToggles.Add(toggle); + settingCopy.SetActive(true); + } + + var sec = radioCopy.AddComponent(); + sec.description = entry.Description; + sec.isInputSettingElement = false; + radioCopy.SetActive(true); + radioCopy.name = name; + return radioCopy; + } + private static GameObject CreateFromAcceptableValueList(ConfigEntry entry, AcceptableValueList acceptableValues) where T : IEquatable { @@ -276,7 +492,7 @@ private static GameObject CreateFromAcceptableValueList(ConfigEntry entry, var lab = ddCopy.GetChild("Label"); lab.GetComponent().SetTerm(entry.Definition.Key); lab.GetComponent().text = entry.Definition.Key; - var dropdown = ddCopy.GetChild("Setting").GetChild("KSP2DropDown"); + var dropdown = ddCopy.GetChild("Setting").GetChild("BTN-Dropdown"); var extended = dropdown.GetComponent(); // Start by clearing the options data extended.options.Clear(); @@ -298,7 +514,7 @@ private static GameObject CreateFromAcceptableValueList(ConfigEntry entry, } internal static GameObject SliderPrefab; - + // We should just assume that stuff can be converted to a float for this private static GameObject CreateFromAcceptableValueRange(ConfigEntry entry, AcceptableValueRange acceptableValues) where T : IComparable @@ -335,12 +551,15 @@ private static GameObject CreateFromAcceptableValueRange(ConfigEntry entry TypeCode.Single => x => (T)(object)x, _ => x => throw new NotImplementedException(typeof(T).ToString()) }; - + slider.minValue = toFloat(acceptableValues.MinValue); + slider.maxValue = toFloat(acceptableValues.MaxValue); + slider.SetValueWithoutNotify(toFloat(entry.Value)); slider.onValueChanged.AddListener(value => { // 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 = (toFloat(acceptableValues.MaxValue) - toFloat(acceptableValues.MinValue)) * value + + // toFloat(acceptableValues.MinValue); + var trueValue = value; entry.Value = toT(trueValue) ?? entry.Value; if (entry.Value != null) text.text = entry.Value.ToString(); @@ -386,6 +605,31 @@ private static GameObject CreateStringConfig(ConfigEntryBase entryBase) return inputFieldCopy; } + private static GameObject CreateStringConfigAbstracted(string name, IConfigEntry entry) + { + var inputFieldCopy = UnityObject.Instantiate(InputFieldPrefab); + var lab = inputFieldCopy.GetChild("Label"); + lab.GetComponent().SetTerm(name); + lab.GetComponent().text = name; + var textField = inputFieldCopy.GetComponentInChildren(); + var textFieldObject = textField.gameObject; + textFieldObject.name = name + ": Text Field"; + textField.characterLimit = 256; + textField.readOnly = false; + textField.interactable = true; + textField.text = (string)entry.Value; + textField.onValueChanged.AddListener(str => { entry.Value = str; }); + var rectTransform = textFieldObject.transform.parent.gameObject.GetComponent(); + rectTransform.anchorMin = new Vector2(0, 2.7f); + rectTransform.anchorMax = new Vector2(0.19f, 2.25f); + var sec = inputFieldCopy.AddComponent(); + sec.description = entry.Description; + sec.isInputSettingElement = false; + inputFieldCopy.SetActive(true); + inputFieldCopy.name = name; + return inputFieldCopy; + } + private static GameObject CreateColorConfig(ConfigEntryBase entryBase) { var entry = (ConfigEntry)entryBase; @@ -419,6 +663,34 @@ private static GameObject CreateColorConfig(ConfigEntryBase entryBase) inputFieldCopy.SetActive(true); inputFieldCopy.name = entry.Definition.Key; return inputFieldCopy; + }private static GameObject CreateColorConfigAbstracted(string name, IConfigEntry entry) + { + var inputFieldCopy = UnityObject.Instantiate(InputFieldPrefab); + var lab = inputFieldCopy.GetChild("Label"); + lab.GetComponent().SetTerm(name); + lab.GetComponent().text = name; + var textField = inputFieldCopy.GetComponentInChildren(); + var textFieldObject = textField.gameObject; + textFieldObject.name = name + ": Color Field"; + textField.characterLimit = 256; + textField.readOnly = false; + textField.interactable = true; + textField.text = ((Color)entry.Value).a < 1 ? ColorUtility.ToHtmlStringRGBA((Color)entry.Value) : ColorUtility.ToHtmlStringRGB((Color)entry.Value); + textField.onValueChanged.AddListener(str => { + if (ColorUtility.TryParseHtmlString(str, out var color)) + { + entry.Value = color; + } + }); + var rectTransform = textFieldObject.transform.parent.gameObject.GetComponent(); + rectTransform.anchorMin = new Vector2(0, 2.7f); + rectTransform.anchorMax = new Vector2(0.19f, 2.25f); + var sec = inputFieldCopy.AddComponent(); + sec.description = entry.Description; + sec.isInputSettingElement = false; + inputFieldCopy.SetActive(true); + inputFieldCopy.name = name; + return inputFieldCopy; } internal static void SetupDefaults() @@ -426,5 +698,8 @@ internal static void SetupDefaults() AllPropertyDrawers[typeof(bool)] = CreateBoolConfig; AllPropertyDrawers[typeof(string)] = CreateStringConfig; AllPropertyDrawers[typeof(Color)] = CreateColorConfig; + AllAbstractedPropertyDrawers[typeof(bool)] = CreateBoolConfigAbstracted; + AllAbstractedPropertyDrawers[typeof(string)] = CreateStringConfigAbstracted; + AllAbstractedPropertyDrawers[typeof(Color)] = CreateColorConfigAbstracted; } } \ No newline at end of file diff --git a/SpaceWarp.UI/API/UI/Skins.cs b/SpaceWarp.UI/API/UI/Skins.cs new file mode 100644 index 00000000..b281628b --- /dev/null +++ b/SpaceWarp.UI/API/UI/Skins.cs @@ -0,0 +1,28 @@ +using System; +using JetBrains.Annotations; +using SpaceWarp.API.Assets; +using UnityEngine; + +namespace SpaceWarp.API.UI; + +// This exposes the SpaceWarp internal skins +[Obsolete("SpaceWarp's support for IMGUI will not be getting updates, please use UITK instead")] +[PublicAPI] +public static class Skins +{ + private static GUISkin _skin; + + [Obsolete("SpaceWarp's support for IMGUI will not be getting updates, please use UITK instead")] + public static GUISkin ConsoleSkin + { + get + { + if (!_skin) + { + AssetManager.TryGetAsset($"{SpaceWarpPlugin.ModGuid}/swconsoleui/spacewarpconsole.guiskin", out _skin); + } + + return _skin; + } + } +} \ No newline at end of file diff --git a/SpaceWarp.UI/Backend/Extensions/ErrorDescriptionExtensions.cs b/SpaceWarp.UI/Backend/Extensions/ErrorDescriptionExtensions.cs new file mode 100644 index 00000000..4a56956e --- /dev/null +++ b/SpaceWarp.UI/Backend/Extensions/ErrorDescriptionExtensions.cs @@ -0,0 +1,46 @@ +using SpaceWarp.API.Mods; +using SpaceWarp.UI.ModList; + +namespace SpaceWarp.Backend.Extensions; + +internal static class ErrorDescriptionExtensions +{ + internal static void Apply(this SpaceWarpErrorDescription instance, ModListItemController controller) + { + controller.SetInfo(instance.Plugin); + controller.SetIsErrored(); + if (instance.MissingSwinfo) controller.SetMissingSWInfo(); + if (instance.BadDirectory) controller.SetBadDirectory(); + if (instance.BadID) controller.SetBadID(); + if (instance.MismatchedVersion) controller.SetMismatchedVersion(); + foreach (var disabledDependency in instance.DisabledDependencies) + { + controller.SetIsDependencyDisabled(disabledDependency); + } + + foreach (var erroredDependency in instance.ErroredDependencies) + { + controller.SetIsDependencyErrored(erroredDependency); + } + + foreach (var missingDependency in instance.MissingDependencies) + { + controller.SetIsDependencyMissing(missingDependency); + } + + foreach (var unsupportedDependency in instance.UnsupportedDependencies) + { + controller.SetIsDependencyUnsupported(unsupportedDependency); + } + + foreach (var unspecifiedDependency in instance.UnspecifiedDependencies) + { + controller.SetIsDependencyUnspecified(unspecifiedDependency); + } + + foreach (var conflict in instance.Incompatibilities) + { + controller.SetIsConflicting(conflict); + } + } +} \ No newline at end of file diff --git a/SpaceWarp/Backend/UI/Appbar/AppbarBackend.cs b/SpaceWarp.UI/Backend/UI/Appbar/AppbarBackend.cs similarity index 65% rename from SpaceWarp/Backend/UI/Appbar/AppbarBackend.cs rename to SpaceWarp.UI/Backend/UI/Appbar/AppbarBackend.cs index 43559c37..abdd99ca 100644 --- a/SpaceWarp/Backend/UI/Appbar/AppbarBackend.cs +++ b/SpaceWarp.UI/Backend/UI/Appbar/AppbarBackend.cs @@ -12,6 +12,7 @@ using KSP.Game; using KSP.OAB; using KSP.Sim.impl; +using KSP.UI; using KSP.UI.Binding; using KSP.UI.Flight; using SpaceWarp.API.Assets; @@ -20,6 +21,7 @@ using UnityEngine.Events; using UnityEngine.UI; using static SpaceWarp.Backend.UI.Appbar.AppbarBackend; +using UnityObject = UnityEngine.Object; namespace SpaceWarp.Backend.UI.Appbar; @@ -29,6 +31,7 @@ internal static class AppbarBackend public static readonly UnityEvent AppBarOABSubscriber = new(); public static readonly UnityEvent AppBarInFlightSubscriber = new(); + public static readonly UnityEvent AppBarKSCSubscriber = new(); internal static void SubscriberSchedulePing(AppbarEvent type) { @@ -41,6 +44,7 @@ internal static void SubscriberSchedulePing(AppbarEvent type) { AppbarEvent.Flight => AppBarInFlightSubscriber, AppbarEvent.OAB => AppBarOABSubscriber, + AppbarEvent.KSC => AppBarKSCSubscriber, _ => waiterObject.CreationEvent }; @@ -51,7 +55,8 @@ internal static void SubscriberSchedulePing(AppbarEvent type) internal enum AppbarEvent { Flight, - OAB + OAB, + KSC } #region Flight App Bar @@ -286,6 +291,140 @@ private static void SetOABTrayState(bool state) } #endregion + + #region KSC App Bar + + private static GameObject _kscTray; + + private static GameObject KSCTray + { + get + { + if (_kscTray == null) + { + return _kscTray = CreateKSCTray(); + } + + return _kscTray; + } + } + + private static GameObject CreateKSCTray() + { + Logger.LogInfo("Creating KSC app tray..."); + + // Find the KSC launch locations menu item; it will be used for cloning the app tray + + // Get the Launch Pads menu item + var kscMenu = GameObject.Find("GameManager/Default Game Instance(Clone)/UI Manager(Clone)/Main Canvas/KSCMenu(Clone)/LandingPanel/InteriorWindow/MenuButtons/Content/Menu"); + var launchLocationsButton = kscMenu != null ? kscMenu.GetChild("LaunchLocationFlyoutHeaderToggle") : null; + + if (kscMenu == null || launchLocationsButton == null) + { + Logger.LogError("Couldn't find Launch Pads menu item for cloning the KSC app tray."); + return null; + } + + // Clone it, add it to the menu and rename it + var kscAppTrayButton = UnityObject.Instantiate(launchLocationsButton, kscMenu.transform); + kscAppTrayButton.name = "KSC-AppTrayButton"; + + // Set the button icon (use OAB app tray icon) + var image = kscAppTrayButton.GetChild("Header").GetChild("Content").GetChild("Icon Panel").GetChild("icon").GetComponent(); + var tex = AssetManager.GetAsset($"{SpaceWarpPlugin.ModGuid}/images/oabTrayButton.png"); + tex.filterMode = FilterMode.Point; + image.sprite = Sprite.Create(tex, new Rect(0, 0, 32, 32), new Vector2(0.5f, 0.5f)); + + // Change the text to APPS + var title = kscAppTrayButton.GetChild("Header").GetChild("Content").GetChild("Title"); + { + // Suppress renaming of the button to Launchpad + var localizer = title.GetComponent(); + if (localizer) + { + UnityObject.Destroy(localizer); + } + var text = title.GetComponent(); + text.text = "Apps"; + } + + // Get the popup tray and rename it + var kscAppTray = kscAppTrayButton.GetChild("LaunchLocationsFlyoutTarget"); + kscAppTray.name = "KSC-AppTray"; + + // Delete existing buttons and separators in the tray + for (var i = 0; i < kscAppTray.transform.childCount; i++) + { + var child = kscAppTray.transform.GetChild(i); + + // Destroy all objects inside the tray, but keep the arrow ("thingy") that points to the menu button + if (!child.name.ToLowerInvariant().Contains("thingy")) + UnityObject.Destroy(child.gameObject); + } + + Logger.LogInfo("Created KSC app tray."); + + return kscAppTray; + } + + public static void AddKSCButton(string buttonText, Sprite buttonIcon, string buttonId, Action function) + { + Logger.LogInfo($"Adding KSC appbar button: {buttonId}."); + + // Grab the Launchpad_1 button, clone it and convert it to a mod launching button + + // Find the Launchpad_1 button. + var kscLaunchLocationsFlyoutTarget = GameObject.Find( + "GameManager/Default Game Instance(Clone)/UI Manager(Clone)/Main Canvas/KSCMenu(Clone)/LandingPanel/InteriorWindow/MenuButtons/Content/Menu/LaunchLocationFlyoutHeaderToggle/LaunchLocationsFlyoutTarget"); + var launchPadButton = kscLaunchLocationsFlyoutTarget != null ? kscLaunchLocationsFlyoutTarget.GetChild("Launchpad_1") : null; + + if (launchPadButton == null) + { + Logger.LogError("Couldn't find the KSC Launchpad_1 button."); + return; + } + + // Clone the button, add it to the popup tray and rename it + var modButton = UnityObject.Instantiate(launchPadButton, KSCTray.transform); + modButton.name = buttonId; + + // Change the text + var modText = modButton.GetChild("Content").GetChild("Text (TMP)").GetComponent(); + modText.text = buttonText; + + // Suppress renaming of the button + var localizer = modText.gameObject.GetComponent(); + if (localizer) + { + UnityObject.Destroy(localizer); + } + + // Change the icon + var icon = modButton.GetChild("Icon"); + var image = icon.GetComponent(); + image.sprite = buttonIcon; + + // Remove previous onclick listeners and add the function that mod will use + var buttonExtended = modButton.GetComponent(); + var previousListeners = modButton.GetComponent(); + if (previousListeners) + { + UnityObject.Destroy(previousListeners); + } + buttonExtended.onClick.AddListener(() => + { + Logger.LogInfo($"Mod button {buttonId} clicked."); + function(); + + // Hide the popup tray after the button is clicked + var toggle = KSCTray.GetComponentInParent(); + toggle.isOn = false; + }); + + Logger.LogInfo($"Added KSC appbar button: {buttonId}."); + } + + #endregion } internal class ToolbarBackendObject : KerbalBehavior @@ -304,6 +443,7 @@ private IEnumerator Awaiter() { AppbarEvent.Flight => new WaitForSeconds(1), AppbarEvent.OAB => new WaitForFixedUpdate(), + AppbarEvent.KSC => new WaitForFixedUpdate(), _ => new WaitForSeconds(1) }; @@ -332,3 +472,13 @@ public static void Postfix() SubscriberSchedulePing(AppbarEvent.OAB); } } + +[HarmonyPatch(typeof(KSCMenuManager))] +[HarmonyPatch("Start")] +internal class ToolbarBackendKSCPatcher +{ + public static void Postfix() + { + SubscriberSchedulePing(AppbarEvent.KSC); + } +} diff --git a/SpaceWarp.UI/Backend/UI/Loading/LoadingScreenManager.cs b/SpaceWarp.UI/Backend/UI/Loading/LoadingScreenManager.cs new file mode 100644 index 00000000..21559161 --- /dev/null +++ b/SpaceWarp.UI/Backend/UI/Loading/LoadingScreenManager.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.IO; +using KSP.Game; +using SpaceWarp.API.Mods; +using UniLinq; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.AddressableAssets.ResourceLocators; +using UnityEngine.ResourceManagement.Exceptions; +using UnityEngine.ResourceManagement.ResourceProviders; + +namespace SpaceWarp.Backend.UI.Loading; + +internal class LoadingScreenManager : ResourceProviderBase +{ + public Dictionary LoadingScreens = new(); + private List _locations = new(); + + public void SetupResourceLocator() + { + Addressables.AddResourceLocator(new ResourceLocationMap("sw-loading-screen-map", _locations)); + Addressables.ResourceManager.ResourceProviders.Add(this); + } + public void LoadScreens(Curtain curtain) + { + List allKeysToAdd = new(); + foreach (var mod in PluginList.AllEnabledAndActivePlugins.Where(x => x.DoLoadingActions)) + { + var directoryInfo = new DirectoryInfo(Path.Combine(mod.Folder.FullName, "assets", "loading_screens")); + if (!directoryInfo.Exists) continue; + foreach (var file in directoryInfo.EnumerateFiles()) + { + var loc = ("loading-screen-" + mod.Guid + "-" + file.FullName).ToLower(); + RegisterScreen(loc, file); + allKeysToAdd.Add(loc); + } + } + + foreach (var screen in curtain.LoadingScreens.Values.Distinct()) + { + screen.ScreenKeys.AddRange(allKeysToAdd); + } + } + + private void RegisterScreen(string location, FileInfo file) + { + var bytes = File.ReadAllBytes(file.FullName); + var tex = new Texture2D(2, 2); + tex.LoadImage(bytes); + var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f)); + sprite.name = location; + LoadingScreens.Add(location,sprite); + _locations.Add(new ResourceLocationData(new[] { location }, location, typeof(Sprite), typeof(Sprite))); + } + + public override void Provide(ProvideHandle provideHandle) + { + if (!LoadingScreens.ContainsKey(provideHandle.Location.PrimaryKey)) + { + provideHandle.Complete(null, false, + new ProviderException($"Unknown loading screen {provideHandle.Location.PrimaryKey}")); + } + + provideHandle.Complete(LoadingScreens[provideHandle.Location.PrimaryKey], true, null); + } +} \ No newline at end of file diff --git a/SpaceWarp/Backend/UI/Settings/CustomSettingsElementDescriptionController.cs b/SpaceWarp.UI/Backend/UI/Settings/CustomSettingsElementDescriptionController.cs similarity index 89% rename from SpaceWarp/Backend/UI/Settings/CustomSettingsElementDescriptionController.cs rename to SpaceWarp.UI/Backend/UI/Settings/CustomSettingsElementDescriptionController.cs index 8ef7bf1e..347f74f8 100644 --- a/SpaceWarp/Backend/UI/Settings/CustomSettingsElementDescriptionController.cs +++ b/SpaceWarp.UI/Backend/UI/Settings/CustomSettingsElementDescriptionController.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using KSP.Game; -using KSP.UI; using UnityEngine.EventSystems; using DG.Tweening; -using DG.Tweening.Core; -using UnityEngine.Serialization; namespace SpaceWarp.Backend.UI.Settings; public class CustomSettingsElementDescriptionController : KerbalMonoBehaviour, IPointerEnterHandler, - IEventSystemHandler, IPointerExitHandler { @@ -25,7 +20,7 @@ public class CustomSettingsElementDescriptionController : public void OnPointerEnter(PointerEventData eventData) => OnHover(true); public void OnPointerExit(PointerEventData eventData) => OnHover(false); - + private void OnHover(bool isHovered) { diff --git a/SpaceWarp.UI/Modules/UI.cs b/SpaceWarp.UI/Modules/UI.cs new file mode 100644 index 00000000..9ec81c5e --- /dev/null +++ b/SpaceWarp.UI/Modules/UI.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using BepInEx.Bootstrap; +using JetBrains.Annotations; +using KSP.Assets; +using KSP.Game; +using KSP.Game.Flow; +using SpaceWarp.API.Assets; +using SpaceWarp.API.Configuration; +using SpaceWarp.API.Loading; +using SpaceWarp.API.UI.Appbar; +using SpaceWarp.Backend.UI.Appbar; +using SpaceWarp.Backend.UI.Loading; +using SpaceWarp.InternalUtilities; +using SpaceWarp.UI.AvcDialog; +using SpaceWarp.UI.Console; +using SpaceWarp.UI.ModList; +using SpaceWarp.UI.Settings; +using UitkForKsp2.API; +using UnityEngine; +using UnityEngine.UIElements; +using ConfigManager = ConfigurationManager.ConfigurationManager; + +namespace SpaceWarp.Modules; + +[PublicAPI] +public class UI : SpaceWarpModule +{ + public override string Name => "SpaceWarp.UI"; + + internal static UI Instance; + + internal ConfigValue ConfigAllColor; + internal ConfigValue ConfigCheckVersions; + internal ConfigValue ConfigShowMainMenuWarningForOutdatedMods; + internal ConfigValue ConfigShowMainMenuWarningForErroredMods; + internal ConfigValue ConfigDebugColor; + internal ConfigValue ConfigDebugMessageLimit; + internal ConfigValue ConfigErrorColor; + internal ConfigValue ConfigInfoColor; + internal ConfigValue ConfigMessageColor; + internal ConfigValue ConfigShowConsoleButton; + internal ConfigValue ConfigShowTimeStamps; + internal ConfigValue ConfigTimeStampFormat; + internal ConfigValue ConfigWarningColor; + internal ConfigManager ConfigurationManager; + internal ModListController ModListController; + internal SpaceWarpConsole SpaceWarpConsole; + + + public override void LoadModule() + { + AppbarBackend.AppBarInFlightSubscriber.AddListener(Appbar.LoadAllButtons); + AppbarBackend.AppBarOABSubscriber.AddListener(Appbar.LoadOABButtons); + AppbarBackend.AppBarKSCSubscriber.AddListener(Appbar.LoadKSCButtons); + Instance = this; + ConfigErrorColor = new(ModuleConfiguration.Bind("Debug Console", "Color Error", Color.red, + "The color for log messages that have the level: Error/Fatal (bolded)")); + ConfigWarningColor = new(ModuleConfiguration.Bind("Debug Console", "Color Warning", Color.yellow, + "The color for log messages that have the level: Warning")); + ConfigMessageColor = new(ModuleConfiguration.Bind("Debug Console", "Color Message", Color.white, + "The color for log messages that have the level: Message")); + ConfigInfoColor = new(ModuleConfiguration.Bind("Debug Console", "Color Info", Color.cyan, + "The color for log messages that have the level: Info")); + ConfigDebugColor = new(ModuleConfiguration.Bind("Debug Console", "Color Debug", Color.green, + "The color for log messages that have the level: Debug")); + ConfigAllColor = new(ModuleConfiguration.Bind("Debug Console", "Color All", Color.magenta, + "The color for log messages that have the level: All")); + ConfigShowConsoleButton = new(ModuleConfiguration.Bind("Debug Console", "Show Console Button", false, + "Show console button in app.bar, requires restart")); + ConfigShowTimeStamps = new(ModuleConfiguration.Bind("Debug Console", "Show Timestamps", true, + "Show time stamps in debug console")); + ConfigTimeStampFormat = new(ModuleConfiguration.Bind("Debug Console", "Timestamp Format", "HH:mm:ss.fff", + "The format for the timestamps in the debug console.")); + ConfigDebugMessageLimit = new(ModuleConfiguration.Bind("Debug Console", "Message Limit", 1000, + "The maximum number of messages to keep in the debug console.")); + ConfigShowMainMenuWarningForOutdatedMods = new(ModuleConfiguration.Bind("Version Checking", + "Show Warning for Outdated Mods", true, + "Whether or not Space Warp should display a warning in main menu if there are outdated mods")); + ConfigShowMainMenuWarningForErroredMods = new(ModuleConfiguration.Bind("Version Checking", + "Show Warning for Errored Mods", true, + "Whether or not Space Warp should display a warning in main menu if there are errored mods")); + BepInEx.Logging.Logger.Listeners.Add(new SpaceWarpConsoleLogListener(this)); + } + + public override void PreInitializeModule() + { + } + + public override void InitializeModule() + { + ModuleLogger.LogInfo("Initializing UI"); + if (VersionChecking.Instance.ConfigFirstLaunch.Value) + { + var ui = new GameObject("Version Check Dialog"); + ui.Persist(); + ui.SetActive(true); + + // Generate a prompt for whether or not space warp should check mod versions + var avcDialogUxml = + AssetManager.GetAsset( + $"{SpaceWarpPlugin.ModGuid}/avcdialog/ui/avcdialog/avcdialog.uxml"); + var avcDialog = Window.CreateFromUxml(avcDialogUxml, "Space Warp AVC Dialog", ui.transform, true); + + var avcDialogController = avcDialog.gameObject.AddComponent(); + avcDialogController.Module = VersionChecking.Instance; + } + + InitializeUI(); + } + + public override void PostInitializeModule() + { + ModuleLogger.LogInfo("Post Initializing UI"); + InitializeSettingsUI(); + InitializeSpaceWarpDetailsFoldout(); + ModListController.AddMainMenuItem(); + } + + public override List Prerequisites => new() + { + "SpaceWarp.VersionChecking" + }; + + private void InitializeUI() + { + ConfigurationManager = (ConfigurationManager.ConfigurationManager)Chainloader + .PluginInfos[ConfigManager.GUID].Instance; + + var ui = new GameObject("Space Warp UI"); + ui.Persist(); + ui.SetActive(true); + + var modListUxml = AssetManager.GetAsset( + $"{SpaceWarpPlugin.ModGuid}/modlist/ui/modlist/modlist.uxml" + ); + var modList = Window.CreateFromUxml(modListUxml, "Space Warp Mod List", ui.transform, true); + ModListController = modList.gameObject.AddComponent(); + + var swConsoleUxml = AssetManager.GetAsset( + $"{SpaceWarpPlugin.ModGuid}/swconsole/ui/console/console.uxml" + ); + + var swConsole = Window.CreateFromUxml(swConsoleUxml, "Space Warp Console", ui.transform, true); + SpaceWarpConsole = swConsole.gameObject.AddComponent(); + } + + + private static void InitializeSettingsUI() + { + GameObject settingsController = new("Space Warp Settings Controller"); + settingsController.Persist(); + settingsController.AddComponent(); + settingsController.SetActive(true); + } + + private static void InitializeSpaceWarpDetailsFoldout() + { + VisualElement GenerateModulesText() + { + var detailsContainer = new ScrollView(); + var websiteContainer = new VisualElement(); + websiteContainer.style.flexDirection = FlexDirection.Row; + detailsContainer.Add(websiteContainer); + var websiteHeader = new TextElement() + { + text = "Wiki: " + }; + websiteContainer.Add(websiteHeader); + var websiteLink = new Button() + { + text = "https://wiki.spacewarp.org" + }; + websiteLink.classList.Add("link"); + websiteLink.RegisterCallback(_ => Application.OpenURL(websiteLink.text)); + websiteContainer.Add(websiteLink); + var loadedModules = new TextElement(); + detailsContainer.Add(loadedModules); + loadedModules.visible = true; + loadedModules.style.display = DisplayStyle.Flex; + detailsContainer.visible = true; + detailsContainer.style.display = DisplayStyle.Flex; + var str = "Loaded modules: "; + foreach (var module in ModuleManager.AllSpaceWarpModules) + { + str += $"\n- {module.Name}"; + } + + loadedModules.text = str; + return detailsContainer; + } + + API.UI.ModList.RegisterDetailsFoldoutGenerator(SpaceWarpPlugin.ModGuid, GenerateModulesText); + } +} \ No newline at end of file diff --git a/SpaceWarp/Patching/ConfigurationPatch.cs b/SpaceWarp.UI/Patching/ConfigurationPatch.cs similarity index 86% rename from SpaceWarp/Patching/ConfigurationPatch.cs rename to SpaceWarp.UI/Patching/ConfigurationPatch.cs index 16e351fe..b2e6a2a7 100644 --- a/SpaceWarp/Patching/ConfigurationPatch.cs +++ b/SpaceWarp.UI/Patching/ConfigurationPatch.cs @@ -9,7 +9,7 @@ namespace SpaceWarp.Patching; [HarmonyPatch(nameof(Utils.FindPlugins))] public class ConfigurationPatch { - static void Postfix(ref BaseUnityPlugin[] __result) + private static void Postfix(ref BaseUnityPlugin[] __result) { //Disable because I know what I am doing. #pragma warning disable CS0618 diff --git a/SpaceWarp.UI/Patching/CurtainPatch.cs b/SpaceWarp.UI/Patching/CurtainPatch.cs new file mode 100644 index 00000000..853abefb --- /dev/null +++ b/SpaceWarp.UI/Patching/CurtainPatch.cs @@ -0,0 +1,23 @@ +using System; +using HarmonyLib; +using KSP.Networking.MP.Utils; +using SpaceWarp.Backend.UI.Loading; +using UnityEngine; + +namespace SpaceWarp.Patching; + +[HarmonyPatch(typeof(Curtain))] +internal static class CurtainPatch +{ + internal static LoadingScreenManager LoadingScreenManager; + + [HarmonyPrefix] + [HarmonyPatch(nameof(Curtain.Awake))] + public static void LoadScreensEarly(Curtain __instance) + { + LoadingScreenManager = new LoadingScreenManager(); + LoadingScreenManager.LoadScreens(__instance); + __instance._appStartLoadingScreenSpriteOptions.AddRange(LoadingScreenManager.LoadingScreens + .Values); + } +} \ No newline at end of file diff --git a/SpaceWarp.UI/Patching/LoadingFlowPatch.cs b/SpaceWarp.UI/Patching/LoadingFlowPatch.cs new file mode 100644 index 00000000..15cbe4f0 --- /dev/null +++ b/SpaceWarp.UI/Patching/LoadingFlowPatch.cs @@ -0,0 +1,40 @@ +using System; +using HarmonyLib; +using KSP.Game; +using KSP.Game.Flow; +using KSP.Startup; +using UnityEngine; + +namespace SpaceWarp.Patching; + +[HarmonyPatch(typeof(SequentialFlow))] +internal static class LoadingFlowPatch +{ + internal static long LoadingScreenTimer; + + [HarmonyPostfix] + [HarmonyPatch(nameof(SequentialFlow.NextFlowAction))] + internal static void SelectRandomLoadingScreen(SequentialFlow __instance) + { + if (__instance.FlowState != FlowState.Finished) + { + var time = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds(); + if (time - LoadingScreenTimer >= 7) + { + LoadingScreenTimer = time; + // Switch loading screen + if (GameManager.Instance == null || GameManager.Instance.Game == null || + GameManager.Instance.Game.UI == null) return; + + var curtain = GameManager.Instance.Game.UI.Curtain; + if (curtain == null) return; + // Just to update this as well + curtain._curDefaultLoadingScreenSprite = curtain.PickRandomStartingSprite(); + var ctx = curtain._curtainContextData; + curtain.SelectRandomImage(ctx); + curtain.SetImage(ctx); + } + } + } + +} \ No newline at end of file diff --git a/SpaceWarp.UI/Patching/LoadingScreenDeserializationPatch.cs b/SpaceWarp.UI/Patching/LoadingScreenDeserializationPatch.cs new file mode 100644 index 00000000..5a75d466 --- /dev/null +++ b/SpaceWarp.UI/Patching/LoadingScreenDeserializationPatch.cs @@ -0,0 +1,21 @@ +using System.Linq; +using HarmonyLib; +using KSP.Game; +using KSP.Game.StartupFlow; + +namespace SpaceWarp.Patching; + +[HarmonyPatch(typeof(DeserializeLoadingScreens))] +public static class LoadingScreenDeserializationPatch +{ + [HarmonyPatch(nameof(DeserializeLoadingScreens.DoAction))] + [HarmonyPostfix] + public static void AddToAllFlows() + { + var curtain = GameManager.Instance.Game.UI.Curtain; + foreach (var flow in curtain.LoadingScreens.Values.Distinct()) + { + flow.ScreenKeys.AddRange(CurtainPatch.LoadingScreenManager.LoadingScreens.Keys); + } + } +} \ No newline at end of file diff --git a/SpaceWarp/Patching/MainMenuPatcher.cs b/SpaceWarp.UI/Patching/MainMenuPatcher.cs similarity index 98% rename from SpaceWarp/Patching/MainMenuPatcher.cs rename to SpaceWarp.UI/Patching/MainMenuPatcher.cs index 40767757..58261589 100644 --- a/SpaceWarp/Patching/MainMenuPatcher.cs +++ b/SpaceWarp.UI/Patching/MainMenuPatcher.cs @@ -5,6 +5,7 @@ using KSP.Game.StartupFlow; using SpaceWarp.API.UI; using TMPro; +using UnityObject = UnityEngine.Object; namespace SpaceWarp.Patching; diff --git a/SpaceWarp.UI/SpaceWarp.UI.csproj b/SpaceWarp.UI/SpaceWarp.UI.csproj new file mode 100644 index 00000000..f403d4d2 --- /dev/null +++ b/SpaceWarp.UI/SpaceWarp.UI.csproj @@ -0,0 +1,22 @@ + + + + + ..\ThirdParty\ConfigurationManager\ConfigurationManager.dll + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SpaceWarp/UI/AvcDialog/AvcDialogController.cs b/SpaceWarp.UI/UI/AvcDialog/AvcDialogController.cs similarity index 65% rename from SpaceWarp/UI/AvcDialog/AvcDialogController.cs rename to SpaceWarp.UI/UI/AvcDialog/AvcDialogController.cs index f14e89af..3966c459 100644 --- a/SpaceWarp/UI/AvcDialog/AvcDialogController.cs +++ b/SpaceWarp.UI/UI/AvcDialog/AvcDialogController.cs @@ -1,4 +1,5 @@ -using UitkForKsp2.API; +using SpaceWarp.Modules; +using UitkForKsp2.API; using UnityEngine; using UnityEngine.UIElements; @@ -6,7 +7,7 @@ namespace SpaceWarp.UI.AvcDialog; public class AvcDialogController : MonoBehaviour { - internal SpaceWarpPlugin Plugin; + internal VersionChecking Module; private void Awake() { @@ -18,16 +19,16 @@ private void Awake() container.Q