Skip to content

Commit c047c32

Browse files
AntonKuzinAnton Kuzin
andauthored
Fix #3149: Allow kOS users to read the non-gui KSPField properties (#3150)
* Just removed the checks * Refactor it by creating a 3 new suffixes. * Revert changes to the solution properties file * No need to make HasHiddenField virtual * Added the GEtAllHiddenFieldNames suffix. Refactored some inherited methods using function overload to avoid unnecessary changes in the child classes * Refactor FieldISVisible method to prevent nulls from sneaking into the list. * Forgot to update this. * Update the docs * Use GUI instead of UI everywhere * Fix typos. --------- Co-authored-by: Anton Kuzin <[email protected]>
1 parent 3089ecc commit c047c32

File tree

3 files changed

+102
-12
lines changed

3 files changed

+102
-12
lines changed

doc/source/structures/vessels/partmodule.rst

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha
2929
* - :attr:`ALLFIELDS`
3030
- :struct:`List` of strings
3131
- Accessible fields
32+
* - :attr:`ALLHIDDENFIELDS`
33+
- :struct:`List` of strings
34+
- Fields not normally accessible via GUI
3235
* - :attr:`ALLFIELDNAMES`
3336
- :struct:`List` of strings
3437
- Accessible fields (name only)
38+
* - :attr:`ALLHIDDENFIELDNAMES`
39+
- :struct:`List` of strings
40+
- Fields not normally accessible via GUI (name only)
3541
* - :attr:`ALLEVENTS`
3642
- :struct:`List` of strings
3743
- Triggerable events
@@ -47,6 +53,9 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha
4753
* - :meth:`GETFIELD(name)`
4854
-
4955
- Get value of a field by name
56+
* - :meth:`GETHIDDENFIELD(name)`
57+
-
58+
- Get value of a hidden field by name
5059
* - :meth:`SETFIELD(name,value)`
5160
-
5261
- 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
5968
* - :meth:`HASFIELD(name)`
6069
- :struct:`Boolean`
6170
- Check if field exists
71+
* - :meth:`HASHIDDENFIELD(name)`
72+
- :struct:`Boolean`
73+
- Check if a hidden field exists
6274
* - :meth:`HASEVENT(name)`
6375
- :struct:`Boolean`
6476
- Check if event exists
@@ -90,12 +102,26 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha
90102

91103
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.
92104

105+
.. attribute:: PartModule:ALLHIDDENFIELDS
106+
107+
:access: Get only
108+
:test: :struct:`List` of strings
109+
110+
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.
111+
93112
.. attribute:: PartModule:ALLFIELDNAMES
94113

95114
:access: Get only
96115
:test: :struct:`List` of strings
97116

98117
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.
118+
119+
.. attribute:: PartModule:ALLHIDDENFIELDNAMES
120+
121+
:access: Get only
122+
:test: :struct:`List` of strings
123+
124+
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.
99125

100126
.. attribute:: PartModule:ALLEVENTS
101127

@@ -132,6 +158,13 @@ Once you have a :struct:`PartModule`, you can use it to invoke the behaviors tha
132158

133159
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.
134160

161+
.. method:: PartModule:GETHIDDENFIELD(name)
162+
163+
:parameter name: (:struct:`String`) Name of the field
164+
:return: varies
165+
166+
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.
167+
135168
.. method:: PartModule:SETFIELD(name,value)
136169

137170
: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
177210

178211
Return true if the given field name is currently available for use with :GETFIELD or :SETFIELD on this PartModule, false otherwise.
179212

213+
.. method:: PartModule:HASHIDDENFIELD(name)
214+
215+
:parameter name: (:struct:`String`) Name of the field
216+
:return: :struct:`Boolean`
217+
218+
Return true if the given hidden(internal) field is present in the PartModule, false otherwise.
219+
180220
.. method:: PartModule:HASEVENT(name)
181221

182222
:parameter name: (:struct:`String`) Name of the event
@@ -197,19 +237,20 @@ Notes
197237
-----
198238

199239
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.
240+
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.
200241

201242
.. note::
202243

203244
**Security and Respecting other Mod Authors**
204245

205-
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.
246+
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.
206247

207248
The access rules that kOS uses are as follows:
208249

209250
KSPFields
210251
~~~~~~~~~
211252

212-
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.
253+
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.
213254

214255
- If the KSPField is boolean:
215256
- The value must be true, false, or 0 or 1.

src/kOS/AddOns/RemoteTech/RemoteTechAntennaModuleFields.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using kOS.Safe.Encapsulation;
1+
using kOS.Safe.Encapsulation;
22
using kOS.Safe.Encapsulation.Suffixes;
33
using kOS.Safe.Exceptions;
44
using kOS.Suffixed;

src/kOS/Suffixed/PartModuleField/PartModuleFields.cs

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ protected virtual ListValue AllFields(string formatter)
183183
{
184184
var returnValue = new ListValue();
185185

186-
IEnumerable<BaseField> visibleFields = partModule.Fields.Cast<BaseField>().Where(FieldIsVisible);
186+
IEnumerable<BaseField> visibleFields = partModule.Fields.Cast<BaseField>().Where(field => FieldIsVisible(field));
187187

188188
foreach (BaseField field in visibleFields)
189189
{
@@ -199,16 +199,42 @@ protected virtual ListValue AllFields(string formatter)
199199
return returnValue;
200200
}
201201

202+
/// <summary>
203+
/// Return a list of all the strings of all KSPfields registered to this PartModule
204+
/// which are currently NOT showing on the part's RMB menu.
205+
/// </summary>
206+
/// <returns>List of all the strings field names.</returns>
207+
protected ListValue AllHiddenFields(string formatter)
208+
{
209+
var returnValue = new ListValue();
210+
211+
IEnumerable<BaseField> hiddenFields = partModule.Fields.Cast<BaseField>().Where(field => FieldIsVisible(field, false));
212+
213+
foreach (BaseField field in hiddenFields)
214+
{
215+
returnValue.Add(new StringValue(string.Format(formatter,
216+
"get-only",
217+
GetFieldName(field).ToLower(),
218+
Utilities.Utils.KOSType(field.FieldInfo.FieldType))));
219+
}
220+
return returnValue;
221+
}
222+
202223
/// <summary>
203224
/// Return a list of all the strings of all KSPfields registered to this PartModule
204225
/// which are currently showing on the part's RMB menu, without formating.
205226
/// </summary>
206227
/// <returns>List of all the strings field names.</returns>
207228
protected virtual ListValue AllFieldNames()
229+
{
230+
return AllFieldNames(field => FieldIsVisible(field));
231+
}
232+
233+
private ListValue AllFieldNames(Func<BaseField, bool> visibilityFilterPredicate)
208234
{
209235
var returnValue = new ListValue();
210236

211-
IEnumerable<BaseField> visibleFields = partModule.Fields.Cast<BaseField>().Where(FieldIsVisible);
237+
IEnumerable<BaseField> visibleFields = partModule.Fields.Cast<BaseField>().Where(visibilityFilterPredicate);
212238

213239
foreach (BaseField field in visibleFields)
214240
{
@@ -228,12 +254,28 @@ public virtual BooleanValue HasField(StringValue fieldName)
228254
return FieldIsVisible(GetField(fieldName));
229255
}
230256

257+
/// <summary>
258+
/// Determine if the Partmodule has this KSPField on it, which is currently
259+
/// NOT showing on the part's RMB menu:
260+
/// </summary>
261+
/// <param name="fieldName">The field to search for</param>
262+
/// <returns>true if it is on the PartModule, false if it is not</returns>
263+
public BooleanValue HasHiddenField(StringValue fieldName)
264+
{
265+
return GetField(fieldName, field => FieldIsVisible(field, false)) != null;
266+
}
267+
231268
/// <summary>
232269
/// Return the field itself that goes with the name (the BaseField, not the value).
233270
/// </summary>
234271
/// <param name="cookedGuiName">The case-insensitive guiName (or name if guiname is empty) of the field.</param>
235272
/// <returns>a BaseField - a KSP type that can be used to get the value, or its GUI name or its reflection info.</returns>
236273
protected BaseField GetField(string cookedGuiName)
274+
{
275+
return GetField(cookedGuiName, field => FieldIsVisible(field));
276+
}
277+
278+
private BaseField GetField(string cookedGuiName, Func<BaseField, bool> visibilityFilterPredicate)
237279
{
238280
// Conceptually this should be a single hit using FirstOrDefault(), because there should only
239281
// 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)
251293
// Issue #2666 is handled here. kOS should not return the invisible field when there's
252294
// a visible one of the same name it could have picked instead. Only return an invisible
253295
// field if there's no visible one to pick.
254-
BaseField preferredMatch = allMatches.FirstOrDefault(field => FieldIsVisible(field));
296+
BaseField preferredMatch = allMatches.FirstOrDefault(visibilityFilterPredicate);
255297
return preferredMatch ?? allMatches.First();
256298
}
257299

@@ -417,14 +459,18 @@ private void InitializeSuffixesAfterConstruction()
417459
AddSuffix("SETFIELD", new TwoArgsSuffix<StringValue, Structure>(SetKSPFieldValue));
418460
AddSuffix("DOEVENT", new OneArgsSuffix<StringValue>(CallKSPEvent));
419461
AddSuffix("DOACTION", new TwoArgsSuffix<StringValue, BooleanValue>(CallKSPAction));
462+
AddSuffix("ALLHIDDENFIELDS", new Suffix<ListValue>(() => AllHiddenFields("({0}) {1}, is {2}")));
463+
AddSuffix("ALLHIDDENFIELDNAMES", new Suffix<ListValue>(() => AllFieldNames(field => FieldIsVisible(field, false))));
464+
AddSuffix("HASHIDDENFIELD", new OneArgsSuffix<BooleanValue, StringValue>(HasHiddenField));
465+
AddSuffix("GETHIDDENFIELD", new OneArgsSuffix<Structure, StringValue>(argument => GetKSPFieldValue(argument, field => FieldIsVisible(field, false))));
420466
}
421467

422-
private static bool FieldIsVisible(BaseField field)
468+
private bool FieldIsVisible(BaseField field, bool isVisible = true)
423469
{
424-
return (field != null) && (HighLogic.LoadedSceneIsEditor ? field.guiActiveEditor : field.guiActive);
470+
return (field != null) && (HighLogic.LoadedSceneIsEditor ? field.guiActiveEditor == isVisible : field.guiActive == isVisible);
425471
}
426472

427-
private static bool EventIsVisible(BaseEvent evt)
473+
private bool EventIsVisible(BaseEvent evt)
428474
{
429475
return (evt != null) && (
430476
(HighLogic.LoadedSceneIsEditor ? evt.guiActiveEditor : evt.guiActive) &&
@@ -440,11 +486,14 @@ private static bool EventIsVisible(BaseEvent evt)
440486
/// <returns></returns>
441487
protected Structure GetKSPFieldValue(StringValue suffixName)
442488
{
443-
BaseField field = GetField(suffixName);
489+
return GetKSPFieldValue(suffixName, field => FieldIsVisible(field));
490+
}
491+
492+
private Structure GetKSPFieldValue(StringValue suffixName, Func<BaseField, bool> visibilityFilterPredicate)
493+
{
494+
BaseField field = GetField(suffixName, visibilityFilterPredicate);
444495
if (field == null)
445496
throw new KOSLookupFailException("FIELD", suffixName, this);
446-
if (!FieldIsVisible(field))
447-
throw new KOSLookupFailException("FIELD", suffixName, this, true);
448497
Structure obj = FromPrimitiveWithAssert(field.GetValue(partModule));
449498
return obj;
450499
}

0 commit comments

Comments
 (0)