Build 1.21.10 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 7m13s

Step 2 of #8. versions/1.21.10/src is gone; 1.21.10 builds from src/ as
:1.21.10, with versions/1.21.10 keeping only gradle.properties and run/.

API differences go behind version comments: render layers (ShaderUtils),
Camera.update's World vs BlockView (CameraMixin), the text and textured-quad
render states (RenderScopeImpl), Screen.init (SplashOverlayMixin) and
jspecify's @NonNull (SaturnRenderState).

Features 1.21.11 leaves out stay on 1.21.10, with a TODO for 1.21.11:
WorldFeatureImpl.getWorldAge, SaturnScreenFabric.resize and
EntityDrawerImpl clearing the hitbox.

Hats now render on 1.21.10: its HatFeatureRenderer overrode the
render-queue method with an empty body, and the working code sat in an old
VertexConsumerProvider method nothing called. 1.21.10 now uses 1.21.11's
render-queue implementation (HatFeatureRenderer, ObjModel, ObjRenderer),
and also gets 1.21.11's null check on the player's display name.

The 1.21.10 jar has the same entries, fabric.mod.json and mixin configs as
before; only those four classes and ShaderUtils (a helper method for
entityAlpha) have different bytecode. The 1.21.11 jar only differs in
ShaderUtils.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-27 05:05:03 +02:00
co-authored by claude
parent a30cf4d3ab
commit 95e4dff448
66 changed files with 80 additions and 3598 deletions
-101
View File
@@ -1,101 +0,0 @@
plugins {
id 'net.fabricmc.fabric-loom-remap' version "${loom_version}"
id 'maven-publish'
}
version = "${project.mod_version}+${project.minecraft_version}"
group = project.maven_group
base {
archivesName = project.archives_base_name
}
repositories {
maven { name = "Selimaj"; url = 'https://git.selimaj.dev/api/packages/selimaj-dev/maven' }
}
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}"
// Saturn's emote library (limb bending, Emotecraft emotes)
include modImplementation("org.saturnclient:emotes-fabric:${project.emotes_version}+${project.minecraft_version}")
implementation project(":common")
include implementation("dev.selimaj:session-java:0.2.0")
include implementation("de.javagl:obj:0.4.0")
include implementation("com.formdev:svgSalamander:1.1.4")
include implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
include implementation("com.fasterxml.jackson.core:jackson-annotations:2.17.0")
include implementation("com.fasterxml.jackson.core:jackson-core:2.17.0")
}
/*
* ================================================================
* 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}"}
}
from project(":common").sourceSets.main.output
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
// 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.
}
}
-1
View File
@@ -10,7 +10,6 @@ org.gradle.configuration-cache=false
minecraft_version=1.21.10
yarn_mappings=1.21.10+build.3
loader_version=0.19.0
loom_version=1.17.21
fabric_api_version=0.138.4+1.21.10
-10
View File
@@ -1,10 +0,0 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}
@@ -1,462 +0,0 @@
package org.saturnclient.impl.cosmetics;
import java.util.Arrays;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Config;
import org.saturnclient.cosmetics.Cloaks;
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
import org.saturnclient.saturnclient.SaturnRenderState;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
import net.minecraft.client.render.entity.equipment.EquipmentModel;
import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType;
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.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.EquippableComponent;
import net.minecraft.item.ItemStack;
import net.minecraft.item.equipment.EquipmentAsset;
import net.minecraft.registry.RegistryKey;
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<PlayerEntityRenderState, PlayerEntityModel> {
private final EquipmentModelLoader equipmentModelLoader;
private static final int PARTS = 24;
private final float[] segmentValues = new float[PARTS];
private static final int H_PARTS = 6;
private float horizontalCurve = 0.0f;
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<PlayerEntityRenderState, PlayerEntityModel> context,
EquipmentModelLoader equipmentModelLoader) {
super(context);
this.equipmentModelLoader = equipmentModelLoader;
Arrays.fill(segmentValues, 0.0f);
}
private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) {
EquippableComponent equippableComponent = (EquippableComponent) stack
.get(DataComponentTypes.EQUIPPABLE);
if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
EquipmentModel equipmentModel = this.equipmentModelLoader
.get((RegistryKey<EquipmentAsset>) equippableComponent.assetId().get());
return !equipmentModel.getLayers(layerType).isEmpty();
} else {
return false;
}
}
private void vertex(VertexConsumer vc, Matrix4f posMat, Matrix3f normalMat,
Vec3d pos, int color,
float u, float v, int overlay, int light,
float nx, float ny, float nz) {
Vector4f p = new Vector4f((float) pos.x, (float) pos.y, (float) pos.z, 1.0f);
p.mul(posMat);
Vector3f n = new Vector3f(nx, ny, nz);
n.mul(normalMat);
vc.vertex(
p.x(), p.y(), p.z(),
color,
u, v,
overlay, light,
n.x(), n.y(), n.z());
}
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack.Entry entry,
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) {
// Extract matrix (you must transform positions manually now)
Matrix4f pos = entry.getPositionMatrix();
Matrix3f normalMat = entry.getNormalMatrix();
int color = 0xFFFFFFFF; // white RGBA
if (!flipWinding) {
// CCW
vertex(vertexConsumer, pos, normalMat, bottomLeft, color, u1, v2, overlay, light, normalX, normalY,
normalZ);
vertex(vertexConsumer, pos, normalMat, bottomRight, color, u2, v2, overlay, light, normalX, normalY,
normalZ);
vertex(vertexConsumer, pos, normalMat, topRight, color, u2, v1, overlay, light, normalX, normalY, normalZ);
vertex(vertexConsumer, pos, normalMat, topLeft, color, u1, v1, overlay, light, normalX, normalY, normalZ);
} else {
// CW
vertex(vertexConsumer, pos, normalMat, topLeft, color, u1, v1, overlay, light, normalX, normalY, normalZ);
vertex(vertexConsumer, pos, normalMat, topRight, color, u2, v1, overlay, light, normalX, normalY, normalZ);
vertex(vertexConsumer, pos, normalMat, bottomRight, color, u2, v2, overlay, light, normalX, normalY,
normalZ);
vertex(vertexConsumer, pos, normalMat, bottomLeft, color, u1, v2, overlay, light, normalX, normalY,
normalZ);
}
}
private void renderCape(MatrixStack.Entry entry, VertexConsumer vertexConsumer,
PlayerEntityRenderState playerState, int light, int overlay) {
float capeWidth = 10.0f / 16.0f;
float capeHeight = 16.0f / 16.0f;
float capeDepth = 1.0f / 16.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();
// =============================
// 1. Spine
// =============================
float[] spineY = new float[PARTS + 1];
float[] spineZ = new float[PARTS + 1];
float partHeight = capeHeight / PARTS;
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);
spineY[i + 1] = spineY[i] + dirY * partHeight;
spineZ[i + 1] = spineZ[i] + dirZ * partHeight;
}
// =============================
// 2. Grid
// =============================
Vec3d[][] inner = new Vec3d[H_PARTS + 1][PARTS + 1];
Vec3d[][] outer = new Vec3d[H_PARTS + 1][PARTS + 1];
for (int x = 0; x <= H_PARTS; x++) {
float t = (float) x / H_PARTS;
float xPos = -capeWidth / 2.0f + capeWidth * t;
float centered = t - 0.5f;
float curve = (centered * centered) * 0.3f;
float zCurve = (curve * horizontalCurve) / PARTS;
for (int i = 0; i <= PARTS; i++) {
float y = spineY[i];
float z = spineZ[i];
int idx = Math.min(i, PARTS - 1);
// angle: 0 is horizontal (y-flat), PI/2 is vertical (z-flat)
float angle = (2.0f - segmentValues[idx]) * ((float) Math.PI / 2f);
float cosA = (float) Math.cos(angle);
float sinA = (float) Math.sin(angle);
// Calculate how much the horizontal curve shifts the vertex
float currentCurve = zCurve * i;
// Rotate the curve offset so it aligns with the cape's tilt
float curveY = sinA * currentCurve;
float curveZ = cosA * currentCurve;
float thickY = sinA * capeDepth;
float thickZ = cosA * capeDepth;
inner[x][i] = new Vec3d(xPos, y - curveY, z - curveZ);
outer[x][i] = new Vec3d(xPos, (y - curveY) - thickY, (z - curveZ) - thickZ);
}
}
// =============================
// 3. Front + Back
// =============================
for (int x = 0; x < H_PARTS; x++) {
float uStart = frontU1 + (frontU2 - frontU1) * (1.0f - ((float) x / H_PARTS));
float uEnd = frontU1 + (frontU2 - frontU1) * (1.0f - ((float) (x + 1) / H_PARTS));
float buStart = backU1 + (backU2 - backU1) * ((float) x / H_PARTS);
float buEnd = backU1 + (backU2 - backU1) * ((float) (x + 1) / H_PARTS);
for (int i = 0; i < PARTS; i++) {
float vStart = frontV1 + frontPartV * i;
float vEnd = frontV1 + frontPartV * (i + 1);
float bvStart = backV1 + backPartV * i;
float bvEnd = backV1 + backPartV * (i + 1);
Vec3d innerTL = inner[x + 1][i];
Vec3d innerTR = inner[x][i];
Vec3d innerBL = inner[x + 1][i + 1];
Vec3d innerBR = inner[x][i + 1];
Vec3d outerTL = outer[x + 1][i];
Vec3d outerTR = outer[x][i];
Vec3d outerBL = outer[x + 1][i + 1];
Vec3d outerBR = outer[x][i + 1];
// FRONT
renderCapeQuad(vertexConsumer, entry,
outerBR, outerBL, outerTL, outerTR,
uStart, vStart, uEnd, vEnd,
light, overlay, 0, 0, 1, false);
// BACK
renderCapeQuad(vertexConsumer, entry,
innerBL, innerBR, innerTR, innerTL,
buEnd, bvStart, buStart, bvEnd,
light, overlay, 0, 0, -1, false);
}
}
// =============================
// 4. LEFT SIDE (x = H_PARTS)
// =============================
for (int i = 0; i < PARTS; i++) {
Vec3d innerTop = inner[H_PARTS][i];
Vec3d innerBot = inner[H_PARTS][i + 1];
Vec3d outerTop = outer[H_PARTS][i];
Vec3d outerBot = outer[H_PARTS][i + 1];
float vStart = leftV1 + leftPartV * i;
float vEnd = leftV1 + leftPartV * (i + 1);
renderCapeQuad(vertexConsumer, entry,
innerTop, outerTop, outerBot, innerBot,
leftU1, vEnd,
leftU2, vStart,
light, overlay, -1, 0, 0, false);
}
// =============================
// 5. RIGHT SIDE (x = 0)
// =============================
for (int i = 0; i < PARTS; i++) {
Vec3d innerTop = inner[0][i];
Vec3d innerBot = inner[0][i + 1];
Vec3d outerTop = outer[0][i];
Vec3d outerBot = outer[0][i + 1];
float vStart = rightV1 + rightPartV * i;
float vEnd = rightV1 + rightPartV * (i + 1);
renderCapeQuad(vertexConsumer, entry,
outerTop, innerTop, innerBot, outerBot,
rightU1, vEnd,
rightU2, vStart,
light, overlay, 1, 0, 0, false);
}
// =============================
// 6. TOP (i = 0)
// =============================
for (int x = 0; x < H_PARTS; x++) {
Vec3d outerL = outer[x][0];
Vec3d outerR = outer[x + 1][0];
Vec3d innerR = inner[x + 1][0];
Vec3d innerL = inner[x][0];
float uStart = topU1 + (topU2 - topU1) * ((float) x / H_PARTS);
float uEnd = topU1 + (topU2 - topU1) * ((float) (x + 1) / H_PARTS);
renderCapeQuad(vertexConsumer, entry,
outerL, outerR, innerR, innerL,
uStart, topV1, uEnd, topV2,
light, overlay, 0, 1, 0, false);
}
// =============================
// 7. BOTTOM (i = PARTS)
// =============================
for (int x = 0; x < H_PARTS; x++) {
Vec3d innerL = inner[x][PARTS];
Vec3d innerR = inner[x + 1][PARTS];
Vec3d outerR = outer[x + 1][PARTS];
Vec3d outerL = outer[x][PARTS];
float uStart = bottomU1 + (bottomU2 - bottomU1) * ((float) x / H_PARTS);
float uEnd = bottomU1 + (bottomU2 - bottomU1) * ((float) (x + 1) / H_PARTS);
renderCapeQuad(vertexConsumer, entry,
innerL, innerR, outerR, outerL,
uStart, bottomV1, uEnd, bottomV2,
light, overlay, 0, -1, 0, false);
}
}
// 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, OrderedRenderCommandQueue queue, int light, PlayerEntityRenderState state,
float limbAngle, float limbDistance) {
if (state.invisible || !state.capeVisible
|| state.skinTextures.cape() != null) {
return;
}
SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey);
if (player == null) {
return;
}
IdentifierRef customCape = Cloaks.getCurrentCloakTexture(player.cloak);
if (customCape == null
|| this.hasCustomModelForLayer(state.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);
matrices.push();
matrices.translate(0.0f, 0.0f, 0.12f);
if (this.hasCustomModelForLayer(state.equippedChestStack, LayerType.HUMANOID)) {
matrices.translate(0.0F, -0.053125F, 0.06875F);
}
if (state.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 = Math.min(0.8f, state.field_53537 / 108.0f);
float rawVelY = state.field_53536;
float velY = (rawVelY > 3.4f ? rawVelY : rawVelY < -3.4f ? rawVelY : 0.0f) / 16;
float value = Math.max(0.0f, Math.min(2.0f, state.isSwimming ? 0.0f : velX + velY));
if (Config.cloakPhysics.value) {
updateSegmentValues(segmentValues[0] + (value - segmentValues[0]) * 0.3f);
} else {
Arrays.fill(segmentValues, segmentValues[0] + (value - segmentValues[0]) * 0.05f);
}
lastUpdate = now;
}
final int l = light;
horizontalCurve = MathHelper.clamp(state.field_53537 / 150.0f, -1.0f, 1.0f);
queue.submitCustom(matrices, ShaderUtils.getRenderLayer(customCape), (entry, vertexConsumer) -> {
renderCape(entry, vertexConsumer, state, l, OverlayTexture.DEFAULT_UV);
});
matrices.pop();
}
}
@@ -1,51 +0,0 @@
package org.saturnclient.impl.cosmetics;
import org.joml.Quaternionf;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.impl.cosmetics.obj.MtlLoader;
import org.saturnclient.impl.cosmetics.obj.ObjModel;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
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.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.client.model.ModelPart;
public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
super(context);
}
// @Override
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
PlayerEntityRenderState state, float limbAngle, float limbDistance) {
if (state.displayName == null)
return;
SaturnPlayer player = SaturnPlayer.get(state.displayName.getString());
if (state.invisible || player == null || player.hat.isEmpty()) {
return;
}
matrices.push();
ModelPart head = this.getContextModel().head;
matrices.multiply(new Quaternionf().rotationZYX(head.roll, head.yaw, head.pitch));
ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
matrices, vertexConsumers, light,
OverlayTexture.DEFAULT_UV);
matrices.pop();
}
@Override
public void render(MatrixStack matrices, OrderedRenderCommandQueue queue, int light, PlayerEntityRenderState state,
float limbAngle, float limbDistance) {
}
}
@@ -1,56 +0,0 @@
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<Identifier, Map<String, Mtl>> loadedMtls = new HashMap<>();
static Map<String, Mtl> loadMtl(Identifier mtlId) throws IOException {
try (InputStream is = MinecraftClient.getInstance().getResourceManager()
.getResource(mtlId).get().getInputStream()) {
List<Mtl> mtlList = MtlReader.read(is);
Map<String, Mtl> mtlMap = new java.util.LinkedHashMap<>();
for (Mtl mtl : mtlList) {
mtlMap.put(mtl.getName(), mtl);
}
return mtlMap;
}
}
public static Map<String, Mtl> of(Identifier mtlId) {
if (loadedMtls.containsKey(mtlId))
return loadedMtls.get(mtlId);
try {
Map<String, Mtl> mtl = loadMtl(mtlId);
loadedMtls.put(mtlId, mtl);
return mtl;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public static Map<String, Mtl> 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"));
}
}
@@ -1,102 +0,0 @@
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<Identifier, ObjModel> 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> 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<String, Mtl> 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();
}
}
@@ -1,112 +0,0 @@
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
import de.javagl.obj.*;
import net.minecraft.client.MinecraftClient;
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<String, Mtl> mtlMap, MatrixStack matrices,
VertexConsumerProvider vertexConsumers, int light, int overlay) {
MatrixStack.Entry entry = matrices.peek();
Map<String, Obj> materialGroups = ObjSplitting.splitByMaterialGroups(obj);
for (Map.Entry<String, Obj> 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
IdentifierRef texture = IdentifierRef.ofVanilla("textures/misc/white.png");
if (mtl != null) {
String mapKd = mtl.getMapKd();
if (mapKd != null) {
if (!mapKd.startsWith("#")) {
texture = IdentifierRef.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(ShaderUtils.getRenderLayer(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);
}
}
@@ -1,71 +0,0 @@
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.setColorArgb(x, y, pixels[index++]);
}
}
} else {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, bufferedImage.getRGB(x, y));
}
}
}
SaturnClient.client.execute(() -> {
NativeImageBackedTexture texture = new NativeImageBackedTexture(
() -> identifier.toString(),
nativeImage);
SaturnClient.client.getTextureManager().registerTexture(identifier, texture);
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register texture: {}", identifierRef, e);
}
}
public static void registerBufferedImageTexturesBulk(Map<IdentifierRef, BufferedImage> textures) {
long start = System.currentTimeMillis();
textures.forEach(IdentifierUtils::registerBufferedImageTextureFast);
long end = System.currentTimeMillis();
SaturnClient.LOGGER.info(
"Registered {} textures in {}ms",
textures.size(),
(end - start));
}
}
@@ -1,39 +0,0 @@
package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.cosmetics.utils.GifUtils;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import net.minecraft.client.gl.RenderPipelines;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderPhase.Textures;
import net.minecraft.client.render.block.entity.EndPortalBlockEntityRenderer;
import net.minecraft.util.Identifier;
public class ShaderUtils {
public static final RenderPipeline END_GATEWAY_PIPELINE = RenderPipelines.register(
RenderPipeline.builder(new RenderPipeline.Snippet[] { RenderPipelines.RENDERTYPE_END_PORTAL_SNIPPET })
.withLocation("pipeline/end_gateway")
.withShaderDefine("PORTAL_LAYERS", 16)
.withCull(false)
.build());
public static final RenderLayer END_GATEWAY = RenderLayer.of("end_gateway", 1536, false, false,
END_GATEWAY_PIPELINE,
RenderLayer.MultiPhaseParameters.builder()
.texture(Textures.create().add(EndPortalBlockEntityRenderer.SKY_TEXTURE, false)
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false).build())
.build(false));
public static RenderLayer getRenderLayer(IdentifierRef texture) {
String path = texture.toString();
if (path.endsWith(".gif")) {
return RenderLayer.getEntityAlpha((Identifier) (Object) GifUtils.get(texture));
} else if (path.endsWith("end.png")) {
return END_GATEWAY;
} else {
return RenderLayer.getEntityAlpha((Identifier) (Object) texture);
}
}
}
@@ -1,11 +0,0 @@
package org.saturnclient.impl.features.entity;
public interface CameraOverriddenEntity {
float freelook$getCameraPitch();
float freelook$getCameraYaw();
void freelook$setCameraPitch(float pitch);
void freelook$setCameraYaw(float yaw);
}
@@ -1,79 +0,0 @@
package org.saturnclient.impl.features.entity;
import org.saturnclient.common.feature.EntityFeature;
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;
/**
* Fabric implementation of {@link EntityFeature}.
*
* 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 EntityFeatureImpl implements EntityFeature {
private final MinecraftClient mc;
public EntityFeatureImpl(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();
}
}
}
@@ -1,24 +0,0 @@
package org.saturnclient.impl.features.entity;
import org.saturnclient.common.feature.EntityFeature;
/**
* 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);
EntityFeature.EntityType saturn$getEntityType();
void saturn$setEntityType(EntityFeature.EntityType type);
}
@@ -1,46 +0,0 @@
package org.saturnclient.impl.features.mixins.entity;
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
import org.saturnclient.mod.mods.FreelookMod;
import org.saturnclient.saturnclient.SaturnClient;
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;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.world.BlockView;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Unique
boolean firstTime = true;
@Shadow
protected abstract void setRotation(float yaw, float pitch);
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(BlockView focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing,
float tickDelta, CallbackInfo ci) {
if (FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
if (firstTime && SaturnClient.client.player != null) {
cameraOverriddenEntity.freelook$setCameraPitch(SaturnClient.client.player.getPitch());
cameraOverriddenEntity.freelook$setCameraYaw(SaturnClient.client.player.getYaw());
firstTime = false;
}
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(),
cameraOverriddenEntity.freelook$getCameraPitch());
}
if (!FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
firstTime = true;
}
}
}
@@ -1,62 +0,0 @@
package org.saturnclient.impl.features.mixins.entity;
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
import org.saturnclient.mod.mods.FreelookMod;
import org.spongepowered.asm.mixin.Mixin;
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;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.MathHelper;
@Mixin(Entity.class)
public class EntityMixin implements CameraOverriddenEntity {
@Unique
private float cameraPitch;
@Unique
private float cameraYaw;
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
// noinspection ConstantValue// IntelliJ is incorrect here, this code block is
// reachable
if (FreelookMod.isFreeLooking() && (Object) this instanceof ClientPlayerEntity) {
double pitchDelta = (yDelta * 0.15);
double yawDelta = (xDelta * 0.15);
this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
this.cameraYaw += (float) yawDelta;
ci.cancel();
}
}
@Override
@Unique
public float freelook$getCameraPitch() {
return this.cameraPitch;
}
@Override
@Unique
public float freelook$getCameraYaw() {
return this.cameraYaw;
}
@Override
@Unique
public void freelook$setCameraPitch(float pitch) {
this.cameraPitch = pitch;
}
@Override
@Unique
public void freelook$setCameraYaw(float yaw) {
this.cameraYaw = yaw;
}
}
@@ -1,21 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.fog.FogRenderer;
import org.saturnclient.mod.mods.NoFogMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(FogRenderer.class)
public class BackgroundRendererMixin {
// fog
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
private static float modifyFogStart(float viewDistance) {
if (NoFogMod.isActive()) {
return viewDistance * 2.0f;
}
return viewDistance;
}
}
@@ -1,37 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.GameRenderer;
import org.saturnclient.mod.mods.ZoomMod;
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 ZoomMod} is active.
*
* The old code checked both {@code Zoom.isZooming} and
* {@code Zoom.shouldZoom()} separately. The refactored
* {@link ZoomMod#shouldZoom()} already ORs both conditions
* ({@code enabled.value && isZooming}), so only one check is needed.
*
* No {@link org.saturnclient.common.module.ModProvider} 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<Float> cir) {
if (ZoomMod.shouldZoom()) {
float zoomedFov = cir.getReturnValue() / ZoomMod.getZoomLevel();
cir.setReturnValue(zoomedFov);
}
}
}
@@ -1,35 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
// 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.mod.mods.FullbrightMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Overrides the lightmap's gamma computation when
* {@link FullbrightMod} 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 FullbrightMod}. 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 (FullbrightMod.shouldOverrideBrightness()) {
return FullbrightMod.getBrightnessValue();
}
return original;
}
}
@@ -1,59 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Implements {@link HealthRenderState} on
* {@link LivingEntityRenderState}, injecting health and entity-type
* storage into every living entity's render-state snapshot.
*
* Previously imported {@code NametagsInterface.EntityType}; the enum
* now lives in {@link EntityFeature.EntityType} which is the single
* canonical definition shared by the mixin layer and the feature layer.
*
* The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read by
* {@link NametagsMixin} at render time.
*/
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState {
@Unique
private float saturn$health = 0f;
@Unique
private float saturn$maxHealth = 1f;
@Unique
private EntityFeature.EntityType saturn$entityType = EntityFeature.EntityType.OTHER;
@Override
public float saturn$getHealth() {
return saturn$health;
}
@Override
public float saturn$getMaxHealth() {
return saturn$maxHealth;
}
@Override
public void saturn$setHealth(float health, float maxHealth) {
this.saturn$health = health;
this.saturn$maxHealth = maxHealth;
}
@Override
public EntityFeature.EntityType saturn$getEntityType() {
return saturn$entityType;
}
@Override
public void saturn$setEntityType(EntityFeature.EntityType type) {
saturn$entityType = type;
}
}
@@ -1,51 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
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.feature.EntityFeature;
import org.saturnclient.impl.features.entity.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 EntityFeature.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<T extends LivingEntity, S extends LivingEntityRenderState> {
@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
EntityFeature.EntityType type;
if (entity instanceof PlayerEntity)
type = EntityFeature.EntityType.PLAYER;
else if (entity instanceof HostileEntity)
type = EntityFeature.EntityType.HOSTILE;
else if (entity instanceof PassiveEntity)
type = EntityFeature.EntityType.PASSIVE;
else
type = EntityFeature.EntityType.OTHER;
hrs.saturn$setEntityType(type);
}
}
@@ -1,82 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
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.impl.ui.RenderScopeImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModManager;
import org.saturnclient.saturnclient.SaturnClient;
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 ModManager}, and
* dispatches either {@link HudMod#renderHud} (for HUD elements) or
* {@link Mod#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);
for (Mod feature : ModManager.ENABLED_MODS) {
if (!feature.isEnabled())
continue;
if (feature instanceof HudMod hud) {
ModLayout dim = hud.getDimensions();
scope.getMatrixStack().push();
scope.getMatrixStack().translate(dim.x.value, (float) dim.y.value);
scope.getMatrixStack().scale(dim.scale.value, dim.scale.value);
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);
}
}
}
}
@@ -1,30 +0,0 @@
package org.saturnclient.impl.features.mixins.world;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket;
import org.saturnclient.mod.mods.TpsMod;
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 TpsMod}'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 TpsMod#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 TpsMod}.
*/
@Mixin(ClientPlayNetworkHandler.class)
public class TpsMixin {
@Inject(method = "onWorldTimeUpdate", at = @At("HEAD"))
private void onWorldTimeUpdate(WorldTimeUpdateS2CPacket packet, CallbackInfo ci) {
TpsMod.onTimePacket(packet.time());
}
}
@@ -1,37 +0,0 @@
package org.saturnclient.impl.features.network;
import org.saturnclient.common.feature.NetworkFeature;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.PlayerListEntry;
/**
* Fabric implementation of {@link NetworkFeature}.
*
* 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 NetworkFeatureImpl implements NetworkFeature {
private final MinecraftClient mc;
public NetworkFeatureImpl(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;
}
}
@@ -1,254 +0,0 @@
package org.saturnclient.impl.features.player;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.registry.Registries;
import net.minecraft.util.Identifier;
import org.saturnclient.common.feature.PlayerFeature;
import org.saturnclient.common.ref.game.EffectRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Fabric implementation of {@link PlayerFeature}.
*
* All access to {@link MinecraftClient} and {@link ClientPlayerEntity}
* is encapsulated here. Features only ever see the {@link PlayerFeature}
* 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 PlayerFeatureImpl implements PlayerFeature {
private final MinecraftClient mc;
public PlayerFeatureImpl(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().getEquippedStack(EquipmentSlot.HEAD) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getChestplate() {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.CHEST) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getLeggings() {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.LEGS) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getBoots() {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.FEET) : 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<? extends EffectRef> getActiveEffects() {
if (!hasPlayer())
return Collections.emptyList();
var effects = player().getActiveStatusEffects().values();
List<EffectRef> result = new ArrayList<>(effects.size());
for (StatusEffectInstance instance : effects) {
result.add((EffectRef) instance);
}
return result;
}
@Override
public List<? extends EffectRef> getDummyEffects() {
return List.of(
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2));
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static ItemStackRef wrap(ItemStack stack) {
return (ItemStackRef) (Object) stack;
}
}
@@ -1,64 +0,0 @@
package org.saturnclient.impl.features.render;
import org.saturnclient.common.feature.RenderFeature;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.option.Perspective;
/**
* Fabric implementation of {@link RenderFeature}.
*
* Window dimensions and FPS are read directly from
* {@link MinecraftClient}. Camera perspective control delegates to
* {@link net.minecraft.client.option.GameOptions}.
*/
public class RenderFeatureImpl implements RenderFeature {
private final MinecraftClient mc;
public RenderFeatureImpl(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);
}
}
@@ -1,36 +0,0 @@
package org.saturnclient.impl.features.world;
import org.saturnclient.common.feature.WorldFeature;
import net.minecraft.client.MinecraftClient;
/**
* Fabric implementation of {@link WorldFeature}.
*
* Reads the client world's time directly from {@link MinecraftClient}.
* Returns safe defaults when no world is loaded.
*/
public class WorldFeatureImpl implements WorldFeature {
private final MinecraftClient mc;
public WorldFeatureImpl(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;
}
}
@@ -1,13 +0,0 @@
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();
}
@@ -1,19 +0,0 @@
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();
}
@@ -1,162 +0,0 @@
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);
}
@Override
@SuppressWarnings("resource")
public String getPlayerListEntry(UUID uuid) {
var client = (MinecraftClient) (Object) this;
var handler = client.getNetworkHandler();
if (handler == null)
return null;
var entry = handler.getPlayerListEntry(uuid);
if (entry == null)
return null;
return entry.getProfile().name();
}
@Override
@SuppressWarnings("resource")
public UUID getPlayerListEntryUUID(String name) {
var client = (MinecraftClient) (Object) this;
var handler = client.getNetworkHandler();
if (handler == null)
return null;
var entry = handler.getPlayerListEntry(name);
if (entry == null)
return null;
return entry.getProfile().id();
}
}
@@ -1,9 +0,0 @@
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 {
}
@@ -1,9 +0,0 @@
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 {
}
@@ -1,51 +0,0 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.common.ref.game.EffectRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.util.Identifier;
@Mixin(StatusEffectInstance.class)
public abstract class StatusEffectMixin implements EffectRef {
@Shadow
public abstract boolean shouldShowIcon();
@Shadow
public abstract boolean isInfinite();
@Shadow
public abstract int getDuration();
@Shadow
public abstract RegistryEntry<StatusEffect> getEffectType();
@Override
public SpriteRef getIcon() {
return null;
}
@Override
public IdentifierRef getIconId() {
Identifier id = InGameHud.getEffectTexture(getEffectType());
return IdentifierRef.of(id.getNamespace(), "textures/" + id.getPath() + ".png");
}
@Override
public String getInfiniteText() {
return "∞";
}
@Override
public int getDurationSeconds() {
return getDuration() / 20; // ticks → seconds
}
}
@@ -1,25 +0,0 @@
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.text.StyleSpriteSource.Font;
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(new Font((Identifier) (Object) font))));
}
@Override
public default int getWidth() {
return SaturnClient.client.textRenderer.getWidth((Text) this);
}
}
@@ -1,22 +0,0 @@
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();
}
@@ -1,68 +0,0 @@
package org.saturnclient.impl.provider;
import net.minecraft.client.MinecraftClient;
import org.saturnclient.common.feature.*;
import org.saturnclient.common.provider.FeatureProvider;
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
import org.saturnclient.impl.features.network.NetworkFeatureImpl;
import org.saturnclient.impl.features.player.PlayerFeatureImpl;
import org.saturnclient.impl.features.render.RenderFeatureImpl;
import org.saturnclient.impl.features.world.WorldFeatureImpl;
/**
* Fabric implementation of {@link FeatureProvider}.
*
* 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.
*
* <pre>{@code
* FeatureProvider provider = new FeatureProviderFabric(MinecraftClient.getInstance());
* FeatureManager.register(new AutoSprintFeature(provider));
* FeatureManager.register(new CoordinatesFeature(provider));
* // …
* }</pre>
*/
public class FeatureProviderFabric implements FeatureProvider {
private final PlayerFeature player;
private final WorldFeature world;
private final EntityFeature entity;
private final RenderFeature render;
private final NetworkFeature network;
public FeatureProviderFabric(MinecraftClient mc) {
this.player = new PlayerFeatureImpl(mc);
this.world = new WorldFeatureImpl(mc);
this.entity = new EntityFeatureImpl(mc);
this.render = new RenderFeatureImpl(mc);
this.network = new NetworkFeatureImpl(mc);
}
@Override
public PlayerFeature player() {
return player;
}
@Override
public WorldFeature world() {
return world;
}
@Override
public EntityFeature entity() {
return entity;
}
@Override
public RenderFeature render() {
return render;
}
@Override
public NetworkFeature network() {
return network;
}
}
@@ -1,18 +0,0 @@
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));
}
}
@@ -1,25 +0,0 @@
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);
}
}
@@ -1,71 +0,0 @@
package org.saturnclient.impl.provider;
import java.awt.image.BufferedImage;
import java.util.UUID;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.toast.SystemToast;
import net.minecraft.text.Text;
import org.saturnclient.common.provider.SaturnProvider;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.game.MinecraftClientRef;
import org.saturnclient.cosmetics.Emotes;
import org.saturnclient.impl.cosmetics.utils.IdentifierUtils;
import org.saturnclient.saturnclient.SaturnClient;
public class SaturnProviderImpl implements SaturnProvider {
@Override
public MinecraftClientRef getClient() {
return (MinecraftClientRef) SaturnClient.client;
}
@Override
public void playEmote(UUID fromPlayerUuid, String emoteIdOrNull) {
if (fromPlayerUuid == null) {
return;
}
// An empty or unknown ID stops whatever the player is playing.
Emotes.get(emoteIdOrNull == null ? "" : emoteIdOrNull).ifPresentOrElse(
emote -> org.saturnclient.emotes.fabric.Emotes.play(fromPlayerUuid, emote.animation()),
() -> org.saturnclient.emotes.fabric.Emotes.stop(fromPlayerUuid));
}
@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);
}
/** Shown longer than the vanilla default, since these ask the player to act. */
private static final SystemToast.Type WARNING_TOAST = new SystemToast.Type(15_000L);
@Override
public String getModVersion() {
return FabricLoader.getInstance().getModContainer(SaturnClient.MOD_ID)
.map(mod -> mod.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
}
@Override
public void showWarning(String title, String message) {
MinecraftClient client = MinecraftClient.getInstance();
client.execute(() -> client.getToastManager().add(
SystemToast.create(client, WARNING_TOAST, Text.literal(title), Text.literal(message))));
}
@Override
public void registerBufferedImageTexture(IdentifierRef i, BufferedImage bi) {
IdentifierUtils.registerBufferedImageTextureFast(i, bi);
}
}
@@ -1,44 +0,0 @@
package org.saturnclient.impl.ref;
import org.joml.Matrix3x2fStack;
import org.saturnclient.common.ref.render.MatrixStackRef;
public class Matrix3x2fStackRef implements MatrixStackRef {
public final Matrix3x2fStack stack;
public Matrix3x2fStackRef(Matrix3x2fStack stack) {
this.stack = stack;
}
@Override
public void push() {
stack.pushMatrix();
}
@Override
public void pop() {
stack.popMatrix();
}
@Override
public void scale(float x, float y) {
stack.scale(x, y);
}
@Override
public void translate(float x, float y) {
stack.translate(x, y);
}
@Override
public void rotate(float angle) {
stack.rotate((float) Math.toRadians(angle));
}
@Override
public void rotate(float angle, float originX, float originY, float originZ) {
translate(originX, originY);
rotate(angle);
translate(-originX, -originY);
}
}
@@ -1,70 +0,0 @@
package org.saturnclient.impl.ui;
import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;
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.entity.EntityRenderManager;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.state.EntityRenderState;
import net.minecraft.entity.LivingEntity;
public class EntityDrawerImpl implements EntityDrawer {
@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,
SaturnClient.client.player);
}
}
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float scale,
float mouseX, float mouseY, LivingEntity entity) {
float f = (float) (x1 + x2) / 2.0F;
float g = (float) (y1 + y2) / 2.0F;
renderScope.enableScissor(x1, y1, x2, y2);
float h = (float) Math.atan((double) ((f - mouseX) / 40.0F));
float i = (float) Math.atan((double) ((g - mouseY) / 40.0F));
Quaternionf quaternionf = (new Quaternionf()).rotateZ((float) Math.PI);
Quaternionf quaternionf2 = (new Quaternionf()).rotateX(i * 20.0F * ((float) Math.PI / 180F));
quaternionf.mul(quaternionf2);
float j = entity.bodyYaw;
float k = entity.getYaw();
float l = entity.getPitch();
float m = entity.lastHeadYaw;
float n = entity.headYaw;
entity.bodyYaw = 180.0F + h * 20.0F;
entity.setYaw(180.0F + h * 40.0F);
entity.setPitch(-i * 20.0F);
entity.headYaw = entity.getYaw();
entity.lastHeadYaw = entity.getYaw();
float o = entity.getScale();
Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + scale * o, 0.0F);
float p = (float) size / o;
drawEntity(renderScope, x1, y1, x2, y2, p, vector3f, quaternionf, quaternionf2, entity);
entity.bodyYaw = j;
entity.setYaw(k);
entity.setPitch(l);
entity.lastHeadYaw = m;
entity.headYaw = n;
renderScope.disableScissor();
}
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, float scale,
Vector3f translation, Quaternionf rotation, @Nullable Quaternionf overrideCameraAngle,
LivingEntity entity) {
EntityRenderManager entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
EntityRenderer<? super LivingEntity, ?> entityRenderer = entityRenderDispatcher.getRenderer(entity);
EntityRenderState entityRenderState = entityRenderer.getAndUpdateRenderState(entity, 1.0F);
entityRenderState.hitbox = null;
if (renderScope instanceof RenderScopeImpl i)
i.addEntity(entityRenderState, scale, translation, rotation, overrideCameraAngle, x1, y1, x2, y2);
}
}
@@ -1,347 +0,0 @@
package org.saturnclient.impl.ui;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3x2f;
import org.joml.Matrix3x2fStack;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import com.mojang.blaze3d.textures.GpuTextureView;
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.impl.ref.Matrix3x2fStackRef;
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.gl.RenderPipelines;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.ScreenRect;
import net.minecraft.client.gui.render.state.ColoredQuadGuiElementRenderState;
import net.minecraft.client.gui.render.state.GuiRenderState;
import net.minecraft.client.gui.render.state.ItemGuiElementRenderState;
import net.minecraft.client.gui.render.state.TextGuiElementRenderState;
import net.minecraft.client.gui.render.state.TexturedQuadGuiElementRenderState;
import net.minecraft.client.gui.render.state.special.EntityGuiElementRenderState;
import net.minecraft.client.render.entity.state.EntityRenderState;
import net.minecraft.client.render.item.KeyedItemRenderState;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.texture.TextureSetup;
import net.minecraft.entity.LivingEntity;
import net.minecraft.item.ItemDisplayContext;
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.world.World;
public class RenderScopeImpl implements RenderScope {
public final Matrix3x2fStackRef matrices;
public final GuiRenderState state;
private ScissorStack scissorStack;
private int opacity = 0xFF << 24;
public RenderScopeImpl(DrawContext context) {
this(context.getMatrices(), ((DrawContextAccessor) (Object) context).getState());
}
public RenderScopeImpl(Matrix3x2fStack matrices, GuiRenderState state) {
this.matrices = new Matrix3x2fStackRef(matrices);
this.state = state;
this.scissorStack = new ScissorStack();
}
@Override
public MatrixStackRef getMatrixStack() {
return 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 fill(int x1, int y1, int x2, int y2, int color) {
this.state.addSimpleElement(
new ColoredQuadGuiElementRenderState(
RenderPipelines.GUI,
TextureSetup.empty(),
new Matrix3x2f(this.matrices.stack),
x1, y1, x2, y2, color, color,
this.scissorStack.peekLast()));
}
@Override
public void drawRectangle(int x, int y, int width, int height, int color) {
if (color == 0)
return;
color = getColor(color);
int x1 = x;
int x2 = x + width;
int y1 = y;
int y2 = y + height;
if (x2 < x1) {
int tmp = x1;
x1 = x2;
x2 = tmp;
}
if (y2 < y1) {
int tmp = y1;
y1 = y2;
y2 = tmp;
}
this.fill(x1, y1, x2, y2, 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 lineIndex = 0;
for (String line : text.split("\n")) {
int finalColor = getColor(color);
matrices.push();
matrices.translate(x, y + (lineIndex * Fonts.getHeight()));
matrices.scale(scale, scale);
this.state.addText(
new TextGuiElementRenderState(
SaturnClient.client.textRenderer,
((Text) Fonts.setFont(line, font)).asOrderedText(),
new Matrix3x2f(this.matrices.stack),
0, font == 0 ? 1 : 8,
finalColor, 0, false, this.scissorStack.peekLast()));
matrices.pop();
lineIndex++;
}
}
@Override
public int getScaledWindowWidth() {
return SaturnClient.client.getWindow().getScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getScaledHeight();
}
// Texture rendering
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height) {
drawTexture(sprite, x, y, u, v, width, height, -1);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, int color) {
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,
int regionWidth, int regionHeight, int textureWidth, int textureHeight) {
drawTexture(sprite, x, y, u, v, width, height, regionWidth, 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) {
drawTexturedQuad((Identifier) (Object) sprite, x, x + width, y, y + height,
(u + 0f) / textureWidth,
(u + regionWidth) / textureWidth,
(v + 0f) / textureHeight,
(v + regionHeight) / 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 (color == 0)
return;
color = getColor(color);
if (sprite.getPath().endsWith(".svg")) {
sprite = (Identifier) (Object) SvgTexture.getSvg(
(IdentifierRef) (Object) sprite,
(x2 - x1) * 2, (y2 - y1) * 2);
}
GpuTextureView gpuTextureView = SaturnClient.client.getTextureManager()
.getTexture(sprite).getGlTextureView();
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState(
RenderPipelines.GUI_TEXTURED,
TextureSetup.withoutGlTexture(gpuTextureView),
new Matrix3x2f(this.matrices.stack),
x1, y1, x2, y2, u1, u2, v1, v2, color,
this.scissorStack.peekLast()));
}
// Scissor management
@Override
public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRect rect = new ScreenRect(x1, y1, x2 - x1, y2 - y1).transform(this.matrices.stack);
scissorStack.push(rect);
}
@Override
public void disableScissor() {
scissorStack.pop();
}
@Override
public boolean scissorContains(int x, int y) {
return scissorStack.contains(x, y);
}
public static class ScissorStack {
private final Deque<ScreenRect> stack = new ArrayDeque<>();
public ScreenRect push(ScreenRect rect) {
ScreenRect last = stack.peekLast();
if (last != null) {
ScreenRect intersection = Objects.requireNonNullElse(rect.intersection(last), ScreenRect.empty());
stack.addLast(intersection);
return intersection;
} else {
stack.addLast(rect);
return rect;
}
}
@Nullable
public ScreenRect pop() {
if (stack.isEmpty())
throw new IllegalStateException("Scissor stack underflow");
stack.removeLast();
return stack.peekLast();
}
@Nullable
public ScreenRect peekLast() {
return stack.peekLast();
}
public boolean contains(int x, int y) {
return stack.isEmpty() || stack.peekLast().contains(x, y);
}
}
// Item rendering
@Override
public void drawItem(ItemStackRef stack, int x, int y) {
drawItem(null, SaturnClient.client.world, stack, x, y, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y,
int seed) {
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()) {
KeyedItemRenderState keyedItemRenderState = new KeyedItemRenderState();
SaturnClient.client.getItemModelManager().clearAndUpdate(keyedItemRenderState, stack,
ItemDisplayContext.GUI, world,
entity, seed);
try {
this.state.addItem(new ItemGuiElementRenderState(stack.getItem().getName().toString(),
new Matrix3x2f(this.matrices.stack), keyedItemRenderState, x, y,
this.scissorStack.peekLast()));
} 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);
}
}
}
@Override
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height) {
drawSpriteStretched(sprite, x, y, width, height, -1);
}
@Override
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height, int color) {
Sprite mcSprite = (Sprite) sprite;
if (color == 0 || width == 0 || height == 0)
return;
drawTexturedQuad(mcSprite.getAtlasId(), x, x + width, y, y + height,
mcSprite.getMinU(), mcSprite.getMaxU(), mcSprite.getMinV(), mcSprite.getMaxV(), color);
}
@Override
public void draw() {
// No-op for now
}
public void drawBorder(int x, int y, int width, int height, int color) {
fill(x, y, x + width, y + 1, color);
fill(x, y + height - 1, x + width, y + height, color);
fill(x, y + 1, x + 1, y + height - 1, color);
fill(x + width - 1, y + 1, x + width, y + height - 1, color);
}
public void addEntity(EntityRenderState entityState, float scale, Vector3f translation, Quaternionf rotation,
@Nullable Quaternionf overrideCameraAngle, int x1, int y1, int x2, int y2) {
Matrix3x2f mat = this.matrices.stack;
float offsetX = mat.m20;
float offsetY = mat.m21;
float scaleX = mat.m00; // current matrix X scale
float scaleY = mat.m11; // current matrix Y scale
int adjustedX1 = (int) (x1 * scaleX + offsetX);
int adjustedY1 = (int) (y1 * scaleY + offsetY);
int adjustedX2 = (int) (x2 * scaleX + offsetX);
int adjustedY2 = (int) (y2 * scaleY + offsetY);
float adjustedScale = scale * scaleX; // apply matrix scale to entity scale too
this.state.addSpecialElement(new EntityGuiElementRenderState(entityState, translation, rotation,
overrideCameraAngle, adjustedX1, adjustedY1, adjustedX2, adjustedY2, adjustedScale,
this.scissorStack.peekLast()));
}
}
@@ -1,140 +0,0 @@
package org.saturnclient.impl.ui;
import java.time.Duration;
import java.time.Instant;
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.gui.Click;
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.input.KeyInput;
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);
}
public final SaturnScreen screen;
public static void preload(MinecraftClient client) {
TextureManager textureManager = client.getTextureManager();
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();
}
@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, true);
}
RenderScope renderScope = new RenderScopeImpl(context);
screen.render(renderScope, mouseX, mouseY, delta, elapsed);
}
@Override
public boolean mouseClicked(Click click, boolean doubled) {
if (screen.mouseClicked(click.x(), click.y(), click.button())) {
return true;
}
return super.mouseClicked(click, doubled);
}
@Override
public boolean mouseDragged(Click click, double offsetX, double offsetY) {
if (screen.mouseDragged(click.x(), click.y(), click.button(), 0.0, 0.0)) {
return true;
}
return super.mouseDragged(click, offsetX, offsetY);
}
@Override
public boolean mouseReleased(Click click) {
if (screen.mouseReleased(click.x(), click.y(), click.button())) {
return true;
}
return super.mouseReleased(click);
}
@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(KeyInput input) {
if (screen.keyPressed(input.getKeycode(), input.scancode(), input.modifiers())) {
return true;
}
return super.keyPressed(input);
}
@Override
public void resize(MinecraftClient client, int width, int height) {
screen.resize(width, height);
super.resize(client, width, height);
}
@Override
public void close() {
this.screen.onClose();
super.close();
}
@Override
public int getWidth() {
return width * 2;
}
@Override
public int getHeight() {
return height * 2;
}
}
@@ -1,64 +0,0 @@
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.common.provider.Providers;
import org.saturnclient.impl.provider.GLFWProviderImpl;
import org.saturnclient.impl.provider.FeatureProviderFabric;
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.mod.ModManager;
import org.saturnclient.saturnclient.event.KeyInputHandler;
import org.saturnclient.config.Config;
import org.saturnclient.config.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.feature = new FeatureProviderFabric(client);
SkinPreview.DRAWER = new EntityDrawerImpl();
Config.init();
ModManager.init();
client.execute(() -> {
SaturnScreenFabric.preload(client);
});
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> ConfigManager.save());
ClientTickEvents.END_CLIENT_TICK.register(client -> {
for (org.saturnclient.mod.Mod m : ModManager.ENABLED_MODS) {
m.tick();
}
});
KeyInputHandler.register();
Emotes.initialize();
ServiceClient.initialize();
}
}
@@ -1,10 +0,0 @@
package org.saturnclient.saturnclient;
import org.saturnclient.client.player.SaturnPlayer;
import net.fabricmc.fabric.api.client.rendering.v1.RenderStateDataKey;
public class SaturnRenderState {
public static RenderStateDataKey<SaturnPlayer> saturnDataKey = RenderStateDataKey
.create(() -> "saturn-client-player");
}
@@ -1,55 +0,0 @@
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 net.minecraft.util.Identifier;
import org.saturnclient.config.Config;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.emotes.fabric.Emotes;
import org.saturnclient.ui.screens.EmoteWheel;
import org.saturnclient.ui.screens.ShiftMenu;
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 Identifier KEY_CATEGORY = Identifier.of("saturnclient", "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,
DEFAULT_MENU_KEY,
KeyBinding.Category.create(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(), GLFW.GLFW_KEY_F3)) {
Providers.saturn.getClient().setScreen(new EmoteWheel());
}
// Sneaking cancels your own emote, for everyone.
if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()
&& Emotes.playing(SaturnClient.client.player.getUuid()).isPresent()) {
Emotes.stop(SaturnClient.client.player.getUuid());
ServiceClient.emote("");
}
});
}
}
@@ -1,12 +0,0 @@
package org.saturnclient.saturnclient.mixin;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.render.state.GuiRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(DrawContext.class)
public interface DrawContextAccessor {
@Accessor("state")
GuiRenderState getState();
}
@@ -1,61 +0,0 @@
package org.saturnclient.saturnclient.mixin;
import net.minecraft.client.network.ClientPlayerLikeEntity;
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 net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.entity.PlayerLikeEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.saturnclient.client.player.Roles;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.impl.cosmetics.CloakFeatureRenderer;
import org.saturnclient.impl.cosmetics.HatFeatureRenderer;
import org.saturnclient.saturnclient.SaturnRenderState;
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<PlayerEntity, PlayerEntityRenderState, PlayerEntityModel> {
protected PlayerEntityRendererMixin() {
super(null, null, 0.0f);
}
@Inject(method = "<init>", at = @At("TAIL"))
private void addCustomFeatureRenderer(EntityRendererFactory.Context ctx, boolean slim, CallbackInfo ci) {
this.addFeature(new HatFeatureRenderer(this));
this.addFeature(new CloakFeatureRenderer(this, ctx.getEquipmentModelLoader()));
}
@Inject(method = "updateRenderState", at = @At("TAIL"))
public <A extends PlayerLikeEntity & ClientPlayerLikeEntity> void updateRenderState(
A entity, PlayerEntityRenderState state,
float f, CallbackInfo ci) {
if (entity == null) {
return;
}
SaturnPlayer player = SaturnPlayer.get(entity.getUuid());
state.setData(SaturnRenderState.saturnDataKey, player);
if (player != null) {
MutableText iconText = Text.literal(Roles.getSaturnIndicator())
.styled(style -> style.withColor(Roles.getIconColor(player.uuid)));
state.displayName = Text.empty()
.append(iconText.copy())
.append(Text.empty().formatted(Formatting.RESET))
.append(state.displayName.copy());
}
}
}
@@ -1,27 +0,0 @@
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;
}
}
@@ -1,162 +0,0 @@
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<Optional<Throwable>> 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());
}
}
}
}
@@ -1,49 +0,0 @@
package org.saturnclient.saturnclient.mixin;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.scoreboard.Team;
import net.minecraft.text.Text;
import java.util.UUID;
import org.saturnclient.client.player.Roles;
import org.saturnclient.client.player.SaturnPlayer;
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;
import com.mojang.authlib.GameProfile;
@Mixin(PlayerListEntry.class)
public abstract class TabListMixin {
/**
* Adds the Saturn Client icon to the player's name if they are online with
* Saturn Client, keeping any name, rank prefix or team formatting the server
* set.
*/
@Inject(method = "getDisplayName", at = @At("RETURN"), cancellable = true)
private void saturn$addSaturnIcon(CallbackInfoReturnable<Text> cir) {
PlayerListEntry entry = (PlayerListEntry) (Object) this;
GameProfile profile = entry.getProfile();
String name = profile.name();
UUID uuid = profile.id();
if (SaturnPlayer.get(name, uuid) == null) {
return;
}
Text displayName = cir.getReturnValue();
// With no server-set name, vanilla shows the team-decorated profile name.
if (displayName == null) {
displayName = Team.decorateName(entry.getScoreboardTeam(), Text.literal(name));
}
cir.setReturnValue(Text.empty()
.append(Text.literal(Roles.getSaturnIndicator())
.styled(style -> style.withColor(Roles.getIconColor(uuid))))
.append(displayName));
}
}
@@ -1,32 +0,0 @@
/*
* 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;
}
}
@@ -1,28 +0,0 @@
{
"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",
"features.mixins.json",
"impl.mixins.json"
],
"depends": {
"fabricloader": ">=${loader_version}",
"fabric": "*",
"minecraft": "${minecraft_version}"
}
}
@@ -1,20 +0,0 @@
{
"required": true,
"minVersion": "0.8",
"package": "org.saturnclient.impl.features.mixins",
"compatibilityLevel": "JAVA_21",
"client": [
"render.GameRendererMixin",
"render.LightmapTextureManagerMixin",
"render.LivingEntityRendererMixin",
"render.LivingEntityRenderStateMixin",
"render.BackgroundRendererMixin",
"render.RenderMixin",
"world.TpsMixin",
"entity.CameraMixin",
"entity.EntityMixin"
],
"injectors": {
"defaultRequire": 1
}
}
@@ -1,19 +0,0 @@
{
"required": true,
"minVersion": "0.8",
"package": "org.saturnclient.impl.mixins",
"compatibilityLevel": "JAVA_21",
"client": [
"IdentifierMixin",
"ItemStackMixin",
"MinecraftClientMixin",
"QuaternionfMixin",
"WindowMixin",
"TextMixin",
"SpriteMixin",
"StatusEffectMixin"
],
"injectors": {
"defaultRequire": 1
}
}
@@ -1,17 +0,0 @@
{
"required": true,
"minVersion": "0.8",
"package": "org.saturnclient.saturnclient.mixin",
"compatibilityLevel": "JAVA_21",
"client": [
"TabListMixin",
"PlayerEntityRendererMixin",
"WhoAmI",
"DrawContextAccessor",
"SplashOverlayMixin",
"SetScreenMixin"
],
"injectors": {
"defaultRequire": 1
}
}