Fix forced chunks never being ticked by the server (#10325)

This commit is contained in:
Jonathing authored and GitHub committed 2025-02-03 13:47:36 -08:00
1 parent 464483db5b
commit 79efe2ac36
5 files changed
+182 -4

No files matched your search

@@ -24,6 +24,35 @@
this.write(chunkpos, completablefuture::join).handle((p_358676_, p_358677_) -> {
if (p_358677_ != null) {
this.level.getServer().reportChunkSaveFailure(p_358677_, this.storageInfo(), chunkpos);
@@ -934,10 +_,27 @@
void forEachSpawnCandidateChunk(Consumer<ChunkHolder> p_364582_) {
LongIterator longiterator = this.distanceManager.getSpawnCandidateChunks();
+ // FORGE: separated forEach logic into its own method
+ this.forEachChunk(longiterator, this::anyPlayerCloseEnoughForSpawningInternal, p_364582_);
+ }
+
+ /** FORGE: grab forced chunks, use forEachChunk to avoid duplicate logic */
+ void forEachForcedChunk(Consumer<ChunkHolder> callback) {
+ LongIterator longiterator = this.distanceManager.forcedTickets.keySet().iterator();
+ this.forEachChunk(longiterator, pos -> true, callback);
+ }
+
+ /** FORGE: check if there are any forced chunks */
+ boolean hasForcedChunks() {
+ return !this.distanceManager.forcedTickets.isEmpty();
+ }
+
+ /** FORGE: commonize logic between forEachSpawnCandidateChunk and forEachForcedChunk */
+ private void forEachChunk(LongIterator longiterator, java.util.function.Predicate<ChunkPos> filter, Consumer<ChunkHolder> p_364582_) {
while (longiterator.hasNext()) {
long i = longiterator.nextLong();
ChunkHolder chunkholder = this.visibleChunkMap.get(i);
- if (chunkholder != null && this.anyPlayerCloseEnoughForSpawningInternal(chunkholder.getPos())) {
+ if (chunkholder != null && filter.test(chunkholder.getPos())) {
p_364582_.accept(chunkholder);
}
}
@@ -1102,7 +_,7 @@
}
@@ -4,7 +4,7 @@
final Executor mainThreadExecutor;
private long ticketTickCounter;
private int simulationDistance = 10;
+ private final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> forcedTickets = new Long2ObjectOpenHashMap<>();
+ final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> forcedTickets = new Long2ObjectOpenHashMap<>();
protected DistanceManager(Executor p_140774_, Executor p_140775_) {
TaskScheduler<Runnable> taskscheduler = TaskScheduler.wrapExecutor("player ticket throttler", p_140775_);
@@ -44,7 +44,7 @@
+ }
+
+ public <T> void addRegionTicket(TicketType<T> p_140841_, ChunkPos p_140842_, int p_140843_, T p_140844_, boolean forceTicks) {
+ Ticket<T> ticket = new Ticket<>(p_140841_, ChunkLevel.byStatus(FullChunkStatus.FULL) - p_140843_, p_140844_, forceTicks);
+ Ticket<T> ticket = new Ticket<>(p_140841_, ChunkLevel.byStatus(FullChunkStatus.FULL) - p_140843_, p_140844_, forceTicks);
long i = p_140842_.toLong();
this.addTicket(i, ticket);
this.tickingTicketsTracker.addTicket(i, ticket);
@@ -11,6 +11,34 @@
ChunkAccess chunkaccess1 = chunkholder.getChunkIfPresent(ChunkStatus.FULL);
if (chunkaccess1 != null) {
this.storeInCache(i, chunkaccess1, ChunkStatus.FULL);
@@ -368,9 +_,25 @@
}
private void collectTickingChunks(List<LevelChunk> p_368970_) {
- this.chunkMap.forEachSpawnCandidateChunk(p_358696_ -> {
+ // FORGE: If there are no forced chunks, just do the vanilla logic
+ if (!this.chunkMap.hasForcedChunks()) {
+ this.chunkMap.forEachSpawnCandidateChunk(this.getChunkCollector(p_368970_, chunk -> this.level.isNaturalSpawningAllowed(chunk.getPos())));
+ return;
+ }
+
+ // FORGE: We have forced chunks? Ok, use a linked hash set to preserve order
+ Set<LevelChunk> set = new java.util.LinkedHashSet<>();
+ this.chunkMap.forEachSpawnCandidateChunk(this.getChunkCollector(set, chunk -> this.level.isNaturalSpawningAllowed(chunk.getPos())));
+ this.chunkMap.forEachForcedChunk(this.getChunkCollector(set, chunk -> true));
+ p_368970_.addAll(set);
+ }
+
+ /** FORGE: Isolate chunk collection callback */
+ private Consumer<ChunkHolder> getChunkCollector(java.util.Collection<LevelChunk> p_368970_, java.util.function.Predicate<LevelChunk> predicate) {
+ return (p_358696_ -> {
LevelChunk levelchunk = p_358696_.getTickingChunk();
- if (levelchunk != null && this.level.isNaturalSpawningAllowed(p_358696_.getPos())) {
+ // FORGE: Use custom predicate instead of only checking isNaturalSpawningAllowed
+ if (levelchunk != null && predicate.test(levelchunk)) {
p_368970_.add(levelchunk);
}
});
@@ -466,11 +_,19 @@
}
@@ -13,6 +13,9 @@ import java.util.function.Supplier;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.network.chat.MutableComponent;
import org.jetbrains.annotations.Nullable;
import com.mojang.authlib.GameProfile;
@@ -46,14 +49,25 @@ public interface IForgeGameTestHelper {
}
default void say(String message, Style style) {
var component = ForgeI18n.getPattern(message) != null ? Component.translatable(message) : Component.literal(message);
this.say(component.withStyle(style));
this.say(this.getMessage(message).withStyle(style));
}
default void say(String message, ChatFormatting style) {
this.say(this.getMessage(message).withStyle(style));
}
default void say(String message, int color) {
this.say(this.getMessage(message).withColor(color));
}
default void say(Component component) {
this.self().getLevel().players().forEach(p -> p.sendSystemMessage(component));
}
private MutableComponent getMessage(String message) {
return ForgeI18n.getPattern(message) != null ? Component.translatable(message) : Component.literal(message);
}
default void assertTrue(boolean value, Supplier<String> message) {
if (!value)
throw new GameTestAssertException(message.get());
@@ -180,6 +194,14 @@ public interface IForgeGameTestHelper {
return new IntFlag(name);
}
default IntFlag intFlag(String name, int value) {
return this.intFlag(name, (long) value);
}
default IntFlag intFlag(String name, long value) {
return Util.make(new IntFlag(name), flag -> flag.set(value));
}
default BoolFlag boolFlag(String name) {
return new BoolFlag(name);
}
@@ -258,6 +280,32 @@ public interface IForgeGameTestHelper {
super.set(value);
}
public void increment() {
this.decrement(1L);
}
public void increment(int amount) {
this.decrement((long) amount);
}
public void increment(long amount) {
if (this.value != null)
this.set(this.value + amount);
}
public void decrement() {
this.decrement(1L);
}
public void decrement(int amount) {
this.decrement((long) amount);
}
public void decrement(long amount) {
if (this.value != null)
this.set(this.value - amount);
}
public byte getByte() {
return this.value == null ? -1 : this.value.byteValue();
}
@@ -0,0 +1,73 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.debug.gameplay.level;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.FullChunkStatus;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.gametest.GameTestHolder;
import net.minecraftforge.test.BaseTestMod;
import org.jetbrains.annotations.Nullable;
@GameTestHolder("forge." + ForcedChunkLoadingTest.MOD_ID)
@Mod(ForcedChunkLoadingTest.MOD_ID)
public class ForcedChunkLoadingTest extends BaseTestMod {
static final String MOD_ID = "forced_chunk_loading";
public ForcedChunkLoadingTest(FMLJavaModLoadingContext context) {
super(context);
}
private static final int MAX_CHUNK_LOCATION_ATTEMPTS = 5;
@GameTest(template = "forge:empty3x3x3")
public static void force_far_away_chunk(GameTestHelper helper) {
var random = RandomSource.create();
var level = helper.getLevel();
var chunkSource = level.getChunkSource();
// try to find a far away chunk that is not already ticking
int attempts;
var origin = helper.absolutePos(BlockPos.ZERO);
ChunkAccess chunk = null;
for (attempts = 0; attempts < MAX_CHUNK_LOCATION_ATTEMPTS && chunk == null; attempts++) {
int x = random.nextInt(1, 10) * 10000;
int z = random.nextInt(1, 10) * 10000;
chunk = level.getChunk(origin.offset(x, 0, z));
// If the chunk is already ticking, we can't force it
chunk = chunkSource.isPositionTicking(chunk.getPos().toLong()) ? null : chunk;
}
if (chunk == null) {
helper.fail("Failed to find a far away chunk that is not already ticking after " + MAX_CHUNK_LOCATION_ATTEMPTS + " attempts");
return;
} else if (attempts > 1) {
helper.say("WARNING: Finding a far away chunk took " + attempts + " attempts.", ChatFormatting.YELLOW);
}
var pos = chunk.getPos();
helper.say("Attempting to force far away chunk: " + pos, ChatFormatting.YELLOW);
chunkSource.chunkMap.getDistanceManager().addRegionTicket(TicketType.FORCED, pos, ChunkLevel.byStatus(FullChunkStatus.FULL), pos, true);
helper.runAfterDelay(20, () -> {
helper.assertTrue(chunkSource.chunkMap.getDistanceManager().shouldForceTicks(pos.toLong()), "Chunk is not ticketed as a forced loaded chunk");
helper.assertTrue(chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(pos.toLong()), "Forced chunk cannot tick entities");
helper.assertTrue(chunkSource.chunkMap.getDistanceManager().inBlockTickingRange(pos.toLong()), "Forced chunk cannot tick blocks");
helper.assertTrue(chunkSource.hasChunk(pos.x, pos.z), "Chunk is not loaded despite being ticketed as a force loaded chunk");
helper.succeed();
});
}
}