package net.minecraft.world.level.block; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.Object2IntMap.Entry; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Vec3i; import net.minecraft.nbt.CompoundTag; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.tags.BlockTags; import net.minecraft.tags.TagKey; import net.minecraft.util.RandomSource; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; public class SculkSpreader { public static final int MAX_GROWTH_RATE_RADIUS = 24; public static final int MAX_CHARGE = 1000; public static final float MAX_DECAY_FACTOR = 0.5F; private static final int MAX_CURSORS = 32; public static final int SHRIEKER_PLACEMENT_RATE = 11; public static final int MAX_CURSOR_DISTANCE = 1024; final boolean isWorldGeneration; private final TagKey replaceableBlocks; private final int growthSpawnCost; private final int noGrowthRadius; private final int chargeDecayRate; private final int additionalDecayRate; private List cursors = new ArrayList(); public SculkSpreader( boolean isWorldGeneration, TagKey replaceableBlocks, int growthSpawnCoat, int noGrowthRadius, int chargeDecayRate, int additionalDecayRate ) { this.isWorldGeneration = isWorldGeneration; this.replaceableBlocks = replaceableBlocks; this.growthSpawnCost = growthSpawnCoat; this.noGrowthRadius = noGrowthRadius; this.chargeDecayRate = chargeDecayRate; this.additionalDecayRate = additionalDecayRate; } public static SculkSpreader createLevelSpreader() { return new SculkSpreader(false, BlockTags.SCULK_REPLACEABLE, 10, 4, 10, 5); } public static SculkSpreader createWorldGenSpreader() { return new SculkSpreader(true, BlockTags.SCULK_REPLACEABLE_WORLD_GEN, 50, 1, 5, 10); } public TagKey replaceableBlocks() { return this.replaceableBlocks; } public int growthSpawnCost() { return this.growthSpawnCost; } public int noGrowthRadius() { return this.noGrowthRadius; } public int chargeDecayRate() { return this.chargeDecayRate; } public int additionalDecayRate() { return this.additionalDecayRate; } public boolean isWorldGeneration() { return this.isWorldGeneration; } @VisibleForTesting public List getCursors() { return this.cursors; } public void clear() { this.cursors.clear(); } public void load(CompoundTag tag) { this.cursors.clear(); ((List)tag.read("cursors", SculkSpreader.ChargeCursor.CODEC.sizeLimitedListOf(32)).orElse(List.of())).forEach(this::addCursor); } public void save(CompoundTag tag) { tag.store("cursors", SculkSpreader.ChargeCursor.CODEC.listOf(), this.cursors); } public void addCursors(BlockPos pos, int charge) { while (charge > 0) { int i = Math.min(charge, 1000); this.addCursor(new SculkSpreader.ChargeCursor(pos, i)); charge -= i; } } private void addCursor(SculkSpreader.ChargeCursor cursor) { if (this.cursors.size() < 32) { this.cursors.add(cursor); } } public void updateCursors(LevelAccessor level, BlockPos pos, RandomSource random, boolean shouldConvertBlocks) { if (!this.cursors.isEmpty()) { List list = new ArrayList(); Map map = new HashMap(); Object2IntMap object2IntMap = new Object2IntOpenHashMap<>(); for (SculkSpreader.ChargeCursor chargeCursor : this.cursors) { if (!chargeCursor.isPosUnreasonable(pos)) { chargeCursor.update(level, pos, random, this, shouldConvertBlocks); if (chargeCursor.charge <= 0) { level.levelEvent(3006, chargeCursor.getPos(), 0); } else { BlockPos blockPos = chargeCursor.getPos(); object2IntMap.computeInt(blockPos, (blockPosx, integer) -> (integer == null ? 0 : integer) + chargeCursor.charge); SculkSpreader.ChargeCursor chargeCursor2 = (SculkSpreader.ChargeCursor)map.get(blockPos); if (chargeCursor2 == null) { map.put(blockPos, chargeCursor); list.add(chargeCursor); } else if (!this.isWorldGeneration() && chargeCursor.charge + chargeCursor2.charge <= 1000) { chargeCursor2.mergeWith(chargeCursor); } else { list.add(chargeCursor); if (chargeCursor.charge < chargeCursor2.charge) { map.put(blockPos, chargeCursor); } } } } } for (Entry entry : object2IntMap.object2IntEntrySet()) { BlockPos blockPos = (BlockPos)entry.getKey(); int i = entry.getIntValue(); SculkSpreader.ChargeCursor chargeCursor3 = (SculkSpreader.ChargeCursor)map.get(blockPos); Collection collection = chargeCursor3 == null ? null : chargeCursor3.getFacingData(); if (i > 0 && collection != null) { int j = (int)(Math.log1p(i) / 2.3F) + 1; int k = (j << 6) + MultifaceBlock.pack(collection); level.levelEvent(3006, blockPos, k); } } this.cursors = list; } } public static class ChargeCursor { private static final ObjectArrayList NON_CORNER_NEIGHBOURS = Util.make( new ObjectArrayList<>(18), objectArrayList -> BlockPos.betweenClosedStream(new BlockPos(-1, -1, -1), new BlockPos(1, 1, 1)) .filter(blockPos -> (blockPos.getX() == 0 || blockPos.getY() == 0 || blockPos.getZ() == 0) && !blockPos.equals(BlockPos.ZERO)) .map(BlockPos::immutable) .forEach(objectArrayList::add) ); public static final int MAX_CURSOR_DECAY_DELAY = 1; private BlockPos pos; int charge; private int updateDelay; private int decayDelay; @Nullable private Set facings; private static final Codec> DIRECTION_SET = Direction.CODEC.listOf().xmap(list -> Sets.newEnumSet(list, Direction.class), Lists::newArrayList); public static final Codec CODEC = RecordCodecBuilder.create( instance -> instance.group( BlockPos.CODEC.fieldOf("pos").forGetter(SculkSpreader.ChargeCursor::getPos), Codec.intRange(0, 1000).fieldOf("charge").orElse(0).forGetter(SculkSpreader.ChargeCursor::getCharge), Codec.intRange(0, 1).fieldOf("decay_delay").orElse(1).forGetter(SculkSpreader.ChargeCursor::getDecayDelay), Codec.intRange(0, Integer.MAX_VALUE).fieldOf("update_delay").orElse(0).forGetter(chargeCursor -> chargeCursor.updateDelay), DIRECTION_SET.lenientOptionalFieldOf("facings").forGetter(chargeCursor -> Optional.ofNullable(chargeCursor.getFacingData())) ) .apply(instance, SculkSpreader.ChargeCursor::new) ); private ChargeCursor(BlockPos pos, int charge, int decayDelay, int updateDelay, Optional> facings) { this.pos = pos; this.charge = charge; this.decayDelay = decayDelay; this.updateDelay = updateDelay; this.facings = (Set)facings.orElse(null); } public ChargeCursor(BlockPos pos, int charge) { this(pos, charge, 1, 0, Optional.empty()); } public BlockPos getPos() { return this.pos; } boolean isPosUnreasonable(BlockPos pos) { return this.pos.distChessboard(pos) > 1024; } public int getCharge() { return this.charge; } public int getDecayDelay() { return this.decayDelay; } @Nullable public Set getFacingData() { return this.facings; } private boolean shouldUpdate(LevelAccessor level, BlockPos pos, boolean isWorldGeneration) { if (this.charge <= 0) { return false; } else if (isWorldGeneration) { return true; } else { return level instanceof ServerLevel serverLevel ? serverLevel.shouldTickBlocksAt(pos) : false; } } public void update(LevelAccessor level, BlockPos pos, RandomSource random, SculkSpreader spreader, boolean shouldConvertBlocks) { if (this.shouldUpdate(level, pos, spreader.isWorldGeneration)) { if (this.updateDelay > 0) { this.updateDelay--; } else { BlockState blockState = level.getBlockState(this.pos); SculkBehaviour sculkBehaviour = getBlockBehaviour(blockState); if (shouldConvertBlocks && sculkBehaviour.attemptSpreadVein(level, this.pos, blockState, this.facings, spreader.isWorldGeneration())) { if (sculkBehaviour.canChangeBlockStateOnSpread()) { blockState = level.getBlockState(this.pos); sculkBehaviour = getBlockBehaviour(blockState); } level.playSound(null, this.pos, SoundEvents.SCULK_BLOCK_SPREAD, SoundSource.BLOCKS, 1.0F, 1.0F); } this.charge = sculkBehaviour.attemptUseCharge(this, level, pos, random, spreader, shouldConvertBlocks); if (this.charge <= 0) { sculkBehaviour.onDischarged(level, blockState, this.pos, random); } else { BlockPos blockPos = getValidMovementPos(level, this.pos, random); if (blockPos != null) { sculkBehaviour.onDischarged(level, blockState, this.pos, random); this.pos = blockPos.immutable(); if (spreader.isWorldGeneration() && !this.pos.closerThan(new Vec3i(pos.getX(), this.pos.getY(), pos.getZ()), 15.0)) { this.charge = 0; return; } blockState = level.getBlockState(blockPos); } if (blockState.getBlock() instanceof SculkBehaviour) { this.facings = MultifaceBlock.availableFaces(blockState); } this.decayDelay = sculkBehaviour.updateDecayDelay(this.decayDelay); this.updateDelay = sculkBehaviour.getSculkSpreadDelay(); } } } } void mergeWith(SculkSpreader.ChargeCursor cursor) { this.charge = this.charge + cursor.charge; cursor.charge = 0; this.updateDelay = Math.min(this.updateDelay, cursor.updateDelay); } private static SculkBehaviour getBlockBehaviour(BlockState state) { return state.getBlock() instanceof SculkBehaviour sculkBehaviour ? sculkBehaviour : SculkBehaviour.DEFAULT; } private static List getRandomizedNonCornerNeighbourOffsets(RandomSource random) { return Util.shuffledCopy(NON_CORNER_NEIGHBOURS, random); } @Nullable private static BlockPos getValidMovementPos(LevelAccessor level, BlockPos pos, RandomSource random) { BlockPos.MutableBlockPos mutableBlockPos = pos.mutable(); BlockPos.MutableBlockPos mutableBlockPos2 = pos.mutable(); for (Vec3i vec3i : getRandomizedNonCornerNeighbourOffsets(random)) { mutableBlockPos2.setWithOffset(pos, vec3i); BlockState blockState = level.getBlockState(mutableBlockPos2); if (blockState.getBlock() instanceof SculkBehaviour && isMovementUnobstructed(level, pos, mutableBlockPos2)) { mutableBlockPos.set(mutableBlockPos2); if (SculkVeinBlock.hasSubstrateAccess(level, blockState, mutableBlockPos2)) { break; } } } return mutableBlockPos.equals(pos) ? null : mutableBlockPos; } private static boolean isMovementUnobstructed(LevelAccessor level, BlockPos fromPos, BlockPos toPos) { if (fromPos.distManhattan(toPos) == 1) { return true; } else { BlockPos blockPos = toPos.subtract(fromPos); Direction direction = Direction.fromAxisAndDirection( Direction.Axis.X, blockPos.getX() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE ); Direction direction2 = Direction.fromAxisAndDirection( Direction.Axis.Y, blockPos.getY() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE ); Direction direction3 = Direction.fromAxisAndDirection( Direction.Axis.Z, blockPos.getZ() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE ); if (blockPos.getX() == 0) { return isUnobstructed(level, fromPos, direction2) || isUnobstructed(level, fromPos, direction3); } else { return blockPos.getY() == 0 ? isUnobstructed(level, fromPos, direction) || isUnobstructed(level, fromPos, direction3) : isUnobstructed(level, fromPos, direction) || isUnobstructed(level, fromPos, direction2); } } } private static boolean isUnobstructed(LevelAccessor level, BlockPos pos, Direction direction) { BlockPos blockPos = pos.relative(direction); return !level.getBlockState(blockPos).isFaceSturdy(level, blockPos, direction.getOpposite()); } } }