2025-11-24 22:52:51 +03:00

348 lines
16 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.google.common.annotations.VisibleForTesting
* com.google.common.collect.Lists
* com.google.common.collect.Sets
* com.mojang.datafixers.kinds.App
* com.mojang.datafixers.kinds.Applicative
* com.mojang.serialization.Codec
* com.mojang.serialization.codecs.RecordCodecBuilder
* it.unimi.dsi.fastutil.objects.Object2IntMap$Entry
* it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
* it.unimi.dsi.fastutil.objects.ObjectArrayList
* org.jspecify.annotations.Nullable
*/
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.datafixers.kinds.App;
import com.mojang.datafixers.kinds.Applicative;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Vec3i;
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.util.Util;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.MultifaceBlock;
import net.minecraft.world.level.block.SculkBehaviour;
import net.minecraft.world.level.block.SculkVeinBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import org.jspecify.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;
private final boolean isWorldGeneration;
private final TagKey<Block> replaceableBlocks;
private final int growthSpawnCost;
private final int noGrowthRadius;
private final int chargeDecayRate;
private final int additionalDecayRate;
private List<ChargeCursor> cursors = new ArrayList<ChargeCursor>();
public SculkSpreader(boolean isWorldGeneration, TagKey<Block> replaceableBlocks, int growthSpawnCost, int noGrowthRadius, int chargeDecayRate, int additionalDecayRate) {
this.isWorldGeneration = isWorldGeneration;
this.replaceableBlocks = replaceableBlocks;
this.growthSpawnCost = growthSpawnCost;
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<Block> 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<ChargeCursor> getCursors() {
return this.cursors;
}
public void clear() {
this.cursors.clear();
}
public void load(ValueInput input) {
this.cursors.clear();
input.read("cursors", ChargeCursor.CODEC.sizeLimitedListOf(32)).orElse(List.of()).forEach(this::addCursor);
}
public void save(ValueOutput output) {
output.store("cursors", ChargeCursor.CODEC.listOf(), this.cursors);
if (SharedConstants.DEBUG_SCULK_CATALYST) {
int charge = this.getCursors().stream().map(ChargeCursor::getCharge).reduce(0, Integer::sum);
int charges = this.getCursors().stream().map(c -> 1).reduce(0, Integer::sum);
int max = this.getCursors().stream().map(ChargeCursor::getCharge).reduce(0, Math::max);
output.putInt("stats.total", charge);
output.putInt("stats.count", charges);
output.putInt("stats.max", max);
output.putInt("stats.avg", charge / (charges + 1));
}
}
public void addCursors(BlockPos startPos, int charge) {
while (charge > 0) {
int currentCharge = Math.min(charge, 1000);
this.addCursor(new ChargeCursor(startPos, currentCharge));
charge -= currentCharge;
}
}
private void addCursor(ChargeCursor cursor) {
if (this.cursors.size() >= 32) {
return;
}
this.cursors.add(cursor);
}
public void updateCursors(LevelAccessor level, BlockPos originPos, RandomSource random, boolean spreadVeins) {
BlockPos pos;
if (this.cursors.isEmpty()) {
return;
}
ArrayList<ChargeCursor> processedCursors = new ArrayList<ChargeCursor>();
HashMap<BlockPos, ChargeCursor> mergeableCursors = new HashMap<BlockPos, ChargeCursor>();
Object2IntOpenHashMap chargeMap = new Object2IntOpenHashMap();
for (ChargeCursor cursor : this.cursors) {
if (cursor.isPosUnreasonable(originPos)) continue;
cursor.update(level, originPos, random, this, spreadVeins);
if (cursor.charge <= 0) {
level.levelEvent(3006, cursor.getPos(), 0);
continue;
}
pos = cursor.getPos();
chargeMap.computeInt((Object)pos, (k, count) -> (count == null ? 0 : count) + cursor.charge);
ChargeCursor existing = (ChargeCursor)mergeableCursors.get(pos);
if (existing == null) {
mergeableCursors.put(pos, cursor);
processedCursors.add(cursor);
continue;
}
if (!this.isWorldGeneration() && cursor.charge + existing.charge <= 1000) {
existing.mergeWith(cursor);
continue;
}
processedCursors.add(cursor);
if (cursor.charge >= existing.charge) continue;
mergeableCursors.put(pos, cursor);
}
for (Object2IntMap.Entry entry : chargeMap.object2IntEntrySet()) {
Set<Direction> faces;
pos = (BlockPos)entry.getKey();
int charge = entry.getIntValue();
ChargeCursor cursor = (ChargeCursor)mergeableCursors.get(pos);
Set<Direction> set = faces = cursor == null ? null : cursor.getFacingData();
if (charge <= 0 || faces == null) continue;
int numParticles = (int)(Math.log1p(charge) / (double)2.3f) + 1;
int data = (numParticles << 6) + MultifaceBlock.pack(faces);
level.levelEvent(3006, pos, data);
}
this.cursors = processedCursors;
}
public static class ChargeCursor {
private static final ObjectArrayList<Vec3i> NON_CORNER_NEIGHBOURS = Util.make(new ObjectArrayList(18), list -> BlockPos.betweenClosedStream(new BlockPos(-1, -1, -1), new BlockPos(1, 1, 1)).filter(position -> (position.getX() == 0 || position.getY() == 0 || position.getZ() == 0) && !position.equals(BlockPos.ZERO)).map(BlockPos::immutable).forEach(arg_0 -> ((ObjectArrayList)list).add(arg_0)));
public static final int MAX_CURSOR_DECAY_DELAY = 1;
private BlockPos pos;
private int charge;
private int updateDelay;
private int decayDelay;
private @Nullable Set<Direction> facings;
private static final Codec<Set<Direction>> DIRECTION_SET = Direction.CODEC.listOf().xmap(l -> Sets.newEnumSet((Iterable)l, Direction.class), Lists::newArrayList);
public static final Codec<ChargeCursor> CODEC = RecordCodecBuilder.create(i -> i.group((App)BlockPos.CODEC.fieldOf("pos").forGetter(ChargeCursor::getPos), (App)Codec.intRange((int)0, (int)1000).fieldOf("charge").orElse((Object)0).forGetter(ChargeCursor::getCharge), (App)Codec.intRange((int)0, (int)1).fieldOf("decay_delay").orElse((Object)1).forGetter(ChargeCursor::getDecayDelay), (App)Codec.intRange((int)0, (int)Integer.MAX_VALUE).fieldOf("update_delay").orElse((Object)0).forGetter(o -> o.updateDelay), (App)DIRECTION_SET.lenientOptionalFieldOf("facings").forGetter(o -> Optional.ofNullable(o.getFacingData()))).apply((Applicative)i, ChargeCursor::new));
private ChargeCursor(BlockPos pos, int charge, int decayDelay, int updateDelay, Optional<Set<Direction>> facings) {
this.pos = pos;
this.charge = charge;
this.decayDelay = decayDelay;
this.updateDelay = updateDelay;
this.facings = facings.orElse(null);
}
public ChargeCursor(BlockPos pos, int charge) {
this(pos, charge, 1, 0, Optional.empty());
}
public BlockPos getPos() {
return this.pos;
}
private boolean isPosUnreasonable(BlockPos originPos) {
return this.pos.distChessboard(originPos) > 1024;
}
public int getCharge() {
return this.charge;
}
public int getDecayDelay() {
return this.decayDelay;
}
public @Nullable Set<Direction> getFacingData() {
return this.facings;
}
private boolean shouldUpdate(LevelAccessor level, BlockPos pos, boolean isWorldGen) {
if (this.charge <= 0) {
return false;
}
if (isWorldGen) {
return true;
}
if (level instanceof ServerLevel) {
ServerLevel serverLevel = (ServerLevel)level;
return serverLevel.shouldTickBlocksAt(pos);
}
return false;
}
public void update(LevelAccessor level, BlockPos originPos, RandomSource random, SculkSpreader spreader, boolean spreadVeins) {
if (!this.shouldUpdate(level, originPos, spreader.isWorldGeneration)) {
return;
}
if (this.updateDelay > 0) {
--this.updateDelay;
return;
}
BlockState currentState = level.getBlockState(this.pos);
SculkBehaviour sculkBehaviour = ChargeCursor.getBlockBehaviour(currentState);
if (spreadVeins && sculkBehaviour.attemptSpreadVein(level, this.pos, currentState, this.facings, spreader.isWorldGeneration())) {
if (sculkBehaviour.canChangeBlockStateOnSpread()) {
currentState = level.getBlockState(this.pos);
sculkBehaviour = ChargeCursor.getBlockBehaviour(currentState);
}
level.playSound(null, this.pos, SoundEvents.SCULK_BLOCK_SPREAD, SoundSource.BLOCKS, 1.0f, 1.0f);
}
this.charge = sculkBehaviour.attemptUseCharge(this, level, originPos, random, spreader, spreadVeins);
if (this.charge <= 0) {
sculkBehaviour.onDischarged(level, currentState, this.pos, random);
return;
}
BlockPos transferPos = ChargeCursor.getValidMovementPos(level, this.pos, random);
if (transferPos != null) {
sculkBehaviour.onDischarged(level, currentState, this.pos, random);
this.pos = transferPos.immutable();
if (spreader.isWorldGeneration() && !this.pos.closerThan(new Vec3i(originPos.getX(), this.pos.getY(), originPos.getZ()), 15.0)) {
this.charge = 0;
return;
}
currentState = level.getBlockState(transferPos);
}
if (currentState.getBlock() instanceof SculkBehaviour) {
this.facings = MultifaceBlock.availableFaces(currentState);
}
this.decayDelay = sculkBehaviour.updateDecayDelay(this.decayDelay);
this.updateDelay = sculkBehaviour.getSculkSpreadDelay();
}
private void mergeWith(ChargeCursor other) {
this.charge += other.charge;
other.charge = 0;
this.updateDelay = Math.min(this.updateDelay, other.updateDelay);
}
private static SculkBehaviour getBlockBehaviour(BlockState state) {
SculkBehaviour behaviour;
Block block = state.getBlock();
return block instanceof SculkBehaviour ? (behaviour = (SculkBehaviour)((Object)block)) : SculkBehaviour.DEFAULT;
}
private static List<Vec3i> getRandomizedNonCornerNeighbourOffsets(RandomSource random) {
return Util.shuffledCopy(NON_CORNER_NEIGHBOURS, random);
}
private static @Nullable BlockPos getValidMovementPos(LevelAccessor level, BlockPos pos, RandomSource random) {
BlockPos.MutableBlockPos sculkPosition = pos.mutable();
BlockPos.MutableBlockPos neighbour = pos.mutable();
for (Vec3i offset : ChargeCursor.getRandomizedNonCornerNeighbourOffsets(random)) {
neighbour.setWithOffset((Vec3i)pos, offset);
BlockState transferee = level.getBlockState(neighbour);
if (!(transferee.getBlock() instanceof SculkBehaviour) || !ChargeCursor.isMovementUnobstructed(level, pos, neighbour)) continue;
sculkPosition.set(neighbour);
if (!SculkVeinBlock.hasSubstrateAccess(level, transferee, neighbour)) continue;
break;
}
return sculkPosition.equals(pos) ? null : sculkPosition;
}
private static boolean isMovementUnobstructed(LevelAccessor level, BlockPos from, BlockPos to) {
if (from.distManhattan(to) == 1) {
return true;
}
BlockPos delta = to.subtract(from);
Direction directionX = Direction.fromAxisAndDirection(Direction.Axis.X, delta.getX() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE);
Direction directionY = Direction.fromAxisAndDirection(Direction.Axis.Y, delta.getY() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE);
Direction directionZ = Direction.fromAxisAndDirection(Direction.Axis.Z, delta.getZ() < 0 ? Direction.AxisDirection.NEGATIVE : Direction.AxisDirection.POSITIVE);
if (delta.getX() == 0) {
return ChargeCursor.isUnobstructed(level, from, directionY) || ChargeCursor.isUnobstructed(level, from, directionZ);
}
if (delta.getY() == 0) {
return ChargeCursor.isUnobstructed(level, from, directionX) || ChargeCursor.isUnobstructed(level, from, directionZ);
}
return ChargeCursor.isUnobstructed(level, from, directionX) || ChargeCursor.isUnobstructed(level, from, directionY);
}
private static boolean isUnobstructed(LevelAccessor level, BlockPos from, Direction direction) {
BlockPos testPos = from.relative(direction);
return !level.getBlockState(testPos).isFaceSturdy(level, testPos, direction.getOpposite());
}
}
}