diff --git a/_tmp_sdd_yarn_1 b/_tmp_sdd_yarn_1 new file mode 160000 index 0000000..49f5c0d --- /dev/null +++ b/_tmp_sdd_yarn_1 @@ -0,0 +1 @@ +Subproject commit 49f5c0d13935b5e0f3ba86bf6f3f8561888ab5bb diff --git a/aa.png b/aa.png new file mode 100644 index 0000000..050ac2d Binary files /dev/null and b/aa.png differ diff --git a/aaaa.b b/aaaa.b new file mode 100644 index 0000000..e69de29 diff --git a/src/client/java/com/straice/TemplateModClient.java b/src/client/java/com/straice/TemplateModClient.java index 25d31e2..e92f05b 100644 --- a/src/client/java/com/straice/TemplateModClient.java +++ b/src/client/java/com/straice/TemplateModClient.java @@ -1,10 +1,11 @@ package com.straice; +import com.straice.smoothdoors.client.anim.SddAnimator; import net.fabricmc.api.ClientModInitializer; public class TemplateModClient implements ClientModInitializer { @Override public void onInitializeClient() { - // This entrypoint is suitable for setting up client-specific logic, such as rendering. + SddAnimator.initClientHooks(); } -} \ No newline at end of file +} diff --git a/src/client/java/com/straice/mixin/client/BlockModelRendererMixin.java b/src/client/java/com/straice/mixin/client/BlockModelRendererMixin.java new file mode 100644 index 0000000..f6024c4 --- /dev/null +++ b/src/client/java/com/straice/mixin/client/BlockModelRendererMixin.java @@ -0,0 +1,61 @@ +package com.straice.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.block.ModelBlockRenderer; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.util.RandomSource; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ModelBlockRenderer.class) +public class BlockModelRendererMixin { + + @Inject( + method = "tesselateBlock", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$tesselateBlock(BlockAndTintGetter world, BakedModel model, BlockState state, BlockPos pos, + PoseStack matrices, VertexConsumer vertices, boolean cull, RandomSource random, + long seed, int overlay, CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } + + @Inject( + method = "tesselateWithAO", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$tesselateWithAo(BlockAndTintGetter world, BakedModel model, BlockState state, BlockPos pos, + PoseStack matrices, VertexConsumer vertices, boolean cull, RandomSource random, + long seed, int overlay, CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } + + @Inject( + method = "tesselateWithoutAO", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$tesselateWithoutAo(BlockAndTintGetter world, BakedModel model, BlockState state, BlockPos pos, + PoseStack matrices, VertexConsumer vertices, boolean cull, RandomSource random, + long seed, int overlay, CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } +} diff --git a/src/client/java/com/straice/mixin/client/BlockRenderManagerMixin.java b/src/client/java/com/straice/mixin/client/BlockRenderManagerMixin.java new file mode 100644 index 0000000..167e5c7 --- /dev/null +++ b/src/client/java/com/straice/mixin/client/BlockRenderManagerMixin.java @@ -0,0 +1,33 @@ +package com.straice.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BlockRenderDispatcher.class) +public class BlockRenderManagerMixin { + + @Inject( + method = "renderBatched", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$renderBatched(BlockState state, BlockPos pos, BlockAndTintGetter world, + PoseStack matrices, VertexConsumer vertexConsumer, + boolean cull, RandomSource random, + CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } +} diff --git a/src/client/java/com/straice/mixin/client/ChunkRendererRegionMixin.java b/src/client/java/com/straice/mixin/client/ChunkRendererRegionMixin.java new file mode 100644 index 0000000..3c800d2 --- /dev/null +++ b/src/client/java/com/straice/mixin/client/ChunkRendererRegionMixin.java @@ -0,0 +1,23 @@ +package com.straice.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import net.minecraft.client.renderer.chunk.RenderChunkRegion; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(RenderChunkRegion.class) +public class ChunkRendererRegionMixin { + + @Inject(method = "getBlockState", at = @At("HEAD"), cancellable = true) + private void sdd$getBlockState(BlockPos pos, CallbackInfoReturnable cir) { + if (SddAnimator.isAnimatingAt(pos)) { + cir.setReturnValue(Blocks.AIR.defaultBlockState()); + cir.cancel(); + } + } +} diff --git a/src/client/java/com/straice/mixin/client/ClientWorldMixin.java b/src/client/java/com/straice/mixin/client/ClientWorldMixin.java new file mode 100644 index 0000000..285f51c --- /dev/null +++ b/src/client/java/com/straice/mixin/client/ClientWorldMixin.java @@ -0,0 +1,19 @@ +package com.straice.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientLevel.class) +public class ClientWorldMixin { + + @Inject(method = "sendBlockUpdated", at = @At("TAIL")) + private void sdd$updateListeners(BlockPos pos, BlockState oldState, BlockState newState, int flags, CallbackInfo ci) { + SddAnimator.onBlockUpdate(pos, oldState, newState); + } +} diff --git a/src/client/java/com/straice/mixin/client/ExampleClientMixin.java b/src/client/java/com/straice/mixin/client/ExampleClientMixin.java index 2c88d92..d087552 100644 --- a/src/client/java/com/straice/mixin/client/ExampleClientMixin.java +++ b/src/client/java/com/straice/mixin/client/ExampleClientMixin.java @@ -12,4 +12,4 @@ public class ExampleClientMixin { private void init(CallbackInfo info) { // This code is injected into the start of Minecraft.run()V } -} \ No newline at end of file +} diff --git a/src/client/java/com/straice/smoothdoors/client/anim/DoorAnimation.java b/src/client/java/com/straice/smoothdoors/client/anim/DoorAnimation.java new file mode 100644 index 0000000..270b5cd --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/client/anim/DoorAnimation.java @@ -0,0 +1,78 @@ +package com.straice.smoothdoors.client.anim; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.DoorHingeSide; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.CollisionContext; + +final class DoorAnimation { + + private DoorAnimation() {} + + static void apply(BlockState state, float angleDeg, PoseStack matrices, BlockGetter world) { + if (world == null) return; + + Direction facing = state.getValue(DoorBlock.FACING); + DoorHingeSide hinge = state.getValue(DoorBlock.HINGE); + + Direction hingeSide = (hinge == DoorHingeSide.RIGHT) + ? facing.getClockWise() + : facing.getCounterClockWise(); + + AABB bb = state.getShape(world, BlockPos.ZERO, CollisionContext.empty()).bounds(); + + float cx = (float) ((bb.minX + bb.maxX) * 0.5); + float cz = (float) ((bb.minZ + bb.maxZ) * 0.5); + + float pivotX = cx; + float pivotZ = cz; + + switch (hingeSide) { + case EAST -> pivotX = (float) bb.maxX; + case WEST -> pivotX = (float) bb.minX; + case SOUTH -> pivotZ = (float) bb.maxZ; + case NORTH -> pivotZ = (float) bb.minZ; + default -> { + pivotX = cx; + pivotZ = cz; + } + } + + float thickness = (facing == Direction.NORTH || facing == Direction.SOUTH) + ? (float) (bb.maxZ - bb.minZ) + : (float) (bb.maxX - bb.minX); + float halfT = thickness * 0.5f; + + float dX = 0.0f; + float dZ = 0.0f; + switch (facing) { + case NORTH -> dZ = halfT; + case SOUTH -> dZ = -halfT; + case WEST -> dX = halfT; + case EAST -> dX = -halfT; + default -> { + dX = 0.0f; + dZ = 0.0f; + } + } + + double rad = Math.toRadians(angleDeg); + float cos = (float) Math.cos(rad); + float sin = (float) Math.sin(rad); + float rotDX = dX * cos - dZ * sin; + float rotDZ = dX * sin + dZ * cos; + float shiftX = dX - rotDX; + float shiftZ = dZ - rotDZ; + + matrices.translate(shiftX, 0.0f, shiftZ); + matrices.translate(pivotX, 0.0f, pivotZ); + matrices.mulPose(Axis.YP.rotationDegrees(angleDeg)); + matrices.translate(-pivotX, 0.0f, -pivotZ); + } +} diff --git a/src/client/java/com/straice/smoothdoors/client/anim/FenceGateAnimation.java b/src/client/java/com/straice/smoothdoors/client/anim/FenceGateAnimation.java new file mode 100644 index 0000000..65eb0c3 --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/client/anim/FenceGateAnimation.java @@ -0,0 +1,374 @@ +package com.straice.smoothdoors.client.anim; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ItemBlockRenderTypes; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.block.ModelBlockRenderer; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.FenceGateBlock; +import net.minecraft.world.level.block.state.BlockState; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; + +final class FenceGateAnimation { + + private static final float POST_EDGE = 2.0f / 16.0f; + private static final float EDGE_EPS = 1.0e-4f; + + private static final Method RENDER_TYPE_ONE_ARG; + private static final Method RENDER_TYPE_TWO_ARG; + private static final Method QUAD_LIGHT_EMISSION; + private static final Constructor QUAD_CTOR_5; + private static final Constructor QUAD_CTOR_6; + + static { + Method oneArg = null; + Method twoArg = null; + Method lightEmission = null; + Constructor ctor5 = null; + Constructor ctor6 = null; + try { + oneArg = ItemBlockRenderTypes.class.getMethod("getRenderType", BlockState.class); + } catch (Throwable ignored) { + // Older API. + } + try { + twoArg = ItemBlockRenderTypes.class.getMethod("getRenderType", BlockState.class, boolean.class); + } catch (Throwable ignored) { + // Newer API. + } + if (oneArg == null) { + oneArg = findRenderTypeMethod(BlockState.class); + } + if (twoArg == null) { + twoArg = findRenderTypeMethod(BlockState.class, boolean.class); + } + try { + lightEmission = BakedQuad.class.getMethod("getLightEmission"); + } catch (Throwable ignored) { + // Older quad API. + } + try { + ctor5 = BakedQuad.class.getConstructor(int[].class, int.class, Direction.class, TextureAtlasSprite.class, boolean.class); + } catch (Throwable ignored) { + // Newer quad API. + } + try { + ctor6 = BakedQuad.class.getConstructor(int[].class, int.class, Direction.class, TextureAtlasSprite.class, boolean.class, int.class); + } catch (Throwable ignored) { + // Older quad API. + } + RENDER_TYPE_ONE_ARG = oneArg; + RENDER_TYPE_TWO_ARG = twoArg; + QUAD_LIGHT_EMISSION = lightEmission; + QUAD_CTOR_5 = ctor5; + QUAD_CTOR_6 = ctor6; + } + + private FenceGateAnimation() {} + + static void render(BlockState state, float angleDeg, PoseStack matrices, + MultiBufferSource consumers, int light, int overlay) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return; + + Direction facing = state.getValue(FenceGateBlock.FACING); + boolean leftRightIsX = facing.getAxis() == Direction.Axis.Z; + + BakedModel baseModel = mc.getBlockRenderer().getBlockModel(state); + FenceGateModels models = splitFenceGateModels(state, baseModel, leftRightIsX); + if (models.isEmpty()) return; + + int tint = mc.getBlockColors().getColor(state, null, null, 0); + float r = ((tint >> 16) & 0xFF) / 255.0f; + float g = ((tint >> 8) & 0xFF) / 255.0f; + float b = (tint & 0xFF) / 255.0f; + + RenderType renderType = getRenderTypeCompat(state); + VertexConsumer consumer = consumers.getBuffer(renderType); + ModelBlockRenderer renderer = mc.getBlockRenderer().getModelRenderer(); + + if (models.posts != null) { + renderer.renderModel(matrices.last(), consumer, state, models.posts, r, g, b, light, overlay); + } + + float leftPivotX = leftRightIsX ? (1.0f / 16.0f) : 0.5f; + float leftPivotZ = leftRightIsX ? 0.5f : (1.0f / 16.0f); + float rightPivotX = leftRightIsX ? (15.0f / 16.0f) : 0.5f; + float rightPivotZ = leftRightIsX ? 0.5f : (15.0f / 16.0f); + + float leftAngle = -angleDeg; + float rightAngle = angleDeg; + + if (models.left != null) { + matrices.pushPose(); + matrices.translate(leftPivotX, 0.0f, leftPivotZ); + matrices.mulPose(Axis.YP.rotationDegrees(leftAngle)); + matrices.translate(-leftPivotX, 0.0f, -leftPivotZ); + renderer.renderModel(matrices.last(), consumer, state, models.left, r, g, b, light, overlay); + matrices.popPose(); + } + + if (models.right != null) { + matrices.pushPose(); + matrices.translate(rightPivotX, 0.0f, rightPivotZ); + matrices.mulPose(Axis.YP.rotationDegrees(rightAngle)); + matrices.translate(-rightPivotX, 0.0f, -rightPivotZ); + renderer.renderModel(matrices.last(), consumer, state, models.right, r, g, b, light, overlay); + matrices.popPose(); + } + } + + private static FenceGateModels splitFenceGateModels(BlockState state, BakedModel model, boolean leftRightIsX) { + List posts = new ArrayList<>(); + List left = new ArrayList<>(); + List right = new ArrayList<>(); + + for (Direction dir : Direction.values()) { + addFenceGateQuads(state, model, dir, leftRightIsX, posts, left, right); + } + addFenceGateQuads(state, model, null, leftRightIsX, posts, left, right); + + return new FenceGateModels( + posts.isEmpty() ? null : buildStaticModel(model, buildFaces(posts), model.useAmbientOcclusion(), model.getParticleIcon()), + left.isEmpty() ? null : buildStaticModel(model, buildFaces(left), model.useAmbientOcclusion(), model.getParticleIcon()), + right.isEmpty() ? null : buildStaticModel(model, buildFaces(right), model.useAmbientOcclusion(), model.getParticleIcon()) + ); + } + + private static FaceBuckets buildFaces(List quads) { + EnumMap> faces = new EnumMap<>(Direction.class); + List unculled = new ArrayList<>(); + + for (BakedQuad quad : quads) { + Direction face = quad.getDirection(); + if (face == null) { + unculled.add(quad); + } else { + faces.computeIfAbsent(face, k -> new ArrayList<>()).add(quad); + } + } + + return new FaceBuckets(faces, unculled); + } + + private static void addFenceGateQuads(BlockState state, BakedModel model, Direction dir, boolean leftRightIsX, + List posts, List left, List right) { + RandomSource random = RandomSource.create(42L); + List quads = model.getQuads(state, dir, random); + if (quads.isEmpty()) return; + + for (BakedQuad quad : quads) { + FenceGateSection section = classifyFenceGateQuad(quad, leftRightIsX); + switch (section) { + case POSTS -> posts.add(quad); + case LEFT -> { + left.add(quad); + left.add(reverseQuad(quad)); + } + case RIGHT -> { + right.add(quad); + right.add(reverseQuad(quad)); + } + } + } + } + + private static FenceGateSection classifyFenceGateQuad(BakedQuad quad, boolean leftRightIsX) { + int[] data = quad.getVertices(); + if (data.length < 8) return FenceGateSection.LEFT; + + int stride = data.length / 4; + float min = Float.POSITIVE_INFINITY; + float max = Float.NEGATIVE_INFINITY; + int coordOffset = leftRightIsX ? 0 : 2; + + for (int i = 0; i < 4; i++) { + int base = i * stride + coordOffset; + float coord = Float.intBitsToFloat(data[base]); + min = Math.min(min, coord); + max = Math.max(max, coord); + } + + float scale = (max > 1.001f) ? 16.0f : 1.0f; + float postEdge = POST_EDGE * scale; + float edgeEps = EDGE_EPS * scale; + float one = 1.0f * scale; + + if (max <= postEdge + edgeEps || min >= (one - postEdge) - edgeEps) { + return FenceGateSection.POSTS; + } + + float center = (min + max) * 0.5f; + return center <= (0.5f * scale) ? FenceGateSection.LEFT : FenceGateSection.RIGHT; + } + + private enum FenceGateSection { POSTS, LEFT, RIGHT } + + private static BakedQuad reverseQuad(BakedQuad quad) { + int[] data = quad.getVertices(); + int stride = data.length / 4; + int[] flipped = new int[data.length]; + + for (int v = 0; v < 4; v++) { + int src = (3 - v) * stride; + int dst = v * stride; + System.arraycopy(data, src, flipped, dst, stride); + } + + Direction face = quad.getDirection(); + Direction opposite = (face == null) ? null : face.getOpposite(); + return newQuadCompat(flipped, quad.getTintIndex(), opposite, quad.getSprite(), quad.isShade(), quad); + } + + private static RenderType getRenderTypeCompat(BlockState state) { + try { + if (RENDER_TYPE_TWO_ARG != null) { + return (RenderType) RENDER_TYPE_TWO_ARG.invoke(null, state, true); + } + if (RENDER_TYPE_ONE_ARG != null) { + return (RenderType) RENDER_TYPE_ONE_ARG.invoke(null, state); + } + } catch (Throwable e) { + throw new IllegalStateException("Failed to resolve ItemBlockRenderTypes#getRenderType", e); + } + throw new IllegalStateException("No compatible ItemBlockRenderTypes#getRenderType found"); + } + + private static Method findRenderTypeMethod(Class... params) { + for (Method method : ItemBlockRenderTypes.class.getDeclaredMethods()) { + if (!Modifier.isStatic(method.getModifiers())) continue; + if (!RenderType.class.isAssignableFrom(method.getReturnType())) continue; + Class[] types = method.getParameterTypes(); + if (types.length != params.length) continue; + boolean match = true; + for (int i = 0; i < types.length; i++) { + if (types[i] != params[i]) { + match = false; + break; + } + } + if (match) { + method.setAccessible(true); + return method; + } + } + return null; + } + + private static final class FenceGateModels { + final BakedModel posts; + final BakedModel left; + final BakedModel right; + + FenceGateModels(BakedModel posts, BakedModel left, BakedModel right) { + this.posts = posts; + this.left = left; + this.right = right; + } + + boolean isEmpty() { + return posts == null && left == null && right == null; + } + } + + private static BakedModel buildStaticModel(BakedModel base, FaceBuckets buckets, + boolean useAo, TextureAtlasSprite particleIcon) { + return (BakedModel) Proxy.newProxyInstance( + BakedModel.class.getClassLoader(), + new Class[]{BakedModel.class}, + (proxy, method, args) -> { + String name = method.getName(); + switch (name) { + case "getQuads" -> { + Direction face = (Direction) args[1]; + if (face == null) return buckets.unculled; + List quads = buckets.faces.get(face); + return (quads == null) ? List.of() : quads; + } + case "useAmbientOcclusion" -> { + return useAo; + } + case "isGui3d" -> { + return base.isGui3d(); + } + case "usesBlockLight" -> { + return base.usesBlockLight(); + } + case "isCustomRenderer" -> { + return invokeBoolean(base, "isCustomRenderer", false); + } + case "getParticleIcon" -> { + return particleIcon; + } + case "getTransforms" -> { + return base.getTransforms(); + } + case "getOverrides", "overrides" -> { + return method.invoke(base, args); + } + default -> { + return method.invoke(base, args); + } + } + } + ); + } + + private static boolean invokeBoolean(Object target, String methodName, boolean fallback) { + try { + Method method = target.getClass().getMethod(methodName); + Object result = method.invoke(target); + return (result instanceof Boolean) ? (Boolean) result : fallback; + } catch (Throwable ignored) { + return fallback; + } + } + + private static final class FaceBuckets { + final EnumMap> faces; + final List unculled; + + FaceBuckets(EnumMap> faces, List unculled) { + this.faces = faces; + this.unculled = unculled; + } + } + + private static BakedQuad newQuadCompat(int[] vertices, int tintIndex, Direction face, + TextureAtlasSprite sprite, boolean shade, BakedQuad source) { + int lightEmission = 0; + if (QUAD_LIGHT_EMISSION != null) { + try { + lightEmission = (int) QUAD_LIGHT_EMISSION.invoke(source); + } catch (Throwable ignored) { + lightEmission = 0; + } + } + try { + if (QUAD_CTOR_6 != null) { + return QUAD_CTOR_6.newInstance(vertices, tintIndex, face, sprite, shade, lightEmission); + } + if (QUAD_CTOR_5 != null) { + return QUAD_CTOR_5.newInstance(vertices, tintIndex, face, sprite, shade); + } + } catch (Throwable e) { + throw new IllegalStateException("Failed to construct BakedQuad", e); + } + throw new IllegalStateException("No compatible BakedQuad constructor found"); + } +} diff --git a/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java b/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java new file mode 100644 index 0000000..65f4605 --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java @@ -0,0 +1,469 @@ +package com.straice.smoothdoors.client.anim; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.straice.smoothdoors.config.SddConfig; +import com.straice.smoothdoors.config.SddConfigManager; +import com.straice.smoothdoors.util.ConnectedUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.FenceGateBlock; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.DoorHingeSide; +import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; +import net.minecraft.world.phys.Vec3; +import java.lang.reflect.Field; + +public final class SddAnimator { + + private static final Minecraft MC = Minecraft.getInstance(); + + // Base duration (seconds) at speed = 1.0x + private static final float BASE_DURATION_S = 0.35f; + private static final double END_GRACE_TICKS = 2.0; + + private static final Long2ObjectOpenHashMap ANIMS = new Long2ObjectOpenHashMap<>(); + private static boolean hooksInit = false; + + private SddAnimator() {} + + /** Llamar desde SmoothDoubleDoorsClient#onInitializeClient(). */ + public static void initClientHooks() { + if (hooksInit) return; + hooksInit = true; + WorldRenderEvents.AFTER_ENTITIES.register(context -> { + if (MC.level == null || ANIMS.isEmpty()) return; + float tickDelta = MC.getFrameTime(); + PoseStack matrices = context.matrixStack(); + MultiBufferSource consumers = context.consumers(); + if (matrices == null || consumers == null) return; + Vec3 camPos = context.camera().getPosition(); + renderAll(tickDelta, matrices, consumers, camPos); + }); + } + + // === API usada por mixins === + + /** Oculta el bloque vanilla mientras animamos (evita doble puerta). */ + public static boolean shouldHideInChunk(BlockPos pos, BlockState state) { + synchronized (ANIMS) { + if (isHiddenByAnim(pos)) return true; + + if (state.getBlock() instanceof DoorBlock && state.hasProperty(DoorBlock.HALF)) { + DoubleBlockHalf half = state.getValue(DoorBlock.HALF); + BlockPos other = (half == DoubleBlockHalf.LOWER) ? pos.above() : pos.below(); + return isHiddenByAnim(other); + } + + return false; + } + } + + public static boolean isAnimatingAt(BlockPos pos) { + synchronized (ANIMS) { + return isHiddenByAnim(pos); + } + } + + /** Se llama cuando cambia el bloque (abrir/cerrar). */ + public static void onBlockUpdate(BlockPos pos, BlockState oldState, BlockState newState) { + if (MC.level == null) return; + + Kind kind = kindOf(oldState, newState); + if (kind == null) { + synchronized (ANIMS) { + ANIMS.remove(pos.asLong()); + } + return; + } + + if (!isKindBlock(kind, newState)) { + removeKindAt(pos, kind); + return; + } + + // DOOR: si llega update de la mitad UPPER, arrancamos desde abajo usando el estado superior. + if (kind == Kind.DOOR && newState.hasProperty(DoorBlock.HALF)) { + if (newState.getValue(DoorBlock.HALF) == DoubleBlockHalf.UPPER) { + BlockPos lowerPos = pos.below(); + if (isAnimatingAt(lowerPos) || isAnimatingAt(pos)) return; + + boolean oldOpen = isOpen(oldState); + boolean newOpen = isOpen(newState); + if (oldOpen == newOpen) return; + + SddConfig cfg = SddConfigManager.get(); + if (!isAnimationEnabled(cfg, kind)) { + removeKindAt(pos, kind); + return; + } + + float speed = speedFor(cfg, kind); + if (speed <= 0.001f) speed = 1.0f; + + long startTick = MC.level.getGameTime(); + BlockState lowerNew = MC.level.getBlockState(lowerPos); + if (!lowerNew.hasProperty(DoorBlock.HALF)) { + lowerNew = newState; + } + if (lowerNew.hasProperty(DoorBlock.OPEN)) { + lowerNew = lowerNew.setValue(DoorBlock.OPEN, newOpen); + } + lowerNew = lowerNew.setValue(DoorBlock.HALF, DoubleBlockHalf.LOWER); + startDoorAnimBothHalves(lowerPos, lowerNew, oldOpen, newOpen, speed, startTick); + startPairedDoorAnimIfAny(lowerPos, lowerNew, oldOpen, newOpen, speed, startTick, cfg); + return; + } + } + + boolean oldOpen = isOpen(oldState); + boolean newOpen = isOpen(newState); + if (oldOpen == newOpen) return; + + SddConfig cfg = SddConfigManager.get(); + if (!isAnimationEnabled(cfg, kind)) { + removeKindAt(pos, kind); + return; + } + + float speed = speedFor(cfg, kind); + if (speed <= 0.001f) speed = 1.0f; + + long startTick = MC.level.getGameTime(); + + if (kind == Kind.DOOR) { + if (isAnimatingAt(pos) || isAnimatingAt(pos.above())) return; + startDoorAnimBothHalves(pos, newState, oldOpen, newOpen, speed, startTick); + startPairedDoorAnimIfAny(pos, newState, oldOpen, newOpen, speed, startTick, cfg); + } else { + BlockState base = forceClosedModel(newState, kind); + synchronized (ANIMS) { + ANIMS.put(pos.asLong(), new Anim(pos, base, kind, oldOpen, newOpen, speed, startTick)); + } + requestRerender(pos); + } + } + + // === Render === + + private static void renderAll(float tickDelta, PoseStack matrices, MultiBufferSource consumers, Vec3 camPos) { + if (MC.level == null || ANIMS.isEmpty()) return; + + long worldTick = MC.level.getGameTime(); + double nowTick = worldTick + (double) Mth.clamp(tickDelta, 0.0f, 1.0f); + + Anim[] snapshot; + synchronized (ANIMS) { + snapshot = ANIMS.values().toArray(new Anim[0]); + } + for (Anim a : snapshot) { + float t = a.progress01(nowTick); + renderOne(a, t, camPos, matrices, consumers); + } + cleanupFinished(nowTick); + } + + private static void cleanupFinished(double nowTick) { + synchronized (ANIMS) { + var it = ANIMS.long2ObjectEntrySet().iterator(); + while (it.hasNext()) { + var entry = it.next(); + Anim anim = entry.getValue(); + if (anim.hasEnded(nowTick) && anim.hideVanilla) { + anim.hideVanilla = false; + requestRerender(anim.pos); + } + if (anim.isFinished(nowTick)) { + it.remove(); + requestRerender(anim.pos); + } + } + } + } + + private static void startPairedDoorAnimIfAny(BlockPos lowerPos, BlockState lowerState, + boolean oldOpen, boolean newOpen, + float speed, long startTick, SddConfig cfg) { + if (MC.level == null) return; + if (!cfg.connectDoors) return; + + BlockPos lower = ConnectedUtil.getDoorLowerPos(lowerPos, lowerState); + BlockState lowerNow = MC.level.getBlockState(lower); + if (!(lowerNow.getBlock() instanceof DoorBlock)) return; + + BlockPos mateLower = ConnectedUtil.findPairedDoorLower(MC.level, lower, lowerNow); + if (mateLower == null) return; + if (isHiddenByAnim(mateLower) || isHiddenByAnim(mateLower.above())) return; + + BlockState mateLowerState = MC.level.getBlockState(mateLower); + if (!(mateLowerState.getBlock() instanceof DoorBlock)) return; + if (mateLowerState.hasProperty(DoorBlock.OPEN)) { + mateLowerState = mateLowerState.setValue(DoorBlock.OPEN, newOpen); + } + mateLowerState = mateLowerState.setValue(DoorBlock.HALF, DoubleBlockHalf.LOWER); + + startDoorAnimBothHalves(mateLower, mateLowerState, oldOpen, newOpen, speed, startTick); + } + + private static void renderOne(Anim anim, float t, Vec3 camPos, PoseStack matrices, + MultiBufferSource consumers) { + if (MC.level == null) return; + + BlockPos pos = anim.pos; + BlockState state = anim.baseState; + + matrices.pushPose(); + + // Coordenadas relativas a camara (requerido por WorldRenderEvents consumidores) + matrices.translate( + pos.getX() - camPos.x, + pos.getY() - camPos.y, + pos.getZ() - camPos.z + ); + + float eased = easeInOut(t); + float angleDeg = lerpAngleDeg(anim.fromOpen, anim.toOpen, eased, anim.kind, state); + + int light = LevelRenderer.getLightColor((BlockAndTintGetter) MC.level, pos); + if (anim.kind == Kind.FENCE_GATE) { + FenceGateAnimation.render(state, angleDeg, matrices, consumers, light, OverlayTexture.NO_OVERLAY); + } else { + applyTransform(anim.kind, state, angleDeg, matrices); + renderBlockAsEntity(state, matrices, consumers, light, OverlayTexture.NO_OVERLAY); + } + + matrices.popPose(); + } + + // === DOOR (dos mitades) === + + private static void startDoorAnimBothHalves(BlockPos lowerPos, BlockState lowerNewState, boolean oldOpen, boolean newOpen, + float speed, long startTick) { + BlockState baseLower = forceClosedModel( + lowerNewState.setValue(DoorBlock.HALF, DoubleBlockHalf.LOWER), + Kind.DOOR + ); + + BlockPos upperPos = lowerPos.above(); + BlockState baseUpper = baseLower.setValue(DoorBlock.HALF, DoubleBlockHalf.UPPER); + + Anim aLower = new Anim(lowerPos, baseLower, Kind.DOOR, oldOpen, newOpen, speed, startTick); + Anim aUpper = new Anim(upperPos, baseUpper, Kind.DOOR, oldOpen, newOpen, speed, startTick); + + synchronized (ANIMS) { + ANIMS.put(lowerPos.asLong(), aLower); + ANIMS.put(upperPos.asLong(), aUpper); + } + requestRerender(lowerPos); + requestRerender(upperPos); + } + + private static void removeKindAt(BlockPos pos, Kind kind) { + synchronized (ANIMS) { + ANIMS.remove(pos.asLong()); + if (kind == Kind.DOOR) { + ANIMS.remove(pos.above().asLong()); + ANIMS.remove(pos.below().asLong()); + } + } + } + + // === Angulos y transforms === + + private static float lerpAngleDeg(boolean fromOpen, boolean toOpen, float t, Kind kind, BlockState state) { + float a = angleFor(kind, state, fromOpen); + float b = angleFor(kind, state, toOpen); + return Mth.lerp(t, a, b); + } + + private static float angleFor(Kind kind, BlockState state, boolean open) { + if (!open) return 0.0f; + + return switch (kind) { + case DOOR -> { + DoorHingeSide hinge = state.getValue(DoorBlock.HINGE); + yield (hinge == DoorHingeSide.RIGHT) ? -90.0f : 90.0f; + } + case TRAPDOOR -> 90.0f; + case FENCE_GATE -> 90.0f; + }; + } + + private static void applyTransform(Kind kind, BlockState state, float angleDeg, PoseStack matrices) { + switch (kind) { + case DOOR -> DoorAnimation.apply(state, angleDeg, matrices, MC.level); + case TRAPDOOR -> TrapdoorAnimation.apply(state, angleDeg, matrices, MC.level); + case FENCE_GATE -> { + } + } + } + + private static void renderBlockAsEntity(BlockState state, PoseStack matrices, + MultiBufferSource consumers, int light, int overlay) { + MC.getBlockRenderer().renderSingleBlock(state, matrices, consumers, light, overlay); + } + + // === Utilidades === + + private static BlockState forceClosedModel(BlockState s, Kind kind) { + return switch (kind) { + case DOOR -> s.setValue(DoorBlock.OPEN, false); + case TRAPDOOR -> s.setValue(TrapDoorBlock.OPEN, false); + case FENCE_GATE -> s.setValue(FenceGateBlock.OPEN, false); + }; + } + + private static boolean isKindBlock(Kind kind, BlockState state) { + Block b = state.getBlock(); + return switch (kind) { + case DOOR -> b instanceof DoorBlock; + case TRAPDOOR -> b instanceof TrapDoorBlock; + case FENCE_GATE -> b instanceof FenceGateBlock; + }; + } + + private static void requestRerender(BlockPos pos) { + if (MC.levelRenderer == null || MC.level == null) return; + BlockState st = MC.level.getBlockState(pos); + MC.levelRenderer.blockChanged(MC.level, pos, st, st, 0); + MC.levelRenderer.setBlocksDirty( + pos.getX(), pos.getY(), pos.getZ(), + pos.getX(), pos.getY(), pos.getZ() + ); + MC.levelRenderer.setSectionDirty( + SectionPos.blockToSectionCoord(pos.getX()), + SectionPos.blockToSectionCoord(pos.getY()), + SectionPos.blockToSectionCoord(pos.getZ()) + ); + } + + private static boolean isOpen(BlockState s) { + Block b = s.getBlock(); + if (b instanceof DoorBlock) return s.getValue(DoorBlock.OPEN); + if (b instanceof TrapDoorBlock) return s.getValue(TrapDoorBlock.OPEN); + if (b instanceof FenceGateBlock) return s.getValue(FenceGateBlock.OPEN); + return false; + } + + private static Kind kindOf(BlockState oldState, BlockState newState) { + Block b = newState.getBlock(); + if (b instanceof DoorBlock) return Kind.DOOR; + if (b instanceof TrapDoorBlock) return Kind.TRAPDOOR; + if (b instanceof FenceGateBlock) return Kind.FENCE_GATE; + + b = oldState.getBlock(); + if (b instanceof DoorBlock) return Kind.DOOR; + if (b instanceof TrapDoorBlock) return Kind.TRAPDOOR; + if (b instanceof FenceGateBlock) return Kind.FENCE_GATE; + + return null; + } + + private static boolean isAnimationEnabled(SddConfig cfg, Kind kind) { + return switch (kind) { + case DOOR -> cfg.animateDoors; + case TRAPDOOR -> cfg.animateTrapdoors; + case FENCE_GATE -> getBool(cfg, "animateFenceGates", false); + }; + } + + private static float speedFor(SddConfig cfg, Kind kind) { + return switch (kind) { + case DOOR -> cfg.doorSpeed; + case TRAPDOOR -> cfg.trapdoorSpeed; + case FENCE_GATE -> getFloat(cfg, "fenceGateSpeed", 1.0f); + }; + } + + private static float easeInOut(float t) { + return t * t * (3.0f - 2.0f * t); + } + + private static boolean isHiddenByAnim(BlockPos pos) { + Anim anim = ANIMS.get(pos.asLong()); + return anim != null && anim.hideVanilla; + } + + // === Reflect helpers (por si el campo no existe) === + + private static boolean getBool(Object obj, String field, boolean def) { + try { + Field f = obj.getClass().getDeclaredField(field); + f.setAccessible(true); + Object v = f.get(obj); + return (v instanceof Boolean) ? (Boolean) v : def; + } catch (Throwable ignored) { + return def; + } + } + + private static float getFloat(Object obj, String field, float def) { + try { + Field f = obj.getClass().getDeclaredField(field); + f.setAccessible(true); + Object v = f.get(obj); + if (v instanceof Float) return (Float) v; + if (v instanceof Double) return ((Double) v).floatValue(); + if (v instanceof Number) return ((Number) v).floatValue(); + return def; + } catch (Throwable ignored) { + return def; + } + } + + // === Tipos === + + public enum Kind { DOOR, TRAPDOOR, FENCE_GATE } + + private static final class Anim { + final BlockPos pos; + final BlockState baseState; + final Kind kind; + final boolean fromOpen; + final boolean toOpen; + boolean hideVanilla = true; + + final double startTick; + final double durationTicks; + + Anim(BlockPos pos, BlockState baseState, Kind kind, boolean fromOpen, boolean toOpen, + float speed, long startTick) { + this.pos = pos; + this.baseState = baseState; + this.kind = kind; + this.fromOpen = fromOpen; + this.toOpen = toOpen; + + double durS = BASE_DURATION_S / speed; + if (durS < 0.05) durS = 0.05; + + this.startTick = (double) startTick; + this.durationTicks = Math.max(1.0, durS * 20.0); + } + + float progress01(double nowTick) { + double t = (nowTick - startTick) / durationTicks; + if (t <= 0.0) return 0.0f; + if (t >= 1.0) return 1.0f; + return (float) t; + } + + boolean hasEnded(double nowTick) { + return (nowTick - startTick) >= durationTicks; + } + + boolean isFinished(double nowTick) { + return (nowTick - startTick) >= (durationTicks + END_GRACE_TICKS); + } + } +} diff --git a/src/client/java/com/straice/smoothdoors/client/anim/TrapdoorAnimation.java b/src/client/java/com/straice/smoothdoors/client/anim/TrapdoorAnimation.java new file mode 100644 index 0000000..fe69f0c --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/client/anim/TrapdoorAnimation.java @@ -0,0 +1,87 @@ +package com.straice.smoothdoors.client.anim; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Half; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +final class TrapdoorAnimation { + + private TrapdoorAnimation() {} + + static void apply(BlockState state, float baseAngleDeg, PoseStack matrices, BlockGetter world) { + if (world == null) return; + + Direction facing = state.getValue(TrapDoorBlock.FACING); + Half half = state.getValue(TrapDoorBlock.HALF); + + VoxelShape collShape = state.getCollisionShape(world, BlockPos.ZERO, CollisionContext.empty()); + AABB bb = collShape.isEmpty() + ? state.getShape(world, BlockPos.ZERO, CollisionContext.empty()).bounds() + : collShape.bounds(); + + Direction hingeSide = facing.getOpposite(); + + float pivotX = (float) ((bb.minX + bb.maxX) * 0.5); + float pivotZ = (float) ((bb.minZ + bb.maxZ) * 0.5); + switch (hingeSide) { + case NORTH -> pivotZ = (float) bb.minZ; + case SOUTH -> pivotZ = (float) bb.maxZ; + case WEST -> pivotX = (float) bb.minX; + case EAST -> pivotX = (float) bb.maxX; + default -> { + pivotX = (float) ((bb.minX + bb.maxX) * 0.5); + pivotZ = (float) ((bb.minZ + bb.maxZ) * 0.5); + } + } + float pivotY = (half == Half.TOP) ? (float) bb.maxY : (float) bb.minY; + + float angle; + switch (hingeSide) { + case NORTH, EAST -> angle = -baseAngleDeg; + case SOUTH, WEST -> angle = baseAngleDeg; + default -> angle = baseAngleDeg; + } + if (half == Half.TOP) angle = -angle; + + float thickness = (float) (bb.maxY - bb.minY); + float halfT = thickness > 0.0f ? thickness * 0.5f : (3.0f / 32.0f); + float dY = (half == Half.TOP) ? -halfT : halfT; + + double rad = Math.toRadians(angle); + float cos = (float) Math.cos(rad); + float sin = (float) Math.sin(rad); + + float shiftX = 0.0f; + float shiftY = 0.0f; + float shiftZ = 0.0f; + if (hingeSide == Direction.NORTH || hingeSide == Direction.SOUTH) { + float rotY = dY * cos; + float rotZ = dY * sin; + shiftY = dY - rotY; + shiftZ = -rotZ; + } else { + float rotX = -dY * sin; + float rotY = dY * cos; + shiftX = -rotX; + shiftY = dY - rotY; + } + + matrices.translate(shiftX, shiftY, shiftZ); + matrices.translate(pivotX, pivotY, pivotZ); + if (hingeSide == Direction.NORTH || hingeSide == Direction.SOUTH) { + matrices.mulPose(Axis.XP.rotationDegrees(angle)); + } else { + matrices.mulPose(Axis.ZP.rotationDegrees(angle)); + } + matrices.translate(-pivotX, -pivotY, -pivotZ); + matrices.translate(-shiftX, -shiftY, -shiftZ); + } +} diff --git a/src/client/java/com/straice/smoothdoors/mixin/client/BlockModelRendererMixin.java b/src/client/java/com/straice/smoothdoors/mixin/client/BlockModelRendererMixin.java new file mode 100644 index 0000000..b3d994b --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/mixin/client/BlockModelRendererMixin.java @@ -0,0 +1,61 @@ +package com.straice.smoothdoors.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.block.ModelBlockRenderer; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.util.RandomSource; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ModelBlockRenderer.class) +public class BlockModelRendererMixin { + + @Inject( + method = "tesselateBlock", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$tesselateBlock(BlockAndTintGetter world, BakedModel model, BlockState state, BlockPos pos, + PoseStack matrices, VertexConsumer vertices, boolean cull, RandomSource random, + long seed, int overlay, CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } + + @Inject( + method = "tesselateWithAO", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$tesselateWithAo(BlockAndTintGetter world, BakedModel model, BlockState state, BlockPos pos, + PoseStack matrices, VertexConsumer vertices, boolean cull, RandomSource random, + long seed, int overlay, CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } + + @Inject( + method = "tesselateWithoutAO", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$tesselateWithoutAo(BlockAndTintGetter world, BakedModel model, BlockState state, BlockPos pos, + PoseStack matrices, VertexConsumer vertices, boolean cull, RandomSource random, + long seed, int overlay, CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } +} diff --git a/src/client/java/com/straice/smoothdoors/mixin/client/BlockRenderManagerMixin.java b/src/client/java/com/straice/smoothdoors/mixin/client/BlockRenderManagerMixin.java new file mode 100644 index 0000000..24ea944 --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/mixin/client/BlockRenderManagerMixin.java @@ -0,0 +1,33 @@ +package com.straice.smoothdoors.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.renderer.block.BlockRenderDispatcher; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.BlockAndTintGetter; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BlockRenderDispatcher.class) +public class BlockRenderManagerMixin { + + @Inject( + method = "renderBatched", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$renderBatched(BlockState state, BlockPos pos, BlockAndTintGetter world, + PoseStack matrices, VertexConsumer vertexConsumer, + boolean cull, RandomSource random, + CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } +} diff --git a/src/client/java/com/straice/smoothdoors/mixin/client/ChunkRendererRegionMixin.java b/src/client/java/com/straice/smoothdoors/mixin/client/ChunkRendererRegionMixin.java new file mode 100644 index 0000000..48e7b67 --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/mixin/client/ChunkRendererRegionMixin.java @@ -0,0 +1,23 @@ +package com.straice.smoothdoors.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import net.minecraft.client.renderer.chunk.RenderChunkRegion; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(RenderChunkRegion.class) +public class ChunkRendererRegionMixin { + + @Inject(method = "getBlockState", at = @At("HEAD"), cancellable = true) + private void sdd$getBlockState(BlockPos pos, CallbackInfoReturnable cir) { + if (SddAnimator.isAnimatingAt(pos)) { + cir.setReturnValue(Blocks.AIR.defaultBlockState()); + cir.cancel(); + } + } +} diff --git a/src/client/java/com/straice/smoothdoors/mixin/client/ClientWorldMixin.java b/src/client/java/com/straice/smoothdoors/mixin/client/ClientWorldMixin.java new file mode 100644 index 0000000..0809d05 --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/mixin/client/ClientWorldMixin.java @@ -0,0 +1,19 @@ +package com.straice.smoothdoors.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientLevel.class) +public class ClientWorldMixin { + + @Inject(method = "sendBlockUpdated", at = @At("TAIL")) + private void sdd$updateListeners(BlockPos pos, BlockState oldState, BlockState newState, int flags, CallbackInfo ci) { + SddAnimator.onBlockUpdate(pos, oldState, newState); + } +} diff --git a/src/client/resources/template-mod.client.mixins.json b/src/client/resources/template-mod.client.mixins.json index 26d75db..8122fb5 100644 --- a/src/client/resources/template-mod.client.mixins.json +++ b/src/client/resources/template-mod.client.mixins.json @@ -1,9 +1,12 @@ { "required": true, - "package": "com.straice.mixin.client", + "package": "com.straice.smoothdoors.mixin.client", "compatibilityLevel": "JAVA_17", "client": [ - "ExampleClientMixin" + "BlockModelRendererMixin", + "BlockRenderManagerMixin", + "ChunkRendererRegionMixin", + "ClientWorldMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/java/com/straice/TemplateMod.java b/src/main/java/com/straice/TemplateMod.java index 1f53723..9904e55 100644 --- a/src/main/java/com/straice/TemplateMod.java +++ b/src/main/java/com/straice/TemplateMod.java @@ -5,6 +5,8 @@ import net.fabricmc.api.ModInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.straice.smoothdoors.config.SddConfigManager; + public class TemplateMod implements ModInitializer { public static final String MOD_ID = "template-mod"; @@ -15,10 +17,7 @@ public class TemplateMod implements ModInitializer { @Override public void onInitialize() { - // This code runs as soon as Minecraft is in a mod-load-ready state. - // However, some things (like resources) may still be uninitialized. - // Proceed with mild caution. - - LOGGER.info("Hello Fabric world!"); + SddConfigManager.load(); + LOGGER.info("Smooth Double Doors loaded"); } -} \ No newline at end of file +} diff --git a/src/main/java/com/straice/smoothdoors/config/SddConfig.java b/src/main/java/com/straice/smoothdoors/config/SddConfig.java new file mode 100644 index 0000000..4015704 --- /dev/null +++ b/src/main/java/com/straice/smoothdoors/config/SddConfig.java @@ -0,0 +1,20 @@ +package com.straice.smoothdoors.config; + +public class SddConfig { + public boolean connectDoors = true; + public boolean redstoneDoubleDoors = true; + + public boolean connectTrapdoors = false; + public boolean redstoneDoubleTrapdoors = false; + + public boolean connectFenceGates = false; + public boolean redstoneDoubleFenceGates = false; + + public boolean animateDoors = true; + public boolean animateTrapdoors = true; + public boolean animateFenceGates = true; + + public float doorSpeed = 1.0f; + public float trapdoorSpeed = 1.0f; + public float fenceGateSpeed = 1.0f; +} diff --git a/src/main/java/com/straice/smoothdoors/config/SddConfigManager.java b/src/main/java/com/straice/smoothdoors/config/SddConfigManager.java new file mode 100644 index 0000000..9213773 --- /dev/null +++ b/src/main/java/com/straice/smoothdoors/config/SddConfigManager.java @@ -0,0 +1,40 @@ +package com.straice.smoothdoors.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.fabricmc.loader.api.FabricLoader; + +import java.nio.file.Files; +import java.nio.file.Path; + +public final class SddConfigManager { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve("smooth-double-doors.json"); + + private static SddConfig config = new SddConfig(); + + private SddConfigManager() {} + + public static SddConfig get() { return config; } + + public static void load() { + try { + if (Files.exists(PATH)) { + String json = Files.readString(PATH); + SddConfig read = GSON.fromJson(json, SddConfig.class); + if (read != null) config = read; + } else { + save(); + } + } catch (Exception ignored) { + // si falla, usamos defaults + } + } + + public static void save() { + try { + Files.createDirectories(PATH.getParent()); + Files.writeString(PATH, GSON.toJson(config)); + } catch (Exception ignored) {} + } +} diff --git a/src/main/java/com/straice/smoothdoors/mixin/DoorBlockMixin.java b/src/main/java/com/straice/smoothdoors/mixin/DoorBlockMixin.java new file mode 100644 index 0000000..3c0be87 --- /dev/null +++ b/src/main/java/com/straice/smoothdoors/mixin/DoorBlockMixin.java @@ -0,0 +1,68 @@ +package com.straice.smoothdoors.mixin; + +import com.straice.smoothdoors.config.SddConfigManager; +import com.straice.smoothdoors.util.ConnectedUtil; +import com.straice.smoothdoors.util.SyncGuard; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(DoorBlock.class) +public class DoorBlockMixin { + + private void sdd$handleUse(BlockState state, Level level, BlockPos pos) { + if (level.isClientSide()) return; + if (!SddConfigManager.get().connectDoors) return; + if (SyncGuard.isIn()) return; + + BlockPos lower = ConnectedUtil.getDoorLowerPos(pos, state); + BlockState lowerNow = level.getBlockState(lower); + if (!lowerNow.hasProperty(DoorBlock.OPEN)) return; + + boolean openNow = lowerNow.getValue(DoorBlock.OPEN); + BlockPos mateLower = ConnectedUtil.findPairedDoorLower(level, lower, lowerNow); + if (mateLower == null) return; + + SyncGuard.run(() -> ConnectedUtil.setDoorOpen(level, mateLower, openNow)); + } + + @Inject(method = "use", at = @At("TAIL")) + private void sdd$use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, + BlockHitResult hit, CallbackInfoReturnable cir) { + sdd$handleUse(state, level, pos); + } + + private void sdd$handleNeighborUpdate(BlockState state, Level level, BlockPos pos) { + if (level.isClientSide()) return; + if (!SddConfigManager.get().connectDoors) return; + if (!SddConfigManager.get().redstoneDoubleDoors) return; + if (SyncGuard.isIn()) return; + + BlockPos lower = ConnectedUtil.getDoorLowerPos(pos, state); + BlockState lowerNow = level.getBlockState(lower); + if (!lowerNow.hasProperty(DoorBlock.OPEN)) return; + + boolean openNow = lowerNow.getValue(DoorBlock.OPEN); + BlockPos mateLower = ConnectedUtil.findPairedDoorLower(level, lower, lowerNow); + if (mateLower == null) return; + + SyncGuard.run(() -> ConnectedUtil.setDoorOpen(level, mateLower, openNow)); + } + + @Inject(method = "neighborChanged", at = @At("TAIL")) + private void sdd$neighborChanged(BlockState state, Level level, BlockPos pos, Block block, + BlockPos fromPos, boolean notify, CallbackInfo ci) { + sdd$handleNeighborUpdate(state, level, pos); + } +} diff --git a/src/main/java/com/straice/smoothdoors/mixin/FenceGateBlockMixin.java b/src/main/java/com/straice/smoothdoors/mixin/FenceGateBlockMixin.java new file mode 100644 index 0000000..df1c985 --- /dev/null +++ b/src/main/java/com/straice/smoothdoors/mixin/FenceGateBlockMixin.java @@ -0,0 +1,66 @@ +package com.straice.smoothdoors.mixin; + +import com.straice.smoothdoors.config.SddConfigManager; +import com.straice.smoothdoors.util.ConnectedUtil; +import com.straice.smoothdoors.util.SyncGuard; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.FenceGateBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(FenceGateBlock.class) +public class FenceGateBlockMixin { + + private void sdd$handleUse(BlockState state, Level level, BlockPos pos) { + if (level.isClientSide()) return; + if (!SddConfigManager.get().connectFenceGates) return; + if (SyncGuard.isIn()) return; + + BlockState now = level.getBlockState(pos); + if (!now.hasProperty(FenceGateBlock.OPEN)) return; + + boolean openNow = now.getValue(FenceGateBlock.OPEN); + BlockPos mate = ConnectedUtil.findPairedFenceGate(level, pos, now); + if (mate == null) return; + + SyncGuard.run(() -> ConnectedUtil.setFenceGateOpen(level, mate, openNow)); + } + + @Inject(method = "use", at = @At("TAIL")) + private void sdd$use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, + BlockHitResult hit, CallbackInfoReturnable cir) { + sdd$handleUse(state, level, pos); + } + + private void sdd$handleNeighborUpdate(BlockState state, Level level, BlockPos pos) { + if (level.isClientSide()) return; + if (!SddConfigManager.get().connectFenceGates) return; + if (!SddConfigManager.get().redstoneDoubleFenceGates) return; + if (SyncGuard.isIn()) return; + + BlockState now = level.getBlockState(pos); + if (!now.hasProperty(FenceGateBlock.OPEN)) return; + + boolean openNow = now.getValue(FenceGateBlock.OPEN); + BlockPos mate = ConnectedUtil.findPairedFenceGate(level, pos, now); + if (mate == null) return; + + SyncGuard.run(() -> ConnectedUtil.setFenceGateOpen(level, mate, openNow)); + } + + @Inject(method = "neighborChanged", at = @At("TAIL")) + private void sdd$neighborChanged(BlockState state, Level level, BlockPos pos, Block block, + BlockPos fromPos, boolean notify, CallbackInfo ci) { + sdd$handleNeighborUpdate(state, level, pos); + } +} diff --git a/src/main/java/com/straice/smoothdoors/mixin/TrapdoorBlockMixin.java b/src/main/java/com/straice/smoothdoors/mixin/TrapdoorBlockMixin.java new file mode 100644 index 0000000..50bb207 --- /dev/null +++ b/src/main/java/com/straice/smoothdoors/mixin/TrapdoorBlockMixin.java @@ -0,0 +1,66 @@ +package com.straice.smoothdoors.mixin; + +import com.straice.smoothdoors.config.SddConfigManager; +import com.straice.smoothdoors.util.ConnectedUtil; +import com.straice.smoothdoors.util.SyncGuard; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(TrapDoorBlock.class) +public class TrapdoorBlockMixin { + + private void sdd$handleUse(BlockState state, Level level, BlockPos pos) { + if (level.isClientSide()) return; + if (!SddConfigManager.get().connectTrapdoors) return; + if (SyncGuard.isIn()) return; + + BlockState now = level.getBlockState(pos); + if (!now.hasProperty(TrapDoorBlock.OPEN)) return; + + boolean openNow = now.getValue(TrapDoorBlock.OPEN); + BlockPos mate = ConnectedUtil.findPairedTrapdoor(level, pos, now); + if (mate == null) return; + + SyncGuard.run(() -> ConnectedUtil.setTrapdoorOpen(level, mate, openNow)); + } + + @Inject(method = "use", at = @At("TAIL")) + private void sdd$use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, + BlockHitResult hit, CallbackInfoReturnable cir) { + sdd$handleUse(state, level, pos); + } + + private void sdd$handleNeighborUpdate(BlockState state, Level level, BlockPos pos) { + if (level.isClientSide()) return; + if (!SddConfigManager.get().connectTrapdoors) return; + if (!SddConfigManager.get().redstoneDoubleTrapdoors) return; + if (SyncGuard.isIn()) return; + + BlockState now = level.getBlockState(pos); + if (!now.hasProperty(TrapDoorBlock.OPEN)) return; + + boolean openNow = now.getValue(TrapDoorBlock.OPEN); + BlockPos mate = ConnectedUtil.findPairedTrapdoor(level, pos, now); + if (mate == null) return; + + SyncGuard.run(() -> ConnectedUtil.setTrapdoorOpen(level, mate, openNow)); + } + + @Inject(method = "neighborChanged", at = @At("TAIL")) + private void sdd$neighborChanged(BlockState state, Level level, BlockPos pos, Block block, + BlockPos fromPos, boolean notify, CallbackInfo ci) { + sdd$handleNeighborUpdate(state, level, pos); + } +} diff --git a/src/main/java/com/straice/smoothdoors/util/ConnectedUtil.java b/src/main/java/com/straice/smoothdoors/util/ConnectedUtil.java new file mode 100644 index 0000000..30ebff2 --- /dev/null +++ b/src/main/java/com/straice/smoothdoors/util/ConnectedUtil.java @@ -0,0 +1,125 @@ +package com.straice.smoothdoors.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.FenceGateBlock; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.DoorHingeSide; +import net.minecraft.world.level.block.state.properties.DoubleBlockHalf; +import net.minecraft.world.level.block.state.properties.Half; + +public final class ConnectedUtil { + private ConnectedUtil() {} + + // ===== DOORS ===== + + public static BlockPos getDoorLowerPos(BlockPos pos, BlockState state) { + DoubleBlockHalf half = state.getValue(DoorBlock.HALF); + return half == DoubleBlockHalf.LOWER ? pos : pos.below(); + } + + public static BlockPos findPairedDoorLower(Level world, BlockPos lowerPos, BlockState lowerState) { + if (!(lowerState.getBlock() instanceof DoorBlock)) return null; + + Direction facing = lowerState.getValue(DoorBlock.FACING); + DoorHingeSide hinge = lowerState.getValue(DoorBlock.HINGE); + + Direction side = (hinge == DoorHingeSide.LEFT) + ? facing.getClockWise() + : facing.getCounterClockWise(); + + BlockPos otherLower = lowerPos.relative(side); + BlockState other = world.getBlockState(otherLower); + + if (other.getBlock() != lowerState.getBlock()) return null; + if (!(other.getBlock() instanceof DoorBlock)) return null; + if (other.getValue(DoorBlock.HALF) != DoubleBlockHalf.LOWER) return null; + if (other.getValue(DoorBlock.FACING) != facing) return null; + if (other.getValue(DoorBlock.HINGE) == hinge) return null; + + return otherLower; + } + + public static void setDoorOpen(Level world, BlockPos doorLower, boolean open) { + BlockState lower = world.getBlockState(doorLower); + if (!(lower.getBlock() instanceof DoorBlock)) return; + + BlockPos upperPos = doorLower.above(); + BlockState upper = world.getBlockState(upperPos); + + world.setBlock(doorLower, lower.setValue(DoorBlock.OPEN, open), Block.UPDATE_ALL); + + if (upper.getBlock() == lower.getBlock()) { + world.setBlock(upperPos, upper.setValue(DoorBlock.OPEN, open), Block.UPDATE_ALL); + } + } + + // ===== TRAPDOORS ===== + + public static BlockPos findPairedTrapdoor(Level world, BlockPos pos, BlockState state) { + if (!(state.getBlock() instanceof TrapDoorBlock)) return null; + + Direction facing = state.getValue(TrapDoorBlock.FACING); + Half half = state.getValue(TrapDoorBlock.HALF); + + BlockPos left = pos.relative(facing.getCounterClockWise()); + BlockPos right = pos.relative(facing.getClockWise()); + + BlockPos mate = matchTrapdoor(world, state, left, facing, half); + if (mate != null) return mate; + return matchTrapdoor(world, state, right, facing, half); + } + + private static BlockPos matchTrapdoor(Level world, BlockState a, BlockPos candidate, Direction facing, Half half) { + BlockState b = world.getBlockState(candidate); + if (b.getBlock() != a.getBlock()) return null; + if (!(b.getBlock() instanceof TrapDoorBlock)) return null; + + if (b.getValue(TrapDoorBlock.FACING) != facing) return null; + if (b.getValue(TrapDoorBlock.HALF) != half) return null; + + return candidate; + } + + public static void setTrapdoorOpen(Level world, BlockPos pos, boolean open) { + BlockState st = world.getBlockState(pos); + if (!(st.getBlock() instanceof TrapDoorBlock)) return; + + world.setBlock(pos, st.setValue(TrapDoorBlock.OPEN, open), Block.UPDATE_ALL); + } + + // ===== FENCE GATES ===== + + public static BlockPos findPairedFenceGate(Level world, BlockPos pos, BlockState state) { + if (!(state.getBlock() instanceof FenceGateBlock)) return null; + + Direction facing = state.getValue(FenceGateBlock.FACING); + + BlockPos left = pos.relative(facing.getCounterClockWise()); + BlockPos right = pos.relative(facing.getClockWise()); + + BlockPos mate = matchFenceGate(world, state, left, facing); + if (mate != null) return mate; + return matchFenceGate(world, state, right, facing); + } + + private static BlockPos matchFenceGate(Level world, BlockState a, BlockPos candidate, Direction facing) { + BlockState b = world.getBlockState(candidate); + if (b.getBlock() != a.getBlock()) return null; + if (!(b.getBlock() instanceof FenceGateBlock)) return null; + + if (b.getValue(FenceGateBlock.FACING) != facing) return null; + return candidate; + } + + public static void setFenceGateOpen(Level world, BlockPos pos, boolean open) { + BlockState st = world.getBlockState(pos); + if (!(st.getBlock() instanceof FenceGateBlock)) return; + + world.setBlock(pos, st.setValue(FenceGateBlock.OPEN, open), Block.UPDATE_ALL); + } +} diff --git a/src/main/java/com/straice/smoothdoors/util/SyncGuard.java b/src/main/java/com/straice/smoothdoors/util/SyncGuard.java new file mode 100644 index 0000000..a8e5305 --- /dev/null +++ b/src/main/java/com/straice/smoothdoors/util/SyncGuard.java @@ -0,0 +1,16 @@ +package com.straice.smoothdoors.util; + +public final class SyncGuard { + private static final ThreadLocal IN = ThreadLocal.withInitial(() -> false); + + private SyncGuard() {} + + public static boolean isIn() { return IN.get(); } + + public static void run(Runnable r) { + if (isIn()) return; + IN.set(true); + try { r.run(); } + finally { IN.set(false); } + } +} diff --git a/src/main/resources/template-mod.mixins.json b/src/main/resources/template-mod.mixins.json index 4e5c87e..1196547 100644 --- a/src/main/resources/template-mod.mixins.json +++ b/src/main/resources/template-mod.mixins.json @@ -1,10 +1,11 @@ { "required": true, - "package": "com.straice.mixin", + "package": "com.straice.smoothdoors.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ "DoorBlockMixin", - "ExampleMixin" + "FenceGateBlockMixin", + "TrapdoorBlockMixin" ], "injectors": { "defaultRequire": 1 diff --git a/tt.png b/tt.png new file mode 100644 index 0000000..63a55e0 Binary files /dev/null and b/tt.png differ