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 com.mojang.serialization.codecs.RecordCodecBuilder.Instance; 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 javax.annotation.Nullable; 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; 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 p_222248_, TagKey p_222249_, int p_222250_, int p_222251_, int p_222252_, int p_222253_) { this.isWorldGeneration = p_222248_; this.replaceableBlocks = p_222249_; this.growthSpawnCost = p_222250_; this.noGrowthRadius = p_222251_; this.chargeDecayRate = p_222252_; this.additionalDecayRate = p_222253_; } 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 p_222270_) { this.cursors.clear(); p_222270_.read("cursors", SculkSpreader.ChargeCursor.CODEC.sizeLimitedListOf(32)).orElse(List.of()).forEach(this::addCursor); } public void save(CompoundTag p_222276_) { p_222276_.store("cursors", SculkSpreader.ChargeCursor.CODEC.listOf(), this.cursors); } public void addCursors(BlockPos p_222267_, int p_222268_) { while (p_222268_ > 0) { int i = Math.min(p_222268_, 1000); this.addCursor(new SculkSpreader.ChargeCursor(p_222267_, i)); p_222268_ -= i; } } private void addCursor(SculkSpreader.ChargeCursor p_222261_) { if (this.cursors.size() < 32) { this.cursors.add(p_222261_); } } public void updateCursors(LevelAccessor p_222256_, BlockPos p_222257_, RandomSource p_222258_, boolean p_222259_) { if (!this.cursors.isEmpty()) { List list = new ArrayList<>(); Map map = new HashMap<>(); Object2IntMap object2intmap = new Object2IntOpenHashMap<>(); for (SculkSpreader.ChargeCursor sculkspreader$chargecursor : this.cursors) { if (!sculkspreader$chargecursor.isPosUnreasonable(p_222257_)) { sculkspreader$chargecursor.update(p_222256_, p_222257_, p_222258_, this, p_222259_); if (sculkspreader$chargecursor.charge <= 0) { p_222256_.levelEvent(3006, sculkspreader$chargecursor.getPos(), 0); } else { BlockPos blockpos = sculkspreader$chargecursor.getPos(); object2intmap.computeInt(blockpos, (p_222264_, p_222265_) -> (p_222265_ == null ? 0 : p_222265_) + sculkspreader$chargecursor.charge); SculkSpreader.ChargeCursor sculkspreader$chargecursor1 = map.get(blockpos); if (sculkspreader$chargecursor1 == null) { map.put(blockpos, sculkspreader$chargecursor); list.add(sculkspreader$chargecursor); } else if (!this.isWorldGeneration() && sculkspreader$chargecursor.charge + sculkspreader$chargecursor1.charge <= 1000) { sculkspreader$chargecursor1.mergeWith(sculkspreader$chargecursor); } else { list.add(sculkspreader$chargecursor); if (sculkspreader$chargecursor.charge < sculkspreader$chargecursor1.charge) { map.put(blockpos, sculkspreader$chargecursor); } } } } } for (Entry entry : object2intmap.object2IntEntrySet()) { BlockPos blockpos1 = entry.getKey(); int k = entry.getIntValue(); SculkSpreader.ChargeCursor sculkspreader$chargecursor2 = map.get(blockpos1); Collection collection = sculkspreader$chargecursor2 == null ? null : sculkspreader$chargecursor2.getFacingData(); if (k > 0 && collection != null) { int i = (int)(Math.log1p(k) / 2.3F) + 1; int j = (i << 6) + MultifaceBlock.pack(collection); p_222256_.levelEvent(3006, blockpos1, j); } } this.cursors = list; } } public static class ChargeCursor { private static final ObjectArrayList NON_CORNER_NEIGHBOURS = Util.make( new ObjectArrayList<>(18), p_222338_ -> BlockPos.betweenClosedStream(new BlockPos(-1, -1, -1), new BlockPos(1, 1, 1)) .filter( p_222336_ -> (p_222336_.getX() == 0 || p_222336_.getY() == 0 || p_222336_.getZ() == 0) && !p_222336_.equals(BlockPos.ZERO) ) .map(BlockPos::immutable) .forEach(p_222338_::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(p_222340_ -> Sets.newEnumSet(p_222340_, Direction.class), Lists::newArrayList); public static final Codec CODEC = RecordCodecBuilder.create( p_222330_ -> p_222330_.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(p_222346_ -> p_222346_.updateDelay), DIRECTION_SET.lenientOptionalFieldOf("facings").forGetter(p_222343_ -> Optional.ofNullable(p_222343_.getFacingData())) ) .apply(p_222330_, SculkSpreader.ChargeCursor::new) ); private ChargeCursor(BlockPos p_222299_, int p_222300_, int p_222301_, int p_222302_, Optional> p_222303_) { this.pos = p_222299_; this.charge = p_222300_; this.decayDelay = p_222301_; this.updateDelay = p_222302_; this.facings = p_222303_.orElse(null); } public ChargeCursor(BlockPos p_222296_, int p_222297_) { this(p_222296_, p_222297_, 1, 0, Optional.empty()); } public BlockPos getPos() { return this.pos; } boolean isPosUnreasonable(BlockPos p_363353_) { return this.pos.distChessboard(p_363353_) > 1024; } public int getCharge() { return this.charge; } public int getDecayDelay() { return this.decayDelay; } @Nullable public Set getFacingData() { return this.facings; } private boolean shouldUpdate(LevelAccessor p_222326_, BlockPos p_222327_, boolean p_222328_) { if (this.charge <= 0) { return false; } else if (p_222328_) { return true; } else { return p_222326_ instanceof ServerLevel serverlevel ? serverlevel.shouldTickBlocksAt(p_222327_) : false; } } public void update(LevelAccessor p_222312_, BlockPos p_222313_, RandomSource p_222314_, SculkSpreader p_222315_, boolean p_222316_) { if (this.shouldUpdate(p_222312_, p_222313_, p_222315_.isWorldGeneration)) { if (this.updateDelay > 0) { this.updateDelay--; } else { BlockState blockstate = p_222312_.getBlockState(this.pos); SculkBehaviour sculkbehaviour = getBlockBehaviour(blockstate); if (p_222316_ && sculkbehaviour.attemptSpreadVein(p_222312_, this.pos, blockstate, this.facings, p_222315_.isWorldGeneration())) { if (sculkbehaviour.canChangeBlockStateOnSpread()) { blockstate = p_222312_.getBlockState(this.pos); sculkbehaviour = getBlockBehaviour(blockstate); } p_222312_.playSound(null, this.pos, SoundEvents.SCULK_BLOCK_SPREAD, SoundSource.BLOCKS, 1.0F, 1.0F); } this.charge = sculkbehaviour.attemptUseCharge(this, p_222312_, p_222313_, p_222314_, p_222315_, p_222316_); if (this.charge <= 0) { sculkbehaviour.onDischarged(p_222312_, blockstate, this.pos, p_222314_); } else { BlockPos blockpos = getValidMovementPos(p_222312_, this.pos, p_222314_); if (blockpos != null) { sculkbehaviour.onDischarged(p_222312_, blockstate, this.pos, p_222314_); this.pos = blockpos.immutable(); if (p_222315_.isWorldGeneration() && !this.pos.closerThan(new Vec3i(p_222313_.getX(), this.pos.getY(), p_222313_.getZ()), 15.0)) { this.charge = 0; return; } blockstate = p_222312_.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 p_222332_) { this.charge = this.charge + p_222332_.charge; p_222332_.charge = 0; this.updateDelay = Math.min(this.updateDelay, p_222332_.updateDelay); } private static SculkBehaviour getBlockBehaviour(BlockState p_222334_) { return p_222334_.getBlock() instanceof SculkBehaviour sculkbehaviour ? sculkbehaviour : SculkBehaviour.DEFAULT; } private static List getRandomizedNonCornerNeighbourOffsets(RandomSource p_222306_) { return Util.shuffledCopy(NON_CORNER_NEIGHBOURS, p_222306_); } @Nullable private static BlockPos getValidMovementPos(LevelAccessor p_222308_, BlockPos p_222309_, RandomSource p_222310_) { BlockPos.MutableBlockPos blockpos$mutableblockpos = p_222309_.mutable(); BlockPos.MutableBlockPos blockpos$mutableblockpos1 = p_222309_.mutable(); for (Vec3i vec3i : getRandomizedNonCornerNeighbourOffsets(p_222310_)) { blockpos$mutableblockpos1.setWithOffset(p_222309_, vec3i); BlockState blockstate = p_222308_.getBlockState(blockpos$mutableblockpos1); if (blockstate.getBlock() instanceof SculkBehaviour && isMovementUnobstructed(p_222308_, p_222309_, blockpos$mutableblockpos1)) { blockpos$mutableblockpos.set(blockpos$mutableblockpos1); if (SculkVeinBlock.hasSubstrateAccess(p_222308_, blockstate, blockpos$mutableblockpos1)) { break; } } } return blockpos$mutableblockpos.equals(p_222309_) ? null : blockpos$mutableblockpos; } private static boolean isMovementUnobstructed(LevelAccessor p_222318_, BlockPos p_222319_, BlockPos p_222320_) { if (p_222319_.distManhattan(p_222320_) == 1) { return true; } else { BlockPos blockpos = p_222320_.subtract(p_222319_); Direction direction = Direction.fromAxisAndDirection( Direction.Axis.X, blockpos.getX() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE ); Direction direction1 = Direction.fromAxisAndDirection( Direction.Axis.Y, blockpos.getY() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE ); Direction direction2 = Direction.fromAxisAndDirection( Direction.Axis.Z, blockpos.getZ() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE ); if (blockpos.getX() == 0) { return isUnobstructed(p_222318_, p_222319_, direction1) || isUnobstructed(p_222318_, p_222319_, direction2); } else { return blockpos.getY() == 0 ? isUnobstructed(p_222318_, p_222319_, direction) || isUnobstructed(p_222318_, p_222319_, direction2) : isUnobstructed(p_222318_, p_222319_, direction) || isUnobstructed(p_222318_, p_222319_, direction1); } } } private static boolean isUnobstructed(LevelAccessor p_222322_, BlockPos p_222323_, Direction p_222324_) { BlockPos blockpos = p_222323_.relative(p_222324_); return !p_222322_.getBlockState(blockpos).isFaceSturdy(p_222322_, blockpos, p_222324_.getOpposite()); } } }