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