Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2025-01-08 14:20:11 +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.Registries;
|
||||||
import org.geysermc.geyser.registry.type.ItemMapping;
|
import org.geysermc.geyser.registry.type.ItemMapping;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
|
import org.geysermc.geyser.session.cache.BundleCache;
|
||||||
import org.geysermc.geyser.translator.item.ItemTranslator;
|
import org.geysermc.geyser.translator.item.ItemTranslator;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
|
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
|
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
|
||||||
@ -59,19 +60,23 @@ public class GeyserItemStack {
|
|||||||
private DataComponents components;
|
private DataComponents components;
|
||||||
private int netId;
|
private int netId;
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
private BundleCache.BundleData bundleData;
|
||||||
|
|
||||||
@Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
|
||||||
@EqualsAndHashCode.Exclude
|
@EqualsAndHashCode.Exclude
|
||||||
private Item item;
|
private Item item;
|
||||||
|
|
||||||
private GeyserItemStack(int javaId, int amount, DataComponents components) {
|
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.javaId = javaId;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
this.components = components;
|
this.components = components;
|
||||||
this.netId = netId;
|
this.netId = netId;
|
||||||
|
this.bundleData = bundleData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NonNull GeyserItemStack of(int javaId, int amount) {
|
public static @NonNull GeyserItemStack of(int javaId, int amount) {
|
||||||
@ -173,6 +178,24 @@ public class GeyserItemStack {
|
|||||||
return isEmpty() ? 0 : netId;
|
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) {
|
public void add(int add) {
|
||||||
amount += add;
|
amount += add;
|
||||||
}
|
}
|
||||||
@ -186,6 +209,21 @@ public class GeyserItemStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable ItemStack getItemStack(int newAmount) {
|
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);
|
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.Builder itemData = ItemTranslator.translateToBedrock(session, javaId, amount, components);
|
||||||
itemData.netId(getNetId());
|
itemData.netId(getNetId());
|
||||||
itemData.usingNetId(true);
|
itemData.usingNetId(true);
|
||||||
return itemData.build();
|
|
||||||
|
return session.getBundleCache().checkForBundle(this, itemData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ItemMapping getMapping(GeyserSession session) {
|
public ItemMapping getMapping(GeyserSession session) {
|
||||||
@ -229,6 +268,6 @@ public class GeyserItemStack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public GeyserItemStack copy(int newAmount) {
|
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()) {
|
if (!newItem.isEmpty()) {
|
||||||
ItemDefinition oldMapping = ItemTranslator.getBedrockItemDefinition(session, oldItem);
|
ItemDefinition oldMapping = ItemTranslator.getBedrockItemDefinition(session, oldItem);
|
||||||
ItemDefinition newMapping = ItemTranslator.getBedrockItemDefinition(session, newItem);
|
ItemDefinition newMapping = ItemTranslator.getBedrockItemDefinition(session, newItem);
|
||||||
if (oldMapping.equals(newMapping)) {
|
if (oldMapping.equals(newMapping)) {
|
||||||
newItem.setNetId(oldItem.getNetId());
|
newItem.setNetId(oldItem.getNetId());
|
||||||
|
newItem.mergeBundleData(session, oldItem.getBundleData());
|
||||||
} else {
|
} else {
|
||||||
newItem.setNetId(session.getNextItemNetId());
|
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
|
@AllArgsConstructor
|
||||||
public enum Click {
|
public enum Click {
|
||||||
LEFT(ContainerActionType.CLICK_ITEM, ClickItemAction.LEFT_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(ContainerActionType.CLICK_ITEM, ClickItemAction.RIGHT_CLICK),
|
||||||
|
RIGHT_BUNDLE(ContainerActionType.CLICK_ITEM, ClickItemAction.RIGHT_CLICK),
|
||||||
LEFT_SHIFT(ContainerActionType.SHIFT_CLICK_ITEM, ShiftClickItemAction.LEFT_CLICK),
|
LEFT_SHIFT(ContainerActionType.SHIFT_CLICK_ITEM, ShiftClickItemAction.LEFT_CLICK),
|
||||||
DROP_ONE(ContainerActionType.DROP_ITEM, DropItemAction.DROP_FROM_SELECTED),
|
DROP_ONE(ContainerActionType.DROP_ITEM, DropItemAction.DROP_FROM_SELECTED),
|
||||||
DROP_ALL(ContainerActionType.DROP_ITEM, DropItemAction.DROP_SELECTED_STACK),
|
DROP_ALL(ContainerActionType.DROP_ITEM, DropItemAction.DROP_SELECTED_STACK),
|
||||||
|
@ -25,19 +25,26 @@
|
|||||||
|
|
||||||
package org.geysermc.geyser.inventory.click;
|
package org.geysermc.geyser.inventory.click;
|
||||||
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerActionType;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.inventory.ContainerType;
|
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.inventory.MoveToHotbarAction;
|
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
|
|
||||||
import it.unimi.dsi.fastutil.ints.*;
|
|
||||||
import org.geysermc.geyser.inventory.GeyserItemStack;
|
import org.geysermc.geyser.inventory.GeyserItemStack;
|
||||||
import org.geysermc.geyser.inventory.Inventory;
|
import org.geysermc.geyser.inventory.Inventory;
|
||||||
import org.geysermc.geyser.inventory.SlotType;
|
import org.geysermc.geyser.inventory.SlotType;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
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.CraftingInventoryTranslator;
|
||||||
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
|
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
|
||||||
import org.geysermc.geyser.util.InventoryUtils;
|
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 org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -52,7 +59,8 @@ public final class ClickPlan {
|
|||||||
*/
|
*/
|
||||||
private Int2ObjectMap<ItemStack> changedItems;
|
private Int2ObjectMap<ItemStack> changedItems;
|
||||||
private GeyserItemStack simulatedCursor;
|
private GeyserItemStack simulatedCursor;
|
||||||
private boolean finished;
|
private int desiredBundleSlot;
|
||||||
|
private boolean executionBegan;
|
||||||
|
|
||||||
private final GeyserSession session;
|
private final GeyserSession session;
|
||||||
private final InventoryTranslator translator;
|
private final InventoryTranslator translator;
|
||||||
@ -67,7 +75,7 @@ public final class ClickPlan {
|
|||||||
this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize());
|
this.simulatedItems = new Int2ObjectOpenHashMap<>(inventory.getSize());
|
||||||
this.changedItems = null;
|
this.changedItems = null;
|
||||||
this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
|
this.simulatedCursor = session.getPlayerInventory().getCursor().copy();
|
||||||
this.finished = false;
|
this.executionBegan = false;
|
||||||
|
|
||||||
gridSize = translator.getGridSize();
|
gridSize = translator.getGridSize();
|
||||||
}
|
}
|
||||||
@ -82,7 +90,7 @@ public final class ClickPlan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void add(Click click, int slot, boolean force) {
|
public void add(Click click, int slot, boolean force) {
|
||||||
if (finished)
|
if (executionBegan)
|
||||||
throw new UnsupportedOperationException("ClickPlan already executed");
|
throw new UnsupportedOperationException("ClickPlan already executed");
|
||||||
|
|
||||||
if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) {
|
if (click == Click.LEFT_OUTSIDE || click == Click.RIGHT_OUTSIDE) {
|
||||||
@ -97,6 +105,7 @@ public final class ClickPlan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void execute(boolean refresh) {
|
public void execute(boolean refresh) {
|
||||||
|
executionBegan = true;
|
||||||
//update geyser inventory after simulation to avoid net id desync
|
//update geyser inventory after simulation to avoid net id desync
|
||||||
resetSimulation();
|
resetSimulation();
|
||||||
ListIterator<ClickAction> planIter = plan.listIterator();
|
ListIterator<ClickAction> planIter = plan.listIterator();
|
||||||
@ -159,7 +168,27 @@ public final class ClickPlan {
|
|||||||
for (Int2ObjectMap.Entry<GeyserItemStack> simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
|
for (Int2ObjectMap.Entry<GeyserItemStack> simulatedSlot : simulatedItems.int2ObjectEntrySet()) {
|
||||||
inventory.setItem(simulatedSlot.getIntKey(), simulatedSlot.getValue(), session);
|
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() {
|
public Inventory getInventory() {
|
||||||
@ -187,6 +216,10 @@ public final class ClickPlan {
|
|||||||
return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
|
return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDesiredBundleSlot(int desiredBundleSlot) {
|
||||||
|
this.desiredBundleSlot = desiredBundleSlot;
|
||||||
|
}
|
||||||
|
|
||||||
public GeyserItemStack getCursor() {
|
public GeyserItemStack getCursor() {
|
||||||
return simulatedCursor;
|
return simulatedCursor;
|
||||||
}
|
}
|
||||||
@ -275,8 +308,60 @@ public final class ClickPlan {
|
|||||||
} else if (InventoryUtils.canStack(cursor, clicked)) {
|
} else if (InventoryUtils.canStack(cursor, clicked)) {
|
||||||
cursor.sub(1);
|
cursor.sub(1);
|
||||||
add(action.slot, clicked, 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;
|
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:
|
case SWAP_TO_HOTBAR_1:
|
||||||
swap(action.slot, inventory.getOffsetForHotbar(0), clicked);
|
swap(action.slot, inventory.getOffsetForHotbar(0), clicked);
|
||||||
break;
|
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}
|
* 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.auth.BedrockClientData;
|
||||||
import org.geysermc.geyser.session.cache.AdvancementsCache;
|
import org.geysermc.geyser.session.cache.AdvancementsCache;
|
||||||
import org.geysermc.geyser.session.cache.BookEditCache;
|
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.ChunkCache;
|
||||||
import org.geysermc.geyser.session.cache.EntityCache;
|
import org.geysermc.geyser.session.cache.EntityCache;
|
||||||
import org.geysermc.geyser.session.cache.EntityEffectCache;
|
import org.geysermc.geyser.session.cache.EntityEffectCache;
|
||||||
@ -275,6 +276,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
|
|
||||||
private final AdvancementsCache advancementsCache;
|
private final AdvancementsCache advancementsCache;
|
||||||
private final BookEditCache bookEditCache;
|
private final BookEditCache bookEditCache;
|
||||||
|
private final BundleCache bundleCache;
|
||||||
private final ChunkCache chunkCache;
|
private final ChunkCache chunkCache;
|
||||||
private final EntityCache entityCache;
|
private final EntityCache entityCache;
|
||||||
private final EntityEffectCache effectCache;
|
private final EntityEffectCache effectCache;
|
||||||
@ -677,6 +679,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
|
|
||||||
this.advancementsCache = new AdvancementsCache(this);
|
this.advancementsCache = new AdvancementsCache(this);
|
||||||
this.bookEditCache = new BookEditCache(this);
|
this.bookEditCache = new BookEditCache(this);
|
||||||
|
this.bundleCache = new BundleCache(this);
|
||||||
this.chunkCache = new ChunkCache(this);
|
this.chunkCache = new ChunkCache(this);
|
||||||
this.entityCache = new EntityCache(this);
|
this.entityCache = new EntityCache(this);
|
||||||
this.effectCache = new EntityEffectCache();
|
this.effectCache = new EntityEffectCache();
|
||||||
@ -1352,6 +1355,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.bundleCache.tick();
|
||||||
|
|
||||||
if (spawned) {
|
if (spawned) {
|
||||||
// Could move this to the PlayerAuthInput translator, in the event the player lags
|
// 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
|
// 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()));
|
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
|
* 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.
|
* 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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.geysermc.geyser.translator.inventory.BundleInventoryTranslator.isBundle;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public abstract class InventoryTranslator {
|
public abstract class InventoryTranslator {
|
||||||
|
|
||||||
@ -241,6 +243,13 @@ public abstract class InventoryTranslator {
|
|||||||
return rejectRequest(request);
|
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 sourceSlot = bedrockSlotToJava(transferAction.getSource());
|
||||||
int destSlot = bedrockSlotToJava(transferAction.getDestination());
|
int destSlot = bedrockSlotToJava(transferAction.getDestination());
|
||||||
boolean isSourceCursor = isCursor(transferAction.getSource());
|
boolean isSourceCursor = isCursor(transferAction.getSource());
|
||||||
@ -393,6 +402,7 @@ public abstract class InventoryTranslator {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SWAP: {
|
case SWAP: {
|
||||||
|
// TODO breaks with bundles
|
||||||
SwapAction swapAction = (SwapAction) action;
|
SwapAction swapAction = (SwapAction) action;
|
||||||
ItemStackRequestSlotData source = swapAction.getSource();
|
ItemStackRequestSlotData source = swapAction.getSource();
|
||||||
ItemStackRequestSlotData destination = swapAction.getDestination();
|
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) { //???
|
if (isSourceCursor && isDestCursor) { //???
|
||||||
return rejectRequest(request);
|
return rejectRequest(request);
|
||||||
} else if (isSourceCursor) { //swap cursor
|
} else if (isSourceCursor) { //swap cursor
|
||||||
if (InventoryUtils.canStack(cursor, plan.getItem(destSlot))) { //TODO: cannot simply swap if cursor stacks with slot (temp slot)
|
if (InventoryUtils.canStack(cursor, plan.getItem(destSlot))) { //TODO: cannot simply swap if cursor stacks with slot (temp slot)
|
||||||
return rejectRequest(request);
|
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
|
} else if (isDestCursor) { //swap cursor
|
||||||
if (InventoryUtils.canStack(cursor, plan.getItem(sourceSlot))) { //TODO
|
if (InventoryUtils.canStack(cursor, plan.getItem(sourceSlot))) { //TODO
|
||||||
return rejectRequest(request);
|
return rejectRequest(request);
|
||||||
}
|
}
|
||||||
plan.add(Click.LEFT, sourceSlot);
|
plan.add(isBundle(plan, sourceSlot) || isBundle(cursor) ? Click.RIGHT : Click.LEFT, sourceSlot);
|
||||||
} else {
|
} else {
|
||||||
if (!cursor.isEmpty()) { //TODO: (temp slot)
|
if (!cursor.isEmpty()) { //TODO: (temp slot)
|
||||||
return rejectRequest(request);
|
return rejectRequest(request);
|
||||||
@ -449,7 +465,7 @@ public abstract class InventoryTranslator {
|
|||||||
return rejectRequest(request);
|
return rejectRequest(request);
|
||||||
}
|
}
|
||||||
plan.add(Click.LEFT, sourceSlot); //pickup source into cursor
|
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
|
plan.add(Click.LEFT, sourceSlot); //release cursor onto source
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -915,6 +931,11 @@ public abstract class InventoryTranslator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkNetId(GeyserSession session, Inventory inventory, ItemStackRequestSlotData slotInfoData) {
|
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();
|
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.
|
// "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"
|
// 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);
|
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();
|
int transferAmount = transferAction.getCount();
|
||||||
if (isCursor(transferAction.getDestination())) {
|
if (isCursor(transferAction.getDestination())) {
|
||||||
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
|
int sourceSlot = bedrockSlotToJava(transferAction.getSource());
|
||||||
@ -415,6 +424,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
|||||||
@Override
|
@Override
|
||||||
protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
|
protected ItemStackResponse translateCreativeRequest(GeyserSession session, Inventory inventory, ItemStackRequest request) {
|
||||||
ItemStack javaCreativeItem = null;
|
ItemStack javaCreativeItem = null;
|
||||||
|
boolean bundle = false;
|
||||||
IntSet affectedSlots = new IntOpenHashSet();
|
IntSet affectedSlots = new IntOpenHashSet();
|
||||||
CraftState craftState = CraftState.START;
|
CraftState craftState = CraftState.START;
|
||||||
for (ItemStackRequestAction action : request.getActions()) {
|
for (ItemStackRequestAction action : request.getActions()) {
|
||||||
@ -469,8 +479,10 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
|||||||
if (isCursor(transferAction.getDestination())) {
|
if (isCursor(transferAction.getDestination())) {
|
||||||
if (session.getPlayerInventory().getCursor().isEmpty()) {
|
if (session.getPlayerInventory().getCursor().isEmpty()) {
|
||||||
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
|
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
|
||||||
|
session.getBundleCache().initialize(newItemStack);
|
||||||
newItemStack.setAmount(transferAction.getCount());
|
newItemStack.setAmount(transferAction.getCount());
|
||||||
session.getPlayerInventory().setCursor(newItemStack, session);
|
session.getPlayerInventory().setCursor(newItemStack, session);
|
||||||
|
bundle = newItemStack.getBundleData() != null;
|
||||||
} else {
|
} else {
|
||||||
session.getPlayerInventory().getCursor().add(transferAction.getCount());
|
session.getPlayerInventory().getCursor().add(transferAction.getCount());
|
||||||
}
|
}
|
||||||
@ -479,8 +491,10 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
|||||||
int destSlot = bedrockSlotToJava(transferAction.getDestination());
|
int destSlot = bedrockSlotToJava(transferAction.getDestination());
|
||||||
if (inventory.getItem(destSlot).isEmpty()) {
|
if (inventory.getItem(destSlot).isEmpty()) {
|
||||||
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
|
GeyserItemStack newItemStack = GeyserItemStack.from(javaCreativeItem);
|
||||||
|
session.getBundleCache().initialize(newItemStack);
|
||||||
newItemStack.setAmount(transferAction.getCount());
|
newItemStack.setAmount(transferAction.getCount());
|
||||||
inventory.setItem(destSlot, newItemStack, session);
|
inventory.setItem(destSlot, newItemStack, session);
|
||||||
|
bundle = newItemStack.getBundleData() != null;
|
||||||
} else {
|
} else {
|
||||||
inventory.getItem(destSlot).add(transferAction.getCount());
|
inventory.getItem(destSlot).add(transferAction.getCount());
|
||||||
}
|
}
|
||||||
@ -520,7 +534,11 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
|||||||
int slot = it.nextInt();
|
int slot = it.nextInt();
|
||||||
sendCreativeAction(session, inventory, slot);
|
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) {
|
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.useItem(Hand.MAIN_HAND);
|
||||||
|
|
||||||
|
session.getBundleCache().awaitRelease();
|
||||||
|
|
||||||
List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
|
List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
|
||||||
if (packet.getActions().size() == 1 && !legacySlots.isEmpty()) {
|
if (packet.getActions().size() == 1 && !legacySlots.isEmpty()) {
|
||||||
InventoryActionData actionData = packet.getActions().get(0);
|
InventoryActionData actionData = packet.getActions().get(0);
|
||||||
@ -439,10 +441,8 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||||||
break;
|
break;
|
||||||
case ITEM_RELEASE:
|
case ITEM_RELEASE:
|
||||||
if (packet.getActionType() == 0) {
|
if (packet.getActionType() == 0) {
|
||||||
// Followed to the Minecraft Protocol specification outlined at wiki.vg
|
session.releaseItem();
|
||||||
ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO,
|
session.getBundleCache().markRelease();
|
||||||
Direction.DOWN, 0);
|
|
||||||
session.sendDownstreamGamePacket(releaseItemPacket);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ITEM_USE_ON_ENTITY:
|
case ITEM_USE_ON_ENTITY:
|
||||||
|
@ -64,6 +64,7 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
|
|||||||
}
|
}
|
||||||
|
|
||||||
GeyserItemStack newItem = GeyserItemStack.from(packet.getItems()[i]);
|
GeyserItemStack newItem = GeyserItemStack.from(packet.getItems()[i]);
|
||||||
|
session.getBundleCache().initialize(newItem);
|
||||||
inventory.setItem(i, newItem, session);
|
inventory.setItem(i, newItem, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +74,9 @@ public class JavaContainerSetContentTranslator extends PacketTranslator<Clientbo
|
|||||||
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
|
session.setEmulatePost1_16Logic(stateId > 0 || stateId != inventory.getStateId());
|
||||||
inventory.setStateId(stateId);
|
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);
|
InventoryUtils.updateCursor(session);
|
||||||
|
|
||||||
if (session.getInventoryTranslator() instanceof SmithingInventoryTranslator) {
|
if (session.getInventoryTranslator() instanceof SmithingInventoryTranslator) {
|
||||||
|
@ -93,6 +93,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
|
|||||||
}
|
}
|
||||||
|
|
||||||
GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
|
GeyserItemStack newItem = GeyserItemStack.from(packet.getItem());
|
||||||
|
session.getBundleCache().initialize(newItem);
|
||||||
if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
|
if (packet.getContainerId() == 0 && !(translator instanceof PlayerInventoryTranslator)) {
|
||||||
// In rare cases, the window ID can still be 0 but Java treats it as valid
|
// 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
|
// 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
|
@Override
|
||||||
public void translate(GeyserSession session, ClientboundSetCursorItemPacket packet) {
|
public void translate(GeyserSession session, ClientboundSetCursorItemPacket packet) {
|
||||||
GeyserItemStack newItem = GeyserItemStack.from(packet.getContents());
|
GeyserItemStack newItem = GeyserItemStack.from(packet.getContents());
|
||||||
|
session.getBundleCache().initialize(newItem);
|
||||||
session.getPlayerInventory().setCursor(newItem, session);
|
session.getPlayerInventory().setCursor(newItem, session);
|
||||||
InventoryUtils.updateCursor(session);
|
InventoryUtils.updateCursor(session);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,9 @@ public class JavaSetPlayerInventoryTranslator extends PacketTranslator<Clientbou
|
|||||||
return;
|
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);
|
InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR.updateSlot(session, session.getPlayerInventory(), slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,6 +140,7 @@ public class InventoryUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR);
|
session.setInventoryTranslator(InventoryTranslator.PLAYER_INVENTORY_TRANSLATOR);
|
||||||
|
session.getBundleCache().onInventoryClose(inventory);
|
||||||
session.setOpenInventory(null);
|
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