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.core.util;
020
021import com.plotsquared.core.PlotSquared;
022import com.plotsquared.core.configuration.caption.Caption;
023import com.plotsquared.core.location.Location;
024import com.plotsquared.core.player.PlotPlayer;
025import com.plotsquared.core.plot.Plot;
026import com.plotsquared.core.util.task.RunnableVal;
027import com.sk89q.jnbt.CompoundTag;
028import com.sk89q.jnbt.IntTag;
029import com.sk89q.jnbt.NBTInputStream;
030import com.sk89q.jnbt.NBTOutputStream;
031import com.sk89q.jnbt.Tag;
032import com.sk89q.worldedit.math.BlockVector2;
033import com.sk89q.worldedit.math.BlockVector3;
034import com.sk89q.worldedit.regions.CuboidRegion;
035import com.sk89q.worldedit.world.World;
036import com.sk89q.worldedit.world.biome.BiomeType;
037import com.sk89q.worldedit.world.block.BlockState;
038import com.sk89q.worldedit.world.block.BlockType;
039import com.sk89q.worldedit.world.entity.EntityType;
040import net.kyori.adventure.text.minimessage.Template;
041import org.checkerframework.checker.index.qual.NonNegative;
042import org.checkerframework.checker.nullness.qual.NonNull;
043import org.checkerframework.checker.nullness.qual.Nullable;
044
045import java.io.ByteArrayOutputStream;
046import java.io.File;
047import java.io.FileInputStream;
048import java.io.IOException;
049import java.io.OutputStream;
050import java.net.URL;
051import java.util.Collection;
052import java.util.HashMap;
053import java.util.HashSet;
054import java.util.Map;
055import java.util.Set;
056import java.util.UUID;
057import java.util.function.Consumer;
058import java.util.function.IntConsumer;
059import java.util.zip.GZIPInputStream;
060import java.util.zip.GZIPOutputStream;
061import java.util.zip.ZipEntry;
062import java.util.zip.ZipOutputStream;
063
064public abstract class WorldUtil {
065
066    /**
067     * Set the biome in a region
068     *
069     * @param world World name
070     * @param p1x   Min X
071     * @param p1z   Min Z
072     * @param p2x   Max X
073     * @param p2z   Max Z
074     * @param biome Biome
075     * @deprecated use {@link WorldUtil#setBiome(String, CuboidRegion, BiomeType)}
076     */
077    @Deprecated(forRemoval = true)
078    public static void setBiome(String world, int p1x, int p1z, int p2x, int p2z, BiomeType biome) {
079        World weWorld = PlotSquared.platform().worldUtil().getWeWorld(world);
080        BlockVector3 pos1 = BlockVector2.at(p1x, p1z).toBlockVector3(weWorld.getMinY());
081        BlockVector3 pos2 = BlockVector2.at(p2x, p2z).toBlockVector3(weWorld.getMaxY());
082        CuboidRegion region = new CuboidRegion(pos1, pos2);
083        PlotSquared.platform().worldUtil().setBiomes(world, region, biome);
084    }
085
086    /**
087     * Set the biome in a region
088     *
089     * @param world  World name
090     * @param region Region
091     * @param biome  Biome
092     * @since 6.6.0
093     */
094    public static void setBiome(String world, final CuboidRegion region, BiomeType biome) {
095        PlotSquared.platform().worldUtil().setBiomes(world, region, biome);
096    }
097
098    /**
099     * Check if a given world name corresponds to a real world
100     *
101     * @param worldName World name
102     * @return {@code true} if there exists a world with the given world name,
103     *         {@code false} if not
104     */
105    public abstract boolean isWorld(@NonNull String worldName);
106
107    /**
108     * @param location Sign location
109     * @return Sign content (or an empty string array if the block is not a sign)
110     * @deprecated May result in synchronous chunk loading
111     */
112    @Deprecated
113    public @NonNull
114    abstract String[] getSignSynchronous(@NonNull Location location);
115
116    /**
117     * Get the world spawn location
118     *
119     * @param world World name
120     * @return World spawn location
121     */
122    public @NonNull
123    abstract Location getSpawn(@NonNull String world);
124
125    /**
126     * Set the world spawn location
127     *
128     * @param location New spawn
129     */
130    public abstract void setSpawn(@NonNull Location location);
131
132    /**
133     * Save a world
134     *
135     * @param world World name
136     */
137    public abstract void saveWorld(@NonNull String world);
138
139    /**
140     * Get a string comparison with the closets block state matching a given string
141     *
142     * @param name Block name
143     * @return Comparison result containing the closets matching block
144     */
145    public @NonNull
146    abstract StringComparison<BlockState>.ComparisonResult getClosestBlock(@NonNull String name);
147
148    /**
149     * Set the block at the specified location to a sign, with given text
150     *
151     * @param location     Block location
152     * @param lines        Sign text
153     * @param replacements Text replacements
154     */
155    public abstract void setSign(
156            @NonNull Location location,
157            @NonNull Caption[] lines,
158            @NonNull Template... replacements
159    );
160
161    /**
162     * Get the biome in a given chunk, asynchronously
163     *
164     * @param world  World
165     * @param x      Chunk X coordinate
166     * @param z      Chunk Z coordinate
167     * @param result Result consumer
168     */
169    public abstract void getBiome(@NonNull String world, int x, int z, @NonNull Consumer<BiomeType> result);
170
171    /**
172     * Get the biome in a given chunk, asynchronously
173     *
174     * @param world World
175     * @param x     Chunk X coordinate
176     * @param z     Chunk Z coordinate
177     * @return Biome
178     * @deprecated Use {@link #getBiome(String, int, int, Consumer)}
179     */
180    @Deprecated
181    public @NonNull
182    abstract BiomeType getBiomeSynchronous(@NonNull String world, int x, int z);
183
184    /**
185     * Get the block at a given location (asynchronously)
186     *
187     * @param location Block location
188     * @param result   Result consumer
189     */
190    public abstract void getBlock(@NonNull Location location, @NonNull Consumer<BlockState> result);
191
192    /**
193     * Get the block at a given location (synchronously)
194     *
195     * @param location Block location
196     * @return Result
197     * @deprecated Use {@link #getBlock(Location, Consumer)}
198     */
199    @Deprecated
200    public @NonNull
201    abstract BlockState getBlockSynchronous(@NonNull Location location);
202
203    /**
204     * Get the Y coordinate of the highest non-air block in the world, asynchronously
205     *
206     * @param world  World name
207     * @param x      X coordinate
208     * @param z      Z coordinate
209     * @param result Result consumer
210     */
211    public abstract void getHighestBlock(@NonNull String world, int x, int z, @NonNull IntConsumer result);
212
213    /**
214     * Get the Y coordinate of the highest non-air block in the world, synchronously
215     *
216     * @param world World name
217     * @param x     X coordinate
218     * @param z     Z coordinate
219     * @return Result
220     * @deprecated Use {@link #getHighestBlock(String, int, int, IntConsumer)}
221     */
222    @Deprecated
223    @NonNegative
224    public abstract int getHighestBlockSynchronous(@NonNull String world, int x, int z);
225
226    /**
227     * Set the biome in a region
228     *
229     * @param worldName World name
230     * @param region    Region
231     * @param biome     New biome
232     */
233    public void setBiomes(@NonNull String worldName, @NonNull CuboidRegion region, @NonNull BiomeType biome) {
234        final World world = getWeWorld(worldName);
235        region.forEach(bv -> world.setBiome(bv, biome));
236    }
237
238    /**
239     * Get the WorldEdit {@link com.sk89q.worldedit.world.World} corresponding to a world name
240     *
241     * @param world World name
242     * @return World object
243     */
244    public abstract com.sk89q.worldedit.world.@NonNull World getWeWorld(@NonNull String world);
245
246    /**
247     * Refresh (resend) chunk to player. Usually after setting the biome
248     *
249     * @param x     Chunk x location
250     * @param z     Chunk z location
251     * @param world World of the chunk
252     */
253    public abstract void refreshChunk(int x, int z, String world);
254
255    /**
256     * The legacy web interface is deprecated for removal in favor of Arkitektonika.
257     */
258    @Deprecated(forRemoval = true, since = "6.11.0")
259    public void upload(
260            final @NonNull Plot plot,
261            final @Nullable UUID uuid,
262            final @Nullable String file,
263            final @NonNull RunnableVal<URL> whenDone
264    ) {
265        plot.getHome(home -> SchematicHandler.upload(uuid, file, "zip", new RunnableVal<>() {
266            @Override
267            public void run(OutputStream output) {
268                try (final ZipOutputStream zos = new ZipOutputStream(output)) {
269                    File dat = getDat(plot.getWorldName());
270                    Location spawn = getSpawn(plot.getWorldName());
271                    if (dat != null) {
272                        ZipEntry ze = new ZipEntry("world" + File.separator + dat.getName());
273                        zos.putNextEntry(ze);
274                        try (NBTInputStream nis = new NBTInputStream(new GZIPInputStream(new FileInputStream(dat)))) {
275                            Map<String, Tag> tag = ((CompoundTag) nis.readNamedTag().getTag()).getValue();
276                            Map<String, Tag> newMap = new HashMap<>();
277                            for (Map.Entry<String, Tag> entry : tag.entrySet()) {
278                                if (!entry.getKey().equals("Data")) {
279                                    newMap.put(entry.getKey(), entry.getValue());
280                                    continue;
281                                }
282                                Map<String, Tag> data = new HashMap<>(((CompoundTag) entry.getValue()).getValue());
283                                data.put("SpawnX", new IntTag(home.getX()));
284                                data.put("SpawnY", new IntTag(home.getY()));
285                                data.put("SpawnZ", new IntTag(home.getZ()));
286                                newMap.put("Data", new CompoundTag(data));
287                            }
288                            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
289                                try (NBTOutputStream out = new NBTOutputStream(new GZIPOutputStream(baos, true))) {
290                                    //TODO Find what this should be called
291                                    out.writeNamedTag("Schematic????", new CompoundTag(newMap));
292                                }
293                                zos.write(baos.toByteArray());
294                            }
295                        }
296                    }
297                    setSpawn(spawn);
298                    byte[] buffer = new byte[1024];
299                    Set<BlockVector2> added = new HashSet<>();
300                    for (Plot current : plot.getConnectedPlots()) {
301                        Location bot = current.getBottomAbs();
302                        Location top = current.getTopAbs();
303                        int brx = bot.getX() >> 9;
304                        int brz = bot.getZ() >> 9;
305                        int trx = top.getX() >> 9;
306                        int trz = top.getZ() >> 9;
307                        Set<BlockVector2> files = getChunkChunks(bot.getWorldName());
308                        for (BlockVector2 mca : files) {
309                            if (mca.getX() >= brx && mca.getX() <= trx && mca.getZ() >= brz && mca.getZ() <= trz && !added.contains(mca)) {
310                                final File file = getMcr(plot.getWorldName(), mca.getX(), mca.getZ());
311                                if (file != null) {
312                                    //final String name = "r." + (x - cx) + "." + (z - cz) + ".mca";
313                                    String name = file.getName();
314                                    final ZipEntry ze = new ZipEntry("world" + File.separator + "region" + File.separator + name);
315                                    zos.putNextEntry(ze);
316                                    added.add(mca);
317                                    try (FileInputStream in = new FileInputStream(file)) {
318                                        int len;
319                                        while ((len = in.read(buffer)) > 0) {
320                                            zos.write(buffer, 0, len);
321                                        }
322                                    }
323                                    zos.closeEntry();
324                                }
325                            }
326                        }
327                    }
328                    zos.closeEntry();
329                    zos.flush();
330                    zos.finish();
331                } catch (IOException e) {
332                    e.printStackTrace();
333                }
334            }
335        }, whenDone));
336    }
337
338    final @Nullable File getDat(final @NonNull String world) {
339        File file = new File(PlotSquared.platform().worldContainer() + File.separator + world + File.separator + "level.dat");
340        if (file.exists()) {
341            return file;
342        }
343        return null;
344    }
345
346    @Nullable
347    private File getMcr(final @NonNull String world, final int x, final int z) {
348        final File file =
349                new File(
350                        PlotSquared.platform().worldContainer(),
351                        world + File.separator + "region" + File.separator + "r." + x + '.' + z + ".mca"
352                );
353        if (file.exists()) {
354            return file;
355        }
356        return null;
357    }
358
359
360    public Set<BlockVector2> getChunkChunks(String world) {
361        File folder = new File(PlotSquared.platform().worldContainer(), world + File.separator + "region");
362        File[] regionFiles = folder.listFiles();
363        if (regionFiles == null) {
364            throw new RuntimeException("Could not find worlds folder: " + folder + " ? (no read access?)");
365        }
366        HashSet<BlockVector2> chunks = new HashSet<>();
367        for (File file : regionFiles) {
368            String name = file.getName();
369            if (name.endsWith("mca")) {
370                String[] split = name.split("\\.");
371                try {
372                    int x = Integer.parseInt(split[1]);
373                    int z = Integer.parseInt(split[2]);
374                    BlockVector2 loc = BlockVector2.at(x, z);
375                    chunks.add(loc);
376                } catch (NumberFormatException ignored) {
377                }
378            }
379        }
380        return chunks;
381    }
382
383    /**
384     * Check if two blocks are the same type)
385     *
386     * @param block1 First block
387     * @param block2 Second block
388     * @return {@code true} if the blocks have the same type, {@code false} if not
389     */
390    public abstract boolean isBlockSame(@NonNull BlockState block1, @NonNull BlockState block2);
391
392    /**
393     * Get the player health
394     *
395     * @param player Player
396     * @return Non-negative health
397     */
398    @NonNegative
399    public abstract double getHealth(@NonNull PlotPlayer<?> player);
400
401    /**
402     * Set the player health
403     *
404     * @param player Player health
405     * @param health Non-negative health
406     */
407    public abstract void setHealth(@NonNull PlotPlayer<?> player, @NonNegative double health);
408
409    /**
410     * Get the player food level
411     *
412     * @param player Player
413     * @return Non-negative food level
414     */
415    @NonNegative
416    public abstract int getFoodLevel(@NonNull PlotPlayer<?> player);
417
418    /**
419     * Set the player food level
420     *
421     * @param player    Player food level
422     * @param foodLevel Non-negative food level
423     */
424    public abstract void setFoodLevel(@NonNull PlotPlayer<?> player, @NonNegative int foodLevel);
425
426    /**
427     * Get all entity types belonging to an entity category
428     *
429     * @param category Entity category
430     * @return Set containing all entities belonging to the given category
431     */
432    public @NonNull
433    abstract Set<EntityType> getTypesInCategory(@NonNull String category);
434
435    /**
436     * Get all recognized tile entity types
437     *
438     * @return Collection containing all known tile entity types
439     */
440    public @NonNull
441    abstract Collection<BlockType> getTileEntityTypes();
442
443    /**
444     * Get the tile entity count in a chunk
445     *
446     * @param world World
447     * @param chunk Chunk coordinates
448     * @return Tile entity count
449     */
450    @NonNegative
451    public abstract int getTileEntityCount(@NonNull String world, @NonNull BlockVector2 chunk);
452
453}