diff --git a/patches/minecraft/net/minecraft/server/level/ChunkMap.java.patch b/patches/minecraft/net/minecraft/server/level/ChunkMap.java.patch index 987f9b20bd..71333ff559 100644 --- a/patches/minecraft/net/minecraft/server/level/ChunkMap.java.patch +++ b/patches/minecraft/net/minecraft/server/level/ChunkMap.java.patch @@ -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 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 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 filter, Consumer 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 @@ } diff --git a/patches/minecraft/net/minecraft/server/level/DistanceManager.java.patch b/patches/minecraft/net/minecraft/server/level/DistanceManager.java.patch index 870608f05b..c07749b854 100644 --- a/patches/minecraft/net/minecraft/server/level/DistanceManager.java.patch +++ b/patches/minecraft/net/minecraft/server/level/DistanceManager.java.patch @@ -4,7 +4,7 @@ final Executor mainThreadExecutor; private long ticketTickCounter; private int simulationDistance = 10; -+ private final Long2ObjectOpenHashMap>> forcedTickets = new Long2ObjectOpenHashMap<>(); ++ final Long2ObjectOpenHashMap>> forcedTickets = new Long2ObjectOpenHashMap<>(); protected DistanceManager(Executor p_140774_, Executor p_140775_) { TaskScheduler taskscheduler = TaskScheduler.wrapExecutor("player ticket throttler", p_140775_); @@ -44,7 +44,7 @@ + } + + public void addRegionTicket(TicketType p_140841_, ChunkPos p_140842_, int p_140843_, T p_140844_, boolean forceTicks) { -+ Ticket ticket = new Ticket<>(p_140841_, ChunkLevel.byStatus(FullChunkStatus.FULL) - p_140843_, p_140844_, forceTicks); ++ Ticket 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); diff --git a/patches/minecraft/net/minecraft/server/level/ServerChunkCache.java.patch b/patches/minecraft/net/minecraft/server/level/ServerChunkCache.java.patch index 0b24fb64c0..1ffffc4bc4 100644 --- a/patches/minecraft/net/minecraft/server/level/ServerChunkCache.java.patch +++ b/patches/minecraft/net/minecraft/server/level/ServerChunkCache.java.patch @@ -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 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 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 getChunkCollector(java.util.Collection p_368970_, java.util.function.Predicate 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 @@ } diff --git a/src/main/java/net/minecraftforge/common/extensions/IForgeGameTestHelper.java b/src/main/java/net/minecraftforge/common/extensions/IForgeGameTestHelper.java index 55678e2427..1f2cc57569 100644 --- a/src/main/java/net/minecraftforge/common/extensions/IForgeGameTestHelper.java +++ b/src/main/java/net/minecraftforge/common/extensions/IForgeGameTestHelper.java @@ -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 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(); } diff --git a/src/test/java/net/minecraftforge/debug/gameplay/level/ForcedChunkLoadingTest.java b/src/test/java/net/minecraftforge/debug/gameplay/level/ForcedChunkLoadingTest.java new file mode 100644 index 0000000000..4909d316f5 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/gameplay/level/ForcedChunkLoadingTest.java @@ -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(); + }); + } +}