Skip to content

Commit

Permalink
Fast-path exit end portal search by counting bedrock blocks
Browse files Browse the repository at this point in the history
Useful during ender dragon respawning when placing the last end crystal
#618
  • Loading branch information
2No2Name committed Feb 1, 2025
1 parent 50edc12 commit 9376fca
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Lithium _LithiumVersion_ for Minecraft _MCVersion_ adds many new features.
Make sure to take a backup of your world before using the mod and please report any bugs and mod compatibility issues at the [issue tracker](https://github.com/CaffeineMC/lithium-fabric/issues). You can check the [description of each optimization](https://github.com/CaffeineMC/lithium/blob/_ReleaseTag_/lithium-mixin-config.md) and how to disable it when encountering a problem.

## Additions
- Some new stuff
- Fast-path exit end portal search by counting nearby bedrock blocks. Reduces lag when placing the last end crystal when respawning the ender dragon.

## Changes
- Many changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.caffeinemc.mods.lithium.common.world.block_pattern_matching;

import net.minecraft.world.level.block.Block;

public interface BlockPatternExtended {

void lithium$setRequiredBlock(Block block, int count);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package net.caffeinemc.mods.lithium.common.world.block_pattern_matching;

import net.caffeinemc.mods.lithium.common.util.Pos;
import net.minecraft.core.BlockBox;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;

import java.util.function.Predicate;

public class BlockSearch {

public static boolean hasAtLeast(LevelReader levelReader, BlockBox searchBox, Block requiredBlock, int requiredBlockCount) {
Predicate<BlockState> predicate = blockState -> blockState.is(requiredBlock);
for (int chunkX = SectionPos.blockToSectionCoord(searchBox.min().getX()); chunkX <= SectionPos.blockToSectionCoord(searchBox.max().getX()); chunkX++) {
for (int chunkZ = SectionPos.blockToSectionCoord(searchBox.min().getZ()); chunkZ <= SectionPos.blockToSectionCoord(searchBox.max().getZ()); chunkZ++) {

ChunkAccess chunk = levelReader.getChunk(chunkX, chunkZ);
int minSectionYIndex = Pos.SectionYIndex.fromBlockCoord(levelReader, searchBox.min().getY());
int maxSectionYIndex = Pos.SectionYIndex.fromBlockCoord(levelReader, searchBox.max().getY());
for (int sectionYIndex = minSectionYIndex; sectionYIndex <= maxSectionYIndex; sectionYIndex++) {
if (sectionYIndex >= 0 && sectionYIndex <= chunk.getSectionsCount()) {
LevelChunkSection section = chunk.getSection(sectionYIndex);
if (section.maybeHas(predicate)) {
int sectionYCoord = Pos.SectionYCoord.fromSectionIndex(levelReader, sectionYIndex);
requiredBlockCount -= countBlocksInBoxInSection(
section,
Math.max(searchBox.min().getX(), chunkX << 4),
Math.max(searchBox.min().getY(), sectionYCoord << 4),
Math.max(searchBox.min().getZ(), chunkZ << 4),
Math.min(searchBox.max().getX(), (chunkX << 4) + 15),
Math.min(searchBox.max().getY(), (sectionYCoord << 4) + 15),
Math.min(searchBox.max().getZ(), (chunkZ << 4) + 15),
requiredBlock, requiredBlockCount
);
if (requiredBlockCount <= 0) {
return true;
}
}
} else if (requiredBlock == Blocks.VOID_AIR) {
return true; //Handle VOID_AIR somewhat correctly. We should count the volume, but noone uses void air anyway
}
}
}
}
return false;
}

public static int countBlocksInBoxInSection(LevelChunkSection section, int minX, int minY, int minZ, int maxX, int maxY, int maxZ, Block requiredBlock, int findMax) {
int found = 0;
//Optimized iteration order xzy
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
if (section.getBlockState(x & 15, y & 15, z & 15).is(requiredBlock)) {
found++;
if (found >= findMax) {
return found;
}
}
}
}
}
return found;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package net.caffeinemc.mods.lithium.mixin.block_pattern_matching;

import com.llamalad7.mixinextras.sugar.Local;
import net.caffeinemc.mods.lithium.common.world.block_pattern_matching.BlockPatternExtended;
import net.caffeinemc.mods.lithium.common.world.block_pattern_matching.BlockSearch;
import net.minecraft.core.BlockBox;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.pattern.BlockPattern;
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.CallbackInfoReturnable;

@Mixin(BlockPattern.class)
public class BlockPatternMixin implements BlockPatternExtended {


@Unique
private Block requiredBlock;
@Unique
private int requiredBlockCount;

@Inject(
method = "find", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos;offset(III)Lnet/minecraft/core/BlockPos;"), cancellable = true
)
private void countRequiredBlocksBeforeExpensiveSearch(LevelReader levelReader, BlockPos blockPos, CallbackInfoReturnable<BlockPattern.BlockPatternMatch> cir, @Local int size) {
if (this.requiredBlock != null) {
BlockPos maxCorner = blockPos.offset(2 * size - 1, 2 * size - 1, 2 * size - 1);
BlockPos minCorner = blockPos.offset(-size, -size, -size);

BlockBox searchBox = BlockBox.of(minCorner, maxCorner);
if (!BlockSearch.hasAtLeast(levelReader, searchBox, this.requiredBlock, this.requiredBlockCount)) {
cir.setReturnValue(null);
}
}
}

@Override
public void lithium$setRequiredBlock(Block block, int count) {
this.requiredBlock = block;
this.requiredBlockCount = count;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.caffeinemc.mods.lithium.mixin.block_pattern_matching;

import net.caffeinemc.mods.lithium.common.world.block_pattern_matching.BlockPatternExtended;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.pattern.BlockPattern;
import net.minecraft.world.level.dimension.end.EndDragonFight;
import org.spongepowered.asm.mixin.Final;
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(EndDragonFight.class)
public class EndDragonFightMixin {

@Shadow
@Final
private BlockPattern exitPortalPattern;

@Inject(
method = "<init>(Lnet/minecraft/server/level/ServerLevel;JLnet/minecraft/world/level/dimension/end/EndDragonFight$Data;Lnet/minecraft/core/BlockPos;)V", at = @At("RETURN")
)
private void setPatternToDragonPattern(ServerLevel serverLevel, long l, EndDragonFight.Data data, BlockPos blockPos, CallbackInfo ci) {
//Small todo: Find a way to not hardcode this, as this breaks mod compatibility when modifying the exit portal pattern
((BlockPatternExtended) this.exitPortalPattern).lithium$setRequiredBlock(Blocks.BEDROCK, 41);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@MixinConfigOption(description = "Fast-path exit end portal search by counting nearby bedrock blocks. Reduces lag when placing the last end crystal when respawning the ender dragon.")
package net.caffeinemc.mods.lithium.mixin.block_pattern_matching;

import net.caffeinemc.gradle.MixinConfigOption;
2 changes: 2 additions & 0 deletions common/src/main/resources/lithium.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
"block.redstone_wire.DefaultRedstoneWireEvaluatorMixin",
"block.redstone_wire.RedStoneWireBlockMixin",
"block.redstone_wire.RedstoneWireEvaluatorMixin",
"block_pattern_matching.BlockPatternMixin",
"block_pattern_matching.EndDragonFightMixin",
"cached_hashcode.FlowingFluid$BlockStatePairKeyMixin",
"chunk.entity_class_groups.ClassInstanceMultiMapMixin",
"chunk.no_locking.LevelChunkSectionMixin",
Expand Down
4 changes: 4 additions & 0 deletions lithium-fabric-mixin-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ Moving blocks and retracting pistons avoid calculating their VoxelShapes by reus
(default: `true`)
Redstone wire power calculations avoid duplicate block accesses

### `mixin.block_pattern_matching`
(default: `true`)
Fast-path exit end portal search by counting nearby bedrock blocks. Reduces lag when placing the last end crystal when respawning the ender dragon.

### `mixin.cached_hashcode`
(default: `true`)
BlockNeighborGroups used in fluid code cache their hashcode
Expand Down
4 changes: 4 additions & 0 deletions lithium-neoforge-mixin-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ Moving blocks and retracting pistons avoid calculating their VoxelShapes by reus
(default: `true`)
Redstone wire power calculations avoid duplicate block accesses

### `mixin.block_pattern_matching`
(default: `true`)
Fast-path exit end portal search by counting nearby bedrock blocks. Reduces lag when placing the last end crystal when respawning the ender dragon.

### `mixin.cached_hashcode`
(default: `true`)
BlockNeighborGroups used in fluid code cache their hashcode
Expand Down

0 comments on commit 9376fca

Please sign in to comment.