001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.bukkit.listener; 020 021import com.google.inject.Inject; 022import com.plotsquared.core.PlotSquared; 023import com.plotsquared.core.configuration.Settings; 024import com.plotsquared.core.location.Location; 025import com.plotsquared.core.plot.Plot; 026import com.plotsquared.core.plot.PlotArea; 027import com.plotsquared.core.plot.world.PlotAreaManager; 028import com.plotsquared.core.plot.world.SinglePlotArea; 029import com.plotsquared.core.util.ReflectionUtils.RefClass; 030import com.plotsquared.core.util.ReflectionUtils.RefField; 031import com.plotsquared.core.util.ReflectionUtils.RefMethod; 032import com.plotsquared.core.util.task.PlotSquaredTask; 033import com.plotsquared.core.util.task.TaskManager; 034import com.plotsquared.core.util.task.TaskTime; 035import io.papermc.lib.PaperLib; 036import org.bukkit.Bukkit; 037import org.bukkit.Chunk; 038import org.bukkit.Material; 039import org.bukkit.World; 040import org.bukkit.block.BlockState; 041import org.bukkit.entity.Entity; 042import org.bukkit.entity.Item; 043import org.bukkit.entity.LivingEntity; 044import org.bukkit.entity.Player; 045import org.bukkit.event.EventHandler; 046import org.bukkit.event.EventPriority; 047import org.bukkit.event.Listener; 048import org.bukkit.event.block.BlockPhysicsEvent; 049import org.bukkit.event.entity.CreatureSpawnEvent; 050import org.bukkit.event.entity.ItemSpawnEvent; 051import org.bukkit.event.world.ChunkLoadEvent; 052import org.bukkit.event.world.ChunkUnloadEvent; 053import org.checkerframework.checker.nullness.qual.NonNull; 054 055import java.lang.reflect.Method; 056import java.util.HashSet; 057import java.util.Objects; 058 059import static com.plotsquared.core.util.ReflectionUtils.getRefClass; 060 061@SuppressWarnings("unused") 062public class ChunkListener implements Listener { 063 064 private final PlotAreaManager plotAreaManager; 065 private final int version; 066 067 private RefMethod methodGetHandleChunk; 068 private RefMethod methodGetHandleWorld; 069 private RefField mustSave; 070 /* 071 private RefMethod methodGetFullChunk; 072 private RefMethod methodGetBukkitChunk; 073 private RefMethod methodGetChunkProvider; 074 private RefMethod methodGetVisibleMap; 075 private RefField worldServer; 076 private RefField playerChunkMap; 077 private RefField updatingChunks; 078 private RefField visibleChunks; 079 */ 080 private Chunk lastChunk; 081 private boolean ignoreUnload = false; 082 private boolean isTrueForNotSave = true; 083 084 @Inject 085 public ChunkListener(final @NonNull PlotAreaManager plotAreaManager) { 086 this.plotAreaManager = plotAreaManager; 087 version = PlotSquared.platform().serverVersion()[1]; 088 if (!Settings.Chunk_Processor.AUTO_TRIM) { 089 return; 090 } 091 try { 092 RefClass classCraftWorld = getRefClass("{cb}.CraftWorld"); 093 this.methodGetHandleWorld = classCraftWorld.getMethod("getHandle"); 094 RefClass classCraftChunk = getRefClass("{cb}.CraftChunk"); 095 this.methodGetHandleChunk = classCraftChunk.getMethod("getHandle"); 096 try { 097 if (version < 17) { 098 RefClass classChunk = getRefClass("{nms}.Chunk"); 099 if (version == 13) { 100 this.mustSave = classChunk.getField("mustSave"); 101 this.isTrueForNotSave = false; 102 } else { 103 this.mustSave = classChunk.getField("mustNotSave"); 104 } 105 } else { 106 RefClass classChunk = getRefClass("net.minecraft.world.level.chunk.Chunk"); 107 this.mustSave = classChunk.getField("mustNotSave"); 108 109 } 110 } catch (NoSuchFieldException e) { 111 e.printStackTrace(); 112 } 113 } catch (Throwable ignored) { 114 Settings.Chunk_Processor.AUTO_TRIM = false; 115 } 116 for (World world : Bukkit.getWorlds()) { 117 world.setAutoSave(false); 118 } 119 if (version > 13) { 120 return; 121 } 122 TaskManager.runTaskRepeat(() -> { 123 try { 124 HashSet<Chunk> toUnload = new HashSet<>(); 125 for (World world : Bukkit.getWorlds()) { 126 String worldName = world.getName(); 127 if (!this.plotAreaManager.hasPlotArea(worldName)) { 128 continue; 129 } 130 Object craftWorld = methodGetHandleWorld.of(world).call(); 131 if (version == 13) { 132 Object chunkMap = craftWorld.getClass().getDeclaredMethod("getPlayerChunkMap").invoke(craftWorld); 133 Method methodIsChunkInUse = 134 chunkMap.getClass().getDeclaredMethod("isChunkInUse", int.class, int.class); 135 Chunk[] chunks = world.getLoadedChunks(); 136 for (Chunk chunk : chunks) { 137 if ((boolean) methodIsChunkInUse.invoke(chunkMap, chunk.getX(), chunk.getZ())) { 138 continue; 139 } 140 int x = chunk.getX(); 141 int z = chunk.getZ(); 142 if (!shouldSave(worldName, x, z)) { 143 unloadChunk(worldName, chunk, false); 144 continue; 145 } 146 toUnload.add(chunk); 147 } 148 } 149 } 150 if (toUnload.isEmpty()) { 151 return; 152 } 153 long start = System.currentTimeMillis(); 154 for (Chunk chunk : toUnload) { 155 if (System.currentTimeMillis() - start > 5) { 156 return; 157 } 158 chunk.unload(true); 159 } 160 } catch (Throwable e) { 161 e.printStackTrace(); 162 } 163 }, TaskTime.ticks(1L)); 164 } 165 166 public boolean unloadChunk(String world, Chunk chunk, boolean safe) { 167 if (safe && shouldSave(world, chunk.getX(), chunk.getZ())) { 168 return false; 169 } 170 Object c = this.methodGetHandleChunk.of(chunk).call(); 171 RefField.RefExecutor field = this.mustSave.of(c); 172 if ((Boolean) field.get() != isTrueForNotSave) { 173 field.set(isTrueForNotSave); 174 if (chunk.isLoaded()) { 175 ignoreUnload = true; 176 chunk.unload(false); 177 ignoreUnload = false; 178 } 179 } 180 return true; 181 } 182 183 public boolean shouldSave(String world, int chunkX, int chunkZ) { 184 int x = chunkX << 4; 185 int z = chunkZ << 4; 186 int x2 = x + 15; 187 int z2 = z + 15; 188 Location loc = Location.at(world, x, 1, z); 189 PlotArea plotArea = plotAreaManager.getPlotArea(loc); 190 if (plotArea != null) { 191 Plot plot = plotArea.getPlot(loc); 192 if (plot != null && plot.hasOwner()) { 193 return true; 194 } 195 } 196 loc = Location.at(world, x2, 1, z2); 197 plotArea = plotAreaManager.getPlotArea(loc); 198 if (plotArea != null) { 199 Plot plot = plotArea.getPlot(loc); 200 if (plot != null && plot.hasOwner()) { 201 return true; 202 } 203 } 204 loc = Location.at(world, x2, 1, z); 205 plotArea = plotAreaManager.getPlotArea(loc); 206 if (plotArea != null) { 207 Plot plot = plotArea.getPlot(loc); 208 if (plot != null && plot.hasOwner()) { 209 return true; 210 } 211 } 212 loc = Location.at(world, x, 1, z2); 213 plotArea = plotAreaManager.getPlotArea(loc); 214 if (plotArea != null) { 215 Plot plot = plotArea.getPlot(loc); 216 if (plot != null && plot.hasOwner()) { 217 return true; 218 } 219 } 220 loc = Location.at(world, x + 7, 1, z + 7); 221 plotArea = plotAreaManager.getPlotArea(loc); 222 if (plotArea == null) { 223 return false; 224 } 225 Plot plot = plotArea.getPlot(loc); 226 return plot != null && plot.hasOwner(); 227 } 228 229 @EventHandler 230 public void onChunkUnload(ChunkUnloadEvent event) { 231 if (ignoreUnload) { 232 return; 233 } 234 Chunk chunk = event.getChunk(); 235 if (Settings.Chunk_Processor.AUTO_TRIM) { 236 String world = chunk.getWorld().getName(); 237 if ((!Settings.Enabled_Components.WORLDS || !SinglePlotArea.isSinglePlotWorld(world)) && this.plotAreaManager.hasPlotArea(world)) { 238 if (unloadChunk(world, chunk, true)) { 239 return; 240 } 241 } 242 } 243 if (processChunk(event.getChunk(), true)) { 244 chunk.setForceLoaded(true); 245 } 246 } 247 248 @EventHandler 249 public void onChunkLoad(ChunkLoadEvent event) { 250 processChunk(event.getChunk(), false); 251 } 252 253 @EventHandler(priority = EventPriority.LOWEST) 254 public void onItemSpawn(ItemSpawnEvent event) { 255 Item entity = event.getEntity(); 256 PaperLib.getChunkAtAsync(event.getLocation()).thenAccept(chunk -> { 257 if (chunk == this.lastChunk) { 258 event.getEntity().remove(); 259 event.setCancelled(true); 260 return; 261 } 262 if (!this.plotAreaManager.hasPlotArea(chunk.getWorld().getName())) { 263 return; 264 } 265 Entity[] entities = chunk.getEntities(); 266 if (entities.length > Settings.Chunk_Processor.MAX_ENTITIES) { 267 event.getEntity().remove(); 268 event.setCancelled(true); 269 this.lastChunk = chunk; 270 } else { 271 this.lastChunk = null; 272 } 273 }); 274 } 275 276 @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 277 public void onBlockPhysics(BlockPhysicsEvent event) { 278 if (Settings.Chunk_Processor.DISABLE_PHYSICS) { 279 event.setCancelled(true); 280 } 281 } 282 283 @EventHandler(priority = EventPriority.LOWEST) 284 public void onEntitySpawn(CreatureSpawnEvent event) { 285 LivingEntity entity = event.getEntity(); 286 PaperLib.getChunkAtAsync(event.getLocation()).thenAccept(chunk -> { 287 if (chunk == this.lastChunk) { 288 event.getEntity().remove(); 289 event.setCancelled(true); 290 return; 291 } 292 if (!this.plotAreaManager.hasPlotArea(chunk.getWorld().getName())) { 293 return; 294 } 295 Entity[] entities = chunk.getEntities(); 296 if (entities.length > Settings.Chunk_Processor.MAX_ENTITIES) { 297 event.getEntity().remove(); 298 event.setCancelled(true); 299 this.lastChunk = chunk; 300 } else { 301 this.lastChunk = null; 302 } 303 }); 304 } 305 306 private void cleanChunk(final Chunk chunk) { 307 final int currentIndex = TaskManager.index.incrementAndGet(); 308 PlotSquaredTask task = TaskManager.runTaskRepeat(() -> { 309 if (!chunk.isLoaded()) { 310 Objects.requireNonNull(TaskManager.removeTask(currentIndex)).cancel(); 311 chunk.unload(true); 312 return; 313 } 314 BlockState[] tiles = chunk.getTileEntities(); 315 if (tiles.length == 0) { 316 Objects.requireNonNull(TaskManager.removeTask(currentIndex)).cancel(); 317 chunk.unload(true); 318 return; 319 } 320 long start = System.currentTimeMillis(); 321 int i = 0; 322 while (System.currentTimeMillis() - start < 250) { 323 if (i >= tiles.length - Settings.Chunk_Processor.MAX_TILES) { 324 Objects.requireNonNull(TaskManager.removeTask(currentIndex)).cancel(); 325 chunk.unload(true); 326 return; 327 } 328 tiles[i].getBlock().setType(Material.AIR, false); 329 i++; 330 } 331 }, TaskTime.ticks(5L)); 332 TaskManager.addTask(task, currentIndex); 333 } 334 335 public boolean processChunk(Chunk chunk, boolean unload) { 336 if (!this.plotAreaManager.hasPlotArea(chunk.getWorld().getName())) { 337 return false; 338 } 339 Entity[] entities = chunk.getEntities(); 340 BlockState[] tiles = chunk.getTileEntities(); 341 if (entities.length > Settings.Chunk_Processor.MAX_ENTITIES) { 342 int toRemove = entities.length - Settings.Chunk_Processor.MAX_ENTITIES; 343 int index = 0; 344 while (toRemove > 0 && index < entities.length) { 345 final Entity entity = entities[index++]; 346 if (!(entity instanceof Player)) { 347 entity.remove(); 348 toRemove--; 349 } 350 } 351 } 352 if (tiles.length > Settings.Chunk_Processor.MAX_TILES) { 353 if (unload) { 354 cleanChunk(chunk); 355 return true; 356 } 357 358 for (int i = 0; i < (tiles.length - Settings.Chunk_Processor.MAX_TILES); i++) { 359 tiles[i].getBlock().setType(Material.AIR, false); 360 } 361 } 362 return false; 363 } 364 365}