1010import it .unimi .dsi .fastutil .ints .IntLinkedOpenHashSet ;
1111import it .unimi .dsi .fastutil .ints .IntSet ;
1212import net .kyori .adventure .text .Component ;
13- import net .minecraft .core .NonNullList ;
1413import net .minecraft .core .component .DataComponentPatch ;
1514import net .minecraft .core .component .DataComponents ;
1615import net .minecraft .core .component .TypedDataComponent ;
2928import net .minecraft .world .SimpleContainer ;
3029import net .minecraft .world .inventory .AbstractContainerMenu ;
3130import net .minecraft .world .inventory .MenuType ;
31+ import net .minecraft .world .inventory .RemoteSlot ;
3232import net .minecraft .world .item .ItemStack ;
3333import net .minecraft .world .item .Items ;
3434import net .minecraft .world .item .component .BundleContents ;
5959public abstract class CustomContainerMenu {
6060
6161 /**
62- * The amount of slots in the lower (player) inventory.
62+ * The number of slots in the lower (player) inventory.
6363 */
6464 private static final int LOWER_INVENTORY_SIZE = 36 ;
6565
@@ -79,7 +79,7 @@ public abstract class CustomContainerMenu {
7979 private static final HashedPatchMap .HashGenerator HASH_GENERATOR = new HashedPatchMap .HashGenerator () {
8080
8181 private final LoadingCache <TypedDataComponent <?>, Integer > cache = CacheBuilder .newBuilder ()
82- .expireAfterAccess (Duration .ofMinutes (1 ))
82+ .expireAfterAccess (Duration .ofMinutes (5 ))
8383 .build (new CacheLoader <>() {
8484
8585 private final DynamicOps <HashCode > hashOps = RegistryOps .create (
@@ -104,12 +104,36 @@ public Integer apply(TypedDataComponent<?> tdc) {
104104 /**
105105 * An item stack used for marking a remote slot dirty.
106106 */
107- protected static final HashedStack DIRTY_MARKER = HashedStack .create (new ItemStack (
108- BuiltInRegistries .ITEM .wrapAsHolder (Items .DIRT ), 1 ,
107+ protected static final ItemStack DIRTY_MARKER = new ItemStack (
108+ // use obscure item / count combination to fail item comparison as early as possible
109+ BuiltInRegistries .ITEM .wrapAsHolder (Items .GREEN_CANDLE ), 51 ,
109110 DataComponentPatch .builder ()
110111 .set (DataComponents .ITEM_MODEL , Identifier .fromNamespaceAndPath ("invui" , "dirty_marker" ))
111112 .build ()
112- ), HASH_GENERATOR );
113+ );
114+
115+ /**
116+ * A hash generator that caches component CRCs by their identity to avoid expensive hashCode() calls.
117+ * Delegates to {@link #HASH_GENERATOR} on cache miss.
118+ */
119+ private final HashedPatchMap .HashGenerator hashGenerator = new HashedPatchMap .HashGenerator () {
120+
121+ private final WeakIdentityToIntMap <Object > cache = new WeakIdentityToIntMap <>();
122+
123+ @ Override
124+ public Integer apply (TypedDataComponent <?> tdc ) {
125+ assert Bukkit .isOwnedByCurrentRegion (player );
126+
127+ var c = tdc .value ();
128+ var cachedValue = cache .get (c );
129+ if (cachedValue .isPresent ())
130+ return cachedValue .getAsInt ();
131+ var value = HASH_GENERATOR .apply (tdc );
132+ cache .putAssertAbsent (c , value );
133+ return value ;
134+ }
135+
136+ };
113137
114138 private final MenuType <?> menuType ;
115139 protected final int containerId ;
@@ -118,11 +142,11 @@ public Integer apply(TypedDataComponent<?> tdc) {
118142 private @ Nullable Window window ;
119143 private final ContainerMenuProxy proxy ;
120144
121- private final NonNullList <ItemStack > items ;
122- private final NonNullList <HashedStack > remoteItems ;
145+ private final ItemStack [] items ;
123146 private ItemStack carried = ItemStack .EMPTY ;
124- private HashedStack remoteCarried = HashedStack .EMPTY ;
125- private HashedStack remoteOffHand ;
147+ private final RemoteSlot .Synchronized [] remoteSlots ;
148+ private final RemoteSlot .Synchronized remoteCarried = remoteSlot (ItemStack .EMPTY );
149+ private final RemoteSlot .Synchronized remoteOffHand ;
126150 protected final int [] dataSlots ;
127151 protected final int [] remoteDataSlots ;
128152 private int stateId ;
@@ -141,25 +165,6 @@ public Integer apply(TypedDataComponent<?> tdc) {
141165 private boolean dirtyCarried ;
142166 private boolean dirtyOffHand ;
143167
144- private final HashedPatchMap .HashGenerator hashGenerator = new HashedPatchMap .HashGenerator () {
145-
146- private final WeakIdentityToIntMap <Object > cache = new WeakIdentityToIntMap <>();
147-
148- @ Override
149- public Integer apply (TypedDataComponent <?> tdc ) {
150- assert Bukkit .isOwnedByCurrentRegion (player );
151-
152- var c = tdc .value ();
153- var cachedValue = cache .get (c );
154- if (cachedValue .isPresent ())
155- return cachedValue .getAsInt ();
156- var value = HASH_GENERATOR .apply (tdc );
157- cache .putAssertAbsent (c , value );
158- return value ;
159- }
160-
161- };
162-
163168 /**
164169 * Creates a new {@link CustomContainerMenu} for the specified player.
165170 *
@@ -173,9 +178,9 @@ protected CustomContainerMenu(MenuType<?> menuType, org.bukkit.entity.Player pla
173178 this .containerId = serverPlayer .nextContainerCounter ();
174179
175180 int size = InventoryUtils .getSizeOf (menuType ) + LOWER_INVENTORY_SIZE ;
176- this .items = NonNullList . withSize ( size , ItemStack .EMPTY );
177- this .remoteItems = NonNullList . withSize ( size , HashedStack .EMPTY );
178- this .remoteOffHand = HashedStack . create (serverPlayer .getOffhandItem (), hashGenerator );
181+ this .items = ArrayUtils . newArray ( ItemStack []:: new , size , ItemStack .EMPTY );
182+ this .remoteSlots = ArrayUtils . newArrayBy ( RemoteSlot . Synchronized []:: new , size , _ -> remoteSlot ( ItemStack .EMPTY ) );
183+ this .remoteOffHand = remoteSlot (serverPlayer .getOffhandItem ());
179184 this .dirtyItems = new BitSet (size );
180185
181186 int dataSize = InventoryUtils .getDataSlotCountOf (menuType );
@@ -192,18 +197,18 @@ protected CustomContainerMenu(MenuType<?> menuType, org.bukkit.entity.Player pla
192197 * @param item The {@link org.bukkit.inventory.ItemStack} to put into the slot
193198 */
194199 public void setItem (int slot , org .bukkit .inventory .@ Nullable ItemStack item ) {
195- if (slot < 0 || slot >= items .size () )
200+ if (slot < 0 || slot >= items .length )
196201 throw new IllegalArgumentException ("Slot out of bounds: " + slot );
197202
198- items . set ( slot , item == null ? ItemStack .EMPTY : CraftItemStack .unwrap (item ) );
203+ items [ slot ] = item == null ? ItemStack .EMPTY : CraftItemStack .unwrap (item );
199204 dirtyItems .set (slot );
200205 }
201206
202207 public ItemStack getItem (int slot ) {
203- if (slot < 0 || slot >= items .size () )
208+ if (slot < 0 || slot >= items .length )
204209 throw new IllegalArgumentException ("Slot out of bounds: " + slot );
205210
206- return items . get ( slot ) ;
211+ return items [ slot ] ;
207212 }
208213
209214 /**
@@ -226,21 +231,29 @@ public org.bukkit.inventory.ItemStack getCursor() {
226231 }
227232
228233 //<editor-fold desc="synchronization">
229- protected void setRemoteItem (int slot , HashedStack item ) {
230- if (slot < 0 || slot >= remoteItems .size ())
231- throw new IllegalArgumentException ("Slot out of bounds: " + slot );
232-
233- remoteItems .set (slot , item );
234+ private RemoteSlot .Synchronized remoteSlot (ItemStack initial ) {
235+ var slot = new RemoteSlot .Synchronized ( hashGenerator );
236+ slot .force (initial );
237+ return slot ;
238+ }
239+
240+ protected void forceRemoteItem (int slot , ItemStack item ) {
241+ remoteSlots [slot ].force (item );
242+ dirtyItems .set (slot );
243+ }
244+
245+ protected void receiveRemoteItem (int slot , HashedStack item ) {
246+ remoteSlots [slot ].receive (item );
234247 dirtyItems .set (slot );
235248 }
236249
237- protected void setRemoteCarried ( HashedStack item ) {
238- remoteCarried = item ;
250+ protected void forceRemoteCarried ( ItemStack item ) {
251+ remoteCarried . force ( item ) ;
239252 dirtyCarried = true ;
240253 }
241254
242- protected void setRemoteOffHand ( HashedStack item ) {
243- remoteOffHand = item ;
255+ protected void forceRemoteOffHand ( ItemStack item ) {
256+ remoteOffHand . force ( item ) ;
244257 dirtyOffHand = true ;
245258 }
246259
@@ -254,28 +267,28 @@ public void sendChangesToRemote(int pingId) {
254267
255268 int itemSlot = -1 ;
256269 while ((itemSlot = dirtyItems .nextSetBit (itemSlot + 1 )) != -1 ) {
257- var item = items . get ( itemSlot ) ;
258- var remoteItem = remoteItems . get ( itemSlot ) ;
259- if (remoteItem == DIRTY_MARKER || ! remoteItem .matches (item , hashGenerator )) {
270+ var item = items [ itemSlot ] ;
271+ var remoteSlot = remoteSlots [ itemSlot ] ;
272+ if (! remoteSlot .matches (item )) {
260273 packets .add (new ClientboundContainerSetSlotPacket (containerId , incrementStateId (), itemSlot , item .copy ()));
261- remoteItems . set ( itemSlot , HashedStack . create ( item , hashGenerator ) );
274+ remoteSlot . force ( item );
262275 }
263276 }
264277 dirtyItems .clear ();
265278
266279 if (dirtyOffHand ) {
267280 var offHand = serverPlayer .getOffhandItem ();
268- if (remoteOffHand == DIRTY_MARKER || !remoteOffHand .matches (offHand , hashGenerator )) {
281+ if (!remoteOffHand .matches (offHand )) {
269282 packets .add (new ClientboundContainerSetSlotPacket (serverPlayer .inventoryMenu .containerId , incrementStateId (), OFF_HAND_SLOT , offHand .copy ()));
270- remoteOffHand = HashedStack . create (offHand , hashGenerator );
283+ remoteOffHand . force (offHand );
271284 }
272285 dirtyOffHand = false ;
273286 }
274287
275288 if (dirtyCarried ) {
276- if (remoteCarried == DIRTY_MARKER || !remoteCarried .matches (carried , hashGenerator )) {
289+ if (!remoteCarried .matches (carried )) {
277290 packets .add (new ClientboundSetCursorItemPacket (carried .copy ()));
278- remoteCarried = HashedStack . create (carried , hashGenerator );
291+ remoteCarried . force (carried );
279292 }
280293 dirtyCarried = false ;
281294 }
@@ -300,7 +313,7 @@ public void sendChangesToRemote(int pingId) {
300313 public void sendCarriedToRemote () {
301314 var content = new ClientboundSetCursorItemPacket (carried .copy ());
302315 PacketListener .getInstance ().injectOutgoing (player , content );
303- remoteCarried = HashedStack . create (carried , hashGenerator );
316+ remoteCarried . force (carried );
304317 dirtyCarried = false ;
305318 }
306319
@@ -339,7 +352,7 @@ private List<Packet<? super ClientGamePacketListener>> createContainerInitPacket
339352 packets .add (new ClientboundContainerSetContentPacket (
340353 containerId ,
341354 incrementStateId (),
342- items .stream ().map (ItemStack ::copy ).toList (),
355+ Arrays .stream (items ).map (ItemStack ::copy ).toList (),
343356 carried .copy ()
344357 ));
345358 for (int i = 0 ; i < dataSlots .length ; i ++) {
@@ -355,10 +368,10 @@ private List<Packet<? super ClientGamePacketListener>> createContainerInitPacket
355368 * Marks the current state as synced with the remote client.
356369 */
357370 private void markRemoteSynced () {
358- for (int i = 0 ; i < items .size () ; i ++) {
359- remoteItems . set ( i , HashedStack . create (items . get ( i ), hashGenerator ) );
371+ for (int i = 0 ; i < items .length ; i ++) {
372+ remoteSlots [ i ]. force (items [ i ] );
360373 }
361- remoteCarried = HashedStack . create (carried , hashGenerator );
374+ remoteCarried . force (carried );
362375 System .arraycopy (dataSlots , 0 , remoteDataSlots , 0 , dataSlots .length );
363376
364377 dirtyItems .clear ();
@@ -405,11 +418,17 @@ public void handleClosed() {
405418
406419 // transfer remote items state to inventory menu
407420 for (int i = 0 ; i < LOWER_INVENTORY_SIZE ; i ++) {
408- var item = remoteItems . get (( remoteItems . size () - LOWER_INVENTORY_SIZE ) + i ) ;
421+ var remoteSlot = remoteSlots [( remoteSlots . length - LOWER_INVENTORY_SIZE ) + i ] ;
409422 // in the inventory menu, contents start at 9
410- serverPlayer .inventoryMenu .setRemoteSlotUnsafe (9 + i , item );
423+ transferRemoteSlot ( remoteSlot , serverPlayer .inventoryMenu .remoteSlots . get (9 + i ) );
411424 }
412- serverPlayer .inventoryMenu .setRemoteSlotUnsafe (OFF_HAND_SLOT , remoteOffHand );
425+ transferRemoteSlot (remoteCarried , serverPlayer .inventoryMenu .remoteSlots .get (OFF_HAND_SLOT ));
426+ }
427+
428+ private void transferRemoteSlot (RemoteSlot .Synchronized from , RemoteSlot to ) {
429+ if (to instanceof RemoteSlot .Synchronized toSync ) {
430+ toSync .copyFrom (from );
431+ } // AbstractContainerMenu#transferTo doesn't do anything otherwise either
413432 }
414433
415434 /**
@@ -511,11 +530,11 @@ protected UpdateType handleClick(ServerboundContainerClickPacket packet) {
511530 for (Int2ObjectMap .Entry <HashedStack > entry : packet .changedSlots ().int2ObjectEntrySet ()) {
512531 int slot = entry .getIntKey ();
513532 HashedStack stack = entry .getValue ();
514- if (slot < 0 || slot > remoteItems . size () )
533+ if (slot < 0 || slot > remoteSlots . length )
515534 continue ;
516- setRemoteItem (slot , stack );
535+ receiveRemoteItem (slot , stack );
517536 }
518- setRemoteCarried (DIRTY_MARKER );
537+ forceRemoteCarried (DIRTY_MARKER );
519538
520539 if (packet .clickType () == net .minecraft .world .inventory .ClickType .QUICK_CRAFT ) {
521540 if (!handleDragClick (packet ))
@@ -553,7 +572,7 @@ private void handleNormalClick(ServerboundContainerClickPacket packet) {
553572 yield ClickType .NUMBER_KEY ;
554573 }
555574 case 40 -> {
556- setRemoteOffHand (DIRTY_MARKER );
575+ forceRemoteOffHand (DIRTY_MARKER );
557576 yield ClickType .SWAP_OFFHAND ;
558577 }
559578 default -> ClickType .UNKNOWN ;
@@ -595,7 +614,7 @@ private boolean handleDragClick(ServerboundContainerClickPacket packet) {
595614 // add slot for left, right, middle drag
596615 case 1 , 5 , 9 -> {
597616 var slot = packet .slotNum ();
598- if (slot >= 0 && slot < items .size () ) {
617+ if (slot >= 0 && slot < items .length ) {
599618 dragSlots .add (packet .slotNum ());
600619 }
601620
@@ -640,11 +659,11 @@ private boolean handleDragClick(ServerboundContainerClickPacket packet) {
640659 private UpdateType handleBundleSelect (ServerboundSelectBundleItemPacket packet ) {
641660 // verify legal slot
642661 int slot = packet .slotId ();
643- if (slot < 0 || slot >= items .size () )
662+ if (slot < 0 || slot >= items .length )
644663 return UpdateType .NONE ;
645664
646665 // verify bundle at slot
647- var bundle = items . get ( slot ) ;
666+ var bundle = items [ slot ] ;
648667 var bundleContents = bundle .get (DataComponents .BUNDLE_CONTENTS );
649668 if (bundleContents == null )
650669 return UpdateType .NONE ;
0 commit comments