mirror of
https://github.com/PaperMC/Paper.git
synced 2024-09-11 22:48:21 +02:00
Upstream has released updates that appear to apply and compile correctly. This update has not been tested by PaperMC and as with ANY update, please do your own testing Bukkit Changes: 7902647a PR-737: Update WorldCreator#generatorSettings docs 67556a50 PR-736: Update README CraftBukkit Changes: 10922194 Java 18 support d53c4fb6 PR-1039: Use correct ops in GeneratorSettings a567e4ae PR-1038: Removed the no longer needed getChunkUnchecked method from ChunkProviderServer. 4ac8fcce SPIGOT-6980: Since 1.18.2, World#isChunkLoaded returned false for chunks that have just been loaded (e.g. inside ChunkLoadEvent). e6cc7c70 PR-1035: Update README 3ec79a27 SPIGOT-5140: Call EntityChangeBlockEvent when a ChorusFlower is destroyed by a projectile
431 lines
19 KiB
Diff
431 lines
19 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
|
Date: Sat, 1 Jun 2019 13:00:55 -0700
|
|
Subject: [PATCH] Chunk debug command
|
|
|
|
Prints all chunk information to a text file into the debug
|
|
folder in the root server folder. The format is in JSON, and
|
|
the data format is described in MCUtil#dumpChunks(File)
|
|
|
|
The command will output server version and all online players to the
|
|
file as well. We do not log anything but the location, world and
|
|
username of the player.
|
|
|
|
Also logs the value of these config values (note not all are paper's):
|
|
- keep spawn loaded value
|
|
- spawn radius
|
|
- view distance
|
|
|
|
Each chunk has the following logged:
|
|
- Coordinate
|
|
- Ticket level & its corresponding state
|
|
- Whether it is queued for unload
|
|
- Chunk status (may be unloaded)
|
|
- All tickets on the chunk
|
|
|
|
Example log:
|
|
https://gist.githubusercontent.com/Spottedleaf/0131e7710ffd5d531e5fd246c3367380/raw/169ae1b2e240485f99bc7a6bd8e78d90e3af7397/chunks-2019-06-01_19.57.05.txt
|
|
|
|
For references on certain keywords (ticket, status, etc), please see:
|
|
|
|
https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528273&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-528273
|
|
https://bugs.mojang.com/browse/MC-141484?focusedCommentId=528577&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-528577
|
|
|
|
diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
|
|
index 1eb45df9dca5d0c31ac46709e706136a246cb8ea..53e290aa5c90b241f8d817b4eede08da140197cd 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
|
|
@@ -6,13 +6,15 @@ import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Maps;
|
|
-import net.minecraft.resources.ResourceLocation;
|
|
import net.minecraft.server.MinecraftServer;
|
|
+import net.minecraft.server.level.ChunkHolder;
|
|
import net.minecraft.server.level.ServerChunkCache;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.entity.EntityType;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
+import net.minecraft.resources.ResourceLocation;
|
|
+import net.minecraft.server.MCUtil;
|
|
import org.apache.commons.lang3.tuple.MutablePair;
|
|
import org.apache.commons.lang3.tuple.Pair;
|
|
import org.bukkit.Bukkit;
|
|
@@ -41,7 +43,7 @@ import java.util.stream.Collectors;
|
|
|
|
public class PaperCommand extends Command {
|
|
private static final String BASE_PERM = "bukkit.command.paper.";
|
|
- private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version").build();
|
|
+ private static final ImmutableSet<String> SUBCOMMANDS = ImmutableSet.<String>builder().add("heap", "entity", "reload", "version", "debug", "chunkinfo").build();
|
|
|
|
public PaperCommand(String name) {
|
|
super(name);
|
|
@@ -69,6 +71,21 @@ public class PaperCommand extends Command {
|
|
if (args.length == 3)
|
|
return getListMatchingLast(sender, args, EntityType.getEntityNameList().stream().map(ResourceLocation::toString).sorted().toArray(String[]::new));
|
|
break;
|
|
+ case "debug":
|
|
+ if (args.length == 2) {
|
|
+ return getListMatchingLast(sender, args, "help", "chunks");
|
|
+ }
|
|
+ break;
|
|
+ case "chunkinfo":
|
|
+ List<String> worldNames = new ArrayList<>();
|
|
+ worldNames.add("*");
|
|
+ for (org.bukkit.World world : Bukkit.getWorlds()) {
|
|
+ worldNames.add(world.getName());
|
|
+ }
|
|
+ if (args.length == 2) {
|
|
+ return getListMatchingLast(sender, args, worldNames);
|
|
+ }
|
|
+ break;
|
|
}
|
|
return Collections.emptyList();
|
|
}
|
|
@@ -135,6 +152,12 @@ public class PaperCommand extends Command {
|
|
case "reload":
|
|
doReload(sender);
|
|
break;
|
|
+ case "debug":
|
|
+ doDebug(sender, args);
|
|
+ break;
|
|
+ case "chunkinfo":
|
|
+ doChunkInfo(sender, args);
|
|
+ break;
|
|
case "ver":
|
|
if (!testPermission(sender, "version")) break; // "ver" needs a special check because it's an alias. All other commands are checked up before the switch statement (because they are present in the SUBCOMMANDS set)
|
|
case "version":
|
|
@@ -152,6 +175,114 @@ public class PaperCommand extends Command {
|
|
return true;
|
|
}
|
|
|
|
+ private void doChunkInfo(CommandSender sender, String[] args) {
|
|
+ List<org.bukkit.World> worlds;
|
|
+ if (args.length < 2 || args[1].equals("*")) {
|
|
+ worlds = Bukkit.getWorlds();
|
|
+ } else {
|
|
+ worlds = new ArrayList<>(args.length - 1);
|
|
+ for (int i = 1; i < args.length; ++i) {
|
|
+ org.bukkit.World world = Bukkit.getWorld(args[i]);
|
|
+ if (world == null) {
|
|
+ sender.sendMessage(ChatColor.RED + "World '" + args[i] + "' is invalid");
|
|
+ return;
|
|
+ }
|
|
+ worlds.add(world);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ int accumulatedTotal = 0;
|
|
+ int accumulatedInactive = 0;
|
|
+ int accumulatedBorder = 0;
|
|
+ int accumulatedTicking = 0;
|
|
+ int accumulatedEntityTicking = 0;
|
|
+
|
|
+ for (org.bukkit.World bukkitWorld : worlds) {
|
|
+ ServerLevel world = ((CraftWorld)bukkitWorld).getHandle();
|
|
+
|
|
+ int total = 0;
|
|
+ int inactive = 0;
|
|
+ int border = 0;
|
|
+ int ticking = 0;
|
|
+ int entityTicking = 0;
|
|
+
|
|
+ for (ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) {
|
|
+ if (chunk.getFullChunkNowUnchecked() == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ ++total;
|
|
+
|
|
+ ChunkHolder.FullChunkStatus state = ChunkHolder.getFullChunkStatus(chunk.getTicketLevel());
|
|
+
|
|
+ switch (state) {
|
|
+ case INACCESSIBLE:
|
|
+ ++inactive;
|
|
+ continue;
|
|
+ case BORDER:
|
|
+ ++border;
|
|
+ continue;
|
|
+ case TICKING:
|
|
+ ++ticking;
|
|
+ continue;
|
|
+ case ENTITY_TICKING:
|
|
+ ++entityTicking;
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ accumulatedTotal += total;
|
|
+ accumulatedInactive += inactive;
|
|
+ accumulatedBorder += border;
|
|
+ accumulatedTicking += ticking;
|
|
+ accumulatedEntityTicking += entityTicking;
|
|
+
|
|
+ sender.sendMessage(ChatColor.BLUE + "Chunks in " + ChatColor.GREEN + bukkitWorld.getName() + ChatColor.DARK_AQUA + ":");
|
|
+ sender.sendMessage(ChatColor.BLUE + "Total: " + ChatColor.DARK_AQUA + total + ChatColor.BLUE + " Inactive: " + ChatColor.DARK_AQUA
|
|
+ + inactive + ChatColor.BLUE + " Border: " + ChatColor.DARK_AQUA + border + ChatColor.BLUE + " Ticking: "
|
|
+ + ChatColor.DARK_AQUA + ticking + ChatColor.BLUE + " Entity: " + ChatColor.DARK_AQUA + entityTicking);
|
|
+ }
|
|
+ if (worlds.size() > 1) {
|
|
+ sender.sendMessage(ChatColor.BLUE + "Chunks in " + ChatColor.GREEN + "all listed worlds" + ChatColor.DARK_AQUA + ":");
|
|
+ sender.sendMessage(ChatColor.BLUE + "Total: " + ChatColor.DARK_AQUA + accumulatedTotal + ChatColor.BLUE + " Inactive: " + ChatColor.DARK_AQUA
|
|
+ + accumulatedInactive + ChatColor.BLUE + " Border: " + ChatColor.DARK_AQUA + accumulatedBorder + ChatColor.BLUE + " Ticking: "
|
|
+ + ChatColor.DARK_AQUA + accumulatedTicking + ChatColor.BLUE + " Entity: " + ChatColor.DARK_AQUA + accumulatedEntityTicking);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void doDebug(CommandSender sender, String[] args) {
|
|
+ if (args.length < 2) {
|
|
+ sender.sendMessage(ChatColor.RED + "Use /paper debug [chunks] help for more information on a specific command");
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ String debugType = args[1].toLowerCase(Locale.ENGLISH);
|
|
+ switch (debugType) {
|
|
+ case "chunks":
|
|
+ if (args.length >= 3 && args[2].toLowerCase(Locale.ENGLISH).equals("help")) {
|
|
+ sender.sendMessage(ChatColor.RED + "Use /paper debug chunks to dump loaded chunk information to a file");
|
|
+ break;
|
|
+ }
|
|
+ File file = new File(new File(new File("."), "debug"),
|
|
+ "chunks-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt");
|
|
+ sender.sendMessage(ChatColor.GREEN + "Writing chunk information dump to " + file.toString());
|
|
+ try {
|
|
+ MCUtil.dumpChunks(file);
|
|
+ sender.sendMessage(ChatColor.GREEN + "Successfully written chunk information!");
|
|
+ } catch (Throwable thr) {
|
|
+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr);
|
|
+ sender.sendMessage(ChatColor.RED + "Failed to dump chunk information, see console");
|
|
+ }
|
|
+
|
|
+ break;
|
|
+ case "help":
|
|
+ // fall through to default
|
|
+ default:
|
|
+ sender.sendMessage(ChatColor.RED + "Use /paper debug [chunks] help for more information on a specific command");
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+
|
|
/*
|
|
* Ported from MinecraftForge - author: LexManos <LexManos@gmail.com> - License: LGPLv2.1
|
|
*/
|
|
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
|
|
index 162aa7718488a74980843944e0d026ccfd5a65a5..defeba92b2a7a5d9d5031f2adc0581d87070bb47 100644
|
|
--- a/src/main/java/net/minecraft/server/MCUtil.java
|
|
+++ b/src/main/java/net/minecraft/server/MCUtil.java
|
|
@@ -9,13 +9,27 @@ import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.Direction;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import net.minecraft.network.chat.Component;
|
|
+import net.minecraft.server.level.ChunkHolder;
|
|
+import net.minecraft.server.level.ChunkMap;
|
|
+import net.minecraft.server.level.DistanceManager;
|
|
+import net.minecraft.server.level.ServerChunkCache;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
+import net.minecraft.server.level.ServerPlayer;
|
|
+import net.minecraft.server.level.Ticket;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import net.minecraft.world.level.ClipContext;
|
|
import net.minecraft.world.level.Level;
|
|
+import net.minecraft.world.level.chunk.ChunkAccess;
|
|
+import net.minecraft.world.level.chunk.ChunkStatus;
|
|
import org.apache.commons.lang.exception.ExceptionUtils;
|
|
+import com.google.gson.JsonArray;
|
|
+import com.google.gson.JsonObject;
|
|
+import com.google.gson.internal.Streams;
|
|
+import com.google.gson.stream.JsonWriter;
|
|
import com.mojang.authlib.GameProfile;
|
|
+import com.mojang.datafixers.util.Either;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.block.BlockFace;
|
|
import org.bukkit.craftbukkit.CraftWorld;
|
|
@@ -24,8 +38,11 @@ import org.spigotmc.AsyncCatcher;
|
|
|
|
import javax.annotation.Nonnull;
|
|
import javax.annotation.Nullable;
|
|
+import java.io.*;
|
|
+import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Queue;
|
|
+import java.util.Set;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
@@ -541,6 +558,172 @@ public final class MCUtil {
|
|
return null;
|
|
}
|
|
|
|
+ public static ChunkStatus getChunkStatus(ChunkHolder chunk) {
|
|
+ List<ChunkStatus> statuses = net.minecraft.server.level.ServerChunkCache.CHUNK_STATUSES;
|
|
+ for (int i = statuses.size() - 1; i >= 0; --i) {
|
|
+ ChunkStatus curr = statuses.get(i);
|
|
+ CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = chunk.getFutureIfPresentUnchecked(curr);
|
|
+ if (future != ChunkHolder.UNLOADED_CHUNK_FUTURE) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ return null; // unloaded
|
|
+ }
|
|
+
|
|
+ public static void dumpChunks(File file) throws IOException {
|
|
+ file.getParentFile().mkdirs();
|
|
+ file.createNewFile();
|
|
+ /*
|
|
+ * Json format:
|
|
+ *
|
|
+ * Main data format:
|
|
+ * -server-version:<string>
|
|
+ * -data-version:<int>
|
|
+ * -worlds:
|
|
+ * -name:<world name>
|
|
+ * -view-distance:<int>
|
|
+ * -keep-spawn-loaded:<boolean>
|
|
+ * -keep-spawn-loaded-range:<int>
|
|
+ * -visible-chunk-count:<int>
|
|
+ * -loaded-chunk-count:<int>
|
|
+ * -verified-fully-loaded-chunks:<int>
|
|
+ * -players:<array of player>
|
|
+ * -chunk-data:<array of chunks>
|
|
+ *
|
|
+ * Player format:
|
|
+ * -name:<string>
|
|
+ * -x:<double>
|
|
+ * -y:<double>
|
|
+ * -z:<double>
|
|
+ *
|
|
+ * Chunk Format:
|
|
+ * -x:<integer>
|
|
+ * -z:<integer>
|
|
+ * -ticket-level:<integer>
|
|
+ * -state:<string>
|
|
+ * -queued-for-unload:<boolean>
|
|
+ * -status:<string>
|
|
+ * -tickets:<array of tickets>
|
|
+ *
|
|
+ *
|
|
+ * Ticket format:
|
|
+ * -ticket-type:<string>
|
|
+ * -ticket-level:<int>
|
|
+ * -add-tick:<long>
|
|
+ * -object-reason:<string> // This depends on the type of ticket. ie POST_TELEPORT -> entity id
|
|
+ */
|
|
+ List<org.bukkit.World> worlds = org.bukkit.Bukkit.getWorlds();
|
|
+ JsonObject data = new JsonObject();
|
|
+
|
|
+ data.addProperty("server-version", org.bukkit.Bukkit.getVersion());
|
|
+ data.addProperty("data-version", 0);
|
|
+
|
|
+ JsonArray worldsData = new JsonArray();
|
|
+
|
|
+ for (org.bukkit.World bukkitWorld : worlds) {
|
|
+ JsonObject worldData = new JsonObject();
|
|
+
|
|
+ ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
|
|
+ ChunkMap chunkMap = world.getChunkSource().chunkMap;
|
|
+ Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = chunkMap.visibleChunkMap;
|
|
+ DistanceManager chunkMapDistance = chunkMap.distanceManager;
|
|
+ List<ChunkHolder> allChunks = new ArrayList<>(visibleChunks.values());
|
|
+ List<ServerPlayer> players = world.players;
|
|
+
|
|
+ int fullLoadedChunks = 0;
|
|
+
|
|
+ for (ChunkHolder chunk : allChunks) {
|
|
+ if (chunk.getFullChunkNowUnchecked() != null) {
|
|
+ ++fullLoadedChunks;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // sorting by coordinate makes the log easier to read
|
|
+ allChunks.sort((ChunkHolder v1, ChunkHolder v2) -> {
|
|
+ if (v1.pos.x != v2.pos.x) {
|
|
+ return Integer.compare(v1.pos.x, v2.pos.x);
|
|
+ }
|
|
+ return Integer.compare(v1.pos.z, v2.pos.z);
|
|
+ });
|
|
+
|
|
+ worldData.addProperty("name", world.getWorld().getName());
|
|
+ worldData.addProperty("view-distance", world.spigotConfig.viewDistance);
|
|
+ worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
|
|
+ worldData.addProperty("keep-spawn-loaded-range", world.paperConfig.keepLoadedRange);
|
|
+ worldData.addProperty("visible-chunk-count", visibleChunks.size());
|
|
+ worldData.addProperty("loaded-chunk-count", chunkMap.entitiesInLevel.size());
|
|
+ worldData.addProperty("verified-fully-loaded-chunks", fullLoadedChunks);
|
|
+
|
|
+ JsonArray playersData = new JsonArray();
|
|
+
|
|
+ for (ServerPlayer player : players) {
|
|
+ JsonObject playerData = new JsonObject();
|
|
+
|
|
+ playerData.addProperty("name", player.getScoreboardName());
|
|
+ playerData.addProperty("x", player.getX());
|
|
+ playerData.addProperty("y", player.getY());
|
|
+ playerData.addProperty("z", player.getZ());
|
|
+
|
|
+ playersData.add(playerData);
|
|
+
|
|
+ }
|
|
+
|
|
+ worldData.add("players", playersData);
|
|
+
|
|
+ JsonArray chunksData = new JsonArray();
|
|
+
|
|
+ for (ChunkHolder playerChunk : allChunks) {
|
|
+ JsonObject chunkData = new JsonObject();
|
|
+
|
|
+ Set<Ticket<?>> tickets = chunkMapDistance.tickets.get(playerChunk.pos.longKey);
|
|
+ ChunkStatus status = getChunkStatus(playerChunk);
|
|
+
|
|
+ chunkData.addProperty("x", playerChunk.pos.x);
|
|
+ chunkData.addProperty("z", playerChunk.pos.z);
|
|
+ chunkData.addProperty("ticket-level", playerChunk.getTicketLevel());
|
|
+ chunkData.addProperty("state", ChunkHolder.getFullChunkStatus(playerChunk.getTicketLevel()).toString());
|
|
+ chunkData.addProperty("queued-for-unload", chunkMap.toDrop.contains(playerChunk.pos.longKey));
|
|
+ chunkData.addProperty("status", status == null ? "unloaded" : status.toString());
|
|
+
|
|
+ JsonArray ticketsData = new JsonArray();
|
|
+
|
|
+ if (tickets != null) {
|
|
+ for (Ticket<?> ticket : tickets) {
|
|
+ JsonObject ticketData = new JsonObject();
|
|
+
|
|
+ ticketData.addProperty("ticket-type", ticket.getType().toString());
|
|
+ ticketData.addProperty("ticket-level", ticket.getTicketLevel());
|
|
+ ticketData.addProperty("object-reason", String.valueOf(ticket.key));
|
|
+ ticketData.addProperty("add-tick", ticket.createdTick);
|
|
+
|
|
+ ticketsData.add(ticketData);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ chunkData.add("tickets", ticketsData);
|
|
+ chunksData.add(chunkData);
|
|
+ }
|
|
+
|
|
+
|
|
+ worldData.add("chunk-data", chunksData);
|
|
+ worldsData.add(worldData);
|
|
+ }
|
|
+
|
|
+ data.add("worlds", worldsData);
|
|
+
|
|
+ StringWriter stringWriter = new StringWriter();
|
|
+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
|
|
+ jsonWriter.setIndent(" ");
|
|
+ jsonWriter.setLenient(false);
|
|
+ Streams.write(data, jsonWriter);
|
|
+
|
|
+ String fileData = stringWriter.toString();
|
|
+
|
|
+ try (PrintStream out = new PrintStream(new FileOutputStream(file), false, "UTF-8")) {
|
|
+ out.print(fileData);
|
|
+ }
|
|
+ }
|
|
+
|
|
public static int getTicketLevelFor(net.minecraft.world.level.chunk.ChunkStatus status) {
|
|
return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status);
|
|
}
|