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