From 36708199fb54d40f0c09577af7d91ccb32d6f167 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 21:47:43 -0700 Subject: [PATCH 01/18] Major Surgery to remove much of the KSPAPIExtensions functionality in favor of a leaner set of tools focused on fixing Squad's broken tweakables. --- Source/CommonAPI/GameObjectExt.cs | 2 +- Source/CommonAPI/ResourceExtensions.cs | 169 --- Source/DuckTyping/AmbiguousTypesException.cs | 42 - Source/DuckTyping/DelegateDuckCaster.cs | 119 -- Source/DuckTyping/DelegateDuckProxyType.cs | 249 ---- Source/DuckTyping/DuckProxyType.cs | 593 -------- Source/DuckTyping/DuckTyping.cs | 641 --------- Source/DuckTyping/EnumStringDuckCaster.cs | 120 -- Source/DuckTyping/FromTypeTree.cs | 66 - .../DuckTyping/GenericParameterDuckCaster.cs | 45 - Source/DuckTyping/IDuckCaster.cs | 87 -- Source/DuckTyping/IDuckProxy.cs | 32 - Source/DuckTyping/IStaticDuckCaster.cs | 56 - Source/DuckTyping/InterfaceDuckCaster.cs | 171 --- Source/DuckTyping/InterfaceDuckProxyType.cs | 882 ------------ .../DuckTyping/InterfaceStaticDuckCaster.cs | 99 -- Source/DuckTyping/ToTypeFromTypeTree.cs | 66 - Source/DuckTyping/TypeKey.cs | 116 -- ...SPAPIExtensions.csproj => KSPAPIEL.csproj} | 72 +- Source/{KSPAPIExtensions.sln => KSPAPIEL.sln} | 4 +- Source/PartMessage/Attributes.cs | 137 -- Source/PartMessage/CommonMessages.cs | 157 --- Source/PartMessage/PartMessageService.cs | 291 ---- Source/PartMessage/zzImplementation.cs | 1192 ----------------- Source/Tweakables/UIPartActionChooseOption.cs | 2 +- Source/Tweakables/UIPartActionFloatEdit.cs | 2 +- Source/Tweakables/UIPartActionScaleEdit.cs | 2 +- Source/Tweakables/UIPartActionsExtended.cs | 8 +- Source/Utils/ConfigNodeUtils.cs | 227 ---- Source/Utils/DebuggingUtils.cs | 204 --- Source/Utils/EngineWrapper.cs | 354 ----- Source/Utils/KSPUtils.cs | 290 +--- Source/Utils/MathUtils.cs | 166 +-- Source/Utils/ParseUtils.cs | 166 --- Source/Utils/PhysicsDebugger.cs | 67 - Source/Utils/SystemUtils.cs | 2 +- Source/Utils/UnityUtils.cs | 256 ---- Source/zzVersionChecker.cs | 2 +- 38 files changed, 35 insertions(+), 7121 deletions(-) delete mode 100644 Source/CommonAPI/ResourceExtensions.cs delete mode 100644 Source/DuckTyping/AmbiguousTypesException.cs delete mode 100644 Source/DuckTyping/DelegateDuckCaster.cs delete mode 100644 Source/DuckTyping/DelegateDuckProxyType.cs delete mode 100644 Source/DuckTyping/DuckProxyType.cs delete mode 100644 Source/DuckTyping/DuckTyping.cs delete mode 100644 Source/DuckTyping/EnumStringDuckCaster.cs delete mode 100644 Source/DuckTyping/FromTypeTree.cs delete mode 100644 Source/DuckTyping/GenericParameterDuckCaster.cs delete mode 100644 Source/DuckTyping/IDuckCaster.cs delete mode 100644 Source/DuckTyping/IDuckProxy.cs delete mode 100644 Source/DuckTyping/IStaticDuckCaster.cs delete mode 100644 Source/DuckTyping/InterfaceDuckCaster.cs delete mode 100644 Source/DuckTyping/InterfaceDuckProxyType.cs delete mode 100644 Source/DuckTyping/InterfaceStaticDuckCaster.cs delete mode 100644 Source/DuckTyping/ToTypeFromTypeTree.cs delete mode 100644 Source/DuckTyping/TypeKey.cs rename Source/{KSPAPIExtensions.csproj => KSPAPIEL.csproj} (60%) rename Source/{KSPAPIExtensions.sln => KSPAPIEL.sln} (83%) delete mode 100644 Source/PartMessage/Attributes.cs delete mode 100644 Source/PartMessage/CommonMessages.cs delete mode 100644 Source/PartMessage/PartMessageService.cs delete mode 100644 Source/PartMessage/zzImplementation.cs delete mode 100644 Source/Utils/ConfigNodeUtils.cs delete mode 100644 Source/Utils/DebuggingUtils.cs delete mode 100644 Source/Utils/EngineWrapper.cs delete mode 100644 Source/Utils/ParseUtils.cs delete mode 100644 Source/Utils/PhysicsDebugger.cs delete mode 100644 Source/Utils/UnityUtils.cs diff --git a/Source/CommonAPI/GameObjectExt.cs b/Source/CommonAPI/GameObjectExt.cs index d7422f8a..5c44ab6a 100644 --- a/Source/CommonAPI/GameObjectExt.cs +++ b/Source/CommonAPI/GameObjectExt.cs @@ -5,7 +5,7 @@ using KSP.IO; -namespace KSPAPIExtensions { +namespace KSPAPIEL { internal static class GameObjectExtension { internal static T AddTaggedComponent (this GameObject go) where T : Component diff --git a/Source/CommonAPI/ResourceExtensions.cs b/Source/CommonAPI/ResourceExtensions.cs deleted file mode 100644 index 45fb7cdb..00000000 --- a/Source/CommonAPI/ResourceExtensions.cs +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2014 Bill Currie -// This file is in the public domain (use it as you see fit). -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -using KSP.IO; - -namespace KSPAPIExtensions { - /// - /// Extended resource attributes - /// - public class ExtendedResourceDefinition - { - PartResourceDefinition res_def; - - public bool isMineable = false; - public bool isHullResource = false; - public float volume = 5; // liters per unit - - public string name - { - get { - return res_def.name; - } - } - public Color color - { - get { - return res_def.color; - } - } - public float density - { - get { - return res_def.density; - } - } - public int id - { - get { - return res_def.id; - } - } - public bool isTweakable - { - get { - return res_def.isTweakable; - } - } - public ResourceFlowMode resourceFlowMode - { - get { - return res_def.resourceFlowMode; - } - } - public ResourceTransferMode resourceTransferMode - { - get { - return res_def.resourceTransferMode; - } - } - - public void Load (ConfigNode node) - { - string name = node.GetValue ("name"); - res_def = PartResourceLibrary.Instance.GetDefinition (name); - if (node.HasValue ("isMineable")) { - bool.TryParse (node.GetValue ("isMineable"), out isMineable); - } - if (node.HasValue ("isHullResource")) { - bool.TryParse (node.GetValue ("isHullResource"), - out isHullResource); - } - if (node.HasValue ("volume")) { - float.TryParse (node.GetValue ("volume"), out volume); - } - } - } - - /// - /// Extended resource attributes - /// - public static class PartResourceDefinitionExtension - { - static Dictionary resource_dict; - - /// - /// List of all resources as extended resources. The list is a private - /// copy. - /// - public static List resources - { - get { - if (resource_dict == null) { - Initialize (); - } - return new List (resource_dict.Values); - } - } - - /// - /// Find and load all resources as extended resources. - /// - static void Initialize () - { - var dbase = GameDatabase.Instance; - var resourceNodes = dbase.GetConfigNodes ("RESOURCE_DEFINITION"); - resource_dict = new Dictionary (); - foreach (var resource in resourceNodes) { - var res = new ExtendedResourceDefinition (); - res.Load (resource); - resource_dict.Add (res.name, res); - } - } - - /// - /// Get an extended resource. - /// - /// Resource name - /// An extended resource defintiion or null if not found - public static ExtendedResourceDefinition GetResource (string name) - { - if (resource_dict == null) { - Initialize (); - } - if (resource_dict.ContainsKey (name)) { - return resource_dict[name]; - } - return null; - } - - /// - /// Can the resource be mined? That is, is this a raw resource (eg, - /// Kethane, ore, etc). - /// - /// The resource defintion - /// Whether the resource can be mined. - public static bool isMineable (this PartResourceDefinition resdef) - { - return GetResource (resdef.name).isMineable; - } - - /// - /// Is this a resource needed for building ship "hulls". Used primarily - /// by Extraplanetary Launchpads. - /// - /// The resource defintion - /// Whether the resource is mull material. - public static bool isHullResource (this PartResourceDefinition resdef) - { - return GetResource (resdef.name).isHullResource; - } - - /// - /// The amount of space in liters taken up by one unit of the resource. - /// 5l/u is common (LiquidFuel, Oxidizer, Ore, Metal), though RealFuels - /// will likely set it to 1l/u. - /// - /// The resource defintion - /// The volume of a single unit of the resource in liters - public static float getVolume (this PartResourceDefinition resdef) - { - return GetResource (resdef.name).volume; - } - } - -} diff --git a/Source/DuckTyping/AmbiguousTypesException.cs b/Source/DuckTyping/AmbiguousTypesException.cs deleted file mode 100644 index 44739d15..00000000 --- a/Source/DuckTyping/AmbiguousTypesException.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Exception thrown when two types cannot be distinguished from one another in a way that enables comparison. - /// - public sealed class AmbiguousTypesException : Exception - { - private Type m_Type1; - private Type m_Type2; - - /// - /// Constructs an object. - /// - /// First type. - /// Second type. - public AmbiguousTypesException(Type type1, Type type2) - : base("Types cannot be distinguished from one another in a way that enables comparison.\r\n\r\n" - + "Type 1: " + type1.FullName + "\r\n" - + "Type 2: " + type2.FullName + "\r\n\r\n" - + "NOTE: This exception indicates an internal problem in the duck typing library. Please let the author know if " - + "this exception is encountered. (http://www.deftflux.net) Developers: For more information on the types " - + "involved, refer to the Type1 and Type2 properties of this exception.") - { - m_Type1 = type1; - m_Type2 = type2; - } - - /// - /// Gets the first type. - /// - public Type Type1 { get { return m_Type1; } } - - /// - /// Gets the second type. - /// - public Type Type2 { get { return m_Type2; } } - } -} diff --git a/Source/DuckTyping/DelegateDuckCaster.cs b/Source/DuckTyping/DelegateDuckCaster.cs deleted file mode 100644 index 2e73edcd..00000000 --- a/Source/DuckTyping/DelegateDuckCaster.cs +++ /dev/null @@ -1,119 +0,0 @@ -// DelegateDuckCaster.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Duck casting strategy used to cast two delegate types. - /// - internal class DelegateDuckCaster : IDuckCaster - { - private static ToTypeFromTypeTree s_ProxyTypeTree; - - /// - /// Initializes static members. - /// - static DelegateDuckCaster() - { - s_ProxyTypeTree = new ToTypeFromTypeTree(); - } - - /// - /// Constructs an object. - /// - public DelegateDuckCaster() - { } - - public bool ShouldCast(Type toType, Type fromType) - { - return (toType.IsSubclassOf(typeof(Delegate)) && fromType.IsSubclassOf(typeof(Delegate))); - } - - public bool ShouldUncast(object duck) - { - return (duck is Delegate) && (((Delegate)duck).Target is IDuckProxy); - } - - public bool CanCast(Type toType, Type fromType) - { - return ShouldCast(toType, fromType) && GetProxyType(toType, fromType).CanProxy(); - } - - public bool CouldUncast(Type originalType, Type castedType) - { - return CanCast(castedType, originalType); - } - - public void PrepareCast(Type toType, Type fromType) - { - GetProxyType(toType, fromType).Prepare(); - } - - public object Cast(Type toType, object duck) - { - return GetProxyType(toType, duck.GetType()).WrapDuck((Delegate)duck); - } - - public object Uncast(object duck) - { - object duckDelegateObj = ((IDuckProxy)(((Delegate)duck).Target)).UnwrapDuck(); - if (duckDelegateObj is Delegate) duck = duckDelegateObj; - - return duck; - } - - /// - /// Gets the DelegateDuckProxyType object for a given duck type. - /// - /// Type to be casted to. - /// Type of delegate to be casted. - /// The duck proxy type to use to cast or prepare for casting. - private static DelegateDuckProxyType GetProxyType(Type toType, Type fromType) - { - DelegateDuckProxyType proxyType = null; - - FromTypeTree fromTypeTree = null; - if (s_ProxyTypeTree.ContainsKey(toType)) - { - fromTypeTree = s_ProxyTypeTree[toType]; - - if (fromTypeTree.ContainsKey(fromType)) - { - proxyType = fromTypeTree[fromType]; - } - } - - if (proxyType == null) - { - proxyType = new DelegateDuckProxyType(toType, fromType); - - if (proxyType.CanProxy()) - { - if (fromTypeTree == null) - { - fromTypeTree = new FromTypeTree(); - s_ProxyTypeTree.Add(toType, fromTypeTree); - } - - fromTypeTree.Add(fromType, proxyType); - } - } - - return proxyType; - } - } -} diff --git a/Source/DuckTyping/DelegateDuckProxyType.cs b/Source/DuckTyping/DelegateDuckProxyType.cs deleted file mode 100644 index 1723d164..00000000 --- a/Source/DuckTyping/DelegateDuckProxyType.cs +++ /dev/null @@ -1,249 +0,0 @@ -// DelegateDuckProxyType.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; -using System.Reflection; -using System.Reflection.Emit; -using System.Threading; -using System.IO; - -namespace DeftTech.DuckTyping -{ - /// - /// Class that performs the generation of duck proxy types for forwarding delegate calls. - /// - internal sealed class DelegateDuckProxyType - { - private delegate Delegate WrapDuckDelegate(Delegate duck); - - private Type m_ToDelegateType; - private Type m_FromDelegateType; - private Type m_ProxyType; - private WrapDuckDelegate m_WrapDuck; - private MethodInfo m_InvokeMethod; - - /// - /// Constructs an object. - /// - /// Type to be casted to. - /// Type of delegate to be casted. - public DelegateDuckProxyType(Type toDelegateType, Type fromDelegateType) - { - m_ToDelegateType = toDelegateType; - m_FromDelegateType = fromDelegateType; - m_ProxyType = null; - } - - /// - /// Determines whether a proxy can be generated for the types given in the constructor. - /// - /// If a proxy can be generated for the types given in the constructor, true; otherwise, false. - public bool CanProxy() - { - bool canProxy = false; - - if (m_ToDelegateType.IsSubclassOf(typeof(Delegate)) && m_FromDelegateType.IsSubclassOf(typeof(Delegate))) - { - MethodInfo variantMethod = m_ToDelegateType.GetMethod("Invoke"); - MethodInfo duckMethod = m_FromDelegateType.GetMethod("Invoke"); - - // Must have a compatible parameter list - ParameterInfo[] variantMethodParameters = variantMethod.GetParameters(); - ParameterInfo[] duckMethodParameters = duckMethod.GetParameters(); - - if (duckMethodParameters.Length == variantMethodParameters.Length) - { - bool areParameterTypesCompatible = true; - - for (int i = 0; i < duckMethodParameters.Length; i++) - { - if (!DuckTyping.AreTypesCompatible(variantMethodParameters[i].ParameterType, duckMethodParameters[i].ParameterType)) - { - areParameterTypesCompatible = false; - break; - } - } - - if (areParameterTypesCompatible) - { - // Must have a compatible return type - if (DuckTyping.AreTypesCompatible(variantMethod.ReturnType, duckMethod.ReturnType)) - { - canProxy = true; - } - } - } - } - - return canProxy; - } - - /// - /// Prepares by generating the proxy type for the types given in the constructor. - /// - public void Prepare() - { - GenerateProxyType(); - } - - /// - /// Calls the static WrapDuck method of the generated proxy type to initialize a new instance of - /// the proxy type with a given duck delegate and return a delegate to its invoke method. - /// - /// Duck delegate that the new proxy object should forward calls to. - /// A delegate of the type given in the constructor that calls the given duck delegate. - public Delegate WrapDuck(Delegate duck) - { - GenerateProxyType(); - - return m_WrapDuck(duck); - } - - /// - /// If a proxy type has not been generated yet, generates a proxy type that defines a method - /// matching the method signature of the to delegate type given in the constructor which forwards - /// calls to a delegate of the from type given in the constructor. - /// - private void GenerateProxyType() - { - if (m_ProxyType == null) - { - if (!CanProxy()) - { - throw new ArgumentException(m_FromDelegateType.FullName + " is not compatible with " + m_ToDelegateType.FullName + "."); - } - - AssemblyBuilderAccess assemblyBuilderAccess; - -//#if !DEBUG - assemblyBuilderAccess = AssemblyBuilderAccess.Run; -//#else - //assemblyBuilderAccess = AssemblyBuilderAccess.RunAndSave; -//#endif - - AppDomain domain = Thread.GetDomain(); - string assemblyName = "DuckDelegateProxy_" + m_ToDelegateType.Name.Replace(".", "_").Replace("+", "-") + "_" + m_FromDelegateType.Name.Replace(".", "_").Replace("+", "-") + ".dll"; - AssemblyBuilder assembly = domain.DefineDynamicAssembly(new AssemblyName(assemblyName), assemblyBuilderAccess); - ModuleBuilder module = assembly.DefineDynamicModule(assemblyName); - - TypeBuilder proxyType = module.DefineType("DuckDelegateProxy"); - proxyType.AddInterfaceImplementation(typeof(IDuckProxy)); - - // Define private field to hold a reference to the duck delegate to forward calls to. - FieldBuilder duckDelegateField = proxyType.DefineField("m_DuckDelegate", m_FromDelegateType, FieldAttributes.Private); - - // Define private .ctor(duckDelegateType duckDelegate) - ConstructorBuilder constructor = proxyType.DefineConstructor(MethodAttributes.Private, CallingConventions.HasThis, new Type[] { m_FromDelegateType }); - ILGenerator constructorIL = constructor.GetILGenerator(); - constructorIL.Emit(OpCodes.Ldarg_0); - constructorIL.Emit(OpCodes.Ldarg_1); - constructorIL.Emit(OpCodes.Stfld, duckDelegateField); - constructorIL.Emit(OpCodes.Ret); - - // Define Invoke method - MethodBuilder invokeMethod = ImplementInvokeMethod(proxyType, duckDelegateField); - - // Define public static Delegate Wrap(Delegate duck) - MethodBuilder wrapMethod = proxyType.DefineMethod("Wrap", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(Delegate), new Type[] { typeof(Delegate) }); - ILGenerator wrapMethodIL = wrapMethod.GetILGenerator(); - wrapMethodIL.Emit(OpCodes.Ldarg_0); - wrapMethodIL.Emit(OpCodes.Newobj, constructor); - wrapMethodIL.Emit(OpCodes.Ldftn, invokeMethod); - wrapMethodIL.Emit(OpCodes.Newobj, m_ToDelegateType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) })); - wrapMethodIL.Emit(OpCodes.Ret); - - // Define public object UnwrapDuck() - Implementation of IDuckProxy - MethodBuilder unwrapMethod = proxyType.DefineMethod("UnwrapDuck", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(object), new Type[0]); - ILGenerator unwrapMethodIL = unwrapMethod.GetILGenerator(); - unwrapMethodIL.Emit(OpCodes.Ldarg_0); - unwrapMethodIL.Emit(OpCodes.Ldfld, duckDelegateField); - unwrapMethodIL.Emit(OpCodes.Ret); - - // Bake it - m_ProxyType = proxyType.CreateType(); - m_WrapDuck = (WrapDuckDelegate)(Delegate.CreateDelegate(typeof(WrapDuckDelegate), m_ProxyType, wrapMethod.Name)); - m_InvokeMethod = m_ProxyType.GetMethod("Invoke"); - -//#if DEBUG - // If we're in debug mode, save the assembly so we can disassemble it if we want. - //assembly.Save(assemblyName); -//#endif - } - } - - /// - /// Implements the method of the proxy type that invokes the delegate given in its constructor. - /// - /// Type builder to define the invoke method in. - /// Field that stores a reference to the delegate to call. - /// The implemented invoke method. - private MethodBuilder ImplementInvokeMethod(TypeBuilder proxyType, FieldInfo duckDelegateField) - { - MethodInfo variantMethod = m_ToDelegateType.GetMethod("Invoke"); - MethodInfo duckMethod = m_FromDelegateType.GetMethod("Invoke"); - - ParameterInfo[] variantMethodParameters = variantMethod.GetParameters(); - ParameterInfo[] duckMethodParameters = duckMethod.GetParameters(); - - MethodBuilder proxyMethod = proxyType.DefineMethod("Invoke", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.HasThis, variantMethod.ReturnType, GetParameterTypes(variantMethodParameters)); - ILGenerator proxyMethodIL = proxyMethod.GetILGenerator(); - - // Emit IL to load the proxy instance, then load the value of its duck delegate field - proxyMethodIL.Emit(OpCodes.Ldarg_0); - proxyMethodIL.Emit(OpCodes.Ldfld, duckDelegateField); - - // Emit IL to load method arguments - for (int i = 0; i < variantMethodParameters.Length; i++) - { - // Emit IL to load the argument - proxyMethodIL.Emit(OpCodes.Ldarg, i + 1); - - // Emit IL to cast the argument if necessary - DuckTyping.EmitCastIL(proxyMethodIL, duckMethodParameters[i].ParameterType, variantMethodParameters[i].ParameterType); - } - - // Emit IL to call the delegate - proxyMethodIL.Emit(OpCodes.Callvirt, duckMethod); - - // If we are returning something... - if (duckMethod.ReturnType != typeof(void)) - { - // Emit IL to cast the return value if necessary - DuckTyping.EmitCastIL(proxyMethodIL, variantMethod.ReturnType, duckMethod.ReturnType); - } - - // Emit IL to return. - proxyMethodIL.Emit(OpCodes.Ret); - - return proxyMethod; - } - - /// - /// Utility to create an array of parameter types from an array of parameters. - /// - /// Array of parameters to return the parameter types of. - /// An array of parameter types of the given method. - private Type[] GetParameterTypes(ParameterInfo[] parameters) - { - Type[] parameterTypes = new Type[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) - { - parameterTypes[i] = parameters[i].ParameterType; - } - - return parameterTypes; - } - } -} diff --git a/Source/DuckTyping/DuckProxyType.cs b/Source/DuckTyping/DuckProxyType.cs deleted file mode 100644 index f9816f7b..00000000 --- a/Source/DuckTyping/DuckProxyType.cs +++ /dev/null @@ -1,593 +0,0 @@ -// DuckProxyType.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net - - -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; -using System.Threading; - -namespace DeftTech.DuckTyping -{ - /// - /// Generic class that performs the generation of duck proxy types. - /// - /// Interface to cast to. - internal sealed class DuckProxyType - { - private delegate TInterface WrapDuckDelegate(object duck); - - private static Dictionary> s_ProxyTypes; - - /// - /// Initializes static members. - /// - static DuckProxyType() - { - s_ProxyTypes = new Dictionary>(); - } - - - private Type m_DuckType; - private Type m_ProxyType; - private WrapDuckDelegate m_WrapDuck; - - /// - /// Constructs an object. - /// - /// Type of object being casted. - private DuckProxyType(Type duckType) - { - m_DuckType = duckType; - m_ProxyType = null; - } - - - /// - /// If a proxy type has not been generated yet, generates a proxy type that implements - /// TInterface and forwards calls to an object of the type given in the constructor. - /// - private void GenerateProxyType() - { - if (m_ProxyType == null) - { - AppDomain domain = Thread.GetDomain(); - AssemblyBuilder assembly = domain.DefineDynamicAssembly(new AssemblyName("DuckProxy_" + typeof(TInterface).FullName.Replace(".", "_") + "_" + m_DuckType.FullName.Replace(".", "_")), AssemblyBuilderAccess.Run); - ModuleBuilder module = assembly.DefineDynamicModule("DuckProxy"); - - TypeBuilder proxyType = module.DefineType("DuckProxy"); - proxyType.AddInterfaceImplementation(typeof(IDuckProxy)); - proxyType.AddInterfaceImplementation(typeof(TInterface)); - - // Define private field to hold a reference to the duck object to forward calls to. - FieldBuilder duckField = proxyType.DefineField("m_Duck", m_DuckType, FieldAttributes.Private); - - // Define private .ctor(duckType duck) - ConstructorBuilder constructor = proxyType.DefineConstructor(MethodAttributes.Private, CallingConventions.HasThis, new Type[] { m_DuckType }); - ILGenerator constructorIL = constructor.GetILGenerator(); - constructorIL.Emit(OpCodes.Ldarg_0); - constructorIL.Emit(OpCodes.Ldarg_1); - constructorIL.Emit(OpCodes.Stfld, duckField); - constructorIL.Emit(OpCodes.Ret); - - // Define public static TInterface Wrap(object duck) - MethodBuilder wrapMethod = proxyType.DefineMethod("Wrap", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(TInterface), new Type[] { typeof(object) }); - ILGenerator wrapMethodIL = wrapMethod.GetILGenerator(); - wrapMethodIL.Emit(OpCodes.Ldarg_0); - if (m_DuckType.IsValueType) - { - wrapMethodIL.Emit(OpCodes.Box, m_DuckType); - } - wrapMethodIL.Emit(OpCodes.Newobj, constructor); - wrapMethodIL.Emit(OpCodes.Ret); - - // Define public object UnwrapDuck() - Implementation of IDuckProxy - MethodBuilder unwrapMethod = proxyType.DefineMethod("UnwrapDuck", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(object), new Type[0]); - ILGenerator unwrapMethodIL = unwrapMethod.GetILGenerator(); - unwrapMethodIL.Emit(OpCodes.Ldarg_0); - unwrapMethodIL.Emit(OpCodes.Ldfld, duckField); - if (m_DuckType.IsValueType) - { - unwrapMethodIL.Emit(OpCodes.Box, m_DuckType); - } - unwrapMethodIL.Emit(OpCodes.Ret); - - // Define all members of TInterface - MemberInfo[] members = typeof(TInterface).GetMembers(); - ProxyMemberDictionary proxyMembers = new ProxyMemberDictionary(); - foreach (MemberInfo member in members) - { - ImplementMember(proxyType, proxyMembers, duckField, member); - } - - m_ProxyType = proxyType.CreateType(); - m_WrapDuck = (WrapDuckDelegate)(Delegate.CreateDelegate(typeof(WrapDuckDelegate), m_ProxyType, wrapMethod.Name)); - } - } - - #region Proxy type member implementation methods - - /// - /// Implements an interface member in a duck proxy type using a given type builder. - /// If successful, the implemented member will be added to the given proxy member dictionary. - /// - /// Type builder for the duck proxy type. - /// Dictionary of members of the proxy type. - /// Field that holds a reference to the duck object to forward calls to. - /// The interface member to implement. - private void ImplementMember(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, MemberInfo interfaceMember) - { - if (interfaceMember.MemberType == MemberTypes.Method) - { - ImplementMethod(proxyType, proxyMembers, duckField, (MethodInfo)interfaceMember); - } - else if (interfaceMember.MemberType == MemberTypes.Property) - { - ImplementProperty(proxyType, proxyMembers, duckField, (PropertyInfo)interfaceMember); - } - else if (interfaceMember.MemberType == MemberTypes.Event) - { - ImplementEvent(proxyType, proxyMembers, duckField, (EventInfo)interfaceMember); - } - else - { - throw new NotSupportedException("Interface defines a member type that is not supported."); - } - } - - /// - /// Implements an interface method in a duck proxy type using a given type builder. - /// If successful, the implemented method will be added to the given proxy member dictionary. - /// - /// Type builder for the duck proxy type. - /// Dictionary of members of the proxy type. - /// Field that holds a reference to the duck object to forward calls to. - /// The interface method to implement. - private void ImplementMethod(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, MethodInfo interfaceMethod) - { - MethodInfo duckMethod = FindDuckMethod(interfaceMethod); - if (duckMethod == null) - { - throw new NotImplementedException("Duck type does not implement a method named \"" + interfaceMethod.Name + "\" with the same parameters and return type."); - } - - ParameterInfo[] interfaceMethodParameters = interfaceMethod.GetParameters(); - ParameterInfo[] duckMethodParameters = duckMethod.GetParameters(); - - MethodBuilder proxyMethod = proxyType.DefineMethod(interfaceMethod.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, interfaceMethod.ReturnType, GetParameterTypes(interfaceMethodParameters)); - ILGenerator proxyMethodIL = proxyMethod.GetILGenerator(); - - // Emit IL to load the proxy instance, then load the value of its duck field - proxyMethodIL.Emit(OpCodes.Ldarg_0); - proxyMethodIL.Emit(OpCodes.Ldfld, duckField); - - // Emit IL to load method arguments - for (int i = 0; i < interfaceMethodParameters.Length; i++) - { - // Emit IL to load the argument - proxyMethodIL.Emit(OpCodes.Ldarg, i + 1); - - // Emit IL to cast the argument if necessary - DuckTyping.EmitCastIL(proxyMethodIL, duckMethodParameters[i].ParameterType, interfaceMethodParameters[i].ParameterType); - } - - // Emit IL to call the method - proxyMethodIL.Emit(OpCodes.Callvirt, duckMethod); - - // If we are returning something... - if (duckMethod.ReturnType != typeof(void)) - { - // Emit IL to cast the return value if necessary - DuckTyping.EmitCastIL(proxyMethodIL, interfaceMethod.ReturnType, duckMethod.ReturnType); - } - - // Emit IL to return. - proxyMethodIL.Emit(OpCodes.Ret); - - // Add proxy method to proxy member dictionary - // (This is so that any associated properties or events can refer to it later) - proxyMembers.Add(duckMethod, proxyMethod); - } - - /// - /// Implements an interface property in a duck proxy type using a given type builder. - /// If successful, the implemented property will be added to the given proxy member dictionary. - /// - /// Type builder for the duck proxy type. - /// Dictionary of members of the proxy type. - /// Field that holds a reference to the duck object to forward calls to. - /// The interface property to implement. - private void ImplementProperty(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, PropertyInfo interfaceProperty) - { - PropertyInfo duckProperty = FindDuckProperty(interfaceProperty); - if (duckProperty == null) - { - throw new NotImplementedException("Duck type does not implement a property named \"" + interfaceProperty.Name + "\" with the same property type and index parameters."); - } - - PropertyBuilder proxyProperty = proxyType.DefineProperty(interfaceProperty.Name, PropertyAttributes.None, interfaceProperty.PropertyType, GetParameterTypes(interfaceProperty.GetIndexParameters())); - - // Get and set methods appear before associated properties in the member array; thus, they have - // already been defined and implemented. The proxy member dictionary is used to refer to these. - if (interfaceProperty.CanRead) - { - proxyProperty.SetGetMethod((MethodBuilder)(proxyMembers[duckProperty.GetGetMethod()])); - } - if (interfaceProperty.CanWrite) - { - proxyProperty.SetSetMethod((MethodBuilder)(proxyMembers[duckProperty.GetSetMethod()])); - } - - // Add proxy property to the proxy member dictionary - // (This is not really necessary, but good to keep things consistent) - proxyMembers.Add(duckProperty, proxyProperty); - } - - /// - /// Implements an interface event in a duck proxy type using a given type builder. - /// If successful, the implemented event will be added to the given proxy member dictionary. - /// - /// Type builder for the duck proxy type. - /// Dictionary of members of the proxy type. - /// Field that holds a reference to the duck object to forward calls to. - /// The interface event to implement. - private void ImplementEvent(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, EventInfo interfaceEvent) - { - EventInfo duckEvent = FindDuckEvent(interfaceEvent); - if (duckEvent == null) - { - throw new NotImplementedException("Duck type does not implement an event named \"" + interfaceEvent.Name + "\" with the same event handler type."); - } - - EventBuilder proxyEvent = proxyType.DefineEvent(interfaceEvent.Name, EventAttributes.None, interfaceEvent.EventHandlerType); - - // Associated methods appear before the event in the member array; thus, they have already been - // defined and implemented. The proxy member dictionary is used to refer to these. - proxyEvent.SetAddOnMethod((MethodBuilder)(proxyMembers[duckEvent.GetAddMethod()])); - proxyEvent.SetRemoveOnMethod((MethodBuilder)(proxyMembers[duckEvent.GetRemoveMethod()])); - proxyEvent.SetRaiseMethod((MethodBuilder)(proxyMembers[duckEvent.GetRaiseMethod()])); - MethodInfo[] otherDuckMethods = duckEvent.GetOtherMethods(); - foreach (MethodInfo otherDuckMethod in otherDuckMethods) - { - if (proxyMembers.ContainsKey(otherDuckMethod)) - { - proxyEvent.AddOtherMethod((MethodBuilder)(proxyMembers[otherDuckMethod])); - } - } - - // Add proxy event to the proxy member dictionary - // (This is not really necessary, but good to keep things consistent) - // proxyMembers.Add(duckEvent, proxyEvent); - // For some reason, EventBuilder does not inherit MemberInfo, so it cannot be added. - } - - #endregion - - #region Duck type implementation determination and search methods - - /// - /// Determines whether the type given in the constructor implements all the members of TInterface. - /// - /// If the type given in the constructor implements all the members of TInterface, true; otherwise, false. - private bool DuckImplementsAllMembers() - { - bool implementsAll = true; - - MemberInfo[] members = typeof(TInterface).GetMembers(); - foreach (MemberInfo member in members) - { - if (!DuckImplementsMember(member)) - { - implementsAll = false; - break; - } - } - - return implementsAll; - } - - /// - /// Determines whether the type given in the constructor implements a given interface member. - /// - /// - /// This method is used in determining whether an object of a given type can be duck typed to TInterface. - /// - /// Interface member to check for. - /// If the type given in the constructor implements the given interface member, true; otherwise, false. - private bool DuckImplementsMember(MemberInfo interfaceMember) - { - bool implementsMember; - - if (interfaceMember.MemberType == MemberTypes.Method) - { - implementsMember = (FindDuckMethod((MethodInfo)interfaceMember) != null); - } - else if (interfaceMember.MemberType == MemberTypes.Property) - { - implementsMember = (FindDuckProperty((PropertyInfo)interfaceMember) != null); - } - else - { - throw new NotSupportedException("Interface defines a member type that is not supported."); - } - - return implementsMember; - } - - /// - /// Finds the duck implementation for a given interface method. - /// - /// Interface method to find the implementation of. - /// If the type given in the constructor implements the given interface method, such method; otherwise, null. - private MethodInfo FindDuckMethod(MethodInfo interfaceMethod) - { - MethodInfo duckMethod = null; - - MethodInfo[] methods = m_DuckType.GetMethods(); - foreach (MethodInfo method in methods) - { - // Must be public and have the same name - if (method.IsPublic && method.Name == interfaceMethod.Name) - { - // Must have a compatible parameter list - ParameterInfo[] parameters = method.GetParameters(); - ParameterInfo[] interfaceMethodParameters = interfaceMethod.GetParameters(); - - if (parameters.Length == interfaceMethodParameters.Length) - { - bool areParameterTypesCompatible = true; - - for (int i = 0; i < parameters.Length; i++) - { - if (!DuckTyping.AreTypesCompatible(interfaceMethodParameters[i].ParameterType, parameters[i].ParameterType)) - { - areParameterTypesCompatible = false; - break; - } - } - - if (areParameterTypesCompatible) - { - // Must have a compatible return type - if (DuckTyping.AreTypesCompatible(interfaceMethod.ReturnType, method.ReturnType)) - { - duckMethod = method; - break; - } - } - } - } - } - - return duckMethod; - } - - /// - /// Finds the duck implementation for a given interface property. - /// - /// Interface property to find the implementation of. - /// If the type given in the constructor implements the given interface property, such property; otherwise, null. - private PropertyInfo FindDuckProperty(PropertyInfo interfaceProperty) - { - PropertyInfo duckProperty = null; - - PropertyInfo[] properties = m_DuckType.GetProperties(); - foreach (PropertyInfo property in properties) - { - // Must have the same name - if (property.Name == interfaceProperty.Name) - { - // Must have a compatible property type - if (DuckTyping.AreTypesCompatible(interfaceProperty.PropertyType, property.PropertyType)) - { - // Must implement the capabilities of the interface property - if ((property.CanRead || !interfaceProperty.CanRead) && (property.CanWrite || !interfaceProperty.CanWrite) - && (!interfaceProperty.CanRead || property.GetGetMethod().IsPublic) && (!interfaceProperty.CanWrite || property.GetSetMethod().IsPublic)) - { - // Must have a compatible index parameter list - ParameterInfo[] indexParameters = property.GetIndexParameters(); - ParameterInfo[] interfacePropertyIndexParameters = interfaceProperty.GetIndexParameters(); - - if (indexParameters.Length == interfacePropertyIndexParameters.Length) - { - bool areParameterTypesCompatible = true; - - for (int i = 0; i < indexParameters.Length; i++) - { - if (!DuckTyping.AreTypesCompatible(interfacePropertyIndexParameters[i].ParameterType, indexParameters[i].ParameterType)) - { - areParameterTypesCompatible = false; - break; - } - } - - if (areParameterTypesCompatible) - { - duckProperty = property; - break; - } - } - } - } - } - } - - return duckProperty; - } - - /// - /// Finds the duck implementation for a given interface event. - /// - /// Interface event to find the implementation of. - /// If the type given in the constructor implements the given interface event, such event; otherwise, null. - private EventInfo FindDuckEvent(EventInfo interfaceEvent) - { - EventInfo duckEvent = null; - - EventInfo[] events = m_DuckType.GetEvents(); - foreach (EventInfo _event in events) - { - // Must have the same name - if (_event.Name == interfaceEvent.Name) - { - // Must have a compatible event handler type - if (DuckTyping.AreTypesCompatible(interfaceEvent.EventHandlerType, duckEvent.EventHandlerType)) - { - duckEvent = _event; - break; - } - } - } - - return duckEvent; - } - - #endregion - - /// - /// Utility to create an array of parameter types from an array of parameters. - /// - /// Array of parameters to return the parameter types of. - /// An array of parameter types of the given method. - private Type[] GetParameterTypes(ParameterInfo[] parameters) - { - Type[] parameterTypes = new Type[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) - { - parameterTypes[i] = parameters[i].ParameterType; - } - - return parameterTypes; - } - - - /// - /// Calls the static WrapDuck method of the generated proxy type to initialize a new instance of - /// the proxy type with a given duck object. - /// - /// Duck object that the new proxy object should forward calls to. - /// - /// An instance of the proxy type, which implements TInterface, initialized with the given duck - /// object. - /// - internal static TInterface WrapDuck(object duck) - { - DuckProxyType proxyType = GetProxyType(duck.GetType()); - - // Make sure the proxy type is generated before we try to call it. - proxyType.GenerateProxyType(); - - // Call wrap method in the proxy type to create an instance of the proxy for the duck and return it. - return proxyType.m_WrapDuck(duck); - } - - internal static bool ShouldCast(Type duckType) - { - return (typeof(TInterface).IsInterface && !duckType.IsInterface); - } - - /// - /// Determines whether a given type can be duck casted to TInterface. - /// - /// Type of object being casted. - /// If the given type can be duck casted to TInterface, true; otherwise, false. - internal static bool CanCast(Type duckType) - { - return ShouldCast(duckType) && new DuckProxyType(duckType).DuckImplementsAllMembers(); - } - - /// - /// Prepares for a cast by generating the proxy type for a given duck type. - /// - /// Type of object being casted. - internal static void PrepareCast(Type duckType) - { - GetProxyType(duckType).GenerateProxyType(); - } - - /// - /// Gets the DuckProxyType<TInterface> object for a given duck type. - /// - /// - /// First, the most general type in the inheritance chain of the given type that implements all the - /// members of TInterface is selected. Then if a proxy type has already been generated, it returns - /// it. Finally, if not, it creates a new object and adds it to the dictionary. - /// - /// Type of object being casted. - /// The duck proxy type to use to cast or prepare for casting. - private static DuckProxyType GetProxyType(Type duckType) - { - DuckProxyType proxyType = null; - - // Examine the duck type's inheritance chain. - List inheritanceChain = new List(); - Type baseType = duckType; - do - { - inheritanceChain.Add(baseType); - baseType = baseType.BaseType; - } - while (baseType != null); - - // Iterate the inheritance chain backwards (most general to most specialized) to find a proxy type - // that has already been generated. - for (int i = inheritanceChain.Count - 1; i >= 0; i--) - { - Type type = inheritanceChain[i]; - - if (s_ProxyTypes.ContainsKey(type)) - { - proxyType = s_ProxyTypes[type]; - break; - } - } - - if (proxyType == null) - { - // Iterate the inheritance chain backwards (most general to most specialized) to find the most - // general type that can be casted to TInterface. - for (int i = inheritanceChain.Count - 1; i >= 0; i--) - { - Type type = inheritanceChain[i]; - - if (CanCast(type)) - { - proxyType = new DuckProxyType(type); - break; - } - } - - if (proxyType == null) - { - // If proxyType is null, then DuckTyping.CanCast<>() returned false for every type in the - // inheritance chain. The following line will cause an exception to be thrown when - // GenerateProxyType() is called. The exception will indicate why the duck cannot be casted. - proxyType = new DuckProxyType(duckType); - } - else - { - // If we got to this point, the cast is valid and it is ok to add the proxy type to the dictionary. - s_ProxyTypes.Add(proxyType.m_DuckType, proxyType); - } - } - - return proxyType; - } - - /// - /// Class for keeping track of member builders while implementing proxy members. - /// The key is the duck member that is called by the proxy. The value is the proxy member that calls it. - /// - /// - /// This was defined in case it is decided to define a custom class instead of using Dictionary. - /// - internal class ProxyMemberDictionary : Dictionary - { } - } -} diff --git a/Source/DuckTyping/DuckTyping.cs b/Source/DuckTyping/DuckTyping.cs deleted file mode 100644 index d4ccdcd7..00000000 --- a/Source/DuckTyping/DuckTyping.cs +++ /dev/null @@ -1,641 +0,0 @@ -// DuckTyping.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Reflection.Emit; -using System.Reflection; - -namespace DeftTech.DuckTyping -{ - /// - /// Class for casting objects using "duck typing". Casting will succeed if a given duck type implements - /// all the members of an interface even though it does not explicitly implement said interface at - /// compile time. Hence, implementation of interfaces is moved to runtime. Also supports delegate - /// casting. - /// - public static class DuckTyping - { - private static List s_DuckCasters; - private static List s_StaticDuckCasters; - - private static ToTypeFromTypeTree s_CanCastTree; - private static ToTypeFromTypeTree s_CanStaticCastTree; - - [ThreadStatic] private static Stack t_ToTypeStack; - [ThreadStatic] private static Stack t_FromTypeStack; - - /// - /// Initializes static members. - /// - static DuckTyping() - { - s_DuckCasters = new List(); - s_DuckCasters.Add(new InterfaceDuckCaster()); - s_DuckCasters.Add(new DelegateDuckCaster()); - s_DuckCasters.Add(new EnumStringDuckCaster()); - s_DuckCasters.Add(new EnumEnumDuckCaster()); - s_DuckCasters.Add(new GenericParameterDuckCaster()); - - s_StaticDuckCasters = new List(); - s_StaticDuckCasters.Add(new InterfaceStaticDuckCaster()); - - s_CanCastTree = new ToTypeFromTypeTree(); - - s_CanStaticCastTree = new ToTypeFromTypeTree(); - } - - private static Stack ToTypeStack - { - get - { - if (t_ToTypeStack == null) - { - t_ToTypeStack = new Stack(); - } - - return t_ToTypeStack; - } - } - - private static Stack FromTypeStack - { - get - { - if (t_FromTypeStack == null) - { - t_FromTypeStack = new Stack(); - } - - return t_FromTypeStack; - } - } - - /// - /// Casts an object using duck typing. - /// - /// - /// This method will use a normal cast if one is possible. - /// - /// Type to cast to. - /// Object to cast. - /// A T casting of the given duck object. - public static T Cast(object duck) - { - return (T)(Cast(typeof(T), duck)); - } - - /// - /// Casts an object using duck typing. - /// - /// - /// This method will use a normal cast if one is possible. - /// - /// Type to cast to. - /// Object to cast. - /// A casting of the given duck object to the given type. - public static object Cast(Type toType, object duck) - { - if (toType == null) throw new ArgumentNullException("toType"); - if (duck == null) return null; - - Type originalDuckType = duck.GetType(); - - duck = Uncast(duck); - - Type duckType = duck.GetType(); - - object proxy = null; - if (CanNormalCast(toType, duckType, false)) - { - proxy = duck; - } - else - { - bool casted = false; - foreach (IDuckCaster caster in s_DuckCasters) - { - if (caster.ShouldCast(toType, duckType)) - { - proxy = caster.Cast(toType, duck); - casted = true; - break; - } - } - - if (!casted) throw new InvalidCastException("A conversion from " + originalDuckType.FullName + " to " + toType.FullName + " does not exist."); - } - - return proxy; - } - - /// - /// Casts a static type to an object using duck typing. - /// - /// Type to cast to. - /// Static type to cast. - /// A casting of the given static type. - public static T StaticCast(Type staticType) - { - return (T)(StaticCast(typeof(T), staticType)); - } - - /// - /// Casts a static type to an object using duck typing. - /// - /// Type to cast to. - /// Static type to cast. - /// A casting of the given static type. - public static object StaticCast(Type toType, Type staticType) - { - object proxy = null; - - foreach (IStaticDuckCaster caster in s_StaticDuckCasters) - { - if (caster.ShouldStaticCast(toType, staticType)) - { - proxy = caster.StaticCast(toType, staticType); - break; - } - } - - if (proxy == null) throw new InvalidCastException("A conversion from static type " + staticType.FullName + " to an object of " + toType.FullName + " does not exist."); - - return proxy; - } - - /// - /// If the given object is a duck casted object, uncasts the object to retrieve the original duck object. - /// - /// Object that may be duck casted. - /// If the given object is duck casted, the original duck object; otherwise, the same object that was given. - public static object Uncast(object duck) - { - foreach (IDuckCaster caster in s_DuckCasters) - { - if (caster.ShouldUncast(duck)) - { - duck = caster.Uncast(duck); - } - } - - return duck; - } - - /// - /// Determines whether a given object can be casted to a given type. - /// - /// Type to cast to. - /// The object to cast. - /// If the given object can be casted to the given to type, true; otherwise, false. - public static bool CanCast(object duck) - { - return CanCast(typeof(T), duck.GetType()); - } - - /// - /// Determines whether a given object can be casted to a given type. - /// - /// Type to cast to. - /// The object to cast. - /// If the given object can be casted to the given to type, true; otherwise, false. - public static bool CanCast(Type toType, object duck) - { - return CanCast(toType, duck.GetType()); - } - - /// - /// Determines whether a type can be casted to another type. - /// - /// Type to cast to. - /// Type of object to be casted. - /// If an object of the given from type can be casted to the given to type, true; otherwise, false. - public static bool CanCast() - { - return CanCast(typeof(TTo), typeof(TFrom)); - } - - /// - /// Determines whether a type can be casted to type T. - /// - /// Type to cast to. - /// Type of object to be casted. - /// If an object of the given type can be casted to T, true; otherwise, false. - public static bool CanCast(Type duckType) - { - return CanCast(typeof(T), duckType); - } - - /// - /// Determines whether a type can be casted to another type. - /// - /// Type to cast to. - /// Type of object to be casted. - /// If an object of the given from type can be casted to the given to type, true; otherwise, false. - public static bool CanCast(Type toType, Type fromType) - { - bool canCast; - - FromTypeTree canCastFromTree; - if (s_CanCastTree.ContainsKey(toType)) - { - canCastFromTree = s_CanCastTree[toType]; - } - else - { - canCastFromTree = new FromTypeTree(); - s_CanCastTree.Add(toType, canCastFromTree); - } - - if (canCastFromTree.ContainsKey(fromType)) - { - canCast = canCastFromTree[fromType]; - } - else - { - canCast = CanNormalCast(toType, fromType, true); - if (!canCast) - { - foreach (IDuckCaster caster in s_DuckCasters) - { - if (caster.CanCast(toType, fromType)) - { - canCast = true; - break; - } - } - } - - if (!canCast) - { - foreach (IDuckCaster caster in s_DuckCasters) - { - if (caster.CouldUncast(toType, fromType)) - { - canCast = true; - break; - } - } - } - - canCastFromTree.Add(fromType, canCast); - } - - return canCast; - } - - /// - /// Determines whether a static type can be casted to another type. - /// - /// Type to cast to. - /// Static type to be casted. - /// If the given static type can be casted to the given to type, true; otherwise, false. - public static bool CanStaticCast(Type staticType) - { - return CanStaticCast(typeof(T), staticType); - } - - /// - /// Determines whether a static type can be casted to another type. - /// - /// Type to cast to. - /// Static type to be casted. - /// If the given static type can be casted to the given to type, true; otherwise, false. - public static bool CanStaticCast(Type toType, Type staticType) - { - bool canCast; - - FromTypeTree canCastFromTree; - if (s_CanStaticCastTree.ContainsKey(toType)) - { - canCastFromTree = s_CanStaticCastTree[toType]; - } - else - { - canCastFromTree = new FromTypeTree(); - s_CanStaticCastTree.Add(toType, canCastFromTree); - } - - if (canCastFromTree.ContainsKey(staticType)) - { - canCast = canCastFromTree[staticType]; - } - else - { - canCast = false; - foreach (IStaticDuckCaster caster in s_StaticDuckCasters) - { - if (caster.CanStaticCast(toType, staticType)) - { - canCast = true; - break; - } - } - - canCastFromTree.Add(staticType, canCast); - } - - return canCast; - } - - /// - /// Prepares for a duck casting operation by performing all the necessary overhead. - /// - /// Type to be casted to. - /// The object to be casted. - public static void PrepareCast(object duck) - { - PrepareCast(typeof(T), duck.GetType()); - } - - /// - /// Prepares for a duck casting operation by performing all the necessary overhead. - /// - /// Type to be casted to. - /// The object to be casted. - public static void PrepareCast(Type toType, object duck) - { - PrepareCast(toType, duck.GetType()); - } - - /// - /// Prepares for a duck casting operation by performing all the necessary overhead. - /// - /// Type to be casted to. - /// Type of object to be casted. - public static void PrepareCast() - { - PrepareCast(typeof(TTo), typeof(TFrom)); - } - - /// - /// Prepares for a duck casting operation by performing all the necessary overhead. - /// - /// Type to be casted to. - /// Type of object to be casted. - public static void PrepareCast(Type duckType) - { - PrepareCast(typeof(T), duckType); - } - - /// - /// Prepares for a duck casting operation by performing all the necessary overhead. - /// - /// Type to be casted to. - /// Type of object to be casted. - public static void PrepareCast(Type toType, Type fromType) - { - if (!CanNormalCast(toType, fromType, false)) - { - foreach (IDuckCaster caster in s_DuckCasters) - { - if (caster.ShouldCast(toType, fromType)) - { - caster.PrepareCast(toType, fromType); - break; - } - } - } - } - - /// - /// Prepares for a static duck casting operation by performing all the necessary overhead. - /// - /// Type to be casted to. - /// Static type to be casted. - public static void PrepareStaticCast(Type staticType) - { - PrepareStaticCast(typeof(T), staticType); - } - - /// - /// Prepares for a static duck casting operation by performing all the necessary overhead. - /// - /// Type to be casted to. - /// Static type to be casted. - public static void PrepareStaticCast(Type toType, Type staticType) - { - foreach (IStaticDuckCaster caster in s_StaticDuckCasters) - { - if (caster.ShouldStaticCast(toType, staticType)) - { - caster.PrepareStaticCast(toType, staticType); - break; - } - } - } - - /// - /// Determines whether two types are compatible. - /// - /// Type to be casted to. - /// Type of object to be casted. - /// Whether to also check the reverse of the given conversion when checking for an infinite loop. - /// If the given types are compatible, true; otherwise, false. - internal static bool AreTypesCompatible(Type toType, Type fromType, bool bidirectionalContext) - { - bool areCompatible = false; - - // This code prevents an infinite loop. ToTypeStack and FromTypeStack are thread static Type stacks that keep track of - // what conversions are currently being checked in this thread. If the given conversion is already being checked, we - // can assume that the types are compatible since they are as long as all the other members are. - - Type[] toTypes = ToTypeStack.ToArray(); - Type[] fromTypes = FromTypeStack.ToArray(); - - for (int i = 0; i < toTypes.Length; i++) - { - if ((toType == toTypes[i] && fromType == fromTypes[i]) || (bidirectionalContext && toType == fromTypes[i] && fromType == toTypes[i])) - { - areCompatible = true; - break; - } - } - - // If the current conversion is not in the stack, proceed with normal check: - if (!areCompatible) - { - ToTypeStack.Push(toType); - FromTypeStack.Push(fromType); - - areCompatible = CanCast(toType, fromType); - - ToTypeStack.Pop(); - FromTypeStack.Pop(); - } - - return areCompatible; - } - - /// - /// Determines whether two types are compatible. - /// - /// Type to be casted to. - /// Type of object to be casted. - /// If the given types are compatible, true; otherwise, false. - internal static bool AreTypesCompatible(Type toType, Type fromType) - { - return AreTypesCompatible(toType, fromType, false); - } - - /// - /// Assuming that the two types are compatible, scores how well the types match. 0 is the best - /// match, worse matches are less than 0. - /// - /// Type to be casted to. - /// Type of object to be casted. - /// A number indicating how well the given types match. 0 is the best match, worse matches are less than 0. - internal static int GetTypeCompatibilityScore(Type toType, Type fromType) - { - int score; - - if (toType == fromType) - { - // If the types are equal, the score is 0 (the best). - score = 0; - } - else - { - Type baseType = null; - Type subType = null; - - // Determine which type is the subclass of the other. - if (toType.IsSubclassOf(fromType)) - { - baseType = fromType; - subType = toType; - } - else if (fromType.IsSubclassOf(toType)) - { - baseType = toType; - subType = fromType; - } - - if (baseType != null && subType != null) - { - // If one of the types is the subclass of the other, the score is the additive inverse - // of how many levels down the inheritance chain the base class is. Thus, the closer - // the base class is to the subclass, the higher the score. - score = 0; - do - { - score--; - subType = subType.BaseType; - } - while (subType != baseType && subType != null); - - // I guess if one type is an interface that the other implements, IsSubclassOf will - // return true, but the interface will, of course, not be found in the type's - // inheritance chain. We want the score to be -1 in this case: - if (subType == null) - { - score = -1; - } - } - else - { - // If one of the types is not the subclass of the other, then, assuming that the types - // are compatible, they are so because one type is an interface that the other type - // implements or the types can be duck casted. In both cases, the inheritance is - // logically one level deep, so we will return a score of -1. - score = -1; - } - } - - return score; - } - - /// - /// Helps maintain consistent support for typing class members by centralizing the generation of IL - /// that converts types. Emits IL to perform whatever conversion is necessary from one type to - /// another. - /// - /// IL generator to use to emit the IL. - /// Type to be casted to. - /// Type of object to be casted. - internal static void EmitCastIL(ILGenerator il, Type toType, Type fromType) - { - if (toType != typeof(void) && fromType != typeof(void)) - { - if (CanNormalCast(toType, fromType, true)) - { - // Emit IL to box or unbox a value type if necessary - if (fromType.IsValueType && !toType.IsValueType) - { - il.Emit(OpCodes.Box, fromType); - } - else if (!fromType.IsValueType && toType.IsValueType) - { - il.Emit(OpCodes.Unbox_Any, toType); - } - - // Emit IL to cast the object if necessary - if (fromType != toType && !toType.IsValueType) - { - il.Emit(OpCodes.Castclass, toType); - } - } - else if (ShouldDuckCast(toType, fromType) || ShouldDuckCast(fromType, toType)) - { - // Emit IL to box a value type if necessary - if (fromType.IsValueType) - { - il.Emit(OpCodes.Box, fromType); - } - - // Emit IL to call the DuckTyping.Cast<> method - MethodInfo castMethod = typeof(DuckTyping).GetMethod("Cast", new Type[] { typeof(object) }).MakeGenericMethod(toType); - il.Emit(OpCodes.Call, castMethod); - } - else - { - // This exception should never be thrown - throw new NotImplementedException("EmitCastIL method does not know how to handle the type conversion. This exception indicates an inconsistency in the program; the AreTypesCompatible method accounts for a possible type conversion that has not been implemented in the EmitCastIL method."); - } - } - } - - /// - /// Determines whether a given type can be casted to another type using normal casting. - /// - /// Type to be casted to. - /// Type to be casted from. - /// If true, true will also be returned if normal casting is possible in reverse. - /// If the given type can normally be casted to the other given type, true; otherwise, false. - private static bool CanNormalCast(Type toType, Type fromType, bool bidirectional) - { - return toType == fromType || toType.IsAssignableFrom(fromType) || (bidirectional && fromType.IsAssignableFrom(toType)); - } - - /// - /// Determines whether duck typing should be used to perform the given cast. (As opposed to normal casting.) - /// - /// Type to be casted to. - /// Type of object being casted. - /// - private static bool ShouldDuckCast(Type toType, Type fromType) - { - bool shouldDuckCast = false; - foreach (IDuckCaster caster in s_DuckCasters) - { - if (caster.ShouldCast(toType, fromType)) - { - shouldDuckCast = true; - break; - } - } - - return shouldDuckCast; - } - } -} diff --git a/Source/DuckTyping/EnumStringDuckCaster.cs b/Source/DuckTyping/EnumStringDuckCaster.cs deleted file mode 100644 index 8659b79d..00000000 --- a/Source/DuckTyping/EnumStringDuckCaster.cs +++ /dev/null @@ -1,120 +0,0 @@ -// EnumStringDuckCaster.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Duck casting strategy used to cast between enums and strings. - /// - internal class EnumStringDuckCaster : IDuckCaster - { - /// - /// Constructs an object. - /// - public EnumStringDuckCaster() - { } - - public bool ShouldCast(Type toType, Type fromType) - { - return ((toType.IsSubclassOf(typeof(Enum)) && fromType == typeof(string)) || (toType == typeof(string) && fromType.IsSubclassOf(typeof(Enum)))); - } - - public bool ShouldUncast(object duck) - { - return false; - } - - public bool CanCast(Type toType, Type fromType) - { - return ShouldCast(toType, fromType); - } - - public bool CouldUncast(Type originalType, Type castedType) - { - return CanCast(castedType, originalType); - } - - public void PrepareCast(Type toType, Type fromType) - { } - - public object Cast(Type toType, object duck) - { - object casted; - - if (duck is string) - { - casted = Enum.Parse(toType, (string)duck); - } - else - { - casted = duck.ToString(); - } - - return casted; - } - - public object Uncast(object duck) - { - return duck; - } - } - - /// - /// Duck casting strategy used to cast between enums with the same name. - /// - internal class EnumEnumDuckCaster : IDuckCaster - { - /// - /// Constructs an object. - /// - public EnumEnumDuckCaster() - { } - - public bool ShouldCast(Type toType, Type fromType) - { - return toType.IsSubclassOf(typeof(Enum)) && fromType.IsSubclassOf(typeof(Enum)); - } - - public bool ShouldUncast(object duck) - { - return false; - } - - public bool CanCast(Type toType, Type fromType) - { - return ShouldCast(toType, fromType) && toType.FullName == fromType.FullName; - } - - public bool CouldUncast(Type originalType, Type castedType) - { - return CanCast(castedType, originalType); - } - - public void PrepareCast(Type toType, Type fromType) - { } - - public object Cast(Type toType, object duck) - { - return Enum.Parse(toType, duck.ToString()); - } - - public object Uncast(object duck) - { - return duck; - } - } -} diff --git a/Source/DuckTyping/FromTypeTree.cs b/Source/DuckTyping/FromTypeTree.cs deleted file mode 100644 index ed5e289c..00000000 --- a/Source/DuckTyping/FromTypeTree.cs +++ /dev/null @@ -1,66 +0,0 @@ -// FromTypeTree.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Tree to refer to T objects by from type. - /// - /// Type of object to store. - internal class FromTypeTree - { - private Dictionary m_FromTypeTree; - - /// - /// Constructs an object. - /// - public FromTypeTree() - { - m_FromTypeTree = new Dictionary(); - } - - /// - /// Determines whether a T object exists for the given from type. - /// - /// From type to search for. - /// If a T object exists for the given from type, true; otherwise, false. - public bool ContainsKey(Type fromType) - { - return m_FromTypeTree.ContainsKey(fromType); - } - - /// - /// Adds a T object to the tree. - /// - /// From type for the object. - /// The object to add. - public void Add(Type fromType, T item) - { - m_FromTypeTree.Add(fromType, item); - } - - /// - /// Gets the T object for a given from type. - /// - /// From type for the object. - /// The object for the given from type. - public T this[Type fromType] - { - get { return m_FromTypeTree[fromType]; } - } - } -} diff --git a/Source/DuckTyping/GenericParameterDuckCaster.cs b/Source/DuckTyping/GenericParameterDuckCaster.cs deleted file mode 100644 index 7fb2e384..00000000 --- a/Source/DuckTyping/GenericParameterDuckCaster.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - internal class GenericParameterDuckCaster : IDuckCaster - { - public GenericParameterDuckCaster() - { } - - public bool ShouldCast(Type toType, Type fromType) - { - return toType.IsGenericParameter && fromType.IsGenericParameter; - } - - public bool ShouldUncast(object duck) - { - return false; - } - - public bool CanCast(Type toType, Type fromType) - { - return ShouldCast(toType, fromType) && toType.GenericParameterPosition == fromType.GenericParameterPosition; - } - - public bool CouldUncast(Type originalType, Type castedType) - { - return false; - } - - public void PrepareCast(Type toType, Type fromType) - { } - - public object Cast(Type toType, object duck) - { - throw new InvalidOperationException("Cast should never be called since an object cannot exist of a type that is a generic parameter."); - } - - public object Uncast(object duck) - { - throw new InvalidOperationException("Uncast should never be called because ShouldUncast always returns false."); - } - } -} diff --git a/Source/DuckTyping/IDuckCaster.cs b/Source/DuckTyping/IDuckCaster.cs deleted file mode 100644 index 3e0c4580..00000000 --- a/Source/DuckTyping/IDuckCaster.cs +++ /dev/null @@ -1,87 +0,0 @@ -// IDuckCaster.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Interface for strategies of duck casting. - /// - internal interface IDuckCaster - { - /// - /// Determines whether this duck caster should be used to perform the given cast. - /// - /// Type to be casted to. - /// Type of object to be casted. - /// If this duck caster should be used to perform the given cast, true; otherwise, false. - bool ShouldCast(Type toType, Type fromType); - - /// - /// Determines whether this duck caster needs to perform an uncasting operation before the - /// duck is casted to another type. - /// - /// - /// If the given object is a proxy generated by this strategy, true should be returned. This - /// prevents proxies to proxies being generated. If true is returned here, DuckTyping will - /// respond by calling Uncast. - /// - /// The object to be casted. - /// If this duck caster needs to perform an uncasting operation before the duck is casted to another type, true; otherwise, false. - bool ShouldUncast(object duck); - - /// - /// Determines whether a given cast is valid. - /// - /// Type to be casted to. - /// The object to be casted. - /// If the given cast is valid, true; otherwise, false. - bool CanCast(Type toType, Type fromType); - - /// - /// Determines whether a given uncast operation might be possible without knowing the actual casted object. - /// - /// Original type to be uncasted to. - /// The type of the casted object. - /// If the given uncast operation might be possible, true; otherwise, false. - bool CouldUncast(Type originalType, Type castedType); - - /// - /// Prepares for a given cast by performing all necessary overhead. - /// - /// Type to be casted to. - /// The object to be casted. - void PrepareCast(Type toType, Type fromType); - - /// - /// Performs a given cast. - /// - /// Type to be casted to. - /// The object to cast. - /// The casted object. - object Cast(Type toType, object duck); - - /// - /// Uncasts a given object. - /// - /// - /// This method should only be called if ShouldUncast returns true. - /// - /// The object to uncast. - /// The uncasted object. - object Uncast(object duck); - } -} diff --git a/Source/DuckTyping/IDuckProxy.cs b/Source/DuckTyping/IDuckProxy.cs deleted file mode 100644 index 2a1df7eb..00000000 --- a/Source/DuckTyping/IDuckProxy.cs +++ /dev/null @@ -1,32 +0,0 @@ -// IDuckProxy.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// All duck proxy types implement this interface. This interface should normally not be used by code - /// outside the duck typing library. - /// - public interface IDuckProxy - { - /// - /// Returns the duck object that the proxy is forwarding calls to. - /// - /// The duck object that the proxy is forwarding calls to. - object UnwrapDuck(); - } -} diff --git a/Source/DuckTyping/IStaticDuckCaster.cs b/Source/DuckTyping/IStaticDuckCaster.cs deleted file mode 100644 index 501322ac..00000000 --- a/Source/DuckTyping/IStaticDuckCaster.cs +++ /dev/null @@ -1,56 +0,0 @@ -// IStaticDuckCaster.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Interface for strategies of duck casting a static type. - /// - internal interface IStaticDuckCaster - { - /// - /// Determines whether this duck caster should be used to perform the given static cast. - /// - /// Type to be casted to. - /// Static type to be casted. - /// If this duck caster should be used to perform the given static cast, true; otherwise, false. - bool ShouldStaticCast(Type toType, Type staticType); - - /// - /// Determines whether a given static cast is valid. - /// - /// Type to be casted to. - /// Static type to be casted. - /// If the given static cast is valid, true; otherwise, false. - bool CanStaticCast(Type toType, Type staticType); - - /// - /// Prepares for a given static cast by performing all necessary overhead. - /// - /// Type to be casted to. - /// Static type to be casted. - void PrepareStaticCast(Type toType, Type staticType); - - /// - /// Performs a given static cast. - /// - /// Type to be casted to. - /// Static type to cast. - /// The casted static type. - object StaticCast(Type toType, Type staticType); - } -} diff --git a/Source/DuckTyping/InterfaceDuckCaster.cs b/Source/DuckTyping/InterfaceDuckCaster.cs deleted file mode 100644 index 04d8352e..00000000 --- a/Source/DuckTyping/InterfaceDuckCaster.cs +++ /dev/null @@ -1,171 +0,0 @@ -// InterfaceDuckCaster.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Duck casting strategy that casts an object to an interface. - /// - internal class InterfaceDuckCaster : IDuckCaster - { - private static ToTypeFromTypeTree s_ProxyTypeTree; - - /// - /// Initializes static members. - /// - static InterfaceDuckCaster() - { - s_ProxyTypeTree = new ToTypeFromTypeTree(); - } - - - /// - /// Constructs an object. - /// - public InterfaceDuckCaster() - { } - - public bool ShouldCast(Type toType, Type fromType) - { - return (toType.IsInterface /*&& !fromType.IsInterface*/); - } - - public bool ShouldUncast(object duck) - { - return (duck is IDuckProxy); - } - - public bool CanCast(Type toType, Type fromType) - { - return ShouldCast(toType, fromType) && new InterfaceDuckProxyType(toType, fromType, false).CanProxy(); - } - - public bool CouldUncast(Type originalType, Type castedType) - { - return CanCast(castedType, originalType); - } - - public void PrepareCast(Type toType, Type fromType) - { - GetProxyType(toType, fromType).Prepare(); - } - - public object Cast(Type toType, object duck) - { - return GetProxyType(toType, duck.GetType()).WrapDuck(duck); - } - - public object Uncast(object duck) - { - object uncasted = ((IDuckProxy)duck).UnwrapDuck(); - - if (uncasted != null) duck = uncasted; - - return duck; - } - - /// - /// Gets the InterfaceDuckProxyType object for a given cast. - /// - /// - /// First, the most general type in the inheritance chain of the given type that implements all the - /// members of TInterface is selected. Then if a proxy type has already been generated, it returns - /// it. Finally, if not, it creates a new object and adds it to the dictionary. - /// - /// Type to cast to. - /// Type of object being casted. - /// The duck proxy type to use to cast or prepare for casting. - private InterfaceDuckProxyType GetProxyType(Type interfaceType, Type duckType) - { - InterfaceDuckProxyType proxyType = null; - - FromTypeTree fromTypeTree = null; - - // Examine the duck type's inheritance chain. - List inheritanceChain = new List(); - Type baseType = duckType; - do - { - inheritanceChain.Add(baseType); - baseType = baseType.BaseType; - } - while (baseType != null); - - // Include interfaces in the inheritance chain. We don't know which order, but it will still help. - /*Type[] interfaces = duckType.GetInterfaces(); - foreach (Type _interface in interfaces) - { - inheritanceChain.Add(_interface); - }*/ - - if (s_ProxyTypeTree.ContainsKey(interfaceType)) - { - fromTypeTree = s_ProxyTypeTree[interfaceType]; - - // Iterate the inheritance chain backwards (most general to most specialized) to find a proxy type - // that has already been generated. - for (int i = inheritanceChain.Count - 1; i >= 0; i--) - { - Type type = inheritanceChain[i]; - - if (fromTypeTree.ContainsKey(type)) - { - proxyType = fromTypeTree[type]; - break; - } - } - } - - if (proxyType == null) - { - // Iterate the inheritance chain backwards (most general to most specialized) to find the most - // general type that can be casted to TInterface. - for (int i = inheritanceChain.Count - 1; i >= 0; i--) - { - Type type = inheritanceChain[i]; - - if (CanCast(interfaceType, type)) - { - proxyType = new InterfaceDuckProxyType(interfaceType, type, false); - break; - } - } - - if (proxyType == null) - { - // If proxyType is null, then DuckTyping.CanCast<>() returned false for every type in the - // inheritance chain. The following line will cause an exception to be thrown when - // GenerateProxyType() is called. The exception will indicate why the duck cannot be casted. - proxyType = new InterfaceDuckProxyType(interfaceType, duckType, false); - } - else - { - // If we got to this point, the cast is valid and it is ok to add the proxy type to the tree. - if (fromTypeTree == null) - { - fromTypeTree = new FromTypeTree(); - s_ProxyTypeTree.Add(interfaceType, fromTypeTree); - } - - fromTypeTree.Add(proxyType.DuckType, proxyType); - } - } - - return proxyType; - } - } -} diff --git a/Source/DuckTyping/InterfaceDuckProxyType.cs b/Source/DuckTyping/InterfaceDuckProxyType.cs deleted file mode 100644 index 5711acce..00000000 --- a/Source/DuckTyping/InterfaceDuckProxyType.cs +++ /dev/null @@ -1,882 +0,0 @@ -// InterfaceDuckProxyType.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Reflection.Emit; -using System.Threading; - -namespace DeftTech.DuckTyping -{ - /// - /// Class that performs the generation of duck proxy types for the dynamic implementation of interfaces. - /// - internal sealed class InterfaceDuckProxyType - { - private delegate object WrapDuckDelegate(object duck); - - private Type m_InterfaceType; - private Type m_DuckType; - private bool m_IsDuckStatic; - - private Type m_ProxyType; - private WrapDuckDelegate m_WrapDuck; - private object m_StaticProxy; - - /// - /// Constructs an object. - /// - /// Type to be casted to. - /// Type of object to be casted. - /// Whether or not the duck is a static class instead of an object instance. - public InterfaceDuckProxyType(Type interfaceType, Type duckType, bool isDuckStatic) - { - m_InterfaceType = interfaceType; - m_DuckType = duckType; - m_IsDuckStatic = isDuckStatic; - - m_ProxyType = null; - } - - /// - /// Gets the interface type to be casted to. - /// - public Type InterfaceType { get { return m_InterfaceType; } } - - /// - /// Gets the duck type to be casted. - /// - public Type DuckType { get { return m_DuckType; } } - - /// - /// Gets whether the duck is a static class instead of an object instance. - /// - public bool IsDuckStatic { get { return m_IsDuckStatic; } } - - /// - /// Determines whether a proxy can be generated for the types given in the constructor. - /// - /// If a proxy can be generated for the types given in the constructor, true; otherwise, false. - public bool CanProxy() - { - return DuckImplementsAllMembers(); - } - - /// - /// Prepares by generating the proxy type for the types given in the constructor. - /// - public void Prepare() - { - GenerateProxyType(); - } - - /// - /// Calls the static WrapDuck method of the generated proxy type to initialize a new instance of - /// the proxy type with a given duck object. - /// - /// Duck object that the new proxy object should forward calls to. - /// - /// An instance of the proxy type, which implements the interface type given in the constructor, initialized with - /// the given duck object. - /// - public object WrapDuck(object duck) - { - if (m_IsDuckStatic) throw new InvalidOperationException("Cannot call WrapDuck when IsDuckStatic is true."); - - // Make sure the proxy type is generated before we try to call it. - GenerateProxyType(); - - // Call wrap method in the proxy type to create an instance of the proxy for the duck and return it. - return m_WrapDuck(duck); - } - - /// - /// Gets an instance of the proxy type that forwards calls to the static members of the duck type. - /// - /// - /// An instance of the proxy type, which implements the interface type given in the constructor and forwards calls to - /// the static members of the duck type. - /// - public object GetStaticProxy() - { - if (!m_IsDuckStatic) throw new InvalidOperationException("Cannot call GetStaticProxy when IsDuckStatic is false."); - - GenerateProxyType(); - - return m_StaticProxy; - } - - /// - /// If a proxy type has not been generated yet, generates a proxy type that implements - /// the interface type given in the constructor and forwards calls to an object of the - /// duck type given in the constructor. - /// - private void GenerateProxyType() - { - if (m_ProxyType == null) - { - AssemblyBuilderAccess assemblyBuilderAccess; - -#if !DEBUG - assemblyBuilderAccess = AssemblyBuilderAccess.Run; -#else - assemblyBuilderAccess = AssemblyBuilderAccess.RunAndSave; -#endif - - AppDomain domain = Thread.GetDomain(); - string assemblyName = "DuckInterfaceProxy_" + m_InterfaceType.Name.Replace(".", "_").Replace("+", "-") + "_" + m_DuckType.Name.Replace(".", "_").Replace("+", "-") + ".dll"; - AssemblyBuilder assembly = domain.DefineDynamicAssembly(new AssemblyName(assemblyName), assemblyBuilderAccess); - ModuleBuilder module = assembly.DefineDynamicModule(assemblyName); - - TypeBuilder proxyType = module.DefineType("DuckProxy"); - proxyType.AddInterfaceImplementation(typeof(IDuckProxy)); - proxyType.AddInterfaceImplementation(m_InterfaceType); - - FieldBuilder duckField; - - ConstructorBuilder constructor; - ILGenerator constructorIL; - - MethodBuilder wrapMethod; - - if (!m_IsDuckStatic) - { - // Define private field to hold a reference to the duck object to forward calls to. - duckField = proxyType.DefineField("m_Duck", m_DuckType, FieldAttributes.Private); - - // Define private .ctor(duckType duck) - constructor = proxyType.DefineConstructor(MethodAttributes.Private, CallingConventions.HasThis, new Type[] { m_DuckType }); - constructorIL = constructor.GetILGenerator(); - constructorIL.Emit(OpCodes.Ldarg_0); - constructorIL.Emit(OpCodes.Ldarg_1); - constructorIL.Emit(OpCodes.Stfld, duckField); - // constructorIL is passed to other methods so that they can add code to the constructor. - // constructorIL.Emit(OpCodes.Ret); - This code should appear below after other methods are called. - - // Define public static object Wrap(object duck) - wrapMethod = proxyType.DefineMethod("Wrap", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(object), new Type[] { typeof(object) }); - ILGenerator wrapMethodIL = wrapMethod.GetILGenerator(); - wrapMethodIL.Emit(OpCodes.Ldarg_0); - if (m_DuckType.IsValueType) - { - wrapMethodIL.Emit(OpCodes.Box, m_DuckType); - } - wrapMethodIL.Emit(OpCodes.Newobj, constructor); - wrapMethodIL.Emit(OpCodes.Ret); - - // Define public object UnwrapDuck() - Implementation of IDuckProxy - MethodBuilder unwrapMethod = proxyType.DefineMethod("UnwrapDuck", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(object), new Type[0]); - ILGenerator unwrapMethodIL = unwrapMethod.GetILGenerator(); - unwrapMethodIL.Emit(OpCodes.Ldarg_0); - unwrapMethodIL.Emit(OpCodes.Ldfld, duckField); - if (m_DuckType.IsValueType) - { - unwrapMethodIL.Emit(OpCodes.Box, m_DuckType); - } - unwrapMethodIL.Emit(OpCodes.Ret); - - // Override public bool Equals() method - MethodBuilder equalsMethod = proxyType.DefineMethod("Equals", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(bool), new Type[] { typeof(object) }); - MethodInfo objectEqualsMethod = typeof(object).GetMethod("Equals", new Type[] { typeof(object) }); - proxyType.DefineMethodOverride(equalsMethod, objectEqualsMethod); - ILGenerator equalsMethodIL = equalsMethod.GetILGenerator(); - equalsMethodIL.Emit(OpCodes.Ldarg_0); - equalsMethodIL.Emit(OpCodes.Ldfld, duckField); - equalsMethodIL.Emit(OpCodes.Ldarg_1); - equalsMethodIL.Emit(OpCodes.Call, typeof(DuckTyping).GetMethod("Uncast", new Type[] { typeof(object) })); - equalsMethodIL.Emit(OpCodes.Callvirt, objectEqualsMethod); - equalsMethodIL.Emit(OpCodes.Ret); - - // Override public int GetHashCode() method - MethodBuilder getHashCodeMethod = proxyType.DefineMethod("GetHashCode", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(int), new Type[0]); - MethodInfo objectGetHashCodeMethod = typeof(object).GetMethod("GetHashCode", new Type[0]); - proxyType.DefineMethodOverride(getHashCodeMethod, objectGetHashCodeMethod); - ILGenerator getHashCodeMethodIL = getHashCodeMethod.GetILGenerator(); - getHashCodeMethodIL.Emit(OpCodes.Ldarg_0); - getHashCodeMethodIL.Emit(OpCodes.Ldfld, duckField); - getHashCodeMethodIL.Emit(OpCodes.Callvirt, objectGetHashCodeMethod); - getHashCodeMethodIL.Emit(OpCodes.Ret); - - // Override string ToString() method - MethodBuilder toStringMethod = proxyType.DefineMethod("ToString", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(string), new Type[0]); - MethodInfo objectToStringMethod = typeof(object).GetMethod("ToString", new Type[0]); - proxyType.DefineMethodOverride(toStringMethod, objectToStringMethod); - ILGenerator toStringMethodIL = toStringMethod.GetILGenerator(); - toStringMethodIL.Emit(OpCodes.Ldarg_0); - toStringMethodIL.Emit(OpCodes.Ldfld, duckField); - toStringMethodIL.Emit(OpCodes.Callvirt, objectToStringMethod); - toStringMethodIL.Emit(OpCodes.Ret); - } - else - { - duckField = null; - - // Define public .ctor() - constructor = proxyType.DefineConstructor(MethodAttributes.Public, CallingConventions.HasThis, new Type[0]); - constructorIL = constructor.GetILGenerator(); - - wrapMethod = null; - - // Define public object UnwrapDuck() - Implementation of IDuckProxy - MethodBuilder unwrapMethod = proxyType.DefineMethod("UnwrapDuck", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(object), new Type[0]); - ILGenerator unwrapMethodIL = unwrapMethod.GetILGenerator(); - unwrapMethodIL.Emit(OpCodes.Ldnull); - unwrapMethodIL.Emit(OpCodes.Ret); - } - - // Define all members of m_InterfaceType - ProxyMemberDictionary proxyMembers = new ProxyMemberDictionary(); - ImplementInterface(proxyType, proxyMembers, duckField, constructorIL, m_InterfaceType); - - // Now that we are done implementing members, finish constructor method body: - constructorIL.Emit(OpCodes.Ret); - - // Bake it - m_ProxyType = proxyType.CreateType(); - - if (!m_IsDuckStatic) - { - m_WrapDuck = (WrapDuckDelegate)(Delegate.CreateDelegate(typeof(WrapDuckDelegate), m_ProxyType, wrapMethod.Name)); - } - else - { - m_StaticProxy = Activator.CreateInstance(m_ProxyType); - } - -//#if DEBUG - // If we're in debug mode, save the assembly so we can disassemble it if we want. - //assembly.Save(assemblyName); -//#endif - } - } - - #region Proxy type member implementation methods - - private void ImplementInterface(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, Type interfaceType) - { - ImplementInterface(proxyType, proxyMembers, duckField, constructorIL, interfaceType, new List()); - } - - private void ImplementInterface(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, Type interfaceType, List implementedInterfaces) - { - if (!implementedInterfaces.Contains(interfaceType)) - { - MemberInfo[] members = interfaceType.GetMembers(); - foreach (MemberInfo member in members) - { - ImplementMember(proxyType, proxyMembers, duckField, constructorIL, member); - } - - implementedInterfaces.Add(interfaceType); - - Type[] subInterfaces = interfaceType.GetInterfaces(); - foreach (Type subInterfaceType in subInterfaces) - { - ImplementInterface(proxyType, proxyMembers, duckField, constructorIL, subInterfaceType, implementedInterfaces); - } - } - } - - /// - /// Implements an interface member in a duck proxy type using a given type builder. - /// If successful, the implemented member will be added to the given proxy member dictionary. - /// - /// Type builder for the duck proxy type. - /// Dictionary of members of the proxy type. - /// Field that holds a reference to the duck object to forward calls to. - /// IL generator to use to add code to the constructor if necessary. - /// The interface member to implement. - private void ImplementMember(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, MemberInfo interfaceMember) - { - if (interfaceMember.MemberType == MemberTypes.Method) - { - ImplementMethod(proxyType, proxyMembers, duckField, constructorIL, (MethodInfo)interfaceMember); - } - else if (interfaceMember.MemberType == MemberTypes.Property) - { - ImplementProperty(proxyType, proxyMembers, duckField, constructorIL, (PropertyInfo)interfaceMember); - } - else if (interfaceMember.MemberType == MemberTypes.Event) - { - ImplementEvent(proxyType, proxyMembers, duckField, constructorIL, (EventInfo)interfaceMember); - } - else - { - throw new NotSupportedException("Interface defines a member type that is not supported."); - } - } - - /// - /// Implements an interface method in a duck proxy type using a given type builder. - /// If successful, the implemented method will be added to the given proxy member dictionary. - /// - /// Type builder for the duck proxy type. - /// Dictionary of members of the proxy type. - /// Field that holds a reference to the duck object to forward calls to. - /// IL generator to use to add code to the constructor if necessary. - /// The interface method to implement. - private void ImplementMethod(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, MethodInfo interfaceMethod) - { - MethodInfo duckMethod = FindDuckMethod(interfaceMethod); - if (duckMethod == null) - { - throw new NotImplementedException("Duck type does not implement a method named \"" + interfaceMethod.Name + "\" with compatible parameters and return type."); - } - - if (!duckMethod.IsSpecialName || (!duckMethod.Name.StartsWith("add_") && !duckMethod.Name.StartsWith("remove_"))) - { - ParameterInfo[] interfaceMethodParameters = interfaceMethod.GetParameters(); - ParameterInfo[] duckMethodParameters = duckMethod.GetParameters(); - - MethodBuilder proxyMethod = proxyType.DefineMethod(interfaceMethod.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, interfaceMethod.ReturnType, GetParameterTypes(interfaceMethodParameters)); - - GenericTypeParameterBuilder[] genericParameters; - - if (interfaceMethod.IsGenericMethodDefinition) - { - Type[] interfaceMethodGenericArguments = interfaceMethod.GetGenericArguments(); - string[] genericParameterNames = new string[interfaceMethodGenericArguments.Length]; - for (int i = 0; i < interfaceMethodGenericArguments.Length; i++) - { - genericParameterNames[i] = interfaceMethodGenericArguments[i].Name; - } - genericParameters = proxyMethod.DefineGenericParameters(genericParameterNames); - } - else - { - genericParameters = new GenericTypeParameterBuilder[0]; - } - - ILGenerator proxyMethodIL = proxyMethod.GetILGenerator(); - - // Emit IL to load the duck object if the method is not static - if (!duckMethod.IsStatic) - { - // Emit IL to load the proxy instance, then load the value of its duck field - proxyMethodIL.Emit(OpCodes.Ldarg_0); - proxyMethodIL.Emit(OpCodes.Ldfld, duckField); - } - - // Emit IL to load method arguments - for (int i = 0; i < interfaceMethodParameters.Length; i++) - { - // Emit IL to load the argument - proxyMethodIL.Emit(OpCodes.Ldarg, 1 + i); - - // Emit IL to cast the argument if necessary - DuckTyping.EmitCastIL(proxyMethodIL, duckMethodParameters[i].ParameterType, interfaceMethodParameters[i].ParameterType); - } - - MethodInfo methodToCall; - if (!duckMethod.IsGenericMethodDefinition) - { - methodToCall = duckMethod; - } - else - { - methodToCall = duckMethod.MakeGenericMethod((Type[])(genericParameters)); - } - - // Emit IL to call the method - if (!duckMethod.IsStatic) - { - proxyMethodIL.Emit(OpCodes.Callvirt, methodToCall); - } - else - { - proxyMethodIL.Emit(OpCodes.Call, methodToCall); - } - - // If we are returning something... - if (duckMethod.ReturnType != typeof(void)) - { - // Emit IL to cast the return value if necessary - DuckTyping.EmitCastIL(proxyMethodIL, interfaceMethod.ReturnType, duckMethod.ReturnType); - } - - // Emit IL to return. - proxyMethodIL.Emit(OpCodes.Ret); - - // Add proxy method to proxy member dictionary - // (This is so that any associated properties or events can refer to it later) - if (proxyMembers != null) proxyMembers[duckMethod] = proxyMethod; - } - } - - /// - /// Implements an interface property in a duck proxy type using a given type builder. - /// If successful, the implemented property will be added to the given proxy member dictionary. - /// - /// Type builder for the duck proxy type. - /// Dictionary of members of the proxy type. - /// Field that holds a reference to the duck object to forward calls to. - /// IL generator to use to add code to the constructor if necessary. - /// The interface property to implement. - private void ImplementProperty(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, PropertyInfo interfaceProperty) - { - PropertyInfo duckProperty = FindDuckProperty(interfaceProperty); - if (duckProperty == null) - { - throw new NotImplementedException("Duck type does not implement a property named \"" + interfaceProperty.Name + "\" with the same property type and index parameters."); - } - - PropertyBuilder proxyProperty = proxyType.DefineProperty(interfaceProperty.Name, PropertyAttributes.None, interfaceProperty.PropertyType, GetParameterTypes(interfaceProperty.GetIndexParameters())); - - // Get and set methods appear before associated properties in the member array; thus, they have - // already been defined and implemented. The proxy member dictionary is used to refer to these. - if (interfaceProperty.CanRead) - { - proxyProperty.SetGetMethod((MethodBuilder)(proxyMembers[duckProperty.GetGetMethod()])); - } - if (interfaceProperty.CanWrite) - { - proxyProperty.SetSetMethod((MethodBuilder)(proxyMembers[duckProperty.GetSetMethod()])); - } - - // Add proxy property to the proxy member dictionary - // (This is not really necessary, but good to keep things consistent) - if (proxyMembers != null) proxyMembers[duckProperty] = proxyProperty; - } - - /// - /// Implements an interface event in a duck proxy type using a given type builder. - /// If successful, the implemented event will be added to the given proxy member dictionary. - /// - /// Type builder for the duck proxy type. - /// Dictionary of members of the proxy type. - /// Field that holds a reference to the duck object to forward calls to. - /// IL generator to use to add code to the constructor if necessary. - /// The interface event to implement. - private void ImplementEvent(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, EventInfo interfaceEvent) - { - EventInfo duckEvent = FindDuckEvent(interfaceEvent); - if (duckEvent == null) - { - throw new NotImplementedException("Duck type does not implement an event named \"" + interfaceEvent.Name + "\" with the same event handler type."); - } - - EventBuilder proxyEvent = proxyType.DefineEvent(interfaceEvent.Name, EventAttributes.None, interfaceEvent.EventHandlerType); - - Type interfaceEventHandlerType = interfaceEvent.EventHandlerType; - Type duckEventHandlerType = duckEvent.EventHandlerType; - - // Define event add method - MethodBuilder addMethod = proxyType.DefineMethod("add_" + interfaceEvent.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(void), new Type[] { interfaceEventHandlerType }); - ILGenerator addMethodIL = addMethod.GetILGenerator(); - - // Define event remove method - MethodBuilder removeMethod = proxyType.DefineMethod("remove_" + interfaceEvent.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(void), new Type[] { interfaceEventHandlerType }); - ILGenerator removeMethodIL = removeMethod.GetILGenerator(); - - if (interfaceEventHandlerType == duckEventHandlerType) - { - // If the event handler types are the same, we can just forward calls to add and remove like normal. - - MethodInfo duckAddMethod = duckEvent.GetAddMethod(); - if (!duckAddMethod.IsStatic) - { - addMethodIL.Emit(OpCodes.Ldarg_0); - addMethodIL.Emit(OpCodes.Ldfld, duckField); - addMethodIL.Emit(OpCodes.Ldarg_1); - addMethodIL.Emit(OpCodes.Callvirt, duckAddMethod); - } - else - { - addMethodIL.Emit(OpCodes.Ldarg_1); - addMethodIL.Emit(OpCodes.Call, duckAddMethod); - } - - MethodInfo duckRemoveMethod = duckEvent.GetRemoveMethod(); - if (!duckRemoveMethod.IsStatic) - { - removeMethodIL.Emit(OpCodes.Ldarg_0); - removeMethodIL.Emit(OpCodes.Ldfld, duckField); - removeMethodIL.Emit(OpCodes.Ldarg_1); - removeMethodIL.Emit(OpCodes.Callvirt, duckRemoveMethod); - } - else - { - removeMethodIL.Emit(OpCodes.Ldarg_1); - removeMethodIL.Emit(OpCodes.Call, duckRemoveMethod); - } - } - else - { - // If the event handler types are different, we need some special behavior. Specifically, the event handlers - // added using the interface are kept as a seperate multicast delegate in the proxy class and an event handler - // is added to the duck that calls the proxy event delegate. - - // Define the event multicast delegate field - FieldBuilder eventHandlerField = proxyType.DefineField(interfaceEvent.Name, interfaceEventHandlerType, FieldAttributes.Private); - - // Implement the add method - addMethodIL.Emit(OpCodes.Ldarg_0); - addMethodIL.Emit(OpCodes.Ldarg_0); - addMethodIL.Emit(OpCodes.Ldfld, eventHandlerField); - addMethodIL.Emit(OpCodes.Ldarg_1); - addMethodIL.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Combine", new Type[] { typeof(Delegate), typeof(Delegate) })); - addMethodIL.Emit(OpCodes.Castclass, interfaceEventHandlerType); - addMethodIL.Emit(OpCodes.Stfld, eventHandlerField); - - // Implement the remove method - removeMethodIL.Emit(OpCodes.Ldarg_0); - removeMethodIL.Emit(OpCodes.Ldarg_0); - removeMethodIL.Emit(OpCodes.Ldfld, eventHandlerField); - removeMethodIL.Emit(OpCodes.Ldarg_1); - removeMethodIL.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Remove", new Type[] { typeof(Delegate), typeof(Delegate) })); - removeMethodIL.Emit(OpCodes.Castclass, interfaceEventHandlerType); - removeMethodIL.Emit(OpCodes.Stfld, eventHandlerField); - - // Set some local variables for later use... - MethodInfo interfaceInvokeMethod = interfaceEventHandlerType.GetMethod("Invoke"); - Type[] interfaceInvokeMethodParameterTypes = GetParameterTypes(interfaceInvokeMethod.GetParameters()); - - MethodInfo duckInvokeMethod = duckEventHandlerType.GetMethod("Invoke"); - Type[] duckInvokeMethodParameterTypes = GetParameterTypes(duckInvokeMethod.GetParameters()); - - - // Define the method that will serve as an event handler to the duck which will invoke the proxy's delegate - MethodBuilder proxyInvokeMethod = proxyType.DefineMethod("Invoke" + interfaceEvent.Name, MethodAttributes.Private | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, duckInvokeMethod.ReturnType, duckInvokeMethodParameterTypes); - ILGenerator proxyInvokeMethodIL = proxyInvokeMethod.GetILGenerator(); - - // First, check if the proxy event handler is null - Label ifEventHandlerIsNullLabel = proxyInvokeMethodIL.DefineLabel(); - proxyInvokeMethodIL.Emit(OpCodes.Ldarg_0); - proxyInvokeMethodIL.Emit(OpCodes.Ldfld, eventHandlerField); - proxyInvokeMethodIL.Emit(OpCodes.Ldnull); - proxyInvokeMethodIL.Emit(OpCodes.Ceq); - proxyInvokeMethodIL.Emit(OpCodes.Brtrue_S, ifEventHandlerIsNullLabel); - - // If the proxy event handler is not null, invoke it - proxyInvokeMethodIL.Emit(OpCodes.Ldarg_0); - proxyInvokeMethodIL.Emit(OpCodes.Ldfld, eventHandlerField); - for (int i = 0; i < interfaceInvokeMethodParameterTypes.Length; i++) - { - proxyInvokeMethodIL.Emit(OpCodes.Ldarg, i + 1); - DuckTyping.EmitCastIL(proxyInvokeMethodIL, interfaceInvokeMethodParameterTypes[i], duckInvokeMethodParameterTypes[i]); - } - proxyInvokeMethodIL.Emit(OpCodes.Callvirt, interfaceInvokeMethod); - - // If the proxy event handler is null, execution jumps here - proxyInvokeMethodIL.MarkLabel(ifEventHandlerIsNullLabel); - - // Return - proxyInvokeMethodIL.Emit(OpCodes.Ret); - - - // Add code to the constructor to add the event handler to the duck. - MethodInfo duckAddMethod = duckEvent.GetAddMethod(); - if (!duckAddMethod.IsStatic) - { - constructorIL.Emit(OpCodes.Ldarg_0); - constructorIL.Emit(OpCodes.Ldfld, duckField); - } - constructorIL.Emit(OpCodes.Ldarg_0); - constructorIL.Emit(OpCodes.Ldftn, proxyInvokeMethod); - constructorIL.Emit(OpCodes.Newobj, duckEventHandlerType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) })); - if (!duckAddMethod.IsStatic) - { - constructorIL.Emit(OpCodes.Callvirt, duckAddMethod); - } - else - { - constructorIL.Emit(OpCodes.Call, duckAddMethod); - } - } - - // Finish add method and set it for the event - addMethodIL.Emit(OpCodes.Ret); - proxyEvent.SetAddOnMethod(addMethod); - - // Finish remove method and set it for the event - removeMethodIL.Emit(OpCodes.Ret); - proxyEvent.SetRemoveOnMethod(removeMethod); - - // Other associated methods appear before the event in the member array; thus, they have already been - // defined and implemented. The proxy member dictionary is used to refer to these. - MethodInfo raiseMethod = duckEvent.GetRaiseMethod(); - if (raiseMethod != null) proxyEvent.SetRaiseMethod((MethodBuilder)(proxyMembers[raiseMethod])); - - MethodInfo[] otherDuckMethods = duckEvent.GetOtherMethods(); - foreach (MethodInfo otherDuckMethod in otherDuckMethods) - { - if (proxyMembers.ContainsKey(otherDuckMethod)) - { - proxyEvent.AddOtherMethod((MethodBuilder)(proxyMembers[otherDuckMethod])); - } - } - - // Add proxy event to the proxy member dictionary - // (This is not really necessary, but good to keep things consistent) - // if (proxyMembers != null) proxyMembers[duckEvent] = proxyEvent; - // For some reason, EventBuilder does not inherit MemberInfo, so it cannot be added. - } - - #endregion - - #region Duck type implementation determination and search methods - - /// - /// Determines whether the type given in the constructor implements all the members of TInterface. - /// - /// If the type given in the constructor implements all the members of TInterface, true; otherwise, false. - private bool DuckImplementsAllMembers() - { - bool implementsAll = true; - - MemberInfo[] members = m_InterfaceType.GetMembers(); - foreach (MemberInfo member in members) - { - if (!DuckImplementsMember(member)) - { - implementsAll = false; - break; - } - } - - return implementsAll; - } - - /// - /// Determines whether the type given in the constructor implements a given interface member. - /// - /// - /// This method is used in determining whether an object of a given type can be duck typed to TInterface. - /// - /// Interface member to check for. - /// If the type given in the constructor implements the given interface member, true; otherwise, false. - private bool DuckImplementsMember(MemberInfo interfaceMember) - { - bool implementsMember; - - if (interfaceMember.MemberType == MemberTypes.Method) - { - implementsMember = (FindDuckMethod((MethodInfo)interfaceMember) != null); - } - else if (interfaceMember.MemberType == MemberTypes.Property) - { - implementsMember = (FindDuckProperty((PropertyInfo)interfaceMember) != null); - } - else if (interfaceMember.MemberType == MemberTypes.Event) - { - implementsMember = (FindDuckEvent((EventInfo)interfaceMember) != null); - } - else - { - throw new NotSupportedException("Interface defines a member type that is not supported."); - } - - return implementsMember; - } - - /// - /// Finds the duck implementation for a given interface method. - /// - /// Interface method to find the implementation of. - /// If the type given in the constructor implements the given interface method, such method; otherwise, null. - private MethodInfo FindDuckMethod(MethodInfo interfaceMethod) - { - MethodInfo duckMethod = null; - int bestParameterCompatibilityScore = int.MinValue; - - MethodInfo[] methods = m_DuckType.GetMethods(); - foreach (MethodInfo method in methods) - { - // If casting a static class, must be static; if not, must not be static - if (method.IsStatic == m_IsDuckStatic) - { - // Must be public and have the same name - if (method.IsPublic && method.Name == interfaceMethod.Name) - { - // Must both be non-generic or both generic - if (method.IsGenericMethodDefinition == interfaceMethod.IsGenericMethodDefinition) - { - // Must have compatible generic arguments - bool genericArgumentsMatch = false; - - if (method.IsGenericMethodDefinition) - { - Type[] genericArguments = method.GetGenericArguments(); - Type[] interfaceGenericArguments = interfaceMethod.GetGenericArguments(); - - if (genericArguments.Length == interfaceGenericArguments.Length) - { - // TODO: Check generic argument constraints. - - genericArgumentsMatch = true; - } - } - else - { - genericArgumentsMatch = true; - } - - if (genericArgumentsMatch) - { - // Must have a compatible parameter list - int parameterCompatibilityScore = 0; - - ParameterInfo[] parameters = method.GetParameters(); - ParameterInfo[] interfaceMethodParameters = interfaceMethod.GetParameters(); - - if (parameters.Length == interfaceMethodParameters.Length) - { - bool areParameterTypesCompatible = true; - - for (int i = 0; i < parameters.Length; i++) - { - if (!DuckTyping.AreTypesCompatible(parameters[i].ParameterType, interfaceMethodParameters[i].ParameterType, true)) - { - areParameterTypesCompatible = false; - break; - } - - parameterCompatibilityScore += DuckTyping.GetTypeCompatibilityScore(parameters[i].ParameterType, interfaceMethodParameters[i].ParameterType); - } - - if (areParameterTypesCompatible) - { - // Must have a compatible return type - if (DuckTyping.AreTypesCompatible(interfaceMethod.ReturnType, method.ReturnType, true)) - { - parameterCompatibilityScore += DuckTyping.GetTypeCompatibilityScore(interfaceMethod.ReturnType, method.ReturnType); - - // Since there may be multiple method overloads that are compatible with the - // interface method, we want to choose the best one. - if (parameterCompatibilityScore > bestParameterCompatibilityScore) - { - duckMethod = method; - bestParameterCompatibilityScore = parameterCompatibilityScore; - } - } - } - } - } - } - } - } - } - - return duckMethod; - } - - /// - /// Finds the duck implementation for a given interface property. - /// - /// Interface property to find the implementation of. - /// If the type given in the constructor implements the given interface property, such property; otherwise, null. - private PropertyInfo FindDuckProperty(PropertyInfo interfaceProperty) - { - PropertyInfo duckProperty = null; - - PropertyInfo[] properties = m_DuckType.GetProperties(); - foreach (PropertyInfo property in properties) - { - // Must have the same name - if (property.Name == interfaceProperty.Name) - { - // Must have a compatible property type - if (DuckTyping.AreTypesCompatible(interfaceProperty.PropertyType, property.PropertyType)) - { - // Must implement the capabilities of the interface property - if ((property.CanRead || !interfaceProperty.CanRead) && (property.CanWrite || !interfaceProperty.CanWrite) - && (!interfaceProperty.CanRead || property.GetGetMethod().IsPublic) && (!interfaceProperty.CanWrite || property.GetSetMethod().IsPublic)) - { - // Must have a compatible index parameter list - ParameterInfo[] indexParameters = property.GetIndexParameters(); - ParameterInfo[] interfacePropertyIndexParameters = interfaceProperty.GetIndexParameters(); - - if (indexParameters.Length == interfacePropertyIndexParameters.Length) - { - bool areParameterTypesCompatible = true; - - for (int i = 0; i < indexParameters.Length; i++) - { - if (!DuckTyping.AreTypesCompatible(interfacePropertyIndexParameters[i].ParameterType, indexParameters[i].ParameterType)) - { - areParameterTypesCompatible = false; - break; - } - } - - if (areParameterTypesCompatible) - { - duckProperty = property; - break; - } - } - } - } - } - } - - return duckProperty; - } - - /// - /// Finds the duck implementation for a given interface event. - /// - /// Interface event to find the implementation of. - /// If the type given in the constructor implements the given interface event, such event; otherwise, null. - private EventInfo FindDuckEvent(EventInfo interfaceEvent) - { - EventInfo duckEvent = null; - - EventInfo[] events = m_DuckType.GetEvents(); - foreach (EventInfo _event in events) - { - // Must have the same name - if (_event.Name == interfaceEvent.Name) - { - // Must have a compatible event handler type - if (DuckTyping.AreTypesCompatible(interfaceEvent.EventHandlerType, _event.EventHandlerType)) - { - duckEvent = _event; - break; - } - } - } - - return duckEvent; - } - - #endregion - - /// - /// Utility to create an array of parameter types from an array of parameters. - /// - /// Array of parameters to return the parameter types of. - /// An array of parameter types of the given method. - private Type[] GetParameterTypes(ParameterInfo[] parameters) - { - Type[] parameterTypes = new Type[parameters.Length]; - for (int i = 0; i < parameters.Length; i++) - { - parameterTypes[i] = parameters[i].ParameterType; - } - - return parameterTypes; - } - - /// - /// Class for keeping track of member builders while implementing proxy members. - /// The key is the duck member that is called by the proxy. The value is the proxy member that calls it. - /// - /// - /// This was defined in case it is decided to change the class later. - /// - private class ProxyMemberDictionary : Dictionary - { } - } -} diff --git a/Source/DuckTyping/InterfaceStaticDuckCaster.cs b/Source/DuckTyping/InterfaceStaticDuckCaster.cs deleted file mode 100644 index 8d7540d3..00000000 --- a/Source/DuckTyping/InterfaceStaticDuckCaster.cs +++ /dev/null @@ -1,99 +0,0 @@ -// InterfaceStaticDuckCaster.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Static duck casting strategy that casts a static type to an interface. - /// - internal class InterfaceStaticDuckCaster : IStaticDuckCaster - { - private static ToTypeFromTypeTree s_ProxyTypeTree; - - /// - /// Initializes static members. - /// - static InterfaceStaticDuckCaster() - { - s_ProxyTypeTree = new ToTypeFromTypeTree(); - } - - /// - /// Constructs an object. - /// - public InterfaceStaticDuckCaster() - { } - - public bool ShouldStaticCast(Type toType, Type staticType) - { - return (toType.IsInterface && !staticType.IsInterface); - } - - public bool CanStaticCast(Type toType, Type staticType) - { - return ShouldStaticCast(toType, staticType) && new InterfaceDuckProxyType(toType, staticType, true).CanProxy(); - } - - public void PrepareStaticCast(Type toType, Type staticType) - { - GetProxyType(toType, staticType).Prepare(); - } - - public object StaticCast(Type toType, Type staticType) - { - return GetProxyType(toType, staticType).GetStaticProxy(); - } - - /// - /// Gets the InterfaceDuckProxyType object for a given cast. - /// - /// Type to cast to. - /// Static type being casted. - /// The duck proxy type to use to cast or prepare for casting. - private InterfaceDuckProxyType GetProxyType(Type interfaceType, Type duckType) - { - InterfaceDuckProxyType proxyType = null; - - FromTypeTree fromTypeTree = null; - - if (s_ProxyTypeTree.ContainsKey(interfaceType)) - { - fromTypeTree = s_ProxyTypeTree[interfaceType]; - - if (fromTypeTree.ContainsKey(duckType)) - { - proxyType = fromTypeTree[duckType]; - } - } - - if (proxyType == null) - { - proxyType = new InterfaceDuckProxyType(interfaceType, duckType, true); - - if (fromTypeTree == null) - { - fromTypeTree = new FromTypeTree(); - s_ProxyTypeTree.Add(interfaceType, fromTypeTree); - } - - fromTypeTree.Add(proxyType.DuckType, proxyType); - } - - return proxyType; - } - } -} diff --git a/Source/DuckTyping/ToTypeFromTypeTree.cs b/Source/DuckTyping/ToTypeFromTypeTree.cs deleted file mode 100644 index 64f308da..00000000 --- a/Source/DuckTyping/ToTypeFromTypeTree.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ToTypeFromTypeTree.cs -// -// Copyright (C) 2007 David Meyer -// All Rights Reserved -// -// Website: http://www.deftflux.net/ -// E-mail: deftflux@deftflux.net -// -// This source is licensed to the public via Artistic License 2.0 which should be found in a file -// named license.txt included with the package. It is also available online at: -// http://www.perlfoundation.org/artistic_license_2_0 - - -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - /// - /// Tree to refer to from type trees by to type. - /// - /// Type of object to store. - internal class ToTypeFromTypeTree - { - private Dictionary> m_ToTypeTree; - - /// - /// Constructs an object. - /// - public ToTypeFromTypeTree() - { - m_ToTypeTree = new Dictionary>(); - } - - /// - /// Determines whether a from type tree exists for the given from type. - /// - /// To type to search for. - /// If a from type tree exists for the given to type, true; otherwise, false. - public bool ContainsKey(Type toType) - { - return m_ToTypeTree.ContainsKey(toType); - } - - /// - /// Adds a from type tree to the to type tree. - /// - /// To type for the from type tree. - /// The from type tree to add. - public void Add(Type toType, FromTypeTree fromTypeTree) - { - m_ToTypeTree.Add(toType, fromTypeTree); - } - - /// - /// Gets the from type tree for a given to type. - /// - /// To type for the object. - /// The from type tree for the given from type. - public FromTypeTree this[Type toType] - { - get { return m_ToTypeTree[toType]; } - } - } -} diff --git a/Source/DuckTyping/TypeKey.cs b/Source/DuckTyping/TypeKey.cs deleted file mode 100644 index f731ead6..00000000 --- a/Source/DuckTyping/TypeKey.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DeftTech.DuckTyping -{ - internal sealed class TypeKey : IComparable - { - private Type m_Type; - - public TypeKey(Type type) - { - m_Type = type; - } - - public override string ToString() - { - return m_Type.ToString(); - } - - public int CompareTo(TypeKey other) - { - Type type1 = m_Type; - Type type2 = other.m_Type; - - int result; - - // If the types are equal, immediately return 0. - if (type1 == type2) - { - result = 0; - } - else - { - // At this point, we know that the types are not equal, so we need some way to compare them. - - // First, compare their GUIDs. This should work most of the time. - if ((result = type1.GUID.CompareTo(type2.GUID)) == 0) - { - // If the GUIDs are the same somehow, compare the assembly qualified name. This should also work most of the time. - if (type1.AssemblyQualifiedName != null && type2.AssemblyQualifiedName != null) - { - result = type1.AssemblyQualifiedName.CompareTo(type2.AssemblyQualifiedName); - } - - if (result == 0) - { - // The only known case where the GUID and assembly qualified name of two types are equal is when both types - // are generic parameters of the same name. So let's check for that: - if (type1.IsGenericParameter && type2.IsGenericParameter) - { - // If the generic parameters belong to generic methods... - if (type1.DeclaringMethod != null && type2.DeclaringMethod != null) - { - // Compare the types that define the generic methods: - if ((result = new TypeKey(type1.DeclaringMethod.DeclaringType).CompareTo(new TypeKey(type2.DeclaringMethod.DeclaringType))) == 0) - { - // If the generic methods are declared in the same type, compare the name of the generic methods: - if ((result = type1.DeclaringMethod.Name.CompareTo(type2.DeclaringMethod.Name)) == 0) - { - // If the generic parameters belong to the same generic method, compare their position in - // the list of generic parameters. - result = type1.GenericParameterPosition.CompareTo(type2.GenericParameterPosition); - } - } - } - // If the generic parameters belong to a generic type... - else if (type1.DeclaringType != null && type2.DeclaringType != null) - { - // Compare the generic types: - if ((result = new TypeKey(type1.DeclaringType).CompareTo(new TypeKey(type2.DeclaringType))) == 0) - { - // If they belong to the same generic type, compare their position in the list of generic - // parameters: - result = type1.GenericParameterPosition.CompareTo(type2.GenericParameterPosition); - } - } - } - - // If we still haven't distinguished the types, I suppose it's possible that they are two generic types, - // perhaps of the same generic type definition. - if (result == 0 && type1.IsGenericType && type2.IsGenericType) - { - Type[] type1GenericArguments = type1.GetGenericArguments(); - Type[] type2GenericArguments = type2.GetGenericArguments(); - - // Compare the length of the generic argument lists: - if ((result = type1GenericArguments.Length.CompareTo(type2GenericArguments.Length)) == 0) - { - // If the lists are the same length, compare each argument: - int i = 0; - do - { - result = new TypeKey(type1GenericArguments[i]).CompareTo(new TypeKey(type2GenericArguments[i])); - - i++; - } - while (i < type1GenericArguments.Length && result == 0); - } - } - - // If we still haven't distinguished the types, yet we know that they are not equal, we have no choice but - // to throw an exception. Theoretically, this should never happen, but in case it does, we'll know what - // the problem is and have some information to work with. - if (result == 0) - { - throw new AmbiguousTypesException(type1, type2); - } - } - } - } - - return result; - } - } -} diff --git a/Source/KSPAPIExtensions.csproj b/Source/KSPAPIEL.csproj similarity index 60% rename from Source/KSPAPIExtensions.csproj rename to Source/KSPAPIEL.csproj index 24258ee7..da6abe0b 100644 --- a/Source/KSPAPIExtensions.csproj +++ b/Source/KSPAPIEL.csproj @@ -1,5 +1,5 @@  - + Debug @@ -8,10 +8,10 @@ Library Properties KSPAPIExtensions - KSPAPIExtensions v3.5 512 + False true @@ -25,71 +25,31 @@ AnyCPU 1607 true + KSPAPIEL none true ..\..\Build\KSPAPIExtensions\obj\ - ..\ + . TRACE prompt 4 true + KSPAPIExtensions - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\Assembly-CSharp.dll - False - - - ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\Assembly-CSharp-firstpass.dll - False - - - - ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\UnityEngine.dll - False - + + + + @@ -106,4 +66,18 @@ --> + + + ..\..\_KSPAssemblies\Assembly-CSharp.dll + + + ..\..\_KSPAssemblies\System.dll + + + ..\..\_KSPAssemblies\Assembly-CSharp-firstpass.dll + + + ..\..\_KSPAssemblies\UnityEngine.dll + + \ No newline at end of file diff --git a/Source/KSPAPIExtensions.sln b/Source/KSPAPIEL.sln similarity index 83% rename from Source/KSPAPIExtensions.sln rename to Source/KSPAPIEL.sln index 0da812a3..300c066a 100644 --- a/Source/KSPAPIExtensions.sln +++ b/Source/KSPAPIEL.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 +# Visual Studio 2012 VisualStudioVersion = 12.0.21005.1 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KSPAPIExtensions", "KSPAPIExtensions.csproj", "{4282B0DA-C314-4524-9C32-EDAB2C9666A9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KSPAPIEL", "KSPAPIEL.csproj", "{4282B0DA-C314-4524-9C32-EDAB2C9666A9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Source/PartMessage/Attributes.cs b/Source/PartMessage/Attributes.cs deleted file mode 100644 index 4688e158..00000000 --- a/Source/PartMessage/Attributes.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using KSPAPIExtensions.DebuggingUtils; - -namespace KSPAPIExtensions.PartMessage -{ - /// - /// Apply this attribute to any method you wish to receive messages. - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class PartMessageListener : Attribute, IPartMessageListenerV1 - { - public PartMessageListener(Type delegateType, PartRelationship relations = PartRelationship.Self, GameSceneFilter scenes = GameSceneFilter.Any) - { - if (delegateType == null) - throw new ArgumentException("Message argument is null"); - if (!delegateType.IsSubclassOf(typeof(Delegate))) - throw new ArgumentException("Message is not a delegate type: " + typeof(Delegate).AssemblyQualifiedName + "\n" + Debugging.DumpTypeHeirachy(delegateType)); - if (delegateType.GetCustomAttributes(typeof(PartMessageDelegate), true).Length == 0) - throw new ArgumentException("Message does not have the PartMessageDelegate attribute"); - - DelegateType = delegateType; - Scenes = scenes; - Relations = relations; - } - - /// - /// The delegate type that we are listening for. - /// - public Type DelegateType { get; private set; } - - /// - /// Scene to listen for message in. Defaults to All. - /// - public GameSceneFilter Scenes { get; private set; } - - /// - /// Filter for relation between the sender and the reciever. - /// - public PartRelationship Relations { get; private set; } - } - - /// - /// Marker attribute to apply to events using part messages. - /// - [AttributeUsage(AttributeTargets.Event)] - public class PartMessageEvent : Attribute, IPartMessageEventV1 - { - /// - /// Constructor for attribute. - /// - /// If this is true then the event will be raised asynchonously. Any message filters will be called immediatly, - /// then the message will be delivered in the next update frame. - public PartMessageEvent(bool isAsync = false) - { - IsAsync = isAsync; - } - - /// - /// If this is true then the event will be raised asynchonously. Any message filters will be called immediatly, - /// then the message will be delivered in the next update frame. - /// - public bool IsAsync - { - get; - private set; - } - } - - /// - /// The attribute to be applied to a delegate to mark it as a PartMessageDelegate type. - /// - /// To use the message, define an event within a Part or PartModule that uses this delegate. - /// - [AttributeUsage(AttributeTargets.Delegate)] - public class PartMessageDelegate : Attribute, IPartMessageDelegateV1 - { - public PartMessageDelegate(Type parent = null, bool isAbstract = false) - { - if (parent != null) - { - if (!parent.IsSubclassOf(typeof(Delegate))) - throw new ArgumentException("Parent is not a delegate type"); - if (parent.GetCustomAttributes(typeof(PartMessageDelegate), true).Length != 1) - throw new ArgumentException("Parent does not have the PartMessageDelegate attribute"); - } - Parent = parent; - IsAbstract = isAbstract; - } - - /// - /// Often there is a heirachy of events - with more specific events and encompasing general events. - /// Define a general event as the parent in this instance and any listeners to the general event - /// will also be notified. Note that the arguments in this situation are expected to be a truncation - /// of the argument list for this event. - /// - public Type Parent { get; private set; } - - /// - /// This event is considered abstract - it should not be sent directly but should be sent from one of the child events. - /// - public bool IsAbstract { get; private set; } - } - - /// - /// If the message as defined in a is idempotent, or partially idempotent, then mark - /// any arguments or the whole delegate with this attribute. - /// - /// In essence: There's no point updating the part's mass three times in a row, you only need to know about the most recent. - /// If you're using then any messages with everything else the same except for the - /// mass will be consolidated together if the mass is marked with [UseLatest] in the delegate. - /// - /// Note that listeners do not need to use this attribute in their argument list. - /// - [AttributeUsage(AttributeTargets.Parameter)] - public class UseLatest : Attribute { } - - /// - /// Interface to implement on things that aren't either Parts or PartModules to enable them to send/recieve messages - /// using the event system as a proxy for an actual part. This interface is not required, however if not implemented - /// the part recievers will not be able to filter by source relationship. - /// You will need to call PartMessageService.Register(object) in the Awake method or constructor. - /// - public interface IPartMessagePartProxy - { - Part ProxyPart { get; } - } - - /// - /// A filter method for outgoing messages. This is called prior to delivery of any messages. If the method returns true - /// then the message is considered handled and will not be delivered. - /// - /// Information about the source of the message is avaiable from the ICurrentEventInfo as usual, this is passed as an argument for convenience. - /// - /// True if the message is considered handled and is not to be delivered. - public delegate bool PartMessageFilter(ICurrentEventInfo message); - -} diff --git a/Source/PartMessage/CommonMessages.cs b/Source/PartMessage/CommonMessages.cs deleted file mode 100644 index a2acd709..00000000 --- a/Source/PartMessage/CommonMessages.cs +++ /dev/null @@ -1,157 +0,0 @@ -using UnityEngine; - -namespace KSPAPIExtensions.PartMessage -{ - /// - /// Listen for this to get notification when any physical constant is changed - /// including the mass, CoM, moments of inertia, boyancy, ect. - /// - [PartMessageDelegate(isAbstract: true)] - public delegate void PartPhysicsChanged(); - - /// - /// Message for when the part's mass is modified. - /// - [PartMessageDelegate(typeof(PartPhysicsChanged))] - public delegate void PartMassChanged([UseLatest] float mass); - - /// - /// Message for when the part's CoMOffset changes. - /// - [PartMessageDelegate(typeof(PartPhysicsChanged))] - public delegate void PartCoMOffsetChanged([UseLatest] Vector3 offset); - - /// - /// Message for when the part's moments of intertia change. - /// - [PartMessageDelegate(typeof(PartPhysicsChanged))] - public delegate void PartMomentsChanged([UseLatest] Vector3 intertiaTensor, [UseLatest] Quaternion intertiaTensorRotation); - - /// - /// When the the volume of some space within the part changes, this message is raised - /// - /// The name of the area. This may be one of the names of the enum for a 'standard volume', or some custom value which obviously the sender and reciever will need to agree on. - /// The volume in cubic meters (kilolitres) - [PartMessageDelegate] - public delegate void PartVolumeChanged(string name, [UseLatest] float volume); - - /// - /// Well known volumes within a part. - /// - public enum PartVolumes - { - /// - /// Tankage - the volume devoted to storage of fuel, life support resources, ect - /// - Tankage, - /// - /// The volume devoted to habitable space. - /// - Habitable, - } - - /// - /// Abstract message - some change to an attach node has occured. - /// - [PartMessageDelegate(isAbstract: true)] - public delegate void PartAttachNodeChanged(AttachNode node); - - /// - /// Raised when the size of an attachment node is changed. - /// - /// The attachment node - /// The minimum diameter across the attachment area. For circular areas this will be the diameter - /// Area in square meters of the attachment. - [PartMessageDelegate(typeof(PartAttachNodeChanged))] - public delegate void PartAttachNodeSizeChanged(AttachNode node, [UseLatest] float minDia, [UseLatest] float area); - - /// - /// Location or orientation of the attachment node is changed - /// - /// The attachment node - [PartMessageDelegate(typeof(PartAttachNodeChanged))] - public delegate void PartAttachNodePositionChanged(AttachNode node, [UseLatest] Vector3 location, [UseLatest] Vector3 orientation, [UseLatest] Vector3 secondaryAxis); - - /// - /// Message for when the part's resource list is modified in some way. - /// - [PartMessageDelegate(isAbstract: true)] - public delegate void PartResourcesChanged(); - - /// - /// Message for when the part's resource list is modified (added to or subtracted from). - /// - [PartMessageDelegate(typeof(PartResourcesChanged))] - public delegate void PartResourceListChanged(); - - /// - /// Message for when the max amount of a resource is modified. - /// - [PartMessageDelegate(typeof(PartResourcesChanged))] - public delegate void PartResourceMaxAmountChanged(PartResource resource, [UseLatest] double maxAmount); - - /// - /// Message for when the initial amount of a resource is modified (only raised in the editor) - /// - [PartMessageDelegate(typeof(PartResourcesChanged))] - public delegate void PartResourceInitialAmountChanged(PartResource resource, [UseLatest] double amount); - - /// - /// Message for when some change has been made to the part's rendering model. - /// - [PartMessageDelegate] - public delegate void PartModelChanged(); - - /// - /// Message for when some change has been made to the part's collider. - /// - [PartMessageDelegate] - public delegate void PartColliderChanged(); - - /// - /// There has been some change to the part heirachy. - /// - [PartMessageDelegate(isAbstract:true)] - public delegate void PartHeirachyChanged(); - - /// - /// A root part has been selected in the VAB. - /// Note: this is automatically invoked by the framework, so you don't ever need to raise the event. - /// - [PartMessageDelegate(typeof(PartHeirachyChanged))] - public delegate void PartRootSelected(); - - /// - /// A root part has been removed in the VAB. - /// Note: this is automatically invoked by the framework, so you don't ever need to raise the event. - /// - [PartMessageDelegate(typeof(PartHeirachyChanged))] - public delegate void PartRootRemoved(); - - /// - /// The parent of this part has changed. Note: this is automatically invoked by the framework, so you don't ever need to raise the event. - /// - /// New parent, or null if it has been detached. - [PartMessageDelegate(typeof(PartHeirachyChanged))] - public delegate void PartParentChanged([UseLatest] Part parent); - - /// - /// New child has been attached to this part. Note: this is automatically invoked by the framework, so you don't ever need to raise the event. - /// - [PartMessageDelegate(typeof(PartHeirachyChanged))] - public delegate void PartChildAttached(Part child); - - /// - /// Child part has been detached from this part. Note: this is automatically invoked by the framework, so you don't ever need to raise the event. - /// - [PartMessageDelegate(typeof(PartHeirachyChanged))] - public delegate void PartChildDetached(Part child); - - - /// - /// There has been some change to the configuration for the engine. - /// - [PartMessageDelegate] - public delegate void PartEngineConfigChanged(); - -} diff --git a/Source/PartMessage/PartMessageService.cs b/Source/PartMessage/PartMessageService.cs deleted file mode 100644 index b64db488..00000000 --- a/Source/PartMessage/PartMessageService.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using DeftTech.DuckTyping; - -namespace KSPAPIExtensions.PartMessage -{ - /// - /// Interface for a part message once it is passed into the system. - /// - public interface IPartMessage : IEnumerable - { - /// - /// String name of the part message. This will be equal to the FullName attibute of the delegate type. - /// - string Name { get; } - - /// - /// Delegate type of the message. Note: do not rely on type equality with this attribute. This is because - /// the source may be using a different assembly to the target. - /// - Type DelegateType { get; } - - /// - /// Often there is a heirachy of events - with more specific events and encompasing general events. - /// Define a general event as the parent in this instance and any listeners to the general event - /// will also be notified. Note that the arguments in this situation are expected to be a truncation - /// of the argument list for this event. - /// - IPartMessage Parent { get; } - - /// - /// This event is considered abstract - it should not be sent directly but should be sent from one of the child events. - /// - bool IsAbstract { get; } - } - - /// - /// PartMessageListeners can use the properties in this class to examine details about the current message being - /// handled - /// - public interface ICurrentEventInfo : IEquatable - { - /// - /// The message - /// - IPartMessage Message { get; } - - /// - /// The source of the event - /// - object Source { get; } - - /// - /// The source part. This may be null if the source was not a Part or PartModule - /// - Part SourcePart { get; } - - /// - /// The source PartModule. This will be null if the source was not a PartModule - /// - PartModule SourceModule { get; } - - /// - /// The arguments to the current event. Treat as unmodifiable. - /// - object[] Arguments { get; } - - /// - /// The arguments that are used when determining event equality. This is any arguments not explicitly marked with - /// - IEnumerable IdentArguments { get; } - - /// - /// Find relationship between the message source and the specified part. - /// - /// The relationship. This will be PartRelationship.Unknown if the dest part is null or the source part is unknown. - PartRelationship SourceRelationTo(Part destPart); - } - - public interface IPartMessageService - { - /// - /// CurrentEventInfo for the current message being processed. This is used to get information about the current message source. - /// - ICurrentEventInfo CurrentEventInfo { get; } - - /// - /// Scan an object for events marked with and methods marked with and hook them up. - /// This is generally called either in the constructor, or in OnAwake for Part and PartModules. - /// Note that this method does not scan base classes for events and listeners, they need to be scanned explicitly. - /// - /// Object to register. If this is a Part, a PartModule, or a IPartMessagePartProxy the recieving part will be discovered. - void Register(T obj); - - /// - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// - /// Message type. This must be a delegate type marked with the PartMessageDelegate attribute. - /// Source of the message. If this is a Part, a PartModule, or a IPartMessagePartProxy the source part will be discovered. - /// message arguments - void Send(object source, params object[] args); - - /// - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// - /// The message delegate type. This must have the PartMessageDelegate attribute. - /// Source of the message. If this is a Part, a PartModule, or a IPartMessagePartProxy the source part will be discovered. - /// message arguments - void Send(Type message, object source, params object[] args); - - /// - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// This version allows the source to proxy for some other part. - /// - /// Message type. This must be a delegate type marked with the PartMessageDelegate attribute. - /// Source of the message. This may be any object. This variant does not do automatic part discovery. - /// Part that the message source is proxying for - /// message arguments - void SendProxy(object source, Part part, params object[] args); - - /// - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// This version allows the source to proxy for some other part. - /// - /// The message delegate type. This must have the PartMessageDelegate attribute. - /// Source of the message. This may be any object. This variant does not do automatic part discovery. - /// Part that the message source is proxying for - /// message arguments - void SendProxy(Type message, object source, Part part, params object[] args); - - /// - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// This version of the method will send the message asynchonously - the message will be delivered in the next update frame. Any message filters will - /// be invoked prior to returning. - /// - /// Message type. This must be a delegate type marked with the PartMessageDelegate attribute. - /// Source of the message. If this is a Part, a PartModule, or a IPartMessagePartProxy the source part will be discovered. - /// message arguments - void SendAsync(object source, params object[] args); - - /// - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// This version of the method will send the message asynchonously - the message will be delivered in the next update frame. Any message filters will - /// be invoked prior to returning. - /// - /// The message delegate type. This must have the PartMessageDelegate attribute. - /// Source of the message. If this is a Part, a PartModule, or a IPartMessagePartProxy the source part will be discovered. - /// message arguments - void SendAsync(Type message, object source, params object[] args); - - /// - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// This version of the method will send the message asynchonously - the message will be delivered in the next update frame. Any message filters will - /// be invoked prior to returning. - /// - /// Message type. This must be a delegate type marked with the PartMessageDelegate attribute. - /// Source of the message. This may be any object. This variant does not do automatic part discovery. - /// Part that the message source is proxying for - /// message arguments - void SendAsyncProxy(object source, Part part, params object[] args); - - /// - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// This version of the method will send the message asynchonously - the message will be delivered in the next update frame. Any message filters will - /// be invoked prior to returning. - /// - /// The message delegate type. This must have the PartMessageDelegate attribute. - /// Source of the message. This may be any object. This variant does not do automatic part discovery. - /// Part that the message source is proxying for - /// message arguments - void SendAsyncProxy(Type message, object source, Part part, params object[] args); - - /// - /// Register a message filter. This delegate will be called for every message sent from the source. - /// If it returns true, the message is considered handled and no futher processing will occour. - /// - /// The delegate for the filter - /// Message source, must match. If null will match all sources. - /// Source part to match. If null will match all parts. - /// Optional list of messages to match. If empty, all messages are matched. - /// Disposable object. When done call dispose. Works well with using clauses. - IDisposable Filter(PartMessageFilter filter, object source = null, Part part = null, params Type[] messages); - - /// - /// Consolidate messages. All messages sent by the source will be held until the returned object is destroyed. - /// Any duplicates of the same message and same arguments will be swallowed silently. - /// - /// source to consolidate from. Not specified or null will match all sources - /// Source part to match. If null will match all parts. - /// messages to consolidate. If not specified, all messages are consolidated. - /// Disposable object. When done call dispose. Works well with using clauses. - IDisposable Consolidate(object source = null, Part part = null, params Type[] messages); - - /// - /// Ignore messages sent by the source until the returned object is destroyed. - /// - /// Source to ignore. Null will ignore all sources. - /// Source part to match. If null will match all parts. - /// Messages to ignore. If not specified, all messages are ignored. - /// Disposable object. When done call dispose. Works well with using clauses. - IDisposable Ignore(object source = null, Part part = null, params Type[] messages); - - /// - /// Convert delegate type into the IPartMessage interface. - /// - /// Delegate type to convert. This must be a delegate type marked with the attribute. - IPartMessage AsIPartMessage(Type type); - - /// - /// Convert delegate type into the IPartMessage interface. - /// - /// Delegate type to convert. This must be a delegate type marked with the attribute. - IPartMessage AsIPartMessage(); - } - - /// - /// PartMessageService. This abstract base class primaraly implements the finder for the service (Instance) along with some static short-cut methods. - /// - static public class PartMessageService - { - internal static readonly string PartMessageServiceName = typeof(PartMessageService).FullName + ":SingletonInstance"; - // ReSharper disable once InconsistentNaming - internal static IPartMessageService _instance; - - /// - /// Get the instance of the PartMessageService. Note that this may be a duck-typed proxy. - /// - public static IPartMessageService Instance - { - get - { - if (_instance != null) - return _instance; - - GameObject serviceGo = GameObject.Find(PartMessageServiceName); - if (serviceGo == null) - throw new InvalidProgramException("PartMessageService has not been initialized."); - - foreach (Component comp in serviceGo.GetComponents()) - { - if (comp.GetType().FullName.StartsWith(typeof(ServiceImpl).FullName)) - return _instance = DuckTyping.Cast(comp); - } - - throw new InvalidProgramException("Unable to find a compatible part message service from updated assembly. Something has gone very wrong."); - } - internal set - { - _instance = value; - } - } - - /// - /// Convenience short-cut method for getting the current CurrentEventInfo. This interface allows message listeners to find information about the sender. - /// This object can be cached for later use, and will not update in future invocations. - /// - /// If there is no current invocation occouring. - public static ICurrentEventInfo MessageInfo - { - get { return Instance.CurrentEventInfo; } - } - - /// - /// Convenience short-cut . - /// Scan an object for events marked with and methods marked with and hook them up. - /// This is generally called either in the constructor, or in OnAwake for Part and PartModules. - /// Note that this method does not scan base classes for events and listeners, they need to be scanned explicitly. - /// - /// The type of the object to register. This can normally be inferred from the argument type. - /// Object to register. If this is a Part, a PartModule, or a IPartMessagePartProxy the recieving part will be discovered. - public static void Register(T obj) - { - Instance.Register(obj); - } - - /// - /// Convenience short-cut . - /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. - /// This version allows the source to proxy for some other part. - /// - /// The delegate type of the message to send. This must be a delegate, and must be marked with attribute. - /// Source of the message. This may be any object. This variant does not do automatic part discovery. - /// Part that the message source is proxying for - /// message arguments - public static void Send(object source, Part part, params object[] args) - { - Instance.SendProxy(source, part, args); - } - } -} diff --git a/Source/PartMessage/zzImplementation.cs b/Source/PartMessage/zzImplementation.cs deleted file mode 100644 index 2dcdaf6e..00000000 --- a/Source/PartMessage/zzImplementation.cs +++ /dev/null @@ -1,1192 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; -using System.Linq.Expressions; -using System.Collections; -using DeftTech.DuckTyping; - -namespace KSPAPIExtensions.PartMessage -{ - #region Duck Typing interfaces - /// - /// Interface to allow duck casting of message listeners. - /// - /// Do not change this interface or duck casting will fail. - /// - internal interface IPartMessageListenerV1 - { - Type DelegateType { get; } - - GameSceneFilter Scenes { get; } - - PartRelationship Relations { get; } - } - - /// - /// Interface to allow duck casting of messages. - /// - /// Do not change this interface or duck casting will fail. - /// - internal interface IPartMessageDelegateV1 - { - Type Parent { get; } - - bool IsAbstract { get; } - } - - /// - /// Interface to allow duck casting of messages. - /// - /// Do not change this interface or duck casting will fail. - /// - internal interface IPartMessageEventV1 - { - bool IsAsync { get; } - } - - #endregion - - #region Current Event Info - internal class CurrentEventInfoImpl : ICurrentEventInfo, IDisposable - { - #region Internal Bits - [ThreadStatic] - internal static CurrentEventInfoImpl current; - -#if DEBUG - private bool onStack; -#endif - private CurrentEventInfoImpl previous; - internal bool filterComplete; - - internal CurrentEventInfoImpl(IPartMessage message, object source, Part part, object[] args) - { - Source = source; - SourcePart = part; - Message = message; - Arguments = args; - - List identityArgs = new List(); - ParameterInfo [] paramInfos = message.DelegateType.GetMethod("Invoke").GetParameters(); - for(int i = 0; i < paramInfos.Length; ++i) - { - foreach(Attribute attr in paramInfos[i].GetCustomAttributes(false)) - if(attr.GetType().FullName == typeof(UseLatest).FullName) - goto foundAttr; - identityArgs.Add(args[i]); - foundAttr: - ; - } - IdentArguments = identityArgs.AsReadOnly(); - } - - internal IDisposable Push() - { -#if DEBUG - if(onStack) - throw new InvalidProgramException("Pushing message onto the stack when it's already on it"); - onStack = true; -#endif - - previous = current; - current = this; - return this; - } - - void IDisposable.Dispose() - { -#if DEBUG - if (!onStack) - throw new InvalidProgramException("Disposed called when not on the stack."); - onStack = false; -#endif - - current = previous; - previous = null; - } - -#if DEBUG - ~CurrentEventInfoImpl() - { - if (onStack) - { - Debug.LogError("CurrentEventInfoImpl somehow left on the call stack"); - } - - } -#endif - - #endregion - - #region Interface methods - - public IPartMessage Message { get; private set; } - - public object Source { get; private set; } - - public Part SourcePart { get; private set; } - - public object[] Arguments { get; private set; } - - public IEnumerable IdentArguments { get; private set; } - - public PartModule SourceModule { get { return Source as PartModule; } } - - public PartRelationship SourceRelationTo(Part destPart) - { - // ReSharper disable once InvokeAsExtensionMethod Don't because SourcePart may be null - return PartUtils.RelationTo(SourcePart, destPart); - } - - public override string ToString() - { - return string.Format("CurrentEventInfoImpl(Message:{0}, Source:{1}, SourcePart:{2}, Arguments.Length={3})", Message, Source, SourcePart, (Arguments == null) ? -1 : Arguments.Length); - } - - public bool Equals(ICurrentEventInfo other) - { - if (other == null) - return false; - if (ReferenceEquals(this, other)) - return true; - - if (GetHashCode() != other.GetHashCode()) - return false; - - if (Source != other.Source) - return false; - if (Message.Name != other.Message.Name) - return false; - if (!IdentArguments.SequenceEqual(other.IdentArguments)) - return false; - return true; - } - - public override bool Equals(object obj) - { - return Equals(obj as ICurrentEventInfo); - } - - private int hashCode; - - public override int GetHashCode() - { - // ReSharper disable NonReadonlyFieldInGetHashCode - if (hashCode != 0) - return hashCode; - - hashCode = - Source.GetHashCode() - ^ ((SourcePart == null) ? 0 : SourcePart.GetHashCode()) - ^ Message.Name.GetHashCode() - ^ Arguments.Length; - foreach (object arg in IdentArguments) - hashCode ^= (arg == null ? 0 : arg.GetHashCode()); - return hashCode; - // ReSharper restore NonReadonlyFieldInGetHashCode - } - - #endregion - - } - - #endregion - - #region Part Message - internal class MessageImpl : IPartMessage - { - private readonly IPartMessageDelegateV1 ifMsg; - internal MessageImpl parent; - - internal MessageImpl(ServiceImpl service, Type message) - { - if (!typeof(Delegate).IsAssignableFrom(message)) - throw new ArgumentException("Message type " + message + " is not a delegate type"); - - Attribute attribute; - foreach(Attribute attr in message.GetCustomAttributes(false)) - if (attr.GetType().FullName == typeof(PartMessageDelegate).FullName) - { - attribute = attr; - goto foundAttribute; - } - throw new ArgumentException("Message does not have the PartMessageDelegate attribute"); - - foundAttribute: - DelegateType = message; - - ifMsg = ServiceImpl.AsDelegate(attribute); - if(ifMsg.Parent != null) - parent = (MessageImpl)service.AsIPartMessage(ifMsg.Parent); - } - - public string Name - { - get { return DelegateType.FullName; } - } - - public Type DelegateType - { - get; - private set; - } - - public IPartMessage Parent - { - get { return parent; } - } - - public bool IsAbstract - { - get { return ifMsg.IsAbstract; } - } - - public IEnumerator GetEnumerator() - { - return new Enumerator - { - head = this - }; - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - private class Enumerator : IEnumerator - { - internal MessageImpl head; - private MessageImpl current; - private bool atEnd; - - public IPartMessage Current - { - get - { - if(head == null) - throw new InvalidOperationException("Iterator disposed"); - if (current == null) - throw new InvalidOperationException("Iterator is at " + (atEnd?"end":"start")); - return current; - } - } - - public void Dispose() - { - current = head = null; - atEnd = true; - } - - object IEnumerator.Current - { - get { return Current; } - } - - public bool MoveNext() - { - if (head == null) - throw new InvalidOperationException("Iterator disposed"); - if (atEnd) - throw new InvalidOperationException("Iterator is at end"); - - current = current == null ? head : current.parent; - - return !(atEnd = (current == null)); - } - - public void Reset() - { - current = null; - atEnd = false; - } - } - - public override string ToString() - { - return Name; - } - } - #endregion - - internal class ServiceImpl : MonoBehaviour, IPartMessageService - { - public ICurrentEventInfo CurrentEventInfo - { - get { - if (CurrentEventInfoImpl.current == null) - throw new InvalidOperationException("Cannot retrieve source info as not currently in invocation."); - - return CurrentEventInfoImpl.current; - } - } - - #region Registration - /// - /// Scan an object for message events and message listeners and hook them up. - /// Note that all references are dumped on game scene change, so objects must be rescanned when reloaded. - /// - /// the object to scan - public void Register(T obj) - { - Type objType = typeof(T); - - foreach (MethodInfo meth in objType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) - { - try - { - foreach (object attr in meth.GetCustomAttributes(true)) - if (attr.GetType().FullName == typeof (PartMessageListener).FullName) - AddListener(obj, meth, AsListener(attr)); - } - catch - { - Debug.LogError("Exception when attempting to register listener: " + (meth.DeclaringType != null ? meth.DeclaringType.AssemblyQualifiedName : "" ) + "." + meth.Name); - throw; - } - } - - foreach (EventInfo evt in objType.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) - { - bool async; - try - { - foreach (object attr in evt.GetCustomAttributes(true)) - if (attr.GetType().FullName == typeof(PartMessageEvent).FullName) - { - async = AsEvent(attr).IsAsync; - goto foundEvent; - } - } - catch - { - Debug.LogError("Exception when attempting to register event: " + (evt.DeclaringType != null ? evt.DeclaringType.AssemblyQualifiedName : "" ) + "." + evt.Name); - throw; - } - - continue; - - foundEvent: - Type deleg = evt.EventHandlerType; - - // sanity check - foreach (object attr in deleg.GetCustomAttributes(true)) - if (attr.GetType().FullName == typeof(PartMessageDelegate).FullName) - { - goto checkedDelegate; - } - - Debug.LogWarning(string.Format("[PartMessageService] Event: {0} in class: {1} declares an event with a part message, but does not have the PartMessageEvent attribute. Will ignore", evt.Name, objType.FullName)); - continue; - - checkedDelegate: - GenerateEventHandoff(async, obj, evt); - } - } - - internal static IPartMessageEventV1 AsEvent(object attr) - { - return DuckTyping.Cast(attr); - } - - internal static IPartMessageListenerV1 AsListener(object attr) - { - return DuckTyping.Cast(attr); - } - - internal static IPartMessageDelegateV1 AsDelegate(Attribute attribute) - { - return DuckTyping.Cast(attribute); - } - - #endregion - - #region Listeners and Event Delegates - - private Dictionary> listeners = new Dictionary>(); - - private class ListenerInfo - { - public WeakReference targetRef; - public MethodInfo method; - public IPartMessageListenerV1 attr; - - public object Target - { - get - { - return targetRef.Target; - } - } - - private Part TargetPart - { - get - { - object target = Target; - return AsPart(target); - } - } - - public bool CheckPrereq(ICurrentEventInfo info) - { - if (!attr.Scenes.IsLoaded()) - return false; - // ReSharper disable once InvokeAsExtensionMethod SourcePart can be null - if (!PartUtils.RelationTest(info.SourcePart, TargetPart, attr.Relations)) - return false; - return true; - } - } - - private void AddListener(object target, MethodInfo meth, IPartMessageListenerV1 attr) - { - if (!attr.Scenes.IsLoaded()) - return; - - if (Delegate.CreateDelegate(attr.DelegateType, target, meth, false) == null) - { - Debug.LogError(string.Format("PartMessageListener method {0}.{1} does not support the delegate type {2} as declared in the attribute", meth.DeclaringType, meth.Name, attr.DelegateType.FullName)); - return; - } - - string message = attr.DelegateType.FullName; - - LinkedList listenerList; - if (!listeners.TryGetValue(message, out listenerList)) - { - listenerList = new LinkedList(); - listeners.Add(message, listenerList); - } - - listenerList.AddLast(new ListenerInfo - { - targetRef = new WeakReference(target), - method = meth, - attr = attr - }); - } - - private static readonly MethodInfo HandoffSend = typeof(ServiceImpl).GetMethod("SendProxy", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(Type), typeof(object), typeof(Part), typeof(object[]) }, null); - private static readonly MethodInfo HandoffSendAsync = typeof(ServiceImpl).GetMethod("SendAsyncProxy", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(Type), typeof(object), typeof(Part), typeof(object[]) }, null); - - private void GenerateEventHandoff(bool async, object source, EventInfo evt) - { - Part part = AsPart(source); - - // This generates a dynamic method that pulls the properties of the event - // plus the arguments passed and hands it off to the EventHandler method below. - Type message = evt.EventHandlerType; - MethodInfo m = message.GetMethod("Invoke", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - - ParameterInfo[] pLst = m.GetParameters(); - ParameterExpression[] peLst = new ParameterExpression[pLst.Length]; - Expression[] cvrt = new Expression[pLst.Length]; - for (int i = 0; i < pLst.Length; i++) - { - peLst[i] = Expression.Parameter(pLst[i].ParameterType, pLst[i].Name); - cvrt[i] = Expression.Convert(peLst[i], typeof(object)); - } - Expression createArr = Expression.NewArrayInit(typeof(object), cvrt); - - Expression invoke = Expression.Call(Expression.Constant(this), async?HandoffSendAsync:HandoffSend, - Expression.Constant(message), Expression.Constant(source), Expression.Constant(part), createArr); - - Delegate d = Expression.Lambda(message, invoke, peLst).Compile(); - - // Shouldn't need to use a weak delegate here. - evt.AddEventHandler(source, d); - } - - #endregion - - #region Message delivery - - private readonly LinkedList asyncMessages = new LinkedList(); - - public void Send(object source, params object[] args) - { - SendProxy(typeof(T), source, AsPart(source), args); - } - - public void Send(Type message, object source, params object[] args) - { - SendProxy(message, source, AsPart(source), args); - } - - public void SendProxy(object source, Part part, params object[] args) - { - SendProxy(typeof(T), source, part, args); - } - - public void SendProxy(Type messageCls, object source, Part part, params object[] args) - { - IPartMessage message = AsIPartMessage(messageCls); - CurrentEventInfoImpl info = new CurrentEventInfoImpl(message, source, part, args); - Send(info); - } - - public void SendAsync(object source, params object[] args) - { - SendAsyncProxy(typeof(T), source, AsPart(source), args); - } - - public void SendAsync(Type message, object source, params object[] args) - { - SendAsyncProxy(message, source, AsPart(source), args); - } - - public void SendAsyncProxy(object source, Part part, params object[] args) - { - SendAsyncProxy(typeof(T), source, part, args); - } - - public void SendAsyncProxy(Type messageCls, object source, Part part, params object[] args) - { - CurrentEventInfoImpl message = new CurrentEventInfoImpl(AsIPartMessage(messageCls), source, part, args); - - // Eat duplicate messages, just send the last one. - var node = asyncMessages.First; - while (node != null) - { - if (node.Value.Equals(message)) - { - var delete = node; - node = node.Next; - asyncMessages.Remove(delete); - } - else - node = node.Next; - } - - using (message.Push()) - { - if (filters != null && filters.Any(info => info.CheckPrereq(message) && info.Filter(message))) - return; - } - message.filterComplete = true; - - asyncMessages.AddLast(message); - } - - public void Update() - { - while (asyncMessages.Count > 0) - { - CurrentEventInfoImpl message = asyncMessages.First.Value; - asyncMessages.RemoveFirst(); - try - { - Send(message); - } - catch(Exception ex) - { - Debug.LogException(ex); - } - } - - if (HighLogic.LoadedSceneIsEditor) - UpdatePartHeirachy(); - } - - internal void Send(CurrentEventInfoImpl message) - { - if (!gameObject) - return; - - using (message.Push()) - { - if (!message.filterComplete && filters != null) - if (filters.Any(info => info.CheckPrereq(message) && info.Filter(message))) - return; - - // Send the message - foreach (IPartMessage currMessage in message.Message) - { - string messageName = currMessage.Name; - - LinkedList listenerList; - if (!listeners.TryGetValue(messageName, out listenerList)) - continue; - - // Shorten parameter list if required - object[] newArgs = null; - - for (var node = listenerList.First; node != null; ) - { - // hold reference for duration of call - ListenerInfo info = node.Value; - object target = info.Target; - if (target == null) - { - // Remove dead links from the list - var tmp = node; - node = node.Next; - listenerList.Remove(tmp); - continue; - } - - // Declarative event filtering - if (!info.CheckPrereq(message)) - { - node = node.Next; - continue; - } - - - if (newArgs == null) - newArgs = ShortenArgs(message.Arguments, currMessage.DelegateType); - - try - { - node.Value.method.Invoke(target, newArgs); - } - catch (TargetException ex) - { - // Swallow target exceptions, but not anything else. - Debug.LogError(string.Format("Invoking {0}.{1} to handle DelegateType {2} resulted in an exception.", target.GetType(), node.Value.method, CurrentEventInfo.Message)); - Debug.LogException(ex.InnerException); - } - - node = node.Next; - } - - } - } - } - - private static object[] ShortenArgs(object[] args, Type messageCls) - { - ParameterInfo[] methodParams = messageCls.GetMethod("Invoke").GetParameters(); - object[] newArgs = args; - if (args.Length > methodParams.Length) - { - newArgs = new object[methodParams.Length]; - Array.Copy(args, newArgs, methodParams.Length); - } - return newArgs; - } - #endregion - - #region Message Filters - - // Store the list of current filters in a thread static - [ThreadStatic] - private static LinkedList filters; - - /// - /// Register a message filter. This delegate will be called for every message sent from the source. - /// If it returns true, the message is considered handled and no futher processing will occour. - /// - /// The delegate for the filter - /// Message source, must match. If null will match all sources. - /// Part to filter. If null will match all parts. - /// Optional list of messages to match. If empty, all messages are matched. - /// Disposable object. When done call dispose. Works well with using clauses. - public IDisposable Filter(PartMessageFilter filter, object source = null, Part part = null, params Type[] messages) - { - FilterInfo info = new FilterInfo - { - Filter = filter - }; - - RegisterFilterInfo(source, part, messages, info); - - return info; - } - - /// - /// Consolidate messages. All messages sent by the source will be held until the returned object is destroyed. - /// Any duplicates of the same message will be swallowed silently. - /// - /// source to consolidate from. Null will match all sources - /// Part to filter. If null will match all parts. - /// messages to consolidate. If not specified, all messages are consolidated. - /// Disposable object. When done call dispose. Works well with using clauses. - public IDisposable Consolidate(object source = null, Part part = null, params Type[] messages) - { - FilterInfo consolidator = new MessageConsolidator(); - - RegisterFilterInfo(source, part, messages, consolidator); - - return consolidator; - } - - /// - /// Ignore messages sent by the source until the returned object is destroyed. - /// - /// Source to ignore. Null will ignore all sources. - /// Part to filter. If null will match all parts. - /// Messages to ignore. If not specified, all messages are ignored. - /// Disposable object. When done call dispose. Works well with using clauses. - public IDisposable Ignore(object source = null, Part part = null, params Type[] messages) - { - return Filter(message => true, source, part, messages); - } - - private void RegisterFilterInfo(object source, Part part, Type[] messages, FilterInfo info) - { - info.source = source; - info.part = part; - info.service = this; - - // ReSharper disable once ForCanBeConvertedToForeach - for (int i = 0; i < messages.Length; i++) - info.messages.Add(messages[i].FullName); - - if(filters == null) - filters = new LinkedList(); - - info.node = filters.AddFirst(info); - } - - internal class FilterInfo : IDisposable - { - public object source; - public Part part; - public PartMessageFilter Filter; - public HashSet messages = new HashSet(); - - public ServiceImpl service; - - public LinkedListNode node; - - public bool CheckPrereq(ICurrentEventInfo info) - { - if (source != null && source != info.Source) - return false; - if (part != null && part != info.SourcePart) - return false; - if (messages.Count == 0) - return true; - - return info.Message.Any(message => !messages.Contains(message.Name)); - } - - public virtual void Dispose() - { - if(service == null) - throw new InvalidOperationException("Already disposed"); - - filters.Remove(node); - if (filters.Count == 0) - filters = null; - service = null; - } - - ~FilterInfo() - { - if (service == null) - return; - Dispose(); - Debug.LogError("Warning: Filter has been created and not disposed prior to finalization. Please check the code."); - } - } - - internal class MessageConsolidator : FilterInfo - { - public MessageConsolidator() - { - Filter = ConsolidatingFilter; - } - - private LinkedList messageList = new LinkedList(); - - private bool ConsolidatingFilter(ICurrentEventInfo message) - { - // Remove any matching previous - messageList.RemoveAll(evt => evt.Equals(message)); - messageList.AddLast(message); - return true; - } - - public override void Dispose() - { - ServiceImpl theService = service; - base.Dispose(); - - // Safe as we've already deregistered the filter, so no loops. - foreach (ICurrentEventInfo message in messageList) - { - CurrentEventInfoImpl info = (CurrentEventInfoImpl)message; - info.filterComplete = false; - theService.Send((CurrentEventInfoImpl)message); - } - messageList = null; - } - - } - #endregion - - #region Startup - - internal void Awake() - { - // Clear the listeners list when reloaded. - GameEvents.onGameSceneLoadRequested.Add(SceneLoadedListener); - GameEvents.onInputLocksModified.Add(OnInputLocksModified); - GameEvents.onPartAttach.Add(OnPartAttach); - GameEvents.onPartRemove.Add(OnPartRemove); - } - - internal void OnDestroy() - { - GameEvents.onGameSceneLoadRequested.Remove(SceneLoadedListener); - GameEvents.onInputLocksModified.Remove(OnInputLocksModified); - GameEvents.onPartAttach.Remove(OnPartAttach); - GameEvents.onPartRemove.Remove(OnPartRemove); - listeners = null; - } - - private void SceneLoadedListener(GameScenes scene) - { - currRoot = null; - listeners.Clear(); - } - - private Part currRoot; - - private void OnInputLocksModified(GameEvents.FromToAction data) - { - if (!HighLogic.LoadedSceneIsEditor) - return; - - if (EditorLogic.fetch == null) - return; - - var ship = EditorLogic.fetch.ship; - - if (ship == null) - return; - - if (ship.parts.Count > 0) - { - if (!ReferenceEquals(ship.parts[0], currRoot)) - { - Part root = ship.parts[0]; - CheckKnown(root); - SendAsyncProxy(this, root); - currRoot = root; - } - } - else if(currRoot != null) - { - SendAsyncProxy(this, currRoot); - currRoot = null; - } - } - - private Part lastSelected; - - private void UpdatePartHeirachy() - { - Part selectedPart = EditorLogic.SelectedPart; - if (ReferenceEquals(lastSelected, selectedPart) - || (lastSelected = selectedPart) == null - || CheckKnown(selectedPart)) - return; - - // Parts that are clones (Alt-Click) won't have had their attach methods called. - PartAttachSymmetry(selectedPart); - } - - private bool CheckKnown(Part part) - { - if (!part.isClone) - return true; - - KnownPartMarker marker = part.gameObject.GetTaggedComponent(); - if (marker == null) - { - marker = part.gameObject.AddTaggedComponent(); - marker.known = true; - return false; - } - - bool ret = marker.known; - marker.known = true; - return ret; - } - - private void OnPartAttach(GameEvents.HostTargetAction data) - { - // Target is the parent, host is the child part - SendAsyncProxy(this, data.host, data.target); - SendAsyncProxy(this, data.target, data.host); - - if (CheckKnown(data.host)) - return; - - // Symmetry clones won't have had either of the above listeners called for any of their children - PartAttachSymmetry(data.host); - } - - private void PartAttachSymmetry(Part thisPart) - { - foreach (Part child in thisPart.children) - { - SendAsyncProxy(this, child, thisPart); - SendAsyncProxy(this, thisPart, child); - PartAttachSymmetry(child); - } - } - - private void OnPartRemove(GameEvents.HostTargetAction data) - { - // host is null, target is the child part. - SendAsyncProxy(this, data.target, new object[] { null }); - SendAsyncProxy(this, data.target.parent, data.target); - - if (data.target.attachMode == AttachModes.SRF_ATTACH) - data.target.srfAttachNode.attachedPart = null; - - } - - #endregion - - #region Conversion to IPartMessage - private readonly Dictionary cachedPartMessages = new Dictionary(); - - /// - /// Convert delegate type into the IPartMessage interface. - /// - /// Delegate type to convert. This must be a delegate type marked with the attribute. - public IPartMessage AsIPartMessage(Type type) - { - IPartMessage value; - if (cachedPartMessages.TryGetValue(type, out value)) - return value; - return cachedPartMessages[type] = new MessageImpl(this, type); - } - - /// - /// Convert delegate type into the IPartMessage interface. - /// - /// Delegate type to convert. This must be a delegate type marked with the attribute. - public IPartMessage AsIPartMessage() - { - return AsIPartMessage(typeof(T)); - } - #endregion - - #region Utility Functions - private static Part AsPart(object src) - { - Part part = src as Part; - if (part != null) - return part; - PartModule module = src as PartModule; - if (module != null) - return module.part; - - if(src.GetType().GetInterfaces().Any(t => t.FullName == typeof (IPartMessagePartProxy).FullName || t.FullName == "KSPAPIExtensions.PartMessagePartProxy")) - return DuckTyping.Cast(src).ProxyPart; - return null; - } - #endregion - } - - #region Initialization and Other Mod Interfacing - internal class PartMessageServiceInitializer : MonoBehaviour - { - private static bool loadedInScene; - - internal void Awake() - { - // Ensure that only one copy of the service is run per scene change. - if (loadedInScene) - { - Assembly currentAssembly = Assembly.GetExecutingAssembly(); - Debug.Log("[PartMessageService] Multiple copies of current version. Using the first copy. Version: " + currentAssembly.GetName().Version); - Destroy(gameObject); - return; - } - loadedInScene = true; - - if (!SystemUtils.RunTypeElection(typeof(PartMessageService), "KSPAPIExtensions")) - return; - - // So at this point we know we have won the election, and will be using the class versions as in this assembly. - - // Destroy the old service - if (PartMessageService._instance != null) - { - Debug.Log("[PartMessageService] destroying service from previous load"); - Destroy(((ServiceImpl)PartMessageService._instance).gameObject); - } - - // Create the part message service - GameObject serviceGo = new GameObject(PartMessageService.PartMessageServiceName); - DontDestroyOnLoad(serviceGo); - - // Assign the service to the static variable - PartMessageService._instance = serviceGo.AddTaggedComponent(); - - // At this point the losers will duck-type themselves to the latest version of the service if they're called. - - ListenerFerramAerospaceResearch.AddListener(serviceGo); - } - - public void Update() - { - loadedInScene = false; - Destroy(gameObject); - } - } - - internal class KnownPartMarker : MonoBehaviour - { - [NonSerialized] - internal bool known; - } - - internal class ListenerFerramAerospaceResearch : MonoBehaviour - { - // ReSharper disable once InconsistentNaming - private static Action SetBasicDragModuleProperties; - - public static void AddListener(GameObject serviceGo) - { - Type typeFARAeroUtil = Type.GetType("ferram4.FARAeroUtil", false); - if (typeFARAeroUtil == null) - return; - - MethodInfo info = typeFARAeroUtil.GetMethod("SetBasicDragModuleProperties", new[] { typeof(Part) }); - - if (info == null) - { - Debug.LogWarning("[PartMessageService] FAR update method seems to have changed. Cannot interface with FAR."); - return; - } - - SetBasicDragModuleProperties = (Action)Delegate.CreateDelegate(typeFARAeroUtil, info); - - serviceGo.AddTaggedComponent(); - } - - private IPartMessageService service; - - public void Awake() - { - service = PartMessageService.Instance; - GameEvents.onGameSceneLoadRequested.Add(GameSceneLoaded); - } - - private void GameSceneLoaded(GameScenes data) - { - if(!GameSceneFilter.AnyEditorOrFlight.IsLoaded()) - return; - - PartMessageService.Register(this); - } - - [PartMessageListener(typeof(PartModelChanged), relations:PartRelationship.Unknown, scenes: GameSceneFilter.AnyEditorOrFlight)] - public void PartModelChanged() - { - Part part = service.CurrentEventInfo.SourcePart; - if (part == null) - return; - - SetBasicDragModuleProperties(part); - } - } - - - #endregion - - #region Message Enumerator - internal class MessageEnumerable : IEnumerable - { - internal MessageEnumerable(Type message) - { - this.message = message; - } - - readonly internal Type message; - - IEnumerator IEnumerable.GetEnumerator() - { - return new MessageEnumerator(message); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new MessageEnumerator(message); - } - - private class MessageEnumerator : IEnumerator - { - - public MessageEnumerator(Type top) - { - current = this.top = top; - } - - private int pos = -1; - private Type current; - private Type top; - - object IEnumerator.Current - { - get - { - if (pos != 0) - throw new InvalidOperationException(); - return current; - } - } - - Type IEnumerator.Current - { - get - { - if (pos != 0) - throw new InvalidOperationException(); - return current; - } - } - - bool IEnumerator.MoveNext() - { - switch (pos) - { - case -1: - current = top; - pos = 0; - break; - case 1: - return false; - case 0: - PartMessageDelegate evt = (PartMessageDelegate)current.GetCustomAttributes(typeof(PartMessageDelegate), true)[0]; - current = evt.Parent; - break; - case 2: - throw new InvalidOperationException("Enumerator disposed"); - } - if (current == null) - { - pos = 1; - return false; - } - return true; - } - - void IEnumerator.Reset() - { - pos = -1; - current = null; - } - - void IDisposable.Dispose() - { - current = top = null; - pos = 2; - } - } - } - #endregion - -} diff --git a/Source/Tweakables/UIPartActionChooseOption.cs b/Source/Tweakables/UIPartActionChooseOption.cs index 4719996c..59fcd59f 100644 --- a/Source/Tweakables/UIPartActionChooseOption.cs +++ b/Source/Tweakables/UIPartActionChooseOption.cs @@ -2,7 +2,7 @@ using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIExtensions +namespace KSPAPIEL { [UI_ChooseOption] public class UIPartActionChooseOption : UIPartActionFieldItem diff --git a/Source/Tweakables/UIPartActionFloatEdit.cs b/Source/Tweakables/UIPartActionFloatEdit.cs index 6e09ee1c..545f3fae 100644 --- a/Source/Tweakables/UIPartActionFloatEdit.cs +++ b/Source/Tweakables/UIPartActionFloatEdit.cs @@ -3,7 +3,7 @@ using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIExtensions +namespace KSPAPIEL { [UI_FloatEdit] diff --git a/Source/Tweakables/UIPartActionScaleEdit.cs b/Source/Tweakables/UIPartActionScaleEdit.cs index b09c0252..2961dd95 100644 --- a/Source/Tweakables/UIPartActionScaleEdit.cs +++ b/Source/Tweakables/UIPartActionScaleEdit.cs @@ -3,7 +3,7 @@ using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIExtensions +namespace KSPAPIEL { [UI_ScaleEdit] diff --git a/Source/Tweakables/UIPartActionsExtended.cs b/Source/Tweakables/UIPartActionsExtended.cs index c036af4b..fd00a7dc 100644 --- a/Source/Tweakables/UIPartActionsExtended.cs +++ b/Source/Tweakables/UIPartActionsExtended.cs @@ -5,10 +5,9 @@ using System.Linq; using System.Reflection; using UnityEngine; -using KSPAPIExtensions.PartMessage; // ReSharper disable once CheckNamespace -namespace KSPAPIExtensions +namespace KSPAPIEL { internal class UIPartActionsExtendedRegistration : MonoBehaviour { @@ -28,7 +27,7 @@ public void Start() DontDestroyOnLoad(gameObject); - isLatestVersion = SystemUtils.RunTypeElection(typeof(UIPartActionsExtendedRegistration), "KSPAPIExtensions"); + isLatestVersion = SystemUtils.RunTypeElection(typeof(UIPartActionsExtendedRegistration), "KSPAPIEL"); } public void OnLevelWasLoaded(int level) @@ -118,7 +117,6 @@ private void OnSliderChanged(IUIObject obj) SIPrefix prefix = resource.maxAmount.GetSIPrefix(); resource.amount = prefix.Round(slider.Value * resource.maxAmount, digits:4); - PartMessageService.Send(this, part, resource, resource.amount); if (scene == UI_Scene.Editor) SetSymCounterpartsAmount(resource.amount); resourceAmnt.Text = resource.amount.ToString("F1"); @@ -136,7 +134,6 @@ private void OnSliderChanged(IUIObject obj) continue; PartResource symResource = sym.Resources[resource.info.name]; symResource.amount = amount; - PartMessageService.Send(this, sym, symResource, symResource.amount); } } @@ -205,5 +202,4 @@ internal static UIPartActionLabelImproved CreateTemplate(UIPartActionLabel oldLa return label; } } - } diff --git a/Source/Utils/ConfigNodeUtils.cs b/Source/Utils/ConfigNodeUtils.cs deleted file mode 100644 index af3b6c0a..00000000 --- a/Source/Utils/ConfigNodeUtils.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System.Linq; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions -{ - public static class ConfigNodeUtils - { - #region TryGetValue - - /// - /// Get value from the node. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or default value if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out string value) - { - value = node.GetValue(name); - return value != null; - } - - /// - /// Get the value and parse as a string array. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or null if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out string[] value) - { - value = ParseUtils.ParseArray(node.GetValue(name)); - return value != null; - } - - /// - /// Get the value and parse as a boolean. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or false if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out bool value) - { - string val = node.GetValue(name); - if (string.IsNullOrEmpty(val)) - { - value = false; - return false; - } - return bool.TryParse(val, out value); - } - - /// - /// Get the value and parse as an int. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or 0 if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out int value) - { - string val = node.GetValue(name); - if (string.IsNullOrEmpty(val)) - { - value = 0; - return false; - } - return int.TryParse(val, out value); - } - - /// - /// Get the value and parse as a float. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or 0 if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out float value) - { - string val = node.GetValue(name); - if (string.IsNullOrEmpty(val)) - { - value = 0; - return false; - } - return float.TryParse(val, out value); - } - - /// - /// Get the value and parse as a double. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or 0 if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out double value) - { - string val = node.GetValue(name); - if (string.IsNullOrEmpty(val)) - { - value = 0; - return false; - } - return double.TryParse(val, out value); - } - - /// - /// Get the value and parse as a Vector3. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or default value if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out Vector3 value) - { - string val = node.GetValue(name); - if (string.IsNullOrEmpty(val)) - { - value = default(Vector3); - return false; - } - return ParseUtils.TryParseVector3(val, out value); - } - - /// - /// Get the value and parse as a Vector3d. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or default value if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out Vector3d value) - { - string val = node.GetValue(name); - if (string.IsNullOrEmpty(val)) - { - value = default(Vector3d); - return false; - } - return ParseUtils.TryParseVector3d(val, out value); - } - - /// - /// Get the value and parse as a Color. - /// - /// Node to get value from - /// Name of the value to find - /// The result, or default value if fails - /// True if successful. False if the value is not present or can't be parsed. - public static bool TryGetValue(this ConfigNode node, string name, out Color value) - { - string val = node.GetValue(name); - if (string.IsNullOrEmpty(val)) - { - value = default(Color); - return false; - } - return ParseUtils.TryParseColor(val, out value); - } - #endregion - - #region TryGetNode - /// - /// Get the config node, returns true if successful. - /// - /// Node to get value from - /// Name of the node to find - /// The result, or null if fails - /// True if successful. False if the value is not present. - public static bool TryGetNode(this ConfigNode node, string name, out ConfigNode result) - { - result = node.GetNode(name); - return result != null; - } - - /// - /// Sees if the node has any ConfigNode of the given name, and stores all the found occurences in the ref value. Returns false if there was none. - /// - /// Node to get value from - /// Name of the nodes to find - /// Array to store the results in - /// True if successful. False if the value is not present. - public static bool TryGetNodes(this ConfigNode node, string name, out ConfigNode[] results) - { - results = node.GetNodes(name); - return results.Length != 0; - } - - /// - /// Sees if the node has any value of the given name, and stores all occurences found in the ref value. Returns false if there was none. - /// - /// Node to get value from - /// Name of the values to find - /// Array to store the results in - /// True if successful. False if the value is not present. - public static bool TryGetValues(this ConfigNode node, string name, out string[] results) - { - results = node.GetValues(name); - return results.Length != 0; - } - #endregion - - #region HasValues / HasNodes - /// - /// Checks to see if the ConfigNode has all specified values - /// - /// Node to get values from - /// Values to find - public static bool HasAllValues(this ConfigNode node, params string[] values) - { - return values.All(node.HasValue); - } - - /// - /// Checks to see if the given ConfigNode has all specified nodes. - /// - /// Node to get nodes from - /// Nodes to find - public static bool HasAllNodes(this ConfigNode node, params string[] nodes) - { - return nodes.All(node.HasNode); - } - #endregion - } -} diff --git a/Source/Utils/DebuggingUtils.cs b/Source/Utils/DebuggingUtils.cs deleted file mode 100644 index 72fc2e66..00000000 --- a/Source/Utils/DebuggingUtils.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Reflection; -using System.Text; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions.DebuggingUtils -{ - [Flags] - public enum DumpTreeOption - { - None = 0x0, - Default = 0x0, - - Active = 0x1, - TransformPosition = 0x2, - TransformRotation = 0x4, - - // Optons that require iterating through components should have the 0x10 bit set. - Components = 0x30, - Materials = 0x50, - Mesh = 0x90, - - Rigidbody = 0x110, - } - - public static class Debugging - { - - public static string DumpTypeHeirachy(Type type) - { - StringBuilder sb = new StringBuilder(); - sb.Append("Declared class: ").Append(type.AssemblyQualifiedName); - while(true) - { - Type [] interfaces = type.GetInterfaces(); - if (interfaces.Length > 0) - { - sb.AppendLine(" Interfaces:"); - for (int i = 0; i < interfaces.Length; i++) - sb.Append(" ").Append(interfaces[i].AssemblyQualifiedName); - } - type = type.BaseType; - if (type == null) - { - sb.Length = sb.Length - 1; - return sb.ToString(); - } - sb.AppendLine(" Base Type: " + type.AssemblyQualifiedName); - } - } - - /// - /// Produce a string that displays the heirachy and other information about a transform. - /// - /// String containing the dump. - public static string DumpTree(this Transform t, DumpTreeOption options = DumpTreeOption.Default) - { - StringBuilder sb = new StringBuilder(); - DumpTree(t, options, 0, sb); - return sb.ToString(); - } - - private static void DumpTree(Transform t, DumpTreeOption options, int level, StringBuilder sb) - { - string space = ""; - for (int i = 0; i < level; ++i) - space += '-'; - sb.AppendLine(space + t.name); - if ((options & DumpTreeOption.Active) == DumpTreeOption.Active) - { - sb.AppendLine(space + "+ activeSelf=" + t.gameObject.activeSelf + " activeInHeirachy=" + t.gameObject.activeInHierarchy); - } - if ((options & DumpTreeOption.TransformPosition) == DumpTreeOption.TransformPosition) - { - sb.AppendLine(space + "+ position: " + t.position + " localPosition: " + t.localPosition); - } - if ((options & DumpTreeOption.TransformRotation) == DumpTreeOption.TransformRotation) - { - sb.AppendLine(space + "+ rotation: " + t.rotation + " localRotation: " + t.localRotation); - } - - if ((((int)options & 0x10) == 0)) - goto skipComponents; - - foreach (Component c in t.gameObject.GetComponents()) - { - - if (!(c is Transform) && ((options & DumpTreeOption.Components) == DumpTreeOption.Components)) - sb.AppendLine(space + "+ component:" + c.GetType()); - - if (c is Renderer && (options & DumpTreeOption.Materials) == DumpTreeOption.Materials) - foreach (Material m in t.renderer.sharedMaterials) - sb.AppendLine(space + "++ mat:" + m.name); - - if (c is MeshFilter && (options & DumpTreeOption.Mesh) == DumpTreeOption.Mesh) - { - MeshFilter filter = (MeshFilter)c; - sb.AppendLine(space + "++ mesh:" + ((filter.sharedMesh == null) ? "*null*" : (filter.sharedMesh.name + " verts:" + filter.sharedMesh.vertexCount))); - } - - if (c is MeshCollider && (options & DumpTreeOption.Mesh) == DumpTreeOption.Mesh) - { - MeshCollider collider = (MeshCollider)c; - sb.AppendLine(space + "++ mesh:" + ((collider.sharedMesh == null) ? "*null*" : (collider.sharedMesh.name + " verts:" + collider.sharedMesh.vertexCount))); - } - - if (c is Rigidbody && (options & DumpTreeOption.Rigidbody) == DumpTreeOption.Rigidbody) - { - Rigidbody body = (Rigidbody)c; - sb.AppendLine(space + "++ Mass:" + body.mass.ToString("F3")); - sb.AppendLine(space + "++ CoM:" + body.centerOfMass.ToString("F3")); - } - if (c is Joint && (options & DumpTreeOption.Rigidbody) == DumpTreeOption.Rigidbody) - { - Joint joint = (Joint)c; - sb.AppendLine(space + "++ anchor:" + joint.anchor.ToString("F3")); - - sb.AppendLine(space + "++ connectedBody: " + (joint.connectedBody != null)); - if (joint.connectedBody != null) - DumpTree(joint.connectedBody.transform, options, level + 1, sb); - } - } - - skipComponents: - for (int i = 0; i < t.childCount; ++i) - DumpTree(t.GetChild(i), options, level + 1, sb); - } - - /// - /// Create a string with the contents of a mesh. - /// - public static string DumpMesh(this Mesh mesh) - { - Vector3[] verticies = mesh.vertices; - Vector3[] normals = mesh.normals; - Vector4[] tangents = mesh.tangents; - Vector2[] uv = mesh.uv; - - StringBuilder sb = new StringBuilder().AppendLine(); - for (int i = 0; i < mesh.vertexCount; ++i) - { - sb - .Append(verticies[i].ToString("F4")).Append(", ") - .Append(uv[i].ToString("F4")).Append(", ") - .Append(normals[i].ToString("F4")).Append(", ") - .Append(tangents[i].ToString("F4")).AppendLine(); - } - sb.Replace("(", "").Replace(")", ""); - sb.AppendLine(); - - for (int i = 0; i < mesh.triangles.Length; i += 3) - { - sb - .Append(mesh.triangles[i]).Append(',') - .Append(mesh.triangles[i + 1]).Append(',') - .Append(mesh.triangles[i + 2]).AppendLine(); - } - - return sb.ToString(); - } - - /// - /// Dump out all the fields from an object - /// - public static string DumpObjectFields(this object obj, BindingFlags flags = BindingFlags.Default) - { - StringBuilder sb = new StringBuilder(); - Type type = obj.GetType(); - - foreach (FieldInfo field in type.GetFields(flags)) - { - object value = field.GetValue(obj); - if (value == null) - sb.AppendLine(field.FieldType.Name + " " + field.Name + "is null"); - else - sb.AppendLine(field.FieldType.Name + " " + field.Name + " = " + value); - } - return sb.ToString(); - } - - public static string DumpNotEqualFields(this T obj, T that, BindingFlags flags = BindingFlags.Default) - { - StringBuilder sb = new StringBuilder(); - if (ReferenceEquals(obj, that)) - sb.AppendLine("Same object"); - - Type type = typeof(T); - - foreach (FieldInfo field in type.GetFields(flags)) - { - object thisValue = field.GetValue(obj); - object thatValue = field.GetValue(that); - - if (field.FieldType.IsPrimitive && Equals(thisValue, thatValue)) - continue; - if (ReferenceEquals(thisValue, thatValue)) - continue; - sb.AppendLine("Different fields: " + field.FieldType.Name + " " + field.Name + (Equals(thisValue, thatValue) ? "(compute equal)" : (" " + thisValue + " != " + thatValue))); - } - return sb.ToString(); - } - } -} diff --git a/Source/Utils/EngineWrapper.cs b/Source/Utils/EngineWrapper.cs deleted file mode 100644 index defea5a3..00000000 --- a/Source/Utils/EngineWrapper.cs +++ /dev/null @@ -1,354 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace KSPAPIExtensions.Utils -{ - // ReSharper disable InconsistentNaming - public class EngineWrapper - { - public enum ModuleType - { - MODULEENGINES, - MODULEENGINESFX, - MODULERCS - } - - private readonly ModuleType type; - private readonly ModuleEngines mE; - private readonly ModuleEnginesFX mEFX; - - public EngineWrapper(Part part) - { - if ((mEFX = part.transform.GetComponent()) != null) - type = ModuleType.MODULEENGINESFX; - else if ((mE = part.transform.GetComponent()) != null) - type = ModuleType.MODULEENGINES; - else - throw new ArgumentException("Unable to find engine-like module"); - } - - public EngineWrapper(ModuleEngines mod) - { - mE = mod; - type = ModuleType.MODULEENGINES; - } - - public EngineWrapper(ModuleEnginesFX mod) - { - mEFX = mod; - type = ModuleType.MODULEENGINESFX; - } - - public static implicit operator PartModule(EngineWrapper wrapper) - { - return (PartModule)wrapper.mE ?? wrapper.mEFX; - } - - public static explicit operator ModuleEngines(EngineWrapper wrapper) - { - return wrapper.mE; - } - - public static explicit operator ModuleEnginesFX(EngineWrapper wrapper) - { - return wrapper.mEFX; - } - - public ModuleType Type { get { return type; } } - - // ReSharper disable once InconsistentNaming - public List propellants - { - get - { - switch(type) - { - case ModuleType.MODULEENGINES: - return mE.propellants; - case ModuleType.MODULEENGINESFX: - return mEFX.propellants; - default: - throw new InvalidProgramException(); - } - } - } - public void SetupPropellant() - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.SetupPropellant(); - break; - case ModuleType.MODULEENGINESFX: - mEFX.SetupPropellant(); - break; - default: - throw new InvalidProgramException(); - } - } - public BaseActionList Actions - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.Actions; - case ModuleType.MODULEENGINESFX: - return mEFX.Actions; - default: - throw new InvalidProgramException(); - } - } - } - public bool getIgnitionState - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.getIgnitionState; - case ModuleType.MODULEENGINESFX: - return mEFX.getIgnitionState; - default: - throw new InvalidProgramException(); - } - } - } - public bool EngineIgnited - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.EngineIgnited; - case ModuleType.MODULEENGINESFX: - return mEFX.EngineIgnited; - default: - throw new InvalidProgramException(); - } - } - set - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.EngineIgnited = value; - break; - case ModuleType.MODULEENGINESFX: - mEFX.EngineIgnited = value; - break; - default: - throw new InvalidProgramException(); - } - } - } - public FloatCurve atmosphereCurve - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.atmosphereCurve; - case ModuleType.MODULEENGINESFX: - return mEFX.atmosphereCurve; - default: - throw new InvalidProgramException(); - } - } - set - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.atmosphereCurve = value; - break; - case ModuleType.MODULEENGINESFX: - mEFX.atmosphereCurve = value; - break; - default: - throw new InvalidProgramException(); - } - } - } - public FloatCurve velCurve - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.velCurve; - case ModuleType.MODULEENGINESFX: - return mEFX.velCurve; - default: - throw new InvalidProgramException(); - } - } - set - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.velCurve = value; - break; - case ModuleType.MODULEENGINESFX: - mEFX.velCurve = value; - break; - default: - throw new InvalidProgramException(); - } - } - } - public bool useVelCurve - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.useVelCurve; - case ModuleType.MODULEENGINESFX: - return mEFX.useVelCurve; - default: - throw new InvalidProgramException(); - } - } - set - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.useVelCurve = value; - break; - case ModuleType.MODULEENGINESFX: - mEFX.useVelCurve = value; - break; - default: - throw new InvalidProgramException(); - } - } - } - public float maxThrust - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.maxThrust; - case ModuleType.MODULEENGINESFX: - return mEFX.maxThrust; - default: - throw new InvalidProgramException(); - } - } - set - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.maxThrust = value; - break; - case ModuleType.MODULEENGINESFX: - mEFX.maxThrust = value; - break; - default: - throw new InvalidProgramException(); - } - } - } - public float minThrust - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.minThrust; - case ModuleType.MODULEENGINESFX: - return mEFX.minThrust; - default: - throw new InvalidProgramException(); - } - } - set - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.minThrust = value; - break; - case ModuleType.MODULEENGINESFX: - mEFX.minThrust = value; - break; - default: - throw new InvalidProgramException(); - } - } - } - public float heatProduction - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.heatProduction; - case ModuleType.MODULEENGINESFX: - return mEFX.heatProduction; - default: - throw new InvalidProgramException(); - } - } - set - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.heatProduction = value; - break; - case ModuleType.MODULEENGINESFX: - mEFX.heatProduction = value; - break; - default: - throw new InvalidProgramException(); - } - } - } - - public float g - { - get - { - switch (type) - { - case ModuleType.MODULEENGINES: - return mE.g; - case ModuleType.MODULEENGINESFX: - return mEFX.g; - default: - throw new InvalidProgramException(); - } - } - set - { - switch (type) - { - case ModuleType.MODULEENGINES: - mE.g = value; - break; - case ModuleType.MODULEENGINESFX: - mEFX.g = value; - break; - default: - throw new InvalidProgramException(); - } - } - } - } - // ReSharper restore InconsistentNaming -} diff --git a/Source/Utils/KSPUtils.cs b/Source/Utils/KSPUtils.cs index 8d390de9..ddca8cda 100644 --- a/Source/Utils/KSPUtils.cs +++ b/Source/Utils/KSPUtils.cs @@ -2,34 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using DeftTech.DuckTyping; using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIExtensions +namespace KSPAPIEL { - /// - /// Flags to determine relationship between two parts. - /// - [Flags] - public enum PartRelationship - { - Vessel = 0x1, - Self = 0x2, - Symmetry = 0x4, - Decendent = 0x8 , - Child = 0x10, - Ancestor = 0x20, - Parent = 0x40, - Sibling = 0x80, - Unrelated = 0x100, - Unknown = 0x0, - - AnyOnVessel = Vessel | Self | Symmetry | Decendent | Child | Ancestor | Parent | Sibling, - AnyPart = AnyOnVessel | Unrelated, - } - /// /// Flags to filter particular game scenes. /// @@ -54,141 +32,6 @@ public enum GameSceneFilter public static class PartUtils { - private static FieldInfo windowListField; - - /// - /// Find the UIPartActionWindow for a part. Usually this is useful just to mark it as dirty. - /// - public static UIPartActionWindow FindActionWindow(this Part part) - { - if (part == null) - return null; - - // We need to do quite a bit of piss-farting about with reflection to - // dig the thing out. We could just use Object.Find, but that requires hitting a heap more objects. - UIPartActionController controller = UIPartActionController.Instance; - if (controller == null) - return null; - - if (windowListField == null) - { - Type cntrType = typeof(UIPartActionController); - foreach (FieldInfo info in cntrType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)) - { - if (info.FieldType == typeof(List)) - { - windowListField = info; - goto foundField; - } - } - Debug.LogWarning("*PartUtils* Unable to find UIPartActionWindow list"); - return null; - } - foundField: - - List uiPartActionWindows = (List) windowListField.GetValue(controller); - if (uiPartActionWindows == null) - return null; - - return uiPartActionWindows.FirstOrDefault(window => window != null && window.part == part); - } - - /// - /// If this part is a symmetry clone of another part, this method will return the original part. - /// - /// The part to find the original of - /// The original part, or the part itself if it was the original part - public static Part GetSymmetryCloneOriginal(this Part part) - { - if (!part.isClone || part.symmetryCounterparts == null || part.symmetryCounterparts.Count == 0) - return part; - - // Symmetry counterparts always are named xxxx(Clone) if they are cloned from xxxx. So the shortest name is the one. - int nameLength = part.transform.name.Length; - foreach (Part other in part.symmetryCounterparts) - { - if (other.transform.name.Length < nameLength) - return other; - } - return part; - } - - /// - /// Find the relationship between two parts. - /// - public static PartRelationship RelationTo(this Part part, Part other) - { - if (other == null || part == null) - return PartRelationship.Unknown; - - if (other == part) - return PartRelationship.Self; - if (part.localRoot != other.localRoot) - return PartRelationship.Unrelated; - if (part.parent == other) - return PartRelationship.Child; - if (other.parent == part) - return PartRelationship.Parent; - if (other.parent == part.parent) - return PartRelationship.Sibling; - for (Part tmp = part.parent; tmp != null; tmp = tmp.parent) - if (tmp == other) - return PartRelationship.Decendent; - for (Part tmp = other.parent; tmp != null; tmp = tmp.parent) - if (tmp == part) - return PartRelationship.Ancestor; - if(part.localRoot == other.localRoot) - return PartRelationship.Vessel; - return PartRelationship.Unrelated; - } - - /// - /// Test if two parts are related by a set of criteria. Because PartRelationship is a flags - /// enumeration, multiple flags can be tested at the same time. - /// - public static bool RelationTest(this Part part, Part other, PartRelationship relation) - { - if (relation == PartRelationship.Unknown) - return true; - if(part == null || other == null) - return false; - - if (TestFlag(relation, PartRelationship.Self) && part == other) - return true; - if (TestFlag(relation, PartRelationship.Vessel) && part.localRoot == other.localRoot) - return true; - if (TestFlag(relation, PartRelationship.Unrelated) && part.localRoot != other.localRoot) - return true; - if (TestFlag(relation, PartRelationship.Sibling) && part.parent == other.parent) - return true; - if (TestFlag(relation, PartRelationship.Ancestor)) - { - for (Part upto = other.parent; upto != null; upto = upto.parent) - if (upto == part) - return true; - } - else if (TestFlag(relation, PartRelationship.Parent) && part == other.parent) - return true; - - if (TestFlag(relation, PartRelationship.Decendent)) - { - for (Part upto = part.parent; upto != null; upto = upto.parent) - if (upto == other) - return true; - } - else if (TestFlag(relation, PartRelationship.Child) && part.parent == other) - return true; - - if (TestFlag(relation, PartRelationship.Symmetry)) - return other.symmetryCounterparts.Any(sym => part == sym); - return false; - } - - internal static bool TestFlag(this PartRelationship e, PartRelationship flags) - { - return (e & flags) == flags; - } - /// /// Convert GameScene enum into GameSceneFilter /// @@ -204,136 +47,5 @@ public static bool IsLoaded(this GameSceneFilter filter) { return (int)(filter & HighLogic.LoadedScene.AsFilter()) != 0; } - - /// - /// Register an 'OnUpdate' method for use in the editor. - /// This should be done in the OnAwake method of the module, and will ensure that all modules have the - /// registered method called in order of declaration of the module in the part file. - /// - /// Module that is being registered. - /// Method to call - public static void RegisterOnUpdateEditor(this PartModule module, Action action) - { - if (!HighLogic.LoadedSceneIsEditor) - return; - Part part = module.part; - IOnEditorUpdateUtility utility; - foreach (MonoBehaviour c in part.GetComponents()) - { - if (c.GetType().FullName == typeof (OnEditorUpdateUtility).FullName) - { - utility = DuckTyping.Cast(c); - goto found; - } - } - - PartModule onEditorUpdate = (PartModule)part.gameObject.AddComponent(OnEditorUpdateUtility.LatestVersion); - utility = DuckTyping.Cast(onEditorUpdate); - part.Modules.Add(onEditorUpdate); - - found: - utility.AddOnUpdate(module, action); - } } - - internal interface IOnEditorUpdateUtility - { - void AddOnUpdate(PartModule module, Action action); - } - - internal class OnEditorUpdateUtility : PartModule, IOnEditorUpdateUtility - { - public static readonly Type LatestVersion = SystemUtils.VersionTaggedType(SystemUtils.TypeElectionWinner(typeof (OnEditorUpdateUtility), "KSPAPIExtensions")); - - public override void OnStart(StartState state) - { - started = true; - part.Modules.Remove(this); - } - - private class ModAction : IComparable - { - public PartModule module; - public Action action; - - private int Index - { - get - { - return module.part.Modules.IndexOf(module); - } - } - - public int CompareTo(ModAction other) - { - return Index.CompareTo(other.Index); - } - } - - private readonly List modules = new List(); - private bool started; - - public void AddOnUpdate(PartModule module, Action action) - { - ModAction item = new ModAction - { - module = module, - action = action, - }; - int insert = modules.BinarySearch(item); - if (insert < 0) - modules.Insert(~insert, item); - } - - - public void Update() - { - if (!started) - return; - - for (int i = 0; i < modules.Count; i++) - { - ModAction action = modules[i]; - if (!action.module) - modules.RemoveAt(i--); - else if (action.module.enabled) - action.action(); - } - } - } - - /// - /// KSPAddon with equality checking using an additional type parameter. Fixes the issue where AddonLoader prevents multiple start-once addons with the same start scene. - /// - public class KSPAddonFixed : KSPAddon, IEquatable - { - private readonly Type type; - - public KSPAddonFixed(Startup startup, bool once, Type type) - : base(startup, once) - { - this.type = type; - } - - public override bool Equals(object obj) - { - if (obj.GetType() != GetType()) { return false; } - return Equals((KSPAddonFixed)obj); - } - - public bool Equals(KSPAddonFixed other) - { - if (once != other.once) { return false; } - if (startup != other.startup) { return false; } - if (type != other.type) { return false; } - return true; - } - - public override int GetHashCode() - { - // ReSharper disable NonReadonlyFieldInGetHashCode - return startup.GetHashCode() ^ once.GetHashCode() ^ type.GetHashCode(); - // ReSharper restore NonReadonlyFieldInGetHashCode - } - } } diff --git a/Source/Utils/MathUtils.cs b/Source/Utils/MathUtils.cs index 3898f7d7..aefd6aad 100644 --- a/Source/Utils/MathUtils.cs +++ b/Source/Utils/MathUtils.cs @@ -2,44 +2,10 @@ using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIExtensions +namespace KSPAPIEL { public static class MathUtils { - - /// - /// Clamp a value between the max and min values. If < min it will be changed to min, if > max it will be changed to max. - /// - /// Value to clamp - /// Minimum - /// Maximum - /// true if the value has been clamped. - public static bool TestClamp(ref float value, float min, float max) - { - if (value < min) - { - value = min; - return true; - } - if (value > max) - { - value = max; - return true; - } - return false; - } - - /// - /// Round value to the nearest. - /// - /// Value to round - /// Precision, eg 0.1, 0.02, 0.005 - /// The rounded value - public static float RoundTo(float value, float precision) - { - return Mathf.Round(value / precision) * precision; - } - /// /// Format a numeric value using SI prefexes. /// @@ -126,115 +92,6 @@ public static string ToStringExt(this float value, string format) return ToStringExt((double)value, format); } - /// - /// Parse a string in SI format - with SI unit prefix to a double. - /// Note - units must not be present. - /// - public static bool TryParseExt(string str, out double value) - { - str = str.Trim(); - if(str.Length == 0) { - value = 0; - return false; - } - - char last = str[str.Length - 1]; - - if (char.IsDigit(last)) - return double.TryParse(str, out value); - - double exponent; - int newLen = str.Length - 1; - switch (last) - { - case 'k': exponent = 1e3; break; - case 'M': exponent = 1e6; break; - case 'G': exponent = 1e9; break; - case 'T': exponent = 1e12; break; - case 'P': exponent = 1e15; break; - case 'E': exponent = 1e18; break; - case 'Z': exponent = 1e21; break; - case 'Y': exponent = 1e24; break; - case 'm': - exponent = 1e-3; break; - case '\x3bc': - case '\xb5': - case 'u': - exponent = 1e-6; break; - case 'c': - if (!str.EndsWith("mic")) - { - value = 0; - return false; - } - newLen = str.Length - 3; - exponent = 1e-6; - break; - case 'n': exponent = 1e-9; break; - case 'p': exponent = 1e-12; break; - case 'f': exponent = 1e-15; break; - case 'a': exponent = 1e-18; break; - case 'z': exponent = 1e-21; break; - case 'y': exponent = 1e-24; break; - default: - value = 0; - return false; - } - - double parsed; - if (!double.TryParse(str.Substring(0, newLen), out parsed)) - { - value = 0; - return false; - } - - value = parsed * exponent; - return true; - } - - /// - /// Parse a string in SI format - with SI unit prefix to a float. - /// Note - units must not be present. - /// - public static bool TryParseExt(string str, out float value) - { - double dVal; - if (TryParseExt(str, out dVal)) - { - value = (float)dVal; - return true; - } - value = 0; - return false; - } - - /// - /// Round a number to a set number of significant figures. - /// - /// number to round - /// number of significant figures, defaults to 3 - /// - public static float RoundSigFigs(this float d, int sigFigs = 3) - { - - int exponent = (int)Math.Floor(Math.Log10(Math.Abs(d))) - sigFigs; - float div = Mathf.Pow(10, exponent); - return Mathf.Round(d / div) * div; - } - - /// - /// Round a number to a set number of significant figures. - /// - /// number to round - /// number of significant figures, defaults to 3 - /// - public static double RoundSigFigs(this double value, int sigFigs = 3) - { - int exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))) - sigFigs; - double div = Mathf.Pow(10, exponent); - return Math.Round(value / div) * div; - } - /// /// Find the SI prefix for a number /// @@ -344,27 +201,6 @@ public static Func GetFormatter(this SIPrefix pfx, double value, } return v => (v/div).ToString("F" + (sigFigs - exp - 1)); } - - - /// - /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 - /// - public static string FormatMass(float mass, int sigFigs = 4, int exponent = 0) - { - return mass < 1.0f ? - mass.ToStringSI(sigFigs, exponent + 6, "g") : - mass.ToStringSI(sigFigs, exponent, "t"); - } - - /// - /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 - /// - public static string FormatMass(double mass, int sigFigs = 4, int exponent = 0) - { - return mass < 1.0f ? - mass.ToStringSI(sigFigs, exponent+6, "g") : - mass.ToStringSI(sigFigs, exponent, "t"); - } } /// diff --git a/Source/Utils/ParseUtils.cs b/Source/Utils/ParseUtils.cs deleted file mode 100644 index 05b6da00..00000000 --- a/Source/Utils/ParseUtils.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Linq; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions -{ - /// - /// Utility methods for parsing things - /// - public static class ParseUtils - { - /// - /// Tries to parse a Vector3 from the given string. Returns false if anything goes wrong. - /// - /// String to parse - /// The result, or default value if fails - /// True if successful - public static bool TryParseVector3(string text, out Vector3 result) - { - try - { - result = ParseVector3(text); - return true; - } - catch (ArgumentNullException) { } - catch (FormatException) { } - result = default(Vector3); - return false; - } - - /// - /// Parses a Vector3 from the given string. - /// - /// String to parse - /// The parsed vector - /// If is null - /// If cannot be parsed, or one of the component values would overflow a float - public static Vector3 ParseVector3(string text) - { - if (text == null) - throw new ArgumentNullException("text"); - - string[] splits = text.Split(',').Select(s => s.Trim()).ToArray(); - if (splits.Length != 3) - throw new FormatException("Unable to parse string as Vector3"); - - try - { - return new Vector3(float.Parse(splits[0]), float.Parse(splits[1]), float.Parse(splits[2])); - } - catch (OverflowException ex) - { - throw new FormatException("Unable to parse string as Vector3", ex); - } - } - - // ReSharper disable once InconsistentNaming - /// - /// Tries to parse a Vector3d from the given string. - /// - /// String to parse - /// The result, or default value if fails - /// True if successful - public static bool TryParseVector3d(string text, out Vector3d result) - { - try - { - result = ParseVector3d(text); - return true; - } - catch (ArgumentNullException) { } - catch (FormatException) { } - result = default(Vector3d); - return false; - } - - // ReSharper disable once InconsistentNaming - /// - /// Parses a Vector3d from the given string. - /// - /// String to parse - /// The parsed vector - /// If is null - /// If cannot be parsed, or one of the component values would overflow a double - public static Vector3d ParseVector3d(string text) - { - if (text == null) - throw new ArgumentNullException("text"); - - string[] splits = text.Split(',').Select(s => s.Trim()).ToArray(); - if (splits.Length != 3) - throw new FormatException("Unable to parse string as Vector3"); - - try - { - return new Vector3d(double.Parse(splits[0]), double.Parse(splits[1]), double.Parse(splits[2])); - } - catch (OverflowException ex) - { - throw new FormatException("Unable to parse string as Vector3", ex); - } - } - - /// - /// Tries to parse a Color from the given string. - /// - /// String to parse - /// The result, or default value if fails - /// True if successful - public static bool TryParseColor(string text, out Color result) - { - try - { - result = ParseColor(text); - return true; - } - catch (ArgumentNullException) { } - catch (FormatException) { } - result = default(Color); - return false; - } - - /// - /// Parses a Color from the given string. - /// - /// String to parse - /// The parsed Color - /// If is null - /// If cannot be parsed, or one of the component values would overflow a float - public static Color ParseColor(string text) - { - if (text == null) - throw new ArgumentNullException("text"); - string[] splits = text.Split(',').Select(s => s.Trim()).ToArray(); - try - { - switch (splits.Length) - { - case 3: - return new Color(float.Parse(splits[0]), float.Parse(splits[1]), float.Parse(splits[2])); - case 4: - return new Color(float.Parse(splits[0]), float.Parse(splits[1]), float.Parse(splits[2]), float.Parse(splits[3])); - default: - throw new FormatException("Unable to parse string as Color"); - } - } - catch (OverflowException ex) - { - throw new FormatException("Unable to parse string as Color", ex); - } - } - - /// - /// Parses the given string as an array where each element is separated by a comma. - /// - /// String to parse - /// The array, or null if is null - public static string[] ParseArray(string text) - { - if (text == null) - return null; - return text.Split(',').Select(s => s.Trim()).ToArray(); - } - } -} diff --git a/Source/Utils/PhysicsDebugger.cs b/Source/Utils/PhysicsDebugger.cs deleted file mode 100644 index b491176f..00000000 --- a/Source/Utils/PhysicsDebugger.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Text; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions.DebuggingUtils -{ - - /// - /// Part module that dumps out the physics constants pertaining to the part every 10 seconds. - /// - public class ModulePhysicsDebugger : PartModule - { - - [KSPField(guiActive = true, guiActiveEditor = true, guiName = "Physics Debugger")] - private double lastFixedUpdate; - - public void FixedUpdate() - { - if (HighLogic.LoadedSceneIsFlight && (Time.time - lastFixedUpdate) > 10) - { - lastFixedUpdate = Time.time; - - Transform rootT = part.vessel.rootPart.transform; - - StringBuilder sb = new StringBuilder(); - - float massTotal = 0; - Vector3 wtSum = Vector3.zero; - - foreach (Part p in part.vessel.parts) - { - sb.AppendLine(p.name + " position(wrt root)=" + rootT.InverseTransformPoint(p.transform.position).ToString("F5")); - - if (p.rigidbody != null) - { - massTotal += p.rigidbody.mass; - wtSum += p.rigidbody.mass * rootT.InverseTransformPoint(p.transform.TransformPoint(p.rigidbody.centerOfMass)); - sb.AppendLine(p.name + " mass=" + p.rigidbody.mass); - if (p.rigidbody.centerOfMass != Vector3.zero) - sb.AppendLine(p.name + " CoM offset=" + p.rigidbody.centerOfMass.ToString("F5")); - sb.AppendLine(p.name + " inertia tensor=" + p.rigidbody.inertiaTensor.ToString("F5") + " rotation=" + p.rigidbody.inertiaTensorRotation.ToStringAngleAxis("F5")); - - foreach(Joint j in p.gameObject.GetComponents()) - { - sb.AppendLine(p.name + " joint type=" + j.GetType() + " position=" + j.anchor.ToString("F5") + " force=" + j.breakForce + " torque=" + j.breakTorque); - } - } - - if (p.Modules.Contains("ModuleEngines")) - { - ModuleEngines mE = (ModuleEngines)p.Modules["ModuleEngines"]; - foreach (Transform t in mE.thrustTransforms) - { - sb.AppendLine(p.name + " thrust transform position=" + t.position.ToString("F5")); - } - } - } - if (massTotal > 0) - { - sb.AppendLine("CoM = " + (wtSum / massTotal).ToString("F5")); - } - Debug.Log(sb); - } - } - - } -} \ No newline at end of file diff --git a/Source/Utils/SystemUtils.cs b/Source/Utils/SystemUtils.cs index abfc5b7b..c7734751 100644 --- a/Source/Utils/SystemUtils.cs +++ b/Source/Utils/SystemUtils.cs @@ -6,7 +6,7 @@ using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIExtensions +namespace KSPAPIEL { public static class SystemUtils { diff --git a/Source/Utils/UnityUtils.cs b/Source/Utils/UnityUtils.cs deleted file mode 100644 index b4aad2da..00000000 --- a/Source/Utils/UnityUtils.cs +++ /dev/null @@ -1,256 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions -{ - - public static class UnityUtils - { - /// - /// Formats a quarternion as an axis and rotation angle about the axis. Useful for debugging quarternions. - /// - /// The quarternion - /// Number format for the constituent values - /// The formatted string - public static string ToStringAngleAxis(this Quaternion q, string format = "F3") - { - Vector3 axis; - float angle; - q.ToAngleAxis(out angle, out axis); - return "(axis:" + axis.ToString(format) + " angle: " + angle.ToString(format) + ")"; - } - - /// - /// Find decendant transform / game object with the specified name. - /// - /// Parent transform - /// Name to search for - /// If true, inactive transforms are ignored - /// - public static Transform FindDecendant(this Transform t, string name, bool activeOnly = false) - { - bool found; - return FindDecendant(t, name, activeOnly, out found); - } - - private static Transform FindDecendant(Transform t, string name, bool activeOnly, out bool found) - { - if (t.name == name && (!activeOnly || t.gameObject.activeSelf)) - { - found = true; - return t; - } - found = false; - if (activeOnly && !t.gameObject.activeInHierarchy) - return null; - - for (int i = 0; i < t.childCount; ++i) - { - Transform ret = FindDecendant(t.GetChild(i), name, activeOnly, out found); - if (found) - return ret; - } - - return null; - } - - /// - /// Returns the path between the parent and the child, with names of decendents in sequence separated - /// by '/'. The returned string could be used with parent.Find to get the child. - /// - /// Parent transform - /// Child transform - /// The path between - /// If the parent is not an ancestor of the child. - public static string PathToDecendant(this Transform parent, Transform child) - { - List inBetween = new List(); - for (Transform track = child; track != parent; track = track.parent) - { - inBetween.Add(track.name); - if (track.parent == null) - throw new ArgumentException("Parent transform is not ancestor of child", "parent"); - } - inBetween.Reverse(); - return string.Join("/", inBetween.ToArray()); - } - } - - - /// - /// Holds all the bits and pieces for a mesh without the checking code. - /// Useful when in the process of creating dynamic meshes. - /// - public class UncheckedMesh - { - public readonly int nVrt; - public readonly int nTri; - - public readonly Vector3[] verticies; - public readonly Vector3[] normals; - public readonly Vector4[] tangents; - public readonly Vector2[] uv; - public readonly int[] triangles; - - /// - /// Create a new unchecked mesh - /// - /// Number of vertexes - /// Number of triangles - public UncheckedMesh(int nVrt, int nTri) - { - this.nVrt = nVrt; - this.nTri = nTri; - - verticies = new Vector3[nVrt]; - normals = new Vector3[nVrt]; - tangents = new Vector4[nVrt]; - uv = new Vector2[nVrt]; - - triangles = new int[nTri * 3]; - } - - /// - /// Write the mesh data to the given mesh. - /// - /// Mesh to write into - /// Name for the mesh - public void WriteTo(Mesh mesh, string name = null) - { - mesh.Clear(); - if (name != null) - mesh.name = name; - mesh.vertices = verticies; - mesh.normals = normals; - mesh.tangents = tangents; - mesh.uv = uv; - mesh.triangles = triangles; - } - - /// - /// Create a new mesh object with the mesh data as current. - /// - /// Name of the mesh - /// The new mesh - public Mesh AsMesh(string name = null) - { - Mesh mesh = new Mesh(); - WriteTo(mesh, name); - return mesh; - } - - /// - /// Dump the mesh as a string, useful for debugging. - /// - /// Mesh as string - public string DumpMesh() - { - StringBuilder sb = new StringBuilder().AppendLine(); - for (int i = 0; i < verticies.Length; ++i) - { - sb - .Append(verticies[i].ToString("F4")).Append(", ") - .Append(uv[i].ToString("F4")).Append(", ") - .Append(normals[i].ToString("F4")).Append(", ") - .Append(tangents[i].ToString("F4")).AppendLine(); - } - sb.Replace("(", "").Replace(")", ""); - sb.AppendLine(); - - for (int i = 0; i < triangles.Length; i += 3) - { - sb - .Append(triangles[i]).Append(',') - .Append(triangles[i + 1]).Append(',') - .Append(triangles[i + 2]).AppendLine(); - } - - return sb.ToString(); - } - } - - /// - /// Be aware this will not prevent a non singleton constructor - /// such as `T myT = new T();` - /// To prevent that, add `protected T () {}` to your singleton class. - /// - /// As a note, this is made as MonoBehaviour because we need Coroutines. - /// - public abstract class Singleton : MonoBehaviour where T : MonoBehaviour - { - private static T _instance; - - // ReSharper disable once StaticFieldInGenericType - private static readonly object Lock = new object(); - - public static T Instance - { - get - { - if (applicationIsQuitting) - { - Debug.LogWarning("[Singleton] Instance '" + typeof(T) + - "' already destroyed on application quit." + - " Won'type create again - returning null."); - return null; - } - - lock (Lock) - { - if (_instance == null) - { - _instance = (T)FindObjectOfType(typeof(T)); - - if (FindObjectsOfType(typeof(T)).Length > 1) - { - Debug.LogError("[Singleton] Something went really wrong " + - " - there should never be more than 1 singleton!" + - " Reopenning the scene might fix it."); - return _instance; - } - - if (_instance == null) - { - GameObject singleton = new GameObject(); - _instance = singleton.AddComponent(); - singleton.name = "(singleton) " + typeof(T); - - DontDestroyOnLoad(singleton); - - Debug.Log("[Singleton] An instance of " + typeof(T) + - " is needed in the scene, so '" + singleton + - "' was created with DontDestroyOnLoad."); - } - else - { - Debug.Log("[Singleton] Using instance already created: " + - _instance.gameObject.name); - } - } - - return _instance; - } - } - } - - // ReSharper disable once StaticFieldInGenericType - private static bool applicationIsQuitting; - - /// - /// When Unity quits, it destroys objects in a random order. - /// In principle, a Singleton is only destroyed when application quits. - /// If any script calls Instance after it have been destroyed, - /// it will create a buggy ghost object that will stay on the Editor scene - /// even after stopping playing the Application. Really bad! - /// So, this was made to be sure we're not creating that buggy ghost object. - /// - public void OnDestroy() - { - applicationIsQuitting = true; - } - } - -} \ No newline at end of file diff --git a/Source/zzVersionChecker.cs b/Source/zzVersionChecker.cs index 689766e1..852a5ced 100644 --- a/Source/zzVersionChecker.cs +++ b/Source/zzVersionChecker.cs @@ -33,7 +33,7 @@ /*-----------------------------------------*\ | SUBSTITUTE YOUR MOD'S NAMESPACE HERE. | \*-----------------------------------------*/ -namespace KSPAPIExtensions +namespace KSPAPIEL { /** From 4181cfe46d704e5b677cbe2daca3cf0ac3e570e8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 21:50:22 -0700 Subject: [PATCH 02/18] Struck much of the readme. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 252deceb..e223c2b0 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Utility methods to determine relationships between parts, plus some debugging co Available is two extra tweakable controls, plus improvements to the stock tweakers. To use these you *must* include the KSIAPIUtils.dll in your project rather than just copying the code as there's an election process to ensure the latest version is being run. If backwards compatibility breaks, I will ensure that the user is warned to upgrade plugins. -## SI Formatting for label fields. +~~## SI Formatting for label fields. Just use an 'S' type format code and SI prefixes will be handled. @@ -43,9 +43,9 @@ Just use an 'S' type format code and SI prefixes will be handled. public float tankVolume = 0.0f; ```` -For this example, the tankVolume variable is in kL, and will be displayed with 3 sig figs of accuracy. +For this example, the tankVolume variable is in kL, and will be displayed with 3 sig figs of accuracy.~~ -## SI Formatting and dynamic updating for resources in the editor +~~## SI Formatting and dynamic updating for resources in the editor This happens transparently. You can change the amount and maxAmount variables in your PartResource object in the editor, and they will be updated in the tweaker. @@ -57,7 +57,7 @@ If you add and remove resources, you will need to force a refresh of the part tw UIPartActionWindow window = part.FindActionWindow(); if (window != null) window.displayDirty = true; -```` +````~~ ## UI_ChooseOption @@ -106,7 +106,7 @@ if incrementSmall is not set, then no button is visible in the control. If incre The slider is set to run between the smallest available increment, so in the above if the current value was 1.3, then the slider would run from 1.25 to 1.5. -# PartMessage +~~# PartMessage This is an improvement over the existing SendMessage system of Unity. It creates a publish/subscribe model for messages both within and between parts on a vessel. It will automatically wire message event senders and listeners in a PartModule or Part if they have the appropriate attributes. @@ -191,4 +191,4 @@ Note the two optional filtering properties. ## Advanced message management -You can filter messages, buffer them up and send them in one hit, and send them dynamically. I won't go into the full details in this document. Have a look at the source for more details. +You can filter messages, buffer them up and send them in one hit, and send them dynamically. I won't go into the full details in this document. Have a look at the source for more details.~~ From c29e8318f70e76b9cdf98adbd8065fcea41d6499 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 22:08:24 -0700 Subject: [PATCH 03/18] Added onEditorShipModified calls to Float/ScaleEdits when values have actually changed. --- Source/Tweakables/UIPartActionFloatEdit.cs | 7 +++++++ Source/Tweakables/UIPartActionScaleEdit.cs | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Source/Tweakables/UIPartActionFloatEdit.cs b/Source/Tweakables/UIPartActionFloatEdit.cs index 545f3fae..3ce63814 100644 --- a/Source/Tweakables/UIPartActionFloatEdit.cs +++ b/Source/Tweakables/UIPartActionFloatEdit.cs @@ -259,6 +259,13 @@ public override void UpdateItem() private void UpdateValueDisplay(float newValue) { + if (FieldInfo.incrementSlide != 0 && + Mathf.Abs(this.value - newValue) >= 0.5f * FieldInfo.incrementSlide + ) + { + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + this.value = newValue; // ReSharper disable once CompareOfFloatsByEqualityOperator if (FieldInfo.incrementSlide != 0) diff --git a/Source/Tweakables/UIPartActionScaleEdit.cs b/Source/Tweakables/UIPartActionScaleEdit.cs index 2961dd95..4a760e1d 100644 --- a/Source/Tweakables/UIPartActionScaleEdit.cs +++ b/Source/Tweakables/UIPartActionScaleEdit.cs @@ -218,9 +218,17 @@ public override void UpdateItem() private void UpdateValueDisplay(float newValue) { + float inc = GetIncrementSlide(); + + if (inc != 0 && + Mathf.Abs(this.value - newValue) >= 0.5 * inc) + { + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + this.value = newValue; // ReSharper disable once CompareOfFloatsByEqualityOperator - float inc = GetIncrementSlide(); + if (inc != 0) { float valueLow = FieldInfo.intervals [intervalNo]; From 9fd2f9a9f1eeec4f48b8b7a4822421abbd89752e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 22:15:31 -0700 Subject: [PATCH 04/18] Trimmed the readme more. --- README.md | 117 ++---------------------------------------------------- 1 file changed, 4 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index e223c2b0..4c60c7b3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -KSPAPIExtensions +KSPAPIEL ================ -Extensions and utilities for Kerbal Space Program +A smaller set of utilities for plugin developers for Kerbal Space Program + +Forked from KSPAPIExtensions by taniwha, swamp_ig, et al. This add in is useful for providing some functions that make interacting with the KSP API functionally nicer and with an improved interface. @@ -34,31 +36,6 @@ Utility methods to determine relationships between parts, plus some debugging co Available is two extra tweakable controls, plus improvements to the stock tweakers. To use these you *must* include the KSIAPIUtils.dll in your project rather than just copying the code as there's an election process to ensure the latest version is being run. If backwards compatibility breaks, I will ensure that the user is warned to upgrade plugins. -~~## SI Formatting for label fields. - -Just use an 'S' type format code and SI prefixes will be handled. - -````c# - [KSPField(guiActive = true, guiActiveEditor = true, guiName = "Volume", guiFormat = "S3+3", guiUnits = "L")] - public float tankVolume = 0.0f; -```` - -For this example, the tankVolume variable is in kL, and will be displayed with 3 sig figs of accuracy.~~ - -~~## SI Formatting and dynamic updating for resources in the editor - -This happens transparently. You can change the amount and maxAmount variables in your PartResource object in the editor, and they will be updated in the tweaker. - -The amount and max amount are displayed with four significant figures of accuracy. - -If you add and remove resources, you will need to force a refresh of the part tweaker window like this: - -````c# - UIPartActionWindow window = part.FindActionWindow(); - if (window != null) - window.displayDirty = true; -````~~ - ## UI_ChooseOption This allows the user to chose from a range of options. It's equivalent to a dropdown list only without the dropdown (dropdowns were difficult to do with the API). @@ -106,89 +83,3 @@ if incrementSmall is not set, then no button is visible in the control. If incre The slider is set to run between the smallest available increment, so in the above if the current value was 1.3, then the slider would run from 1.25 to 1.5. -~~# PartMessage - -This is an improvement over the existing SendMessage system of Unity. It creates a publish/subscribe model for messages both within and between parts on a vessel. It will automatically wire message event senders and listeners in a PartModule or Part if they have the appropriate attributes. - -There's also a set of common messages defined, so addon builders can send and listen for those events in their code. - -Once again, to use these you *must* include the KSIAPIUtils.dll in your project rather than just copying the code as there's an election process to ensure the latest version is being run. If backwards compatibility breaks, I will ensure that the user is warned to upgrade plugins. - -## Initialization of message system - -Any class or module that uses part messages must initialize the system. The easiest way is like this: - -````c# - public abstract class ProceduralAbstractShape : PartModule - { - public override void OnAwake() - { - base.OnAwake(); - PartMessageService.Register(this); - } -```` - -You need to do this in OnAwake, as the part variable needs to be initialized. - -## Part messages - -Event types are declared as delegates with a special marker interface: - -````c# - [PartMessage(isAbstract: true)] - public delegate void PartPhysicsChanged(); - - [PartMessageDelegate(typeof(PartPhysicsChanged))] - public delegate void PartMassChanged([UseLatest] float mass); -```` - -These two events are in the common library. There's no constraints on the argument list, however you do not need to have the sending part as an argument as that is handled. - -Note that the PartMassChanged message has a parent event - PartPhysicsChanged. If you ever raise a PartMassChanged message then any listeners for PartPhysicsChanged will also be informed. The isAbstract flag indicates this should not be sent directly as an event. Currently this is not enforced but may be in the future. - -The argument list for any parent should either be the same, or a truncation of the list for the child event. Truncation is handled gracefully. - -Note the [UseLatest] attribute for the PartMassChanged event. This indicates that if a whole pile of PartMassChanged events gets sent from the same source that differ only in their mass arguments, then these can be consolidated into one event where the mass is the last one recieved. - -````c# - [PartMessageDelegate(typeof(PartResourcesChanged))] - public delegate void PartResourceMaxAmountChanged(string resource, [UseLatest] double maxAmount); -```` - -Above is an example of where this is not the case, the resource name uniquely identifies a message, however multiple messages with the same source but differ only in the maxAmount can be consolidated. - -## Message sending - -In your PartModule and Part objects, you can just declare and use an event like so: - -````c# - [PartMessageEvent] - public event PartMassChanged MassChanged; - - private void UpdateMass(float value) - { - part.mass = mass; - MassChanged(mass); - } -```` - -If you can send the update message from some other source, see the code for details of how to do it. - -## Message receiving - -You can listen for messages by just declaring a method with a marker attribute: - -````c# - [PartMessageListener(typeof(PartMassChanged), scenes:GameSceneFilter.AnyEditor, relations:PartRelationship.Self)] - private void ChangeVolume(float volume) -```` - -Note the two optional filtering properties. - -*scenes:* this allows you to only listen in certain game scenes. Good ones to use are GameSceneFilter.AnyEditor for editor mode, and GameSceneFilter.Flight for flight mode. Note that messages can still get sent during the GetInfo phase of initialization, which is useful if you'd like to tweak the VAB icon. - -*relations:* If not specified, the default is to only listen for messages from the same part, however you can listen to messages from Parents, Siblings, Ancestors... others. - -## Advanced message management - -You can filter messages, buffer them up and send them in one hit, and send them dynamically. I won't go into the full details in this document. Have a look at the source for more details.~~ From 098b4115609793a889ed228013d523ecd38a5d1f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 22:16:55 -0700 Subject: [PATCH 05/18] Removed Dummy part because that shouldn't be necessary in the Lite version. --- 000Dummy.cfg | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 000Dummy.cfg diff --git a/000Dummy.cfg b/000Dummy.cfg deleted file mode 100644 index 4df8d534..00000000 --- a/000Dummy.cfg +++ /dev/null @@ -1,25 +0,0 @@ - -// This part is here so that database reloads will correctly run ModuleManager and PartMessageManager -// on the first defined part. If you don't have this then the first part has issues. - -PART { - name = dummyPartIgnore - module = Part - - // --- asset parameters --- - MODEL - { - model = ProceduralParts/Parts/cylinderTank - scale = 1,1,1 - } - - scale = 1 - rescaleFactor = 1 - - cost = 4000 - category = -1 - TechRequired = InaccessableNonExistentTech - entryCost = 15000 - subcategory = 0 - -} From 262870daa227372886ed875972a673d8c534d58f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 22:19:26 -0700 Subject: [PATCH 06/18] Fixed up version-gen to make KSPAPIEL code. --- version-gen | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/version-gen b/version-gen index a478f28c..8c13ffd3 100755 --- a/version-gen +++ b/version-gen @@ -28,9 +28,8 @@ gen_tagged_class_wrappers() { echo "using System.Collections.Generic;" echo "using System.Linq;" echo "using System.Reflection;" - echo "using DeftTech.DuckTyping;" echo "using UnityEngine;" - echo "namespace KSPAPIExtensions {" + echo "namespace KSPAPIEL {" echo " [KSPAddon(KSPAddon.Startup.Instantly, true)]" gen_tagged_class UIPartActionsExtendedRegistration gen_tagged_class UIPartActionResourceEditorImproved @@ -39,14 +38,6 @@ gen_tagged_class_wrappers() { gen_tagged_class UIPartActionChooseOption echo " [KSPAddon(KSPAddon.Startup.Instantly, true)]" gen_tagged_class CompatibilityChecker - gen_tagged_class OnEditorUpdateUtility - echo "}" - echo "namespace KSPAPIExtensions.PartMessage {" - gen_tagged_class ServiceImpl - gen_tagged_addon PartMessageServiceInitializer - gen_tagged_class PartMessageServiceInitializer - gen_tagged_class KnownPartMarker - gen_tagged_class ListenerFerramAerospaceResearch echo "}" } From 0ae2e4212037f4d8126033b230ca2258313d0f21 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 22:39:58 -0700 Subject: [PATCH 07/18] csproj changes --- Source/KSPAPIEL.csproj | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Source/KSPAPIEL.csproj b/Source/KSPAPIEL.csproj index da6abe0b..21ed99cb 100644 --- a/Source/KSPAPIEL.csproj +++ b/Source/KSPAPIEL.csproj @@ -12,6 +12,7 @@ 512 False + KSPAPIEL true @@ -25,18 +26,16 @@ AnyCPU 1607 true - KSPAPIEL none true ..\..\Build\KSPAPIExtensions\obj\ - . + ..\GameData\ TRACE prompt 4 true - KSPAPIExtensions @@ -69,15 +68,19 @@ ..\..\_KSPAssemblies\Assembly-CSharp.dll + False ..\..\_KSPAssemblies\System.dll + False ..\..\_KSPAssemblies\Assembly-CSharp-firstpass.dll + False ..\..\_KSPAssemblies\UnityEngine.dll + False \ No newline at end of file From e7a5bf1798e41ca8e9c9a56967d5e8c3cc1cddec Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 22:40:25 -0700 Subject: [PATCH 08/18] Avoid ArgumentExceptions when guiFormat strings are null. --- Source/Utils/MathUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Utils/MathUtils.cs b/Source/Utils/MathUtils.cs index aefd6aad..59dd917b 100644 --- a/Source/Utils/MathUtils.cs +++ b/Source/Utils/MathUtils.cs @@ -62,7 +62,7 @@ internal static string FormatSI(this SIPrefix pfx, double value, int sigFigs = 3 /// public static string ToStringExt(this double value, string format) { - if (format[0] == 'S' || format[0] == 's') + if (format.Length > 0 && (format[0] == 'S' || format[0] == 's')) { if (format.Length == 1) return ToStringSI(value, 0); From abe4a7e11d67efe262840713e365537de56dc2d3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 May 2015 22:46:58 -0700 Subject: [PATCH 09/18] Added the dll for short-term distribution. --- GameData/KSPAPIEL.dll | Bin 0 -> 44544 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 GameData/KSPAPIEL.dll diff --git a/GameData/KSPAPIEL.dll b/GameData/KSPAPIEL.dll new file mode 100755 index 0000000000000000000000000000000000000000..f0dd0b3d18df68abf7b4b8f9cecb2c2f419d51f7 GIT binary patch literal 44544 zcmeIb3w&HvwLiYjnVB=sOfr)+=_75Yw1H{T)}}9#Hf@upuh1t=-z~P2WYP{za?+Ve z+Yk~GKwbiZAWzYjr}gKHpWxN2a$5xzg?mBqDk|3^z7V{EqKIPq|E{&qnMaZ$`uBH# zpWp9y1GCQFYp=cbT5GSp_Up`a-OiiHK}1e`&YU6oJf8elFYukgAc}*N|2asHc%Po~ zd2Q{}Q#S8Uq$1r(b6+yn8EK7mb(!f%OFWY7>53$}BFi^4M>@^6_^g5g-!zN$#uY?s zH7C7&)s`)GYo|z`;@3Px_X2|l|IIoH5W(jZJ}g}X4_(QPl*rFds@F0Z2i3ois7U_L zKCP?}gWU3)dwFmjtIrV0x<44lS&pQTyGSQVgA?Ghb zw0BmjEtZavlHn8zp));*PtIRG(TQ2fRI(K~rGsUIkL3^{3A*}O$#{neLeUAj3BON_ zpj(!Y`S0BStrjRcsWfBX)X7DUw-A-Qmk6Hmcl~YEqj4gi?&$wBKb-w<<54+D@8c{# zB~Mg5+d&xm+)l{RGo5IH&!}AhMD#CQPsj~{=4l{m$8r(k6N;FFA<#s1UzJvh|0_|> zJh$SBArgu_aZmYB4lh#!X)JP!M%vCCKBk^U`wU}pLCbbLCbq@^$4Po}NHP;RgGP#H zJLyKWflv`Dzo7yORFo$76j1lQ6=lg*d5%@mUq1s%)sAC%L=Rt>PM0#y{2mmjaiL&f z2U7P|j7z4Z;&`Ro^)s^-Syz8pJ6+1S5fvvShqz+&3!s8+qwDy|GV0U-_WU;h!#Qht zwChnHilZx;rfiVW7G@PN&wxUYLMdNzvrgU7eMH6}WOr7c*89dY3qP|+wsRqT#W&Cd zu#k}h7jTOGrBW~JXIhI4Vbymo#1VN4@kDqFrLF^KpP~16A#bqI>@JmJ*K5#mFiS}9 zJBUJ`KBk|a#tMkVcw$};;Zw5*gf$VQ^wR5V&;|V_==8pBz?p6c!PRQ&K%_?vaIf(9 z9{|Q(5?#-te*MguGrHc#D03kYlc$**@EAyP^-#%a<0goh+6dGjqXZxI5>>GQtNJ^D zJA30}Rpb8xd!P-bwOVQhzcjR%P!Z_w1lm*kC6=R))8=Lr)_wwck+%+W`i+_i7{ z{HQGOL6j;BFlvB-i5TT8$f5S;rp-MlO)rIOL~B`yy%`-Wsu&Q_`6%k=l-|eQSgM>? zd0lB`8AmI2tju!>T#J3oa?Hx{sWw!;WW8=S;`t1Bt_-2mS|xDoTF=1kwc$7si-#KM z;Quoq1wRLX;c;LPL?-xcDCp{I2NJ>5=?Xf7E*_PTbw8ld&n0?ag5hanE-IwZ;XV`N zk3)crr(<}!c|1ig7Gkc*=Jl0EFGmH8Lg6K-@uktLf%A*c`rJ7sCj7yd{5gEch+YYj zD5A8_ECmp=WoTI-p;cuT2PI~vpW<`V%=tufd?I2yIvKbdag_r~INz zbBdgfyjmSOMMaq+G+ou-&qX}0;Ab+v;NHH<3v@NBYcZ|9jv#++4NovFnB8rFFL`=VEHEk<~Tk-?jU?Hs1UpjBS$eg;Stj_U72 zEsKcNv83g41l51Av<@Zh`)p|uszo|c3Yw2DZB3bon3csaud+4aIH?n+Io`*N=EE?G z?ZaUt|1KY9=RvF!=fNt&SIK^&V<^CvL&W-W5zPl5?JZ>iXddTQ$&FO#?vDNWlXpJfy0B z@-V>*LIooPR~WGQOg+G<69D-}C{Th&z{}0!%E{*d-7*R1DH03?M<}8&=ggh^+0hlT z%87n?7Da+ektxFzDGU{kP()!M{!`3r6h#V^A}E9+43q0Qr1MH6Pm!Wf(FjEp=In}C z7nTv3E!65&nth`Vx*-iHY7OJf$(IUc*| zZgx%l+3VGy5giK(y^sAItJGB-pE9ddy^p;H=+(o}+y_864MVd=KwmHn&EW)SEUI&P z>(OOEU~oRQczZt#h$nU5{ z17x^&=&BTp3MV?B>UTB3Kh^rc>E2-oZs+!ZHJ5X3V39`!a*wR6mH-ynjzA6OlMJ(f z?upj3(0t}Fhys=wOI4;0^TM$Kg$9=DYyw#oM=@FY0#+-m5Q{^L5Q#HwW72WfgAsY$ zjXL84HRuN9a%9`l$OL(q99bOnS(Nf$P8HQq1V~2?c2^Xk*_VUZS)|$JGayILvgi+a9kh3+-9vOpoq_eB))U?J~$B;_gEdk8-*r{ zfhhYcCr!|R=8|mBi~RN2Yp|z?Kd?MoT~>x$-dbs8*fSJ;C)3*sWnE%>4&z_Jh%>0VU8#WHSncR!=BqT#Xd9pEu=u&%2BYD2L}5su>~ZxzUQ;+MQ;fRioom~N zZRn&Q>1Y&xIbPBMe^!6>U*i%9sj_ubn=|3=%tLUz%tLSh&qH9mbxJdmG{aMXHFb-k=L!SDnxTSlziYUF@S^&h)jj(*?G*RM=ApE>&3h>U(VBBP&; z_=oCu&h*_IMqSjjCw$Ju3lOeUBWXSQl)mPESs`Qu`gT5R10wshb01L$uvdi!n=OH5%X;tg~{;F0x_)<^@^jJLxjJUGeD> zIUCQZ@>gU)#9|p(Gg(~c)VwiKw&TD~UVr;?8dbETSb_H(kANFkVO_*Zl`cKT+|@DI zGM+da0iKzIwXED0)GyBzs|we1kwp)khb?o_DYm(UP}aINh@`nDSB&nm4l(UBw~@@; zT+I*`dK7ai?>}7FZUCE=zYg^vKacF>96qi^r-CyZi|0R*l@AuoATxNI=&aqq6|(so z6H1;XYR$%sEcM`VzT|rF%%Qeq&t5&$z#dlnXQ{?sHlkWiA6AINotx9hjR~Jowgg)! zFlI$m?E?n-dY9-^gE3*1LCL|TN=Y2Gqkq@IKImEWZ-l#Nv_3VZHW%l1B+Oj}1bc;1 zkJUB3;9^r7mSc$lWyD}?p>gFs*j8KV9F1;FES0{q#?XyI6B*VVmTFa4U@JDDTCu^214b)%)%cJ*Lq%w3pBJm68Z1R+C4|sC;Lb50LvQykiFXh8O z=L@{k!&x(@7xJL?@=D)_D>f+wLhgGTxIe7FSuhNLxaM4phtV$C&e2PwQ`u?H(aTf< zs|B1t$SLkRG*Ed}^aFU5$wn)B6Os$DI-uyyNR2(M@v8*BGG2$f=rIIQ2ovIqR$~Aj zr>603$fZ6A@oOu;C@M={%14a~DCb}+?8reKud<%)S=zD=<+ zL%}G)t4nA&LzQo!8c1Vcfla-Osc&O`j1qhlJyEp?=Az2vb2xvJDk?l|1);hrz5}V~ zoeaRi7eOaoHmF!*Tq@L227IhjHBe(yT*MSO{P7JkO7Kw}^HdGg+7$22 zP%ujHiejNnu_r^pr~w9wVv$X;mnlBXtuShUL3BJ?iB16U8v}JVVT=jyVnRj8qu6a|if6h&Kxf>8qu6h*yF5zkOCYJh>FXs{{TnL<=z z)BuC5N&z6k1wewSR9>=+NoKfFa4Ud2`Vp`d-cj_n&zrmpSQ$0SeR$BQ6b~Ae z`lwx{z?(EYc}xmKE<{91`cNWmct*5wIf|l3naigI9gwG-6`K8}?6KLBtQWqyL6VDL#ipTR)OyjLLe&jpYX zMhNL8r8iWf(EJq09$+%G>w94)iv7tnm@59}r%{{@0d5roxVOuEFMyVpP;zg~=?N2P zCOl2EWE)mDBk~wup2!)c|1q+oXZ{z+j>!STRPOE_RH$KTDRVe?FTa1pPh*7+8@TX+ z-%!o30QBf**nAHI^!I=SQ`#XsY98S-y-!emjJd1-Fc<56{4oC~(;q?Vg%AkO)un$G z0(q{xLEJwNB^aq?&}AX_nuhXnl*|Q6PIllF1plI zTrd^$dq!Zu@_~RKDx(AkK`j#~<@q%j%MUhEX*-435Aa?LHw>p#Pkt|kWw7el@5R8j zcd;W~tY6B4l}?|&1W&jsxmDk;orE2AN`CcvxkS7^LC~77Cv`d zo!O%Ql{;39r~LB(dyk%b9`KL4V}&o}3I6}*j@1cX?mmY98_op~f$|jjJI)2%*1K`OJ%|HN`Wc?%Bp!2_&ryq5g>4VS10kT%SAgV=iLpECcy(dqAQe^F_B-k^ zaU*VIs8zw(?RWU}3gLth$P8;nr2_+B&CX1#rAz8ZZRaellpWa!P!2~yIzQk4v2pY6|1iB61>SSOiWc#*$M6_Cyz1t{i_1yeD+0^= zdIvS37~?U735U&4>a%a)G8QxLSdIpGN6G|78Qz1=k%W&zmOjlBhJ#;sF)IIr+mcLO zW$innlMvkH>Tg&HG~mq{%1~Wo`h&$sZIS=h>%G70+6um=(>kcgf0pp*5;#`b5~!4m zvh=~hhA3iD)#vmoqe}0n6x%C5*5#daD2KBFj-ol|71o^?4+6duV=uSw#NgSMnN1jv z@5Fp4weQ4ul;BQ`QD}t6${!#o+J@h7=lkR$nigTU#(-hI)`ggT&vI(1(eaL#VO+;z z63g0v$D3sX|A=lzX9nC3Tz4x^Gj?uAw<_m^pg#NZfxXr3$+;l2ZV1GSnb5w`f?K}p zYcsLIbeNa_blz+mwcp@mAIB-Vv9jCe@STU?kd=qvxR{3!(O?nZJ6r;B*Jkd#Mm-N# zT_c`hU6l+|dAK2z#5G&;^{~y8(Zfb$^so^bJ#56V_TV~Vw<^evO>}O|K~UxQ=c#m< zt1n5gFuC5J;|3fH>Q;jwX=35VFVf08);`&3%2qO`CWk)ZQ+7iHYs!8eIJw2f(Xz@? ze+?Fzc6;o6?mKeYbHn50&OSL-8@;`Z_@mZsgwm9}^{jQl-;d+5Ow8h$W~qr-S2pVP z6(2oeJ*bYJB=IYKoG4fE%*BvBiZcRVzYL+V3SEl(xBKdjR+RBQ+~T8_4eCLL8!(yN z5K1mkDFxZYcc9SWkXdipTUOahS0!l-rZS;CzqwfPS zZxB+}EqY^?l(p1F;}N7Rz6|e0iYsfXi`FAZS%q9u)>dggf|S+BC1s73_QOb_t4@|ge}aW-H>2?h_Lri5iVZuy6g6t4dn?8!|0Lz(a?3vtCihm1PmcB5 z-)c(zGfunY~$H)IfQjMSHbA7qr^dC5^V&}dV1GsV9! z1)~Jd3y-R3M7ED^J_E1nA3|Aj3DO$@LIe^X=G2IS1#NlluZ&xca?LwEL{E1@*} z2O|omWjMlrnBtxMXOv2L{*x{LYyR^wx~_^$y!$SA?{ zg;>?VN}D3Z6klfwMhQMz!4#`(iZoMT4dNSQl;HXPqiO&%q-v#yDV}5sMhQN89w?$w z0KYpr9gj+M8?V!O>jQ@kr5-{hEEfB(LKUNrp%~Qfmg6Pp^fBi8L2#8z2_TN=A^5$h zs(_~!en`n3=y8wvEf8C*IGMc<@Kh=^s&H&Ue4_}*7V~M4`D(CFA%|i9D>CN40o3yN zFe(l687?*!p2AdFV%3EWXbS3<;u|E-qS#+k&9&X;x4HIr0OUT-Z9XT3CCmw14R^r8 z=pOUCC~+@*5=^mM;yX8lW`Y_ar|vNS9fd<^U;@rBu_vW-o%q(|7)H1SU79`C%$#eM zlu%h4)*h@IZ}Q*+&}|zy$v5ca>z25XGQZCvxeH2*oi$Y;^M;&v1w1`*)U3rqlT@^!+gVHD0gdv=*6+1&tFV54xr9>7R@?s92WVdq%S2$}E?b#+cDNX#~9h$&F^hp$F zRlwwLa!{zeD)|7qdU&xal>WvswD|%kt%GVD_5KHQ8pdvpg%+OR%{6#|0b9tsm5$8! zP8~T_SxH?@6U&jTg&&hD*ZT(9f!XUy%^w1)=J^tC0p6G!LZLc1x7$dLrJ{tWw@I&Vg_fbZ;1^w52bRu|yI`j;Jg!-nTeBA^` z2<9)q`T<6ByX?;yTEvoJw7+CpMk`tu-&?Ei-8t>=1>y^b(aU%&gBRYZ%9dXNB%)78 zhTyv7581Iee>KV+@(n;;z9<-UMn5W+;Qi?X-~stU4)bN;N|)S(yrb0o6*7~X7X@>2 zsd_|C_y^OK)%%df%U~tZeip|uxypP6c%M;P2X5q;JTsH$QST@-Uqx~B*8qOqovBua zOMLS`GbIRG8M1)x_E_8t$su3y!>YQ&5goMj*XaV5fyeArB;>CiS=k%?hIzim;B|n6 z1BWBy7JE@>j9IE^AgPD*V|R?(HEw66Ay68)oEzZ2TB!+B(kGAIzQT!gnT23<2x)zC zSQY$M5ekF`C%W$Z zvP@k&6w7`L5=`4%T3T1YZJ##&2Ve^4H$Kd^Qmv2qE@hjiK8Z5F+vtBkFzyVyT)G`) z+b!1DG0kb?Q(1PWjZb7Ub_4}i9*@vN^hT6rk#^zN36fi2LtPp#+o5iC--J|ZNN}C^ zyki|*gx@i;YNlm-4Z-$CyR4IHT<>GqcSYIOD!r5C*#UV}>yyg#$&>fAVB0MQgB(!Kps51iBXBWbkgoGE%^iSY`izI$eGV{4Kl6|`2uZkf zhL_=TK!e&~z##p|%cXx7yyoN5nZEOVLE0wi1Ynp_K9>Jc7&J_OKpJz7f2==9xA~d= zWB&7fK6=!D0+?_6|AbP1fH_nPydc1BT^?YWKL;43X9G<0O5n{v5U*fx=^S{8K`Q}+ zbSp58biZIu3H*cLCkL5wADRx*1Hp%bLHeO!l7)=u&)X^qnHczgEN& z215+11l}29*}g9@5N7<+FiWx~ye(|dXTx{_Px}gBkX}Wpsg1!D@1xq{6Mi2Rm5{fT zW|jQORZ2?%gJhNrl?3Uiq`zIloI_(69vZ`?zEbAj2WZee@O0R!lg2{({q& z1yb*3A- zfck_|)0y%D>PM?^U*x{5T|n1Ju0(RXkz=0Igz{qKn9CqW$#J?E_VFY41ac>}1IRg$ ztMYT1om&v%To2tMW!J-pj^jPryAhO%h~FAKPM6b1r0fC7T}k&#?%R^P3ct_7JkNul z9H*=4E1kWzdko>-8Yg_e%OdCHwwUy5GV5ex9QgZ}nFh z*U^Q8DaSbG;3x-tzd*%v9(>QBI)RGAGO4@X;k4=+beAInntK65v_NP4VnB`73EZZ$ zH?)fkXOr_0k@KX`d`Hs%A?eZOA5HR)i(a1<_$7hg68L_=5bc4?tm)?+A$raKuYh|4 zv-J=?TFCIJLhLE&mBM;JSJ7U;5`j|%&ME2zrlE*SHwtEtz%GIP0lg7oGY+a;1Yq$0qwRfaLxhGO#*ib+$ZpWKvUo$fmaFqfWX@Y9uxRk zfnNnI*1qF?T654czo9#5lOJ0;YVj8W9`Tm}4)`YkUhQ`xihR(o?9bMI&3_&GVs8NB zgf@nr#&1vTz-kAK75~7MU;2$=rdeM470C0sG2GVbQich^bd}!VzN`WPgsvL;5^4#^GC z%Q!`G)Aj3^a)?fsebVEhx2@cxW7{<^UASJAeRzC3;@M4Bt^{R%`d2GAd+ZrRVCM!! z`RVbT+bg-R(Vl_@$USbAEkV5ieFG~qZtude3-}iqHgWDMIyrtab`<9~bM9t}jlJDd zKx-s7M2E(H(i5c3R_=1-3h5%rDSH;t9V$oPM(ag%ua*0umOXCfD- zCna|U{dD{^UkQCF$MPlgl;qf+tGp%j>LQkm?RgNnw0KRYK z9)+9}sBN(-dmP%9)3hZj_nhRGFXf!FQaOF8o^wj~a(YH`O81HMyp?0!C(%!oe&S1$ z>6=ryp0dPbdQRoA7hQmPaLPGSh9kLN&tz(o+!a(-HqjNKt8+XuLN`f{HF`pi(D$tz z_g@8Fb1rikqGRxo3R-&}=dPqLmVLo}VIi2e% zZKL#gUL0e#damrJo+wSr83EI2hUD1tzw=C|g*-`%<)?b5(`v~n%TK4*B*&f{^}dJh zmtC#$Gs zv2s4iMOJXx%`|@8Ehwu~xv}StyBE3TR@uyP4R&K?(n>@>?%gSAd zTq9j>NWaSd$zUW;+4_UdMagTdf(xXU4`gUM9 ztSDDac*?h%I;`AY^rSh;(U+e_Zv%;jeK?1aDg zVpL}3o|@qEx6mvr_p=G({H?Ue%AKBYuD^{Y@Trh^$VPvhDl@s={&rfJ$(jEBv_^7| zQ(5^H{sXjIl~MhK>--(`G0ELR73FvOyXZM9SA(2M^j>b^Hd<5upud|UncS!RX{wjp zEwr`#bN(aLW|i$JKjH7EF?&?KJ>^gNFQbT+J5c^@KgLfc_q_iK8nkkU%TM{=N6+n5 z^*-eOx&JD9JCpkj=J;q#m0el>NB@oVV2jG#R$dbLAbrZpeYCtha0^|g;-Ey#Tj?6f z{ah;zPIlZ%l-=(65FM~`cOiEhoe{57$lT9nYCZ^{(9Vyc00*i7?6cqznzv# znQHxZ+HB>x_1kHes;A8yKhbqNwOYBI<6rRJPL1Ldc6)0jcLn5}=f9oqv~sr~cL!~Z zuq;d&aNkMoD(6`@=F9rs)Me#XjVW>8O-Cg6HF~S`y`H=2DJz%uKB;|}X3L!OHM+ZC zDRK|xwEkiGbWZCZrl&HkFY@0*ujEjEgnpNyyugp0ig?`>bW7O<{*Thj($h+#qx6R4 zSfj$gQHn}WGi71m7_~}HQQk`(RvA;?OMF7}U4zeLegx>C7Xh90DxizL0%*{00X6D% zGrUyb-DiOj?8R&vMY#nS+(!vMH~qJ^IFj=*oPFd=&N4eiW*1GzBFdIzsif!PoI<1D z3w*b&>vUS^^R?EYk+V>TtqKa6r?SRK3019kw!`h%p=zYyCnY@+{=48uLPa?epNlyX zX8T_bsA0{XO@E`9F;hzz!&j^M>2|>%C}AC^;4ng?M}i&`DpH(zD^8LMiiMdamV^TCPkj zE`7%0kelXK9?RjZ(#%bxT%P5nis!q<+cGdc`=>_N;ru|Oh-j^RMp+=&o}AV#uTcIo z(&ozYy~53g6UdA6o9XzB$GSSMmD5T*y?9Q?XBkZ=-f2|>F2$M5G}lJlaK7%`2DsYQ zMlrh7l>off)kO)q&y}Qu^hMWUq))i|f&aPda_n1PcixFRv@g0Iq&sPy@i6Kt${oh% z=@?2ML#+GSS!fG6D(wd1hoRN%kqJ`d>^-A-_R(U^hK zU%6)k^CM#s(!X;r1-CQc=GB6pMs14L!irGWi_ z)3uvD2efMKe$PRzR{IJdV|ug(?FXJq(JouENn?1SHqASv?Lhh}?P1wbJxsh(y-?$s z;Gp)3_gUbX@^vclodW;KzMpB=YE8Z&@Vv2s zAN10`19(jPh3|~^1!?h^z&q(8-z2?>KH@)DpC*=kR(s0ds6VTn^0(;MYQJ)u`V9KH z|4HyXo03d0ypT>wM~I>@Qs^2x9APp_Q0iD3GEK-)n*7RAv187 zUP8Tr`vGqad|Z#wJ%NYydGzDJm-UeTMq~+{N z=N;1&{bDIi7x1a(V8JPrUX66Mes{qy^po`If>-tB^mxIa^|iwPBz?P}z%fOCy`b39 zq!$F2Y7KgCV60;;@e1WaomU?RX>pLfbail+BO(5#={tis#ng`kYaN=%)7Z$Zb~ zgG(IG6Z47bUkf%mxZNfP>%P&UX`FtZUI}h@yd}1MoyOpZ;22FP>~uU#Qw#3_&8))X zj+ow1*yu=TTMIu0%pPD4>iva}0DeH=F~H5z7rU(<`BdRcj%T&U3ti4O3v=9kuJdK> zhlT88KP{|vb_hN#@KS-j+TBIBIHzkzi|z*eY2m%jA^qVZ_Q0POe%yIn`%2MgoHy$~ zF8ZAFPW{cIM;z5S<~iv+rcVle-TAQ2oSW$W(2GcaIy8jzNu296(Nm#UoL|si3QczO z>aPL+1%0sK_rRYCz3n`ymxguMNj)0&xL(v2gbQ6)N-sRAKVC4=bv^o}%JrsFEf!vdex&MQv2?zhtGi;ufr)VCDB?YvNH zDgGwX$zsLf8P_L-<`IEk68N~lrvyH$y;0l(KX;TY)ieit+HxxszhU9`l)PX#wcfxf{X+EhQpo>U$?uKnjz5BDwIe!)G1I(%HcHU@ z0e6YU>$?(-*L+R1WektgmNDbp<>DLDG(Jb&jWU7{{fiR7UCxHi1rfFmukNR9Mpadc%{H=wBI6qz4j*HP1@Ul zw`v;B@9vP&4{HX}M>Rj-{aO*=6C&-CkUUIJi~Qde%=3bIK`<{1=GTIGT`*pqH4O=z zqO-mglCGBYd`UM*dIi$w;g@(9;%&1Dp5=jAxFg&NSmikeIM?$K;3B~{dQKv}M)1u7 zcX^%%rq$ys05q7x0Z+7`5%)(c=_P>STC?a;3-!tXyb2H5pfeiv<0*3@16L?bKivl%|&i6?j76DS;Fa zdV%!<_X->ocvRpCf%OG!=e+_41)eI97K6e;V7Roqz5H^RNx7Lrvy?+>I$q6u^k2l9u;^(;3c=G*P_!=jME7;R(k?) zy!LgpU#acGx^sr4=V%F}=V=GD4}yN1wop&&pVOb$-_Tu-laA*d6P;_F7dx+Tj&V(O zt#j>n9d-SOD`+e-4j6YBpEJH;JY&IM8o$N&rzW|h?$z!K-J9`WrDv{ZooAQlLN}cv z+>|;8dC%d71ze>5uz3b1_ixuE&N(s^M87CFAUHjT>=y-x1($yfeTw z<7pgp4?yz}ZjZw_eGgLvpDFkaf^+zH2=I31K?S1ql)`x#_RB5~Q_uw;K+vdEFQdp5)@2t{(qbDq5x_t8Y7Ut4PoX_pw&wT~H9+LMk4!1F_RdTBm9 zb3UtAx6W+q>4-1E&CbS#jZG`o(w3%;v1GcTHJvcKQY((6<6Uj>woUPUiBvinW8|Ld zJ?HP4w`YDH<)(Pb>`Au9SF|P4X0oXh|8mu#c-yF?Yhx|(j(4P3*+jZ{z-W>w$Q3)Y{Fhu?~?E;_u1LWVOz1fT@P%vxK9X=Utd3 zk0j3=c_kS$un^hIXsS|kZz>({+>%aoq-aC3EuM_GEl)@v#*)2z=ip_D&9o}k8Q;)y zalADx-I_vQ(z;lBzpB;Tv@scPPaF}l=GJ&ud}X2|9ZwSLt4I#TI(p)J_7J)UK0%4p z+E_ZCN^gxPg;yfAsi&(8uY!@;(bn9NfD~J~26b?cQ*(DRk&bWXf2^xL5$|YQ4{=mV zQ6;)s*T$0j;>*p$UC?-0PdW_&bFpG7#x!@vIy!_dH*X_FezIkXennjmfkTnnGzu3r~Rbgheb^`N_B zt?@?a0~gxbu#B2FuBMe8iGBOiv^g1TJpf|1Cp%$tJe`&|2pi$HiFAq@x_VV0$)+|W zt;#5C>Pn;&v5v$g+#vF_r3;2mv?pRM9q~r|vv`m$nP}-jXDP*JHOE_f;C;O`3$i99 z*tNBB%V{`wPj}AH058z3=-QWnsW$g^$JvtnT$waGSI4^AI^qO3PbWI#jI}V(fFtIi zID1egd|ED?i?aH@14h{wPw#2!N~L36t#OJUf~A@;gn1CgT8IZZI;$%!WQe?7sdh8j z$)>YXD-&I9Qn)WOtn(U|fUzmowa*%4igAk8B9QN`whhy0cCi0)te>?CV?LQ^&F8`+ zmF1$kCGC6m#5$+7##;BsXA{`9C0a2az)J-cQp%ubVD}W1>y9N^fvw_OLd#SdpoFszPXB*J0g^6)_Ro&9Di zo#D@k=HAXmRK#oX6_cV|QPUXpD(` zN#BBa^@7^=d2?pQv3* zG?`py&bFh}>Uc*tx&l371 z&?Uy}Kw!kuA#K9%l4cL4i8!ssB%-{IN#bp?SZPXGGe#cHnlmKKXBlKu;w$^&$<>Lr zws=>jCv63nCt?UpsdS<>MRjTo98WH3-LnUw2a91dIh*QQ)oc#`TC!&k@@e!#N=$)# zHPyAV)l~3h+oXU+C#CVvyV5jFN)jTc@hp#6hXF8cV^H?mJ?&+AZ^=^RUb8FOEF+E77--il?aZA4EaI4ntO;nFVUJvQ$s2h z?`-Mlh#4<+nEn;&in&VLx$1M3VTO z8-uN-7`RFimSM7|;Ed{AYjVu3lfUuEsE@vZ`l7gHW7{x1|6Bf>Gx=d@ly2_n?lzN% z4=e0-9JM3fCwTU=Ws$kTZj)l2V%5>)QOb3<@!)0#Shh7NYMT{@bc9LsKs>{}p_2!L z*~4AC*|b8U)uAkRs<*QMg8O(o;Jwzoza(_^NwW3H1FdX8|%IXAGsU~dA+VOH3#=^|sfgLG} z#dAHDd60#z)+q^^A!}HaA*OXHB$f;nB6CUc^0?(4L{hrR)#a5dcOaenZ$H!&FD4tXUD^0VjXXKV7a zwfWhG{Oq#)Y-4_Qd46_9es(1_V<^%ECe}O+-5jE_UERZ$x8n^Pu^F2Q!ncg%xzX(I z=|^z6L#Br`x~H=xo|NucWz7QA zE&?r&x595Bq|CH3LPQWAOR7_;)9PJWfwF;`Qat{!OykTZtbLPRQk%1x5DzmiH#=i6 zC8HZv*t9UKps>;-fmIQ5s!2Pha=e0R<4LG;0Bc5Tly2UijH_KCFRTzL5Lji^OxSjz zkw&bp#604sby$lq+ZEQDg6G+WF0%`he=^=Xo0s+2pJe7Kndg(T@5GAEp1gQQ>x$c} z4pw9>wj2`Pc$i|BFB|+=XEw%TXk!tPqn=tpC`7MBO*v6n){CVH_7JkoZL$to@@Cu3 z`{P~9R7?=Hcs9iv3DYvRc(N2qiEVLGwIA-T)MJ%rV~fs?0@P->fqHPBGgOyr73@=B zg?!soQw#PQjUA{#b(S-4j_t$lglp#0@+6Ek!G@&0*Eg&#Kdq|#<*Mp_&lSf0Q`hj$qWuCWfwCK)p6h0!Q7)i)>7 z9XV@!Hj<#&U#070Bi`MFRbzU#uv5z{E@omLdTe}k!jRgQK##YoL6F2E#d1( zn`x01lRE|tCwcUw_G7Z5eK>DvgWc>SfmCAO$^;gI@gvq50Y^mYZa+dwXOUoSylWpC zWnr*Yl??zEUK`q}xu*rIZJvN+YU8z9^=xbh_F+}og{7apP;X1@abCbJz3Fb4_c>_ z!&V-L97|@2v90yyC{=T+3i-l-h^e%+#Uho(<|`(Z+(YtX}xQ3zI4-0n|wn`vLD~ z)beJFEN>;vt3y7fR4AcQYm0noEEUm2Pq(FKCXEo7Nn(X|R$7@fmRXrJ)>xS|7Fd}yR#%xcmR6ZGmM57s7FC%vR#dX8 z$&0ntkzbxO+mkm>9eHj#N`2d-VOtE0bk06x6yo8Hp;BF5WBD8~DiEex({dCchvdBQ z$oPUfCYP%o^mU5IYgcP8ACf6A!S+feP>Ii5a84k{_rq2ha>i>fIn?Be6H8f?XQxov zxXRon$2e+QgL~jOwioPKE_>dw>RMZkoB}m{j9BETF+WTw%dbYk!ox;c4S)&UZS(^8 z-slBz#L){Dus)c15%Fc~in&LbB0B?tU^vR&uQoT4nkta6aadL`&&(AQ@8c6kvNp=h zEpoPjN!Yr2YO({e^dw9BZk^bU$ksA;v}(5RB7o0&2eRQ_%ke(XYwfzW2k?~y$s>8q@b(1<;G1kdly(0^-YHh=r>&Svev%5FRw<;rxutaH#cgB(jvPI~@t=LQE zO`w)Ca5lcwm?i6YvpaJHihXm=fhk`T+iPvBh8;BJJT}{uO*XT!8Ox9EY!Bd&sx#GU zCOZ->nOo;sZ5T}aSG5K225-dszDdH76tw~-$Rr$mBOSxADWWrVg# zqx|wQHb8U{Ff7WRQ5wd}C7?0EJOY-S>OqbD$Z_3PGy)B!1n2XarXyJMumU|ON#TVm z=8s!HX)Piuc2F;{X>f|;*^V-t_#i)vsPFHUvkmQaiwtbOv~bSKp(Iw2%aWFiDYVZf z2?pJnNv|b#+7cDz1+p|5V<2@JPT`Q>?E*yiWu$9Q$1se7m!q>J~8}=0n zHA{-EV>$C#2gpj$o6=5ZfH>gj4!C)!6Y_N7$CGXUV@wP~CkHc;De`@HEog97!ZM9g z+6-x#AEX?$)E;u|yN!^n6E`0WUGl87kB1Fc88iwAuK-#nd% z-;QAc*`1W*sKLnrf=$$h+J{kt2k>Ty#)D*}P$nEATVK%}Ky9weZoys5s>P&rmV!+m z120xqac=_EdBUAr;Q*ro1Up_U>f=lqDVB(bjtU%X;0VmBG-S<~H{MwQo;kCN&ZRl; zQb*RDYos7GbKtJ9WnT+S7o=0~~ir1o&KeTBHX1EqACg6f2D*EwsRrwF#e%lIE^e-E4bvHfC^y<}bkT zDCc&RbJ+8M9f$L+1EcTz+s0KBoS)B$JIJP7 zi`v6G&#G0dXjD#xJ8~R=z1HEg3ZKpRY{X{;KKKCx2Y&9r(TPtFK3({9BYjUN&18rvpj`nm!%2#1G&4Q4W5pbvfRWEb2$3&Ps-cA5y9_&!7>FL-r4aa=Zawb z;dcN(TH1B%J*}74V z?;_}7Slb)XCZaSn_%r1TNb68Bq;p~{{s|MP`)k-G^9bpaNOwVi!6yPmY%!|@hA1yr zomv42z}!WU?RAmx_X6Ky>y($5qfIoyT@D)-lZRc+15+B|Aq^acTw=RoIB-EQ;*!3V zo*fD?NeCn%e7_S4vg4xNV9-)7rrM7oYK;jEO+=|%p5Yj`KQx7_bB};Y5LOj6hBUV3 zJaiWP45^`+Of?hM)nQ50sz!xicxXPp4Z&Rt*VcmNVt~JJ%^C)EPVCq&JZs;M+~@hp zr56RzyUJN~bR>oqyO75t`h_cWw{$~=&J9%fEG*3KRd}Sv73o(0XnFlJNpt)3a0Ms| z&_xq7eU#~9kKM(dFuIssTJhyxgx^9DE`?wAg2P1d<+b7M#UwXtCulNQnIw zX9|LDi3$nXJ#u~+!W1v>;mzCsWeqUSY61hT{^%Q7K{jB2ojzMlAQa#f#W@4!7pPzsAN%6lor(TMKst+)i(}S{4Bn z2rHNr2A^>8V+F_y{BB2RJ3B)`Fu)>(hrWPXq3vA1An104V^;7+xONj_G*1t1Jv=2m z&+T@W!*c?Rt_ctRMWJh8j&NP2y96Y4;hBK^-(u^ja(NJ@3)gex)peJfmkJ(Ubrj)0 zr&|wr8DQ~1;pjAX2wb9P;pvhVT+D3-6bpLXPEXJi9()=;qWj%Wo?s^;k_5%B5^>l( z-6+?UY3kl=EN`^oHE76@`J^{}G;f{CLp?>4^wi2-G~AR0Q3y{-qpcAs_NS z5DX3?wuURpWThbE6rREZSdCUMdx)SzYNE+03%MQeJi87L@o>pfR|-Pe^3fDS(G7QS&^ZDRmtSwh8#_U z5%Ft|*cAY9dg$+mTe7!uClF3@ z^t>}z;PJ>@7{Jp@u3%_8V>ntA1mTyMjyN5_;!@`#c?5vdKTs;Hdrx9t1mD$(;LA0M!~0|M?*N1|S7ni}9FIii%&wk? zBNdIN#pf-Ex7D`Io!L@bJ7?y+_S)FYnmE9MdGqHksIFcRo6`zP-b5e{PaH)HZrd&4 z4?ild(Pq7LTQb&-FI#0^rop>BcyUD={KzbA@Jj2#&Hhf(tc~$xXCfu`$b3;Li_6@o z&4}37W-}tzE%Rdjd3O9aBVxUJy0|OegYQFh%!q8nOT>v*+~RJ=3&vfGTh2c}HotZL zg6i72^Ww8>YR71s^&-44*$qzko`-rxPV@ZJCA+tdn|JD3&!w-v?|Y9u)O({9)%oE$ znaxb%bR?V4O%-M6@X~p5c}K@Oey31mkH<3?eP=2tQlDGx|AO@3QYZnX2J!jtzyG%_ zz`u7c|MvD^z8%zWuj4Fc1fJde~^@z*l!qUxtvQzROCU^WQK>puyWbu2{I6X>&k2=BS18P16$S>jf3#>!Fx7t*;)Yk-`CSo zSDonESLT=$B($u>H}vf?zOv}TDcyQJ<@N|Pd@IGdZrFkE$09<*=XQLd#0UL+DjfqQ z;pU0ku{Gr5b$qu=PF{PN;t~Wj5~h|NHO%Jq!GQ!l2L3 literal 0 HcmV?d00001 From 1f628bed8a747b4c3d949754470a600b8f2e14ed Mon Sep 17 00:00:00 2001 From: toadicus Date: Wed, 20 May 2015 10:11:04 -0700 Subject: [PATCH 10/18] Trying to bring in the old code nicely~ --- .../{ => KSPAPIEL}/CommonAPI/GameObjectExt.cs | 0 Source/{ => KSPAPIEL}/KSPAPIEL.csproj | 0 Source/KSPAPIEL/Properties/AssemblyInfo.cs | 35 + .../{ => KSPAPIEL}/Properties/AssemblyInfo.in | 0 .../Tweakables/UIPartActionChooseOption.cs | 0 .../Tweakables/UIPartActionFloatEdit.cs | 0 .../Tweakables/UIPartActionScaleEdit.cs | 0 .../Tweakables/UIPartActionsExtended.cs | 0 Source/{ => KSPAPIEL}/Utils/KSPUtils.cs | 0 Source/{ => KSPAPIEL}/Utils/MathUtils.cs | 0 Source/{ => KSPAPIEL}/Utils/SystemUtils.cs | 0 Source/KSPAPIEL/tagged_classes.cs | 15 + Source/{ => KSPAPIEL}/zzVersionChecker.cs | 0 Source/KSPAPIExt/CommonAPI/GameObjectExt.cs | 22 + .../KSPAPIExt/CommonAPI/ResourceExtensions.cs | 169 +++ .../DuckTyping/AmbiguousTypesException.cs | 42 + .../DuckTyping/DelegateDuckCaster.cs | 119 ++ .../DuckTyping/DelegateDuckProxyType.cs | 249 ++++ Source/KSPAPIExt/DuckTyping/DuckProxyType.cs | 593 ++++++++ Source/KSPAPIExt/DuckTyping/DuckTyping.cs | 641 +++++++++ .../DuckTyping/EnumStringDuckCaster.cs | 120 ++ Source/KSPAPIExt/DuckTyping/FromTypeTree.cs | 66 + .../DuckTyping/GenericParameterDuckCaster.cs | 45 + Source/KSPAPIExt/DuckTyping/IDuckCaster.cs | 87 ++ Source/KSPAPIExt/DuckTyping/IDuckProxy.cs | 32 + .../KSPAPIExt/DuckTyping/IStaticDuckCaster.cs | 56 + .../DuckTyping/InterfaceDuckCaster.cs | 171 +++ .../DuckTyping/InterfaceDuckProxyType.cs | 882 ++++++++++++ .../DuckTyping/InterfaceStaticDuckCaster.cs | 99 ++ .../DuckTyping/ToTypeFromTypeTree.cs | 66 + Source/KSPAPIExt/DuckTyping/TypeKey.cs | 116 ++ Source/KSPAPIExt/KSPAPIExtensions.csproj | 109 ++ Source/KSPAPIExt/PartMessage/Attributes.cs | 137 ++ .../KSPAPIExt/PartMessage/CommonMessages.cs | 157 +++ .../PartMessage/PartMessageService.cs | 291 ++++ .../KSPAPIExt/PartMessage/zzImplementation.cs | 1192 +++++++++++++++++ Source/KSPAPIExt/Properties/AssemblyInfo.cs | 35 + .../Tweakables/UIPartActionChooseOption.cs | 217 +++ .../Tweakables/UIPartActionFloatEdit.cs | 396 ++++++ .../Tweakables/UIPartActionScaleEdit.cs | 332 +++++ .../Tweakables/UIPartActionsExtended.cs | 209 +++ Source/KSPAPIExt/Utils/ConfigNodeUtils.cs | 227 ++++ Source/KSPAPIExt/Utils/DebuggingUtils.cs | 204 +++ Source/KSPAPIExt/Utils/EngineWrapper.cs | 354 +++++ Source/KSPAPIExt/Utils/KSPUtils.cs | 339 +++++ Source/KSPAPIExt/Utils/MathUtils.cs | 393 ++++++ Source/KSPAPIExt/Utils/ParseUtils.cs | 166 +++ Source/KSPAPIExt/Utils/PhysicsDebugger.cs | 67 + Source/KSPAPIExt/Utils/SystemUtils.cs | 225 ++++ Source/KSPAPIExt/Utils/UnityUtils.cs | 256 ++++ Source/KSPAPIExt/tagged_classes.cs | 15 + Source/KSPAPIExt/zzVersionChecker.cs | 224 ++++ 52 files changed, 9170 insertions(+) rename Source/{ => KSPAPIEL}/CommonAPI/GameObjectExt.cs (100%) rename Source/{ => KSPAPIEL}/KSPAPIEL.csproj (100%) create mode 100644 Source/KSPAPIEL/Properties/AssemblyInfo.cs rename Source/{ => KSPAPIEL}/Properties/AssemblyInfo.in (100%) rename Source/{ => KSPAPIEL}/Tweakables/UIPartActionChooseOption.cs (100%) rename Source/{ => KSPAPIEL}/Tweakables/UIPartActionFloatEdit.cs (100%) rename Source/{ => KSPAPIEL}/Tweakables/UIPartActionScaleEdit.cs (100%) rename Source/{ => KSPAPIEL}/Tweakables/UIPartActionsExtended.cs (100%) rename Source/{ => KSPAPIEL}/Utils/KSPUtils.cs (100%) rename Source/{ => KSPAPIEL}/Utils/MathUtils.cs (100%) rename Source/{ => KSPAPIEL}/Utils/SystemUtils.cs (100%) create mode 100644 Source/KSPAPIEL/tagged_classes.cs rename Source/{ => KSPAPIEL}/zzVersionChecker.cs (100%) create mode 100644 Source/KSPAPIExt/CommonAPI/GameObjectExt.cs create mode 100644 Source/KSPAPIExt/CommonAPI/ResourceExtensions.cs create mode 100644 Source/KSPAPIExt/DuckTyping/AmbiguousTypesException.cs create mode 100644 Source/KSPAPIExt/DuckTyping/DelegateDuckCaster.cs create mode 100644 Source/KSPAPIExt/DuckTyping/DelegateDuckProxyType.cs create mode 100644 Source/KSPAPIExt/DuckTyping/DuckProxyType.cs create mode 100644 Source/KSPAPIExt/DuckTyping/DuckTyping.cs create mode 100644 Source/KSPAPIExt/DuckTyping/EnumStringDuckCaster.cs create mode 100644 Source/KSPAPIExt/DuckTyping/FromTypeTree.cs create mode 100644 Source/KSPAPIExt/DuckTyping/GenericParameterDuckCaster.cs create mode 100644 Source/KSPAPIExt/DuckTyping/IDuckCaster.cs create mode 100644 Source/KSPAPIExt/DuckTyping/IDuckProxy.cs create mode 100644 Source/KSPAPIExt/DuckTyping/IStaticDuckCaster.cs create mode 100644 Source/KSPAPIExt/DuckTyping/InterfaceDuckCaster.cs create mode 100644 Source/KSPAPIExt/DuckTyping/InterfaceDuckProxyType.cs create mode 100644 Source/KSPAPIExt/DuckTyping/InterfaceStaticDuckCaster.cs create mode 100644 Source/KSPAPIExt/DuckTyping/ToTypeFromTypeTree.cs create mode 100644 Source/KSPAPIExt/DuckTyping/TypeKey.cs create mode 100644 Source/KSPAPIExt/KSPAPIExtensions.csproj create mode 100644 Source/KSPAPIExt/PartMessage/Attributes.cs create mode 100644 Source/KSPAPIExt/PartMessage/CommonMessages.cs create mode 100644 Source/KSPAPIExt/PartMessage/PartMessageService.cs create mode 100644 Source/KSPAPIExt/PartMessage/zzImplementation.cs create mode 100644 Source/KSPAPIExt/Properties/AssemblyInfo.cs create mode 100644 Source/KSPAPIExt/Tweakables/UIPartActionChooseOption.cs create mode 100644 Source/KSPAPIExt/Tweakables/UIPartActionFloatEdit.cs create mode 100644 Source/KSPAPIExt/Tweakables/UIPartActionScaleEdit.cs create mode 100644 Source/KSPAPIExt/Tweakables/UIPartActionsExtended.cs create mode 100644 Source/KSPAPIExt/Utils/ConfigNodeUtils.cs create mode 100644 Source/KSPAPIExt/Utils/DebuggingUtils.cs create mode 100644 Source/KSPAPIExt/Utils/EngineWrapper.cs create mode 100644 Source/KSPAPIExt/Utils/KSPUtils.cs create mode 100644 Source/KSPAPIExt/Utils/MathUtils.cs create mode 100644 Source/KSPAPIExt/Utils/ParseUtils.cs create mode 100644 Source/KSPAPIExt/Utils/PhysicsDebugger.cs create mode 100644 Source/KSPAPIExt/Utils/SystemUtils.cs create mode 100644 Source/KSPAPIExt/Utils/UnityUtils.cs create mode 100644 Source/KSPAPIExt/tagged_classes.cs create mode 100644 Source/KSPAPIExt/zzVersionChecker.cs diff --git a/Source/CommonAPI/GameObjectExt.cs b/Source/KSPAPIEL/CommonAPI/GameObjectExt.cs similarity index 100% rename from Source/CommonAPI/GameObjectExt.cs rename to Source/KSPAPIEL/CommonAPI/GameObjectExt.cs diff --git a/Source/KSPAPIEL.csproj b/Source/KSPAPIEL/KSPAPIEL.csproj similarity index 100% rename from Source/KSPAPIEL.csproj rename to Source/KSPAPIEL/KSPAPIEL.csproj diff --git a/Source/KSPAPIEL/Properties/AssemblyInfo.cs b/Source/KSPAPIEL/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..7c18db2e --- /dev/null +++ b/Source/KSPAPIEL/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("KSPAPIExtensions")] +[assembly: AssemblyDescription("A set of utilities for plugin developers for Kerbal Space Program")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("swamp_ig and taniwha")] +[assembly: AssemblyProduct("KSPAPIExtensions")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e46ed9d3-b992-4f9a-8e9a-64536116a2c4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.7.4.9")] +[assembly: KSPAssembly("KSPAPIExtensions", 1, 7)] diff --git a/Source/Properties/AssemblyInfo.in b/Source/KSPAPIEL/Properties/AssemblyInfo.in similarity index 100% rename from Source/Properties/AssemblyInfo.in rename to Source/KSPAPIEL/Properties/AssemblyInfo.in diff --git a/Source/Tweakables/UIPartActionChooseOption.cs b/Source/KSPAPIEL/Tweakables/UIPartActionChooseOption.cs similarity index 100% rename from Source/Tweakables/UIPartActionChooseOption.cs rename to Source/KSPAPIEL/Tweakables/UIPartActionChooseOption.cs diff --git a/Source/Tweakables/UIPartActionFloatEdit.cs b/Source/KSPAPIEL/Tweakables/UIPartActionFloatEdit.cs similarity index 100% rename from Source/Tweakables/UIPartActionFloatEdit.cs rename to Source/KSPAPIEL/Tweakables/UIPartActionFloatEdit.cs diff --git a/Source/Tweakables/UIPartActionScaleEdit.cs b/Source/KSPAPIEL/Tweakables/UIPartActionScaleEdit.cs similarity index 100% rename from Source/Tweakables/UIPartActionScaleEdit.cs rename to Source/KSPAPIEL/Tweakables/UIPartActionScaleEdit.cs diff --git a/Source/Tweakables/UIPartActionsExtended.cs b/Source/KSPAPIEL/Tweakables/UIPartActionsExtended.cs similarity index 100% rename from Source/Tweakables/UIPartActionsExtended.cs rename to Source/KSPAPIEL/Tweakables/UIPartActionsExtended.cs diff --git a/Source/Utils/KSPUtils.cs b/Source/KSPAPIEL/Utils/KSPUtils.cs similarity index 100% rename from Source/Utils/KSPUtils.cs rename to Source/KSPAPIEL/Utils/KSPUtils.cs diff --git a/Source/Utils/MathUtils.cs b/Source/KSPAPIEL/Utils/MathUtils.cs similarity index 100% rename from Source/Utils/MathUtils.cs rename to Source/KSPAPIEL/Utils/MathUtils.cs diff --git a/Source/Utils/SystemUtils.cs b/Source/KSPAPIEL/Utils/SystemUtils.cs similarity index 100% rename from Source/Utils/SystemUtils.cs rename to Source/KSPAPIEL/Utils/SystemUtils.cs diff --git a/Source/KSPAPIEL/tagged_classes.cs b/Source/KSPAPIEL/tagged_classes.cs new file mode 100644 index 00000000..04749715 --- /dev/null +++ b/Source/KSPAPIEL/tagged_classes.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; +namespace KSPAPIEL { + [KSPAddon(KSPAddon.Startup.Instantly, true)] + internal class UIPartActionsExtendedRegistration_1_7_4_0 : UIPartActionsExtendedRegistration { } + internal class UIPartActionResourceEditorImproved_1_7_4_0 : UIPartActionResourceEditorImproved { } + internal class UIPartActionLabelImproved_1_7_4_0 : UIPartActionLabelImproved { } + internal class UIPartActionFloatEdit_1_7_4_0 : UIPartActionFloatEdit { } + internal class UIPartActionChooseOption_1_7_4_0 : UIPartActionChooseOption { } + [KSPAddon(KSPAddon.Startup.Instantly, true)] + internal class CompatibilityChecker_1_7_4_0 : CompatibilityChecker { } +} diff --git a/Source/zzVersionChecker.cs b/Source/KSPAPIEL/zzVersionChecker.cs similarity index 100% rename from Source/zzVersionChecker.cs rename to Source/KSPAPIEL/zzVersionChecker.cs diff --git a/Source/KSPAPIExt/CommonAPI/GameObjectExt.cs b/Source/KSPAPIExt/CommonAPI/GameObjectExt.cs new file mode 100644 index 00000000..d7422f8a --- /dev/null +++ b/Source/KSPAPIExt/CommonAPI/GameObjectExt.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using KSP.IO; + +namespace KSPAPIExtensions { + internal static class GameObjectExtension + { + internal static T AddTaggedComponent (this GameObject go) where T : Component + { + Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); + return (T)go.AddComponent(taggedType); + } + internal static T GetTaggedComponent (this GameObject go) where T : Component + { + Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); + return (T)go.GetComponent(taggedType); + } + } +} diff --git a/Source/KSPAPIExt/CommonAPI/ResourceExtensions.cs b/Source/KSPAPIExt/CommonAPI/ResourceExtensions.cs new file mode 100644 index 00000000..45fb7cdb --- /dev/null +++ b/Source/KSPAPIExt/CommonAPI/ResourceExtensions.cs @@ -0,0 +1,169 @@ +// Copyright 2014 Bill Currie +// This file is in the public domain (use it as you see fit). +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using KSP.IO; + +namespace KSPAPIExtensions { + /// + /// Extended resource attributes + /// + public class ExtendedResourceDefinition + { + PartResourceDefinition res_def; + + public bool isMineable = false; + public bool isHullResource = false; + public float volume = 5; // liters per unit + + public string name + { + get { + return res_def.name; + } + } + public Color color + { + get { + return res_def.color; + } + } + public float density + { + get { + return res_def.density; + } + } + public int id + { + get { + return res_def.id; + } + } + public bool isTweakable + { + get { + return res_def.isTweakable; + } + } + public ResourceFlowMode resourceFlowMode + { + get { + return res_def.resourceFlowMode; + } + } + public ResourceTransferMode resourceTransferMode + { + get { + return res_def.resourceTransferMode; + } + } + + public void Load (ConfigNode node) + { + string name = node.GetValue ("name"); + res_def = PartResourceLibrary.Instance.GetDefinition (name); + if (node.HasValue ("isMineable")) { + bool.TryParse (node.GetValue ("isMineable"), out isMineable); + } + if (node.HasValue ("isHullResource")) { + bool.TryParse (node.GetValue ("isHullResource"), + out isHullResource); + } + if (node.HasValue ("volume")) { + float.TryParse (node.GetValue ("volume"), out volume); + } + } + } + + /// + /// Extended resource attributes + /// + public static class PartResourceDefinitionExtension + { + static Dictionary resource_dict; + + /// + /// List of all resources as extended resources. The list is a private + /// copy. + /// + public static List resources + { + get { + if (resource_dict == null) { + Initialize (); + } + return new List (resource_dict.Values); + } + } + + /// + /// Find and load all resources as extended resources. + /// + static void Initialize () + { + var dbase = GameDatabase.Instance; + var resourceNodes = dbase.GetConfigNodes ("RESOURCE_DEFINITION"); + resource_dict = new Dictionary (); + foreach (var resource in resourceNodes) { + var res = new ExtendedResourceDefinition (); + res.Load (resource); + resource_dict.Add (res.name, res); + } + } + + /// + /// Get an extended resource. + /// + /// Resource name + /// An extended resource defintiion or null if not found + public static ExtendedResourceDefinition GetResource (string name) + { + if (resource_dict == null) { + Initialize (); + } + if (resource_dict.ContainsKey (name)) { + return resource_dict[name]; + } + return null; + } + + /// + /// Can the resource be mined? That is, is this a raw resource (eg, + /// Kethane, ore, etc). + /// + /// The resource defintion + /// Whether the resource can be mined. + public static bool isMineable (this PartResourceDefinition resdef) + { + return GetResource (resdef.name).isMineable; + } + + /// + /// Is this a resource needed for building ship "hulls". Used primarily + /// by Extraplanetary Launchpads. + /// + /// The resource defintion + /// Whether the resource is mull material. + public static bool isHullResource (this PartResourceDefinition resdef) + { + return GetResource (resdef.name).isHullResource; + } + + /// + /// The amount of space in liters taken up by one unit of the resource. + /// 5l/u is common (LiquidFuel, Oxidizer, Ore, Metal), though RealFuels + /// will likely set it to 1l/u. + /// + /// The resource defintion + /// The volume of a single unit of the resource in liters + public static float getVolume (this PartResourceDefinition resdef) + { + return GetResource (resdef.name).volume; + } + } + +} diff --git a/Source/KSPAPIExt/DuckTyping/AmbiguousTypesException.cs b/Source/KSPAPIExt/DuckTyping/AmbiguousTypesException.cs new file mode 100644 index 00000000..44739d15 --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/AmbiguousTypesException.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Exception thrown when two types cannot be distinguished from one another in a way that enables comparison. + /// + public sealed class AmbiguousTypesException : Exception + { + private Type m_Type1; + private Type m_Type2; + + /// + /// Constructs an object. + /// + /// First type. + /// Second type. + public AmbiguousTypesException(Type type1, Type type2) + : base("Types cannot be distinguished from one another in a way that enables comparison.\r\n\r\n" + + "Type 1: " + type1.FullName + "\r\n" + + "Type 2: " + type2.FullName + "\r\n\r\n" + + "NOTE: This exception indicates an internal problem in the duck typing library. Please let the author know if " + + "this exception is encountered. (http://www.deftflux.net) Developers: For more information on the types " + + "involved, refer to the Type1 and Type2 properties of this exception.") + { + m_Type1 = type1; + m_Type2 = type2; + } + + /// + /// Gets the first type. + /// + public Type Type1 { get { return m_Type1; } } + + /// + /// Gets the second type. + /// + public Type Type2 { get { return m_Type2; } } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/DelegateDuckCaster.cs b/Source/KSPAPIExt/DuckTyping/DelegateDuckCaster.cs new file mode 100644 index 00000000..2e73edcd --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/DelegateDuckCaster.cs @@ -0,0 +1,119 @@ +// DelegateDuckCaster.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Duck casting strategy used to cast two delegate types. + /// + internal class DelegateDuckCaster : IDuckCaster + { + private static ToTypeFromTypeTree s_ProxyTypeTree; + + /// + /// Initializes static members. + /// + static DelegateDuckCaster() + { + s_ProxyTypeTree = new ToTypeFromTypeTree(); + } + + /// + /// Constructs an object. + /// + public DelegateDuckCaster() + { } + + public bool ShouldCast(Type toType, Type fromType) + { + return (toType.IsSubclassOf(typeof(Delegate)) && fromType.IsSubclassOf(typeof(Delegate))); + } + + public bool ShouldUncast(object duck) + { + return (duck is Delegate) && (((Delegate)duck).Target is IDuckProxy); + } + + public bool CanCast(Type toType, Type fromType) + { + return ShouldCast(toType, fromType) && GetProxyType(toType, fromType).CanProxy(); + } + + public bool CouldUncast(Type originalType, Type castedType) + { + return CanCast(castedType, originalType); + } + + public void PrepareCast(Type toType, Type fromType) + { + GetProxyType(toType, fromType).Prepare(); + } + + public object Cast(Type toType, object duck) + { + return GetProxyType(toType, duck.GetType()).WrapDuck((Delegate)duck); + } + + public object Uncast(object duck) + { + object duckDelegateObj = ((IDuckProxy)(((Delegate)duck).Target)).UnwrapDuck(); + if (duckDelegateObj is Delegate) duck = duckDelegateObj; + + return duck; + } + + /// + /// Gets the DelegateDuckProxyType object for a given duck type. + /// + /// Type to be casted to. + /// Type of delegate to be casted. + /// The duck proxy type to use to cast or prepare for casting. + private static DelegateDuckProxyType GetProxyType(Type toType, Type fromType) + { + DelegateDuckProxyType proxyType = null; + + FromTypeTree fromTypeTree = null; + if (s_ProxyTypeTree.ContainsKey(toType)) + { + fromTypeTree = s_ProxyTypeTree[toType]; + + if (fromTypeTree.ContainsKey(fromType)) + { + proxyType = fromTypeTree[fromType]; + } + } + + if (proxyType == null) + { + proxyType = new DelegateDuckProxyType(toType, fromType); + + if (proxyType.CanProxy()) + { + if (fromTypeTree == null) + { + fromTypeTree = new FromTypeTree(); + s_ProxyTypeTree.Add(toType, fromTypeTree); + } + + fromTypeTree.Add(fromType, proxyType); + } + } + + return proxyType; + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/DelegateDuckProxyType.cs b/Source/KSPAPIExt/DuckTyping/DelegateDuckProxyType.cs new file mode 100644 index 00000000..1723d164 --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/DelegateDuckProxyType.cs @@ -0,0 +1,249 @@ +// DelegateDuckProxyType.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; +using System.IO; + +namespace DeftTech.DuckTyping +{ + /// + /// Class that performs the generation of duck proxy types for forwarding delegate calls. + /// + internal sealed class DelegateDuckProxyType + { + private delegate Delegate WrapDuckDelegate(Delegate duck); + + private Type m_ToDelegateType; + private Type m_FromDelegateType; + private Type m_ProxyType; + private WrapDuckDelegate m_WrapDuck; + private MethodInfo m_InvokeMethod; + + /// + /// Constructs an object. + /// + /// Type to be casted to. + /// Type of delegate to be casted. + public DelegateDuckProxyType(Type toDelegateType, Type fromDelegateType) + { + m_ToDelegateType = toDelegateType; + m_FromDelegateType = fromDelegateType; + m_ProxyType = null; + } + + /// + /// Determines whether a proxy can be generated for the types given in the constructor. + /// + /// If a proxy can be generated for the types given in the constructor, true; otherwise, false. + public bool CanProxy() + { + bool canProxy = false; + + if (m_ToDelegateType.IsSubclassOf(typeof(Delegate)) && m_FromDelegateType.IsSubclassOf(typeof(Delegate))) + { + MethodInfo variantMethod = m_ToDelegateType.GetMethod("Invoke"); + MethodInfo duckMethod = m_FromDelegateType.GetMethod("Invoke"); + + // Must have a compatible parameter list + ParameterInfo[] variantMethodParameters = variantMethod.GetParameters(); + ParameterInfo[] duckMethodParameters = duckMethod.GetParameters(); + + if (duckMethodParameters.Length == variantMethodParameters.Length) + { + bool areParameterTypesCompatible = true; + + for (int i = 0; i < duckMethodParameters.Length; i++) + { + if (!DuckTyping.AreTypesCompatible(variantMethodParameters[i].ParameterType, duckMethodParameters[i].ParameterType)) + { + areParameterTypesCompatible = false; + break; + } + } + + if (areParameterTypesCompatible) + { + // Must have a compatible return type + if (DuckTyping.AreTypesCompatible(variantMethod.ReturnType, duckMethod.ReturnType)) + { + canProxy = true; + } + } + } + } + + return canProxy; + } + + /// + /// Prepares by generating the proxy type for the types given in the constructor. + /// + public void Prepare() + { + GenerateProxyType(); + } + + /// + /// Calls the static WrapDuck method of the generated proxy type to initialize a new instance of + /// the proxy type with a given duck delegate and return a delegate to its invoke method. + /// + /// Duck delegate that the new proxy object should forward calls to. + /// A delegate of the type given in the constructor that calls the given duck delegate. + public Delegate WrapDuck(Delegate duck) + { + GenerateProxyType(); + + return m_WrapDuck(duck); + } + + /// + /// If a proxy type has not been generated yet, generates a proxy type that defines a method + /// matching the method signature of the to delegate type given in the constructor which forwards + /// calls to a delegate of the from type given in the constructor. + /// + private void GenerateProxyType() + { + if (m_ProxyType == null) + { + if (!CanProxy()) + { + throw new ArgumentException(m_FromDelegateType.FullName + " is not compatible with " + m_ToDelegateType.FullName + "."); + } + + AssemblyBuilderAccess assemblyBuilderAccess; + +//#if !DEBUG + assemblyBuilderAccess = AssemblyBuilderAccess.Run; +//#else + //assemblyBuilderAccess = AssemblyBuilderAccess.RunAndSave; +//#endif + + AppDomain domain = Thread.GetDomain(); + string assemblyName = "DuckDelegateProxy_" + m_ToDelegateType.Name.Replace(".", "_").Replace("+", "-") + "_" + m_FromDelegateType.Name.Replace(".", "_").Replace("+", "-") + ".dll"; + AssemblyBuilder assembly = domain.DefineDynamicAssembly(new AssemblyName(assemblyName), assemblyBuilderAccess); + ModuleBuilder module = assembly.DefineDynamicModule(assemblyName); + + TypeBuilder proxyType = module.DefineType("DuckDelegateProxy"); + proxyType.AddInterfaceImplementation(typeof(IDuckProxy)); + + // Define private field to hold a reference to the duck delegate to forward calls to. + FieldBuilder duckDelegateField = proxyType.DefineField("m_DuckDelegate", m_FromDelegateType, FieldAttributes.Private); + + // Define private .ctor(duckDelegateType duckDelegate) + ConstructorBuilder constructor = proxyType.DefineConstructor(MethodAttributes.Private, CallingConventions.HasThis, new Type[] { m_FromDelegateType }); + ILGenerator constructorIL = constructor.GetILGenerator(); + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Ldarg_1); + constructorIL.Emit(OpCodes.Stfld, duckDelegateField); + constructorIL.Emit(OpCodes.Ret); + + // Define Invoke method + MethodBuilder invokeMethod = ImplementInvokeMethod(proxyType, duckDelegateField); + + // Define public static Delegate Wrap(Delegate duck) + MethodBuilder wrapMethod = proxyType.DefineMethod("Wrap", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(Delegate), new Type[] { typeof(Delegate) }); + ILGenerator wrapMethodIL = wrapMethod.GetILGenerator(); + wrapMethodIL.Emit(OpCodes.Ldarg_0); + wrapMethodIL.Emit(OpCodes.Newobj, constructor); + wrapMethodIL.Emit(OpCodes.Ldftn, invokeMethod); + wrapMethodIL.Emit(OpCodes.Newobj, m_ToDelegateType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) })); + wrapMethodIL.Emit(OpCodes.Ret); + + // Define public object UnwrapDuck() - Implementation of IDuckProxy + MethodBuilder unwrapMethod = proxyType.DefineMethod("UnwrapDuck", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(object), new Type[0]); + ILGenerator unwrapMethodIL = unwrapMethod.GetILGenerator(); + unwrapMethodIL.Emit(OpCodes.Ldarg_0); + unwrapMethodIL.Emit(OpCodes.Ldfld, duckDelegateField); + unwrapMethodIL.Emit(OpCodes.Ret); + + // Bake it + m_ProxyType = proxyType.CreateType(); + m_WrapDuck = (WrapDuckDelegate)(Delegate.CreateDelegate(typeof(WrapDuckDelegate), m_ProxyType, wrapMethod.Name)); + m_InvokeMethod = m_ProxyType.GetMethod("Invoke"); + +//#if DEBUG + // If we're in debug mode, save the assembly so we can disassemble it if we want. + //assembly.Save(assemblyName); +//#endif + } + } + + /// + /// Implements the method of the proxy type that invokes the delegate given in its constructor. + /// + /// Type builder to define the invoke method in. + /// Field that stores a reference to the delegate to call. + /// The implemented invoke method. + private MethodBuilder ImplementInvokeMethod(TypeBuilder proxyType, FieldInfo duckDelegateField) + { + MethodInfo variantMethod = m_ToDelegateType.GetMethod("Invoke"); + MethodInfo duckMethod = m_FromDelegateType.GetMethod("Invoke"); + + ParameterInfo[] variantMethodParameters = variantMethod.GetParameters(); + ParameterInfo[] duckMethodParameters = duckMethod.GetParameters(); + + MethodBuilder proxyMethod = proxyType.DefineMethod("Invoke", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.HasThis, variantMethod.ReturnType, GetParameterTypes(variantMethodParameters)); + ILGenerator proxyMethodIL = proxyMethod.GetILGenerator(); + + // Emit IL to load the proxy instance, then load the value of its duck delegate field + proxyMethodIL.Emit(OpCodes.Ldarg_0); + proxyMethodIL.Emit(OpCodes.Ldfld, duckDelegateField); + + // Emit IL to load method arguments + for (int i = 0; i < variantMethodParameters.Length; i++) + { + // Emit IL to load the argument + proxyMethodIL.Emit(OpCodes.Ldarg, i + 1); + + // Emit IL to cast the argument if necessary + DuckTyping.EmitCastIL(proxyMethodIL, duckMethodParameters[i].ParameterType, variantMethodParameters[i].ParameterType); + } + + // Emit IL to call the delegate + proxyMethodIL.Emit(OpCodes.Callvirt, duckMethod); + + // If we are returning something... + if (duckMethod.ReturnType != typeof(void)) + { + // Emit IL to cast the return value if necessary + DuckTyping.EmitCastIL(proxyMethodIL, variantMethod.ReturnType, duckMethod.ReturnType); + } + + // Emit IL to return. + proxyMethodIL.Emit(OpCodes.Ret); + + return proxyMethod; + } + + /// + /// Utility to create an array of parameter types from an array of parameters. + /// + /// Array of parameters to return the parameter types of. + /// An array of parameter types of the given method. + private Type[] GetParameterTypes(ParameterInfo[] parameters) + { + Type[] parameterTypes = new Type[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].ParameterType; + } + + return parameterTypes; + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/DuckProxyType.cs b/Source/KSPAPIExt/DuckTyping/DuckProxyType.cs new file mode 100644 index 00000000..f9816f7b --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/DuckProxyType.cs @@ -0,0 +1,593 @@ +// DuckProxyType.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net + + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; + +namespace DeftTech.DuckTyping +{ + /// + /// Generic class that performs the generation of duck proxy types. + /// + /// Interface to cast to. + internal sealed class DuckProxyType + { + private delegate TInterface WrapDuckDelegate(object duck); + + private static Dictionary> s_ProxyTypes; + + /// + /// Initializes static members. + /// + static DuckProxyType() + { + s_ProxyTypes = new Dictionary>(); + } + + + private Type m_DuckType; + private Type m_ProxyType; + private WrapDuckDelegate m_WrapDuck; + + /// + /// Constructs an object. + /// + /// Type of object being casted. + private DuckProxyType(Type duckType) + { + m_DuckType = duckType; + m_ProxyType = null; + } + + + /// + /// If a proxy type has not been generated yet, generates a proxy type that implements + /// TInterface and forwards calls to an object of the type given in the constructor. + /// + private void GenerateProxyType() + { + if (m_ProxyType == null) + { + AppDomain domain = Thread.GetDomain(); + AssemblyBuilder assembly = domain.DefineDynamicAssembly(new AssemblyName("DuckProxy_" + typeof(TInterface).FullName.Replace(".", "_") + "_" + m_DuckType.FullName.Replace(".", "_")), AssemblyBuilderAccess.Run); + ModuleBuilder module = assembly.DefineDynamicModule("DuckProxy"); + + TypeBuilder proxyType = module.DefineType("DuckProxy"); + proxyType.AddInterfaceImplementation(typeof(IDuckProxy)); + proxyType.AddInterfaceImplementation(typeof(TInterface)); + + // Define private field to hold a reference to the duck object to forward calls to. + FieldBuilder duckField = proxyType.DefineField("m_Duck", m_DuckType, FieldAttributes.Private); + + // Define private .ctor(duckType duck) + ConstructorBuilder constructor = proxyType.DefineConstructor(MethodAttributes.Private, CallingConventions.HasThis, new Type[] { m_DuckType }); + ILGenerator constructorIL = constructor.GetILGenerator(); + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Ldarg_1); + constructorIL.Emit(OpCodes.Stfld, duckField); + constructorIL.Emit(OpCodes.Ret); + + // Define public static TInterface Wrap(object duck) + MethodBuilder wrapMethod = proxyType.DefineMethod("Wrap", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(TInterface), new Type[] { typeof(object) }); + ILGenerator wrapMethodIL = wrapMethod.GetILGenerator(); + wrapMethodIL.Emit(OpCodes.Ldarg_0); + if (m_DuckType.IsValueType) + { + wrapMethodIL.Emit(OpCodes.Box, m_DuckType); + } + wrapMethodIL.Emit(OpCodes.Newobj, constructor); + wrapMethodIL.Emit(OpCodes.Ret); + + // Define public object UnwrapDuck() - Implementation of IDuckProxy + MethodBuilder unwrapMethod = proxyType.DefineMethod("UnwrapDuck", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(object), new Type[0]); + ILGenerator unwrapMethodIL = unwrapMethod.GetILGenerator(); + unwrapMethodIL.Emit(OpCodes.Ldarg_0); + unwrapMethodIL.Emit(OpCodes.Ldfld, duckField); + if (m_DuckType.IsValueType) + { + unwrapMethodIL.Emit(OpCodes.Box, m_DuckType); + } + unwrapMethodIL.Emit(OpCodes.Ret); + + // Define all members of TInterface + MemberInfo[] members = typeof(TInterface).GetMembers(); + ProxyMemberDictionary proxyMembers = new ProxyMemberDictionary(); + foreach (MemberInfo member in members) + { + ImplementMember(proxyType, proxyMembers, duckField, member); + } + + m_ProxyType = proxyType.CreateType(); + m_WrapDuck = (WrapDuckDelegate)(Delegate.CreateDelegate(typeof(WrapDuckDelegate), m_ProxyType, wrapMethod.Name)); + } + } + + #region Proxy type member implementation methods + + /// + /// Implements an interface member in a duck proxy type using a given type builder. + /// If successful, the implemented member will be added to the given proxy member dictionary. + /// + /// Type builder for the duck proxy type. + /// Dictionary of members of the proxy type. + /// Field that holds a reference to the duck object to forward calls to. + /// The interface member to implement. + private void ImplementMember(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, MemberInfo interfaceMember) + { + if (interfaceMember.MemberType == MemberTypes.Method) + { + ImplementMethod(proxyType, proxyMembers, duckField, (MethodInfo)interfaceMember); + } + else if (interfaceMember.MemberType == MemberTypes.Property) + { + ImplementProperty(proxyType, proxyMembers, duckField, (PropertyInfo)interfaceMember); + } + else if (interfaceMember.MemberType == MemberTypes.Event) + { + ImplementEvent(proxyType, proxyMembers, duckField, (EventInfo)interfaceMember); + } + else + { + throw new NotSupportedException("Interface defines a member type that is not supported."); + } + } + + /// + /// Implements an interface method in a duck proxy type using a given type builder. + /// If successful, the implemented method will be added to the given proxy member dictionary. + /// + /// Type builder for the duck proxy type. + /// Dictionary of members of the proxy type. + /// Field that holds a reference to the duck object to forward calls to. + /// The interface method to implement. + private void ImplementMethod(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, MethodInfo interfaceMethod) + { + MethodInfo duckMethod = FindDuckMethod(interfaceMethod); + if (duckMethod == null) + { + throw new NotImplementedException("Duck type does not implement a method named \"" + interfaceMethod.Name + "\" with the same parameters and return type."); + } + + ParameterInfo[] interfaceMethodParameters = interfaceMethod.GetParameters(); + ParameterInfo[] duckMethodParameters = duckMethod.GetParameters(); + + MethodBuilder proxyMethod = proxyType.DefineMethod(interfaceMethod.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, interfaceMethod.ReturnType, GetParameterTypes(interfaceMethodParameters)); + ILGenerator proxyMethodIL = proxyMethod.GetILGenerator(); + + // Emit IL to load the proxy instance, then load the value of its duck field + proxyMethodIL.Emit(OpCodes.Ldarg_0); + proxyMethodIL.Emit(OpCodes.Ldfld, duckField); + + // Emit IL to load method arguments + for (int i = 0; i < interfaceMethodParameters.Length; i++) + { + // Emit IL to load the argument + proxyMethodIL.Emit(OpCodes.Ldarg, i + 1); + + // Emit IL to cast the argument if necessary + DuckTyping.EmitCastIL(proxyMethodIL, duckMethodParameters[i].ParameterType, interfaceMethodParameters[i].ParameterType); + } + + // Emit IL to call the method + proxyMethodIL.Emit(OpCodes.Callvirt, duckMethod); + + // If we are returning something... + if (duckMethod.ReturnType != typeof(void)) + { + // Emit IL to cast the return value if necessary + DuckTyping.EmitCastIL(proxyMethodIL, interfaceMethod.ReturnType, duckMethod.ReturnType); + } + + // Emit IL to return. + proxyMethodIL.Emit(OpCodes.Ret); + + // Add proxy method to proxy member dictionary + // (This is so that any associated properties or events can refer to it later) + proxyMembers.Add(duckMethod, proxyMethod); + } + + /// + /// Implements an interface property in a duck proxy type using a given type builder. + /// If successful, the implemented property will be added to the given proxy member dictionary. + /// + /// Type builder for the duck proxy type. + /// Dictionary of members of the proxy type. + /// Field that holds a reference to the duck object to forward calls to. + /// The interface property to implement. + private void ImplementProperty(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, PropertyInfo interfaceProperty) + { + PropertyInfo duckProperty = FindDuckProperty(interfaceProperty); + if (duckProperty == null) + { + throw new NotImplementedException("Duck type does not implement a property named \"" + interfaceProperty.Name + "\" with the same property type and index parameters."); + } + + PropertyBuilder proxyProperty = proxyType.DefineProperty(interfaceProperty.Name, PropertyAttributes.None, interfaceProperty.PropertyType, GetParameterTypes(interfaceProperty.GetIndexParameters())); + + // Get and set methods appear before associated properties in the member array; thus, they have + // already been defined and implemented. The proxy member dictionary is used to refer to these. + if (interfaceProperty.CanRead) + { + proxyProperty.SetGetMethod((MethodBuilder)(proxyMembers[duckProperty.GetGetMethod()])); + } + if (interfaceProperty.CanWrite) + { + proxyProperty.SetSetMethod((MethodBuilder)(proxyMembers[duckProperty.GetSetMethod()])); + } + + // Add proxy property to the proxy member dictionary + // (This is not really necessary, but good to keep things consistent) + proxyMembers.Add(duckProperty, proxyProperty); + } + + /// + /// Implements an interface event in a duck proxy type using a given type builder. + /// If successful, the implemented event will be added to the given proxy member dictionary. + /// + /// Type builder for the duck proxy type. + /// Dictionary of members of the proxy type. + /// Field that holds a reference to the duck object to forward calls to. + /// The interface event to implement. + private void ImplementEvent(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, EventInfo interfaceEvent) + { + EventInfo duckEvent = FindDuckEvent(interfaceEvent); + if (duckEvent == null) + { + throw new NotImplementedException("Duck type does not implement an event named \"" + interfaceEvent.Name + "\" with the same event handler type."); + } + + EventBuilder proxyEvent = proxyType.DefineEvent(interfaceEvent.Name, EventAttributes.None, interfaceEvent.EventHandlerType); + + // Associated methods appear before the event in the member array; thus, they have already been + // defined and implemented. The proxy member dictionary is used to refer to these. + proxyEvent.SetAddOnMethod((MethodBuilder)(proxyMembers[duckEvent.GetAddMethod()])); + proxyEvent.SetRemoveOnMethod((MethodBuilder)(proxyMembers[duckEvent.GetRemoveMethod()])); + proxyEvent.SetRaiseMethod((MethodBuilder)(proxyMembers[duckEvent.GetRaiseMethod()])); + MethodInfo[] otherDuckMethods = duckEvent.GetOtherMethods(); + foreach (MethodInfo otherDuckMethod in otherDuckMethods) + { + if (proxyMembers.ContainsKey(otherDuckMethod)) + { + proxyEvent.AddOtherMethod((MethodBuilder)(proxyMembers[otherDuckMethod])); + } + } + + // Add proxy event to the proxy member dictionary + // (This is not really necessary, but good to keep things consistent) + // proxyMembers.Add(duckEvent, proxyEvent); + // For some reason, EventBuilder does not inherit MemberInfo, so it cannot be added. + } + + #endregion + + #region Duck type implementation determination and search methods + + /// + /// Determines whether the type given in the constructor implements all the members of TInterface. + /// + /// If the type given in the constructor implements all the members of TInterface, true; otherwise, false. + private bool DuckImplementsAllMembers() + { + bool implementsAll = true; + + MemberInfo[] members = typeof(TInterface).GetMembers(); + foreach (MemberInfo member in members) + { + if (!DuckImplementsMember(member)) + { + implementsAll = false; + break; + } + } + + return implementsAll; + } + + /// + /// Determines whether the type given in the constructor implements a given interface member. + /// + /// + /// This method is used in determining whether an object of a given type can be duck typed to TInterface. + /// + /// Interface member to check for. + /// If the type given in the constructor implements the given interface member, true; otherwise, false. + private bool DuckImplementsMember(MemberInfo interfaceMember) + { + bool implementsMember; + + if (interfaceMember.MemberType == MemberTypes.Method) + { + implementsMember = (FindDuckMethod((MethodInfo)interfaceMember) != null); + } + else if (interfaceMember.MemberType == MemberTypes.Property) + { + implementsMember = (FindDuckProperty((PropertyInfo)interfaceMember) != null); + } + else + { + throw new NotSupportedException("Interface defines a member type that is not supported."); + } + + return implementsMember; + } + + /// + /// Finds the duck implementation for a given interface method. + /// + /// Interface method to find the implementation of. + /// If the type given in the constructor implements the given interface method, such method; otherwise, null. + private MethodInfo FindDuckMethod(MethodInfo interfaceMethod) + { + MethodInfo duckMethod = null; + + MethodInfo[] methods = m_DuckType.GetMethods(); + foreach (MethodInfo method in methods) + { + // Must be public and have the same name + if (method.IsPublic && method.Name == interfaceMethod.Name) + { + // Must have a compatible parameter list + ParameterInfo[] parameters = method.GetParameters(); + ParameterInfo[] interfaceMethodParameters = interfaceMethod.GetParameters(); + + if (parameters.Length == interfaceMethodParameters.Length) + { + bool areParameterTypesCompatible = true; + + for (int i = 0; i < parameters.Length; i++) + { + if (!DuckTyping.AreTypesCompatible(interfaceMethodParameters[i].ParameterType, parameters[i].ParameterType)) + { + areParameterTypesCompatible = false; + break; + } + } + + if (areParameterTypesCompatible) + { + // Must have a compatible return type + if (DuckTyping.AreTypesCompatible(interfaceMethod.ReturnType, method.ReturnType)) + { + duckMethod = method; + break; + } + } + } + } + } + + return duckMethod; + } + + /// + /// Finds the duck implementation for a given interface property. + /// + /// Interface property to find the implementation of. + /// If the type given in the constructor implements the given interface property, such property; otherwise, null. + private PropertyInfo FindDuckProperty(PropertyInfo interfaceProperty) + { + PropertyInfo duckProperty = null; + + PropertyInfo[] properties = m_DuckType.GetProperties(); + foreach (PropertyInfo property in properties) + { + // Must have the same name + if (property.Name == interfaceProperty.Name) + { + // Must have a compatible property type + if (DuckTyping.AreTypesCompatible(interfaceProperty.PropertyType, property.PropertyType)) + { + // Must implement the capabilities of the interface property + if ((property.CanRead || !interfaceProperty.CanRead) && (property.CanWrite || !interfaceProperty.CanWrite) + && (!interfaceProperty.CanRead || property.GetGetMethod().IsPublic) && (!interfaceProperty.CanWrite || property.GetSetMethod().IsPublic)) + { + // Must have a compatible index parameter list + ParameterInfo[] indexParameters = property.GetIndexParameters(); + ParameterInfo[] interfacePropertyIndexParameters = interfaceProperty.GetIndexParameters(); + + if (indexParameters.Length == interfacePropertyIndexParameters.Length) + { + bool areParameterTypesCompatible = true; + + for (int i = 0; i < indexParameters.Length; i++) + { + if (!DuckTyping.AreTypesCompatible(interfacePropertyIndexParameters[i].ParameterType, indexParameters[i].ParameterType)) + { + areParameterTypesCompatible = false; + break; + } + } + + if (areParameterTypesCompatible) + { + duckProperty = property; + break; + } + } + } + } + } + } + + return duckProperty; + } + + /// + /// Finds the duck implementation for a given interface event. + /// + /// Interface event to find the implementation of. + /// If the type given in the constructor implements the given interface event, such event; otherwise, null. + private EventInfo FindDuckEvent(EventInfo interfaceEvent) + { + EventInfo duckEvent = null; + + EventInfo[] events = m_DuckType.GetEvents(); + foreach (EventInfo _event in events) + { + // Must have the same name + if (_event.Name == interfaceEvent.Name) + { + // Must have a compatible event handler type + if (DuckTyping.AreTypesCompatible(interfaceEvent.EventHandlerType, duckEvent.EventHandlerType)) + { + duckEvent = _event; + break; + } + } + } + + return duckEvent; + } + + #endregion + + /// + /// Utility to create an array of parameter types from an array of parameters. + /// + /// Array of parameters to return the parameter types of. + /// An array of parameter types of the given method. + private Type[] GetParameterTypes(ParameterInfo[] parameters) + { + Type[] parameterTypes = new Type[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].ParameterType; + } + + return parameterTypes; + } + + + /// + /// Calls the static WrapDuck method of the generated proxy type to initialize a new instance of + /// the proxy type with a given duck object. + /// + /// Duck object that the new proxy object should forward calls to. + /// + /// An instance of the proxy type, which implements TInterface, initialized with the given duck + /// object. + /// + internal static TInterface WrapDuck(object duck) + { + DuckProxyType proxyType = GetProxyType(duck.GetType()); + + // Make sure the proxy type is generated before we try to call it. + proxyType.GenerateProxyType(); + + // Call wrap method in the proxy type to create an instance of the proxy for the duck and return it. + return proxyType.m_WrapDuck(duck); + } + + internal static bool ShouldCast(Type duckType) + { + return (typeof(TInterface).IsInterface && !duckType.IsInterface); + } + + /// + /// Determines whether a given type can be duck casted to TInterface. + /// + /// Type of object being casted. + /// If the given type can be duck casted to TInterface, true; otherwise, false. + internal static bool CanCast(Type duckType) + { + return ShouldCast(duckType) && new DuckProxyType(duckType).DuckImplementsAllMembers(); + } + + /// + /// Prepares for a cast by generating the proxy type for a given duck type. + /// + /// Type of object being casted. + internal static void PrepareCast(Type duckType) + { + GetProxyType(duckType).GenerateProxyType(); + } + + /// + /// Gets the DuckProxyType<TInterface> object for a given duck type. + /// + /// + /// First, the most general type in the inheritance chain of the given type that implements all the + /// members of TInterface is selected. Then if a proxy type has already been generated, it returns + /// it. Finally, if not, it creates a new object and adds it to the dictionary. + /// + /// Type of object being casted. + /// The duck proxy type to use to cast or prepare for casting. + private static DuckProxyType GetProxyType(Type duckType) + { + DuckProxyType proxyType = null; + + // Examine the duck type's inheritance chain. + List inheritanceChain = new List(); + Type baseType = duckType; + do + { + inheritanceChain.Add(baseType); + baseType = baseType.BaseType; + } + while (baseType != null); + + // Iterate the inheritance chain backwards (most general to most specialized) to find a proxy type + // that has already been generated. + for (int i = inheritanceChain.Count - 1; i >= 0; i--) + { + Type type = inheritanceChain[i]; + + if (s_ProxyTypes.ContainsKey(type)) + { + proxyType = s_ProxyTypes[type]; + break; + } + } + + if (proxyType == null) + { + // Iterate the inheritance chain backwards (most general to most specialized) to find the most + // general type that can be casted to TInterface. + for (int i = inheritanceChain.Count - 1; i >= 0; i--) + { + Type type = inheritanceChain[i]; + + if (CanCast(type)) + { + proxyType = new DuckProxyType(type); + break; + } + } + + if (proxyType == null) + { + // If proxyType is null, then DuckTyping.CanCast<>() returned false for every type in the + // inheritance chain. The following line will cause an exception to be thrown when + // GenerateProxyType() is called. The exception will indicate why the duck cannot be casted. + proxyType = new DuckProxyType(duckType); + } + else + { + // If we got to this point, the cast is valid and it is ok to add the proxy type to the dictionary. + s_ProxyTypes.Add(proxyType.m_DuckType, proxyType); + } + } + + return proxyType; + } + + /// + /// Class for keeping track of member builders while implementing proxy members. + /// The key is the duck member that is called by the proxy. The value is the proxy member that calls it. + /// + /// + /// This was defined in case it is decided to define a custom class instead of using Dictionary. + /// + internal class ProxyMemberDictionary : Dictionary + { } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/DuckTyping.cs b/Source/KSPAPIExt/DuckTyping/DuckTyping.cs new file mode 100644 index 00000000..d4ccdcd7 --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/DuckTyping.cs @@ -0,0 +1,641 @@ +// DuckTyping.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Reflection.Emit; +using System.Reflection; + +namespace DeftTech.DuckTyping +{ + /// + /// Class for casting objects using "duck typing". Casting will succeed if a given duck type implements + /// all the members of an interface even though it does not explicitly implement said interface at + /// compile time. Hence, implementation of interfaces is moved to runtime. Also supports delegate + /// casting. + /// + public static class DuckTyping + { + private static List s_DuckCasters; + private static List s_StaticDuckCasters; + + private static ToTypeFromTypeTree s_CanCastTree; + private static ToTypeFromTypeTree s_CanStaticCastTree; + + [ThreadStatic] private static Stack t_ToTypeStack; + [ThreadStatic] private static Stack t_FromTypeStack; + + /// + /// Initializes static members. + /// + static DuckTyping() + { + s_DuckCasters = new List(); + s_DuckCasters.Add(new InterfaceDuckCaster()); + s_DuckCasters.Add(new DelegateDuckCaster()); + s_DuckCasters.Add(new EnumStringDuckCaster()); + s_DuckCasters.Add(new EnumEnumDuckCaster()); + s_DuckCasters.Add(new GenericParameterDuckCaster()); + + s_StaticDuckCasters = new List(); + s_StaticDuckCasters.Add(new InterfaceStaticDuckCaster()); + + s_CanCastTree = new ToTypeFromTypeTree(); + + s_CanStaticCastTree = new ToTypeFromTypeTree(); + } + + private static Stack ToTypeStack + { + get + { + if (t_ToTypeStack == null) + { + t_ToTypeStack = new Stack(); + } + + return t_ToTypeStack; + } + } + + private static Stack FromTypeStack + { + get + { + if (t_FromTypeStack == null) + { + t_FromTypeStack = new Stack(); + } + + return t_FromTypeStack; + } + } + + /// + /// Casts an object using duck typing. + /// + /// + /// This method will use a normal cast if one is possible. + /// + /// Type to cast to. + /// Object to cast. + /// A T casting of the given duck object. + public static T Cast(object duck) + { + return (T)(Cast(typeof(T), duck)); + } + + /// + /// Casts an object using duck typing. + /// + /// + /// This method will use a normal cast if one is possible. + /// + /// Type to cast to. + /// Object to cast. + /// A casting of the given duck object to the given type. + public static object Cast(Type toType, object duck) + { + if (toType == null) throw new ArgumentNullException("toType"); + if (duck == null) return null; + + Type originalDuckType = duck.GetType(); + + duck = Uncast(duck); + + Type duckType = duck.GetType(); + + object proxy = null; + if (CanNormalCast(toType, duckType, false)) + { + proxy = duck; + } + else + { + bool casted = false; + foreach (IDuckCaster caster in s_DuckCasters) + { + if (caster.ShouldCast(toType, duckType)) + { + proxy = caster.Cast(toType, duck); + casted = true; + break; + } + } + + if (!casted) throw new InvalidCastException("A conversion from " + originalDuckType.FullName + " to " + toType.FullName + " does not exist."); + } + + return proxy; + } + + /// + /// Casts a static type to an object using duck typing. + /// + /// Type to cast to. + /// Static type to cast. + /// A casting of the given static type. + public static T StaticCast(Type staticType) + { + return (T)(StaticCast(typeof(T), staticType)); + } + + /// + /// Casts a static type to an object using duck typing. + /// + /// Type to cast to. + /// Static type to cast. + /// A casting of the given static type. + public static object StaticCast(Type toType, Type staticType) + { + object proxy = null; + + foreach (IStaticDuckCaster caster in s_StaticDuckCasters) + { + if (caster.ShouldStaticCast(toType, staticType)) + { + proxy = caster.StaticCast(toType, staticType); + break; + } + } + + if (proxy == null) throw new InvalidCastException("A conversion from static type " + staticType.FullName + " to an object of " + toType.FullName + " does not exist."); + + return proxy; + } + + /// + /// If the given object is a duck casted object, uncasts the object to retrieve the original duck object. + /// + /// Object that may be duck casted. + /// If the given object is duck casted, the original duck object; otherwise, the same object that was given. + public static object Uncast(object duck) + { + foreach (IDuckCaster caster in s_DuckCasters) + { + if (caster.ShouldUncast(duck)) + { + duck = caster.Uncast(duck); + } + } + + return duck; + } + + /// + /// Determines whether a given object can be casted to a given type. + /// + /// Type to cast to. + /// The object to cast. + /// If the given object can be casted to the given to type, true; otherwise, false. + public static bool CanCast(object duck) + { + return CanCast(typeof(T), duck.GetType()); + } + + /// + /// Determines whether a given object can be casted to a given type. + /// + /// Type to cast to. + /// The object to cast. + /// If the given object can be casted to the given to type, true; otherwise, false. + public static bool CanCast(Type toType, object duck) + { + return CanCast(toType, duck.GetType()); + } + + /// + /// Determines whether a type can be casted to another type. + /// + /// Type to cast to. + /// Type of object to be casted. + /// If an object of the given from type can be casted to the given to type, true; otherwise, false. + public static bool CanCast() + { + return CanCast(typeof(TTo), typeof(TFrom)); + } + + /// + /// Determines whether a type can be casted to type T. + /// + /// Type to cast to. + /// Type of object to be casted. + /// If an object of the given type can be casted to T, true; otherwise, false. + public static bool CanCast(Type duckType) + { + return CanCast(typeof(T), duckType); + } + + /// + /// Determines whether a type can be casted to another type. + /// + /// Type to cast to. + /// Type of object to be casted. + /// If an object of the given from type can be casted to the given to type, true; otherwise, false. + public static bool CanCast(Type toType, Type fromType) + { + bool canCast; + + FromTypeTree canCastFromTree; + if (s_CanCastTree.ContainsKey(toType)) + { + canCastFromTree = s_CanCastTree[toType]; + } + else + { + canCastFromTree = new FromTypeTree(); + s_CanCastTree.Add(toType, canCastFromTree); + } + + if (canCastFromTree.ContainsKey(fromType)) + { + canCast = canCastFromTree[fromType]; + } + else + { + canCast = CanNormalCast(toType, fromType, true); + if (!canCast) + { + foreach (IDuckCaster caster in s_DuckCasters) + { + if (caster.CanCast(toType, fromType)) + { + canCast = true; + break; + } + } + } + + if (!canCast) + { + foreach (IDuckCaster caster in s_DuckCasters) + { + if (caster.CouldUncast(toType, fromType)) + { + canCast = true; + break; + } + } + } + + canCastFromTree.Add(fromType, canCast); + } + + return canCast; + } + + /// + /// Determines whether a static type can be casted to another type. + /// + /// Type to cast to. + /// Static type to be casted. + /// If the given static type can be casted to the given to type, true; otherwise, false. + public static bool CanStaticCast(Type staticType) + { + return CanStaticCast(typeof(T), staticType); + } + + /// + /// Determines whether a static type can be casted to another type. + /// + /// Type to cast to. + /// Static type to be casted. + /// If the given static type can be casted to the given to type, true; otherwise, false. + public static bool CanStaticCast(Type toType, Type staticType) + { + bool canCast; + + FromTypeTree canCastFromTree; + if (s_CanStaticCastTree.ContainsKey(toType)) + { + canCastFromTree = s_CanStaticCastTree[toType]; + } + else + { + canCastFromTree = new FromTypeTree(); + s_CanStaticCastTree.Add(toType, canCastFromTree); + } + + if (canCastFromTree.ContainsKey(staticType)) + { + canCast = canCastFromTree[staticType]; + } + else + { + canCast = false; + foreach (IStaticDuckCaster caster in s_StaticDuckCasters) + { + if (caster.CanStaticCast(toType, staticType)) + { + canCast = true; + break; + } + } + + canCastFromTree.Add(staticType, canCast); + } + + return canCast; + } + + /// + /// Prepares for a duck casting operation by performing all the necessary overhead. + /// + /// Type to be casted to. + /// The object to be casted. + public static void PrepareCast(object duck) + { + PrepareCast(typeof(T), duck.GetType()); + } + + /// + /// Prepares for a duck casting operation by performing all the necessary overhead. + /// + /// Type to be casted to. + /// The object to be casted. + public static void PrepareCast(Type toType, object duck) + { + PrepareCast(toType, duck.GetType()); + } + + /// + /// Prepares for a duck casting operation by performing all the necessary overhead. + /// + /// Type to be casted to. + /// Type of object to be casted. + public static void PrepareCast() + { + PrepareCast(typeof(TTo), typeof(TFrom)); + } + + /// + /// Prepares for a duck casting operation by performing all the necessary overhead. + /// + /// Type to be casted to. + /// Type of object to be casted. + public static void PrepareCast(Type duckType) + { + PrepareCast(typeof(T), duckType); + } + + /// + /// Prepares for a duck casting operation by performing all the necessary overhead. + /// + /// Type to be casted to. + /// Type of object to be casted. + public static void PrepareCast(Type toType, Type fromType) + { + if (!CanNormalCast(toType, fromType, false)) + { + foreach (IDuckCaster caster in s_DuckCasters) + { + if (caster.ShouldCast(toType, fromType)) + { + caster.PrepareCast(toType, fromType); + break; + } + } + } + } + + /// + /// Prepares for a static duck casting operation by performing all the necessary overhead. + /// + /// Type to be casted to. + /// Static type to be casted. + public static void PrepareStaticCast(Type staticType) + { + PrepareStaticCast(typeof(T), staticType); + } + + /// + /// Prepares for a static duck casting operation by performing all the necessary overhead. + /// + /// Type to be casted to. + /// Static type to be casted. + public static void PrepareStaticCast(Type toType, Type staticType) + { + foreach (IStaticDuckCaster caster in s_StaticDuckCasters) + { + if (caster.ShouldStaticCast(toType, staticType)) + { + caster.PrepareStaticCast(toType, staticType); + break; + } + } + } + + /// + /// Determines whether two types are compatible. + /// + /// Type to be casted to. + /// Type of object to be casted. + /// Whether to also check the reverse of the given conversion when checking for an infinite loop. + /// If the given types are compatible, true; otherwise, false. + internal static bool AreTypesCompatible(Type toType, Type fromType, bool bidirectionalContext) + { + bool areCompatible = false; + + // This code prevents an infinite loop. ToTypeStack and FromTypeStack are thread static Type stacks that keep track of + // what conversions are currently being checked in this thread. If the given conversion is already being checked, we + // can assume that the types are compatible since they are as long as all the other members are. + + Type[] toTypes = ToTypeStack.ToArray(); + Type[] fromTypes = FromTypeStack.ToArray(); + + for (int i = 0; i < toTypes.Length; i++) + { + if ((toType == toTypes[i] && fromType == fromTypes[i]) || (bidirectionalContext && toType == fromTypes[i] && fromType == toTypes[i])) + { + areCompatible = true; + break; + } + } + + // If the current conversion is not in the stack, proceed with normal check: + if (!areCompatible) + { + ToTypeStack.Push(toType); + FromTypeStack.Push(fromType); + + areCompatible = CanCast(toType, fromType); + + ToTypeStack.Pop(); + FromTypeStack.Pop(); + } + + return areCompatible; + } + + /// + /// Determines whether two types are compatible. + /// + /// Type to be casted to. + /// Type of object to be casted. + /// If the given types are compatible, true; otherwise, false. + internal static bool AreTypesCompatible(Type toType, Type fromType) + { + return AreTypesCompatible(toType, fromType, false); + } + + /// + /// Assuming that the two types are compatible, scores how well the types match. 0 is the best + /// match, worse matches are less than 0. + /// + /// Type to be casted to. + /// Type of object to be casted. + /// A number indicating how well the given types match. 0 is the best match, worse matches are less than 0. + internal static int GetTypeCompatibilityScore(Type toType, Type fromType) + { + int score; + + if (toType == fromType) + { + // If the types are equal, the score is 0 (the best). + score = 0; + } + else + { + Type baseType = null; + Type subType = null; + + // Determine which type is the subclass of the other. + if (toType.IsSubclassOf(fromType)) + { + baseType = fromType; + subType = toType; + } + else if (fromType.IsSubclassOf(toType)) + { + baseType = toType; + subType = fromType; + } + + if (baseType != null && subType != null) + { + // If one of the types is the subclass of the other, the score is the additive inverse + // of how many levels down the inheritance chain the base class is. Thus, the closer + // the base class is to the subclass, the higher the score. + score = 0; + do + { + score--; + subType = subType.BaseType; + } + while (subType != baseType && subType != null); + + // I guess if one type is an interface that the other implements, IsSubclassOf will + // return true, but the interface will, of course, not be found in the type's + // inheritance chain. We want the score to be -1 in this case: + if (subType == null) + { + score = -1; + } + } + else + { + // If one of the types is not the subclass of the other, then, assuming that the types + // are compatible, they are so because one type is an interface that the other type + // implements or the types can be duck casted. In both cases, the inheritance is + // logically one level deep, so we will return a score of -1. + score = -1; + } + } + + return score; + } + + /// + /// Helps maintain consistent support for typing class members by centralizing the generation of IL + /// that converts types. Emits IL to perform whatever conversion is necessary from one type to + /// another. + /// + /// IL generator to use to emit the IL. + /// Type to be casted to. + /// Type of object to be casted. + internal static void EmitCastIL(ILGenerator il, Type toType, Type fromType) + { + if (toType != typeof(void) && fromType != typeof(void)) + { + if (CanNormalCast(toType, fromType, true)) + { + // Emit IL to box or unbox a value type if necessary + if (fromType.IsValueType && !toType.IsValueType) + { + il.Emit(OpCodes.Box, fromType); + } + else if (!fromType.IsValueType && toType.IsValueType) + { + il.Emit(OpCodes.Unbox_Any, toType); + } + + // Emit IL to cast the object if necessary + if (fromType != toType && !toType.IsValueType) + { + il.Emit(OpCodes.Castclass, toType); + } + } + else if (ShouldDuckCast(toType, fromType) || ShouldDuckCast(fromType, toType)) + { + // Emit IL to box a value type if necessary + if (fromType.IsValueType) + { + il.Emit(OpCodes.Box, fromType); + } + + // Emit IL to call the DuckTyping.Cast<> method + MethodInfo castMethod = typeof(DuckTyping).GetMethod("Cast", new Type[] { typeof(object) }).MakeGenericMethod(toType); + il.Emit(OpCodes.Call, castMethod); + } + else + { + // This exception should never be thrown + throw new NotImplementedException("EmitCastIL method does not know how to handle the type conversion. This exception indicates an inconsistency in the program; the AreTypesCompatible method accounts for a possible type conversion that has not been implemented in the EmitCastIL method."); + } + } + } + + /// + /// Determines whether a given type can be casted to another type using normal casting. + /// + /// Type to be casted to. + /// Type to be casted from. + /// If true, true will also be returned if normal casting is possible in reverse. + /// If the given type can normally be casted to the other given type, true; otherwise, false. + private static bool CanNormalCast(Type toType, Type fromType, bool bidirectional) + { + return toType == fromType || toType.IsAssignableFrom(fromType) || (bidirectional && fromType.IsAssignableFrom(toType)); + } + + /// + /// Determines whether duck typing should be used to perform the given cast. (As opposed to normal casting.) + /// + /// Type to be casted to. + /// Type of object being casted. + /// + private static bool ShouldDuckCast(Type toType, Type fromType) + { + bool shouldDuckCast = false; + foreach (IDuckCaster caster in s_DuckCasters) + { + if (caster.ShouldCast(toType, fromType)) + { + shouldDuckCast = true; + break; + } + } + + return shouldDuckCast; + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/EnumStringDuckCaster.cs b/Source/KSPAPIExt/DuckTyping/EnumStringDuckCaster.cs new file mode 100644 index 00000000..8659b79d --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/EnumStringDuckCaster.cs @@ -0,0 +1,120 @@ +// EnumStringDuckCaster.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Duck casting strategy used to cast between enums and strings. + /// + internal class EnumStringDuckCaster : IDuckCaster + { + /// + /// Constructs an object. + /// + public EnumStringDuckCaster() + { } + + public bool ShouldCast(Type toType, Type fromType) + { + return ((toType.IsSubclassOf(typeof(Enum)) && fromType == typeof(string)) || (toType == typeof(string) && fromType.IsSubclassOf(typeof(Enum)))); + } + + public bool ShouldUncast(object duck) + { + return false; + } + + public bool CanCast(Type toType, Type fromType) + { + return ShouldCast(toType, fromType); + } + + public bool CouldUncast(Type originalType, Type castedType) + { + return CanCast(castedType, originalType); + } + + public void PrepareCast(Type toType, Type fromType) + { } + + public object Cast(Type toType, object duck) + { + object casted; + + if (duck is string) + { + casted = Enum.Parse(toType, (string)duck); + } + else + { + casted = duck.ToString(); + } + + return casted; + } + + public object Uncast(object duck) + { + return duck; + } + } + + /// + /// Duck casting strategy used to cast between enums with the same name. + /// + internal class EnumEnumDuckCaster : IDuckCaster + { + /// + /// Constructs an object. + /// + public EnumEnumDuckCaster() + { } + + public bool ShouldCast(Type toType, Type fromType) + { + return toType.IsSubclassOf(typeof(Enum)) && fromType.IsSubclassOf(typeof(Enum)); + } + + public bool ShouldUncast(object duck) + { + return false; + } + + public bool CanCast(Type toType, Type fromType) + { + return ShouldCast(toType, fromType) && toType.FullName == fromType.FullName; + } + + public bool CouldUncast(Type originalType, Type castedType) + { + return CanCast(castedType, originalType); + } + + public void PrepareCast(Type toType, Type fromType) + { } + + public object Cast(Type toType, object duck) + { + return Enum.Parse(toType, duck.ToString()); + } + + public object Uncast(object duck) + { + return duck; + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/FromTypeTree.cs b/Source/KSPAPIExt/DuckTyping/FromTypeTree.cs new file mode 100644 index 00000000..ed5e289c --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/FromTypeTree.cs @@ -0,0 +1,66 @@ +// FromTypeTree.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Tree to refer to T objects by from type. + /// + /// Type of object to store. + internal class FromTypeTree + { + private Dictionary m_FromTypeTree; + + /// + /// Constructs an object. + /// + public FromTypeTree() + { + m_FromTypeTree = new Dictionary(); + } + + /// + /// Determines whether a T object exists for the given from type. + /// + /// From type to search for. + /// If a T object exists for the given from type, true; otherwise, false. + public bool ContainsKey(Type fromType) + { + return m_FromTypeTree.ContainsKey(fromType); + } + + /// + /// Adds a T object to the tree. + /// + /// From type for the object. + /// The object to add. + public void Add(Type fromType, T item) + { + m_FromTypeTree.Add(fromType, item); + } + + /// + /// Gets the T object for a given from type. + /// + /// From type for the object. + /// The object for the given from type. + public T this[Type fromType] + { + get { return m_FromTypeTree[fromType]; } + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/GenericParameterDuckCaster.cs b/Source/KSPAPIExt/DuckTyping/GenericParameterDuckCaster.cs new file mode 100644 index 00000000..7fb2e384 --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/GenericParameterDuckCaster.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + internal class GenericParameterDuckCaster : IDuckCaster + { + public GenericParameterDuckCaster() + { } + + public bool ShouldCast(Type toType, Type fromType) + { + return toType.IsGenericParameter && fromType.IsGenericParameter; + } + + public bool ShouldUncast(object duck) + { + return false; + } + + public bool CanCast(Type toType, Type fromType) + { + return ShouldCast(toType, fromType) && toType.GenericParameterPosition == fromType.GenericParameterPosition; + } + + public bool CouldUncast(Type originalType, Type castedType) + { + return false; + } + + public void PrepareCast(Type toType, Type fromType) + { } + + public object Cast(Type toType, object duck) + { + throw new InvalidOperationException("Cast should never be called since an object cannot exist of a type that is a generic parameter."); + } + + public object Uncast(object duck) + { + throw new InvalidOperationException("Uncast should never be called because ShouldUncast always returns false."); + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/IDuckCaster.cs b/Source/KSPAPIExt/DuckTyping/IDuckCaster.cs new file mode 100644 index 00000000..3e0c4580 --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/IDuckCaster.cs @@ -0,0 +1,87 @@ +// IDuckCaster.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Interface for strategies of duck casting. + /// + internal interface IDuckCaster + { + /// + /// Determines whether this duck caster should be used to perform the given cast. + /// + /// Type to be casted to. + /// Type of object to be casted. + /// If this duck caster should be used to perform the given cast, true; otherwise, false. + bool ShouldCast(Type toType, Type fromType); + + /// + /// Determines whether this duck caster needs to perform an uncasting operation before the + /// duck is casted to another type. + /// + /// + /// If the given object is a proxy generated by this strategy, true should be returned. This + /// prevents proxies to proxies being generated. If true is returned here, DuckTyping will + /// respond by calling Uncast. + /// + /// The object to be casted. + /// If this duck caster needs to perform an uncasting operation before the duck is casted to another type, true; otherwise, false. + bool ShouldUncast(object duck); + + /// + /// Determines whether a given cast is valid. + /// + /// Type to be casted to. + /// The object to be casted. + /// If the given cast is valid, true; otherwise, false. + bool CanCast(Type toType, Type fromType); + + /// + /// Determines whether a given uncast operation might be possible without knowing the actual casted object. + /// + /// Original type to be uncasted to. + /// The type of the casted object. + /// If the given uncast operation might be possible, true; otherwise, false. + bool CouldUncast(Type originalType, Type castedType); + + /// + /// Prepares for a given cast by performing all necessary overhead. + /// + /// Type to be casted to. + /// The object to be casted. + void PrepareCast(Type toType, Type fromType); + + /// + /// Performs a given cast. + /// + /// Type to be casted to. + /// The object to cast. + /// The casted object. + object Cast(Type toType, object duck); + + /// + /// Uncasts a given object. + /// + /// + /// This method should only be called if ShouldUncast returns true. + /// + /// The object to uncast. + /// The uncasted object. + object Uncast(object duck); + } +} diff --git a/Source/KSPAPIExt/DuckTyping/IDuckProxy.cs b/Source/KSPAPIExt/DuckTyping/IDuckProxy.cs new file mode 100644 index 00000000..2a1df7eb --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/IDuckProxy.cs @@ -0,0 +1,32 @@ +// IDuckProxy.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// All duck proxy types implement this interface. This interface should normally not be used by code + /// outside the duck typing library. + /// + public interface IDuckProxy + { + /// + /// Returns the duck object that the proxy is forwarding calls to. + /// + /// The duck object that the proxy is forwarding calls to. + object UnwrapDuck(); + } +} diff --git a/Source/KSPAPIExt/DuckTyping/IStaticDuckCaster.cs b/Source/KSPAPIExt/DuckTyping/IStaticDuckCaster.cs new file mode 100644 index 00000000..501322ac --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/IStaticDuckCaster.cs @@ -0,0 +1,56 @@ +// IStaticDuckCaster.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Interface for strategies of duck casting a static type. + /// + internal interface IStaticDuckCaster + { + /// + /// Determines whether this duck caster should be used to perform the given static cast. + /// + /// Type to be casted to. + /// Static type to be casted. + /// If this duck caster should be used to perform the given static cast, true; otherwise, false. + bool ShouldStaticCast(Type toType, Type staticType); + + /// + /// Determines whether a given static cast is valid. + /// + /// Type to be casted to. + /// Static type to be casted. + /// If the given static cast is valid, true; otherwise, false. + bool CanStaticCast(Type toType, Type staticType); + + /// + /// Prepares for a given static cast by performing all necessary overhead. + /// + /// Type to be casted to. + /// Static type to be casted. + void PrepareStaticCast(Type toType, Type staticType); + + /// + /// Performs a given static cast. + /// + /// Type to be casted to. + /// Static type to cast. + /// The casted static type. + object StaticCast(Type toType, Type staticType); + } +} diff --git a/Source/KSPAPIExt/DuckTyping/InterfaceDuckCaster.cs b/Source/KSPAPIExt/DuckTyping/InterfaceDuckCaster.cs new file mode 100644 index 00000000..04d8352e --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/InterfaceDuckCaster.cs @@ -0,0 +1,171 @@ +// InterfaceDuckCaster.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Duck casting strategy that casts an object to an interface. + /// + internal class InterfaceDuckCaster : IDuckCaster + { + private static ToTypeFromTypeTree s_ProxyTypeTree; + + /// + /// Initializes static members. + /// + static InterfaceDuckCaster() + { + s_ProxyTypeTree = new ToTypeFromTypeTree(); + } + + + /// + /// Constructs an object. + /// + public InterfaceDuckCaster() + { } + + public bool ShouldCast(Type toType, Type fromType) + { + return (toType.IsInterface /*&& !fromType.IsInterface*/); + } + + public bool ShouldUncast(object duck) + { + return (duck is IDuckProxy); + } + + public bool CanCast(Type toType, Type fromType) + { + return ShouldCast(toType, fromType) && new InterfaceDuckProxyType(toType, fromType, false).CanProxy(); + } + + public bool CouldUncast(Type originalType, Type castedType) + { + return CanCast(castedType, originalType); + } + + public void PrepareCast(Type toType, Type fromType) + { + GetProxyType(toType, fromType).Prepare(); + } + + public object Cast(Type toType, object duck) + { + return GetProxyType(toType, duck.GetType()).WrapDuck(duck); + } + + public object Uncast(object duck) + { + object uncasted = ((IDuckProxy)duck).UnwrapDuck(); + + if (uncasted != null) duck = uncasted; + + return duck; + } + + /// + /// Gets the InterfaceDuckProxyType object for a given cast. + /// + /// + /// First, the most general type in the inheritance chain of the given type that implements all the + /// members of TInterface is selected. Then if a proxy type has already been generated, it returns + /// it. Finally, if not, it creates a new object and adds it to the dictionary. + /// + /// Type to cast to. + /// Type of object being casted. + /// The duck proxy type to use to cast or prepare for casting. + private InterfaceDuckProxyType GetProxyType(Type interfaceType, Type duckType) + { + InterfaceDuckProxyType proxyType = null; + + FromTypeTree fromTypeTree = null; + + // Examine the duck type's inheritance chain. + List inheritanceChain = new List(); + Type baseType = duckType; + do + { + inheritanceChain.Add(baseType); + baseType = baseType.BaseType; + } + while (baseType != null); + + // Include interfaces in the inheritance chain. We don't know which order, but it will still help. + /*Type[] interfaces = duckType.GetInterfaces(); + foreach (Type _interface in interfaces) + { + inheritanceChain.Add(_interface); + }*/ + + if (s_ProxyTypeTree.ContainsKey(interfaceType)) + { + fromTypeTree = s_ProxyTypeTree[interfaceType]; + + // Iterate the inheritance chain backwards (most general to most specialized) to find a proxy type + // that has already been generated. + for (int i = inheritanceChain.Count - 1; i >= 0; i--) + { + Type type = inheritanceChain[i]; + + if (fromTypeTree.ContainsKey(type)) + { + proxyType = fromTypeTree[type]; + break; + } + } + } + + if (proxyType == null) + { + // Iterate the inheritance chain backwards (most general to most specialized) to find the most + // general type that can be casted to TInterface. + for (int i = inheritanceChain.Count - 1; i >= 0; i--) + { + Type type = inheritanceChain[i]; + + if (CanCast(interfaceType, type)) + { + proxyType = new InterfaceDuckProxyType(interfaceType, type, false); + break; + } + } + + if (proxyType == null) + { + // If proxyType is null, then DuckTyping.CanCast<>() returned false for every type in the + // inheritance chain. The following line will cause an exception to be thrown when + // GenerateProxyType() is called. The exception will indicate why the duck cannot be casted. + proxyType = new InterfaceDuckProxyType(interfaceType, duckType, false); + } + else + { + // If we got to this point, the cast is valid and it is ok to add the proxy type to the tree. + if (fromTypeTree == null) + { + fromTypeTree = new FromTypeTree(); + s_ProxyTypeTree.Add(interfaceType, fromTypeTree); + } + + fromTypeTree.Add(proxyType.DuckType, proxyType); + } + } + + return proxyType; + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/InterfaceDuckProxyType.cs b/Source/KSPAPIExt/DuckTyping/InterfaceDuckProxyType.cs new file mode 100644 index 00000000..5711acce --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/InterfaceDuckProxyType.cs @@ -0,0 +1,882 @@ +// InterfaceDuckProxyType.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; + +namespace DeftTech.DuckTyping +{ + /// + /// Class that performs the generation of duck proxy types for the dynamic implementation of interfaces. + /// + internal sealed class InterfaceDuckProxyType + { + private delegate object WrapDuckDelegate(object duck); + + private Type m_InterfaceType; + private Type m_DuckType; + private bool m_IsDuckStatic; + + private Type m_ProxyType; + private WrapDuckDelegate m_WrapDuck; + private object m_StaticProxy; + + /// + /// Constructs an object. + /// + /// Type to be casted to. + /// Type of object to be casted. + /// Whether or not the duck is a static class instead of an object instance. + public InterfaceDuckProxyType(Type interfaceType, Type duckType, bool isDuckStatic) + { + m_InterfaceType = interfaceType; + m_DuckType = duckType; + m_IsDuckStatic = isDuckStatic; + + m_ProxyType = null; + } + + /// + /// Gets the interface type to be casted to. + /// + public Type InterfaceType { get { return m_InterfaceType; } } + + /// + /// Gets the duck type to be casted. + /// + public Type DuckType { get { return m_DuckType; } } + + /// + /// Gets whether the duck is a static class instead of an object instance. + /// + public bool IsDuckStatic { get { return m_IsDuckStatic; } } + + /// + /// Determines whether a proxy can be generated for the types given in the constructor. + /// + /// If a proxy can be generated for the types given in the constructor, true; otherwise, false. + public bool CanProxy() + { + return DuckImplementsAllMembers(); + } + + /// + /// Prepares by generating the proxy type for the types given in the constructor. + /// + public void Prepare() + { + GenerateProxyType(); + } + + /// + /// Calls the static WrapDuck method of the generated proxy type to initialize a new instance of + /// the proxy type with a given duck object. + /// + /// Duck object that the new proxy object should forward calls to. + /// + /// An instance of the proxy type, which implements the interface type given in the constructor, initialized with + /// the given duck object. + /// + public object WrapDuck(object duck) + { + if (m_IsDuckStatic) throw new InvalidOperationException("Cannot call WrapDuck when IsDuckStatic is true."); + + // Make sure the proxy type is generated before we try to call it. + GenerateProxyType(); + + // Call wrap method in the proxy type to create an instance of the proxy for the duck and return it. + return m_WrapDuck(duck); + } + + /// + /// Gets an instance of the proxy type that forwards calls to the static members of the duck type. + /// + /// + /// An instance of the proxy type, which implements the interface type given in the constructor and forwards calls to + /// the static members of the duck type. + /// + public object GetStaticProxy() + { + if (!m_IsDuckStatic) throw new InvalidOperationException("Cannot call GetStaticProxy when IsDuckStatic is false."); + + GenerateProxyType(); + + return m_StaticProxy; + } + + /// + /// If a proxy type has not been generated yet, generates a proxy type that implements + /// the interface type given in the constructor and forwards calls to an object of the + /// duck type given in the constructor. + /// + private void GenerateProxyType() + { + if (m_ProxyType == null) + { + AssemblyBuilderAccess assemblyBuilderAccess; + +#if !DEBUG + assemblyBuilderAccess = AssemblyBuilderAccess.Run; +#else + assemblyBuilderAccess = AssemblyBuilderAccess.RunAndSave; +#endif + + AppDomain domain = Thread.GetDomain(); + string assemblyName = "DuckInterfaceProxy_" + m_InterfaceType.Name.Replace(".", "_").Replace("+", "-") + "_" + m_DuckType.Name.Replace(".", "_").Replace("+", "-") + ".dll"; + AssemblyBuilder assembly = domain.DefineDynamicAssembly(new AssemblyName(assemblyName), assemblyBuilderAccess); + ModuleBuilder module = assembly.DefineDynamicModule(assemblyName); + + TypeBuilder proxyType = module.DefineType("DuckProxy"); + proxyType.AddInterfaceImplementation(typeof(IDuckProxy)); + proxyType.AddInterfaceImplementation(m_InterfaceType); + + FieldBuilder duckField; + + ConstructorBuilder constructor; + ILGenerator constructorIL; + + MethodBuilder wrapMethod; + + if (!m_IsDuckStatic) + { + // Define private field to hold a reference to the duck object to forward calls to. + duckField = proxyType.DefineField("m_Duck", m_DuckType, FieldAttributes.Private); + + // Define private .ctor(duckType duck) + constructor = proxyType.DefineConstructor(MethodAttributes.Private, CallingConventions.HasThis, new Type[] { m_DuckType }); + constructorIL = constructor.GetILGenerator(); + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Ldarg_1); + constructorIL.Emit(OpCodes.Stfld, duckField); + // constructorIL is passed to other methods so that they can add code to the constructor. + // constructorIL.Emit(OpCodes.Ret); - This code should appear below after other methods are called. + + // Define public static object Wrap(object duck) + wrapMethod = proxyType.DefineMethod("Wrap", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(object), new Type[] { typeof(object) }); + ILGenerator wrapMethodIL = wrapMethod.GetILGenerator(); + wrapMethodIL.Emit(OpCodes.Ldarg_0); + if (m_DuckType.IsValueType) + { + wrapMethodIL.Emit(OpCodes.Box, m_DuckType); + } + wrapMethodIL.Emit(OpCodes.Newobj, constructor); + wrapMethodIL.Emit(OpCodes.Ret); + + // Define public object UnwrapDuck() - Implementation of IDuckProxy + MethodBuilder unwrapMethod = proxyType.DefineMethod("UnwrapDuck", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(object), new Type[0]); + ILGenerator unwrapMethodIL = unwrapMethod.GetILGenerator(); + unwrapMethodIL.Emit(OpCodes.Ldarg_0); + unwrapMethodIL.Emit(OpCodes.Ldfld, duckField); + if (m_DuckType.IsValueType) + { + unwrapMethodIL.Emit(OpCodes.Box, m_DuckType); + } + unwrapMethodIL.Emit(OpCodes.Ret); + + // Override public bool Equals() method + MethodBuilder equalsMethod = proxyType.DefineMethod("Equals", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(bool), new Type[] { typeof(object) }); + MethodInfo objectEqualsMethod = typeof(object).GetMethod("Equals", new Type[] { typeof(object) }); + proxyType.DefineMethodOverride(equalsMethod, objectEqualsMethod); + ILGenerator equalsMethodIL = equalsMethod.GetILGenerator(); + equalsMethodIL.Emit(OpCodes.Ldarg_0); + equalsMethodIL.Emit(OpCodes.Ldfld, duckField); + equalsMethodIL.Emit(OpCodes.Ldarg_1); + equalsMethodIL.Emit(OpCodes.Call, typeof(DuckTyping).GetMethod("Uncast", new Type[] { typeof(object) })); + equalsMethodIL.Emit(OpCodes.Callvirt, objectEqualsMethod); + equalsMethodIL.Emit(OpCodes.Ret); + + // Override public int GetHashCode() method + MethodBuilder getHashCodeMethod = proxyType.DefineMethod("GetHashCode", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(int), new Type[0]); + MethodInfo objectGetHashCodeMethod = typeof(object).GetMethod("GetHashCode", new Type[0]); + proxyType.DefineMethodOverride(getHashCodeMethod, objectGetHashCodeMethod); + ILGenerator getHashCodeMethodIL = getHashCodeMethod.GetILGenerator(); + getHashCodeMethodIL.Emit(OpCodes.Ldarg_0); + getHashCodeMethodIL.Emit(OpCodes.Ldfld, duckField); + getHashCodeMethodIL.Emit(OpCodes.Callvirt, objectGetHashCodeMethod); + getHashCodeMethodIL.Emit(OpCodes.Ret); + + // Override string ToString() method + MethodBuilder toStringMethod = proxyType.DefineMethod("ToString", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(string), new Type[0]); + MethodInfo objectToStringMethod = typeof(object).GetMethod("ToString", new Type[0]); + proxyType.DefineMethodOverride(toStringMethod, objectToStringMethod); + ILGenerator toStringMethodIL = toStringMethod.GetILGenerator(); + toStringMethodIL.Emit(OpCodes.Ldarg_0); + toStringMethodIL.Emit(OpCodes.Ldfld, duckField); + toStringMethodIL.Emit(OpCodes.Callvirt, objectToStringMethod); + toStringMethodIL.Emit(OpCodes.Ret); + } + else + { + duckField = null; + + // Define public .ctor() + constructor = proxyType.DefineConstructor(MethodAttributes.Public, CallingConventions.HasThis, new Type[0]); + constructorIL = constructor.GetILGenerator(); + + wrapMethod = null; + + // Define public object UnwrapDuck() - Implementation of IDuckProxy + MethodBuilder unwrapMethod = proxyType.DefineMethod("UnwrapDuck", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(object), new Type[0]); + ILGenerator unwrapMethodIL = unwrapMethod.GetILGenerator(); + unwrapMethodIL.Emit(OpCodes.Ldnull); + unwrapMethodIL.Emit(OpCodes.Ret); + } + + // Define all members of m_InterfaceType + ProxyMemberDictionary proxyMembers = new ProxyMemberDictionary(); + ImplementInterface(proxyType, proxyMembers, duckField, constructorIL, m_InterfaceType); + + // Now that we are done implementing members, finish constructor method body: + constructorIL.Emit(OpCodes.Ret); + + // Bake it + m_ProxyType = proxyType.CreateType(); + + if (!m_IsDuckStatic) + { + m_WrapDuck = (WrapDuckDelegate)(Delegate.CreateDelegate(typeof(WrapDuckDelegate), m_ProxyType, wrapMethod.Name)); + } + else + { + m_StaticProxy = Activator.CreateInstance(m_ProxyType); + } + +//#if DEBUG + // If we're in debug mode, save the assembly so we can disassemble it if we want. + //assembly.Save(assemblyName); +//#endif + } + } + + #region Proxy type member implementation methods + + private void ImplementInterface(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, Type interfaceType) + { + ImplementInterface(proxyType, proxyMembers, duckField, constructorIL, interfaceType, new List()); + } + + private void ImplementInterface(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, Type interfaceType, List implementedInterfaces) + { + if (!implementedInterfaces.Contains(interfaceType)) + { + MemberInfo[] members = interfaceType.GetMembers(); + foreach (MemberInfo member in members) + { + ImplementMember(proxyType, proxyMembers, duckField, constructorIL, member); + } + + implementedInterfaces.Add(interfaceType); + + Type[] subInterfaces = interfaceType.GetInterfaces(); + foreach (Type subInterfaceType in subInterfaces) + { + ImplementInterface(proxyType, proxyMembers, duckField, constructorIL, subInterfaceType, implementedInterfaces); + } + } + } + + /// + /// Implements an interface member in a duck proxy type using a given type builder. + /// If successful, the implemented member will be added to the given proxy member dictionary. + /// + /// Type builder for the duck proxy type. + /// Dictionary of members of the proxy type. + /// Field that holds a reference to the duck object to forward calls to. + /// IL generator to use to add code to the constructor if necessary. + /// The interface member to implement. + private void ImplementMember(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, MemberInfo interfaceMember) + { + if (interfaceMember.MemberType == MemberTypes.Method) + { + ImplementMethod(proxyType, proxyMembers, duckField, constructorIL, (MethodInfo)interfaceMember); + } + else if (interfaceMember.MemberType == MemberTypes.Property) + { + ImplementProperty(proxyType, proxyMembers, duckField, constructorIL, (PropertyInfo)interfaceMember); + } + else if (interfaceMember.MemberType == MemberTypes.Event) + { + ImplementEvent(proxyType, proxyMembers, duckField, constructorIL, (EventInfo)interfaceMember); + } + else + { + throw new NotSupportedException("Interface defines a member type that is not supported."); + } + } + + /// + /// Implements an interface method in a duck proxy type using a given type builder. + /// If successful, the implemented method will be added to the given proxy member dictionary. + /// + /// Type builder for the duck proxy type. + /// Dictionary of members of the proxy type. + /// Field that holds a reference to the duck object to forward calls to. + /// IL generator to use to add code to the constructor if necessary. + /// The interface method to implement. + private void ImplementMethod(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, MethodInfo interfaceMethod) + { + MethodInfo duckMethod = FindDuckMethod(interfaceMethod); + if (duckMethod == null) + { + throw new NotImplementedException("Duck type does not implement a method named \"" + interfaceMethod.Name + "\" with compatible parameters and return type."); + } + + if (!duckMethod.IsSpecialName || (!duckMethod.Name.StartsWith("add_") && !duckMethod.Name.StartsWith("remove_"))) + { + ParameterInfo[] interfaceMethodParameters = interfaceMethod.GetParameters(); + ParameterInfo[] duckMethodParameters = duckMethod.GetParameters(); + + MethodBuilder proxyMethod = proxyType.DefineMethod(interfaceMethod.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, interfaceMethod.ReturnType, GetParameterTypes(interfaceMethodParameters)); + + GenericTypeParameterBuilder[] genericParameters; + + if (interfaceMethod.IsGenericMethodDefinition) + { + Type[] interfaceMethodGenericArguments = interfaceMethod.GetGenericArguments(); + string[] genericParameterNames = new string[interfaceMethodGenericArguments.Length]; + for (int i = 0; i < interfaceMethodGenericArguments.Length; i++) + { + genericParameterNames[i] = interfaceMethodGenericArguments[i].Name; + } + genericParameters = proxyMethod.DefineGenericParameters(genericParameterNames); + } + else + { + genericParameters = new GenericTypeParameterBuilder[0]; + } + + ILGenerator proxyMethodIL = proxyMethod.GetILGenerator(); + + // Emit IL to load the duck object if the method is not static + if (!duckMethod.IsStatic) + { + // Emit IL to load the proxy instance, then load the value of its duck field + proxyMethodIL.Emit(OpCodes.Ldarg_0); + proxyMethodIL.Emit(OpCodes.Ldfld, duckField); + } + + // Emit IL to load method arguments + for (int i = 0; i < interfaceMethodParameters.Length; i++) + { + // Emit IL to load the argument + proxyMethodIL.Emit(OpCodes.Ldarg, 1 + i); + + // Emit IL to cast the argument if necessary + DuckTyping.EmitCastIL(proxyMethodIL, duckMethodParameters[i].ParameterType, interfaceMethodParameters[i].ParameterType); + } + + MethodInfo methodToCall; + if (!duckMethod.IsGenericMethodDefinition) + { + methodToCall = duckMethod; + } + else + { + methodToCall = duckMethod.MakeGenericMethod((Type[])(genericParameters)); + } + + // Emit IL to call the method + if (!duckMethod.IsStatic) + { + proxyMethodIL.Emit(OpCodes.Callvirt, methodToCall); + } + else + { + proxyMethodIL.Emit(OpCodes.Call, methodToCall); + } + + // If we are returning something... + if (duckMethod.ReturnType != typeof(void)) + { + // Emit IL to cast the return value if necessary + DuckTyping.EmitCastIL(proxyMethodIL, interfaceMethod.ReturnType, duckMethod.ReturnType); + } + + // Emit IL to return. + proxyMethodIL.Emit(OpCodes.Ret); + + // Add proxy method to proxy member dictionary + // (This is so that any associated properties or events can refer to it later) + if (proxyMembers != null) proxyMembers[duckMethod] = proxyMethod; + } + } + + /// + /// Implements an interface property in a duck proxy type using a given type builder. + /// If successful, the implemented property will be added to the given proxy member dictionary. + /// + /// Type builder for the duck proxy type. + /// Dictionary of members of the proxy type. + /// Field that holds a reference to the duck object to forward calls to. + /// IL generator to use to add code to the constructor if necessary. + /// The interface property to implement. + private void ImplementProperty(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, PropertyInfo interfaceProperty) + { + PropertyInfo duckProperty = FindDuckProperty(interfaceProperty); + if (duckProperty == null) + { + throw new NotImplementedException("Duck type does not implement a property named \"" + interfaceProperty.Name + "\" with the same property type and index parameters."); + } + + PropertyBuilder proxyProperty = proxyType.DefineProperty(interfaceProperty.Name, PropertyAttributes.None, interfaceProperty.PropertyType, GetParameterTypes(interfaceProperty.GetIndexParameters())); + + // Get and set methods appear before associated properties in the member array; thus, they have + // already been defined and implemented. The proxy member dictionary is used to refer to these. + if (interfaceProperty.CanRead) + { + proxyProperty.SetGetMethod((MethodBuilder)(proxyMembers[duckProperty.GetGetMethod()])); + } + if (interfaceProperty.CanWrite) + { + proxyProperty.SetSetMethod((MethodBuilder)(proxyMembers[duckProperty.GetSetMethod()])); + } + + // Add proxy property to the proxy member dictionary + // (This is not really necessary, but good to keep things consistent) + if (proxyMembers != null) proxyMembers[duckProperty] = proxyProperty; + } + + /// + /// Implements an interface event in a duck proxy type using a given type builder. + /// If successful, the implemented event will be added to the given proxy member dictionary. + /// + /// Type builder for the duck proxy type. + /// Dictionary of members of the proxy type. + /// Field that holds a reference to the duck object to forward calls to. + /// IL generator to use to add code to the constructor if necessary. + /// The interface event to implement. + private void ImplementEvent(TypeBuilder proxyType, ProxyMemberDictionary proxyMembers, FieldInfo duckField, ILGenerator constructorIL, EventInfo interfaceEvent) + { + EventInfo duckEvent = FindDuckEvent(interfaceEvent); + if (duckEvent == null) + { + throw new NotImplementedException("Duck type does not implement an event named \"" + interfaceEvent.Name + "\" with the same event handler type."); + } + + EventBuilder proxyEvent = proxyType.DefineEvent(interfaceEvent.Name, EventAttributes.None, interfaceEvent.EventHandlerType); + + Type interfaceEventHandlerType = interfaceEvent.EventHandlerType; + Type duckEventHandlerType = duckEvent.EventHandlerType; + + // Define event add method + MethodBuilder addMethod = proxyType.DefineMethod("add_" + interfaceEvent.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(void), new Type[] { interfaceEventHandlerType }); + ILGenerator addMethodIL = addMethod.GetILGenerator(); + + // Define event remove method + MethodBuilder removeMethod = proxyType.DefineMethod("remove_" + interfaceEvent.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, typeof(void), new Type[] { interfaceEventHandlerType }); + ILGenerator removeMethodIL = removeMethod.GetILGenerator(); + + if (interfaceEventHandlerType == duckEventHandlerType) + { + // If the event handler types are the same, we can just forward calls to add and remove like normal. + + MethodInfo duckAddMethod = duckEvent.GetAddMethod(); + if (!duckAddMethod.IsStatic) + { + addMethodIL.Emit(OpCodes.Ldarg_0); + addMethodIL.Emit(OpCodes.Ldfld, duckField); + addMethodIL.Emit(OpCodes.Ldarg_1); + addMethodIL.Emit(OpCodes.Callvirt, duckAddMethod); + } + else + { + addMethodIL.Emit(OpCodes.Ldarg_1); + addMethodIL.Emit(OpCodes.Call, duckAddMethod); + } + + MethodInfo duckRemoveMethod = duckEvent.GetRemoveMethod(); + if (!duckRemoveMethod.IsStatic) + { + removeMethodIL.Emit(OpCodes.Ldarg_0); + removeMethodIL.Emit(OpCodes.Ldfld, duckField); + removeMethodIL.Emit(OpCodes.Ldarg_1); + removeMethodIL.Emit(OpCodes.Callvirt, duckRemoveMethod); + } + else + { + removeMethodIL.Emit(OpCodes.Ldarg_1); + removeMethodIL.Emit(OpCodes.Call, duckRemoveMethod); + } + } + else + { + // If the event handler types are different, we need some special behavior. Specifically, the event handlers + // added using the interface are kept as a seperate multicast delegate in the proxy class and an event handler + // is added to the duck that calls the proxy event delegate. + + // Define the event multicast delegate field + FieldBuilder eventHandlerField = proxyType.DefineField(interfaceEvent.Name, interfaceEventHandlerType, FieldAttributes.Private); + + // Implement the add method + addMethodIL.Emit(OpCodes.Ldarg_0); + addMethodIL.Emit(OpCodes.Ldarg_0); + addMethodIL.Emit(OpCodes.Ldfld, eventHandlerField); + addMethodIL.Emit(OpCodes.Ldarg_1); + addMethodIL.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Combine", new Type[] { typeof(Delegate), typeof(Delegate) })); + addMethodIL.Emit(OpCodes.Castclass, interfaceEventHandlerType); + addMethodIL.Emit(OpCodes.Stfld, eventHandlerField); + + // Implement the remove method + removeMethodIL.Emit(OpCodes.Ldarg_0); + removeMethodIL.Emit(OpCodes.Ldarg_0); + removeMethodIL.Emit(OpCodes.Ldfld, eventHandlerField); + removeMethodIL.Emit(OpCodes.Ldarg_1); + removeMethodIL.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Remove", new Type[] { typeof(Delegate), typeof(Delegate) })); + removeMethodIL.Emit(OpCodes.Castclass, interfaceEventHandlerType); + removeMethodIL.Emit(OpCodes.Stfld, eventHandlerField); + + // Set some local variables for later use... + MethodInfo interfaceInvokeMethod = interfaceEventHandlerType.GetMethod("Invoke"); + Type[] interfaceInvokeMethodParameterTypes = GetParameterTypes(interfaceInvokeMethod.GetParameters()); + + MethodInfo duckInvokeMethod = duckEventHandlerType.GetMethod("Invoke"); + Type[] duckInvokeMethodParameterTypes = GetParameterTypes(duckInvokeMethod.GetParameters()); + + + // Define the method that will serve as an event handler to the duck which will invoke the proxy's delegate + MethodBuilder proxyInvokeMethod = proxyType.DefineMethod("Invoke" + interfaceEvent.Name, MethodAttributes.Private | MethodAttributes.Virtual | MethodAttributes.Final, CallingConventions.HasThis, duckInvokeMethod.ReturnType, duckInvokeMethodParameterTypes); + ILGenerator proxyInvokeMethodIL = proxyInvokeMethod.GetILGenerator(); + + // First, check if the proxy event handler is null + Label ifEventHandlerIsNullLabel = proxyInvokeMethodIL.DefineLabel(); + proxyInvokeMethodIL.Emit(OpCodes.Ldarg_0); + proxyInvokeMethodIL.Emit(OpCodes.Ldfld, eventHandlerField); + proxyInvokeMethodIL.Emit(OpCodes.Ldnull); + proxyInvokeMethodIL.Emit(OpCodes.Ceq); + proxyInvokeMethodIL.Emit(OpCodes.Brtrue_S, ifEventHandlerIsNullLabel); + + // If the proxy event handler is not null, invoke it + proxyInvokeMethodIL.Emit(OpCodes.Ldarg_0); + proxyInvokeMethodIL.Emit(OpCodes.Ldfld, eventHandlerField); + for (int i = 0; i < interfaceInvokeMethodParameterTypes.Length; i++) + { + proxyInvokeMethodIL.Emit(OpCodes.Ldarg, i + 1); + DuckTyping.EmitCastIL(proxyInvokeMethodIL, interfaceInvokeMethodParameterTypes[i], duckInvokeMethodParameterTypes[i]); + } + proxyInvokeMethodIL.Emit(OpCodes.Callvirt, interfaceInvokeMethod); + + // If the proxy event handler is null, execution jumps here + proxyInvokeMethodIL.MarkLabel(ifEventHandlerIsNullLabel); + + // Return + proxyInvokeMethodIL.Emit(OpCodes.Ret); + + + // Add code to the constructor to add the event handler to the duck. + MethodInfo duckAddMethod = duckEvent.GetAddMethod(); + if (!duckAddMethod.IsStatic) + { + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Ldfld, duckField); + } + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Ldftn, proxyInvokeMethod); + constructorIL.Emit(OpCodes.Newobj, duckEventHandlerType.GetConstructor(new Type[] { typeof(object), typeof(IntPtr) })); + if (!duckAddMethod.IsStatic) + { + constructorIL.Emit(OpCodes.Callvirt, duckAddMethod); + } + else + { + constructorIL.Emit(OpCodes.Call, duckAddMethod); + } + } + + // Finish add method and set it for the event + addMethodIL.Emit(OpCodes.Ret); + proxyEvent.SetAddOnMethod(addMethod); + + // Finish remove method and set it for the event + removeMethodIL.Emit(OpCodes.Ret); + proxyEvent.SetRemoveOnMethod(removeMethod); + + // Other associated methods appear before the event in the member array; thus, they have already been + // defined and implemented. The proxy member dictionary is used to refer to these. + MethodInfo raiseMethod = duckEvent.GetRaiseMethod(); + if (raiseMethod != null) proxyEvent.SetRaiseMethod((MethodBuilder)(proxyMembers[raiseMethod])); + + MethodInfo[] otherDuckMethods = duckEvent.GetOtherMethods(); + foreach (MethodInfo otherDuckMethod in otherDuckMethods) + { + if (proxyMembers.ContainsKey(otherDuckMethod)) + { + proxyEvent.AddOtherMethod((MethodBuilder)(proxyMembers[otherDuckMethod])); + } + } + + // Add proxy event to the proxy member dictionary + // (This is not really necessary, but good to keep things consistent) + // if (proxyMembers != null) proxyMembers[duckEvent] = proxyEvent; + // For some reason, EventBuilder does not inherit MemberInfo, so it cannot be added. + } + + #endregion + + #region Duck type implementation determination and search methods + + /// + /// Determines whether the type given in the constructor implements all the members of TInterface. + /// + /// If the type given in the constructor implements all the members of TInterface, true; otherwise, false. + private bool DuckImplementsAllMembers() + { + bool implementsAll = true; + + MemberInfo[] members = m_InterfaceType.GetMembers(); + foreach (MemberInfo member in members) + { + if (!DuckImplementsMember(member)) + { + implementsAll = false; + break; + } + } + + return implementsAll; + } + + /// + /// Determines whether the type given in the constructor implements a given interface member. + /// + /// + /// This method is used in determining whether an object of a given type can be duck typed to TInterface. + /// + /// Interface member to check for. + /// If the type given in the constructor implements the given interface member, true; otherwise, false. + private bool DuckImplementsMember(MemberInfo interfaceMember) + { + bool implementsMember; + + if (interfaceMember.MemberType == MemberTypes.Method) + { + implementsMember = (FindDuckMethod((MethodInfo)interfaceMember) != null); + } + else if (interfaceMember.MemberType == MemberTypes.Property) + { + implementsMember = (FindDuckProperty((PropertyInfo)interfaceMember) != null); + } + else if (interfaceMember.MemberType == MemberTypes.Event) + { + implementsMember = (FindDuckEvent((EventInfo)interfaceMember) != null); + } + else + { + throw new NotSupportedException("Interface defines a member type that is not supported."); + } + + return implementsMember; + } + + /// + /// Finds the duck implementation for a given interface method. + /// + /// Interface method to find the implementation of. + /// If the type given in the constructor implements the given interface method, such method; otherwise, null. + private MethodInfo FindDuckMethod(MethodInfo interfaceMethod) + { + MethodInfo duckMethod = null; + int bestParameterCompatibilityScore = int.MinValue; + + MethodInfo[] methods = m_DuckType.GetMethods(); + foreach (MethodInfo method in methods) + { + // If casting a static class, must be static; if not, must not be static + if (method.IsStatic == m_IsDuckStatic) + { + // Must be public and have the same name + if (method.IsPublic && method.Name == interfaceMethod.Name) + { + // Must both be non-generic or both generic + if (method.IsGenericMethodDefinition == interfaceMethod.IsGenericMethodDefinition) + { + // Must have compatible generic arguments + bool genericArgumentsMatch = false; + + if (method.IsGenericMethodDefinition) + { + Type[] genericArguments = method.GetGenericArguments(); + Type[] interfaceGenericArguments = interfaceMethod.GetGenericArguments(); + + if (genericArguments.Length == interfaceGenericArguments.Length) + { + // TODO: Check generic argument constraints. + + genericArgumentsMatch = true; + } + } + else + { + genericArgumentsMatch = true; + } + + if (genericArgumentsMatch) + { + // Must have a compatible parameter list + int parameterCompatibilityScore = 0; + + ParameterInfo[] parameters = method.GetParameters(); + ParameterInfo[] interfaceMethodParameters = interfaceMethod.GetParameters(); + + if (parameters.Length == interfaceMethodParameters.Length) + { + bool areParameterTypesCompatible = true; + + for (int i = 0; i < parameters.Length; i++) + { + if (!DuckTyping.AreTypesCompatible(parameters[i].ParameterType, interfaceMethodParameters[i].ParameterType, true)) + { + areParameterTypesCompatible = false; + break; + } + + parameterCompatibilityScore += DuckTyping.GetTypeCompatibilityScore(parameters[i].ParameterType, interfaceMethodParameters[i].ParameterType); + } + + if (areParameterTypesCompatible) + { + // Must have a compatible return type + if (DuckTyping.AreTypesCompatible(interfaceMethod.ReturnType, method.ReturnType, true)) + { + parameterCompatibilityScore += DuckTyping.GetTypeCompatibilityScore(interfaceMethod.ReturnType, method.ReturnType); + + // Since there may be multiple method overloads that are compatible with the + // interface method, we want to choose the best one. + if (parameterCompatibilityScore > bestParameterCompatibilityScore) + { + duckMethod = method; + bestParameterCompatibilityScore = parameterCompatibilityScore; + } + } + } + } + } + } + } + } + } + + return duckMethod; + } + + /// + /// Finds the duck implementation for a given interface property. + /// + /// Interface property to find the implementation of. + /// If the type given in the constructor implements the given interface property, such property; otherwise, null. + private PropertyInfo FindDuckProperty(PropertyInfo interfaceProperty) + { + PropertyInfo duckProperty = null; + + PropertyInfo[] properties = m_DuckType.GetProperties(); + foreach (PropertyInfo property in properties) + { + // Must have the same name + if (property.Name == interfaceProperty.Name) + { + // Must have a compatible property type + if (DuckTyping.AreTypesCompatible(interfaceProperty.PropertyType, property.PropertyType)) + { + // Must implement the capabilities of the interface property + if ((property.CanRead || !interfaceProperty.CanRead) && (property.CanWrite || !interfaceProperty.CanWrite) + && (!interfaceProperty.CanRead || property.GetGetMethod().IsPublic) && (!interfaceProperty.CanWrite || property.GetSetMethod().IsPublic)) + { + // Must have a compatible index parameter list + ParameterInfo[] indexParameters = property.GetIndexParameters(); + ParameterInfo[] interfacePropertyIndexParameters = interfaceProperty.GetIndexParameters(); + + if (indexParameters.Length == interfacePropertyIndexParameters.Length) + { + bool areParameterTypesCompatible = true; + + for (int i = 0; i < indexParameters.Length; i++) + { + if (!DuckTyping.AreTypesCompatible(interfacePropertyIndexParameters[i].ParameterType, indexParameters[i].ParameterType)) + { + areParameterTypesCompatible = false; + break; + } + } + + if (areParameterTypesCompatible) + { + duckProperty = property; + break; + } + } + } + } + } + } + + return duckProperty; + } + + /// + /// Finds the duck implementation for a given interface event. + /// + /// Interface event to find the implementation of. + /// If the type given in the constructor implements the given interface event, such event; otherwise, null. + private EventInfo FindDuckEvent(EventInfo interfaceEvent) + { + EventInfo duckEvent = null; + + EventInfo[] events = m_DuckType.GetEvents(); + foreach (EventInfo _event in events) + { + // Must have the same name + if (_event.Name == interfaceEvent.Name) + { + // Must have a compatible event handler type + if (DuckTyping.AreTypesCompatible(interfaceEvent.EventHandlerType, _event.EventHandlerType)) + { + duckEvent = _event; + break; + } + } + } + + return duckEvent; + } + + #endregion + + /// + /// Utility to create an array of parameter types from an array of parameters. + /// + /// Array of parameters to return the parameter types of. + /// An array of parameter types of the given method. + private Type[] GetParameterTypes(ParameterInfo[] parameters) + { + Type[] parameterTypes = new Type[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].ParameterType; + } + + return parameterTypes; + } + + /// + /// Class for keeping track of member builders while implementing proxy members. + /// The key is the duck member that is called by the proxy. The value is the proxy member that calls it. + /// + /// + /// This was defined in case it is decided to change the class later. + /// + private class ProxyMemberDictionary : Dictionary + { } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/InterfaceStaticDuckCaster.cs b/Source/KSPAPIExt/DuckTyping/InterfaceStaticDuckCaster.cs new file mode 100644 index 00000000..8d7540d3 --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/InterfaceStaticDuckCaster.cs @@ -0,0 +1,99 @@ +// InterfaceStaticDuckCaster.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Static duck casting strategy that casts a static type to an interface. + /// + internal class InterfaceStaticDuckCaster : IStaticDuckCaster + { + private static ToTypeFromTypeTree s_ProxyTypeTree; + + /// + /// Initializes static members. + /// + static InterfaceStaticDuckCaster() + { + s_ProxyTypeTree = new ToTypeFromTypeTree(); + } + + /// + /// Constructs an object. + /// + public InterfaceStaticDuckCaster() + { } + + public bool ShouldStaticCast(Type toType, Type staticType) + { + return (toType.IsInterface && !staticType.IsInterface); + } + + public bool CanStaticCast(Type toType, Type staticType) + { + return ShouldStaticCast(toType, staticType) && new InterfaceDuckProxyType(toType, staticType, true).CanProxy(); + } + + public void PrepareStaticCast(Type toType, Type staticType) + { + GetProxyType(toType, staticType).Prepare(); + } + + public object StaticCast(Type toType, Type staticType) + { + return GetProxyType(toType, staticType).GetStaticProxy(); + } + + /// + /// Gets the InterfaceDuckProxyType object for a given cast. + /// + /// Type to cast to. + /// Static type being casted. + /// The duck proxy type to use to cast or prepare for casting. + private InterfaceDuckProxyType GetProxyType(Type interfaceType, Type duckType) + { + InterfaceDuckProxyType proxyType = null; + + FromTypeTree fromTypeTree = null; + + if (s_ProxyTypeTree.ContainsKey(interfaceType)) + { + fromTypeTree = s_ProxyTypeTree[interfaceType]; + + if (fromTypeTree.ContainsKey(duckType)) + { + proxyType = fromTypeTree[duckType]; + } + } + + if (proxyType == null) + { + proxyType = new InterfaceDuckProxyType(interfaceType, duckType, true); + + if (fromTypeTree == null) + { + fromTypeTree = new FromTypeTree(); + s_ProxyTypeTree.Add(interfaceType, fromTypeTree); + } + + fromTypeTree.Add(proxyType.DuckType, proxyType); + } + + return proxyType; + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/ToTypeFromTypeTree.cs b/Source/KSPAPIExt/DuckTyping/ToTypeFromTypeTree.cs new file mode 100644 index 00000000..64f308da --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/ToTypeFromTypeTree.cs @@ -0,0 +1,66 @@ +// ToTypeFromTypeTree.cs +// +// Copyright (C) 2007 David Meyer +// All Rights Reserved +// +// Website: http://www.deftflux.net/ +// E-mail: deftflux@deftflux.net +// +// This source is licensed to the public via Artistic License 2.0 which should be found in a file +// named license.txt included with the package. It is also available online at: +// http://www.perlfoundation.org/artistic_license_2_0 + + +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + /// + /// Tree to refer to from type trees by to type. + /// + /// Type of object to store. + internal class ToTypeFromTypeTree + { + private Dictionary> m_ToTypeTree; + + /// + /// Constructs an object. + /// + public ToTypeFromTypeTree() + { + m_ToTypeTree = new Dictionary>(); + } + + /// + /// Determines whether a from type tree exists for the given from type. + /// + /// To type to search for. + /// If a from type tree exists for the given to type, true; otherwise, false. + public bool ContainsKey(Type toType) + { + return m_ToTypeTree.ContainsKey(toType); + } + + /// + /// Adds a from type tree to the to type tree. + /// + /// To type for the from type tree. + /// The from type tree to add. + public void Add(Type toType, FromTypeTree fromTypeTree) + { + m_ToTypeTree.Add(toType, fromTypeTree); + } + + /// + /// Gets the from type tree for a given to type. + /// + /// To type for the object. + /// The from type tree for the given from type. + public FromTypeTree this[Type toType] + { + get { return m_ToTypeTree[toType]; } + } + } +} diff --git a/Source/KSPAPIExt/DuckTyping/TypeKey.cs b/Source/KSPAPIExt/DuckTyping/TypeKey.cs new file mode 100644 index 00000000..f731ead6 --- /dev/null +++ b/Source/KSPAPIExt/DuckTyping/TypeKey.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DeftTech.DuckTyping +{ + internal sealed class TypeKey : IComparable + { + private Type m_Type; + + public TypeKey(Type type) + { + m_Type = type; + } + + public override string ToString() + { + return m_Type.ToString(); + } + + public int CompareTo(TypeKey other) + { + Type type1 = m_Type; + Type type2 = other.m_Type; + + int result; + + // If the types are equal, immediately return 0. + if (type1 == type2) + { + result = 0; + } + else + { + // At this point, we know that the types are not equal, so we need some way to compare them. + + // First, compare their GUIDs. This should work most of the time. + if ((result = type1.GUID.CompareTo(type2.GUID)) == 0) + { + // If the GUIDs are the same somehow, compare the assembly qualified name. This should also work most of the time. + if (type1.AssemblyQualifiedName != null && type2.AssemblyQualifiedName != null) + { + result = type1.AssemblyQualifiedName.CompareTo(type2.AssemblyQualifiedName); + } + + if (result == 0) + { + // The only known case where the GUID and assembly qualified name of two types are equal is when both types + // are generic parameters of the same name. So let's check for that: + if (type1.IsGenericParameter && type2.IsGenericParameter) + { + // If the generic parameters belong to generic methods... + if (type1.DeclaringMethod != null && type2.DeclaringMethod != null) + { + // Compare the types that define the generic methods: + if ((result = new TypeKey(type1.DeclaringMethod.DeclaringType).CompareTo(new TypeKey(type2.DeclaringMethod.DeclaringType))) == 0) + { + // If the generic methods are declared in the same type, compare the name of the generic methods: + if ((result = type1.DeclaringMethod.Name.CompareTo(type2.DeclaringMethod.Name)) == 0) + { + // If the generic parameters belong to the same generic method, compare their position in + // the list of generic parameters. + result = type1.GenericParameterPosition.CompareTo(type2.GenericParameterPosition); + } + } + } + // If the generic parameters belong to a generic type... + else if (type1.DeclaringType != null && type2.DeclaringType != null) + { + // Compare the generic types: + if ((result = new TypeKey(type1.DeclaringType).CompareTo(new TypeKey(type2.DeclaringType))) == 0) + { + // If they belong to the same generic type, compare their position in the list of generic + // parameters: + result = type1.GenericParameterPosition.CompareTo(type2.GenericParameterPosition); + } + } + } + + // If we still haven't distinguished the types, I suppose it's possible that they are two generic types, + // perhaps of the same generic type definition. + if (result == 0 && type1.IsGenericType && type2.IsGenericType) + { + Type[] type1GenericArguments = type1.GetGenericArguments(); + Type[] type2GenericArguments = type2.GetGenericArguments(); + + // Compare the length of the generic argument lists: + if ((result = type1GenericArguments.Length.CompareTo(type2GenericArguments.Length)) == 0) + { + // If the lists are the same length, compare each argument: + int i = 0; + do + { + result = new TypeKey(type1GenericArguments[i]).CompareTo(new TypeKey(type2GenericArguments[i])); + + i++; + } + while (i < type1GenericArguments.Length && result == 0); + } + } + + // If we still haven't distinguished the types, yet we know that they are not equal, we have no choice but + // to throw an exception. Theoretically, this should never happen, but in case it does, we'll know what + // the problem is and have some information to work with. + if (result == 0) + { + throw new AmbiguousTypesException(type1, type2); + } + } + } + } + + return result; + } + } +} diff --git a/Source/KSPAPIExt/KSPAPIExtensions.csproj b/Source/KSPAPIExt/KSPAPIExtensions.csproj new file mode 100644 index 00000000..24258ee7 --- /dev/null +++ b/Source/KSPAPIExt/KSPAPIExtensions.csproj @@ -0,0 +1,109 @@ + + + + + Debug + AnyCPU + {4282B0DA-C314-4524-9C32-EDAB2C9666A9} + Library + Properties + KSPAPIExtensions + KSPAPIExtensions + v3.5 + 512 + + + + true + full + false + ..\..\Build\KSPAPIExtensions\obj\ + ..\..\Build\KSPAPIExtensions\Debug\ + DEBUG;TRACE + prompt + 4 + AnyCPU + 1607 + true + + + none + true + ..\..\Build\KSPAPIExtensions\obj\ + ..\ + TRACE + prompt + 4 + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\Assembly-CSharp.dll + False + + + ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\Assembly-CSharp-firstpass.dll + False + + + + ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\UnityEngine.dll + False + + + + + + + + + sh version-gen + + + \ No newline at end of file diff --git a/Source/KSPAPIExt/PartMessage/Attributes.cs b/Source/KSPAPIExt/PartMessage/Attributes.cs new file mode 100644 index 00000000..4688e158 --- /dev/null +++ b/Source/KSPAPIExt/PartMessage/Attributes.cs @@ -0,0 +1,137 @@ +using System; +using KSPAPIExtensions.DebuggingUtils; + +namespace KSPAPIExtensions.PartMessage +{ + /// + /// Apply this attribute to any method you wish to receive messages. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class PartMessageListener : Attribute, IPartMessageListenerV1 + { + public PartMessageListener(Type delegateType, PartRelationship relations = PartRelationship.Self, GameSceneFilter scenes = GameSceneFilter.Any) + { + if (delegateType == null) + throw new ArgumentException("Message argument is null"); + if (!delegateType.IsSubclassOf(typeof(Delegate))) + throw new ArgumentException("Message is not a delegate type: " + typeof(Delegate).AssemblyQualifiedName + "\n" + Debugging.DumpTypeHeirachy(delegateType)); + if (delegateType.GetCustomAttributes(typeof(PartMessageDelegate), true).Length == 0) + throw new ArgumentException("Message does not have the PartMessageDelegate attribute"); + + DelegateType = delegateType; + Scenes = scenes; + Relations = relations; + } + + /// + /// The delegate type that we are listening for. + /// + public Type DelegateType { get; private set; } + + /// + /// Scene to listen for message in. Defaults to All. + /// + public GameSceneFilter Scenes { get; private set; } + + /// + /// Filter for relation between the sender and the reciever. + /// + public PartRelationship Relations { get; private set; } + } + + /// + /// Marker attribute to apply to events using part messages. + /// + [AttributeUsage(AttributeTargets.Event)] + public class PartMessageEvent : Attribute, IPartMessageEventV1 + { + /// + /// Constructor for attribute. + /// + /// If this is true then the event will be raised asynchonously. Any message filters will be called immediatly, + /// then the message will be delivered in the next update frame. + public PartMessageEvent(bool isAsync = false) + { + IsAsync = isAsync; + } + + /// + /// If this is true then the event will be raised asynchonously. Any message filters will be called immediatly, + /// then the message will be delivered in the next update frame. + /// + public bool IsAsync + { + get; + private set; + } + } + + /// + /// The attribute to be applied to a delegate to mark it as a PartMessageDelegate type. + /// + /// To use the message, define an event within a Part or PartModule that uses this delegate. + /// + [AttributeUsage(AttributeTargets.Delegate)] + public class PartMessageDelegate : Attribute, IPartMessageDelegateV1 + { + public PartMessageDelegate(Type parent = null, bool isAbstract = false) + { + if (parent != null) + { + if (!parent.IsSubclassOf(typeof(Delegate))) + throw new ArgumentException("Parent is not a delegate type"); + if (parent.GetCustomAttributes(typeof(PartMessageDelegate), true).Length != 1) + throw new ArgumentException("Parent does not have the PartMessageDelegate attribute"); + } + Parent = parent; + IsAbstract = isAbstract; + } + + /// + /// Often there is a heirachy of events - with more specific events and encompasing general events. + /// Define a general event as the parent in this instance and any listeners to the general event + /// will also be notified. Note that the arguments in this situation are expected to be a truncation + /// of the argument list for this event. + /// + public Type Parent { get; private set; } + + /// + /// This event is considered abstract - it should not be sent directly but should be sent from one of the child events. + /// + public bool IsAbstract { get; private set; } + } + + /// + /// If the message as defined in a is idempotent, or partially idempotent, then mark + /// any arguments or the whole delegate with this attribute. + /// + /// In essence: There's no point updating the part's mass three times in a row, you only need to know about the most recent. + /// If you're using then any messages with everything else the same except for the + /// mass will be consolidated together if the mass is marked with [UseLatest] in the delegate. + /// + /// Note that listeners do not need to use this attribute in their argument list. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public class UseLatest : Attribute { } + + /// + /// Interface to implement on things that aren't either Parts or PartModules to enable them to send/recieve messages + /// using the event system as a proxy for an actual part. This interface is not required, however if not implemented + /// the part recievers will not be able to filter by source relationship. + /// You will need to call PartMessageService.Register(object) in the Awake method or constructor. + /// + public interface IPartMessagePartProxy + { + Part ProxyPart { get; } + } + + /// + /// A filter method for outgoing messages. This is called prior to delivery of any messages. If the method returns true + /// then the message is considered handled and will not be delivered. + /// + /// Information about the source of the message is avaiable from the ICurrentEventInfo as usual, this is passed as an argument for convenience. + /// + /// True if the message is considered handled and is not to be delivered. + public delegate bool PartMessageFilter(ICurrentEventInfo message); + +} diff --git a/Source/KSPAPIExt/PartMessage/CommonMessages.cs b/Source/KSPAPIExt/PartMessage/CommonMessages.cs new file mode 100644 index 00000000..a2acd709 --- /dev/null +++ b/Source/KSPAPIExt/PartMessage/CommonMessages.cs @@ -0,0 +1,157 @@ +using UnityEngine; + +namespace KSPAPIExtensions.PartMessage +{ + /// + /// Listen for this to get notification when any physical constant is changed + /// including the mass, CoM, moments of inertia, boyancy, ect. + /// + [PartMessageDelegate(isAbstract: true)] + public delegate void PartPhysicsChanged(); + + /// + /// Message for when the part's mass is modified. + /// + [PartMessageDelegate(typeof(PartPhysicsChanged))] + public delegate void PartMassChanged([UseLatest] float mass); + + /// + /// Message for when the part's CoMOffset changes. + /// + [PartMessageDelegate(typeof(PartPhysicsChanged))] + public delegate void PartCoMOffsetChanged([UseLatest] Vector3 offset); + + /// + /// Message for when the part's moments of intertia change. + /// + [PartMessageDelegate(typeof(PartPhysicsChanged))] + public delegate void PartMomentsChanged([UseLatest] Vector3 intertiaTensor, [UseLatest] Quaternion intertiaTensorRotation); + + /// + /// When the the volume of some space within the part changes, this message is raised + /// + /// The name of the area. This may be one of the names of the enum for a 'standard volume', or some custom value which obviously the sender and reciever will need to agree on. + /// The volume in cubic meters (kilolitres) + [PartMessageDelegate] + public delegate void PartVolumeChanged(string name, [UseLatest] float volume); + + /// + /// Well known volumes within a part. + /// + public enum PartVolumes + { + /// + /// Tankage - the volume devoted to storage of fuel, life support resources, ect + /// + Tankage, + /// + /// The volume devoted to habitable space. + /// + Habitable, + } + + /// + /// Abstract message - some change to an attach node has occured. + /// + [PartMessageDelegate(isAbstract: true)] + public delegate void PartAttachNodeChanged(AttachNode node); + + /// + /// Raised when the size of an attachment node is changed. + /// + /// The attachment node + /// The minimum diameter across the attachment area. For circular areas this will be the diameter + /// Area in square meters of the attachment. + [PartMessageDelegate(typeof(PartAttachNodeChanged))] + public delegate void PartAttachNodeSizeChanged(AttachNode node, [UseLatest] float minDia, [UseLatest] float area); + + /// + /// Location or orientation of the attachment node is changed + /// + /// The attachment node + [PartMessageDelegate(typeof(PartAttachNodeChanged))] + public delegate void PartAttachNodePositionChanged(AttachNode node, [UseLatest] Vector3 location, [UseLatest] Vector3 orientation, [UseLatest] Vector3 secondaryAxis); + + /// + /// Message for when the part's resource list is modified in some way. + /// + [PartMessageDelegate(isAbstract: true)] + public delegate void PartResourcesChanged(); + + /// + /// Message for when the part's resource list is modified (added to or subtracted from). + /// + [PartMessageDelegate(typeof(PartResourcesChanged))] + public delegate void PartResourceListChanged(); + + /// + /// Message for when the max amount of a resource is modified. + /// + [PartMessageDelegate(typeof(PartResourcesChanged))] + public delegate void PartResourceMaxAmountChanged(PartResource resource, [UseLatest] double maxAmount); + + /// + /// Message for when the initial amount of a resource is modified (only raised in the editor) + /// + [PartMessageDelegate(typeof(PartResourcesChanged))] + public delegate void PartResourceInitialAmountChanged(PartResource resource, [UseLatest] double amount); + + /// + /// Message for when some change has been made to the part's rendering model. + /// + [PartMessageDelegate] + public delegate void PartModelChanged(); + + /// + /// Message for when some change has been made to the part's collider. + /// + [PartMessageDelegate] + public delegate void PartColliderChanged(); + + /// + /// There has been some change to the part heirachy. + /// + [PartMessageDelegate(isAbstract:true)] + public delegate void PartHeirachyChanged(); + + /// + /// A root part has been selected in the VAB. + /// Note: this is automatically invoked by the framework, so you don't ever need to raise the event. + /// + [PartMessageDelegate(typeof(PartHeirachyChanged))] + public delegate void PartRootSelected(); + + /// + /// A root part has been removed in the VAB. + /// Note: this is automatically invoked by the framework, so you don't ever need to raise the event. + /// + [PartMessageDelegate(typeof(PartHeirachyChanged))] + public delegate void PartRootRemoved(); + + /// + /// The parent of this part has changed. Note: this is automatically invoked by the framework, so you don't ever need to raise the event. + /// + /// New parent, or null if it has been detached. + [PartMessageDelegate(typeof(PartHeirachyChanged))] + public delegate void PartParentChanged([UseLatest] Part parent); + + /// + /// New child has been attached to this part. Note: this is automatically invoked by the framework, so you don't ever need to raise the event. + /// + [PartMessageDelegate(typeof(PartHeirachyChanged))] + public delegate void PartChildAttached(Part child); + + /// + /// Child part has been detached from this part. Note: this is automatically invoked by the framework, so you don't ever need to raise the event. + /// + [PartMessageDelegate(typeof(PartHeirachyChanged))] + public delegate void PartChildDetached(Part child); + + + /// + /// There has been some change to the configuration for the engine. + /// + [PartMessageDelegate] + public delegate void PartEngineConfigChanged(); + +} diff --git a/Source/KSPAPIExt/PartMessage/PartMessageService.cs b/Source/KSPAPIExt/PartMessage/PartMessageService.cs new file mode 100644 index 00000000..b64db488 --- /dev/null +++ b/Source/KSPAPIExt/PartMessage/PartMessageService.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using DeftTech.DuckTyping; + +namespace KSPAPIExtensions.PartMessage +{ + /// + /// Interface for a part message once it is passed into the system. + /// + public interface IPartMessage : IEnumerable + { + /// + /// String name of the part message. This will be equal to the FullName attibute of the delegate type. + /// + string Name { get; } + + /// + /// Delegate type of the message. Note: do not rely on type equality with this attribute. This is because + /// the source may be using a different assembly to the target. + /// + Type DelegateType { get; } + + /// + /// Often there is a heirachy of events - with more specific events and encompasing general events. + /// Define a general event as the parent in this instance and any listeners to the general event + /// will also be notified. Note that the arguments in this situation are expected to be a truncation + /// of the argument list for this event. + /// + IPartMessage Parent { get; } + + /// + /// This event is considered abstract - it should not be sent directly but should be sent from one of the child events. + /// + bool IsAbstract { get; } + } + + /// + /// PartMessageListeners can use the properties in this class to examine details about the current message being + /// handled + /// + public interface ICurrentEventInfo : IEquatable + { + /// + /// The message + /// + IPartMessage Message { get; } + + /// + /// The source of the event + /// + object Source { get; } + + /// + /// The source part. This may be null if the source was not a Part or PartModule + /// + Part SourcePart { get; } + + /// + /// The source PartModule. This will be null if the source was not a PartModule + /// + PartModule SourceModule { get; } + + /// + /// The arguments to the current event. Treat as unmodifiable. + /// + object[] Arguments { get; } + + /// + /// The arguments that are used when determining event equality. This is any arguments not explicitly marked with + /// + IEnumerable IdentArguments { get; } + + /// + /// Find relationship between the message source and the specified part. + /// + /// The relationship. This will be PartRelationship.Unknown if the dest part is null or the source part is unknown. + PartRelationship SourceRelationTo(Part destPart); + } + + public interface IPartMessageService + { + /// + /// CurrentEventInfo for the current message being processed. This is used to get information about the current message source. + /// + ICurrentEventInfo CurrentEventInfo { get; } + + /// + /// Scan an object for events marked with and methods marked with and hook them up. + /// This is generally called either in the constructor, or in OnAwake for Part and PartModules. + /// Note that this method does not scan base classes for events and listeners, they need to be scanned explicitly. + /// + /// Object to register. If this is a Part, a PartModule, or a IPartMessagePartProxy the recieving part will be discovered. + void Register(T obj); + + /// + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// + /// Message type. This must be a delegate type marked with the PartMessageDelegate attribute. + /// Source of the message. If this is a Part, a PartModule, or a IPartMessagePartProxy the source part will be discovered. + /// message arguments + void Send(object source, params object[] args); + + /// + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// + /// The message delegate type. This must have the PartMessageDelegate attribute. + /// Source of the message. If this is a Part, a PartModule, or a IPartMessagePartProxy the source part will be discovered. + /// message arguments + void Send(Type message, object source, params object[] args); + + /// + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// This version allows the source to proxy for some other part. + /// + /// Message type. This must be a delegate type marked with the PartMessageDelegate attribute. + /// Source of the message. This may be any object. This variant does not do automatic part discovery. + /// Part that the message source is proxying for + /// message arguments + void SendProxy(object source, Part part, params object[] args); + + /// + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// This version allows the source to proxy for some other part. + /// + /// The message delegate type. This must have the PartMessageDelegate attribute. + /// Source of the message. This may be any object. This variant does not do automatic part discovery. + /// Part that the message source is proxying for + /// message arguments + void SendProxy(Type message, object source, Part part, params object[] args); + + /// + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// This version of the method will send the message asynchonously - the message will be delivered in the next update frame. Any message filters will + /// be invoked prior to returning. + /// + /// Message type. This must be a delegate type marked with the PartMessageDelegate attribute. + /// Source of the message. If this is a Part, a PartModule, or a IPartMessagePartProxy the source part will be discovered. + /// message arguments + void SendAsync(object source, params object[] args); + + /// + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// This version of the method will send the message asynchonously - the message will be delivered in the next update frame. Any message filters will + /// be invoked prior to returning. + /// + /// The message delegate type. This must have the PartMessageDelegate attribute. + /// Source of the message. If this is a Part, a PartModule, or a IPartMessagePartProxy the source part will be discovered. + /// message arguments + void SendAsync(Type message, object source, params object[] args); + + /// + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// This version of the method will send the message asynchonously - the message will be delivered in the next update frame. Any message filters will + /// be invoked prior to returning. + /// + /// Message type. This must be a delegate type marked with the PartMessageDelegate attribute. + /// Source of the message. This may be any object. This variant does not do automatic part discovery. + /// Part that the message source is proxying for + /// message arguments + void SendAsyncProxy(object source, Part part, params object[] args); + + /// + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// This version of the method will send the message asynchonously - the message will be delivered in the next update frame. Any message filters will + /// be invoked prior to returning. + /// + /// The message delegate type. This must have the PartMessageDelegate attribute. + /// Source of the message. This may be any object. This variant does not do automatic part discovery. + /// Part that the message source is proxying for + /// message arguments + void SendAsyncProxy(Type message, object source, Part part, params object[] args); + + /// + /// Register a message filter. This delegate will be called for every message sent from the source. + /// If it returns true, the message is considered handled and no futher processing will occour. + /// + /// The delegate for the filter + /// Message source, must match. If null will match all sources. + /// Source part to match. If null will match all parts. + /// Optional list of messages to match. If empty, all messages are matched. + /// Disposable object. When done call dispose. Works well with using clauses. + IDisposable Filter(PartMessageFilter filter, object source = null, Part part = null, params Type[] messages); + + /// + /// Consolidate messages. All messages sent by the source will be held until the returned object is destroyed. + /// Any duplicates of the same message and same arguments will be swallowed silently. + /// + /// source to consolidate from. Not specified or null will match all sources + /// Source part to match. If null will match all parts. + /// messages to consolidate. If not specified, all messages are consolidated. + /// Disposable object. When done call dispose. Works well with using clauses. + IDisposable Consolidate(object source = null, Part part = null, params Type[] messages); + + /// + /// Ignore messages sent by the source until the returned object is destroyed. + /// + /// Source to ignore. Null will ignore all sources. + /// Source part to match. If null will match all parts. + /// Messages to ignore. If not specified, all messages are ignored. + /// Disposable object. When done call dispose. Works well with using clauses. + IDisposable Ignore(object source = null, Part part = null, params Type[] messages); + + /// + /// Convert delegate type into the IPartMessage interface. + /// + /// Delegate type to convert. This must be a delegate type marked with the attribute. + IPartMessage AsIPartMessage(Type type); + + /// + /// Convert delegate type into the IPartMessage interface. + /// + /// Delegate type to convert. This must be a delegate type marked with the attribute. + IPartMessage AsIPartMessage(); + } + + /// + /// PartMessageService. This abstract base class primaraly implements the finder for the service (Instance) along with some static short-cut methods. + /// + static public class PartMessageService + { + internal static readonly string PartMessageServiceName = typeof(PartMessageService).FullName + ":SingletonInstance"; + // ReSharper disable once InconsistentNaming + internal static IPartMessageService _instance; + + /// + /// Get the instance of the PartMessageService. Note that this may be a duck-typed proxy. + /// + public static IPartMessageService Instance + { + get + { + if (_instance != null) + return _instance; + + GameObject serviceGo = GameObject.Find(PartMessageServiceName); + if (serviceGo == null) + throw new InvalidProgramException("PartMessageService has not been initialized."); + + foreach (Component comp in serviceGo.GetComponents()) + { + if (comp.GetType().FullName.StartsWith(typeof(ServiceImpl).FullName)) + return _instance = DuckTyping.Cast(comp); + } + + throw new InvalidProgramException("Unable to find a compatible part message service from updated assembly. Something has gone very wrong."); + } + internal set + { + _instance = value; + } + } + + /// + /// Convenience short-cut method for getting the current CurrentEventInfo. This interface allows message listeners to find information about the sender. + /// This object can be cached for later use, and will not update in future invocations. + /// + /// If there is no current invocation occouring. + public static ICurrentEventInfo MessageInfo + { + get { return Instance.CurrentEventInfo; } + } + + /// + /// Convenience short-cut . + /// Scan an object for events marked with and methods marked with and hook them up. + /// This is generally called either in the constructor, or in OnAwake for Part and PartModules. + /// Note that this method does not scan base classes for events and listeners, they need to be scanned explicitly. + /// + /// The type of the object to register. This can normally be inferred from the argument type. + /// Object to register. If this is a Part, a PartModule, or a IPartMessagePartProxy the recieving part will be discovered. + public static void Register(T obj) + { + Instance.Register(obj); + } + + /// + /// Convenience short-cut . + /// Send a message. Normally this will be automatically invoked by the event, but there are types when dynamic invocation is required. + /// This version allows the source to proxy for some other part. + /// + /// The delegate type of the message to send. This must be a delegate, and must be marked with attribute. + /// Source of the message. This may be any object. This variant does not do automatic part discovery. + /// Part that the message source is proxying for + /// message arguments + public static void Send(object source, Part part, params object[] args) + { + Instance.SendProxy(source, part, args); + } + } +} diff --git a/Source/KSPAPIExt/PartMessage/zzImplementation.cs b/Source/KSPAPIExt/PartMessage/zzImplementation.cs new file mode 100644 index 00000000..2dcdaf6e --- /dev/null +++ b/Source/KSPAPIExt/PartMessage/zzImplementation.cs @@ -0,0 +1,1192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; +using System.Linq.Expressions; +using System.Collections; +using DeftTech.DuckTyping; + +namespace KSPAPIExtensions.PartMessage +{ + #region Duck Typing interfaces + /// + /// Interface to allow duck casting of message listeners. + /// + /// Do not change this interface or duck casting will fail. + /// + internal interface IPartMessageListenerV1 + { + Type DelegateType { get; } + + GameSceneFilter Scenes { get; } + + PartRelationship Relations { get; } + } + + /// + /// Interface to allow duck casting of messages. + /// + /// Do not change this interface or duck casting will fail. + /// + internal interface IPartMessageDelegateV1 + { + Type Parent { get; } + + bool IsAbstract { get; } + } + + /// + /// Interface to allow duck casting of messages. + /// + /// Do not change this interface or duck casting will fail. + /// + internal interface IPartMessageEventV1 + { + bool IsAsync { get; } + } + + #endregion + + #region Current Event Info + internal class CurrentEventInfoImpl : ICurrentEventInfo, IDisposable + { + #region Internal Bits + [ThreadStatic] + internal static CurrentEventInfoImpl current; + +#if DEBUG + private bool onStack; +#endif + private CurrentEventInfoImpl previous; + internal bool filterComplete; + + internal CurrentEventInfoImpl(IPartMessage message, object source, Part part, object[] args) + { + Source = source; + SourcePart = part; + Message = message; + Arguments = args; + + List identityArgs = new List(); + ParameterInfo [] paramInfos = message.DelegateType.GetMethod("Invoke").GetParameters(); + for(int i = 0; i < paramInfos.Length; ++i) + { + foreach(Attribute attr in paramInfos[i].GetCustomAttributes(false)) + if(attr.GetType().FullName == typeof(UseLatest).FullName) + goto foundAttr; + identityArgs.Add(args[i]); + foundAttr: + ; + } + IdentArguments = identityArgs.AsReadOnly(); + } + + internal IDisposable Push() + { +#if DEBUG + if(onStack) + throw new InvalidProgramException("Pushing message onto the stack when it's already on it"); + onStack = true; +#endif + + previous = current; + current = this; + return this; + } + + void IDisposable.Dispose() + { +#if DEBUG + if (!onStack) + throw new InvalidProgramException("Disposed called when not on the stack."); + onStack = false; +#endif + + current = previous; + previous = null; + } + +#if DEBUG + ~CurrentEventInfoImpl() + { + if (onStack) + { + Debug.LogError("CurrentEventInfoImpl somehow left on the call stack"); + } + + } +#endif + + #endregion + + #region Interface methods + + public IPartMessage Message { get; private set; } + + public object Source { get; private set; } + + public Part SourcePart { get; private set; } + + public object[] Arguments { get; private set; } + + public IEnumerable IdentArguments { get; private set; } + + public PartModule SourceModule { get { return Source as PartModule; } } + + public PartRelationship SourceRelationTo(Part destPart) + { + // ReSharper disable once InvokeAsExtensionMethod Don't because SourcePart may be null + return PartUtils.RelationTo(SourcePart, destPart); + } + + public override string ToString() + { + return string.Format("CurrentEventInfoImpl(Message:{0}, Source:{1}, SourcePart:{2}, Arguments.Length={3})", Message, Source, SourcePart, (Arguments == null) ? -1 : Arguments.Length); + } + + public bool Equals(ICurrentEventInfo other) + { + if (other == null) + return false; + if (ReferenceEquals(this, other)) + return true; + + if (GetHashCode() != other.GetHashCode()) + return false; + + if (Source != other.Source) + return false; + if (Message.Name != other.Message.Name) + return false; + if (!IdentArguments.SequenceEqual(other.IdentArguments)) + return false; + return true; + } + + public override bool Equals(object obj) + { + return Equals(obj as ICurrentEventInfo); + } + + private int hashCode; + + public override int GetHashCode() + { + // ReSharper disable NonReadonlyFieldInGetHashCode + if (hashCode != 0) + return hashCode; + + hashCode = + Source.GetHashCode() + ^ ((SourcePart == null) ? 0 : SourcePart.GetHashCode()) + ^ Message.Name.GetHashCode() + ^ Arguments.Length; + foreach (object arg in IdentArguments) + hashCode ^= (arg == null ? 0 : arg.GetHashCode()); + return hashCode; + // ReSharper restore NonReadonlyFieldInGetHashCode + } + + #endregion + + } + + #endregion + + #region Part Message + internal class MessageImpl : IPartMessage + { + private readonly IPartMessageDelegateV1 ifMsg; + internal MessageImpl parent; + + internal MessageImpl(ServiceImpl service, Type message) + { + if (!typeof(Delegate).IsAssignableFrom(message)) + throw new ArgumentException("Message type " + message + " is not a delegate type"); + + Attribute attribute; + foreach(Attribute attr in message.GetCustomAttributes(false)) + if (attr.GetType().FullName == typeof(PartMessageDelegate).FullName) + { + attribute = attr; + goto foundAttribute; + } + throw new ArgumentException("Message does not have the PartMessageDelegate attribute"); + + foundAttribute: + DelegateType = message; + + ifMsg = ServiceImpl.AsDelegate(attribute); + if(ifMsg.Parent != null) + parent = (MessageImpl)service.AsIPartMessage(ifMsg.Parent); + } + + public string Name + { + get { return DelegateType.FullName; } + } + + public Type DelegateType + { + get; + private set; + } + + public IPartMessage Parent + { + get { return parent; } + } + + public bool IsAbstract + { + get { return ifMsg.IsAbstract; } + } + + public IEnumerator GetEnumerator() + { + return new Enumerator + { + head = this + }; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private class Enumerator : IEnumerator + { + internal MessageImpl head; + private MessageImpl current; + private bool atEnd; + + public IPartMessage Current + { + get + { + if(head == null) + throw new InvalidOperationException("Iterator disposed"); + if (current == null) + throw new InvalidOperationException("Iterator is at " + (atEnd?"end":"start")); + return current; + } + } + + public void Dispose() + { + current = head = null; + atEnd = true; + } + + object IEnumerator.Current + { + get { return Current; } + } + + public bool MoveNext() + { + if (head == null) + throw new InvalidOperationException("Iterator disposed"); + if (atEnd) + throw new InvalidOperationException("Iterator is at end"); + + current = current == null ? head : current.parent; + + return !(atEnd = (current == null)); + } + + public void Reset() + { + current = null; + atEnd = false; + } + } + + public override string ToString() + { + return Name; + } + } + #endregion + + internal class ServiceImpl : MonoBehaviour, IPartMessageService + { + public ICurrentEventInfo CurrentEventInfo + { + get { + if (CurrentEventInfoImpl.current == null) + throw new InvalidOperationException("Cannot retrieve source info as not currently in invocation."); + + return CurrentEventInfoImpl.current; + } + } + + #region Registration + /// + /// Scan an object for message events and message listeners and hook them up. + /// Note that all references are dumped on game scene change, so objects must be rescanned when reloaded. + /// + /// the object to scan + public void Register(T obj) + { + Type objType = typeof(T); + + foreach (MethodInfo meth in objType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + try + { + foreach (object attr in meth.GetCustomAttributes(true)) + if (attr.GetType().FullName == typeof (PartMessageListener).FullName) + AddListener(obj, meth, AsListener(attr)); + } + catch + { + Debug.LogError("Exception when attempting to register listener: " + (meth.DeclaringType != null ? meth.DeclaringType.AssemblyQualifiedName : "" ) + "." + meth.Name); + throw; + } + } + + foreach (EventInfo evt in objType.GetEvents(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + bool async; + try + { + foreach (object attr in evt.GetCustomAttributes(true)) + if (attr.GetType().FullName == typeof(PartMessageEvent).FullName) + { + async = AsEvent(attr).IsAsync; + goto foundEvent; + } + } + catch + { + Debug.LogError("Exception when attempting to register event: " + (evt.DeclaringType != null ? evt.DeclaringType.AssemblyQualifiedName : "" ) + "." + evt.Name); + throw; + } + + continue; + + foundEvent: + Type deleg = evt.EventHandlerType; + + // sanity check + foreach (object attr in deleg.GetCustomAttributes(true)) + if (attr.GetType().FullName == typeof(PartMessageDelegate).FullName) + { + goto checkedDelegate; + } + + Debug.LogWarning(string.Format("[PartMessageService] Event: {0} in class: {1} declares an event with a part message, but does not have the PartMessageEvent attribute. Will ignore", evt.Name, objType.FullName)); + continue; + + checkedDelegate: + GenerateEventHandoff(async, obj, evt); + } + } + + internal static IPartMessageEventV1 AsEvent(object attr) + { + return DuckTyping.Cast(attr); + } + + internal static IPartMessageListenerV1 AsListener(object attr) + { + return DuckTyping.Cast(attr); + } + + internal static IPartMessageDelegateV1 AsDelegate(Attribute attribute) + { + return DuckTyping.Cast(attribute); + } + + #endregion + + #region Listeners and Event Delegates + + private Dictionary> listeners = new Dictionary>(); + + private class ListenerInfo + { + public WeakReference targetRef; + public MethodInfo method; + public IPartMessageListenerV1 attr; + + public object Target + { + get + { + return targetRef.Target; + } + } + + private Part TargetPart + { + get + { + object target = Target; + return AsPart(target); + } + } + + public bool CheckPrereq(ICurrentEventInfo info) + { + if (!attr.Scenes.IsLoaded()) + return false; + // ReSharper disable once InvokeAsExtensionMethod SourcePart can be null + if (!PartUtils.RelationTest(info.SourcePart, TargetPart, attr.Relations)) + return false; + return true; + } + } + + private void AddListener(object target, MethodInfo meth, IPartMessageListenerV1 attr) + { + if (!attr.Scenes.IsLoaded()) + return; + + if (Delegate.CreateDelegate(attr.DelegateType, target, meth, false) == null) + { + Debug.LogError(string.Format("PartMessageListener method {0}.{1} does not support the delegate type {2} as declared in the attribute", meth.DeclaringType, meth.Name, attr.DelegateType.FullName)); + return; + } + + string message = attr.DelegateType.FullName; + + LinkedList listenerList; + if (!listeners.TryGetValue(message, out listenerList)) + { + listenerList = new LinkedList(); + listeners.Add(message, listenerList); + } + + listenerList.AddLast(new ListenerInfo + { + targetRef = new WeakReference(target), + method = meth, + attr = attr + }); + } + + private static readonly MethodInfo HandoffSend = typeof(ServiceImpl).GetMethod("SendProxy", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(Type), typeof(object), typeof(Part), typeof(object[]) }, null); + private static readonly MethodInfo HandoffSendAsync = typeof(ServiceImpl).GetMethod("SendAsyncProxy", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(Type), typeof(object), typeof(Part), typeof(object[]) }, null); + + private void GenerateEventHandoff(bool async, object source, EventInfo evt) + { + Part part = AsPart(source); + + // This generates a dynamic method that pulls the properties of the event + // plus the arguments passed and hands it off to the EventHandler method below. + Type message = evt.EventHandlerType; + MethodInfo m = message.GetMethod("Invoke", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + + ParameterInfo[] pLst = m.GetParameters(); + ParameterExpression[] peLst = new ParameterExpression[pLst.Length]; + Expression[] cvrt = new Expression[pLst.Length]; + for (int i = 0; i < pLst.Length; i++) + { + peLst[i] = Expression.Parameter(pLst[i].ParameterType, pLst[i].Name); + cvrt[i] = Expression.Convert(peLst[i], typeof(object)); + } + Expression createArr = Expression.NewArrayInit(typeof(object), cvrt); + + Expression invoke = Expression.Call(Expression.Constant(this), async?HandoffSendAsync:HandoffSend, + Expression.Constant(message), Expression.Constant(source), Expression.Constant(part), createArr); + + Delegate d = Expression.Lambda(message, invoke, peLst).Compile(); + + // Shouldn't need to use a weak delegate here. + evt.AddEventHandler(source, d); + } + + #endregion + + #region Message delivery + + private readonly LinkedList asyncMessages = new LinkedList(); + + public void Send(object source, params object[] args) + { + SendProxy(typeof(T), source, AsPart(source), args); + } + + public void Send(Type message, object source, params object[] args) + { + SendProxy(message, source, AsPart(source), args); + } + + public void SendProxy(object source, Part part, params object[] args) + { + SendProxy(typeof(T), source, part, args); + } + + public void SendProxy(Type messageCls, object source, Part part, params object[] args) + { + IPartMessage message = AsIPartMessage(messageCls); + CurrentEventInfoImpl info = new CurrentEventInfoImpl(message, source, part, args); + Send(info); + } + + public void SendAsync(object source, params object[] args) + { + SendAsyncProxy(typeof(T), source, AsPart(source), args); + } + + public void SendAsync(Type message, object source, params object[] args) + { + SendAsyncProxy(message, source, AsPart(source), args); + } + + public void SendAsyncProxy(object source, Part part, params object[] args) + { + SendAsyncProxy(typeof(T), source, part, args); + } + + public void SendAsyncProxy(Type messageCls, object source, Part part, params object[] args) + { + CurrentEventInfoImpl message = new CurrentEventInfoImpl(AsIPartMessage(messageCls), source, part, args); + + // Eat duplicate messages, just send the last one. + var node = asyncMessages.First; + while (node != null) + { + if (node.Value.Equals(message)) + { + var delete = node; + node = node.Next; + asyncMessages.Remove(delete); + } + else + node = node.Next; + } + + using (message.Push()) + { + if (filters != null && filters.Any(info => info.CheckPrereq(message) && info.Filter(message))) + return; + } + message.filterComplete = true; + + asyncMessages.AddLast(message); + } + + public void Update() + { + while (asyncMessages.Count > 0) + { + CurrentEventInfoImpl message = asyncMessages.First.Value; + asyncMessages.RemoveFirst(); + try + { + Send(message); + } + catch(Exception ex) + { + Debug.LogException(ex); + } + } + + if (HighLogic.LoadedSceneIsEditor) + UpdatePartHeirachy(); + } + + internal void Send(CurrentEventInfoImpl message) + { + if (!gameObject) + return; + + using (message.Push()) + { + if (!message.filterComplete && filters != null) + if (filters.Any(info => info.CheckPrereq(message) && info.Filter(message))) + return; + + // Send the message + foreach (IPartMessage currMessage in message.Message) + { + string messageName = currMessage.Name; + + LinkedList listenerList; + if (!listeners.TryGetValue(messageName, out listenerList)) + continue; + + // Shorten parameter list if required + object[] newArgs = null; + + for (var node = listenerList.First; node != null; ) + { + // hold reference for duration of call + ListenerInfo info = node.Value; + object target = info.Target; + if (target == null) + { + // Remove dead links from the list + var tmp = node; + node = node.Next; + listenerList.Remove(tmp); + continue; + } + + // Declarative event filtering + if (!info.CheckPrereq(message)) + { + node = node.Next; + continue; + } + + + if (newArgs == null) + newArgs = ShortenArgs(message.Arguments, currMessage.DelegateType); + + try + { + node.Value.method.Invoke(target, newArgs); + } + catch (TargetException ex) + { + // Swallow target exceptions, but not anything else. + Debug.LogError(string.Format("Invoking {0}.{1} to handle DelegateType {2} resulted in an exception.", target.GetType(), node.Value.method, CurrentEventInfo.Message)); + Debug.LogException(ex.InnerException); + } + + node = node.Next; + } + + } + } + } + + private static object[] ShortenArgs(object[] args, Type messageCls) + { + ParameterInfo[] methodParams = messageCls.GetMethod("Invoke").GetParameters(); + object[] newArgs = args; + if (args.Length > methodParams.Length) + { + newArgs = new object[methodParams.Length]; + Array.Copy(args, newArgs, methodParams.Length); + } + return newArgs; + } + #endregion + + #region Message Filters + + // Store the list of current filters in a thread static + [ThreadStatic] + private static LinkedList filters; + + /// + /// Register a message filter. This delegate will be called for every message sent from the source. + /// If it returns true, the message is considered handled and no futher processing will occour. + /// + /// The delegate for the filter + /// Message source, must match. If null will match all sources. + /// Part to filter. If null will match all parts. + /// Optional list of messages to match. If empty, all messages are matched. + /// Disposable object. When done call dispose. Works well with using clauses. + public IDisposable Filter(PartMessageFilter filter, object source = null, Part part = null, params Type[] messages) + { + FilterInfo info = new FilterInfo + { + Filter = filter + }; + + RegisterFilterInfo(source, part, messages, info); + + return info; + } + + /// + /// Consolidate messages. All messages sent by the source will be held until the returned object is destroyed. + /// Any duplicates of the same message will be swallowed silently. + /// + /// source to consolidate from. Null will match all sources + /// Part to filter. If null will match all parts. + /// messages to consolidate. If not specified, all messages are consolidated. + /// Disposable object. When done call dispose. Works well with using clauses. + public IDisposable Consolidate(object source = null, Part part = null, params Type[] messages) + { + FilterInfo consolidator = new MessageConsolidator(); + + RegisterFilterInfo(source, part, messages, consolidator); + + return consolidator; + } + + /// + /// Ignore messages sent by the source until the returned object is destroyed. + /// + /// Source to ignore. Null will ignore all sources. + /// Part to filter. If null will match all parts. + /// Messages to ignore. If not specified, all messages are ignored. + /// Disposable object. When done call dispose. Works well with using clauses. + public IDisposable Ignore(object source = null, Part part = null, params Type[] messages) + { + return Filter(message => true, source, part, messages); + } + + private void RegisterFilterInfo(object source, Part part, Type[] messages, FilterInfo info) + { + info.source = source; + info.part = part; + info.service = this; + + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < messages.Length; i++) + info.messages.Add(messages[i].FullName); + + if(filters == null) + filters = new LinkedList(); + + info.node = filters.AddFirst(info); + } + + internal class FilterInfo : IDisposable + { + public object source; + public Part part; + public PartMessageFilter Filter; + public HashSet messages = new HashSet(); + + public ServiceImpl service; + + public LinkedListNode node; + + public bool CheckPrereq(ICurrentEventInfo info) + { + if (source != null && source != info.Source) + return false; + if (part != null && part != info.SourcePart) + return false; + if (messages.Count == 0) + return true; + + return info.Message.Any(message => !messages.Contains(message.Name)); + } + + public virtual void Dispose() + { + if(service == null) + throw new InvalidOperationException("Already disposed"); + + filters.Remove(node); + if (filters.Count == 0) + filters = null; + service = null; + } + + ~FilterInfo() + { + if (service == null) + return; + Dispose(); + Debug.LogError("Warning: Filter has been created and not disposed prior to finalization. Please check the code."); + } + } + + internal class MessageConsolidator : FilterInfo + { + public MessageConsolidator() + { + Filter = ConsolidatingFilter; + } + + private LinkedList messageList = new LinkedList(); + + private bool ConsolidatingFilter(ICurrentEventInfo message) + { + // Remove any matching previous + messageList.RemoveAll(evt => evt.Equals(message)); + messageList.AddLast(message); + return true; + } + + public override void Dispose() + { + ServiceImpl theService = service; + base.Dispose(); + + // Safe as we've already deregistered the filter, so no loops. + foreach (ICurrentEventInfo message in messageList) + { + CurrentEventInfoImpl info = (CurrentEventInfoImpl)message; + info.filterComplete = false; + theService.Send((CurrentEventInfoImpl)message); + } + messageList = null; + } + + } + #endregion + + #region Startup + + internal void Awake() + { + // Clear the listeners list when reloaded. + GameEvents.onGameSceneLoadRequested.Add(SceneLoadedListener); + GameEvents.onInputLocksModified.Add(OnInputLocksModified); + GameEvents.onPartAttach.Add(OnPartAttach); + GameEvents.onPartRemove.Add(OnPartRemove); + } + + internal void OnDestroy() + { + GameEvents.onGameSceneLoadRequested.Remove(SceneLoadedListener); + GameEvents.onInputLocksModified.Remove(OnInputLocksModified); + GameEvents.onPartAttach.Remove(OnPartAttach); + GameEvents.onPartRemove.Remove(OnPartRemove); + listeners = null; + } + + private void SceneLoadedListener(GameScenes scene) + { + currRoot = null; + listeners.Clear(); + } + + private Part currRoot; + + private void OnInputLocksModified(GameEvents.FromToAction data) + { + if (!HighLogic.LoadedSceneIsEditor) + return; + + if (EditorLogic.fetch == null) + return; + + var ship = EditorLogic.fetch.ship; + + if (ship == null) + return; + + if (ship.parts.Count > 0) + { + if (!ReferenceEquals(ship.parts[0], currRoot)) + { + Part root = ship.parts[0]; + CheckKnown(root); + SendAsyncProxy(this, root); + currRoot = root; + } + } + else if(currRoot != null) + { + SendAsyncProxy(this, currRoot); + currRoot = null; + } + } + + private Part lastSelected; + + private void UpdatePartHeirachy() + { + Part selectedPart = EditorLogic.SelectedPart; + if (ReferenceEquals(lastSelected, selectedPart) + || (lastSelected = selectedPart) == null + || CheckKnown(selectedPart)) + return; + + // Parts that are clones (Alt-Click) won't have had their attach methods called. + PartAttachSymmetry(selectedPart); + } + + private bool CheckKnown(Part part) + { + if (!part.isClone) + return true; + + KnownPartMarker marker = part.gameObject.GetTaggedComponent(); + if (marker == null) + { + marker = part.gameObject.AddTaggedComponent(); + marker.known = true; + return false; + } + + bool ret = marker.known; + marker.known = true; + return ret; + } + + private void OnPartAttach(GameEvents.HostTargetAction data) + { + // Target is the parent, host is the child part + SendAsyncProxy(this, data.host, data.target); + SendAsyncProxy(this, data.target, data.host); + + if (CheckKnown(data.host)) + return; + + // Symmetry clones won't have had either of the above listeners called for any of their children + PartAttachSymmetry(data.host); + } + + private void PartAttachSymmetry(Part thisPart) + { + foreach (Part child in thisPart.children) + { + SendAsyncProxy(this, child, thisPart); + SendAsyncProxy(this, thisPart, child); + PartAttachSymmetry(child); + } + } + + private void OnPartRemove(GameEvents.HostTargetAction data) + { + // host is null, target is the child part. + SendAsyncProxy(this, data.target, new object[] { null }); + SendAsyncProxy(this, data.target.parent, data.target); + + if (data.target.attachMode == AttachModes.SRF_ATTACH) + data.target.srfAttachNode.attachedPart = null; + + } + + #endregion + + #region Conversion to IPartMessage + private readonly Dictionary cachedPartMessages = new Dictionary(); + + /// + /// Convert delegate type into the IPartMessage interface. + /// + /// Delegate type to convert. This must be a delegate type marked with the attribute. + public IPartMessage AsIPartMessage(Type type) + { + IPartMessage value; + if (cachedPartMessages.TryGetValue(type, out value)) + return value; + return cachedPartMessages[type] = new MessageImpl(this, type); + } + + /// + /// Convert delegate type into the IPartMessage interface. + /// + /// Delegate type to convert. This must be a delegate type marked with the attribute. + public IPartMessage AsIPartMessage() + { + return AsIPartMessage(typeof(T)); + } + #endregion + + #region Utility Functions + private static Part AsPart(object src) + { + Part part = src as Part; + if (part != null) + return part; + PartModule module = src as PartModule; + if (module != null) + return module.part; + + if(src.GetType().GetInterfaces().Any(t => t.FullName == typeof (IPartMessagePartProxy).FullName || t.FullName == "KSPAPIExtensions.PartMessagePartProxy")) + return DuckTyping.Cast(src).ProxyPart; + return null; + } + #endregion + } + + #region Initialization and Other Mod Interfacing + internal class PartMessageServiceInitializer : MonoBehaviour + { + private static bool loadedInScene; + + internal void Awake() + { + // Ensure that only one copy of the service is run per scene change. + if (loadedInScene) + { + Assembly currentAssembly = Assembly.GetExecutingAssembly(); + Debug.Log("[PartMessageService] Multiple copies of current version. Using the first copy. Version: " + currentAssembly.GetName().Version); + Destroy(gameObject); + return; + } + loadedInScene = true; + + if (!SystemUtils.RunTypeElection(typeof(PartMessageService), "KSPAPIExtensions")) + return; + + // So at this point we know we have won the election, and will be using the class versions as in this assembly. + + // Destroy the old service + if (PartMessageService._instance != null) + { + Debug.Log("[PartMessageService] destroying service from previous load"); + Destroy(((ServiceImpl)PartMessageService._instance).gameObject); + } + + // Create the part message service + GameObject serviceGo = new GameObject(PartMessageService.PartMessageServiceName); + DontDestroyOnLoad(serviceGo); + + // Assign the service to the static variable + PartMessageService._instance = serviceGo.AddTaggedComponent(); + + // At this point the losers will duck-type themselves to the latest version of the service if they're called. + + ListenerFerramAerospaceResearch.AddListener(serviceGo); + } + + public void Update() + { + loadedInScene = false; + Destroy(gameObject); + } + } + + internal class KnownPartMarker : MonoBehaviour + { + [NonSerialized] + internal bool known; + } + + internal class ListenerFerramAerospaceResearch : MonoBehaviour + { + // ReSharper disable once InconsistentNaming + private static Action SetBasicDragModuleProperties; + + public static void AddListener(GameObject serviceGo) + { + Type typeFARAeroUtil = Type.GetType("ferram4.FARAeroUtil", false); + if (typeFARAeroUtil == null) + return; + + MethodInfo info = typeFARAeroUtil.GetMethod("SetBasicDragModuleProperties", new[] { typeof(Part) }); + + if (info == null) + { + Debug.LogWarning("[PartMessageService] FAR update method seems to have changed. Cannot interface with FAR."); + return; + } + + SetBasicDragModuleProperties = (Action)Delegate.CreateDelegate(typeFARAeroUtil, info); + + serviceGo.AddTaggedComponent(); + } + + private IPartMessageService service; + + public void Awake() + { + service = PartMessageService.Instance; + GameEvents.onGameSceneLoadRequested.Add(GameSceneLoaded); + } + + private void GameSceneLoaded(GameScenes data) + { + if(!GameSceneFilter.AnyEditorOrFlight.IsLoaded()) + return; + + PartMessageService.Register(this); + } + + [PartMessageListener(typeof(PartModelChanged), relations:PartRelationship.Unknown, scenes: GameSceneFilter.AnyEditorOrFlight)] + public void PartModelChanged() + { + Part part = service.CurrentEventInfo.SourcePart; + if (part == null) + return; + + SetBasicDragModuleProperties(part); + } + } + + + #endregion + + #region Message Enumerator + internal class MessageEnumerable : IEnumerable + { + internal MessageEnumerable(Type message) + { + this.message = message; + } + + readonly internal Type message; + + IEnumerator IEnumerable.GetEnumerator() + { + return new MessageEnumerator(message); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return new MessageEnumerator(message); + } + + private class MessageEnumerator : IEnumerator + { + + public MessageEnumerator(Type top) + { + current = this.top = top; + } + + private int pos = -1; + private Type current; + private Type top; + + object IEnumerator.Current + { + get + { + if (pos != 0) + throw new InvalidOperationException(); + return current; + } + } + + Type IEnumerator.Current + { + get + { + if (pos != 0) + throw new InvalidOperationException(); + return current; + } + } + + bool IEnumerator.MoveNext() + { + switch (pos) + { + case -1: + current = top; + pos = 0; + break; + case 1: + return false; + case 0: + PartMessageDelegate evt = (PartMessageDelegate)current.GetCustomAttributes(typeof(PartMessageDelegate), true)[0]; + current = evt.Parent; + break; + case 2: + throw new InvalidOperationException("Enumerator disposed"); + } + if (current == null) + { + pos = 1; + return false; + } + return true; + } + + void IEnumerator.Reset() + { + pos = -1; + current = null; + } + + void IDisposable.Dispose() + { + current = top = null; + pos = 2; + } + } + } + #endregion + +} diff --git a/Source/KSPAPIExt/Properties/AssemblyInfo.cs b/Source/KSPAPIExt/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..7c18db2e --- /dev/null +++ b/Source/KSPAPIExt/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("KSPAPIExtensions")] +[assembly: AssemblyDescription("A set of utilities for plugin developers for Kerbal Space Program")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("swamp_ig and taniwha")] +[assembly: AssemblyProduct("KSPAPIExtensions")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e46ed9d3-b992-4f9a-8e9a-64536116a2c4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.7.4.9")] +[assembly: KSPAssembly("KSPAPIExtensions", 1, 7)] diff --git a/Source/KSPAPIExt/Tweakables/UIPartActionChooseOption.cs b/Source/KSPAPIExt/Tweakables/UIPartActionChooseOption.cs new file mode 100644 index 00000000..4719996c --- /dev/null +++ b/Source/KSPAPIExt/Tweakables/UIPartActionChooseOption.cs @@ -0,0 +1,217 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + [UI_ChooseOption] + public class UIPartActionChooseOption : UIPartActionFieldItem + { + public SpriteText fieldName; + public UIButton incDown; + public UIButton incUp; + public UIProgressSlider slider; + + private int selectedIdx = -1; + //private string selectedValue; + + public static UIPartActionChooseOption CreateTemplate() + { + // Create the control + GameObject editGo = new GameObject("UIPartActionChooseOption", SystemUtils.VersionTaggedType(typeof(UIPartActionChooseOption))); + UIPartActionChooseOption edit = editGo.GetTaggedComponent(); + editGo.SetActive(false); + + // TODO: since I don'type have access to EZE GUI, I'm copying out bits from other existing GUIs + // if someone does have access, they could do this better although really it works pretty well. + UIPartActionButton evtp = UIPartActionController.Instance.eventItemPrefab; + GameObject srcTextGo = evtp.transform.Find("Text").gameObject; + GameObject srcBackgroundGo = evtp.transform.Find("Background").gameObject; + GameObject srcButtonGo = evtp.transform.Find("Btn").gameObject; + + UIPartActionFloatRange paFlt = (UIPartActionFloatRange)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange)); + GameObject srcSliderGo = paFlt.transform.Find("Slider").gameObject; + + + // Start building our control + GameObject backgroundGo = (GameObject)Instantiate(srcBackgroundGo); + backgroundGo.transform.parent = editGo.transform; + + GameObject sliderGo = (GameObject)Instantiate(srcSliderGo); + sliderGo.transform.parent = editGo.transform; + sliderGo.transform.localScale = new Vector3(0.81f, 1, 1); + edit.slider = sliderGo.GetComponent(); + edit.slider.ignoreDefault = true; + + GameObject fieldNameGo = (GameObject)Instantiate(srcTextGo); + fieldNameGo.transform.parent = editGo.transform; + fieldNameGo.transform.localPosition = new Vector3(24, -8, 0); + edit.fieldName = fieldNameGo.GetComponent(); + + GameObject incDownGo = (GameObject)Instantiate(srcButtonGo); + incDownGo.transform.parent = edit.transform; + incDownGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); + incDownGo.transform.localPosition = new Vector3(11.5f, -9, 0); //>11 + edit.incDown = incDownGo.GetComponent(); + + GameObject incDownLabelGo = (GameObject)Instantiate(srcTextGo); + incDownLabelGo.transform.parent = editGo.transform; + incDownLabelGo.transform.localPosition = new Vector3(5.5f, -7, 0); // <6 + SpriteText incDownLabel = incDownLabelGo.GetComponent(); + incDownLabel.Text = "<<"; + + GameObject incUpGo = (GameObject)Instantiate(srcButtonGo); + incUpGo.transform.parent = edit.transform; + incUpGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); + incUpGo.transform.localPosition = new Vector3(187.5f, -9, 0); // >187 + edit.incUp = incUpGo.GetComponent(); + + GameObject incUpLabelGo = (GameObject)Instantiate(srcTextGo); + incUpLabelGo.transform.parent = editGo.transform; + incUpLabelGo.transform.localPosition = new Vector3(181.5f, -7, 0); //<182 + SpriteText incUpLabel = incUpLabelGo.GetComponent(); + incUpLabel.Text = ">>"; + + return edit; + } + + protected UI_ChooseOption FieldInfo + { + get + { + return (UI_ChooseOption)control; + } + } + + // ReSharper disable ParameterHidesMember + public override void Setup(UIPartActionWindow window, Part part, PartModule partModule, UI_Scene scene, UI_Control control, BaseField field) + { + base.Setup(window, part, partModule, scene, control, field); + incDown.SetValueChangedDelegate(obj => IncrementValue(false)); + incUp.SetValueChangedDelegate(obj => IncrementValue(true)); + slider.SetValueChangedDelegate(OnValueChanged); + } + // ReSharper restore ParameterHidesMember + + private void IncrementValue(bool up) + { + if (FieldInfo.options == null || FieldInfo.options.Length == 0) + selectedIdx = -1; + else + selectedIdx = (selectedIdx + FieldInfo.options.Length + (up ? 1 : -1)) % FieldInfo.options.Length; + SetValueFromIdx(); + } + + private void OnValueChanged(IUIObject obj) + { + slider.SetValueChangedDelegate(null); + if (FieldInfo.options == null || FieldInfo.options.Length == 0) + selectedIdx = -1; + else + selectedIdx = Mathf.RoundToInt(slider.Value * (FieldInfo.options.Length - 1)); + SetValueFromIdx(); + slider.SetValueChangedDelegate(OnValueChanged); + } + + private void SetValueFromIdx() + { + if (selectedIdx >= 0) + { + if (field.FieldInfo.FieldType == typeof(int)) + { + field.SetValue(selectedIdx, field.host); + if (scene == UI_Scene.Editor) + SetSymCounterpartValue(selectedIdx); + } + else + { + string selectedValue = FieldInfo.options[selectedIdx]; + field.SetValue(selectedValue, field.host); + if (scene == UI_Scene.Editor) + SetSymCounterpartValue(selectedValue); + } + } + UpdateControls(); + } + + private void UpdateControls() + { + if (selectedIdx < 0) + { + fieldName.Text = "**Not Found**"; + slider.Value = 0; + return; + } + + if (FieldInfo.display != null && FieldInfo.display.Length > selectedIdx) + { + fieldName.Text = field.guiName + ": " + FieldInfo.display[selectedIdx]; + } + else + { + fieldName.Text = field.guiName + ": " + FieldInfo.options[selectedIdx]; + } + int length = (FieldInfo.options ?? FieldInfo.display).Length; + if (length > 1) + { + slider.Value = selectedIdx / (float)(length - 1); + } + else + { + slider.Value = 1; + } + } + + + bool exceptionPrinted; + public override void UpdateItem() + { + try + { + if (field.FieldInfo.FieldType == typeof(int)) + { + int newSelectedIdx = field.GetValue(field.host); + if (selectedIdx == newSelectedIdx) + return; + + selectedIdx = newSelectedIdx; + if (FieldInfo.options == null || selectedIdx < 0 || selectedIdx >= FieldInfo.options.Length) + selectedIdx = -1; + } + else + { + string newSelectedValue = field.GetValue(field.host); + if (selectedIdx >= 0 && newSelectedValue == FieldInfo.options[selectedIdx]) + return; + + selectedIdx = -1; + for (int i = 0; i < FieldInfo.options.Length; ++i) + if (newSelectedValue == FieldInfo.options[i]) + { + selectedIdx = i; + break; + } + } + UpdateControls(); + exceptionPrinted = false; + } + catch (Exception ex) + { + if (!exceptionPrinted) + print(ex); + exceptionPrinted = true; + } + } + } + + + // ReSharper disable once InconsistentNaming + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] + public class UI_ChooseOption : UI_Control + { + + public string[] options; + public string[] display; + + } +} diff --git a/Source/KSPAPIExt/Tweakables/UIPartActionFloatEdit.cs b/Source/KSPAPIExt/Tweakables/UIPartActionFloatEdit.cs new file mode 100644 index 00000000..6e09ee1c --- /dev/null +++ b/Source/KSPAPIExt/Tweakables/UIPartActionFloatEdit.cs @@ -0,0 +1,396 @@ +using System; +using System.Globalization; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + + [UI_FloatEdit] + public class UIPartActionFloatEdit : UIPartActionFieldItem + { + public SpriteText fieldName; + public SpriteText fieldValue; + public UIButton incLargeDown; + public SpriteText incLargeDownLabel; + public UIButton incSmallDown; + public SpriteText incSmallDownLabel; + public UIButton incSmallUp; + public SpriteText incSmallUpLabel; + public UIButton incLargeUp; + public SpriteText incLargeUpLabel; + public UIProgressSlider slider; + + private float value; + private uint controlState; + + public static UIPartActionFloatEdit CreateTemplate() + { + // Create the control + GameObject editGo = new GameObject("UIPartActionFloatEdit", SystemUtils.VersionTaggedType(typeof(UIPartActionFloatEdit))); + UIPartActionFloatEdit edit = editGo.GetTaggedComponent(); + editGo.SetActive(false); + + // TODO: since I don'type have access to EZE GUI, I'm copying out bits from other existing GUIs + // if someone does have access, they could do this better although really it works pretty well. + UIPartActionButton evtp = UIPartActionController.Instance.eventItemPrefab; + GameObject srcTextGo = evtp.transform.Find("Text").gameObject; + GameObject srcBackgroundGo = evtp.transform.Find("Background").gameObject; + GameObject srcButtonGo = evtp.transform.Find("Btn").gameObject; + + UIPartActionFloatRange paFlt = (UIPartActionFloatRange)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange)); + GameObject srcSliderGo = paFlt.transform.Find("Slider").gameObject; + + + // Start building our control + GameObject backgroundGo = (GameObject)Instantiate(srcBackgroundGo); + backgroundGo.transform.parent = editGo.transform; + + GameObject sliderGo = (GameObject)Instantiate(srcSliderGo); + sliderGo.transform.parent = editGo.transform; + sliderGo.transform.localScale = new Vector3(0.65f, 1, 1); + edit.slider = sliderGo.GetComponent(); + edit.slider.ignoreDefault = true; + + + GameObject fieldNameGo = (GameObject)Instantiate(srcTextGo); + fieldNameGo.transform.parent = editGo.transform; + fieldNameGo.transform.localPosition = new Vector3(40, -8, 0); + edit.fieldName = fieldNameGo.GetComponent(); + + GameObject fieldValueGo = (GameObject)Instantiate(srcTextGo); + fieldValueGo.transform.parent = editGo.transform; + fieldValueGo.transform.localPosition = new Vector3(110, -8, 0); + edit.fieldValue = fieldValueGo.GetComponent(); + + + GameObject incLargeDownGo = (GameObject)Instantiate(srcButtonGo); + incLargeDownGo.transform.parent = edit.transform; + incLargeDownGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); + incLargeDownGo.transform.localPosition = new Vector3(11.5f, -9, 0); //>11 + edit.incLargeDown = incLargeDownGo.GetComponent(); + + GameObject incLargeDownLabelGo = (GameObject)Instantiate(srcTextGo); + incLargeDownLabelGo.transform.parent = editGo.transform; + incLargeDownLabelGo.transform.localPosition = new Vector3(5.5f, -7, 0); // <6 + edit.incLargeDownLabel = incLargeDownLabelGo.GetComponent(); + edit.incLargeDownLabel.Text = "<<"; + + + GameObject incSmallDownGo = (GameObject)Instantiate(srcButtonGo); + incSmallDownGo.transform.parent = edit.transform; + incSmallDownGo.transform.localScale = new Vector3(0.35f, 1.1f, 1f); + incSmallDownGo.transform.localPosition = new Vector3(29, -9, 0); // <31.5 + edit.incSmallDown = incSmallDownGo.GetComponent(); + + GameObject incSmallDownLabelGo = (GameObject)Instantiate(srcTextGo); + incSmallDownLabelGo.transform.parent = editGo.transform; + incSmallDownLabelGo.transform.localPosition = new Vector3(25.5f, -7, 0); //<28 + edit.incSmallDownLabel = incSmallDownLabelGo.GetComponent(); + edit.incSmallDownLabel.Text = "<"; + + GameObject incSmallUpGo = (GameObject)Instantiate(srcButtonGo); + incSmallUpGo.transform.parent = edit.transform; + incSmallUpGo.transform.localScale = new Vector3(0.35f, 1.1f, 1f); + incSmallUpGo.transform.localPosition = new Vector3(170, -9, 0); + edit.incSmallUp = incSmallUpGo.GetComponent(); + + GameObject incSmallUpLabelGo = (GameObject)Instantiate(srcTextGo); + incSmallUpLabelGo.transform.parent = editGo.transform; + incSmallUpLabelGo.transform.localPosition = new Vector3(167.5f, -7, 0); //<168 + edit.incSmallUpLabel = incSmallUpLabelGo.GetComponent(); + edit.incSmallUpLabel.Text = ">"; + + GameObject incLargeUpGo = (GameObject)Instantiate(srcButtonGo); + incLargeUpGo.transform.parent = edit.transform; + incLargeUpGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); + incLargeUpGo.transform.localPosition = new Vector3(187.5f, -9, 0); // >187 + edit.incLargeUp = incLargeUpGo.GetComponent(); + + GameObject incLargeUpLabelGo = (GameObject)Instantiate(srcTextGo); + incLargeUpLabelGo.transform.parent = editGo.transform; + incLargeUpLabelGo.transform.localPosition = new Vector3(181.5f, -7, 0); //<182 + edit.incLargeUpLabel = incLargeUpLabelGo.GetComponent(); + edit.incLargeUpLabel.Text = ">>"; + return edit; + } + + + protected UI_FloatEdit FieldInfo + { + get + { + return (UI_FloatEdit)control; + } + } + + // ReSharper disable ParameterHidesMember + public override void Setup(UIPartActionWindow window, Part part, PartModule partModule, UI_Scene scene, UI_Control control, BaseField field) + { + base.Setup(window, part, partModule, scene, control, field); + incLargeDown.SetValueChangedDelegate(obj => buttons_ValueChanged(false, true)); + incSmallDown.SetValueChangedDelegate(obj => buttons_ValueChanged(false, false)); + incSmallUp.SetValueChangedDelegate(obj => buttons_ValueChanged(true, false)); + incLargeUp.SetValueChangedDelegate(obj => buttons_ValueChanged(true, true)); + slider.SetValueChangedDelegate(slider_OnValueChanged); + + // so update runs. + value = GetFieldValue() + 0.1f; + UpdateFieldInfo(); + } + // ReSharper restore ParameterHidesMember + + private void buttons_ValueChanged(bool up, bool large) + { + float increment = (large ? FieldInfo.incrementLarge : FieldInfo.incrementSmall); + float excess = value % increment; + float newValue; + if (up) + { + if (increment - excess < FieldInfo.incrementSlide / 2) + newValue = value - excess + increment * 2; + else + newValue = value - excess + increment; + } + else + { + if (excess < FieldInfo.incrementSlide / 2) + newValue = value - excess - increment; + else + newValue = value - excess; + } + SetValueFromGUI(newValue); + } + + private void slider_OnValueChanged(IUIObject obj) + { + float valueLow, valueHi; + SliderRange(value, out valueLow, out valueHi); + + float newValue = Mathf.Lerp(valueLow, valueHi, slider.Value); + + // ReSharper disable CompareOfFloatsByEqualityOperator + if (FieldInfo.incrementLarge == 0 || valueHi == FieldInfo.maxValue) + { + if (newValue > valueHi) + newValue = valueHi; + } + else if (newValue > valueHi - FieldInfo.incrementSlide) + newValue = valueHi - FieldInfo.incrementSlide; + + SetValueFromGUI(newValue); + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + private void SetValueFromGUI(float newValue) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (FieldInfo.incrementSlide != 0) + newValue = Mathf.Round(newValue / FieldInfo.incrementSlide) * FieldInfo.incrementSlide; + + if (newValue < FieldInfo.minValue) + newValue = FieldInfo.minValue; + else if (newValue > FieldInfo.maxValue) + newValue = FieldInfo.maxValue; + + UpdateValueDisplay(newValue); + + field.SetValue(newValue, field.host); + if (scene == UI_Scene.Editor) + SetSymCounterpartValue(newValue); + } + + private void SliderRange(float newValue, out float valueLow, out float valueHi) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + if (FieldInfo.incrementLarge == 0) + { + valueLow = FieldInfo.minValue; + valueHi = FieldInfo.maxValue; + return; + } + + if (FieldInfo.incrementSmall == 0) + { + valueLow = Mathf.Floor((newValue + FieldInfo.incrementSlide / 2f) / FieldInfo.incrementLarge) * FieldInfo.incrementLarge; + valueHi = valueLow + FieldInfo.incrementLarge; + if (valueLow == FieldInfo.maxValue) + { + valueHi = valueLow; + valueLow -= FieldInfo.incrementLarge; + } + } + else + { + valueLow = Mathf.Floor((newValue + FieldInfo.incrementSlide / 2f) / FieldInfo.incrementSmall) * FieldInfo.incrementSmall; + valueHi = valueLow + FieldInfo.incrementSmall; + if (valueLow == FieldInfo.maxValue) + { + valueHi = valueLow; + valueLow -= FieldInfo.incrementSmall; + } + } + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + private float GetFieldValue() + { + return isModule ? field.GetValue(partModule) : field.GetValue(part); + } + + public override void UpdateItem() + { + // update from fieldName. No listeners. + fieldName.Text = field.guiName; + + // Update the value. + float fValue = GetFieldValue(); + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (fValue != value) + UpdateValueDisplay(fValue); + + uint newHash = FieldInfo.GetHashedState(); + if (controlState != newHash) + { + UpdateFieldInfo(); + controlState = newHash; + } + } + + private void UpdateValueDisplay(float newValue) + { + this.value = newValue; + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (FieldInfo.incrementSlide != 0) + newValue = Mathf.Round(newValue / FieldInfo.incrementSlide) * FieldInfo.incrementSlide; + + float valueLow, valueHi; + SliderRange(newValue, out valueLow, out valueHi); + slider.Value = Mathf.InverseLerp(valueLow, valueHi, newValue); + + fieldValue.Text = newValue.ToStringExt(field.guiFormat) + field.guiUnits; + } + + private void UpdateFieldInfo() + { + // ReSharper disable CompareOfFloatsByEqualityOperator + if (FieldInfo.incrementLarge == 0.0) + { + incLargeDown.gameObject.SetActive(false); + incLargeDownLabel.gameObject.SetActive(false); + incLargeUp.gameObject.SetActive(false); + incLargeUpLabel.gameObject.SetActive(false); + + incSmallDown.gameObject.SetActive(false); + incSmallDownLabel.gameObject.SetActive(false); + incSmallUp.gameObject.SetActive(false); + incSmallUpLabel.gameObject.SetActive(false); + + slider.transform.localScale = Vector3.one; + fieldName.transform.localPosition = new Vector3(6, -8, 0); + } + else if (FieldInfo.incrementSmall == 0.0) + { + incLargeDown.gameObject.SetActive(true); + incLargeDownLabel.gameObject.SetActive(true); + incLargeUp.gameObject.SetActive(true); + incLargeUpLabel.gameObject.SetActive(true); + + incSmallDown.gameObject.SetActive(false); + incSmallDownLabel.gameObject.SetActive(false); + incSmallUp.gameObject.SetActive(false); + incSmallUpLabel.gameObject.SetActive(false); + + slider.transform.localScale = new Vector3(0.81f, 1, 1); + fieldName.transform.localPosition = new Vector3(24, -8, 0); //>23 + } + else + { + incLargeDown.gameObject.SetActive(true); + incLargeDownLabel.gameObject.SetActive(true); + incLargeUp.gameObject.SetActive(true); + incLargeUpLabel.gameObject.SetActive(true); + + incSmallDown.gameObject.SetActive(true); + incSmallDownLabel.gameObject.SetActive(true); + incSmallUp.gameObject.SetActive(true); + incSmallUpLabel.gameObject.SetActive(true); + + slider.transform.localScale = new Vector3(0.64f, 1, 1); + fieldName.transform.localPosition = new Vector3(40, -8, 0); + } + + if (FieldInfo.incrementSlide == 0) + slider.gameObject.SetActive(false); + else + slider.gameObject.SetActive(true); + // ReSharper restore CompareOfFloatsByEqualityOperator + } + } + + // ReSharper disable once InconsistentNaming + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] + public class UI_FloatEdit : UI_Control + { + private const string UIControlName = "UIPartActionFloatEdit"; + + public float minValue = float.NegativeInfinity; + public float maxValue = float.PositiveInfinity; + + public float incrementLarge = 0; + public float incrementSmall = 0; + public float incrementSlide = 0; + + public override void Load(ConfigNode node, object host) + { + base.Load(node, host); + if (!ParseFloat(out minValue, node, "minValue", UIControlName, null)) + { + minValue = float.NegativeInfinity; + } + if (!ParseFloat(out maxValue, node, "maxValue", UIControlName, null)) + { + maxValue = float.PositiveInfinity; + } + if (!ParseFloat(out incrementLarge, node, "incrementLarge", UIControlName, null)) + { + incrementLarge = 0.0f; + } + if (!ParseFloat(out incrementSmall, node, "incrementSmall", UIControlName, null)) + { + incrementSmall = 0.0f; + } + if (!ParseFloat(out incrementSlide, node, "incrementSlide", UIControlName, null)) + { + incrementSlide = 0.0f; + } + } + + public override void Save(ConfigNode node, object host) + { + base.Save(node, host); + if (!float.IsNegativeInfinity(minValue)) + node.AddValue("minValue", minValue.ToString(CultureInfo.InvariantCulture)); + if (!float.IsPositiveInfinity(maxValue)) + node.AddValue("maxValue", maxValue.ToString(CultureInfo.InvariantCulture)); + // ReSharper disable CompareOfFloatsByEqualityOperator + if (incrementLarge != 0.0f) + node.AddValue("incrementLarge", incrementLarge.ToString(CultureInfo.InvariantCulture)); + if (incrementSmall != 0.0f) + node.AddValue("incrementSmall", incrementSmall.ToString(CultureInfo.InvariantCulture)); + if (incrementSlide != 0.0f) + node.AddValue("incrementSlide", incrementSlide.ToString(CultureInfo.InvariantCulture)); + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + internal unsafe uint GetHashedState() + { + // ReSharper disable LocalVariableHidesMember + fixed (float* minValue = &this.minValue, maxValue = &this.maxValue, incrementLarge = &this.incrementLarge, incrementSmall = &this.incrementSmall, incrementSlide = &this.incrementSlide) + { + return *((uint*)minValue) ^ *((uint*)maxValue) ^ *((uint*)incrementLarge) ^ *((uint*)incrementSmall) ^ *((uint*)incrementSlide); + } + // ReSharper restore LocalVariableHidesMember + } + } +} diff --git a/Source/KSPAPIExt/Tweakables/UIPartActionScaleEdit.cs b/Source/KSPAPIExt/Tweakables/UIPartActionScaleEdit.cs new file mode 100644 index 00000000..b09c0252 --- /dev/null +++ b/Source/KSPAPIExt/Tweakables/UIPartActionScaleEdit.cs @@ -0,0 +1,332 @@ +using System; +using System.Globalization; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + + [UI_ScaleEdit] + public class UIPartActionScaleEdit : UIPartActionFieldItem + { + public SpriteText fieldName; + public SpriteText fieldValue; + public UIButton incLargeDown; + public SpriteText incLargeDownLabel; + public UIButton incLargeUp; + public SpriteText incLargeUpLabel; + public UIProgressSlider slider; + + private float value; + public int intervalNo = 0; + + private uint controlState; + + public static UIPartActionScaleEdit CreateTemplate() + { + // Create the control + GameObject editGo = new GameObject("UIPartActionScaleEdit", SystemUtils.VersionTaggedType(typeof(UIPartActionScaleEdit))); + UIPartActionScaleEdit edit = editGo.GetTaggedComponent(); + editGo.SetActive(false); + + // TODO: since I don'type have access to EZE GUI, I'm copying out bits from other existing GUIs + // if someone does have access, they could do this better although really it works pretty well. + UIPartActionButton evtp = UIPartActionController.Instance.eventItemPrefab; + GameObject srcTextGo = evtp.transform.Find("Text").gameObject; + GameObject srcBackgroundGo = evtp.transform.Find("Background").gameObject; + GameObject srcButtonGo = evtp.transform.Find("Btn").gameObject; + + UIPartActionFloatRange paFlt = (UIPartActionFloatRange)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange)); + GameObject srcSliderGo = paFlt.transform.Find("Slider").gameObject; + + + // Start building our control + GameObject backgroundGo = (GameObject)Instantiate(srcBackgroundGo); + backgroundGo.transform.parent = editGo.transform; + + GameObject sliderGo = (GameObject)Instantiate(srcSliderGo); + sliderGo.transform.parent = editGo.transform; + sliderGo.transform.localScale = new Vector3(0.65f, 1, 1); + edit.slider = sliderGo.GetComponent(); + edit.slider.ignoreDefault = true; + + + GameObject fieldNameGo = (GameObject)Instantiate(srcTextGo); + fieldNameGo.transform.parent = editGo.transform; + fieldNameGo.transform.localPosition = new Vector3(40, -8, 0); + edit.fieldName = fieldNameGo.GetComponent(); + + GameObject fieldValueGo = (GameObject)Instantiate(srcTextGo); + fieldValueGo.transform.parent = editGo.transform; + fieldValueGo.transform.localPosition = new Vector3(110, -8, 0); + edit.fieldValue = fieldValueGo.GetComponent(); + + + GameObject incLargeDownGo = (GameObject)Instantiate(srcButtonGo); + incLargeDownGo.transform.parent = edit.transform; + incLargeDownGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); + incLargeDownGo.transform.localPosition = new Vector3(11.5f, -9, 0); //>11 + edit.incLargeDown = incLargeDownGo.GetComponent(); + + GameObject incLargeDownLabelGo = (GameObject)Instantiate(srcTextGo); + incLargeDownLabelGo.transform.parent = editGo.transform; + incLargeDownLabelGo.transform.localPosition = new Vector3(5.5f, -7, 0); // <6 + edit.incLargeDownLabel = incLargeDownLabelGo.GetComponent(); + edit.incLargeDownLabel.Text = "<<"; + + GameObject incLargeUpGo = (GameObject)Instantiate(srcButtonGo); + incLargeUpGo.transform.parent = edit.transform; + incLargeUpGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); + incLargeUpGo.transform.localPosition = new Vector3(187.5f, -9, 0); // >187 + edit.incLargeUp = incLargeUpGo.GetComponent(); + + GameObject incLargeUpLabelGo = (GameObject)Instantiate(srcTextGo); + incLargeUpLabelGo.transform.parent = editGo.transform; + incLargeUpLabelGo.transform.localPosition = new Vector3(181.5f, -7, 0); //<182 + edit.incLargeUpLabel = incLargeUpLabelGo.GetComponent(); + edit.incLargeUpLabel.Text = ">>"; + return edit; + } + + + protected UI_ScaleEdit FieldInfo + { + get + { + return (UI_ScaleEdit)control; + } + } + + // ReSharper disable ParameterHidesMember + public override void Setup(UIPartActionWindow window, Part part, PartModule partModule, UI_Scene scene, UI_Control control, BaseField field) + { + base.Setup(window, part, partModule, scene, control, field); + incLargeDown.SetValueChangedDelegate(obj => buttons_ValueChanged(false)); + incLargeUp.SetValueChangedDelegate(obj => buttons_ValueChanged(true)); + slider.SetValueChangedDelegate(slider_OnValueChanged); + + // so update runs. + value = GetFieldValue() + 0.1f; + UpdateFieldInfo(); + } + // ReSharper restore ParameterHidesMember + + private void buttons_ValueChanged(bool up) + { + float newValue = this.value; + if (up) + { + if (intervalNo < FieldInfo.intervals.Length - 2) + { + if (newValue == FieldInfo.intervals[intervalNo+1]) + newValue = FieldInfo.intervals[intervalNo+2]; + intervalNo++; + } + else + newValue = FieldInfo.intervals [intervalNo+1]; + } + else + { + if (intervalNo > 0) + { + if (newValue == FieldInfo.intervals[intervalNo]) + newValue = FieldInfo.intervals[intervalNo-1]; + intervalNo--; + } + else + newValue = FieldInfo.intervals [0]; + } + RestrictToInterval (newValue); + } + + private void slider_OnValueChanged(IUIObject obj) + { + float valueLow = FieldInfo.intervals [intervalNo]; + float valueHi = FieldInfo.intervals [intervalNo + 1]; + float newValue = Mathf.Lerp(valueLow, valueHi, slider.Value); + + // ReSharper disable once CompareOfFloatsByEqualityOperator + float inc = GetIncrementSlide (); + if (inc != 0) + newValue = valueLow + Mathf.Round((newValue-valueLow) / inc) * inc; + + SetValueFromGUI(newValue); + } + + private void OnValueChanged(float newValue) + { + //update intervalNo + intervalNo = 0; + + for( int i=0; i= FieldInfo.intervals[i]) + intervalNo = i; + + UpdateValueDisplay (newValue); + } + + private void RestrictToInterval(float newValue) + { + newValue = Math.Max(newValue, FieldInfo.intervals [intervalNo]); + newValue = Math.Min(newValue, FieldInfo.intervals [intervalNo + 1]); + + SetValueFromGUI(newValue); + } + + private void SetValueFromGUI(float newValue) + { + UpdateValueDisplay(newValue); + + field.SetValue(newValue, field.host); + if (scene == UI_Scene.Editor) + SetSymCounterpartValue(newValue); + } + + private float GetFieldValue() + { + return isModule ? field.GetValue(partModule) : field.GetValue(part); + } + + private float GetIncrementSlide() + { + if (FieldInfo.incrementSlide.Length > 1) + return FieldInfo.incrementSlide [intervalNo]; + else if (FieldInfo.incrementSlide.Length == 1) + return FieldInfo.incrementSlide [0]; + else + return 0; + } + + public override void UpdateItem() + { + // update from fieldName. No listeners. + fieldName.Text = field.guiName; + + // Update the value. + float fValue = GetFieldValue(); + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (fValue != value) + OnValueChanged(fValue); + + uint newHash = FieldInfo.GetHashedState(); + if (controlState != newHash) + { + UpdateFieldInfo(); + controlState = newHash; + } + } + + private void UpdateValueDisplay(float newValue) + { + this.value = newValue; + // ReSharper disable once CompareOfFloatsByEqualityOperator + float inc = GetIncrementSlide(); + if (inc != 0) + { + float valueLow = FieldInfo.intervals [intervalNo]; + float valueHi = FieldInfo.intervals [intervalNo + 1]; + newValue = valueLow + Mathf.Round((newValue - valueLow) / inc) * inc; + slider.gameObject.SetActive(true); + slider.Value = Mathf.InverseLerp (valueLow, valueHi, newValue); + } + else + slider.gameObject.SetActive(false); + + fieldValue.Text = newValue.ToStringExt(field.guiFormat) + field.guiUnits; + } + + private void UpdateFieldInfo() + { + if (CheckConsistency ()) + { + incLargeDown.gameObject.SetActive (true); + incLargeDownLabel.gameObject.SetActive (true); + incLargeUp.gameObject.SetActive (true); + incLargeUpLabel.gameObject.SetActive (true); + + slider.transform.localScale = new Vector3(0.81f, 1, 1); + fieldName.transform.localPosition = new Vector3(24, -8, 0); + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (GetIncrementSlide() == 0) + slider.gameObject.SetActive(false); + else + slider.gameObject.SetActive(true); + } + else + { + incLargeDown.gameObject.SetActive (false); + incLargeDownLabel.gameObject.SetActive (false); + incLargeUp.gameObject.SetActive (false); + incLargeUpLabel.gameObject.SetActive (false); + + slider.gameObject.SetActive(false); + } + } + + public bool CheckConsistency() + { + if (FieldInfo.intervals.Length < 2) + return false; + + if ((FieldInfo.incrementSlide.Length > 1) && + (FieldInfo.incrementSlide.Length < FieldInfo.intervals.Length - 1)) + { + Debug.LogWarning("[KAE Warning] UI_ScaleEdit: not enough incrementSlide values. Using only the first." + Environment.NewLine + StackTraceUtility.ExtractStackTrace()); + float first = FieldInfo.incrementSlide[0]; + FieldInfo.incrementSlide = new float[1]; + FieldInfo.incrementSlide [0] = first; + return true; + } + + for (int i = 0; i < FieldInfo.intervals.Length-2; i++) + { + if (FieldInfo.intervals [i] == FieldInfo.intervals [i + 1]) + { + Debug.LogWarning("[KAE Warning] UI_ScaleEdit: duplicate value in intervals list" + Environment.NewLine + StackTraceUtility.ExtractStackTrace()); + return false; + } + else if (FieldInfo.intervals [i] > FieldInfo.intervals [i + 1]) + { + Debug.LogWarning("[KAE Warning] UI_ScaleEdit: intervals list not sorted" + Environment.NewLine + StackTraceUtility.ExtractStackTrace()); + return false; + } + } + return true; + } + } + + // ReSharper disable once InconsistentNaming + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] + public class UI_ScaleEdit : UI_Control + { + private const string UIControlName = "UIPartActionScaleEdit"; + + public float[] intervals = { 1, 2, 4 }; + public float[] incrementSlide = {0.02f, 0.04f }; + + public float MinValue() + { + return intervals [0]; + } + public float MaxValue() + { + return intervals [intervals.Length-1]; + } + + public override void Load(ConfigNode node, object host) + { + base.Load(node, host); + } + + public override void Save(ConfigNode node, object host) + { + base.Save(node, host); + } + + internal uint GetHashedState() + { + return ((uint)intervals.GetHashCode()) ^ ((uint)incrementSlide.GetHashCode()); + } + } +} diff --git a/Source/KSPAPIExt/Tweakables/UIPartActionsExtended.cs b/Source/KSPAPIExt/Tweakables/UIPartActionsExtended.cs new file mode 100644 index 00000000..c036af4b --- /dev/null +++ b/Source/KSPAPIExt/Tweakables/UIPartActionsExtended.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using UnityEngine; +using KSPAPIExtensions.PartMessage; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + internal class UIPartActionsExtendedRegistration : MonoBehaviour + { + private static bool loaded; + private static bool isLatestVersion; + private bool isRunning; + + public void Start() + { + if (loaded) + { + // prevent multiple copies of same object + Destroy(gameObject); + return; + } + loaded = true; + + DontDestroyOnLoad(gameObject); + + isLatestVersion = SystemUtils.RunTypeElection(typeof(UIPartActionsExtendedRegistration), "KSPAPIExtensions"); + } + + public void OnLevelWasLoaded(int level) + { + if(isRunning) + StopCoroutine("Register"); + if (!HighLogic.LoadedSceneIsEditor && !HighLogic.LoadedSceneIsFlight) + return; + isRunning = true; + StartCoroutine("Register"); + } + + internal IEnumerator Register() + { + UIPartActionController controller; + while((controller = UIPartActionController.Instance) == null) + yield return false; + + FieldInfo typesField = (from fld in controller.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + where fld.FieldType == typeof(List) + select fld).First(); + List fieldPrefabTypes; + while((fieldPrefabTypes = (List)typesField.GetValue(controller)) == null + || fieldPrefabTypes.Count == 0 + || !UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange))) + yield return false; + + Debug.Log("[KAE] Registering field prefabs for version " + Assembly.GetExecutingAssembly().GetName().Version + (isLatestVersion?" (latest)":"")); + + // Register prefabs. This needs to be done for every version of the assembly. (the types might be called the same, but they aren't the same) + controller.fieldPrefabs.Add(UIPartActionFloatEdit.CreateTemplate()); + fieldPrefabTypes.Add(typeof(UI_FloatEdit)); + + controller.fieldPrefabs.Add(UIPartActionScaleEdit.CreateTemplate()); + fieldPrefabTypes.Add(typeof(UI_ScaleEdit)); + + controller.fieldPrefabs.Add(UIPartActionChooseOption.CreateTemplate()); + fieldPrefabTypes.Add(typeof(UI_ChooseOption)); + + // Register the label and resource editor fields. This should only be done by the most recent version. + if (isLatestVersion && GameSceneFilter.AnyEditor.IsLoaded()) + { + int idx = controller.fieldPrefabs.FindIndex(item => item.GetType() == typeof (UIPartActionLabel)); + controller.fieldPrefabs[idx] = UIPartActionLabelImproved.CreateTemplate((UIPartActionLabel)controller.fieldPrefabs[idx]); + controller.resourceItemEditorPrefab = UIPartActionResourceEditorImproved.CreateTemplate(controller.resourceItemEditorPrefab); + } + isRunning = false; + } + } + + + internal class UIPartActionResourceEditorImproved : UIPartActionResourceEditor + { + // ReSharper disable ParameterHidesMember + public override void Setup(UIPartActionWindow window, Part part, UI_Scene scene, UI_Control control, PartResource resource) + { + double amount = resource.amount; + base.Setup(window, part, scene, control, resource); + this.resource.amount = amount; + + slider.SetValueChangedDelegate(OnSliderChanged); + } + // ReSharper restore ParameterHidesMember + + private float oldSliderValue; + + public override void UpdateItem() + { + base.UpdateItem(); + + SIPrefix prefix = (resource.maxAmount).GetSIPrefix(); + // ReSharper disable once InconsistentNaming + Func Formatter = prefix.GetFormatter(resource.maxAmount, sigFigs: 4); + + resourceMax.Text = Formatter(resource.maxAmount) + " " + prefix.PrefixString(); + resourceAmnt.Text = Formatter(resource.amount); + + oldSliderValue = slider.Value = (float)(resource.amount / resource.maxAmount); + } + + private void OnSliderChanged(IUIObject obj) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (oldSliderValue == slider.Value) + return; + oldSliderValue = slider.Value; + + SIPrefix prefix = resource.maxAmount.GetSIPrefix(); + resource.amount = prefix.Round(slider.Value * resource.maxAmount, digits:4); + PartMessageService.Send(this, part, resource, resource.amount); + if (scene == UI_Scene.Editor) + SetSymCounterpartsAmount(resource.amount); + resourceAmnt.Text = resource.amount.ToString("F1"); + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } + + protected new void SetSymCounterpartsAmount(double amount) + { + if (part == null) + return; + + foreach (Part sym in part.symmetryCounterparts) + { + if (sym == part) + continue; + PartResource symResource = sym.Resources[resource.info.name]; + symResource.amount = amount; + PartMessageService.Send(this, sym, symResource, symResource.amount); + } + } + + internal static UIPartActionResourceEditorImproved CreateTemplate(UIPartActionResourceEditor oldEditor) + { + GameObject editGo = (GameObject)Instantiate(oldEditor.gameObject); + Destroy(editGo.GetComponent()); + UIPartActionResourceEditorImproved edit = editGo.AddTaggedComponent(); + editGo.SetActive(false); + edit.transform.parent = oldEditor.transform.parent; + edit.transform.localPosition = oldEditor.transform.localPosition; + + // Find all the bits. + edit.slider = editGo.transform.Find("Slider").GetComponent(); + edit.resourceAmnt = editGo.transform.Find("amnt").GetComponent(); + edit.resourceName = editGo.transform.Find("name").GetComponent(); + edit.resourceMax = editGo.transform.Find("total").GetComponent(); + edit.flowBtn = editGo.transform.Find("StateBtn").GetComponent(); + + return edit; + } + } + + + internal class UIPartActionLabelImproved : UIPartActionLabel + { + private SpriteText label; + + public void Awake() + { + label = gameObject.GetComponentInChildren(); + } + + public override void UpdateItem() + { + object target = isModule ? (object)partModule : part; + + Type fieldType = field.FieldInfo.FieldType; + if (fieldType == typeof(double)) + { + double value = (double)field.FieldInfo.GetValue(target); + label.Text = (string.IsNullOrEmpty(field.guiName) ? field.name : field.guiName) + " " + + (string.IsNullOrEmpty(field.guiFormat) ? value.ToString(CultureInfo.CurrentUICulture) : value.ToStringExt(field.guiFormat)) + + field.guiUnits; + } + if (fieldType == typeof(float)) + { + float value = (float)field.FieldInfo.GetValue(target); + label.Text = (string.IsNullOrEmpty(field.guiName) ? field.name : field.guiName) + " " + + (string.IsNullOrEmpty(field.guiFormat) ? value.ToString(CultureInfo.CurrentUICulture) : value.ToStringExt(field.guiFormat)) + + field.guiUnits; + } + else + label.Text = field.GuiString(target); + } + + internal static UIPartActionLabelImproved CreateTemplate(UIPartActionLabel oldLabel) + { + GameObject labelGo = (GameObject)Instantiate(oldLabel.gameObject); + Destroy(labelGo.GetComponent()); + UIPartActionLabelImproved label = labelGo.AddTaggedComponent(); + labelGo.SetActive(false); + label.transform.parent = oldLabel.transform.parent; + label.transform.localPosition = oldLabel.transform.localPosition; + + return label; + } + } + +} diff --git a/Source/KSPAPIExt/Utils/ConfigNodeUtils.cs b/Source/KSPAPIExt/Utils/ConfigNodeUtils.cs new file mode 100644 index 00000000..af3b6c0a --- /dev/null +++ b/Source/KSPAPIExt/Utils/ConfigNodeUtils.cs @@ -0,0 +1,227 @@ +using System.Linq; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + public static class ConfigNodeUtils + { + #region TryGetValue + + /// + /// Get value from the node. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or default value if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out string value) + { + value = node.GetValue(name); + return value != null; + } + + /// + /// Get the value and parse as a string array. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or null if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out string[] value) + { + value = ParseUtils.ParseArray(node.GetValue(name)); + return value != null; + } + + /// + /// Get the value and parse as a boolean. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or false if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out bool value) + { + string val = node.GetValue(name); + if (string.IsNullOrEmpty(val)) + { + value = false; + return false; + } + return bool.TryParse(val, out value); + } + + /// + /// Get the value and parse as an int. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or 0 if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out int value) + { + string val = node.GetValue(name); + if (string.IsNullOrEmpty(val)) + { + value = 0; + return false; + } + return int.TryParse(val, out value); + } + + /// + /// Get the value and parse as a float. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or 0 if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out float value) + { + string val = node.GetValue(name); + if (string.IsNullOrEmpty(val)) + { + value = 0; + return false; + } + return float.TryParse(val, out value); + } + + /// + /// Get the value and parse as a double. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or 0 if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out double value) + { + string val = node.GetValue(name); + if (string.IsNullOrEmpty(val)) + { + value = 0; + return false; + } + return double.TryParse(val, out value); + } + + /// + /// Get the value and parse as a Vector3. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or default value if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out Vector3 value) + { + string val = node.GetValue(name); + if (string.IsNullOrEmpty(val)) + { + value = default(Vector3); + return false; + } + return ParseUtils.TryParseVector3(val, out value); + } + + /// + /// Get the value and parse as a Vector3d. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or default value if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out Vector3d value) + { + string val = node.GetValue(name); + if (string.IsNullOrEmpty(val)) + { + value = default(Vector3d); + return false; + } + return ParseUtils.TryParseVector3d(val, out value); + } + + /// + /// Get the value and parse as a Color. + /// + /// Node to get value from + /// Name of the value to find + /// The result, or default value if fails + /// True if successful. False if the value is not present or can't be parsed. + public static bool TryGetValue(this ConfigNode node, string name, out Color value) + { + string val = node.GetValue(name); + if (string.IsNullOrEmpty(val)) + { + value = default(Color); + return false; + } + return ParseUtils.TryParseColor(val, out value); + } + #endregion + + #region TryGetNode + /// + /// Get the config node, returns true if successful. + /// + /// Node to get value from + /// Name of the node to find + /// The result, or null if fails + /// True if successful. False if the value is not present. + public static bool TryGetNode(this ConfigNode node, string name, out ConfigNode result) + { + result = node.GetNode(name); + return result != null; + } + + /// + /// Sees if the node has any ConfigNode of the given name, and stores all the found occurences in the ref value. Returns false if there was none. + /// + /// Node to get value from + /// Name of the nodes to find + /// Array to store the results in + /// True if successful. False if the value is not present. + public static bool TryGetNodes(this ConfigNode node, string name, out ConfigNode[] results) + { + results = node.GetNodes(name); + return results.Length != 0; + } + + /// + /// Sees if the node has any value of the given name, and stores all occurences found in the ref value. Returns false if there was none. + /// + /// Node to get value from + /// Name of the values to find + /// Array to store the results in + /// True if successful. False if the value is not present. + public static bool TryGetValues(this ConfigNode node, string name, out string[] results) + { + results = node.GetValues(name); + return results.Length != 0; + } + #endregion + + #region HasValues / HasNodes + /// + /// Checks to see if the ConfigNode has all specified values + /// + /// Node to get values from + /// Values to find + public static bool HasAllValues(this ConfigNode node, params string[] values) + { + return values.All(node.HasValue); + } + + /// + /// Checks to see if the given ConfigNode has all specified nodes. + /// + /// Node to get nodes from + /// Nodes to find + public static bool HasAllNodes(this ConfigNode node, params string[] nodes) + { + return nodes.All(node.HasNode); + } + #endregion + } +} diff --git a/Source/KSPAPIExt/Utils/DebuggingUtils.cs b/Source/KSPAPIExt/Utils/DebuggingUtils.cs new file mode 100644 index 00000000..72fc2e66 --- /dev/null +++ b/Source/KSPAPIExt/Utils/DebuggingUtils.cs @@ -0,0 +1,204 @@ +using System; +using System.Reflection; +using System.Text; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions.DebuggingUtils +{ + [Flags] + public enum DumpTreeOption + { + None = 0x0, + Default = 0x0, + + Active = 0x1, + TransformPosition = 0x2, + TransformRotation = 0x4, + + // Optons that require iterating through components should have the 0x10 bit set. + Components = 0x30, + Materials = 0x50, + Mesh = 0x90, + + Rigidbody = 0x110, + } + + public static class Debugging + { + + public static string DumpTypeHeirachy(Type type) + { + StringBuilder sb = new StringBuilder(); + sb.Append("Declared class: ").Append(type.AssemblyQualifiedName); + while(true) + { + Type [] interfaces = type.GetInterfaces(); + if (interfaces.Length > 0) + { + sb.AppendLine(" Interfaces:"); + for (int i = 0; i < interfaces.Length; i++) + sb.Append(" ").Append(interfaces[i].AssemblyQualifiedName); + } + type = type.BaseType; + if (type == null) + { + sb.Length = sb.Length - 1; + return sb.ToString(); + } + sb.AppendLine(" Base Type: " + type.AssemblyQualifiedName); + } + } + + /// + /// Produce a string that displays the heirachy and other information about a transform. + /// + /// String containing the dump. + public static string DumpTree(this Transform t, DumpTreeOption options = DumpTreeOption.Default) + { + StringBuilder sb = new StringBuilder(); + DumpTree(t, options, 0, sb); + return sb.ToString(); + } + + private static void DumpTree(Transform t, DumpTreeOption options, int level, StringBuilder sb) + { + string space = ""; + for (int i = 0; i < level; ++i) + space += '-'; + sb.AppendLine(space + t.name); + if ((options & DumpTreeOption.Active) == DumpTreeOption.Active) + { + sb.AppendLine(space + "+ activeSelf=" + t.gameObject.activeSelf + " activeInHeirachy=" + t.gameObject.activeInHierarchy); + } + if ((options & DumpTreeOption.TransformPosition) == DumpTreeOption.TransformPosition) + { + sb.AppendLine(space + "+ position: " + t.position + " localPosition: " + t.localPosition); + } + if ((options & DumpTreeOption.TransformRotation) == DumpTreeOption.TransformRotation) + { + sb.AppendLine(space + "+ rotation: " + t.rotation + " localRotation: " + t.localRotation); + } + + if ((((int)options & 0x10) == 0)) + goto skipComponents; + + foreach (Component c in t.gameObject.GetComponents()) + { + + if (!(c is Transform) && ((options & DumpTreeOption.Components) == DumpTreeOption.Components)) + sb.AppendLine(space + "+ component:" + c.GetType()); + + if (c is Renderer && (options & DumpTreeOption.Materials) == DumpTreeOption.Materials) + foreach (Material m in t.renderer.sharedMaterials) + sb.AppendLine(space + "++ mat:" + m.name); + + if (c is MeshFilter && (options & DumpTreeOption.Mesh) == DumpTreeOption.Mesh) + { + MeshFilter filter = (MeshFilter)c; + sb.AppendLine(space + "++ mesh:" + ((filter.sharedMesh == null) ? "*null*" : (filter.sharedMesh.name + " verts:" + filter.sharedMesh.vertexCount))); + } + + if (c is MeshCollider && (options & DumpTreeOption.Mesh) == DumpTreeOption.Mesh) + { + MeshCollider collider = (MeshCollider)c; + sb.AppendLine(space + "++ mesh:" + ((collider.sharedMesh == null) ? "*null*" : (collider.sharedMesh.name + " verts:" + collider.sharedMesh.vertexCount))); + } + + if (c is Rigidbody && (options & DumpTreeOption.Rigidbody) == DumpTreeOption.Rigidbody) + { + Rigidbody body = (Rigidbody)c; + sb.AppendLine(space + "++ Mass:" + body.mass.ToString("F3")); + sb.AppendLine(space + "++ CoM:" + body.centerOfMass.ToString("F3")); + } + if (c is Joint && (options & DumpTreeOption.Rigidbody) == DumpTreeOption.Rigidbody) + { + Joint joint = (Joint)c; + sb.AppendLine(space + "++ anchor:" + joint.anchor.ToString("F3")); + + sb.AppendLine(space + "++ connectedBody: " + (joint.connectedBody != null)); + if (joint.connectedBody != null) + DumpTree(joint.connectedBody.transform, options, level + 1, sb); + } + } + + skipComponents: + for (int i = 0; i < t.childCount; ++i) + DumpTree(t.GetChild(i), options, level + 1, sb); + } + + /// + /// Create a string with the contents of a mesh. + /// + public static string DumpMesh(this Mesh mesh) + { + Vector3[] verticies = mesh.vertices; + Vector3[] normals = mesh.normals; + Vector4[] tangents = mesh.tangents; + Vector2[] uv = mesh.uv; + + StringBuilder sb = new StringBuilder().AppendLine(); + for (int i = 0; i < mesh.vertexCount; ++i) + { + sb + .Append(verticies[i].ToString("F4")).Append(", ") + .Append(uv[i].ToString("F4")).Append(", ") + .Append(normals[i].ToString("F4")).Append(", ") + .Append(tangents[i].ToString("F4")).AppendLine(); + } + sb.Replace("(", "").Replace(")", ""); + sb.AppendLine(); + + for (int i = 0; i < mesh.triangles.Length; i += 3) + { + sb + .Append(mesh.triangles[i]).Append(',') + .Append(mesh.triangles[i + 1]).Append(',') + .Append(mesh.triangles[i + 2]).AppendLine(); + } + + return sb.ToString(); + } + + /// + /// Dump out all the fields from an object + /// + public static string DumpObjectFields(this object obj, BindingFlags flags = BindingFlags.Default) + { + StringBuilder sb = new StringBuilder(); + Type type = obj.GetType(); + + foreach (FieldInfo field in type.GetFields(flags)) + { + object value = field.GetValue(obj); + if (value == null) + sb.AppendLine(field.FieldType.Name + " " + field.Name + "is null"); + else + sb.AppendLine(field.FieldType.Name + " " + field.Name + " = " + value); + } + return sb.ToString(); + } + + public static string DumpNotEqualFields(this T obj, T that, BindingFlags flags = BindingFlags.Default) + { + StringBuilder sb = new StringBuilder(); + if (ReferenceEquals(obj, that)) + sb.AppendLine("Same object"); + + Type type = typeof(T); + + foreach (FieldInfo field in type.GetFields(flags)) + { + object thisValue = field.GetValue(obj); + object thatValue = field.GetValue(that); + + if (field.FieldType.IsPrimitive && Equals(thisValue, thatValue)) + continue; + if (ReferenceEquals(thisValue, thatValue)) + continue; + sb.AppendLine("Different fields: " + field.FieldType.Name + " " + field.Name + (Equals(thisValue, thatValue) ? "(compute equal)" : (" " + thisValue + " != " + thatValue))); + } + return sb.ToString(); + } + } +} diff --git a/Source/KSPAPIExt/Utils/EngineWrapper.cs b/Source/KSPAPIExt/Utils/EngineWrapper.cs new file mode 100644 index 00000000..defea5a3 --- /dev/null +++ b/Source/KSPAPIExt/Utils/EngineWrapper.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections.Generic; + +namespace KSPAPIExtensions.Utils +{ + // ReSharper disable InconsistentNaming + public class EngineWrapper + { + public enum ModuleType + { + MODULEENGINES, + MODULEENGINESFX, + MODULERCS + } + + private readonly ModuleType type; + private readonly ModuleEngines mE; + private readonly ModuleEnginesFX mEFX; + + public EngineWrapper(Part part) + { + if ((mEFX = part.transform.GetComponent()) != null) + type = ModuleType.MODULEENGINESFX; + else if ((mE = part.transform.GetComponent()) != null) + type = ModuleType.MODULEENGINES; + else + throw new ArgumentException("Unable to find engine-like module"); + } + + public EngineWrapper(ModuleEngines mod) + { + mE = mod; + type = ModuleType.MODULEENGINES; + } + + public EngineWrapper(ModuleEnginesFX mod) + { + mEFX = mod; + type = ModuleType.MODULEENGINESFX; + } + + public static implicit operator PartModule(EngineWrapper wrapper) + { + return (PartModule)wrapper.mE ?? wrapper.mEFX; + } + + public static explicit operator ModuleEngines(EngineWrapper wrapper) + { + return wrapper.mE; + } + + public static explicit operator ModuleEnginesFX(EngineWrapper wrapper) + { + return wrapper.mEFX; + } + + public ModuleType Type { get { return type; } } + + // ReSharper disable once InconsistentNaming + public List propellants + { + get + { + switch(type) + { + case ModuleType.MODULEENGINES: + return mE.propellants; + case ModuleType.MODULEENGINESFX: + return mEFX.propellants; + default: + throw new InvalidProgramException(); + } + } + } + public void SetupPropellant() + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.SetupPropellant(); + break; + case ModuleType.MODULEENGINESFX: + mEFX.SetupPropellant(); + break; + default: + throw new InvalidProgramException(); + } + } + public BaseActionList Actions + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.Actions; + case ModuleType.MODULEENGINESFX: + return mEFX.Actions; + default: + throw new InvalidProgramException(); + } + } + } + public bool getIgnitionState + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.getIgnitionState; + case ModuleType.MODULEENGINESFX: + return mEFX.getIgnitionState; + default: + throw new InvalidProgramException(); + } + } + } + public bool EngineIgnited + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.EngineIgnited; + case ModuleType.MODULEENGINESFX: + return mEFX.EngineIgnited; + default: + throw new InvalidProgramException(); + } + } + set + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.EngineIgnited = value; + break; + case ModuleType.MODULEENGINESFX: + mEFX.EngineIgnited = value; + break; + default: + throw new InvalidProgramException(); + } + } + } + public FloatCurve atmosphereCurve + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.atmosphereCurve; + case ModuleType.MODULEENGINESFX: + return mEFX.atmosphereCurve; + default: + throw new InvalidProgramException(); + } + } + set + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.atmosphereCurve = value; + break; + case ModuleType.MODULEENGINESFX: + mEFX.atmosphereCurve = value; + break; + default: + throw new InvalidProgramException(); + } + } + } + public FloatCurve velCurve + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.velCurve; + case ModuleType.MODULEENGINESFX: + return mEFX.velCurve; + default: + throw new InvalidProgramException(); + } + } + set + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.velCurve = value; + break; + case ModuleType.MODULEENGINESFX: + mEFX.velCurve = value; + break; + default: + throw new InvalidProgramException(); + } + } + } + public bool useVelCurve + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.useVelCurve; + case ModuleType.MODULEENGINESFX: + return mEFX.useVelCurve; + default: + throw new InvalidProgramException(); + } + } + set + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.useVelCurve = value; + break; + case ModuleType.MODULEENGINESFX: + mEFX.useVelCurve = value; + break; + default: + throw new InvalidProgramException(); + } + } + } + public float maxThrust + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.maxThrust; + case ModuleType.MODULEENGINESFX: + return mEFX.maxThrust; + default: + throw new InvalidProgramException(); + } + } + set + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.maxThrust = value; + break; + case ModuleType.MODULEENGINESFX: + mEFX.maxThrust = value; + break; + default: + throw new InvalidProgramException(); + } + } + } + public float minThrust + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.minThrust; + case ModuleType.MODULEENGINESFX: + return mEFX.minThrust; + default: + throw new InvalidProgramException(); + } + } + set + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.minThrust = value; + break; + case ModuleType.MODULEENGINESFX: + mEFX.minThrust = value; + break; + default: + throw new InvalidProgramException(); + } + } + } + public float heatProduction + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.heatProduction; + case ModuleType.MODULEENGINESFX: + return mEFX.heatProduction; + default: + throw new InvalidProgramException(); + } + } + set + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.heatProduction = value; + break; + case ModuleType.MODULEENGINESFX: + mEFX.heatProduction = value; + break; + default: + throw new InvalidProgramException(); + } + } + } + + public float g + { + get + { + switch (type) + { + case ModuleType.MODULEENGINES: + return mE.g; + case ModuleType.MODULEENGINESFX: + return mEFX.g; + default: + throw new InvalidProgramException(); + } + } + set + { + switch (type) + { + case ModuleType.MODULEENGINES: + mE.g = value; + break; + case ModuleType.MODULEENGINESFX: + mEFX.g = value; + break; + default: + throw new InvalidProgramException(); + } + } + } + } + // ReSharper restore InconsistentNaming +} diff --git a/Source/KSPAPIExt/Utils/KSPUtils.cs b/Source/KSPAPIExt/Utils/KSPUtils.cs new file mode 100644 index 00000000..8d390de9 --- /dev/null +++ b/Source/KSPAPIExt/Utils/KSPUtils.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using DeftTech.DuckTyping; +using UnityEngine; + + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + /// + /// Flags to determine relationship between two parts. + /// + [Flags] + public enum PartRelationship + { + Vessel = 0x1, + Self = 0x2, + Symmetry = 0x4, + Decendent = 0x8 , + Child = 0x10, + Ancestor = 0x20, + Parent = 0x40, + Sibling = 0x80, + Unrelated = 0x100, + Unknown = 0x0, + + AnyOnVessel = Vessel | Self | Symmetry | Decendent | Child | Ancestor | Parent | Sibling, + AnyPart = AnyOnVessel | Unrelated, + } + + /// + /// Flags to filter particular game scenes. + /// + [Flags] + public enum GameSceneFilter + { + Loading = 1 << GameScenes.LOADING, + MainMenu = 1 << GameScenes.MAINMENU, + SpaceCenter = 1 << GameScenes.SPACECENTER, + VAB = 1 << GameScenes.EDITOR, + SPH = 1 << GameScenes.EDITOR, + Flight = 1 << GameScenes.FLIGHT, + TrackingStation = 1 << GameScenes.TRACKSTATION, + Settings = 1 << GameScenes.SETTINGS, + Credits = 1 << GameScenes.CREDITS, + + AnyEditor = VAB | SPH, + AnyEditorOrFlight = AnyEditor | Flight, + AnyInitializing = 0xFFFF & ~(AnyEditor | Flight), + Any = 0xFFFF + } + + public static class PartUtils + { + private static FieldInfo windowListField; + + /// + /// Find the UIPartActionWindow for a part. Usually this is useful just to mark it as dirty. + /// + public static UIPartActionWindow FindActionWindow(this Part part) + { + if (part == null) + return null; + + // We need to do quite a bit of piss-farting about with reflection to + // dig the thing out. We could just use Object.Find, but that requires hitting a heap more objects. + UIPartActionController controller = UIPartActionController.Instance; + if (controller == null) + return null; + + if (windowListField == null) + { + Type cntrType = typeof(UIPartActionController); + foreach (FieldInfo info in cntrType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)) + { + if (info.FieldType == typeof(List)) + { + windowListField = info; + goto foundField; + } + } + Debug.LogWarning("*PartUtils* Unable to find UIPartActionWindow list"); + return null; + } + foundField: + + List uiPartActionWindows = (List) windowListField.GetValue(controller); + if (uiPartActionWindows == null) + return null; + + return uiPartActionWindows.FirstOrDefault(window => window != null && window.part == part); + } + + /// + /// If this part is a symmetry clone of another part, this method will return the original part. + /// + /// The part to find the original of + /// The original part, or the part itself if it was the original part + public static Part GetSymmetryCloneOriginal(this Part part) + { + if (!part.isClone || part.symmetryCounterparts == null || part.symmetryCounterparts.Count == 0) + return part; + + // Symmetry counterparts always are named xxxx(Clone) if they are cloned from xxxx. So the shortest name is the one. + int nameLength = part.transform.name.Length; + foreach (Part other in part.symmetryCounterparts) + { + if (other.transform.name.Length < nameLength) + return other; + } + return part; + } + + /// + /// Find the relationship between two parts. + /// + public static PartRelationship RelationTo(this Part part, Part other) + { + if (other == null || part == null) + return PartRelationship.Unknown; + + if (other == part) + return PartRelationship.Self; + if (part.localRoot != other.localRoot) + return PartRelationship.Unrelated; + if (part.parent == other) + return PartRelationship.Child; + if (other.parent == part) + return PartRelationship.Parent; + if (other.parent == part.parent) + return PartRelationship.Sibling; + for (Part tmp = part.parent; tmp != null; tmp = tmp.parent) + if (tmp == other) + return PartRelationship.Decendent; + for (Part tmp = other.parent; tmp != null; tmp = tmp.parent) + if (tmp == part) + return PartRelationship.Ancestor; + if(part.localRoot == other.localRoot) + return PartRelationship.Vessel; + return PartRelationship.Unrelated; + } + + /// + /// Test if two parts are related by a set of criteria. Because PartRelationship is a flags + /// enumeration, multiple flags can be tested at the same time. + /// + public static bool RelationTest(this Part part, Part other, PartRelationship relation) + { + if (relation == PartRelationship.Unknown) + return true; + if(part == null || other == null) + return false; + + if (TestFlag(relation, PartRelationship.Self) && part == other) + return true; + if (TestFlag(relation, PartRelationship.Vessel) && part.localRoot == other.localRoot) + return true; + if (TestFlag(relation, PartRelationship.Unrelated) && part.localRoot != other.localRoot) + return true; + if (TestFlag(relation, PartRelationship.Sibling) && part.parent == other.parent) + return true; + if (TestFlag(relation, PartRelationship.Ancestor)) + { + for (Part upto = other.parent; upto != null; upto = upto.parent) + if (upto == part) + return true; + } + else if (TestFlag(relation, PartRelationship.Parent) && part == other.parent) + return true; + + if (TestFlag(relation, PartRelationship.Decendent)) + { + for (Part upto = part.parent; upto != null; upto = upto.parent) + if (upto == other) + return true; + } + else if (TestFlag(relation, PartRelationship.Child) && part.parent == other) + return true; + + if (TestFlag(relation, PartRelationship.Symmetry)) + return other.symmetryCounterparts.Any(sym => part == sym); + return false; + } + + internal static bool TestFlag(this PartRelationship e, PartRelationship flags) + { + return (e & flags) == flags; + } + + /// + /// Convert GameScene enum into GameSceneFilter + /// + public static GameSceneFilter AsFilter(this GameScenes scene) + { + return (GameSceneFilter)(1 << (int)scene); + } + + /// + /// True if the current game scene matches the filter. + /// + public static bool IsLoaded(this GameSceneFilter filter) + { + return (int)(filter & HighLogic.LoadedScene.AsFilter()) != 0; + } + + /// + /// Register an 'OnUpdate' method for use in the editor. + /// This should be done in the OnAwake method of the module, and will ensure that all modules have the + /// registered method called in order of declaration of the module in the part file. + /// + /// Module that is being registered. + /// Method to call + public static void RegisterOnUpdateEditor(this PartModule module, Action action) + { + if (!HighLogic.LoadedSceneIsEditor) + return; + Part part = module.part; + IOnEditorUpdateUtility utility; + foreach (MonoBehaviour c in part.GetComponents()) + { + if (c.GetType().FullName == typeof (OnEditorUpdateUtility).FullName) + { + utility = DuckTyping.Cast(c); + goto found; + } + } + + PartModule onEditorUpdate = (PartModule)part.gameObject.AddComponent(OnEditorUpdateUtility.LatestVersion); + utility = DuckTyping.Cast(onEditorUpdate); + part.Modules.Add(onEditorUpdate); + + found: + utility.AddOnUpdate(module, action); + } + } + + internal interface IOnEditorUpdateUtility + { + void AddOnUpdate(PartModule module, Action action); + } + + internal class OnEditorUpdateUtility : PartModule, IOnEditorUpdateUtility + { + public static readonly Type LatestVersion = SystemUtils.VersionTaggedType(SystemUtils.TypeElectionWinner(typeof (OnEditorUpdateUtility), "KSPAPIExtensions")); + + public override void OnStart(StartState state) + { + started = true; + part.Modules.Remove(this); + } + + private class ModAction : IComparable + { + public PartModule module; + public Action action; + + private int Index + { + get + { + return module.part.Modules.IndexOf(module); + } + } + + public int CompareTo(ModAction other) + { + return Index.CompareTo(other.Index); + } + } + + private readonly List modules = new List(); + private bool started; + + public void AddOnUpdate(PartModule module, Action action) + { + ModAction item = new ModAction + { + module = module, + action = action, + }; + int insert = modules.BinarySearch(item); + if (insert < 0) + modules.Insert(~insert, item); + } + + + public void Update() + { + if (!started) + return; + + for (int i = 0; i < modules.Count; i++) + { + ModAction action = modules[i]; + if (!action.module) + modules.RemoveAt(i--); + else if (action.module.enabled) + action.action(); + } + } + } + + /// + /// KSPAddon with equality checking using an additional type parameter. Fixes the issue where AddonLoader prevents multiple start-once addons with the same start scene. + /// + public class KSPAddonFixed : KSPAddon, IEquatable + { + private readonly Type type; + + public KSPAddonFixed(Startup startup, bool once, Type type) + : base(startup, once) + { + this.type = type; + } + + public override bool Equals(object obj) + { + if (obj.GetType() != GetType()) { return false; } + return Equals((KSPAddonFixed)obj); + } + + public bool Equals(KSPAddonFixed other) + { + if (once != other.once) { return false; } + if (startup != other.startup) { return false; } + if (type != other.type) { return false; } + return true; + } + + public override int GetHashCode() + { + // ReSharper disable NonReadonlyFieldInGetHashCode + return startup.GetHashCode() ^ once.GetHashCode() ^ type.GetHashCode(); + // ReSharper restore NonReadonlyFieldInGetHashCode + } + } +} diff --git a/Source/KSPAPIExt/Utils/MathUtils.cs b/Source/KSPAPIExt/Utils/MathUtils.cs new file mode 100644 index 00000000..3898f7d7 --- /dev/null +++ b/Source/KSPAPIExt/Utils/MathUtils.cs @@ -0,0 +1,393 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + public static class MathUtils + { + + /// + /// Clamp a value between the max and min values. If < min it will be changed to min, if > max it will be changed to max. + /// + /// Value to clamp + /// Minimum + /// Maximum + /// true if the value has been clamped. + public static bool TestClamp(ref float value, float min, float max) + { + if (value < min) + { + value = min; + return true; + } + if (value > max) + { + value = max; + return true; + } + return false; + } + + /// + /// Round value to the nearest. + /// + /// Value to round + /// Precision, eg 0.1, 0.02, 0.005 + /// The rounded value + public static float RoundTo(float value, float precision) + { + return Mathf.Round(value / precision) * precision; + } + + /// + /// Format a numeric value using SI prefexes. + /// + /// eg: 13401 -> 13.4 k + /// + /// + /// value to format + /// unit string + /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. + /// number of signifigant figures to display + /// + public static string ToStringSI(this double value, int sigFigs = 3, int exponent = 0, string unit = null) + { + SIPrefix prefix = value.GetSIPrefix(exponent); + return prefix.FormatSI(value, sigFigs, exponent, unit); + } + + /// + /// Format a numeric value using SI prefexes. + /// + /// eg: 13401 -> 13.4 k + /// + /// + /// value to format + /// unit string + /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. + /// number of signifigant figures to display + /// + public static string ToStringSI(this float value, int sigFigs = 3, int exponent = 0, string unit = null) + { + SIPrefix prefix = value.GetSIPrefix(exponent); + return prefix.FormatSI(value, sigFigs, exponent, unit); + } + + internal static string FormatSI(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0, string unit = null) + { + return string.Format("{0}{1}{2}", pfx.GetFormatter(value, sigFigs, exponent)(value), pfx.PrefixString(), unit); + } + + /// + /// This extension of the standard string format method has available an extra standard format. + /// + /// Examples: + /// + /// 12.ToStringExt("S") -> "12" + /// 12.ToStringExt("S3") -> "12.0" + /// 120.ToStringExt("S3") -> "120" + /// 1254.ToStringExt("S3") -> "1250" (4 digit numbers do not use k as a special case) + /// 12540.ToStringExt("S3") -> "1.25k" (using SI prefixes) + /// 12540.ToStringExt("S4") -> "1.254k" (more significant figures) + /// (1.254).ToStringExt("S4+3") -> "1.254k" (+3 means the 'natural prefix' is k) + /// (1.254).ToStringExt("S4-3") -> "1.254m" (-3 means the 'natural prefix' is m) + /// + /// + public static string ToStringExt(this double value, string format) + { + if (format[0] == 'S' || format[0] == 's') + { + if (format.Length == 1) + return ToStringSI(value, 0); + int pmi = format.IndexOf('+'); + int sigFigs; + if (pmi < 0) + { + pmi = format.IndexOf('-'); + if (pmi < 0) + { + sigFigs = int.Parse(format.Substring(1)); + return ToStringSI(value, sigFigs); + } + } + sigFigs = int.Parse(format.Substring(1, pmi - 1)); + int exponent = int.Parse(format.Substring(pmi)); + return ToStringSI(value, sigFigs, exponent); + } + return value.ToString(format); + } + + /// + /// ToStringExt for floats. See doc for doubles. + /// + public static string ToStringExt(this float value, string format) + { + return ToStringExt((double)value, format); + } + + /// + /// Parse a string in SI format - with SI unit prefix to a double. + /// Note - units must not be present. + /// + public static bool TryParseExt(string str, out double value) + { + str = str.Trim(); + if(str.Length == 0) { + value = 0; + return false; + } + + char last = str[str.Length - 1]; + + if (char.IsDigit(last)) + return double.TryParse(str, out value); + + double exponent; + int newLen = str.Length - 1; + switch (last) + { + case 'k': exponent = 1e3; break; + case 'M': exponent = 1e6; break; + case 'G': exponent = 1e9; break; + case 'T': exponent = 1e12; break; + case 'P': exponent = 1e15; break; + case 'E': exponent = 1e18; break; + case 'Z': exponent = 1e21; break; + case 'Y': exponent = 1e24; break; + case 'm': + exponent = 1e-3; break; + case '\x3bc': + case '\xb5': + case 'u': + exponent = 1e-6; break; + case 'c': + if (!str.EndsWith("mic")) + { + value = 0; + return false; + } + newLen = str.Length - 3; + exponent = 1e-6; + break; + case 'n': exponent = 1e-9; break; + case 'p': exponent = 1e-12; break; + case 'f': exponent = 1e-15; break; + case 'a': exponent = 1e-18; break; + case 'z': exponent = 1e-21; break; + case 'y': exponent = 1e-24; break; + default: + value = 0; + return false; + } + + double parsed; + if (!double.TryParse(str.Substring(0, newLen), out parsed)) + { + value = 0; + return false; + } + + value = parsed * exponent; + return true; + } + + /// + /// Parse a string in SI format - with SI unit prefix to a float. + /// Note - units must not be present. + /// + public static bool TryParseExt(string str, out float value) + { + double dVal; + if (TryParseExt(str, out dVal)) + { + value = (float)dVal; + return true; + } + value = 0; + return false; + } + + /// + /// Round a number to a set number of significant figures. + /// + /// number to round + /// number of significant figures, defaults to 3 + /// + public static float RoundSigFigs(this float d, int sigFigs = 3) + { + + int exponent = (int)Math.Floor(Math.Log10(Math.Abs(d))) - sigFigs; + float div = Mathf.Pow(10, exponent); + return Mathf.Round(d / div) * div; + } + + /// + /// Round a number to a set number of significant figures. + /// + /// number to round + /// number of significant figures, defaults to 3 + /// + public static double RoundSigFigs(this double value, int sigFigs = 3) + { + int exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))) - sigFigs; + double div = Mathf.Pow(10, exponent); + return Math.Round(value / div) * div; + } + + /// + /// Find the SI prefix for a number + /// + /// The value to find the prefix for + /// The natural exponent, if your value was km rather than m, use 3 + public static SIPrefix GetSIPrefix(this double value, int exponent = 0) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (value == 0) + return SIPrefix.None; + + int exp = (int)Math.Floor(Math.Log10(Math.Abs(value))) + exponent; + + if (exp <= 3 && exp >= -1) + return SIPrefix.None; + if (exp < 0) + return (SIPrefix)((exp-2) / 3 * 3); + return (SIPrefix)(exp / 3 * 3); + } + + /// + /// Find the SI prefix for a number + /// + public static SIPrefix GetSIPrefix(this float value, int exponent = 0) + { + return GetSIPrefix((double)value, exponent); + } + + /// + /// The prefix string for a particular exponent. Note Micro will use the string "mic" rather than "µ", so is longer. + /// + public static string PrefixString(this SIPrefix pfx) + { + switch (pfx) + { + case SIPrefix.None: return ""; + case SIPrefix.Kilo: return "k"; + case SIPrefix.Mega: return "M"; + case SIPrefix.Giga: return "G"; + case SIPrefix.Tera: return "T"; + case SIPrefix.Peta: return "P"; + case SIPrefix.Exa: return "E"; + case SIPrefix.Zotta: return "Z"; + case SIPrefix.Yotta: return "Y"; + case SIPrefix.Milli: return "m"; + case SIPrefix.Micro: return "u"; + case SIPrefix.Nano: return "n"; + case SIPrefix.Pico: return "p"; + case SIPrefix.Femto: return "f"; + case SIPrefix.Atto: return "a"; + case SIPrefix.Zepto: return "z"; + case SIPrefix.Yocto: return "y"; + default: throw new ArgumentException("Illegal prefix", "pfx"); + } + } + + /// + /// Round a value with respect the the given SI prefix. + /// + /// eg: + /// SIPrefix.Mega.Round(34.456e6f, digits:1) -> 34.6e6f + /// + /// + /// The SI prefix + /// Value to round + /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) + /// Natural exponent of value, eg: use 3 if value is km rather than m + /// + public static float Round(this SIPrefix pfx, float value, int digits = 3, int exponent = 0) + { + float div = Mathf.Pow(10, (int)pfx - digits + exponent); + return Mathf.Round(value / div) * div; + } + + /// + /// Round a value with respect the the given SI prefix. + /// + /// eg: + /// SIPrefix.Mega.Round(34.456e6, digits:1) -> 34.6e6 + /// + /// + /// The SI prefix + /// Value to round + /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) + /// Natural exponent of value, eg: use 3 if value is km rather than m + /// + public static double Round(this SIPrefix pfx, double value, int digits = 3, int exponent = 0) + { + double div = Math.Pow(10, (int)pfx - digits + exponent); + return Math.Round(value / div) * div; + } + + /// + /// Get a formatter function. This is useful when you have a range of values you'd like formatted the same way. + /// + public static Func GetFormatter(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0) + { + int exp = (int)(Math.Floor(Math.Log10(Math.Abs(value)))) - (int)pfx + exponent; + double div = Math.Pow(10, (int)pfx - exponent); + + if (exp < 0) + return v => (v/div).ToString("F" + (sigFigs-1)); + if (exp >= sigFigs) + { + double mult = Math.Pow(10, exp - sigFigs + 1); + return v => (Math.Round(v / div / mult) * mult).ToString("F0"); + } + return v => (v/div).ToString("F" + (sigFigs - exp - 1)); + } + + + /// + /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 + /// + public static string FormatMass(float mass, int sigFigs = 4, int exponent = 0) + { + return mass < 1.0f ? + mass.ToStringSI(sigFigs, exponent + 6, "g") : + mass.ToStringSI(sigFigs, exponent, "t"); + } + + /// + /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 + /// + public static string FormatMass(double mass, int sigFigs = 4, int exponent = 0) + { + return mass < 1.0f ? + mass.ToStringSI(sigFigs, exponent+6, "g") : + mass.ToStringSI(sigFigs, exponent, "t"); + } + } + + /// + /// SI Prefix. If cast as an int, the base 10 log for the prefix will be produced. + /// + public enum SIPrefix + { + None = 0, + Kilo = 3, + Mega = 6, + Giga = 9, + Tera = 12, + Peta = 15, + Exa = 18, + Zotta = 21, + Yotta = 24, + Milli = -3, + Micro = -6, + Nano = -9, + Pico = -12, + Femto = -15, + Atto = -18, + Zepto = -21, + Yocto = -24 + } +} \ No newline at end of file diff --git a/Source/KSPAPIExt/Utils/ParseUtils.cs b/Source/KSPAPIExt/Utils/ParseUtils.cs new file mode 100644 index 00000000..05b6da00 --- /dev/null +++ b/Source/KSPAPIExt/Utils/ParseUtils.cs @@ -0,0 +1,166 @@ +using System; +using System.Linq; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + /// + /// Utility methods for parsing things + /// + public static class ParseUtils + { + /// + /// Tries to parse a Vector3 from the given string. Returns false if anything goes wrong. + /// + /// String to parse + /// The result, or default value if fails + /// True if successful + public static bool TryParseVector3(string text, out Vector3 result) + { + try + { + result = ParseVector3(text); + return true; + } + catch (ArgumentNullException) { } + catch (FormatException) { } + result = default(Vector3); + return false; + } + + /// + /// Parses a Vector3 from the given string. + /// + /// String to parse + /// The parsed vector + /// If is null + /// If cannot be parsed, or one of the component values would overflow a float + public static Vector3 ParseVector3(string text) + { + if (text == null) + throw new ArgumentNullException("text"); + + string[] splits = text.Split(',').Select(s => s.Trim()).ToArray(); + if (splits.Length != 3) + throw new FormatException("Unable to parse string as Vector3"); + + try + { + return new Vector3(float.Parse(splits[0]), float.Parse(splits[1]), float.Parse(splits[2])); + } + catch (OverflowException ex) + { + throw new FormatException("Unable to parse string as Vector3", ex); + } + } + + // ReSharper disable once InconsistentNaming + /// + /// Tries to parse a Vector3d from the given string. + /// + /// String to parse + /// The result, or default value if fails + /// True if successful + public static bool TryParseVector3d(string text, out Vector3d result) + { + try + { + result = ParseVector3d(text); + return true; + } + catch (ArgumentNullException) { } + catch (FormatException) { } + result = default(Vector3d); + return false; + } + + // ReSharper disable once InconsistentNaming + /// + /// Parses a Vector3d from the given string. + /// + /// String to parse + /// The parsed vector + /// If is null + /// If cannot be parsed, or one of the component values would overflow a double + public static Vector3d ParseVector3d(string text) + { + if (text == null) + throw new ArgumentNullException("text"); + + string[] splits = text.Split(',').Select(s => s.Trim()).ToArray(); + if (splits.Length != 3) + throw new FormatException("Unable to parse string as Vector3"); + + try + { + return new Vector3d(double.Parse(splits[0]), double.Parse(splits[1]), double.Parse(splits[2])); + } + catch (OverflowException ex) + { + throw new FormatException("Unable to parse string as Vector3", ex); + } + } + + /// + /// Tries to parse a Color from the given string. + /// + /// String to parse + /// The result, or default value if fails + /// True if successful + public static bool TryParseColor(string text, out Color result) + { + try + { + result = ParseColor(text); + return true; + } + catch (ArgumentNullException) { } + catch (FormatException) { } + result = default(Color); + return false; + } + + /// + /// Parses a Color from the given string. + /// + /// String to parse + /// The parsed Color + /// If is null + /// If cannot be parsed, or one of the component values would overflow a float + public static Color ParseColor(string text) + { + if (text == null) + throw new ArgumentNullException("text"); + string[] splits = text.Split(',').Select(s => s.Trim()).ToArray(); + try + { + switch (splits.Length) + { + case 3: + return new Color(float.Parse(splits[0]), float.Parse(splits[1]), float.Parse(splits[2])); + case 4: + return new Color(float.Parse(splits[0]), float.Parse(splits[1]), float.Parse(splits[2]), float.Parse(splits[3])); + default: + throw new FormatException("Unable to parse string as Color"); + } + } + catch (OverflowException ex) + { + throw new FormatException("Unable to parse string as Color", ex); + } + } + + /// + /// Parses the given string as an array where each element is separated by a comma. + /// + /// String to parse + /// The array, or null if is null + public static string[] ParseArray(string text) + { + if (text == null) + return null; + return text.Split(',').Select(s => s.Trim()).ToArray(); + } + } +} diff --git a/Source/KSPAPIExt/Utils/PhysicsDebugger.cs b/Source/KSPAPIExt/Utils/PhysicsDebugger.cs new file mode 100644 index 00000000..b491176f --- /dev/null +++ b/Source/KSPAPIExt/Utils/PhysicsDebugger.cs @@ -0,0 +1,67 @@ +using System.Text; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions.DebuggingUtils +{ + + /// + /// Part module that dumps out the physics constants pertaining to the part every 10 seconds. + /// + public class ModulePhysicsDebugger : PartModule + { + + [KSPField(guiActive = true, guiActiveEditor = true, guiName = "Physics Debugger")] + private double lastFixedUpdate; + + public void FixedUpdate() + { + if (HighLogic.LoadedSceneIsFlight && (Time.time - lastFixedUpdate) > 10) + { + lastFixedUpdate = Time.time; + + Transform rootT = part.vessel.rootPart.transform; + + StringBuilder sb = new StringBuilder(); + + float massTotal = 0; + Vector3 wtSum = Vector3.zero; + + foreach (Part p in part.vessel.parts) + { + sb.AppendLine(p.name + " position(wrt root)=" + rootT.InverseTransformPoint(p.transform.position).ToString("F5")); + + if (p.rigidbody != null) + { + massTotal += p.rigidbody.mass; + wtSum += p.rigidbody.mass * rootT.InverseTransformPoint(p.transform.TransformPoint(p.rigidbody.centerOfMass)); + sb.AppendLine(p.name + " mass=" + p.rigidbody.mass); + if (p.rigidbody.centerOfMass != Vector3.zero) + sb.AppendLine(p.name + " CoM offset=" + p.rigidbody.centerOfMass.ToString("F5")); + sb.AppendLine(p.name + " inertia tensor=" + p.rigidbody.inertiaTensor.ToString("F5") + " rotation=" + p.rigidbody.inertiaTensorRotation.ToStringAngleAxis("F5")); + + foreach(Joint j in p.gameObject.GetComponents()) + { + sb.AppendLine(p.name + " joint type=" + j.GetType() + " position=" + j.anchor.ToString("F5") + " force=" + j.breakForce + " torque=" + j.breakTorque); + } + } + + if (p.Modules.Contains("ModuleEngines")) + { + ModuleEngines mE = (ModuleEngines)p.Modules["ModuleEngines"]; + foreach (Transform t in mE.thrustTransforms) + { + sb.AppendLine(p.name + " thrust transform position=" + t.position.ToString("F5")); + } + } + } + if (massTotal > 0) + { + sb.AppendLine("CoM = " + (wtSum / massTotal).ToString("F5")); + } + Debug.Log(sb); + } + } + + } +} \ No newline at end of file diff --git a/Source/KSPAPIExt/Utils/SystemUtils.cs b/Source/KSPAPIExt/Utils/SystemUtils.cs new file mode 100644 index 00000000..abfc5b7b --- /dev/null +++ b/Source/KSPAPIExt/Utils/SystemUtils.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + public static class SystemUtils + { + + /// + /// Ensure the latest version of a particular type is used. This is useful when multiple versions of the same assembly are potentially loaded. + /// + /// This will return true for the latest version of the class (specified with ) + /// that appears the earliest in the list of loaded assemblies (as loaded from ). + /// + /// Target class to search for. Searched by the attribute. + /// Assembly name of the expected assembly. + /// If the class is not in an assembly named . + /// True if this class wins the election, false otherwise. + public static bool RunTypeElection(Type targetCls, String assemName) + { + if (targetCls.Assembly.GetName().Name != assemName) + throw new InvalidProgramException("Assembly: " + targetCls.Assembly.GetName().Name + " at location: " + targetCls.Assembly.Location + " is not in the expected assembly. Code has been copied and this will cause problems."); + + // If we are loaded from the first loaded assembly that has this class, then we are responsible to destroy + var candidates = (from ass in AssemblyLoader.loadedAssemblies + where ass.assembly.GetName().Name == assemName && + ass.assembly.GetType(targetCls.FullName, false) != null + orderby ass.assembly.GetName().Version descending, ass.path ascending + select ass).ToArray(); + var winner = candidates.First(); + + if (!ReferenceEquals(targetCls.Assembly, winner.assembly)) + return false; + + if (candidates.Length > 1) + { + string losers = string.Join("\n", (from t in candidates + where t != winner + select string.Format("Version: {0} Location: {1}", t.assembly.GetName().Version, t.path)).ToArray()); + + Debug.Log("[" + targetCls.Name + "] version " + winner.assembly.GetName().Version + " at " + winner.path + " won the election against\n" + losers); + } + else + Debug.Log("[" + targetCls.Name + "] Elected unopposed version= " + winner.assembly.GetName().Version + " at " + winner.path); + + return true; + } + + /// + /// Ensure the latest version of a particular type is used. This is useful when multiple versions of the same assembly are potentially loaded. + /// + /// This will the latest version of the class (specified with ) + /// that appears the earliest in the list of loaded assemblies (as loaded from ). + /// + /// Target class to search for. Searched by the attribute. + /// Assembly name of the expected assembly. + /// If the class is not in an assembly named . + /// True if this class wins the election, false otherwise. + public static Type TypeElectionWinner(Type targetCls, String assemName) + { + if (targetCls.Assembly.GetName().Name != assemName) + throw new InvalidProgramException("Assembly: " + targetCls.Assembly.GetName().Name + " at location: " + + targetCls.Assembly.Location + + " is not in the expected assembly. Code has been copied and this will cause problems."); + + // If we are loaded from the first loaded assembly that has this class, then we are responsible to destroy + var candidates = (from ass in AssemblyLoader.loadedAssemblies + where ass.assembly.GetName().Name == assemName + let t = ass.assembly.GetType(targetCls.FullName, false) + where t != null + orderby ass.assembly.GetName().Version descending, ass.path ascending + select t).ToArray(); + return candidates.First(); + } + + /// + /// Find a version-tagged class for an untagged class. + /// + /// The tagged class must be directly derived from the untagged class and in the same assembly and namespace. + /// + /// The untagged class for which the tagged class will be searched. The attribute. + /// The tagged class if found, otherwise the base class + public static Type VersionTaggedType(Type baseClass) + { + var ass = baseClass.Assembly; + Type tagged = ass.GetTypes().Where(t => t.BaseType == baseClass).Where(t => t.FullName.StartsWith(baseClass.FullName)).FirstOrDefault(); + if (tagged != null) { + Debug.Log(String.Format("[VersionTaggedType] found {0} for {1}", tagged.FullName, baseClass.FullName)); + return tagged; + } + return baseClass; + } + + public static LinkedListNode FindFirstNode(this LinkedList list, Predicate match) + { + for (var node = list.First; node != null; node = node.Next) + if(match(node.Value)) + return node; + return null; + } + + public static int RemoveAll(this LinkedList list, Predicate match) + { + int count = 0; + for (var node = list.First; node != null; ) + { + if (!match(node.Value)) + { + node = node.Next; + continue; + } + ++count; + var tmp = node; + node = node.Next; + list.Remove(tmp); + } + return count; + } + + // ReSharper disable once InconsistentNaming + public static bool TryGet(this KeyedCollection coll, K key, out T value) + { + try + { + value = coll[key]; + return true; + } + catch (KeyNotFoundException) + { + value = default(T); + return false; + } + } + + /// + /// Retrieve the most informative version string available in the given assembly. + /// + /// The assembly from which to retrieve the version string + /// The version string from AssemblyInformationalVersionAttribute if available, otherwise from AssemblyVersion. + public static string GetAssemblyVersionString (Assembly assembly) + { + string version = assembly.GetName().Version.ToString (); + + var cattrs = assembly.GetCustomAttributes(true); + foreach (var attr in cattrs) { + if (attr is AssemblyInformationalVersionAttribute) { + var ver = attr as AssemblyInformationalVersionAttribute; + version = ver.InformationalVersion; + break; + } + } + + return version; + } + + /// + /// Retrieve the best available title string available in the given assembly. + /// + /// The assembly from which to retrieve the title string + /// The title string from AssemblyTitle if available, otherwise the name of the dll. + public static string GetAssemblyTitle (Assembly assembly) + { + string title = assembly.GetName().Name; + + var cattrs = assembly.GetCustomAttributes(true); + foreach (var attr in cattrs) { + if (attr is AssemblyTitleAttribute) { + var ver = attr as AssemblyTitleAttribute; + title = ver.Title; + break; + } + } + + return title; + } + } + + // ReSharper disable InconsistentNaming + public class OrderedDictionary : KeyedCollection + { + private readonly Func GetKey; + + public OrderedDictionary(Func GetKey) + { + this.GetKey = GetKey; + } + + public OrderedDictionary(Func GetKey, IEqualityComparer comparer) + : base(comparer) + { + this.GetKey = GetKey; + } + + public OrderedDictionary(Func GetKey, IEqualityComparer comparer, int dictionaryCreationThreshold) + : base(comparer, dictionaryCreationThreshold) + { + this.GetKey = GetKey; + } + + protected override K GetKeyForItem(T item) + { + return GetKey(item); + } + + public bool TryGet(K key, out T value) + { + try + { + value = this[key]; + return true; + } + catch (KeyNotFoundException) + { + value = default(T); + return false; + } + } + } + // ReSharper restore InconsistentNaming +} diff --git a/Source/KSPAPIExt/Utils/UnityUtils.cs b/Source/KSPAPIExt/Utils/UnityUtils.cs new file mode 100644 index 00000000..b4aad2da --- /dev/null +++ b/Source/KSPAPIExt/Utils/UnityUtils.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + + public static class UnityUtils + { + /// + /// Formats a quarternion as an axis and rotation angle about the axis. Useful for debugging quarternions. + /// + /// The quarternion + /// Number format for the constituent values + /// The formatted string + public static string ToStringAngleAxis(this Quaternion q, string format = "F3") + { + Vector3 axis; + float angle; + q.ToAngleAxis(out angle, out axis); + return "(axis:" + axis.ToString(format) + " angle: " + angle.ToString(format) + ")"; + } + + /// + /// Find decendant transform / game object with the specified name. + /// + /// Parent transform + /// Name to search for + /// If true, inactive transforms are ignored + /// + public static Transform FindDecendant(this Transform t, string name, bool activeOnly = false) + { + bool found; + return FindDecendant(t, name, activeOnly, out found); + } + + private static Transform FindDecendant(Transform t, string name, bool activeOnly, out bool found) + { + if (t.name == name && (!activeOnly || t.gameObject.activeSelf)) + { + found = true; + return t; + } + found = false; + if (activeOnly && !t.gameObject.activeInHierarchy) + return null; + + for (int i = 0; i < t.childCount; ++i) + { + Transform ret = FindDecendant(t.GetChild(i), name, activeOnly, out found); + if (found) + return ret; + } + + return null; + } + + /// + /// Returns the path between the parent and the child, with names of decendents in sequence separated + /// by '/'. The returned string could be used with parent.Find to get the child. + /// + /// Parent transform + /// Child transform + /// The path between + /// If the parent is not an ancestor of the child. + public static string PathToDecendant(this Transform parent, Transform child) + { + List inBetween = new List(); + for (Transform track = child; track != parent; track = track.parent) + { + inBetween.Add(track.name); + if (track.parent == null) + throw new ArgumentException("Parent transform is not ancestor of child", "parent"); + } + inBetween.Reverse(); + return string.Join("/", inBetween.ToArray()); + } + } + + + /// + /// Holds all the bits and pieces for a mesh without the checking code. + /// Useful when in the process of creating dynamic meshes. + /// + public class UncheckedMesh + { + public readonly int nVrt; + public readonly int nTri; + + public readonly Vector3[] verticies; + public readonly Vector3[] normals; + public readonly Vector4[] tangents; + public readonly Vector2[] uv; + public readonly int[] triangles; + + /// + /// Create a new unchecked mesh + /// + /// Number of vertexes + /// Number of triangles + public UncheckedMesh(int nVrt, int nTri) + { + this.nVrt = nVrt; + this.nTri = nTri; + + verticies = new Vector3[nVrt]; + normals = new Vector3[nVrt]; + tangents = new Vector4[nVrt]; + uv = new Vector2[nVrt]; + + triangles = new int[nTri * 3]; + } + + /// + /// Write the mesh data to the given mesh. + /// + /// Mesh to write into + /// Name for the mesh + public void WriteTo(Mesh mesh, string name = null) + { + mesh.Clear(); + if (name != null) + mesh.name = name; + mesh.vertices = verticies; + mesh.normals = normals; + mesh.tangents = tangents; + mesh.uv = uv; + mesh.triangles = triangles; + } + + /// + /// Create a new mesh object with the mesh data as current. + /// + /// Name of the mesh + /// The new mesh + public Mesh AsMesh(string name = null) + { + Mesh mesh = new Mesh(); + WriteTo(mesh, name); + return mesh; + } + + /// + /// Dump the mesh as a string, useful for debugging. + /// + /// Mesh as string + public string DumpMesh() + { + StringBuilder sb = new StringBuilder().AppendLine(); + for (int i = 0; i < verticies.Length; ++i) + { + sb + .Append(verticies[i].ToString("F4")).Append(", ") + .Append(uv[i].ToString("F4")).Append(", ") + .Append(normals[i].ToString("F4")).Append(", ") + .Append(tangents[i].ToString("F4")).AppendLine(); + } + sb.Replace("(", "").Replace(")", ""); + sb.AppendLine(); + + for (int i = 0; i < triangles.Length; i += 3) + { + sb + .Append(triangles[i]).Append(',') + .Append(triangles[i + 1]).Append(',') + .Append(triangles[i + 2]).AppendLine(); + } + + return sb.ToString(); + } + } + + /// + /// Be aware this will not prevent a non singleton constructor + /// such as `T myT = new T();` + /// To prevent that, add `protected T () {}` to your singleton class. + /// + /// As a note, this is made as MonoBehaviour because we need Coroutines. + /// + public abstract class Singleton : MonoBehaviour where T : MonoBehaviour + { + private static T _instance; + + // ReSharper disable once StaticFieldInGenericType + private static readonly object Lock = new object(); + + public static T Instance + { + get + { + if (applicationIsQuitting) + { + Debug.LogWarning("[Singleton] Instance '" + typeof(T) + + "' already destroyed on application quit." + + " Won'type create again - returning null."); + return null; + } + + lock (Lock) + { + if (_instance == null) + { + _instance = (T)FindObjectOfType(typeof(T)); + + if (FindObjectsOfType(typeof(T)).Length > 1) + { + Debug.LogError("[Singleton] Something went really wrong " + + " - there should never be more than 1 singleton!" + + " Reopenning the scene might fix it."); + return _instance; + } + + if (_instance == null) + { + GameObject singleton = new GameObject(); + _instance = singleton.AddComponent(); + singleton.name = "(singleton) " + typeof(T); + + DontDestroyOnLoad(singleton); + + Debug.Log("[Singleton] An instance of " + typeof(T) + + " is needed in the scene, so '" + singleton + + "' was created with DontDestroyOnLoad."); + } + else + { + Debug.Log("[Singleton] Using instance already created: " + + _instance.gameObject.name); + } + } + + return _instance; + } + } + } + + // ReSharper disable once StaticFieldInGenericType + private static bool applicationIsQuitting; + + /// + /// When Unity quits, it destroys objects in a random order. + /// In principle, a Singleton is only destroyed when application quits. + /// If any script calls Instance after it have been destroyed, + /// it will create a buggy ghost object that will stay on the Editor scene + /// even after stopping playing the Application. Really bad! + /// So, this was made to be sure we're not creating that buggy ghost object. + /// + public void OnDestroy() + { + applicationIsQuitting = true; + } + } + +} \ No newline at end of file diff --git a/Source/KSPAPIExt/tagged_classes.cs b/Source/KSPAPIExt/tagged_classes.cs new file mode 100644 index 00000000..04749715 --- /dev/null +++ b/Source/KSPAPIExt/tagged_classes.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; +namespace KSPAPIEL { + [KSPAddon(KSPAddon.Startup.Instantly, true)] + internal class UIPartActionsExtendedRegistration_1_7_4_0 : UIPartActionsExtendedRegistration { } + internal class UIPartActionResourceEditorImproved_1_7_4_0 : UIPartActionResourceEditorImproved { } + internal class UIPartActionLabelImproved_1_7_4_0 : UIPartActionLabelImproved { } + internal class UIPartActionFloatEdit_1_7_4_0 : UIPartActionFloatEdit { } + internal class UIPartActionChooseOption_1_7_4_0 : UIPartActionChooseOption { } + [KSPAddon(KSPAddon.Startup.Instantly, true)] + internal class CompatibilityChecker_1_7_4_0 : CompatibilityChecker { } +} diff --git a/Source/KSPAPIExt/zzVersionChecker.cs b/Source/KSPAPIExt/zzVersionChecker.cs new file mode 100644 index 00000000..689766e1 --- /dev/null +++ b/Source/KSPAPIExt/zzVersionChecker.cs @@ -0,0 +1,224 @@ + +/** + * Copyright (c) 2014, Majiir + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +/*-----------------------------------------*\ +| SUBSTITUTE YOUR MOD'S NAMESPACE HERE. | +\*-----------------------------------------*/ +namespace KSPAPIExtensions +{ + + /** + * This utility displays a warning with a list of mods that determine themselves + * to be incompatible with the current running version of Kerbal Space Program. + * + * See this forum thread for details: + * http://forum.kerbalspaceprogram.com/threads/65395-Voluntarily-Locking-Plugins-to-a-Particular-KSP-Version + */ + + internal class CompatibilityChecker : MonoBehaviour + { + public static bool IsCompatible() + { + /*-----------------------------------------------*\ + | BEGIN IMPLEMENTATION-SPECIFIC EDITS HERE. | + \*-----------------------------------------------*/ + + // TODO: Implement your own compatibility check. + // + // If you want to disable some behavior when incompatible, other parts of the plugin + // should query this method: + // + // if (!CompatibilityChecker.IsCompatible()) { + // ...disable some features... + // } + // + // Even if you don't lock down functionality, you should return true if your users + // can expect a future update to be available. + // + return Versioning.version_major == 1 && Versioning.version_minor == 0 && Versioning.Revision == 2; + + /*-----------------------------------------------*\ + | IMPLEMENTERS SHOULD NOT EDIT BEYOND THIS POINT! | + \*-----------------------------------------------*/ + } + + public static bool IsUnityCompatible() + { + /*-----------------------------------------------*\ + | BEGIN IMPLEMENTATION-SPECIFIC EDITS HERE. | + \*-----------------------------------------------*/ + + // TODO: Implement your own Unity compatibility check. + // + // DRE is not going to care about the fact that KSP .25 OSX uses a different Unity... + return true; + + /*-----------------------------------------------*\ + | IMPLEMENTERS SHOULD NOT EDIT BEYOND THIS POINT! | + \*-----------------------------------------------*/ + } + + // Version of the compatibility checker itself. + private static int _version = 4; + + public void Start() + { + // Checkers are identified by the type name and version field name. + FieldInfo[] fields = + getAllTypes() + .Where(t => t.Name == "CompatibilityChecker") + .Select(t => t.GetField("_version", BindingFlags.Static | BindingFlags.NonPublic)) + .Where(f => f != null) + .Where(f => f.FieldType == typeof(int)) + .ToArray(); + + // Let the latest version of the checker execute. + if (_version != fields.Max(f => (int)f.GetValue(null))) { return; } + + Debug.Log(String.Format("[CompatibilityChecker] Running checker version {0} from '{1}'", _version, Assembly.GetExecutingAssembly().GetName().Name)); + + // Other checkers will see this version and not run. + // This accomplishes the same as an explicit "ran" flag with fewer moving parts. + _version = int.MaxValue; + + // A mod is incompatible if its compatibility checker has an IsCompatible method which returns false. + String[] incompatible = + fields + .Select(f => f.DeclaringType.GetMethod("IsCompatible", Type.EmptyTypes)) + .Where(m => m.IsStatic) + .Where(m => m.ReturnType == typeof(bool)) + .Where(m => + { + try + { + return !(bool)m.Invoke(null, new object[0]); + } + catch (Exception e) + { + // If a mod throws an exception from IsCompatible, it's not compatible. + Debug.LogWarning(String.Format("[CompatibilityChecker] Exception while invoking IsCompatible() from '{0}':\n\n{1}", m.DeclaringType.Assembly.GetName().Name, e)); + return true; + } + }) + .Select(m => m.DeclaringType.Assembly.GetName().Name) + .ToArray(); + + // A mod is incompatible with Unity if its compatibility checker has an IsUnityCompatible method which returns false. + String[] incompatibleUnity = + fields + .Select(f => f.DeclaringType.GetMethod("IsUnityCompatible", Type.EmptyTypes)) + .Where(m => m != null) // Mods without IsUnityCompatible() are assumed to be compatible. + .Where(m => m.IsStatic) + .Where(m => m.ReturnType == typeof(bool)) + .Where(m => + { + try + { + return !(bool)m.Invoke(null, new object[0]); + } + catch (Exception e) + { + // If a mod throws an exception from IsUnityCompatible, it's not compatible. + Debug.LogWarning(String.Format("[CompatibilityChecker] Exception while invoking IsUnityCompatible() from '{0}':\n\n{1}", m.DeclaringType.Assembly.GetName().Name, e)); + return true; + } + }) + .Select(m => m.DeclaringType.Assembly.GetName().Name) + .ToArray(); + + Array.Sort(incompatible); + Array.Sort(incompatibleUnity); + + String message = String.Empty; + + if (IsWin64()) + { + message += "WARNING: You are using 64-bit KSP on Windows. This version of KSP is known to cause crashes. It's highly recommended that you use either 32-bit KSP on Windows or switch to Linux."; + } + + if ((incompatible.Length > 0) || (incompatibleUnity.Length > 0)) + { + message += ((message == String.Empty) ? "Some" : "\n\nAdditionally, some") + " installed mods may be incompatible with this version of Kerbal Space Program. Features may be broken or disabled. Please check for updates to the listed mods."; + + if (incompatible.Length > 0) + { + Debug.LogWarning("[CompatibilityChecker] Incompatible mods detected: " + String.Join(", ", incompatible)); + message += String.Format("\n\nThese mods are incompatible with KSP {0}.{1}.{2}:\n\n", Versioning.version_major, Versioning.version_minor, Versioning.Revision); + message += String.Join("\n", incompatible); + } + + if (incompatibleUnity.Length > 0) + { + Debug.LogWarning("[CompatibilityChecker] Incompatible mods (Unity) detected: " + String.Join(", ", incompatibleUnity)); + message += String.Format("\n\nThese mods are incompatible with Unity {0}:\n\n", Application.unityVersion); + message += String.Join("\n", incompatibleUnity); + } + } + + if ((incompatible.Length > 0) || (incompatibleUnity.Length > 0) || IsWin64()) + { + PopupDialog.SpawnPopupDialog("Incompatible Mods Detected", message, "OK", true, HighLogic.Skin); + } + } + + public static bool IsWin64() + { + return (IntPtr.Size == 8) && (Environment.OSVersion.Platform == PlatformID.Win32NT); + } + + public static bool IsAllCompatible() + { + return IsCompatible() && IsUnityCompatible() && !IsWin64(); + } + + private static IEnumerable getAllTypes() + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + Type[] types; + try + { + types = assembly.GetTypes(); + } + catch (Exception) + { + types = Type.EmptyTypes; + } + + foreach (var type in types) + { + yield return type; + } + } + } + } +} From 0d86185c2fd5013fe5d53d5a0d03f579d756c211 Mon Sep 17 00:00:00 2001 From: toadicus Date: Wed, 20 May 2015 11:12:34 -0700 Subject: [PATCH 11/18] Split into KSPAPIExtensions and KSPAPIExtras --- GameData/KSPAPIEL.dll | Bin 44544 -> 0 bytes Source/KSPAPIEL/CommonAPI/GameObjectExt.cs | 22 - .../Tweakables/UIPartActionChooseOption.cs | 217 ---------- Source/KSPAPIEL/Utils/MathUtils.cs | 229 ---------- Source/KSPAPIEL/Utils/SystemUtils.cs | 225 ---------- Source/KSPAPIEL/tagged_classes.cs | 15 - .../Tweakables/UIPartActionFloatEdit.cs | 396 ------------------ .../Tweakables/UIPartActionScaleEdit.cs | 332 --------------- .../Tweakables/UIPartActionsExtended.cs | 209 --------- Source/KSPAPIExt/Utils/MathUtils.cs | 393 ----------------- Source/KSPAPIExt/tagged_classes.cs | 15 - Source/{KSPAPIEL.sln => KSPAPIExtensions.sln} | 8 +- .../CommonAPI/GameObjectExt.cs | 6 +- .../KSPAPIExtensions.csproj} | 22 +- .../Properties/AssemblyInfo.cs | 2 +- .../Properties/AssemblyInfo.in | 0 .../Tweakables/UIPartActionChooseOption.cs | 0 .../Tweakables/UIPartActionFloatEdit.cs | 2 +- .../Tweakables/UIPartActionScaleEdit.cs | 2 +- .../Tweakables/UIPartActionsExtended.cs | 4 +- .../Utils/GameScenesUtils.cs} | 4 +- Source/KSPAPIExtensions/Utils/MathUtils.cs | 393 +++++++++++++++++ .../Utils/SystemUtils.cs | 0 .../KSPAPIExtensions/version-gen-lite | 8 +- .../zzVersionChecker.cs | 0 .../CommonAPI/ResourceExtensions.cs | 0 .../DuckTyping/AmbiguousTypesException.cs | 0 .../DuckTyping/DelegateDuckCaster.cs | 0 .../DuckTyping/DelegateDuckProxyType.cs | 0 .../DuckTyping/DuckProxyType.cs | 0 .../DuckTyping/DuckTyping.cs | 0 .../DuckTyping/EnumStringDuckCaster.cs | 0 .../DuckTyping/FromTypeTree.cs | 0 .../DuckTyping/GenericParameterDuckCaster.cs | 0 .../DuckTyping/IDuckCaster.cs | 0 .../DuckTyping/IDuckProxy.cs | 0 .../DuckTyping/IStaticDuckCaster.cs | 0 .../DuckTyping/InterfaceDuckCaster.cs | 0 .../DuckTyping/InterfaceDuckProxyType.cs | 0 .../DuckTyping/InterfaceStaticDuckCaster.cs | 0 .../DuckTyping/ToTypeFromTypeTree.cs | 0 .../DuckTyping/TypeKey.cs | 0 .../KSPAPIExtras.csproj} | 60 +-- .../PartMessage/Attributes.cs | 3 +- .../PartMessage/CommonMessages.cs | 0 .../PartMessage/PartMessageService.cs | 0 .../PartMessage/zzImplementation.cs | 5 +- .../Properties/AssemblyInfo.cs | 2 +- .../KSPAPIExtras/Properties/AssemblyInfo.in | 35 ++ .../Utils/ConfigNodeUtils.cs | 0 .../Utils/DebuggingUtils.cs | 0 .../Utils/EngineWrapper.cs | 0 .../Utils/KSPUtils.cs | 41 +- .../Utils/ParseUtils.cs | 0 .../Utils/PhysicsDebugger.cs | 0 .../Utils/UnityUtils.cs | 0 Source/KSPAPIExtras/version-gen-ext | 60 +++ .../zzVersionChecker.cs | 2 +- 58 files changed, 562 insertions(+), 2150 deletions(-) delete mode 100755 GameData/KSPAPIEL.dll delete mode 100644 Source/KSPAPIEL/CommonAPI/GameObjectExt.cs delete mode 100644 Source/KSPAPIEL/Tweakables/UIPartActionChooseOption.cs delete mode 100644 Source/KSPAPIEL/Utils/MathUtils.cs delete mode 100644 Source/KSPAPIEL/Utils/SystemUtils.cs delete mode 100644 Source/KSPAPIEL/tagged_classes.cs delete mode 100644 Source/KSPAPIExt/Tweakables/UIPartActionFloatEdit.cs delete mode 100644 Source/KSPAPIExt/Tweakables/UIPartActionScaleEdit.cs delete mode 100644 Source/KSPAPIExt/Tweakables/UIPartActionsExtended.cs delete mode 100644 Source/KSPAPIExt/Utils/MathUtils.cs delete mode 100644 Source/KSPAPIExt/tagged_classes.cs rename Source/{KSPAPIEL.sln => KSPAPIExtensions.sln} (56%) rename Source/{KSPAPIExt => KSPAPIExtensions}/CommonAPI/GameObjectExt.cs (63%) rename Source/{KSPAPIEL/KSPAPIEL.csproj => KSPAPIExtensions/KSPAPIExtensions.csproj} (87%) rename Source/{KSPAPIEL => KSPAPIExtensions}/Properties/AssemblyInfo.cs (97%) rename Source/{KSPAPIEL => KSPAPIExtensions}/Properties/AssemblyInfo.in (100%) rename Source/{KSPAPIExt => KSPAPIExtensions}/Tweakables/UIPartActionChooseOption.cs (100%) rename Source/{KSPAPIEL => KSPAPIExtensions}/Tweakables/UIPartActionFloatEdit.cs (99%) rename Source/{KSPAPIEL => KSPAPIExtensions}/Tweakables/UIPartActionScaleEdit.cs (99%) rename Source/{KSPAPIEL => KSPAPIExtensions}/Tweakables/UIPartActionsExtended.cs (99%) rename Source/{KSPAPIEL/Utils/KSPUtils.cs => KSPAPIExtensions/Utils/GameScenesUtils.cs} (95%) create mode 100644 Source/KSPAPIExtensions/Utils/MathUtils.cs rename Source/{KSPAPIExt => KSPAPIExtensions}/Utils/SystemUtils.cs (100%) rename version-gen => Source/KSPAPIExtensions/version-gen-lite (86%) mode change 100755 => 100644 rename Source/{KSPAPIExt => KSPAPIExtensions}/zzVersionChecker.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/CommonAPI/ResourceExtensions.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/AmbiguousTypesException.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/DelegateDuckCaster.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/DelegateDuckProxyType.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/DuckProxyType.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/DuckTyping.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/EnumStringDuckCaster.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/FromTypeTree.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/GenericParameterDuckCaster.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/IDuckCaster.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/IDuckProxy.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/IStaticDuckCaster.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/InterfaceDuckCaster.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/InterfaceDuckProxyType.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/InterfaceStaticDuckCaster.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/ToTypeFromTypeTree.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/DuckTyping/TypeKey.cs (100%) rename Source/{KSPAPIExt/KSPAPIExtensions.csproj => KSPAPIExtras/KSPAPIExtras.csproj} (81%) rename Source/{KSPAPIExt => KSPAPIExtras}/PartMessage/Attributes.cs (99%) rename Source/{KSPAPIExt => KSPAPIExtras}/PartMessage/CommonMessages.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/PartMessage/PartMessageService.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/PartMessage/zzImplementation.cs (99%) rename Source/{KSPAPIExt => KSPAPIExtras}/Properties/AssemblyInfo.cs (97%) create mode 100644 Source/KSPAPIExtras/Properties/AssemblyInfo.in rename Source/{KSPAPIExt => KSPAPIExtras}/Utils/ConfigNodeUtils.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/Utils/DebuggingUtils.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/Utils/EngineWrapper.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/Utils/KSPUtils.cs (89%) rename Source/{KSPAPIExt => KSPAPIExtras}/Utils/ParseUtils.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/Utils/PhysicsDebugger.cs (100%) rename Source/{KSPAPIExt => KSPAPIExtras}/Utils/UnityUtils.cs (100%) create mode 100644 Source/KSPAPIExtras/version-gen-ext rename Source/{KSPAPIEL => KSPAPIExtras}/zzVersionChecker.cs (99%) diff --git a/GameData/KSPAPIEL.dll b/GameData/KSPAPIEL.dll deleted file mode 100755 index f0dd0b3d18df68abf7b4b8f9cecb2c2f419d51f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44544 zcmeIb3w&HvwLiYjnVB=sOfr)+=_75Yw1H{T)}}9#Hf@upuh1t=-z~P2WYP{za?+Ve z+Yk~GKwbiZAWzYjr}gKHpWxN2a$5xzg?mBqDk|3^z7V{EqKIPq|E{&qnMaZ$`uBH# zpWp9y1GCQFYp=cbT5GSp_Up`a-OiiHK}1e`&YU6oJf8elFYukgAc}*N|2asHc%Po~ zd2Q{}Q#S8Uq$1r(b6+yn8EK7mb(!f%OFWY7>53$}BFi^4M>@^6_^g5g-!zN$#uY?s zH7C7&)s`)GYo|z`;@3Px_X2|l|IIoH5W(jZJ}g}X4_(QPl*rFds@F0Z2i3ois7U_L zKCP?}gWU3)dwFmjtIrV0x<44lS&pQTyGSQVgA?Ghb zw0BmjEtZavlHn8zp));*PtIRG(TQ2fRI(K~rGsUIkL3^{3A*}O$#{neLeUAj3BON_ zpj(!Y`S0BStrjRcsWfBX)X7DUw-A-Qmk6Hmcl~YEqj4gi?&$wBKb-w<<54+D@8c{# zB~Mg5+d&xm+)l{RGo5IH&!}AhMD#CQPsj~{=4l{m$8r(k6N;FFA<#s1UzJvh|0_|> zJh$SBArgu_aZmYB4lh#!X)JP!M%vCCKBk^U`wU}pLCbbLCbq@^$4Po}NHP;RgGP#H zJLyKWflv`Dzo7yORFo$76j1lQ6=lg*d5%@mUq1s%)sAC%L=Rt>PM0#y{2mmjaiL&f z2U7P|j7z4Z;&`Ro^)s^-Syz8pJ6+1S5fvvShqz+&3!s8+qwDy|GV0U-_WU;h!#Qht zwChnHilZx;rfiVW7G@PN&wxUYLMdNzvrgU7eMH6}WOr7c*89dY3qP|+wsRqT#W&Cd zu#k}h7jTOGrBW~JXIhI4Vbymo#1VN4@kDqFrLF^KpP~16A#bqI>@JmJ*K5#mFiS}9 zJBUJ`KBk|a#tMkVcw$};;Zw5*gf$VQ^wR5V&;|V_==8pBz?p6c!PRQ&K%_?vaIf(9 z9{|Q(5?#-te*MguGrHc#D03kYlc$**@EAyP^-#%a<0goh+6dGjqXZxI5>>GQtNJ^D zJA30}Rpb8xd!P-bwOVQhzcjR%P!Z_w1lm*kC6=R))8=Lr)_wwck+%+W`i+_i7{ z{HQGOL6j;BFlvB-i5TT8$f5S;rp-MlO)rIOL~B`yy%`-Wsu&Q_`6%k=l-|eQSgM>? zd0lB`8AmI2tju!>T#J3oa?Hx{sWw!;WW8=S;`t1Bt_-2mS|xDoTF=1kwc$7si-#KM z;Quoq1wRLX;c;LPL?-xcDCp{I2NJ>5=?Xf7E*_PTbw8ld&n0?ag5hanE-IwZ;XV`N zk3)crr(<}!c|1ig7Gkc*=Jl0EFGmH8Lg6K-@uktLf%A*c`rJ7sCj7yd{5gEch+YYj zD5A8_ECmp=WoTI-p;cuT2PI~vpW<`V%=tufd?I2yIvKbdag_r~INz zbBdgfyjmSOMMaq+G+ou-&qX}0;Ab+v;NHH<3v@NBYcZ|9jv#++4NovFnB8rFFL`=VEHEk<~Tk-?jU?Hs1UpjBS$eg;Stj_U72 zEsKcNv83g41l51Av<@Zh`)p|uszo|c3Yw2DZB3bon3csaud+4aIH?n+Io`*N=EE?G z?ZaUt|1KY9=RvF!=fNt&SIK^&V<^CvL&W-W5zPl5?JZ>iXddTQ$&FO#?vDNWlXpJfy0B z@-V>*LIooPR~WGQOg+G<69D-}C{Th&z{}0!%E{*d-7*R1DH03?M<}8&=ggh^+0hlT z%87n?7Da+ektxFzDGU{kP()!M{!`3r6h#V^A}E9+43q0Qr1MH6Pm!Wf(FjEp=In}C z7nTv3E!65&nth`Vx*-iHY7OJf$(IUc*| zZgx%l+3VGy5giK(y^sAItJGB-pE9ddy^p;H=+(o}+y_864MVd=KwmHn&EW)SEUI&P z>(OOEU~oRQczZt#h$nU5{ z17x^&=&BTp3MV?B>UTB3Kh^rc>E2-oZs+!ZHJ5X3V39`!a*wR6mH-ynjzA6OlMJ(f z?upj3(0t}Fhys=wOI4;0^TM$Kg$9=DYyw#oM=@FY0#+-m5Q{^L5Q#HwW72WfgAsY$ zjXL84HRuN9a%9`l$OL(q99bOnS(Nf$P8HQq1V~2?c2^Xk*_VUZS)|$JGayILvgi+a9kh3+-9vOpoq_eB))U?J~$B;_gEdk8-*r{ zfhhYcCr!|R=8|mBi~RN2Yp|z?Kd?MoT~>x$-dbs8*fSJ;C)3*sWnE%>4&z_Jh%>0VU8#WHSncR!=BqT#Xd9pEu=u&%2BYD2L}5su>~ZxzUQ;+MQ;fRioom~N zZRn&Q>1Y&xIbPBMe^!6>U*i%9sj_ubn=|3=%tLUz%tLSh&qH9mbxJdmG{aMXHFb-k=L!SDnxTSlziYUF@S^&h)jj(*?G*RM=ApE>&3h>U(VBBP&; z_=oCu&h*_IMqSjjCw$Ju3lOeUBWXSQl)mPESs`Qu`gT5R10wshb01L$uvdi!n=OH5%X;tg~{;F0x_)<^@^jJLxjJUGeD> zIUCQZ@>gU)#9|p(Gg(~c)VwiKw&TD~UVr;?8dbETSb_H(kANFkVO_*Zl`cKT+|@DI zGM+da0iKzIwXED0)GyBzs|we1kwp)khb?o_DYm(UP}aINh@`nDSB&nm4l(UBw~@@; zT+I*`dK7ai?>}7FZUCE=zYg^vKacF>96qi^r-CyZi|0R*l@AuoATxNI=&aqq6|(so z6H1;XYR$%sEcM`VzT|rF%%Qeq&t5&$z#dlnXQ{?sHlkWiA6AINotx9hjR~Jowgg)! zFlI$m?E?n-dY9-^gE3*1LCL|TN=Y2Gqkq@IKImEWZ-l#Nv_3VZHW%l1B+Oj}1bc;1 zkJUB3;9^r7mSc$lWyD}?p>gFs*j8KV9F1;FES0{q#?XyI6B*VVmTFa4U@JDDTCu^214b)%)%cJ*Lq%w3pBJm68Z1R+C4|sC;Lb50LvQykiFXh8O z=L@{k!&x(@7xJL?@=D)_D>f+wLhgGTxIe7FSuhNLxaM4phtV$C&e2PwQ`u?H(aTf< zs|B1t$SLkRG*Ed}^aFU5$wn)B6Os$DI-uyyNR2(M@v8*BGG2$f=rIIQ2ovIqR$~Aj zr>603$fZ6A@oOu;C@M={%14a~DCb}+?8reKud<%)S=zD=<+ zL%}G)t4nA&LzQo!8c1Vcfla-Osc&O`j1qhlJyEp?=Az2vb2xvJDk?l|1);hrz5}V~ zoeaRi7eOaoHmF!*Tq@L227IhjHBe(yT*MSO{P7JkO7Kw}^HdGg+7$22 zP%ujHiejNnu_r^pr~w9wVv$X;mnlBXtuShUL3BJ?iB16U8v}JVVT=jyVnRj8qu6a|if6h&Kxf>8qu6h*yF5zkOCYJh>FXs{{TnL<=z z)BuC5N&z6k1wewSR9>=+NoKfFa4Ud2`Vp`d-cj_n&zrmpSQ$0SeR$BQ6b~Ae z`lwx{z?(EYc}xmKE<{91`cNWmct*5wIf|l3naigI9gwG-6`K8}?6KLBtQWqyL6VDL#ipTR)OyjLLe&jpYX zMhNL8r8iWf(EJq09$+%G>w94)iv7tnm@59}r%{{@0d5roxVOuEFMyVpP;zg~=?N2P zCOl2EWE)mDBk~wup2!)c|1q+oXZ{z+j>!STRPOE_RH$KTDRVe?FTa1pPh*7+8@TX+ z-%!o30QBf**nAHI^!I=SQ`#XsY98S-y-!emjJd1-Fc<56{4oC~(;q?Vg%AkO)un$G z0(q{xLEJwNB^aq?&}AX_nuhXnl*|Q6PIllF1plI zTrd^$dq!Zu@_~RKDx(AkK`j#~<@q%j%MUhEX*-435Aa?LHw>p#Pkt|kWw7el@5R8j zcd;W~tY6B4l}?|&1W&jsxmDk;orE2AN`CcvxkS7^LC~77Cv`d zo!O%Ql{;39r~LB(dyk%b9`KL4V}&o}3I6}*j@1cX?mmY98_op~f$|jjJI)2%*1K`OJ%|HN`Wc?%Bp!2_&ryq5g>4VS10kT%SAgV=iLpECcy(dqAQe^F_B-k^ zaU*VIs8zw(?RWU}3gLth$P8;nr2_+B&CX1#rAz8ZZRaellpWa!P!2~yIzQk4v2pY6|1iB61>SSOiWc#*$M6_Cyz1t{i_1yeD+0^= zdIvS37~?U735U&4>a%a)G8QxLSdIpGN6G|78Qz1=k%W&zmOjlBhJ#;sF)IIr+mcLO zW$innlMvkH>Tg&HG~mq{%1~Wo`h&$sZIS=h>%G70+6um=(>kcgf0pp*5;#`b5~!4m zvh=~hhA3iD)#vmoqe}0n6x%C5*5#daD2KBFj-ol|71o^?4+6duV=uSw#NgSMnN1jv z@5Fp4weQ4ul;BQ`QD}t6${!#o+J@h7=lkR$nigTU#(-hI)`ggT&vI(1(eaL#VO+;z z63g0v$D3sX|A=lzX9nC3Tz4x^Gj?uAw<_m^pg#NZfxXr3$+;l2ZV1GSnb5w`f?K}p zYcsLIbeNa_blz+mwcp@mAIB-Vv9jCe@STU?kd=qvxR{3!(O?nZJ6r;B*Jkd#Mm-N# zT_c`hU6l+|dAK2z#5G&;^{~y8(Zfb$^so^bJ#56V_TV~Vw<^evO>}O|K~UxQ=c#m< zt1n5gFuC5J;|3fH>Q;jwX=35VFVf08);`&3%2qO`CWk)ZQ+7iHYs!8eIJw2f(Xz@? ze+?Fzc6;o6?mKeYbHn50&OSL-8@;`Z_@mZsgwm9}^{jQl-;d+5Ow8h$W~qr-S2pVP z6(2oeJ*bYJB=IYKoG4fE%*BvBiZcRVzYL+V3SEl(xBKdjR+RBQ+~T8_4eCLL8!(yN z5K1mkDFxZYcc9SWkXdipTUOahS0!l-rZS;CzqwfPS zZxB+}EqY^?l(p1F;}N7Rz6|e0iYsfXi`FAZS%q9u)>dggf|S+BC1s73_QOb_t4@|ge}aW-H>2?h_Lri5iVZuy6g6t4dn?8!|0Lz(a?3vtCihm1PmcB5 z-)c(zGfunY~$H)IfQjMSHbA7qr^dC5^V&}dV1GsV9! z1)~Jd3y-R3M7ED^J_E1nA3|Aj3DO$@LIe^X=G2IS1#NlluZ&xca?LwEL{E1@*} z2O|omWjMlrnBtxMXOv2L{*x{LYyR^wx~_^$y!$SA?{ zg;>?VN}D3Z6klfwMhQMz!4#`(iZoMT4dNSQl;HXPqiO&%q-v#yDV}5sMhQN89w?$w z0KYpr9gj+M8?V!O>jQ@kr5-{hEEfB(LKUNrp%~Qfmg6Pp^fBi8L2#8z2_TN=A^5$h zs(_~!en`n3=y8wvEf8C*IGMc<@Kh=^s&H&Ue4_}*7V~M4`D(CFA%|i9D>CN40o3yN zFe(l687?*!p2AdFV%3EWXbS3<;u|E-qS#+k&9&X;x4HIr0OUT-Z9XT3CCmw14R^r8 z=pOUCC~+@*5=^mM;yX8lW`Y_ar|vNS9fd<^U;@rBu_vW-o%q(|7)H1SU79`C%$#eM zlu%h4)*h@IZ}Q*+&}|zy$v5ca>z25XGQZCvxeH2*oi$Y;^M;&v1w1`*)U3rqlT@^!+gVHD0gdv=*6+1&tFV54xr9>7R@?s92WVdq%S2$}E?b#+cDNX#~9h$&F^hp$F zRlwwLa!{zeD)|7qdU&xal>WvswD|%kt%GVD_5KHQ8pdvpg%+OR%{6#|0b9tsm5$8! zP8~T_SxH?@6U&jTg&&hD*ZT(9f!XUy%^w1)=J^tC0p6G!LZLc1x7$dLrJ{tWw@I&Vg_fbZ;1^w52bRu|yI`j;Jg!-nTeBA^` z2<9)q`T<6ByX?;yTEvoJw7+CpMk`tu-&?Ei-8t>=1>y^b(aU%&gBRYZ%9dXNB%)78 zhTyv7581Iee>KV+@(n;;z9<-UMn5W+;Qi?X-~stU4)bN;N|)S(yrb0o6*7~X7X@>2 zsd_|C_y^OK)%%df%U~tZeip|uxypP6c%M;P2X5q;JTsH$QST@-Uqx~B*8qOqovBua zOMLS`GbIRG8M1)x_E_8t$su3y!>YQ&5goMj*XaV5fyeArB;>CiS=k%?hIzim;B|n6 z1BWBy7JE@>j9IE^AgPD*V|R?(HEw66Ay68)oEzZ2TB!+B(kGAIzQT!gnT23<2x)zC zSQY$M5ekF`C%W$Z zvP@k&6w7`L5=`4%T3T1YZJ##&2Ve^4H$Kd^Qmv2qE@hjiK8Z5F+vtBkFzyVyT)G`) z+b!1DG0kb?Q(1PWjZb7Ub_4}i9*@vN^hT6rk#^zN36fi2LtPp#+o5iC--J|ZNN}C^ zyki|*gx@i;YNlm-4Z-$CyR4IHT<>GqcSYIOD!r5C*#UV}>yyg#$&>fAVB0MQgB(!Kps51iBXBWbkgoGE%^iSY`izI$eGV{4Kl6|`2uZkf zhL_=TK!e&~z##p|%cXx7yyoN5nZEOVLE0wi1Ynp_K9>Jc7&J_OKpJz7f2==9xA~d= zWB&7fK6=!D0+?_6|AbP1fH_nPydc1BT^?YWKL;43X9G<0O5n{v5U*fx=^S{8K`Q}+ zbSp58biZIu3H*cLCkL5wADRx*1Hp%bLHeO!l7)=u&)X^qnHczgEN& z215+11l}29*}g9@5N7<+FiWx~ye(|dXTx{_Px}gBkX}Wpsg1!D@1xq{6Mi2Rm5{fT zW|jQORZ2?%gJhNrl?3Uiq`zIloI_(69vZ`?zEbAj2WZee@O0R!lg2{({q& z1yb*3A- zfck_|)0y%D>PM?^U*x{5T|n1Ju0(RXkz=0Igz{qKn9CqW$#J?E_VFY41ac>}1IRg$ ztMYT1om&v%To2tMW!J-pj^jPryAhO%h~FAKPM6b1r0fC7T}k&#?%R^P3ct_7JkNul z9H*=4E1kWzdko>-8Yg_e%OdCHwwUy5GV5ex9QgZ}nFh z*U^Q8DaSbG;3x-tzd*%v9(>QBI)RGAGO4@X;k4=+beAInntK65v_NP4VnB`73EZZ$ zH?)fkXOr_0k@KX`d`Hs%A?eZOA5HR)i(a1<_$7hg68L_=5bc4?tm)?+A$raKuYh|4 zv-J=?TFCIJLhLE&mBM;JSJ7U;5`j|%&ME2zrlE*SHwtEtz%GIP0lg7oGY+a;1Yq$0qwRfaLxhGO#*ib+$ZpWKvUo$fmaFqfWX@Y9uxRk zfnNnI*1qF?T654czo9#5lOJ0;YVj8W9`Tm}4)`YkUhQ`xihR(o?9bMI&3_&GVs8NB zgf@nr#&1vTz-kAK75~7MU;2$=rdeM470C0sG2GVbQich^bd}!VzN`WPgsvL;5^4#^GC z%Q!`G)Aj3^a)?fsebVEhx2@cxW7{<^UASJAeRzC3;@M4Bt^{R%`d2GAd+ZrRVCM!! z`RVbT+bg-R(Vl_@$USbAEkV5ieFG~qZtude3-}iqHgWDMIyrtab`<9~bM9t}jlJDd zKx-s7M2E(H(i5c3R_=1-3h5%rDSH;t9V$oPM(ag%ua*0umOXCfD- zCna|U{dD{^UkQCF$MPlgl;qf+tGp%j>LQkm?RgNnw0KRYK z9)+9}sBN(-dmP%9)3hZj_nhRGFXf!FQaOF8o^wj~a(YH`O81HMyp?0!C(%!oe&S1$ z>6=ryp0dPbdQRoA7hQmPaLPGSh9kLN&tz(o+!a(-HqjNKt8+XuLN`f{HF`pi(D$tz z_g@8Fb1rikqGRxo3R-&}=dPqLmVLo}VIi2e% zZKL#gUL0e#damrJo+wSr83EI2hUD1tzw=C|g*-`%<)?b5(`v~n%TK4*B*&f{^}dJh zmtC#$Gs zv2s4iMOJXx%`|@8Ehwu~xv}StyBE3TR@uyP4R&K?(n>@>?%gSAd zTq9j>NWaSd$zUW;+4_UdMagTdf(xXU4`gUM9 ztSDDac*?h%I;`AY^rSh;(U+e_Zv%;jeK?1aDg zVpL}3o|@qEx6mvr_p=G({H?Ue%AKBYuD^{Y@Trh^$VPvhDl@s={&rfJ$(jEBv_^7| zQ(5^H{sXjIl~MhK>--(`G0ELR73FvOyXZM9SA(2M^j>b^Hd<5upud|UncS!RX{wjp zEwr`#bN(aLW|i$JKjH7EF?&?KJ>^gNFQbT+J5c^@KgLfc_q_iK8nkkU%TM{=N6+n5 z^*-eOx&JD9JCpkj=J;q#m0el>NB@oVV2jG#R$dbLAbrZpeYCtha0^|g;-Ey#Tj?6f z{ah;zPIlZ%l-=(65FM~`cOiEhoe{57$lT9nYCZ^{(9Vyc00*i7?6cqznzv# znQHxZ+HB>x_1kHes;A8yKhbqNwOYBI<6rRJPL1Ldc6)0jcLn5}=f9oqv~sr~cL!~Z zuq;d&aNkMoD(6`@=F9rs)Me#XjVW>8O-Cg6HF~S`y`H=2DJz%uKB;|}X3L!OHM+ZC zDRK|xwEkiGbWZCZrl&HkFY@0*ujEjEgnpNyyugp0ig?`>bW7O<{*Thj($h+#qx6R4 zSfj$gQHn}WGi71m7_~}HQQk`(RvA;?OMF7}U4zeLegx>C7Xh90DxizL0%*{00X6D% zGrUyb-DiOj?8R&vMY#nS+(!vMH~qJ^IFj=*oPFd=&N4eiW*1GzBFdIzsif!PoI<1D z3w*b&>vUS^^R?EYk+V>TtqKa6r?SRK3019kw!`h%p=zYyCnY@+{=48uLPa?epNlyX zX8T_bsA0{XO@E`9F;hzz!&j^M>2|>%C}AC^;4ng?M}i&`DpH(zD^8LMiiMdamV^TCPkj zE`7%0kelXK9?RjZ(#%bxT%P5nis!q<+cGdc`=>_N;ru|Oh-j^RMp+=&o}AV#uTcIo z(&ozYy~53g6UdA6o9XzB$GSSMmD5T*y?9Q?XBkZ=-f2|>F2$M5G}lJlaK7%`2DsYQ zMlrh7l>off)kO)q&y}Qu^hMWUq))i|f&aPda_n1PcixFRv@g0Iq&sPy@i6Kt${oh% z=@?2ML#+GSS!fG6D(wd1hoRN%kqJ`d>^-A-_R(U^hK zU%6)k^CM#s(!X;r1-CQc=GB6pMs14L!irGWi_ z)3uvD2efMKe$PRzR{IJdV|ug(?FXJq(JouENn?1SHqASv?Lhh}?P1wbJxsh(y-?$s z;Gp)3_gUbX@^vclodW;KzMpB=YE8Z&@Vv2s zAN10`19(jPh3|~^1!?h^z&q(8-z2?>KH@)DpC*=kR(s0ds6VTn^0(;MYQJ)u`V9KH z|4HyXo03d0ypT>wM~I>@Qs^2x9APp_Q0iD3GEK-)n*7RAv187 zUP8Tr`vGqad|Z#wJ%NYydGzDJm-UeTMq~+{N z=N;1&{bDIi7x1a(V8JPrUX66Mes{qy^po`If>-tB^mxIa^|iwPBz?P}z%fOCy`b39 zq!$F2Y7KgCV60;;@e1WaomU?RX>pLfbail+BO(5#={tis#ng`kYaN=%)7Z$Zb~ zgG(IG6Z47bUkf%mxZNfP>%P&UX`FtZUI}h@yd}1MoyOpZ;22FP>~uU#Qw#3_&8))X zj+ow1*yu=TTMIu0%pPD4>iva}0DeH=F~H5z7rU(<`BdRcj%T&U3ti4O3v=9kuJdK> zhlT88KP{|vb_hN#@KS-j+TBIBIHzkzi|z*eY2m%jA^qVZ_Q0POe%yIn`%2MgoHy$~ zF8ZAFPW{cIM;z5S<~iv+rcVle-TAQ2oSW$W(2GcaIy8jzNu296(Nm#UoL|si3QczO z>aPL+1%0sK_rRYCz3n`ymxguMNj)0&xL(v2gbQ6)N-sRAKVC4=bv^o}%JrsFEf!vdex&MQv2?zhtGi;ufr)VCDB?YvNH zDgGwX$zsLf8P_L-<`IEk68N~lrvyH$y;0l(KX;TY)ieit+HxxszhU9`l)PX#wcfxf{X+EhQpo>U$?uKnjz5BDwIe!)G1I(%HcHU@ z0e6YU>$?(-*L+R1WektgmNDbp<>DLDG(Jb&jWU7{{fiR7UCxHi1rfFmukNR9Mpadc%{H=wBI6qz4j*HP1@Ul zw`v;B@9vP&4{HX}M>Rj-{aO*=6C&-CkUUIJi~Qde%=3bIK`<{1=GTIGT`*pqH4O=z zqO-mglCGBYd`UM*dIi$w;g@(9;%&1Dp5=jAxFg&NSmikeIM?$K;3B~{dQKv}M)1u7 zcX^%%rq$ys05q7x0Z+7`5%)(c=_P>STC?a;3-!tXyb2H5pfeiv<0*3@16L?bKivl%|&i6?j76DS;Fa zdV%!<_X->ocvRpCf%OG!=e+_41)eI97K6e;V7Roqz5H^RNx7Lrvy?+>I$q6u^k2l9u;^(;3c=G*P_!=jME7;R(k?) zy!LgpU#acGx^sr4=V%F}=V=GD4}yN1wop&&pVOb$-_Tu-laA*d6P;_F7dx+Tj&V(O zt#j>n9d-SOD`+e-4j6YBpEJH;JY&IM8o$N&rzW|h?$z!K-J9`WrDv{ZooAQlLN}cv z+>|;8dC%d71ze>5uz3b1_ixuE&N(s^M87CFAUHjT>=y-x1($yfeTw z<7pgp4?yz}ZjZw_eGgLvpDFkaf^+zH2=I31K?S1ql)`x#_RB5~Q_uw;K+vdEFQdp5)@2t{(qbDq5x_t8Y7Ut4PoX_pw&wT~H9+LMk4!1F_RdTBm9 zb3UtAx6W+q>4-1E&CbS#jZG`o(w3%;v1GcTHJvcKQY((6<6Uj>woUPUiBvinW8|Ld zJ?HP4w`YDH<)(Pb>`Au9SF|P4X0oXh|8mu#c-yF?Yhx|(j(4P3*+jZ{z-W>w$Q3)Y{Fhu?~?E;_u1LWVOz1fT@P%vxK9X=Utd3 zk0j3=c_kS$un^hIXsS|kZz>({+>%aoq-aC3EuM_GEl)@v#*)2z=ip_D&9o}k8Q;)y zalADx-I_vQ(z;lBzpB;Tv@scPPaF}l=GJ&ud}X2|9ZwSLt4I#TI(p)J_7J)UK0%4p z+E_ZCN^gxPg;yfAsi&(8uY!@;(bn9NfD~J~26b?cQ*(DRk&bWXf2^xL5$|YQ4{=mV zQ6;)s*T$0j;>*p$UC?-0PdW_&bFpG7#x!@vIy!_dH*X_FezIkXennjmfkTnnGzu3r~Rbgheb^`N_B zt?@?a0~gxbu#B2FuBMe8iGBOiv^g1TJpf|1Cp%$tJe`&|2pi$HiFAq@x_VV0$)+|W zt;#5C>Pn;&v5v$g+#vF_r3;2mv?pRM9q~r|vv`m$nP}-jXDP*JHOE_f;C;O`3$i99 z*tNBB%V{`wPj}AH058z3=-QWnsW$g^$JvtnT$waGSI4^AI^qO3PbWI#jI}V(fFtIi zID1egd|ED?i?aH@14h{wPw#2!N~L36t#OJUf~A@;gn1CgT8IZZI;$%!WQe?7sdh8j z$)>YXD-&I9Qn)WOtn(U|fUzmowa*%4igAk8B9QN`whhy0cCi0)te>?CV?LQ^&F8`+ zmF1$kCGC6m#5$+7##;BsXA{`9C0a2az)J-cQp%ubVD}W1>y9N^fvw_OLd#SdpoFszPXB*J0g^6)_Ro&9Di zo#D@k=HAXmRK#oX6_cV|QPUXpD(` zN#BBa^@7^=d2?pQv3* zG?`py&bFh}>Uc*tx&l371 z&?Uy}Kw!kuA#K9%l4cL4i8!ssB%-{IN#bp?SZPXGGe#cHnlmKKXBlKu;w$^&$<>Lr zws=>jCv63nCt?UpsdS<>MRjTo98WH3-LnUw2a91dIh*QQ)oc#`TC!&k@@e!#N=$)# zHPyAV)l~3h+oXU+C#CVvyV5jFN)jTc@hp#6hXF8cV^H?mJ?&+AZ^=^RUb8FOEF+E77--il?aZA4EaI4ntO;nFVUJvQ$s2h z?`-Mlh#4<+nEn;&in&VLx$1M3VTO z8-uN-7`RFimSM7|;Ed{AYjVu3lfUuEsE@vZ`l7gHW7{x1|6Bf>Gx=d@ly2_n?lzN% z4=e0-9JM3fCwTU=Ws$kTZj)l2V%5>)QOb3<@!)0#Shh7NYMT{@bc9LsKs>{}p_2!L z*~4AC*|b8U)uAkRs<*QMg8O(o;Jwzoza(_^NwW3H1FdX8|%IXAGsU~dA+VOH3#=^|sfgLG} z#dAHDd60#z)+q^^A!}HaA*OXHB$f;nB6CUc^0?(4L{hrR)#a5dcOaenZ$H!&FD4tXUD^0VjXXKV7a zwfWhG{Oq#)Y-4_Qd46_9es(1_V<^%ECe}O+-5jE_UERZ$x8n^Pu^F2Q!ncg%xzX(I z=|^z6L#Br`x~H=xo|NucWz7QA zE&?r&x595Bq|CH3LPQWAOR7_;)9PJWfwF;`Qat{!OykTZtbLPRQk%1x5DzmiH#=i6 zC8HZv*t9UKps>;-fmIQ5s!2Pha=e0R<4LG;0Bc5Tly2UijH_KCFRTzL5Lji^OxSjz zkw&bp#604sby$lq+ZEQDg6G+WF0%`he=^=Xo0s+2pJe7Kndg(T@5GAEp1gQQ>x$c} z4pw9>wj2`Pc$i|BFB|+=XEw%TXk!tPqn=tpC`7MBO*v6n){CVH_7JkoZL$to@@Cu3 z`{P~9R7?=Hcs9iv3DYvRc(N2qiEVLGwIA-T)MJ%rV~fs?0@P->fqHPBGgOyr73@=B zg?!soQw#PQjUA{#b(S-4j_t$lglp#0@+6Ek!G@&0*Eg&#Kdq|#<*Mp_&lSf0Q`hj$qWuCWfwCK)p6h0!Q7)i)>7 z9XV@!Hj<#&U#070Bi`MFRbzU#uv5z{E@omLdTe}k!jRgQK##YoL6F2E#d1( zn`x01lRE|tCwcUw_G7Z5eK>DvgWc>SfmCAO$^;gI@gvq50Y^mYZa+dwXOUoSylWpC zWnr*Yl??zEUK`q}xu*rIZJvN+YU8z9^=xbh_F+}og{7apP;X1@abCbJz3Fb4_c>_ z!&V-L97|@2v90yyC{=T+3i-l-h^e%+#Uho(<|`(Z+(YtX}xQ3zI4-0n|wn`vLD~ z)beJFEN>;vt3y7fR4AcQYm0noEEUm2Pq(FKCXEo7Nn(X|R$7@fmRXrJ)>xS|7Fd}yR#%xcmR6ZGmM57s7FC%vR#dX8 z$&0ntkzbxO+mkm>9eHj#N`2d-VOtE0bk06x6yo8Hp;BF5WBD8~DiEex({dCchvdBQ z$oPUfCYP%o^mU5IYgcP8ACf6A!S+feP>Ii5a84k{_rq2ha>i>fIn?Be6H8f?XQxov zxXRon$2e+QgL~jOwioPKE_>dw>RMZkoB}m{j9BETF+WTw%dbYk!ox;c4S)&UZS(^8 z-slBz#L){Dus)c15%Fc~in&LbB0B?tU^vR&uQoT4nkta6aadL`&&(AQ@8c6kvNp=h zEpoPjN!Yr2YO({e^dw9BZk^bU$ksA;v}(5RB7o0&2eRQ_%ke(XYwfzW2k?~y$s>8q@b(1<;G1kdly(0^-YHh=r>&Svev%5FRw<;rxutaH#cgB(jvPI~@t=LQE zO`w)Ca5lcwm?i6YvpaJHihXm=fhk`T+iPvBh8;BJJT}{uO*XT!8Ox9EY!Bd&sx#GU zCOZ->nOo;sZ5T}aSG5K225-dszDdH76tw~-$Rr$mBOSxADWWrVg# zqx|wQHb8U{Ff7WRQ5wd}C7?0EJOY-S>OqbD$Z_3PGy)B!1n2XarXyJMumU|ON#TVm z=8s!HX)Piuc2F;{X>f|;*^V-t_#i)vsPFHUvkmQaiwtbOv~bSKp(Iw2%aWFiDYVZf z2?pJnNv|b#+7cDz1+p|5V<2@JPT`Q>?E*yiWu$9Q$1se7m!q>J~8}=0n zHA{-EV>$C#2gpj$o6=5ZfH>gj4!C)!6Y_N7$CGXUV@wP~CkHc;De`@HEog97!ZM9g z+6-x#AEX?$)E;u|yN!^n6E`0WUGl87kB1Fc88iwAuK-#nd% z-;QAc*`1W*sKLnrf=$$h+J{kt2k>Ty#)D*}P$nEATVK%}Ky9weZoys5s>P&rmV!+m z120xqac=_EdBUAr;Q*ro1Up_U>f=lqDVB(bjtU%X;0VmBG-S<~H{MwQo;kCN&ZRl; zQb*RDYos7GbKtJ9WnT+S7o=0~~ir1o&KeTBHX1EqACg6f2D*EwsRrwF#e%lIE^e-E4bvHfC^y<}bkT zDCc&RbJ+8M9f$L+1EcTz+s0KBoS)B$JIJP7 zi`v6G&#G0dXjD#xJ8~R=z1HEg3ZKpRY{X{;KKKCx2Y&9r(TPtFK3({9BYjUN&18rvpj`nm!%2#1G&4Q4W5pbvfRWEb2$3&Ps-cA5y9_&!7>FL-r4aa=Zawb z;dcN(TH1B%J*}74V z?;_}7Slb)XCZaSn_%r1TNb68Bq;p~{{s|MP`)k-G^9bpaNOwVi!6yPmY%!|@hA1yr zomv42z}!WU?RAmx_X6Ky>y($5qfIoyT@D)-lZRc+15+B|Aq^acTw=RoIB-EQ;*!3V zo*fD?NeCn%e7_S4vg4xNV9-)7rrM7oYK;jEO+=|%p5Yj`KQx7_bB};Y5LOj6hBUV3 zJaiWP45^`+Of?hM)nQ50sz!xicxXPp4Z&Rt*VcmNVt~JJ%^C)EPVCq&JZs;M+~@hp zr56RzyUJN~bR>oqyO75t`h_cWw{$~=&J9%fEG*3KRd}Sv73o(0XnFlJNpt)3a0Ms| z&_xq7eU#~9kKM(dFuIssTJhyxgx^9DE`?wAg2P1d<+b7M#UwXtCulNQnIw zX9|LDi3$nXJ#u~+!W1v>;mzCsWeqUSY61hT{^%Q7K{jB2ojzMlAQa#f#W@4!7pPzsAN%6lor(TMKst+)i(}S{4Bn z2rHNr2A^>8V+F_y{BB2RJ3B)`Fu)>(hrWPXq3vA1An104V^;7+xONj_G*1t1Jv=2m z&+T@W!*c?Rt_ctRMWJh8j&NP2y96Y4;hBK^-(u^ja(NJ@3)gex)peJfmkJ(Ubrj)0 zr&|wr8DQ~1;pjAX2wb9P;pvhVT+D3-6bpLXPEXJi9()=;qWj%Wo?s^;k_5%B5^>l( z-6+?UY3kl=EN`^oHE76@`J^{}G;f{CLp?>4^wi2-G~AR0Q3y{-qpcAs_NS z5DX3?wuURpWThbE6rREZSdCUMdx)SzYNE+03%MQeJi87L@o>pfR|-Pe^3fDS(G7QS&^ZDRmtSwh8#_U z5%Ft|*cAY9dg$+mTe7!uClF3@ z^t>}z;PJ>@7{Jp@u3%_8V>ntA1mTyMjyN5_;!@`#c?5vdKTs;Hdrx9t1mD$(;LA0M!~0|M?*N1|S7ni}9FIii%&wk? zBNdIN#pf-Ex7D`Io!L@bJ7?y+_S)FYnmE9MdGqHksIFcRo6`zP-b5e{PaH)HZrd&4 z4?ild(Pq7LTQb&-FI#0^rop>BcyUD={KzbA@Jj2#&Hhf(tc~$xXCfu`$b3;Li_6@o z&4}37W-}tzE%Rdjd3O9aBVxUJy0|OegYQFh%!q8nOT>v*+~RJ=3&vfGTh2c}HotZL zg6i72^Ww8>YR71s^&-44*$qzko`-rxPV@ZJCA+tdn|JD3&!w-v?|Y9u)O({9)%oE$ znaxb%bR?V4O%-M6@X~p5c}K@Oey31mkH<3?eP=2tQlDGx|AO@3QYZnX2J!jtzyG%_ zz`u7c|MvD^z8%zWuj4Fc1fJde~^@z*l!qUxtvQzROCU^WQK>puyWbu2{I6X>&k2=BS18P16$S>jf3#>!Fx7t*;)Yk-`CSo zSDonESLT=$B($u>H}vf?zOv}TDcyQJ<@N|Pd@IGdZrFkE$09<*=XQLd#0UL+DjfqQ z;pU0ku{Gr5b$qu=PF{PN;t~Wj5~h|NHO%Jq!GQ!l2L3 diff --git a/Source/KSPAPIEL/CommonAPI/GameObjectExt.cs b/Source/KSPAPIEL/CommonAPI/GameObjectExt.cs deleted file mode 100644 index 5c44ab6a..00000000 --- a/Source/KSPAPIEL/CommonAPI/GameObjectExt.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -using KSP.IO; - -namespace KSPAPIEL { - internal static class GameObjectExtension - { - internal static T AddTaggedComponent (this GameObject go) where T : Component - { - Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); - return (T)go.AddComponent(taggedType); - } - internal static T GetTaggedComponent (this GameObject go) where T : Component - { - Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); - return (T)go.GetComponent(taggedType); - } - } -} diff --git a/Source/KSPAPIEL/Tweakables/UIPartActionChooseOption.cs b/Source/KSPAPIEL/Tweakables/UIPartActionChooseOption.cs deleted file mode 100644 index 59fcd59f..00000000 --- a/Source/KSPAPIEL/Tweakables/UIPartActionChooseOption.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIEL -{ - [UI_ChooseOption] - public class UIPartActionChooseOption : UIPartActionFieldItem - { - public SpriteText fieldName; - public UIButton incDown; - public UIButton incUp; - public UIProgressSlider slider; - - private int selectedIdx = -1; - //private string selectedValue; - - public static UIPartActionChooseOption CreateTemplate() - { - // Create the control - GameObject editGo = new GameObject("UIPartActionChooseOption", SystemUtils.VersionTaggedType(typeof(UIPartActionChooseOption))); - UIPartActionChooseOption edit = editGo.GetTaggedComponent(); - editGo.SetActive(false); - - // TODO: since I don'type have access to EZE GUI, I'm copying out bits from other existing GUIs - // if someone does have access, they could do this better although really it works pretty well. - UIPartActionButton evtp = UIPartActionController.Instance.eventItemPrefab; - GameObject srcTextGo = evtp.transform.Find("Text").gameObject; - GameObject srcBackgroundGo = evtp.transform.Find("Background").gameObject; - GameObject srcButtonGo = evtp.transform.Find("Btn").gameObject; - - UIPartActionFloatRange paFlt = (UIPartActionFloatRange)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange)); - GameObject srcSliderGo = paFlt.transform.Find("Slider").gameObject; - - - // Start building our control - GameObject backgroundGo = (GameObject)Instantiate(srcBackgroundGo); - backgroundGo.transform.parent = editGo.transform; - - GameObject sliderGo = (GameObject)Instantiate(srcSliderGo); - sliderGo.transform.parent = editGo.transform; - sliderGo.transform.localScale = new Vector3(0.81f, 1, 1); - edit.slider = sliderGo.GetComponent(); - edit.slider.ignoreDefault = true; - - GameObject fieldNameGo = (GameObject)Instantiate(srcTextGo); - fieldNameGo.transform.parent = editGo.transform; - fieldNameGo.transform.localPosition = new Vector3(24, -8, 0); - edit.fieldName = fieldNameGo.GetComponent(); - - GameObject incDownGo = (GameObject)Instantiate(srcButtonGo); - incDownGo.transform.parent = edit.transform; - incDownGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); - incDownGo.transform.localPosition = new Vector3(11.5f, -9, 0); //>11 - edit.incDown = incDownGo.GetComponent(); - - GameObject incDownLabelGo = (GameObject)Instantiate(srcTextGo); - incDownLabelGo.transform.parent = editGo.transform; - incDownLabelGo.transform.localPosition = new Vector3(5.5f, -7, 0); // <6 - SpriteText incDownLabel = incDownLabelGo.GetComponent(); - incDownLabel.Text = "<<"; - - GameObject incUpGo = (GameObject)Instantiate(srcButtonGo); - incUpGo.transform.parent = edit.transform; - incUpGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); - incUpGo.transform.localPosition = new Vector3(187.5f, -9, 0); // >187 - edit.incUp = incUpGo.GetComponent(); - - GameObject incUpLabelGo = (GameObject)Instantiate(srcTextGo); - incUpLabelGo.transform.parent = editGo.transform; - incUpLabelGo.transform.localPosition = new Vector3(181.5f, -7, 0); //<182 - SpriteText incUpLabel = incUpLabelGo.GetComponent(); - incUpLabel.Text = ">>"; - - return edit; - } - - protected UI_ChooseOption FieldInfo - { - get - { - return (UI_ChooseOption)control; - } - } - - // ReSharper disable ParameterHidesMember - public override void Setup(UIPartActionWindow window, Part part, PartModule partModule, UI_Scene scene, UI_Control control, BaseField field) - { - base.Setup(window, part, partModule, scene, control, field); - incDown.SetValueChangedDelegate(obj => IncrementValue(false)); - incUp.SetValueChangedDelegate(obj => IncrementValue(true)); - slider.SetValueChangedDelegate(OnValueChanged); - } - // ReSharper restore ParameterHidesMember - - private void IncrementValue(bool up) - { - if (FieldInfo.options == null || FieldInfo.options.Length == 0) - selectedIdx = -1; - else - selectedIdx = (selectedIdx + FieldInfo.options.Length + (up ? 1 : -1)) % FieldInfo.options.Length; - SetValueFromIdx(); - } - - private void OnValueChanged(IUIObject obj) - { - slider.SetValueChangedDelegate(null); - if (FieldInfo.options == null || FieldInfo.options.Length == 0) - selectedIdx = -1; - else - selectedIdx = Mathf.RoundToInt(slider.Value * (FieldInfo.options.Length - 1)); - SetValueFromIdx(); - slider.SetValueChangedDelegate(OnValueChanged); - } - - private void SetValueFromIdx() - { - if (selectedIdx >= 0) - { - if (field.FieldInfo.FieldType == typeof(int)) - { - field.SetValue(selectedIdx, field.host); - if (scene == UI_Scene.Editor) - SetSymCounterpartValue(selectedIdx); - } - else - { - string selectedValue = FieldInfo.options[selectedIdx]; - field.SetValue(selectedValue, field.host); - if (scene == UI_Scene.Editor) - SetSymCounterpartValue(selectedValue); - } - } - UpdateControls(); - } - - private void UpdateControls() - { - if (selectedIdx < 0) - { - fieldName.Text = "**Not Found**"; - slider.Value = 0; - return; - } - - if (FieldInfo.display != null && FieldInfo.display.Length > selectedIdx) - { - fieldName.Text = field.guiName + ": " + FieldInfo.display[selectedIdx]; - } - else - { - fieldName.Text = field.guiName + ": " + FieldInfo.options[selectedIdx]; - } - int length = (FieldInfo.options ?? FieldInfo.display).Length; - if (length > 1) - { - slider.Value = selectedIdx / (float)(length - 1); - } - else - { - slider.Value = 1; - } - } - - - bool exceptionPrinted; - public override void UpdateItem() - { - try - { - if (field.FieldInfo.FieldType == typeof(int)) - { - int newSelectedIdx = field.GetValue(field.host); - if (selectedIdx == newSelectedIdx) - return; - - selectedIdx = newSelectedIdx; - if (FieldInfo.options == null || selectedIdx < 0 || selectedIdx >= FieldInfo.options.Length) - selectedIdx = -1; - } - else - { - string newSelectedValue = field.GetValue(field.host); - if (selectedIdx >= 0 && newSelectedValue == FieldInfo.options[selectedIdx]) - return; - - selectedIdx = -1; - for (int i = 0; i < FieldInfo.options.Length; ++i) - if (newSelectedValue == FieldInfo.options[i]) - { - selectedIdx = i; - break; - } - } - UpdateControls(); - exceptionPrinted = false; - } - catch (Exception ex) - { - if (!exceptionPrinted) - print(ex); - exceptionPrinted = true; - } - } - } - - - // ReSharper disable once InconsistentNaming - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] - public class UI_ChooseOption : UI_Control - { - - public string[] options; - public string[] display; - - } -} diff --git a/Source/KSPAPIEL/Utils/MathUtils.cs b/Source/KSPAPIEL/Utils/MathUtils.cs deleted file mode 100644 index 59dd917b..00000000 --- a/Source/KSPAPIEL/Utils/MathUtils.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIEL -{ - public static class MathUtils - { - /// - /// Format a numeric value using SI prefexes. - /// - /// eg: 13401 -> 13.4 k - /// - /// - /// value to format - /// unit string - /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. - /// number of signifigant figures to display - /// - public static string ToStringSI(this double value, int sigFigs = 3, int exponent = 0, string unit = null) - { - SIPrefix prefix = value.GetSIPrefix(exponent); - return prefix.FormatSI(value, sigFigs, exponent, unit); - } - - /// - /// Format a numeric value using SI prefexes. - /// - /// eg: 13401 -> 13.4 k - /// - /// - /// value to format - /// unit string - /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. - /// number of signifigant figures to display - /// - public static string ToStringSI(this float value, int sigFigs = 3, int exponent = 0, string unit = null) - { - SIPrefix prefix = value.GetSIPrefix(exponent); - return prefix.FormatSI(value, sigFigs, exponent, unit); - } - - internal static string FormatSI(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0, string unit = null) - { - return string.Format("{0}{1}{2}", pfx.GetFormatter(value, sigFigs, exponent)(value), pfx.PrefixString(), unit); - } - - /// - /// This extension of the standard string format method has available an extra standard format. - /// - /// Examples: - /// - /// 12.ToStringExt("S") -> "12" - /// 12.ToStringExt("S3") -> "12.0" - /// 120.ToStringExt("S3") -> "120" - /// 1254.ToStringExt("S3") -> "1250" (4 digit numbers do not use k as a special case) - /// 12540.ToStringExt("S3") -> "1.25k" (using SI prefixes) - /// 12540.ToStringExt("S4") -> "1.254k" (more significant figures) - /// (1.254).ToStringExt("S4+3") -> "1.254k" (+3 means the 'natural prefix' is k) - /// (1.254).ToStringExt("S4-3") -> "1.254m" (-3 means the 'natural prefix' is m) - /// - /// - public static string ToStringExt(this double value, string format) - { - if (format.Length > 0 && (format[0] == 'S' || format[0] == 's')) - { - if (format.Length == 1) - return ToStringSI(value, 0); - int pmi = format.IndexOf('+'); - int sigFigs; - if (pmi < 0) - { - pmi = format.IndexOf('-'); - if (pmi < 0) - { - sigFigs = int.Parse(format.Substring(1)); - return ToStringSI(value, sigFigs); - } - } - sigFigs = int.Parse(format.Substring(1, pmi - 1)); - int exponent = int.Parse(format.Substring(pmi)); - return ToStringSI(value, sigFigs, exponent); - } - return value.ToString(format); - } - - /// - /// ToStringExt for floats. See doc for doubles. - /// - public static string ToStringExt(this float value, string format) - { - return ToStringExt((double)value, format); - } - - /// - /// Find the SI prefix for a number - /// - /// The value to find the prefix for - /// The natural exponent, if your value was km rather than m, use 3 - public static SIPrefix GetSIPrefix(this double value, int exponent = 0) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (value == 0) - return SIPrefix.None; - - int exp = (int)Math.Floor(Math.Log10(Math.Abs(value))) + exponent; - - if (exp <= 3 && exp >= -1) - return SIPrefix.None; - if (exp < 0) - return (SIPrefix)((exp-2) / 3 * 3); - return (SIPrefix)(exp / 3 * 3); - } - - /// - /// Find the SI prefix for a number - /// - public static SIPrefix GetSIPrefix(this float value, int exponent = 0) - { - return GetSIPrefix((double)value, exponent); - } - - /// - /// The prefix string for a particular exponent. Note Micro will use the string "mic" rather than "µ", so is longer. - /// - public static string PrefixString(this SIPrefix pfx) - { - switch (pfx) - { - case SIPrefix.None: return ""; - case SIPrefix.Kilo: return "k"; - case SIPrefix.Mega: return "M"; - case SIPrefix.Giga: return "G"; - case SIPrefix.Tera: return "T"; - case SIPrefix.Peta: return "P"; - case SIPrefix.Exa: return "E"; - case SIPrefix.Zotta: return "Z"; - case SIPrefix.Yotta: return "Y"; - case SIPrefix.Milli: return "m"; - case SIPrefix.Micro: return "u"; - case SIPrefix.Nano: return "n"; - case SIPrefix.Pico: return "p"; - case SIPrefix.Femto: return "f"; - case SIPrefix.Atto: return "a"; - case SIPrefix.Zepto: return "z"; - case SIPrefix.Yocto: return "y"; - default: throw new ArgumentException("Illegal prefix", "pfx"); - } - } - - /// - /// Round a value with respect the the given SI prefix. - /// - /// eg: - /// SIPrefix.Mega.Round(34.456e6f, digits:1) -> 34.6e6f - /// - /// - /// The SI prefix - /// Value to round - /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) - /// Natural exponent of value, eg: use 3 if value is km rather than m - /// - public static float Round(this SIPrefix pfx, float value, int digits = 3, int exponent = 0) - { - float div = Mathf.Pow(10, (int)pfx - digits + exponent); - return Mathf.Round(value / div) * div; - } - - /// - /// Round a value with respect the the given SI prefix. - /// - /// eg: - /// SIPrefix.Mega.Round(34.456e6, digits:1) -> 34.6e6 - /// - /// - /// The SI prefix - /// Value to round - /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) - /// Natural exponent of value, eg: use 3 if value is km rather than m - /// - public static double Round(this SIPrefix pfx, double value, int digits = 3, int exponent = 0) - { - double div = Math.Pow(10, (int)pfx - digits + exponent); - return Math.Round(value / div) * div; - } - - /// - /// Get a formatter function. This is useful when you have a range of values you'd like formatted the same way. - /// - public static Func GetFormatter(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0) - { - int exp = (int)(Math.Floor(Math.Log10(Math.Abs(value)))) - (int)pfx + exponent; - double div = Math.Pow(10, (int)pfx - exponent); - - if (exp < 0) - return v => (v/div).ToString("F" + (sigFigs-1)); - if (exp >= sigFigs) - { - double mult = Math.Pow(10, exp - sigFigs + 1); - return v => (Math.Round(v / div / mult) * mult).ToString("F0"); - } - return v => (v/div).ToString("F" + (sigFigs - exp - 1)); - } - } - - /// - /// SI Prefix. If cast as an int, the base 10 log for the prefix will be produced. - /// - public enum SIPrefix - { - None = 0, - Kilo = 3, - Mega = 6, - Giga = 9, - Tera = 12, - Peta = 15, - Exa = 18, - Zotta = 21, - Yotta = 24, - Milli = -3, - Micro = -6, - Nano = -9, - Pico = -12, - Femto = -15, - Atto = -18, - Zepto = -21, - Yocto = -24 - } -} \ No newline at end of file diff --git a/Source/KSPAPIEL/Utils/SystemUtils.cs b/Source/KSPAPIEL/Utils/SystemUtils.cs deleted file mode 100644 index c7734751..00000000 --- a/Source/KSPAPIEL/Utils/SystemUtils.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reflection; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIEL -{ - public static class SystemUtils - { - - /// - /// Ensure the latest version of a particular type is used. This is useful when multiple versions of the same assembly are potentially loaded. - /// - /// This will return true for the latest version of the class (specified with ) - /// that appears the earliest in the list of loaded assemblies (as loaded from ). - /// - /// Target class to search for. Searched by the attribute. - /// Assembly name of the expected assembly. - /// If the class is not in an assembly named . - /// True if this class wins the election, false otherwise. - public static bool RunTypeElection(Type targetCls, String assemName) - { - if (targetCls.Assembly.GetName().Name != assemName) - throw new InvalidProgramException("Assembly: " + targetCls.Assembly.GetName().Name + " at location: " + targetCls.Assembly.Location + " is not in the expected assembly. Code has been copied and this will cause problems."); - - // If we are loaded from the first loaded assembly that has this class, then we are responsible to destroy - var candidates = (from ass in AssemblyLoader.loadedAssemblies - where ass.assembly.GetName().Name == assemName && - ass.assembly.GetType(targetCls.FullName, false) != null - orderby ass.assembly.GetName().Version descending, ass.path ascending - select ass).ToArray(); - var winner = candidates.First(); - - if (!ReferenceEquals(targetCls.Assembly, winner.assembly)) - return false; - - if (candidates.Length > 1) - { - string losers = string.Join("\n", (from t in candidates - where t != winner - select string.Format("Version: {0} Location: {1}", t.assembly.GetName().Version, t.path)).ToArray()); - - Debug.Log("[" + targetCls.Name + "] version " + winner.assembly.GetName().Version + " at " + winner.path + " won the election against\n" + losers); - } - else - Debug.Log("[" + targetCls.Name + "] Elected unopposed version= " + winner.assembly.GetName().Version + " at " + winner.path); - - return true; - } - - /// - /// Ensure the latest version of a particular type is used. This is useful when multiple versions of the same assembly are potentially loaded. - /// - /// This will the latest version of the class (specified with ) - /// that appears the earliest in the list of loaded assemblies (as loaded from ). - /// - /// Target class to search for. Searched by the attribute. - /// Assembly name of the expected assembly. - /// If the class is not in an assembly named . - /// True if this class wins the election, false otherwise. - public static Type TypeElectionWinner(Type targetCls, String assemName) - { - if (targetCls.Assembly.GetName().Name != assemName) - throw new InvalidProgramException("Assembly: " + targetCls.Assembly.GetName().Name + " at location: " + - targetCls.Assembly.Location + - " is not in the expected assembly. Code has been copied and this will cause problems."); - - // If we are loaded from the first loaded assembly that has this class, then we are responsible to destroy - var candidates = (from ass in AssemblyLoader.loadedAssemblies - where ass.assembly.GetName().Name == assemName - let t = ass.assembly.GetType(targetCls.FullName, false) - where t != null - orderby ass.assembly.GetName().Version descending, ass.path ascending - select t).ToArray(); - return candidates.First(); - } - - /// - /// Find a version-tagged class for an untagged class. - /// - /// The tagged class must be directly derived from the untagged class and in the same assembly and namespace. - /// - /// The untagged class for which the tagged class will be searched. The attribute. - /// The tagged class if found, otherwise the base class - public static Type VersionTaggedType(Type baseClass) - { - var ass = baseClass.Assembly; - Type tagged = ass.GetTypes().Where(t => t.BaseType == baseClass).Where(t => t.FullName.StartsWith(baseClass.FullName)).FirstOrDefault(); - if (tagged != null) { - Debug.Log(String.Format("[VersionTaggedType] found {0} for {1}", tagged.FullName, baseClass.FullName)); - return tagged; - } - return baseClass; - } - - public static LinkedListNode FindFirstNode(this LinkedList list, Predicate match) - { - for (var node = list.First; node != null; node = node.Next) - if(match(node.Value)) - return node; - return null; - } - - public static int RemoveAll(this LinkedList list, Predicate match) - { - int count = 0; - for (var node = list.First; node != null; ) - { - if (!match(node.Value)) - { - node = node.Next; - continue; - } - ++count; - var tmp = node; - node = node.Next; - list.Remove(tmp); - } - return count; - } - - // ReSharper disable once InconsistentNaming - public static bool TryGet(this KeyedCollection coll, K key, out T value) - { - try - { - value = coll[key]; - return true; - } - catch (KeyNotFoundException) - { - value = default(T); - return false; - } - } - - /// - /// Retrieve the most informative version string available in the given assembly. - /// - /// The assembly from which to retrieve the version string - /// The version string from AssemblyInformationalVersionAttribute if available, otherwise from AssemblyVersion. - public static string GetAssemblyVersionString (Assembly assembly) - { - string version = assembly.GetName().Version.ToString (); - - var cattrs = assembly.GetCustomAttributes(true); - foreach (var attr in cattrs) { - if (attr is AssemblyInformationalVersionAttribute) { - var ver = attr as AssemblyInformationalVersionAttribute; - version = ver.InformationalVersion; - break; - } - } - - return version; - } - - /// - /// Retrieve the best available title string available in the given assembly. - /// - /// The assembly from which to retrieve the title string - /// The title string from AssemblyTitle if available, otherwise the name of the dll. - public static string GetAssemblyTitle (Assembly assembly) - { - string title = assembly.GetName().Name; - - var cattrs = assembly.GetCustomAttributes(true); - foreach (var attr in cattrs) { - if (attr is AssemblyTitleAttribute) { - var ver = attr as AssemblyTitleAttribute; - title = ver.Title; - break; - } - } - - return title; - } - } - - // ReSharper disable InconsistentNaming - public class OrderedDictionary : KeyedCollection - { - private readonly Func GetKey; - - public OrderedDictionary(Func GetKey) - { - this.GetKey = GetKey; - } - - public OrderedDictionary(Func GetKey, IEqualityComparer comparer) - : base(comparer) - { - this.GetKey = GetKey; - } - - public OrderedDictionary(Func GetKey, IEqualityComparer comparer, int dictionaryCreationThreshold) - : base(comparer, dictionaryCreationThreshold) - { - this.GetKey = GetKey; - } - - protected override K GetKeyForItem(T item) - { - return GetKey(item); - } - - public bool TryGet(K key, out T value) - { - try - { - value = this[key]; - return true; - } - catch (KeyNotFoundException) - { - value = default(T); - return false; - } - } - } - // ReSharper restore InconsistentNaming -} diff --git a/Source/KSPAPIEL/tagged_classes.cs b/Source/KSPAPIEL/tagged_classes.cs deleted file mode 100644 index 04749715..00000000 --- a/Source/KSPAPIEL/tagged_classes.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; -namespace KSPAPIEL { - [KSPAddon(KSPAddon.Startup.Instantly, true)] - internal class UIPartActionsExtendedRegistration_1_7_4_0 : UIPartActionsExtendedRegistration { } - internal class UIPartActionResourceEditorImproved_1_7_4_0 : UIPartActionResourceEditorImproved { } - internal class UIPartActionLabelImproved_1_7_4_0 : UIPartActionLabelImproved { } - internal class UIPartActionFloatEdit_1_7_4_0 : UIPartActionFloatEdit { } - internal class UIPartActionChooseOption_1_7_4_0 : UIPartActionChooseOption { } - [KSPAddon(KSPAddon.Startup.Instantly, true)] - internal class CompatibilityChecker_1_7_4_0 : CompatibilityChecker { } -} diff --git a/Source/KSPAPIExt/Tweakables/UIPartActionFloatEdit.cs b/Source/KSPAPIExt/Tweakables/UIPartActionFloatEdit.cs deleted file mode 100644 index 6e09ee1c..00000000 --- a/Source/KSPAPIExt/Tweakables/UIPartActionFloatEdit.cs +++ /dev/null @@ -1,396 +0,0 @@ -using System; -using System.Globalization; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions -{ - - [UI_FloatEdit] - public class UIPartActionFloatEdit : UIPartActionFieldItem - { - public SpriteText fieldName; - public SpriteText fieldValue; - public UIButton incLargeDown; - public SpriteText incLargeDownLabel; - public UIButton incSmallDown; - public SpriteText incSmallDownLabel; - public UIButton incSmallUp; - public SpriteText incSmallUpLabel; - public UIButton incLargeUp; - public SpriteText incLargeUpLabel; - public UIProgressSlider slider; - - private float value; - private uint controlState; - - public static UIPartActionFloatEdit CreateTemplate() - { - // Create the control - GameObject editGo = new GameObject("UIPartActionFloatEdit", SystemUtils.VersionTaggedType(typeof(UIPartActionFloatEdit))); - UIPartActionFloatEdit edit = editGo.GetTaggedComponent(); - editGo.SetActive(false); - - // TODO: since I don'type have access to EZE GUI, I'm copying out bits from other existing GUIs - // if someone does have access, they could do this better although really it works pretty well. - UIPartActionButton evtp = UIPartActionController.Instance.eventItemPrefab; - GameObject srcTextGo = evtp.transform.Find("Text").gameObject; - GameObject srcBackgroundGo = evtp.transform.Find("Background").gameObject; - GameObject srcButtonGo = evtp.transform.Find("Btn").gameObject; - - UIPartActionFloatRange paFlt = (UIPartActionFloatRange)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange)); - GameObject srcSliderGo = paFlt.transform.Find("Slider").gameObject; - - - // Start building our control - GameObject backgroundGo = (GameObject)Instantiate(srcBackgroundGo); - backgroundGo.transform.parent = editGo.transform; - - GameObject sliderGo = (GameObject)Instantiate(srcSliderGo); - sliderGo.transform.parent = editGo.transform; - sliderGo.transform.localScale = new Vector3(0.65f, 1, 1); - edit.slider = sliderGo.GetComponent(); - edit.slider.ignoreDefault = true; - - - GameObject fieldNameGo = (GameObject)Instantiate(srcTextGo); - fieldNameGo.transform.parent = editGo.transform; - fieldNameGo.transform.localPosition = new Vector3(40, -8, 0); - edit.fieldName = fieldNameGo.GetComponent(); - - GameObject fieldValueGo = (GameObject)Instantiate(srcTextGo); - fieldValueGo.transform.parent = editGo.transform; - fieldValueGo.transform.localPosition = new Vector3(110, -8, 0); - edit.fieldValue = fieldValueGo.GetComponent(); - - - GameObject incLargeDownGo = (GameObject)Instantiate(srcButtonGo); - incLargeDownGo.transform.parent = edit.transform; - incLargeDownGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); - incLargeDownGo.transform.localPosition = new Vector3(11.5f, -9, 0); //>11 - edit.incLargeDown = incLargeDownGo.GetComponent(); - - GameObject incLargeDownLabelGo = (GameObject)Instantiate(srcTextGo); - incLargeDownLabelGo.transform.parent = editGo.transform; - incLargeDownLabelGo.transform.localPosition = new Vector3(5.5f, -7, 0); // <6 - edit.incLargeDownLabel = incLargeDownLabelGo.GetComponent(); - edit.incLargeDownLabel.Text = "<<"; - - - GameObject incSmallDownGo = (GameObject)Instantiate(srcButtonGo); - incSmallDownGo.transform.parent = edit.transform; - incSmallDownGo.transform.localScale = new Vector3(0.35f, 1.1f, 1f); - incSmallDownGo.transform.localPosition = new Vector3(29, -9, 0); // <31.5 - edit.incSmallDown = incSmallDownGo.GetComponent(); - - GameObject incSmallDownLabelGo = (GameObject)Instantiate(srcTextGo); - incSmallDownLabelGo.transform.parent = editGo.transform; - incSmallDownLabelGo.transform.localPosition = new Vector3(25.5f, -7, 0); //<28 - edit.incSmallDownLabel = incSmallDownLabelGo.GetComponent(); - edit.incSmallDownLabel.Text = "<"; - - GameObject incSmallUpGo = (GameObject)Instantiate(srcButtonGo); - incSmallUpGo.transform.parent = edit.transform; - incSmallUpGo.transform.localScale = new Vector3(0.35f, 1.1f, 1f); - incSmallUpGo.transform.localPosition = new Vector3(170, -9, 0); - edit.incSmallUp = incSmallUpGo.GetComponent(); - - GameObject incSmallUpLabelGo = (GameObject)Instantiate(srcTextGo); - incSmallUpLabelGo.transform.parent = editGo.transform; - incSmallUpLabelGo.transform.localPosition = new Vector3(167.5f, -7, 0); //<168 - edit.incSmallUpLabel = incSmallUpLabelGo.GetComponent(); - edit.incSmallUpLabel.Text = ">"; - - GameObject incLargeUpGo = (GameObject)Instantiate(srcButtonGo); - incLargeUpGo.transform.parent = edit.transform; - incLargeUpGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); - incLargeUpGo.transform.localPosition = new Vector3(187.5f, -9, 0); // >187 - edit.incLargeUp = incLargeUpGo.GetComponent(); - - GameObject incLargeUpLabelGo = (GameObject)Instantiate(srcTextGo); - incLargeUpLabelGo.transform.parent = editGo.transform; - incLargeUpLabelGo.transform.localPosition = new Vector3(181.5f, -7, 0); //<182 - edit.incLargeUpLabel = incLargeUpLabelGo.GetComponent(); - edit.incLargeUpLabel.Text = ">>"; - return edit; - } - - - protected UI_FloatEdit FieldInfo - { - get - { - return (UI_FloatEdit)control; - } - } - - // ReSharper disable ParameterHidesMember - public override void Setup(UIPartActionWindow window, Part part, PartModule partModule, UI_Scene scene, UI_Control control, BaseField field) - { - base.Setup(window, part, partModule, scene, control, field); - incLargeDown.SetValueChangedDelegate(obj => buttons_ValueChanged(false, true)); - incSmallDown.SetValueChangedDelegate(obj => buttons_ValueChanged(false, false)); - incSmallUp.SetValueChangedDelegate(obj => buttons_ValueChanged(true, false)); - incLargeUp.SetValueChangedDelegate(obj => buttons_ValueChanged(true, true)); - slider.SetValueChangedDelegate(slider_OnValueChanged); - - // so update runs. - value = GetFieldValue() + 0.1f; - UpdateFieldInfo(); - } - // ReSharper restore ParameterHidesMember - - private void buttons_ValueChanged(bool up, bool large) - { - float increment = (large ? FieldInfo.incrementLarge : FieldInfo.incrementSmall); - float excess = value % increment; - float newValue; - if (up) - { - if (increment - excess < FieldInfo.incrementSlide / 2) - newValue = value - excess + increment * 2; - else - newValue = value - excess + increment; - } - else - { - if (excess < FieldInfo.incrementSlide / 2) - newValue = value - excess - increment; - else - newValue = value - excess; - } - SetValueFromGUI(newValue); - } - - private void slider_OnValueChanged(IUIObject obj) - { - float valueLow, valueHi; - SliderRange(value, out valueLow, out valueHi); - - float newValue = Mathf.Lerp(valueLow, valueHi, slider.Value); - - // ReSharper disable CompareOfFloatsByEqualityOperator - if (FieldInfo.incrementLarge == 0 || valueHi == FieldInfo.maxValue) - { - if (newValue > valueHi) - newValue = valueHi; - } - else if (newValue > valueHi - FieldInfo.incrementSlide) - newValue = valueHi - FieldInfo.incrementSlide; - - SetValueFromGUI(newValue); - // ReSharper restore CompareOfFloatsByEqualityOperator - } - - private void SetValueFromGUI(float newValue) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (FieldInfo.incrementSlide != 0) - newValue = Mathf.Round(newValue / FieldInfo.incrementSlide) * FieldInfo.incrementSlide; - - if (newValue < FieldInfo.minValue) - newValue = FieldInfo.minValue; - else if (newValue > FieldInfo.maxValue) - newValue = FieldInfo.maxValue; - - UpdateValueDisplay(newValue); - - field.SetValue(newValue, field.host); - if (scene == UI_Scene.Editor) - SetSymCounterpartValue(newValue); - } - - private void SliderRange(float newValue, out float valueLow, out float valueHi) - { - // ReSharper disable CompareOfFloatsByEqualityOperator - if (FieldInfo.incrementLarge == 0) - { - valueLow = FieldInfo.minValue; - valueHi = FieldInfo.maxValue; - return; - } - - if (FieldInfo.incrementSmall == 0) - { - valueLow = Mathf.Floor((newValue + FieldInfo.incrementSlide / 2f) / FieldInfo.incrementLarge) * FieldInfo.incrementLarge; - valueHi = valueLow + FieldInfo.incrementLarge; - if (valueLow == FieldInfo.maxValue) - { - valueHi = valueLow; - valueLow -= FieldInfo.incrementLarge; - } - } - else - { - valueLow = Mathf.Floor((newValue + FieldInfo.incrementSlide / 2f) / FieldInfo.incrementSmall) * FieldInfo.incrementSmall; - valueHi = valueLow + FieldInfo.incrementSmall; - if (valueLow == FieldInfo.maxValue) - { - valueHi = valueLow; - valueLow -= FieldInfo.incrementSmall; - } - } - // ReSharper restore CompareOfFloatsByEqualityOperator - } - - private float GetFieldValue() - { - return isModule ? field.GetValue(partModule) : field.GetValue(part); - } - - public override void UpdateItem() - { - // update from fieldName. No listeners. - fieldName.Text = field.guiName; - - // Update the value. - float fValue = GetFieldValue(); - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (fValue != value) - UpdateValueDisplay(fValue); - - uint newHash = FieldInfo.GetHashedState(); - if (controlState != newHash) - { - UpdateFieldInfo(); - controlState = newHash; - } - } - - private void UpdateValueDisplay(float newValue) - { - this.value = newValue; - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (FieldInfo.incrementSlide != 0) - newValue = Mathf.Round(newValue / FieldInfo.incrementSlide) * FieldInfo.incrementSlide; - - float valueLow, valueHi; - SliderRange(newValue, out valueLow, out valueHi); - slider.Value = Mathf.InverseLerp(valueLow, valueHi, newValue); - - fieldValue.Text = newValue.ToStringExt(field.guiFormat) + field.guiUnits; - } - - private void UpdateFieldInfo() - { - // ReSharper disable CompareOfFloatsByEqualityOperator - if (FieldInfo.incrementLarge == 0.0) - { - incLargeDown.gameObject.SetActive(false); - incLargeDownLabel.gameObject.SetActive(false); - incLargeUp.gameObject.SetActive(false); - incLargeUpLabel.gameObject.SetActive(false); - - incSmallDown.gameObject.SetActive(false); - incSmallDownLabel.gameObject.SetActive(false); - incSmallUp.gameObject.SetActive(false); - incSmallUpLabel.gameObject.SetActive(false); - - slider.transform.localScale = Vector3.one; - fieldName.transform.localPosition = new Vector3(6, -8, 0); - } - else if (FieldInfo.incrementSmall == 0.0) - { - incLargeDown.gameObject.SetActive(true); - incLargeDownLabel.gameObject.SetActive(true); - incLargeUp.gameObject.SetActive(true); - incLargeUpLabel.gameObject.SetActive(true); - - incSmallDown.gameObject.SetActive(false); - incSmallDownLabel.gameObject.SetActive(false); - incSmallUp.gameObject.SetActive(false); - incSmallUpLabel.gameObject.SetActive(false); - - slider.transform.localScale = new Vector3(0.81f, 1, 1); - fieldName.transform.localPosition = new Vector3(24, -8, 0); //>23 - } - else - { - incLargeDown.gameObject.SetActive(true); - incLargeDownLabel.gameObject.SetActive(true); - incLargeUp.gameObject.SetActive(true); - incLargeUpLabel.gameObject.SetActive(true); - - incSmallDown.gameObject.SetActive(true); - incSmallDownLabel.gameObject.SetActive(true); - incSmallUp.gameObject.SetActive(true); - incSmallUpLabel.gameObject.SetActive(true); - - slider.transform.localScale = new Vector3(0.64f, 1, 1); - fieldName.transform.localPosition = new Vector3(40, -8, 0); - } - - if (FieldInfo.incrementSlide == 0) - slider.gameObject.SetActive(false); - else - slider.gameObject.SetActive(true); - // ReSharper restore CompareOfFloatsByEqualityOperator - } - } - - // ReSharper disable once InconsistentNaming - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] - public class UI_FloatEdit : UI_Control - { - private const string UIControlName = "UIPartActionFloatEdit"; - - public float minValue = float.NegativeInfinity; - public float maxValue = float.PositiveInfinity; - - public float incrementLarge = 0; - public float incrementSmall = 0; - public float incrementSlide = 0; - - public override void Load(ConfigNode node, object host) - { - base.Load(node, host); - if (!ParseFloat(out minValue, node, "minValue", UIControlName, null)) - { - minValue = float.NegativeInfinity; - } - if (!ParseFloat(out maxValue, node, "maxValue", UIControlName, null)) - { - maxValue = float.PositiveInfinity; - } - if (!ParseFloat(out incrementLarge, node, "incrementLarge", UIControlName, null)) - { - incrementLarge = 0.0f; - } - if (!ParseFloat(out incrementSmall, node, "incrementSmall", UIControlName, null)) - { - incrementSmall = 0.0f; - } - if (!ParseFloat(out incrementSlide, node, "incrementSlide", UIControlName, null)) - { - incrementSlide = 0.0f; - } - } - - public override void Save(ConfigNode node, object host) - { - base.Save(node, host); - if (!float.IsNegativeInfinity(minValue)) - node.AddValue("minValue", minValue.ToString(CultureInfo.InvariantCulture)); - if (!float.IsPositiveInfinity(maxValue)) - node.AddValue("maxValue", maxValue.ToString(CultureInfo.InvariantCulture)); - // ReSharper disable CompareOfFloatsByEqualityOperator - if (incrementLarge != 0.0f) - node.AddValue("incrementLarge", incrementLarge.ToString(CultureInfo.InvariantCulture)); - if (incrementSmall != 0.0f) - node.AddValue("incrementSmall", incrementSmall.ToString(CultureInfo.InvariantCulture)); - if (incrementSlide != 0.0f) - node.AddValue("incrementSlide", incrementSlide.ToString(CultureInfo.InvariantCulture)); - // ReSharper restore CompareOfFloatsByEqualityOperator - } - - internal unsafe uint GetHashedState() - { - // ReSharper disable LocalVariableHidesMember - fixed (float* minValue = &this.minValue, maxValue = &this.maxValue, incrementLarge = &this.incrementLarge, incrementSmall = &this.incrementSmall, incrementSlide = &this.incrementSlide) - { - return *((uint*)minValue) ^ *((uint*)maxValue) ^ *((uint*)incrementLarge) ^ *((uint*)incrementSmall) ^ *((uint*)incrementSlide); - } - // ReSharper restore LocalVariableHidesMember - } - } -} diff --git a/Source/KSPAPIExt/Tweakables/UIPartActionScaleEdit.cs b/Source/KSPAPIExt/Tweakables/UIPartActionScaleEdit.cs deleted file mode 100644 index b09c0252..00000000 --- a/Source/KSPAPIExt/Tweakables/UIPartActionScaleEdit.cs +++ /dev/null @@ -1,332 +0,0 @@ -using System; -using System.Globalization; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions -{ - - [UI_ScaleEdit] - public class UIPartActionScaleEdit : UIPartActionFieldItem - { - public SpriteText fieldName; - public SpriteText fieldValue; - public UIButton incLargeDown; - public SpriteText incLargeDownLabel; - public UIButton incLargeUp; - public SpriteText incLargeUpLabel; - public UIProgressSlider slider; - - private float value; - public int intervalNo = 0; - - private uint controlState; - - public static UIPartActionScaleEdit CreateTemplate() - { - // Create the control - GameObject editGo = new GameObject("UIPartActionScaleEdit", SystemUtils.VersionTaggedType(typeof(UIPartActionScaleEdit))); - UIPartActionScaleEdit edit = editGo.GetTaggedComponent(); - editGo.SetActive(false); - - // TODO: since I don'type have access to EZE GUI, I'm copying out bits from other existing GUIs - // if someone does have access, they could do this better although really it works pretty well. - UIPartActionButton evtp = UIPartActionController.Instance.eventItemPrefab; - GameObject srcTextGo = evtp.transform.Find("Text").gameObject; - GameObject srcBackgroundGo = evtp.transform.Find("Background").gameObject; - GameObject srcButtonGo = evtp.transform.Find("Btn").gameObject; - - UIPartActionFloatRange paFlt = (UIPartActionFloatRange)UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange)); - GameObject srcSliderGo = paFlt.transform.Find("Slider").gameObject; - - - // Start building our control - GameObject backgroundGo = (GameObject)Instantiate(srcBackgroundGo); - backgroundGo.transform.parent = editGo.transform; - - GameObject sliderGo = (GameObject)Instantiate(srcSliderGo); - sliderGo.transform.parent = editGo.transform; - sliderGo.transform.localScale = new Vector3(0.65f, 1, 1); - edit.slider = sliderGo.GetComponent(); - edit.slider.ignoreDefault = true; - - - GameObject fieldNameGo = (GameObject)Instantiate(srcTextGo); - fieldNameGo.transform.parent = editGo.transform; - fieldNameGo.transform.localPosition = new Vector3(40, -8, 0); - edit.fieldName = fieldNameGo.GetComponent(); - - GameObject fieldValueGo = (GameObject)Instantiate(srcTextGo); - fieldValueGo.transform.parent = editGo.transform; - fieldValueGo.transform.localPosition = new Vector3(110, -8, 0); - edit.fieldValue = fieldValueGo.GetComponent(); - - - GameObject incLargeDownGo = (GameObject)Instantiate(srcButtonGo); - incLargeDownGo.transform.parent = edit.transform; - incLargeDownGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); - incLargeDownGo.transform.localPosition = new Vector3(11.5f, -9, 0); //>11 - edit.incLargeDown = incLargeDownGo.GetComponent(); - - GameObject incLargeDownLabelGo = (GameObject)Instantiate(srcTextGo); - incLargeDownLabelGo.transform.parent = editGo.transform; - incLargeDownLabelGo.transform.localPosition = new Vector3(5.5f, -7, 0); // <6 - edit.incLargeDownLabel = incLargeDownLabelGo.GetComponent(); - edit.incLargeDownLabel.Text = "<<"; - - GameObject incLargeUpGo = (GameObject)Instantiate(srcButtonGo); - incLargeUpGo.transform.parent = edit.transform; - incLargeUpGo.transform.localScale = new Vector3(0.45f, 1.1f, 1f); - incLargeUpGo.transform.localPosition = new Vector3(187.5f, -9, 0); // >187 - edit.incLargeUp = incLargeUpGo.GetComponent(); - - GameObject incLargeUpLabelGo = (GameObject)Instantiate(srcTextGo); - incLargeUpLabelGo.transform.parent = editGo.transform; - incLargeUpLabelGo.transform.localPosition = new Vector3(181.5f, -7, 0); //<182 - edit.incLargeUpLabel = incLargeUpLabelGo.GetComponent(); - edit.incLargeUpLabel.Text = ">>"; - return edit; - } - - - protected UI_ScaleEdit FieldInfo - { - get - { - return (UI_ScaleEdit)control; - } - } - - // ReSharper disable ParameterHidesMember - public override void Setup(UIPartActionWindow window, Part part, PartModule partModule, UI_Scene scene, UI_Control control, BaseField field) - { - base.Setup(window, part, partModule, scene, control, field); - incLargeDown.SetValueChangedDelegate(obj => buttons_ValueChanged(false)); - incLargeUp.SetValueChangedDelegate(obj => buttons_ValueChanged(true)); - slider.SetValueChangedDelegate(slider_OnValueChanged); - - // so update runs. - value = GetFieldValue() + 0.1f; - UpdateFieldInfo(); - } - // ReSharper restore ParameterHidesMember - - private void buttons_ValueChanged(bool up) - { - float newValue = this.value; - if (up) - { - if (intervalNo < FieldInfo.intervals.Length - 2) - { - if (newValue == FieldInfo.intervals[intervalNo+1]) - newValue = FieldInfo.intervals[intervalNo+2]; - intervalNo++; - } - else - newValue = FieldInfo.intervals [intervalNo+1]; - } - else - { - if (intervalNo > 0) - { - if (newValue == FieldInfo.intervals[intervalNo]) - newValue = FieldInfo.intervals[intervalNo-1]; - intervalNo--; - } - else - newValue = FieldInfo.intervals [0]; - } - RestrictToInterval (newValue); - } - - private void slider_OnValueChanged(IUIObject obj) - { - float valueLow = FieldInfo.intervals [intervalNo]; - float valueHi = FieldInfo.intervals [intervalNo + 1]; - float newValue = Mathf.Lerp(valueLow, valueHi, slider.Value); - - // ReSharper disable once CompareOfFloatsByEqualityOperator - float inc = GetIncrementSlide (); - if (inc != 0) - newValue = valueLow + Mathf.Round((newValue-valueLow) / inc) * inc; - - SetValueFromGUI(newValue); - } - - private void OnValueChanged(float newValue) - { - //update intervalNo - intervalNo = 0; - - for( int i=0; i= FieldInfo.intervals[i]) - intervalNo = i; - - UpdateValueDisplay (newValue); - } - - private void RestrictToInterval(float newValue) - { - newValue = Math.Max(newValue, FieldInfo.intervals [intervalNo]); - newValue = Math.Min(newValue, FieldInfo.intervals [intervalNo + 1]); - - SetValueFromGUI(newValue); - } - - private void SetValueFromGUI(float newValue) - { - UpdateValueDisplay(newValue); - - field.SetValue(newValue, field.host); - if (scene == UI_Scene.Editor) - SetSymCounterpartValue(newValue); - } - - private float GetFieldValue() - { - return isModule ? field.GetValue(partModule) : field.GetValue(part); - } - - private float GetIncrementSlide() - { - if (FieldInfo.incrementSlide.Length > 1) - return FieldInfo.incrementSlide [intervalNo]; - else if (FieldInfo.incrementSlide.Length == 1) - return FieldInfo.incrementSlide [0]; - else - return 0; - } - - public override void UpdateItem() - { - // update from fieldName. No listeners. - fieldName.Text = field.guiName; - - // Update the value. - float fValue = GetFieldValue(); - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (fValue != value) - OnValueChanged(fValue); - - uint newHash = FieldInfo.GetHashedState(); - if (controlState != newHash) - { - UpdateFieldInfo(); - controlState = newHash; - } - } - - private void UpdateValueDisplay(float newValue) - { - this.value = newValue; - // ReSharper disable once CompareOfFloatsByEqualityOperator - float inc = GetIncrementSlide(); - if (inc != 0) - { - float valueLow = FieldInfo.intervals [intervalNo]; - float valueHi = FieldInfo.intervals [intervalNo + 1]; - newValue = valueLow + Mathf.Round((newValue - valueLow) / inc) * inc; - slider.gameObject.SetActive(true); - slider.Value = Mathf.InverseLerp (valueLow, valueHi, newValue); - } - else - slider.gameObject.SetActive(false); - - fieldValue.Text = newValue.ToStringExt(field.guiFormat) + field.guiUnits; - } - - private void UpdateFieldInfo() - { - if (CheckConsistency ()) - { - incLargeDown.gameObject.SetActive (true); - incLargeDownLabel.gameObject.SetActive (true); - incLargeUp.gameObject.SetActive (true); - incLargeUpLabel.gameObject.SetActive (true); - - slider.transform.localScale = new Vector3(0.81f, 1, 1); - fieldName.transform.localPosition = new Vector3(24, -8, 0); - - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (GetIncrementSlide() == 0) - slider.gameObject.SetActive(false); - else - slider.gameObject.SetActive(true); - } - else - { - incLargeDown.gameObject.SetActive (false); - incLargeDownLabel.gameObject.SetActive (false); - incLargeUp.gameObject.SetActive (false); - incLargeUpLabel.gameObject.SetActive (false); - - slider.gameObject.SetActive(false); - } - } - - public bool CheckConsistency() - { - if (FieldInfo.intervals.Length < 2) - return false; - - if ((FieldInfo.incrementSlide.Length > 1) && - (FieldInfo.incrementSlide.Length < FieldInfo.intervals.Length - 1)) - { - Debug.LogWarning("[KAE Warning] UI_ScaleEdit: not enough incrementSlide values. Using only the first." + Environment.NewLine + StackTraceUtility.ExtractStackTrace()); - float first = FieldInfo.incrementSlide[0]; - FieldInfo.incrementSlide = new float[1]; - FieldInfo.incrementSlide [0] = first; - return true; - } - - for (int i = 0; i < FieldInfo.intervals.Length-2; i++) - { - if (FieldInfo.intervals [i] == FieldInfo.intervals [i + 1]) - { - Debug.LogWarning("[KAE Warning] UI_ScaleEdit: duplicate value in intervals list" + Environment.NewLine + StackTraceUtility.ExtractStackTrace()); - return false; - } - else if (FieldInfo.intervals [i] > FieldInfo.intervals [i + 1]) - { - Debug.LogWarning("[KAE Warning] UI_ScaleEdit: intervals list not sorted" + Environment.NewLine + StackTraceUtility.ExtractStackTrace()); - return false; - } - } - return true; - } - } - - // ReSharper disable once InconsistentNaming - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)] - public class UI_ScaleEdit : UI_Control - { - private const string UIControlName = "UIPartActionScaleEdit"; - - public float[] intervals = { 1, 2, 4 }; - public float[] incrementSlide = {0.02f, 0.04f }; - - public float MinValue() - { - return intervals [0]; - } - public float MaxValue() - { - return intervals [intervals.Length-1]; - } - - public override void Load(ConfigNode node, object host) - { - base.Load(node, host); - } - - public override void Save(ConfigNode node, object host) - { - base.Save(node, host); - } - - internal uint GetHashedState() - { - return ((uint)intervals.GetHashCode()) ^ ((uint)incrementSlide.GetHashCode()); - } - } -} diff --git a/Source/KSPAPIExt/Tweakables/UIPartActionsExtended.cs b/Source/KSPAPIExt/Tweakables/UIPartActionsExtended.cs deleted file mode 100644 index c036af4b..00000000 --- a/Source/KSPAPIExt/Tweakables/UIPartActionsExtended.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using UnityEngine; -using KSPAPIExtensions.PartMessage; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions -{ - internal class UIPartActionsExtendedRegistration : MonoBehaviour - { - private static bool loaded; - private static bool isLatestVersion; - private bool isRunning; - - public void Start() - { - if (loaded) - { - // prevent multiple copies of same object - Destroy(gameObject); - return; - } - loaded = true; - - DontDestroyOnLoad(gameObject); - - isLatestVersion = SystemUtils.RunTypeElection(typeof(UIPartActionsExtendedRegistration), "KSPAPIExtensions"); - } - - public void OnLevelWasLoaded(int level) - { - if(isRunning) - StopCoroutine("Register"); - if (!HighLogic.LoadedSceneIsEditor && !HighLogic.LoadedSceneIsFlight) - return; - isRunning = true; - StartCoroutine("Register"); - } - - internal IEnumerator Register() - { - UIPartActionController controller; - while((controller = UIPartActionController.Instance) == null) - yield return false; - - FieldInfo typesField = (from fld in controller.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance) - where fld.FieldType == typeof(List) - select fld).First(); - List fieldPrefabTypes; - while((fieldPrefabTypes = (List)typesField.GetValue(controller)) == null - || fieldPrefabTypes.Count == 0 - || !UIPartActionController.Instance.fieldPrefabs.Find(cls => cls.GetType() == typeof(UIPartActionFloatRange))) - yield return false; - - Debug.Log("[KAE] Registering field prefabs for version " + Assembly.GetExecutingAssembly().GetName().Version + (isLatestVersion?" (latest)":"")); - - // Register prefabs. This needs to be done for every version of the assembly. (the types might be called the same, but they aren't the same) - controller.fieldPrefabs.Add(UIPartActionFloatEdit.CreateTemplate()); - fieldPrefabTypes.Add(typeof(UI_FloatEdit)); - - controller.fieldPrefabs.Add(UIPartActionScaleEdit.CreateTemplate()); - fieldPrefabTypes.Add(typeof(UI_ScaleEdit)); - - controller.fieldPrefabs.Add(UIPartActionChooseOption.CreateTemplate()); - fieldPrefabTypes.Add(typeof(UI_ChooseOption)); - - // Register the label and resource editor fields. This should only be done by the most recent version. - if (isLatestVersion && GameSceneFilter.AnyEditor.IsLoaded()) - { - int idx = controller.fieldPrefabs.FindIndex(item => item.GetType() == typeof (UIPartActionLabel)); - controller.fieldPrefabs[idx] = UIPartActionLabelImproved.CreateTemplate((UIPartActionLabel)controller.fieldPrefabs[idx]); - controller.resourceItemEditorPrefab = UIPartActionResourceEditorImproved.CreateTemplate(controller.resourceItemEditorPrefab); - } - isRunning = false; - } - } - - - internal class UIPartActionResourceEditorImproved : UIPartActionResourceEditor - { - // ReSharper disable ParameterHidesMember - public override void Setup(UIPartActionWindow window, Part part, UI_Scene scene, UI_Control control, PartResource resource) - { - double amount = resource.amount; - base.Setup(window, part, scene, control, resource); - this.resource.amount = amount; - - slider.SetValueChangedDelegate(OnSliderChanged); - } - // ReSharper restore ParameterHidesMember - - private float oldSliderValue; - - public override void UpdateItem() - { - base.UpdateItem(); - - SIPrefix prefix = (resource.maxAmount).GetSIPrefix(); - // ReSharper disable once InconsistentNaming - Func Formatter = prefix.GetFormatter(resource.maxAmount, sigFigs: 4); - - resourceMax.Text = Formatter(resource.maxAmount) + " " + prefix.PrefixString(); - resourceAmnt.Text = Formatter(resource.amount); - - oldSliderValue = slider.Value = (float)(resource.amount / resource.maxAmount); - } - - private void OnSliderChanged(IUIObject obj) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (oldSliderValue == slider.Value) - return; - oldSliderValue = slider.Value; - - SIPrefix prefix = resource.maxAmount.GetSIPrefix(); - resource.amount = prefix.Round(slider.Value * resource.maxAmount, digits:4); - PartMessageService.Send(this, part, resource, resource.amount); - if (scene == UI_Scene.Editor) - SetSymCounterpartsAmount(resource.amount); - resourceAmnt.Text = resource.amount.ToString("F1"); - GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); - } - - protected new void SetSymCounterpartsAmount(double amount) - { - if (part == null) - return; - - foreach (Part sym in part.symmetryCounterparts) - { - if (sym == part) - continue; - PartResource symResource = sym.Resources[resource.info.name]; - symResource.amount = amount; - PartMessageService.Send(this, sym, symResource, symResource.amount); - } - } - - internal static UIPartActionResourceEditorImproved CreateTemplate(UIPartActionResourceEditor oldEditor) - { - GameObject editGo = (GameObject)Instantiate(oldEditor.gameObject); - Destroy(editGo.GetComponent()); - UIPartActionResourceEditorImproved edit = editGo.AddTaggedComponent(); - editGo.SetActive(false); - edit.transform.parent = oldEditor.transform.parent; - edit.transform.localPosition = oldEditor.transform.localPosition; - - // Find all the bits. - edit.slider = editGo.transform.Find("Slider").GetComponent(); - edit.resourceAmnt = editGo.transform.Find("amnt").GetComponent(); - edit.resourceName = editGo.transform.Find("name").GetComponent(); - edit.resourceMax = editGo.transform.Find("total").GetComponent(); - edit.flowBtn = editGo.transform.Find("StateBtn").GetComponent(); - - return edit; - } - } - - - internal class UIPartActionLabelImproved : UIPartActionLabel - { - private SpriteText label; - - public void Awake() - { - label = gameObject.GetComponentInChildren(); - } - - public override void UpdateItem() - { - object target = isModule ? (object)partModule : part; - - Type fieldType = field.FieldInfo.FieldType; - if (fieldType == typeof(double)) - { - double value = (double)field.FieldInfo.GetValue(target); - label.Text = (string.IsNullOrEmpty(field.guiName) ? field.name : field.guiName) + " " + - (string.IsNullOrEmpty(field.guiFormat) ? value.ToString(CultureInfo.CurrentUICulture) : value.ToStringExt(field.guiFormat)) - + field.guiUnits; - } - if (fieldType == typeof(float)) - { - float value = (float)field.FieldInfo.GetValue(target); - label.Text = (string.IsNullOrEmpty(field.guiName) ? field.name : field.guiName) + " " + - (string.IsNullOrEmpty(field.guiFormat) ? value.ToString(CultureInfo.CurrentUICulture) : value.ToStringExt(field.guiFormat)) - + field.guiUnits; - } - else - label.Text = field.GuiString(target); - } - - internal static UIPartActionLabelImproved CreateTemplate(UIPartActionLabel oldLabel) - { - GameObject labelGo = (GameObject)Instantiate(oldLabel.gameObject); - Destroy(labelGo.GetComponent()); - UIPartActionLabelImproved label = labelGo.AddTaggedComponent(); - labelGo.SetActive(false); - label.transform.parent = oldLabel.transform.parent; - label.transform.localPosition = oldLabel.transform.localPosition; - - return label; - } - } - -} diff --git a/Source/KSPAPIExt/Utils/MathUtils.cs b/Source/KSPAPIExt/Utils/MathUtils.cs deleted file mode 100644 index 3898f7d7..00000000 --- a/Source/KSPAPIExt/Utils/MathUtils.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System; -using UnityEngine; - -// ReSharper disable once CheckNamespace -namespace KSPAPIExtensions -{ - public static class MathUtils - { - - /// - /// Clamp a value between the max and min values. If < min it will be changed to min, if > max it will be changed to max. - /// - /// Value to clamp - /// Minimum - /// Maximum - /// true if the value has been clamped. - public static bool TestClamp(ref float value, float min, float max) - { - if (value < min) - { - value = min; - return true; - } - if (value > max) - { - value = max; - return true; - } - return false; - } - - /// - /// Round value to the nearest. - /// - /// Value to round - /// Precision, eg 0.1, 0.02, 0.005 - /// The rounded value - public static float RoundTo(float value, float precision) - { - return Mathf.Round(value / precision) * precision; - } - - /// - /// Format a numeric value using SI prefexes. - /// - /// eg: 13401 -> 13.4 k - /// - /// - /// value to format - /// unit string - /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. - /// number of signifigant figures to display - /// - public static string ToStringSI(this double value, int sigFigs = 3, int exponent = 0, string unit = null) - { - SIPrefix prefix = value.GetSIPrefix(exponent); - return prefix.FormatSI(value, sigFigs, exponent, unit); - } - - /// - /// Format a numeric value using SI prefexes. - /// - /// eg: 13401 -> 13.4 k - /// - /// - /// value to format - /// unit string - /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. - /// number of signifigant figures to display - /// - public static string ToStringSI(this float value, int sigFigs = 3, int exponent = 0, string unit = null) - { - SIPrefix prefix = value.GetSIPrefix(exponent); - return prefix.FormatSI(value, sigFigs, exponent, unit); - } - - internal static string FormatSI(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0, string unit = null) - { - return string.Format("{0}{1}{2}", pfx.GetFormatter(value, sigFigs, exponent)(value), pfx.PrefixString(), unit); - } - - /// - /// This extension of the standard string format method has available an extra standard format. - /// - /// Examples: - /// - /// 12.ToStringExt("S") -> "12" - /// 12.ToStringExt("S3") -> "12.0" - /// 120.ToStringExt("S3") -> "120" - /// 1254.ToStringExt("S3") -> "1250" (4 digit numbers do not use k as a special case) - /// 12540.ToStringExt("S3") -> "1.25k" (using SI prefixes) - /// 12540.ToStringExt("S4") -> "1.254k" (more significant figures) - /// (1.254).ToStringExt("S4+3") -> "1.254k" (+3 means the 'natural prefix' is k) - /// (1.254).ToStringExt("S4-3") -> "1.254m" (-3 means the 'natural prefix' is m) - /// - /// - public static string ToStringExt(this double value, string format) - { - if (format[0] == 'S' || format[0] == 's') - { - if (format.Length == 1) - return ToStringSI(value, 0); - int pmi = format.IndexOf('+'); - int sigFigs; - if (pmi < 0) - { - pmi = format.IndexOf('-'); - if (pmi < 0) - { - sigFigs = int.Parse(format.Substring(1)); - return ToStringSI(value, sigFigs); - } - } - sigFigs = int.Parse(format.Substring(1, pmi - 1)); - int exponent = int.Parse(format.Substring(pmi)); - return ToStringSI(value, sigFigs, exponent); - } - return value.ToString(format); - } - - /// - /// ToStringExt for floats. See doc for doubles. - /// - public static string ToStringExt(this float value, string format) - { - return ToStringExt((double)value, format); - } - - /// - /// Parse a string in SI format - with SI unit prefix to a double. - /// Note - units must not be present. - /// - public static bool TryParseExt(string str, out double value) - { - str = str.Trim(); - if(str.Length == 0) { - value = 0; - return false; - } - - char last = str[str.Length - 1]; - - if (char.IsDigit(last)) - return double.TryParse(str, out value); - - double exponent; - int newLen = str.Length - 1; - switch (last) - { - case 'k': exponent = 1e3; break; - case 'M': exponent = 1e6; break; - case 'G': exponent = 1e9; break; - case 'T': exponent = 1e12; break; - case 'P': exponent = 1e15; break; - case 'E': exponent = 1e18; break; - case 'Z': exponent = 1e21; break; - case 'Y': exponent = 1e24; break; - case 'm': - exponent = 1e-3; break; - case '\x3bc': - case '\xb5': - case 'u': - exponent = 1e-6; break; - case 'c': - if (!str.EndsWith("mic")) - { - value = 0; - return false; - } - newLen = str.Length - 3; - exponent = 1e-6; - break; - case 'n': exponent = 1e-9; break; - case 'p': exponent = 1e-12; break; - case 'f': exponent = 1e-15; break; - case 'a': exponent = 1e-18; break; - case 'z': exponent = 1e-21; break; - case 'y': exponent = 1e-24; break; - default: - value = 0; - return false; - } - - double parsed; - if (!double.TryParse(str.Substring(0, newLen), out parsed)) - { - value = 0; - return false; - } - - value = parsed * exponent; - return true; - } - - /// - /// Parse a string in SI format - with SI unit prefix to a float. - /// Note - units must not be present. - /// - public static bool TryParseExt(string str, out float value) - { - double dVal; - if (TryParseExt(str, out dVal)) - { - value = (float)dVal; - return true; - } - value = 0; - return false; - } - - /// - /// Round a number to a set number of significant figures. - /// - /// number to round - /// number of significant figures, defaults to 3 - /// - public static float RoundSigFigs(this float d, int sigFigs = 3) - { - - int exponent = (int)Math.Floor(Math.Log10(Math.Abs(d))) - sigFigs; - float div = Mathf.Pow(10, exponent); - return Mathf.Round(d / div) * div; - } - - /// - /// Round a number to a set number of significant figures. - /// - /// number to round - /// number of significant figures, defaults to 3 - /// - public static double RoundSigFigs(this double value, int sigFigs = 3) - { - int exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))) - sigFigs; - double div = Mathf.Pow(10, exponent); - return Math.Round(value / div) * div; - } - - /// - /// Find the SI prefix for a number - /// - /// The value to find the prefix for - /// The natural exponent, if your value was km rather than m, use 3 - public static SIPrefix GetSIPrefix(this double value, int exponent = 0) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (value == 0) - return SIPrefix.None; - - int exp = (int)Math.Floor(Math.Log10(Math.Abs(value))) + exponent; - - if (exp <= 3 && exp >= -1) - return SIPrefix.None; - if (exp < 0) - return (SIPrefix)((exp-2) / 3 * 3); - return (SIPrefix)(exp / 3 * 3); - } - - /// - /// Find the SI prefix for a number - /// - public static SIPrefix GetSIPrefix(this float value, int exponent = 0) - { - return GetSIPrefix((double)value, exponent); - } - - /// - /// The prefix string for a particular exponent. Note Micro will use the string "mic" rather than "µ", so is longer. - /// - public static string PrefixString(this SIPrefix pfx) - { - switch (pfx) - { - case SIPrefix.None: return ""; - case SIPrefix.Kilo: return "k"; - case SIPrefix.Mega: return "M"; - case SIPrefix.Giga: return "G"; - case SIPrefix.Tera: return "T"; - case SIPrefix.Peta: return "P"; - case SIPrefix.Exa: return "E"; - case SIPrefix.Zotta: return "Z"; - case SIPrefix.Yotta: return "Y"; - case SIPrefix.Milli: return "m"; - case SIPrefix.Micro: return "u"; - case SIPrefix.Nano: return "n"; - case SIPrefix.Pico: return "p"; - case SIPrefix.Femto: return "f"; - case SIPrefix.Atto: return "a"; - case SIPrefix.Zepto: return "z"; - case SIPrefix.Yocto: return "y"; - default: throw new ArgumentException("Illegal prefix", "pfx"); - } - } - - /// - /// Round a value with respect the the given SI prefix. - /// - /// eg: - /// SIPrefix.Mega.Round(34.456e6f, digits:1) -> 34.6e6f - /// - /// - /// The SI prefix - /// Value to round - /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) - /// Natural exponent of value, eg: use 3 if value is km rather than m - /// - public static float Round(this SIPrefix pfx, float value, int digits = 3, int exponent = 0) - { - float div = Mathf.Pow(10, (int)pfx - digits + exponent); - return Mathf.Round(value / div) * div; - } - - /// - /// Round a value with respect the the given SI prefix. - /// - /// eg: - /// SIPrefix.Mega.Round(34.456e6, digits:1) -> 34.6e6 - /// - /// - /// The SI prefix - /// Value to round - /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) - /// Natural exponent of value, eg: use 3 if value is km rather than m - /// - public static double Round(this SIPrefix pfx, double value, int digits = 3, int exponent = 0) - { - double div = Math.Pow(10, (int)pfx - digits + exponent); - return Math.Round(value / div) * div; - } - - /// - /// Get a formatter function. This is useful when you have a range of values you'd like formatted the same way. - /// - public static Func GetFormatter(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0) - { - int exp = (int)(Math.Floor(Math.Log10(Math.Abs(value)))) - (int)pfx + exponent; - double div = Math.Pow(10, (int)pfx - exponent); - - if (exp < 0) - return v => (v/div).ToString("F" + (sigFigs-1)); - if (exp >= sigFigs) - { - double mult = Math.Pow(10, exp - sigFigs + 1); - return v => (Math.Round(v / div / mult) * mult).ToString("F0"); - } - return v => (v/div).ToString("F" + (sigFigs - exp - 1)); - } - - - /// - /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 - /// - public static string FormatMass(float mass, int sigFigs = 4, int exponent = 0) - { - return mass < 1.0f ? - mass.ToStringSI(sigFigs, exponent + 6, "g") : - mass.ToStringSI(sigFigs, exponent, "t"); - } - - /// - /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 - /// - public static string FormatMass(double mass, int sigFigs = 4, int exponent = 0) - { - return mass < 1.0f ? - mass.ToStringSI(sigFigs, exponent+6, "g") : - mass.ToStringSI(sigFigs, exponent, "t"); - } - } - - /// - /// SI Prefix. If cast as an int, the base 10 log for the prefix will be produced. - /// - public enum SIPrefix - { - None = 0, - Kilo = 3, - Mega = 6, - Giga = 9, - Tera = 12, - Peta = 15, - Exa = 18, - Zotta = 21, - Yotta = 24, - Milli = -3, - Micro = -6, - Nano = -9, - Pico = -12, - Femto = -15, - Atto = -18, - Zepto = -21, - Yocto = -24 - } -} \ No newline at end of file diff --git a/Source/KSPAPIExt/tagged_classes.cs b/Source/KSPAPIExt/tagged_classes.cs deleted file mode 100644 index 04749715..00000000 --- a/Source/KSPAPIExt/tagged_classes.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; -namespace KSPAPIEL { - [KSPAddon(KSPAddon.Startup.Instantly, true)] - internal class UIPartActionsExtendedRegistration_1_7_4_0 : UIPartActionsExtendedRegistration { } - internal class UIPartActionResourceEditorImproved_1_7_4_0 : UIPartActionResourceEditorImproved { } - internal class UIPartActionLabelImproved_1_7_4_0 : UIPartActionLabelImproved { } - internal class UIPartActionFloatEdit_1_7_4_0 : UIPartActionFloatEdit { } - internal class UIPartActionChooseOption_1_7_4_0 : UIPartActionChooseOption { } - [KSPAddon(KSPAddon.Startup.Instantly, true)] - internal class CompatibilityChecker_1_7_4_0 : CompatibilityChecker { } -} diff --git a/Source/KSPAPIEL.sln b/Source/KSPAPIExtensions.sln similarity index 56% rename from Source/KSPAPIEL.sln rename to Source/KSPAPIExtensions.sln index 300c066a..2e02eebe 100644 --- a/Source/KSPAPIEL.sln +++ b/Source/KSPAPIExtensions.sln @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 VisualStudioVersion = 12.0.21005.1 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KSPAPIEL", "KSPAPIEL.csproj", "{4282B0DA-C314-4524-9C32-EDAB2C9666A9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KSPAPIExtensions", "KSPAPIExtensions\KSPAPIExtensions.csproj", "{4282B0DA-C314-4524-9C32-EDAB2C9666A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KSPAPIExtras", "KSPAPIExtras\KSPAPIExtras.csproj", "{7C69B948-FF19-11E4-B985-1E3E84ADF75F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {4282B0DA-C314-4524-9C32-EDAB2C9666A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {4282B0DA-C314-4524-9C32-EDAB2C9666A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {4282B0DA-C314-4524-9C32-EDAB2C9666A9}.Release|Any CPU.Build.0 = Release|Any CPU + {7C69B948-FF19-11E4-B985-1E3E84ADF75F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C69B948-FF19-11E4-B985-1E3E84ADF75F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C69B948-FF19-11E4-B985-1E3E84ADF75F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C69B948-FF19-11E4-B985-1E3E84ADF75F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/KSPAPIExt/CommonAPI/GameObjectExt.cs b/Source/KSPAPIExtensions/CommonAPI/GameObjectExt.cs similarity index 63% rename from Source/KSPAPIExt/CommonAPI/GameObjectExt.cs rename to Source/KSPAPIExtensions/CommonAPI/GameObjectExt.cs index d7422f8a..c7fdba57 100644 --- a/Source/KSPAPIExt/CommonAPI/GameObjectExt.cs +++ b/Source/KSPAPIExtensions/CommonAPI/GameObjectExt.cs @@ -6,14 +6,14 @@ using KSP.IO; namespace KSPAPIExtensions { - internal static class GameObjectExtension + public static class GameObjectExtension { - internal static T AddTaggedComponent (this GameObject go) where T : Component + public static T AddTaggedComponent (this GameObject go) where T : Component { Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); return (T)go.AddComponent(taggedType); } - internal static T GetTaggedComponent (this GameObject go) where T : Component + public static T GetTaggedComponent (this GameObject go) where T : Component { Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); return (T)go.GetComponent(taggedType); diff --git a/Source/KSPAPIEL/KSPAPIEL.csproj b/Source/KSPAPIExtensions/KSPAPIExtensions.csproj similarity index 87% rename from Source/KSPAPIEL/KSPAPIEL.csproj rename to Source/KSPAPIExtensions/KSPAPIExtensions.csproj index 21ed99cb..2cebe098 100644 --- a/Source/KSPAPIEL/KSPAPIEL.csproj +++ b/Source/KSPAPIExtensions/KSPAPIExtensions.csproj @@ -12,14 +12,14 @@ 512 False - KSPAPIEL + KSPAPIExtensions true full false ..\..\Build\KSPAPIExtensions\obj\ - ..\..\Build\KSPAPIExtensions\Debug\ + ..\..\GameData DEBUG;TRACE prompt 4 @@ -31,7 +31,7 @@ none true ..\..\Build\KSPAPIExtensions\obj\ - ..\GameData\ + ..\..\GameData TRACE prompt 4 @@ -48,7 +48,7 @@ - + @@ -67,19 +67,19 @@ --> - ..\..\_KSPAssemblies\Assembly-CSharp.dll - False - - - ..\..\_KSPAssemblies\System.dll + ..\..\..\..\KSP\_KSPAssemblies\Assembly-CSharp.dll False - ..\..\_KSPAssemblies\Assembly-CSharp-firstpass.dll + ..\..\..\..\KSP\_KSPAssemblies\Assembly-CSharp-firstpass.dll False - ..\..\_KSPAssemblies\UnityEngine.dll + ..\..\..\..\KSP\_KSPAssemblies\UnityEngine.dll + False + + + ..\..\..\..\KSP\_KSPAssemblies\System.dll False diff --git a/Source/KSPAPIEL/Properties/AssemblyInfo.cs b/Source/KSPAPIExtensions/Properties/AssemblyInfo.cs similarity index 97% rename from Source/KSPAPIEL/Properties/AssemblyInfo.cs rename to Source/KSPAPIExtensions/Properties/AssemblyInfo.cs index 7c18db2e..742014fd 100644 --- a/Source/KSPAPIEL/Properties/AssemblyInfo.cs +++ b/Source/KSPAPIExtensions/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("1.7.4.9")] +[assembly: AssemblyVersion("1.7.4.10")] [assembly: KSPAssembly("KSPAPIExtensions", 1, 7)] diff --git a/Source/KSPAPIEL/Properties/AssemblyInfo.in b/Source/KSPAPIExtensions/Properties/AssemblyInfo.in similarity index 100% rename from Source/KSPAPIEL/Properties/AssemblyInfo.in rename to Source/KSPAPIExtensions/Properties/AssemblyInfo.in diff --git a/Source/KSPAPIExt/Tweakables/UIPartActionChooseOption.cs b/Source/KSPAPIExtensions/Tweakables/UIPartActionChooseOption.cs similarity index 100% rename from Source/KSPAPIExt/Tweakables/UIPartActionChooseOption.cs rename to Source/KSPAPIExtensions/Tweakables/UIPartActionChooseOption.cs diff --git a/Source/KSPAPIEL/Tweakables/UIPartActionFloatEdit.cs b/Source/KSPAPIExtensions/Tweakables/UIPartActionFloatEdit.cs similarity index 99% rename from Source/KSPAPIEL/Tweakables/UIPartActionFloatEdit.cs rename to Source/KSPAPIExtensions/Tweakables/UIPartActionFloatEdit.cs index 3ce63814..970da8dc 100644 --- a/Source/KSPAPIEL/Tweakables/UIPartActionFloatEdit.cs +++ b/Source/KSPAPIExtensions/Tweakables/UIPartActionFloatEdit.cs @@ -3,7 +3,7 @@ using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIEL +namespace KSPAPIExtensions { [UI_FloatEdit] diff --git a/Source/KSPAPIEL/Tweakables/UIPartActionScaleEdit.cs b/Source/KSPAPIExtensions/Tweakables/UIPartActionScaleEdit.cs similarity index 99% rename from Source/KSPAPIEL/Tweakables/UIPartActionScaleEdit.cs rename to Source/KSPAPIExtensions/Tweakables/UIPartActionScaleEdit.cs index 4a760e1d..e77d5872 100644 --- a/Source/KSPAPIEL/Tweakables/UIPartActionScaleEdit.cs +++ b/Source/KSPAPIExtensions/Tweakables/UIPartActionScaleEdit.cs @@ -3,7 +3,7 @@ using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIEL +namespace KSPAPIExtensions { [UI_ScaleEdit] diff --git a/Source/KSPAPIEL/Tweakables/UIPartActionsExtended.cs b/Source/KSPAPIExtensions/Tweakables/UIPartActionsExtended.cs similarity index 99% rename from Source/KSPAPIEL/Tweakables/UIPartActionsExtended.cs rename to Source/KSPAPIExtensions/Tweakables/UIPartActionsExtended.cs index fd00a7dc..7476249a 100644 --- a/Source/KSPAPIEL/Tweakables/UIPartActionsExtended.cs +++ b/Source/KSPAPIExtensions/Tweakables/UIPartActionsExtended.cs @@ -7,7 +7,7 @@ using UnityEngine; // ReSharper disable once CheckNamespace -namespace KSPAPIEL +namespace KSPAPIExtensions { internal class UIPartActionsExtendedRegistration : MonoBehaviour { @@ -27,7 +27,7 @@ public void Start() DontDestroyOnLoad(gameObject); - isLatestVersion = SystemUtils.RunTypeElection(typeof(UIPartActionsExtendedRegistration), "KSPAPIEL"); + isLatestVersion = SystemUtils.RunTypeElection(typeof(UIPartActionsExtendedRegistration), "KSPAPIExtensions"); } public void OnLevelWasLoaded(int level) diff --git a/Source/KSPAPIEL/Utils/KSPUtils.cs b/Source/KSPAPIExtensions/Utils/GameScenesUtils.cs similarity index 95% rename from Source/KSPAPIEL/Utils/KSPUtils.cs rename to Source/KSPAPIExtensions/Utils/GameScenesUtils.cs index ddca8cda..87145af8 100644 --- a/Source/KSPAPIEL/Utils/KSPUtils.cs +++ b/Source/KSPAPIExtensions/Utils/GameScenesUtils.cs @@ -6,7 +6,7 @@ // ReSharper disable once CheckNamespace -namespace KSPAPIEL +namespace KSPAPIExtensions { /// /// Flags to filter particular game scenes. @@ -30,7 +30,7 @@ public enum GameSceneFilter Any = 0xFFFF } - public static class PartUtils + public static class GameScenesUtils { /// /// Convert GameScene enum into GameSceneFilter diff --git a/Source/KSPAPIExtensions/Utils/MathUtils.cs b/Source/KSPAPIExtensions/Utils/MathUtils.cs new file mode 100644 index 00000000..1f692932 --- /dev/null +++ b/Source/KSPAPIExtensions/Utils/MathUtils.cs @@ -0,0 +1,393 @@ +using System; +using UnityEngine; + +// ReSharper disable once CheckNamespace +namespace KSPAPIExtensions +{ + public static class MathUtils + { + + /// + /// Clamp a value between the max and min values. If < min it will be changed to min, if > max it will be changed to max. + /// + /// Value to clamp + /// Minimum + /// Maximum + /// true if the value has been clamped. + public static bool TestClamp(ref float value, float min, float max) + { + if (value < min) + { + value = min; + return true; + } + if (value > max) + { + value = max; + return true; + } + return false; + } + + /// + /// Round value to the nearest. + /// + /// Value to round + /// Precision, eg 0.1, 0.02, 0.005 + /// The rounded value + public static float RoundTo(float value, float precision) + { + return Mathf.Round(value / precision) * precision; + } + + /// + /// Format a numeric value using SI prefexes. + /// + /// eg: 13401 -> 13.4 k + /// + /// + /// value to format + /// unit string + /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. + /// number of signifigant figures to display + /// + public static string ToStringSI(this double value, int sigFigs = 3, int exponent = 0, string unit = null) + { + SIPrefix prefix = value.GetSIPrefix(exponent); + return prefix.FormatSI(value, sigFigs, exponent, unit); + } + + /// + /// Format a numeric value using SI prefexes. + /// + /// eg: 13401 -> 13.4 k + /// + /// + /// value to format + /// unit string + /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. + /// number of signifigant figures to display + /// + public static string ToStringSI(this float value, int sigFigs = 3, int exponent = 0, string unit = null) + { + SIPrefix prefix = value.GetSIPrefix(exponent); + return prefix.FormatSI(value, sigFigs, exponent, unit); + } + + internal static string FormatSI(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0, string unit = null) + { + return string.Format("{0}{1}{2}", pfx.GetFormatter(value, sigFigs, exponent)(value), pfx.PrefixString(), unit); + } + + /// + /// This extension of the standard string format method has available an extra standard format. + /// + /// Examples: + /// + /// 12.ToStringExt("S") -> "12" + /// 12.ToStringExt("S3") -> "12.0" + /// 120.ToStringExt("S3") -> "120" + /// 1254.ToStringExt("S3") -> "1250" (4 digit numbers do not use k as a special case) + /// 12540.ToStringExt("S3") -> "1.25k" (using SI prefixes) + /// 12540.ToStringExt("S4") -> "1.254k" (more significant figures) + /// (1.254).ToStringExt("S4+3") -> "1.254k" (+3 means the 'natural prefix' is k) + /// (1.254).ToStringExt("S4-3") -> "1.254m" (-3 means the 'natural prefix' is m) + /// + /// + public static string ToStringExt(this double value, string format) + { + if (format[0] == 'S' || format[0] == 's') + { + if (format.Length == 1) + return ToStringSI(value, 0); + int pmi = format.IndexOf('+'); + int sigFigs; + if (pmi < 0) + { + pmi = format.IndexOf('-'); + if (pmi < 0) + { + sigFigs = int.Parse(format.Substring(1)); + return ToStringSI(value, sigFigs); + } + } + sigFigs = int.Parse(format.Substring(1, pmi - 1)); + int exponent = int.Parse(format.Substring(pmi)); + return ToStringSI(value, sigFigs, exponent); + } + return value.ToString(format); + } + + /// + /// ToStringExt for floats. See doc for doubles. + /// + public static string ToStringExt(this float value, string format) + { + return ToStringExt((double)value, format); + } + + /// + /// Parse a string in SI format - with SI unit prefix to a double. + /// Note - units must not be present. + /// + public static bool TryParseExt(string str, out double value) + { + str = str.Trim(); + if(str.Length == 0) { + value = 0; + return false; + } + + char last = str[str.Length - 1]; + + if (char.IsDigit(last)) + return double.TryParse(str, out value); + + double exponent; + int newLen = str.Length - 1; + switch (last) + { + case 'k': exponent = 1e3; break; + case 'M': exponent = 1e6; break; + case 'G': exponent = 1e9; break; + case 'T': exponent = 1e12; break; + case 'P': exponent = 1e15; break; + case 'E': exponent = 1e18; break; + case 'Z': exponent = 1e21; break; + case 'Y': exponent = 1e24; break; + case 'm': + exponent = 1e-3; break; + case '\x3bc': + case '\xb5': + case 'u': + exponent = 1e-6; break; + case 'c': + if (!str.EndsWith("mic")) + { + value = 0; + return false; + } + newLen = str.Length - 3; + exponent = 1e-6; + break; + case 'n': exponent = 1e-9; break; + case 'p': exponent = 1e-12; break; + case 'f': exponent = 1e-15; break; + case 'a': exponent = 1e-18; break; + case 'z': exponent = 1e-21; break; + case 'y': exponent = 1e-24; break; + default: + value = 0; + return false; + } + + double parsed; + if (!double.TryParse(str.Substring(0, newLen), out parsed)) + { + value = 0; + return false; + } + + value = parsed * exponent; + return true; + } + + /// + /// Parse a string in SI format - with SI unit prefix to a float. + /// Note - units must not be present. + /// + public static bool TryParseExt(string str, out float value) + { + double dVal; + if (TryParseExt(str, out dVal)) + { + value = (float)dVal; + return true; + } + value = 0; + return false; + } + + /// + /// Round a number to a set number of significant figures. + /// + /// number to round + /// number of significant figures, defaults to 3 + /// + public static float RoundSigFigs(this float d, int sigFigs = 3) + { + + int exponent = (int)Math.Floor(Math.Log10(Math.Abs(d))) - sigFigs; + float div = Mathf.Pow(10, exponent); + return Mathf.Round(d / div) * div; + } + + /// + /// Round a number to a set number of significant figures. + /// + /// number to round + /// number of significant figures, defaults to 3 + /// + public static double RoundSigFigs(this double value, int sigFigs = 3) + { + int exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))) - sigFigs; + double div = Mathf.Pow(10, exponent); + return Math.Round(value / div) * div; + } + + /// + /// Find the SI prefix for a number + /// + /// The value to find the prefix for + /// The natural exponent, if your value was km rather than m, use 3 + public static SIPrefix GetSIPrefix(this double value, int exponent = 0) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (value == 0) + return SIPrefix.None; + + int exp = (int)Math.Floor(Math.Log10(Math.Abs(value))) + exponent; + + if (exp <= 3 && exp >= -1) + return SIPrefix.None; + if (exp < 0) + return (SIPrefix)((exp-2) / 3 * 3); + return (SIPrefix)(exp / 3 * 3); + } + + /// + /// Find the SI prefix for a number + /// + public static SIPrefix GetSIPrefix(this float value, int exponent = 0) + { + return GetSIPrefix((double)value, exponent); + } + + /// + /// The prefix string for a particular exponent. Note Micro will use the string "mic" rather than "µ", so is longer. + /// + public static string PrefixString(this SIPrefix pfx) + { + switch (pfx) + { + case SIPrefix.None: return ""; + case SIPrefix.Kilo: return "k"; + case SIPrefix.Mega: return "M"; + case SIPrefix.Giga: return "G"; + case SIPrefix.Tera: return "T"; + case SIPrefix.Peta: return "P"; + case SIPrefix.Exa: return "E"; + case SIPrefix.Zotta: return "Z"; + case SIPrefix.Yotta: return "Y"; + case SIPrefix.Milli: return "m"; + case SIPrefix.Micro: return "u"; + case SIPrefix.Nano: return "n"; + case SIPrefix.Pico: return "p"; + case SIPrefix.Femto: return "f"; + case SIPrefix.Atto: return "a"; + case SIPrefix.Zepto: return "z"; + case SIPrefix.Yocto: return "y"; + default: throw new ArgumentException("Illegal prefix", "pfx"); + } + } + + /// + /// Round a value with respect the the given SI prefix. + /// + /// eg: + /// SIPrefix.Mega.Round(34.456e6f, digits:1) -> 34.6e6f + /// + /// + /// The SI prefix + /// Value to round + /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) + /// Natural exponent of value, eg: use 3 if value is km rather than m + /// + public static float Round(this SIPrefix pfx, float value, int digits = 3, int exponent = 0) + { + float div = Mathf.Pow(10, (int)pfx - digits + exponent); + return Mathf.Round(value / div) * div; + } + + /// + /// Round a value with respect the the given SI prefix. + /// + /// eg: + /// SIPrefix.Mega.Round(34.456e6, digits:1) -> 34.6e6 + /// + /// + /// The SI prefix + /// Value to round + /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) + /// Natural exponent of value, eg: use 3 if value is km rather than m + /// + public static double Round(this SIPrefix pfx, double value, int digits = 3, int exponent = 0) + { + double div = Math.Pow(10, (int)pfx - digits + exponent); + return Math.Round(value / div) * div; + } + + /// + /// Get a formatter function. This is useful when you have a range of values you'd like formatted the same way. + /// + public static Func GetFormatter(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0) + { + int exp = (int)(Math.Floor(Math.Log10(Math.Abs(value)))) - (int)pfx + exponent; + double div = Math.Pow(10, (int)pfx - exponent); + + if (exp < 0) + return v => (v/div).ToString("F" + (sigFigs-1)); + if (exp >= sigFigs) + { + double mult = Math.Pow(10, exp - sigFigs + 1); + return v => (Math.Round(v / div / mult) * mult).ToString("F0"); + } + return v => (v/div).ToString("F" + (sigFigs - exp - 1)); + } + + + /// + /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 + /// + public static string FormatMass(float mass, int sigFigs = 4, int exponent = 0) + { + return mass < 1.0f ? + mass.ToStringSI(sigFigs, exponent + 6, "g") : + mass.ToStringSI(sigFigs, exponent, "t"); + } + + /// + /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 + /// + public static string FormatMass(double mass, int sigFigs = 4, int exponent = 0) + { + return mass < 1.0f ? + mass.ToStringSI(sigFigs, exponent+6, "g") : + mass.ToStringSI(sigFigs, exponent, "t"); + } + } + + /// + /// SI Prefix. If cast as an int, the base 10 log for the prefix will be produced. + /// + public enum SIPrefix + { + None = 0, + Kilo = 3, + Mega = 6, + Giga = 9, + Tera = 12, + Peta = 15, + Exa = 18, + Zotta = 21, + Yotta = 24, + Milli = -3, + Micro = -6, + Nano = -9, + Pico = -12, + Femto = -15, + Atto = -18, + Zepto = -21, + Yocto = -24 + } +} \ No newline at end of file diff --git a/Source/KSPAPIExt/Utils/SystemUtils.cs b/Source/KSPAPIExtensions/Utils/SystemUtils.cs similarity index 100% rename from Source/KSPAPIExt/Utils/SystemUtils.cs rename to Source/KSPAPIExtensions/Utils/SystemUtils.cs diff --git a/version-gen b/Source/KSPAPIExtensions/version-gen-lite old mode 100755 new mode 100644 similarity index 86% rename from version-gen rename to Source/KSPAPIExtensions/version-gen-lite index 8c13ffd3..ab59fdeb --- a/version-gen +++ b/Source/KSPAPIExtensions/version-gen-lite @@ -3,7 +3,7 @@ # Updates the version code cd "$(dirname "$0")" -cd "$(git rev-parse --show-toplevel)" +# cd "$(git rev-parse --show-toplevel)" git_desc=$(git describe --tags --long --match "v*") version=$(echo "$git_desc" | sed 's:v\(.*\)-\(.*\)-.*:\1.\2:') @@ -41,13 +41,13 @@ gen_tagged_class_wrappers() { echo "}" } -sed -e "s/@VERSION@/$version/;s/@MAJOR/$major/;s/@MINOR/$minor/" Source/Properties/AssemblyInfo.in > tmp -cmp -s Source/Properties/AssemblyInfo.cs tmp || cp tmp Source/Properties/AssemblyInfo.cs +sed -e "s/@VERSION@/$version/;s/@MAJOR/$major/;s/@MINOR/$minor/" Properties/AssemblyInfo.in > tmp +cmp -s Properties/AssemblyInfo.cs tmp || cp tmp Properties/AssemblyInfo.cs #sed -e "s/@VERSION@/$short_version/" ProceduralParts.version.in > tmp #cmp -s ProceduralParts.version tmp || cp tmp ProceduralParts.version gen_tagged_class_wrappers > tmp -cmp -s Source/tagged_classes.cs tmp || cp tmp Source/tagged_classes.cs +cmp -s tagged_classes.cs tmp || cp tmp tagged_classes.cs rm tmp diff --git a/Source/KSPAPIExt/zzVersionChecker.cs b/Source/KSPAPIExtensions/zzVersionChecker.cs similarity index 100% rename from Source/KSPAPIExt/zzVersionChecker.cs rename to Source/KSPAPIExtensions/zzVersionChecker.cs diff --git a/Source/KSPAPIExt/CommonAPI/ResourceExtensions.cs b/Source/KSPAPIExtras/CommonAPI/ResourceExtensions.cs similarity index 100% rename from Source/KSPAPIExt/CommonAPI/ResourceExtensions.cs rename to Source/KSPAPIExtras/CommonAPI/ResourceExtensions.cs diff --git a/Source/KSPAPIExt/DuckTyping/AmbiguousTypesException.cs b/Source/KSPAPIExtras/DuckTyping/AmbiguousTypesException.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/AmbiguousTypesException.cs rename to Source/KSPAPIExtras/DuckTyping/AmbiguousTypesException.cs diff --git a/Source/KSPAPIExt/DuckTyping/DelegateDuckCaster.cs b/Source/KSPAPIExtras/DuckTyping/DelegateDuckCaster.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/DelegateDuckCaster.cs rename to Source/KSPAPIExtras/DuckTyping/DelegateDuckCaster.cs diff --git a/Source/KSPAPIExt/DuckTyping/DelegateDuckProxyType.cs b/Source/KSPAPIExtras/DuckTyping/DelegateDuckProxyType.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/DelegateDuckProxyType.cs rename to Source/KSPAPIExtras/DuckTyping/DelegateDuckProxyType.cs diff --git a/Source/KSPAPIExt/DuckTyping/DuckProxyType.cs b/Source/KSPAPIExtras/DuckTyping/DuckProxyType.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/DuckProxyType.cs rename to Source/KSPAPIExtras/DuckTyping/DuckProxyType.cs diff --git a/Source/KSPAPIExt/DuckTyping/DuckTyping.cs b/Source/KSPAPIExtras/DuckTyping/DuckTyping.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/DuckTyping.cs rename to Source/KSPAPIExtras/DuckTyping/DuckTyping.cs diff --git a/Source/KSPAPIExt/DuckTyping/EnumStringDuckCaster.cs b/Source/KSPAPIExtras/DuckTyping/EnumStringDuckCaster.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/EnumStringDuckCaster.cs rename to Source/KSPAPIExtras/DuckTyping/EnumStringDuckCaster.cs diff --git a/Source/KSPAPIExt/DuckTyping/FromTypeTree.cs b/Source/KSPAPIExtras/DuckTyping/FromTypeTree.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/FromTypeTree.cs rename to Source/KSPAPIExtras/DuckTyping/FromTypeTree.cs diff --git a/Source/KSPAPIExt/DuckTyping/GenericParameterDuckCaster.cs b/Source/KSPAPIExtras/DuckTyping/GenericParameterDuckCaster.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/GenericParameterDuckCaster.cs rename to Source/KSPAPIExtras/DuckTyping/GenericParameterDuckCaster.cs diff --git a/Source/KSPAPIExt/DuckTyping/IDuckCaster.cs b/Source/KSPAPIExtras/DuckTyping/IDuckCaster.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/IDuckCaster.cs rename to Source/KSPAPIExtras/DuckTyping/IDuckCaster.cs diff --git a/Source/KSPAPIExt/DuckTyping/IDuckProxy.cs b/Source/KSPAPIExtras/DuckTyping/IDuckProxy.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/IDuckProxy.cs rename to Source/KSPAPIExtras/DuckTyping/IDuckProxy.cs diff --git a/Source/KSPAPIExt/DuckTyping/IStaticDuckCaster.cs b/Source/KSPAPIExtras/DuckTyping/IStaticDuckCaster.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/IStaticDuckCaster.cs rename to Source/KSPAPIExtras/DuckTyping/IStaticDuckCaster.cs diff --git a/Source/KSPAPIExt/DuckTyping/InterfaceDuckCaster.cs b/Source/KSPAPIExtras/DuckTyping/InterfaceDuckCaster.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/InterfaceDuckCaster.cs rename to Source/KSPAPIExtras/DuckTyping/InterfaceDuckCaster.cs diff --git a/Source/KSPAPIExt/DuckTyping/InterfaceDuckProxyType.cs b/Source/KSPAPIExtras/DuckTyping/InterfaceDuckProxyType.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/InterfaceDuckProxyType.cs rename to Source/KSPAPIExtras/DuckTyping/InterfaceDuckProxyType.cs diff --git a/Source/KSPAPIExt/DuckTyping/InterfaceStaticDuckCaster.cs b/Source/KSPAPIExtras/DuckTyping/InterfaceStaticDuckCaster.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/InterfaceStaticDuckCaster.cs rename to Source/KSPAPIExtras/DuckTyping/InterfaceStaticDuckCaster.cs diff --git a/Source/KSPAPIExt/DuckTyping/ToTypeFromTypeTree.cs b/Source/KSPAPIExtras/DuckTyping/ToTypeFromTypeTree.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/ToTypeFromTypeTree.cs rename to Source/KSPAPIExtras/DuckTyping/ToTypeFromTypeTree.cs diff --git a/Source/KSPAPIExt/DuckTyping/TypeKey.cs b/Source/KSPAPIExtras/DuckTyping/TypeKey.cs similarity index 100% rename from Source/KSPAPIExt/DuckTyping/TypeKey.cs rename to Source/KSPAPIExtras/DuckTyping/TypeKey.cs diff --git a/Source/KSPAPIExt/KSPAPIExtensions.csproj b/Source/KSPAPIExtras/KSPAPIExtras.csproj similarity index 81% rename from Source/KSPAPIExt/KSPAPIExtensions.csproj rename to Source/KSPAPIExtras/KSPAPIExtras.csproj index 24258ee7..5a1bf5e5 100644 --- a/Source/KSPAPIExt/KSPAPIExtensions.csproj +++ b/Source/KSPAPIExtras/KSPAPIExtras.csproj @@ -1,17 +1,17 @@  - + Debug AnyCPU - {4282B0DA-C314-4524-9C32-EDAB2C9666A9} + {7C69B948-FF19-11E4-B985-1E3E84ADF75F} Library Properties KSPAPIExtensions - KSPAPIExtensions v3.5 512 + False true @@ -25,19 +25,20 @@ AnyCPU 1607 true + KSPAPIExtensions none true ..\..\Build\KSPAPIExtensions\obj\ - ..\ + ..\..\GameData TRACE prompt 4 true + KSPAPIExtras - @@ -56,10 +57,8 @@ - - @@ -68,28 +67,10 @@ - - - - - - - - - ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\Assembly-CSharp.dll - False - - - ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\Assembly-CSharp-firstpass.dll - False - - - - ..\..\..\..\..\..\Games\KSP_nightly\KSP_Data\Managed\UnityEngine.dll - False - + + @@ -106,4 +87,29 @@ --> + + + ..\..\..\..\KSP\_KSPAssemblies\Assembly-CSharp.dll + False + + + ..\..\..\..\KSP\_KSPAssemblies\Assembly-CSharp-firstpass.dll + False + + + ..\..\..\..\KSP\_KSPAssemblies\System.dll + False + + + ..\..\..\..\KSP\_KSPAssemblies\UnityEngine.dll + False + + + + + {4282B0DA-C314-4524-9C32-EDAB2C9666A9} + KSPAPIExtensions + False + + \ No newline at end of file diff --git a/Source/KSPAPIExt/PartMessage/Attributes.cs b/Source/KSPAPIExtras/PartMessage/Attributes.cs similarity index 99% rename from Source/KSPAPIExt/PartMessage/Attributes.cs rename to Source/KSPAPIExtras/PartMessage/Attributes.cs index 4688e158..c2e04a59 100644 --- a/Source/KSPAPIExt/PartMessage/Attributes.cs +++ b/Source/KSPAPIExtras/PartMessage/Attributes.cs @@ -1,4 +1,5 @@ -using System; +using KSPAPIExtensions; +using System; using KSPAPIExtensions.DebuggingUtils; namespace KSPAPIExtensions.PartMessage diff --git a/Source/KSPAPIExt/PartMessage/CommonMessages.cs b/Source/KSPAPIExtras/PartMessage/CommonMessages.cs similarity index 100% rename from Source/KSPAPIExt/PartMessage/CommonMessages.cs rename to Source/KSPAPIExtras/PartMessage/CommonMessages.cs diff --git a/Source/KSPAPIExt/PartMessage/PartMessageService.cs b/Source/KSPAPIExtras/PartMessage/PartMessageService.cs similarity index 100% rename from Source/KSPAPIExt/PartMessage/PartMessageService.cs rename to Source/KSPAPIExtras/PartMessage/PartMessageService.cs diff --git a/Source/KSPAPIExt/PartMessage/zzImplementation.cs b/Source/KSPAPIExtras/PartMessage/zzImplementation.cs similarity index 99% rename from Source/KSPAPIExt/PartMessage/zzImplementation.cs rename to Source/KSPAPIExtras/PartMessage/zzImplementation.cs index 2dcdaf6e..61d76575 100644 --- a/Source/KSPAPIExt/PartMessage/zzImplementation.cs +++ b/Source/KSPAPIExtras/PartMessage/zzImplementation.cs @@ -1,4 +1,5 @@ -using System; +using KSPAPIExtensions; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -1006,7 +1007,7 @@ internal void Awake() } loadedInScene = true; - if (!SystemUtils.RunTypeElection(typeof(PartMessageService), "KSPAPIExtensions")) + if (!SystemUtils.RunTypeElection(typeof(PartMessageService), "KSPAPIExtras")) return; // So at this point we know we have won the election, and will be using the class versions as in this assembly. diff --git a/Source/KSPAPIExt/Properties/AssemblyInfo.cs b/Source/KSPAPIExtras/Properties/AssemblyInfo.cs similarity index 97% rename from Source/KSPAPIExt/Properties/AssemblyInfo.cs rename to Source/KSPAPIExtras/Properties/AssemblyInfo.cs index 7c18db2e..742014fd 100644 --- a/Source/KSPAPIExt/Properties/AssemblyInfo.cs +++ b/Source/KSPAPIExtras/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("1.7.4.9")] +[assembly: AssemblyVersion("1.7.4.10")] [assembly: KSPAssembly("KSPAPIExtensions", 1, 7)] diff --git a/Source/KSPAPIExtras/Properties/AssemblyInfo.in b/Source/KSPAPIExtras/Properties/AssemblyInfo.in new file mode 100644 index 00000000..06262f9b --- /dev/null +++ b/Source/KSPAPIExtras/Properties/AssemblyInfo.in @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("KSPAPIExtensions")] +[assembly: AssemblyDescription("A set of utilities for plugin developers for Kerbal Space Program")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("swamp_ig and taniwha")] +[assembly: AssemblyProduct("KSPAPIExtensions")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("e46ed9d3-b992-4f9a-8e9a-64536116a2c4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("@VERSION@")] +[assembly: KSPAssembly("KSPAPIExtensions", @MAJOR, @MINOR)] diff --git a/Source/KSPAPIExt/Utils/ConfigNodeUtils.cs b/Source/KSPAPIExtras/Utils/ConfigNodeUtils.cs similarity index 100% rename from Source/KSPAPIExt/Utils/ConfigNodeUtils.cs rename to Source/KSPAPIExtras/Utils/ConfigNodeUtils.cs diff --git a/Source/KSPAPIExt/Utils/DebuggingUtils.cs b/Source/KSPAPIExtras/Utils/DebuggingUtils.cs similarity index 100% rename from Source/KSPAPIExt/Utils/DebuggingUtils.cs rename to Source/KSPAPIExtras/Utils/DebuggingUtils.cs diff --git a/Source/KSPAPIExt/Utils/EngineWrapper.cs b/Source/KSPAPIExtras/Utils/EngineWrapper.cs similarity index 100% rename from Source/KSPAPIExt/Utils/EngineWrapper.cs rename to Source/KSPAPIExtras/Utils/EngineWrapper.cs diff --git a/Source/KSPAPIExt/Utils/KSPUtils.cs b/Source/KSPAPIExtras/Utils/KSPUtils.cs similarity index 89% rename from Source/KSPAPIExt/Utils/KSPUtils.cs rename to Source/KSPAPIExtras/Utils/KSPUtils.cs index 8d390de9..470f8d42 100644 --- a/Source/KSPAPIExt/Utils/KSPUtils.cs +++ b/Source/KSPAPIExtras/Utils/KSPUtils.cs @@ -1,4 +1,5 @@ -using System; +using KSPAPIExtensions; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -30,28 +31,6 @@ public enum PartRelationship AnyPart = AnyOnVessel | Unrelated, } - /// - /// Flags to filter particular game scenes. - /// - [Flags] - public enum GameSceneFilter - { - Loading = 1 << GameScenes.LOADING, - MainMenu = 1 << GameScenes.MAINMENU, - SpaceCenter = 1 << GameScenes.SPACECENTER, - VAB = 1 << GameScenes.EDITOR, - SPH = 1 << GameScenes.EDITOR, - Flight = 1 << GameScenes.FLIGHT, - TrackingStation = 1 << GameScenes.TRACKSTATION, - Settings = 1 << GameScenes.SETTINGS, - Credits = 1 << GameScenes.CREDITS, - - AnyEditor = VAB | SPH, - AnyEditorOrFlight = AnyEditor | Flight, - AnyInitializing = 0xFFFF & ~(AnyEditor | Flight), - Any = 0xFFFF - } - public static class PartUtils { private static FieldInfo windowListField; @@ -189,22 +168,6 @@ internal static bool TestFlag(this PartRelationship e, PartRelationship flags) return (e & flags) == flags; } - /// - /// Convert GameScene enum into GameSceneFilter - /// - public static GameSceneFilter AsFilter(this GameScenes scene) - { - return (GameSceneFilter)(1 << (int)scene); - } - - /// - /// True if the current game scene matches the filter. - /// - public static bool IsLoaded(this GameSceneFilter filter) - { - return (int)(filter & HighLogic.LoadedScene.AsFilter()) != 0; - } - /// /// Register an 'OnUpdate' method for use in the editor. /// This should be done in the OnAwake method of the module, and will ensure that all modules have the diff --git a/Source/KSPAPIExt/Utils/ParseUtils.cs b/Source/KSPAPIExtras/Utils/ParseUtils.cs similarity index 100% rename from Source/KSPAPIExt/Utils/ParseUtils.cs rename to Source/KSPAPIExtras/Utils/ParseUtils.cs diff --git a/Source/KSPAPIExt/Utils/PhysicsDebugger.cs b/Source/KSPAPIExtras/Utils/PhysicsDebugger.cs similarity index 100% rename from Source/KSPAPIExt/Utils/PhysicsDebugger.cs rename to Source/KSPAPIExtras/Utils/PhysicsDebugger.cs diff --git a/Source/KSPAPIExt/Utils/UnityUtils.cs b/Source/KSPAPIExtras/Utils/UnityUtils.cs similarity index 100% rename from Source/KSPAPIExt/Utils/UnityUtils.cs rename to Source/KSPAPIExtras/Utils/UnityUtils.cs diff --git a/Source/KSPAPIExtras/version-gen-ext b/Source/KSPAPIExtras/version-gen-ext new file mode 100644 index 00000000..957533f3 --- /dev/null +++ b/Source/KSPAPIExtras/version-gen-ext @@ -0,0 +1,60 @@ +#!/bin/sh +# +# Updates the version code + +cd "$(dirname "$0")" +# cd "$(git rev-parse --show-toplevel)" + +git_desc=$(git describe --tags --long --match "v*") +version=$(echo "$git_desc" | sed 's:v\(.*\)-\(.*\)-.*:\1.\2:') +short_version=$(echo "$git_desc" | sed 's:v\([^-]*\).*:\1:') +major=$(echo "$git_desc" | sed 's:v\(.*\)\.\(.*\)\..*:\1:') +minor=$(echo "$git_desc" | sed 's:v\(.*\)\.\(.*\)\..*:\2:') + +version_tag=`echo $version | tr . _` + +gen_tagged_addon() { + classname=$1 + echo " [KSPAddonFixed(KSPAddon.Startup.Instantly, false, typeof(${classname}_${version_tag}))]" +} + +gen_tagged_class() { + classname=$1 + echo " internal class ${classname}_${version_tag} : ${classname} { }" +} + +gen_tagged_class_wrappers() { + echo "using KSPAPIEL;" + echo "using System;" + echo "using System.Collections.Generic;" + echo "using System.Linq;" + echo "using System.Reflection;" + echo "using DeftTech.DuckTyping;" + echo "using UnityEngine;" + echo "namespace KSPAPIExtensions {" + echo " [KSPAddon(KSPAddon.Startup.Instantly, true)]" + gen_tagged_class OnEditorUpdateUtility + echo " [KSPAddon(KSPAddon.Startup.Instantly, true)]" + gen_tagged_class CompatibilityChecker + echo "}" + echo "namespace KSPAPIExtensions.PartMessage {" + gen_tagged_class ServiceImpl + gen_tagged_addon PartMessageServiceInitializer + gen_tagged_class PartMessageServiceInitializer + gen_tagged_class KnownPartMarker + gen_tagged_class ListenerFerramAerospaceResearch + echo "}" +} + + + +sed -e "s/@VERSION@/$version/;s/@MAJOR/$major/;s/@MINOR/$minor/" Properties/AssemblyInfo.in > tmp +cmp -s Properties/AssemblyInfo.cs tmp || cp tmp Properties/AssemblyInfo.cs + +#sed -e "s/@VERSION@/$short_version/" ProceduralParts.version.in > tmp +#cmp -s ProceduralParts.version tmp || cp tmp ProceduralParts.version + +gen_tagged_class_wrappers > tmp +cmp -s tagged_classes.cs tmp || cp tmp tagged_classes.cs + +rm tmp diff --git a/Source/KSPAPIEL/zzVersionChecker.cs b/Source/KSPAPIExtras/zzVersionChecker.cs similarity index 99% rename from Source/KSPAPIEL/zzVersionChecker.cs rename to Source/KSPAPIExtras/zzVersionChecker.cs index 852a5ced..689766e1 100644 --- a/Source/KSPAPIEL/zzVersionChecker.cs +++ b/Source/KSPAPIExtras/zzVersionChecker.cs @@ -33,7 +33,7 @@ /*-----------------------------------------*\ | SUBSTITUTE YOUR MOD'S NAMESPACE HERE. | \*-----------------------------------------*/ -namespace KSPAPIEL +namespace KSPAPIExtensions { /** From b847d1dc0bc1066e305bb9f9cc8a62adacd396ee Mon Sep 17 00:00:00 2001 From: toadicus Date: Wed, 20 May 2015 11:14:22 -0700 Subject: [PATCH 12/18] ToStringExt: Checking format length to avoid an index exception. --- Source/KSPAPIExtensions/Utils/MathUtils.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/KSPAPIExtensions/Utils/MathUtils.cs b/Source/KSPAPIExtensions/Utils/MathUtils.cs index 1f692932..e0b752e4 100644 --- a/Source/KSPAPIExtensions/Utils/MathUtils.cs +++ b/Source/KSPAPIExtensions/Utils/MathUtils.cs @@ -96,6 +96,11 @@ internal static string FormatSI(this SIPrefix pfx, double value, int sigFigs = 3 /// public static string ToStringExt(this double value, string format) { + if (format.Length < 1) + { + return value.ToString(); + } + if (format[0] == 'S' || format[0] == 's') { if (format.Length == 1) From 730adac973651cd4c681a2c4f7f5e8565478262e Mon Sep 17 00:00:00 2001 From: toadicus Date: Wed, 20 May 2015 11:47:20 -0700 Subject: [PATCH 13/18] Tabs->Spaces for consistency with taniwha's coding standards --- .../CommonAPI/GameObjectExt.cs | 26 +- .../Tweakables/UIPartActionFloatEdit.cs | 12 +- .../Tweakables/UIPartActionScaleEdit.cs | 12 +- .../Tweakables/UIPartActionsExtended.cs | 2 +- Source/KSPAPIExtensions/Utils/MathUtils.cs | 782 +++++++++--------- .../CommonAPI/ResourceExtensions.cs | 294 +++---- 6 files changed, 564 insertions(+), 564 deletions(-) diff --git a/Source/KSPAPIExtensions/CommonAPI/GameObjectExt.cs b/Source/KSPAPIExtensions/CommonAPI/GameObjectExt.cs index c7fdba57..914e3138 100644 --- a/Source/KSPAPIExtensions/CommonAPI/GameObjectExt.cs +++ b/Source/KSPAPIExtensions/CommonAPI/GameObjectExt.cs @@ -6,17 +6,17 @@ using KSP.IO; namespace KSPAPIExtensions { - public static class GameObjectExtension - { - public static T AddTaggedComponent (this GameObject go) where T : Component - { - Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); - return (T)go.AddComponent(taggedType); - } - public static T GetTaggedComponent (this GameObject go) where T : Component - { - Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); - return (T)go.GetComponent(taggedType); - } - } + public static class GameObjectExtension + { + public static T AddTaggedComponent (this GameObject go) where T : Component + { + Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); + return (T)go.AddComponent(taggedType); + } + public static T GetTaggedComponent (this GameObject go) where T : Component + { + Type taggedType = SystemUtils.VersionTaggedType(typeof(T)); + return (T)go.GetComponent(taggedType); + } + } } diff --git a/Source/KSPAPIExtensions/Tweakables/UIPartActionFloatEdit.cs b/Source/KSPAPIExtensions/Tweakables/UIPartActionFloatEdit.cs index 970da8dc..64c8df20 100644 --- a/Source/KSPAPIExtensions/Tweakables/UIPartActionFloatEdit.cs +++ b/Source/KSPAPIExtensions/Tweakables/UIPartActionFloatEdit.cs @@ -259,12 +259,12 @@ public override void UpdateItem() private void UpdateValueDisplay(float newValue) { - if (FieldInfo.incrementSlide != 0 && - Mathf.Abs(this.value - newValue) >= 0.5f * FieldInfo.incrementSlide - ) - { - GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); - } + if (FieldInfo.incrementSlide != 0 && + Mathf.Abs(this.value - newValue) >= 0.5f * FieldInfo.incrementSlide + ) + { + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } this.value = newValue; // ReSharper disable once CompareOfFloatsByEqualityOperator diff --git a/Source/KSPAPIExtensions/Tweakables/UIPartActionScaleEdit.cs b/Source/KSPAPIExtensions/Tweakables/UIPartActionScaleEdit.cs index e77d5872..6f37813f 100644 --- a/Source/KSPAPIExtensions/Tweakables/UIPartActionScaleEdit.cs +++ b/Source/KSPAPIExtensions/Tweakables/UIPartActionScaleEdit.cs @@ -218,13 +218,13 @@ public override void UpdateItem() private void UpdateValueDisplay(float newValue) { - float inc = GetIncrementSlide(); + float inc = GetIncrementSlide(); - if (inc != 0 && - Mathf.Abs(this.value - newValue) >= 0.5 * inc) - { - GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); - } + if (inc != 0 && + Mathf.Abs(this.value - newValue) >= 0.5 * inc) + { + GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); + } this.value = newValue; // ReSharper disable once CompareOfFloatsByEqualityOperator diff --git a/Source/KSPAPIExtensions/Tweakables/UIPartActionsExtended.cs b/Source/KSPAPIExtensions/Tweakables/UIPartActionsExtended.cs index 7476249a..991219fd 100644 --- a/Source/KSPAPIExtensions/Tweakables/UIPartActionsExtended.cs +++ b/Source/KSPAPIExtensions/Tweakables/UIPartActionsExtended.cs @@ -27,7 +27,7 @@ public void Start() DontDestroyOnLoad(gameObject); - isLatestVersion = SystemUtils.RunTypeElection(typeof(UIPartActionsExtendedRegistration), "KSPAPIExtensions"); + isLatestVersion = SystemUtils.RunTypeElection(typeof(UIPartActionsExtendedRegistration), "KSPAPIExtensions"); } public void OnLevelWasLoaded(int level) diff --git a/Source/KSPAPIExtensions/Utils/MathUtils.cs b/Source/KSPAPIExtensions/Utils/MathUtils.cs index e0b752e4..fe7dfa7e 100644 --- a/Source/KSPAPIExtensions/Utils/MathUtils.cs +++ b/Source/KSPAPIExtensions/Utils/MathUtils.cs @@ -4,395 +4,395 @@ // ReSharper disable once CheckNamespace namespace KSPAPIExtensions { - public static class MathUtils - { - - /// - /// Clamp a value between the max and min values. If < min it will be changed to min, if > max it will be changed to max. - /// - /// Value to clamp - /// Minimum - /// Maximum - /// true if the value has been clamped. - public static bool TestClamp(ref float value, float min, float max) - { - if (value < min) - { - value = min; - return true; - } - if (value > max) - { - value = max; - return true; - } - return false; - } - - /// - /// Round value to the nearest. - /// - /// Value to round - /// Precision, eg 0.1, 0.02, 0.005 - /// The rounded value - public static float RoundTo(float value, float precision) - { - return Mathf.Round(value / precision) * precision; - } - - /// - /// Format a numeric value using SI prefexes. - /// - /// eg: 13401 -> 13.4 k - /// - /// - /// value to format - /// unit string - /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. - /// number of signifigant figures to display - /// - public static string ToStringSI(this double value, int sigFigs = 3, int exponent = 0, string unit = null) - { - SIPrefix prefix = value.GetSIPrefix(exponent); - return prefix.FormatSI(value, sigFigs, exponent, unit); - } - - /// - /// Format a numeric value using SI prefexes. - /// - /// eg: 13401 -> 13.4 k - /// - /// - /// value to format - /// unit string - /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. - /// number of signifigant figures to display - /// - public static string ToStringSI(this float value, int sigFigs = 3, int exponent = 0, string unit = null) - { - SIPrefix prefix = value.GetSIPrefix(exponent); - return prefix.FormatSI(value, sigFigs, exponent, unit); - } - - internal static string FormatSI(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0, string unit = null) - { - return string.Format("{0}{1}{2}", pfx.GetFormatter(value, sigFigs, exponent)(value), pfx.PrefixString(), unit); - } - - /// - /// This extension of the standard string format method has available an extra standard format. - /// - /// Examples: - /// - /// 12.ToStringExt("S") -> "12" - /// 12.ToStringExt("S3") -> "12.0" - /// 120.ToStringExt("S3") -> "120" - /// 1254.ToStringExt("S3") -> "1250" (4 digit numbers do not use k as a special case) - /// 12540.ToStringExt("S3") -> "1.25k" (using SI prefixes) - /// 12540.ToStringExt("S4") -> "1.254k" (more significant figures) - /// (1.254).ToStringExt("S4+3") -> "1.254k" (+3 means the 'natural prefix' is k) - /// (1.254).ToStringExt("S4-3") -> "1.254m" (-3 means the 'natural prefix' is m) - /// - /// - public static string ToStringExt(this double value, string format) - { - if (format.Length < 1) - { - return value.ToString(); - } - - if (format[0] == 'S' || format[0] == 's') - { - if (format.Length == 1) - return ToStringSI(value, 0); - int pmi = format.IndexOf('+'); - int sigFigs; - if (pmi < 0) - { - pmi = format.IndexOf('-'); - if (pmi < 0) - { - sigFigs = int.Parse(format.Substring(1)); - return ToStringSI(value, sigFigs); - } - } - sigFigs = int.Parse(format.Substring(1, pmi - 1)); - int exponent = int.Parse(format.Substring(pmi)); - return ToStringSI(value, sigFigs, exponent); - } - return value.ToString(format); - } - - /// - /// ToStringExt for floats. See doc for doubles. - /// - public static string ToStringExt(this float value, string format) - { - return ToStringExt((double)value, format); - } - - /// - /// Parse a string in SI format - with SI unit prefix to a double. - /// Note - units must not be present. - /// - public static bool TryParseExt(string str, out double value) - { - str = str.Trim(); - if(str.Length == 0) { - value = 0; - return false; - } - - char last = str[str.Length - 1]; - - if (char.IsDigit(last)) - return double.TryParse(str, out value); - - double exponent; - int newLen = str.Length - 1; - switch (last) - { - case 'k': exponent = 1e3; break; - case 'M': exponent = 1e6; break; - case 'G': exponent = 1e9; break; - case 'T': exponent = 1e12; break; - case 'P': exponent = 1e15; break; - case 'E': exponent = 1e18; break; - case 'Z': exponent = 1e21; break; - case 'Y': exponent = 1e24; break; - case 'm': - exponent = 1e-3; break; - case '\x3bc': - case '\xb5': - case 'u': - exponent = 1e-6; break; - case 'c': - if (!str.EndsWith("mic")) - { - value = 0; - return false; - } - newLen = str.Length - 3; - exponent = 1e-6; - break; - case 'n': exponent = 1e-9; break; - case 'p': exponent = 1e-12; break; - case 'f': exponent = 1e-15; break; - case 'a': exponent = 1e-18; break; - case 'z': exponent = 1e-21; break; - case 'y': exponent = 1e-24; break; - default: - value = 0; - return false; - } - - double parsed; - if (!double.TryParse(str.Substring(0, newLen), out parsed)) - { - value = 0; - return false; - } - - value = parsed * exponent; - return true; - } - - /// - /// Parse a string in SI format - with SI unit prefix to a float. - /// Note - units must not be present. - /// - public static bool TryParseExt(string str, out float value) - { - double dVal; - if (TryParseExt(str, out dVal)) - { - value = (float)dVal; - return true; - } - value = 0; - return false; - } - - /// - /// Round a number to a set number of significant figures. - /// - /// number to round - /// number of significant figures, defaults to 3 - /// - public static float RoundSigFigs(this float d, int sigFigs = 3) - { - - int exponent = (int)Math.Floor(Math.Log10(Math.Abs(d))) - sigFigs; - float div = Mathf.Pow(10, exponent); - return Mathf.Round(d / div) * div; - } - - /// - /// Round a number to a set number of significant figures. - /// - /// number to round - /// number of significant figures, defaults to 3 - /// - public static double RoundSigFigs(this double value, int sigFigs = 3) - { - int exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))) - sigFigs; - double div = Mathf.Pow(10, exponent); - return Math.Round(value / div) * div; - } - - /// - /// Find the SI prefix for a number - /// - /// The value to find the prefix for - /// The natural exponent, if your value was km rather than m, use 3 - public static SIPrefix GetSIPrefix(this double value, int exponent = 0) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (value == 0) - return SIPrefix.None; - - int exp = (int)Math.Floor(Math.Log10(Math.Abs(value))) + exponent; - - if (exp <= 3 && exp >= -1) - return SIPrefix.None; - if (exp < 0) - return (SIPrefix)((exp-2) / 3 * 3); - return (SIPrefix)(exp / 3 * 3); - } - - /// - /// Find the SI prefix for a number - /// - public static SIPrefix GetSIPrefix(this float value, int exponent = 0) - { - return GetSIPrefix((double)value, exponent); - } - - /// - /// The prefix string for a particular exponent. Note Micro will use the string "mic" rather than "µ", so is longer. - /// - public static string PrefixString(this SIPrefix pfx) - { - switch (pfx) - { - case SIPrefix.None: return ""; - case SIPrefix.Kilo: return "k"; - case SIPrefix.Mega: return "M"; - case SIPrefix.Giga: return "G"; - case SIPrefix.Tera: return "T"; - case SIPrefix.Peta: return "P"; - case SIPrefix.Exa: return "E"; - case SIPrefix.Zotta: return "Z"; - case SIPrefix.Yotta: return "Y"; - case SIPrefix.Milli: return "m"; - case SIPrefix.Micro: return "u"; - case SIPrefix.Nano: return "n"; - case SIPrefix.Pico: return "p"; - case SIPrefix.Femto: return "f"; - case SIPrefix.Atto: return "a"; - case SIPrefix.Zepto: return "z"; - case SIPrefix.Yocto: return "y"; - default: throw new ArgumentException("Illegal prefix", "pfx"); - } - } - - /// - /// Round a value with respect the the given SI prefix. - /// - /// eg: - /// SIPrefix.Mega.Round(34.456e6f, digits:1) -> 34.6e6f - /// - /// - /// The SI prefix - /// Value to round - /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) - /// Natural exponent of value, eg: use 3 if value is km rather than m - /// - public static float Round(this SIPrefix pfx, float value, int digits = 3, int exponent = 0) - { - float div = Mathf.Pow(10, (int)pfx - digits + exponent); - return Mathf.Round(value / div) * div; - } - - /// - /// Round a value with respect the the given SI prefix. - /// - /// eg: - /// SIPrefix.Mega.Round(34.456e6, digits:1) -> 34.6e6 - /// - /// - /// The SI prefix - /// Value to round - /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) - /// Natural exponent of value, eg: use 3 if value is km rather than m - /// - public static double Round(this SIPrefix pfx, double value, int digits = 3, int exponent = 0) - { - double div = Math.Pow(10, (int)pfx - digits + exponent); - return Math.Round(value / div) * div; - } - - /// - /// Get a formatter function. This is useful when you have a range of values you'd like formatted the same way. - /// - public static Func GetFormatter(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0) - { - int exp = (int)(Math.Floor(Math.Log10(Math.Abs(value)))) - (int)pfx + exponent; - double div = Math.Pow(10, (int)pfx - exponent); - - if (exp < 0) - return v => (v/div).ToString("F" + (sigFigs-1)); - if (exp >= sigFigs) - { - double mult = Math.Pow(10, exp - sigFigs + 1); - return v => (Math.Round(v / div / mult) * mult).ToString("F0"); - } - return v => (v/div).ToString("F" + (sigFigs - exp - 1)); - } - - - /// - /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 - /// - public static string FormatMass(float mass, int sigFigs = 4, int exponent = 0) - { - return mass < 1.0f ? - mass.ToStringSI(sigFigs, exponent + 6, "g") : - mass.ToStringSI(sigFigs, exponent, "t"); - } - - /// - /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 - /// - public static string FormatMass(double mass, int sigFigs = 4, int exponent = 0) - { - return mass < 1.0f ? - mass.ToStringSI(sigFigs, exponent+6, "g") : - mass.ToStringSI(sigFigs, exponent, "t"); - } - } - - /// - /// SI Prefix. If cast as an int, the base 10 log for the prefix will be produced. - /// - public enum SIPrefix - { - None = 0, - Kilo = 3, - Mega = 6, - Giga = 9, - Tera = 12, - Peta = 15, - Exa = 18, - Zotta = 21, - Yotta = 24, - Milli = -3, - Micro = -6, - Nano = -9, - Pico = -12, - Femto = -15, - Atto = -18, - Zepto = -21, - Yocto = -24 - } + public static class MathUtils + { + + /// + /// Clamp a value between the max and min values. If < min it will be changed to min, if > max it will be changed to max. + /// + /// Value to clamp + /// Minimum + /// Maximum + /// true if the value has been clamped. + public static bool TestClamp(ref float value, float min, float max) + { + if (value < min) + { + value = min; + return true; + } + if (value > max) + { + value = max; + return true; + } + return false; + } + + /// + /// Round value to the nearest. + /// + /// Value to round + /// Precision, eg 0.1, 0.02, 0.005 + /// The rounded value + public static float RoundTo(float value, float precision) + { + return Mathf.Round(value / precision) * precision; + } + + /// + /// Format a numeric value using SI prefexes. + /// + /// eg: 13401 -> 13.4 k + /// + /// + /// value to format + /// unit string + /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. + /// number of signifigant figures to display + /// + public static string ToStringSI(this double value, int sigFigs = 3, int exponent = 0, string unit = null) + { + SIPrefix prefix = value.GetSIPrefix(exponent); + return prefix.FormatSI(value, sigFigs, exponent, unit); + } + + /// + /// Format a numeric value using SI prefexes. + /// + /// eg: 13401 -> 13.4 k + /// + /// + /// value to format + /// unit string + /// Exponennt to the existing value. eg: if value was km rather than m this would be 3. + /// number of signifigant figures to display + /// + public static string ToStringSI(this float value, int sigFigs = 3, int exponent = 0, string unit = null) + { + SIPrefix prefix = value.GetSIPrefix(exponent); + return prefix.FormatSI(value, sigFigs, exponent, unit); + } + + internal static string FormatSI(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0, string unit = null) + { + return string.Format("{0}{1}{2}", pfx.GetFormatter(value, sigFigs, exponent)(value), pfx.PrefixString(), unit); + } + + /// + /// This extension of the standard string format method has available an extra standard format. + /// + /// Examples: + /// + /// 12.ToStringExt("S") -> "12" + /// 12.ToStringExt("S3") -> "12.0" + /// 120.ToStringExt("S3") -> "120" + /// 1254.ToStringExt("S3") -> "1250" (4 digit numbers do not use k as a special case) + /// 12540.ToStringExt("S3") -> "1.25k" (using SI prefixes) + /// 12540.ToStringExt("S4") -> "1.254k" (more significant figures) + /// (1.254).ToStringExt("S4+3") -> "1.254k" (+3 means the 'natural prefix' is k) + /// (1.254).ToStringExt("S4-3") -> "1.254m" (-3 means the 'natural prefix' is m) + /// + /// + public static string ToStringExt(this double value, string format) + { + if (format.Length < 1) + { + return value.ToString(); + } + + if (format[0] == 'S' || format[0] == 's') + { + if (format.Length == 1) + return ToStringSI(value, 0); + int pmi = format.IndexOf('+'); + int sigFigs; + if (pmi < 0) + { + pmi = format.IndexOf('-'); + if (pmi < 0) + { + sigFigs = int.Parse(format.Substring(1)); + return ToStringSI(value, sigFigs); + } + } + sigFigs = int.Parse(format.Substring(1, pmi - 1)); + int exponent = int.Parse(format.Substring(pmi)); + return ToStringSI(value, sigFigs, exponent); + } + return value.ToString(format); + } + + /// + /// ToStringExt for floats. See doc for doubles. + /// + public static string ToStringExt(this float value, string format) + { + return ToStringExt((double)value, format); + } + + /// + /// Parse a string in SI format - with SI unit prefix to a double. + /// Note - units must not be present. + /// + public static bool TryParseExt(string str, out double value) + { + str = str.Trim(); + if(str.Length == 0) { + value = 0; + return false; + } + + char last = str[str.Length - 1]; + + if (char.IsDigit(last)) + return double.TryParse(str, out value); + + double exponent; + int newLen = str.Length - 1; + switch (last) + { + case 'k': exponent = 1e3; break; + case 'M': exponent = 1e6; break; + case 'G': exponent = 1e9; break; + case 'T': exponent = 1e12; break; + case 'P': exponent = 1e15; break; + case 'E': exponent = 1e18; break; + case 'Z': exponent = 1e21; break; + case 'Y': exponent = 1e24; break; + case 'm': + exponent = 1e-3; break; + case '\x3bc': + case '\xb5': + case 'u': + exponent = 1e-6; break; + case 'c': + if (!str.EndsWith("mic")) + { + value = 0; + return false; + } + newLen = str.Length - 3; + exponent = 1e-6; + break; + case 'n': exponent = 1e-9; break; + case 'p': exponent = 1e-12; break; + case 'f': exponent = 1e-15; break; + case 'a': exponent = 1e-18; break; + case 'z': exponent = 1e-21; break; + case 'y': exponent = 1e-24; break; + default: + value = 0; + return false; + } + + double parsed; + if (!double.TryParse(str.Substring(0, newLen), out parsed)) + { + value = 0; + return false; + } + + value = parsed * exponent; + return true; + } + + /// + /// Parse a string in SI format - with SI unit prefix to a float. + /// Note - units must not be present. + /// + public static bool TryParseExt(string str, out float value) + { + double dVal; + if (TryParseExt(str, out dVal)) + { + value = (float)dVal; + return true; + } + value = 0; + return false; + } + + /// + /// Round a number to a set number of significant figures. + /// + /// number to round + /// number of significant figures, defaults to 3 + /// + public static float RoundSigFigs(this float d, int sigFigs = 3) + { + + int exponent = (int)Math.Floor(Math.Log10(Math.Abs(d))) - sigFigs; + float div = Mathf.Pow(10, exponent); + return Mathf.Round(d / div) * div; + } + + /// + /// Round a number to a set number of significant figures. + /// + /// number to round + /// number of significant figures, defaults to 3 + /// + public static double RoundSigFigs(this double value, int sigFigs = 3) + { + int exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))) - sigFigs; + double div = Mathf.Pow(10, exponent); + return Math.Round(value / div) * div; + } + + /// + /// Find the SI prefix for a number + /// + /// The value to find the prefix for + /// The natural exponent, if your value was km rather than m, use 3 + public static SIPrefix GetSIPrefix(this double value, int exponent = 0) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (value == 0) + return SIPrefix.None; + + int exp = (int)Math.Floor(Math.Log10(Math.Abs(value))) + exponent; + + if (exp <= 3 && exp >= -1) + return SIPrefix.None; + if (exp < 0) + return (SIPrefix)((exp-2) / 3 * 3); + return (SIPrefix)(exp / 3 * 3); + } + + /// + /// Find the SI prefix for a number + /// + public static SIPrefix GetSIPrefix(this float value, int exponent = 0) + { + return GetSIPrefix((double)value, exponent); + } + + /// + /// The prefix string for a particular exponent. Note Micro will use the string "mic" rather than "µ", so is longer. + /// + public static string PrefixString(this SIPrefix pfx) + { + switch (pfx) + { + case SIPrefix.None: return ""; + case SIPrefix.Kilo: return "k"; + case SIPrefix.Mega: return "M"; + case SIPrefix.Giga: return "G"; + case SIPrefix.Tera: return "T"; + case SIPrefix.Peta: return "P"; + case SIPrefix.Exa: return "E"; + case SIPrefix.Zotta: return "Z"; + case SIPrefix.Yotta: return "Y"; + case SIPrefix.Milli: return "m"; + case SIPrefix.Micro: return "u"; + case SIPrefix.Nano: return "n"; + case SIPrefix.Pico: return "p"; + case SIPrefix.Femto: return "f"; + case SIPrefix.Atto: return "a"; + case SIPrefix.Zepto: return "z"; + case SIPrefix.Yocto: return "y"; + default: throw new ArgumentException("Illegal prefix", "pfx"); + } + } + + /// + /// Round a value with respect the the given SI prefix. + /// + /// eg: + /// SIPrefix.Mega.Round(34.456e6f, digits:1) -> 34.6e6f + /// + /// + /// The SI prefix + /// Value to round + /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) + /// Natural exponent of value, eg: use 3 if value is km rather than m + /// + public static float Round(this SIPrefix pfx, float value, int digits = 3, int exponent = 0) + { + float div = Mathf.Pow(10, (int)pfx - digits + exponent); + return Mathf.Round(value / div) * div; + } + + /// + /// Round a value with respect the the given SI prefix. + /// + /// eg: + /// SIPrefix.Mega.Round(34.456e6, digits:1) -> 34.6e6 + /// + /// + /// The SI prefix + /// Value to round + /// Number of decimal places. Negatives will work (eg: -1 rounds to nearest 10) + /// Natural exponent of value, eg: use 3 if value is km rather than m + /// + public static double Round(this SIPrefix pfx, double value, int digits = 3, int exponent = 0) + { + double div = Math.Pow(10, (int)pfx - digits + exponent); + return Math.Round(value / div) * div; + } + + /// + /// Get a formatter function. This is useful when you have a range of values you'd like formatted the same way. + /// + public static Func GetFormatter(this SIPrefix pfx, double value, int sigFigs = 3, int exponent = 0) + { + int exp = (int)(Math.Floor(Math.Log10(Math.Abs(value)))) - (int)pfx + exponent; + double div = Math.Pow(10, (int)pfx - exponent); + + if (exp < 0) + return v => (v/div).ToString("F" + (sigFigs-1)); + if (exp >= sigFigs) + { + double mult = Math.Pow(10, exp - sigFigs + 1); + return v => (Math.Round(v / div / mult) * mult).ToString("F0"); + } + return v => (v/div).ToString("F" + (sigFigs - exp - 1)); + } + + + /// + /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 + /// + public static string FormatMass(float mass, int sigFigs = 4, int exponent = 0) + { + return mass < 1.0f ? + mass.ToStringSI(sigFigs, exponent + 6, "g") : + mass.ToStringSI(sigFigs, exponent, "t"); + } + + /// + /// Formats a mass in tons as either tons if >= 1.0, or as grams if < 1.0 + /// + public static string FormatMass(double mass, int sigFigs = 4, int exponent = 0) + { + return mass < 1.0f ? + mass.ToStringSI(sigFigs, exponent+6, "g") : + mass.ToStringSI(sigFigs, exponent, "t"); + } + } + + /// + /// SI Prefix. If cast as an int, the base 10 log for the prefix will be produced. + /// + public enum SIPrefix + { + None = 0, + Kilo = 3, + Mega = 6, + Giga = 9, + Tera = 12, + Peta = 15, + Exa = 18, + Zotta = 21, + Yotta = 24, + Milli = -3, + Micro = -6, + Nano = -9, + Pico = -12, + Femto = -15, + Atto = -18, + Zepto = -21, + Yocto = -24 + } } \ No newline at end of file diff --git a/Source/KSPAPIExtras/CommonAPI/ResourceExtensions.cs b/Source/KSPAPIExtras/CommonAPI/ResourceExtensions.cs index 45fb7cdb..31d5cfd9 100644 --- a/Source/KSPAPIExtras/CommonAPI/ResourceExtensions.cs +++ b/Source/KSPAPIExtras/CommonAPI/ResourceExtensions.cs @@ -8,162 +8,162 @@ using KSP.IO; namespace KSPAPIExtensions { - /// - /// Extended resource attributes - /// - public class ExtendedResourceDefinition - { - PartResourceDefinition res_def; + /// + /// Extended resource attributes + /// + public class ExtendedResourceDefinition + { + PartResourceDefinition res_def; - public bool isMineable = false; - public bool isHullResource = false; - public float volume = 5; // liters per unit + public bool isMineable = false; + public bool isHullResource = false; + public float volume = 5; // liters per unit - public string name - { - get { - return res_def.name; - } - } - public Color color - { - get { - return res_def.color; - } - } - public float density - { - get { - return res_def.density; - } - } - public int id - { - get { - return res_def.id; - } - } - public bool isTweakable - { - get { - return res_def.isTweakable; - } - } - public ResourceFlowMode resourceFlowMode - { - get { - return res_def.resourceFlowMode; - } - } - public ResourceTransferMode resourceTransferMode - { - get { - return res_def.resourceTransferMode; - } - } + public string name + { + get { + return res_def.name; + } + } + public Color color + { + get { + return res_def.color; + } + } + public float density + { + get { + return res_def.density; + } + } + public int id + { + get { + return res_def.id; + } + } + public bool isTweakable + { + get { + return res_def.isTweakable; + } + } + public ResourceFlowMode resourceFlowMode + { + get { + return res_def.resourceFlowMode; + } + } + public ResourceTransferMode resourceTransferMode + { + get { + return res_def.resourceTransferMode; + } + } - public void Load (ConfigNode node) - { - string name = node.GetValue ("name"); - res_def = PartResourceLibrary.Instance.GetDefinition (name); - if (node.HasValue ("isMineable")) { - bool.TryParse (node.GetValue ("isMineable"), out isMineable); - } - if (node.HasValue ("isHullResource")) { - bool.TryParse (node.GetValue ("isHullResource"), - out isHullResource); - } - if (node.HasValue ("volume")) { - float.TryParse (node.GetValue ("volume"), out volume); - } - } - } + public void Load (ConfigNode node) + { + string name = node.GetValue ("name"); + res_def = PartResourceLibrary.Instance.GetDefinition (name); + if (node.HasValue ("isMineable")) { + bool.TryParse (node.GetValue ("isMineable"), out isMineable); + } + if (node.HasValue ("isHullResource")) { + bool.TryParse (node.GetValue ("isHullResource"), + out isHullResource); + } + if (node.HasValue ("volume")) { + float.TryParse (node.GetValue ("volume"), out volume); + } + } + } - /// - /// Extended resource attributes - /// - public static class PartResourceDefinitionExtension - { - static Dictionary resource_dict; + /// + /// Extended resource attributes + /// + public static class PartResourceDefinitionExtension + { + static Dictionary resource_dict; - /// - /// List of all resources as extended resources. The list is a private - /// copy. - /// - public static List resources - { - get { - if (resource_dict == null) { - Initialize (); - } - return new List (resource_dict.Values); - } - } + /// + /// List of all resources as extended resources. The list is a private + /// copy. + /// + public static List resources + { + get { + if (resource_dict == null) { + Initialize (); + } + return new List (resource_dict.Values); + } + } - /// - /// Find and load all resources as extended resources. - /// - static void Initialize () - { - var dbase = GameDatabase.Instance; - var resourceNodes = dbase.GetConfigNodes ("RESOURCE_DEFINITION"); - resource_dict = new Dictionary (); - foreach (var resource in resourceNodes) { - var res = new ExtendedResourceDefinition (); - res.Load (resource); - resource_dict.Add (res.name, res); - } - } + /// + /// Find and load all resources as extended resources. + /// + static void Initialize () + { + var dbase = GameDatabase.Instance; + var resourceNodes = dbase.GetConfigNodes ("RESOURCE_DEFINITION"); + resource_dict = new Dictionary (); + foreach (var resource in resourceNodes) { + var res = new ExtendedResourceDefinition (); + res.Load (resource); + resource_dict.Add (res.name, res); + } + } - /// - /// Get an extended resource. - /// - /// Resource name - /// An extended resource defintiion or null if not found - public static ExtendedResourceDefinition GetResource (string name) - { - if (resource_dict == null) { - Initialize (); - } - if (resource_dict.ContainsKey (name)) { - return resource_dict[name]; - } - return null; - } + /// + /// Get an extended resource. + /// + /// Resource name + /// An extended resource defintiion or null if not found + public static ExtendedResourceDefinition GetResource (string name) + { + if (resource_dict == null) { + Initialize (); + } + if (resource_dict.ContainsKey (name)) { + return resource_dict[name]; + } + return null; + } - /// - /// Can the resource be mined? That is, is this a raw resource (eg, - /// Kethane, ore, etc). - /// - /// The resource defintion - /// Whether the resource can be mined. - public static bool isMineable (this PartResourceDefinition resdef) - { - return GetResource (resdef.name).isMineable; - } + /// + /// Can the resource be mined? That is, is this a raw resource (eg, + /// Kethane, ore, etc). + /// + /// The resource defintion + /// Whether the resource can be mined. + public static bool isMineable (this PartResourceDefinition resdef) + { + return GetResource (resdef.name).isMineable; + } - /// - /// Is this a resource needed for building ship "hulls". Used primarily - /// by Extraplanetary Launchpads. - /// - /// The resource defintion - /// Whether the resource is mull material. - public static bool isHullResource (this PartResourceDefinition resdef) - { - return GetResource (resdef.name).isHullResource; - } + /// + /// Is this a resource needed for building ship "hulls". Used primarily + /// by Extraplanetary Launchpads. + /// + /// The resource defintion + /// Whether the resource is mull material. + public static bool isHullResource (this PartResourceDefinition resdef) + { + return GetResource (resdef.name).isHullResource; + } - /// - /// The amount of space in liters taken up by one unit of the resource. - /// 5l/u is common (LiquidFuel, Oxidizer, Ore, Metal), though RealFuels - /// will likely set it to 1l/u. - /// - /// The resource defintion - /// The volume of a single unit of the resource in liters - public static float getVolume (this PartResourceDefinition resdef) - { - return GetResource (resdef.name).volume; - } - } + /// + /// The amount of space in liters taken up by one unit of the resource. + /// 5l/u is common (LiquidFuel, Oxidizer, Ore, Metal), though RealFuels + /// will likely set it to 1l/u. + /// + /// The resource defintion + /// The volume of a single unit of the resource in liters + public static float getVolume (this PartResourceDefinition resdef) + { + return GetResource (resdef.name).volume; + } + } } From 72194aa4ba245f967836c91d93b5d6b9f9975be7 Mon Sep 17 00:00:00 2001 From: toadicus Date: Wed, 20 May 2015 11:54:19 -0700 Subject: [PATCH 14/18] Re-added 'Dummy' part for use with KSPAPIExtras --- GameData/000Dummy.cfg | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 GameData/000Dummy.cfg diff --git a/GameData/000Dummy.cfg b/GameData/000Dummy.cfg new file mode 100644 index 00000000..4df8d534 --- /dev/null +++ b/GameData/000Dummy.cfg @@ -0,0 +1,25 @@ + +// This part is here so that database reloads will correctly run ModuleManager and PartMessageManager +// on the first defined part. If you don't have this then the first part has issues. + +PART { + name = dummyPartIgnore + module = Part + + // --- asset parameters --- + MODEL + { + model = ProceduralParts/Parts/cylinderTank + scale = 1,1,1 + } + + scale = 1 + rescaleFactor = 1 + + cost = 4000 + category = -1 + TechRequired = InaccessableNonExistentTech + entryCost = 15000 + subcategory = 0 + +} From 0e1e36456ae2ed6a47229415722e1f36f28933e9 Mon Sep 17 00:00:00 2001 From: toadicus Date: Wed, 20 May 2015 12:08:08 -0700 Subject: [PATCH 15/18] Reverted to the old readme and added a split to KSPAPIExtras in the middle. --- README.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4c60c7b3..1b51f6b4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -KSPAPIEL +KSPAPIExtensions ================ -A smaller set of utilities for plugin developers for Kerbal Space Program - -Forked from KSPAPIExtensions by taniwha, swamp_ig, et al. +Extensions and utilities for Kerbal Space Program This add in is useful for providing some functions that make interacting with the KSP API functionally nicer and with an improved interface. @@ -36,6 +34,31 @@ Utility methods to determine relationships between parts, plus some debugging co Available is two extra tweakable controls, plus improvements to the stock tweakers. To use these you *must* include the KSIAPIUtils.dll in your project rather than just copying the code as there's an election process to ensure the latest version is being run. If backwards compatibility breaks, I will ensure that the user is warned to upgrade plugins. +## SI Formatting for label fields. + +Just use an 'S' type format code and SI prefixes will be handled. + +````c# + [KSPField(guiActive = true, guiActiveEditor = true, guiName = "Volume", guiFormat = "S3+3", guiUnits = "L")] + public float tankVolume = 0.0f; +```` + +For this example, the tankVolume variable is in kL, and will be displayed with 3 sig figs of accuracy. + +## SI Formatting and dynamic updating for resources in the editor + +This happens transparently. You can change the amount and maxAmount variables in your PartResource object in the editor, and they will be updated in the tweaker. + +The amount and max amount are displayed with four significant figures of accuracy. + +If you add and remove resources, you will need to force a refresh of the part tweaker window like this: + +````c# + UIPartActionWindow window = part.FindActionWindow(); + if (window != null) + window.displayDirty = true; +```` + ## UI_ChooseOption This allows the user to chose from a range of options. It's equivalent to a dropdown list only without the dropdown (dropdowns were difficult to do with the API). @@ -83,3 +106,93 @@ if incrementSmall is not set, then no button is visible in the control. If incre The slider is set to run between the smallest available increment, so in the above if the current value was 1.3, then the slider would run from 1.25 to 1.5. + +KSPAPIExtras +================ +KSPAPIExtras is a set of additional tools built on the KSPAPIExtensions framework. It ships in a separate assembly and depends on KSPAPIExtensions. +# PartMessage + +This is an improvement over the existing SendMessage system of Unity. It creates a publish/subscribe model for messages both within and between parts on a vessel. It will automatically wire message event senders and listeners in a PartModule or Part if they have the appropriate attributes. + +There's also a set of common messages defined, so addon builders can send and listen for those events in their code. + +Once again, to use these you *must* include the KSIAPIUtils.dll in your project rather than just copying the code as there's an election process to ensure the latest version is being run. If backwards compatibility breaks, I will ensure that the user is warned to upgrade plugins. + +## Initialization of message system + +Any class or module that uses part messages must initialize the system. The easiest way is like this: + +````c# + public abstract class ProceduralAbstractShape : PartModule + { + public override void OnAwake() + { + base.OnAwake(); + PartMessageService.Register(this); + } +```` + +You need to do this in OnAwake, as the part variable needs to be initialized. + +## Part messages + +Event types are declared as delegates with a special marker interface: + +````c# + [PartMessage(isAbstract: true)] + public delegate void PartPhysicsChanged(); + + [PartMessageDelegate(typeof(PartPhysicsChanged))] + public delegate void PartMassChanged([UseLatest] float mass); +```` + +These two events are in the common library. There's no constraints on the argument list, however you do not need to have the sending part as an argument as that is handled. + +Note that the PartMassChanged message has a parent event - PartPhysicsChanged. If you ever raise a PartMassChanged message then any listeners for PartPhysicsChanged will also be informed. The isAbstract flag indicates this should not be sent directly as an event. Currently this is not enforced but may be in the future. + +The argument list for any parent should either be the same, or a truncation of the list for the child event. Truncation is handled gracefully. + +Note the [UseLatest] attribute for the PartMassChanged event. This indicates that if a whole pile of PartMassChanged events gets sent from the same source that differ only in their mass arguments, then these can be consolidated into one event where the mass is the last one recieved. + +````c# + [PartMessageDelegate(typeof(PartResourcesChanged))] + public delegate void PartResourceMaxAmountChanged(string resource, [UseLatest] double maxAmount); +```` + +Above is an example of where this is not the case, the resource name uniquely identifies a message, however multiple messages with the same source but differ only in the maxAmount can be consolidated. + +## Message sending + +In your PartModule and Part objects, you can just declare and use an event like so: + +````c# + [PartMessageEvent] + public event PartMassChanged MassChanged; + + private void UpdateMass(float value) + { + part.mass = mass; + MassChanged(mass); + } +```` + +If you can send the update message from some other source, see the code for details of how to do it. + +## Message receiving + +You can listen for messages by just declaring a method with a marker attribute: + +````c# + [PartMessageListener(typeof(PartMassChanged), scenes:GameSceneFilter.AnyEditor, relations:PartRelationship.Self)] + private void ChangeVolume(float volume) +```` + +Note the two optional filtering properties. + +*scenes:* this allows you to only listen in certain game scenes. Good ones to use are GameSceneFilter.AnyEditor for editor mode, and GameSceneFilter.Flight for flight mode. Note that messages can still get sent during the GetInfo phase of initialization, which is useful if you'd like to tweak the VAB icon. + +*relations:* If not specified, the default is to only listen for messages from the same part, however you can listen to messages from Parents, Siblings, Ancestors... others. + +## Advanced message management + +You can filter messages, buffer them up and send them in one hit, and send them dynamically. I won't go into the full details in this document. Have a look at the source for more details. From e8b78f3a893df6d45d1d33250a09a5ac91735b56 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 20 May 2015 21:07:16 -0700 Subject: [PATCH 16/18] Removed an extra file we didn't need. --- Source/KSPAPIExtensions/Properties/AssemblyInfo.cs | 2 +- Source/KSPAPIExtras/KSPAPIExtras.csproj | 1 - Source/KSPAPIExtras/Properties/AssemblyInfo.cs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Source/KSPAPIExtensions/Properties/AssemblyInfo.cs b/Source/KSPAPIExtensions/Properties/AssemblyInfo.cs index 742014fd..1f372dcb 100644 --- a/Source/KSPAPIExtensions/Properties/AssemblyInfo.cs +++ b/Source/KSPAPIExtensions/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("1.7.4.10")] +[assembly: AssemblyVersion("1.7.4.15")] [assembly: KSPAssembly("KSPAPIExtensions", 1, 7)] diff --git a/Source/KSPAPIExtras/KSPAPIExtras.csproj b/Source/KSPAPIExtras/KSPAPIExtras.csproj index 5a1bf5e5..255d038e 100644 --- a/Source/KSPAPIExtras/KSPAPIExtras.csproj +++ b/Source/KSPAPIExtras/KSPAPIExtras.csproj @@ -70,7 +70,6 @@ - diff --git a/Source/KSPAPIExtras/Properties/AssemblyInfo.cs b/Source/KSPAPIExtras/Properties/AssemblyInfo.cs index 742014fd..1f372dcb 100644 --- a/Source/KSPAPIExtras/Properties/AssemblyInfo.cs +++ b/Source/KSPAPIExtras/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("1.7.4.10")] +[assembly: AssemblyVersion("1.7.4.15")] [assembly: KSPAssembly("KSPAPIExtensions", 1, 7)] From 0572696da7bf3f1306ce18f92b20e0098cd0c8e0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 20 May 2015 21:08:13 -0700 Subject: [PATCH 17/18] Removed KSPAPIEL references from the version-gen scripts --- Source/KSPAPIExtensions/version-gen-lite | 2 +- Source/KSPAPIExtras/version-gen-ext | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/KSPAPIExtensions/version-gen-lite b/Source/KSPAPIExtensions/version-gen-lite index ab59fdeb..280e38cf 100644 --- a/Source/KSPAPIExtensions/version-gen-lite +++ b/Source/KSPAPIExtensions/version-gen-lite @@ -29,7 +29,7 @@ gen_tagged_class_wrappers() { echo "using System.Linq;" echo "using System.Reflection;" echo "using UnityEngine;" - echo "namespace KSPAPIEL {" + echo "namespace KSPAPIExtensions {" echo " [KSPAddon(KSPAddon.Startup.Instantly, true)]" gen_tagged_class UIPartActionsExtendedRegistration gen_tagged_class UIPartActionResourceEditorImproved diff --git a/Source/KSPAPIExtras/version-gen-ext b/Source/KSPAPIExtras/version-gen-ext index 957533f3..ba0d4fcd 100644 --- a/Source/KSPAPIExtras/version-gen-ext +++ b/Source/KSPAPIExtras/version-gen-ext @@ -24,7 +24,6 @@ gen_tagged_class() { } gen_tagged_class_wrappers() { - echo "using KSPAPIEL;" echo "using System;" echo "using System.Collections.Generic;" echo "using System.Linq;" From fd1fb8d340bb1eefd0e8adac37ef97074106439b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 20 May 2015 21:10:46 -0700 Subject: [PATCH 18/18] Added .dll files for ease of distribution --- GameData/KSPAPIExtensions.dll | Bin 0 -> 46080 bytes GameData/KSPAPIExtras.dll | Bin 0 -> 91648 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100755 GameData/KSPAPIExtensions.dll create mode 100755 GameData/KSPAPIExtras.dll diff --git a/GameData/KSPAPIExtensions.dll b/GameData/KSPAPIExtensions.dll new file mode 100755 index 0000000000000000000000000000000000000000..067b3532e789d0940a2636af1f27fa6e96eaba60 GIT binary patch literal 46080 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

3!HHk0s9}c{v$Nun<{f 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