Add a block appearance API (#2579)

* Add block appearance API

* Add class javadoc for FabricBlock and FabricBlockState

* Address reviews

* Remove OverrideOnly from getAppearance

* Fix javadoc issues
This commit is contained in:
Technici4n 2022-11-07 19:29:51 +01:00 committed by GitHub
parent a1d87cb885
commit 12bfe4ea1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 531 additions and 2 deletions

View file

@ -0,0 +1,2 @@
archivesBaseName = "fabric-block-api-v1"
version = getSubprojectVersion(project)

View file

@ -0,0 +1,104 @@
/*
* 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.block.v1;
import org.jetbrains.annotations.Nullable;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.BlockRenderView;
import net.minecraft.world.World;
/**
* General-purpose Fabric-provided extensions for {@link Block} subclasses.
*
* <p>Note: This interface is automatically implemented on all blocks via Mixin and interface injection.
*/
// Note to maintainers: Functions should only be added to this interface if they are general-purpose enough,
// to be evaluated on a case-by-case basis. Otherwise, they are better suited for more specialized APIs.
public interface FabricBlock {
/**
* Return the current appearance of the block, i.e. which block state this block reports to look like on a given side.
*
* <p>Common implementors are covers and facades, or any other mimic blocks that proxy another block's model.
* These will want to override this method. In that case, make sure to carefully read the implementation guidelines below.
*
* <p>Common consumers are models with connected textures that wish to seamlessly connect to mimic blocks.
* These will want to check the apparent block state using {@link FabricBlockState#getAppearance}.
*
* <p>Generally, the appearance will be queried from a nearby block,
* identified by the optional {@code sourcePos} and {@code sourceState} parameters.
*
* <p>When a block changes appearance, it should trigger a chunk remesh for itself and the adjacent blocks,
* for example by calling {@link World#updateListeners}.
*
* <p>Note: Overriding this method for a block does <strong>not</strong> change how it renders.
* It's up to modded models to check for the appearance of nearby blocks and adjust accordingly.
*
* <h3>Implementation guidelines</h3>
*
* <p>This can be called on the server, where block entity data can be safely accessed,
* and on the client, possibly in a meshing thread, where block entity data is not safe to access!
* Here is an example of how data from a block entity can be handled safely.
* The block entity needs to implement {@code RenderAttachmentBlockEntity} for this to work.
* <pre>{@code @Override
* public BlockState getAppearance(BlockState state, BlockRenderView renderView, BlockPos pos, Direction side, @Nullable BlockState sourceState, @Nullable BlockPos sourcePos) {
* if (renderView instanceof ServerWorld serverWorld) {
* // Server side, ok to use block entity directly!
* BlockEntity blockEntity = serverWorld.getBlockEntity(pos);
*
* if (blockEntity instanceof ...) {
* // Get data from block entity
* return ...;
* }
* } else {
* // Client side, need to use the render attachment!
* RenderAttachedBlockView attachmentView = (RenderAttachedBlockView) renderView;
* Object data = attachmentView.getBlockEntityRenderAttachment(pos);
*
* // Check if data is not null and of the correct type, and use that to determine the appearance
* if (data instanceof ...) {
* // get appearance for side ...
* return ...;
* }
* }
*
* // Example of varying the appearance based on the source pos
* if (sourcePos != null) {
* // get appearance for side ...
* return ...;
* }
*
* // If there is no other appearance, just return the original block state
* return state;
* });
* }</pre>
*
* @param state state of this block, whose appearance is being queried
* @param renderView the world this block is in
* @param pos position of this block, whose appearance is being queried
* @param side the side for which the appearance is being queried
* @param sourceState (optional) state of the block that is querying the appearance, or null if unknown
* @param sourcePos (optional) position of the block that is querying the appearance, or null if unknown
* @return the appearance of the block on the given side; the original {@code state} can be returned if there is no better option
*/
default BlockState getAppearance(BlockState state, BlockRenderView renderView, BlockPos pos, Direction side, @Nullable BlockState sourceState, @Nullable BlockPos sourcePos) {
return state;
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.block.v1;
import org.jetbrains.annotations.Nullable;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.BlockRenderView;
/**
* General-purpose Fabric-provided extensions for {@link BlockState}, matching the functionality provided in {@link FabricBlock}.
*
* <p>Note: This interface is automatically implemented on all block states via Mixin and interface injection.
*/
public interface FabricBlockState {
/**
* Return the current appearance of the block, i.e. which block state this block reports to look like on a given side.
*
* @param renderView the world this block is in
* @param pos position of this block, whose appearance is being queried
* @param side the side for which the appearance is being queried
* @param sourceState (optional) state of the block that is querying the appearance, or null if unknown
* @param sourcePos (optional) position of the block that is querying the appearance, or null if unknown
* @return the appearance of the block on the given side; the original {@code state} can be returned if there is no better option
* @see FabricBlock#getAppearance
*/
default BlockState getAppearance(BlockRenderView renderView, BlockPos pos, Direction side, @Nullable BlockState sourceState, @Nullable BlockPos sourcePos) {
BlockState self = (BlockState) this;
return self.getBlock().getAppearance(self, renderView, pos, side, sourceState, sourcePos);
}
}

View file

@ -0,0 +1,26 @@
/*
* 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.block;
import org.spongepowered.asm.mixin.Mixin;
import net.minecraft.block.Block;
import net.fabricmc.fabric.api.block.v1.FabricBlock;
@Mixin(Block.class)
public class BlockMixin implements FabricBlock { }

View file

@ -0,0 +1,26 @@
/*
* 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.block;
import org.spongepowered.asm.mixin.Mixin;
import net.minecraft.block.BlockState;
import net.fabricmc.fabric.api.block.v1.FabricBlockState;
@Mixin(BlockState.class)
public class BlockStateMixin implements FabricBlockState { }

View file

@ -0,0 +1,12 @@
{
"required": true,
"package": "net.fabricmc.fabric.mixin.block",
"compatibilityLevel": "JAVA_17",
"mixins": [
"BlockMixin",
"BlockStateMixin"
],
"injectors": {
"defaultRequire": 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,34 @@
{
"schemaVersion": 1,
"id": "fabric-block-api-v1",
"name": "Fabric Block API (v1)",
"version": "${version}",
"environment": "*",
"license": "Apache-2.0",
"icon": "assets/fabric-block-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.14.9"
},
"entrypoints": {
},
"description": "Hooks for blocks",
"mixins": [
"fabric-block-api-v1.mixins.json"
],
"custom": {
"fabric-api:module-lifecycle": "stable",
"loom:injected_interfaces": {
"net/minecraft/class_2248": ["net/fabricmc/fabric/api/block/v1/FabricBlock"],
"net/minecraft/class_2680": ["net/fabricmc/fabric/api/block/v1/FabricBlockState"]
}
}
}

View file

@ -6,6 +6,7 @@ moduleDependencies(project, [
])
testDependencies(project, [
':fabric-block-api-v1',
':fabric-blockrenderlayer-v1',
':fabric-models-v0',
':fabric-object-builder-api-v1',

View file

@ -30,11 +30,16 @@ import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.BlockRenderView;
import net.minecraft.world.World;
import net.fabricmc.fabric.api.block.v1.FabricBlock;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import net.fabricmc.fabric.api.rendering.data.v1.RenderAttachedBlockView;
public final class FrameBlock extends Block implements BlockEntityProvider {
// Need to implement FabricBlock manually because this is a testmod for another Fabric module, otherwise it would be injected.
public final class FrameBlock extends Block implements BlockEntityProvider, FabricBlock {
public final Identifier id;
public FrameBlock(Identifier id) {
@ -94,4 +99,16 @@ public final class FrameBlock extends Block implements BlockEntityProvider {
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new FrameBlockEntity(pos, state);
}
// The frames don't look exactly like the block they are mimicking,
// but the goal here is just to test the behavior with the pillar's connected textures. ;-)
@Override
public BlockState getAppearance(BlockState state, BlockRenderView renderView, BlockPos pos, Direction side, @Nullable BlockState sourceState, @Nullable BlockPos sourcePos) {
// For this specific block, the render attachment works on both the client and the server, so let's use that.
if (((RenderAttachedBlockView) renderView).getBlockEntityRenderAttachment(pos) instanceof Block mimickedBlock) {
return mimickedBlock.getDefaultState();
}
return state;
}
}

View file

@ -16,6 +16,8 @@
package net.fabricmc.fabric.test.renderer.simple;
import net.minecraft.block.Block;
import net.minecraft.block.Material;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.item.BlockItem;
import net.minecraft.item.Item;
@ -24,6 +26,7 @@ import net.minecraft.util.Identifier;
import net.minecraft.util.registry.Registry;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
/**
@ -41,6 +44,9 @@ public final class RendererTest implements ModInitializer {
};
public static final BlockEntityType<FrameBlockEntity> FRAME_BLOCK_ENTITY = FabricBlockEntityTypeBuilder.create(FrameBlockEntity::new, FRAMES).build(null);
public static final Identifier PILLAR_ID = id("pillar");
public static final Block PILLAR = new Block(FabricBlockSettings.of(Material.STONE));
@Override
public void onInitialize() {
for (FrameBlock frameBlock : FRAMES) {
@ -48,6 +54,12 @@ public final class RendererTest implements ModInitializer {
Registry.register(Registry.ITEM, frameBlock.id, new BlockItem(frameBlock, new Item.Settings().group(ItemGroup.MISC)));
}
// To anyone testing this: pillars are supposed to connect vertically with each other.
// Additionally, they should also connect vertically to frame blocks containing a pillar.
// (The frame block will not change, but adjacent pillars should adjust their textures).
Registry.register(Registry.BLOCK, PILLAR_ID, PILLAR);
Registry.register(Registry.ITEM, PILLAR_ID, new BlockItem(PILLAR, new Item.Settings().group(ItemGroup.MISC)));
Registry.register(Registry.BLOCK_ENTITY_TYPE, id("frame"), FRAME_BLOCK_ENTITY);
}

View file

@ -0,0 +1,142 @@
/*
* 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.renderer.simple.client;
import java.util.List;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import net.minecraft.block.BlockState;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.BakedQuad;
import net.minecraft.client.render.model.json.ModelOverrideList;
import net.minecraft.client.render.model.json.ModelTransformation;
import net.minecraft.client.texture.Sprite;
import net.minecraft.item.ItemStack;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.Random;
import net.minecraft.world.BlockRenderView;
import net.fabricmc.fabric.api.block.v1.FabricBlockState;
import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView;
import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter;
import net.fabricmc.fabric.api.renderer.v1.model.FabricBakedModel;
import net.fabricmc.fabric.api.renderer.v1.model.ModelHelper;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.fabricmc.fabric.test.renderer.simple.RendererTest;
/**
* Very crude implementation of a pillar block model that connects with pillars above and below.
*/
public class PillarBakedModel implements BakedModel, FabricBakedModel {
private enum ConnectedTexture {
ALONE, BOTTOM, MIDDLE, TOP
}
// alone, bottom, middle, top
private final Sprite[] sprites;
public PillarBakedModel(Sprite[] sprites) {
this.sprites = sprites;
}
@Override
public boolean isVanillaAdapter() {
return false;
}
@Override
public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier<Random> randomSupplier, RenderContext context) {
emitQuads(context.getEmitter(), blockView, state, pos);
}
@Override
public void emitItemQuads(ItemStack stack, Supplier<Random> randomSupplier, RenderContext context) {
emitQuads(context.getEmitter(), null, null, null);
}
private void emitQuads(QuadEmitter emitter, @Nullable BlockRenderView blockView, @Nullable BlockState state, @Nullable BlockPos pos) {
for (Direction side : Direction.values()) {
ConnectedTexture texture = ConnectedTexture.ALONE;
if (side.getAxis().isHorizontal() && blockView != null && state != null && pos != null) {
boolean connectAbove = canConnect(blockView, pos.offset(Direction.UP), side, state, pos);
boolean connectBelow = canConnect(blockView, pos.offset(Direction.DOWN), side, state, pos);
if (connectAbove && connectBelow) {
texture = ConnectedTexture.MIDDLE;
} else if (connectAbove) {
texture = ConnectedTexture.BOTTOM;
} else if (connectBelow) {
texture = ConnectedTexture.TOP;
}
}
emitter.square(side, 0, 0, 1, 1, 0);
emitter.spriteBake(0, sprites[texture.ordinal()], MutableQuadView.BAKE_LOCK_UV);
emitter.spriteColor(0, -1, -1, -1, -1);
emitter.emit();
}
}
private static boolean canConnect(BlockRenderView blockView, BlockPos pos, Direction side, BlockState sourceState, BlockPos sourcePos) {
// In this testmod we can't rely on injected interfaces - in normal mods the (FabricBlockState) cast will be unnecessary
return ((FabricBlockState) blockView.getBlockState(pos)).getAppearance(blockView, pos, side, sourceState, sourcePos).isOf(RendererTest.PILLAR);
}
@Override
public List<BakedQuad> getQuads(@Nullable BlockState state, @Nullable Direction face, Random random) {
return List.of();
}
@Override
public boolean useAmbientOcclusion() {
return true;
}
@Override
public boolean hasDepth() {
return false;
}
@Override
public boolean isSideLit() {
return true;
}
@Override
public boolean isBuiltin() {
return false;
}
@Override
public Sprite getParticleSprite() {
return sprites[0];
}
@Override
public ModelTransformation getTransformation() {
return ModelHelper.MODEL_TRANSFORM_BLOCK;
}
@Override
public ModelOverrideList getOverrides() {
return ModelOverrideList.EMPTY;
}
}

View file

@ -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.renderer.simple.client;
import org.jetbrains.annotations.Nullable;
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.util.ModelIdentifier;
import net.fabricmc.fabric.api.client.model.ModelProviderContext;
import net.fabricmc.fabric.api.client.model.ModelVariantProvider;
import net.fabricmc.fabric.test.renderer.simple.RendererTest;
public class PillarModelVariantProvider implements ModelVariantProvider {
@Override
@Nullable
public UnbakedModel loadModelVariant(ModelIdentifier modelId, ModelProviderContext context) {
if (RendererTest.PILLAR_ID.equals(modelId)) {
return new PillarUnbakedModel();
} else {
return null;
}
}
}

View file

@ -0,0 +1,65 @@
/*
* 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.renderer.simple.client;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import com.mojang.datafixers.util.Pair;
import org.jetbrains.annotations.Nullable;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.ModelBakeSettings;
import net.minecraft.client.render.model.ModelLoader;
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.SpriteIdentifier;
import net.minecraft.screen.PlayerScreenHandler;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.test.renderer.simple.RendererTest;
public class PillarUnbakedModel implements UnbakedModel {
private static final List<SpriteIdentifier> SPRITES = Stream.of("alone", "bottom", "middle", "top")
.map(suffix -> new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, RendererTest.id("block/pillar_" + suffix)))
.toList();
@Override
public Collection<Identifier> getModelDependencies() {
return List.of();
}
@Override
public Collection<SpriteIdentifier> getTextureDependencies(Function<Identifier, UnbakedModel> unbakedModelGetter, Set<Pair<String, String>> unresolvedTextureReferences) {
return SPRITES;
}
@Nullable
@Override
public BakedModel bake(ModelLoader loader, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) {
Sprite[] sprites = new Sprite[SPRITES.size()];
for (int i = 0; i < sprites.length; ++i) {
sprites[i] = textureGetter.apply(SPRITES.get(i));
}
return new PillarBakedModel(sprites);
}
}

View file

@ -28,6 +28,7 @@ public final class RendererClientTest implements ClientModInitializer {
@Override
public void onInitializeClient() {
ModelLoadingRegistry.INSTANCE.registerResourceProvider(manager -> new FrameModelResourceProvider());
ModelLoadingRegistry.INSTANCE.registerVariantProvider(manager -> new PillarModelVariantProvider());
for (FrameBlock frameBlock : RendererTest.FRAMES) {
BlockRenderLayerMap.INSTANCE.putBlock(frameBlock, RenderLayer.getCutoutMipped());

View file

@ -48,7 +48,7 @@ import net.minecraft.world.BlockRenderView;
* and then use {@link #getBlockEntityRenderAttachment(BlockPos)} to retrieve it. When called from the
* main thread, that method will simply retrieve the data directly.
*
* <p>This interface is only guaranteed to be present in the client environment.
* <p>This interface is guaranteed to be implemented on every {@link BlockRenderView} subclass.
*/
// XXX can not link net.fabricmc.fabric.api.renderer.v1.model.FabricBakedModel
public interface RenderAttachedBlockView extends BlockRenderView {

View file

@ -12,6 +12,7 @@ prerelease=false
fabric-api-base-version=0.4.12
fabric-api-lookup-api-v1-version=1.6.10
fabric-biome-api-v1-version=9.0.18
fabric-block-api-v1-version=1.0.0
fabric-blockrenderlayer-v1-version=1.1.21
fabric-command-api-v1-version=1.2.12
fabric-command-api-v2-version=2.1.8

View file

@ -15,6 +15,7 @@ include 'fabric-api-base'
include 'fabric-api-lookup-api-v1'
include 'fabric-biome-api-v1'
include 'fabric-block-api-v1'
include 'fabric-blockrenderlayer-v1'
include 'fabric-command-api-v2'
include 'fabric-content-registries-v0'