From 7381af4ed0c4fe42484bc55e42298d6d34c1eb4c Mon Sep 17 00:00:00 2001 From: HabsW Date: Sat, 15 Aug 2026 23:35:08 +0200 Subject: [PATCH 1/3] Skip empty random-tick sections Moonrise already skips the inner random-tick loop when a section has no ticking blocks, but the outer loop still visits every section. Keep a live per-chunk bitset and skip empty sections when sparse. --- .../LevelChunkSectionMixin.java | 38 +++++- .../mixin/random_ticking/LevelChunkMixin.java | 111 ++++++++++++++++++ .../random_ticking/ServerLevelMixin.java | 96 ++++++++++----- .../RandomTickChunkSection.java | 9 ++ .../random_ticking/RandomTickLevelChunk.java | 13 ++ src/main/resources/moonrise.mixins.json | 1 + 6 files changed, 240 insertions(+), 28 deletions(-) create mode 100644 src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java create mode 100644 src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickChunkSection.java create mode 100644 src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/block_counting/LevelChunkSectionMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/block_counting/LevelChunkSectionMixin.java index e99907d..ac66332 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/block_counting/LevelChunkSectionMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/block_counting/LevelChunkSectionMixin.java @@ -4,12 +4,15 @@ import ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage; import ca.spottedleaf.moonrise.patches.collisions.CollisionUtil; import ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection; +import ca.spottedleaf.moonrise.patches.random_ticking.RandomTickChunkSection; +import ca.spottedleaf.moonrise.patches.random_ticking.RandomTickLevelChunk; import com.llamalad7.mixinextras.sugar.Local; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.shorts.ShortArrayList; import net.minecraft.util.BitStorage; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.Palette; import net.minecraft.world.level.chunk.PalettedContainer; @@ -29,7 +32,7 @@ import java.util.function.Predicate; @Mixin(LevelChunkSection.class) -abstract class LevelChunkSectionMixin implements BlockCountingChunkSection { +abstract class LevelChunkSectionMixin implements BlockCountingChunkSection, RandomTickChunkSection { @Shadow @Final @@ -70,6 +73,26 @@ abstract class LevelChunkSectionMixin implements BlockCountingChunkSection { @Unique private final ShortList tickingBlocks = new ShortList(); + @Unique + private RandomTickLevelChunk moonrise$randomTickChunk; + + @Unique + private int moonrise$randomTickIndex = -1; + + @Override + public final void moonrise$bindRandomTickChunk(final LevelChunk chunk, final int index) { + this.moonrise$randomTickChunk = (RandomTickLevelChunk)chunk; + this.moonrise$randomTickIndex = index; + } + + @Unique + private void moonrise$notifyRandomTickIndex(final boolean wasTicking) { + final boolean nowTicking = this.tickingBlockCount > 0; + if (wasTicking != nowTicking && this.moonrise$randomTickChunk != null) { + this.moonrise$randomTickChunk.moonrise$noteRandomTickSection(this.moonrise$randomTickIndex, nowTicking); + } + } + @Override public final boolean moonrise$hasSpecialCollidingBlocks() { return this.specialCollidingBlocks != 0; @@ -125,6 +148,17 @@ private void updateBlockCallback(final int x, final int y, final int z, final Bl tickingBlocks.add(position); } } + + // tickingBlockCount is already updated by vanilla; recover the previous 0/non-zero state. + final boolean wasTicking; + if (oldTicking == newTicking) { + wasTicking = this.tickingBlockCount > 0; + } else if (newTicking) { + wasTicking = this.tickingBlockCount > 1; + } else { + wasTicking = true; + } + this.moonrise$notifyRandomTickIndex(wasTicking); } /** @@ -134,6 +168,7 @@ private void updateBlockCallback(final int x, final int y, final int z, final Bl @Overwrite public void recalcBlockCounts() { // reset, then recalculate + final boolean wasTicking = this.tickingBlockCount > 0; this.nonEmptyBlockCount = (short)0; this.fluidCount = (short)0; this.tickingBlockCount = (short)0; @@ -196,6 +231,7 @@ public void recalcBlockCounts() { } } } + this.moonrise$notifyRandomTickIndex(wasTicking); } /** diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java new file mode 100644 index 0000000..070ebb3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java @@ -0,0 +1,111 @@ +package ca.spottedleaf.moonrise.mixin.random_ticking; + +import ca.spottedleaf.moonrise.patches.random_ticking.RandomTickChunkSection; +import ca.spottedleaf.moonrise.patches.random_ticking.RandomTickLevelChunk; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.EmptyLevelChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import net.minecraft.world.ticks.LevelChunkTicks; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.Arrays; + +@Mixin(LevelChunk.class) +abstract class LevelChunkMixin extends ChunkAccess implements RandomTickLevelChunk { + + public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, final LevelHeightAccessor levelHeightAccessor, + final PalettedContainerFactory palettedContainerFactory, final long l, final LevelChunkSection[] levelChunkSections, final BlendingData blendingData) { + super(chunkPos, upgradeData, levelHeightAccessor, palettedContainerFactory, l, levelChunkSections, blendingData); + } + + @Unique + private long[] moonrise$randomTickSectionMask; + + @Unique + private int moonrise$randomTickEligibleCount; + + @Override + public final void moonrise$noteRandomTickSection(final int index, final boolean ticking) { + final long[] mask = this.moonrise$randomTickSectionMask; + if (mask == null || index < 0) { + return; + } + final int word = index >>> 6; + if (word >= mask.length) { + return; + } + final long bit = 1L << (index & 63); + final boolean was = (mask[word] & bit) != 0L; + if (was == ticking) { + return; + } + if (ticking) { + mask[word] |= bit; + this.moonrise$randomTickEligibleCount++; + } else { + mask[word] &= ~bit; + this.moonrise$randomTickEligibleCount--; + } + } + + @Override + public final void moonrise$bindRandomTickSections() { + final LevelChunkSection[] sections = this.getSections(); + final int words = (sections.length + 63) >> 6; + if (this.moonrise$randomTickSectionMask == null || this.moonrise$randomTickSectionMask.length != words) { + this.moonrise$randomTickSectionMask = new long[Math.max(words, 1)]; + } else { + Arrays.fill(this.moonrise$randomTickSectionMask, 0L); + } + this.moonrise$randomTickEligibleCount = 0; + for (int i = 0; i < sections.length; i++) { + final LevelChunkSection section = sections[i]; + if (section != null) { + ((RandomTickChunkSection)section).moonrise$bindRandomTickChunk((LevelChunk)(Object)this, i); + if (section.isRandomlyTickingBlocks()) { + this.moonrise$noteRandomTickSection(i, true); + } + } + } + } + + @Override + public final int moonrise$randomTickEligibleCount() { + return this.moonrise$randomTickEligibleCount; + } + + @Override + public final long[] moonrise$randomTickSectionMask() { + return this.moonrise$randomTickSectionMask; + } + + /** + * @reason Bind the live random-tick section bitset after construction. + * Empty chunks are skipped; they are never randomly ticked. + * @author HabsW + */ + @Inject( + method = "(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/UpgradeData;Lnet/minecraft/world/ticks/LevelChunkTicks;Lnet/minecraft/world/ticks/LevelChunkTicks;J[Lnet/minecraft/world/level/chunk/LevelChunkSection;Lnet/minecraft/world/level/chunk/LevelChunk$PostLoadProcessor;Lnet/minecraft/world/level/levelgen/blending/BlendingData;)V", + at = @At("TAIL") + ) + private void moonrise$bindRandomTickSectionsOnConstruct(final Level level, final ChunkPos chunkPos, final UpgradeData upgradeData, + final LevelChunkTicks levelChunkTicks, final LevelChunkTicks levelChunkTicks2, + final long l, final LevelChunkSection[] levelChunkSections, + final LevelChunk.PostLoadProcessor postLoadProcessor, + final BlendingData blendingData, final CallbackInfo ci) { + if ((Object)this instanceof EmptyLevelChunk) { + return; + } + this.moonrise$bindRandomTickSections(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java index 47064b9..8d2e0b8 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java @@ -5,6 +5,7 @@ import ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom; import ca.spottedleaf.moonrise.common.util.WorldUtil; import ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection; +import ca.spottedleaf.moonrise.patches.random_ticking.RandomTickLevelChunk; import com.llamalad7.mixinextras.sugar.Local; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; @@ -58,8 +59,10 @@ private int nextInt(final RandomSource instance, final int bound) { /** * @reason Optimise random ticking so that it will not retrieve BlockStates unnecessarily, as well as - * optionally avoiding double ticking fluid blocks. + * optionally avoiding double ticking fluid blocks. Skip sections with no randomly ticking + * blocks in the outer loop when the chunk is sparse. * @author Spottedleaf + * @author HabsW */ @Redirect( method = "tickChunk", @@ -79,42 +82,81 @@ private LevelChunkSection[] optimiseRandomTick(final LevelChunk chunk, final ChunkPos cpos = chunk.getPos(); final int offsetX = cpos.x() << 4; final int offsetZ = cpos.z() << 4; + final int sectionsLen = sections.length; - for (int sectionIndex = 0, sectionsLen = sections.length; sectionIndex < sectionsLen; sectionIndex++) { - final int offsetY = (sectionIndex + minSection) << 4; - final LevelChunkSection section = sections[sectionIndex]; - final PalettedContainer states = section.states; - if (!section.isRandomlyTickingBlocks()) { - continue; + // Empty sections never consume random-tick RNG (the inner loop is behind tickingBlockCount > 0). + // Walk only those sections when sparse; dense chunks keep the original linear scan. + final RandomTickLevelChunk randomTickChunk = (RandomTickLevelChunk)chunk; + if (randomTickChunk.moonrise$randomTickSectionMask() == null) { + randomTickChunk.moonrise$bindRandomTickSections(); + } + final int eligible = randomTickChunk.moonrise$randomTickEligibleCount(); + if (eligible <= 0) { + return EMPTY_SECTION_ARRAY; + } + if (eligible * 2 >= sectionsLen) { + for (int sectionIndex = 0; sectionIndex < sectionsLen; sectionIndex++) { + this.randomTickSection(sections[sectionIndex], sectionIndex, minSection, offsetX, offsetZ, tickSpeed, simpleRandom, doubleTickFluids); } + } else { + final long[] mask = randomTickChunk.moonrise$randomTickSectionMask(); + int from = 0; + while (from < sectionsLen) { + final int word = from >>> 6; + final long bits = mask[word] >>> (from & 63); + if (bits != 0L) { + final int sectionIndex = from + Long.numberOfTrailingZeros(bits); + if (sectionIndex >= sectionsLen) { + break; + } + this.randomTickSection(sections[sectionIndex], sectionIndex, minSection, offsetX, offsetZ, tickSpeed, simpleRandom, doubleTickFluids); + from = sectionIndex + 1; + continue; + } + from = (word + 1) << 6; + } + } + + return EMPTY_SECTION_ARRAY; + } - final ShortList tickList = ((BlockCountingChunkSection)section).moonrise$getTickingBlockList(); + /** + * @reason Inner random-tick work for one section. Unchanged vs the original ShortList selection. + * @author HabsW + */ + @Unique + private void randomTickSection(final LevelChunkSection section, final int sectionIndex, final int minSection, + final int offsetX, final int offsetZ, final int tickSpeed, + final SimpleThreadUnsafeRandom simpleRandom, final boolean doubleTickFluids) { + if (!section.isRandomlyTickingBlocks()) { + return; + } + final int offsetY = (sectionIndex + minSection) << 4; + final PalettedContainer states = section.states; + final ShortList tickList = ((BlockCountingChunkSection)section).moonrise$getTickingBlockList(); - for (int i = 0; i < tickSpeed; ++i) { - final int tickingBlocks = tickList.size(); - final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1); + for (int i = 0; i < tickSpeed; ++i) { + final int tickingBlocks = tickList.size(); + final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1); - if (index >= tickingBlocks) { - // most of the time we fall here - continue; - } + if (index >= tickingBlocks) { + // most of the time we fall here + continue; + } - final int location = (int)tickList.getRaw(index) & 0xFFFF; - final BlockState state = states.get(location); + final int location = (int)tickList.getRaw(index) & 0xFFFF; + final BlockState state = states.get(location); - // do not use a mutable pos, as some random tick implementations store the input without calling immutable()! - final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ); + // do not use a mutable pos, as some random tick implementations store the input without calling immutable()! + final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ); - state.randomTick((ServerLevel)(Object)this, pos, simpleRandom); - if (doubleTickFluids) { - final FluidState fluidState = state.getFluidState(); - if (fluidState.isRandomlyTicking()) { - fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom); - } + state.randomTick((ServerLevel)(Object)this, pos, simpleRandom); + if (doubleTickFluids) { + final FluidState fluidState = state.getFluidState(); + if (fluidState.isRandomlyTicking()) { + fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom); } } } - - return EMPTY_SECTION_ARRAY; } } diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickChunkSection.java new file mode 100644 index 0000000..0f9b4b2 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickChunkSection.java @@ -0,0 +1,9 @@ +package ca.spottedleaf.moonrise.patches.random_ticking; + +import net.minecraft.world.level.chunk.LevelChunk; + +public interface RandomTickChunkSection { + + public void moonrise$bindRandomTickChunk(final LevelChunk chunk, final int index); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java new file mode 100644 index 0000000..de84d0f --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java @@ -0,0 +1,13 @@ +package ca.spottedleaf.moonrise.patches.random_ticking; + +public interface RandomTickLevelChunk { + + public void moonrise$noteRandomTickSection(final int index, final boolean ticking); + + public void moonrise$bindRandomTickSections(); + + public int moonrise$randomTickEligibleCount(); + + public long[] moonrise$randomTickSectionMask(); + +} diff --git a/src/main/resources/moonrise.mixins.json b/src/main/resources/moonrise.mixins.json index 8874daf..51ce269 100644 --- a/src/main/resources/moonrise.mixins.json +++ b/src/main/resources/moonrise.mixins.json @@ -112,6 +112,7 @@ "random.LevelMixin", "random_ticking.BiomeManagerMixin", "random_ticking.BiomeMixin", + "random_ticking.LevelChunkMixin", "random_ticking.LevelMixin", "random_ticking.ServerLevelMixin", "serverlist.ConnectionMixin", From 565499df65f3991aa3e20f5db4d02c03d3e16a64 Mon Sep 17 00:00:00 2001 From: HabsW Date: Mon, 17 Aug 2026 01:56:30 +0200 Subject: [PATCH 2/3] Rebuild random-tick section mask when section objects change Vanilla has no setSection hook. Detect getSections() identity changes at the start of optimiseRandomTick, unbind detached sections, and rebuild from tickingBlockCount. Also rebind after runPostLoad. --- .../LevelChunkSectionMixin.java | 6 +++ .../mixin/random_ticking/LevelChunkMixin.java | 51 +++++++++++++++++++ .../random_ticking/ServerLevelMixin.java | 6 +-- .../RandomTickChunkSection.java | 2 + .../random_ticking/RandomTickLevelChunk.java | 2 + 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/block_counting/LevelChunkSectionMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/block_counting/LevelChunkSectionMixin.java index ac66332..f065ee3 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/block_counting/LevelChunkSectionMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/block_counting/LevelChunkSectionMixin.java @@ -85,6 +85,12 @@ abstract class LevelChunkSectionMixin implements BlockCountingChunkSection, Rand this.moonrise$randomTickIndex = index; } + @Override + public final void moonrise$unbindRandomTickChunk() { + this.moonrise$randomTickChunk = null; + this.moonrise$randomTickIndex = -1; + } + @Unique private void moonrise$notifyRandomTickIndex(final boolean wasTicking) { final boolean nowTicking = this.tickingBlockCount > 0; diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java index 070ebb3..97f45de 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java @@ -34,6 +34,9 @@ public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, f @Unique private int moonrise$randomTickEligibleCount; + @Unique + private LevelChunkSection[] moonrise$boundSectionRefs; + @Override public final void moonrise$noteRandomTickSection(final int index, final boolean ticking) { final long[] mask = this.moonrise$randomTickSectionMask; @@ -58,9 +61,33 @@ public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, f } } + @Unique + private boolean moonrise$sectionRefsChanged(final LevelChunkSection[] sections) { + final LevelChunkSection[] bound = this.moonrise$boundSectionRefs; + if (bound == null || bound.length != sections.length) { + return true; + } + for (int i = 0; i < sections.length; i++) { + if (bound[i] != sections[i]) { + return true; + } + } + return false; + } + @Override public final void moonrise$bindRandomTickSections() { final LevelChunkSection[] sections = this.getSections(); + final LevelChunkSection[] oldRefs = this.moonrise$boundSectionRefs; + if (oldRefs != null) { + for (int i = 0; i < oldRefs.length; i++) { + final LevelChunkSection old = oldRefs[i]; + if (old != null && (i >= sections.length || old != sections[i])) { + ((RandomTickChunkSection)old).moonrise$unbindRandomTickChunk(); + } + } + } + final int words = (sections.length + 63) >> 6; if (this.moonrise$randomTickSectionMask == null || this.moonrise$randomTickSectionMask.length != words) { this.moonrise$randomTickSectionMask = new long[Math.max(words, 1)]; @@ -77,6 +104,15 @@ public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, f } } } + this.moonrise$boundSectionRefs = Arrays.copyOf(sections, sections.length); + } + + @Override + public final void moonrise$ensureRandomTickSections() { + final LevelChunkSection[] sections = this.getSections(); + if (this.moonrise$randomTickSectionMask == null || this.moonrise$sectionRefsChanged(sections)) { + this.moonrise$bindRandomTickSections(); + } } @Override @@ -108,4 +144,19 @@ public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, f } this.moonrise$bindRandomTickSections(); } + + /** + * @reason Post-load processors may replace section objects after the constructor bind. + * @author HabsW + */ + @Inject( + method = "runPostLoad", + at = @At("RETURN") + ) + private void moonrise$bindRandomTickSectionsAfterPostLoad(final CallbackInfo ci) { + if ((Object)this instanceof EmptyLevelChunk) { + return; + } + this.moonrise$bindRandomTickSections(); + } } diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java index 8d2e0b8..8a7d78d 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java @@ -87,9 +87,9 @@ private LevelChunkSection[] optimiseRandomTick(final LevelChunk chunk, // Empty sections never consume random-tick RNG (the inner loop is behind tickingBlockCount > 0). // Walk only those sections when sparse; dense chunks keep the original linear scan. final RandomTickLevelChunk randomTickChunk = (RandomTickLevelChunk)chunk; - if (randomTickChunk.moonrise$randomTickSectionMask() == null) { - randomTickChunk.moonrise$bindRandomTickSections(); - } + // Rebuild if never bound, or if a caller replaced LevelChunkSection objects in getSections() + // (WorldEdit/FAWE-style). Vanilla has no setSection hook. + randomTickChunk.moonrise$ensureRandomTickSections(); final int eligible = randomTickChunk.moonrise$randomTickEligibleCount(); if (eligible <= 0) { return EMPTY_SECTION_ARRAY; diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickChunkSection.java index 0f9b4b2..21a1227 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickChunkSection.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickChunkSection.java @@ -6,4 +6,6 @@ public interface RandomTickChunkSection { public void moonrise$bindRandomTickChunk(final LevelChunk chunk, final int index); + public void moonrise$unbindRandomTickChunk(); + } diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java index de84d0f..2f893bf 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java @@ -6,6 +6,8 @@ public interface RandomTickLevelChunk { public void moonrise$bindRandomTickSections(); + public void moonrise$ensureRandomTickSections(); + public int moonrise$randomTickEligibleCount(); public long[] moonrise$randomTickSectionMask(); From e605e618b5316732d9e76737aed840f15127a824 Mon Sep 17 00:00:00 2001 From: HabsW Date: Mon, 17 Aug 2026 03:49:44 +0200 Subject: [PATCH 3/3] Rebind random-tick mask after getSections() section replacement --- .../random_ticking/ChunkAccessMixin.java | 46 +++++++++++++++++++ .../mixin/random_ticking/LevelChunkMixin.java | 42 ++++++++++++++++- .../random_ticking/ServerLevelMixin.java | 10 ++-- .../random_ticking/RandomTickLevelChunk.java | 8 ++++ src/main/resources/moonrise.mixins.json | 1 + 5 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ChunkAccessMixin.java diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ChunkAccessMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ChunkAccessMixin.java new file mode 100644 index 0000000..f9543f6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ChunkAccessMixin.java @@ -0,0 +1,46 @@ +package ca.spottedleaf.moonrise.mixin.random_ticking; + +import ca.spottedleaf.moonrise.patches.random_ticking.RandomTickLevelChunk; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +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.CallbackInfoReturnable; + +@Mixin(ChunkAccess.class) +abstract class ChunkAccessMixin { + + @Shadow + @Final + protected LevelChunkSection[] sections; + + /** + * @reason FAWE/WorldEdit replace sections via {@code getSections()[i] = newSection}. + * Mark the chunk so the next random tick rebinds. Random-tick's own + * getSections is suppressed by begin/end on the chunk. + * @author HabsW + */ + @Inject( + method = "getSections", + at = @At("HEAD") + ) + private void moonrise$noteSectionArrayBorrowed(final CallbackInfoReturnable cir) { + if ((Object)this instanceof RandomTickLevelChunk chunk) { + chunk.moonrise$noteSectionArrayBorrowed(); + } + } + + /** + * @reason Avoid routing {@code getSection} through {@code getSections}, which would + * mark every block lookup as a FAWE-style array borrow. + * @author HabsW + */ + @Overwrite + public LevelChunkSection getSection(final int sectionIndex) { + return this.sections[sectionIndex]; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java index 97f45de..1258cb4 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/LevelChunkMixin.java @@ -13,7 +13,9 @@ import net.minecraft.world.level.chunk.UpgradeData; import net.minecraft.world.level.levelgen.blending.BlendingData; import net.minecraft.world.ticks.LevelChunkTicks; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -28,6 +30,10 @@ public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, f super(chunkPos, upgradeData, levelHeightAccessor, palettedContainerFactory, l, levelChunkSections, blendingData); } + @Shadow + @Final + protected LevelChunkSection[] sections; + @Unique private long[] moonrise$randomTickSectionMask; @@ -37,6 +43,12 @@ public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, f @Unique private LevelChunkSection[] moonrise$boundSectionRefs; + @Unique + private int moonrise$randomTickGetSectionsSuppress; + + @Unique + private boolean moonrise$sectionArrayBorrowed; + @Override public final void moonrise$noteRandomTickSection(final int index, final boolean ticking) { final long[] mask = this.moonrise$randomTickSectionMask; @@ -77,7 +89,7 @@ public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, f @Override public final void moonrise$bindRandomTickSections() { - final LevelChunkSection[] sections = this.getSections(); + final LevelChunkSection[] sections = this.sections; final LevelChunkSection[] oldRefs = this.moonrise$boundSectionRefs; if (oldRefs != null) { for (int i = 0; i < oldRefs.length; i++) { @@ -109,12 +121,38 @@ public LevelChunkMixin(final ChunkPos chunkPos, final UpgradeData upgradeData, f @Override public final void moonrise$ensureRandomTickSections() { - final LevelChunkSection[] sections = this.getSections(); + final LevelChunkSection[] sections = this.sections; if (this.moonrise$randomTickSectionMask == null || this.moonrise$sectionRefsChanged(sections)) { this.moonrise$bindRandomTickSections(); } } + @Override + public final void moonrise$beginRandomTickGetSections() { + this.moonrise$randomTickGetSectionsSuppress++; + } + + @Override + public final void moonrise$endRandomTickGetSections() { + this.moonrise$randomTickGetSectionsSuppress--; + } + + @Override + public final void moonrise$noteSectionArrayBorrowed() { + if (this.moonrise$randomTickGetSectionsSuppress == 0) { + this.moonrise$sectionArrayBorrowed = true; + } + } + + @Override + public final boolean moonrise$consumeSectionArrayBorrowed() { + if (!this.moonrise$sectionArrayBorrowed) { + return false; + } + this.moonrise$sectionArrayBorrowed = false; + return true; + } + @Override public final int moonrise$randomTickEligibleCount() { return this.moonrise$randomTickEligibleCount; diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java index 8a7d78d..51c24b2 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/random_ticking/ServerLevelMixin.java @@ -74,7 +74,13 @@ private int nextInt(final RandomSource instance, final int bound) { ) private LevelChunkSection[] optimiseRandomTick(final LevelChunk chunk, @Local(ordinal = 0, argsOnly = true) final int tickSpeed) { + final RandomTickLevelChunk randomTickChunk = (RandomTickLevelChunk)chunk; + randomTickChunk.moonrise$beginRandomTickGetSections(); final LevelChunkSection[] sections = chunk.getSections(); + randomTickChunk.moonrise$endRandomTickGetSections(); + if (randomTickChunk.moonrise$randomTickSectionMask() == null || randomTickChunk.moonrise$consumeSectionArrayBorrowed()) { + randomTickChunk.moonrise$ensureRandomTickSections(); + } final int minSection = WorldUtil.getMinSection((ServerLevel)(Object)this); final SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; final boolean doubleTickFluids = !PlatformHooks.get().configFixMC224294(); @@ -86,10 +92,6 @@ private LevelChunkSection[] optimiseRandomTick(final LevelChunk chunk, // Empty sections never consume random-tick RNG (the inner loop is behind tickingBlockCount > 0). // Walk only those sections when sparse; dense chunks keep the original linear scan. - final RandomTickLevelChunk randomTickChunk = (RandomTickLevelChunk)chunk; - // Rebuild if never bound, or if a caller replaced LevelChunkSection objects in getSections() - // (WorldEdit/FAWE-style). Vanilla has no setSection hook. - randomTickChunk.moonrise$ensureRandomTickSections(); final int eligible = randomTickChunk.moonrise$randomTickEligibleCount(); if (eligible <= 0) { return EMPTY_SECTION_ARRAY; diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java index 2f893bf..722b9c5 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/random_ticking/RandomTickLevelChunk.java @@ -8,6 +8,14 @@ public interface RandomTickLevelChunk { public void moonrise$ensureRandomTickSections(); + public void moonrise$beginRandomTickGetSections(); + + public void moonrise$endRandomTickGetSections(); + + public void moonrise$noteSectionArrayBorrowed(); + + public boolean moonrise$consumeSectionArrayBorrowed(); + public int moonrise$randomTickEligibleCount(); public long[] moonrise$randomTickSectionMask(); diff --git a/src/main/resources/moonrise.mixins.json b/src/main/resources/moonrise.mixins.json index 51ce269..b0f8133 100644 --- a/src/main/resources/moonrise.mixins.json +++ b/src/main/resources/moonrise.mixins.json @@ -112,6 +112,7 @@ "random.LevelMixin", "random_ticking.BiomeManagerMixin", "random_ticking.BiomeMixin", + "random_ticking.ChunkAccessMixin", "random_ticking.LevelChunkMixin", "random_ticking.LevelMixin", "random_ticking.ServerLevelMixin",