Compare commits

...
126 Commits
Author SHA1 Message Date
selimaj-dev dd91f3844a Merge pull request 'Bump the version to 0.1.2-beta' (#37) from release-0.1.2-beta into master
Release / build (release) Successful in 4m34s
Reviewed-on: #37
2026-09-27 17:21:05 +00:00
selimaj-devandclaude b7cf3092da Bump the version to 0.1.2-beta
Check / compile (pull_request) Successful in 2m32s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:18:29 +02:00
selimaj-dev 60afc12893 Merge pull request 'Bring in the UI fixes' (#36) from ui-fixes into master
Reviewed-on: #36
2026-09-27 17:16:14 +00:00
selimaj-devandclaude 8677837a8f Point common at the UI fixes
Check / compile (pull_request) Successful in 2m35s
common#7 fixes the emote wheel and restyles the buttons, title screen and
HUD editor, which the two commits before this one support.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:15:55 +02:00
selimaj-devandclaude 02f6cf7d71 Give common the player's skin texture
SaturnProvider.getPlayerSkin returns the signed-in player's skin texture,
or the default skin until it loads, for the title screen's player chip.
1.21.9 moved SkinTextures and renamed the skin provider's lookup, so each
side has its own version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:01:42 +02:00
selimaj-devandclaude 85a46c0bba Close the emote wheel with its key
The emote wheel key only opened the wheel. It now closes it too while the
wheel is open. This is done in the key handler: closing from the screen
would reopen it on the next tick, since the key is still down by then.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 18:43:27 +02:00
selimaj-dev 30f65bd914 Merge pull request 'Bring in the UI redesign' (#35) from ui-redesign into master
Reviewed-on: #35
2026-09-27 16:32:20 +00:00
selimaj-devandclaude bedb45c8aa Point common at the UI redesign
Check / compile (pull_request) Successful in 2m39s
common#6 redesigns the mod menu, mod and client settings and cosmetics
screens, which the two commits before this one build on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 18:28:20 +02:00
selimaj-devandclaude 1602520624 Blur behind Saturn's screens on 1.21.6+
From 1.21.6 vanilla's renderBackground drew its own panorama and blurred
at the vanilla setting, then Saturn's panorama was drawn unblurred on top.
SaturnScreenFabric now draws Saturn's panorama in renderBackground, blurs
it and keeps vanilla's darkening in a world, and GameRendererMixin gives
the blur shader Saturn's strength while a Saturn screen is open. Before
1.21.6 the existing blur reads the same setting.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 18:26:42 +02:00
selimaj-devandclaude f69c379ae1 Draw the player preview at a given yaw
EntityDrawerImpl follows common's new EntityDrawer contract: it draws the
player filling the preview box, turned by the yaw the cosmetics screen's
drag sets, and returns false when there's no player. The angle no longer
follows the cursor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 17:33:25 +02:00
selimaj-dev fbd0e23fb3 Merge pull request 'Title panorama on 1.21.6+, and Fabric Loader 0.19.5 everywhere' (#34) from panorama-1.21.6 into master
Reviewed-on: #34
2026-09-27 05:34:32 +00:00
selimaj-devandclaude 8dbb1d84c1 Use Fabric Loader 0.19.5 for every version
Check / compile (pull_request) Successful in 2m42s
1.21.4-1.21.6 declared loader 0.18.x and the others 0.19.0, but the
bundled emotes-fabric pulls in loader 0.19.5, and its nested mod already
requires loader >=0.19. So the declared version was never what ran: in
dev, Loom's loader libraries came from the declared version while the
loader itself was 0.19.5, and 1.21.4/1.21.6 sometimes started with
sponge-mixin 0.17.0 and crashed (NoClassDefFoundError:
IAdviceProvider). Declaring 0.19.5 makes dev match what runs, and makes
fabric.mod.json's fabricloader requirement (>=0.19.5) honest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 07:26:11 +02:00
selimaj-devandclaude ab70c67a4f Draw the title panorama behind Saturn screens on 1.21.6+
The call was commented out on 1.21.6+ (#30). From 1.21.6 CubeMapRenderer
draws from one cube map texture instead of six face textures, and it
crashes ("Texture view does not exist") if that texture isn't loaded.
Saturn's preload registered the six faces, and the vanilla
registerTextures only stores the cube map for the next resource reload,
which has already run by the time preload does.

- preload registers a CubemapTexture for the panorama through the
  TextureManager overload that loads it immediately, on 1.21.6+. Before
  1.21.6 it registers the six faces as before.
- The panorama is only drawn once preload has run, which also covers the
  splash overlay drawing the title menu on its first frames.
- 1.21.6+'s RotatingCubeMapRenderer.render has no opacity, so the
  panorama is fully opaque there. The background blur stays 1.21.5-only
  (TODO, #23).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 07:26:11 +02:00
selimaj-dev 0a6aa90e23 Merge pull request 'Custom nametags on every version (replace the name in the render state)' (#33) from nametags-render-state into master
Reviewed-on: #33
2026-09-27 05:14:42 +00:00
selimaj-devandclaude 22a7152f0d Replace nametags while the render state is built, on every version
Check / compile (pull_request) Successful in 2m49s
NametagsMixin hooked EntityRenderer.renderLabelIfPresent with a
VertexConsumerProvider, which 1.21.9 moved to the render command queue, so
custom nametags only worked before 1.21.9 (#30).

LivingEntityRendererMixin already captures health and entity type at the
end of LivingEntityRenderer.updateRenderState. It now also applies
NametagsMod's replacement to state.displayName there, the label the
renderer draws later. This works the same on every version, so
NametagsMixin and its features.mixins.json entry are gone.

As before, only entities that already show a label get a replacement. For
players, the role icon is added after this (PlayerEntityRendererMixin), so
it now comes before the replaced name and keeps its colour, instead of
being folded into the replacement text.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 07:13:54 +02:00
selimaj-dev b5ca9dbc8f Merge pull request 'Fill five version gaps from #30' (#32) from version-gaps-small into master
Reviewed-on: #32
2026-09-27 05:09:20 +00:00
selimaj-devandclaude 48118f6a84 Fill five version gaps from #30
Check / compile (pull_request) Successful in 2m31s
- World age on 1.21.11: World.getTime() was a shortcut for
  getLevelProperties().getTime(), which every version has, so
  getWorldAge uses that everywhere (the TPS feature and day counter got 0
  on 1.21.11).
- Screen resize on 1.21.11: override resize(int, int), the new signature.
- Splash loading-bar border on 1.21.9+: drawBorder was renamed
  drawStrokedRectangle.
- Drag deltas on 1.21.9+: mouseDragged(Click, offsetX, offsetY) gets the
  deltas as offsetX/offsetY; Saturn's screens got 0.0 before.
- Role icon in player names on 1.21.4-1.21.7: only storing the player in
  the render state needs Fabric API's render state data (1.21.8+). The
  updateRenderState hook now runs on every version and only the setData
  call is limited to 1.21.8+.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 06:59:18 +02:00
selimaj-dev 771ed93992 Merge pull request 'Build every Minecraft version from one shared src/ with Stonecutter' (#31) from stonecutter into master
Reviewed-on: #31
2026-09-27 04:50:14 +00:00
selimaj-devandclaude a8e6f14022 Fix the Release workflow's comment on how buildAll remaps
Check / compile (pull_request) Successful in 2m22s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 06:48:57 +02:00
selimaj-dev 84270c023a Merge pull request 'Stonecutter step 6: build 1.21.4 and 1.21.5 from the shared src/' (#29) from stonecutter-1.21.5 into stonecutter
Reviewed-on: #29
2026-09-27 04:36:09 +00:00
selimaj-devandclaude 25a19d7951 Build 1.21.4 with Stonecutter; every version now shares src/
Check / compile (pull_request) Successful in 7m44s
Step 6 of #8. versions/1.21.4/src is gone, so all eight versions (1.21.4 to
1.21.11) build from src/.

1.21.4's differences from 1.21.5 are behind version comments:
- The end gateway render layer has no render pipeline before 1.21.5
  (ShaderUtils: the pipeline field only exists from 1.21.5, and the layer
  is a four-way chain).
- Hat head rotation, the NativeImageBackedTexture constructor, armor slots
  (inventory.armor vs getEquippedStack), MatrixStackMixin's Quaternionf,
  and inside the pre-1.21.6 implementations: prevHeadYaw, GUI lighting,
  ModelTransformationMode, item depth, setShaderTexture and the blur
  uniform call. Those are one-line version comments, since the pre-1.21.6
  blocks are already inside /* */ on newer versions.

1.21.5's drawItem wrapped its body in a second isEmpty check after already
returning early for empty stacks; the redundant check is gone, so 1.21.4
and 1.21.5 share the body.

Freelook's zoom-out (FreelookMod.shouldZoomOut in GameRendererMixin) only
existed on 1.21.4. It doesn't depend on the Minecraft version, so it's
back on every version.

With no old copies left, the mc-<v> fallback in settings.gradle.kts and
scripts/port.sh are removed, and README and VERSION_GUIDE describe the
single layout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 06:34:32 +02:00
selimaj-devandclaude c5665548a0 Build 1.21.5 with Stonecutter from the shared src/
1.21.5 is the first version before the 1.21.6 GUI rewrite, which moved
GUI drawing from vertex consumers and a MatrixStack to render states and
a 2D Matrix3x2fStack.

- RenderScopeImpl and EntityDrawerImpl have two whole implementations,
  one per side of 1.21.6 (a single >=1.21.6 / else split), instead of
  conditionals scattered through them. Callers use RenderScopeImpl(context)
  in every version; 1.21.5's did the same thing through its
  (matrices, vertexConsumers) constructor.
- MatrixStackMixin (MatrixStack as a MatrixStackRef) is real before 1.21.6
  and empty from 1.21.6, registered everywhere like NametagsMixin.
- DrawContextAccessor exposes the vertex consumers before 1.21.6 and the
  GUI render state from it.
- Smaller changes: fog (BackgroundRenderer vs FogRenderer), the end
  portal texture arguments (ShaderUtils, now a flat three-way chain), the
  status effect sprite manager, and getIconId only from 1.21.6.

Features 1.21.6+ lacks stay on 1.21.5, with a TODO: the title panorama
behind Saturn screens and the background blur (screen.backgroundBlur,
see #23).

The 1.21.5 jar differs from the old one only in the helpers from earlier
steps, the RenderScopeImpl(context) calls, the empty SaturnRenderState
and unused Matrix3x2fStackRef, and 1.21.5's dead NameTagMixin (never
registered) being gone. 1.21.6-1.21.11 only gain the empty
MatrixStackMixin and its impl.mixins.json entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 06:23:23 +02:00
selimaj-dev 22142be7b5 Merge pull request 'Stonecutter step 5: build 1.21.6 and 1.21.7 from the shared src/' (#28) from stonecutter-1.21.7 into stonecutter
Reviewed-on: #28
2026-09-27 04:16:33 +00:00
selimaj-devandclaude a949c18ec1 Build 1.21.6 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 8m3s
1.21.6's copy only differed from 1.21.7's in RenderScopeImpl: item
rendering uses ItemRenderState, which 1.21.7 renamed to
KeyedItemRenderState. That's behind a version comment; 1.21.6 keeps its
older loader_version (0.18.5) in its gradle.properties.

Moving a version over leaves the old mc-<v> project's compiled classes in
the shared build/ folder, so a removed class (1.21.6's dead NameTagMixin)
still got packaged until a clean. VERSION_GUIDE now says to run
:<v>:clean once after moving a version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 06:10:31 +02:00
selimaj-devandclaude 6f122b5237 Build 1.21.7 with Stonecutter from the shared src/
Step 5 of #8. versions/1.21.7/src is gone; 1.21.7 builds from src/ as
:1.21.7.

1.21.7's copy differed from 1.21.8's in the player render state: Fabric
API's RenderStateDataKey only exists from 1.21.8 (fabric-rendering-v1
12.6.0), so 1.21.7 had no updateRenderState hook, no SaturnRenderState,
and looked players up by name for the cloak and hat. That stays the case:
- The cloak and hat use getData on >=1.21.8 and SaturnPlayer.get(name)
  before that.
- PlayerEntityRendererMixin's updateRenderState exists only on >=1.21.8,
  as flat ranges (>=1.21.9, else if >=1.21.8) calling a shared
  saturn$applyPlayer helper, instead of nested version blocks.
- SaturnRenderState's key only exists on >=1.21.8; the class is empty
  before.
- TODO: 1.21.6 and 1.21.7 still don't show the role icon in names.

1.21.7's NameTagMixin isn't carried over: it wasn't registered in any
mixin config, so it never ran.

The 1.21.7 jar differs from the old one only as 1.21.8's did in step 4
(entityAlpha and drawGroup helpers, getIconId, and the cloak getting its
buffer at the draw call), plus the empty SaturnRenderState and the
missing dead NameTagMixin. 1.21.8-1.21.11 only differ in
PlayerEntityRendererMixin (the helper).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 05:46:41 +02:00
selimaj-dev 85e5ef3f59 Merge pull request 'Stonecutter step 4: build 1.21.8 from the shared src/' (#27) from stonecutter-1.21.8 into stonecutter
Reviewed-on: #27
2026-09-27 03:31:32 +00:00
selimaj-devandclaude 0bf4fc6808 Build 1.21.8 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 10m37s
Step 4 of #8, the first version before the 1.21.9 render queue.
versions/1.21.8/src is gone; 1.21.8 builds from src/ as :1.21.8.

API differences behind version comments (<1.21.9 / >=1.21.9):
- Player cosmetics: FeatureRenderer.render takes a VertexConsumerProvider
  instead of the render queue. The cloak, hat and OBJ renderers name the
  render target "output" in both, so only the parameter type and the final
  draw call differ; the OBJ face loop moved into drawGroup.
- PlayerEntityRenderer.updateRenderState's entity type, the cape texture
  accessor, GameProfile getName()/getId() vs name()/id(), fonts in
  TextMixin, EntityRenderDispatcher vs EntityRenderManager, Screen input
  events (Click/KeyInput), and KeyBinding categories and isKeyPressed.

Features 1.21.9+ lacks stay on 1.21.8, with a TODO for 1.21.9+:
- NametagsMixin moves into src/. It hooks renderLabelIfPresent with a
  VertexConsumerProvider, which 1.21.9 changed, so on 1.21.9+ its body is
  empty; it stays registered, so features.mixins.json is the same for
  every version.
- The splash screen's loading-bar border, and the real drag deltas in
  SaturnScreenFabric.mouseDragged.
- Status effect icons keep 1.21.8's GUI atlas sprite; 1.21.9+ draws the
  texture instead (getIconId), as before.

Behaviour changes on 1.21.8: the cloak and hat read the Saturn player from
the render state (set by PlayerEntityRendererMixin, by UUID) instead of
looking it up by name, and the display-name null check applies.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 05:24:24 +02:00
selimaj-dev a44b4f4e90 Merge pull request 'Stonecutter step 3: build 1.21.9 from the shared src/' (#26) from stonecutter-1.21.9 into stonecutter
Reviewed-on: #26
2026-09-27 03:10:59 +00:00
selimaj-devandclaude f2d30e6611 Build 1.21.9 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 7m8s
Step 3 of #8. 1.21.9's copy was identical to the old 1.21.10 one, so the
existing version comments already cover it and no code changes: 1.21.9 is
added to stonecutterVersions and versions/1.21.9/src is removed.

Like 1.21.10 in step 2, 1.21.9 now renders hats (1.21.11's render-queue
implementation) and gets the display-name null check. The jar has the same
entries, fabric.mod.json and mixin configs as before; only those classes
and ShaderUtils's entityAlpha helper differ in bytecode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 05:10:15 +02:00
selimaj-dev 242653054d Merge pull request 'Stonecutter step 2: build 1.21.10 from the shared src/' (#25) from stonecutter-1.21.10 into stonecutter
Reviewed-on: #25
2026-09-27 03:06:12 +00:00
selimaj-devandclaude 95e4dff448 Build 1.21.10 with Stonecutter from the shared src/
Check / compile (pull_request) Successful in 7m13s
Step 2 of #8. versions/1.21.10/src is gone; 1.21.10 builds from src/ as
:1.21.10, with versions/1.21.10 keeping only gradle.properties and run/.

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 05:05:03 +02:00
selimaj-dev a30cf4d3ab Merge pull request 'Stonecutter step 1: build 1.21.11 from a shared src/' (#24) from stonecutter-scaffold into stonecutter
Reviewed-on: #24
2026-09-27 02:56:41 +00:00
selimaj-devandclaude eebfe8fa4d Switch the root build scripts to Kotlin and limit parallel remaps
Check / compile (pull_request) Successful in 7m47s
- settings.gradle, stonecutter.gradle and the root build.gradle become
  .kts. Loom's version for the Stonecutter versions moves to the root
  gradle.properties (loom_version), read by pluginManagement, since the
  Kotlin plugins block can't read properties. The old mc-<v> projects keep
  their Groovy scripts until they're folded in.
- buildAll ran all eight remapJar tasks at once. Each loads a Minecraft
  classpath and remaps a ~50 MB jar, which doesn't fit in the 3 GB daemon
  heap: a cold build stalled in garbage collection for 40+ minutes, and
  earlier runs failed. A shared build service now lets two remaps run
  together; a cold buildAll takes about 2 minutes.
- port.sh and VERSION_GUIDE follow the .kts file names.

The 1.21.11 jar is the same as with the Groovy scripts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 04:51:28 +02:00
selimaj-devandclaude 019e5cfb43 Build 1.21.11 with Stonecutter from a shared src/
Step 1 of #8. 1.21.11's version-specific code moves from versions/1.21.11/src
to the top-level src/, built by Stonecutter as the project :1.21.11. The other
versions still build from their own copies as :mc-<v> until they're folded in.

- settings.gradle: applies Stonecutter (Groovy controller) with
  stonecutterVersions = ["1.21.11"] and skips those folders when including
  the old mc-<v> projects.
- The old root build.gradle becomes stonecutter.gradle (the root project's
  script, with the shared repositories, common wiring and buildAll), and
  1.21.11's build.gradle becomes the root build.gradle that Stonecutter runs
  for each version. versions/1.21.11 keeps its gradle.properties and run/.
- The jar has the same 415 entries, fabric.mod.json and mixin configs as
  before; the only class differences also appear when rebuilding an
  untouched version today.
- scripts/port.sh treats src/ as the active Stonecutter version, so changes
  port between it and the old copies both ways.
- README and VERSION_GUIDE describe the layout, version comments and how to
  add a version.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 04:18:42 +02:00
selimaj-dev 6ed2ac1b60 Merge pull request 'Replace player-animation-lib with the Saturn emote library' (#22) from emote-library into master
Reviewed-on: #22
2026-09-26 20:04:00 +00:00
selimaj-devandclaude 1853b09443 Point common at master after saturnclientmc/common#5 merged
Check / compile (pull_request) Successful in 7m5s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 21:21:48 +02:00
selimaj-devandclaude 9126faa11c Replace player-animation-lib with the Saturn emote library
- Every version depends on org.saturnclient:emotes-fabric
  (${emotes_version}+<mc>, bundled). playEmote plays Saturn's emotes
  through it, and sneaking stops your own emote and tells the server.
  Emotes now work on 1.21.6+ too, where player-animation-lib was
  commented out.
- Removed every player-animation-lib and bendy-lib reference: the KoSmX
  repository, the dependencies, the player_anim properties, and the old
  (partly commented out) code in SaturnProviderImpl and KeyInputHandler.
- Build: Loom 1.15 -> 1.17.21 (the emotes jar is built with 1.17 and
  older Loom refuses it), Gradle 9.3 -> 9.8 (needed by Loom 1.17), and
  sourceSets moved out of the dependencies block in the root build.gradle,
  which Gradle 9.8 rejects.
- common: points at the emote-library branch (saturnclientmc/common#5).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 21:16:29 +02:00
selimaj-dev 090e5132b2 Merge pull request 'Refresh the README and use a local logo' (#21) from readme-refresh into master
Reviewed-on: #21
2026-09-26 02:24:33 +00:00
selimaj-devandclaude bbe6d02568 Refresh the README and use a local logo
Check / compile (pull_request) Successful in 2m4s
- Reference logo.png from the repo instead of the dead raw.github URL
  (the logo lives in the common submodule, which Gitea can't serve from
  a relative path, so a copy sits next to screenshot.png)
- Drop the section emojis and horizontal-rule separators
- Add supported versions, the built-in mods, installation (Modrinth,
  releases), network use (Mojang session sign-in; the access token never
  reaches the Saturn server), building from source with submodules, and
  a pointer to the version guide for multi-version changes

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 04:22:11 +02:00
selimaj-dev 04e7064790 Merge pull request 'CI: setup-java v5 and Gradle build cache' (#20) from ci-caching into master
Reviewed-on: #20
2026-09-26 02:10:44 +00:00
selimaj-devandclaude ff0bce4d24 Run the compile check on pull requests only, not on merges
Check / compile (pull_request) Successful in 10m49s
A merge to master re-ran the same compile the PR had just done. Keep
the pull_request trigger and drop the push-to-master one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 03:48:54 +02:00
selimaj-devandclaude 2ecb4f80ea Move setup-java to v5 and enable Gradle's build cache
Check / compile (pull_request) Successful in 9m39s
- setup-java v4 is deprecated; use v5 (still with `cache: gradle`,
  which works once job containers can reach the runner's cache server).
- Enable Gradle's build cache (org.gradle.caching) so unchanged
  compilations are restored from cache instead of re-run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 03:19:11 +02:00
selimaj-dev 41f9be8595 Merge pull request 'Version port script and compile check for pull requests' (#19) from port-tooling into master
Check / compile (push) Canceled after 12m52s
Reviewed-on: #19
2026-09-26 00:52:56 +00:00
selimaj-devandclaude f73f7e733c Add a version port script and a compile check for pull requests
Check / compile (pull_request) Successful in 18m7s
- scripts/port.sh ports a change made under versions/<from>/src to the
  other versions with `git apply --3way`: clean where files match,
  normal conflicts only where a version really differs. Supports
  uncommitted changes or --commit, --to, --dry-run, and detects changes
  already applied.
- .gitea/workflows/check.yml compiles every version on pull requests and
  pushes to master (--continue reports all failing versions).
- VERSION_GUIDE.md documents the layout, porting, and adding a version.

Closes #17, closes #18

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 02:41:22 +02:00
selimaj-dev 91b53daac9 Merge pull request 'Publish to Modrinth with Node instead of Python' (#16) from modrinth-node into master
Reviewed-on: #16
2026-09-25 17:18:13 +00:00
selimaj-devandclaude 5bcdc72b83 Publish to Modrinth with Node instead of Python
actions/setup-python can only install Python on GitHub's Ubuntu images,
so it fails on this runner's job image ("version 3.12 ... was not found
for this operating system"). Every Gitea runner has Node, since it runs
JavaScript actions, so port the publisher to a dependency-free Node
script (fetch/FormData/Blob) and drop the setup step. Behaviour is
unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 19:16:55 +02:00
selimaj-dev 3f0670ea9d Merge pull request 'Add a manual workflow that publishes a Gitea release to Modrinth' (#15) from modrinth-publish into master
Reviewed-on: #15
2026-09-25 17:13:53 +00:00
selimaj-devandclaude c54b5ae6d7 Add a manual workflow that publishes a Gitea release to Modrinth
"Publish to Modrinth" (workflow_dispatch, tag + dry_run inputs) takes the
jars the Release workflow attached to a Gitea release and creates one
Modrinth version per jar: version_number <version>+<mc>, that Minecraft
version, fabric loader, Fabric API as a required dependency, the release
notes as the changelog, and alpha/beta/release from the tag. It doesn't
rebuild, skips versions already on Modrinth, and downloads jars through
the public URL (Gitea's reported URLs use its internal ROOT_URL).

Needs a MODRINTH_TOKEN secret with the "Create versions" scope.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 19:11:41 +02:00
selimaj-dev 14c16d8fe0 Merge pull request 'Release workflow: upload through the public URL again; smaller jars' (#14) from release-public-url into master
Release / build (release) Successful in 18m36s
2026-09-25 16:36:40 +00:00
selimaj-devandclaude e3d1e4a442 Point common at the panorama downscale merge commit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 18:36:24 +02:00
selimaj-devandclaude 04605f6855 Upload release jars through the public URL again, with a size guard
gitea:3000 isn't resolvable from act_runner's job containers, which run
on their own Docker network. With the panorama downscaled in common the
jars are ~45 MB, under the Cloudflare tunnel's 100 MB request limit, so
upload through github.server_url again. Fail with a readable error
before uploading if any jar is 100 MB or more.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 18:31:14 +02:00
selimaj-dev 451ac986c0 Merge pull request 'Release workflow: upload through Gitea's internal address; add manual trigger' (#13) from release-upload-internal-url into master
Reviewed-on: #13
2026-09-25 15:35:17 +00:00
selimaj-devandclaude 80d478d2c9 Upload release jars through Gitea's internal address
The Cloudflare tunnel in front of git.selimaj.dev rejects the jar
uploads with 413 Payload Too Large. Call the API at http://gitea:3000
instead, look the release up by tag, and add a workflow_dispatch trigger
that builds a given tag and attaches the jars to its existing release.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 17:32:52 +02:00
selimaj-dev 79719262a2 Merge pull request 'Release workflow: build every version and attach jars; release 0.1.1-beta' (#12) from release-workflow into master
Release / build (release) Failing after 20m1s
Reviewed-on: #12
2026-09-25 14:39:58 +00:00
selimaj-devandclaude 806c4ab643 Make mod_version the source of truth for releases; bump to 0.1.1-beta
The release workflow no longer overrides mod_version from the tag.
Instead it fails early unless the release tag (optionally prefixed with
"v") matches mod_version in gradle.properties, so released jars always
report the same version as the code the version check sees.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 16:34:03 +02:00
selimaj-devandclaude 6ee8b64a38 Add release workflow that builds every version and attaches the jars
On a published release, build all versions with buildAll using the tag
as the mod version, and upload build/allJars/*.jar to the release.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 16:33:42 +02:00
selimaj-dev a97e5703c9 Merge pull request 'Mojang session auth and version check; release 0.1.0-beta3' (#11) from mojang-session-auth into master
Reviewed-on: #11
2026-09-25 13:29:55 +00:00
selimaj-devandclaude 39f140dae1 Mojang session auth and version check; release 0.1.0-beta3
- Implement SaturnProvider.getModVersion() (Fabric mod metadata) and
  showWarning() (15 s SystemToast) in every version
- Bump mod_version to 0.1.0-beta3, the first version on the new
  Mojang session-server authentication
- Point common at its merge commit for the new auth flow and version check

Fixes #7

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 15:27:27 +02:00
selimaj-devandclaude 632591f8d8 Point common at its master merge commit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 14:49:46 +02:00
selimaj-dev 66e994b5ab Merge pull request 'Reconnect to the Saturn server and time out requests (session-java 0.2.0)' (#10) from reconnect-and-timeouts into master
Reviewed-on: #10
2026-09-25 12:48:27 +00:00
selimaj-devandclaude 1d56094388 Use session-java 0.2.0 from the Gitea registry; reconnect and timeouts
Replace JitPack (which can no longer build session-java since its GitHub
repo is gone) with the Gitea package registry in every build file, move
to dev.selimaj:session-java:0.2.0, and bump common for server reconnect
and request timeouts.

Fixes #4, fixes #5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 14:46:45 +02:00
selimaj-devandclaude 69f572288f Point common at its master merge commit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 14:11:20 +02:00
selimaj-dev 1a5c7f2a2c Merge pull request 'Keep server tab names and team formatting; fix player lookups' (#9) from fix-tablist-and-player-lookups into master
Reviewed-on: #9
2026-09-25 12:01:39 +00:00
selimaj-devandclaude 68734f6ac5 Keep server tab names and team formatting in the tab list
TabListMixin @Overwrote PlayerListEntry.getDisplayName() and always
returned the bare profile name, dropping server-set display names (rank
prefixes, colours) and, by never returning null, vanilla's team
decoration, for every player. Inject at RETURN instead: non-Saturn
players keep the original value, and Saturn players get the icon in
front of the original name, or the team-decorated profile name when the
server set none.

Also bump common for the player-lookup and thread-safety fixes.

Fixes #3. Refs #2, #6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 14:00:39 +02:00
selimaj-dev 333c408d1b Merge pull request 'Bye github!' (#1) from bye-github into master
Reviewed-on: #1
2026-09-24 22:17:53 +00:00
selimaj-dev 3c57328b3c byebye github 2026-09-25 00:13:58 +02:00
selimaj-dev 08178b60fe removed module 2026-09-25 00:12:43 +02:00
selimaj-dev 4342233876 Merge pull request #81 from saturnclientmc/cloak-physics
Cloak physics
2026-04-10 05:27:51 +02:00
selimaj-dev 89cb467016 Improved cloak max x speed 2026-04-10 04:10:57 +02:00
selimaj-dev 9488a8647e Improved cloak physics 1.21.4 2026-04-10 04:07:26 +02:00
selimaj-dev fa1d1f312a Improved cloak physics 1.21.5 2026-04-10 04:07:12 +02:00
selimaj-dev b359224ecd Improved cloak physics 1.21.6 2026-04-10 04:06:33 +02:00
selimaj-dev a909aa880a Improved cloak physics 1.21.7 2026-04-10 04:06:02 +02:00
selimaj-dev 8f8fec0816 Improved cloak physics 1.21.8 2026-04-10 04:04:36 +02:00
selimaj-dev c2d20492c3 Improved cloak physics 1.21.9 2026-04-10 04:02:28 +02:00
selimaj-dev 4a7eade70d Improved cloak physics 1.21.10 2026-04-10 04:02:08 +02:00
selimaj-dev 6466d8d8fd Improved cloak physics 2026-04-10 04:00:58 +02:00
selimaj-dev 7c72da978d Improved physics 2026-04-09 23:06:19 +02:00
selimaj-dev 76b842e6a0 Refactored cloak physics 2026-04-09 22:31:38 +02:00
selimaj-dev 00cefe14f7 Refactoring cloak physics 2026-04-09 22:03:28 +02:00
Kr4ight 2a3034be2c Fix inputs #80 2026-04-09 01:28:04 +02:00
selimaj-dev 910633819a Merge pull request #79 from saturnclientmc/nametags-patch
Nametags patch
2026-04-08 20:45:27 +02:00
selimaj-dev daddd0a005 1.21.8 implementation 2026-04-08 20:42:29 +02:00
selimaj-dev cdd6ae8ec9 1.21.9 nametags 2026-04-08 20:40:47 +02:00
selimaj-dev 65c9454370 Re-added old nametag to unsupported versions 2026-04-08 20:39:03 +02:00
selimaj-dev 8a1e9b49bc Added 1.21.8-9 nametag 2026-04-08 20:34:21 +02:00
selimaj-dev f288f9148c Removed nametag mixins 2026-04-08 20:27:59 +02:00
selimaj-dev e165f6b65c Removed nametag from all mixin files 2026-04-08 20:25:44 +02:00
selimaj-dev 019730c756 Nametags 1.21.10 and color fix 2026-04-08 20:23:58 +02:00
selimaj-dev 7fa951b7fa Merge pull request #77 from saturnclientmc/more-versions
More versions
2026-04-08 20:07:16 +02:00
selimaj-dev b39b19f107 Fixed Cloak Renderer 2026-04-08 20:04:00 +02:00
selimaj-dev 88db7a8383 End shader 2026-04-08 19:58:10 +02:00
selimaj-dev d782bccb5f Hat renderer 2026-04-08 19:50:44 +02:00
selimaj-dev 84947d6b33 Working Player tracking 2026-04-08 19:18:31 +02:00
selimaj-dev f79849d9fb Improving render state tracking 2026-04-08 19:08:24 +02:00
selimaj-dev cb38639e11 1.21.11: Small stuff 2026-04-08 10:08:19 +02:00
selimaj-dev 8f567c83c6 1.21.11: Moved mixin to direct 2026-04-08 09:52:14 +02:00
selimaj-dev 8876abde1c 1.21.11: Fix Camera Mixin 2026-04-08 09:41:20 +02:00
selimaj-dev 2d95bf9043 1.21.11: Render Scope 2026-04-08 09:35:43 +02:00
selimaj-dev 0475d8c43d 1.21.11: Fix text stuff 2026-04-08 09:30:39 +02:00
selimaj-dev 491199cfea 1.21.11: Fix Splash overlay and shader utils 2026-04-08 09:29:15 +02:00
selimaj-dev 5aa40d2e61 1.21.11: Fix CloakFeatureRenderer and gradle.properties 2026-04-08 09:21:21 +02:00
selimaj-dev c5b6a858e1 1.21.11: Fix RenderScope 2026-04-08 09:18:08 +02:00
selimaj-dev e28051a109 1.21.11: Tmp fix WorldFeature 2026-04-08 09:17:01 +02:00
selimaj-dev 1471e53a91 1.21.11: Tmp fix ShaderUtils 2026-04-08 09:15:49 +02:00
selimaj-dev 6ae4f980e4 1.21.11: Fix EntityDrawer 2026-04-08 09:11:07 +02:00
selimaj-dev 6fbacbcae6 1.21.11: Fix temp resize on screen 2026-04-08 09:10:38 +02:00
selimaj-dev e934f24d3e 1.21.11: Fix splash overlay 2026-04-08 09:07:38 +02:00
selimaj-dev 191bc1318e Added 1.21.11 (needs fix) 2026-04-08 09:04:06 +02:00
selimaj-dev 249219fe57 Added 1.21.10 2026-04-08 08:58:56 +02:00
selimaj-dev 5a86d4cb4a 1.21.9: Fix crash 2026-04-08 08:46:52 +02:00
selimaj-dev 75a5d7f52a 1.21.9: Fix cloaks 2026-04-08 08:40:33 +02:00
selimaj-dev c078637d3d 1.21.9: Fix status effect 2026-04-08 08:27:24 +02:00
selimaj-dev 6c41e774ff 1.21.9: Fix entity drawer 2026-04-08 07:58:56 +02:00
selimaj-dev c630a4e41a 1.21.9: Removed nametags implementation 2026-04-08 07:50:18 +02:00
selimaj-dev 33956ea550 Fix: text 2026-04-08 07:16:59 +02:00
selimaj-dev 2e45cc39f7 Merge pull request #75 from saturnclientmc/more-versions
More versions
2026-04-07 21:57:20 +02:00
Kr4ight c8860ab837 Running 1.21.9 #72 2026-04-07 21:53:48 +02:00
Kr4ight 64ab091807 Fix 3 2026-04-07 21:47:14 +02:00
Kr4ight 243275b414 Fix 2 2026-04-07 21:32:22 +02:00
Kr4ight 70d7953b45 Fix 1 2026-04-07 21:27:56 +02:00
Kr4ight 812e7bf593 Added 1.21.9 but with errors #72 2026-04-07 21:08:34 +02:00
Kr4ight e5c2d8bd36 Added 1.21.8 #72 2026-04-07 20:59:31 +02:00
Kr4ight 670961acce Added 1.21.7 #72 2026-04-07 20:41:59 +02:00
selimaj-dev b46410c43b Merge pull request #73 from saturnclientmc/fix-auth-timeout
Fix auth timeout
2026-04-07 20:17:52 +02:00
Kr4ight 2cd7db9ade Fix auth timeout 2026-04-07 20:17:15 +02:00
201 changed files with 3000 additions and 8877 deletions
+164
View File
@@ -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)));
+33
View File
@@ -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
+36
View File
@@ -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
+101
View File
@@ -0,0 +1,101 @@
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 Minecraft version (two at a time, to stay within the
# Gradle heap) 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
View File
@@ -1,3 +1,3 @@
[submodule "common"]
path = common
url = https://github.com/saturnclientmc/common.git
url = https://git.selimaj.dev/saturnclientmc/common.git
+38 -22
View File
@@ -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
./gradlew :1.21.11:runClient # launch a development client
```
Building needs Java 21.
## Contributing
We welcome contributions of all kinds:
@@ -43,27 +63,23 @@ 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 share one copy of the code with Stonecutter, writing version-specific code, 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`
- **Panorama:**
- **Panorama:**
https://www.planetminecraft.com/texture-pack/minecraft-cavesand-cliffs-part-ii-main-menu-panorama/
If any code or assets belong to you and you would like them removed, please open an issue.
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
+66 -1
View File
@@ -1,3 +1,68 @@
# 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
Every supported Minecraft version (1.21.4 to 1.21.11) builds from one copy of the version-specific code with [Stonecutter](https://stonecutter.kikugie.dev/):
- `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), one copy for every version. Where Minecraft's API differs between versions, the code goes behind version comments (see below).
- `versions/<mc>/`: each version's `gradle.properties` (Minecraft, Yarn, Fabric Loader and Fabric API versions), plus its `build/` and `run/` folders. Each version is the Gradle project `:<mc>`.
`settings.gradle.kts` lists the versions in `stonecutterVersions`. The root `build.gradle.kts` builds each version, and `stonecutter.gradle.kts` configures the root project (shared repositories, the `common` wiring and `buildAll`). Loom's version 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. The panorama in `SaturnScreenFabric.render` is an example. Search for `TODO` to find what newer versions still lack (tracked in #30), and remove the condition once a version gap is fixed.
A whole class that only exists for some versions (such as `MatrixStackMixin`, which only makes sense before 1.21.6's GUI rewrite) can keep an empty class body on the other versions. Before splitting a class like that, look for a hook that works on every version: nametags used to hook label rendering, which 1.21.9 changed, and now replace the name while the render state is built (`LivingEntityRendererMixin`), with no version condition at all. 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.
## Checking a change on every version
Since every version shares `src/`, a change applies to all of them at once. Code that uses the Minecraft API may still only compile on some, so build them all:
```sh
./gradlew compileJava
```
The **Check** workflow runs the same compile on every pull request, so a version a change doesn't compile on fails before merging. Mixins whose target changed fail at startup rather than at compile time, so launch the versions a change touches with `./gradlew :<mc>:runClient`.
For bigger changes, `./gradlew buildAll` builds every version's jar into `build/allJars/`.
## Adding a Minecraft version
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`.
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.
-64
View File
@@ -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"
}
}
+81
View File
@@ -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...ef0ead6bf8
+9 -1
View File
@@ -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.2-beta
# Saturn's emote library (git.selimaj.dev/saturnclientmc/emotes)
emotes_version=0.1.0
# Loom, for every Minecraft version (read in settings.gradle.kts).
loom_version=1.17.21
Binary file not shown.
+1 -1
View File
@@ -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
Vendored
+3 -3
View File
@@ -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
View File
@@ -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%
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

-21
View File
@@ -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
}
+33
View File
@@ -0,0 +1,33 @@
pluginManagement {
repositories {
maven("https://maven.fabricmc.net/")
maven("https://maven.kikugie.dev/releases")
gradlePluginPortal()
mavenCentral()
}
// Loom's version for build.gradle.kts, which can't read properties in its plugins block.
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.4", "1.21.5", "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"
}
}
@@ -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();
}
}
@@ -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;
@@ -30,11 +41,14 @@ public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState,
matrices.push();
//? if >=1.21.5 {
ModelPart head = this.getContextModel().head;
matrices.multiply(new Quaternionf().rotationZYX(head.roll, head.yaw, head.pitch));
//?} else
//this.getContextModel().head.rotate(matrices);
ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
matrices, vertexConsumers, light,
matrices, output, light,
OverlayTexture.DEFAULT_UV);
matrices.pop();
@@ -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();
}
@@ -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("#")) {
texture = IdentifierRef.of(mapKd);
}
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;
for (int f = 0; f < faces; f++) {
ObjFace face = groupObj.getFace(f);
int verts = face.getNumVertices();
//? 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);
}
}
// Your original winding order logic, untouched
for (int i = verts - 1; i >= 0; i--) {
writeVertex(groupObj, face, i, consumer, entry, light, overlay, fr, fg, fb);
}
private static void 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 = obj.getFace(f);
int verts = face.getNumVertices();
for (int i = verts - 1; i >= 0; i--) {
writeVertex(obj, face, i, consumer, entry, light, overlay, r, g, b);
}
}
}
@@ -43,9 +43,12 @@ public class IdentifierUtils {
}
SaturnClient.client.execute(() -> {
//? if >=1.21.5 {
NativeImageBackedTexture texture = new NativeImageBackedTexture(
() -> identifier.toString(),
nativeImage);
//?} else
//NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
SaturnClient.client.getTextureManager().registerTexture(identifier, texture);
});
@@ -0,0 +1,93 @@
package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.cosmetics.utils.GifUtils;
//? if >=1.21.5
import com.mojang.blaze3d.pipeline.RenderPipeline;
//? if >=1.21.5
import net.minecraft.client.gl.RenderPipelines;
import net.minecraft.client.render.RenderLayer;
//? if <1.21.5 {
/*import net.minecraft.client.render.RenderPhase;
import net.minecraft.client.render.VertexFormat.DrawMode;
import net.minecraft.client.render.VertexFormats;
*///?}
//? 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 {
// Render pipelines exist from 1.21.5; before that the layer names a shader program directly.
//? if >=1.21.5 {
public static final RenderPipeline END_GATEWAY_PIPELINE = RenderPipelines.register(
RenderPipeline.builder(new RenderPipeline.Snippet[] {
RenderPipelines.RENDERTYPE_END_PORTAL_SNIPPET })
.withLocation("pipeline/end_gateway")
.withShaderDefine("PORTAL_LAYERS", 16)
.withCull(false)
.build());
//?}
//? 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 if >=1.21.6 {
/*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));
*///?} else if >=1.21.5 {
/*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, false)
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false, false).build())
.build(false));
*///?} else {
/*public static final RenderLayer END_GATEWAY = RenderLayer.of(
"end_gateway",
VertexFormats.POSITION,
DrawMode.QUADS,
1536,
false,
false,
RenderLayer.MultiPhaseParameters.builder()
.program(RenderLayer.END_GATEWAY_PROGRAM)
.texture(Textures.create()
.add(EndPortalBlockEntityRenderer.SKY_TEXTURE, false, false)
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false, false)
.build())
.cull(RenderPhase.DISABLE_CULLING)
.build(false));
*///?}
public static RenderLayer getRenderLayer(IdentifierRef texture) {
String path = texture.toString();
if (path.endsWith(".gif")) {
return entityAlpha((Identifier) (Object) GifUtils.get(texture));
} else if (path.endsWith("end.png")) {
return END_GATEWAY;
} else {
return entityAlpha((Identifier) (Object) texture);
}
}
private static RenderLayer entityAlpha(Identifier texture) {
//? if >=1.21.11 {
return RenderLayers.entityAlpha(texture);
//?} else
//return RenderLayer.getEntityAlpha(texture);
}
}
@@ -12,10 +12,9 @@ import net.minecraft.entity.LivingEntity;
* Fabric implementation of {@link EntityFeature}.
*
* Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}.
* Entity state for nametag rendering is supplied at render time by
* {@link org.saturnclient.modules.mixins.LivingEntityRendererMixin} which
* writes into the render state; {@link NametagsMixin} then constructs
* a {@link RenderStateEntityState} from the already-extracted snapshot.
* Entity state for nametags is built by LivingEntityRendererMixin, which
* writes health into the render state and then constructs a
* {@link RenderStateEntityState} from that snapshot for NametagsMod.
*/
public class EntityFeatureImpl implements EntityFeature {
@@ -43,7 +42,7 @@ public class EntityFeatureImpl implements EntityFeature {
* Minecraft render-state object) as a platform-neutral
* {@link EntityState} for use by {@link NametagsFeature}.
*
* This is constructed inside {@link NametagsMixin} where both the
* This is constructed in LivingEntityRendererMixin, where both the
* render-state mixin interface and the entity name are available.
*/
public static final class RenderStateEntityState implements EntityState {
@@ -7,8 +7,8 @@ import org.saturnclient.common.feature.EntityFeature;
* {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}.
*
* Carries the entity's health snapshot and classification into the render
* pipeline so that {@link NametagsMixin} can read them without touching
* the live entity again.
* render state, so NametagsMod's replacement nametag (applied in
* LivingEntityRendererMixin) can use them without touching the live entity again.
*/
public interface HealthRenderState {
@@ -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;
@@ -1,13 +1,19 @@
package org.saturnclient.impl.features.mixins.render;
//? if >=1.21.6 {
import net.minecraft.client.render.fog.FogRenderer;
//?} else
//import net.minecraft.client.render.BackgroundRenderer;
import org.saturnclient.mod.mods.NoFogMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
//? if >=1.21.6 {
@Mixin(FogRenderer.class)
//?} else
//@Mixin(BackgroundRenderer.class)
public class BackgroundRendererMixin {
// fog
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
@@ -1,12 +1,20 @@
package org.saturnclient.impl.features.mixins.render;
//? if >=1.21.6
import net.minecraft.client.MinecraftClient;
//? if >=1.21.6
import net.minecraft.client.option.GameOptions;
import net.minecraft.client.render.GameRenderer;
//? if >=1.21.6
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.mod.mods.FreelookMod;
import org.saturnclient.mod.mods.ZoomMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
//? if >=1.21.6
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
@@ -38,4 +46,18 @@ public class GameRendererMixin {
cir.setReturnValue(zoomedFov);
}
}
//? if >=1.21.6 {
// From 1.21.6 the menu blur's strength is the vanilla setting, read here every frame. While a
// Saturn screen is open, it's Saturn's background blur instead, faded in with the screen.
@Redirect(method = "render", at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/option/GameOptions;getMenuBackgroundBlurrinessValue()I"))
private int useSaturnBlur(GameOptions options) {
if (MinecraftClient.getInstance().currentScreen instanceof SaturnScreenFabric saturnScreen) {
return saturnScreen.screen.getBackgroundBlur();
}
return options.getMenuBackgroundBlurrinessValue();
}
//?}
}
@@ -4,7 +4,6 @@ import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.impl.features.mixins.misc.NametagsMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@@ -19,8 +18,8 @@ import org.spongepowered.asm.mixin.Unique;
*
* The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read by
* {@link NametagsMixin} at render time.
* render state is extracted from a live entity, and read there right away for
* NametagsMod's replacement nametag.
*/
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState {
@@ -6,24 +6,25 @@ import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.passive.PassiveEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.text.Text;
import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.mod.mods.NametagsMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Captures each living entity's health and type into the render state
* during {@code updateRenderState}, so that nametag rendering can read
* them without touching the live entity again.
* Captures each living entity's health and type into the render state during
* {@code updateRenderState}, and applies {@link NametagsMod}'s replacement nametag to the state's
* display name.
*
* Previously imported {@code NametagsInterface.EntityType}; now uses
* {@link EntityFeature.EntityType} — the canonical enum shared across
* the whole module system.
*
* The injection target and strategy are unchanged from the original.
* Replacing the name here, while the render state is built, works the same on every version. Hooking
* label rendering instead doesn't: 1.21.9 moved it to the render command queue. Player role icons are
* added after this, in PlayerEntityRendererMixin.
*/
@Mixin(LivingEntityRenderer.class)
public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> {
@@ -47,5 +48,15 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
type = EntityFeature.EntityType.OTHER;
hrs.saturn$setEntityType(type);
// Only entities that already show a label get a replacement one, as when this hooked label
// rendering.
if (NametagsMod.shouldReplaceName() && state.displayName != null) {
String replacement = NametagsMod.getNametagString(
new EntityFeatureImpl.RenderStateEntityState(state.displayName.getString(), hrs));
if (replacement != null) {
state.displayName = Text.literal(replacement);
}
}
}
}
@@ -169,22 +169,34 @@ public class PlayerFeatureImpl implements PlayerFeature {
@Override
public ItemStackRef getHelmet() {
//? if >=1.21.5 {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.HEAD) : ItemStack.EMPTY);
//?} else
//return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getChestplate() {
//? if >=1.21.5 {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.CHEST) : ItemStack.EMPTY);
//?} else
//return wrap(hasPlayer() ? player().getInventory().armor.get(2) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getLeggings() {
//? if >=1.21.5 {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.LEGS) : ItemStack.EMPTY);
//?} else
//return wrap(hasPlayer() ? player().getInventory().armor.get(1) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getBoots() {
//? if >=1.21.5 {
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.FEET) : ItemStack.EMPTY);
//?} else
//return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY);
}
// ---------------------------------------------------------------
@@ -31,6 +31,7 @@ public class WorldFeatureImpl implements WorldFeature {
*/
@Override
public long getWorldAge() {
return hasWorld() ? mc.world.getTime() : 0L;
// World.getTime() was a shortcut for this, and 1.21.11 removed it.
return hasWorld() ? mc.world.getLevelProperties().getTime() : 0L;
}
}
@@ -1,14 +1,25 @@
package org.saturnclient.impl.mixins;
import org.joml.Quaternionfc;
import org.saturnclient.common.ref.render.MatrixStackRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.client.util.math.MatrixStack;
import org.spongepowered.asm.mixin.Mixin;
//? if <1.21.6 {
/*import org.saturnclient.common.ref.render.MatrixStackRef;
//? if >=1.21.5
import org.joml.Quaternionfc;
//? if <1.21.5
//import org.joml.Quaternionf;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.util.math.RotationAxis;
*///?}
/**
* Makes MatrixStack a MatrixStackRef, for GUI drawing before 1.21.6. From 1.21.6 the GUI uses a 2D
* Matrix3x2fStack (Matrix3x2fStackRef) instead, so this mixin is empty but stays registered, keeping
* impl.mixins.json the same for every version.
*/
@Mixin(MatrixStack.class)
public abstract class MatrixStackMixin implements MatrixStackRef {
//? if <1.21.6 {
/*public abstract class MatrixStackMixin implements MatrixStackRef {
@Shadow
public abstract void push();
@@ -22,10 +33,16 @@ public abstract class MatrixStackMixin implements MatrixStackRef {
public abstract void scale(float x, float y, float z);
@Shadow
//? if >=1.21.5
public abstract void multiply(Quaternionfc quaternion);
//? if <1.21.5
//public abstract void multiply(Quaternionf quaternion);
@Shadow
//? if >=1.21.5
public abstract void multiply(Quaternionfc quaternion, float originX, float originY, float originZ);
//? if <1.21.5
//public abstract void multiply(Quaternionf quaternion, float originX, float originY, float originZ);
@Override
public void translate(float x, float y) {
@@ -46,4 +63,6 @@ public abstract class MatrixStackMixin implements MatrixStackRef {
public void rotate(float angle) {
multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(angle));
}
}
}
*///?} else
public abstract class MatrixStackMixin {}
@@ -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();
}
}
@@ -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,9 +31,25 @@ 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 if >=1.21.6 {
//return (SpriteRef) SaturnClient.client.getGuiAtlasManager().getSprite((InGameHud.getEffectTexture(getEffectType())));
//?} else
//return (SpriteRef) SaturnClient.client.getStatusEffectSpriteManager().getSprite(getEffectType());
}
// Before 1.21.6 there's no InGameHud.getEffectTexture; getIcon's sprite is used there instead.
//? if >=1.21.6 {
@Override
public IdentifierRef getIconId() {
Identifier id = InGameHud.getEffectTexture(getEffectType());
return IdentifierRef.of(id.getNamespace(), "textures/" + id.getPath() + ".png");
}
//?}
@Override
public String getInfiniteText() {
return "∞";
@@ -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
@@ -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,97 @@
package org.saturnclient.impl.provider;
import java.awt.image.BufferedImage;
import java.util.UUID;
import java.util.function.Supplier;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.toast.SystemToast;
//? if >=1.21.9 {
import net.minecraft.entity.player.SkinTextures;
//?} else
//import net.minecraft.client.util.SkinTextures;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
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 {
/** Looks up the signed-in player's skin; made once, since the skin provider caches it */
private Supplier<Identifier> playerSkin;
@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 IdentifierRef getPlayerSkin() {
if (playerSkin == null) {
MinecraftClient client = MinecraftClient.getInstance();
// 1.21.9 moved SkinTextures and renamed the skin provider's lookup.
//? if >=1.21.9 {
Supplier<SkinTextures> textures = client.getSkinProvider().supplySkinTextures(client.getGameProfile(), false);
playerSkin = () -> textures.get().body().texturePath();
//?} else {
/*Supplier<SkinTextures> textures = client.getSkinProvider().getSkinTexturesSupplier(client.getGameProfile());
playerSkin = () -> textures.get().texture();
*///?}
}
return (IdentifierRef) (Object) playerSkin.get();
}
@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);
}
}
@@ -0,0 +1,175 @@
package org.saturnclient.impl.ui;
// Entities in the UI are drawn through the GUI render state from 1.21.6, and straight into vertex
// consumers before. This file has one implementation for each side.
//? if >=1.21.6 {
import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.components.SkinPreview.EntityDrawer;
//? 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;
public class EntityDrawerImpl implements EntityDrawer {
@Override
public boolean render(RenderScope renderScope, int width, int height, float yaw) {
LivingEntity entity = SaturnClient.client.player;
if (entity == null) {
return false;
}
// The player is about 1.8 blocks tall; leave some room above and below.
drawEntity(renderScope, 0, 0, width, height, (int) (height / 2.2F), 0.0625F, yaw, entity);
return true;
}
// Draws the entity in the box, turned by yaw degrees (0 faces the viewer) with its head straight.
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float scale,
float yaw, LivingEntity entity) {
renderScope.enableScissor(x1, y1, x2, y2);
Quaternionf quaternionf = (new Quaternionf()).rotateZ((float) Math.PI);
Quaternionf quaternionf2 = new Quaternionf();
float j = entity.bodyYaw;
float k = entity.getYaw();
float l = entity.getPitch();
float m = entity.lastHeadYaw;
float n = entity.headYaw;
entity.bodyYaw = 180.0F + yaw;
entity.setYaw(180.0F + yaw);
entity.setPitch(0.0F);
entity.headYaw = entity.getYaw();
entity.lastHeadYaw = entity.getYaw();
float o = entity.getScale();
Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + scale * o, 0.0F);
float p = (float) size / o;
drawEntity(renderScope, x1, y1, x2, y2, p, vector3f, quaternionf, quaternionf2, entity);
entity.bodyYaw = j;
entity.setYaw(k);
entity.setPitch(l);
entity.lastHeadYaw = m;
entity.headYaw = n;
renderScope.disableScissor();
}
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, float scale,
Vector3f translation, Quaternionf rotation, @Nullable Quaternionf overrideCameraAngle,
LivingEntity entity) {
//? 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);
//? if <1.21.11
//entityRenderState.hitbox = null;
if (renderScope instanceof RenderScopeImpl i)
i.addEntity(entityRenderState, scale, translation, rotation, overrideCameraAngle, x1, y1, x2, y2);
}
}
//?} else {
/*import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.components.SkinPreview.EntityDrawer;
import net.minecraft.client.render.DiffuseLighting;
import net.minecraft.client.render.entity.EntityRenderDispatcher;
import net.minecraft.entity.LivingEntity;
public class EntityDrawerImpl implements EntityDrawer {
// Draws the entity in the box, turned by yaw degrees (0 faces the viewer) with its head straight.
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float f,
float yaw, LivingEntity entity) {
float g = (float) (x1 + x2) / 2.0F;
float h = (float) (y1 + y2) / 2.0F;
renderScope.enableScissor(x1, y1, x2, y2);
Quaternionf quaternionf = (new Quaternionf()).rotateZ(3.1415927F);
float k = entity.bodyYaw;
float l = entity.getYaw();
float m = entity.getPitch();
//? if >=1.21.5
float n = entity.lastHeadYaw;
//? if <1.21.5
//float n = entity.prevHeadYaw;
float o = entity.headYaw;
entity.setYaw(180.0F + yaw);
entity.bodyYaw = 180.0F + yaw;
entity.setPitch(0.0F);
entity.headYaw = entity.getYaw();
//? if >=1.21.5
entity.lastHeadYaw = entity.getHeadYaw();
//? if <1.21.5
//entity.prevHeadYaw = entity.getHeadYaw();
entity.setCustomNameVisible(false);
float p = entity.getScale();
Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + f * p, 0.0F);
float q = (float) size / p;
drawEntity(renderScope, g, h, q, vector3f, quaternionf, null, entity);
entity.setCustomNameVisible(true);
entity.bodyYaw = k;
entity.setYaw(l);
entity.setPitch(m);
//? if >=1.21.5
entity.lastHeadYaw = n;
//? if <1.21.5
//entity.prevHeadYaw = n;
entity.headYaw = o;
renderScope.disableScissor();
}
public static void drawEntity(RenderScope renderScope, float x, float y, float size, Vector3f vector3f,
Quaternionf quaternionf, @Nullable Quaternionf quaternionf2, LivingEntity entity) {
if (renderScope instanceof RenderScopeImpl scopeImpl) {
scopeImpl.matrices.push();
scopeImpl.matrices.translate((double) x, (double) y, 0);
scopeImpl.matrices.scale(size, size, -size);
scopeImpl.matrices.translate(vector3f.x, vector3f.y, vector3f.z);
scopeImpl.matrices.multiply(quaternionf);
scopeImpl.draw();
//? if >=1.21.5
DiffuseLighting.disableGuiDepthLighting();
//? if <1.21.5
//DiffuseLighting.method_34742();
EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
entityRenderDispatcher.setRenderShadows(false);
scopeImpl.draw((vertexConsumers) -> {
entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 1.0F,
scopeImpl.matrices, vertexConsumers, 15728880);
});
scopeImpl.draw();
entityRenderDispatcher.setRenderShadows(true);
scopeImpl.getMatrixStack().pop();
DiffuseLighting.enableGuiDepthLighting();
}
}
@Override
public boolean render(RenderScope renderScope, int width, int height, float yaw) {
LivingEntity entity = SaturnClient.client.player;
if (entity == null) {
return false;
}
// The player is about 1.8 blocks tall; leave some room above and below.
drawEntity(renderScope, 0, 0, width, height, (int) (height / 2.2F), 0.0625F, yaw, entity);
return true;
}
}
*///?}
@@ -0,0 +1,786 @@
package org.saturnclient.impl.ui;
// 1.21.6 rewrote GUI rendering: render states and a 2D matrix stack (Matrix3x2fStack) instead of
// drawing into vertex consumers with a MatrixStack. This file has one implementation for each side.
//? if >=1.21.6 {
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3x2f;
import org.joml.Matrix3x2fStack;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import com.mojang.blaze3d.textures.GpuTextureView;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.common.ref.render.MatrixStackRef;
import org.saturnclient.impl.ref.Matrix3x2fStackRef;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.SvgTexture;
import net.minecraft.client.gl.RenderPipelines;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.ScreenRect;
import net.minecraft.client.gui.render.state.ColoredQuadGuiElementRenderState;
import net.minecraft.client.gui.render.state.GuiRenderState;
import net.minecraft.client.gui.render.state.ItemGuiElementRenderState;
import net.minecraft.client.gui.render.state.TextGuiElementRenderState;
import net.minecraft.client.gui.render.state.TexturedQuadGuiElementRenderState;
import net.minecraft.client.gui.render.state.special.EntityGuiElementRenderState;
import net.minecraft.client.render.entity.state.EntityRenderState;
//? 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;
import net.minecraft.item.ItemDisplayContext;
import net.minecraft.item.ItemStack;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.crash.CrashException;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashReportSection;
import net.minecraft.world.World;
public class RenderScopeImpl implements RenderScope {
public final Matrix3x2fStackRef matrices;
public final GuiRenderState state;
private ScissorStack scissorStack;
private int opacity = 0xFF << 24;
public RenderScopeImpl(DrawContext context) {
this(context.getMatrices(), ((DrawContextAccessor) (Object) context).getState());
}
public RenderScopeImpl(Matrix3x2fStack matrices, GuiRenderState state) {
this.matrices = new Matrix3x2fStackRef(matrices);
this.state = state;
this.scissorStack = new ScissorStack();
}
@Override
public MatrixStackRef getMatrixStack() {
return matrices;
}
@Override
public void setOpacity(float alpha) {
this.opacity = ((int) (alpha * 255)) << 24;
}
@Override
public int getColor(int color) {
int originalAlpha = (color >>> 24) & 0xFF;
int newAlpha = (opacity >>> 24) & 0xFF;
int mixedAlpha = (originalAlpha * newAlpha) / 255;
return (mixedAlpha << 24) | (color & 0x00FFFFFF);
}
@Override
public void fill(int x1, int y1, int x2, int y2, int color) {
this.state.addSimpleElement(
new ColoredQuadGuiElementRenderState(
RenderPipelines.GUI,
TextureSetup.empty(),
new Matrix3x2f(this.matrices.stack),
x1, y1, x2, y2, color, color,
this.scissorStack.peekLast()));
}
@Override
public void drawRectangle(int x, int y, int width, int height, int color) {
if (color == 0)
return;
color = getColor(color);
int x1 = x;
int x2 = x + width;
int y1 = y;
int y2 = y + height;
if (x2 < x1) {
int tmp = x1;
x1 = x2;
x2 = tmp;
}
if (y2 < y1) {
int tmp = y1;
y1 = y2;
y2 = tmp;
}
this.fill(x1, y1, x2, y2, color);
}
@Override
public void drawText(String text, int x, int y, int font, int color) {
drawText(1.0f, text, x, y, font, color);
}
@Override
public void drawText(float scale, String text, int x, int y, int font, int color) {
if (color == 0)
return;
if (font == 0)
scale *= 2;
int lineIndex = 0;
for (String line : text.split("\n")) {
int finalColor = getColor(color);
matrices.push();
matrices.translate(x, y + (lineIndex * Fonts.getHeight()));
matrices.scale(scale, scale);
this.state.addText(
new TextGuiElementRenderState(
SaturnClient.client.textRenderer,
((Text) Fonts.setFont(line, font)).asOrderedText(),
new Matrix3x2f(this.matrices.stack),
0, font == 0 ? 1 : 8,
//? if >=1.21.11 {
finalColor, 0, false, false, this.scissorStack.peekLast()));
//?} else
//finalColor, 0, false, this.scissorStack.peekLast()));
matrices.pop();
lineIndex++;
}
}
@Override
public int getScaledWindowWidth() {
return SaturnClient.client.getWindow().getScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getScaledHeight();
}
// Texture rendering
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height) {
drawTexture(sprite, x, y, u, v, width, height, -1);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, int color) {
drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
int regionWidth, int regionHeight, int textureWidth, int textureHeight) {
drawTexture(sprite, x, y, u, v, width, height, regionWidth, regionHeight, textureWidth, textureHeight, -1);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
int regionWidth, int regionHeight, int textureWidth, int textureHeight, int color) {
drawTexturedQuad((Identifier) (Object) sprite, x, x + width, y, y + height,
(u + 0f) / textureWidth,
(u + regionWidth) / textureWidth,
(v + 0f) / textureHeight,
(v + regionHeight) / textureHeight,
color);
}
private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2,
float u1, float u2, float v1, float v2, int color) {
if (color == 0)
return;
color = getColor(color);
if (sprite.getPath().endsWith(".svg")) {
sprite = (Identifier) (Object) SvgTexture.getSvg(
(IdentifierRef) (Object) sprite,
(x2 - x1) * 2, (y2 - y1) * 2);
}
GpuTextureView gpuTextureView = SaturnClient.client.getTextureManager()
.getTexture(sprite).getGlTextureView();
//? if >=1.21.11
AbstractTexture abstractTexture = SaturnClient.client.getTextureManager().getTexture(sprite);
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState(
RenderPipelines.GUI_TEXTURED,
//? 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()));
}
// Scissor management
@Override
public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRect rect = new ScreenRect(x1, y1, x2 - x1, y2 - y1).transform(this.matrices.stack);
scissorStack.push(rect);
}
@Override
public void disableScissor() {
scissorStack.pop();
}
@Override
public boolean scissorContains(int x, int y) {
return scissorStack.contains(x, y);
}
public static class ScissorStack {
private final Deque<ScreenRect> stack = new ArrayDeque<>();
public ScreenRect push(ScreenRect rect) {
ScreenRect last = stack.peekLast();
if (last != null) {
ScreenRect intersection = Objects.requireNonNullElse(rect.intersection(last), ScreenRect.empty());
stack.addLast(intersection);
return intersection;
} else {
stack.addLast(rect);
return rect;
}
}
@Nullable
public ScreenRect pop() {
if (stack.isEmpty())
throw new IllegalStateException("Scissor stack underflow");
stack.removeLast();
return stack.peekLast();
}
@Nullable
public ScreenRect peekLast() {
return stack.peekLast();
}
public boolean contains(int x, int y) {
return stack.isEmpty() || stack.peekLast().contains(x, y);
}
}
// Item rendering
@Override
public void drawItem(ItemStackRef stack, int x, int y) {
drawItem(null, SaturnClient.client.world, stack, x, y, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y,
int seed) {
drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
int z) {
if (!stack.isEmpty()) {
//? 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), keyedItemRenderState, x, y,
this.scissorStack.peekLast()));
} catch (Throwable throwable) {
CrashReport crashReport = CrashReport.create(throwable, "Rendering item");
CrashReportSection crashReportSection = crashReport.addElement("Item being rendered");
crashReportSection.add("Item Type", () -> String.valueOf(stack.getItem()));
crashReportSection.add("Item Components", () -> String.valueOf(stack.getComponents()));
crashReportSection.add("Item Foil", () -> String.valueOf(stack.hasGlint()));
throw new CrashException(crashReport);
}
}
}
@Override
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height) {
drawSpriteStretched(sprite, x, y, width, height, -1);
}
@Override
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height, int color) {
Sprite mcSprite = (Sprite) sprite;
if (color == 0 || width == 0 || height == 0)
return;
drawTexturedQuad(mcSprite.getAtlasId(), x, x + width, y, y + height,
mcSprite.getMinU(), mcSprite.getMaxU(), mcSprite.getMinV(), mcSprite.getMaxV(), color);
}
@Override
public void draw() {
// No-op for now
}
public void drawBorder(int x, int y, int width, int height, int color) {
fill(x, y, x + width, y + 1, color);
fill(x, y + height - 1, x + width, y + height, color);
fill(x, y + 1, x + 1, y + height - 1, color);
fill(x + width - 1, y + 1, x + width, y + height - 1, color);
}
public void addEntity(EntityRenderState entityState, float scale, Vector3f translation, Quaternionf rotation,
@Nullable Quaternionf overrideCameraAngle, int x1, int y1, int x2, int y2) {
Matrix3x2f mat = this.matrices.stack;
float offsetX = mat.m20;
float offsetY = mat.m21;
float scaleX = mat.m00; // current matrix X scale
float scaleY = mat.m11; // current matrix Y scale
int adjustedX1 = (int) (x1 * scaleX + offsetX);
int adjustedY1 = (int) (y1 * scaleY + offsetY);
int adjustedX2 = (int) (x2 * scaleX + offsetX);
int adjustedY2 = (int) (y2 * scaleY + offsetY);
float adjustedScale = scale * scaleX; // apply matrix scale to entity scale too
this.state.addSpecialElement(new EntityGuiElementRenderState(entityState, translation, rotation,
overrideCameraAngle, adjustedX1, adjustedY1, adjustedX2, adjustedY2, adjustedScale,
this.scissorStack.peekLast()));
}
}
//?} else {
/*import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import com.mojang.blaze3d.systems.RenderSystem;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.common.ref.render.MatrixStackRef;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.SvgTexture;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.font.TextRenderer.TextLayerType;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.ScreenRect;
import net.minecraft.client.render.DiffuseLighting;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.item.ItemRenderState;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.Window;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.LivingEntity;
//? if >=1.21.5
import net.minecraft.item.ItemDisplayContext;
//? if <1.21.5
//import net.minecraft.item.ModelTransformationMode;
import net.minecraft.item.ItemStack;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.crash.CrashException;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashReportSection;
import net.minecraft.world.World;
public class RenderScopeImpl implements RenderScope {
// ----------------------------
// Fields
// ----------------------------
public MatrixStack matrices;
public VertexConsumerProvider.Immediate vertexConsumers;
private ScissorStack scissorStack = new ScissorStack();
private int opacity = 255 << 24;
private final ItemRenderState itemRenderState;
// ----------------------------
// Constructors
// ----------------------------
public RenderScopeImpl(DrawContext context) {
this(context.getMatrices(), ((DrawContextAccessor) context).getVertexConsumers());
}
public RenderScopeImpl(MatrixStack matrices, VertexConsumerProvider.Immediate vertexConsumers) {
this.matrices = matrices;
this.vertexConsumers = vertexConsumers;
this.scissorStack = new ScissorStack();
this.itemRenderState = new ItemRenderState();
}
// ----------------------------
// Matrix & Transform
// ----------------------------
@Override
public MatrixStackRef getMatrixStack() {
return (MatrixStackRef) matrices;
}
// ----------------------------
// Color & Opacity
// ----------------------------
@Override
public void setOpacity(float alpha) {
this.opacity = (int) (alpha * 255) << 24;
}
@Override
public int getColor(int color) {
int originalAlpha = (color >>> 24) & 0xFF;
int newAlpha = (opacity >>> 24) & 0xFF;
int mixedAlpha = (originalAlpha * newAlpha) / 255;
return (mixedAlpha << 24) | (color & 0x00FFFFFF);
}
// ----------------------------
// Draw / Window Info
// ----------------------------
@Override
public void draw() {
this.vertexConsumers.draw();
}
@Override
public int getScaledWindowWidth() {
return SaturnClient.client.getWindow().getScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getScaledHeight();
}
public void draw(Consumer<VertexConsumerProvider> drawer) {
drawer.accept(this.vertexConsumers);
this.vertexConsumers.draw();
}
// ----------------------------
// Shapes
// ----------------------------
@Override
public void drawRectangle(int x, int y, int width, int height, int color) {
if (color == 0)
return;
color = getColor(color);
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
int x1 = x;
int x2 = x + width;
int y1 = y;
int y2 = y + height;
if (x1 < x2) {
int tmp = x1;
x1 = x2;
x2 = tmp;
}
if (y1 < y2) {
int tmp = y1;
y1 = y2;
y2 = tmp;
}
VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(RenderLayer.getGui());
vertexConsumer.vertex(matrix4f, x1, y1, 0).color(color);
vertexConsumer.vertex(matrix4f, x1, y2, 0).color(color);
vertexConsumer.vertex(matrix4f, x2, y2, 0).color(color);
vertexConsumer.vertex(matrix4f, x2, y1, 0).color(color);
}
@Override
public void fill(int x1, int y1, int x2, int y2, int color) {
fill(x1, y1, x2, y2, 0, color);
}
public void fill(int x1, int y1, int x2, int y2, int z, int color) {
fill(RenderLayer.getGui(), x1, y1, x2, y2, z, color);
}
public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int color) {
fill(layer, x1, y1, x2, y2, 0, color);
}
public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int z, int color) {
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
if (x1 < x2) {
int tmp = x1;
x1 = x2;
x2 = tmp;
}
if (y1 < y2) {
int tmp = y1;
y1 = y2;
y2 = tmp;
}
VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(layer);
vertexConsumer.vertex(matrix4f, x1, y1, z).color(color);
vertexConsumer.vertex(matrix4f, x1, y2, z).color(color);
vertexConsumer.vertex(matrix4f, x2, y2, z).color(color);
vertexConsumer.vertex(matrix4f, x2, y1, z).color(color);
}
@Override
public void drawBorder(int x, int y, int width, int height, int color) {
fill(x, y, x + width, y + 1, color);
fill(x, y + height - 1, x + width, y + height, color);
fill(x, y + 1, x + 1, y + height - 1, color);
fill(x + width - 1, y + 1, x + width, y + height - 1, color);
}
// ----------------------------
// Text
// ----------------------------
@Override
public void drawText(String text, int x, int y, int font, int color) {
drawText(1.0f, text, x, y, font, color);
}
@Override
public void drawText(float scale, String text, int x, int y, int font, int color) {
if (color == 0)
return;
if (font == 0)
scale *= 2;
int i = 0;
for (String line : text.split("\n")) {
color = getColor(color);
matrices.push();
matrices.translate(x, y + i * Fonts.getHeight(), 0);
matrices.scale(scale, scale, 1.0f);
TextRenderer textRenderer = SaturnClient.client.textRenderer;
textRenderer.draw((Text) Fonts.setFont(line, font), 0, font == 0 ? 1 : 7, color, false,
matrices.peek().getPositionMatrix(), vertexConsumers, TextLayerType.NORMAL, 0, 15728880);
matrices.pop();
i++;
}
}
// ----------------------------
// Textures & Sprites
// ----------------------------
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height) {
drawTexture(sprite, x, y, u, v, width, height, width, height, width, height);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, int color) {
drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
int regionWidth, int regionHeight, int textureWidth, int textureHeight) {
drawTexture(sprite, x, y, u, v, width, height, regionWidth, regionHeight, textureWidth, textureHeight, -1);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
int regionWidth, int regionHeight, int textureWidth, int textureHeight, int color) {
drawTexturedQuad((Identifier) (Object) sprite, x, x + width, y, y + height,
(u + 0.0f) / textureWidth, (u + regionWidth) / textureWidth,
(v + 0.0f) / textureHeight, (v + regionHeight) / textureHeight, color);
}
@Override
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height) {
drawSpriteStretched(sprite, x, y, width, height, -1);
}
@Override
public void drawSpriteStretched(SpriteRef saturnSprite, int x, int y, int width, int height, int color) {
Sprite sprite = (Sprite) saturnSprite;
if (color == 0)
return;
if (width != 0 && height != 0) {
drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height,
sprite.getMinU(), sprite.getMaxU(), sprite.getMinV(), sprite.getMaxV(), color);
}
}
private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2,
float u1, float u2, float v1, float v2, int color) {
if (sprite.toString().endsWith(".svg")) {
sprite = (Identifier) (Object) SvgTexture.getSvg((IdentifierRef) (Object) sprite, (x2 - x1) * 2,
(y2 - y1) * 2);
}
if (color == 0)
return;
x1 *= 4;
x2 *= 4;
y1 *= 4;
y2 *= 4;
color = getColor(color);
matrices.push();
matrices.scale(0.25f, 0.25f, 1.0f);
//? if <1.21.5
//RenderSystem.setShaderTexture(0, sprite);
RenderLayer renderLayer = RenderLayer.getGuiTextured(sprite);
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(renderLayer);
vertexConsumer.vertex(matrix4f, x1, y1, 0).texture(u1, v1).color(color);
vertexConsumer.vertex(matrix4f, x1, y2, 0).texture(u1, v2).color(color);
vertexConsumer.vertex(matrix4f, x2, y2, 0).texture(u2, v2).color(color);
vertexConsumer.vertex(matrix4f, x2, y1, 0).texture(u2, v1).color(color);
matrices.pop();
}
// ----------------------------
// Items
// ----------------------------
@Override
public void drawItem(ItemStackRef item, int x, int y) {
drawItem(SaturnClient.client.player, SaturnClient.client.world, item, x, y, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y,
int seed) {
drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
int z) {
if (stack.isEmpty())
return;
//? if >=1.21.5
SaturnClient.client.getItemModelManager().update(itemRenderState, stack, ItemDisplayContext.GUI, world, entity, seed);
//? if >=1.21.5
SaturnClient.client.getItemModelManager().clearAndUpdate(this.itemRenderState, stack, ItemDisplayContext.GUI, world, entity, seed);
//? if <1.21.5
//SaturnClient.client.getItemModelManager().update(itemRenderState, stack, ModelTransformationMode.GUI, false, world, entity, seed);
this.matrices.push();
//? if >=1.21.5
this.matrices.translate((float) (x + 8), (float) (y + 8), (float) (150 + z));
//? if <1.21.5
//this.matrices.translate(x + 8, y + 8, 150 + (itemRenderState.hasDepth() ? z : 0));
try {
this.matrices.scale(16.0F, -16.0F, 16.0F);
boolean bl = !this.itemRenderState.isSideLit();
if (bl) {
this.draw();
DiffuseLighting.disableGuiDepthLighting();
}
this.itemRenderState.render(this.matrices, this.vertexConsumers, 15728880, OverlayTexture.DEFAULT_UV);
this.draw();
if (bl) {
DiffuseLighting.enableGuiDepthLighting();
}
} catch (Throwable throwable) {
CrashReport crashReport = CrashReport.create(throwable, "Rendering item");
CrashReportSection crashReportSection = crashReport.addElement("Item being rendered");
crashReportSection.add("Item Type", () -> String.valueOf(stack.getItem()));
crashReportSection.add("Item Components", () -> String.valueOf(stack.getComponents()));
crashReportSection.add("Item Foil", () -> String.valueOf(stack.hasGlint()));
throw new CrashException(crashReport);
}
this.matrices.pop();
}
// ----------------------------
// Scissor / Clipping
// ----------------------------
@Override
public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRect screenRect = new ScreenRect(x1, y1, x2 - x1, y2 - y1)
.transform(matrices.peek().getPositionMatrix());
setScissor(scissorStack.push(screenRect));
}
@Override
public void disableScissor() {
setScissor(scissorStack.pop());
}
@Override
public boolean scissorContains(int x, int y) {
return scissorStack.containsPoint(x, y);
}
private void setScissor(@Nullable ScreenRect rect) {
draw();
if (rect != null) {
Window window = SaturnClient.client.getWindow();
int framebufferHeight = window.getFramebufferHeight();
double scaleFactor = window.getScaleFactor();
RenderSystem.enableScissor(
(int) (rect.getLeft() * scaleFactor),
(int) (framebufferHeight - rect.getBottom() * scaleFactor),
Math.max(0, (int) (rect.width() * scaleFactor)),
Math.max(0, (int) (rect.height() * scaleFactor)));
} else {
RenderSystem.disableScissor();
}
}
static class ScissorStack {
private final Deque<ScreenRect> stack = new ArrayDeque<>();
public ScreenRect push(ScreenRect p_281812_) {
ScreenRect screenrectangle = this.stack.peekLast();
if (screenrectangle != null) {
ScreenRect screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle),
ScreenRect.empty());
this.stack.addLast(screenrectangle1);
return screenrectangle1;
} else {
this.stack.addLast(p_281812_);
return p_281812_;
}
}
@Nullable
public ScreenRect pop() {
if (this.stack.isEmpty()) {
throw new IllegalStateException("Scissor stack underflow");
} else {
this.stack.removeLast();
return this.stack.peekLast();
}
}
public boolean containsPoint(int p_329411_, int p_333404_) {
return this.stack.isEmpty() ? true : this.stack.peek().contains(p_329411_, p_333404_);
}
}
}
*///?}
@@ -3,21 +3,30 @@ package org.saturnclient.impl.ui;
import java.time.Duration;
import java.time.Instant;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.SaturnScreen.ScreenProvider;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gl.PostEffectProcessor;
//? 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;
import net.minecraft.client.render.DefaultFramebufferSet;
import net.minecraft.client.texture.ResourceTexture;
//? if >=1.21.6 {
import net.minecraft.client.texture.CubemapTexture;
//?} else {
/*import net.minecraft.client.texture.ResourceTexture;
import net.minecraft.client.texture.TextureManager;
*///?}
//? if >=1.21.9
import net.minecraft.client.input.KeyInput;
//? if <1.21.6 {
/*import net.minecraft.client.gl.PostEffectProcessor;
import net.minecraft.client.render.DefaultFramebufferSet;
import net.minecraft.client.util.Pool;
*///?}
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
@@ -32,19 +41,29 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
ROTATING_PANORAMA_RENDERER = new RotatingCubeMapRenderer(PANORAMA_RENDERER);
}
private final Pool pool = new Pool(3);
//? if <1.21.6
//private final Pool pool = new Pool(3);
public final SaturnScreen screen;
public static void preload(MinecraftClient client) {
TextureManager textureManager = client.getTextureManager();
/** Whether the panorama's textures are loaded; it isn't drawn before (e.g. under the splash). */
private static volatile boolean panoramaReady = false;
// Force upload each texture to GPU
public static void preload(MinecraftClient client) {
//? if >=1.21.6 {
// From 1.21.6 CubeMapRenderer draws from one cube map texture made of panorama_0..5.png.
// CubeMapRenderer.registerTextures only stores it for the next resource reload, which has
// already happened by now, so register it through the overload that loads it right away.
client.getTextureManager().registerTexture(PANORAMA, new CubemapTexture(PANORAMA));
//?} else {
/*TextureManager textureManager = client.getTextureManager();
for (int i = 0; i < 6; i++) {
String var10001 = PANORAMA.getPath();
Identifier tex = PANORAMA.withPath(var10001 + "_" + i + ".png");
ResourceTexture resourceTexture = new ResourceTexture(tex);
textureManager.registerTexture(tex, resourceTexture);
}
*///?}
panoramaReady = true;
}
public SaturnScreenFabric(SaturnScreen screen) {
@@ -58,7 +77,8 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
screen.init();
}
@SuppressWarnings("deprecation")
//? if <1.21.6
//@SuppressWarnings("deprecation")
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
if (screen.start == null) {
@@ -69,7 +89,9 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
mouseX *= 2;
mouseY *= 2;
if (client.world == null && client.getCurrentServerEntry() == null) {
// From 1.21.6 the background, blur included, is drawn in renderBackground.
//? if <1.21.6 {
/*if (isShowingPanorama()) {
ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, screen.backgroundOpacity, delta);
}
@@ -78,24 +100,82 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
DefaultFramebufferSet.MAIN_ONLY);
if (postEffectProcessor != null) {
postEffectProcessor.setUniforms("Radius", screen.backgroundBlur * Math.min((float) elapsed / 700, 1.0f));
postEffectProcessor.render(this.client.getFramebuffer(), this.pool);
float radius = screen.getBackgroundBlur();
//? if >=1.21.5
postEffectProcessor.render(this.client.getFramebuffer(), this.pool, pass -> pass.setUniform("Radius", radius));
//? if <1.21.5
//postEffectProcessor.setUniforms("Radius", radius);
//? if <1.21.5
//postEffectProcessor.render(this.client.getFramebuffer(), this.pool);
}
*///?}
// We are using a Abstracted RenderScope because older minecraft versions don't
// use DrawContext
RenderScope renderScope = new RenderScopeImpl(context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
RenderScope renderScope = new RenderScopeImpl(context);
screen.render(renderScope, mouseX, mouseY, delta, elapsed);
}
//? if >=1.21.6 {
// Vanilla's background would cover Saturn's panorama, and blurs by the vanilla setting. The
// strength of this blur comes from GameRendererMixin, which gives Saturn's setting instead.
@Override
public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) {
// The panorama renderer has no opacity (and takes no delta) from 1.21.6, so it's drawn fully
// opaque.
if (isShowingPanorama()) {
ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, true);
}
if (screen.getBackgroundBlur() >= 1) {
context.applyBlur();
}
if (client.world != null) {
renderDarkening(context);
}
}
//?}
/** Whether Saturn's panorama is behind the screen: outside a world, once it's loaded. */
private boolean isShowingPanorama() {
return panoramaReady && client.world == null && client.getCurrentServerEntry() == null;
}
// 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) {
// offsetX and offsetY are the drag deltas, as mouseDragged's deltaX and deltaY were before 1.21.9.
if (screen.mouseDragged(click.x(), click.y(), click.button(), offsetX, offsetY)) {
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;
}
return super.mouseClicked(mouseX, mouseY, button);
}
@@ -104,7 +184,6 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
if (screen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) {
return true;
}
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
}
@@ -113,37 +192,50 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
if (screen.mouseReleased(mouseX, mouseY, button)) {
return true;
}
return super.mouseReleased(mouseX, mouseY, button);
}
*///?}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
if (screen.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
return true;
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
}
//? 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);
}
*///?}
//? if >=1.21.11 {
@Override
public void resize(int width, int height) {
screen.resize(width, height);
super.resize(width, height);
}
//?} else {
/*@Override
public void resize(MinecraftClient client, int width, int height) {
screen.resize(width, height);
super.resize(client, width, height);
}
// ------------------------------------------
// ScreenProvider implementations
// ------------------------------------------
*///?}
@Override
public void close() {
@@ -160,4 +252,4 @@ public class SaturnScreenFabric extends Screen implements ScreenProvider {
public int getHeight() {
return height * 2;
}
}
}
@@ -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");
*///?}
}
@@ -0,0 +1,79 @@
package org.saturnclient.saturnclient.event;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
//? 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.impl.ui.SaturnScreenFabric;
import org.saturnclient.ui.screens.EmoteWheel;
import org.saturnclient.ui.screens.ShiftMenu;
import org.lwjgl.glfw.GLFW;
/**
* Handles keyboard input events for the Saturn Client.
* Manages keybindings and their associated actions.
*/
public class KeyInputHandler {
//? 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() {
//? 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 -> {
if (mainMenuKeyBinding.wasPressed()) {
Providers.saturn.getClient().setScreen(new ShiftMenu());
}
// The emote wheel key opens the wheel, and closes it again while it's open. Closing it
// from the screen's own key handling would reopen it on the next tick, since the key is
// still down and no screen is open by then.
if (Config.openEmoteWheel.wasKeyPressed()
//? if >=1.21.9 {
&& !InputUtil.isKeyPressed(client.getWindow(), GLFW.GLFW_KEY_F3)) {
//?} else
//&& !InputUtil.isKeyPressed(client.getWindow().getHandle(), GLFW.GLFW_KEY_F3)) {
if (client.currentScreen == null) {
Providers.saturn.getClient().setScreen(new EmoteWheel());
} else if (client.currentScreen instanceof SaturnScreenFabric saturnScreen
&& saturnScreen.screen instanceof EmoteWheel) {
saturnScreen.close();
}
}
// Sneaking cancels your own emote, for everyone.
if (SaturnClient.client.player != null && SaturnClient.client.player.isSneaking()
&& Emotes.playing(SaturnClient.client.player.getUuid()).isPresent()) {
Emotes.stop(SaturnClient.client.player.getUuid());
ServiceClient.emote("");
}
});
}
}
@@ -1,12 +1,20 @@
package org.saturnclient.saturnclient.mixin;
import net.minecraft.client.gui.DrawContext;
//? if >=1.21.6 {
import net.minecraft.client.gui.render.state.GuiRenderState;
//?} else
//import net.minecraft.client.render.VertexConsumerProvider;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(DrawContext.class)
public interface DrawContextAccessor {
//? if >=1.21.6 {
@Accessor("state")
GuiRenderState getState();
//?} else {
/*@Accessor("vertexConsumers")
VertexConsumerProvider.Immediate getVertexConsumers();
*///?}
}
@@ -0,0 +1,79 @@
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()));
}
// Adds the role icon to the player's name, and stores the SaturnPlayer in the render state for the
// cloak and hat (Fabric API only has render state data from 1.21.8).
//? 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 {
/*@Inject(method = "updateRenderState", at = @At("TAIL"))
public void updateRenderState(
AbstractClientPlayerEntity entity, PlayerEntityRenderState state, float f, CallbackInfo ci) {
saturn$applyPlayer(entity, state);
}
*///?}
private void saturn$applyPlayer(Entity entity, PlayerEntityRenderState state) {
if (entity == null) {
return;
}
SaturnPlayer player = SaturnPlayer.get(entity.getUuid());
//? if >=1.21.8
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());
}
}
}
@@ -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,10 @@ 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));
//? if >=1.21.9 {
context.drawStrokedRectangle((screenWidth - 120) / 2, progressY, 120, 5, Theme.withAlpha(alpha, 0xFFFFFFFF));
//?} else
//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 +158,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));
}
}
@@ -9,7 +9,6 @@
"render.LivingEntityRendererMixin",
"render.LivingEntityRenderStateMixin",
"render.BackgroundRendererMixin",
"misc.NametagsMixin",
"render.RenderMixin",
"world.TpsMixin",
"entity.CameraMixin",
@@ -5,7 +5,6 @@
"compatibilityLevel": "JAVA_21",
"client": [
"TabListMixin",
"NameTagMixin",
"PlayerEntityRendererMixin",
"WhoAmI",
"DrawContextAccessor",
+71
View File
@@ -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")
}
}
+18
View File
@@ -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.5
fabric_api_version=0.138.4+1.21.10
# Mod Properties
maven_group=org.saturnclient
archives_base_name=saturn-client
+18
View File
@@ -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.5
fabric_api_version=0.141.3+1.21.11
# Mod Properties
maven_group=org.saturnclient
archives_base_name=saturn-client
-109
View File
@@ -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.
}
}
+1 -4
View File
@@ -9,8 +9,7 @@ org.gradle.configuration-cache=false
# check these on https://fabricmc.net/develop
minecraft_version=1.21.4
yarn_mappings=1.21.4+build.8
loader_version=0.18.4
loom_version=1.15-SNAPSHOT
loader_version=0.19.5
# Mod Properties
maven_group=org.saturnclient
@@ -18,5 +17,3 @@ archives_base_name=saturn-client
# Dependencies
fabric_api_version=0.119.4+1.21.4
player_anim = 2.0.3+1.21.4
-10
View File
@@ -1,10 +0,0 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}
@@ -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 || player.cloak.isEmpty()) {
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,39 +0,0 @@
package org.saturnclient.impl.cosmetics;
import org.saturnclient.client.player.SaturnPlayer;
import org.saturnclient.impl.cosmetics.obj.MtlLoader;
import org.saturnclient.impl.cosmetics.obj.ObjModel;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.feature.FeatureRenderer;
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
import net.minecraft.client.render.entity.model.PlayerEntityModel;
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
super(context);
}
@Override
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
PlayerEntityRenderState state, float limbAngle, float limbDistance) {
SaturnPlayer player = SaturnPlayer.get(state.name);
if (state.invisible || player == null || player.hat.isEmpty()) {
return;
}
matrices.push();
this.getContextModel().head.rotate(matrices);
ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
matrices, vertexConsumers, light,
OverlayTexture.DEFAULT_UV);
matrices.pop();
}
}
@@ -1,112 +0,0 @@
package org.saturnclient.impl.cosmetics.obj;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
import de.javagl.obj.*;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier;
public class ObjRenderer {
public static Obj loadObj(Identifier objId) throws IOException {
try (InputStream is = MinecraftClient.getInstance().getResourceManager()
.getResource(objId).get().getInputStream()) {
return ObjReader.read(is);
}
}
public static void renderObj(Obj obj, Map<String, Mtl> mtlMap, MatrixStack matrices,
VertexConsumerProvider vertexConsumers, int light, int overlay) {
MatrixStack.Entry entry = matrices.peek();
Map<String, Obj> materialGroups = ObjSplitting.splitByMaterialGroups(obj);
for (Map.Entry<String, Obj> group : materialGroups.entrySet()) {
String materialName = group.getKey();
Obj groupObj = group.getValue();
Mtl mtl = mtlMap != null ? mtlMap.get(materialName) : null;
// Resolve texture identifier using same logic as ObjUnbakedModelModel
IdentifierRef texture = IdentifierRef.ofVanilla("textures/misc/white.png");
if (mtl != null) {
String mapKd = mtl.getMapKd();
if (mapKd != null) {
if (!mapKd.startsWith("#")) {
texture = IdentifierRef.of(mapKd);
}
}
}
// Diffuse color (Kd)
float r = 1f, g = 1f, b = 1f;
if (mtl != null) {
FloatTuple kd = mtl.getKd();
if (kd != null) {
r = kd.getX();
g = kd.getY();
b = kd.getZ();
}
}
VertexConsumer consumer = vertexConsumers.getBuffer(ShaderUtils.getRenderLayer(texture));
int faces = groupObj.getNumFaces();
final float fr = r, fg = g, fb = b;
for (int f = 0; f < faces; f++) {
ObjFace face = groupObj.getFace(f);
int verts = face.getNumVertices();
// Your original winding order logic, untouched
for (int i = verts - 1; i >= 0; i--) {
writeVertex(groupObj, face, i, consumer, entry, light, overlay, fr, fg, fb);
}
}
}
}
private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer,
MatrixStack.Entry matrix, int light, int overlay,
float r, float g, float b) {
FloatTuple pos = obj.getVertex(face.getVertexIndex(index));
float x = pos.getX();
float y = pos.getY();
float z = pos.getZ();
float nx = 0f, ny = 1f, nz = 0f;
int normalIndex = face.getNormalIndex(index);
if (normalIndex >= 0) {
FloatTuple n = obj.getNormal(normalIndex);
nx = n.getX();
ny = n.getY();
nz = n.getZ();
}
float u = 0f, v = 0f;
int texIndex = face.getTexCoordIndex(index);
if (texIndex >= 0) {
FloatTuple tc = obj.getTexCoord(texIndex);
u = tc.getX();
v = 1f - tc.getY(); // Flip V: OBJ bottom-left origin -> Minecraft top-left
}
consumer.vertex(matrix, x, y, z)
.color(r, g, b, 1f)
.texture(u, v)
.overlay(overlay)
.light(light)
.normal(matrix, nx, ny, nz);
}
}
@@ -1,68 +0,0 @@
package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.saturnclient.SaturnClient;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture;
import net.minecraft.util.Identifier;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.Map;
public class IdentifierUtils {
public static void registerBufferedImageTextureFast(IdentifierRef identifierRef, BufferedImage bufferedImage) {
try {
Identifier identifier = (Identifier) (Object) identifierRef;
int width = bufferedImage.getWidth();
int height = bufferedImage.getHeight();
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false);
if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, pixels[index++]);
}
}
} else {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
nativeImage.setColorArgb(x, y, bufferedImage.getRGB(x, y));
}
}
}
SaturnClient.client.execute(() -> {
NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
SaturnClient.client.getTextureManager().registerTexture(identifier, texture);
});
} catch (Exception e) {
SaturnClient.LOGGER.error("Failed to register texture: {}", identifierRef, e);
}
}
public static void registerBufferedImageTexturesBulk(Map<IdentifierRef, BufferedImage> textures) {
long start = System.currentTimeMillis();
textures.forEach(IdentifierUtils::registerBufferedImageTextureFast);
long end = System.currentTimeMillis();
SaturnClient.LOGGER.info(
"Registered {} textures in {}ms",
textures.size(),
(end - start));
}
}
@@ -1,41 +0,0 @@
package org.saturnclient.impl.cosmetics.utils;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.cosmetics.utils.GifUtils;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderPhase;
import net.minecraft.client.render.VertexFormat.DrawMode;
import net.minecraft.client.render.VertexFormats;
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 RenderLayer END_GATEWAY = RenderLayer.of(
"end_gateway",
VertexFormats.POSITION,
DrawMode.QUADS,
1536,
false,
false,
RenderLayer.MultiPhaseParameters.builder()
.program(RenderLayer.END_GATEWAY_PROGRAM)
.texture(Textures.create()
.add(EndPortalBlockEntityRenderer.SKY_TEXTURE, false, false)
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false, false)
.build())
.cull(RenderPhase.DISABLE_CULLING)
.build(false));
public static RenderLayer getRenderLayer(IdentifierRef texture) {
String path = texture.toString();
if (path.endsWith(".gif")) {
return RenderLayer.getEntityAlpha((Identifier) (Object) GifUtils.get(texture));
} else if (path.endsWith("end.png")) {
return END_GATEWAY;
} else {
return RenderLayer.getEntityAlpha((Identifier) (Object) texture);
}
}
}
@@ -1,46 +0,0 @@
package org.saturnclient.impl.features.mixins.entity;
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
import org.saturnclient.mod.mods.FreelookMod;
import org.saturnclient.saturnclient.SaturnClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.world.BlockView;
@Mixin(Camera.class)
public abstract class CameraMixin {
@Unique
boolean firstTime = true;
@Shadow
protected abstract void setRotation(float yaw, float pitch);
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
public void lockRotation(BlockView focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing,
float tickDelta, CallbackInfo ci) {
if (FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
if (firstTime && SaturnClient.client.player != null) {
cameraOverriddenEntity.freelook$setCameraPitch(SaturnClient.client.player.getPitch());
cameraOverriddenEntity.freelook$setCameraYaw(SaturnClient.client.player.getYaw());
firstTime = false;
}
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(),
cameraOverriddenEntity.freelook$getCameraPitch());
}
if (!FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
firstTime = true;
}
}
}
@@ -1,95 +0,0 @@
package org.saturnclient.impl.features.mixins.misc;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.state.EntityRenderState;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
import org.saturnclient.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
* the string produced by {@link NametagsMod#getNametagString}.
*
* Key changes from the original:
*
* <ul>
* <li>The old {@code NametagsFabric.EntityStateImpl} bridge class is
* replaced by {@link EntityFeatureImpl.RenderStateEntityState},
* which wraps the {@link HealthRenderState} already injected into
* the render state by {@link LivingEntityRenderStateMixin}.</li>
* <li>The feature import changes from {@code Nametags} to
* {@link NametagsMod}; the two static methods
* ({@code shouldReplaceName} / {@code getNametagString}) are
* unchanged in signature.</li>
* <li>The entity's display name is extracted from the render state's
* {@code nameVisible} / text fields where available, falling back
* to the plain text string passed to this method.</li>
* </ul>
*
* The re-entrancy guard ({@code saturn$rendering}) is kept intact to
* prevent infinite recursion when we call {@code renderLabelIfPresent}
* with the replacement text.
*/
@Mixin(EntityRenderer.class)
public abstract class NametagsMixin {
/** Guards against the recursive call we make with the replacement text. */
@Unique
private static final ThreadLocal<Boolean> saturn$rendering = ThreadLocal.withInitial(() -> false);
@Shadow
protected abstract <S extends EntityRenderState> void renderLabelIfPresent(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light);
@Inject(method = "renderLabelIfPresent", at = @At("HEAD"), cancellable = true)
private <S extends EntityRenderState> void saturn$replaceNametag(
S state, Text text,
MatrixStack matrices, VertexConsumerProvider vertexConsumers,
int light, CallbackInfo ci) {
// Skip our own recursive call
if (saturn$rendering.get())
return;
if (!NametagsMod.shouldReplaceName())
return;
// Only living entities carry health data
if (!(state instanceof LivingEntityRenderState living))
return;
if (!(living instanceof HealthRenderState hrs))
return;
// Build the platform-neutral EntityState from the render-state snapshot
// (health/type were written by LivingEntityRendererMixin at extract time).
String customName = text != null ? text.getString() : null;
EntityFeatureImpl.RenderStateEntityState entityState = new EntityFeatureImpl.RenderStateEntityState(
customName, hrs);
// Ask the feature for the replacement string
String replacement = NametagsMod.getNametagString(entityState);
if (replacement == null)
return; // feature decided not to replace this entity
ci.cancel();
saturn$rendering.set(true);
try {
renderLabelIfPresent(state, Text.literal(replacement), matrices, vertexConsumers, light);
} finally {
saturn$rendering.set(false);
}
}
}
@@ -1,21 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.BackgroundRenderer;
import org.saturnclient.mod.mods.NoFogMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(BackgroundRenderer.class)
public class BackgroundRendererMixin {
// fog
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
private static float modifyFogStart(float viewDistance) {
if (NoFogMod.isActive()) {
return viewDistance * 2.0f;
}
return viewDistance;
}
}
@@ -1,35 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
// Code approach adapted from Gamma Utils: https://github.com/Sjouwer/gamma-utils
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.render.LightmapTextureManager;
import org.saturnclient.mod.mods.FullbrightMod;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Overrides the lightmap's gamma computation when
* {@link FullbrightMod} is active, replacing the vanilla
* brightness with a user-configured value.
*
* The old import was {@code org.saturnclient.Mod.Mods.Fullbright};
* it is now {@link FullbrightMod}. The injection target and
* strategy are unchanged.
*/
@Mixin(LightmapTextureManager.class)
public class LightmapTextureManagerMixin {
/**
* If fullbright is active, substitute the Mod's brightness
* value for whatever vanilla calculated.
*/
@ModifyExpressionValue(method = "update", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0))
private float modifyGamma(float original) {
if (FullbrightMod.shouldOverrideBrightness()) {
return FullbrightMod.getBrightnessValue();
}
return original;
}
}
@@ -1,60 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.saturnclient.impl.features.mixins.misc.NametagsMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Implements {@link HealthRenderState} on
* {@link LivingEntityRenderState}, injecting health and entity-type
* storage into every living entity's render-state snapshot.
*
* Previously imported {@code NametagsInterface.EntityType}; the enum
* now lives in {@link EntityFeature.EntityType} which is the single
* canonical definition shared by the mixin layer and the feature layer.
*
* The stored values are written by
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
* render state is extracted from a live entity, and read by
* {@link NametagsMixin} at render time.
*/
@Mixin(LivingEntityRenderState.class)
public class LivingEntityRenderStateMixin implements HealthRenderState {
@Unique
private float saturn$health = 0f;
@Unique
private float saturn$maxHealth = 1f;
@Unique
private EntityFeature.EntityType saturn$entityType = EntityFeature.EntityType.OTHER;
@Override
public float saturn$getHealth() {
return saturn$health;
}
@Override
public float saturn$getMaxHealth() {
return saturn$maxHealth;
}
@Override
public void saturn$setHealth(float health, float maxHealth) {
this.saturn$health = health;
this.saturn$maxHealth = maxHealth;
}
@Override
public EntityFeature.EntityType saturn$getEntityType() {
return saturn$entityType;
}
@Override
public void saturn$setEntityType(EntityFeature.EntityType type) {
saturn$entityType = type;
}
}
@@ -1,51 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.render.entity.LivingEntityRenderer;
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.passive.PassiveEntity;
import net.minecraft.entity.player.PlayerEntity;
import org.saturnclient.common.feature.EntityFeature;
import org.saturnclient.impl.features.entity.HealthRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Captures each living entity's health and type into the render state
* during {@code updateRenderState}, so that nametag rendering can read
* them without touching the live entity again.
*
* Previously imported {@code NametagsInterface.EntityType}; now uses
* {@link EntityFeature.EntityType} — the canonical enum shared across
* the whole module system.
*
* The injection target and strategy are unchanged from the original.
*/
@Mixin(LivingEntityRenderer.class)
public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> {
@Inject(method = "updateRenderState", at = @At("TAIL"))
private void saturn$captureHealth(T entity, S state, float tickDelta, CallbackInfo ci) {
if (!(state instanceof HealthRenderState hrs))
return;
hrs.saturn$setHealth(entity.getHealth(), entity.getMaxHealth());
// Classify entity type for the nametag feature's filtering logic
EntityFeature.EntityType type;
if (entity instanceof PlayerEntity)
type = EntityFeature.EntityType.PLAYER;
else if (entity instanceof HostileEntity)
type = EntityFeature.EntityType.HOSTILE;
else if (entity instanceof PassiveEntity)
type = EntityFeature.EntityType.PASSIVE;
else
type = EntityFeature.EntityType.OTHER;
hrs.saturn$setEntityType(type);
}
}
@@ -1,85 +0,0 @@
package org.saturnclient.impl.features.mixins.render;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderTickCounter;
import org.saturnclient.impl.ui.RenderScopeImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModManager;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.screens.HudEditor;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Drives per-frame rendering for all registered features.
*
* This mixin is intentionally kept thin: it creates a {@link RenderScope},
* iterates the enabled feature list from {@link ModManager}, and
* dispatches either {@link HudMod#renderHud} (for HUD elements) or
* {@link Mod#render} (for world-overlay features like Crosshair).
*
* No feature-specific logic lives here — all decisions about what to
* draw are made inside each feature class.
*
* Changes from the original:
* - Package imports updated to the refactored feature names (no
* more {@code feature.features.*}).
* - The HUD-editor screen check still uses the existing
* {@link SaturnScreenFabric} / {@link HudEditor} pair; that is
* unchanged infrastructure.
*/
@Mixin(InGameHud.class)
public class RenderMixin {
@Inject(method = "renderChat", at = @At("TAIL"))
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
TextRenderer textRenderer = SaturnClient.client.textRenderer;
// Skip rendering while the HUD editor is open so the editor
// can draw its own preview layout without double-drawing.
if (textRenderer == null)
return;
if (SaturnClient.client.currentScreen instanceof SaturnScreenFabric f
&& f.screen instanceof HudEditor)
return;
RenderScope scope = new RenderScopeImpl(
context.getMatrices(),
((DrawContextAccessor) context).getVertexConsumers());
for (Mod feature : ModManager.ENABLED_MODS) {
if (!feature.isEnabled())
continue;
if (feature instanceof HudMod hud) {
ModLayout dim = hud.getDimensions();
scope.getMatrixStack().push();
scope.getMatrixStack().translate(dim.x.value, (float) dim.y.value);
scope.getMatrixStack().scale(dim.scale.value, dim.scale.value);
if (dim.renderBackground) {
scope.drawRoundedRectangle(0, 0, dim.width, dim.height,
dim.radius.value, dim.bgColor.value);
}
hud.renderHud(scope);
scope.getMatrixStack().pop();
} else {
// Non-HUD features (e.g. CrosshairMod) use the plain render hook.
feature.render(scope);
}
}
}
}
@@ -1,253 +0,0 @@
package org.saturnclient.impl.features.player;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.registry.Registries;
import net.minecraft.util.Identifier;
import org.saturnclient.common.feature.PlayerFeature;
import org.saturnclient.common.ref.game.EffectRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Fabric implementation of {@link PlayerFeature}.
*
* All access to {@link MinecraftClient} and {@link ClientPlayerEntity}
* is encapsulated here. Features only ever see the {@link PlayerFeature}
* interface; they never import Minecraft classes directly.
*
* Every method guards against a missing player and returns a safe
* default so features are free of null checks.
*/
public class PlayerFeatureImpl implements PlayerFeature {
private final MinecraftClient mc;
public PlayerFeatureImpl(MinecraftClient mc) {
this.mc = mc;
}
// ---------------------------------------------------------------
// Internal helper — never returns null
// ---------------------------------------------------------------
/** Returns the local player, or {@code null} if one is not loaded. */
private ClientPlayerEntity player() {
return mc.player;
}
// ---------------------------------------------------------------
// Presence
// ---------------------------------------------------------------
@Override
public boolean hasPlayer() {
return mc.player != null;
}
// ---------------------------------------------------------------
// Position
// ---------------------------------------------------------------
@Override
public int getX() {
return hasPlayer() ? (int) player().getX() : 0;
}
@Override
public int getY() {
return hasPlayer() ? (int) player().getY() : 0;
}
@Override
public int getZ() {
return hasPlayer() ? (int) player().getZ() : 0;
}
// ---------------------------------------------------------------
// Input / movement
// ---------------------------------------------------------------
@Override
public boolean isForwardPressed() {
return mc.options.forwardKey.isPressed();
}
@Override
public boolean isBackPressed() {
return mc.options.backKey.isPressed();
}
@Override
public boolean isLeftPressed() {
return mc.options.leftKey.isPressed();
}
@Override
public boolean isRightPressed() {
return mc.options.rightKey.isPressed();
}
@Override
public boolean isJumpPressed() {
return mc.options.jumpKey.isPressed();
}
@Override
public boolean isAttackPressed() {
return mc.options.attackKey.isPressed();
}
@Override
public boolean isUsePressed() {
return mc.options.useKey.isPressed();
}
@Override
public boolean isSneaking() {
return hasPlayer() && player().isSneaking();
}
@Override
public boolean isUsingItem() {
return hasPlayer() && player().isUsingItem();
}
@Override
public boolean hasHorizontalCollision() {
return hasPlayer() && player().horizontalCollision;
}
@Override
public boolean isOnGround() {
return hasPlayer() && player().isOnGround();
}
@Override
public void setSprinting(boolean sprinting) {
if (hasPlayer())
player().setSprinting(sprinting);
}
// ---------------------------------------------------------------
// Velocity
// ---------------------------------------------------------------
@Override
public Velocity getVelocity() {
if (!hasPlayer())
return Velocity.ZERO;
var v = player().getVelocity();
return new Velocity(v.x, v.y, v.z);
}
// ---------------------------------------------------------------
// Stats
// ---------------------------------------------------------------
@Override
public float getHealth() {
return hasPlayer() ? player().getHealth() : 0f;
}
// ---------------------------------------------------------------
// Equipment
// ---------------------------------------------------------------
@Override
public ItemStackRef getMainHand() {
return wrap(hasPlayer() ? player().getMainHandStack() : ItemStack.EMPTY);
}
@Override
public ItemStackRef getHelmet() {
return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getChestplate() {
return wrap(hasPlayer() ? player().getInventory().armor.get(2) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getLeggings() {
return wrap(hasPlayer() ? player().getInventory().armor.get(1) : ItemStack.EMPTY);
}
@Override
public ItemStackRef getBoots() {
return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY);
}
// ---------------------------------------------------------------
// Dummy equipment
// ---------------------------------------------------------------
@Override
public ItemStackRef getDummyMainHand() {
return wrap(new ItemStack(Items.DIAMOND_SWORD));
}
@Override
public ItemStackRef getDummyHelmet() {
return wrap(new ItemStack(Items.DIAMOND_HELMET));
}
@Override
public ItemStackRef getDummyChestplate() {
return wrap(new ItemStack(Items.DIAMOND_CHESTPLATE));
}
@Override
public ItemStackRef getDummyLeggings() {
return wrap(new ItemStack(Items.DIAMOND_LEGGINGS));
}
@Override
public ItemStackRef getDummyBoots() {
return wrap(new ItemStack(Items.DIAMOND_BOOTS));
}
// ---------------------------------------------------------------
// Status effects
// ---------------------------------------------------------------
@Override
public List<? extends EffectRef> getActiveEffects() {
if (!hasPlayer())
return Collections.emptyList();
var effects = player().getActiveStatusEffects().values();
List<EffectRef> result = new ArrayList<>(effects.size());
for (StatusEffectInstance instance : effects) {
result.add((EffectRef) instance);
}
return result;
}
@Override
public List<? extends EffectRef> getDummyEffects() {
return List.of(
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
(EffectRef) new StatusEffectInstance(
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2));
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static ItemStackRef wrap(ItemStack stack) {
return (ItemStackRef) (Object) stack;
}
}
@@ -1,36 +0,0 @@
package org.saturnclient.impl.features.world;
import org.saturnclient.common.feature.WorldFeature;
import net.minecraft.client.MinecraftClient;
/**
* Fabric implementation of {@link WorldFeature}.
*
* Reads the client world's time directly from {@link MinecraftClient}.
* Returns safe defaults when no world is loaded.
*/
public class WorldFeatureImpl implements WorldFeature {
private final MinecraftClient mc;
public WorldFeatureImpl(MinecraftClient mc) {
this.mc = mc;
}
@Override
public boolean hasWorld() {
return mc.world != null;
}
/**
* Returns the total world age in ticks, or {@code 0} when no world
* is loaded. Used by {@link TpsFeature#onTimePacket} (which is
* called from the mixin and stores its own copy) and by
* {@link DayCounterFeature} for the day count.
*/
@Override
public long getWorldAge() {
return hasWorld() ? mc.world.getTime() : 0L;
}
}
@@ -1,50 +0,0 @@
package org.saturnclient.impl.mixins;
import org.joml.Quaternionf;
import org.saturnclient.common.ref.render.MatrixStackRef;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.RotationAxis;
@Mixin(MatrixStack.class)
public abstract class MatrixStackMixin implements MatrixStackRef {
@Shadow
public abstract void push();
@Shadow
public abstract void pop();
@Shadow
public abstract void translate(float x, float y, float z);
@Shadow
public abstract void scale(float x, float y, float z);
@Shadow
public abstract void multiply(Quaternionf quaternion);
@Shadow
public abstract void multiply(Quaternionf quaternion, float originX, float originY, float originZ);
@Override
public void translate(float x, float y) {
translate(x, y, 0);
}
@Override
public void scale(float x, float y) {
scale(x, y, 1.0f);
}
@Override
public void rotate(float angle, float originX, float originY, float originZ) {
multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(angle), originX, originY, originZ);
}
@Override
public void rotate(float angle) {
multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(angle));
}
}
@@ -1,162 +0,0 @@
package org.saturnclient.impl.mixins;
import java.io.File;
import java.io.InputStream;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.game.MinecraftClientRef;
import org.saturnclient.common.ref.render.WindowRef;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.screens.TitleMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
import net.minecraft.client.gui.screen.option.OptionsScreen;
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
import net.minecraft.client.option.GameOptions;
import net.minecraft.client.session.Session;
import net.minecraft.client.util.Window;
import net.minecraft.resource.ReloadableResourceManagerImpl;
import net.minecraft.util.Identifier;
@Mixin(MinecraftClient.class)
public abstract class MinecraftClientMixin implements MinecraftClientRef {
@Shadow
public File runDirectory;
@Shadow
private Window window;
@Shadow
private ReloadableResourceManagerImpl resourceManager;
@Shadow
public GameOptions options;
@Shadow
private Session session;
@Shadow
public abstract void setScreen(@Nullable Screen screen);
@Shadow
@Override
public abstract void scheduleStop();
@Override
public File getRunDirectory() {
return runDirectory;
}
@Override
public InputStream getResource(IdentifierRef identifier) {
try {
return resourceManager.getResource((Identifier) (Object) identifier).get().getInputStream();
} catch (Exception e) {
return null;
}
}
@Override
public WindowRef getWindow() {
return (WindowRef) (Object) this.window;
}
@Override
public void setScreen(SaturnScreen screen) {
this.setScreen(new SaturnScreenFabric(screen));
}
@Override
public void setScreen(MinecraftScreen screen) {
SaturnScreenFabric exitTarget = new SaturnScreenFabric(new TitleMenu());
switch (screen) {
case SelectWorld:
this.setScreen(new SelectWorldScreen(exitTarget));
break;
case Multiplayer:
this.setScreen(new MultiplayerScreen(exitTarget));
break;
case Options:
this.setScreen(new OptionsScreen(exitTarget, this.options));
break;
}
}
@Override
public String getAccessToken() {
if (session == null) {
return null;
}
return this.session.getAccessToken();
}
@Override
public UUID getUuid() {
if (session == null) {
return null;
}
return this.session.getUuidOrNull();
}
@Override
public String getUsername() {
if (session == null) {
return null;
}
return this.session.getUsername();
}
@Override
public void onClientStopping(Runnable handler) {
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> handler.run());
}
@Override
public void executeOnThread(Runnable runnable) {
((java.util.concurrent.Executor) (Object) this).execute(runnable);
}
@Override
@SuppressWarnings("resource")
public String getPlayerListEntry(UUID uuid) {
var client = (MinecraftClient) (Object) this;
var handler = client.getNetworkHandler();
if (handler == null)
return null;
var entry = handler.getPlayerListEntry(uuid);
if (entry == null)
return null;
return entry.getProfile().getName();
}
@Override
@SuppressWarnings("resource")
public UUID getPlayerListEntryUUID(String name) {
var client = (MinecraftClient) (Object) this;
var handler = client.getNetworkHandler();
if (handler == null)
return null;
var entry = handler.getPlayerListEntry(name);
if (entry == null)
return null;
return entry.getProfile().getId();
}
}
@@ -1,44 +0,0 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.common.ref.game.EffectRef;
import org.saturnclient.saturnclient.SaturnClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import net.minecraft.entity.effect.StatusEffect;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.registry.entry.RegistryEntry;
@Mixin(StatusEffectInstance.class)
public abstract class StatusEffectMixin implements EffectRef {
@Shadow
public abstract boolean shouldShowIcon();
@Shadow
public abstract boolean isInfinite();
@Shadow
public abstract int getDuration();
@Shadow
public abstract RegistryEntry<StatusEffect> getEffectType();
@Override
public SpriteRef getIcon() {
return (SpriteRef) SaturnClient.client
.getStatusEffectSpriteManager()
.getSprite(getEffectType());
}
@Override
public String getInfiniteText() {
return "∞";
}
@Override
public int getDurationSeconds() {
return getDuration() / 20; // ticks → seconds
}
}
@@ -1,24 +0,0 @@
package org.saturnclient.impl.mixins;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.render.TextRef;
import org.saturnclient.saturnclient.SaturnClient;
import org.spongepowered.asm.mixin.Mixin;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
@Mixin(Text.class)
public interface TextMixin extends TextRef {
@Override
public default TextRef withFont(IdentifierRef font) {
return (TextRef) (((Text) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font)));
}
@Override
public default int getWidth() {
return SaturnClient.client.textRenderer.getWidth((Text) this);
}
}
@@ -1,25 +0,0 @@
package org.saturnclient.impl.provider;
import org.saturnclient.common.provider.RefConstructorProvider;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.render.TextRef;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
public class RefConstructorImpl implements RefConstructorProvider {
@Override
public IdentifierRef identifier(String fullPath) {
return (IdentifierRef) (Object) Identifier.of(fullPath);
}
@Override
public IdentifierRef identifier(String namespace, String path) {
return (IdentifierRef) (Object) Identifier.of(namespace, path);
}
@Override
public TextRef text(String text) {
return (TextRef) Text.literal(text);
}
}
@@ -1,72 +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;
import dev.kosmx.playerAnim.api.layered.AnimationStack;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.util.Identifier;
public class SaturnProviderImpl implements SaturnProvider {
@Override
public MinecraftClientRef getClient() {
return (MinecraftClientRef) SaturnClient.client;
}
@Override
public void playEmote(UUID fromPlayerUuid, String emoteIdOrNull) {
if (SaturnClient.client == null || SaturnClient.client.world == null) {
return;
}
for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
if (!player.getUuid().equals(fromPlayerUuid)) {
continue;
}
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
if (emoteIdOrNull != null && !emoteIdOrNull.isEmpty()) {
if (animationStack.isActive() && animationStack.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,85 +0,0 @@
package org.saturnclient.impl.ui;
import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.components.SkinPreview.EntityDrawer;
import net.minecraft.client.render.DiffuseLighting;
import net.minecraft.client.render.entity.EntityRenderDispatcher;
import net.minecraft.entity.LivingEntity;
public class EntityDrawerImpl implements EntityDrawer {
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float f,
float mouseX,
float mouseY, LivingEntity entity, float angle, boolean negativeAngle) {
float g = (float) (x1 + x2) / 2.0F;
float h = (float) (y1 + y2) / 2.0F;
renderScope.enableScissor(0, 6, x2, y2);
float i = (float) Math.atan((double) ((g - mouseX) / 40.0F));
float j = (float) Math.atan((double) ((h - mouseY) / 40.0F));
Quaternionf quaternionf = (new Quaternionf()).rotateZ(3.1415927F);
float k = entity.bodyYaw;
float l = entity.getYaw();
float m = entity.getPitch();
float n = entity.prevHeadYaw;
float o = entity.headYaw;
entity.setYaw(angle);
entity.bodyYaw = angle;
entity.setPitch(-(j * 20.0F));
entity.headYaw = negativeAngle ? (i * 40.0F) + 180f : -(i * 40.0F);
entity.prevHeadYaw = entity.getHeadYaw();
entity.setCustomNameVisible(false);
float p = entity.getScale();
Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + f * p, 0.0F);
float q = (float) size / p;
drawEntity(renderScope, g, h, q, vector3f, quaternionf, null, entity);
entity.setCustomNameVisible(true);
entity.bodyYaw = k;
entity.setYaw(l);
entity.setPitch(m);
entity.prevHeadYaw = n;
entity.headYaw = o;
renderScope.disableScissor();
}
public static void drawEntity(RenderScope renderScope, float x, float y, float size, Vector3f vector3f,
Quaternionf quaternionf, @Nullable Quaternionf quaternionf2, LivingEntity entity) {
if (renderScope instanceof RenderScopeImpl scopeImpl) {
scopeImpl.matrices.push();
scopeImpl.matrices.translate((double) x, (double) y, 0);
scopeImpl.matrices.scale(size, size, -size);
scopeImpl.matrices.translate(vector3f.x, vector3f.y, vector3f.z);
scopeImpl.matrices.multiply(quaternionf);
scopeImpl.draw();
DiffuseLighting.method_34742();
EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
entityRenderDispatcher.setRenderShadows(false);
scopeImpl.draw((vertexConsumers) -> {
entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 1.0F,
scopeImpl.matrices, vertexConsumers, 15728880);
});
scopeImpl.draw();
entityRenderDispatcher.setRenderShadows(true);
scopeImpl.getMatrixStack().pop();
DiffuseLighting.enableGuiDepthLighting();
}
}
@Override
public void render(RenderScope renderScope, boolean negativeAngle, float angle, float scale, ElementContext ctx) {
LivingEntity entity = SaturnClient.client.player;
if (entity != null) {
drawEntity(renderScope, 0, 0, 75, 78, 30, 0.0625F, ctx.mouseX / scale, ctx.mouseY / scale, entity, angle,
negativeAngle);
}
}
}
@@ -1,407 +0,0 @@
package org.saturnclient.impl.ui;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import com.mojang.blaze3d.systems.RenderSystem;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.common.ref.asset.SpriteRef;
import org.saturnclient.common.ref.game.ItemStackRef;
import org.saturnclient.common.ref.render.MatrixStackRef;
import org.saturnclient.saturnclient.SaturnClient;
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.SvgTexture;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.font.TextRenderer.TextLayerType;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.ScreenRect;
import net.minecraft.client.render.DiffuseLighting;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.item.ItemRenderState;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.Window;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.LivingEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ModelTransformationMode;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.crash.CrashException;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashReportSection;
import net.minecraft.world.World;
public class RenderScopeImpl implements RenderScope {
// ----------------------------
// Fields
// ----------------------------
public MatrixStack matrices;
public VertexConsumerProvider.Immediate vertexConsumers;
private ScissorStack scissorStack = new ScissorStack();
private int opacity = 255 << 24;
private final ItemRenderState itemRenderState;
// ----------------------------
// Constructors
// ----------------------------
public RenderScopeImpl(DrawContext context) {
this(context.getMatrices(), ((DrawContextAccessor) context).getVertexConsumers());
}
public RenderScopeImpl(MatrixStack matrices, VertexConsumerProvider.Immediate vertexConsumers) {
this.matrices = matrices;
this.vertexConsumers = vertexConsumers;
this.scissorStack = new ScissorStack();
this.itemRenderState = new ItemRenderState();
}
// ----------------------------
// Matrix & Transform
// ----------------------------
@Override
public MatrixStackRef getMatrixStack() {
return (MatrixStackRef) matrices;
}
// ----------------------------
// Color & Opacity
// ----------------------------
@Override
public void setOpacity(float alpha) {
this.opacity = (int) (alpha * 255) << 24;
}
@Override
public int getColor(int color) {
int originalAlpha = (color >>> 24) & 0xFF;
int newAlpha = (opacity >>> 24) & 0xFF;
int mixedAlpha = (originalAlpha * newAlpha) / 255;
return (mixedAlpha << 24) | (color & 0x00FFFFFF);
}
// ----------------------------
// Draw / Window Info
// ----------------------------
@Override
public void draw() {
this.vertexConsumers.draw();
}
@Override
public int getScaledWindowWidth() {
return SaturnClient.client.getWindow().getScaledWidth();
}
@Override
public int getScaledWindowHeight() {
return SaturnClient.client.getWindow().getScaledHeight();
}
public void draw(Consumer<VertexConsumerProvider> drawer) {
drawer.accept(this.vertexConsumers);
this.vertexConsumers.draw();
}
// ----------------------------
// Shapes
// ----------------------------
@Override
public void drawRectangle(int x, int y, int width, int height, int color) {
if (color == 0)
return;
color = getColor(color);
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
int x1 = x;
int x2 = x + width;
int y1 = y;
int y2 = y + height;
if (x1 < x2) {
int tmp = x1;
x1 = x2;
x2 = tmp;
}
if (y1 < y2) {
int tmp = y1;
y1 = y2;
y2 = tmp;
}
VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(RenderLayer.getGui());
vertexConsumer.vertex(matrix4f, x1, y1, 0).color(color);
vertexConsumer.vertex(matrix4f, x1, y2, 0).color(color);
vertexConsumer.vertex(matrix4f, x2, y2, 0).color(color);
vertexConsumer.vertex(matrix4f, x2, y1, 0).color(color);
}
@Override
public void fill(int x1, int y1, int x2, int y2, int color) {
fill(x1, y1, x2, y2, 0, color);
}
public void fill(int x1, int y1, int x2, int y2, int z, int color) {
fill(RenderLayer.getGui(), x1, y1, x2, y2, z, color);
}
public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int color) {
fill(layer, x1, y1, x2, y2, 0, color);
}
public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int z, int color) {
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
if (x1 < x2) {
int tmp = x1;
x1 = x2;
x2 = tmp;
}
if (y1 < y2) {
int tmp = y1;
y1 = y2;
y2 = tmp;
}
VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(layer);
vertexConsumer.vertex(matrix4f, x1, y1, z).color(color);
vertexConsumer.vertex(matrix4f, x1, y2, z).color(color);
vertexConsumer.vertex(matrix4f, x2, y2, z).color(color);
vertexConsumer.vertex(matrix4f, x2, y1, z).color(color);
}
@Override
public void drawBorder(int x, int y, int width, int height, int color) {
fill(x, y, x + width, y + 1, color);
fill(x, y + height - 1, x + width, y + height, color);
fill(x, y + 1, x + 1, y + height - 1, color);
fill(x + width - 1, y + 1, x + width, y + height - 1, color);
}
// ----------------------------
// Text
// ----------------------------
@Override
public void drawText(String text, int x, int y, int font, int color) {
drawText(1.0f, text, x, y, font, color);
}
@Override
public void drawText(float scale, String text, int x, int y, int font, int color) {
if (color == 0)
return;
if (font == 0)
scale *= 2;
int i = 0;
for (String line : text.split("\n")) {
color = getColor(color);
matrices.push();
matrices.translate(x, y + i * Fonts.getHeight(), 0);
matrices.scale(scale, scale, 1.0f);
TextRenderer textRenderer = SaturnClient.client.textRenderer;
textRenderer.draw((Text) Fonts.setFont(line, font), 0, font == 0 ? 1 : 7, color, false,
matrices.peek().getPositionMatrix(), vertexConsumers, TextLayerType.NORMAL, 0, 15728880);
matrices.pop();
i++;
}
}
// ----------------------------
// Textures & Sprites
// ----------------------------
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height) {
drawTexture(sprite, x, y, u, v, width, height, width, height, width, height);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, int color) {
drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
int regionWidth, int regionHeight, int textureWidth, int textureHeight) {
drawTexture(sprite, x, y, u, v, width, height, regionWidth, regionHeight, textureWidth, textureHeight, -1);
}
@Override
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
int regionWidth, int regionHeight, int textureWidth, int textureHeight, int color) {
drawTexturedQuad((Identifier) (Object) sprite, x, x + width, y, y + height,
(u + 0.0f) / textureWidth, (u + regionWidth) / textureWidth,
(v + 0.0f) / textureHeight, (v + regionHeight) / textureHeight, color);
}
@Override
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height) {
drawSpriteStretched(sprite, x, y, width, height, -1);
}
@Override
public void drawSpriteStretched(SpriteRef saturnSprite, int x, int y, int width, int height, int color) {
Sprite sprite = (Sprite) saturnSprite;
if (color == 0)
return;
if (width != 0 && height != 0) {
drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height,
sprite.getMinU(), sprite.getMaxU(), sprite.getMinV(), sprite.getMaxV(), color);
}
}
private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2,
float u1, float u2, float v1, float v2, int color) {
if (sprite.toString().endsWith(".svg")) {
sprite = (Identifier) (Object) SvgTexture.getSvg((IdentifierRef) (Object) sprite, (x2 - x1) * 2,
(y2 - y1) * 2);
}
if (color == 0)
return;
x1 *= 4;
x2 *= 4;
y1 *= 4;
y2 *= 4;
color = getColor(color);
matrices.push();
matrices.scale(0.25f, 0.25f, 1.0f);
RenderSystem.setShaderTexture(0, sprite);
RenderLayer renderLayer = RenderLayer.getGuiTextured(sprite);
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(renderLayer);
vertexConsumer.vertex(matrix4f, x1, y1, 0).texture(u1, v1).color(color);
vertexConsumer.vertex(matrix4f, x1, y2, 0).texture(u1, v2).color(color);
vertexConsumer.vertex(matrix4f, x2, y2, 0).texture(u2, v2).color(color);
vertexConsumer.vertex(matrix4f, x2, y1, 0).texture(u2, v1).color(color);
matrices.pop();
}
// ----------------------------
// Items
// ----------------------------
@Override
public void drawItem(ItemStackRef item, int x, int y) {
drawItem(SaturnClient.client.player, SaturnClient.client.world, item, x, y, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y,
int seed) {
drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0);
}
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
int z) {
if (stack.isEmpty())
return;
SaturnClient.client.getItemModelManager().update(itemRenderState, stack, ModelTransformationMode.GUI,
false, world, entity, seed);
matrices.push();
matrices.translate(x + 8, y + 8, 150 + (itemRenderState.hasDepth() ? z : 0));
try {
matrices.scale(16.0f, -16.0f, 16.0f);
boolean disableLighting = !itemRenderState.isSideLit();
if (disableLighting) {
draw();
DiffuseLighting.disableGuiDepthLighting();
}
itemRenderState.render(matrices, vertexConsumers, 15728880, OverlayTexture.DEFAULT_UV);
draw();
if (disableLighting)
DiffuseLighting.enableGuiDepthLighting();
} catch (Throwable e) {
CrashReport crashReport = CrashReport.create(e, "Rendering item");
CrashReportSection section = crashReport.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(crashReport);
}
matrices.pop();
}
// ----------------------------
// Scissor / Clipping
// ----------------------------
@Override
public void enableScissor(int x1, int y1, int x2, int y2) {
ScreenRect screenRect = new ScreenRect(x1, y1, x2 - x1, y2 - y1)
.transform(matrices.peek().getPositionMatrix());
setScissor(scissorStack.push(screenRect));
}
@Override
public void disableScissor() {
setScissor(scissorStack.pop());
}
@Override
public boolean scissorContains(int x, int y) {
return scissorStack.containsPoint(x, y);
}
private void setScissor(@Nullable ScreenRect rect) {
draw();
if (rect != null) {
Window window = SaturnClient.client.getWindow();
int framebufferHeight = window.getFramebufferHeight();
double scaleFactor = window.getScaleFactor();
RenderSystem.enableScissor(
(int) (rect.getLeft() * scaleFactor),
(int) (framebufferHeight - rect.getBottom() * scaleFactor),
Math.max(0, (int) (rect.width() * scaleFactor)),
Math.max(0, (int) (rect.height() * scaleFactor)));
} else {
RenderSystem.disableScissor();
}
}
static class ScissorStack {
private final Deque<ScreenRect> stack = new ArrayDeque<>();
public ScreenRect push(ScreenRect p_281812_) {
ScreenRect screenrectangle = this.stack.peekLast();
if (screenrectangle != null) {
ScreenRect screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle),
ScreenRect.empty());
this.stack.addLast(screenrectangle1);
return screenrectangle1;
} else {
this.stack.addLast(p_281812_);
return p_281812_;
}
}
@Nullable
public ScreenRect pop() {
if (this.stack.isEmpty()) {
throw new IllegalStateException("Scissor stack underflow");
} else {
this.stack.removeLast();
return this.stack.peekLast();
}
}
public boolean containsPoint(int p_329411_, int p_333404_) {
return this.stack.isEmpty() ? true : this.stack.peek().contains(p_329411_, p_333404_);
}
}
}
@@ -1,72 +0,0 @@
package org.saturnclient.saturnclient;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.client.MinecraftClient;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.cosmetics.Emotes;
import org.saturnclient.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;
import org.saturnclient.impl.provider.RefConstructorImpl;
import org.saturnclient.impl.provider.SaturnProviderImpl;
import org.saturnclient.impl.ui.EntityDrawerImpl;
import org.saturnclient.impl.ui.SaturnScreenFabric;
import org.saturnclient.mod.ModManager;
import org.saturnclient.saturnclient.event.KeyInputHandler;
import org.saturnclient.config.Config;
import org.saturnclient.config.ConfigManager;
import org.saturnclient.ui.components.SkinPreview;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Main class for the Saturn Client mod.
*/
public class SaturnClient implements ModInitializer {
public static final String MOD_ID = "saturnclient";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
public static MinecraftClient client;
@Override
public void onInitialize() {
LOGGER.info("Initializing " + MOD_ID);
client = MinecraftClient.getInstance();
Providers.saturn = new SaturnProviderImpl();
Providers.refConstructor = new RefConstructorImpl();
Providers.GLFW = new GLFWProviderImpl();
Providers.feature = new FeatureProviderFabric(client);
SkinPreview.DRAWER = new EntityDrawerImpl();
Config.init();
ModManager.init();
client.execute(() -> {
SaturnScreenFabric.preload(client);
});
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> ConfigManager.save());
ClientTickEvents.END_CLIENT_TICK.register(client -> {
for (org.saturnclient.mod.Mod m : ModManager.ENABLED_MODS) {
m.tick();
}
});
KeyInputHandler.register();
Emotes.initialize();
if (ServiceClient.authenticate()) {
Cloaks.initialize();
Hats.initialize();
LOGGER.info(MOD_ID + " initialization complete");
} else {
LOGGER.error("Failed to authenticate with the server");
}
}
}

Some files were not shown because too many files have changed in this diff Show More