diff --git a/checkstyle.xml b/checkstyle.xml index e3810c06fa..2a4912b5f5 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -62,6 +62,13 @@ + + + + + + + diff --git a/fabric-api-lookup-api-v1/README.md b/fabric-api-lookup-api-v1/README.md new file mode 100644 index 0000000000..c3ba17722d --- /dev/null +++ b/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. diff --git a/fabric-api-lookup-api-v1/build.gradle b/fabric-api-lookup-api-v1/build.gradle new file mode 100644 index 0000000000..42faa01158 --- /dev/null +++ b/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') +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiCache.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiCache.java new file mode 100644 index 0000000000..d0393990dc --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiCache.java @@ -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. + * + *

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 The type of the API. + * @param The type of the additional context object. + * @see BlockApiLookup + */ +@ApiStatus.NonExtendable +public interface BlockApiCache { + /** + * Attempt to retrieve an API from a block in the world, using the world and the position passed at creation time. + * + *

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 BlockApiCache create(BlockApiLookup 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, Class);` to get instances."); + } + + return new BlockApiCacheImpl<>((BlockApiLookupImpl) lookup, world, pos); + } +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiLookup.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiLookup.java new file mode 100644 index 0000000000..8509bc9a22 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/block/BlockApiLookup.java @@ -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}. + * + *

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. + * + *

Note: If you are going to query APIs a lot, consider using {@link BlockApiCache}, it may drastically improve performance. + * + *

Usage Example

+ * Let us pretend we have the following interface that we would like to attach to some blocks depending on the direction. + * + *
{@code
+ * public interface FluidContainer {
+ *     boolean containsFluids(); // return true if not empty
+ * }}
+ * Let us first create a static {@code BlockApiLookup} instance that will manage the registration and the query. + * + *
{@code
+ * public final class MyApi {
+ *     public static final BlockApiLookup FLUID_CONTAINER = BlockApiLookup.get(new Identifier("mymod:fluid_container"), FluidContainer.class, Direction.class);
+ * }}
+ * Using that, we can query instances of {@code FluidContainer}: + * + *
{@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!");
+ *     }
+ * }}
+ * For the query to return a useful result, functions that provide an API for a block or a block entity must be registered. + * + *
{@code
+ * // If the block entity directly implements the interface, registerSelf can be used.
+ * public class ContainerBlockEntity implements FluidContainer {
+ *     // ...
+ * }
+ * BlockEntityType 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
+ * });}
+ * + *

Improving performance

+ * When performing queries every tick, it is recommended to use {@link BlockApiCache BlockApiCache<A, C>} + * instead of directly querying the {@code BlockApiLookup}. + * + *
{@code
+ * // 1) create and store an instance
+ * BlockApiCache 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}
+ * + *

Generic context types

+ * Note that {@code FluidContainer} and {@code Direction} were completely arbitrary in this example. + * We can define any {@code BlockApiLookup<A, C>}, 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
The type of the API. + * @param The type of the additional context object. + */ +@ApiStatus.NonExtendable +public interface BlockApiLookup { + /** + * 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 BlockApiLookup get(Identifier lookupId, Class apiClass, Class 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. + * + *

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. + * + *

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 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 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 fallbackProvider); + + @FunctionalInterface + interface BlockApiProvider { + /** + * 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 { + /** + * 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); + } +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/custom/ApiLookupMap.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/custom/ApiLookupMap.java new file mode 100644 index 0000000000..d1c4804be1 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/custom/ApiLookupMap.java @@ -0,0 +1,129 @@ +/* + * 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.custom; + +import java.util.Objects; + +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.impl.lookup.custom.ApiLookupMapImpl; + +//CHECKSTYLE.OFF: JavadocStyle - Checkstyle didn't like , even though {@code ... } already escapes it. +/** + * A a map meant to be used as the backing storage for custom {@code ApiLookup} instances, + * to implement a custom equivalent of {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup#get BlockApiLookup#get}. + * + *

Usage Example

+ * We will be implementing the following simplified version of an API lookup interface for item stacks + * to illustrate how to use {@link ApiLookupMap} and {@link ApiProviderMap}. + *
{@code
+ * public interface ItemStackApiLookup {
+ *     static  ItemStackApiLookup get(Identifier lookupId, Class apiClass, Class contextClass) {
+ *         return ItemStackApiLookupImpl.get(lookupId, apiClass, contextClass);
+ *     }
+ *     // Find an API instance.
+ *     @Nullable
+ *     A find(ItemStack stack, C context);
+ *     // Expose the API for some item.
+ *     void register(ItemStackApiProvider provider, Item item);
+ *
+ *     interface ItemStackApiProvider {
+ *         // Return an API instance if available, or null otherwise.
+ *         @Nullable
+ *         A find(ItemStack stack, C context);
+ *     }
+ * }
+ * }
+ * All the implementation can fit in a single class: + *
{@code
+ * public class ItemStackApiLookupImpl implements ItemStackApiLookup {
+ *     // Management of lookup instances is handled by ApiLookupMap.
+ *     private static final ApiLookupMap> LOOKUPS = ApiLookupMap.create(ItemStackApiLookupImpl::new);
+ *     // We have to perform an unchecked cast to convert  back to .
+ *     @SuppressWarnings("unchecked")
+ *     public static  ItemStackApiLookup get(Identifier lookupId, Class apiClass, Class contextClass) {
+ *         // Null checks are already handled by ApiLookupMap#get.
+ *         return (ItemStackApiLookup) LOOKUPS.getLookup(lookupId, apiClass, contextClass);
+ *     }
+ *
+ *     private ItemStackApiLookupImpl(Class apiClass, Class contextClass) {
+ *         // We don't use these classes, so nothing to do here.
+ *     }
+ *     // We will use an ApiProviderMap to store the providers.
+ *     private final ApiProviderMap> providerMap = ApiProviderMap.create();
+ *     @Nullable
+ *     public A find(ItemStack stack, C context) {
+ *         ItemStackApiProvider provider = providerMap.get(stack.getItem());
+ *         if (provider == null) {
+ *             return null;
+ *         } else {
+ *             return provider.find(stack, context);
+ *         }
+ *     }
+ *     public void register(ItemStackApiProvider provider, Item item) {
+ *         // Let's add a few null checks just in case.
+ *         Objects.requireNonNull(provider, "ItemStackApiProvider may not be null.");
+ *         Objects.requireNonNull(item, "Item may not be null.");
+ *         // Register the provider, or warn if it is already registered
+ *         if (providerMap.putIfAbsent(item, provider) != null) {
+ *             // Emit a warning printing the item ID to help users debug more easily.
+ *             LogManager.getLogger("The name of your mod").warn("Encountered duplicate API provider registration for item " + Registry.ITEM.getId(item) + ".");
+ *         }
+ *     }
+ * }
+ * }
+ * + * @param The type of the lookup implementation, similar to the existing {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup BlockApiLookup}. + */ +//CHECKSTYLE.ON: JavadocStyle +@ApiStatus.NonExtendable +public interface ApiLookupMap extends Iterable { + /** + * Create a new instance. + * + * @param lookupFactory The factory that is used to create API lookup instances. + */ + static ApiLookupMap create(LookupFactory lookupFactory) { + Objects.requireNonNull(lookupFactory, "Lookup factory may not be null."); + + return new ApiLookupMapImpl<>(lookupFactory); + } + + /** + * Retrieve the API lookup associated with an identifier. + * + * @param lookupId The unique identifier of the lookup. + * @param apiClass The class of the queried API. + * @param contextClass The class of the queried 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. + * @throws NullPointerException If one of the arguments is null. + */ + L getLookup(Identifier lookupId, Class apiClass, Class contextClass); + + interface LookupFactory { + /** + * Create a new API lookup implementation. + * + * @param apiClass The API class passed to {@link #getLookup}. + * @param contextClass The context class passed to {@link #getLookup}. + */ + L get(Class apiClass, Class contextClass); + } +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/custom/ApiProviderMap.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/custom/ApiProviderMap.java new file mode 100644 index 0000000000..e31a9083e8 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/custom/ApiProviderMap.java @@ -0,0 +1,59 @@ +/* + * 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.custom; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.fabric.impl.lookup.custom.ApiProviderHashMap; + +/** + * A fast thread-safe copy-on-write map meant to be used as the backing storage for registered providers. + * See {@link ApiLookupMap} for a usage example. + * + *

Note: This map allows very fast lock-free concurrent reads, but in exchange writes are very expensive and should not be too frequent. + * Also, keys are compared by reference ({@code ==}) and not using {@link Object#equals}. + * + * @param The key type of the map, compared by reference ({@code ==}). + * @param The value type of the map. + */ +@ApiStatus.NonExtendable +public interface ApiProviderMap { + /** + * Create a new instance. + */ + static ApiProviderMap create() { + return new ApiProviderHashMap<>(); + } + + /** + * Return the provider to which the specified key is mapped, + * or {@code null} if this map contains no mapping for the key. + * + * @throws NullPointerException If the key is null. + */ + @Nullable + V get(K key); + + /** + * If the specified key is not already associated with a provider, + * associate it with the given value and return {@code null}, else return the current value. + * + * @throws NullPointerException If the key or the provider is null. + */ + V putIfAbsent(K key, V provider); +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/package-info.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/package-info.java new file mode 100644 index 0000000000..d548e9a237 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/api/lookup/v1/package-info.java @@ -0,0 +1,58 @@ +/* + * 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. + */ + +/** + *

The API Lookup, version 1.

+ * + *

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.

+ * + *

Definitions and purpose

+ *
    + *
  • 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 {@code A}.
  • + *
  • It also provides building blocks for defining custom ways of retrieving APIs from other game objects.
  • + *
+ *

+ * + *

Retrieving APIs from blocks in the world

+ *
    + *
  • A block query for an API is an operation that takes a world, a block position, and additional context of type {@code C}, and uses that + * to find an object of type {@code A}, or {@code null} if there was no such object.
  • + *
  • An instance of {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup BlockApiLookup<A, C>} + * provides a {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup#find find()} function that does exactly that.
  • + *
  • It also allows registering APIs for blocks, because for the query to work the API must be registered first. + * Registration primarily happens through {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup#registerSelf registerSelf()}, + * {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup#registerForBlocks registerForBlocks()} + * and {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup#registerForBlockEntities registerForBlockEntities()}.
  • + *
  • {@code BlockApiLookup} instances can be accessed through {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup#get BlockApiLookup#get()}. + * For optimal performance, it is better to store them in a {@code public static final} field instead of querying them multiple times.
  • + *
  • See {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup BlockApiLookup} for example code.
  • + *
+ *

+ * + *

Retrieving APIs from custom game objects

+ *
    + *
  • The subpackage {@code custom} provides helper classes to accelerate implementations of {@code ApiLookup}s for custom objects, + * similar to the existing {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup BlockApiLookup}, but with different query parameters.
  • + *
  • {@link net.fabricmc.fabric.api.lookup.v1.custom.ApiLookupMap ApiLookupMap} is a map meant to be used as the backing storage for custom {@code ApiLookup} instances, + * to implement a custom equivalent of {@link net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup#get BlockApiLookup#get}.
  • + *
  • {@link net.fabricmc.fabric.api.lookup.v1.custom.ApiProviderMap ApiProviderMap} is a fast thread-safe copy-on-write map meant to be used as the backing storage for registered providers.
  • + *
  • See {@link net.fabricmc.fabric.api.lookup.v1.custom.ApiLookupMap ApiLookupMap} for example code.
  • + *
+ *

+ */ +package net.fabricmc.fabric.api.lookup.v1; diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiCacheImpl.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiCacheImpl.java new file mode 100644 index 0000000000..75316f4bb7 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiCacheImpl.java @@ -0,0 +1,117 @@ +/* + * 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.impl.lookup.block; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; + +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerBlockEntityEvents; +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache; +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; + +public final class BlockApiCacheImpl implements BlockApiCache { + private final BlockApiLookupImpl lookup; + private final ServerWorld world; + private final BlockPos pos; + /** + * We always cache the block entity, even if it's null. We rely on BE load and unload events to invalidate the cache when necessary. + * blockEntityCacheValid maintains whether the cache is valid or not. + */ + private boolean blockEntityCacheValid = false; + private BlockEntity cachedBlockEntity = null; + /** + * We also cache the BlockApiProvider at the target position. We check if the block state has changed to invalidate the cache. + * lastState maintains for which block state the cachedProvider is valid. + */ + private BlockState lastState = null; + private BlockApiLookup.BlockApiProvider cachedProvider = null; + + public BlockApiCacheImpl(BlockApiLookupImpl lookup, ServerWorld world, BlockPos pos) { + ((ServerWorldCache) world).fabric_registerCache(pos, this); + this.lookup = lookup; + this.world = world; + this.pos = pos.toImmutable(); + } + + public void invalidate() { + blockEntityCacheValid = false; + cachedBlockEntity = null; + lastState = null; + cachedProvider = null; + } + + @Nullable + @Override + public A find(@Nullable BlockState state, C context) { + // Get block entity + if (!blockEntityCacheValid) { + cachedBlockEntity = world.getBlockEntity(pos); + blockEntityCacheValid = true; + } + + // Get block state + if (state == null) { + if (cachedBlockEntity != null) { + state = cachedBlockEntity.getCachedState(); + } else { + state = world.getBlockState(pos); + } + } + + // Get provider + if (lastState != state) { + cachedProvider = lookup.getProvider(state.getBlock()); + lastState = state; + } + + // Query the provider + A instance = null; + + if (cachedProvider != null) { + instance = cachedProvider.find(world, pos, state, cachedBlockEntity, context); + } + + if (instance != null) { + return instance; + } + + // Query the fallback providers + for (BlockApiLookup.BlockApiProvider fallbackProvider : lookup.getFallbackProviders()) { + instance = fallbackProvider.find(world, pos, state, cachedBlockEntity, context); + + if (instance != null) { + return instance; + } + } + + return null; + } + + static { + ServerBlockEntityEvents.BLOCK_ENTITY_LOAD.register((blockEntity, world) -> { + ((ServerWorldCache) world).fabric_invalidateCache(blockEntity.getPos()); + }); + + ServerBlockEntityEvents.BLOCK_ENTITY_UNLOAD.register((blockEntity, world) -> { + ((ServerWorldCache) world).fabric_invalidateCache(blockEntity.getPos()); + }); + } +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiLookupImpl.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiLookupImpl.java new file mode 100644 index 0000000000..e51947cba4 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/BlockApiLookupImpl.java @@ -0,0 +1,181 @@ +/* + * 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.impl.lookup.block; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +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.util.registry.Registry; +import net.minecraft.world.World; + +import net.fabricmc.fabric.api.lookup.v1.custom.ApiLookupMap; +import net.fabricmc.fabric.api.lookup.v1.custom.ApiProviderMap; +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; +import net.fabricmc.fabric.mixin.lookup.BlockEntityTypeAccessor; + +public final class BlockApiLookupImpl implements BlockApiLookup { + private static final Logger LOGGER = LogManager.getLogger("fabric-api-lookup-api-v1/block"); + private static final ApiLookupMap> LOOKUPS = ApiLookupMap.create(BlockApiLookupImpl::new); + + @SuppressWarnings("unchecked") + public static BlockApiLookup get(Identifier lookupId, Class
apiClass, Class contextClass) { + return (BlockApiLookup) LOOKUPS.getLookup(lookupId, apiClass, contextClass); + } + + private final Class apiClass; + private final ApiProviderMap> providerMap = ApiProviderMap.create(); + private final List> fallbackProviders = new CopyOnWriteArrayList<>(); + + @SuppressWarnings("unchecked") + private BlockApiLookupImpl(Class apiClass, Class contextClass) { + this.apiClass = (Class) apiClass; + } + + @Nullable + @Override + public A find(World world, BlockPos pos, @Nullable BlockState state, @Nullable BlockEntity blockEntity, C context) { + Objects.requireNonNull(world, "World may not be null."); + Objects.requireNonNull(pos, "BlockPos may not be null."); + // Providers have the final say whether a null context is allowed. + + // Get the block state and the block entity + if (blockEntity == null) { + if (state == null) { + state = world.getBlockState(pos); + } + + if (state.getBlock().hasBlockEntity()) { + blockEntity = world.getBlockEntity(pos); + } + } else { + if (state == null) { + state = blockEntity.getCachedState(); + } + } + + @Nullable + BlockApiProvider provider = getProvider(state.getBlock()); + A instance = null; + + if (provider != null) { + instance = provider.find(world, pos, state, blockEntity, context); + } + + if (instance != null) { + return instance; + } + + // Query the fallback providers + for (BlockApiProvider fallbackProvider : fallbackProviders) { + instance = fallbackProvider.find(world, pos, state, blockEntity, context); + + if (instance != null) { + return instance; + } + } + + return null; + } + + @SuppressWarnings("unchecked") + @Override + public void registerSelf(BlockEntityType... blockEntityTypes) { + for (BlockEntityType blockEntityType : blockEntityTypes) { + BlockEntity blockEntity = blockEntityType.instantiate(); + Objects.requireNonNull(blockEntity, "Instantiated block entity may not be null."); + + if (!apiClass.isAssignableFrom(blockEntity.getClass())) { + String errorMessage = String.format( + "Failed to register self-implementing block entities. API class %s is not assignable from block entity class %s.", + apiClass.getCanonicalName(), + blockEntity.getClass().getCanonicalName() + ); + throw new IllegalArgumentException(errorMessage); + } + } + + registerForBlockEntities((blockEntity, context) -> (A) blockEntity, blockEntityTypes); + } + + @Override + public void registerForBlocks(BlockApiProvider provider, Block... blocks) { + Objects.requireNonNull(provider, "BlockApiProvider may not be null."); + + if (blocks.length == 0) { + throw new IllegalArgumentException("Must register at least one Block instance with a BlockApiProvider."); + } + + for (Block block : blocks) { + Objects.requireNonNull(block, "Encountered null block while registering a block API provider mapping."); + + if (providerMap.putIfAbsent(block, provider) != null) { + LOGGER.warn("Encountered duplicate API provider registration for block: " + Registry.BLOCK.getId(block)); + } + } + } + + @Override + public void registerForBlockEntities(BlockEntityApiProvider provider, BlockEntityType... blockEntityTypes) { + Objects.requireNonNull(provider, "BlockEntityApiProvider may not be null."); + + if (blockEntityTypes.length == 0) { + throw new IllegalArgumentException("Must register at least one BlockEntityType instance with a BlockEntityApiProvider."); + } + + BlockApiProvider nullCheckedProvider = (world, pos, state, blockEntity, context) -> { + if (blockEntity == null) { + return null; + } else { + return provider.find(blockEntity, context); + } + }; + + for (BlockEntityType blockEntityType : blockEntityTypes) { + Objects.requireNonNull(blockEntityType, "Encountered null block entity type while registering a block entity API provider mapping."); + + Block[] blocks = ((BlockEntityTypeAccessor) blockEntityType).getBlocks().toArray(new Block[0]); + registerForBlocks(nullCheckedProvider, blocks); + } + } + + @Override + public void registerFallback(BlockApiProvider fallbackProvider) { + Objects.requireNonNull(fallbackProvider, "BlockApiProvider may not be null."); + + fallbackProviders.add(fallbackProvider); + } + + @Nullable + public BlockApiProvider getProvider(Block block) { + return providerMap.get(block); + } + + public List> getFallbackProviders() { + return fallbackProviders; + } +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/ServerWorldCache.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/ServerWorldCache.java new file mode 100644 index 0000000000..e26ac2b807 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/block/ServerWorldCache.java @@ -0,0 +1,28 @@ +/* + * 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.impl.lookup.block; + +import net.minecraft.util.math.BlockPos; + +/** + * Allows attachment of a BlockApiCache to a {@link net.minecraft.server.world.ServerWorld}. + */ +public interface ServerWorldCache { + void fabric_registerCache(BlockPos pos, BlockApiCacheImpl cache); + + void fabric_invalidateCache(BlockPos pos); +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/custom/ApiLookupMapImpl.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/custom/ApiLookupMapImpl.java new file mode 100644 index 0000000000..dccc4e9f07 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/custom/ApiLookupMapImpl.java @@ -0,0 +1,77 @@ +/* + * 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.impl.lookup.custom; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.lookup.v1.custom.ApiLookupMap; + +public final class ApiLookupMapImpl implements ApiLookupMap { + private final Map> lookups = new HashMap<>(); + private final LookupFactory lookupFactory; + + public ApiLookupMapImpl(LookupFactory lookupFactory) { + this.lookupFactory = lookupFactory; + } + + @Override + public synchronized L getLookup(Identifier lookupId, Class apiClass, Class contextClass) { + Objects.requireNonNull(lookupId, "Lookup Identifier may not be null."); + Objects.requireNonNull(apiClass, "API class may not be null."); + Objects.requireNonNull(contextClass, "Context class may not be null."); + + StoredLookup storedLookup = lookups.computeIfAbsent(lookupId, id -> new StoredLookup<>(lookupFactory.get(apiClass, contextClass), apiClass, contextClass)); + + if (storedLookup.apiClass == apiClass && storedLookup.contextClass == contextClass) { + return storedLookup.lookup; + } + + String errorMessage = String.format( + "Lookup with id %s is already registered with api class %s and context class %s. It can't be registered with api class %s and context class %s.", + lookupId, + storedLookup.apiClass.getCanonicalName(), + storedLookup.contextClass.getCanonicalName(), + apiClass.getCanonicalName(), + contextClass.getCanonicalName() + ); + + throw new IllegalArgumentException(errorMessage); + } + + @Override + public synchronized Iterator iterator() { + return lookups.values().stream().map(storedLookup -> storedLookup.lookup).collect(Collectors.toList()).iterator(); + } + + private static final class StoredLookup { + final L lookup; + final Class apiClass; + final Class contextClass; + + StoredLookup(L lookup, Class apiClass, Class contextClass) { + this.lookup = lookup; + this.apiClass = apiClass; + this.contextClass = contextClass; + } + } +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/custom/ApiProviderHashMap.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/custom/ApiProviderHashMap.java new file mode 100644 index 0000000000..1aa1be2c14 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/impl/lookup/custom/ApiProviderHashMap.java @@ -0,0 +1,50 @@ +/* + * 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.impl.lookup.custom; + +import java.util.Map; +import java.util.Objects; + +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.fabric.api.lookup.v1.custom.ApiProviderMap; + +public final class ApiProviderHashMap implements ApiProviderMap { + private volatile Map lookups = new Reference2ReferenceOpenHashMap<>(); + + @Nullable + @Override + public V get(K key) { + Objects.requireNonNull(key, "Key may not be null."); + + return lookups.get(key); + } + + @Override + public synchronized V putIfAbsent(K key, V provider) { + Objects.requireNonNull(key, "Key may not be null."); + Objects.requireNonNull(provider, "Provider may not be null."); + + // We use a copy-on-write strategy to allow any number of reads to concur with a write + Map lookupsCopy = new Reference2ReferenceOpenHashMap<>(lookups); + V result = lookupsCopy.putIfAbsent(key, provider); + lookups = lookupsCopy; + + return result; + } +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/mixin/lookup/BlockEntityTypeAccessor.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/mixin/lookup/BlockEntityTypeAccessor.java new file mode 100644 index 0000000000..75d9b93bfe --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/mixin/lookup/BlockEntityTypeAccessor.java @@ -0,0 +1,31 @@ +/* + * 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.mixin.lookup; + +import java.util.Set; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.block.Block; +import net.minecraft.block.entity.BlockEntityType; + +@Mixin(BlockEntityType.class) +public interface BlockEntityTypeAccessor { + @Accessor("blocks") + Set getBlocks(); +} diff --git a/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/mixin/lookup/ServerWorldMixin.java b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/mixin/lookup/ServerWorldMixin.java new file mode 100644 index 0000000000..3b7f57a51a --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/java/net/fabricmc/fabric/mixin/lookup/ServerWorldMixin.java @@ -0,0 +1,84 @@ +/* + * 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.mixin.lookup; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +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.ServerWorldCache; + +@Mixin(ServerWorld.class) +abstract class ServerWorldMixin implements ServerWorldCache { + @Unique + private final Map>>> apiLookupCaches = new Object2ReferenceOpenHashMap<>(); + /** + * Ensures that the apiLookupCaches map is iterated over every once in a while to clean up caches. + */ + @Unique + private int apiLookupAccessesWithoutCleanup = 0; + + @Override + public void fabric_registerCache(BlockPos pos, BlockApiCacheImpl cache) { + List>> caches = apiLookupCaches.computeIfAbsent(pos.toImmutable(), ignored -> new ArrayList<>()); + caches.removeIf(weakReference -> weakReference.get() == null); + caches.add(new WeakReference<>(cache)); + apiLookupAccessesWithoutCleanup++; + } + + @Override + public void fabric_invalidateCache(BlockPos pos) { + List>> caches = apiLookupCaches.get(pos); + + if (caches != null) { + caches.removeIf(weakReference -> weakReference.get() == null); + + if (caches.size() == 0) { + apiLookupCaches.remove(pos); + } else { + caches.forEach(weakReference -> { + BlockApiCacheImpl cache = weakReference.get(); + + if (cache != null) { + cache.invalidate(); + } + }); + } + } + + apiLookupAccessesWithoutCleanup++; + + // Try to invalidate GC'd lookups from the cache after 2 * the number of cached lookups + if (apiLookupAccessesWithoutCleanup > 2 * apiLookupCaches.size()) { + apiLookupCaches.entrySet().removeIf(entry -> { + entry.getValue().removeIf(weakReference -> weakReference.get() == null); + return entry.getValue().isEmpty(); + }); + + apiLookupAccessesWithoutCleanup = 0; + } + } +} diff --git a/fabric-api-lookup-api-v1/src/main/resources/assets/fabric-api-lookup-api-v1/icon.png b/fabric-api-lookup-api-v1/src/main/resources/assets/fabric-api-lookup-api-v1/icon.png new file mode 100644 index 0000000000..2931efbf61 Binary files /dev/null and b/fabric-api-lookup-api-v1/src/main/resources/assets/fabric-api-lookup-api-v1/icon.png differ diff --git a/fabric-api-lookup-api-v1/src/main/resources/fabric-api-lookup-api-v1.mixins.json b/fabric-api-lookup-api-v1/src/main/resources/fabric-api-lookup-api-v1.mixins.json new file mode 100644 index 0000000000..919d6d3d0d --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/resources/fabric-api-lookup-api-v1.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.mixin.lookup", + "compatibilityLevel": "JAVA_8", + "mixins": [ + "BlockEntityTypeAccessor", + "ServerWorldMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/fabric-api-lookup-api-v1/src/main/resources/fabric.mod.json b/fabric-api-lookup-api-v1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000000..a5e37db323 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/main/resources/fabric.mod.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "id": "fabric-api-lookup-api-v1", + "name": "Fabric API Lookup API (v1)", + "version": "${version}", + "environment": "*", + "license": "Apache-2.0", + "icon": "assets/fabric-api-lookup-api-v1/icon.png", + "contact": { + "homepage": "https://fabricmc.net", + "irc": "irc://irc.esper.net:6667/fabric", + "issues": "https://github.com/FabricMC/fabric/issues", + "sources": "https://github.com/FabricMC/fabric" + }, + "authors": [ + "FabricMC" + ], + "depends": { + "fabricloader": ">=0.9.2", + "fabric-api-base": "*", + "fabric-lifecycle-events-v1": "*" + }, + "description": "A universal way to expose and query APIs", + "mixins": [ + "fabric-api-lookup-api-v1.mixins.json" + ], + "custom": { + "fabric-api:module-lifecycle": "stable" + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/ChuteBlock.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/ChuteBlock.java new file mode 100644 index 0000000000..0841e1e2a2 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/ChuteBlock.java @@ -0,0 +1,35 @@ +/* + * 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.test.lookup; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockEntityProvider; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.world.BlockView; + +public class ChuteBlock extends Block implements BlockEntityProvider { + public ChuteBlock(Settings settings) { + super(settings); + } + + @Override + public @Nullable BlockEntity createBlockEntity(BlockView world) { + return new ChuteBlockEntity(); + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/ChuteBlockEntity.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/ChuteBlockEntity.java new file mode 100644 index 0000000000..81b52c95a5 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/ChuteBlockEntity.java @@ -0,0 +1,69 @@ +/* + * 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.test.lookup; + +import org.jetbrains.annotations.NotNull; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Tickable; +import net.minecraft.util.math.Direction; + +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache; +import net.fabricmc.fabric.test.lookup.api.ItemApis; +import net.fabricmc.fabric.test.lookup.api.ItemExtractable; +import net.fabricmc.fabric.test.lookup.api.ItemInsertable; +import net.fabricmc.fabric.test.lookup.api.ItemUtils; + +public class ChuteBlockEntity extends BlockEntity implements Tickable { + private int moveDelay = 0; + private BlockApiCache cachedInsertable = null; + private BlockApiCache cachedExtractable = null; + + public ChuteBlockEntity() { + super(FabricApiLookupTest.CHUTE_BLOCK_ENTITY_TYPE); + } + + @Override + public void tick() { + //noinspection ConstantConditions - Intellij intrinsics don't know that hasWorld makes getWorld evaluate to non-null + if (!this.hasWorld() || this.getWorld().isClient()) { + return; + } + + if (cachedInsertable == null) { + cachedInsertable = BlockApiCache.create(ItemApis.INSERTABLE, (ServerWorld) world, pos.offset(Direction.DOWN)); + } + + if (cachedExtractable == null) { + cachedExtractable = BlockApiCache.create(ItemApis.EXTRACTABLE, (ServerWorld) world, pos.offset(Direction.UP)); + } + + if (moveDelay == 0) { + ItemExtractable from = cachedExtractable.find(Direction.DOWN); + ItemInsertable to = cachedInsertable.find(Direction.UP); + + if (from != null && to != null) { + ItemUtils.move(from, to, 1); + } + + moveDelay = 20; + } + + --moveDelay; + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/CobbleGenBlock.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/CobbleGenBlock.java new file mode 100644 index 0000000000..9fd40df333 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/CobbleGenBlock.java @@ -0,0 +1,35 @@ +/* + * 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.test.lookup; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockEntityProvider; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.world.BlockView; + +public class CobbleGenBlock extends Block implements BlockEntityProvider { + public CobbleGenBlock(Settings settings) { + super(settings); + } + + @Override + public @Nullable BlockEntity createBlockEntity(BlockView world) { + return new CobbleGenBlockEntity(); + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/CobbleGenBlockEntity.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/CobbleGenBlockEntity.java new file mode 100644 index 0000000000..f676184ab3 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/CobbleGenBlockEntity.java @@ -0,0 +1,42 @@ +/* + * 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.test.lookup; + +import java.util.function.Predicate; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +import net.fabricmc.fabric.test.lookup.api.ItemExtractable; + +public class CobbleGenBlockEntity extends BlockEntity implements ItemExtractable { + public CobbleGenBlockEntity() { + super(FabricApiLookupTest.COBBLE_GEN_BLOCK_ENTITY_TYPE); + } + + @Override + public ItemStack tryExtract(int maxCount, Predicate filter, boolean simulate) { + ItemStack cobble = new ItemStack(Items.COBBLESTONE); + + if (filter.test(cobble)) { + return cobble; + } else { + return ItemStack.EMPTY; + } + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/FabricApiLookupTest.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/FabricApiLookupTest.java new file mode 100644 index 0000000000..e92e62a36b --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/FabricApiLookupTest.java @@ -0,0 +1,106 @@ +/* + * 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.test.lookup; + +import org.jetbrains.annotations.NotNull; + +import net.minecraft.block.Material; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.item.BlockItem; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Direction; +import net.minecraft.util.registry.Registry; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; +import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings; +import net.fabricmc.fabric.test.lookup.api.ItemApis; +import net.fabricmc.fabric.test.lookup.api.ItemInsertable; +import net.fabricmc.fabric.test.lookup.compat.InventoryExtractableProvider; +import net.fabricmc.fabric.test.lookup.compat.InventoryInsertableProvider; + +public class FabricApiLookupTest implements ModInitializer { + public static final String MOD_ID = "fabric-lookup-api-v1-testmod"; + // Chute - Block without model that transfers item from the container above to the container below. + // It's meant to work with unsided containers: chests, dispensers, droppers and hoppers. + public static final ChuteBlock CHUTE_BLOCK = new ChuteBlock(FabricBlockSettings.of(Material.METAL)); + public static final BlockItem CHUTE_ITEM = new BlockItem(CHUTE_BLOCK, new Item.Settings().group(ItemGroup.MISC)); + public static BlockEntityType CHUTE_BLOCK_ENTITY_TYPE; + // Cobble gen - Block without model that can generate infinite cobblestone when placed above a chute. + // It's meant to test BlockApiLookup#registerSelf. + public static final CobbleGenBlock COBBLE_GEN_BLOCK = new CobbleGenBlock(FabricBlockSettings.of(Material.METAL)); + public static final BlockItem COBBLE_GEN_ITEM = new BlockItem(COBBLE_GEN_BLOCK, new Item.Settings().group(ItemGroup.MISC)); + public static BlockEntityType COBBLE_GEN_BLOCK_ENTITY_TYPE; + + @Override + public void onInitialize() { + Identifier chute = new Identifier(MOD_ID, "chute"); + Registry.register(Registry.BLOCK, chute, CHUTE_BLOCK); + Registry.register(Registry.ITEM, chute, CHUTE_ITEM); + CHUTE_BLOCK_ENTITY_TYPE = Registry.register(Registry.BLOCK_ENTITY_TYPE, chute, BlockEntityType.Builder.create(ChuteBlockEntity::new, CHUTE_BLOCK).build(null)); + + Identifier cobbleGen = new Identifier(MOD_ID, "cobble_gen"); + Registry.register(Registry.BLOCK, cobbleGen, COBBLE_GEN_BLOCK); + Registry.register(Registry.ITEM, cobbleGen, COBBLE_GEN_ITEM); + COBBLE_GEN_BLOCK_ENTITY_TYPE = Registry.register(Registry.BLOCK_ENTITY_TYPE, cobbleGen, BlockEntityType.Builder.create(CobbleGenBlockEntity::new, COBBLE_GEN_BLOCK).build(null)); + + InventoryExtractableProvider extractableProvider = new InventoryExtractableProvider(); + InventoryInsertableProvider insertableProvider = new InventoryInsertableProvider(); + + ItemApis.INSERTABLE.registerForBlockEntities(insertableProvider, BlockEntityType.CHEST, BlockEntityType.DISPENSER, BlockEntityType.DROPPER, BlockEntityType.HOPPER); + ItemApis.EXTRACTABLE.registerForBlockEntities(extractableProvider, BlockEntityType.CHEST, BlockEntityType.DISPENSER, BlockEntityType.DROPPER, BlockEntityType.HOPPER); + ItemApis.EXTRACTABLE.registerSelf(COBBLE_GEN_BLOCK_ENTITY_TYPE); + + testLookupRegistry(); + testSelfRegistration(); + } + + private static void testLookupRegistry() { + BlockApiLookup insertable2 = BlockApiLookup.get(new Identifier("testmod:item_insertable"), ItemInsertable.class, Direction.class); + + if (insertable2 != ItemApis.INSERTABLE) { + throw new AssertionError("The registry should have returned the same instance."); + } + + ensureException(() -> { + BlockApiLookup wrongInsertable = BlockApiLookup.get(new Identifier("testmod:item_insertable"), Void.class, Void.class); + wrongInsertable.registerFallback((world, pos, state, be, nocontext) -> null); + }, "The registry should have prevented creation of another instance with different classes, but same id."); + } + + private static void testSelfRegistration() { + ensureException(() -> { + ItemApis.INSERTABLE.registerSelf(COBBLE_GEN_BLOCK_ENTITY_TYPE); + }, "The BlockApiLookup should have prevented self-registration of incompatible block entity types."); + } + + private static void ensureException(Runnable runnable, String message) { + boolean failed = false; + + try { + runnable.run(); + } catch (Throwable t) { + failed = true; + } + + if (!failed) { + throw new AssertionError(message); + } + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemApis.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemApis.java new file mode 100644 index 0000000000..cb1ab002da --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemApis.java @@ -0,0 +1,34 @@ +/* + * 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.test.lookup.api; + +import org.jetbrains.annotations.NotNull; + +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Direction; + +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; + +public final class ItemApis { + public static final BlockApiLookup INSERTABLE = + BlockApiLookup.get(new Identifier("testmod:item_insertable"), ItemInsertable.class, Direction.class); + public static final BlockApiLookup EXTRACTABLE = + BlockApiLookup.get(new Identifier("testmod:item_extractable"), ItemExtractable.class, Direction.class); + + private ItemApis() { + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemExtractable.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemExtractable.java new file mode 100644 index 0000000000..f215fe78c3 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemExtractable.java @@ -0,0 +1,35 @@ +/* + * 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.test.lookup.api; + +import java.util.function.Predicate; + +import net.minecraft.item.ItemStack; + +/** + * Something that can provide items. + */ +public interface ItemExtractable { + /** + * Try to extract a single stack. + * @param maxCount The maximum number of items to extract + * @param filter What items to extract. Please note that the predicate should be independent of the count of the stack! + * @param simulate If true, don't modify any state + * @return The extracted stack + */ + ItemStack tryExtract(int maxCount, Predicate filter, boolean simulate); +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemInsertable.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemInsertable.java new file mode 100644 index 0000000000..1ae262237a --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemInsertable.java @@ -0,0 +1,33 @@ +/* + * 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.test.lookup.api; + +import net.minecraft.item.ItemStack; + +/** + * Something that can accept items. + */ +public interface ItemInsertable { + /** + * Try to insert some items. If this object can accept a stack of n items, it should also accept the same stack with + * a smaller count! + * @param input The input items. Must not be changed by this function! + * @param simulate If true, don't modify any state + * @return The leftover items; it should never be the received input stack! + */ + ItemStack tryInsert(ItemStack input, boolean simulate); +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemUtils.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemUtils.java new file mode 100644 index 0000000000..1682634e4f --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/api/ItemUtils.java @@ -0,0 +1,44 @@ +/* + * 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.test.lookup.api; + +import java.util.function.Predicate; + +import net.minecraft.item.ItemStack; + +public final class ItemUtils { + /** + * Move at most maxCount items, and return the number of items moved. + */ + public static int move(ItemExtractable from, ItemInsertable to, int maxCount) { + Predicate insertionFilter = stack -> { + if (stack.isEmpty()) return false; + + ItemStack insertedStack = to.tryInsert(stack, true); + return insertedStack.isEmpty() || insertedStack.getCount() < stack.getCount(); + }; + + ItemStack extracted = from.tryExtract(maxCount, insertionFilter, true); + ItemStack leftover = to.tryInsert(extracted, false); + int moved = extracted.getCount() - leftover.getCount(); + from.tryExtract(moved, insertionFilter, false); + return moved; + } + + private ItemUtils() { + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/InventoryExtractableProvider.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/InventoryExtractableProvider.java new file mode 100644 index 0000000000..365532f57f --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/InventoryExtractableProvider.java @@ -0,0 +1,38 @@ +/* + * 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.test.lookup.compat; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.util.math.Direction; + +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; +import net.fabricmc.fabric.test.lookup.api.ItemExtractable; + +public class InventoryExtractableProvider implements BlockApiLookup.BlockEntityApiProvider { + @Override + public @Nullable ItemExtractable find(BlockEntity blockEntity, @NotNull Direction context) { + if (blockEntity instanceof Inventory) { + return new WrappedInventory((Inventory) blockEntity); + } + + return null; + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/InventoryInsertableProvider.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/InventoryInsertableProvider.java new file mode 100644 index 0000000000..b60ce8e06e --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/InventoryInsertableProvider.java @@ -0,0 +1,38 @@ +/* + * 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.test.lookup.compat; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.util.math.Direction; + +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; +import net.fabricmc.fabric.test.lookup.api.ItemInsertable; + +public class InventoryInsertableProvider implements BlockApiLookup.BlockEntityApiProvider { + @Override + public @Nullable ItemInsertable find(BlockEntity blockEntity, @NotNull Direction context) { + if (blockEntity instanceof Inventory) { + return new WrappedInventory((Inventory) blockEntity); + } + + return null; + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/ItemStackUtil.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/ItemStackUtil.java new file mode 100644 index 0000000000..426ce3e401 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/ItemStackUtil.java @@ -0,0 +1,28 @@ +/* + * 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.test.lookup.compat; + +import net.minecraft.item.ItemStack; + +final class ItemStackUtil { + public static boolean areEqualIgnoreCount(ItemStack s1, ItemStack s2) { + return s1.getItem() == s2.getItem() && ItemStack.areTagsEqual(s1, s2); + } + + private ItemStackUtil() { + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/WrappedInventory.java b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/WrappedInventory.java new file mode 100644 index 0000000000..13447dca90 --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/java/net/fabricmc/fabric/test/lookup/compat/WrappedInventory.java @@ -0,0 +1,83 @@ +/* + * 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.test.lookup.compat; + +import java.util.function.Predicate; + +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemStack; + +import net.fabricmc.fabric.test.lookup.api.ItemExtractable; +import net.fabricmc.fabric.test.lookup.api.ItemInsertable; + +final class WrappedInventory implements ItemInsertable, ItemExtractable { + private final Inventory inv; + + WrappedInventory(Inventory inv) { + this.inv = inv; + } + + @Override + public ItemStack tryExtract(int maxCount, Predicate filter, boolean simulate) { + for (int i = 0; i < inv.size(); ++i) { + ItemStack stack = inv.getStack(i); + + if (!stack.isEmpty() && filter.test(stack)) { + ItemStack returned; + + if (simulate) { + returned = stack.copy().split(maxCount); + } else { + returned = stack.split(maxCount); + } + + return returned; + } + } + + return ItemStack.EMPTY; + } + + @Override + public ItemStack tryInsert(ItemStack input, boolean simulate) { + input = input.copy(); + + for (int i = 0; i < inv.size(); ++i) { + if (inv.isValid(i, input)) { + ItemStack stack = inv.getStack(i); + + if (stack.isEmpty() || ItemStackUtil.areEqualIgnoreCount(stack, input)) { + int remainingSpace = Math.min(inv.getMaxCountPerStack(), stack.getItem().getMaxCount()) - stack.getCount(); + int inserted = Math.min(remainingSpace, input.getCount()); + + if (!simulate) { + if (stack.isEmpty()) { + inv.setStack(i, input.copy()); + inv.getStack(i).setCooldown(inserted); + } else { + stack.increment(inserted); + } + } + + input.decrement(inserted); + } + } + } + + return input; + } +} diff --git a/fabric-api-lookup-api-v1/src/testmod/resources/fabric.mod.json b/fabric-api-lookup-api-v1/src/testmod/resources/fabric.mod.json new file mode 100644 index 0000000000..5a961e1e2e --- /dev/null +++ b/fabric-api-lookup-api-v1/src/testmod/resources/fabric.mod.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "id": "fabric-api-lookup-api-v1-testmod", + "name": "Fabric API Lookup API (v1) Test Mod", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "depends": { + "fabric-api-lookup-api-v1": "*" + }, + "entrypoints": { + "main": [ + "net.fabricmc.fabric.test.lookup.FabricApiLookupTest" + ] + } +} diff --git a/settings.gradle b/settings.gradle index df79d28693..216493ef7d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,6 +14,7 @@ rootProject.name = "fabric-api" include 'fabric-api-base' +include 'fabric-api-lookup-api-v1' include 'fabric-biome-api-v1' include 'fabric-blockrenderlayer-v1' include 'fabric-commands-v0'