1
+ // #define EXAMPLE_BaseFieldListUseFieldHost
2
+
3
+ using HarmonyLib ;
4
+ using System ;
5
+ using System . Collections . Generic ;
6
+ using System . Reflection ;
7
+ using UnityEngine ;
8
+
9
+ /*
10
+ The purpose of this patch if to allow BaseField and associated features (PAW controls, persistence, etc) to work when
11
+ a custom BaseField is added to a BaseFieldList (ie, a Part or PartModule) with a host instance other than the BaseFieldList
12
+ owner. This allow to dynamically add fields defined in another class to a Part or PartModule and to benefit from all the
13
+ associated KSP sugar :
14
+ - PAW UI controls
15
+ - Value and symmetry events
16
+ - Automatic persistence on the Part/PartModule hosting the BaseFieldList
17
+
18
+ Potential use cases for this are either :
19
+ - Having a part or module-level PAW item associated to and sharing the state of a common field, for example a field in a KSPAddon.
20
+ - Extending external (typically stock) modules with additional PAW UI controls and/or persisted fields.
21
+
22
+ The whole thing seems actually designed with such a scenario in mind, but for some reason some BaseField and BaseFieldList
23
+ methods are using the BaseFieldList.host instance instead of the BaseField.host instance (as for why BaseFieldList has a
24
+ "host" at all, I've no idea and this seems to be a design oversight). There is little to no consistency in which host
25
+ reference is used, they are even sometimes mixed in the same method. For example, BaseFieldList.Load() uses BaseFieldList.host
26
+ in its main body, then calls BaseFieldList.SetOriginalValue() which is relying on BaseField.host.
27
+
28
+ Changing every place where a `host` reference is acquired to ensure the BaseField.host reference is used allow to use a custom
29
+ host instance, and shouldn't result in any behavior change. This being said, the stock code can technically allow a plugin
30
+ to instantiate a custom BaseField with a null host and have it kinda functional if that field is only used to SetValue() /
31
+ Getvalue(), as long as the field isn't persistent and doesn't have any associated UI_Control, so we implement a fallback
32
+ to the stock behavior in this case.
33
+ */
34
+
35
+ namespace KSPCommunityFixes . Modding
36
+ {
37
+ [ PatchPriority ( Order = 0 ) ]
38
+ class BaseFieldListUseFieldHost : BasePatch
39
+ {
40
+ private static AccessTools . FieldRef < object , Callback < object > > BaseField_OnValueModified_FieldRef ;
41
+
42
+ protected override Version VersionMin => new Version ( 1 , 12 , 3 ) ;
43
+
44
+ protected override void ApplyPatches ( )
45
+ {
46
+ BaseField_OnValueModified_FieldRef = AccessTools . FieldRefAccess < Callback < object > > ( typeof ( BaseField < KSPField > ) , nameof ( BaseField < KSPField > . OnValueModified ) ) ;
47
+ if ( BaseField_OnValueModified_FieldRef == null )
48
+ throw new MissingFieldException ( $ "BaseFieldListUseFieldHost patch could not find the BaseField.OnValueModified event backing field") ;
49
+
50
+ // Note : fortunately, we don't need to patch the generic BaseField.GetValue<T>(object host) method because it calls
51
+ // the non-generic GetValue method
52
+ AddPatch ( PatchType . Override , AccessTools . FirstMethod ( typeof ( BaseField < KSPField > ) , ( m ) => m . Name == nameof ( BaseField < KSPField > . GetValue ) && ! m . IsGenericMethod ) , nameof ( BaseField_GetValue_Override ) ) ;
53
+
54
+ AddPatch ( PatchType . Override , typeof ( BaseField < KSPField > ) , nameof ( BaseField < KSPField > . SetValue ) , nameof ( BaseField_SetValue_Override ) ) ;
55
+
56
+ // BaseField.Read() is a public method called from :
57
+ // - BaseFieldList.Load()
58
+ // - BaseFieldList.ReadValue() (2 overloads)
59
+ // The method is really tiny so there is a potential inlining risk (doesn't happen in my tests, but this stuff can be platform
60
+ // dependent). It's only really critical to have BaseFieldList.Load() being patched, the ReadValue() methods are unused in the
61
+ // stock codebase, and it is doubtfull anybody would ever call them.
62
+ AddPatch ( PatchType . Override , typeof ( BaseField ) , nameof ( BaseField . Read ) ) ;
63
+
64
+ // We also patch BaseFieldList.Load() because :
65
+ // - Of the above mentioned inlining risk
66
+ // - Because it pass the (arguably wrong) host reference to the UI_Control.Load() method, and even though none
67
+ // of the various overloads make use of that argument we might want to be consistent.
68
+ AddPatch ( PatchType . Override , typeof ( BaseFieldList ) , nameof ( BaseFieldList . Load ) ) ;
69
+ }
70
+
71
+ static object BaseField_GetValue_Override ( BaseField < KSPField > instance , object host )
72
+ {
73
+ try
74
+ {
75
+ // In case the field host is null, use the parameter
76
+ if ( ReferenceEquals ( instance . _host , null ) )
77
+ return instance . _fieldInfo . GetValue ( host ) ;
78
+
79
+ // Uses the field host reference instead of the reference passed as a parameter
80
+ return instance . _fieldInfo . GetValue ( instance . _host ) ;
81
+ }
82
+ catch
83
+ {
84
+ PDebug . Error ( "Value could not be retrieved from field '" + instance . _name + "'" ) ;
85
+ return null ;
86
+ }
87
+ }
88
+
89
+ static bool BaseField_SetValue_Override ( BaseField < KSPField > instance , object newValue , object host )
90
+ {
91
+ try
92
+ {
93
+ // In case the field host is null, use the parameter
94
+ if ( ReferenceEquals ( instance . _host , null ) )
95
+ instance . _fieldInfo . SetValue ( host , newValue ) ;
96
+
97
+ // Uses the field host reference instead of the reference passed as a parameter
98
+ instance . _fieldInfo . SetValue ( instance . _host , newValue ) ;
99
+
100
+ // Note : since BaseField.OnValueModified is a "field-like event", it is relying on a compiler-generated
101
+ // private backing field, and the public event "syntactic sugar" member can only be invoked from the declaring
102
+ // class, so we can't directly call "__instance.OnValueModified()" here. Additionally, the compiler has the extremly
103
+ // bad taste to name the backing field "OnValueModified" too, resulting in an ambiguous reference if we try to use
104
+ // it through the publicized assembly. So we have to resort to creating a FieldRef open delegate for that backing field.
105
+ BaseField_OnValueModified_FieldRef ( instance ) . Invoke ( newValue ) ;
106
+ return true ;
107
+ }
108
+ catch ( Exception ex )
109
+ {
110
+ PDebug . Error ( string . Concat ( "Value '" , newValue , "' could not be set to field '" , instance . _name , "'" ) ) ;
111
+ PDebug . Error ( ex . Message + "\n " + ex . StackTrace + "\n " + ex . Data ) ;
112
+ return false ;
113
+ }
114
+ }
115
+
116
+ static void BaseField_Read_Override ( BaseField instance , string value , object host )
117
+ {
118
+ if ( ReferenceEquals ( instance . _host , null ) )
119
+ BaseField . ReadPvt ( instance . _fieldInfo , value , host ) ;
120
+
121
+ BaseField . ReadPvt ( instance . _fieldInfo , value , instance . _host ) ;
122
+ }
123
+
124
+ static void BaseFieldList_Load_Override ( BaseFieldList instance , ConfigNode node )
125
+ {
126
+ for ( int i = 0 ; i < node . values . Count ; i ++ )
127
+ {
128
+ ConfigNode . Value value = node . values [ i ] ;
129
+ BaseField baseField = instance [ value . name ] ;
130
+ if ( baseField == null || baseField . hasInterface || baseField . uiControlOnly )
131
+ continue ;
132
+
133
+ object host = ReferenceEquals ( baseField . _host , null ) ? instance . host : baseField . _host ;
134
+
135
+ // The original code calls BaseField.Read() here. We bypass it to avoid
136
+ // any inlining risk and call directly the underlying static method.
137
+ BaseField . ReadPvt ( baseField . _fieldInfo , value . value , host ) ;
138
+
139
+ if ( baseField . uiControlFlight . GetType ( ) != typeof ( UI_Label ) )
140
+ {
141
+ ConfigNode controlNode = node . GetNode ( value . name + "_UIFlight" ) ;
142
+ if ( controlNode != null )
143
+ baseField . uiControlFlight . Load ( controlNode , host ) ;
144
+ }
145
+ else if ( baseField . uiControlEditor . GetType ( ) != typeof ( UI_Label ) )
146
+ {
147
+ ConfigNode controlNode = node . GetNode ( value . name + "_UIEditor" ) ;
148
+ if ( controlNode != null )
149
+ baseField . uiControlEditor . Load ( controlNode , host ) ;
150
+ }
151
+ }
152
+ for ( int j = 0 ; j < node . nodes . Count ; j ++ )
153
+ {
154
+ ConfigNode configNode = node . nodes [ j ] ;
155
+ BaseField baseField = instance [ configNode . name ] ;
156
+ if ( baseField == null || ! baseField . hasInterface || baseField . uiControlOnly )
157
+ continue ;
158
+
159
+ object host = ReferenceEquals ( baseField . _host , null ) ? instance . host : baseField . _host ;
160
+
161
+ object value = baseField . GetValue ( host ) ;
162
+ if ( value == null )
163
+ continue ;
164
+
165
+ ( value as IConfigNode ) ? . Load ( configNode ) ;
166
+
167
+ if ( baseField . uiControlFlight . GetType ( ) != typeof ( UI_Label ) )
168
+ {
169
+ ConfigNode controlNode = node . GetNode ( configNode . name + "_UIFlight" ) ;
170
+ if ( controlNode != null )
171
+ baseField . uiControlFlight . Load ( controlNode , host ) ;
172
+ }
173
+ else if ( baseField . uiControlEditor . GetType ( ) != typeof ( UI_Label ) )
174
+ {
175
+ ConfigNode controlNode = node . GetNode ( configNode . name + "_UIEditor" ) ;
176
+ if ( controlNode != null )
177
+ baseField . uiControlEditor . Load ( controlNode , host ) ;
178
+ }
179
+ }
180
+ instance . SetOriginalValue ( ) ;
181
+ }
182
+ }
183
+
184
+ #if EXAMPLE_BaseFieldListUseFieldHost
185
+
186
+ // This is an example module we want to add fields to.
187
+ // Here we use the most failsafe and recommended pattern, which is to create and destroy an extension object at the
188
+ // earliest and latest possible moment in the module lifecycle.
189
+ // In a real use case, this would have to be accomplished by harmony-patching the relevant methods.
190
+ // It's technically possible to use other patterns, but tracking parts/modules lifetime externally is full of difficult
191
+ // to handle corner-case scenarios, and is likely to cause way more overhead, I really advise against attempting it.
192
+
193
+ // This mean that adding BaseFields to external modules is only achievable if the methods exist to be patched :
194
+ // - A post-instantiation method, ideally OnAwake(), but OnStart()/Start()/OnStartFinished() can work too.
195
+ // - OnDestroy(), so the object associated with the external module can be untracked and thus garbage collected
196
+ // Without that, the pattern would result in a memory leak. In the absence of an OnDestroy() method, it's possible
197
+ // to clean all extensions on scene switches, which still result in a leak, but a temporary one. If you have to
198
+ // resort to this, I would advise to consider other options for achieving equivalent functionality, either using a
199
+ // new, separate module, or (but that isn't very recommended either) putting the functionality in a derived module
200
+ // and MM-swapping it.
201
+
202
+ // Do note that if you want to define a [KSPField(isPersistant = true)] field and benefit from the built-in persistence,
203
+ // your custom BaseField must be added to the target module before PartModule.Load() is called, so the only option is
204
+ // to add the BaseField from OnAwake().
205
+
206
+ public class CustomFieldDemoModule : PartModule
207
+ {
208
+ public override void OnAwake ( )
209
+ {
210
+ CustomFieldDemoModuleExtension . Instantiate ( this ) ;
211
+ CustomFieldDemoModuleGlobalField . RegisterModule ( this ) ;
212
+ }
213
+
214
+ private void OnDestroy ( )
215
+ {
216
+ CustomFieldDemoModuleExtension . Destroy ( this ) ;
217
+ }
218
+ }
219
+
220
+ // This demonstrate adding a field to an existing module, from an object acting as an extension of the module.
221
+ // We set the KSPField
222
+ public class CustomFieldDemoModuleExtension
223
+ {
224
+ private static Dictionary < int , CustomFieldDemoModuleExtension > extensions = new Dictionary < int , CustomFieldDemoModuleExtension > ( ) ;
225
+
226
+ public static void Instantiate ( CustomFieldDemoModule target )
227
+ {
228
+ // Using the instance ID for the dictionary keys is slightly faster than using the instance itself.
229
+ int targetID = target . GetInstanceID ( ) ;
230
+
231
+ // Using TryAdd is not necessary if you can guarantee that Instantiate() will never be called more than once for the same
232
+ // module, which is a relatively safe assumption if you call it from the module OnAwake() or constructor, and a very unsafe
233
+ // one in pretty much every other case (Start, OnStart...)
234
+ extensions . TryAdd ( targetID , new CustomFieldDemoModuleExtension ( target ) ) ;
235
+ }
236
+
237
+ public static void Destroy ( CustomFieldDemoModule target )
238
+ {
239
+ extensions . Remove ( target . GetInstanceID ( ) ) ;
240
+ }
241
+
242
+ public static CustomFieldDemoModuleExtension Get ( CustomFieldDemoModule module )
243
+ {
244
+ if ( extensions . TryGetValue ( module . GetInstanceID ( ) , out CustomFieldDemoModuleExtension extension ) )
245
+ return extension ;
246
+
247
+ return null ;
248
+ }
249
+
250
+ private static FieldInfo customFieldInfo ;
251
+ private static KSPField customKSPField ;
252
+ private static UI_FloatRange customFieldControl ;
253
+
254
+ static CustomFieldDemoModuleExtension ( )
255
+ {
256
+ // we need the FieldInfo for our field, might as well cache it in a static field
257
+ customFieldInfo = typeof ( CustomFieldDemoModuleExtension ) . GetField ( nameof ( customField ) , BindingFlags . Instance | BindingFlags . NonPublic ) ;
258
+
259
+ // Here we define static KSPField and UI_FloatRange attributes for our field. This can be also be done
260
+ // on a per-instance basis, but this is more performant.
261
+ // This would be equivalent to applying the following attributes to a module field :
262
+
263
+ // [KSPField(guiActive = true, guiActiveEditor = true, guiName = "MyExtensionField", isPersistant = true)]
264
+ customKSPField = new KSPField ( ) ;
265
+ customKSPField . guiActive = true ;
266
+ customKSPField . guiActiveEditor = true ;
267
+ customKSPField . guiName = "MyExtensionField" ;
268
+ customKSPField . isPersistant = true ;
269
+
270
+ // [UI_FloatRange(minValue = 5f, maxValue = 25f, stepIncrement = 1f, affectSymCounterparts = UI_Scene.All)]
271
+ customFieldControl = new UI_FloatRange ( ) ;
272
+ customFieldControl . minValue = 5f ;
273
+ customFieldControl . maxValue = 25f ;
274
+ customFieldControl . stepIncrement = 1f ;
275
+ customFieldControl . affectSymCounterparts = UI_Scene . All ;
276
+ }
277
+
278
+ private float customField ;
279
+
280
+ private CustomFieldDemoModuleExtension ( CustomFieldDemoModule target )
281
+ {
282
+ BaseField customBaseField = new BaseField ( customKSPField , customFieldInfo , this ) ;
283
+ customBaseField . uiControlEditor = customFieldControl ;
284
+ customBaseField . uiControlFlight = customFieldControl ;
285
+ target . Fields . Add ( customBaseField ) ;
286
+ customBaseField . OnValueModified += OnValueModified ;
287
+ }
288
+
289
+ private void OnValueModified ( object newValue )
290
+ {
291
+ UnityEngine . Debug . Log ( $ "extension field value changed : { newValue } ") ;
292
+ }
293
+ }
294
+
295
+ // This demonstrate having a field on a singleton object, and where all module instances are sharing this
296
+ // global field value.
297
+ // Note that in this use case, using persistent fields doesn't make any sense, as you would end up with
298
+ // conflicting persisted values stored on different modules.
299
+ // Also note that in this case, there is no memory leak risk as long as the global instance doesn't keep
300
+ // a reference to the modules or BaseFields, so we don't need anything called from the module OnDestroy().
301
+ [ KSPAddon ( KSPAddon . Startup . FlightAndEditor , false ) ]
302
+ public class CustomFieldDemoModuleGlobalField : MonoBehaviour
303
+ {
304
+ private static CustomFieldDemoModuleGlobalField instance ;
305
+ private static FieldInfo globalFieldInfo ;
306
+ private static KSPField globalKSPField ;
307
+ private static UI_FloatRange globalFieldControl ;
308
+
309
+ static CustomFieldDemoModuleGlobalField ( )
310
+ {
311
+ globalFieldInfo = typeof ( CustomFieldDemoModuleGlobalField ) . GetField ( nameof ( globalField ) , BindingFlags . Instance | BindingFlags . NonPublic ) ;
312
+ globalKSPField = new KSPField ( ) ;
313
+ globalKSPField . guiActive = true ;
314
+ globalKSPField . guiActiveEditor = true ;
315
+ globalKSPField . guiName = "MyGlobalField" ;
316
+ globalFieldControl = new UI_FloatRange ( ) ;
317
+ globalFieldControl . minValue = 5f ;
318
+ globalFieldControl . maxValue = 25f ;
319
+ globalFieldControl . stepIncrement = 1f ;
320
+ globalFieldControl . affectSymCounterparts = UI_Scene . None ;
321
+ }
322
+
323
+ private void Awake ( ) => instance = this ;
324
+ private void OnDestroy ( ) => instance = null ;
325
+
326
+ public static void RegisterModule ( CustomFieldDemoModule module )
327
+ {
328
+ if ( instance == null )
329
+ return ;
330
+
331
+ BaseField globalBaseField = new BaseField ( globalKSPField , globalFieldInfo , instance ) ;
332
+ globalBaseField . uiControlEditor = globalFieldControl ;
333
+ globalBaseField . uiControlFlight = globalFieldControl ;
334
+ module . Fields . Add ( globalBaseField ) ;
335
+ globalBaseField . OnValueModified += instance . OnValueModified ;
336
+ }
337
+
338
+ private float globalField ;
339
+
340
+ private void OnValueModified ( object newValue )
341
+ {
342
+ UnityEngine . Debug . Log ( $ "global field value changed : { newValue } ") ;
343
+ }
344
+ }
345
+ #endif
346
+ }
0 commit comments