RyanHub – file viewer
filename: common/src/main/java/rearth/oritech/util/ApiLookupCache.java
branch: 1.21
back to repo
package rearth.oritech.util;

import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;

import java.lang.ref.WeakReference;

/**
 * ApiLookupCache is used to cache results from a lookup function (for example,
 * fetching a FluidStorage) based on a given block state and entity. The cache
 * is refreshed only if the block state changes or if the underlying block entity
 * is removed.
 *
 * @param <T> The type of the target object (e.g., FluidStorage)
 */
public class ApiLookupCache<T> {
    
    @FunctionalInterface
    public interface LookupFunction<T> {
        T invoke(World world, BlockPos targetPos, BlockState state, @Nullable BlockEntity entity, @Nullable Direction direction);
    }
    
    private WeakReference<BlockEntity> cachedEntity;
    private WeakReference<T> cachedTarget;
    
    // Cache the last known block state to avoid unnecessary refreshes.
    private BlockState lastBlockState;
    
    private final BlockPos targetPos;
    private final Direction direction;
    private final World world;
    
    private final LookupFunction<T> lookupFunction;
    
    private ApiLookupCache(BlockEntity cachedEntity, T cachedTarget, BlockPos targetPos, Direction direction, World world, LookupFunction<T> lookupFunction) {
        this.cachedEntity = new WeakReference<>(cachedEntity);
        this.cachedTarget = new WeakReference<>(cachedTarget);
        this.targetPos = targetPos;
        this.direction = direction;
        this.world = world;
        this.lookupFunction = lookupFunction;
        // Initialize the lastBlockState using the current state at creation.
        this.lastBlockState = world.getBlockState(targetPos);
    }
    
    public static <T> ApiLookupCache<T> create(BlockPos targetPos, Direction direction, World world, LookupFunction<T> lookupFunction) {
        var state = world.getBlockState(targetPos);
        var entity = world.getBlockEntity(targetPos);
        T target = lookupFunction.invoke(world, targetPos, state, entity, direction);
        
        return new ApiLookupCache<>(entity, target, targetPos, direction, world, lookupFunction);
    }
    
    /**
     * Returns the target value. The lookup is recomputed only if needed
     * -- either because the block state has changed or the cache is otherwise invalid.
     * This is intended to be called every tick.
     *
     * @return The cached or freshly looked up target.
     */
    public T lookup() {
        
        // If cache is valid, return the cached target.
        if (isCacheValid()) {
            return cachedTarget.get();
        }
        
        cachedTarget = new WeakReference<>(null);
        cachedEntity = new WeakReference<>(null);
        
        refresh();
        return cachedTarget.get();
    }
    
    /**
     * Refreshes the cache by issuing a new lookup only if the entity exists.
     * The lastBlockState is updated to reflect the current state.
     * If the new state is identical to the old state, it is canceled early.
     *
     */
    private void refresh() {
        var currentState = world.getBlockState(targetPos);
        
        // if no blockstate change has occurred, the lookup would fail anyway
        if (currentState.equals(lastBlockState)) {
            return;
        }
        
        var entity = world.getBlockEntity(targetPos);
        T target = null;
        
        if (entity != null) {
            target = lookupFunction.invoke(world, targetPos, currentState, entity, direction);
        }
        
        cachedTarget = new WeakReference<>(target);
        cachedEntity = new WeakReference<>(entity);
        lastBlockState = currentState;
    }
    
    /**
     * Checks if the cache is still valid:
     * - The cached BlockEntity still exists and is not removed.
     * - There is a valid target.
     *
     * @return true if the cache can be used; false otherwise.
     */
    private boolean isCacheValid() {
        var entity = cachedEntity.get();
        var target = cachedTarget.get();
        
        // Check if block entity is valid.
        if (entity == null || entity.isRemoved()) {
            return false;
        }
        
        // Check that our target object is still valid.
        return target != null;
    }
}