220 lines
11 KiB
Diff
220 lines
11 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||
From: Luminol Contributors <luminol@example.com>
|
||
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<UUID, Long> LAST_ACTIVE = new ConcurrentHashMap<>();
|
||
+ /** Player UUID → current update-skip counter. */
|
||
+ private static final Map<UUID, Integer> 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());
|
||
+ }
|
||
+}
|