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}