diff --git a/versions/1.21.1/build.gradle b/versions/1.21.1/build.gradle new file mode 100644 index 0000000..bca2b04 --- /dev/null +++ b/versions/1.21.1/build.gradle @@ -0,0 +1,96 @@ +plugins { + id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = project.archives_base_name +} + +repositories { + maven { name = "KoSmX Maven"; url = 'https://maven.kosmx.dev/' } +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + // Player Animation Library + include modImplementation( + "dev.kosmx.player-anim:player-animation-lib-fabric:${project.player_anim}" + ) + + // Optional: BendyLib + // include modRuntimeOnly( + // "io.github.kosmx.bendy-lib:bendy-lib-fabric:${project.bendylib_version}" + // ) +} + +/* + * ================================================================ + * Resource Processing + * ================================================================ + * Injects version values into fabric.mod.json. + */ +processResources { + inputs.property "version", project.version + inputs.property "minecraft_version", project.minecraft_version + inputs.property "loader_version", project.loader_version + filteringCharset "UTF-8" + + filesMatching("fabric.mod.json") { + expand( + "version": project.version, + "minecraft_version": project.minecraft_version, + "loader_version": project.loader_version + ) + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 +} + +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +jar { + inputs.property "archivesName", project.base.archivesName + + from("LICENSE") { + rename { "${it}_${inputs.properties.archivesName}"} + } +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} \ No newline at end of file diff --git a/versions/1.21.1/gradle.properties b/versions/1.21.1/gradle.properties new file mode 100644 index 0000000..01be582 --- /dev/null +++ b/versions/1.21.1/gradle.properties @@ -0,0 +1,23 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true + +# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 +org.gradle.configuration-cache=false + +# Fabric Properties +# check these on https://fabricmc.net/develop +minecraft_version=1.21.1 +yarn_mappings=1.21.1+build.3 +loader_version=0.18.4 +loom_version=1.15-SNAPSHOT + +# Mod Properties +mod_version=1.0.0 +maven_group=org.saturnclient +archives_base_name=saturn-client + +# Dependencies +fabric_api_version=0.116.9+1.21.1 + +player_anim = 2.0.4+1.21.1 diff --git a/versions/1.21.1/settings.gradle b/versions/1.21.1/settings.gradle new file mode 100644 index 0000000..75c4d72 --- /dev/null +++ b/versions/1.21.1/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/CloakFeatureRenderer.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/CloakFeatureRenderer.java new file mode 100644 index 0000000..1b5bc6a --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/CloakFeatureRenderer.java @@ -0,0 +1,411 @@ +package org.saturnclient.impl.cosmetics; + +import java.util.Arrays; + +import org.saturnclient.client.player.SaturnPlayer; +import org.saturnclient.config.Config; +import org.saturnclient.cosmetics.Cloaks; + +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.feature.FeatureRenderer; +import net.minecraft.client.render.entity.feature.FeatureRendererContext; +import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RotationAxis; +import net.minecraft.util.math.Vec3d; + +/** + * The cloak renderer + * - **Efficient**: uses a image only consisting of the cloak texture, 16x8 + * whitespace. + * - **Curving**: Curves when player velocity increases. + * - **Wavey**: Makes the cloak feel like wind, when jump sprinting. + */ +public class CloakFeatureRenderer + extends FeatureRenderer> { + private static final int PARTS = 24; + private final float[] segmentValues = new float[PARTS]; + + private static final float TEX_W = 176f; + private static final float TEX_H = 138f; + + private static final class PxRect { + final int x, y, w, h; + + PxRect(int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + + float u1() { + return x / TEX_W; + } + + float v1() { + return y / TEX_H; + } + + float u2() { + return (x + w) / TEX_W; + } + + float v2() { + return (y + h) / TEX_H; + } + } + + private static final PxRect FRONT_RECT = new PxRect(8, 8, 80, 128); + private static final PxRect BACK_RECT = new PxRect(96, 8, 80, 128); + private static final PxRect LEFT_EDGE_RECT = new PxRect(0, 8, 8, 128); + private static final PxRect RIGHT_EDGE_RECT = new PxRect(88, 8, 8, 128); + private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8); + private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8); + + public CloakFeatureRenderer( + FeatureRendererContext> featureRendererContext) { + super(featureRendererContext); + Arrays.fill(segmentValues, 0.0f); + } + + private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack, + Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft, + float u1, float v1, float u2, float v2, int light, int overlay, + float normalX, float normalY, float normalZ, boolean flipWinding) { + MatrixStack.Entry entry = matrixStack.peek(); + + if (!flipWinding) { + // CCW + vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y, + (float) bottomLeft.z) + .color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light) + .normal(normalX, normalY, normalZ); + vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y, + (float) bottomRight.z) + .color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light) + .normal(normalX, normalY, normalZ); + vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y, + (float) topRight.z) + .color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light) + .normal(normalX, normalY, normalZ); + vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y, + (float) topLeft.z) + .color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light) + .normal(normalX, normalY, normalZ); + } else { + // CW + vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y, + (float) topLeft.z) + .color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light) + .normal(normalX, normalY, normalZ); + vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y, + (float) topRight.z) + .color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light) + .normal(normalX, normalY, normalZ); + vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y, + (float) bottomRight.z) + .color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light) + .normal(normalX, normalY, normalZ); + vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y, + (float) bottomLeft.z) + .color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light) + .normal(normalX, normalY, normalZ); + } + } + + private void renderCape(MatrixStack matrixStack, VertexConsumer vertexConsumer, int light, int overlay) { + float capeWidth = 10.0f / 16.0f; + float capeHeight = 16.0f / 16.0f; + float capeDepth = 1.0f / 16.0f; + float capePartHeight = capeHeight / PARTS; + + float x1 = -capeWidth / 2.0f; + float x2 = capeWidth / 2.0f; + + final float frontU1 = FRONT_RECT.u1(); + final float frontU2 = FRONT_RECT.u2(); + final float frontV1 = FRONT_RECT.v1(); + final float frontV2 = FRONT_RECT.v2(); + final float frontPartV = (frontV2 - frontV1) / PARTS; + final float backU1 = BACK_RECT.u1(); + final float backU2 = BACK_RECT.u2(); + final float backV1 = BACK_RECT.v1(); + final float backV2 = BACK_RECT.v2(); + final float backPartV = (backV2 - backV1) / PARTS; + final float leftU1 = LEFT_EDGE_RECT.u1(); + final float leftU2 = LEFT_EDGE_RECT.u2(); + final float leftV1 = LEFT_EDGE_RECT.v1(); + final float leftV2 = LEFT_EDGE_RECT.v2(); + final float leftPartV = (leftV2 - leftV1) / PARTS; + final float rightU1 = RIGHT_EDGE_RECT.u1(); + final float rightU2 = RIGHT_EDGE_RECT.u2(); + final float rightV1 = RIGHT_EDGE_RECT.v1(); + final float rightV2 = RIGHT_EDGE_RECT.v2(); + final float rightPartV = (rightV2 - rightV1) / PARTS; + final float topU1 = TOP_RECT.u1(); + final float topU2 = TOP_RECT.u2(); + final float topV1 = TOP_RECT.v1(); + final float topV2 = TOP_RECT.v2(); + final float bottomU1 = BOTTOM_RECT.u1(); + final float bottomU2 = BOTTOM_RECT.u2(); + final float bottomV1 = BOTTOM_RECT.v1(); + final float bottomV2 = BOTTOM_RECT.v2(); + + float curY = 0.0f; + float curZ = 0.0f; + + for (int i = 0; i < PARTS; i++) { + float angle = (2.0f - segmentValues[i]) * ((float) Math.PI / 2f); + float dirY = -(float) Math.cos(angle); + float dirZ = (float) Math.sin(angle); + + // 1. Thickness at the START of this segment (Must match PREVIOUS segment's end) + float prevAngle = (i > 0) ? (2.0f - segmentValues[i - 1]) * ((float) Math.PI / 2f) : angle; + float startAvgAngle = (angle + prevAngle) / 2.0f; + float thickYStart = (float) Math.sin(startAvgAngle) * capeDepth; + float thickZStart = (float) Math.cos(startAvgAngle) * capeDepth; + + // 2. Thickness at the END of this segment (Must match NEXT segment's start) + float nextAngle = (i < PARTS - 1) ? (2.0f - segmentValues[i + 1]) * ((float) Math.PI / 2f) : angle; + float endAvgAngle = (angle + nextAngle) / 2.0f; + float thickYEnd = (float) Math.sin(endAvgAngle) * capeDepth; + float thickZEnd = (float) Math.cos(endAvgAngle) * capeDepth; + + float nextY = curY + dirY * capePartHeight; + float nextZ = curZ + dirZ * capePartHeight; + + // Inner Vertices (The spine of the cape) + Vec3d innerTopLeft = new Vec3d(x2, curY, curZ); + Vec3d innerTopRight = new Vec3d(x1, curY, curZ); + Vec3d innerBotLeft = new Vec3d(x2, nextY, nextZ); + Vec3d innerBotRight = new Vec3d(x1, nextY, nextZ); + + // Outer Vertices (Offset by the specific joint thickness) + Vec3d outerTopLeft = new Vec3d(x2, curY - thickYStart, curZ - thickZStart); + Vec3d outerTopRight = new Vec3d(x1, curY - thickYStart, curZ - thickZStart); + Vec3d outerBotLeft = new Vec3d(x2, nextY - thickYEnd, nextZ - thickZEnd); + Vec3d outerBotRight = new Vec3d(x1, nextY - thickYEnd, nextZ - thickZEnd); + + // FRONT (facing camera) + this.renderCapeQuad(vertexConsumer, matrixStack, + outerBotRight, outerBotLeft, outerTopLeft, outerTopRight, + frontU2, frontV1 + (frontPartV * i), frontU1, frontV1 + (frontPartV * (i + 1)), + light, overlay, 0, 0, 1, false); + + // BACK (Facing player) + this.renderCapeQuad(vertexConsumer, matrixStack, + innerBotLeft, innerBotRight, innerTopRight, innerTopLeft, + backU2, backV1 + (backPartV * i), backU1, backV1 + (backPartV * (i + 1)), + light, overlay, 0, 0, -1, false); + + // LEFT EDGE + this.renderCapeQuad(vertexConsumer, matrixStack, + innerBotLeft, outerBotLeft, outerTopLeft, innerTopLeft, + leftU1, leftV1 + (leftPartV * i), leftU2, leftV1 + (leftPartV * (i + 1)), + light, overlay, 1, 0, 0, false); + + // RIGHT EDGE + this.renderCapeQuad(vertexConsumer, matrixStack, + outerBotRight, innerBotRight, innerTopRight, outerTopRight, + rightU1, rightV1 + (rightPartV * i), rightU2, rightV1 + (rightPartV * (i + 1)), + light, overlay, -1, 0, 0, false); + + // TOP FACE (Only on first segment) + if (i == 0) { + this.renderCapeQuad(vertexConsumer, matrixStack, + outerTopLeft, outerTopRight, innerTopRight, innerTopLeft, + topU1, topV1, topU2, topV2, light, overlay, 0, 1, 0, false); + } + + // BOTTOM FACE (Only on last segment) + if (i == PARTS - 1) { + this.renderCapeQuad(vertexConsumer, matrixStack, + innerBotLeft, innerBotRight, outerBotRight, outerBotLeft, + bottomU1, bottomV1, bottomU2, bottomV2, light, overlay, 0, -1, 0, false); + } + + curY = nextY; + curZ = nextZ; + } + } + + // Shift array left and insert new value at the start + private void updateSegmentValues(float value) { + // Shift all elements one position to the right (from end to start) + for (int i = segmentValues.length - 1; i > 0; i--) { + segmentValues[i] = segmentValues[i - 1]; + } + + // Insert the new value at index 0 + segmentValues[0] = value; + } + + private long lastUpdate = 0; + + @Override + public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, + AbstractClientPlayerEntity entity, float limbAngle, float limbDistance, float tickDelta, + float animationProgress, float headYaw, float headPitch) { + if (entity.isInvisible() || entity.getSkinTextures().capeTexture() != null) { + return; + } + + SaturnPlayer player = SaturnPlayer.get(entity.getUuid()); + + if (player == null) { + return; + } + + ItemStack chestStack = entity.getInventory().armor.get(1); + + Identifier customCape = (Identifier) (Object) Cloaks.getCurrentCloakTexture(player.uuid); + if (customCape == null || chestStack.itemMatches(Items.ELYTRA.getRegistryEntry())) { + return; + } + + int minBrightness = 7; + int blockLight = (light >> 4) & 0xF; + int skyLight = (light >> 20) & 0xF; + blockLight = Math.max(blockLight, minBrightness); + skyLight = Math.max(skyLight, minBrightness); + light = (skyLight << 20) | (blockLight << 4); + + VertexConsumer vertexConsumer = vertexConsumers.getBuffer(RenderLayer.getEntityAlpha(customCape)); + + matrices.push(); + + matrices.translate(0.0f, 0.0f, 0.12f); + + if (!chestStack.isEmpty()) { + matrices.translate(0.0F, -0.053125F, 0.06875F); + } + + if (entity.isInSneakingPose()) { + matrices.translate(0, 0.16f, 0.0f); + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(29.0f)); + } + + long now = System.currentTimeMillis(); + if (now - lastUpdate >= 20) { + float velX = (float) (MathHelper.lerp((double) tickDelta, entity.prevCapeX, + entity.capeX) + - MathHelper.lerp((double) tickDelta, entity.prevX, entity.getX())); + float velZ = (float) (MathHelper.lerp((double) tickDelta, entity.prevCapeZ, + entity.capeZ) + - MathHelper.lerp((double) tickDelta, entity.prevZ, entity.getZ())); + float rawVelY = (float) (MathHelper.lerp((double) tickDelta, entity.prevCapeY, entity.capeY) + - MathHelper.lerp((double) tickDelta, entity.prevY, entity.getY())); + + float velHorizontal = Math.min(0.6f, velZ > velX ? velZ : velX); + float velY = (rawVelY > 0.25f ? rawVelY : 0.0f); + + float value = entity.isSwimming() ? 0.0f : Math.max(0.0f, Math.min(2.0f, velHorizontal + velY)); + + if (Config.cloakPhysics.value) { + updateSegmentValues(value); + } else { + float last = segmentValues[0]; + float maxStep = 0.02f; + float next = value; + + if (next > last) + next = Math.min(next, last + maxStep); + else if (next < last) + next = Math.max(next, last - maxStep); + + Arrays.fill(segmentValues, next); + } + lastUpdate = now; + } + + renderCape(matrices, vertexConsumer, light, OverlayTexture.DEFAULT_UV); + + matrices.pop(); + } + + // public void render(MatrixStack matrixStack, VertexConsumerProvider + // vertexConsumerProvider, int light, + // PlayerEntityRenderState playerEntityRenderState, float f, float g) { + // if (playerEntityRenderState.invisible || !playerEntityRenderState.capeVisible + // || playerEntityRenderState.skinTextures.capeTexture() != null) { + // return; + // } + + // SaturnPlayer player = SaturnPlayer.get(playerEntityRenderState.name); + + // if (player == null) { + // return; + // } + + // Identifier customCape = (Identifier) (Object) + // Cloaks.getCurrentCloakTexture(player.uuid); + // if (customCape == null + // || this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, + // LayerType.WINGS)) { + // return; + // } + + // int minBrightness = 7; + // int blockLight = (light >> 4) & 0xF; + // int skyLight = (light >> 20) & 0xF; + // blockLight = Math.max(blockLight, minBrightness); + // skyLight = Math.max(skyLight, minBrightness); + // light = (skyLight << 20) | (blockLight << 4); + + // VertexConsumer vertexConsumer = vertexConsumerProvider + // .getBuffer(RenderLayer.getEntityAlpha(customCape)); + + // matrixStack.push(); + + // matrixStack.translate(0.0f, 0.0f, 0.12f); + + // if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, + // LayerType.HUMANOID)) { + // matrixStack.translate(0.0F, -0.053125F, 0.06875F); + // } + + // if (playerEntityRenderState.isInSneakingPose) { + // matrixStack.translate(0, 0.16f, 0.0f); + // matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(29.0f)); + // } + + // long now = System.currentTimeMillis(); + // if (now - lastUpdate >= 20) { + // float velX = Math.min(1.0f, playerEntityRenderState.field_53537 / 108.0f); + // float rawVelY = playerEntityRenderState.field_53536; + // float velY = (rawVelY > 4.0f ? rawVelY : 0.0f) / 16; + + // float value = playerEntityRenderState.isSwimming ? 0.0f : velX + velY; + + // if (Config.cloakPhysics.value) { + // updateSegmentValues(value); + // } else { + // float last = segmentValues[0]; + // float maxStep = 0.02f; + // float next = value; + + // if (next > last) + // next = Math.min(next, last + maxStep); + // else if (next < last) + // next = Math.max(next, last - maxStep); + + // Arrays.fill(segmentValues, next); + // } + // lastUpdate = now; + // } + + // renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light, + // OverlayTexture.DEFAULT_UV); + + // matrixStack.pop(); + // } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/HatFeatureRenderer.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/HatFeatureRenderer.java new file mode 100644 index 0000000..a9b2b5d --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/HatFeatureRenderer.java @@ -0,0 +1,43 @@ +package org.saturnclient.impl.cosmetics; + +import org.saturnclient.client.player.SaturnPlayer; +import org.saturnclient.impl.cosmetics.obj.MtlLoader; +import org.saturnclient.impl.cosmetics.obj.ObjModel; + +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.feature.FeatureRenderer; +import net.minecraft.client.render.entity.feature.FeatureRendererContext; +import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.client.util.math.MatrixStack; + +public class HatFeatureRenderer + extends FeatureRenderer> { + + public HatFeatureRenderer( + FeatureRendererContext> featureRendererContext) { + super(featureRendererContext); + } + + @Override + public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, + AbstractClientPlayerEntity entity, float limbAngle, float limbDistance, float tickDelta, + float animationProgress, float headYaw, float headPitch) { + SaturnPlayer player = SaturnPlayer.get(entity.getUuid()); + + if (entity.isInvisible() || player == null || player.hat.isEmpty()) { + return; + } + + matrices.push(); + + this.getContextModel().head.rotate(matrices); + + ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat), + matrices, vertexConsumers, light, + OverlayTexture.DEFAULT_UV); + + matrices.pop(); + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/MtlLoader.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/MtlLoader.java new file mode 100644 index 0000000..11b06d3 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/MtlLoader.java @@ -0,0 +1,56 @@ +package org.saturnclient.impl.cosmetics.obj; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import de.javagl.obj.*; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.Identifier; + +public class MtlLoader { + static Map> loadedMtls = new HashMap<>(); + + static Map loadMtl(Identifier mtlId) throws IOException { + try (InputStream is = MinecraftClient.getInstance().getResourceManager() + .getResource(mtlId).get().getInputStream()) { + List mtlList = MtlReader.read(is); + Map mtlMap = new java.util.LinkedHashMap<>(); + for (Mtl mtl : mtlList) { + mtlMap.put(mtl.getName(), mtl); + } + return mtlMap; + } + } + + public static Map of(Identifier mtlId) { + if (loadedMtls.containsKey(mtlId)) + return loadedMtls.get(mtlId); + + try { + Map mtl = loadMtl(mtlId); + loadedMtls.put(mtlId, mtl); + + return mtl; + } catch (IOException e) { + e.printStackTrace(); + + return null; + } + + } + + public static Map cosmetic(String cosmeticId) { + int index = cosmeticId.indexOf('_'); + + String mtlId = (index == -1) ? "" : cosmeticId.substring(index + 1); + + if (mtlId.isEmpty()) + mtlId = "white"; + + return of(Identifier.of("saturnclient", "models/cosmetic/materials/" + mtlId + ".mtl")); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/ObjModel.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/ObjModel.java new file mode 100644 index 0000000..3e7237e --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/ObjModel.java @@ -0,0 +1,102 @@ +package org.saturnclient.impl.cosmetics.obj; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.javagl.obj.Mtl; +import de.javagl.obj.Obj; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; + +public class ObjModel { + private static final ObjectMapper MAPPER = new ObjectMapper(); + public static final Map loadedObjModels = new HashMap<>(); + + public record Config( + @JsonProperty("translate") Vec3d transform, + @JsonProperty("scale") Vec3d scale, + @JsonProperty("rotation") Vec3d rotation) { + + public Config { + transform = transform == null ? new Vec3d(0, 0, 0) : transform; + scale = scale == null ? new Vec3d(1, 1, 1) : scale; + rotation = rotation == null ? new Vec3d(0, 0, 0) : rotation; + } + + @JsonFormat(shape = JsonFormat.Shape.ARRAY) + public record Vec3d(float x, float y, float z) { + } + } + + Obj obj; + Config config; + + public static ObjModel cosmetic(String cosmeticKind, String id) { + return of(Identifier.of("saturnclient", "models/cosmetic/" + cosmeticKind + "/" + id.split("_")[0] + "/model")); + } + + public static ObjModel of(Identifier obj) { + if (loadedObjModels.containsKey(obj)) + return loadedObjModels.get(obj); + + ObjModel o = new ObjModel(obj); + loadedObjModels.put(obj, o); + return o; + } + + ObjModel(Identifier identifier) { + Identifier objId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".obj"); + Identifier jsonId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".json"); + + try { + this.obj = ObjRenderer.loadObj(objId); + } catch (IOException e) { + e.printStackTrace(); + } + + // Try loading JSON config if it exists + ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager(); + Optional resource = resourceManager.getResource(jsonId); + + if (resource.isPresent()) { + try (InputStream stream = resource.get().getInputStream()) { + this.config = MAPPER.readValue(stream, Config.class); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public void render(Map mtlMap, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, + int overlay) { + matrices.push(); + + matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f)); + matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f)); + + if (this.config != null) { + matrices.translate(config.transform.x, config.transform.y, config.transform.z); + + matrices.scale(config.scale.x, config.scale.y, config.scale.z); + + matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(config.rotation.x)); + matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(config.rotation.y)); + matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Z.rotationDegrees(config.rotation.z)); + } + + ObjRenderer.renderObj(this.obj, mtlMap, matrices, vertexConsumers, light, overlay); + + matrices.pop(); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/ObjRenderer.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/ObjRenderer.java new file mode 100644 index 0000000..27635a2 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/obj/ObjRenderer.java @@ -0,0 +1,111 @@ +package org.saturnclient.impl.cosmetics.obj; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import de.javagl.obj.*; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; + +public class ObjRenderer { + public static Obj loadObj(Identifier objId) throws IOException { + try (InputStream is = MinecraftClient.getInstance().getResourceManager() + .getResource(objId).get().getInputStream()) { + return ObjReader.read(is); + } + } + + public static void renderObj(Obj obj, Map mtlMap, MatrixStack matrices, + VertexConsumerProvider vertexConsumers, int light, int overlay) { + MatrixStack.Entry entry = matrices.peek(); + + Map materialGroups = ObjSplitting.splitByMaterialGroups(obj); + + for (Map.Entry group : materialGroups.entrySet()) { + String materialName = group.getKey(); + Obj groupObj = group.getValue(); + + Mtl mtl = mtlMap != null ? mtlMap.get(materialName) : null; + + // Resolve texture identifier using same logic as ObjUnbakedModelModel + Identifier texture = Identifier.of("minecraft:textures/misc/white.png"); + + if (mtl != null) { + String mapKd = mtl.getMapKd(); + if (mapKd != null) { + // "#key" references are not resolvable at runtime without a texture map, + // so we only support direct resource location strings here + if (!mapKd.startsWith("#")) { + texture = Identifier.of(mapKd); + } + } + } + + // Diffuse color (Kd) + float r = 1f, g = 1f, b = 1f; + if (mtl != null) { + FloatTuple kd = mtl.getKd(); + if (kd != null) { + r = kd.getX(); + g = kd.getY(); + b = kd.getZ(); + } + } + + VertexConsumer consumer = vertexConsumers.getBuffer(RenderLayer.getEntityAlpha(texture)); + + int faces = groupObj.getNumFaces(); + final float fr = r, fg = g, fb = b; + + for (int f = 0; f < faces; f++) { + ObjFace face = groupObj.getFace(f); + int verts = face.getNumVertices(); + + // Your original winding order logic, untouched + for (int i = verts - 1; i >= 0; i--) { + writeVertex(groupObj, face, i, consumer, entry, light, overlay, fr, fg, fb); + } + } + } + } + + private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer, + MatrixStack.Entry matrix, int light, int overlay, + float r, float g, float b) { + + FloatTuple pos = obj.getVertex(face.getVertexIndex(index)); + float x = pos.getX(); + float y = pos.getY(); + float z = pos.getZ(); + + float nx = 0f, ny = 1f, nz = 0f; + int normalIndex = face.getNormalIndex(index); + if (normalIndex >= 0) { + FloatTuple n = obj.getNormal(normalIndex); + nx = n.getX(); + ny = n.getY(); + nz = n.getZ(); + } + + float u = 0f, v = 0f; + int texIndex = face.getTexCoordIndex(index); + if (texIndex >= 0) { + FloatTuple tc = obj.getTexCoord(texIndex); + u = tc.getX(); + v = 1f - tc.getY(); // Flip V: OBJ bottom-left origin -> Minecraft top-left + } + + consumer.vertex(matrix, x, y, z) + .color(r, g, b, 1f) + .texture(u, v) + .overlay(overlay) + .light(light) + .normal(matrix, nx, ny, nz); + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/utils/IdentifierUtils.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/utils/IdentifierUtils.java new file mode 100644 index 0000000..7da48e6 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/cosmetics/utils/IdentifierUtils.java @@ -0,0 +1,70 @@ +package org.saturnclient.impl.cosmetics.utils; + +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.saturnclient.SaturnClient; + +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.util.Identifier; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.Map; + +public class IdentifierUtils { + + public static void registerBufferedImageTextureFast(IdentifierRef identifierRef, BufferedImage bufferedImage) { + try { + Identifier identifier = (Identifier) (Object) identifierRef; + + int width = bufferedImage.getWidth(); + int height = bufferedImage.getHeight(); + + NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false); + + if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) { + int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); + + int index = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + nativeImage.setColor(x, y, pixels[index++]); + } + } + + } else { + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + nativeImage.setColor(x, y, bufferedImage.getRGB(x, y)); + } + } + + } + + NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage); + + SaturnClient.client.execute(() -> + SaturnClient.client.getTextureManager().registerTexture(identifier, texture) + ); + + } catch (Exception e) { + SaturnClient.LOGGER.error("Failed to register texture: {}", identifierRef, e); + } + } + + public static void registerBufferedImageTexturesBulk(Map textures) { + + long start = System.currentTimeMillis(); + + textures.forEach(IdentifierUtils::registerBufferedImageTextureFast); + + long end = System.currentTimeMillis(); + + SaturnClient.LOGGER.info( + "Registered {} textures in {}ms", + textures.size(), + (end - start) + ); + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/IdentifierMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/IdentifierMixin.java new file mode 100644 index 0000000..edfcdb9 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/IdentifierMixin.java @@ -0,0 +1,13 @@ +package org.saturnclient.impl.mixins; + +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.minecraft.util.Identifier; + +@Mixin(Identifier.class) +public abstract class IdentifierMixin implements IdentifierRef { + @Shadow + public abstract String toString(); +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/ItemStackMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/ItemStackMixin.java new file mode 100644 index 0000000..9a69341 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/ItemStackMixin.java @@ -0,0 +1,19 @@ +package org.saturnclient.impl.mixins; + +import org.saturnclient.common.ref.game.ItemStackRef; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.minecraft.item.ItemStack; + +@Mixin(ItemStack.class) +public abstract class ItemStackMixin implements ItemStackRef { + @Shadow + public abstract boolean isEmpty(); + + @Shadow + public abstract int getMaxDamage(); + + @Shadow + public abstract int getDamage(); +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/MatrixStackMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/MatrixStackMixin.java new file mode 100644 index 0000000..d8e88a4 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/MatrixStackMixin.java @@ -0,0 +1,59 @@ +package org.saturnclient.impl.mixins; + +import org.joml.Quaternionf; +import org.saturnclient.common.ref.render.MatrixStackRef; +import org.saturnclient.common.ref.render.QuaternionfRef; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.gen.Invoker; + +import net.minecraft.client.util.math.MatrixStack; + +@Mixin(MatrixStack.class) +public abstract class MatrixStackMixin implements MatrixStackRef { + + @Shadow + public abstract void translate(double x, double y, double z); + + @Shadow + public abstract void translate(float x, float y, float z); + + @Shadow + public abstract void scale(float x, float y, float z); + + @Shadow + public abstract void multiply(Quaternionf quaternion); + + @Shadow + public abstract void multiply(Quaternionf quaternion, float originX, float originY, float originZ); + + @Shadow + public abstract void push(); + + @Shadow + public abstract void pop(); + + @Invoker("peek") + public abstract MatrixStack.Entry peekM(); + + @Shadow + public abstract boolean isEmpty(); + + @Shadow + public abstract void loadIdentity(); + + @Override + public void multiply(QuaternionfRef quaternion) { + this.multiply((Quaternionf) quaternion); + } + + @Override + public void multiply(QuaternionfRef quaternion, float originX, float originY, float originZ) { + this.multiply((Quaternionf) quaternion, originX, originY, originZ); + } + + @Override + public Object peek() { + return this.peekM(); + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/MinecraftClientMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/MinecraftClientMixin.java new file mode 100644 index 0000000..328917d --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/MinecraftClientMixin.java @@ -0,0 +1,130 @@ +package org.saturnclient.impl.mixins; + +import java.io.File; +import java.io.InputStream; +import java.util.UUID; + +import org.jetbrains.annotations.Nullable; +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.common.ref.game.MinecraftClientRef; +import org.saturnclient.common.ref.render.WindowRef; +import org.saturnclient.impl.ui.SaturnScreenFabric; +import org.saturnclient.ui.SaturnScreen; +import org.saturnclient.ui.screens.TitleMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; +import net.minecraft.client.gui.screen.option.OptionsScreen; +import net.minecraft.client.gui.screen.world.SelectWorldScreen; +import net.minecraft.client.option.GameOptions; +import net.minecraft.client.session.Session; +import net.minecraft.client.util.Window; +import net.minecraft.resource.ReloadableResourceManagerImpl; +import net.minecraft.util.Identifier; + +@Mixin(MinecraftClient.class) +public abstract class MinecraftClientMixin implements MinecraftClientRef { + @Shadow + public File runDirectory; + + @Shadow + private Window window; + + @Shadow + private ReloadableResourceManagerImpl resourceManager; + + @Shadow + public GameOptions options; + + @Shadow + private Session session; + + @Shadow + public abstract void setScreen(@Nullable Screen screen); + + @Shadow + @Override + public abstract void scheduleStop(); + + @Override + public File getRunDirectory() { + return runDirectory; + } + + @Override + public InputStream getResource(IdentifierRef identifier) { + try { + return resourceManager.getResource((Identifier) (Object) identifier).get().getInputStream(); + } catch (Exception e) { + return null; + } + } + + @Override + public WindowRef getWindow() { + return (WindowRef) (Object) this.window; + } + + @Override + public void setScreen(SaturnScreen screen) { + this.setScreen(new SaturnScreenFabric(screen)); + } + + @Override + public void setScreen(MinecraftScreen screen) { + SaturnScreenFabric exitTarget = new SaturnScreenFabric(new TitleMenu()); + + switch (screen) { + case SelectWorld: + this.setScreen(new SelectWorldScreen(exitTarget)); + break; + + case Multiplayer: + this.setScreen(new MultiplayerScreen(exitTarget)); + break; + + case Options: + this.setScreen(new OptionsScreen(exitTarget, this.options)); + break; + } + } + + @Override + public String getAccessToken() { + if (session == null) { + return null; + } + + return this.session.getAccessToken(); + } + + @Override + public UUID getUuid() { + if (session == null) { + return null; + } + return this.session.getUuidOrNull(); + } + + @Override + public String getUsername() { + if (session == null) { + return null; + } + return this.session.getUsername(); + } + + @Override + public void onClientStopping(Runnable handler) { + ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> handler.run()); + } + + @Override + public void executeOnThread(Runnable runnable) { + ((java.util.concurrent.Executor) (Object) this).execute(runnable); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/QuaternionfMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/QuaternionfMixin.java new file mode 100644 index 0000000..628e433 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/QuaternionfMixin.java @@ -0,0 +1,9 @@ +package org.saturnclient.impl.mixins; + +import org.joml.Quaternionf; +import org.saturnclient.common.ref.render.QuaternionfRef; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(Quaternionf.class) +public abstract class QuaternionfMixin implements QuaternionfRef { +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/SpriteMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/SpriteMixin.java new file mode 100644 index 0000000..6cf07bc --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/SpriteMixin.java @@ -0,0 +1,9 @@ +package org.saturnclient.impl.mixins; + +import net.minecraft.client.texture.Sprite; +import org.saturnclient.common.ref.asset.SpriteRef; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(Sprite.class) +public abstract class SpriteMixin implements SpriteRef { +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/TextMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/TextMixin.java new file mode 100644 index 0000000..3e1a8b8 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/TextMixin.java @@ -0,0 +1,24 @@ +package org.saturnclient.impl.mixins; + +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.common.ref.render.TextRef; +import org.saturnclient.saturnclient.SaturnClient; +import org.spongepowered.asm.mixin.Mixin; + +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +@Mixin(Text.class) +public interface TextMixin extends TextRef { + + @Override + public default TextRef withFont(IdentifierRef font) { + return (TextRef) (((Text) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font))); + } + + @Override + public default int getWidth() { + return SaturnClient.client.textRenderer.getWidth((Text) this); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/WindowMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/WindowMixin.java new file mode 100644 index 0000000..5629068 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/mixins/WindowMixin.java @@ -0,0 +1,22 @@ +package org.saturnclient.impl.mixins; + +import org.saturnclient.common.ref.render.WindowRef; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import net.minecraft.client.util.Window; + +@Mixin(Window.class) +public abstract class WindowMixin implements WindowRef { + @Shadow + public abstract int getWidth(); + + @Shadow + public abstract int getHeight(); + + @Shadow + public abstract int getFramebufferWidth(); + + @Shadow + public abstract int getFramebufferHeight(); +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/EntityModuleFabric.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/EntityModuleFabric.java new file mode 100644 index 0000000..aa42d4f --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/EntityModuleFabric.java @@ -0,0 +1,80 @@ +package org.saturnclient.impl.modules; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.entity.LivingEntity; + +import org.saturnclient.common.module.EntityModule; + + +/** + * Fabric implementation of {@link EntityModule}. + * + * Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}. + * Entity state for nametag rendering is supplied at render time by + * {@link org.saturnclient.modules.mixins.LivingEntityRendererMixin} which + * writes into the render state; {@link NametagsMixin} then constructs + * a {@link RenderStateEntityState} from the already-extracted snapshot. + */ +public class EntityModuleFabric implements EntityModule { + + private final MinecraftClient mc; + + public EntityModuleFabric(MinecraftClient mc) { + this.mc = mc; + } + + @Override + public boolean isTargetingLivingEntity() { + HitResult target = mc.crosshairTarget; + if (target == null || target.getType() != HitResult.Type.ENTITY) + return false; + Entity hit = ((EntityHitResult) target).getEntity(); + return hit instanceof LivingEntity; + } + + // --------------------------------------------------------------- + // Render-state adapter + // --------------------------------------------------------------- + + /** + * Wraps a {@link HealthRenderState} (already injected into the + * Minecraft render-state object) as a platform-neutral + * {@link EntityState} for use by {@link NametagsFeature}. + * + * This is constructed inside {@link NametagsMixin} where both the + * render-state mixin interface and the entity name are available. + */ + public static final class RenderStateEntityState implements EntityState { + + private final String customName; + private final HealthRenderState hrs; + + public RenderStateEntityState(String customName, HealthRenderState hrs) { + this.customName = customName; + this.hrs = hrs; + } + + @Override + public String getCustomName() { + return customName; + } + + @Override + public float getHealth() { + return hrs.saturn$getHealth(); + } + + @Override + public float getMaxHealth() { + return hrs.saturn$getMaxHealth(); + } + + @Override + public EntityType getEntityType() { + return hrs.saturn$getEntityType(); + } + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/HealthRenderState.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/HealthRenderState.java new file mode 100644 index 0000000..d5a063b --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/HealthRenderState.java @@ -0,0 +1,20 @@ +package org.saturnclient.impl.modules; + +import org.saturnclient.common.module.EntityModule; + +/** + * Injected interface on {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}. + * + * Carries the entity's health snapshot and classification into the render + * pipeline so that {@link NametagsMixin} can read them without touching + * the live entity again. + */ +public interface HealthRenderState { + + float saturn$getHealth(); + float saturn$getMaxHealth(); + void saturn$setHealth(float health, float maxHealth); + + EntityModule.EntityType saturn$getEntityType(); + void saturn$setEntityType(EntityModule.EntityType type); +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/NetworkModuleFabric.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/NetworkModuleFabric.java new file mode 100644 index 0000000..61b1b9c --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/NetworkModuleFabric.java @@ -0,0 +1,37 @@ +package org.saturnclient.impl.modules; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.PlayerListEntry; + +import org.saturnclient.common.module.NetworkModule; + +/** + * Fabric implementation of {@link NetworkModule}. + * + * Connection presence is checked via the network handler. + * Ping is read from the player's {@link PlayerListEntry} which the + * server populates; returns {@code -1} when unavailable. + */ +public class NetworkModuleFabric implements NetworkModule { + + private final MinecraftClient mc; + + public NetworkModuleFabric(MinecraftClient mc) { + this.mc = mc; + } + + @Override + public boolean hasNetwork() { + return mc.getNetworkHandler() != null; + } + + @Override + public int getPing() { + if (mc.player == null || mc.getNetworkHandler() == null) + return -1; + + PlayerListEntry entry = mc.getNetworkHandler().getPlayerListEntry(mc.player.getUuid()); + + return entry != null ? entry.getLatency() : -1; + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/PlayerModuleFabric.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/PlayerModuleFabric.java new file mode 100644 index 0000000..68be4b9 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/PlayerModuleFabric.java @@ -0,0 +1,302 @@ +package org.saturnclient.impl.modules; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +import org.saturnclient.common.module.PlayerModule; +import org.saturnclient.common.ref.asset.SpriteRef; +import org.saturnclient.common.ref.game.ItemStackRef; +import org.saturnclient.feature.features.StatusEffectsFeature; +import org.saturnclient.saturnclient.SaturnClient; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Fabric implementation of {@link PlayerModule}. + * + * All access to {@link MinecraftClient} and {@link ClientPlayerEntity} + * is encapsulated here. Features only ever see the {@link PlayerModule} + * interface; they never import Minecraft classes directly. + * + * Every method guards against a missing player and returns a safe + * default so features are free of null checks. + */ +public class PlayerModuleFabric implements PlayerModule { + + private final MinecraftClient mc; + + public PlayerModuleFabric(MinecraftClient mc) { + this.mc = mc; + } + + // --------------------------------------------------------------- + // Internal helper — never returns null + // --------------------------------------------------------------- + + /** Returns the local player, or {@code null} if one is not loaded. */ + private ClientPlayerEntity player() { + return mc.player; + } + + // --------------------------------------------------------------- + // Presence + // --------------------------------------------------------------- + + @Override + public boolean hasPlayer() { + return mc.player != null; + } + + // --------------------------------------------------------------- + // Position + // --------------------------------------------------------------- + + @Override + public int getX() { + return hasPlayer() ? (int) player().getX() : 0; + } + + @Override + public int getY() { + return hasPlayer() ? (int) player().getY() : 0; + } + + @Override + public int getZ() { + return hasPlayer() ? (int) player().getZ() : 0; + } + + // --------------------------------------------------------------- + // Input / movement + // --------------------------------------------------------------- + + @Override + public boolean isForwardPressed() { + return mc.options.forwardKey.isPressed(); + } + + @Override + public boolean isBackPressed() { + return mc.options.backKey.isPressed(); + } + + @Override + public boolean isLeftPressed() { + return mc.options.leftKey.isPressed(); + } + + @Override + public boolean isRightPressed() { + return mc.options.rightKey.isPressed(); + } + + @Override + public boolean isJumpPressed() { + return mc.options.jumpKey.isPressed(); + } + + @Override + public boolean isAttackPressed() { + return mc.options.attackKey.isPressed(); + } + + @Override + public boolean isUsePressed() { + return mc.options.useKey.isPressed(); + } + + @Override + public boolean isSneaking() { + return hasPlayer() && player().isSneaking(); + } + + @Override + public boolean isUsingItem() { + return hasPlayer() && player().isUsingItem(); + } + + @Override + public boolean hasHorizontalCollision() { + return hasPlayer() && player().horizontalCollision; + } + + @Override + public boolean isOnGround() { + return hasPlayer() && player().isOnGround(); + } + + @Override + public void setSprinting(boolean sprinting) { + if (hasPlayer()) + player().setSprinting(sprinting); + } + + // --------------------------------------------------------------- + // Velocity + // --------------------------------------------------------------- + + @Override + public Velocity getVelocity() { + if (!hasPlayer()) + return Velocity.ZERO; + var v = player().getVelocity(); + return new Velocity(v.x, v.y, v.z); + } + + // --------------------------------------------------------------- + // Stats + // --------------------------------------------------------------- + + @Override + public float getHealth() { + return hasPlayer() ? player().getHealth() : 0f; + } + + // --------------------------------------------------------------- + // Equipment + // --------------------------------------------------------------- + + @Override + public ItemStackRef getMainHand() { + return wrap(hasPlayer() ? player().getMainHandStack() : ItemStack.EMPTY); + } + + @Override + public ItemStackRef getHelmet() { + return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY); + } + + @Override + public ItemStackRef getChestplate() { + return wrap(hasPlayer() ? player().getInventory().armor.get(2) : ItemStack.EMPTY); + } + + @Override + public ItemStackRef getLeggings() { + return wrap(hasPlayer() ? player().getInventory().armor.get(1) : ItemStack.EMPTY); + } + + @Override + public ItemStackRef getBoots() { + return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY); + } + + // --------------------------------------------------------------- + // Dummy equipment + // --------------------------------------------------------------- + + @Override + public ItemStackRef getDummyMainHand() { + return wrap(new ItemStack(Items.DIAMOND_SWORD)); + } + + @Override + public ItemStackRef getDummyHelmet() { + return wrap(new ItemStack(Items.DIAMOND_HELMET)); + } + + @Override + public ItemStackRef getDummyChestplate() { + return wrap(new ItemStack(Items.DIAMOND_CHESTPLATE)); + } + + @Override + public ItemStackRef getDummyLeggings() { + return wrap(new ItemStack(Items.DIAMOND_LEGGINGS)); + } + + @Override + public ItemStackRef getDummyBoots() { + return wrap(new ItemStack(Items.DIAMOND_BOOTS)); + } + + // --------------------------------------------------------------- + // Status effects + // --------------------------------------------------------------- + + @Override + public List getActiveEffects() { + if (!hasPlayer()) + return Collections.emptyList(); + + var effects = player().getActiveStatusEffects().values(); + List result = new ArrayList<>(effects.size()); + for (StatusEffectInstance instance : effects) { + result.add(new FabricEffectView(instance)); + } + return result; + } + + @Override + public List getDummyEffects() { + if (!hasPlayer()) + return Collections.emptyList(); + + var effects = player().getActiveStatusEffects().values(); + List result = new ArrayList<>(effects.size()); + for (StatusEffectInstance instance : effects) { + result.add(new FabricEffectView(instance)); + } + return result; + } + + // --------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------- + + private static ItemStackRef wrap(ItemStack stack) { + return (ItemStackRef) (Object) stack; + } + + // --------------------------------------------------------------- + // Inner — EffectView adapter + // --------------------------------------------------------------- + + /** + * Bridges a Fabric {@link StatusEffectInstance} to the + * platform-neutral {@link StatusEffectsFeature.EffectView}. + */ + public static final class FabricEffectView implements StatusEffectsFeature.EffectView { + + private final StatusEffectInstance instance; + + public FabricEffectView(StatusEffectInstance instance) { + this.instance = instance; + } + + @Override + public boolean shouldShowIcon() { + return instance.shouldShowIcon(); + } + + /** + * Returns the Minecraft sprite identifier for this effect's icon. + * The platform-specific renderer knows how to draw a + * {@link net.minecraft.util.Identifier}. + */ + @Override + public SpriteRef getIcon() { + return (SpriteRef) SaturnClient.client.getStatusEffectSpriteManager().getSprite(instance.getEffectType()); + } + + @Override + public boolean isInfinite() { + return instance.isInfinite(); + } + + @Override + public String getInfiniteText() { + return "∞"; + } + + @Override + public int getDurationSeconds() { + return instance.getDuration() / 20; // ticks → seconds + } + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/RenderModuleFabric.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/RenderModuleFabric.java new file mode 100644 index 0000000..bfde729 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/RenderModuleFabric.java @@ -0,0 +1,64 @@ +package org.saturnclient.impl.modules; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.Perspective; + +import org.saturnclient.common.module.RenderModule; + +/** + * Fabric implementation of {@link RenderModule}. + * + * Window dimensions and FPS are read directly from + * {@link MinecraftClient}. Camera perspective control delegates to + * {@link net.minecraft.client.option.GameOptions}. + */ +public class RenderModuleFabric implements RenderModule { + + private final MinecraftClient mc; + + public RenderModuleFabric(MinecraftClient mc) { + this.mc = mc; + } + + // --------------------------------------------------------------- + // Window + // --------------------------------------------------------------- + + @Override + public int getScaledWindowWidth() { + return mc.getWindow().getScaledWidth(); + } + + @Override + public int getScaledWindowHeight() { + return mc.getWindow().getScaledHeight(); + } + + // --------------------------------------------------------------- + // Performance + // --------------------------------------------------------------- + + @Override + public int getFps() { + return mc.getCurrentFps(); + } + + // --------------------------------------------------------------- + // Camera perspective + // --------------------------------------------------------------- + + @Override + public boolean isFirstPerson() { + return mc.options.getPerspective() == Perspective.FIRST_PERSON; + } + + @Override + public void setFirstPerson() { + mc.options.setPerspective(Perspective.FIRST_PERSON); + } + + @Override + public void setThirdPersonBack() { + mc.options.setPerspective(Perspective.THIRD_PERSON_BACK); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/WorldModuleFabric.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/WorldModuleFabric.java new file mode 100644 index 0000000..b555a23 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/WorldModuleFabric.java @@ -0,0 +1,36 @@ +package org.saturnclient.impl.modules; + +import net.minecraft.client.MinecraftClient; + +import org.saturnclient.common.module.WorldModule; + +/** + * Fabric implementation of {@link WorldModule}. + * + * Reads the client world's time directly from {@link MinecraftClient}. + * Returns safe defaults when no world is loaded. + */ +public class WorldModuleFabric implements WorldModule { + + private final MinecraftClient mc; + + public WorldModuleFabric(MinecraftClient mc) { + this.mc = mc; + } + + @Override + public boolean hasWorld() { + return mc.world != null; + } + + /** + * Returns the total world age in ticks, or {@code 0} when no world + * is loaded. Used by {@link TpsFeature#onTimePacket} (which is + * called from the mixin and stores its own copy) and by + * {@link DayCounterFeature} for the day count. + */ + @Override + public long getWorldAge() { + return hasWorld() ? mc.world.getTime() : 0L; + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/GameRendererMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/GameRendererMixin.java new file mode 100644 index 0000000..3dfd659 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/GameRendererMixin.java @@ -0,0 +1,37 @@ +package org.saturnclient.impl.modules.mixins; + +import net.minecraft.client.render.GameRenderer; + +import org.saturnclient.feature.features.ZoomFeature; +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; + +/** + * Overrides the game's field-of-view when {@link ZoomFeature} is active. + * + * The old code checked both {@code Zoom.isZooming} and + * {@code Zoom.shouldZoom()} separately. The refactored + * {@link ZoomFeature#shouldZoom()} already ORs both conditions + * ({@code enabled.value && isZooming}), so only one check is needed. + * + * No {@link org.saturnclient.common.module.ModuleProvider} is required + * here because zooming is a pure render-time override with no per-tick + * engine queries. + */ +@Mixin(GameRenderer.class) +public class GameRendererMixin { + + /** + * After the vanilla FOV has been computed, divide it by the zoom + * level to produce the narrowed field-of-view. + */ + @Inject(method = "getFov", at = @At("RETURN"), cancellable = true) + private void onGetFov(CallbackInfoReturnable cir) { + if (ZoomFeature.shouldZoom()) { + float zoomedFov = cir.getReturnValue() / ZoomFeature.getZoomLevel(); + cir.setReturnValue(zoomedFov); + } + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/LightmapTextureManagerMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/LightmapTextureManagerMixin.java new file mode 100644 index 0000000..564e781 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/LightmapTextureManagerMixin.java @@ -0,0 +1,35 @@ +package org.saturnclient.impl.modules.mixins; + +// Code approach adapted from Gamma Utils: https://github.com/Sjouwer/gamma-utils + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.client.render.LightmapTextureManager; + +import org.saturnclient.feature.features.FullbrightFeature; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Overrides the lightmap's gamma computation when + * {@link FullbrightFeature} is active, replacing the vanilla + * brightness with a user-configured value. + * + * The old import was {@code org.saturnclient.feature.features.Fullbright}; + * it is now {@link FullbrightFeature}. The injection target and + * strategy are unchanged. + */ +@Mixin(LightmapTextureManager.class) +public class LightmapTextureManagerMixin { + + /** + * If fullbright is active, substitute the feature's brightness + * value for whatever vanilla calculated. + */ + @ModifyExpressionValue(method = "update", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0)) + private float modifyGamma(float original) { + if (FullbrightFeature.shouldOverrideBrightness()) { + return FullbrightFeature.getBrightnessValue(); + } + return original; + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/LivingEntityRendererMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/LivingEntityRendererMixin.java new file mode 100644 index 0000000..f67bc95 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/LivingEntityRendererMixin.java @@ -0,0 +1,55 @@ +package org.saturnclient.impl.modules.mixins; + +import net.minecraft.client.render.entity.LivingEntityRenderer; +// import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.HostileEntity; +import net.minecraft.entity.passive.PassiveEntity; +import net.minecraft.entity.player.PlayerEntity; + +import org.saturnclient.common.module.EntityModule; +import org.saturnclient.impl.modules.HealthRenderState; +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; + +/** + * Captures each living entity's health and type into the render state + * during {@code updateRenderState}, so that nametag rendering can read + * them without touching the live entity again. + * + * Previously imported {@code NametagsInterface.EntityType}; now uses + * {@link EntityModule.EntityType} — the canonical enum shared across + * the whole module system. + * + * The injection target and strategy are unchanged from the original. + */ +@Mixin(LivingEntityRenderer.class) +public abstract class LivingEntityRendererMixin { +} +// public abstract class LivingEntityRendererMixin { + +// @Inject(method = "updateRenderState", at = @At("TAIL")) +// private void saturn$captureHealth(T entity, S state, float tickDelta, +// CallbackInfo ci) { +// if (!(state instanceof HealthRenderState hrs)) +// return; + +// hrs.saturn$setHealth(entity.getHealth(), entity.getMaxHealth()); + +// // Classify entity type for the nametag feature's filtering logic +// EntityModule.EntityType type; +// if (entity instanceof PlayerEntity) +// type = EntityModule.EntityType.PLAYER; +// else if (entity instanceof HostileEntity) +// type = EntityModule.EntityType.HOSTILE; +// else if (entity instanceof PassiveEntity) +// type = EntityModule.EntityType.PASSIVE; +// else +// type = EntityModule.EntityType.OTHER; + +// hrs.saturn$setEntityType(type); +// } +// } diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/NametagsMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/NametagsMixin.java new file mode 100644 index 0000000..a721d04 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/NametagsMixin.java @@ -0,0 +1,94 @@ +package org.saturnclient.impl.modules.mixins; + +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRenderer; +// import net.minecraft.client.render.entity.state.EntityRenderState; +// import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; + +import org.saturnclient.feature.features.NametagsFeature; +import org.saturnclient.impl.modules.EntityModuleFabric; +import org.saturnclient.impl.modules.HealthRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Intercepts nametag rendering and replaces the displayed text with + * the string produced by {@link NametagsFeature#getNametagString}. + * + * Key changes from the original: + * + *
    + *
  • The old {@code NametagsFabric.EntityStateImpl} bridge class is + * replaced by {@link EntityModuleFabric.RenderStateEntityState}, + * which wraps the {@link HealthRenderState} already injected into + * the render state by {@link LivingEntityRenderStateMixin}.
  • + *
  • The feature import changes from {@code Nametags} to + * {@link NametagsFeature}; the two static methods + * ({@code shouldReplaceName} / {@code getNametagString}) are + * unchanged in signature.
  • + *
  • The entity's display name is extracted from the render state's + * {@code nameVisible} / text fields where available, falling back + * to the plain text string passed to this method.
  • + *
+ * + * The re-entrancy guard ({@code saturn$rendering}) is kept intact to + * prevent infinite recursion when we call {@code renderLabelIfPresent} + * with the replacement text. + */ +@Mixin(EntityRenderer.class) +public abstract class NametagsMixin { + + /** Guards against the recursive call we make with the replacement text. */ + @Unique + private static final ThreadLocal saturn$rendering = ThreadLocal.withInitial(() -> false); + + // @Shadow + // protected abstract void renderLabelIfPresent( + // S state, Text text, + // MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light); + + // @Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true) + // private void saturn$replaceNametag( + // S state, Text text, + // MatrixStack matrices, VertexConsumerProvider vertexConsumers, + // int light, CallbackInfo ci) { + + // // Skip our own recursive call + // if (saturn$rendering.get()) + // return; + + // if (!NametagsFeature.shouldReplaceName()) + // return; + + // // Only living entities carry health data + // if (!(state instanceof LivingEntityRenderState living)) + // return; + // if (!(living instanceof HealthRenderState hrs)) + // return; + + // // Build the platform-neutral EntityState from the render-state snapshot + // // (health/type were written by LivingEntityRendererMixin at extract time). + // String customName = text != null ? text.getString() : null; + // EntityModuleFabric.RenderStateEntityState entityState = new EntityModuleFabric.RenderStateEntityState( + // customName, hrs); + + // // Ask the feature for the replacement string + // String replacement = NametagsFeature.getNametagString(entityState); + // if (replacement == null) + // return; // feature decided not to replace this entity + + // ci.cancel(); + // saturn$rendering.set(true); + // try { + // renderLabelIfPresent(state, Text.literal(replacement), matrices, vertexConsumers, light); + // } finally { + // saturn$rendering.set(false); + // } + // } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/RenderMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/RenderMixin.java new file mode 100644 index 0000000..7b45975 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/RenderMixin.java @@ -0,0 +1,85 @@ +package org.saturnclient.impl.modules.mixins; + +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.InGameHud; +import net.minecraft.client.render.RenderTickCounter; + +import org.saturnclient.feature.Feature; +import org.saturnclient.feature.FeatureLayout; +import org.saturnclient.feature.FeatureManager; +import org.saturnclient.feature.HudFeature; +import org.saturnclient.impl.ui.RenderScopeImpl; +import org.saturnclient.impl.ui.SaturnScreenFabric; +import org.saturnclient.saturnclient.SaturnClient; +import org.saturnclient.saturnclient.mixin.DrawContextAccessor; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.screens.HudEditor; +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; + +/** + * Drives per-frame rendering for all registered features. + * + * This mixin is intentionally kept thin: it creates a {@link RenderScope}, + * iterates the enabled feature list from {@link FeatureManager}, and + * dispatches either {@link HudFeature#renderHud} (for HUD elements) or + * {@link Feature#render} (for world-overlay features like Crosshair). + * + * No feature-specific logic lives here — all decisions about what to + * draw are made inside each feature class. + * + * Changes from the original: + * - Package imports updated to the refactored feature names (no + * more {@code feature.features.*}). + * - The HUD-editor screen check still uses the existing + * {@link SaturnScreenFabric} / {@link HudEditor} pair; that is + * unchanged infrastructure. + */ +@Mixin(InGameHud.class) +public class RenderMixin { + + @Inject(method = "renderChat", at = @At("TAIL")) + public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + TextRenderer textRenderer = SaturnClient.client.textRenderer; + + // Skip rendering while the HUD editor is open so the editor + // can draw its own preview layout without double-drawing. + if (textRenderer == null) + return; + if (SaturnClient.client.currentScreen instanceof SaturnScreenFabric f + && f.screen instanceof HudEditor) + return; + + RenderScope scope = new RenderScopeImpl( + context.getMatrices(), + ((DrawContextAccessor) context).getVertexConsumers()); + + for (Feature feature : FeatureManager.ENABLED_MODS) { + if (!feature.isEnabled()) + continue; + + if (feature instanceof HudFeature hud) { + FeatureLayout dim = hud.getDimensions(); + + scope.getMatrixStack().push(); + scope.getMatrixStack().translate(dim.x.value, (float) dim.y.value, 0f); + scope.getMatrixStack().scale(dim.scale.value, dim.scale.value, 1.0f); + + if (dim.renderBackground) { + scope.drawRoundedRectangle(0, 0, dim.width, dim.height, + dim.radius.value, dim.bgColor.value); + } + + hud.renderHud(scope); + scope.getMatrixStack().pop(); + + } else { + // Non-HUD features (e.g. CrosshairFeature) use the plain render hook. + feature.render(scope); + } + } + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/TpsMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/TpsMixin.java new file mode 100644 index 0000000..0e452a7 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/modules/mixins/TpsMixin.java @@ -0,0 +1,30 @@ +package org.saturnclient.impl.modules.mixins; + +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket; + +import org.saturnclient.feature.features.TpsFeature; +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; + +/** + * Feeds world-time packets into {@link TpsFeature}'s TPS estimator. + * + * This is the only place in the entire codebase that reads a network + * packet and passes data to a feature. The feature's + * {@link TpsFeature#onTimePacket(long)} method handles all averaging + * logic; the mixin is intentionally reduced to a single forwarding call. + * + * The only change from the original is renaming the import from + * {@code feature.features.Tps} to {@link TpsFeature}. + */ +@Mixin(ClientPlayNetworkHandler.class) +public class TpsMixin { + + @Inject(method = "onWorldTimeUpdate", at = @At("HEAD")) + private void onWorldTimeUpdate(WorldTimeUpdateS2CPacket packet, CallbackInfo ci) { + TpsFeature.onTimePacket(packet.getTime()); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/GLFWProviderImpl.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/GLFWProviderImpl.java new file mode 100644 index 0000000..f8a896f --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/GLFWProviderImpl.java @@ -0,0 +1,18 @@ +package org.saturnclient.impl.provider; + +import org.lwjgl.glfw.GLFW; +import org.saturnclient.common.provider.GLFWProvider; +import org.saturnclient.saturnclient.SaturnClient; + +public class GLFWProviderImpl implements GLFWProvider { + @Override + public boolean isKeyPressed(int key) { + return GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), key) == GLFW.GLFW_PRESS + && SaturnClient.client.currentScreen == null; + } + + @Override + public String getKeyName(int key) { + return GLFW.glfwGetKeyName(key, GLFW.glfwGetKeyScancode(key)); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/ModuleProviderFabric.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/ModuleProviderFabric.java new file mode 100644 index 0000000..b74f7c6 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/ModuleProviderFabric.java @@ -0,0 +1,68 @@ +package org.saturnclient.impl.provider; + +import net.minecraft.client.MinecraftClient; + +import org.saturnclient.common.provider.ModuleProvider; +import org.saturnclient.impl.modules.EntityModuleFabric; +import org.saturnclient.impl.modules.NetworkModuleFabric; +import org.saturnclient.impl.modules.PlayerModuleFabric; +import org.saturnclient.impl.modules.RenderModuleFabric; +import org.saturnclient.impl.modules.WorldModuleFabric; +import org.saturnclient.common.module.*; + +/** + * Fabric implementation of {@link ModuleProvider}. + * + * Instantiated once during client startup (e.g. in the mod initialiser) + * and passed to every feature constructor. All five sub-modules share + * the same {@link MinecraftClient} reference so there is no + * inconsistency between modules reading state within the same tick. + * + *
{@code
+ * ModuleProvider provider = new ModuleProviderFabric(MinecraftClient.getInstance());
+ * FeatureManager.register(new AutoSprintFeature(provider));
+ * FeatureManager.register(new CoordinatesFeature(provider));
+ * // …
+ * }
+ */ +public class ModuleProviderFabric implements ModuleProvider { + + private final PlayerModule player; + private final WorldModule world; + private final EntityModule entity; + private final RenderModule render; + private final NetworkModule network; + + public ModuleProviderFabric(MinecraftClient mc) { + this.player = new PlayerModuleFabric(mc); + this.world = new WorldModuleFabric(mc); + this.entity = new EntityModuleFabric(mc); + this.render = new RenderModuleFabric(mc); + this.network = new NetworkModuleFabric(mc); + } + + @Override + public PlayerModule player() { + return player; + } + + @Override + public WorldModule world() { + return world; + } + + @Override + public EntityModule entity() { + return entity; + } + + @Override + public RenderModule render() { + return render; + } + + @Override + public NetworkModule network() { + return network; + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/RefConstructorImpl.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/RefConstructorImpl.java new file mode 100644 index 0000000..cf59bd7 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/RefConstructorImpl.java @@ -0,0 +1,25 @@ +package org.saturnclient.impl.provider; + +import org.saturnclient.common.provider.RefConstructorProvider; +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.common.ref.render.TextRef; + +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public class RefConstructorImpl implements RefConstructorProvider { + @Override + public IdentifierRef identifier(String fullPath) { + return (IdentifierRef) (Object) Identifier.of(fullPath); + } + + @Override + public IdentifierRef identifier(String namespace, String path) { + return (IdentifierRef) (Object) Identifier.of(namespace, path); + } + + @Override + public TextRef text(String text) { + return (TextRef) Text.literal(text); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/SaturnProviderImpl.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/SaturnProviderImpl.java new file mode 100644 index 0000000..61238c9 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/provider/SaturnProviderImpl.java @@ -0,0 +1,72 @@ +package org.saturnclient.impl.provider; + +import java.awt.image.BufferedImage; +import java.util.UUID; + +import org.saturnclient.common.provider.SaturnProvider; +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.common.ref.game.MinecraftClientRef; +import org.saturnclient.impl.cosmetics.utils.IdentifierUtils; +import org.saturnclient.saturnclient.SaturnClient; + +import dev.kosmx.playerAnim.api.layered.AnimationStack; +import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess; +import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.util.Identifier; + +public class SaturnProviderImpl implements SaturnProvider { + @Override + public MinecraftClientRef getClient() { + return (MinecraftClientRef) SaturnClient.client; + } + + @Override + public void playEmote(UUID fromPlayerUuid, String emoteIdOrNull) { + if (SaturnClient.client == null || SaturnClient.client.world == null) { + return; + } + + for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) { + if (!player.getUuid().equals(fromPlayerUuid)) { + continue; + } + + AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player); + + if (emoteIdOrNull != null && !emoteIdOrNull.isEmpty()) { + if (animationStack.isActive()) { + animationStack.removeLayer(1000); + } + animationStack.addAnimLayer(1000, + PlayerAnimationRegistry + .getAnimation(Identifier.of("saturnclient", emoteIdOrNull)) + .playAnimation()); + } else { + animationStack.removeLayer(1000); + } + + break; + } + } + + @Override + public void logInfo(String message) { + SaturnClient.LOGGER.info(message); + } + + @Override + public void logError(String message) { + SaturnClient.LOGGER.error(message); + } + + @Override + public void logError(String message, Throwable throwable) { + SaturnClient.LOGGER.error(message, throwable); + } + + @Override + public void registerBufferedImageTexture(IdentifierRef i, BufferedImage bi) { + IdentifierUtils.registerBufferedImageTextureFast(i, bi); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/EntityDrawerImpl.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/EntityDrawerImpl.java new file mode 100644 index 0000000..d30b6f5 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/EntityDrawerImpl.java @@ -0,0 +1,85 @@ +package org.saturnclient.impl.ui; + +import org.jetbrains.annotations.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.saturnclient.common.ref.render.QuaternionfRef; +import org.saturnclient.saturnclient.SaturnClient; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.components.SkinPreview.EntityDrawer; + +import net.minecraft.client.render.DiffuseLighting; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.entity.LivingEntity; + +public class EntityDrawerImpl implements EntityDrawer { + public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float f, + float mouseX, + float mouseY, LivingEntity entity, float angle, boolean negativeAngle) { + float g = (float) (x1 + x2) / 2.0F; + float h = (float) (y1 + y2) / 2.0F; + renderScope.enableScissor(0, 6, x2, y2); + float i = (float) Math.atan((double) ((g - mouseX) / 40.0F)); + float j = (float) Math.atan((double) ((h - mouseY) / 40.0F)); + Quaternionf quaternionf = (new Quaternionf()).rotateZ(3.1415927F); + float k = entity.bodyYaw; + float l = entity.getYaw(); + float m = entity.getPitch(); + float n = entity.prevHeadYaw; + float o = entity.headYaw; + entity.setYaw(angle); + entity.bodyYaw = angle; + entity.setPitch(-(j * 20.0F)); + entity.headYaw = negativeAngle ? (i * 40.0F) + 180f : -(i * 40.0F); + entity.prevHeadYaw = entity.getHeadYaw(); + entity.setCustomNameVisible(false); + + float p = entity.getScale(); + Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + f * p, 0.0F); + float q = (float) size / p; + drawEntity(renderScope, g, h, q, vector3f, quaternionf, null, entity); + + entity.setCustomNameVisible(true); + entity.bodyYaw = k; + entity.setYaw(l); + entity.setPitch(m); + entity.prevHeadYaw = n; + entity.headYaw = o; + renderScope.disableScissor(); + } + + public static void drawEntity(RenderScope renderScope, float x, float y, float size, Vector3f vector3f, + Quaternionf quaternionf, @Nullable Quaternionf quaternionf2, LivingEntity entity) { + renderScope.getMatrixStack().push(); + renderScope.getMatrixStack().translate((double) x, (double) y, 50.0); + renderScope.getMatrixStack().scale(size, size, -size); + renderScope.getMatrixStack().translate(vector3f.x, vector3f.y, vector3f.z); + renderScope.getMatrixStack().multiply((QuaternionfRef) quaternionf); + renderScope.draw(); + DiffuseLighting.method_34742(); + EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher(); + + entityRenderDispatcher.setRenderShadows(false); + if (renderScope instanceof RenderScopeImpl r) { + r.draw((vertexConsumers) -> { + entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 1.0F, 1.0f, + r.matrices, vertexConsumers, + 15728880); + }); + } + renderScope.draw(); + entityRenderDispatcher.setRenderShadows(true); + renderScope.getMatrixStack().pop(); + DiffuseLighting.enableGuiDepthLighting(); + } + + @Override + public void render(RenderScope renderScope, boolean negativeAngle, float angle, float scale, ElementContext ctx) { + LivingEntity entity = SaturnClient.client.player; + if (entity != null) { + drawEntity(renderScope, 0, 0, 75, 78, 30, 0.0625F, ctx.mouseX / scale, ctx.mouseY / scale, entity, angle, + negativeAngle); + } + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/RenderScopeImpl.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/RenderScopeImpl.java new file mode 100644 index 0000000..1acc300 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/RenderScopeImpl.java @@ -0,0 +1,501 @@ +package org.saturnclient.impl.ui; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Objects; +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; + +import com.mojang.blaze3d.systems.RenderSystem; + +import org.saturnclient.common.provider.Providers; +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.common.ref.asset.SpriteRef; +import org.saturnclient.common.ref.game.ItemStackRef; +import org.saturnclient.common.ref.render.MatrixStackRef; +import org.saturnclient.saturnclient.SaturnClient; +import org.saturnclient.saturnclient.mixin.DrawContextAccessor; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.resources.Fonts; +import org.saturnclient.ui.resources.SvgTexture; + +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.font.TextRenderer.TextLayerType; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.ScreenRect; +import net.minecraft.client.render.BufferBuilder; +import net.minecraft.client.render.BufferRenderer; +import net.minecraft.client.render.DiffuseLighting; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.VertexFormat.DrawMode; +import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.client.render.model.json.ModelTransformationMode; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.util.Window; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.crash.CrashException; +import net.minecraft.util.crash.CrashReport; +import net.minecraft.util.crash.CrashReportSection; +import net.minecraft.util.math.RotationAxis; +import net.minecraft.world.World; + +public class RenderScopeImpl implements RenderScope { + public MatrixStack matrices; + public VertexConsumerProvider.Immediate vertexConsumers; + private ScissorStack scissorStack = new ScissorStack(); + private int opacity = 255 << 24; + + public RenderScopeImpl(DrawContext context) { + this(context.getMatrices(), ((DrawContextAccessor) context).getVertexConsumers()); + } + + public RenderScopeImpl(MatrixStack matrices, VertexConsumerProvider.Immediate vertexConsumers) { + this.matrices = matrices; + this.vertexConsumers = vertexConsumers; + this.scissorStack = new ScissorStack(); + } + + @Override + public MatrixStackRef getMatrixStack() { + return (MatrixStackRef) matrices; + } + + @Override + public void setOpacity(float alpha) { + this.opacity = (int) (alpha * 255) << 24; + } + + @Override + public int getColor(int color) { + int originalAlpha = (color >>> 24) & 0xFF; + int newAlpha = (opacity >>> 24) & 0xFF; + int mixedAlpha = (originalAlpha * newAlpha) / 255; + return (mixedAlpha << 24) | (color & 0x00FFFFFF); + } + + @Override + public void drawRect(int x, int y, int width, int height, int color) { + if (color == 0) + return; + color = getColor(color); + Matrix4f matrix4f = this.matrices.peek().getPositionMatrix(); + + int x1 = x; + int x2 = x + width; + int y1 = y; + int y2 = y + height; + + int i; + if (x1 < x2) { + i = x1; + x1 = x2; + x2 = i; + } + + if (y1 < y2) { + i = y1; + y1 = y2; + y2 = i; + } + + VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(RenderLayer.getGui()); + vertexConsumer.vertex(matrix4f, (float) x1, (float) y1, 0).color(color); + vertexConsumer.vertex(matrix4f, (float) x1, (float) y2, 0).color(color); + vertexConsumer.vertex(matrix4f, (float) x2, (float) y2, 0).color(color); + vertexConsumer.vertex(matrix4f, (float) x2, (float) y1, 0).color(color); + } + + @Override + public void drawText(String text, int x, int y, int font, int color) { + drawText(1.0f, text, x, y, font, color); + } + + @Override + public void drawText(float scale, String text, int x, int y, int font, int color) { + if (color == 0) + return; + if (font == 0) { + scale *= 2; + } + int i = 0; + for (String line : text.split("\n")) { + color = getColor(color); + matrices.push(); + matrices.translate(x, y + (i * Fonts.getHeight()), 0); + matrices.scale(scale, scale, 1.0f); + TextRenderer textRenderer = SaturnClient.client.textRenderer; + textRenderer.draw((Text) Fonts.setFont(line, font), 0, + font == 0 ? 1 : 7, color, false, + this.matrices.peek().getPositionMatrix(), + this.vertexConsumers, TextLayerType.NORMAL, 0, 15728880); + matrices.pop(); + i++; + } + } + + private void drawRoundedCorner(int width, int height, int radius, int color) { + int w = width * 20; + int h = height * 20; + + for (int y = 0; y < h; y++) { + int startX = 0; + + if (y < radius) { + double dy = radius - y - 0.5; + double dx = Math.sqrt(Math.max(0, radius * radius - dy * dy)); + startX = radius - (int) dx; + } + + this.drawRect(startX, y, w - startX, 1, color); + } + } + + private void drawRoundedSide(int cornerWidth, int cornerHeight, int radius, int color) { + // Top + this.matrices.push(); + + this.matrices.scale(0.05f, 0.05f, 1.0f); + + this.drawRoundedCorner(cornerWidth, cornerHeight, radius * 10, color); + + this.matrices.pop(); + + // Bottom + this.matrices.push(); + + this.matrices.translate(cornerWidth, cornerHeight * 2, 0); + + this.matrices.multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(90)); + + this.matrices.translate(0, -cornerWidth, 0); + + this.matrices.scale(0.05f, 0.05f, 1.0f); + + this.drawRoundedCorner(cornerHeight, cornerWidth, radius * 10, color); + + this.matrices.pop(); + } + + @Override + public void drawRoundedRectangle(int x, int y, int width, int height, int radius, int color) { + if (color == 0) + return; + radius = Math.min(radius, Math.min(width, height)); + int cornerWidth = width / 2; + int cornerHeight = height / 2; + + this.matrices.push(); + + this.matrices.translate(x, y, 0); + + this.drawRoundedSide(cornerWidth, cornerHeight, radius, color); + + this.matrices.translate(cornerWidth * 2, cornerHeight * 2, 0); + + this.matrices.multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(180)); + + this.matrices.translate(0, 0, 0); + + this.drawRoundedSide(cornerWidth, cornerHeight, radius, color); + + this.matrices.pop(); + } + + @Override + public int getScaledWindowWidth() { + return SaturnClient.client.getWindow().getScaledWidth(); + } + + @Override + public int getScaledWindowHeight() { + return SaturnClient.client.getWindow().getScaledHeight(); + } + + @Override + public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, int color) { + this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color); + } + + @Override + public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height) { + this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height); + } + + @Override + public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, + int regionWith, + int regionHeight, int textureWidth, int textureHeight) { + this.drawTexture(sprite, x, y, u, v, width, height, regionWith, regionHeight, textureWidth, textureHeight, -1); + } + + @Override + public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, + int regionWidth, + int regionHeight, int textureWidth, int textureHeight, int color) { + this.drawTexturedQuad((Identifier) (Object) sprite, x, x + width, y, y + height, + (u + 0.0F) / (float) textureWidth, + (u + (float) regionWidth) / (float) textureWidth, (v + 0.0F) / (float) textureHeight, + (v + (float) regionHeight) / (float) textureHeight, color); + } + + /* Minecraft sprites */ + + public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int color) { + this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color); + } + + public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height) { + this.drawTexture(sprite, x, y, u, v, width, height, width, height, width, height); + } + + public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWith, + int regionHeight, int textureWidth, int textureHeight) { + this.drawTexture(sprite, x, y, u, v, width, height, regionWith, regionHeight, textureWidth, textureHeight, -1); + } + + public void drawTexture(Identifier sprite, int x, int y, float u, float v, int width, int height, int regionWidth, + int regionHeight, int textureWidth, int textureHeight, int color) { + this.drawTexturedQuad(sprite, x, x + width, y, y + height, (u + 0.0F) / (float) textureWidth, + (u + (float) regionWidth) / (float) textureWidth, (v + 0.0F) / (float) textureHeight, + (v + (float) regionHeight) / (float) textureHeight, color); + } + + private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2, float u1, float u2, float v1, + float v2, int color) { + + if (sprite.toString().endsWith(".svg")) { + sprite = (Identifier) (Object) SvgTexture.getSvg(Providers.saturn.getClient(), + (IdentifierRef) (Object) sprite, + (x2 - x1) * 2, (y2 - y1) * 2); + } + + if (color == 0) + return; + x1 *= 4; + x2 *= 4; + y1 *= 4; + y2 *= 4; + color = getColor(color); + + matrices.push(); + matrices.scale(0.25f, 0.25f, 1.0f); + + RenderSystem.setShaderTexture(0, sprite); + RenderSystem.setShader(GameRenderer::getPositionTexProgram); + Matrix4f matrix4f = this.matrices.peek().getPositionMatrix(); + BufferBuilder bufferBuilder = Tessellator.getInstance().begin(DrawMode.QUADS, VertexFormats.POSITION_TEXTURE); + bufferBuilder.vertex(matrix4f, (float) x1, (float) y1, 0f).texture(u1, v1); + bufferBuilder.vertex(matrix4f, (float) x1, (float) y2, 0f).texture(u1, v2); + bufferBuilder.vertex(matrix4f, (float) x2, (float) y2, 0f).texture(u2, v2); + bufferBuilder.vertex(matrix4f, (float) x2, (float) y1, 0f).texture(u2, v1); + BufferRenderer.drawWithGlobalProgram(bufferBuilder.end()); + + matrices.pop(); + } + + @Override + public void enableScissor(int x1, int y1, int x2, int y2) { + this.setScissor(this.scissorStack.push(new ScreenRect(x1, y1, x2 - x1, y2 - y1))); + } + + public void disableScissor() { + this.setScissor(this.scissorStack.pop()); + } + + public boolean scissorContains(int x, int y) { + return this.scissorStack.contains(x, y); + } + + private void setScissor(@Nullable ScreenRect rect) { + if (rect != null) { + Window window = SaturnClient.client.getWindow(); + int i = window.getFramebufferHeight(); + double d = window.getScaleFactor(); + double e = (double) rect.getLeft() * d; + double f = (double) i - (double) rect.getBottom() * d; + double g = (double) rect.width() * d; + double h = (double) rect.height() * d; + RenderSystem.enableScissor((int) e, (int) f, Math.max(0, (int) g), Math.max(0, (int) h)); + } else { + RenderSystem.disableScissor(); + } + + } + + @Override + public void draw() { + this.vertexConsumers.draw(); + } + + public void draw(Consumer drawer) { + drawer.accept(this.vertexConsumers); + this.vertexConsumers.draw(); + } + + static class ScissorStack { + private final Deque stack = new ArrayDeque<>(); + + ScissorStack() { + } + + public ScreenRect push(ScreenRect rect) { + ScreenRect screenRect = (ScreenRect) this.stack.peekLast(); + if (screenRect != null) { + ScreenRect screenRect2 = (ScreenRect) Objects.requireNonNullElse(rect.intersection(screenRect), + ScreenRect.empty()); + this.stack.addLast(screenRect2); + return screenRect2; + } else { + this.stack.addLast(rect); + return rect; + } + } + + @Nullable + public ScreenRect pop() { + if (this.stack.isEmpty()) { + throw new IllegalStateException("Scissor stack underflow"); + } else { + this.stack.removeLast(); + return (ScreenRect) this.stack.peekLast(); + } + } + + public boolean contains(int x, int y) { + return this.stack.isEmpty() ? true : ((ScreenRect) this.stack.peek()).contains(x, y); + } + } + + @Override + public void drawItem(ItemStackRef item, int x, int y) { + this.drawItem(SaturnClient.client.player, SaturnClient.client.world, item, x, y, 0); + } + + @Override + public void drawItem(ItemStackRef stack, int x, int y, int seed) { + this.drawItem(SaturnClient.client.player, SaturnClient.client.world, stack, x, y, seed); + } + + @Override + public void drawItem(ItemStackRef stack, int x, int y, int seed, int z) { + this.drawItem(SaturnClient.client.player, SaturnClient.client.world, (ItemStack) (Object) stack, x, y, seed, z); + } + + @Override + public void drawItemWithoutEntity(ItemStackRef stack, int x, int y) { + this.drawItemWithoutEntity(stack, x, y, 0); + } + + @Override + public void drawItemWithoutEntity(ItemStackRef stack, int x, int y, int seed) { + this.drawItem((LivingEntity) null, SaturnClient.client.world, stack, x, y, seed); + } + + private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y, + int seed) { + this.drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0); + } + + private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed, + int z) { + if (!stack.isEmpty()) { + BakedModel bakedModel = SaturnClient.client.getItemRenderer().getModel(stack, world, entity, seed); + this.matrices.push(); + this.matrices.translate((float) (x + 8), (float) (y + 8), (float) (150 + (bakedModel.hasDepth() ? z : 0))); + + try { + this.matrices.scale(16.0F, -16.0F, 16.0F); + boolean bl = !bakedModel.isSideLit(); + if (bl) { + DiffuseLighting.disableGuiDepthLighting(); + } + + SaturnClient.client.getItemRenderer().renderItem(stack, ModelTransformationMode.GUI, false, + this.matrices, + this.vertexConsumers, 15728880, OverlayTexture.DEFAULT_UV, bakedModel); + this.draw(); + if (bl) { + DiffuseLighting.enableGuiDepthLighting(); + } + } catch (Throwable throwable) { + CrashReport crashReport = CrashReport.create(throwable, "Rendering item"); + CrashReportSection crashReportSection = crashReport.addElement("Item being rendered"); + crashReportSection.add("Item Type", () -> String.valueOf(stack.getItem())); + crashReportSection.add("Item Components", () -> String.valueOf(stack.getComponents())); + crashReportSection.add("Item Foil", () -> String.valueOf(stack.hasGlint())); + throw new CrashException(crashReport); + } + + this.matrices.pop(); + } + } + + @Override + public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height) { + this.drawSpriteStretched(sprite, x, y, width, height, -1); + } + + @Override + public void drawSpriteStretched(SpriteRef saturnSprite, int x, int y, int width, int height, int color) { + Sprite sprite = (Sprite) saturnSprite; + if (color == 0) + return; + if (width != 0 && height != 0) { + this.drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height, sprite.getMinU(), sprite.getMaxU(), + sprite.getMinV(), sprite.getMaxV(), color); + } + } + + public void fill(int x1, int y1, int x2, int y2, int color) { + this.fill(x1, y1, x2, y2, 0, color); + } + + public void fill(int x1, int y1, int x2, int y2, int z, int color) { + this.fill(RenderLayer.getGui(), x1, y1, x2, y2, z, color); + } + + public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int color) { + this.fill(layer, x1, y1, x2, y2, 0, color); + } + + public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int z, int color) { + Matrix4f matrix4f = this.matrices.peek().getPositionMatrix(); + int i; + if (x1 < x2) { + i = x1; + x1 = x2; + x2 = i; + } + + if (y1 < y2) { + i = y1; + y1 = y2; + y2 = i; + } + + VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(layer); + vertexConsumer.vertex(matrix4f, (float) x1, (float) y1, (float) z).color(color); + vertexConsumer.vertex(matrix4f, (float) x1, (float) y2, (float) z).color(color); + vertexConsumer.vertex(matrix4f, (float) x2, (float) y2, (float) z).color(color); + vertexConsumer.vertex(matrix4f, (float) x2, (float) y1, (float) z).color(color); + } + + public void drawBorder(int x, int y, int width, int height, int color) { + this.fill(x, y, x + width, y + 1, color); + this.fill(x, y + height - 1, x + width, y + height, color); + this.fill(x, y + 1, x + 1, y + height - 1, color); + this.fill(x + width - 1, y + 1, x + width, y + height - 1, color); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/SaturnScreenFabric.java b/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/SaturnScreenFabric.java new file mode 100644 index 0000000..face3af --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/impl/ui/SaturnScreenFabric.java @@ -0,0 +1,161 @@ +package org.saturnclient.impl.ui; + +import java.time.Duration; +import java.time.Instant; + +import org.saturnclient.saturnclient.mixin.DrawContextAccessor; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.SaturnScreen; +import org.saturnclient.ui.SaturnScreen.ScreenProvider; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gl.PostEffectProcessor; +import net.minecraft.client.gui.CubeMapRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.RotatingCubeMapRenderer; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.texture.ResourceTexture; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public class SaturnScreenFabric extends Screen implements ScreenProvider { + protected static final Identifier PANORAMA = Identifier.of("saturnclient", + "textures/gui/title/background/panorama"); + protected static final CubeMapRenderer PANORAMA_RENDERER = new CubeMapRenderer(PANORAMA); + + public static final RotatingCubeMapRenderer ROTATING_PANORAMA_RENDERER; + + static { + ROTATING_PANORAMA_RENDERER = new RotatingCubeMapRenderer(PANORAMA_RENDERER); + } + + // private final Pool pool = new Pool(3); + public final SaturnScreen screen; + + public static void preload(MinecraftClient client) { + TextureManager textureManager = client.getTextureManager(); + + // Force upload each texture to GPU + for (int i = 0; i < 6; i++) { + String var10001 = PANORAMA.getPath(); + Identifier tex = PANORAMA.withPath(var10001 + "_" + i + ".png"); + ResourceTexture resourceTexture = new ResourceTexture(tex); + textureManager.registerTexture(tex, resourceTexture); + } + } + + public SaturnScreenFabric(SaturnScreen screen) { + super(Text.literal(screen.title)); + screen.provider = this; + this.screen = screen; + } + + @Override + protected void init() { + screen.init(); + } + + @SuppressWarnings("deprecation") + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + if (screen.start == null) { + screen.start = Instant.now(); + } + long elapsed = Duration.between(screen.start, Instant.now()).toMillis(); + + mouseX *= 2; + mouseY *= 2; + + if (client.world == null && client.getCurrentServerEntry() == null) { + ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, screen.backgroundOpacity, delta); + } + + // PostEffectProcessor postEffectProcessor = this.client.getShaderLoader().loadPostEffect( + // Identifier.ofVanilla("blur"), + // DefaultFramebufferSet.MAIN_ONLY); + + // if (postEffectProcessor != null) { + // postEffectProcessor.setUniforms("Radius", screen.backgroundBlur * Math.min((float) elapsed / 700, 1.0f)); + // postEffectProcessor.render(this.client.getFramebuffer(), this.pool); + // } + + // We are using a Abstracted RenderScope because older minecraft versions don't + // use DrawContext + RenderScope renderScope = new RenderScopeImpl(context.getMatrices(), + ((DrawContextAccessor) context).getVertexConsumers()); + + screen.render(renderScope, mouseX, mouseY, delta, elapsed); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (screen.mouseClicked(mouseX, mouseY, button)) { + return true; + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (screen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) { + return true; + } + + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (screen.mouseReleased(mouseX, mouseY, button)) { + return true; + } + + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (screen.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) { + return true; + } + + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (screen.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void resize(MinecraftClient client, int width, int height) { + screen.resize(width, height); + super.resize(client, width, height); + } + + // ------------------------------------------ + // ScreenProvider implementations + // ------------------------------------------ + + @Override + public void close() { + this.screen.onClose(); + super.close(); + } + + @Override + public int getWidth() { + return width * 2; + } + + @Override + public int getHeight() { + return height * 2; + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/PlayerTracker.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/PlayerTracker.java new file mode 100644 index 0000000..8e37f09 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/PlayerTracker.java @@ -0,0 +1,52 @@ +package org.saturnclient.saturnclient; + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; + +import org.saturnclient.client.ServiceClient; + +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.MinecraftClient; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import com.mojang.authlib.GameProfile; + +public class PlayerTracker { + private static Set trackedPlayers = new HashSet<>(); + + public static void initialize() { + // When client joins a server, reset tracked players + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { + trackedPlayers.clear(); + client.execute(() -> { + checkForNewPlayers(SaturnClient.client); + }); + }); + + // Check for new players every tick + ClientTickEvents.END_CLIENT_TICK.register(client -> checkForNewPlayers(client)); + } + + private static void checkForNewPlayers(MinecraftClient client) { + if (client.getNetworkHandler() == null) + return; + + for (PlayerListEntry player : client.getNetworkHandler().getPlayerList()) { + UUID playerUUID = player.getProfile().getId(); + + if (!trackedPlayers.contains(playerUUID)) { + // A new player has joined + onPlayerJoin(client, player); + trackedPlayers.add(playerUUID); + } + } + } + + private static void onPlayerJoin(MinecraftClient client, PlayerListEntry player) { + GameProfile profile = player.getProfile(); + ServiceClient.player(profile.getId(), profile.getName()); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/SaturnClient.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/SaturnClient.java new file mode 100644 index 0000000..370b12c --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/SaturnClient.java @@ -0,0 +1,72 @@ +package org.saturnclient.saturnclient; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.client.MinecraftClient; + +import org.saturnclient.client.ServiceClient; +import org.saturnclient.cosmetics.Emotes; +import org.saturnclient.cosmetics.Hats; +import org.saturnclient.feature.FeatureManager; +import org.saturnclient.cosmetics.Cloaks; +import org.saturnclient.common.provider.Providers; +import org.saturnclient.impl.provider.GLFWProviderImpl; +import org.saturnclient.impl.provider.ModuleProviderFabric; +import org.saturnclient.impl.provider.RefConstructorImpl; +import org.saturnclient.impl.provider.SaturnProviderImpl; +import org.saturnclient.impl.ui.EntityDrawerImpl; +import org.saturnclient.impl.ui.SaturnScreenFabric; +import org.saturnclient.saturnclient.event.KeyInputHandler; +import org.saturnclient.config.Config; +import org.saturnclient.config.manager.ConfigManager; +import org.saturnclient.ui.components.SkinPreview; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Main class for the Saturn Client mod. + */ +public class SaturnClient implements ModInitializer { + + public static final String MOD_ID = "saturnclient"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + public static MinecraftClient client; + + @Override + public void onInitialize() { + LOGGER.info("Initializing " + MOD_ID); + client = MinecraftClient.getInstance(); + + Providers.saturn = new SaturnProviderImpl(); + Providers.refConstructor = new RefConstructorImpl(); + Providers.GLFW = new GLFWProviderImpl(); + Providers.module = new ModuleProviderFabric(client); + SkinPreview.DRAWER = new EntityDrawerImpl(); + + Config.init(); + FeatureManager.init(); + + client.execute(() -> { + SaturnScreenFabric.preload(client); + }); + + ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> ConfigManager.save()); + + ClientTickEvents.END_CLIENT_TICK.register(client -> { + for (org.saturnclient.feature.Feature m : FeatureManager.ENABLED_MODS) { + m.tick(); + } + }); + + KeyInputHandler.register(); + Emotes.initialize(); + if (ServiceClient.authenticate()) { + Cloaks.initialize(); + Hats.initialize(); + LOGGER.info(MOD_ID + " initialization complete"); + } else { + LOGGER.error("Failed to authenticate with the server"); + } + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/event/KeyInputHandler.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/event/KeyInputHandler.java new file mode 100644 index 0000000..090aec8 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/event/KeyInputHandler.java @@ -0,0 +1,57 @@ +package org.saturnclient.saturnclient.event; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; + +import org.saturnclient.config.Config; +import org.saturnclient.saturnclient.SaturnClient; +import org.saturnclient.client.ServiceClient; +import org.saturnclient.common.provider.Providers; +import org.saturnclient.ui.screens.EmoteWheel; +import org.saturnclient.ui.screens.ShiftMenu; + +import dev.kosmx.playerAnim.api.layered.AnimationStack; +import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess; + +import org.lwjgl.glfw.GLFW; + +/** + * Handles keyboard input events for the Saturn Client. + * Manages keybindings and their associated actions. + */ +public class KeyInputHandler { + private static final String KEY_CATEGORY = "key.category.saturnclient"; + private static final String MAIN_MENU_KEY_ID = "key.saturnclient.main_menu"; + private static final int DEFAULT_MENU_KEY = GLFW.GLFW_KEY_RIGHT_SHIFT; + private static KeyBinding mainMenuKeyBinding; + + public static void register() { + mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding( + MAIN_MENU_KEY_ID, + InputUtil.Type.KEYSYM, + DEFAULT_MENU_KEY, + KEY_CATEGORY)); + + // Register the event handler for the main menu key + ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (mainMenuKeyBinding.wasPressed()) { + Providers.saturn.getClient().setScreen(new ShiftMenu()); + } + + if (Config.openEmoteWheel.wasKeyPressed() && client.currentScreen == null + && !InputUtil.isKeyPressed(client.getWindow().getHandle(), GLFW.GLFW_KEY_F3)) { + Providers.saturn.getClient().setScreen(new EmoteWheel()); + } + + if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()) { + AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(SaturnClient.client.player); + if (animationStack.isActive()) { + animationStack.removeLayer(1000); + ServiceClient.emote(""); + } + } + }); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/DrawContextAccessor.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/DrawContextAccessor.java new file mode 100644 index 0000000..f049e40 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/DrawContextAccessor.java @@ -0,0 +1,12 @@ +package org.saturnclient.saturnclient.mixin; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.render.VertexConsumerProvider; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(DrawContext.class) +public interface DrawContextAccessor { + @Accessor("vertexConsumers") + VertexConsumerProvider.Immediate getVertexConsumers(); +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/NameTagMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/NameTagMixin.java new file mode 100644 index 0000000..f23190c --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/NameTagMixin.java @@ -0,0 +1,101 @@ +package org.saturnclient.saturnclient.mixin; + +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.font.TextRenderer.TextLayerType; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityAttachmentType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.Vec3d; + +import org.saturnclient.client.player.Roles; +import org.saturnclient.client.player.SaturnPlayer; +import org.saturnclient.saturnclient.SaturnClient; + +import java.util.UUID; + +import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = EntityRenderer.class, priority = 1800) +public abstract class NameTagMixin { + @Shadow + public EntityRenderDispatcher dispatcher; + + @Shadow + private TextRenderer textRenderer; + + /** + * @author HexLeo + * @reason Adds the Saturn Client icon to the player's name if they are online + * with Saturn Client + */ + @Inject(method = "renderLabelIfPresent(Lnet/minecraft/client/render/entity/state/EntityRenderState;Lnet/minecraft/text/Text;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At("HEAD"), cancellable = true) + protected void renderLabelIfPresent(T entity, Text text, MatrixStack matrices, + VertexConsumerProvider vertexConsumers, int light, float tickDelta, CallbackInfo ci) { + + UUID uuid = isSaturn(entity); + + if (uuid != null) { + MutableText iconText = Text.literal(Roles.getSaturnIndicator()) + .styled(style -> style.withColor(Roles.getIconColor(uuid))); + + Text nameText = text.copy().styled(style -> style.withColor(Formatting.WHITE)); + + renderLabel(entity, iconText.append(nameText), matrices, vertexConsumers, light, tickDelta); + + ci.cancel(); + } + } + + private UUID isSaturn(T entity) { + if (entity instanceof PlayerEntity playerEntity) { + SaturnPlayer player = SaturnPlayer.get(playerEntity.getUuid()); + if (player != null) { + return player.uuid; + } + } + + return null; + } + + private void renderLabel(T entity, Text text, MatrixStack matrices, + VertexConsumerProvider vertexConsumers, int light, float tickDelta) { + double d = this.dispatcher.getSquaredDistanceToCamera(entity); + if (!(d > (double) 4096.0F)) { + Vec3d vec3d = entity.getAttachments().getPointNullable(EntityAttachmentType.NAME_TAG, 0, + entity.getYaw(tickDelta)); + if (vec3d != null) { + boolean bl = !entity.isSneaky(); + int i = "deadmau5".equals(text.getString()) ? -10 : 0; + matrices.push(); + matrices.translate(vec3d.x, vec3d.y + (double) 0.5F, vec3d.z); + matrices.multiply(this.dispatcher.getRotation()); + matrices.scale(0.025F, -0.025F, 0.025F); + Matrix4f matrix4f = matrices.peek().getPositionMatrix(); + float f = SaturnClient.client.options.getTextBackgroundOpacity(0.25F); + int j = (int) (f * 255.0F) << 24; + TextRenderer textRenderer = this.textRenderer; + float g = (float) (-textRenderer.getWidth(text) / 2); + textRenderer.draw(text, g, (float) i, 553648127, false, matrix4f, vertexConsumers, + bl ? TextLayerType.SEE_THROUGH : TextLayerType.NORMAL, j, light); + if (bl) { + textRenderer.draw(text, g, (float) i, -1, false, matrix4f, vertexConsumers, TextLayerType.NORMAL, 0, + light); + } + + matrices.pop(); + } + } + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java new file mode 100644 index 0000000..f0e169d --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/PlayerEntityRendererMixin.java @@ -0,0 +1,29 @@ +package org.saturnclient.saturnclient.mixin; + +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.entity.EntityRendererFactory; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.PlayerEntityRenderer; +import net.minecraft.client.render.entity.model.PlayerEntityModel; + +import org.saturnclient.impl.cosmetics.CloakFeatureRenderer; +import org.saturnclient.impl.cosmetics.HatFeatureRenderer; +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(PlayerEntityRenderer.class) +public abstract class PlayerEntityRendererMixin + extends LivingEntityRenderer> { + + protected PlayerEntityRendererMixin() { + super(null, null, 0.0f); + } + + @Inject(method = "", at = @At("TAIL")) + private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) { + this.addFeature(new HatFeatureRenderer(this)); + this.addFeature(new CloakFeatureRenderer(this)); + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/SetScreenMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/SetScreenMixin.java new file mode 100644 index 0000000..061d641 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/SetScreenMixin.java @@ -0,0 +1,27 @@ +package org.saturnclient.saturnclient.mixin; + +import org.saturnclient.config.Config; +import org.saturnclient.impl.ui.SaturnScreenFabric; +import org.saturnclient.ui.screens.TitleMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.TitleScreen; + +@Mixin(MinecraftClient.class) +public class SetScreenMixin { + @ModifyVariable(method = "setScreen", at = @At("HEAD"), argsOnly = true) + private Screen replaceTitleScreen(Screen screen) { + if (Config.saturnTitleScreen.value) { + if (screen instanceof TitleScreen) + return new SaturnScreenFabric(new TitleMenu()); + if (screen == null && ((MinecraftClient) (Object) this).world == null) + return new SaturnScreenFabric(new TitleMenu()); + } + + return screen; + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/SplashOverlayMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/SplashOverlayMixin.java new file mode 100644 index 0000000..ecc4823 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/SplashOverlayMixin.java @@ -0,0 +1,161 @@ +package org.saturnclient.saturnclient.mixin; + +import java.util.Optional; +import java.util.function.Consumer; + +import org.saturnclient.config.Theme; +import org.saturnclient.impl.ui.RenderScopeImpl; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.resources.Textures; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Final; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.SplashOverlay; +import net.minecraft.resource.ResourceReload; +import net.minecraft.util.Util; +import net.minecraft.util.math.MathHelper; + +@Mixin(SplashOverlay.class) +public abstract class SplashOverlayMixin { + + @Shadow + private MinecraftClient client; + @Shadow + private ResourceReload reload; + + @Shadow + private long reloadCompleteTime; + + @Shadow + private long reloadStartTime; + + @Shadow + @Final + private boolean reloading; + + @Shadow + @Final + private Consumer> exceptionHandler; + + @Shadow + private float progress; + + /** + * @author SaturnClient + * @reason Replace vanilla splash rendering completely but keep exit logic + */ + @Overwrite + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + int screenWidth = context.getScaledWindowWidth(); + int screenHeight = context.getScaledWindowHeight(); + long currentTime = Util.getMeasuringTimeMs(); + + // Initialize reload start time if not set + if (reloadStartTime == -1L) { + reloadStartTime = currentTime; + } + + // Compute fade-out progress after reload completes + float fadeOutProgress = reloadCompleteTime > -1L + ? (float) (currentTime - reloadCompleteTime) / 1000f + : -1f; + + int alpha = fadeOutProgress >= 0f + ? MathHelper.ceil((1.0f - MathHelper.clamp(fadeOutProgress - 1f, 0f, 1f)) * 255f) + : 255; + + // Compute fade-in progress while reloading + float reloadProgress = reloadStartTime > -1L + ? (float) (currentTime - reloadStartTime) / 500f + : -1f; + + // Get render scope for better UI + RenderScope scope = new RenderScopeImpl(context); + + // Draw background overlay + if (fadeOutProgress >= 1.0F) { + renderCurrentScreen(context, mouseX, mouseY, delta); + int overlayAlpha = computeOverlayAlpha(fadeOutProgress - 1f); + scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight, + Theme.withAlpha(overlayAlpha, 0xFFFFFFFF)); + } else if (reloading) { + if (reloadProgress < 1.0F) { + renderCurrentScreen(context, mouseX, mouseY, delta); + } + scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight, + Theme.withAlpha(alpha, 0xFFFFFFFF)); + } else { + scope.drawTexture(Textures.SPLASH, 0, 0, 0, 0, screenWidth, screenHeight, + Theme.withAlpha(alpha, 0xFFFFFFFF)); + } + + float u = this.reload.getProgress(); + this.progress = MathHelper.clamp(this.progress * 0.95F + u * 0.050000012F, 0.0F, 1.0F); + + // Draw central logo + drawLogo(scope, screenWidth, screenHeight, alpha); + renderProgressBar(scope, context, screenWidth, screenHeight, alpha); + + // Remove overlay after fade-out completes + if (fadeOutProgress >= 2.0F) { + client.setOverlay(null); + } + + // Handle reload completion and exceptions + handleReloadCompletion(context); + } + + private void renderCurrentScreen(DrawContext context, int mouseX, int mouseY, float delta) { + if (client.currentScreen != null) { + client.currentScreen.render(context, mouseX, mouseY, delta); + } + } + + /** Draw the main logo centered */ + private void drawLogo(RenderScope scope, int screenWidth, int screenHeight, int alpha) { + scope.drawTexture(Textures.LOGO, (screenWidth - 60) / 2, ((screenHeight - 60) / 2) - 20, 0, 0, 60, 60, + Theme.withAlpha(alpha, 0xFFFFFFFF)); + } + + private void renderProgressBar(RenderScope scope, DrawContext context, int screenWidth, int screenHeight, + int alpha) { + int progressY = ((screenHeight - 5) / 2) + 25; + context.drawBorder((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF)); + + int progressWidth = Math.min((int) (progress * 120), 120); + int progressX = (screenWidth - 120) / 2; + + context.fill(progressX, progressY, progressX + progressWidth, progressY + 5, + Theme.withAlpha(alpha, 0xFFFFFFFF)); + } + + /** Compute overlay alpha from fade progress */ + private int computeOverlayAlpha(float progress) { + return MathHelper.ceil((1.0F - MathHelper.clamp(progress, 0.0F, 1.0F)) * 255.0F); + } + + /** Handle reload completion logic */ + private void handleReloadCompletion(DrawContext context) { + float reloadProgress = reloadStartTime > -1L ? (float) (Util.getMeasuringTimeMs() - reloadStartTime) / 500f + : -1f; + + if (reloadCompleteTime == -1L && reload.isComplete() && (!reloading || reloadProgress >= 2.0F)) { + try { + reload.throwException(); + exceptionHandler.accept(Optional.empty()); + } catch (Throwable ex) { + exceptionHandler.accept(Optional.of(ex)); + } + + reloadCompleteTime = Util.getMeasuringTimeMs(); + + if (client.currentScreen != null) { + client.currentScreen.init(client, context.getScaledWindowWidth(), context.getScaledWindowHeight()); + } + } + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/TabListMixin.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/TabListMixin.java new file mode 100644 index 0000000..5dd00f6 --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/TabListMixin.java @@ -0,0 +1,43 @@ +package org.saturnclient.saturnclient.mixin; + +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.UUID; + +import org.jetbrains.annotations.Nullable; +import org.saturnclient.client.player.Roles; +import org.saturnclient.client.player.SaturnPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import com.mojang.authlib.GameProfile; + +@Mixin(PlayerListEntry.class) +public abstract class TabListMixin { + + @Shadow + private GameProfile profile; + + /** + * @author HexLeo + * @reason Adds the Saturn Client icon to the player's name if they are online + * with Saturn Client + */ + @Overwrite + @Nullable + public Text getDisplayName() { + GameProfile profile = ((PlayerListEntry) (Object) this).getProfile(); + String name = profile.getName(); + UUID uuid = profile.getId(); + + if (SaturnPlayer.get(uuid) != null) { + return Text.literal(Roles.getSaturnIndicator()) + .styled(style -> style.withColor(Roles.getIconColor(uuid))) + .append(Text.literal(name).styled(style -> style.withColor(Formatting.WHITE))); + } + return Text.literal(name); + } +} diff --git a/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/WhoAmI.java b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/WhoAmI.java new file mode 100644 index 0000000..041e0cf --- /dev/null +++ b/versions/1.21.1/src/main/java/org/saturnclient/saturnclient/mixin/WhoAmI.java @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/* + * Original code from FokshaWasTaken (GitHub), + * Repository: https://github.com/FokshaWasTaken/who-am-i/ +*/ + +package org.saturnclient.saturnclient.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LivingEntityRenderer.class) +public class WhoAmI { + @ModifyExpressionValue(method = "hasLabel(Lnet/minecraft/entity/LivingEntity;D)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getCameraEntity()Lnet/minecraft/entity/Entity;")) + public Entity obfuscateCameraEntity(Entity original) { + // hasLabel checks if the rendered entity is equal to the camera entity (aka our + // player), + // so by setting the camera entity to null in this instance makes the check + // always succeed + // this also allows to keep the other checks in mojang's code, such as checking + // if the hud is enabled etc. + return null; + } +} \ No newline at end of file diff --git a/versions/1.21.1/src/main/resources/fabric.mod.json b/versions/1.21.1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..f447ac6 --- /dev/null +++ b/versions/1.21.1/src/main/resources/fabric.mod.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "id": "saturnclient", + "version": "${version}", + "name": "saturnclient", + "description": "The ultimate open source client", + "authors": ["Saturn", "ORUS"], + "contact": { + "website": "https://selimaj.dev", + "sources": "https://github.com/saturnclientmc/saturnclient" + }, + "license": "Apache-2.0", + "icon": "assets/saturnclient/icon.png", + "environment": "client", + "entrypoints": { + "main": ["org.saturnclient.saturnclient.SaturnClient"] + }, + "mixins": [ + "saturnclient.mixins.json", + "mods.mixins.json", + "impl.mixins.json" + ], + "depends": { + "fabricloader": ">=${loader_version}", + "fabric": "*", + "minecraft": "${minecraft_version}" + } +} diff --git a/versions/1.21.1/src/main/resources/impl.mixins.json b/versions/1.21.1/src/main/resources/impl.mixins.json new file mode 100644 index 0000000..15e322f --- /dev/null +++ b/versions/1.21.1/src/main/resources/impl.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.saturnclient.impl.mixins", + "compatibilityLevel": "JAVA_21", + "client": [ + "IdentifierMixin", + "ItemStackMixin", + "MatrixStackMixin", + "MinecraftClientMixin", + "QuaternionfMixin", + "WindowMixin", + "TextMixin", + "SpriteMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/versions/1.21.1/src/main/resources/mods.mixins.json b/versions/1.21.1/src/main/resources/mods.mixins.json new file mode 100644 index 0000000..c402565 --- /dev/null +++ b/versions/1.21.1/src/main/resources/mods.mixins.json @@ -0,0 +1,17 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.saturnclient.impl.modules.mixins", + "compatibilityLevel": "JAVA_21", + "client": [ + "GameRendererMixin", + "LightmapTextureManagerMixin", + "LivingEntityRendererMixin", + "NametagsMixin", + "RenderMixin", + "TpsMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/versions/1.21.1/src/main/resources/saturnclient.mixins.json b/versions/1.21.1/src/main/resources/saturnclient.mixins.json new file mode 100644 index 0000000..3bc9ccc --- /dev/null +++ b/versions/1.21.1/src/main/resources/saturnclient.mixins.json @@ -0,0 +1,18 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.saturnclient.saturnclient.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "TabListMixin", + "NameTagMixin", + "PlayerEntityRendererMixin", + "WhoAmI", + "DrawContextAccessor", + "SplashOverlayMixin", + "SetScreenMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}