diff --git a/build.gradle b/build.gradle index d64f7c1..dfe9f23 100755 --- a/build.gradle +++ b/build.gradle @@ -93,8 +93,8 @@ dependencies { modRuntimeOnly "earth.terrarium.athena:athena-fabric-${project.minecraft_version}:${project.athena_version}" // Chipped to test athena implementation -// modRuntimeOnly "com.teamresourceful.resourcefullib:resourcefullib-fabric-${project.minecraft_version}:2.4.7" -// modRuntimeOnly "earth.terrarium.chipped:Chipped-fabric-${project.minecraft_version}:3.1.2" + modRuntimeOnly "com.teamresourceful.resourcefullib:resourcefullib-fabric-${project.minecraft_version}:2.4.7" + modRuntimeOnly "earth.terrarium.chipped:Chipped-fabric-${project.minecraft_version}:3.1.2" // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" diff --git a/src/main/java/fr/adrien1106/reframed/block/ReFramedDoubleEntity.java b/src/main/java/fr/adrien1106/reframed/block/ReFramedDoubleEntity.java new file mode 100644 index 0000000..76eadbf --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/block/ReFramedDoubleEntity.java @@ -0,0 +1,53 @@ +package fr.adrien1106.reframed.block; + +import fr.adrien1106.reframed.ReFramed; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtHelper; +import net.minecraft.registry.Registries; +import net.minecraft.util.math.BlockPos; + +import java.util.Objects; + +public class ReFramedDoubleEntity extends ReFramedEntity { + + protected BlockState second_state = Blocks.AIR.getDefaultState(); + + public ReFramedDoubleEntity(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); + } + + @Override + public BlockState getSecondTheme() { + return second_state; + } + + public void setSecondTheme(BlockState newState) { + if(!Objects.equals(second_state, newState)) { + second_state = newState; + markDirtyAndDispatch(); + } + } + + @Override + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); + + BlockState rendered_state = second_state;// keep previous state to check if rerender is needed + first_state = NbtHelper.toBlockState(Registries.BLOCK.getReadOnlyWrapper(), nbt.getCompound(BLOCKSTATE_KEY + 2)); + + // Force a chunk remesh on the client if the displayed blockstate has changed + if(world != null && world.isClient && !Objects.equals(rendered_state, second_state)) { + ReFramed.chunkRerenderProxy.accept(world, pos); + } + } + + @Override + public void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); + + if(second_state != Blocks.AIR.getDefaultState()) nbt.put(BLOCKSTATE_KEY + 2, NbtHelper.fromBlockState(second_state)); + } +} diff --git a/src/main/java/fr/adrien1106/reframed/block/ReFramedEntity.java b/src/main/java/fr/adrien1106/reframed/block/ReFramedEntity.java index 5f179a5..c2ad09d 100644 --- a/src/main/java/fr/adrien1106/reframed/block/ReFramedEntity.java +++ b/src/main/java/fr/adrien1106/reframed/block/ReFramedEntity.java @@ -27,19 +27,13 @@ import java.util.Objects; //is pretty important since players might place a lot of them. There were tons and tons of these at Blanketcon. //To that end, most of the state has been crammed into a bitfield. public class ReFramedEntity extends BlockEntity implements ThemeableBlockEntity { - protected BlockState renderedState = Blocks.AIR.getDefaultState(); - protected byte bitfield = DEFAULT_BITFIELD; + protected BlockState first_state = Blocks.AIR.getDefaultState(); + protected byte bitfield = SOLIDITY_MASK; - protected static final int SPENT_GLOWSTONE_DUST_MASK = 0b00000001; - protected static final int SPENT_REDSTONE_TORCH_MASK = 0b00000010; - protected static final int SPENT_POPPED_CHORUS_MASK = 0b00000100; - protected static final int EMITS_REDSTONE_MASK = 0b00001000; - protected static final int IS_SOLID_MASK = 0b00010000; - protected static final byte DEFAULT_BITFIELD = IS_SOLID_MASK; //brand-new frames shall be solid - - //Using one-character names is a little brash, like, what if there's a mod that adds crap to the NBT of every - //block entity, and uses short names for the same reason I am (because there are lots and lots of block entities)? - //Kinda doubt it? + protected static final byte LIGHT_MASK = 0b001; + protected static final byte REDSTONE_MASK = 0b010; + protected static final byte SOLIDITY_MASK = 0b100; + protected static final String BLOCKSTATE_KEY = "s"; protected static final String BITFIELD_KEY = "b"; @@ -48,50 +42,38 @@ public class ReFramedEntity extends BlockEntity implements ThemeableBlockEntity } @Override - public void readNbt(NbtCompound tag) { - super.readNbt(tag); + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); - BlockState lastRenderedState = renderedState; + BlockState rendered_state = first_state; // keep previous state to check if rerender is needed + first_state = NbtHelper.toBlockState(Registries.BLOCK.getReadOnlyWrapper(), nbt.getCompound(BLOCKSTATE_KEY + 1)); + if (nbt.contains(BITFIELD_KEY)) bitfield = nbt.getByte(BITFIELD_KEY); - if(tag.contains("BlockState")) { //2.0.4 and earlier - renderedState = NbtHelper.toBlockState(Registries.BLOCK.getReadOnlyWrapper(), tag.getCompound("BlockState")); - - if(tag.getBoolean("spentglow")) spentGlowstoneDust(); - if(tag.getBoolean("spentredst")) spentRedstoneTorch(); - if(tag.getBoolean("spentchor")) spentPoppedChorus(); - setEmitsRedstone(tag.getBoolean("emitsredst")); - setSolidity(!tag.contains("solid") || tag.getBoolean("solid")); //default to "true" if it's nonexistent - } else { - renderedState = NbtHelper.toBlockState(Registries.BLOCK.getReadOnlyWrapper(), tag.getCompound(BLOCKSTATE_KEY)); - bitfield = tag.contains(BITFIELD_KEY) ? tag.getByte(BITFIELD_KEY) : DEFAULT_BITFIELD; - } - - //Force a chunk remesh on the client if the displayed blockstate has changed - if(world != null && world.isClient && !Objects.equals(lastRenderedState, renderedState)) { + // Force a chunk remesh on the client if the displayed blockstate has changed + if(world != null && world.isClient && !Objects.equals(rendered_state, first_state)) { ReFramed.chunkRerenderProxy.accept(world, pos); } } @Override - public void writeNbt(NbtCompound tag) { - super.writeNbt(tag); + public void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); - if(renderedState != Blocks.AIR.getDefaultState()) tag.put(BLOCKSTATE_KEY, NbtHelper.fromBlockState(renderedState)); - if(bitfield != DEFAULT_BITFIELD) tag.putByte(BITFIELD_KEY, bitfield); + if(first_state != Blocks.AIR.getDefaultState()) nbt.put(BLOCKSTATE_KEY + 1, NbtHelper.fromBlockState(first_state)); + if(bitfield != SOLIDITY_MASK) nbt.putByte(BITFIELD_KEY, bitfield); } - + + // TODO revisit public static @NotNull BlockState readStateFromItem(ItemStack stack) { NbtCompound blockEntityTag = BlockItem.getBlockEntityNbt(stack); if(blockEntityTag == null) return Blocks.AIR.getDefaultState(); //slightly paranoid NBT handling cause you never know what mysteries are afoot with items NbtElement subElement; - if(blockEntityTag.contains(BLOCKSTATE_KEY)) subElement = blockEntityTag.get(BLOCKSTATE_KEY); //2.0.5 - else if(blockEntityTag.contains("BlockState")) subElement = blockEntityTag.get("BlockState"); //old 2.0.4 items + if(blockEntityTag.contains(BLOCKSTATE_KEY)) subElement = blockEntityTag.get(BLOCKSTATE_KEY + 1); else return Blocks.AIR.getDefaultState(); if(!(subElement instanceof NbtCompound subCompound)) return Blocks.AIR.getDefaultState(); - else return NbtHelper.toBlockState(Registries.BLOCK.getReadOnlyWrapper(), subCompound); } @@ -102,91 +84,65 @@ public class ReFramedEntity extends BlockEntity implements ThemeableBlockEntity public static @Nullable BlockState weirdNbtLightLevelStuff(@Nullable BlockState state, ItemStack stack) { if(state == null || stack == null) return state; - NbtCompound blockEntityTag = BlockItem.getBlockEntityNbt(stack); - if(blockEntityTag == null) return state; + NbtCompound nbt = BlockItem.getBlockEntityNbt(stack); + if(nbt == null) return state; if(state.contains(ReFramedInteractionUtil.LIGHT)) { state = state.with(ReFramedInteractionUtil.LIGHT, - blockEntityTag.getBoolean("spentglow") || //2.0.4 - ((blockEntityTag.contains(BITFIELD_KEY) ? blockEntityTag.getByte(BITFIELD_KEY) : DEFAULT_BITFIELD) & SPENT_GLOWSTONE_DUST_MASK) != 0 || //2.0.5 - readStateFromItem(stack).getLuminance() != 0 //glowstone dust wasn't manually added, the block just emits light + ((nbt.contains(BITFIELD_KEY) + ? nbt.getByte(BITFIELD_KEY) + : SOLIDITY_MASK) + & LIGHT_MASK) != 0 ); } return state; } - - //RenderAttachmentBlockEntity impl. Note that ThemeableBlockEntity depends on this returning a BlockState object. + @Override - public BlockState getRenderAttachmentData() { - return renderedState; + public BlockState getFirstTheme() { + return first_state; } - public void setRenderedState(BlockState newState) { - if(!Objects.equals(renderedState, newState)) { - renderedState = newState; + public void setFirstTheme(BlockState newState) { + if(!Objects.equals(first_state, newState)) { + first_state = newState; markDirtyAndDispatch(); } } - - public boolean hasSpentGlowstoneDust() { - return (bitfield & SPENT_GLOWSTONE_DUST_MASK) != 0; + + /* --------------------------------------------------- ADDONS --------------------------------------------------- */ + public boolean emitsLight() { + return (bitfield & LIGHT_MASK) != 0; } - public void spentGlowstoneDust() { - bitfield |= SPENT_GLOWSTONE_DUST_MASK; + public void toggleLight() { + bitfield |= emitsLight() ? ~LIGHT_MASK: LIGHT_MASK; markDirtyAndDispatch(); } - public boolean hasSpentRedstoneTorch() { - return (bitfield & SPENT_REDSTONE_TORCH_MASK) != 0; - } - - public void spentRedstoneTorch() { - bitfield |= SPENT_REDSTONE_TORCH_MASK; + public void toggleRedstone() { + bitfield |= emitsRedstone() ? ~REDSTONE_MASK: REDSTONE_MASK; + + if(world != null) world.updateNeighbors(pos, getCachedState().getBlock()); markDirtyAndDispatch(); } - - public boolean hasSpentPoppedChorus() { - return (bitfield & SPENT_POPPED_CHORUS_MASK) != 0; - } - - public void spentPoppedChorus() { - bitfield |= SPENT_POPPED_CHORUS_MASK; - markDirtyAndDispatch(); - } - + public boolean emitsRedstone() { - return (bitfield & EMITS_REDSTONE_MASK) != 0; + return (bitfield & REDSTONE_MASK) != 0; } - public void setEmitsRedstone(boolean nextEmitsRedstone) { - boolean currentlyEmitsRedstone = emitsRedstone(); - - if(currentlyEmitsRedstone != nextEmitsRedstone) { - if(currentlyEmitsRedstone) bitfield &= ~EMITS_REDSTONE_MASK; - else bitfield |= EMITS_REDSTONE_MASK; - markDirtyAndDispatch(); - if(world != null) world.updateNeighbors(pos, getCachedState().getBlock()); - } + public void toggleSolidity() { + bitfield |= isSolid() ? ~SOLIDITY_MASK: SOLIDITY_MASK; + + if(world != null) world.setBlockState(pos, getCachedState()); + markDirtyAndDispatch(); } public boolean isSolid() { - return (bitfield & IS_SOLID_MASK) != 0; + return (bitfield & SOLIDITY_MASK) != 0; } - - public void setSolidity(boolean nextSolid) { - boolean currentlySolid = isSolid(); - - if(currentlySolid != nextSolid) { - if(currentlySolid) bitfield &= ~IS_SOLID_MASK; - else bitfield |= IS_SOLID_MASK; - markDirtyAndDispatch(); - if(world != null) world.setBlockState(pos, getCachedState()); //do i need to invalidate any shape caches or something - } - } - - // + @Nullable @Override public Packet toUpdatePacket() { @@ -195,8 +151,6 @@ public class ReFramedEntity extends BlockEntity implements ThemeableBlockEntity @Override public NbtCompound toInitialChunkDataNbt() { - //TERRIBLE yarn name, this is "getUpdateTag", it's the nbt that will be sent to clients - //and it just calls "writeNbt" return createNbt(); } @@ -208,5 +162,4 @@ public class ReFramedEntity extends BlockEntity implements ThemeableBlockEntity markDirty(); dispatch(); } - // } diff --git a/src/main/java/fr/adrien1106/reframed/client/ReFramedClientHelper.java b/src/main/java/fr/adrien1106/reframed/client/ReFramedClientHelper.java index 87c7591..9ac76d7 100644 --- a/src/main/java/fr/adrien1106/reframed/client/ReFramedClientHelper.java +++ b/src/main/java/fr/adrien1106/reframed/client/ReFramedClientHelper.java @@ -3,9 +3,11 @@ package fr.adrien1106.reframed.client; import fr.adrien1106.reframed.client.model.apperance.CamoAppearanceManager; import fr.adrien1106.reframed.client.model.UnbakedAutoRetexturedModel; import fr.adrien1106.reframed.client.model.UnbakedJsonRetexturedModel; +import fr.adrien1106.reframed.util.ThemeableBlockEntity; import net.fabricmc.fabric.api.renderer.v1.Renderer; import net.fabricmc.fabric.api.renderer.v1.RendererAccess; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.block.BlockState; import net.minecraft.client.render.model.UnbakedModel; import net.minecraft.client.texture.Sprite; import net.minecraft.client.util.SpriteIdentifier; @@ -24,11 +26,19 @@ public class ReFramedClientHelper { private final ReFramedModelProvider prov; public UnbakedModel auto(Identifier parent) { - return new UnbakedAutoRetexturedModel(parent); + return auto(parent, ThemeableBlockEntity::getFirstTheme); + } + + public UnbakedModel auto(Identifier parent, Function state_getter) { + return new UnbakedAutoRetexturedModel(parent, state_getter); } public UnbakedModel json(Identifier parent) { - return new UnbakedJsonRetexturedModel(parent); + return json(parent, ThemeableBlockEntity::getFirstTheme); + } + + public UnbakedModel json(Identifier parent, Function state_getter) { + return new UnbakedJsonRetexturedModel(parent, state_getter); } public void addReFramedModel(Identifier id, UnbakedModel unbaked) { diff --git a/src/main/java/fr/adrien1106/reframed/client/model/DoubleRetexturingBakedModel.java b/src/main/java/fr/adrien1106/reframed/client/model/DoubleRetexturingBakedModel.java new file mode 100644 index 0000000..c255fb9 --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/client/model/DoubleRetexturingBakedModel.java @@ -0,0 +1,43 @@ +package fr.adrien1106.reframed.client.model; + +import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel; +import net.fabricmc.fabric.api.renderer.v1.render.RenderContext; +import net.minecraft.block.BlockState; +import net.minecraft.client.texture.Sprite; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.random.Random; +import net.minecraft.world.BlockRenderView; + +import java.util.function.Supplier; + +public class DoubleRetexturingBakedModel extends ForwardingBakedModel { + + private final ForwardingBakedModel model_1, model_2; + public DoubleRetexturingBakedModel(ForwardingBakedModel model_1, ForwardingBakedModel model_2) { + this.model_1 = model_1; + this.model_2 = model_2; + } + + @Override + public boolean isVanillaAdapter() { + return false; + } + + @Override + public Sprite getParticleSprite() { + return model_1.getParticleSprite(); // TODO determine which face is on top + } + + @Override + public void emitBlockQuads(BlockRenderView world, BlockState state, BlockPos pos, Supplier randomSupplier, RenderContext context) { + model_1.emitBlockQuads(world, state, pos, randomSupplier, context); + model_2.emitBlockQuads(world, state, pos, randomSupplier, context); + } + + @Override + public void emitItemQuads(ItemStack stack, Supplier randomSupplier, RenderContext context) { + model_1.emitItemQuads(stack, randomSupplier, context); + model_2.emitItemQuads(stack, randomSupplier, context); + } +} diff --git a/src/main/java/fr/adrien1106/reframed/client/model/RetexturingBakedModel.java b/src/main/java/fr/adrien1106/reframed/client/model/RetexturingBakedModel.java index be809a6..32782b0 100644 --- a/src/main/java/fr/adrien1106/reframed/client/model/RetexturingBakedModel.java +++ b/src/main/java/fr/adrien1106/reframed/client/model/RetexturingBakedModel.java @@ -6,6 +6,7 @@ import fr.adrien1106.reframed.mixin.MinecraftAccessor; import fr.adrien1106.reframed.client.model.apperance.CamoAppearance; import fr.adrien1106.reframed.client.model.apperance.CamoAppearanceManager; import fr.adrien1106.reframed.client.model.apperance.WeightedComputedAppearance; +import fr.adrien1106.reframed.util.ThemeableBlockEntity; import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView; import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter; @@ -26,19 +27,22 @@ import net.minecraft.world.BlockRenderView; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; import java.util.function.Supplier; public abstract class RetexturingBakedModel extends ForwardingBakedModel { - public RetexturingBakedModel(BakedModel baseModel, CamoAppearanceManager tam, ModelBakeSettings settings, BlockState itemModelState, boolean ao) { + public RetexturingBakedModel(BakedModel baseModel, CamoAppearanceManager tam, Function state_getter, ModelBakeSettings settings, BlockState itemModelState, boolean ao) { this.wrapped = baseModel; //field from the superclass; vanilla getQuads etc. will delegate through to this this.tam = tam; + this.state_getter = state_getter; this.uvlock = settings.isUvLocked(); this.itemModelState = itemModelState; this.ao = ao; } - + protected final CamoAppearanceManager tam; + protected final Function state_getter; protected final boolean uvlock; protected final BlockState itemModelState; protected final boolean ao; @@ -51,8 +55,17 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel { protected static final Direction[] DIRECTIONS = Direction.values(); protected static final Direction[] DIRECTIONS_AND_NULL = new Direction[DIRECTIONS.length + 1]; static { System.arraycopy(DIRECTIONS, 0, DIRECTIONS_AND_NULL, 0, DIRECTIONS.length); } - - protected abstract Mesh getBaseMesh(BlockState state); + + + protected final ConcurrentMap jsonToMesh = new ConcurrentHashMap<>(); + + + protected Mesh getBaseMesh(BlockState state) { + //Convert models to re-texturable Meshes lazily, the first time we encounter each blockstate + return jsonToMesh.computeIfAbsent(state, this::convertModel); + } + + protected abstract Mesh convertModel(BlockState state); @Override public boolean isVanillaAdapter() { @@ -66,7 +79,7 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel { @Override public void emitBlockQuads(BlockRenderView world, BlockState state, BlockPos pos, Supplier randomSupplier, RenderContext context) { - BlockState theme = (world.getBlockEntityRenderData(pos) instanceof BlockState s) ? s : null; + BlockState theme = (world.getBlockEntity(pos) instanceof ThemeableBlockEntity s) ? state_getter.apply(s) : null; QuadEmitter quad_emitter = context.getEmitter(); if(theme == null || theme.isAir()) { getUntintedRetexturedMesh(new MeshCacheKey(state, new TransformCacheKey(tam.getDefaultAppearance(), 0)), 0).outputTo(quad_emitter); @@ -105,7 +118,7 @@ public abstract class RetexturingBakedModel extends ForwardingBakedModel { //none of this is accessible unless you're in creative mode doing ctrl-pick btw CamoAppearance nbtAppearance; int tint; - BlockState theme = ReFramedEntity.readStateFromItem(stack); + BlockState theme = ReFramedEntity.readStateFromItem(stack); // TODO Different states for both models if(!theme.isAir()) { nbtAppearance = tam.getCamoAppearance(null, theme, null); tint = 0xFF000000 | ((MinecraftAccessor) MinecraftClient.getInstance()).getItemColors().getColor(new ItemStack(theme.getBlock()), 0); diff --git a/src/main/java/fr/adrien1106/reframed/client/model/UnbakedAutoRetexturedModel.java b/src/main/java/fr/adrien1106/reframed/client/model/UnbakedAutoRetexturedModel.java index 216da7c..c6b4266 100644 --- a/src/main/java/fr/adrien1106/reframed/client/model/UnbakedAutoRetexturedModel.java +++ b/src/main/java/fr/adrien1106/reframed/client/model/UnbakedAutoRetexturedModel.java @@ -1,6 +1,7 @@ package fr.adrien1106.reframed.client.model; import fr.adrien1106.reframed.client.ReFramedClient; +import fr.adrien1106.reframed.util.ThemeableBlockEntity; import net.fabricmc.fabric.api.renderer.v1.Renderer; import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; @@ -12,7 +13,6 @@ import net.minecraft.client.render.model.BakedModel; import net.minecraft.client.render.model.BakedQuad; import net.minecraft.client.render.model.Baker; import net.minecraft.client.render.model.ModelBakeSettings; -import net.minecraft.client.render.model.UnbakedModel; import net.minecraft.client.texture.Sprite; import net.minecraft.client.util.SpriteIdentifier; import net.minecraft.util.Identifier; @@ -20,50 +20,27 @@ import net.minecraft.util.math.Direction; import net.minecraft.util.math.random.Random; import org.jetbrains.annotations.Nullable; -import java.util.Collection; -import java.util.Collections; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.Function; -public class UnbakedAutoRetexturedModel implements UnbakedModel { - public UnbakedAutoRetexturedModel(Identifier parent) { - this.parent = parent; - } - - protected final Identifier parent; - protected BlockState itemModelState = Blocks.AIR.getDefaultState(); - protected boolean ao = true; - - @Override - public Collection getModelDependencies() { - return Collections.singletonList(parent); - } - - @Override - public void setParents(Function function) { - function.apply(parent).setParents(function); +public class UnbakedAutoRetexturedModel extends UnbakedRetexturedModel { + + public UnbakedAutoRetexturedModel(Identifier parent, Function state_getter) { + super(parent, state_getter); + item_state = Blocks.AIR.getDefaultState(); } @Nullable @Override - public BakedModel bake(Baker baker, Function spriteLookup, ModelBakeSettings modelBakeSettings, Identifier identifier) { + public BakedModel bake(Baker baker, Function texture_getter, ModelBakeSettings bake_settings, Identifier identifier) { return new RetexturingBakedModel( - baker.bake(parent, modelBakeSettings), - ReFramedClient.HELPER.getCamoApperanceManager(spriteLookup), - modelBakeSettings, - itemModelState, + baker.bake(parent, bake_settings), + ReFramedClient.HELPER.getCamoApperanceManager(texture_getter), + state_getter, + bake_settings, + item_state, ao ) { - final ConcurrentMap jsonToMesh = new ConcurrentHashMap<>(); - - @Override - protected Mesh getBaseMesh(BlockState state) { - //Convert models to retexturable Meshes lazily, the first time we encounter each blockstate - return jsonToMesh.computeIfAbsent(state, this::convertModel); - } - - private Mesh convertModel(BlockState state) { + protected Mesh convertModel(BlockState state) { Renderer r = ReFramedClient.HELPER.getFabricRenderer(); MeshBuilder builder = r.meshBuilder(); QuadEmitter emitter = builder.getEmitter(); diff --git a/src/main/java/fr/adrien1106/reframed/client/model/UnbakedDoubleRetexturedModel.java b/src/main/java/fr/adrien1106/reframed/client/model/UnbakedDoubleRetexturedModel.java new file mode 100644 index 0000000..d765e75 --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/client/model/UnbakedDoubleRetexturedModel.java @@ -0,0 +1,47 @@ +package fr.adrien1106.reframed.client.model; + +import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.Baker; +import net.minecraft.client.render.model.ModelBakeSettings; +import net.minecraft.client.render.model.UnbakedModel; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.util.SpriteIdentifier; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +public class UnbakedDoubleRetexturedModel implements UnbakedModel { + + protected final UnbakedModel model_1; + protected final UnbakedModel model_2; + + public UnbakedDoubleRetexturedModel(UnbakedModel model_1, UnbakedModel model_2) { + this.model_1 = model_1; + this.model_2 = model_2; + } + + + @Override + public Collection getModelDependencies() { + return List.of(model_1.getModelDependencies().iterator().next(), model_2.getModelDependencies().iterator().next()); + } + + @Override + public void setParents(Function function) { + model_1.setParents(function); + model_2.setParents(function); + } + + @Nullable + @Override + public BakedModel bake(Baker baker, Function texture_getter, ModelBakeSettings model_bake_settings, Identifier identifier) { + return new DoubleRetexturingBakedModel( + (ForwardingBakedModel) model_1.bake(baker, texture_getter, model_bake_settings, identifier), + (ForwardingBakedModel) model_2.bake(baker, texture_getter, model_bake_settings, identifier) + ); + } +} diff --git a/src/main/java/fr/adrien1106/reframed/client/model/UnbakedJsonRetexturedModel.java b/src/main/java/fr/adrien1106/reframed/client/model/UnbakedJsonRetexturedModel.java index b984c3d..40cba62 100644 --- a/src/main/java/fr/adrien1106/reframed/client/model/UnbakedJsonRetexturedModel.java +++ b/src/main/java/fr/adrien1106/reframed/client/model/UnbakedJsonRetexturedModel.java @@ -2,13 +2,17 @@ package fr.adrien1106.reframed.client.model; import fr.adrien1106.reframed.ReFramed; import fr.adrien1106.reframed.client.ReFramedClient; +import fr.adrien1106.reframed.util.ThemeableBlockEntity; import net.fabricmc.fabric.api.renderer.v1.Renderer; import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial; import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh; import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder; import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter; import net.minecraft.block.BlockState; -import net.minecraft.client.render.model.*; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.BakedQuad; +import net.minecraft.client.render.model.Baker; +import net.minecraft.client.render.model.ModelBakeSettings; import net.minecraft.client.texture.Sprite; import net.minecraft.client.util.SpriteIdentifier; import net.minecraft.screen.PlayerScreenHandler; @@ -17,35 +21,17 @@ import net.minecraft.util.math.Direction; import net.minecraft.util.math.random.Random; import org.jetbrains.annotations.Nullable; -import java.util.Collection; -import java.util.Collections; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.Function; -public class UnbakedJsonRetexturedModel implements UnbakedModel { - public UnbakedJsonRetexturedModel(Identifier parent) { - this.parent = parent; - } - - protected final Identifier parent; - protected BlockState itemModelState; - protected boolean ao = true; - - @Override - public Collection getModelDependencies() { - return Collections.singletonList(parent); - } - - @Override - public void setParents(Function function) { - function.apply(parent).setParents(function); +public class UnbakedJsonRetexturedModel extends UnbakedRetexturedModel { + public UnbakedJsonRetexturedModel(Identifier parent, Function state_getter) { + super(parent, state_getter); } @Nullable @Override - public BakedModel bake(Baker baker, Function spriteLookup, ModelBakeSettings modelBakeSettings, Identifier identifier) { + public BakedModel bake(Baker baker, Function spriteLookup, ModelBakeSettings bake_settings, Identifier identifier) { Direction[] DIRECTIONS = RetexturingBakedModel.DIRECTIONS; Sprite[] specialSprites = new Sprite[DIRECTIONS.length]; @@ -54,24 +40,15 @@ public class UnbakedJsonRetexturedModel implements UnbakedModel { specialSprites[i] = Objects.requireNonNull(spriteLookup.apply(id), () -> "Couldn't find sprite " + id + " !"); } - BakedModel model = baker.bake(parent, modelBakeSettings); - return new RetexturingBakedModel( - model, + baker.bake(parent, bake_settings), ReFramedClient.HELPER.getCamoApperanceManager(spriteLookup), - modelBakeSettings, - itemModelState, + state_getter, + bake_settings, + item_state, ao ) { - final ConcurrentMap jsonToMesh = new ConcurrentHashMap<>(); - - @Override - protected Mesh getBaseMesh(BlockState state) { - //Convert models to re-texturable Meshes lazily, the first time we encounter each blockstate - return jsonToMesh.computeIfAbsent(state, this::convertModel); - } - - private Mesh convertModel(BlockState state) { + protected Mesh convertModel(BlockState state) { Renderer r = ReFramedClient.HELPER.getFabricRenderer(); MeshBuilder builder = r.meshBuilder(); QuadEmitter emitter = builder.getEmitter(); diff --git a/src/main/java/fr/adrien1106/reframed/client/model/UnbakedRetexturedModel.java b/src/main/java/fr/adrien1106/reframed/client/model/UnbakedRetexturedModel.java new file mode 100644 index 0000000..7d88d8e --- /dev/null +++ b/src/main/java/fr/adrien1106/reframed/client/model/UnbakedRetexturedModel.java @@ -0,0 +1,34 @@ +package fr.adrien1106.reframed.client.model; + +import fr.adrien1106.reframed.util.ThemeableBlockEntity; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.model.UnbakedModel; +import net.minecraft.util.Identifier; + +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +public abstract class UnbakedRetexturedModel implements UnbakedModel { + + protected final Identifier parent; + protected final Function state_getter; + + protected BlockState item_state; + protected boolean ao = true; + + public UnbakedRetexturedModel(Identifier parent, Function state_getter) { + this.parent = parent; + this.state_getter = state_getter; + } + + @Override + public Collection getModelDependencies() { + return List.of(parent); + } + + @Override + public void setParents(Function function) { + function.apply(parent).setParents(function); + } +} diff --git a/src/main/java/fr/adrien1106/reframed/mixin/BlockMixin.java b/src/main/java/fr/adrien1106/reframed/mixin/BlockMixin.java index 7e5f4d1..65e848d 100644 --- a/src/main/java/fr/adrien1106/reframed/mixin/BlockMixin.java +++ b/src/main/java/fr/adrien1106/reframed/mixin/BlockMixin.java @@ -22,13 +22,13 @@ public class BlockMixin { private static boolean isNeighborCamoOpaque(BlockState state, @Local(argsOnly = true) BlockView world, @Local(ordinal = 1, argsOnly = true) BlockPos pos) { BlockEntity block_entity = world.getBlockEntity(pos); if (!(block_entity instanceof ThemeableBlockEntity frame_entity)) return state.isOpaque(); - return frame_entity.getThemeState().isOpaque(); + return frame_entity.getFirstTheme().isOpaque(); } @Redirect(method = "shouldDrawSide", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;isSideInvisible(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/Direction;)Z")) private static boolean isCamoInvisible(BlockState state, BlockState other_state, Direction direction, @Local(argsOnly = true) BlockView world, @Local(ordinal = 0, argsOnly = true) BlockPos pos, @Local(ordinal = 1, argsOnly = true) BlockPos other_pos) { - if (world.getBlockEntity(other_pos) instanceof ThemeableBlockEntity entity) other_state = entity.getThemeState(); - if (world.getBlockEntity(pos) instanceof ThemeableBlockEntity entity) state = entity.getThemeState(); + if (world.getBlockEntity(other_pos) instanceof ThemeableBlockEntity entity) other_state = entity.getFirstTheme(); + if (world.getBlockEntity(pos) instanceof ThemeableBlockEntity entity) state = entity.getFirstTheme(); return state.isSideInvisible(other_state, direction); } diff --git a/src/main/java/fr/adrien1106/reframed/mixin/BlockRenderInfoMixin.java b/src/main/java/fr/adrien1106/reframed/mixin/BlockRenderInfoMixin.java index cb1ffb4..254275a 100644 --- a/src/main/java/fr/adrien1106/reframed/mixin/BlockRenderInfoMixin.java +++ b/src/main/java/fr/adrien1106/reframed/mixin/BlockRenderInfoMixin.java @@ -35,7 +35,7 @@ public abstract class BlockRenderInfoMixin { public BlockState prepareCamoLayer(BlockState state, @Local(argsOnly = true) BlockPos pos) { BlockEntity block_entity = MinecraftClient.getInstance().world.getBlockEntity(pos); if (!(block_entity instanceof ThemeableBlockEntity frame_entity)) return state; - return frame_entity.getThemeState(); + return frame_entity.getFirstTheme(); } @Inject(method = "shouldDrawFace", diff --git a/src/main/java/fr/adrien1106/reframed/mixin/compat/AthenaWrappedGetterMixin.java b/src/main/java/fr/adrien1106/reframed/mixin/compat/AthenaWrappedGetterMixin.java index 63781de..6cbee5d 100644 --- a/src/main/java/fr/adrien1106/reframed/mixin/compat/AthenaWrappedGetterMixin.java +++ b/src/main/java/fr/adrien1106/reframed/mixin/compat/AthenaWrappedGetterMixin.java @@ -21,7 +21,7 @@ public class AthenaWrappedGetterMixin { @Inject(method = "getBlockState", at = @At(value = "HEAD"), cancellable = true) private void getCamoState(BlockPos pos, CallbackInfoReturnable cir) { if (!(getter.getBlockEntity(pos) instanceof ThemeableBlockEntity framed_entity)) return; - cir.setReturnValue(framed_entity.getThemeState()); + cir.setReturnValue(framed_entity.getFirstTheme()); } @Redirect(method = "getAppearance(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/math/Direction;)" + @@ -30,7 +30,7 @@ public class AthenaWrappedGetterMixin { "getBlockState(Lnet/minecraft/util/math/BlockPos;)" + "Lnet/minecraft/block/BlockState;")) private BlockState appearanceCamoState(BlockRenderView world, BlockPos pos) { - if (world.getBlockEntity(pos) instanceof ThemeableBlockEntity framed_entity) return framed_entity.getThemeState(); + if (world.getBlockEntity(pos) instanceof ThemeableBlockEntity framed_entity) return framed_entity.getFirstTheme(); return world.getBlockState(pos); } @@ -39,7 +39,7 @@ public class AthenaWrappedGetterMixin { "getBlockState(Lnet/minecraft/util/math/BlockPos;)" + "Lnet/minecraft/block/BlockState;")) private BlockState queryCamoState(BlockRenderView world, BlockPos pos) { - if (world.getBlockEntity(pos) instanceof ThemeableBlockEntity framed_entity) return framed_entity.getThemeState(); + if (world.getBlockEntity(pos) instanceof ThemeableBlockEntity framed_entity) return framed_entity.getFirstTheme(); return world.getBlockState(pos); } } diff --git a/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinBlockDustParticle.java b/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinBlockDustParticle.java index d541d1e..16c9018 100644 --- a/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinBlockDustParticle.java +++ b/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinBlockDustParticle.java @@ -21,7 +21,7 @@ public class MixinBlockDustParticle { void modifyParticleSprite(ClientWorld clientWorld, double d, double e, double f, double g, double h, double i, BlockState state, BlockPos pos, CallbackInfo ci) { AccessorParticle a = (AccessorParticle) this; if(a.getRandom().nextBoolean() && clientWorld.getBlockEntity(pos) instanceof ThemeableBlockEntity themeable) { - BlockState theme = themeable.getThemeState(); + BlockState theme = themeable.getFirstTheme(); if(theme == null || theme.isAir()) return; Sprite replacement = MinecraftClient.getInstance().getBlockRenderManager().getModels().getModelParticleSprite(theme); diff --git a/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinEntity.java b/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinEntity.java index 8e7e6ac..8235431 100644 --- a/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinEntity.java +++ b/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinEntity.java @@ -22,7 +22,7 @@ public abstract class MixinEntity { World world = ((Entity) (Object) this).getWorld(); if(world.getBlockEntity(getLandingPos()) instanceof ThemeableBlockEntity themeable) { - BlockState theme = themeable.getThemeState(); + BlockState theme = themeable.getFirstTheme(); if(!theme.isAir()) return theme; } diff --git a/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinLivingEntity.java b/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinLivingEntity.java index ec86a44..884bf81 100644 --- a/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinLivingEntity.java +++ b/src/main/java/fr/adrien1106/reframed/mixin/particles/MixinLivingEntity.java @@ -30,7 +30,7 @@ public class MixinLivingEntity { World world = ((Entity) (Object) this).getWorld(); if(lastFallCheckPos != null && world.getBlockEntity(lastFallCheckPos) instanceof ThemeableBlockEntity themeable) { - BlockState theme = themeable.getThemeState(); + BlockState theme = themeable.getFirstTheme(); if(!theme.isAir()) return theme; } diff --git a/src/main/java/fr/adrien1106/reframed/util/ReFramedInteractionUtil.java b/src/main/java/fr/adrien1106/reframed/util/ReFramedInteractionUtil.java index 14d3f9a..f334fd0 100644 --- a/src/main/java/fr/adrien1106/reframed/util/ReFramedInteractionUtil.java +++ b/src/main/java/fr/adrien1106/reframed/util/ReFramedInteractionUtil.java @@ -1,19 +1,10 @@ package fr.adrien1106.reframed.util; import fr.adrien1106.reframed.block.ReFramedEntity; -import net.minecraft.block.AbstractBlock; -import net.minecraft.block.Block; -import net.minecraft.block.BlockEntityProvider; -import net.minecraft.block.BlockState; -import net.minecraft.block.Blocks; -import net.minecraft.block.ShapeContext; +import net.minecraft.block.*; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.item.BlockItem; -import net.minecraft.item.ItemPlacementContext; -import net.minecraft.item.ItemStack; -import net.minecraft.item.ItemUsageContext; -import net.minecraft.item.Items; +import net.minecraft.item.*; import net.minecraft.nbt.NbtCompound; import net.minecraft.sound.BlockSoundGroup; import net.minecraft.sound.SoundCategory; @@ -65,60 +56,66 @@ public class ReFramedInteractionUtil { } public static ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { - if(!(world.getBlockEntity(pos) instanceof ReFramedEntity be)) return ActionResult.PASS; + if(!(world.getBlockEntity(pos) instanceof ReFramedEntity block_entity)) return ActionResult.PASS; if(!player.canModifyBlocks() || !world.canPlayerModifyAt(player, pos)) return ActionResult.PASS; ItemStack held = player.getStackInHand(hand); ReframedInteractible ext = state.getBlock() instanceof ReframedInteractible e ? e : ReframedInteractible.Default.INSTANCE; - //Glowstone - if(state.contains(LIGHT) && held.getItem() == Items.GLOWSTONE_DUST && !state.get(LIGHT) && !be.hasSpentGlowstoneDust()) { - world.setBlockState(pos, state.with(LIGHT, true)); - be.spentGlowstoneDust(); + // frame will emit light if applied with glowstone + if(state.contains(LIGHT) && held.getItem() == Items.GLOWSTONE_DUST) { + block_entity.toggleLight(); + world.setBlockState(pos, state.with(LIGHT, block_entity.emitsLight())); - if(!player.isCreative()) held.decrement(1); + if(!player.isCreative()) + if (block_entity.emitsLight()) held.decrement(1); + else held.increment(1); world.playSound(player, pos, SoundEvents.BLOCK_GLASS_HIT, SoundCategory.BLOCKS, 1f, 1f); return ActionResult.SUCCESS; } - //Redstone - if(held.getItem() == Blocks.REDSTONE_TORCH.asItem() && - !be.emitsRedstone() && - !be.hasSpentRedstoneTorch() && - ext.canAddRedstoneEmission(state, world, pos) - ) { - be.setEmitsRedstone(true); - be.spentRedstoneTorch(); + // frame will emit redstone if applied with redstone torch can deactivate redstone block camo emission + if(held.getItem() == Items.REDSTONE_TORCH && ext.canAddRedstoneEmission(state, world, pos)) { + block_entity.toggleRedstone(); - if(!player.isCreative()) held.decrement(1); + if(!player.isCreative()) + if (block_entity.emitsRedstone()) held.decrement(1); + else held.increment(1); world.playSound(player, pos, SoundEvents.BLOCK_LEVER_CLICK, SoundCategory.BLOCKS, 1f, 1f); return ActionResult.SUCCESS; } - //Popped chorus fruit - if(held.getItem() == Items.POPPED_CHORUS_FRUIT && - be.isSolid() && - !be.hasSpentPoppedChorus() && - ext.canRemoveCollision(state, world, pos) - ) { - be.setSolidity(false); - be.spentPoppedChorus(); + // Frame will lose its collision if applied with popped chorus fruit + if(held.getItem() == Items.POPPED_CHORUS_FRUIT && ext.canRemoveCollision(state, world, pos)) { + block_entity.toggleSolidity(); - if(!player.isCreative()) held.decrement(1); + if(!player.isCreative()) + if (!block_entity.isSolid()) held.decrement(1); + else held.increment(1); world.playSound(player, pos, SoundEvents.ITEM_CHORUS_FRUIT_TELEPORT, SoundCategory.BLOCKS, 1f, 1f); return ActionResult.SUCCESS; } - //Changing the theme - if(held.getItem() instanceof BlockItem bi && be.getThemeState().getBlock() == Blocks.AIR) { - Block block = bi.getBlock(); + // Changing the theme TODO Move outside + if(held.getItem() instanceof BlockItem block_item && block_entity.getFirstTheme().getBlock() == Blocks.AIR) { + Block block = block_item.getBlock(); ItemPlacementContext ctx = new ItemPlacementContext(new ItemUsageContext(player, hand, hit)); BlockState placementState = block.getPlacementState(ctx); if(placementState != null && Block.isShapeFullCube(placementState.getCollisionShape(world, pos)) && !(block instanceof BlockEntityProvider)) { - if(!world.isClient) be.setRenderedState(placementState); - - world.setBlockState(pos, state.with(LIGHT, be.hasSpentGlowstoneDust() || (placementState.getLuminance() != 0))); - be.setEmitsRedstone(be.hasSpentRedstoneTorch() || placementState.getWeakRedstonePower(world, pos, Direction.NORTH) != 0); + // TODO FOR SECOND + if(!world.isClient) block_entity.setFirstTheme(placementState); + + // check for default light emission + if (placementState.getLuminance() > 0) + if (block_entity.emitsLight()) Block.dropStack(world, pos, new ItemStack(Items.GLOWSTONE_DUST)); + else block_entity.toggleLight(); + + world.setBlockState(pos, state.with(LIGHT, block_entity.emitsLight())); + + // check for redstone emission + if (placementState.getWeakRedstonePower(world, pos, Direction.NORTH) > 0) + if (block_entity.emitsRedstone()) Block.dropStack(world, pos, new ItemStack(Items.GLOWSTONE_DUST)); + else block_entity.toggleRedstone(); if(!player.isCreative()) held.decrement(1); world.playSound(player, pos, placementState.getSoundGroup().getPlaceSound(), SoundCategory.BLOCKS, 1f, 1.1f); @@ -128,22 +125,23 @@ public class ReFramedInteractionUtil { return ActionResult.PASS; } - - //Maybe an odd spot to put this logic but it's consistent w/ vanilla chests, barrels, etc + public static void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { if(!state.isOf(newState.getBlock()) && - world.getBlockEntity(pos) instanceof ReFramedEntity frame && + world.getBlockEntity(pos) instanceof ReFramedEntity frame_entity && world.getGameRules().getBoolean(GameRules.DO_TILE_DROPS) ) { DefaultedList drops = DefaultedList.of(); + + BlockState theme = frame_entity.getFirstTheme(); + if(theme.getBlock() != Blocks.AIR) drops.add(new ItemStack(theme.getBlock())); - //TODO: remember the specific ItemStack - Block theme = frame.getThemeState().getBlock(); - if(theme != Blocks.AIR) drops.add(new ItemStack(theme)); - - if(frame.hasSpentRedstoneTorch()) drops.add(new ItemStack(Items.REDSTONE_TORCH)); - if(frame.hasSpentGlowstoneDust()) drops.add(new ItemStack(Items.GLOWSTONE_DUST)); - if(frame.hasSpentPoppedChorus()) drops.add(new ItemStack(Items.POPPED_CHORUS_FRUIT)); + if(frame_entity.emitsRedstone() && theme.getWeakRedstonePower(world, pos, Direction.NORTH) == 0) + drops.add(new ItemStack(Items.REDSTONE_TORCH)); + if(frame_entity.emitsLight() && theme.getLuminance() == 0) + drops.add(new ItemStack(Items.GLOWSTONE_DUST)); + if(!frame_entity.isSolid() && theme.isSolid()) + drops.add(new ItemStack(Items.POPPED_CHORUS_FRUIT)); ItemScatterer.spawn(world, pos, drops); } diff --git a/src/main/java/fr/adrien1106/reframed/util/ThemeableBlockEntity.java b/src/main/java/fr/adrien1106/reframed/util/ThemeableBlockEntity.java index 92e76e1..d84542b 100644 --- a/src/main/java/fr/adrien1106/reframed/util/ThemeableBlockEntity.java +++ b/src/main/java/fr/adrien1106/reframed/util/ThemeableBlockEntity.java @@ -1,10 +1,17 @@ package fr.adrien1106.reframed.util; -import net.fabricmc.fabric.api.rendering.data.v1.RenderAttachmentBlockEntity; import net.minecraft.block.BlockState; -public interface ThemeableBlockEntity extends RenderAttachmentBlockEntity { - default BlockState getThemeState() { - return (BlockState) getRenderAttachmentData(); +public interface ThemeableBlockEntity { + BlockState getFirstTheme(); + + default BlockState getSecondTheme() { + return getFirstTheme(); + } + + void setFirstTheme(BlockState state); + + default void setSecondTheme(BlockState state) { + setFirstTheme(state); } }