net/minecraft/server/level

This commit is contained in:
Bjarne Koll
2024-12-15 05:32:25 +01:00
parent 0262d9a165
commit 35afd218f5
19 changed files with 3854 additions and 4329 deletions

View File

@@ -0,0 +1,190 @@
--- a/net/minecraft/server/level/ChunkHolder.java
+++ b/net/minecraft/server/level/ChunkHolder.java
@@ -33,9 +_,9 @@
public static final ChunkResult<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
private static final CompletableFuture<ChunkResult<LevelChunk>> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_LEVEL_CHUNK);
private final LevelHeightAccessor levelHeightAccessor;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
+ private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
public int oldTicketLevel;
private int ticketLevel;
private int queueLevel;
@@ -71,6 +_,18 @@
this.changedBlocksPerSection = new ShortSet[levelHeightAccessor.getSectionsCount()];
}
+ // CraftBukkit start
+ public LevelChunk getFullChunkNow() {
+ // Note: We use the oldTicketLevel for isLoaded checks.
+ if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null;
+ return this.getFullChunkNowUnchecked();
+ }
+
+ public LevelChunk getFullChunkNowUnchecked() {
+ return (LevelChunk) this.getChunkIfPresentUnchecked(ChunkStatus.FULL);
+ }
+ // CraftBukkit end
+
public CompletableFuture<ChunkResult<LevelChunk>> getTickingChunkFuture() {
return this.tickingChunkFuture;
}
@@ -129,6 +_,7 @@
} else {
boolean flag = this.hasChangedSections;
int sectionIndex = this.levelHeightAccessor.getSectionIndex(pos.getY());
+ if (sectionIndex < 0 || sectionIndex >= this.changedBlocksPerSection.length) return false; // CraftBukkit - SPIGOT-6086, SPIGOT-6296
if (this.changedBlocksPerSection[sectionIndex] == null) {
this.hasChangedSections = true;
this.changedBlocksPerSection[sectionIndex] = new ShortOpenHashSet();
@@ -274,6 +_,38 @@
chunkMap.onFullChunkStatusChange(this.pos, fullChunkStatus);
}
+ // CraftBukkit start
+ // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins.
+ // SPIGOT-7780: Moved out of updateFutures to call all chunk unload events before calling updateHighestAllowedStatus for all chunks
+ protected void callEventIfUnloading(ChunkMap chunkMap) {
+ FullChunkStatus oldFullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel);
+ FullChunkStatus newFullChunkStatus = ChunkLevel.fullStatus(this.ticketLevel);
+ boolean oldIsFull = oldFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
+ boolean newIsFull = newFullChunkStatus.isOrAfter(FullChunkStatus.FULL);
+ if (oldIsFull && !newIsFull) {
+ this.getFullChunkFuture().thenAccept((either) -> {
+ LevelChunk chunk = either.orElse(null);
+ if (chunk != null) {
+ chunkMap.callbackExecutor.execute(() -> {
+ // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick
+ // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag.
+ // These actions may however happen deferred, so we manually set the needsSaving flag already here.
+ chunk.markUnsaved();
+ chunk.unloadCallback();
+ });
+ }
+ }).exceptionally((throwable) -> {
+ // ensure exceptions are printed, by default this is not the case
+ net.minecraft.server.MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable);
+ return null;
+ });
+
+ // Run callback right away if the future was already done
+ chunkMap.callbackExecutor.run();
+ }
+ }
+ // CraftBukkit end
+
protected void updateFutures(ChunkMap chunkMap, Executor executor) {
FullChunkStatus fullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel);
FullChunkStatus fullChunkStatus1 = ChunkLevel.fullStatus(this.ticketLevel);
@@ -281,12 +_,28 @@
boolean isOrAfter1 = fullChunkStatus1.isOrAfter(FullChunkStatus.FULL);
this.wasAccessibleSinceLastSave |= isOrAfter1;
if (!isOrAfter && isOrAfter1) {
+ int expectCreateCount = ++this.fullChunkCreateCount; // Paper
this.fullChunkFuture = chunkMap.prepareAccessibleChunk(this);
this.scheduleFullChunkPromotion(chunkMap, this.fullChunkFuture, executor, FullChunkStatus.FULL);
+ // Paper start - cache ticking ready status
+ this.fullChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ if (ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
+ ChunkHolder.this.isFullChunkReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkBorder(chunk, this);
+ }
+ });
+ });
+ // Paper end - cache ticking ready status
this.addSaveDependency(this.fullChunkFuture);
}
if (isOrAfter && !isOrAfter1) {
+ // Paper start
+ if (this.isFullChunkReady) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+ }
+ // Paper end
this.fullChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
this.fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
}
@@ -296,11 +_,25 @@
if (!isOrAfter2 && isOrAfter3) {
this.tickingChunkFuture = chunkMap.prepareTickingChunk(this);
this.scheduleFullChunkPromotion(chunkMap, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING);
+ // Paper start - cache ticking ready status
+ this.tickingChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ // note: Here is a very good place to add callbacks to logic waiting on this.
+ ChunkHolder.this.isTickingReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkTicking(chunk, this);
+ });
+ });
+ // Paper end
this.addSaveDependency(this.tickingChunkFuture);
}
if (isOrAfter2 && !isOrAfter3) {
- this.tickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isTickingReady) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().orElseThrow(IllegalStateException::new), this); // Paper
+ }
+ // Paper end
+ this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
this.tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
}
@@ -313,11 +_,24 @@
this.entityTickingChunkFuture = chunkMap.prepareEntityTickingChunk(this);
this.scheduleFullChunkPromotion(chunkMap, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING);
+ // Paper start - cache ticking ready status
+ this.entityTickingChunkFuture.thenAccept(chunkResult -> {
+ chunkResult.ifSuccess(chunk -> {
+ ChunkHolder.this.isEntityTickingReady = true;
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkEntityTicking(chunk, this);
+ });
+ });
+ // Paper end
this.addSaveDependency(this.entityTickingChunkFuture);
}
if (isOrAfter4 && !isOrAfter5) {
- this.entityTickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK);
+ // Paper start
+ if (this.isEntityTickingReady) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().orElseThrow(IllegalStateException::new), this);
+ }
+ // Paper end
+ this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
this.entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE;
}
@@ -327,6 +_,26 @@
this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel);
this.oldTicketLevel = this.ticketLevel;
+ // CraftBukkit start
+ // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins.
+ if (!fullChunkStatus.isOrAfter(FullChunkStatus.FULL) && fullChunkStatus1.isOrAfter(FullChunkStatus.FULL)) {
+ this.getFullChunkFuture().thenAccept((either) -> {
+ LevelChunk chunk = (LevelChunk) either.orElse(null);
+ if (chunk != null) {
+ chunkMap.callbackExecutor.execute(() -> {
+ chunk.loadCallback();
+ });
+ }
+ }).exceptionally((throwable) -> {
+ // ensure exceptions are printed, by default this is not the case
+ net.minecraft.server.MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable);
+ return null;
+ });
+
+ // Run callback right away if the future was already done
+ chunkMap.callbackExecutor.run();
+ }
+ // CraftBukkit end
}
public boolean wasAccessibleSinceLastSave() {

View File

@@ -0,0 +1,375 @@
--- a/net/minecraft/server/level/ChunkMap.java
+++ b/net/minecraft/server/level/ChunkMap.java
@@ -145,6 +_,33 @@
public int serverViewDistance;
private final WorldGenContext worldGenContext;
+ // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
+ public final CallbackExecutor callbackExecutor = new CallbackExecutor();
+ public static final class CallbackExecutor implements java.util.concurrent.Executor, Runnable {
+
+ private final java.util.Queue<Runnable> queue = new java.util.ArrayDeque<>();
+
+ @Override
+ public void execute(Runnable runnable) {
+ this.queue.add(runnable);
+ }
+
+ @Override
+ public void run() {
+ Runnable task;
+ while ((task = this.queue.poll()) != null) {
+ task.run();
+ }
+ }
+ };
+ // CraftBukkit end
+
+ // Paper start
+ public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) {
+ return this.pendingUnloads.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+ // Paper end
+
public ChunkMap(
ServerLevel level,
LevelStorageSource.LevelStorageAccess levelStorageAccess,
@@ -171,13 +_,19 @@
this.level = level;
RegistryAccess registryAccess = level.registryAccess();
long seed = level.getSeed();
- if (generator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) {
+ // CraftBukkit start - SPIGOT-7051: It's a rigged game! Use delegate for random state creation, otherwise it is not so random.
+ ChunkGenerator randomGenerator = generator;
+ if (randomGenerator instanceof org.bukkit.craftbukkit.generator.CustomChunkGenerator customChunkGenerator) {
+ randomGenerator = customChunkGenerator.getDelegate();
+ }
+ if (randomGenerator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) {
+ // CraftBukkit end
this.randomState = RandomState.create(noiseBasedChunkGenerator.generatorSettings().value(), registryAccess.lookupOrThrow(Registries.NOISE), seed);
} else {
this.randomState = RandomState.create(NoiseGeneratorSettings.dummy(), registryAccess.lookupOrThrow(Registries.NOISE), seed);
}
- this.chunkGeneratorState = generator.createState(registryAccess.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, seed);
+ this.chunkGeneratorState = generator.createState(registryAccess.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, seed, level.spigotConfig); // Spigot
this.mainThreadExecutor = mainThreadExecutor;
ConsecutiveExecutor consecutiveExecutor = new ConsecutiveExecutor(dispatcher, "worldgen");
this.progressListener = progressListener;
@@ -207,6 +_,12 @@
this.chunksToEagerlySave.add(chunkPos.toLong());
}
+ // Paper start
+ public int getMobCountNear(final ServerPlayer player, final net.minecraft.world.entity.MobCategory mobCategory) {
+ return -1;
+ }
+ // Paper end
+
protected ChunkGenerator generator() {
return this.worldGenContext.generator();
}
@@ -354,9 +_,9 @@
}
);
stringBuilder.append("Updating:").append(System.lineSeparator());
- this.updatingChunkMap.values().forEach(consumer);
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
stringBuilder.append("Visible:").append(System.lineSeparator());
- this.visibleChunkMap.values().forEach(consumer);
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
CrashReport crashReport = CrashReport.forThrowable(exception, "Chunk loading");
CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk loading");
crashReportCategory.setDetail("Details", details);
@@ -392,6 +_,9 @@
holder.setTicketLevel(newLevel);
} else {
holder = new ChunkHolder(new ChunkPos(chunkPos), newLevel, this.level, this.lightEngine, this::onLevelChange, this);
+ // Paper start
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderCreate(this.level, holder);
+ // Paper end
}
this.updatingChunkMap.put(chunkPos, holder);
@@ -420,8 +_,8 @@
protected void saveAllChunks(boolean flush) {
if (flush) {
- List<ChunkHolder> list = this.visibleChunkMap
- .values()
+ List<ChunkHolder> list = ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level) // Paper - moonrise
+ //.values() // Paper - moonrise
.stream()
.filter(ChunkHolder::wasAccessibleSinceLastSave)
.peek(ChunkHolder::refreshAccessibility)
@@ -447,7 +_,7 @@
this.nextChunkSaveTime.clear();
long millis = Util.getMillis();
- for (ChunkHolder chunkHolder : this.visibleChunkMap.values()) {
+ for (ChunkHolder chunkHolder : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)) { // Paper
this.saveChunkIfNeeded(chunkHolder, millis);
}
}
@@ -468,6 +_,7 @@
public boolean hasWork() {
return this.lightEngine.hasLightWork()
|| !this.pendingUnloads.isEmpty()
+ || ca.spottedleaf.moonrise.common.util.ChunkSystem.hasAnyChunkHolders(this.level) // Paper - moonrise
|| !this.updatingChunkMap.isEmpty()
|| this.poiManager.hasWork()
|| !this.toDrop.isEmpty()
@@ -526,7 +_,11 @@
this.scheduleUnload(chunkPos, chunkHolder);
} else {
ChunkAccess latestChunk = chunkHolder.getLatestChunk();
- if (this.pendingUnloads.remove(chunkPos, chunkHolder) && latestChunk != null) {
+ // Paper start
+ boolean removed;
+ if ((removed = this.pendingUnloads.remove(chunkPos, chunkHolder)) && latestChunk != null) {
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunkHolder);
+ // Paper end
if (latestChunk instanceof LevelChunk levelChunk) {
levelChunk.setLoaded(false);
}
@@ -540,7 +_,9 @@
this.lightEngine.tryScheduleUpdate();
this.progressListener.onStatusChange(latestChunk.getPos(), null);
this.nextChunkSaveTime.remove(latestChunk.getPos().toLong());
- }
+ } else if (removed) { // Paper start
+ ca.spottedleaf.moonrise.common.util.ChunkSystem.onChunkHolderDelete(this.level, chunkHolder);
+ } // Paper end
}
}, this.unloadQueue::add).whenComplete((_void, error) -> {
if (error != null) {
@@ -818,7 +_,7 @@
}
}
- protected void setServerViewDistance(int viewDistance) {
+ public void setServerViewDistance(int viewDistance) { // Paper - publi
int i = Mth.clamp(viewDistance, 2, 32);
if (i != this.serverViewDistance) {
this.serverViewDistance = i;
@@ -856,7 +_,7 @@
}
public int size() {
- return this.visibleChunkMap.size();
+ return ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
}
public net.minecraft.server.level.DistanceManager getDistanceManager() {
@@ -864,7 +_,7 @@
}
protected Iterable<ChunkHolder> getChunks() {
- return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
+ return Iterables.unmodifiableIterable(ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
}
void dumpChunks(Writer writer) throws IOException {
@@ -888,10 +_,10 @@
.build(writer);
TickingTracker tickingTracker = this.distanceManager.tickingTracker();
- for (Entry<ChunkHolder> entry : this.visibleChunkMap.long2ObjectEntrySet()) {
- long longKey = entry.getLongKey();
+ for (ChunkHolder entry : ca.spottedleaf.moonrise.common.util.ChunkSystem.getVisibleChunkHolders(this.level)) { // Paper - Moonrise
+ long longKey = entry.pos.toLong(); // Paper - Moonrise
ChunkPos chunkPos = new ChunkPos(longKey);
- ChunkHolder chunkHolder = entry.getValue();
+ ChunkHolder chunkHolder = entry; // Paper - Moonrise
Optional<ChunkAccess> optional = Optional.ofNullable(chunkHolder.getLatestChunk());
Optional<LevelChunk> optional1 = optional.flatMap(chunk -> chunk instanceof LevelChunk ? Optional.of((LevelChunk)chunk) : Optional.empty());
csvOutput.writeRow(
@@ -931,11 +_,13 @@
}
private CompletableFuture<Optional<CompoundTag>> readChunk(ChunkPos pos) {
- return this.read(pos).thenApplyAsync(optional -> optional.map(this::upgradeChunkTag), Util.backgroundExecutor().forName("upgradeChunk"));
+ return this.read(pos).thenApplyAsync(optional -> optional.map(tag -> upgradeChunkTag(tag, pos)), Util.backgroundExecutor().forName("upgradeChunk")); // CraftBukkit
}
- private CompoundTag upgradeChunkTag(CompoundTag tag) {
- return this.upgradeChunkTag(this.level.dimension(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer());
+ // CraftBukkit start
+ private CompoundTag upgradeChunkTag(CompoundTag tag, ChunkPos pos) {
+ return this.upgradeChunkTag(this.level.dimension(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer(), pos, this.level);
+ // CraftBukkit end
}
void forEachSpawnCandidateChunk(Consumer<ChunkHolder> action) {
@@ -951,12 +_,34 @@
}
public boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos) {
- return this.distanceManager.hasPlayersNearby(chunkPos.toLong()) && this.anyPlayerCloseEnoughForSpawningInternal(chunkPos);
+ // Spigot start
+ return this.anyPlayerCloseEnoughForSpawning(chunkPos, false);
+ }
+
+ boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos, boolean reducedRange) {
+ return this.distanceManager.hasPlayersNearby(chunkPos.toLong()) && this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, reducedRange);
+ // Spigot end
}
private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos) {
+ // Spigot start
+ return this.anyPlayerCloseEnoughForSpawningInternal(chunkPos, false);
+ }
+
+ private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos, boolean reducedRange) {
+ double blockRange; // Paper - use from event
+ // Spigot end
for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
- if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos)) {
+ // Paper start - PlayerNaturallySpawnCreaturesEvent
+ com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event;
+ blockRange = 16384.0D;
+ if (reducedRange) {
+ event = serverPlayer.playerNaturallySpawnedEvent;
+ if (event == null || event.isCancelled()) continue;
+ blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4));
+ }
+ // Paper end - PlayerNaturallySpawnCreaturesEvent
+ if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos, blockRange)) {
return true;
}
}
@@ -972,7 +_,7 @@
Builder<ServerPlayer> builder = ImmutableList.builder();
for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
- if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos)) {
+ if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos, 16384.0D)) { // Spigot
builder.add(serverPlayer);
}
}
@@ -981,12 +_,12 @@
}
}
- private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos) {
+ private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos, double range) { // Spigot
if (player.isSpectator()) {
return false;
} else {
double d = euclideanDistanceSquared(chunkPos, player);
- return d < 16384.0;
+ return d < range; // Spigot
}
}
@@ -1100,9 +_,19 @@
}
public void addEntity(Entity entity) {
+ org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot
+ // Paper start - ignore and warn about illegal addEntity calls instead of crashing server
+ if (!entity.valid || entity.level() != this.level || this.entityMap.containsKey(entity.getId())) {
+ LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName()
+ + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable());
+ return;
+ }
+ // Paper end - ignore and warn about illegal addEntity calls instead of crashing server
+ if (entity instanceof ServerPlayer && ((ServerPlayer) entity).supressTrackerForLogin) return; // Paper - Fire PlayerJoinEvent when Player is actually ready; Delayadding to tracker until after list packets
if (!(entity instanceof EnderDragonPart)) {
EntityType<?> type = entity.getType();
int i = type.clientTrackingRange() * 16;
+ i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot
if (i != 0) {
int updateInterval = type.updateInterval();
if (this.entityMap.containsKey(entity.getId())) {
@@ -1126,6 +_,7 @@
}
protected void removeEntity(Entity entity) {
+ org.spigotmc.AsyncCatcher.catchOp("entity untrack"); // Spigot
if (entity instanceof ServerPlayer serverPlayer) {
this.updatePlayerStatus(serverPlayer, false);
@@ -1230,7 +_,7 @@
});
}
- class DistanceManager extends net.minecraft.server.level.DistanceManager {
+ public class DistanceManager extends net.minecraft.server.level.DistanceManager { // Paper - public
protected DistanceManager(final Executor dispatcher, final Executor mainThreadExecutor) {
super(dispatcher, mainThreadExecutor);
}
@@ -1258,10 +_,10 @@
final Entity entity;
private final int range;
SectionPos lastSectionPos;
- public final Set<ServerPlayerConnection> seenBy = Sets.newIdentityHashSet();
+ public final Set<ServerPlayerConnection> seenBy = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); // Paper - Perf: optimise map impl
public TrackedEntity(final Entity entity, final int range, final int updateInterval, final boolean trackDelta) {
- this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast);
+ this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast, this.seenBy); // CraftBukkit
this.entity = entity;
this.range = range;
this.lastSectionPos = SectionPos.of(entity);
@@ -1297,24 +_,47 @@
}
public void removePlayer(ServerPlayer player) {
+ org.spigotmc.AsyncCatcher.catchOp("player tracker clear"); // Spigot
if (this.seenBy.remove(player.connection)) {
this.serverEntity.removePairing(player);
}
}
public void updatePlayer(ServerPlayer player) {
+ org.spigotmc.AsyncCatcher.catchOp("player tracker update"); // Spigot
if (player != this.entity) {
- Vec3 vec3 = player.position().subtract(this.entity.position());
+ // Paper start - remove allocation of Vec3D here
+ // Vec3 vec3d = player.position().subtract(this.entity.position());
+ double vec3d_dx = player.getX() - this.entity.getX();
+ double vec3d_dz = player.getZ() - this.entity.getZ();
+ // Paper end - remove allocation of Vec3D here
int playerViewDistance = ChunkMap.this.getPlayerViewDistance(player);
double d = Math.min(this.getEffectiveRange(), playerViewDistance * 16);
- double d1 = vec3.x * vec3.x + vec3.z * vec3.z;
+ double d1 = vec3d_dx * vec3d_dx + vec3d_dz * vec3d_dz; // Paper
double d2 = d * d;
- boolean flag = d1 <= d2
- && this.entity.broadcastToPlayer(player)
- && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
+ // Paper start - Configurable entity tracking range by Y
+ boolean flag = d1 <= d2;
+ if (flag && level.paperConfig().entities.trackingRangeY.enabled) {
+ double rangeY = level.paperConfig().entities.trackingRangeY.get(this.entity, -1);
+ if (rangeY != -1) {
+ double vec3d_dy = player.getY() - this.entity.getY();
+ flag = vec3d_dy * vec3d_dy <= rangeY * rangeY;
+ }
+ }
+ flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
+ // Paper end - Configurable entity tracking range by Y
+ // CraftBukkit start - respect vanish API
+ if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits
+ flag = false;
+ }
+ // CraftBukkit end
if (flag) {
if (this.seenBy.add(player.connection)) {
+ // Paper start - entity tracking events
+ if (io.papermc.paper.event.player.PlayerTrackEntityEvent.getHandlerList().getRegisteredListeners().length == 0 || new io.papermc.paper.event.player.PlayerTrackEntityEvent(player.getBukkitEntity(), this.entity.getBukkitEntity()).callEvent()) {
this.serverEntity.addPairing(player);
+ }
+ // Paper end - entity tracking events
}
} else if (this.seenBy.remove(player.connection)) {
this.serverEntity.removePairing(player);
@@ -1331,6 +_,7 @@
for (Entity entity : this.entity.getIndirectPassengers()) {
int i1 = entity.getType().clientTrackingRange() * 16;
+ i1 = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i1); // Paper
if (i1 > i) {
i = i1;
}

View File

@@ -0,0 +1,128 @@
--- a/net/minecraft/server/level/DistanceManager.java
+++ b/net/minecraft/server/level/DistanceManager.java
@@ -107,6 +_,12 @@
}
if (!this.chunksToUpdateFutures.isEmpty()) {
+ // CraftBukkit start - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus
+ for (final ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
+ chunkHolder.callEventIfUnloading(chunkMap);
+ }
+ // CraftBukkit end - SPIGOT-7780: Call chunk unload events before updateHighestAllowedStatus
+
for (ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
chunkHolder.updateHighestAllowedStatus(chunkMap);
}
@@ -143,7 +_,7 @@
}
}
- void addTicket(long chunkPos, Ticket<?> ticket) {
+ boolean addTicket(long chunkPos, Ticket<?> ticket) { // CraftBukkit - void -> boolean
SortedArraySet<Ticket<?>> tickets = this.getTickets(chunkPos);
int ticketLevelAt = getTicketLevelAt(tickets);
Ticket<?> ticket1 = tickets.addOrGet(ticket);
@@ -151,11 +_,14 @@
if (ticket.getTicketLevel() < ticketLevelAt) {
this.ticketTracker.update(chunkPos, ticket.getTicketLevel(), true);
}
+ return ticket == ticket1; // CraftBukkit
}
- void removeTicket(long chunkPos, Ticket<?> ticket) {
+ boolean removeTicket(long chunkPos, Ticket<?> ticket) { // CraftBukkit - void -> boolean
SortedArraySet<Ticket<?>> tickets = this.getTickets(chunkPos);
+ boolean removed = false; // CraftBukkit
if (tickets.remove(ticket)) {
+ removed = true; // CraftBukkit
}
if (tickets.isEmpty()) {
@@ -163,6 +_,7 @@
}
this.ticketTracker.update(chunkPos, getTicketLevelAt(tickets), false);
+ return removed; // CraftBukkit
}
public <T> void addTicket(TicketType<T> type, ChunkPos pos, int level, T value) {
@@ -175,17 +_,29 @@
}
public <T> void addRegionTicket(TicketType<T> type, ChunkPos pos, int distance, T value) {
+ // CraftBukkit start
+ this.addRegionTicketAtDistance(type, pos, distance, value);
+ }
+ public <T> boolean addRegionTicketAtDistance(TicketType<T> type, ChunkPos pos, int distance, T value) {
+ // CraftBukkit end
Ticket<T> ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value);
long packedChunkPos = pos.toLong();
- this.addTicket(packedChunkPos, ticket);
+ final boolean addded = this.addTicket(packedChunkPos, ticket); // CraftBukkit
this.tickingTicketsTracker.addTicket(packedChunkPos, ticket);
+ return addded; // CraftBukkit
}
public <T> void removeRegionTicket(TicketType<T> type, ChunkPos pos, int distance, T value) {
+ // CraftBukkit start
+ removeRegionTicketAtDistance(type, pos, distance, value);
+ }
+ public <T> boolean removeRegionTicketAtDistance(TicketType<T> type, ChunkPos pos, int distance, T value) {
+ // CraftBukkit end
Ticket<T> ticket = new Ticket<>(type, ChunkLevel.byStatus(FullChunkStatus.FULL) - distance, value);
long packedChunkPos = pos.toLong();
- this.removeTicket(packedChunkPos, ticket);
+ final boolean removed = this.removeTicket(packedChunkPos, ticket); // CraftBukkit
this.tickingTicketsTracker.removeTicket(packedChunkPos, ticket);
+ return removed; // CraftBukkit
}
private SortedArraySet<Ticket<?>> getTickets(long chunkPos) {
@@ -217,8 +_,9 @@
ChunkPos chunkPos = sectionPos.chunk();
long packedChunkPos = chunkPos.toLong();
ObjectSet<ServerPlayer> set = this.playersPerChunk.get(packedChunkPos);
- set.remove(player);
- if (set.isEmpty()) {
+ if (set == null) return; // CraftBukkit - SPIGOT-6208
+ if (set != null) set.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully
+ if (set == null || set.isEmpty()) { // Paper
this.playersPerChunk.remove(packedChunkPos);
this.naturalSpawnChunkCounter.update(packedChunkPos, Integer.MAX_VALUE, false);
this.playerTicketManager.update(packedChunkPos, Integer.MAX_VALUE, false);
@@ -299,7 +_,7 @@
}
public void removeTicketsOnClosing() {
- ImmutableSet<TicketType<?>> set = ImmutableSet.of(TicketType.UNKNOWN);
+ ImmutableSet<TicketType<?>> set = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectIterator = this.tickets.long2ObjectEntrySet().fastIterator();
while (objectIterator.hasNext()) {
@@ -329,6 +_,26 @@
public boolean hasTickets() {
return !this.tickets.isEmpty();
}
+
+ // CraftBukkit start
+ public <T> void removeAllTicketsFor(TicketType<T> ticketType, int ticketLevel, T ticketIdentifier) {
+ Ticket<T> target = new Ticket<>(ticketType, ticketLevel, ticketIdentifier);
+
+ for (java.util.Iterator<Entry<SortedArraySet<Ticket<?>>>> iterator = this.tickets.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+ Entry<SortedArraySet<Ticket<?>>> entry = iterator.next();
+ SortedArraySet<Ticket<?>> tickets = entry.getValue();
+ if (tickets.remove(target)) {
+ // copied from removeTicket
+ this.ticketTracker.update(entry.getLongKey(), DistanceManager.getTicketLevelAt(tickets), false);
+
+ // can't use entry after it's removed
+ if (tickets.isEmpty()) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+ // CraftBukkit end
class ChunkTicketTracker extends ChunkTracker {
private static final int MAX_LEVEL = ChunkLevel.MAX_LEVEL + 1;

View File

@@ -0,0 +1,240 @@
--- a/net/minecraft/server/level/ServerChunkCache.java
+++ b/net/minecraft/server/level/ServerChunkCache.java
@@ -73,6 +_,13 @@
@Nullable
@VisibleForDebug
private NaturalSpawner.SpawnState lastSpawnState;
+ // Paper start
+ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
+ public int getFullChunksCount() {
+ return this.fullChunks.size();
+ }
+ long chunkFutureAwaitCounter;
+ // Paper end
public ServerChunkCache(
ServerLevel level,
@@ -121,6 +_,64 @@
this.clearCache();
}
+ // CraftBukkit start - properly implement isChunkLoaded
+ public boolean isChunkLoaded(int chunkX, int chunkZ) {
+ ChunkHolder chunk = this.chunkMap.getUpdatingChunkIfPresent(ChunkPos.asLong(chunkX, chunkZ));
+ if (chunk == null) {
+ return false;
+ }
+ return chunk.getFullChunkNow() != null;
+ }
+ // CraftBukkit end
+ // Paper start
+ public void addLoadedChunk(LevelChunk chunk) {
+ this.fullChunks.put(chunk.coordinateKey, chunk);
+ }
+
+ public void removeLoadedChunk(LevelChunk chunk) {
+ this.fullChunks.remove(chunk.coordinateKey);
+ }
+
+ @Nullable
+ public ChunkAccess getChunkAtImmediately(int x, int z) {
+ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
+ if (holder == null) {
+ return null;
+ }
+
+ return holder.getLatestChunk();
+ }
+
+ public <T> void addTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
+ this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier);
+ }
+
+ public <T> void removeTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
+ this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier);
+ }
+
+ // "real" get chunk if loaded
+ // Note: Partially copied from the getChunkAt method below
+ @Nullable
+ public LevelChunk getChunkAtIfCachedImmediately(int x, int z) {
+ long k = ChunkPos.asLong(x, z);
+
+ // Note: Bypass cache since we need to check ticket level, and to make this MT-Safe
+
+ ChunkHolder playerChunk = this.getVisibleChunkIfPresent(k);
+ if (playerChunk == null) {
+ return null;
+ }
+
+ return playerChunk.getFullChunkNowUnchecked();
+ }
+
+ @Nullable
+ public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) {
+ return this.fullChunks.get(ChunkPos.asLong(x, z));
+ }
+ // Paper end
+
@Override
public ThreadedLevelLightEngine getLightEngine() {
return this.lightEngine;
@@ -160,7 +_,7 @@
for (int i = 0; i < 4; i++) {
if (packedChunkPos == this.lastChunkPos[i] && chunkStatus == this.lastChunkStatus[i]) {
ChunkAccess chunkAccess = this.lastChunk[i];
- if (chunkAccess != null || !requireChunk) {
+ if (chunkAccess != null) { // CraftBukkit - the chunk can become accessible in the meantime TODO for non-null chunks it might also make sense to check that the chunk's state hasn't changed in the meantime
return chunkAccess;
}
}
@@ -169,6 +_,7 @@
profilerFiller.incrementCounter("getChunkCacheMiss");
CompletableFuture<ChunkResult<ChunkAccess>> chunkFutureMainThread = this.getChunkFutureMainThread(x, z, chunkStatus, requireChunk);
this.mainThreadProcessor.managedBlock(chunkFutureMainThread::isDone);
+ // com.destroystokyo.paper.io.SyncLoadFinder.logSyncLoad(this.level, x, z); // Paper - Add debug for sync chunk loads
ChunkResult<ChunkAccess> chunkResult = chunkFutureMainThread.join();
ChunkAccess chunkAccess1 = chunkResult.orElse(null);
if (chunkAccess1 == null && requireChunk) {
@@ -240,7 +_,15 @@
long packedChunkPos = chunkPos.toLong();
int i = ChunkLevel.byStatus(chunkStatus);
ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(packedChunkPos);
- if (requireChunk) {
+ // CraftBukkit start - don't add new ticket for currently unloading chunk
+ boolean currentlyUnloading = false;
+ if (visibleChunkIfPresent != null) {
+ FullChunkStatus oldChunkState = ChunkLevel.fullStatus(visibleChunkIfPresent.oldTicketLevel);
+ FullChunkStatus currentChunkState = ChunkLevel.fullStatus(visibleChunkIfPresent.getTicketLevel());
+ currentlyUnloading = (oldChunkState.isOrAfter(FullChunkStatus.FULL) && !currentChunkState.isOrAfter(FullChunkStatus.FULL));
+ }
+ if (requireChunk && !currentlyUnloading) {
+ // CraftBukkit end
this.distanceManager.addTicket(TicketType.UNKNOWN, chunkPos, i, chunkPos);
if (this.chunkAbsent(visibleChunkIfPresent, i)) {
ProfilerFiller profilerFiller = Profiler.get();
@@ -260,7 +_,7 @@
}
private boolean chunkAbsent(@Nullable ChunkHolder chunkHolder, int status) {
- return chunkHolder == null || chunkHolder.getTicketLevel() > status;
+ return chunkHolder == null || chunkHolder.oldTicketLevel > status; // CraftBukkit using oldTicketLevel for isLoaded checks
}
@Override
@@ -287,7 +_,7 @@
return this.mainThreadProcessor.pollTask();
}
- boolean runDistanceManagerUpdates() {
+ public boolean runDistanceManagerUpdates() { // Paper - public
boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
boolean flag1 = this.chunkMap.promoteChunkMap();
this.chunkMap.runGenerationTasks();
@@ -315,17 +_,38 @@
@Override
public void close() throws IOException {
- this.save(true);
+ this.close(true);
+ }
+
+ public void close(boolean save) throws IOException {
+ if (save) {
+ this.save(true);
+ }
+ // CraftBukkit end
this.dataStorage.close();
this.lightEngine.close();
this.chunkMap.close();
}
+ // CraftBukkit start - modelled on below
+ public void purgeUnload() {
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.push("purge");
+ this.distanceManager.purgeStaleTickets();
+ this.runDistanceManagerUpdates();
+ gameprofilerfiller.popPush("unload");
+ this.chunkMap.tick(() -> true);
+ gameprofilerfiller.pop();
+ this.clearCache();
+ }
+ // CraftBukkit end
+
@Override
public void tick(BooleanSupplier hasTimeLeft, boolean tickChunks) {
ProfilerFiller profilerFiller = Profiler.get();
profilerFiller.push("purge");
- if (this.level.tickRateManager().runsNormally() || !tickChunks) {
+ if (this.level.tickRateManager().runsNormally() || !tickChunks || this.level.spigotConfig.unloadFrozenChunks) { // Spigot
this.distanceManager.purgeStaleTickets();
}
@@ -400,11 +_,19 @@
);
this.lastSpawnState = spawnState;
profiler.popPush("spawnAndTick");
- boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING);
+ boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit;
int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
List<MobCategory> filteredSpawningCategories;
if (_boolean && (this.spawnEnemies || this.spawnFriendlies)) {
- boolean flag = this.level.getLevelData().getGameTime() % 400L == 0L;
+ // Paper start - PlayerNaturallySpawnCreaturesEvent
+ for (ServerPlayer entityPlayer : this.level.players()) {
+ int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance());
+ chunkRange = Math.min(chunkRange, 8);
+ entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange);
+ entityPlayer.playerNaturallySpawnedEvent.callEvent();
+ }
+ // Paper end - PlayerNaturallySpawnCreaturesEvent
+ boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit
filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag);
} else {
filteredSpawningCategories = List.of();
@@ -413,7 +_,7 @@
for (LevelChunk levelChunk : chunks) {
ChunkPos pos = levelChunk.getPos();
levelChunk.incrementInhabitedTime(timeInhabited);
- if (!filteredSpawningCategories.isEmpty() && this.level.getWorldBorder().isWithinBounds(pos)) {
+ if (!filteredSpawningCategories.isEmpty() && this.level.getWorldBorder().isWithinBounds(pos) && this.chunkMap.anyPlayerCloseEnoughForSpawning(pos, true)) { // Spigot
NaturalSpawner.spawnForChunk(this.level, levelChunk, spawnState, filteredSpawningCategories);
}
@@ -526,8 +_,13 @@
@Override
public void setSpawnSettings(boolean spawnSettings) {
- this.spawnEnemies = spawnSettings;
- this.spawnFriendlies = this.spawnFriendlies;
+ // CraftBukkit start
+ this.setSpawnSettings(spawnSettings, this.spawnFriendlies);
+ }
+ public void setSpawnSettings(boolean spawnEnemies, boolean spawnFriendlies) {
+ this.spawnEnemies = spawnEnemies;
+ this.spawnFriendlies = spawnFriendlies;
+ // CraftBukkit end
}
public String getChunkDebugData(ChunkPos chunkPos) {
@@ -603,12 +_,18 @@
@Override
public boolean pollTask() {
+ try { // CraftBukkit - process pending Chunk loadCallback() and unloadCallback() after each run task
if (ServerChunkCache.this.runDistanceManagerUpdates()) {
return true;
} else {
ServerChunkCache.this.lightEngine.tryScheduleUpdate();
return super.pollTask();
}
+ // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
+ } finally {
+ ServerChunkCache.this.chunkMap.callbackExecutor.run();
+ }
+ // CraftBukkit end - process pending Chunk loadCallback() and unloadCallback() after each run task
}
}
}

View File

@@ -0,0 +1,134 @@
--- a/net/minecraft/server/level/ServerEntity.java
+++ b/net/minecraft/server/level/ServerEntity.java
@@ -65,13 +_,17 @@
private Vec3 lastSentMovement;
private int tickCount;
private int teleportDelay;
- private List<Entity> lastPassengers = Collections.emptyList();
+ private List<Entity> lastPassengers = com.google.common.collect.ImmutableList.of(); // Paper - optimize passenger checks
private boolean wasRiding;
private boolean wasOnGround;
@Nullable
private List<SynchedEntityData.DataValue<?>> trackedDataValues;
- public ServerEntity(ServerLevel level, Entity entity, int updateInterval, boolean trackDelta, Consumer<Packet<?>> broadcast) {
+ // CraftBukkit start
+ private final Set<net.minecraft.server.network.ServerPlayerConnection> trackedPlayers;
+ public ServerEntity(ServerLevel level, Entity entity, int updateInterval, boolean trackDelta, Consumer<Packet<?>> broadcast, final Set<net.minecraft.server.network.ServerPlayerConnection> trackedPlayers) {
+ this.trackedPlayers = trackedPlayers;
+ // CraftBukkit end
this.level = level;
this.broadcast = broadcast;
this.entity = entity;
@@ -89,7 +_,7 @@
public void sendChanges() {
List<Entity> passengers = this.entity.getPassengers();
if (!passengers.equals(this.lastPassengers)) {
- this.broadcast.accept(new ClientboundSetPassengersPacket(this.entity));
+ this.broadcastAndSend(new ClientboundSetPassengersPacket(this.entity)); // CraftBukkit
removedPassengers(passengers, this.lastPassengers)
.forEach(
removedPassenger -> {
@@ -102,10 +_,10 @@
this.lastPassengers = passengers;
}
- if (this.entity instanceof ItemFrame itemFrame && this.tickCount % 10 == 0) {
+ if (!this.trackedPlayers.isEmpty() && this.entity instanceof ItemFrame itemFrame /*&& this.tickCount % 10 == 0*/) { // CraftBukkit - moved tickCount below // Paper - Perf: Only tick item frames if players can see it
ItemStack item = itemFrame.getItem();
- if (item.getItem() instanceof MapItem) {
- MapId mapId = item.get(DataComponents.MAP_ID);
+ if (this.level.paperConfig().maps.itemFrameCursorUpdateInterval > 0 && this.tickCount % this.level.paperConfig().maps.itemFrameCursorUpdateInterval == 0 && item.getItem() instanceof MapItem) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks // Paper - Make item frame map cursor update interval configurable
+ MapId mapId = itemFrame.cachedMapId; // Paper - Perf: Cache map ids on item frames
MapItemSavedData savedData = MapItem.getSavedData(mapId, this.level);
if (savedData != null) {
for (ServerPlayer serverPlayer : this.level.players()) {
@@ -141,7 +_,13 @@
} else {
this.teleportDelay++;
Vec3 vec3 = this.entity.trackingPosition();
- boolean flag1 = this.positionCodec.delta(vec3).lengthSqr() >= 7.6293945E-6F;
+ // Paper start - reduce allocation of Vec3D here
+ Vec3 base = this.positionCodec.base;
+ double vec3_dx = vec3.x - base.x;
+ double vec3_dy = vec3.y - base.y;
+ double vec3_dz = vec3.z - base.z;
+ boolean flag1 = (vec3_dx * vec3_dx + vec3_dy * vec3_dy + vec3_dz * vec3_dz) >= 7.62939453125E-6D;
+ // Paper end - reduce allocation of Vec3D here
Packet<?> packet = null;
boolean flag2 = flag1 || this.tickCount % 60 == 0;
boolean flag3 = false;
@@ -219,6 +_,25 @@
this.tickCount++;
if (this.entity.hurtMarked) {
+ // CraftBukkit start - Create PlayerVelocity event
+ boolean cancelled = false;
+
+ if (this.entity instanceof ServerPlayer) {
+ org.bukkit.entity.Player player = (org.bukkit.entity.Player) this.entity.getBukkitEntity();
+ org.bukkit.util.Vector velocity = player.getVelocity();
+
+ org.bukkit.event.player.PlayerVelocityEvent event = new org.bukkit.event.player.PlayerVelocityEvent(player, velocity.clone());
+ if (!event.callEvent()) {
+ cancelled = true;
+ } else if (!velocity.equals(event.getVelocity())) {
+ player.setVelocity(event.getVelocity());
+ }
+ }
+
+ if (cancelled) {
+ return;
+ }
+ // CraftBukkit end
this.entity.hurtMarked = false;
this.broadcastAndSend(new ClientboundSetEntityMotionPacket(this.entity));
}
@@ -273,7 +_,10 @@
public void sendPairingData(ServerPlayer player, Consumer<Packet<ClientGamePacketListener>> consumer) {
if (this.entity.isRemoved()) {
- LOGGER.warn("Fetching packet for removed entity {}", this.entity);
+ // CraftBukkit start - Remove useless error spam, just return
+ // LOGGER.warn("Fetching packet for removed entity {}", this.entity);
+ return;
+ // CraftBukkit end
}
Packet<ClientGamePacketListener> addEntityPacket = this.entity.getAddEntityPacket(this);
@@ -285,6 +_,12 @@
boolean flag = this.trackDelta;
if (this.entity instanceof LivingEntity) {
Collection<AttributeInstance> syncableAttributes = ((LivingEntity)this.entity).getAttributes().getSyncableAttributes();
+
+ // CraftBukkit start - If sending own attributes send scaled health instead of current maximum health
+ if (this.entity.getId() == player.getId()) {
+ ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(syncableAttributes, false);
+ }
+ // CraftBukkit end
if (!syncableAttributes.isEmpty()) {
consumer.accept(new ClientboundUpdateAttributesPacket(this.entity.getId(), syncableAttributes));
}
@@ -309,8 +_,9 @@
}
if (!list.isEmpty()) {
- consumer.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list));
+ consumer.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list, true)); // Paper - data sanitization
}
+ ((LivingEntity) this.entity).detectEquipmentUpdatesPublic(); // CraftBukkit - SPIGOT-3789: sync again immediately after sending
}
if (!this.entity.getPassengers().isEmpty()) {
@@ -357,6 +_,11 @@
if (this.entity instanceof LivingEntity) {
Set<AttributeInstance> attributesToSync = ((LivingEntity)this.entity).getAttributes().getAttributesToSync();
if (!attributesToSync.isEmpty()) {
+ // CraftBukkit start - Send scaled max health
+ if (this.entity instanceof ServerPlayer) {
+ ((ServerPlayer) this.entity).getBukkitEntity().injectScaledMaxHealth(attributesToSync, false);
+ }
+ // CraftBukkit end
this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), attributesToSync));
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,436 @@
--- a/net/minecraft/server/level/ServerPlayerGameMode.java
+++ b/net/minecraft/server/level/ServerPlayerGameMode.java
@@ -13,6 +_,7 @@
import net.minecraft.world.InteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.item.DoubleHighBlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
@@ -41,6 +_,8 @@
private BlockPos delayedDestroyPos = BlockPos.ZERO;
private int delayedTickStart;
private int lastSentState = -1;
+ public boolean captureSentBlockEntities = false; // Paper - Send block entities after destroy prediction
+ public boolean capturedBlockEntity = false; // Paper - Send block entities after destroy prediction
public ServerPlayerGameMode(ServerPlayer player) {
this.player = player;
@@ -48,21 +_,39 @@
}
public boolean changeGameModeForPlayer(GameType gameModeForPlayer) {
+ // Paper start - Expand PlayerGameModeChangeEvent
+ org.bukkit.event.player.PlayerGameModeChangeEvent event = this.changeGameModeForPlayer(gameModeForPlayer, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.UNKNOWN, null);
+ return event != null && event.isCancelled();
+ }
+ @Nullable
+ public org.bukkit.event.player.PlayerGameModeChangeEvent changeGameModeForPlayer(GameType gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause playerGameModeChangeCause, @Nullable net.kyori.adventure.text.Component cancelMessage) {
+ // Paper end - Expand PlayerGameModeChangeEvent
if (gameModeForPlayer == this.gameModeForPlayer) {
- return false;
+ return null; // Paper - Expand PlayerGameModeChangeEvent
} else {
- this.setGameModeForPlayer(gameModeForPlayer, this.previousGameModeForPlayer);
+ // CraftBukkit start
+ org.bukkit.event.player.PlayerGameModeChangeEvent event = new org.bukkit.event.player.PlayerGameModeChangeEvent(
+ this.player.getBukkitEntity(),
+ org.bukkit.GameMode.getByValue(gameMode.getId()),
+ playerGameModeChangeCause, // Paper
+ cancelMessage
+ );
+ if (!event.callEvent()) {
+ return event; // Paper - Expand PlayerGameModeChangeEvent
+ }
+ // CraftBukkit end
+ this.setGameModeForPlayer(gameMode, this.gameModeForPlayer); // Paper - Fix MC-259571
this.player.onUpdateAbilities();
this.player
.server
.getPlayerList()
- .broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, this.player));
+ .broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, this.player), this.player); // CraftBukkit
this.level.updateSleepingPlayerList();
if (gameModeForPlayer == GameType.CREATIVE) {
this.player.resetCurrentImpulseContext();
}
- return true;
+ return event; // Paper - Expand PlayerGameModeChangeEvent
}
}
@@ -90,10 +_,10 @@
}
public void tick() {
- this.gameTicks++;
+ this.gameTicks = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit;
if (this.hasDelayedDestroy) {
- BlockState blockState = this.level.getBlockState(this.delayedDestroyPos);
- if (blockState.isAir()) {
+ BlockState blockState = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks
+ if (blockState == null || blockState.isAir()) { // Paper - Don't allow digging into unloaded chunks
this.hasDelayedDestroy = false;
} else {
float f = this.incrementDestroyProgress(blockState, this.delayedDestroyPos, this.delayedTickStart);
@@ -103,7 +_,13 @@
}
}
} else if (this.isDestroyingBlock) {
- BlockState blockState = this.level.getBlockState(this.destroyPos);
+ // Paper start - Don't allow digging into unloaded chunks; don't want to do same logic as above, return instead
+ BlockState blockState = this.level.getBlockStateIfLoaded(this.destroyPos);
+ if (blockState == null) {
+ this.isDestroyingBlock = false;
+ return;
+ }
+ // Paper end - Don't allow digging into unloaded chunks
if (blockState.isAir()) {
this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
this.lastSentState = -1;
@@ -131,6 +_,7 @@
public void handleBlockBreakAction(BlockPos pos, ServerboundPlayerActionPacket.Action action, Direction face, int maxBuildHeight, int sequence) {
if (!this.player.canInteractWithBlock(pos, 1.0)) {
+ if (true) return; // Paper - Don't allow digging into unloaded chunks; Don't notify if unreasonably far away
this.debugLogging(pos, false, sequence, "too far");
} else if (pos.getY() > maxBuildHeight) {
this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
@@ -138,16 +_,40 @@
} else {
if (action == ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK) {
if (!this.level.mayInteract(this.player, pos)) {
+ // CraftBukkit start - fire PlayerInteractEvent
+ org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(this.player, org.bukkit.event.block.Action.LEFT_CLICK_BLOCK, pos, face, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
this.debugLogging(pos, false, sequence, "may not interact");
- return;
- }
+ // Update any tile entity data for this block
+ capturedBlockEntity = true; // Paper - Send block entities after destroy prediction
+ // CraftBukkit end
+ return;
+ }
+
+ // CraftBukkit start
+ org.bukkit.event.player.PlayerInteractEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(this.player, org.bukkit.event.block.Action.LEFT_CLICK_BLOCK, pos, face, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
+ if (event.isCancelled()) {
+ // Let the client know the block still exists
+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks
+ // Update any tile entity data for this block
+ capturedBlockEntity = true; // Paper - Send block entities after destroy prediction
+ return;
+ }
+ // CraftBukkit end
if (this.isCreative()) {
this.destroyAndAck(pos, sequence, "creative destroy");
return;
}
+ // Spigot start - handle debug stick left click for non-creative
+ if (this.player.getMainHandItem().is(net.minecraft.world.item.Items.DEBUG_STICK)
+ && ((net.minecraft.world.item.DebugStickItem) net.minecraft.world.item.Items.DEBUG_STICK).handleInteraction(this.player, this.level.getBlockState(pos), this.level, pos, false, this.player.getMainHandItem())) {
+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync block
+ return;
+ }
+ // Spigot end
+
if (this.player.blockActionRestricted(this.level, pos, this.gameModeForPlayer)) {
this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
this.debugLogging(pos, false, sequence, "block action restricted");
@@ -157,7 +_,21 @@
this.destroyProgressStart = this.gameTicks;
float f = 1.0F;
BlockState blockState = this.level.getBlockState(pos);
- if (!blockState.isAir()) {
+ // CraftBukkit start - Swings at air do *NOT* exist.
+ if (event.useInteractedBlock() == org.bukkit.event.Event.Result.DENY) {
+ // If we denied a door from opening, we need to send a correcting update to the client, as it already opened the door.
+ // Paper start - Don't resync blocks
+ //BlockState data = this.level.getBlockState(pos);
+ //if (data.getBlock() instanceof DoorBlock) {
+ // // For some reason *BOTH* the bottom/top part have to be marked updated.
+ // boolean bottom = data.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER;
+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos));
+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, bottom ? pos.above() : pos.below()));
+ //} else if (data.getBlock() instanceof TrapDoorBlock) {
+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos));
+ //}
+ // Paper end - Don't resync blocks
+ } else if (!blockState.isAir()) {
EnchantmentHelper.onHitBlock(
this.level,
this.player.getMainHandItem(),
@@ -172,6 +_,26 @@
f = blockState.getDestroyProgress(this.player, this.player.level(), pos);
}
+ if (event.useItemInHand() == org.bukkit.event.Event.Result.DENY) {
+ // If we 'insta destroyed' then the client needs to be informed.
+ if (f > 1.0f) {
+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync blocks
+ }
+ return;
+ }
+ org.bukkit.event.block.BlockDamageEvent blockEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDamageEvent(this.player, pos, face, this.player.getInventory().getSelected(), f >= 1.0f); // Paper - Add BlockFace to BlockDamageEvent
+
+ if (blockEvent.isCancelled()) {
+ // Let the client know the block still exists
+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos)); // Paper - Don't resync block
+ return;
+ }
+
+ if (blockEvent.getInstaBreak()) {
+ f = 2.0f;
+ }
+ // CraftBukkit end
+
if (!blockState.isAir() && f >= 1.0F) {
this.destroyAndAck(pos, sequence, "insta mine");
} else {
@@ -212,14 +_,18 @@
this.debugLogging(pos, true, sequence, "stopped destroying");
} else if (action == ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK) {
this.isDestroyingBlock = false;
- if (!Objects.equals(this.destroyPos, pos)) {
- LOGGER.warn("Mismatch in destroy block pos: {} {}", this.destroyPos, pos);
- this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
- this.debugLogging(pos, true, sequence, "aborted mismatched destroying");
+ if (!Objects.equals(this.destroyPos, pos) && !BlockPos.ZERO.equals(this.destroyPos)) { // Paper
+ ServerPlayerGameMode.LOGGER.debug("Mismatch in destroy block pos: {} {}", this.destroyPos, pos); // CraftBukkit - SPIGOT-5457 sent by client when interact event cancelled
+ BlockState type = this.level.getBlockStateIfLoaded(this.destroyPos); // Paper - don't load unloaded chunks for stale records here
+ if (type != null) this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
+ if (type != null) this.debugLogging(pos, true, sequence, "aborted mismatched destroying");
+ this.destroyPos = BlockPos.ZERO; // Paper
}
this.level.destroyBlockProgress(this.player.getId(), pos, -1);
this.debugLogging(pos, true, sequence, "aborted destroying");
+
+ org.bukkit.craftbukkit.event.CraftEventFactory.callBlockDamageAbortEvent(this.player, pos, this.player.getInventory().getSelected()); // CraftBukkit
}
}
}
@@ -235,36 +_,125 @@
public boolean destroyBlock(BlockPos pos) {
BlockState blockState = this.level.getBlockState(pos);
- if (!this.player.getMainHandItem().getItem().canAttackBlock(blockState, this.level, pos, this.player)) {
+ // CraftBukkit start - fire BlockBreakEvent
+ org.bukkit.block.Block bblock = org.bukkit.craftbukkit.block.CraftBlock.at(this.level, pos);
+ org.bukkit.event.block.BlockBreakEvent event = null;
+ if (this.player instanceof ServerPlayer) {
+ // Sword + Creative mode pre-cancel
+ boolean isSwordNoBreak = !this.player.getMainHandItem().getItem().canAttackBlock(blockState, this.level, pos, this.player);
+
+ // Tell client the block is gone immediately then process events
+ // Don't tell the client if its a creative sword break because its not broken!
+ if (false && this.level.getBlockEntity(pos) == null && !isSwordNoBreak) { // Paper - Don't resync block
+ ClientboundBlockUpdatePacket packet = new ClientboundBlockUpdatePacket(pos, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState());
+ this.player.connection.send(packet);
+ }
+
+ event = new org.bukkit.event.block.BlockBreakEvent(bblock, this.player.getBukkitEntity());
+
+ // Sword + Creative mode pre-cancel
+ event.setCancelled(isSwordNoBreak);
+
+ // Calculate default block experience
+ BlockState nmsData = this.level.getBlockState(pos);
+ Block nmsBlock = nmsData.getBlock();
+
+ ItemStack itemstack = this.player.getItemBySlot(EquipmentSlot.MAINHAND);
+
+ if (nmsBlock != null && !event.isCancelled() && !this.isCreative() && this.player.hasCorrectToolForDrops(nmsBlock.defaultBlockState())) {
+ event.setExpToDrop(nmsBlock.getExpDrop(nmsData, this.level, pos, itemstack, true));
+ }
+
+ this.level.getCraftServer().getPluginManager().callEvent(event);
+
+ if (event.isCancelled()) {
+ if (isSwordNoBreak) {
+ return false;
+ }
+ // Paper start - Don't resync blocks
+ // Let the client know the block still exists
+ //this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos));
+
+ // Brute force all possible updates
+ //for (Direction dir : Direction.values()) {
+ // this.player.connection.send(new ClientboundBlockUpdatePacket(this.level, pos.relative(dir)));
+ //}
+ // Paper end - Don't resync blocks
+
+ // Update any tile entity data for this block
+ if (!captureSentBlockEntities) { // Paper - Send block entities after destroy prediction
+ BlockEntity tileentity = this.level.getBlockEntity(pos);
+ if (tileentity != null) {
+ this.player.connection.send(tileentity.getUpdatePacket());
+ }
+ } else {capturedBlockEntity = true;} // Paper - Send block entities after destroy prediction
+ return false;
+ }
+ }
+ // CraftBukkit end
+
+ if (false && !this.player.getMainHandItem().getItem().canAttackBlock(blockState, this.level, pos, this.player)) { // CraftBukkit - false
return false;
} else {
+ blockState = this.level.getBlockState(pos); // CraftBukkit - update state from plugins
+ if (blockState.isAir()) return false; // CraftBukkit - A plugin set block to air without cancelling
BlockEntity blockEntity = this.level.getBlockEntity(pos);
Block block = blockState.getBlock();
- if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks()) {
+ if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks() && !(block instanceof net.minecraft.world.level.block.CommandBlock && (this.player.isCreative() && this.player.getBukkitEntity().hasPermission("minecraft.commandblock")))) { // Paper - command block permission
this.level.sendBlockUpdated(pos, blockState, blockState, 3);
return false;
} else if (this.player.blockActionRestricted(this.level, pos, this.gameModeForPlayer)) {
return false;
} else {
+ // CraftBukkit start
+ org.bukkit.block.BlockState state = bblock.getState();
+ this.level.captureDrops = new java.util.ArrayList<>();
+ // CraftBukkit end
BlockState blockState1 = block.playerWillDestroy(this.level, pos, blockState, this.player);
boolean flag = this.level.removeBlock(pos, false);
if (flag) {
block.destroy(this.level, pos, blockState1);
}
+ ItemStack mainHandStack = null; // Paper - Trigger bee_nest_destroyed trigger in the correct place
+ boolean isCorrectTool = false; // Paper - Trigger bee_nest_destroyed trigger in the correct place
if (this.isCreative()) {
- return true;
+ // return true; // CraftBukkit
} else {
ItemStack mainHandItem = this.player.getMainHandItem();
ItemStack itemStack = mainHandItem.copy();
boolean hasCorrectToolForDrops = this.player.hasCorrectToolForDrops(blockState1);
+ mainHandStack = itemStack; // Paper - Trigger bee_nest_destroyed trigger in the correct place
+ isCorrectTool = hasCorrectToolForDrops; // Paper - Trigger bee_nest_destroyed trigger in the correct place
mainHandItem.mineBlock(this.level, blockState1, pos, this.player);
- if (flag && hasCorrectToolForDrops) {
- block.playerDestroy(this.level, this.player, pos, blockState1, blockEntity, itemStack);
- }
-
- return true;
- }
+ if (flag && hasCorrectToolForDrops/* && event.isDropItems() */) { // CraftBukkit - Check if block should drop items // Paper - fix drops not preventing stats/food exhaustion
+ block.playerDestroy(this.level, this.player, pos, blockState1, blockEntity, itemStack, event.isDropItems(), false); // Paper - fix drops not preventing stats/food exhaustion
+ }
+
+ // return true; // CraftBukkit
+ }
+ // CraftBukkit start
+ java.util.List<net.minecraft.world.entity.item.ItemEntity> itemsToDrop = this.level.captureDrops; // Paper - capture all item additions to the world
+ this.level.captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff
+ if (event.isDropItems()) {
+ org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - capture all item additions to the world
+ }
+ //this.level.captureDrops = null; // Paper - capture all item additions to the world; move up
+
+ // Drop event experience
+ if (flag && event != null) {
+ blockState.getBlock().popExperience(this.level, pos, event.getExpToDrop(), this.player); // Paper
+ }
+ // Paper start - Trigger bee_nest_destroyed trigger in the correct place (check impls of block#playerDestroy)
+ if (mainHandStack != null) {
+ if (flag && isCorrectTool && event.isDropItems() && block instanceof net.minecraft.world.level.block.BeehiveBlock && blockEntity instanceof net.minecraft.world.level.block.entity.BeehiveBlockEntity beehiveBlockEntity) { // simulates the guard on block#playerDestroy above
+ CriteriaTriggers.BEE_NEST_DESTROYED.trigger(player, blockState, mainHandStack, beehiveBlockEntity.getOccupantCount());
+ }
+ }
+ // Paper end - Trigger bee_nest_destroyed trigger in the correct place
+
+ return true;
+ // CraftBukkit end
}
}
}
@@ -307,15 +_,61 @@
}
}
+ // CraftBukkit start - whole method
+ public boolean interactResult = false;
+ public boolean firedInteract = false;
+ public BlockPos interactPosition;
+ public InteractionHand interactHand;
+ public ItemStack interactItemStack;
public InteractionResult useItemOn(ServerPlayer player, Level level, ItemStack stack, InteractionHand hand, BlockHitResult hitResult) {
BlockPos blockPos = hitResult.getBlockPos();
BlockState blockState = level.getBlockState(blockPos);
+ boolean cancelledBlock = false;
+ boolean cancelledItem = false; // Paper - correctly handle items on cooldown
if (!blockState.getBlock().isEnabled(level.enabledFeatures())) {
return InteractionResult.FAIL;
} else if (this.gameModeForPlayer == GameType.SPECTATOR) {
MenuProvider menuProvider = blockState.getMenuProvider(level, blockPos);
- if (menuProvider != null) {
- player.openMenu(menuProvider);
+ cancelledBlock = !(menuProvider instanceof MenuProvider);
+ }
+
+ if (player.getCooldowns().isOnCooldown(stack)) {
+ cancelledItem = true; // Paper - correctly handle items on cooldown
+ }
+ org.bukkit.event.player.PlayerInteractEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerInteractEvent(player, org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK, blockPos, hitResult.getDirection(), stack, cancelledBlock, cancelledItem, hand, hitResult.getLocation()); // Paper - correctly handle items on cooldown
+ this.firedInteract = true;
+ this.interactResult = event.useItemInHand() == org.bukkit.event.Event.Result.DENY;
+ this.interactPosition = blockPos.immutable();
+ this.interactHand = hand;
+ this.interactItemStack = stack.copy();
+
+ if (event.useInteractedBlock() == org.bukkit.event.Event.Result.DENY) {
+ // If we denied a door from opening, we need to send a correcting update to the client, as it already opened the door.
+ if (blockState.getBlock() instanceof net.minecraft.world.level.block.DoorBlock) {
+ // Paper start - Don't resync blocks
+ // boolean bottom = iblockdata.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER;
+ // player.connection.send(new ClientboundBlockUpdatePacket(world, bottom ? blockposition.above() : blockposition.below()));
+ // Paper end - Don't resync blocks
+ } else if (blockState.getBlock() instanceof net.minecraft.world.level.block.CakeBlock) {
+ player.getBukkitEntity().sendHealthUpdate(); // SPIGOT-1341 - reset health for cake
+ } else if (this.interactItemStack.getItem() instanceof DoubleHighBlockItem) {
+ // send a correcting update to the client, as it already placed the upper half of the bisected item
+ //player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.relative(hitResult.getDirection()).above())); // Paper - Don't resync blocks
+
+ // send a correcting update to the client for the block above as well, this because of replaceable blocks (such as grass, sea grass etc)
+ //player.connection.send(new ClientboundBlockUpdatePacket(world, blockposition.above())); // Paper - Don't resync blocks
+ // Paper start - extend Player Interact cancellation // TODO: consider merging this into the extracted method
+ } else if (blockState.is(net.minecraft.world.level.block.Blocks.JIGSAW) || blockState.is(net.minecraft.world.level.block.Blocks.STRUCTURE_BLOCK) || blockState.getBlock() instanceof net.minecraft.world.level.block.CommandBlock) {
+ player.connection.send(new net.minecraft.network.protocol.game.ClientboundContainerClosePacket(this.player.containerMenu.containerId));
+ }
+ // Paper end - extend Player Interact cancellation
+ player.getBukkitEntity().updateInventory(); // SPIGOT-2867
+ this.player.resyncUsingItem(this.player); // Paper - Properly cancel usable items
+ return (event.useItemInHand() != org.bukkit.event.Event.Result.ALLOW) ? InteractionResult.SUCCESS : InteractionResult.PASS;
+ } else if (this.gameModeForPlayer == GameType.SPECTATOR) {
+ MenuProvider itileinventory = blockState.getMenuProvider(level, blockPos);
+
+ if (itileinventory != null && player.openMenu(itileinventory).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
return InteractionResult.CONSUME;
} else {
return InteractionResult.PASS;
@@ -340,7 +_,7 @@
}
}
- if (!stack.isEmpty() && !player.getCooldowns().isOnCooldown(stack)) {
+ if (!stack.isEmpty() && !this.interactResult) { // add !interactResult SPIGOT-764
UseOnContext useOnContext = new UseOnContext(player, hand, hitResult);
InteractionResult interactionResult1;
if (this.isCreative()) {
@@ -357,6 +_,11 @@
return interactionResult1;
} else {
+ // Paper start - Properly cancel usable items; Cancel only if cancelled + if the interact result is different from default response
+ if (this.interactResult && this.interactResult != cancelledItem) {
+ this.player.resyncUsingItem(this.player);
+ }
+ // Paper end - Properly cancel usable items
return InteractionResult.PASS;
}
}

View File

@@ -0,0 +1,20 @@
--- a/net/minecraft/server/level/TicketType.java
+++ b/net/minecraft/server/level/TicketType.java
@@ -7,6 +_,7 @@
import net.minecraft.world.level.ChunkPos;
public class TicketType<T> {
+ public static final TicketType<Long> FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper
private final String name;
private final Comparator<T> comparator;
public long timeout;
@@ -17,6 +_,9 @@
public static final TicketType<BlockPos> PORTAL = create("portal", Vec3i::compareTo, 300);
public static final TicketType<ChunkPos> ENDER_PEARL = create("ender_pearl", Comparator.comparingLong(ChunkPos::toLong), 40);
public static final TicketType<ChunkPos> UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1);
+ public static final TicketType<Unit> PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit
+ public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
+ public static final TicketType<Integer> POST_TELEPORT = TicketType.create("post_teleport", Integer::compare, 5); // Paper - post teleport ticket type
public static <T> TicketType<T> create(String name, Comparator<T> comparator) {
return new TicketType<>(name, comparator, 0L);

View File

@@ -0,0 +1,110 @@
--- a/net/minecraft/server/level/WorldGenRegion.java
+++ b/net/minecraft/server/level/WorldGenRegion.java
@@ -151,6 +_,26 @@
return chessboardDistance < this.generatingStep.directDependencies().size();
}
+ // Paper start - if loaded util
+ @Nullable
+ @Override
+ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+ return this.getChunk(x, z, ChunkStatus.FULL, false);
+ }
+
+ @Override
+ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getBlockState(blockposition);
+ }
+
+ @Override
+ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
+ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
+ // Paper end
+
@Override
public BlockState getBlockState(BlockPos pos) {
return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())).getBlockState(pos);
@@ -198,7 +_,8 @@
if (blockState.isAir()) {
return false;
} else {
- if (dropBlock) {
+ if (dropBlock) LOGGER.warn("Potential async entity add during worldgen", new Throwable()); // Paper - Fix async entity add due to fungus trees; log when this happens
+ if (false) { // CraftBukkit - SPIGOT-6833: Do not drop during world generation
BlockEntity blockEntity = blockState.hasBlockEntity() ? this.getBlockEntity(pos) : null;
Block.dropResources(blockState, this.level, pos, blockEntity, entity, ItemStack.EMPTY);
}
@@ -242,6 +_,7 @@
}
}
+ private boolean hasSetFarWarned = false; // Paper - Buffer OOB setBlock calls
@Override
public boolean ensureCanWrite(BlockPos pos) {
int sectionPosX = SectionPos.blockToSectionCoord(pos.getX());
@@ -259,6 +_,8 @@
return true;
} else {
+ // Paper start - Buffer OOB setBlock calls
+ if (!hasSetFarWarned) {
Util.logAndPauseIfInIde(
"Detected setBlock in a far chunk ["
+ sectionPosX
@@ -270,6 +_,12 @@
+ this.generatingStep.targetStatus()
+ (this.currentlyGenerating == null ? "" : ", currently generating: " + this.currentlyGenerating.get())
);
+ hasSetFarWarned = true;
+ if (this.getServer() != null && this.getServer().isDebugging()) {
+ io.papermc.paper.util.TraceUtil.dumpTraceForThread("far setBlock call");
+ }
+ }
+ // Paper end - Buffer OOB setBlock calls
return false;
}
}
@@ -280,7 +_,7 @@
return false;
} else {
ChunkAccess chunk = this.getChunk(pos);
- BlockState blockState = chunk.setBlockState(pos, state, false);
+ BlockState blockState = chunk.setBlockState(pos, state, false); final BlockState previousBlockState = blockState; // Paper - Clear block entity before setting up a DUMMY block entity - obfhelper
if (blockState != null) {
this.level.onBlockStateChange(pos, blockState, state);
}
@@ -294,6 +_,17 @@
chunk.removeBlockEntity(pos);
}
} else {
+ // Paper start - Clear block entity before setting up a DUMMY block entity
+ // The concept of removing a block entity when the block itself changes is generally lifted
+ // from LevelChunk#setBlockState.
+ // It is however to note that this may only run if the block actually changes.
+ // Otherwise a chest block entity generated by a structure template that is later "updated" to
+ // be waterlogged would remove its existing block entity (see PaperMC/Paper#10750)
+ // This logic is *also* found in LevelChunk#setBlockState.
+ if (previousBlockState != null && !java.util.Objects.equals(previousBlockState.getBlock(), state.getBlock())) {
+ chunk.removeBlockEntity(pos);
+ }
+ // Paper end - Clear block entity before setting up a DUMMY block entity
CompoundTag compoundTag = new CompoundTag();
compoundTag.putInt("x", pos.getX());
compoundTag.putInt("y", pos.getY());
@@ -319,6 +_,13 @@
@Override
public boolean addFreshEntity(Entity entity) {
+ // CraftBukkit start
+ return this.addFreshEntity(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
+ }
+
+ @Override
+ public boolean addFreshEntity(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
+ // CraftBukkit end
int sectionPosX = SectionPos.blockToSectionCoord(entity.getBlockX());
int sectionPosZ = SectionPos.blockToSectionCoord(entity.getBlockZ());
this.getChunk(sectionPosX, sectionPosZ).addEntity(entity);