diff --git a/assets/minecraft/blockstates/oak_trapdoor.json b/assets/minecraft/blockstates/oak_trapdoor.json new file mode 100644 index 0000000..168faf1 --- /dev/null +++ b/assets/minecraft/blockstates/oak_trapdoor.json @@ -0,0 +1,58 @@ +{ + "variants": { + "facing=east,half=bottom,open=false": { + "model": "minecraft:block/oak_trapdoor_bottom" + }, + "facing=east,half=bottom,open=true": { + "model": "minecraft:block/oak_trapdoor_open", + "y": 90 + }, + "facing=east,half=top,open=false": { + "model": "minecraft:block/oak_trapdoor_top" + }, + "facing=east,half=top,open=true": { + "model": "minecraft:block/oak_trapdoor_open", + "y": 90 + }, + "facing=north,half=bottom,open=false": { + "model": "minecraft:block/oak_trapdoor_bottom" + }, + "facing=north,half=bottom,open=true": { + "model": "minecraft:block/oak_trapdoor_open" + }, + "facing=north,half=top,open=false": { + "model": "minecraft:block/oak_trapdoor_top" + }, + "facing=north,half=top,open=true": { + "model": "minecraft:block/oak_trapdoor_open" + }, + "facing=south,half=bottom,open=false": { + "model": "minecraft:block/oak_trapdoor_bottom" + }, + "facing=south,half=bottom,open=true": { + "model": "minecraft:block/oak_trapdoor_open", + "y": 180 + }, + "facing=south,half=top,open=false": { + "model": "minecraft:block/oak_trapdoor_top" + }, + "facing=south,half=top,open=true": { + "model": "minecraft:block/oak_trapdoor_open", + "y": 180 + }, + "facing=west,half=bottom,open=false": { + "model": "minecraft:block/oak_trapdoor_bottom" + }, + "facing=west,half=bottom,open=true": { + "model": "minecraft:block/oak_trapdoor_open", + "y": 270 + }, + "facing=west,half=top,open=false": { + "model": "minecraft:block/oak_trapdoor_top" + }, + "facing=west,half=top,open=true": { + "model": "minecraft:block/oak_trapdoor_open", + "y": 270 + } + } +} \ No newline at end of file diff --git a/assets/minecraft/models/block/oak_trapdoor_open.json b/assets/minecraft/models/block/oak_trapdoor_open.json new file mode 100644 index 0000000..e8b0bb3 --- /dev/null +++ b/assets/minecraft/models/block/oak_trapdoor_open.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/template_trapdoor_open", + "textures": { + "texture": "minecraft:block/oak_trapdoor" + } +} \ No newline at end of file diff --git a/assets/minecraft/models/block/template_trapdoor_bottom.json b/assets/minecraft/models/block/template_trapdoor_bottom.json new file mode 100644 index 0000000..2b6c8da --- /dev/null +++ b/assets/minecraft/models/block/template_trapdoor_bottom.json @@ -0,0 +1,18 @@ +{ "parent": "block/thin_block", + "textures": { + "particle": "#texture" + }, + "elements": [ + { "from": [ 0, 0, 0 ], + "to": [ 16, 3, 16 ], + "faces": { + "down": { "uv": [ 0, 0, 16, 16 ], "texture": "#texture", "cullface": "down" }, + "up": { "uv": [ 0, 0, 16, 16 ], "texture": "#texture" }, + "north": { "uv": [ 0, 16, 16, 13 ], "texture": "#texture", "cullface": "north" }, + "south": { "uv": [ 0, 16, 16, 13 ], "texture": "#texture", "cullface": "south" }, + "west": { "uv": [ 0, 16, 16, 13 ], "texture": "#texture", "cullface": "west" }, + "east": { "uv": [ 0, 16, 16, 13 ], "texture": "#texture", "cullface": "east" } + } + } + ] +} diff --git a/assets/minecraft/models/block/template_trapdoor_open.json b/assets/minecraft/models/block/template_trapdoor_open.json new file mode 100644 index 0000000..b301619 --- /dev/null +++ b/assets/minecraft/models/block/template_trapdoor_open.json @@ -0,0 +1,18 @@ +{ + "textures": { + "particle": "#texture" + }, + "elements": [ + { "from": [ 0, 0, 13 ], + "to": [ 16, 16, 16 ], + "faces": { + "down": { "uv": [ 0, 13, 16, 16 ], "texture": "#texture", "cullface": "down" }, + "up": { "uv": [ 0, 16, 16, 13 ], "texture": "#texture", "cullface": "up" }, + "north": { "uv": [ 0, 0, 16, 16 ], "texture": "#texture" }, + "south": { "uv": [ 0, 0, 16, 16 ], "texture": "#texture", "cullface": "south" }, + "west": { "uv": [ 16, 0, 13, 16 ], "texture": "#texture", "cullface": "west" }, + "east": { "uv": [ 13, 0, 16, 16 ], "texture": "#texture", "cullface": "east" } + } + } + ] +} diff --git a/ccc.png b/ccc.png new file mode 100644 index 0000000..1e76290 Binary files /dev/null and b/ccc.png differ diff --git a/dss.png b/dss.png new file mode 100644 index 0000000..ebff7c5 Binary files /dev/null and b/dss.png differ diff --git a/fuera.png b/fuera.png new file mode 100644 index 0000000..b336641 Binary files /dev/null and b/fuera.png differ diff --git a/mal.png b/mal.png new file mode 100644 index 0000000..ab8ee04 Binary files /dev/null and b/mal.png differ diff --git a/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java b/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java index 5caf4c7..bfe4112 100644 --- a/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java +++ b/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java @@ -14,112 +14,143 @@ import net.minecraft.client.render.VertexConsumerProvider; import net.minecraft.client.render.WorldRenderer; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.util.math.*; -import net.minecraft.util.math.RotationAxis; -import net.minecraft.world.BlockRenderView; -import net.minecraft.world.World; +import net.minecraft.util.shape.VoxelShape; + +import java.lang.reflect.Field; public final class SddAnimator { private static final MinecraftClient MC = MinecraftClient.getInstance(); - // Duración base (segundos) a speed=1.0x + // Duración base (segundos) a speed = 1.0x private static final float BASE_DURATION_S = 0.35f; - private static final float MIN_DURATION_S = 0.05f; private static final Long2ObjectOpenHashMap ANIMS = new Long2ObjectOpenHashMap<>(); private static boolean hooksInit = false; private SddAnimator() {} - /** - * Llamar desde SmoothDoubleDoorsClient.onInitializeClient() - * (evita mixins a WorldRenderer y firmas rotas). - */ + /** Llamar desde SmoothDoubleDoorsClient#onInitializeClient(). */ public static void initClientHooks() { if (hooksInit) return; hooksInit = true; - // Render cada frame - WorldRenderEvents.END.register(ctx -> { + WorldRenderEvents.AFTER_ENTITIES.register(ctx -> { if (MC.world == null || ANIMS.isEmpty()) return; - - Vec3d camPos = ctx.camera().getPos(); float tickDelta = MC.getRenderTickCounter().getTickProgress(true); - - renderAll(camPos, tickDelta); + renderAll(tickDelta); }); } - // === API que usan tus mixins === + // === API usada por mixins === - /** Se usa en BlockRenderManagerMixin para NO meter el modelo vanilla en el chunk mientras animamos. */ + /** Oculta el bloque vanilla mientras animamos (evita “doble puerta”). */ public static boolean shouldHideInChunk(BlockPos pos, BlockState state) { - return ANIMS.containsKey(pos.asLong()); + synchronized (ANIMS) { + if (ANIMS.containsKey(pos.asLong())) return true; + + if (state.getBlock() instanceof DoorBlock && state.contains(DoorBlock.HALF)) { + DoubleBlockHalf half = state.get(DoorBlock.HALF); + BlockPos other = (half == DoubleBlockHalf.LOWER) ? pos.up() : pos.down(); + return ANIMS.containsKey(other.asLong()); + } + + return false; + } } - /** - * Se llama desde tu WorldMixin (scheduleBlockRerenderIfNeeded). - * Arranca/actualiza animaciones. - */ + public static boolean isAnimatingAt(BlockPos pos) { + synchronized (ANIMS) { + return ANIMS.containsKey(pos.asLong()); + } + } + + /** Se llama cuando cambia el bloque (abrir/cerrar). */ public static void onBlockUpdate(BlockPos pos, BlockState oldState, BlockState newState) { if (MC.world == null) return; Kind kind = kindOf(oldState, newState); if (kind == null) { - ANIMS.remove(pos.asLong()); + synchronized (ANIMS) { + ANIMS.remove(pos.asLong()); + } return; } + if (!isKindBlock(kind, newState)) { + removeKindAt(pos, kind); + return; + } + + // DOOR: arrancamos solo desde la mitad LOWER para no duplicar. + if (kind == Kind.DOOR && newState.contains(DoorBlock.HALF)) { + if (newState.get(DoorBlock.HALF) == DoubleBlockHalf.UPPER) return; + } + boolean oldOpen = isOpen(oldState); boolean newOpen = isOpen(newState); if (oldOpen == newOpen) return; SddConfig cfg = SddConfigManager.get(); if (!isAnimationEnabled(cfg, kind)) { - // por seguridad, limpia este pos y (si es puerta) también su otra mitad - removeWithPairIfDoor(pos, newState); + removeKindAt(pos, kind); return; } float speed = speedFor(cfg, kind); if (speed <= 0.001f) speed = 1.0f; - long startNs = System.nanoTime(); + long startTick = MC.world.getTime(); if (kind == Kind.DOOR) { - // IMPORTANTÍSIMO: - // 1) animar SIEMPRE LAS 2 MITADES (upper+lower) para que no quede “puerta vanilla” encima - // 2) si cfg.connectDoors está ON, arrancar también la puerta gemela con el MISMO startNs para que vayan a la vez - startDoorAnimationWithPairs(cfg, pos, oldOpen, newOpen, speed, startNs); - return; + startDoorAnimBothHalves(pos, newState, oldOpen, newOpen, speed, startTick); + } else { + BlockState base = forceClosedModel(newState, kind); + synchronized (ANIMS) { + ANIMS.put(pos.asLong(), new Anim(pos.toImmutable(), base, kind, oldOpen, newOpen, speed, startTick)); + } + requestRerender(pos); } - - // Trapdoor / Fence gate (1 bloque) - BlockState base = forceClosedModel(newState, kind); - putOrReplaceAnim(pos, base, kind, oldOpen, newOpen, speed, startNs); - ensureRerender(pos); } // === Render === - public static void renderAll(Vec3d camPos, float tickDelta) { + private static void renderAll(float tickDelta) { if (MC.world == null || ANIMS.isEmpty()) return; - VertexConsumerProvider.Immediate consumers = MC.getBufferBuilders().getEntityVertexConsumers(); + Vec3d camPos = MC.gameRenderer.getCamera().getPos(); MatrixStack matrices = new MatrixStack(); + VertexConsumerProvider.Immediate consumers = MC.getBufferBuilders().getEntityVertexConsumers(); - long nowNs = System.nanoTime(); + long worldTick = MC.world.getTime(); + double nowTick = worldTick + (double) MathHelper.clamp(tickDelta, 0.0f, 1.0f); - for (Anim a : ANIMS.values()) { - float t = a.progress01(nowNs); + 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); } consumers.draw(); - cleanupFinished(nowNs); + cleanupFinished(nowTick); } - // === Interno === + private static void cleanupFinished(double nowTick) { + synchronized (ANIMS) { + var it = ANIMS.long2ObjectEntrySet().iterator(); + while (it.hasNext()) { + var entry = it.next(); + if (entry.getValue().isFinished(nowTick)) { + BlockPos pos = entry.getValue().pos; + it.remove(); + requestRerender(pos); + } + } + } + } private static void renderOne(Anim anim, float t, Vec3d camPos, MatrixStack matrices, VertexConsumerProvider consumers) { if (MC.world == null) return; @@ -129,7 +160,7 @@ public final class SddAnimator { matrices.push(); - // Fijado al bloque (NO a la cámara): world -> camera-relative + // Fijo en mundo: bloque - cámara matrices.translate( pos.getX() - camPos.x, pos.getY() - camPos.y, @@ -138,155 +169,45 @@ public final class SddAnimator { float eased = easeInOut(t); float angleDeg = lerpAngleDeg(anim.fromOpen, anim.toOpen, eased, anim.kind, state); + applyTransform(anim.kind, state, angleDeg, matrices); - int light = WorldRenderer.getLightmapCoordinates((BlockRenderView) MC.world, pos); + int light = WorldRenderer.getLightmapCoordinates((net.minecraft.world.BlockRenderView) MC.world, pos); MC.getBlockRenderManager().renderBlockAsEntity(state, matrices, consumers, light, OverlayTexture.DEFAULT_UV); matrices.pop(); } - private static void cleanupFinished(long nowNs) { - if (ANIMS.isEmpty() || MC.world == null) return; + // === DOOR (dos mitades) === - // Cuando termina una animación, hay que forzar rerender para que el chunk vuelva a incluir el bloque vanilla final. - ANIMS.long2ObjectEntrySet().removeIf(e -> { - Anim a = e.getValue(); - if (!a.isFinished(nowNs)) return false; + private static void startDoorAnimBothHalves(BlockPos lowerPos, BlockState lowerNewState, boolean oldOpen, boolean newOpen, float speed, long startTick) { + BlockState baseLower = forceClosedModel(lowerNewState, Kind.DOOR); - ensureRerender(a.pos); - return true; - }); - } + BlockPos upperPos = lowerPos.up(); + BlockState baseUpper = baseLower.with(DoorBlock.HALF, DoubleBlockHalf.UPPER); - private static void putOrReplaceAnim(BlockPos pos, BlockState base, Kind kind, - boolean fromOpen, boolean toOpen, - float speed, long startNs) { + Anim aLower = new Anim(lowerPos.toImmutable(), baseLower, Kind.DOOR, oldOpen, newOpen, speed, startTick); + Anim aUpper = new Anim(upperPos.toImmutable(), baseUpper, Kind.DOOR, oldOpen, newOpen, speed, startTick); - Anim anim = new Anim(pos.toImmutable(), base, kind, fromOpen, toOpen, speed, startNs); - ANIMS.put(pos.asLong(), anim); - } - - private static void removeWithPairIfDoor(BlockPos pos, BlockState state) { - ANIMS.remove(pos.asLong()); - - if (!(state.getBlock() instanceof DoorBlock) || MC.world == null) return; - BlockPos other = otherDoorHalfPos(pos, state); - if (other != null) ANIMS.remove(other.asLong()); - } - - private static void startDoorAnimationWithPairs(SddConfig cfg, BlockPos anyHalfPos, - boolean fromOpen, boolean toOpen, - float speed, long startNs) { - if (MC.world == null) return; - - // Normaliza a LOWER para encontrar gemela de doble puerta de forma estable - BlockPos lowerPos = anyHalfPos; - BlockState anyStateNow = MC.world.getBlockState(anyHalfPos); - - if (!(anyStateNow.getBlock() instanceof DoorBlock)) return; - - if (anyStateNow.get(DoorBlock.HALF) == DoubleBlockHalf.UPPER) { - lowerPos = anyHalfPos.down(); + synchronized (ANIMS) { + ANIMS.put(lowerPos.asLong(), aLower); + ANIMS.put(upperPos.asLong(), aUpper); } + requestRerender(lowerPos); + requestRerender(upperPos); + } - BlockState lowerState = MC.world.getBlockState(lowerPos); - if (!(lowerState.getBlock() instanceof DoorBlock)) { - // fallback: al menos animar la mitad recibida - putOrReplaceAnim(anyHalfPos, forceClosedModel(anyStateNow, Kind.DOOR), Kind.DOOR, fromOpen, toOpen, speed, startNs); - ensureRerender(anyHalfPos); - return; - } - - // 1) Esta puerta: lower + upper - startDoorBothHalves(lowerPos, lowerState, fromOpen, toOpen, speed, startNs); - - // 2) Doble puerta (gemela), sincronizada - if (cfg.connectDoors) { - BlockPos otherLower = findDoubleDoorOtherLower(lowerPos, lowerState); - if (otherLower != null) { - BlockState otherLowerState = MC.world.getBlockState(otherLower); - // Solo si sigue siendo puerta - if (otherLowerState.getBlock() instanceof DoorBlock) { - startDoorBothHalves(otherLower, otherLowerState, fromOpen, toOpen, speed, startNs); - - // Forzamos rerender ya, aunque la gemela cambie un tick después: evita ver la puerta vanilla encima. - ensureRerender(otherLower); - BlockPos otherUpper = otherLower.up(); - ensureRerender(otherUpper); - } + private static void removeKindAt(BlockPos pos, Kind kind) { + synchronized (ANIMS) { + ANIMS.remove(pos.asLong()); + if (kind == Kind.DOOR) { + ANIMS.remove(pos.up().asLong()); + ANIMS.remove(pos.down().asLong()); } } - - // Forzamos rerender de esta puerta ya - ensureRerender(lowerPos); - ensureRerender(lowerPos.up()); } - private static void startDoorBothHalves(BlockPos lowerPos, BlockState lowerState, - boolean fromOpen, boolean toOpen, - float speed, long startNs) { - if (MC.world == null) return; - - // LOWER - BlockState baseLower = forceClosedModel(lowerState, Kind.DOOR); - putOrReplaceAnim(lowerPos, baseLower, Kind.DOOR, fromOpen, toOpen, speed, startNs); - - // UPPER (si existe) - BlockPos upperPos = lowerPos.up(); - BlockState upperState = MC.world.getBlockState(upperPos); - if (upperState.getBlock() instanceof DoorBlock) { - BlockState baseUpper = forceClosedModel(upperState, Kind.DOOR); - putOrReplaceAnim(upperPos, baseUpper, Kind.DOOR, fromOpen, toOpen, speed, startNs); - } - } - - /** - * Encuentra la otra puerta (LOWER) de una doble puerta. - * Regla: - * - misma FACING - * - hinge opuesto - * - vecina en el lado “no bisagra” (donde se juntan) - */ - private static BlockPos findDoubleDoorOtherLower(BlockPos lowerPos, BlockState lowerState) { - if (MC.world == null) return null; - - if (!(lowerState.getBlock() instanceof DoorBlock)) return null; - if (lowerState.get(DoorBlock.HALF) != DoubleBlockHalf.LOWER) return null; - - Direction facing = lowerState.get(DoorBlock.FACING); - DoorHinge hinge = lowerState.get(DoorBlock.HINGE); - - // Si mi bisagra está a la IZQ, mi “lado de unión” es a la DCHA (clockwise), y viceversa. - Direction joinSide = (hinge == DoorHinge.LEFT) ? facing.rotateYClockwise() - : facing.rotateYCounterclockwise(); - - BlockPos otherLower = lowerPos.offset(joinSide); - BlockState other = MC.world.getBlockState(otherLower); - - if (!(other.getBlock() instanceof DoorBlock)) return null; - if (other.get(DoorBlock.HALF) != DoubleBlockHalf.LOWER) return null; - if (other.get(DoorBlock.FACING) != facing) return null; - - DoorHinge otherHinge = other.get(DoorBlock.HINGE); - if (otherHinge == hinge) return null; // deben ser opuestas - - return otherLower; - } - - private static BlockPos otherDoorHalfPos(BlockPos pos, BlockState state) { - if (!(state.getBlock() instanceof DoorBlock)) return null; - return (state.get(DoorBlock.HALF) == DoubleBlockHalf.UPPER) ? pos.down() : pos.up(); - } - - private static void ensureRerender(BlockPos pos) { - if (MC.world == null) return; - - // Esta existe (la estás mixineando). Esto fuerza al motor a reconstruir el chunk. - World w = MC.world; - BlockState s = w.getBlockState(pos); - w.scheduleBlockRerenderIfNeeded(pos, s, s); - } + // === Ángulos y transforms === private static float lerpAngleDeg(boolean fromOpen, boolean toOpen, float t, Kind kind, BlockState state) { float a = angleFor(kind, state, fromOpen); @@ -297,30 +218,14 @@ public final class SddAnimator { private static float angleFor(Kind kind, BlockState state, boolean open) { if (!open) return 0.0f; - switch (kind) { + return switch (kind) { case DOOR -> { - Direction facing = state.get(DoorBlock.FACING); DoorHinge hinge = state.get(DoorBlock.HINGE); - - // Igual que vanilla (en shapes): al abrir, la “dirección efectiva” rota ±90 según hinge. - Direction openDir = (hinge == DoorHinge.LEFT) ? facing.rotateYClockwise() - : facing.rotateYCounterclockwise(); - - float yawClosed = Direction.getHorizontalDegreesOrThrow(facing); - float yawOpen = Direction.getHorizontalDegreesOrThrow(openDir); - - return MathHelper.wrapDegrees(yawOpen - yawClosed); // +90 o -90 + yield (hinge == DoorHinge.RIGHT) ? -90.0f : 90.0f; } - case TRAPDOOR -> { - // (lo afinamos luego) 90º - return 90.0f; - } - case FENCE_GATE -> { - // (lo afinamos luego) 90º - return 90.0f; - } - } - return 0.0f; + case TRAPDOOR -> 90.0f; + case FENCE_GATE -> 90.0f; + }; } private static void applyTransform(Kind kind, BlockState state, float angleDeg, MatrixStack matrices) { @@ -331,37 +236,130 @@ public final class SddAnimator { } } + /** + * FIX del pivote: + * Estabas rotando alrededor de una “punta” (esquina). Una puerta real rota alrededor de la línea de bisagra: + * - Coordenada del eje de bisagra (hingeSide): en el BORDE del modelo (min/max del bounding box) + * - Coordenada perpendicular: en el CENTRO del modelo (centro del bounding box) + * + * Esto quita el efecto de que la animación “empiece por encima/encima” y que parezca que se desplaza. + */ private static void transformDoor(BlockState state, float angleDeg, MatrixStack matrices) { + if (MC.world == null) return; + Direction facing = state.get(DoorBlock.FACING); - DoorHinge hinge = state.get(DoorBlock.HINGE); + DoorHinge hinge = state.get(DoorBlock.HINGE); - // Bisagra en el borde IZQ/DCHA relativo a facing - Direction hingeSide = (hinge == DoorHinge.LEFT) ? facing.rotateYCounterclockwise() - : facing.rotateYClockwise(); + Direction hingeSide = (hinge == DoorHinge.RIGHT) + ? facing.rotateYClockwise() + : facing.rotateYCounterclockwise(); - float pivotX = (hingeSide == Direction.EAST) ? 1.0f : (hingeSide == Direction.WEST ? 0.0f : 0.5f); - float pivotZ = (hingeSide == Direction.SOUTH) ? 1.0f : (hingeSide == Direction.NORTH ? 0.0f : 0.5f); + Box bb = state.getOutlineShape(MC.world, BlockPos.ORIGIN, ShapeContext.absent()).getBoundingBox(); + 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.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(angleDeg)); matrices.translate(-pivotX, 0.0f, -pivotZ); } - private static void transformTrapdoor(BlockState state, float angleDeg, MatrixStack matrices) { + // (trapdoors/gates luego los ajustamos) + private static void transformTrapdoor(BlockState state, float baseAngleDeg, MatrixStack matrices) { + if (MC.world == null) return; + Direction facing = state.get(TrapdoorBlock.FACING); + BlockHalf half = state.get(TrapdoorBlock.HALF); - float pivotX = (facing == Direction.EAST) ? 1.0f : (facing == Direction.WEST ? 0.0f : 0.5f); - float pivotZ = (facing == Direction.SOUTH) ? 1.0f : (facing == Direction.NORTH ? 0.0f : 0.5f); + VoxelShape collShape = state.getCollisionShape(MC.world, BlockPos.ORIGIN, ShapeContext.absent()); + Box bb = collShape.isEmpty() + ? state.getOutlineShape(MC.world, BlockPos.ORIGIN, ShapeContext.absent()).getBoundingBox() + : collShape.getBoundingBox(); - matrices.translate(pivotX, 0.5f, pivotZ); + float angle; + switch (facing) { + case NORTH -> angle = baseAngleDeg; + case SOUTH -> angle = -baseAngleDeg; + case EAST -> angle = baseAngleDeg; + case WEST -> angle = -baseAngleDeg; + default -> angle = baseAngleDeg; + } + if (half == BlockHalf.TOP) angle = -angle; + + float pivotX = (float) ((bb.minX + bb.maxX) * 0.5); + float pivotZ = (float) ((bb.minZ + bb.maxZ) * 0.5); + Direction hingeSide = facing.getOpposite(); + 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 thickness = (float) (bb.maxY - bb.minY); + float halfT = thickness > 0.0f ? thickness * 0.5f : (3.0f / 32.0f); + + float pivotYClosed = (half == BlockHalf.TOP) ? (float) bb.maxY - halfT : (float) bb.minY + halfT; + float pivotYOpen = (half == BlockHalf.TOP) ? (float) bb.maxY : (float) bb.minY; + + float progress = MathHelper.clamp(Math.abs(angle) / 90.0f, 0.0f, 1.0f); + float pivotY = MathHelper.lerp(progress, pivotYClosed, pivotYOpen); + float pivotXInterp = pivotX; + float pivotZInterp = pivotZ; + + matrices.translate(pivotXInterp, pivotY, pivotZInterp); if (facing == Direction.NORTH || facing == Direction.SOUTH) { - matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(angleDeg)); + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(angle)); } else { - matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(-angleDeg)); + matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(angle)); } - matrices.translate(-pivotX, -0.5f, -pivotZ); + matrices.translate(-pivotXInterp, -pivotY, -pivotZInterp); } private static void transformFenceGate(BlockState state, float angleDeg, MatrixStack matrices) { @@ -370,6 +368,8 @@ public final class SddAnimator { matrices.translate(-0.5f, 0.0f, -0.5f); } + // === Utilidades === + private static BlockState forceClosedModel(BlockState s, Kind kind) { return switch (kind) { case DOOR -> s.with(DoorBlock.OPEN, false); @@ -378,6 +378,30 @@ public final class SddAnimator { }; } + 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.worldRenderer == null || MC.world == null) return; + BlockState st = MC.world.getBlockState(pos); + MC.worldRenderer.updateBlock(MC.world, pos, st, st, 0); + MC.worldRenderer.scheduleBlockRenders( + pos.getX(), pos.getY(), pos.getZ(), + pos.getX(), pos.getY(), pos.getZ() + ); + MC.worldRenderer.scheduleChunkRender( + pos.getX() >> 4, + pos.getY() >> 4, + pos.getZ() >> 4 + ); + } + private static boolean isOpen(BlockState s) { Block b = s.getBlock(); if (b instanceof DoorBlock) return s.get(DoorBlock.OPEN); @@ -404,7 +428,7 @@ public final class SddAnimator { return switch (kind) { case DOOR -> cfg.animateDoors; case TRAPDOOR -> cfg.animateTrapdoors; - case FENCE_GATE -> cfg.animateFenceGates; + case FENCE_GATE -> getBool(cfg, "animateFenceGates", false); }; } @@ -412,14 +436,43 @@ public final class SddAnimator { return switch (kind) { case DOOR -> cfg.doorSpeed; case TRAPDOOR -> cfg.trapdoorSpeed; - case FENCE_GATE -> cfg.fenceGateSpeed; + case FENCE_GATE -> getFloat(cfg, "fenceGateSpeed", 1.0f); }; } private static float easeInOut(float t) { - return t * t * (3.0f - 2.0f * t); // smoothstep + return t * t * (3.0f - 2.0f * t); } + // === 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 { @@ -428,35 +481,34 @@ public final class SddAnimator { final Kind kind; final boolean fromOpen; final boolean toOpen; - final float speed; - final long startNs; - final long durationNs; - Anim(BlockPos pos, BlockState baseState, Kind kind, - boolean fromOpen, boolean toOpen, float speed, long startNs) { + 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; - this.speed = speed; - this.startNs = startNs; - float dur = BASE_DURATION_S / Math.max(0.001f, speed); - if (dur < MIN_DURATION_S) dur = MIN_DURATION_S; - this.durationNs = (long) (dur * 1_000_000_000L); + 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(long nowNs) { - long dt = nowNs - startNs; - if (dt <= 0) return 0.0f; - if (dt >= durationNs) return 1.0f; - return (float) dt / (float) durationNs; + 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 isFinished(long nowNs) { - return nowNs - startNs >= durationNs; + boolean isFinished(double nowTick) { + return (nowTick - startTick) >= durationTicks; } } } + 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..6d69cee --- /dev/null +++ b/src/client/java/com/straice/smoothdoors/mixin/client/BlockModelRendererMixin.java @@ -0,0 +1,62 @@ +package com.straice.smoothdoors.mixin.client; + +import com.straice.smoothdoors.client.anim.SddAnimator; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.block.BlockModelRenderer; +import net.minecraft.client.render.model.BlockModelPart; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.BlockRenderView; +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 java.util.List; + +@Mixin(BlockModelRenderer.class) +public class BlockModelRendererMixin { + + @Inject( + method = "render(Lnet/minecraft/world/BlockRenderView;Ljava/util/List;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;ZI)V", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$render(BlockRenderView world, List parts, BlockState state, BlockPos pos, + MatrixStack matrices, VertexConsumer vertices, boolean cull, int overlay, + CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } + + @Inject( + method = "renderSmooth(Lnet/minecraft/world/BlockRenderView;Ljava/util/List;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;ZI)V", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$renderSmooth(BlockRenderView world, List parts, BlockState state, BlockPos pos, + MatrixStack matrices, VertexConsumer vertices, boolean cull, int overlay, + CallbackInfo ci) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + ci.cancel(); + } + } + + @Inject( + method = "renderFlat(Lnet/minecraft/world/BlockRenderView;Ljava/util/List;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;ZI)V", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$renderFlat(BlockRenderView world, List parts, BlockState state, BlockPos pos, + MatrixStack matrices, VertexConsumer vertices, boolean cull, 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 index e1b26b5..62858b8 100644 --- a/src/client/java/com/straice/smoothdoors/mixin/client/BlockRenderManagerMixin.java +++ b/src/client/java/com/straice/smoothdoors/mixin/client/BlockRenderManagerMixin.java @@ -11,6 +11,7 @@ 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; import java.util.List; @@ -20,7 +21,8 @@ public class BlockRenderManagerMixin { @Inject( method = "renderBlock(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/world/BlockRenderView;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;ZLjava/util/List;)V", at = @At("HEAD"), - cancellable = true + cancellable = true, + require = 0 ) private void sdd$renderBlock(BlockState state, BlockPos pos, BlockRenderView world, MatrixStack matrices, VertexConsumer vertexConsumer, @@ -30,4 +32,19 @@ public class BlockRenderManagerMixin { ci.cancel(); } } + + @Inject( + method = "renderBlock(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/world/BlockRenderView;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;Z)Z", + at = @At("HEAD"), + cancellable = true, + require = 0 + ) + private void sdd$renderBlock(BlockState state, BlockPos pos, BlockRenderView world, + MatrixStack matrices, VertexConsumer vertexConsumer, + boolean cull, CallbackInfoReturnable cir) { + if (SddAnimator.shouldHideInChunk(pos, state)) { + cir.setReturnValue(false); + cir.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..c9ffce5 --- /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.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.render.chunk.ChunkRendererRegion; +import net.minecraft.util.math.BlockPos; +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(ChunkRendererRegion.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.getDefaultState()); + cir.cancel(); + } + } +} diff --git a/src/client/resources/smooth-double-doors.client.mixins.json b/src/client/resources/smooth-double-doors.client.mixins.json index ece13b1..43c7c49 100644 --- a/src/client/resources/smooth-double-doors.client.mixins.json +++ b/src/client/resources/smooth-double-doors.client.mixins.json @@ -4,9 +4,11 @@ "compatibilityLevel": "JAVA_21", "client": [ "client.ClientWorldMixin", - "client.BlockRenderManagerMixin" + "client.BlockRenderManagerMixin", + "client.BlockModelRendererMixin", + "client.ChunkRendererRegionMixin" ], "injectors": { "defaultRequire": 1 } -} \ No newline at end of file +}