Skip to content

Commit

Permalink
add more configuration to inventory-lag
Browse files Browse the repository at this point in the history
  • Loading branch information
xGinko committed Aug 3, 2024
1 parent c05c113 commit eb7efa9
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
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 long rateLimitBytes, lockoutBytes, lockoutMillis;
private final int screenOpenLimit, screenOpenDelay;
private final boolean closeInventory, log;

public InventoryLag() {
Expand All @@ -33,18 +35,31 @@ public InventoryLag() {
"during which they will be very limited in terms of ItemStack or\n" +
"Inventory interactions.");
this.log = config.getBoolean(configPath + ".log", false);
this.closeInventory = config.getBoolean(configPath + ".close-open-inventory", true,
"Whether to immediately close any open inventory of the player on limit exceed\n" +
"Side note: Closing has to be scheduled so it will take a bit if the server is heavily\n" +
"lagging.");
this.playerDataCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofMillis(Math.max(1L,
config.getLong(configPath + ".timeframe-millis", 20000,
config.getLong(configPath + ".data-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,
"Needs to be at least as long as your cooldown millis.")))).build();
this.rateLimitBytes = config.getLong(configPath + "rate-limit.bytesize-limit", 3584000,
"The limit in bytes the server has sent the server in the form of ItemStacks,\n" +
"before the player will be put on a rate-limit.\n" +
"Should always be lower than lockout bytesize limit.");
this.screenOpenDelay = config.getInt(configPath + "rate-limit.timeframe-millis", 1500,
"The time in millis in which a player is allowed to open x amounts of windows\n" +
"but not more.");
this.screenOpenLimit = config.getInt(configPath + "rate-limit.max-window-opens-per-timeframe", 2,
"The amount of windows that can be opened during the timeframe-millis.");
this.lockoutBytes = config.getLong(configPath + ".lockout.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,
"within the configured timeframe before he will be put on cooldown.\n" +
"During the cooldown, he will not be able to open any inventory screens\n" +
"or interact with items.");
this.lockoutMillis = config.getLong(configPath + "lockout.duration-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
Expand All @@ -65,60 +80,87 @@ public void disable() {
}

@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()) {
PlayerData data = playerDataCache.get(event.getUser().getUUID(), PlayerData::new);

if (data.screenNextAllowedOpen.get() > System.currentTimeMillis()) {
event.setCancelled(true);
if (log) info("Player '" + event.getUser().getName() + "' could not open screen because they are on cooldown.");
return;
}

if (
data.servedSetSlotBytes.get() > rateLimitBytes
&& data.servedSetSlotBytes.get() < lockoutBytes
&& data.screenOpenCount.getAndIncrement() > screenOpenLimit
) {
data.screenNextAllowedOpen.set(System.currentTimeMillis() + screenOpenDelay);
if (log) info("Player '" + event.getUser().getName() + "' is now on ratelimit cooldown as they exceeded the set limit.");
return;
}

return;
}

if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) {
PlayerData data = playerDataCache.get(event.getUser().getUUID(), PlayerData::new);

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

if (data.receivedSetSlotBytes.addAndGet(ByteBufHelper.readableBytes(event.getByteBuf())) <= maxByteSize) {
if (data.servedSetSlotBytes.addAndGet(ByteBufHelper.readableBytes(event.getByteBuf())) <= lockoutBytes) {
return;
}

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

if (log) {
info("Player " + event.getUser().getName() + " is now on cooldown for requesting more data " +
"from the server than the set limit.");
}
data.screenNextAllowedOpen.set(System.currentTimeMillis() + lockoutMillis);
if (log) info("Player '" + event.getUser().getName() + "' is now on lockout cooldown as they exceeded the set limit.");

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

if (log) info("Player '" + event.getUser().getName() + "' is now on lockout cooldown for requesting more data " +
"from the server than the set limit.");
}
}

@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()) {
PlayerData data = playerDataCache.get(event.getPlayer().getUniqueId(), PlayerData::new);

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

if (
data.servedSetSlotBytes.get() > rateLimitBytes
&& data.servedSetSlotBytes.get() < lockoutBytes
&& data.screenOpenCount.getAndIncrement() > screenOpenLimit
) {
data.screenNextAllowedOpen.set(System.currentTimeMillis() + screenOpenDelay);
}
}

private static class PlayerData {

public final UUID playerUniqueId;
public final AtomicLong receivedSetSlotBytes, nextAllowedInvOpen;
public final UUID uuid;
public final AtomicLong servedSetSlotBytes, screenNextAllowedOpen;
public final AtomicInteger screenOpenCount;

public PlayerData(UUID playerUniqueId) {
this.playerUniqueId = playerUniqueId;
this.receivedSetSlotBytes = new AtomicLong(0L);
this.nextAllowedInvOpen = new AtomicLong(0L);
public PlayerData(UUID uuid) {
this.uuid = uuid;
this.servedSetSlotBytes = new AtomicLong(0L);
this.screenNextAllowedOpen = new AtomicLong(0L);
this.screenOpenCount = new AtomicInteger(0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
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 long rateLimitBytes, lockoutBytes, lockoutMillis;
private final int screenOpenLimit, screenOpenDelay;
private final boolean closeInventory, log;

public InventoryLag() {
Expand All @@ -33,18 +35,31 @@ public InventoryLag() {
"during which they will be very limited in terms of ItemStack or\n" +
"Inventory interactions.");
this.log = config.getBoolean(configPath + ".log", false);
this.closeInventory = config.getBoolean(configPath + ".close-open-inventory", true,
"Whether to immediately close any open inventory of the player on limit exceed\n" +
"Side note: Closing has to be scheduled so it will take a bit if the server is heavily\n" +
"lagging.");
this.playerDataCache = Caffeine.newBuilder().expireAfterWrite(Duration.ofMillis(Math.max(1L,
config.getLong(configPath + ".timeframe-millis", 20000,
config.getLong(configPath + ".data-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,
"Needs to be at least as long as your cooldown millis.")))).build();
this.rateLimitBytes = config.getLong(configPath + "rate-limit.bytesize-limit", 3584000,
"The limit in bytes the server has sent the server in the form of ItemStacks,\n" +
"before the player will be put on a rate-limit.\n" +
"Should always be lower than lockout bytesize limit.");
this.screenOpenDelay = config.getInt(configPath + "rate-limit.timeframe-millis", 1500,
"The time in millis in which a player is allowed to open x amounts of windows\n" +
"but not more.");
this.screenOpenLimit = config.getInt(configPath + "rate-limit.max-window-opens-per-timeframe", 2,
"The amount of windows that can be opened during the timeframe-millis.");
this.lockoutBytes = config.getLong(configPath + ".lockout.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,
"within the configured timeframe before he will be put on cooldown.\n" +
"During the cooldown, he will not be able to open any inventory screens\n" +
"or interact with items.");
this.lockoutMillis = config.getLong(configPath + "lockout.duration-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
Expand All @@ -70,56 +85,81 @@ 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()) {
PlayerData data = playerDataCache.get(event.getUser().getUUID(), PlayerData::new);

if (data.screenNextAllowedOpen.get() > System.currentTimeMillis()) {
event.setCancelled(true);
if (log) info("Player '" + event.getUser().getName() + "' could not open screen because they are on cooldown.");
return;
}

if (
data.servedSetSlotBytes.get() > rateLimitBytes
&& data.servedSetSlotBytes.get() < lockoutBytes
&& data.screenOpenCount.getAndIncrement() > screenOpenLimit
) {
data.screenNextAllowedOpen.set(System.currentTimeMillis() + screenOpenDelay);
if (log) info("Player '" + event.getUser().getName() + "' is now on ratelimit cooldown as they exceeded the set limit.");
return;
}

return;
}

if (event.getPacketType() == PacketType.Play.Server.SET_SLOT) {
PlayerData data = playerDataCache.get(event.getUser().getUUID(), PlayerData::new);

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

if (data.receivedSetSlotBytes.addAndGet(ByteBufHelper.readableBytes(event.getByteBuf())) <= maxByteSize) {
if (data.servedSetSlotBytes.addAndGet(ByteBufHelper.readableBytes(event.getByteBuf())) <= lockoutBytes) {
return;
}

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

if (log) {
info("Player " + event.getUser().getName() + " is now on cooldown for requesting more data " +
"from the server than the set limit.");
}
data.screenNextAllowedOpen.set(System.currentTimeMillis() + lockoutMillis);
if (log) info("Player '" + event.getUser().getName() + "' is now on lockout cooldown as they exceeded the set limit.");

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

if (log) info("Player '" + event.getUser().getName() + "' is now on lockout cooldown for requesting more data " +
"from the server than the set limit.");
}
}

@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()) {
PlayerData data = playerDataCache.get(event.getPlayer().getUniqueId(), PlayerData::new);

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

if (
data.servedSetSlotBytes.get() > rateLimitBytes
&& data.servedSetSlotBytes.get() < lockoutBytes
&& data.screenOpenCount.getAndIncrement() > screenOpenLimit
) {
data.screenNextAllowedOpen.set(System.currentTimeMillis() + screenOpenDelay);
}
}

private static class PlayerData {

public final UUID playerUniqueId;
public final AtomicLong receivedSetSlotBytes, nextAllowedInvOpen;
public final UUID uuid;
public final AtomicLong servedSetSlotBytes, screenNextAllowedOpen;
public final AtomicInteger screenOpenCount;

public PlayerData(UUID playerUniqueId) {
this.playerUniqueId = playerUniqueId;
this.receivedSetSlotBytes = new AtomicLong(0L);
this.nextAllowedInvOpen = new AtomicLong(0L);
public PlayerData(UUID uuid) {
this.uuid = uuid;
this.servedSetSlotBytes = new AtomicLong(0L);
this.screenNextAllowedOpen = new AtomicLong(0L);
this.screenOpenCount = new AtomicInteger(0);
}
}
}

0 comments on commit eb7efa9

Please sign in to comment.