1
+ using HarmonyLib ;
2
+ using System ;
3
+
4
+ /*
5
+ The purpose of this patch if to allow BaseField and associated features (PAW controls, persistence, etc) to work when
6
+ a custom BaseField is added to a BaseFieldList (ie, a Part or PartModule) with a host instance other than the BaseFieldList
7
+ owner. This allow to dynamically add fields defined in another class to a Part or PartModule and to benefit from all the
8
+ associated KSP sugar :
9
+ - PAW UI controls
10
+ - Value and symmetry events
11
+ - Automatic persistence on the Part/PartModule hosting the BaseFieldList
12
+
13
+ The whole thing seems actually designed with such a scenario in mind, but for some reason some BaseField and BaseFieldList
14
+ methods are using the BaseFieldList.host instance instead of the BaseField.host instance (as for why BaseFieldList has a
15
+ "host" at all, I've no idea and this seems to be a design oversight). There is little to no consistency in which host
16
+ reference is used, they are even sometimes mixed in the same method. For example, BaseFieldList.Load() uses BaseFieldList.host
17
+ in its main body, then calls BaseFieldList.SetOriginalValue() which is relying on BaseField.host.
18
+
19
+ Changing every place where a `host` reference is acquired to ensure the BaseField.host reference is used allow to use a custom
20
+ host instance, and shouldn't result in any behavior change. This being said, the stock code can theoretically allow a plugin
21
+ to instantiate a custom BaseField with a null host and have it kinda functional if that field is only used to SetValue() /
22
+ Getvalue() and as long as the field isn't persistent and doesn't have any associated UI_Control. This feels like an extremly
23
+ improbable scenario, so this is probably fine.
24
+ */
25
+
26
+ namespace KSPCommunityFixes . Modding
27
+ {
28
+ [ PatchPriority ( Order = 0 ) ]
29
+ class BaseFieldListUseFieldHost : BasePatch
30
+ {
31
+ private static AccessTools . FieldRef < object , Callback < object > > BaseField_OnValueModified_FieldRef ;
32
+
33
+ protected override Version VersionMin => new Version ( 1 , 12 , 3 ) ;
34
+
35
+ protected override void ApplyPatches ( )
36
+ {
37
+ BaseField_OnValueModified_FieldRef = AccessTools . FieldRefAccess < Callback < object > > ( typeof ( BaseField < KSPField > ) , nameof ( BaseField < KSPField > . OnValueModified ) ) ;
38
+ if ( BaseField_OnValueModified_FieldRef == null )
39
+ throw new MissingFieldException ( $ "BaseFieldListUseFieldHost patch could not find the BaseField.OnValueModified event backing field") ;
40
+
41
+ // Note : fortunately, we don't need to patch the generic BaseField.GetValue<T>(object host) method because it calls
42
+ // the non-generic GetValue method
43
+ AddPatch ( PatchType . Override , AccessTools . FirstMethod ( typeof ( BaseField < KSPField > ) , ( m ) => m . Name == nameof ( BaseField < KSPField > . GetValue ) && ! m . IsGenericMethod ) , nameof ( BaseField_GetValue_Override ) ) ;
44
+
45
+ AddPatch ( PatchType . Override , typeof ( BaseField < KSPField > ) , nameof ( BaseField < KSPField > . SetValue ) , nameof ( BaseField_SetValue_Override ) ) ;
46
+
47
+ // BaseField.Read() is a public method called from :
48
+ // - BaseFieldList.Load()
49
+ // - BaseFieldList.ReadValue() (2 overloads)
50
+ // The method is really tiny so there is a potential inlining risk (doesn't happen in my tests, but this stuff can be platform
51
+ // dependent). It's only really critical to have BaseFieldList.Load() being patched, the ReadValue() methods are unused in the
52
+ // stock codebase, and it is doubtfull anybody would ever call them.
53
+ AddPatch ( PatchType . Override , typeof ( BaseField ) , nameof ( BaseField . Read ) ) ;
54
+
55
+ // We also patch BaseFieldList.Load() because :
56
+ // - Of the above mentioned inlining risk
57
+ // - Because it pass the (arguably wrong) host reference to the UI_Control.Load() method, and even though none
58
+ // of the various overloads make use of that argument we might want to be consistent.
59
+ AddPatch ( PatchType . Override , typeof ( BaseFieldList ) , nameof ( BaseFieldList . Load ) ) ;
60
+ }
61
+
62
+ static object BaseField_GetValue_Override ( BaseField < KSPField > instance , object host )
63
+ {
64
+ try
65
+ {
66
+ // In case the field host is null, use the parameter
67
+ if ( ReferenceEquals ( instance . _host , null ) )
68
+ return instance . _fieldInfo . GetValue ( host ) ;
69
+
70
+ // Uses the field host reference instead of the reference passed as a parameter
71
+ return instance . _fieldInfo . GetValue ( instance . _host ) ;
72
+ }
73
+ catch
74
+ {
75
+ PDebug . Error ( "Value could not be retrieved from field '" + instance . _name + "'" ) ;
76
+ return null ;
77
+ }
78
+ }
79
+
80
+ static bool BaseField_SetValue_Override ( BaseField < KSPField > instance , object newValue , object host )
81
+ {
82
+ try
83
+ {
84
+ // In case the field host is null, use the parameter
85
+ if ( ReferenceEquals ( instance . _host , null ) )
86
+ instance . _fieldInfo . SetValue ( host , newValue ) ;
87
+
88
+ // Uses the field host reference instead of the reference passed as a parameter
89
+ instance . _fieldInfo . SetValue ( instance . _host , newValue ) ;
90
+
91
+ // Note : since BaseField.OnValueModified is a "field-like event", it is relying on a compiler-generated
92
+ // private backing field, and the public event "synctatic suger" member can only be invoked from the declaring
93
+ // class, so we can't directly call "__instance.OnValueModified()" here. Additionally, the compiler has the extremly
94
+ // bad taste to name the backing field "OnValueModified" too, resulting in an "ambiguous reference" if we try to use
95
+ // it through the publicized assembly. So we have to resort to creating a FieldRef open delegate for that backing field.
96
+ BaseField_OnValueModified_FieldRef ( instance ) . Invoke ( newValue ) ;
97
+ return true ;
98
+ }
99
+ catch ( Exception ex )
100
+ {
101
+ PDebug . Error ( string . Concat ( "Value '" , newValue , "' could not be set to field '" , instance . _name , "'" ) ) ;
102
+ PDebug . Error ( ex . Message + "\n " + ex . StackTrace + "\n " + ex . Data ) ;
103
+ return false ;
104
+ }
105
+ }
106
+
107
+ static void BaseField_Read_Override ( BaseField instance , string value , object host )
108
+ {
109
+ if ( ReferenceEquals ( instance . _host , null ) )
110
+ BaseField . ReadPvt ( instance . _fieldInfo , value , host ) ;
111
+
112
+ BaseField . ReadPvt ( instance . _fieldInfo , value , instance . _host ) ;
113
+ }
114
+
115
+ static void BaseFieldList_Load_Override ( BaseFieldList instance , ConfigNode node )
116
+ {
117
+ for ( int i = 0 ; i < node . values . Count ; i ++ )
118
+ {
119
+ ConfigNode . Value value = node . values [ i ] ;
120
+ BaseField baseField = instance [ value . name ] ;
121
+ if ( baseField == null || baseField . hasInterface || baseField . uiControlOnly )
122
+ continue ;
123
+
124
+ object host = ReferenceEquals ( baseField . _host , null ) ? instance . host : baseField . _host ;
125
+
126
+ // The original code calls BaseField.Read() here. We bypass it to avoid
127
+ // any inlining risk and call directly the underlying static method.
128
+ BaseField . ReadPvt ( baseField . _fieldInfo , value . value , host ) ;
129
+
130
+ if ( baseField . uiControlFlight . GetType ( ) != typeof ( UI_Label ) )
131
+ {
132
+ ConfigNode controlNode = node . GetNode ( value . name + "_UIFlight" ) ;
133
+ if ( controlNode != null )
134
+ baseField . uiControlFlight . Load ( controlNode , host ) ;
135
+ }
136
+ else if ( baseField . uiControlEditor . GetType ( ) != typeof ( UI_Label ) )
137
+ {
138
+ ConfigNode controlNode = node . GetNode ( value . name + "_UIEditor" ) ;
139
+ if ( controlNode != null )
140
+ baseField . uiControlEditor . Load ( controlNode , host ) ;
141
+ }
142
+ }
143
+ for ( int j = 0 ; j < node . nodes . Count ; j ++ )
144
+ {
145
+ ConfigNode configNode = node . nodes [ j ] ;
146
+ BaseField baseField = instance [ configNode . name ] ;
147
+ if ( baseField == null || ! baseField . hasInterface || baseField . uiControlOnly )
148
+ continue ;
149
+
150
+ object host = ReferenceEquals ( baseField . _host , null ) ? instance . host : baseField . _host ;
151
+
152
+ object value = baseField . GetValue ( host ) ;
153
+ if ( value == null )
154
+ continue ;
155
+
156
+ ( value as IConfigNode ) ? . Load ( configNode ) ;
157
+
158
+ if ( baseField . uiControlFlight . GetType ( ) != typeof ( UI_Label ) )
159
+ {
160
+ ConfigNode controlNode = node . GetNode ( configNode . name + "_UIFlight" ) ;
161
+ if ( controlNode != null )
162
+ baseField . uiControlFlight . Load ( controlNode , host ) ;
163
+ }
164
+ else if ( baseField . uiControlEditor . GetType ( ) != typeof ( UI_Label ) )
165
+ {
166
+ ConfigNode controlNode = node . GetNode ( configNode . name + "_UIEditor" ) ;
167
+ if ( controlNode != null )
168
+ baseField . uiControlEditor . Load ( controlNode , host ) ;
169
+ }
170
+ }
171
+ instance . SetOriginalValue ( ) ;
172
+ }
173
+ }
174
+ }
0 commit comments