Compare commits
103
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a949c18ec1 | ||
|
|
6f122b5237 | ||
|
|
85e5ef3f59 | ||
|
|
0bf4fc6808 | ||
|
|
a44b4f4e90 | ||
|
|
f2d30e6611 | ||
|
|
242653054d | ||
|
|
95e4dff448 | ||
|
|
a30cf4d3ab | ||
|
|
eebfe8fa4d | ||
|
|
019e5cfb43 | ||
|
|
6ed2ac1b60 | ||
|
|
1853b09443 | ||
|
|
9126faa11c | ||
|
|
090e5132b2 | ||
|
|
bbe6d02568 | ||
|
|
04e7064790 | ||
|
|
ff0bce4d24 | ||
|
|
2ecb4f80ea | ||
|
|
41f9be8595 | ||
|
|
f73f7e733c | ||
|
|
91b53daac9 | ||
|
|
5bcdc72b83 | ||
|
|
3f0670ea9d | ||
|
|
c54b5ae6d7 | ||
|
|
14c16d8fe0 | ||
|
|
e3d1e4a442 | ||
|
|
04605f6855 | ||
|
|
451ac986c0 | ||
|
|
80d478d2c9 | ||
|
|
79719262a2 | ||
|
|
806c4ab643 | ||
|
|
6ee8b64a38 | ||
|
|
a97e5703c9 | ||
|
|
39f140dae1 | ||
|
|
632591f8d8 | ||
|
|
66e994b5ab | ||
|
|
1d56094388 | ||
|
|
69f572288f | ||
|
|
1a5c7f2a2c | ||
|
|
68734f6ac5 | ||
|
|
333c408d1b | ||
|
|
3c57328b3c | ||
|
|
08178b60fe | ||
|
|
4342233876 | ||
|
|
89cb467016 | ||
|
|
9488a8647e | ||
|
|
fa1d1f312a | ||
|
|
b359224ecd | ||
|
|
a909aa880a | ||
|
|
8f8fec0816 | ||
|
|
c2d20492c3 | ||
|
|
4a7eade70d | ||
|
|
6466d8d8fd | ||
|
|
7c72da978d | ||
|
|
76b842e6a0 | ||
|
|
00cefe14f7 | ||
|
|
2a3034be2c | ||
|
|
910633819a | ||
|
|
daddd0a005 | ||
|
|
cdd6ae8ec9 | ||
|
|
65c9454370 | ||
|
|
8a1e9b49bc | ||
|
|
f288f9148c | ||
|
|
e165f6b65c | ||
|
|
019730c756 | ||
|
|
7fa951b7fa | ||
|
|
b39b19f107 | ||
|
|
88db7a8383 | ||
|
|
d782bccb5f | ||
|
|
84947d6b33 | ||
|
|
f79849d9fb | ||
|
|
cb38639e11 | ||
|
|
8f567c83c6 | ||
|
|
8876abde1c | ||
|
|
2d95bf9043 | ||
|
|
0475d8c43d | ||
|
|
491199cfea | ||
|
|
5aa40d2e61 | ||
|
|
c5b6a858e1 | ||
|
|
e28051a109 | ||
|
|
1471e53a91 | ||
|
|
6ae4f980e4 | ||
|
|
6fbacbcae6 | ||
|
|
e934f24d3e | ||
|
|
191bc1318e | ||
|
|
249219fe57 | ||
|
|
5a86d4cb4a | ||
|
|
75a5d7f52a | ||
|
|
c078637d3d | ||
|
|
6c41e774ff | ||
|
|
c630a4e41a | ||
|
|
33956ea550 | ||
|
|
2e45cc39f7 | ||
|
|
c8860ab837 | ||
|
|
64ab091807 | ||
|
|
243275b414 | ||
|
|
70d7953b45 | ||
|
|
812e7bf593 | ||
|
|
e5c2d8bd36 | ||
|
|
670961acce | ||
|
|
b46410c43b | ||
|
|
2cd7db9ade |
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env node
|
||||
// Publish the jars attached to a Gitea release to Modrinth.
|
||||
//
|
||||
// Creates one Modrinth version per jar (saturn-client-<version>+<mc>.jar), for
|
||||
// that Minecraft version, with Fabric API as a required dependency and the
|
||||
// Gitea release notes as the changelog. Versions that already exist on
|
||||
// Modrinth are skipped, so the script is safe to re-run.
|
||||
//
|
||||
// Runs on plain Node (>= 18: fetch, FormData, Blob) with no dependencies, since
|
||||
// every Gitea Actions runner has Node to run JavaScript actions.
|
||||
//
|
||||
// Environment:
|
||||
// TAG release tag, e.g. 0.1.1-beta
|
||||
// SERVER_URL public Gitea URL, e.g. https://git.selimaj.dev
|
||||
// REPO owner/name, e.g. saturnclientmc/saturnclient
|
||||
// MODRINTH_TOKEN Modrinth PAT with the "Create versions" scope
|
||||
// DRY_RUN "true" to check everything and print what would be
|
||||
// published without uploading (no token needed)
|
||||
|
||||
const MODRINTH_API = "https://api.modrinth.com/v2";
|
||||
const MODRINTH_PROJECT_ID = "i6JDSY9x"; // saturnclient
|
||||
const FABRIC_API_PROJECT_ID = "P7dR8mSH";
|
||||
const USER_AGENT =
|
||||
"saturnclientmc/saturnclient release workflow (https://git.selimaj.dev/saturnclientmc/saturnclient)";
|
||||
|
||||
const JAR_NAME = /^saturn-client-(?<version>.+)\+(?<mc>\d+\.\d+(?:\.\d+)?)\.jar$/;
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function http(url, options = {}) {
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: { "User-Agent": USER_AGENT, ...options.headers },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = new Error(`${options.method ?? "GET"} ${url} returned ${res.status}: ${await res.text()}`);
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function getJson(url) {
|
||||
return (await http(url)).json();
|
||||
}
|
||||
|
||||
function versionType(tag) {
|
||||
const t = tag.toLowerCase();
|
||||
if (t.includes("alpha")) return "alpha";
|
||||
if (t.includes("beta") || t.includes("rc") || t.includes("pre")) return "beta";
|
||||
return "release";
|
||||
}
|
||||
|
||||
function compareMc(a, b) {
|
||||
const pa = a.split(".").map(Number);
|
||||
const pb = b.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { TAG: tag, SERVER_URL, REPO: repo } = process.env;
|
||||
const serverUrl = (SERVER_URL ?? "").replace(/\/+$/, "");
|
||||
const dryRun = (process.env.DRY_RUN ?? "").toLowerCase() === "true";
|
||||
const token = process.env.MODRINTH_TOKEN ?? "";
|
||||
|
||||
if (!tag || !serverUrl || !repo) fail("TAG, SERVER_URL and REPO must be set.");
|
||||
if (!token && !dryRun) {
|
||||
fail("MODRINTH_TOKEN is not set. Add it as a repository secret (Settings → Actions → Secrets).");
|
||||
}
|
||||
|
||||
let release;
|
||||
try {
|
||||
release = await getJson(`${serverUrl}/api/v1/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`);
|
||||
} catch (e) {
|
||||
if (e.status === 404) fail(`No Gitea release with tag ${tag}.`);
|
||||
throw e;
|
||||
}
|
||||
const changelog = release.body ?? "";
|
||||
|
||||
const jars = (release.assets ?? [])
|
||||
.map((asset) => ({ name: asset.name, match: JAR_NAME.exec(asset.name) }))
|
||||
.filter(({ match }) => match)
|
||||
.map(({ name, match }) => ({ name, version: match.groups.version, mc: match.groups.mc }))
|
||||
.sort((a, b) => compareMc(a.mc, b.mc));
|
||||
|
||||
if (jars.length === 0) fail(`Release ${tag} has no saturn-client-*.jar assets. Run the Release workflow first.`);
|
||||
|
||||
for (const jar of jars) {
|
||||
if (jar.version !== tag.replace(/^v/, "")) fail(`${jar.name} is version ${jar.version}, but the release tag is ${tag}.`);
|
||||
}
|
||||
|
||||
const knownGameVersions = new Set((await getJson(`${MODRINTH_API}/tag/game_version`)).map((g) => g.version));
|
||||
const unknown = [...new Set(jars.map((j) => j.mc))].filter((mc) => !knownGameVersions.has(mc));
|
||||
if (unknown.length > 0) fail(`Modrinth doesn't know these Minecraft versions: ${unknown.join(", ")}`);
|
||||
|
||||
const existing = new Set(
|
||||
(await getJson(`${MODRINTH_API}/project/${MODRINTH_PROJECT_ID}/version`)).map((v) => v.version_number),
|
||||
);
|
||||
const kind = versionType(tag);
|
||||
const failed = [];
|
||||
|
||||
for (const { name, version, mc } of jars) {
|
||||
const versionNumber = `${version}+${mc}`;
|
||||
|
||||
if (existing.has(versionNumber)) {
|
||||
console.log(`skip ${versionNumber}: already on Modrinth`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gitea's own download URLs use its internal ROOT_URL, so build a public one.
|
||||
const downloadUrl = `${serverUrl}/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`;
|
||||
|
||||
const data = {
|
||||
project_id: MODRINTH_PROJECT_ID,
|
||||
name: `Saturn Client ${versionNumber}`,
|
||||
version_number: versionNumber,
|
||||
changelog,
|
||||
dependencies: [{ project_id: FABRIC_API_PROJECT_ID, dependency_type: "required" }],
|
||||
game_versions: [mc],
|
||||
version_type: kind,
|
||||
loaders: ["fabric"],
|
||||
featured: false,
|
||||
status: "listed",
|
||||
file_parts: ["file"],
|
||||
primary_file: "file",
|
||||
};
|
||||
|
||||
if (dryRun) {
|
||||
const probe = await http(downloadUrl, { headers: { Range: "bytes=0-0" } });
|
||||
await probe.arrayBuffer();
|
||||
console.log(`would publish ${versionNumber} (${kind}, Minecraft ${mc}) from ${downloadUrl} [reachable]`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const jarBytes = await (await http(downloadUrl)).arrayBuffer();
|
||||
|
||||
const form = new FormData();
|
||||
form.append("data", JSON.stringify(data));
|
||||
form.append("file", new Blob([jarBytes], { type: "application/java-archive" }), name);
|
||||
|
||||
await http(`${MODRINTH_API}/version`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: token },
|
||||
body: form,
|
||||
});
|
||||
console.log(`publish ${versionNumber}: done (${(jarBytes.byteLength / 1_000_000).toFixed(1)} MB)`);
|
||||
} catch (e) {
|
||||
console.error(`publish ${versionNumber}: FAILED ${e.message}`);
|
||||
failed.push(versionNumber);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.length > 0) fail(`Failed to publish: ${failed.join(", ")}`);
|
||||
}
|
||||
|
||||
main().catch((e) => fail(e.stack ?? String(e)));
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Check
|
||||
|
||||
# Compiles every Minecraft version on pull requests, so a change that breaks
|
||||
# one version (e.g. a missed or bad port) fails before it's merged instead of
|
||||
# in the release build. Merges to master aren't re-checked: the PR already
|
||||
# checked that code.
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
# The JDK comes from the runner's tool cache. `cache: gradle` stores Gradle's
|
||||
# dependencies, Loom's Minecraft cache and the build cache in the runner's
|
||||
# actions cache.
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
cache: gradle
|
||||
|
||||
- name: Compile all versions
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
# --continue reports every version that fails, not just the first.
|
||||
./gradlew compileJava --no-daemon --stacktrace --continue
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Publish to Modrinth
|
||||
|
||||
# Publishes the jars already attached to a Gitea release (built by the Release
|
||||
# workflow) to Modrinth, one version per Minecraft version. Run it manually
|
||||
# after smoke-testing the release. It doesn't rebuild anything, and versions
|
||||
# already on Modrinth are skipped, so re-running is safe.
|
||||
#
|
||||
# Needs a MODRINTH_TOKEN repository secret: a Modrinth PAT with the
|
||||
# "Create versions" scope.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Tag of the Gitea release to publish
|
||||
required: true
|
||||
dry_run:
|
||||
description: Only check and print what would be published
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Publish to Modrinth
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
|
||||
# Node is always available on Gitea runners (it runs JavaScript actions).
|
||||
run: node .gitea/scripts/publish_modrinth.mjs
|
||||
@@ -0,0 +1,100 @@
|
||||
name: Release
|
||||
|
||||
# Publishing a release builds every Minecraft version and attaches the mod jars
|
||||
# to it. mod_version in gradle.properties is the source of truth: the release
|
||||
# tag must match it (a leading "v" is allowed), so bump mod_version in a PR
|
||||
# before tagging the release.
|
||||
#
|
||||
# To (re)attach jars to an existing release, run this workflow manually with
|
||||
# the release's tag.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Tag of the existing release to build and attach jars to
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
||||
# The public address. Uploads go through the Cloudflare tunnel, which caps
|
||||
# request bodies at 100 MB, so each mod jar must stay under that.
|
||||
GITEA_API: ${{ github.server_url }}/api/v1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.TAG }}
|
||||
submodules: true
|
||||
|
||||
- name: Check tag matches mod_version
|
||||
run: |
|
||||
MOD_VERSION="$(sed -n 's/^mod_version=//p' gradle.properties | tr -d '[:space:]')"
|
||||
if [ "${TAG#v}" != "$MOD_VERSION" ]; then
|
||||
echo "::error::Release tag '$TAG' doesn't match mod_version '$MOD_VERSION' in gradle.properties. Bump mod_version to '${TAG#v}' on the release branch, or tag the release '$MOD_VERSION'." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Building Saturn Client $MOD_VERSION"
|
||||
|
||||
# The JDK comes from the runner's tool cache. `cache: gradle` stores Gradle's
|
||||
# dependencies, Loom's Minecraft cache and the build cache in the runner's
|
||||
# actions cache.
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
cache: gradle
|
||||
|
||||
- name: Build all versions
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
# buildAll remaps every versions/* project in parallel into build/allJars.
|
||||
./gradlew buildAll --no-daemon --stacktrace
|
||||
|
||||
- name: Upload jars to the release
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
jars=(build/allJars/*.jar)
|
||||
if [ ${#jars[@]} -eq 0 ]; then
|
||||
echo "No jars found in build/allJars" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cloudflare in front of the server rejects uploads of 100 MB or more.
|
||||
for jar in "${jars[@]}"; do
|
||||
size=$(stat -c%s "$jar")
|
||||
if [ "$size" -ge 100000000 ]; then
|
||||
echo "::error::$(basename "$jar") is $((size / 1000000)) MB; uploads must be under 100 MB. Check what's taking space with: unzip -l \"$jar\" | sort -n" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Look the release up by tag so manual runs work too.
|
||||
RELEASE_ID="$(curl --fail-with-body -sS -H "Authorization: token $TOKEN" \
|
||||
"$GITEA_API/repos/$REPO/releases/tags/$TAG" \
|
||||
| grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)"
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "No release found for tag $TAG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for jar in "${jars[@]}"; do
|
||||
echo "Uploading $(basename "$jar") to release $TAG ($RELEASE_ID)"
|
||||
curl --fail-with-body -sS -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-F "attachment=@$jar" \
|
||||
"$GITEA_API/repos/$REPO/releases/$RELEASE_ID/assets"
|
||||
echo
|
||||
done
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[submodule "common"]
|
||||
path = common
|
||||
url = https://github.com/saturnclientmc/common.git
|
||||
url = https://git.selimaj.dev/saturnclientmc/common.git
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.github.com/saturnclientmc/common/master/src/main/resources/assets/saturnclient/textures/logo/logo.png" alt="Saturn Client Logo" width="150px"/>
|
||||
<img src="logo.png" alt="Saturn Client Logo" width="150px"/>
|
||||
</p>
|
||||
|
||||
<h1 align="center">Saturn Client</h1>
|
||||
@@ -13,27 +13,47 @@
|
||||
<img src="screenshot.png" alt="Saturn Client Screenshot"/>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## ✨ About
|
||||
## About
|
||||
|
||||
Saturn Client is a free, open-source Minecraft client designed to enhance the gameplay experience while staying lightweight and customizable.
|
||||
Our goal is to build a clean, optimized client that the community can contribute to and improve together.
|
||||
|
||||
Saturn is currently trying to support multiple versions of minecraft, so please report any issues you are experiencing.
|
||||
Saturn runs on Fabric and supports **Minecraft 1.21.4 to 1.21.11**, with one build per version. If something doesn't work on your version, please open an issue.
|
||||
|
||||
---
|
||||
## Features
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- Free cosmetics that don't require real money
|
||||
- Free cosmetics that don't require real money: cloaks, including animated ones, and hats
|
||||
- 18 built-in mods: Armor Display, Auto Sprint, Clock, Coordinates, Crosshair, Day Counter, FPS Display, Freelook, Fullbright, Health Display, Keystrokes, Nametags, No Fog, Ping Display, Speedometer, Status Effects, TPS Display and Zoom
|
||||
- Clean and modern UI with animations
|
||||
- Ongoing performance improvements
|
||||
- Open-source and community-driven
|
||||
|
||||
---
|
||||
## Installation
|
||||
|
||||
## 🤝 Contributing
|
||||
1. Install [Fabric Loader](https://fabricmc.net/use/) and [Fabric API](https://modrinth.com/mod/fabric-api) for your Minecraft version.
|
||||
2. Download the Saturn Client jar for the same version from [Modrinth](https://modrinth.com/mod/saturnclient) or the [releases page](https://git.selimaj.dev/saturnclientmc/saturnclient/releases).
|
||||
3. Put it in your `mods` folder.
|
||||
|
||||
### Network use
|
||||
|
||||
For cosmetics, Saturn Client connects to the Saturn server (`saturn-server.selimaj.dev`). You're signed in the same way as on an online-mode Minecraft server: your client proves who you are to Mojang's session server, and the Saturn server asks Mojang to confirm it. **Your Minecraft access token is only ever sent to Mojang, never to the Saturn server.**
|
||||
|
||||
## Building from source
|
||||
|
||||
The version-independent code lives in the `common` submodule, so clone with submodules:
|
||||
|
||||
```sh
|
||||
git clone --recurse-submodules https://git.selimaj.dev/saturnclientmc/saturnclient.git
|
||||
cd saturnclient
|
||||
|
||||
./gradlew buildAll # every version → build/allJars/
|
||||
./gradlew :1.21.11:build # a single version (versions not on Stonecutter yet: :mc-1.21.5)
|
||||
./gradlew :1.21.11:runClient # launch a development client
|
||||
```
|
||||
|
||||
Building needs Java 21.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions of all kinds:
|
||||
|
||||
@@ -43,11 +63,11 @@ We welcome contributions of all kinds:
|
||||
- Cosmetic designs
|
||||
- Suggestions and feedback
|
||||
|
||||
If you're an artist or designer interested in helping with cosmetics or visual assets, we’d love to collaborate.
|
||||
If you're an artist or designer interested in helping with cosmetics or visual assets, we'd love to collaborate.
|
||||
|
||||
---
|
||||
Most features belong in `common`, which every Minecraft version shares. For code that differs between versions, see the [version guide](VERSION_GUIDE.md): it covers how the versions are laid out, porting a change to all of them with `scripts/port.sh`, and adding a new Minecraft version. Every pull request is compiled for all versions automatically.
|
||||
|
||||
## 🙌 Credits
|
||||
## Credits
|
||||
|
||||
- **Cloak Rendering:** `IIpho3nix` & Saturn
|
||||
- **Who Am I:** `FokshaWasTaken`
|
||||
@@ -57,13 +77,9 @@ If you're an artist or designer interested in helping with cosmetics or visual a
|
||||
If any code or assets belong to you and you would like them removed, please open an issue.
|
||||
We aim to properly credit all contributors and respect original creators of their respective art.
|
||||
|
||||
---
|
||||
## License
|
||||
|
||||
## 📜 License
|
||||
|
||||
Saturn Client is free and open-source using **Apache-2.0**
|
||||
|
||||
---
|
||||
Saturn Client is free and open-source under the **Apache-2.0** license.
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ by the Saturn Client team
|
||||
|
||||
+81
-1
@@ -1,3 +1,83 @@
|
||||
# Version guide
|
||||
|
||||
This guide will help you to add or edit minecraft versions.
|
||||
How Saturn Client supports several Minecraft versions, and how to change or add one.
|
||||
|
||||
## Layout
|
||||
|
||||
Saturn is moving from one full copy of the version-specific code per Minecraft version to a single copy built with [Stonecutter](https://stonecutter.kikugie.dev/) (issue #8). During the move there are two kinds of version:
|
||||
|
||||
- `common/`: all version-independent code (mods, UI, cosmetics, the server client). It has no Minecraft dependency and reaches the game only through the interfaces in `org.saturnclient.common`.
|
||||
- `src/`: the version-specific code (providers, refs, mixins and the three mixin configs) for the versions built by Stonecutter, currently **1.21.6 to 1.21.11**. It's one copy: where Minecraft's API differs between them, the code goes behind version comments (see below). Each of these versions is the Gradle project `:<mc>`, and `versions/<mc>/` holds only its `gradle.properties` (Minecraft, Yarn, Fabric Loader and Fabric API versions), `build/` and `run/`.
|
||||
- `versions/<mc>/src/`: the versions not moved yet (1.21.4 and 1.21.5), each a full copy, as the Gradle project `:mc-<mc>` with its own `build.gradle`.
|
||||
|
||||
`settings.gradle.kts` lists the Stonecutter versions in `stonecutterVersions` and includes every other folder under `versions/` as `:mc-<mc>`. The root `build.gradle.kts` builds each Stonecutter version, and `stonecutter.gradle.kts` configures the root project (shared repositories, the `common` wiring and `buildAll`). Loom's version for the Stonecutter versions is `loom_version` in the root `gradle.properties`.
|
||||
|
||||
## Version-specific code with Stonecutter
|
||||
|
||||
The code in `src/` is always plain Java for the **active** version (1.21.11, set in `stonecutter.gradle.kts`). Code for other versions sits in comments that Stonecutter swaps before building each version:
|
||||
|
||||
```java
|
||||
//? if >=1.21.9 {
|
||||
public void render(MatrixStack matrices, OrderedRenderCommandQueue queue, int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
//?} else
|
||||
//public void render(MatrixStack matrices, VertexConsumerProvider consumers, int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
```
|
||||
|
||||
A condition without braces covers the next line only. For several lines, use braces and wrap the inactive block in `/* … */`:
|
||||
|
||||
```java
|
||||
//? if >=1.21.11 {
|
||||
import net.minecraft.client.render.RenderLayers;
|
||||
import net.minecraft.client.render.RenderSetup;
|
||||
//?} else {
|
||||
/*import net.minecraft.client.render.RenderPhase.Textures;
|
||||
*///?}
|
||||
```
|
||||
|
||||
Stonecutter rewrites inactive code into its own layout when switching versions (a single inactive line becomes `//code`), so write it that way to begin with, or run a switch and a reset before committing.
|
||||
|
||||
When a newer version leaves something out that an older one implements, keep the older version's code behind a condition and mark the newer branch with a `TODO`, rather than dropping it for every version. `WorldFeatureImpl.getWorldAge` and `SaturnScreenFabric.resize` are examples. Search for `TODO` to find what newer versions still lack.
|
||||
|
||||
A whole class that only exists for some versions (such as `NametagsMixin`, which hooks a method that 1.21.9 changed) can keep an empty class body on the other versions. An empty mixin changes nothing, and it keeps the mixin configs the same for every version, since JSON can't hold version comments.
|
||||
|
||||
Inactive code sits inside a `/* … */` comment, so it can't contain `/* … */` comments itself: use `//` comments in code that's only active for some versions. For the same reason, don't nest multi-line version blocks; use `//?} else if <condition> {` for more than two version ranges, as in `PlayerEntityRendererMixin` and `SaturnRenderState`.
|
||||
|
||||
- `./gradlew "Set active project to <mc>"` (or the Stonecutter IntelliJ plugin) rewrites `src/` so that version's code is the uncommented one, for editing it in the IDE.
|
||||
- `./gradlew "Reset active project"` switches back to 1.21.11. **Run it before committing**, so `src/` is always committed with 1.21.11 active.
|
||||
- Each version's processed copy is compiled from `versions/<mc>/build/generated/stonecutter/`, which is also where to look when a non-active version fails to compile.
|
||||
|
||||
The `emotes` repository uses the same setup, and its `BentCubeRenderer` and `PlayerModelMixin` are small examples of version comments.
|
||||
|
||||
## Porting a change to the other versions
|
||||
|
||||
Make the change in one version, usually the newest, then run:
|
||||
|
||||
```sh
|
||||
scripts/port.sh # port uncommitted changes to every other version
|
||||
scripts/port.sh --dry-run # see what would apply cleanly first
|
||||
scripts/port.sh --to 1.21.4,1.21.5 # only some versions
|
||||
scripts/port.sh --commit <rev> # port what a commit changed instead
|
||||
```
|
||||
|
||||
The script applies the diff under one version's source to each other version with `git apply --3way`. The Stonecutter `src/` counts as one version (1.21.11), so changes port between it and the old copies in both directions:
|
||||
|
||||
- **applied**: the files matched, and nothing else is needed.
|
||||
- **CONFLICTS**: that version's code really differs. The file gets normal conflict markers, with the version's own code in `ours` and the ported change in `theirs`. Adapt the change to that version's API and resolve.
|
||||
- **already has this change**: nothing to do, which is safe to see when re-running.
|
||||
|
||||
Only source is ported. `build.gradle` and `gradle.properties` are version-specific on purpose. Then build everything:
|
||||
|
||||
```sh
|
||||
./gradlew compileJava
|
||||
```
|
||||
|
||||
The **Check** workflow runs the same compile on every pull request, so a version that was missed or mis-ported fails before merging.
|
||||
|
||||
## Adding a Minecraft version
|
||||
|
||||
Add new versions to Stonecutter rather than copying a folder:
|
||||
|
||||
1. Create `versions/<new>/gradle.properties` with `minecraft_version`, `yarn_mappings`, `loader_version` and `fabric_api_version` (copy 1.21.11's and change the values; they're listed at <https://fabricmc.net/develop>). Loom's version is shared, in the root `gradle.properties`.
|
||||
2. Add `<new>` to `stonecutterVersions` in `settings.gradle.kts`. When moving an existing `mc-<v>` version over instead, delete its `versions/<v>/src` and run `./gradlew :<v>:clean` once: the new project shares the old one's `build/` folder, and classes compiled from the removed sources would otherwise end up in the jar.
|
||||
3. Build it with `./gradlew :<new>:compileJava` and put what Minecraft changed behind version comments. Check the mixin configs in `src/main/resources/*.mixins.json` as well: a mixin whose target changed fails at startup, not at compile time, so launch it with `./gradlew :<new>:runClient`.
|
||||
4. The release workflow builds everything through `buildAll`, and "Publish to Modrinth" takes the game version from each jar's name, so neither needs changing.
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
subprojects {
|
||||
apply plugin: "java"
|
||||
|
||||
repositories {
|
||||
maven { url "https://maven.fabricmc.net/" }
|
||||
maven { url "https://jitpack.io" }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "de.javagl:obj:0.4.0"
|
||||
implementation "com.formdev:svgSalamander:1.1.4"
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.17.0'
|
||||
}
|
||||
}
|
||||
|
||||
configure(subprojects.findAll { it.name != "common" }) {
|
||||
dependencies {
|
||||
implementation project(":common")
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources {
|
||||
srcDir project(":common").file("src/main/resources")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("buildAll") {
|
||||
group = "build"
|
||||
description = "Builds all subprojects and collects their remapped jars"
|
||||
|
||||
// Only build remapped jars (not full build)
|
||||
dependsOn subprojects.collect { proj ->
|
||||
proj.tasks.matching { it.name == "remapJar" }
|
||||
}
|
||||
|
||||
doLast {
|
||||
def outputDir = file("$buildDir/allJars")
|
||||
outputDir.mkdirs()
|
||||
|
||||
subprojects.each { proj ->
|
||||
def remapTask = proj.tasks.findByName("remapJar")
|
||||
|
||||
if (remapTask != null) {
|
||||
def jarFile = remapTask.archiveFile.get().asFile
|
||||
|
||||
if (jarFile.exists() && !jarFile.name.contains("sources")) {
|
||||
copy {
|
||||
from jarFile
|
||||
into outputDir
|
||||
rename { "${jarFile.name}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println "All remapped jars collected in: $outputDir"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Builds one Minecraft version from the shared src/. Stonecutter runs this for each version in
|
||||
// stonecutterVersions (settings.gradle.kts), with that version's versions/<v>/gradle.properties.
|
||||
plugins {
|
||||
id("net.fabricmc.fabric-loom-remap")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val minecraftVersion = property("minecraft_version") as String
|
||||
val loaderVersion = property("loader_version") as String
|
||||
|
||||
version = "${property("mod_version")}+$minecraftVersion"
|
||||
group = property("maven_group") as String
|
||||
|
||||
base {
|
||||
archivesName = property("archives_base_name") as String
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft("com.mojang:minecraft:$minecraftVersion")
|
||||
mappings("net.fabricmc:yarn:${property("yarn_mappings")}:v2")
|
||||
modImplementation("net.fabricmc:fabric-loader:$loaderVersion")
|
||||
modImplementation("net.fabricmc.fabric-api:fabric-api:${property("fabric_api_version")}")
|
||||
|
||||
// Saturn's emote library (limb bending, Emotecraft emotes)
|
||||
include(modImplementation("org.saturnclient:emotes-fabric:${property("emotes_version")}+$minecraftVersion")!!)
|
||||
|
||||
implementation(project(":common"))
|
||||
|
||||
for (library in listOf(
|
||||
"dev.selimaj:session-java:0.2.0",
|
||||
"de.javagl:obj:0.4.0",
|
||||
"com.formdev:svgSalamander:1.1.4",
|
||||
"com.fasterxml.jackson.core:jackson-databind:2.17.0",
|
||||
"com.fasterxml.jackson.core:jackson-annotations:2.17.0",
|
||||
"com.fasterxml.jackson.core:jackson-core:2.17.0",
|
||||
)) {
|
||||
include(implementation(library)!!)
|
||||
}
|
||||
}
|
||||
|
||||
// Injects the version values into fabric.mod.json.
|
||||
tasks.processResources {
|
||||
val values = mapOf(
|
||||
"version" to project.version.toString(),
|
||||
"minecraft_version" to minecraftVersion,
|
||||
"loader_version" to loaderVersion,
|
||||
)
|
||||
inputs.properties(values)
|
||||
filteringCharset = "UTF-8"
|
||||
filesMatching("fabric.mod.json") { expand(values) }
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.release = 21
|
||||
}
|
||||
|
||||
java {
|
||||
// Loom attaches the sources jar to remapSourcesJar and build.
|
||||
withSourcesJar()
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
val archivesName = base.archivesName
|
||||
inputs.property("archivesName", archivesName)
|
||||
from("LICENSE") {
|
||||
rename { "${it}_${archivesName.get()}" }
|
||||
}
|
||||
from(project(":common").extensions.getByType<SourceSetContainer>()["main"].output)
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenJava") {
|
||||
artifactId = property("archives_base_name") as String
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule common updated: f07750cdcb...15fa973553
+9
-1
@@ -1,7 +1,15 @@
|
||||
org.gradle.jvmargs=-Xmx3G
|
||||
org.gradle.parallel=true
|
||||
# Reuse task outputs (e.g. compiled classes) when inputs haven't changed.
|
||||
org.gradle.caching=true
|
||||
|
||||
# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
mod_version=0.1.0-beta2
|
||||
mod_version=0.1.1-beta
|
||||
|
||||
# Saturn's emote library (git.selimaj.dev/saturnclientmc/emotes)
|
||||
emotes_version=0.1.0
|
||||
|
||||
# Loom, for the Stonecutter versions (settings.gradle.kts). Old mc-<v> projects set their own.
|
||||
loom_version=1.17.21
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
@@ -29,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
Vendored
+52
-33
@@ -19,12 +19,39 @@
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
@rem Catch executions from older scripts and ensure they exit cleanly.
|
||||
@rem This can be removed once we can be reasonably confident that few people
|
||||
@rem will be migrating directly to this new wrapper.
|
||||
goto afterSafetyNet
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
goto exitWithErrorLevel
|
||||
:afterSafetyNet
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@@ -45,13 +72,14 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
1>&2 echo.
|
||||
1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
1>&2 echo.
|
||||
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
|
||||
1>&2 echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto exitWithErrorLevel
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -59,35 +87,26 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
1>&2 echo.
|
||||
1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
1>&2 echo.
|
||||
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
|
||||
1>&2 echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto exitWithErrorLevel
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem This label must not be changed. We rely on old scripts being able to jump to this point.
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
Executable
+184
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env bash
|
||||
# Port a change made in one Minecraft version's code to the other versions.
|
||||
#
|
||||
# Takes the diff under one version's source folder and applies it to the others
|
||||
# with `git apply --3way`: it applies cleanly where the files match, and leaves
|
||||
# normal merge conflicts only where a version really differs. Only source is
|
||||
# ported; build.gradle and gradle.properties are version-specific on purpose.
|
||||
#
|
||||
# Versions moved to Stonecutter (stonecutterVersions in settings.gradle.kts)
|
||||
# share the top-level src/, which counts as one version: the active one set in
|
||||
# stonecutter.gradle.kts. The others keep their own copy in versions/<v>/src.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/port.sh [--from VERSION] [--to V1,V2,...] [--commit REV] [--dry-run]
|
||||
#
|
||||
# --from version the change was made in. Default: the only version with
|
||||
# uncommitted changes (or changed by --commit).
|
||||
# --to comma-separated versions to port to. Default: all others.
|
||||
# --commit port the changes a commit made instead of uncommitted changes.
|
||||
# --dry-run report what would happen without changing anything.
|
||||
#
|
||||
# Exit status: 0 if every version applied cleanly (or already had the change),
|
||||
# 1 if any version has conflicts or failed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
from=""
|
||||
to=""
|
||||
commit=""
|
||||
dry_run=false
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--from) from="$2"; shift 2 ;;
|
||||
--to) to="$2"; shift 2 ;;
|
||||
--commit) commit="$2"; shift 2 ;;
|
||||
--dry-run) dry_run=true; shift ;;
|
||||
-h|--help) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "Unknown option: $1 (see --help)" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Versions sharing the Stonecutter src/, and the one that stands for them.
|
||||
stonecutter_versions=$(sed -n 's/^val stonecutterVersions = listOf(\(.*\))/\1/p' settings.gradle.kts | tr -d '" ' | tr ',' ' ')
|
||||
stonecutter_active=$(sed -n 's/^stonecutter active "\(.*\)"/\1/p' stonecutter.gradle.kts 2>/dev/null || true)
|
||||
|
||||
is_stonecutter() {
|
||||
printf '%s\n' $stonecutter_versions | grep -qx "$1"
|
||||
}
|
||||
|
||||
# Where a version's source lives.
|
||||
src_of() {
|
||||
if is_stonecutter "$1"; then echo "src"; else echo "versions/$1/src"; fi
|
||||
}
|
||||
|
||||
old_versions=$(for d in versions/*/; do [ -d "$d/src" ] && basename "$d"; done)
|
||||
all_versions=$(printf '%s\n' $old_versions $stonecutter_active | grep . | sort -t. -k1,1n -k2,2n -k3,3n)
|
||||
|
||||
version_exists() {
|
||||
[ -d "$(src_of "$1")" ]
|
||||
}
|
||||
|
||||
# Versions with changes, from the working tree or from the given commit.
|
||||
changed_versions() {
|
||||
if [ -n "$commit" ]; then
|
||||
git show --name-only --format= "$commit" -- versions/ src/
|
||||
else
|
||||
git diff HEAD --name-only -- versions/ src/
|
||||
git ls-files --others --exclude-standard -- versions/ src/
|
||||
fi | sed -n -e 's#^versions/\([^/]*\)/src/.*#\1#p' -e "s#^src/.*#${stonecutter_active}#p" | sort -u
|
||||
}
|
||||
|
||||
if [ -z "$from" ]; then
|
||||
candidates=$(changed_versions)
|
||||
count=$(printf '%s\n' "$candidates" | grep -c . || true)
|
||||
if [ "$count" -ne 1 ]; then
|
||||
echo "Changes found in ${count} versions (${candidates//$'\n'/, }); pass --from to choose one." >&2
|
||||
exit 2
|
||||
fi
|
||||
from="$candidates"
|
||||
fi
|
||||
|
||||
version_exists "$from" || { echo "No such version: $from" >&2; exit 2; }
|
||||
if is_stonecutter "$from"; then
|
||||
from="$stonecutter_active"
|
||||
fi
|
||||
from_src=$(src_of "$from")
|
||||
|
||||
if [ -z "$to" ]; then
|
||||
to=$(printf '%s\n' $all_versions | grep -vx "$from" | paste -sd, -)
|
||||
fi
|
||||
|
||||
# New files are only included in `git diff HEAD` once git knows about them.
|
||||
if [ -z "$commit" ]; then
|
||||
untracked=$(git ls-files --others --exclude-standard -- "$from_src")
|
||||
if [ -n "$untracked" ]; then
|
||||
if $dry_run; then
|
||||
echo "note: new files in $from_src are untracked and won't be ported in a dry run:" >&2
|
||||
printf ' %s\n' $untracked >&2
|
||||
else
|
||||
printf '%s\n' "$untracked" | xargs git add --intent-to-add --
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# --full-index gives --3way the real blob ids to merge against.
|
||||
if [ -n "$commit" ]; then
|
||||
patch=$(git show --full-index --binary --format= "$commit" -- "$from_src")
|
||||
else
|
||||
patch=$(git diff HEAD --full-index --binary -- "$from_src")
|
||||
fi
|
||||
|
||||
if [ -z "$patch" ]; then
|
||||
echo "No changes under $from_src to port." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
files=$(printf '%s\n' "$patch" | grep -c '^diff --git' || true)
|
||||
echo "Porting $files file(s) from $from${commit:+ (commit $commit)}"
|
||||
|
||||
from_re=$(printf '%s' "$from_src" | sed 's/\./\\./g')
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
status=0
|
||||
IFS=',' read -r -a targets <<< "$to"
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
if [ "$target" = "$from" ]; then
|
||||
continue
|
||||
fi
|
||||
if is_stonecutter "$target" && [ "$target" != "$stonecutter_active" ]; then
|
||||
printf ' %-8s skipped: shares src/ with %s\n' "$target" "$stonecutter_active"
|
||||
[ "$from" = "$stonecutter_active" ] || status=1
|
||||
continue
|
||||
fi
|
||||
if ! version_exists "$target"; then
|
||||
printf ' %-8s skipped: no such version\n' "$target"
|
||||
status=1
|
||||
continue
|
||||
fi
|
||||
|
||||
# Point the patch's file headers at the target version's source folder.
|
||||
target_src=$(src_of "$target")
|
||||
printf '%s\n' "$patch" | sed -E "/^(diff --git |--- |\+\+\+ |rename (from|to) |copy (from|to) )/ s#([ab]/|(from|to) )${from_re}/#\1${target_src}/#g" > "$tmp/$target.patch"
|
||||
|
||||
if git apply --check --reverse "$tmp/$target.patch" 2>/dev/null; then
|
||||
printf ' %-8s already has this change\n' "$target"
|
||||
continue
|
||||
fi
|
||||
|
||||
if $dry_run; then
|
||||
if git apply --check "$tmp/$target.patch" 2>/dev/null; then
|
||||
printf ' %-8s would apply cleanly\n' "$target"
|
||||
else
|
||||
printf ' %-8s would need a 3-way merge (possible conflicts)\n' "$target"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
if output=$(git apply --3way "$tmp/$target.patch" 2>&1); then
|
||||
printf ' %-8s applied\n' "$target"
|
||||
else
|
||||
conflicts=$(git diff --name-only --diff-filter=U -- "$target_src")
|
||||
if [ -n "$conflicts" ]; then
|
||||
printf ' %-8s CONFLICTS in:\n' "$target"
|
||||
printf '%s\n' "$conflicts" | sed 's/^/ /'
|
||||
else
|
||||
printf ' %-8s FAILED:\n' "$target"
|
||||
printf '%s\n' "$output" | sed 's/^/ /'
|
||||
fi
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! $dry_run; then
|
||||
echo
|
||||
echo "Ported changes are staged in the target versions. Resolve any conflicts, then build"
|
||||
echo "the affected versions, e.g. ./gradlew compileJava"
|
||||
fi
|
||||
|
||||
exit $status
|
||||
@@ -1,21 +0,0 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven { url "https://maven.fabricmc.net/" }
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "saturn-client"
|
||||
|
||||
include("common")
|
||||
|
||||
def versionsDir = file("versions")
|
||||
|
||||
versionsDir.eachDir { dir ->
|
||||
def version = dir.name
|
||||
def projectName = "mc-${version}"
|
||||
|
||||
include(projectName)
|
||||
project(":${projectName}").projectDir = dir
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven("https://maven.fabricmc.net/")
|
||||
maven("https://maven.kikugie.dev/releases")
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// Loom's version, for the Stonecutter versions' build.gradle.kts (the old mc-<v> projects set
|
||||
// their own in versions/<v>/gradle.properties).
|
||||
val loom_version: String by settings
|
||||
plugins {
|
||||
id("net.fabricmc.fabric-loom-remap") version loom_version
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.kikugie.stonecutter") version "0.9.8"
|
||||
}
|
||||
|
||||
rootProject.name = "saturn-client"
|
||||
|
||||
include("common")
|
||||
|
||||
// Versions built by Stonecutter from the shared src/ (see stonecutter.gradle.kts). Each one's
|
||||
// versions/<v>/gradle.properties holds its Minecraft, Yarn and Fabric API versions.
|
||||
val stonecutterVersions = listOf("1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11")
|
||||
|
||||
stonecutter {
|
||||
create(rootProject) {
|
||||
versions(stonecutterVersions)
|
||||
vcsVersion = "1.21.11"
|
||||
}
|
||||
}
|
||||
|
||||
// Versions not moved to Stonecutter yet still build from their own copy in versions/<v>/src,
|
||||
// as projects named mc-<v>.
|
||||
file("versions").listFiles { file -> file.isDirectory && file.name !in stonecutterVersions }!!
|
||||
.sortedBy { it.name }
|
||||
.forEach { dir ->
|
||||
include("mc-${dir.name}")
|
||||
project(":mc-${dir.name}").projectDir = dir
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
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;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.VertexConsumerProvider;
|
||||
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,
|
||||
//? if >=1.21.9 {
|
||||
OrderedRenderCommandQueue output,
|
||||
//?} else
|
||||
//VertexConsumerProvider output,
|
||||
int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
if (state.invisible || !state.capeVisible
|
||||
//? if >=1.21.9 {
|
||||
|| state.skinTextures.cape() != null) {
|
||||
//?} else
|
||||
//|| state.skinTextures.capeTexture() != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//? if >=1.21.8 {
|
||||
SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey);
|
||||
//?} else
|
||||
//SaturnPlayer player = SaturnPlayer.get(state.name);
|
||||
|
||||
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);
|
||||
|
||||
//? if >=1.21.9 {
|
||||
output.submitCustom(matrices, ShaderUtils.getRenderLayer(customCape), (entry, vertexConsumer) -> {
|
||||
renderCape(entry, vertexConsumer, state, l, OverlayTexture.DEFAULT_UV);
|
||||
});
|
||||
//?} else
|
||||
//renderCape(matrices.peek(), output.getBuffer(ShaderUtils.getRenderLayer(customCape)), state, l, OverlayTexture.DEFAULT_UV);
|
||||
|
||||
matrices.pop();
|
||||
}
|
||||
}
|
||||
+16
-5
@@ -4,9 +4,13 @@ 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 org.saturnclient.saturnclient.SaturnRenderState;
|
||||
|
||||
import net.minecraft.client.render.OverlayTexture;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||
@@ -20,9 +24,16 @@ public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
|
||||
PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
SaturnPlayer player = SaturnPlayer.get(state.name);
|
||||
public void render(MatrixStack matrices,
|
||||
//? if >=1.21.9 {
|
||||
OrderedRenderCommandQueue output,
|
||||
//?} else
|
||||
//VertexConsumerProvider output,
|
||||
int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
//? if >=1.21.8 {
|
||||
SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey);
|
||||
//?} else
|
||||
//SaturnPlayer player = SaturnPlayer.get(state.name);
|
||||
|
||||
if (state.invisible || player == null || player.hat.isEmpty()) {
|
||||
return;
|
||||
@@ -34,7 +45,7 @@ public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState,
|
||||
matrices.multiply(new Quaternionf().rotationZYX(head.roll, head.yaw, head.pitch));
|
||||
|
||||
ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
|
||||
matrices, vertexConsumers, light,
|
||||
matrices, output, light,
|
||||
OverlayTexture.DEFAULT_UV);
|
||||
|
||||
matrices.pop();
|
||||
+11
-4
@@ -13,7 +13,10 @@ 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;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.resource.Resource;
|
||||
import net.minecraft.resource.ResourceManager;
|
||||
@@ -78,8 +81,12 @@ public class ObjModel {
|
||||
}
|
||||
}
|
||||
|
||||
public void render(Map<String, Mtl> mtlMap, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
|
||||
int overlay) {
|
||||
public void render(Map<String, Mtl> mtlMap, MatrixStack matrices,
|
||||
//? if >=1.21.9 {
|
||||
OrderedRenderCommandQueue output,
|
||||
//?} else
|
||||
//VertexConsumerProvider output,
|
||||
int light, int overlay) {
|
||||
matrices.push();
|
||||
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f));
|
||||
@@ -95,7 +102,7 @@ public class ObjModel {
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Z.rotationDegrees(config.rotation.z));
|
||||
}
|
||||
|
||||
ObjRenderer.renderObj(this.obj, mtlMap, matrices, vertexConsumers, light, overlay);
|
||||
ObjRenderer.renderObj(this.obj, mtlMap, matrices, output, light, overlay);
|
||||
|
||||
matrices.pop();
|
||||
}
|
||||
+31
-15
@@ -10,8 +10,12 @@ import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
|
||||
import de.javagl.obj.*;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.VertexConsumer;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
@@ -24,9 +28,15 @@ public class ObjRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
public static void renderObj(Obj obj, Map<String, Mtl> mtlMap, MatrixStack matrices,
|
||||
VertexConsumerProvider vertexConsumers, int light, int overlay) {
|
||||
MatrixStack.Entry entry = matrices.peek();
|
||||
public static void renderObj(Obj obj,
|
||||
Map<String, Mtl> mtlMap,
|
||||
MatrixStack matrices,
|
||||
//? if >=1.21.9 {
|
||||
OrderedRenderCommandQueue output,
|
||||
//?} else
|
||||
//VertexConsumerProvider output,
|
||||
int light,
|
||||
int overlay) {
|
||||
|
||||
Map<String, Obj> materialGroups = ObjSplitting.splitByMaterialGroups(obj);
|
||||
|
||||
@@ -36,19 +46,15 @@ public class ObjRenderer {
|
||||
|
||||
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("#")) {
|
||||
if (mapKd != null && !mapKd.startsWith("#")) {
|
||||
texture = IdentifierRef.of(mapKd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diffuse color (Kd)
|
||||
float r = 1f, g = 1f, b = 1f;
|
||||
if (mtl != null) {
|
||||
FloatTuple kd = mtl.getKd();
|
||||
@@ -59,19 +65,29 @@ public class ObjRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
VertexConsumer consumer = vertexConsumers.getBuffer(ShaderUtils.getRenderLayer(texture));
|
||||
RenderLayer layer = ShaderUtils.getRenderLayer(texture);
|
||||
|
||||
int faces = groupObj.getNumFaces();
|
||||
final float fr = r, fg = g, fb = b;
|
||||
final Obj finalObj = groupObj;
|
||||
|
||||
//? if >=1.21.9 {
|
||||
output.submitCustom(matrices, layer,
|
||||
(entry, consumer) -> drawGroup(finalObj, consumer, entry, light, overlay, fr, fg, fb));
|
||||
//?} else
|
||||
//drawGroup(finalObj, output.getBuffer(layer), matrices.peek(), light, overlay, fr, fg, fb);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawGroup(Obj obj, VertexConsumer consumer, MatrixStack.Entry entry, int light, int overlay,
|
||||
float r, float g, float b) {
|
||||
int faces = obj.getNumFaces();
|
||||
|
||||
for (int f = 0; f < faces; f++) {
|
||||
ObjFace face = groupObj.getFace(f);
|
||||
ObjFace face = obj.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);
|
||||
}
|
||||
writeVertex(obj, face, i, consumer, entry, light, overlay, r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-5
@@ -7,33 +7,54 @@ 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;
|
||||
//? if >=1.21.11 {
|
||||
import net.minecraft.client.render.RenderLayers;
|
||||
import net.minecraft.client.render.RenderSetup;
|
||||
import net.minecraft.client.render.block.entity.AbstractEndPortalBlockEntityRenderer;
|
||||
//?} else {
|
||||
/*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 })
|
||||
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,
|
||||
//? if >=1.21.11 {
|
||||
public static final RenderLayer END_GATEWAY = RenderLayer.of("end_gateway",
|
||||
RenderSetup.builder(END_GATEWAY_PIPELINE)
|
||||
.texture("Sampler0", AbstractEndPortalBlockEntityRenderer.SKY_TEXTURE)
|
||||
.texture("Sampler1", AbstractEndPortalBlockEntityRenderer.PORTAL_TEXTURE).build());
|
||||
//?} else {
|
||||
/*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));
|
||||
return entityAlpha((Identifier) (Object) GifUtils.get(texture));
|
||||
} else if (path.endsWith("end.png")) {
|
||||
return END_GATEWAY;
|
||||
} else {
|
||||
return RenderLayer.getEntityAlpha((Identifier) (Object) texture);
|
||||
return entityAlpha((Identifier) (Object) texture);
|
||||
}
|
||||
}
|
||||
|
||||
private static RenderLayer entityAlpha(Identifier texture) {
|
||||
//? if >=1.21.11 {
|
||||
return RenderLayers.entityAlpha(texture);
|
||||
//?} else
|
||||
//return RenderLayer.getEntityAlpha(texture);
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -13,7 +13,10 @@ 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;
|
||||
//? if >=1.21.11 {
|
||||
import net.minecraft.world.World;
|
||||
//?} else
|
||||
//import net.minecraft.world.BlockView;
|
||||
|
||||
@Mixin(Camera.class)
|
||||
public abstract class CameraMixin {
|
||||
@@ -24,8 +27,16 @@ public abstract class CameraMixin {
|
||||
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) {
|
||||
public void lockRotation(
|
||||
//? if >=1.21.11 {
|
||||
World focusedBlock,
|
||||
//?} else
|
||||
//BlockView focusedBlock,
|
||||
Entity cameraEntity,
|
||||
boolean isThirdPerson,
|
||||
boolean isFrontFacing,
|
||||
float tickDelta,
|
||||
CallbackInfo ci) {
|
||||
if (FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
|
||||
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
|
||||
|
||||
+10
-4
@@ -1,7 +1,9 @@
|
||||
package org.saturnclient.impl.features.mixins.misc;
|
||||
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.EntityRenderer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
//? if <1.21.9 {
|
||||
/*import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.state.EntityRenderState;
|
||||
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
@@ -11,12 +13,12 @@ import org.saturnclient.impl.features.entity.EntityFeatureImpl;
|
||||
import org.saturnclient.impl.features.entity.HealthRenderState;
|
||||
import org.saturnclient.impl.features.mixins.render.LivingEntityRenderStateMixin;
|
||||
import org.saturnclient.mod.mods.NametagsMod;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
*///?}
|
||||
|
||||
/**
|
||||
* Intercepts nametag rendering and replaces the displayed text with
|
||||
@@ -44,8 +46,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
*/
|
||||
@Mixin(EntityRenderer.class)
|
||||
public abstract class NametagsMixin {
|
||||
|
||||
/** Guards against the recursive call we make with the replacement text. */
|
||||
// TODO: 1.21.9 moved label rendering to the render queue (renderLabelIfPresent no longer takes a
|
||||
// VertexConsumerProvider), so custom nametags only work on older versions. On 1.21.9+ this mixin is
|
||||
// empty but stays registered, so features.mixins.json is the same for every version.
|
||||
//? if <1.21.9 {
|
||||
/*// Guards against the recursive call we make with the replacement text.
|
||||
@Unique
|
||||
private static final ThreadLocal<Boolean> saturn$rendering = ThreadLocal.withInitial(() -> false);
|
||||
|
||||
@@ -92,4 +97,5 @@ public abstract class NametagsMixin {
|
||||
saturn$rendering.set(false);
|
||||
}
|
||||
}
|
||||
*///?}
|
||||
}
|
||||
+5
-1
@@ -31,6 +31,10 @@ public class WorldFeatureImpl implements WorldFeature {
|
||||
*/
|
||||
@Override
|
||||
public long getWorldAge() {
|
||||
return hasWorld() ? mc.world.getTime() : 0L;
|
||||
//? if >=1.21.11 {
|
||||
// TODO: not implemented on 1.21.11 yet.
|
||||
return 0L;
|
||||
//?} else
|
||||
//return hasWorld() ? mc.world.getTime() : 0L;
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -141,7 +141,10 @@ public abstract class MinecraftClientMixin implements MinecraftClientRef {
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
return entry.getProfile().getName();
|
||||
//? if >=1.21.9 {
|
||||
return entry.getProfile().name();
|
||||
//?} else
|
||||
//return entry.getProfile().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -157,6 +160,9 @@ public abstract class MinecraftClientMixin implements MinecraftClientRef {
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
return entry.getProfile().getId();
|
||||
//? if >=1.21.9 {
|
||||
return entry.getProfile().id();
|
||||
//?} else
|
||||
//return entry.getProfile().getId();
|
||||
}
|
||||
}
|
||||
+16
-2
@@ -1,8 +1,10 @@
|
||||
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.saturnclient.saturnclient.SaturnClient;
|
||||
//? if <1.21.9
|
||||
//import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
@@ -10,6 +12,7 @@ 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 {
|
||||
@@ -28,7 +31,18 @@ public abstract class StatusEffectMixin implements EffectRef {
|
||||
|
||||
@Override
|
||||
public SpriteRef getIcon() {
|
||||
return (SpriteRef) SaturnClient.client.getGuiAtlasManager().getSprite((InGameHud.getEffectTexture(getEffectType())));
|
||||
// 1.21.9 changed the GUI atlas, so newer versions draw the effect's texture (getIconId) instead.
|
||||
//? if >=1.21.9 {
|
||||
return null;
|
||||
//?} else
|
||||
//return (SpriteRef) SaturnClient.client.getGuiAtlasManager().getSprite((InGameHud.getEffectTexture(getEffectType())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentifierRef getIconId() {
|
||||
Identifier id = InGameHud.getEffectTexture(getEffectType());
|
||||
|
||||
return IdentifierRef.of(id.getNamespace(), "textures/" + id.getPath() + ".png");
|
||||
}
|
||||
|
||||
@Override
|
||||
+6
-1
@@ -7,6 +7,8 @@ import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
import net.minecraft.text.Style;
|
||||
import net.minecraft.text.Text;
|
||||
//? if >=1.21.9
|
||||
import net.minecraft.text.StyleSpriteSource.Font;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
@Mixin(Text.class)
|
||||
@@ -14,7 +16,10 @@ public interface TextMixin extends TextRef {
|
||||
|
||||
@Override
|
||||
public default TextRef withFont(IdentifierRef font) {
|
||||
return (TextRef) (((Text) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font)));
|
||||
//? if >=1.21.9 {
|
||||
return (TextRef) (Text) (((Text) this).copy().setStyle(Style.EMPTY.withFont(new Font((Identifier) (Object) font))));
|
||||
//?} else
|
||||
//return (TextRef) (((Text) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font)));
|
||||
}
|
||||
|
||||
@Override
|
||||
+1
-1
@@ -20,6 +20,6 @@ public class RefConstructorImpl implements RefConstructorProvider {
|
||||
|
||||
@Override
|
||||
public TextRef text(String text) {
|
||||
return (TextRef) Text.literal(text);
|
||||
return (TextRef) (Text) Text.literal(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -8,7 +8,10 @@ import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.components.SkinPreview.EntityDrawer;
|
||||
|
||||
import net.minecraft.client.render.entity.EntityRenderDispatcher;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.entity.EntityRenderManager;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.entity.EntityRenderDispatcher;
|
||||
import net.minecraft.client.render.entity.EntityRenderer;
|
||||
import net.minecraft.client.render.entity.state.EntityRenderState;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
@@ -59,10 +62,14 @@ public class EntityDrawerImpl implements EntityDrawer {
|
||||
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) {
|
||||
EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
|
||||
//? if >=1.21.9 {
|
||||
EntityRenderManager entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
|
||||
//?} else
|
||||
//EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
|
||||
EntityRenderer<? super LivingEntity, ?> entityRenderer = entityRenderDispatcher.getRenderer(entity);
|
||||
EntityRenderState entityRenderState = entityRenderer.getAndUpdateRenderState(entity, 1.0F);
|
||||
entityRenderState.hitbox = null;
|
||||
//? if <1.21.11
|
||||
//entityRenderState.hitbox = null;
|
||||
|
||||
if (renderScope instanceof RenderScopeImpl i)
|
||||
i.addEntity(entityRenderState, scale, translation, rotation, overrideCameraAngle, x1, y1, x2, y2);
|
||||
+32
-13
@@ -33,7 +33,12 @@ 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.ItemRenderState;
|
||||
//? if >=1.21.7 {
|
||||
import net.minecraft.client.render.item.KeyedItemRenderState;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.item.ItemRenderState;
|
||||
//? if >=1.21.11
|
||||
import net.minecraft.client.texture.AbstractTexture;
|
||||
import net.minecraft.client.texture.Sprite;
|
||||
import net.minecraft.client.texture.TextureSetup;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
@@ -144,7 +149,10 @@ public class RenderScopeImpl implements RenderScope {
|
||||
((Text) Fonts.setFont(line, font)).asOrderedText(),
|
||||
new Matrix3x2f(this.matrices.stack),
|
||||
0, font == 0 ? 1 : 8,
|
||||
finalColor, 0, false, this.scissorStack.peekLast()));
|
||||
//? if >=1.21.11 {
|
||||
finalColor, 0, false, false, this.scissorStack.peekLast()));
|
||||
//?} else
|
||||
//finalColor, 0, false, this.scissorStack.peekLast()));
|
||||
|
||||
matrices.pop();
|
||||
|
||||
@@ -205,9 +213,15 @@ public class RenderScopeImpl implements RenderScope {
|
||||
GpuTextureView gpuTextureView = SaturnClient.client.getTextureManager()
|
||||
.getTexture(sprite).getGlTextureView();
|
||||
|
||||
//? if >=1.21.11
|
||||
AbstractTexture abstractTexture = SaturnClient.client.getTextureManager().getTexture(sprite);
|
||||
|
||||
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState(
|
||||
RenderPipelines.GUI_TEXTURED,
|
||||
TextureSetup.withoutGlTexture(gpuTextureView),
|
||||
//? if >=1.21.11 {
|
||||
TextureSetup.of(gpuTextureView, abstractTexture.getSampler()),
|
||||
//?} else
|
||||
//TextureSetup.withoutGlTexture(gpuTextureView),
|
||||
new Matrix3x2f(this.matrices.stack),
|
||||
x1, y1, x2, y2, u1, u2, v1, v2, color,
|
||||
this.scissorStack.peekLast()));
|
||||
@@ -277,20 +291,25 @@ public class RenderScopeImpl implements RenderScope {
|
||||
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
|
||||
int z) {
|
||||
if (!stack.isEmpty()) {
|
||||
ItemRenderState state = new ItemRenderState();
|
||||
SaturnClient.client.getItemModelManager().clearAndUpdate(state, stack, ItemDisplayContext.GUI, world,
|
||||
//? if >=1.21.7 {
|
||||
KeyedItemRenderState keyedItemRenderState = new KeyedItemRenderState();
|
||||
//?} else
|
||||
//ItemRenderState keyedItemRenderState = new ItemRenderState();
|
||||
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), state, x, y, scissorStack.peekLast()));
|
||||
} catch (Throwable t) {
|
||||
CrashReport report = CrashReport.create(t, "Rendering item");
|
||||
CrashReportSection section = report.addElement("Item being rendered");
|
||||
section.add("Item Type", () -> String.valueOf(stack.getItem()));
|
||||
section.add("Item Components", () -> String.valueOf(stack.getComponents()));
|
||||
section.add("Item Foil", () -> String.valueOf(stack.hasGlint()));
|
||||
throw new CrashException(report);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
-1
@@ -8,10 +8,14 @@ import org.saturnclient.ui.SaturnScreen;
|
||||
import org.saturnclient.ui.SaturnScreen.ScreenProvider;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
//? if >=1.21.9
|
||||
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;
|
||||
//? if >=1.21.9
|
||||
import net.minecraft.client.input.KeyInput;
|
||||
import net.minecraft.client.texture.ResourceTexture;
|
||||
import net.minecraft.client.texture.TextureManager;
|
||||
import net.minecraft.text.Text;
|
||||
@@ -70,7 +74,37 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
|
||||
screen.render(renderScope, mouseX, mouseY, delta, elapsed);
|
||||
}
|
||||
|
||||
// 1.21.9 moved mouse and key events to Click and KeyInput.
|
||||
//? if >=1.21.9 {
|
||||
@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) {
|
||||
// TODO: 1.21.8 passes the drag deltas; check whether offsetX/offsetY are the same on 1.21.9+.
|
||||
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);
|
||||
}
|
||||
//?} else {
|
||||
/*@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (screen.mouseClicked(mouseX, mouseY, button)) {
|
||||
return true;
|
||||
@@ -93,6 +127,7 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
|
||||
}
|
||||
return super.mouseReleased(mouseX, mouseY, button);
|
||||
}
|
||||
*///?}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
|
||||
@@ -102,19 +137,33 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
|
||||
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
}
|
||||
|
||||
//? if >=1.21.9 {
|
||||
@Override
|
||||
public boolean keyPressed(KeyInput input) {
|
||||
if (screen.keyPressed(input.getKeycode(), input.scancode(), input.modifiers())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.keyPressed(input);
|
||||
}
|
||||
//?} else {
|
||||
/*@Override
|
||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||
if (screen.keyPressed(keyCode, scanCode, modifiers)) {
|
||||
return true;
|
||||
}
|
||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||
}
|
||||
*///?}
|
||||
|
||||
@Override
|
||||
// TODO: resize isn't hooked on 1.21.11 yet.
|
||||
//? if <1.21.11 {
|
||||
/*@Override
|
||||
public void resize(MinecraftClient client, int width, int height) {
|
||||
screen.resize(width, height);
|
||||
super.resize(client, width, height);
|
||||
}
|
||||
*///?}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
+1
-9
@@ -7,8 +7,6 @@ import net.minecraft.client.MinecraftClient;
|
||||
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.cosmetics.Emotes;
|
||||
import org.saturnclient.cosmetics.Hats;
|
||||
import org.saturnclient.cosmetics.Cloaks;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.impl.provider.GLFWProviderImpl;
|
||||
import org.saturnclient.impl.provider.FeatureProviderFabric;
|
||||
@@ -61,12 +59,6 @@ public class SaturnClient implements ModInitializer {
|
||||
|
||||
KeyInputHandler.register();
|
||||
Emotes.initialize();
|
||||
if (ServiceClient.authenticate()) {
|
||||
Cloaks.initialize();
|
||||
Hats.initialize();
|
||||
LOGGER.info(MOD_ID + " initialization complete");
|
||||
} else {
|
||||
LOGGER.error("Failed to authenticate with the server");
|
||||
}
|
||||
ServiceClient.initialize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.saturnclient.saturnclient;
|
||||
|
||||
//? if >=1.21.11
|
||||
import org.jspecify.annotations.NonNull;
|
||||
//? if >=1.21.8 {
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.RenderStateDataKey;
|
||||
//?}
|
||||
|
||||
/**
|
||||
* The key PlayerEntityRendererMixin stores each player's SaturnPlayer under in their render state.
|
||||
* Fabric API's render state data only exists from 1.21.8, so older versions look players up by name.
|
||||
*/
|
||||
public class SaturnRenderState {
|
||||
//? if >=1.21.11 {
|
||||
@NonNull
|
||||
public static RenderStateDataKey<SaturnPlayer> saturnDataKey = RenderStateDataKey
|
||||
.create(() -> "saturn-client-player");
|
||||
//?} else if >=1.21.8 {
|
||||
/*public static RenderStateDataKey<SaturnPlayer> saturnDataKey = RenderStateDataKey
|
||||
.create(() -> "saturn-client-player");
|
||||
*///?}
|
||||
}
|
||||
+26
-12
@@ -4,16 +4,17 @@ 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;
|
||||
//? if >=1.21.9
|
||||
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 dev.kosmx.playerAnim.api.layered.AnimationStack;
|
||||
// import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
||||
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
/**
|
||||
@@ -21,17 +22,28 @@ import org.lwjgl.glfw.GLFW;
|
||||
* Manages keybindings and their associated actions.
|
||||
*/
|
||||
public class KeyInputHandler {
|
||||
private static final String KEY_CATEGORY = "key.category.saturnclient";
|
||||
//? if >=1.21.9 {
|
||||
private static final Identifier KEY_CATEGORY = Identifier.of("saturnclient", "key.category.saturnclient");
|
||||
//?} else
|
||||
//private static final String KEY_CATEGORY = "key.category.saturnclient";
|
||||
private static final String MAIN_MENU_KEY_ID = "key.saturnclient.main_menu";
|
||||
private static final int DEFAULT_MENU_KEY = GLFW.GLFW_KEY_RIGHT_SHIFT;
|
||||
private static KeyBinding mainMenuKeyBinding;
|
||||
|
||||
public static void register() {
|
||||
mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
|
||||
//? if >=1.21.9 {
|
||||
mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(
|
||||
new KeyBinding(
|
||||
MAIN_MENU_KEY_ID,
|
||||
DEFAULT_MENU_KEY,
|
||||
KeyBinding.Category.create(KEY_CATEGORY)));
|
||||
//?} else {
|
||||
/*mainMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
|
||||
MAIN_MENU_KEY_ID,
|
||||
InputUtil.Type.KEYSYM,
|
||||
DEFAULT_MENU_KEY,
|
||||
KEY_CATEGORY));
|
||||
*///?}
|
||||
|
||||
// Register the event handler for the main menu key
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
@@ -40,16 +52,18 @@ public class KeyInputHandler {
|
||||
}
|
||||
|
||||
if (Config.openEmoteWheel.wasKeyPressed() && client.currentScreen == null
|
||||
&& !InputUtil.isKeyPressed(client.getWindow().getHandle(), GLFW.GLFW_KEY_F3)) {
|
||||
//? if >=1.21.9 {
|
||||
&& !InputUtil.isKeyPressed(client.getWindow(), GLFW.GLFW_KEY_F3)) {
|
||||
//?} else
|
||||
//&& !InputUtil.isKeyPressed(client.getWindow().getHandle(), GLFW.GLFW_KEY_F3)) {
|
||||
Providers.saturn.getClient().setScreen(new EmoteWheel());
|
||||
}
|
||||
|
||||
if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()) {
|
||||
// AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(SaturnClient.client.player);
|
||||
// if (animationStack.isActive() && animationStack.getPriority() == 1000) {
|
||||
// animationStack.removeLayer(1000);
|
||||
// ServiceClient.emote("");
|
||||
// }
|
||||
// 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("");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.saturnclient.saturnclient.mixin;
|
||||
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.network.ClientPlayerLikeEntity;
|
||||
import net.minecraft.entity.PlayerLikeEntity;
|
||||
//?} else
|
||||
//import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||
import net.minecraft.client.render.entity.EntityRendererFactory;
|
||||
import net.minecraft.client.render.entity.LivingEntityRenderer;
|
||||
import net.minecraft.client.render.entity.PlayerEntityRenderer;
|
||||
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
import net.minecraft.entity.Entity;
|
||||
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()));
|
||||
}
|
||||
|
||||
// Stores the SaturnPlayer in the render state and adds the role icon to the name. Render state
|
||||
// data only exists from 1.21.8.
|
||||
// TODO: 1.21.6 and 1.21.7 don't show the role icon in player names.
|
||||
//? if >=1.21.9 {
|
||||
@Inject(method = "updateRenderState", at = @At("TAIL"))
|
||||
public <A extends PlayerLikeEntity & ClientPlayerLikeEntity> void updateRenderState(
|
||||
A entity, PlayerEntityRenderState state, float f, CallbackInfo ci) {
|
||||
saturn$applyPlayer(entity, state);
|
||||
}
|
||||
//?} else if >=1.21.8 {
|
||||
/*@Inject(method = "updateRenderState", at = @At("TAIL"))
|
||||
public void updateRenderState(
|
||||
AbstractClientPlayerEntity entity, PlayerEntityRenderState state, float f, CallbackInfo ci) {
|
||||
saturn$applyPlayer(entity, state);
|
||||
}
|
||||
*///?}
|
||||
|
||||
//? if >=1.21.8 {
|
||||
private void saturn$applyPlayer(Entity entity, PlayerEntityRenderState state) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SaturnPlayer player = SaturnPlayer.get(entity.getUuid());
|
||||
|
||||
state.setData(SaturnRenderState.saturnDataKey, player);
|
||||
|
||||
if (player != null && state.displayName != 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());
|
||||
}
|
||||
}
|
||||
//?}
|
||||
}
|
||||
+11
-2
@@ -15,6 +15,8 @@ import org.spongepowered.asm.mixin.Final;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.screen.SplashOverlay;
|
||||
//? if >=1.21.11
|
||||
import net.minecraft.client.util.Window;
|
||||
import net.minecraft.resource.ResourceReload;
|
||||
import net.minecraft.util.Util;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
@@ -124,7 +126,9 @@ public abstract class SplashOverlayMixin {
|
||||
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));
|
||||
// TODO: the loading bar's border isn't drawn on 1.21.9+ yet.
|
||||
//? if <1.21.9
|
||||
//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;
|
||||
@@ -153,8 +157,13 @@ public abstract class SplashOverlayMixin {
|
||||
|
||||
reloadCompleteTime = Util.getMeasuringTimeMs();
|
||||
|
||||
//? if >=1.21.11
|
||||
Window window = client.getWindow();
|
||||
if (client.currentScreen != null) {
|
||||
client.currentScreen.init(client, context.getScaledWindowWidth(), context.getScaledWindowHeight());
|
||||
//? if >=1.21.11 {
|
||||
client.currentScreen.init(window.getScaledWidth(), window.getScaledHeight());
|
||||
//?} else
|
||||
//client.currentScreen.init(client, context.getScaledWindowWidth(), context.getScaledWindowHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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();
|
||||
//? if >=1.21.9 {
|
||||
String name = profile.name();
|
||||
UUID uuid = profile.id();
|
||||
//?} else {
|
||||
/*String name = profile.getName();
|
||||
UUID uuid = profile.getId();
|
||||
*///?}
|
||||
|
||||
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
@@ -5,7 +5,6 @@
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"TabListMixin",
|
||||
"NameTagMixin",
|
||||
"PlayerEntityRendererMixin",
|
||||
"WhoAmI",
|
||||
"DrawContextAccessor",
|
||||
@@ -0,0 +1,71 @@
|
||||
plugins {
|
||||
id("dev.kikugie.stonecutter")
|
||||
}
|
||||
|
||||
// The version whose code is uncommented in src/. Switch with "Set active project to <v>", and run
|
||||
// "Reset active project" before committing.
|
||||
stonecutter active "1.21.11"
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "java")
|
||||
|
||||
repositories {
|
||||
maven("https://maven.fabricmc.net/")
|
||||
maven("https://git.selimaj.dev/api/packages/selimaj-dev/maven")
|
||||
maven("https://git.selimaj.dev/api/packages/saturnclientmc/maven")
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
"implementation"("de.javagl:obj:0.4.0")
|
||||
"implementation"("com.formdev:svgSalamander:1.1.4")
|
||||
"implementation"("com.fasterxml.jackson.core:jackson-databind:2.17.0")
|
||||
"implementation"("com.fasterxml.jackson.core:jackson-annotations:2.17.0")
|
||||
"implementation"("com.fasterxml.jackson.core:jackson-core:2.17.0")
|
||||
}
|
||||
}
|
||||
|
||||
// Every Minecraft version builds on common and ships its resources.
|
||||
configure(subprojects.filter { it.name != "common" }) {
|
||||
dependencies {
|
||||
"implementation"(project(":common"))
|
||||
}
|
||||
|
||||
extensions.configure<SourceSetContainer> {
|
||||
named("main") {
|
||||
resources.srcDir(project(":common").file("src/main/resources"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remapJar loads a whole Minecraft classpath and remaps a ~50 MB jar. Eight at once don't fit in the
|
||||
// daemon's heap (org.gradle.jvmargs) and the build stalls in garbage collection, so only two run
|
||||
// together. Everything else still builds in parallel.
|
||||
abstract class RemapLimit : BuildService<BuildServiceParameters.None>
|
||||
|
||||
val remapLimit = gradle.sharedServices.registerIfAbsent("remapLimit", RemapLimit::class) {
|
||||
maxParallelUsages = 2
|
||||
}
|
||||
|
||||
subprojects {
|
||||
tasks.matching { it.name == "remapJar" }.configureEach {
|
||||
usesService(remapLimit)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("buildAll") {
|
||||
group = "build"
|
||||
description = "Builds every Minecraft version's remapped jar and collects them in build/allJars"
|
||||
|
||||
val remapJars = subprojects.flatMap { it.tasks.matching { task -> task.name == "remapJar" } }
|
||||
dependsOn(remapJars)
|
||||
|
||||
doLast {
|
||||
val outputDir = layout.buildDirectory.dir("allJars").get().asFile
|
||||
outputDir.mkdirs()
|
||||
remapJars.map { (it as AbstractArchiveTask).archiveFile.get().asFile }
|
||||
.filter { it.exists() && !it.name.contains("sources") }
|
||||
.forEach { it.copyTo(outputDir.resolve(it.name), overwrite = true) }
|
||||
println("All remapped jars collected in: $outputDir")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Done to increase the memory available to gradle.
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
org.gradle.parallel=true
|
||||
|
||||
# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
# Fabric Properties
|
||||
# check these on https://fabricmc.net/develop
|
||||
minecraft_version=1.21.10
|
||||
yarn_mappings=1.21.10+build.3
|
||||
loader_version=0.19.0
|
||||
|
||||
fabric_api_version=0.138.4+1.21.10
|
||||
|
||||
# Mod Properties
|
||||
maven_group=org.saturnclient
|
||||
archives_base_name=saturn-client
|
||||
@@ -0,0 +1,18 @@
|
||||
# Done to increase the memory available to gradle.
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
org.gradle.parallel=true
|
||||
|
||||
# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
# Fabric Properties
|
||||
# check these on https://fabricmc.net/develop
|
||||
minecraft_version=1.21.11
|
||||
yarn_mappings=1.21.11+build.4
|
||||
loader_version=0.19.0
|
||||
|
||||
fabric_api_version=0.141.3+1.21.11
|
||||
|
||||
# Mod Properties
|
||||
maven_group=org.saturnclient
|
||||
archives_base_name=saturn-client
|
||||
@@ -11,8 +11,7 @@ base {
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { name = "KoSmX Maven"; url = 'https://maven.kosmx.dev/' }
|
||||
maven { name = "JitPack"; url = 'https://jitpack.io' }
|
||||
maven { name = "Selimaj"; url = 'https://git.selimaj.dev/api/packages/selimaj-dev/maven' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -24,24 +23,17 @@ dependencies {
|
||||
// Fabric API. This is technically optional, but you probably want it anyway.
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
|
||||
|
||||
// Player Animation Library
|
||||
include modImplementation(
|
||||
"dev.kosmx.player-anim:player-animation-lib-fabric:${project.player_anim}"
|
||||
)
|
||||
// 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("com.github.selimaj-dev:session-java:0.1.6")
|
||||
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")
|
||||
|
||||
// Optional: BendyLib
|
||||
// include modRuntimeOnly(
|
||||
// "io.github.kosmx.bendy-lib:bendy-lib-fabric:${project.bendylib_version}"
|
||||
// )
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -10,7 +10,7 @@ org.gradle.configuration-cache=false
|
||||
minecraft_version=1.21.4
|
||||
yarn_mappings=1.21.4+build.8
|
||||
loader_version=0.18.4
|
||||
loom_version=1.15-SNAPSHOT
|
||||
loom_version=1.17.21
|
||||
|
||||
# Mod Properties
|
||||
maven_group=org.saturnclient
|
||||
@@ -18,5 +18,3 @@ archives_base_name=saturn-client
|
||||
|
||||
# Dependencies
|
||||
fabric_api_version=0.119.4+1.21.4
|
||||
|
||||
player_anim = 2.0.3+1.21.4
|
||||
|
||||
+221
-97
@@ -2,6 +2,10 @@ 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;
|
||||
@@ -24,6 +28,7 @@ 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;
|
||||
|
||||
@@ -38,6 +43,8 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
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;
|
||||
@@ -95,165 +102,280 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack,
|
||||
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) {
|
||||
MatrixStack.Entry entry = matrixStack.peek();
|
||||
// Extract matrix (you must transform positions manually now)
|
||||
Matrix4f pos = entry.getPositionMatrix();
|
||||
Matrix3f normalMat = entry.getNormalMatrix();
|
||||
|
||||
int color = 0xFFFFFFFF; // white RGBA
|
||||
|
||||
if (!flipWinding) {
|
||||
// CCW
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
|
||||
(float) bottomLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
|
||||
(float) bottomRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
|
||||
(float) topRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
|
||||
(float) topLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
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
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
|
||||
(float) topLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
|
||||
(float) topRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
|
||||
(float) bottomRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
|
||||
(float) bottomLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
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 matrixStack, VertexConsumer vertexConsumer, PlayerEntityRenderState playerState,
|
||||
int light, int overlay) {
|
||||
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;
|
||||
float capePartHeight = capeHeight / PARTS;
|
||||
|
||||
float x1 = -capeWidth / 2.0f;
|
||||
float x2 = capeWidth / 2.0f;
|
||||
|
||||
final float frontU1 = FRONT_RECT.u1();
|
||||
final float frontU2 = FRONT_RECT.u2();
|
||||
final float frontV1 = FRONT_RECT.v1();
|
||||
final float frontV2 = FRONT_RECT.v2();
|
||||
final float frontPartV = (frontV2 - frontV1) / PARTS;
|
||||
|
||||
final float backU1 = BACK_RECT.u1();
|
||||
final float backU2 = BACK_RECT.u2();
|
||||
final float backV1 = BACK_RECT.v1();
|
||||
final float backV2 = BACK_RECT.v2();
|
||||
final float backPartV = (backV2 - backV1) / PARTS;
|
||||
|
||||
final float leftU1 = LEFT_EDGE_RECT.u1();
|
||||
final float leftU2 = LEFT_EDGE_RECT.u2();
|
||||
final float leftV1 = LEFT_EDGE_RECT.v1();
|
||||
final float leftV2 = LEFT_EDGE_RECT.v2();
|
||||
final float leftPartV = (leftV2 - leftV1) / PARTS;
|
||||
|
||||
final float rightU1 = RIGHT_EDGE_RECT.u1();
|
||||
final float rightU2 = RIGHT_EDGE_RECT.u2();
|
||||
final float rightV1 = RIGHT_EDGE_RECT.v1();
|
||||
final float rightV2 = RIGHT_EDGE_RECT.v2();
|
||||
final float rightPartV = (rightV2 - rightV1) / PARTS;
|
||||
|
||||
final float topU1 = TOP_RECT.u1();
|
||||
final float topU2 = TOP_RECT.u2();
|
||||
final float topV1 = TOP_RECT.v1();
|
||||
final float topV2 = TOP_RECT.v2();
|
||||
|
||||
final float bottomU1 = BOTTOM_RECT.u1();
|
||||
final float bottomU2 = BOTTOM_RECT.u2();
|
||||
final float bottomV1 = BOTTOM_RECT.v1();
|
||||
final float bottomV2 = BOTTOM_RECT.v2();
|
||||
|
||||
float curY = 0.0f;
|
||||
float curZ = 0.0f;
|
||||
// =============================
|
||||
// 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);
|
||||
|
||||
// 1. Thickness at the START of this segment (Must match PREVIOUS segment's end)
|
||||
float prevAngle = (i > 0) ? (2.0f - segmentValues[i - 1]) * ((float) Math.PI / 2f) : angle;
|
||||
float startAvgAngle = (angle + prevAngle) / 2.0f;
|
||||
float thickYStart = (float) Math.sin(startAvgAngle) * capeDepth;
|
||||
float thickZStart = (float) Math.cos(startAvgAngle) * capeDepth;
|
||||
spineY[i + 1] = spineY[i] + dirY * partHeight;
|
||||
spineZ[i + 1] = spineZ[i] + dirZ * partHeight;
|
||||
}
|
||||
|
||||
// 2. Thickness at the END of this segment (Must match NEXT segment's start)
|
||||
float nextAngle = (i < PARTS - 1) ? (2.0f - segmentValues[i + 1]) * ((float) Math.PI / 2f) : angle;
|
||||
float endAvgAngle = (angle + nextAngle) / 2.0f;
|
||||
float thickYEnd = (float) Math.sin(endAvgAngle) * capeDepth;
|
||||
float thickZEnd = (float) Math.cos(endAvgAngle) * capeDepth;
|
||||
// =============================
|
||||
// 2. Grid
|
||||
// =============================
|
||||
|
||||
float nextY = curY + dirY * capePartHeight;
|
||||
float nextZ = curZ + dirZ * capePartHeight;
|
||||
Vec3d[][] inner = new Vec3d[H_PARTS + 1][PARTS + 1];
|
||||
Vec3d[][] outer = new Vec3d[H_PARTS + 1][PARTS + 1];
|
||||
|
||||
// Inner Vertices (The spine of the cape)
|
||||
Vec3d innerTopLeft = new Vec3d(x2, curY, curZ);
|
||||
Vec3d innerTopRight = new Vec3d(x1, curY, curZ);
|
||||
Vec3d innerBotLeft = new Vec3d(x2, nextY, nextZ);
|
||||
Vec3d innerBotRight = new Vec3d(x1, nextY, nextZ);
|
||||
for (int x = 0; x <= H_PARTS; x++) {
|
||||
|
||||
// Outer Vertices (Offset by the specific joint thickness)
|
||||
Vec3d outerTopLeft = new Vec3d(x2, curY - thickYStart, curZ - thickZStart);
|
||||
Vec3d outerTopRight = new Vec3d(x1, curY - thickYStart, curZ - thickZStart);
|
||||
Vec3d outerBotLeft = new Vec3d(x2, nextY - thickYEnd, nextZ - thickZEnd);
|
||||
Vec3d outerBotRight = new Vec3d(x1, nextY - thickYEnd, nextZ - thickZEnd);
|
||||
float t = (float) x / H_PARTS;
|
||||
float xPos = -capeWidth / 2.0f + capeWidth * t;
|
||||
|
||||
// FRONT (facing camera)
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerBotRight, outerBotLeft, outerTopLeft, outerTopRight,
|
||||
frontU2, frontV1 + (frontPartV * i), frontU1, frontV1 + (frontPartV * (i + 1)),
|
||||
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 (Facing player)
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, innerBotRight, innerTopRight, innerTopLeft,
|
||||
backU2, backV1 + (backPartV * i), backU1, backV1 + (backPartV * (i + 1)),
|
||||
// BACK
|
||||
renderCapeQuad(vertexConsumer, entry,
|
||||
innerBL, innerBR, innerTR, innerTL,
|
||||
buEnd, bvStart, buStart, bvEnd,
|
||||
light, overlay, 0, 0, -1, false);
|
||||
}
|
||||
}
|
||||
|
||||
// LEFT EDGE
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, outerBotLeft, outerTopLeft, innerTopLeft,
|
||||
leftU1, leftV1 + (leftPartV * i), leftU2, leftV1 + (leftPartV * (i + 1)),
|
||||
light, overlay, 1, 0, 0, false);
|
||||
// =============================
|
||||
// 4. LEFT SIDE (x = H_PARTS)
|
||||
// =============================
|
||||
|
||||
// RIGHT EDGE
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerBotRight, innerBotRight, innerTopRight, outerTopRight,
|
||||
rightU1, rightV1 + (rightPartV * i), rightU2, rightV1 + (rightPartV * (i + 1)),
|
||||
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);
|
||||
|
||||
// TOP FACE (Only on first segment)
|
||||
if (i == 0) {
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerTopLeft, outerTopRight, innerTopRight, innerTopLeft,
|
||||
topU1, topV1, topU2, topV2, light, overlay, 0, 1, 0, false);
|
||||
}
|
||||
|
||||
// BOTTOM FACE (Only on last segment)
|
||||
if (i == PARTS - 1) {
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, innerBotRight, outerBotRight, outerBotLeft,
|
||||
bottomU1, bottomV1, bottomU2, bottomV2, light, overlay, 0, -1, 0, false);
|
||||
// =============================
|
||||
// 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);
|
||||
}
|
||||
|
||||
curY = nextY;
|
||||
curZ = nextZ;
|
||||
// =============================
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +401,7 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
|
||||
SaturnPlayer player = SaturnPlayer.get(playerEntityRenderState.name);
|
||||
|
||||
if (player == null || player.cloak.isEmpty()) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -314,7 +436,7 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastUpdate >= 20) {
|
||||
float velX = Math.min(0.6f, playerEntityRenderState.field_53537 / 108.0f);
|
||||
float velX = Math.min(0.8f, playerEntityRenderState.field_53537 / 108.0f);
|
||||
float rawVelY = playerEntityRenderState.field_53536;
|
||||
float velY = (rawVelY > 3.4f ? rawVelY : rawVelY < -3.4f ? rawVelY : 0.0f) / 16;
|
||||
|
||||
@@ -328,7 +450,9 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
lastUpdate = now;
|
||||
}
|
||||
|
||||
renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV);
|
||||
horizontalCurve = MathHelper.clamp(playerEntityRenderState.field_53537 / 150.0f, -1.0f, 1.0f);
|
||||
|
||||
renderCape(matrixStack.peek(), vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV);
|
||||
|
||||
matrixStack.pop();
|
||||
}
|
||||
|
||||
+28
-29
@@ -3,18 +3,18 @@ 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;
|
||||
|
||||
import dev.kosmx.playerAnim.api.layered.AnimationStack;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
|
||||
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class SaturnProviderImpl implements SaturnProvider {
|
||||
@Override
|
||||
public MinecraftClientRef getClient() {
|
||||
@@ -23,31 +23,13 @@ public class SaturnProviderImpl implements SaturnProvider {
|
||||
|
||||
@Override
|
||||
public void playEmote(UUID fromPlayerUuid, String emoteIdOrNull) {
|
||||
if (SaturnClient.client == null || SaturnClient.client.world == null) {
|
||||
if (fromPlayerUuid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
|
||||
if (!player.getUuid().equals(fromPlayerUuid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
|
||||
|
||||
if (emoteIdOrNull != null && !emoteIdOrNull.isEmpty()) {
|
||||
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
|
||||
animationStack.removeLayer(1000);
|
||||
}
|
||||
animationStack.addAnimLayer(1000,
|
||||
PlayerAnimationRegistry
|
||||
.getAnimation(Identifier.of("saturnclient", emoteIdOrNull))
|
||||
.playAnimation());
|
||||
} else {
|
||||
animationStack.removeLayer(1000);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// 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
|
||||
@@ -65,6 +47,23 @@ public class SaturnProviderImpl implements SaturnProvider {
|
||||
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);
|
||||
|
||||
@@ -7,8 +7,6 @@ import net.minecraft.client.MinecraftClient;
|
||||
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.cosmetics.Emotes;
|
||||
import org.saturnclient.cosmetics.Hats;
|
||||
import org.saturnclient.cosmetics.Cloaks;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.impl.provider.GLFWProviderImpl;
|
||||
import org.saturnclient.impl.provider.FeatureProviderFabric;
|
||||
@@ -61,12 +59,6 @@ public class SaturnClient implements ModInitializer {
|
||||
|
||||
KeyInputHandler.register();
|
||||
Emotes.initialize();
|
||||
if (ServiceClient.authenticate()) {
|
||||
Cloaks.initialize();
|
||||
Hats.initialize();
|
||||
LOGGER.info(MOD_ID + " initialization complete");
|
||||
} else {
|
||||
LOGGER.error("Failed to authenticate with the server");
|
||||
}
|
||||
ServiceClient.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
+5
-8
@@ -9,12 +9,10 @@ import org.saturnclient.config.Config;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.emotes.fabric.Emotes;
|
||||
import org.saturnclient.ui.screens.EmoteWheel;
|
||||
import org.saturnclient.ui.screens.ShiftMenu;
|
||||
|
||||
import dev.kosmx.playerAnim.api.layered.AnimationStack;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
||||
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
/**
|
||||
@@ -45,13 +43,12 @@ public class KeyInputHandler {
|
||||
Providers.saturn.getClient().setScreen(new EmoteWheel());
|
||||
}
|
||||
|
||||
if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()) {
|
||||
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(SaturnClient.client.player);
|
||||
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
|
||||
animationStack.removeLayer(1000);
|
||||
// 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("");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+25
-19
@@ -1,43 +1,49 @@
|
||||
package org.saturnclient.saturnclient.mixin;
|
||||
|
||||
import net.minecraft.client.network.PlayerListEntry;
|
||||
import net.minecraft.scoreboard.Team;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Formatting;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.saturnclient.client.player.Roles;
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import 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 {
|
||||
|
||||
@Shadow
|
||||
private GameProfile profile;
|
||||
|
||||
/**
|
||||
* @author HexLeo
|
||||
* @reason Adds the Saturn Client icon to the player's name if they are online
|
||||
* with Saturn Client
|
||||
* 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.
|
||||
*/
|
||||
@Overwrite
|
||||
@Nullable
|
||||
public Text getDisplayName() {
|
||||
GameProfile profile = ((PlayerListEntry) (Object) this).getProfile();
|
||||
@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.getName();
|
||||
UUID uuid = profile.getId();
|
||||
|
||||
if (SaturnPlayer.get(name, uuid) != null) {
|
||||
return Text.literal(Roles.getSaturnIndicator())
|
||||
.styled(style -> style.withColor(Roles.getIconColor(uuid)))
|
||||
.append(Text.literal(name).styled(style -> style.withColor(Formatting.WHITE)));
|
||||
if (SaturnPlayer.get(name, uuid) == null) {
|
||||
return;
|
||||
}
|
||||
return Text.literal(name);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"TabListMixin",
|
||||
"NameTagMixin",
|
||||
"PlayerEntityRendererMixin",
|
||||
"WhoAmI",
|
||||
"DrawContextAccessor",
|
||||
|
||||
@@ -11,8 +11,7 @@ base {
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { name = "KoSmX Maven"; url = 'https://maven.kosmx.dev/' }
|
||||
maven { name = "JitPack"; url = 'https://jitpack.io' }
|
||||
maven { name = "Selimaj"; url = 'https://git.selimaj.dev/api/packages/selimaj-dev/maven' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -24,24 +23,17 @@ dependencies {
|
||||
// Fabric API. This is technically optional, but you probably want it anyway.
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
|
||||
|
||||
// Player Animation Library
|
||||
include modImplementation(
|
||||
"dev.kosmx.player-anim:player-animation-lib-fabric:${project.player_anim}"
|
||||
)
|
||||
// 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("com.github.selimaj-dev:session-java:0.1.6")
|
||||
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")
|
||||
|
||||
// Optional: BendyLib
|
||||
// include modRuntimeOnly(
|
||||
// "io.github.kosmx.bendy-lib:bendy-lib-fabric:${project.bendylib_version}"
|
||||
// )
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -10,12 +10,10 @@ org.gradle.configuration-cache=false
|
||||
minecraft_version=1.21.5
|
||||
yarn_mappings=1.21.5+build.1
|
||||
loader_version=0.18.4
|
||||
loom_version=1.15-SNAPSHOT
|
||||
loom_version=1.17.21
|
||||
|
||||
fabric_api_version=0.128.2+1.21.5
|
||||
|
||||
# Mod Properties
|
||||
maven_group=org.saturnclient
|
||||
archives_base_name=saturn-client
|
||||
|
||||
player_anim = 2.0.2+1.21.5
|
||||
|
||||
+220
-96
@@ -2,6 +2,10 @@ 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;
|
||||
@@ -24,6 +28,7 @@ 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;
|
||||
|
||||
@@ -38,6 +43,8 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
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;
|
||||
@@ -95,165 +102,280 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack,
|
||||
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) {
|
||||
MatrixStack.Entry entry = matrixStack.peek();
|
||||
// Extract matrix (you must transform positions manually now)
|
||||
Matrix4f pos = entry.getPositionMatrix();
|
||||
Matrix3f normalMat = entry.getNormalMatrix();
|
||||
|
||||
int color = 0xFFFFFFFF; // white RGBA
|
||||
|
||||
if (!flipWinding) {
|
||||
// CCW
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
|
||||
(float) bottomLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
|
||||
(float) bottomRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
|
||||
(float) topRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
|
||||
(float) topLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
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
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
|
||||
(float) topLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
|
||||
(float) topRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
|
||||
(float) bottomRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
|
||||
(float) bottomLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
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 matrixStack, VertexConsumer vertexConsumer, PlayerEntityRenderState playerState,
|
||||
int light, int overlay) {
|
||||
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;
|
||||
float capePartHeight = capeHeight / PARTS;
|
||||
|
||||
float x1 = -capeWidth / 2.0f;
|
||||
float x2 = capeWidth / 2.0f;
|
||||
|
||||
final float frontU1 = FRONT_RECT.u1();
|
||||
final float frontU2 = FRONT_RECT.u2();
|
||||
final float frontV1 = FRONT_RECT.v1();
|
||||
final float frontV2 = FRONT_RECT.v2();
|
||||
final float frontPartV = (frontV2 - frontV1) / PARTS;
|
||||
|
||||
final float backU1 = BACK_RECT.u1();
|
||||
final float backU2 = BACK_RECT.u2();
|
||||
final float backV1 = BACK_RECT.v1();
|
||||
final float backV2 = BACK_RECT.v2();
|
||||
final float backPartV = (backV2 - backV1) / PARTS;
|
||||
|
||||
final float leftU1 = LEFT_EDGE_RECT.u1();
|
||||
final float leftU2 = LEFT_EDGE_RECT.u2();
|
||||
final float leftV1 = LEFT_EDGE_RECT.v1();
|
||||
final float leftV2 = LEFT_EDGE_RECT.v2();
|
||||
final float leftPartV = (leftV2 - leftV1) / PARTS;
|
||||
|
||||
final float rightU1 = RIGHT_EDGE_RECT.u1();
|
||||
final float rightU2 = RIGHT_EDGE_RECT.u2();
|
||||
final float rightV1 = RIGHT_EDGE_RECT.v1();
|
||||
final float rightV2 = RIGHT_EDGE_RECT.v2();
|
||||
final float rightPartV = (rightV2 - rightV1) / PARTS;
|
||||
|
||||
final float topU1 = TOP_RECT.u1();
|
||||
final float topU2 = TOP_RECT.u2();
|
||||
final float topV1 = TOP_RECT.v1();
|
||||
final float topV2 = TOP_RECT.v2();
|
||||
|
||||
final float bottomU1 = BOTTOM_RECT.u1();
|
||||
final float bottomU2 = BOTTOM_RECT.u2();
|
||||
final float bottomV1 = BOTTOM_RECT.v1();
|
||||
final float bottomV2 = BOTTOM_RECT.v2();
|
||||
|
||||
float curY = 0.0f;
|
||||
float curZ = 0.0f;
|
||||
// =============================
|
||||
// 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);
|
||||
|
||||
// 1. Thickness at the START of this segment (Must match PREVIOUS segment's end)
|
||||
float prevAngle = (i > 0) ? (2.0f - segmentValues[i - 1]) * ((float) Math.PI / 2f) : angle;
|
||||
float startAvgAngle = (angle + prevAngle) / 2.0f;
|
||||
float thickYStart = (float) Math.sin(startAvgAngle) * capeDepth;
|
||||
float thickZStart = (float) Math.cos(startAvgAngle) * capeDepth;
|
||||
spineY[i + 1] = spineY[i] + dirY * partHeight;
|
||||
spineZ[i + 1] = spineZ[i] + dirZ * partHeight;
|
||||
}
|
||||
|
||||
// 2. Thickness at the END of this segment (Must match NEXT segment's start)
|
||||
float nextAngle = (i < PARTS - 1) ? (2.0f - segmentValues[i + 1]) * ((float) Math.PI / 2f) : angle;
|
||||
float endAvgAngle = (angle + nextAngle) / 2.0f;
|
||||
float thickYEnd = (float) Math.sin(endAvgAngle) * capeDepth;
|
||||
float thickZEnd = (float) Math.cos(endAvgAngle) * capeDepth;
|
||||
// =============================
|
||||
// 2. Grid
|
||||
// =============================
|
||||
|
||||
float nextY = curY + dirY * capePartHeight;
|
||||
float nextZ = curZ + dirZ * capePartHeight;
|
||||
Vec3d[][] inner = new Vec3d[H_PARTS + 1][PARTS + 1];
|
||||
Vec3d[][] outer = new Vec3d[H_PARTS + 1][PARTS + 1];
|
||||
|
||||
// Inner Vertices (The spine of the cape)
|
||||
Vec3d innerTopLeft = new Vec3d(x2, curY, curZ);
|
||||
Vec3d innerTopRight = new Vec3d(x1, curY, curZ);
|
||||
Vec3d innerBotLeft = new Vec3d(x2, nextY, nextZ);
|
||||
Vec3d innerBotRight = new Vec3d(x1, nextY, nextZ);
|
||||
for (int x = 0; x <= H_PARTS; x++) {
|
||||
|
||||
// Outer Vertices (Offset by the specific joint thickness)
|
||||
Vec3d outerTopLeft = new Vec3d(x2, curY - thickYStart, curZ - thickZStart);
|
||||
Vec3d outerTopRight = new Vec3d(x1, curY - thickYStart, curZ - thickZStart);
|
||||
Vec3d outerBotLeft = new Vec3d(x2, nextY - thickYEnd, nextZ - thickZEnd);
|
||||
Vec3d outerBotRight = new Vec3d(x1, nextY - thickYEnd, nextZ - thickZEnd);
|
||||
float t = (float) x / H_PARTS;
|
||||
float xPos = -capeWidth / 2.0f + capeWidth * t;
|
||||
|
||||
// FRONT (facing camera)
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerBotRight, outerBotLeft, outerTopLeft, outerTopRight,
|
||||
frontU2, frontV1 + (frontPartV * i), frontU1, frontV1 + (frontPartV * (i + 1)),
|
||||
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 (Facing player)
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, innerBotRight, innerTopRight, innerTopLeft,
|
||||
backU2, backV1 + (backPartV * i), backU1, backV1 + (backPartV * (i + 1)),
|
||||
// BACK
|
||||
renderCapeQuad(vertexConsumer, entry,
|
||||
innerBL, innerBR, innerTR, innerTL,
|
||||
buEnd, bvStart, buStart, bvEnd,
|
||||
light, overlay, 0, 0, -1, false);
|
||||
}
|
||||
}
|
||||
|
||||
// LEFT EDGE
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, outerBotLeft, outerTopLeft, innerTopLeft,
|
||||
leftU1, leftV1 + (leftPartV * i), leftU2, leftV1 + (leftPartV * (i + 1)),
|
||||
light, overlay, 1, 0, 0, false);
|
||||
// =============================
|
||||
// 4. LEFT SIDE (x = H_PARTS)
|
||||
// =============================
|
||||
|
||||
// RIGHT EDGE
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerBotRight, innerBotRight, innerTopRight, outerTopRight,
|
||||
rightU1, rightV1 + (rightPartV * i), rightU2, rightV1 + (rightPartV * (i + 1)),
|
||||
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);
|
||||
|
||||
// TOP FACE (Only on first segment)
|
||||
if (i == 0) {
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerTopLeft, outerTopRight, innerTopRight, innerTopLeft,
|
||||
topU1, topV1, topU2, topV2, light, overlay, 0, 1, 0, false);
|
||||
}
|
||||
|
||||
// BOTTOM FACE (Only on last segment)
|
||||
if (i == PARTS - 1) {
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, innerBotRight, outerBotRight, outerBotLeft,
|
||||
bottomU1, bottomV1, bottomU2, bottomV2, light, overlay, 0, -1, 0, false);
|
||||
// =============================
|
||||
// 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);
|
||||
}
|
||||
|
||||
curY = nextY;
|
||||
curZ = nextZ;
|
||||
// =============================
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +436,7 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastUpdate >= 20) {
|
||||
float velX = Math.min(0.6f, playerEntityRenderState.field_53537 / 108.0f);
|
||||
float velX = Math.min(0.8f, playerEntityRenderState.field_53537 / 108.0f);
|
||||
float rawVelY = playerEntityRenderState.field_53536;
|
||||
float velY = (rawVelY > 3.4f ? rawVelY : rawVelY < -3.4f ? rawVelY : 0.0f) / 16;
|
||||
|
||||
@@ -328,7 +450,9 @@ public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderStat
|
||||
lastUpdate = now;
|
||||
}
|
||||
|
||||
renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV);
|
||||
horizontalCurve = MathHelper.clamp(playerEntityRenderState.field_53537 / 150.0f, -1.0f, 1.0f);
|
||||
|
||||
renderCape(matrixStack.peek(), vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV);
|
||||
|
||||
matrixStack.pop();
|
||||
}
|
||||
|
||||
+28
-29
@@ -3,18 +3,18 @@ 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;
|
||||
|
||||
import dev.kosmx.playerAnim.api.layered.AnimationStack;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
|
||||
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class SaturnProviderImpl implements SaturnProvider {
|
||||
@Override
|
||||
public MinecraftClientRef getClient() {
|
||||
@@ -23,31 +23,13 @@ public class SaturnProviderImpl implements SaturnProvider {
|
||||
|
||||
@Override
|
||||
public void playEmote(UUID fromPlayerUuid, String emoteIdOrNull) {
|
||||
if (SaturnClient.client == null || SaturnClient.client.world == null) {
|
||||
if (fromPlayerUuid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
|
||||
if (!player.getUuid().equals(fromPlayerUuid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
|
||||
|
||||
if (emoteIdOrNull != null && !emoteIdOrNull.isEmpty()) {
|
||||
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
|
||||
animationStack.removeLayer(1000);
|
||||
}
|
||||
animationStack.addAnimLayer(1000,
|
||||
PlayerAnimationRegistry
|
||||
.getAnimation(Identifier.of("saturnclient", emoteIdOrNull))
|
||||
.playAnimation());
|
||||
} else {
|
||||
animationStack.removeLayer(1000);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// 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
|
||||
@@ -65,6 +47,23 @@ public class SaturnProviderImpl implements SaturnProvider {
|
||||
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);
|
||||
|
||||
@@ -7,8 +7,6 @@ import net.minecraft.client.MinecraftClient;
|
||||
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.cosmetics.Emotes;
|
||||
import org.saturnclient.cosmetics.Hats;
|
||||
import org.saturnclient.cosmetics.Cloaks;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.impl.provider.GLFWProviderImpl;
|
||||
import org.saturnclient.impl.provider.FeatureProviderFabric;
|
||||
@@ -61,12 +59,6 @@ public class SaturnClient implements ModInitializer {
|
||||
|
||||
KeyInputHandler.register();
|
||||
Emotes.initialize();
|
||||
if (ServiceClient.authenticate()) {
|
||||
Cloaks.initialize();
|
||||
Hats.initialize();
|
||||
LOGGER.info(MOD_ID + " initialization complete");
|
||||
} else {
|
||||
LOGGER.error("Failed to authenticate with the server");
|
||||
}
|
||||
ServiceClient.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
+5
-8
@@ -9,12 +9,10 @@ import org.saturnclient.config.Config;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.emotes.fabric.Emotes;
|
||||
import org.saturnclient.ui.screens.EmoteWheel;
|
||||
import org.saturnclient.ui.screens.ShiftMenu;
|
||||
|
||||
import dev.kosmx.playerAnim.api.layered.AnimationStack;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
||||
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
/**
|
||||
@@ -45,13 +43,12 @@ public class KeyInputHandler {
|
||||
Providers.saturn.getClient().setScreen(new EmoteWheel());
|
||||
}
|
||||
|
||||
if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()) {
|
||||
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(SaturnClient.client.player);
|
||||
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
|
||||
animationStack.removeLayer(1000);
|
||||
// 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("");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+25
-19
@@ -1,43 +1,49 @@
|
||||
package org.saturnclient.saturnclient.mixin;
|
||||
|
||||
import net.minecraft.client.network.PlayerListEntry;
|
||||
import net.minecraft.scoreboard.Team;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Formatting;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.saturnclient.client.player.Roles;
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import 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 {
|
||||
|
||||
@Shadow
|
||||
private GameProfile profile;
|
||||
|
||||
/**
|
||||
* @author HexLeo
|
||||
* @reason Adds the Saturn Client icon to the player's name if they are online
|
||||
* with Saturn Client
|
||||
* 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.
|
||||
*/
|
||||
@Overwrite
|
||||
@Nullable
|
||||
public Text getDisplayName() {
|
||||
GameProfile profile = ((PlayerListEntry) (Object) this).getProfile();
|
||||
@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.getName();
|
||||
UUID uuid = profile.getId();
|
||||
|
||||
if (SaturnPlayer.get(uuid) != null) {
|
||||
return Text.literal(Roles.getSaturnIndicator())
|
||||
.styled(style -> style.withColor(Roles.getIconColor(uuid)))
|
||||
.append(Text.literal(name).styled(style -> style.withColor(Formatting.WHITE)));
|
||||
if (SaturnPlayer.get(name, uuid) == null) {
|
||||
return;
|
||||
}
|
||||
return Text.literal(name);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"TabListMixin",
|
||||
"NameTagMixin",
|
||||
"PlayerEntityRendererMixin",
|
||||
"WhoAmI",
|
||||
"DrawContextAccessor",
|
||||
|
||||
@@ -1,109 +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 = "KoSmX Maven"; url = 'https://maven.kosmx.dev/' }
|
||||
maven { name = "JitPack"; url = 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// To change the versions see the gradle.properties file
|
||||
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
||||
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||
|
||||
// Fabric API. This is technically optional, but you probably want it anyway.
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"
|
||||
|
||||
// Player Animation Library
|
||||
// include modImplementation(
|
||||
// "dev.kosmx.player-anim:player-animation-lib-fabric:${project.player_anim}"
|
||||
// )
|
||||
|
||||
implementation project(":common")
|
||||
|
||||
include implementation("com.github.selimaj-dev:session-java:0.1.6")
|
||||
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")
|
||||
|
||||
// Optional: BendyLib
|
||||
// include modRuntimeOnly(
|
||||
// "io.github.kosmx.bendy-lib:bendy-lib-fabric:${project.bendylib_version}"
|
||||
// )
|
||||
}
|
||||
|
||||
/*
|
||||
* ================================================================
|
||||
* Resource Processing
|
||||
* ================================================================
|
||||
* Injects version values into fabric.mod.json.
|
||||
*/
|
||||
processResources {
|
||||
inputs.property "version", project.version
|
||||
inputs.property "minecraft_version", project.minecraft_version
|
||||
inputs.property "loader_version", project.loader_version
|
||||
filteringCharset "UTF-8"
|
||||
|
||||
filesMatching("fabric.mod.json") {
|
||||
expand(
|
||||
"version": project.version,
|
||||
"minecraft_version": project.minecraft_version,
|
||||
"loader_version": project.loader_version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
it.options.release = 21
|
||||
}
|
||||
|
||||
java {
|
||||
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
|
||||
// if it is present.
|
||||
// If you remove this line, sources will not be generated.
|
||||
withSourcesJar()
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
jar {
|
||||
inputs.property "archivesName", project.base.archivesName
|
||||
|
||||
from("LICENSE") {
|
||||
rename { "${it}_${inputs.properties.archivesName}"}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,9 @@ org.gradle.configuration-cache=false
|
||||
minecraft_version=1.21.6
|
||||
yarn_mappings=1.21.6+build.1
|
||||
loader_version=0.18.5
|
||||
loom_version=1.15-SNAPSHOT
|
||||
|
||||
fabric_api_version=0.128.2+1.21.6
|
||||
|
||||
# Mod Properties
|
||||
maven_group=org.saturnclient
|
||||
archives_base_name=saturn-client
|
||||
|
||||
# player_anim = 2.0.2+1.21.5
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = 'Fabric'
|
||||
url = 'https://maven.fabricmc.net/'
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
-335
@@ -1,335 +0,0 @@
|
||||
package org.saturnclient.impl.cosmetics;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
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 net.minecraft.client.render.OverlayTexture;
|
||||
import net.minecraft.client.render.VertexConsumer;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
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.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 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 renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack,
|
||||
Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft,
|
||||
float u1, float v1, float u2, float v2, int light, int overlay,
|
||||
float normalX, float normalY, float normalZ, boolean flipWinding) {
|
||||
MatrixStack.Entry entry = matrixStack.peek();
|
||||
|
||||
if (!flipWinding) {
|
||||
// CCW
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
|
||||
(float) bottomLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
|
||||
(float) bottomRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
|
||||
(float) topRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
|
||||
(float) topLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
} else {
|
||||
// CW
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
|
||||
(float) topLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
|
||||
(float) topRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
|
||||
(float) bottomRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
|
||||
(float) bottomLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCape(MatrixStack matrixStack, VertexConsumer vertexConsumer, PlayerEntityRenderState playerState,
|
||||
int light, int overlay) {
|
||||
float capeWidth = 10.0f / 16.0f;
|
||||
float capeHeight = 16.0f / 16.0f;
|
||||
float capeDepth = 1.0f / 16.0f;
|
||||
float capePartHeight = capeHeight / PARTS;
|
||||
|
||||
float x1 = -capeWidth / 2.0f;
|
||||
float x2 = capeWidth / 2.0f;
|
||||
|
||||
final float frontU1 = FRONT_RECT.u1();
|
||||
final float frontU2 = FRONT_RECT.u2();
|
||||
final float frontV1 = FRONT_RECT.v1();
|
||||
final float frontV2 = FRONT_RECT.v2();
|
||||
final float frontPartV = (frontV2 - frontV1) / PARTS;
|
||||
final float backU1 = BACK_RECT.u1();
|
||||
final float backU2 = BACK_RECT.u2();
|
||||
final float backV1 = BACK_RECT.v1();
|
||||
final float backV2 = BACK_RECT.v2();
|
||||
final float backPartV = (backV2 - backV1) / PARTS;
|
||||
final float leftU1 = LEFT_EDGE_RECT.u1();
|
||||
final float leftU2 = LEFT_EDGE_RECT.u2();
|
||||
final float leftV1 = LEFT_EDGE_RECT.v1();
|
||||
final float leftV2 = LEFT_EDGE_RECT.v2();
|
||||
final float leftPartV = (leftV2 - leftV1) / PARTS;
|
||||
final float rightU1 = RIGHT_EDGE_RECT.u1();
|
||||
final float rightU2 = RIGHT_EDGE_RECT.u2();
|
||||
final float rightV1 = RIGHT_EDGE_RECT.v1();
|
||||
final float rightV2 = RIGHT_EDGE_RECT.v2();
|
||||
final float rightPartV = (rightV2 - rightV1) / PARTS;
|
||||
final float topU1 = TOP_RECT.u1();
|
||||
final float topU2 = TOP_RECT.u2();
|
||||
final float topV1 = TOP_RECT.v1();
|
||||
final float topV2 = TOP_RECT.v2();
|
||||
final float bottomU1 = BOTTOM_RECT.u1();
|
||||
final float bottomU2 = BOTTOM_RECT.u2();
|
||||
final float bottomV1 = BOTTOM_RECT.v1();
|
||||
final float bottomV2 = BOTTOM_RECT.v2();
|
||||
|
||||
float curY = 0.0f;
|
||||
float curZ = 0.0f;
|
||||
|
||||
for (int i = 0; i < PARTS; i++) {
|
||||
float angle = (2.0f - segmentValues[i]) * ((float) Math.PI / 2f);
|
||||
float dirY = -(float) Math.cos(angle);
|
||||
float dirZ = (float) Math.sin(angle);
|
||||
|
||||
// 1. Thickness at the START of this segment (Must match PREVIOUS segment's end)
|
||||
float prevAngle = (i > 0) ? (2.0f - segmentValues[i - 1]) * ((float) Math.PI / 2f) : angle;
|
||||
float startAvgAngle = (angle + prevAngle) / 2.0f;
|
||||
float thickYStart = (float) Math.sin(startAvgAngle) * capeDepth;
|
||||
float thickZStart = (float) Math.cos(startAvgAngle) * capeDepth;
|
||||
|
||||
// 2. Thickness at the END of this segment (Must match NEXT segment's start)
|
||||
float nextAngle = (i < PARTS - 1) ? (2.0f - segmentValues[i + 1]) * ((float) Math.PI / 2f) : angle;
|
||||
float endAvgAngle = (angle + nextAngle) / 2.0f;
|
||||
float thickYEnd = (float) Math.sin(endAvgAngle) * capeDepth;
|
||||
float thickZEnd = (float) Math.cos(endAvgAngle) * capeDepth;
|
||||
|
||||
float nextY = curY + dirY * capePartHeight;
|
||||
float nextZ = curZ + dirZ * capePartHeight;
|
||||
|
||||
// Inner Vertices (The spine of the cape)
|
||||
Vec3d innerTopLeft = new Vec3d(x2, curY, curZ);
|
||||
Vec3d innerTopRight = new Vec3d(x1, curY, curZ);
|
||||
Vec3d innerBotLeft = new Vec3d(x2, nextY, nextZ);
|
||||
Vec3d innerBotRight = new Vec3d(x1, nextY, nextZ);
|
||||
|
||||
// Outer Vertices (Offset by the specific joint thickness)
|
||||
Vec3d outerTopLeft = new Vec3d(x2, curY - thickYStart, curZ - thickZStart);
|
||||
Vec3d outerTopRight = new Vec3d(x1, curY - thickYStart, curZ - thickZStart);
|
||||
Vec3d outerBotLeft = new Vec3d(x2, nextY - thickYEnd, nextZ - thickZEnd);
|
||||
Vec3d outerBotRight = new Vec3d(x1, nextY - thickYEnd, nextZ - thickZEnd);
|
||||
|
||||
// FRONT (facing camera)
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerBotRight, outerBotLeft, outerTopLeft, outerTopRight,
|
||||
frontU2, frontV1 + (frontPartV * i), frontU1, frontV1 + (frontPartV * (i + 1)),
|
||||
light, overlay, 0, 0, 1, false);
|
||||
|
||||
// BACK (Facing player)
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, innerBotRight, innerTopRight, innerTopLeft,
|
||||
backU2, backV1 + (backPartV * i), backU1, backV1 + (backPartV * (i + 1)),
|
||||
light, overlay, 0, 0, -1, false);
|
||||
|
||||
// LEFT EDGE
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, outerBotLeft, outerTopLeft, innerTopLeft,
|
||||
leftU1, leftV1 + (leftPartV * i), leftU2, leftV1 + (leftPartV * (i + 1)),
|
||||
light, overlay, 1, 0, 0, false);
|
||||
|
||||
// RIGHT EDGE
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerBotRight, innerBotRight, innerTopRight, outerTopRight,
|
||||
rightU1, rightV1 + (rightPartV * i), rightU2, rightV1 + (rightPartV * (i + 1)),
|
||||
light, overlay, -1, 0, 0, false);
|
||||
|
||||
// TOP FACE (Only on first segment)
|
||||
if (i == 0) {
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
outerTopLeft, outerTopRight, innerTopRight, innerTopLeft,
|
||||
topU1, topV1, topU2, topV2, light, overlay, 0, 1, 0, false);
|
||||
}
|
||||
|
||||
// BOTTOM FACE (Only on last segment)
|
||||
if (i == PARTS - 1) {
|
||||
this.renderCapeQuad(vertexConsumer, matrixStack,
|
||||
innerBotLeft, innerBotRight, outerBotRight, outerBotLeft,
|
||||
bottomU1, bottomV1, bottomU2, bottomV2, light, overlay, 0, -1, 0, false);
|
||||
}
|
||||
|
||||
curY = nextY;
|
||||
curZ = nextZ;
|
||||
}
|
||||
}
|
||||
|
||||
// Shift array left and insert new value at the start
|
||||
private void updateSegmentValues(float value) {
|
||||
// Shift all elements one position to the right (from end to start)
|
||||
for (int i = segmentValues.length - 1; i > 0; i--) {
|
||||
segmentValues[i] = segmentValues[i - 1];
|
||||
}
|
||||
|
||||
// Insert the new value at index 0
|
||||
segmentValues[0] = value;
|
||||
}
|
||||
|
||||
private long lastUpdate = 0;
|
||||
|
||||
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light,
|
||||
PlayerEntityRenderState playerEntityRenderState, float f, float g) {
|
||||
if (playerEntityRenderState.invisible || !playerEntityRenderState.capeVisible
|
||||
|| playerEntityRenderState.skinTextures.capeTexture() != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SaturnPlayer player = SaturnPlayer.get(playerEntityRenderState.name);
|
||||
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
IdentifierRef customCape = Cloaks.getCurrentCloakTexture(player.cloak);
|
||||
if (customCape == null
|
||||
|| this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.WINGS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minBrightness = 7;
|
||||
int blockLight = (light >> 4) & 0xF;
|
||||
int skyLight = (light >> 20) & 0xF;
|
||||
blockLight = Math.max(blockLight, minBrightness);
|
||||
skyLight = Math.max(skyLight, minBrightness);
|
||||
light = (skyLight << 20) | (blockLight << 4);
|
||||
|
||||
VertexConsumer vertexConsumer = vertexConsumerProvider
|
||||
.getBuffer(ShaderUtils.getRenderLayer(customCape));
|
||||
|
||||
matrixStack.push();
|
||||
|
||||
matrixStack.translate(0.0f, 0.0f, 0.12f);
|
||||
|
||||
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack, LayerType.HUMANOID)) {
|
||||
matrixStack.translate(0.0F, -0.053125F, 0.06875F);
|
||||
}
|
||||
|
||||
if (playerEntityRenderState.isInSneakingPose) {
|
||||
matrixStack.translate(0, 0.16f, 0.0f);
|
||||
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(29.0f));
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastUpdate >= 20) {
|
||||
float velX = Math.min(0.6f, playerEntityRenderState.field_53537 / 108.0f);
|
||||
float rawVelY = playerEntityRenderState.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, playerEntityRenderState.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;
|
||||
}
|
||||
|
||||
renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light, OverlayTexture.DEFAULT_UV);
|
||||
|
||||
matrixStack.pop();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.saturnclient.impl.provider;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.saturnclient.common.provider.SaturnProvider;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.game.MinecraftClientRef;
|
||||
import org.saturnclient.impl.cosmetics.utils.IdentifierUtils;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
public class SaturnProviderImpl implements SaturnProvider {
|
||||
@Override
|
||||
public MinecraftClientRef getClient() {
|
||||
return (MinecraftClientRef) SaturnClient.client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playEmote(UUID fromPlayerUuid, String emoteIdOrNull) {
|
||||
// if (SaturnClient.client == null || SaturnClient.client.world == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
|
||||
// if (!player.getUuid().equals(fromPlayerUuid)) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
|
||||
|
||||
// if (emoteIdOrNull != null && !emoteIdOrNull.isEmpty()) {
|
||||
// if (animationStack.isActive() && animationStack.getPriority() == 1000) {
|
||||
// animationStack.removeLayer(1000);
|
||||
// }
|
||||
// animationStack.addAnimLayer(1000,
|
||||
// PlayerAnimationRegistry
|
||||
// .getAnimation(Identifier.of("saturnclient", emoteIdOrNull))
|
||||
// .playAnimation());
|
||||
// } else {
|
||||
// animationStack.removeLayer(1000);
|
||||
// }
|
||||
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logInfo(String message) {
|
||||
SaturnClient.LOGGER.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logError(String message) {
|
||||
SaturnClient.LOGGER.error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logError(String message, Throwable throwable) {
|
||||
SaturnClient.LOGGER.error(message, throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBufferedImageTexture(IdentifierRef i, BufferedImage bi) {
|
||||
IdentifierUtils.registerBufferedImageTextureFast(i, bi);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.saturnclient.saturnclient.mixin;
|
||||
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
import net.minecraft.client.font.TextRenderer.TextLayerType;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.EntityRenderDispatcher;
|
||||
import net.minecraft.client.render.entity.EntityRenderer;
|
||||
import net.minecraft.client.render.entity.state.EntityRenderState;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.text.MutableText;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Formatting;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
|
||||
import org.saturnclient.client.player.Roles;
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
@Mixin(value = EntityRenderer.class, priority = 1800)
|
||||
public abstract class NameTagMixin<S extends EntityRenderState> {
|
||||
@Shadow
|
||||
public EntityRenderDispatcher dispatcher;
|
||||
|
||||
/**
|
||||
* @author HexLeo
|
||||
* @reason Adds the Saturn Client icon to the player's name if they are online
|
||||
* with Saturn Client
|
||||
*/
|
||||
@Inject(method = "renderLabelIfPresent(Lnet/minecraft/client/render/entity/state/EntityRenderState;Lnet/minecraft/text/Text;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At("HEAD"), cancellable = true)
|
||||
private void onRenderLabelIfPresent(S state, Text text, MatrixStack matrices,
|
||||
VertexConsumerProvider vertexConsumers, int light, CallbackInfo ci) {
|
||||
|
||||
UUID uuid = isSaturn(state);
|
||||
|
||||
if (uuid != null) {
|
||||
MutableText iconText = Text.literal(Roles.getSaturnIndicator())
|
||||
.styled(style -> style.withColor(Roles.getIconColor(uuid)));
|
||||
|
||||
Text nameText = text.copy().styled(style -> style.withColor(Formatting.WHITE));
|
||||
|
||||
renderLabel(state, iconText.append(nameText), matrices,
|
||||
vertexConsumers, light);
|
||||
|
||||
ci.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private UUID isSaturn(S state) {
|
||||
if (state instanceof PlayerEntityRenderState) {
|
||||
String name = ((PlayerEntityRenderState) state).name;
|
||||
SaturnPlayer player = SaturnPlayer.get(name);
|
||||
if (player != null) {
|
||||
return player.uuid;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void renderLabel(S state, Text text, MatrixStack matrices,
|
||||
VertexConsumerProvider vertexConsumers, int light) {
|
||||
Vec3d vec3d = state.nameLabelPos;
|
||||
if (vec3d != null) {
|
||||
boolean bl = !state.sneaking;
|
||||
int i = "deadmau5".equals(text.getString()) ? -10 : 0;
|
||||
matrices.push();
|
||||
matrices.translate(vec3d.x, vec3d.y + 0.5, vec3d.z);
|
||||
matrices.multiply(this.dispatcher.getRotation());
|
||||
matrices.scale(0.025F, -0.025F, 0.025F);
|
||||
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
|
||||
TextRenderer textRenderer = SaturnClient.client.textRenderer;
|
||||
float f = (float) (-textRenderer.getWidth(text)) / 2.0F;
|
||||
int j = (int) (SaturnClient.client.options.getTextBackgroundOpacity(0.25F) * 255.0F) << 24;
|
||||
textRenderer.draw(text, f, (float) i, -2130706433, false, matrix4f, vertexConsumers,
|
||||
bl ? TextLayerType.SEE_THROUGH : TextLayerType.NORMAL, j, light);
|
||||
if (bl) {
|
||||
textRenderer.draw(text, f, (float) i, -1, false, matrix4f, vertexConsumers, TextLayerType.NORMAL, 0,
|
||||
light);
|
||||
}
|
||||
matrices.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
package org.saturnclient.saturnclient.mixin;
|
||||
|
||||
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.player.PlayerEntity;
|
||||
|
||||
import org.saturnclient.impl.cosmetics.CloakFeatureRenderer;
|
||||
import org.saturnclient.impl.cosmetics.HatFeatureRenderer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(PlayerEntityRenderer.class)
|
||||
public abstract class PlayerEntityRendererMixin
|
||||
extends LivingEntityRenderer<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()));
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package org.saturnclient.saturnclient.mixin;
|
||||
|
||||
import net.minecraft.client.network.PlayerListEntry;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Formatting;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.saturnclient.client.player.Roles;
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
@Mixin(PlayerListEntry.class)
|
||||
public abstract class TabListMixin {
|
||||
|
||||
@Shadow
|
||||
private GameProfile profile;
|
||||
|
||||
/**
|
||||
* @author HexLeo
|
||||
* @reason Adds the Saturn Client icon to the player's name if they are online
|
||||
* with Saturn Client
|
||||
*/
|
||||
@Overwrite
|
||||
@Nullable
|
||||
public Text getDisplayName() {
|
||||
GameProfile profile = ((PlayerListEntry) (Object) this).getProfile();
|
||||
String name = profile.getName();
|
||||
UUID uuid = profile.getId();
|
||||
|
||||
if (SaturnPlayer.get(uuid) != null) {
|
||||
return Text.literal(Roles.getSaturnIndicator())
|
||||
.styled(style -> style.withColor(Roles.getIconColor(uuid)))
|
||||
.append(Text.literal(name).styled(style -> style.withColor(Formatting.WHITE)));
|
||||
}
|
||||
return Text.literal(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Done to increase the memory available to gradle.
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
org.gradle.parallel=true
|
||||
|
||||
# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
# Fabric Properties
|
||||
# check these on https://fabricmc.net/develop
|
||||
minecraft_version=1.21.7
|
||||
yarn_mappings=1.21.7+build.8
|
||||
loader_version=0.19.0
|
||||
|
||||
fabric_api_version=0.129.0+1.21.7
|
||||
|
||||
# Mod Properties
|
||||
maven_group=org.saturnclient
|
||||
archives_base_name=saturn-client
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user