Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bundle support #5145

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@

package org.geysermc.geyser.inventory;

import lombok.*;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
Expand All @@ -35,6 +39,7 @@
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.BundleCache;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
Expand All @@ -55,19 +60,23 @@ public class GeyserItemStack {
private DataComponents components;
private int netId;

@EqualsAndHashCode.Exclude
private BundleCache.BundleData bundleData;

@Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private Item item;

private GeyserItemStack(int javaId, int amount, DataComponents components) {
this(javaId, amount, components, 1);
this(javaId, amount, components, 1, null);
}

private GeyserItemStack(int javaId, int amount, DataComponents components, int netId) {
private GeyserItemStack(int javaId, int amount, DataComponents components, int netId, BundleCache.BundleData bundleData) {
this.javaId = javaId;
this.amount = amount;
this.components = components;
this.netId = netId;
this.bundleData = bundleData;
}

public static @NonNull GeyserItemStack of(int javaId, int amount) {
Expand Down Expand Up @@ -152,6 +161,24 @@ public int getNetId() {
return isEmpty() ? 0 : netId;
}

public int getBundleId() {
if (isEmpty()) {
return -1;
}

return bundleData == null ? -1 : bundleData.bundleId();
}

public void mergeBundleData(GeyserSession session, BundleCache.BundleData oldBundleData) {
if (oldBundleData != null && this.bundleData != null) {
// Old bundle; re-use old IDs
this.bundleData.updateNetIds(session, oldBundleData);
} else if (this.bundleData != null) {
// New bundle; allocate new ID
session.getBundleCache().markNewBundle(this.bundleData);
}
}

public void add(int add) {
amount += add;
}
Expand All @@ -165,6 +192,24 @@ public ItemStack getItemStack() {
}

public @Nullable ItemStack getItemStack(int newAmount) {
if (isEmpty()) {
return null;
}
// Sync our updated bundle data to server, if applicable
// Not fresh from server? Then we have changes to apply!~
if (bundleData != null && !bundleData.freshFromServer()) {
if (!bundleData.contents().isEmpty()) {
if (components == null) {
components = new DataComponents(new HashMap<>());
}
components.put(DataComponentType.BUNDLE_CONTENTS, bundleData.toComponent());
} else {
if (components != null) {
// Empty list = no component = should delete
components.getDataComponents().remove(DataComponentType.BUNDLE_CONTENTS);
}
}
}
return isEmpty() ? null : new ItemStack(javaId, newAmount, components);
}

Expand All @@ -175,7 +220,8 @@ public ItemData getItemData(GeyserSession session) {
ItemData.Builder itemData = ItemTranslator.translateToBedrock(session, javaId, amount, components);
itemData.netId(getNetId());
itemData.usingNetId(true);
return itemData.build();

return session.getBundleCache().checkForBundle(this, itemData);
}

public ItemMapping getMapping(GeyserSession session) {
Expand Down Expand Up @@ -208,6 +254,6 @@ public GeyserItemStack copy() {
}

public GeyserItemStack copy(int newAmount) {
return isEmpty() ? EMPTY : new GeyserItemStack(javaId, newAmount, components == null ? null : components.clone(), netId);
return isEmpty() ? EMPTY : new GeyserItemStack(javaId, newAmount, components == null ? null : components.clone(), netId, bundleData == null ? null : bundleData.copy());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,21 @@ public void setItem(int slot, @NonNull GeyserItemStack newItem, GeyserSession se
}
}

protected void updateItemNetId(GeyserItemStack oldItem, GeyserItemStack newItem, GeyserSession session) {
public static void updateItemNetId(GeyserItemStack oldItem, GeyserItemStack newItem, GeyserSession session) {
if (!newItem.isEmpty()) {
ItemDefinition oldMapping = ItemTranslator.getBedrockItemDefinition(session, oldItem);
ItemDefinition newMapping = ItemTranslator.getBedrockItemDefinition(session, newItem);
if (oldMapping.equals(newMapping)) {
newItem.setNetId(oldItem.getNetId());
newItem.mergeBundleData(session, oldItem.getBundleData());
} else {
newItem.setNetId(session.getNextItemNetId());
session.getBundleCache().markNewBundle(newItem.getBundleData());
session.getBundleCache().onOldItemDelete(oldItem);
}
} else {
// Empty item means no more bundle if one existed.
session.getBundleCache().onOldItemDelete(oldItem);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
@AllArgsConstructor
public enum Click {
LEFT(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK),
LEFT_BUNDLE(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK),
LEFT_BUNDLE_FROM_CURSOR(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_CLICK),
RIGHT(ContainerActionType.CLICK_ITEM, ClickItemAction.RIGHT_CLICK),
RIGHT_BUNDLE(ContainerActionType.CLICK_ITEM, ClickItemAction.RIGHT_CLICK),
LEFT_SHIFT(ContainerActionType.SHIFT_CLICK_ITEM, ShiftClickItemAction.LEFT_CLICK),
DROP_ONE(ContainerActionType.DROP_ITEM, DropItemAction.DROP_FROM_SELECTED),
DROP_ALL(ContainerActionType.DROP_ITEM, DropItemAction.DROP_SELECTED_STACK),
Expand Down
110 changes: 100 additions & 10 deletions core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,26 @@

package org.geysermc.geyser.inventory.click;

import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType;
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
import org.geysermc.mcprotocollib.protocol.data.game.inventory.MoveToHotbarAction;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.SlotType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.inventory.BundleInventoryTranslator;
import org.geysermc.geyser.translator.inventory.CraftingInventoryTranslator;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.util.InventoryUtils;
import org.geysermc.geyser.util.thirdparty.Fraction;
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType;
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
import org.geysermc.mcprotocollib.protocol.data.game.inventory.MoveToHotbarAction;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSelectBundleItemPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundSetCreativeModeSlotPacket;
import org.jetbrains.annotations.Contract;

import java.util.ArrayList;
Expand All @@ -52,7 +59,8 @@ public final class ClickPlan {
*/
private Int2ObjectMap<ItemStack> changedItems;
private GeyserItemStack simulatedCursor;
private boolean finished;
private int desiredBundleSlot;
private boolean executionBegan;

private final GeyserSession session;
private final InventoryTranslator translator;
Expand All @@ -67,7 +75,7 @@ public ClickPlan(GeyserSession session, InventoryTranslator translator, Inventor
this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize());
this.changedItems = null;
this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
this.finished = false;
this.executionBegan = false;

gridSize = translator.getGridSize();
}
Expand All @@ -82,7 +90,7 @@ public void add(Click click, int slot) {
}

public void add(Click click, int slot, boolean force) {
if (finished)
if (executionBegan)
throw new UnsupportedOperationException("ClickPlan already executed");

if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) {
Expand All @@ -97,6 +105,7 @@ public void add(Click click, int slot, boolean force) {
}

public void execute(boolean refresh) {
executionBegan = true;
//update geyser inventory after simulation to avoid net id desync
resetSimulation();
ListIterator<ClickAction> planIter = plan.listIterator();
Expand Down Expand Up @@ -159,7 +168,27 @@ public void execute(boolean refresh) {
for (Int2ObjectMap.Entry<GeyserItemStack> simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
}
finished = true;
}

public void executeForCreativeMode() {
executionBegan = true;
//update geyser inventory after simulation to avoid net id desync
resetSimulation();
changedItems = new Int2ObjectOpenHashMap<>();
for (ClickAction action : plan) {
simulateAction(action);
}
session.getPlayerInventory().setCursor(simulatedCursor, session);
for (Int2ObjectMap.Entry<GeyserItemStack> simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
}
for (Int2ObjectMap.Entry<ItemStack> changedSlot : changedItems.int2ObjectEntrySet()) {
ItemStack value = changedSlot.getValue();
ItemStack toSend = InventoryUtils.isEmpty(value) ? new ItemStack(-1, 0, null) : value;
session.sendDownstreamGamePacket(
new ServerboundSetCreativeModeSlotPacket((short) changedSlot.getIntKey(), toSend)
);
}
}

public Inventory getInventory() {
Expand Down Expand Up @@ -187,6 +216,10 @@ public GeyserItemStack getItem(int slot) {
return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
}

public void setDesiredBundleSlot(int desiredBundleSlot) {
this.desiredBundleSlot = desiredBundleSlot;
}

public GeyserItemStack getCursor() {
return simulatedCursor;
}
Expand Down Expand Up @@ -275,7 +308,59 @@ private void simulateAction(ClickAction action) {
} else if (InventoryUtils.canStack(cursor, clicked)) {
cursor.sub(1);
add(action.slot, clicked, 1);
} else {
// Can't stack, but both the cursor and the slot have an item
// (Called for bundles)
setCursor(clicked);
setItem(action.slot, cursor);
}
break;
case LEFT_BUNDLE:
Fraction bundleWeight = BundleInventoryTranslator.calculateBundleWeight(clicked.getBundleData().contents());
int amountToAddInBundle = Math.min(BundleInventoryTranslator.capacityForItemStack(bundleWeight, cursor), cursor.getAmount());
GeyserItemStack toInsertInBundle = cursor.copy(amountToAddInBundle);
if (executionBegan) {
clicked.getBundleData().contents().add(0, toInsertInBundle);
session.getBundleCache().onItemAdded(clicked); // Must be run before onSlotItemChange as the latter exports an ItemStack from the bundle
}
onSlotItemChange(action.slot, clicked);
cursor.sub(amountToAddInBundle);
break;
case LEFT_BUNDLE_FROM_CURSOR:
List<GeyserItemStack> contents = cursor.getBundleData().contents();
bundleWeight = BundleInventoryTranslator.calculateBundleWeight(contents);
amountToAddInBundle = Math.min(BundleInventoryTranslator.capacityForItemStack(bundleWeight, clicked), clicked.getAmount());
toInsertInBundle = clicked.copy(amountToAddInBundle);
if (executionBegan) {
cursor.getBundleData().contents().add(0, toInsertInBundle);
session.getBundleCache().onItemAdded(cursor);
}
sub(action.slot, clicked, amountToAddInBundle);
break;
case RIGHT_BUNDLE:
if (!cursor.isEmpty()) {
// Bundle should be in player's hand.
GeyserItemStack itemStack = cursor.getBundleData()
.contents()
.remove(0);
if (executionBegan) {
session.getBundleCache().onItemRemoved(cursor, 0);
}
setItem(action.slot, itemStack);
break;
}

if (executionBegan) {
sendSelectedBundleSlot(action.slot);
}
GeyserItemStack itemStack = clicked.getBundleData()
.contents()
.remove(desiredBundleSlot);
if (executionBegan) {
session.getBundleCache().onItemRemoved(clicked, desiredBundleSlot);
}
onSlotItemChange(action.slot, clicked);
setCursor(itemStack);
break;
case SWAP_TO_HOTBAR_1:
swap(action.slot, inventory.getOffsetForHotbar(0), clicked);
Expand Down Expand Up @@ -319,6 +404,11 @@ private void simulateAction(ClickAction action) {
}
}

private void sendSelectedBundleSlot(int slot) {
// Looks like this is also technically sent in creative mode.
session.sendDownstreamGamePacket(new ServerboundSelectBundleItemPacket(slot, desiredBundleSlot));
}

/**
* Swap between two inventory slots without a cursor. This should only be used with {@link ContainerActionType#MOVE_TO_HOTBAR_SLOT}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
import org.geysermc.geyser.session.auth.BedrockClientData;
import org.geysermc.geyser.session.cache.AdvancementsCache;
import org.geysermc.geyser.session.cache.BookEditCache;
import org.geysermc.geyser.session.cache.BundleCache;
import org.geysermc.geyser.session.cache.ChunkCache;
import org.geysermc.geyser.session.cache.EntityCache;
import org.geysermc.geyser.session.cache.EntityEffectCache;
Expand Down Expand Up @@ -275,6 +276,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {

private final AdvancementsCache advancementsCache;
private final BookEditCache bookEditCache;
private final BundleCache bundleCache;
private final ChunkCache chunkCache;
private final EntityCache entityCache;
private final EntityEffectCache effectCache;
Expand Down Expand Up @@ -662,6 +664,7 @@ public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSessio

this.advancementsCache = new AdvancementsCache(this);
this.bookEditCache = new BookEditCache(this);
this.bundleCache = new BundleCache(this);
this.chunkCache = new ChunkCache(this);
this.entityCache = new EntityCache(this);
this.effectCache = new EntityEffectCache();
Expand Down
Loading