minecraft-src/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java
2025-07-04 03:45:38 +03:00

333 lines
14 KiB
Java

package net.minecraft.world.level.block.entity.vault;
import com.google.common.annotations.VisibleForTesting;
import com.mojang.serialization.DynamicOps;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import net.minecraft.Util;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.stats.Stats;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.VaultBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraft.world.level.storage.loot.LootParams.Builder;
import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets;
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
public class VaultBlockEntity extends BlockEntity {
private final VaultServerData serverData = new VaultServerData();
private final VaultSharedData sharedData = new VaultSharedData();
private final VaultClientData clientData = new VaultClientData();
private VaultConfig config = VaultConfig.DEFAULT;
public VaultBlockEntity(BlockPos pos, BlockState state) {
super(BlockEntityType.VAULT, pos, state);
}
@Nullable
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
return Util.make(
new CompoundTag(),
compoundTag -> compoundTag.store("shared_data", VaultSharedData.CODEC, registries.createSerializationContext(NbtOps.INSTANCE), this.sharedData)
);
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
RegistryOps<Tag> registryOps = registries.createSerializationContext(NbtOps.INSTANCE);
tag.store("config", VaultConfig.CODEC, registryOps, this.config);
tag.store("shared_data", VaultSharedData.CODEC, registryOps, this.sharedData);
tag.store("server_data", VaultServerData.CODEC, registryOps, this.serverData);
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
DynamicOps<Tag> dynamicOps = registries.createSerializationContext(NbtOps.INSTANCE);
tag.read("server_data", VaultServerData.CODEC, dynamicOps).ifPresent(this.serverData::set);
this.config = (VaultConfig)tag.read("config", VaultConfig.CODEC, dynamicOps).orElse(VaultConfig.DEFAULT);
tag.read("shared_data", VaultSharedData.CODEC, dynamicOps).ifPresent(this.sharedData::set);
}
@Nullable
public VaultServerData getServerData() {
return this.level != null && !this.level.isClientSide ? this.serverData : null;
}
public VaultSharedData getSharedData() {
return this.sharedData;
}
public VaultClientData getClientData() {
return this.clientData;
}
public VaultConfig getConfig() {
return this.config;
}
@VisibleForTesting
public void setConfig(VaultConfig config) {
this.config = config;
}
public static final class Client {
private static final int PARTICLE_TICK_RATE = 20;
private static final float IDLE_PARTICLE_CHANCE = 0.5F;
private static final float AMBIENT_SOUND_CHANCE = 0.02F;
private static final int ACTIVATION_PARTICLE_COUNT = 20;
private static final int DEACTIVATION_PARTICLE_COUNT = 20;
public static void tick(Level level, BlockPos pos, BlockState state, VaultClientData clientData, VaultSharedData sharedData) {
clientData.updateDisplayItemSpin();
if (level.getGameTime() % 20L == 0L) {
emitConnectionParticlesForNearbyPlayers(level, pos, state, sharedData);
}
emitIdleParticles(level, pos, sharedData, state.getValue(VaultBlock.OMINOUS) ? ParticleTypes.SOUL_FIRE_FLAME : ParticleTypes.SMALL_FLAME);
playIdleSounds(level, pos, sharedData);
}
public static void emitActivationParticles(Level level, BlockPos pos, BlockState state, VaultSharedData sharedData, ParticleOptions particle) {
emitConnectionParticlesForNearbyPlayers(level, pos, state, sharedData);
RandomSource randomSource = level.random;
for (int i = 0; i < 20; i++) {
Vec3 vec3 = randomPosInsideCage(pos, randomSource);
level.addParticle(ParticleTypes.SMOKE, vec3.x(), vec3.y(), vec3.z(), 0.0, 0.0, 0.0);
level.addParticle(particle, vec3.x(), vec3.y(), vec3.z(), 0.0, 0.0, 0.0);
}
}
public static void emitDeactivationParticles(Level level, BlockPos pos, ParticleOptions particle) {
RandomSource randomSource = level.random;
for (int i = 0; i < 20; i++) {
Vec3 vec3 = randomPosCenterOfCage(pos, randomSource);
Vec3 vec32 = new Vec3(randomSource.nextGaussian() * 0.02, randomSource.nextGaussian() * 0.02, randomSource.nextGaussian() * 0.02);
level.addParticle(particle, vec3.x(), vec3.y(), vec3.z(), vec32.x(), vec32.y(), vec32.z());
}
}
private static void emitIdleParticles(Level level, BlockPos pos, VaultSharedData sharedData, ParticleOptions particle) {
RandomSource randomSource = level.getRandom();
if (randomSource.nextFloat() <= 0.5F) {
Vec3 vec3 = randomPosInsideCage(pos, randomSource);
level.addParticle(ParticleTypes.SMOKE, vec3.x(), vec3.y(), vec3.z(), 0.0, 0.0, 0.0);
if (shouldDisplayActiveEffects(sharedData)) {
level.addParticle(particle, vec3.x(), vec3.y(), vec3.z(), 0.0, 0.0, 0.0);
}
}
}
private static void emitConnectionParticlesForPlayer(Level level, Vec3 pos, Player player) {
RandomSource randomSource = level.random;
Vec3 vec3 = pos.vectorTo(player.position().add(0.0, player.getBbHeight() / 2.0F, 0.0));
int i = Mth.nextInt(randomSource, 2, 5);
for (int j = 0; j < i; j++) {
Vec3 vec32 = vec3.offsetRandom(randomSource, 1.0F);
level.addParticle(ParticleTypes.VAULT_CONNECTION, pos.x(), pos.y(), pos.z(), vec32.x(), vec32.y(), vec32.z());
}
}
private static void emitConnectionParticlesForNearbyPlayers(Level level, BlockPos pos, BlockState state, VaultSharedData sharedData) {
Set<UUID> set = sharedData.getConnectedPlayers();
if (!set.isEmpty()) {
Vec3 vec3 = keyholePos(pos, state.getValue(VaultBlock.FACING));
for (UUID uUID : set) {
Player player = level.getPlayerByUUID(uUID);
if (player != null && isWithinConnectionRange(pos, sharedData, player)) {
emitConnectionParticlesForPlayer(level, vec3, player);
}
}
}
}
private static boolean isWithinConnectionRange(BlockPos pos, VaultSharedData sharedData, Player player) {
return player.blockPosition().distSqr(pos) <= Mth.square(sharedData.connectedParticlesRange());
}
private static void playIdleSounds(Level level, BlockPos pos, VaultSharedData sharedData) {
if (shouldDisplayActiveEffects(sharedData)) {
RandomSource randomSource = level.getRandom();
if (randomSource.nextFloat() <= 0.02F) {
level.playLocalSound(pos, SoundEvents.VAULT_AMBIENT, SoundSource.BLOCKS, randomSource.nextFloat() * 0.25F + 0.75F, randomSource.nextFloat() + 0.5F, false);
}
}
}
public static boolean shouldDisplayActiveEffects(VaultSharedData sharedData) {
return sharedData.hasDisplayItem();
}
private static Vec3 randomPosCenterOfCage(BlockPos pos, RandomSource random) {
return Vec3.atLowerCornerOf(pos).add(Mth.nextDouble(random, 0.4, 0.6), Mth.nextDouble(random, 0.4, 0.6), Mth.nextDouble(random, 0.4, 0.6));
}
private static Vec3 randomPosInsideCage(BlockPos pos, RandomSource random) {
return Vec3.atLowerCornerOf(pos).add(Mth.nextDouble(random, 0.1, 0.9), Mth.nextDouble(random, 0.25, 0.75), Mth.nextDouble(random, 0.1, 0.9));
}
private static Vec3 keyholePos(BlockPos pos, Direction facing) {
return Vec3.atBottomCenterOf(pos).add(facing.getStepX() * 0.5, 1.75, facing.getStepZ() * 0.5);
}
}
public static final class Server {
private static final int UNLOCKING_DELAY_TICKS = 14;
private static final int DISPLAY_CYCLE_TICK_RATE = 20;
private static final int INSERT_FAIL_SOUND_BUFFER_TICKS = 15;
public static void tick(ServerLevel level, BlockPos pos, BlockState state, VaultConfig config, VaultServerData serverData, VaultSharedData sharedData) {
VaultState vaultState = state.getValue(VaultBlock.STATE);
if (shouldCycleDisplayItem(level.getGameTime(), vaultState)) {
cycleDisplayItemFromLootTable(level, vaultState, config, sharedData, pos);
}
BlockState blockState = state;
if (level.getGameTime() >= serverData.stateUpdatingResumesAt()) {
blockState = state.setValue(VaultBlock.STATE, vaultState.tickAndGetNext(level, pos, config, serverData, sharedData));
if (state != blockState) {
setVaultState(level, pos, state, blockState, config, sharedData);
}
}
if (serverData.isDirty || sharedData.isDirty) {
VaultBlockEntity.setChanged(level, pos, state);
if (sharedData.isDirty) {
level.sendBlockUpdated(pos, state, blockState, 2);
}
serverData.isDirty = false;
sharedData.isDirty = false;
}
}
public static void tryInsertKey(
ServerLevel level,
BlockPos pos,
BlockState state,
VaultConfig config,
VaultServerData serverData,
VaultSharedData sharedData,
Player player,
ItemStack stack
) {
VaultState vaultState = state.getValue(VaultBlock.STATE);
if (canEjectReward(config, vaultState)) {
if (!isValidToInsert(config, stack)) {
playInsertFailSound(level, serverData, pos, SoundEvents.VAULT_INSERT_ITEM_FAIL);
} else if (serverData.hasRewardedPlayer(player)) {
playInsertFailSound(level, serverData, pos, SoundEvents.VAULT_REJECT_REWARDED_PLAYER);
} else {
List<ItemStack> list = resolveItemsToEject(level, config, pos, player, stack);
if (!list.isEmpty()) {
player.awardStat(Stats.ITEM_USED.get(stack.getItem()));
stack.consume(config.keyItem().getCount(), player);
unlock(level, state, pos, config, serverData, sharedData, list);
serverData.addToRewardedPlayers(player);
sharedData.updateConnectedPlayersWithinRange(level, pos, serverData, config, config.deactivationRange());
}
}
}
}
static void setVaultState(ServerLevel level, BlockPos pos, BlockState oldState, BlockState newState, VaultConfig config, VaultSharedData sharedData) {
VaultState vaultState = oldState.getValue(VaultBlock.STATE);
VaultState vaultState2 = newState.getValue(VaultBlock.STATE);
level.setBlock(pos, newState, 3);
vaultState.onTransition(level, pos, vaultState2, config, sharedData, (Boolean)newState.getValue(VaultBlock.OMINOUS));
}
static void cycleDisplayItemFromLootTable(ServerLevel level, VaultState state, VaultConfig config, VaultSharedData sharedData, BlockPos pos) {
if (!canEjectReward(config, state)) {
sharedData.setDisplayItem(ItemStack.EMPTY);
} else {
ItemStack itemStack = getRandomDisplayItemFromLootTable(level, pos, (ResourceKey<LootTable>)config.overrideLootTableToDisplay().orElse(config.lootTable()));
sharedData.setDisplayItem(itemStack);
}
}
private static ItemStack getRandomDisplayItemFromLootTable(ServerLevel level, BlockPos pos, ResourceKey<LootTable> lootTable) {
LootTable lootTable2 = level.getServer().reloadableRegistries().getLootTable(lootTable);
LootParams lootParams = new Builder(level).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)).create(LootContextParamSets.VAULT);
List<ItemStack> list = lootTable2.getRandomItems(lootParams, level.getRandom());
return list.isEmpty() ? ItemStack.EMPTY : Util.getRandom(list, level.getRandom());
}
private static void unlock(
ServerLevel level, BlockState state, BlockPos pos, VaultConfig config, VaultServerData serverData, VaultSharedData sharedData, List<ItemStack> itemsToEject
) {
serverData.setItemsToEject(itemsToEject);
sharedData.setDisplayItem(serverData.getNextItemToEject());
serverData.pauseStateUpdatingUntil(level.getGameTime() + 14L);
setVaultState(level, pos, state, state.setValue(VaultBlock.STATE, VaultState.UNLOCKING), config, sharedData);
}
private static List<ItemStack> resolveItemsToEject(ServerLevel level, VaultConfig config, BlockPos pos, Player player, ItemStack key) {
LootTable lootTable = level.getServer().reloadableRegistries().getLootTable(config.lootTable());
LootParams lootParams = new Builder(level)
.withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos))
.withLuck(player.getLuck())
.withParameter(LootContextParams.THIS_ENTITY, player)
.withParameter(LootContextParams.TOOL, key)
.create(LootContextParamSets.VAULT);
return lootTable.getRandomItems(lootParams);
}
private static boolean canEjectReward(VaultConfig config, VaultState state) {
return !config.keyItem().isEmpty() && state != VaultState.INACTIVE;
}
private static boolean isValidToInsert(VaultConfig config, ItemStack stack) {
return ItemStack.isSameItemSameComponents(stack, config.keyItem()) && stack.getCount() >= config.keyItem().getCount();
}
private static boolean shouldCycleDisplayItem(long gameTime, VaultState state) {
return gameTime % 20L == 0L && state == VaultState.ACTIVE;
}
private static void playInsertFailSound(ServerLevel level, VaultServerData serverData, BlockPos pos, SoundEvent sound) {
if (level.getGameTime() >= serverData.getLastInsertFailTimestamp() + 15L) {
level.playSound(null, pos, sound, SoundSource.BLOCKS);
serverData.setLastInsertFailTimestamp(level.getGameTime());
}
}
}
}