239 lines
8.4 KiB
Java
239 lines
8.4 KiB
Java
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.Provider;
|
|
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.chat.Component.Serializer;
|
|
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<String, CompletableFuture<Optional<GameProfile>>> profileCacheByName;
|
|
@Nullable
|
|
private static LoadingCache<UUID, CompletableFuture<Optional<GameProfile>>> 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<String, CompletableFuture<Optional<GameProfile>>>() {
|
|
public CompletableFuture<Optional<GameProfile>> load(String username) {
|
|
return SkullBlockEntity.fetchProfileByName(username, services);
|
|
}
|
|
});
|
|
profileCacheById = CacheBuilder.newBuilder()
|
|
.expireAfterAccess(Duration.ofMinutes(10L))
|
|
.maximumSize(256L)
|
|
.build(new CacheLoader<UUID, CompletableFuture<Optional<GameProfile>>>() {
|
|
public CompletableFuture<Optional<GameProfile>> load(UUID id) {
|
|
return SkullBlockEntity.fetchProfileById(id, services, booleanSupplier);
|
|
}
|
|
});
|
|
}
|
|
|
|
static CompletableFuture<Optional<GameProfile>> fetchProfileByName(String name, Services services) {
|
|
return services.profileCache()
|
|
.getAsync(name)
|
|
.thenCompose(
|
|
optional -> {
|
|
LoadingCache<UUID, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheById;
|
|
return loadingCache != null && !optional.isEmpty()
|
|
? loadingCache.getUnchecked(((GameProfile)optional.get()).getId()).thenApply(optional2 -> optional2.or(() -> optional))
|
|
: CompletableFuture.completedFuture(Optional.empty());
|
|
}
|
|
);
|
|
}
|
|
|
|
static CompletableFuture<Optional<GameProfile>> 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, 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", Serializer.toJson(this.customName, registries));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void loadAdditional(CompoundTag tag, 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(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<Optional<GameProfile>> fetchGameProfile(String profileName) {
|
|
LoadingCache<String, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheByName;
|
|
return loadingCache != null && StringUtil.isValidPlayerName(profileName)
|
|
? loadingCache.getUnchecked(profileName)
|
|
: CompletableFuture.completedFuture(Optional.empty());
|
|
}
|
|
|
|
public static CompletableFuture<Optional<GameProfile>> fetchGameProfile(UUID profileUuid) {
|
|
LoadingCache<UUID, CompletableFuture<Optional<GameProfile>>> 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");
|
|
}
|
|
}
|