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.location.World; 025import com.plotsquared.core.player.PlotPlayer; 026import com.plotsquared.core.plot.Plot; 027import com.plotsquared.core.util.task.RunnableVal; 028import com.sk89q.jnbt.CompoundTag; 029import com.sk89q.jnbt.CompoundTagBuilder; 030import com.sk89q.jnbt.NBTInputStream; 031import com.sk89q.jnbt.NBTOutputStream; 032import com.sk89q.jnbt.Tag; 033import com.sk89q.worldedit.math.BlockVector2; 034import com.sk89q.worldedit.regions.CuboidRegion; 035import com.sk89q.worldedit.world.biome.BiomeType; 036import com.sk89q.worldedit.world.block.BlockState; 037import com.sk89q.worldedit.world.block.BlockType; 038import com.sk89q.worldedit.world.entity.EntityType; 039import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; 040import org.apache.logging.log4j.LogManager; 041import org.apache.logging.log4j.Logger; 042import org.checkerframework.checker.index.qual.NonNegative; 043import org.checkerframework.checker.nullness.qual.NonNull; 044import org.checkerframework.checker.nullness.qual.Nullable; 045 046import java.io.IOException; 047import java.io.InputStream; 048import java.io.OutputStream; 049import java.net.URL; 050import java.nio.file.Files; 051import java.nio.file.Path; 052import java.nio.file.attribute.BasicFileAttributes; 053import java.util.Collection; 054import java.util.HashSet; 055import java.util.Map; 056import java.util.Objects; 057import java.util.Set; 058import java.util.UUID; 059import java.util.function.Consumer; 060import java.util.function.IntConsumer; 061import java.util.function.Predicate; 062import java.util.stream.Collectors; 063import java.util.stream.Stream; 064import java.util.zip.GZIPInputStream; 065import java.util.zip.GZIPOutputStream; 066import java.util.zip.ZipEntry; 067import java.util.zip.ZipOutputStream; 068 069public abstract class WorldUtil { 070 071 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + WorldUtil.class.getSimpleName()); 072 private static final boolean MODERN_LEVEL_FORMAT = 073 MinecraftVersion.current().isNewerOrEqualThan(MinecraftVersion.TINY_TAKEOVER); 074 075 /** 076 * {@return whether the given location is valid in the world} 077 * @param location the location to check 078 * @since 7.3.6 079 */ 080 public static boolean isValidLocation(Location location) { 081 return Math.abs(location.getX()) < 30000000 && Math.abs(location.getZ()) < 30000000; 082 } 083 084 /** 085 * Set the biome in a region 086 * 087 * @param world World name 088 * @param region Region 089 * @param biome Biome 090 * @since 6.6.0 091 */ 092 public static void setBiome(String world, final CuboidRegion region, BiomeType biome) { 093 PlotSquared.platform().worldUtil().setBiomes(world, region, biome); 094 } 095 096 /** 097 * Check if the server is using the modern world storage format introduced in 26.1. 098 * 099 * @return if the server is using the modern server level structure 100 * @since 7.6.0 101 */ 102 public static boolean isModernServerLevelStructure() { 103 return MODERN_LEVEL_FORMAT; 104 } 105 106 /** 107 * Check if a given world name corresponds to a real world 108 * 109 * @param worldName World name 110 * @return {@code true} if there exists a world with the given world name, 111 * {@code false} if not 112 */ 113 public abstract boolean isWorld(@NonNull String worldName); 114 115 /** 116 * @param location Sign location 117 * @return Sign content (or an empty string array if the block is not a sign) 118 * @deprecated May result in synchronous chunk loading 119 */ 120 @Deprecated 121 public @NonNull 122 abstract String[] getSignSynchronous(@NonNull Location location); 123 124 /** 125 * Get the world spawn location 126 * 127 * @param world World name 128 * @return World spawn location 129 */ 130 public @NonNull 131 abstract Location getSpawn(@NonNull String world); 132 133 /** 134 * Set the world spawn location 135 * 136 * @param location New spawn 137 */ 138 public abstract void setSpawn(@NonNull Location location); 139 140 /** 141 * Save a world 142 * 143 * @param world World name 144 */ 145 public abstract void saveWorld(@NonNull String world); 146 147 /** 148 * Get a string comparison with the closets block state matching a given string 149 * 150 * @param name Block name 151 * @return Comparison result containing the closets matching block 152 */ 153 public @NonNull 154 abstract StringComparison<BlockState>.ComparisonResult getClosestBlock(@NonNull String name); 155 156 /** 157 * Set the block at the specified location to a sign, with given text 158 * 159 * @param location Block location 160 * @param lines Sign text 161 * @param replacements Text replacements 162 */ 163 public abstract void setSign( 164 @NonNull Location location, 165 @NonNull Caption[] lines, 166 @NonNull TagResolver... replacements 167 ); 168 169 /** 170 * Get the biome in a given chunk, asynchronously 171 * 172 * @param world World 173 * @param x Chunk X coordinate 174 * @param z Chunk Z coordinate 175 * @param result Result consumer 176 */ 177 public abstract void getBiome(@NonNull String world, int x, int z, @NonNull Consumer<BiomeType> result); 178 179 /** 180 * Get the biome in a given chunk, asynchronously 181 * 182 * @param world World 183 * @param x Chunk X coordinate 184 * @param z Chunk Z coordinate 185 * @return Biome 186 * @deprecated Use {@link #getBiome(String, int, int, Consumer)} 187 */ 188 @Deprecated 189 public @NonNull 190 abstract BiomeType getBiomeSynchronous(@NonNull String world, int x, int z); 191 192 /** 193 * Get the block at a given location (asynchronously) 194 * 195 * @param location Block location 196 * @param result Result consumer 197 */ 198 public abstract void getBlock(@NonNull Location location, @NonNull Consumer<BlockState> result); 199 200 /** 201 * Checks if the block smaller as a slab 202 * @param location Block location 203 * @return true if it smaller as a slab 204 */ 205 public abstract boolean isSmallBlock(@NonNull Location location); 206 207 /** 208 * Get the block at a given location (synchronously) 209 * 210 * @param location Block location 211 * @return Result 212 * @deprecated Use {@link #getBlock(Location, Consumer)} 213 */ 214 @Deprecated 215 public @NonNull 216 abstract BlockState getBlockSynchronous(@NonNull Location location); 217 218 /** 219 * Get the Y coordinate of the highest non-air block in the world, asynchronously 220 * 221 * @param world World name 222 * @param x X coordinate 223 * @param z Z coordinate 224 * @param result Result consumer 225 */ 226 public abstract void getHighestBlock(@NonNull String world, int x, int z, @NonNull IntConsumer result); 227 228 /** 229 * Get the Y coordinate of the highest non-air block in the world, synchronously 230 * 231 * @param world World name 232 * @param x X coordinate 233 * @param z Z coordinate 234 * @return Result 235 * @deprecated Use {@link #getHighestBlock(String, int, int, IntConsumer)} 236 */ 237 @Deprecated 238 @NonNegative 239 public abstract int getHighestBlockSynchronous(@NonNull String world, int x, int z); 240 241 /** 242 * Set the biome in a region 243 * 244 * @param worldName World name 245 * @param region Region 246 * @param biome New biome 247 */ 248 public void setBiomes(@NonNull String worldName, @NonNull CuboidRegion region, @NonNull BiomeType biome) { 249 final com.sk89q.worldedit.world.World world = getWeWorld(worldName); 250 region.forEach(bv -> world.setBiome(bv, biome)); 251 } 252 253 /** 254 * Get the WorldEdit {@link com.sk89q.worldedit.world.World} corresponding to a world name 255 * 256 * @param world World name 257 * @return World object 258 */ 259 public abstract com.sk89q.worldedit.world.@NonNull World getWeWorld(@NonNull String world); 260 261 /** 262 * Refresh (resend) chunk to player. Usually after setting the biome 263 * 264 * @param x Chunk x location 265 * @param z Chunk z location 266 * @param world World of the chunk 267 */ 268 public abstract void refreshChunk(int x, int z, String world); 269 270 /** 271 * The legacy web interface is deprecated for removal in favor of Arkitektonika. 272 */ 273 @Deprecated(forRemoval = true, since = "6.11.0") 274 public void upload( 275 final @NonNull Plot plot, 276 final @Nullable UUID uuid, 277 final @Nullable String file, 278 final @NonNull RunnableVal<URL> whenDone 279 ) { 280 World<?> world = PlotSquared.platform().getPlatformWorld(plot.getWorldName()); 281 String relativeMcaRoot = MODERN_LEVEL_FORMAT ? "dimensions/minecraft/overworld/region" : "region"; 282 plot.getHome(home -> SchematicHandler.upload(uuid, file, "zip", new RunnableVal<>() { 283 @Override 284 public void run(OutputStream output) { 285 try (final ZipOutputStream zos = new ZipOutputStream(output)) { 286 Path dat = getLevelData(world); 287 Location spawn = getSpawn(plot.getWorldName()); 288 if (dat != null) { 289 ZipEntry ze = new ZipEntry("level.dat"); 290 zos.putNextEntry(ze); 291 try (NBTInputStream nis = new NBTInputStream(new GZIPInputStream(Files.newInputStream(dat)))) { 292 CompoundTag levelData = modifyLevelData((CompoundTag) nis.readNamedTag().getTag(), home); 293 try (NBTOutputStream out = 294 new NBTOutputStream(new GZIPOutputStream(new CloseShieldOutputStream(zos), true))) { 295 out.writeNamedTag("", levelData); 296 } 297 } 298 zos.closeEntry(); 299 } 300 setSpawn(spawn); 301 Set<BlockVector2> added = new HashSet<>(); 302 for (Plot current : plot.getConnectedPlots()) { 303 Location bot = current.getBottomAbs(); 304 Location top = current.getTopAbs(); 305 int brx = bot.getX() >> 9; 306 int brz = bot.getZ() >> 9; 307 int trx = top.getX() >> 9; 308 int trz = top.getZ() >> 9; 309 Set<BlockVector2> files = getChunkChunks(world); 310 for (BlockVector2 mca : files) { 311 if (mca.getX() >= brx && mca.getX() <= trx && mca.getZ() >= brz && mca.getZ() <= trz && !added.contains( 312 mca)) { 313 final Path path = getMca(world, mca.getX(), mca.getZ()); 314 if (path != null) { 315 final ZipEntry ze = new ZipEntry(relativeMcaRoot + "/" + path.getFileName().toString()); 316 zos.putNextEntry(ze); 317 added.add(mca); 318 try (InputStream in = Files.newInputStream(path)) { 319 in.transferTo(zos); 320 } 321 zos.closeEntry(); 322 } 323 } 324 } 325 } 326 zos.closeEntry(); 327 zos.flush(); 328 zos.finish(); 329 } catch (IOException e) { 330 e.printStackTrace(); 331 } 332 } 333 }, whenDone)); 334 } 335 336 private @Nullable Path getLevelData(final @NonNull World<?> world) { 337 Path path = world.getWorldFolder().resolve("level.dat"); 338 return Files.exists(path) ? path : null; 339 } 340 341 @Nullable 342 private Path getMca(final @NonNull World<?> world, final int x, final int z) { 343 Path path = getRegionFolder(world).resolve(String.format("r.%s.%s.mca", x, z)); 344 return Files.exists(path) ? path : null; 345 } 346 347 private CompoundTag modifyLevelData(CompoundTag input, Location home) { 348 Map<String, Tag> root = input.getValue(); 349 if (!(root.get("Data") instanceof CompoundTag data)) { 350 return input; 351 } 352 CompoundTagBuilder dataBuilder = data.createBuilder(); 353 if (MODERN_LEVEL_FORMAT) { 354 if (data.getValue().get("spawn") instanceof CompoundTag spawn) { 355 dataBuilder.put( 356 "spawn", spawn.createBuilder() 357 .putString("dimension", "minecraft:overworld") 358 .putIntArray("pos", new int[]{home.getX(), home.getY(), home.getZ()}) 359 .build() 360 ); 361 } 362 } else { 363 // legacy 364 dataBuilder 365 .putInt("SpawnX", home.getX()) 366 .putInt("SpawnY", home.getY()) 367 .putInt("SpawnZ", home.getZ()); 368 } 369 return input.createBuilder().put("Data", dataBuilder.build()).build(); 370 } 371 372 373 public Set<BlockVector2> getChunkChunks(World<?> world) { 374 Path regionRoot = getRegionFolder(world); 375 if (!Files.exists(regionRoot)) { 376 throw new RuntimeException("Could not find regions folder: " + regionRoot + " ? (no read access?)"); 377 } 378 try (Stream<Path> stream = Files.find(regionRoot, 1, WorldUtil::isMcaRegionFile)) { 379 return stream.filter(Predicate.not(p -> p.equals(regionRoot))) // skip root 380 .map(Path::getFileName) 381 .map(this::fromMcaFileName) 382 .filter(Objects::nonNull) 383 .collect(Collectors.toSet()); 384 } catch (IOException e) { 385 LOGGER.error("Failed to traverse region directory", e); 386 return Set.of(); 387 } 388 } 389 390 private static Path getRegionFolder(World<?> world) { 391 return world.getWorldFolder().resolve("region"); 392 } 393 394 /** 395 * Checks if the given file, by its path and BasicFileAttributes, is a mca region file. 396 * 397 * @param path full path to file 398 * @param bfa attributes of the given file 399 * @return {@code true} if the given file is a seemingly valid mca region file. {@code false} otherwise 400 */ 401 private static boolean isMcaRegionFile(Path path, BasicFileAttributes bfa) { 402 if (bfa.isDirectory()) { 403 return false; 404 } 405 String name = path.getFileName().toString(); 406 return name.startsWith("r.") && name.endsWith(".mca"); 407 } 408 409 /** 410 * Retrieves the coordinates from a region mca file. 411 * 412 * @param filename the filename part of the full path ({@link Path#getFileName()}) 413 * @return A BV2 containg the coordinates, or {@code null} if the filename does not match the expected format 414 */ 415 private BlockVector2 fromMcaFileName(Path filename) { 416 String[] parts = filename.toString().split("\\."); 417 if (parts.length < 3) { 418 return null; 419 } 420 try { 421 return BlockVector2.at(Integer.parseInt(parts[1]), Integer.parseInt(parts[2])); 422 } catch (NumberFormatException e) { 423 return null; 424 } 425 } 426 427 /** 428 * Check if two blocks are the same type) 429 * 430 * @param block1 First block 431 * @param block2 Second block 432 * @return {@code true} if the blocks have the same type, {@code false} if not 433 */ 434 public abstract boolean isBlockSame(@NonNull BlockState block1, @NonNull BlockState block2); 435 436 /** 437 * Get the player health 438 * 439 * @param player Player 440 * @return Non-negative health 441 */ 442 @NonNegative 443 public abstract double getHealth(@NonNull PlotPlayer<?> player); 444 445 /** 446 * Set the player health 447 * 448 * @param player Player health 449 * @param health Non-negative health 450 */ 451 public abstract void setHealth(@NonNull PlotPlayer<?> player, @NonNegative double health); 452 453 /** 454 * Get the player food level 455 * 456 * @param player Player 457 * @return Non-negative food level 458 */ 459 @NonNegative 460 public abstract int getFoodLevel(@NonNull PlotPlayer<?> player); 461 462 /** 463 * Set the player food level 464 * 465 * @param player Player food level 466 * @param foodLevel Non-negative food level 467 */ 468 public abstract void setFoodLevel(@NonNull PlotPlayer<?> player, @NonNegative int foodLevel); 469 470 /** 471 * Get all entity types belonging to an entity category 472 * 473 * @param category Entity category 474 * @return Set containing all entities belonging to the given category 475 */ 476 public @NonNull 477 abstract Set<EntityType> getTypesInCategory(@NonNull String category); 478 479 /** 480 * Get all recognized tile entity types 481 * 482 * @return Collection containing all known tile entity types 483 */ 484 public @NonNull 485 abstract Collection<BlockType> getTileEntityTypes(); 486 487 /** 488 * Get the tile entity count in a chunk 489 * 490 * @param world World 491 * @param chunk Chunk coordinates 492 * @return Tile entity count 493 */ 494 @NonNegative 495 public abstract int getTileEntityCount(@NonNull String world, @NonNull BlockVector2 chunk); 496 497}