3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2025-01-07 22:00:24 +01:00
* Bundle support

* Touchups

* Correct bundle mapping

* Grabbing a bundle from creative mode does work

* Fix inserting items that already exist in a bundle

* Add bundle drop workaround

* Address review
Dieser Commit ist enthalten in:
Camotoy 2025-01-05 10:34:57 -05:00 committet von GitHub
Ursprung a19f0305fb
Commit ddd1afabd1
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: B5690EEEBB952194
17 geänderte Dateien mit 1856 neuen und 26 gelöschten Zeilen

Datei anzeigen

@ -39,6 +39,7 @@ import org.geysermc.geyser.item.type.Item;
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;
@ -59,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) {
@ -173,6 +178,24 @@ public class GeyserItemStack {
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;
}
@ -186,6 +209,21 @@ public class GeyserItemStack {
}
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()) {
getOrCreateComponents().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);
}
@ -196,7 +234,8 @@ public class GeyserItemStack {
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) {
@ -229,6 +268,6 @@ public class GeyserItemStack {
}
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());
}
}

Datei anzeigen

@ -142,15 +142,21 @@ public abstract class Inventory {
}
}
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);
}
}

Datei anzeigen

@ -31,7 +31,10 @@ import lombok.AllArgsConstructor;
@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),

Datei anzeigen

@ -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;
@ -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;
@ -67,7 +75,7 @@ public final class ClickPlan {
this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize());
this.changedItems = null;
this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
this.finished = false;
this.executionBegan = false;
gridSize = translator.getGridSize();
}
@ -82,7 +90,7 @@ public final class ClickPlan {
}
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) {
@ -97,6 +105,7 @@ public final class ClickPlan {
}
public void execute(boolean refresh) {
executionBegan = true;
//update geyser inventory after simulation to avoid net id desync
resetSimulation();
ListIterator<ClickAction> planIter = plan.listIterator();
@ -159,7 +168,27 @@ public final class ClickPlan {
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() {
@ -187,6 +216,10 @@ public final class ClickPlan {
return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
}
public void setDesiredBundleSlot(int desiredBundleSlot) {
this.desiredBundleSlot = desiredBundleSlot;
}
public GeyserItemStack getCursor() {
return simulatedCursor;
}
@ -275,8 +308,60 @@ public final class ClickPlan {
} 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);
break;
@ -319,6 +404,11 @@ public final class ClickPlan {
}
}
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}
*/

Datei anzeigen

@ -158,6 +158,7 @@ import org.geysermc.geyser.session.auth.AuthData;
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;
@ -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;
@ -677,6 +679,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
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();
@ -1352,6 +1355,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
}
this.bundleCache.tick();
if (spawned) {
// Could move this to the PlayerAuthInput translator, in the event the player lags
// but this will work once we implement matching Java custom tick cycles
@ -1470,6 +1475,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
hand, worldCache.nextPredictionSequence(), playerEntity.getYaw(), playerEntity.getPitch()));
}
public void releaseItem() {
// Followed to the Minecraft Protocol specification outlined at wiki.vg
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO,
Direction.DOWN, 0);
sendDownstreamGamePacket(releaseItemPacket);
}
/**
* Checks to see if a shield is in either hand to activate blocking. If so, it sets the Bedrock client to display
* blocking and sends a packet to the Java server.

Datei anzeigen

@ -0,0 +1,383 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.session.cache;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.ContainerRegistryCleanupPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.InventoryUtils;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class BundleCache {
private static final int BUNDLE_CONTAINER_ID = 125; // BDS 1.21.44
private final GeyserSession session;
private int nextBundleId;
private int releaseTick = -1;
public BundleCache(GeyserSession session) {
this.session = session;
}
/**
* Checks to see if the given item from the server is a bundle.
* If so, we initialize our bundle cache.
*/
public void initialize(GeyserItemStack itemStack) {
// Message before 1.21.4 - "Can't check for BUNDLE_CONTENTS, which may be missing if the bundle is empty."
// Now irrelevant, but keeping as-is for the time being.
if (session.getTagCache().is(ItemTag.BUNDLES, itemStack)) {
if (itemStack.getBundleData() != null) {
session.getGeyser().getLogger().warning("Stack has bundle data already! It should not!");
if (session.getGeyser().getConfig().isDebugMode()) {
session.getGeyser().getLogger().debug("Player: " + session.javaUsername());
session.getGeyser().getLogger().debug("Stack: " + itemStack);
}
}
BundleData bundleData;
List<ItemStack> rawContents = itemStack.getComponent(DataComponentType.BUNDLE_CONTENTS);
if (rawContents != null) {
// Use existing list and transform it to support net IDs
bundleData = new BundleData(session, rawContents);
} else {
// This is valid behavior (as of vanilla 1.21.2) if the bundle is empty.
// Create new list
bundleData = new BundleData();
}
itemStack.setBundleData(bundleData);
}
}
public void markNewBundle(@Nullable BundleData bundleData) {
if (bundleData == null) {
return;
}
if (bundleData.bundleId != -1) {
return;
}
bundleData.bundleId = nextBundleId++;
for (GeyserItemStack stack : bundleData.contents()) {
stack.setNetId(session.getNextItemNetId());
session.getBundleCache().markNewBundle(stack.getBundleData());
}
}
public ItemData checkForBundle(GeyserItemStack itemStack, ItemData.Builder itemData) {
if (itemStack.getBundleData() == null) {
return itemData.build();
}
// Not ideal, since Cloudburst NBT is immutable, but there isn't another ideal intersection between
// item instance tracking and item translation
// (Java just reads the contents of each item, while Bedrock kind of wants its own ID for each bundle item stack)
List<GeyserItemStack> contents = itemStack.getBundleData().contents();
int containerId = itemStack.getBundleId();
if (containerId == -1) {
session.getGeyser().getLogger().warning("Bundle ID should not be -1!");
}
NbtMap nbt = itemData.build().getTag();
NbtMapBuilder builder = nbt == null ? NbtMap.builder() : nbt.toBuilder();
builder.putInt("bundle_id", containerId);
itemData.tag(builder.build());
// Now that the tag is updated...
ItemData finalItem = itemData.build();
if (!itemStack.getBundleData().triggerFullContentsUpdate) {
// We are probably in the middle of updating one slot. Let's save bandwidth! :)
return finalItem;
}
// This is how BDS does it, so while it isn't pretty, it is accurate.
// Ensure that all bundle slots are cleared when we re-send data.
// Otherwise, if we don't indicate an item for a slot, Bedrock will think
// the old item still exists.
ItemData[] array = new ItemData[64];
Arrays.fill(array, ItemData.AIR);
List<ItemData> bedrockItems = Arrays.asList(array);
// Reverse order to ensure contents line up with Java.
int j = 0;
for (int i = contents.size() - 1; i >= 0; i--) {
// Ensure item data can be tracked
bedrockItems.set(j++, contents.get(i).getItemData(session));
}
InventoryContentPacket packet = new InventoryContentPacket();
packet.setContainerId(BUNDLE_CONTAINER_ID);
packet.setContents(bedrockItems);
packet.setContainerNameData(BundleCache.createContainer(containerId));
packet.setStorageItem(finalItem);
session.sendUpstreamPacket(packet);
return finalItem;
}
/*
* We need to send an InventorySlotPacket to the Bedrock client so it updates its changes and doesn't desync.
*/
public void onItemAdded(GeyserItemStack bundle) {
BundleData data = bundle.getBundleData();
data.freshFromServer = false;
data.triggerFullContentsUpdate = false;
List<GeyserItemStack> contents = data.contents();
int bedrockSlot = platformConvertSlot(contents.size(), 0);
ItemData bedrockContent = contents.get(0).getItemData(session);
sendInventoryPacket(data.bundleId(), bedrockSlot, bedrockContent, bundle.getItemData(session));
data.triggerFullContentsUpdate = true;
}
public void onItemRemoved(GeyserItemStack bundle, int slot) {
// Whatever item used to be in here should have been removed *before* this was triggered.
BundleData data = bundle.getBundleData();
data.freshFromServer = false;
data.triggerFullContentsUpdate = false;
List<GeyserItemStack> contents = data.contents();
ItemData baseBundle = bundle.getItemData(session);
// This first slot is now blank!
sendInventoryPacket(data.bundleId(), platformConvertSlot(contents.size() + 1, 0), ItemData.AIR, baseBundle);
// Adjust the index of every item that came before this item.
for (int i = 0; i < slot; i++) {
sendInventoryPacket(data.bundleId(), platformConvertSlot(contents.size(), i),
contents.get(i).getItemData(session), baseBundle);
}
data.triggerFullContentsUpdate = true;
}
private void sendInventoryPacket(int bundleId, int bedrockSlot, ItemData bedrockContent, ItemData baseBundle) {
InventorySlotPacket packet = new InventorySlotPacket();
packet.setContainerId(BUNDLE_CONTAINER_ID);
packet.setItem(bedrockContent);
packet.setSlot(bedrockSlot);
packet.setContainerNameData(createContainer(bundleId));
packet.setStorageItem(baseBundle);
session.sendUpstreamPacket(packet);
}
/**
* If a bundle is no longer present in the working inventory, delete the cache
* from the client.
*/
public void onOldItemDelete(GeyserItemStack itemStack) {
if (itemStack.getBundleId() != -1) {
// Clean up old container ID, to match BDS behavior.
ContainerRegistryCleanupPacket packet = new ContainerRegistryCleanupPacket();
packet.getContainers().add(createContainer(itemStack.getBundleId()));
session.sendUpstreamPacket(packet);
}
}
public void onInventoryClose(Inventory inventory) {
if (inventory instanceof PlayerInventory) {
// Don't bother; items are still here.
return;
}
for (int i = 0; i < inventory.getSize(); i++) {
GeyserItemStack item = inventory.getItem(i);
onOldItemDelete(item);
}
}
/* All utilities to track when a release item packet should be sent.
* As of 1.21.50, Bedrock seems to be picky and inspecific when sending its own release packet,
* but if Java does not receive a release packet, then it will continue to drop items out of a bundle.
* This workaround releases items on behalf of the client if it does not send a packet, while respecting
* if Bedrock sends its own. */
public void awaitRelease() {
if (session.getTagCache().is(ItemTag.BUNDLES, session.getPlayerInventory().getItemInHand())) {
releaseTick = session.getTicks() + 1;
}
}
public void markRelease() {
releaseTick = -1;
}
public void tick() {
if (this.releaseTick != -1) {
if (session.getTicks() >= this.releaseTick) {
session.releaseItem();
markRelease();
}
}
}
/**
* Bidirectional; works for both Bedrock and Java.
*/
public static int platformConvertSlot(int contentsSize, int rawSlot) {
return contentsSize - rawSlot - 1;
}
public static FullContainerName createContainer(int id) {
return new FullContainerName(ContainerSlotType.DYNAMIC_CONTAINER, id);
}
/**
* Primarily exists to support net IDs within bundles.
* Important to prevent accidental item deletion in creative mode.
*/
public static final class BundleData {
private final List<GeyserItemStack> contents;
/**
* Will be set to a positive integer after checking for existing bundle data.
*/
private int bundleId = -1;
/**
* If false, blocks a complete InventoryContentPacket being sent to the server.
*/
private boolean triggerFullContentsUpdate = true;
/**
* Sets whether data is accurate from the server; if so, any old bundle contents
* will be overwritten.
* This will be set to false if we are the most recent change-makers.
*/
private boolean freshFromServer = true;
BundleData(GeyserSession session, List<ItemStack> contents) {
this();
for (ItemStack content : contents) {
GeyserItemStack itemStack = GeyserItemStack.from(content);
// Check recursively
session.getBundleCache().initialize(itemStack);
this.contents.add(itemStack);
}
}
BundleData() {
this.contents = new ArrayList<>();
}
public int bundleId() {
return bundleId;
}
public List<GeyserItemStack> contents() {
return contents;
}
public boolean freshFromServer() {
return freshFromServer;
}
public List<ItemStack> toComponent() {
List<ItemStack> component = new ArrayList<>(this.contents.size());
for (GeyserItemStack content : this.contents) {
component.add(content.getItemStack());
}
return component;
}
/**
* Merge in changes from the server and re-use net IDs where possible.
*/
public void updateNetIds(GeyserSession session, BundleData oldData) {
List<GeyserItemStack> oldContents = oldData.contents();
// Items can't exactly be rearranged in a bundle; they can only be removed at an index, or inserted.
int oldIndex = 0;
for (int newIndex = 0; newIndex < this.contents.size(); newIndex++) {
GeyserItemStack itemStack = this.contents.get(newIndex);
if (oldIndex >= oldContents.size()) {
// Assume new item if it goes out of bounds of our existing stack
if (this.freshFromServer) {
// Only update net IDs for new items if the data is fresh from server.
// Otherwise, we can update net IDs for something that already has
// net IDs allocated, which can cause desyncs.
Inventory.updateItemNetId(GeyserItemStack.EMPTY, itemStack, session);
session.getBundleCache().markNewBundle(itemStack.getBundleData());
}
continue;
}
GeyserItemStack oldItem = oldContents.get(oldIndex);
// If it stacks with the old item at this index, then
if (!InventoryUtils.canStack(oldItem, itemStack)) {
// New item?
boolean found = false;
if (oldIndex + 1 < oldContents.size()) {
oldItem = oldContents.get(oldIndex + 1);
if (InventoryUtils.canStack(oldItem, itemStack)) {
// Permanently increment and assume all contents shifted here
oldIndex++;
found = true;
}
}
if (!found && oldIndex - 1 >= 0) {
oldItem = oldContents.get(oldIndex - 1);
if (InventoryUtils.canStack(oldItem, itemStack)) {
// Permanently decrement and assume all contents shifted here
oldIndex--;
found = true;
}
}
if (!found) {
oldItem = GeyserItemStack.EMPTY;
}
}
if (oldItem != GeyserItemStack.EMPTY || this.freshFromServer) {
Inventory.updateItemNetId(oldItem, itemStack, session);
}
oldIndex++;
}
this.bundleId = oldData.bundleId();
}
public BundleData copy() {
BundleData data = new BundleData();
data.bundleId = this.bundleId;
for (GeyserItemStack content : this.contents) {
data.contents.add(content.copy());
}
data.freshFromServer = this.freshFromServer;
return data;
}
}
}

Datei anzeigen

@ -0,0 +1,339 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.inventory;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.ints.IntSets;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.ItemStackRequestAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.SwapAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.action.TransferItemStackRequestAction;
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.response.ItemStackResponse;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.inventory.click.Click;
import org.geysermc.geyser.inventory.click.ClickPlan;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.BundleCache;
import org.geysermc.geyser.util.InventoryUtils;
import org.geysermc.geyser.util.thirdparty.Fraction;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
import java.util.List;
import static org.geysermc.geyser.translator.inventory.InventoryTranslator.*;
public final class BundleInventoryTranslator {
/**
* @return a processed bundle interaction, or null to resume normal transaction handling.
*/
@Nullable
static ItemStackResponse handleBundle(GeyserSession session, InventoryTranslator translator, Inventory inventory, ItemStackRequest request, boolean sendCreativePackets) {
TransferItemStackRequestAction action = null;
for (ItemStackRequestAction requestAction : request.getActions()) {
if (requestAction instanceof SwapAction swapAction) {
if (isBundle(swapAction.getSource()) && isBundle(swapAction.getDestination())) {
// Can be seen when inserting an item that's already present within the bundle
continue;
}
return null;
}
if (!(requestAction instanceof TransferItemStackRequestAction transferAction)) {
// No other known bundle action that does not use transfer actions
return null;
}
boolean sourceIsBundle = isBundle(transferAction.getSource());
boolean destIsBundle = isBundle(transferAction.getDestination());
if (sourceIsBundle && destIsBundle) {
// The client is rearranging the bundle inventory; we're going to ignore translating these actions.
continue;
}
if (sourceIsBundle || destIsBundle) {
// This action is moving to a bundle or moving out of a bundle. This is the one we want to track
action = transferAction;
} else {
// Neither location is a bundle location. We don't need to deal with this here.
return null;
}
}
if (action == null) {
return null;
}
ClickPlan plan = new ClickPlan(session, translator, inventory);
if (isBundle(action.getDestination())) {
// Placing into bundle
var bundleSlotData = action.getDestination();
var inventorySlotData = action.getSource();
int bundleId = bundleSlotData.getContainerName().getDynamicId();
GeyserItemStack cursor = session.getPlayerInventory().getCursor();
if (cursor.getBundleId() == bundleId) {
List<GeyserItemStack> contents = cursor.getBundleData().contents();
// Placing items into bundles can mean their contents are empty
// We are currently holding the bundle and trying to pick an item up.
int sourceSlot = translator.bedrockSlotToJava(inventorySlotData);
GeyserItemStack sourceItem = inventory.getItem(sourceSlot);
if (sourceItem.isEmpty()) {
// This would be treated as just... plumping the bundle down,
// and that should not be called here.
return rejectRequest(request);
}
if (inventorySlotData.getStackNetworkId() != sourceItem.getNetId()) {
return rejectRequest(request);
}
// Note that this is also called in ClickPlan. Not ideal...
Fraction bundleWeight = calculateBundleWeight(contents);
int allowedCapacity = Math.min(capacityForItemStack(bundleWeight, sourceItem), sourceItem.getAmount());
if (action.getCount() != allowedCapacity) {
// Might trigger if bundle weight is different between Java and Bedrock (see calculateBundleWeight)
return rejectRequest(request);
}
plan.add(Click.LEFT_BUNDLE_FROM_CURSOR, sourceSlot);
if (sendCreativePackets) {
plan.executeForCreativeMode();
} else {
plan.execute(false);
}
return acceptRequest(request, translator.makeContainerEntries(session, inventory, IntSets.singleton(sourceSlot)));
}
for (int javaSlot = 0; javaSlot < inventory.getSize(); javaSlot++) {
GeyserItemStack bundle = inventory.getItem(javaSlot);
if (bundle.getBundleId() != bundleId) {
continue;
}
if (!translator.checkNetId(session, inventory, inventorySlotData)) {
return rejectRequest(request);
}
// Placing items into bundles can mean their contents are empty
// Bundle slot does not matter; Java always appends an item to the beginning of a bundle inventory
IntSet affectedSlots = new IntOpenHashSet(2);
affectedSlots.add(javaSlot);
boolean slotIsInventory = !isCursor(inventorySlotData);
int sourceSlot;
// If source is cursor, logic lines up better with Java.
if (slotIsInventory) {
// Simulate picking up the item and adding it to our cursor,
// which is what Java would expect
sourceSlot = translator.bedrockSlotToJava(inventorySlotData);
plan.add(Click.LEFT, sourceSlot);
affectedSlots.add(sourceSlot);
} else {
sourceSlot = -1;
}
Fraction bundleWeight = calculateBundleWeight(bundle.getBundleData().contents());
// plan.getCursor() covers if we just picked up the item above from a slot
int allowedCapacity = Math.min(capacityForItemStack(bundleWeight, plan.getCursor()), plan.getCursor().getAmount());
if (action.getCount() != allowedCapacity) {
// Might trigger if bundle weight is different between Java and Bedrock (see calculateBundleWeight)
return rejectRequest(request);
}
plan.add(Click.LEFT_BUNDLE, javaSlot);
if (slotIsInventory && allowedCapacity != plan.getCursor().getAmount()) {
// We will need to place the item back in its original slot.
plan.add(Click.LEFT, sourceSlot);
}
if (sendCreativePackets) {
plan.executeForCreativeMode();
} else {
plan.execute(false);
}
return acceptRequest(request, translator.makeContainerEntries(session, inventory, affectedSlots));
}
// Could not find bundle in inventory
} else {
// Taking from bundle
var bundleSlotData = action.getSource();
var inventorySlotData = action.getDestination();
int bundleId = bundleSlotData.getContainerName().getDynamicId();
GeyserItemStack cursor = session.getPlayerInventory().getCursor();
if (cursor.getBundleId() == bundleId) {
// We are currently holding the bundle
List<GeyserItemStack> contents = cursor.getBundleData().contents();
if (contents.isEmpty()) {
// Nothing would be ejected?
return rejectRequest(request);
}
// Can't select bundle slots while holding bundle in any version; don't set desired bundle slot
if (bundleSlotData.getStackNetworkId() != contents.get(0).getNetId()) {
// We're pulling out the first item; if something mismatches, wuh oh.
return rejectRequest(request);
}
int destSlot = translator.bedrockSlotToJava(inventorySlotData);
if (!inventory.getItem(destSlot).isEmpty()) {
// Illegal action to place an item down on an existing stack, even if
// the bundle contains the item.
return rejectRequest(request);
}
plan.add(Click.RIGHT_BUNDLE, destSlot);
if (sendCreativePackets) {
plan.executeForCreativeMode();
} else {
plan.execute(false);
}
return acceptRequest(request, translator.makeContainerEntries(session, inventory, IntSets.singleton(destSlot)));
}
// We need context of what slot the bundle is in.
for (int javaSlot = 0; javaSlot < inventory.getSize(); javaSlot++) {
GeyserItemStack bundle = inventory.getItem(javaSlot);
if (bundle.getBundleId() != bundleId) {
continue;
}
List<GeyserItemStack> contents = bundle.getBundleData().contents();
int rawSelectedSlot = bundleSlotData.getSlot();
if (rawSelectedSlot >= contents.size()) {
// Illegal?
return rejectRequest(request);
}
// Bedrock's indexes are flipped around - first item shown to it is the last index.
int slot = BundleCache.platformConvertSlot(contents.size(), rawSelectedSlot);
plan.setDesiredBundleSlot(slot);
// We'll need it even if the final destination isn't the cursor.
// I can't think of a situation where we shouldn't reject it and use a temp slot,
// but we will see.
if (!cursor.isEmpty()) {
return rejectRequest(request);
}
IntSet affectedSlots = new IntOpenHashSet(2);
affectedSlots.add(javaSlot);
GeyserItemStack bundledItem = contents.get(slot);
if (bundledItem.getNetId() != bundleSlotData.getStackNetworkId()) {
// !!!
return rejectRequest(request);
}
plan.add(Click.RIGHT_BUNDLE, javaSlot);
// If false, simple logic that matches nicely with Java Edition
if (!isCursor(inventorySlotData)) {
// Alas, two-click time.
int destSlot = translator.bedrockSlotToJava(inventorySlotData);
GeyserItemStack existing = inventory.getItem(destSlot);
// Empty slot is good, but otherwise let's just check that
// the two can stack...
if (!existing.isEmpty()) {
if (!InventoryUtils.canStack(bundledItem, existing)) {
return rejectRequest(request);
}
}
// Copy the full stack to the new slot.
plan.add(Click.LEFT, destSlot);
affectedSlots.add(destSlot);
}
if (sendCreativePackets) {
plan.executeForCreativeMode();
} else {
plan.execute(false);
}
return acceptRequest(request, translator.makeContainerEntries(session, inventory, affectedSlots));
}
// Could not find bundle in inventory
}
return rejectRequest(request);
}
private static final Fraction BUNDLE_IN_BUNDLE_WEIGHT = Fraction.getFraction(1, 16);
public static Fraction calculateBundleWeight(List<GeyserItemStack> contents) {
Fraction fraction = Fraction.ZERO;
for (GeyserItemStack content : contents) {
fraction = fraction.add(calculateWeight(content)
.multiplyBy(Fraction.getFraction(content.getAmount(), 1)));
}
return fraction;
}
private static Fraction calculateWeight(GeyserItemStack itemStack) {
if (itemStack.getBundleData() != null) {
return BUNDLE_IN_BUNDLE_WEIGHT.add(calculateBundleWeight(itemStack.getBundleData().contents()));
}
DataComponents components = itemStack.getComponents();
if (components != null) {
// NOTE: this seems to be Java-only, so it can technically cause a bundle weight desync,
// but it'll be so rare we can probably ignore it.
List<?> bees = components.get(DataComponentType.BEES);
if (bees != null && !bees.isEmpty()) {
// Bees be heavy, I guess.
return Fraction.ONE;
}
}
return Fraction.getFraction(1, itemStack.getComponentOrFallback(DataComponentType.MAX_STACK_SIZE, itemStack.asItem().defaultMaxStackSize()));
}
public static int capacityForItemStack(Fraction bundleWeight, GeyserItemStack itemStack) {
Fraction inverse = Fraction.ONE.subtract(bundleWeight);
return Math.max(inverse.divideBy(calculateWeight(itemStack)).intValue(), 0);
}
static boolean isBundle(ItemStackRequestSlotData slotData) {
return slotData.getContainerName().getContainer() == ContainerSlotType.DYNAMIC_CONTAINER;
}
static boolean isBundle(ClickPlan plan, int slot) {
return isBundle(plan.getItem(slot));
}
static boolean isBundle(GeyserItemStack stack) {
return stack.getBundleData() != null;
}
private BundleInventoryTranslator() {
}
}

Datei anzeigen

@ -86,6 +86,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.geysermc.geyser.translator.inventory.BundleInventoryTranslator.isBundle;
@AllArgsConstructor
public abstract class InventoryTranslator {
@ -241,6 +243,13 @@ public abstract class InventoryTranslator {
return rejectRequest(request);
}
// Might be a bundle action... let's check.
ItemStackResponse bundleResponse = BundleInventoryTranslator.handleBundle(session, this, inventory, request, false);
if (bundleResponse != null) {
// We can simplify a lot of logic because we aren't expecting multi-slot interactions.
return bundleResponse;
}
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
int destSlot = bedrockSlotToJava(transferAction.getDestination());
boolean isSourceCursor = isCursor(transferAction.getSource());
@ -393,6 +402,7 @@ public abstract class InventoryTranslator {
break;
}
case SWAP: {
// TODO breaks with bundles
SwapAction swapAction = (SwapAction) action;
ItemStackRequestSlotData source = swapAction.getSource();
ItemStackRequestSlotData destination = swapAction.getDestination();
@ -426,18 +436,24 @@ public abstract class InventoryTranslator {
}
}
// A note on all the bundle checks for clicks...
// Left clicking in these contexts can count as using the bundle
// and adding the stack to the contents of the bundle.
// In these cases, we can safely use right-clicking while holding the bundle
// as its stack size is 1.
if (isSourceCursor && isDestCursor) { //???
return rejectRequest(request);
} else if (isSourceCursor) { //swap cursor
if (InventoryUtils.canStack(cursor, plan.getItem(destSlot))) { //TODO: cannot simply swap if cursor stacks with slot (temp slot)
return rejectRequest(request);
}
plan.add(Click.LEFT, destSlot);
plan.add(isBundle(plan, destSlot) || isBundle(cursor) ? Click.RIGHT : Click.LEFT, destSlot);
} else if (isDestCursor) { //swap cursor
if (InventoryUtils.canStack(cursor, plan.getItem(sourceSlot))) { //TODO
return rejectRequest(request);
}
plan.add(Click.LEFT, sourceSlot);
plan.add(isBundle(plan, sourceSlot) || isBundle(cursor) ? Click.RIGHT : Click.LEFT, sourceSlot);
} else {
if (!cursor.isEmpty()) { //TODO: (temp slot)
return rejectRequest(request);
@ -449,7 +465,7 @@ public abstract class InventoryTranslator {
return rejectRequest(request);
}
plan.add(Click.LEFT, sourceSlot); //pickup source into cursor
plan.add(Click.LEFT, destSlot); //swap cursor with dest slot
plan.add(isBundle(plan, sourceSlot) || isBundle(plan, destSlot) ? Click.RIGHT : Click.LEFT, destSlot); //swap cursor with dest slot
plan.add(Click.LEFT, sourceSlot); //release cursor onto source
}
break;
@ -915,6 +931,11 @@ public abstract class InventoryTranslator {
}
public boolean checkNetId(GeyserSession session, Inventory inventory, ItemStackRequestSlotData slotInfoData) {
if (BundleInventoryTranslator.isBundle(slotInfoData)) {
// Will thoroughly be investigated, if needed, in bundle checks.
return true;
}
int netId = slotInfoData.getStackNetworkId();
// "In my testing, sometimes the client thinks the netId of an item in the crafting grid is 1, even though we never said it was.
// I think it only happens when we manually set the grid but that was my quick fix"

Datei anzeigen

@ -265,6 +265,15 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
return rejectRequest(request, false);
}
// Might be a bundle action... let's check.
// If we're in creative mode, instead of replacing logic (more hassle for updates),
// let's just reuse as much logic as possible!!
ItemStackResponse bundleResponse = BundleInventoryTranslator.handleBundle(session, this, inventory, request, true);
if (bundleResponse != null) {
// We can simplify a lot of logic because we aren't expecting multi-slot interactions.
return bundleResponse;
}
int transferAmount = transferAction.getCount();
if (isCursor(transferAction.getDestination())) {
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
@ -415,6 +424,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
@Override
protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
ItemStack javaCreativeItem = null;
boolean bundle = false;
IntSet affectedSlots = new IntOpenHashSet();
CraftState craftState = CraftState.START;
for (ItemStackRequestAction action : request.getActions()) {
@ -469,8 +479,10 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
if (isCursor(transferAction.getDestination())) {
if (session.getPlayerInventory().getCursor().isEmpty()) {
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
session.getBundleCache().initialize(newItemStack);
newItemStack.setAmount(transferAction.getCount());
session.getPlayerInventory().setCursor(newItemStack, session);
bundle = newItemStack.getBundleData() != null;
} else {
session.getPlayerInventory().getCursor().add(transferAction.getCount());
}
@ -479,8 +491,10 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
int destSlot = bedrockSlotToJava(transferAction.getDestination());
if (inventory.getItem(destSlot).isEmpty()) {
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
session.getBundleCache().initialize(newItemStack);
newItemStack.setAmount(transferAction.getCount());
inventory.setItem(destSlot, newItemStack, session);
bundle = newItemStack.getBundleData() != null;
} else {
inventory.getItem(destSlot).add(transferAction.getCount());
}
@ -520,7 +534,11 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
int slot = it.nextInt();
sendCreativeAction(session, inventory, slot);
}
return acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
// On the bundle check:
// We can also accept the request, but sending a bad request indicates to Geyser to refresh the inventory
// and we need to refresh the inventory to send the bundle ID/inventory to the client.
// It's not great, but I don't want to create a container class for request responses
return bundle ? rejectRequest(request, false) : acceptRequest(request, makeContainerEntries(session, inventory, affectedSlots));
}
private static void sendCreativeAction(GeyserSession session, Inventory inventory, int slot) {

Datei anzeigen

@ -411,6 +411,8 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
session.useItem(Hand.MAIN_HAND);
session.getBundleCache().awaitRelease();
List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
if (packet.getActions().size() == 1 && !legacySlots.isEmpty()) {
InventoryActionData actionData = packet.getActions().get(0);
@ -439,10 +441,8 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
break;
case ITEM_RELEASE:
if (packet.getActionType() == 0) {
// Followed to the Minecraft Protocol specification outlined at wiki.vg
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO,
Direction.DOWN, 0);
session.sendDownstreamGamePacket(releaseItemPacket);
session.releaseItem();
session.getBundleCache().markRelease();
}
break;
case ITEM_USE_ON_ENTITY:

Datei anzeigen

@ -64,6 +64,7 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
}
GeyserItemStack newItem = GeyserItemStack.from(packet.getItems()[i]);
session.getBundleCache().initialize(newItem);
inventory.setItem(i, newItem, session);
}
@ -73,7 +74,9 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
inventory.setStateId(stateId);
session.getPlayerInventory().setCursor(GeyserItemStack.from(packet.getCarriedItem()), session);
GeyserItemStack cursor = GeyserItemStack.from(packet.getCarriedItem());
session.getBundleCache().initialize(cursor);
session.getPlayerInventory().setCursor(cursor, session);
InventoryUtils.updateCursor(session);
if (session.getInventoryTranslator() instanceof SmithingInventoryTranslator) {

Datei anzeigen

@ -93,6 +93,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
}
GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
session.getBundleCache().initialize(newItem);
if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
// In rare cases, the window ID can still be 0 but Java treats it as valid
// This behavior still exists as of Java Edition 1.21.2, despite the new packet

Datei anzeigen

@ -38,6 +38,7 @@ public class JavaSetCursorItemTranslator extends PacketTranslator<ClientboundSet
@Override
public void translate(GeyserSession session, ClientboundSetCursorItemPacket packet) {
GeyserItemStack newItem = GeyserItemStack.from(packet.getContents());
session.getBundleCache().initialize(newItem);
session.getPlayerInventory().setCursor(newItem, session);
InventoryUtils.updateCursor(session);
}

Datei anzeigen

@ -55,7 +55,9 @@ public class JavaSetPlayerInventoryTranslator extends PacketTranslator<Clientbou
return;
}
session.getPlayerInventory().setItem(slot, GeyserItemStack.from(packet.getContents()), session);
GeyserItemStack newItem = GeyserItemStack.from(packet.getContents());
session.getBundleCache().initialize(newItem);
session.getPlayerInventory().setItem(slot, newItem, session);
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), slot);
}
}

Datei anzeigen

@ -140,6 +140,7 @@ public class InventoryUtils {
}
}
session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR);
session.getBundleCache().onInventoryClose(inventory);
session.setOpenInventory(null);
}

Datei anzeigen

@ -0,0 +1,911 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.geysermc.geyser.util.thirdparty; // Geyser
import java.math.BigInteger;
import java.util.Objects;
/**
* {@link Fraction} is a {@link Number} implementation that
* stores fractions accurately.
*
* <p>This class is immutable, and interoperable with most methods that accept
* a {@link Number}.</p>
*
* <p>Note that this class is intended for common use cases, it is <i>int</i>
* based and thus suffers from various overflow issues. For a BigInteger based
* equivalent, please see the Commons Math BigFraction class.</p>
*
* @since 2.0
*/
// Geyser: Java Edition uses this for 1.21.3 bundle calculation. Rather than
// Reimplementing an open-source class or bringing in a whole library,
// the single class is used to ensure accuracy.
public final class Fraction extends Number implements Comparable<Fraction> {
/**
* Required for serialization support. Lang version 2.0.
*
* @see java.io.Serializable
*/
private static final long serialVersionUID = 65382027393090L;
/**
* {@link Fraction} representation of 0.
*/
public static final Fraction ZERO = new Fraction(0, 1);
/**
* {@link Fraction} representation of 1.
*/
public static final Fraction ONE = new Fraction(1, 1);
/**
* {@link Fraction} representation of 1/2.
*/
public static final Fraction ONE_HALF = new Fraction(1, 2);
/**
* {@link Fraction} representation of 1/3.
*/
public static final Fraction ONE_THIRD = new Fraction(1, 3);
/**
* {@link Fraction} representation of 2/3.
*/
public static final Fraction TWO_THIRDS = new Fraction(2, 3);
/**
* {@link Fraction} representation of 1/4.
*/
public static final Fraction ONE_QUARTER = new Fraction(1, 4);
/**
* {@link Fraction} representation of 2/4.
*/
public static final Fraction TWO_QUARTERS = new Fraction(2, 4);
/**
* {@link Fraction} representation of 3/4.
*/
public static final Fraction THREE_QUARTERS = new Fraction(3, 4);
/**
* {@link Fraction} representation of 1/5.
*/
public static final Fraction ONE_FIFTH = new Fraction(1, 5);
/**
* {@link Fraction} representation of 2/5.
*/
public static final Fraction TWO_FIFTHS = new Fraction(2, 5);
/**
* {@link Fraction} representation of 3/5.
*/
public static final Fraction THREE_FIFTHS = new Fraction(3, 5);
/**
* {@link Fraction} representation of 4/5.
*/
public static final Fraction FOUR_FIFTHS = new Fraction(4, 5);
/**
* Add two integers, checking for overflow.
*
* @param x an addend
* @param y an addend
* @return the sum {@code x+y}
* @throws ArithmeticException if the result can not be represented as
* an int
*/
private static int addAndCheck(final int x, final int y) {
final long s = (long) x + (long) y;
if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) {
throw new ArithmeticException("overflow: add");
}
return (int) s;
}
/**
* Creates a {@link Fraction} instance from a {@code double} value.
*
* <p>This method uses the <a href="https://web.archive.org/web/20210516065058/http%3A//archives.math.utk.edu/articles/atuyl/confrac/">
* continued fraction algorithm</a>, computing a maximum of
* 25 convergents and bounding the denominator by 10,000.</p>
*
* @param value the double value to convert
* @return a new fraction instance that is close to the value
* @throws ArithmeticException if {@code |value| &gt; Integer.MAX_VALUE}
* or {@code value = NaN}
* @throws ArithmeticException if the calculated denominator is {@code zero}
* @throws ArithmeticException if the algorithm does not converge
*/
public static Fraction getFraction(double value) {
final int sign = value < 0 ? -1 : 1;
value = Math.abs(value);
if (value > Integer.MAX_VALUE || Double.isNaN(value)) {
throw new ArithmeticException("The value must not be greater than Integer.MAX_VALUE or NaN");
}
final int wholeNumber = (int) value;
value -= wholeNumber;
int numer0 = 0; // the pre-previous
int denom0 = 1; // the pre-previous
int numer1 = 1; // the previous
int denom1 = 0; // the previous
int numer2; // the current, setup in calculation
int denom2; // the current, setup in calculation
int a1 = (int) value;
int a2;
double x1 = 1;
double x2;
double y1 = value - a1;
double y2;
double delta1, delta2 = Double.MAX_VALUE;
double fraction;
int i = 1;
do {
delta1 = delta2;
a2 = (int) (x1 / y1);
x2 = y1;
y2 = x1 - a2 * y1;
numer2 = a1 * numer1 + numer0;
denom2 = a1 * denom1 + denom0;
fraction = (double) numer2 / (double) denom2;
delta2 = Math.abs(value - fraction);
a1 = a2;
x1 = x2;
y1 = y2;
numer0 = numer1;
denom0 = denom1;
numer1 = numer2;
denom1 = denom2;
i++;
} while (delta1 > delta2 && denom2 <= 10000 && denom2 > 0 && i < 25);
if (i == 25) {
throw new ArithmeticException("Unable to convert double to fraction");
}
return getReducedFraction((numer0 + wholeNumber * denom0) * sign, denom0);
}
/**
* Creates a {@link Fraction} instance with the 2 parts
* of a fraction Y/Z.
*
* <p>Any negative signs are resolved to be on the numerator.</p>
*
* @param numerator the numerator, for example the three in 'three sevenths'
* @param denominator the denominator, for example the seven in 'three sevenths'
* @return a new fraction instance
* @throws ArithmeticException if the denominator is {@code zero}
* or the denominator is {@code negative} and the numerator is {@code Integer#MIN_VALUE}
*/
public static Fraction getFraction(int numerator, int denominator) {
if (denominator == 0) {
throw new ArithmeticException("The denominator must not be zero");
}
if (denominator < 0) {
if (numerator == Integer.MIN_VALUE || denominator == Integer.MIN_VALUE) {
throw new ArithmeticException("overflow: can't negate");
}
numerator = -numerator;
denominator = -denominator;
}
return new Fraction(numerator, denominator);
}
/**
* Creates a {@link Fraction} instance with the 3 parts
* of a fraction X Y/Z.
*
* <p>The negative sign must be passed in on the whole number part.</p>
*
* @param whole the whole number, for example the one in 'one and three sevenths'
* @param numerator the numerator, for example the three in 'one and three sevenths'
* @param denominator the denominator, for example the seven in 'one and three sevenths'
* @return a new fraction instance
* @throws ArithmeticException if the denominator is {@code zero}
* @throws ArithmeticException if the denominator is negative
* @throws ArithmeticException if the numerator is negative
* @throws ArithmeticException if the resulting numerator exceeds
* {@code Integer.MAX_VALUE}
*/
public static Fraction getFraction(final int whole, final int numerator, final int denominator) {
if (denominator == 0) {
throw new ArithmeticException("The denominator must not be zero");
}
if (denominator < 0) {
throw new ArithmeticException("The denominator must not be negative");
}
if (numerator < 0) {
throw new ArithmeticException("The numerator must not be negative");
}
final long numeratorValue;
if (whole < 0) {
numeratorValue = whole * (long) denominator - numerator;
} else {
numeratorValue = whole * (long) denominator + numerator;
}
if (numeratorValue < Integer.MIN_VALUE || numeratorValue > Integer.MAX_VALUE) {
throw new ArithmeticException("Numerator too large to represent as an Integer.");
}
return new Fraction((int) numeratorValue, denominator);
}
/**
* Creates a Fraction from a {@link String}.
*
* <p>The formats accepted are:</p>
*
* <ol>
* <li>{@code double} String containing a dot</li>
* <li>'X Y/Z'</li>
* <li>'Y/Z'</li>
* <li>'X' (a simple whole number)</li>
* </ol>
* <p>and a .</p>
*
* @param str the string to parse, must not be {@code null}
* @return the new {@link Fraction} instance
* @throws NullPointerException if the string is {@code null}
* @throws NumberFormatException if the number format is invalid
*/
public static Fraction getFraction(String str) {
Objects.requireNonNull(str, "str");
// parse double format
int pos = str.indexOf('.');
if (pos >= 0) {
return getFraction(Double.parseDouble(str));
}
// parse X Y/Z format
pos = str.indexOf(' ');
if (pos > 0) {
final int whole = Integer.parseInt(str.substring(0, pos));
str = str.substring(pos + 1);
pos = str.indexOf('/');
if (pos < 0) {
throw new NumberFormatException("The fraction could not be parsed as the format X Y/Z");
}
final int numer = Integer.parseInt(str.substring(0, pos));
final int denom = Integer.parseInt(str.substring(pos + 1));
return getFraction(whole, numer, denom);
}
// parse Y/Z format
pos = str.indexOf('/');
if (pos < 0) {
// simple whole number
return getFraction(Integer.parseInt(str), 1);
}
final int numer = Integer.parseInt(str.substring(0, pos));
final int denom = Integer.parseInt(str.substring(pos + 1));
return getFraction(numer, denom);
}
/**
* Creates a reduced {@link Fraction} instance with the 2 parts
* of a fraction Y/Z.
*
* <p>For example, if the input parameters represent 2/4, then the created
* fraction will be 1/2.</p>
*
* <p>Any negative signs are resolved to be on the numerator.</p>
*
* @param numerator the numerator, for example the three in 'three sevenths'
* @param denominator the denominator, for example the seven in 'three sevenths'
* @return a new fraction instance, with the numerator and denominator reduced
* @throws ArithmeticException if the denominator is {@code zero}
*/
public static Fraction getReducedFraction(int numerator, int denominator) {
if (denominator == 0) {
throw new ArithmeticException("The denominator must not be zero");
}
if (numerator == 0) {
return ZERO; // normalize zero.
}
// allow 2^k/-2^31 as a valid fraction (where k>0)
if (denominator == Integer.MIN_VALUE && (numerator & 1) == 0) {
numerator /= 2;
denominator /= 2;
}
if (denominator < 0) {
if (numerator == Integer.MIN_VALUE || denominator == Integer.MIN_VALUE) {
throw new ArithmeticException("overflow: can't negate");
}
numerator = -numerator;
denominator = -denominator;
}
// simplify fraction.
final int gcd = greatestCommonDivisor(numerator, denominator);
numerator /= gcd;
denominator /= gcd;
return new Fraction(numerator, denominator);
}
/**
* Gets the greatest common divisor of the absolute value of
* two numbers, using the "binary gcd" method which avoids
* division and modulo operations. See Knuth 4.5.2 algorithm B.
* This algorithm is due to Josef Stein (1961).
*
* @param u a non-zero number
* @param v a non-zero number
* @return the greatest common divisor, never zero
*/
private static int greatestCommonDivisor(int u, int v) {
// From Commons Math:
if (u == 0 || v == 0) {
if (u == Integer.MIN_VALUE || v == Integer.MIN_VALUE) {
throw new ArithmeticException("overflow: gcd is 2^31");
}
return Math.abs(u) + Math.abs(v);
}
// if either operand is abs 1, return 1:
if (Math.abs(u) == 1 || Math.abs(v) == 1) {
return 1;
}
// keep u and v negative, as negative integers range down to
// -2^31, while positive numbers can only be as large as 2^31-1
// (i.e. we can't necessarily negate a negative number without
// overflow)
if (u > 0) {
u = -u;
} // make u negative
if (v > 0) {
v = -v;
} // make v negative
// B1. [Find power of 2]
int k = 0;
while ((u & 1) == 0 && (v & 1) == 0 && k < 31) { // while u and v are both even...
u /= 2;
v /= 2;
k++; // cast out twos.
}
if (k == 31) {
throw new ArithmeticException("overflow: gcd is 2^31");
}
// B2. Initialize: u and v have been divided by 2^k and at least
// one is odd.
int t = (u & 1) == 1 ? v : -(u / 2)/* B3 */;
// t negative: u was odd, v may be even (t replaces v)
// t positive: u was even, v is odd (t replaces u)
do {
/* assert u<0 && v<0; */
// B4/B3: cast out twos from t.
while ((t & 1) == 0) { // while t is even.
t /= 2; // cast out twos
}
// B5 [reset max(u,v)]
if (t > 0) {
u = -t;
} else {
v = t;
}
// B6/B3. at this point both u and v should be odd.
t = (v - u) / 2;
// |u| larger: t positive (replace u)
// |v| larger: t negative (replace v)
} while (t != 0);
return -u * (1 << k); // gcd is u*2^k
}
/**
* Multiply two integers, checking for overflow.
*
* @param x a factor
* @param y a factor
* @return the product {@code x*y}
* @throws ArithmeticException if the result can not be represented as
* an int
*/
private static int mulAndCheck(final int x, final int y) {
final long m = (long) x * (long) y;
if (m < Integer.MIN_VALUE || m > Integer.MAX_VALUE) {
throw new ArithmeticException("overflow: mul");
}
return (int) m;
}
/**
* Multiply two non-negative integers, checking for overflow.
*
* @param x a non-negative factor
* @param y a non-negative factor
* @return the product {@code x*y}
* @throws ArithmeticException if the result can not be represented as
* an int
*/
private static int mulPosAndCheck(final int x, final int y) {
/* assert x>=0 && y>=0; */
final long m = (long) x * (long) y;
if (m > Integer.MAX_VALUE) {
throw new ArithmeticException("overflow: mulPos");
}
return (int) m;
}
/**
* Subtract two integers, checking for overflow.
*
* @param x the minuend
* @param y the subtrahend
* @return the difference {@code x-y}
* @throws ArithmeticException if the result can not be represented as
* an int
*/
private static int subAndCheck(final int x, final int y) {
final long s = (long) x - (long) y;
if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) {
throw new ArithmeticException("overflow: add");
}
return (int) s;
}
/**
* The numerator number part of the fraction (the three in three sevenths).
*/
private final int numerator;
/**
* The denominator number part of the fraction (the seven in three sevenths).
*/
private final int denominator;
/**
* Cached output hashCode (class is immutable).
*/
private transient int hashCode;
/**
* Cached output toString (class is immutable).
*/
private transient String toString;
/**
* Cached output toProperString (class is immutable).
*/
private transient String toProperString;
/**
* Constructs a {@link Fraction} instance with the 2 parts
* of a fraction Y/Z.
*
* @param numerator the numerator, for example the three in 'three sevenths'
* @param denominator the denominator, for example the seven in 'three sevenths'
*/
private Fraction(final int numerator, final int denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
/**
* Gets a fraction that is the positive equivalent of this one.
* <p>More precisely: {@code (fraction &gt;= 0 ? this : -fraction)}</p>
*
* <p>The returned fraction is not reduced.</p>
*
* @return {@code this} if it is positive, or a new positive fraction
* instance with the opposite signed numerator
*/
public Fraction abs() {
if (numerator >= 0) {
return this;
}
return negate();
}
/**
* Adds the value of this fraction to another, returning the result in reduced form.
* The algorithm follows Knuth, 4.5.1.
*
* @param fraction the fraction to add, must not be {@code null}
* @return a {@link Fraction} instance with the resulting values
* @throws NullPointerException if the fraction is {@code null}
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* {@code Integer.MAX_VALUE}
*/
public Fraction add(final Fraction fraction) {
return addSub(fraction, true /* add */);
}
/**
* Implement add and subtract using algorithm described in Knuth 4.5.1.
*
* @param fraction the fraction to subtract, must not be {@code null}
* @param isAdd true to add, false to subtract
* @return a {@link Fraction} instance with the resulting values
* @throws IllegalArgumentException if the fraction is {@code null}
* @throws ArithmeticException if the resulting numerator or denominator
* cannot be represented in an {@code int}.
*/
private Fraction addSub(final Fraction fraction, final boolean isAdd) {
Objects.requireNonNull(fraction, "fraction");
// zero is identity for addition.
if (numerator == 0) {
return isAdd ? fraction : fraction.negate();
}
if (fraction.numerator == 0) {
return this;
}
// if denominators are randomly distributed, d1 will be 1 about 61%
// of the time.
final int d1 = greatestCommonDivisor(denominator, fraction.denominator);
if (d1 == 1) {
// result is ( (u*v' +/- u'v) / u'v')
final int uvp = mulAndCheck(numerator, fraction.denominator);
final int upv = mulAndCheck(fraction.numerator, denominator);
return new Fraction(isAdd ? addAndCheck(uvp, upv) : subAndCheck(uvp, upv), mulPosAndCheck(denominator,
fraction.denominator));
}
// the quantity 't' requires 65 bits of precision; see knuth 4.5.1
// exercise 7. we're going to use a BigInteger.
// t = u(v'/d1) +/- v(u'/d1)
final BigInteger uvp = BigInteger.valueOf(numerator).multiply(BigInteger.valueOf(fraction.denominator / d1));
final BigInteger upv = BigInteger.valueOf(fraction.numerator).multiply(BigInteger.valueOf(denominator / d1));
final BigInteger t = isAdd ? uvp.add(upv) : uvp.subtract(upv);
// but d2 doesn't need extra precision because
// d2 = gcd(t,d1) = gcd(t mod d1, d1)
final int tmodd1 = t.mod(BigInteger.valueOf(d1)).intValue();
final int d2 = tmodd1 == 0 ? d1 : greatestCommonDivisor(tmodd1, d1);
// result is (t/d2) / (u'/d1)(v'/d2)
final BigInteger w = t.divide(BigInteger.valueOf(d2));
if (w.bitLength() > 31) {
throw new ArithmeticException("overflow: numerator too large after multiply");
}
return new Fraction(w.intValue(), mulPosAndCheck(denominator / d1, fraction.denominator / d2));
}
/**
* Compares this object to another based on size.
*
* <p>Note: this class has a natural ordering that is inconsistent
* with equals, because, for example, equals treats 1/2 and 2/4 as
* different, whereas compareTo treats them as equal.
*
* @param other the object to compare to
* @return -1 if this is less, 0 if equal, +1 if greater
* @throws ClassCastException if the object is not a {@link Fraction}
* @throws NullPointerException if the object is {@code null}
*/
@Override
public int compareTo(final Fraction other) {
if (this == other) {
return 0;
}
if (numerator == other.numerator && denominator == other.denominator) {
return 0;
}
// otherwise see which is less
final long first = (long) numerator * (long) other.denominator;
final long second = (long) other.numerator * (long) denominator;
return Long.compare(first, second);
}
/**
* Divide the value of this fraction by another.
*
* @param fraction the fraction to divide by, must not be {@code null}
* @return a {@link Fraction} instance with the resulting values
* @throws NullPointerException if the fraction is {@code null}
* @throws ArithmeticException if the fraction to divide by is zero
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* {@code Integer.MAX_VALUE}
*/
public Fraction divideBy(final Fraction fraction) {
Objects.requireNonNull(fraction, "fraction");
if (fraction.numerator == 0) {
throw new ArithmeticException("The fraction to divide by must not be zero");
}
return multiplyBy(fraction.invert());
}
/**
* Gets the fraction as a {@code double}. This calculates the fraction
* as the numerator divided by denominator.
*
* @return the fraction as a {@code double}
*/
@Override
public double doubleValue() {
return (double) numerator / (double) denominator;
}
/**
* Compares this fraction to another object to test if they are equal..
*
* <p>To be equal, both values must be equal. Thus 2/4 is not equal to 1/2.</p>
*
* @param obj the reference object with which to compare
* @return {@code true} if this object is equal
*/
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Fraction)) {
return false;
}
final Fraction other = (Fraction) obj;
return getNumerator() == other.getNumerator() && getDenominator() == other.getDenominator();
}
/**
* Gets the fraction as a {@code float}. This calculates the fraction
* as the numerator divided by denominator.
*
* @return the fraction as a {@code float}
*/
@Override
public float floatValue() {
return (float) numerator / (float) denominator;
}
/**
* Gets the denominator part of the fraction.
*
* @return the denominator fraction part
*/
public int getDenominator() {
return denominator;
}
/**
* Gets the numerator part of the fraction.
*
* <p>This method may return a value greater than the denominator, an
* improper fraction, such as the seven in 7/4.</p>
*
* @return the numerator fraction part
*/
public int getNumerator() {
return numerator;
}
/**
* Gets the proper numerator, always positive.
*
* <p>An improper fraction 7/4 can be resolved into a proper one, 1 3/4.
* This method returns the 3 from the proper fraction.</p>
*
* <p>If the fraction is negative such as -7/4, it can be resolved into
* -1 3/4, so this method returns the positive proper numerator, 3.</p>
*
* @return the numerator fraction part of a proper fraction, always positive
*/
public int getProperNumerator() {
return Math.abs(numerator % denominator);
}
/**
* Gets the proper whole part of the fraction.
*
* <p>An improper fraction 7/4 can be resolved into a proper one, 1 3/4.
* This method returns the 1 from the proper fraction.</p>
*
* <p>If the fraction is negative such as -7/4, it can be resolved into
* -1 3/4, so this method returns the positive whole part -1.</p>
*
* @return the whole fraction part of a proper fraction, that includes the sign
*/
public int getProperWhole() {
return numerator / denominator;
}
/**
* Gets a hashCode for the fraction.
*
* @return a hash code value for this object
*/
@Override
public int hashCode() {
if (hashCode == 0) {
// hash code update should be atomic.
hashCode = 37 * (37 * 17 + getNumerator()) + getDenominator();
}
return hashCode;
}
/**
* Gets the fraction as an {@code int}. This returns the whole number
* part of the fraction.
*
* @return the whole number fraction part
*/
@Override
public int intValue() {
return numerator / denominator;
}
/**
* Gets a fraction that is the inverse (1/fraction) of this one.
*
* <p>The returned fraction is not reduced.</p>
*
* @return a new fraction instance with the numerator and denominator
* inverted.
* @throws ArithmeticException if the fraction represents zero.
*/
public Fraction invert() {
if (numerator == 0) {
throw new ArithmeticException("Unable to invert zero.");
}
if (numerator==Integer.MIN_VALUE) {
throw new ArithmeticException("overflow: can't negate numerator");
}
if (numerator<0) {
return new Fraction(-denominator, -numerator);
}
return new Fraction(denominator, numerator);
}
/**
* Gets the fraction as a {@code long}. This returns the whole number
* part of the fraction.
*
* @return the whole number fraction part
*/
@Override
public long longValue() {
return (long) numerator / denominator;
}
/**
* Multiplies the value of this fraction by another, returning the
* result in reduced form.
*
* @param fraction the fraction to multiply by, must not be {@code null}
* @return a {@link Fraction} instance with the resulting values
* @throws NullPointerException if the fraction is {@code null}
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* {@code Integer.MAX_VALUE}
*/
public Fraction multiplyBy(final Fraction fraction) {
Objects.requireNonNull(fraction, "fraction");
if (numerator == 0 || fraction.numerator == 0) {
return ZERO;
}
// knuth 4.5.1
// make sure we don't overflow unless the result *must* overflow.
final int d1 = greatestCommonDivisor(numerator, fraction.denominator);
final int d2 = greatestCommonDivisor(fraction.numerator, denominator);
return getReducedFraction(mulAndCheck(numerator / d1, fraction.numerator / d2),
mulPosAndCheck(denominator / d2, fraction.denominator / d1));
}
/**
* Gets a fraction that is the negative (-fraction) of this one.
*
* <p>The returned fraction is not reduced.</p>
*
* @return a new fraction instance with the opposite signed numerator
*/
public Fraction negate() {
// the positive range is one smaller than the negative range of an int.
if (numerator==Integer.MIN_VALUE) {
throw new ArithmeticException("overflow: too large to negate");
}
return new Fraction(-numerator, denominator);
}
/**
* Gets a fraction that is raised to the passed in power.
*
* <p>The returned fraction is in reduced form.</p>
*
* @param power the power to raise the fraction to
* @return {@code this} if the power is one, {@link #ONE} if the power
* is zero (even if the fraction equals ZERO) or a new fraction instance
* raised to the appropriate power
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* {@code Integer.MAX_VALUE}
*/
public Fraction pow(final int power) {
if (power == 1) {
return this;
}
if (power == 0) {
return ONE;
}
if (power < 0) {
if (power == Integer.MIN_VALUE) { // MIN_VALUE can't be negated.
return this.invert().pow(2).pow(-(power / 2));
}
return this.invert().pow(-power);
}
final Fraction f = this.multiplyBy(this);
if (power % 2 == 0) { // if even...
return f.pow(power / 2);
}
return f.pow(power / 2).multiplyBy(this);
}
/**
* Reduce the fraction to the smallest values for the numerator and
* denominator, returning the result.
*
* <p>For example, if this fraction represents 2/4, then the result
* will be 1/2.</p>
*
* @return a new reduced fraction instance, or this if no simplification possible
*/
public Fraction reduce() {
if (numerator == 0) {
return equals(ZERO) ? this : ZERO;
}
final int gcd = greatestCommonDivisor(Math.abs(numerator), denominator);
if (gcd == 1) {
return this;
}
return getFraction(numerator / gcd, denominator / gcd);
}
/**
* Subtracts the value of another fraction from the value of this one,
* returning the result in reduced form.
*
* @param fraction the fraction to subtract, must not be {@code null}
* @return a {@link Fraction} instance with the resulting values
* @throws NullPointerException if the fraction is {@code null}
* @throws ArithmeticException if the resulting numerator or denominator
* cannot be represented in an {@code int}.
*/
public Fraction subtract(final Fraction fraction) {
return addSub(fraction, false /* subtract */);
}
/**
* Gets the fraction as a proper {@link String} in the format X Y/Z.
*
* <p>The format used in '<i>wholeNumber</i> <i>numerator</i>/<i>denominator</i>'.
* If the whole number is zero it will be omitted. If the numerator is zero,
* only the whole number is returned.</p>
*
* @return a {@link String} form of the fraction
*/
public String toProperString() {
if (toProperString == null) {
if (numerator == 0) {
toProperString = "0";
} else if (numerator == denominator) {
toProperString = "1";
} else if (numerator == -1 * denominator) {
toProperString = "-1";
} else if ((numerator > 0 ? -numerator : numerator) < -denominator) {
// note that we do the magnitude comparison test above with
// NEGATIVE (not positive) numbers, since negative numbers
// have a larger range. otherwise numerator==Integer.MIN_VALUE
// is handled incorrectly.
final int properNumerator = getProperNumerator();
if (properNumerator == 0) {
toProperString = Integer.toString(getProperWhole());
} else {
toProperString = getProperWhole() + " " + properNumerator + "/" + getDenominator();
}
} else {
toProperString = getNumerator() + "/" + getDenominator();
}
}
return toProperString;
}
/**
* Gets the fraction as a {@link String}.
*
* <p>The format used is '<i>numerator</i>/<i>denominator</i>' always.
*
* @return a {@link String} form of the fraction
*/
@Override
public String toString() {
if (toString == null) {
toString = getNumerator() + "/" + getDenominator();
}
return toString;
}
}

@ -1 +1 @@
Subproject commit e277062f3bccbe772baefcd631f0a5442311467c
Subproject commit 6808d0e16a85e5e569d9d7f89ace59c73196c1f4