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.bukkit.util.PaperSupport;
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.MinecraftVersion;
030import com.plotsquared.core.util.ReflectionUtils;
031import com.plotsquared.core.util.ReflectionUtils.RefClass;
032import com.plotsquared.core.util.ReflectionUtils.RefField;
033import com.plotsquared.core.util.ReflectionUtils.RefMethod;
034import com.plotsquared.core.util.task.PlotSquaredTask;
035import com.plotsquared.core.util.task.TaskManager;
036import com.plotsquared.core.util.task.TaskTime;
037import org.apache.logging.log4j.LogManager;
038import org.apache.logging.log4j.Logger;
039import org.bukkit.Bukkit;
040import org.bukkit.Chunk;
041import org.bukkit.Material;
042import org.bukkit.World;
043import org.bukkit.block.BlockState;
044import org.bukkit.entity.Entity;
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.EntitySpawnEvent;
052import org.bukkit.event.entity.ItemSpawnEvent;
053import org.bukkit.event.world.ChunkLoadEvent;
054import org.bukkit.event.world.ChunkUnloadEvent;
055import org.checkerframework.checker.nullness.qual.NonNull;
056
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 static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + ChunkListener.class.getSimpleName());
065
066    private final PlotAreaManager plotAreaManager;
067
068    private RefMethod methodSetUnsaved;
069    private RefMethod methodGetHandleChunk;
070    private RefMethod methodGetHandleWorld;
071    private RefField mustNotSave;
072    private Object objChunkStatusFull = null;
073    private Chunk lastChunk;
074    private boolean ignoreUnload = false;
075
076    @Inject
077    public ChunkListener(final @NonNull PlotAreaManager plotAreaManager) {
078        this.plotAreaManager = plotAreaManager;
079        if (!Settings.Chunk_Processor.AUTO_TRIM) {
080            return;
081        }
082        try {
083            RefClass classCraftWorld = getRefClass("{cb}.CraftWorld");
084            RefClass classCraftChunk = getRefClass("{cb}.CraftChunk");
085            ReflectionUtils.RefClass classChunkAccess = getRefClass("net.minecraft.world.level.chunk.IChunkAccess");
086            this.methodSetUnsaved = classChunkAccess.getMethod("a", boolean.class);
087            try {
088                this.methodGetHandleChunk = classCraftChunk.getMethod("getHandle");
089            } catch (NoSuchMethodException ignored) {
090                RefClass classChunkStatus = getRefClass("net.minecraft.world.level.chunk.ChunkStatus");
091                this.objChunkStatusFull = classChunkStatus.getRealClass().getField("n").get(null);
092                this.methodGetHandleChunk = classCraftChunk.getMethod("getHandle", classChunkStatus.getRealClass());
093            }
094            if (MinecraftVersion.current().isNewerOrEqualThan(MinecraftVersion.CAVES_AND_CLIFFS)) {
095                RefClass classChunk = getRefClass("net.minecraft.world.level.chunk.Chunk");
096                this.mustNotSave = classChunk.getField("mustNotSave");
097            } else {
098                RefClass classChunk = getRefClass("{nms}.Chunk");
099                this.mustNotSave = classChunk.getField("mustNotSave");
100            }
101        } catch (Throwable t) {
102            Settings.Chunk_Processor.AUTO_TRIM = false;
103            LOGGER.error("Failed to initialize NMS access for auto-trimming capabilities. Disabling auto trim", t);
104        }
105        for (World world : Bukkit.getWorlds()) {
106            world.setAutoSave(false);
107        }
108    }
109
110    public boolean unloadChunk(String world, Chunk chunk, boolean safe) {
111        if (safe && shouldSave(world, chunk.getX(), chunk.getZ())) {
112            return false;
113        }
114        Object c = objChunkStatusFull != null
115                ? this.methodGetHandleChunk.of(chunk).call(objChunkStatusFull)
116                : this.methodGetHandleChunk.of(chunk).call();
117        RefField.RefExecutor field = this.mustNotSave.of(c);
118        methodSetUnsaved.of(c).call(false);
119        if (!((Boolean) field.get())) {
120            field.set(true);
121            if (chunk.isLoaded()) {
122                ignoreUnload = true;
123                chunk.unload(false);
124                ignoreUnload = false;
125            }
126        }
127        return true;
128    }
129
130    public boolean shouldSave(String world, int chunkX, int chunkZ) {
131        int x = chunkX << 4;
132        int z = chunkZ << 4;
133        int x2 = x + 15;
134        int z2 = z + 15;
135        Location loc = Location.at(world, x, 1, z);
136        PlotArea plotArea = plotAreaManager.getPlotArea(loc);
137        if (plotArea != null) {
138            Plot plot = plotArea.getPlot(loc);
139            if (plot != null && plot.hasOwner()) {
140                return true;
141            }
142        }
143        loc = Location.at(world, x2, 1, z2);
144        plotArea = plotAreaManager.getPlotArea(loc);
145        if (plotArea != null) {
146            Plot plot = plotArea.getPlot(loc);
147            if (plot != null && plot.hasOwner()) {
148                return true;
149            }
150        }
151        loc = Location.at(world, x2, 1, z);
152        plotArea = plotAreaManager.getPlotArea(loc);
153        if (plotArea != null) {
154            Plot plot = plotArea.getPlot(loc);
155            if (plot != null && plot.hasOwner()) {
156                return true;
157            }
158        }
159        loc = Location.at(world, x, 1, z2);
160        plotArea = plotAreaManager.getPlotArea(loc);
161        if (plotArea != null) {
162            Plot plot = plotArea.getPlot(loc);
163            if (plot != null && plot.hasOwner()) {
164                return true;
165            }
166        }
167        loc = Location.at(world, x + 7, 1, z + 7);
168        plotArea = plotAreaManager.getPlotArea(loc);
169        if (plotArea == null) {
170            return false;
171        }
172        Plot plot = plotArea.getPlot(loc);
173        return plot != null && plot.hasOwner();
174    }
175
176    @EventHandler
177    public void onChunkUnload(ChunkUnloadEvent event) {
178        if (ignoreUnload) {
179            return;
180        }
181        Chunk chunk = event.getChunk();
182        if (Settings.Chunk_Processor.AUTO_TRIM) {
183            String world = chunk.getWorld().getName();
184            if ((!Settings.Enabled_Components.WORLDS || !SinglePlotArea.isSinglePlotWorld(world)) && this.plotAreaManager.hasPlotArea(
185                    world)) {
186                if (unloadChunk(world, chunk, true)) {
187                    return;
188                }
189            }
190        }
191        if (processChunk(event.getChunk(), true)) {
192            chunk.setForceLoaded(true);
193        }
194    }
195
196    @EventHandler
197    public void onChunkLoad(ChunkLoadEvent event) {
198        processChunk(event.getChunk(), false);
199    }
200
201    @EventHandler(priority = EventPriority.LOWEST)
202    public void onItemSpawn(ItemSpawnEvent event) {
203        this.onInternalEntitySpawn(event);
204    }
205
206    @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
207    public void onBlockPhysics(BlockPhysicsEvent event) {
208        if (Settings.Chunk_Processor.DISABLE_PHYSICS) {
209            event.setCancelled(true);
210        }
211    }
212
213    @EventHandler(priority = EventPriority.LOWEST)
214    public void onEntitySpawn(CreatureSpawnEvent event) {
215        this.onInternalEntitySpawn(event);
216    }
217
218    private void onInternalEntitySpawn(EntitySpawnEvent event) {
219        PaperSupport.getChunkAtAsync(event.getLocation()).thenAccept(chunk -> {
220            if (chunk == this.lastChunk) {
221                event.getEntity().remove();
222                event.setCancelled(true);
223                return;
224            }
225            if (!this.plotAreaManager.hasPlotArea(chunk.getWorld().getName())) {
226                return;
227            }
228            Entity[] entities = chunk.getEntities();
229            if (entities.length > Settings.Chunk_Processor.MAX_ENTITIES) {
230                event.getEntity().remove();
231                event.setCancelled(true);
232                this.lastChunk = chunk;
233            } else {
234                this.lastChunk = null;
235            }
236        });
237    }
238
239    private void cleanChunk(final Chunk chunk) {
240        final int currentIndex = TaskManager.index.incrementAndGet();
241        PlotSquaredTask task = TaskManager.runTaskRepeat(() -> {
242            if (!chunk.isLoaded()) {
243                Objects.requireNonNull(TaskManager.removeTask(currentIndex)).cancel();
244                chunk.unload(true);
245                return;
246            }
247            BlockState[] tiles = getTiles(chunk);
248            if (tiles.length == 0) {
249                Objects.requireNonNull(TaskManager.removeTask(currentIndex)).cancel();
250                chunk.unload(true);
251                return;
252            }
253            long start = System.currentTimeMillis();
254            int i = 0;
255            while (System.currentTimeMillis() - start < 250) {
256                if (i >= tiles.length - Settings.Chunk_Processor.MAX_TILES) {
257                    Objects.requireNonNull(TaskManager.removeTask(currentIndex)).cancel();
258                    chunk.unload(true);
259                    return;
260                }
261                tiles[i].getBlock().setType(Material.AIR, false);
262                i++;
263            }
264        }, TaskTime.ticks(5L));
265        TaskManager.addTask(task, currentIndex);
266    }
267
268    public boolean processChunk(Chunk chunk, boolean unload) {
269        if (!this.plotAreaManager.hasPlotArea(chunk.getWorld().getName())) {
270            return false;
271        }
272        Entity[] entities = chunk.getEntities();
273        BlockState[] tiles = getTiles(chunk);
274        if (entities.length > Settings.Chunk_Processor.MAX_ENTITIES) {
275            int toRemove = entities.length - Settings.Chunk_Processor.MAX_ENTITIES;
276            int index = 0;
277            while (toRemove > 0 && index < entities.length) {
278                final Entity entity = entities[index++];
279                if (!(entity instanceof Player)) {
280                    entity.remove();
281                    toRemove--;
282                }
283            }
284        }
285        if (tiles.length > Settings.Chunk_Processor.MAX_TILES) {
286            if (unload) {
287                cleanChunk(chunk);
288                return true;
289            }
290
291            for (int i = 0; i < (tiles.length - Settings.Chunk_Processor.MAX_TILES); i++) {
292                tiles[i].getBlock().setType(Material.AIR, false);
293            }
294        }
295        return false;
296    }
297
298    private static BlockState[] getTiles(Chunk chunk) {
299        return PaperSupport.isPaper() ? chunk.getTileEntities(false) : chunk.getTileEntities();
300    }
301
302}