33import com .google .common .cache .CacheBuilder ;
44import com .google .common .cache .CacheLoader ;
55import com .google .common .cache .LoadingCache ;
6+ import com .google .common .collect .MapMaker ;
67import com .google .common .hash .HashCode ;
78import com .mojang .serialization .DynamicOps ;
89import io .papermc .paper .adventure .PaperAdventure ;
5152import xyz .xenondevs .invui .window .Window ;
5253
5354import java .time .Duration ;
54- import java .util .ArrayList ;
55- import java .util .List ;
56- import java .util .Map ;
57- import java .util .Queue ;
55+ import java .util .*;
5856import java .util .concurrent .ConcurrentHashMap ;
5957import java .util .concurrent .ConcurrentLinkedQueue ;
58+ import java .util .concurrent .ConcurrentMap ;
6059
6160/**
6261 * A packet-based container menu.
@@ -83,7 +82,12 @@ public abstract class CustomContainerMenu {
8382 */
8483 private static final HashedPatchMap .HashGenerator HASH_GENERATOR = new HashedPatchMap .HashGenerator () {
8584
86- private final LoadingCache <TypedDataComponent <?>, Integer > cache = CacheBuilder .newBuilder ()
85+ // layer 1 cache uses identity hash code to avoid expensive component hash code calculations
86+ private final ConcurrentMap <Object , Integer > layer1 = new MapMaker ()
87+ .weakKeys () // also enables identity-based lookup
88+ .makeMap ();
89+
90+ private final LoadingCache <TypedDataComponent <?>, Integer > layer2 = CacheBuilder .newBuilder ()
8791 .expireAfterAccess (Duration .ofMinutes (1 ))
8892 .build (new CacheLoader <>() {
8993
@@ -101,7 +105,10 @@ public Integer load(TypedDataComponent<?> key) {
101105
102106 @ Override
103107 public Integer apply (TypedDataComponent <?> typedDataComponent ) {
104- return cache .getUnchecked (typedDataComponent );
108+ return layer1 .computeIfAbsent (
109+ typedDataComponent .value (),
110+ _ -> layer2 .getUnchecked (typedDataComponent )
111+ );
105112 }
106113
107114 };
@@ -123,8 +130,8 @@ public Integer apply(TypedDataComponent<?> typedDataComponent) {
123130 private @ Nullable Window window ;
124131 private final ContainerMenuProxy proxy ;
125132
126- protected final NonNullList <ItemStack > items ;
127- protected final NonNullList <HashedStack > remoteItems ;
133+ private final NonNullList <ItemStack > items ;
134+ private final NonNullList <HashedStack > remoteItems ;
128135 private ItemStack carried = ItemStack .EMPTY ;
129136 private HashedStack remoteCarried = HashedStack .EMPTY ;
130137 private HashedStack remoteOffHand ;
@@ -139,6 +146,13 @@ public Integer apply(TypedDataComponent<?> typedDataComponent) {
139146 protected final Queue <Packet <? super ServerGamePacketListener >> incoming = new ConcurrentLinkedQueue <>();
140147 private final Map <Integer , PingData > pendingPongs = new ConcurrentHashMap <>();
141148
149+ // Only items marked as dirty via the fields below will be compared with their remote counterparts.
150+ // Then, only if they differ, an update packet will be created for them.
151+ // The intention of these dirty fields is to avoid expensive comparisons between the remote and server items.
152+ private final BitSet dirtyItems ;
153+ private boolean dirtyCarried ;
154+ private boolean dirtyOffHand ;
155+
142156 /**
143157 * Creates a new {@link CustomContainerMenu} for the specified player.
144158 *
@@ -155,6 +169,7 @@ protected CustomContainerMenu(MenuType<?> menuType, org.bukkit.entity.Player pla
155169 this .items = NonNullList .withSize (size , ItemStack .EMPTY );
156170 this .remoteItems = NonNullList .withSize (size , HashedStack .EMPTY );
157171 this .remoteOffHand = HashedStack .create (serverPlayer .getOffhandItem (), HASH_GENERATOR );
172+ this .dirtyItems = new BitSet (size );
158173
159174 int dataSize = InventoryUtils .getDataSlotCountOf (menuType );
160175 this .dataSlots = new int [dataSize ];
@@ -174,6 +189,14 @@ public void setItem(int slot, org.bukkit.inventory.@Nullable ItemStack item) {
174189 throw new IllegalArgumentException ("Slot out of bounds: " + slot );
175190
176191 items .set (slot , item == null ? ItemStack .EMPTY : CraftItemStack .unwrap (item ));
192+ dirtyItems .set (slot );
193+ }
194+
195+ public ItemStack getItem (int slot ) {
196+ if (slot < 0 || slot >= items .size ())
197+ throw new IllegalArgumentException ("Slot out of bounds: " + slot );
198+
199+ return items .get (slot );
177200 }
178201
179202 /**
@@ -183,6 +206,7 @@ public void setItem(int slot, org.bukkit.inventory.@Nullable ItemStack item) {
183206 */
184207 public void setCursor (org .bukkit .inventory .@ Nullable ItemStack item ) {
185208 carried = item == null ? ItemStack .EMPTY : CraftItemStack .unwrap (item );
209+ dirtyCarried = true ;
186210 }
187211
188212 /**
@@ -195,6 +219,23 @@ public org.bukkit.inventory.ItemStack getCursor() {
195219 }
196220
197221 //<editor-fold desc="synchronization">
222+ protected void setRemoteItem (int slot , HashedStack item ) {
223+ if (slot < 0 || slot >= remoteItems .size ())
224+ throw new IllegalArgumentException ("Slot out of bounds: " + slot );
225+
226+ remoteItems .set (slot , item );
227+ dirtyItems .set (slot );
228+ }
229+
230+ protected void setRemoteCarried (HashedStack item ) {
231+ remoteCarried = item ;
232+ dirtyCarried = true ;
233+ }
234+
235+ protected void setRemoteOffHand (HashedStack item ) {
236+ remoteOffHand = item ;
237+ dirtyOffHand = true ;
238+ }
198239
199240 /**
200241 * Sends all changes to the remote client.
@@ -204,29 +245,38 @@ public org.bukkit.inventory.ItemStack getCursor() {
204245 public void sendChangesToRemote (int pingId ) {
205246 var packets = new ArrayList <Packet <? super ClientGamePacketListener >>();
206247
207- for (int i = 0 ; i < items .size (); i ++) {
208- var item = items .get (i );
209- if (!remoteItems .get (i ).matches (item , HASH_GENERATOR )) {
210- packets .add (new ClientboundContainerSetSlotPacket (containerId , incrementStateId (), i , item .copy ()));
211- remoteItems .set (i , HashedStack .create (item , HASH_GENERATOR ));
248+ int itemSlot = -1 ;
249+ while ((itemSlot = dirtyItems .nextSetBit (itemSlot + 1 )) != -1 ) {
250+ var item = items .get (itemSlot );
251+ var remoteItem = remoteItems .get (itemSlot );
252+ if (remoteItem == DIRTY_MARKER || !remoteItem .matches (item , HASH_GENERATOR )) {
253+ packets .add (new ClientboundContainerSetSlotPacket (containerId , incrementStateId (), itemSlot , item .copy ()));
254+ remoteItems .set (itemSlot , HashedStack .create (item , HASH_GENERATOR ));
212255 }
213256 }
257+ dirtyItems .clear ();
214258
215- var offHand = serverPlayer .getOffhandItem ();
216- if (!remoteOffHand .matches (offHand , HASH_GENERATOR )) {
217- packets .add (new ClientboundContainerSetSlotPacket (serverPlayer .inventoryMenu .containerId , incrementStateId (), OFF_HAND_SLOT , offHand .copy ()));
218- remoteOffHand = HashedStack .create (offHand , HASH_GENERATOR );
259+ if (dirtyOffHand ) {
260+ var offHand = serverPlayer .getOffhandItem ();
261+ if (remoteOffHand == DIRTY_MARKER || !remoteOffHand .matches (offHand , HASH_GENERATOR )) {
262+ packets .add (new ClientboundContainerSetSlotPacket (serverPlayer .inventoryMenu .containerId , incrementStateId (), OFF_HAND_SLOT , offHand .copy ()));
263+ remoteOffHand = HashedStack .create (offHand , HASH_GENERATOR );
264+ }
265+ dirtyOffHand = false ;
219266 }
220267
221- if (!remoteCarried .matches (carried , HASH_GENERATOR )) {
222- packets .add (new ClientboundSetCursorItemPacket (carried .copy ()));
223- remoteCarried = HashedStack .create (carried , HASH_GENERATOR );
268+ if (dirtyCarried ) {
269+ if (remoteCarried == DIRTY_MARKER || !remoteCarried .matches (carried , HASH_GENERATOR )) {
270+ packets .add (new ClientboundSetCursorItemPacket (carried .copy ()));
271+ remoteCarried = HashedStack .create (carried , HASH_GENERATOR );
272+ }
273+ dirtyCarried = false ;
224274 }
225275
226- for (int i = 0 ; i < dataSlots .length ; i ++) {
227- if (dataSlots [i ] != remoteDataSlots [i ]) {
228- packets .add (new ClientboundContainerSetDataPacket (containerId , i , dataSlots [i ]));
229- remoteDataSlots [i ] = dataSlots [i ];
276+ for (int dataSlot = 0 ; dataSlot < dataSlots .length ; dataSlot ++) {
277+ if (dataSlots [dataSlot ] != remoteDataSlots [dataSlot ]) {
278+ packets .add (new ClientboundContainerSetDataPacket (containerId , dataSlot , dataSlots [dataSlot ]));
279+ remoteDataSlots [dataSlot ] = dataSlots [dataSlot ];
230280 }
231281 }
232282
@@ -244,6 +294,7 @@ public void sendCarriedToRemote() {
244294 var content = new ClientboundSetCursorItemPacket (carried .copy ());
245295 PacketListener .getInstance ().injectOutgoing (player , content );
246296 remoteCarried = HashedStack .create (carried , HASH_GENERATOR );
297+ dirtyCarried = false ;
247298 }
248299
249300 /**
@@ -302,6 +353,10 @@ private void markRemoteSynced() {
302353 }
303354 remoteCarried = HashedStack .create (carried , HASH_GENERATOR );
304355 System .arraycopy (dataSlots , 0 , remoteDataSlots , 0 , dataSlots .length );
356+
357+ dirtyItems .clear ();
358+ dirtyCarried = false ;
359+ dirtyOffHand = false ;
305360 }
306361 //</editor-fold>
307362
@@ -451,9 +506,9 @@ protected UpdateType handleClick(ServerboundContainerClickPacket packet) {
451506 HashedStack stack = entry .getValue ();
452507 if (slot < 0 || slot > remoteItems .size ())
453508 continue ;
454- remoteItems . set (slot , stack );
509+ setRemoteItem (slot , stack );
455510 }
456- remoteCarried = DIRTY_MARKER ;
511+ setRemoteCarried ( DIRTY_MARKER ) ;
457512
458513 if (packet .clickType () == net .minecraft .world .inventory .ClickType .QUICK_CRAFT ) {
459514 if (!handleDragClick (packet ))
@@ -491,7 +546,7 @@ private void handleNormalClick(ServerboundContainerClickPacket packet) {
491546 yield ClickType .NUMBER_KEY ;
492547 }
493548 case 40 -> {
494- remoteOffHand = DIRTY_MARKER ;
549+ setRemoteOffHand ( DIRTY_MARKER ) ;
495550 yield ClickType .SWAP_OFFHAND ;
496551 }
497552 default -> ClickType .UNKNOWN ;
0 commit comments