/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the Software), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.lanternpowered.server.block;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.lanternpowered.server.block.PropertyProviders.solidCube;
import static org.lanternpowered.server.block.PropertyProviders.solidSide;
import static org.lanternpowered.server.text.translation.TranslationHelper.tr;
import com.flowpowered.math.vector.Vector3d;
import org.lanternpowered.server.behavior.Behavior;
import org.lanternpowered.server.behavior.pipeline.BehaviorPipeline;
import org.lanternpowered.server.behavior.pipeline.MutableBehaviorPipeline;
import org.lanternpowered.server.behavior.pipeline.impl.MutableBehaviorPipelineImpl;
import org.lanternpowered.server.block.aabb.BoundingBoxes;
import org.lanternpowered.server.block.state.LanternBlockState;
import org.lanternpowered.server.block.tile.LanternTileEntityType;
import org.lanternpowered.server.item.ItemTypeBuilder;
import org.lanternpowered.server.item.ItemTypeBuilderImpl;
import org.lanternpowered.server.item.behavior.simple.InteractWithBlockItemBehavior;
import org.lanternpowered.server.item.behavior.types.InteractWithItemBehavior;
import org.spongepowered.api.block.BlockState;
import org.spongepowered.api.block.tileentity.TileEntity;
import org.spongepowered.api.block.tileentity.TileEntityType;
import org.spongepowered.api.block.trait.BlockTrait;
import org.spongepowered.api.data.property.block.SolidCubeProperty;
import org.spongepowered.api.item.ItemType;
import org.spongepowered.api.text.translation.Translation;
import org.spongepowered.api.util.AABB;
import org.spongepowered.api.util.Direction;
import org.spongepowered.api.world.Location;
import org.spongepowered.api.world.World;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nullable;
public class BlockTypeBuilderImpl implements BlockTypeBuilder {
@Nullable private ExtendedBlockStateProvider extendedBlockStateProvider;
@Nullable private Function<BlockState, BlockState> defaultStateProvider;
@Nullable private PropertyProviderCollection.Builder propertiesBuilder;
@Nullable private MutableBehaviorPipeline<Behavior> behaviorPipeline;
private final List<BlockTrait<?>> traits = new ArrayList<>();
@Nullable private TranslationProvider translationProvider;
@Nullable private TileEntityProvider tileEntityProvider;
@Nullable private ItemTypeBuilder itemTypeBuilder;
private ObjectProvider<AABB> boundingBoxProvider = new ConstantObjectProvider<>(BoundingBoxes.DEFAULT);
@Override
public BlockTypeBuilder boundingBox(AABB boundingBox) {
checkNotNull(boundingBox, "boundingBox");
return boundingBox(new ConstantObjectProvider<>(boundingBox));
}
@Override
public BlockTypeBuilder boundingBox(Function<BlockState, AABB> boundingBoxProvider) {
checkNotNull(boundingBoxProvider, "boundingBoxProvider");
return boundingBox(new SimpleObjectProvider<>(boundingBoxProvider));
}
@Override
public BlockTypeBuilder boundingBox(ObjectProvider<AABB> boundingBoxProvider) {
this.boundingBoxProvider = checkNotNull(boundingBoxProvider, "boundingBoxProvider");
return this;
}
@Override
public BlockTypeBuilder defaultState(Function<BlockState, BlockState> function) {
checkNotNull(function, "function");
this.defaultStateProvider = function;
return this;
}
@Override
public BlockTypeBuilder extendedStateProvider(ExtendedBlockStateProvider provider) {
checkNotNull(provider, "provider");
this.extendedBlockStateProvider = provider;
return this;
}
@Override
public BlockTypeBuilder properties(PropertyProviderCollection collection) {
checkNotNull(collection, "collection");
this.propertiesBuilder = collection.toBuilder();
return this;
}
@Override
public BlockTypeBuilder properties(Consumer<PropertyProviderCollection.Builder> consumer) {
checkNotNull(consumer, "consumer");
if (this.propertiesBuilder == null) {
this.propertiesBuilder = PropertyProviderCollections.DEFAULT.toBuilder();
}
consumer.accept(this.propertiesBuilder);
return this;
}
@Override
public BlockTypeBuilderImpl tileEntityType(Supplier<TileEntityType> tileEntityType) {
checkNotNull(tileEntityType, "tileEntityType");
this.tileEntityProvider = (blockState, location, face) -> ((LanternTileEntityType) tileEntityType.get()).getTileEntityConstructor().get();
return this;
}
@Override
public BlockTypeBuilderImpl tileEntity(Supplier<TileEntity> supplier) {
checkNotNull(supplier, "supplier");
this.tileEntityProvider = TileEntityProvider.of(supplier);
return this;
}
@Override
public BlockTypeBuilderImpl tileEntity(TileEntityProvider tileEntityProvider) {
checkNotNull(tileEntityProvider, "tileEntityProvider");
this.tileEntityProvider = tileEntityProvider;
return this;
}
@Override
public BlockTypeBuilderImpl traits(BlockTrait<?>... blockTraits) {
checkNotNull(blockTraits, "blockTraits");
for (BlockTrait<?> blockTrait : blockTraits) {
trait(blockTrait);
}
return this;
}
@Override
public BlockTypeBuilderImpl trait(BlockTrait<?> blockTrait) {
checkNotNull(blockTrait, "blockTrait");
this.traits.add(blockTrait);
return this;
}
@Override
public BlockTypeBuilderImpl translation(String translation) {
return translation(tr(translation));
}
@Override
public BlockTypeBuilderImpl translation(Translation translation) {
this.translationProvider = TranslationProvider.of(checkNotNull(translation, "translation"));
return this;
}
@Override
public BlockTypeBuilderImpl translation(TranslationProvider translationProvider) {
this.translationProvider = checkNotNull(translationProvider, "translationProvider");
return this;
}
@Override
public BlockTypeBuilderImpl behaviors(BehaviorPipeline<Behavior> behaviorPipeline) {
checkNotNull(behaviorPipeline, "behaviorPipeline");
this.behaviorPipeline = new MutableBehaviorPipelineImpl<>(Behavior.class, behaviorPipeline.getBehaviors());
return this;
}
@Override
public BlockTypeBuilderImpl behaviors(Consumer<MutableBehaviorPipeline<Behavior>> consumer) {
checkNotNull(consumer, "consumer");
if (this.behaviorPipeline == null) {
this.behaviorPipeline = new MutableBehaviorPipelineImpl<>(Behavior.class, new ArrayList<>());
}
consumer.accept(this.behaviorPipeline);
return this;
}
@Override
public BlockTypeBuilderImpl itemType() {
this.itemTypeBuilder = new ItemTypeBuilderImpl();
return this;
}
@Override
public BlockTypeBuilderImpl itemType(Consumer<ItemTypeBuilder> consumer) {
checkNotNull(consumer, "consumer");
if (this.itemTypeBuilder == null) {
this.itemTypeBuilder = new ItemTypeBuilderImpl();
}
consumer.accept(this.itemTypeBuilder);
return this;
}
@Override
public LanternBlockType build(String pluginId, String id) {
MutableBehaviorPipeline<Behavior> behaviorPipeline = this.behaviorPipeline;
if (behaviorPipeline == null) {
behaviorPipeline = new MutableBehaviorPipelineImpl<>(Behavior.class, new ArrayList<>());
} else {
behaviorPipeline = new MutableBehaviorPipelineImpl<>(Behavior.class, new ArrayList<>(behaviorPipeline.getBehaviors()));
}
TranslationProvider translationProvider = this.translationProvider;
if (translationProvider == null) {
String path = "tile." + id + ".name";
if (!pluginId.equals("minecraft")) {
path = pluginId + '.' + path;
}
translationProvider = TranslationProvider.of(tr(path));
}
PropertyProviderCollection.Builder properties;
if (this.propertiesBuilder != null) {
properties = this.propertiesBuilder;
} else {
properties = PropertyProviderCollections.DEFAULT.toBuilder();
}
ExtendedBlockStateProvider extendedBlockStateProvider = this.extendedBlockStateProvider;
if (extendedBlockStateProvider == null) {
extendedBlockStateProvider = new ExtendedBlockStateProvider() {
@Override
public BlockState get(BlockState blockState, @Nullable Location<World> location, @Nullable Direction face) {
return blockState;
}
@Override
public BlockState remove(BlockState blockState) {
return blockState;
}
};
}
final LanternBlockType blockType = new LanternBlockType(pluginId, id, this.traits,
translationProvider, behaviorPipeline, this.tileEntityProvider, extendedBlockStateProvider);
// Override the default solid cube property provider if necessary
final PropertyProvider<SolidCubeProperty> provider = properties.build().get(SolidCubeProperty.class).orElse(null);
ObjectProvider<AABB> boundingBoxProvider = this.boundingBoxProvider;
if (boundingBoxProvider instanceof SimpleObjectProvider) {
//noinspection unchecked
boundingBoxProvider = new CachedSimpleObjectProvider(blockType, ((SimpleObjectProvider) boundingBoxProvider).getProvider());
}
//noinspection ConstantConditions
if (provider instanceof ConstantObjectProvider && provider.get(null, null, null).getValue()) {
if (boundingBoxProvider instanceof ConstantObjectProvider) {
//noinspection ConstantConditions
final AABB aabb = boundingBoxProvider.get(null, null, null);
final boolean isSolid = isSolid(aabb);
if (isSolid) {
properties.add(solidCube(true));
properties.add(solidSide(true));
} else {
properties.add(solidCube(false));
final BitSet solidSides = compileSidePropertyBitSet(aabb);
// Check if all the direction bits are set
final byte[] bytes = solidSides.toByteArray();
if (bytes.length == 0 || bytes[0] != (1 << DIRECTION_INDEXES) - 1) {
properties.add(solidSide((blockState, location, face) -> {
final int index = getDirectionIndex(face);
return index != -1 && solidSides.get(index);
}));
} else {
properties.add(solidSide(false));
}
}
} else if (boundingBoxProvider instanceof CachedSimpleObjectProvider) {
final List<AABB> values = ((CachedSimpleObjectProvider<AABB>) boundingBoxProvider).getValues();
final BitSet bitSet = new BitSet();
int count = 0;
for (int i = 0; i < values.size(); i++) {
if (isSolid(values.get(i))) {
bitSet.set(i);
count++;
}
}
final boolean flag1 = count == values.size();
final boolean flag2 = count == 0;
// Use the best possible solid cube property
if (flag1) {
properties.add(solidCube(true));
properties.add(solidSide(false));
} else if (flag2) {
properties.add(solidCube(false));
} else {
properties.add(solidCube(((blockState, location, face) -> bitSet.get(((LanternBlockState) blockState).getInternalId()))));
}
if (!flag1) {
final BitSet[] solidSides = new BitSet[values.size()];
int solidCount = 0;
for (int i = 0; i < values.size(); i++) {
solidSides[i] = compileSidePropertyBitSet(values.get(i));
// Check if all the direction bits are set
final byte[] bytes = solidSides[i].toByteArray();
if (bytes.length != 0 && bytes[0] == (1 << DIRECTION_INDEXES) - 1) {
solidCount++;
}
}
if (solidCount == 0) {
properties.add(solidSide(false));
} else {
properties.add(solidSide((blockState, location, face) -> {
final int index = getDirectionIndex(face);
if (index == -1) {
return false;
}
final int state = ((LanternBlockState) blockState).getInternalId();
return solidSides[state].get(index);
}));
}
}
} else {
final ObjectProvider<AABB> boundingBoxProvider1 = boundingBoxProvider;
properties.add(solidCube(((blockState, location, face) -> isSolid(boundingBoxProvider1.get(blockState, location, face)))));
properties.add(solidSide(((blockState, location, face) -> isSideSolid(boundingBoxProvider1.get(blockState, location, face), face))));
}
}
blockType.setBoundingBoxProvider(boundingBoxProvider);
blockType.setPropertyProviderCollection(properties.build());
if (this.defaultStateProvider != null) {
blockType.setDefaultBlockState(this.defaultStateProvider.apply(blockType.getDefaultState()));
}
if (this.itemTypeBuilder != null) {
final ItemType itemType = this.itemTypeBuilder.blockType(blockType)
.behaviors(pipeline -> {
// Only add the default behavior if there isn't any interaction behavior present
if (pipeline.pipeline(InteractWithItemBehavior.class).getBehaviors().isEmpty()) {
pipeline.add(new InteractWithBlockItemBehavior());
}
})
.build(blockType.getPluginId(), blockType.getName());
blockType.setItemType(itemType);
}
return blockType;
}
private static boolean isSolid(AABB boundingBox) {
return boundingBox.getMin().equals(BoundingBoxes.DEFAULT.getMin()) && boundingBox.getMax().equals(BoundingBoxes.DEFAULT.getMax());
}
private static BitSet compileSidePropertyBitSet(AABB boundingBox) {
final BitSet bitSet = new BitSet(DIRECTION_INDEXES);
if (isSideSolid(boundingBox, Direction.DOWN)) {
bitSet.set(INDEX_DOWN);
}
if (isSideSolid(boundingBox, Direction.UP)) {
bitSet.set(INDEX_UP);
}
if (isSideSolid(boundingBox, Direction.WEST)) {
bitSet.set(INDEX_WEST);
}
if (isSideSolid(boundingBox, Direction.EAST)) {
bitSet.set(INDEX_EAST);
}
if (isSideSolid(boundingBox, Direction.NORTH)) {
bitSet.set(INDEX_NORTH);
}
if (isSideSolid(boundingBox, Direction.SOUTH)) {
bitSet.set(INDEX_SOUTH);
}
return bitSet;
}
private static boolean isSideSolid(AABB boundingBox, @Nullable Direction face) {
final Vector3d min = boundingBox.getMin();
final Vector3d max = boundingBox.getMax();
if (face == Direction.NORTH) {
return min.getZ() == 0.0 && min.getX() == 0.0 && min.getY() == 0.0 && max.getX() >= 1.0 && max.getY() >= 1.0;
} else if (face == Direction.SOUTH) {
return min.getZ() == 1.0 && min.getX() == 0.0 && min.getY() == 0.0 && max.getX() >= 1.0 && max.getY() >= 1.0;
} else if (face == Direction.WEST) {
return min.getZ() == 0.0 && min.getY() == 0.0 && min.getZ() == 0.0 && max.getY() >= 1.0 && max.getZ() >= 1.0;
} else if (face == Direction.EAST) {
return min.getZ() == 1.0 && min.getY() == 0.0 && min.getZ() == 0.0 && max.getY() >= 1.0 && max.getZ() >= 1.0;
} else if (face == Direction.DOWN) {
return min.getZ() == 0.0 && min.getX() == 0.0 && min.getZ() == 0.0 && max.getX() == 1.0 && max.getZ() == 1.0;
} else if (face == Direction.UP) {
return min.getZ() == 1.0 && min.getX() == 0.0 && min.getZ() == 0.0 && max.getX() == 1.0 && max.getZ() == 1.0;
} else {
return false;
}
}
private static final int DIRECTION_INDEXES = 6;
private static final int INDEX_NORTH = 0;
private static final int INDEX_SOUTH = 1;
private static final int INDEX_WEST = 2;
private static final int INDEX_EAST = 3;
private static final int INDEX_UP = 4;
private static final int INDEX_DOWN = 5;
private static int getDirectionIndex(@Nullable Direction direction) {
if (direction == null) {
return -1;
}
switch (direction) {
case NORTH:
return 0;
case SOUTH:
return 1;
case WEST:
return 2;
case EAST:
return 3;
case UP:
return 4;
case DOWN:
return 5;
default:
return -1;
}
}
}