mirror of https://github.com/Minestom/Minestom.git
Follow client predictions whenever possible
This commit is contained in:
parent
67b22fc06b
commit
5c236944e2
|
@ -11,6 +11,7 @@ import net.minestom.server.inventory.click.ClickProcessors;
|
|||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.network.packet.server.play.OpenWindowPacket;
|
||||
import net.minestom.server.network.packet.server.play.WindowPropertyPacket;
|
||||
import net.minestom.server.utils.inventory.ClickUtils;
|
||||
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
@ -35,42 +36,27 @@ public non-sealed class ContainerInventory extends InventoryImpl {
|
|||
* @return the click result, or null if the click did not occur
|
||||
*/
|
||||
public static @Nullable List<Click.Change> handleClick(@NotNull Inventory inventory, @NotNull Player player, @NotNull Click.Info info,
|
||||
@NotNull ClickProcessors.InventoryProcessor processor) {
|
||||
@NotNull ClickProcessors.InventoryProcessor processor) {
|
||||
PlayerInventory playerInventory = player.getInventory();
|
||||
|
||||
InventoryPreClickEvent preClickEvent = new InventoryPreClickEvent(playerInventory, inventory, player, info);
|
||||
EventDispatcher.call(preClickEvent);
|
||||
if (!preClickEvent.isCancelled()) {
|
||||
final Click.Info newInfo = preClickEvent.getClickInfo();
|
||||
Click.Getter getter = new Click.Getter(inventory::getItemStack, playerInventory::getItemStack, playerInventory.getCursorItem(), inventory.getSize());
|
||||
final List<Click.Change> changes = processor.apply(newInfo, getter);
|
||||
if (preClickEvent.isCancelled()) return null;
|
||||
|
||||
InventoryClickEvent clickEvent = new InventoryClickEvent(playerInventory, inventory, player, newInfo, changes);
|
||||
EventDispatcher.call(clickEvent);
|
||||
final Click.Info newInfo = preClickEvent.getClickInfo();
|
||||
final List<Click.Change> changes = processor.apply(newInfo, ClickUtils.makeGetter(inventory, playerInventory));
|
||||
|
||||
if (!clickEvent.isCancelled()) {
|
||||
final List<Click.Change> newChanges = clickEvent.getChanges();
|
||||
InventoryClickEvent clickEvent = new InventoryClickEvent(playerInventory, inventory, player, newInfo, changes);
|
||||
EventDispatcher.call(clickEvent);
|
||||
if (clickEvent.isCancelled()) return null;
|
||||
|
||||
apply(newChanges, player, inventory);
|
||||
final List<Click.Change> newChanges = clickEvent.getChanges();
|
||||
|
||||
EventDispatcher.call(new InventoryPostClickEvent(player, inventory, newInfo, newChanges));
|
||||
apply(newChanges, player, inventory);
|
||||
|
||||
if (!info.equals(newInfo) || !changes.equals(newChanges)) {
|
||||
inventory.update(player);
|
||||
if (inventory != playerInventory) {
|
||||
playerInventory.update(player);
|
||||
}
|
||||
}
|
||||
EventDispatcher.call(new InventoryPostClickEvent(player, inventory, newInfo, newChanges));
|
||||
|
||||
return newChanges;
|
||||
}
|
||||
}
|
||||
|
||||
inventory.update(player);
|
||||
if (inventory != playerInventory) {
|
||||
playerInventory.update(player);
|
||||
}
|
||||
return null;
|
||||
return newChanges;
|
||||
}
|
||||
|
||||
public static void apply(@NotNull List<Click.Change> changes, @NotNull Player player, @NotNull Inventory inventory) {
|
||||
|
@ -146,9 +132,18 @@ public non-sealed class ContainerInventory extends InventoryImpl {
|
|||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<Click.Change> handleClick(@NotNull Player player, Click.@NotNull Info info) {
|
||||
return ContainerInventory.handleClick(this, player, info,
|
||||
ClickProcessors.PROCESSORS_MAP.getOrDefault(inventoryType, ClickProcessors.GENERIC_PROCESSOR));
|
||||
public @Nullable List<Click.Change> handleClick(@NotNull Player player, Click.@NotNull Info info, @Nullable List<Click.Change> clientPrediction) {
|
||||
// We can use the client prediction if it's conservative (i.e. doesn't create or delete items) or the client is in creative.
|
||||
// Otherwise, we make our own.
|
||||
if (clientPrediction != null && (ClickUtils.conservative(clientPrediction, this, player.getInventory()) || player.isCreative())) {
|
||||
return ContainerInventory.handleClick(this, player, info, (i, g) -> clientPrediction);
|
||||
} else {
|
||||
var results = ContainerInventory.handleClick(this, player, info,
|
||||
ClickProcessors.PROCESSORS_MAP.getOrDefault(inventoryType, ClickProcessors.GENERIC_PROCESSOR));
|
||||
update(player);
|
||||
player.getInventory().update(player);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -51,9 +51,10 @@ public sealed interface Inventory extends Taggable, Viewable permits InventoryIm
|
|||
*
|
||||
* @param player the player that clicked
|
||||
* @param info the information about the player's click
|
||||
* @param clientPrediction the client prediction (null if none)
|
||||
* @return the results of the click, or null if the click was cancelled or otherwise was not handled
|
||||
*/
|
||||
@Nullable List<Click.Change> handleClick(@NotNull Player player, @NotNull Click.Info info);
|
||||
@Nullable List<Click.Change> handleClick(@NotNull Player player, @NotNull Click.Info info, @Nullable List<Click.Change> clientPrediction);
|
||||
|
||||
/**
|
||||
* Gets all the {@link ItemStack} in the inventory.
|
||||
|
|
|
@ -9,6 +9,7 @@ import net.minestom.server.inventory.click.ClickProcessors;
|
|||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.network.packet.server.play.SetSlotPacket;
|
||||
import net.minestom.server.network.packet.server.play.WindowItemsPacket;
|
||||
import net.minestom.server.utils.inventory.ClickUtils;
|
||||
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
@ -158,8 +159,16 @@ public non-sealed class PlayerInventory extends InventoryImpl {
|
|||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<Click.Change> handleClick(@NotNull Player player, @NotNull Click.Info info) {
|
||||
return ContainerInventory.handleClick(this, player, info, ClickProcessors.PLAYER_PROCESSOR);
|
||||
public @Nullable List<Click.Change> handleClick(@NotNull Player player, Click.@NotNull Info info, @Nullable List<Click.Change> clientPrediction) {
|
||||
// We can use the client prediction if it's conservative (i.e. doesn't create or delete items) or the client is in creative.
|
||||
// Otherwise, we make our own.
|
||||
if (clientPrediction != null && (ClickUtils.conservative(clientPrediction, this, this) || player.isCreative())) {
|
||||
return ContainerInventory.handleClick(this, player, info, (i, g) -> clientPrediction);
|
||||
} else {
|
||||
var results = ContainerInventory.handleClick(this, player, info, ClickProcessors.PLAYER_PROCESSOR);
|
||||
update(player);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
public @NotNull ItemStack getEquipment(@NotNull EquipmentSlot slot, int heldSlot) {
|
||||
|
|
|
@ -6,6 +6,8 @@ import net.minestom.server.item.ItemStack;
|
|||
import net.minestom.server.network.packet.client.play.ClientCreativeInventoryActionPacket;
|
||||
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class CreativeInventoryActionListener {
|
||||
public static void listener(ClientCreativeInventoryActionPacket packet, Player player) {
|
||||
if (!player.isCreative()) return;
|
||||
|
@ -13,12 +15,12 @@ public final class CreativeInventoryActionListener {
|
|||
ItemStack item = packet.item();
|
||||
|
||||
if (packet.slot() == -1) { // -1 here indicates a drop
|
||||
player.getInventory().handleClick(player, new Click.Info.CreativeDropItem(item));
|
||||
player.getInventory().handleClick(player, new Click.Info.CreativeDropItem(item), List.of(new Click.Change.DropFromPlayer(item)));
|
||||
}
|
||||
|
||||
int slot = PlayerInventoryUtils.protocolToMinestom(packet.slot());
|
||||
if (slot == -1) return; // -1 after conversion indicates an invalid slot
|
||||
|
||||
player.getInventory().handleClick(player, new Click.Info.CreativeSetItem(slot, item));
|
||||
player.getInventory().handleClick(player, new Click.Info.CreativeSetItem(slot, item), List.of(new Click.Change.Container(slot, item)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,12 @@ import net.minestom.server.network.packet.client.play.ClientPlayerDiggingPacket;
|
|||
import net.minestom.server.network.packet.server.play.AcknowledgeBlockChangePacket;
|
||||
import net.minestom.server.network.packet.server.play.BlockEntityDataPacket;
|
||||
import net.minestom.server.utils.block.BlockUtils;
|
||||
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class PlayerDiggingListener {
|
||||
|
||||
public static void playerDiggingListener(ClientPlayerDiggingPacket packet, Player player) {
|
||||
|
@ -41,13 +44,19 @@ public final class PlayerDiggingListener {
|
|||
if (!instance.isChunkLoaded(blockPosition)) return;
|
||||
diggingResult = finishDigging(player, instance, blockPosition, packet.blockFace());
|
||||
} else if (status == ClientPlayerDiggingPacket.Status.DROP_ITEM_STACK) {
|
||||
player.getInventory().handleClick(player, new Click.Info.DropSlot(player.getHeldSlot(), true));
|
||||
player.getInventory().handleClick(player, new Click.Info.DropSlot(player.getHeldSlot(), true),
|
||||
List.of(new Click.Change.DropFromPlayer(player.getItemInMainHand())));
|
||||
} else if (status == ClientPlayerDiggingPacket.Status.DROP_ITEM) {
|
||||
player.getInventory().handleClick(player, new Click.Info.DropSlot(player.getHeldSlot(), false));
|
||||
player.getInventory().handleClick(player, new Click.Info.DropSlot(player.getHeldSlot(), false),
|
||||
List.of(new Click.Change.DropFromPlayer(player.getItemInMainHand().withAmount(1))));
|
||||
} else if (status == ClientPlayerDiggingPacket.Status.UPDATE_ITEM_STATE) {
|
||||
updateItemState(player);
|
||||
} else if (status == ClientPlayerDiggingPacket.Status.SWAP_ITEM_HAND) {
|
||||
player.getInventory().handleClick(player, new Click.Info.OffhandSwap(player.getHeldSlot()));
|
||||
player.getInventory().handleClick(player, new Click.Info.OffhandSwap(player.getHeldSlot()),
|
||||
List.of(
|
||||
new Click.Change.Player(PlayerInventoryUtils.OFF_HAND_SLOT, player.getItemInMainHand()),
|
||||
new Click.Change.Player(player.getHeldSlot(), player.getItemInOffHand())
|
||||
));
|
||||
}
|
||||
// Acknowledge start/cancel/finish digging status
|
||||
if (diggingResult != null) {
|
||||
|
|
|
@ -11,6 +11,9 @@ import net.minestom.server.network.packet.client.play.ClientClickWindowButtonPac
|
|||
import net.minestom.server.network.packet.client.play.ClientClickWindowPacket;
|
||||
import net.minestom.server.network.packet.client.play.ClientCloseWindowPacket;
|
||||
import net.minestom.server.network.packet.server.common.PingPacket;
|
||||
import net.minestom.server.utils.inventory.ClickUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WindowListener {
|
||||
|
||||
|
@ -24,7 +27,12 @@ public class WindowListener {
|
|||
|
||||
Click.Preprocessor preprocessor = player.clickPreprocessor();
|
||||
final Click.Info info = preprocessor.processClick(packet, player.isCreative(), playerInventory ? null : inventory.getSize());
|
||||
if (info != null) inventory.handleClick(player, info);
|
||||
if (info != null) {
|
||||
Click.Getter getter = ClickUtils.makeGetter(inventory, player.getInventory());
|
||||
List<Click.Change> clientPrediction = ClickUtils.packetToChanges(packet, info, getter, playerInventory);
|
||||
|
||||
inventory.handleClick(player, info, clientPrediction);
|
||||
}
|
||||
|
||||
// (Why is the ping packet necessary?)
|
||||
player.sendPacket(new PingPacket((1 << 30) | (windowId << 16)));
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package net.minestom.server.utils.inventory;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import net.minestom.server.inventory.Inventory;
|
||||
import net.minestom.server.inventory.PlayerInventory;
|
||||
import net.minestom.server.inventory.click.Click;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.network.packet.client.play.ClientClickWindowPacket;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public class ClickUtils {
|
||||
|
||||
public static @NotNull Click.Getter makeGetter(@NotNull Inventory inventory, @NotNull PlayerInventory playerInventory) {
|
||||
return new Click.Getter(inventory::getItemStack, playerInventory::getItemStack, playerInventory.getCursorItem(), inventory.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not the given changes are conservative (i.e., whether or not they create or delete items).
|
||||
*/
|
||||
public static boolean conservative(@NotNull List<Click.Change> clientDefault, @NotNull Inventory inventory, @NotNull PlayerInventory playerInventory) {
|
||||
Click.Getter getter = makeGetter(inventory, playerInventory);
|
||||
return consolidate(clientDefault, getter.mainSize()).conservative(getter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a click window packet and a click info into a list of changes.
|
||||
*/
|
||||
public static @NotNull List<Click.Change> packetToChanges(@NotNull ClientClickWindowPacket packet, @NotNull Click.Info info, @NotNull Click.Getter getter, boolean playerInventory) {
|
||||
List<Click.Change> changes = new ArrayList<>();
|
||||
|
||||
for (var change : packet.changedSlots()) {
|
||||
int slot = change.slot();
|
||||
if (playerInventory) slot = PlayerInventoryUtils.protocolToMinestom(slot);
|
||||
changes.add(new Click.Change.Container(slot, change.item()));
|
||||
}
|
||||
|
||||
changes.add(new Click.Change.Cursor(packet.clickedItem()));
|
||||
|
||||
if (info instanceof Click.Info.OffhandSwap swap && !playerInventory) {
|
||||
changes.add(new Click.Change.Player(PlayerInventoryUtils.OFF_HAND_SLOT, getter.get(swap.slot())));
|
||||
}
|
||||
|
||||
switch (info) {
|
||||
case Click.Info.LeftDropCursor() -> changes.add(new Click.Change.DropFromPlayer(getter.cursor()));
|
||||
case Click.Info.RightDropCursor() -> changes.add(new Click.Change.DropFromPlayer(getter.cursor().withAmount(1)));
|
||||
case Click.Info.DropSlot(int slot, boolean all) -> changes.add(new Click.Change.DropFromPlayer(all ? getter.get(slot) : getter.get(slot).withAmount(1)));
|
||||
case Click.Info.CreativeDropItem(ItemStack item) -> changes.add(new Click.Change.DropFromPlayer(item));
|
||||
default -> {}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidates a list of changes into a single object.
|
||||
*/
|
||||
public static @NotNull FlatChanges consolidate(@NotNull List<Click.Change> changes, int size) {
|
||||
Map<Integer, ItemStack> container = new HashMap<>();
|
||||
Map<Integer, ItemStack> player = new HashMap<>();
|
||||
@Nullable ItemStack cursor = null;
|
||||
List<ItemStack> dropped = new ArrayList<>();
|
||||
|
||||
for (var change : changes) {
|
||||
switch (change) {
|
||||
case Click.Change.Container(int slot, ItemStack item) -> {
|
||||
if (slot < size) {
|
||||
container.put(slot, item);
|
||||
} else {
|
||||
player.put(PlayerInventoryUtils.protocolToMinestom(slot, size), item);
|
||||
}
|
||||
}
|
||||
case Click.Change.Player(int slot, ItemStack item) -> player.put(slot, item);
|
||||
case Click.Change.Cursor(ItemStack item) -> cursor = item;
|
||||
case Click.Change.DropFromPlayer(ItemStack item) -> dropped.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return new FlatChanges(container, player, cursor, dropped);
|
||||
}
|
||||
|
||||
public record FlatChanges(@NotNull Map<Integer, ItemStack> container, @NotNull Map<Integer, ItemStack> player,
|
||||
@Nullable ItemStack cursor, @NotNull List<ItemStack> dropped) {
|
||||
|
||||
/**
|
||||
* Determines whether or not these changes are conservative. If they create or destroy any items, the changes
|
||||
* are not conservative.
|
||||
*/
|
||||
public boolean conservative(@NotNull Click.Getter getter) {
|
||||
// A count of each item; this will ideally end up with everything at zero.
|
||||
Object2IntMap<ItemStack> items = new Object2IntOpenHashMap<>();
|
||||
items.defaultReturnValue(0);
|
||||
|
||||
BiConsumer<ItemStack, Integer> updater = (item, count) -> {
|
||||
if (item.isAir()) return;
|
||||
|
||||
ItemStack one = item.withAmount(1);
|
||||
items.put(one, items.getInt(one) + count);
|
||||
};
|
||||
|
||||
container.values().forEach(item -> updater.accept(item, item.amount()));
|
||||
player.values().forEach(item -> updater.accept(item, item.amount()));
|
||||
dropped.forEach(item -> updater.accept(item, item.amount()));
|
||||
|
||||
container.keySet().stream().map(getter.main()::apply).forEach(item -> updater.accept(item, -item.amount()));
|
||||
player.keySet().stream().map(getter.player()::apply).forEach(item -> updater.accept(item, -item.amount()));
|
||||
|
||||
if (cursor != null) {
|
||||
updater.accept(cursor, cursor.amount());
|
||||
updater.accept(getter.cursor(), -getter.cursor().amount());
|
||||
}
|
||||
|
||||
return items.values().intStream().allMatch(i -> i == 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -9,13 +9,14 @@ import net.minestom.server.item.Material;
|
|||
import net.minestom.server.network.packet.client.play.ClientClickWindowPacket;
|
||||
import net.minestom.server.network.packet.server.SendablePacket;
|
||||
import net.minestom.server.network.player.PlayerConnection;
|
||||
import net.minestom.server.utils.inventory.PlayerInventoryUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.*;
|
||||
|
||||
import static net.minestom.server.utils.inventory.ClickUtils.consolidate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public final class ClickUtils {
|
||||
|
@ -61,7 +62,7 @@ public final class ClickUtils {
|
|||
var inventory = createInventory();
|
||||
|
||||
ContainerInventory.apply(initial, player, inventory);
|
||||
var actual = inventory.handleClick(player, info);
|
||||
var actual = inventory.handleClick(player, info, null);
|
||||
|
||||
assertChanges(expected, actual, inventory.getSize());
|
||||
}
|
||||
|
@ -71,40 +72,13 @@ public final class ClickUtils {
|
|||
var inventory = player.getInventory();
|
||||
|
||||
ContainerInventory.apply(initial, player, inventory);
|
||||
var actual = inventory.handleClick(player, info);
|
||||
var actual = inventory.handleClick(player, info, null);
|
||||
|
||||
assertChanges(expected, actual, inventory.getSize());
|
||||
}
|
||||
|
||||
public static void assertChanges(List<Click.Change> expected, List<Click.Change> actual, int size) {
|
||||
assertEquals(ChangeResult.make(expected, size), ChangeResult.make(actual, size));
|
||||
}
|
||||
|
||||
private record ChangeResult(Map<Integer, ItemStack> main, Map<Integer, ItemStack> player,
|
||||
@Nullable ItemStack cursor, List<ItemStack> drops) {
|
||||
private static ChangeResult make(@NotNull List<Click.Change> changes, int size) {
|
||||
Map<Integer, ItemStack> main = new HashMap<>();
|
||||
Map<Integer, ItemStack> player = new HashMap<>();
|
||||
@Nullable ItemStack cursor = null;
|
||||
List<ItemStack> drops = new ArrayList<>();
|
||||
|
||||
for (var change : changes) {
|
||||
switch (change) {
|
||||
case Click.Change.Container(int slot, ItemStack item) -> {
|
||||
if (slot < size) {
|
||||
main.put(slot, item);
|
||||
} else {
|
||||
player.put(PlayerInventoryUtils.protocolToMinestom(slot, size), item);
|
||||
}
|
||||
}
|
||||
case Click.Change.Player(int slot, ItemStack item) -> player.put(slot, item);
|
||||
case Click.Change.Cursor(ItemStack item) -> cursor = item;
|
||||
case Click.Change.DropFromPlayer(ItemStack item) -> drops.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return new ChangeResult(main, player, cursor, drops);
|
||||
}
|
||||
assertEquals(consolidate(expected, size), consolidate(actual, size));
|
||||
}
|
||||
|
||||
public static void assertProcessed(@NotNull Click.Preprocessor preprocessor, @NotNull Player player, @Nullable Click.Info info, @NotNull ClientClickWindowPacket packet) {
|
||||
|
|
Loading…
Reference in New Issue