From a8c5fd84664522d01ea7a61535b3f4a2c78245db Mon Sep 17 00:00:00 2001 From: Tommy Date: Tue, 8 Sep 2026 11:46:47 +0200 Subject: [PATCH] Fix packet flood caused by dead scale/color dirty-check sendScale() and sendColor() compared the current value against the last sent one but never stored it, so lastScale/lastColor stayed at their initial -1.0f/null forever and every comparison failed. With the 50Hz per-entity task this resent scale and color to every viewer on every tick: ~7.8k plugin messages/s on geyserutils:main for 52 models and 2 Bedrock players, of which 99.7% were byte-identical duplicates. - Store the sent value in both ModelEngine and BetterModel handlers, so the existing dirty-check actually filters. - BetterModel: sendScale had no dirty-check at all and sendColor had the condition inverted (it only applied on firstSend). Consume the hurt flag together with the tint calculation, otherwise an early return leaves the model tinted forever. - Keep a forced resend every ~100 ticks. The one-shot send after spawn is dropped silently by the Geyser side when the custom entity is not registered yet, and the 50Hz loop was the only thing papering over it. - Schedule BedrockMountControlRunnable at a configurable period (default 50ms) instead of 1ms, and the per-entity task at a configurable period (default 20ms, unchanged behaviour). - Skip null players in BedrockMountControlRunnable: the quit handler removes the UUID after the player is already gone. - Make playerJoinedCache a concurrent set; it was a plain HashSet written from the main thread and iterated from the scheduler pool. - Cache the Bedrock player list once per global update cycle instead of scanning Bukkit.getOnlinePlayers() from every per-entity task. --- .../geysermodelengine/GeyserModelEngine.java | 2 +- .../managers/model/EntityTaskManager.java | 18 ++++++++++++++++-- .../managers/model/ModelManager.java | 4 ++-- .../BetterModelPropertyHandler.java | 18 +++++++++++++----- .../ModelEnginePropertyHandler.java | 4 ++++ .../taskshandler/BetterModelTaskHandler.java | 12 +++++++++--- .../taskshandler/ModelEngineTaskHandler.java | 12 +++++++++--- .../runnables/BedrockMountControlRunnable.java | 1 + .../runnables/UpdateTaskRunnable.java | 2 ++ paper/src/main/resources/config.yml | 2 ++ 10 files changed, 59 insertions(+), 16 deletions(-) diff --git a/paper/src/main/java/re/imc/geysermodelengine/GeyserModelEngine.java b/paper/src/main/java/re/imc/geysermodelengine/GeyserModelEngine.java index eb45dc9..b25dcbb 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/GeyserModelEngine.java +++ b/paper/src/main/java/re/imc/geysermodelengine/GeyserModelEngine.java @@ -85,7 +85,7 @@ private void loadManagers() { private void loadRunnables() { this.schedulerPool = Executors.newScheduledThreadPool(configManager.getConfig().getInt("models.thread-pool-size", 4)); this.schedulerPool.scheduleAtFixedRate(new UpdateTaskRunnable(this), 10, configManager.getConfig().getLong("models.entity-position-update-period", 35), TimeUnit.MILLISECONDS); - this.schedulerPool.scheduleAtFixedRate(new BedrockMountControlRunnable(this), 1, 1, TimeUnit.MILLISECONDS); + this.schedulerPool.scheduleAtFixedRate(new BedrockMountControlRunnable(this), 1, configManager.getConfig().getLong("models.mount-control-period", 50), TimeUnit.MILLISECONDS); } public ConfigManager getConfigManager() { diff --git a/paper/src/main/java/re/imc/geysermodelengine/managers/model/EntityTaskManager.java b/paper/src/main/java/re/imc/geysermodelengine/managers/model/EntityTaskManager.java index e9cbdf7..4f470e3 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/managers/model/EntityTaskManager.java +++ b/paper/src/main/java/re/imc/geysermodelengine/managers/model/EntityTaskManager.java @@ -25,6 +25,8 @@ public class EntityTaskManager { private PropertyHandler propertyHandler; + private volatile List bedrockPlayers = Collections.emptyList(); + public EntityTaskManager(GeyserModelEngine plugin) { this.plugin = plugin; @@ -40,10 +42,22 @@ public EntityTaskManager(GeyserModelEngine plugin) { } } - public void checkViewers(EntityData model, Set viewers) { + /** + * Refreshes the cached list of Bedrock players. Called once per global update cycle instead of + * scanning the online player list from every per-entity task. + */ + public void refreshBedrockPlayers() { + List players = new ArrayList<>(); + for (Player onlinePlayer : Bukkit.getOnlinePlayers()) { - if (!BedrockUtils.isBedrockPlayer(onlinePlayer)) continue; + if (BedrockUtils.isBedrockPlayer(onlinePlayer)) players.add(onlinePlayer); + } + bedrockPlayers = players; + } + + public void checkViewers(EntityData model, Set viewers) { + for (Player onlinePlayer : bedrockPlayers) { if (canSee(onlinePlayer, model.getEntity(), model.getModelInstance())) { if (!viewers.contains(onlinePlayer)) { viewers.add(onlinePlayer); diff --git a/paper/src/main/java/re/imc/geysermodelengine/managers/model/ModelManager.java b/paper/src/main/java/re/imc/geysermodelengine/managers/model/ModelManager.java index 1f21d23..5d05071 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/managers/model/ModelManager.java +++ b/paper/src/main/java/re/imc/geysermodelengine/managers/model/ModelManager.java @@ -20,7 +20,7 @@ public class ModelManager { private ModelHandler modelHandler; - private final HashSet playerJoinedCache = new HashSet<>(); + private final Set playerJoinedCache = ConcurrentHashMap.newKeySet(); private final ConcurrentHashMap modelEntitiesCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap> entitiesCache = new ConcurrentHashMap<>(); @@ -56,7 +56,7 @@ public ModelHandler getModelHandler() { return modelHandler; } - public HashSet getPlayerJoinedCache() { + public Set getPlayerJoinedCache() { return playerJoinedCache; } diff --git a/paper/src/main/java/re/imc/geysermodelengine/managers/model/propertyhandler/BetterModelPropertyHandler.java b/paper/src/main/java/re/imc/geysermodelengine/managers/model/propertyhandler/BetterModelPropertyHandler.java index 519c43a..fbea8af 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/managers/model/propertyhandler/BetterModelPropertyHandler.java +++ b/paper/src/main/java/re/imc/geysermodelengine/managers/model/propertyhandler/BetterModelPropertyHandler.java @@ -30,11 +30,18 @@ public BetterModelPropertyHandler(GeyserModelEngine plugin) { @Override public void sendScale(EntityData entityData, Collection players, float lastScale, boolean firstSend) { + if (players.isEmpty()) return; + BetterModelEntityData betterModelEntityData = (BetterModelEntityData) entityData; Tracker tracker = (Tracker) betterModelEntityData.getModelInstance(); ModelScaler scaler = tracker.scaler(); var scale = scaler.scale(tracker); + + if (!firstSend && scale == lastScale) return; + players.forEach(player -> EntityUtils.sendCustomScale(player, betterModelEntityData.getEntity().getEntityId(), scale)); + + betterModelEntityData.getEntityTask().setLastScale(scale); } @Override @@ -44,17 +51,18 @@ public void sendColor(EntityData entityData, Collection players, Color l BetterModelEntityData betterModelEntityData = (BetterModelEntityData) entityData; Color color = new Color(0xFFFFFF); - if (betterModelEntityData.isHurt()) color = new Color(betterModelEntityData.getEntityTracker().damageTintValue()); - - if (firstSend) { - if (color.equals(lastColor)) return; + if (betterModelEntityData.isHurt()) { + color = new Color(betterModelEntityData.getEntityTracker().damageTintValue()); + betterModelEntityData.setHurt(false); } + if (!firstSend && color.equals(lastColor)) return; + for (Player player : players) { EntityUtils.sendCustomColor(player, betterModelEntityData.getEntity().getEntityId(), color); } - betterModelEntityData.setHurt(false); + betterModelEntityData.getEntityTask().setLastColor(color); } @Override diff --git a/paper/src/main/java/re/imc/geysermodelengine/managers/model/propertyhandler/ModelEnginePropertyHandler.java b/paper/src/main/java/re/imc/geysermodelengine/managers/model/propertyhandler/ModelEnginePropertyHandler.java index d6c86fc..8a851b9 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/managers/model/propertyhandler/ModelEnginePropertyHandler.java +++ b/paper/src/main/java/re/imc/geysermodelengine/managers/model/propertyhandler/ModelEnginePropertyHandler.java @@ -40,6 +40,8 @@ public void sendScale(EntityData modelData, Collection players, float la } players.forEach(player -> EntityUtils.sendCustomScale(player, modelEngineEntityData.getEntity().getEntityId(), average)); + + modelEngineEntityData.getEntityTask().setLastScale(average); } catch (Exception err) { throw new RuntimeException(err); } @@ -55,6 +57,8 @@ public void sendColor(EntityData entityData, Collection players, Color l if (!firstSend && color.equals(lastColor)) return; players.forEach(player -> EntityUtils.sendCustomColor(player, data.getEntity().getEntityId(), color)); + + data.getEntityTask().setLastColor(color); } @Override diff --git a/paper/src/main/java/re/imc/geysermodelengine/managers/model/taskshandler/BetterModelTaskHandler.java b/paper/src/main/java/re/imc/geysermodelengine/managers/model/taskshandler/BetterModelTaskHandler.java index d552736..7d28428 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/managers/model/taskshandler/BetterModelTaskHandler.java +++ b/paper/src/main/java/re/imc/geysermodelengine/managers/model/taskshandler/BetterModelTaskHandler.java @@ -50,7 +50,7 @@ public BetterModelTaskHandler(GeyserModelEngine plugin, BetterModelEntityData en } catch (Throwable err) { err.printStackTrace(); } - }, 0, 20, TimeUnit.MILLISECONDS); + }, 0, plugin.getConfigManager().getConfig().getLong("models.entity-update-period", 20), TimeUnit.MILLISECONDS); } @Override @@ -84,6 +84,12 @@ public void runAsync() { } tick++; + + // The first scale/color send after spawn is fired once, and the Geyser side silently drops it + // if the custom entity is not registered yet. Force a resend every ~2s so a model that missed + // that window recovers instead of staying at default scale / no tint forever. + boolean forceSync = tick % 100 == 0; + if (tick > 400) { tick = 0; plugin.getEntityTaskManager().sendHitBoxToAll(entityData); @@ -91,8 +97,8 @@ public void runAsync() { if (viewers.isEmpty()) return; - plugin.getEntityTaskManager().getPropertyHandler().sendScale(entityData, viewers, lastScale, false); - plugin.getEntityTaskManager().getPropertyHandler().sendColor(entityData, viewers, lastColor, false); + plugin.getEntityTaskManager().getPropertyHandler().sendScale(entityData, viewers, lastScale, forceSync); + plugin.getEntityTaskManager().getPropertyHandler().sendColor(entityData, viewers, lastColor, forceSync); } @Override diff --git a/paper/src/main/java/re/imc/geysermodelengine/managers/model/taskshandler/ModelEngineTaskHandler.java b/paper/src/main/java/re/imc/geysermodelengine/managers/model/taskshandler/ModelEngineTaskHandler.java index 8630e4f..c524ffb 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/managers/model/taskshandler/ModelEngineTaskHandler.java +++ b/paper/src/main/java/re/imc/geysermodelengine/managers/model/taskshandler/ModelEngineTaskHandler.java @@ -50,7 +50,7 @@ public ModelEngineTaskHandler(GeyserModelEngine plugin, ModelEngineEntityData en } catch (Throwable err) { err.printStackTrace(); } - }, 0, 20, TimeUnit.MILLISECONDS); + }, 0, plugin.getConfigManager().getConfig().getLong("models.entity-update-period", 20), TimeUnit.MILLISECONDS); } @Override @@ -88,6 +88,12 @@ public void runAsync() { } tick++; + + // The first scale/color send after spawn is fired once, and the Geyser side silently drops it + // if the custom entity is not registered yet. Force a resend every ~2s so a model that missed + // that window recovers instead of staying at default scale / no tint forever. + boolean forceSync = tick % 100 == 0; + if (tick > 400) { tick = 0; plugin.getEntityTaskManager().sendHitBoxToAll(entityData); @@ -95,8 +101,8 @@ public void runAsync() { if (viewers.isEmpty()) return; - plugin.getEntityTaskManager().getPropertyHandler().sendScale(entityData, viewers, lastScale, false); - plugin.getEntityTaskManager().getPropertyHandler().sendColor(entityData, viewers, lastColor, false); + plugin.getEntityTaskManager().getPropertyHandler().sendScale(entityData, viewers, lastScale, forceSync); + plugin.getEntityTaskManager().getPropertyHandler().sendColor(entityData, viewers, lastColor, forceSync); } @Override diff --git a/paper/src/main/java/re/imc/geysermodelengine/runnables/BedrockMountControlRunnable.java b/paper/src/main/java/re/imc/geysermodelengine/runnables/BedrockMountControlRunnable.java index 3f55d56..8702d08 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/runnables/BedrockMountControlRunnable.java +++ b/paper/src/main/java/re/imc/geysermodelengine/runnables/BedrockMountControlRunnable.java @@ -24,6 +24,7 @@ public BedrockMountControlRunnable(GeyserModelEngine plugin) { public void run() { for (UUID playerUUID : plugin.getModelManager().getPlayerJoinedCache()) { Player player = Bukkit.getPlayer(playerUUID); + if (player == null) continue; float pitch = player.getLocation().getPitch(); Pair seat = plugin.getModelManager().getDriversCache().get(player.getUniqueId()); diff --git a/paper/src/main/java/re/imc/geysermodelengine/runnables/UpdateTaskRunnable.java b/paper/src/main/java/re/imc/geysermodelengine/runnables/UpdateTaskRunnable.java index a6fa609..61db721 100644 --- a/paper/src/main/java/re/imc/geysermodelengine/runnables/UpdateTaskRunnable.java +++ b/paper/src/main/java/re/imc/geysermodelengine/runnables/UpdateTaskRunnable.java @@ -17,6 +17,8 @@ public UpdateTaskRunnable(GeyserModelEngine plugin) { @Override public void run() { + plugin.getEntityTaskManager().refreshBedrockPlayers(); + ConcurrentHashMap> entitiesCache = plugin.getModelManager().getEntitiesCache(); if (entitiesCache.isEmpty()) return; diff --git a/paper/src/main/resources/config.yml b/paper/src/main/resources/config.yml index d03e6f2..fa1ba4b 100644 --- a/paper/src/main/resources/config.yml +++ b/paper/src/main/resources/config.yml @@ -10,6 +10,8 @@ models: custom-entity-sync-resend-interval: 40 custom-entity-sync-resend-count: 3 entity-position-update-period: 35 # ms + entity-update-period: 20 # ms + mount-control-period: 50 # ms thread-pool-size: 4 options: