package net.minecraft.world.level.block.entity; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.mojang.authlib.GameProfile; import com.mojang.authlib.yggdrasil.ProfileResult; import com.mojang.logging.LogUtils; import java.time.Duration; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.BooleanSupplier; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponentMap.Builder; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.NbtOps; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.Services; import net.minecraft.util.StringUtil; import net.minecraft.world.item.component.ResolvableProfile; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.SkullBlock; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; public class SkullBlockEntity extends BlockEntity { private static final String TAG_PROFILE = "profile"; private static final String TAG_NOTE_BLOCK_SOUND = "note_block_sound"; private static final String TAG_CUSTOM_NAME = "custom_name"; private static final Logger LOGGER = LogUtils.getLogger(); @Nullable private static Executor mainThreadExecutor; @Nullable private static LoadingCache>> profileCacheByName; @Nullable private static LoadingCache>> profileCacheById; public static final Executor CHECKED_MAIN_THREAD_EXECUTOR = runnable -> { Executor executor = mainThreadExecutor; if (executor != null) { executor.execute(runnable); } }; @Nullable private ResolvableProfile owner; @Nullable private ResourceLocation noteBlockSound; private int animationTickCount; private boolean isAnimating; @Nullable private Component customName; public SkullBlockEntity(BlockPos pos, BlockState blockState) { super(BlockEntityType.SKULL, pos, blockState); } public static void setup(Services services, Executor mainThreadExecutor) { SkullBlockEntity.mainThreadExecutor = mainThreadExecutor; final BooleanSupplier booleanSupplier = () -> profileCacheById == null; profileCacheByName = CacheBuilder.newBuilder() .expireAfterAccess(Duration.ofMinutes(10L)) .maximumSize(256L) .build(new CacheLoader>>() { public CompletableFuture> load(String username) { return SkullBlockEntity.fetchProfileByName(username, services); } }); profileCacheById = CacheBuilder.newBuilder() .expireAfterAccess(Duration.ofMinutes(10L)) .maximumSize(256L) .build(new CacheLoader>>() { public CompletableFuture> load(UUID id) { return SkullBlockEntity.fetchProfileById(id, services, booleanSupplier); } }); } static CompletableFuture> fetchProfileByName(String name, Services services) { return services.profileCache() .getAsync(name) .thenCompose( optional -> { LoadingCache>> loadingCache = profileCacheById; return loadingCache != null && !optional.isEmpty() ? loadingCache.getUnchecked(((GameProfile)optional.get()).getId()).thenApply(optional2 -> optional2.or(() -> optional)) : CompletableFuture.completedFuture(Optional.empty()); } ); } static CompletableFuture> fetchProfileById(UUID id, Services services, BooleanSupplier cacheUninitialized) { return CompletableFuture.supplyAsync(() -> { if (cacheUninitialized.getAsBoolean()) { return Optional.empty(); } else { ProfileResult profileResult = services.sessionService().fetchProfile(id, true); return Optional.ofNullable(profileResult).map(ProfileResult::profile); } }, Util.backgroundExecutor().forName("fetchProfile")); } public static void clear() { mainThreadExecutor = null; profileCacheByName = null; profileCacheById = null; } @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.saveAdditional(tag, registries); if (this.owner != null) { tag.put("profile", ResolvableProfile.CODEC.encodeStart(NbtOps.INSTANCE, this.owner).getOrThrow()); } if (this.noteBlockSound != null) { tag.putString("note_block_sound", this.noteBlockSound.toString()); } if (this.customName != null) { tag.putString("custom_name", Component.Serializer.toJson(this.customName, registries)); } } @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.loadAdditional(tag, registries); if (tag.contains("profile")) { ResolvableProfile.CODEC .parse(NbtOps.INSTANCE, tag.get("profile")) .resultOrPartial(string -> LOGGER.error("Failed to load profile from player head: {}", string)) .ifPresent(this::setOwner); } if (tag.contains("note_block_sound", 8)) { this.noteBlockSound = ResourceLocation.tryParse(tag.getString("note_block_sound")); } if (tag.contains("custom_name", 8)) { this.customName = parseCustomNameSafe(tag.getString("custom_name"), registries); } else { this.customName = null; } } public static void animation(Level level, BlockPos pos, BlockState state, SkullBlockEntity blockEntity) { if (state.hasProperty(SkullBlock.POWERED) && (Boolean)state.getValue(SkullBlock.POWERED)) { blockEntity.isAnimating = true; blockEntity.animationTickCount++; } else { blockEntity.isAnimating = false; } } public float getAnimation(float partialTick) { return this.isAnimating ? this.animationTickCount + partialTick : this.animationTickCount; } @Nullable public ResolvableProfile getOwnerProfile() { return this.owner; } @Nullable public ResourceLocation getNoteBlockSound() { return this.noteBlockSound; } public ClientboundBlockEntityDataPacket getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } @Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return this.saveCustomOnly(registries); } public void setOwner(@Nullable ResolvableProfile owner) { synchronized (this) { this.owner = owner; } this.updateOwnerProfile(); } private void updateOwnerProfile() { if (this.owner != null && !this.owner.isResolved()) { this.owner.resolve().thenAcceptAsync(resolvableProfile -> { this.owner = resolvableProfile; this.setChanged(); }, CHECKED_MAIN_THREAD_EXECUTOR); } else { this.setChanged(); } } public static CompletableFuture> fetchGameProfile(String profileName) { LoadingCache>> loadingCache = profileCacheByName; return loadingCache != null && StringUtil.isValidPlayerName(profileName) ? loadingCache.getUnchecked(profileName) : CompletableFuture.completedFuture(Optional.empty()); } public static CompletableFuture> fetchGameProfile(UUID profileUuid) { LoadingCache>> loadingCache = profileCacheById; return loadingCache != null ? loadingCache.getUnchecked(profileUuid) : CompletableFuture.completedFuture(Optional.empty()); } @Override protected void applyImplicitComponents(BlockEntity.DataComponentInput componentInput) { super.applyImplicitComponents(componentInput); this.setOwner(componentInput.get(DataComponents.PROFILE)); this.noteBlockSound = componentInput.get(DataComponents.NOTE_BLOCK_SOUND); this.customName = componentInput.get(DataComponents.CUSTOM_NAME); } @Override protected void collectImplicitComponents(Builder components) { super.collectImplicitComponents(components); components.set(DataComponents.PROFILE, this.owner); components.set(DataComponents.NOTE_BLOCK_SOUND, this.noteBlockSound); components.set(DataComponents.CUSTOM_NAME, this.customName); } @Override public void removeComponentsFromTag(CompoundTag tag) { super.removeComponentsFromTag(tag); tag.remove("profile"); tag.remove("note_block_sound"); tag.remove("custom_name"); } }