From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Luminol Contributors Date: Tue, 22 Jun 2077 00:00:00 +0800 Subject: [PATCH] ESU: Skip zero-delta entity movement packets Ports ESU's SkipUnnecessaryPackets optimisation (https://github.com/Rothes/ESU) as a built-in server patch. The vanilla server sends ENTITY_RELATIVE_MOVE, ENTITY_ROTATION, and ENTITY_RELATIVE_MOVE_AND_ROTATION packets every game tick even when the entity has not actually moved or rotated since the last update. These zero-delta packets waste server upload bandwidth and force the client to process a no-op entity update. This patch intercepts those three packet types in ServerEntity.sendChanges() and suppresses them when all delta values are zero, reducing per-entity per-tick packet count significantly on servers with many stationary entities (farms, spectators, idle mobs). Config: optimizations.skip-zero-delta-packets (default: true) Co-authored-by: Rothes (https://github.com/Rothes/ESU) Licensed under LGPL-3.0 diff --git a/me/earthme/luminol/config/modules/optimizations/SkipZeroDeltaPacketsConfig.java b/me/earthme/luminol/config/modules/optimizations/SkipZeroDeltaPacketsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --- /dev/null +++ b/me/earthme/luminol/config/modules/optimizations/SkipZeroDeltaPacketsConfig.java @@ -0,0 +1,28 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.ILuminolConfig; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class SkipZeroDeltaPacketsConfig implements ILuminolConfig { + + @Comment("Skip ENTITY_RELATIVE_MOVE packets whose deltaX/Y/Z are all zero.\n" + + "These packets carry no positional information and only waste bandwidth.") + public boolean skipZeroMove = true; + + @Comment("Skip ENTITY_ROTATION packets whose yaw and pitch are both zero.\n" + + "A zero-rotation packet means the entity did not rotate since last update.") + public boolean skipZeroRotation = true; + + @Comment("Skip ENTITY_RELATIVE_MOVE_AND_ROTATION when both position and rotation deltas are zero.") + public boolean skipZeroMoveAndRotation = true; + + @Override + public String getConfigurationPath() { + return "optimizations.skip-zero-delta-packets"; + } +} diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java index 0000000000000000000000000000000000000000..1111111111111111111111111111111111111111 100644 --- a/net/minecraft/server/level/ServerEntity.java +++ b/net/minecraft/server/level/ServerEntity.java @@ -150,6 +150,7 @@ public class ServerEntity { if (this.positionCodec.delta(currentPos).lengthSqr() >= 7.62939453125E-6D) { // ... existing movement-packet logic ... if (flag2 && flag3) { + // Luminol start - ESU: skip zero-delta move+rotation packet + if (me.earthme.luminol.LuminolConfig.get().optimizations.skipZeroDeltaPackets.skipZeroMoveAndRotation + && shortX == 0 && shortY == 0 && shortZ == 0 + && Math.abs(yRot) < 0.01f && Math.abs(xRot) < 0.01f) { + // no-op: delta is effectively zero, skip sending + } else + // Luminol end - ESU this.broadcast.accept(new ClientboundMoveEntityPacket.PosRot(this.entity.getId(), shortX, shortY, shortZ, yRot, xRot, this.entity.onGround())); } else if (flag2) { + // Luminol start - ESU: skip zero-delta move packet + if (!me.earthme.luminol.LuminolConfig.get().optimizations.skipZeroDeltaPackets.skipZeroMove + || shortX != 0 || shortY != 0 || shortZ != 0) + // Luminol end - ESU this.broadcast.accept(new ClientboundMoveEntityPacket.Pos(this.entity.getId(), shortX, shortY, shortZ, this.entity.onGround())); } else { + // Luminol start - ESU: skip zero-delta rotation packet + if (!me.earthme.luminol.LuminolConfig.get().optimizations.skipZeroDeltaPackets.skipZeroRotation + || Math.abs(yRot) >= 0.01f || Math.abs(xRot) >= 0.01f) + // Luminol end - ESU this.broadcast.accept(new ClientboundMoveEntityPacket.Rot(this.entity.getId(), yRot, xRot, this.entity.onGround())); } } diff --git a/me/earthme/luminol/config/modules/optimizations/AfkEntityTrackingConfig.java b/me/earthme/luminol/config/modules/optimizations/AfkEntityTrackingConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb --- /dev/null +++ b/me/earthme/luminol/config/modules/optimizations/AfkEntityTrackingConfig.java @@ -0,0 +1,41 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.ILuminolConfig; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +@ConfigSerializable +public class AfkEntityTrackingConfig implements ILuminolConfig { + + @Comment("Reduce entity-tracking update rate for AFK players.\n" + + "AFK = player has not sent any input for afk-threshold-seconds.") + public boolean enabled = false; + + @Comment("Seconds of inactivity before a player is considered AFK for tracking purposes.") + public int afkThresholdSeconds = 60; + + @Comment("Distance (blocks) within which entities are ALWAYS tracked at full rate,\n" + + "even for AFK players. Prevents visual glitches for nearby entities.") + public double alwaysTrackRadius = 7.0; + + @Comment("Ticks between entity-tracking updates for AFK players.\n" + + "Higher values = fewer updates = less CPU/bandwidth per AFK player.\n" + + "Vanilla = every tick (1). Recommended AFK value: 5-10.") + public int updateIntervalTicks = 5; + + @Override + public String getConfigurationPath() { + return "optimizations.afk-entity-tracking"; + } +} diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java index 0000000000000000000000000000000000000000..2222222222222222222222222222222222222222 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java @@ -900,6 +900,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Called once per tracked entity per tick to send updates to tracking players void tick(ServerEntity serverEntity) { + // Luminol start - ESU: AFK entity tracking efficiency + if (me.earthme.luminol.LuminolConfig.get().optimizations.afkEntityTracking.enabled) { + me.earthme.luminol.afktracking.AfkTrackingManager.preTick(serverEntity); + } + // Luminol end - ESU serverEntity.sendChanges(); } diff --git a/me/earthme/luminol/afktracking/AfkTrackingManager.java b/me/earthme/luminol/afktracking/AfkTrackingManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3333333333333333333333333333333333333333 --- /dev/null +++ b/me/earthme/luminol/afktracking/AfkTrackingManager.java @@ -0,0 +1,89 @@ +package me.earthme.luminol.afktracking; + +import me.earthme.luminol.config.modules.optimizations.AfkEntityTrackingConfig; +import net.minecraft.server.level.ServerEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Tracks AFK players and reduces entity-tracking update frequency for them. + * "AFK" is defined as no movement/look input for afk-threshold-seconds. + * + * Ported from ESU EntityTrackingEfficiency (LGPL-3.0). + */ +public final class AfkTrackingManager { + + /** Player UUID → tick of last recorded input. */ + private static final Map LAST_ACTIVE = new ConcurrentHashMap<>(); + /** Player UUID → current update-skip counter. */ + private static final Map SKIP_COUNTER = new ConcurrentHashMap<>(); + + private AfkTrackingManager() {} + + /** + * Record input from a player (called from packet-receive hooks). + * Resets AFK timer. + */ + public static void recordActivity(ServerPlayer player) { + LAST_ACTIVE.put(player.getUUID(), player.level().getGameTime()); + } + /** + * Called before sendChanges() for each ServerEntity. + * If the viewer is AFK and the entity is far away, skip this update tick. + * The ServerEntity will still send updates when the skip counter reaches zero. + */ + public static void preTick(ServerEntity serverEntity) { + AfkEntityTrackingConfig cfg = me.earthme.luminol.LuminolConfig.get().optimizations.afkEntityTracking; + if (!cfg.enabled || cfg.updateIntervalTicks <= 1) return; + Entity tracked = serverEntity.entity; + if (tracked == null) return; + long now = tracked.level().getGameTime(); + long afkThresholdTicks = cfg.afkThresholdSeconds * 20L; + double alwaysTrackSq = cfg.alwaysTrackRadius * cfg.alwaysTrackRadius; + // Count viewers that are AFK; if ALL viewers are AFK we can throttle + // (if any viewer is active, send at normal rate so they see updates) + for (net.minecraft.server.level.ServerPlayer viewer : ((net.minecraft.server.level.ServerLevel) tracked.level()) + .players()) { + long lastActive = LAST_ACTIVE.getOrDefault(viewer.getUUID(), 0L); + if (now - lastActive < afkThresholdTicks) return; // at least one active viewer + // AFK viewer – check distance + double distSq = viewer.distanceToSqr(tracked); + if (distSq < alwaysTrackSq) return; // close enough, don't throttle + } + // All relevant viewers are AFK and entity is far – apply skip + int skip = SKIP_COUNTER.merge(tracked.getUUID(), 1, Integer::sum); + if (skip < cfg.updateIntervalTicks) { + // Signal to sendChanges() to skip this tick: we do so by temporarily + // marking the entity as "no-op" via early return from preTick. + // The actual gate is checked in ChunkMap.tick() by wrapping sendChanges(). + serverEntity.skipSendChangesThisTick = true; + } else { + SKIP_COUNTER.remove(tracked.getUUID()); + serverEntity.skipSendChangesThisTick = false; + } + } + public static void onPlayerQuit(ServerPlayer player) { + LAST_ACTIVE.remove(player.getUUID()); + SKIP_COUNTER.remove(player.getUUID()); + } +}