Skip to content

Commit 1f70893

Browse files
Cache remote item stack instances
Instead of converting everything to hashed stacks, cache the remote item stack instances by using RemoteSlot.Synchronized, which does this. This improves performance as equality of identical (==) item stacks can be determined more quickly. Previously, HashedStack#matches had to first extract all components and run them through the hash generator (with identity map cache) for this.
1 parent 28a09dc commit 1f70893

7 files changed

Lines changed: 132 additions & 84 deletions

File tree

invui/src/main/java/xyz/xenondevs/invui/internal/menu/CustomAnvilMenu.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ private void handleRename(ServerboundRenameItemPacket packet) {
6969
renameText = packet.getName();
7070
if (renameHandler != null)
7171
renameHandler.accept(renameText);
72-
setRemoteItem(2, DIRTY_MARKER);
72+
forceRemoteItem(2, DIRTY_MARKER);
7373
remoteDataSlots[0] = ENCHANTMENT_COST_DIRTY_MARKER;
7474
}
7575

invui/src/main/java/xyz/xenondevs/invui/internal/menu/CustomContainerMenu.java

Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet;
1111
import it.unimi.dsi.fastutil.ints.IntSet;
1212
import net.kyori.adventure.text.Component;
13-
import net.minecraft.core.NonNullList;
1413
import net.minecraft.core.component.DataComponentPatch;
1514
import net.minecraft.core.component.DataComponents;
1615
import net.minecraft.core.component.TypedDataComponent;
@@ -29,6 +28,7 @@
2928
import net.minecraft.world.SimpleContainer;
3029
import net.minecraft.world.inventory.AbstractContainerMenu;
3130
import net.minecraft.world.inventory.MenuType;
31+
import net.minecraft.world.inventory.RemoteSlot;
3232
import net.minecraft.world.item.ItemStack;
3333
import net.minecraft.world.item.Items;
3434
import net.minecraft.world.item.component.BundleContents;
@@ -59,7 +59,7 @@
5959
public 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;

invui/src/main/java/xyz/xenondevs/invui/internal/menu/CustomCrafterMenu.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package xyz.xenondevs.invui.internal.menu;
22

33
import net.kyori.adventure.text.Component;
4-
import net.minecraft.network.HashedStack;
54
import net.minecraft.network.protocol.Packet;
65
import net.minecraft.network.protocol.game.ServerGamePacketListener;
76
import net.minecraft.network.protocol.game.ServerboundContainerSlotStateChangedPacket;
87
import net.minecraft.world.inventory.MenuType;
8+
import net.minecraft.world.item.ItemStack;
99
import org.bukkit.entity.Player;
1010
import org.jspecify.annotations.Nullable;
1111
import xyz.xenondevs.invui.internal.network.PacketListener;
@@ -72,7 +72,7 @@ public boolean isSlotDisabled(int slot) {
7272
*/
7373
public void setSlotDisabled(int slot, boolean state) {
7474
dataSlots[slot] = state ? 1 : 0;
75-
setRemoteItem(slot, HashedStack.EMPTY);
75+
forceRemoteItem(slot, ItemStack.EMPTY);
7676
}
7777

7878
/**

invui/src/main/java/xyz/xenondevs/invui/internal/menu/CustomGrindstoneMenu.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package xyz.xenondevs.invui.internal.menu;
22

3-
import net.minecraft.network.HashedStack;
43
import net.minecraft.world.inventory.MenuType;
4+
import net.minecraft.world.item.ItemStack;
55
import org.bukkit.entity.Player;
6-
import org.bukkit.inventory.ItemStack;
76
import org.jspecify.annotations.Nullable;
87

98
/**
@@ -21,12 +20,12 @@ public CustomGrindstoneMenu(Player player) {
2120
}
2221

2322
@Override
24-
public void setItem(int slot, @Nullable ItemStack item) {
23+
public void setItem(int slot, org.bukkit.inventory.@Nullable ItemStack item) {
2524
super.setItem(slot, item);
2625

2726
// client-side prediction clears output slot when input slots are modified
2827
if (slot == 0 || slot == 1) {
29-
setRemoteItem(2, HashedStack.EMPTY);
28+
forceRemoteItem(2, ItemStack.EMPTY);
3029
}
3130
}
3231

0 commit comments

Comments
 (0)