Compare commits

...

17 Commits

Author SHA1 Message Date
Spottedleaf
7ea20cde31 Set version to 0.1.0-beta.14 2025-06-20 18:29:35 -07:00
Spottedleaf
5e2b1ad7d3 Do not allow ticket level decreases to be processed asynchronously
Note: This cannot happen on the Fabric/NeoForge versions since
async ticket level processing is not allowed, but can happen on
Paper. This change is made here so that Paper can
remain in sync.

Ticket level decreases may be handled asynchronously when the
off-thread invokes processTicketUpdates() when the main thread
is running ChunkHolderManager#tick(). This is because the ticket
update is queued during tick(), but not executed (invoking
processTicketUpdates) until after releasing the ticket lock.
This creates a small window for an off-thread to invoke
processTicketUpdates() and steal the update.

When the update is stolen, the full chunk status update (if any)
will be eventually queued to execute via the chunk task queue.
If the chunk queue is processed during the server tick at any
point other than the ChunkHolderManager tick, then any ticket
level decrease will violate an important invariant in the
Moonrise chunk system: ticket level decreases only occur during
ChunkHolderManager tick. This invariant exists to make interfacing
with the chunk system easier, especially working with off-thread
contexts.

This change is specifically made to work towards fixing
https://github.com/PaperMC/Folia/issues/363
2025-06-20 18:28:16 -07:00
Spottedleaf
3146aa2e3e Add chunk unload delay config option
The default config option is no unload delay, so that
it maintains parity with Vanilla.

Fixes https://github.com/Tuinity/Moonrise/issues/115
2025-06-20 15:41:45 -07:00
Spottedleaf
035093d2c6 Fix incompatibility with "Fast IP Ping"
Inject in a different place to prevent the mixin from failing to
apply. It looks like we do essentially the same logic anyways.
2025-06-20 15:41:33 -07:00
Spottedleaf
21d106ffa2 Set version to 0.1.0-SNAPSHOT 2025-06-09 02:17:56 -07:00
Spottedleaf
6841805446 Set version to 0.1.0-beta.13 2025-06-09 02:13:51 -07:00
Spottedleaf
eb20846213 Fix infinite loop in RegionFile IO
If an exception is thrown during decompress then the read process
would be started again, which of course would eventually throw in
the decompress process.
2025-06-09 02:12:01 -07:00
Spottedleaf
66a0850ac9 Set version to 0.1.0-SNAPSHOT 2025-02-24 20:59:03 -08:00
Spottedleaf
9a16a91fb8 Set version to 0.1.0-beta.12 2025-02-24 20:52:25 -08:00
Jason Penilla
580041a1a1 Clear lastSection on game event listener removal (fixes #87) (backport of #99) 2025-02-24 17:40:26 -07:00
Spottedleaf
fb31d0e1f7 Set version to 0.1.0-SNAPSHOT 2025-01-27 08:20:45 -08:00
Spottedleaf
0b2070d781 Set version to 0.1.0-beta.11 2025-01-27 08:12:17 -08:00
Spottedleaf
30a79469e3 Correctly retrun true for empty input shapes in EntityGetter#isUnobstructed
Vanilla will return true for empty shapes, so we should as well.
2025-01-27 07:59:20 -08:00
Jason Penilla
254b331259 Exclude transitive deps provided by Minecraft 2024-12-04 10:19:20 -07:00
Jason Penilla
a21c44ad31 Use libraries from Paper repo (#82)
* Use YamlConfig from Paper repo

* Add comment to settings

* Use ConcurrentUtil from Paper repo and use release of YamlConfig

* Use ConcurrentUtil release
2024-12-03 20:33:24 -07:00
Spottedleaf
77ba4a4584 Throw when sync loading chunks after shutdown
The caller would block indefinitely if a sync load is requested
during shutdown. Rather than block indefinitely, we should throw
an exception to indicate to the caller that the chunk system is
unable to process requests.
2024-12-03 20:32:14 -07:00
Spottedleaf
a510acbd78 Set version to 0.1.0-SNAPSHOT 2024-12-01 15:57:47 -08:00
22 changed files with 170 additions and 95 deletions

View File

@@ -31,8 +31,6 @@ jobs:
key: ${{ runner.os }}-project-local-gradle-caches-${{ hashFiles('**/libs.versions.toml', '**/*.gradle*', '**/gradle-wrapper.properties') }} key: ${{ runner.os }}-project-local-gradle-caches-${{ hashFiles('**/libs.versions.toml', '**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: | restore-keys: |
${{ runner.os }}-project-local-gradle-caches- ${{ runner.os }}-project-local-gradle-caches-
- name: "setup dependencies"
run: ./install_deps.sh
- name: "execute gradle build" - name: "execute gradle build"
run: ./gradlew build run: ./gradlew build
- name: Determine Snapshot Status - name: Determine Snapshot Status

6
.gitmodules vendored
View File

@@ -1,6 +0,0 @@
[submodule "ConcurrentUtil"]
path = ConcurrentUtil
url = https://github.com/Spottedleaf/ConcurrentUtil.git
[submodule "YamlConfig"]
path = YamlConfig
url = https://github.com/Spottedleaf/YamlConfig.git

Submodule ConcurrentUtil deleted from 08d3ca3241

Submodule YamlConfig deleted from 67552e7707

View File

@@ -22,8 +22,8 @@ def getGitCommit = { ->
dependencies { dependencies {
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
api("ca.spottedleaf:concurrentutil:${rootProject.concurrentutil_version}") api("ca.spottedleaf:concurrentutil:${rootProject.concurrentutil_version}") { setTransitive(false) }
api("ca.spottedleaf:yamlconfig:${rootProject.yamlconfig_version}") api("ca.spottedleaf:yamlconfig:${rootProject.yamlconfig_version}") { setTransitive(false) }
api("org.yaml:snakeyaml:${rootProject.snakeyaml_version}") api("org.yaml:snakeyaml:${rootProject.snakeyaml_version}")
modImplementation "me.shedaniel.cloth:cloth-config:${rootProject.cloth_version}" modImplementation "me.shedaniel.cloth:cloth-config:${rootProject.cloth_version}"
@@ -46,10 +46,10 @@ allprojects {
} }
repositories { repositories {
mavenLocal { maven {
url "https://repo.papermc.io/repository/maven-public/"
mavenContent { mavenContent {
includeModule("ca.spottedleaf", "concurrentutil") includeGroup("ca.spottedleaf")
includeModule("ca.spottedleaf", "yamlconfig")
} }
} }
maven { maven {

View File

@@ -17,8 +17,8 @@ dependencies {
runtimeOnly(project(":").sourceSets.main.output) runtimeOnly(project(":").sourceSets.main.output)
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
libs("ca.spottedleaf:concurrentutil:${rootProject.concurrentutil_version}") libs("ca.spottedleaf:concurrentutil:${rootProject.concurrentutil_version}") { setTransitive(false) }
libs("ca.spottedleaf:yamlconfig:${rootProject.yamlconfig_version}") libs("ca.spottedleaf:yamlconfig:${rootProject.yamlconfig_version}") { setTransitive(false) }
libs("org.yaml:snakeyaml:${rootProject.snakeyaml_version}") libs("org.yaml:snakeyaml:${rootProject.snakeyaml_version}")
modImplementation "me.shedaniel.cloth:cloth-config-fabric:${rootProject.cloth_version}" modImplementation "me.shedaniel.cloth:cloth-config-fabric:${rootProject.cloth_version}"

View File

@@ -8,13 +8,13 @@ supported_minecraft_versions=1.21,1.21.1
loader_version=0.16.5 loader_version=0.16.5
neoforge_version=21.1.79 neoforge_version=21.1.79
snakeyaml_version=2.3 snakeyaml_version=2.3
concurrentutil_version=0.0.2-SNAPSHOT concurrentutil_version=0.0.2
yamlconfig_version=1.0.2-SNAPSHOT yamlconfig_version=1.0.2
cloth_version=15.0.128 cloth_version=15.0.128
# version ids from modrinth # version ids from modrinth
fabric_lithium_version=frXUdgvL fabric_lithium_version=frXUdgvL
neo_lithium_version=KhdehJ6l neo_lithium_version=KhdehJ6l
# Mod Properties # Mod Properties
mod_version=0.1.0-beta.10 mod_version=0.1.0-beta.14
maven_group=ca.spottedleaf.moonrise maven_group=ca.spottedleaf.moonrise
archives_base_name=moonrise archives_base_name=moonrise

View File

@@ -1,12 +0,0 @@
#!/bin/bash
set -eou pipefail
git submodule update --init --recursive
cd ConcurrentUtil
mvn install
cd ..
cd YamlConfig
mvn install

View File

@@ -21,8 +21,8 @@ dependencies {
add('shadow', project([path: ":", configuration: "namedElements"])) add('shadow', project([path: ":", configuration: "namedElements"]))
neoForge "net.neoforged:neoforge:${rootProject.neoforge_version}" neoForge "net.neoforged:neoforge:${rootProject.neoforge_version}"
shadow("ca.spottedleaf:concurrentutil:${rootProject.concurrentutil_version}") shadow("ca.spottedleaf:concurrentutil:${rootProject.concurrentutil_version}") { setTransitive(false) }
shadow("ca.spottedleaf:yamlconfig:${rootProject.yamlconfig_version}") shadow("ca.spottedleaf:yamlconfig:${rootProject.yamlconfig_version}") { setTransitive(false) }
shadow("org.yaml:snakeyaml:${rootProject.snakeyaml_version}") shadow("org.yaml:snakeyaml:${rootProject.snakeyaml_version}")
forgeExtra("org.yaml:snakeyaml:${rootProject.snakeyaml_version}") forgeExtra("org.yaml:snakeyaml:${rootProject.snakeyaml_version}")

View File

@@ -48,3 +48,6 @@ include("fabric")
findProject(":fabric").name = "Moonrise-Fabric" findProject(":fabric").name = "Moonrise-Fabric"
include("neoforge") include("neoforge")
findProject(":neoforge").name = "Moonrise-NeoForge" findProject(":neoforge").name = "Moonrise-NeoForge"
// includeBuild("../YamlConfig") // Uncomment to use local YamlConfig
// includeBuild("../ConcurrentUtil") // Uncomment to use local ConcurrentUtil

View File

@@ -2,6 +2,7 @@ package ca.spottedleaf.moonrise.common.config.moonrise;
import ca.spottedleaf.moonrise.common.config.ui.ClothConfig; import ca.spottedleaf.moonrise.common.config.ui.ClothConfig;
import ca.spottedleaf.moonrise.common.util.MoonriseCommon; import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.yamlconfig.InitialiseHook; import ca.spottedleaf.yamlconfig.InitialiseHook;
import ca.spottedleaf.yamlconfig.annotation.Adaptable; import ca.spottedleaf.yamlconfig.annotation.Adaptable;
@@ -38,7 +39,7 @@ public final class MoonriseConfig {
@Adaptable @Adaptable
public static final class Basic { public static final class Basic implements InitialiseHook {
@Serializable( @Serializable(
comment = """ comment = """
The maximum rate of chunks to send to any given player, per second. If this value is <= 0, The maximum rate of chunks to send to any given player, per second. If this value is <= 0,
@@ -72,6 +73,20 @@ public final class MoonriseConfig {
section = CHUNK_SYSTEM_SECTION section = CHUNK_SYSTEM_SECTION
) )
public double playerMaxGenRate = -1.0; public double playerMaxGenRate = -1.0;
@Serializable(
comment = """
The delay before chunks are unloaded around players once they leave their view distance.
The Vanilla value is 0 ticks. Setting this value higher (i.e 5s) will allow pets to teleport
to their owners when they teleport.
"""
)
public Duration playerChunkUnloadDelay = Duration.parse("0t");
@Override
public void initialise() {
RegionizedPlayerChunkLoader.setUnloadDelay(this.playerChunkUnloadDelay.getTimeTicks());
}
} }
@Serializable( @Serializable(

View File

@@ -0,0 +1,25 @@
package ca.spottedleaf.moonrise.mixin.chunk_system;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.gameevent.DynamicGameEventListener;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(DynamicGameEventListener.class)
abstract class DynamicGameEventListenerMixin {
@Shadow @Nullable private SectionPos lastSection;
@Inject(method = "remove", at = @At("RETURN"))
private void onRemove(final CallbackInfo ci) {
// We need to unset the last section when removed, otherwise if the same instance is re-added at the same position it
// will assume there was no change and fail to re-register.
// In vanilla, chunks rarely unload and re-load quickly enough to trigger this issue. However, our chunk system has a
// quirk where fast chunk reload cycles will often occur on player login (see PR #22).
// So we fix this vanilla oversight as our changes cause it to manifest in bugs much more often (see issue #87).
this.lastSection = null;
}
}

View File

@@ -81,6 +81,13 @@ abstract class ServerChunkCacheMixin extends ChunkSource implements ChunkSystemS
completable::complete completable::complete
); );
if (!completable.isDone() && chunkTaskScheduler.hasShutdown()) {
throw new IllegalStateException(
"Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.level) + "' at "
+ "(" + chunkX + "," + chunkZ + ") status: " + toStatus
);
}
if (TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) { if (TickThread.isTickThreadFor(this.level, chunkX, chunkZ)) {
ChunkTaskScheduler.pushChunkWait(this.level, chunkX, chunkZ); ChunkTaskScheduler.pushChunkWait(this.level, chunkX, chunkZ);
this.mainThreadProcessor.managedBlock(completable::isDone); this.mainThreadProcessor.managedBlock(completable::isDone);

View File

@@ -74,7 +74,7 @@ interface EntityGetterMixin {
@Overwrite @Overwrite
default boolean isUnobstructed(final Entity entity, final VoxelShape voxel) { default boolean isUnobstructed(final Entity entity, final VoxelShape voxel) {
if (voxel.isEmpty()) { if (voxel.isEmpty()) {
return false; return true;
} }
final AABB singleAABB = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation(); final AABB singleAABB = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();

View File

@@ -55,7 +55,7 @@ abstract class LevelMixin implements LevelAccessor, AutoCloseable {
public boolean isUnobstructed(final Entity entity) { public boolean isUnobstructed(final Entity entity) {
final AABB boundingBox = entity.getBoundingBox(); final AABB boundingBox = entity.getBoundingBox();
if (CollisionUtil.isEmpty(boundingBox)) { if (CollisionUtil.isEmpty(boundingBox)) {
return false; return true;
} }
final List<Entity> entities = this.getEntities( final List<Entity> entities = this.getEntities(

View File

@@ -1,10 +1,13 @@
package ca.spottedleaf.moonrise.mixin.serverlist; package ca.spottedleaf.moonrise.mixin.serverlist;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.multiplayer.resolver.ServerAddress;
import net.minecraft.client.multiplayer.resolver.ServerAddressResolver; import net.minecraft.client.multiplayer.resolver.ServerAddressResolver;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
@Mixin(ServerAddressResolver.class) @Mixin(ServerAddressResolver.class)
@@ -15,24 +18,23 @@ interface ServerAddressResolverMixin {
* @author Spottedleaf * @author Spottedleaf
*/ */
@Redirect( @Redirect(
method = { method = {
"method_36903", "method_36903",
"*(Lnet/minecraft/client/multiplayer/resolver/ServerAddress;)Ljava/util/Optional;" "lambda$static$0"
}, },
at = @At( at = @At(
value = "INVOKE", value = "NEW",
target = "Ljava/net/InetAddress;getByName(Ljava/lang/String;)Ljava/net/InetAddress;" target = "(Ljava/net/InetAddress;I)Ljava/net/InetSocketAddress;"
) )
) )
private static InetAddress eliminateRDNS(final String name) throws UnknownHostException { private static InetSocketAddress eliminateRDNS(InetAddress addr, final int port,
final InetAddress ret = InetAddress.getByName(name); @Local(ordinal = 0, argsOnly = true) final ServerAddress serverAddress) throws UnknownHostException {
final byte[] address = addr.getAddress();
final byte[] address = ret.getAddress();
if (address != null) { if (address != null) {
// pass name to prevent rDNS // pass name to prevent rDNS
return InetAddress.getByAddress(name, address); addr = InetAddress.getByAddress(serverAddress.getHost(), address);
} }
return ret; return new InetSocketAddress(addr, port);
} }
} }

View File

@@ -1042,7 +1042,7 @@ public final class MoonriseRegionFileIO {
LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr); LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr);
} }
if (compoundTag == null) { if (throwable == null && compoundTag == null) {
// need to re-try from the start // need to re-try from the start
this.scheduleReadIO(); this.scheduleReadIO();
return; return;

View File

@@ -46,7 +46,7 @@ import java.util.function.Function;
public final class RegionizedPlayerChunkLoader { public final class RegionizedPlayerChunkLoader {
public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo);
public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20); public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 1);
public static final int MIN_VIEW_DISTANCE = 2; public static final int MIN_VIEW_DISTANCE = 2;
public static final int MAX_VIEW_DISTANCE = 32; public static final int MAX_VIEW_DISTANCE = 32;
@@ -55,6 +55,10 @@ public final class RegionizedPlayerChunkLoader {
public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY); public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY);
public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL; public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL;
public static void setUnloadDelay(final long ticks) {
PLAYER_TICKET_DELAYED.timeout = Math.max(1, ticks);
}
public static final class ViewDistanceHolder { public static final class ViewDistanceHolder {
private volatile ViewDistances viewDistances; private volatile ViewDistances viewDistances;

View File

@@ -1,5 +1,6 @@
package ca.spottedleaf.moonrise.patches.chunk_system.scheduling; package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
import ca.spottedleaf.concurrentutil.util.Priority; import ca.spottedleaf.concurrentutil.util.Priority;
@@ -82,6 +83,7 @@ public final class ChunkHolderManager {
private long currentTick; private long currentTick;
private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>(); private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
private final MultiThreadedQueue<NewChunkHolder> offThreadPendingFullLoadUpdate = new MultiThreadedQueue<>();
private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
if (c1 == c2) { if (c1 == c2) {
return 0; return 0;
@@ -110,20 +112,20 @@ public final class ChunkHolderManager {
this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift()); this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift());
} }
public boolean processTicketUpdates(final int posX, final int posZ) { public boolean processTicketUpdates(final int chunkX, final int chunkZ) {
final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT; final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
final int ticketMask = (1 << ticketShift) - 1; final int ticketMask = (1 << ticketShift) - 1;
final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>(); final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
final List<NewChunkHolder> changedFullStatus = new ArrayList<>(); final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
final boolean ret; final boolean ret;
final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
((posX >> ticketShift) - 1) << ticketShift, ((chunkX >> ticketShift) - 1) << ticketShift,
((posZ >> ticketShift) - 1) << ticketShift, ((chunkZ >> ticketShift) - 1) << ticketShift,
(((posX >> ticketShift) + 1) << ticketShift) | ticketMask, (((chunkX >> ticketShift) + 1) << ticketShift) | ticketMask,
(((posZ >> ticketShift) + 1) << ticketShift) | ticketMask (((chunkZ >> ticketShift) + 1) << ticketShift) | ticketMask
); );
try { try {
ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus); ret = this.processTicketUpdatesNoLock(chunkX >> ticketShift, chunkZ >> ticketShift, scheduledTasks, changedFullStatus);
} finally { } finally {
this.ticketLockArea.unlock(ticketLock); this.ticketLockArea.unlock(ticketLock);
} }
@@ -219,6 +221,8 @@ public final class ChunkHolderManager {
LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex); LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex);
} }
} }
this.taskScheduler.setShutdown(true);
} }
void ensureInAutosave(final NewChunkHolder holder) { void ensureInAutosave(final NewChunkHolder holder) {
@@ -719,6 +723,9 @@ public final class ChunkHolderManager {
return removeDelay <= 0L; return removeDelay <= 0L;
}; };
final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
final long sectionKey = iterator.nextLong(); final long sectionKey = iterator.nextLong();
@@ -727,9 +734,16 @@ public final class ChunkHolderManager {
continue; continue;
} }
final int lowerChunkX = CoordinateUtils.getChunkX(sectionKey) << sectionShift;
final int lowerChunkZ = CoordinateUtils.getChunkZ(sectionKey) << sectionShift;
final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
final int ticketMask = (1 << ticketShift) - 1;
final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock( final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
CoordinateUtils.getChunkX(sectionKey) << sectionShift, ((lowerChunkX >> ticketShift) - 1) << ticketShift,
CoordinateUtils.getChunkZ(sectionKey) << sectionShift ((lowerChunkZ >> ticketShift) - 1) << ticketShift,
(((lowerChunkX >> ticketShift) + 1) << ticketShift) | ticketMask,
(((lowerChunkZ >> ticketShift) + 1) << ticketShift) | ticketMask
); );
try { try {
@@ -776,9 +790,23 @@ public final class ChunkHolderManager {
if (chunkToExpireCount.isEmpty()) { if (chunkToExpireCount.isEmpty()) {
this.sectionToChunkToExpireCount.remove(sectionKey); this.sectionToChunkToExpireCount.remove(sectionKey);
} }
// In order to prevent a race condition where an off-thread invokes processTicketUpdates(), we need to process ticket updates here
// so that we catch any additions to the changed full status list. If an off-thread were to process tickets here, it would not be guaranteed
// that it would be added to the full changed status set by the end of the call - possibly allowing ticket level decreases to be processed
// outside of this call, which is not an intended or expected of this chunk system.
this.processTicketUpdatesNoLock(lowerChunkX >> ThreadedTicketLevelPropagator.SECTION_SHIFT, lowerChunkZ >> ThreadedTicketLevelPropagator.SECTION_SHIFT, scheduledTasks, changedFullStatus);
} finally { } finally {
this.ticketLockArea.unlock(ticketLock); this.ticketLockArea.unlock(ticketLock);
} }
this.addChangedStatuses(changedFullStatus);
changedFullStatus.clear(); // clear for next loop iteration
for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
scheduledTasks.get(i).schedule();
}
scheduledTasks.clear(); // clear for next loop iteration
} }
this.processTicketUpdates(); this.processTicketUpdates();
@@ -1005,14 +1033,9 @@ public final class ChunkHolderManager {
return; return;
} }
if (!TickThread.isTickThread()) { if (!TickThread.isTickThread()) {
this.taskScheduler.scheduleChunkTask(() -> { // These will be handled on the next ServerChunkCache$MainThreadExecutor#pollTask, as it runs the distance manager update
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; // which will invoke processTicketUpdates
for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { this.offThreadPendingFullLoadUpdate.addAll(changedFullStatus);
pendingFullLoadUpdate.add(changedFullStatus.get(i));
}
ChunkHolderManager.this.processPendingFullUpdate();
}, Priority.HIGHEST);
} else { } else {
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate; final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
@@ -1293,36 +1316,20 @@ public final class ChunkHolderManager {
} }
public boolean processTicketUpdates() { public boolean processTicketUpdates() {
return this.processTicketUpdates(true, null);
}
private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
return CURRENT_TICKET_UPDATE_SCHEDULING.get();
}
private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) {
if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) { if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager"); throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager");
} }
if (!PlatformHooks.get().allowAsyncTicketUpdates() && !TickThread.isTickThread()) { final boolean isTickThread = TickThread.isTickThread();
if (!PlatformHooks.get().allowAsyncTicketUpdates() && isTickThread) {
TickThread.ensureTickThread("Cannot asynchronously process ticket updates"); TickThread.ensureTickThread("Cannot asynchronously process ticket updates");
} }
List<NewChunkHolder> changedFullStatus = null;
final boolean isTickThread = TickThread.isTickThread();
boolean ret = false; boolean ret = false;
final boolean canProcessFullUpdates = processFullUpdates & isTickThread;
final boolean canProcessScheduling = scheduledTasks == null;
if (this.ticketLevelPropagator.hasPendingUpdates()) { if (this.ticketLevelPropagator.hasPendingUpdates()) {
if (scheduledTasks == null) { final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
scheduledTasks = new ArrayList<>(); final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
}
changedFullStatus = new ArrayList<>();
this.blockTicketUpdates(); this.blockTicketUpdates();
try { try {
@@ -1333,27 +1340,42 @@ public final class ChunkHolderManager {
} finally { } finally {
this.unblockTicketUpdates(Boolean.FALSE); this.unblockTicketUpdates(Boolean.FALSE);
} }
}
if (changedFullStatus != null) {
this.addChangedStatuses(changedFullStatus); this.addChangedStatuses(changedFullStatus);
}
if (canProcessScheduling && scheduledTasks != null) {
for (int i = 0, len = scheduledTasks.size(); i < len; ++i) { for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
scheduledTasks.get(i).schedule(); scheduledTasks.get(i).schedule();
} }
} }
if (canProcessFullUpdates) { if (isTickThread) {
ret |= this.processPendingFullUpdate(); ret |= this.processPendingFullUpdate();
} }
return ret; return ret;
} }
private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
return CURRENT_TICKET_UPDATE_SCHEDULING.get();
}
// only call on tick thread
private void processOffThreadFullUpdates() {
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
final MultiThreadedQueue<NewChunkHolder> offThreadPendingFullLoadUpdate = this.offThreadPendingFullLoadUpdate;
NewChunkHolder toUpdate;
while ((toUpdate = offThreadPendingFullLoadUpdate.poll()) != null) {
pendingFullLoadUpdate.add(toUpdate);
}
}
// only call on tick thread // only call on tick thread
private boolean processPendingFullUpdate() { private boolean processPendingFullUpdate() {
this.processOffThreadFullUpdates();
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate; final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
boolean ret = false; boolean ret = false;

View File

@@ -271,6 +271,16 @@ public final class ChunkTaskScheduler {
return this.lockShift; return this.lockShift;
} }
private volatile boolean shutdown;
public boolean hasShutdown() {
return this.shutdown;
}
public void setShutdown(final boolean shutdown) {
this.shutdown = shutdown;
}
public ChunkTaskScheduler(final ServerLevel world) { public ChunkTaskScheduler(final ServerLevel world) {
this.world = world; this.world = world;
// must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift // must be >= region shift (in paper, doesn't exist) and must be >= ticket propagator section shift
@@ -524,6 +534,13 @@ public final class ChunkTaskScheduler {
return loaded; return loaded;
} }
if (this.hasShutdown()) {
throw new IllegalStateException(
"Chunk system has shut down, cannot process chunk requests in world '" + ca.spottedleaf.moonrise.common.util.WorldUtil.getWorldName(this.world) + "' at "
+ "(" + chunkX + "," + chunkZ + ") status: " + status
);
}
final Long ticketId = getNextNonFullLoadId(); final Long ticketId = getNextNonFullLoadId();
final int ticketLevel = getTicketLevel(status); final int ticketLevel = getTicketLevel(status);
this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId); this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);

View File

@@ -210,6 +210,7 @@ accessible class net/minecraft/server/level/DistanceManager$FixedPlayerDistanceC
# Ticket # Ticket
accessible field net/minecraft/server/level/Ticket key Ljava/lang/Object; accessible field net/minecraft/server/level/Ticket key Ljava/lang/Object;
accessible field net/minecraft/server/level/TicketType timeout J accessible field net/minecraft/server/level/TicketType timeout J
mutable field net/minecraft/server/level/TicketType timeout J
# ServerChunkCache # ServerChunkCache

View File

@@ -30,6 +30,7 @@
"chunk_system.ChunkStepMixin", "chunk_system.ChunkStepMixin",
"chunk_system.ChunkStorageMixin", "chunk_system.ChunkStorageMixin",
"chunk_system.DistanceManagerMixin", "chunk_system.DistanceManagerMixin",
"chunk_system.DynamicGameEventListenerMixin",
"chunk_system.EntityGetterMixin", "chunk_system.EntityGetterMixin",
"chunk_system.EntityMixin", "chunk_system.EntityMixin",
"chunk_system.EntityTickListMixin", "chunk_system.EntityTickListMixin",