Skip to content

Commit

Permalink
initial inventory lag patch concept
Browse files Browse the repository at this point in the history
  • Loading branch information
xGinko committed Aug 3, 2024
1 parent 322bb21 commit 0aca7d0
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package me.xginko.aef.modules.packets;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.item.ItemStack;
import com.github.retrooper.packetevents.protocol.nbt.NBTCompound;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetSlot;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryOpenEvent;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;

public class InventoryLag extends PacketModule implements Listener {

private final Cache<UUID, PlayerData> playerDataCache;
private final long maxByteSize, inventoryOpenCooldownMillis;
private final boolean closeInventory;

public InventoryLag() {
super("patches.inventory-lag", PacketListenerPriority.HIGHEST);
config.addComment(configPath + ".enable",
"Checks if a player is requesting unusual amounts of traffic\n" +
"from the server using big ItemStacks.\n" +
"If a player exceeds the limit, they will be put on a cooldown,\n" +
"during which they will be very limited in terms of ItemStack or\n" +
"Inventory interactions.");
this.playerDataCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofMillis(Math.max(1L,
config.getLong(configPath + ".timeframe-millis", 20000,
"The time in millis in which to check if the player exceeded the limit.\n" +
"Needs to be at least as long as your cooldown millis")))).build();
this.maxByteSize = config.getLong(configPath + ".packet-bytesize-limit", 5120000,
"The upper limit in bytes a player is allowed to request from the server\n" +
"within the configured timeframe before he will be put on cooldown.");
this.inventoryOpenCooldownMillis = config.getLong(configPath + ".cooldown-millis", 15000,
"The time in milliseconds the player will have to wait before\n" +
"being able to open an inventory again after he exceeded the limit.");
this.closeInventory = config.getBoolean(configPath + ".close-open-inventory", true,
"Whether to immediately close any open inventory of the player on limit exceed");
}

@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false);
}

@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
PacketEvents.getAPI().getEventManager().registerListener(asAbstract);
}

@Override
public void disable() {
HandlerList.unregisterAll(this);
PacketEvents.getAPI().getEventManager().unregisterListener(asAbstract);
}

@Override
public void onPacketSend(PacketSendEvent event) {
if (event.isCancelled()) return;

if (event.getPacketType() == PacketType.Play.Server.OPEN_WINDOW) {
if (playerDataCache.get(event.getUser().getUUID(), PlayerData::new)
.nextAllowedInvOpen.get() > System.currentTimeMillis()) {
event.setCancelled(true);
}
return;
}

if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) {
WrapperPlayServerSetSlot packet = new WrapperPlayServerSetSlot(event);

ItemStack itemStack = packet.getItem();
if (itemStack == null || itemStack.isEmpty()) return;
NBTCompound nbt = itemStack.getNBT();
if (nbt == null || nbt.isEmpty()) return;

PlayerData data = playerDataCache.get(event.getUser().getUUID(), PlayerData::new);

if (data.nextAllowedInvOpen.get() > System.currentTimeMillis()) {
event.setCancelled(true);
return;
}

if (data.receivedSetSlotBytes.addAndGet(nbt.toString().getBytes(StandardCharsets.UTF_8).length) <= maxByteSize) {
return;
}

data.nextAllowedInvOpen.set(System.currentTimeMillis() + inventoryOpenCooldownMillis);

if (closeInventory) {
Player bukkitPlayer = (Player) event.getPlayer();
if (bukkitPlayer != null)
bukkitPlayer.getScheduler().execute(plugin, bukkitPlayer::closeInventory, null, 1L);
}
}
}

@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onInventoryOpen(InventoryOpenEvent event) {
if (playerDataCache.get(event.getPlayer().getUniqueId(), PlayerData::new)
.nextAllowedInvOpen.get() > System.currentTimeMillis()) {
event.setCancelled(true);
}
}

private static class PlayerData {

public final UUID playerUniqueId;
public final AtomicLong receivedSetSlotBytes, nextAllowedInvOpen;

public PlayerData(UUID playerUniqueId) {
this.playerUniqueId = playerUniqueId;
this.receivedSetSlotBytes = new AtomicLong(0L);
this.nextAllowedInvOpen = new AtomicLong(0L);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package me.xginko.aef.modules.packets;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.item.ItemStack;
import com.github.retrooper.packetevents.protocol.nbt.NBTCompound;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSetSlot;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryOpenEvent;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;

public class InventoryLag extends PacketModule implements Listener {

private final Cache<UUID, PlayerData> playerDataCache;
private final long maxByteSize, inventoryOpenCooldownMillis;
private final boolean closeInventory;

public InventoryLag() {
super("patches.inventory-lag", PacketListenerPriority.HIGHEST);
config.addComment(configPath + ".enable",
"Checks if a player is requesting unusual amounts of traffic\n" +
"from the server using big ItemStacks.\n" +
"If a player exceeds the limit, they will be put on a cooldown,\n" +
"during which they will be very limited in terms of ItemStack or\n" +
"Inventory interactions.");
this.playerDataCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofMillis(Math.max(1L,
config.getLong(configPath + ".timeframe-millis", 20000,
"The time in millis in which to check if the player exceeded the limit.\n" +
"Needs to be at least as long as your cooldown millis")))).build();
this.maxByteSize = config.getLong(configPath + ".packet-bytesize-limit", 5120000,
"The upper limit in bytes a player is allowed to request from the server\n" +
"within the configured timeframe before he will be put on cooldown.");
this.inventoryOpenCooldownMillis = config.getLong(configPath + ".cooldown-millis", 15000,
"The time in milliseconds the player will have to wait before\n" +
"being able to open an inventory again after he exceeded the limit.");
this.closeInventory = config.getBoolean(configPath + ".close-open-inventory", true,
"Whether to immediately close any open inventory of the player on limit exceed");
}

@Override
public boolean shouldEnable() {
return config.getBoolean(configPath + ".enable", false);
}

@Override
public void enable() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
PacketEvents.getAPI().getEventManager().registerListener(asAbstract);
}

@Override
public void disable() {
HandlerList.unregisterAll(this);
PacketEvents.getAPI().getEventManager().unregisterListener(asAbstract);
}

@Override
@SuppressWarnings("DataFlowIssue")
public void onPacketSend(PacketSendEvent event) {
if (event.isCancelled()) return;

if (event.getPacketType() == PacketType.Play.Server.OPEN_WINDOW) {
if (playerDataCache.get(event.getUser().getUUID(), PlayerData::new)
.nextAllowedInvOpen.get() > System.currentTimeMillis()) {
event.setCancelled(true);
}
return;
}

if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) {
WrapperPlayServerSetSlot packet = new WrapperPlayServerSetSlot(event);

ItemStack itemStack = packet.getItem();
if (itemStack == null || itemStack.isEmpty()) return;
NBTCompound nbt = itemStack.getNBT();
if (nbt == null || nbt.isEmpty()) return;

PlayerData data = playerDataCache.get(event.getUser().getUUID(), PlayerData::new);

if (data.nextAllowedInvOpen.get() > System.currentTimeMillis()) {
event.setCancelled(true);
return;
}

if (data.receivedSetSlotBytes.addAndGet(nbt.toString().getBytes(StandardCharsets.UTF_8).length) <= maxByteSize) {
return;
}

data.nextAllowedInvOpen.set(System.currentTimeMillis() + inventoryOpenCooldownMillis);

if (closeInventory) {
Player bukkitPlayer = (Player) event.getPlayer();
if (bukkitPlayer != null)
plugin.getServer().getScheduler().runTask(plugin, bukkitPlayer::closeInventory);
}
}
}

@SuppressWarnings("DataFlowIssue")
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
private void onInventoryOpen(InventoryOpenEvent event) {
if (playerDataCache.get(event.getPlayer().getUniqueId(), PlayerData::new)
.nextAllowedInvOpen.get() > System.currentTimeMillis()) {
event.setCancelled(true);
}
}

private static class PlayerData {

public final UUID playerUniqueId;
public final AtomicLong receivedSetSlotBytes, nextAllowedInvOpen;

public PlayerData(UUID playerUniqueId) {
this.playerUniqueId = playerUniqueId;
this.receivedSetSlotBytes = new AtomicLong(0L);
this.nextAllowedInvOpen = new AtomicLong(0L);
}
}
}

0 comments on commit 0aca7d0

Please sign in to comment.