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}