Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2025-01-07 22:00:24 +01:00
Bundle support (#5145)
* 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:
Ursprung
a19f0305fb
Commit
ddd1afabd1
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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}
|
||||
*/
|
||||
|
@ -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.
|
||||
|
383
core/src/main/java/org/geysermc/geyser/session/cache/BundleCache.java
vendored
Normale Datei
383
core/src/main/java/org/geysermc/geyser/session/cache/BundleCache.java
vendored
Normale Datei
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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) {
|
||||
|
@ -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:
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +140,7 @@ public class InventoryUtils {
|
||||
}
|
||||
}
|
||||
session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR);
|
||||
session.getBundleCache().onInventoryClose(inventory);
|
||||
session.setOpenInventory(null);
|
||||
}
|
||||
|
||||
|
911
core/src/main/java/org/geysermc/geyser/util/thirdparty/Fraction.java
vendored
Normale Datei
911
core/src/main/java/org/geysermc/geyser/util/thirdparty/Fraction.java
vendored
Normale Datei
@ -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| > 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 >= 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
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren