package slimeknights.tconstruct.shared.client; import com.google.common.base.Function; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.block.model.BakedQuad; import net.minecraft.client.renderer.block.model.IBakedModel; import net.minecraft.client.renderer.block.model.ItemCameraTransforms; import net.minecraft.client.renderer.block.model.ItemOverrideList; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.vertex.VertexFormat; import net.minecraft.entity.EntityLivingBase; import net.minecraft.item.ItemStack; import net.minecraft.util.EnumFacing; import net.minecraft.util.ResourceLocation; import net.minecraft.world.World; import net.minecraftforge.client.model.IModel; import net.minecraftforge.client.model.IPerspectiveAwareModel; import net.minecraftforge.client.model.IRetexturableModel; import net.minecraftforge.client.model.SimpleModelState; import net.minecraftforge.common.model.IModelState; import net.minecraftforge.common.model.TRSRTransformation; import net.minecraftforge.common.property.IExtendedBlockState; import net.minecraftforge.common.property.IUnlistedProperty; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.Logger; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import javax.annotation.Nonnull; import javax.vecmath.Matrix4f; import slimeknights.mantle.client.model.BakedCompositeModel; import slimeknights.mantle.client.model.TRSRBakedModel; import slimeknights.tconstruct.common.config.Config; import slimeknights.tconstruct.library.Util; import slimeknights.tconstruct.library.client.model.ModelHelper; import slimeknights.tconstruct.library.utils.TagUtil; import slimeknights.tconstruct.shared.block.BlockTable; import slimeknights.tconstruct.shared.block.PropertyTableItem; import slimeknights.tconstruct.shared.tileentity.TileTable; public class BakedTableModel implements IPerspectiveAwareModel { static final Logger log = Util.getLogger("Table Model"); private final IPerspectiveAwareModel standard; private final IRetexturableModel tableModel; private final Map<String, IBakedModel> cache = Maps.newHashMap(); private final Function<ResourceLocation, TextureAtlasSprite> textureGetter; private final VertexFormat format; private final ImmutableMap<ItemCameraTransforms.TransformType, TRSRTransformation> transforms; private final LoadingCache<PropertyTableItem.TableItem, IBakedModel> tableItemCache = CacheBuilder .newBuilder() .maximumSize(250) .build(new CacheLoader<PropertyTableItem.TableItem, IBakedModel>() { @Override public IBakedModel load(PropertyTableItem.TableItem key) throws Exception { return BakedTableModel.this.getModelForTableItem(key); } }); private final Cache<TableItemCombinationCacheKey, IBakedModel> tableItemCombinedCache = CacheBuilder .newBuilder() .maximumSize(20) .build(); public BakedTableModel(IPerspectiveAwareModel standard, IRetexturableModel tableModel, VertexFormat format) { this.standard = standard; this.tableModel = tableModel; this.textureGetter = location -> Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(location.toString()); this.format = format; this.transforms = ModelHelper.getTransforms(standard); } protected IBakedModel getActualModel(String texture, List<PropertyTableItem.TableItem> items, EnumFacing facing) { IBakedModel bakedModel = standard; if(texture != null) { if(cache.containsKey(texture)) { bakedModel = cache.get(texture); } else if(tableModel != null) { ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); builder.put("bottom", texture); builder.put("leg", texture); builder.put("legBottom", texture); IModel retexturedModel = tableModel.retexture(builder.build()); IModelState modelState = new SimpleModelState(transforms); bakedModel = retexturedModel.bake(modelState, format, textureGetter); cache.put(texture, bakedModel); } } final IBakedModel parentModel = bakedModel; try { bakedModel = tableItemCombinedCache.get(new TableItemCombinationCacheKey(items, bakedModel, facing), () -> getCombinedBakedModel(items, facing, parentModel)); } catch(ExecutionException e) { log.error(e); } return bakedModel; } private IBakedModel getCombinedBakedModel(List<PropertyTableItem.TableItem> items, EnumFacing facing, IBakedModel parentModel) { IBakedModel out = parentModel; // add all the items to display on the table if(items != null && !items.isEmpty()) { BakedCompositeModel.Builder builder = new BakedCompositeModel.Builder(); builder.add(parentModel, null, 0); for(PropertyTableItem.TableItem item : items) { try { builder.add(tableItemCache.get(item), null, 0); } catch(ExecutionException e) { log.error(e); } } out = builder.build(parentModel); } if(facing != null) { out = new TRSRBakedModel(out, facing); } return out; } private IBakedModel getModelForTableItem(PropertyTableItem.TableItem item) { return new TRSRBakedModel(item.model, item.x, item.y + 1f, item.z, item.r, (float) (Math.PI), 0, item.s); } @Nonnull @Override public List<BakedQuad> getQuads(IBlockState state, EnumFacing side, long rand) { // get texture from state String texture = null; List<PropertyTableItem.TableItem> items = Collections.emptyList(); EnumFacing face = EnumFacing.SOUTH; if(state instanceof IExtendedBlockState) { IExtendedBlockState extendedState = (IExtendedBlockState) state; if(extendedState.getUnlistedNames().contains(BlockTable.TEXTURE)) { texture = extendedState.getValue(BlockTable.TEXTURE); } if(Config.renderTableItems && extendedState.getUnlistedNames().contains(BlockTable.INVENTORY)) { if(extendedState.getValue(BlockTable.INVENTORY) != null) { items = extendedState.getValue(BlockTable.INVENTORY).items; } } if(extendedState.getUnlistedNames().contains(BlockTable.FACING)) { face = extendedState.getValue((IUnlistedProperty<EnumFacing>) BlockTable.FACING); } // remove all world specific data // This is so that the next call to getQuads from the transformed TRSRModel doesn't do this again // otherwise table models inside table model items recursively calls this with the state of the original table state = extendedState.withProperty(BlockTable.INVENTORY, PropertyTableItem.TableItems.EMPTY).withProperty((IUnlistedProperty<EnumFacing>) BlockTable.FACING, null); } // models are symmetric, no need to rotate if there's nothing on it where rotation matters, so we just use default if(texture == null && items == null) { return standard.getQuads(state, side, rand); } // the model returned by getActualModel should be a simple model with no special handling return getActualModel(texture, items, face).getQuads(state, side, rand); } @Override public boolean isAmbientOcclusion() { return standard.isAmbientOcclusion(); } @Override public boolean isGui3d() { return standard.isGui3d(); } @Override public boolean isBuiltInRenderer() { return standard.isBuiltInRenderer(); } @Nonnull @Override public TextureAtlasSprite getParticleTexture() { return standard.getParticleTexture(); } @Nonnull @Override public ItemCameraTransforms getItemCameraTransforms() { return standard.getItemCameraTransforms(); } @Nonnull @Override public ItemOverrideList getOverrides() { return TableItemOverrideList.INSTANCE; } @Override public Pair<? extends IBakedModel, Matrix4f> handlePerspective(ItemCameraTransforms.TransformType cameraTransformType) { Pair<? extends IBakedModel, Matrix4f> pair = standard.handlePerspective(cameraTransformType); return Pair.of(this, pair.getRight()); } private static class TableItemOverrideList extends ItemOverrideList { static TableItemOverrideList INSTANCE = new TableItemOverrideList(); private TableItemOverrideList() { super(ImmutableList.of()); } @Nonnull @Override public IBakedModel handleItemState(@Nonnull IBakedModel originalModel, ItemStack stack, @Nonnull World world, @Nonnull EntityLivingBase entity) { if(originalModel instanceof BakedTableModel) { // read out the data on the itemstack ItemStack blockStack = ItemStack .loadItemStackFromNBT(TagUtil.getTagSafe(stack).getCompoundTag(TileTable.FEET_TAG)); if(blockStack != null) { // get model from data Block block = Block.getBlockFromItem(blockStack.getItem()); String texture = ModelHelper.getTextureFromBlock(block, blockStack.getItemDamage()).getIconName(); return ((BakedTableModel) originalModel) .getActualModel(texture, Collections.emptyList(), null); } } return originalModel; } } private static class TableItemCombinationCacheKey { private final List<PropertyTableItem.TableItem> tableItems; private final IBakedModel bakedBaseModel; private final EnumFacing facing; public TableItemCombinationCacheKey(List<PropertyTableItem.TableItem> tableItems, IBakedModel bakedBaseModel, EnumFacing facing) { this.tableItems = tableItems; this.bakedBaseModel = bakedBaseModel; this.facing = facing; } @Override public boolean equals(Object o) { if(this == o) { return true; } if(o == null || getClass() != o.getClass()) { return false; } TableItemCombinationCacheKey that = (TableItemCombinationCacheKey) o; if(tableItems != null ? !tableItems.equals(that.tableItems) : that.tableItems != null) { return false; } if(bakedBaseModel != null ? !bakedBaseModel.equals(that.bakedBaseModel) : that.bakedBaseModel != null) { return false; } return facing == that.facing; } @Override public int hashCode() { int result = tableItems != null ? tableItems.hashCode() : 0; result = 31 * result + (bakedBaseModel != null ? bakedBaseModel.hashCode() : 0); result = 31 * result + (facing != null ? facing.hashCode() : 0); return result; } } }