1111#endif
1212
1313using System ;
14- using System . Collections . Generic ;
1514using System . Reflection ;
1615using UnityEngine ;
1716using UnityEngine . Events ;
@@ -26,7 +25,7 @@ namespace S1API.Entities
2625 public sealed class NPCInventory
2726 {
2827 private static readonly Logging . Log Logger = new Logging . Log ( "NPCInventory" ) ;
29- internal readonly NPC NPC ;
28+ private readonly NPC NPC ;
3029
3130 internal NPCInventory ( NPC npc )
3231 {
@@ -41,8 +40,6 @@ public bool CanItemFit(string itemId, int quantity = 1)
4140 if ( string . IsNullOrEmpty ( itemId ) || quantity <= 0 )
4241 return false ;
4342 var temp = BuildTempItem ( itemId , quantity ) ;
44- if ( temp == null )
45- return false ;
4643 return CanItemFitInternal ( temp ) ;
4744 }
4845
@@ -54,8 +51,6 @@ public int GetCapacityForItem(string itemId, int quantity = 1)
5451 if ( string . IsNullOrEmpty ( itemId ) || quantity <= 0 )
5552 return 0 ;
5653 var temp = BuildTempItem ( itemId , quantity ) ;
57- if ( temp == null )
58- return 0 ;
5954 return GetCapacityForItemInternal ( temp ) ;
6055 }
6156
@@ -68,8 +63,6 @@ public bool TryInsert(string itemId, int quantity = 1, bool network = true)
6863 if ( string . IsNullOrEmpty ( itemId ) || quantity <= 0 )
6964 return false ;
7065 var temp = BuildTempItem ( itemId , quantity ) ;
71- if ( temp == null )
72- return false ;
7366 if ( ! CanItemFitInternal ( temp ) )
7467 return false ;
7568 InsertItemInternal ( temp , network ) ;
@@ -84,16 +77,17 @@ public bool TryInsert(string itemId, int quantity = 1, bool network = true)
8477 /// </summary>
8578 public void EnsureInitialized ( )
8679 {
87- string npcId = NPC ? . S1NPC ? . ID ?? "<null>" ;
80+ var npcId = NPC ? . S1NPC ? . ID ?? "<null>" ;
8881 var inv = Component ;
89- if ( inv == null )
82+ if ( inv == null && NPC != null )
9083 {
91- // Attach inventory if missing
9284 var comp = NPC . gameObject . GetComponent < S1NPCs . NPCInventory > ( ) ?? NPC . gameObject . AddComponent < S1NPCs . NPCInventory > ( ) ;
9385 inv = comp ;
9486 }
9587
96- // Ensure ItemSlots list exists
88+ if ( inv == null )
89+ return ;
90+
9791 if ( inv . ItemSlots == null )
9892 {
9993#if ( IL2CPPMELON || IL2CPPBEPINEX )
@@ -102,64 +96,67 @@ public void EnsureInitialized()
10296 inv . ItemSlots = new System . Collections . Generic . List < S1Items . ItemSlot > ( ) ;
10397#endif
10498 }
105-
106- // Remove duplicate slots (same instance appearing multiple times)
107- int duplicatesRemoved = DeduplicateSlots ( inv ) ;
108-
109- // Ensure we have the correct number of slots
110- int currentCount = inv . ItemSlots . Count ;
111- int targetCount = inv . SlotCount ;
112-
113- // Remove excess slots if we have too many
99+
100+ var currentCount = inv . ItemSlots . Count ;
101+ var targetCount = inv . SlotCount ;
102+
114103 if ( currentCount > targetCount )
115104 {
116- int trimmed = currentCount - targetCount ;
117- for ( int i = currentCount - 1 ; i >= targetCount ; i -- )
105+ for ( var i = currentCount - 1 ; i >= targetCount ; i -- )
118106 {
119107 var slot = inv . ItemSlots [ i ] ;
120108 if ( slot != null )
121109 {
122- try { slot . ClearStoredInstance ( true ) ; } catch { }
110+ try { slot . ClearStoredInstance ( true ) ; }
111+ catch
112+ {
113+ // ignored
114+ }
123115 }
124116 inv . ItemSlots . RemoveAt ( i ) ;
125117 }
126118 currentCount = inv . ItemSlots . Count ;
127119 }
128-
129- // CRITICAL: Unlock all existing slots before creating new ones
130- // Base game NPCs may have locked slots, which prevents AddCash/InsertItem from working
131- for ( int i = 0 ; i < inv . ItemSlots . Count ; i ++ )
120+
121+ foreach ( var slot in inv . ItemSlots )
132122 {
133- var slot = inv . ItemSlots [ i ] ;
134- if ( slot != null )
123+ if ( slot == null ) continue ;
124+ try
125+ {
126+ ReflectionUtils . TrySetFieldOrProperty ( slot , "ActiveLock" , null ) ;
127+ }
128+ catch
129+ {
130+ // ignored
131+ }
132+
133+ try
134+ {
135+ ReflectionUtils . TrySetFieldOrProperty ( slot , "IsAddLocked" , false ) ;
136+ }
137+ catch
135138 {
136- try
137- {
138- ReflectionUtils . TrySetFieldOrProperty ( slot , "ActiveLock" , null ) ;
139- }
140- catch { }
141- try
142- {
143- ReflectionUtils . TrySetFieldOrProperty ( slot , "IsAddLocked" , false ) ;
144- }
145- catch { }
139+ // ignored
146140 }
147141 }
148142
149143 // Create missing slots if we have too few
150144 // Note: SetSlotOwner automatically adds the slot to ItemSlots, so we don't call Add() manually
151145 if ( currentCount < targetCount )
152146 {
153- int slotsToCreate = targetCount - currentCount ;
154- for ( int i = 0 ; i < slotsToCreate ; i ++ )
147+ var slotsToCreate = targetCount - currentCount ;
148+ for ( var i = 0 ; i < slotsToCreate ; i ++ )
155149 {
156150 var slot = new S1Items . ItemSlot ( ) ;
157151
158- // Set up event handler before SetSlotOwner (which adds to list)
159152#if ( IL2CPPMELON || IL2CPPBEPINEX )
160- System . Action handler = new System . Action ( ( ) =>
153+ var handler = new System . Action ( ( ) =>
161154 {
162- try { inv . onContentsChanged ? . Invoke ( ) ; } catch { }
155+ try { inv . onContentsChanged ? . Invoke ( ) ; }
156+ catch
157+ {
158+ // ignored
159+ }
163160 } ) ;
164161 slot . onItemDataChanged = ( Il2CppSystem . Action ) Il2CppSystem . Delegate . Combine (
165162 slot . onItemDataChanged ,
@@ -184,27 +181,35 @@ public void EnsureInitialized()
184181 } )
185182 ) ;
186183#endif
187-
188- // SetSlotOwner automatically adds slot to inv.ItemSlots - DO NOT call Add() manually
184+
189185#if MONOMELON
190186 slot . SetSlotOwner ( inv ) ;
191187#else
192188 slot . SetSlotOwner ( inv . Cast < S1Items . IItemSlotOwner > ( ) ) ;
193189#endif
194190
195191 // Ensure slot is unlocked
196- try { ReflectionUtils . TrySetFieldOrProperty ( slot , "ActiveLock" , null ) ; } catch { }
197- try { ReflectionUtils . TrySetFieldOrProperty ( slot , "IsAddLocked" , false ) ; } catch { }
192+ try { ReflectionUtils . TrySetFieldOrProperty ( slot , "ActiveLock" , null ) ; }
193+ catch
194+ {
195+ // ignored
196+ }
197+
198+ try { ReflectionUtils . TrySetFieldOrProperty ( slot , "IsAddLocked" , false ) ; }
199+ catch
200+ {
201+ // ignored
202+ }
198203 }
199204 }
200205
201- if ( inv . PickpocketIntObj == null )
206+ if ( inv . PickpocketIntObj == null && NPC != null )
202207 {
203208 var talk = NPC . GetType ( ) . GetMethod ( "GetPrimaryInteractable" , System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) ;
204209 var primary = talk ? . Invoke ( NPC , null ) as S1Interaction . InteractableObject ;
205210 var interactables = NPC . gameObject . GetComponentsInChildren < S1Interaction . InteractableObject > ( true ) ;
206- S1Interaction . InteractableObject pick = null ;
207- for ( int i = 0 ; i < interactables . Length ; i ++ )
211+ S1Interaction . InteractableObject ? pick = null ;
212+ for ( var i = 0 ; i < interactables . Length ; i ++ )
208213 {
209214 if ( interactables [ i ] != null && interactables [ i ] != primary )
210215 {
@@ -221,13 +226,12 @@ public void EnsureInitialized()
221226
222227 try
223228 {
224- ReflectionUtils . TrySetFieldOrProperty ( inv , "npc" , NPC . S1NPC ) ;
225-
226- // NOTE: Do NOT add listeners here - the game's NPCInventory.Awake() already adds
227- // onHovered and onInteractStart listeners. Adding them again causes duplicate
228- // event firing which breaks the pickpocket screen.
229+ ReflectionUtils . TrySetFieldOrProperty ( inv , "npc" , NPC ? . S1NPC ) ;
230+ }
231+ catch
232+ {
233+ // ignored
229234 }
230- catch { }
231235
232236 try
233237 {
@@ -237,14 +241,17 @@ public void EnsureInitialized()
237241 ReflectionUtils . TrySetFieldOrProperty ( inv , "onContentsChanged" , new UnityEvent ( ) ) ;
238242 }
239243 }
240- catch { }
244+ catch
245+ {
246+ // ignored
247+ }
241248
242249 try { inv . NetworkInitializeIfDisabled ( ) ; } catch ( Exception ex ) { Logger . Warning ( $ "[NPCInventory] EnsureInitialized: NetworkInitializeIfDisabled threw for '{ npcId } ': { ex . Message } ") ; }
243250 }
244251
245- internal S1NPCs . NPCInventory Component => NPC . gameObject . GetComponent < S1NPCs . NPCInventory > ( ) ;
252+ private S1NPCs . NPCInventory Component => NPC . gameObject . GetComponent < S1NPCs . NPCInventory > ( ) ;
246253
247- private S1Items . ItemInstance BuildTempItem ( string itemId , int quantity )
254+ private S1Items . ItemInstance ? BuildTempItem ( string itemId , int quantity )
248255 {
249256 try
250257 {
@@ -261,81 +268,24 @@ private S1Items.ItemInstance BuildTempItem(string itemId, int quantity)
261268 }
262269 }
263270
264- internal bool CanItemFitInternal ( S1Items . ItemInstance item )
271+ private bool CanItemFitInternal ( S1Items . ItemInstance item )
265272 {
266- if ( item == null ) return false ;
267273 EnsureInitialized ( ) ;
268274 var inv = Component ;
269275 return inv != null && inv . CanItemFit ( item ) ;
270276 }
271277
272- internal int GetCapacityForItemInternal ( S1Items . ItemInstance item )
278+ private int GetCapacityForItemInternal ( S1Items . ItemInstance item )
273279 {
274- if ( item == null ) return 0 ;
275280 EnsureInitialized ( ) ;
276281 var inv = Component ;
277282 return inv != null ? inv . GetCapacityForItem ( item ) : 0 ;
278283 }
279284
280- internal void InsertItemInternal ( S1Items . ItemInstance item , bool network = true )
285+ private void InsertItemInternal ( S1Items . ItemInstance item , bool network = true )
281286 {
282- if ( item == null ) return ;
283287 EnsureInitialized ( ) ;
284288 Component ? . InsertItem ( item , network ) ;
285289 }
286-
287- /// <summary>
288- /// Removes duplicate slot instances from the inventory's ItemSlots list.
289- /// Duplicates can occur if slots are added manually after SetSlotOwner (which already adds them).
290- /// </summary>
291- private int DeduplicateSlots ( S1NPCs . NPCInventory inv )
292- {
293- if ( inv ? . ItemSlots == null )
294- return 0 ;
295-
296- try
297- {
298- // Use a set to track seen slot instances (by reference equality)
299- var seen = new HashSet < object > ( ) ;
300- var toRemove = new List < int > ( ) ;
301-
302- for ( int i = 0 ; i < inv . ItemSlots . Count ; i ++ )
303- {
304- var slot = inv . ItemSlots [ i ] ;
305- if ( slot == null )
306- {
307- toRemove . Add ( i ) ;
308- continue ;
309- }
310-
311- // Check if we've seen this exact slot instance before
312- if ( seen . Contains ( slot ) )
313- {
314- // This is a duplicate - clear it and mark for removal
315- try { slot . ClearStoredInstance ( true ) ; } catch { }
316- toRemove . Add ( i ) ;
317- }
318- else
319- {
320- seen . Add ( slot ) ;
321- }
322- }
323-
324- // Remove duplicates from the end to preserve indices
325- for ( int i = toRemove . Count - 1 ; i >= 0 ; i -- )
326- {
327- inv . ItemSlots . RemoveAt ( toRemove [ i ] ) ;
328- }
329-
330- return toRemove . Count ;
331- }
332- catch ( Exception ex )
333- {
334- // If deduplication fails, continue - better to have duplicates than crash
335- Logger . Warning ( $ "[NPCInventory] DeduplicateSlots: Failed with exception: { ex . Message } ") ;
336- Logger . Warning ( $ "[NPCInventory] DeduplicateSlots: Stack trace: { ex . StackTrace } ") ;
337- return 0 ;
338- }
339- }
340290 }
341291}
0 commit comments