Skip to content

Commit

Permalink
Fabric API Lookup (#1234)
Browse files Browse the repository at this point in the history
# Fabric API Lookup API v1
## Introduction
This module allows Api instances to be associated with game objects without specifying how the association is implemented. This is useful when the same Api could be implemented more than once or implemented in different ways.

Many thanks to @grondag for providing the original concept (#1072).
Thanks also go to @i509VCB, @Pyrofab, @sfPlayer1 and the others who were involved with the design of this module.

This is the foundation upon which can be built for example a fluid transfer api (#1166). Closes #1199.

## Flexible Api Querying
## Block Api Usage example
## Building blocks
This PR was changed a lot, please have a look at the README, the package info, and the javadoc for `BlockApiLookup` and `ApiLookupMap` for up-to-date documentation.

## More usage examples
FastTransferLib (https://github.com/Technici4n/FastTransferLib) is an experiment to build an item, fluid and energy transfer api on top of this module. (Which was until recently called `fabric-provider-api-v1`.)

## Missing things?
~~I could add an overload of `BlockApiLookup#find` with nullable `BlockState` and `BlockEntity` parameters, so that the caller can directly provide them if they are available for some reason.~~ Added in later commits.

There is no module to retrieve apis from items or entities yet because there were unsolved issues with those. The community can use the provided building blocks to experiment with their own implementations of `ItemStackApiLookup` and `EntityApiLookup` until the way forward becomes clear, but let's please not delay the `BlockApiLookup` because of that.

Co-authored-by: i509VCB <git@i509.me>
Co-authored-by: PepperBell <44146161+PepperCode1@users.noreply.github.com>
(cherry picked from commit dc716ea)
  • Loading branch information
Technici4n authored and modmuss50 committed Mar 8, 2021
1 parent 0d6fdde commit f995ab4
Show file tree
Hide file tree
Showing 33 changed files with 1,869 additions and 0 deletions.
7 changes: 7 additions & 0 deletions checkstyle.xml
Expand Up @@ -62,6 +62,13 @@
</module>

<module name="TreeWalker">
<!-- Allow "//CHECKSTYLE.OFF: <InspectionName>" and "//CHECKSTYLE.ON: <InspectionName>" pairs to toggle some inspections -->
<module name="SuppressionCommentFilter">
<property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)"/>
<property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)"/>
<property name="checkFormat" value="$1"/>
</module>

<!-- Ensure all imports are ship shape -->
<module name="AvoidStarImport"/>
<module name="IllegalImport"/>
Expand Down
30 changes: 30 additions & 0 deletions fabric-api-lookup-api-v1/README.md
@@ -0,0 +1,30 @@
# Fabric API Lookup API (v1)
This module allows API instances to be associated with game objects without specifying how the association is implemented.
This is useful when the same API could be implemented more than once or implemented in different ways.
See also the [package-info.java file](src/main/java/net/fabricmc/fabric/api/lookup/v1/package-info.java).

* What we call an API is any object that can be offered or queried, possibly by different mods, to be used in an agreed-upon manner.
* This module allows flexible retrieving of such APIs from blocks in the world, represented by the generic type `A`.
* It also provides building blocks for defining custom ways of retrieving APIs from other game objects.

# Retrieving APIs from blocks
See the javadoc of `BlockApiLookup` for a full usage example.

## [`BlockApiLookup`](src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiLookup.java)
The primary way of querying API instances for blocks in the world.
It exposes a `find` function to retrieve an API instance, and multiple `register*` functions to register Apis for blocks and block entities.

Instances can be obtained using the `get` function.

## [`BlockApiCache`](src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiCache.java)
A `BlockApiLookup` bound to a position and a server world, allowing much faster repeated API queries.

# Retrieving APIs from custom objects
The subpackage `custom` provides helper classes to accelerate implementations of `ApiLookup`s for custom objects,
similar to the existing `BlockApiLookup`, but with different query parameters.

## [`ApiLookupMap`](src/main/java/net/fabricmc/fabric/api/lookup/v1/custom/ApiLookupMap.java)
A map meant to be used as the backing storage for custom `ApiLookup` instances, to implement a custom equivalent of `BlockApiLookup#get`.

## [`ApiProviderMap`](src/main/java/net/fabricmc/fabric/api/lookup/v1/custom/ApiProviderMap.java)
A fast thread-safe copy-on-write map meant to be used as the backing storage for registered providers.
11 changes: 11 additions & 0 deletions fabric-api-lookup-api-v1/build.gradle
@@ -0,0 +1,11 @@
archivesBaseName = "fabric-api-lookup-api-v1"
version = getSubprojectVersion(project, "1.0.0")

moduleDependencies(project, [
'fabric-api-base',
'fabric-lifecycle-events-v1'
])

dependencies {
testmodCompile project(path: ':fabric-object-builder-api-v1', configuration: 'dev')
}
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.api.lookup.v1.block;

import java.util.Objects;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

import net.minecraft.block.BlockState;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;

import net.fabricmc.fabric.impl.lookup.block.BlockApiCacheImpl;
import net.fabricmc.fabric.impl.lookup.block.BlockApiLookupImpl;

/**
* A {@link BlockApiLookup} bound to a {@link ServerWorld} and a position, providing much faster API access.
* Refer to {@link BlockApiLookup} for example code.
*
* <p>This object caches the block entity at the target position, and the last used API provider, removing those queries.
* If a block entity is available or if the block state is passed as a parameter, the block state doesn't have to be looked up either.
*
* @param <A> The type of the API.
* @param <C> The type of the additional context object.
* @see BlockApiLookup
*/
@ApiStatus.NonExtendable
public interface BlockApiCache<A, C> {
/**
* Attempt to retrieve an API from a block in the world, using the world and the position passed at creation time.
*
* <p>Note: If the block state is known, it is more efficient to use {@link BlockApiCache#find(BlockState, Object)}.
*
* @param context Additional context for the query, defined by type parameter C.
* @return The retrieved API, or {@code null} if no API was found.
*/
@Nullable
default A find(C context) {
return find(null, context);
}

/**
* Attempt to retrieve an API from a block in the world, using the world and the position passed at creation time.
*
* @param state The block state at the target position, or null if unknown.
* @param context Additional context for the query, defined by type parameter C.
* @return The retrieved API, or {@code null} if no API was found.
*/
@Nullable
A find(@Nullable BlockState state, C context);

/**
* Create a new instance bound to the passed {@link ServerWorld} and position, and querying the same API as the passed lookup.
*/
static <A, C> BlockApiCache<A, C> create(BlockApiLookup<A, C> lookup, ServerWorld world, BlockPos pos) {
Objects.requireNonNull(pos, "BlockPos may not be null.");
Objects.requireNonNull(world, "ServerWorld may not be null.");

if (!(lookup instanceof BlockApiLookupImpl)) {
throw new IllegalArgumentException("Cannot cache foreign implementation of BlockApiLookup. Use `BlockApiLookup#get(Identifier, Class<A>, Class<C>);` to get instances.");
}

return new BlockApiCacheImpl<>((BlockApiLookupImpl<A, C>) lookup, world, pos);
}
}
@@ -0,0 +1,248 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.fabricmc.fabric.api.lookup.v1.block;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;

import net.fabricmc.fabric.impl.lookup.block.BlockApiLookupImpl;

/**
* An object that allows retrieving APIs from blocks in a world.
* Instances of this interface can be obtained through {@link #get}.
*
* <p>When trying to {@link BlockApiLookup#find} an API, the block or block entity at that position will be queried if it exists.
* If it doesn't exist, or if it returns {@code null}, the fallback providers will be queried in order.
*
* <p>Note: If you are going to query APIs a lot, consider using {@link BlockApiCache}, it may drastically improve performance.
*
* <p><h3>Usage Example</h3>
* Let us pretend we have the following interface that we would like to attach to some blocks depending on the direction.
*
* <pre>{@code
* public interface FluidContainer {
* boolean containsFluids(); // return true if not empty
* }}</pre>
* Let us first create a static {@code BlockApiLookup} instance that will manage the registration and the query.
*
* <pre>{@code
* public final class MyApi {
* public static final BlockApiLookup<FluidContainer, Direction> FLUID_CONTAINER = BlockApiLookup.get(new Identifier("mymod:fluid_container"), FluidContainer.class, Direction.class);
* }}</pre>
* Using that, we can query instances of {@code FluidContainer}:
*
* <pre>{@code
* FluidContainer container = MyApi.FLUID_CONTAINER.find(world, pos, direction);
* if (container != null) {
* // Do something with the container
* if (container.containsFluids()) {
* System.out.println("It contains fluids!");
* }
* }}</pre>
* For the query to return a useful result, functions that provide an API for a block or a block entity must be registered.
*
* <pre>{@code
* // If the block entity directly implements the interface, registerSelf can be used.
* public class ContainerBlockEntity implements FluidContainer {
* // ...
* }
* BlockEntityType<ContainerBlockEntity> CONTAINER_BLOCK_ENTITY_TYPE;
* MyApi.FLUID_CONTAINER.registerSelf(CONTAINER_BLOCK_ENTITY_TYPE);
*
* // For more complicated block entity logic, registerForBlockEntities can be used.
* // For example, let's provide a stored field, and only when the direction is UP:
* public class MyBlockEntity {
* public final FluidContainer upContainer;
* // ...
* }
* MyApi.FLUID_CONTAINER.registerForBlockEntities((blockEntity, direction) -> {
* if (direction == Direction.UP) { // only expose from the top
* // return a field
* return ((MyBlockEntity) blockEntity).upContainer;
* } else {
* return null;
* }
* }, BLOCK_ENTITY_TYPE_1, BLOCK_ENTITY_TYPE_2);
*
* // Without a block entity, registerForBlocks can be used.
* MyApi.FLUID_CONTAINER.registerForBlocks((world, pos, state, blockEntity, direction) -> {
* // return a FluidContainer for your block, or null if there is none
* }, BLOCK_INSTANCE, ANOTHER_BLOCK_INSTANCE); // register as many blocks as you want
*
* // Block entity fallback, for example to interface with another mod's FluidInventory.
* MyApi.FLUID_CONTAINER.registerFallback((world, pos, state, blockEntity, direction) -> {
* if (blockEntity instanceof FluidInventory) {
* // return wrapper
* }
* return null;
* });
*
* // General fallback, to interface with anything, for example another BlockApiLookup.
* MyApi.FLUID_CONTAINER.registerFallback((world, pos, state, blockEntity, direction) -> {
* // return something if available, or null
* });}</pre>
*
* <p><h3>Improving performance</h3>
* When performing queries every tick, it is recommended to use {@link BlockApiCache BlockApiCache&lt;A, C&gt;}
* instead of directly querying the {@code BlockApiLookup}.
*
* <pre>{@code
* // 1) create and store an instance
* BlockApiCache<FluidContainer, Direction> cache = BlockApiCache.create(MyApi.FLUID_CONTAINER, serverWorld, pos);
*
* // 2) use it later, the block entity instance will be cached among other things
* FluidContainer container = cache.find(direction);
* if (container != null) {
* // ...
* }
*
* // 2bis) if the caller is able to cache the block state as well, for example by listening to neighbor updates,
* // that will further improve performance.
* FluidContainer container = cache.find(direction, cachedBlockState);
* if (container != null) {
* // ...
* }
*
* // no need to destroy the cache, the garbage collector will take care of it}</pre>
*
* <p><h3>Generic context types</h3>
* Note that {@code FluidContainer} and {@code Direction} were completely arbitrary in this example.
* We can define any {@code BlockApiLookup&lt;A, C&gt;}, where {@code A} is the type of the queried API, and {@code C} is the type of the additional context
* (the direction parameter in the previous example).
* If no context is necessary, {@code Void} should be used, and {@code null} instances should be passed.
*
* @param <A> The type of the API.
* @param <C> The type of the additional context object.
*/
@ApiStatus.NonExtendable
public interface BlockApiLookup<A, C> {
/**
* Retrieve the {@link BlockApiLookup} associated with an identifier, or create it if it didn't exist yet.
*
* @param lookupId The unique identifier of the lookup.
* @param apiClass The class of the API.
* @param contextClass The class of the additional context.
* @return The unique lookup with the passed lookupId.
* @throws IllegalArgumentException If another {@code apiClass} or another {@code contextClass} was already registered with the same identifier.
*/
static <A, C> BlockApiLookup<A, C> get(Identifier lookupId, Class<A> apiClass, Class<C> contextClass) {
return BlockApiLookupImpl.get(lookupId, apiClass, contextClass);
}

/**
* Attempt to retrieve an API from a block in the world.
* Consider using {@link BlockApiCache} if you are doing frequent queries at the same position.
*
* <p>Note: If the block state or the block entity is known, it is more efficient to use {@link BlockApiLookup#find(World, BlockPos, BlockState, BlockEntity, Object)}.
*
* @param world The world.
* @param pos The position of the block.
* @param context Additional context for the query, defined by type parameter C.
* @return The retrieved API, or {@code null} if no API was found.
*/
@Nullable
default A find(World world, BlockPos pos, C context) {
return find(world, pos, null, null, context);
}

/**
* Attempt to retrieve an API from a block in the world.
* Consider using {@link BlockApiCache} if you are doing frequent queries at the same position.
*
* @param world The world.
* @param pos The position of the block.
* @param context Additional context for the query, defined by type parameter C.
* @param state The block state at the target position, or null if unknown.
* @param blockEntity The block entity at the target position if it is known, or null if it is unknown or does not exist.
* @return The retrieved API, or {@code null} if no API was found.
*/
@Nullable
A find(World world, BlockPos pos, @Nullable BlockState state, @Nullable BlockEntity blockEntity, C context);

/**
* Expose the API for the passed block entities directly implementing it.
*
* <p>Implementation note: this is checked at registration time by creating block entity instances using the passed types.
*
* @param blockEntityTypes Block entity types for which to expose the API.
* @throws IllegalArgumentException If the API class is not assignable from instances of the passed block entity types.
*/
void registerSelf(BlockEntityType<?>... blockEntityTypes);

/**
* Expose the API for the passed blocks.
* The mapping from the parameters of the query to the API is handled by the passed {@link BlockApiProvider}.
*
* @param provider The provider.
* @param blocks The blocks.
*/
void registerForBlocks(BlockApiProvider<A, C> provider, Block... blocks);

/**
* Expose the API for instances of the passed block entity types.
* The mapping from the parameters of the query to the API is handled by the passed {@link BlockEntityApiProvider}.
*
* @param provider The provider.
* @param blockEntityTypes The block entity types.
*/
void registerForBlockEntities(BlockEntityApiProvider<A, C> provider, BlockEntityType<?>... blockEntityTypes);

/**
* Expose the API for all queries: the provider will be invoked if no object was found using the block or block entity providers.
* This may have a big performance impact on all queries, use cautiously.
*
* @param fallbackProvider The fallback provider.
*/
void registerFallback(BlockApiProvider<A, C> fallbackProvider);

@FunctionalInterface
interface BlockApiProvider<A, C> {
/**
* Return an API of type {@code A} if available in the world at the given pos with the given context, or {@code null} otherwise.
*
* @param world The world.
* @param pos The position in the world.
* @param state The block state.
* @param blockEntity The block entity, if it exists in the world.
* @param context Additional context passed to the query.
* @return An API of type {@code A}, or {@code null} if no API is available.
*/
@Nullable
A find(World world, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, C context);
}

@FunctionalInterface
interface BlockEntityApiProvider<A, C> {
/**
* Return an API of type {@code A} if available in the given block entity with the given context, or {@code null} otherwise.
*
* @param blockEntity The block entity. It is guaranteed that it is never null.
* @param context Additional context passed to the query.
* @return An API of type {@code A}, or {@code null} if no API is available.
*/
@Nullable
A find(BlockEntity blockEntity, C context);
}
}

0 comments on commit f995ab4

Please sign in to comment.