package net.minecraft.world.level.block.entity; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.HolderLookup; import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponentMap.Builder; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.tags.BlockTags; import net.minecraft.util.ARGB; import net.minecraft.world.LockCode; import net.minecraft.world.MenuProvider; import net.minecraft.world.Nameable; import net.minecraft.world.effect.MobEffect; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.BeaconMenu; import net.minecraft.world.inventory.ContainerData; import net.minecraft.world.inventory.ContainerLevelAccess; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BeaconBeamBlock; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.phys.AABB; import org.jetbrains.annotations.Nullable; public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Nameable { private static final int MAX_LEVELS = 4; /** * A list of effects that beacons can apply. */ public static final List>> BEACON_EFFECTS = List.of( List.of(MobEffects.MOVEMENT_SPEED, MobEffects.DIG_SPEED), List.of(MobEffects.DAMAGE_RESISTANCE, MobEffects.JUMP), List.of(MobEffects.DAMAGE_BOOST), List.of(MobEffects.REGENERATION) ); private static final Set> VALID_EFFECTS = (Set>)BEACON_EFFECTS.stream() .flatMap(Collection::stream) .collect(Collectors.toSet()); public static final int DATA_LEVELS = 0; public static final int DATA_PRIMARY = 1; public static final int DATA_SECONDARY = 2; public static final int NUM_DATA_VALUES = 3; private static final int BLOCKS_CHECK_PER_TICK = 10; private static final Component DEFAULT_NAME = Component.translatable("container.beacon"); private static final String TAG_PRIMARY = "primary_effect"; private static final String TAG_SECONDARY = "secondary_effect"; /** * A list of beam segments for this beacon. */ List beamSections = Lists.newArrayList(); private List checkingBeamSections = Lists.newArrayList(); /** * The number of levels of this beacon's pyramid. */ int levels; private int lastCheckY; /** * The primary effect given by this beacon. */ @Nullable Holder primaryPower; /** * The secondary effect given by this beacon. */ @Nullable Holder secondaryPower; /** * The custom name for this beacon. */ @Nullable private Component name; private LockCode lockKey = LockCode.NO_LOCK; private final ContainerData dataAccess = new ContainerData() { @Override public int get(int index) { return switch (index) { case 0 -> BeaconBlockEntity.this.levels; case 1 -> BeaconMenu.encodeEffect(BeaconBlockEntity.this.primaryPower); case 2 -> BeaconMenu.encodeEffect(BeaconBlockEntity.this.secondaryPower); default -> 0; }; } @Override public void set(int index, int value) { switch (index) { case 0: BeaconBlockEntity.this.levels = value; break; case 1: if (!BeaconBlockEntity.this.level.isClientSide && !BeaconBlockEntity.this.beamSections.isEmpty()) { BeaconBlockEntity.playSound(BeaconBlockEntity.this.level, BeaconBlockEntity.this.worldPosition, SoundEvents.BEACON_POWER_SELECT); } BeaconBlockEntity.this.primaryPower = BeaconBlockEntity.filterEffect(BeaconMenu.decodeEffect(value)); break; case 2: BeaconBlockEntity.this.secondaryPower = BeaconBlockEntity.filterEffect(BeaconMenu.decodeEffect(value)); } } @Override public int getCount() { return 3; } }; @Nullable static Holder filterEffect(@Nullable Holder effect) { return VALID_EFFECTS.contains(effect) ? effect : null; } public BeaconBlockEntity(BlockPos pos, BlockState blockState) { super(BlockEntityType.BEACON, pos, blockState); } public static void tick(Level level, BlockPos pos, BlockState state, BeaconBlockEntity blockEntity) { int i = pos.getX(); int j = pos.getY(); int k = pos.getZ(); BlockPos blockPos; if (blockEntity.lastCheckY < j) { blockPos = pos; blockEntity.checkingBeamSections = Lists.newArrayList(); blockEntity.lastCheckY = pos.getY() - 1; } else { blockPos = new BlockPos(i, blockEntity.lastCheckY + 1, k); } BeaconBlockEntity.BeaconBeamSection beaconBeamSection = blockEntity.checkingBeamSections.isEmpty() ? null : (BeaconBlockEntity.BeaconBeamSection)blockEntity.checkingBeamSections.get(blockEntity.checkingBeamSections.size() - 1); int l = level.getHeight(Heightmap.Types.WORLD_SURFACE, i, k); for (int m = 0; m < 10 && blockPos.getY() <= l; m++) { BlockState blockState = level.getBlockState(blockPos); if (blockState.getBlock() instanceof BeaconBeamBlock beaconBeamBlock) { int n = beaconBeamBlock.getColor().getTextureDiffuseColor(); if (blockEntity.checkingBeamSections.size() <= 1) { beaconBeamSection = new BeaconBlockEntity.BeaconBeamSection(n); blockEntity.checkingBeamSections.add(beaconBeamSection); } else if (beaconBeamSection != null) { if (n == beaconBeamSection.color) { beaconBeamSection.increaseHeight(); } else { beaconBeamSection = new BeaconBlockEntity.BeaconBeamSection(ARGB.average(beaconBeamSection.color, n)); blockEntity.checkingBeamSections.add(beaconBeamSection); } } } else { if (beaconBeamSection == null || blockState.getLightBlock() >= 15 && !blockState.is(Blocks.BEDROCK)) { blockEntity.checkingBeamSections.clear(); blockEntity.lastCheckY = l; break; } beaconBeamSection.increaseHeight(); } blockPos = blockPos.above(); blockEntity.lastCheckY++; } int m = blockEntity.levels; if (level.getGameTime() % 80L == 0L) { if (!blockEntity.beamSections.isEmpty()) { blockEntity.levels = updateBase(level, i, j, k); } if (blockEntity.levels > 0 && !blockEntity.beamSections.isEmpty()) { applyEffects(level, pos, blockEntity.levels, blockEntity.primaryPower, blockEntity.secondaryPower); playSound(level, pos, SoundEvents.BEACON_AMBIENT); } } if (blockEntity.lastCheckY >= l) { blockEntity.lastCheckY = level.getMinY() - 1; boolean bl = m > 0; blockEntity.beamSections = blockEntity.checkingBeamSections; if (!level.isClientSide) { boolean bl2 = blockEntity.levels > 0; if (!bl && bl2) { playSound(level, pos, SoundEvents.BEACON_ACTIVATE); for (ServerPlayer serverPlayer : level.getEntitiesOfClass(ServerPlayer.class, new AABB(i, j, k, i, j - 4, k).inflate(10.0, 5.0, 10.0))) { CriteriaTriggers.CONSTRUCT_BEACON.trigger(serverPlayer, blockEntity.levels); } } else if (bl && !bl2) { playSound(level, pos, SoundEvents.BEACON_DEACTIVATE); } } } } private static int updateBase(Level level, int x, int y, int z) { int i = 0; for (int j = 1; j <= 4; i = j++) { int k = y - j; if (k < level.getMinY()) { break; } boolean bl = true; for (int l = x - j; l <= x + j && bl; l++) { for (int m = z - j; m <= z + j; m++) { if (!level.getBlockState(new BlockPos(l, k, m)).is(BlockTags.BEACON_BASE_BLOCKS)) { bl = false; break; } } } if (!bl) { break; } } return i; } @Override public void setRemoved() { playSound(this.level, this.worldPosition, SoundEvents.BEACON_DEACTIVATE); super.setRemoved(); } private static void applyEffects( Level level, BlockPos pos, int beaconLevel, @Nullable Holder primaryEffect, @Nullable Holder secondaryEffect ) { if (!level.isClientSide && primaryEffect != null) { double d = beaconLevel * 10 + 10; int i = 0; if (beaconLevel >= 4 && Objects.equals(primaryEffect, secondaryEffect)) { i = 1; } int j = (9 + beaconLevel * 2) * 20; AABB aABB = new AABB(pos).inflate(d).expandTowards(0.0, level.getHeight(), 0.0); List list = level.getEntitiesOfClass(Player.class, aABB); for (Player player : list) { player.addEffect(new MobEffectInstance(primaryEffect, j, i, true, true)); } if (beaconLevel >= 4 && !Objects.equals(primaryEffect, secondaryEffect) && secondaryEffect != null) { for (Player player : list) { player.addEffect(new MobEffectInstance(secondaryEffect, j, 0, true, true)); } } } } public static void playSound(Level level, BlockPos pos, SoundEvent sound) { level.playSound(null, pos, sound, SoundSource.BLOCKS, 1.0F, 1.0F); } public List getBeamSections() { return (List)(this.levels == 0 ? ImmutableList.of() : this.beamSections); } public ClientboundBlockEntityDataPacket getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } @Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return this.saveCustomOnly(registries); } private static void storeEffect(CompoundTag tag, String key, @Nullable Holder effect) { if (effect != null) { effect.unwrapKey().ifPresent(resourceKey -> tag.putString(key, resourceKey.location().toString())); } } @Nullable private static Holder loadEffect(CompoundTag tag, String key) { if (tag.contains(key, 8)) { ResourceLocation resourceLocation = ResourceLocation.tryParse(tag.getString(key)); return resourceLocation == null ? null : (Holder)BuiltInRegistries.MOB_EFFECT.get(resourceLocation).map(BeaconBlockEntity::filterEffect).orElse(null); } else { return null; } } @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.loadAdditional(tag, registries); this.primaryPower = loadEffect(tag, "primary_effect"); this.secondaryPower = loadEffect(tag, "secondary_effect"); if (tag.contains("CustomName", 8)) { this.name = parseCustomNameSafe(tag.getString("CustomName"), registries); } this.lockKey = LockCode.fromTag(tag, registries); } @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.saveAdditional(tag, registries); storeEffect(tag, "primary_effect", this.primaryPower); storeEffect(tag, "secondary_effect", this.secondaryPower); tag.putInt("Levels", this.levels); if (this.name != null) { tag.putString("CustomName", Component.Serializer.toJson(this.name, registries)); } this.lockKey.addToTag(tag, registries); } /** * Sets the custom name for this beacon. */ public void setCustomName(@Nullable Component name) { this.name = name; } @Nullable @Override public Component getCustomName() { return this.name; } @Nullable @Override public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { return BaseContainerBlockEntity.canUnlock(player, this.lockKey, this.getDisplayName()) ? new BeaconMenu(i, inventory, this.dataAccess, ContainerLevelAccess.create(this.level, this.getBlockPos())) : null; } @Override public Component getDisplayName() { return this.getName(); } @Override public Component getName() { return this.name != null ? this.name : DEFAULT_NAME; } @Override protected void applyImplicitComponents(BlockEntity.DataComponentInput componentInput) { super.applyImplicitComponents(componentInput); this.name = componentInput.get(DataComponents.CUSTOM_NAME); this.lockKey = componentInput.getOrDefault(DataComponents.LOCK, LockCode.NO_LOCK); } @Override protected void collectImplicitComponents(Builder components) { super.collectImplicitComponents(components); components.set(DataComponents.CUSTOM_NAME, this.name); if (!this.lockKey.equals(LockCode.NO_LOCK)) { components.set(DataComponents.LOCK, this.lockKey); } } @Override public void removeComponentsFromTag(CompoundTag tag) { tag.remove("CustomName"); tag.remove("lock"); } @Override public void setLevel(Level level) { super.setLevel(level); this.lastCheckY = level.getMinY() - 1; } public static class BeaconBeamSection { /** * The colors of this section of a beacon beam, in RGB float format. */ final int color; private int height; public BeaconBeamSection(int color) { this.color = color; this.height = 1; } protected void increaseHeight() { this.height++; } public int getColor() { return this.color; } public int getHeight() { return this.height; } } }