diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch index 16f39fcba1..f7949f17c6 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch @@ -14,12 +14,10 @@ public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { this.level = world; -@@ -93,8 +100,66 @@ - this.distanceManager = this.chunkMap.getDistanceManager(); - this.distanceManager.updateSimulationDistance(simulationDistance); +@@ -95,6 +102,64 @@ this.clearCache(); -+ } -+ + } + + // CraftBukkit start - properly implement isChunkLoaded + public boolean isChunkLoaded(int chunkX, int chunkZ) { + ChunkHolder chunk = this.chunkMap.getUpdatingChunkIfPresent(ChunkPos.asLong(chunkX, chunkZ)); @@ -50,8 +48,8 @@ + + public void addTicketAtLevel(TicketType ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) { + this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier); - } - ++ } ++ + public void removeTicketAtLevel(TicketType ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) { + this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier); + } @@ -90,16 +88,18 @@ return ichunkaccess; } } -@@ -151,7 +216,7 @@ +@@ -150,8 +215,9 @@ + Objects.requireNonNull(completablefuture); chunkproviderserver_b.managedBlock(completablefuture::isDone); ++ // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads ChunkResult chunkresult = (ChunkResult) completablefuture.join(); - ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse((Object) null); + ChunkAccess ichunkaccess1 = (ChunkAccess) chunkresult.orElse(null); // CraftBukkit - decompile error if (ichunkaccess1 == null && create) { throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError())); -@@ -231,7 +296,15 @@ +@@ -231,7 +297,15 @@ int l = ChunkLevel.byStatus(leastStatus); ChunkHolder playerchunk = this.getVisibleChunkIfPresent(k); @@ -116,7 +116,7 @@ this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); if (this.chunkAbsent(playerchunk, l)) { ProfilerFiller gameprofilerfiller = Profiler.get(); -@@ -250,7 +323,7 @@ +@@ -250,7 +324,7 @@ } private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { @@ -125,7 +125,7 @@ } @Override -@@ -279,7 +352,7 @@ +@@ -279,7 +353,7 @@ return this.mainThreadProcessor.pollTask(); } @@ -134,7 +134,7 @@ boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); boolean flag1 = this.chunkMap.promoteChunkMap(); -@@ -309,18 +382,40 @@ +@@ -309,18 +383,40 @@ @Override public void close() throws IOException { @@ -177,7 +177,7 @@ this.distanceManager.purgeStaleTickets(); } -@@ -401,14 +496,22 @@ +@@ -401,14 +497,22 @@ this.lastSpawnState = spawnercreature_d; profiler.popPush("spawnAndTick"); @@ -203,7 +203,7 @@ } else { list1 = List.of(); } -@@ -420,7 +523,7 @@ +@@ -420,7 +524,7 @@ ChunkPos chunkcoordintpair = chunk.getPos(); chunk.incrementInhabitedTime(timeDelta); @@ -212,7 +212,7 @@ NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, list1); } -@@ -541,10 +644,16 @@ +@@ -541,10 +645,16 @@ @Override public void setSpawnSettings(boolean spawnMonsters) { @@ -231,7 +231,7 @@ public String getChunkDebugData(ChunkPos pos) { return this.chunkMap.getChunkDebugData(pos); } -@@ -618,14 +727,20 @@ +@@ -618,14 +728,20 @@ } @Override diff --git a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch index a57bbc483a..103da8c231 100644 --- a/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/level/ServerLevel.java.patch @@ -61,7 +61,7 @@ private int lastSpawnChunkRadius; final EntityTickList entityTickList = new EntityTickList(); public final PersistentEntitySectionManager entityManager; -@@ -214,52 +226,194 @@ +@@ -214,54 +226,203 @@ private final boolean tickTime; private final RandomSequences randomSequences; @@ -129,7 +129,7 @@ + } + int minBlockX = Mth.floor(axisalignedbb.minX - 1.0E-7D) - 3; + int minBlockZ = Mth.floor(axisalignedbb.minZ - 1.0E-7D) - 3; -+ + + int maxBlockX = Mth.floor(axisalignedbb.maxX + 1.0E-7D) + 3; + int maxBlockZ = Mth.floor(axisalignedbb.maxZ + 1.0E-7D) + 3; + @@ -209,7 +209,7 @@ + ChunkGenerator chunkgenerator = worlddimension.generator(); + // CraftBukkit start + this.serverLevelData.setWorld(this); - ++ + if (biomeProvider != null) { + BiomeSource worldChunkManager = new CustomWorldChunkManager(this.getWorld(), biomeProvider, this.server.registryAccess().lookupOrThrow(Registries.BIOME)); + if (chunkgenerator instanceof NoiseBasedChunkGenerator cga) { @@ -279,8 +279,17 @@ + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit } ++ // Paper start ++ @Override ++ public boolean hasChunk(int chunkX, int chunkZ) { ++ return this.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ) != null; ++ } ++ // Paper end ++ /** @deprecated */ -@@ -305,12 +459,20 @@ + @Deprecated + @VisibleForTesting +@@ -305,12 +466,20 @@ long j; if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { @@ -304,7 +313,7 @@ if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { this.resetWeatherCycle(); } -@@ -345,7 +507,7 @@ +@@ -345,7 +514,7 @@ this.handlingTick = false; gameprofilerfiller.pop(); @@ -313,7 +322,7 @@ if (flag1) { this.resetEmptyTime(); -@@ -359,6 +521,7 @@ +@@ -359,6 +528,7 @@ gameprofilerfiller.pop(); } @@ -321,7 +330,7 @@ this.entityTickList.forEach((entity) -> { if (!entity.isRemoved()) { if (!tickratemanager.isEntityFrozen(entity)) { -@@ -429,7 +592,7 @@ +@@ -429,7 +599,7 @@ private void wakeUpAllPlayers() { this.sleepStatus.removeAllSleepers(); @@ -330,7 +339,7 @@ entityplayer.stopSleepInBed(false, false); }); } -@@ -442,12 +605,12 @@ +@@ -442,12 +612,12 @@ ProfilerFiller gameprofilerfiller = Profiler.get(); gameprofilerfiller.push("thunder"); @@ -345,7 +354,7 @@ if (flag1) { SkeletonHorse entityhorseskeleton = (SkeletonHorse) EntityType.SKELETON_HORSE.create(this, EntitySpawnReason.EVENT); -@@ -456,7 +619,7 @@ +@@ -456,7 +626,7 @@ entityhorseskeleton.setTrap(true); entityhorseskeleton.setAge(0); entityhorseskeleton.setPos((double) blockposition.getX(), (double) blockposition.getY(), (double) blockposition.getZ()); @@ -354,7 +363,7 @@ } } -@@ -465,18 +628,20 @@ +@@ -465,18 +635,20 @@ if (entitylightning != null) { entitylightning.moveTo(Vec3.atBottomCenterOf(blockposition)); entitylightning.setVisualOnly(flag1); @@ -376,7 +385,7 @@ gameprofilerfiller.popPush("tickBlocks"); if (randomTickSpeed > 0) { -@@ -521,7 +686,7 @@ +@@ -521,7 +693,7 @@ Biome biomebase = (Biome) this.getBiome(blockposition1).value(); if (biomebase.shouldFreeze(this, blockposition2)) { @@ -385,7 +394,7 @@ } if (this.isRaining()) { -@@ -537,10 +702,10 @@ +@@ -537,10 +709,10 @@ BlockState iblockdata1 = (BlockState) iblockdata.setValue(SnowLayerBlock.LAYERS, j + 1); Block.pushEntitiesUp(iblockdata, iblockdata1, this, blockposition1); @@ -398,7 +407,7 @@ } } -@@ -701,33 +866,67 @@ +@@ -701,33 +873,67 @@ this.rainLevel = Mth.clamp(this.rainLevel, 0.0F, 1.0F); } @@ -474,7 +483,7 @@ } public void resetEmptyTime() { -@@ -754,6 +953,13 @@ +@@ -754,6 +960,13 @@ } public void tickNonPassenger(Entity entity) { @@ -488,7 +497,7 @@ entity.setOldPosAndRot(); ProfilerFiller gameprofilerfiller = Profiler.get(); -@@ -763,6 +969,7 @@ +@@ -763,6 +976,7 @@ }); gameprofilerfiller.incrementCounter("tickNonPassenger"); entity.tick(); @@ -496,7 +505,7 @@ gameprofilerfiller.pop(); Iterator iterator = entity.getPassengers().iterator(); -@@ -786,6 +993,7 @@ +@@ -786,6 +1000,7 @@ }); gameprofilerfiller.incrementCounter("tickPassenger"); passenger.rideTick(); @@ -504,7 +513,7 @@ gameprofilerfiller.pop(); Iterator iterator = passenger.getPassengers().iterator(); -@@ -810,6 +1018,7 @@ +@@ -810,6 +1025,7 @@ ServerChunkCache chunkproviderserver = this.getChunkSource(); if (!savingDisabled) { @@ -512,7 +521,7 @@ if (progressListener != null) { progressListener.progressStartNoAbort(Component.translatable("menu.savingLevel")); } -@@ -827,11 +1036,19 @@ +@@ -827,11 +1043,19 @@ } } @@ -533,7 +542,7 @@ } DimensionDataStorage worldpersistentdata = this.getChunkSource().getDataStorage(); -@@ -903,18 +1120,40 @@ +@@ -903,18 +1127,40 @@ @Override public boolean addFreshEntity(Entity entity) { @@ -577,7 +586,7 @@ } } -@@ -939,41 +1178,93 @@ +@@ -939,41 +1185,93 @@ this.entityManager.addNewEntity(player); } @@ -676,7 +685,7 @@ while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); -@@ -982,6 +1273,12 @@ +@@ -982,6 +1280,12 @@ double d1 = (double) pos.getY() - entityplayer.getY(); double d2 = (double) pos.getZ() - entityplayer.getZ(); @@ -689,7 +698,7 @@ if (d0 * d0 + d1 * d1 + d2 * d2 < 1024.0D) { entityplayer.connection.send(new ClientboundBlockDestructionPacket(entityId, pos, progress)); } -@@ -1030,7 +1327,7 @@ +@@ -1030,7 +1334,7 @@ @Override public void levelEvent(@Nullable Player player, int eventId, BlockPos pos, int data) { @@ -698,7 +707,7 @@ } public int getLogicalHeight() { -@@ -1060,7 +1357,18 @@ +@@ -1060,7 +1364,18 @@ Iterator iterator = this.navigatingMobs.iterator(); while (iterator.hasNext()) { @@ -718,7 +727,7 @@ PathNavigation navigationabstract = entityinsentient.getNavigation(); if (navigationabstract.shouldRecomputePath(pos)) { -@@ -1086,11 +1394,13 @@ +@@ -1086,11 +1401,13 @@ @Override public void updateNeighborsAt(BlockPos pos, Block block) { @@ -732,7 +741,7 @@ this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null, orientation); } -@@ -1126,9 +1436,20 @@ +@@ -1126,9 +1443,20 @@ @Override public void explode(@Nullable Entity entity, @Nullable DamageSource damageSource, @Nullable ExplosionDamageCalculator behavior, double x, double y, double z, float power, boolean createFire, Level.ExplosionInteraction explosionSourceType, ParticleOptions smallParticle, ParticleOptions largeParticle, Holder soundEvent) { @@ -754,7 +763,7 @@ case NONE: explosion_effect = Explosion.BlockInteraction.KEEP; break; -@@ -1144,16 +1465,27 @@ +@@ -1144,16 +1472,27 @@ case TRIGGER: explosion_effect = Explosion.BlockInteraction.TRIGGER_BLOCK; break; @@ -785,7 +794,7 @@ Iterator iterator = this.players.iterator(); while (iterator.hasNext()) { -@@ -1162,10 +1494,11 @@ +@@ -1162,10 +1501,11 @@ if (entityplayer.distanceToSqr(vec3d) < 4096.0D) { Optional optional = Optional.ofNullable((Vec3) serverexplosion.getHitPlayers().get(entityplayer)); @@ -798,7 +807,7 @@ } private Explosion.BlockInteraction getDestroyType(GameRules.Key decayRule) { -@@ -1226,17 +1559,29 @@ +@@ -1226,17 +1566,29 @@ } public int sendParticles(T parameters, double x, double y, double z, int count, double offsetX, double offsetY, double offsetZ, double speed) { @@ -834,7 +843,7 @@ ++j; } } -@@ -1292,7 +1637,7 @@ +@@ -1292,7 +1644,7 @@ @Nullable public BlockPos findNearestMapStructure(TagKey structureTag, BlockPos pos, int radius, boolean skipReferencedStructures) { @@ -843,7 +852,7 @@ return null; } else { Optional> optional = this.registryAccess().lookupOrThrow(Registries.STRUCTURE).get(structureTag); -@@ -1334,11 +1679,22 @@ +@@ -1334,11 +1686,22 @@ @Nullable @Override public MapItemSavedData getMapData(MapId id) { @@ -867,7 +876,7 @@ this.getServer().overworld().getDataStorage().set(id.key(), state); } -@@ -1649,6 +2005,11 @@ +@@ -1649,6 +2012,11 @@ @Override public void blockUpdated(BlockPos pos, Block block) { if (!this.isDebug()) { @@ -879,7 +888,7 @@ this.updateNeighborsAt(pos, block); } -@@ -1668,12 +2029,12 @@ +@@ -1668,12 +2036,12 @@ } public boolean isFlat() { @@ -894,7 +903,7 @@ } @Nullable -@@ -1696,7 +2057,7 @@ +@@ -1696,7 +2064,7 @@ private static String getTypeCount(Iterable items, Function classifier) { try { Object2IntOpenHashMap object2intopenhashmap = new Object2IntOpenHashMap(); @@ -903,7 +912,7 @@ while (iterator.hasNext()) { T t0 = iterator.next(); -@@ -1705,7 +2066,7 @@ +@@ -1705,7 +2073,7 @@ object2intopenhashmap.addTo(s, 1); } @@ -912,7 +921,7 @@ String s1 = (String) entry.getKey(); return s1 + ":" + entry.getIntValue(); -@@ -1717,6 +2078,7 @@ +@@ -1717,6 +2085,7 @@ @Override public LevelEntityGetter getEntities() { @@ -920,7 +929,7 @@ return this.entityManager.getEntityGetter(); } -@@ -1802,6 +2164,17 @@ +@@ -1802,6 +2171,17 @@ return this.serverLevelData.getGameRules(); } @@ -938,7 +947,7 @@ @Override public CrashReportCategory fillReportDetails(CrashReport report) { CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report); -@@ -1836,6 +2209,7 @@ +@@ -1836,6 +2216,7 @@ } public void onTrackingStart(Entity entity) { @@ -946,7 +955,7 @@ ServerLevel.this.getChunkSource().addEntity(entity); if (entity instanceof ServerPlayer entityplayer) { ServerLevel.this.players.add(entityplayer); -@@ -1864,9 +2238,52 @@ +@@ -1864,9 +2245,52 @@ } entity.updateDynamicGameEventListener(DynamicGameEventListener::add); @@ -999,7 +1008,7 @@ ServerLevel.this.getChunkSource().removeEntity(entity); if (entity instanceof ServerPlayer entityplayer) { ServerLevel.this.players.remove(entityplayer); -@@ -1895,6 +2312,15 @@ +@@ -1895,6 +2319,15 @@ } entity.updateDynamicGameEventListener(DynamicGameEventListener::remove); diff --git a/paper-server/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java b/paper-server/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java new file mode 100644 index 0000000000..605a4a83d0 --- /dev/null +++ b/paper-server/src/main/java/com/destroystokyo/paper/io/SyncLoadFinder.java @@ -0,0 +1,177 @@ +package com.destroystokyo.paper.io; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Pair; +import it.unimi.dsi.fastutil.longs.Long2IntMap; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; + +public class SyncLoadFinder { + + public static final boolean ENABLED = Boolean.getBoolean("paper.debug-sync-loads"); + + private static final WeakHashMap> SYNC_LOADS = new WeakHashMap<>(); + + private static final class SyncLoadInformation { + + public int times; + + public final Long2IntOpenHashMap coordinateTimes = new Long2IntOpenHashMap(); + } + + public static void clear() { + SYNC_LOADS.clear(); + } + + public static void logSyncLoad(final Level world, final int chunkX, final int chunkZ) { + if (!ENABLED) { + return; + } + + final ThrowableWithEquals stacktrace = new ThrowableWithEquals(Thread.currentThread().getStackTrace()); + + SYNC_LOADS.compute(world, (final Level keyInMap, Object2ObjectOpenHashMap map) -> { + if (map == null) { + map = new Object2ObjectOpenHashMap<>(); + } + + map.compute(stacktrace, (ThrowableWithEquals keyInMap0, SyncLoadInformation valueInMap) -> { + if (valueInMap == null) { + valueInMap = new SyncLoadInformation(); + } + + ++valueInMap.times; + + valueInMap.coordinateTimes.compute(ChunkPos.asLong(chunkX, chunkZ), (Long keyInMap1, Integer valueInMap1) -> { + return valueInMap1 == null ? Integer.valueOf(1) : Integer.valueOf(valueInMap1.intValue() + 1); + }); + + return valueInMap; + }); + + return map; + }); + } + + public static JsonObject serialize() { + final JsonObject ret = new JsonObject(); + + final JsonArray worldsData = new JsonArray(); + + for (final Map.Entry> entry : SYNC_LOADS.entrySet()) { + final Level world = entry.getKey(); + + final JsonObject worldData = new JsonObject(); + + worldData.addProperty("name", world.getWorld().getName()); + + final List> data = new ArrayList<>(); + + entry.getValue().forEach((ThrowableWithEquals stacktrace, SyncLoadInformation times) -> { + data.add(new Pair<>(stacktrace, times)); + }); + + data.sort((Pair pair1, Pair pair2) -> { + return Integer.compare(pair2.getSecond().times, pair1.getSecond().times); // reverse order + }); + + final JsonArray stacktraces = new JsonArray(); + + for (Pair pair : data) { + final JsonObject stacktrace = new JsonObject(); + + stacktrace.addProperty("times", pair.getSecond().times); + + final JsonArray traces = new JsonArray(); + + for (StackTraceElement element : pair.getFirst().stacktrace) { + traces.add(String.valueOf(element)); + } + + stacktrace.add("stacktrace", traces); + + final JsonArray coordinates = new JsonArray(); + + for (Long2IntMap.Entry coordinate : pair.getSecond().coordinateTimes.long2IntEntrySet()) { + final long key = coordinate.getLongKey(); + final int times = coordinate.getIntValue(); + final ChunkPos chunkPos = new ChunkPos(key); + coordinates.add("(" + chunkPos.x + "," + chunkPos.z + "): " + times); + } + + stacktrace.add("coordinates", coordinates); + + stacktraces.add(stacktrace); + } + + + worldData.add("stacktraces", stacktraces); + worldsData.add(worldData); + } + + ret.add("worlds", worldsData); + + return ret; + } + + static final class ThrowableWithEquals { + + private final StackTraceElement[] stacktrace; + private final int hash; + + public ThrowableWithEquals(final StackTraceElement[] stacktrace) { + this.stacktrace = stacktrace; + this.hash = ThrowableWithEquals.hash(stacktrace); + } + + public static int hash(final StackTraceElement[] stacktrace) { + int hash = 0; + + for (int i = 0; i < stacktrace.length; ++i) { + hash *= 31; + hash += stacktrace[i].hashCode(); + } + + return hash; + } + + @Override + public int hashCode() { + return this.hash; + } + + @Override + public boolean equals(final Object obj) { + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + + final ThrowableWithEquals other = (ThrowableWithEquals)obj; + final StackTraceElement[] otherStackTrace = other.stacktrace; + + if (this.stacktrace.length != otherStackTrace.length || this.hash != other.hash) { + return false; + } + + if (this == obj) { + return true; + } + + for (int i = 0; i < this.stacktrace.length; ++i) { + if (!this.stacktrace[i].equals(otherStackTrace[i])) { + return false; + } + } + + return true; + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java b/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java index e1820a3394..3010d57efc 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java @@ -38,6 +38,7 @@ public final class PaperCommand extends Command { commands.put(Set.of("reload"), new ReloadCommand()); commands.put(Set.of("version"), new VersionCommand()); commands.put(Set.of("dumpplugins"), new DumpPluginsCommand()); + commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand()); return commands.entrySet().stream() .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/SyncLoadInfoCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/SyncLoadInfoCommand.java new file mode 100644 index 0000000000..95d6022c9c --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/SyncLoadInfoCommand.java @@ -0,0 +1,88 @@ +package io.papermc.paper.command.subcommands; + +import com.destroystokyo.paper.io.SyncLoadFinder; +import com.google.gson.JsonObject; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonWriter; +import io.papermc.paper.command.CommandUtil; +import io.papermc.paper.command.PaperSubcommand; +import java.io.File; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.minecraft.server.MinecraftServer; +import org.bukkit.command.CommandSender; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static net.kyori.adventure.text.format.NamedTextColor.WHITE; + +@DefaultQualifier(NonNull.class) +public final class SyncLoadInfoCommand implements PaperSubcommand { + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + this.doSyncLoadInfo(sender, args); + return true; + } + + @Override + public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { + return CommandUtil.getListMatchingLast(sender, args, "clear"); + } + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); + + private void doSyncLoadInfo(final CommandSender sender, final String[] args) { + if (!SyncLoadFinder.ENABLED) { + String systemFlag = "-Dpaper.debug-sync-loads=true"; + sender.sendMessage(text().color(RED).append(text("This command requires the server startup flag '")).append( + text(systemFlag, WHITE).clickEvent(ClickEvent.copyToClipboard(systemFlag)) + .hoverEvent(HoverEvent.showText(text("Click to copy the system flag")))).append( + text("' to be set."))); + return; + } + + if (args.length > 0 && args[0].equals("clear")) { + SyncLoadFinder.clear(); + sender.sendMessage(text("Sync load data cleared.", GRAY)); + return; + } + + File file = new File(new File(new File("."), "debug"), + "sync-load-info-" + FORMATTER.format(LocalDateTime.now()) + ".txt"); + file.getParentFile().mkdirs(); + sender.sendMessage(text("Writing sync load info to " + file, GREEN)); + + + try { + final JsonObject data = SyncLoadFinder.serialize(); + + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setIndent(" "); + jsonWriter.setLenient(false); + Streams.write(data, jsonWriter); + + try ( + PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8) + ) { + out.print(stringWriter); + } + sender.sendMessage(text("Successfully written sync load information!", GREEN)); + } catch (Throwable thr) { + sender.sendMessage(text("Failed to write sync load information! See the console for more info.", RED)); + MinecraftServer.LOGGER.warn("Error occurred while dumping sync chunk load info", thr); + } + } +}