diff --git a/doc/source/structures/vessels/partmodule.rst b/doc/source/structures/vessels/partmodule.rst index 162eb9910..bf4abe489 100644 --- a/doc/source/structures/vessels/partmodule.rst +++ b/doc/source/structures/vessels/partmodule.rst @@ -29,9 +29,15 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha * - :attr:`ALLFIELDS` - :struct:`List` of strings - Accessible fields + * - :attr:`ALLHIDDENFIELDS` + - :struct:`List` of strings + - Fields not normally accessible via GUI * - :attr:`ALLFIELDNAMES` - :struct:`List` of strings - Accessible fields (name only) + * - :attr:`ALLHIDDENFIELDNAMES` + - :struct:`List` of strings + - Fields not normally accessible via GUI (name only) * - :attr:`ALLEVENTS` - :struct:`List` of strings - Triggerable events @@ -47,6 +53,9 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha * - :meth:`GETFIELD(name)` - - Get value of a field by name + * - :meth:`GETHIDDENFIELD(name)` + - + - Get value of a hidden field by name * - :meth:`SETFIELD(name,value)` - - Set value of a field by name @@ -59,6 +68,9 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha * - :meth:`HASFIELD(name)` - :struct:`Boolean` - Check if field exists + * - :meth:`HASHIDDENFIELD(name)` + - :struct:`Boolean` + - Check if a hidden field exists * - :meth:`HASEVENT(name)` - :struct:`Boolean` - Check if event exists @@ -90,12 +102,26 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha Get a list of all the names of KSPFields on this PartModule that the kos script is CURRENTLY allowed to get or set with :GETFIELD or :SETFIELD. Note the Security access comments below. This list can become obsolete as the game continues running depending on what the PartModule chooses to do. +.. attribute:: PartModule:ALLHIDDENFIELDS + + :access: Get only + :test: :struct:`List` of strings + + Get a list of all the names of KSPFields on this PartModule that are not normally displayed in the GUI. The returned field names should be used with :GETHIDDENFIELD and cannot be used with :SETFIELD. Note the Security access comments below. This list can become obsolete as the game continues running depending on what the PartModule chooses to do. + .. attribute:: PartModule:ALLFIELDNAMES :access: Get only :test: :struct:`List` of strings Similar to :ALLFIELDS except that it returns the string without the formatting to make it easier to use in a script. This list can become obsolete as the game continues running depending on what the PartModule chooses to do. + +.. attribute:: PartModule:ALLHIDDENFIELDNAMES + + :access: Get only + :test: :struct:`List` of strings + + Similar to :ALLHIDDENFIELDS except that it returns the string without the formatting to make it easier to use in a script. This list can become obsolete as the game continues running depending on what the PartModule chooses to do. .. attribute:: PartModule:ALLEVENTS @@ -132,6 +158,13 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha Get the value of one of the fields that this PartModule has placed onto the right-click menu for the part. Note the Security comments below. +.. method:: PartModule:GETHIDDENFIELD(name) + + :parameter name: (:struct:`String`) Name of the field + :return: varies + + Get the value of one of the hidden(internal) PartModule fields that aren't normally displayed in the right-click menu for the part. Note the Security comments below. + .. method:: PartModule:SETFIELD(name,value) :parameter name: (:struct:`String`) Name of the field @@ -177,6 +210,13 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha Return true if the given field name is currently available for use with :GETFIELD or :SETFIELD on this PartModule, false otherwise. +.. method:: PartModule:HASHIDDENFIELD(name) + + :parameter name: (:struct:`String`) Name of the field + :return: :struct:`Boolean` + + Return true if the given hidden(internal) field is present in the PartModule, false otherwise. + .. method:: PartModule:HASEVENT(name) :parameter name: (:struct:`String`) Name of the event @@ -197,19 +237,20 @@ Notes ----- In all the above cases where there is a name being passed in to :GETFIELD, :SETFIELD, :DOEVENT, or :DOACTION, the name is meant to be the name that is seen by you, the user, in the GUI screen, and NOT necessarily the actual name of the variable that the programmer of that PartModule chose to call the value behind the scenes. This is so that you can view the GUI right-click menu to see what to call things in your script. +When it comes to the hidden fields, they can be inspected using the :ALLHIDDENFIELDS method, or by looking into the source code. Generally, manipulating the hidden fields is recommended for experienced users only. .. note:: **Security and Respecting other Mod Authors** - There are often a lot more fields and events and actions that a partmodule can do than are usable via kOS. In designing kOS, the kOS developers have deliberately chosen NOT to expose any "hidden" fields of a partmodule that are not normally shown to the user, without the express permission of a mod's author to do so. + It was decided that providing the kOS users with a read-only access to the part module's hidden fields would allow to create even better scripts and wouldn't do any harm. Obviously, manipulating the module's internal state would result in all kind of troubles, so this is forbidden. The access rules that kOS uses are as follows: KSPFields ~~~~~~~~~ -Is this a value that the user can normally see on the right-click context menu for a part? If so, then let kOS scripts GET the value. Is this a value that the user can normally manipulate via "tweakable" adjustments on the right-click context menu for a part, AND, is that tweakable a CURRENTLY enabled one? If so, then let KOS scripts SET the value, BUT they must set it to one of the values that the GUI would normally allow, according to the following rules. +Is this a value that the user can normally see on the right-click context menu for a part? If so, then let kOS scripts GET the value using the :GETFIELD method. Is this a value that represents the part's internal state and not normally displayed in the GUI? Then the :GETHIDDENFIELD method should be used. Is this a value that the user can normally manipulate via "tweakable" adjustments on the right-click context menu for a part, AND, is that tweakable a CURRENTLY enabled one? If so, then let KOS scripts SET the value, BUT they must set it to one of the values that the GUI would normally allow, according to the following rules. - If the KSPField is boolean: - The value must be true, false, or 0 or 1. diff --git a/src/kOS/AddOns/RemoteTech/RemoteTechAntennaModuleFields.cs b/src/kOS/AddOns/RemoteTech/RemoteTechAntennaModuleFields.cs index ff9042fec..8266420a5 100644 --- a/src/kOS/AddOns/RemoteTech/RemoteTechAntennaModuleFields.cs +++ b/src/kOS/AddOns/RemoteTech/RemoteTechAntennaModuleFields.cs @@ -1,4 +1,4 @@ -using kOS.Safe.Encapsulation; +using kOS.Safe.Encapsulation; using kOS.Safe.Encapsulation.Suffixes; using kOS.Safe.Exceptions; using kOS.Suffixed; diff --git a/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs b/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs index 9ec14cdf6..f2dc78c69 100644 --- a/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs +++ b/src/kOS/Suffixed/PartModuleField/PartModuleFields.cs @@ -183,7 +183,7 @@ protected virtual ListValue AllFields(string formatter) { var returnValue = new ListValue(); - IEnumerable visibleFields = partModule.Fields.Cast().Where(FieldIsVisible); + IEnumerable visibleFields = partModule.Fields.Cast().Where(field => FieldIsVisible(field)); foreach (BaseField field in visibleFields) { @@ -199,16 +199,42 @@ protected virtual ListValue AllFields(string formatter) return returnValue; } + /// + /// Return a list of all the strings of all KSPfields registered to this PartModule + /// which are currently NOT showing on the part's RMB menu. + /// + /// List of all the strings field names. + protected ListValue AllHiddenFields(string formatter) + { + var returnValue = new ListValue(); + + IEnumerable hiddenFields = partModule.Fields.Cast().Where(field => FieldIsVisible(field, false)); + + foreach (BaseField field in hiddenFields) + { + returnValue.Add(new StringValue(string.Format(formatter, + "get-only", + GetFieldName(field).ToLower(), + Utilities.Utils.KOSType(field.FieldInfo.FieldType)))); + } + return returnValue; + } + /// /// Return a list of all the strings of all KSPfields registered to this PartModule /// which are currently showing on the part's RMB menu, without formating. /// /// List of all the strings field names. protected virtual ListValue AllFieldNames() + { + return AllFieldNames(field => FieldIsVisible(field)); + } + + private ListValue AllFieldNames(Func visibilityFilterPredicate) { var returnValue = new ListValue(); - IEnumerable visibleFields = partModule.Fields.Cast().Where(FieldIsVisible); + IEnumerable visibleFields = partModule.Fields.Cast().Where(visibilityFilterPredicate); foreach (BaseField field in visibleFields) { @@ -228,12 +254,28 @@ public virtual BooleanValue HasField(StringValue fieldName) return FieldIsVisible(GetField(fieldName)); } + /// + /// Determine if the Partmodule has this KSPField on it, which is currently + /// NOT showing on the part's RMB menu: + /// + /// The field to search for + /// true if it is on the PartModule, false if it is not + public BooleanValue HasHiddenField(StringValue fieldName) + { + return GetField(fieldName, field => FieldIsVisible(field, false)) != null; + } + /// /// Return the field itself that goes with the name (the BaseField, not the value). /// /// The case-insensitive guiName (or name if guiname is empty) of the field. /// a BaseField - a KSP type that can be used to get the value, or its GUI name or its reflection info. protected BaseField GetField(string cookedGuiName) + { + return GetField(cookedGuiName, field => FieldIsVisible(field)); + } + + private BaseField GetField(string cookedGuiName, Func visibilityFilterPredicate) { // Conceptually this should be a single hit using FirstOrDefault(), because there should only // be one Field with the given GUI name. But Issue #2666 forced kOS to change it to an array of hits @@ -251,7 +293,7 @@ protected BaseField GetField(string cookedGuiName) // Issue #2666 is handled here. kOS should not return the invisible field when there's // a visible one of the same name it could have picked instead. Only return an invisible // field if there's no visible one to pick. - BaseField preferredMatch = allMatches.FirstOrDefault(field => FieldIsVisible(field)); + BaseField preferredMatch = allMatches.FirstOrDefault(visibilityFilterPredicate); return preferredMatch ?? allMatches.First(); } @@ -417,14 +459,18 @@ private void InitializeSuffixesAfterConstruction() AddSuffix("SETFIELD", new TwoArgsSuffix(SetKSPFieldValue)); AddSuffix("DOEVENT", new OneArgsSuffix(CallKSPEvent)); AddSuffix("DOACTION", new TwoArgsSuffix(CallKSPAction)); + AddSuffix("ALLHIDDENFIELDS", new Suffix(() => AllHiddenFields("({0}) {1}, is {2}"))); + AddSuffix("ALLHIDDENFIELDNAMES", new Suffix(() => AllFieldNames(field => FieldIsVisible(field, false)))); + AddSuffix("HASHIDDENFIELD", new OneArgsSuffix(HasHiddenField)); + AddSuffix("GETHIDDENFIELD", new OneArgsSuffix(argument => GetKSPFieldValue(argument, field => FieldIsVisible(field, false)))); } - private static bool FieldIsVisible(BaseField field) + private bool FieldIsVisible(BaseField field, bool isVisible = true) { - return (field != null) && (HighLogic.LoadedSceneIsEditor ? field.guiActiveEditor : field.guiActive); + return (field != null) && (HighLogic.LoadedSceneIsEditor ? field.guiActiveEditor == isVisible : field.guiActive == isVisible); } - private static bool EventIsVisible(BaseEvent evt) + private bool EventIsVisible(BaseEvent evt) { return (evt != null) && ( (HighLogic.LoadedSceneIsEditor ? evt.guiActiveEditor : evt.guiActive) && @@ -440,11 +486,14 @@ private static bool EventIsVisible(BaseEvent evt) /// protected Structure GetKSPFieldValue(StringValue suffixName) { - BaseField field = GetField(suffixName); + return GetKSPFieldValue(suffixName, field => FieldIsVisible(field)); + } + + private Structure GetKSPFieldValue(StringValue suffixName, Func visibilityFilterPredicate) + { + BaseField field = GetField(suffixName, visibilityFilterPredicate); if (field == null) throw new KOSLookupFailException("FIELD", suffixName, this); - if (!FieldIsVisible(field)) - throw new KOSLookupFailException("FIELD", suffixName, this, true); Structure obj = FromPrimitiveWithAssert(field.GetValue(partModule)); return obj; }